1:枚举不但定义了整数常量集,而且还把它们聚集成组;
(1):用来初始化枚举成员的值必须是一个常量表达式。常量表达式是编译器在编译时就能够计算出结果的整型表达式。整型字面值常量是常量表达式,正如一个通过常量表达式自我初始化的 const对象也是常量表达式一样。
(2):枚举成员值可以是不唯一的。
enum Points { point2d = 2, point2w,point3d = 3, point3w };// point2d is 2, point2w is 3, point3d is 3, point3w is 4
(3):不能改变枚举成员的值。枚举成员本身就是一个常量表达式,所以也可用于需要常量表达式的任何地方
(4):Points pt2w = 3; // error注意把 3 赋给 Points 对象是非法的,即使 3 与一个 Points 枚举成员相关联。
2:C++ 支持另一个关键字 struct,它也可以定义类类型。struct 关键字是从C 语言中继承过来的。如果使用 class 关键字来定义类,那么定义在第一个访问标号(public、private、、、)前的任何成员都隐式指定为 private;如果使用 struct 关键字,那么这些成员都是public。用 class 和 struct 关键字定义类的唯一差别在于默认访问级别:默认情况下,struct 的成员为 public,而 class 的成员为 private。
3:为了允许把程序分成独立的逻辑块,C++ 支持所谓的分别编译。这样程序可以由多个文件组成,现在的编译器都有 Time stamp 的功能,编译器在编译整个工程的时候,它只会编译那些经过修改的文件,而不会去编译那些从上次编译过,到现在没有被修改过的文件。那么为什么还要预编译头文件呢?答案在这里,我们知道编译器是以文件为单位编译的,一个文件经过修改后,会重新编译整个文件,当然在这个文件里包含的所有头文件中的东西( .eg Macro, Preprocessor )都要重新处理一遍。 预编译头文件保存的正是这部分信息。以避免每次都要重新处理这些头文件(为了减少处理头文件的编译时间,有些 C++的实现支持预编译头文件)。
4:头文件用于声明而不是用于定义,因为头文件包含在多个源文件中,所以不应该含有变量或函数的定义。对于头文件不应该含有定义这一规则,有三个例外。头文件可以定义类、值在编译时就已知道的 const 对象和 inline 函数。这些实体可在多个源文件中定义,只要每个源文件中的定义是相同的。在头文件中定义这些实体,是因为编译器需要它们的定义(不只是声明)来产生代码。例如:为了产生能定义或使用类的对象的代码,编译器需要知道组成该类型的数据成员。同样还需要知道能够在这些对象上执行的操作。类定义提供所需要的信息。又如:const 变量默认时是定义该变量的文件的局部变量。正如我们现在所看到的,这样设置默认情况的原因在于允许 const 变量定义在头文件中。但是,C++ 中的任何变量都只能定义一次。定义会分配存储空间,而所有对该变量的使用都关联到同一存储空间。因为 const 对象默认为定义它的文件的局部变量,所以把它们的定义放在头文件中是合法的。这种行为有一个很重要的含义:当我们在头文件中定义了 const 变量后,每个包含该头文件的源文件都有了自己的 const 变量,其名称和值都一样。当该 const 变量是用常量表达式初始化时,可以保证所有的变量都有相同的值。但是在实践中,大部分的编译器在编译时都会用相应的常量表达式替换这些 const 变量的任何使用。所以,在实践中不会有任何存储空间用于存储用常量表达式初始化的 const 变量。如果 const 变量不是用常量表达式初始化,那么它就不应该在头文件中定义。相反,和其他的变量一样,该 const 变量应该在一个源文件中定义并初始化。应在头文件中为它添加 extern 声明,以使其能被多个文件共享。
5:const对象(在这里的对象可指内置基本对象也可以指类对象),因为常量在定义后就不能被修改,所以定义时必须初始化,与其他变量不同,除非特别说明,在全局作用域声明的 const 变量是定义该对象的文件的局部变量。此变量只存在于那个文件中,不能被其他文件访问。通过指定 const 变更为 extern,就可以在整个程序中访问 const 对象
6:extern 声明不是定义,也不分配存储空间。事实上,它只是说明变量定义在程序的其他地方。C++是一种静态类型语言:变量和函数在使用前必须先声明,程序中变量可以声明多次,但只能定义一次。只有当声明也是定义时,声明才可以有初始化式,因为只有定义才分配存储空间。初始化式必须要有存储空间来进行初始化。如果声明有初始化式,那么它可被当作是定义,即使声明标记为extern double pi = 3.1416; // definition,虽然使用了 extern ,但是这条语句还是定义了 pi,分配并初始化了存储空间。只有当 extern 声明位于函数外部时,才可以含有初始化式。
7:引用就是对象的另一个名字。在实际程序中,引用主要用作函数的形式参数。引用是一种复合类型,通过在变量名前添加“&”符号来定义,不能定义引用类型的引用,但可以定义任何其他类型的引用。引用必须用与该引用同类型的对象初始化,可以在一个类型定义行中定义多个引用。必须在每个引用标识符前添加“&”符号,const 引用是指向 const 对象的引用。const 引用则可以绑定到不同但相关的类型的对象或绑定到右值。非 const 引用只能绑定到与该引用同类型的对象。
const 引用可以初始化为不同类型的对象或者初始化为右值,如字面值常量:
int i = 42;
// legal for const references only
const int &r = 42;
const int &r2 = r + i;
同样的初始化对于非 const 引用却是不合法的,而且会导致编译时错误。其原因非常微妙,值得解释一下。观察将引用绑定到不同的类型时所发生的事情,最容易理解上述行为。假如我们编写
double dval = 3.14;
const int &ri = dval;
编译器会把这些代码转换成如以下形式的编码:
int temp = dval; // create temporary int from the double
const int &ri = temp; // bind ri to that temporary
如果 ri 不是 const,那么可以给 ri 赋一新值。这样做不会修改 dval,而是修改了 temp。期望对 ri 的赋值会修改 dval 的程序员会发现 dval 并没有被修改。仅允许const 引用绑定到需要临时使用的值完全避免了这个问题,因为 const 引用是只读的。:
8:进行 string 对象和字符串字面值混合连接操作时,+ 操作符的左右操作数必须至少有一个是 string 类型的
string s6 = "hello" + ", " + s2; // error: can't add string literals
9:所有的标准库容器都定义了相应的迭代器类型,而只有少数的容器支持下标操作。因为迭代器对所有的容器都适用,现代 C++ 程序更倾向于使用迭代器而不是下标操作访问容器元素,迭代器类型可使用解引用操作符(dereference operator)(*)来访问迭
代器所指向的元素
// reset all the elements in ivec to 0
for (vector<int>::size_type ix = 0; ix != ivec.size(); ++ix)
ivec[ix] = 0;
// equivalent loop using iterators to reset all the elements in ivecto 0
for (vector<int>::iterator iter = ivec.begin();iter != ivec.end(); ++iter)
*iter = 0; // set element to which iter refers to 0
vector::iterator 改变 vector 中的元素值。每种容器类型还定义了一种名为 const_iterator 的类型,使用 const_iterator 类型时,我们可以得到一个迭代器,它自身的值可以改变,但不能用来改变其所指向的元素的值。可以对迭代器进行自增以及使用解引用操作符来读取值,但不能对该元素赋值。不要把 const_iterator 对象与 const 的 iterator 对象混淆起来(就好比C中的常量指针和指针常量)。声明一个 const 迭代器时,必须初始化迭代器。一旦被初始化后,就不能改变它的值
vector<int> nums(10); // nums is nonconst
const vector<int>::iterator cit = nums.begin();
*cit = 1; // ok: cit can change its underlying element
++cit; // error: can't change the value of cit
const_iterator 对象可以用于 const vector 或非 const vector,因为不能改写元素值。const 迭代器这种类型几乎没什么用处:一旦它被初始化后,只能用它来改写其指向的元素,但不能使它指向任何其他元素。
任何改变 vector 长度的操作都会使已存在的迭代器失效。例如,在调用 push_back 之后,就不能再信赖指向 vector 的迭代器的值了
10:在函数体外定义的内置数组,其元素均初始化为 0。在函数体内定义的内置数组,其元素无初始化。不管数组在哪里定义,如果其元素为类类型,则自动调用该类的默认构造函数进行初始化;如果该类没有默认构造函数,则必须为该数组的元素提供显式初始化。除非显式地提供元素初值,否则内置类型的局部数组的元素没有初始化。此时,除了给元素赋值外,其他使用这些元素的操作没有定。
11:字符数组既可以用一组由花括号括起来、逗号隔开的字符字面值进行初始化,也可以用一个字符串字面值进行初始化。然而,要注意这两种初始化形式并不完全相同,字符串字面值包含一个额外的空字符(null)用于结束字符串。与 vector 不同,一个数组不能用另外一个数组初始化,也不能将一个数组赋值给另一个数组,这些操作都是非法的。
12:把 int 型变量赋给指针是非法的,尽管此 int 型变量的值可能为 0。但允许把数值 0 或在编译时可获得 0 值的 const 量赋给指针,除了使用数值0 或在编译时值为 0 的 const 量外,还可以使用 C++ 语言从 C 语言中继承下来的预处理器变量 NULL(第 2.9.2 节),该变量在 cstdlib头文件中定义,其值为 0。如果在代码中使用了这个预处理器变量,则编译时会自动被数值 0 替换。因此,把指针初始化为 NULL 等效于初始化为 0 值,void* 表明该指针与一地址值相关,但不清楚存储在此地址上的对象的类型。void* 指针只支持几种有限的操作:与另一个指针进行比较;向函数传递void* 指针或从函数返回 void* 指针;给另一个 void* 指针赋值。不允许使用void* 指针操纵它所指向的对象。C++ 允许计算数组或对象的超出末端的地址,但不允许对此地址进行解引用操作。而计算数组超出末端位置之后或数组首地址之前的地址都是不合法的
13:对于位操作符,由于系统不能确保如何处理其操作数的符号位,所以强烈建议使用unsigned 整型操作数;
赋值操作符的左操作数必须是非 const 的左值(表示地址);
赋值操作具有右结合特性;
输入输出标准库(IO library)分别重载了位操作符 >> 和 << 用于输入和输出。即使很多程序员从未直接使用过位操作符,但是相当多的程序都大量用到这些操作符在IO 标准库中的重载版本。重载的操作符与该操作符的内置类型版本有相同的优先级和结合性。因此,即使程序员从不使用这些操作符的内置含义来实现移位操作,但是还是应该先了解这些操作符的优先级和结合性。移位操作符也是左结合的,所以IO 操作符为左结合,移位操作符具有中等优先级:其优先级比算术操作符低,但比关系操作符、赋值操作符和条件操作符优先级高。
cout << 10 < 42; // error: attempt to compare cout to 42!
建议:只有在必要时才使用后置操作符;有使用 C 语言背景的读者可能会觉得奇怪,为什么要在程序中使用前自增操作。道理很简单:因为前置操作需要做的工作更少,只需加 1 后返回加 1 后的结果即可。而后置操作符则必须先保存操作数原来的值, 加 1后再返回原值的副本。
条件操作符是 C++ 中唯一的三元操作符,它允许将简单的 if-else 判断语句嵌入表达式中,条件操作符的优先级相当低;
14:sizeof 操作符的作用是返回一个对象(广义的)或类型名的长度,返回值的类型为size_t,长度的单位是字节。sizeof是编译时计算的,将 sizeof 用于 表达式时,并没有计算表达式的值sizeof(exper)==sizeof exper。
15:复合表达式的求值顺序依赖于编译器,
eg:一个表达式里,不要在两个或更多的子表达式中对同一对象做自增或自减操作。
假设 index 初值为 0,编译器可以用下面两种方式之一求该表达式的值:
if (ia[index++] < ia[index])
1:if (ia[0] < ia[0]) // execution if rhs is evaluated first
2:if (ia[0] < ia[1]) // execution if lhs is evaluated first
C++ 语言不能确保从左到右的计算次序。事实上,这类表达式的行为没有明确定义.
建议:如果要修改操作数的值,则不要在同一个语句的其他地方使用该操作数。如果必须使用改变的值,则把该表达式分割成两个独立语句:在一个语句中改变该操作数的值,再在下一个语句使用它。
16:动态分配的数组:
string *psa = new string[10]; // array of 10 empty strings
int *pia = new int[10]; // array of 10 uninitialized ints
这两个 new 表达式都分配了含有 10 个对象的数组。其中第一个数组是 string类型,分配了保存对象的内存空间后,将调用 string 类型的默认构造函数依次初始化数组中的每个元素。第二个数组则具有内置类型的元素,分配了存储 10个 int 对象的内存空间,但这些元素没有初始化。可使用跟在数组长度后面的一对空圆括号,对数组元素做值初始化int *pia2 = new int[10] (); // array of 10 uninitialized ints
圆括号要求编译器对数组做值初始化,在本例中即把数组元素都设置为0。
C++ 虽然不允许定义长度为 0 的数组变量,但明确指出,调用 new 动态创建长度为 0 的数组是合法的;
C++ 语言为指针提供 delete [] 表达式释放指针所指向的数组空间,如果遗漏了空方括号对,这是一个编译器无法发现的错误,将导致程序在运行时出错。
new 和 delete 表达式动态创建和释放数组,这两种表达式也可用于动态创建和释放单个对象,
定义变量时,必须指定其数据类型和名字。而动态创建对象时,只需指定其数据类型,而不必为该对象命名。取而代之的是,new 表达式返回指向新创建对象的指针,我们通过该指针来初始化和访问此对象:
int *pi = new int; // pi points to dynamically allocated,unnamed, uninitialized int
int *pi = new int(); // pi points to an int value-initialized to 0
int *pi = new int(1024); // object to which pi points is 1024
*pi=10;//initialized
string *ps = new string(10, '9'); // *ps is "9999999999"
正如我们(几乎)总是要初始化定义为变量的对象一样,在动态创建对象时,(几乎)总是对它做初始化也是一个好办法。
动态创建的对象用完后,程序员必须显式地将该对象占用的内存返回给自由存储区。C++ 提供了 delete 表达式释放指针所指向的地址空间。delete pi;C++ 保证:删除 0 值的指针是安全的
delete p; // delete object
delete [] p; // delete array
删除指针后,该指针变成悬垂指针。悬垂指针指向曾经存放对象的内存,但该对象已经不再存在了。悬垂指针往往导致程序错误,而且很难检测出来。一旦删除了指针所指向的对象,立即将指针置为 0,这样就非常清楚地表明指针不再指向任何对象。
C++ 允许动态创建 const 对象:
// allocate and initialize a const object
const int *pci = new const int(1024);
动态创建的 const 对象必须在创建时初始化,并且一经初始化,其值就不能再修改。
对于类类型的 const 动态对象,如果该类提供了默认的构造函数,则此对象可隐式初始化:
// allocate default initialized const empty string
const string *pcs = new const string;
new 表达式没有显式初始化 pcs 所指向的对象,而是隐式地将 pcs 所指向的对象初始化为空的 string 对象。内置类型对象或未提供默认构造函数的类类型对象必须显式初始化。
补充:复合表达式(即含有多个操作符的表达式)涉及到优先级、结合性以及操作数的求值次序。每一个操作符都有自己的优先级别和结合性。优先级规定复合表达式中操作符结合的方式,而结合性则决定同一个优先级的操作符如何结合。大多数操作符没有规定其操作数的求值顺序:由编译器自由选择先计算左操作数还是右操作数。通常,操作数的求值顺序不会影响表达式的结果。但是,如果操作符的两个操作数都与同一个对象相关,而且其中一个操作数改变了该对象的值,则程序将会因此而产生严重的错误——而且这类错误很难发现。
17:将 enum 对象或枚举成员提升为什么类型由机器定义,并且依赖于枚举成员的最大值。无论其最大值是什么, enum 对象或枚举成员至少提升为 int 型。如果 int 型无法表示枚举成员的最大值,则提升到能表示所有枚举成员值的、大于 int 型的最小类型( unsigned int、long 或 unsigned long)。
当使用非 const 对象初始化 const 对象的引用时,系统将非 const 对象转换为 const 对象;
while (cin >> s)隐式使用了 IO 标准库定义的类型转换,将 istream 类型转换为bool 类型,意味着要检验流的状态。如果最后一次读 cin 的尝试是成功的,则流的状态将导致上述类型转换为 bool 类型后获得 true 值——while 循环条件成立。如果最后一次尝试失败,比如说已经读到文件尾了,此时将 istream 类型转换为 bool 类型后得 false,while 循环条件不成立
18:强制类型转化:命名的强制类型转换(C++引入),旧式强制类型转换(C:用圆括号将类型括起来实现)
命名的强制类型转换:static_cast、dynamic_cast、const_cast 和reinterpret_cast,形式:cast-name<type>(expression);
标准 C++ 为了加强类型转换的可视性,引入命名的强制转换操作符,为程序员在必须使用强制转换时提供了更好的工具。例如,非指针的 static_cast 和const_cast 要比 reinterpret_cast 更安全。结果使程序员(以及读者和操纵程序的工具)可清楚地辨别代码中每个显式的强制转换潜在的风险级别。虽然标准 C++ 仍然支持旧式强制转换符号,但是我们建议,只有在 C 语言或标准 C++ 之前的编译器上编写代码时,才使用这种语法。支持旧式强制转换符号是为了对“在标准 C++ 之前编写的程序”保持向后兼容性,并保持与 C 语言的兼容性。
强制类型转换关闭或挂起了正常的类型检查。强烈建议程序员避免使用强制类型转换,不依赖强制类型转换也能写出很好的 C++程序。每次使用强制转换前,程序员应该仔细考虑是否还有其他不同的方法可以达到同一目的。如果非强制转换不可,则应限制强制转换值的作用域,并且记录所有假定涉及的类型,这样能减少错误发生的机会。
19:case 标号必须是整型常量表达式,switch可以是非常复杂的表达式
存在一个普遍的误解:以为程序只会执行匹配的 case 标号相关联的语句。实际上,程序从该点开始执行,并跨越 case 边界继续执行其他语句,直到 switch 结束或遇到 break 语句为止。
switch (ch) {
case 'a':
++aCnt; // oops: should have a break statement
case 'e':
++eCnt; // oops: should have a break statement
case 'i':
++iCnt; // oops: should have a break statement
case 'o':
++oCnt; // oops: should have a break statement
case 'u':
++uCnt; // oops: should have a break statement
}
为了搞清楚该程序导致了什么结果,假设 ch 的值是 'i' 来跟踪这个版本的代码。程序从 case 'i' 后面的语句开始执行,iCnt 的值加 1。但是,程序的执行并没有在这里停止,而是越过 case 标号继续执行,同时将 oCnt 和 uCnt 的值都加了 1.如果 ch 是 'e' 的话,那么 eCnt、iCnt、oCnt 以及 uCnt 的值都会加 1。
尽管没有严格要求在 switch 结构的最后一个标号之后指定 break 语句,但是,为了安全起见,最好在每个标号后面提供一个 break 语句,即使是最后一个标号也一样。如果以后在 switch 结构的末尾又需要添加一个新的 case 标号,则不用再在前面加 break 语句了。故意省略 case 后面的 break 语句是很罕见的,(有一些特殊用途)因此应该提供一些注释说明其逻辑。
20:省略 condition,则等效于循环条件永远为 true:
for (int i = 0; /* no condition */ ; ++i)
相当于程序写为:
for (int i = 0; true ; ++i)
这么一来,循环体内就必须包含一个 break 或者 return 语句。否则,循环会一直执行直到耗尽系统的资源为止。
21:continue 语句导致最近的循环语句的当次迭代提前结束。对于 while 和do while 语句,继续求解循环条件。而对于 for 循环,程序流程接着求解 for语句头中的 expression 表达式
22:指针数组:int *matrix[10]; // array of 10 pointers
数组指针:int (*matrix)[10]; // pointer to an array of 10 ints
引用数组:int &arr[10] // arr is an array of references
数组引用:int (&arr)[10] // arr is a reference to an array of 10 ints
23:任何处理数组的程序都要确保程序停留在数组的边界内,有三种常见的编程技巧确保函数的操作不超出数组实参 的边界。第一种方法是在数组本身放置一个标记来检测数组的结束。C 风格字符串就是采用这种方法的一个例子,它是一种字符数组,并且以空字符 null 作为结束的标记。处理 C风格字符串的程序就是使用这个标记停止数组元素的处理。第二种方法(使用标准库规范)是传递指向数组第一个和最后一个元素的下一个位置的指针,第三种方法是将第二个形参定义为表示数组的大小,这种用法在 C 程序和标准化之前的 C++ 程序中十分普遍。
24:确保返回引用安全的一个好方法是:请自问,这个引用指向哪个在此之前存在的对象?
25:默认实参是通过给形参表中的形参提供明确的初始值来指定的。程序员可为一个或多个形参定义默认值。但是,如果有一个形参具有默认实参,那么,它后面所有的形参都必须有默认实参。函数调用的实参按位置解析,默认实参只能用来替换函数调用缺少的尾部实参。设计带有默认实参的函数,其中部分工作就是排列形参,使最少使用默认实参的形参排在最前,最可能使用默认实参的形参排在最后。默认实参可以是任何适当类型的表达式。
26:inline函数避免函数调用(保存寄存器、栈指针、返回地址、、、)的开销,将函数指定为 inline 函数,(通常)就是将它在程序中每个调用点上“内联地”展开。内联机制适用于优化小的、只有几行的而且经常被调用的函数。大多数的编译器都不支持递归函数的内联。内联函数应该在头文件中定义,这一点不同于其他函数。inline 函数的定义对编译器而言必须是可见的,以便编译器能够在调用点内联展开该函数的代码。此时,仅有函数原型是不够的。
27:const 对象、指向 const 对象的指针或引用只能用于调用其const 成员函数,如果尝试用它们来调用非 const 成员函数,则是错误的。成员函数声明的形参表后面+ const 是(const)常量成员函数:const 改变了隐含的 this 形参的类型。
bool Sales_item::same_isbn(const Sales_item &rhs) const
{ return isbn == rhs.isbn; }
《==》
bool Sales_item::same_isbn(const Sales_item *this,const Sales_item &rhs) const
{ return this->isbn == rhs.isbn; }
函数必须在类中声明,但是可以在类中或类外定义,在类的定义外面定义成员函数必须指明它们是类的成员,函数名:Sales_item::same_isbn,使用作用域操作符指明函数same_isbn是在类 Sales_item 的作用域范围内定义的。
28:用 typedef 简化函数指针的定义,函数指针类型相当地冗长。使用 typedef 为指针类型定义同义词,可将函
数指针的使用大大简化:typedef bool (*cmpFcn)(const string &, const string &);该定义表示 cmpFcn 是一种指向函数的指针类型的名字。该指针类型为“指向返回 bool 类型并带有两个 const string 引用形参的函数的指针”。在要使用这种函数指针类型时,只需直接使用 cmpFcn 即可,不必每次都把整个类型声明全部写出来。
bool lengthCompare(const string &, const string &)
直接引用函数名等效于在函数名上应用取地址操作符;
cmpFcn pf1 = lengthCompare;《===》cmpFcn pf2 = &lengthCompare;
函数指针只能通过同类型的函数或函数指针或 0 值常量表达式进行初始化或赋值。
29:循环条件使用了逗号操作符,逗号操作符的求解过程:首先计算它的每一个操作数,然后返回最右边操作数作为整个操作的结果。
while (cin >> ival, !cin.eof()){...}
循环条件只读入 cin 而忽略了其结果。该条件的结果是 !cin.eof() 的值。如果 cin 到达文件结束符,条件则为假,退出循环。如果 cin 没有到达文件结束符,则不管在读取时是否发生了其他可能遇到的错误,都进入循环。
30:C++ 使用标准库类处理输入和输出:
• iostream 类处理面向流的输入和输出。
• fstream 类处理已命名文件的 IO。
• stringstream 类处理内存中字符串的 IO。
所有的这些类都是通过继承相互关联的。输入类继承了 istream,而输出类则继承了 ostream。因此,可在 istream 对象上执行的操作同样适用于ifstream 或 istringstream 对象。而继承 ostream 的输出类也是类似的。所有 IO 对象都有一组条件状态,用来指示是否可以通过该对象进行 IO 操作。如果出现了错误(例如遇到文件结束符)对象的状态将标志无法再进行输入,直到修正了错误为止。标准库提供了一组函数设置和检查这些状态。
31:将一个容器复制给另一个容器时,容器类型和元素类型都必须相同。eg:
vector<int> ivec;
vector<int> ivec2(ivec); // ok: ivec is vector<int>
list<int> ilist(ivec); // error: ivec is not list<int>
初始化为一段元素的副本.使用迭代器时,不要求容器类型相同。容器内的元素类型也可以不相同,只要它们相互兼容,能够将要复制的元素转换为所构建的新容器的元素类型,即可实现复制。采用这种初始化形式可复制不能
直接复制的容器。更重要的是,可以实现复制其他容器的一个子序列。
vector<int> ivec;
...
list<string> slist(svec.begin(), svec.end());
指针就是迭代器,因此允许通过使用内置数组中的一对指针初始化容器:
char *words[] = {"stately", "plump", "buck", "mulligan"};
// calculate how many elements in words
size_t words_size = sizeof(words)/sizeof(char *);
// use entire array to initialize words2
list<string> words2(words, words + words_size);
创建顺序容器时,可显式指定容器大小和一个(可选的)元素初始化式。容器大小可以是常量或非常量表达式,采用这种类型的初始化,元素类型必须是内置或复合类型,或者是提供了默认构造函数的类类型。如果元素类型没有默认构造函数,则必须显式指定其元素初始化式。,接受容器大小做形参的构造函数只适用于顺序容器,而关联容器不支持这种初始化
32:C++ 语言中,大多数类型都可用作容器的元素类型。容器元素类型必须满足以下两个约束:
• 元素类型必须支持赋值运算。
• 元素类型的对象必须可以复制。
此外,关联容器的键类型还需满足其他的约束
除了引用类型外,所有内置或复合类型都可用做元素类型。引用不支持一般意义的赋值运算,因此没有元素是引用类型的容器。除输入输出(IO)标准库类型之外,所有其他标准库类型都是有效的容器元素类型。特别地,容器本身也满足上述要求,因此,可以定义元素本身就是容器类型的容器,IO 库类型不支持复制或赋值。因此,不能创建存放 IO 类型对象的容器。
注意,在指定容器元素为容器类型时,必须用空格隔开两个相邻的 > 符号,以示这是两个分开的符号,否则,系统会认为 >> 是单个符号,为右移操作符,并导致编译时错误
vector< vector<string> > lines; // ok: space required between close>
vector< vector<string>> lines; // error: >> treated as shiftoperator
33:比较两个迭代器是否相等(或不等)。当两个迭代器指向同一个容器中的同一个元素,或者当它们都指向同一个容器的超出末端的下一位置时,两个迭代器相等
关系操作符只适用于 vector 和 deque 容器,这是因为只有这种两种容器为其元素提供快速、随机的访问。它们确保可根据元素位置直接有效地访问指定的容器元素。这两种容器都支持通过元素位置实现的随机访问,因此它们的迭代器可以有效地实现算术和关系运算。
最后三种类型使程序员无须直接知道容器元素的真正类型,就能使用它。需要使用元素类型时,只要用 value_type 即可。如果要引用该类型,则通过 reference 和 const_reference 类型实现。
上述每个操作都有两个不同版本:一个是 const 成员,另一个是非 const 成员。这些操作返回什么类型取决于容器是否为 const。如果容器不是 const,则这些操作返回 iterator 或 reverse_iterator 类型。如果容器是 const,则其返回类型要加上 const_ 前缀,也就是 const_iterator 和const_reverse_iterator 类型。
34:所有顺序容器都支持push_back 操作,提供在容器尾部插入一个元素的功能,list 和 deque 容器类型还提供了类似的操作:push_front,这个操作实现在容器首部插入新元素的功能。容器中添加元素时,系统是将元素值复制到容器里。insert 操作则提供了一组更通用的插入方法,实现在容器的任意指定位置插入新元素。任何 insert 或 push 操作都可能导致迭代器失效。当编写循环将元素插入到 vector 或 deque 容器中时,程序必须确保迭代器在每次循环后都得到更新。
35:两个容器的比较,比较的容器必须具有相同的容器类型,而且其元素类型也必须相同,
• 如果两个容器具有相同的长度而且所有元素都相等,那么这两个容器就相等;否则,它们就不相等。
• 如果两个容器的长度不相同,但较短的容器中所有元素都等于较长容器中对应的元素,则称较短的容器小于另一个容器。
• 如果两个容器都不是对文的初始子序列,则它们的比较结果取决于所比较的第一个不相等的元素。
36:容器大小的操作
访问容器元素
if (!ilist.empty())
{
// val and val2 refer to the same element
list<int>::reference val = *ilist.begin();
list<int>::reference val2 = ilist.front();
// last and last2 refer to the same element
list<int>::reference last = *--ilist.end();
list<int>::reference last2 = ilist.back();
}
删除顺序容器内元素的操作
顺序容器的赋值操作
37:为了使 vector 容器实现快速的内存分配,其实际分配的容量要比当前所需:的空间多一些。vector 容器预留了这些额外的存储区,用于存放新添加的元素。于是,不必为每个新元素重新分配容器。所分配的额外内存容量的确切数目因库的实现不同而不同。比起每添加一个新元素就必须重新分配一次容器,这个分配策略带来显著的效率。事实上,其性能非常好,因此在实际应用中,比起 list 和deque 容器,vector 的增长效率通常会更高。
size 指容器当前拥有的元素个数;而 capacity 则指容器在必须分配新存储空间之前可以存储的元素总数。事实上,只要有剩余的容量,vector 就不必为其元素重新分配存储空间。每当 vector 容器不得不分配新的存储空间时,以加倍当前容量的分配策略实现重新分配
38:选择容器类型的法则:
1. 如果程序要求随机访问元素,则应使用 vector 或 deque 容器。
2. 如果程序必须在容器的中间位置插入或删除元素,则应采用 list 容器。
3. 如果程序不是在容器的中间位置,而是在容器首部或尾部插入或删除元素,则应采用 deque 容器。
4. 如果只需在读取输入时在容器的中间位置插入元素,然后需要随机访问元素,则可考虑在输入时将元素读入到一个 list 容器,接着对此容器重新排序,使其适合顺序访问,然后将排序后的 list 容器复制到一个 vector容器。
如果程序既需要随机访问又必须在容器的中间位置插入或删除元素,那应该怎么办呢?
此时,选择何种容器取决于下面两种操作付出的相对代价:随机访问 list 容器元素的代价,以及在 vector 或 deque 容器中插入/删除元素时复制元素的代价。通常来说,应用中占优势的操作(程序中更多使用的是访问操作还是插入/删除操作)将决定应该什么类型的容器。
39:除了顺序容器,标准库还提供了三种顺序容器适配器:queue、priority_queue 和 stack。适配器(adaptor)是标准库中通用的概念,使某种标准库类型、函数或迭代器的行为类似于另外一种标准库类型、函数或迭代器。包括容器适配器、迭代器适配器和函数适配器。本质上,适配器是使一事物的行为类似于另一事物的行为的一种机制。容器适配器让一种已存在的容器类型采用另一种不同的抽象类型的工作方式实现。例如,stack(栈)适配器可使任何一种顺序容器以栈的方式工作stack 适配器所关联的基础容器可以是任意一种顺序容器类型。因此,stack 栈可以建立在vector、list 或者 deque 容器之上。而 queue 适配器要求其关联的基础容器必须提供 push_front 运算,因此只能建立在 list 容器上,而不能建立在vector 容器上。priority_queue 适配器要求提供随机访问功能,因此可建立在vector 或 deque 容器上,但不能建立在 list 容器上。