复习用,第一周目记的要点会多一些
第二章 变量和基本类型
第39页:lvalue 和 rvalue
第43页:初始化不是赋值。初始化指创建变量并给它赋初始值,而赋值则是擦出对象的当前值并用新值代替。
第44页:内置类型变量是否自动初始化取决于变量定义的位置。在函数体外定义的变量都初始化成0,在函数体里定义的内置类型变量不进行自动初始化。……未初始化变量引起的错误难以发现。……永远不要依赖未定义行为。
第45页:如果类具有默认构造函数,那么就可以在定义该类的变量时不用显式地初始化变量。……
有些类类型没有默认构造函数。对于这些类型来说,每个定义都必须提供显式的初始化式。没有初始值是根本不可能定义这种类型的变量的。
第46页:声明和定义之间的区别可能看起来微不足道,但事实上确实举足轻重的
第50页:非const变量默认为extern。要使const变量能够在其他的文件中访问,必须显式地指定它为extern。
第52页:非const引用只能绑定到与该引用同类型的对象。const引用则可以绑定到不同但相关的类型的对象或绑定到右值
利用temporary赋值
第59页:头文件用于声明而不是用于定义
第三章 标准库类型
第70页:因为历史原因以及为了与C语言兼容,字符串字面值与标准库string类型不是同一种类型。
第73页:任何存储string的size操作结果的变量必须为string::size_type类型。特别重要的是,不要把size的返回值赋给一个int变量。
第75页:当进行string对象和字符串字面值混合连接操作时,+操作符的左右操作数必须至少有一个是string类型的
第78页:通常,C++程序中应采用cname这种头文件版本,而不是采用name.h版本,这样,标准库中的名字在命名空间str中保持一致。
第81页:使用size_type类型时,必须指出该类型是那里定义的。vector类型总是包括vector的元素类型:
vector<int>::size_type // ok
vector::size_type // error
C++程序员习惯于有限选用!=而不是<来编写循环判断条件。
为什么?学习完第二部分的泛型编程就会明白这种习惯的合理性
第83页:必须是已存在的元素才能用下标操作符进行索引。通过下标操作进行赋值时,不会添加任何元素。
……
警告:仅能对确知已存在的元素进行下标操作
……
试图获取不存在的元素必然产生运行时错误。……不能确保执行过程可以捕捉到这类错误,运行程序的结果是不确定的。……
本警告适用于任何使用下标操作的时候,……
不幸的是,试图对不存在的元素进行下标操作是程序设计过程中经常会犯的严重错误。所谓的“缓冲区溢出”错误就是对不存在的元素进行下标操作的结果。……
第84页:由end操作返回的迭代器并不指向vector中任何实际的元素,相反,它只是起一个哨兵(sentinel)的作用,表示我们已处理完vector中所有元素。
第88页:任何改变vector长度的操作都会使已存在的迭代器失效。例如,在调用push_back之后,就不能再信赖指向vector的迭代器的值了。
第89页:sring对象和bitset对象之间是反向转化的
第四章 数组和指针
第96页:与vector相比,数组的显著缺陷在于:数组的长度是固定的,而且程序员无法知道一个给定数组的长度。数组没有没有获取其容量大小的size操作,也不提供push_back操作在其中自动添加元素。如果需要更改数组的长度,程序员只能创建一个更大的新数组,然后把原数组的所有元素复制到新数组空间中去。
与使用标准vector类型的程序相比,依赖于内置数组的程序更容易出错而且难于调试。
在出现标准库之前,C++程序大量使用数组保存一组对象。……在将来一段时间之内,原来依赖于数组的程序仍大量存在,因此,C++程序员还是必须掌握数组的使用方法。
第96页:数组的维数必须用值大于等于1的常量表达式定义。此常量表达式只能包含整型字面值常量、枚举常量或者用常量表达式初始化的整型const对象。非const变量以及要到运行阶段才知道其值的const变量都不能用于定义数组的维数。
实践了一下,VC中符合以上描述,g++中却可以用非const整型变量定义数组维数,可能的原因是g++兼容c99标准,而c99标准中允许使用变量定义数组维数。
第99页:……而数组下标的正确类型是size_t……
第100页:导致安全问题的最常见原因是所谓“缓冲区溢出(buffer overflow)”错误。当我们在编程时没有检查下标,并且引用了越出数组或其他类似数据结构边界的元素时,就会导致这类错误。
第102页:避免使用未初始化的指针
很多运行时错误都源于使用了未初始化的指针。
就像使用其他没有初始化的变量一样,使用未初始化的指针时的行为C++标准中并没有定义,它几乎总是会导致运行时崩溃。然而,导致崩溃的这一原因很难发现。
C++语言无法检测指针是否未被初始化,也无法区分有效地址和由指针分配到的存储空间中存放的二进制位形成的地址。建议程序员在使用之前初始化所有的变量,尤其是指针。
如果可能的话,除非所指向的对象已经存在,否则不要定义指针,这样可避免定义一个未初始化的指针。
如果必须分开定义指针和其所指向的对象,则将指针初始化为0。因为编译器可检测出0值的指针,程序可判断该指针并未指向一个对象。
第104页:void*指针只支持几种有限的操作:与另一个指针进行比较;向函数传递void*指针或从函数返回void*指针;给另一个void*指针赋值。不允许使用void*指针操纵它所指向的对象。
第108页:在使用下标访问数组时,实际上是对指向数组元素的指针做下标操作。只要指针指向数组元素,就可以对它进行下标操作:
int ia[] = {0, 2, 4, 6, 8};
int *p = &ia[2]; // ok: p point to the element indexed by 2
int j = p[1]; // ok: p[1] equvalent to *(p + 1),
// p[1] is the same element as ia[3]
int k = p[-2]; // ok: p[-2] is the same element as ia[0]
第111页:在实际的程序中,指向const指针常用用作函数的形参。将形参定义为指向const的指针,以此确保传递给函数的实际对象在函数中不因为形参而被修改
事实上,除非实参定义为const,否则形参无论是用const指针或是const引用,都并不能确保实参对象不被修改,使用强制类型转换获取其地址即可跳过这种约束。如果出现了这种编码,就得考虑程序设计的缺陷和编码人员的沟通不足了。
第112页:下面是一个几乎所有人刚开始都会答错的问题。假设给出以下语句:
typedef string *pstring;
const pstring cstr;
请问cstr变量是什么类型?……很多人都认为真正的类型是:
const string *cstr; // wrong interpretation of const pstring cstr
……但这是错误的。
错误的原因在于将typedef当作文本扩展了。声明const pstring时,const修饰的是pstring的类型,这是一个指针。因此,该声明语句应该是把cstr定义为指向string类型对象的const指针,这个定义等价于:
// cstr is a const pointer to string
string *const cstr; // equivalent to const pstring cstr
第115页:在使用处理C风格字符串的标准库函数时,牢记字符串必须以结束符null结束……
传递给标准库函数strcat和strcpy的第一个实参数组必须具有足够大的空间存放新生成的字符串。……
如果必须使用C风格字符串,则使用标准库函数strncat和strncpy比strcat和strcpy函数更安全:……
如果使用C++标准库类型string,则不存在上述问题:……
对大部分的应用而言,使用标准库类型string,除了增强安全性外,效率也提高了,因此应该尽量避免使用C风格字符串。
第118页:动态分配内存最后必须进行释放,否则,内存最终将会逐渐耗尽。如果不再需要使用动态创建的数组,程序员必须显式地将其占用的存储空间返还给程序的自由存储区。C++语言为指针提供delete [ ]表达式释放指针所指向的数组空间:
delete [ ] pia;
该语句回收了pia所指的数组,把相应的内存返还给自由存储区。在关键字delete和指针之间的空方括号对是必不可少的:它告诉编译器该指针指向的是自由存储区中的数组,而并非单个对象。
如果遗漏了空方括号对,这是一个编译器无法发现的错误,将导致程序在运行时出错。
理论上,回收数组时缺少空方括号对,至少会导致运行时少释放了内存空间,从而产生内存泄漏(memory leak)。对于某些系统和/或元素类型,有可能会带来更严重的运行时错误。因此,在释放动态数组时千万别忘了方括号对。
第122页:严格地说,C++中没有多维数组,通常所指的多维数组其实就是数组的数组:
// array of size 3, each element is an array of ints of size 4
int ia[3][4]
……
为了对多维数组进行索引,每一维都需要一个下标。……
如果表达式只提供了一个下标,则结果获取的元素是该行下标索引的内层数组。如ia[2]将获得ia数组的最后一行,即这一行的内层数组本身,而并非该数组中的任何元素。
int (*p)[4] = ia;
int *q = *p;
++p; // 步进为4
++q; // 步进为1
第132页:逻辑与和逻辑或操作符总是先计算其左操作数,然后再计算其右操作数。只有在仅靠左操作数的值无法确定该逻辑表达式的结果时,才会求解其右操作数。我们常常称这种求值策略为“短路求值(short-circuit evaluation)”。
第140页:建议:只有在必要时才使用后置操作符
……道理很简单:因为前置操作需要做的工作更少,只需加1后返回加1后的结果即可。而后置操作符则必须先保存操作数原来的值,以便返回未加1之前的值作为操作的结果。……因此,养成使用前置操作这个好习惯,就不必操心性能差异的问题。
第143页:条件操作符的优先级相当低。当我们要在一个更大的表达式中嵌入条件表达式时,通常必须用圆括号把条件表达式括起来。
第144页:对数组做sizeof操作等效于将对其元素类型做sizeof操作的结果乘上数组元素的个数。……
// sizeof(ia)/sizeof(*ia) returns the number of elements in ia
int sz = sizeof(ia)/sizeof(*ia);
第145页:逗号表达式的结果是其最右边表达式的值。
第150页:一个表达式里,不要在两个或更多的子表达式中对同一对象做自增或自减操作。
第152页:一旦删除了指针所指向的对象,立即将指针置为0,这样就非常清楚地表明指针不再指向任何对象。
第154页:下面三种常见的程序错误都与动态内存分配相关:
(1)删除(delete)指向动态分配内存的指针失败,因而无法将该块内存返还给自由存储区。
(2)读写已删除的对象。如果删除指针所指向的对象之后,将指针置为0值,则比较容易检测出这类错误。
(3)对同一个内存空间使用两次delete表达式。
操纵动态分配内存时,很容易发生上述错误,但这些错误却难以跟踪和修正。
第160页:建议:避免使用强制类型转换
强制类型转换关闭了正常的类型检查。强烈建议程序员避免使用强制类型转换,不依赖强制类型转换也能写出很好的C++程序。
第六章 语句
第七章 函数
第196页:函数的运行以形参的(隐式)定义和初始化开始。
第198页:参数名是可选的,但在函数定义中,通常所有参数都要命名。参数必须在命名后才能使用。
第203页:如果使用引用形参的唯一目的是避免复制实参,则应将形参定义为const引用。
第204页:问题的关键是非const引用形参只能与完全同类型的非const对象关联。
应该将不修改相应实参的形参定义为const引用。如果将这样的形参定义为非const引用,则毫无必要地限制了该函数的使用。
……
应该将不需要修改的引用形参定义为const引用。普通的非const引用形参在使用时不太灵活。这样的形参既不能用const对象初始化,也不能用字面值或产生右值的表达式实参初始化。
第206页:因为数组不能复制,所以无法编写使用数组类型形参的函数。因为数组会被自动转化为指针,所以处理数组的函数通常通过操纵指向数组中的元素的指针来处理数组。
……
虽然不能直接传递数组,但是函数的形参可以写成数组的形式。虽然形参表示方式不同,但可将使用数组语法定义的形参看作数组元素类型的指针。……
通常,将数组形参直接定义为指针要比使用数组语法定义更好。这样就明确地表示,函数操纵的是指向数组元素的指针,而不是数组本身。由于忽略了数组长度,形参定义中如果包含了数组长度则特别容易引起误解。
第207页:当编译器检查数组形参关联的实参时,它只会检查实参是不是指针、指针的类型和数组元素的类型是否匹配,而不会检查数组的长度。
第208页:和其他类型一样,数组形参可声明为数组的引用。如果形参是数组的引用,编译器不会将数组实参转化为指针,而是传递数组的引用本身。在这种情况下,数组大小成为形参和实参类型的一部分。编译器检查数组实参的大小与形参的大小是否匹配……
第208页:和其他数组一样,多维数组以指向0号元素的指针方式传递。多维数组的元素本身就是数组,除了第一维意外的所有维的长度都是元素类型的一部分,必须明确指定……
我们也可以用数组语法定义多维数组。与一维数组一样,编译器忽略第一维的长度,所以最好不要把它包括在形参表内……
第209页:任何处理数组的程序都要确保程序停留在数组的边界内。
有三种常见的编程技巧确保函数的操作不超出数组实参的边界。第一种方法是在数组本身放置一个标记来检测数组的结束。……
第二种方法是传递指向数组第一个和最后一个元素的下一个位置的指针。……
第三种方法是将第二个形参定义为表示ishuzu的大小,这种用法在C程序和标准化之前的C++程序中十分普遍。
第212页:返回类型为void的函数通常不能使用第二种形式的return语句,但是,它可以返回另一个返回类型同样是void的函数的调用结果……
返回任何其他表达式的尝试都会导致编译时的错误。
第213页:在含有return语句的循环后没有提供return语句是很危险的,因为大部分的编译器不能检测出这个漏洞,运行时会出现什么问题是不确定的。
第214页:理解返回引用至关重要的是:千万不能返回局部变量的引用。
当函数执行完毕时,将释放分配给局部对象的存储空间。此时,对局部对象的引用就会指向不确定的内存。……
确保返回引用安全的一个好方法是:请自问,这个引用指向哪个在此之前存在的对象?
写的还挺萌……
第215页:返回引用的函数返回一个左值。因此,这样的函数可用于任何要求使用左值的地方……
给函数返回值赋值可能让人惊讶,由于函数返回的是一个引用,因此这是正确的,该引用是被返回元素的同义词。
入托不希望引用返回值被修改,返回值应该声明为const……
char &get_val(string &str, string::size_type ix)
{
return str[ix];
}
int main()
{
string s("a value");
cout << s << endl; // prints a value
get_val(s, 0) = 'A'; // changes s[0] to A
cout << s << endl; // prints A value
return 0;
}
第215页:千万不要返回指向局部对象的指针
……和返回局部对象的引用一样,返回指向局部对象的指针也是错误的。一旦函数结束,局部对象被释放,返回的指针就变成了指向不再存在的对象的悬垂指针。
第217页:把函数声明直接放到每个使用该函数的源文件中,这可能是大家希望的方式,而且也是合法的。但问题在于这种用法比较呆板而且容易出错。解决的方法是把函数声明放在头文件中,这样可以确保对于指定函数其所有声明保持一致。如果函数接口发生变化,则只要修改其唯一的声明即可。
第218页:默认实参是通过给形参表中的形参提供明确的初始值来指定的。程序员可为一个或多个形参定义默认值。但是,如果有一个形参具有默认实参,那么,它后面所有的形参都必须有默认实参。
第219页:既可以在函数声明也可以在函数定义中指定默认实参。但是,在一个文件中,只能为一个形参指定默认实参一次。……
通常,应在函数声明中指定默认实参,并将该声明放在合适的头文件中。
如果在函数定义的形参表中提供默认实参,那么只有在包含该函数定义的源文件中调用该函数时,默认实参才是有效的。
第221页:为这样的小操作定义一个函数的好处是:
#阅读和理解函数shortString的调用,要比读一条用等价的条件表达式取代函数调用表达式并解释它的含义要容易得多。
#如果需要做任何修改,修改函数要比找出并修改每一处等价表达式容易得多。
#使用函数可以确保统一的行为,每个测试都保证以相同的方式实现。
#函数可以重用,不必为其他应用重写代码。
但是,将shorterString写成函数有一个潜在的缺点:调用函数比求解等价表达式要慢得多。
第222页:内联函数应该在头文件中定义,这一点不同于其他函数。
内敛函数的定义对编译器而言必须是可见的,以便编译器能够在调用点内联展开该函数的代码。此时,仅有函数原型是不够的。
……
在头文件中加入或修改内联函数时,使用了该头文件的所有源文件都必须重新编译。
第223页:编译器隐式地将类内定义的成员函数当作内联函数。
第226页:在冒号和花括号之间的代码称为构造函数初始化列表。构造函数的初始化列表为类的一个或多个数据成员指定初值。它跟在构造函数的形参表之后,以冒号开头。构造函数的初始化式是一系列成员名,每个成员后面是括在圆括号中的初始值。多个成员的初始化用逗号分隔。
第227页:如果没有为一个类显式定义任何构造函数,编译器将自动为这个类生成默认构造函数。
由编译器创建的默认构造函数通常称为合成的默认构造函数,它将依据如同变量初始化的规则来初始化类中所有成员。……
合成的默认构造函数一般适用于仅包含类类型成员的类。而对于含有内置类型或复合类型成员的类,则通常应该定义他们自己的默认构造函数初始化这些成员。
第229页:虽然,对于通常的操作,函数重载能避免不必要的函数命名(和名字记忆),但很容易就会过分使用重载。在一些情况下,使用不同的函数名能提供较多的信息,使程序易于理解。
第231页:在C++中,名字查找发生在类型检查之前。
第234页:在实际应用中,调用重载函数时应尽量避免对实参做强制类型转换:需要使用强制类型转换意味着所设计的形参集合不合理。
第235页:重载和const形参
仅当形参为引用或指针时,形参是否为const才有影响。
第237页:在引用函数名但又没有调用该函数时,函数名将被自动解释为指向函数的指针。……
此时,直接引用函数名等效于在函数名上应用取地址符……
第238页:函数可以返回指向函数的指针,但是,正确写出这种返回类型相当不容易……
阅读函数指针声明的最佳方法是从声明的名字开始由里而外理解。
……
使用typedef可使该定义更简洁易懂……
允许将形参定义为函数类型,但函数的返回类型则必须是指向函数的指针,而不能是函数。
最后一句话没有翻译好,第一个分句和后面分句没有什么关系。要表达的意思如下:
函数类型可以用作形参类型。函数的返回类型只能是指向函数的指针,而不能是函数类型。
第239页:C++语言允许使用函数指针指向重载的函数……
指针的类型必须与重载函数的一个版本精确匹配。如果没有精确匹配的函数,则对该指针的初始化或赋值都将导致编译错误……
第八章 标准IO库
第246页:出于某些原因,标准库类型不允许做复制或赋值操作。……
这个要求有两层特别重要的含义。……只有支持复制的元素类型才能存储在vector或其他容器类型里。由于流对象不能复制,因此不能存储在vector(或其他)容器中(即不存在存储流对象的vector或其他容器)。
第二个含义是:形参或返回类型也不能为流类型。如果需要传递或返回IO对象,则必须传递指向该对象的引用或指针……
第248页:badbit标志着系统级的故障,如无法恢复的读写错误。……如果出现的是可恢复的错误,如在希望获得数值型数据时输入了字符,此时则设置failbit标志,这种导致设置failbit的问题通常是可以修正的。eofbit是在遇到文件结束符时设置的,此时同时还设置了failbit。
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
}
第249页:下面几种情况将导致缓冲区的内容被刷新,即写入到真实的输出设备或者文件:
(1)程序正常结束。……
(2)在一些不确定的时候,缓冲区可能已经满了,在这种情况下,缓冲区将会在写下一个值之前刷新。
(3)用操纵符显式地刷新缓冲区,例如行结束符endl。
(4)在每次输出操作执行完后,用unitbuf操纵符设置流的内部状态,从而清空缓冲区。
(5)可将输出流与输入流关联(tie)起来。在这种情况下,在读输入流时将刷新其关联的输出缓冲区。
……
为了确保用户看到程序实际上处理的所有输出,最好的方法是保证所有的输出操作都显式地调用了flush或endl。
cout << "hi!" << flush; // 清空缓冲区
cout << "hi!" << ends; // 插入null,然后清空缓冲区
cout << "hi!" << endl; // 插入换行符,然后清空缓冲区
// 下面两句等价
cout << unitbuf << "first" << "second" << nounitbuf;
cout << "first" << flush << "second" << flush;
第253页:关闭流并不能改变流对象的内部状态。……
如果打算重用已存在的流对象,那么while循环必须在每次循环时记得关闭(close)和清空(clear)文件流……
第255页:从效果来看,为ofstream对象指定out模式等效于同时指定了out和trunc模式。
第258页:stringstream对象的一个常见用法是,需要在多种数据类型之间实现自动格式化时使用该类类型。
第九章 顺序容器
第267页:容器元素类型必须满足以下两个约束:
元素类型必须支持赋值运算。
元素类型的对象必须可以复制。
第268页:必须用空格隔开两个相邻的>符号,以示这是两个分开的符号,否则,系统会认为>>是单个符号,为右移操作符,并结果导致编译时错误。
第269页:list容器的迭代器既不支持算术运算(加法或减法),也不支持关系运算(<=, <, >=, >),它只提供前置和后置的自增、自减运算以及相等(不等)运算。
第270页:迭代器first和last如果满足以下条件,则可形成一个迭代器范围:
#它们指向同一个容器中的元素或超出末端的下一位置。
#如果这两个迭代器不相等,则对first反复做自增运算必须能够到达last。换句话说,在容器中,last绝对不能位于first之前。
编译器自己不能保证上述要求。编译器无法知道迭代器所关联的是哪个容器,也不知道容器内有多少个元素。若不能满足上述要求,将导致运行时未定义的行为。
第271页:使用迭代器编写程序时,必须留意哪些操作会使迭代器失效。使用无效迭代器会导致严重的运行时错误。
第276页:任何insert或push操作都可能导致迭代器失效。当编写循环将元素插入vector或deque容器中时,程序必须确保迭代器在每次循环后都得到更新。
第276页:不要存储end操作返回的迭代器。添加或删除deque或vector容器内的元素都会导致存储的迭代器失效。
第279页:resize操作可能会使迭代器失效。
第280页:使用越界的下标,或调用空容器的front或back函数,都会导致程序出现严重的错误。
第281页:使用erase操作删除单个元素必须确保该元素确实存在——如果删除指向超出末端的下一位置的迭代器,那么erase操作的行为未定义。
第283页:由于assign操作首先删除容器中原来存储的所有元素,因此,传递给assign函数的迭代器不能指向调用该函数的容器内的元素。
第289页:在某些方面,可将string类型视为字符容器。……string类型与vector类型不同的是,它不支持以栈方式操纵容器:在string类型中不能使用front、back和pop_back操作。
第296页:find操作的返回类型是string::size_type,请使用该类型的对象存储find的返回值。
第301页:priority_queue允许用户为队列中存储的元素设置优先级。这种队列不是直接将新元素放置在队列尾部,而是放在比它优先级低的元素前面。
第十章 关联容器
第308页:“容器元素根据键的次序排列”这一事实就是一个重要的结论:在迭代遍历关联容器时,我们可确保按 键的顺序访问元素,而与元素在容器中存放的顺序无关。
第309页:在使用关联容器时,它的键不但有一个类型,而且还有一个相关的比较函数。默认情况下,标准库使用键类型定义的<操作符来实现键的比较。……
所用的比较函数必须在键类型上定义严格弱排序(strict weak ordering)。……
在实际应用中,键类型必须定义<操作符,而且该操作符应能“正确地工作”,这一点很重要。
……
对于键类型,唯一的约束就是必须支持<操作符,至于是否支持其他的关系或相等运算,则不作要求。
第310页:在学习map的接口时,需谨记value_type是pair类型,它的值成员可以修改,但键成员不能修改。
第311页:使用下标访问map与使用下标访问数组或vector的行为截然不同:用下标访问不存在的元素将导致在map容器中添加一个新的元素,它的键即为该下标值。
第312页:有别于vector或string类型,map下标操作符返回的类型与对map迭代器进行解引用获得的类型不相同。
显然,map迭代器返回value_type类型的值——包含const key_type和mapped_type类型成员的pair对象;下标操作符则返回一个mapped_type类型的值。
第315页:下标操作符给出了读取一个值的最简单方法……
但是,使用下标存在一个很危险的副作用:如果该键不在map容器中,那么下标操作会插入一个具有该键的新元素。
这样的行为是否正确取决于程序员的意愿。
……
map容器提供了两个操作:count和find,用于检查某个键是否存在而不会插入该键。
第319页:除了两种例外情况,set容器支持大部分的map操作,包括下面几种:
……
两种例外包括:set不支持下标操作符,而且没有定义mapped_type类型。
第十一章 泛型算法
第338页:泛型算法本身从不执行容器操作,只是单独以来迭代器和迭代器操作实现。
第342页:对指定数目的元素做写入运算,或者写到目标迭代器的算法,都不检查目标的大小是否足以存储要写入的元素。
第345页:算法不直接修改容器的大小。如果需要添加或删除元素,则必须使用容器操作。
第348页:只有当容器提供push_front操作时,才能使用front_inserter。在vector或其他没有push_front运算的容器上使用front_inserter,将产生错误。
第352页:流迭代器有下面几个重要的限制:
#不可能从ostream_iterator对象读入,也不可能写到istream_iterator对象中。
#一旦给ostream_iterator对象赋了一个值,写入就提交了。赋值后,没有办法再改变这个值。此外,ostream_iterator对象中每个不同的值都只能正好输出一次。
ostream_iterator没有->操作符。
第355页:反向迭代器用于表示范围,而所表示的范围是不对称的,这个事实可推导出一个重要的结论:使用普通的迭代器对反向迭代器进行初始化或赋值时,所得到的迭代器并不是指向原迭代器所指向的元素。
第357页:map、set和list类型提供双向迭代器,而string、vector和deque容器上定义的迭代器都是随机访问迭代器,用作访问内置术组元素的指针也是随机访问迭代器。istream_iterator是输入迭代器,而ostream_iterator则是输出迭代器。
尽管map和set类型提供双向迭代器,但关联容器只能使用算法的一个子集。问题在于:关联容器的键是const对象。因此,关联容器不能使用任何写序列元素的算法。只能使用与关联容器绑在一起的迭代器来提供用于读操作的实参。
在处理算法时,最好将关联容器上的迭代器视为支持自减运算的输入迭代器,而不是完整的双向迭代器。
第358页:对每一个形参,迭代器必须保证最低功能。将支持更少功能的迭代器传递给函数是错误的;而传递更强功能的迭代器则没问题。
向算法传递无效的迭代器类别所引起的错误,无法保证会在编译时被捕获到。
第359页:调用这些算法时,必须确保输出容器有足够大的容量存储输出数据,这正是通常要使用插入迭代器或者ostream_iterator来调用这些算法的原因。如果使用容器迭代器调用这些算法,算法将假定容器里有足够多个需要的元素。
第362页:对于list对象,应该优先使用list容器特有的成员版本,而不是泛型算法。
……
与对应的泛型算法不同,list容器特有的操作能添加和删除元素。
第十二章 类和数据抽象
第370页:另一方面,类的设计者与实现者之间的区别,也反映了应用程序的用户与设计和实现者之间的区别。用户只关心应用程序能否以合理的费用满足他们的需求。同样的,类的使用者只关心它的接口。好的类设计者会定义直观和易用的类接口,而使用者只关心类中影响他们使用的那部分实现。如果类的实现速度太慢或给类的使用者加上负担,则必然引起使用者的关注。在良好设计的类中,只有类的设计者会关心实现。
第一句翻译的有误,应改为:
另一方面,类的设计者与使用者之间的区别……
第372页:除了定义数据和函数成员之外,类还可以定义自己的局部类型名字。
第373页:在声明和定义处指定inline都是合法的。在类的外部定义inline的一个好处是可以使得类比较容易阅读。
像其他inline一样,inline成员函数的定义必须在调用该函数的每个源文件中是可见的。不在类定义体内定义的inline成员函数,其定义通常应放在有类定义的同一头文件中。
第374页:可以声明一个类而不定义它:
class Screen; // declaration of the Screen class
这个声明,有时称为前向声明(forward declaration),在程序中引入了类类型的Screen。在声明之后、定义之前,类Screen是一个不完全类型(incomplete type),即已知Screen是一个类型,但不知道包含哪些成员。
不完全类型智能以有限方式使用。不能定义该类型的对象。不完全类型只能用于定义指向该类性的指针或引用,或者用于声明(而不是定义)使用该类型作为形参类型或返回类型的函数。
在创建类的对象之前,必须完整地定义该类。必须定义类,而不只是声明类,这样,编译器就会给类的对象预定相应的存储空间。同样的,在使用引用或指针访问类的成员之前,必须已经定义类。
第377页:尽管在成员函数内部显式地引用this通常是不必要的,但有一种情况下必须这样做:当我们需要将一个对象作为整体引用而不是引用对象的一个成员时。最常见的情况是在这样的函数中使用this:该函数返回对调用该函数的对象的引用。
第378页:不能从const成员函数返回指向类对象的普通引用。const成员函数只能返回*this作为一个const引用。
第378页:有时(但不是很经常),我们希望类的数据成员(甚至在const成员函数内)可以修改。这可以通过将它们声明为mutable来实现。
第381页:在定义于类外部的成员函数中,形参表和成员函数体都出现在成员名之后。这些都是在类作用域中定义,所以可以不用限定而引用其他成员。……
与形参类型相比,返回类型出现在成员名字前面。如果函数在类定义体之外定义,则用于返回类型的名字在类作用域之外。如果返回类型使用由类定义的类型,则必须使用完全限定名。
class Screen {
public:
typedef std::string::size_type index;
char get(index, index) const;
index get_cursor() const;
private:
std::string contents;
index cursor;
};
char Scren::get(index r, index c) const
{
index row = r * width;
return contents[row + c];
}
inline Screen::index Screen::get_cursor() const
{
return cursor;
}
第382页:按以下方式确定在类成员的声明中用到的名字。
#检查出现在名字使用之前的类成员的声明。
#如果第1步查找不成功,则检查包含类定义的作用域中出现的声明以及出现在类定义之前的声明。
第383页:按以下方式确定在成员函数中的函数体中用到的名字。
#首先检查成员函数局部作用域中的声明。
#如果在成员函数中找不到该名字的声明,则检查对所有类成员的声明。
#如果在类中找不到该名字的声明,则检查在此成员函数定义之前的作用域中出现的声明。
第387页:与其他函数不同的是,构造函数也可以包含一个构造函数初始化列表……
构造函数的初始化列表以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个数据成员后面跟一个放在圆括号中的初始化式。……与任意的成员函数一样,构造函数可以定义在类的内部或外部。构造函数初始化式只在构造函数的定义中而不是声明中指定。
构造函数初始化列表是许多相当有经验的C++程序员都没有掌握的一个特性。
……
不管成员是否在构造函数初始化列表中显式初始化,类类型的数据成员总是在初始化阶段初始化。初始化发生在计算阶段开始之前。
第388页:如果没有为类成员提供初始化式,则编译器会隐式地使用成员类型的默认构造函数。如果那个类没有默认构造函数,则编译器尝试使用默认构造函数将会失败。在这种情况下,为了初始化数据成员,必须提供初始化式。
有些成员必须在构造函数初始化列表中进行初始化。对于这样的成员,在构造函数体中对它们赋值不起作用。没有默认构造函数的类类型的成员,以及const或引用类型的成员,不管是哪种类型,都必须在构造函数初始化列表中进行初始化。
第389页:初始化的次序常常无关紧要。然而,如果一个成员是根据其他成员而初始化,则成员初始化的次序是至关重要的。
……
按照与成员声明一致的次序编写构造函数初始化列表是个好主意。此外,尽可能避免使用成员来初始化其他成员。
第391页:我们更喜欢用默认实参,因为它减少代码重复。
第392页:一个类哪怕只定义了一个构造函数,编译器也不会再生成默认构造函数。这条规则的根据是,如果一个类在某种情况下需要控制对象初始化,则该类很可能在所有情况下都需要控制。
只有当一个类没有定义构造函数时,编译器才会自动生成一个默认构造函数。
……
如果类包含内置或复合类型的成员,则该类不应该依赖于合成的默认构造函数。它应该定义自己的构造函数来初始化这些成员。
……
实际上,如果定义了其他构造函数,则提供一个默认构造函数几乎总是对的。通常,在默认构造函数中给成员提供的初始值应该指出该对象是“空”的。
第393页:可以用单个实参来调用的构造函数定义了从形参类型到该类类型的一个隐式转换。
……
这个行为是否我们想要的,依赖于我们认为用户将如何使用这个转换。……
可以通过将构造函数声明为explicit,来防止在需要隐式转换的上下文中使用构造函数……
explicit关键字智能用于类内部的构造函数声明上。在类的定义体外部所做的定义上不再重复它……
当构造函数被声明为explicit时,编译器将不使用它作为转换操作符。
……
通常,除非有明显的理由想要定义隐式转换,否则,单形参构造函数应该为explicit。将构造函数设置为explicit可以避免错误,并且当转换有用时,用户可以显式地构造对象。
第396页:显式初始化类类型对象的成员有三个重大的缺点。
(1)要求类的全体数据成员都是public。
(2)将初始化每个对象的每个成员的负担放在程序员身上。这样的初始化是乏味且易于出错的,因为容易遗忘初始化式或提供不适当的初始化式。
(3)如果增加或删除一个成员,必须找到所有的初始化并正确更新。
定义和使用构造函数几乎总是较好的。当我们为自己定义的类型提供一个默认构造函数时,允许编译器自动运行那个构造函数,以保证每个类对象在初次使用之前正确地初始化。
第398页:友元可以是普通的非成员函数,或前面定义的其他类的成员函数,或整个类。将一个类设为友元,友元类的所有成员函数都可以访问授予友元关系的那个类的非公有成员。
……
当我们将成员函数声明为友元时,函数名必须用该函数所属的类名字加以限定。
……
友元声明将已命名的类或非成员函数引入到外围作用域中。此外,友元函数可以在类的内部定义,该函数的作用域扩展到包围该类定义的作用域。
用友元引入的类名和函数(定义或声明),可以像预先声明的一样使用……
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 intrduced by X
};
第399页:通常,非static数据成员存在于类类型的每个对象中。不像普通的数据成员,static数据成员独立于该类的任意对象而存在;每个static数据成员是与类关联的对象,并不与该类的对象相关联。
……static成员函数没有this形参,它可以直接访问所属类的static成员,但不能直接使用非static成员。
……
使用static成员而不是全局对象有三个优点。
(1)static成员的名字是在类的作用域中,因此可以避免与其他类的成员或全局对象名字冲突。
(2)可以实施封装。static成员可以使私有成员,而全局对象不可以。
(3)通过阅读程序容易看出static成员是与特定类关联的。这种可见性可清晰地显示程序员的意图。
……
可以通过作用域操作符从类直接调用static成员,或者通过对象、引用或指向该类类型对象的指针间接调用。
第400页:当我们在类的外部定义static成员时,无须重复指定static保留字,该保留字只出现在类定义体内部的声明处……
static函数没有this指针
……通过使用非static成员显式或隐式地引用this是一个编译时错误。
因为static成员不是任何对象的组成部分,所以static成员函数不能声明为const。……最后,static成员函数页不能被声明为虚函数。
第401页:static数据成员必须在类定义体的外部定义(正好一次)。不像普通数据成员,static成员不是通过类构造函数进行初始化,而是应该在定义时进行初始化。
保证对象正好定义一次的最好办法,就是将static数据成员的定义放在包含类的非内联成员函数定义的文件中。
……
像使用任意的类成员一样,在类定义体外部引用类的static成员时,必须指定成员是在哪个类中定义的。然而,static关键字只能用于类定义体内部的声明中,定义不能标示为static。
内联函数需要在每个调用处都有定义
第401页:一般而言,类的static成员,像普通数据成员一样,不能在类的定义体中初始化。相反,static数据成员通常在定义时才初始化。
这个规则的一个例外是,只要初始化式是一个常量表达式,整形const static数据成员就可以在类的定义体中进行初始化……
const static数据成员在类的定义体中初始化时,该数据成员仍必须在类的定义体之外进行定义。
第402页:普通成员都是给定类的每个对象的组成部分。static成员独立于任何对象而存在,不是类类型对象的组成部分。因为static数据成员不是任何对象的组成部分,所以它们的使用方式对于非static数据成员而言是不合法的。
例如,static数据成员的类型可以使该成员所属的类类型。……
类似的,static数据成员可用作默认实参……
这两个例子可以对照书看一下
第十三章 复制控制
第406页:复制构造函数、赋值操作符和析构函数总称为复制控制(copy control)。编译器自动实现这些操作,但类也可以定义自己的版本。
……
通常,编译器合成的复制控制函数是非常精练的——它们只做必需的工作。但对某些类而言,依赖于默认定义会导致灾难。……有一种特别常见的情况需要类定义自己的复制控制成员的:类具有指针成员。
第410页:有些类需要完全禁止复制。……如果想要禁止复制,似乎可以省略复制构造函数,然而,如果不定义复制构造函数,编译器将合成一个。
为了防止复制,类必须显式生命其复制构造函数为private。
……
如果想要连友元和成员中的复制也禁止,就可以声明一个(private)复制构造函数但不对其定义。
声明而不定义成员函数是合法的,但是,使用未定义成员的任何尝试将导致链接失败。
第411页:大多数类应定义复制构造函数和默认构造函数
不定义复制构造函数和/或默认构造函数,会严重局限类的使用。不允许复制的类对象只能作为引用传递给函数或从函数返回,它们也不能用作容器的元素。
一般来说,最好显式或隐式定义默认构造函数和复制构造函数。只有不存在其他构造函数时才合成默认构造函数。如果定义了复制构造函数,也必须定义默认构造函数。
第412页:一般而言,如果类需要复制构造函数,它也会需要赋值操作符。
实际上,应将这两个操作符看作一个单元。如果需要其中一个,我们几乎也肯定需要另一个。
第413页:动态分配的对象只有在指向该对象的指针被删除时才撤销。如果没有删除指向动态对象的指针,则不会运行该对象的析构函数,对象就一直存在,从而导致内存泄漏,而且,对象内部使用的任何资源也不会释放。
当对象的引用或指针超出作用域时,不会运行析构函数。只有删除指向动态分配对象的指针或实际对象(而不是对象的引用)超出作用域时,才会运行析构函数。
第413页:许多类不需要显式析构函数,尤其是具有构造函数的类不一定需要定义自己的析构函数。……析构函数通常用于释放在构造函数或在对象生命期内获取的资源。
如果类需要析构函数,则它也需要赋值操作符和复制构造函数,这是一个有用的经验法则。……
析构函数并不仅限于用来释放资源。
第414页:析构函数与复制构造函数或赋值操作符之间的一个重要区别是,即使我们编写了自己的析构函数,合成析构函数仍然运行。
第417页:编写自己的复制构造函数时,必须显式复制需要复制的任意成员。显式定义的复制构造函数不会进行任何自动复制。
第418页:即使对象赋值给自己,赋值操作符的正确工作也非常重要。保证这个行为的通用方法是显式检查对自身的赋值。
第十四章 重载操作符与转换
第431页:2.重载操作符必须具有一个类类型操作数
……
3.优先级和结核性是固定的
……
4.不再具备短路求值特性
……
5.类成员与非成员
大多数重载操作符可以定义为普通非成员函数或类的成员函数。
作为类成员的重载函数,其形参看起来比操作数数目少1。作为成员函数的操作符有一个隐含的this形参,限定为第一个操作数。
第433页:重载逗号、取地址、逻辑与、逻辑或等操作符通常不是好做法。这些操作符具有有用的内置含义,如果我们定义了自己的版本,就不能再使用这些内置含义。
第444页:当一个重载操作符的含义不明显时,给操作去一个名字更好。对于很少用的操作,使用命名函数通常也比用操作符更好。如果不是普通操作,没有必要为简洁而使用操作符。
第435页:#赋值(=)、下标([])、调用(())和成员访问箭头(->)等操作符必须定义为成员,将这些操作符定义为非成员函数将在编译时标记为错误。
#像复制一样,复合赋值操作符通常应定义为类的成员。与赋值不同的是,不一定非得这样做,如果定义非成员复合赋值操作符,不会出现编译错误。
#改变对象状态或与给定类型紧密联系的其他一些操作符,如自增、自减和解引用,通常应定义为类成员。
#对称的操作符,如算术操作符、相等操作符、关系操作符和位操作符,最好定义为普通非成员函数。
第436页:IO操作符必须为非成员函数
……
……否则,左操作数将只能是该类类型的对象……
这个用法与为其他类型定义的输出操作的正常使用方式相反。
第437页:输入操作符>>的重载
……它的第二个形参是对要读入的对象的非const引用,该形参必须为非const,因为输入操作符的目的是将数据读到这个对象中。
更重要但通常重视不够的是,输入和输出操作符有如下区别:输入操作符必须处理错误和文件结束的可能性。
第438页:设计输入操作符时,如果可能,要确定错误恢复措施,这很重要。
第440页:既定义了算术操作符又定义了相关复合赋值操作符的类,一般应使用复合赋值实现算术操作符。
第441页:关系操作符
这个不应该定义<操作符的例子很有趣
第442页:一般而言,赋值操作符与复合赋值操作符应返回左操作数的引用。
第443页:类定义下标操作符时,一般需要定义两个版本:一个为非const成员并返回引用,另一个为const成员并返回const引用。
第445页:重载箭头操作符必须返回指向类类型的指针,或者返回定义了自己的箭头操作符的类类型对象。
箭头操作符会递归应用,详见445-446
第449页:一般而言,最好前缀式和后缀式都定义。只定义前缀式或后缀式的类,将会让习惯于使用两种形式的用户感到奇怪。
第449页:调用操作符和函数对象
参考449-453页
第455页:转换操作符(conversion operator)是一种特殊的类成员函数。它定义将类类型值转变为其他类型的转换。……
转换函数采用如下通用形式:
operator type();
……
转换函数必须是成员函数,不能指定返回类型,并且形参表必须为空。
……
转换函数一般不应该改变被转换的对象。因此,转换操作符通常应定义为const成员。
第456页:类类型转换之后不能再跟另一个类类型转换。如果需要多个类类型转换,则代码将出错。
本章之后的内容是“高级主题”……第一次略读
第458页:如果小心使用,类类型转换可以大大简化类代码和用户代码。如果使用的太过自由,类类型转换会产生令人迷惑的编译时错误,这些错误难以理解而且难以避免
第460页:与使用重载操作符一样,转换操作符的适当使用可以大大简化类设计者的工作并使得类的使用更简单。但是,有两个潜在的缺陷:定义太多转换操作符可能导致二义性代码,一些转换可能利大于弊。
第463页:在调用重载函数时,需要使用构造函数或强制类型转换来转换实参,这是设计拙劣的表现。
第464页:一般而言,函数调用的候选集只包括成员函数或非成员函数,不会两者都包括。而确定操作符的使用时,操作符的非成员和成员版本可能都是候选者。
第465页:正确设计类的重载操作符、转换构造函数和转换函数需要多加小心。尤其是,如果类既定义了转换操作符又定义了重载操作符,容易产生二义性。下面的几条经验规则会有所帮助:
(1)不要定义相互转换的类,即如果类Foo具有接受类Bar的对象的构造函数,不要再为类Bar定义到类型Foo的转换操作符。
(2)避免到内置算术类型的转换。具体而言,如果定义了到算术类型的转换,则
#不要定义接受算术类型的操作符的重载版本。如果用户需要使用这些操作符,转换操作符将转换你所定义的类型的对象,然后可以使用内置操作符。
#不要定义转换到一个以上算术类型的转换。让标准转换提供到其他算术类型的转换。
最简单的规则是:对于那些“明显正确”的,应避免定义转换函数并限制非显式构造函数。
……
既为算术类型提供转换函数,又为同一类类型提供重载操作符,可能会导致重载操作符和内置操作符之间的二义性。
第十五章 面向对象编程
第472页:面向对象编程的关键思想是多态性(polymorphism)。
第473页:在C++中,通过基类的引用(或指针)调用虚函数时,发生动态绑定 。引用(或指针)既可以指向基类对象也可以指向派生类对象,这一事实是动态绑定的关键。用引用(或指针)调用的虚函数在运行时确定,被调用的函数是引用(或指针)所指对象的实际类型所定义的。
第474页:保留字virtual的目的是启用动态绑定。成员默认为非虚函数,对非虚函数的调用在编译时确定。……
基类通常应将派生类需要重定义的任意函数定义为虚函数。
第474页:派生类对基类的public和private成员的访问权限与程序中任意其他部分一样:它可以访问public成员而不能访问private成员。
……protected成员可以被派生类对象访问但不能被该类型的普通用户访问。
……
可以认为protected访问标号是private和public的混合:
#像private成员一样,protected成员不能被类的用户访问。
#像public成员一样,protected成员可被该类的派生类访问。
此外,protected还有另一重要性质:
#派生类只能通过派生类对象访问其基类的protected,派生类对其基类类型对象的protected成员没有特殊访问权限。
第477页:派生类中虚函数的声明必须与基类中的定义方式完全匹配,但有一个例外:返回对基类型的引用(或指针)的虚函数。派生类中的虚函数可以返回基类函数所返回类型的派生类的引用(或指针)。
……
一旦函数在基类中声明为虚函数,它就一直为虚函数,派生类无法改变该函数为虚函数这一事实。派生类重定义虚函数时,可以使用virtual保留字,但不是必须这样做。
第478页:已定义的类才可以用作基类。……
……这一规则暗示着不可能从类自身派生出一个类。
……
基类本身可以是一个派生类……
……最底层的派生类继承其基类的成员,基类又继承自己的积累的成员,如此沿着继承链依次向上。从效果来说,最底层的派生类对象包含其每个直接基类(immediate-base)和间接基类(indirect-base)的子对象。
……
如果需要声明(但并不实现)一个派生类,则声明包含类名但不包含派生列表。
第480页:基类类型引用和指针的关键点在于静态类型(static type,在编译时可知的引用类型或指针类型)和动态类型(dynamic type,指针或引用所绑定的对象的类型,这是仅在运行时克制的)可能不同。
……
引用和指针的静态类型与动态类型可以不同,这是C++用以支持多态性的基石。
……
另一方面,对象是非多台的——对象类型已知且不变。……
只有通过引用或指针调用,虚函数才在运行时确定。只有在这些情况下,直到运行时才知道对象的动态类型。
第481页:只有成员函数中的代码才应该使用作用域操作符覆盖虚函数机制。
……
派生类虚函数调用基类版本时,必须显式使用作用域操作符。如果派生类函数忽略了这样做,则函数调用会在运行时确定并且将是一个自身调用,从而导致无穷递归。
第482页:如果一个调用省略了具有默认值的实参,则所用的值由调用该函数的类型定义,与对象的动态类型无关。通过基类的引用或指针调用虚函数时,默认实参为在基类虚函数声明中指定的值,如果通过派生类的指针或引用调用虚函数,则默认实参是在派生类的版本中声明的值。
在同一虚函数的积累版本和派生类版本中使用不同的默认实参几乎一定会引起麻烦。如果通过基类的引用或指针调用虚函数,但实际执行的是派生类中定义的版本,这时就可能会出现问题。在这种情况下,为虚函数的基类版本定义的默认实参将传给派生类定义的版本,而派生类版本使用不同的默认实参定义的。
#include <iostream>
using namespace std;
class Base {
public:
virtual void print(int k = 5) { cout << "base " << k << endl; }
};
class Derived: public Base {
public:
void print(int k = 10) { cout << "derived " << k << endl; }
};
int main(void) {
Derived d;
Base &p = d;
p.print();
return 0;
}
运行结果为:derived 5
第483页:如果是公用继承(public inheritance),基类成员保持自己的访问级别:基类的public成员为派生类的public成员,基类的protected成员为派生类的protected成员。
如果是受保护继承(protected inheritance),基类的public和protected成员在派生类中为protected成员。
如果是私有继承(private inheritance),基类的所有成员在派生类中为private成员。
第485页:有一种常见的误解认为用struct保留字定义的类与用class定义的类有更大的区别。唯一的不同只是默认的成员保护级别和默认的派生保护级别,没有其他区别……
尽管私有继承在使用class保留字时是默认情况,但这在实践中相对罕见。因为私有继承是如此罕见,通常显式指定private是比依赖于默认更好的办法。显式指定可清楚指出想要私有继承而不是一时疏忽。
第486页:友元关系不能继承。基类的友元对派生类的成员没有特殊访问权限。如果基类被授予友元关系,则只有基类具有特殊访问权限,该基类的派生类不能访问授予友元关系的类。
如果派生类想要将自己成员的访问权授予其基类的友元,派生类必须显式地这样做:基类的友元对从该基类派生的类型没有特殊访问权限。同样,如果基类和派生类都需要访问另一个类,那个类必须特地将访问权限授予基类和每一个派生类。
第489页:要确定到基类的转换是否可访问,可以考虑基类的public成员是否可访问,如果可以,转换是可访问的,否则,转换是不可访问的。
第489页:从基类到派生类的自动转换是不存在的。……
……甚至当基类指针或引用实际绑定到派生类对象时,从基类到派生类的转换也存在限制……
第493页:关键概念:尊重基类接口
构造函数只能初始化其直接基类的原因是每个类都定义了自己的接口。
第494页:如果派生类显式定义自己的复制构造函数或赋值操作符,则该定义将完全覆盖默认定义。被继承类的复制构造函数和赋值操作符负责对基类成分以及类自己的成员进行复制或赋值。
第495页:赋值操作符通常与复制构造函数类似:如果派生类定义了自己的赋值操作符,则操作符必须对基类部分进行显式赋值。
……
赋值操作符必须防止自身赋值……基类操作符将释放左操作数中基类部分的值……
析构函数的工作与复制构造函数和赋值操作符不同:派生类析构函数不负责撤销基类对象的成员。编译器总是显式调用派生类对象基类部分的析构函数。每个析构函数只负责清除自己的成员……
要保证运行时当的析构函数,基类中的析构函数必须为虚函数。
虚析构函数是为了解决这样的一个问题:基类的指针指向派生类对象,并用基类的指针删除派生类对象。
如果某个类不包含虚函数,那一般是表示它将不作为一个基类来使用。当一个类不准备作为基类使用时,使析构函数为虚一般是个坏主意。因为它会为类增加一个虚函数表,使得对象的体积翻倍,还有可能降低其可移植性。
第496页:如果基类为了将析构函数设为虎函数而具有空析构函数,那么,类具有析构函数并不表示也需要赋值操作符或复制构造函数。
即使析构函数没有工作要做,继承层次的根类也应该定义一个虚析构函数。
第496页:在复制控制成员中,只有析构函数应定义为虚函数,构造函数不能定义为虚函数。构造函数实在对象完全构造之前运行的,在构造函数运行的时候,对象的动态类型还不完整。
……
将类的赋值操作符设为虚函数很可能会令人混淆,而且不会有什么用处。
第497页:如果在构造函数或析构函数中调用虚函数,则运行的是为构造函数或析构函数自身类型定义的版本。
第498页:对象、引用或指针的静态类型决定了对象能够完成的行为。甚至当静态类型和动态类型可能不同的时候,就像使用基类类型的引用或指针时可能会发生的,静态类型仍然决定着可以使用什么成员的。
第498页:与基类成员同名的派生类成员将屏蔽对基类成员的直接访问。
……
设计派生类时,只要可能,最好避免与基类成员的名字冲突。
……
在基类和派生类中使用同一名字的成员函数,其行为与数据成员一样:在派生类作用域中派生类成员将屏蔽基类成员。即使函数原型不同,基类成员也会被屏蔽……
第500页:局部作用域中声明的函数不会重载全局作用域中定义的函数,同样,派生类中定义的函数也不重载基类中定义的成员。通过派生类对象调用函数时,实参必须与派生类中定义的版本相匹配,只有在派生类根本没有定义该函数时,才考虑基类函数。
第500页:如果派生类重定义了重载成员,则通过派生类型只能访问派生类中重定义的那些成员。
如果派生类想通过自身类型使用所有的重载版本,则派生类必须要么重定义所有的重载版本,要么一个也不重定义。
有时类需要仅仅重定义一个重载集中某些版本行为,并且想要继承其他版本的含义,在这种情况下,为了重定义需要特化的某个版本而不得不重定义每一个基类版本,可能会令人厌烦。
派生类不用重定义所继承的每一个基类版本,它可以为重载成员提供using声明。一个using声明只能指定一个名字,不能指定形参表,它可以为重载成员提供using声明。
第501页:理解C++中继承层次的关键在于理解如何确定函数调用。确定函数调用遵循以下四个步骤:
(1)首先确定进行函数调用的对象、引用或指针的静态类型。
(2)在该类中查找函数,如果找不到,就在直接基类中查找,如此循着类的继承链往上找,知道找到该函数或者查找完最后一个类。如果不能在类或其相关基类中找到改名字,则调用是错误的。
(3)一旦找到了该名字,就进行常规类型检查,查看如果给定找到的定义,该函数调用是否合法。
(4)假定函数调用合法,编译器就生成代码。如果函数是虚函数且通过引用或指针调用,则编译器生成代码以确定根据对象的动态类型运行哪个函数版本,否则,编译器生成代码直接调用函数。
第503页:将函数定义为纯虚能够说明,该函数为后代类型提供了可以覆盖的接口,但是这个类中的版本绝不会调用。……
含有(或继承)一个或多个纯虚函数的类是抽象基类(abstract base class)。除了作为抽象基类的派生类的对象的组成部分,不能创建抽象类型的对象。
第504页:因为派生类对象在赋值给基类对象时会被“切掉”,所以容器与通过继承相关的类型不能很好地融合。
第504页:C++中面向对象编程的一个颇具讽刺意味的地方是,不能使用对象支持面向对象编程,相反,必须使用指针或引用。……
但是,使用指针或引用会加重类用户的负担。……
C++中一个通用的技术是定义包装(cover)类或句柄(handle)类。句柄类存储和管理基类指针。指针所指对象的类型可以变化,既可以指向基类类型对象又可以指向派生类型对象。用户通过句柄类访问继承层次的操作。因为句柄类使用指针执行操作,虚成员的行为将在运行时根据句柄实际绑定的对象的类型而变化。因此,句柄的用户可以获得动态行为但无须操心指针的管理。
后面的章节是关于文本查询的示例,跳着看了下
第十六章 模板与泛型编程
第526页:面向对象编程所依赖的多态性称为运行时多态性,泛型编程所依赖的多态性称为编译时多态性或参数式多态性——译者注
第527页:模板定义以关键字template开始,后接模板形参表(template parameter list),模板形参表使用间括号括住的一个或多个模板形参(template parameter)的列表,形参之间以逗号分隔。
模板形参表不能为空
第530页:模板形参遵循常规名字屏蔽规则。与全局作用域中声明的对象、函数或类型同名的模板形参会屏蔽全局名字……
用作模板形参的名字不能在模板内部重用……
这一限制还意味着模板形参的名字只能在同一模板形参表中使用一次:
// error: illegal reuse of template parameter name V
template <class V, class V> V calc(const V&, const V&) ;
……
像其他任意函数或类一样,对于模板可以只声明而不定义。声明必须指出函数或类是一个模板……
每个模板类型形参 前面必须带上关键字class或typename,每个非类型形参前面必须带上类型名字,省略关键字或类型说明符是错误的……
第532页:除了定义数据成员或函数成员之外,类还可以定义类型成员。……如果要在函数模板内部使用这样的类型,必须告诉编译器我们正在使用的名字指的是一个类型。必须显式地这样做,因为编译器(以及程序的读者)不能通过检查得知,由类型形参定义的名字何时是一个类型何时是一个值。
……
通过在成员名前加上关键字typename作为前缀,可以告诉编译器将成员当作类型。……
如果拿不准是否需要要以typename指明一个名字是一个类型,那么指定它是个好主意。在类型之前指定typename没有害处,因此,即使typename是不必要的,也没有关系。
第534页:在函数模板内部完成的操作限制了可用于实例化该函数的类型。程序员的责任是,保证用作函数实参的类型支持所用的任意操作,以及保证在模板使用那些操作的环境中那些操作运行正常。
第534页:编写模板代码时,对实参类型的要求尽可能少是有益的。
后面有将这一原则用于compare模板函数的例子
第535页:警告:链接时的编译时错误
第536页:想要使用类模板,就必须显式指定模板实参……
使用函数模板时,编译器通常会为我们推断模板实参……
第539页:可以使用函数模板对函数指针进行初始化或赋值,这样做的时候,编译器使用指针的类型实例化具有适当模板实参的模板版本。
……
获取函数模板实例化的地址的时候,上下文必须是这样的:它允许为每个模板形参确定唯一的类型或值。
看例子
第542页:可以使用显式模板实参的另一个例子是16.2.1节中有二义性的程序,通过使用显式模板实参能够消除二义性……
第544页:export关键字能够指明给定的定义可能会需要在其他文件中产生实例化。在一个程序中,一个模板只能定义为导出一次。……
一般我们在函数模板的定义中指明函数模板为导出的,这是通过在关键字之前包含export关键字而实现的……
第547页:通常,当使用类模板的名字的时候,必须指定模板形参。这一规则有个例外:在类本身的作用域内部,可以使用类模板的非限定名。……
编译器不会为类总使用的其他模板的模板形参进行这样的推断……
第551页:非类型模板实参必须是编译时常量表达式。
第553页:当授予对给定模板的所有实例的访问权的时候,在作用域中不需要存在该类模板或函数模板的声明。实质上,编译器将友元声明也当作类或函数的声明对待。
想要限制对特定实例化的友元关系时,必须在可以用于友元声明之前声明类或函数……
第557页:当在类模板作用域外部定义成员模板的时候,必须包含两个模板形参表……
第560页:通常,可以通过类类型的对象访问类模板的static成员,或者通过使用作用域操作符直接访问成员。当然,当试图通过类使用static成员的时候,必须引用实际的实例化……
第560页:16.5 一个泛型句柄类
这个例子跳过了
第565页:模板特化(template specialization)是这样一个定义,该定义中一个或多个模板形参的实际类型或实际值是指定的。特化的形式如下:
# 关键字template后面接一对空的尖括号(< >);
# 再接模板名和一对尖括号,尖括号中指定这个特化定义的模板形参;
# 函数形参表;
# 函数体。
……
模板特化必须总是包含空模板形参说明符,即template<>,而且,还必须包含函数形参表。如果可以从函数形参表推断模板实参,则不必显式指定模板实参……
第566页:在特化中省略孔的模板形参表template<>会有令人惊讶的结果。如果缺少该特化语法,则结果是声明该函数的重载非模板版本……
现在,重要的是知道,当定义非模板函数的时候,对实参应用常规转换;当特化模板的时候,对实参类型不应用转换。在模板特化版本的调用中,实参类型必须与特化版本函数的形参类型完全匹配,如果不完全匹配,编译器将为实参从模板定义实例化一个实例。
……
如果程序由多个文件构成,模板特化的声明必须在使用该特化的每个文件中出现。不能在一些文件中从泛型模板定义实例化一个函数模板,而在其他文件中为同一模板实参集合特化该函数模板。
与其他函数声明一样,应当在一个头文件中包含模板特化的声明,然后使用该特化的每个源文件包含该头文件。
第568页:值得注意的是,特化可以定义与模板本身完全不同的成员。……
类模板特化应该与它所特化的模板定义相同的接口,否则当用户试图使用未定义的成员时会感到奇怪。
……
在类特化外部定义成员时,成员之前不能加template<>标记。
第569页:16.6.3特化成员而不特化类
……
成员特化的声明与任何其他函数模板特化一样,必须以空的模板形参表开头……
第570页:如果类模板有一个以上的模板形参,我们也许想要特化某些模板形参而非全部。使用类模板的部分特化可以做到这一点。
……
类模板的部分特化(partial specialization)本身也是模板。……
当声明了部分特化的时候,编译器将为实例化选择最特化的模板定义,当没有部分特化可以使用的时候,就使用通用模板定义。……
部分特化的定义与通用模板的定义完全不会冲突。部分特化可以具有与通用模板完全不同的成员集合。类模板成员的通用定义永远不会用来实例化类模板部分特化的成员。
第573页:设计既包含函数模板又包含非模板函数的重载函数集合是困难的,因为可能会使函数的用户感到奇怪,定义函数模板特化几乎总是比使用非模板版本更好。
第十七章 用于大型程序的工具
第581页:异常对象通过复制被抛出表达式的结果创建,该结果必须是可以复制的类型。
第583页:栈展开期间,释放局部对象所用的内存并运行类类型局部对象的析构函数。
……
析构函数应该从不抛出异常
……
不能不处理异常。宜昌市足够重要的、使程序不能继续正常执行的时间。如果找不到匹配的catch,程序就调用库函数terminate。
第584页:异常与catch异常说明符匹配的规则比匹配实参和形参类型的规则更严格,大多数转换都不允许——除下面几种可能的区别之外,异常的类型与catch说明符的类型必须完全匹配:
#允许从非const到const的转换。……
#允许从派生类型到基类类型的转换。
#将数组转换为指向数组类型的指针,将函数转换为指向函数类型的适当指针。
第584页:如果说明符不是引用,就将异常对象复制到catch形参中,catch操作异常对象的副本,对形参所做的任何改变都只作用于副本,不会作用于异常对象本身。如果说明符是引用,则像引用形参一样,不存在单独的catch对象,catch形参只是异常对象的另一名字。对catch形参所做的改变作用于异常对象。
……
通常,如果catch子句处理因继承而相关的类型的异常,它就应该将自己的形参定义为引用。
第586页:一般而言,catch可以改变它的形参。在改变它的形参之后,如果catch重新抛出异常,那么,只有当异常说明符是引用的时候,才会传播那些改变。
第586页:捕获所有异常的catch子句形式为(...)。
……
catch(...)经常与重新抛出表达式结合使用,catch完成可做的所有局部工作,然后重新抛出异常……
如果catch(...)与其他catch子句结合使用,它必须是最后一个,否则,任何跟在它后面的catch子句都将不能被匹配。
第586页:为了处理来自构造函数初始化式的异常,必须将构造函数编写为函数测试块(function try block)。可以使用函数测试块将一组catch语句与函数联成一个整体。
……
构造函数要处理来自构造函数初始化式的异常,唯一的方法是将构造函数编写为函数测试块
template <typename 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); }
第590页:通过定义一个类来封装资源的分配和释放,可以保证正确释放资源。这一技术常称为“资源分配即初始化”,简称RAII。
……
可能存在异常的程序以及分配资源的程序应该使用类来管理那些资源。如本节所述,使用类管理分配和回收可以保证如果发生异常就释放资源。
……
标准库的auto_ptr类是上一节中介绍的异常安全的“资源分配即初始化”技术的例子。
第593页:auto_ptr和内置指针对待复制和赋值有非常关键的重要区别。当复制auto_ptr对象或者将它的值赋给其他auto_ptr对象的时候,将基础对象的所有权从原来的auto_ptr对象转给副本,原来的auto_ptr对象重置为未绑定状态。
……
除了将所有权从右操作数转给左操作数之外,赋值还删除左操作数原来指向的对象——假如两个对象不同。
……
因为复制和赋值是破坏性操作,所以不能将auto_ptr对象存储在标准容器中。
指针的复制和赋值是两个指向同一对象
第594页:应该只用get询问auto_ptr对象或者使用返回的指针值,不能用get作为创建其他auto_ptr对象的实参。
使用get成员初始化其他auto_ptr对象违反auto_ptr类设计原则:在任意时刻只有一个auto_ptr对象保存给指定指针……
第594页:……不能直接将一个地址(或者其他指针)赋给auto_ptr对象……
相反,必须调用reset函数来改变指针……
第595页:(1)不要使用auto_ptr对象保存指向静态分配对象的指针……
(2)永远不要使用两个auto_ptr对象指向同一对象……
(3)不要使用auto_ptr对象保存指向动态分配数组的指针……
(4)不要将auto_ptr对象存储在容器中。
第603页:不能在不相关的命名空间中定义成员
第603页:因为全局命名空间是隐含的,它没有名字,所以记号::member_name引用全局命名空间的成员。
第604页:未命名的命名空间与其他命名空间不同,未命名的命名空间的定义局部于特定文件,从不跨越多个文本文件。
第605页:未命名的命名空间取代文件中的静态声明
……
C++不赞成文件静态声明。不赞成的特征是在未来版本中可能不支持的特征。应该避免文件静态而使用未命名的命名空间代替。
第607页:一个命名空间可以有许多别名,所有别名以及原来的命名空间名字都可以互换使用。
第607页:可以尝试用using指示编写程序,但在使用多个库的时候,这样做会重新引入名字冲突的所有问题。
……
using指示有用的一种情况是,用在命名空间本身的实现文件中。
第609页:警告:避免using指示
这一点我以前没注意过,怎么方便怎么写,要注意养成好习惯。
第614页:为了提供命名空间中所定义模板的自己的特化,必须保证在包含原始模板定义的命名空间中定义特化。
第616页:基类构造函数按照基类构造函数在类派生列表中的出现次序调用。
……
总是按构造函数运行的逆序调用析构函数。
第623页:在虚继承下,对给定虚基类,无论该类在派生层次中作为虚基类出现多少次,只继承一个共享的基类子对象。
第625页:在虚派生中,由最低层派生类的构造函数初始化虚基类。
第626页:无论虚基类出现在继承层次中任何地方,总是在构造非虚基类之前构造虚基类。
第十八章 特殊工具与技术
第632页:如果必须构造预先分配的内存中的对象,就不能有基类型为没有默认构造函数的vector——vector没有办法知道怎样构造这些对象。
这里翻译的不太好,漏了“类型”:……就不能有基类型为没有默认构造函数的类型的vector……
第632页:本节提出的结束不保证使所有程序更快。即使它们确实能改善性能,也可能带来其他开销,如空间的使用或调试困难。最好将优化推迟到已知程序能工作,并且运行时测度指出改进内存分配将解决已知的性能问题的时候。
很中肯,性能优化要推后考虑
第632页:对未构造的内存中的对象进行赋值而不是初始化,其行为是未定义的。对许多类而言,这样做引起运行时崩溃。赋值涉及删除现存对象,如果没有现存对象,赋值操作符中的动作就会有灾难性效果。
第633页:现代C++程序一般应该使用allocator类来分配内存,它更安全更灵活。但是,在构造对象的时候,用new表达式比allocator::construct成员更灵活。有几种情况下必须使用new。
第637页:术语对比:new表达式与operator new函数
标准库函数operator new和operator delete的命名容易让人误解。……
因为new(或delete)表达式与标准库函数同名,所以二者容易混淆。
第637页:operator new和operator delete函数有两个重载版本,每个版本支持相关的new表达式和delete表达式:
void *operator new(size_t);
void *operator new[](size_t);
void *operator delete(void*);
void *operator delete[](void*);
虽然operator new和operator delete函数的设计意图是供new表达式使用,但它们通常是标准库中的可用函数。……
这些函数的表现与allocator类的allocate和deallocate成员类似。但是,它们在一个重要方面有不同:它们在void*指针而不是类型化的指针上进行操作。
勘误:operator delete返回类型是void而不是void指针
第638页:一般而言,使用allocator比直接使用operator new和operator delete函数更为类型安全。
第638页:定位new表达式的形式是:
new (place_address) type
new (place_address) type (initializer-list)
第638页:定位new表达式初始化一个对象的时候,它可以使用任何构造函数,并直接建立对象。construct函数总是使用复制构造函数。
……
通常,这些区别是不相干的……但对某些类而言,使用复制构造函数是不可能的(因为复制构造函数是私有的),或者是应该避免的,在这种情况下,也许有必要使用定位new表达式。
第639页:显式调用析构函数的效果是适当地清除对象本身。但是,并没有释放对象所占的内存,如果需要,可以重用该内存空间。
调用operator delete函数不会运行析构函数,它只释放指定的内存。
第641页:如果new表达式调用全局operator new函数分配内存,则delete表达式也应该调用全局operator delete函数。
第646页:通过运行时类型识别(RTTI),程序能够使用基类的指针或引用来检索这些指针或引用所指对象的实际派生类型。
第647页:使用动态强制类型转换要小心。只要有可能,定义和使用虚函数比直接接管类型管理好得多。
第647页:如果转换到指针类型的dynamic_cast失败,则dynamic_cast的结果是0值;如果转换到引用类型的dynamic_cast失败,则抛出一个bad_cast类型的异常。
第648页:在条件中执行dynamic_cast保证了转换和其结果测试在一个表达式中进行。
第649页:只有当typeid的操作数是带虚函数的类类型的对象的时候,才返回动态类型信息。测试指针(相对于指针指向的对象)返回指针的静态的、编译时类型。
第651页:类型敏感的相等操作符
bool operator==(const Base &lhs, const Base &rhs)
{
// returns flase if typeids are different otherwise
// returns lhs.equal(rhs)
return typeid(lhs) == typeid(rhs) && lhs.equal(rhs);
}
第660页:在看到在类定义体外部定义的嵌套类的实际定义之前,该类是不完全类型,应用所有使用不完全类型的常规限制。
第662页:联合(union)是一种特殊的类。一个union对象可以有多个数据成员,但在任何时刻,只有一个成员可以有值。当将一个值赋给union对象的一个成员的时候,其他所有成员都变为未定义的。
第663页:union不能具有静态数据成员或引用成员,而且,union不能具有定义了构造函数、析构函数或赋值操作符的类类型的成员……
第663页:使用union对象时,我们必须总是知道union对象中当前存储的是什么类型的值……
避免通过错误成员访问union值的最佳办法是,定义一个单独的对象跟踪union中存储了什么值。这个附加对象称为union的判别式(discriminant)。
第664页:因为匿名union不提高访问其成员的途径,所以将成员作为定义匿名union的作用域的一部分直接访问。
……
匿名union不能有私有成员或受保护成员,也不能定义成员函数。
第665页:可以在函数体内部定义类,这样的类称为局部类(local class)。
……
局部类的所有成员(包括函数)必须完全在类定义体内部,因此,局部类远不如嵌套类有用。
第665页:局部类可以访问的外围作用域中的名字是有限的。局部类只能访问在外围作用域中定义的类型名、static变量和枚举成员,不能使用定义该类的函数中的变量……
第669页:链接指示不能出现在类定义或函数定义的内部,它必须出现在函数的第一次声明上。
第671页:在一组重载函数中只能为一个C函数指定链接指示。用带给定名字的C链接声明多于一个函数是错误的……
C函数的指针与C++函数的指针具有不同的类型,不能将C函数的指针初始化或赋值为C++函数的指针(反之亦然)。
(收起)