《C++ Primer》读后总结

这是去年寒假读完第一遍《C++ Primer》的时候做的一些总结,刚才整理资料的时候翻到了,就先粘过来吧。

因为要复习C++,把这篇文章又重读了一遍,发现了不少错误,重新修订了一次。(2012年3月30日)

浮点数:

float只能保证6位有效数字,double只能保证10位有效数字。一般而言,对付典型的比较需要使用大于或小于,而不使用等于或不等于,其根本原因在于计算机对于浮点数的存储方式使浮点数存储精度过低。

字面值常量:

整形字面值常量默认为intlong,浮点型字面值常量默认为double,字符串字面值常量默认为C风格字符串。编写代码时可以利用\将长代码分行,特别对于较长的字面值常量来说,非常管用。

初始化:

内置类型在函数体外定义时被初始化为0,类对象根据构造函数初始化。一般而言,变量在定义时应显示初始化,以避免未初始化的对象被使用。C++对象的初始化分为直接初始化与复制初始化,直接初始化将对变量直接进行初始化,复制初始化将在初始化时先建立一个临时对象,然后调用变量的复制构造函数将临时对象复制给变量。需注意,复制初始化与直接初始化不同,同时,初始化与赋值也不同,需注意区分。

例如:

int a(10);//直接初始化int b=10;//复制初始化,先建立一个int型临时变量,值为10,然后将临时变量复制给bint c;c=10;//赋值,不是初始化




const限定符:

一般字面值常量应该赋值给const变量,防止出现魔数。在全局作用域声明中,const对象默认为文件的局部变量,非const变量默认为extern。如果const变量需要在其他文件中使用,需在变量定义时显示声明为extern。在遇到const限定符时建议从右往左读,const限定符修饰了的类型就是常类型,例如:

const int a=10;//a为常整型,const修饰了int。const int *p=&a;//p为指向常整型的指针,const修饰了int 。int *const q=&a;//q为指向整数的常指针,const修饰了变量q,该指针不能改变,指针指向的值可以改变,由于此处指向的是常整 数,因此指向的值也不能改变。

在类中,const成员变量必须在构造函数初始化列表中进行初始化,const成员函数中的const修饰符改变了this指针的性质,该函数只能做读取操作,不能做修改操作,注意从const成员函数中返回的值仍为const类型。const对象(类对象)、指向const对象的指针或引用只能调用const成员函数,以防止const对象被改变。(const用法较多,此处只是简略说明,更多内容请看C++Primer

引用:

可以建立任意对象的引用,因此可以建立指针的引用,例如:

int *a = new int(10);  int *&b=a;//b是一个指针的引用,引用的对象为一个指针,使用b时仍然需要按照指针的方法使用,*b值为10,b为b所指向的内存空间的地址。

引用必须在定义时初始化,一旦初始化后就不能改变引用对象。引用实际上为被引用变量的别名,两个变量指向同一块内存空间。例如:

int a=10;  int &b=a;//b为a的引用,a与b指向同一块内存空间  int &c;//错误  c=a;//错误

在类中定义的引用数据成员只能在构造函数初始化列表中初始化。

引用常用于函数的形参,函数形参使用地址传递时常使用引用或指针。使用引用可以使函数实参在函数内部被直接改变,减少形参传递时的时间消耗。

typedef

由typedef定义的数据可以代替其类型来使用。例如:

typedef void *p(void *temp);//这里的p可以作为一种函数类型来使用,由类型p定义的变量均为指定类型(返回一个void型指针,且参数为一个指针)的函数。

typedef主要用于隐藏复杂类型或使数据类型更为容易理解。

详细内容请参考文章《typedef四个用途和两大陷阱》。

数组:

数组中可以存放任意类型的对象。因此可以建立指针数组,多维数组(可以理解为数组中的每一个存储单元存储的内容仍为数组)。数组中对象的初始化遵循常规对象初始化的规则,如:引用变量必须在建立时进行初始化,引用数组建立时必须对数组中的所有元素进行初始化。sizeof运算符可以求出表达式结果所占的字节数,当表达式为数组名时,可以求出该数组所占的字节数,用sizeof(数组名)/sizeof(数组类型)可以求出数组长度。当数组名被用于表达式时,数组名转化为指针,其实为数组名所代表的地址(数组中第一个元素的地址)被赋值给一个临时的指针变量,由指针变量完成其余操作,数组名保持其属性不变。

指针:

可以建立指向任意对象的指针,因此可以建立指向指针的指针,指向函数的指针,指向数组的指针等等。指针所占用的字内存空间长度为一个字长(在32位系统中为4个字节,64位系统中为8个字节),内存空间中存储的是指针所指对象的地址, 即指针存储的是地址。一个有效的指针必然只有三种可能:

1、保存一个特定对象的地址;

2、指向某个对象后面的另一个对象;(指向数组中最后一个元素的下一个字节,以表示数组越界)

3、为0值,即NULL

未初始化的指针是无效的,使用无效的指针时会出现未定义错误。使用指针指向一个动态对象时,如果一个指针所指的对象已经被释放,该指针应该显示指向NULL,否则会出现野指针(指针指向的空间已被释放,但指针内所存的地址没有改变,指针仍然指向该内存空间,其值为任意值)。使用野指针会使程序出现意想不到的错误。

如果在某些时候需要用到多个指针指向同一对象,在释放指针所指的对象时需要确保释放最后一个指针时才真正的释放相关内存空间,此时可以使用句柄类或者智能指针达到对指针的控制,避免一个对象被释放多次。释放动态内存空间后可将指针赋0,C++保证释放指向0的指针是安全的。

C风格字符串:

C风格字符串实质上是字符数组,该数组最后一位为'\0',字符串字面值常量默认为C风格字符串。使用C风格字符串时,使用者必须确保目标字符串具有足够的大小,且字符串结束符为NULL(即'\0')。在使用C++程序时应尽量避免使用C风格字符串。

复合表达式的优先级:

复合表达式的优先级非常重要,C++程序员习惯将长代码利用复合表达式的缩短为短代码,如代码“++p.data;"中,运算符包括自增运算符与成员选择运算符(.),成员选择运算符的优先级高于自增运算符,因此,此代码的意思为:首先读取p对象的data成员变量,然后使data成员变量自增一次。*p++的意思为:首先保存p,然后使p指针自增一次,然后解引用所保存的p的值,结果为p自增前所指向的对象。

C++中操作符的优先级如下表所示:

操作符 结合性 功能 优先级
::

::

::

L

L

L

全局作用域

类作用域

命名空间作用域

::name

class::name

namespace::name

.

->

[]

()

()

L

L

L

L

L

成员选择

成员选择

下标

函数调用

类型构造

object.member

pointer->member

variable[expr]

name(expr_list)

type(expr_list)

++

--

typeid

typeid

显示强制类型转换

R

R

R

R

R

后自增操作

后自减操作

类型ID

运行时类型ID

类型转换

lvalue++

lvalue--

typeid(type)

typeid(type)

cast_name(expr)

sizeof

sizeof

++

--

~

!

-

+

*

&

()

new

new[]

delete

delete[]

R

R

R

R

R

R

R

R

R

R

R

R

R

R

R

对象的大小

类型的大小

前自增操作

前自减操作

位取反

逻辑非

一元负号

一元正号

解引用

取地址

类型转换

创建对象

创建对象数组

释放对象

释放对象数组

sizeof expr

sizeof(type)

++lvalue

--lvalue

~expr

!expr

-expr

+expr

*expr

&expr

(type)expr

new type

new type[lvalue]

delete expr

delete[] expr

->*

.*

L

L

指向成员操作的指针

指向成员操作的指针

ptr->*ptr_to_member

ptr.*ptr_to_member

*

/

%

L

L

L

乘法

除法

求模(求余)

expr * expr

expr / expr

expr % expr

+

-

L

L

加法

减法

expr + expr

expr - expr

<<>>L

L

位左移

位右移

expr << exprexpr >> expr
<<=>

>=

L

L

L

L

小于

小于等于

大于

大于等于

expr < exprexpr <= exprexpr > expr

expr >= expr

==

!=

L

L

等于

不等于

expr == expr

expr != expr

&L位与expr & expr
^L位异或expr ^ expr
|L位或expr | expr
&&L逻辑与expr && expr
||L逻辑或expr || expr
?:R条件操作expr ? expr : expr
=

*= 、/=、 %=、

+=、 -=、

<<=、>>=、

&=、 |=、 ^=

R

R

R

R

R

赋值操作

复合赋值操作

复合赋值操作

复合赋值操作

复合赋值操作

lvalue = expr

lvalue += expr等等

throwR抛出异常throw expr
,L逗号expr , expr

类型转换:

当实际类型与表达式或函数所要求的类型不同时,实际类型会被隐式(或显示)的进行类型转换。在算数转换时,所有小于整型的类型都会被转换为整型,达到整型仍然不符合要求的类型会被继续提升,直到符合要求为止。C++自动将枚举转换为整型,将用作条件判断的其他内置类型转换为bool型,当需要使用const类型时,非const将被隐式转换为const类型。使用类类型时,如果在需要使用类对象时使用了其他类型,但该类型可以成为类构造函数的参数,则编译器会建立一个临时变量,调用类相匹配的构造函数初始化该类。如果类的成员函数中重载了转换构造函数,则该类的转换构造函数可以用在任何能够调用匹配隐式类型转换的地方。注意,类的隐式类型转换后只能根有标准库类型转换。当隐式类型转换造成二义性的时候可以使用强制类型转换。标准C++一般使用static_castdynamic_castconst_castreinterpret_cast操作符作为显示类型转换。编译器因实质性的任何类型转换都可以由static_cast显示完成如:

double b=8;char ch = static_cast<char> b;

const_cast可以既可以添加表达式的const属性,又可以取消表达式的const属性,reinterpret_castdynamic_cast不常用,前者用于位运算,依赖于编译器,后者用于动态类型识别。

C标准强制类型转换具有与const_cast、static_cast、reinterpret_cast一样的行为,在合法使用static_cast或const_cast的地方,旧式强制转提供了与各自对应的命名强制转换一样的功能。如果两种强制转换均不合法,则旧式强制转换执行reinterpret_cast的功能。

如果不希望类的构造函数在隐式转换中使用,可以在构造函数的声明前添加关键字explicit。

newdelete

new运算其实分为三个部分,首先调用operator new函数分配足够大的为类型化的内存空间以保存指定类型的一个对象,然后运行该对象的构造函数,初始化该对象,最后返回指向新分配内存空间的指针。我们可以重载operator new函数,人为地进行内存分配,达到优化内存的效果。在类中一旦出现重载的operator new函数,则系统不再调用标准库的operator new函数。

delete运算使用时分为两个步骤,首先调用指定对象的析构函数,然后调用名为operator delete的标准库函数释放该对象所占用的内存。operator delete函数同样可以重载。当使用new新建一个动态对象以后需要使用assert判断动态建立是否成功,以防止程序运行时对未建立的对象进行使用。delete数组时需使用[]注明。如果动态建立一个数组而使用了delete去释放它,会导致只有数组中第一个元素被释放,出现内存泄漏。

循环:

循环的形式主要分为三种,即for循环,while循环以及do while循环。理论上来说,三中循环具有等效价值,能够用一种循环书写的代码都能够使用另外两种循环书写。但是在写程序的过程中应当适当区分三种循环的的使用范围,for循环多用于计数,while循环多用于当满足一定条件时执行一定结果,do while循环一般用于限制性后判断循环条件。三种循环区分使用便于程序员阅读代码。

函数:

函数非引用形参的传递是一个复制的过程。函数调用时,编译器将实参的值复制给非引用形参(包括指针也是如此),然后由形参执行相关操作。函数调用结束后,若函数具有非引用返回值,则编译器将函数返回值复制到函数调用处(在函数调用处建立一个临时变量,将返回值复制给临时变量)。当函数形参或返回值为引用类型,函数形参将成为实参的引用,形参与实参使用同一块内存空间。

C++中能够实现函数重载,即多个函数拥有相同的函数名,不同的函数形参。注意,不能根据函数返回值区分函数是否发生重载(即多个函数拥有相同函数名及形参,不同函数返回值时编译错误)。当指定函数中的默认实参时,既可以在声明中指定默认实参,也可以在定义中指定,但一个文件中只能为一个形参指定默认实参一次。发生函数重载时,编译器首先根据函数名确定函数的选择范围,然后根据函数形参数目缩小函数候选集,最后根据函数的形参类型在函数的候选集中寻找最佳匹配,若出现唯一的最佳匹配,则发生函数调用,若没有最佳匹配,但根据隐式类型转换后出现唯一的匹配,则发生函数调用,若隐式转换后出现多个匹配,则出现二义性,编译失败。根据函数形参类型选择匹配时,若发生隐式类型转换,则该编译器根据转换等级区分最佳匹配,转换等级降序排列如下如下:

1、精确匹配,即完全匹配

2、通过类型提升实现的匹配,即实参由低精度提升到高精度

3、通过标准转换实现的匹配,即标准库类型转换

4、通过类类型转换实现的匹配,即由用户定义的类型转换

高等级的转换相对于低等级的转换总是更好的匹配。

当在函数中定义了一个静态变量(static)时,该变量在多次函数调用过程中仅被初始化一次。内联函数的定义应该放在头文件中,内联函数的定义对编译器而言必须是可见的。

可以声明指向函数的指针,用于指向具有相同形参与返回类型的函数。指向函数的指针常用于简化函数的调用,或者使用一个指针调用多个函数。

标准IO

标准IO库使用了ios类作为基类,其余类均直接或间接地派生自ios。因此,我们常用的输入输出流均为类的对象。IO标准库提供了一系列的函数用于对输入输出流状态进行条件判断,写程序时可以根据条件判断对错误的输入进行抛弃,从而保证结果的正确性。

顺序容器:

顺序容器分为vectordequelist三种,vector类似于我们使用的数组,deque类似于我们使用的队列,而list则是链表。vectordeque支持快速随机访问,能够使用类似于数组的方式随机访问已存在的任意元素,而list不支持该功能。无论是顺序容器,还是关联容器,其元素类型必须满足两个约束:

1、元素类型必须支持赋值运算

2、元素类型的对象必须可以复制

由于这两个约束条件的存在,引用类型不能作为容器元素而存在。容器提供一系列的操作使用户在使用容器时能够更方便的得到想要的结果。

容器的选用需要根据用户常用操作来判断。若用户常用操作为随机访问,则应使用vectordeque容器,若用户常用操作为在任意位置的添加删除元素,则应使用list容器。

string类:

string类常用于对字符串进行保存与修改,string类提供一系列的操作以便于用户对string对象中存储的字符串进行修改。同时用户可以像使用数组一样对string对象中的单独字符进行修改,而对象中的其他字符不变,达到随机访问的目的。string类支持与C风格字符串的相互转化,用户可以用C风格字符串赋值给string对象,也可以调用string对象的c_str函数返回string对象的C风格字符串格式。用户可以直接使用string类的成员函数实现对string类对象的部分字符进行查找、添加、删除及比较功能。

pair类:

pair类中包含两个共有数据成员,firstsecond。用户可以为pair类对象自定义数据成员类型,以使pair对象满足自己的要求。关联容器中常用pair类存储表示两个相关联的变量。如map容器使用pair类的first成员保存键,用second成员保存值。

关联容器:

关联容器分为mapset以及multimapmulitset四种。在使用关联容器时,键需要有一个约束条件,即需要能够被比较,默认情况下,标准库使用键类型定义的<操作符来实现两个键的比较。在键的比较过程中,遵循严格弱序比较,两个键之间只能有小于和相等两种结果。

map容器也被称为关联数组,是键-值对的集合,元素通过键来存储和读取相关的值,在map容器中,一个键仅对应一个值,键和值可以是任意类型。在使用map容器时,若使用[]作为容器键的读取方式,则如果指定的键不存在,编译器会自动新增该键,并利用初始化规则初始化相应的值。

set容器只是单纯的键的集合。set容器不支持下标操作符,用户在set容器中添加获取元素需使用set容器的成员函数。

multimapmultiset容器与mapset容器类似,只是单纯的增加了实例个数,在multimap容器中,一个键能够对应多个值,同一个键的不同值之间顺序排列,能够通过迭代器的自增或自减实现值的改变。multimap容器不支持下标操作符。

迭代器:

迭代器是一种检查容器内元素并遍历元素的数据类型。每种容器都定义了自己的迭代器类型,一般为:容器类型<元素类型>::iterator

容器常用迭代器对容器内的元素进行遍历,迭代器可以使用解引用操作符对容器内的元素进行读取或改变。部分容器支持迭代器算术操作符以简化迭代器运算。

在泛型算法中,常使用流迭代器,反向迭代器进行遍历输入输出流、反向遍历操作。泛型算法使用流迭代器从流对象中读取或写入数据。使用反向迭代器反向遍历元素。插入器个人感觉更像是一个函数。

适配器:

本质上,适配器是一种是一事物的行为类似于另一事物的行为的一种机制。容器适配器让一种已存在的容器类型采用另一种不同的抽象类型的工作方式实现。标准库提供了queue适配器实现队列操作,提供stack适配器实现栈操作,提供priority_queue适配器实现优先级队列操作。

泛型算法:

泛型算法是指可以在多种容器类型上实现相同操作的一系列函数。每个泛型算法都独立于单独的容器并且只是隐式的依赖于容器存储的元素类型,不同的泛型算法对元素类型的隐式依赖不同,一般来说,泛型算法对容器中元素要求如下:

1、需要某种遍历集合的方式,能够从一个元素向前移动到下一个元素

2、必须能够知道是否到达了集合的末尾

3、必须能够对容器中的每一个元素与被查找的元素进行比较(不同的算法要求的操作符不同)

4、需要一个类型来指出元素在容器中的位置,或者表示找不到该元素

可以使用谓词函数实现对泛型函数按照自定义函数进行比较。使用算法写元素时,需要用户自己确保算法所写的序列至少足以存储要写入的元素。由于泛型算法不能改变容器的大小,因此在执行添加或删除容器元素时需要使用迭代器进行操作。泛型算法不改变容器大小,而迭代器改变。

数据抽象和封装:

类背后蕴涵的基本思想是数据抽象和封装。数据抽象将程序的接口和实现分离,类设计者必须关心类是如何实现的,但使用类的程序员则不必了解这些细节。相反使用类的程序员仅需了解类型的接口,他们可以抽象的考虑该类型做了什么,不必具体的考虑该类型如何工作。封装是一项将低层次的元素组合起来形成新的高层次实体的技术。函数是封装的一种,函数所执行的细节被封装在函数定义中。被封装的元素隐藏了实现细节。类也是一个封装实体,大多数设计良好的类类型隐藏了实现该类型的成员。

内联函数:

内联函数需要在函数定义处函数返回值前加上inline(内联说明),编译器在读取到内联说明时会判断该函数是否适合内联,如果不适合则忽略该建议。一般来说,内联函数能够消除函数的额外开销。内联函数会在调用时将函数展开为代码,编译器负责传递参数,然后再调用地点直接执行代码,如果调用地点为表达式,则调用时实现复合表达式的运算。内联函数机制使用云优化小的、只有几行而且经常被调用的函数,大多数编译器不支持递归函数的内联。内联函数应该在头文件中定义,以便多个文件使用。

类的声明只包含类类型关键字和类名,类的定义则包括类类型关键字、类名以及类的全部成员变量声明。类中的成员可以是除类本身的对象以外的任意类型,因此类中的成员可以是另一个类,此处称为局部类。struct类与class类只在默认访问标识符处有区别,struct类默认访问标识符为publicclass类默认访问标识符为private

类的成员函数(即函数成员变量)既可以在类定义内定义也可以在类定义外定义,如果在类定义外定义,则定义时需要增加作用域说明,完全限定其作用域。无论成员函数在类定义内还是类定义外定义,都必须在类定义内具有声明。在类内部定义的成员函数会被编译器默认为内联函数,调用该函数时编译器会在调用函数处直接展开函数代码,提高运行速度,但内联函数所具有的约束条件在此处仍然适用。如果成员函数被声明为常成员函数,则在类外定义时仍然需要显示声明该成员函数为const。类中的非静态成员函数均带有隐式形参this*,该形参指出函数所处的对象,不同的对象调用相同成员函数时,由于this指针所指的对象不同,结果也不一定相同。静态成员函数为该类的所有对象共享,调用结果相同。

类成员变量在类定义内仅为声明,不能初始化。const数据成员可以显示的使用构造函数初始化列表进行初始化,也可以直接在类定义内初始化。类的静态数据成员必须在类的定义体外恰好定义一次。由于静态数据成员被所有该类的对象共享,因此静态数据成员在类定义时被分配内存空间。特殊的,常静态数据成员必须在类内与类外各定义一次,如果在类内部已经被初始化过,则再类外可以不用初始化。静态成员在类外需要通过作用域操作符访问。如果数据成员需要在任何地点(能够访问该数据成员的地方)均能够做出改变,即无论调用该数据成员的对象是否为const,无论使用该数据成员的函数是否为const,该数据成员都能够改变,则需要将数据成员声明为mutable

访问标识符分为三种,public标识符、private标识符、protect标识符。public标识符是公开标识符,任何使用该类对象的地方都可以访问public标识符范围内的成员。private标识符是私有标识符,在该标识符范围内被声明的成员只能在类的友元或类本身的成员函数中被访问。protect标识符是类对派生类的接口,protect标识符范围内的成员可以被该类的成员函数、友元以及派生类访问。一般需要派生类知道(访问)而不需要使用类的成员知道(访问)的成员可以声明为protect

类的作用域:

在类的成员函数中使用的变量首先在该成员函数作用域中查找,如果找不到,则在类的定义体中查找相关变量,如果仍然找不到,则在定义类的文件全局作用域中查找。在类外定义的成员函数,函数的形参表和函数体处于类的作用域范围内,函数返回类型不一定在类作用域范围内。如果函数返回类型为在类中定义的别名,则在类外定义函数时,需要显示指出该函数的返回值处于类作用域。

构造函数:

如果类的设计者没有自定义类的构造函数,则系统会默认生成一个默认合成构造函数。默认合成构造函数不带形参,仅初始化类类型对象,适用于仅包含类类型数据成员变量的类。构造函数常用来完成对该类对象的初始化工作,可以使用初始化列表完成对部分数据元素的初始化,初始化列表中变量的排列顺序不影响类对象在定义时类中数据成员的初始化顺序,其初始化顺序仅与相应数据成员在类定义中的声明顺序有关。类的设计者可以根据类要实现的功能设计自定义的构造函数,一旦出现自定义构造函数,则编译器不会为该类生成默认合成构造函数。一般而言,类设计者定义的构造函数能够进行隐式类型转换,例如:

加入类Screen具有string为形参的构造函数,则

void display(Screen &p);display("1234");//OK

在上例中,函数调用时,编译器将调用类的构造函数建立一个临时对象,并用字符串"1234"初始化该对象,然后使用临时对象完成函数内部操作。此处完成了一次隐式类型转换。如果类设计者不希望出现构造函数定义的隐式类型转换,则需要将构造函数声明为explicit,编译器将不适用该构造函数作为转换操作符。

友元:

A类将B声明为友元,则B可以通过A类的对象访问到A类的全部成员,B可以是一个类,也可以是一个函数。友元关系是独立的,类不能通过继承关系继承来自基类的友元,也不能通过派生关系使派生类认同基类的友元。友元关系范围无法通过继承、派生与友元扩大。

复制控制:

复制控制一般是指复制构造函数、赋值操作符函数、析构函数,一般三者同时需要。如果类的设计者没有自定义复制构造函数与赋值操作符函数,则编译器会生成合成复制构造函数与合成的赋值操作符函数。无论类的设计者有没有定义析构函数,编译器都会生成一个合成析构函数,并在调用完自定义析构函数后运行。

合成的复制构造函数只能将指定对象的成员变量的值复制给新对象的相应成员变量,若成员变量中出现指针,则调用合成复制构造函数后会出现两个对象中的某个成员变量指向同一块内存空间,为了避免这种情况发生,类设计者需要自定义复制构造函数,以达到完全正确复制对象的功能。

赋值操作符函数功能与复制构造函数功能类似,类的使用者使用类时,如果能够正确使用复制构造函数,则使用者会认为也能够正确使用赋值操作符,因此类的设计者在设计复制控制时,复制构造函数与赋值操作符函数往往需要同时定义。

析构函数需要确保类对象在使用结束后能够正确被释放。无论类的设计者是否自定义析构函数,编译器仍然会为类合成默认析构函数。析构函数不仅仅只能做释放工作,也可以做任何类设计者希望类对象在被释放后想做的扫尾工作。

操作符重载:

C++中只有作用域操作符(::)、条件操作符(?:)不能重载。重载操作符既可以作为类的非成员函数,也可以作为类的成员函数,如果操作符重载函数成为类的非成员函数,当操作符重载函数需要调用类的私有成员或保护成员时需要定义操作符重载函数为类的友元函数。操作符重载不能改变操作符的优先级、结合性以及操作数数目,也不能添加用户自定义操作符作为重载的操作符。当操作符重载函数作为类的成员函数时,该函数可以省略第一个形参(默认为this指针)。如果类的设计者没有定义对应的操作符重载,则编译器自己定义如下操作符:

1、合成赋值操作符进行逐个成员复制。

2、取地址操作符和逗号操作符,其效果与在内置类型对象上执行一样。

3、内置逻辑与、逻辑或操作符使用短路求值(只有上一个表达式结果使整个复合表达式结果为真时才进行后续表达式求值),如果重定义该操作符会丧失短路求值特性。

在设计操作符重载时需要注意:

1、赋值、下标、调用和成员访问箭头等操作符必须定义为类的成员函数,将这些操作符定义为非成员函数将引起编译错误。

2、复合操作符(+=-=*=/=)通常应定义为类的成员,但不是一定的。

3、改变对象状态或与给定类型紧密联系的其他操作符,如自增、自减、解引用操作符通常应该定义为类的成员函数。

4、对称的操作符,如算术操作符、相等操作符、关系操作符和位操作符,最好定义为普通非成员函数。

自增自减操作符在重载时为了区分前自增/后自增与前自减/与后自减,类设计者需要使后缀式操作符函数接受一个额外的(无用的)int型参数,该参数无实际意义,只为区分两种自增自减操作符。

可以为类类型对象重载函数调用操作符,函数调用操作符必须声明为成员函数,定义了函数调用操作符的对象成为函数对象。类设计者可以根据函数调用操作符内的形参类型及形参数对函数调用操作符再次进行重载。函数对象常用于通用算法的实参。关于函数对象的更多使用请参照《C++ Primer》第十四章第八节。

类型转换操作符重载:

类设计者可以设计类型转换操作符重载,用以将类类型转换为需要转换到的类型,该转换过程为隐式类型转换。编译器只能够从类类型转换到相应类型一次,如果需要多次转换则只能跟标准库转换,不能连续多次类类型转换,即不能递归转换。标准转换可以放在类类型转换之前。可以利用类类型转换操作符重载与构造函数实现类类型与内置类型的相互转换。

继承与派生:

派生类能够继承基类定义的成员,派生类可以无需改变而使用那些与派生类具体特性不相关的操作,派生类可以重定义那些与派生类型相关的成员函数,将函数特化,考虑派生类性的特性。除了从基类继承来的成员以外,派生类可以定义自己特有的成员。基类必须指出希望派生类重定义哪些函数,希望重定义的函数应该被声明为virtual(虚函数),基类希望派生类继承的函数不能定义为虚函数。基类中的私有成员在派生类中无法直接访问,如果需要访问基类中的私有成员,则派生类需要借助从基类继承来的成员函数。一旦出现派生,则派生类是一个新类,派生类继承了基类的部分成员,但根据基类建立的对象在派生类中没有特殊访问权限。如果在基类中定义了静态成员,则基类与派生类共用静态成员,如果派生类定义有静态成员,则基类无法访问派生类定义的静态成员。友元无法通过派生关系继承。

如果一个基类只是作为基类存在,不需要定义对象,并且基类中的部分成员函数在基类中不适用,但是需要在派生类中被重定义,则可以定义该成员函数为纯虚函数。定义有纯虚函数的类称为抽象类,抽象类不能创建对象。

每个派生类构造函数除了初始化自己的数据成员之外,还要初始化基类。如果派生类继承自多个基类,则基类的初始化顺序由派生类派生列表中的基类顺序决定。如果基类没有默认构造函数,则需要派生类在构造函数中显示指出调用基类的哪个构造函数进行初始化。

类是否需要定义复制控制成员完全取决于类自身的直接成员,与基类无关。合成的默认复制控制函数会调用基类的复制控制函数对基类进行复制控制。如果派生类定义了自己的复制控制函数,则需要显示指出对基类复制控制函数的调用。

在类的继承与派生过程中一般需要定义基类的析构函数为虚析构函数,如果析构函数为虚函数,那么通过指针调用时,运行那个析构函数将因指针所指对象类型的不同而不同。

当把一个派生类复制给基类时,将会把派生类中的基类分离出来,只复制基类部分,复制完成后,复制类为基类,因此无法访问派生类部分,也不存在派生类部分。

动态绑定:

通过动态绑定我们能够编写程序使用继承层次中任意类型的对象,无需关心对象的具体类型。在C++中,通过积累的引用或指针调用虚函数时发生动态绑定。引用或指针即可一直向基类对象也可以指向派生类对象。用引用或指针调用的虚函数在运行时确定。被调用的函数是引用或指针所指对象的实际类型所定义的。基类的指针只能够调用基类部分的成员函数,即使基类引用或指针在运行时指向派生类对象,也只能够执行派生类中基类部分的成员函数。

模板:

模板是创建类或函数的蓝图或公式。模板形参可以使表示类型的类型形参,也可以是表示常量表达式的非类型形参。使用函数模板时,编译器会推断哪个模板实参绑定到模板形参。一旦编译器确定了实际的模板实参,就实例化了一个函数模板的实例。类类型模板形参表示该类型为未知类,非类类型形参表示该处为一个未知值。模板类型形参的使用方法与内置类型完全一致,不过模板类型形参只能在该模板作用域内使用。如果模板中包含有非类型形参,函数调用时非类型形参将用值代替,值的类型在模板形参表中指定。模板非类型形参时模板定义内部的常量值,在需要常量表达是的时候可以使用非类型形参。编写模板代码的时候,应该对实参类型的要求尽可能少,已达到泛型代码(尽量少的限制实例化,使更多的类型可以作为该类的元素)的目的。类模板在引用实际模板类型时实例化,类模板中的成员函数在调用该成员函数的时候实例化。函数模板在调用它或者用它对函数指针进行初始化或赋值时实例化。因为模板类在实例化时无法进行类型推导,因此模板类定义的类型总是包含模板实参。函数模板可以进行类型推导,因此可以函数模板可以显示包含模板实参,也可以隐式由类型推导得出模板类型。如果函数的返回值是模板形参中的一种类型,则必须用模板实参显示指出该类型。

将类模板设置为友元时,类设计者必须决定友元关系的范围应该有多大。类既可以设置模板类的所有实例化为友元,也可以设置特定的实例化为友元。

在有些时候,模板中的操作可能对部分类型不适用,此时可以使用模板特化,对特定的模板实例进行编码,实现特定操作。

函数模板可以重载,可以定义有相同名字但形参数目或类型不同的多个函数模板或普通非模板函数。重载的函数模板由于类型的不确定可能会导致二义性。如果二义性产生于非模板函数与模板函数,则对非模板函数的调用为最佳匹配。

命名空间:

一个命名空间是一个作用域,通过在命名空间内部定义库中的名字可以避免全局名字的冲突。同一个命名空间可以不连续的分布在多个文件内,但未命名命名空间为每个文件独有,只能在文件内部不连续分布,不同的文件未命名空间代表不同的作用域。命名空间中的成员在程序运行的开始处即进行初始化,因此命名空间中的成员在程序中可以通过作用域限制直接使用,也可以通过using指示或using声明使用。using指示由于使整个命名空间中所有成员成为可见,大量使用时会重新引入命名冲突问题,成为命名空间污染。using指示将命名空间成员提升到外围作用域,当在类的成员函数中使用using指示时,命名空间的成员具有与类成员相同的作用域。如果在命名空间中使用模板特化,则模板的现实特化必须在定义通用模板的命名空间中声明。

多重继承:

多重继承时,如果同名函数在多个基类中出现将产生二义性。派生类中名字的查找首先发生在派生类内,如果在派生类内没有找到该函数,则在多个基类中同时查找(只检查函数名称),一旦找到多个函数具有相同名称,则该处产生二义性,如果只找到一个名称匹配的函数,则检查该函数调用是否合法(函数是否匹配)。

多重继承时,一个基类可能在派生层次中出现多次,此时派生类会拥有多个相同的基类,使用基类对象时会产生二义性。虚继承可以使多个相同基类在派生类中只被共享一次。在虚继承下,对给定虚基类,无论该类在派生层次中作为虚基类出现多少次,只继承一个共享的基类子对象。
原文链接: https://www.cnblogs.com/pgx1030/archive/2011/12/29/2305524.html

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/39356

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年2月8日 下午4:00
下一篇 2023年2月8日 下午4:01

相关推荐