C++Primer学习笔记之类设计者的工具

目录

一、拷贝控制

1、拷贝、赋值与销毁

1.1 拷贝构造函数

拷贝初始化

1.2 拷贝赋值运算符

1.3 析构函数 

1.4 三/五法则

1.4 阻止拷贝

=delete

1.5 两种类设计(拷贝控制和资源管理)

1.6 交换操作

2、动态内存管理类

3、对象移动

3.1 右值引用

std::move

3.2 移动构造函数和移动赋值运算符

移动迭代器

引用限定符

二、重载运算与类型转换

1、输入输出运算符

2、[]与++  

3、函数调用运算符

3.1 lambda是函数对象

3.2 标准库定义的函数对象

3.3 可调用对象与function

4、重载、类型转换与运算符

4.1 类型转换运算符

4.2 避免有二义性的类型转换

三、面向对象程序设计

1、基类与派生类

1.1 类型转换与继承

final

2、虚函数

​3、抽象基类

4、访问控制与继承

5、继承中的类作用域

6、构造函数与拷贝控制

6.1 虚析构函数

6.2 合成拷贝控制与继承

6.3 派生类的拷贝控制成员

6.4 继承的构造函数

7、容器与继承

四、模板与泛型编程

1、定义模板

1.1 函数模板

模板类型参数

非类型模板参数

1.2 类模板

 1.3 模板参数

1.4 成员模板

1.5 控制实例化

2、模板实参推断

2.1 实例化实参与模板类型参数间的类型转换问题

2.2 模板实参的显式指定

2.3 尾置返回类型

2.4 模板实参推断和引用

引用折叠

2.5 转发

3、重载与模板

4、可变参数模板

5、模板特例化



一、拷贝控制

发生在同类型的另一个对象初始化本对象:拷贝构造函数、移动构造函数

发生在将一个对象赋予同类型的另一个对象时:拷贝赋值运算符、移动赋值运算符

发生在本类型对象销毁时:析构函数

1、拷贝、赋值与销毁

1.1 拷贝构造函数

  1. 如果一个类未定义自己的拷贝构造函数,即使定义了其他构造函数,编译器也会为类合成一个拷贝构造函数
  2. 拷贝构造函数通常不应该是explicit的
  3. 它的参数通常为一个同类型的类对象的const的引用
  4. 编译器从给定对象中依次将每个非static成员拷贝到正在创建的对象中(其中类类型的成员会使用其自己的拷贝构造函数来完成拷贝、内置类型直接拷贝、数组则逐元素地拷贝)

能不能说除了内置类型(直接拷贝),其他的数据对象(包含多个数据成员)只要发生了复制操作,就会调用它的拷贝构造函数,然后将它的数据成员一个一个地拷贝给对方对应成员

拷贝初始化

当类对象作为实参传递、作为函数返回值(当然都是非引用的)会发生拷贝初始化

这里也解释了为什么拷贝构造函数必须为同类型的类对象的引用:

如果形参不是引用类型,那么为了调用拷贝构造函数,必须拷贝它的实参,而为了拷贝实参,又需要调用拷贝构造函数,如此无限循环,调用无法成功

当用花括号列表初始化一个数组或一个聚合类中的成员

vector<string>v {10};    //构造一个包含10个默认初始化元素的临时vector对象,再拷贝给v

 对于聚合类Data,它有两个数据成员(int ival和string s),则

Data vall = { 0, "Anna" };      //拷贝初始化

c.push_back(Sales_data("978-0590353403", 25, 15.99));      //拷贝初始化

c.emplace_back("978-0590353403", 25, 15.99);      //三个参数将传递给Sales_data的构造函数,这个是直接初始化

需要明白一点,拷贝初始化不代表就是使用了拷贝构造函数,也可能是移动构造函数——左值被拷贝、右值被移动:

1.2 拷贝赋值运算符

Foo& operator=(const Foo&)

其实就是一个重载了的赋值运算符,它能完成一个对象(包含多个数据成员)之间的赋值操作,即右侧运算对象的每个非static成员赋予左侧运算对象的对应成员

返回的是一个指向其左侧运算对象的引用,一般为 *this

标准库通常要求保存在容器中的类型要具有赋值运算符,且返回值是左侧运算符对象中的引用

1.3 析构函数 

析构函数释放对象使用的资源,并销毁对象的非static数据成员

析构函数没有返回值,也不接受参数,这就导致它不能被重载,所以一个类只会有唯一一个析构函数

一个内置指针类型的成员或者指向一个对象的引用,当它被隐式销毁或者离开作用域时,析构函数不会执行,它们所指向的对象不会被释放

析构函数体自身并不直接销毁成员,成员是在析构函数体之后隐含的析构阶段中被销毁的

1.4 三/五法则

一切的问题就出在一个指针数据成员,指向的是类内动态分配的一段内存,即给类分配了资源

 可见这个类在构造函数中分配动态内存,对于这样的一个类,合成的析构函数不会delete一个指针数据成员,因此需要定义一个析构函数来释放构造函数分配的内存:

~HasPtr() { delete ps; }

然而如果是合成的拷贝构造函数:

将会把数据成员中的指针成员ps直接拷贝给对方,结果就是多个HasPtr对象可能指向相同的内存:

由上可见,当ret和hp都被销毁时,都将调用各自的析构函数,所以会执行两次delete ps,即这一段内存将被释放两次,这样产生一个未定义的错误。

所以如果一个类定义了析构函数,需要自定义拷贝构造函数:

  1. 自定义析构,则必需自定义拷贝构造和拷贝赋值运算符(一个例外是基类定义虚析构函数)
  2. 需要拷贝构造的类也需要赋值操作,反之亦然
  3. 自定义了拷贝构造,就不会有合成的移动构造
  4. 自定义了移动操作,就必须定义自己的拷贝操作,否则这些成员默认被定义为删除的

结合移动构造和移动赋值两个操作,一般来说,只要一个类定义了任何一个拷贝操作,它就应该定义所有五个操作。其中移动操作只是在拷贝操作非必要的时候,为避免这种拷贝所引起的额外开销而设计出来的。

可对具有合成版本的成员函数(默认构造函数、拷贝控制成员)使用 =default ,来显式地要求编译器生成合成的版本

1.4 阻止拷贝

显然对于iostream这种类是应该阻止对象的拷贝的,以避免多个对象写入或读取相同的IO缓冲

=delete

通知编译器,我们不希望定义这些成员

=delete必须出现在函数第一次声明时

可以对任何函数指定=delete

析构函数不能是删除的成员

 本质上,当不可能默认构造、拷贝、赋值或销毁类的成员时,类的合成拷贝控制成员就被定义为删除的          P450

对于具有引用成员或无法默认构造的const成员的类,合成拷贝赋值运算符被定义为删除的

1.5 两种类设计(拷贝控制和资源管理)**

拷贝指向的string 

上述因为自定义了析构函数,所以也要定义相应的拷贝赋值运算符,定义如下:

 其实就是完成了以下工作:

可见赋值运算符通常组合了析构函数和构造函数的操作

这里要考虑一个自赋值问题,如果直接delete ps,试想如果是自己给自己赋值,那么再执行步骤1的时候就是在拷贝一段已经释放了的内存,引发错误

拷贝指针成员本身

也就是这个情况

 但是它不是使用指向它的这些指针来管理这段共享的内存,而是使用shared_ptr的引用计数count,这个数值也分配了一块动态内存来保存它,只有当count变为0时,才释放这段共享资源

具体类定义和相应的类设计和拷贝控制成员设计见  P456

1.6 交换操作

 它直接完成了左侧运算对象中原内存的释放,而且自动处理了自赋值情况。

2、动态内存管理类

与StrBlob类相对应的,StrBlob使用shared_ptr来管理动态分配的vector<string>

这里使用动态内存管理(主要使用了allocate类)和拷贝控制等技术完成了一个vector<string>容器的设计

见P470笔记

3、对象移动

StrVec类中从就内存拷贝元素到新内存是没有必要的,一些对象较大,或者对象本身要求分配内存空间(如string),不必要的拷贝代价非常高。

还有如IO类和unique_ptr类,它们包含不能共享的资源,所以这些类型的对象不能拷贝但是可以移动

3.1 右值引用

当一个对象被用作右值的时候,用的是对象的值(内容);包括返回非引用类型的函数、算术、关系、位和后置递增/递减运算符

当对象被用作左值的时候,用的是对象的身份(在内存中的位置);包括返回左值引用的函数、赋值、下标、解引用和前置递增/递减运算符

int &&rr1 = 42;          //字面值常量是右值

int &&rr2 = rr1;          //表达式rr1是左值

变量是左值,不能将右值引用绑定到一个变量上,即使这个变量时右值引用类型也不行

std::move

  1. C++ 标准库使用比如vector::push_back 等这类函数时,会对参数的对象进行复制,连数据也会复制.这就会造成对象内存的额外创建, 本来原意是想把参数push_back进去就行了,通过std::move,可以避免不必要的拷贝操作。
  2. std::move是将对象的状态或者所有权从一个对象转移到另一个对象,只是转移,没有内存的搬迁或者内存拷贝所以可以提高利用效率,改善性能.。
  3. 对指针类型的标准库对象并不需要这么做.

#include<utility>

int &&rr3 = std::move(rr1);          //获得绑定到左值上的右值引用

但是只能对rr1(移后源对象)赋值或销毁,而不能使用它

std::string str = "Hello";

std::vector<std::string> v;

v.push_back(std::move(str));

str = ""       ;       v[0] = Hello

std::move其实是一个模板,其模板原型以及对它如何返回一个绑定到左值上的右值引用的理解见P611

3.2 移动构造函数和移动赋值运算符

与拷贝构造函数一样,任何额外的参数都必须有默认参数

移动构造函数是移动源对象中的资源,一旦资源完成移动,源对象必须不再指向被移动的资源,这些资源的所有权已经归属新创建的对象,这类似一种资源“窃取”行为。

参数类型是一个右值引用

可见移动构造其实就是新创建对象的指针直接指向源对象中的指向资源的指针,而源对象的这些指针被置为nullptr,也就放弃了资源的所有权,归新创建对象所有

标准库可能会对这种资源“窃取”行为视为异常,并做一些额外工作去处理这种可能的异常;

可能的异常:在重新分配内存的过程中使用了移动构造函数,且在移动了部分而不是全部元素就抛出了一个异常,这会导致,旧空间中的移动源元素已经被改变了,而新空间中未构造的元素可能尚不存在。
所以除非知道移动构造函数不会抛出异常,此时就应该使用noexcept显式地告诉标准库可以安全使用。反之,在重新分配内存的过程中,它必须使用拷贝构造函数而不是移动构造函数。

判断一个移动构造函数会不会抛出异常的方法是,看这个函数内部有没有涉及到分配内存的操作,例如往set容器插入一个元素,可能会抛出bad_alloc异常 

当然这种窃取行为不应该损坏了移后源对象,源对象必须保持有效的、可析构的状态(上面的StrVec类是把源对象的指针都置为nullptr,相当于是默认初始化的对象的状态)

删除的移动构造函数或移动赋值运算符与阻止拷贝(=delete)的规则类似  P476

值得注意的是,如果定义了这两个移动操作,该类的合成拷贝操作(构造和赋值)会被定义为删除的,所以必须自定义这两个拷贝操作。

如果一个类既有移动构造函数,也有拷贝构造函数,编译器使用普通的函数匹配规则来确定使用哪个构造函数。(虽然拷贝构造函数接收const &引用类型的参数,移动构造函数接受 &&引用类型,型,但是完全可以实现从 && 到 const & 的转换)P477

移动迭代器

移动迭代器的解引用运算符生成一个右值引用

标准库的make_move_iterator函数将一个普通迭代器转换为一个移动迭代器,原迭代器的所有其他操作在移动迭代器中都照常工作

for(size_t i = 0; i != size(); ++i)

{   alloc.construct(dest++, std::move(*elem++))  }      //*elem代表旧数组中的元素

等价代码:

auto last = uninitialized_copy(make_move_iterator(begin()), make_move_iterator(end()), dest);

//这里的begin与end前隐藏了 this->

由于uninitialized_copy对输入序列的每个元素调用construct,但是这里是移动迭代器,解引用以后是右值引用,所以这里调用的construct其实是使用移动构造函数来构造函数

引用限定符

右值可以进行赋值,类中可使用引用限定符阻止这种用法

引用限定符可以是&和&&,分别指出this可以指向一个左值或右值

引用限定符只能用于(非static)成员函数,且必须同时出现在函数的声明和定义中

引用限定符必须跟随在const限定符之后

Foo someMem() const &;

对于同名且同参的成员函数,有没有const限定符被认为是两个版本的函数,但引用限定符则不然,一个有则其他必须都有。


二、重载运算与类型转换

不能被重载:   ::     .*     .    ? :

不建议重载:   &&     ||     ,     &    

 非成员运算符函数的等价调用:

成员运算符函数的等价调用,可见也是使用点运算符:

 将运算符定义为成员函数还是普通非成员函数的一般准则

试想运算符成员函数的等价调用是  "hi".operator+(s) ,显然错误 

1、输入输出运算符

当运算符左侧的操作数属于C++标准类型或是一个其他类的对象时,显然运算符重载函数不能作为成员函数,解决方法是采用友元函数:

例如  A = 2.75 * B

其函数原型: Time operator * (double m, const Time & t);     由于此时为非成员函数,无法直接访问类的私有数据,则需要声明为友元函数。

其原型如下     friend Time operator * (double m, const Time & t);  函数定义不需要加关键字friend

基于以上原因分析,重载<<运算符的函数必须为友元函数,否则为“trip << cout”

ostream &operator << (ostream & os, Complex & c) {  os << “(” << c.real << “+” << c.imag << “i)” << std::endl;

当读取操作发生错误时,输入运算符应该负责从错误中恢复  P496

2、[]与++  

operator[]

必须是成员函数

 由于下边运算符通常返回访问元素的引用,所以它可能出现在赋值运算符的任意一端(一个读,一个写),所以通常应该定义下标运算符的两个版本:一个返回普通引用,一个是类的常量成员且返回常量引用

递增与递减运算符,只要注意当显式调用前置或后置运算符时:

 否则编译器无法区分调用的是前置版本还是后置版本

建议:一个类应该同时定义前置版本和后置版本,且定义成类的成员。

3、函数调用运算符 **

STL之仿函数实现详解_一个菜鸟的博客-CSDN博客_仿函数

类重载了函数调用运算符,则可以像使用函数一样使用该类的对象。由于这样的类同时也能存储状态,所以它们比普通函数更加灵活。  

函数对象:一个类定义了调用运算符,则该类的对象称作函数对象,可以调用这种对象的,其行为类似函数。   P506

for_each(vs.begin(),vs.end(),PrintString(cerr, '\n'));

生成一个临时类对象(该类重载了函数调用运算符),再对于vs中的每个元素调用函数调用运算符。

3.1 lambda是函数对象

编译器将lambda表达式翻译成一个未命名类的未命名对象。   P508(看书解释更直观)

3.2 标准库定义的函数对象

#include<functional>

 transform(gr8.begin(), gr8.end(), out, plus<double>());           //计算gr8的每个元素的和并将结果发送到输出流

 plus为STL提供的与“+”等价的函数符,该参数是用plus<double>构造函数构造的一个临时函数对象,该对象负责在double元素之间执行+运算。

比较指针操作     P510

3.3 可调用对象与function

#include<functional>

C++有多中可调用对象:函数、函数指针、lambda表达式、bind创建的对象以及重载了函数调用运算符的类对象,function为不同类型的可调用对象提供一种共享的调用形式。

function模板,它可以接受两个int、返回一个int的可调用对象

 利用map构建运算符到可调用对象的映射关系:

 实现调用形式共享:

 当重载函数与可调用对象名重复时,会产生二义性:

解决方法是借助函数指针或者lambda表达式 P513

4、重载、类型转换与运算符

4.1 类型转换运算符  **

operator type() const       { return type; }      //可见type是要转换成的目标类型

si为一个类类型对象,operator int() const  { return val; }为类的成员函数,val为size_t型的数据成员

si + 3.14               //转换运算符先将si转换为int,再由内置类型转换为double,计算double的和

可见类型转换运算符都是隐式执行的,所以无法给这些函数传递实参,当然也就不能在类型转换运算符的定义中使用任何形参。且一般被定义为const成员

实际的类设计中一般不定义类型转换运算符,但是返回bool的类型转换比较普遍,而且易引发问题:    cin << i;               //cin被隐式转换为bool值,而bool又被提升成int,成了左移i个位置的运算

explicit operator int() const  { return val; }

但是当“cin << i”被用作条件时,这种显式的类型转换还是会被隐式地执行 

4.2 避免有二义性的类型转换

一是两个类提供相同的类型转换(在A类中提供接受B类对象为参数的转换构造函数,而在B类中又提供B类到A类的类型转换运算符)

二是定义了多个转换源或者转换目标是算术类型的转换(因为内置的算术类型本身会发生类型转化)    P518

三是因为重载函数发生的一些二义性问题

四是运算符的重载与内置运算符的二义性问题   

class Fraction{

Fraction(int num, int den=1)   //定义转换构造函数

operator double() const { }   //定义类型转换运算符

}

Fraction f(3,5);

double d=4+f;    //二义性,是4转Fraction,还是f转double呢

5、重载new与delete运算符

operator new 

这里有提到,内存池可以节省空间,因为它不需要分配cookie(每块在堆上申请的内存都会有两个cookie记录内存块大小,方便内存管理)

这是在类里面重载new与delete运算符,delete的第二参数可选,可有可无

注意重载的new与operator是static成员函数,因为调用operator new一般都是在创建一个类对象的时候使用,由于还没有对象,也就没有this指针,所以这个函数要求是静态的

也可以使用::运算符,此时将使用标准库的那个全局的new运算符

placement new 

重载operator new除了必需的第一个参数size_t以外(隐藏了,自动计算存储这个对象的内存的大小,在前面分析的new底层实现的第一步中),其他的参数必须以placement arguments的形式出现在new后面的小括号中,即placement new:

即placement new分配内存后,调用构造函数构造对象时却失败,抛出异常,此时才会调用这个重载的operator delete(),以归还这段内存

1~4将对应调用重载的(1)~(4)new运算符函数以及默认的构造函数

这里第5条new语句将调用接受一个int参数的构造函数,所以抛出异常(这是自己为了测试故意写的抛出异常)

分析这里类外定义模板成员函数的语句是怎么写的 

这一页需要

三、面向对象程序设计

1、基类与派生类

display是基类成员函数:

Student stud1(~,~,~);   //定义基类对象    

Graduate grad1(~,~,~,~);     //定义派生类对象    

Student *pt = &stud1;         //定义指向基类对象的指针变量

①pt -> display();           //调用基类对象的display()函数    

   pt = &grad1;              //这是向上强制类型转换,派生类引用或指针转换为基类引用或指针

②pt -> display();         //所以该指针指向的是派生类对象中的基类部分

注意:如果display是在Graduate类中定义的成员函数,而基类Student没有这个函数,则对于②,即使pt此时的动态类型是Graduate*(比如说某函数接受形参类型是Student *,但是传递给他的实参却 是Graduate,那么可以说这个基类Student指针的动态类型是由运行时调用该函数传递的实参决定,即Graduate类型),但是②仍出错,因为Graduate的作用域是嵌套在Student内的,意味着pt对display的搜索是从Student开始的,也就是display对于Student来说不可见。所以说,一个对象的成员是否可见是由一个对象、引用或指针的静态类型(这里是Student*)决定的。          

2、虚函数***

一个基类可以有多个派生类。每个派生类又产生新的派生类,形成同一基类的类族,这样假设每个派生类都有同名函数display(),在程序中要调用同一类族中的不同类的同名函数,就要指定多个指向各派生类的指针变量。(上面说了由静态类型决定,基类指针即使指向了派生类对象,但是还是无法调用到派生类中的函数)     

此时定义基类display()函数为虚函数,则②调用的是派生类对象的display()函数(这个时候就由动态类型决定了)。这也就是多态性:对同一消息,不同对象有不同的响应方式 

当把基类的某个成员函数声明为虚函数时,允许在其派生类中对该函数重新定义,赋予它新的功能,又可通过以上方式调用同一类族中不同类的对象。而在之前,为实现不同功能,必须取许多不同的函数名,很不方便。       

当然要回避虚函数的机制的话(就是基类版本被覆盖了,但是还打算使用它的情况下),可以在函数调用前加上作用域运算符:

pt->Student::display();      //此时不管pt的动态类型是什么,调用的就是Student版本的display()函数

智能指针类也支持派生类向基类的类型转换

final

C++11提供了一种防止继承发生的方法,即在类名后跟一个关键字final:

class Last final : Base { /* */ };           //此时Last不能作为基类

virtual void f1(int) const final;              //也可以定义一个虚函数不允许被后续的其他类覆盖

基类虚函数可覆盖,也可直接继承

一旦某个函数被声明为虚函数,则在所有派生类中它都是虚函数

派生类中的虚函数,其参数列表与返回类型必须与基类函数完全匹配,才会覆盖基类的虚函数(注意这里的覆盖只是相对派生类的作用域内,基类虚函数被覆盖了)

(存在一个例外,当返回类型是各自对象的指针类型时,只要派生类指针可以转换为基类指针,也是可以覆盖基类版本的)

派生类指针向基类指针的转换受访问权限的影响,即派生类必须能访问其基类部分的成员才可以完成这种转换

如果派生类定义了一个函数与基类的虚函数同名但是形参列表不同,编译器会认为这是一个新定义的函数,它并没有覆盖基类的版本。但就编程习惯而言,是不应该这样的,所以可以通过关键字override来避免这种情况的发生(也即指明这个同名函数必须是虚函数,只能覆盖,不能隐藏基类方法;这时如果形参列表不同的话编译器就会报错了)

对于其他成员函数,派生类将在其作用域内隐藏基类的同名成员,即使形参列表不一致

举例如下:

Note:

  1. 对于虚函数且通过指针或引用进行调用,则会发生动态绑定 
  2. 对于非虚函数或通过对象进行调用,则实际调用的函数版本由指针的静态类型决定(由于作用于嵌套),静态绑定在底层被解析成汇编语言的CALL

  vptr与vtbl(虚函数指针与虚函数表)***

当生成类对象的时候,编译器会自动的将类对象的前四个字节设置为虚表的地址,而这四个字节就可以看作是一个指向虚函数表的指针。虚函数表可以看做一个函数指针数组。

虚函数表中放的都是函数指针,指向这些虚函数

虚函数表存储在进程的只读数据段(.rdata段)

其中*(p->vptr)[n](p) 的意思是: 一个指针p,最终转换成 p指向的虚函数表中存放的第n个函数指针(虚函数表中的次序如上图)

发生动态绑定的三个条件:

  • 调用者是一个指针
  • 该指针发生了向上强制类型转换
  • 调用的是虚函数

关于this

C++中所有的成员函数都隐藏着接受this指针的函数参数

this始终指向类对象myDoc,所以myDoc.OnFileOpen()被解析为如图调用CmyDoc类的基类部分的OnFileOpen(其中已完成从派生类CMyDoc的指针向上类型转换成基类CDocument指针的过程),同时调用虚函数Serialize();综上,将发生动态绑定(this指针指向的是myDoc,然后查询该类的虚函数表,最终指向了CMyDoc::Serialize()),所以调用的是派生类版本的Serialize()。 

个人理解:这也体现了设计模式中的模板方法模式

稳定的写成非虚函数,直接由派生类继承;变化的写成虚函数,变化的操作在派生类中重新定义即可。

3、抽象基类

纯虚函数如果要定义,则函数体必须定义在类的外部

含有(或者未经覆盖直接继承)纯虚函数的类是抽象基类

将函数定义为纯虚函数,则编译器要求在派生类中必须予以重写以实现多态性

不能创建抽象基类的对象

解释纯虚函数不能被实例化的原因:

纯虚函数在类的vftable表中对应的表项被赋值为0。也就是指向一个不存在的函数。由于编译器绝对不允许有调用一个不存在的函数的可能,所以该类不能生成对象。在它的派生类中,除非重写此函数,否则也不能生成对象。

纯虚函数是为了实现一个接口,用来规范派生类的行为,让所有的类对象(主要是派生类对象)都可以执行纯虚函数的动作

4、访问控制与继承

protected:希望与派生类分享但是不想被其他公共访问使用的成员

  1. 若继承方式是public,基类成员在派生类中的访问权限保持不变,也就是说,基类中的成员访问权限,在派生类中仍然保持原来的访问权限;
  2. 若继承方式是private,基类所有成员在派生类中的访问权限都会变为私有(private)权限;
  3. 若继承方式是protected,基类的共有成员和保护成员在派生类中的访问权限都会变为保护(protected)权限,私有成员在派生类中的访问权限仍然是私有(private)权限。

派生访问说明符的目的是控制派生类用户对于所有基类成员的访问权限

有时我们希望改变个别成员的可访问性,可通过using声明实现:

默认访问说明符:

class默认私有继承,struct默认公有继承

5、继承中的类作用域

派生类的作用域嵌套在其基类的作用域内,一个名字在派生类的作用域内无法解析,则编译器将继续在外层的基类作用域中寻找该名字的定义

也真是因为这种继承嵌套关系,派生类才能使用基类部分的成员

派生类中的成员将隐藏同名的基类成员,此时要使用被隐藏的基类成员的话,可借助作用域运算符。不建议覆盖除虚函数外的其他基类中的名字

6、构造函数与拷贝控制

6.1 虚析构函数

delete一个动态分配的对象的指针时将执行析构函数

基类的析构函数需要定义成虚析构函数,因为对于数据成员类型为指针,且该指针是一个指向派生类对象(包含动态分配的内存)的基类指针时,可防止delete这样一个指针而产生未定义的行为(只会运行基类的析构函数,而实际目的是想调用派生类的析构函数以释放这个指针指向的动态分配的内存,防止内存泄漏)

或者解释为:

虚函数是为了实现多态(看1.1对于多态性的论述)

根据基类指针或引用可指向派生类对象的特性,可定义如下指针数组:

Brass * p_clients[4]    

其数组成员可以是指向Brass类的指针,也可以是指向派生类BrassPlus的指针。这样可以用基类指针数组同时管理包括其自身在内的所有派生类对象。

所以为了让基类指针也可以同时管理派生类对象的释放操作,将基类的虚构函数定义为virtual的必要性是显而易见的

虚析构函数将阻止合成移动操作

6.2 合成拷贝控制与继承

由于虚析构函数阻止合成移动操作,则移动基类对象时实际使用的是合成的拷贝操作P477(Foo&&转换为const Foo&)

最后一行:派生类调用移动构造函数,则其基类部分也需要调用移动构造函数;由于基类自定义了拷贝构造函数,所以编译器不会再合成移动构造函数。没有移动构造函数则会隐式调用拷贝构造函数以代替,但基类拷贝构造函数又被定义为删除的;所以该语句错误。

6.3 派生类的拷贝控制成员

一般要在派生类的构造函数的初始值列表中显式地使用基类地拷贝(或移动)构造函数,这样才会拷贝(或移动)基类部分;否则,派生类对象的基类部分将被基类默认构造函数初始化。

Base派生D:

D(const D& d) : Base(d),/* D成员初始值 */     {...}

认识到一点:对于一个派生类对象,构造时先构建该对象的基类部分,然后再初始化派生类自己的成员;而析构时正好相反,先隐式销毁派生类对象自身的成员,再隐式销毁基类部分的成员。

6.4 继承的构造函数

类不能继承默认构造、拷贝构造和移动构造函数

派生类继承基类构造函数的方式是提供一条注明了(直接)基类名的using声明语句

 这里的“继承”,实际上是由编译器生成的构造函数,形式如下:

derived(parms)  : base(args)  { }

一些继承的构造函数的特点P557

7、容器与继承 **

 一个容器中不能存放具有继承关系的多种类型的对象

6.1说到多态性的设计是为了基类指针可以管理所有的派生类对象,这很好的在容器中得到了体现。

当在容器中存放具有继承关系的对象时,实际上存放的通常就是基类的指针:此时指针的静态类型(即都是基类指针类型)决定了它们可以存放在同一个容器中,而动态类型又决定了它们可以代表不同继承级别的类的对象。

这个容器的设计与使用(类设计经验技巧)  P561

四、模板与泛型编程 ***

1、定义模板

模板不是类型,实例化以后才是一个类型。模板参数的实际类型在编译时才能确定。

1.1 函数模板

template<typename T>        //定义函数模板及其模板参数列表

模板类型参数

可以看作类型说明符,可以用来指定返回类型或函数的参数类型,以及在函数体内用于变量声明或类型转换

 类型参数前必须使用关键字class或typename:

template<typename T, class U> T calc(const T&, const U&);

非类型模板参数

当我们希望能比较不同长度的字符串字面常量时,可对这个“不同长度”定义为模板参数,它表示一个值而非类型;非类型参数前是一个特定的类型名

可见实参必须是常量表达式, 如果是绑定到指针或引用非类型参数(即N和M可以是指针或引用)的实参必须具有静态的生存期。

体现的两点原则:函数参数是const的引用;函数体中仅使用<比较运算

保证传递给模板的实参支持模板所需求的操作(例如这里传递Sales_data类对象,显然无法进行<比较),这是调用者的责任

注意inline和constexpr关键字必须跟在模板参数列表后面

由于模板必须被实例化出一个特定版本,编译器才会生成代码;而要实例化一个模板,编译器需要掌握这些函数的定义;所以,函数模板和类模板成员函数的定义通常放在头文件中,而不能跟函数声明分离开。

1.2 类模板

认识到一点很重要:vector是模板而非类型,由vector生成的类型必须在模板名后的尖括号中提供额外信息,即vector中元素(对象)的类型。由于引用不是对象,所以不存在包含引用的vector

类模板的成员函数

ret-type ClassName::member-name(parm-list)

定义在类外的形式如下:

template <typename T> ret-type ClassName<T>::member-name(parm-list)

一个类模板的成员函数只有当程序用到它时才进行实例化,这使得即使某种类型不能完全符合模板操作的要求,也能用该类型实例化类

实例化类模板:

Blob<string> articles = {"a", "an", "the"};

在一个类模板的作用域内,可以直接使用模板名而不必指定模板实参   P588

  • 类模板和友元  P589
  • 类模板的static成员,当使用一个特定的模板实参类型实例化类模板时,将会为该类类型实例化一个独立的static成员  P591

理解以上两点的关键是,每个特定的模板实参类型实例化类模板后,都代表不相同的类类型 

模板类型别名

typedef std::array<std::string,12> arrst;                  arrst months;

新标准允许为类模板定义一个类型别名

当然也可以固定其中的一个或多个模板参数:

 1.3 模板参数

与函数参数相同,声明中的模板参数的名字不必与定义中相同。

也会隐藏外层作用域中声明的相同名字,还应避免引发二义性。

使用类的类型成员    P593

考虑     T::size_type * p       

这里的size_type是T中的一个类型还是一个名为size_type的static数据成员根本无从分辨。

由于C++默认通过作用域运算符访问的名字不是类型,所以希望使用size_type类型必须借助关键字typename:

typename T::size_type * p;                //定义一个类型为T::size_type *的指针p

注意是必须使用关键字typename,而不能使用class

默认模板实参,原先只允许为类模板提供默认实参,C++11规定函数模板也可以有默认实参

能看懂以下代码即可(P594):

其调用形式:

对于类模板,如果类模板的所有模板参数都有默认实参,显然此时即使不用给类模板实例化也不会有任何二义性(就使用默认的实参类型即可),所以可以使用空<>来实例化类模板:

Blob<> articles;

1.4 成员模板 **

即一个类(无论是普通类还是模板类)可以包含本身是模板的成员函数

P595 学习这里定义一个模板删除器的设计思想

对于类模板的成员模板:

template<typename T> class Blob{

        template<typename It> Blob(It b, It e);     };

必须同时为类模板和成员模板提供模板参数列表:

实例化时也必须同时提供类和函数模板的实参:

int ia[] = {0,1,2,3,4,5,6};

Blob<int> a1(begin(ia), end(ia));

1.5 控制实例化

由于模板的定义及声明一般在头文件中,而实例化它的代码却在其他源文件中,且模板的实例化只发生在编译时,通过传递给模板实参的参数判断。这就导致一个问题,同一个模板可能会在不同的源文件中实例化多次,导致额外的开销。

解决方法是通过显式实例化

//template.h

template<typename T> class Blob{ ... };

template<typename T> int compare(const T &v1, const T  &v2) { ... }

需要注意1.1节提到的,模板函数或类模板的成员函数的定义也要放在头文件。

//Application.cc

extern template class Blob<string>;          //实例化声明,不在本文件中生成实例化代码

extern template int compare(const int&, const int &);

//templateBuild.cc

template class Blob<string>;          //实例化定义实例化类模板的所有成员,生成实例化代码

template int compare(const int&, const int &);

这里与类模板不同的是它会实例化所有成员,这也就要求所用的实例化类型必须能用于模板的所有成员函数。

2、模板实参推断

2.1 实例化实参与模板类型参数间的类型转换问题

template<typename T> fref(const T&, const T&);

int a[10], b[42];

frep(a, b);          //调用错误

C++允许将变量定义成数组的引用:

int arr[10];

int (&arrRef)[10] = arr;         //arrRef引用有一个含有10个整数的数组

分析:  a→T           int (&T) [10] = a;        

             b→T           int (&T) [42] = b;

可见T是两种不同类型的引用,类型不匹配,调用错误(这里不会发生数组到指针的转换)

2.2 模板实参的显式指定

同定义类模板实例一样,可以使用尖括号显式指定模板实参类型

template<typename T1, typename T2, typename T3>

T1 sum(T2, T3);

当返回值也是一个模板参数时,根本没有任何函数参数的类型可以用来推断T1的类型,所以需要提供显式模板实参:

auto val = sum<long long>(i, lng)        //long long sum(i, lng)

且提供了显式模板实参以后,就可以正常进行类型转换了:

 int compare(const T&, const T&);

2.3 尾置返回类型

返回类型不明(需要实例化以后才能确定),则需要表示为尾置返回类型,且借助decltype:

注意这里迭代器解引用返回的是容器元素的引用

如果想要返回该元素的拷贝,可以使用标准库模板类进行类型转换  P606

#include<type_traits >

remove_reference<T>::type    总是将T的类型(无论引用或是非引用)转换为type(非引用)类型

函数指针也可以实例化一个函数模板

int (*pf1)(const int&, const int&) = compare;

2.4 模板实参推断和引用

引用折叠

 P609

大概涉及到,通过类型别名或通过模板类型参数可以间接定义一个引用的引用,而模板参数类型T也是可以被实例化为一个左值引用类型的 

2.5 转发

P613 提供了两种调用模板所提供的实参,而在实例化模板的过程中其原有const属性或左值/右值属性没有得到有效保持的两种情况

std::forward

#include<utility>

其中的forward必须通过显式模板实参来调用,forward返回该显示实参类型的右值引用 

再结合可变参数模板,实现参数包的转发,同时对forward的应用有更深的理解  P623

3、重载与模板

  1. 考虑有多个可行的、精确匹配的模板函数时,选择更特例化的模板(P616一个例子)
  2. 对于一个调用,如果一个非函数模板与一个函数模板提供同样好的匹配,则选择非模板版本(P617、P621)

4、可变参数模板 **

template<typename ... Args> void foo(Args ... args)   //其中Args为模板参数包,args为函数参数包

cout<< sizeof...(Args)<<endl;         //输出类型参数的数目

cout<< sizeof...(args)<<endl;         //输出函数参数的数目

对应initializer_list来定义一个可接受可变数目实参的函数,所有实参必须具有相同的类型(或它们的类型可以转换为同一个公共类型),而对于不知道实参数目及类型的情况,显然就是可变模板参数的用武之地

可变模板参数通常是递归的(P620示例),这里要注意一点是,一个非可变参数版本的声明必须在作用域中,否则递归无法终止。 **

包扩展    每个元素可以按某种指定的模式进行扩展( P622示例)

5、模板特例化 **

特例化其中一个模板,并定义自己需要的特殊操作(还是前面的一个道理,模板的每一个特例化,但是一种不同的类型)

类模板特例化必须在原模版定义所在的命名空间中特例化,所以在使用这个特例化版本时,需要在在前面加上命名空间以限定

std::hash<Sales_data>

可见特例化为hash<Sales_data>也需要满足hash的键值对

类模板也可以进行部分特例化  P628

只是特例化为左值或右值引用类型,但还是没有特例化一个具体的参数类型,即部分特例化。

特例化类的成员,而不是类本身   P629

6、模板偏特化 **

1、

2、将模板实参类型T缩小范围为可以指向任意类型T的指针:

7、模板模板参数 **

第一个用法错误的原因是 list本身并不止有一个模板参数,虽然都有默认值,

allocator<T>  ?? 

 以上两张图片仔细对比,为什么他不是模板模板参数?

因为 class Sequence=deque<T> 定义的是一个默认模板实参deque<T>,并不是模板模板实参!P594

可以看到statck<int,list<int>> s2也是一般的模板实例化,与上面的 Lst 的实例化方式有根本性的区别。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值