CPP Primer Plus Note
- 1 类型转换。
- 2 以列表初始化变量时的类型转换(C++11)。
- 3 union共用体
- 4 枚举量enum
- 5 关于存储单元
- 6 关于数组的地址和数组的大小
- 7 存储类型,静态变量
- 8 运算符的优先级
- 9 break和coutinue
- 10 关于头文件
- 11关于const 。指针和const以及引用和const
- 12 函数指针
- 13 临时变量、引用参数和const
- 14 左值和右值
- 15 函数模板
- 16 关于默认构造函数
- 17 作用域为类的常量
- 18 关于友元
- 19 类的类型转换
- 20 基于对象和面向对象
- 21 成员初始化列表和C++11的类内初始化
- 22 对多态的理解
- 23 为何需要虚析构函数
- 24 多态公有继承
- 25 动态联编和静态联编
- 26 继承和动态内存分配
- 27 关于has - a关系 (包含和私有继承)
- 28 含虚基类的多重继承
- 29 类模板
- 30 友元类
- 31 RTTI
- 32 const_cast
- 33 智能指针
- 34 右值引用 和 移动语义
- 35 特殊的成员函数
1 类型转换。
将浮点数转换为整形时,C++采用截取而不是四舍五入的方法。
2 以列表初始化变量时的类型转换(C++11)。
1、 将使用{ }的初始化方式称为列表初始化。列表初始化来初始化变量并且存在类型转换的时候,类型转换的规则要比直接=严格。具体的说,列表初始化不允许缩窄转换(比如浮点数转换为整形在列表初始化中是不被允许的,而这在一般的=初始化中是被允许的,=初始化既允许缩窄转换,也允许扩张转换)。
2、 使用列表初始化变量时,大括号前的=可以省略。
3 union共用体
他能够存储不同的数据类型,类似于结构体。但是只能同时存储其中的一种类型。所以共用体的长度为其最大成员的长度。
4 枚举量enum
使用enum的方式和结构体非常类似,他们的句法结构类似。
如:
enum spectrum(red,orange,yellow,green,blue,violet,indigo,utraviolet);
spectrum被称为枚举。red、orange等作为符号常量,他们对应整数值0~7.这些常量叫做枚举量。
spectrum band;
这就声明了一个这种类型的变量。这个变量只能被赋值为八个枚举量或者8个枚举量对应的整数。将一个非法值赋给他,编译器会发出警告。
5 关于存储单元
一般来讲,计算机是按照字节编址。即一个字节是一个存储单元。地址加1,就是移动一个存储单元,就是加8位。
6 关于数组的地址和数组的大小
假设有 short tell[10];
实际上,数组名字就是数组首元素的地址,即tell==&tell[0]; *(tell+1)==tell[1];
对数组名字使用sizeof 得到的是整个数组的大小。(即使数组名字可看做是一个指针!)
注意区分:
- 对数组名字取地址的时候,得到的是整个数组的地址;
- 数组名字代表的是数组首元素的地址
即使这两个地址相同,但是还是有区别的。当地址+1的时候区别就会显示出来!
7 存储类型,静态变量
1、比如,区分静态内存分配和静态存储:
- 和静态存储相关系的一组是,自动存储(栈)、静态存储(全局变量或者是static变量,存储在全局区)、动态存储(堆)。
- 和静态内存分配对应的是一组词汇是,静态内存分配、动态内存分配。
- 其中静态内存分配包括:自动存储和静态存储。
2、自动存储、静态存储、动态存储变量的链接性:
- 自动变量作用域为局部,没有链接性。
- 静态变量有3中链接性,分别是外部链接性(可在其他文件访问)、内部链接性(只能在当前文件访问)和无链接性(只能在当前函数或代码段中访问)。
- 动态存储的链接性比较灵活,具体要看怎么使用。
3、静态变量的链接性
- 如果没有显式的初始化静态变量,编译器会将对他们进行0初始化。而对自动变量来说,没有进行初始化是不能使用的!因为值会不确定。
- 要想创建链接性为外部的静态变量,必须在代码块外面声明它
- 要想创建链接性为内部的静态变量,必须在代码块的外面声明它,并使用static限定符
- 要想创建没有链接性的静态变量,必须在代码块内声明它,并使用static限定符。
4、具有外部链接性的静态变量
具有外部链接性的静态变量又被叫做外部变量,或全局变量。
- 单定义规则:变量只能有一次定义。
- 要想在多个文件中使用外部变量,只需在一个文件中包含该变量的定义(单定义规则),但在使用该变量的其他所有文件中,都必须使用关键字extern声明它。
- 在函数内部声明了一个与全局变量同名的变量,结果是在该函数内部,自动变量将隐藏全局变量。
5、具有内部链接性的静态变量
- 链接性为内部的变量只能在其所属的文件中使用。
- 如果文件定义了一个static静态变量,其名称与另一个文件中声明的常规外部变量相同,则在该文件中,static静态变量将隐藏常规外部变量。
- 如果这种局部静态变量没有显示初始值,他将执行值初始化,内置类型的局部静态变量初始化为0。(自动变量的没有显示初始化是不能被使用的)
6、无链接性的静态变量
- 在代码块中使用static时,将导致局部变量的存储持续性变为静态的,这意味着虽然该变量只在给代码块中可用,但他在该代码快不处于活状态时仍然存在。
- 静态局部变量,也就是代码块中的static变量,只在程序启动进行一次初始化,以后再调用代码块时,将不会像自动变量那样再次被初始化。
7、 const对存储类型的影响
- 在默认情况下全局变量的链接性为外部的,但const全局变量的链接性为内部的。也就是说,在C++看来,全局const定义就像使用了static说明符一样。
即:
const int finger=10 //same as static const int finger =10;
int main()
{
...
}
- 这么规定是因为,常量通常放在头文件中,并在同一个程序的多个文件中使用该头文件,那么预处理器将头文件的内容包含到每个源文件中后,所有的源文件都将包含类似下面的定义:const int finger =10; 假如全局const声明的链接性像常规外部变量那样,那么将违反单定义规则。
- 链接性为内部的还意味着,包含头文件的每个文件都将有自己的一组常量,而不是所有文件共享一组常量。
- 如果想在多个文件中共享const对象,必须在变量的定义之前加extern关键字,在使用的这个变量的其他文件里也用extern声明这个变量。
8、函数的存储持续性和链接性
- 所有函数的存储持续性都默认为静态的,且是具有外部链接的。即可以在文件间共享。
- 可以使用关键字static使函数的链接性设置为内部的,使之只能在一个文件中使用。这种情况下,函数的声明和定义都必须在此文件中,且声明和定义中都必须使用static关键字。和变量一样,在定义static静态函数的文件中,static静态函数将覆盖外部定义。
- 单规则定义也适用于非内联函数,程序只能包含一个函数定义。内联函数不受这项规则的约束,这使得我们可以将内联函数放在头文件中,这样包含了头文件的每个文件都将有内联函数的定义。
补充:
内存到底分几个区?(见操作系统)
- 1、栈区(stack)— 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。
- 2、堆区(heap) — 一般由程序员分配释放, 若程序员不释放,程序结束时可能由os回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。
- 3、全局区(静态区)(static)—全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束后有系统释放。
- 4、文字常量区 — 常量字符串就是放在这里的。 程序结束后由系统释放。
- 5、程序代码区 — 存放函数体的二进制代码。
8 运算符的优先级
逻辑运算符||和&&的优先级都低于关系运算符(<,>,==)等,所以,有||和&&的时候括号可以不加。
而最后一个逻辑运算符!(非)的优先级要高于关系运算符,所以要对表达式求反,要先对表达式加括号,如!(X>5);
9 break和coutinue
break和continue语句都能使程序调过部分代码。可以再swich语句或任何循环中使用break,使程序跳到swich语句的后面或者是循环的后面(跳出循环)。continue语句用于循环中,让程序跳过循环体中的剩余代码,来立即开始下一次循环。
10 关于头文件
头文件一般是放函数声明(原型)、类声明、结构声明模板声明、内联函数以及#define或const定义的符号常量。
头文件不包括函数定义和变量声明。一般来讲,头文件的内容是不会创建变量的,而只是告诉编译器如何生成变量,唯一的例外是const变量和内联函数!!(头文件里的const条款7有说明。关于内联函数,可以放在头文件的原因是,内联函数可以被多次重定义,单定义规则对内联函数不适用)
实际上,我们程序开头包含的库函数的头文件不包括函数定义。库函数的函数定义是在库中的.o文件中。在编译中链接的过程才会将函数的定义链接起来。
11关于const 。指针和const以及引用和const
区分指向常量的指针和常量指针。
例1:指向常量的指针
int age=99;
int sage=80;
const int *pt=&age;
- 这里pt是指向常量的指针。也就是说,pt指向const int。不能用pt来修改这个值,如不能 *pt+=1。
- 但是实质上age并不是const,可以通过age=10或者其他途径来改变age,但就是不能通过指针pt。
- pt是一个指向常量的指针,但是pt并不是常量指针。也就说,pt还可以指向别处。如pt=&sage。
注:指向常量的指针可以指向常规变量,也可以指向const变量。但是常规指针不能指向const变量。也就是说:
const csge=70;
int *pt1=&cage;//INVALID!
const int *pt2=&cage;//VALID
例2: 常量指针
int age=90;
int *const finger=&age;
这种声明表示finger是一个常量指针,只能指向age,但是允许finger来修改age的值。
例3:绑定常量的引用
常量引用(绑定常量的引用)就是对const的引用。按理来说,引用也应该和指针一样有上面两种情况,但是引用天然的就是类似于常量指针(绑定的对象不能变,但是所绑定对象的值可以变),所以,只考虑类似于指针的第一种情况,也就是所谓的绑定常量的引用,即书上所说的const的引用。
int i=42;
int &r1=i;
const int &r2=i;//
上面r2就是一个绑定常量的引用,不能通过r2改变i的值(和上面例一是一样的),但是i的值还是可以通过其他途径改变的(和上面例一是一样的)。这里需要注意的是,i可以是const,也可以不是。
12 函数指针
CPP primer plus P241
13 临时变量、引用参数和const
例如有两个函数:
double cu(double ra);
double cube(double &ra);
double refcube(const double &ra);
-
cu函数是按值传递的,因此可以使用多种类型的实参,比如可以向其传递表达式、double、int等等,既可以是左值,又可以是右值,也可是类型不匹配但是可以进行类型转换的左值或右值。
-
但是向cube或者refcube传递参数时,条件将变得严格。
先说cube,他应该接受一个左值变量,不能向其传入类型不匹配或者要进行类型转换的类型,否则编译器是会报错的。比如说,ra是一个引用变量,是一个左值,如果向其传入右值(如7.0、表达式、int)或需进行类型转换的左值或右值(long a=5L),假设函数会成功调用,那么一定会产生临时变量。如果这个函数的作用是改变ra引用的值,当有临时变量存在的时候,改变的就不再是原来的引用变量,所以矛盾。所以编译器拒绝接受传入这样的参数。 -
对refcube,情况又有点不一样。C++规定,函数的形参是引用参数,并且引用参数是const的时候,才会生成临时变量。也就是说,当引用参数是const的时候,上面所描述的cube所不适用的参数,是可以适用于refcube的。因为引用参数是const时,函数并不会修改引用变量,所以即使产生临时变量也没关系。函数的形参是引用参数,并且引用参数是const的时候,才会生成临时变量。
总而言之,当引用参数是const,则编译器将在下面两种情况下生成临时变量。
- 实参的类型正确,但是不是左值
- 实参的类型不正确,但是可以转换为正确的类型。
所以下面的调用是成立的:
double side=3.0;
long edge =5L;
double c=refcube(side);
double c1=refcube(edge);
double c2=refcube(7.0);
double c3=refcube(side+10.0);
但是上面的refcube若是换成cube,那么只有第三行的那个调用才成立。
14 左值和右值
左值参数是可以被引用的数据对象,例如,变量、数组元素、结构成员、引用和解除引用的指针都是左值。非左值包括字面常量(用引号括起来的字符串除外,它们由其地址表示)和包含多项的表达式。右值的特点是可以出现在赋值表达式的右边,但是不能对其应用地址运算符的值。在c语言中,左值最初是的是可出现在赋值语句左边的实体,但这是引入关键字const之前的情况。现在,常规变量和const变量都可以视为左值,因为可通过地址访问它们。但常规变量属于可修改的左值,而const变量属于不可修改的左值。
15 函数模板
1、重载的函数模板
和常规函数重载一样,被重载的模板的函数特征标必须不同。所谓特征标不同,就是模板接受的形参类型、个数不一样。实质上,当函数名相同,特征标不一样的函数或者模板,编译器在做出处理的时候,会对名称进行mingling,所以函数或模板名相同,但是特征标不同时,在编译器看来是完全两个不同的函数或模板。
2 、显式具体化,就是模板的特化(和重载无关系)
下面展示用于交换job结构的非模板函数、模板函数、特化的模板函数原型:
void swap(job&, job&);//非模板函数
template <typename T>
void swap(T&, T&); // 模板函数
template<> void swap<job>(job&,jon &); //特化模板
如果由多个原型,则编译器在选择原型的是时候,非模板版本优先于模板特化版本和模板版本,而模板特化版本优先于使用模板生成的版本。
16 关于默认构造函数
当且仅当没有定义任何构造函数的时候,编译器才会提供默认构造函数。为类定义了构造函数后,程序员就必须为他提供默认构造函数。如果提供了非默认构造函数,但没有提供默认构造函数,下面的声明将出错:Stock stack1;
合成的默认构造函数对对象(中的内置类型或复合类型)的初始化类似于常规的自动变量,也就是说,他使得成员的初始化值时不确定的。
一个好的做法是,设计类时,要提供对所有类成员做隐式初始化的默认构造函数。
17 作用域为类的常量
下面是一种创建由所有对象共享的常量的方法:
class blackery
{
private:
const int Months=12;
double costs[Months];
....
};
实际上,这样不可行。这里将Months定义为无链接性的静态变量,但这是行不通的,因为声明类只是描述了对象的形式,并没有创建对象,也没有用于存储值得空间。
有两种方法可以替代这个不好的方法。
第一种是在类中声明一个枚举。
在类声明中声明的枚举作用域为整个类,因此可以用枚举为整形常量提供作用域为整个类的符号名称。
class blacery
{
private:
enum {Months=12};
double costs[Months];
...
};
这种方式声明枚举并不会创建类数据成员,也就是说所有对象中都不包含枚举。Months只是一个符号名称,在作用域为整个类的代码中遇到他时,编译器将用30来替换他。由于这里使用枚举只是为了创建符号常量,并不打算创建枚举类型的变量,因此不需要提供枚举名。
第二种方法是使用关键词static。
class Blackery
{
private:
static const int Months=12;
double costs[Months];
...
};
这将创建一个名为Months的常量,该常量将与其他静态变量存储在一起,而不是存储在对象中。因此只有一个Months常量,被所有blackery对象共享。见上面条款7关于static的讨论!
注意:这里Months是const常量,因此可以初始化,并且可以在类声明中使用。
但是一般的类中的静态成员(非const),是不能在类声明中初始化的。这是因为类声明并不分配内存。例如:
class string
{
private:
char * str;
static int num_strings;
public:
string();
...其他成员函数
};
int Sting::num_strings=0;// 可以这样初始化,注意这个位置
string::string()
{
}
...其他类成员函数的定义
int main()
{
...
}
18 关于友元
这里只针对友元函数。
创建友元函数的第一步是将其原型放在类声明中,并在原型声明加上关键字friend。(在定义中不加friend关键字!而且前面不加类名和限定符::)
- 虽然友元函数是在类声明中声明的,但它不是成员函数,因此不能使用成员运算符来调用。
- 虽然友元函数不是成员函数,但是他与成员函数的访问权限相同。
友元函数的出现很大一部分原因是为了重载运算符。
- 对于很多运算符来说,可以选择使用成员函数或非成员函数来实现运算符重载。一般来说,非成员函数应该是友元函数,这样才能访问类的私有数据。
- 最好的例子就是<<运算符的重载,使用了友元函数来重载这个运算符。
- 这些运算符需要两个参数,对于成员函数版本来说,一个操作符通过this指针隐式地传递,另一个操作符作为函数参数显式地传递;对于友元版本来说,两个操作数都作为参数来传递。
- 运算符的重载,选择那个版本来重载比较好呢。一般来说,差异不大,但是有一些情况下,是只能用友元函数来重载的,成员函数无法完成。(如<<)
19 类的类型转换
1、类的自动类型转换和强制类型转换
在C++中,接受一个参数的构造函数为将类型与该参数相同的值转换为类体统了蓝图。例如假设Stonewt包含如下构造函数(声明):
Stonewt(double lbs);
那么编写如下代码:
Stonewt mycat=19.6;
那么编译器将使用构造函数Stonewt(double lbs)来创建一个临时的Stonewt对象,并将19.6作为初始化值。然后将该临时对象赋值(copy赋值)给mycat。因此从表面上看,我们将19.6转换成了一个Stonwt类对象,而实际上,我们是利用的构造函数。这一过程称为隐式类型转换。
然而这种隐式类型转换有时是有害的,我们可以利用关键字explicit来关闭这种隐式类型转换,也就说,我们可以这样编写构造函数:
explicit Stonewt(double lbs);
这将关闭上面的隐式类型转换,但是,还是允许显式转换,即显式强制类型转换:
Stonewt mycat;
mycat=19.6;// 编译错误,因为我们使用了explicit关闭了隐式转换
mycat=Stonewt(19.6);// 编译通过,这是显式类型转换
如果在声明中没有使用关键字explicit,则Stonwt(double)还可用于下面的隐式转换:
- 将Stonewt对象初始化为double值时。
- 将double值赋给Stonewt对象时;
- 将double值传递给接受Stonewt参数的函数时;
- 返回值被声明为Stonewt的函数试图返回double值时。
- 在上述任意一种情况下,使用可转换为double类型的内置类型时。
2、转换函数
构造函数值用于从某种类型到类类型的转换。要进行相反的转换,必须使用特殊的C++运算符函数——转换函数。转换函数是用户定义的强制类型转换。
要把类类型转换为typeName类型,需要使用这种形式的转换函数:
operator typeName();同时注意下面几点:
- 转换函数必须是类方法;
- 转换函数不能指定返回类型;返回类型确定为typeName
- 转换函数不能有参数;
将类类型转换为double类型的函数的原型如下:
operator double();
与构造函数的自动类型转换一样,转换函数也有时候也会自动进行,如:
Stonewt poppins(9,2.8);
double p_wt=poppins; //隐式转换
double p_wt2=double(poppins); //显式转换
同理,在转换函数的声明的时候加上explicit可以关闭隐式转换!
20 基于对象和面向对象
1、基于对象。基于对象的两种情况要分清。可以根据成员变量有没有指针(构造函数有没有动态分配内存)分成两种。
- 第一种就是简单地,不需要自己定义析构函数、复制构造函数以及赋值运算符,默认的就表现得很好。
- 第二种是成员变量有指针,或者是说构造函数中使用了动态内存分配,这种情况下要自己定义析构函数、复制构造函数以及赋值运算符。
2、面向对象。继承、封装、多态
21 成员初始化列表和C++11的类内初始化
1、成员初始化列表
语法如下:
Classy::Classy(int n,int m):mem1(n),mem2(0),mem3(m)
{
...
}
注意:
- 成员初始化列表只用于构造函数
- 必须用这种方式来初始化非静态const数据成员。(因为const数据成员必须在定义时就初始化)
- 必须用这种方式来初始化引用数据成员。(因为引用数据成员必须在定义的时候初始化)
2、C++11的类内初始化
C++11新增的可以以更直观的方式进行初始化:
class Classy
{
int mem1=10;
const int mem2=20;
//....
};
这是类的声明,实际上,这并不是给成员初始化或赋值,更不是分配内存。而仅仅相当于提供了一个默认值。为的是在定义类对象的时候,提供一个默认的初始化值。上面就相当于:
Classy::Classy():mem1(10),mem2(20)
{
...
}
也就说,提供对默认构造函数的默认值。。*然而使用成员初始化列表的构造函数将覆盖响应的类内初始化。*也就是说,类内初始化起到的是默认构造函数的作用。
22 对多态的理解
多态就是,方法的行为应取决于调用该方法的对象或者是取决于指针(或引用)指向的对象类型。即同一个方法的行为随上下文而异。
对多态的说明在《深入理解C++对象模型》中有更加深入说明。
23 为何需要虚析构函数
如果析构函数不是虚的,则将只调用对应于指针类型的析构函数(见条款24!)。如果析构函数是虚的,将调用相应对象类型的析构函数。因此,如果指针指向的是一个派生类对象,将调用派生类的析构函数,然后自动调用基类的虚构函数。因此使用虚析构函数可以保证正确的析构函数序列被调用。对于一些派生类的析构函数不执行任何操作的情况来说,虚析构函数不是很有必要,然而,如果派生类包含一个执行某些操作的析构函数,则基类必须得有一个析构函数,即使该析构函数不执行任何操作。
24 多态公有继承
cpp plus p493中又这么一段话:
“如果没有使用关键字virtual,程序将根据引用类型或指针类型选择方法;如果使用了virtual,程序将根据引用或指针指向的对象的类型来选择方法。”
这第一句话有问题。假如说基类和和派生类都有一个同名函数,按理来说,是应该声明为virtual的,因为派生类要使用针对派生类的这种方法,但是第一句话的意思是不声明为virtual,这将根据指针和引用的类型来确定使用基类还是派生类的方法。这就很怪,既然派生类要定义一个和基类没有关系的函数,为啥不用一个全新的名字,而非要用和基类同名的函数。这违反了effective C++中的一个条款。
effictive C++中有一个条款,说的就是如果派生类中要重定义基类的某一个方法,就要将该方法声明为virtual。派生类重新增加的方法,不要和基类的某个方法重名。
25 动态联编和静态联编
将源代码中的函数调用转换解释为执行特定的函数代码块被称为函数名联编,又称为联编。
在编译过程中进行联编被称为静态联编。编译器对非虚方法使用静态联编。
在程序运行过程时选择正确的虚方法的,被称为动态联编。编译器对虚方法使用动态联编。
26 继承和动态内存分配
如果基类使用动态内存分配,将如何影响派生类的实现呢。
如果基类中使用了动态内存分配,说明成员中可能有指针类型的变量。并且基类一定会包含自定义的特殊的析构函数、复制构造函数、赋值运算符。为了保险起见(考虑到派生类的析构函数能要做些什么东西),将析构函数声明为virtual。(为简单起见,只要一个类是作为基类,就可以将其析构函数声明为虚的)(见条款24)
1、第一种情况:派生类不使用new
假设从上面描述的基类中(使用了动态内存分配)派生出一个派生类,这个派生类不使用动态内存分配,也不包含一些不常用的、需要特殊处理的设计特性。
这种情况下,不需要显式定义析构函数、复制构造函数和赋值运算符。
首先来看析构函数。如果没有定义析构函数,编译器将定义一个不执行任何操作的默认构造函数。实际上,派生类的默认构函数总是要进行一些操作:执行自身的代码后调用基类析构函数。因为我们假设派生类成员不需执行任何特殊操作,所以默认析构函数是合适的。
接着看复制构造函数和赋值运算符。派生类只需考虑自身自加的数据成员。因为派生类会自动使用基类的复制构造函数和赋值运算符来对基类组件进行复制或赋值。而派生类中新增的成员没有指针类型,所以默认的复制构造函数和赋值运算符也是合适的。
2、第二种情况:派生类使用new
也就是说,派生类中新加了一个指针变量(或者说在构造函数中使用了动态内存分配),在这种情况下,必须显式为派生类定义析构函数、复制构造函数和赋值运算符。
派生类析构函数自动调用基类的析构函数,故其自身的职责是对派生类构造函数执行的工作进行清理。
派生类的复制构造函数只能访问派生类的数据,因此他必须调用基类的赋值构造函数来处理继承下来的数据。具体的实现见相关书籍。
赋值运算符也是同理,派生类的显式赋值运算符必须负责处理所有继承的基类对象的赋值,可以通过显式调用基类赋值运算符来完成这项工作。具体的代码实现也非常重要见相关书籍。
27 关于has - a关系 (包含和私有继承)
通常,尽量使用包含来建立has-a关系,不用私有继承。
28 含虚基类的多重继承
不使用虚基类的多重继承不会引入新规则,但是当在多重继承中引入虚基类后,规则有所改变。
假设有如下的继承关系:
并且是通过虚继承来实现的。
class Singer:virtual public Worker {...};
class Waiter: virtual public Worker{...};
class SingerWaiter:public Signer,public Worker{...};//注意这里没有virtual
1、新的构造函数规则
C++在基类是虚的时候,禁止信息通过中间类自动传递给基类。
所以如果不希望默认构造函数来构造虚基类对象,则需要显式地调用所需的基类构造函数。因此 构造函数应该是这样:
SingerWaiter(const Worker &wk,int p=0;int v=Singer::other)
:Worker(wk),Waiter(wk,p),Singer(wk,v){ }
上述代码将显式地调用基类的构造函数Worker(const Worker &)。请注意,这种用法是合法的,对于虚基类,必须这样做;但对于不含虚基类的派生,则是非法的。
小结:如果有间接虚基类,则除非只需使用该虚基类的默认构造函数,否则必须显式地调用该虚基类的某个构造函数。
2、使用哪个方法?
多重继承可能导致函数调用的二义性。例如Singerwaiter可能从Worker类和Singer类哪里继承了两个完全不一样的Draw()方法。
这时候在,可以使用作用域解析运算符来澄清编程者的意图。然而更好的方法是在SingerWaiter类中重新定义Draw()。
29 类模板
1、带有非类型参数的类模板
template <class T,int n>
class ArrayTP
{
private:
T ar[n];
public:
...
};
对于:
template <class T,int n>
这个声明,关键字class(也可以是typename)指出T为类型参数,也就是说,下面的类声明中的T是一个类型,而n是一个非类型参数。模板代码不能修改非类型参数的值,也不能使用参数的地址,所以诸如n++和&n等都是非法的。
假如有以下声明:
ArrayTP<double,12> eggweights;
这将导致编译器定义名为ArrayTP<double,12>的类。并创建一个类型为ArrayTP<double,12>的eggweight对象。注意这里的类型参数和非类型参数。
2、模板的多功能性
可以将常规类的技术用于模板类。模板类可用作基类,也可用作组件类,还可以用于其他模板的类型参数。
3、使用多个类型参数的模板
例子:
template<class T1,class T2>
class Pair
{
private:
T1 a;
T2 b;
...
}
4、默认类型参数模板
例子:
template<class T1,class T2=int>
class Topo
{
...
};
Topo<double,double> m1;
Topo<double> m2;// T1是double,T2是int
5、隐式实例化和显式实例化
实例化就是编译器根据模板的定义和所提供的的类型参数来生成相应的类声明和类定义。
即“模板声明定义---->类声明和定义”
一般来讲我们都是使用隐式实例化。也就是说,我们声明一个或多个对象,指出所需的类型,而编译器使用通用模板提供的处方生成具体的类定义。如:
ArrayTp<int,100> stuff;
隐式实例化有一个细节,就是,在需要对象之前,编译器不会生成类的隐式实例化。如:
ArrayTP<double,30> *pt; //一个指针,没有对象生成
pt=new ArrayTP<double,30>; // 一个新的对象生成,这一步才会成成类定义,并根据类定义创建一个对象
当时用关键字template并指出所需类型来声明类的时候,编译器将生成类声明的显示实例化。在这种情况下,虽然没有创建或提及类对象,编译器也将生成类声明和类方法。如:
template class ArrayTP<string ,100 >; //生成ArrayTP<string ,100> 类
6、模板的特化(显示具体化)
可以提供一个特化的模板,这将采用为具体类型定义的模板,而不是为泛型定义的模板。当特化模板和通用模板都与实例化请求匹配时,编译器将使用具体化版本。
特化版本的模板定义如下:
template<> class Classname<specialized-type-name>{...};
或者写成:
template<>
class Classname<specialized-type-name>{...};
7、模板的偏特化
偏特化的一个例子是可以给类型参数之一指定具体的类型:
//泛化版本
template<class T1,class T2>
class Pair
{...};
//特化版本
template<>
class Pair<int,int>
{...};
//偏特化版本
template<class T1>
class Pair<T1,int>
{...};
偏特化的另一个例子是为指针提供特殊版本:
//泛化版本
template<class T>
class Feeb
{...};
//偏特化版本
template<class T*>
class Feed
{...};
如果有多个模板可以选择,编译器将使用特化程度最高的那个模板来进行实例化!!!
8、成员模板
模板可以用作结构、类或模板类的成员。如下面:
template <typename T>
class beta
{
private:
template<typename V>
class hold
{
private:
V val;
public:
hold(V v=0):val(v){}
V value() const {return val;}
};
hold<T>q;
hold<int>n;
public:
beta(T t,int i):q(t),n(i) {}
template<typename U>
U blab(U u,T t){ return (n.value()+q.value())*u/t;}
};
9、模板类和友元
模板类声明也可以有友元,模板的友元分三类:
- 非模板友元
- 约束模板友元,即友元的类型取决于类被实例化时的类型
- 非约束模板友元,即友元的所有具体化都是类的没一个具体化的友元
9-1、模板类的非模板友元函数
友元在模板中的声明的格式如下:
template <class T>
class HasFriend<int>
{
...
friend void report(HasFriend<T> &);
//下面是错误实例,因为没有HasFriend这样的对象
//friend void report(HasFriend &);
};
当定义友元时要注意,因为友元函数不是一个模板函数,这意味着必须为要是用的友元定义显式具体化(特化),如:
void report(HasFriend<short> &) {...};
void report(HasFriend<short> &) {...};
...
9-2、模板类的约束模板友元函数
9-3、模板类的非约束模板友元函数
30 友元类
前面提到过友元函数,这里针对友元类
在一个类中声明友元类后,就可以在友元类中访问该类的私有成员。
使用友元类的时候,友元类和使用友元类的类,两者的声明顺序和定义顺序有一定的讲究,会牵涉到前向声明的使用。
31 RTTI
RTTI是运行阶段类型识别的简称
C++有三个支持RTTI的元素,分别是dynamic_cast运算符、typeid以及type_info。只能将RTTI用于包含虚函数的类层次结构,原因在于只有对于这样的类层次结构,才应该将派生类对象地址赋给基类指针。(RTTI只适用于包含虚函数的类)
1、dynamic_cast
如果可能的话,dynamic_cast运算符将使用一个指向基类的指针来生成一个指向派生类的指针,否则,该运算符返回0—空指针。他回答的是“是否可以将对象的地址赋给特定类型的指针”这样的问题。
有时我们需要将一个多态指针转换为其实际指向对象的类型!
该转换符用于将一个指向派生类的基类指针或引用转换为派生类的指针或引用(注意dynamic_cast转换符只能用于含有虚函数的类)。
其表达式为dynamic_cast<类型>(表达式),其中的类型是指把表达式要转换成的目标类型,比如含有虚函数的基类B和从基类B派生出的派生类D,则B *pb; D pd, md; pb=&md; pd=dynamic<D>(pb); 最后一条语句表示把指向派生类D的基类指针pb转换为派生类D的指针,然后将这个指针赋给派生类D的指针pd,有人可能会觉得这样做没有意义,既然指针pd要指向派生类为什么不pd=&md;这样做更直接呢?
因为有些时候我们需要强制转换,比如如果指向派生类的基类指针B想访问派生类D中的除虚函数之外的成员时就需要把该指针转换为指向派生类D的指针,以达到访问派生类D中特有的成员的目的,比如派生类D中含有特有的成员函数g(),这时可以这样来访问该成员dynamic_cast<D*>(pb)->g();因为dynamic_cast转换后的结果是一个指向派生类的指针,所以可以这样访问派生类中特有的成员。但是该语句不影响原来的指针的类型,即基类指针pb仍然是指向基类B的。
dynamic_cast转换符只能用于指针或者引用。dynamic_cast转换符只能用于含有虚函数的类。dynamic_cast转换操作符在执行类型转换时首先将检查能否成功转换,如果能成功转换则转换之,如果转换失败,如果是指针则反回一个0值,如果是转换的是引用,则抛出一个bad_cast异常,所以在使用dynamic_cast转换之间应使用if语句对其转换成功与否进行测试,比如pd=dynamic_cast<D*>(pb); if(pd){…}else{…},或者这样测试if(dynamic_cast<D*>(pb)){…}else{…}。
2、typeid运算符
我们使用typeid比较两条表达式的类型是否相同,或者一条表达式的类型是否与指定类型相同。需要注意的是这个运算符是在运行时才来检查,即是比较的动态类型。
Derived *dp=new Derived;
Base *bp =dp;//两个指针都指向Derived对象
if(typeid(*bp)==typeid(*dp))//ture
{
...
}
if(typeid(*bp)==typeid(Derived))//true
{
...
}
typeid运算符返回一个对typeid对象的引用,其中,type_id是在头文件typeinfo中定义的一个类。type_info重载了==和!=运算符。
32 const_cast
const_cast运算符用于执行只有一种用途的类型转换,即改变值为const或volatile,其语法与dynamic_cast运算符相同:
const_cast<type_name>(expression)
High bar;
const High *pbar=&bar;
High *pb=const_cast<High*>(pbar);
- 这里,type_name和expression的类型必须相同。
- 本身,pbar是指向常量的指针,const_cast可以将const标签拿走,使其pb成为一个可用于修改bar值的指针。
- 这里并不改变pbar的属性,而是将const_cast后的指针赋予给pb。
33 智能指针
首先要明确的是,智能指针是一个模板类,来看智能指针的初始化:
auto_ptr<double> pd(new double);// 来替代 double *pd=new double;
auto_ptr<string> ps(new string);// 来替代 string *ps=new string;
auto_ptr<string> ps(new string("l love you"));// 来替代 string *ps=new string("l love you");
即智能指针是用地址来初始化的!
另外需要注意这里的new的对象的初始化的使用。
回忆,类对象的初始化:
Stone st1(…);// 直接初始化
Stone st2=Stone(…); //先用构造函数建立一个临时对象,然后将这个对象赋值给st2
Stone *st3=new Stone; // 用默认构造函数建立一个对象,将其地址交给st3
Stone *st4=new Stone(…);// 用构造函数初始化一个对象,然后将其地址交给st4
34 右值引用 和 移动语义
1、右值引用
左值与右值的概念见条款14.
简单来讲,可以把右值看做常量或常量表达式。
左值引用与右值引用的最简单的例子:
int x=10;
int y=23;
int &r=x;//左值引用,因为x是一个左值
int && r1=13;//右值引用,因为13是右值
int && r2=x+y;//右值引用,因为表达式(x+y)是右值,如果单独的x或y就是左值
将右值关联到右值引用导致该右值被存储到特定的位置,且可以获取该右值引用的地址。(右值的一个特点是无法获取其地址,但是可以通过右值引用变相的访问其地址,例如可以写出,&r1,&r2)
2、移动构造函数解析
移动构造函数对应的是复制构造函数。
假设有一个Useless类,并且one、two、three、four是类对象。
Useless two=one;//匹配Useless::Useless(const Useless &)
Useless four(one+three);// 匹配 Useless::Useless(Useless &&)
上面一个匹配复制构造函数,一个匹配移动构造函数。对象one是左值。而表达式(one+three)是右值,与右值引用匹配。如果没有定义移动构造函数,编译器将用创建临时对象法来创建一个临时对象,再调用复制构造函数。
移动构造函数的编写也很重要,尤其是用了new的时候,见相关代码。
3、移动赋值运算符
与移动赋值运算符对应的是复制赋值运算符。
重点也是对于使用new的时候的移动赋值运算符的编写,以及其和复制赋值运算符在程序上的区别,两者的区别才是最重要的!!
35 特殊的成员函数
- 默认构造函数
- 复制构造函数
- 复制赋值运算符
- 移动构造函数
- 移动赋值运算符
- 析构函数
如果提供了赋值构造函数或复制赋值运算符,编译器将不会自动提供移动构造函数或移动赋值运算符;如果提供了移动构造函数或移动赋值运算符,编译器将不会自动提供复制构造函数或复制赋值运算符。
default:
如果提供了某余个版本的构造函数,编译器就不会生成默认构造函数。在这种情况下,可以使用default显式地声明这些方法的默认版本。
delete:
关键字delete可用于禁止编译器使用特定的方法。例如,要禁止复制对象,可禁用复制函数和复制赋值运算符。