仅做个人学习《C++ Primer》的一点儿记录。
13. 拷贝控制
- 三个控制类的拷贝操作的基本操作:
拷贝构造函数、拷贝赋值运算符、析构函数
- 拷贝构造函数的第一个参数必须是引用类型,几乎总是const引用;在很多情况下会被隐式调用,所以不能采用explict关键字;
- 拷贝函数发生得情况:1. 用等于号定义变量时;2. 讲一个对象作为实参传递给非引用的形参;3. 从一个返回类型为非引用类型的函数返回一个对象;4. 用花括号列表初始化一个数组中的元素;例如:标准库容器的
insert/ push
成员。但emplace
是初始化,不会调用 - 为什么拷贝构造函数的形参需要是const引用?因为形参如果不是const引用,必须需要拷贝实参进行传入,而拷贝函数还没有定义;所以一般都是const引用,避免拷贝构造;
- 通常赋值运算符返回一个指向其左侧运算对象的引用;另外,标准库通常要求保存在容器中的类型具有赋值运算符,且返回左侧对象的引用。
析构函数
- 析构函数没有返回值且不接受参数,用
~
+类名构成; - 内置类型没有析构函数,不需要处理内置类型的成员;智能指针成员在析构阶段自动被销毁;
- 类内成员是在析构函数体执行之后被销毁的;
基本原则
- 需要析构函数的类也需要拷贝和赋值操作:需要析构的往往定义了动态内存指针等,在拷贝时如果没有显示定义,则合成的函数只是简单的拷贝指针成员,指向了同一块内存,这样在一个被销毁时另一个会出现错误;
- 需要拷贝操作的类,也需要赋值操作;反之亦然。
其他
=default
关键词,在默认构造函数,或拷贝控制成员后,加上=default,来显示地要求编译器自动生成合成的函数。如果希望是内联的,则在类内;如果不希望内联,则在类外定义时加;=delete
定义“删除的函数”,禁止某种形式的拷贝构造函数或拷贝赋值运算,不能以任何形式进行调用;- 赋值运算符一定要验证保证能够对自身对象赋值!可以采用销毁左侧对象前,首先拷贝右侧对象;
- 右值引用
&&
:绑定到一个将要销毁的对象上,例如临时变量。很多情况下临时变量赋值给其他变量后会进行销毁,不如直接转移临时变量的控制权给相应的变量,从而“右值引用”。实现了函数的完美转发。 - 可以采用
std::move()
函数,获得绑定到左值上的右值引用
// 拷贝构造函数
class Foo{
Foo();
Foo(const Foo&);
};
string s1("123"); // 直接初始化
string s2(s1); // 直接初始化
string s3 = "123"; // 拷贝初始化
---
// 赋值运算符
Foo& operator=(const Foo&);
---
struct NoCopy{
NoCopy() = default; // 用合成的默认构造函数
NoCopy(const NoCopy&) = delete; // 禁止拷贝
~NoCopy() = default; // 是用合成的析构函数
};
---
// 自赋值运算的一个错误:
HasPtr& HasPtr::operator=(const HasPtr &rhs){
delete ps; // ps 是HasPtr中的成员变量 string *ps;
ps = new string(*(rhs.ps)); // 发生错误:因为已经删掉了this.ps,而this.ps就是rhs.ps
}
---
int i = 5;
int &r0 = 5; // 错误,5是右值,&左值引用不能绑定到右值;
int &r1 = i; // 正确,r1引用了i,i是左值
int &r2 = i*2; // 错误,i*2是右值
const int &r3 = i*42; // 正确,const可以绑定到右值(因为const创建了一个对象,再进行绑定)
int &&rr = i*42; // 正确,右值引用,rr绑定到乘法结果上
14. 重载运算与类型转换
- 成员函数通过一个名为
this
的额外的隐式参数,访问调用他的对象 - 运算符函数,必须要求是类的成员,或者至少含有一个类类型的参数;不能是两个内置类型的运算对象
- 只能重载已有的运算符,不能自己创造;并尽可能保证重载的含义与原有的相同
- 作为成员函数,还是非成员?建议:赋值、下标、调用、访问箭头(
=/ []/ ()/ ->
)采用成员函数;改变对象状态(自增自减、解引用)等成员函数;而具有对称性的,用作非成员函数,例如:算数、相等性、关系、位运算符等; - 重载输出运算符
<<
,打印输出结果,一般返回他的 ostream 形参;第一个形参是非常量ostream
对象的引用,第二个一般是常量的引用(避免修改); - 重载输入运算符
>>
,第一个形参是istream
的引用,第二个是读入到的(非常量)对象的引用;重载输入运算符应检查输入的有效性; - 重载相等运算符
==
:如果类需要比较是否相等,优先采用重载而不是函数形式(避免记忆),相等运算符应具有传递性;一般也会同时定义!=
运算符,但通常将一个运算委托给另一个,保证一致性; - 下表运算符
[]
:建议返回所访问元素的引用,这样方便下标索引能够出现在等好的两侧都有意义,与原始的[]运算一致; - 增减运算符重载:前置运算符,应返回递增递减后对象的引用;后置运算符,应返回对象的原值,而非引用;为进行区分,后置运算符在声明时加一个int
- 重载成员访问运算符:
*/ ->
,箭头运算符必须要求是类的成员,而且一定要具有获取成员这一功能;解引用运算符*大多定义为类的成员。 - 函数调用运算符
()
:可以向调用函数一样,调用该类的对象; - 类型转化运算符
operator type() const
:将类转成指定的成员函数;但应避免误导性,有些转化是没有意义的。为避免隐式的转化产生错误,可以增加explict
关键字;
// 左侧参数绑定到成员变量的 this
total.isbn(); //
Sales_Data::isbn(&total); // 伪代码,用于解释说明
---
// 重载的运算符函数等价调用
data1 + data2; // 非成员函数,普通的表达式
operator+(data1, data2); // 等价表达式
data1 += data2; // 成员函数,基于“调用”的表达式
data1.operator+=(data2); // 成员函数运算符的等价调用
---
string s = "world";
string t = s + "!"; // 正确,把一个const char*加到string对象
string u = "hi," + s; // 错误,const char*不具有成员函数+
---
// 重载输出运算符
ostream &operator<<(ostream &os, const Sales_Data &item){
os << item.details;
return os;
}
// 注意尽量避免格式化操作,以使用者能够自行控制
---
Class Student{
Student& operator++(); // 前置版本,返回自增后的引用
Student operator++(int); // 后置版本,多一个int,返回原值
};
Stduent s;
s.operator++(0); // 调用后置版本的自增
s.operator++(); //
---
class Printer{
void operator()(const string &s) const {cout << s << endl;}
};
Printer pr;
pr("hello");
---
class SmallInt{
SmallInt(int i) : val(i) {}
operator int() const {return val;}
size_t val;
};
SmallInt si;
si + 3; // 正确,si隐式转化为了SmallInt,然后执行了加法操作;
15. 面向对象程序设计
- 面向对象程序设计的核心思想是:数据抽象、继承、动态绑定;
- C++中,基类将类型相关的函数,与派生类不做改变直接继承的函数,区分对待。对于某些函数,希望派生类进行定义,需要声明称虚函数
virtual
,加载函数声明前;派生类内部必须重新定义所有的虚函数,可以在后面加上override
显示地注明;virtual
只能在类内声明,而不能出现在类外的定义 - “动态绑定”又称为“运行时绑定”,即函数的运行版本由实参决定。我们使用基类的引用或指针,调用一个虚函数时,将发生动态绑定;
- 基类通常需要定义一个虚析构函数,即使该函数不执行任何操作
protected
访问运算符,表示派生类可以访问,但其他用户不能访问的类成员- 派生类必须使用基类的构造函数来初始化继承于基类的成员变量;派生类首先初始化基类的变量,然后按照声明顺序初始化特有的成员变量;
- 继承静态成员:静态成员是惟一的,只有不是private时才能够被访问
- 使用
final
在类的声明后面,避免这个类被后续继承 - 某个类继承某个基类时,必须要求基类已经声明并定义,因为派生类需要知道继承的成员是什么;
- 静态类型/动态类型:静态类型是编译时就可以知道的类型,而动态类型是程序运行时才可知的;
- 派生类型可以向基类进行转化,但不存在基类向派生类型的转化(否则派生类中可以访问的成员变量在基类中找不到)。派生类向基类进行初始化、拷贝时,基类中不存在的派生类的成员会被忽略掉;
- 基类的指针或引用,的静态类型可能与运行时绑定的动态类型不一致。
虚函数
-
当使用基类的引用或指针调用一个虚函数成员函数时,会发生动态绑定,此时编译器在编译时也不知道调用了那个虚函数,所以我们需要将所有派生类中的虚函数都进行定义;运行时才能根据动态绑定自动选择合适的函数进行执行;
-
动态绑定,但只有当我们采用指针或者引用方式,调用虚函数时,才会发生!如果使用的是普通类型的表达式进行调用,则编译时就能确定版本;
-
多态性:是OOP的核心思想,表示我们能够使用继承关系的多个类型,而不用考虑他们的差异;引用或静态指针的静态类型与动态类型的不同,是C++支持多态性的根本所在;
-
派生类中的虚函数,可以使用virtual关键字也可以省略,因为虚函数在所有派生类中都是虚函数;如果派生类覆盖了某个虚函数,则要求形参和返回值必须与基类的虚函数相同;
-
override
:显式说明我们要覆盖某个虚函数。如果派生类中的虚函数,形参或返回值与基类的不一致,编译器会认为是重新声明了一个虚函数而不是覆盖;采用override关键字则告诉编译器是进行覆盖,避免不必要的错误; -
final
:如果我们把某个函数指定为final,则之后任何函数不能对这个函数进行覆盖,否则会发生编译错误;final/ override
出现在函数的形参列表以及const或引用修饰符之后。 -
虚函数调用的回避机制:有时需要调用虚函数时不进行动态绑定,此时使用作用域运算符进行实现(见下面代码)
-
纯虚函数与抽象基类:在虚函数体的后面书写
=0
定义成一个纯虚函数,一般纯虚函数不需要定义;含有纯虚函数的类称为“抽象基类”;不能定义一个抽象基类的对象;抽象基类可以被继承,继承时如果重写了纯虚函数则可以创建对象,否则仍旧是抽象基类;抽象基类定义了接口。
访问与继承
protected
声明希望与派生类分享,但不想被其他公共访问使用的成员;- 派生访问说明符,用于控制派生类用户对基类成员的访问权限。例如用
private
继承而来的,无论基类中的成员是public还是protected的,在派生类中均变为private的,不能被外部访问。 - 但可以通过
using
方式改变成员的可访问性,using改变后的权限为using语句之前的访问说明符决定;派生类只能为它能够访问的名字提供using声明 - 用
class
关键字定义的继承,是私有继承的;struct
定义的继承,是公有继承的; - 派生类的作用域嵌套在基类的作用域之内,当在派生类的作用域之内找不到名字时,才回到基类中去寻找;派生类中可以定义与基类一样的名字,此时会隐藏基类作用域中的名字,但可以通过作用域运算符来访问被隐藏的基类成员。建议不要使用相同的名字
- 派生类中的函数,不会重载基类中的函数,哪怕参数列表不一致,也会隐去基类的函数;这就是为什么虚函数覆盖时,一定要保证参数列表相同,以及override检查;
其他
- 由于指向基类的指针可能动态绑定到了派生类,此时析构时需要让系统知道应该执行派生类的析构函数;由此需要将基类的析构函数定义成虚析构函数。只要基类的析构函数是虚函数,就能确保delete基类的指针时调用了正确的析构函数版本;
- 派生类执行析构函数时,基类的析构函数自动被调用,所以派生类虚构函数只需要处理派生类自己的成员;
- 容器中不能直接存储具有继承关系的多种类型的对象;一般采用指向基类的(智能)指针进行存储,此时指针可以指向基类也可以指向派生类;
// 派生类向基类的转化
Quote item;
Bulk_Quote bulk;
Quote *p = &item; // p指向Quote对象
p = &bulk; // p指向Bulk_Quote的Quote部分
Quote &r = bulk; // r绑定到bulk的Quote部分
---
// 静态类型、动态类型与动态绑定
double print_total(ostream &os, const Quote &q){
os << "Cout: " << q.price() <<endl;
}
print_total(cout, item); // 调用基类的price()函数
print_total(cout, bulk); // 调用派生类price()函数
// 可以看出,函数内部的 q 的静态类型是Quote,但动态类型可以是Bulk_Quote
item = bulk; // 将Quote部分拷贝到item
item.price(20); // 调用的是 Quote 中的price,不存在动态绑定(静态类型以确定)
---
Bulk_Quote bulk;
Quote *itemP = &bulk; // 正确,动态类型是Bulk_Quote
Bulk_Quote *bulkP = itemP; // 错误,不能将基类转化为派生类(编译器无法确定动态类型)
---
double un = baseP->Quote::price(20); // 强制调用基类的版本,而不管动态绑定;
---
class Base{
Base():mem(0){}
int mem;
};
class Derived:Base{
Derived(int i):mem(i){}
int get_mem() {return mem;}
int get_mem2() {return Base::mem;}
int mem;
};
Derived d(42);
d.get_mem(); // 返回的是42;
d.get_mem2(); // 返回的是Base::mem,0
16. 模板与泛型编程
- 函数模板可以理解成一个公式,生成针对特定类型的函数版本;
template
后面跟“模板参数列表”,具有一个或多个模板参数(用逗号分割),用<>
包围起来;吗,模板参数列表不能为空- 调用一个函数模板时,编译器用函数实参推断模板实参,将实参的类型绑定到模板参数T——实例化
- 模板中的函数参数建议使用const引用,避免拷贝操作;
- 模板只有在实例化时,才会进行编译,所以模板的定义应放到头文件中,保证实例化时能够找到定义。函数模板、类模板成员函数,定义放在头文件。
- 需要由模板使用者保证,实例化时的对象,能够调用模板内部的运算符号;
- 类模板:用来生成类,在实例化时需要提供类得显示模板实参进行绑定;不同的实例形成独立的类。
- 类模板成员函数,只有在使用的时候才能够实例化,不使用时不会实例化。这使得某些类型不符合类模板成员函数,也能实例化成类。例如vector标准容器中,可以用string/vector等不同的类型,不符合的成员函数不使用也不会有问题;
- 模板参数会隐藏外层作用域中的相同名字,且在模板内模板参数名不能重复、重用
typename
关键字:显式告诉编译器该名字是一个类型,否则编译器无法判断是成员变量还是类型;默认理解为访问的是成员变量,而不是类型;- 控制实例化:当模板和类模板被实例化时,才进行编译,这样可能发生:不同源文件生成了多个相同类型的实例化,占用空间。为此可以显式实例化避免开销,采用
extern
关键字。类模板的显式实例化会实例化所有的模板函数,因此需要保证所有类型均能够用于模板的所有成员函数; - 由于模板函数在调用的时候,编译器根据实参类型判断模板类型,这时候采用特殊的初始化规则,不会发生类型的自动转换;
- 有时编译器无法判断某种类型,例如返回类型与传入参数类型是不同的模板,此时需要用户进行显式指定;见下方代码;
template <typename T>
int compare(const T &v1, const T &v2){;} //
---
typedef double AA;
template <typename AA, typename B> void f(AA a, B b){
AA tmp = a; // tmp 此时是绑定的AA类型,而不是double,因为屏蔽了外层
double b; // 错误:不能重新声明模板参数
}
---
template <typename T> T calc(const T &t); //声明
template <typename U>
U calc(const U &t){ /* */} // 定义时,可以与声明时的符号不同;
---
T::size_type *p; // 编译器不知道size_type是成员变量(执行乘法),还是类型。需要typename
typename T::size_type *p; // 这个正确;
---
template <class T=int> class Numbers{}; // 定义默认实参为int类型
Numbers<double> nb; // 使用double类型
Numbers<> nd; // 使用默认类型
---
long lng;
compare(lng, 1024); // 错误,根据lng判断comapre的模板类型是long,而1024不能自动转成long。
---
template <typenmae T1, typenmae T2, typenmae T3>
T1 sum(T2, T3); // 调用时,无法推断返回的T1是什么类型
auto v3 = sum<long long>(i, lng); // 返回的是long long类型,两个输入参数分别是int和long类型
// 另一种情况,显式声明所有类型,注意是按照顺序的
template <typenmae T1, typenmae T2, typenmae T3>
T2 sum2(T1, T3);
auto v3 = sum<long long, long, int>(lglg, i); // 按照T1T2顺序,返回的是long
小结
对:对象移动、可调用对象与function等不知道在说什么玩意;第16章模板与泛型编程基本上从第2小结开始就已经不知道在说什么了,感觉根本没有见过的东西,看了也是记不住的。希望以后能够遇到再认真学习。
总得来说,用两周左右的时间重新读了一遍《C++ Primer》,还是挺有收获的。毕竟距上次认真学习已经过了两年半(2018年2月购于当当),有不少这段时间内见过但不知道是啥玩意的知识,得到了学习;也搞清楚了一些自己只是知道名字但不知道是干啥的概念。但还有很多根本没见过的技巧,也无法深入学习。在今后的学习过程中,慢慢积累吧。暂时告一段落。