-
endl 是一个特殊值,称为操纵符,将它写入输出流时,具有输出换行的效果,并刷新与设备相关联的 缓冲区。通过刷新缓冲区,用户可立即看到写入到流中的输出
程序员经常在调试过程中插入输出语句,这些语句都应该刷新输出流。忘记刷新输出流可能会造成输出停留在缓冲区中,如果程序崩溃,将会导致程序错误推断崩溃位置
-
非 const 变量默认为 extern。要使 const 变量能够在其他的文件中访问,必须地指定它为 extern
-
复合类型是指用其他类型定义的类型。如引用;指针是指向某种类型对象的复合数据类型
-
头文件保护符用于避免在已经见到头文件的情况下重新处理该头文件的内容
#ifndef SALESITEM_H #define SALESITEM_H // Definition of Sales_itemclass and related functions goes here #endif
第三章 标准库类型
string
-
任何存储 string 的 size 操作结果的变量必须为 string::size_type 类型。特别重要的是,还要把 size 的返回值赋给一个 int 变量。例如:从概念上讲,赋值操作确实需要做一些工作。它必须先把 st1 占用的相关内存释放掉,然后再分配给 st2 足够存放 st2 副本的内存空间,最后把 st2 中的所有字符复制到新分配的内存空间
string s1 = s2;
-
C 标准库头文件命名形式为 name 而 C++ 版本则命名为 cname ,少了后缀,.h 而在头文件名前加了 c 表示这个头文件源自 C 标准库。因此,cctype 与 ctype.h 文件的内容是一样的,只是采用了更适合 C++程序的形式。特别地,cname 头文件中定义的名字都定义在命名空间 std 内,而 .h 版本中的名字却不是这样。 通常,C++ 程序中应采用 cname 这种头文件的版本,而不采用 name.h 版本,这样,标准库中的名字在命名空间 std 中保持一致。使用 .h 版本会给程序员带来负担,因为他们必须记得哪些标准库名字是从 C 继承来的,而哪些是 C++ 所特有的
-
string 不支持带有单个容器长度作为参数的构造函数
vector
-
vector 不是一种数据类型,而只是一个类模板,可用来定义任意多种数据类型。vector 类型的每一种都指定了其保存元素的类型。因此,vector 和 vector 都是数据类型
vector<string> svec(10); // 10 elements, each an empty string vector<int> ivec4(10, -1); // 10 elements, each initialized to -1
-
C++ 程序员习惯于优先选用 != 而不是 < 来编写循环判断条件。在上例中,选用或不用某种操作符并没有特别的取舍理由。学习完本书第二部分的泛型编程后,你将会明白这种习惯的合理性
for (vector<int>::size_type ix = 0; ix != 10; ++ix) ivec.push_back(ix);
-
由 end 操作返回的迭代器指向 vector 的“末端元素的下一个”。“超出末端迭代器”(off-the-enditerator)。表明它指向了一个不存在的元素。如果 vector 为空,begin 返回的迭代器与 end 返回的迭代器相同。 由 end 操作返回的迭代器并不指向 vector 中任何实际的元素,相反,它只是起一个**哨兵(sentinel)**的作用,表示我们已处理完 vector 中所有元素。
由于 end 操作返回的迭代器不指向任何元素,因此不能对它进行解引用或自增操作。
vector<int>::iterator iter = ivec.end();
-
迭代器类型可使用解引用操作符(dereference operator)(*)来访问迭代器所指向的元素
解引用操作符返回迭代器当前所指向的元素。假设 iter 指向 vector 对象 ivec 的第一元素,那么 *iter 和 ivec[0] 就是指向同一个元素。下面这个语句的效果就是把这个元素的值赋为 0
*iter = 0;
-
const_iterator 的类型,该类型只能用于读取容器内元素,但不能改变其值。
对 const_iterator 类型解引用时,则可以得到一个指向 const 对象的引用,如同任何常量一样,该对象不能进行重写。
可以对此迭代器进行自增以及使用解引用操作符来读取值,但不能对该元素赋值
-
difference_type 两个迭代器相减操作得到的类型
difference_type 的 signed 类型 size_type 的值,这里的 difference_type 是 signed 类型,因为减法运算可能产生负数的结果。该类型可以保证足够大以存储任何两个迭代器对象间的距离
-
任何改变 vector 长度的操作都会使已存在的迭代器失效
标准库 bitset
- string 对象和 bitsets 对象之间是反向转化的:string 对象的最右边字符(即下标最大的那个字符)用来初始化 bitset 对象的低阶位(即下标为 0 的位)。当用 string 对象初始化 bitset 对象时,记住这一差别很重要
- bitset 优于整型数据的低级直接位操作
第四章 数组和指针
指针
- 两个指针减法操作的结果是标准库类型(library type)ptrdiff_t 的数据。与 size_t 类型一样,ptrdiff_t 也是一种与机器相关的类型,在 cstddef 头文件中定义。size_t 是 unsigned 类型,而 ptrdiff_t 则是 signed 整型
1 指向 const 对象的指针
-
cptr的值不能修改 ,可以修改其指针指向。const在*的左边,常用作函数形参
const double *cptr;
2 const 指针
-
curErr的值可以修改,其指针不可修改
int errNumb = 0; int *const curErr = &errNumb; // curErr is a constant pointer
-
指向 const 对象的 const 指针: 既不能修改 pi_ptr 所指向对象的值,也不允许修改该指针的指向
(即 pi_ptr 中存放的地址值)const double pi = 3.14159; // pi_ptr is const and points to a const object const double *const pi_ptr = π
3 指针和 typedef
typedef string *pstring;
const pstring cstr;
//以上声明等价于
// cstr is a const pointer to string
string *const cstr; // equivalent to const pstring cstr
C 风格字符串
- 尽管 C++ 支持 C 风格字符串,但不应该在 C++ 程序中使用这个类型。C 风格字符串常常带来许多错误,是导致大量安全问题的根源
- 字符串字面值的类型就是 const char 类型的数组
- C 风格字符串既不能确切地归结为 C 语言的类型,也不能归结为 C++ 语言的类型,而是以空字符 null(\0) 结束的字符数组
第六章 语句
- 复合语句,通常被称为块,是用一对花括号括起来的语句序列(也可能是空的)。块标识了一个作用域,在块中引入的名字只能在该块内部或嵌套在块中的子块里访问。通常,一个名字只从其定义处到该块的结尾这段范围内可见
switch语句
-
尽管没有严格要求在 switch 结构的最后一个标号之后指定 break 语句,但是,为了安全起见,最好在每个标号后面提供一个 break 语句,即使是最后一个标号也一样。如果以后在 switch 结构的末尾又需要添加一个新的 case 标号,则不用再在前面加 break 语句了
-
哪怕没有语句要在 default 标号下执行,定义 default 标号仍然是有用的。定义 default 标号是为了告诉它的读者,表明这种情况已经考虑到了,只是没什么要执行的
-
对于 switch 结构,只能在它的最后一个 case 标号或 default 标号后面定义变量
需要为某个特殊的 case 定义变量,则可以引入块语句,在该块语句中定义变量,从而保证这个变量在使用前被定义和初始化
goto语句
- 提供了函数内部的无条件跳转,实现从 goto 语句跳转到同一函数内某个带标号的语句
- 从上世纪 60 年代后期开始,不主张使用 goto 语句。goto 语句使跟踪程序控制流程变得很困难,并且使程序难以理解,也难以修改。所有使用 goto 的程序都可以改写为不用 goto 语句,因此也就没有必要使用 goto 语句了
- goto 语句不能跨越变量的定义语句向前跳转!如果确实需要在 goto 和其跳转对应的标号之间定义变量,则定义必须放在一个块语句中
throw / try…catch表达式
-
系统通过 throw 表达式抛出异常。throw 表达式的类型决定了所抛出异常的类型
if (!item1.same_isbn(item2)) throw runtime_error("Data must refer to same ISBN");
runtime_error 类型是标准库异常类中的一种,在 stdexcept 头文件中定义
-
如果不存在处理该异常的 catch 子句或没有try块定义,程序的运行就要跳转到名为 terminate 的标准库函数,该函数在 exception 头文件中定义。这个标准库函数的行为依赖于系统,通常情况下,它的执行将导致程序非正常退出
使用预处理器进行调试
-
NDEBUG 预处理变量实现有条件的调试代码
int main() { #ifndef NDEBUG cerr << "starting main" << endl; #endif // ... __FILE__ //文件名 __LINE__ //当前行号 __TIME__ //文件被编译的时间 __DATE__ //文件被编译的日期
-
assert 预处理宏: 在 cassert 头文件中定义
第七章 函数
-
应该将不需要修改的引用形参定义为 const 引用。普通的非 const 引用形参在使用时不太灵活。这样的形参既不能用 const 对象初始化,也不能用字面值或产生右值的表达式实参初始化。
//普通的非 const 引用形参, 不可以用字面值或产生右值的表达式实参初始化 int incr(int &val) { return ++val; } // const 引用形参, 可以用字面值或产生右值的表达式实参初始化 int incr(const int &val) { return ++val; }
-
通常,将数组形参直接定义为指针要比使用数组语法定义更好。这样就明确地表示,函数操纵的是指向数组元素的指针,而不是数组本身。由于忽略了数组长度,形参定义中如果包含了数组长度则特别容易引起误解。
以下函数形参定义都是等价的,都是等于int*
// three equivalent definitions of printValues void printValues(int*) { /* ... */ } //建议这么使用 void printValues(int[]) { /* ... */ } void printValues(int[10]) { /* ... */ }
当编译器检查数组形参关联的实参时,它只会检查实参是不是指针、指针的类型和数组元素的类型时是否匹配,而不会检查数组的长度。
-
通过引用传递数组: 形参是数组的引用,编译器不会将数组实参转化为指针,而是传递数组的引用本身。在这种情况下,数组大小成为形参和实参类型的一部分。编译器检查数组的实参的大小与形参的大小是否匹配
// ok: parameter is a reference to an array; size of array is fixed void printValues(int (&arr)[10]) { /* ... */ }
printValues 函数只严格地接受含有 10 个 int 型数值的数组,这限制了哪些数组可以传递。然而,由于形参是引用,在函数体中依赖数组的大小是安全的
-
递归函数:主函数 main 不能调用自身
-
自动对象:只有当定义它的函数被调用时才存在的对象称为自动对象。自动对象在每次调用函数时创建和撤销。函数形参也是自动对象。
-
静态局部对象
//这个程序会依次输出 1 到 10(包含 10)的整数 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; }
-
inline 函数放入头文件: 在头文件中加入或修改 inline 函数时,使用了该头文件的所
有源文件都必须重新编译。使用inline定义的内联函数必须将类的声明和内联成员函数的定义都放在同一个文件中,否则编译时无法进行代码的置换。编译器隐式地将在类内定义的成员函数当作内联函数 -
形参与 const 形参的等价性仅适用于非引用形参。
仅当形参是引用或指针时,形参是否为 const 才有影响:
-
有 const 引用形参的函数与有非 const 引用形参的函数是不同的。
-
如果函数带有指向 const 类型的指针形参,则与带有指向相同类型的非 const 对象的指针形参的函数不相同。
-
-
指向函数的指针: 函数类型由其返回类型以及形参表确定,而与函数名无关
// pf points to function returning bool that takes two const string references bool (*pf)(const string &, const string &);
- typedef 简化函数指针的定义
该定义表示 cmpFcn 是一种指向函数的指针类型的名字。该指针类型为“指向返回 bool 类型并带有两个 const string 引用形参的函数的指针”。在要使用这种函数指针类型时,只需直接使用 cmpFcn 即可,不必每次都把整个类型声明全部写出来。
typedef bool (*cmpFcn)(const string &, const string &);
- 函数指针只能通过同类型的函数或函数指针或 0 值常量表达式进行初始化或赋值。
将函数指针初始化为 0,表示该指针不指向任何函数。
- 允许将形参定义为函数类型,但函数的返回类型则必须是指向函数的指针,而不能是函数。
第八章 标准 IO 库
-
IO 对象不可复制或赋值
只有支持复制的元素类型可以存储在 vector 或其他容器类型里。由于流对象不能复制,因此不能存储在 vector(或其他)容器中(即不存在存储流对象的 vector 或其他容器)
形参或返回类型也不能为流类型。如果需要传递或返回 IO 对象,则必须传递或返回指向该对象的指针或引用
-
输出缓冲区的刷新
-
flush: 刷新流,但不在输出中添加任何字符
-
ends: 输出一个换行符并刷新缓冲区
-
endl: 在缓冲区中插入空字符 null,然后后刷新它
cout << "hi!" << flush; // flushes the buffer; adds no data cout << "hi!" << ends; // inserts a null, then flushes the buffer cout << "hi!" << endl; // inserts a newline, then flushes the buffer
- unitbuf 操纵符: 每次执行完写操作后都刷新流
nounitbuf 操纵符: 将流恢复为使用正常的、由系统管理的缓冲区刷新方式
-
-
tie 函数将输入和输出绑在一起
如果一个流调用 tie 函数将其本身绑在传递给 tie 的 ostream 实参对象上,则该流上的任何 IO 操作都会刷新实参所关联的缓冲区
一个 ostream 对象每次只能与一个 istream 对象绑在一起。如果在调用
tie 函数时传递实参 0,则打破该流上已存在的捆绑cin.tie(&cout); // illustration only: the library ties cin and cout for us ostream *old_tie = cin.tie(); cin.tie(0); // break tie to cout, cout no longer flushed when cin is read cin.tie(&cerr); // ties cin and cerr, not necessarily a good idea! // ... cin.tie(0); // break tie between cin and cerr cin.tie(old_tie); // restablish normal tie between cin and cout
-
如果程序员需要重用文件流读写多个文件,必须在读另一个文件之前调用 clear 清除该流的状态
ifstream input; input.open(it->c_str()); if (!input) break; //... input.close(); // close file when we're done with it input.clear();
例如:最后一次对流的读操作到达了文件结束符,事实上该文件结束符对应的是另一个与本文件无关的其他文件,所以读取本文件就失败了
第九章 顺序容器
-
1)顺序容器: vector、list、deque
2)顺序容器适配器: stack、queue、priority_queue
-
接受容器大小做形参的构造函数只适用于顺序容器,而关联容
器不支持这种初始化 -
容器元素类型必须满足以下两个约束: 1)元素类型必须支持赋值运算。 2)元素类型的对象必须可以复制
引用不支持一般意义的赋值运算,因此没有元素是引用类型的容器
-
迭代器运算:++、–、==、!=、解引用*、->
vector 和 deque 容器除了以上的运算,还支持迭代器算术运算除了 == 和 != 之外的关系操作符,例如:+、-、>=、<
-
迭代器范围: [ first, last )或[ begin, end )
-
使用迭代器编写程序时,必须留意哪些操作会使迭代器失效
使用无效迭代器将会导致严重的运行时错误 -
不要存储 end 操作返回的迭代器。添加或删除 deque 或 vector 容器内的元素都会导致存储的迭代器失效
-
C++ 语言只允许两个容器做其元素类型定义的关系运算
-
删除容器内的一个元素或多个
#include <algorithm> string searchValue("Quasimodo"); list<string>::iterator iter = find(slist.begin(), slist.end(), searchValue); if (iter != slist.end()) slist.erase(iter); //删除从迭代器 elem1 开始一直到 elem2 之间的所有元素,但不包括 elem2 指向的元素 slist.erase(elem1, elem2);
-
assign重置元素:
-
如果在不同(或相同)类型的容器内,元素类型不相同但是相互兼容,则其赋值运算必须使用 assign 函数。例如,可通过 assign 操作实现将 vector 容器中一段 char* 类型的元素赋给 string 类型 list 容器
-
由于 assign 操作首先删除容器中原来存储的所有元素,因此,传递给 assign 函数的迭代器不能指向调用该函数的容器内的元素
//重新设置 c 的元素:将迭代器 b 和 e 标记的范围内所有的元素复制到 c 中。b 和 e 必须不是指向 c 中元素的迭代器 c.assign(b,e); //将容器 c 重新设置为存储 n 个值为 t 的元素 c.assign(n,t);
-
-
使用 swap 操作以节省删除元素的成本
关于 swap 的一个重要问题在于:该操作不会删除或插入任何元素,而且保证在常量时间内实现交换。由于容器内没有移动任何元素,因此迭代器不会失效
9.4. vector 容器的自增长
-
弄清楚容器的 capacity(容量)与 size(长度)的区别非常重要
1)size 指容器当前拥有的元素个数
2)而 capacity 则指容器在必须分配新存储空间之前可以存储的元素总数
-
capacity 操作获取在容器需要分配更多的存储空间之前能够存储的元素总数,而 reserve (或resize)操作则告诉 vector 容器应该预留多少个元素的存储空间
9.5. 容器的选用
- 通常来说,除非找到选择使用其他容器的更好理由,否则 vector 容器都是最佳选择
- 如果程序要求随机访问元素,则应使用 vector 或 deque 容器。
- 如果程序必须在容器的中间位置插入或删除元素,则应采用 list 容器。
- 如果程序不是在容器的中间位置,而是在容器首部或尾部插入或删除元
素,则应采用 deque 容器。 - 如果只需在读取输入时在容器的中间位置插入元素,然后需要随机访问元
素,则可考虑在输入时将元素读入到一个 list 容器,接着对此容器重新
排序,使其适合顺序访问,然后将排序后的 list 容器复制到一个 vector
容器
- 如果无法确定某种应用应该采用哪种容器,则编写代码时尝试只使用 vector 和 lists 容器都提供的操作:使用迭代器,而不是下标,并且避免随机访问元素。这样编写,在必要时,可很方便地将程序从使用 vector 容器修改为使用 list 的容器
9.7. 容器适配器
三种顺序容器适配器:queue、priority_queue 和 stack
- 适配器是使一事物的行为类似于另一事物的行为的一种机制。容器适配器让一种已存在的容器类型采用另一种
不同的抽象类型的工作方式实现 - 所有适配器都支持全部关系操作符
第十章 关联容器
关联容器类型: map、set、multimap、multiset
10.1. 引言:pair 类型
- 头文件: utility.h
- 是一种模板类型, 包含两个数据值
- 操作:1)make_pair(v1, v2): 创建。2)操作符<、==。3)first、second:第一、第二个元素。4)默认构造函数 、有参构造函数pair p1(v1, v2)
10.3. map 类型
-
map《K, V>::value_type: 对于 map 容器,其 value_type 是 pair 类型。value_type 是存储元素的键以及值的 pair 类型,而且键为 const。在学习 map 的接口时,需谨记 value_type 是 pair 类型,它的值成员可以修改,但键成员不能修改
-
map 类额外定义了两种类型:key_type 和 mapped_type,以获得键或值的类型
-
以 insert 代替下标运算: 使用 insert 成员可避免使用下标操作符所带来的副作用【不必要的初始化】
使用下标存在一个很危险的副作用:如果该键不在 map 容器中,那么下标操作会插入一个具有该键的新元素
-
使用 count 检查 map 对象中某键是否存在: 对于 map 对象,count 成员的返回值只能是 0 或 1
-
读取元素而不插入该元素
如果希望当具有指定键的元素存在时,就获取该元素的引用,否则就不在容器中创建新元素,那么应该使用 find
int occurs = 0; map<string,int>::iterator it = word_count.find("foobar"); if (it != word_count.end()) occurs = it->second;
10.4. set 、multimap、multiset类型
-
set 中的键也为 const。在获得指向 set 中某元素的迭代器后,只能对其做读操作,而不能做写操作
-
multiset 和 multimap类型则允许一个键对应多个实例.
-
multimap 不支持下标运算,因为在这类容器中,某个键可能对应多个值
10.5.2. 在 multimap 和 multiset 中查找元素
- 迭代遍历 multimap 或 multiset 容器时,可保证依次返回特定键所关联的所有元素
m.lower_bound(k) | 返回一个迭代器,指向键不小于 k 的第一个元素 |
---|---|
m.upper_bound(k) | 返回一个迭代器,指向键大于 k 的第一个元素 |
m.equal_range(k) | 返回一个迭代器的 pair 对象。 它的 first 成员等价于 m.lower_bound(k)。而 second 成员则等价于 m.upper_bound(k) |
// definitions of authors and search_item as above
// pos holds iterators that denote range of elements for this key
pair<authors_it, authors_it> pos = authors.equal_range(search_item);
// loop through the number of entries there are for this author
while (pos.first != pos.second) {
cout << pos.first->second << endl; // print each title
++pos.first;
}
第十一章 泛型算法
- 使用泛型算法必须包含 algorithm 头文件。泛化的算术算法包含numeric头文件
- 输入范围参数的算法两个形参是分别指向要处理的第一个元素和最后一个元素的下一位置的迭代器
- find算法
//find运算 找到返回其迭代器,失败返回end()
vector<int>::const_iterator result = find(vec.begin(), vec.end(), search_value);
11.2. 初窥算法
11.2.1. 只读算法
-
accumulate算法: 在 numeric 头文件中
//将 sum 设置为 vec 的元素之和再加上 42 int sum = accumulate(vec.begin(), vec.end(), 42); //从空字符串开始,把 vec 里的每个元素连接成一个字符串 string sum = accumulate(v.begin(), v.end(), string(""));
- 用于指定累加起始值的第三个实参是必要的,因为 accumulate 对将要累加的元素类型一无所知,因此,除此之外,没有别的办法创建合适的起始值或者关联的类型
-
find_first_of算法 :带有两对迭代器参数来标记两段元素范围,在第一段范围内查找与第二段范围中任意元素匹配的元素,然后返回一个迭代器,指向第一个匹配的元素。如果找不到元素,则返回第一个范围的 end 迭代器
list<string>::iterator it = roster1.begin(); it = find_first_of(it, roster1.end(), roster2.begin(), roster2.end())
11.2.2. 写容器元素的算法
- fill 函数: 写入到输入序列的算法
将该范围内的每个元素都设为给定的值。如果输入范围有效,则可安全写入。这个算法只会对输入范围内已存在的元素进行写入操作
fill(vec.begin(), vec.end(), 0); // reset each element to 0
-
fill_n 函数: 不检查写入操作的算法
- 对指定数目的元素做写入运算,或者写到目标迭代器的算法,都不检查目标的大小是否足以存储要写入的元素
//参数包括:一个迭代器、一个计数器以及一个值 //从迭代器指向的元素开始,将指定数量的元素设置为给定的值 fill_n(vec.begin(), 10, 0);
-
back_inserter函数: 必须包含 iterator 头文件,是迭代器适配器。插入迭代器是可以给基础容器添加元素的迭代器
//如何安全使用写容器的算法 vector<int> vec; // empty vector // ok: back_inserter creates an insert iterator that adds elements to vec fill_n (back_inserter(vec), 10, 0); // appends 10 elements to vec
-
copy 函数:写入到目标迭代器的算法
copy 带有三个迭代器参数:头两个指定输入范围,第三个则指向目标序列的一个元素。传递给 copy 的目标序列必须至少要与输入范围一样大(或者拿back_inserter函数)
-
replace 算法: 一对指定输入范围的迭代器和两个值。每一个等于第一值的元素替换成第二个值
// replace any element with value of 0 by 42 replace(ilst.begin(), ilst.end(), 0, 42);
-
replace_copy算法:
// create empty vector to hold the replacement vector<int> ivec; // use back_inserter to grow destination as needed replace_copy (ilst.begin(), ilst.end(), back_inserter(ivec), 0, 42);
调用该函数后,ilst 没有改变,ivec 存储 ilst 一份副本,而 ilst 内所有的 0 在 ivec 中都变成了 42
11.2.3. 对容器元素重新排序的算法
-
unique算法: "删除"相邻的重复元素,然后重新排列输入范围内的元素,并且返回一个迭代器,表示无重复的值范围的结束
给“删除”加上引号是因为 unique 实际上并没有删除任何元素,而是将无重复的元素复制到序列的前端,从而覆盖相邻的重复元素。unique 返回的迭代器指向超出无重复的元素范围末端的下一位置
-
算法不直接修改容器的大小。如果需要添加或删除元素,则必须使用容器操作
-
谓词: 是做某些检测的函数,返回用于条件判断的类型,指出条件是否成立
-
标准库定义了四种不同的排序算法: 1)sort;2)stable_sort 保留相等元素的原始相对位置;
3)count_if(start, end, condition); //统计符合condition的个数
11.3. 再谈迭代器
11.3.1. 插入迭代器
- back_inserter: 创建使用 push_back 实现插入的迭代器
- front_inserter: 使用 push_front 实现插入。只有当容器提供 push_front 操作时,才能使用
- inserter: 使用 insert 实现插入操作。除了所关联的容器外,inserter还带有第二实参:指向插入起始位置的迭代器,即第二个参数指向位置的前面插入
11.3.2. iostream 迭代器
-
在创建流迭代器时,必须指定迭代器所读写的对象类型:istream_iterator和ostream_iterator
-
限制: 1)不可能从 ostream_iterator 对象读入,也不可能写到 stream_iterator 对象中。
2)一旦给 ostream_iterator 对象赋了一个值,写入就提交了。赋值后,没有办法再改变这个值。此外,ostream_iterator 对象中每个不同的值都只能正好输出一次。
3)ostream_iterator 没有 -> 操作符 -
unique_copy 算法: 这是 unique 的“复制”版本。该算法将
输入范围中不重复的值复制到目标迭代器 -
流迭代器不能反向遍历流,因此流迭代器不能创建反向迭代器
-
使用普通的迭代器对反向迭代器进行初始化或赋值时,所得到的迭代器并不是指向原迭代器所指向的元素
-
5种迭代器:
1)输入迭代器:读,不能写;只支持自增运算;例如泛型算法包括 find 和 accumulate,istream_iterator
2)输出迭代器:写,不能读;只支持自增运算;例如 ostream_iterator
3)前向迭代器:读和写;只支持自增运算;例如泛型算法包括 replace
4)双向迭代器:读和写;支持自增和自减运算;例如泛型算法包括 reverse、reverse_copy;map、set 和 list 类型提供双向迭代器
5)随机访问迭代器:读和写;支持完整的迭代器算术运算;例如泛型算法包括 sort;vector、deque 和
string 迭代器;内置数组元素的指针;string、vector 和 deque 容器上定义的迭代器 -
在处理算法时,最好将关联容器上的迭代器视为支持自减运算的输入迭代器,而不是完整的双向迭代器
关联容器的键是 const 对象。因此,关联容器不能使用任何写序列元素的算法。只能使用与关联容器绑在一起的迭代器来提供用于读操作的实参
-
向算法传递无效的迭代器类别所引起的错误,无法保证会在编译时被捕获到
11.5. 容器特有的算法
- 对于 list 对象,应该优先使用 list 容器特有的成员版本,而不是泛型算法
- 与对应的泛型算法不同,list 容器特有的操作能添加和删除元素
第十二章 类
12.1. 类的定义和声明
-
最简单地说,类就是定义了一个新的类型和一个新作用域
-
在类内部定义的函数默认为 inline(即声明的同时定义)
-
const 成员不能改变其所操作的对象的数据成员。const 必须同时出现在声明和定义中,若只出现在其中一处,就会出现一个编译时错误
double avg_price() const;
-
标准库中的 pair 类就是一个实用的、设计良好的具体类而不是抽象类。具体类会暴露而非隐藏其实现细节
-
注意,C++ 程序员经常会将应用程序的用户和类的使用者都称为“用户”
-
前向声明(forward declaraton)
class Screen; //声明一个类而不定义它
不完全类型(incomplete type)只能以有限方式使用。不能定义该类型的对象。不完全类型只能用于定义指向该类型的指针及引用,或者用于声明(而不是定义)使用该类型作为形参类型或返回类型的函数
-
不能从 const 成员函数返回指向类对象的普通引用。const 成员函数只能返回 *this 作为一个 const 引用
-
mutable可变数据成员: 永远都不能为 const; 要将数据成员声明为可变的,必须将关键字 mutable 放在成员声明之前
-
类成员声明的名字查找:1)检查出现在名字使用之前的类成员的声明;2)如果第 1 步查找不成功,则检查包含类定义的作用域中出现的声明以及出现在类定义之前的声明
函数作用域之后,在类作用域中查找;类作用域之后,在外围作用域中查找
12.4. 构造函数
-
构造函数的工作是保证每个对象的数据成员具有合适的初始值
-
构造函数不能声明为 const:
Sales_item() const; // error
12.4.1. 构造函数初始化式
-
有些成员必须在构造函数初始化列表中进行初始化。对于这样的成员,在构造函数函数体中对它们赋值不起作用,例如:
-
没有默认构造函数的类类型的成员
-
以及 const 或引用类型的成员
-
-
必须对任何 const 或引用类型成员以及没有默认构造函数的类类型的任何成员使用初始化式
-
初始化的次序常常无关紧要。然而,如果一个成员是根据其他成员而初始化,则成员初始化的次序是至关重要的
-
按照与成员声明一致的次序编写构造函数初始化列表是个好主意。此外,尽可能避免使用成员来初始化其他成员
12.4.2. 默认实参与构造函数
- 我们更喜欢使用默认实参,因为它减少代码重复
12.4.3. 默认构造函数
- 为所有形参提供默认实参的构造函数也定义了默认构造函数
- 只有当一个类没有定义构造函数时,编译器才会自动生成一个默认构造函数
- 如果一个类在某种情况下需要控制对象初始化,则该类很可能在所有情况下都需要控制
- 如果类包含内置或复合类型(例如指针引用)的成员,则该类不应该依赖于合成的默认构造函数。它应该定义自己的构造函数来初始化这些成员
- 每个构造函数应该为每个内置或复合类型的成员提供初始化式
- 使用默认构造函数: 1)Sales_item myobj; 2)Sales_item myobj = Sales_item();
12.4.4. 隐式类类型转换
- 当构造函数被声明 explicit 时,编译器将不使用它作为转换操作符
- 通常,除非有明显的理由想要定义隐式转换,否则,单形参构造函数应该为 explicit。将构造函数设置为 explicit 可以避免错误,并且当转换有用时,用户可以显式地构造对象
12.4.5. 类成员的显式初始化
-
有三个重大的缺点
- 要求类的全体数据成员都是 public。
- 将初始化每个对象的每个成员的负担放在程序员身上。这样的初始化是乏
味且易于出错的,因为容易遗忘初始化式或提供不适当的初始化式。 - 如果增加或删除一个成员,必须找到所有的初始化并正确更新。
struct Data { int ival; char *ptr; }; // val1.ival = 0; val1.ptr = 0 Data val1 = { 0, 0 };
12.5. 友元
- 允许一个类将对其非公有成员的访问权授予指定的函数或类;只能出现在类定义的内部
- 友元声明可以出现在类中的任何地方:友元不是授予友元关系的那个类的成员,所以它们不受声明出现部分的访问控制影响
- 通常,将友元声明成组地放在类定义的开始或结尾是个好主意
- 友元声明将已命名的类或非成员函数引入到外围作用域中。此外,友元函数可以在类的内部定义,该函数的作用域扩展到包围该类定义的作用域
12.6. static 类成员
-
全局对象会破坏封装:对象需要支持特定类抽象的实现。类可以定义类 静态成员,而不是定义一个可普遍访问的全局对象
-
static 成员函数没有 this 形参,它可以直接访问所属类的 static 成员,但不能直接使用非 static 成员
static 成员是类的组成部分但不是任何对象的组成部分,因此,static 成员函数没有 this 指针
因为 static 成员不是任何对象的组成部分,所以 static 成员函数不能被声明为 const
static 成员函数也不能被声明为虚函数
-
使用 static 成员而不是全局对象有三个优点:
- static 成员的名字是在类的作用域中,因此可以避免与其他类的成员或
全局对象名字冲突。 - 可以实施封装。static 成员可以是私有成员,而全局对象不可以。
- 通过阅读程序容易看出 static 成员是与特定类关联的。这种可见性可清
晰地显示程序员的意图。
12.6.2. static 数据成员
-
保证对象正好定义一次的最好办法,就是将 static 数据成员的定义放在包含类非内联成员函数定义的文件中
double Account::interestRate = initRate();
-
像使用任意的类成员一样,在类定义体外部引用类的 static 成员时,必须指定成员是在哪个类中定义的。然而,static 关键字只能用于类定义体内部的声明中,定义不能标示为 static
-
特殊的整型 const static 成员:static 数据成员通常在定义时才初始化,例外是整型 const static 数据成员就可以在类的定义体中进行初始化
class Account { public: static double rate() { return interestRate; } static void rate(double); // sets a new rate private: static const int period = 30; // interest posted every 30 days double daily_tbl[period]; // ok: period is constant expression };
-
const static 数据成员在类的定义体中初始化时,该数据成员仍必须在类的定义体之外进行定义
const int Account::period; //无需指定初始值
-
static 数据成员的类型可以是该成员所属的类类型。非 static 成员被限定声明为其自身类对象的指针或引用
第十三章 复制控制
- 复制构造函数、赋值操作符和析构函数总称为复制控制
- 有一种特别常见的情况需要类定义自己的复制控制成员的:类具有指针成员
13.1. 复制构造函数
-
赋值构造函数用于:
• 根据另一个同类型的对象显式或隐式初始化一个对象。
• 复制一个对象,将它作为实参传给一个函数。
• 从函数返回时复制一个对象。
• 初始化顺序容器中的元素。
• 根据元素初始化式列表初始化数组元素
class Foo {
public:
Foo(); // default constructor
Foo(const Foo&); // copy constructor
// ...
};
- 编写自己的复制构造函数时,必须显式复制需要复制的任意成员。显式定义的复制构造函数不会进行任何自动复制
13.1.1. 合成的复制构造函数
-
如果我们没有定义复制构造函数,编译器就会为我们合成一个
-
执行逐个成员初始化,将新对象初始化为原对象的副本:其中每个数据成员在构造函数初始化列表中进行初始化
Sales_item::Sales_item(const Sales_item &orig): isbn(orig.isbn), // uses string copy constructor units_sold(orig.units_sold), // copies orig.units_sold revenue(orig.revenue) // copy orig.revenue { } // empty body
13.1.3. 禁止复制
- 为了防止复制,类必须显式声明其复制构造函数为 private
- 不允许复制的类对象只能作为引用传递给函数或从函数返回,它们也不能用作容器的元素
- 一般来说,最好显式或隐式定义默认构造函数和复制构造函数。只有不存在其他构造函数时才合成默认构造函数。如果定义了复制构造函数,也必须定义默认构造函数
13.2. 赋值操作符
-
重载操作符:操作符函数有一个返回值和一个形参表。形参表必须具有与该操作符数目相同的形参(如果操作符是一个类成员,则包括隐式 this 形参)
class Sales_item { public: // other members as before // equivalent to the synthesized assignment operator Sales_item& operator=(const Sales_item &); };
-
一般而言,如果类需要复制构造函数,它也会需要赋值操作符
13.3. 析构函数
- 如果类需要析构函数,则它也需要赋值操作符和复制构造函数,这是一个有用的经验法则。这个规则常称为三法则,指的是如果需要析构函数,则需要所有这三个复制控制成员
- 析构函数与复制构造函数或赋值操作符之间的一个重要区别是,即使我们编写了自己的析构函数,合成析构函数仍然运行
13.5. 管理指针成员
- 指针成员采取常规指针型行为。这样的类具有指针的所有缺陷但无需特殊的复制控制。
- 类可以实现所谓的“智能指针”行为。指针所指向的对象是共享的,但类能够防止悬垂指针。
- 类采取值型行为。指针所指向的对象是唯一的,由每个类对象独立管理:string 类是值型类的一个例子
第十四章 重载操作符与转换
14.1. 重载操作符的定义
-
定义:保留字 operator 后接需定义的操作符号,重载操作符的形参数目(包括成员函数的隐式
this 指针)与操作符的操作数数目相同作为类成员的重载函数,其形参看起来比操作数数目少 1。作为成员函数的操作符有一个隐含的 this 形参,限定为第一个操作数
Sales_item operator+(const Sales_item&, const Sales_item&);
-
不能重载的操作符:
- ::
- .*
- .
- ?:
-
重载操作符必须具有至少一个类类型或枚举类型(第 2.7 节)的操作数。这条规则强制重载操作符不能重新定义用于内置类型对象的操作符的含义
-
不再具备短路求值特性:重载 &&、|| 或逗号操作符不是一种好的做法
-
一般将算术和关系操作符定义非成员函数,而将赋值操作符定义为成员
-
一般将算术和关系操作符定义非成员函数,而将赋值操作符定义为成员
-
重载逗号、取地址、逻辑与、逻辑或等等操作符通常不是好做法。这些操作符具有有用的内置含义,如果我们定义了自己的版本,就不能再使用这些内置含义
-
当一个重载操作符的含义不明显时,给操作取一个名字更好。对于很少用的操作,使用命名函数通常也比用操作符更好。如果不是普通操作,没有必要为简洁而使用操作符
-
选择成员或非成员实现
• 赋值(=)、下标([])、调用(())和成员访问箭头(->)等操作符必须定义为成员,将这些操作符定义为非成员函数将在编译时标记为错误。
• 像赋值一样,复合赋值操作符通常应定义为类的成员,与赋值不同的是,不一定非得这样做,如果定义非成员复合赋值操作符,不会出现编译错误。
• 改变对象状态或与给定类型紧密联系的其他一些操作符,如自增、自减和解引用,通常就定义为类成员。
• 对称的操作符,如算术操作符、相等操作符、关系操作符和位操作符,最好定义为普通非成员函数。
14.2. 输入和输出操作符
14.2.1. 输出操作符 << 的重载
-
为了与 IO 标准库一致,操作符应接受 ostream& 作为第一个形参,对类类型 const 对象的引用作为第二个形参,并返回对 ostream 形参的引用
// 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; }
-
一般而言,输出操作符应输出对象的内容,进行最小限度的格式化,它们不应该输出换行符
-
IO 操作符必须为非成员函数
-
IO 操作符通常对非公用数据成员进行读写,因此,类通常将 IO 操作符设为友元
14.2.2. 输入操作符 >> 的重载
- 输入操作符的第一个形参是一个引用,指向它要读的流,并且返回的也是对同一个流的引用。它的第二个形参是对要读入的对象的非const 引用,该形参必须为非 const,因为输入操作符的目的是将数据读到这个对象中
- 更重要但通常重视不够的是,输入和输出操作符有如下区别:输入操作符必须处理错误和文件结束的可能性
- 设计输入操作符时,如果可能,要确定错误恢复措施,这很重要
14.3. 算术操作符和关系操作符
定义为非成员函数
-
加法+操作符:1)注意,为了与内置操作符保持一致,加法返回一个右值,而不是一个引用;2)既定义了算术操作符又定义了相关复合赋值操作符的类,一般应使用复合赋值实现算术操作符
Sales_item operator+(const Sales_item& lhs, const Sales_item& rhs) { Sales_item ret(lhs); // copy lhs into a local object that we'll return ret += rhs; // add in the contents of rhs return ret; // return ret by value }
-
相等==操作符和不相等!=操作符
inline bool operator==(const Sales_item &lhs, const Sales_item &rhs) { // must be made a friend of Sales_item return lhs.units_sold == rhs.units_sold && lhs.revenue == rhs.revenue && lhs.same_isbn(rhs); } inline bool operator!=(const Sales_item &lhs, const Sales_item &rhs) { return !(lhs == rhs); // != defined in terms of operator== }
-
赋值操作符可以重载。无论形参为何种类型,赋值操作符必须定义为成员函数,这一点与复合赋值操作符有所不同
-
赋值必须返回对 *this 的引用.一般而言,赋值操作符与复合赋值操作符应返回操作符的引用
14.5. 下标操作符 operator[]
- 下标操作符必须定义为类成员函数.可用作赋值的左右操作符数
- 类定义下标操作符时,一般需要定义两个版本:一个为非 const 成员并返回引用,另一个为 const 成员并返回 const 引用
14.6. 成员访问操作符 解引用操作符(*)和箭头操作符(->)
-
箭头操作符必须定义为类成员函数。解引用操作不要求定义为成员,但将它作为成员一般也是正确的
-
解引用操作符和箭头操作符常用在实现智能指针(第 13.5.1 节)的类中
-
支持指针操作
class ScreenPtr { public: // constructor and copy control members as before Screen &operator*() { return *ptr->sp; } Screen *operator->() { return ptr->sp; } const Screen &operator*() const { return *ptr->sp; } const Screen *operator->() const { return ptr->sp; } private: ScrPtr *ptr; // points to use-counted ScrPtr class };
-
箭头操作符不接受显式形参,没有第二个形参,由编译器处理获取成员的工作
-
对重载箭头的返回值的约束: 重载箭头操作符必须返回指向类类型的指针,或者返回定义了自己的箭头操作符的类类型对象
14.7. 自增(++)操作符和自减(–)操作符
经常由诸如迭代器这样的类实现
-
C++ 语言不要求自增操作符或自减操作符一定作为类的成员,但是,因为这些操作符改变操作对象的状态,所以更倾向于将它们作为成员
-
定义前自增/前自减操作符: 为了与内置类型一致,前缀式操作符应返回被增量或减量对象的引用
class CheckedPtr { public: CheckedPtr& operator++(); // prefix operators CheckedPtr& operator--(); // other members as before };
-
区别操作符的前缀和后缀形式:形参数目和类型相同
后缀式操作符函数接受一个额外的(即,无用的)int 型 形参。使用后缀式操作符进,编译器提供 0 作为这个形参的实参。尽管我们的 前缀式操作符函数可以使用这个额外的形参,但通常不应该这样做。那个形参不 是后缀式操作符的正常工作所需要的,它的唯一目的是使后缀函数与前缀函数区 别开来
-
定义后缀式操作符:
1)因为不使用 int 形参,所以没有对其命名
2)通过调用前缀式版本实现这些操作符,不需要检查 curr 是否在范围之内,那个检查以及必要的 throw,在相应的前缀式操作符中完成
3)为了与内置操作符一致,后缀式操作符应返回旧值(即,尚未自增或自减的值),并且,应作为值返回,而不是返回引用
class CheckedPtr { public: // increment and decrement CheckedPtr operator++(int); // postfix operators CheckedPtr operator--(int); // other members as before };
14.8. 调用操作符和函数对象
函数对象经常用作通用算法的实参
-
函数调用操作符必须声明为成员函数。一个类可以定义函数调用操作符的多个版本,由形参的数目或类型加以区别
-
定义了调用操作符的类,其对象常称为函数对象,即它们是行为类似函数的对象
-
函数对象的函数适配器:用于特化和扩展一元和二元函数对象
1)的二元函数对象类:为二元操作符定义的调用操作符需要两个给定类型的形参,而一元函数对象类型定义了接受一个实参的调用操作符
2)绑定器,是一种函数适配器,它通过将一个操作数绑定到给定值而将二元
函数对象转换为一元函数对象。标准库定义了两个绑定器适配器:bind1st 将给定值绑定到二元函数对象的第一个实参,bind2nd 将给定值绑定到二元函数对象的第二个实参
3)求反器,是一种函数适配器,它将谓词函数对象的真值求反。
标准库还定义了两个求反器:not1 将一元函数对象的真值求反,not2 将二元函数对象的真值求反
14.9. 转换与类类型
- 一种特殊的类成员函数:定义将类类型值转变为其他类型值的转换;在类定义体内声明,在保留字 operator之后跟着转换的目标类型
operator type();
type 表示内置类型名、类类型名或由类型别名定义的名字。对任何可作为函数返回类型的类型(除了 void 之外)都可以定义转换函数。一般而言,不允许转换为数组或函数类型,转换为指针类型(数据和函数指针)以及引用类型是可以的
-
转换函数必须是成员函数,不能指定返回类型,并且形参表必须为空;必须显式返回一个指定类型的值
-
转换函数一般不应该改变被转换的对象。因此,转换操作符通常应定义为 const 成员
-
一般而言,给出一个类与两个内置类型之间的转换是不好的做法
-
避免二义性最好的方法是避免编写互相提供隐式转换的成对的类
-
避免转换函数的过度使用:避免二义性最好的方法是,保证最多只有一种途径将一个类型转换为另一类型
-
在调用重载函数时,需要使用构造函数或强制类型转换来转换实参,这是设计拙劣的表现
-
既为算术类型提供转换函数,又为同一类类型提供重载操作符,可能会导致重载操作符和内置操作符之间的二义性
第十五章. 面向对象编程
15.2. 定义基类和派生类
-
virtual:除了构造函数之外,任意非 static 成员函数都可以是虚函数。保留字只在类内部的成员函数声明中出现,不能用在类定义体外部出现的函数定义上
-
基类通常应将派生类需要重定义的任意函数定义为虚函数
-
protected 成员:派生类只能通过派生类对象访问其(此对象)基类的 protected 成员,派生类对其基类类型对象的 protected 成员没有特殊访问权限
class Bulk_item : protected Item_base {...} 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 }
-
派生类的声明
class Bulk_item : public Item_base {...} class Bulk_item; class Item_base;
-
多态触发条件(动态绑定):第一,只有指定为虚函数的成员函数才能进行动态绑定,成员函数默认为非虚函数,非虚函数不进行动态绑定;第二,必须通过基类类型的引用或指针进行函数调用
-
只有通过引用或指针调用,虚函数才在运行时确定。只有在这些情况下,直到运行时才知道对象的动态类型
-
只有成员函数中的代码才应该使用作用域操作符覆盖虚函数机制
-
派生类虚函数调用基类版本时,必须显式使用作用域操作符。如果派生类函数忽略了这样做,则函数调用会在运行时确定并且将是一个自身调用,从而导致无穷递归
-
在同一虚函数的基类版本和派生类版本中使用不同的默认实参几乎一定会引起麻烦:如果通过基类的引用或指针调用虚函数,但实际执行的是派生类中定义的版本,这时就可能会出现问题。在这种情况下,为虚函数的基类版本定义的默认实参将传给派生类定义的版本,而派生类版本是用不同的默认实参定义的
15.2.5. 公用、私有和受保护的继承
- 派生类可以恢复继承成员的访问级别,但不能使访问级别比基类中原来指定的更严格或更宽松:
class Base {
public:
std::size_t size() const { return n; }
protected:
std::size_t n;
};
class Derived : private Base {
public:
// 为了使 size 在 Derived 中成为 public,可以在 Derived 的 public 部分增加一个 using 声明
using Base::size;
protected:
//同理用using
using Base::n;
};
- 友元关系与继承:友元关系不能继承。基类的友元对派生类的成员没有特殊访问权限。如果基类被授予友元关系,则只有基类具有特殊访问权限,该基类的派生类不能访问授予友元关系的类。
15.3. 转换与继承
- 要确定到基类的转换是否可访问,可以考虑基类的 public 成员是否访问,如果可以,转换是可访问的,否则,转换是不可访问的
- 没有从基类类型到派生类型的(自动)转换,原因在于基类对象只能是基类对象,它不能包含派生类型成员。如果允许用基类对象给派生类型对象赋值,那么就可以试图使用该派生类对象访问不存在的成员。static_cast、dynamic_cast可强制转换
15.4. 构造函数和复制控制
-
向基类构造函数传递实参:派生类构造函数通过将基类(类名)包含在(基类)构造函数初始化列表中来间接初
始化继承成员 -
一个类只能初始化自己的直接基类:原因是,类 B 的作者已经指定了怎样构造和初始化 B 类型的对象。像类 B 的任何用户一样,类 C 的作者无权改变这个规约;每个类都定义了自己的接口
class B : public A {}; class C : public B {};
-
重构:包括重新定义类层次,将操作和/或数据从一个类移到另一个类。为了适应应用程序的需要而重新设计类以便增加新函数或处理其他改变时,最有可能需要进行重构
-
尊重基类接口:派生类构造函数不能初始化基类的成员且不应该对基类成员赋值。派生类应通过使用基类构造函数尊重基类的初始化意图,而不是在派生类构造函数函数体中对这些成员赋值
-
派生类定义了自己的复制构造函数,该复制构造函数一般应显式使用基类复制构造函数初始化对象的基类部分
class Base { /* ... */ }; class Derived: public Base { public: // Base::Base(const Base&) not invoked automatically Derived(const Derived& d): Base(d) /* other member initialization */ { /*... */ } };
初始化函数 Base(d) 将派生类对象 d 转换(第 15.3 节)为它的基类部分的引用,并调用基类复制构造函数
-
派生类定义了自己的赋值操作符,则该操作符必须对基类部分进行显式赋值
-
要保证运行适当的析构函数,基类中的析构函数必须为虚函数
-
析构函数的虚函数性质都将继承。因此,如果层次中根类的析构函数为虚函数,则派生类析构函数也将是虚函数,无论派生类显式定义析构函数还是使用合成析构函数,派生类析构函数都是虚函数
-
即使析构函数没有工作要做,继承层次的根类也应该定义一个虚析构函数
-
构造函数和赋值操作符不是虚函数。构造函数不能定义为虚函数
-
将类的赋值操作符设为虚函数很可能会令人混淆,而且不会有什么用处
-
如果在构造函数或析构函数中调用虚函数,则运行的是为构造函数或析构函数自身类型定义的版本
15.5. 继承情况下的类作用域
- 与基类成员同名的派生类成员将屏蔽对基类成员的直接访问,可以使用作用域操作符访问被屏蔽成员
- 设计派生类时,只要可能,最好避免与基类成员的名字冲突
- 在派生类作用域中派生类成员将屏蔽基类成员。即使函数原型不同,基类成员也会被屏蔽
15.6. 纯虚函数
-
在函数形参表后面写上 = 0 以指定纯虚函数
class Disc_item : public Item_base { public: double net_price(std::size_t) const = 0; };
将函数定义为纯虚能够说明,该函数为后代类型提供了可以覆盖的接口,但是这个类中的版本决不会调用。重要的是,用户将不能创建 Disc_item 类型的对象
-
含有(或继承)一个或多个纯虚函数的类是抽象基类。除了作为抽象基类的派生类的对象的组成部分,不能创建抽象类型的对象
15.7. 容器与继承
- 因为派生类对象在赋值给基类对象时会被“切掉”,所以容器与通过继承相关的类型不能很好地融合
15.8. 句柄类与继承
- 是定义包装(cover)类或句柄类。句柄类存储和管理基类指针,句柄的用户可以获得动态行为但无须操心指针的管理
- 复制未知类型:解决这个问题的通用方法是定义虚操作进行复制,我们称将该操作命名为 clone
//每个类必须重定义该虚函数
class Item_base {
public:
virtual Item_base* clone() const
{ return new Item_base(*this); }
};
class Bulk_item : public Item_base {
public:
Bulk_item* clone() const
{ return new Bulk_item(*this); }
};
- 使用带关联容器的比较器:
//类型别名 将 Comp 定义为函数类型指针的同义词,该函数类型与我们希望用来比较 Sales_item 对象的比较函数相匹配
// type of the comparison function used to order the multiset
typedef bool (*Comp)(const Sales_item&, const Sales_item&);
//items 是一个 multiset,它保存 Sales_item 对象并使用 Comp 类型的对象比较它们。multiset 是空的——我们没有提供任何元素,但我们的确提供了一个名为 compare 的比较函数。当在 items 中增加或查找元素时,将用 compare 函数对 multiset 进行排序
std::multiset<Sales_item, Comp> items(compare);
第十六章 模板和泛型编程
- 模板是泛型编程的基础
16.1. 模板定义
16.1.1. 定义函数模板
- 模板定义以关键字 template 开始,后接模板形参表,模板形参表是用尖括号括住的一个或多个模板形参的列表,形参之间以逗号分隔
- 模板形参表不能为空
- 关键字 class 或 typename没有区别
template <typename T>
int compare(const T &v1, const T &v2)
{
if (v1 < v2) return -1;
if (v2 < v1) return 1;
return 0;
}
- inline 函数模板:inline说明符放在模板形参表之后、返回类型之前,不能放在关键字 template 之前
template <typename T> inline T min(const T&, const T&);
16.1.2. 定义类模板
- 类名为Queue模板
template <class Type> class Queue { ... };
- 模板形参名字的限制:用作模板形参的名字不能在模板内部重用;模板形参的名字只能在同一模板形参表中使用一次
- 每个模板类型形参前面必须带上关键字 class 或 typename,每个非类型形参前面必须带上类型名字,省略关键字或类型说明符是错误的
- typename 与 class 的区别:关键字 typename 是作为标准 C++ 的组成部分加入到 C++ 中的,因此旧的程序更有可能只用关键字 class
- 在模板定义内部指定类型:
template <class Parm, class U>
Parm fcn(Parm* array, U value)
{
typename Parm::size_type * p; // ok: declares p to be a pointer
}
如果拿不准是否需要以 typename 指明一个名字是一个类型,那么指定它是个好主意。在类型之前指定 typename 没有害处,因此,即使 typename 是不必要的,也没有关系
-
编写模板代码时,对实参类型的要求尽可能少是很有益的
-
从函数实参确定模板实参的类型和值的过程叫做模板实参推断
16.3. 模板编译模型
-
要编译使用自己的类模板和函数模板的代码,必须查阅编译器的用户指南,看看编译器怎样处理实例化
-
两种模型:“包含”模型所有编译器都支持;“分别编译”模型,只有一些编译器支持
-
分别编译模型: export 关键字让编译器知道要记住给定的模板定义
export 关键字能够指明给定的定义可能会需要在其他文件中产生实例化
export 关键字不必在模板声明中出现,如果在头文件中使用了 export,则该头文件只能被程序中的一个源文件使用;应该在类的实现文件中使用 export
// .h头文件声明 template <class Type> class Queue { ... }; // .cpp源文件定义 export template <class Type> class Queue; #include "Queue.h" // ...
16.4. 类模板成员
-
在类本身的作用域内部,可以使用类模板的非限定名,如下所示;但编译器不会为类中使用的其他模板的模板形参进行这样的推断
vector<int> vec;可以 写为 vector vec;
-
类模板成员函数定义: 1)必须以关键字 template 开关,后接类的模板形参表。 2)必须指出它是哪个类的成员。 3)类名必须包含其模板形参
template <class T> ret-type Queue<T>::member-name
-
非类型模板实参必须是编译时常量表达式
-
类模板中的友元声明:
1)普通非模板类或函数的友元声明,将友元关系授予明确指定的类或函数
template <class Type> class Bar { // grants access to ordinary, nontemplate class and function friend class FooBar; friend void fcn(); };
2)类模板或函数模板的友元声明,授予对友元所有实例的访问权
template <class Type> class Bar { // grants access to Foo1 or templ_fcn1 parameterized by any type template <class T> friend class Foo1; template <class T> friend void templ_fcn1(const T&); };
3)只授予对类模板或函数模板的特定实例的访问权的友元声明
template <class T> class Foo2; template <class T> void templ_fcn2(const T&); template <class Type> class Bar { // grants access to a single specific instance parameterized by char* friend class Foo2<char*>; friend void templ_fcn2<char*>(char* const &); };
-
友元模板声明:如果没有事先告诉编译器该友元是一个模板,则编译器将认为该友元是一个普通非模板类或非模板函数;想要限制对特定实例化的友元关系时,必须在可以用于友元声明之前声明类或函数
-
成员模板:任意类(模板或非模板)可以拥有本身为类模板或函数模板的成员且不能为虚。
当成员模板是类模板的成员时,它的定义必须包含类模板形参以及自己的模板形参。首先是类模板形参表,后面接着成员自己的模板形参表
-
使用类模板的 static 成员:,可以通过类类型的对象访问类模板的 static 成员,或者通过使用作用域操作符直接访问成员
-
定义 static 成员
template <class T> size_t Foo<T>::ctr = 0; // define and initialize ctr
16.6. 模板特化
-
函数特化定义形式: 关键字 template 后面接一对空的尖括号(<>); 再接模板名和一对尖括号,尖括号中指定这个特化定义的模板形参; 函数形参表; 函数体。
// special version of compare to handle C-style character strings template <> int compare<const char*>(const char* const &v1, const char* const &v2) { return strcmp(v1, v2); }
-
与其他函数声明一样,应在一个头文件中包含模板特化的声明,然后使用该特化的每个源文件包含该头文件;声明除了函数体其余和定义一样
-
特化出现在对该模板实例的调用之后是错误的。对具有同一模板实参集的同一模板,程序不能既有显式特化又有实例化
-
声明类模板的特化:类模板特化应该与它所特化的模板定义相同的接口,否则当用户试图使用未定义的成员时会感到奇怪
template<> class Queue<const char*> { ... };
-
类特化定义:在类特化外部定义成员时,成员之前不能加 template<> 标记
-
特化类的成员而不特化类,定义前加上 template<> 标记
//声明 template <> void Queue<const char*>::push(const char* const &); //定义 template <> void Queue<const char*>::push(const char *const &val) { ... }
-
类模板的部分特化
类模板的部分特化本身也是模板。部分特化的定义看来像模板定义,这种定义以关键字 template 开头,接着是由尖括号(<>)括住的模板形参表。部分特化的模板形参表是对应的类模板定义形参表的子集
当声明了部分特化的时候,编译器将为实例化选择最特化的模板定义,当没有部分特化可以使用的时候,就使用通用模板定义;类模板成员的通用定义永远不会用来实例化类模板部分特化的成员
16.7. 重载与函数模板
- 重载函数中既有普通函数又有函数模板 调用顺序:
- 为这个函数名建立候选函数集合,包括:
a. 与被调用函数名字相同的任意普通函数。
b. 任意函数模板实例化,在其中,模板实参推断发现了与调用中所用函数实参相匹配的模板实参。 - 确定哪些普通函数是可行的(第 7.8.2 节)(如果有可行函数的话)。候选集合中的每个模板实例都 可行的,因为模板实参推断保证函数可以被调用。
- 如果需要转换来进行调用,根据转换的种类排列可靠函数,记住,调用模板函数实例所允许的转换是有限的。
a. 如果只有一个函数可选,就调用这个函数。
b. 如果调用有二义性,从可行函数集合中去掉所有函数模板实例。 - 重新排列去掉函数模板实例的可行函数。
• 如果只有一个函数可选,就调用这个函数。
• 否则,调用有二义性。
- 当匹配同样好时,非模板版本优先;定义函数模板特化(第 16.6 节)几乎总是比使用非模板版本更好。
第十七章 用于大型程序的工具
17.1. 异常处理
- 通过异常我们能够将问题的检测和问题的解决分离,这样程序的问题检测部分可以不必了解如何处理问题
- 有效使用异常处理需要理解:在抛出异常时会发生什么,在捕获异常时又会发生什么,还有用来传递错误的对象的含义
17.1.1. 抛出类类型的异常
-
异常是通过抛出对象而引发的。该对象的类型决定应该激活哪个处理代码。被选中的处理代码是调用链中与该对象类型匹配且离抛出异常位置最近的那个
因为在处理异常的时候会释放局部存储,所以被抛出的对象就不能再局部存储,而是用 throw 表达式初始化一个称为异常对象的特殊对象
异常对象通过复制被抛出表达式的结果创建,该结果必须是可以复制的类型
执行 throw 的时候,不会执行跟在 throw 后面的语句,而是将控制从 throw 转移到匹配的 catch
-
异常对象与继承:当抛出一个表达式的时候,被抛出对象的静态编译时类型将决定异常对象的类型
-
抛出指针通常是个坏主意:抛出指针要求在对应处理代码存在的任意地方存在指针所指向的对象
17.1.2. 栈展开
-
栈展开(stack unwinding),沿嵌套函数调用链继续向上,直到为异常找到一个 catch 子句
-
为局部对象调用析构函数:栈展开期间,释放局部对象所用的内存并运行类类型局部对象的析构函数;不包括new分配的
-
析构函数应该从不抛出异常:标准库类型都保证它们的析构函数不会引发异常
析构函数如果又抛出自己的未经处理的另一个异常,将会导致调用标准库 terminate 函数。一般而言,terminate 函数将调用 abort 函数,强制从整个程序非正常退出
-
异常与构造函数:构造函数内部所做的事情经常会抛出异常;即使对象只是部分被构造了,也要保证将会适当地撤销已构造的成员
-
未捕获的异常终止程序:不能不处理异常。异常是足够重要的、使程序不能继续正常执行的事件。如果找不到匹配的 catch,程序就调用库函数 terminate
17.1.3. 捕获异常
-
catch 子句中的异常说明符的类型必须是完全类型,即必须是内置类型或者是已经定义的程序员自定义类型。类型的前向声明不行
-
查找匹配的处理代码:最特殊的 catch 必须最先出现
除下面几种可能的区别之外,异常的类型与 catch 说明符的类型必须完全匹配:
• 允许从非 const 到 const 的转换。也就是说,非 const 对象的 throw 可以与指定接受 const 引用的 catch 匹配。
• 允许从派生类型型到基类类型的转换。
• 将数组转换为指向数组类型的指针,将函数转换为指向函数类型的适当指针 -
异常说明符与继承:通常,如果 catch 子句处理因继承而相关的类型的异常,它就应该将自己的形参定义为引用。如果 catch 形参是引用类型,catch 对象就直接访问异常对象,catch 对象的静态类型可以与 catch 对象所引用的异常对象的动态类型不同
-
带有因继承而相关的类型的多个 catch 子句,必须从最低派生类类到最高派生类型排序
17.1.4. 重新抛出 throw
-
重新抛出是后面不跟类型或表达式的一个 throw,将异常传递函数调用链中更上层的函数
throw;
空 throw 语句将重新抛出异常对象,它只能出现在 catch 或者从 catch 调用的函数中。如果在处理代码不活动时碰到空 throw,就调用 terminate 函数。只有当异常说明符是引用的时候,才会传播那些改变
17.1.5. 捕获所有异常的处理代码
- 捕获所有异常的 catch 子句形式为 (…)
// matches any exception that might be thrown
catch (...) { }
- 如果 catch(…) 与其他 catch 子句结合使用,它必须是最后一个,否则,任何跟在它后面的 catch 子句都将不能被匹配
17.1.6. 函数测试块与构造函数
-
构造函数要处理来自构造函数初始化式的异常,唯一的方法是将构造函数编写为函数测试块
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); }
17.1.7. 异常类层次
graph TD
1(exception) ==> 2(bad_cast)
1 ==> 3(runtime_error)
1 ==> 4(logic_error)
1 ==> 5(bad_alloc)
3 --> 6(overflow_error)
4 --> 9(domain_error)
subgraph
6 --> 7(underflow_error)
7 --> 8(range_error)
end
subgraph
9 --> 10(invalid_error)
10 --> 11(out_of_error)
11 --> 12(length_error)
end
- exception 类型所定义的唯一操作是一个名为 what 的虚成员,该函数返回 const char* 对象,它一般返回用来在抛出位置构造异常对象的信息
17.1.8. 自动资源释放
- RAII:通过定义一个类来封闭资源的分配和释放,可以保证正确释放资源。这一技术常称为“资源分配即初始化”
- 可能存在异常的程序以及分配资源的程序应该使用类来管理那些资源。如本节所述,使用类管理分配和回收可以保证如果发生异常就释放资源
17.1.9. auto_ptr 类
-
auto_ptr 只能用于管理从 new 返回的一个对象,它不能管理动态分配的数组
-
auto_ptr 对象的复制和赋值是破坏性操作
auto_ptr 和内置指针对待复制和赋值有非常关键的重要区别。当复制 auto_ptr 对象或者将它的值赋给其他 auto_ptr 对象的时候,将基础对象的所有权从原来的 auto_ptr 对象转给副本,原来的 auto_ptr 对象重置为未绑定状态
-
因为复制和赋值是破坏性操作,所以 auto_ptrs 不能将 auto_ptr 对象存储在标准容器中。标准库的容器类要求在复制或赋值之后两个对象相等,auto_ptr 不满足这一要求,如果将 ap2 赋给 ap1,则在赋值之后 ap1 != ap2,复制也类似
-
应该只用 get 询问 auto_ptr 对象或者使用返回的指针值,不能用 get 作为创建其他 auto_ptr 对象的实参
if (p_auto.get())
*p_auto = 1024;
- 必须调用 reset 函数来改变指针
p_auto.reset(new int(1024));
17.1.10. 异常说明
-
警告:Auto_ptr 缺陷
- 不要使用 auto_ptr 对象保存指向静态分配对象的指针,否则,
当 auto_ptr 对象本身被撤销的时候,它将试图删除指向非动态
分配对象的指针,导致未定义的行为。 - 永远不要使用两个 auto_ptrs 对象指向同一对象,导致这个错误
的一种明显方式是,使用同一指针来初始化或者 reset 两个不同
的 auto_ptr 对象。另一种导致这个错误的微妙方式可能是,使
用一个 auto_ptr 对象的 get 函数的结果来初始化或者 reset
另一个 auto_ptr 对象。 - 不要使用 auto_ptr 对象保存指向动态分配数组的指针。当
auto_ptr 对象被删除的时候,它只释放一个对象——它使用普通
delete 操作符,而不用数组的 delete [] 操作符。 - 不要将 auto_ptr 对象存储在容器中。容器要求所保存的类型定
义复制和赋值操作符,使它们表现得类似于内置类型的操作符:
在复制(或者赋值)之后,两个对象必须具有相同值,auto_ptr 类
不满足这个要求。
- 不要使用 auto_ptr 对象保存指向静态分配对象的指针,否则,
-
定义异常说明:异常说明跟在函数形参表之后。一个异常说明在关键字 throw 之后跟着一个(可能为空的)由圆括号括住的异常类型列表
void recoup(int) throw(runtime_error);
如果 recoup 抛出一个异常,该异常将是 runtime_error 对象,或者是由 runtime_error 派生的类型的异常
-
空说明列表指出函数不抛出任何异常
void no_problem() throw(); void no_problem() noexcept;
在 const 成员函数声明中,异常说明跟在 const 限定符之后
-
如果一个函数声明没有指定异常说明,则该函数可以抛出任意类型的异常
-
在编译的时候,编译器不能也不会试图验证异常说明
-
如果函数抛出了没有在其异常说明中列出的异常,就调用标准库函数 unexpected。默认情况下,unexpected 函数调用 terminate 函数,terminate 函数一般会终止程序
-
基类中的异常列表是虚函数的派生类版本可以抛出的异常列表的超集
-
异常说明是函数类型的一部分
void (*pf)(int) throw(runtime_error);
-
在用另一指针初始化带异常说明的函数的指针,或者将后者赋值给函数地址的时候,两个指针的异常说明不必相同,但是,源指针的异常说明必须至少与目标指针的一样严格
void recoup(int) throw(runtime_error); // ok: recoup is as restrictive as pf1 void (*pf1)(int) throw(runtime_error) = recoup;
17.2. 命名空间
-
名字冲突问题称为命名空间污染问题
-
像其他名字一样,命名空间的名字在定义该命名空间的作用域中必须是唯一的。命名空间可以在全局作用域或其他作用域内部定义,但不能在函数或类内部定义
namespace cplusplus_primer { // 全局作用域的任意声明:类、变量(以及它们的初始化)、函数 //(以及它们的定义)、模板以及其他命名空间 }
-
命名空间作用域不能以分号结束
-
每个命名空间是一个作用域
-
定义多个不相关类型的命名空间应该使用分离的文件,表示该命名空间定义的每个类型
-
全局命名空间:全局命名空间是隐式声明的,存在于每个程序中。因为全局命名空间是隐含的,它没有名字
::member_name
-
未命名的命名空间与其他命名空间不同,未命名的命名空间的
定义局部于特定文件,从不跨越多个文本文件 -
如果头文件定义了未命名的命名空间,那么,在每个包含该头文件的文件中,该命名空间中的名字将定义不同的局部实体
-
C++ 不赞成文件静态(static)声明。在未来版本中可能不支持。应该避免文件静态而使用未命名空间代替
17.2.4. 命名空间成员的使用
-
除了在函数或其他作用域内部,头文件不应该包含 using 指示或 using 声明。在其顶级作用域包含 using 指示或 using 声明的头文件,具有将该名字注入包含该头文件的文件中的效果。头文件应该只定义作为其接口的一部分的名字,不要定义在其实现中使用的名字
-
一个 using 声明一次只引入一个命名空间成员
using std::vector;
-
命名空间别名
关键字 namespace 开头,接(较短的)命名空间别名名字,再接 =,再接原来的命名空间名字和分号。如果原来的命名空间名字是未定义的,就会出错
namespace cplusplus_primer { /* ... */ }; namespace primer = cplusplus_primer; //别名primer namespace Qlib = cplusplus_primer::QueryLib; //别名Qlib
-
using 指示:名字都是可见的
using 指示以关键字 using 开头,后接关键字 namespace,再接命名空间名字。如果该名字不是已经定义的命名空间名字,就会出错
using 指示将命名空间成员提升到外围作用域
using namespace std;
-
警告:避免 Using 指示
全局命名空间污染问题。对程序中使用的每个命名空间名字使用 using 声明更好,这样做减少注入到命名空间中的名字数目,由 using 声明引起的二义性错误在声明点而不是使用点检测,因此更容易发现和修正
-
实参相关的查找与类类型形参
屏蔽命名空间名字规则的一个重要例外:接受类类型形参(或类类型指针及引用形参)的函数(包括重载操作符),以及与类本身定义在同一命名空间中的函数(包括重载操作符),在用类类型对象(或类类型的引用及指针)作为实参的时候是可见的
std::string s; // ok: calls std::getline(std::istream&, const std::string&) getline(std::cin, s);
-
重载与 using 声明:没有办法编写 using 声明来引用特定函数声明
由 using 声明引入的函数,重载出现 using 声明的作用域中的任意其他同名函数的声明
using NS::print(int); // error: cannot specify parameter list using NS::print; // ok: using declarations specify names only
17.2.7. 命名空间与模板
- 模板的显式特化必须在定义通用模板的命名空间中声明,否则,该特化将与它所特化的模板不同名。为了提供命名空间中所定义模板的自己的特化,必须保证在包含原始模板定义的命名空间中定义特化
17.3. 多重继承与虚继承
-
多重继承
在一个给定派生列表中,一个基类只能出现一次
构造函数调用次序既不受构造函数初始化列表中出现的基类的影响,也不受基类在构造函数初始化列表中的出现次序的影响
-
多重继承派生类的复制控制
像单继承(第 15.4.3 节)的情况一样,如果具有多个基类的类定义了自己的析构函数,该析构函数只负责清除派生类。如果派生类定义了自己的复制构造函数或赋值操作符,则类负责复制(赋值)所有的基类子部分。只有派生类使用复制构造函数或赋值操作符的合成版本,才自动复制或赋值基类部分
-
多重继承下的类作用域
当一个类有多个基类的时候,通过所有直接基类同时进行名字查找。多重继承的派生类有可能从两个或多个基类继承同名成员,对该成员不加限定的使用是二义性的,即名字的使用必须显式指定使用哪个基类
ying_yang.Endangered::print(cout);
-
虚继承:解决菱形继承
对给定虚基类,无论该类在派生层次中作为虚基类出现多少次,只继承一个共享的基类子对象。共享的基类子对象称为虚基类
class istream : public virtual ios { ... }; class ostream : virtual public ios { ... }; // iostream inherits only one copy of its ios base class class iostream: public istream, public ostream { ... };
-
虚基类的声明
指定虚派生只影响从指定了虚基类的类派生的类。除了影响派生类自己的对象之外,它也是关于派生类与自己的未来派生类的关系的一个陈述。陈述了在后代派生类中共享指定基类的单个实例的愿望
-
特定派生类实例的优先级高于共享虚基类实例。像非虚多重继承层次一样,这种二义性最好用在派生类中提供覆盖实例的类来解决
-
在虚派生中,由最低层派生类(此类没有子类)的构造函数初始化虚基类
-
无论虚基类出现在继承层次中任何地方,总是在构造非虚基类之前构造虚基类、
第十八章 特殊工具与技术
许多程序员从不(或者很少)需要使用本章所介绍的这些特征
18.1. 优化内存分配
18.1.1. C++ 中的内存分配
- C++ 提供下面两种方法分配和释放未构造的原始内存
- allocator 类,它提供可感知类型的内存分配。这个类支持一个抽象接口,以分配内存并随后使用该内存保存对象。
- 标准库中的 operator new 和 operator delete,它们分配和释放需要大小的原始的、未类型化的内存
- 在原始内存中构造和撤销对象
- allocator 类定义了名为 construct 和 destroy 的成员,其操作正如它们的名字所指出的那样:construct 成员在未构造内存中初始化对象,destroy 成员在对象上运行适当的析构函数。
- 定位 new 表达式接受指向未构造内存的指针,并在该空间中初始化一个对象或一个数组。
- 可以直接调用对象的析构函数来撤销对象。运行析构函数并不释放对象所在的内存。
- 算法 uninitialized_fill 和 uninitialized_copy 像 fill 和 copy 算法一样执行,除了它们的目的地构造对象而不是给对象赋值之外
18.1.3. operator new 函数和 operator delete 函数
-
new 表达式三个步骤
1)调用名为 operator new 的标准库函数,分配足够大的原始的未类型化的内存,以保存指定类型的一个对象
2)运行该类型的一个构造函数,用指定初始化式构造对象
3)返回指向新分配并构造的对象的指针
string * sp = new string("initialized");
-
delete 表达式两个步骤
1)对 sp 指向的对象运行适当的析构函数
2)调用名为 operator delete 的标准库函数释放该对象所用内存
delete sp;
-
一般而言,使用 allocator 比直接使用 operator new 和 operator delete 函数更为类型安全
-
标准库函数 operator new 和 operator delete 是 allocator 的 allocate 和 deallocate 成员的低级版本,它们都分配但不初始化内存
18.1.4. 定位 new 表达式
-
在已分配的原始内存中初始化一个对象,它不分配内存
-
语法形式:place_address 必须是一个指针。initializer-list 提供了(可
能为空的)初始化列表,以便在构造新分配的对象时使用new (place_address) type new (place_address) type (initializer-list) alloc.construct (first_free, t); 等价于 new (first_free) T(t);
-
定位 new 表达式比 allocator 类的 construct 成员更灵活。定位 new 表达式初始化一个对象的时候,它可以使用任何构造函数,并直接建立对象。construct 函数总是使用复制构造函数
18.1.5. 显式析构函数的调用
-
对于使用定位 new 表达式构造对象的程序,显式调用析构函数
p->~T(); // call the destructor
-
调用 operator delete 函数不会运行析构函数,它只释放指定的内存
18.1.6. 类特定的 new 和 delete
-
定义自己的名为 operator new 和 operator delete 的成员,类可以管理用于自身类型的内存
-
类成员 new 和 delete 函数:如果类定义了这两个成员中的一个,它也应该定义另一个
1) operator new 函数必须具有返回类型 void* 并接受 size_t 类型的形参
void *operator new(std::size_t);
2)operator delete 函数必须具有返回类型 void。有单个 void* 类型形参;也有两个形参,即 void* 和 size_t 类型;void*可以为空。除非类是某继承层次的一部分,否则形参 size_t 不是必需的
void operator delete(void *, std::size_t);
3)这些函数隐式地为静态函数,不必显式地将它们声明为 static,虽然这样做是合法的
4)成员数组操作符 new[] 和操作符 delete[] 同理
-
覆盖类特定的内存分配:使用全局作用域确定操作符,跳过类定义自己的类特定的 operator new/delete,调用全局的 operator new/delete
Type *p = ::new Type; // uses global operator new ::delete p; // uses global operator delete
-
如果用 new 表达式调用全局 operator new 函数分配内存,则 delete 表达式也应该调用全局 operator delete 函数
-
自由列表:已分配但未构造的对象的链表
18.2. 运行时类型识别(RTTI)
程序能够使用基类的指针或引用来检索这些指针或引用所指对象的实际派生类型
- 两个RTTI 操作符:这些操作符只为带有一个或多个虚函数的类返回动态类型信息,对于其他类型,返回静态(即编译时)类型的信息
- typeid 操作符,返回指针或引用所指对象的实际类型。
- dynamic_cast 操作符,将基类类型的指针或引用安全地转换为派生类型的指针或引用
- 使用动态强制类型转换要小心。只要有可能,定义和使用虚函数比直接接管类型管理好得多
18.2.1. dynamic_cast 操作符
-
与 dynamic_cast 一起使用的指针必须是有效的——它必须为 0 或者指向一个对象
-
涉及运行时类型检查转换到指针类型的 dynamic_cast 失败,则dynamic_cast 的结果是 0 值;转换到引用类型的 dynamic_cast 失败,则抛出一个 bad_cast 类型的异常
-
dynamic_cast 操作符一次执行两个操作。它首先验证被请求的转换是否有效,只有转换有效,操作符才实际进行转换
-
可以对值为 0 的指针应用 dynamic_cast,这样做的结果是 0
-
在条件中执行 dynamic_cast 保证了转换和其结果测试在一个表达式中进行
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 }
-
使用 dynamic_cast 和引用类型
//形式 dynamic_cast< Type& >(val) //使用 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 }
18.2.2. typeid 操作符
-
表达式
typeid(e) //使用例子 typeid(e).name(); //返回 C 风格字符串
-
可以与任何类型的表达式一起使用。结果是名为 type_info 的标准库类型的对象引用头文件 typeinfo
-
最常见的用途是比较两个表达式的类型,或者将表达式的类型与特定类型相比较
18.2.4. type_info 类
-
程序中创建 type_info 对象的唯一方法是使用 typeid 操作符
默认构造函数和复制构造函数以及赋值操作符都定义为 private,所以不能定义或复制 type_info 类型的对象
18.3. 类成员的指针
-
成员指针只应用于类的非 static 成员;static 成员指针是普通指针
-
定义数据成员的指针
class Screen { public: std::string contents; };
1)指向 std::string 类型的 Screen 类成员的指针:
string Screen::*
2)定义指向 Screen 类的 string 成员的指针:
string Screen::*ps_Screen;
3)用 contents 的地址初始化 ps_Screen
string Screen::*ps_Screen = &Screen::contents;
-
定义成员函数的指针:1)函数形参的类型和数目,包括成员是否为 const。 2)返回类型。 3)所属类的类型。
class Screen { public: char get() const; };
1)get 的 Screen 成员函数的指针
char (Screen::*)() const
2)定义和初始化get 的 Screen 成员函数的指针
// pmf points to the Screen get member that takes no arguments char (Screen::*pmf)() const = &Screen::get;
-
为成员指针使用类型别名:
1)将 Action 定义为带两个形参的 get 函数版本的类型的另一名字
// Action is a type name typedef char (Screen::*Action)(Screen::index, Screen::index) const;
Action 是类型“Screen 类的接受两个 index 类型形参并返回 char 的成员函数的指针”的名字
2)定义
Action get = &Screen::get;
18.3.2.使用类成员指针
-
成员指针解引用操作符(.*)从对象或引用获取成员
成员指针箭头操作符(->*)通过对象的指针获取成员
-
使用成员函数的指针
// pmf points to the Screen get member that takes no arguments char (Screen::*pmf)() const = &Screen::get; Screen myScreen; char c1 = myScreen.get(); // call get on myScreen char c2 = (myScreen.*pmf)(); // equivalent call to get Screen *pScreen = &myScreen; c1 = pScreen->get(); // call get on object to which pScreen points c2 = (pScreen->*pmf)(); // equivalent call to get
-
使用数据成员的指针
Screen::index Screen::*pindex = &Screen::width; Screen myScreen; // equivalent ways to fetch width member of myScreen Screen::index ind1 = myScreen.width; // directly Screen::index ind2 = myScreen.*pindex; // dereference to get width Screen *pScreen; // equivalent ways to fetch width member of *pScreen ind1 = pScreen->width; // directly ind2 = pScreen->*pindex; // dereference pindex to get width
18.4. 嵌套类/嵌套类型
- 套类最常用于定义执行类,是独立的类,基本上与它们的外围类不相关
18.4.1. 嵌套类的实现
-
嵌套在类模板内部的类是模板,嵌套类成员的名字在类外部是不可见的
-
定义嵌套类的成员
在其类外部定义的嵌套类成员,必须定义在定义外围类的同一作用域中。在其类外部定义的嵌套类的成员,不能定义在外围类内部,嵌套类的成员不是外围类的成员
// defines the QueueItem constructor // for class QueueItem nested inside class Queue<Type> template <class Type> Queue<Type>::QueueItem::QueueItem(const Type &t): item(t), next(0) { }
-
在外围类外部定义嵌套类
template <class Type> class Queue { // interface functions to Queue are unchanged private: struct QueueItem; // forward declaration of nested type QueueItem QueueItem *head; // pointer to first element in Queue QueueItem *tail; // pointer to last element in Queue }; template <class Type> struct Queue<Type>::QueueItem { QueueItem(const Type &t): item(t), next(0) { } Type item; // value stored in this element QueueItem *next; // pointer to next element in the Queue };
1)像其他前向声明一样,嵌套类的前向声明使嵌套类能够具有相互引用的成员
2)在看到在类定义体外部定义的嵌套类的实际定义之前,该类是不完全类型,应用所有使用不完全类型的常规限制
-
嵌套类静态成员定义
// defines an int static member of QueueItem, // which is a type nested inside Queue<Type> template <class Type> int Queue<Type>::QueueItem::static_mem = 1024;
-
嵌套类可以直接引用外围类的静态成员、类型名和枚举成员
-
嵌套模板的实例化:实例化外围类模板的时候,不会自动实例化类模板的嵌套类。像任何成员函数一样,只有当在需要完整类类型的情况下使用嵌套类本身的时候,才会实例化嵌套类
18.4.2. 嵌套类作用域中的名字查找
- 对嵌套类中所用名字的名字查找在普通类的名字查找之前进行
- 使用作用域操作符控制名字查找
18.5. 联合:节省空间的类
-
一种特殊的类,一个 union 定义了一个新的类型;union 对象分配的存储的量至少与包含其最大数据成员的一样多
union TokenValue { //名字(类型名)可选 成员类型默认public char cval; int ival; double dval; }; union { //未命名联合 char cval; int ival; double dval; } val; union { //匿名联合 char cval; int ival; double dval; };
-
没有静态数据成员、引用成员或类数据成员;可以定义成员函数,包括构造函数和析构函数。但是,union 不能作为基类使用,所以成员函数不能为虚数
-
给 union 对象的某个数据成员一个值使得其他数据成员变为未定义的
-
避免通过错误成员访问 union 值的最佳办法是,定义一个单独的对象跟踪 union 中存储了什么值。这个附加对象称为 union 的判别式(例如枚举)
-
嵌套联合:union 最经常用作嵌套类型
-
匿名 union 不能有私有成员或受保护成员,也不能定义成员函数
因为匿名 union 不提供访问其成员的途径,所以将成员作为定义匿名 union 的作用域的一部分直接访问
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; }; }; 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; }
18.6. 局部类
函数体内部定义的类,局部类的成员是严格受限的
- 局部类的所有成员(包括函数)必须完全定义在类定义体内部,因此,局部类远不如嵌套类有用
- 不允许局部类声明 static 数据成员,没有办法定义它们
- 局部类不能使用函数作用域中的变量:局部类只能访问在外围作用域中定义的类型名、static 变量和枚举成员
- 实际上,局部类中 private 成员几乎是不必要的,通常局部类的所有成员都为 public 成员
- 嵌套的局部类:嵌套在局部类中的类本身是一个带有所有附加限制的局部类。嵌套类的所有成员必须在嵌套类本身定义体内部定义
18.7. 固有的不可移植的特征
- 将程序移到新机器的过程称为“移植”,所以说 C 程序是可移植的
- C++ 的两个从 C 语言继承来的不可移植特征:位域和 volatile 限定符。这些特征可使与硬件接口的直接通信更容易
- 从 C 语言继承来的链接指示:它使得可以链接到用其他语言编写的程序
18.7.1. 位域
-
一种特殊的类数据成员,保存特定的位数。当程序需要将二进制数据传递给另一程序或硬件设备的时候,通常使用位域
-
位域在内存中的布局是机器相关的
-
必须是整型数据类型。在成员名后面接一个冒号以及指定位数的常量表达式,指出成员是一个位域
typedef unsigned int Bit; class File { Bit mode: 2; Bit modified: 1; Bit prot_owner: 3; };
-
通常最好将位域设为 unsigned 类型。存储在 signed 类型中的位域的行为由实现定义
-
定义了位域成员的类通常也定义一组内联成员函数来测试和设置位域的值
-
地址操作符(&)不能应用于位域,所以不可能有引用类位域的指针,位域也不能是类的静态成员
18.7.2. volatile 限定符
-
volatile 的确切含义与机器相关,只能通过阅读编译器文档来理解。使用 volatile 的程序在移到新的机器或编译器时通常必须改变
-
当可以用编译器的控制或检测之外的方式改变对象值的时候,应该将对象声明为 volatile。给编译器的指示,指出对这样的对象不应该执行优化
-
类也可以将成员函数定义为 volatile,volatile 对象只能调用 volatile 成员函数,和使用const一样
-
合成的复制控制不适用于 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 };
-
虽然可以定义复制控制成员来处理 volatile 对象,但更深入的问题是复制 volatile 对象是否有意义,对该问题的回答与任意特定程序中使用 volatile 的原因密切相关
18.7.3. 链接指示 extern “C”
-
C++ 使用链接指示指出任意非 C++ 函数所用的语言
-
声明非 C++ 函数:链接指示有两种形式单个的或复合的
链接指示不能出现在类定义或函数定义的内部,它必须出现在函数的第一次声明上
extern "C" { int strcmp(const char*, const char*); char *strcat(char*, const char*); }
-
链接指示与头文件
extern "C" { #include <string.h> // C functions that manipulate C-style strings }
允许将 C++ 从 C 函数库继承而来的函数定义为 C 函数,但不是必须定义为 C 函数——决定是用 C 还是用 C++ 实现 C 函数库,是每个 C++ 实现的事情
-
导出 C++ 函数到其他语言
用链接指示定义的函数的每个声明都必须使用相同的链接指示
-
支持什么语言随编译器而变。你必须查阅用户指南,获得关于编译器可以提供的任意非 C 链接说明的进一步信息。例如,extern “Ada”、extern “FORTRAN” 等
-
对链接到 C 的预处理器支持:自动定义预处理器名字 __cplusplus(两个下划线),所以,可以根据是否正在编译 C++ 有条件地包含代码
#ifdef __cplusplus // ok: we're compiling C++ extern "C" #endif int strcmp(const char*, const char*);
-
可以从 C 程序和 C++ 程序调用 calc 的 C 版本。其余函数是带类型形参的 C++ 函数,只能从 C++ 程序调用。声明的次序不重要
-
C 函数的指针与 C++ 函数的指针具有不同的类型,不能将 C 函数的指针初始化或赋值为 C++ 函数的指针(反之亦然)
extern "C" void (*pf)(int); extern "C" void f1(void(*)(int)); //链接指示应用于一个声明中的所有函数
-
因为链接指示应用于一个声明中的所有函数,所以必须使用类型别名,以便将 C 函数的指针传递给 C++ 函数
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 *);