《C++ primer》笔记
——————2012/6
2.2字符串字面值的连接、多行字面值
两个相邻的仅由空格、制表符或换行符分开的字符串字面值(或宽字符串字
面值),可连接成一个新字符串字面值。这使得多行书写长字符串字面值变得简
单:
// concatenated long string literal
std::cout << "a multi-line "
"string literal "
"using concatenation"
<< std::endl;
执行这条语句将会输出:
a multi-line string literal using concatenation
和下面等价(反斜杠的力量):
std:\
:cou\
t << "a multi-line \
string literal \
using a backslash"
<< std::endl;
注意反斜线符号必须是该行的尾字符——不允许有注释或空格符。同样,后
继行行首的任何空格和制表符都是字符串字面值的一部分。正因如此,长字符串
字面值的后继行才不会有正常的缩进。
如果连接字符串字面值和宽字符串字面值,将会出现什么结果呢?例如:
// Concatenating plain and wide character strings is undefined
std::cout << "multi-line " L"literal " << std::endl;
其结果是未定义的,也就是说,连接不同类型的行为标准没有定义。这个程
序可能会执行,也可能会崩溃或者产生没有用的值,而且在不同的编译器下程序
的动作可能不同。
2.3.5声明和定义
C++ 是一种静态类型语言:变量和函数在使用前必须先声明。变量可以声明多次但是只能定义一次。定义变量时就进行初始化几乎总是个好主意。
只有当声明也是定义时,声明才可以有初始化式,因为只有定义才分配存储空间。初始化式必须要有存储空间来进行初始化。如果声明有初始化式,那么它可被当作是定义,即使声明标记为 extern:
extern double pi = 3.1416; // definition
虽然使用了 extern ,但是这条语句还是定义了 pi,分配并初始化了存储空间。只有当 extern 声明位于函数外部时,才可以含有初始化式。
int a; // definition因为没有使用extern,所以该语句是定义。
在 C++ 中理解“初始化不是赋值”是必要的。初始化指创建变量并给它赋初始值,而赋值则是擦除对象的当前值并用新值代替。
2.4 const限定符
必须在定义时初始化。与其他变量不同,除非特别说明,在全局作用域声明的 const 变量是定义该对象的文件的局部变量。此变量只存在于那个文件中,不能被其他文件访问。
在定义的时候加入extern就可以:extern const int bufSize = 10;
非 const 变量默认为 extern。要使 const 变量能够在其他的文件中访问,必须地指定它为 extern。
使用 const 的函数称为常量成员函数。由于 this 是指向 const 对象的指针,const 成员函数不能修改调用该函数的对象。
2.5 引用
(1)非 const 引用只能绑定到与该引用同类型的对象。(当引用初始化后,只要该引用存在,它就保持绑定到初始化时指向的对象。不可能将引用绑定到另一个对象。)引用只是对象的另一名字。
(2)const 引用则可以绑定到不同但相关的类型的对象或绑定到右值。(“const 引用”的意思是“指向 const 对象的引用”。)
1.
const int ival = 1024;
const int &ref1 = ival;
int &ref2 = ival; // error: non const reference to a const object
2.
int i = 42;
//legal for const references only
const int &r = 42;
const int &r2 = r + i;
int &refVal3 = 10; // error: initializer must be an object
假如我们编写
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 引用是只读的。
2.7 枚举类型
枚举类型在定义时若不采用系统默认初始值(第一个为0,以后逐渐加1),初始化表达式必须是一个常量表达式,即是在编译时就能确定值。枚举成员是常量,之后不能改变枚举成员的值。 之后定义枚举对象时,不能用一般的常量做右值,只能将相应的枚举成员作为右值。
如:enum Points { point2d = 2, point2w, point3d = 3, point3w };
Points pt3d = point3d; // ok: point3d is a Points enumerator
Points pt2w = 3; // error: pt2w initialized with int
pt2w = pt3d; // ok: both are objects of Points enum type
2.9 头文件
因为头文件包含在多个源文件中,所以不应该含有变量或函数的定义。对于头文件不应该含有定义这一规则,有三个例外。头文件可以定义类、值在编译时就已知道的 const 对象(const默认为非extern,即是局部于该文件的常量)和 inline 函数。这些实体可在多个源文件中定义,只要每个源文件中的定义是相同的。
(定义在头文件中的const常量,当该 const 常量是用常量表达式初始化时,可以保证所有的常量都有相同的值。但是在实践中,大部分的编译器在编译时都会用相应的常量表达式替换这些 const 变量的任何使用。所以,在实践中不会有任何存储空间用于存储用常量表达式初始化的 const 变量。如果 const 变量不是用常量表达式初始化,那么它就不应该在头文件中定义。一般来说,常量表达式是编译器在编译时就能够计算出结果的表达式。)
Chapter 3.库类型向量和串 & Chapter 4.内置数组和c字符串
vector 对应 数组
string 对应 c字符串 //<string> <cstring>
iterator 对应 访问数组的指针 //指针用于指向单个对象,而迭代器只能用于访问容器内的元素。
应该尽量使用库类型,它提供很很多内置操作(+,-,[],还有各种行为函数),而且不容易出错,方便检查。如果对性能要求很高,且内置类型的性能明显优于库函数时方才使用。但是一般情况库类型的性能都比内置类型的高。
定义如下变量:
vector<int> ivec (10); //代表有10个int型成员;//多个构造函数
int iArray[10]; //有10个int型成员
库类型的2种访问方式:
for(vector<int>::iterator iter=ivec.begin(); iter!=ivec.end(); ++iter)
{ //end()的返回结果是最后一个元素的后一个位置,当该数组长为0时,begin()等于end();
*iter = 0;
}
for (vector<int>::size_type ix = 0; ix != ivec.size(); ++ix)
{
ivec[ix] = 0; //或 用指针访问
} //若采用默认构造函数(即定义时:vector<int> ivec;),则为0个元素(长度为0),此时要添加成员应用库函数ivec.push_back(0)添加一个值为0的元素(可变长,很方便,而内置数组不能变长)
********应该尽量使用迭代器解引用来访问数组元素,因为有的类模版不支持下标访问。***
内置数组的访问:
for(size_t i=0; i != 10; ++i) // size_t 被定义为 unsigned int;机器相关。
iArry[i] = 0;
char chaArry1[] = {‘a’,’b’,’c’}; //该数组的长度为 3 字节。不属于c风格字符串。结束位置未知,知道遇到一个空字符为止。
char chaArry2[] = “abc”; //该数组的长度为 4 字节。
char ca1[] = {'C', '+', '+'};
char ca2[] = {'C', '+', '+', '\0'};
char ca3[] = "C++";
const char *cp = "C++";
char *cp1 = ca1;
char *cp2 = ca2;
除了ca1和cp1其他都是c风格字符串。
String对象的c_str()函数返回c-style字符串。(常用)
只有支持复制的元素类型可以存储在 vector 或其他容器类型里。
5. static_cast、dynamic_cast、const_cast 和reinterpret_cast
6.6 switch语句
case 标号必须是整型常量表达式, 如果两个 case 标号具有相同的值,同样也会导致编译时的错误。
对于 switch 结构,只能在它的最后一个 case 标号或 default 标号后面定义变量, 制定这个规则是为避免出现代码跳过变量的定义和初始化的情况。如果需要为某个特殊的 case 定义变量,则可以引入块语句,在该块语句中定义变量,从而保证这个变量在使用前被定义和初始化。
Break只能在循环和开关中使用。
goto语句
goto语句提供了函数内部的无条件跳转,实现从 goto 语句跳转到同一函数内某个带标号的语句。
goto 语句不能跨越变量的定义语句向下跳转,如果确实需要在 goto 和其跳转对应的标号之间定义变量,则定义必须放在一个块语句中
向上跳过已经执行的变量定义语句则是合法的。为什么?向下跳过未执行的变量定义语句,意味着变量可能在没有定义的情况下使用。向上跳回到一个变量定义之前,则会使系统撤销这个变量,然后再重新创建它。
Chapter 7. Functions
函数传参时尽量避免使用指针,以引用代替。
若函数不修改参数,应用(const引用),它比非const引用更灵活,因为非const引用不能指向const实参、字面值表达式、非左值表达式等,而这些实参均可传递给const引用。
数组作为参数传递时一定要注意数组元素的边界(3种方式):1. 在数组本身放置一个标记来检测数组的结束。C 风格字符串就是采用这种方法的一个例子。2. 传递指向数组第一个和最后一个元素的下一个位置的指针。这种编程风格由标准库所使用的技术启发而得。3. 第二个形参定义为表示数组的大小,这种用法在 C 程序和标准化之前的 C++ 程序中十分普遍。
通过引用传递数组:(数组的引用。这样不用传递数组的大小了,限制实参形参数组大小保持一致)
void printValues(int (&arr)[10]) { /* ... */ }
int main()
{
int i = 0, j[2] = {0, 1};
int k[10] = {0,1,2,3,4,5,6,7,8,9};
printValues(&i); // error: argument is not an array of 10 ints
printValues(j); // error: argument is not an array of 10 ints
printValues(k); // ok: argument is an array of 10 ints
return 0;
}
函数返回值千万不能是指向局部对象的引用或指针。
返回引用可以作为左值。如:将函数调用作为赋值表达式的左值、还有连续输入输出流等等。
编译器隐式地将在类内定义的成员函数当作内联函数。(inline 说明对于编译器来说只是一个建议,编译器可以选择忽略这个。内联机制适用于优化小的、只有几行的而且经常被调用的函数。大多数的编译器都不支持递归函数的内联。一个 1200 行的函数也不太可能在调用点内联展开。)
bool same_isbn(const Sales_item &rhs) const {…} 使用 const 的函数称为常量成员函数。 this 是指向 const 对象的const指针,const 成员函数不能修改调用该函数的对象。但是用mutable申明的变量可改变,即使它是const对象的成员时,也可以被改变。
在局部声明一个函数,将会像局部变量一样屏蔽外部的函数,即使是重载函数也会屏蔽,编译器将无法识别外部的重载函数。局部地声明函数是一种不明智的选择。函数的声明应放在头文件中。
Chapter 8. 输入输出库
cin/cout/cerr wcin/wcout/wcerr
各个条件状态:failbit/badbit等等判断流对象是否正确(详见msdn)。一个流错误之后,调用close()关闭并不能清理错误状态,若要重新打新的流,应在打开前先调用clear()使它和刚创建时一样。
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
标准库将 cout 与 cin 绑在一起,当输入流与输出流绑在一起时,任何读输入流的尝试都将首先刷新其输出流关联的缓冲区。(cin.tie(&cout); 断开cin.tie(0); 如果一个流调用 tie 函数将其本身绑在传递给 tie 的 ostream 实参对象上,则该流上的任何 IO 操作都会刷新实参所关联的缓冲区。)
I/O对象不允许赋值和拷贝。
iostream fstream stringstream
getline(io,Dest)
chapter 9.容器
容器内元素最低要求必须支持赋值和复制。所以引用和I/O对象不能作为容器的元素。
顺序容器:<list> <vector> <deque> 后两者的迭代器额外提供算数、关系运算。List是链式存储结构。
容器适配器:<stack> <queue> <priority_queue>
stack<string, vector<string> > str_stk2(svec);
迭代器适配器:<iterator>
关联容器:<map> <set>
// count number of times each word occurs in the input
map<string, int> word_count; // empty map from string to int
string word;
while (cin >> word)
++word_count[word];// ++word_count[“str”];不存在的索引自动添加,并初始化映射的值为0; multimap中则不允许下标访问
在输入中出现多少次,值最后索引映射的值就为多少。
关联容器 map 和 set 的元素是按顺序存储的。而 multimap 和 multset 也一样。因此,在 multimap 和 multiset 容器中,如果某个键对应多个实例,则这些实例在容器中将
不可能从 ostream_iterator 对象读入,也不可能写到 istream_iterator 对象中。一旦给 ostream_iterator 对象赋了一个值,写入就提交了。赋值后,没有办法再改变这个值。流迭代器不能创建反向迭代器。
map、set 和 list 类型提供双向迭代器,而 string、vector 和 deque 容器上定义的迭代器都是随机访问迭代器都是随机访问迭代器,用作访问内置数组元素的指针也是随机访问迭代器。istream_iterator 是输入迭代器,而 ostream_iterator 则是输出迭代器。
对于list容器使用泛型算法将会有很大的开销,所以应该尽量使用自己类定义的函数。
Input iterator(输入迭代器) |
Read, but not write; increment only 读,不能写;只支持自增运算 |
Output iterator(输出迭代器) |
Write, but not read; increment only 写,不能读;只支持自增运算 |
Forward iterator(前向迭代器) |
Read and write; increment only 读和写;只支持自增运算 |
Bidirectional iterator(双向迭代器) |
Read and write; increment and decrement 读和写;支持自增和自减运算 |
Random access iterator(随机访问迭代器) |
Read and write; full iterator arithmetic 读和写;支持完整的迭代器算术运算 |
alg (beg, end, other parms);
alg (beg, end, dest, other parms);
alg (beg, end, beg2, other parms);
alg (beg, end, beg2, end2, other parms);
alg_if alg_copy
chapter 12. class
定义一个类,它的数据成员初始化是在初始化列表处初始化,构造函数中只是赋值。执行构造函数体时,初始化已完成。所以对于没有默认构造函数的类类型的成员,以及 const 或引用类型的成员,都必须在构造函数初始化列表中进行初始化。
按照与成员声明一致的次序编写构造函数初始化列表是个好主意。此外,尽可能避免使用成员来初始化其他成员。如下:
Class test
{
Private: int a; int b;
Public: test(int i):b(i),a(b) {}
};
按照定义顺序初始化则先初始化a,此时b未知,则a被未知的b初始化了,编译器只会提示警告消息。
explicit 关键字只能用于类内部的构造函数声明上,在类的定义体外部所做的定义上不再重复它(explicit , friend , static , mutable)。当构造函数被声明 explicit 时,编译器将不使用它作为转换操作符。即传递给单形参构造函数的实参必须是显示类型构造为该类类型。:
当没有explicit 关键字声明的单形参构造函数的形参是该类类型,而传来的实参是其他类型,则可能会将实参隐式转换为类类型(依靠具有该实参类型的其他构造函数转换)。
static const型的数据能直接在类定义中赋初值:
class test
{
Private: static const int a = 30; // ok
Public: /*…*/
};
const int a; // 该语句可有可无,若为非const类型则必须有。
类中申明static成员变量时,要像外部定义函数一样定义该static成员。、、、、、、、、、、、、、、、、、
static 数据成员必须在类定义体的外部定义(正好一次)。不像普通数据成员,static 成员不是通过类构造函数进行初始化,而是应该在定义时进行初始化。保证对象正好定义一次的最好办法,就是将 static 数据成员的定义放在包含类非内联成员函数定义的文件中。
复制构造函数、赋值操作符和析构函数总称为复制控制。编译器自动实现这些操作,但类也可以定义自己的版本。有一种特别常见的情况需要类定义自己的复制控制成员的:类具有指针成员。
当用于类类型对象时,初始化的复制形式和直接形式有所不同:直接初始化直接调用与实参匹配的构造函数,复制初始化总是调用复制构造函数。复制初始化首先使用指定构造函数创建一个临时对象,然后用复制构造函数将那个临时对象复制到正在创建的对象:string null_book = "9-999-99999-9"; // copy-initialization。创建 null_book 时,编译器首先调用接受一个 C 风格字符串形参的 string 构造函数,创建一个临时对象,然后,编译器使用 string 复制构造函数将 null_book 初始化为那个临时对象的副本。
函数传参和返回值,若不是指针或引用均是对象的一份拷贝,会调用复制构造函数。复制构造函数应该使用构造函数初始化列表初始化新创建对象的成员。
容器初始化时也采用复制构造函数。Vector<string> ivec(10,”aaa”);//先创建一个临时string对象,然后对每个ivec成员使用复制构造函数。
如果我们没有定义复制构造函数,编译器就会为我们自动定义一个合成构造函数:执行逐个成员初始化(采用初始化列表),将新对象初始化为原对象的副本。只包含类类型成员或内置类型(可以是数组,但不是指针类型)成员的类,无须显式地定义复制构造函数,也可以复制。
有些类需要完全禁止复制。例如,iostream 类就不允许复制。如果想要连友元和成员函数中的复制也禁止,就可以声明一个(private)复制构造函数但不对其定义。
与复制构造函数一样,如果类没有定义自己的赋值操作符,则编译器会合成一个。
有些操作符(包括赋值操作符:= , [] , () , -> )必须是定义自己的类的成员。因为赋值必须是类的成员,所以 this 绑定到指向左操作数的指针。
内置类型,复制操作符返回右操作数的引用。那么类类型应该返回该类的引用(必须为*this)。
析构函数与复制构造函数或赋值操作符之间的一个重要区别是,即使我们编写了自己的析构函数,合成析构函数仍然运行。 若需要析构函数,则一定需要拷贝(复制)构造函数,赋值操作符。
重载操作符必须具有至少一个类类型或枚举类型的操作数。这条规则强制重载操作符不能重新定义用于内置类型对象的操作符的含义。操作符的优先级、结合性或操作数目不能改变。当操作符为成员函数,this 指向左操作数。
通过连接其他合法符号可以创建新的操作符。例如,定义一个 operator** 以提供求幂运算是合法的。
(不再具备短路求值特性)在 && 和 || 的重载版本中,两个操作数都要进行求值,而且对操作数的求值顺序不做规定。因此,重载 &&、|| 或逗号操作符不是一种好的做法。
赋值(=)、下标([])、调用(())和成员访问箭头(->)等操作符必须定义为成员,将这些操作符定义为非成员函数将在编译时标记为错误。
改变对象状态或与给定类型紧密联系的其他一些操作符,如自增、自减和解引用,通常就定义为类成员。
对称的操作符,如算术操作符、相等操作符、关系操作符和位操作符,最好定义为普通非成员函数。
输出操作符的重载应该尽量少做格式化,不应该输出换行符。(这些格式由用户控制)
更重要但通常重视不够的是,输入和输出操作符有如下区别:输入操作符必须处理错误和文件结束的可能性:(1,类型匹配。2,输入未完成就结束了)
istream& operator>>(istream& in, Sales_item& s)
{
double price;
in >> s.isbn >> s.units_sold >> price;
// check that the inputs succeeded
if (in)
s.revenue = s.units_sold * price;
else
s = Sales_item(); // input failed: reset object to default state
return in;
}
设计输入操作符时,如果可能,要确定错误恢复措施,这很重要。
前自增和后自增的区别:后自增有一个被忽略的int参数,前自增没有。
若要重载,则最好2个都应该有。
定义了函数调用操作符的类,其对象常称为函数对象,即它们是行为类似函数的对象。
struct absInt {
int operator() (int val) {
return val < 0 ? -val : val;
}
};
int i = -42;
absInt absObj; // object that defines function call operator
unsigned int ui = absObj(i); // calls absInt::operator(int)
尽管 absObj 是一个对象而不是函数,我们仍然可以“调用”该对象,效果是运行由 absObj 对象定义的重载调用操作符。
转换操作符
从类类型转换:在不同类类型间算数运算或函数传参赋值时,编译器自动调用已定义了的转换操作符(如同内置类型算术运算时的隐式类型转换)。转换函数必须是成员函数,不能指定返回类型,并且形参表必须为空。
classname::operator int() const {return val;} //int类型转换符
虽然转换函数不能指定返回类型,但是每个转换函数必须显式返回一个指定类型的值。例如,operator int 返回一个 int 值。
类类型转换之后不能再跟另一个类类型转换。如果需要多个类类型转换,则代码将出错。如:有2个类:Integral(定义了SmallInt类型转换)和SmallInt(定义了int类型转换),那么程序中在需要SmallInt的地方可以用Integral,而在需要int的地方不能用Integral,这将用到两次类类型转换。
类类型转换加标准转换:先用类类型转换将其转换为内置类型,让内置类型自动转换为另一个内置类型。如:一个类只定义了该类到int的转换,而需要比较 类对象和double的大小,则编译器会调用int到double的隐式转换。
到类类型转换:主要依靠构造函数。 若Integral类有Integral(long)构造函数,则有从long到Integral类的隐式转换。
避免二义性最好的方法是,保证最多只有一种途径将一个类型转换为另一类型。做到这点,最好的办法是限制转换操作符的数目,尤其是,到一种内置类型应该只有一个转换。
重载确定:
1. 确定候选函数集合:这些是与被调用函数同名的函数。
2. 选择可行的函数:这些是形参数目和类型与函数调用中的实参相匹配的候选函数。选择可行函数时,如果有转换操作,编译器还要确定需要哪个转换操作来匹配每个形参。
3. 选择最佳匹配的函数。为了确定最佳匹配,对将实参转换为对应形参所需的类型转换进行分类。对于类类型的实参和形参,可能的转换的集合包括类类型转换。
如果重载集中的两个函数可以用同一转换函数匹配,则使用在转换之后或之前的标准转换序列的等级来确定哪个函数具有最佳匹配。否则,如果可以使用不同转换操作,则认为这两个转换是一样好的匹配,不管可能需要或不需要的标准转换的等级如何。
正确设计类的重载操作符、转换构造函数和转换函数需要多加小心。尤其是,如果类既定义了转换操作符又定义了重载操作符,容易产生二义性。
OOP
要触发动态绑定,满足两个条件:第一,只有指定为虚函数的成员函数才能进行动态绑定。第二,必须通过基类类型的引用或指针进行函数调用。
只有通过引用或指针调用,虚函数才在运行时确定。只有在这些情况下,直到运行时才知道对象的动态类型。
基类类型的引用或指针可以引用基类类型对象,也可以引用派生类型对象。无论实际对象具有哪种类型,编译器都将它当作基类类型对象。将派生类对象当作基类对象是安全的,因为每个派生类对象都拥有基类子对象。而且,派生类继承基类的操作,即,任何可以在基类对象上执行的操作也可以通过派生类对象使用。
若要覆盖虚函数机制并强制函数调用使用虚函数的特定版本(例如派生类调用基类的虚函数),可以使用特定类的作用域操作符。 Pbase -> subclass::virFunction();
虚函数的默认实参:通过基类的引用或指针调用虚函数时,默认实参为在基类虚函数声明中指定的值,如果通过派生类的指针或引用调用虚函数,则默认实参是在派生类的版本中声明的值。在同一虚函数的基类版本和派生类版本中使用不同的默认实参几乎一定会引起麻烦。如果通过基类的引用或指针调用虚函数,但实际执行的是派生类中定义的版本,这时就可能会出现问题。在这种情况下,为虚函数的基类版本定义的默认实参将传给派生类定义的版本,而派生类版本是用不同的默认实参定义的。
若基类虚函数返回该类的对象、指针或引用,子类可以返回子类和基类的类型。
继承设计思考:是一种 or有一个。
使用 class 保留字定义的派生默认具有 private 继承,而用 struct 保留字定义的类默认具有 public 继承。
若过在继承中想让基类的一些成员为private继承,一些为public继承,可使用using实现。
友元关系不能继承。构造函数和复制控制成员不能继承
如果基类定义 static 成员,则整个继承层次中只有一个这样的成员。
可以有基类到派生类的指针和引用,还有派生类对基类赋值。用派生类对基类直接赋值和初始化,将调用基类的赋值操作符和拷贝构造函数。而他们的形参均是基类引用。 如果类是使用 private 或 protected 继承派生的,则用户代码不能将派生类型对象转换为基类对象。如果是 private 继承,则从 private 继承类派生的类不能转换为基类。如果是 protected 继承,则后续派生类的成员可以转换为基类类型。
如果派生类显式定义自己的复制构造函数或赋值操作符,则该定义将完全覆盖默认定义。被继承类的复制构造函数和赋值操作符负责对基类成分以及类自己的成员进行复制或赋值。(派生类在自己的赋值控制中显示调用基类的赋值控制初始化基类部分,)
如果在构造函数或析构函数中调用虚函数,则运行的是为构造函数或析构函数自身类型定义的版本。
处理继承层次中的对象时,指针的静态类型可能与被删除对象的动态类型不同(静态为CBase,动态为CSub。CBase *p=new CSub ),可能会删除实际指向派生类对象的基类类型指针。如果删除基类指针,则需要运行基类析构函数并清除基类的成员,如果对象实际是派生类型的,则没有定义该行为。要保证运行适当的析构函数,基类中的析构函数必须为虚函数。
在基类和派生类中使用同一名字的成员函数,其行为与数据成员一样:在派生类作用域中派生类成员将屏蔽基类成员。即使函数原型不同,基类成员也会被屏蔽。要访问基类被屏蔽的函数可用作用域访问符,和using声明。
回忆一下,局部作用域中声明的函数不会重载全局作用域中定义的函数(第 7.8.1 节),同样,派生类中定义的函数也不重载基类中定义的成员。通过派生类对象调用函数时,实参必须与派生类中定义的版本相匹配,只有在派生类根本没有定义该函数时,才考虑基类函数。
理解 C++ 中继承层次的关键在于理解如何确定函数调用。确定函数调用遵循以下四个步骤:
1.首先确定进行函数调用的对象、引用或指针的静态类型。
2.在该类中查找函数,如果找不到,就在直接基类中查找,如此循着类的继承链往上找,直到找到该函数或者查找完最后一个类。如果不能在类或其相关基类中找到该名字,则调用是错误的。
3.一旦找到了该名字,就进行常规类型检查,查看如果给定找到的定义,该函数调用是否合法。
4.假定函数调用合法,编译器就生成代码。如果函数是虚函数且通过引用或指针调用,则编译器生成代码以确定根据对象的动态类型运行哪个函数版本,否则,编译器生成代码直接调用函数。
定义句柄,让用户减少使用指针和引用的负担。
Chapter 16 模版
模版在编译时要实例化,编译一般为分别编译和包含编译,他们均用头文件和源文件方式。在模版实例化时要访问源文件,如果多个源文件包含了模版的头文件则会产生多个实力,为提高性能应该在模版的实现中加入export关键词表明只到处一次。
类模版的static成员:同一类型实例化的类共享同一个static成员,不同类型实例化的static成员不同。因为一个类模版类型的实例化即新定义一个类。
当定义非模板函数的时候,对实参应用常规转换;当特化模板的时候,对实参类型不应用转换。在模板特化版本的调用中,实参类型必须与特化版本函数的形参类型完全匹配,如果不完全匹配,编译器将为实参从模板定义实例化一个实例。
普通函数和函数模板都完全匹配。像通常一样,当匹配同样好时,非模板版本优先。设计既包含函数模板又包含非模板函数的重载函数集合是困难的,因为可能会使函数的用户感到奇怪,定义函数模板特化(第 16.6 节)几乎总是比使用非模板版本更好。
Chapter 17
异常(6.13 17.1)
执行 throw 的时候,不会执行跟在 throw 后面的语句,而是将控制从 throw 转移到匹配的 catch,该 catch 可以是同一函数中局部的 catch,也可以在直接或间接调用发生异常的函数的另一个函数中。
异常以类似于将实参传递给函数的方式抛出和捕获。异常可以是可传给非引用形参的任意类型的对象,这意味着必须能够复制该类型的对象。
抛出异常对象时,不能抛出在catch时已经被销毁的对象(指向局部变量的指针或引用)。
抛出的异常是指针则是一个坏的决定。(若要使用继承层次的动态绑定一般使用引用)
与函数调用相反的方向展开栈寻早匹配的catch。当 catch 结束的时候,在紧接在与该 try 块相关的最后一个 catch 子句之后的点继续执行。(抛出异常之前的局部对象将会被销毁,而动态分配的内存应该有程序员处理。类类型会调用自己的析构函数)
在查找匹配的 catch 期间,找到的 catch 不必是与异常最匹配的那个 catch,相反,将选中第一个找到的可以处理该异常的 catch。因此,在 catch 子句列表中,最特殊的 catch 必须最先出现。(转型只允许:1. 从非const对象到const的引用。2. 从派生类到基类。3. 从数组或函数到指针)
带有因继承而相关的类型的多个 catch 子句,必须从最低派生类类到最高派生类型排序。(以便派生类型的处理代码出现在其基类类型的 catch 之前)
重新抛出异常:当自己没处理完是,可使用throw;(不带参数)语句将原先抛出的异常重新抛出。
即使函数不能处理被抛出的异常,它也可能想要在随抛出异常退出之前执行一些动作。除了为每个可能的异常提供特定 catch 子句之外,因为不可能知道可能被抛出的所有异常,所以可以使用捕获所有异常catch 子句的。形式:catch(…)
在进入构造函数函数体之前处理构造函数初始化式,构造函数函数体内部的 catch 子句不能处理在处理构造函数初始化时可能发生的异常。应该使用函数测试块:
test(int ii, char ch) try : i(new int(ii)), c(ch)
{
}
catch(const std::bad_alloc &e){ } //捕获动态分配异常
catch 子句既可以处理从成员初始化列表中抛出的异常,也可以处理从构造函数函数体中抛出的异常。
异常说明跟在函数形参表之后。一个异常说明在关键字 throw 之后跟着一个(可能为空的)由圆括号括住的异常类型列表:
void recoup(int) throw(runtime_error); 如果 recoup 抛出一个异常,该异常将是 runtime_error 对象,或者是由 runtime_error 派生的类型的异常。
空说明列表指出函数不抛出任何异常:void no_problem() throw();
但是,不可能在编译时知道程序是否抛出异常以及会抛出哪些异常,只有在运行时才能检测是否违反函数异常说明。如果函数抛出了没有在其异常说明中列出的异常,就调用标准库函数 unexpected。默认情况下,unexpected 函数调用 terminate 函数,terminate 函数一般会终止程序。
基类中虚函数的异常说明,可以与派生类中对应虚函数的异常说明不同。但是,派生类虚函数的异常说明必须与对应基类虚函数的异常说明同样严格,或者比后者更受限。
名字空间(17.2)
namespace spaceName{ /****************/ }
名字空间的接口声明和定义可以分离,即采用头文件源文件方式。
声明和实现都要有namespace spaceName{/*实现或声明*/}
全局命名空间是隐藏的,所以可以这样访问 ::member_name
未命名的命名空间与其他命名空间不同,未命名的命名空间的定义局部于特定文件,从不跨越多个文本文件。
未命名的命名空间中定义的名字只在包含该命名空间的文件中可见。如果另一文件包含一个未命名的命名空间,两个命名空间不相关。两个命名空间可以定义相同的名字,而这些定义将引用不同的实体。
未命名的名字空间如果定义在文件的全局区,则不能和全局变量同名。
C++ 不赞成文件静态声明。不造成的特征是在未来版本中可能不支持的特征。应该避免文件静态而使用未命名空间代替。
相对于依赖于 using 指示,对程序中使用的每个命名空间名字使用 using 声明更好。
18.1 allocator模版 与 new、delete操作符
allocator分配指定类型的预留控件,但是不做初始化,等到使用是再初始化,vector即采用这种策略,所以提高性能。
New操作符的执行。首先,该表达式调用名为 operator new 的标准库函数,分配足够大的原始的未类型化的内存,以保存指定类型的一个对象;接下来,运行该类型的一个构造函数,用指定初始化式构造对象;最后,返回指向新分配并构造的对象的指针。
delete删除动态分配对象的时候,发生两个步骤。首先,对指针指向的对象运行适当的析构函数;然后,通过调用名为 operator delete 的标准库函数释放该对象所用内存。
18.2 dynamic_cast动态类型转换 typeid 动态类型识别,主要用于有虚函数的类
联合、 位域、 volatile