从《
C++ Primer
第三版
》中更新我的
C++
知识
0.
我的前言:
§
书中关于
STL
和
IOStream
的内容,我基本上没写(这方面的详细介绍在《
C++
标准库读书笔记》中)。这主要是因为书中讲的不详细不透彻,还有在读这本书时,我学到的主要是关于语言本身而非标准库的知识。
§
虽说
VC2005
常常被我用来验证知识点,但是它本身并不是完美支持标准
C++
的!
§
若无特殊说明,“成员函数”指非静态同时不加任何限定修饰符的成员函数。
1.
C++
的关键字:
asm
|
auto
|
bool
|
break
|
case
|
catch
|
char
|
class
|
const
|
const_cast
|
continue
|
default
|
delete
|
do
|
double
|
dynamic_cast
|
else
|
enum
|
explicit
|
export
|
extern
|
false
|
float
|
for
|
friend
|
goto
|
if
|
inline
|
int
|
long
|
mutable
|
namespace
|
new
|
operator
|
private
|
protected
|
public
|
register
|
reinterpret_cast
|
return
|
short
|
signed
|
sizeof
|
static
|
static_cast
|
struct
|
switch
|
template
|
this
|
throw
|
true
|
try
|
typedef
|
typeid
|
typename
|
union
|
unsigned
|
using
|
virtual
|
void
|
volatile
|
wchar_t
|
while
|
|
|
以下为
C++
保留的可选运算符(也算作关键字):
and
|
and_eq
|
bitand
|
bitor
|
compl
|
not
|
not_eq
|
or
|
or_eq
|
xor
|
xor_eq
|
|
2. 对于内置数据类型长度C++可以且只可以保证:
sizeof(char) == 1。
3.
整数类型(integer type)包括char、short、 int、 long的有符号和无符号类型。
整值类型(integral type)包括整数类型(integer type)以及bool型、
wchar_t型。
§
整值文字常量可以写成十进制,八进制和十六进制三种形式:20,024,0x14分别为20的十进制,八进制,十六进制形式。(
千万别以为024前面的0是可有可无!)
4. 转义字符的表示:/OOO 或者 /xHH,其中OOO表示最多为三位的八进制数据,HH表示最多为两位的十六进制数据(
‘/’后面的‘x’不可省略)。
5. 字符串常量后面带了一个‘/0’字符作为终止,所以:
char c_str[] = “Hello”;
strlen(c_str) == 5;
sizeof(c_str) == 6;
const char* pstr = “Hello”;
strlen(pstr) == 5;
sizeof(pstr) == 4; //对于32bit 系统,指针用于存储32bit地址,故需要4byte!
6. 对于每个变量,都有两个值与其相关联:
(1) 它的数据值,亦被称为对象的
右值,意思是被读取的值,文字常量和变量都可以做右值。
(2) 它的地址值,亦被称为变量的
左值,意思是位置值。
文字常量不可以用做左值,所以我们可以写出如下语句:
// ptr是一个已经被定义了的指针变量
if (NULL == ptr)
//这样可以防止“==”被误写成“=” ,如果误写编译无法通过
{ /* ... */ }
7. 不同数据类型的指针在指针的表示和所持有的值(地址)上是相同的;当然,函数指针和数据指针不同,函数指针指向程序的代码段。
8. 注意区别
“常量的指针”与
“指针常量”:
const int x = 1024;
const int *p = &x;
//p为常量的指针,p指向的变量的值不可修改,但可以重新赋值p
//
准确的说是,p
指向的变量的值不可通过p
来修改
int* const q = &x;
//q为指针常量,q不可被改变,却可以修改q指向的值
9.
const
引用可以用不同类型的对象初始化(只要能从一种类型转换到另一种类型即可),也可以是不可寻址的值;而对于非const引用这样是不合法的。
分析:引用在内部存放的是一个对象的地址,它是该对象的别名。对于不可寻址的值以及不同类型的对象,编译器为了实现引用,必须生成一个临时对象,引用实际上指向该临时对象,用户不能访问它。若此时我们修改引用的值,实际上修改的是临时变量的值,不会修改用来初始化引用的那个对象的值,这会让人莫名其妙!而const引用不可被修改,所以它不会暴露这个问题。
10.const int ival = 1024;如何定义一个引用并初始化为&val?
const int *const &pi_ref = &ival;
分析:
&val
类型为
const int*
;同时表达式
&val
是一个不可寻值的值(在内部用临时对象表示)则要求引用为
const
引用!
11
.
指针与引用的区别:
(1)
引用必须总指向一个对象,而指针可以为
NULL
,表示该指针不指向任何对象;
(2)
如果用一个引用给另一个引用赋值,那么改变的是被引用的对象,而不是引用本身,换句话说,引用一旦被初始化指向某个对象,则该引用不会再去指向其他对象;而指针不同,指针之间赋值改变的是指针本身。
12
.
标准
C++
新增了
bool
类型
,布尔常量
true
和
false
可以自动转换成
0
和
1
,同样,如有必要,算术值和指针值也能隐式地被转换成布尔类型的值:
0
或空指针被转换成
false
,所有其他的值都被转换成
true
。
以我认为,虽然存在这种转换,但是为了程序可读性最好不要利用这种转换
,比如:
//ptr
是已被定义过的指针变量
if (NULL == ptr)
{ /* ... */ }
比起:
if (ptr)
{ /* ... */ }
可读性要好!所以一般的规律是(以
if
的条件语句中与零比较为例):
bool bFlag;
if (bFlag)或者
if (!bFlag)
int iValue;
if (0 == iValue)或者
if(0 != iValue)
const float EPSILON = 1e-6;
float fValue;
if (abs(fValue) <= EPSILON)或者
if (abs(fValue) > EPSILON)
比较两个float型是否相等,可以:
float fValue1, fValue2;
if (abs(fValue1 - fValue2) <= EPSILON)或者
if (abs(fValue1- fValue2) > EPSILON)
// const int NULL = 0;
char *p = NULL;
if (NULL == p)或者
if (NULL != p)
13.注意区别
“指针数组”和
“数组指针”:
int *p[10]; //p是一个数组,含有10个int* 类型元素
int (*p)[10]; //p为一个指针,它指向一个含有10个int型元素的数组
14.
不要把typedef当作宏扩展:
typedef char *cstring;
extern const cstring cstr;
问:cstr类型到底是什么?
正确答案:为 char * const型,而不是const char *型。
分析:typedef是定义了一个已存在的类型的别名,它没有产生新类型;在解析cstr类型中关键之处就在于const修饰什么,可以把cstring看成一个基本类型,比如是int,则const int cstr; 这下就下就显而易见const修饰的是cstr而不是int!
15.
操作符(operator)
§
操作符优先级:(
如果你记忆力超人,请记住如下表格,否则只需记住较高和较低的那几个操作符即可,而用小括号来明确表达式计算顺序 。
操作符
|
功能
|
相对优先级
|
结合性
(L/R)
|
::
|
全局域
类域
名字空间域
|
|
L
|
.
|
类对象成员选择
|
1
|
L
|
->
|
类指针成员选择
|
1
|
L
|
[]
|
下标
|
1
|
L
|
()
|
函数调用
类型构造
|
1
|
L
|
++
|
后置递增
|
1
|
R
|
--
|
后置递减
|
1
|
R
|
typeid
|
类型ID
运行时刻类型ID
|
1
|
R
|
const_cast
static_cast
dynamic_cast
reinterpret_cast
|
类型转换
|
1
|
R
|
sizeof
|
类型的大小
对象的大小
|
2
|
R
|
++
|
前置递增
|
2
|
R
|
--
|
前置递减
|
2
|
R
|
~
|
按位取反
|
2
|
R
|
!
|
逻辑非
|
2
|
R
|
-
|
一元减(负号)
|
2
|
R
|
+
|
一元加(正号)
|
2
|
R
|
*
|
解引用
|
2
|
R
|
&
|
取地址
|
2
|
R
|
()
|
旧式类型转换
|
2
|
R
|
new
|
分配对象
分配/初始化对象
分配/替换对象
|
2
|
R
|
new [ ]
|
分配数组
|
2
|
R
|
delete
|
释放对象
|
2
|
R
|
delete []
|
释放数组
|
2
|
R
|
->*
|
指向成员选择
|
3
|
L
|
.*
|
指向成员选择
|
3
|
L
|
*
|
乘
|
4
|
L
|
/
|
除
|
4
|
L
|
%
|
取模(求余)
|
4
|
L
|
+
|
加
|
5
|
L
|
-
|
减
|
5
|
L
|
<<
|
按位左移
|
6
|
L
|
>>
|
按位右移
|
6
|
L
|
<
|
小于
|
7
|
L
|
<=
|
小于等于
|
7
|
L
|
>
|
大于
|
7
|
L
|
>=
|
大于等于
|
7
|
L
|
==
|
等于
|
8
|
L
|
!=
|
不等于
|
8
|
L
|
&
|
按位与
|
9
|
L
|
^
|
按位异或
|
10
|
L
|
|
|
按位或
|
11
|
L
|
&&
|
逻辑与
|
12
|
L
|
||
|
逻辑或
|
13
|
L
|
?:
|
条件表达式
|
14
|
R
|
=, *=, /=, %=, +=
-=, <<=, >>=, &=
|=, ^=
|
赋值
|
15
|
R
|
throw
|
抛出异常
|
16
|
R
|
,
|
逗号
|
17
|
L
|
§
%
运算符计算两个数相除的余数,第一个数被第二个数除。该操作符只能被应用在整值类型的操作数上。当两个操作数都为正数时,结果为正;
若有一个或两个操作数为负,余数的符号无定义,取决于机器。
§
二元关系操作符
的左右操作数的计算顺序在标准
C
和
C++
中都是未定义的,因此计算过程必须是与顺序无关的。
if ( ia[index++] < ia[index]) //C++语言本身未定义计算顺序
{ /* ... */ }
§
sizeof
操作符的三种形式:
sizeof (type_name);
sizeof (expression);
sizeof expression;
(顺便说一句,
sizeof class_name 的形式也可以,但是该书中未提,但是却用过)
§
sizeof
操作符
在编译时刻计算,可以被看作常量表达式:
int array[ sizeof (int) ]; //OK, (在32bit系统中)定义一个包含4个int型元素的数组
§ 在逻辑与表达式
expr1 && expr2中,如果expr1的计算结果为false;或者是在逻辑或表达式expr1 || expr2中,expr1的计算结果为true;则保证不会计算expr2。
(
提醒一下,&& 的优先级高于 ||, 别以为它们是同级别的,详见上表)
§ new操作符的不常见用法(
对于之前的我而言):
new
表达式分配多维数组,只有第一维可以用运行时刻计算的表达式来指定,其他维必须是编译时刻已知的常量,分配的数组空间都是用delete [] 来释放;
定位
new
表达式:
new (place_address) type_specifier; 它允许我们将对象创建在已经被分配好的内存中。
16.类型转换:
(1)
隐式类型return语句的表达式类型不同;转换,它发生在:
算术转换(指导原则:若有必要,类型总是被提升为较宽的类型;所有含有小于整型的数值类型的算术表达式,在计算之前,其类型都会被转换成整型),
不同类型的对象间赋值,
形参与实参类型不同,
函数返回值与
(a)精确匹配:
左值到右值的转换(从一个左值表达式中抽取值),
数组到指针的转换(数组名为数组首元素的地址),
函数到指针的转换(函数名可以被转换为函数指针),
限定修饰符转换(将限定修饰符const或volatile
加到指针指向的类型上);
前三种转换又被合称为
左值转换。
(
关于“限定修饰符转换”在《
C++
程序设计语言》(以后简称《语言》)中被放在“标准转换”中,从
VC2005
的测试情况,以及个人理解来看,我觉得极为不妥,不知是书上表述错误,还是真是如此?另外“
T
到
const T
的转换”在《语言》中也被列入“精确匹配”之中,而本书认为是“没有转换”,我倾向于后者,
VC2005
中同样验证了我的想法!
另外说一句,我觉得“限定修饰符转换”应该同样适用于引用。)
(b)类型转换:
§
提升(promotion):
·
char, unsigned char,short提升为int型,若int型比short型长,则unsigned short提升为int型,否则提升为unsigned int;
·
float型提升为double;
·
wchar_t或枚举类型被提升为下列第一个能表示wchar_t或所有枚举常量的类型:int、unsigned int、long、unsigned long;
· 布尔类型被提升为int型;
· 如果int可以表示一个位域的所有值,就将这个位域转换到int,如果unsigned int可以表示这个位域的所有值,就将这个位域转换到unsigned int,否则不发生提升。
(
在《语言》中这条被加入“提升”的行列
)
§
标准转换:
· 整值转换(integral conversions):任何integer type或枚举类型向其他integer type转换,以及bool型到integer type的转换;(
不包括“提升”中的转换)
·
浮点转换:任何浮点类型到其他浮点类型的转换;(
不包括“提升”中的转换)
·
浮点与整值之间的转换:从任何浮点型到
integer type
或从
integer type
到任何浮点型的转换;
·
指针转换:整数
0
到指针类型的转换和任何数据类型指针到
void*
型的转换;
(
注意,函数指针和指向成员的指针不可以转换到
void*
)
·
bool
转换:从任何整值类型、浮点类型、枚举类型、指针类型到bool型的转换。)
(
在《语言》中派生类的指针(或引用)到基类指针(或引用)的转换,以及
T*
(或者
T&
)到
const T*(
或者
const T&)
的转换都被列入“标准转换”中)
· 派生类的对象(指针或左值)转化为基类的对象(指针或引用);
对于从派生类类型到不同基类类型的不同标准转换进行等级划分时,对于从派生类类型到基类类型移动较少(距离较近)的转换,被认为好于移动较多(距离较远)的转换;到基类类型指针的转换好于到void*的转换。
§
用户定义的转换(在类类型和
转换函数指定的类型之间的转换):
·
转换函数:在
类的成员函数,形式如下:
operator type (); type可以为
除了数组和函数类型之外的类型, 若转换的目标与转换函数的类型不完全匹配,则在用户定义的转换
之后只允许标准转换序列);
·
构造函数作为转换函数:只带一个参数且未被声明为explicit的class的构造函数都定义了一组隐式转换,如果需要,可以在构造函数执行用户定义转换
之前应用标准转换序列。
§ 省略号匹配(
... ellipsis):
这种类型的转换出现在函数参数为
“...”时,可以匹配任意个任意类型参数。
(2)
显示转换:通过它程序员关闭了C++语言的类型检查设施。
§ 新的强制类型转换操作符格式为:
cast_name<new_type> (expression);
其中cast_name可以为以下:
·
static_cast编译器隐式执行的任何类型转换都可以由它显式完成;同时它可以完成
一些行为不佳的转换:void*型到任何数据指针型,算术值转换为枚举值,一个基类转
换成其派生类(或者这种类的指针或引用);
·
const_cast
转换掉表达式的常量性(以及
volatile
对象的
volatile
型);
·
dynamic_cast支持在运行时刻识别由指针或引用指向的类对象向后转换;
(
关于
dynamic_cast
的详细讨论参见第
59
条)
·
reinterpret_cast 通常对操作数的位模式执行一个比较低层次的重新解释;
§ 旧式强制类型转换:
type (expr); //类似于类的构造函数
(type) expr; //C语言的强制转换符号
17.在
if (condition)
,
switch (expr)
, while (conditon), for (init; condition; expr)
这些语句的()中定义的对象,只在与对应的
if, switch, while, for
相关语句或语句块中可见
。且
for
中
init
语句只可以出现一个声明语句,因此所有对象必须是相同类型的。
18.空悬else二义性解决规则:
else
子句与“最后出现的未被匹配的
if
子句”相匹配。
也就是说,从最后一个else开始向前寻找if匹配。
19.switch —
case语句:
§ 关键字case后面的值必须是整值型的常量表达式;
§ 不要忘记在每个case语句块结束后加上 “
break;” 语句,否则程序会一直执行下去而不管下一个case;
§
把一条声明语句放在
case
或
default
相关联的语句是非法的,除非它被放在一个
语句块
中;
§
default
语句
的摆放次序不会影响程序选择,当所有的
case
标签都与
switch
表达式不匹配时,才执行可选的
default
语句。
20.
goto 语句:
§ 语法:
goto label; 其中,标号(label)语句只能用作goto的目标,必须由冒号结束,且
标号语句不能紧接在结束“
}
”的前面,处理这种情况可以:
goto_label: ; //
空语句
}
§ goto语句不能向前跳过没有被语句块包围的声明语句。
21.
函数的参数:
§ 当以数组作为函数参数时,数组被传递为指针,
数组的长度不是参数类型的一部分,为了告诉函数数组的长度(元素的数目),可以采用如下两种方法之一:
提供一个含有数组长度的额外参数;当参数是一个数组类型的引用时,数组长度成为参数和实参类型的一部分。
§
函数的参数类型不能是函数类型,函数类型的参数将自动被转换成该函数类型的指针。
§ 对于类对象,按指针或引用传递参数比按值传递要高效。
§
函数的缺省实参
:
· 函数的缺省实参只能用来替换函数调用缺少的尾部实参;
·一个参数只能在一个文件中被指定一次缺省实参;
·缺省实参可以为任意表达式,而不一定非要是常量表达式;
·函数后继的声明中可以指定其他的参数的缺省实参。
22.
函数的返回值:
§ 函数类型和数组类型不可以作为函数的返回值类型。
§ 除了
类的构造函数、析构函数、类型转换函数没有返回值之外,其它函数必须指定一个返回值,
C++不支持“C中的没有返回值意味着返回int型”的规则。
§ C++语言不能有效地返回一个类对象,这被视为
C++语言的重大缺陷:
不要返回在栈上建立的对象的指针或引用,因为在函数退出时,这块内存将不再有效。
23.inline函数:
inline指示队编译器来说只是一个建议,编译器可以选择忽略该建议;建议把inline函数的定义放到头文件中。
§
类的
inline
成员函数的定义若不在类体中,则必须定义在头文件中(换句话说,函数定义在类体中或者在类定义的头文件中函数,是类的inline成员函数)。
24.
链接指示符extren “C”,程序员用它来告诉编译器,该函数是用其他程序语言写的
§ 编译指示符有两种格式:单一语句和复合语句形式;对于符合语句形式,一对{ } 包含多个函数声明,此时花括号仅用作分隔符,在其他意义上该花括号被忽略,所以在花括号中声明的函数名对外是可见的;
§ 也可以使用extern ”C”来使C++函数为C程序可用;
§ 如果一个函数在同一文件中声明了多次,则链接指示符可以出现在每个声明中,也可以只出现在第一次声明中,在这种情况下,第二个以及后面的声明都接受第一个声明中链接指示符指定的链接规则;
当链接指示符应用在一个声明上时,所有被它声明的函数都将受到链接指示符的影响,
例如:
extern “C” void f1( void (*pfParm)(int) );
此处,f1()是个C函数,它有一个指向C函数指针的参数pfParm。
25.
指向函数的指针类型:当一个函数名没有被调用操作符(也就是一对小括号)修饰时,会被解释成指向该类型函数的指针;取地址操作符作用在函数名上也能产生指向该函数类型的指针;所以说以下语句全都正确:
typedef int (*func_ptr) (int);
int func(int iParam);
func_ptr p1 = func;
func_ptr p2 = &func;
(*p1)(1024);
p1(1024);
(*p2)(1024);
p2(1024);
26.
隐藏(遮蔽, hide)规则:在名字解析期间查找域的顺序由内向外,所以在外围域中的声明被嵌套域中的同名声明所隐藏。
如果隐藏发生在基类与派生类的函数上时,规则如下(前提是函数同名):
若参数列表不同,无论有无
virtual
关键字,基类函数被隐藏(此处与重载的区别在于它们属于不同的域);若参数列表相同,但基类函数没有
virtual
关键字,基类函数也被隐藏(与覆盖相比,它没有
virtual
关键字)。
27.
关键字
extern:既指定extern,又指定了一个显示初始值的全局对象声明将被视为该对对象的定义;
关键字extern也可以在函数声明中指定,唯一的影响是将该函数声明的隐式属性“在其他地方定义”变为显示,也就是说可有可无。
28.我们可以用
未命名的名字空间来声明一个局部于某个文件的实体。
例如:
//在test .cpp中
namespace {
void func() { /* …. */ }
}
此时func()只在文件test .cpp中可见;在标准C++之前,我们使用从C语言中继承来的关键字static来解决这一问题。
29.
using
声明(using declaration)同其他声明的行为一样,它有一个域,它引入的名字从该声明开始直到其所在的域结束都是可见的;using声明不仅可以引入名字空间生命的名字,还可以引入类域中的名字。
using
指示符
(
using directive)
: 它是域内的,对于该域来说,被指示的名字空间中的成员好像是在该名字空间被定义的地方之外声明一样;由它引起的二义性错误是在该名字被使用时才被检测到,而不是在遇到using指示符;
30.C++支持三种域(scope):
局部域,
名字空间域,
类域。
最外层的名字空间域称为
全局域。
31.
重载(overload)规则:如果两个函数名字相同,并且在相同的域中被声明(有可能是通过using声明或using指示符引入的),但是参数表不同,则他们就是重载函数。
32.
extern “C”
指示符只能指定重载函数集中的一个函数,这是因为C++的“类型安全连接”规则确保函数在底层必须有不同的函数名,但是它并不会应用到extern ”C”声明的函数上,对于两个同名extern “C”函数会被视为同一个函数。
33.
重载函数(overload function) 的解析过程:
步骤
1:确定重载函数集合,即确定
候选函数集合,确定函数调用中实参表的属性:
§ 一般的来讲,是
实参类型所在的名字空间中声明的同名的函数,以及那些
在调用点上可见的同名函数。
§ 如果一个函数调用的参数是一个类类型的对象、类类型的指针、类类型的引用或者是指向类成员的指针,则候选函数集是以下集合函数集合的并集:在调用点可见的同名函数、在“定义该类的名字空间”和“定义该类的基类的名字空间”中声明的同名函数、声明为该类或其基类的友元同名函数。
§ 考虑与函数调用同名的函数模板:如果对于该函数调用的实参,模板实参推演能够成功,则实例化一个函数模板,或者对于推演出来的函数实参存在一个模板特化,则模板特化就是一个候选函数。
§ 对于成员函数的重载:静态和非静态成员函数都可以包含在候选函数集中(
虽说在编译器内部,会为非静态成员函数的参数加上this指针,但是不要试图定义参数表相同的静态和非静态成员函数,这样会编译错误!换句话说,在语言层面上,成员函数前的static关键字的有无对重载函数不构成影响。);
const或
volatile成员函数同样的会被选入候选函数集,它们可以重载没有此限定修饰符的相同成员函数。
§ 操作符的重载解析:(
只有当操作数为类类型或枚举类型时才会考虑操作符的重载解析)候选函数集从以下中考虑:调用点可见的操作符集合、在“操作数类型被定义的名字空间”中声明的操作符集合、被声明为“操作数类类型的友元”的操作符集合、在左操作数的类中被声明的成员操作符集合、内置操作符集合。(如果操作数为类类型对应的基类相关的操作符也该加入吧!)
步骤
2:从重载函数集合中选择
可行函数集合:
可行函数的参数个数与调用的实参表中的参数数目相同,或者可行函数的参数个数多一些,且多出来的参数都要有相关的缺省实参,对于每个可行函数,调用中的实参与该函数的对应的参数类型之间必须存在
转换。(
关于转换具体参考第
16
条)
§ 静态成员函数和非静态成员函数都可以包含在可行函数集中,这与函数调用形式无关。(
即使成员函数通过classname::func()的形式调用,非静态成员函数仍然包含在可行函数集中,如果最后的最佳可行函数选择了该非静态成员函数会导致编译错误。)
§ static成员函数不会因为“被调对象或指针的限定修饰符(
const或volatile)”,而被排除在可行函数集之外;而non-static 成员函数则需和被调的对象或指针的限定修饰符相匹配。
步骤
3:从可行函数集合中选择与调用最匹配的函数(也叫最佳可行函数):
§
最佳可行函数是被适用于如下规则的函数:应用在实参上的转换不比调用其他可行函数所需的转换差;在某些实参上的转换要比其他可行函数对该参数的转换好。
§
函数参数转换优先级:
精确匹配 > 提升 > 标准转换 > 用户自定义转换 > 省略号匹配。
§
转换序列被用来把实参转换成可行函数的参数类型,通过比较转换序列来判断哪个转换更优。(它包含
标准转换序列和
用户自定义转换序列)
§ 如果两个
用户定义的转换序列只包含一个不同转换函数或不同的构造函数,则两个转换序列被认为一样好;如果用户定义转换序列包含多于1个的转换,如果某个参数类型要求不同的用户定义的转换,则不可能选择哪一个转换序列更好,除非用户定义的转换涉及到相同的转换参数。
§ 函数模板实例会选择
最特化的函数模板作为最佳可行函数。(
一个模板要比另一个更特化:两模板必须同名,且参数个数相同,对于不同类型的相应函数参数,一个参数必须能接受另一个模板中相应参数能接受的实参的超集):
例如:
template <typename T>
T sum(T*, int);
template <typename T>
T sum(T, int);
int ia[1024];
sum(ia, 1024); //
此时调用T sum(T*, int) ==> T为int
§ 当定义了一个普通函数,它的返回类型和参数表与另一个从模板实例化的函数的相同时,则普通函数将被给予更高的优先级。(定义这种普通函数的作用:当调用从模板实例化的函数时,只有有限的类型转换可以应用在模板实参推演过程,而普通函数可以考虑所有的类型转换来转换实参。)
34.
模板template)----- [此处将函数模板与类模板进行了对比](
(1)
模板参数可以是一个
模板类型参数,它代表一种类型;也可以是一个
模板非类型参数,它代表一个常量表达式(即,它必须能在编译时刻计算出结果)。
§ 关键字
typename或 class以及其后的标识符构成模板的类型参数,至于引入关键字typename的原因在于:
除了用typename修饰之外,template内的任何标识符号都被视为一个值而非一个类别。
§ 模板实参的类型与非类型模板参数的类型之间允许的转换:
左值转换、限定修饰转换、提升、整值转换。(以上转换的具体内容参见第
16条)
(2)
模板的实例化(template instantiation)
§ 函数模板在它
被调用或
取其地址时被实例化, 并且函数模板的
实例化点总是在名字空间域中,并且跟在引用该实例的函数后。
§ 类模板只有在代码中使用了它的一个实例的名字,并且
上下文环境要求必须存在类的定义时,这个类模板才被实例化。所以,声明一个类模板实例的指针和引用不会引起类模板被实例化(只有当检查这个指针或引用所指向的对象时才被实例化,比如:解引用或是访问成员函数),而定义一个类对象时则会。
§ 类模板的
实例化点总是在名字空间域中,而且他总是在“引用类模板实例的声明或定义”
之前;类模板的成员函数和静态数据成员的实例化点也总是在“引用类模板成员实例的声明或定义”
之后。
§ 类模板被实例化时,类模板的成员函数并不自动被实例化,只有当一个成员函数被程序用到(被调用或取地址)时,它才被实例化。
§ 类模板被实例化时,它的嵌套类不会被自动实例化,只有当上下文环境确实需要嵌套类的完整类类型时,嵌套类才被实例化。
(3)
函数模板实参推演(function template argument deduction)是当函数模板被调用时,对函数实参类型的检查决定了模板参数的类型和值的过程。
§ 此过程中,函数实参不必和相应的模版函数参数类型严格匹配,允许发生
精确匹配和
基类转换(该基类根据一个类模板实例化而来)。
§ 如果在多个函数参数中找到同一个模板参数,则
从每个相应的函数实参推演出的模板实参必须相同。
(4)
显式函数模板实参通过显示指定模板实参来改变模板实参推演机制。
§ 我们只需要列出不能被隐式推演的模板实参,只能省略尾部的实参。
§
当函数的返回值类型为单独的模板实参(不与函数参数重复)时,就需要显示模板实参来指定返回值类型。
(5)
模板编译模式
:
§
在
包含编译模式
下,我们在每个模板被实例化的文件中包含模板的定义,并且往往把定义放在头文件中。
§
在
分离编译模式
下,我们通过在模板定义中的关键字
template
之前加上关键字
export,
来声明一个可导出的模板(关键字
export
不需要出现在头文件中)。
对于类模板来说,可以使整个类为可导出的,此时只需要在类的定义中
template关键字之前加上export;也可以只把某个成员函数声明为可导出的,这样需要在该成员函数的定义中,在template关键字之前加上export。
(6)
模板显式实例化声明用来帮助程序员控制实例化发生的时间。
§
语法:在关键字template后面是模板实例化的声明,其中显式地指定了模板实参。
template <typename Type>
Type sum (Type op1, int op2) { /* … */ }
//
模板函数显式实例化
template int* sum<int*>( int* , int );
template <class T>
class A
{ /* ... */ }
//
类模板显式实例化
template class A<int>;
§ 对于给定的模板实例,显式实例化声明在一个程序中只能出现一次。
§ 在显式实例化声明所在的文件中,模板的定义必须给出,如果该定义不可见,则该显式实例化声明是错误的;显式实例化类模板时,它的所有成员也被显式实例化,而且针对同一组模板实参类型。
(7)
模板显式特化
§
语法:模板显式特化定义中,先是 template <> ,后面是模板特化的定义:
//
函数模板的显示特化
typedef const char* PCC;
template<> PCC max< PCC >(PCC s1, PCC s2)
{ return (strcmp(s1, s2) > 0 ? s1 : s2) }
//
成员函数特化
template<> double Queue<double>::min()
{ /* ... */ }
//
整个类特化
//
在头文件中:
template<> class Queue<double>
{
public:
Queue<double>();
~Queue<double>();
//...
}
//
在
CPP
文件中
Queue<double>::Queue()
{ /* ... */ }
//....
其它定义
§ 如果模板实参可从函数参数中(注意:不包括返回值)推演出来,则模板实参的显示特化可以从显示特化声明中省略。
§ 即使函数模板显示特化所指定的函数模板只有声明而没有定义,我们仍然可以声明函数模板显示特化。(分析:特化本身就包含了函数的定义,这一点和显示实例化不同。)
§ 一个程序不能对相同模板实参集的同一模板同时有一个显示特化和一个实例。
§ 即使定义了一个模板特化,我们也必须定义与这个特化相关的所有成员。
类模板的通用成员定义不会被用来创建显示特化的成员的定义,这是因为类模板特化可能拥有与通用模板完全不同的成员集合。
§ 部分特化:为一个特定的模板实参或一组模板实参特化模板。
除了语法上,在前导的
template<>的<>中列出模板实参仍然未知的那些参数之外,与
“模板特化”没有什么不同(也就说以上的规则也适用于它)。
(8)
模板定义中的名字解析:
不依赖于模板参数的名字在模板定义时被解析;依赖于模板参数的名字在模板被实例化时被解析。
(9)
成员模板
§
模板构造函数不会遮蔽类隐式生成的拷贝构造函数。
§ 如果一个成员模板被定义在类模板定义之外,则它的定义前面就必须加上类模板参数表,然后再跟上自己的模板参数表。例如:
template <class T>
class Queue
{
template <class Type> class CL;
public:
template <class Iter>
void assign(Iter first, Iter last);
//...
}
template <class T> template <class Type>
//
此处不必一定用T和Iter
class Queue<T>::CL<Type>
{
//...
类定义
}
template <class T> template <class Iter>
//
此处不必一定用T和Iter
void Queue<T>::assign(Iter first, Iter last)
{ /* ... */ }
(10)
类缺省模板参数
§ 只有类模板才可以有缺省模板参数,函数模板不可以。
§ 类型模板参数和非类型模板参数都可以有缺省值。
35.
异常处理:两个独立开发的程序组在程序执行期间,遇到程序不正常的情况时,用来相互通信的一种机制。
§ 异常就是程序可能检测到的,运行时刻不正常的情况。
§ 虽然异常往往是
class类型的对象,但是throw表达式也可以抛出任意类型的对象。
§ 函数try块:
int func()
try {
[
函数定义]
}
catch (
异常类型)
{ /* .... */ }
[
其他catch语句]
函数
try块是保证“在构造函数中捕获所有在对象构造期间抛出的异常”的唯一解决办法:
Account::Account(const char* name, double opening_bal)
try
: _balance(opening_bal)
{
/*
函数实现 */
}
§ catch子句语法:
catch (type_name | object_declaration) { [复合语句] }
catch子句的异常声明行为特别象函数参数声明
,catch子句象函数定义, throw表达式象函数调用。
§ 当异常被组织成类层次结构时,类类型的异常可能会被该类类型的公有基类的
catch子句捕获。这对于类类型的对象,指针,引用都适用。
§ catch (...); 语句可以处理任何种类异常,它与其它
catch子句联用时,它必须放在异常处理代码表的最后,否则会产生编译错误。
§ 当异常被捕获即(相应得
catch子句完成工作后),如果它没有包含返回语句,则程序将在catch子句列表的最后子句之后继续。(还请注意,catch子句与case语句不同,前者摆放顺序会影响程序执行,且它不会象case语句那样由于没有break语句而继续执行向一个case语句!)
§ 异常对象总是在抛出点被创建,即使
throw表达式不是一个构造函数调用,或者它没有表现出要创建一个异常对象。
§ 重新抛出异常:
throw; 重新抛出的是原来的异常对象。
§ 异常规范:它随着函数声明列出该函数可能抛出的异常。
§1. 如果函数声明指定了一个异常规范,则同一函数的重复声明必须指定同一类型的异常规范。
§2. 空的异常规范保证函数不会抛出任何异常,而不指定异常规范的函数可以抛出任何类型的异常。
§3. 如果函数抛出一个没有列在其异常规范中的异常,
且该函数自己没有把该异常处理掉,则系统会调用unexpected(),它的缺省行为是调用terminate()。异常规范违例只有在运行时刻才会被检测到。
§4. 当
带有异常规范的函数指针被初始化(或被赋值)时,用作初始值(或右值)的指针异常规范必须与被初始化(或被赋值)的指针异常规范
一样或更加严格。(所谓严格,就是只抛出异常的类型及数量)
§5. 抛出的异常类型与异常规范中指定的类型之间不允许类型转换;但是这里有个
例外:当异常规范指定一个类类型或(类类型指针)时,可以抛出从该类类型公有派生的类类型(或指针类型)的异常对象。(
引用应该也是一样,书中没说!)
36.
类的定义包括
类头(由关键字class及后面的类名构成)和
类体(由一对花括号包围),以及后接一个分号或一系列声明。在类体中,定义了
类成员表。
§ 每个类的定义引入不同的类类型,即使两个类具有相同的成员表,他们仍是不同的类型。
§ 一旦到了类体的结尾,即结束右括号“
}”,我们就说一个类被定义了一次。一旦定义了一个类,则该类的所有成员就都是已知的,类的大小也是已知的了。
由此可以知道
:
(1)
class B { /* ... */ } obj;可以这么定义
obj对象。
(2)
一个类不能有自身类型的非静态数据成员(
换句话说,可以有自身类型的静态数据成员
)
。
§ 当一个类的类头被看到时,他就被视为已经
被声明了,所以一个类可以用指向自身类型的指针或引用作为非静态数据成员。指针和引用都有固定的大小(
此处所指的是引用在底层是用指针实现的,所以说引用的大小固定,而不是说表达式
sizeof ref_obj;
的值固定),与他们指向的对象的大小无关。
37.
成员函数可以直接访问它所属类的成员.
例如:
class A
{
private:
int _x;
public:
void copy(const A& src)
{
_x = src._x; //虽然
_x为src私有成员,但是依然可以直接访问
}
}
§ const成员函数不可修改类的数据成员,除非该数据成员被声明为
mutable。
它可以被相同参数表的非
const成员函数重载,此时对象的常量性决定了调用哪个函数。
§
一个const类对象从函数构造完成时到析构开始时刻这段时间内被认为是const,所以说即使是const类对象也可以调用构造函数和析构函数。一个const类对象只可以调const成员函数,构造和析构函数。
§ 与
const对象一样,volatile类对象,只有volatile成员函数,构造函数,析构函数可以被调用。
volatile
限定修饰符: 当一个对象的值可能在编译器的控制或监测之外被改变时(例如多线程程序设计中),那么将该变量声明成
volatile,则编译器执行的某些优化行为不能应用在该对象上。
38.
this指针:每个类的非静态成员函数都含有一个指向被调用对象的指针。
编译器为了支持
this指针,做了如下工作:
在类的成员函数上加上this指针,同时在成员函数的调用上加上一个额外的实参—被调用对象的地址:
例如:
void myclass::foo(int x)
{ m_data = x; }
将被转换为:
void foo(myclass* this, int x)
{ this->m_data = x; }
myclass mc;
mc.foo(1);
被转换为:
mc.(&mc, 1);
39.
类的静态数据成员:
§ 整值型静态常量数据成员可以在类体中用常量初始化,但仍然需要被定义在类定义之外。
§ 静态数据成员是独立于类的任何对象而存在的唯一实例。
也就是说在类对象的内存模型中不包含静态数据成员。
§ 静态数据成员的类型可以是其所属类,同时静态数据成员可以作为成员函数的缺省实参。而非静态数据成员均不满足以上两点。这是由于以下规则:
类的非静态数据成员和成员函数在被程序使用前必须被绑定到该类类型的对象上或者是指向该类类型的对象的指针上,而访问静态成员﹑类型名和枚举值则不需要。
§ 可以通过成员访问操作符(
.和->)来访问静态数据成员,也可以用 classname::来限定静态数据成员来访问它。
40.
静态成员函数:
§ 它
只能访问类的静态数据成员和
静态成员函数。
§ 它不能被声明为
const 或 volatile。(这是由于static member function 只能修改static数据成员,而static数据成员不属于某个类对象一部分,也就是说访问不了non-static数据成员,所以说不存在const函数之说)。
§ 它也不可以声明为
virtual。
§ 可以通过成员访问操作符(
.和->)或用 classname::来限定函数名来访问和调用它。
41.
指向类成员的指针
§ 类的非静态成员类型与普通的类型相比多了一个类域限定,而
静态类成员指针是普通类型的指针。
例如:
class B
{
public:
int _x;
void foo(int x);
}
int B::*pd = &B::_x;
void (B::*pf)() = &B::foo; //此处
&不可少,和普通的函数指针不一样
§ 使用指向类成员的指针:通过操作符
->* 和 .*
例如:
(接上例)
B b;
B* pb = &b;
(b.*pf)(0); // .* 操作符优先级低于()所以要加括号
(pb->*pf)(0); // ->* 操作符优先级低于()所以要加括号
b.*pd = 1;
pb->*pd = 1;
42.
联合(union):一种节省空间的类,它的数据成员在内存中存储是相互重叠的,每个数据成员都在相投的内存地址开始。分配给联合的存储区数量是要包含它最大的数据成员所需的内存数。同一时刻只有一个成员可以被赋给一个值。
§ 缺省情况下,
union的成员都是公有成员。
§ union不能有静态数据成员或是引用成员,如果一个类类型定义了构造函数,析构函数或拷贝赋值函数,则它不能成为
union的成员类型。
§ 在定义
union时,union的名字是可选的。如果在程序中不需要用union的名字作为类型声明其他的对象,则在定义union类型时就没必要提供名字了。
§ 匿名union是没有名字的
union,并且它后面也没有跟着对象定义。
匿名
union的数据成员可以在定义该union的域中被直接访问,且它不能含有私有和保护成员,也不能定义成员函数。全局域中定义的匿名union必须被声明在未命名的名字空间中或者被声明为static。
43.
位域(bit-field): 一种节省空间的成员, 不能将取地址操作符(&)应用到位域上,也
没有指向类的位域的指针,位域不能是类的静态成员。
举个例子:
class File
{
int mode : 2;
int modified : 1;
int prot_owner : 1;
/* ... */
}
44.
类域中的名字解析
§ inline成员函数定义中的名字解析:函数声明在其所在的类定义中出现的位置被处理,而函数体在完整的类域中被处理
----所有成员声明都被看到。
§ 用作函数缺省实参的名字解析:在完整的类域中被解析。
§ 用在类定义中的名字解析(以上两种除外):首先在名字使用之前出现的类成员的声明应予以考虑;若不成功,则在类定义之前的名字空间域中出现的声明应予以考虑。
§ 用在类成员函数定义中的名字解析:首先考虑函数局部域中的声明;若不成功考虑所有类成员声明;还不成功的话,考虑在成员函数定义之前的名字空间域中出现的声明。
§ 用在类静态成员定义中的名字解析:首先考虑完整的类域,其次考虑静态成员定义之前的名字空间域中的声明。
45.
嵌套类(Nested class): 一个类定义在另一个类中。
§ 如果没有在嵌套类体内以
inline形式定义嵌套类的成员函数时,就必须在最外围的类之外定义这些成员函数。
例如:
B为A的嵌套类,且B的成员函数foo未在类体中定义,则:
void A::B::f()
{ /* ... */ }
§ 嵌套类也可以被定义在外围类之外:
class A
{
class B;
//...
其他声明
};
class A::B //要用外围类名限定修饰嵌套类名
{ /* ... */ };
46.
局部类:定义在函数体内的类
§ 局部类的成员函数必须定义在类定义中。
47.
枚举(
enum)的定义并不像类定义一样维护了自己相关的域,因此枚举值可以在定义枚举的域内被直接访问,而不需要也不可以加上枚举类型名修饰限定。
48.
缺省构造函数:是指不需要用户指定实参就能够被调用的构造函数(这不意味着它不能接受参数)。
§ 如果类不含有构造函数,且该类含有类数据成员或者该类是继承来的比较复杂的类,编译器可能会自动生成一个缺省构造函数,但是他不会为内置或符合类型数据(如指针和数组)提供初始值。(
不要错误地以为:如果不存在构造函数则编译器就会自动生成一个缺省构造函数)
49.
非公有的构造函数的主要作用:
§ 防止用一个类的对象向该类的另一个对象作拷贝;
§ 指出一个类只能在继承层次用作基类,而不能直接被应用程序操作
;
50.
析构函数:
§ 显式的析构函数调用,主要发生在用定位
new表达式,说不清最好看代码:
class Image; // 在其它地方已定义过
char *pstr = new char[sizeof (Image)];
Image *pImage = new (pstr)Image;
//...
//
如果像下面这样的话,虽然会自动调用析构函数,但是同时释放了存储区
// delete pImage;
pImage->~Image();
//此后仍然可以在存储区上操作
//...
delete pstr; //最终释放存储区
§ inline析构函数可能是代码程序膨胀的一个源泉,因为它被插入到函数中的每个退出点,以析构每个活动的局部类对象。
51.
成员初始化表:在观念上可以把类的构造函数分为初始化阶段和计算阶段,所以在成员处理化表中初始化类成员可以看作是“初始化”,而在构造函数体内初始化类成员是“赋值”,相比之下,成员初始化表的效率一般较高。
§ 类的
const和引用成员必须在成员初始化表中被初始化。
§ 每个成员在成员初始化表中只能出现一次,初始化的顺序不是由名字所在初始化表的顺序决定,而是
由成员在类中被声明的顺序决定的。
52.
操作符重载:
§ 重载操作符的应用对象:
不可重载的操作符:
(还是记住这几个比较实在)
::
|
.*
|
.
|
? :
|
sizeof
|
typeid
|
static_cast
|
const_cast
|
reinterpret_cast
|
dynamic_cast
|
可以重载的运算符:
+
|
-
|
*
|
/
|
%
|
^
|
&
|
|
|
~
|
!
|
,
|
=
|
<
|
>
|
<=
|
>=
|
++
|
--
|
<<
|
>>
|
==
|
!=
|
&&
|
||
|
+=
|
-=
|
/=
|
%=
|
^=
|
&=
|
|=
|
*=
|
<<=
|
>>=
|
[]
|
()
|
->
|
->*
|
new
|
new[]
|
delete
|
delete[]
|
|
|
|
§ 操作符函数可以为类成员函数,也可以是非成员函数。
但是要注意以下情况:
·
如果一个重载操作符是类成员,那么只有跟他一起被使用的左操作数是该类的对象时,它才被考虑。如果该操作符的做操作数必须是其它类型,那么重载操作符必须是名字空间成员。
注意:类非静态成员重载操作符比对应的名字空间的重载运算符少一个参数,这是由于类成员函数的隐含的this指针。
·
赋值(=)、下标([])、调用(())和成员访问箭头(->)操作符必须被定义为类成员操作符。
· 如果一个重载操作符为类成员,那么除了
operator new, operator delete, operator new[], operator delete[] 这四个为静态成员函数之外,其他的必须声明为非静态成员函数。
之所以这四个为静态成员函数,是由于在该类对象被创建之前调用operator new或operator new[], 在该类对象被销毁后才调用operator delete或operator delete[], 这么说来哪来的this指针!同时请注意,不必在这四个重载运算符的声明前加static,编译器已经自动添加了。
§ 规则:
·
只能为类类型或枚举类型的操作数定义重载操作符,即要么把重载操作符声明为类的成员,要么声明为名字空间成员,同时至少有一个类或枚举类型的参数。
·
操作符预定义的优先级,结合性,操作数个数都不可改变。
·
除了operator()之外,对其它重载操作符提供缺省实参都是非法的。
·
对于复合赋值操作符必须被显示定义才会有效。
也就是说,即使定义了
operator+ 和operator=,它也不会自动生成operator+=。
·
不可以自己创造操作符,没有出现在上表中的操作符均不可重载。
§ 个别特殊的重载操作符:
·
函数对象是一个重载了调用操作符
operator()的类。
它与函数指针相比
优点在于:
函数指针变量在运行时刻才能确定其值,所以不可能被inline,而通过定义inline的operator()可以实现编译时刻函数代码展开,避免函数调用的开销。
·
operator -> 的必须返回值必须为:一个类类型指针或者是“定义operator->的类” 的一个对象。
如果返回类型是一个类类型的指针,则内置成员访问操作符箭头的语义被应用在返回值上;如果返回值是另外一个类的对象或引用,则递归应用该过程,直到返回的类型是指针类型或者语句错误。
·
前置与后置的 ++ 和 -- 操作符:对于后置的 ++ 和 -- 操作符 多了一个int型参数以作区别,当想显式调用后置版本的这两个操作符时,需要指定一个整型实参。
·
操作符new (以及new[])和delete(以及delete[]):两个操作符主要重载为类成员函数,如果需要也可以重载为名字空间的操作符函数。
类对象先调用new操作符再调用构造函数,而先调用析构函数再调用操作符delete。
用具体的代码给出语法要求:
#include <cstddef> //包含:
typedef unsigned int size_t
class A
{
public:
void *operator new(size_t);
void *operator new[](size_t);
void *operator new(size_t, A*); //定位操作符
new
//还可以重载操作符
new,但是第一参数必须为size_t
//...
//以下两个都可以,一般只需要提供一个即可
void operator delete(void *);
void operator delete(void *, size_t);
//以下两个都可以,一般只需要提供一个即可
void operator delete[](void *);
void operator delete[](void *, size_t);
//与之匹配的
new操作符被调用时如果抛出异常,则调用它!
//从第二参数开始相同算匹配。
void operator delete(void *
, A*);
//还可以重载操作符
delete,但是第一参数必须为void *
//...
}
§ 也可以显示调用操作符函数:
例如
: a + b;
也可以写成:
operator+(a, b);
53.
友元(
friend):让程序在外部拥有访问类的非公有成员的权利。
§ 友元不是类的成员,所以它的声明不受所在类的声明区域(
private、public、protected)的影响。
§ 一个名字空间函数、另一个在此之前
被定义的类的成员函数、或者一个完整的类都可以声明为类的友元。(
被声明为友元的“名字空间函数”或者“一个完整的类”不需要先被声明或定义。)
54.
多态(polymorphism):
总的来说,多态就是单一的接口对不同的事件作出不同的反应;对于
C++来说,一般指的是基类的指针或引用可以操纵其派生类的能力。我觉得函数重载、运算符重载、隐藏、虚函数、动态绑定这些都是多态的表现。
55.
派的继承(
inheritance)与派生
§ 定义一个派生类,语法:
class Derive_Class : access_level Base_Class [, access_level Base_Class [, ...]]
{
/*
类定义 */
};
在派生表中指定的类必须首先被
定义好,方可被指定为基类;派生类的前向声明不能包含它的派生表,而只是类名。
§ 虽然基类的指针和引用可以操纵派生类对象,但是
通过基类的指针和引用只能访问基类的public接口。
§ 每个基类代表了一个由该基类的非静态数据成员组成的
子对象;派生类对象由其基类子对象以及“由派生类的非静态数据成员构成的派生部分”组成。
§ 虽然基类的成员可以被派生类直接访问,但是它仍然属于
基类的域,派生类的名字会隐藏(不是重载)基类的名字。
§ 派生类可以直接访问自身以及该类其他对象的基类
protected成员,以及该类其它对象的protected和private成员;派生类不能访问另一个对立的基类对象的protected成员。
§ 友元关系没有被继承。
§ 构造函数调用顺序:
(
1)调用基类构造函数(如果有多个基类,则调用次序是基类在类派生表中出现的次序);(2) 成员类对象构造函数(如果有多个成员类对象,则调用次序是对象在类中声明的顺序);
(
3)派生类构造函数。
当然,这是一个递归的过程,需要注意的是(1)和(2)中的调用次序和它们在成员初始化表中出现的顺序无关。
§ 析构函数调用的顺序和构造函数调用的顺序
相反。
§ 在非虚拟派生中,派生类只能显示初始化其直接基类;在虚拟派生中,虚拟基类的初始化变成了最终派生类的责任,这个最终派生类是由每个特定类对象的声明来决定的。
§ 按成员初始化和赋值:先对基类子对象按成员初始化(或赋值),如果基类体提供了一个显式的拷贝构造函数(或拷贝赋值操作符),则调用之;否则,递归地对基类和基类子对象的成员应用缺省的按成员初始化(或赋值)。
§ 派生类可以将继承得到的成员
恢复到原来的访问级别,该访问级别不能比基类中原来指定的级别更严格或更不严格。具体方法如下:
class Base
{
public:
int _x;
};
class Derive : private Base
{
public:
using Base::_x;
};
§ public、private和protected继承:
|
public
|
protected
|
private
|
|||
public
|
public
|
protected
|
不可访问
|
|||
protected
|
protected
|
protected
|
不可访问
|
|||
private
|
private
|
private
|
不可访问
|
(
其中Base)代表成员在基类中的访问级别(access level),D代表成员在派生类中的访问级别,I代表继承方式。
另外,
public继承下任何函数都可以将Derive*(或者Derive&)转换到Base*(或Base&);而对于非公有继承,这种转换只在Derive内部和友元可以执行。
§ 用公有派生类对象初始化基类对象:(
关于它的优先级参考第16条)
class Base { /* ... */ };
class Derive : public Base { /* ... */ };
Derive d;
Base b = d; //OK,实际上调用的是
Base的拷贝构造函数
b = d; //OK,实际上调用的是
Base的赋值操作符函数
56.
虚拟函数和覆盖(override):
§ 当
成员函数是virtual的时候,通过一个类对象的指针或引用而调用的该成员函数,是在该类对象的指针或引用的动态类型中被定义的成员函数。
§ 覆盖(
override,改写)规则的条件:成员函数必须在不同的域中(即在基类和派生类中);函数名称和参数列表必须完全相同;基类函数必须有virtual关键字;返回值也必须相同,但有一个特例:派生类实例的返回值可以是基类实例的返回类型的公有派生类类型。(这也叫做虚拟函数机制)。
§ 纯虚函数(pure virtual funtion):
class A
{
public:
virtual void f() = 0;
//
声明一个纯虚函数
//...
}
纯虚函数只提供一个可以被子类型改写的接口,它本身并不能通过虚拟函数机制被调用,但是你仍然可以为纯虚函数提供定义,只不过如果想调用它的话只有通过在函数名前加类名修饰限制。
§
包含(或继承)一个或多个纯虚拟函数的类被编译器识别为
抽象基类
(abstract base class),试图创建一个抽象基类的独立类对象会导致编译时刻错误。
§ 通过在调用的
virtual函数名前加上类名修饰限定可以明确指出被调用的函数,而不会遵守覆盖机制。
§ 虚函数的
virtual关键字应该加在类定义里的成员函数声明上,而不该加在类外成员函数定义上;一旦基类有了virtual关键字,派生类中有无virtual关键字无所谓。
§ 如果通过基类指针或引用调用派生类的
virtual函数,那么传递给它的缺省实参是由基类指定的。(缺省实参并不是在运行时刻决定的。)
§ 虚拟函数承接了“调用者所属类类型”的访问级别。
§ 虚拟析构函数:为了能够通过基类的指针释放派生类的对象。例如:
Base *pb = new Derive;
delete = pb; //如果
Base的析构函数为virtual,则可以正确地调用Derive的析构函数。
为了防止用基类的指针释放派生类的对象,可以将基类的析构函数声明为protected的虚拟函数。(一般用在根基类)
§ 静态成员函数不可以为虚拟函数。
§ 在基类构造函数和析构函数中调用的虚拟函数实例均为该基类中的实例,而不遵守覆盖规则。
57.
虚拟继承:
§ 语句:
class Base { /* ... */ };
class Derive1 : public virtual Base { /* ... */ };
//public
和virtual的顺序
class Derive2 : virtual public Base { /* ... */ };
//
无关紧要
§ 在虚拟继承下,只有一个共享的基类子对象被继承,而无论基类在派生层次中出现多少次,共享的基类子对象被称为
虚拟基类(virtual base class)。
§ 虚拟基类的初始化是最终派生类的责任,这个最终派生类是由每个特定的类对象的声明决定的。
§ 在类层次结构中,虚拟基类的构造函数总是比其它的类先被调用。
建议:除非虚拟继承为一个眼前的设计问题提供了解决方案,否则不要使用它。
58.
多继承:
§ 多继承下的名字解析:查找过程对每个基类的继承子树同时进行检查,如果只在其中一个基类子树中找到了声明,则该标识符被解析,查找算法结束;如果在两个或多个基类子树中都找到可声明,则表示这个引用是二义的;如果都找不到,则编译错误。
§ 对于多继承的派生类,即使它的声明被称成功编译,也不能保证没有
潜在的二义性问题。
例如:
class Base1
{
public:
void f(int) { cout << “void Base1::f(int)/n”; }
}
class Base2
{
public:
void f(double) { cout << “void Base2::f(double)/n”; }
}
class Derive : public Base1, public Base2
{
}
int main()
{
Derive d;
d.f(1);
//
二义性, 如果没有此句则编译无误!
}
§ 构造函数调用次序:
编译器按照直接基类在派生类表中声明的次序,对于每个基类子树按照深度优先的次序查找虚拟基类,并依次调用其构造函数;然后非虚拟基类的构造函数按照声明次序被调用。
析构函数调用顺序与构造函数顺序相反。
59.
RTTI(Run-Time Type Identification, 运行时刻类型识别):
(
对RTTI的支持是与编译器实现相关的,你应该查询编译器手册来找到确切的RTTI支持)
(
1)dynamic_cast操作符:它允许在运行时刻进行类型转换,从而使程序能够在一个类层次结构中安全地转换类型,把基类的指针转换成派生类指针,或把指向基类的左值转换成派生类的引用。
§ 如果针对指针类型的
dynamic_cast失败,则dynamic_cast的结果为0;如果针对引用类型的dynamic_cast失败,则dynamic_cast会抛出std::bad_cast异常。
§
dynamic_cast的操作数为:必须是
含有virtual function 的class的指针或引用。
(
2)typeid操作符:它在程序中可以获得一个表达式的类型,如果表达式是一个类类型,且含有虚拟函数,则答案会不同于表达式本身的类型。
§
typeid操作符的操作数:可以是任何的类型名、对象、指针和引用;但是只有当操作数为含有
virtual function的class的对象或引用时(指针不可以),才能获得派生类的信息。
§ 如果操作数为引用的
typeid表达式失败,则抛出std::bad_typeid异常。
§ 操作数是对基类指针解引用得到的并不一定是一个基类类型:
class Base
{
public:
virtual void foo(){};
/*
其他定义 */
}
class Derive : public Base
{ /*
类成员定义 */ }
Base *pb = new Derive;
此时
typeid(*pb) == typeid(Derive);
§ type_info类:它的用途在于作为
typeid操作符的返回值类型,具体实现依赖于编译器,该类一般定义如下:
class type_info
{
private:
type_info(const type_info&);
type_info& operator=(const type_info&);
public:
virtual ~type_info();
int operator==(const type_info&) const;
int operator!=(const type_info&) const;
const char * name() const;
/* 依赖于编译器的其他定义
*/
};
我们不能在程序中自行定义一个
type_info对象。
60.
扩展一个类库:
只要不增加虚拟成员函数,我们就可以扩展已有的类库。
这是由于:为类层次结构增加虚拟函数时,我们必须重新编译类层次结构中的所有类成员函数。(
VC的头文件属性都不是“只读”,莫非是为了方便我们扩展?)