【C++ Primer Plus】类、运算符重载、虚函数、友元函数模板
1.运算符重载
1.1 普通运算符重载
在类内重写operator+函数,实现加号运算符的重载,下面给出了两种调用方式,注意加号前为调用者,加号后为参数,第三行代码的完整写法实际上是第四行
Time Time::operator+(int minutes)const;
Time time;
Time time2 = time+50;
Time time3 = time.operator+(50);
1.2 运用友元实现运算符重载
上述运算符重载存在一个问题,50 + time 是无效的,因为50没有对应的加法运算符重载,我们可以使用友元解决
- 虽然 operator+() 函数在类内声明,但它并不是成员函数
- 虽然 operator+() 不是成员函数,但它与成员函数访问权限相同
// 类内声明友元函数
friend Time operator+(int minutes, const Time& t);
// 类外实现函数
Time operator+(int minutes, const Time& t);
// 调用
Time time2 = 50 + time;
Time time3 = operator+(50, time);
1.3 其他运算符重载
同样使用友元实现左移运算符的重载
// 类内声明友元函数
friend std::ostream& operator<<(std::ostream& os, const Time& time);
// 类外实现函数
std::ostream& operator<<(std::ostream& os, const Time& time){
cout << time.hours << " hours, " << time.minutes << " minutes" << endl;
return os;
}
// 调用
Time time;
cout<<time;
1.4 示例代码
// main.cpp
#include "time.h"
#include <iostream>
int main()
{
Time time1, time2;
time1.setHours(1); time1.setMinutes(30);
time2.setHours(5); time2.setMinutes(55);
// ----------- 普通的运算符重载 ------------
Time time3 = time1 + time2; // 这里 + 号前的是函数调用者
Time time4 = time1.operator+(time2); // 上面的函数等同于下面
// ----------- 这类运算符重载会有调用顺序的问题, 可以用友元解决 --------------
Time time5 = time1 + 50; // 这里就会出现一个问题, 只能把time1放在前面, 50放在后面
Time time6 = 50 + time1; // 这个实际上也是运算符重载, 但利用了友元, 具体调用是下面的方式
Time time7 = operator+(50, time1); // 这里是上面等效的样式
std::cout << time1 << time2 << time3 << time4 << time5 << time6 << time7;
} // time.h
#pragma once
#include <ostream>
class Time
{
private:
int hours;
int minutes;
public:
void setHours(int hours) { this->hours = hours; }
void setMinutes(int minutes) { this->minutes = minutes; }
Time operator+(const Time& t) const;
Time operator+(int minutes) const;
friend Time operator+(int minutes, const Time& t);
friend std::ostream& operator<<(std::ostream& os, const Time& time);
}; // time.cpp
#include "time.h"
#include <iostream>
using namespace std; Time Time::operator+(const Time& t)const
{
Time sum;
sum.minutes = minutes + t.minutes;
sum.hours = hours + t.hours + sum.minutes / 60;
sum.minutes %= 60;
return sum;
} Time Time::operator+(int minutes)const
{
Time sum;
sum.minutes = this->minutes + minutes;
sum.hours = hours + sum.minutes / 60;
sum.minutes %= 60;
return sum;
} std::ostream& operator<<(std::ostream& os, const Time& time)
{
cout << time.hours << " hours, " << time.minutes << " minutes" << endl;
return os;
} Time operator+(int minutes, const Time& t)
{
Time sum;
sum.minutes = t.minutes + minutes;
sum.hours = t.hours + sum.minutes / 60;
sum.minutes %= 60;
return sum;
}
2. 类和动态内存分配
2.1 静态成员变量初始化
- 不能在类声明中初始化静态成员变量,因为声明只描述如何分配内存,但不分配内存,我们通过这种格式创建对象,从而分配和初始化内存
- 静态变量先于对象创建,而对象创建时才去设置这个变量是不合适的
- 使用const修饰的整数或枚举可以在类的声明中初始化
// ----- student.h -----
class student{
public:
char* name;
static int numofstu;
const static int maxname = 5; // 如果静态类型是整形或枚举型 const 可以在类内初始化
};
// ----- student.cpp -----
#include "student.h"
int student::numofstu = 0; // 静态变量初始化必须在类外
2.2 类的初始化
- 初始化顺序:构造函数中多个项目被初始化的顺序是他们在类中声明的顺序,而不是在初始化列表中的顺序
class Student{
private:
char* name;
double scores;
public:
Student(double scores, const char* str)
: scores(score), name(str){} // 仍然是name首先被初始化,因为它首先被声明
}
- 使用 explicit 防止构造函数的隐式转换,下图给出了如果显示给出 explicit 关键字编译器会报出的错误
#include <iostream>
class Square {
public:
int x;
int y;
Square(int x = 0, int y = 0) :x(x), y(y) {}
}; void printSquare(const Square& s) {
std::cout << "Square x: " << s.x << " ," << s.y << std::endl;
}
int main(){
Square s = 1; // 隐式调用构造函数
printSquare(2); // 这里会隐式调用构造函数, 创建一个Square对象传入, 可能引起一些错误
}

2.3 特殊成员函数
设计类的过程中如果不正确处理c++自动提供的这些函数可能造成很多问题,必须正确处理
- 默认构造函数
- 默认构造函数不接受任何参数,也不执行任何操作,没有任何数据的初始化等行为
- 默认析构函数
- 默认不做任何处理
- 注意:构造函数可以存在多个,但析构函数只有一个。所以构造函数中 new 或 new[] 必须统一,析构函数只能是new 或 new[]
- 复制构造函数
- 复制构造函数是新建一个对象并将其初始化为同类现有对象时调用的(实际上只要是按值传递都会调用复制构造函数,包括函数传参,返回值)
- 默认复制构造函数会逐个复制非静态成员(浅复制),对于指针可能造成重复回收,或者另一个对象销毁后本对象指向一个空值等各种问题(只要包括new,就必须重写)
- 以下声明都会调用复制构造函数
StringBad ditto(mitto); // StringBad(const StringBad &);
StringBad metoo = motto; // StringBad(Const StringBad &);
StringBad also = StringBad(motoo) // StringBad(Const StringBad &);
StringBad * pStringBad = new StringBad(motto); // StringBad(Const StringBad &);
- 赋值运算符
- 默认赋值运算符也会存在浅复制的问题,同样应该重写
- 以下声明会调用复制运算符
StringBad ditto; // 首先声明一个变量(调用无参构造函数)
ditto = mitto; // 调用赋值运算符
- 地址运算符
- 一般情况下都没啥问题,特殊情况下可以给取地址运算符返回nullptr避免其他人获取地址
2.4 虚函数
2.4.1 virtual 函数调用
- 如果不使用 virtual 标记,函数不是虚函数,程序只根据引用类型或指针类型调用方法(根据引用者调用方法)
- 如果使用 virtual 标记,函数是虚函数,程序将根据实际指向的对象类型来选择方法(根据实际对象调用方法)
基类用virtual标记的函数,派生类不论是否标记都是虚的,但应该标记
// (Banana继承自Fruit,假设他们都有输出方法 View();)
// 假设没有虚函数关键字, 程序将简单的根据指针类型去选择方法
Fruit fruit;
Banana banana;
Fruit & f1 = fruit; // 根据 Fruit 类型直接调用 Fruit::View()
Fruit & f2 = banana; // 根据 Fruit 类型直接调用 Fruit::View()
f1.view(); f2.view();
// 假设有虚函数关键字,程序将根据实际引用的对象选择调用的方法
Fruit fruit;
Banana banana;
Fruit & f1 = fruit; // 根据 fruit 类型选择调用 Fruit::View()
Fruit & f2 = banana; // 根据 banana 类型选择调用 Banana::View()
f1.view(); f2.view();
注意:如果重新定义一个同名的虚函数,不会生成函数的重载版本,子类会隐藏基类的函数版本
- 所以如果基类的声明被重载了,子类想重新定义一个重载版本,必须重新定义所有重载版本,否则其他的重载将被隐藏!
- 特殊情况:如果返回值是基类的引用或指针,则子类可以修改为子类的引用和指针,这不会导致隐藏(这种特性是返回类型协变)
// 如果子类重载了方法,父类方法将被隐藏,所以如果子类重载,必须完整实现父类所有的重载方法
class Fruit{
public:
virtual void show();
}
class Banana{
public:
virtual void show(int a);
}
Banana banana;
banana.show(5); // 合法调用
banana.show(); // 非法调用(隐藏同名的基类方法) Fruit b2 = banana;
b2.show(); // 合法调用,这样才可以调用隐藏的方法
// 对于返回值为基类指针,子类重写返回值指针的情况
// 不论引用者是父类还是子类,都调用对象自己的方法,这种情况就是普通的 virtual 标记的函数
class Fruit{
public:
virtual Fruit* build();
}
class Banana{
public:
virtual Banana* build();
}
Banana banana;
Fruit b2= banana;
banana.show(); // 调用 Banana::build()
b2.show(); // 调用Banana::build()
2.4.2 函数传值
函数传参过程中,引用传递和指针传递都会将对象完整传递过去,而值传递可能只将部分对象传递到函数内
void fr(Fruit & rf); // rf.View();
void fp(Fruit * pf); // pf->View();
void fv(Fruit f); // f.View();
int main(){
Banana banana;
fr(banana); // Banana::View(); 隐式进行向上转换
fp(banana); // Banana::View(); 隐式进行向上转换
fv(banana); // Fruit:View(); 值传递,只将Fruit部分传递给函数fv
}
2.4.3 虚析构函数
其实这里的原理与普通的 virtual 关键字声明的函数相同,单独哪出来是为了强调和提醒只要做基类的析构函数都应该是虚函数。
如果不用 virtual 关键字声明析构函数,则只会调用指针类型指向的析构函数。例如Fruit* 指向一个Banana,但它只会调用Fruit的析构函数,导致内存泄漏。但如果析构函数是虚函数,将调用相应对象的析构函数,然后自动调用基类的析构函数,这样才可以完整释放该类占用的内存。
2.4.4 复制构造函数
子类的赋值构造函数,在子类有new分配内存的时候也需要重写,但他必须调用基类的复制构造函数来处理基类的数据
hasDMA::hasDMA(const hasDMA & hs) : baseDMA(hs);
2.4.5 赋值运算符
子类存在 new 动态分配内存的时候,需要重写赋值运算符,但它作为子类的方法,只能访问子类的数据,但派生类必须处理父类的数据来对父类进行赋值
hasDMA & hadDMA::operator=(const hasDMA & hs){
if(this == &hs)
return *this
baseDMA::operator=(hs); // 显示调用父类的赋值运算符来给父类赋值
/* ... */
}
2.4.6 静态联编和动态联编
将源代码中的函数调用解释为指定特定的代码块被称为函数名联编
- 静态联编:普通的函数以及函数重载都可以在编译过程中完成这种联编(static binding、early binding)
- 动态联编:编译器必须生成能够在运行时选择正确的虚函数的代码(dynamic binding、late binding)
- 动态联编通过虚函数表实现,每个类中都有一个隐藏的指针成员vptr,它指向自己这个类的虚函数表
- 在调用函数的时候,不管指针对象是什么,都通过该对象对应的vptr指针来调用它对应的函数

2.5 继承
- 使用私有继承时,只能在派生类的方法中使用基类的方法(第三代基类将不能再直接调用)
- 使用保护继承时,基类的公有成员和保护乘员都将成为派生类的保护乘员(后代仍然可以调用)
- 使用公有继承时,还是public
2.5.1 多重继承
- 多重继承从不同的基类中继承同名方法
- 多重继承从不同的基类中继承同一个类的多个实例

class Worker{};
class Waiter : public Worker{};
class Singer : public Worker{};
class SingingWaiter : public Waiter, public Singer{};
SingingWaiter ed;
Worker * pw = &ed; //存在错误
通常情况下,这种赋值把基类指针设置为派生对象中的基类对象的地址。但是 ed 中包含两个 Worker 对象,有两个地址可以选择,这会产生问题
Worker * pw1 = (Waiter *) &ed; // Waiter 中的 Worker 对象
Worker * pw2 = (Singer *) &ed; // Singer 中的 Worker 对象
同样如果想调用基类中同名的函数也应该显示的声明
ed.Waiter::View();
ed.Singer::View();
// 更好的做法是重新定义View方法或者指明使用哪个版本的View
void SingingWaiter::View(){
Singer::View();
}

- 虚基类:虚基类可以让多个基类相同的类,派生出的对象只继承一个基类对象(实际上引入了一种新的规则)
class Singer : virtual public Worker {};
class Waiter : virtual public Worker {};
class SingingWaiter : public Singer, public Waiter {};
// 注意, Waiter(wk, p)和Singer(wk, v) 这两条路径是不会将 wk 传给基类的, 因为两条传输路径存在冲突, 所以会调用基类的默认构造函数
SingingWaiter(const Worker & wk, int p=0, int v = Singer::other)
: Waiter(wk, p), Singer(wk, v) {}
// 只有显示调用基类的构造函数才可以, 这里是调用复制构造函数的例子
SingingWaiter(const Worker & wk, int p=0, int v = Singer::other)
: Worker(wk), Waiter(wk, p), Singer(wk, v) {}

2.6 模板
2.6.1 模板示例和非类型参数
class 指出 T 为类型参数,而后面的 int 指出 n 的类型为 int,这种参数是非类型参数或表达式参数
template<class T, int n>
class Array {
private:
T data[n];
}; int main(){
Array<int, 100> m; // 表达式参数可以是整形、枚举、引用或指针
}
2.6.2 将模板用作参数
鄙人有些菜,实际使用中一般都是直接传入一个 queue<int> 而不是写下面这种我感觉很复杂的方式
// T2 的模板参数类型是 template<class T> class
template <template <class T> class T2>
class A{
T2<int> t;
};
template <class T>
class queue{
T data;
};
int main(){
A<queue> a; // 相当于把用queue<int> 替换了T2<int>
}
2.6.3 友元函数
如果有一个友元函数 View,它的参数是类本身,会存在一个问题,类具体化和友元函数之间没有对应的关系,没有办法直接调用友元函数
friend void View(HasFriend<T> &); // 不可以这样声明,因为不存在HasFriend这样的对象,只有特定的具体化对象HasFriend<int>等
- 非模板友元:这种友元函数最简单,直接在声明时写清楚类型
template<class T>
class HasFriend {
public:
T data;
friend void View(HasFriend<T>& hf);
};
void View(HasFriend<int>& hf) {
cout << "模板类的非模板的友元函数,int 类型" << endl;
}
void View(HasFriend<char>& hf) {
cout << "模板类的非模板的友元函数,char 类型" << endl;
}
int main(){
HasFriend<int> hf;
hf.data = 10; // 因为HasFriend具体化为FasFriend<int>类型,所以它会调用对应的函数
View(hf);
}
- 约束(bound)模板友元
- 声明模板原型 -> 类内声明友元函数 -> 类外实现友元函数
- 通过给定模板类的 T 类型变量,编译器推断对应的友元函数形式
- 模板原型只是一个形式,友元函数实现中”长得接近即可“,参见 View() 的第三个重载
// main.cpp
#include "HasFriendT.hpp" int main()
{
HasFriendT<char> hf;
hf.data = 'x';
View<char>(); // 这里没有办法自动类型推断,需要显示的写
View(hf); // 这里存在自动类型推断, 与后面的写法效果相同
View('c', hf);
View<char>(hf);
} // HasFriendT.hpp
#pragma once
#include<iostream>
using namespace std;
// *第一步* 声明模板原型
template<class T> void View();
template<class T> void View(const T& hf);
template<class T1, class T2> void View(T1 t1, T2& t2); template<class T>
class HasFriendT {
public:
T data;
// *第二步* 在模板类中声明友元
friend void View<T>();
friend void View<HasFriendT<T>>(const HasFriendT<T>& hf);
friend void View<T, HasFriendT<T>>(T data, HasFriendT<T>& hf);
}; // *第三步* 根据原型实现模板函数
template<class T>
void View() {
cout << "sizeof(T): " << sizeof(T) << endl;
} template<class T>
void View(const HasFriendT<T>& hf) {
cout << hf.data << endl;
} template<class T>
void View(T data, HasFriendT<T>& hf) {
hf.data = data;
}
- 非约束(unbound)模板友元
- 只需要在类内声明一个友元函数,类外去实现,自由的过分
// main.cpp
#include "HasFriendT.hpp"
int main(){
HasFriendT<char> hf1;
HasFriendT<int> hf2;
View(hf1, hf2);
int a = 1;
int b = 2;
View(a, b);
} // HasFriendT.cpp
#pragma once
#include<iostream>
using namespace std;
template<class T>
class HasFriendT {
public:
T data;
template<class T1, class T2> friend void View(T1& t1, T2& t2);
}; template<class T1, class T2>
void View(T1& t1, T2& t2) {
cout << "友元非约束方式, 这个真是太自由了" << endl;
}
【C++ Primer Plus】类、运算符重载、虚函数、友元函数模板的更多相关文章
- 自绘CListCtrl类,重载虚函数DrawItem
//自绘CListCtrl类,重载虚函数DrawItem void CNewListCtrl::DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct) { // TOD ...
- 用C++设计一个不能被继承的类(用私有构造函数+友元函数)
题目:用C++设计一个不能被继承的类. 分析:这是Adobe公司2007年校园招聘的最新笔试题.这道题除了考察应聘者的C++基本功底外,还能考察反应能力,是一道很好的题目. 在Java中定义了关键字f ...
- C++ 类 & 对象-类成员函数-类访问修饰符-C++ 友元函数-构造函数 & 析构函数-C++ 拷贝构造函数
C++ 类成员函数 成员函数可以定义在类定义内部,或者单独使用范围解析运算符 :: 来定义. 需要强调一点,在 :: 运算符之前必须使用类名.调用成员函数是在对象上使用点运算符(.),这样它就能操作与 ...
- c++类运算符重载遇到的函数形参问题
class A { public: A(int arg1, int arg2); ~A(); A &operator = ( A &other); A operator + ( A & ...
- C++ Primer笔记10_运算符重载_赋值运算符_进入/输出操作符
1.颂值运营商 首先来福值运算符引入后面要说的运算符重载.上一节说了构造函数.拷贝构造函数:一个类要想进行更好的控制.须要定义自己的构造函数.拷贝构造函数.析构函数.当然,还有赋值运算符.常说的三大函 ...
- C++ Primer笔记13_运算符重载_总结
总结: 1.不能重载的运算符: . 和 .* 和 ?: 和 :: 和 sizeof 和 typeid 2.重载运算符有两种基本选择: 类的成员函数或者友元函数, 建议规则例如以下: 运算符 建议使用 ...
- C++ Primer笔记12_运算符重载_递增递减运算符_成员訪问运算符
1.递增递减运算符 C++语言并不要求递增递减运算符必须是类的成员.可是由于他们改变的正好是所操作对象的状态.所以建议设定为成员函数. 对于递增与递减运算符来说,有前置与后置两个版本号,因此.我们应该 ...
- String类运算符重载,自己实现
body, table{font-family: 微软雅黑; font-size: 10pt} table{border-collapse: collapse; border: solid gray; ...
- C++:成员运算符重载函数和友元运算符重载函数的比较
5.2.4 成员运算符重载函数和友元运算符重载函数的比较 (1)对双目运算符而言,成员运算符重载函数参数表中含有一个参数,而友元运算符重载函数参数表中有两个参数:对于单目运算符而言,成员运算符重载函数 ...
- C++之友元机制(友元函数和友元类)
一.为什么引入友元机制? 总的来说就是为了让非成员函数即普通函数或其他类可以访问类的私有成员,这确实破坏了类的封装性和数据的隐蔽性,但为什么要这么做呢? (c++ primer:尽管友元被授予从外部访 ...
随机推荐
- 【多线程】Java多线程与并发编程全解析
Java多线程与并发编程全解析 多线程编程是Java中最具挑战性的部分之一,它能够显著提升应用程序的性能和响应能力.本文将全面解析Java多线程与并发编程的核心概念.线程安全机制以及JUC工具类的使用 ...
- 揭秘!测试开发速看,Mockaroo 如何轻松解决 90% 测试数据难题!
在软件测试领域,模拟生成测试数据一直是至关重要的环节.无论是验证系统功能的准确性,还是测试边界条件下的系统稳定性,都离不开丰富且真实的测试数据. 今天,向大家推荐一款强大的模拟生成测试数据工具 --M ...
- 第9.3讲、Tiny Transformer: 极简版Transformer
简介 极简版的 Transformer 编码器-解码器(Seq2Seq)结构,适合用于学习.实验和小型序列到序列(如翻译.摘要)任务. 该实现包含了位置编码.多层编码器.多层解码器.训练与推理流程,代 ...
- MySQL Explain查看执行计划详解
目录 前言 EXPLAIN 中的列 id 和select_type table type possible_keys key 和 key_len ref 和 rows Extra 小结 Referen ...
- 「Log」2023.9.25 小记
序幕 \(\text{6:40}\):准时到校,整理博客,今天少来点嘻嘻哈哈,认真做题了. \(\text{6:55}\):整理一下 POI 2011 题单. \(\text{7:10}\):开始板刷 ...
- python基础—集合
一.集合(数字,字符串,元组) 1.定义 由不同元素组成的集合,集合中是一组无序排列的哈希值,可以作为字典的key 2.特性 无序,不同元素组成,必须是不可变类型 3.set输出与去重 s=set(' ...
- javacv添加字幕 剧中显示
介绍 javacv目前不能像ffmpeg那样 直接加载字体文件到视频 参考这里 所以实现流程为:提取帧 -> 转图片 -> 编辑图片增加文字 -> 转回帧 -> 输出视频 上代 ...
- MongoDB入门实战教程(7)
本系列教程目录: MongoDB入门实战教程(1) MongoDB入门实战教程(2) MongoDB入门实战教程(3) MongoDB入门实战教程(4) MongoDB入门实战教程(5) MongoD ...
- 新版Excel 用“#”引用函数溢出的范围
新版本EXCEL函数支持溢出,那么另一个函数如果要引用前一个函数溢出的范围,可以使用"A2#"表示(A2为前一个有溢出结果函数所在的单元格,假设溢出后范围是A2:A6),这个&qu ...
- Set 的各个实现类的性能分析
HashSet 和TreeSet是Set的典型实现.HashSet 比TreeSet性能好,TreeSet需要额外通过红黑树算法维护集合 的顺序.除了需要维护集合的顺序外,其他的都优先用HashSet ...