C++ PRIMER 笔记
-----------------------------------7.2.2--------------------------------------------------------
考虑下面不适宜复制实参的例子,该函数希望交换两个实参的值:
// incorrect version of swap: The arguments are not changed!
void swap(int v1, int v2)
{
int tmp = v2;
v2 = v1; // assigns new value to local copy of the argument
v1 = tmp;
} // local objects v1 and v2 no longer exist
这个例子期望改变实参本身的值。但对于上述的函数定义,swap 无法影响实参本身。执行 swap 时,只交换了其实参的局部副本,而传递 swap 的实参并没有修改:
int main()
{
int i = 10;
int j = 20;
cout << "Before swap():\ti: "
<< i << "\tj: " << j << endl;
swap(i, j);
cout << "After swap():\ti: "
<< i << "\tj: " << j << endl;
return 0;
}
编译并执行程序,产生如下输出结果:
Before swap(): i: 10 j: 20
After swap(): i: 10 j: 20
为了使 swap 函数以期望的方式工作,交换实参的值,需要将形参定义为引用类型:
// ok: swap acts on references to its arguments
void swap(int &v1, int &v2)
{
int tmp = v2;
v2 = v1;
v1 = tmp;
}
与所有引用一样,引用形参直接关联到其所绑定的圣贤,而并非这些对象的副本。定义引用时,必须用与该引用绑定的对象初始化该引用。引用形参完全以相同的方式工作。每次调用函数,引用形参被创建并与相应实参关联。此时,当调用 swap
swap(i, j);
形参 v1 只是对象 i 的另一个名字,而 v2 则是对象 j 的另一个名字。对 v1 的任何修改实际上也是对 i 的修改。同样地,v2 上的任何修改实际上也是对 j 的修改。重新编译使用 swap 的这个修订版本的 main 函数后,可以看到输出结果是正确的:
Before swap(): i: 10 j: 20
After swap(): i: 20 j: 10
Tips: 从 C 语言背景转到 C++ 的程序员习惯通过传递指针来实现对实参的访问。在 C++中,使用引用形参则更安全和更自然。
-----------------------------------7.3.2---------------------------------------------
千万不要返回局部对象的引用
理解返回引用至关重要的是:千万不能返回局部变量的引用。
当函数执行完毕时,将释放分配给局部对象的存储空间。此时,对局部对象的引用就会指向不确定的内存。考虑下面的程序:
// Disaster: Function returns a reference to a local object
const string &manip(const string& s)
{
string ret = s;
// transform ret in some way
return ret; // Wrong: Returning reference to a local object!
}
这个函数会在运行时出错,因为它返回了局部对象的引用。当函数执行完毕,字符串 ret 占用的储存空间被释放,函数返回值指向了对于这个程序来说不再有效的内存空间。
确保返回引用安全的一个好方法是:请自问,这个引用指向哪个在此之前存在的对象?
千万不要返回指向局部对象的指针
函数的返回类型可以是大多数类型。特别地,函数也可以返回指针类型。和返回局部对象的引用一样,返回指向局部对象的指针也是错误的。一旦函数结束,局部对象被释放,返回的指针就变成了指向不再存在的对象的悬垂指针(第 5.11 节)。
------------------------------7.8.2.函数匹配与实参转换----------------------------------------------------
大多数情况下,编译器都可以直接明确地判断一个实际的调用是否合法,如果合法,则应该调用哪一个函数。重载集合中的函数通常有不同个数的参数或无关联的参数类型。当多个函数的形参具有可通过隐式转换(第 5.12 节)关联起来的类型,则函数匹配将相当灵活。在这种情况下,需要程序员充分地掌握函数匹配的过程。
考虑下面的这组函数和函数调用:
void f();
void f(int);
void f(int, int);
void f(double, double = 3.14);
f(5.6); // calls void f(double, double)
候选函数
函数重载确定的第一步是确定该调用所考虑的重载函数集合,该集合中的函数称为候选函数。候选函数是与被调函数同名的函数,并且在调用点上,它的声明可见。在这个例子中,有四个名为 f 的候选函数。
选择可行函数
第二步是从候选函数中选择一个或多个函数,它们能够用该调用中指定的实参来调用。因此,选出来的函数称为可行函数。可行函数必须满足两个条件:第一,函数的形参个数与该调用的实参个数相同;第二,每一个实参的类型必须与对应形参的类型匹配,或者可被隐式转换为对应的形参类型。
寻找最佳匹配(如果有的话)
函数重载确定的第三步是确定与函数调用中使用的实际参数匹配最佳的可行函数。这个过程考虑函数调用中的每一个实参,选择对应形参与之最匹配的一个或多个可行函数。这里所谓“最佳”的细节将在下一节中解释,其原则是实参类型与形参类型越接近则匹配越佳。因此,实参类型与形参类型之间的精确类型匹配比需要转换的匹配好。
在上述例子中,只需考虑一个 double 类型的显式实参。如果调用 f(int),实参需从 double 型转换为 int型。而另一个可行函数 f(double, double) 则与该实参精确匹配。由于精确匹配优于需要类型转换的匹配,因此编译器将会把函数调用 f(5.6) 解释为对带有两个 double 形参的 f 函数的调用。
含有多个形参的重载确定
如果函数调用使用了两个或两个以上的显式实参,则函数匹配会更加复杂。假设有两样的名为 f 的函数,分析下面的函数调用:
f(42, 2.56);
在本例子的调用中,首先分析第一个实参,发现函数 f(int, int) 匹配精确。如果使之与第二个函数匹配,就必须将 int型实参 42 转换为 double 型的值。通过内置转换的匹配“劣于”精确匹配。所以,如果只考虑这个形参,带有两个 int型形参的函数比带有两个 double 型形参的函数匹配更佳。
但是,当分析第二个实参时,有两个 double 型形参的函数为实参 2.56 提供了精确匹配。而调用两个 int型形参的 f 函数版本则需要把 2.56 从 double 型转换为 int型。所以只考虑第二个形参的话,函数 f(double, double) 匹配更佳。
因此,这个调用有二义性:每个可行函数都对函数调用的一个实参实现更好的匹配。编译器将产生错误。解决这样的二义性,可通过显式的强制类型转换强制函数匹配:
f(static_cast<double>(42), 2.56); // calls f(double, double)
f(42, static_cast<int>(2.56)); // calls f(int, int)
在实际应用中,调用重载函数时应尽量避免对实参做强制类型转换:需要使用强制类型转换意味着所设计的形参集合不合理。
----------------------------8.2 条件状态-------------------------------
表 8.2. IO 标准库的条件状态
strm::iostate |
机器相关的整型名,由各个 iostream 类定义,用于定义条件状态 |
strm::badbit |
strm::iostate类型的值,用于指出被破坏的流 |
strm::failbit |
strm::iostate类型的值,用于指出失败的 IO 操作 |
strm::eofbit |
strm::iostate类型的值,用于指出流已经到达文件结束符 |
s.eof() |
如果设置了流 s 的 eofbit 值,则该函数返回 true |
s.fail() |
如果设置了流 s 的 failbit 值,则该函数返回 true |
s.bad() |
如果设置了流 s 的 badbit 值,则该函数返回 true |
s.good() |
如果流 s 处于有效状态,则该函数返回 true |
s.clear() |
将流 s 中的所有状态值都重设为有效状态 |
s.clear(flag) |
将流 s 中的某个指定条件状态设置为有效。flag 的类型是 strm::iostate |
s.setstate(flag) |
给流 s 添加指定条件。flag 的类型是 strm::iostate |
s.rdstate() |
返回流 s 的当前条件,返回值类型为 strm::iostate |
badbit标志着系统级的故障,如无法恢复的读写错误。如果出现了这类错误,则该流通常就不能再继续使用了。如果出现的是可恢复的错误,如在希望获得数值型数据时输入了字符,此时则设置 failbit标志,这种导致设置 failbit的问题通常是可以修正的。eofbit是在遇到文件结束符时设置的,此时同时还设置了 failbit。
流的状态由 bad、fail、eof和 good操作提示。如果 bad、fail或者 eof中的任意一个为 true,则检查流本身将显示该流处于错误状态。类似地,如果这三个条件没有一个为 true,则 good操作将返回 true。
clear和 setstate操作用于改变条件成员的状态。clear操作将条件重设为有效状态。在流的使用出现了问题并做出补救后,如果我们希望把流重设为有效状态,则可以调用 clear操作。使用 setstate操作可打开某个指定的条件,用于表示某个问题的发生。除了添加的标记状态,setstate将保留其他已存在的状态变量不变。
流状态的查询和控制
int ival;
// read cin and test only for EOF; loop is executed even if there are other IO failures
while (cin >> ival, !cin.eof()) {
if (cin.bad()) // input stream is corrupted; bail out
throw runtime_error("IO stream corrupted");
if (cin.fail()) { // bad input
cerr<< "bad data, try again"; // warn the user
cin.clear(istream::failbit); // reset the stream
continue; // get next input
}
// ok to process ival
}
这个循环不断读入 cin,直到到达文件结束符或者发生不可恢复的读取错误为止。循环条件使用了逗号操作符(第 5.9 节)。回顾逗号操作符的求解过程:首先计算它的每一个操作数,然后返回最右边操作数作为整个操作的结果。因此,循环条件只读入 cin而忽略了其结果。该条件的结果是 !cin.eof()的值。如果 cin到达文件结束符,条件则为假,退出循环。如果 cin没有到达文件结束符,则不管在读取时是否发生了其他可能遇到的错误,都进入循环。
在循环中,首先检查流是否已破坏。如果是的放,抛出异常并退出循环。如果输入无效,则输出警告并清除 failbit状态。在本例中,执行 continue语句(第 6.11 节)回到 while的开头,读入另一个值 ival。如果没有出现任何错误,那么循环体中余下的部分则可以很安全地使用 ival。
警告:如果程序崩溃了,则不会刷新缓冲区
如果程序不正常结束,输出缓冲区将不会刷新。在尝试调试已崩溃的程序时,通常会根据最后的输出找出程序发生错误的区域。如果崩溃出现在某个特定的输出语句后面,则可知是在程序的这个位置之后出错。
调试程序时,必须保证期待写入的每个输出都确实被刷新了。因为系统不会在程序崩溃时自动刷新缓冲区,这就可能出现这样的情况:程序做了写输出的工作,但写的内容并没有显示在标准输出上,仍然存储在输出缓冲区中等待输出。
如果需要使用最后的输出给程序错误定位,则必须确定所有要输出的都已经输出。为了确保用户看到程序实际上处理的所有输出,最好的方法是保证所有的输出操作都显式地调用了 flush或 endl。
如果仅因为缓冲区没有刷新,程序员将浪费大量的时间跟踪调试并没有执行的代码。基于这个原因,输出时应多使用 endl而非 '\n'。使用 endl则不必担心程序崩溃时输出是否悬而未决(即还留在缓冲区,未输出到设备中)。
将输入和输出绑在一起
交互式系统通常应确保它们的输入和输出流是绑在一起的。这样做意味着可以保证任何输出,包括给用户的提示,都在试图读之前输出。
警告:C++ 中的文件名
由于历史原因,IO 标准库使用 C 风格字符串(第 4.3 节)而不是 C++ strings类型的字符串作为文件名。在创建 fstream对象时,如果调用 open或使用文件名作初始化式,需要传递的实参应为 C 风格字符串,而不是标准库 strings对象。程序常常从标准输入获得文件名。通常,比较好的方法是将文件名读入 string 对象,而不是 C 风格字符数组。假设要使用的文件名保存在 string对象中,则可调用 c_str成员(第 4.3.2 节)获取 C 风格字符串。
检查文件打开是否成功
// check that the open succeeded
if (!infile) {
cerr << "error: unable to open input file: "
<< ifile << endl;
return -1;
}
清除文件流的状态
考虑这样的程序,它有一个 vector对象,包含一些要打开并读取的文件名,程序要对每个文件中存储的单词做一些处理。假设该 vector对象命名为 files,程序也许会有如下循环:
// for each file in the vector
while (it != files.end()) {
ifstream input(it->c_str()); // open the file;
// if the file is ok, read and "process" the input
if (!input)
break; // error: bail out!
while(input >> s) // do the work on this file
process(s);
++it; // increment iterator to get next file
}
每一次循环都构造了名为 input的 ifstream对象,打开并读取指定的文件。构造函数的初始化式使用了箭头操作符(第 5.6 节)对 it进行解引用,从而获取 it当前表示的 string对象的 c_str成员。文件由构造函数打开,并假设打开成功,读取文件直到到达文件结束符或者出现其他的错误条件为止。在这个点上,input处于错误状态。任何读 input的尝试都会失败。因为 input是 while循环的局部变量,在每次迭代中创建。这就意味着它在每次循环中都以干净的状态即 input.good()为 true,开始使用。
如果希望避免在每次 while循环过程中创建新流对象,可将 input的定义移到 while之前。这点小小的改动意味着必须更仔细地管理流的状态。如果遇到文件结束符或其他错误,将设置流的内部状态,以便之后不允许再对该流做读写操作。关闭流并不能改变流对象的内部状态。如果最后的读写操作失败了,对象的状态将保持为错误模式,直到执行 clear操作重新恢复流的状态为止。调用 clear后,就像重新创建了该对象一样。
如果打算重用已存在的流对象,那么 while循环必须在每次循环进记得关闭(close)和清空(clear)文件流:
ifstream input;
vector<string>::const_iterator it = files.begin();
// for each file in the vector
while (it != files.end()) {
input.open(it->c_str()); // open the file
// if the file is ok, read and "process" the input
if (!input)
break; // error: bail out!
while(input >> s) // do the work on this file
process(s);
input.close(); // close file when we're done with it
input.clear(); // reset state to ok
++it; // increment iterator to get next file
}
如果忽略 clear的调用,则循环只能读入第一个文件。要了解其原因,就需要考虑在循环中发生了什么:首先打开指定的文件。假设打开成功,则读取文件直到文件结束或者出现其他错误条件为止。在这个点上,input处于错误状态。如果在关闭(close)该流前没有调用 clear清除流的状态,接着在 input上做的任何输入运算都会失败。一旦关闭该文件,再打开 下一个文件时,在内层 while循环上读 input仍然会失败——毕竟最后一次对流的读操作到达了文件结束符,事实上该文件结束符对应的是另一个与本文件无关的其他文件。
Beware: |
如果程序员需要重用文件流读写多个文件,必须在读另一个文件之前调用 clear 清除该流的状态。 |
表 8.3 文件模式
in |
打开文件做读操作 |
out |
打开文件做写操作 |
app |
在每次写之前找到文件尾 |
ate |
打开文件后立即将文件定位在文件尾 |
trunc |
打开文件时清空已存在的文件流 |
binary |
以二进制模式进行 IO 操作 |
out、trunc和 app模式只能用于指定与 ofstream或 fstream对象关联的文件;in模式只能用于指定与 ifstream或 fstream对象关联的文件。所有的文件都可以用 ate或 binary模式打开。ate模式只在打开时有效:文件打开后将定位在文件尾。以 binary模式打开的流则将文件以字节序列的形式处理,而不解释流中的字符。
默认时,与 ifstream流对象关联的文件将以 in模式打开,该模式允许文件做读的操作:与 ofstream关联的文件则以 out模式打开,使文件可写。以 out模式打开的文件会被清空:丢弃该文件存储的所有数据。
Beware: |
从效果来看,为 ofstream对象指定 out模式等效于同时指定了 out和 trunc模式。 |
--------------------------9.3 顺序容器的操作---------------------------------------
9.3.1容器定义的类型别名
使用容器定义类型的表达式看上去非常复杂:
// iter is the iterator type defined by list<string>
list<string>::iterator iter;
// cnt is the difference_type type defined by vector<int>
vector<int>::difference_type cnt;
iter 所声明使用了作用域操作符,以表明此时所使用的符号 :: 右边的类型名字是在符号 iter 左边指定容器的作用域内定义的。其效果是将 iter 声明为 iterator类型,而 iterator 是存放 string 类型元素的 list类的成员。
正如我们在第 9.4 节中了解的一样,在 vector 容器中添加元素可能会导致整个容器的重新加载,这样的话,该容器涉及的所有迭代器都会失效。即使需要重新加载整个容器,指向新插入元素后面的那个元素的迭代器也会失效。
|
任何 insert 或 push 操作都可能导致迭代器失效。当编写循环将元素插入到 vector 或 deque 容器中时,程序必须确保迭代器在每次循环后都得到更新。 |
避免存储 end 操作返回的迭代器
在 vector 或 deque 容器中添加元素时,可能会导致某些或全部迭代器失效。假设所有迭代器失效是最安全的做法。这个建议特别适用于由 end 操作返回的迭代器。在容器的任何位置插入任何元素都会使该迭代器失效。
例如,考虑一个读取容器中每个元素的循环,对读出元素做完处理后,在原始元素后面插入一个新元素。我们希望该循环可以处理每个原始元素,然后使用 insert 函数插入新元素,并返回指向刚插入元素的迭代器。在每次插入操作完成后,给返回的迭代器自增 1,以使循环定位在下一个要处理的原始元素。如果我们尝试通过存储 end() 操作返回的迭代器来“优化”该循环,将导致灾难性错误:
vector<int>::iterator first = v.begin(),
last = v.end(); // cache end iterator
// diaster: behavior of this loop is undefined
while (first != last) {
// do some processing
// insert new value and reassign first, which otherwise would be invalid
first = v.insert(first, 42);
++first; // advance first just past the element we added
}
上述代码的行为未定义。在很多实现中,该段代码将导致死循环。问题在于这个程序将 end 操作返回的迭代器值存储在名为 last 的局部变量中。循环体中实现了元素的添加运算,添加元素会使得存储在 last 中的迭代器失效。该迭代器既没有指向容器 v 的元素,也不再指向 v 的超出末端的下一位置。
|
不要存储 end 操作返回的迭代器。添加或删除 deque 或 vector 容器内的元素都会导致存储的迭代器失效。 |
为了避免存储 end 迭代器,可以在每次做完插入运算后重新计算 end 迭代器值:
// safer: recalculate end on each trip whenever the loop adds/erases elements
while (first != v.end()) {
// do some processing
first = v.insert(first, 42); // insert new value
++first; // advance first just past the element we added
}
通常,程序员必须在容器中找出要删除的元素后,才使用 erase操作。寻找一个指定元素的最简单方法是使用标准库的 find算法。我们将在第 11.1 节中进一步讨论 find算法。为了使用 find函数或其他泛型算法,在编程时,必须将 algorithm头文件包含进来。find函数需要一对标记查找范围的迭代器以及一个在该范围内查找的值作参数。查找完成后,该函数返回一个迭代器,它指向具有指定值的第一个元素,或超出末端的下一位置。
string searchValue("Quasimodo");
list<string>::iterator iter = find(slist.begin(), slist.end(), searchValue);
if (iter != slist.end())
slist.erase(iter);
----------------------------9.4 vector 容器的自增长-------------------------------2013-11-03 21:31:05
一般来说,我们不应该关心标准库类型是如何实现的:我们只需要关心如何使用这些标准库类型就可以了。然而,对于 vector 容器,有一些实现也与其接口相关。为了支持快速的随机访问,vector 容器的元素以连续的方式存放——每一个元素都紧挨着前一个元素存储。
已知元素是连续存储的,当我们在容器内添加一个元素时,想想会发生什么事情:如果容器中已经没有空间容纳新的元素,此时,由于元素必须连续存储以便索引访问,所以不能在内存中随便找个地方存储这个新元素。于是,vector 必须重新分配存储空间,用来存放原来的元素以及新添加的元素:存放在旧存储空间中的元素被复制到新存储空间里,接着插入新元素,最后撤销旧的存储空间。如果 vector 容器在在每次添加新元素时,都要这么分配和撤销内存空间,其性能将会非常慢,简直无法接受。
对于不连续存储元素的容器,不存在这样的内存分配问题。例如,在 list 容器中添加一个元素,标准库只需创建一个新元素,然后将该新元素连接在已存在的链表中,不需要重新分配存储空间,也不必复制任何已存在的元素。
由此可以推论:一般而言,使用 list 容器优于 vector 容器。但是,通常出现的反而是以下情况:对于大部分应用,使用 vector 容器是最好的。原因在于,标准库的实现者使用这样内存分配策略:以最小的代价连续存储元素。由此而带来的访问元素的便利弥补了其存储代价。
为了使 vector 容器实现快速的内存分配,其实际分配的容量要比当前所需的空间多一些。vector 容器预留了这些额外的存储区,用于存放新添加的元素。于是,不必为每个新元素重新分配容器。所分配的额外内存容量的确切数目因库的实现不同而不同。比起每添加一个新元素就必须重新分配一次容器,这个分配策略带来显著的效率。事实上,其性能非常好,因此在实际应用中,比起 list 和 deque 容器,vector 的增长效率通常会更高。