《C++ primer 5th》笔记
#引用
引用在定义时必须初始化,之后,引用和它的初始值对象一直绑定在一起,无法让引用重新绑定到新对象。
引用只能绑定在对象上,不能与字面值或某个表达式的计算结果绑定在一起,但是常量引用可以。
比如:
int &r1 = 5; //错误
const int &r2 = 5; //正确
不能定义指向引用的指针,因为引用不是对象,没有实际地址。
#指针
不能直接操作void *指针所指的对象,因为我们不知道这个对象是什么类型,也就无法确定能在这个对象上做哪些操作。
int *p;
int *&r = p; //r是一个int*类型的引用。
#const
默认状态下,const对象只在文件内有效。想要一个const变量在多个文件间都可以使用,对于const变量不管是声明还是定义都添加extern关键字。
当以编译时初始化的方式定义一个const对象时,编译器在编译过程把用到该变量的地方都替换成对应的值。
const int ci = 1024;
const int &r1 = ci;//对
int &r2 = ci;错误,不能让一个非常量引用指向一个常量对象。
double i = 5.6;
const int &r = i;//这样是可以的,编译器会优化成:
double i = 5.6;
const int tmp = i;
const int &r = tmp;
但是:
double i = 5.6;
int &r = i; //这样子是错误的,因为编译器把代码变成:
double i = 5.6;
const int tmp = i;
int &r = tmp;
为什么r不是const的就是错误的呢?
如果r不是const的,就允许对r赋值,这样就会改变r所引用的对象的值,注意,此时r绑定的对象是一个临时变量而不是i,既然让r引用i,就肯定是想通过r改变i的值,但是现在做的不是这样子,C++也就把这种行为归为非法的。
##指向常量的指针
const int * p;
和常量引用一样,指向常量的指针没有规定其所指的对象必须是一个常量,仅仅要求不能通过该指针改变对象的值。
##常量指针:把指针本身定为常量
int * const p;
常量指针是一个常量,必须初始化。
指针的值(也就是放在指针中的地址)在初始化后,不能再改变。
#数组
int a[5];
int *p[5]; //p是含有5个int*的数组
int (*p)[5] = &a; //p指向一个含有5个int的数组
int b[10];
int (*p)[5] = &b; // 编译错误
#类的this
假设有一个类A,total是A的一个对象,isbn()是A的一个成员函数,当我们使用
total.isbn()时,实际上是在替某个对象调用isbn()。成员函数通过一个名为this的额外隐式函数来访问调用它的那个对象。当我们调用一个成员函数时,用请求该函数的对象地址初始化this,例如:
调用total.isbn(),则编译器负责把total的地址传递给isbn的隐式形参this,可以等价的认为
A::isbn(&total)
因为this的目的总是指向“这个”对象,所以this是一个常量指针A *const this,即this的指向的地址不能改变
那么如果想让this为一个const A *const this呢?this是隐式的不会出现在参数列表中,要在哪里声明?
C++的做法是允许把const放在成员函数的参数列表之后,这个const表示this是一个指向常量的指针,这样使用const的成员函数也叫常量成员函数
因为这时候this是const A *const的,所以不能在这个函数里面改变调用它的对象的内容。
不能显性地定义自己的this指针。
常量对象,常量对象的引用或指针都只能调用常量成员函数
编译器处理类的步骤:先编译成员的声明,再是成员函数体,所以成员函数体可以随意使用类中的其他成员而不用在意成员的次序。
类外部定义的成员的名字必须包含它所属的类名。
#构造函数
类通过一个或几个特殊的成员函数来控制其对象的初始化过程,叫做构造函数。
构造函数的名字和类名相同,并且没有返回类型。
构造函数不能被声明为const的,当我们创建一个const对象时,直到构造函数完成初始化过程,对象才能取得起“const”属性。
只有类没有显式地定义构造函数,编译器才会为我们隐式地定义一个默认构造函数。
如果类中包含一个其他类类型的成员且这个成员的类型没有默认构造函数,那么编译器无法初始化该成员。
构造函数初始值列表。
#拷贝,赋值和析构
#访问控制与封装
public:成员在整个程序内可被访问,定义类的接口
private:成员可以被类的成员函数访问,封装了类的实现细节
一个类对于某个访问说明符能出现的次数没有限定。
##我们可以使用class或struct定义类,唯一的一点区别是:struct和class的默认访问权限不太一样。
##类可以在它的第一个访问说明符之前定义成员,对这种成员的访问权限依赖于类定义的方式。
##使用struct,默认是public的
##使用class,默认是private的
#友元
类可以允许其他类或者函数访问它的非公有成员,方法是令其他类或函数成为它的友元,在类里加上friend关键字开始的函数声明语句。
记住,友元声明只能出现在类定义的内部。
在类里面变量的声明中加入mutable关键字表示这是一个可变数据成员,一个可变数据成员永远不会是const,即使它是const对象的成员,所以,一个const成员函数可以改变一个可变数据成员的值。
##一个const成员函数如果以引用的方式返回*this,那么它的返回类型是常量引用。
所以经常要基于const的重载。
对于一个类来说,在我们创建它的对象之前必须被定义,而不能只是声明,否则编译器无法确定这样的对象需要多少存储空间。
编译器处理完类中的全部声明之后才会处理成员函数的定义。
对类的实例的数据成员而言,如果没有在构造函数的初始值列表中显式地初始化成员,则该成员将在构造函数体之前执行默认初始化。
在类的构造函数执行的时候,如果成员是const,引用,或者属于某种未提供默认构造函数的类类型,必须通过构造函数初始值列表初始化,而不能在函数体内赋值。
构造函数初始值列表只说明用于初始化成员的值,而不限定初始化的具体执行顺序。
成员的初始化顺序和它们在类定义中出现顺序一致:第一个成员先被初始化,然后第2个。。。
#委托构造函数
一个委托构造函数使用它所属类的其他构造函数执行它自己的初始化过程。
方式:在成员初始化列表只有一个入口,调用其他构造函数。
转换构造函数
#类的静态成员
static
静态成员可以是public,private,可以是常量,引用,指针,类类型等
类的静态成员存在于任何对象之外,对象中不包含任何与静态数据成员有关的数据。
同样,静态成员函数也不与任何对象绑定在一起,它们不包含this指针。
所以,静态成员函数不能声明成const的,也不能在static函数体内使用this。
使用作用域运算符直接访问静态成员。
虽然静态成员不属于类的某个对象,但是我们依然可以使用类的对象,引用,指针来访问静态成员。
当在类的外部定义静态成员时,不能重复static关键字,static关键字只出现在类内部的声明语句。
因为类的静态数据成员不属于类的任何一个实例,所以它们不是在创建类的实例时被定义的,也就是说它们不是由类的构造函数初始化的。
一般,我们不能在类的内部初始化静态成员,而是在类的外部定义和初始化每个静态成员。
静态数据成员定义在任何函数之外。
#顺序容器
一个容器就是一些特定类型对象的集合。
容器操作:
iterator 此容器类型的迭代器类型
const_iterator 可以读取元素,但不能修改元素的迭代器类型
c.insert(a) 将a中的元素拷贝进c
c.emplace(a) 使用a构造c中的一个元素
c.begin() c.end()
c.cbegin() c.cend() 返回const_iterator
c.rbegin() c.rend()
对容器使用swap函数,常数时间,因为元素本身没有交换,只是交换了2个容器的内部数据结构。
##容器操作可能会使迭代器失效
一个失效的指针,引用,迭代器将不再表示任何元素。
标准库容器没有给每个容器都定义成员函数来实现一些操作,而是定了一组泛型算法,这些算法大多数独立于任何特定的容器,是通用的。
##unique,去重,但是没有真正的删除元素,只是覆盖相邻的重复元素,使得
##不重复元素出现在序列开始的部分,之后的位置的元素的值未知
##比如unique(a.begin(),a.end())
##返回指向不重复区域之后一个位置的迭代器
#lambda表达式
可以将lambda表达式理解为一个未命名的内联函数
##与函数相同之处:具有一个返回类型,一个参数列表,一个函数体
##与函数不同指出:lambda可能定义在函数内部
##形式
##[capture list]/(parameter list/) ->return type /{function body/}
##python的形式是:lambda x : f(x)
##capture list,捕获列表,是一个lambda所在函数中定义的局部变量的列表,常为空
##可以忽略参数列表和返回类型,但是capture list和函数体不能忽略
auto f = [] {return 42;}
调用和普通函数一样使用调用运算符:f()
lambda的参数列表不能有默认参数
[]/(const string &a,const string &b/){return a.size() < b.size() ; }
##一个lambda只有在其捕获列表中捕获一个它所在函数中的局部变量,才能在函数体中使用该变量。
##捕获列表只用于局部非static变量,lambda可以直接使用局部static变量和在它所在函数之外声明的名字。
##捕获方式可以是值或引用。
##值捕获时,与函数传参不同,被捕获的变量的值是在lambda创建时拷贝,而不是调用时拷贝。
##引用捕获时,使用的是引用所绑定的对象。这种情况药保证在lambda执行时变量依然存在。
##可以从一个函数返回lambda。
隐式捕获:可以让编译器根据lambda体中的代码来推断我们使用的变量。
[=]采用值捕获
[&]采用引用捕获
默认对于一个值被拷贝的变量,lambda不会改变其值,如果想要改变一个被捕获的变量的值,必须在参数列表后加上mutable关键字。
#动态内存
动态分配的对象的生存期与它们在哪里创建是无关的,只有当显式地被释放时,这些对象才会销毁。
new 在动态内存中为对象分配空间并返回一个指向该对象的指针。
delete 接受一个动态对象的指针,销毁该对象,并释放与之关联的内存。
2种智能指针,负责自动释放所指向的对象。
#拷贝控制
拷贝构造函数,拷贝赋值运算符
移动构造函数,移动赋值运算符
析构函数
##拷贝构造函数
如果一个构造函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值,则此构造函数是拷贝构造函数。
拷贝构造函数的第一个参数必须是一个引用类型。
一般情况,编译器合成的拷贝构造函数会将其参数的非static成员逐个拷贝到正在创建的对象中。
每个成员的类型决定了它如何拷贝:对类类型的成员,会使用其拷贝构造函数,内置类型的成员则直接拷贝,虽然不能直接拷贝数组,但合成拷贝构造函数会逐元素地拷贝一个数组类型的成员。
拷贝初始化发生的情况:
用=定义变量
将一个对象作为实参传递给一个非引用类型的形参
从一个返回类型为非引用类型的函数返回一个对象
用花括号列表初始化一个数组中的元素或一个聚合类中的成员
某些类类型还会对它们所分配的对象使用拷贝初始化
##拷贝构造函数被用来初始化非引用类类型参数,这一特性解释了为什么拷贝构造函数自己的参数必须是引用类型
因为如果其参数不是引用类型,则调用永远不会成功-为了调用拷贝构造函数,我们必须拷贝它的实参,但为了拷贝实参,我们又需要调用拷贝构造函数,无限循环。
##拷贝赋值运算符
C & operator=(const C&);
和拷贝构造函数一样,如果类没有定义自己的拷贝赋值运算符,编译器会为它合成一个。
重载运算符本质上是函数,也有一个返回类型和一个参数列表。
##赋值运算符通常返回一个指向其左侧运算对象的引用。
##析构函数
析构函数释放对象使用的资源,并销毁对象的非static数据成员。
##名字由~接类名购成,没有返回值,不接受参数。
由于析构函数不接受参数,不能被重载。
在一个析构函数中,首先执行函数体,然后销毁成员。成员按在类中出现的顺序的逆序销毁。
##隐式销毁一个内置指针类型的成员不会delete它所指向的对象。
##当指向一个对象的引用或指针离开作用域时,析构函数不会执行。
无论何时一个对象被销毁,就会自动调用其析构函数。
析构函数体自身并不直接销毁成员,成员是在析构函数体之后隐含的析构阶段被销毁的。
##定义删除的函数
我们可以通过将拷贝构造函数和拷贝赋值运算符定义为deleted function来阻止拷贝,deleted function是这样一种函数:我们虽然声明了它们,但是不能以任何方式使用它们。在函数的参数列表后面加=delete来指出我们希望将它定义为deleted。
析构函数定义为deleted function的话,这个类对象无法销毁。
如果一个类有数据成员不能默认构造,拷贝,复制或销毁,则对应的成员函数将被定义为删除的。
也可以通过将其拷贝构造函数和拷贝赋值运算符声明为private来阻止拷贝。
编写赋值运算符时,需要注意:
如果将一个对象赋予它自身,赋值运算符必须能正确工作。
大多数赋值运算符组合了析构函数和拷贝构造函数的工作。
#重载运算与类型转换
重载的运算符是具有特殊名字的函数:operator + 运算符号 组成名字。
当然也包含返回类型,参数列表,函数体。
重载运算符的参数数量与该运算符作用的运算对象数量一样多,对于二元运算符来说,左侧运算对象传递给第1个参数,右侧运算对象传递给第2个参数,是成员函数的话显式参数就要比总数少一个,因为它的左侧运算对象绑定到隐式的this指针上。
除了operator()外,其他重载运算符不能有默认实参。
对于一个运算符函数来说,它或者是类的成员,或者至少含有一个类类型的参数。
只能重载已有的运算符,不能发明新的运算符。
+-*&既是一元也是二元,从参数的数量可以推断定义的是哪种运算符。
当我们把运算符定义成成员函数时,它的左侧运算对象必须是运算符所属类的一个对象。
通常,输出运算符的<<第一个形参是一个非常量ostream对象的引用,是非常量是因为流写入内容会改变其状态,是引用是因为我们无法直接复制一个ostream对象。
##输入输出运算符必须是非成员函数。
假设输入输出运算符是某个类的成员,则它们也必须是istream和ostream的成员,但是,这2个类属于标准库,我们无法给标准库中的类添加任何成员。
##输入运算符必须处理输入可能失败的情况,而输出运算符不需要。
下标运算符[]必须是成员函数。
递增递减运算符的重载都有前置和后置版本,而且函数都为:
operator++(){} (函数重载不考虑返回值类型)
这样就没有办法重载了,因为重载没有办法区分,为了解决这个问题,后置版本接受一个额外的不被使用的int类型的形参,当我们使用后置运算符时,编译器为这个形参提供一个值为0的实参。尽管从语法上来说函数可以使用这个形参,但是在实际过程中不会去使用它。因为这个形参的唯一作用就是区分前置版本和后置版本的函数。
##前置一般返回的是一个引用,后置一般返回的是一个值。
##函数调用运算符
如果类重载了函数调用运算符,我们可以像使用函数一样使用该类的对象。
operator()
函数调用运算符必须是成员函数。
如果类定义了调用运算符,则该类的对象称作函数对象。
##lambda是函数对象
当我们编写了一个lambda后,编译器将该表达式翻译成一个未命名类的未命名对象,在lambda表达式产生的类中含有一个重载的函数调用运算符。
对于lambda的捕获列表,如果是值捕获,以类的数据成员的形式在类中,如果是引用捕获,编译器可以直接使用该引用。
lambda表达式产生的类不含默认构造函数,赋值运算符和默认析构函数。
C++的可调用的对象:函数,函数指针,lambda表达式,bind创建的对象,重载了函数调用运算符的类。
##类类型转换
转换构造函数和类型转换运算符
类型转换运算符是类的一种特殊成员函数,它负责将一个类类型的值转换成其他类型
operator type() const;
类型转换运算符面向任意类型(除了void)进行定义,只要该类型能作为函数的返回类型,所以,不允许转换成数组或者函数类型,但允许转换成指针或者引用类型。
类类型转换运算符没有显式的返回类型,也没有形参,而且必须定义成类的成员函数。
##类型转换运算符是隐式执行的,所以无法传递实参
##显式的类型转换运算符
explicit operator type() const;
编译器通常不会将一个显式的类型转换运算符用于隐式类型转换
当类型转换运算符是显式时,我们也能执行类型转换,不过必须通过显式的强制类型转换才行。
例外:当表达式被用作条件,编译器会将显式的类型转换自动应用于它。
#面向对象程序设计
3个基本概念:数据抽象,继承,动态绑定。
数据抽象:将类的接口和实现分离
继承:相似关系
动态绑定:一定程度忽略相似类型的区别,以统一的方式使用它们的对象
##继承
通过继承联系在一起的类购成一种层次关系。通常在根部有一个基类(base class)。
其他类则直接或间接从基类继承,继承得到的类称为派生类(derived class)。
基类负责定义在层次关系中所有类共同拥有的成员,而每个派生类定义各自特有的成员。
对于某些函数,基类希望它的派生类各自定义适合自身的版本,此时基类就将这些函数声明成虚函数(virtual function)。
派生类必须通过使用类派生列表(class derivation list) 明确指出它是从哪些基类继承而来的。
形式:在类名后一个冒号:,然后是逗号分隔的基类列表,基类前面可以有访问说明符(public,private等)
派生类必须在其内部对所有重新定义的虚函数进行声明。
##C++中,我们使用基类的引用或指针调用一个虚函数时将发生动态绑定(dynamic binding)
作为继承关系中根节点的类通常都会定义一个虚析构函数,即使该函数不执行任何实际操作也要。
##任何构造函数之外的非静态函数都可以是虚函数。
virtual只能出现在类内部的声明语句之前,不能用于类外部的函数定义。
##如果基类把一个函数声明为virtual,则该函数在派生类中隐式地也是virtual
如果成员函数没有被声明为虚函数,则其解析过程发生在编译时而不是运行时。
派生类可以继承定义在基类中的成员,但是派生类的成员函数不一定有权限访问从基类继承而来的成员。
##派生类能访问public,protected,但是不能访问private
##如果派生类没有覆盖其基类中的某个虚函数,该虚函数的行为类似于其他的普通成员,派生类会直接继承其在基类中的版本。
##派生类到基类的转换(编译器隐式执行)
我们可以将派生类的对象当成基类对象来使用,也能将基类的指针或引用绑定到派生类对象的基类部分上。
虽然派生类对象中含有从基类继承而来的成员,但是派生类不能直接初始化这些成员,需要使用基类的构造函数来初始化它们。
##每个类控制它自己的成员初始化过程。
派生类首先初始化基类的部分,然后按照声明的顺序依次初始化派生类的成员。
派生类可以访问基类的public 和 protected
##如果基类定义了一个静态成员,则在整个继承体系中只存在该成员的唯一定义,无论从基类中派生出多少个派生类,对于每个静态成员来说都只存在唯一的实例。
##一个类不能派生它本身。
##C++11,如果我们不想让一个类作为一个基类,我们可以在类定义的类名后面加final
class A final {};
派生类向基类的自动类型转换只对指针或引用类型有效,在派生类类型和基类类型之间不存在这样的转换。
当我们用一个派生类对象为一个基类对象初始化或赋值时,只有该派生类对象中的基类部分会被拷贝,移动或赋值,它的派生类部分将被忽略掉。
1.基类指针指向派生类:
安全,但是除了虚函数,这个指针只能调用基类的成员函数,和只能使用基类有的数据成员。
2.派生类指针指向基类:
编译错误
##虚函数
在C++中,当我们使用基类的引用或指针调用一个虚成员函数时会执行动态绑定,因为我们直到运行时才能知道到底调用了哪个版本的虚函数,所以所有虚函数都必须有定义。
当某个虚函数通过指针或引用调用时,编译器产生的代码直到运行时才能确定应该调用哪个版本的函数,被调用的函数是与绑定到指针或引用上的对象的动态类型相匹配的那一个。
##动态绑定只有当我们通过指针或引用调用虚函数的时候才会发生。
一个派生类的函数如果覆盖了某个继承而来的虚函数,则它的形参类型,返回类型必须与被它覆盖的基类函数完全一致。不过,当类的虚函数返回类型是类本身的指针或引用时例外。
派生类如果定义了一个函数与基类中虚函数的名字相同但是形参列表不同,编译器将认为新定义的这个函数与基类中原有的函数是相互独立的。
##override
如果我们在派生类中写一个虚函数但是形参列表写错了的时候,编译器会认为我们只是在写一个独立的函数,这种错误比较难发现,所以C++11多了一个override关键字。
用override标记了某个函数,但是该函数没有覆盖某个已存在的虚函数,这时候编译器会报错。
##final
如果我们已经把函数定义成final了,则之后任何尝试覆盖该函数的操作都将引发错误。
override和final说明符出现在形参列表之后。
虚函数也可以有默认实参。如果某次函数调用了默认实参,则该实参由本次调用的静态类型决定。
如果我们希望对虚函数的调用不要动态绑定,而是强迫执行虚函数的某个特定版本的话,作用域运算符可以实现这个目的。
##pure virtual
一个纯虚函数不用定义,我们通过在声明语句的分号之前加上=0就可以将一个虚函数说明为纯虚函数。
##=0只能出现在类内部的虚函数声明语句处,纯虚函数的函数体如果要定义的话,必须定义在类的外部。
也就是说,我们不能在一个类的内部为一个=0的函数提供函数体。
定义纯虚函数是为了实现一个接口。
含有或者未经覆盖直接继承纯虚函数的类是抽象基类(abstract base class),抽象基类负责定义接口,后续的其他类可以覆盖该接口。我们不能直接创建一个抽象基类的对象。
##在一个派生类内部,派生类访问说明符对于派生类的成员和友元能否访问其直接基类的成员没有影响,对基类成员的访问权限只与基类中的访问说明符有关。
##派生访问说明符的目的是控制派生类用户对于基类用成员的访问权限。
##不能继承友元关系,每个类负责控制各自成员的访问权限。
#using语句还可以改变访问权限,各种规则。
##默认情况下,使用class关键字定义的派生类是私有继承,使用struct关键字定义的派生类是公有继承的。
##函数调用的解析过程
如果我们调用p->mem() ,或者obj.mem() ,步骤:
1.首先确定p或obj的静态类型,因为调用的是一个成员,所以该类型必须是类类型。
2.在p或obj的静态类型对应的类中查找mem,如果找不到,依次在直接基类中不断查找直到到达继承链的顶端,如果仍然找不到,编译器报错。
3.如果找到了mem,进行常规的类型检查,以确认对于当前找到的mem,这次调用是否合法
4.如果调用合法,编译器根据调用的是否是虚函数而产生不同的代码:
如果mem是虚函数 && 通过引用或指针调用的,编译器产生的代码将在运行时确定运行虚函数的哪个版本,依据是对象的动态类型。
如果mem不是虚函数或者是通过对象进行的调用,则编译器产生一个常规函数调用。
析构函数的虚属性也会被继承。
##如果基类的析构函数不是虚函数,则delete一个指向派生类对象的基类指针将产生未定义的行为
对象销毁的顺序和其创建的顺序相反,派生类析构函数首先执行,然后是基类的析构函数。
#模板与泛型编程
一个函数模板就是一个公式,用来生成针对特定类型的函数版本。
template< typename T> or
template< class T>
含义相同,一个模板参数列表也可以同时使用typename和class
模板定义中,模板参数列表不能为空。
T的实际类型在编译时根据compare的使用情况来确定。
当我们调用一个函数模板时,编译器通常用函数实参来为我们推断模板实参。
然后编译器用推断出的模板参数来为我们生成一个特定版本的函数。
这些编译器生成的版本通常被称为模板的实例。
还可以在模板中定义非类型参数,一个非类型参数表示一个值而不是一个类型,我们通过一个特定的类型名来指定非类型参数。
当编译器遇到一个模板定义时,它并不生成代码,只有当我们实例化出模板的一个特定版本时,编译器才会生成代码。
函数模板和类模板成员函数的定义通常放在头文件中。
一个类模板的每个实例都形成一个独立的类。
和其他任何类相同,我们可以在类模板内部和类模板外部为其定义成员函数,且定义在类模板内的成员函数被隐式声明为内联函数。
默认,一个类模板的成员函数只有当程序用到它的时候才进行实例化。
当一个类包含一个友元声明时,类与友元各自是否是模板是互相无关的,如果一个类模板包含一个非模板友元,则友元被授权可以访问所有模板实例,如果友元自身是模板,类可以授权给所有友元模板实例,也可以只授权给特定实例。