《C++ Primer》学习记录3:类设计者的工具

仅做个人学习《C++ Primer》的一点儿记录。

13. 拷贝控制

  1. 三个控制类的拷贝操作的基本操作:拷贝构造函数、拷贝赋值运算符、析构函数
  2. 拷贝构造函数的第一个参数必须是引用类型,几乎总是const引用;在很多情况下会被隐式调用,所以不能采用explict关键字;
  3. 拷贝函数发生得情况:1. 用等于号定义变量时;2. 讲一个对象作为实参传递给非引用的形参;3. 从一个返回类型为非引用类型的函数返回一个对象;4. 用花括号列表初始化一个数组中的元素;例如:标准库容器的insert/ push成员。但emplace是初始化,不会调用
  4. 为什么拷贝构造函数的形参需要是const引用?因为形参如果不是const引用,必须需要拷贝实参进行传入,而拷贝函数还没有定义;所以一般都是const引用,避免拷贝构造;
  5. 通常赋值运算符返回一个指向其左侧运算对象的引用;另外,标准库通常要求保存在容器中的类型具有赋值运算符,且返回左侧对象的引用。

析构函数

  1. 析构函数没有返回值且不接受参数,用~+类名构成;
  2. 内置类型没有析构函数,不需要处理内置类型的成员;智能指针成员在析构阶段自动被销毁;
  3. 类内成员是在析构函数体执行之后被销毁的;

基本原则

  1. 需要析构函数的类也需要拷贝和赋值操作:需要析构的往往定义了动态内存指针等,在拷贝时如果没有显示定义,则合成的函数只是简单的拷贝指针成员,指向了同一块内存,这样在一个被销毁时另一个会出现错误;
  2. 需要拷贝操作的类,也需要赋值操作;反之亦然。

其他

  1. =default关键词,在默认构造函数,或拷贝控制成员后,加上=default,来显示地要求编译器自动生成合成的函数。如果希望是内联的,则在类内;如果不希望内联,则在类外定义时加;
  2. =delete定义“删除的函数”,禁止某种形式的拷贝构造函数或拷贝赋值运算,不能以任何形式进行调用;
  3. 赋值运算符一定要验证保证能够对自身对象赋值!可以采用销毁左侧对象前,首先拷贝右侧对象;
  4. 右值引用&&:绑定到一个将要销毁的对象上,例如临时变量。很多情况下临时变量赋值给其他变量后会进行销毁,不如直接转移临时变量的控制权给相应的变量,从而“右值引用”。实现了函数的完美转发。
  5. 可以采用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. 重载运算与类型转换

  1. 成员函数通过一个名为this的额外的隐式参数,访问调用他的对象
  2. 运算符函数,必须要求是类的成员,或者至少含有一个类类型的参数;不能是两个内置类型的运算对象
  3. 只能重载已有的运算符,不能自己创造;并尽可能保证重载的含义与原有的相同
  4. 作为成员函数,还是非成员?建议:赋值、下标、调用、访问箭头(=/ []/ ()/ ->)采用成员函数;改变对象状态(自增自减、解引用)等成员函数;而具有对称性的,用作非成员函数,例如:算数、相等性、关系、位运算符等;
  5. 重载输出运算符<<,打印输出结果,一般返回他的 ostream 形参;第一个形参是非常量ostream对象的引用,第二个一般是常量的引用(避免修改);
  6. 重载输入运算符>>,第一个形参是istream的引用,第二个是读入到的(非常量)对象的引用;重载输入运算符应检查输入的有效性;
  7. 重载相等运算符==:如果类需要比较是否相等,优先采用重载而不是函数形式(避免记忆),相等运算符应具有传递性;一般也会同时定义!=运算符,但通常将一个运算委托给另一个,保证一致性;
  8. 下表运算符[]:建议返回所访问元素的引用,这样方便下标索引能够出现在等好的两侧都有意义,与原始的[]运算一致;
  9. 增减运算符重载:前置运算符,应返回递增递减后对象的引用;后置运算符,应返回对象的原值,而非引用;为进行区分,后置运算符在声明时加一个int
  10. 重载成员访问运算符:*/ ->,箭头运算符必须要求是类的成员,而且一定要具有获取成员这一功能;解引用运算符*大多定义为类的成员。
  11. 函数调用运算符():可以向调用函数一样,调用该类的对象;
  12. 类型转化运算符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. 面向对象程序设计

  1. 面向对象程序设计的核心思想是:数据抽象、继承、动态绑定;
  2. C++中,基类将类型相关的函数,与派生类不做改变直接继承的函数,区分对待。对于某些函数,希望派生类进行定义,需要声明称虚函数virtual,加载函数声明前;派生类内部必须重新定义所有的虚函数,可以在后面加上override显示地注明;virtual只能在类内声明,而不能出现在类外的定义
  3. “动态绑定”又称为“运行时绑定”,即函数的运行版本由实参决定。我们使用基类的引用或指针,调用一个虚函数时,将发生动态绑定;
  4. 基类通常需要定义一个虚析构函数,即使该函数不执行任何操作
  5. protected访问运算符,表示派生类可以访问,但其他用户不能访问的类成员
  6. 派生类必须使用基类的构造函数来初始化继承于基类的成员变量;派生类首先初始化基类的变量,然后按照声明顺序初始化特有的成员变量;
  7. 继承静态成员:静态成员是惟一的,只有不是private时才能够被访问
  8. 使用final在类的声明后面,避免这个类被后续继承
  9. 某个类继承某个基类时,必须要求基类已经声明并定义,因为派生类需要知道继承的成员是什么;
  10. 静态类型/动态类型:静态类型是编译时就可以知道的类型,而动态类型是程序运行时才可知的;
  11. 派生类型可以向基类进行转化,但不存在基类向派生类型的转化(否则派生类中可以访问的成员变量在基类中找不到)。派生类向基类进行初始化、拷贝时,基类中不存在的派生类的成员会被忽略掉;
  12. 基类的指针或引用,的静态类型可能与运行时绑定的动态类型不一致。

虚函数

  1. 当使用基类的引用或指针调用一个虚函数成员函数时,会发生动态绑定,此时编译器在编译时也不知道调用了那个虚函数,所以我们需要将所有派生类中的虚函数都进行定义;运行时才能根据动态绑定自动选择合适的函数进行执行;

  2. 动态绑定,但只有当我们采用指针或者引用方式,调用虚函数时,才会发生!如果使用的是普通类型的表达式进行调用,则编译时就能确定版本;

  3. 多态性:是OOP的核心思想,表示我们能够使用继承关系的多个类型,而不用考虑他们的差异;引用或静态指针的静态类型与动态类型的不同,是C++支持多态性的根本所在;

  4. 派生类中的虚函数,可以使用virtual关键字也可以省略,因为虚函数在所有派生类中都是虚函数;如果派生类覆盖了某个虚函数,则要求形参和返回值必须与基类的虚函数相同;

  5. override:显式说明我们要覆盖某个虚函数。如果派生类中的虚函数,形参或返回值与基类的不一致,编译器会认为是重新声明了一个虚函数而不是覆盖;采用override关键字则告诉编译器是进行覆盖,避免不必要的错误;

  6. final:如果我们把某个函数指定为final,则之后任何函数不能对这个函数进行覆盖,否则会发生编译错误;final/ override出现在函数的形参列表以及const或引用修饰符之后。

  7. 虚函数调用的回避机制:有时需要调用虚函数时不进行动态绑定,此时使用作用域运算符进行实现(见下面代码)

  8. 纯虚函数与抽象基类:在虚函数体的后面书写=0定义成一个纯虚函数,一般纯虚函数不需要定义;含有纯虚函数的类称为“抽象基类”;不能定义一个抽象基类的对象;抽象基类可以被继承,继承时如果重写了纯虚函数则可以创建对象,否则仍旧是抽象基类;抽象基类定义了接口。

访问与继承

  1. protected声明希望与派生类分享,但不想被其他公共访问使用的成员;
  2. 派生访问说明符,用于控制派生类用户对基类成员的访问权限。例如用private继承而来的,无论基类中的成员是public还是protected的,在派生类中均变为private的,不能被外部访问。
  3. 但可以通过using方式改变成员的可访问性,using改变后的权限为using语句之前的访问说明符决定;派生类只能为它能够访问的名字提供using声明
  4. class关键字定义的继承,是私有继承的; struct定义的继承,是公有继承的;
  5. 派生类的作用域嵌套在基类的作用域之内,当在派生类的作用域之内找不到名字时,才回到基类中去寻找;派生类中可以定义与基类一样的名字,此时会隐藏基类作用域中的名字,但可以通过作用域运算符来访问被隐藏的基类成员。建议不要使用相同的名字
  6. 派生类中的函数,不会重载基类中的函数,哪怕参数列表不一致,也会隐去基类的函数;这就是为什么虚函数覆盖时,一定要保证参数列表相同,以及override检查;

其他

  1. 由于指向基类的指针可能动态绑定到了派生类,此时析构时需要让系统知道应该执行派生类的析构函数;由此需要将基类的析构函数定义成虚析构函数。只要基类的析构函数是虚函数,就能确保delete基类的指针时调用了正确的析构函数版本;
  2. 派生类执行析构函数时,基类的析构函数自动被调用,所以派生类虚构函数只需要处理派生类自己的成员;
  3. 容器中不能直接存储具有继承关系的多种类型的对象;一般采用指向基类的(智能)指针进行存储,此时指针可以指向基类也可以指向派生类;
// 派生类向基类的转化
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. 模板与泛型编程

  1. 函数模板可以理解成一个公式,生成针对特定类型的函数版本;
  2. template后面跟“模板参数列表”,具有一个或多个模板参数(用逗号分割),用<>包围起来;吗,模板参数列表不能为空
  3. 调用一个函数模板时,编译器用函数实参推断模板实参,将实参的类型绑定到模板参数T——实例化
  4. 模板中的函数参数建议使用const引用,避免拷贝操作;
  5. 模板只有在实例化时,才会进行编译,所以模板的定义应放到头文件中,保证实例化时能够找到定义。函数模板、类模板成员函数,定义放在头文件。
  6. 需要由模板使用者保证,实例化时的对象,能够调用模板内部的运算符号;
  7. 类模板:用来生成类,在实例化时需要提供类得显示模板实参进行绑定;不同的实例形成独立的类。
  8. 类模板成员函数,只有在使用的时候才能够实例化,不使用时不会实例化。这使得某些类型不符合类模板成员函数,也能实例化成类。例如vector标准容器中,可以用string/vector等不同的类型,不符合的成员函数不使用也不会有问题;
  9. 模板参数会隐藏外层作用域中的相同名字,且在模板内模板参数名不能重复、重用
  10. typename关键字:显式告诉编译器该名字是一个类型,否则编译器无法判断是成员变量还是类型;默认理解为访问的是成员变量,而不是类型;
  11. 控制实例化:当模板和类模板被实例化时,才进行编译,这样可能发生:不同源文件生成了多个相同类型的实例化,占用空间。为此可以显式实例化避免开销,采用extern关键字。类模板的显式实例化会实例化所有的模板函数,因此需要保证所有类型均能够用于模板的所有成员函数;
  12. 由于模板函数在调用的时候,编译器根据实参类型判断模板类型,这时候采用特殊的初始化规则,不会发生类型的自动转换;
  13. 有时编译器无法判断某种类型,例如返回类型与传入参数类型是不同的模板,此时需要用户进行显式指定;见下方代码;
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月购于当当),有不少这段时间内见过但不知道是啥玩意的知识,得到了学习;也搞清楚了一些自己只是知道名字但不知道是干啥的概念。但还有很多根本没见过的技巧,也无法深入学习。在今后的学习过程中,慢慢积累吧。暂时告一段落。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值