C++ primer

来源:互联网 发布:mac动态桌面壁纸 编辑:程序博客网 时间:2024/05/21 19:37

1.C++有两个地方空格不能由换行符代替,第一个字符串字面值,第二就是空格符不能出现在预处理指示中。
2.C++是静态类型,即在编译时进行检查。
3.char有3中类型,普通,unsigned, signed.
4.C++所有字符串字面值都由编译器在后面自动加上一个空字符。两个相邻仅由空格,制表符,或换行符分开的字符串字面值可以连接成一个新字符串字面值。
5. 1024f wrong, 3.14UL wrong 浮点字面没有U
6. 左值就是变量的地址,或者是一个代表“对象在内存中的位置”的表达式。右值就是变量的值。下标操作返回左值。一般表达式的结果是右值。算术表达式产生右值。下标和解引用以及赋值操作产生左值
7. char  _这个声明是正确的
8. int ival = 1024;//copy-initialization,
9. 有多个初始化式时,不能使用复制初始化。int a = b = c = 0;//wrong
10. int month = 09, day = 07,firt wrong, second right., very tricky
11. int ival = ival,语法正确,但是仍然未初始化
12. 没有默认构造函数的类类型,每个定义必须提供显示的初始化式,没有初始值是根本不能定义这种类型的变量的。
13. extern int i;//声明 只有当extern声明位于函数外部时,才可以含有初始化式。extern const int &ri;//声明一个const引用,就是指向一个const对象的引用。
14. 常量在定以后就不能修改,所以定义时必须初始化。
15.非const变量默认为extern,而const变量必须显示指定extern。
16. const int bufsize = fcn();//right
17. 复合类型指用其他类型定义的类型。
18. 不能定义到引用类型的引用。
19. 非const引用只能绑定到与该引用同类型的对象。const引用则可以绑定到不同但相关的类型的对象或者绑定到右值。理由比较微妙
      int i = 42;
      //  legal for const references only
      const int &r = 42;
      const int &r2 = r + i;
    
20. 常量表达式是编译器在编译时就能够计算出结果的整型表达式。
21. 定义变量和定义数据成员有个区别,一般不能把类成员的初始化作为其定义的一部分。类通过构造函数初始化数据成员。
22. 头文件一般包含类的定义、extern变量的声明和函数的声明。头文件可以含有三种定义:类的定义,编译时就已知道的const对象(默认定义时该变量的文件的局部变量),和inline函数。这些实体可以在多个源文件定义,只要每个源文件中的定义是相同的。在实践中,大部分的编译器在编译时都会用相应的常量表达式替换这些 const 变量的任何使用。所以,在实践中不会有任何存储空间用于存储用常量表达式初始化的 const 变量。如果 const 变量不是用常量表达式初始化,那么它就不应该在头文件中定义。相反,和其他的变量一样,该 const 变量应该在一个源文件中定义并初始化。应在头文件中为它添加 extern 声明,以使其能被多个文件共享。
23.指向指针的引用eg int *&i; int (&arr)[10]与int &arr[10]的区别,指针相似。
24.头文件中必须总是使用完全限定的标准库名字,一般不适用using。
     #ifndef CONDITION_H
     #define CONDITION_H
     // Definition of Sales_itemclass and related functions goes here
     #endif
25. getline函数返回时丢弃换行符,换行符将不会存储在string对象中。并且不忽略前面的空白符,直到遇到换行符。
26.vector保存内置类型如int类型,将用0值初始化,前提是定义的时候指明了vector大小。
27. STL在循环中一般使用!=而不使用<等,原因在于STL中vector等容器是动态变化的。
28. 我们对const_iterator类型解引用时,可以得到一个指向const对象的引用。
29. 数组元素不能是引用,可以是其他任意复合类型,类类型,以及内置类型,数组不管在哪里定义,如果元素是类类型,就自动调用该类默认构造函数初始化,如果没有默认构造函数,则必须提供显示初始化。对于内置类型数组,除非显示提供元素初值,否则内置类型局部数组元素没有初始化。
30.非const变量以及要到运行阶段才知道的const变量不能作为数组维数。
31.
          char ca1[] = {'C', '+', '+'};        // no null, not C-style string
          char ca2[] = {'C', '+', '+', '\0'};  // explicit null
          char ca3[] = "C++";     // null terminator added automatically
          const char *cp = "C++"; // null terminator added automatically
          char *cp1 = ca1;   // points to first element of a array, but not C-style string
          char *cp2 = ca2;   // points to first element of a null-terminated char array
ca1,ca2,ca3大小分别为3,4,4,但是puts(ca1)时,不会正常结束,strlen结果未知,sizeof结果是3;
32. const char ca4[3] = "ily";//wrong
33.字符串常量是const char *;
34.把 int 型变量赋给指针是非法的,尽管此 int 型变量的值可能为 0。但允许把数值 0 或在编译时可获得 0 值的 const 量赋给指针。
35. void* 指针只支持几种有限的操作:与另一个指针进行比较;向函数传递void* 指针或从函数返回 void* 指针;给另一个 void* 指针赋值。不允许使用void* 指针操纵它所指向的对象。
36.不能使用 void* 指针保存 const 对象的地址,而必须使用 const void* 类型的指针保存 const 对象6地址。允许把非 const 对象的地址赋给指向 const 对象的指针,但不允许通过该指针修改其所指对象的值。
37. 假设给出以下语句:
          typedef string *pstring;
          const pstring cstr;
是 const pstring 类型的指针。声明 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
问题1:typedef后怎么声明一个指向const对象的指针?
38.允许把非 const 对象的地址赋给指向 const 对象的指针。不能把一个const对象的地址赋值给一个const指针或者普通指针,只能赋值给一个指向const对象的指针。const引用既可以绑定const对象也可以绑定非const对象,但是非const引用只能绑定到非const对象。
39. 在自由存储区中创建的数组对象是没有名字的,程序员只能通过其地址间接地访问堆中的对象。动态分配数组时,如果数组元素具有类类型,将使用该类的默认构造函数实现初始化;如果数组元素是内置类型,则无初始化。对于动态分配的数组,其元素只能初始化为元素类型的默认值,而不能像数组变量一样,用初始化列表为数组元素提供各不相同的初值。如果我们在自由存储区中创建的数组存储了内置类型的 const 对象,则必须为这个数组提供初始化:因为数组元素都是 const 对象,无法赋值。实现这个要求的唯一方法是对数组做值初始化。C++ 允许定义类类型的 const 数组,但该类类型必须提供默认构造函数,当然,已创建的常量元素不允许修改——因此这样的数组实际上用处不大。C++ 虽然不允许定义长度为 0 的数组变量,但明确指出,调用 new 动态创建长度为 0 的数组是合法的,长度为0的动态数组不能解引用。
40. c_str 返回的数组并不保证一定是有效的,因为原始的string对象可能发生改变导致返回的数组失败,所以最好复制c_str返回的数组。
41.在下面的声明中,圆括号是必不可少的,引用类似:
     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;
42. 如果操作数为负数,则位操作符如何如何处理操作数的符号位依赖于机器。因此建议使用unsigned整型操作数。移位操作的右操作数不可以是负数,而且必须是严格小于左操作数的位数的值,否则操作效果未定义。
43. 重载的操作符与该操作符的内置类型版本有相同的优先级和结合性。
44. cout << 10 < 42;//wrong,运算级别问题。
45. 如果不严格使用圆括号将条件操作符括起来,将会得到意外的结果:
     cout << (i < j ? i : j);  // ok: prints larger of i and j
     cout << (i < j) ? i : j;  // prints 1 or 0!
     cout << i < j ? i : j;    // error: compares cout to int
第二个表达式效于:
     cout << (i < j); // prints 1 or 0
     cout ? i : j;    // test cout and then evaluate i or j
                      // depending on whether cout evaluates to true or false
46.sizeof expr时,并不会求表达式的值,所以sizeof *p 中,指针 p 可以持有一个无效地址,因为不需要对 p 做解引用操作。
47. C++只规定了&& 和||,逗号操作符,以及条件操作符的运算顺序。以下为编译器相关的语句:
f1() * f2(); if (ia[index++] < ia[index])
48. 下面两个指导原则有助于处理复合表达式:
1. 如果有怀疑,则在表达式上按程序逻辑要求使用圆括号强制操作数的组合。
2. 如果要修改操作数的值,则不要在同一个语句的其他地方使用该操作数。如果必须使用改变的值,则把该表达式分割成两个独立语句:在一个语句中改变该操作数的值,再在下一个语句使用它。第二个规则有一个重要的例外:如果一个子表达式修改操作数的值,然后将该子表达式的结果用于另一个子表达式,这样则是安全的。
49. C++ 没有明确定义如何释放指向不是用 new 分配的内存地址的指针。
50. C++ 保证:删除 0 值的指针是安全的。一旦删除了指针所指向的对象,立即将指针置为 0,这样就非常清楚地表明指针不再指向任何对象。对同一个内存空间使用两次 delete 表达式。当两个指针指向同一个动态创建的对象,删除时就会发生错误。如果在其中一个指针上做 delete 运算,将该对象的内存空间返还给自由存储区,然后接着 delete 第二个指针,此时则自由存储区可能会被破坏。
51. vector<string> **pvec2 = new vector<string>[10];//wrong
52.最简单的转换为整型提升:对于所有比 int 小的整型,包括 char、signedchar、unsigned char、short 和 unsigned short,如果该类型的所有可能的值都能包容在 int 内,它们就会被提升为 int 型,否则,它们将被提升为unsigned int。如果将 bool 值提升为 int ,则 false 转换为 0,而 true 则转换为 1。
53. 不将数组转换为指针的例外情况有:数组用作取地址(&)操作符的操作数或 sizeof 操作符的操作数时,或用数组对数组的引用进行初始化时,不会将数组转换为指针.C++ 还提供了另外两种指针转换:指向任意数据类型的指针都可转换为void* 类型;整型数值常量 0 可转换为任意指针类型。
54.const_cast ,顾名思义,将转换掉表达式的 const 性质,除了添加或删除const 特性,用 const_cast 符来执行其他任何类型转换,都会引起编译错误. dynamic_cast 支持运行时识别指针或引用所指向的对象,i如,对于下面的强制转换:
     int *ip;
     char *pc = reinterpret_cast<char*>(ip);
程序员必须永远记得 pc 所指向的真实对象其实是 int 型,而并非字符数组。任何假设 pc 是普通字符指针的应用,都有可能带来有趣的运行时错误。例如,下面语句用 pc 来初始化一个 string 对象:
     string str(pc);
它可能会引起运行时的怪异行为。旧式的强制类型转换依次与static_cast, const_cast,reinterpret_cast匹配。
55.与其他大多数语句不同,块并不是以分号结束的。
56.如果在条件表达式中定义了变量,那么变量必须初始化。将已初始化的变量值转换为 bool 值后,该 bool 值决定条件是否成立。
57. case 标号必须是整型常量表达式。对于 switch 结构,只能在它的最后一个 case 标号或 default 标号后面定义变量,或者引入块语句。
58. *dest++ = *source++;执行过程:1.指针dest加1。2.指针source加1。3.将source原来指向的对象赋给dest原来所指向的对象。
59. 可以在 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)
                   // ...
60.do while语句中不能在循环条件中定义变量。
61. goto 语句和获得所转移的控制权的带标号的语句必须位于于同一个函数内。goto 语句不能跨越变量的定义语句向前跳转:
     // ...
     goto end;
     int ix = 10; // error: goto bypasses declaration statement
 end:
     // error: code here could use ix but the goto bypassed its declaration
     ix = 42;
如果确实需要在 goto 和其跳转对应的标号之间定义变量,则定义必须放在一个块语句中:
         // ...
         goto end; // ok: jumps to a point where ix is not defined
         {
            int ix = 10;
            // ... code using ix
         }
     end: // ix no longer visible here
向后跳过已经执行的变量定义语句则是合法的。为什么?向前跳过未执行的变量定义语句,意味着变量可能在没有定义的情况下使用。向后跳回到一个变量定义之前,则会使系统撤销这个变量,然后再重新创建它。
62.调用操作符的操作数是函数名和一组参数。
63.形参是变量,实参是表达式。
64. 函数的运行以形参的(隐式)定义和初始化开始。函数不能返回另一个函数或者内置数组类型,但可以返回指向函数的指针,或指向数组元素的指针的指针。
65. 尽管函数的形参是 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 对象的指针形参的函数不相同。
66. 如果函数具有普通的非 const 引用形参,则显然不能通过 const 对象进行调用。毕竟,此时函数可以修改传递进来的对象,这样就违背了实参的 const 特性。但比较容易忽略的是,调用这样的函数时,传递一个右值或具有需要转换的类型的对象同样是不允许的;而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
     }
67. 如果形参是数组的引用,编译器不会将数组实参转化为指针,而是传递数组的引用本身。在这种情况下,数组大小成为形参和实参类型的一部分。编译器检查数组的实参的大小与形参的大小是否匹配:
     // 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;
     }
68.对于 C++ 程序,只能将简单数据类型传递给含有省略符形参的函数。实际上,当需要传递给省略符形参时,大多数类类型对象都不能正确地复制。
69. 返回类型为 void 的函数可以返回另一个返回类型同样是 void 的函数的调用结果。
70. 在含有 return 语句的循环后没有提供 return 语句是很危险的(比如while里面有return,但是函数最后没有return),因为大部分的编译器不能检测出这个漏洞,运行时会出现什么问题是不确定的。
71. 理解返回引用至关重要的是:千万不能返回局部变量的引用。返回引用的函数返回一个左值。千万不要返回指向局部对象的指针。返回局部对象的引用将导致运行时错误。
72. 主函数 main 不能调用自身(即不能递归)。main函数不能重载。
73. 既可以在函数声明也可以在函数定义中指定默认实参。但是,在一个文件中,只能为一个形参指定默认实参一次。
74.只有当定义某个局部变量的函数被调用时才存在的对象叫做自动变量。
75.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。局部变量分为静态和非静态变量。
76.为了确定最佳匹配,编译器将实参类型到相应形参类型转换划分等级。转换等级以降序排列如下:
1. 精确匹配。实参与形参类型相同。
2. 通过类型提升实现的匹配(整型之间的)。
3. 通过标准转换实现的匹配,标准转换之间都是等级的。
4. 通过类类型转换实现的匹配。
77.在使用有枚举类型形参的重载函数时,请记住:由于不同枚举类型的枚举常量值不相同,在函数重载确定过程中,不同的枚举类型会具有完全不同的行为。其枚举成员决定了它们提升的类型,而所提升的类型依赖于机器。
78.除了用作函数调用的左操作数外,对函数名的任何使用都被解释为相应的函数指针。指向不同函数类型的指针之间不存在转换。指向相应函数的指针调用它所指向的函数时,可以不用接引用操作符。
79.函数的形参可以是指向函数的指针。这种形参可以用以下两种形式编写:
 
     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
80.对象的读入可能只读入一部分内容,必须在读入的时候检测流的状态。
81.一般由复合复制操作符实现算术操作符。
82.前自增和前自减操作符返回的被增量或减量对象的引用,但是后缀式返回的是修改前的对象。
83.每一个版本的重载函数都应在同一个作用域中声明。
84.在实际应用中,调用重载函数时应尽量避免对实参做强制类型转换:需要使用强制类型转换意味着所设计的形参集合不合理。
85.标准库类型不允许做复制或赋值操作,只有支持复制的元素类型可以存储在 vector 或其他容器类型里。由于流对象不能复制,因此不能存储在 vector(或其他)容器(即不存在存储流对象的 vector 或其他容器),形参或返回类型也不能为流类型。如果需要传递或返回 IO对象,则必须传递或返回指向该对象的指针或引用。
86.   1. 程序正常结束。作为 main 返回工作的一部分,将清空所有输出缓冲区。
2. 在一些不确定的时候,缓冲区可能已经满了,在这种情况下,缓冲区将会在写下一个值之前刷新。
3. 用操纵符(第 1.2.2 节)显式地刷新缓冲区,例如行结束符 endl。
4. 在每次输出操作执行完后,用 unitbuf 操作符设置流的内部状态,从而清空缓冲区。
5. 可将输出流与输入流关联(tie)起来。在这种情况下,在读输入流时将刷新其关联的输出缓冲区。
6.如果程序崩溃了,则不会刷新缓冲区。
87.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";
 
88. 大多数的编译器都不支持递归函数的内联。内联函数应该在头文件中定义,这一点不同于其他函数。inline 函数的定义对编译器而言必须是可见的,以便编译器能够在调用点内联展开该函数的代码。此时,仅有函数原型是不够的。
89.const成员函数中const关键字必须同时出现在声明和定义中,否则会导致编译时错误
90.不完全类型只能定义指向该类型的指针及引用,或者用于声明使用该类型作为形参类型或者返回类型的函数。在使用引用或指针访问类的成员之前,必须已经定义类。类不能拥有自身类型的数据成员,除非是static。类只有该类型的对象时才分配对象,一般定义类不分配对象。
91.每个成员函数都有一个额外的、隐含的形参将该成员函数与调用该函数的类对象捆绑在一起。每个成员函数(除了static 成员函数外)都有一个额外的、隐含的形参 this(是一个const指针)。在调用成员函数时,形参 this 初始化为调用函数的对象的地址。成员函数声明的形参表后面的 const 改变了隐含的 this 形参的类型。不能从const 成员函数返回指向类对象的普通引用。const 成员函数只能返回*this 作为一个const 引用。
92.const 对象、指向 const 对象的指针或引用只能用于调用其const 成员函数,如果尝试用它们来调用非 const 成员函数,则是错误的。基于成员函数是否为cosnt可以重载成员函数,const对象只能使用const成员,非const对象都可以,但是非cosnt版本是更好的匹配。const成员函数只能返回*this作为一个const引用。
93.类定义实际是在两个阶段中处理:首先编译成员声明,只有在所有成员出现后才编译它们的定义本身。
94.在C++中,名字查找发生在类型检查之前。
95.一旦一个名字被用作类型名,改名字就不能被重复定义。
     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;
         // ...
     };
类型别名的定义必须出现在它的使用之前。
96.当函数的返回类型不同时,编译器会认为是重载,但是又不是合法重载,所以导致编译错误。
97.不管成员是否在构造函数初始化列表中显示初始化,类类型的数据成员总是在初始化阶段初始化。初始化发生在计算阶段开始之前。假如内置类型和复合类型成员没有在初始化列表初始化,则依赖于对象的作用域。全局初始化为0,局部作用域不初始化。
98.没有默认构造函数的类类型成员,以及const以及引用类型成员,必须在构造函数初始化列表进行初始化。
99.如果类具有内置类型或复合类型数据成员,那么定义构造函数来初始化这些成员就是一个好主意。
100. const 构造函数是不必要的。构造函数初始化式只在构造函数的定义中而不是声明中指定。
101.没有默认构造函数的类类型的成员,以及 const 或引用类型的成员,不管是哪种类型,都必须在构造函数初始化列表中进行初始化。可以初始化 const 对象或引用类型的对象,但不能对它们赋值。
102.只有当一个类没有定义构造函数时,编译器才会自动生成一个默认构造函数。如果类包含内置或复合类型的成员,则该类不应该依赖于合成的默认构造函数。它应该定义自己的构造函数来初始化这些成员。
103.。NoDefault 没有默认构造函数,意味着:
1. 具有 NoDefault 成员的每个类的每个构造函数,必须通过传递一个初始的 string 值给 NoDefault 构造函数来显式地初始化 NoDefault 成员。
2. 编译器将不会为具有 NoDefault 类型成员的类合成默认构造函数。如果这样的类希望提供默认构造函数,就必须显式地定义,并且默认构造函数必须显式地初始化其 NoDefault 成员。
3. NoDefault 类型不能用作动态分配数组的元素类型。
4. NoDefault 类型的静态分配数组必须为每个元素提供一个显式的初始化式。
5. 如果有一个保存 NoDefault 对象的容器,例如 vector,就不能使用接受容器大小而没有同时提供一个元素初始化式的构造函数。
实际上,如果定义了其他构造函数,则提供一个默认构造函数几乎总是对的。通常,在默认构造函数中给成员提供的初始值应该指出该对象是“空”的。
104.尽管大多数对象可以通过运行适当的构造函数进行初始化,但是直接初始化简单的非抽象类的数据成员仍是可能的。对于没有定义构造函数并且其全体数据成员均为 public 的类,可以采用与初始化数组元素相同的方式初始化其成员,这种方式不好。
105.假如定义了一个构造函数,一般应该定义一个默认构造函数,复制构造函数也是构造函数,如果需要定义一个析构函数,则也需要其他复制构造函数和赋值操作符。只有析构函数编译器总是会合成一个。合成的析构函数并不会删除指针成员所指向的对象。析构函数不能指定任何形参,所以不能重载析构函数。先运行自己定义的析构函数,再运行合成的。合成的析构函数不会撤销static成员,static成员怎么构造,怎么析构呢???????????????????
106.对于定义了构造函数的类,不能使用显示初始化式。对于没有定义构造函数而且数据成员都为public才能使用显示初始化式。
107.假如存在两个或多个构造函数的形参都有默认参数,将导致默认构造函数重复定义,构成非法重载。
108.友元也是接口的一部分。
109.假如将A类的成员函数设为B类的友元,则必须A类必须先定义,但是A的该成员函数必须定义在B之后,为了访问B的成员。
110.static成员函数可以直接访问所属类的static成员,但是不能直接使用非static成员,并且没有this形参。static数据成员必须在类定义体外定义,且只定义一次,不是由构造函数初始化的,const static数据成员可以在类体内进行初始化,同样不是由构造函数实现,但是仍然需要在类体外进行定义,但是不用指定初始化值。一般可放在类的实现文件。在类定义体外部实现static成员函数时,可以不重复指定static关键字。static成员函数不能为const,理由在于。。。也不能声明为虚函数。static关键字只能出现声明中,不能出现在定义中。static数据成员可以是该成员所属的类类型,也可以用作默认实参,非static数据成员不能,因为无法提供对象以获取该成员的值。
111.
     Point global;
     Point foo_bar(Point arg)
     {
          Point local = arg;//复制构造函数
          Point *heap = new Point(global);//复制构造函数
          *heap = local;//只是赋值操作符
          Point pa[ 4 ] = { local, *heap };//复制构造函数
          return *heap;//复制构造函数
     }
112.假如类有一个数组成员,那么复制构造函数将复制数组的每个成员,一般情况下数组是不能复制的。复制构造函数一般都不为explicit,因为要用于传参和从函数返回对象。合成的复制构造函数只适用于类只含有类类型或者内置类型,对于有指针成员或者引用成员或者分配了资源,必须自己定义复制构造函数。
113.复制构造函数的参数必须为引用,理由。。。
114.能够声明成员,但是不定义,将导致链接失败,运行时错误。通过声明但是不定义一个private复制构造函数,则在用户代码中复制这个类对象,将在编译时标记为错误,而成员函数和友元中的复制尝试将在链接时导致错误。不允许复制的类对象只能作为引用传递给函数或从函数返回,也不能用作容器的元素。
115.当对象的指针或者引用超出作用域时,不会运行析构函数。
116.复制构造函数和其他构造函数一样,如果没有初始化某个类成员,则该成员的默认构造函数初始化,而不是使用成员的复制构造函数。
117.合成复制构造函数直接复制内置类型成员的值,类类型成员使用该类的复制构造函数进行复制。数组成员的复制是个例外。虽然一般不能复制数组,但如果一个类具有数组成员,则合成复制构造函数将复制数组。复制数组时合成复制构造函数将复制数组的每一个元素。合成操作依次取得每个成员,根据成员类型进行成员的复制、赋值或撤销。如果成员为类类型的,合成操作调用该类的相应操作(即,复制构造函数调用成员的复制构造函数,析构函数调用成员的析构函数,等等)。如果成员为内置类型或指针,则直接复制或赋值,析构函数对撤销内置类型或指针类型的成员没有影响。如果成员为数组,则根据元素类型以适当方式复制、赋值或撤销数组中的元素。
118.动态分配的对象只有在指向该对象的指针被删除时才撤销。如果没有删除指向动态对象的指针,则不会运行该对象的析构函数,对象就一直存在,从而导致内存泄漏,而且,对象内部使用的任何资源也不会释放。当对象的引用或指针超出作用域时,不会运行析构函数。只有删除指向动态分配对象的指针或实际对象(而不是对象的引用)超出作用域时,才会运行析构函数。
119.析构函数并不仅限于用来释放资源。一般而言,析构函数可以执行任意操作,该操作是类设计者希望在该类对象的使用完毕之后执行的。合成析构函数按对象创建时的逆序撤销每个非 static 成员。对于类类型的每个成员,合成析构函数调用该成员的析构函数来撤销对象。撤销内置类型成员或复合类型的成员没什么影响。尤其是,合成析构函数并不删除指针成员所指向的对象。因为不能指定任何形参,所以不能重载析构函数。虽然可以为一个类定义多个构造函数,但只能提供一个析构函数,应用于类的所有对象。析构函数与复制构造函数或赋值操作符之间的一个重要区别是,即使我们编写了自己的析构函数,合成析构函数仍然运行。合成析构函数在自定义析构函数之后运行。
120.习题13.14
121.编写自己的复制构造函数时,必须显式复制需要复制的任意成员。显式定义的复制构造函数不会进行任何自动复制。像其他任何构造函数一样,如果没有初始化某个类成员,则那个成员用该成员的默认构造函数初始化。复制构造函数中的默认初始化不会使用成员的复制构造函数。
122.重载操作符必须具有一个类类型操作数或者枚举类型。不能为内置类型重载操作符,也不能为其添加新的操作符。除了函数调用操作符operator()之外,重载操作符使用默认实参是非法的。重载操作符优先级和结合性是固定的,但不再具备短路求值特性(如, ,and, or,)。一般将算术和关系操作符定义为非成员函数,而将赋值操作符定义为成员。
123.算术运算和输入输出已经关系操作符一般定义为非成员函数,而赋值操作符包括+=,++,--,解引用应定为成员函数,但不强制,=,[],->和()必须定义为成员函数,否则会编译错误,()调用操作符可以重载多个版本。
124.如果没有特定的重载版本,编译器将合成赋值操作符,取地址操作符,逗号操作符,内置逻辑与和逻辑或,保持各个操作符的原有特性。但是如果重载了这些特性将发生改变。
125.IO操作符必须定义为非成员函数,理由是否则左操作数将只能是该类类型的对象:
     // if operator<< is a member of Sales_item
     Sales_item item;
     item << cout;
与习惯相反。如果要想是成员函数,也要保持习惯,则只能是ostream的成员,但是不能为标准库增加成员。
126.const 引用,不能复制。
127.设计输入操作符时,如果可能,要确定错误恢复措施,这很重要。
128. 赋值操作符可以重载。无论形参为何种类型,赋值操作符必须定义为成员函数,这一点与复合赋值操作符有所不同。一般而言,赋值操作符与复合赋值操作符应返回操作符的引用。
129.类定义下标操作符时,一般需要定义两个版本:一个为非 const 成员并返回引用,另一个为 const 成员并返回 const 引用。
130.箭头操作符与众不同。它可能表现得像二元操作符一样:接受一个对象和一个成员名。对对象解引用以获取成员。不管外表如何,箭头操作符不接受显式形参。这里没有第二个形参,因为-> 的右操作数不是表达式,相反,是对应着类成员的一个标识符。没有明显可行的途径将一个标识符作为形参传递给函数,相反,由编译器处理获取成员的工作。当这样编写时:
     point->action();
由于优先级规则,它实际等价于编写:
     (point->action)();
换句话说,我们想要调用的是对 point->action 求值的结果。编译器这样对该代码进行求值:
1. 如果 point 是一个指针,指向具有名为 action 的成员的类对象,则编译器将代码编译为调用该对象的action 成员。
2. 否则,如果 point是定义了 operator-> 操作符的类的一个对象,则 point->action 与 point.operator->()->action 相同。即,执行point 的operator->(),然后使用该结果重复这三步。
3. 否则,代码出错。
131. 重载箭头操作符必须返回指向类类型的指针,或者返回定义了自己的箭头操作符的类类型对象。如果返回类型是指针,则内置箭头操作符可用于该指针,编译器对该指针解引用并从结果对象获取指定成员。如果被指向的类型没有定义那个成员,则编译器产生一个错误。如果返回类型是类类型的其他对象(或是这种对象的引用),则将递归应用该操作符。编译器检查返回对象所属类型是否具有成员箭头,如果有,就应用那个操作符;否则,编译器产生一个错误。这个过程继续下去,直到返回一个指向带有指定成员的的对象的指针,或者返回某些其他值,在后一种情况下,代码出错。(递归的)。需要解引用操作符的const 和非 const 版本。
132.函数调用操作符必须声明为成员函数。一个类可以定义函数调用操作符的多个版本,由形参的数目或类型加以区别。定义了调用操作符的类,其对象常称为函数对象,即它们是行为类似函数的对象。函数对象可以比函数更灵活。
133.标准库提供了一组函数适配器,用于特化和扩展一元和二元函数对象。函数适配器分为如下两类:绑定器(bind1st, bind2nd),求反器(not1, not2)
134.转换操作符在类定义体内声明,在保留字 operator 之后跟着转换的目标类型。一般而言,不允许转换为数组或函数类型,转换为指针类型(数据和函数指针)以及引用类型是可以的。转换函数必须是成员函数,不能指定返回类型,并且形参表必须为空。虽然转换函数不能指定返回类型,但是每个转换函数必须显式返回一个指定类型的值。转换函数一般不应该改变被转换的对象。因此,转换操作符通常应定义为 const 成员。
135.只要存在转换,编译器将在可以使用内置转换的地方自动调用。只能有一次类型转换,但是后面可以跟多次标准转换。标准转换优于类类型转换。
136.使用转换函数时,被转换的类型不必与所需要的类型完全匹配。必要时可在类类型转换之后跟上标准转换以获得想要的类型。只能应用一个类类型转换。使用构造函数执行隐式转换的时候,构造函数的形参类型不必与所提供的类型完全匹配,在类型转换前可以有标准转换。
137.一般而言,给出一个类与两个内置类型之间的转换是不好的做法。如果两个转换操作符都可用在一个调用中,而且在转换函数之后存在标准转换则根据该标准转换的类别选择最佳匹配。当两个类定义了相互转换时,很可能存在二义性。
     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 类的转换操作符可以避免这个额外的步骤。这一小小区别足以使我们倾向于使用转换操作符。避免二义性最好的方法是避免编写互相提供隐式转换的成对的类。保证最多只有一种途径将一个类型转换为另一类型。做到这点,最好的办法是限制转换操作符的数目,尤其是,到一种内置类型应该只有一个转换。编译器将不会试图区别两个不同的类类型转换。具体而言,即使一个调用需要在类类型转换之后跟一个标准转换,而另一个是完全匹配,编译器仍会将该调用标记为错误。面对二义性转换,程序员可以使用强制转换来显式指定应用哪个转换操作。在调用重载函数时,需要使用构造函数或强制类型转换来转换实参,这是设计拙劣的表现。
正确设计类的重载操作符、转换构造函数和转换函数需要多加小心。尤其是,如果类既定义了转换操作符又定义了重载操作符,容易产生二义性。下面几条经验规则会有所帮助:
1. 不要定义相互转换的类,即如果类 Foo 具有接受类Bar 的对象的构造函数,不要再为类Bar 定义到类型Foo 的转换操作符。
2. 避免到内置算术类型的转换。具体而言,如果定义了到算术类型的转换,则
o 不要定义接受算术类型的操作符的重载版本。如果用户需要使用这些操作符,转换操作符将转换你所定义的类型的对象,然后可以使用内置操作符。
o 不要定义转换到一个以上算术类型的转换。让标准转换提供到其他算术类型的转换。
138.一般而言,函数调用的候选集只包括成员函数或非成员函数,不会两者都包括。而确定操作符的使用时,操作符的非成员和成员版本可能都是候选者。
139.在 C++ 中,基类必须指出希望派生类重写哪些函数,定义为 virtual 的函数是基类期待派生类重新定义的,基类希望派生类继承的函数不能定义为虚函数。
140.除了构造函数之外,任意非 static 成员函数都可以是虚函数。virtual保留字只在类内部的成员函数声明中出现,不能用在类定义体外部出现的函数定义上。
141.如果需要声明(但并不实现)一个派生类,则声明包含类名但不包含派生列表。例如,下面的前向声明会导致编译时错误:
     // 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;
 142.派生类只能通过派生类对象访问其基类的 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没有访问权限)。
143.派生类必须对想要重定义的每个继承成员进行声明。派生类中虚函数的声明必须与基类中的定义方式完全匹配,但有一个例外:返回对基类型的引用(或指针)的虚函数。派生类中的虚函数可以返回基类函数所返回类型的派生类的引用(或指针)。一旦函数声明为虚函数,它就一直为虚函数。派生类重定义虚函数可以使用virtual关键字也可以不使用。
144.C++语言不要求编译器将对象的基类部分和派生部分连续排列。
145.已定义的类才可以用作基类。这一规则暗示着不可能从类自身派生出一个类。如果需要声明但是并不实现一个派生类,不能使用派生列表,否则将导致编译错误。
146.因为每个派生类对象都包含基类部分,所以可将基类类型的引用绑定到派生类对象的基类部分,也可以用指向基类的指针指向派生类对象。基类类型引用和指针的关键点在于静态类型(在编译时可知的引用类型或指针类型)和动态类型(指针或引用所绑定的对象的类型这是仅在运行时可知的)可能不同。将基类类型的引用或指针绑定到派生类对象对基类对象没有影响,对象本身不会改变,仍为派生类对象。对象的实际类型可能不同于该对象引用或指针的静态类型,这是 C++ 中动态绑定的关键。。对象的动态类型
总是与静态类型相同,这一点与引用或指针相反。运行的函数(虚函数或非虚函数)是由对象的类型定义的。
147.注意注释
     // 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;
     }
 
148.只有成员函数中的代码才应该使用作用域操作符覆盖虚函数机制。派生类虚函数调用基类版本时,必须显式使用作用域操作符。如果派生类函数忽略了这样做,则函数调用会在运行时确定并且将是一个自身调用,从而导致无穷递归。
     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);
149.如果一个调用省略了具有默认值的实参,则所用的值由调用该函数的类型定义,与对象的动态类型无关。通过基类的引用或指针调用虚函数时,默认实参为在基类虚函数声明中指定的值,如果通过派生类的指针或引用调用虚函数,则默认实参是在派生类的版本中声明的值。在同一虚函数的基类版本和派生类版本中使用不同的默认实参几乎一定会引起麻烦。如果通过基类的引用或指针调用虚函数,但实际执行的是派生类中定义的版本,这时就可能会出现问题。在这种情况下,为虚函数的基类版本定义的默认实参将传给派生类定义的版本,而派生类版本是用不同的默认实参定义的(就是这么规定的)。
150.使用 private 或 protected 派生的类不继承基类的接口,相反,这些派生通常被称为实现继承。
151.派生类可以恢复继承成员的访问级别,但不能使访问级别比基类中原来指定的更严格或更宽松。using 声明访问基类中的名字(恢复基类中的访问控制)
152.如果基类定义 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
153.存在从派生类型引用到基类类型引用的自动转换(指针同样)。一个基类对象可能是也可能不是一个派生类对象的部分,结果,没有从基类引用(或基类指针)到派生类引用(或派生类指针)的(自动)转换。虽然一般可以使用派生类型的对象对基类类型的对象进行初始化或赋值,但,没有从派生类型对象到基类类型对象的直接转换。一个是派生类对象转换为基类类型引用,一个是用派生类对象对基类对象进行初始化或赋值,理解它们之间的区别很重要。
154.对基类对象进行初始化或赋值,实际上是在调用函数:初始化时调用构造函数,赋值时调用赋值操作符。用派生类对象对基类对象进行初始化或赋值时,有两种可能性。第一种(虽然不太可能的)可能性是,基类可能显式定义了将派生类型对象复制或赋值给基类对象的含义,这可以通过定义适当的构造函数或赋值操作符实现基类一般(显式或隐式地)定义自己的复制构造函数和赋值操作符),这些成员接受一个形参,该形参是基类类型(const)引用。因为存在从派生类引用到基类引用的转换,这些复制控制成员可用于从派生类对象对基类对象进行初始化或赋值。
155.派生类要确定到基类的转换是否可访问,可以考虑基类的 public 成员是否访问,如果可以,转换是可访问的,否则,转换是不可访问的。
156.编译器在编译时无法知道特定转换在运行时实际上是安全的。编译器确定转换是否合法,只看指针或引用的静态类型,所以即使一个基类指针指向的是派生类对象,仍然不能赋给派生类指针。在这些情况下,如果知道从基类到派生类的转换是安全的,就可以使用static_cast强制编译器进行转换。或者,可以用dynamic_cast 申请在运行时进行检查。
157.构造函数和复制控制成员不能继承,每个类定义自己的构造函数和复制控制成员。像任何类一样,如果类不定义自己的默认构造函数和复制控制成员,就将使用合成版本。像任意其他成员一样,构造函数可以为protected 或private,某些类需要只希望派生类使用的特殊构造函数,这样的构造函数应定义为protected。
158.初始化函数 Base(d) 将派生类对象 d 转换为它的基类部分的引用,并调用基类复制构造函数。如果省略基类初始化函数,如下代码:
     // probably incorrect definition of the Derived copy constructor
     Derived(const Derived& d) /* derived member initizations */
                                   {/* ... */ }
效果是运行 Base 的默认构造函数初始化对象的基类部分。假定 Derived 成员的初始化从d 复制对应成员,则新构造的对象将具有奇怪的配置:它的Base 部分将保存默认值,而它的Derived 成员是另一对象的副本。
159.编译器总是显式调用派生类对象基类部分的析构函数。每个析构函数只负责清除自己的成员。(由编译器自己来,无需程序猿写代码)。删除指向动态分配对象的指针时,需要运行析构函数在释放对象的内存之前清除对象。处理继承层次中的对象时,指针的静态类型可能与被删除对象的动态类型不同,可能会删除实际指向派生类对象的基类类型指针。如果删除基类指针,则需要运行基类析构函数并清除基类的成员,如果对象实际是派生类型的,则没有定义该行为。要保证运行适当的析构函数,基类中的析构函数必须为虚函数。
160.基类析构函数是三法则的一个重要例外。三法则指出,如果类需要析构函数,则类几乎也确实需要其他复制控制成员。基类几乎总是需要构造函数,从而可以将析构函数设为虚函数。如果基类为了将析构函数设为虚函数则具有空析构函数,那么,类具有析构函数并不表示也需要赋值操作符或复制构造函数。即使析构函数没有工作要做,继承层次的根类也应该定义一个虚析构函数。在复制控制成员中,只有析构函数应定义为虚函数,构造函数不能定义为虚函数。构造函数是在对象完全构造之前运行的,在构造函数运行的时候,对象的动态类型还不完整。虽然可以在基类中将成员函数operator= 定义为虚函数,但这样做并不影响派生类中使用的赋值操作符。每个类有自己的赋值操作符,派生类中的赋值操作符有一个与类本身类型相同的形参,该类型必须不同于继承层次中任意其他类的赋值操作符的形参类型。将赋值操作符设为虚函数可能会令人混淆,因为虚函数必须在基类和派生类中具有同样的形参。基类赋值操作符有一个形参是自身类类型的引用,如果该操作符为虚函数,则每个类都将得到一个虚函数成员,该成员定义了参数为一个基类对象的operator=。但是,对派生类而言,这个操作符与赋值操作符是不同的。将类的赋值操作符设为虚函数很可能会令人混淆,而且不会有什么用处。
161.构造派生类对象时首先运行基类构造函数初始化对象的基类部分。在执行基类构造函数时,对象的派生类部分是未初始化的。实际上,此时对象还不是一个派生类对象。撤销派生类对象时,首先撤销它的派生类部分,然后按照与构造顺序的逆序撤销它的基类部分。在这两种情况下,运行构造函数或析构函数的时候,对象都是不完整的。为了适应这种不完整,编译器将对象的类型视为在构造或析构期间发生了变化。在基类构造函数或析构函数中,将派生类对象当作基类类型对象对待。构造或析构期间的对象类型对虚函数的绑定有影响。如果在构造函数或析构函数中调用虚函数,则运行的是为构造函数或析构函数自身类型定义的版本。
162.对象、引用或指针的静态类型决定了对象能够完成的行为。甚至当静态类型和动态类型可能不同的时候,就像使用基类类型的引用或指针时可能会发生的,静态类型仍然决定着可以使用什么成员。
163.虽然可以直接访问基类成员,就像它是派生类成员一样,但是成员保留了它的基类成员资格。与基类成员同名的派生类成员将屏蔽对基类成员的直接访问。可以使用作用域操作符访问被屏蔽成员。
164.局部作用域中声明的函数不会重载全局作用域中定义的函数同样,派生类中定义的函数也不重载基类中定义的成员。作用域问题。
165.如果不想重定义基类某个函数的所有重载版本,而且也不想屏蔽掉基类中的该函数,可以使用using声明。
166.要获得动态绑定,必须通过基类的引用或指针调用虚成员。当我们这样做时,编译器器将在基类中查找函数。假定找到了名字,编译器就检查实参是否与形参匹配。现在可以理解虚函数为什么必须在基类和派生类中拥有同一原型了(返回类型可以不同)。如果基类成员与派生类成员接受的实参不同,就没有办法通过基类类型的引用或指针调用派生类函数。考虑如下(人为的)为集合:
     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 中定义的版本。
167.派生类重定义继承的虚函数时,可以去掉是否为const成员//////////////////////////////////////
168.理解 C++ 中继承层次的关键在于理解如何确定函数调用。确定函数调用遵循以下四个步骤:
1.  首先确定进行函数调用的对象、引用或指针的静态类型。
2.  在该类中查找函数,如果找不到,就在直接基类中查找,如此循着类的继承链往上找,直到找到该函数或者查找完最后一个类。如果不能在类或其相关基类中找到该名字,则调用是错误的。
3.  一旦找到了该名字,就进行常规类型检查,查看如果给定找到的定义,该函数调用是否合法。
4.  假定函数调用合法,编译器就生成代码。如果函数是虚函数且通过引用或指针调用,则编译器生成代码以确定根据对象的动态类型运行哪个函数版本,否则,编译器生成代码直接调用函数。
169.对于派生类的返回类型必须与基类实例的返回类型完全匹配的要求,但有一个例外。这个例外支持像这个类这样的情况。如果虚函数的基类实例返回类类型的引用或指针,则该虚函数的派生类实例可以返回基类实例返回的类型的派生类(或者是类类型的指针或引用)。
170.对于compare 的模板版本:
     // implement strcmp-like generic compare function
     // returns 0 if the values are equal, 1 if v1 is larger, -1 if v1 is smaller
     template <typename T>
     int compare(const T &v1, const T &v2)
     {
         if (v1 < v2) return -1;
         if (v2 < v1) return 1;
         return 0;
     }
    compare ("hi", "world");怎么匹配:A:CE;
171.作为模板形参的名字不能再模板内重用,这意味着模板形参名字只能在同一模板形参表中使用一次。
172.对于一个A类型的成员名字必须要使用typename A::size_type *p;否则size_type将被当做模板的数据成员。
173.对于有非类型模板形参的模板,非类型形参将用值代替,并且求值结果相同的表达式被认为是等价的,非类型模板实参必须是编译时常量表达式,比如下面这个模板
     // initialize elements of an array to zero
     template <class T, size_t N> void array_init(T (&parm)[N])
     {
         for (size_t i = 0; i != N; ++i) {
             parm[i] = 0;
         }
     }
当有数组int x[42]时,array_init(x)将实例化模板其中N是由编译器从数组实参计算得到
174.模板形参最好是const引用。最好对实参类型的要求尽可能的少。
175.模板在使用时将进行实例化,类模板在引用实际模板类类型时实例化,函数模板在调用它或用它对函数指针进行初始化或者赋值时实例化。
176.一般不会转换实参以匹配已有的实例化,相反,会产生新的实例。除了产生新的实例化之外,编译器只会执行两种转换:
• const 转换:接受 const 引用或 const 指针的函数可以分别用非const 对象的引用或指针来调用,无须产生新的实例化。如果函数接受非引用类型,形参类型实参都忽略const,即,无论传递 const 或非 const 对象给接受非引用类型的函数,都使用相同的实例化。
• 数组或函数到指针的转换:如果模板形参不是引用类型,则对数组或函数类型的实参应用常规指针转换。数组实参将当作指向其第一个元素的指针,函数实参当作指向函数类型的指针。如果是引用类型,将不会发生转换。
177.获取函数模板实例化的地址的时候,上下文必须是这样的:它允许为每个模板形参确定唯一的类型或值。如
     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;
而下面不能使用 compare 作为传给func 的实参:
     // overloaded versions of func; each take a different function pointer type
     void func(int(*) (const string&, const string&));
     void func(int(*) (const int&, const int&));
     func(compare); // error: which instantiation of compare?
178.函数模板指定返回类型的一种方式是引入第三个模板形参,它必须由调用者显式指定:
     // T1 cannot be deduced: it doesn't appear in the function parameter list
     template <class T1, class T2, class T3>
     T1 sum(T2, T3);

     // ok T1 explicitly specified; T2 and T3 inferred from argument types
     long val3 = sum<long>(i, lng); // ok: calls long sum(int, long)
但是如果是这样的,
     // poor design: Users must explicitly specify all three template parameters
     template <class T1, class T2, class T3>
     T3 alternative_sum(T2, T1);
那么必须为所有形参指定实参为所有三个形参指定实参,假如可以从函数形参推断,则只有最右边的形参的显示模板实参可以省略。
     // error: can't infer initial template parameters
     long val3 = alternative_sum<long>(i, lng);
     // ok: All three parameters explicitly specified
     long val2 = alternative_sum<long, int, long>(i, lng);
179.使用显示模板实参的另一个例子,通过使用显式模板实参能够消除二义性:
     template <typename T> int compare(const T&, const T&);
     // overloaded versions of func; each take a different function pointer type
     void func(int(*) (const string&, const string&));
     void func(int(*) (const int&, const int&));
     func(compare<int>); // ok: explicitly specify which version of compare
180.要进行实例化,编译器必须能够访问定义模板的源代码。当调用函数模板或类模板的成员函数时,编译器需要函数的定义。
181.export关键字能够指明给定的定义可能会需要在其他文件中产生实例化。一个程序中,一个模板只能定义为导出一次。编译器在需要产生这些实例化时计算出怎样定位模板定义,export关键字不必在模板声明中出现。导出成员函数的定义不必在使用成员时可见。非导出成员的定义应该放在头文件中。
182.如果编译器支持分别编译模型,类模板的内联成员函数应该与类模板的定义一起放在头文件中,而非内联成员函数和static数据成员的定义应该放在实现文件中。
183.在类模板的作用域内部,可以用它的非限定名字引用该类。但是编译器不会为类中使用其他模板的模板形参进行这样的推断。
184.实例化类模板成员时,编译器不执行模板实参推断,而是由调用该函数的对象的类型确定。此时用类模板形参定义的成员函数形参允许进行常规转换。类模板成员函数只有为程序所用才进行实例化。如果某成员函数从未使用,就不会实例化。所以没有默认构造函数的类型仍然可以使用容器保存该类型,但是不能使用只接受一个容量的构造函数。
185.类模板的指针定义不会对该类进行实例化,只有用到这样的指针时才会对类进行实例化。
186.sizeof(template<Type> tmp)将导致实例化。sizeof需要具体的类。
187.想要限制对特定实例化的友元关系时,必须在可以用于友元声明之前声明类或函数。
188.成员模板不能为虚。
189.当函数模板特化时,假如可以从函数形参表推断模板实参,则不必显示指定模板实参。特化版本的模板函数对实参类型不应用转换。模板特化版本的调用中,实参类型必须与特化版本函数的形参类型完全匹配,如果不完全匹配,编译器将为实参从模板定义实例化一个实例。应在头文件包含模板特化的声明,使用该特化的每个源文件包含该头文件,不能一些文件从泛型模板定义实例化一个函数模板,另一些文件为同一模板实参集合特化该函数模板。特化出现在对该模板实例的调用之后是错误的。
190.注意特化与对应的泛型并没有什么关系,泛型中定义的成员,特化版本假如也需要必须自己定义。
191.在类特化外部定义成员时,成员之前不能加template<>标记。
192.特化和部分特化优于通用模板。
193.可以定义有相同名字但形参数目或类型不同的过个函数模板或者普通非模板函数。
194.对于下面普通函数和函数模板的重载集合:
     // compares two objects
     template <typename T> int compare(const T&, const T&);
     // compares elements in two sequences
     template <class U, class V> int compare(U, U, V);
     // plain functions to handle C-style character strings
     int compare(const char*, const char*);
在不同类型上调用这些函数:
     // calls the ordinary function taking const char* parameters
     char ch_arr1[] = "world", ch_arr2[] = "hi";
     compare(ch_arr1, ch_arr2);
理由在于:候选者为将T绑定到char *的函数模板的版本,以及接受const char*实参的普通函数,两个函数都需要将数组转换为指针,所以优先匹配普通函数。////////////////////不是模板形参为引用时,不会发生转换么?????????????
如果使用指针代替数组,即
     char *p1 = ch_arr1, *p2 = ch_arr2;
     compare(p1, p2);
匹配的是模板函数。
假设compare的模板版本用一个T类型形参代替T的const引用,则如果有一个普通类型的数组,则无论传递数组本身还是传递指针,都将调用模板版本。调用非模板版本的唯一途径是在实参是const char或者const char*指针数组的时候。//////////////
195.一般是定义函数模板特化,而不是定义非模板函数去重载模板函数。
196.异常可以是可传给非引用形参的任意类型的对象,这意味着必须能够复制该类型的对象。传递数组或者函数类型实参时,实参自动转换为一个指针,因此不存在函数或者数组类型的异常。
197.处理异常时,抛出异常的块中0的局部存储不存在了,对于局部对象是类类型的,自动调用该对象的析构函数,对于内置类型对象,编译器一般不撤销。
198.throw表达式初始化一个称为异常对象的特殊对象,是被抛出的表达式的副本。异常对象由编译器管理,保证驻留在可能被激活的任意catch都可以访问的空间。在完全处理异常后撤销。抛出一个对指针进行解引用的对象可能发生分割,只抛出基类部分。无论对象的实际类型是什么,异常对象的类型都与指针的静态类型想匹配。假如抛出指针本身可能导致指向不存在的内存。这里是针对throw的,对于catch,如果catch字句处理因继承而相关的类型的异常,它应该将自己的形参定义为引用,则catch对象的静态类型可以与catch对象所引用的异常对象的动态类型不同。
199.在为某个异常进行栈展开的时候,析构函数如果又抛出自己未经处理的另一个异常,将会导致调用标准库terminate函数。一般terminate函数将调用abort函数,强制整个程序非正常退出。如果找不到匹配的catch,程序也调用库函数terminate。
200.异常说明符必须是完全类型,前向声明不行。
201.除下面几种可能的区别之外,异常的类型与 catch 说明符的类型必须完全匹配,不允许标准转换,也不允许类类型转换:
• 允许从非 const 到 const 的转换。也就是说,非 const 对象的throw 可以与指定接受 const 引用的 catch 匹配。
• 允许从派生类型型到基类类型的转换。
• 将数组转换为指向数组类型的指针,将函数转换为指向函数类型的适当指针。
202.异常说明符可以是引用,这样对catch将作用于异常对象本身。
203.重新抛出是后面不跟类型或者表达式的throw,只能出现在catch或者从catch调用的函数中,如果在处理代码不活动的时候碰到空throw,就调用terminate函数。重新抛出异常抛出的是原来的异常对象,而不是catch的形参。
204.捕获所有异常catch字句的形式为catch (...) {  };捕获所有异常字句与其他catch字句一起使用时,必须是最后一个,否则,任何跟在它后面的catch字句都将不能被匹配。
205.为了处理来自构造函数初始化式的异常,必须将构造函数编写为函数测试块。如:
    template <class T> Handle<T>::Handle(T *p)
    try : ptr(p), use(new size_t(1))
    {
         // empty function body
    }  catch(const std::bad_alloc &e)
           { handle_out_of_memory(e); }
206.可能存在异常的程序以及分配资源的程序应该使用类来管理那些资源。这样可以保证异常发生时释放资源。
207.异常说明是函数接口的一部分,函数定义和该函数的任意声明必须具有相同的异常说明。如果函数没有指定异常说明,则该函数可以抛出任意类型的异常。如果函数抛出了没有在其异常说明中列出的异常,就调用标准库函数unexpected,默认情况下,unexpected函数调用terminate函数。在编译的时候,编译器不能也不会验证异常说明。对于下面这种情况,抛出异常说明中没有的异常,编译器也不会给出提示:
     void f() throw()          // promise not to throw any exception
     {
         throw exception();    // violates exception specification
     }
如果编译器知道不会抛出任何异常,可以执行被可能抛出异常的代码所抑制的优化。对于继承的函数,假如包含空throw()
208.auto_ptr对象只能保存一个指向对象的指针,并且必须是动态分配的,否则可能导致//////////////////////////,并且不能用于指向动态分配的数组。auto_ptr的接受指针的构造函数为explicit,所以必须使用初始化容器的直接形式来创建auto_ptr对象, auto_ptr<int> pi = new int(1024);是错的。对auto_ptr对象的复制和赋值是破坏性的操作。不能将auto_ptr对象存储在 标准容器中。标准库容器要求在复制或赋值之后两个对象相等。auto_ptr的默认构造函数不指向任何对象,内部指针置为0,对其进行解引用程序将出错,并且没有定义其行为。get成员返回包含在auto_ptr对象中的基础指针。不能用get作为创建其他auto_ptr对象的实参,否则将导致相关指针被删除两次。reset成员用来将auto_ptr对象绑定到其他对象。
209.如果基类析构函数承诺不会抛出任何异常,假如派生类的合成版本不能做出承诺,则必须自己定义派生类的析构函数恢复析构函数的不抛出异常的承诺。
210.派生类的对应的虚函数可以与基类对应的虚函数异常说明不同,但是必须必基类对应虚函数异常说明同样严格或者更加严格。这样可以保证指向基类类型的指针调用派生类虚函数的时候,派生类异常不会增加新的可抛出异常。
211.用另一指针初始化带异常说明的函数的指针,或者给带异常说明的函数指针赋值时,源指针的异常说明必须至少与目标指针一样严格。
212.命名空间不能在函数或者类内部定义。
213.命名空间可以在多个文件中定义,但是名字只在声明名字文件中可见。如果命名空间的一个部分需要定义在另一个文件中的名字,仍然必须声明该名字。可以在命名空间定义的外部定义命名空间成员,但是只有包围成员声明的命名空间可以包含成员的定义,比如本身的命名空间,或者全局作用域,但是不能定义在不相关的命名空间中。
214.未命名的空间的定义局部于特定文件中,不能跨越多个文本文件,但是可以在给定文件中不连续,也可以嵌套在另一命名空间内部,每个文件有自己的未命名空间。未命名的命名空间中定义的名字可以直接使用。如果在文件最外层作用域中定义未命名的命名空间,那么未命名的命名空间中的名字必须与全局作用域中定义的名字不同,如:
     int i;   // global declaration for i
     namespace {
         int i;
     }
     // error: ambiguous defined globally and in an unnested, unnamed namespace
     i = 10;
215.如果头文件定义了未命名的命名空间,那么在每个包含该头文件的文件中,改命名空间中的名字将定义不同的局部实体。可以将名字声明为static,使它们局部于一个文件,但是不这么提倡。
216.类作用域中的using声明局限于被定义类的基类中定义的名字。
217.namespace  primer = cplusplus_primer命名空间别名。
218.using指示不声明命名空间成员名字的别名(using声明是的),它将命名空间成员提升到包含命名空间本身和using指示最近的作用域。比如,假定有命名空间A 和函数 f,二者都在全局作用域中定义。如果 f 有关于 A 的 using 指示,那么,在f 中,将好像 A 中的名字出现在全局作用域中 f 的定义之前一样:
    // namespace A and function f are defined at global scope
    namespace A {
        int i, j;
    }
    void f()
    {
        using namespace A;      // injects names from A into the global scope
        cout << i * j << endl; // uses i and j from namespace A
        //...
    }
看一个例子:
    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
    }
219.using指示有用的一种情况是,用在命名空间本身的实现文件中。
220.由using指示引起的二义性错误只能在使用处检测,由using声明引起的二义性错误在声明而不是使用点检测。所以using声明导致的错误更易检测。
221.
    namespace A {
        int i;
        namespace B {
            int i;        // hides A::i within B
            int j;
            int f1()
            {
                int j;    // j is local to f1 and hides A::B::j
                return i; // returns B::i
            }
        } // namespace B is closed and names in it are no longer visible
        int f2() {
           return j;     // error: j is not defined
        }
        int j = i;      // initialized from A::i
    }
 
    namespace A {
        int i;
        int k;
        class C1 {
        public:
            C1(): i(0), j(0) { }   // ok: initializes C1::i and C1::j
            int f1()
            {
                 return k;        // returns A::k
            }
            int f2()
            {
                return h;        // error: h is not defined
            }
            int f3();
        private:
           int i;                // hides A::i within C1
           int j;
        };
        int h = i;               // initialized from A::i
     }
     // member f3 is defined outside class C1 and outside namespace A
     int A::C1::f3()
     {
         return h;               // ok: returns A::h
     }
222.屏蔽命名空间名字规则的一个重要例外,接受类类型形参(或类类型指针及引用形参)的函数(包括重载操作符),以及与类本身定义在同一命名空间中的函数(包括重载操作符),在用类类型对象(或类类型的引用及指针)作为实参的时候是可见的。考虑下面的简单程序:
    std::string s;
    cin >> s;
如果没有查找规则的这个例外,我们将必须编写下面二者之一:
    using std::operator>>;        // need to allow cin >> s
    std::operator>>(std::cin, s); // ok: explicitly use std::>>
223.一个例子:
    namespace A {
        class C {
            friend void f(const C&); // makes f a member of namespace A
        };
    }
因为该友元接受类类型实参并与类隐式声明在同一命名空间中,所以使用它时可以无须使用显式命名空间限定符:
    // f2 defined at global scope
    void f2()
    {
         A::C cobj;
         f(cobj); // calls A::f
    }
命名空间对函数匹配有两个影响。一个影响是明显的:using 声明或 using 指示可以将函数加到候选集合。另一个影响则微妙得多。有一个或多个类类型形参的函数的名字查找包括定义每个形参类型的命名空间。这个规则还影响怎样确定候选集合,为找候选函数而查找定义形参类(以及定义其基类)的每个命名空间,将那些命名空间中任意与被调用函数名字相同的函数加入候选集合。即使这些函数在调用点不可见,也将之加入候选集合。将那些命名空间中带有匹配名字的函数加入候选集合:
或多个类类型形参的函数的名字查找包括定义每个形参类型的命名空间。这个规则还影响怎样确定候选集合,为找候选函数而查找定义形参类(以及定义其基类)的每个命名空间,将那些命名空间中任意与被调用函数名字相同的函数加入候选集合。即使这些函数在调用点不可见,也将之加入候选集合。将那些命名空间中带有匹配名字的函数加入候选集合:
    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;
    }
display 函数的实参 book1 具有类类型 Bulk_item。display 调用的候选函数不仅是在调用display 函数的地方其声明可见的函数,还包括声明 Bulk_item 类及其基类 Item_base 的命名空间中的函数。命名空间NS 中声明的函数 display(const Item_base&) 被加到候选函数集合中。
224.为了提供命名空间中所定义模板的自己的特化,必须保证在包含原始模板定义的命名空间中定义特化。
225.给定下面的类层次,其中每个类定义了一个默认构造函数,
    class X { ... };
    class A { ... };
    class B : public A { ... };
    class C : private B { ... };
    class D : public X, public C { ... };
如果有,下面转换中哪些是不允许的?
    D *pd = new D;
    (a) X *px = pd; (c) B *pb = pd;
    (b) A *pa = pd; (d) C *pc = pd;
B,C不允许,private
226.如果派生类同时继承两个基类,两个基类都定义了同一个成员,则引入了潜在的二义性,即使其中成员函数中参数列表参数不同,即使函数在一个类中是私有的而在另一个类中是公有或受保护的,也是错误的,原因在于名字查找总是以两个步骤发生:首先编译器找到一个匹配的声明,然后编译器才确定所找到的声明是否合法。但是只有使用该成员时才会出错,可以明确指定使用哪个版本就能避免出错,还可以在派生类中定义成员函数的一个版本,在该版本中选择适当的同名函数。
227.无论虚基类出现在继承层次中任何地方,总是在构造非虚基类之前构造虚基类。
228.虚基类有默认构造函数的情况下,派生类可以不必显示为虚基类提供初始化式。
229.给定下面类层次,
    class Class { ... };
    class Base : public Class { ... };
    class Derived1 : virtual public Base { ... };
    class Derived2 : virtual public Base { ... };
    class MI : public Derived1,
               public Derived2 { ... };
    class Final : public MI, public Class { ... };
 
a. 对于Final对象的定义,构造函数和析构函数的次序是什么?
b. 一个 Final 对象中有几个 Base 子对象?有几个 class 子对象?
c. 下面哪个赋值在编译时有错?
    Base     *pb;      Class     *pc;
    MI       *pmi;     Derived2  *pd2;

    (a) pb = new Class;           (c) pmi = pb;
    (b) pc = new Final;           (d) pd2 = pmi;
答案:Class();
           Base();
           Derived1();
           Derived2();
           MI();
           Class();
           Final();
一个Final对象中只有一个Base子对象,有两个Class子对象。
错误的有(a),(c)
230.对未构造的内存中的对象进行赋值而不是初始化,其行为是未定义的。对许多类而言,这样做引起运行时崩溃。赋值涉及删除现存对象,如果没有现存对象,赋值操作符中的动作就会有灾难性效果。
231.C++ 提供下面两种方法分配和释放未构造的原始内存。
1. allocator 类,它提供可感知类型的内存分配。这个类支持一个抽象接口,以分配内存并随后使用该内存保存对象。(为此限制construct函数只能使用元素类型的复制构造函数。
2. 标准库中的 operator new 和 operator delete,它们分配和释放需要大小的原始的、未类型化的内存。
 C++ 还提供不同的方法在原始内存中构造和撤销对象。
1. allocator 类定义了名为 construct 和 destroy 的成员,其操作正如它们的名字所指出的那样:construct 成员在未构造内存中初始化对象,destroy 成员在对象上运行适当的析构函数。
2. 定位 new 表达式接受指向未构造内存的指针,并在该空间中初始化一个对象或一个数组。
3. 可以直接调用对象的析构函数来撤销对象。运行析构函数并不释放对象所在的内存。
4. 算法 uninitialized_fill 和 uninitialized_copy 像fill 和 copy 算法一样执行,除了它们的目的地构造对象而不是给对象赋值之外。
232.现代 C++ 程序一般应该使用 allocator 类来分配内存,它更安全更灵活。但是,在构造对象的时候,用new 表达式比 allocator::construct 成员更灵活。有几种情况下必须使用 new。
233.当使用 new 表达式
     // new expression
     string * sp = new string("initialized");
的时候,实际上发生三个步骤。首先,该表达式调用名为 operator new 的标准库函数,分配足够大的原始的未类型化的内存,以保存指定类型的一个对象;接下来,运行该类型的一个构造函数,用指定初始化式构造对象;最后,返回指向新分配并构造的对象的指针。
当使用 delete 表达式
     delete sp;
删除动态分配对象的时候,发生两个步骤。首先,对 sp 指向的对象运行适当的析构函数;然后,通过调用名为 operator delete 的标准库函数释放该对象所用内存。
不能重定义new和delete表达式的行为。
234.定位 new 表达式在已分配的原始内存中初始化一个对象,它与 new 的其他版本的不同之处在于,它不分配内存。相反,它接受指向已分配但未构造内存的指针,并在该内存中初始化一个对象。定位new 表达式的形式是:
     new (place_address) type
     new (place_address) type (initializer-list)
定位 new 表达式比 allocator 类的 construct 成员更灵活。定位new 表达式初始化一个对象的时候,它可以使用任何构造函数,并直接建立对象。construct 函数总是使用复制构造函数。当不能使用复制构造函数时,应该使用定位new 表达式。
235.显式调用析构函数的效果是适当地清除对象本身。但是,并没有释放对象所占的内存,如果需要,可以重用该内存空间。调用 operator delete 函数不会运行析构函数,它只释放指定的内存。
236.类成员operator new 函数必须具有返回类型 void* 并接受 size_t 类型的形参。类成员operator delete 函数必须具有返回类型 void。它可以定义为接受单个 void* 类型形参,也可以定义为接受两个形参,即void* 和 size_t 类型。由 delete 表达式用被 delete 的指针初始化void* 形参,该指针可以是空指针。如果提供了 size_t 形参,就由编译器用第一个形参所指对象的字节大小自动初始化 size_t 形参。除非类是某继承层次的一部分,否则形参 size_t 不是必需的。当 delete 指向继承层次中类型的指针时,指针可以指向基类对象,也可以指向派生类对象。派生类对象的大小一般比基类对象大。如果基类有virtual 析构函数,则传给 operator delete 的大小将根据被删除指针所指对象的动态类型而变化;如果基类没有virtual 析构函数,那么,通过基类指针删除指向派生类对象的指针的行为,跟往常一样是未定义的。这些函数隐式地为静态函数,不必显式地将它们声明为static,虽然这样做是合法的。成员 new 和 delete 函数必须是静态的,因为它们要么在构造对象之前使用(operator new),要么在撤销对象之后使用(operator delete),因此,这些函数没有成员数据可操纵。像任意其他静态成员函数一样,new 和delete 只能直接访问所属类的静态成员。
237.CachedObj 类应该只被不是基类的类使用。CachedObj 类在固定大小的自由列表上分配对象,这一事实意味着,继承层次中的类不能使用它来处理内存分配。因继承而相关的类几乎总是定义不同大小的对象,处理这些类的单个分配器,可能必须比这里所实现的这个复杂得多。
238.解释下面每个初始化,指出是否有错误的,如果有,为什么错。
     class iStack {
     public:
         iStack(int capacity): stack(capacity), top(0) { }
     private:
         int top;
         vector<int> stack;
     };
     (a) iStack *ps = new iStack(20);
     (b) iStack *ps2 = new const iStack(15);
     (c) iStack *ps3 = new iStack[ 100 ];
(b),(c)错。
239.可以对值为 0 的指针应用 dynamic_cast,这样做的结果是 0。
     if (Derived *derivedPtr = dynamic_cast<Derived*>(basePtr))
     {
         // use the Derived object to which derivedPtr points
     } else { // BasePtr points at a Base object
         // use the Base object to which basePtr points
     }
因为不存在空引用,所以不可能对引用使用用于指针强制类型转换的检查策略,相反,当转换失败的时候,它抛出一个 std::bad_cast 异常,该异常在库头文件typeinfo 中定义。可以重写前面的例子如下,以便使用引用:
     void f(const Base &b)
     {
        try {
            const Derived &d = dynamic_cast<const Derived&>(b);
        // use the Derived object to which b referred
        } catch (bad_cast) {
            // handle the fact that the cast failed
        }
     }
240.定下面的类层次,其中每个类都定义了 public 默认构造函数和虚析构函数。
     class A { /* ... */ };
     class B : public A { /* ... */ };
     class C : public B { /* ... */ };
     class D : public B, public A { /* ... */ };
 
     (c) B *pb = new D;
         A *pa = dynamic_cast< B* >(pb);
如果 D 和 B 都以 A 为虚基类:如果A不是虚基类,则该转换因有二义性而失败,因为D对象有两个A类子对象,转换后不知指针pa应指向哪个子对象。如果A是虚基类则转换成功。
241.如果需要在派生类增加新的成员函数,但是无法取得基类的源代码,因而无法在基类中增加相应的虚函数,这时,可以在派生类中增加非虚函数。但这样一来,就无法用基类指针来调用f。如果在程序中需要通过基类指针(如使用该继承层次的某个类中所包含的的指向基类对象的指针数据成员p)来调用f,则必须使用dynamic_cast将p转换为指向派生类的指针,才能调用f。也就说,如果无法为基类增加虚函数,就可以使用dynamic_cast代替虚函数。
242.只有当 typeid 的操作数是带虚函数的类类型的对象的时候,才返回动态类型信息。测试指针(相对于指针指向的对象)返回指针的静态的、编译时类型。如果指针p 的值是 0,那么,如果 p 的类型是带虚函数的类型,则 typeid(*p) 抛出一个 bad_typeid 异常;如果p 的类型没有定义任何虚函数,则结果与 p 的值是不相关的。正像计算表达式 sizeof一样,编译器不计算 *p,它使用 p 的静态类型,这并不要求 p 本身是有效指针。
243.解决一个继承体系中类型相等性问题。假定类层次是这样的:
     class Base {
         friend bool operator==(const Base&, const Base&);
     public:
         // interface members for Base
     protected:
         virtual bool equal(const Base&) const;
         // data and other implementation members of Base
     };
     class Derived: public Base {
         friend bool operator==(const Base&, const Base&);
     public:
         // other interface members for Derived
     private:
         bool equal(const Base&) const;
         // data and other implementation members of Derived
     };
相等操作符:
     bool operator==(const Base &lhs, const Base &rhs)
     {
        // returns false if typeids are different otherwise
        // returns lhs.equal(rhs)
        return typeid(lhs) == typeid(rhs) && lhs.equal(rhs);
     }
派生类中的 equal 函数将以相同的方式开始:它们将实参强制转换为类本身的类型。
     bool Derived::equal(const Base &rhs) const
     {
        if (const Derived *dp
                   = dynamic_cast<const Derived*>(&rhs)) {
           // do work to compare two Derived objects and return result
        } else
           return false;
     }
基类中的equal函数
     bool Base::equal(const Base &rhs) const
     {
          // do whatever is required to compare to Base objects
     }
244.type_info类的默认构造函数和复制构造函数以及赋值操作符都定义为 private,所以不能定义或复制type_info 类型的对象。程序中创建 type_info 对象的唯一方法是使用 typeid 操作符。
245.成员指针只应用于类的非 static 成员。static 类成员不是任何对象的组成部分,所以不需要特殊语法来指向static 成员,static 成员指针是普通指针。
246.外围类对嵌套类的成员没有特殊访问权,并且嵌套类对其外围类的成员也没有特殊访问权。
247.嵌套类可以直接引用外围类的静态成员、类型名和枚举成员,当然,引用外围类作用域之外的类型名或静态成员,需要作用域确定操作符。
248.实例化外围类模板的时候,不会自动实例化类模板的嵌套类。像任何成员函数一样,只有当在需要完整类类型的情况下使用嵌套类本身的时候,才会实例化嵌套类
249.嵌套类作用域中的名字查找:考虑下面的类声明:
     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
     };
250.union 可以指定保护标记使成员成为公用的、私有的或受保护的。默认情况下为公有。union 也可以定义成员函数,包括构造函数和析构函数。但是,union 不能作为基类使用,所以成员函数不能为虚数。union 不能具有静态数据成员或引用成员,而且,union 不能具有定义了构造函数、析构函数或赋值操作符的类类型的成员。像其他内置类型一样,默认情况下union 对象是未初始化的。可以用与显式初始化简单类对象一样的方法显式初始化 union 对象。但是,只能为第一个成员提供初始化式。该初始化式必须括在一对花括号中。给union 对象的某个数据成员一个值使得其他数据成员变为未定义的。
251.
     // objects of type TokenValue have a single member,
     // which could be of any of the listed types
     union TokenValue {
         char   cval;
         int    ival;
         double dval;
     };
union 最经常用作嵌套类型,其中判别式是外围类的一个成员:
     class Token {
     public:
         // indicates which kind of value is in val
         enum TokenKind {INT, CHAR, DBL};
         TokenKind tok;
         union {             // unnamed union
             char   cval;
             int    ival;
             double dval;
         } val;              // member val is a union of the 3 listed types
     };
经常使用 switch 语句测试判别式,然后根据 union 中当前存储的值进行处理:
     Token token;
     switch (token.tok) {
     case Token::INT:
         token.val.ival = 42; break;
     case Token::CHAR:
         token.val.cval = 'a'; break;
     case Token::DBL:
         token.val.dval = 3.14; break;
     }
不用于定义对象的未命名 union 称为匿名联合。匿名union 的成员的名字出现在外围作用域中。例如,使用匿名 union 重写的 Token 类如下:
     class Token {
     public:
         // indicates which kind of token value is in val
         enum TokenKind {INT, CHAR, DBL};
         TokenKind tok;
         union {                 // anonymous union
             char   cval;
             int    ival;
             double dval;
         };
     };
因为匿名 union 不提供访问其成员的途径,所以将成员作为定义匿名 union 的作用域的一部分直接访问。重写前面的switch 以便使用类的匿名 union 版本,如下:
     Token token;
     switch (token.tok) {
     case Token::INT:
         token.ival = 42; break;
     case Token::CHAR:
         token.cval = 'a'; break;
     case Token::DBL:
         token.dval = 3.14; break;
     }
匿名 union 不能有私有成员或受保护成员,也不能定义成员函数。
252.局部类的所有成员(包括函数)必须完全定义在类定义体内部,因此,局部类远不如嵌套类有用。类似地,不允许局部类声明 static 数据成员,没有办法定义它们。局部类可以访问的外围作用域中的名字是有限的。局部类只能访问在外围作用域中定义的类型名、static 变量(第 7.5.2 节)和枚举成员,不能使用定义该类的函数中的变量:
     int a, val;
     void foo(int val)
     {
        static int si;
        enum Loc { a = 1024, b };
        // Bar is local to foo
        class Bar {
        public:
            Loc locVal; // ok: uses local type name
            int barVal;
            void fooBar(Loc l = a)         // ok: default argument is Loc::a
            {
               barVal = val;      // error: val is local to foo
               barVal = ::val;    // ok: uses global object
               barVal = si;       // ok: uses static local object
               locVal = b;        // ok: uses enumerator
            }
        };
        // ...
     }
外围函数对局部类的私有成员没有特殊访问权,当然,局部类可以将外围函数设为友元。
可以将一个类嵌套在局部类内部。这种情况下,嵌套类定义可以出现在局部类定义体之外,但是,嵌套类必须在定义局部类的同一作用域中定义。照常,嵌套类的名字必须用外围类的名字进行限定,并且嵌套类的声明必须出现在局部类的定义中:
     void foo()
     {
        class Bar {
        public:
            // ...
            class Nested;    // declares class Nested
        };
        //  definition of Nested
        class Bar::Nested {
            // ...
        };
     }
嵌套在局部类中的类本身是一个带有所有附加限制的局部类。嵌套类的所有成员必须在嵌套类本身定义体内部定义。
253.最好将位域设为unsigned类型。存储在signed类型中的位域的行为由实现定义。地址操作符(&)不能应用于位域,所以不可能有引用类位域的指针,位域也不能是类的静态成员。
254.当可以用编译器的控制或检测之外的方式改变对象值的时候,应该将对象声明为 volatile。关键字volatile 是给编译器的指示,指出对这样的对象不应该执行优化。与定义 const 成员函数相同的方式,类也可以将成员函数定义为volatile,volatile 对象只能调用 volatile 成员函数。
255.能将 volatile 对象传递给普通引用或 const 引用。如果类希望允许复制volatile 对象,或者,类希望允许从 volatile 操作数或对 volatile 操作数进行赋值,它必须定义自己的复制构造函数和/或赋值操作符版本:
     class Foo {
     public:
         Foo(const volatile Foo&);    // copy from a volatile object
         // assign from a volatile object to a non volatile objet
         Foo& operator=(volatile const Foo&);
         // assign from a volatile object to a volatile object
         Foo& operator=(volatile const Foo&) volatile;
         // remainder of class Foo
     };
256.链接指示不能出现在类定义或函数定义的内部,它必须出现在函数的第一次声明上。
257.链接指示与函数重载之间的相互作用依赖于目标语言。在一组重载函数中只能为一个 C 函数指定链接指示。用带给定名字的 C 链接声明多于一个函数是错误的。
258.C 函数的指针与 C++ 函数的指针具有不同的类型,不能将 C 函数的指针初始化或赋值为 C++ 函数的指针(反之亦然)。
.因为链接指示应用于一个声明中的所有函数,所以必须使用类型别名,以便将 C 函数的指针传递给 C++ 函数:
     // FC is a pointer to C function
     extern "C" typedef void FC(int);
     // f2 is a C++ function with a parameter that is a pointer to a C function
     void f2(FC *);



0 0
原创粉丝点击