endl 是一个特殊值,称为操纵符,将它写入输出流时,具有输出换行的效果,并刷新与设备相关联的 缓冲区。通过刷新缓冲区,用户可立即看到写入到流中的输出
程序员经常在调试过程中插入输出语句,这些语句都应该刷新输出流。忘记刷新输出流可能会造成输出停留在缓冲区中,如果程序崩溃,将会导致程序错误推断崩溃位置
非 const 变量默认为 extern。要使 const 变量能够在其他的文件中访问,必须地指定它为 extern
复合类型是指用其他类型定义的类型。如引用;指针是指向某种类型对象的复合数据类型
头文件保护符用于避免在已经见到头文件的情况下重新处理该头文件的内容
#ifndef SALESITEM_H
#define SALESITEM_H
// Definition of Sales_itemclass and related functions goes here
#endif
任何存储 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 svec(10); // 10 elements, each an empty string
vector ivec4(10, -1); // 10 elements, each initialized
to -1
C++ 程序员习惯于优先选用 != 而不是 < 来编写循环判断条件。在上例中,选用或不用某种操作符并没有特别的取舍理由。学习完本书第二部分的泛型编程后,你将会明白这种习惯的合理性
for (vector::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::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 长度的操作都会使已存在的迭代器失效
cptr的值不能修改 ,可以修改其指针指向。const在*的左边,常用作函数形参
const double *cptr;
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 = π
typedef string *pstring;
const pstring cstr;
//以上声明等价于
// cstr is a const pointer to string
string *const cstr; // equivalent to const pstring cstr
尽管没有严格要求在 switch 结构的最后一个标号之后指定 break 语句,但是,为了安全起见,最好在每个标号后面提供一个 break 语句,即使是最后一个标号也一样。如果以后在 switch 结构的末尾又需要添加一个新的 case 标号,则不用再在前面加 break 语句了
哪怕没有语句要在 default 标号下执行,定义 default 标号仍然是有用的。定义 default 标号是为了告诉它的读者,表明这种情况已经考虑到了,只是没什么要执行的
对于 switch 结构,只能在它的最后一个 case 标号或 default 标号后面定义变量
需要为某个特殊的 case 定义变量,则可以引入块语句,在该块语句中定义变量,从而保证这个变量在使用前被定义和初始化
系统通过 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 &);
该定义表示 cmpFcn 是一种指向函数的指针类型的名字。该指针类型为“指向返回 bool 类型并带有两个 const string 引用形参的函数的指针”。在要使用这种函数指针类型时,只需直接使用 cmpFcn 即可,不必每次都把整个类型声明全部写出来。
typedef bool (*cmpFcn)(const string &, const string &);
将函数指针初始化为 0,表示该指针不指向任何函数。
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
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
string searchValue("Quasimodo");
list::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 的一个重要问题在于:该操作不会删除或插入任何元素,而且保证在常量时间内实现交换。由于容器内没有移动任何元素,因此迭代器不会失效
弄清楚容器的 capacity(容量)与 size(长度)的区别非常重要
1)size 指容器当前拥有的元素个数
2)而 capacity 则指容器在必须分配新存储空间之前可以存储的元素总数
capacity 操作获取在容器需要分配更多的存储空间之前能够存储的元素总数,而 reserve (或resize)操作则告诉 vector 容器应该预留多少个元素的存储空间
三种顺序容器适配器:queue、priority_queue 和 stack
关联容器类型: map、set、multimap、multiset
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::iterator it = word_count.find("foobar");
if (it != word_count.end())
occurs = it->second;
set 中的键也为 const。在获得指向 set 中某元素的迭代器后,只能对其做读操作,而不能做写操作
multiset 和 multimap类型则允许一个键对应多个实例.
multimap 不支持下标运算,因为在这类容器中,某个键可能对应多个值
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 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;
}
//find运算 找到返回其迭代器,失败返回end()
vector::const_iterator result = find(vec.begin(), vec.end(), search_value);
accumulate算法: 在 numeric 头文件中
//将 sum 设置为 vec 的元素之和再加上 42
int sum = accumulate(vec.begin(), vec.end(), 42);
//从空字符串开始,把 vec 里的每个元素连接成一个字符串
string sum = accumulate(v.begin(), v.end(), string(""));
find_first_of算法 :带有两对迭代器参数来标记两段元素范围,在第一段范围内查找与第二段范围中任意元素匹配的元素,然后返回一个迭代器,指向第一个匹配的元素。如果找不到元素,则返回第一个范围的 end 迭代器
list::iterator it = roster1.begin();
it = find_first_of(it, roster1.end(), roster2.begin(), roster2.end())
将该范围内的每个元素都设为给定的值。如果输入范围有效,则可安全写入。这个算法只会对输入范围内已存在的元素进行写入操作
fill(vec.begin(), vec.end(), 0); // reset each element to 0
fill_n 函数: 不检查写入操作的算法
//参数包括:一个迭代器、一个计数器以及一个值
//从迭代器指向的元素开始,将指定数量的元素设置为给定的值
fill_n(vec.begin(), 10, 0);
back_inserter函数: 必须包含 iterator 头文件,是迭代器适配器。插入迭代器是可以给基础容器添加元素的迭代器
//如何安全使用写容器的算法
vector 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 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
unique算法: "删除"相邻的重复元素,然后重新排列输入范围内的元素,并且返回一个迭代器,表示无重复的值范围的结束
给“删除”加上引号是因为 unique 实际上并没有删除任何元素,而是将无重复的元素复制到序列的前端,从而覆盖相邻的重复元素。unique 返回的迭代器指向超出无重复的元素范围末端的下一位置
算法不直接修改容器的大小。如果需要添加或删除元素,则必须使用容器操作
谓词: 是做某些检测的函数,返回用于条件判断的类型,指出条件是否成立
标准库定义了四种不同的排序算法: 1)sort;2)stable_sort 保留相等元素的原始相对位置;
3)count_if(start, end, condition); //统计符合condition的个数
在创建流迭代器时,必须指定迭代器所读写的对象类型: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 对象。因此,关联容器不能使用任何写序列元素的算法。只能使用与关联容器绑在一起的迭代器来提供用于读操作的实参
向算法传递无效的迭代器类别所引起的错误,无法保证会在编译时被捕获到
最简单地说,类就是定义了一个新的类型和一个新作用域
在类内部定义的函数默认为 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 步查找不成功,则检查包含类定义的作用域中出现的声明以及出现在类定义之前的声明
函数作用域之后,在类作用域中查找;类作用域之后,在外围作用域中查找
构造函数的工作是保证每个对象的数据成员具有合适的初始值
构造函数不能声明为 const:
Sales_item() const; // error
有些成员必须在构造函数初始化列表中进行初始化。对于这样的成员,在构造函数函数体中对它们赋值不起作用,例如:
没有默认构造函数的类类型的成员
以及 const 或引用类型的成员
必须对任何 const 或引用类型成员以及没有默认构造函数的类类型的任何成员使用初始化式
初始化的次序常常无关紧要。然而,如果一个成员是根据其他成员而初始化,则成员初始化的次序是至关重要的
按照与成员声明一致的次序编写构造函数初始化列表是个好主意。此外,尽可能避免使用成员来初始化其他成员
有三个重大的缺点
struct Data {
int ival;
char *ptr;
};
// val1.ival = 0; val1.ptr = 0
Data val1 = { 0, 0 };
全局对象会破坏封装:对象需要支持特定类抽象的实现。类可以定义类 静态成员,而不是定义一个可普遍访问的全局对象
static 成员函数没有 this 形参,它可以直接访问所属类的 static 成员,但不能直接使用非 static 成员
static 成员是类的组成部分但不是任何对象的组成部分,因此,static 成员函数没有 this 指针
因为 static 成员不是任何对象的组成部分,所以 static 成员函数不能被声明为 const
static 成员函数也不能被声明为虚函数
使用 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 成员被限定声明为其自身类对象的指针或引用
赋值构造函数用于:
• 根据另一个同类型的对象显式或隐式初始化一个对象。
• 复制一个对象,将它作为实参传给一个函数。
• 从函数返回时复制一个对象。
• 初始化顺序容器中的元素。
• 根据元素初始化式列表初始化数组元素
class Foo {
public:
Foo(); // default constructor
Foo(const Foo&); // copy constructor
// ...
};
如果我们没有定义复制构造函数,编译器就会为我们合成一个
执行逐个成员初始化,将新对象初始化为原对象的副本:其中每个数据成员在构造函数初始化列表中进行初始化
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
重载操作符:操作符函数有一个返回值和一个形参表。形参表必须具有与该操作符数目相同的形参(如果操作符是一个类成员,则包括隐式 this 形参)
class Sales_item {
public:
// other members as before
// equivalent to the synthesized assignment operator
Sales_item& operator=(const Sales_item &);
};
一般而言,如果类需要复制构造函数,它也会需要赋值操作符
定义:保留字 operator 后接需定义的操作符号,重载操作符的形参数目(包括成员函数的隐式
this 指针)与操作符的操作数数目相同
作为类成员的重载函数,其形参看起来比操作数数目少 1。作为成员函数的操作符有一个隐含的 this 形参,限定为第一个操作数
Sales_item operator+(const Sales_item&, const Sales_item&);
不能重载的操作符:
重载操作符必须具有至少一个类类型或枚举类型(第 2.7 节)的操作数。这条规则强制重载操作符不能重新定义用于内置类型对象的操作符的含义
不再具备短路求值特性:重载 &&、|| 或逗号操作符不是一种好的做法
一般将算术和关系操作符定义非成员函数,而将赋值操作符定义为成员
一般将算术和关系操作符定义非成员函数,而将赋值操作符定义为成员
重载逗号、取地址、逻辑与、逻辑或等等操作符通常不是好做法。这些操作符具有有用的内置含义,如果我们定义了自己的版本,就不能再使用这些内置含义
当一个重载操作符的含义不明显时,给操作取一个名字更好。对于很少用的操作,使用命名函数通常也比用操作符更好。如果不是普通操作,没有必要为简洁而使用操作符
选择成员或非成员实现
• 赋值(=)、下标([])、调用(())和成员访问箭头(->)等操作符必须定义为成员,将这些操作符定义为非成员函数将在编译时标记为错误。
• 像赋值一样,复合赋值操作符通常应定义为类的成员,与赋值不同的是,不一定非得这样做,如果定义非成员复合赋值操作符,不会出现编译错误。
• 改变对象状态或与给定类型紧密联系的其他一些操作符,如自增、自减和解引用,通常就定义为类成员。
• 对称的操作符,如算术操作符、相等操作符、关系操作符和位操作符,最好定义为普通非成员函数。
为了与 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 操作符设为友元
定义为非成员函数
加法+操作符: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 的引用.一般而言,赋值操作符与复合赋值操作符应返回操作符的引用
箭头操作符必须定义为类成员函数。解引用操作不要求定义为成员,但将它作为成员一般也是正确的
解引用操作符和箭头操作符常用在实现智能指针(第 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
};
箭头操作符不接受显式形参,没有第二个形参,由编译器处理获取成员的工作
对重载箭头的返回值的约束: 重载箭头操作符必须返回指向类类型的指针,或者返回定义了自己的箭头操作符的类类型对象
经常由诸如迭代器这样的类实现
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
};
函数对象经常用作通用算法的实参
函数调用操作符必须声明为成员函数。一个类可以定义函数调用操作符的多个版本,由形参的数目或类型加以区别
定义了调用操作符的类,其对象常称为函数对象,即它们是行为类似函数的对象
函数对象的函数适配器:用于特化和扩展一元和二元函数对象
1)的二元函数对象类:为二元操作符定义的调用操作符需要两个给定类型的形参,而一元函数对象类型定义了接受一个实参的调用操作符
2)绑定器,是一种函数适配器,它通过将一个操作数绑定到给定值而将二元
函数对象转换为一元函数对象。
标准库定义了两个绑定器适配器:bind1st 将给定值绑定到二元函数对象的第一个实参,bind2nd 将给定值绑定到二元函数对象的第二个实参
3)求反器,是一种函数适配器,它将谓词函数对象的真值求反。
标准库还定义了两个求反器:not1 将一元函数对象的真值求反,not2 将二元函数对象的真值求反
operator type();
type 表示内置类型名、类类型名或由类型别名定义的名字。对任何可作为函数返回类型的类型(除了 void 之外)都可以定义转换函数。一般而言,不允许转换为数组或函数类型,转换为指针类型(数据和函数指针)以及引用类型是可以的
转换函数必须是成员函数,不能指定返回类型,并且形参表必须为空;必须显式返回一个指定类型的值
转换函数一般不应该改变被转换的对象。因此,转换操作符通常应定义为 const 成员
一般而言,给出一个类与两个内置类型之间的转换是不好的做法
避免二义性最好的方法是避免编写互相提供隐式转换的成对的类
避免转换函数的过度使用:避免二义性最好的方法是,保证最多只有一种途径将一个类型转换为另一类型
在调用重载函数时,需要使用构造函数或强制类型转换来转换实参,这是设计拙劣的表现
既为算术类型提供转换函数,又为同一类类型提供重载操作符,可能会导致重载操作符和内置操作符之间的二义性
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;
多态触发条件(动态绑定):第一,只有指定为虚函数的成员函数才能进行动态绑定,成员函数默认为非虚函数,非虚函数不进行动态绑定;第二,必须通过基类类型的引用或指针进行函数调用
只有通过引用或指针调用,虚函数才在运行时确定。只有在这些情况下,直到运行时才知道对象的动态类型
只有成员函数中的代码才应该使用作用域操作符覆盖虚函数机制
派生类虚函数调用基类版本时,必须显式使用作用域操作符。如果派生类函数忽略了这样做,则函数调用会在运行时确定并且将是一个自身调用,从而导致无穷递归
在同一虚函数的基类版本和派生类版本中使用不同的默认实参几乎一定会引起麻烦:如果通过基类的引用或指针调用虚函数,但实际执行的是派生类中定义的版本,这时就可能会出现问题。在这种情况下,为虚函数的基类版本定义的默认实参将传给派生类定义的版本,而派生类版本是用不同的默认实参定义的
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;
};
向基类构造函数传递实参:派生类构造函数通过将基类(类名)包含在(基类)构造函数初始化列表中来间接初
始化继承成员
一个类只能初始化自己的直接基类:原因是,类 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 节)为它的基类部分的引用,并调用基类复制构造函数
派生类定义了自己的赋值操作符,则该操作符必须对基类部分进行显式赋值
要保证运行适当的析构函数,基类中的析构函数必须为虚函数
析构函数的虚函数性质都将继承。因此,如果层次中根类的析构函数为虚函数,则派生类析构函数也将是虚函数,无论派生类显式定义析构函数还是使用合成析构函数,派生类析构函数都是虚函数
即使析构函数没有工作要做,继承层次的根类也应该定义一个虚析构函数
构造函数和赋值操作符不是虚函数。构造函数不能定义为虚函数
将类的赋值操作符设为虚函数很可能会令人混淆,而且不会有什么用处
如果在构造函数或析构函数中调用虚函数,则运行的是为构造函数或析构函数自身类型定义的版本
在函数形参表后面写上 = 0 以指定纯虚函数
class Disc_item : public Item_base {
public:
double net_price(std::size_t) const = 0;
};
将函数定义为纯虚能够说明,该函数为后代类型提供了可以覆盖的接口,但是这个类中的版本决不会调用。重要的是,用户将不能创建 Disc_item 类型的对象
含有(或继承)一个或多个纯虚函数的类是抽象基类。除了作为抽象基类的派生类的对象的组成部分,不能创建抽象类型的对象
//每个类必须重定义该虚函数
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 items(compare);
template
int compare(const T &v1, const T &v2)
{
if (v1 < v2) return -1;
if (v2 < v1) return 1;
return 0;
}
template inline T min(const T&, const T&);
template class Queue { ... };
template
Parm fcn(Parm* array, U value)
{
typename Parm::size_type * p; // ok: declares p to be a pointer
}
如果拿不准是否需要以 typename 指明一个名字是一个类型,那么指定它是个好主意。在类型之前指定 typename 没有害处,因此,即使 typename 是不必要的,也没有关系
编写模板代码时,对实参类型的要求尽可能少是很有益的
从函数实参确定模板实参的类型和值的过程叫做模板实参推断
要编译使用自己的类模板和函数模板的代码,必须查阅编译器的用户指南,看看编译器怎样处理实例化
两种模型:“包含”模型所有编译器都支持;“分别编译”模型,只有一些编译器支持
分别编译模型: export 关键字让编译器知道要记住给定的模板定义
export 关键字能够指明给定的定义可能会需要在其他文件中产生实例化
export 关键字不必在模板声明中出现,如果在头文件中使用了 export,则该头文件只能被程序中的一个源文件使用;应该在类的实现文件中使用 export
// .h头文件声明
template class Queue { ... };
// .cpp源文件定义
export template class Queue;
#include "Queue.h"
// ...
在类本身的作用域内部,可以使用类模板的非限定名,如下所示;但编译器不会为类中使用的其他模板的模板形参进行这样的推断
vector vec;可以 写为 vector vec;
类模板成员函数定义: 1)必须以关键字 template 开关,后接类的模板形参表。 2)必须指出它是哪个类的成员。 3)类名必须包含其模板形参
template ret-type Queue::member-name
非类型模板实参必须是编译时常量表达式
类模板中的友元声明:
1)普通非模板类或函数的友元声明,将友元关系授予明确指定的类或函数
template class Bar {
// grants access to ordinary, nontemplate class and function
friend class FooBar;
friend void fcn();
};
2)类模板或函数模板的友元声明,授予对友元所有实例的访问权
template class Bar {
// grants access to Foo1 or templ_fcn1 parameterized by any type
template friend class Foo1;
template friend void templ_fcn1(const T&);
};
3)只授予对类模板或函数模板的特定实例的访问权的友元声明
template class Foo2;
template void templ_fcn2(const T&);
template class Bar {
// grants access to a single specific instance parameterized by char*
friend class Foo2;
friend void templ_fcn2(char* const &);
};
友元模板声明:如果没有事先告诉编译器该友元是一个模板,则编译器将认为该友元是一个普通非模板类或非模板函数;想要限制对特定实例化的友元关系时,必须在可以用于友元声明之前声明类或函数
成员模板:任意类(模板或非模板)可以拥有本身为类模板或函数模板的成员且不能为虚。
当成员模板是类模板的成员时,它的定义必须包含类模板形参以及自己的模板形参。首先是类模板形参表,后面接着成员自己的模板形参表
使用类模板的 static 成员:,可以通过类类型的对象访问类模板的 static 成员,或者通过使用作用域操作符直接访问成员
定义 static 成员
template
size_t Foo::ctr = 0; // define and initialize ctr
函数特化定义形式: 关键字 template 后面接一对空的尖括号(<>); 再接模板名和一对尖括号,尖括号中指定这个特化定义的模板形参; 函数形参表; 函数体。
// special version of compare to handle C-style character strings
template <>
int compare(const char* const &v1,
const char* const &v2)
{
return strcmp(v1, v2);
}
与其他函数声明一样,应在一个头文件中包含模板特化的声明,然后使用该特化的每个源文件包含该头文件;声明除了函数体其余和定义一样
特化出现在对该模板实例的调用之后是错误的。对具有同一模板实参集的同一模板,程序不能既有显式特化又有实例化
声明类模板的特化:类模板特化应该与它所特化的模板定义相同的接口,否则当用户试图使用未定义的成员时会感到奇怪
template<> class Queue { ... };
类特化定义:在类特化外部定义成员时,成员之前不能加 template<> 标记
特化类的成员而不特化类,定义前加上 template<> 标记
//声明
template <>
void Queue::push(const char* const &);
//定义
template <>
void Queue::push(const char *const &val) { ... }
类模板的部分特化
类模板的部分特化本身也是模板。部分特化的定义看来像模板定义,这种定义以关键字 template 开头,接着是由尖括号(<>)括住的模板形参表。部分特化的模板形参表是对应的类模板定义形参表的子集
当声明了部分特化的时候,编译器将为实例化选择最特化的模板定义,当没有部分特化可以使用的时候,就使用通用模板定义;类模板成员的通用定义永远不会用来实例化类模板部分特化的成员
异常是通过抛出对象而引发的。该对象的类型决定应该激活哪个处理代码。被选中的处理代码是调用链中与该对象类型匹配且离抛出异常位置最近的那个
因为在处理异常的时候会释放局部存储,所以被抛出的对象就不能再局部存储,而是用 throw 表达式初始化一个称为异常对象的特殊对象
异常对象通过复制被抛出表达式的结果创建,该结果必须是可以复制的类型
执行 throw 的时候,不会执行跟在 throw 后面的语句,而是将控制从 throw 转移到匹配的 catch
异常对象与继承:当抛出一个表达式的时候,被抛出对象的静态编译时类型将决定异常对象的类型
抛出指针通常是个坏主意:抛出指针要求在对应处理代码存在的任意地方存在指针所指向的对象
栈展开(stack unwinding),沿嵌套函数调用链继续向上,直到为异常找到一个 catch 子句
为局部对象调用析构函数:栈展开期间,释放局部对象所用的内存并运行类类型局部对象的析构函数;不包括new分配的
析构函数应该从不抛出异常:标准库类型都保证它们的析构函数不会引发异常
析构函数如果又抛出自己的未经处理的另一个异常,将会导致调用标准库 terminate 函数。一般而言,terminate 函数将调用 abort 函数,强制从整个程序非正常退出
异常与构造函数:构造函数内部所做的事情经常会抛出异常;即使对象只是部分被构造了,也要保证将会适当地撤销已构造的成员
未捕获的异常终止程序:不能不处理异常。异常是足够重要的、使程序不能继续正常执行的事件。如果找不到匹配的 catch,程序就调用库函数 terminate
catch 子句中的异常说明符的类型必须是完全类型,即必须是内置类型或者是已经定义的程序员自定义类型。类型的前向声明不行
查找匹配的处理代码:最特殊的 catch 必须最先出现
除下面几种可能的区别之外,异常的类型与 catch 说明符的类型必须完全匹配:
• 允许从非 const 到 const 的转换。也就是说,非 const 对象的 throw 可以与指定接受 const 引用的 catch 匹配。
• 允许从派生类型型到基类类型的转换。
• 将数组转换为指向数组类型的指针,将函数转换为指向函数类型的适当指针
异常说明符与继承:通常,如果 catch 子句处理因继承而相关的类型的异常,它就应该将自己的形参定义为引用。如果 catch 形参是引用类型,catch 对象就直接访问异常对象,catch 对象的静态类型可以与 catch 对象所引用的异常对象的动态类型不同
带有因继承而相关的类型的多个 catch 子句,必须从最低派生类类到最高派生类型排序
重新抛出是后面不跟类型或表达式的一个 throw,将异常传递函数调用链中更上层的函数
throw;
空 throw 语句将重新抛出异常对象,它只能出现在 catch 或者从 catch 调用的函数中。如果在处理代码不活动时碰到空 throw,就调用 terminate 函数。只有当异常说明符是引用的时候,才会传播那些改变
// matches any exception that might be thrown
catch (...) { }
构造函数要处理来自构造函数初始化式的异常,唯一的方法是将构造函数编写为函数测试块
template Handle::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); }
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
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;
p_auto.reset(new int(1024));
警告: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;
名字冲突问题称为命名空间污染问题
像其他名字一样,命名空间的名字在定义该命名空间的作用域中必须是唯一的。命名空间可以在全局作用域或其他作用域内部定义,但不能在函数或类内部定义
namespace cplusplus_primer {
// 全局作用域的任意声明:类、变量(以及它们的初始化)、函数
//(以及它们的定义)、模板以及其他命名空间
}
命名空间作用域不能以分号结束
每个命名空间是一个作用域
定义多个不相关类型的命名空间应该使用分离的文件,表示该命名空间定义的每个类型
全局命名空间:全局命名空间是隐式声明的,存在于每个程序中。因为全局命名空间是隐含的,它没有名字
::member_name
未命名的命名空间与其他命名空间不同,未命名的命名空间的
定义局部于特定文件,从不跨越多个文本文件
如果头文件定义了未命名的命名空间,那么,在每个包含该头文件的文件中,该命名空间中的名字将定义不同的局部实体
C++ 不赞成文件静态(static)声明。在未来版本中可能不支持。应该避免文件静态而使用未命名空间代替
除了在函数或其他作用域内部,头文件不应该包含 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
多重继承
在一个给定派生列表中,一个基类只能出现一次
构造函数调用次序既不受构造函数初始化列表中出现的基类的影响,也不受基类在构造函数初始化列表中的出现次序的影响
多重继承派生类的复制控制
像单继承(第 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 { ... };
虚基类的声明
指定虚派生只影响从指定了虚基类的类派生的类。除了影响派生类自己的对象之外,它也是关于派生类与自己的未来派生类的关系的一个陈述。陈述了在后代派生类中共享指定基类的单个实例的愿望
特定派生类实例的优先级高于共享虚基类实例。像非虚多重继承层次一样,这种二义性最好用在派生类中提供覆盖实例的类来解决
在虚派生中,由最低层派生类(此类没有子类)的构造函数初始化虚基类
无论虚基类出现在继承层次中任何地方,总是在构造非虚基类之前构造虚基类、
许多程序员从不(或者很少)需要使用本章所介绍的这些特征
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 成员的低级版本,它们都分配但不初始化内存
在已分配的原始内存中初始化一个对象,它不分配内存
语法形式: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 函数总是使用复制构造函数
对于使用定位 new 表达式构造对象的程序,显式调用析构函数
p->~T(); // call the destructor
调用 operator 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 函数
自由列表:已分配但未构造的对象的链表
程序能够使用基类的指针或引用来检索这些指针或引用所指对象的实际派生类型
与 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(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(b);
// use the Derived object to which b referred
} catch (bad_cast) {
// handle the fact that the cast failed
}
表达式
typeid(e)
//使用例子
typeid(e).name(); //返回 C 风格字符串
可以与任何类型的表达式一起使用。结果是名为 type_info 的标准库类型的对象引用头文件 typeinfo
最常见的用途是比较两个表达式的类型,或者将表达式的类型与特定类型相比较
程序中创建 type_info 对象的唯一方法是使用 typeid 操作符
默认构造函数和复制构造函数以及赋值操作符都定义为 private,所以不能定义或复制 type_info 类型的对象
成员指针只应用于类的非 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;
成员指针解引用操作符(.*)从对象或引用获取成员
成员指针箭头操作符(->*)通过对象的指针获取成员
使用成员函数的指针
// 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
嵌套在类模板内部的类是模板,嵌套类成员的名字在类外部是不可见的
定义嵌套类的成员
在其类外部定义的嵌套类成员,必须定义在定义外围类的同一作用域中。在其类外部定义的嵌套类的成员,不能定义在外围类内部,嵌套类的成员不是外围类的成员
// defines the QueueItem constructor
// for class QueueItem nested inside class Queue
template
Queue::QueueItem::QueueItem(const Type &t):
item(t), next(0) { }
在外围类外部定义嵌套类
template 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
struct Queue::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
template
int Queue::QueueItem::static_mem = 1024;
嵌套类可以直接引用外围类的静态成员、类型名和枚举成员
嵌套模板的实例化:实例化外围类模板的时候,不会自动实例化类模板的嵌套类。像任何成员函数一样,只有当在需要完整类类型的情况下使用嵌套类本身的时候,才会实例化嵌套类
一种特殊的类,一个 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;
}
函数体内部定义的类,局部类的成员是严格受限的
一种特殊的类数据成员,保存特定的位数。当程序需要将二进制数据传递给另一程序或硬件设备的时候,通常使用位域
位域在内存中的布局是机器相关的
必须是整型数据类型。在成员名后面接一个冒号以及指定位数的常量表达式,指出成员是一个位域
typedef unsigned int Bit;
class File {
Bit mode: 2;
Bit modified: 1;
Bit prot_owner: 3;
};
通常最好将位域设为 unsigned 类型。存储在 signed 类型中的位域的行为由实现定义
定义了位域成员的类通常也定义一组内联成员函数来测试和设置位域的值
地址操作符(&)不能应用于位域,所以不可能有引用类位域的指针,位域也不能是类的静态成员
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 的原因密切相关
C++ 使用链接指示指出任意非 C++ 函数所用的语言
声明非 C++ 函数:链接指示有两种形式单个的或复合的
链接指示不能出现在类定义或函数定义的内部,它必须出现在函数的第一次声明上
extern "C" {
int strcmp(const char*, const char*);
char *strcat(char*, const char*);
}
链接指示与头文件
extern "C" {
#include // 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 *);