以前分析Android源码分析道native层,一些简单的C++知识还够我流畅的阅读分析源码,最近分析到hal层,发现C++的知识不够用了,这里打算花段时间去温习下C++,知道的温习,不知道的补习。
ps:本文以及后续温习C++的文章若没具体明示,都是参照阅读《C++ Primer Plus(第6版)_中文版_带书签_超清》。
2.1 头文件
包含头文件有如下几种方式:
2.2 名称空间
2.3 位与字节
2.4 运算符重载
2.5 auto关键字
2.6 指针定义注意点
2.7 复合类型
数组、指针和结构
2.8 内联函数 inline
内联函数是C++为提高程序运行速度所做的一项改进。常规函数和内联函数直接的主要区别不在于编写方式,而在于C++编译器如何将它们组合到程序中。
理解代码运行对时间、空间消耗。
无非就是空间换取时间,或者时间换取空间。
如何使用inline函数?
内联与宏的关系
2.9 引用变量
引用变量是一种伪装指针,它允许变量创建别名。引用变量主要被用作处理结构和类对象的函数的参数。
2.9.1 创建引用变量
int rats;
int & rodents = rats; // int & 指的是指向intd的引用
ps:经测试 rodents 和 rats 地址也是一样。
可以理解引用为const 指针,但并不是。
如果程序员的意图是让函数使用传递给它的信息,而不对这些信息进行修改,同时又想使用引用,则应使用常量引用。
如:double refcube(const double &ra);
2.9.2 使用const
2.9.3 const 引用返回
2.9.4 何时使用引用?
3.1 默认参数
函数默认参数的默认值的设置必须通过函数原型。
即,将值赋给原型中的参数。
char * left(const char * str,int n = 1);
4.1 函数重载(多态)
同名同类型返回值函数,参数类型,个数不同。
void print(const char * str, int width);
void print(double d, int width);
void print(long l, int width);
4.1.1 何时使用函数重载?
5.1 函数模板
函数模板是通用的函数描述,也就是说,它们使用泛型来定义函数,其中的泛型可用具体的类型(如int或double)替换。通过将类型作为参数传递给模板,可使编译器生成该类型的函数。由于模板允许以泛型(而不是具体类型)的方式编写程序,因此有时候也被称为通用编程。由于类型是用参数表示的,因此模板特性有时也被称为参数化类型。
关键字template和typename是必需的。除非可以使用关键字class代替typename。必须使用尖括号。
//函数模板原型
template
void Swap(T & a, T &b);
//函数模板定义
template
void Swap(T & a, T &b)
{
T temp;
temp = a;
a = b;
b = temp;
}
若如下使用编译器则会生成对应的函数定义。
int a = 0;
int b = 1;
Swap(a,b);
void Swap(int & a, int &b) //该函数程序员不能看到,但编译器确实生成并使用了它们
{
int temp;
temp = a;
a = b;
b = temp;
}
5.1.2 重载解析
6.1 头文件
6.1.2 头文件管理
7.1 存储持续性
C++使用三种(在C++11中是四种)不同的方案来存储数据,这些方案的区别就在于数据保留在内存中的时间。
7.1.1 全局变量和局部变量
8.1 对象和类
8.1.1 类和结构
8.1.2 内联方法
内联方法其定义位于类声明中的函数都将自动成为内联函数。类声明常将短小的成员函数作为内联函数。
内联函数的特殊规则要去在每个使用它们的文件中都对其进行定义。确保内敛定义对多文件程序中的所有文件都是可用的、最简便的方法是:将内联定义放在定义类的头文件心中。
9.1 客户/服务器模型(C/S)
10.1 构造函数和析构函数
构造函数是一种特殊的类成员函数,在创建类对象时被调用。构造函数的名称和类名相同,但通过函数重载,可以创建多个同名的构造函数。
构造函数没有声明类型。
构造函数通常用于初始化类对象的成员,初始化应与构造函数的参数列表匹配。
用构造函数创建对象后,程序负责跟踪该对象,直到其过期为止。对象过期时,程序将自动调用一个特俗的成员函数,即析构函数。
每个类只能有一个析构函数。
析构函数没有返回类型(连void都没有),也没有参数。
11.1 const成员函数
12.1 this 指针
每个成员函数(包括构造函数和析构函数)都有一个this指针。this指针指向调用对象。如果方法需要引用整个调用对象,则可以使用表达式this。在函数的括号后面使用const限定符将this限定为const,这样将不能使用this来修改对象的值。然而,要返回的并不是this,因为this是对象的地址,而是对象本身,即this(将解除引用运算符*用与指针,将得到指针指向的值)。
13.1 对象数组:声明对象数组的方法与声明标准类型数组相同。
14.1 类作用域:在类中定义的名称(如类数据成员名和类成员函数名)的作用于都是整个类。
使用类成员名时,必须根据上下文使用直接成员运算符(.)、简介成员运算符(->)或作用域解析运算符(:?。
Ik * pik = new Ik;
pik->ViewIk();
Ik ee = Ik(8);
ee.ViewIk();
14.1.2 作用域为类的常量:使用static关键字
14.1.3 作用域内枚举:所有对象中都不包含枚举
eg错误示例:
class Bakery
{
private:
enum {Months = 12};
double costs[Months];
}
需要使用枚举,可以使用作用域内枚举。
15.1 抽象数据类型
16.1 oop总结
17.1 友元
通常公有类方法提供唯一对象私有部分的访问途径。但是有时候太过严格,以至于不适合特定的编程问题。C++提供了另外一种形式的访问权限-友元
17.1.2 友元函数
如何创建友元函数?
17.1.3 常用的友元:重载<<运算符
一个很有用的类特性是,可以对<<运算符进行重载,使之能与cout一起来显示对象的内容。
只有在类声明中的原型中才能使用friend关键字。除非函数定义也是原型,否则不能在函数定义中使用该关键字。
18.1 重载运算符:作为成员函数还是非成员函数?
因为运算符重载是通过函数来实现的,所以只要运算符函数的特征标不同,使用的运算符数量与相应的内置C++运算不同,就可以多次重载同一个运算符。
19.1 类的自动转换和强制类型转换
只接受一个参数的构造函数定义了从参数类型到类类型的转换。如果使用关键字explicit限定了这种构造函数,则它只能用于显示转换,否则也可以用于隐式转换。
19.1.1 转换函数:构造函数只能用于从某种类型到类类型的转换。eg:
Stonewt incognito = 275;
但不能用构造函数进行相反转换,要进行相反的转换,必须使用特殊的C++运算符函数–转换函数。
转换函数是用户定义的强制类型转换,可以像使用强制类型转换那样使用它们。eg:
Stonewt wolfe(285.7);
double host = double (wolfe);
double thinker = (doube) wolfe;
如何创建转换函数?
20.1 特殊成员函数
20.1.2 默认构造函数:如果没有提供任何构造函数,C++将创建默认构造函数。
20.1.3 复制构造函数:复制构造函数用于将一个对象复制到新创建的对象中。也就是说,它用于初始化过程中(包括按值传递参数),而不是常规的赋值过程中。
函数原型如下:
Class_name(const Class_name &);
eg: StringBad(const StringBad &);
对于复制构造函数,需要知道两点:何时调用和有何功能。
何时调用复制构造函数?
新建一个对象并将其初始化为同类现有对象时,复制构造函数都将被调用。
默认的复制构造函数的功能?
默认的复制构造函数逐个复制非静态成员(成员复制也称为浅复制),复制的是成员的值。
21.1 C++11 空指针
22.1 静态类成员函数
23.1 在构造函数中使用new时应注意的事项
1.如果在构造函数中使用new来初始化指针成员,则应在析构函数中使用delete。
2.new和delete必须相互兼容。new对应于delete,new[]对应于delete[]。
3.如果有多个构造函数,则必须以相同的方式使用new,要么都带中括号,要么都不带。因为只有一个析构函数,所有的构造函数都必须与它兼容。 然而,可以在一个构造函数中使用new初始化指针,而在另一个构造函数中将指针初始化为空(0或C++11中的nullptr),这是因为delete(无论待中括号还是不带中括号)可以用于空指针。
4.应定义一个复制构造函数,通过深度复制将一个对象初始化为另一个对象。通常,这种构造函数与下面类似。
String::String(const String & st)
{
num_string++; //hanle static member update if necessary
len = st.len; //sam length as copied string
str = new char[len + 1]; //allot space
std::strcpy(str,st.str); //copy string to new location
}
具体的说,复制构造函数应分配足够的空间来存储复制的数据,并复制数据,而不仅仅是数据的地址。另外,还应该更新所有受影响的静态类成员。
5.应当定义一个赋值运算符,通过深度复制将一个对象复制给另一个对象。通常,该类方法与下面类似:
String & String::operator=(const String & st)
{
if(this == &st) //object assigned to itself
return *this; //all done
delete [] str; //free old string
len = st.len;
str = new char[len + 1]; //get space for new string
std::strcpy(str,st.str); //copy the string
return *this; return reference to invoking object
}
具体的说,该方法应完成这些操作:检查自我赋值的情况,释放成员指针以前指向的内存,复制数据而不仅仅是数据的地址,并返回一个指向调用对象的引用。
24.1 返回对象的说明。
当成员函数或独立的函数返回对象时,有如下几种返回方式可供选择。
1.返回指向对象的引用。
2.返回指向对象的const引用。
3.返回const对象。
24.1.1 返回指向const对象的引用
返回对象将调用复制构造函数,而返回引用不会。
引用指向的对象应该在调用函数执行时存在。
返回的对象尽量和函数传入的参数类型相对应。
24.1.2 返回指向非const对象的引用
常见的两种返回非const对象情形是,第一:重载赋值运算符。第二:重载与cout一起使用的<<运算符。
24.1.3 返回对象
如果返回的对象是被调用函数中的局部变量,则不应按引用的方式返回它,因为在被调用函数执行完毕时,局部对象将调用其析构函数。因此,当控制权回到调用函数时,引用指向的对象将不再存在。在这种情况下,应返回对象而不是引用。
总之,如果方法或者函数要返回局部对象,则应返回对象,而不是指向对象的引用。在这种情况下,将使用复制构造函数来生成返回的对象。如果方法或者函数要返回一个没有公有复制构造函数的类的对象,它必须返回一个指向这种对象的引用。最后,有些方法和函数(如重载的赋值运算符)可以返回对象,也可以返回指向对象的引用,在这种情况下,应首选引用,因为其效率更高。
25.1 使用new初始化对象
26.1 析构函数的调用时机
27.1 指针和对象小结
使用对象指针时,需要注意以下几点:
图示:
1.使用常规表示法来声明指向对象的指针:
String * glamour;
2.可以将指针初始化为指向已有的对象:
String * first = &sayings[0];
3.可以使用new来初始化指针,这将创建一个新的对象(有关使用new初始化指针的细节):
String * favorite = new String(saying[choice]);
图示:
4.对类使用new将调用相应的类构造函数来初始化新创建的对象:
//invokes defalut constructor
String * gsleep = new String;
//invokes the String(const chat *) constructor
String * glop = new String("my my my");
//invokes the String(const String &) constructor
String * favorite = new String(saying[choice]);
5.可以使用->运算符通过指针访问类方法:
if(saying[i].length() < shortest->length())
6 可以对对象指针应用解除引用运算符(*)来获得对象:
if (sayings[i] < *first) //compare object values
first = &sayings[i]; //assign object address
28.1 队列特征
29.1 部分总结
30.1 类构造函数特殊初始化数据成员的特殊语法
C++为类构造函数提供了一种可用来初始化数据成员的特殊语法。这种语法包括冒号和由逗号分隔的初始化列表,都放在构造函数参数的右括号后,函数体的左括号之前。
每一个初始化器都由被初始化的成员的名称和包含初始值的括号组成。
从概念来说,这些初始化操作是在对象创建时进行的,此时函数体中语句还没有执行。语法如下:
queue(int qs) : qsize(qs), item(0), front(NULL), rear(NULL) {}
如果数据成员是非静态const成员或引用,则必须采用这种格式,但可将C++11新增的类内初始化用于非静态初始化const成员。
C++11允许类内初始化,即在类定义中进行初始化:
class Queue
{
private:
...
Node * front = NULL;
enum {Q_SIZE = 10};
Node * rear = NULL;
int items = 0;
const int qszie = Q_SZIE;
...
};
这与使用成员初始化列表等价。
然而,使用成员舒适化列表的构造函数将覆盖相应的类内初始化。
31.1 类继承
可以通过集成完成的工作:
1.可以在已有类的基础上添加功能。例如:对于数组类,可以添加数学运算。
2.可以给类添加数据。例如,对于字符串类,可以派生出一个类,并添加指定字符串显示颜色的数据成员。
3.可以修改类方法的行为。例如,对于代表提供给飞机乘客的服务的Passenger类,可以派生出提供更高级别服务的FirstClassPassenger类。
31.1.1 基类
从一个类派生出另一个类时,原始类称为基类,继承类称为派生类。
为说明继承,首先需要一个基类。
派生一个类:
//RatedPlayer derives from the TableTennisPlayer base class
class RatedPlayer : public TableTennisPlayer
{
...
};
冒号指出RatedPlayer类的基类是TableTennisplayer类。public声明头表明TableTennisPlayer是一个公有基类,这被称为公有派生。派生类对象包含基类对象。
使用公有派生,基类的公有成员将称为派生类的公有成员。
基类的私有部分也将成为派生类的一部分,但只能通过基类的公有和保护方法访问。
需要在继承特性中添加:
1.派生类需要自己的构造函数。
2.派生类可以根据需要添加额外的数据成员和成员函数。
ps:派生类的构造函数必须给新成员(如果有的话)和继承的成员提供数据。
派生类不能直接访问基类的私有成员,而必须通过基类方法进行访问。
有关派生类的构造函数的要点如下:
1.首先创建基类对象;
2.派生类构造函数应通过成员初始化列表将基类信息传递给基类构造函数;
3.派生类构造函数应初始化派生类新增的数据成员。
派生类和基类成员初始化列表:
派生类构造函数可以使用初始化器列表机制将值传递给基类构造函数。
eg:
derived::derived(type1 x, type2 y) : base(x,y) //initializer list
{
...
}
其中deriver是派生类,base是基类,x和y是基类构造函数使用的变量。
如果派生类构造函数接收到参数10和12,则这种机制将把10和12传递给被定义为接受这些类型的参数的基类构造函数。除虚基类外,类只能将值传递回相邻的基类,但后者可以使用相同的机制将信息传递相邻的基类,以此类推。如果没有在成员初始化列表中提供基类构造函数,程序将使用默认的基类构造函数。
派生类和基类之间的特殊关系:
1.派生类对象可以使用基类的方法,条件是方法不是私有的;
2.基类指针可以在不进行显示类型转换的情况下指向派生类对象;
3.基类引用可以在不进行显示类型转换的情况下引用派生类对象。
eg:
RatedPlayer rplayer1(1140, "Mallort", "Duck", true);
TableTennisPlayer & rt = rplayer;
TableTennisPlayer * pt = &rplayer;
rt.Name(); //invoke Name() with reference
pt->Name(); //invoke Name() with pointer
继承:C++有三种继承方式:公有继承、保护继承和私有继承
is-a关系(还有has-a、is-implemented-as-a、uses-a)
笔者第一次接触这种,详情百度,谷歌。
多态公有继承:派生类对象使用基类的方法,而未做任何修改。
但希望同一个方法在派生类和基类中的行为是不同的,换句话说,方法的行为应取决于调用该方法的对象。这种行为叫做多态–具有多种形态。
以下两种重要的机制可用于实现多态公有继承:
1.在派生类中重新定义基类的方法;
2.使用虚方法。
ps:如果要在派生类中重新定义基类的方法,通常应将基类方法声明为虚的。这样,程序将根据对象类型而不是引用或指针的类型来选择方法版本。为基类声明一个虚析构函数也是一种惯例(这样做是为了确保释放派生对象时,按正确的顺序调用析构函数。)。
32.1 静态联编和动态联编
将源代码中的函数调用解释为执行特定的函数代码块被称为函数名联编(binding)。
在编译过程中进行联编被称为静态联编(static binding),又称为早期联编(early binding)。
因为虚函数的出现,编译器必须生成能够在程序运行时选择正确的虚方法的代码,这被称为动态联编(dynamic binding),又称为晚期联编(late binding)。
32.1.2 指针和引用类型的兼容性
在C++中,动态联编与通过指针和引用调用方法相关,从某种程度上说。这是由继承控制。公有继承建立is-a关系的一种方法是如何处理指向对象的指针和引用。
通常,C++不允许将一种类型的地址赋值给另一种类型的指针,也不允许一种类型的引用指向另一种类型。
但是,指向基类的引用或指针可以引用派生类对象,而不必进行显示类型转换。
向上强制转换:将派生类引用或指针转换为基类引用或者指针。向上强制转换是可传递的,也就是说,如果A是基类,B是派生类,B派生C类,A指针或者引用可以引用A对象,B对象或者C对象。
向下强制转换:与向上强制转换相反,将基类指针或者引用转换为派生类指针或者引用。如果不使用显示类型转换,则向下强制转换时不允许的。因为is-a关系通常是不可逆的。
32.1.3 虚成员函数
如果要在派生类中重新定义基类的方法,则将它设置为虚方法;否则,设置非虚方法。
另外,使用虚函数时,在内存和执行速度方面有一定的成本,包括:
1.每个对象都将增大,增大量为存储地址的空间;
2.对于每个类,编译器都创建一个虚函数地址表(数组);
3.对于每个函数调用,都需要执行一项额外的操作,即到表中查找地址。
虽然非虚函数的效率比虚函数高,但不具备动态联编功能。
32.1.4 虚函数注意事项
1.在基类方法的声明中使用关键字virtual可使该方法在基类以及所有的派生类(包括从派生类派生出来的类)中是虚的;
2.如果使用指向对象的引用或指针来调用虚方法,程序将使用为对象类型定义的方法,而不使用为引用或指针类型定义的方法。这称为动态联编或晚期联编。这种行为非常重要,因为这样基类指针或引用可以指向派生类对象;
3.如果定义的类将被用作基类,则应将那些要在派生类中重新定义的类方法声明为虚的。
对于虚方法,还有如下几点需要注意:
1.构造函数
构造函数不能是虚函数。创建派生类对象时,将调用派生类的构造函数,而不是基类的构造函数,然后,派生类的构造函数将使用基类的一个构造函数,这种顺序不同于继承机制。因此,派生类不继承基类的构造函数,所以将类构造函数声明为虚的没什么意义。
2.析构函数
析构函数应该是虚函数,除非类不用做基类。通常应该基类提供一个虚析构函数,即使它并不需要析构函数。原因是关于派生类对象释放的原因。
3.友元
友元不能是虚函数,因为友元不是类成员,而只有成员才能是虚函数。
4.没有重新定义
如果派生类没有重新定义函数,将使用该函数的基类版本。如果派生类位于派生链中,则将使用最新的虚函数版本。例外的情况是基类版本是隐藏的。
5.重新定义将隐藏方法
派生类重新定义不会生成函数的两个重载版本,而是隐藏了基类的函数版本。重新定义继承的方法并不是重载。如果在派生类中重新定义函数,将不是使用相同的函数特征标覆盖基类声明,而是隐藏同名的基类方法,不管参数特征标如何。
如果重新定义继承的方法,应确保与原来的原型完全相同,但如果返回类型是基类引用或者指针,则可以修改为指向派生类的引用或指针。这种特性被称为返回类型协变,因为允许返回类型随类类型的变化而变化。这种例外只适用于返回值,而不适用于参数。
如果基类声明被重载了,则应在派生类中重新定义所有的基类版本。如果派生类只重新定义一个版本,则基类其他的版本将被隐藏,派生类对象将无法使用它们。注意,如果派生类不需要修改,则定义可只调用基类版本。
33.1 访问控制: protected
访问控制还有public、private。
public是公有的,而protected和private的区别只有在基类派生的类中才会表现出来。
派生类的成员可以直接访问基类的保护成员(protected),但不能直接访问基类的私有成员(private)。
因此,对于外部世界来说,保护成员的行为与私有成员相似,但对于派生类来说,保护成员的行为与公有成员相似。
注意: 最好对类数据成员采用私有访问控制,不要使用保护访问控制,同时通过基类方法使派生类能够访问基类数据。(因为基类的设计是只能基类的成员函数才能够修改基类的成员变量)
然而,对于成员函数来说,保护访问控制很有用,它让派生类能够访问公众不能使用的内部函数。
34 抽象基类
C++通过使用纯虚函数(pure virtual function)提供未实现的函数。纯虚函数声明的结尾处为=0。
eg:
virtual double Area() const = 0; //a pure virtual function
当类声明中包含纯虚函数时,则不能创建该类的对象。这里的理念是,包含纯虚函数的类只用作基类。
ps:至少有一个虚函数是纯虚函数,这样的类才是抽象类。
35 编译器生成的成员函数
编译器会自动生成一些公有成员函数–特殊成员函数。
1.默认构造函数
默认构造函数要么没有参数,要么所有的参数都有默认值。如果没有定义任何构造函数,编译器将定义默认构造函数,让程序员能够创建对象。
自动生成的默认构造函数的另一项功能是,调用基类的默认构造函数以及调用本身是对象的成员所属类的默认构造函数。
如果派生类构造函数的成员初始化列表中没有显式调用基类构造函数,则编译器将使用基类的默认构造函数来构造派生类对象的基类部分。在这种情况下,如果基类没有构造函数,将导致编译阶段错误。
如果定义了某种构造函数,编译器将不会定义默认构造函数。在这种情况下,如果需要默认构造函数,则必须自己提供。
提供构造函数的动机之一是确保对象总能够被正确的初始化。如果类包含指针成员,则必须初始化这些成员。因此,最好提供一个显式默认构造函数,将所有的类数据成员都初始化为合理的值。
2.复制构造函数
复制构造函数接受其所属类的对象作为参数。
eg: Star(const Star &);
在下述情况下,将使用复制构造函数:
2.1.将新对象初始化为一个同类对象;
2.2.按值将对象传递给函数;
2.3.函数按值返回对象;
2.4.编译器生成临时对象;
如果程序没有使用(显式或隐式)复制构造函数,编译器将提供原型,但不提供函数定义;否则。程序将定义一个执行成员初始化的复制构造函数。也就是说,新对象的每个成员都被初始化为原始对象相应成员的值。如果成员为类对象,则初始化该成员时,将使用相应类的复制构造函数。
ps:在某些情况下,成员初始化是不合适的。如,使用new初始化的成员指针通过要求执行深复制,或者类可能包含需要修改的静态变量。这就需要定义自己的复制构造函数。
3.赋值运算符
默认的赋值运算符用于处理同类对象之间的赋值。
默认赋值为成员赋值。如果成员为类对象,则默认成员赋值将使用相应类的赋值运算符。
36 定义类的其他注意点
1.构造函数
构造函数不同于其他类方法,因为它创建新的对象,而其他类方法只是被现有的对象调用。这是构造函数不能被继承的原因之一。继承意味着派生类对象可以使用基类的方法。然而,构造函数在完成其工作之前,对象并不存在。
2.析构函数
一定要定义显式析构函数来释放类构造函数使用new分配的所有内存,并完成类对象所需的任何特殊的清理工作。对于基类,即使它不需要析构函数,也应提供一个虚析构函数。
3.转换
使用一个参数就可以调用的构造函数定义了从参数类型到类类型的转换。
将可装换的类型传递给以类为参数的函数时,将调用转换构造函数。
在带一个参数的构造函数原型中使用explicit将禁止进行隐式转换,但仍允许显式转换。
要将类对象转换为其他类型,应定义转换类型。转换函数可以是没有参数的类成员函数,也可以是返回类型被声明为目标类型的类成员函数。即使没有声明返回类型,函数也应返回所需的转换值。
C++11支持将关键字explicit用于转换函数。与构造函数一样,explicit允许使用强制类型转换进行显式转换,但不允许隐式转换。
4.按值传递对象与传递引用
为了提高效率,通常编写使用对象作为参数的函数时,应按引用而不是按值来传递对象。
按值传递对象涉及到生成临时拷贝,即调用复制构造函数,然后调用析构函数。调用这些函数需要时间,复制大型对象比传递引用花费的时间要多得多。如果函数不修改对象,应将参数声明为const引用。
按引用传递对象的另一个原因是在继承使用虚函数时,被定义为接受基类引用参数的函数可以接受派生类。
5.返回对象和返回引用
在编码方面,直接返回对象与返回引用之间的唯一区别在于函数原型和函数头:
Star noval(const Star &); //return a Star object
Star & nova2(const Star &); //return a reference to a Star
何时返回对象何时返回引用,这里有个通用规则,但不排除其他情况。
如果函数返回在函数中创建的临时对象,则不要使用引用。
如果函数返回的是通过引用或指针传递给它的对象,则应按引用返回对象。
6.使用const
使用const时应特别注意。可以用它来确保方法不修改参数;
可以使用const来确保方法不修改调用它的对象;
通常,可以将返回引用的函数放在赋值语句的左侧,这实际上意味着可以将值赋给引用的对象。但可以使用const来确保引用或指针返回的值不能用于修改对象中的数据。
注意,如果函数将参数声明为指向const的引用或指针,则不能将该参数传递给另一个函数,除非后者也确保了参数不会被修改。
37 类函数总结
38 类继承总结
继承通过使用已有的类(基类)定义新的类(派生类),使得能够根据需要修改编程代码。公有继承建立is-a关系,意味着派生类对象也应该是某种基类对象。作为is-a模型的一部分,派生类继承基类的数据成员和大部分方法,但不继承基类的构造函数、析构函数和赋值运算符。派生类可以直接访问基类的公有成员和保护成员,并能够通过基类的公有方法和保护方法访问基类的私有成员。可以在派生类中新增数据成员和方法,还可以将派生类用作基类,来做进一步的开发。每个派生类都必须有自己的构造函数。程序创建派生类对象时,将首先调用基类的构造函数,然后调用派生类的构造函数;程序删除对象时,将首先调用派生类的析构函数,然后调用基类的析构函数。
如果要将类用作基类,则可以将成员声明为保护的,而不是私有的,这样,派生类将可以直接访问这些成员。然而,使用私有成员通过可以减少出现编程问题的可能性。如果希望派生类可以重新定义基类的方法,则可以使用关键字virtual将它声明为虚的。这样对于通过指针或引用访问的对象,能够根据对象类型来处理,而不是根据引用或指针的类型来处理。具体的说,基类的析构函数通常应该是虚的。
可以考虑定义一个ABC(抽象类),只定义接口,而不涉及实现。ABC必须至少包含一个纯虚方法,可以在声明中的分号前面加上=0来声明纯虚方法。
virtual double area() const = 0;
不一定非得定义纯虚方法。对于包含纯虚成员的类,不能使用它来创建对象。纯虚方法用于定义派生类的通用接口。
39 C++中代码重用
C++的一个主要目标是促进代码重用。公有继承是实现这种目标的机制之一,但并不是唯一的机制。
1.使用这样的类成员:本身是另一个类的对象,这种方法称为包含(containment)、组合(composition)或层次化(layering)
2.使用私有或保护继承。通常,包含、私有继承和保护继承用于实现has-a关系,即新的类将包含另一个类的对象。
多重继承使得能够使用两个或更多的基类派生出新的类,将基类的功能组合在一起。
39.1 包含对象成员的类
接口和实现:使用公有继承时,类可以继承接口,可能还有实现(基类的纯虚函数提供接口,但不提供实现)。或者接口是is-a关系的组成部分。而使用组合,类可以获得实现,但不能获得接口。不继承接口是has-a关系的组成部分。
39.2 C++和约束
C++包含让程序员能够限制程序结构的特性–使用explicit防止单参数构造函数的隐式转换,使用const限制方法修改数据,等待。这样做的根本原因是:在编译阶段出现错误优于在运行阶段出现错误。
39.3 初始化顺序
当初始化列表包含多个项目时,这些项目被初始化的顺序为它们被声明的顺序,而不是它们在初始化列表中的顺序。
40 私有继承
C++还有另一种实现has-a关系的途径–私有继承。使用私有继承,基类的公有成员和保护成员都将成为派生类的私有成员。这意味着基类方法将不会成为派生对象公有接口的一部分,但可以在派生类的成员函数中使用它们。
使用公有继承,基类的公有方法将成为派生类的公有方法。总之,派生类将继承基类的接口;这是is-a关系的一部分。使用私有继承,基类的公有方法将成为派生类的私有方法。总之,派生类不继承基类的接口。正如从被包含对象中看到的,这种不完全继承是has-a关系的一部分。
使用私有继承,类将继承实现。
包含将对象作为一个命名的成员对象添加到类中,而私有继承将对象作为一个未被命名的继承对象添加到类中。
因此,私有继承提供的特性与包含相同:获得实现,但不获得接口。所以,私有继承也可以用来实现has-a关系。
要进行私有继承,要使用关键字private而不是public来定义类。(实际上,private是默认值,因此省略访问限定符也将导致私有继承)
使用多个基类的继承被称为多重继承(multiple inheritance,MI)
40.1 使用包含还是私有继承
由于既可以使用包含,也可以使用私有继承来建立has-a关系,那么应使用何种方式呢?
使用包含的优点:
1.易于理解,类声明中包含表示被包含类的显式命名对象,代码可以通过名称引用这些对象,而使用继承将使关系更抽象。
2.继承会引起很多问题,尤其从多个基类继承时,可能必须处理很多问题。如包含同名方法的独立的基类或共享祖先的独立基类。
3.包含能够包括多个同类的子对象。
通常,应使用包含来建立has-a关系;如果新类需要访问原有类的保护成员,或需要重新定义虚函数,则应使用私有继承。
41 保护继承
保护继承是私有继承的变体。保护继承在列出基类时使用关键字protected:
class Student : protected std::string,
protected std::valarray
{...};
使用保护继承时,基类的公有成员和保护成员都将成为派生类的保护成员。
使用保护继承时,基类的公有方法在第二代中将变成受保护的,因此第三代派生类可以使用它们。
41.1 各种继承方式
42 多重继承(MI)
MI描述的是有多个直接基类的类。与单继承一样,公有MI表示的也是is-a关系。例如,可以从Waiter类和Singer类派生出SingingWaiter类:
class SingingWaiter : public Waiter, public Singer {...};
注意,必须使用关键字public来限定每一个基类。这是因为,除非特别指出,否则编译器将认为是私有派生:
class SingingWaiter : public Waiter,Singer {...}; //Singer is a private base
MI可能带来如下主要问题:
1.从两个不同的基类继承同名方法;
2.从两个或者更多相关基类那里继承同一个类的多个实例。
解决方法:祖先相同的MI
定义一个抽象基类Worker,并使用它派生出Waiter类和Singer类。然后,便可以使用MI从Waiter类和Singer类派生出SingingWaiter类。
43 虚基类
C++引入多重继承的同时,引入了一种新技术–虚基类(virtual base class),使MI成为可能。
虚基类使得从多个类(它们的基类相同)派生出的对象只继承一个基类对象。
通过在类的声明中使用关键字virtual,可以使Worker被用作Singer和Waiter的虚基类(virtual和public的次序无关紧要):
class Singer : virtual public Worker {...};
class Waiter : public virtual Worker {...};
然后,可以将SingingWaiter类定义为:
class SingingWaiter : public Singer, public Waiter {...};
ps:在基类不是虚的时候,C++允许信息通过中间类自动传递给基类。
eg:A是基类,B继承A,C继承B,C可以将信息通过构造传递给B,B可以将信息通过构造传递给A。
但是,在基类是虚基类的时候,C++禁止信息通过中间类自动传递给基类。
警告:
1.如果类有间接虚基类,则除非只需使用该虚基类的默认构造函数,否则必须显式的调用该虚基类的某个构造函数。
2.多重继承可能导致函数调用的二义性。(可以使用作用域解析运算符来说明或者在多重继承的派生类里重新定义该方法)
总之,在祖先相同时,使用MI必须引入虚基类,并修改构造函数初始化列表的规则。另外,如果在编写这些类时没有考虑到MI,则还可能需要重新编写它们。
有关MI的问题:
1.混合使用虚基类和非虚基类
如果基类是虚基类,派生类将包含基类的一个子对象;如果基类不是虚基类,派生类将包含多个子对象。当虚基类和非虚基类混合时(当类通过多条虚途径和非虚途径继承某个特定的基类时,该类将包含一个表示所有的虚途径的基类子对象和分别表示各条非虚途径的多个基类子对象)
2.虚基类和支配
使用虚基类将改变C++解析二义性的方式。使用非虚基类时,规则很简单。如果类从不同的类那里继承了两个或者更多的同名成员(数据或者方法),则使用该成员名时,如果没有用类名进行限定,将导致二义性。
如果使用的是虚基类(即使没有使用作用域限定符),派生类中的名称优先于直接或间接祖先类中相同名称。
44 MI小结(多重继承小结)
不适用虚基类的MI,如果一个类从两个不同的类那里继承了两个同名的成员,则需要在派生类中使用类限定符来区分它们。否则,编译器将指出二义性错误。如果一个类通过多种途径继承了一个非虚基类,则该类从每种途径分别继承非虚基类的一个实例。这样,多个基类实例都是问题。
使用虚基类的MI,当派生类使用关键字virtual来指示派生时,基类就称为虚基类:
class marketing : public virtual reality {...};
对于非虚基类的主要变化(也是使用虚基类的原因)就是,从虚基类的一个或多个实例派生而来的类将只继承了一个基类对象。为实现这种特性,必须满足以下要求:
1.有间接虚基类的派生类包含直接调用间接基类构造函数的构造函数,这对于间接非虚基类来说是非法的;
2.通过优先规则解决名称二义性。
MI增加编程的复杂性主要是由于派生类通过多条途径继承同一个基类引起的。避免这种情况后,唯一需要注意的就是:在必要时对继承的名称进行限定。
45 类模板
C++的类模板为生成通用的类声明提供了一种更好的方法(C++最初不支持模板,但模板被引入后,就一直在演化)。
模板提供参数化(parameterized)类型,即能够将类型名作为参数传递给接收方来建立类或函数。
45.1 定义类模板
采用模板类,将使用模板定义替换类声明,使用模板成员函数替换类的成员函数。和模板函数一样,模板类以下面的代码开头:
template
关键字template告诉编译器,将要定义一个模板。尖括号中内容相当于函数的参数列表。可以把关键字class看作是变量的类型名,该变量接受类型作为其值,把Type看作是该变量的名称。
另外,这里使用class并不意味着Type必须是一个类;而只是表明Type是一个通用的类型说明符,在使用模板时,将使用实际的类型替换它。较新的C++实现允许使用不太容易混淆的关键字typename代替class。
template //newer choice
可以使用自己的泛型名代替Type,其命名规则与其他标识符相同。当前流行的选项包括T和Type.
在模板定义中,可以使用泛型名来标识要存储在栈中的类型。
45.2 使用模板类
仅在程序包含模板并不能生成模板类,而必须请求实例化。为此,需要声明一个类型为模板类的对象,方法是使用所需的具体类型替换泛型名。
eg:
Stack kernels; //create a stack of ints
泛型标识符–称为类型参数(type parameter),这意味着它们类似于变量,但赋给它们的不能是数字,而只能是类型。
注意,对于模板类,必须显式的提供所需的类型,这与常规的函数模板是不同的,因为编译器可以根据函数的参数类型来确定要生成哪种函数。
45.2 数组模板示例和非类型参数
模板常常用作容器类,这是因为类型参数的概念非常适合于将相同的存储方案用于不同的类型。
template
关键字class(可替换为typename)指出T为类型参数,int指出n的类型为int。这种参数(指定特殊的类型而不是用作泛型名)称为非类型(non-type)或表达式(expression)参数。
eg:
ArrayTP eggweights;
这将导致编译定义名为ArrayTP
注意:表达式参数有部分限制,表达式参数可以是整型、枚举、引用或者指针。
模板代码不能修改参数的值,也不能使用参数的地址。
实例化模板时,用作表达式参数的值必须式常亮表达式。
45.3 模板多功能性
*.可以用于常规类的技术用于模板类。模板类可以用作基类or组件类or其他模板的类型参数。
*.可以递归使用模板。
eg:
ArrayTP ,10> twodee;
等价
int twodee[10][5];
*.模板可以包含多个类型参数。
*.可以为类型参数提供默认值。
eg:
template class Topo {...};
45.4 模板的具体化
模板以泛型的方式描述类,而具体化是使用具体的类型生成类声明
1.隐式实例化
声明一个或多个对象,指出所需的类型,而编译器使用通用模板提供的处方生成具体的类定义
2.显式实例化
当使用关键字template并指出所需类型来声明类时,编译器将生成类声明的显式实例化(explicit instantiation)。声明必须位于模板定义所在的名称空间中。
3.显式具体化
显示具体化(explicit specialization)是特定类型(用于替换模板中的泛型)的定义。在需要为特殊类型实例化时,对模板进行修改,使其行为不同。可以创建显式具体化。
4.部分具体化
C++还允许部分具体化(partial specialization),即部分限制模板的通用性。
45.5 将模板用作参数
模板可以包含类型参数(typenameT)和非类型参数(int n)。模板还可以包含本身就是模板的参数。
eg:
template class Thing>
模板参数是template class Thing,其中template calss 是类型,Thing是参数。
45.6 模板类和友元
模板类声明也可以有友元,模板的友元分为三类:
1.非模板友元;
在模板类中将一个常规函数声明为友元
template
class HasFriend
{
public:
friend void counts(); //friend to all HasFriend instantiations
...
};
该声明使counts()函数成为模板所有实例化的友元。如,它将是类hasFriend和HasFriend的友元。
因为counts函数是友元,不是成员函数,所以它不是通过对象调用的,也没有对象参数。
如何访问HasFriend对象?如:
* 访问全局对象;
* 使用全局指针访问非全局对象;
* 创建自己的对象;
* 访问独立于对象的模板类的静态数据成员。
为友元函数提供模板类参数?
eg:
friend void report(HasFriend &); //possible? NO
因为不存在HasFriend这样的对象,只有特定的具体化。
要提供模板类参数,必须指明具体化。
friend void report(HasFriend &); //possible? YES bound template friend
2.约束(bound)模板友元,即友元的类型取决于类被实例化时的类型;
使友元函数本身成为模板。使类的每一个具体化都获得与友元匹配的具体化。
在类定义的前面声明每个模板函数.
template void counts();
template void report(T &);
然后,在函数中再次将模板声明为友元。这些语句根据类模板参数的类型声明具体化:
template
class HasFriendT
{
...
friend void counts();
friend void report<>(HasFriendT &);
};
声明中的<>指出这是模板具体化。对于report(),<>可以为空,因为可以从函数参数推断出如下模板类型参数:
HasFriendT
也可以使用:
report > (HasFriendT &)
但是counts()函数没有参数,因此必须使用模板参数语法() 来指明其具体化。TT是HasFriendT类的参数类型。
3.非约束(unbound)模板友元,即友元的所有具体化都是类的每一个具体化的友元。
约束模板友元函数是在类外面声明的模板的具体化。int类具体化获得int函数具体化,以此内推。通过在类内部声明模板,可以创建非约束友元函数,即每个函数具体化都是每个类具体化的友元。对于非约束友元,友元模板类型参数与模板类类型参数是不同的:
template
class ManyFriend
{
...
template friend void show2(C &,D &);
//所有typename 具体化的友元
};
45.7 模板别名(C++11)
通常,可以使用typedef为模板具体化指定别名:
eg:
//define three typedef aliases
typedef std::arrary arrd;
typedef std::arrary arri;
typedef std::arrary arrst;
arrd gallons; //gallons is type std::arrary
arri days; //gallons is type std::arrary
arrst months; //months is type std::arrary
另外,C++11新增了一项功能,使用模板提供一系列别名,
template
using arrtype = std::arrary; //template to create multiple aliases
这将arrtype定义为一个模板别名,可以使用它来指定类型,如下所示:
arrtype gallons; //gallons is type std::arrary
arrtype days; //days is type std::array
arrtype months; //months is type std::array
总之,arrtype表示类型std::array。
C++11允许将语法using=用于非模板。用于非模板时,这种语法与常规typedef等价。
46 代码重用和类模板总结
公有继承能够建立is-a关系,这样派生类可以重用基类的代码。私有继承和保护继承也使得能够重用基类的代码,但建立的是has-a关系。使用私有继承时,基类的公有成员和保护成员将成为派生类的私有成员;
使用保护继承时,基类的公有成员和保护成员将成为派生类的保护成员。
无论使用哪种继承,基类的公有接口都将成为派生类的内部接口。
这有时候被称为继承实现,但并不继承接口,因为派生类对象不能显式的使用基类的接口。
因此,不能将派生对象看作是一种基类对象。
由于这个原因,在不进行显式类型转换的情况下,基类指针或引用将不能指向派生类对象。
通过开发包含对象成员的类来重用类代码。这种方法被称为包含、层次化或组合,它建立的也是has-a关系。与私有继承和保护继承相比,包含更容易实现和使用,通常优先采用这种方式。然而,私有继承和保护继承比包含有一些不同的功能。
如:继承允许派生类访问基类的保护成员,还允许派生类重新定义从基类那里继承的虚函数。因为包含不是继承,所以通过包含来重用类代码时,不能使用这些功能。另一方面,如果需要使用某个类的几个对象,则用包含更合适。
多重继承(MI)使得能够在类设计中重用多个类的代码。私有MI或保护MI建立has-a关系,而公有MI建立is-a关系。MI会带来一些问题,即多次定义同一个名称,继承多个基类对象。可以使用类限定符来解决名称二义性的问题,使用虚基类来避免继承多个基类对象的问题。但使用虚基类后,就需要为编写构造函数初始化列表以及解决二义性问题引入新的规则。
类模板使得能够创建通用的类设计,其中类型(通常是成员类型)由类型参数表示。
类定义(实例化)在声明类对象并指定特定类型时生成。
使用关键字template声明类的特定具体化时,将发生显式实例化。
可以提供显式具体化–覆盖模板定义的具体类声明。方法是以template<>打头,然后是模板类名称,在加上尖括号(其中包含要具体化的类型)。
47 友元
将友元函数用于类的扩展接口中,类并非只能拥有友元函数,也可以将类作为友元。
47.1 友元类
47.2 共同的友元
使用友元的另一种情况:函数需要访问两个类的私有数据。
eg:
class Analyzer; //forward declaration
class Probe
{
friend void sync(Analyzer & a, const Probe & p); //sync a to p
friend void sync(Probe & p, const Analyzer & a); //sync p to a
...
};
class Analyzer
{
friend void sync(Analyzer & a, const Probe & p); //sync a to p
friend void sync(Probe & p,const Analyzer & a); //sync p to a
...
};
//define the friend functions
inline void sync(Analyzer & a, const Probe & p)
{
...
}
inline void sync(Probe & p, const Analyzer & a)
{
...
}
前向声明使编译器看到Probe类声明中的友元声明时,知道Analyzer是一种类型。
48 嵌套类
C++中,可以将类声明放在另一个类中。在另一个类中声明的类被称为嵌套类(nested class),我更习惯叫内部类。?它通过提供新的类型类作用域来避免名称混乱。包含类的成员函数可以创建和使用被嵌套类的对象;而仅当声明位于公有部分,才能在包含类的外面使用嵌套类,而且必须使用作用域解析运算符。
对类进行嵌套与包含并不同。包含意味着将类对象作为另一个类的成员,而对类进行嵌套不创建类成员,而是定义了一种类型,该类型仅在包含嵌套类声明的类中有效。
48.1 嵌套类和访问权限
作用域:
如果嵌套类是在另一个类的私有部分声明的,则只有后者知道它。
如果嵌套类是在另一个类的保护部分声明的,则它对于后者来说是可见的,但是对于外部世界则是不可见的。这种情况中,派生类将知道嵌套类,并可以直接创建这种类型的对象。
如果嵌套类是在另一个类的公有部分声明的,则允许后者,后者的派生类以及外部世界使用它。因为它是公有的。然而,由于嵌套类的作用域为包含它的类,因为在外部时间使用它时,必须使用类限定符。
48.2 访问控制
类声明的位置决定了类的作用域或可见性。类可见后,访问控制规则(公有、保护、私有、友元)将决定程序对嵌套类成员的访问权限。
49 异常
49.1 调用abort()
49.2 返回错误码
49.3 异常机制
C++异常是对程序运行过程中发生的异常情况的一种响应。异常提供了将控制权从程序的一个部分传递到另一部分的途径。对异常的处理有三个组成部分:
1.引发异常;
2.使用处理程序捕获异常;
3.使用try块。
*. 程序在出现问题时将引发异常。throw语句实际上是跳转,即命令程序跳到另一条语句。throw关键字表示引发异常,紧随其后的值指出了异常的特征。
*. 程序使用异常处理程序(exception handler)来捕获异常,异常处理程序位于要处理问题的程序中。catch关键字表示捕获异常。处理程序以关键字catch开头,随后是位于括号中的类型声明,它指出了异常处理程序的异常类型;然后是一个用花括号括起的代码块,指出要采取的措施。catch关键字和异常类型用作标签,指出当异常被引发时,程序应跳到这个位置执行。异常处理程序也被称为catch块。
*. try块标识其中特定的异常可能被激活的代码块,它后面跟一个或多个catch块。try块是由关键字try指示的,关键字try的后面是一个由花括号括起的代码块,表明需要注意这些代码引发的异常。
49.4 将对象用作异常类型
引发异常的函数将传递一个对象。优点之一是可以使用不同的异常类型来区分不同的函数在不同情况下引发的异常。另外,对象可以携带信息,并且,catch块可以根据这些信息来决定采取什么样的措施。
49.5 异常规范和C++11 (C++11抛弃异常规范,不建议使用,不记录)
49.6 栈解退
假设函数由于出现异常(而不是由于返回)而终止,则程序也将释放栈中的内存,但不会在释放栈的第一个返回地址后停止,而是继续释放栈,直到找到一个位于try块(如图)中的返回地址。随后,控制权将转到块尾的异常处理程序,而不是函数调用后面的第一条语句。这个过程被称为栈解退。
函数返回仅仅处理该函数放在栈中的对象,而throw语句则处理try块和throw之间整个函数调用序列放在栈中的对象。
如果没有栈解退这种特性,则引发异常后,对中间函数调用放在栈中的自动类对象,其析构函数将不会被调用。
49.7 其他异常特性
虽然throw-catch机制类似于函数参数和函数返回机制,但还有如下不同:
1.函数fun()中的返回语句将控制权返回到调用fun()的函数,但throw语句将控制权向上返回到第一个这样的函数:包含能够捕获相应异常的try-catch组合。
2.引发异常时编译器总是创建一个临时拷贝,即使异常规范和catch块中指定的是引用。
提示:如果有一个异常类继承层次结构,应这样排列catch块:将捕获位于层次结构最下面的异常类的catch语句放在最前面,将捕获基类异常的catch语句放在最后面。
49.8 exception类
异常是这样的一种特性:类似于类,可以改变编程方式。C++提供exception头文件,定义了exception类,C++可以把它用作其他异常类的基类。代码可以引发exception异常,也可以将exception类用作基类。
C++库定义了一些基于exception的异常类型:
1.stdexcept异常类
2.bad_alloc异常和new
3.空指针和new
49.8 异常、类和继承
异常、类和继承以三种方式相互关联:
*. 可以像标准C++库所做的那样,从一个异常类派生出另一个;
*. 可以在类定义中嵌套异常类声明来组合异常;
*. 这种嵌套声明本身可以被继承,还可用作基类。
49.9 异常何时会迷失方向
异常被引发后,在两种情况下,会导致问题。
*. 如果它是在带异常规范的函数中引发的,则必须与规范列表中的某种异常匹配(在继承层次结构中,类类型与这个类及其派生类的对象匹配),否则称为意外异常(unexpected exception)。
49.10 有关异常的注意事项
异常处理应在设计程序时就加入异常处理功能,而不是以后再添加。
但是,使用异常会增加程序代码,降低程序的运行速度。异常规范不适用于模板,因为模板函数引发的异常可能随特定的具体化而异。
50 RTTI
RTTI是运行阶段类型识别(Runtime Type Identification)的简称。
RTTI旨在为程序在运行阶段确定对象的类型提供一种标准方式。
创建一种RTTI语言标准将使得未来的库能够彼此兼容。
50.1 RTTI的工作原理
C++有三个支持RTTI的元素
*. 如果可能的话,dynamic_cast运算符将使用一个指向基类的指针来生成一个指向派生类的指针,否则,该运算符返回0–空指针。
*. typeid运算符返回一个指出对象的类型的值。
*. type_info结构存储了有关特定类型的信息。
RTTI只适用于包含虚函数的类,因为只有对于这种类层次结构,才应该将派生对象的地址赋给基类指针。
51 类型转换运算符
C++更严格的限制允许的类型转换,并添加4个类型转换运算符
1.dynamic_cast
dynamic_cast (expression)
该运算符的用途是,使得能够在类层次结构中进行向上转换(由于is-a关系,这样的类型转换是安全的),而不允许其他转换。
2.const_cast
该运算符用于执行只有一种用途的类型转换,即改变值为const或volatile,其语法与dynamic_cast运算符相同:
const_cast (expression)
const_cast不是万能的。它可以修改指向一个值的指针,但修改const值的结构是不确定的。
3.static_cast
staic_cast (expression)
仅当type_name可被隐式转换为expression所属的类型或expression可被隐式转换为type_name所属类型时,上述staic_cast转换才是合法的,否则将出错。
4.reinterpret_cast
reinterpret_cast运算符用于天生危险的类型转换。它不允许删除const,但会执行其他令人生厌的操作。该运算符的语法与另外三个相同:
reinterpret_cast (expression)
52 友元、异常和其他总结
友元使得能够为类开发更灵活的接口。类可以将其他函数、其他类和其他类的成员函数作为友元。
在某些情况下,可能需要使用前向声明,需要特比注意类和方法声明的顺序,以正确的组合友元。
嵌套类是在其他类中声明的类,它有助于设计这样的助手类,即实现其他类,但不必是公有接口的组成部分。
C++异常机制为处理拙劣的编程事件,如不适当的值、I/O失败等,提供了一种灵活的方式。引发异常将终止当前执行的函数,将控制权传给匹配的catch块。catch块紧跟在try块的后面。这些代码试图解决问题或终止程序。类可以包含嵌套的异常类,嵌套异常类在相应的问题被发现时将被引发。
函数可以包含异常规范,指出在该函数中可能引发的异常;但C++11摒弃了这项功能。未被捕获的异常(没有匹配的catch块的异常)在默认情况下将终止程序,意外异常(不与任何异常规范匹配的异常)也是如此。
RTTI(运行阶段类型信息)特性让程序能够检测对象的类型。dynamic_cast运算符用于将派生类指针转换为基类指针,其主要用途是确保可以安全的调用虚函数。Typeid运算符返回一个type_info 对象。可以对两个typeid的返回值进行比较,以确定对象是否为特定的类型,而返回的type_info对象可用于获得关于对象的信息。
与通用转换机制相比,dynamic_cast、static_cast、const_cast和reinterpre_cast提供了更安全、更明确的类型转换。
//todo:第十章
//todo:第十五章
//todo: page:672