1.C++有两个地方空格不能由换行符代替,第一个字符串字面值,第二就是空格符不能出现在预处理指示中。
2.C++是静态类型,即在编译时进行检查。
3.char有3中类型,普通,unsigned, signed.
4.C++所有字符串字面值都由编译器在后面自动加上一个空字符。两个相邻仅由空格,制表符,或换行符分开的字符串字面值可以连接成一个新字符串字面值。
1. 1024f wrong
2. 3.14UL wrong
3. 左值就是变量的地址,或者是一个代表“对象在内存中的位置”的表达式。右值就是变量的值。
4. char _这个声明是正确的
5. 初始化不是赋值,初始化指创建变量并给它赋初始值,而赋值则是擦除对象的当前值并用新值代替。比如 int ival = 1024;//copy-initialization,
int ival; ival = 1024;//赋值
6.只能定义一次,可以多次声明
6. 直接初始化语法更灵活且效率更高
7. 有多个初始化式时,不能使用复制初始化。
8. int month = 09, day = 07,firt wrong, second right.
9. int ival = ival,语法正确,但是仍然未初始化
10. double salary = wage = 9999.99;//wrong
11. 没有默认构造函数的类类型,每个定义必须提供显示的初始化式,没有初始值是根本不能定义这种类型的变量的。
12. extern int i;//声明 只有当extern声明位于函数外部时,才可以含有初始化式。extern const int &ri;//声明一个const引用
13. 常量在定以后就不能修改,所以定义时必须初始化。
14. 把一个非const变量定义在一个文件中(全局),在另一个文件可以通过extern使用这个变量。但是对于全局作用域声明的const变量是定义该对象的文件的局部变量,不能被其他文件访问。但是可以通过指定const变量为extern,就可以在整个程序中访问const对象。非const变量默认为exte rn,而const变量必须显示指定extern。
15. const int bufsize = fcn();//right
16. 复合类型指用其他类型定义的类型。
17. 不能定义到引用类型的引用。
18. int &refVal = 10;// wrong
19. const引用(指向const对象的引用)是指向const(对象)的引用。const int ival = 1024; const int &refVal = ival; //right int &ref2 = ival;// wrong,非const引用不能绑定到const对象。
20. const 引用可以初始化为不同类型的对象或者初始化为右值,如字面值常量:
int i = 42; // legal for const references only const int &r = 42; const int &r2 = r + i;
同样的初始化对于非 const 引用却是不合法的,而且会导致编译时错误。其原因非常微妙,值得解释一下。观察将引用绑定到不同的类型时所发生的事情,最容易理解上述行为。假如我们编写
double dval = 3.14; const int &ri = dval;
编译器会把这些代码转换成如以下形式的编码:
int temp = dval; // create temporary int from the double const int &ri = temp; // bind ri to that temporary
如果 ri 不是 const,那么可以给 ri 赋一新值。这样做不会修改 dval,而是修改了 temp。期望对 ri 的赋值会修改 dval 的程序员会发现 dval 并没有被修改。仅允许 const 引用绑定到需要临时使用的值完全避免了这个问题,因为 const 引用是只读的。
总之,非const引用只能绑定到与该引用同类型的对象。const引用则可以绑定到不同但相关的类型的对象或者绑定到右值。
21. 常量表达式是编译器在编译时就能够计算出结果的整型表达式。
22. enum....
23. 定义变量和定义数据成员有个区别,一般不能把类成员的初始化作为其定义的一部分。类通过构造函数初始化数据成员。
24. public部分可以使程序的任何代码执行这些部分,但是对于private部分,只有类的组成部分代码(还有类的友元)才能访问private。
25. 头文件一般包含类的定义、extern变量的声明和函数的声明。头文件可以含有三中定义:类的定义,编译时就已知道的const对象(默认定义时该变量的文件的局部变量),和inline函数。这些实体可以在多个源文件定义,只要每个源文件中的定义时相同的。当我们在头文件中定义了const变量后,每个包含该头文件的源文件都有了自己的const变量,其名称和值都是一样的。当该 const 变量是用常量表达式初始化时,可以保证所有的变量都有相同的值。但是在实践中,大部分的编译器在编译时都会用相应的常量表达式替换这些 const 变量的任何使用。所以,在实践中不会有任何存储空间用于存储用常量表达式初始化的 const 变量。如果 const 变量不是用常量表达式初始化,那么它就不应该在头文件中定义。相反,和其他的变量一样,该 const 变量应该在一个源文件中定义并初始化。应在头文件中为它添加 extern 声明,以使其能被多个文件共享。const double pi = 3.1415926可以放在头文件中,而const double pi = sqrt (2.0)不应该。因为不是由常量表达式产生的。
26.头文件中必须总是使用完全限定的标准库名字,一般不适用using。
27. 字符串字面值与标准库string类型不是同一种类型。
28. 默认构造函数就是不带参数或者为所有形参提供默认实参的构造函数。
29. getline函数返回时丢弃换行符,换行符将不会存储在string对象中。并且不忽略 前面的空白符,直到遇到换行符。
30. 下标操作返回左值!!!!!!!!!!
31.vector保存内置类型如int类型,将用0值初始化,前提是定义的时候指明了vector大小。
32. STL在循环中一般使用!=而不使用<等,原因在于STL中vector等容器是动态变化的。
33. 我们对const_iterator类型解引用时,可以得到一个指向const对象的引用。
34. 数组元素不能是引用,可以是其他任意复合类型,类类型,以及内置类型,数组不管在哪里定义,如果元素是类类型,就自动调用该类默认构造函数初始化,如果没有默认构造函数,则必须提供显示初始化。对于内置类型数组,除非显示提供元素初值,否则内置类型局部数组元素没有初始化。
35.非const变量以及要到运行阶段才知道的const变量不能作为数组维数。
36. char ca1[] = {'C', '+', '+'}; // no null char ca2[] = {'C', '+', '+', '\0'}; // explicit null char ca3[] = "C++"; // null terminator added automatically,大小分别为3,4,4,但是puts(ca1)时,不会正常结束,strlen结果未知,但是sizeof结果是3;
37. const char ca4[3] = "ily";//wrong
38.vector<int> ivec = { 0, 1, 1, 2, 3, 5,8 };错误
39.把 int 型变量赋给指针是非法的,尽管此 int 型变量的值可能为 0。但允许把数值 0 或在编译时可获得 0 值的 const 量赋给指针。
40. void* 指针只支持几种有限的操作:与另一个指针进行比较;向函数传递void* 指针或从函数返回 void* 指针;给另一个 void* 指针赋值。不允许使用void* 指针操纵它所指向的对象。我们将在第 5.12.4 节讨论如何重新获取存储在 void* 指针中的地址。
41. const double *cptr; // cptr may point to a double that is const
*cptr = 42; // error: *cptr might be const
double i; cptr = &i;//right
const double pi = 3.14;
double *ptr = π // error: ptr is a plain pointer
const double *cptr = π // ok: cptr is a pointer to const
不能使用 void* 指针保存 const 对象的地址,而必须使用 const void* 类型的指针保存 const 对象的地址。允许把非 const 对象的地址赋给指向 const 对象的指针,但不允许通过该指针修改其所指对象的值
42. int *const curErr = &errNumb; // curErr is a constant pointer。与其他 const 量一样,const 指针的值不能修改,这就意味着不能使 curErr 指向其他对象。与任何 const 量一样,const 指针也必须在定义时初始化。指针本身是 const 的事实并没有说明是否能使用该指针修改它所指向对象的值。指针所指对象的值能否修改完全取决于该对象的类型。
43. 指向 const 对象的 const 指针。
const double pi = 3.14159;
// pi_ptr is const and points to a const object
const double *const pi_ptr = π 、
本例中,既不能修改 pi_ptr 所指向对象的值,也不允许修改该指针的指向(即 pi_ptr 中存放的地址值)。
44.在 typedef(第 2.6 节)中使用指针往往会带来意外的结果。下面是一个几乎所有人刚开始时都会答错的问题。假设给出以下语句:
typedef string *pstring;
const pstring cstr;
请问 cstr 变量是什么类型?简单的回答是 const pstring 类型的指针。进一步问:const pstring 指针所表示的真实类型是什么?很多人都认为真正的类型是:
const string *cstr; // wrong interpretation of const pstring cstr
也就是说,const pstring 是一种指针,指向 string 类型的 const 对象,但这是错误的。错误的原因在于将 typedef 当做文本扩展了。声明 const pstring 时,const 修饰的是 pstring 的类型,这是一个指针。因此,该声明语句应该是把cstr 定义为指向 string 类型对象的 const 指针,这个定义等价于:
// cstr is a const pointer to string
string *const cstr; // equivalent to const pstring cstr。
string s;
typedef string *pstring;
const pstring cstr1 = &s; // written this way the type is obscured
pstring const cstr2 = &s; // all three decreations are the same type
string *const cstr3 = &s; // they're all const pointers to string
把 const 放在类型 pstring 之后,然后从右向左阅读该声明语句就会非常清楚地知道 cstr2 是 const pstring 类型,即指向 string 对象的 const 指针。问题1:typedef后怎么声明一个指向const对象的指针?
45.允许把非 const 对象的地址赋给指向 const 对象的指针。不能把一个const对象的地址赋值给一个const指针或者普通指针,只能赋值给一个指向const对象的指针。
46. 字符串字面值的类型就是const char 类型的数组。
47. 在自由存储区中创建的数组对象是没有名字的,程序员只能通过其地址间接地访问堆中的对象。动态分配数组时,如果数组元素具有类类型,将使用该类的默认构造函数实现初始化;如果数组元素是内置类型,则无初始化。也可以这样int *pia2 = new int[10] (); // array of 10 uninitialized ints圆括号要求编译器对数组做值初始化,在本例中即把数组元素都设置为0。对于动态分配的数组,其元素只能初始化为元素类型的默认值,而不能像数组变量一样,用初始化列表为数组元素提供各不相同的初值。如果我们在自由存储区中创建的数组存储了内置类型的 const 对象,则必须为这个数组提供初始化:因为数组元素都是 const 对象,无法赋值。实现这个要求的唯一方法是对数组做值初始化。C++ 允许定义类类型的 const 数组,但该类类型必须提供默认构造函数,当然,已创建的常量元素不允许修改——因此这样的数组实际上用处不大。C++ 虽然不允许定义长度为 0 的数组变量,但明确指出,调用 new 动态创建长度为 0 的数组是合法的,长度为0的动态数组不能解引用。
48. c_str 返回的数组并不保证一定是有效的,接下来对 st2 的操作有可能会改变 st2 的值,使刚才返回的数组失效。如果程序需要持续访问该数据,则应该复制 c_str 函数返回的数组。
49. int *ip[4]; // array of pointers to int
int (*ip)[4]; // pointer to an array of 4 ints
以下程序用 typedef 为 ia 的元素类型定义新的类型名:
typedef int int_array[4];
int_array *ip = ia;
50. 一般表达式的结果是右值。
51. 如果操作数为负数,则位操作符如何如何处理操作数的符号位依赖于机器。因此建议使用unsigned整型操作数。
52. 移位操作的右操作数不可以是负数,而且必须是严格小于左操作数的位数的值,否则操作效果未定义。
53. 重载的操作符与该操作符的内置类型版本有相同的优先级和结合性。
54. cout << 10 < 42;//wrong
55. 算术表达式产生右值。下标和解引用以及赋值操作产生左值
56. int i, int *p; i = p = 0;//wrong
57. int i = 0 , j; j = ++i; j = i++; i == 2, j == 1;前置操作返回对象本身,后置操作返回右值
58. vector<int > ::iterator iter = ivec.begin();
while (iter != ivec.end())
cout << *iter++ << endl;
小心这段代码。这段代码是正确的。
59.cout << (i < j) ? i : j; // prints 1 or 0!
cout << i < j ? i : j; // error: compares cout to int
第二个表达式比较有趣:它将i 和j 的比较结果视为 << 操作符的操作数,输出 1 或 0。 << 操作符返回 cout 值,然后将返回结果作为条件操作符的判断条件.
60.sizeof *p 中,指针 p 可以持有一个无效地址,因为不需要对 p 做解引用操作。
61. C++只规定了&& 和||,逗号操作符,以及条件操作符的运算顺序。以下为编译器相关的语句:f1() * f2(); if (ia[index++] < ia[index])
62. 下面两个指导原则有助于处理复合表达式:
1. 如果有怀疑,则在表达式上按程序逻辑要求使用圆括号强制操作数的组合。
2. 如果要修改操作数的值,则不要在同一个语句的其他地方使用该操作数。如果必须使用改变的值,则把该表达式分割成两个独立语句:在一个语句中改变该操作数的值,再在下一个语句使用它。第二个规则有一个重要的例外:如果一个子表达式修改操作数的值,然后将该子表达式的结果用于另一个子表达式,这样则是安全的。
63. practice 5.28
64. ival != jval< kval ptr != 0 && *ptr++ ival++ && ival vec[ival++] <= vec[ival]
65. C++ 没有明确定义如何释放指向不是用 new 分配的内存地址的指针。
66. C++ 保证:删除 0 值的指针是安全的。一旦删除了指针所指向的对象,立即将指针置为 0,这样就非常清楚地表明指针不再指向任何对象。对同一个内存空间使用两次 delete 表达式。当两个指针指向同一个动态创建的对象,删除时就会发生错误。如果在其中一个指针上做 delete 运算,将该对象的内存空间返还给自由存储区,然后接着 delete 第二个指针,此时则自由存储区可能会被破坏。
67. vector<string> **pvec2 = new vector<string>[10];//wrong
68.最简单的转换为整型提升:对于所有比 int 小的整型,包括 char、signedchar、unsigned char、short 和 unsigned short,如果该类型的所有可能的值都能包容在 int 内,它们就会被提升为 int 型,否则,它们将被提升为unsigned int。如果将 bool 值提升为 int ,则 false 转换为 0,而 true 则转换为 1。
69. 不将数组转换为指针的例外情况有:数组用作取地址(&)操作符的操作数或 sizeof 操作符的操作数时,或用数组对数组的引用进行初始化时,不会将数组转换为指针.C++ 还提供了另外两种指针转换:指向任意数据类型的指针都可转换为void* 类型;整型数值常量 0 可转换为任意指针类型。
70. 当使用非 const 对象初始化 const 对象的引用时,系统将非 const 对象转换为const 对象。此外,还可以将非const 对象的地址(或非const 指针)转换为指向相关const 类型的指针(problem?)
71.const_cast ,顾名思义,将转换掉表达式的 const 性质,dynamic_cast 支持运行时识别指针或引用所指向的对象,除了添加或删除const 特性,用 const_cast 符来执行其他任何类型转换,都会引起编译错误.int *ip; char *pc = reinterpret_castchar*>(ip);程序员必须永远记得 pc 所指向的真实对象其实是 int 型,而并非字符数组。任何假设 pc 是普通字符指针的应用,都有可能带来有趣的运行时错误。旧式的强制类型转换依次与static_cast, const_cast,reinterpret_cast匹配。
72.与其他大多数语句不同,块并不是以分号结束的。
73.如果在条件表达式中定义了变量,那么变量必须初始化。将已初始化的变量值转换为 bool 值(第 5.12.3 节)后,该 bool 值决定条件是否成立。
74. case 标号必须是整型常量表达式。对于 switch 结构,只能在它的最后一个 case 标号或 default 标号后面定义变量,或者引入块语句。
75. *dest++ = *source++;执行过程:1.指针dest加1。2.指针source加1。3.将source原来指向的对象赋给dest原来所指向的对象。
76.可以在 for 语句的 init-statement 中定义多个对象;但是不管怎么样,该处只能出现一个语句,因此所有的对象必须具有相同的一般类型
const int size = 42;
int val = 0, ia[size];
// declare 3 variables local to the for loop:
// ival is an int, pi a pointer to int, and ri a reference to int
for (int ival = 0, *pi = ia, &ri = val;
ival != size;
++ival, ++pi, ++ri)
// ...
77.do while语句中不能在循环条件中定义变量。
78. goto 语句提供了函数内部的无条件跳转,实现从 goto 语句跳转到同一函数内某个带标号的语句。goto 语句不能跨越变量的定义语句向前跳转,如果确实需要在 goto 和其跳转对应的标号之间定义变量,则定义必须放在一个块语句中。向后(在源代码中,定义出现在goto之前)跳过已经执行的变量定义语句则是合法的,会导致重新定义该变量。// backward jump over declaration statement ok
begin:
int sz = get_size();
if (sz <= 0) {
goto begin;
}
注意:执行 goto 语句时,首先撤销变量 sz,然后程序的控制流程跳转到带 begin: 标号的语句继续执行,再次重新创建和初始化 sz 变量。
79.异常机制提供程序中错误检测与错误处理部分之间的通信。C++ 的异常处理(实现错误处理与其他代码分离)中包括:
1. throw 表达式,错误检测部分使用这种表达式来说明遇到了不可处理的错误。可以说,throw 引发了异常条件。
2. try 块,错误处理部分使用它来处理异常。try 语句块以 try 关键字开始,并以一个或多个 catch 子句结束。在 try 块中执行的代码所抛出(throw)的异常,通常会被其中一个catch 子句处理。由于它们“处理”异常,catch 子句也称为处理代码。
3. 由标准库定义的一组 异常类,用来在 throw 和相应的 catch 之间传递有关的错误信息。
80. 与异常不同(异常用于处理程序执行时预期要发生的错误),程序员使用assert 来测试“不可能发生”的条件。
79.调用操作符的操作数是函数名和一组参数。
80.字符串常量是const char *;
81.形参是变量,实参是表达式。
81. 函数的运行以形参的(隐式)定义和初始化开始。函数不能返回另一个函数或者内置数组类型,但可以返回指向函数的指针,或指向数组元素的指针的指针
82. 管函数的形参是 const,但是编译器却将 fcn 的定义视为其形码被声明为普通的 int 型:为了兼容C,C语言不区别const形参和非const形参
void fcn(const int i) { /* fcn can read but not write to i */ } void fcn(int i) { /* ... */ } // error: redefines fcn(int)
形参与 const 形参的等价性仅适用于非引用形参。有 const 引用形参的函数与有非 const 引用形参的函数是不同的。类似地,如果函数带有指向 const 类型的指针形参,则与带有指向相同类型的非 const 对象的指针形参的函数不相同。
83.但比较容易忽略的是,调用这样的函数时,传递一个右值(第 2.3.1 节)或具有需要转换的类型的对象同样是不允许的,const引用实参可以转换为形参类型:
// function takes a non-const reference parameter int incr(int &val) { return ++val; } int main() { short v1 = 0; const int v2 = 42; int v3 = incr(v1); // error: v1 is not an int v3 = incr(v2); // error: v2 is const v3 = incr(0); // error: literals are not lvalues v3 = incr(v1 + v2); // error: addition doesn't yield an lvalue int v4 = incr(v3); // ok: v3 is a non const object type int }
83.如果函数具有普通的非 const 引用形参,则显然不能通过 const 对象进行调用。毕竟,此时函数可以修改传递进来的对象,这样就违背了实参的 const 特性。但比较容易忽略的是,调用这样的函数时,传递一个右值或具有需要转换的类型的对象同样是不允许的;而const引用形参是允许的。
84. 指向指针的引用eg int *&i;
85. 果形参是数组的引用,编译器不会将数组实参转化为指针,而是传递数组的引用本身。在这种情况下,数组大小成为形参和实参类型的一部分。编译器检查数组的实参的大小与形参的大小是否匹配:
// ok: parameter is a reference to an array; size of array is fixed void printValues(int (&arr)[10]) { /* ... */ } int main() { int i = 0, j[2] = {0, 1}; int k[10] = {0,1,2,3,4,5,6,7,8,9}; printValues(&i); // error: argument is not an array of 10 ints printValues(j); // error: argument is not an array of 10 ints printValues(k); // ok: argument is an array of 10 ints return 0; }
int (&arr)[10]与int &arr[10]的区别,指针相似。
86. C++ 中的省略符形参是为了编译使用了 varargs 的 C 语言程序。关于如何使用 varargs,请查阅所用 C 语言编译器的文档。对于 C++ 程序,只能将简单数据类型传递给含有省略符形参的函数。实际上,当需要传递给省略符形参时,大多数类类型对象都不能正确地复制。
87. 返回类型为 void 的函数通常不能使用第二种形式(返回了值)的 return 语句,但是,它可以返回另一个返回类型同样是 void 的函数的调用结果。
88. 在含有 return 语句的循环后没有提供 return 语句是很危险的(比如while里面有return,但是函数最后没有return),因为大部分的编译器不能检测出这个漏洞,运行时会出现什么问题是不确定的。
89. 理解返回引用至关重要的是:千万不能返回局部变量的引用。返回引用的函数返回一个左值。千万不要返回指向局部对象的指针。
90. 主函数 main 不能调用自身(即不能递归)。main函数不能重载。
91. 既可以在函数声明也可以在函数定义中指定默认实参。但是,在一个文件中,只能为一个形参指定默认实参一次。
92.只有当定义某个局部变量的函数被调用时才存在的对象叫做自动变量。
92.static 局部对象确保不迟于在程序执行流程第一次经过该对象的定义语句时进行初始化。这种对象一旦被创建,在程序结束前都不会撤销。当定义静态局部对象的函数结束时,静态局部对象不会撤销,注意静态变量的作用域还是遵循局部变量。在该函数被多次调用的过程中,静态局部对象会持续存在并保持它的值。考虑下面的小例子,这个函数计算了自己被调用的次数:
size_t count_calls()
{
static size_t ctr = 0; // value will persist across calls
return ++ctr;
}
int main()
{
for (size_t i = 0; i != 10; ++i)
cout << count_calls() << endl;
return 0;
}
在第一次调用函数 count_calls 之前,ctr 就已创建并赋予初值 0。局部变量分为静态和非静态变量。
93.为了确定最佳匹配,编译器将实参类型到相应形参类型转换划分等级。转换等级以降序排列如下:
-
精确匹配。实参与形参类型相同。
-
通过类型提升实现的匹配(第 5.12.2 节)。
-
通过标准转换实现的匹配(第 5.12.3 节),标准转换之间都是等级的。
-
通过类类型转换实现的匹配(第 14.9 节将介绍这类转换)。
int *const
93. 大多数的编译器都不支持递归函数的内联。内联函数应该在头文件中定义,这一点不同于其他函数。inline 函数的定义对编译器而言必须是可见的,以便编译器能够在调用点内联展开该函数的代码。此时,仅有函数原型是不够的。
94.在使用有枚举类型形参的重载函数时,请记住:由于不同枚举类型的枚举常量值不相同,在函数重载确定过程中,不同的枚举类型会具有完全不同的行为。其枚举成员决定了它们提升的类型,而所提升的类型依赖于机器。
99.除了用作函数调用的左操作数外,对函数名的任何使用都被解释为相应的函数指针。指向不同函数类型的指针之间不存在转换。指向相应函数的指针调用它所指向的函数时,可以不用接引用操作符。
100.
函数的形参可以是指向函数的指针。这种形参可以用以下两种形式编写:
void useBigger(const string &, const string &, bool(const string &, const string &)); // equivalent declaration: explicitly define the parameter as a pointer to function void useBigger(const string &, const string &, bool (*)(const string &, const string &));
函数可以返回指向函数的指针,但是,正确写出这种返回类型相当不容易:
// ff is a function taking an int and returning a function pointer // the function pointed to returns an int and takes an int* and an int int (*ff(int))(int*, int);
使用 typedef 可使该定义更简明易懂:
// PF is a pointer to a function returning an int, taking an int* and an int typedef int (*PF)(int*, int); PF ff(int); // ff returns a pointer to function
有函数类型的形参所对应的实参将被自动转换为指向相应函数类型的指针。但是,当返回的是函数时,同样的转换操作则无法实现:
// func is a function type, not a pointer to function! typedef int func(int*, int); void f1(func); // ok: f1 has a parameter of function type func f2(int); // error: f2 has a return type of function type func *f3(int); // ok: f3 returns a pointer to function type
94.编译器隐式地将在类内定义的成员函数当作内联函数,inline成员函数必须在调用该函数的每个源文件中是可见的,不在类定义体中定义的inline函数通常应放在类定义的头文件中。
95.const对象和指向const对象的指针或引用只能用于调用其const成员函数。
95.const成员函数中const关键字必须同时出现在声明和定义中,否则会导致编译时错误
96.不完全类型只能定义指向该类型的指针及引用,或者用于声明使用该类型作为形参类型或者返回类型的函数。在使用引用或指针访问类的成员之前,必须已经定义类。类不能拥有自身类型的数据成员。类只有该类型的对象时才分配对象,一般定义类不分配对象。
95.每个成员函数都有一个额外的、隐含的形参将该成员函数与调用该函数的类对象捆绑在一起。每个成员函数(除了在第 12.6 节介绍的 static 成员函数外)都有一个额外的、隐含的形参 this(是一个const指针)。在调用成员函数时,形参 this 初始化为调用函数的对象的地址。成员函数声明的形参表后面的 const 改变了隐含的 this 形参的类型。
96.const 对象、指向 const 对象的指针或引用只能用于调用其const 成员函数,如果尝试用它们来调用非 const 成员函数,则是错误的。基于成员函数是否为cosnt可以重载成员函数,const对象只能使用const成员,非const对象都可以,但是非cosnt版本是更好的匹配。const成员函数只能返回*this作为一个const引用。
97.在成员函数中,不必显式地使用 this 指针来访问被调用函数所属对象的成员。对这个类的成员的任何没有前缀的引用,都被假定为通过指针 this 实现的引用。
98.类定义实际是在两个阶段中处理:首先编译成员声明,只有在所有成员出现后才编译它们的定义本身。
99.在C++中,名字查找发生在类型检查之前。
99.一旦一个名字被用作类型名,改名字就不能被重复定义。
typedef double Money; class Account { public: Money balance() { return bal; } // uses global definition of Money private: // error: cannot change meaning of Money typedef long double Money; Money bal; // ... };
类型别名的定义必须出现在它的使用之前。
100.当函数的返回类型不同时,编译器会认为是重载,但是又不是合法重载,所以导致编译错误。
101.不管成员是否在构造函数初始化列表中显示初始化,类类型的数据成员总是在初始化阶段初始化。初始化发生在计算阶段开始之前。假如内置类型和复合类型成员没有在初始化列表初始化,则依赖于对象的作用域。全局初始化为0,局部作用域不初始化。
102.没有默认构造函数的类类型成员,以及const以及引用类型成员,必须在构造函数初始化列表进行初始化。
98. 合成的默认构造函数一般适用于仅包含类类型成员的类。而对于含有内置类型或复合类型成员的类,则通常应该定义他们自己的构造函数初始化这些成员。没有默认构造函数的类型不能用作动态分配数组的元素类型,作为静态数组的元素则必须为每个元素提供一个显示的初始化式。对于保存该类对象的容器,不能使用接受容器大小但是没有同时提供一个元素初始化式的构造函数。
99.假如定义了一个构造函数,一般应该定义一个默认构造函数,复制构造函数也是构造函数,如果需要定义一个析构函数,则也需要其他复制构造函数和赋值操作符。只有析构函数编译器总是会合成一个。合成的析构函数并不会删除指针成员所指向的对象。析构函数不能指定任何形参,所以不能重载析构函数。先运行自己定义的析构函数,再运行合成的。合成的析构函数不会撤销static成员,static成员怎么构造,怎么析构呢???????????????????
100.可以多次发生隐式转换。对于定义了构造函数的类,不能使用显示初始化式。对于没有定义构造函数而且数据成员都为public才能使用显示初始化式。
99.假如存在两个或多个构造函数的形参都有默认参数,将导致默认构造函数重复定义,构成非法重载。
101.友元也是接口的一部分。
102.假如将A类的成员函数设为B类的友元,则必须A类必须先定义,但是A的该成员函数必须定义在B之后,为了访问B的成员。
103.static成员函数可以直接访问所属类的static成员,但是不能直接使用非static成员,并且没有this形参。static数据成员必须在类定义体外定义,且只定义一次,不是由构造函数初始化的,const static数据成员可以在类体内进行初始化,同样不是由构造函数实现,但是仍然需要在类体外进行定义,但是不用指定初始化值。一般可放在类的实现文件。在类定义体外部实现static成员函数时,可以不重复指定static关键字。static成员函数不能为const,理由在于。。。也不能声明为虚函数。static关键字只能出现声明中,不能出现在定义中。static数据成员可以是该成员所属的类类型,也可以用作默认实参,非static数据成员不能,因为无法提供对象以获取该成员的值。
104.如果没有为类类型数组提供初始化式,则用默认构造函数初始化每个元素。但是如果用常规的花括号括住的数组初始化列表来提供显示元素初始化式,则使用复制初始化每个元素。
105.
Point global; Point foo_bar(Point arg) { Point local = arg;//复制构造函数 Point *heap = new Point(global);//复制构造函数 *heap = local;//只是赋值操作符 Point pa[ 4 ] = { local, *heap };//复制构造函数 return *heap;//复制构造函数 }
106.假如类有一个数组成员,那么复制构造函数将复制数组的每个成员,一般情况下数组是不能复制的。复制构造函数一般都不为explicit,因为要用于传参和从函数返回对象。合成的复制构造函数只适用于类只含有类类型或者内置类型,对于有指针成员或者引用成员或者分配了资源,必须自己定义复制构造函数。
107.复制构造函数的参数必须为引用,理由。。。
108.能够声明成员,但是不定义,将导致链接失败,运行时错误。通过声明但是不定义一个private复制构造函数,则在用户代码中复制这个类对象,将在编译时标记为错误,而成员函数和友元中的复制尝试将在链接时导致错误。不允许复制的类对象只能作为引用传递给函数或从函数返回,也不能用作容器的元素。
109.当对象的指针或者引用超出作用域时,不会运行析构函数。
110.复制构造函数和其他构造函数一样,如果没有初始化某个类成员,则该成员的默认构造函数初始化,而不是使用成员的复制构造函数。
111.重载操作符必须具有一个类类型操作数或者枚举类型。不能为内置类型重载操作符,也不能为其添加新的操作符。除了函数调用操作符operator()之外,重载操作符使用默认实参是非法的。重载操作符优先级和结合性是固定的,但不再具备短路求值特性(如, ,and, or,)。一般将算术和关系操作符定义为非成员函数,而将赋值操作符定义为成员。
112.算术运算和输入输出已经关系操作符一般定义为非成员函数,而赋值操作符包括+=,++,--,解引用应定为成员函数,但不强制,=,[],->和()必须定义为成员函数,否则会编译错误,()调用操作符可以重载多个版本。
113.如果没有特定的重载版本,编译器将合成赋值操作符,取地址操作符,逗号操作符,内置逻辑与和逻辑或,保持各个操作符的原有特性。但是如果重载了将发生改变。
114.IO操作符必须定义为非成员函数,理由是否则左操作数将只能是该类类型的对象:
// if operator<< is a member of Sales_item
Sales_item item;
item << cout;
与习惯相反。如果要想是成员函数,也要保持习惯,则只能是ostream的成员,但是不能为标准库增加成员。
115.对象的读入可能只读入一部分内容,必须在读入的时候检测流的状态。
116.返回局部对象的引用将导致运行时错误。
117.一般由复合复制操作符实现算术操作符。
118.前自增和前自减操作符返回的被增量或减量对象的引用,但是后缀式返回的是修改前的对象。
99. 函数不能仅仅基于不同的返回类型而实现重载。
100. // const is irrelevent for nonreference parameters
Record lookup(Phone);
Record lookup(const Phone); // redeclaration
形参与 const 形参的等价性仅适用于非引用形参。有 const 引用形参的函数与有非 const 引用形参的函数是不同的。类似地,如果函数带有指向 const 类型的指针形参,则与带有指向相同类型的非 const 对象的指针形参的函数不相同。建议
101.string init(); // the name init has global scope
void fcn()
{
int init = 0; // init is local and hides global init
string s = init(); // error: global init is hidden
}
如果局部地声明一个函数,则该函数将屏蔽而不是重载在外层作用域中声明的同名函数。由此推论,每一个版本的重载函数都应在同一个作用域中声明。
102. 在 C++ 中,名字查找发生在类型检查之前。
103.如果有且仅有一个函数满足下列条件,则匹配成功:
1. 其每个实参的匹配都不劣于其他可行函数需要的匹配。
2. 至少有一个实参的匹配优于其他可行函数提供的匹配。
104.在实际应用中,调用重载函数时应尽量避免对实参做强制类型转换:需要使用强制类型转换意味着所设计的形参集合不合理。
105.转换等级以降序排列如下:
1. 精确匹配。实参与形参类型相同。
2. 通过类型提升实现的匹配(第 5.12.2 节)。
3. 通过标准转换实现的匹配(第 5.12.3 节)。
4. 通过类类型转换实现的匹配(第 14.9 节将介绍这类转换)。
106.整数对象即使具有与枚举元素相同的值也不能用于调用期望获得枚举类型实参的函数。在使用有枚举类型形参的重载函数时,请记住:由于不同枚举类型的枚举常量值不相同,在函数重载确定过程中,不同的枚举类型会具有完全不同的行为。其枚举成员决定了它们提升的类型,而所提升的类型依赖于机器。
下面的函数调用是否合法?如果不合法,请解释原因。
enum Stat { Fail, Pass };
void test(Stat);
test(0);
该函数调用不合法。因为函数的形参为枚举类型Stat, 函数调用的实参为int类型。枚举类型对象只能用同一枚举类型的另一对象或者一个枚举成员进行初始化,因此不能将int类型的实参值传递给枚举类型的形参。
107.仅当形参是引用或指针时,形参是否为 const 才有影响。注意不能基于指针本身是否为 const 来实现函数的重载。
108. bool (*pf)(const string &, const string &);
// pf points to function returning bool that takes two const string references
bool (*pf)(const string &, const string &);
typedef bool (*cmpFcn)(const string &, const string &);
直接引用函数名等效于在函数名上应用取地址操作符:
cmpFcn pf1 = lengthCompare;
cmpFcn pf2 = &lengthCompare;
函数指针只能通过同类型的函数或函数指针或 0 值常量表达式进行初始化或赋值。将函数指针初始化为 0,表示该指针不指向任何函数。指向不同函数类型的指针之间不存在转换。向函数的指针可用于调用它所指向的函数。可以不需要使用解引用操作符,直接通过指针调用函数:
cmpFcn pf = lengthCompare; lengthCompare("hi", "bye"); // direct call pf("hi", "bye"); // equivalent call: pf1 implicitly dereferenced (*pf)("hi", "bye"); // equivalent call: pf1 explicitly dereferenced
如果指向函数的指针没有初始化,或者具有 0 值,则该指针不能在函数调用中使用。只有当指针已经初始化,或被赋值为指向某个函数,方能安全地用来调用函数。
函数的形参可以是指向函数的指针。这种形参可以用以下两种形式编写:
[/* useBiggerfunction's third parameter is a pointer to function
* that function returns a bool and takes two const string references
* two ways to specify that parameter:
*/
//third parameter is a function type and is automatically treated as a pointer to function
void useBigger(const string &, const string &, bool(const string &, const string &));
// equivalent declaration: explicitly define the parameter as a pointer to function
void useBigger(const string &, const string &, bool (*)(const string &, const string &));
函数可以返回指向函数的指针,但是,正确写出这种返回类型相当不容易:
// ff is a function taking an int and returning a function pointer // the function pointed to returns an int and takes an int* and an int int (*ff(int))(int*, int);
阅读函数指针声明的最佳方法是从声明的名字开始由里而外理解。
要理解该声明的含义,首先观察:
ff(int)
将 ff 声明为一个函数,它带有一个 int 型的形参。该函数返回
int (*)(int*, int);
它是一个指向函数的指针,所指向的函数返回 int 型并带有两个分别是 int* 型和 int 型的形参。
使用 typedef 可使该定义更简明易懂:
// PF is a pointer to a function returning an int, taking an int* and an int typedef int (*PF)(int*, int); PF ff(int); // ff returns a pointer to function
允许将形参定义为函数类型,但函数的返回类型则必须是指向函数的指针,而不能是函数。具有函数类型的形参所对应的实参将被自动转换为指向相应函数类型的指针。但是,当返回的是函数时,同样的转换操作则无法实现。
// func is a function type, not a pointer to function!
typedef int func(int*, int);
void f1(func); // ok: f1 has a parameter of function type
func f2(int); // error: f2 has a return type of function type
func *f3(int); // ok: f3 returns a pointer to function type
C++ 语言允许使用函数指针指向重载的函数,
指针的类型必须与重载函数的一个版本精确匹配(包括返回类型也必须匹配)。如果没有精确匹配的函数,则对该指针的初始化或赋值都将导致编译错误。
109.标准库类型不允许做复制或赋值操作,只有支持复制的元素类型可以存储在 vector 或其他容器类型里。由于流对象不能复制,因此不能存储在 vector(或其他)容器(即不存在存储流对象的 vector 或其他容器),形参或返回类型也不能为流类型。如果需要传递或返回 IO对象,则必须传递或返回指向该对象的指针或引用。
110.
int ival; // read cin and test only for EOF; loop is executed even if there are other IO failures while (cin >> ival, !cin.eof()) { if (cin.bad()) // input stream is corrupted; bail out throw runtime_error("IO stream corrupted"); if (cin.fail()) { // bad input cerr<< "bad data, try again"; // warn the user cin.clear(istream::failbit); // reset the stream continue; // get next input } // ok to process ival }
因此,循环条件只读入 cin 而忽略了其结果。该条件的结果是 !cin.eof() 的值。
111.
1. 程序正常结束。作为 main 返回工作的一部分,将清空所有输出缓冲区。
2. 在一些不确定的时候,缓冲区可能已经满了,在这种情况下,缓冲区将会在写下一个值之前刷新。
3. 用操纵符(第 1.2.2 节)显式地刷新缓冲区,例如行结束符 endl。
4. 在每次输出操作执行完后,用 unitbuf 操作符设置流的内部状态,从而清空缓冲区。
5. 可将输出流与输入流关联(tie)起来。在这种情况下,在读输入流时将刷新其关联的输出缓冲区。
6.如果程序崩溃了,则不会刷新缓冲区。
112.如果程序员需要重用文件流读写多个文件,必须在读另一个文件之前调用 clear 清除该流的状态。
113.stringstream 对象的一个常见用法是,需要在多种数据类型之间实现自动格式化时使用该类类型。例如,有一个数值型数据集合,要获取它们的string 表示形式,或反之。sstream 输入和输出操作可自动地把算术类型转化为相应的string 表示形式,反过来也可以。
int val1 = 512, val2 = 1024; ostringstream format_message; // ok: converts values to a string representation format_message << "val1: " << val1 << "\n" << "val2: " << val2 << "\n";
114.成员函数声明为常量,const必须同时出现在声明和定义中。
115.数据抽象是一种依赖于接口和实现分离的编程(和设计)技术。封装是一项低层次的元素组合起来的形成新的、高层次实体珠技术。函数是封装的一种形式:函数所执行的细节行为被封装在函数本身这个更大的实体中。被封装的元素隐藏了它们的实现细节——可以调用一个函数但不能访问它所执行的语句。同样地,类也是一个封装的实体:它代表若干成员的聚焦,大多数(良好设计的)类类型隐藏了实现该类型的成员。
116.如果类具有内置类型或复合类型数据成员,那么定义构造函数来初始化这些成员就是一个好主意。
117.在public之后定义的成员称为公有成员,可以由程序的所有的部分访问;在private之后定义的成员称为私有成员,只能由本类(的成员函数)访问;在protected之后定义的成员称为受保护成员,只能由本类及本类的后代类访问。
118.在一个给定的源文件中,一个类只能被定义一次。如果在多个文件中定义一个类,那么每个文件中的定义必须是完全相同的。
119.不完全类型只能以有限方式使用。不能定义该类型的对象。不完全类型只能用于定义指向该类型的指针及引用,或者用于声明(而不是定义)使用该类型作为形参类型或返回类型的函数。类不能具有自身类型的数据成员。类的数据成员可以是指向自身类型的指针或引用。
120.尽管在成员函数内部显式引用 this 通常是不必要的,但有一种情况下必须这样做:当我们需要将一个对象作为整体引用而不是引用对象的一个成员时。最常见的情况是在这样的函数中使用this:该函数返回对调用该函数的对象的引用。
121.在普通的非 const 成员函数中,this 的类型是一个指向类类型的 const 指针。可以改变 this 所指向的值,但不能改变 this 所保存的地址。在 const 成员函数中,this 的类型是一个指向const 类类型对象的const 指针。既不能改变this 所指向的对象,也不能改变this 所保存的地址。不能从const 成员函数返回指向类对象的普通引用。const 成员函数只能返回*this 作为一个const 引用。
122.基于成员函数是否为 const,可以重载一个成员函数。const 对象只能使用 const 成员。非const 对象可以使用任一成员,但非const 版本是一个更好的匹配。
123.可变数据成员永远都不能为 const,甚至当它是 const 对象的成员时也如此。因此,const 成员函数可以改变mutable 成员。
124. 类定义实际上是在两个阶段中处理:首先,编译成员声明;只有在所有成员出现之后,才编译它们的定义本身。
125.一旦一个名字被用作类型名,该名字就不能被重复定义。
126. const 构造函数是不必要的。构造函数初始化式只在构造函数的定义中而不是声明中指定。
127.构造函数初始化列表难以理解的一个原因在于,省略初始化列表在构造函数的函数体内对数据成员赋值是合法的。例如,可以将接受一个string 的Sales_item 构造函数编写为:
// legal but sloppier way to write the constructor: // no constructor initializer Sales_item::Sales_item(const string &book) { isbn = book; units_sold = 0; revenue = 0.0; }
这个构造函数给类 Sales_item 的成员赋值,但没有进行显式初始化。不管是否有显式的初始化式,在执行构造函数之前,要初始化isbn 成员。这个构造函数隐式使用默认的string 构造函数来初始化isbn。执行构造函数的函数体时,isbn 成员已经有值了。该值被构造函数函数体中的赋值所覆盖。不管成员是否在构造函数初始化列表中显式初始化,类类型的数据成员总是在初始化阶段初始化。初始化发生在计算阶段开始之前。在构造函数初始化列表中没有显式提及的每个成员,使用与初始化变量相同的规则来进行初始化。运行该类型的默认构造函数,来初始化类类型的数据成员。内置或复合类型的成员的初始值依赖于对象的作用域:在局部作用域中这些成员不被初始化,而在全局作用域中它们被初始化为 0。有些成员必须在构造函数初始化列表中进行初始化。对于这样的成员,在构造函数函数体中对它们赋值不起作用。没有默认构造函数的类类型的成员,以及 const 或引用类型的成员,不管是哪种类型,都必须在构造函数初始化列表中进行初始化。可以初始化 const 对象或引用类型的对象,但不能对它们赋值。初始化const 或引用类型数据成员的唯一机会是构造函数初始化列表中。必须对任何 const 或引用类型成员以及没有默认构造函数的类类型的任何成员使用初始化式。按照与成员声一致的次序编写构造函数初始化列表是个好主意。此外,尽可能避免使用成员来初始化其他成员。
128.习题12.26
129.只有当一个类没有定义构造函数时,编译器才会自动生成一个默认构造函数。如果类包含内置或复合类型的成员,则该类不应该依赖于合成的默认构造函数。它应该定义自己的构造函数来初始化这些成员。
130.。NoDefault 没有默认构造函数,意味着:
1. 具有 NoDefault 成员的每个类的每个构造函数,必须通过传递一个初始的 string 值给 NoDefault 构造函数来显式地初始化 NoDefault 成员。
2. 编译器将不会为具有 NoDefault 类型成员的类合成默认构造函数。如果这样的类希望提供默认构造函数,就必须显式地定义,并且默认构造函数必须显式地初始化其 NoDefault 成员。
3. NoDefault 类型不能用作动态分配数组的元素类型。
4. NoDefault 类型的静态分配数组必须为每个元素提供一个显式的初始化式。
5. 如果有一个保存 NoDefault 对象的容器,例如 vector,就不能使用接受容器大小而没有同时提供一个元素初始化式的构造函数。
实际上,如果定义了其他构造函数,则提供一个默认构造函数几乎总是对的。通常,在默认构造函数中给成员提供的初始值应该指出该对象是“空”的。
131.可以用单个实参来调用的构造函数定义了从形参类型到该类类型的一个隐式转换。
132.实际上,我们构造了一个在测试完成后被丢弃的对象。这个行为几乎肯定是一个错误。可以通过将构造函数声明为 explicit,来防止在需要隐式转换的上下文中使用构造函数。explicit 关键字只能用于类内部的构造函数声明上。显式使用构造函数只是中止了隐式地使用构造函数。任何构造函数都可以用来显式地创建临时对象。通常,除非有明显的理由想要定义隐式转换,否则,单形参构造函数应该为 explicit。将构造函数设置为explicit 可以避免错误,并且当转换有用时,用户可以显式地构造对象。
133.尽管大多数对象可以通过运行适当的构造函数进行初始化,但是直接初始化简单的非抽象类的数据成员仍是可能的。对于没有定义构造函数并且其全体数据成员均为 public 的类,可以采用与初始化数组元素相同的方式初始化其成员,这种方式不好。
134.友元不是授予友元关系的那个类的成员,所以它们不受声明出现部分的访问控制影响。friend Window_Mgr& Window_Mgr::relocate(Window_Mgr::index,Window_Mgr::index);friend class Window_Mgr;
135.友元声明将已命名的类或非成员函数引入到外围作用域中。此外,友元函数可以在类的内部定义,该函数的作用域扩展到包围该类定义的作用域。用友元引入的类名和函数(定义或声明),可以像预先声明的一样使用。
用友元引入的类名和函数(定义或声明),可以像预先声明的一样使用:
class X { friend class Y; friend void f() { /* ok to define friend function in the class body */ } }; class Z { Y *ymem; // ok: declaration for class Y introduced by friend in X void g() { return ::f(); } // ok: declaration of f introduced by X };
136.每个 static 数据成员是与类关联的对象,并不与该类的对象相关联。static 成员函数没有 this 形参,它可以直接访问所属类的 static 成员,但不能直接使用非 static 成员。可以通过作用域操作符从类直接调用 static 成员,或者通过对象、引用或指向该类类型对象的指针间接调用。因为 static 成员不是任何对象的组成部分,所以 static 成员函数不能被声明为 const。
137.static 数据成员必须在类定义体的外部定义(正好一次)。static 成员不是通过类构造函数进行初始化,而是应该在定义时进行初始化。保证对象正好定义一次的最好办法,就是将 static 数据成员的定义放在包含类非内联成员函数定义的文件中。 像使用任意的类成员一样,在类定义体外部引用类的 static成员时,必须指定成员是在哪个类中定义的。然而,static 关键字只能用于类定义体内部的声明中,定义不能标示为static。,只要初始化式是一个常量表达式,整型 const static 数据成员就可以在类的定义体中进行初始化。const static 数据成员在类的定义体中初始化时,该数据成员仍必须在类的定义体之外进行定义。在类内部提供初始化式时,成员的定义不必再指定初始值。因static 数据成员不是任何对象的组成部分,所以它们的使用方式对于非static 数据成员而言是不合法的。
例如,static 数据成员的类型可以是该成员所属的类类型。非 static 成员被限定声明为其自身类对象的指针或引用:
class Bar { public: // ... private: static Bar mem1; // ok Bar *mem2; // ok Bar mem3; // error };
类似地,static 数据成员可用作默认实参,非 static 数据成员不能用作默认实参,因为它的值不能独立于所属的对象而使用。
138.不管类是否定义了自己的析构函数,编译器都自动执行类中非static 数据成员的析构函数。
139.。有一种特别常见的情况需要类定义自己的复制控制成员当类具有指针成员或者另一些类在创建新对象时必须做一些特定工作。为了防止复制,类必须显式声明其复制构造函数为 private。然而,类的友元和成员仍可以进行复制。如果想要连友元和成员中的复制也禁止,就可以声明一个(private)复制构造函数但不对其定义。声明而不定义成员函数是合法的,但是,使用未定义成员的任何尝试将导致链接失败。通过声明(但不定义)private 复制构造函数,可以禁止任何复制类类型对象的尝试:用户代码中复制尝试将在编译时标记为错误,而成员函数和友元中的复制尝试将在链接时导致错误。如果定义了复制构造函数,也必须定义默认构造函数。不定义复制构造函数和/或默认构造函数,会严重局限类的使用。不允许复制的类对象只能作为引用传递给函数或从函数返回,它们也不能用作容器的元素。
140. 只有单个形参,而且该形参是对本类类型对象的引用(常用 const 修饰),这样的构造函数称为复制构造函数,一般不用explicit修饰。与默认构造函数一样,复制构造函数可由编译器隐式调用。复制构造函数可用于:
• 根据另一个同类型的对象显式或隐式初始化一个对象。
• 复制一个对象,将它作为实参传给一个函数。
• 从函数返回时复制一个对象。
• 初始化顺序容器中的元素。
• 根据元素初始化式列表初始化数组元素。
141.复制初始化使用 = 符号,而直接初始化将初始化式放在圆括号中。直接初始化直接调用与实参匹配的构造函数,复制初始化总是调用复制构造函数。复制初始化首先使用指定构造函数创建一个临时对象然后用复制构造函数将那个临时对象复制到正在创建的对象。
142.通常直接初始化和复制初始化仅在低级别上存在差异。然而,对于不支持复制的类型,或者使用非 explicit 构造函数的时候,它们有本质区别:
ifstream file1("filename"); // ok: direct initialization ifstream file2 = "filename"; // error: copy constructor is private // This initialization is okay only if // the Sales_item(const string&) constructor is not explicit Sales_item item = string("9-999-99999-9");
item 的初始化是否正确,取决于正在使用哪个版本的 Sales_item 类。某些版本将参数为一个string 的构造函数定义为explicit。如果构造函数是显式的,则初始化失败;如果构造函数不是显式的,则初始化成功。
143.const 引用,不能复制。
144.复制构造函数可用于初始化顺序容器中的元素。例如,可以用表示容量的单个形参来初始化容器。容器的这种构造方式使用默认构造函数和复制构造函数:
// default string constructor and five string copy constructors invoked
vector<string> svec(5);
编译器首先使用 string 默认构造函数创建一个临时值来初始化 svec,然后使用复制构造函数将临时值复制到svec 的每个元素。
145.如果没有为类类型数组提供元素初始化式,则将用默认构造函数初始化每个元素。然而,如果使用常规的花括号括住的数组初始化列表来提供显式元素初始化式,则使用复制初始化来初始化每个元素。根据指定值创建适当类型的元素,然后用复制构造函数将该值复制到相应元素。
146.合成复制构造函数直接复制内置类型成员的值,类类型成员使用该类的复制构造函数进行复制。数组成员的复制是个例外。虽然一般不能复制数组,但如果一个类具有数组成员,则合成复制构造函数将复制数组。复制数组时合成复制构造函数将复制数组的每一个元素。合成操作依次取得每个成员,根据成员类型进行成员的复制、赋值或撤销。如果成员为类类型的,合成操作调用该类的相应操作(即,复制构造函数调用成员的复制构造函数,析构函数调用成员的析构函数,等等)。如果成员为内置类型或指针,则直接复制或赋值,析构函数对撤销内置类型或指针类型的成员没有影响。如果成员为数组,则根据元素类型以适当方式复制、赋值或撤销数组中的元素。
147.动态分配的对象只有在指向该对象的指针被删除时才撤销。如果没有删除指向动态对象的指针,则不会运行该对象的析构函数,对象就一直存在,从而导致内存泄漏,而且,对象内部使用的任何资源也不会释放。当对象的引用或指针超出作用域时,不会运行析构函数。只有删除指向动态分配对象的指针或实际对象(而不是对象的引用)超出作用域时,才会运行析构函数。
148.析构函数并不仅限于用来释放资源。一般而言,析构函数可以执行任意操作,该操作是类设计者希望在该类对象的使用完毕之后执行的。合成析构函数按对象创建时的逆序撤销每个非 static 成员。对于类类型的每个成员,合成析构函数调用该成员的析构函数来撤销对象。撤销内置类型成员或复合类型的成员没什么影响。尤其是,合成析构函数并不删除指针成员所指向的对象。因为不能指定任何形参,所以不能重载析构函数。虽然可以为一个类定义多个构造函数,但只能提供一个析构函数,应用于类的所有对象。析构函数与复制构造函数或赋值操作符之间的一个重要区别是,即使我们编写了自己的析构函数,合成析构函数仍然运行。合成析构函数在自定义析构函数之后运行。
149.习题13.14
150.下面的代码段中发生了多少次析构函数的调用?
void fcn(const Sales_item *trans, Sales_item accum) { Sales_item item1(*trans), item2(accum); if (!item1.same_isbn(item2)) return; if (item1.avg_price() <= 99) return; else if (item2.avg_price() <= 99) return; // ... }
3次,分别用于函数fcn返回时撤销形参对象accum, 局部对象item1和iterm2。
151.编写自己的复制构造函数时,必须显式复制需要复制的任意成员。显式定义的复制构造函数不会进行任何自动复制。像其他任何构造函数一样,如果没有初始化某个类成员,则那个成员用该成员的默认构造函数初始化。复制构造函数中的默认初始化不会使用成员的复制构造函数。
152.包含指针的类需要特别注意复制控制,原因是复制指针时只复制指针中的地址,而不会复制指针指向的对象。
153.
复制对象后:
/* smart pointer class: takes ownership of the dynamically allocated * object to which it is bound * User code must dynamically allocate an object to initialize a HasPtr * and must not delete that object; the HasPtr class will delete it */ class HasPtr { public: // HasPtr owns the pointer; pmust have been dynamically allocated HasPtr(int *p, int i): ptr(new U_Ptr(p)), val(i) { } // copy members and increment the use count HasPtr(const HasPtr &orig): ptr(orig.ptr), val(orig.val) { ++ptr->use; } HasPtr& operator=(const HasPtr&); // if use count goes to zero, delete the U_Ptr object ~HasPtr() { if (--ptr->use == 0) delete ptr; } private: U_Ptr *ptr; // points to use-counted U_Ptr class int val; };
接受一个指针和一个 int 值的 HasPtr 构造函数使用其指针形参创建一个新的 U_Ptr 对象。HasPtr 构造函数执行完毕后,HasPtr 对象指向一个新分配的 U_Ptr 对象,该U_Ptr 对象存储给定指针。新U_Ptr 中的使用计数为 1,表示只有一个HasPtr 对象指向它。
复制构造函数从形参复制成员并增加使用计数的值。复制构造函数执行完毕后,新创建对象与原有对象指向同一 U_Ptr 对象,该 U_Ptr 对象的使用计数加 1。
析构函数将检查 U_Ptr 基础对象的使用计数。如果使用计数为 0,则这是最后一个指向该 U_Ptr 对象的 HasPtr 对象,在这种情况下,HasPtr 析构函数删除其U_Ptr 指针。删除该指针将引起对U_Ptr 析构函数的调用,U_Ptr 析构函数删除int 基础对象。
赋值操作符比复制构造函数复杂一点:
HasPtr& HasPtr::operator=(const HasPtr &rhs) { ++rhs.ptr->use; // increment use count on rhs first if (--ptr->use == 0) delete ptr; // if use count goes to 0 on this object, delete it ptr = rhs.ptr; // copy the U_Ptr object val = rhs.val; // copy the int member return *this; }
在这里,首先将右操作数中的使用计数加 1,然后将左操作数对象的使用计数减 1 并检查这个使用计数。像析构函数中那样,如果这是指向U_Ptr 对象的最后一个对象,就删除该对象,这会依次撤销int 基础对象。将左操作数中的当前值减 1(可能撤销该对象)之后,再将指针从rhs 复制到这个对象。赋值照常返回对这个对象的引用。这个赋值操作符在减少左操作数的使用计数之前使rhs 的使用计数加 1,从而防止自身赋值。
154. 使用计数是复制控制成员中使用的编程技术。将一个计数器与类指向的对象相关联,用于跟踪该类有多少个对象共享同一指针。创建一个单独类指向共享对象并管理使用计数。由构造函数设置共享对象的状态并将使用计数置为1.每当由复制构造函数或赋值操作符生成一个新副本时,使用计数加1,。由析构函数撤销对象或作为赋值操作符的左操作数撤销对象时,使用计数减1.赋值操作符和析构函数检查使用计数是否已减至0,如果是,则撤销对象。
155.什么是智能指针?智能指针类如何与实现普通指针行为的类相区别?
智能指针是一个行为类似指针但也提供其他功能的类。智能指针类与实现普通指针行为的类的区别在于:智能指针通常接受指向动态分配对象的指针并负责删除该对象。用户分配对象,但由智能指针类删除它,因此智能指针类需要实现复制控制成员来管理指向共享对象的指针。只有在撤销了指向共享对象的最后一个智能指针后,才能删除该共享对象。
156.要使指针成员表现得像一个值,复制 HasPtr 对象时必须复制指针所指向的对象:
/* * Valuelike behavior even though HasPtr has a pointer member: * Each time we copy a HasPtr object, we make a new copy of the * underlying int object to which ptr points. */ class HasPtr { public: // no point to passing a pointer if we're going to copy it anyway // store pointer to a copy of the object we're given HasPtr(const int &p, int i): ptr(new int(p)), val(i) {} // copy members and increment the use count HasPtr(const HasPtr &orig): ptr(new int (*orig.ptr)), val(orig.val) { } HasPtr& operator=(const HasPtr&); ~HasPtr() { delete ptr; } // accessors must change to fetch value from Ptr object int get_ptr_val() const { return *ptr; } int get_int() const { return val; } // change the appropriate data member void set_ptr(int *p) { ptr = p; } void set_int(int i) { val = i; } // return or change the value pointed to, so ok for const objects int *get_ptr() const { return ptr; } void set_ptr_val(int p) const { *ptr = p; } private: int *ptr; // points to an int int val; };
赋值操作符不需要分配新对象,它只是必须记得给其指针所指向的对象赋新值,而不是给指针本身赋值:
HasPtr& HasPtr::operator=(const HasPtr &rhs) { // Note: Every HasPtr is guaranteed to point at an actual int; // We know that ptr cannot be a zero pointer *ptr = *rhs.ptr; // copy the value pointed to val = rhs.val; // copy the int return *this; }
157.什么是值型类?
所谓值型类,是指具有值语义的类,其特征为:对该类对象进行复制时,会得到一个不同的新副本,对副本所做的改变不会影响原有对象。
158.重载操作符是具有特殊名称的函数,用于内置类型的操作符,其含义不能改变。例如,内置的整型加号操作符不能重定义:
// error: cannot redefine built-in operator for ints
int operator+(int, int);
也不能为任何内置类型定义额外的新的操作符。例如不能定义接受两个数组类型操作符的operator+。重载操作符必须具有至少一个类类型或枚举类型的操作数。这条规则强制重载操作符不能重新定义用于内置类型对象的操作符的含义。作符的优先级、结合性或操作数目不能改变。除了函数调用操作符operator() 之外,重载操作符时使用默认实参是非法的。重载操作符并不保证操作数的求值顺序,在&& 和|| 的重载版本中,两个操作数都要进行求值,而且对操作数的求值顺序不做规定。因此,重载&&、|| 或逗号操作符以及取地址符不是一种好的做法。也可以像调用普通函数一样调用重载操作符函数,指定函数并传递适当类型适当数目的形参:
// equivalent direct call to nonmember operator function
cout << operator+(item1, item2) << endl;
item1 += item2; // expression based "call" item1.operator+=(item2); // equivalent call to member operator function
159.下面是一些指导原则,有助于决定将操作符设置为类成员还是普通非成员函数:
• 赋值(=)、下标([])、调用(())和成员访问箭头(->)等操作符必须定义为成员,将这些操作符定义为非成员函数将在编译时标记为错误。
• 像赋值一样,复合赋值操作符通常应定义为类的成员,与赋值不同的是,不一定非得这样做,如果定义非成员复合赋值操作符,不会出现编译错误。
• 改变对象状态或与给定类型紧密联系的其他一些操作符,如自增、自减和解引用,通常就定义为类成员。
• 对称的操作符,如算术操作符、相等操作符、关系操作符和位操作符,最好定义为普通非成员函数。
160.重载输出操作符一般的简单定义如下:
// general skeleton of the overloaded output operator ostream& operator <<(ostream& os, const ClassType &object) { // any special logic to prepare object // actual output of members os << // ... // return ostream object return os; }
我们不能将该操作符(输出操作符同样)定义为类的成员,否则,左操作数将只能是该类类型的对象:
// if operator<< is a member of Sales_item
Sales_item item;
item << cout;
161.设计输入操作符时,如果可能,要确定错误恢复措施,这很重要。
162.为了与内置操作符保持一致,加法返回一个右值,而不是一个引用。算术操作符通常产生一个新值,该值是两个操作数的计算结果,它不同于任一操作数且在一个局部变量中计算,返回对那个变量的引用是一个运行时错误。既定义了算术操作符又定义了相关复合赋值操作符的类,一般应使用复合赋值实现算术操作符。
163. 赋值操作符可以重载。无论形参为何种类型,赋值操作符必须定义为成员函数,这一点与复合赋值操作符有所不同。一般而言,赋值操作符与复合赋值操作符应返回操作符的引用。
164.类定义下标操作符时,一般需要定义两个版本:一个为非 const 成员并返回引用,另一个为 const 成员并返回 const 引用。
165.箭头操作符与众不同。它可能表现得像二元操作符一样:接受一个对象和一个成员名。对对象解引用以获取成员。不管外表如何,箭头操作符不接受显式形参。这里没有第二个形参,因为-> 的右操作数不是表达式,相反,是对应着类成员的一个标识符。没有明显可行的途径将一个标识符作为形参传递给函数,相反,由编译器处理获取成员的工作。当这样编写时:
point->action();
由于优先级规则,它实际等价于编写:
(point->action)();
换句话说,我们想要调用的是对 point->action 求值的结果。编译器这样对该代码进行求值:
-
如果 point 是一个指针,指向具有名为 action 的成员的类对象,则编译器将代码编译为调用该对象的action 成员。
-
否则,如果 point是定义了 operator-> 操作符的类的一个对象,则 point->action 与 point.operator->()->action 相同。即,执行point 的operator->(),然后使用该结果重复这三步。
-
否则,代码出错。
166. 重载箭头操作符必须返回指向类类型的指针,或者返回定义了自己的箭头操作符的类类型对象。如果返回类型是指针,则内置箭头操作符可用于该指针,编译器对该指针解引用并从结果对象获取指定成员。如果被指向的类型没有定义那个成员,则编译器产生一个错误。如果返回类型是类类型的其他对象(或是这种对象的引用),则将递归应用该操作符。编译器检查返回对象所属类型是否具有成员箭头,如果有,就应用那个操作符;否则,编译器产生一个错误。这个过程继续下去,直到返回一个指向带有指定成员的的对象的指针,或者返回某些其他值,在后一种情况下,代码出错。(递归的)。需要解引用操作符的const 和非 const 版本。
167.后缀式操作符函数接受一个额外的(即,无用的)int 型形参。为了与内置类型一致,前缀式操作符应返回被增量或减量对象的引用。了与内置操作符一致,后缀式操作符应返回旧值(即,尚未自增或自减的值),并且,应作为值返回,而不是返回引用。const对象不能自增自减。
// postfix: increment/decrement object but return unchanged value CheckedPtr CheckedPtr::operator++(int) { // no check needed here, the call to prefix increment will do the check CheckedPtr ret(*this); // save current value ++*this; // advance one element, checking the increment return ret; // return saved state } CheckedPtr CheckedPtr::operator--(int) { // no check needed here, the call to prefix decrement will do the check CheckedPtr ret(*this); // save current value --*this; // move backward one element and check return ret; // return saved state }
168.可以显式调用重载操作符而不是将它作为操作符用在表达式中。如果想要使用函数调用来调用后缀式操作符,必须给出一个整型实参值:
CheckedPtr parr(ia, ia + size); // iapoints to an array of ints parr.operator++(0); // call postfix operator++ parr.operator++(); // call prefix operator++
169.习题14.24
170.函数调用操作符必须声明为成员函数。一个类可以定义函数调用操作符的多个版本,由形参的数目或类型加以区别。定义了调用操作符的类,其对象常称为函数对象,即它们是行为类似函数的对象。函数对象可以比函数更灵活。
171.标准库提供了一组函数适配器,用于特化和扩展一元和二元函数对象。函数适配器分为如下两类:绑定器(bind1st, bind2nd),求反器(not1, not2)
172.习题14.37
173.转换操作符在类定义体内声明,在保留字 operator 之后跟着转换的目标类型。一般而言,不允许转换为数组或函数类型,转换为指针类型(数据和函数指针)以及引用类
型是可以的。转换函数必须是成员函数,不能指定返回类型,并且形参表必须为空。虽然转换函数不能指定返回类型,但是每个转换函数必须显式返回一个指定
类型的值。转换函数一般不应该改变被转换的对象。因此,转换操作符通常应定义为 const 成员。
174.
只要存在转换,编译器将在可以使用内置转换的地方自动调用。只能有一次类型转换,但是后面可以跟多次标准转换。标准转换优于类类型转换。
175.使用转换函数时,被转换的类型不必与所需要的类型完全匹配。必要时可在类类型转换之后跟上标准转换以获得想要的类型。只能应用一个类类型转换。使用构造函数执行隐式转换的时候,构造函数的形参类型不必与所提供的类型完全匹配,在类型转换前可以有标准转换。
176.习题14.41。
177.一般而言,给出一个类与两个内置类型之间的转换是不好的做法。如果两个转换操作符都可用在一个调用中,而且在转换函数之后存在标准转换则根据该标准转换的类别选择最佳匹配。当两个类定义了相互转换时,很可能存在二义性。
class Integral; class SmallInt { public: SmallInt(Integral); // convert from Integral to SmallInt // ... }; class Integral { public: operator SmallInt() const; // convert from SmallInt to Integral // ... }; void compute(SmallInt); Integral int_val; compute(int_val); // error: ambiguous
实参 int_val 可以用两种不同方式转换为 SmallInt 对象,编译器可以使用接受Integral 对象的构造函数,也可以使用将Integral 对象转换为SmallInt 对象的Integral 转换操作。因为这两个函数没有高下之分,所以这个调用会出错。在这种情况下,不能用显式类型转换来解决二义性——显式类型转换本身既可以使用转换操作又可以使用构造函数,相反,需要显式调用转换操作符或构造函数:
compute(int_val.operator SmallInt()); // ok: use conversion operator compute(SmallInt(int_val)); // ok: use SmallInt constructor
而且,由于某些似乎微不足道的原因,我们认为可能有二义性的转换是合法的。例如,SmallInt 类构造函数复制它的Integral 实参,如果改变构造函数以接受 const Integral 引用:
class SmallInt { public: SmallInt(const Integral&); };
则对 compute(int_val) 的调用不再有二义性!原因在于使用 SmallInt 构造函数需要将一个引用绑定到int_val,而使用Integral 类的转换操作符可以避免这个额外的步骤。这一小小区别足以使我们倾向于使用转换操作符。避免二义性最好的方法是避免编写互相提供隐式转换的成对的类。保证最多只有一种途径将一个类型转换为另
一类型。做到这点,最好的办法是限制转换操作符的数目,尤其是,到一种内置类型应该只有一个转换。编译器将不会试图区别两个不同的类类型转换。具体而言,即使一个调用需要在类类型转换之后跟一个标准转换,而另一个是完全匹配,编译器仍会将该调用标记为错误。面对二义性转换,程序员可以使用强制转换来显式指定应用哪个转换操作。在调用重载函数时,需要使用构造函数或强制类型转换来转换实参,这是设计拙劣的表现。
正确设计类的重载操作符、转换构造函数和转换函数需要多加小心。尤其是,如果类既定义了转换操作符又定义了重载操作符,容易产生二义性。下面几条经验规则会有所帮助:
-
不要定义相互转换的类,即如果类 Foo 具有接受类Bar 的对象的构造函数,不要再为类Bar 定义到类型Foo 的转换操作符。
-
避免到内置算术类型的转换。具体而言,如果定义了到算术类型的转换,则
-
不要定义接受算术类型的操作符的重载版本。如果用户需要使用这些操作符,转换操作符将转换你所定义的类型的对象,然后可以使用内置操作符。
-
不要定义转换到一个以上算术类型的转换。让标准转换提供到其他算术类型的转换。
-
178.重载操作符就是重载函数。使用与确定重载函数调用一样的过程来确定将哪个操作符(内置的还是类类型的)应用于给定表达式。给定如下代码:
ClassX sc; int iobj = sc + 3;
有四种可能性:
- 有一个重载的加操作符与 ClassX 和 int 相匹配。
- 存在转换,将 sc 和/或 int 值转换为定义了 + 的类型。如果是这样,该表达式将先使用转换,接着应用适当的加操作符。
- 因为既定义了转换操作符又定义了 + 的重载版本,该表达式具有二义性。
- 因为既没有转换又没有重载的 + 可以使用,该表达式非法。
179.确定指定函数的调用时,与操作符的使用相反,由调用本身确定所考虑的名字的作用域。如果是通过类类型的对象(或通过这种对象的引用或指针)的调用,则只需考虑该类的成员函数。具有同一名字的成员函数和非成员函数不会相互重载。使用重载操作符是时,调用本身不会告诉我们与使用的操作符函数作用域相关的任何事情,因此,成员和非成员版本都必须考虑。
180.一般而言,函数调用的候选集只包括成员函数或非成员函数,不会两者都包括。而确定操作符的使用时,操作符的非成员和成员版本可能都是候选者。
180.既为算术类型提供转换函数,又为同一类类型提供重载操作符,可能会导致重载操作符和内置操作符之间的二义性。
181.在 C++ 中,多态性仅用于通过继承而相关联的类型的引用或指针。
182.在 C++ 中,基类必须指出希望派生类重写哪些函数,定义为 virtual 的函数是基类期待派生类重新定义的,基类希望派生类继承的函数不能定义为虚函数。
183.除了构造函数之外,任意非 static 成员函数都可以是虚函数。virtual保留字只在类内部的成员函数声明中出现,不能用在类定义体外部出现的函数定义上。
184.。protected 成员可以被派生类对象访问但不能被该类型的普通用户访问。
185.class Derived : public Base;//wrong
如果需要声明(但并不实现)一个派生类,则声明包含类名但不包含派生列表。例如,下面的前向声明会导致编译时错误:
// error: a forward declaration must not include the derivation list
class Bulk_item : public Item_base;
正确的前向声明为:
// forward declarations of both derived and nonderived class
class Bulk_item;
class Item_base;
186.派生类只能通过派生类对象访问其基类的 protected 成员,派生类对其基类类型对象的 protected 成员没有特殊访问权限。
例如,假定 Bulk_item 定义了一个成员函数,接受一个 Bulk_item 对象的引用和一个 Item_base 对象的引用,该函数可以访问自己对象的protected 成员以及Bulk_item 形参的protected 成员,但是,它不能访问Item_base 形参的protected 成员。
void Bulk_item::memfcn(const Bulk_item &d, const Item_base &b) { // attempt to use protected member double ret = price; // ok: uses this->price ret = d.price; // ok: uses price from a Bulk_item object ret = b.price; // error: no access to price from an Item_base }
d.price 的使用正确,因为是通过 Bulk_item 类型对象引用 price;b.price 的使用非法,因为对Base_item 类型的对象没有特殊访问访问权限(可能指的b对Bulk_item的price没有访问权限)。
188.派生类必须对想要重定义的每个继承成员进行声明。派生类中虚函数的声明必须与基类中的定义方式完全匹配,但有一个例外:返回对基类型的引用(或指针)的虚函数。派生类中的虚函数可以返回基类函数所返回类型的派生类的引用(或指针)。一旦函数声明为虚函数,它就一直为虚函数。派生类重定义虚函数可以使用virtual关键字也可以不使用。
189.C++语言不要求编译器将对象的基类部分和派生部分连续排列。
188.已定义的类才可以用作基类。这一规则暗示着不可能从类自身派生出一个类。如果需要声明但是并不实现一个派生类,不能使用派生列表,否则将导致编译错误。
189.因为每个派生类对象都包含基类部分,所以可将基类类型的引用绑定到派生类对象的基类部分,也可以用指向基类的指针指向派生类对象。基类类型引用和指针的关键点在于静态类型(在编译时可知的引用类型或指针类型)和动态类型(指针或引用所绑定的对象的类型这是仅在运行时可知的)可能不同。将基类类型的引用或指针绑定到派生类对象对基类对象没有影响,对象本身不会改变,仍为派生类对象。对象的实际类型可能不同于该对象引用或指针的静态类型,这是 C++ 中动态绑定的关键。。对象的动态类型
总是与静态类型相同,这一点与引用或指针相反。运行的函数(虚函数或非虚函数)是由对象的类型定义的。
190.注意注释
// calculate and print price for given number of copies, applying any discounts void print_total(ostream &os, const Item_base &item, size_t n) { os << "ISBN: " << item.book() // calls Item_base::book << "\tnumber sold: " << n << "\ttotal price: " // virtual call: which version of net_price to call is resolved at run time << item.net_price(n) << endl; }
190.只有成员函数中的代码才应该使用作用域操作符覆盖虚函数机制。派生类虚函数调用基类版本时,必须显式使用作用域操作符。如果派生类函数忽略了这样做,则函数调用会在运行时确定并且将是一个自身调用,从而导致无穷递归。
Item_base *baseP = &derived; // calls version from the base class regardless of the dynamic type of baseP double d = baseP->Item_base::net_price(42);
191.如果一个调用省略了具有默认值的实参,则所用的值由调用该函数的类型定义,与对象的动态类型无关。通过基类的引用或指针调用虚函数时,默认实参为在基类虚函数声明中指定的值,如果通过派生类的指针或引用调用虚函数,则默认实参是在派生类的版本中声明的值。在同一虚函数的基类版本和派生类版本中使用不同的默认实参几乎一定会
引起麻烦。如果通过基类的引用或指针调用虚函数,但实际执行的是派生类中定义的版本,这时就可能会出现问题。在这种情况下,为虚函数的基类版本定义的默认实参将传给派生类定义的版本,而派生类版本是用不同的默认实参定义的(就是这么规定的)。
192.使用 private 或 protected 派生的类不继承基类的接口,相反,这些派生通常被称为实现继承。
193.派生类可以恢复继承成员的访问级别,但不能使访问级别比基类中原来指定的更严格或更宽松。using 声明访问基类中的名字(恢复基类中的访问控制)
194.如果基类定义 static 成员,则整个继承层次中只有一个这样的成员。无论从基类派生出多少个派生类,每个 static 成员只有一个实例。static 成员遵循常规访问控制:如果成员在基类中为private,则派生类不能访问它。假定可以访问成员,则既可以通过基类访问static 成员,也可以通过派生类访问static 成员。一般而言,既可以使用作用域操作符也可以使用点或箭头成员访问操作符。
struct Base { static void statmem(); // public by default }; struct Derived : Base { void f(const Derived&); }; void Derived::f(const Derived &derived_obj) { Base::statmem(); // ok: Base defines statmem Derived::statmem(); // ok: Derived in herits statmem // ok: derived objects can be used to access static from base derived_obj.statmem(); // accessed through Derived object statmem(); // accessed through this class
195.习题15.13
196.存在从派生类型引用到基类类型引用的自动转换(指针同样)。一个基类对象可能是也可能不是一个派生类对象的部分,结果,没有从基类引用(或基类指针)到派生类引用(或派生类指针)的(自动)转换。虽然一般可以使用派生类型的对象对基类类型的对象进行初始化或赋值,但,没有从派生类型对象到基类类型对象的直接转换。一个是派生类对象转换为基类类型引用,一个是用派生类对象对基类对象进行初始化或赋值,理解它们之间的区别很重要。
197.对基类对象进行初始化或赋值,实际上是在调用函数:初始化时调用构造函数,赋值时调用赋值操作符。用派生类对象对基类对象进行初始化或赋值时,有两种可能性。第一种(虽然不太可能的)可能性是,基类可能显式定义了将派生类型对象复制或赋值给基类对象的含义,这可以通过定义适当的构造函数或赋值操作符实现基类一般(显式或隐式地)定义自己的复制构造函数和赋值操作符),这些成员接受一个形参,该形参是基类类型(const)引用。因为存在从派生类引用到基类引用的转换,这些复制控制成员可用于从派生类对象对基类对象进行初始化或赋值。
用。
198.派生类要确定到基类的转换是否可访问,可以考虑基类的 public 成员是否访问,如果可以,转换是可访问的,否则,转换是不可访问的。
198.编译器在编译时无法知道特定转换在运行时实际上是安全的。编译器确定转换是否合法,只看指针或引用的静态类型,所以即使一个基类指针指向的是派生类对象,仍然不能赋给派生类指针。在这些情况下,如果知道从基类到派生类的转换是安全的,就可以使用static_cast强制编译器进行转换。或者,可以用dynamic_cast 申请在运行时进行检查。
199.构造函数和复制控制成员不能继承,每个类定义自己的构造函数和复制控制成员。像任何类一样,如果类不定义自己的默认构造函数和复制控制成员,就将使用合成版本。像任意其他成员一样,构造函数可以为protected 或private,某些类需要只希望派生类使用的特殊构造函数,这样的构造函数应定义为protected。(感觉与前一句话有点矛盾)。
200.派生类构造函数的初始化列表只能初始化派生类的成员,不能直接初始化继承成员。相反派生类构造函数通过将基类包含在构造函数初始化列表中来间接初始化继承成员。构造函数初始化列表为类的基类和成员提供初始值,它并不指定初始化的执行次序。首先初始化基类,然后根据声明次序初始化派生类的成员。一个类只能初始化自己的直接基类。构造函数只能初始化其直接基类的原因是每个类都定义了自己的接口。定义Disc_item 时,通过定义它的构造函数指定了怎样初始化 Disc_item 对象。一旦类定义了自己的接口,与该类对象的所有交互都应该通过该接口,即使对象是派生类对象的一部分也不例外。假如基类定义了构造函数,则派生类构造函数必须在初始化列表给出基类的构造函数。
202.初始化函数 Base(d) 将派生类对象 d 转换为它的基类部分的引用,并调用基类复制构造函数。如果省略基类初始化函数,如下代码:
// probably incorrect definition of the Derived copy constructor Derived(const Derived& d) /* derived member initizations */ {/* ... */ }
效果是运行 Base 的默认构造函数初始化对象的基类部分。假定 Derived 成员的初始化从d 复制对应成员,则新构造的对象将具有奇怪的配置:它的Base 部分将保存默认值,而它的Derived 成员是另一对象的副本。
202.编译器总是显式调用派生类对象基类部分的析构函数。每个析构函数只负责清除自己的成员。(由编译器自己来,无需程序猿写代码)。删除指向动态分配对象的指针时,需要运行析构函数在释放对象的内存之前清除对象。处理继承层次中的对象时,指针的静态类型可能与被删除对象的动态类型不同,可能会删除实际指向派生类对象的基类类型指针。如果删除基类指针,则需要运行基类析构函数并清除基类的成员,如果对象实际是派生类型的,则没有定义该行为。要保证运行适当的析构函数,基类中的析构函数必须为虚函数。
203.基类析构函数是三法则的一个重要例外。三法则指出,如果类需要析构函数,则类几乎也确实需要其他复制控制成员。基类几乎总是需要构造函数,从而可以将析构函数设为虚函数。如果基类为了将析构函数设为虚函数则具有空析构函数,那么,类具有析构函数并不表示也需要赋值操作符或复制构造函数。即使析构函数没有工作要做,继承层次的根类也应该定义一个虚析构函数。在复制控制成员中,只有析构函数应定义为虚函数,构造函数不能定义为虚函数。构造函数是在对象完全构造之前运行的,在构造函数运行的时候,对象的动态类型还不完整。虽然可以在基类中将成员函数operator= 定义为虚函数,但这样做并不影响派生类中使用的赋值操作符。每个类有自己的赋值操作符,派生类中的赋值操作符有一个与类本身类型相同的形参,该类型必须不同于继承层次中任意其他类的赋值操作符的形参类型。将赋值操作符设为虚函数可能会令人混淆,因为虚函数必须在基类和派生类中具有同样的形参。基类赋值操作符有一个形参是自身类类型的引用,如果该操作符为虚函数,则每个类都将得到一个虚函数成员,该成员定义了参数为一个基类对象的operator=。但是,对派生类而言,这个操作符与赋值操作符是不同的。将类的赋值操作符设为虚函数很可能会令人混淆,而且不会有什么用处。
205.习题15.20
206.构造派生类对象时首先运行基类构造函数初始化对象的基类部分。在执行基类构造函数时,对象的派生类部分是未初始化的。实际上,此时对象还不是一个派生类对象。撤销派生类对象时,首先撤销它的派生类部分,然后按照与构造顺序的逆序撤销它的基类部分。在这两种情况下,运行构造函数或析构函数的时候,对象都是不完整的。为了适应这种不完整,编译器将对象的类型视为在构造或析构期间发生了变化。在基类构造函数或析构函数中,将派生类对象当作基类类型对象对待。构造或析构期间的对象类型对虚函数的绑定有影响。如果在构造函数或析构函数中调用虚函数,则运行的是为构造函数或析构函数自身类型定义的版本。
207.对象、引用或指针的静态类型决定了对象能够完成的行为。甚至当静态类型和动态类型可能不同的时候,就像使用基类类型的引用或指针时可能会发生的,静态类型仍然决定着可以使用什么成员。
208.虽然可以直接访问基类成员,就像它是派生类成员一样,但是成员保留了它的基类成员资格(基类的成员同时也是派生类中的成员)。与基类成员同名的派生类成员将屏蔽对基类成员的直接访问。可以使用作用域操作符访问被屏蔽成员。
210.局部作用域中声明的函数不会重载全局作用域中定义的函数同样,派生类中定义的函数也不重载基类中定义的成员。
211.如果不想重定义基类某个函数的所有重载版本,而且也不想屏蔽掉基类中的该函数,可以使用using声明。
212.要获得动态绑定,必须通过基类的引用或指针调用虚成员。当我们这样做时,编译器器将在基类中查找函数。假定找到了名字,编译器就检查实参是否与形参匹配。现在可以理解虚函数为什么必须在基类和派生类中拥有同一原型了(返回类型可以不同)。如果基类成员与派生类成员接受的实参不同,就没有办法通过基类类型的引用或指针调用派生类函数。考虑如下(人为的)为集合:
class Base { public: virtual int fcn(); }; class D1 : public Base { public: // hides fcn in the base; this fcn is not virtual int fcn(int); // parameter list differs from fcn in Base // D1 inherits definition of Base::fcn() }; class D2 : public D1 { public: int fcn(int); // nonvirtual function hides D1::fcn(int) int fcn(); // redefines virtual fcn from Base };
D1 中的 fcn 版本没有重定义 Base 的虚函数 fcn,相反,它屏蔽了基类的fcn。结果D1 有两个名为fcn 的函数:类从Base 继承了一个名为fcn 的虚函数,类又定义了自己的名为fcn 的非虚成员函数,该函数接受一个int 形参。但是,从Base 继承的虚函数不能通过D1 对象(或D1 的引用或指针)调用,因为该函数被fcn(int) 的定义屏蔽了。类D2 重定义了它继承的两个函数,它重定义了Base 中定义的fcn 的原始版本并重定义了D1 中定义的非虚版本。
通过基类类型的引用或指针调用函数时,编译器将在基类中查找该函数而忽略派生类:
Base bobj; D1 d1obj; D2 d2obj; Base *bp1 = &bobj, *bp2 = &d1obj, *bp3 = &d2obj; bp1->fcn(); // ok: virtual call, will call Base::fcnat run time bp2->fcn(); // ok: virtual call, will call Base::fcnat run time bp3->fcn(); // ok: virtual call, will call D2::fcnat run time
三个指针都是基类类型的指针,因此通过在 Base 中查找 fcn 来确定这三个调用,所以这些调用是合法的。另外,因为fcn 是虚函数,所以编译器会生成代码,在运行时基于引用指针所绑定的对象的实际类型进行调用。在bp2 的情况,基本对象是D1 类的,D1 类没有重定义不接受实参的虚函数版本,通过bp2 的函数调用(在运行时)调用Base 中定义的版本。
214.派生类重定义继承的虚函数时,可以去掉是否为const成员//
213.理解 C++ 中继承层次的关键在于理解如何确定函数调用。确定函数调用遵循以下四个步骤:
1.
|
首先确定进行函数调用的对象、引用或指针的静态类型。
|
2.
|
在该类中查找函数,如果找不到,就在直接基类中查找,如此循着类的继承链往上找,直到找到该函数或者查找完最后一个类。如果不能在类或其相关基类中找到该名字,则调用是错误的。
|
3.
|
一旦找到了该名字,就进行常规类型检查(第 7.1.2 节),查看如果给定找到的定义,该函数调用是否合法。
|
4.
|
假定函数调用合法,编译器就生成代码。如果函数是虚函数且通过引用或指针调用,则编译器生成代码以确定根据对象的动态类型运行哪个函数版本,否则,编译器生成代码直接调用函数。
|
214.void Base::eval() const; void Derived::eval();这个是重定义。
215.在函数形参表后面写上 = 0 以指定纯虚函数:(这个类将不能创建该类的对象,该类称为抽象类)
class Disc_item : public Item_base { public: double net_price(std::size_t) const = 0; };
216.对于派生类的返回类型必须与基类实例的返回类型完全匹配的要求,但有一个例外。这个例外支持像这个类这样的情况。如果虚函数的基类实例返回类类型的引用或指针,则该虚函数的派生类实例可以返回基类实例返回的类型的派生类(或者是类类型的指针或引用)。
217.习题15.42,all
218.习题16.3
219.用作模板形参的名字不能在模板内部重用。这一限制还意味着模板形参的名字只能在同一模板形参表中使用一次
220.除了定义数据成员或函数成员之外,类还可以定义类型成员。必须告诉编译器我们正在使用的名字指的是一个类型。
221.习题16.12.
222.模板非类型形参是模板定义内部的常量值。
223.泛型编码的规则:1.模板的形参使用const引用(两个原因)。2.函数体中的测试只用<比较。
224.类模板不定义类型,只有特定的实例才定义了类型。多个类型形参的实参必须完全匹配,不能有任何转换。设计者想要允许实参的常规转换,则函数必须用两个类型形参来定义。不能转换有两个例外:
-
const 转换:接受 const 引用或 const 指针的函数可以分别用非const 对象的引用或指针来调用,无须产生新的实例化。如果函数接受非引用类型,形参类型实参都忽略const,即,无论传递const 或非const 对象给接受非引用类型的函数,都使用相同的实例化。
-
数组或函数到指针的转换:如果模板形参不是引用类型,则对数组或函数类型的实参应用常规指针转换。数组实参将当作指向其第一个元素的指针,函数实参当作指向函数类型的指针。
例如,考虑对函数 fobj 和 fref 的调用。fobj 函数复制它的形参,而fref 的形参是引用:
template <typename T> T fobj(T, T); // arguments are copied template <typename T> T fref(const T&, const T&); // reference arguments string s1("a value"); const string s2("another value"); fobj(s1, s2); // ok: calls f(string, string), const is ignored fref(s1, s2); // ok: non const object s1 converted to const reference int a[10], b[42]; fobj(a, b); // ok: calls f(int*, int*) fref(a, b); // error: array types don't match; arguments aren't converted to pointers
第一种情况下,传递 string 对象和 const string 对象作为实参,即使这些类型不完全匹配,两个调用也都是合法的。在fobj 的调用中,实参被复制,因此原来的对象是否为const 无关紧要。在fref 的调用中,形参类型是const 引用,对引用形参而言,转换为const 是可以接受的转换,所以这个调用也正确。在第二种情况中,将传递不同长度的数组实参。fobj 的调用中,数组不同无关紧要,两个数组都转换为指针,fobj 的模板形参类型是int*。但是,fref 的调用是非法的,当形参为引用时(第 7.2.4 节),数组不能转换为指针,a 和b 的类型不匹配,所以调用将出错。
224.用普通类型定义的形参可以使用常规转换。
225.可以使用函数模板对函数指针进行初始化或赋值(第 7.9 节),这样做的时候,编译器使用指针的类型实例化具有适当模板实参的模板版本。例如,假定有一个函数指针指向返回int 值的函数,该函数接受两个形参,都是const int 引用,可以用该指针指向compare 的实例化
template <typename T> int compare(const T&, const T&); // pf1 points to the instantiation int compare (const int&, const int&) int (*pf1) (const int&, const int&) = compare;
获取函数模板实例化的地址的时候,上下文必须是这样的:它允许为每个模板形参确定唯一的类型或值。如果不能从函数指针类型确定模板实参,就会出错。
226.一般我们在函数模板的定义中指明函数模板为导出的,这是通过在关键字 template 之前包含 export 关键字而实现的。对类模板使用 export 更复杂一些。通常,类声明必须放在头文件中,头文件中的类定义体不应该使用关键字export,如果在头文件中使用了export,则该头文件只能被程序中的一个源文件使用。相反,应该在类的实现文件中使用export。
227.习题16.28。
228.通常,当使用类模板的名字的时候,必须指定模板形参。这一规则有个例外:在类本身的作用域内部,可以使用类模板的非限定名。编译器不会为类中使用的其他模板的模板形参进行这样的推断。这一事实意味着,调用类模板成员函数比调用类似函数模板更灵活。用模板形参定义的函数形参的实参允许进行常规转换
229.模板形参表不能为空
230.函数模板可以用与非模板函数一样的方式声明为 inline。说明符放在模板形参表之后、返回类型之前,不能放在关键字 template 之
231.template <class Parm, class U> Parm fcn(Parm* array, U value) { Parm::size_type * p; // If Parm::size_type is a type, then a declaration // If Parm::size_type is an object, then multiplication }
232.模板非类型形参是模板定义内部的常量值,在需要常量表达式的时候,可使用非类型形参(例如,像这里所做的一样)指定数组的长度
232.对模板的非类型形参而言,求值结果相同的表达式将认为是等价的。233.通过将形参设为 const 引用,就可以允许使用不允许复制的类型。
234.泛型实例化时,只会执行两种转换,只针对模板形参,对于普通形参执行一般的转换。
const 转换:接受 const 引用或 const 指针的函数可以分别用非const 对象的引用或指针来调用,无须产生新的实例化。如果函数接受非引用类型,形参类型实参都忽略const,即,无论传递const 或非const 对象给接受非引用类型的函数,都使用相同的实例化。
数组或函数到指针的转换:如果模板形参不是引用类型,则对数组或函数类型的实参应用常规指针转换。数组实参将当作指向其第一个元素的指针,函数实参当作指向函数类型的指针。
235.考虑对函数 fobj 和 fref 的调用。fobj 函数复制它的形参,而fref 的形参是引用:
template <typename T> T fobj(T, T); // arguments are copied template <typename T> T fref(const T&, const T&); // reference arguments string s1("a value"); const string s2("another value"); fobj(s1, s2); // ok: calls f(string, string), const is ignored fref(s1, s2); // ok: non const object s1 converted to const reference int a[10], b[42]; fobj(a, b); // ok: calls f(int*, int*) fref(a, b); // error: array types don't match; arguments aren't converted to pointers
当形参为引用时(第 7.2.4 节),数组不能转换为指针。
236.获取函数模板实例化的地址的时候,上下文必须是这样的:它允许为每个模板形参确定唯一的类型或值。
237.在一个程序中,一个模板只能定义为导出一次。对类模板使用 export 更复杂一些。通常,类声明必须放在头文件中,头文件中的类定义体不应该使用关键字export,如果在头文件中使用了export,则该头文件只能被程序中的一个源文件使用。相反,应该在类的实现文件中使用export:也可以将类模板的个别成员声明为导出的,在这种情况下,关键字export 不在类模板本身指定,而是只在被导出的特定成员定义上指定。导出成员函数的定义不必在使用成员时可见。任意非导出成员的定义必须像在包含模型中一样对待:定义应放在定义类模板的头文件中。
238.在类本身的作用域内部,可以使用类模板的非限定名。编译器不会为类中使用的其他模板的模板形参进行这样的推断
239.模板类只有在使用其模板类指针时,才会实例化该指针所指向的模板。非类型模板实参必须是编译时常量表达式。
240.任意类(模板或非模板)可以拥有本身为类模板或函数模板的成员,这种成员称为成员模板,成员模板不能为虚。
241.异常对象必须是可复制的。不存在数组和函数类型的异常,因为会发生转换。
242.无论对象实际类型是什么,异常对象的类型都与指针的静态类型相匹配。如果该指针是一个指向派生类对象的基类类型指针,则该对象将被分割,只抛出基类部分。
243. 一个块如果发生异常而退出,其动态分配的内存不会被释放。
244.析构函数发生了异常,将导致调用标准库terminate函数,从而导致调用abort函数,强制整个程序非正常退出。所以析构函数不应该抛出异常。构造函数发生异常,将导致部分的构造对象。
245.对于找不到匹配的catch异常,将调用terminate。
246.除下面几种可能的区别之外,异常的类型与 catch 说明符的类型必须完全匹配:
-
Conversions from nonconst to const are allowed. That is, athrow of a nonconst object can match acatch specified to take aconst reference.
允许从非 const 到 const 的转换。也就是说,非 const 对象的throw 可以与指定接受const 引用的catch 匹配。
-
Conversions from derived type to base type are allowed.
允许从派生类型型到基类类型的转换。
-
An array is converted to a pointer to the type of the array; a function is converted to the appropriate pointer to function type.
将数组转换为指向数组类型的指针,将函数转换为指向函数类型的适当指针。
247.通常,如果 catch 子句处理因继承而相关的类型的异常,它就应该将自己的形参定义为引用。
248.catch 可以改变它的形参。在改变它的形参之后,如果 catch 重新抛出异常,那么,只有当异常说明符是引用的时候,才会传播那些改变。
catch (my_error &eObj) { // specifier is a reference type eObj.status = severeErr; // modifies the exception object throw; // the status member of the exception object is severeErr } catch (other_error eObj) { // specifier is a nonreference type eObj.status = badErr; // modifies local copy only throw; // the status member of the exception rethrown is unchanged }
249.用捕获所有异常catch 子句的。捕获所有异常的 catch 子句形式为 (...)。例如:
// matches any exception that might be thrown catch (...) { // place our code here }
如果 catch(...) 与其他 catch 子句结合使用,它必须是最后一个,否则,任何跟在它后面的 catch 子句都将不能被匹配。
250.auto_ptr 只能用于管理从 new 返回的一个对象,它不能管理动态分配的数组。
251.auto_ptr 被复制或赋值的时候,有不寻常的行为,因此,不能将 auto_ptrs 存储在标准库容器类型中。
252.如果一个函数声明没有指定异常说明,则该函数可以抛出任意类型的异常。
253.如果函数抛出了没有在其异常说明中列出的异常,就调用标准库函数unexpected。默认情况下,unexpected 函数调用 terminate 函数,terminate 函数一般会终止程序。
254.基类中虚函数的异常说明,可以与派生类中对应虚函数的异常说明不同。但是,派生类虚函数的异常说明必须与对应基类虚函数的异常说明同样严格,或者比后者更受限。这个限制保证,当使用指向基类类型的指针调用派生类虚函数的时候,派生类的异常说明不会增加新的可抛出异常。例如:
class Base { public: virtual double f1(double) throw (); virtual int f2(int) throw (std::logic_error); virtual std::string f3() throw (std::logic_error, std::runtime_error); }; class Derived : public Base { public: // error: exception specification is less restrictive than Base::f1's double f1(double) throw (std::underflow_error); // ok: same exception specification as Base::f2 int f2(int) throw (std::logic_error); // ok: Derived f3 is more restrictive std::string f3() throw (); };
255.用另一指针初始化带异常说明的函数的指针,或者将后者赋值给函数地址的时候,两个指针的异常说明不必相同,但是,源指针的异常说明必须至少与目标指针的一样严格。
void recoup(int) throw(runtime_error); // ok: recoup is as restrictive as pf1 void (*pf1)(int) throw(runtime_error) = recoup; // ok: recoup is more restrictive than pf2 void (*pf2)(int) throw(runtime_error, logic_error) = recoup; // error: recoup is less restrictive than pf3 void (*pf3)(int) throw() = recoup; // ok: recoup is more restrictive than pf4 void (*pf4)(int) = recoup;
256.命名空间可以在全局作用域或其他作用域内部定义,但不能在函数或类内部定义。不能在不相关的命名空间中定义成员。
257.可以用作用域操作符引用全局命名空间的成员。因为全局命名空间是隐含的,它没有名字,所以记号
::member_name
引用全局命名空间的成员。
258.未命名的命名空间与其他命名空间不同,未命名的命名空间的定义局部于特定文件,从不跨越多个文本文件。未命名的命名空间可以在给定文件中不连续,但不能跨越文件,每个文件有自己的未命名的命名空间。如果头文件定义了未命名的命名空间,那么,在每个包含该头文件的文件中,该命名空间中的名字将定义不同的局部实体。
259.
namespace blip { int bi = 16, bj = 15, bk = 23; // other declarations } int bj = 0; // ok: bj inside blip is hidden inside a namespace void manip() { // using directive - names in blip "added" to global scope using namespace blip; // clash between ::bj and blip::bj // detected only if bj is used ++bi; // sets blip::bi to 17 ++bj; // error: ambiguous // global bj or blip::bj? ++::bj; // ok: sets global bj to 1 ++blip::bj; // ok: sets blip::bj to 16 int bk = 97; // local bk hides blip::bk ++bk; // sets local bk to 98 }
260.接受类类型形参(或类类型指针及引用形参)的函数(包括重载操作符),以及与类本身定义在同一命名空间中的函数(包括重载操作符),在用类类型对象(或类类型的引用及指针)作为实参的时候是可见的。261.有一个或多个类类型形参的函数的名字查找包括定义每个形参类型的命名空间。这个规则还影响怎样确定候选集合,为找候选函数而查找定义形参类(以及定义其基类)的每个命名空间,将那些命名空间中任意与被调用函数名字相同的函数加入候选集合。即使这些函数在调用点不可见,也将之加入候选集合。将那些命名空间中带有匹配名字的函数加入候选集合:
namespace NS { class Item_base { /* ... */ }; void display(const Item_base&) { } } // Bulk_item's base class is declared in namespace NS class Bulk_item : public NS::Item_base { }; int main() { Bulk_item book1; display(book1); return 0; }
262.如果 using 声明在已经有同名且带相同形参表的函数的作用域中引入函数,则 using 声明出错
263.模板的显式特化必须在定义通用模板的命名空间中声明,否则,该特化将与它所特化的模板不同名。
264.构造函数初始化式只能控制用于初始化基类的值,不能控制基类的构造次序。基类构造函数按照基类构造函数在类派生列表中的出现次序调用。
255.用基类的指针或引用只能访问基类中定义(或继承)的成员,不能访问派生类中引入的成员。当一个类继承于多个基类的时候,那些基类之间没有隐含的关系,不允许使用一个基类的指针访问其他基类的成员。
266.当一个类有多个基类的时候,通过所有直接基类同时进行名字查找。多重继承的派生类有可能从两个或多个基类继承同名成员,对该成员不加限定的使用是二义性的。虽然两个继承的print 成员的二义性相当明显,但是也许更令人惊讶的是,即使两个继承的函数有不同的形参表,也会产生错误。类似地,即使函数在一个类中是私有的而在另一个类中是公用或受保护的,也是错误的。
267.无论虚基类出现在继承层次中任何地方,总是在构造非虚基类之前构造虚基类。
268.因为 new(或 delete)表达式与标准库函数同名,所以二者容易混淆。
269.成员 new 和 delete 函数必须是静态的,因为它们要么在构造对象之前使用(operator new),要么在撤销对象之后使用(operator delete),
270.如果表达式对基类指针解引用,则该表达式的静态编译时类型是基类类型;但是,如果指针实际指向派生类对象,则 typeid 操作符将说表达式的类型是派生类型。
271.如果指针 p 的值是 0,那么,如果 p 的类型是带虚函数的类型,则 typeid(*p) 抛出一个bad_typeid 异常;如果p 的类型没有定义任何虚函数,则结果与p 的值是不相关的。正像计算表达式sizeof一样,编译器不计算*p,它使用p 的静态类型,这并不要求p 本身是有效指针。
272.嵌套类只是引入了一个作用域。嵌套类定义了其外围类中的一个类型成员。
273.在看到在类定义体外部定义的嵌套类的实际定义之前,该类是不完全类型,应用所有使用不完全类型的常规限制。嵌套类可以直接引用外围类的静态成员、类型名和枚举成员,当然,引用外围类作用域之外的类型名或静态成员,需要作用域确定操作符。
274.
class Outer { public: struct Inner { // ok: reference to incomplete class void process(const Outer&); Inner2 val; // error: Outer::Inner2 not in scope }; class Inner2 { public: // ok: Inner2::val used in definition Inner2(int i = 0): val(i) { } // ok: definition of process compiled after enclosing class is complete void process(const Outer &out) { out.handle(); } private: int val; }; void handle() const; // member of class Outer };
275.union的访问控制与struct相同,可以定义成员类,但是不能为基类,因此不能有虚函数成员。union 不能具有静态数据成员或引用成员,而且,union 不能具有定义了构造函数、析构函数或赋值操作符的类类型的成员:
union illegal_members { Screen s; // error: has constructor static int is; // error: static member int &rfi; // error: reference member Screen *ps; // ok: ordinary built-in pointer type };
像其他内置类型一样,默认情况下 union 对象是未初始化的。可以用与显式初始化简单类对象一样的方法显式初始化union 对象。但是,只能为第一个成员提供初始化式。该初始化式必须括在一对花括号中。匿名union 不能有私有成员或受保护成员,也不能定义成员函数。
276.局部类的所有成员(包括函数)必须完全定义在类定义体内部,因此,局部类远不如嵌套类有用。类似地,不允许局部类声明 static 数据成员,没有办法定义它们。局部类只能访问在外围作用域中定义的类型名、static 变量和枚举成员,不能使用定义该类的函数中的变量。外围函数对局部类的私有成员没有特殊访问权。
277.可以将一个类嵌套在局部类内部。这种情况下,嵌套类定义可以出现在局部类定义体之外,但是,嵌套类必须在定义局部类的同一作用域中定义。照常,嵌套类的名字必须用外围类的名字进行限定,并且嵌套类的声明必须出现在局部类的定义中。嵌套在局部类中的类本身是一个带有所有附加限制的局部类。嵌套类的所有成员必须在嵌套类本身定义体内部定义。
278.位域必须是整型数据类型。位是否压缩到整数以及如何压缩与机器有关。通常最好将位域设为 unsigned 类型。存储在signed 类型中的位域的行为由实现定义。地址操作符(&)不能应用于位域,所以不可能有引用类位域的指针,位域也不能是类的静态成员。
279.当可以用编译器的控制或检测之外的方式改变对象值的时候,应该将对象声明为 volatile。关键字volatile 是给编译器的指示,指出对这样的对象不应该执行优化。volatile 对象只能调用volatile 成员函数。不能使用合成的复制和赋值操作符从volatile 对象进行初始化或赋值,理由是不能将 volatile 对象传递给普通引用或const 引用。
280.链接指示不能出现在类定义或函数定义的内部,它必须出现在函数的第一次声明上。注意,花括号并没有定义一个作用域。可以将多重声明形式应用于整个头文件。
// compound statement linkage directive extern "C" { int strcmp(const char*, const char*); char *strcat(char*, const char*); }
有时需要在 C 和 C++ 中编译同一源文件。当编译 C++ 时,自动定义预处理器名字 __cplusplus(两个下划线),所以,可以根据是否正在编译 C++ 有条件地包含代码。
#ifdef __cplusplus // ok: we're compiling C++ extern "C" #endif int strcmp(const char*, const char*);
在 C++ 程序中,重载 C 函数很常见,但是,重载集合中的其他函数必须都是 C++ 函数:
class SmallInt { /* ... */ }; class BigNum { /* ... */ }; // the C function can be called from C and C++ programs // the C++ functions overload that function and are callable from C++ extern "C" double calc(double); extern SmallInt calc(const SmallInt&); extern BigNum calc(const BigNum&);
可以从 C 程序和 C++ 程序调用 calc 的 C 版本。其余函数是带类型形参的 C++ 函数,只能从 C++ 程序调用。声明的次序不重要。C 函数的指针与 C++ 函数的指针具有不同的类型,不能将 C 函数的指针初始化或赋值为 C++ 函数的指针(反之亦然)。
-