cpp知识点总结
0. 杂类
cin/cout
提速
使用ios::sync_with_stdio(false);加速输入输出速度
<<的优先级
运算符<<和>> 的优先级比表达式中有的运算符要高,有时候要加上括号
1 | cout<<(a<b)<<endl; |
字符函数库cctype
判断一个字符ch是什么类型的:
1 | isalpha(ch);//字母 |
比直接判断ASCII码更加容易使用(有的字符格式没有用ASCII码存就只能这么判断)
文件输入输出IO
包含库文件fstream后可以进行文件的读写
创建ofstream对象和ifstream对象来分别对文件进行写和读用法和cout cin类似;
使用ofstream对象中的open()方法可以打开一个文件
表达式
顺序点
当到达顺序点时,会结算所有副作用(比如自增自减)
一个完整表达式的末尾是一个顺序点(完整表达式代表这个表达式不是其他表达式的子表达式)
逻辑运算符||和&&也是顺序点
函数
内联函数
内联函数在声明和定义的时候都需要加上inline,并且和普通的函数不同,内联函数的定义必须在被调用之前
内联函数在声明和定义的时候都需要加上inline,并且和普通的函数不同,内联函数的定义必须在被调用之前
尽可能地使用const
使用const能够避免无意中修改数据的编程错误
使用const使函数能够处理const和非const实参,否则将只能接受非const数据
使用consti引用能够使函数正确生成并使用临时变量
提升可读性
字符串/字符指针作函数形参
在函数头的形参定义中使用char p[]
,和char *p
往往有相同作用,
但是当只希望实参传入字符串时候使用char p[]
,希望传入一个字符指针时使用char *p
比较好
4.复合类型
4.1字符串字面量
4.1.1其他形式的字符串字面量
在字符串的前面加上L u 和U等(分别表示widechar,char16,char32)
在字符串的前面加上L u 和U等(分别表示widechar,char16,char32)
1 | wchar_t str1[] = L"w_char string"; |
4.1.2原始字符串
一般的原始字符串的格式为R"(...)"
。诸如\n
等转义字符会被解释为两个字符\
和n
1 | cout<<R"(this is a "raw" string \n)"<<std::endl; |
可以通过在双引号”和括号(之间加入任意字符,这样只有遇见与之相同的字符时才会代表整个字符串的结束:(当字符串中出现小括号时使用)
1 | cout<<R"+*(this is a string()())+*"<<std::endl; |
4.2指针和自由存储空间
*运算符 被称为间接值(indirect value) 或解除引用(derefencing)运算符。
指针用于在运行阶段分配未命名的内存以存储值。
1 | int * p = new int; |
4.2.1自由储存空间
使用int a;
生成的变量会被存储在栈(stack)中
而使用new
生成的数据对象则会被储存在堆(heap)或称自由存储区(free store)中
使用delete
来释放内存
1 | int * p = new int; |
这样不会删除p指针本身,只会释放p指针指向的地址
4.2.2动态数组
1 | int * psome = new int [10]; |
假设有数组 arr[10];
那么 arr和&arr的值是相同的,但是表达的意义不一样,arr表示首元素的地址,&arr表示整个数组的地址
可以这样定义一个指向数组本身的指针:(p是 short[10]类型的指针)
1 | short arr[10]; |
使用cout打印时,如果使用的变量是数组名,数组是字符数组,则打印字符(串),是其他类型数组(如int数组,则打印首地址)
1 | int ins[10] = {1,2,3,4,5}; |
如果需要打印数组的首地址,那么可以对指针进行类型转换
1 | std::cout<<(int*)chs<<endl; |
4.2.3指针和const
常量指针const int* ps = &a;
指针常量int * const finger = &b;
区别在于,const的位置不同。
当const位于*前面时候,不能通过ps修改a的值,但是可以修改ps所指向的地址(可以改为ps=&b)
当const位于*后面时,则恰恰相反, 可以通过ps修改a的值,但是不能更换ps指向的地址。
4.3数组的替代品vector/array
模板类 vector,可以实现可变数组的效果,内部使用new和delete动态分配内存
需要包含文件 <vector>
1 | vector<int> vi;//创建一个0长的int类型数组 |
模板类 array(c11)
对于长度固定的数组,使用array类比vector更佳
1 | array<int,5> ai;//长度为5的int类型数组 |
7.函数
7.1函数和二维数组
数组名被视为数组的首地址:
比如一个二维数组data和函数sum
1 | int data[10][20]; |
那么函数的原型应该为
1 | int sum(int (*arr2)[4],int size); |
arr2是指向一个由4个int组成的数组的指针(所以他是个数组指针)
7.2函数指针
7.2.1获取函数的地址
只使用函数名(后面不加参数)就相当于函数的地址
比如对于函数think(a)
,那么think
就代表了这个函数的地址
1 | process1(think); |
对于第一行代码,process1
以think
的地址为实参进行传递
第二行代码,则会先运行think()
函数,然后将他的返回值传递给process2
7.2.2声明指向函数的指针
1 | double (*pf)(int); |
pf表示一个特征标为int,返回值为double的函数指针 。
必须用括号括住(*pf), 如果不带括号会变成声明一个函数。
将一个函数指针指向函数时,必须保证二者的特征标和返回值都相同
7.2.3使用函数指针
在第8行中,(*pf)(5)
也可以替换为pf(5)
,但是前者给出了提示:代码正在使用函数指针
1 | double pam(int); |
7.2.4深入探讨函数指针
1.对于以下特征标,他们看似不同,实则是相同的
1 | const double * f1(const double ar[],int n); |
在函数原型中,可以省去标识符,因此 const double ar[]
可以简化为const double []
2.由函数指针组成的数组
1 | const double* (*pa[3])(const double* ,int)={f1,f2,f3}; |
需要注意的是[3]
的位置,pa是一个包含三个元素的数组,所以首先要使用pa[3],然后由于[]的优先级高于的优先级,所以不需要加括号,所以 **pa[3]**是一个包含三个指针的数组,之后小括号括住,左右两边分别加上返回值和特征标
(C11)这里不能使用自动类型auto来自动获取pa的类型(因为auto只能用于单值初始化,不能用于初始化列表),但是可以在已经声明pa之后,使用自动类型来初始化同样的指针数组:
1 | auto pb = pa; |
8.函数探幽
8.1内联函数
内联函数比常规函数运行更快,但是会占用更多的内存,如果程序在10个不同的地方调用了内联函数,那么就会产生10个副本。
内联函数不能递归!!!
程序员请求函数声明为内联函数时,编译器不一定会满足需求,当编译器认为函数过大或者注意到函数调用了他自己时,不会将他作为内联函数 。
8.2引用变量
8.2.1将引用作为函数参数
将引用作为函数参数成为按引用传递,按引用传递允许被调用的函数能够访问调用函数中的变量
相对于按值传递(需要拷贝整份数据),按引用传递可以提高效率,节省空间,并且避免了使用指针
8.3默认参数
8.3.1声明和定义带默认参数的函数
注意只有在函数声明时需要提供默认参数,函数定义时和没有默认参数时完全相同
并且带参数的形参必须在不带参数的形参之后
8.3.2调用带默认参数的函数
1 | int func(int a=1,int b=2,int c=3); |
优先使用实参,实参数量不足时使用默认参数
1 | func();//func(1,2,3) |
8.4函数重载
函数重载让你能够使用多个同名的函数,只要他们的特征标的种类和顺序不完全相同
8.4.1注意事项
注意,编译器在检查特征标时,将类型引用和类型本身视为同一个特征标
1 | int func1(int a){return 1;} |
如果传入一个int类型的实参,那么会由于二义性导致编译失败
8.4.2重载引用参数
类设计和STL经常使用引用参数,因此知道不同引用类型的重载很有用,比如下面三个原型
1 | void sink(double & r1); |
左值引用参数r1与可修改的左值参数(如double)匹配
const左值引用参数r2与可修改的左值参数、const左值参数和右值参数(如两个double值的和)匹配
右值引用参数r3与右值匹配。
注意到r1和r3匹配的参数都与r2匹配
所以如果重载这三个函数,会调用最合适的版本:
1 | void stove(double & r1); |
这让我们能够根据参数是左值,const还是右值来定制函数的行为
1 | stove(x);//调用stove(double & r1); |
如果没用定义函数stove(double &&),stove(x+pi)则会调用函数stove(double &)
8.4.3何时使用函数模板
只有当函数基本上执行相同任务,但使用不同形式的数据时进行函数重载
8.5函数模板
如果需要将同一种算法应用于不同类型的函数,使用模板来节省需要编写的代码量,比并且提高代码的重用性
8.5.1声明和定义函数模板
在C++98之前,用关键字class来替代typename创建模板,如果不考虑向下兼容性,那么最好使用typename来创建模板
模板并不创建任何函数,只是在告诉编译器如何定义函数,当需要用来交换int的函数时,编译器将按照模板来创建相应的函数,同样需要用来交换double的函数时,则会再创建另一个函数
注意:函数模板并不能缩短可执行程序,最终的代码不会包含任何模板,使用模板的好处是可以使定义更简单,更可靠
通常将模板放在头文件中
1 | void Swap(AnyType &a,AnyType &b) |
8.5.2重载的模板
1 | template <typename T> |
第10行代码传入了两个int类型的参数,使用第一个模板
11行传入两个int*类型的参数,但是因为只有两个参数,不符合第二个模板,所以仍然使用第一个模板
12行传入两个int*类型的参数和一个int类型的参数,使用第二个模板
8.5.2模板的局限性
同一个模板在接收不同的数据类型时会用相同的代码去执行,但有时候某些运算是不适用于所有数据类型的,
如下面代码
1 |
|
当数据类型是地址时,模板中的>变得不再适用。
有时会出现意想不到的结果。比如运算符*在处理int,double等类型时,表示乘法,而如果数据类型是一个指针,那么将会变成解引用的意思
8.5.3显式具体化
当编译器找到与函数调用匹配的具体化定义的时候,将使用该定义,而不再寻找模板
对于给定的函数名,可以有非模板函数,模板函数和显示具体化模板函数以及它们的重载版本
显示具体化的原型和定义应该以template<>打头,并通过名称来指出类型
具体化优先于常规模板,非模板函数又优先于具体化和常规模板
1 | class job{}; |
8.5.4具体化和实例化
最初,,编译器只能通过隐式实例化来使用模板生成函数定义,但现在C++还允许显式实例化,这意味着可以直接命令编译器创建特定的实例,如Swap
1 | template void Swap<int>(int,int);//显示实例化 |
还有一种叫显示具体化,声明如下
1 | template <> void Swap<int>(int,int);//显示具体化 |
显示具体化的意思是“不要使用Swap()模板来生成函数定义,而应该使用专门为int类型显式地定义的函数定义”。这些原型必须又自己的函数定义,显式具体化声明在关键字template后面包含一个<>,而显式具体化没有
警告:在同一文件(或者转换单元)中,同时使用同一种类型的显式实例和显式具体化将出错
创建自定义选择:
在有些情况下,可以通过编写合适的函数调用来引导编译器做出自己希望的选择。
在main函数中,17行调用#2,18行调用double类型的#1,19行为int类型的#1,20为int类型的#1(哪怕传入的是double类型的参数)
1 |
|
9.内存模型和名称空间
9.1单独编译
不要将函数定义或者变量声明放在头文件里面,否则当两个以上的文件包含该头文件的时候,除非函数是内联的,否则将会出错
不要使用include来包含源文件代码,这样做会导致多重声明
翻译单元(一般为一个文件为一个翻译单元)
9.2存储持续性,作用域和链接性
c++使用了三种(c++11之后是4种)方案来存储数据,区别在于数据保存在内存中的时间
自动存储持续性:在函数中声明的变量(包括函数参数),在程序开始执行函数或者代码块的时候被创建,执行完函数或者代码块的时候被释放 。
静态存储持续性:在函数外定义的变量和使用关键字static定义的变量都为静态的,他们在程序的整个运行过程中都存在
线程存储持续性(c++11):变量使用thread_local声明的,那么他的生命周期就和他所属的线程一样长(并行编程)
动态存储持续性:用new运算符分配的内存会一直存在,直到程序使用delete运算符来释放掉或者程序结束。这种内存也称为动态内存(free store)或者堆(heap)
9.2.1作用域和链接
作用域描述了名称在文件(翻译单元)中多大范围可见,链接性描述了名称如何在不同的单元间共享
链接性为外部的名称可以在文件之间共享,里娜姬恶行为内部的名称只能由一个文件中的函数共享,自动变量的名称没有链接性,他们不能共享。
在名称空间中声明的变量的作用域为整个名称空间(全局作用域就是名称空间作用域的特例)
9.2.2自动存储持续性
在函数中创建的变量默认为自动类型变量,
auto关键字:在c++11之前可以使用auto在定义时标明这是个自动类型的变量(由于只能用于默认是自动的变量,所以没有实际作用,只用来提醒),在C++11之后,auto变为一种类型,用来自动判断变量应为什么类型
1 | int (*p)[10];//一个数组指针 |
register寄存器变量
使用register定义的变量是寄存器变量,他会建议编译器使用CPU寄存器来存储自动变量,在C++11中,失去了这种提示作用,register只能用来标明这是个自动类型的变量(虽然这个变量本身就是自动类型的)
9.2.3静态持续变量
static定义的函数是静态的,存储在静态存储区,有两个主要用法:
1.在函数中定义变量时加上static,来改变这个变量的生存周期
1 |
|
2.在全局区定义变量时加上static,会将链接性从外部改为内部,即其他文件(翻译单元)不可见。
1 |
|
9.2.4静态持续性,外部链接性
单定义规则
C++有两种变量声明:一种是定义声明(defining declaration)或 称定义(definition);另一种是引用声明(referencing declaration)或称声明(declaration)。
一个变量可以有多个声明,但只能有一个定义。
使用关键字extern来进行引用声明,(只声明不引用)不进行初始化。否则就会定义。
由于单定义规则,所以只需要在一个文件包含该变量的定义,在其他所有文件中用extern声明他。
tips:如果在定义时对一个变量进行了初始化,那么即使加上extern那么这也是一个定义而不是声明
9.2.5静态持续性,内部链接性
如果一个文件中,同时有外部静态变量和static修饰的内部静态变量,那么内部静态变量会将外部静态变量隐藏
1 | //file1 |
9.2.6静态存储持续性,无链接性
也就是静态局部变量。(在函数中使用static定义的变量)
9.2.7说明符和限定符
存储说明符:
auto(在C++11中已经不再是说明符),register(C++11中无实际作用),static,extern,thread_local(C++11新增,用于多线程中),mutable(与const对应,含义大致相反)
- cv-限定符:const,volatile
const表示代码无法改变该值,volatile表示即使代码没有对内存单元进行修改,它的值仍然可能会改变,主要用来提醒编译器不要对该变量进行编译优化,直接存取该地址的值,以免出错。一般用在1)并行设备的硬件寄存器中。2)中断服务程序中修改的供其他程序检测的变量,需要加volatile3)多任务下各任务间共享的标志。4)存储器映射的硬件寄存器。
- multable
它表示,即使结构体或者类的变量被定义为const,但其中某个被multable修饰的成员变量仍然是可以修改的。
- const!!
对于一个全局变量,使用const修饰后会将他的链接性从外部变为内部(就像加了static一样)。这样方便将const的变量定义放在头文件中,此时内部链接性会保证不会违反单一定义原则。
如果想要让一个常量的链接性为外部,那么可以在加上extern,但只需要对一个文件中的const变量进行初始化
1 | const int one_file = 1; |
9.2.8函数和链接性
函数也有链接性,,但是比变量可选择的范围要小,由于C++不允许在函数中定义另一个函数,所以函数的存储性都是静态的。在默认情况下函数的链接性为外部的。也可以使用static将一个函数的链接性转换为内部的。在函数的声明和定义中都需要有static关键字
9.2.9语言链接性
链接程序要求每一个函数都有自己的符号名,在C++中存在重载,所以会存在相同名字的函数,但是相同函数名的重载函数也有不同的符号名。C++将重载函数名字转换为符号名的过程叫做C++语言链接性。
可以用函数原型来指出使用什么约定。
1 | extern "C" void spiff(int);//使用C的语言链接约定 |
9.3名称空间
名称空间可以是全局的,也可以位于另一个名称空间之中,但不能位于代码块之中。
在默认情况下,名称空间的链接性是外部的,除非他引用了变量。
9.3.1using声明和using编译指令
使用using声明会将名称添加到局部声明区域内,会和其他局部变量一样,将覆盖同名的全局变量
using编译指令会将整个名称空间的所有名称都可用。
1 | namespace aaa{ |
安全性比较:
using声明只会引入一个变量,当出现同名时,会导致报错提醒。更安全
using编译指令则会全部导入变量名字,如果遇到同名则会跳过导入(局部版本隐藏掉名字空间版本)不会报错,这样可能会出现意想不到的结果
所以在导入名字时,最好使用using声明(using std::cout)或者作用域解析运算符(std::cout)
10.对象和类
10.1抽象和类
对于一个类型的确定,需要完成三项工作:
1.决定数据对象需要的内存数量
2.决定如何解释内存的位
3.决定可使用数据对象执行的操作或方法
对于内置类型,有关操作隐藏在编译器中,对于用户自定义类型,需要自己定义这些内容。
10.1.1访问控制
类提供了三种访问控制,用来表示成员变量或者成员函数能够怎样被访问(默认为private)
private(私有):只能通过公有成员函数或者友元函数来访问对象的私有成员
protected(保护):允许本类和本类的派生类访问。
public(公有)允许任意访问。
类设计应该尽可能将公有接口和实现细节分开,(数据隐藏),这让类在被使用时无需关心内部实现,只需注重如何使用接口。
10.1.2类成员函数
注意:成员函数如果在类的声明中就被定义,那么这个函数会自动成为内联函数,而如果在类内声明,类外定义,就是普通的函数。
1 | class person1{ |
10.2类的构造函数和析构函数
10.2.1构造函数的使用
构造函数相对于其他成员函数有以下特点:
1.没有返回值(也不写void)
2.名称和类名一样
3.在实例化对象时自动调用
1 | class person{ |
在构造函数中可以使用初始化列表来初始化数据成员,并且需要注意的是,这些项目被初始化的顺序是他们被声明的顺序而不是在他们在初始化列表中的顺序
初始化列表为在函数头后面跟冒号,然后以项目1 (形参1)的形式表示将形参1赋值给项目1,并且各单元之间用逗号分隔开
1 | person(int height1,int age1):age(age1),height(height1){} |
调用构造函数的两种方法:
1.显式调用:
person p1 = person(1,2)
2.隐式调用
person p1(1,2);
10.2.2析构函数
在对象被释放的时候,会自动调用析构函数,
通常不应该在代码中显示地调用析构函数
使用~加类名来定义析构函数。
当构造函数中用到new来分配空间时,析构函数需要使用delete来释放空间
1 | class person{ |
10.2.3const成员函数
在函数的括号后面加上const的函数是const成员函数,这样的函数不被允许修改他所调用的对象。
1 | public: |
就像尽可能将const引用和指针用作函数形参那样,当一个成员函数不需要修改对象的值时,就应该让他成为const成员函数。
10.2.4其他
上文提到了两种实例化对象并初始化的方法,下面说一下他们之间细节上的区别
1.显式调用:
person p1 = person(1,2)
2.隐式调用
person p1(1,2);
对于第一种调用方法,编译器可以有两种处理情况。一是和第二种隐式调用一样直接生成p1并调用构造函数。二是可能会先生成临时变量,再把这个临时变量赋值给p1,再删除掉临时变量(可能会立刻删除,也可能会等一段时间再删除),删除对象的时候会调用析构函数
再再注意:第一种调用方法可能会不产生临时对象,也可能会产生(编译器决定),但这只是在实例化对象的时候,如果是给对象赋值,那么就肯定会产生临时对象。
1 | person p1(1,2);//不会产生临时变量 |
如果既可以通过初始化,也可以通过赋值的方式来设置对象的值,那么最好使用初始化的方式,因为这样可以提升效率
10.3this指针
所有成员函数(包括构造函数和析构函数,不包括静态成员函数)都内置了一个this指针,this指针指向用来调用函数的成员函数的对象。
对于常成员函数(即在函数括号后面加上const)本质上是将this指针用const修饰。
如果一个方法要引用整个调用对象,那么可以使用this作为调用对象的别名来完成前面方法的定义,(this是调用对象的一个引用而不是一个副本)
1 | const person & comage(const person & p1) const{//compare age |
10.4对象数组
在声明对象数组的时候,和声明一般数据类型的数组相同。直接使用中括号,
1 | person pp[10];//声明和定义对象数组 |
如果没有给定参数,会自动调用默认构造函数。
可以使用构造函数来对数组进行初始化。
1 | person pp[10]{ |
上文中的三个构造函数会分别用来初始化pp[0],pp[1],pp[2].之后的元素会使用默认构造函数来初始化。
初始化队型数组的方案是,先使用默认构造函数创建数组元素,用花括号中的构造函数创建临时对象,再将临时对象复制到响应的元素中,所以所以,想要创建对象数组,就必须有默认构造函数
10.5类作用域
在类内定义的名称的作用域是整个类,作用域为整个类的名称只在该类中是已知的。
在其他情况下使用类中的名称时,需要根据上下文使用直接成员运算符(.)、间接成员运算符(->)、或者作用域解析运算符(::)等
10.5.1作用域为类的常量
有时候需要在类中定义一个常量。在C++11之前下面这样写是无法通过编译的。而在C++11之后也最好不要这么写
1 | class person{ |
可以使用两种其他方法来实现,
1.使用枚举类型
1 | class person{ |
这之中的pi只是一个符号常量,在编译的时候遇到allowage会用18来替换,并且只用用于整数常数(因为枚举类型不允许存在小数)
2.使用static修饰的const成员常量
1 | class person{ |
上面两种方式创建的成员常量都是公共的,不属于任何对象,所以即使他的访问权限是public,也不能使用成员运算符(.和->)来引用常量。而应该用作用域解析符
10.5.2作用域内枚举(C++11)
传统的枚举不允许在不同的枚举中使用相同的关键字。如下代码在c++11之前是不被允许的。如下面的代码;
1 | enum egg{Small,Medium,Large}; |
在C++11之后,可以令枚举的作用域为类(结构stuct也可以),用法是在enum和枚举名称的中间加上class关键字(或者stuct)
1 | //C++11之后允许这么写 |
但是如果一个enum被定义为类枚举类型。那么就不能作为右值赋给int类型。
1 | egg egg1 = egg::Large;//可以~ |
但在必要的时候可以进行显式转换
1 | int temp = int(egg::Large); |
11.使用类
11.1运算符重载
11.1.1运算符重载的使用
为了让对象操作更加美观,可以对运算符进行重载,C++允许将运算符䗏扩展到用户定义的类型当中。比如通过重载来让+(加号运算符)能够对两个对象进行相加。
运算符函数的格式如下:
operatorop(argument-list)
其中括号前的两个字母op应该被替换为一个运算符,括号内写参数列表。 例如:operator+()重载了加号运算符
实例:
1 | class Time{//定义一个时间类,用h,m,s来表示时间 |
在上面的代码中应该注意的是,返回的不能是一个对象的引用,而应该是一个对象,因为在函数中创建的对象在函数结束时就会被释放,引用也就无法起作用。
11.1.2注意事项
1。重载的运算符函数所接受的参数数量必须和运算符本身的操作数一一对应,(比如加法运算符有两个值,那么参数就必须是两个)
2.如果运算符重载函数作为成员函数,那么传入的参数需要减少一个,因为对象本身(*this)始终会作为第一个参数。
3.重载的运算符函数不能所有操作数都是基本类型,必须包含一个自定义类型(编译不会通过,为了防止随意重载导致的严重后果)
4.不能重载以下运算符:
sizeof(sizeof运算符).(一个点,成员运算符) .*(一个点一个星,成员指针运算符)::( 作用域解析运算符)?:(一个问号一个冒号,条件运算符)typeid( 一个RTTI运算符)const_cast(强制类型转换运算符)dynamic_castreinterpret_caststatic_cast
5.以下运算符必须通过成员函数重载:
=(赋值运算符) |
---|
()(左右括号,函数调用运算符) |
[](左右中括号,下标运算符) |
->(通过指针访问类成员的运算符) |
11.2友元
友元有三种,友元类,友元函数,友元成员函数。
让函数成为类的友元,可以让他拥有和成员函数一样的访问权限
11.2.1为什么要使用友元函数
在为类重载二元运算符的时候经常需要用到友元
比如想要重载加号运算符,使其左操作数是一个int类型,右操作数是Time类
1.对于重载运算符成员函数,第一个参数必须是对象本身,所以无法实现目的。
2.对于一个非成员函数的运算符重载函数,则又没有类的访问权限,编写起来复杂
通过友元函数可以解决上述问题:
1 | class Time{//定义一个时间类,用h,m,s来表示时间 |
友元函数不是成员函数,所以不能用成员运算符来调用。
友元函数需要访问哪个类的成员,就成为哪个类友元函数,而其他的参数类则没必要成为友元函数。比如下面的<<重载
1 | friend ostream operator<<(ostream & os,Time & t); |
只需要成为Time的友元即可,不需要成为ostream的友元,因此也就不需要修改ostream的内容。
注意:返回值应该是ostream,而不是引用
*拓展:当一个ostream引用作为参数时(在上文指的是os),默认是cout对象的引用。其他的ostream对象还有cerr(表示将输出发送到标准错误流)
11.3类的自动转换和强制类型转换
先定义一个类作为示例:
1 | class weight{ |
11.3.1赋值运算符的自动转换
对于上面的weight类,有三个构造函数,分别对应了无参,有int参,有double参三种类型。
那么下面这种赋值方法是被允许的
1 | int main(){ |
这种被成为自动类型转换,这样会先生用适合的构造函数来生成一个临时对象,然后将这个临时对象赋值给wei,在上例中调用的是weight(int)构造函数(因为12是int类型的)
有时候这种自动转换会引起一些问题,所以可以在函数的声明中使用了关键字explicit,那么将不支持使用这个函数进行自动类型转换
1 | class weight{ |
当构造函数中没有与赋值运算符右值(上文中是12)匹配的参数类型(即没有weight(int)),那么会寻找勉强合适的构造函数(如weight(double)),然后通过将改变右值的类型(将12的类型改为double)来适配构造函数
但进行这种赋值的前提是1.使用的函数没有被explicit修饰。2.不存在二义性(比如同时有构造函数weight(double)和weight(float),那么12就不知道要转换成double还是float)
11.3.2强制类型转换
可以通过显式地调用构造函数来进行强制类型转换
1 | wei = weight(12); |
11.3.3转换函数
上面讲到通过含有一个参数的构造函数可以将其他类型转换为类类型,那么如果要把类类型转换为其他类型,就需要用到转换函数
格式为:operator typeName()
需要注意的是:1.**转换函数必须是类方法。2.转换函数不能指定返回类型。3.转换函数不能有参数。**
1 | operator double() const {return double(w);} |
警惕二义性:
如果同时定义了转换为int和double(或其他)的转换函数,又使用了如cout等支持多种数据类型的函数,那么会产生二义性导致编译失败。
可以使用显式转换来避免二义性
1 | class weight{ |
避免隐式转换的危害:
像基本类型转换为类类型一样,同样可以使用explicit修饰转换函数来禁用隐式转换
1 | explicit operator double() const {return double(w);} |
也可以使用一个非转换函数来替代转换函数(因为非转换函数不支持隐式转换)
1 | operator int() const {return int(w);} |
12.类和动态内存分配
在类构造函数中使用new运算符在程序运行的时候分配所需要的内存空间
12.1动态内存和类
如果将new应用于类,即在构造函数中用new来为变量分配空间。那么就必须要用到析构函数,并且析构函数中应该有对应的delete来释放new申请的空间
12.1.1静态成员变量
静态成员变量对于一个类,只会生成一个副本,不会随着对象数量的增多而增多。
注意:静态变量不能在类声明中初始化,对于多文件编程,初始化应该在类方法文件中,而不是类声明文件中。(例外:如果这个静态成员变量是cosnt整数类型或者枚举型,那么可以在类声明中初始化)
12.1.2特殊成员变量
C++会自动提供以下成员函数
默认构造函数,如果没有定义构造函数
默认析构函数,如果没有定义
复制构造函数,如果没有定义(也称拷贝构造函数)
赋值运算符,如果没有定义
地址运算符,如果没有定义
如果一个类用到了new[].delete[]等,那么要警惕这些自动生成的成员函数造成的影响。(可能会导致构造函数中new[]和析构函数中delete[] 的调用次数不匹配)
12.2.3复制构造函数
何时使用复制构造函数
1.按值传递对象
2.函数返回对象
3.生成临时对象
深拷贝和浅拷贝(深复制和浅复制)
默认复制函数会进行浅复制(指一个对象一个对象地进行复制),这样有时候会出错(比如将一个指针赋值给另一个指针,或者将一个地址赋值给另一个地址)
深拷贝需要程序员自己编写,区别在于为指针所指的内容分配额外空间来获得一份副本
12.2.4赋值运算符
将一个已有的对象赋值给另一个对象的时候会使用重载的赋值运算符
初始化对象的时候不一样会使用赋值运算符(但总是会调用复制构造函数)
相比复制构造函数,赋值运算符函数与之类似,区别在于
1.由于目标对象可能已经引用了以前分配的数据,所以需要先使用delete[]来释放这些数据
2.函数应当避免自身赋值给自身,否则会导致delete[]删除掉内容
3.函数应该返回一个指向调用类型的引用
4.由于赋值运算符不生成新的对象,所以如果有一个静态变量存储了对象的数量,那么这个静态变量不需要自增。
12.2.5关于临时对象的const问题
在一些函数的进行时,可能创建临时变量(比如重载后的赋值运算符),而临时变量是自带const(无法被修改的),所以如果一个临时变量作为了一个函数的引用参数,那么这个引用参数需要加上const。(因为只有带const的引用才能够同时调用带const类型的变量或者不带const的变量,而不带const的引用只能调用不带const的变量。
所以如果不需要通过一个引用或指针来改变他指向的数据,那么一定要加上const
12.2.6一个关于内存分配的类实例
这个自定义类有基本的存储字符串的功能,并且会用new运算符自动分配合适的内存给字符串。用sum静态成员变量来记录目前实例化的对象数量,
但没有对中括号运算符[],比较运算符<,>,<=,>=,==以及加号运算符+进行重载
1 |
|
12.3返回const对象
12.3.1返回指向const对象的引用
返回引用不会调用复制构造函数,所以运行效率更快
返回的引用必须在调用函数中有定义,否则会导致引用找不到目标。
如果一个引用是const引用,那么返回时也应该加const
12.3.2返回指向非const的引用
在以下两种情况,需要使用返回非const对象的引用,1.重载赋值运算符。2.重载于cout一起使用的<<
第一种这么做能提高效率,第二种则必须这么做
对于第二种情况,如果不返回 ostream & 即 ostream的引用,而是返回ostream本身话,会尝试嗲用ostream的复制构造函数,而ostream类没有公有的复制构造函数。
12.3.3返回对象
当被返回的对是调用函数的局部变量时,由于在调用函数的作用域种没有该变量,所以必须返回对象而不是返回对象的引用
12.3.3返回const对象
对于+加号重载运算符:则需要返回对象,而不是对象的引用。
然而返回的这个对象是可以作为左值被使用的。然而一般+加号运算符作用结果是不能作为左值的。所以可以通过返回一个const修饰的对象来禁止这种行为
1 | //mystring是一个类,str1,str2,str3都是这个类实例化的对象 |
如果加号重载运算符返回的是一个const对象类型,那么就只能够使用第一种方法,而不能使用第二种方法
12.4使用指向对象的指针
如果一个指针指向了一个对象,那么同样可以使用解除引用运算符(*)来获取他指向的对象
12.4.1使用new为对象分配内存
我们可以使用new为对象分配内存(并选择使用哪种构造函数),然后用一个指针来指向这个对象所在的内存
1 | mystring* pstr = new mystring("666"); |
在上文的例子中,先new为这个对象分配内存空间,然后调用构造函数,为对象中的字符串666使用new分配一个空间
当使用delete(注意对象是单个变量,所以不是使用delete[])删除对象的时候,并不会删除在对象内所分配的空间(在这里指的是新建的用来存储666的空间,这一步会交给自定义的析构函数来完成)
12.4.2定位new运算符和对象
定位new运算符能够在分配内存的时候指定内存的位置。
C++prime plus P372
13.类继承
使用类继承来更好地扩展和修改类
13.1基类与派生类
13.1.1派生一个类
被继承的类叫基类,由基类派生出来的类叫派生类。
下面是一个基类和他的派生类的代码示例
1 | class Base{ |
上面代码中,基类是Base类,派生类是Ps类。在Ps的构造函数中使用Base的构造函数来构造继承的Base类的部分。
表明Base是Ps的一个公有继承。使用公有派生,基类的公有成员将成为派生类的公有成员,基类的私有部分也将成为派生类的一部分,但只能够通过基类的公有和保护方法来询问。
在继承的时候:
派生类需要有自己的构造函数
派生类可以根据需要添加额外的成员或者函数
注意:
构造函数必须给新成员和记衡的成员提供数据。
13.1.2访问权限
在创建派生类对象的时候,会先创建基类对象,嗲用基类构造函数。(如果初始化列表中没有调用基类构造函数,那么会自动调用默认构造函数)
所以下面两行代码等效
1 | //代码一 |
在实例化派生类对象时,先调用基类构造函数,再调用派生类构造函数
在销毁派生类对象时,先调用派生类对象的析构函数,然后是基类对象的析构函数
13.1.3派生类和基类之间的关系
1.派生类能够使用基类中的 公有(public)以及保护(protected)类型的成员
2.基类指针可以在不进行显式类型转换的情况下指向派生类对象,基类引用可以不进行显式类型转换的情况下引用派生类对象。
但是基类指针或者引用只能用于调用基类方法。
1 | Ps p1(0,"111"); |
一个函数需要基类对象作为形参时,可以填入一个派生类对象实参
因此可以用派生类对象来给接类对象复制,因为会自动调用隐式重载赋值运算符函数。
1 | const Base & operator=(const Base & b1) {str1 = b1.str1;return b1;} |
13.2继承:is-a关系
继承分为 公有继承(public)保护继承(protected)私有继承(private)
公有继承建立的是is-a关系
公有继承不能建立has-a关系
公有继承不能建立is-like-a关系
- 公有继承不建立is-implemented-as-a(作为…来实现)关系
- 公有继承不建立uses-a关系
私有继承或者包含建立has-a关系
13.3多态公有继承
13.3.1使用虚方法
1.在派生类中重新定义基类的方法,如果派生类和基类中存在特征标相同的函数,派生类会优先调用自己定义的方法。
2.使用虚方法virtual
1 |
|
13.3.2一些注意事项
一、如果想要定义一个虚方法(虚函数),需要在基类的声明前加上virtual(如果定义和声明是分开的,那么定义前面不需要加),在派生类中的函数声明里可以不加virtual(会自动补上)
二、如果一个基类引用 引用了 派生类对象,并且通过引用试图使用一个 在基类和派生类中都被定义的函数 ,那么
1.如果这个函数是虚函数,则调用引用变量所引用的对象的类型所定义函数
2.如果这个函数不是虚函数,则调用引用变量本身类型的函数
在上文中,b是Base类型的引用,他所指向的是Ps类型的对象,调用函数func1(),那么如果func1被定义为虚函数,则使用Ps类中的定义,如果不是虚函数,则使用Base类中的定义
三、使用虚析构函数能够确保释放派生对象的时候,优先调用基类虚构函数,从而确保正确的析构顺序。
13.3.3虚方法应用:基类指针数组
因为使用的是公有继承模型,所以可以使用基类指针(Base)来指向基类对象(Base)或者派生类对象(Ps),当通过基类指针调用虚方法时,会根据它所致的对象类型(基类或者派生类1或者派生类2等等)来动态选择要用哪个方法,这样可以通过创建一个*基类指针数组,来同时管理基类和它的派生类。
13.4静态联编和动态联编
将源代码中的函数调用解释为执行特定函数的过程称为联编
在编译过程中进行联编成为静态联编(早期联编) 在程序执行时进行的联编(虚函数)称为动态联编(晚期联编)
13.4.1指针和引用的类型的兼容性
基类的引用或者指针可以引用派生类对象,不需要进行显式类型转换
将派生类引用或指针转换为基类引用或指针被称为向上强制转换(upcasting),这种转换是可传递的。
相反,将基类指针或者引用转换为派生列指针或引用称为向下强制转换(downcasting),如果不使用显式类型转化,则这种转换时不允许的,因为is-a关系是不可逆的。
13.4.2虚函数的工作原理
编译器处理虚函数的方法:给每个对象添加一个隐藏成员,隐藏成员中保存了一个指向函数地址的指针,这种数组称为”虚函数表“(virtual function table,vbtl)
虚函数表中存储了为类对象进行声明的虚函数的地址。如果派生类提供了虚函数的新的定义,那么虚函数表会保存新函数的地址,如果没有重新定义虚函数,则保存原本的地址,如果定义了新的虚函数,那么这个函数地址也会被i坦加进去。(注意:不论类中包含的虚函数是一个还是十个,都只需在对象中添加一个隐藏的地址成员,指向虚函数表)
在使用虚函数时,在内存和执行速度方面都会有一些成本:
1.每个对象都将增大,增大两位存储地址的空间
2.对于每个类,编译器都将创建一个虚函数地址表(数组)
3.对于每个函数调用,都需要执行一项额外的操作,即到表中查找地址
13.4.3有关虚函数的注意事项
在基类方法的声明中使用关键字virtual可使该方法在基类以及所有的派生类中事虚的
如果定义的类将被用作基类。则应该将那些要在派生类中重新定义类方法声明为虚的
1.构造函数
构造函数不能是虚函数
2.析构函数
析构函数应当是虚函数,除非类不做基类,比如employee是基类,singer是派生类,并添加了一个char 成员,指向new分配的内存,当singer对象过期的时候,必须调用~singer()析构函数来释放内存,因此需要虚析构函数
如果使用默认的静态联编,delete语句会调用~Employee()析构函数,这会释放Singer对象中的Employee部分指向的内存,但不会释放新的类成员指向的内存。
如果使用动态联编,delete会先调用~singer()析构函数,然后调用~Employee()析构函数。
这意味着*,即使基类不需要显式析构函数提供服务,也不应依赖于默认构造函数,而应该提供虚析构函数
即使一个类不做基类来使用,定义为虚析构函数也是可以的,只是会牺牲一些效率。
3.友元
友元不能是虚函数,因为友元不是类成员,如果因为这个原因引起了设计问题,可以通过让友元函数使用虚成员函数来解决
4.没有重新定义
如果派生类没有重新定义,那将使用最新的虚函数,例外情况是基类版本是隐藏的
5.重新定义将隐藏方法
1 | class Base{ |
由于在Ps类中重新定义了虚函数show,所以隐藏了Base类中定义的所有show函数,这导致带参的show函数版本被隐藏,无法通过Ps对象来调用。
由此引出两条经验规则:
1.如果重新定义继承的方法,应该确保与原先的原型完全一致,但如果返回类型是基类引用或者指针,则可以修改为指向派生类的引用或指针。这种特性被称为返回类型协变(covariance of return type) 因为允许返回类型随类型的变化而变化
(注意:这种变化只适用于返回值,不适用于参数)
2.如果基类声明被重载了,那么应该在派生类中重新定义所有重载基类版本
如果只需要重新定义其中部分函数,但还是需要将不需要修改的函数重新定义一下(void Ps::show() const {Base::show();}
13.5访问控制:protected
protected权限和private类似,在类外只能用公有类成员来访问protected部分中的类成员,但区别是派生类的成员可以直接访问基类的protected成员,但不能直接访问基类的私有成员
对于下面的代码:
1 | class Base{ |
虽然dat1是Base中的保护成员,本意希望只通过resetB()来更改dat1的数据,但是通过派生类Ps中的公有成员resetP()间接访问了受保护的dat1成员,这使得dat1似乎成为了公有变量
因此最好对类数据成员采用私有访问控制,不要使用保护访问控制,同时通过基类方法使得派生类能够访问基类数据
13.6抽象基类
如果两个类有一些共性,可以把他们的共性抽象成一个基类,然后由这个基类来派生出这两个类。这样就可以使用基类指针数组来同时管理这两个类。
这个基类可以只指明所需要的函数,而将函数的具体实现交给它的派生类
C++使用纯虚函数来实现这种基类
纯虚函数声明时要在函数头后面加上 =0
1 | class BaseEllipse{ |
如果一个类中包含了纯虚函数,那么就不能创建该类的对象,因此这种类只能用作基类
在基类中的纯虚函数一般只需要声明而不需要定义,但在基类中给出纯虚函数的定义也是可以的。
1 | double BaseEllipse::area() const { return 0.0; } |
包含纯虚函数的类不能被实例化,这种类叫做抽象基类
可以被实例化的类叫做具体(concrete)类
13.7继承和动态内存分配
13.7.1 情况一:派生类不使用new
1 | class BaseDMA{//使用了动态内存分配的基类 |
(上面的代码只给出了函数的声明,没有给出定义)
上面的代码中派生类lacksDMA中没有用new来分配内存,在基类中使用new来动态分配内存。
那么就不需要为派生类lacksDMA定义显式析构函数,复制构造函数和赋值运算符
13.7.2 情况二:派生类使用new
1 | class hasDMA:public BaseDMA{ |
在这种情况下,必须为派生类定义显式析构函数,复制构造函数和复制运算符。
析构函数:hasDMA的析构函数负责释放color管理的内存,然后以来baseDMA的析构函数来释放掉label管理的内存
1 | //析构函数 |
hasDMA复制构造函数只能访问hasDMA的数据,因此他必须调用baseDMA复制构造函数来处理共享的baseDMA数据。(也就是初始化成员列表中的BaseDMA(hs))
在上面的代码中,成员初始化列表将一个hasDMA的引用出啊等你给了BaseDMA复制构造函数,,这用到了类继承中可以向上强制传递的性质。会将hs中的BaseDMA部分传递给BaseDMA的复制构造函数。
1 | //复制构造函数 |
赋值运算符和复制构造函数的代码类似,区别在于需要判断自己等于自己的情况,以及需要在函数块中调用BaseDMA的=运算符
1 | //赋值运算符 |
总之当基类和派生类都采用动态内存分配的时候,派生类的析构函数,复制构造函数,赋值运算符都必须使用相应的基类方法来处理基类元素
13.8类设计回顾
13.8.1要遵循is-a关系
表示is-a关系的方法之一是,无需进行显式类型转换,基类指针就可以指向派生类指针,积累引用可以引用派生类对象。
13.8.2什么不能被继承
构造函数,析构函数,赋值运算符不能被继承
13.8.3赋值运算符
可以将派生类对象赋值给基类对象
但如果相反,想要将基类对象赋值给派生类对象,会有一些前提:
一、包含了这样的构造函数,即对将基类对象转换为派生类对象机型了定义,则可以将基类对象昂赋给派生对象
二、如果派生类定义了用于将基类对象赋给派生对象的赋值运算符,则可以这样做
三、如果上述两个条件都不满足,那么就不能将基类对象赋值给派生对象,除非使用显式强制类型转换。
13.8.4私有成员与保护成员
使用私有数据成员比使用保护数据成员更好,但保护方法很有用
13.8.5虚方法
1.如果希望派生类能够重新定义方法,则应在基类中将方法定义为虚的,这样可以启用动态联编
2.如果不希望基类中的方法被重新定义,则不要声明为虚函数,虽然当这个类被继承的时候仍然可以重新定义,但是这能起到提示作用
3.不适当的代码会阻止动态联编
比如下面两个函数(其中Base是基类,Ps是派生类。二者有虚函数View();分别使用show和inadequate两个函数来调用View函数)
1 | void show(const Base & rba){ |
第一个函数按引用传递对象,第二个函数按值传递对象,传递的是派生类Ps的对象t。
1.在show中,由于Base类的引用可以引用Ps类的对象,所以rba所引用的仍然是一个Ps类对象。由于虚方法的特性,ba.View()调用的是Ps版本的View()函数
2.在inadequate中,传递的实参是Ps类的对象,会先转换成Base类对象再传递给形参p,所以ba是一个Base类对象,因此ba,View()是Base版本的View函数
14.C++中的代码重用
has-a关系通常可以用以下两种方法来实现:
一是一个类中的数据成员可以是另一个类的对象,这种方法被称为包含(containment)、组合(composition)或层次化(layering)。
另一种方法是使用私有继承或保护继承。
14.1包含对象成员的类
在下面的代码中,student类包含了string类和score类的对象成员,是is-a关系
1 | class score{ |
14.2私有继承
使用私有继承,基类的公有方法将称为派生类的私有方法,
派生类不继承基类的接口,这种不完全继承是has-a关系的一部分。
私有继承提供的特性和包含相同:获得实现,但不获得接口,所以私有继承可以用来实现has-a关系
在继承时,private是默认的,因此不写明继承方法时,默认为私有继承
14.2.1初始化基类组件
1 | class student:private score{ |
和包含的区别在:对于继承类,新版本的构造函数将使用成员初始化列语法,使用类名而不是成员名来表示构造函数
(在上面的代码体现在score(c,m,e)而不是sco(e,m,e))
14.2.2访问基类的函数
使用类名和作用域解析运算符来调用基类的方法。比如假设基类score中有成员函数sum(),那么在派生类中可以通过score::sum()来调用。
14.2.3访问基类对象
通过强制类型转换,将派生类转换为基类,从而来访问基类内部的数据成员
1 | //派生类调用基类成员 |
14.2.4访问基类友元函数
不能用类名加作用域运算符的形式来调用友元函数,因为友元函数不是类成员函数。但是可以通过显式地转换为基类来调用正确的函数
假设基类中有如下<<友元函数,用来输出三个科目的分数
1 | friend ostream & operator<<(ostream & os,const score & sco){//友元函数 |
主函数可以这样写来调用基类中的友元函数(这里为调用基类中的<<友元函数)
1 | student stu("小李",100,100,100); |
14.3保护继承
保护继承是私有继承的变体,使用保护继承的时候基类的公有成员和保护成员变为派生类的保护成员,和私有继承一样,和积累的接口在派生类中可以直接使用,但是在继承层次结构之外不可使用
14.4多重继承
14.4.1虚基类
虚基类使得从多个类(它们的基类相同)派生出的对象只继承一个基类对象
在类声明中使用关键字virtual使得基类被用作虚基类(virtual和public的次序关系随意),这样就可以避免重复继承。
比如有基类worker,派生出了类singer和dancer。之后又由singer和dancer派生出star。那么需要在继承worker的时候加上virtual关键字
1 | class worker{ |
为了避免通过不同的途径给基类的成员赋值,虚函数禁止了信息通过中间类自动传递给基类。因此无法通过原本的方法来定义构造函数,在上例中无法在star类中获取name然后将name信息传递给worker。这样会传递失败,然后worker部分会调用默认构造函数。 因此在star的构造函数的初始化成员列表必须直接使用work的某个构造函数来构造work(如果work不是虚基类,这样是不合法的),就似乎是star直接继承了worker一样。
如果不希望只使用虚基类的默认构造函数,那么必须显式地调用改虚基类的某个构造函数
14.4.2使用哪个成员函数?
在单继承中如果没有重新定义基类中的成员函数,调用时会使用基类的定义,而在多继承中,调用基类成员函数可能会导致二义性。
1.可以使用作用域运算符来选择使用哪个版本的成员函数。
例如s为star的对象。
使用s.singer::show()来调用singer版本的show()函数
2.更好的方法是重新定义这个函数,然后在这个函数中选择调用哪个版本的函数
例如给出star中show()的定义void star::show(){singer::show()}
分析两种方法可以得到:第一种方法有一定的安全隐患,而第二种方法则会只显示singer部分的成员,而无法显示dancer部分的成员
第二种方法的补救措施是分别调用singer::show()和dancer::show()。 但这又会导致重复调用了worker::show()。
补救措施的完善方法是:将模块化数据,让singer::data()只显示专属于singer 的部分,dancer同理。在star::work()部分则分别调用singer::data(),dancer::data(),worker::data(),并额外显示专属于star的部分。
总之,在祖先相同时,使用多重继承必须使用虚基类,并且修改构造函数初始化列表的规则。
14.4.3混合使用虚基类和非虚基类
如果有类B被用作C和D的虚基类,X和Y的非虚基类。然后CDXY都用于M的基类,那么M会从CD中继承一个B类子对象,从X和Y中分别继承一个B类子对象。因此它包含了三个B类子对象。
14.4.4虚基类和支配
入股哦一个类从不同的类那里继承了两个或多个成员(数据或方法)那么使用改成员名的时候,入股哦么有类名进行限定,会导致二义性,如果使用的是虚基类,就不一定会导致二义性,这时,如果某个名称优先于其他所有名称,那么使用它的时候即使没有限定符也不会导致二义性。
优先性的判断规则是:派生类中的名称优先于直接或间接祖先类中的相同名称
14.4类模板
通过模板类可以用来实现容器类container class(如STL中的stack和queue等容器),容器类设计用来存储其他对象或者数据结构。
本节比较难懂,且代码都很长,做好心理准备
14.4.1定义类模板
和定义模板函数类似,模板类以下面的代码开头template<class Type>
这里的Type 不必是一个类,而只是表明这是一个通用的类型说明符。可以用不太容易混淆的typename关键字来替换掉class。
下面是一个模板类的声明和定义
1 | //stacktp.h |
模板类声明和普通类基本相同,区别在于
1.最前面要加上模板声明template
2.在使用类限定符的时候应该从Stack;: 改为Stack
模板必须和特定的模板实例化请求一起使用,为此,最简单的方法是将所有模板信息放在一个头文件中
使用模板类:
在上例中可以通过Stack
14.4.2模板的具体化
类模板和函数模板很相似,因此可以又隐式实例化,显式实例化和显式具体化,它们统称为具体化(specialization)
1.隐式实例化(implicit instantiation)
定义好一个类模板后,在创建某对象的时候,会自动进行这个参数类型的类进行隐式实例化,在这之前不会生成类的隐式实例化。
比如在创建Stack
2.显式实例化(explicit instantiation)
顾名思义就是我们直接要求进行的实例化,这时,虽然没有创建或提及类对象,编译器也会生成类声明(包括方法定义)所用到的语句为template class Stack
3.显式具体化(explicit specialization)
有时候我们想要这个模板在某个参数的时候给出不同的定义,这时候就需要对这个参数类型的类进行特殊的定义
template <> class Stack
4.部分具体化(partial specialization)
C++还允许部分具体化,即部分限制模板的通用性,例如:
假设有类模板template
14.4.3成员模板
模板可用作结构、类胡总和模板类的成员。下面的示例代码为一个模板类,它将另一个模板类和模板韩素作为它的成员。试着去理解这段代码
1 |
|
类定义过程:
hold模板是在私有部分声明的,因此只能在beta类中访问它,beta类使用hold模板声明了两个数据成员:hold<T> q;
hold<int> n;
n是基于int类型的hold对象,q成员是基于T类型的hold对象。
使用过程:
1.创建模板类对象
在main中创建guy的时候,模板参数为T是double类型,gay中的q是T类型的(也就是double)类型的hold对象,n是int类型的hold对象。
2.调用模板类对象中的模板函数(此时模板类的各个模板参数已经确定)
之后调用guy. blab(10,2.3)的时候,传入的第一个参数是int类型的,所以blab是参数列表为(int,double),返回值为int的函数。而之后在调用guy.blab(10.0,2.3)的时候,又创建了参数列表为(double,double),返回值为double的函数。需要注意的是,因为blab的第二个参数在生成guy的时候已经确定了是double(T为double),所以不管调用函数的时候传入的是什么类型的参数,都会转换为double类型的参数进行处理。
14.4.4将模板用作另一模板的模板参数
模板可以包含类型参数(如typename T)和非类型参数(如int n),模板还可以包含本身就是模板的参数
1 |
|
上面的代码中用到了14.4.1中的代码段作为头文件,用来提供一个模板。与此同时又创建了参数为一个模板的新的模板。这个模板类包含了一个int类型的模板和double类型的模板。用来同时调用二者的push函数和pop函数。
在主函数调用中,我们传入的参数模板必须包含有push函数和pop函数接口,因此Stack作为参数是合适的。
之后在主函数中以Stack为参数实例化了相关的类和对象(nebula是对象)。然后就可以用这个nebula对象来存储数据了。
14.4.5模板类和友元
模板类声明也可以有友元,模板的友元分为三类:
1.非模板友元
2.约束(bound)模板友元,即友元的类型取决于类被实例化时的类型
3.非约束(unbound)模板友元,即友元的所有具体化都是类的每一个具体化的友元
- 模板类的非模板友元函数
1 | //frnd2tmp.cpp |
有的编译器会对使用非模板友元发出警告。警告内容为:
friend void reports(HasFriend<T> &);
上面中的counts友元函数是所有实例化的友元,例如它同时是HasFriend
counts通过以下方式来访问HasFriend对象1.访问全局对象2.通过全局指针访问非全局对象3.创建自己的对象4.访问独立于对象的模板类的静态数据成员
如果要为友元函数提供模板类参数,那么应该这样声明;friend void report(HasFriend**<T>** &);
重点在于参数中的<T>
,这是不可省的,这样,带HasFriend<int>
参数的report友元函数将会是HasFriend<int>
类的友元,它会和带HasFriend<double>
参数的report友元函数组成重载
2.模板类的约束模板友元函数
可以修改前一个代码示例,使友元函数本身成为模板,没具体地说,为约束模板友元做准备,来使类的每一个具体化都获得与友元匹配的具体化,这比非模板友元复杂一些
首先,在类定义的前面声明每个模板函数template <typename T> void counts();
template <typename T> void report(T & );
然后在函数中再次将模板声明为友元
1 | template <typename T> |
在声明中<>指出这是模板具体化
3.模板类的非约束模板友元函数
在类内创建非约束友元函数,每个函数具体化都是每个类具体化的友元
1 | class ManyFriend{ |
在友元函数中的模板参数可以和类模板中的模板参数不同
14.4.6模板别名
可以用typedef为模板具体化指定别名
1 | typedef std::array<double,12> arrd; |
C++11之后能够使用模板提供一系列别名
1 | template<typename T> |
15.友元、异常、其他
16.string类和标准模板库
17.输入、输出和文件
17.1C++输入输出概述
C++的输入输出依赖于iostream和fstream内定义的的一系列类。
17.1.1流和缓冲区
通过使用流,C++ 程序处理输入输出的方式将独立于其去向
管理输入包含两步:
1.将流和输入去向的程序关联起来
2.将流和文件连接起来
管理输出同理
通常通过缓冲区可以高效地处理输入和输出
在输出时,程序会首先填满缓冲区,然后把整块数据传输给硬盘,并清空缓冲区,以备下一批输出使用,这被称为刷新缓冲区(flushing the buffer)
17.1.2流、缓冲区和iostream文件
在程序中包含iostream文件将自动创建8个流对象(4个用于窄字符流,四个用于宽字符流)
分别为cin 、cout、cerr、clog的窄字符和宽字符版本
17.1.3重定向
通过输入重定向<和输出重定向>,可以使用改变输入和输出流的连接目标.(默认为键盘和显示器)
对标准输出重定向并不会影响到cerr或clog。因此如果使用其中一个对象来打印错误信息,程序会在屏幕上显示错误信息。
17.2使用cout来进行输出
C++将输出看作字节流,很多数据都是以数值类型存储的,因此ostream流最重要的任务之一就是将数值类型(int或double等)转换成以文本形式表示的字符流
17.2.1重载的<<运算符
<<默认是按位左移运算符,但是被ostream重新定义为了为了输出,<<叫做插入运算符,插入运算符的重载使之能够识别C++中所有的基本类型。
拼接输出:cout<<“ABC“返回的是cout对象(的引用),因此这个表达式本身可以再使用<<运算符,这就实现了拼接的效果。可以通过cout<<“ABC”<<‘d’<<endl来不断拼接想要输出的内容。
17.2.2其他ostream方法
除了各种operator<<()函数外,ostream类还提供了put()和write()方法,前者用于显示字符,后者用于显示字符串
1 | cout.put('a');//打印字符a |
- put方法用来输出一个字符
- put方法返回ostream对象,因此可以拼接
- 数值类型(int,double)会转换成字符类型再打印
1 | const char * state1 = "test1"; |
- cout.write()有两个参数,第一个表示要打印的内容,第二个表示要打印多少字符
- cout.write()返回值是ostream类型的对象,所以可以拼接
- write()方法检测到空字符也不会停止,因此如果要打印的字符比字符串长就会导致溢出(例子中虽然指定的是打印state1,但是由于溢出了4个字符,所以还打印 了state2中的前四个字符
17.2.3刷新输出缓冲区
通常缓冲区为512字节或它的整数倍,在屏幕输出的时候不必填满缓冲区也可以刷新,如将换行符发送到缓冲区后,将刷新缓冲区。
1.在即将输入的时候会刷新输出缓冲区
2.控制符endl也会刷新缓冲区
3.控制符flush
4.控制符也是函数,因此可以使用语句flush(cout)来刷新输出缓冲区