C++

Table of Contents

1 C++简介

C++ was developed by Bjarne Stroustrup at Bell Labs since 1979, as an extension of the C language as he wanted an efficient and flexible language similar to C, which also provided high-level features for program organization.
In 1983, it was renamed from C with Classes to C++.

参考:
最权威的书籍:The C++ Programming Language, by Bjarne Stroustrup
比较好的入门书籍:C++ Primer, by Stanley B. Lippman (本文主要摘自该书)
Scott Meyer的Effective系列书籍:《Effective C++》,《More Effective C++》,《Effective STL》,《Effective Modern C++, 2014》
Herb Sutter的Exceptional系列书籍:《Exceptional C++》,《More Exceptional C++》,《Exceptional C++ Style》,其在线版本可参考Guru of the Week: http://www.gotw.ca/gotw/
Inside the C++ Object Model, by Stanley B. Lippman, 1996.

在线参考资料:
Tutorials to help you master C++: http://www.learncpp.com/
Standard C++ Library reference: http://www.cplusplus.com/reference/
C++ reference: http://en.cppreference.com/w/

1.1 C++ Standardization

C++ is standardized by an ISO working group known as JTC1/SC22/WG21.

Table 1: C++ Standard
Year C++ Standard Informal name Note
1998 ISO/IEC 14882:1998 C++98 first standardization
2003 ISO/IEC 14882:2003 C++03 bug fixes only
2011 ISO/IEC 14882:2011 C++11 many new features
2014 ISO/IEC 14882:2014 C++14 minor release
2017 to be determined C++17  

参考:
Last publicly available Committee Draft of "ISO/IEC IS 14882 – Programming Languages – C++": http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2011/n3242.pdf
First draft after the C++11 standard, contains the C++11 standard plus minor editorial changes: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2012/n3337.pdf

1.1.1 C++标准中预定义的宏

下面的总结基于C++11。

预定义的宏:

__cplusplus
__DATE__
__FILE__
__LINE__
__STDC_HOSTED__
__TIME__

还有一些由实现决定:

__STDC__
__STDC_MB_MIGHT_NEQ_WC__
__STDC_VERSION__
__STDC_ISO_10646__
__STDCPP_STRICT_POINTER_SAFETY__
__STDCPP_THREADS__

参考:ISO&IEC 14882 C++11, 16.8 Predefined macro names

C++11从C99中引入了函数名相关的变量

__func__

参考:ISO&IEC 14882 C++11, 8.4.1 In general

1.2 C++11 and C++14

1.2.1 C++11 新特性

这里介绍部分的C++11新特性。

参考:
C++11 - the new ISO C++ standard: http://www.stroustrup.com/C++11FAQ.html
C++ Primer Plus第6版,第18章 探讨C++新标准

1.2.1.1 auto自动推导类型

在C++11之前,auto关键字用来指定变量的存储期。C++11 用 auto 关键字来指示“自动推导类型”。
如:

auto x = 7;     // same as: int x = 7;

auto可以使代码看起来更简单。如,在C++11中可以这样写:

template<class T> void printall(const vector<T>& v)
{
	for (auto p = v.begin(); p!=v.end(); ++p)
		cout << *p << "\n";
}

而在C++11前,你需要烦琐地写为:

template<class T> void printall(const vector<T>& v)
{
	for (typename vector<T>::const_iterator p = v.begin(); p!=v.end(); ++p)
		cout << *p << "\n";
}

参考:http://www.stroustrup.com/C++11FAQ.html#auto

1.2.1.2 Range-for statement

C++11中增强了for语句。直接看实例,前面介绍的例子可以进一步简化为:

template<class T> void printall(const vector<T>& v)
{
	for (auto x: v)
        cout << x << "\n";
}

参考:http://www.stroustrup.com/C++11FAQ.html#for

1.2.1.3 nullptr – a null pointer literal

nullptr is a literal denoting the null pointer; it is not an integer:

char* p = nullptr;
int* q = nullptr;
char* p2 = 0;		// 0 still works and p==p2

void f(int);
void f(char*);

f(0);         		// call f(int)
f(nullptr);   		// call f(char*)

void g(int);
g(nullptr);		// error: nullptr is not an int
int i = nullptr;	// error nullptr is not an int

参考:http://www.stroustrup.com/C++11FAQ.html#nullptr

1.2.1.4 lambda(匿名函数对象)

lambda可以创建匿名函数对象,能简化代码。

vector<int> v = {50, -10, 20, -30};

std::sort(v.begin(), v.end());	// the default sort
// now v should be { -30, -10, 20, 50 }

// sort by absolute value:
std::sort(v.begin(), v.end(), [](int a, int b) { return abs(a)<abs(b); });
// now v should be { -10, 20, -30, 50 }

参考:http://www.stroustrup.com/C++11FAQ.html#lambda

1.2.1.5 增加了std::thread, std::future, std::promise等并行编程设施

1.2.2 C++14 新特性

C++11中的新特性很多,但C++14是一个改动很小的版本。

参考:http://www.infoq.com/cn/news/2014/09/cpp14-here-features

1.3 C++ ABI

2 变量和基本类型

C++支持C中的各种基本类型,新增了引用类型。此外,可通过类来自定义数据类型。

2.1 引用类型(对象别名)

引用就是对象的另一个名字。在实际程序中,引用主要用作函数的形式参数。

可以通过在变量名前添加“&”符号来定义引用。引用必须用与该引用同类型的对象初始化。
当引用初始化后,只要该引用存在,它就保持绑定到初始化时指定的对象。不可能将引用绑定到另一个对象。

int ival = 1024;
int &refVal = ival;  // ok
int &refVal2;        // error: a reference must be initialized
int &refVal3 = 10;   // error: initializer must be an object

2.1.1 通过引用传递数组时,数组的大小是参数的一部分

和其他类型一样,数组形参可声明为数组的引用。如果形参是数组的引用,编译器不会将数组实参转化为指针,而是传递数组的引用本身。在这种情况下,数组大小成为形参与实参类型的一部分,编译器检查数组实参的大小与形参的大小是否匹配。

void printValues(int (&arr)[10]) { /*......*/ }   // &arr两边的圆括号是必需的,因为下标操作符具有更高的优先级

int main() {
    int k[10] = {0,1,2,3,4,5,6,7,8,9};
    printValues(k);    //ok: argument is an array of 10 ints

    int i=0, j[2]={0, 1};
    printValues( &i ); //error: argument is not an array of 10 ints
    printValues( j );  //error: argument is not an array of 10 ints
    return 0;
}

参考:《C++ Primer 第四版》7.2节

2.2 变量的初始化方法(复制初始化和直接初始化)

变量在定义的同时,可以为对象提供初始值。C++支持两种初始化变量的形式:复制初始化(copy-initialization)和直接初始化(direct-initialization)。
复制初始化语法用等号(=),直接初始化则把初始化式放在括号中。如下面两种用法变量ival都被初始化为1024。

int ival = 1024;  // copy-initialization
int ival(1024);   // direct-initialization

对于内置基本类型,复制初始化和直接初始化是一样的。
当用于类类型对象时,直接初始化直接调用与实参匹配的构造函数;复制初始化首先使用指定构造函数创建一个临时对象,然后用复制构造函数将那个临时对象复制到正在创建的对象中。

2.3 变量的初始化规则

当定义没有初始化式的变量时,系统按下面规则帮我们初始化变量:
规则一:内置类型变量是否自动初始化取决于变量定义的位置。在函数体外定义的变量都初始化为它们的默认值,在函数体里定义的内置类型变量不进行自动初始化。
规则二:类类型变量在没有提供初始化式时,不管在哪里定义,总是使用默认构造函数进行初始化。

注1:数组同样遵循上面描述的规则,即如果数组元素是内置类型,则在函数体处定义时会初始化为它们的默认值,在函数体内定义时不会初始化;如果数组元素是类类型,则不管在哪里定义默认构造函数都会被使用。

注2:初始化时的默认值是什么呢?
基本类型的默认值都是0。
参考:Section 6.7.8 Initialization of C99 standard (n1256)

  • if it has pointer type, it is initialized to a null pointer;
  • if it has arithmetic type, it is initialized to (positive or unsigned) zero;
  • if it is an aggregate, every member is initialized (recursively) according to these rules;
  • if it is a union, the first named member is initialized (recursively) according to these rules.

2.3.1 函数体内的内置类型变量不会自动初始化

上面规则一中提到:函数体内的内置类型变量不会自动初始化。
为什么这样设计呢?这是基于效率的考虑。

char buf[100*1024];  //函数体内这样的大buffer如果自动初始化,会影响性能。

2.4 强制类型转换(cast)

C++中有下面几种形式的强制类型转换:

static_cast <new_type> (expression)
const_cast <new_type> (expression)
reinterpret_cast <new_type> (expression)
dynamic_cast <new_type> (expression)

它们的应用场景如下
(1) static_cast的应用场景:Static cast is used to cast between the integer types. 'e.g.' char->long, int->short etc. Static cast is also used to cast pointers to related types, for example casting void* to the appropriate type.
(2) const_cast的应用场景:const_cast(expression) is used to add/remove const attribute of a variable.
(3) reinterpret_cast的应用场景:reinterpret_cast强制转换过程仅仅只是比特位的拷贝,使用时需要特别谨慎。比如,将指针或引用转换为一个足够长的整形、将整型转换为指针或引用类型。
(4) dynamic_cast的应用场景:Dynamic cast is used to convert pointers and references at run-time, generally for the purpose of casting a pointer or reference up or down an inheritance chain (inheritance hierarchy).

说明:前三种转换都在“编译时”完成,只有 dynamic_case 是在“运行时”处理的。

参考:http://www.cplusplus.com/doc/tutorial/typecasting/

2.4.1 Traditional Type-casting

C++还支持旧式强制转换,旧式强制转换符号有下面两种形式:

(new_type) expression      // C-language-style cast notation
new_type (expression)      // Function-style cast notation

3 函数

3.1 参数传递

C++中,函数的非引用类型的参数是“值传递”(会复制参数),引用类型的参数是“引用传递”(不会复制参数)。

3.1.1 非指针非引用的普通形参

对于非指针非引用的普通形参,可以向非const形参传递const或非const实参,也可以向const形参传递const或非const实参。

下面程序能正常编译(没有任何警告信息)和运行:

#include<stdio.h>

void fun1(int i) {
    printf("%d\n", i);
}

void fun2(const int i) {
    printf("%d\n", i);
}

int main()
{
    int a = 1;
    const int b = 2;
    fun1(a);
    fun1(b);
    fun2(a);
    fun2(b);
}

3.1.2 指针形参(尽量用const指针)

在C++中,可以将指向const对象的指针初始化为指向非const对象,但不可以让指向非const对象的指针指向const对象。
下面程序无法用C++编译器编译:

#include<stdio.h>

void fun1(int *ip) {
    printf("%d\n", *ip);
}

void fun2(const int *ip) {    /* 不能通过ip修改指针所指向的对象 */
    printf("%d\n", *ip);
}

int main()
{
    int a = 1;
    const int b = 2;
    int * ap = &a;
    const int * bp = &b;
    fun1(ap);
    fun1(bp);  // 用C++编译器会提示编译错误(invalid conversion from 'const int*' to 'int*'),用C编译器可成功编译(仅有警告)
    fun2(ap);
    fun2(bp);
}

说明:上面的行为和C是不兼容的,用C编译器可以成功编译上面程序,仅会提示警告信息。

总结:const指针形参(如上例中fun2所示)的使用更加方便,可以接受const或非const指针实参。

3.1.3 引用形参(尽量用const引用)

引用形参直接关联到其所绑定的对象,而并非这些对象的副本。这可以避免对参数的复制。
如,下面函数比较两个string的长短,由于string可能很长,我们可以用引用来避免参数的复制。

bool isShorter(const string &s1, const string &s2) {
  return s1.size() < s2.size();
}

非const引用形参与const引用形参在接受参数时有区别,下面程序无法用C++编译器正常编译:

#include<stdio.h>

void fun1(int &i) {
    printf("%d\n", i);
}

void fun2(const int &i) {     /* 由于引用参数由const修饰,故函数内不能修改i */
    printf("%d\n", i);
}

int main()
{
    int a = 1;
    const int b = 2;
    fun1(a);
    fun1(b);  //编译错误(invalid initialization of reference of type 'int&' from expression of type 'const int')
    fun2(a);
    fun2(b);
}

总结:const引用形参(如上例中fun2所示)的使用更加方便,可以接受const或非const对象;非const引用形参只能与完全同类型的非const对象关联

3.2 默认实参

默认实参是通过给形参提供明确的初始化表达式来指定的。
注:一个形参具有默认实参,则它后面的所有形参都必须有默认实参。

#include<stdio.h>

void fun1(int i=0, int j=1);         //声明时指定了默认实参

int main() {
    fun1();      //output: i=0, j=1
    fun1(4);     //output: i=4, j=1
    fun1(7,8);   //output: i=7, j=8
    return 0;
}

void fun1(int i, int j) {            //声明时指定了默认实参,定义时不能再指定!
    printf("i=%d, j=%d\n", i, j);
}

注:一般地,我们仅在声明时指定默认实参(这种情况下定义时不能再指定)。也可以在声明时不指定默认实参,而仅在定义时指定默认实参。但这样有个限制——只有在包含该函数定义的源文件中默认实参才有效,所以不推荐这么做。

#include<stdio.h>

void fun1(int i, int j);           //声明时不指定默认实参

void fun1(int i=0, int j=1) {      //定义时指定默认实参,只有在包含该函数定义的源文件中默认实参才有效
    printf("i=%d, j=%d\n", i, j);
}

int main() {
    fun1();      //output: i=0, j=1
    fun1(4);     //output: i=4, j=1
    fun1(7,8);   //output: i=7, j=8
    return 0;
}

3.2.1 默认实参可指定为函数

函数的默认实参可以是表达式或另一个函数,在调用函数时才求解该表达式或调用另一个函数。

下面演示一个不正确的用法:fun1有个默认实参为fun2,而fun2又调用fun1,形成了死循环,会导致程序异常终止。

#include<stdio.h>

int count;

int fun2();

void fun1(int i=0, int j=fun2()) {         //fun1()中参数j的默认实参为函数fun2(),在调用fun1()时会调用fun2()
    printf("i=%d, j=%d\n", i, j);
}

int fun2() {                               //fun2()中又调用了fun1(),这样形成了一个死循环!
    printf("call %d times\n", count++);
    fun1();
    return 1;
}

int main() {
    fun1();
    return 0;
}

3.3 函数重载 (Overloaded function)

出现在相同作用域中的两个函数,如果具有相同的名字而形参表不同,则称为重载函数。
函数不能仅仅基于不同的返回类型而实现重载,如果有两个函数的形参表相同而返回类型不同,编译器报“重定义”错误。

3.3.1 Overload Resolution

重载确定(Overload resolution)是将函数调用与重载函数集合中最匹配的一个函数进行关联的过程。

可以用下面3个步骤来确定重载函数:
第一步,找候选函数(Candidate functions),候选函数是与被调用函数同名,且在调用点上其声明可见的函数。
第二步,找可行函数(Viable functions),可行函数满足两个条件:第一,函数的形参个数与该调用的实参个数相同;第二,每一个实参的类型必须与对应形参的类型匹配,或者可被隐式转换为对应的形参类型。
第三步,找最佳的可行函数(Best viable function)。

一个形参时如何确定最佳的可行函数?
为了确定最佳的可行函数,编译器将实参类型到相应形参类型的转换划分等级。转换等级以降序排序如下:

  1. 精确匹配(Exact Match)。实参与形参类型相同。
  2. 通过类型提升(Promotion)实现的匹配。如比int小的整数会提升为int。
  3. 通过标准转换(Standard Conversion)实现的匹配。如浮点数到整数的转换。
  4. 通过类类型转换(Class-type Conversion)实现的匹配。

对转换等级的具体分类可参考下表(摘自:C++11 standard. 13.3.3 Best viable function)

 Conversion                       Rank        
 No conversions required        
 Lvalue-to-rvalue conversion    
 Array-to-pointer conversion    
 Function-to-pointer conversion 
 Qualification conversions      
 Exact Match 
             
             
             
             
 Integral promotions            
 Floating point promotion       
 Promotion   
             
 Integral conversions           
 Floating point conversions     
 Floating-integral conversions  
 Pointer conversions            
 Pointer to member conersions   
 Boolean conversions            
 Conversion  
             
             
             
             
             

多个形参时如何确定最佳的可行函数?
如果函数调用使用了两个或两个以上的实参,则编译器依次检查每一个实参来决定哪个函数的匹配是最佳的。
有且仅有一个函数满足下列条件时,则认为它就是最佳匹配:

  1. 其每个实参的匹配都不劣于其他可行函数需要的匹配;
  2. 至少有一个实参的匹配优于其他可行函数提供的匹配。

参考:
C++ Primer, 4th Edition. 7.8.3 The Three Steps in Overload Resolution
C++11 standard. 13.3 Overload resolution

3.3.1.1 重载确定实例1——优先使用精确匹配

假设有下面几个函数:

void f();
void f(int);
void f(int, int);
void f(double, double = 3.14);

调用下面函数时,会使用哪个重载函数呢?

f(5.6);      // call f(double, double)

第一步,确定候选函数。4个名为f的函数都为候选函数。
第二步,确定可行函数,f(int)和f(double, double)这两个函数为可行函数。
第三步,确定最佳的可行函数,f(double, double)属于精确匹配,所以它是最佳的可行函数。

3.3.1.2 重载确定实例2——多个参数的最佳匹配函数不一样

还是使用前面的例子,调用下面函数时,会使用哪个重载函数呢?

f(1, 2.56);      // 编译器报错,有二义性。

第一步,确定候选函数。4个名为f的函数都为候选函数。
第二步,确定可行函数,f(int, int)和f(double, double)这两个函数为可行函数。
第三步,确定最佳的可行函数。对于多个形参的情况,编译器依次检查每一个实参来决定哪个函数的匹配是最佳的。
先考虑第一个实参,f(int, int)属于精确匹配;再考虑第二个实参,f(double, double)属于精确匹配。不能决定哪个函数更好,编译器将产生“二义性”错误。

为了解决这个二义性,可以使用强制类型转换,如:

f(static_cast<double>(1), 2.56);   // call f(double, double)
f(1, static_cast<int>(2.56));      // call f(int, int)
3.3.1.3 重载确定实例3——不唯一的标准转换
void manip(long);
void manip(float);
mainip(3.14);       // 编译器报错,有二义性。

字面值常量3.14的类型为double,这种类型既可以转为long型又可转为float型,都是合法的标准转换,没有哪个标准转换比其它标准转换具有更高的优先级。所以调用具有二义性。

3.3.2 const形参和重载

形参是否为const,能否实现函数的重载?
这个要看情况,总结如下:

对于非指针非引用的普通形参,无法通过对参数是否使用const来进行函数重载。

void fun1(int i) { /* */ }
void fun1(const int i) { /* */ }     //C和C++编译器都会提示“重定义”错误!

对于指针形参,可以通过对参数是否使用const来进行函数重载。

void fun1(int *ip) { /* */ }
void fun1(const int *ip) { /* */ }   //C++中,这是合法的函数重载;但C中,会报“重定义”错误!

注意:不能基于指针本身是否为const来实现函数的重载。 如:

void fun1(int *ip) { /* */ }
void fun1(int *const ip) { /* */ }   //C++和C中,都会报“重定义”错误!

对于引用形参,可以通过对参数是否使用const来进行函数重载。

void fun1(int &i) { /* */ }
void fun1(const int &i) { /* */ }    //合法的函数重载。

4 类的基本概念

C++用类来定义自己的抽象数据类型。

4.1 类的定义

定义类以关键字class开始,其后是该类的名字标识符。类体位于花括号里面,花括号后面必须要跟一个分号。类的成员可以是数据、函数和类型别名(typedef定义)。
在实践中我们往往将类定义分成两个文件:一个头文件用来定义类的基本结构,包含类的数据成员和类成员函数的声明等等;另一个仅文件名后缀不同的.cpp文件用来定义类的成员函数。这是比较好的软件工程实践,分离了类的接口和实现,对类的用户来说无需关心类成员函数的具体实现。

//file Rectangle.h
#ifndef RECTANGLE_H
#define RECTANGLE_H

class Rectangle {
  int width, height;
public:
  Rectangle (int,int);
  Rectangle ();
  int area ();
};

#endif
//file Rectangle.cpp
#include "Rectangle.h"

Rectangle::Rectangle (int a, int b) {
  width = a;
  height = b;
}

Rectangle::Rectangle () {
  width = 0;
  height = 0;
}

int Rectangle::area () {
  return width * height;
}

定义了一个类类型后,可以按下面两种方式来使用:
一,将类的名字直接用作类型名;
二,指定关键字class或struct,后面跟着类的名字。

如:

Rectangle obj1;          //定义类型为Rectangle的对象obj1
class Rectangle obj1;    //同上。这种用法从C中继承而来。

4.1.1 类的访问标号 (public, protected和private)

Table 3: 类的访问标号
访问标号 可以从类外访问 可以从派生类访问
public Yes Yes
protected No Yes
private No No

4.2 构造函数

构造函数的工作是保证每个对象的数据成员具有合适的初始值。创建类类型的新对象时,会自动执行构造函数。
构造函数的名字与类的名字相同,它不能指定返回类型(void也不行)。构造函数可以被重载,实参决定了使用哪个构造函数。

4.2.1 默认构造函数(无参或所有参数都有默认实参)

A default constructor is a constructor which can be called with no arguments (either defined with an empty parameter list, or with default arguments provided for every parameter).

用下面的方法可以使用默认构造函数来定义一个对象:

Rectangle obj1;              // 注意:Rectangle obj1();是定义对象的错误写法,这种方式是声明一个函数!千万要小心!
Rectangle obj1=Rectangle();  // 效果同上。创建并初始化一个Rectangle对象,然后用它来按值初始化obj1(会调用复制构造函数,但往往会被编译器优化为不调用复制构造函数)。

使用默认构造函数来动态创建对象:

Rectangle *p_obj1 = new Rectangle();
Rectangle *p_obj1 = new Rectangle;       // 同上。
4.2.1.1 Synthesized Default Constructor

当且仅当一个类没有定义构造函数时,编译器会自动生成一个默认构造函数。 只要定义了构造函数,不管它是不是默认构造函数,编译器将不再自动生成默认构造函数。
编译器生成的这个函数称为合成的默认构造函数(synthesized default constructor)。
合成的默认构造函数按下面方式来初始化类的成员:
一、类类型的成员通过运行各自的默认构造函数来进行初始化;
二、内置类型成员不进行初始化。

#include <iostream>

using namespace std;

class Date
{
private:
  int m_nMonth;
  int m_nDay;
  int m_nYear;
public:
  void display () { cout<< "Year:" << m_nYear << " Month:" <<  m_nMonth << " Day:" << m_nDay <<endl; }
};

int main()
{
  Date cDate;        // Date中没有默认构造函数,编译器会生成一个synthesized default constructor
  cDate.display();   // 合成的默认构造函数中,内置类型成员不进行初始化。display的输出每次都不一样。
  return 0;
}

4.2.2 构造函数初始化列表

与一般的成员函数不同,构造函数可以包含一个构造函数初始化列表。
构造函数初始化列表以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个数据成员后面跟一个放在圆括号中的初始化式。
构造函数初始化列表仅指定初始化成员的值,并不指定这些初始化执行的次序。成员被初始化的次序就是定义成员的次序。
构造函数初始化列表在定义时指定,而不是在声明中指定。

在构造函数初始化列表中没有显式提及的每个成员,使用与初始化变量相同的规则来进行初始化:类类型调用默认构造函数,内置类型取决于它的作用域(类作用域中不会自动初始化)。

构造函数初始化列表实例:

class Rectangle {
  int width, height;
public:
  Rectangle (int,int);
};

Rectangle::Rectangle (int a, int b): width(a), height(b) { }

省略初始化列表,在构造函数体内对数据成员赋值也能达到相同的目标。如:

Rectangle::Rectangle (int a, int b) {
  width = a;
  height = b;
}

上面Rectangle构造函数的两个版本具有同样的效果。不同之处在于,使用构造函数初始化列表的版本 初始化 数据成员,没有定义初始化列表的构造函数版本在构造函数的函数体中对数据成员 赋值 。这是“初始化”和“赋值”的区别,这个区别的重要性取决于数据成员的类型。

4.2.2.1 必须用构造函数初始化列表的情况

三种成员的初始化必须在构造函数初始化列表中进行:
一:没有默认构造函数的类类型的成员;
二:const成员;
三:引用类型的成员。

实例1:没有默认构造函数的类类型的成员的初始化

class A {
  int value;
 public:
  A(int i) {value = i;}
};

class B {
  A obj1;                        //obj1为类类型成员,如果不在构造函数初始化列表中,会自动调用默认构造函数进行初始化。
                                 //但obj1所属的类没有默认构造函数,所以类成员obj1的初始化只能在构造函数初始化列表中进行!
public:
  B(int i) : obj1(i) {}          //对obj1的初始化只能在构造函数初始化列表中进行。
  //B(int i) { obj1=A(i); }      //这行会报错,因为对obj1的初始化只能在构造函数初始化列表中进行。
};

int main() {
  B obj2(10);
  return 0;
}

实例2:const成员和引用类型的成员的初始化

class ConstRef {
private:
  int i;
  const int ci;
  int &ri;
public:
  ConstRef (int ii);
};

ConstRef::ConstRef(int ii) {
  i = ii;     // ok
  ci = ii;    // error: const成员的初始化必须在构造函数初始化列表中
  ri = i;     // error: 引用成员的初始化必须在构造函数初始化列表中
}

正确的方法是在构造函数初始化列表中对const成员和引用类型的成员进行初始化,如:

ConstRef::ConstRef(int ii): i(ii), ci(i), ri(ii) {}

4.3 const成员函数

将关键字 const 加在形参表之后,可以将成员函数声明为常量。如:

double avg_price() const;

const成员函数不能改变其所操作的对象的数据成员。 const关键字必须同时出现在声明和定义中,若只出现在其中一处,会出现编译时错误。

4.3.1 基于成员函数是否为const的重载

基于成员函数是否为const,可以重载一个成员函数。

class A {
int fun1 ();
int fun1 () const;  /* 基于成员函数是否为const,可以重载函数。 */
};

在调用时,只有A类的const对象才能调用const版本的fun1函数;而类A的非const对象可以调用任意一种,但非const版本的fun1是一个更好的匹配。

4.4 友元

友元机制允许一个类将对其非公有成员的访问权授予指定的函数或类。友元的声明以关键字friend开始。
友元可以是普通的非成员函数,或前面定义的其他类的成员函数,或整个类。
将一个类设为友元,友元类的所有成员函数都可以访问授予友元关系的那个类的非公有成员。

参考:http://www.learncpp.com/cpp-tutorial/813-friend-functions-and-classes/

4.4.1 友元实例——普通的非成员函数作为友元

下面是一个普通的非成员函数作为友元的实例:

class Value
{
private:
  int m_nValue;
public:
  Value(int nValue) { m_nValue = nValue; }
  friend bool IsEqual(const Value &cValue1, const Value &cValue2);
};

bool IsEqual(const Value &cValue1, const Value &cValue2)
{
  return (cValue1.m_nValue == cValue2.m_nValue);  //如果没有友元声明,私有成员m_nValue不能被访问。
}

4.4.2 友元实现——类作为友元

下面是整个类作为友元的实例:

#include<iostream>

class Storage
{
private:
  int m_nValue;
  double m_dValue;
public:
  Storage(int nValue, double dValue)
  {
    m_nValue = nValue;
    m_dValue = dValue;
  }

  // Make the Display class a friend of Storage
  friend class Display;
};

class Display
{
private:
  bool m_bDisplayIntFirst;

public:
  Display(bool bDisplayIntFirst) { m_bDisplayIntFirst = bDisplayIntFirst; }

  void DisplayItem(Storage &cStorage)   //友元类的所有成员函数都可以访问授予友元关系的那个类的非公有成员
  {
    if (m_bDisplayIntFirst)
      std::cout << cStorage.m_nValue << " " << cStorage.m_dValue << std::endl;
    else // display double first
      std::cout << cStorage.m_dValue << " " << cStorage.m_nValue << std::endl;
  }
};

int main()
{
  Storage cStorage(5, 6.7);
  Display cDisplay(false);

  cDisplay.DisplayItem(cStorage);  //输出: 6.7 5

  return 0;
}

4.5 static类成员

对于特定类类型的全体对象而言,访问一个全局对象有时是必要的。但全局对象会破坏封装。
我们还有另外一个更好的选择——使用static数据成员。

4.5.1 static数据成员

static数据成员独立于该类的任意对象而存在:每个static数据成员与类关联,并不与该类的对象相关联。

一般地,类的static数据成员和普通的数据成员一样,不能在类的定义体中初始化。static数据成员通常在定义时才初始化。

// file foo1.h
class foo1
{
private:
    static int i;
};

// file foo1.cpp
int foo1::i = 10;     // static数据成员在定义时才初始化。static关键字只能出现在类定义体内部的声明中,定义时不能标示为static。

这个规则的一个例外是:只是初始化式是一个常量表达式,整形const static数据成员就可以在类的定义体中进行初始化:

// file foo2.h
class foo2
{
private:
    static const int i = 10;  //类的定义体中进行初始化要满足三个要求:1. static, 2. const, 3. int类型
};

// file foo2.cpp
const int foo2::i;            //整形const static数据成员在类内部提供了初始化式时,在定义时不必再指定初始值。
                              //整形const static数据成员在类内部提供了初始化式时,有些编译器可以省略它的定义。

4.5.2 static成员函数

类似的,static成员函数是类的组成部分,不是任何对象的组成部分。
static成员函数有下面性质:
static成员函数没有this指针(static不关联到某个对象,所以没有this指针);
static成员函数不能被声明为const(static不关联到某个对象,不存在能不能修改某个对象的情况);
static成员函数不能被声明为虚函数。

class Date {
    int d, m, y;
    static Date default_date;
public:
    Date(int dd =0, int mm =0, int yy =0);
    // ...
    static void set_default(int dd, int mm, int yy);  // set default_date to Date(dd,mm,yy)
};

5 类的复制控制

当定义一个新类型的时候,需要显式或隐式地指定复制、赋值和撤销该类型的对象时会发生什么——这是通过定义特殊成员:复制构造函数、赋值操作符和析构函数来达到的。
复制构造函数、赋值操作符和析构函数总称为“复制控制”。

5.1 复制构造函数

只有单个形参,而且该形参是对本类类型对象的引用(常用const修饰),这样的构造函数称为复制构造函数。

复制构造函数可用于:
• 根据另一个同类型的对象显式或隐式初始化一个对象;
• 复制一个对象,将它作为实参传给一个函数;
• 从函数返回时复制一个对象;
• 初始化顺序容器中的元素。

5.1.1 复制构造函数使用场景——根据对象初始化另一对象

回忆一下,C++支持两种初始化形式:直接初始化(将初始化式放入圆括号中)和复制初始化(使用等号)。
当用于类类型对象时,初始化的复制形式和直接形式有所不同:直接初始化直接调用与实参匹配的构造函数;复制初始化首先使用指定构造函数创建一个临时对象,然后用复制构造函数将那个临时对象复制到正在创建的对象。

string null_book = "9-999-99999-9";  // copy-initialization
string dots(10, '.');                // direct-initialization

string empty_copy = string();        // copy-initialization
string empty_direct;                 // direct-initialization

5.1.2 Copy elision(避免不必要复制的优化手段)

Copy elision is a compiler optimization technique that avoids unnecessary copying of objects.

实例1:

// file copy_elision.cpp
#include <iostream>
using namespace std;

class A {
public:
  A(const char* str = "\0") {      // default constructor
    cout << "Constructor called" << endl;
  }

  A(const A &a) {                  // copy constructor
    cout << "Copy constructor called" << endl;
  }
};

int main() {
  A obj1 = "test";   // 编译器会优化后效果类似于:A obj1("test");
  return 0;
}

理论上讲,构造obj1时,会先使用能接受"test"为参数的构造函数创建一个临时对象,再调用复制构造函数将临时对象复制到正在创建的对象中。
但测试时,运行的结果为:

$ g++ copy_elision.cpp
$ ./a.out
Constructor called

复制构造函数竟然没有被调用,这时因为现代编译器默认进行了优化,避免了不必要复制。在GCC中可以使用 -fno-elide-constructors 禁止相关优化。

$ g++ -fno-elide-constructors copy_elision.cpp
$ ./a.out
Constructor called
Copy constructor called

5.1.3 复制构造函数使用场景——非引用的形参与返回值

我们知道,当形参为非引用类型的时候,将复制实参的值。类似地,以非引用类型作返回值时,将返回return语句中的值的副本。

5.1.4 复制构造函数使用场景——初始化容器元素

复制构造函数可用于初始化顺序容器中的元素。
如:

// default string constructor and five string copy constructors invoked
vector<string> svec(5);

上例中,编译器首先使用string默认构造函数创建一个临时值来初始化svec,然后使用复制构造函数将临时值复制到svec的每个元素。

除非你想使用容器元素的默认初始值,更有效的办法是,分配一个空容器并将已知元素的值加入容器。如:

vector<string> svec;       // empty vector

svec.push_back(string1);   // append string1 into vector svec
svec.push_back(string2);   // append string2 into vector svec

5.1.5 Synthesized Copy Constructor

如果我们没有定义复制构造函数,编译器就会为我们自动生成一个,称为合成复制构造函数(Synthesized Copy Constructor)。
合成复制构造函数的行为是,执行逐个成员初始化,将新对象初始化为原对象的副本。所谓“逐个成员”,指的是编译器将现在对象的每个非static成员,依次复制到正创建的对象。对于内置类型成员合成复制构造函数直接复制其值,对于类类型成员使用该类的复制构造函数进行复制。对于数组成员,合成复制构造函数将复制数组的每一个元素。

假设有下面类:

class Sales_item {
private:
    std::string isbn;
    int units_sold;
    double revenue;
};

编译器自动生成的合成复制构造函数如下所示:

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

5.1.6 何时要定义自己的复制构造函数(Shallow vs. Deep Copy)

合成复制构造函数只完成必要的工作,对于简单的情况也能很好的工作。
但有些类必须对复制对象时发生的事情加以控制。这样的类经常有一个数据成员是指针(合成复制构造函数仅会复制指针的值,不会复制指针指向的内存空间,这会导致两个成员指针指向同一块内存,这样在分别delete释放时会出现问题),或者有成员表示在构造函数中分配的其他资源。而另一些类在创建新对象时必须做一些特定工作。这两种情况下,都必须定义复制构造函数。

合成复制构造函数所做的工作是浅拷贝,如果需要深拷贝则应该定义自己的复制构造函数。

参考:https://en.wikipedia.org/wiki/Object_copying#Shallow_copy

5.1.7 禁止复制

有些类需要完全禁止复制。例如,iostream类就不允许复制。

为了防止复制,类必须显式声明其复制构造函数为private。如果复制构造函数是私有的,将不允许用户代码复制该类类型的对象,编译器将拒绝任何进行复制的尝试。然而,类的友元和成员仍可以进行复制。如果想要连友元和成员中的复制也禁止,就可以声明一个private的复制构造函数但不对其定义。声明而不定义成员函数是合法的,使用未定义成员的任何尝试将导致链接失败。

通过声明(但不定义)一个private的复制构造函数,可以禁止任何复制类类型对象的尝试:用户代码中复制尝试将在编译时报错,而成员函数和友元中的复制尝试将在链接时报错。

5.2 赋值操作符

与类要控制初始化对象的方式一样,类也定义了该类型对象赋值时会发生什么:

Sales_item trans, accum;
trans = accum;

在介绍合成赋值操作符之前,需要简单了解一下重载操作符(overloaded operator)。

重载操作符是一些函数,其名字为operator后跟着所定义的操作符的符号。因此,通过定义名为operator=的函数,我们可以对赋值进行定义。像任何其他函数一样,操作符函数有一个返回值和一个形参表。形参表必须具有与该操作符数目相同的形参(如果操作符是一个类成员,则包括隐式this形参)。赋值是二元运算,所以该操作符函数有两个形参:第一个形参对应着左操作数,第二个形参对应右操作数。

大多数操作符可以定义为成员函数或非成员函数。当操作符为成员函数时,它的第一个操作数隐式绑定到this指针。有些操作符(包括赋值操作符)必须是定义自己的类的成员。因为赋值必须是类的成员,所以this绑定到指向左操作数的指针。因此,赋值操作符接受单个形参,且该形参是同一类类型的对象。右操作数一般作为const引用传递。

赋值操作符的返回类型应该与内置类型赋值运算返回的类型相同。内置类型的赋值运算返回对右操作数的引用,因此,赋值操作符也返回对同一类类型的引用。

类A的赋值操作符可声明为:

A::A& operator=(const A &);

5.2.1 Synthesized Assignment Operator

与复制构造函数类似,如果类没有定义自己的赋值操作符,则编译器会合成一个。
合成赋值操作符(Synthesized Assignment Operator)与合成复制构造函数的操作类似。它会执行逐个成员赋值:右操作数对象的每个成员赋值给左操作数对象的对应成员。除数组之外,每个成员用所属类型的常规方式进行赋值。对于数组,给每个数组元素赋值。

假设有下面类:

class Sales_item {
private:
    std::string isbn;
    int units_sold;
    double revenue;
};

编译器自动生成的合成赋值操作符如下所示:

// equivalent to the synthesized assignment operator
Sales_item&
Sales_item::operator=(const Sales_item &rhs) {
    isbn = rhs.isbn;             // calls string::operator=
    units_sold=rhs.units_sold;   // uses built-in int assignment
    revenue = rhs.revenue;       // uses built-in double assignment
    return *this;
}

5.3 析构函数

析构函数是个成员函数,它的名字是在类名字之前加上一个代字号(~),它没有返回值,没有形参。因为不能指定任何形参,所以不能重载析构函数。
一般用析构函数来释放在其构造函数中分配的资源。

5.3.1 何时析构函数自动被调用

撤销类对象时会自动调用析构函数。
对于动态分配的对象当对其指针使用delete时会被撤销;对于局部变量,当其超出作用域时会被撤销。

// p points to default constructed object
Sales_item *p = new Sales_item;
{                           // new scope
  Sales_item item(*p);      // copy constructor copies *p into item
  delete p;                 // destructor called on object pointed by p
}                           // exit local scope; destructor called on item

变量(如item)在超出作用域时应该自动撤销。因此,当遇到右花括号时,将运行item的析构函数。
动态分配的对象只有在指向该对象的指针被删除时才撤销。如果没有删除指向动态对象的指针,则不会运行该对象的析构函数,对象就一直存在。从而导致内存泄漏。

又如:

// pointer is destroyed because it goes out of scope,
// but not the object it pointed to. MEMORY LEAK!!!
if (1) {
  Foo *myfoo = new Foo("foo");
}

// pointer is destroyed because it goes out of scope,
// object it points to is deleted. no memory leak
if(1) {
  Foo *myfoo = new Foo("foo");
  delete myfoo;
}

// no memory leak, object goes out of scope
if(1) {
  Foo myfoo("foo");
}

参考:
http://stackoverflow.com/questions/10081429/when-is-a-c-destructor-called

5.3.1.1 容器中的元素总是按逆序撤销

撤销一个容器(不管是标准库容器还是内置数组)时,也会运行容器中的类类型元素的析构函数:

{
  Sales_item *p = new Sales_item[10];   // dynamically allocated
  vector<Sales_item> vec(p, p + 10);    // local object
  // ...
  delete [] p;                          // array is freed; destructor run on each element pointed by p
}                                       // vec goes out of scope; destructor run on each element in vec

容器中的元素总是按逆序撤销:首先撤销下标为 size() - 1 的元素,最后撤销下标为0的元素。

5.3.2 栈展开时是否撤销局部对象

栈展开时是否撤销局部对象(即调用析构函数)要分情况讨论。

5.3.2.1 Unwind Stack by longjmp (不会调用析构函数,避免在C++中使用longjmp)

前面说到撤销类对象时会自动调用析构函数,对于局部变量,当其超出作用域时会被撤销。
调用longjmp时,setjmp和longjmp之间的栈上的局部对象可以认为是超出作用域,它们会被撤销吗?

答案是不会(VC++会撤销,但不要依赖它)。测试如下:

#include <iostream>
#include <setjmp.h>

using namespace std;

class A
{
public:
    A() { cout << "A constructor called" << endl; }
    ~A() { cout << "A destructor called" << endl; }
};

class B
{
public:
    B() { cout << "B constructor called" << endl; }
    ~B() { cout << "B destructor called" << endl; }
};

jmp_buf env;

void func1();

int main() {
    int val;
    val = setjmp (env);
    if (val) {
        cerr << "Error " << val << " happened" << endl;
        return -1;
    }

    func1();
    return 0;
}

void func1() {
    A obja;
    B objb;
    longjmp (env, 101);
}

在Linux中运行上面程序,输出:

A constructor called
B constructor called
Error 101 happened

从上面结果中可知,局部对象obja和objb没有被撤销(A和B的析构函数并没有被调用),这会有潜在的资源泄露问题。

5.3.2.2 Unwind Stack by throw (会调用析构函数)

C++要求因异常而退出函数时,编译器保证撤销在异常发生前创建的局部对象。
测试如下:

#include <iostream>
using namespace std;

class A
{
public:
    A() { cout << "A constructor called" << endl; }
    ~A() { cout << "A destructor called" << endl; }
};

class B
{
public:
    B() { cout << "B constructor called" << endl; }
    ~B() { cout << "B destructor called" << endl; }
};

void func1();

int main() {
    int val;

    try {
        func1();
    } catch (int e) {
        cerr << "Error " << e << " happened" << endl;
    }

    return 0;
}

void func1() {
    A obja;
    B objb;

    throw 101;
}

运行上面程序,输出:

A constructor called
B constructor called
B destructor called
A destructor called
Error 101 happened

从上面结果中可知,局部对象obja和objb被撤销(A和B的析构函数被调用了)。

5.3.3 Synthesized Destructor (总会生成,总会运行)

与复制构造函数或赋值操作符不同,编译器总是会为我们合成一个析构函数。
合成析构函数(Synthesized Destructor)按对象创建时的逆序撤销每个非static成员,因此,它按成员在类中声明次序的逆序撤销成员。对于类类型的每个成员,合成析构函数调用该成员的析构函数来撤销对象。合成析构函数并不删除指针成员所指向的对象。

析构函数与复制构造函数或赋值操作符之间的一个重要区别是,即使我们编写了自己的析构函数,合成析构函数仍会运行(在执行完自己定义的析构函数后将运行合成析构函数)。

假如有下面的程序:

#include <iostream>

using namespace std;

class A
{
public:
    A() { cout << "A constructor called" << endl; }
    ~A() { cout << "A destructor called" << endl; }
};

class B
{
public:
    B() { cout << "B constructor called" << endl; }
    ~B() { cout << "B destructor called" << endl; }
};

class Test
{
    A obj1;
    B obj2;
public:
    Test() { cout << "Test constructor called" << endl; }
    ~Test() { cout << "Test destructor called" << endl; }
};

int main()
{
    Test foo;
    return 0;
} // foo goes out of scope here

运行上面程序会输出:

A constructor called
B constructor called
Test constructor called
Test destructor called
B destructor called
A destructor called

说明:
在Test的构造函数中并没有显式调用A和B的构造函数,为什么A constructor called和B constructor called会被输出?
这是因为在构造函数初始化列表中没有显式提及的每个成员,将使用与初始化变量相同的规则来进行初始化。

在Test的析构函数中并没有显式调用A和B的析构函数,为什么A destructor called和B destructor called会被输出?
这是因为合成析构函数会在执行完自己定义的析构函数后运行。

5.3.4 何时要定义析构函数

分配了资源的类一般需要定义析构函数以释放那些资源。

5.4 三法则

如果类需要析构函数,则它往往也需要复制构造函数和赋值操作符,这是一个有用的经验法则。这个规则常称为三法则(rule of three)。

6 重载操作符与转换

C++允许我们重定义操作符用于类类型对象时的含义。明智地使用操作符重载可以使类类型的使用像内置类型一样直观和方便。

重载操作符是具有特殊名称的函数:保留字operator后接需定义的操作符号。像任意其他函数一样,重载操作符具有返回类型和形参表。

6.1 至少一个参数为类类型或枚举类型

重载操作符必须具有至少一个类类型或枚举类型的操作数。
这条规则强制重载操作符不能重新定义用于内置类型对象的操作符的含义。

如,下面的重载操作符是非法的:

int operator+(int, int);    /* 会报错。重载操作符需要至少一个类类型或枚举类型的操作 */

6.2 可重载和不可重载的操作符

除下面6个操作符不能被重载外,其他的操作符都可以被重载。

.       (member selection)
.*      (member selection with pointer-to-member)
::      (scope resolution)
?:      (conditional)
sizeof  (object size information)
typeid  (object type information)

通过连接其他合法符号可以创建新的操作符。例如,定义一个 operator** 以提供求幂运算是合法的。

参考:
Why can't I overload dot, ::, sizeof, etc.? http://www.stroustrup.com/bs_faq2.html#overload-dot

6.3 重载操作符为类成员与非类成员函数的经验原则

为类设计重载操作符的时候,必须选择是将操作符设置为类成员还是普通非成员函数。在某些情况下,程序员没有选择,操作符必须是成员;在另一些情况下,有些经验原则可指导我们做出决定。

操作符定义为类成员函数时,其形参看起来比操作数数目少1。因为作为成员函数的操作符有一个隐含的this形参,且限定为第一个操作数。
操作符定义为非成员函数时,通常必须将它们设置为所操作类的友元。因为在这种情况下,操作符通常需要访问类的私有部分。

下面是一些指导原则,有助于决定将操作符设置为类成员还是普通非成员函数:
• 赋值(=)、下标([])、调用(())和成员访问箭头(->)等操作符必须定义为成员,将这些操作符定义为非成员函数将在编译时标记为错误。
• 像赋值一样,复合赋值操作符通常应定义为类的成员,与赋值不同的是,不一定非得这样做,如果定义非成员复合赋值操作符,不会出现编译错误。
• 改变对象状态或与给定类型紧密联系的其他一些操作符,如自增、自减和解引用,通常就定义为类成员。
对称的操作符,如算术操作符、相等操作符、关系操作符和位操作符,最好定义为非成员函数。
输入输出操作符(>>和<<)必须为非成员函数。

6.3.1 类成员与非类成员实例:重载+和-操作符

下面演示一个重载操作符实例,重载类Box的加号(定义为成员函数)和减号操作符(定义非成员函数):

#include <iostream>
using namespace std;

class Box {
public:
  double getVolume(void) { return length * breadth * height; }

  void setLength( double len ) { length = len; }
  void setBreadth( double bre ) { breadth = bre; }
  void setHeight( double hei ) { height = hei; }

  Box operator+(const Box& b) {                      // 重载+操作符为成员函数。
    Box box;
    box.length = this->length + b.length;
    box.breadth = this->breadth + b.breadth;
    box.height = this->height + b.height;
    return box;
  }

  friend Box operator-(const Box &, const Box &);
private:
  double length;      // Length of a box
  double breadth;     // Breadth of a box
  double height;      // Height of a box
};

Box operator-(const Box &a, const Box &b) {          // 重载-操作符为非成员函数。
  Box box;
  box.setLength(a.length - b.length);                // 通常要将非成员函数重载操作符设置为所操作类的友元。这样可以直接使用访问私有成员a.length
  box.setBreadth(a.breadth - b.breadth);
  box.setHeight(a.height - b.height);
  return box;
}

int main() {
  Box Box1, Box2, Box3, Box4;
  double volume = 0.0;

  // box 1 specification
  Box1.setLength(6.0);
  Box1.setBreadth(7.0);
  Box1.setHeight(5.0);

  // box 2 specification
  Box2.setLength(12.0);
  Box2.setBreadth(13.0);
  Box2.setHeight(10.0);

  // volume of box 1
  volume = Box1.getVolume();
  cout << "Volume of Box1 : " << volume <<endl;  //Volume of Box1 : 210

  // volume of box 2
  volume = Box2.getVolume();
  cout << "Volume of Box2 : " << volume <<endl;  //Volume of Box2 : 1560

  // Add two object as follows:
  Box3 = Box1 + Box2;                // 操作符+为成员函数。
                                     // 还可以写为Box3 = Box1.operator+(Box2);的形式,但不能写为Box3 = operator+(Box1,  Box2);

  // volume of box 3
  volume = Box3.getVolume();
  cout << "Volume of Box3 : " << volume <<endl;  //Volume of Box3 : 5400

  Box4 = Box2 - Box1;                // 操作符-为非成员函数。
                                     // 还可以写为Box4 = operator-(Box2, Box1);的形式。

  // volume of box 4
  volume = Box4.getVolume();
  cout << "Volume of Box4 : " << volume <<endl;  //Volume of Box4 : 180

  return 0;
}

说明:上面例子仅为演示使用,实际编程时,算术操作符一般都设置为普通非成员函数。

6.4 重载输入输出操作符(>>和<<)

输入操作符(>>)和输出操作符(<<)必须为非成员函数。否则,左操作数只能是该类类型的对象。这与已定义的其他类的输入输出操作符的使用形式不一致,如:

// 把输出操作符<<重载为成员函数的错误例子
A obj;
obj << cout;                       // 由于把<<重载为成员函数,则只能这样使用。这与通常<<的使用形式不一致。

说明:
为了与I/O标准库一致,操作符应接受ostream&作为第一个形参,对类类型const对象的引用作为第二个形参,并返回对ostream形参的引用。
为了与I/O标准库一致,操作符应接受istream&作为第一个形参,对类类型对象的引用作为第二个形参,并返回对istream形参的引用。

和输出操作符不同,输入操作符必须处理输入期间的错误。

6.4.1 实例:重载输入输出操作符

下面演示重载输入输出操作符:

#include <iostream>
using namespace std;

class Distance
{
private:
    int feet;
    int inches;
public:
    Distance(){
        feet = 0;
        inches = 0;
    }

    Distance(int f, int i) {
        feet = f;
        inches = i;
    }

    friend ostream &operator<<(ostream &output, const Distance &D) {  // 第一个形参为ostream&,第二个形参为const对象引用
        output << "Feet : " << D.feet << " Inches : " << D.inches;
        return output;
    }

    friend istream &operator>>(istream  &input, Distance &D) {        // 第一个形参为istream&,第二个形参为对象引用
        input >> D.feet >> D.inches;         // 仅是demo,没有处理输入期间的错误(如输出值和期待的类型不同)。
        return input;
    }
};

int main() {
   Distance D1(11, 10), D2(5, 11), D3;

   cout << "Enter the value of object : " << endl;
   cin >> D3;
   cout << "First Distance : " << D1 << endl;
   cout << "Second Distance :" << D2 << endl;
   cout << "Third Distance :" << D3 << endl;

   return 0;
}

编译并测试运行如下:

$ ./a.out
$ ./4
Enter the value of object :
14
8
First Distance : Feet : 11 Inches : 10
Second Distance :Feet : 5 Inches : 11
Third Distance :Feet : 14 Inches : 8

参考:http://www.tutorialspoint.com/cplusplus/input_output_operators_overloading.htm

6.5 重载赋值操作符

赋值操作符只能定义为成员函数。

我们知道,如果类没有定义自己的赋值操作符,则编译器会合成一个。
可以为一个类定义许多附加的赋值操作符,这些赋值操作符会因右操作符类型不同而不同。

如string类包含好几个重载的赋值操作符:

// illustration of assignment operators for class string
class string {
public:
   string& operator=(const string &);      // s1 = s2;
   string& operator=(const char *);        // s1 = "str";
   string& operator=(char);                // s1 = 'c';
   // ....
};

6.6 重载下标操作符

下标操作符只能定义为成员函数。

类定义下标操作符时,一般需要定义两个版本:一个为非const成员并返回引用,另一个为const成员并返回const引用。

class Foo {
public:
    int &operator[] (const size_t);
    const int &operator[] (const size_t) const;
    // other interface members
private:
    vector<int> data;
    // other member data and private utility functions
};

下标操作符看起来像这样:

int& Foo::operator[] (const size_t index) {
    return data[index];   // no range checking on index
}

const int& Foo::operator[] (const size_t index) const {
    return data[index];   // no range checking on index
}

6.7 重载自增、自减操作符

根据经验原则,自增、自减操作符会改变对象状态,一般重载为类成员函数。

对内置类型而言,自增操作符和自减操作符有前缀和后缀两种形式。我们也可以给自己的类定义自增操作符和自减操作符的前缀和后缀版本。同时定义前缀式操作符和后缀式操作符存在一个问题:它们的形参数目和类型相同,普通重载不能区别所定义的前缀式操作符还是后缀式操作符。
为了解决这一问题,后缀式操作符函数接受一个额外的(即无用的)int型形参,它的唯一目的是使后缀函数与前缀函数区别开来。

说明:
为了与内置类型的操作符一致,前缀式操作符应返回被增量或减量对象的引用。
为了与内置类型的操作符一致,后缀式操作符应返回旧值(即尚未自增或自减的值),并且应作为值返回,而不是返回引用。

6.7.1 实例:重载前缀式和后缀式自增操作符

#include <iostream>
using namespace std;

class Time
{
   private:
      int hours;             // 0 to 23
      int minutes;           // 0 to 59
   public:
      Time() {
         hours = 0;
         minutes = 0;
      }
      Time(int h, int m) {
         hours = h;
         minutes = m;
      }

      void displayTime() {
         cout << "H: " << hours << " M:" << minutes <<endl;
      }

      // overloaded prefix ++ operator
      Time operator++ () {
         ++minutes;          // increment this object
         if(minutes >= 60) {
            ++hours;
            minutes -= 60;
         }
         return *this;                     // 前缀形式返回被增量对象的引用
      }

      // overloaded postfix ++ operator
      Time operator++(int) {               // 后缀形式有额外的int型形参
         // save the orignal value
         Time T(hours, minutes);
         // increment this object
         ++minutes;
         if(minutes >= 60) {
            ++hours;
            minutes -= 60;
         }
         // return old original value
         return T;                          // 后缀形式返回尚未自增的旧值
      }
};

int main() {
   Time T1(11, 59), T2(10,40);

   ++T1;                    // increment T1
   T1.displayTime();        // display T1
   ++T1;                    // increment T1 again
   T1.displayTime();        // display T1

   T2++;                    // increment T2
   T2.displayTime();        // display T2
   T2++;                    // increment T2 again
   T2.displayTime();        // display T2
   return 0;
}

参考:http://www.tutorialspoint.com/cplusplus/increment_decrement_operators_overloading.htm

6.8 不要重载逗号、取地址、逻辑与、逻辑或操作符

默认情况下,取地址操作符(&)和逗号操作符(,)在类类型对象上的执行,与在内置类型对象上的执行一样。取地址操作符返回对象的内存地址,逗号操作符从左至右计算每个表达式的值,并返回最右边操作数的值。
内置逻辑与(&&)和逻辑或(||)操作符使用短路求值。如果重新定义该操作符,将失去操作符的短路求值特征。

总结:重载逗号、取地址、逻辑与、逻辑或等等操作符通常不是好做法。这些操作符具有有用的内置含义,如果我们定义了自己的版本,就不能再使用这些内置含义。

6.9 重载函数调用操作符(函数对象)

可以为类类型的对象重载函数调用操作符。

#include <iostream>
using namespace std;

struct absInt {
  int operator() (int val) {
    return val < 0 ? -val : val;
  }
};

int main() {
  int i = -42;
  absInt absObj;
  unsigned int ui = absObj(i);
  cout << ui << endl;
  return 0;
}

尽管absObj是一个对象而不是函数,我们仍然可以“调用”该对象,效果是运行由absObj对象定义的重载调用操作符,该操作符接受一个int值并返回它的绝对值。
函数对象和函数的使用方式是一样的,能用函数的地方也可以使用函数对象。

6.9.1 函数对象说明(Function Objects or Functors)

定义了调用操作符的类,其对象常称为函数对象(即它们是行为类似函数的对象)。函数对象经常用作通用算法的实参。

Function Objects又称为Functors。

Functors are functions with a state. In C++ you can realize them as a class with one or more private members to store the state and with an overloaded operator () to execute the function. Functors can encapsulate C and C++ function pointers employing the concepts templates and polymorphism. You can build up a list of pointers to member functions of arbitrary classes and call them all through the same interface without bothering about their class or the need of a pointer to an instance. All the functions just have got to have the same return-type and calling parameters. Sometimes functors are also known as closures. You can also use functors to implement callbacks.

参考:http://www.newty.de/fpt/functor.html#chapter4

6.9.2 实例:函数对象代替函数

先看一个普通的函数指针的例子:

#include <iostream>
#include <string>
#include <vector>
using namespace std;

bool GT6(const string &s) {
  return s.size() >= 6;
}

bool GT7(const string &s) {
  return s.size() >= 7;
}

int main() {
  vector<string> words;
  words.push_back("abc");
  words.push_back("abcdef");
  words.push_back("abcdefg");
  words.push_back("qwertyuiop");

  cout << count_if(words.begin(), words.end(), GT6)
       << " words 6 characters or longer" << endl;            // 3 words 6 characters or longer

  cout << count_if(words.begin(), words.end(), GT7)
       << " words 7 characters or longer" << endl;            // 2 words 7 characters or longer

  return 0;
}

再看使用函数对象的例子:

#include <iostream>
#include <string>
#include <vector>
using namespace std;

class GT_cls {
public:
  GT_cls(size_t val = 0): bound(val) { }

  bool operator()(const string &s) {
    return s.size() >= bound;
  }

private:
  string::size_type bound;
};

int main() {
  vector<string> words;
  words.push_back("abc");
  words.push_back("abcdef");
  words.push_back("abcdefg");
  words.push_back("qwertyuiop");

  cout << count_if(words.begin(), words.end(), GT_cls(6))       // 把函数指针GT6换成了函数对象GT_cls(6)
       << " words 6 characters or longer" << endl;

  cout << count_if(words.begin(), words.end(), GT_cls(7))       // 把函数指针GT7换成了函数对象GT_cls(7)
       << " words 7 characters or longer" << endl;

  return 0;
}

在上面使用函数对象的例子中,count_if调用传递一个GT_cls类型的临时对象(用整型值6或7来初始化那个临时对象)而不再是名为GT6或GT7的函数。现在,count_if每次调用它的函数形参时,它都使用GT_cls的调用操作符。

从上面的例子中可知, 函数对象比函数更加灵活,不再需要多个功能类似的函数(如例子中GT6和GT7等等)。

注:我们也可以通过增加一个参数来统一GT6和GT7等函数,如下:

bool GTn(const string &s, size_t n) {
  return s.size() >= n;
}

如何把参数n传递给count_if呢?通过全局变量来传递显然不太合适,可能的办法是“改进”count_if函数,让count_if也增加一个参数来接收n。这种方法可行,但也不好,如果我们要计算长度大于6而小于10的元素个数,是不是又要给count_if增加另一个参数呢?显然不合适。

6.9.3 实例:函数对象可保存状态

下面演示多次调用同一函数对象的例子:

#include <iostream>

class myFunctor1 {
public:
  myFunctor1 (int x) : _x(x) {}
  int operator() (int y) { return _x + y; }
private:
  int _x;
};

class myFunctor2 {
public:
  myFunctor2 (int x) : _x(x) {}
  int operator() (int y) { _x=_x + y; return _x; }     // 重载调用操作符时,修改了对象的状态(数据域)。
private:
  int _x;
};

int main() {
  myFunctor1 addFive1(5);
  std::cout << addFive1(1) << std::endl;
  std::cout << addFive1(1) << std::endl;

  myFunctor2 addFive2(5);
  std::cout << addFive2(1) << std::endl;
  std::cout << addFive2(1) << std::endl;
  return 0;
}

编译运行上面程序,可得到下面输出:

6
6
6
7

上面程序中,以相同参数多次调用函数对象addFive1时,得到的结果相同;而以相同参数多次调用函数对象addFive2时,得到的结果不同。这取决于如何重载调用操作符。

6.9.4 标准库定义的函数对象

标准库定义了一组算术、关系与逻辑函数对象类。这些标准库函数对象类型是在functional头文件中定义的。

Table 4: 标准库函数对象
函数对象 所应用的操作符
plus<Type> +
minus<Type> -
multiplies<Type> *
divides<Type> /
modulus<Type> %
negate<Type> -
equal_to<Type> ==
not_equal_to<Type> !=
greater<Type> >
greater_equal<Type> >=
less<Type> <
less_equal<Type> <=
logical_and<Type> &&
logical_or<Type> ||
logical_not<Type> !

实例如下:

#include <iostream>
#include <string>
#include <vector>
using namespace std;

int main() {
  vector<string> words;
  words.push_back("yabc");
  words.push_back("xabcde");
  words.push_back("znm");

  sort(words.begin(), words.end(), greater<string>());   // 按降序对words进行排序

  for (std::vector<string>::const_iterator i = words.begin(); i != words.end(); ++i) {
    cout << *i << endl;
  }
  return 0;
}

上面程序会输出:

znm
yabc
xabcde

例子中,sort的第三个实参是一个函数对象(即greater<string>类型的临时对象)。

参考:http://en.cppreference.com/w/cpp/utility/functional

6.9.4.1 函数适配器(Function Adapter)

标准库提供了一组函数适配器,用于特化和扩展一元和二元函数对象。函数适配器分为如下两类:

  • 绑定器:它通过将一个操作数绑定到给定值而将二元函数对象转换为一元函数对象。
  • 求反器:它将谓词函数对象的真值求反。

标准库定义了两个绑定器适配器:bind1st和bind2nd。bind1st将给定值绑定到二元函数对象的第一个实参,bind2nd将给定值绑定到二元函数对象的第二个实参。
标准库还定义了两个求反器:not1和not2。not1将一元函数对象的真值求反,not2将二元函数对象的真值求反。

7 面向对象编程

面向对象编程基于三个基本概念:数据抽象、继承和动态绑定。在C++中,用类进行数据抽象,用类派生从一个类继承另一个:派生类继承基类的成员。动态绑定使编译器能够在运行时决定是使用基类中定义的函数还是派生类中定义的函数。

7.1 继承(inheritance)

利用继承可以把相关事物公共的东西共享起来,仅仅特化不同的东西。这样可以减少代码重复。

7.1.1 继承的例子

// 书店关于书的类
#include <string>
#include <iostream>
using namespace std;

class Item_base {
public:
    Item_base(const std::string &book = "", double sales_price = 0.0):
        isbn(book), price(sales_price) { }

    std::string book() const { return isbn; }
    virtual double net_price(std::size_t n) const { return n * price; }
    virtual ~Item_base() { }

private:
    std::string isbn;         // identifier for the item

protected:
    double price;             // normal, undiscounted price
};

// Bulk为批量购买相关的类
class Bulk : public Item_base {
public:
    Bulk(): min_qty(0), discount(0.0) { }

    Bulk(const std::string& book, double sales_price,
              std::size_t qty = 0, double disc_rate = 0.0):
        Item_base(book, sales_price),
        min_qty(qty), discount(disc_rate) { }

    // redefines base version so as to implement bulk purchase discount policy
    double net_price(std::size_t cnt) const {
        if (cnt >= min_qty)
            return cnt * (1 - discount) * price;
        else
            return cnt * price;
    }

private:
    std::size_t min_qty;      // 享受折扣的最少购买量
    double discount;          // 折扣
};

// 输出n本书的价格
void print_total(ostream &os, const Item_base &item, size_t n) {
    os << "ISBN: " << item.book() // calls Item_base::book
       << "\tnumber sold: " << n << "\ttotal price: "
       << item.net_price(n) << endl;
    // virtual call: which version of net_price to call is resolved at run time
}

int main() {

    Item_base base("0-391-85410-4", 10);           // 每本10元
    Bulk bulk("0-201-82470-1", 10, 50, 0.2);       // 每本10元,满50本打8折

    print_total(cout, base, 30);
    print_total(cout, base, 90);
    print_total(cout, bulk, 30);
    print_total(cout, bulk, 90);

    return 0;
}
/* 程序输出:
ISBN: 0-391-85410-4     number sold: 30 total price: 300
ISBN: 0-391-85410-4     number sold: 90 total price: 900
ISBN: 0-201-82470-1     number sold: 30 total price: 300
ISBN: 0-201-82470-1     number sold: 90 total price: 720
*/

说明:
在C++中,基类必须指出希望派生类重写哪些函数。定义为virtual的函数(虚函数)是基类期待派生类重新定义的,基类希望派生类继承的函数不能定义为虚函数。

关于print_total有两点值得注意:
第一,虽然这个函数的第二形参是Item_base的引用但可以将Item_base对象或Bulk对象(其类继承自Item_base)传给它。
第二,因为形参是引用且net_price是虚函数,所以对net_price的调用将在运行时确定。调用哪个版本的net_price将取决于传给print_total的实参。

7.1.2 期望派生类重定义的函数在基类中应声明为虚函数

基类应将期待派生类重定义的函数定义为虚函数。如前面例子中,派生类重定义了net_price虚函数。

除构造函数外,任意非static成员函数都可以是虚函数。
一旦函数在基类中声明为虚函数,它就一直为虚函数,派生类无法改变该函数为虚函数这一事实。派生类重定义虚函数时,可以使用或者省略virtual保留字。

7.1.2.1 构造函数不能为虚函数

前面说到:除构造函数外,任意非static成员函数都可以是虚函数。也就是说构造函数不能为虚函数,如下面的例子是错误的:

class A {
  virtual A();   /* 错误的用法(编辑器会报错)!构造函数不能是虚函数。 */
};

为什么构造函数不能为虚函数呢?

A virtual call is a mechanism to get work done given partial information. In particular, "virtual" allows us to call a function knowing only an interfaces and not the exact type of the object. To create an object you need complete information. In particular, you need to know the exact type of what you want to create. Consequently, a "call to a constructor" cannot be virtual.

摘自:Bjarne Stroustrup's C++ Style and Technique FAQ, Why don't we have virtual constructors?

7.1.3 protected成员可以被派生类访问

派生类中可以直接访问基类的protected成员。

Table 5: 类的访问标号
访问标号 可以从类外访问 可以从派生类访问
public Yes Yes
protected No Yes
private No No

注意:派生类只能通过派生类对象访问其基类的protected成员,派生类对其基类类型对象的protected成员没有特殊访问权限。
如下面实例,memfcn是派生类成员函数,也不能通过b(基类类型对象)来访问price(基类的protected成员)。

void Bulk::memfcn(const Bulk_item &d, const Item_base &b) {
    double ret = price; // ok: 直接使用基类中的protected成员
    ret = d.price;      // ok: 通过派生类对象使用基类中的protected成员
    ret = b.price;      // error: no access to price from an Item_base
}

7.1.4 公用、受保护和私有继承

为了定义派生类,使用类派生列表指定基类。类派生列表指定了一个或多个基类(多个基类之间用逗号分开),具体如下形式:

class classname: access-label base-class [, access-label base-class]

这里access-label是public、protected或private,分别代表公用、受保护和私有继承。其中公用继承的使用最常见。
• 如果是公用继承,基类成员保持自己的访问级别:基类的public成员为派生类的public成员,基类的protected成员为派生类的protected成员。
• 如果是受保护继承,基类的public和protected成员在派生类中为protected成员。
• 如果是私有继承,基类的的所有成员在派生类中为private成员。

Table 6: 公用、受保护和私有继承如何影响派生类
基类成员 公用继承 受保护继承 私有继承
public public protected private
protected protected protected private
private 不被继承 不被继承 不被继承

7.1.5 派生类对象包含基类对象作为子对象

派生类对象由多个部分组成:派生类本身定义的成员加上由基类成员组成的子对象。
如果基类定义static成员,则整个继承层次中只有一个这样的成员。无论从基类派生出多少个派生类,每个static成员只有一个实例。

7.1.6 友元关系不能继承

每个类控制对自己的成员的友元关系。友元关系不能继承。
第一,基类的友元对派生类的成员没有特殊访问权限;
第二,如果基类被授予友元关系,则只有基类具有特殊访问权限,该基类的派生类不能访问授予友元关系的类。

如:

class Base {
    friend class Frnd;
protected:
    int i;
};

// Frnd has no access to members in D1
class D1 : public Base {
protected:
    int j;
};

class Frnd {
public:
    int mem(Base b) { return b.i; }   // ok: Frnd is friend to Base
    int mem(D1 d) { return d.i; }     // error: 基类的友元对派生类的成员没有特殊访问权限
};

// D2 has no access to members in Base
class D2 : public Frnd {
public:
    int mem(Base b) { return b.i; }   // error: 如果基类被授予友元关系,则只有基类具有特殊访问权限,该基类的派生类不能访问授予友元关系的类。
};

7.2 动态绑定(虚函数+基类引用或指针)

动态绑定使编译器能够在运行时决定是使用基类中定义的函数还是派生类中定义的函数。

C++中的函数调用默认不使用动态绑定。要触发动态绑定,满足两个条件:
第一,只有指定为虚函数的成员函数才能进行动态绑定,成员函数默认为非虚函数,非虚函数不进行动态绑定;
第二,必须通过基类类型的引用或指针进行函数调用。

如前面例子中的print_total函数中对item.net_price的调用会使用动态绑定,因为item形参是一个基类引用且net_price是虚函数。

void print_total(ostream &os, const Item_base &item, size_t n) {
    os << "ISBN: " << item.book() // calls Item_base::book
       << "\tnumber sold: " << n << "\ttotal price: "
       << item.net_price(n) << endl;
    // virtual call: which version of net_price to call is resolved at run time
}

7.2.1 强制使用虚函数特定版本(作用域操作符)

在某些情况下,希望覆盖虚函数机制并强制函数调用使用虚函数的特定版本,这时可以使用基类类名加上作用域操作符:

Item_base *baseP = &derived;
// calls version from the base class regardless of the dynamic type of baseP
double d = baseP->Item_base::net_price(42);    // 强制将net_price调用确定为Item_base中定义的版本

在派生类虚函数调用其基类版本时,常常显式使用作用域操作符。

7.3 派生类中的构造函数和复制控制

构造函数和复制控制成员不能继承,每个类定义自己的构造函数和复制控制成员。像任何类一样,如果类不定义自己的默认构造函数和复制控制成员,就将使用编译器自动合成的版本。

7.3.1 派生类构造函数

每个派生类构造函数除了初始化自己的数据成员之外,还要初始化基类。

7.3.1.1 合成的派生类默认构造函数

派生类的合成默认构造函数与非派生的构造函数只有一点不同:除了初始化派生类的数据成员之外,它还初始化派生类对象的基类部分。基类部分由基类的默认构造函数初始化。

对于前面例子中Bulk类,合成的默认构造函数会这样执行:
首先:调用Item_base的默认构造函数,将isbn成员初始化空串,将price成员初始化为0。
然后:用常规变量初始化规则初始化Bulk的成员,也就是说,内置类型成员qty和discount会是未初始化的。

7.3.1.2 定义默认构造函数

因为Bulk具有内置类型成员,所以应定义自己的默认构造函数:

class Bulk : public Item_base {
public:
    Bulk(): min_qty(0), discount(0.0) { }
    // as before
};

这个构造函数使用构造函数初始化列表初始化min_qty和discount成员,由于没有在初始化列表中指定基类的构造函数,该构造函数还会 隐式调用Item_base的默认构造函数 初始化对象的基类部分。如果在初始化列表中指定了基类的构造函数,则调用指定的基类构造函数(可能不是默认构造函数)。

7.3.1.3 只能初始化直接基类

一个类只能初始化自己的直接基类。直接就是在派生列表中指定的类。如果类C从类B派生,类B从类A派生,则B是C的直接基类。虽然每个C类对象包含一个A类部分,但C的构造函数不能直接初始化A部分。相反,需要类C初始化类B,而类B的构造函数再初始化类A。

7.3.2 复制控制和继承

类是否需要定义复制控制成员完全取决于类自身的直接成员。基类可以定义自己的复制控制而派生类使用合成版本,反之亦然。
只包含类类型或内置类型数据成员、不含指针的类一般可以使用合成操作,复制、赋值或撤销这样的成员不需要特殊控制。具有指针成员的类一般需要定义自己的复制控制来管理这些成员。

7.3.2.1 定义派生类复制构造函数(应显式负责基类部分)

如果派生类显式定义自己的复制构造函数或赋值操作符,则该定义将完全覆盖默认定义。
如果派生类定义了自己的复制构造函数,该复制构造函数应显式使用基类复制构造函数初始化对象的基类部分。 如:

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转换为它的基类部分的引用,并调用基类复制构造函数。

7.3.2.2 定义派生类赋值操作符(应显式负责基类部分)

赋值操作符通常与复制构造函数类似:如果派生类定义了自己的赋值操作符,则该操作符必须对基类部分进行显式赋值。

// Base::operator=(const Base&) not invoked automatically
Derived &Derived::operator=(const Derived &rhs) {
    if (this != &rhs) {
        Base::operator=(rhs); // assigns the base part
        // do whatever needed to clean up the old value in the derived part
        // assign the members from the derived
    }
    return *this;
}
7.3.2.3 派生类析构函数(只需负责自身部分)

析构函数的工作与复制构造函数和赋值操作符不同:派生类析构函数不负责撤销基类对象的成员。编译器总是显式调用派生类对象基类部分的析构函数。每个析构函数只负责清除自己的成员。

7.3.3 虚析构函数

为什么要有虚析构函数?
如果删除基类指针,则需要运行基类析构函数并清除基类的成员,如果对象实际是派生类型的,则没有定义该行为。
这种情况下(即当通过基类指针删除派生类对象时),为了保证运行适当的析构函数,基类中的析构函数必须为虚函数。

像其他虚函数一样,析构函数的虚函数性质都将继承。即如果层次中根类的析构函数为虚函数,则派生类析构函数也将是虚函数,无论派生类显式定义析构函数还是使用合成析构函数,派生类析构函数都是虚函数。

总结:如果一个类用作基类,那么它的析构函数应该声明为虚函数。

7.3.3.1 三法则的例外情况

三法则指出,如果类需要析构函数,则它往往也需要复制构造函数和赋值操作符。

基类析构函数是三法则的一个重要例外。
即使析构函数没有工作要做,继承层次的根类也应该定义一个虚析构函数。但这并不表示也需要赋值操作符或复制构造函数。

7.3.4 构造函数或析构函数中调用虚函数(动态绑定会无效)

如果在构造函数或析构函数中调用(或通过其它函数间接调用)虚函数,则动态绑定会无效,运行的是构造函数或析构函数自身类型定义的版本。

为什么编译器要这样做?
运行构造函数或析构函数的时候,对象都是不完整的(可能还没构造好,可能已经部分撤消)。
考虑如果从基类构造函数(或析构函数)调用虚函数的派生类版本会怎么样。虚函数的派生类版本很可能会访问派生类对象的成员。但是,对象的派生部分的成员不会在基类构造函数运行期间初始化。

7.4 纯虚函数和抽象基类

A pure virtual function is a virtual function whose declaration ends in "= 0":

class Base {
  // ...
  virtual void f() = 0;     // pure virtual funciton
  // ...
};

int main() {
    Base obj;               // 出错。因为不能实例化抽象基类!
    return 0;
}

含有(或继承)一个或多个纯虚函数的类称为抽象基类(abstract base class)。抽象基类不能实例化。

纯虚函数的用处:
Pure virtual helps ensure the derived classes do not forget to redefine functions that the base class was expecting them to.

7.5 覆盖(override)、重载(overload)和隐藏(hide)的区别

覆盖(override)是指派生类中函数覆盖基类中的虚函数。特征如下:
一、不同的范围(分别位于派生类与基类中);
二、函数名字相同;
三、参数相同;
四、基类函数必须有virtual关键字。
备注:要实现基于虚函数的多态就必须在派生类中覆盖基类的虚函数。为什么取名为覆盖(override)呢?参见节 7.7.1

重载(overload)函数是指在同一个作用域中的多个函数,它们具有相同的名字而形参表不同。特征如下:
一、相同的范围(如同一个类中);
二、函数名字相同;
三、参数不同;
四、virtual关键字可有可无。

隐藏(hide)是指派生类的函数屏蔽了与其同名的基类函数。特征如下:
一、如果派生类的函数与基类的函数同名,但是参数不同。此时无论有无virtual关键字,基类的函数将被隐藏(注意不要与重载混淆);
二、如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual关键字,此时,基类的函数被隐藏(注意不要与覆盖混淆)。
备注:我们要尽量避免出现隐藏。出现隐藏时,可以使用作用域操作符访问被隐藏的基类函数(或数据成员)。

7.6 多重继承和虚继承

大多数应用程序使用单个基类的公用继承,但是,在某些情况下单继承是不够用的。

多重继承示例图,如:

   +-------------+     +------------+
   |   Bear      |     | Endangered |
   +-------------+     +------------+
                ^        ^
                |        |
                |        |
             +-------------+
             |   Panda     |
             +-------------+

7.6.1 虚继承(解决The Diamond Problem)

在常规多重继承下,一个基类可以在派生层次中出现多次。
如果IO类型使用常规继承,则每个iostream对象可能包含两个ios子对象:一个包含在它的istream子对象中,另一个包含在它的ostream子对象中,从设计角度讲这个是错误的:因为iostream类仅想对单个缓冲区进行读和写。

在C++中,通过使用虚继承解决这类问题。虚继承是一种机制,类通过虚继承指出它希望共享其虚基类的状态。 在虚继承下,对给定虚基类,无论该类在派生层次中作为虚基类出现多少次,只继承一个共享的基类子对象。 共享的基类子对象称为虚基类(virtual base class)。

通过用关键字virtual修改声明,可将基类指定为通过虚继承派生,这时派生类中它们的公共基类部分仅有一个。

// the order of the keywords public and virtual is not significant
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 { ... };

参考:
https://en.wikipedia.org/wiki/Multiple_inheritance#The_diamond_problem

7.7 利用虚函数表实现动态绑定

A virtual method table (or virtual function table, virtual call table, dispatch table, vtable, or vftable) is a mechanism used in a programming language to support dynamic dispatch (or run-time method binding).

说明:关于虚函数表的具体实现不同编译器很可能不一样,下面的说明仅是“原理性的”。

如果一个类含有虚函数,则这个类会有一个虚函数表,它保存着当前类所有虚函数的地址。

假设类B的声明如下:

class B {
public:
  virtual int f1();
  virtual void f2(int);
  virtual int f3(int);
};

那么,类B对象的内存模型如图 1 所示。其中,bp是类B对象的指针,vtbl就是虚函数表,而vptr是指向虚函数表的指针。

cxx_vtbl_1.jpg

Figure 1: 类B对象的内存模型

“A virtual table is per class.” 同一类型的对象可以共用一个虚函数表,没必要为每个对象生成一个虚函数表(不过,严格来说依赖于实现)。

对虚函数的调用会查找vtbl,如:

B *bp = new B;
bp->f3(12);

会被编译器翻译为类似下面的代码:

B *bp = new B;
(*(bp->vptr)[2])(bp, 12);      // f3在vtbl中的下标为2

上面介绍了虚函数表的基本概念,如何利用它来实现动态绑定呢?请看下文。

参考:
Gotcha #78: Failure to Grok Virtual Functions and Overriding: http://www.icodeguru.com/cpp/CppGotchas/0321125185_ch07lev1sec10.html

7.7.1 单继承下的虚函数表


考虑下面继承关系。

class B {
public:
  virtual int f1();
  virtual void f2(int);
  virtual int f3(int);
};

class D : public B {
  int f1();           // override
  virtual void f4();
  int f3(int);        // override
};

类D对象的内存模型如图 2 所示。

cxx_vtbl_2.jpg

Figure 2: 类D对象(它单继承于类B)的内存模型

注意, 基类B中没有被子类D覆盖的虚函数(如f2)会直接出现在子类D的虚函数表中。
对虚函数的调用是通过查找vtbl,如:

B *bp = new D;
bp->f2(11);
bp->f3(12);

会被编译器翻译为类似下面的代码:

B *bp = new D;
(*(bp->vptr)[1])(bp, 11);    // 在这个例子的虚函数表中,对应函数B::f2
(*(bp->vptr)[2])(bp, 12);    // 在这个例子的虚函数表中,对应函数D::f3

当然,下面的写法看起来更像多态。

B *bp = getSomeSortOfB();  // 这里,并不知道bp会指向B的哪个子类对象(比如D1或D2等等),或者基类B的对象。
bp->f2(11);                // 不过,不管bp指向B的哪个子类对象,通过这个对象的虚函数表总可以找到合适的f2。
                           // 编译时,函数bp->f2的地址就确定了,这个例子中是虚函数表中下标为1的位置;
                           // 虚函数表中下标为1的位置上具体是哪个函数在编译时是不确定的,它可能是B::f2,
                           // 也可能是B类的任意子类中的虚函数f2。这就是虚函数表实现动态绑定的原理。
bp->f3(12);                // 和上面分析类似。

通过上面的分析,我们可以知道多态中“覆盖(override)”这个名词的形象解释:

Mechanically speaking, overriding is the process of replacing the address of a base class member function with the address of a derived class member function when constructing a virtual function table for a derived class.

7.7.2 多继承下的虚函数表

关于多继承下的虚函数表,这里不介绍,请参考:Gotcha #78: Failure to Grok Virtual Functions and Overriding

8 模板与泛型编程

所谓泛型编程就是以独立于任何特定类型的方式编写代码。模板是泛型编程的基础。

8.1 函数模板

模板定义以关键字template开始,后接模板形参表,模板形参表是用尖括号括住的一个或多个模板形参的列表,形参之间以逗号分隔。
使用函数模板时,编译器会推断哪个(或哪些)模板实参绑定到模板形参。一旦编译器确定了实际的模板实参,就称它实例化了函数模板的一个实例。

#include<iostream>
using namespace std;

// implement strcmp-like generic compare function
// returns 0 if the values are equal, 1 if v1 is larger, -1 if v1 is smaller
template <typename T>                      // 也可写为 template <class T>
int compare(const T &v1, const T &v2) {
  if (v1 < v2) return -1;
  if (v2 < v1) return 1;
  return 0;
}

int main () {
  // T is int;
  // compiler instantiates int compare(const int&, const int&)
  cout << compare(1, 0) << endl;

  // T is string;
  // compiler instantiates int compare(const string&, const string&)
  string s1 = "hi", s2 = "world";
  cout << compare(s1, s2) << endl;
  return 0;
}

说明:在函数模板中,编译器一般能根据函数实参确定模板实参,所以调用时不用为模板形参指定实参。
但显式指定也是合法的,如:

int main () {
  cout << compare<int>(1, 0) << endl;         // 显示指定了函数模板的实参为int

  string s1 = "hi", s2 = "world";
  cout << compare<string>(s1, s2) << endl;    // 显示指定了函数模板的实参string
  return 0;
}

8.2 类模板

就像可以定义函数模板一样,也可以定义类模板。

#ifndef MYARRAY_H
#define MYARRAY_H
template <typename T, size_t N>      // 也可写为 template <class T, size_t N>
class MyArray {
private:
  T m_ptData[N];

public:
  T& operator[](int nIndex) {
    return m_ptData[nIndex];
  }

  int GetBuffer(); // templated GetBuffer() function defined below
};

template <typename T, size_t N>
int MyArray<T,N>::GetBuffer() { return m_ptData; }
#endif

与调用函数模板不同,使用类模板时,必须为模板形参显式指定实参:

MyArray<int, 12> anArray;

参考:http://www.learncpp.com/cpp-tutorial/143-template-classes/

8.3 模板声明

模板声明像其他任意函数或类一样,对于模板可以只声明而不定义。声明必须指出函数或类是一个模板:

// declares compare but does not define it
template <class T> int compare(const T&, const T&);

8.4 模板特化(Template Specialization)

为什么需要模板特化?
我们并不总是能够写出对所有可能被实例化的类型都最合适的模板。某些情况下,通用模板定义对于某个类型可能是完全错误的,通用模板定义也许不能编译或者做错误的事情;另外一些情况下,可以利用关于类型的一些特殊知识,编写比从模板实例化来的函数更有效率的函数。

通过模板特化,可以使编译器优先使用我们对指定类型模板形参的特化版本。
函数模板特化和类模板特化比较类似,下面仅介绍函数模板特化。

8.4.1 函数模板特化

函数模板特化的形式如下:
• 关键字template后面接一对空的尖括号(<>);
• 再接模板名和一对尖括号,尖括号中指定这个特化定义的模板形参;
• 函数形参表;
• 函数体。

如:

template <>
int compare<const char*>(const char* const &v1,      // 尖括号中指定这个特化定义的模板形参
                         const char* const &v2) {
     return strcmp(v1, v2);
}

现在,当调用compare函数的时候,传给它两个字符指针,编译器将调用特化版本。编译器将为任意其他实参类型(包括普通 char*)调用泛型版本。

const char *cp1 = "world", *cp2 = "hi";
int i1, i2;
compare(cp1, cp2); // calls the specialization
compare(i1, i2);   // calls the generic version instantiated with int

9 异常处理

通过异常我们能够将问题的检测和问题的解决分离。
使用异常的好处:Using exceptions for error handling makes your code simpler, cleaner, and less likely to miss errors.

参考:
Exceptions and Error Handling: https://isocpp.org/wiki/faq/exceptions

9.1 throw表达式和try/catch块

throw表达式用来引发(raise)异常条件,错误检测部分使用它来说明遇到了不可处理的错误。
try/catch块用来捕获和处理异常,在try块中执行的代码所抛出的异常,通常会被其中一个catch子句处理。
try/catch块的通用语法形式为:

try {
    program-statements
} catch (exception-specifier) {
    handler-statements
} catch (exception-specifier) {
    handler-statements
}

实例:

#include <iostream>
using namespace std;

void func1() {
    throw 101;
}

int main() {
    int val;

    try {
        func1();
    } catch (int e) {
        cerr << "Error " << e << " happened" << endl;
    }

    return 0;
}
// 程序会输出: Error 101 happened

9.2 异常处理过程

抛出异常的时候(异常由throw引发),将停止当前函数的执行,开始查找匹配的catch子句。首先检查throw本身是否在try块内部,如果是,检查与该catch相关的catch子句,看是否其中之一与抛出对象相匹配。如果找到匹配的catch,就处理异常;如果找不到,就退出当前函数(释放当前函数的内存并撤销局部对象,自动调用析构函数),并且继续在调用函数(caller)中按类似的方式查找。直到为异常找到一个匹配的catch子句(这个过程称为stack unwinding)。找到后就进入该catch子句,并在该处理代码中继续执行。如果异常发生时,在当前函数及函数调用链中更上层的函数中都找不到匹配的catch语句,则程序会调用库函数 std::terminate

抛出异常后,控制权不会再回到抛出异常的地方。

9.2.1 查找匹配的catch子句

在查找匹配的catch期间,找到的catch不必是与异常最匹配的那个catch,相反,将选中第一个找到的可以处理该异常的catch。因此,在catch子句列表中,最特殊的catch必须最先出现。

异常与catch异常说明符匹配的规则比匹配实参和形参类型的规则更严格 ,大多数转换都不允许——除下面几种可能的区别之外,异常的类型与catch说明符的类型必须完全匹配:
• 允许从非const到const的转换。也就是说,非const对象的throw可以与指定接受const引用的catch匹配。
• 允许从派生类型型到基类类型的转换。
• 将数组转换为指向数组类型的指针,将函数转换为指向函数类型的适当指针。

在查找匹配catch的时候,不允许其他转换。具体而言,既不允许标准算术转换,也不允许为类类型定义的转换。

9.2.2 抛出类类型的异常

异常是通过throw对象而引发的。由于在处理异常时会释放局部存储,所以被抛出的对象就不能再局部存储,而是用throw表达式初始化一个称为异常对象的特殊对象。异常对象由编译器管理,而且保证驻留在可能被激活的任意catch都可以访问的空间。这个对象由throw创建,并被初始化为被抛出的表达式的副本。异常对象将传给对应的catch,并且在完全处理了异常之后撤销。

异常对象通过复制被抛出表达式的结果创建,该结果必须是可以复制的类型。

9.2.3 重新抛出异常(空throw语句)

catch语句中可以通过重新抛出将异常传递给函数调用链中更上层的函数。重新抛出的语法是一个空throw语句:

throw;

注意:空throw语句中能出现在catch或者从catch调用的函数中,如果在异常处理代码不活动时遇到空throw语句,将调用terminate函数。

9.2.4 捕获所有异常(catch三个点)

捕获所有异常的catch子句形式为(…),如:

// matches any exception that may be thrown
catch(...) {
   //
}

9.3 析构函数不要抛出异常

我们知道异常处理时,在栈展开时会撤销局部对象(自动调用析构函数),如果这个过程中执行析构函数时又抛出异常。应该怎么处理呢?是忽略析构函数中的异常接着处理之前未处理完的异常?还是忽略之前异常,处理析构函数中的异常?显然都不合适。

在为某个异常进行栈展开的时候,析构函数如果又抛出自己的未经处理的另一个异常,将会导致调用std::terminate函数。一般而言,std::terminate函数将调用std::abort函数,强制从整个程序非正常退出。

测试如下:

include <iostream>
using namespace std;

class A
{
public:
    A() { cout << "A constructor called" << endl; }
    ~A() {
      cout << "A destructor called" << endl;
      throw 202;                  // 在析构函数中抛出异常,这是危险的用法!
    }
};

void func1();

int main() {
    int val;

    try {
        func1();
    } catch (int e) {
        cerr << "Error " << e << " happened" << endl;
    }

    return 0;
}

void func1() {
    A obja;                        // 这个局部对象会在异常处理时,进行栈展开时被撤销。

    throw 101;
}

上面程序在处理异常(throw 101)时会撤销局部对象obja,调用析构函数~A(),但析构函数又抛出了异常(throw 202),这会导致程序异常中止:

A constructor called
A destructor called
terminate called after throwing an instance of 'int'
Aborted

所以,在实践中要求析构函数永远不要抛出异常。标准库类型都保证它们的析构函数不会引发异常。

9.4 构造函数抛出异常前要释放资源

与析构函数不同,构造函数内部所做的事情经常会抛出异常。如果在构造函数对象的时候发生异常,则该对象可能只是部分被构造,它的一些成员可能已经初始化,而另一些成员在异常发生之前还没有初始化。

如果对象没有完全构造出来,只是部分被构造,C++不会为它调用析构函数。 因为这很难做到,可能的办法是在每个对象里加入一些字节来标记构造函数执行了多少步,析构函数检测这些字节来判断该执行哪些操作。但这会影响到效率,C++没有这么做。

所以,我们在构造函数中出现异常,导致对象无法完整地被构造时,需要在继续传递异常前自己释放已经分配的资源。

如:

BookEntry::BookEntry() {
    try {
        theImage = new Image();
        theAudioClip = new AudioClip();
    }
    catch (...) {              // 捕获所有异常
        delete theImage;
        delete theAudioClip;
        throw;                 // 继续传递异常
    }
}

参考:More Effective C++, Item M10:在构造函数中防止资源泄漏

9.5 Exception Specifications

In older C++ code, you may find exception specifications. For example:

void f(int) throw(Bad, Worse); // may only throw Bad or Worse exceptions
void g(int) throw();           // may not throw, if an exception is thrown, the program terminates.

This feature has not been a success and is deprecated. Don’t use it.

9.6 Function try-Blocks(可以捕获初始化列表中的异常)

The body of a function can be a try-block. For example:

int main() try
{
  // ... do something ...
}
catch (...) {
  // ... handle exception ...
}

对于普通函数来说上面形式没太多用。 对于构造函数来说它很有用,因为它可以捕获初始化列表中的异常!

class X {
  vector<int> vi;
  vector<string> vs;
  // ...
public:
  X(int,int);
  // ...
};

X::X(int sz1, int sz2)
try
    :vi(sz1),           // construct vi with sz1 ints
     vs(sz2),           // construct vs with sz2 strings
{
  // ...
}
catch (std::exception& err) {     // exceptions thrown for vi and vs are caught here
  // ...
}

参考:The C++ Programming Language, by Bjarne Stroustrup, 4th Edition, 13.5.2.4 Function try-Blocks

9.7 C++ Standard Exceptions

类exception的hierarchy如下:

std::exception <exception> interface (debatable if you should catch this)
    std::bad_alloc <new> failure to allocate storage
        std::bad_array_new_length <new> invalid array length
    std::bad_cast <typeinfo> execution of an invalid dynamic-cast
    std::bad_exception <exception> signifies an incorrect exception was thrown
    std::bad_function_call <functional> thrown by "null" std::function
    std::bad_typeid <typeinfo> using typeinfo on a null pointer
    std::bad_weak_ptr <memory> constructing a shared_ptr from a bad weak_ptr
    std::logic_error <stdexcept> errors detectable before the program executes
        std::domain_error <stdexcept> parameter outside the valid range
        std::future_error <future> violated a std::promise/std::future condition
        std::invalid_argument <stdexcept> invalid argument
        std::length_error <stdexcept> length exceeds its maximum allowable size
        std::out_of_range <stdexcept> argument value not in its expected range
    std::runtime_error <stdexcept> errors detectable when the program executes
        std::overflow_error <stdexcept> arithmetic overflow error.
        std::underflow_error <stdexcept> arithmetic underflow error.
        std::range_error <stdexcept> range errors in internal computations
        std::regex_error <regex> errors from the regular expression library.
        std::system_error <system_error> from operating system or other C API
            std::ios_base::failure <ios> Input or output error

exception类所定义的唯一操作是一个名为what的虚函数,该函数返回const char*对象,它一般返回用来在抛出位置构造异常对象的信息。由于what是虚函数,如果捕获了基类类型引用,对what的调用将执行适合异常对象的动态类型的版本。

logic_error和runtime_error是最主要的两种异常。logic_error是程序中可以避免的(如通过检测函数的参数),而runtime_error是其它所有异常。

参考:
http://stackoverflow.com/questions/11938979/what-exception-classes-are-in-the-standard-c-library
http://www.cplusplus.com/reference/exception/
http://www.cplusplus.com/reference/stdexcept/
http://en.cppreference.com/w/cpp/error/exception

9.7.1 标准库会抛出哪些异常

                     Standard-Library Exceptions                                  
 bitset             
 iostream           
 regex              
 string             
 vector             
 new T              
 dynamic_cast(r) 
 typeid()           
 thread             
 call_once()        
 mutex              
 condition_variable 
 async()            
 packaged_task      
 future and promise 
 Throws invalid_argument, out_of_range, overflow_error       
 Throws ios_base::failure                                    
 Throws regex_error                                          
 Throws length_error, out_of_range                           
 Throws out_of_range                                         
 Throws bad_alloc if it cannot allocate memory for a T       
 Throws bad_cast if it cannot convert the reference r to a T 
 Throws bad_typeid if it cannot deliver a type_info          
 Throws system_error                                         
 Throws system_error                                         
 Throws system_error                                         
 Throws system_error                                         
 Throws system_error                                         
 Throws system_error                                         
 Throws future_error                                         

参考:The C++ Programming Language, by Bjarne Stroustrup, 4th Edition, 30.4.1 Exceptions

9.8 避免C++函数向C函数抛出异常

在C语言中调用C++函数时,要注意避免C++中的异常向C语言抛出。
如果C++中的异常抛出到C函数中,由于C语言中不支持catch语句,异常无法被处理,这将导致程序异常终止。

测试如下:
有如下C++程序:

// file cpp_func1.cpp
extern "C" int cpp_func1(void);

int cpp_func1(void) {
  // code
  throw 101;
  // code
  return 0;
}

在下面C程序中调用C++实现的函数:

// file main.c
int cpp_func1(void);

int main() {
  cpp_func1();                    /* C++实现的函数cpp_func1会抛出异常 */
  return 0;
}

编译程序测试如下:

$ g++ -c cpp_func1.cpp -o cpp_func1.o
$ gcc -c main.c -o main.o
$ g++ -o main main.o cpp_func1.o
$ ./main
terminate called after throwing an instance of 'int'
Aborted

解决办法——在C++函数内用try/catch捕获并吞下所有异常。如:

extern "C" int cpp_func1(void);

int cpp_func1(void)
try {
    // code
    throw 101;
    // code
    return 0;
} catch (...) {       /* 捕获所有异常 */
    return -1;
}

参考:Binary Hacks黑客秘笈100选,Hack#18 在链接C程序和C++程序时要注意的问题

9.9 Exception safety

There are several levels of exception safety (in decreasing order of safety):

  • No-throw guarantee, also known as failure transparency: Operations are guaranteed to succeed and satisfy all requirements even in exceptional situations. If an exception occurs, it will be handled internally and not observed by clients.
  • Strong exception safety, also known as commit or rollback semantics: Operations can fail, but failed operations are guaranteed to have no side effects, so all data retain their original values.
  • Basic exception safety, also known as a no-leak guarantee: Partial execution of failed operations can cause side effects, but all invariants are preserved and no resources are leaked. Any stored data will contain valid values, even if they differ from what they were before the exception.
  • No exception safety: No guarantees are made.

Usually, at least basic exception safety is required to write robust code.

参考:
https://en.wikipedia.org/wiki/Exception_safety
Exception Safety: Concepts and Techniques http://www.stroustrup.com/except.pdf

9.9.1 实现异常安全之——RAII(Resource Acquisition Is Initialization)

Resource Acquisition Is Initialization is a C++ programming technique which binds the life cycle of a resource (allocated memory, open socket, open file, mutex, database connection - anything that exists in limited supply) to the lifetime of an object with automatic storage duration.

RAII is a strange name for a simple but awesome concept. Better is the name Scope Bound Resource Management (SBRM).

参考:
http://en.cppreference.com/w/cpp/language/raii
http://stackoverflow.com/questions/395123/raii-and-smart-pointers-in-c

9.9.1.1 如何实现RAII

RAII can be summarized as follows:
第一步: encapsulate each resource into a class, where

  • the constructor acquires the resource and establishes all class invariants or throws an exception if that cannot be done
  • the destructor releases the resource and never throws exceptions

第二步: always use the resource via an instance of a RAII-class that either

  • has automatic storage duration
  • is a non-static member of a class whose instance has automatic storage duration

Classes with open()/close(), lock()/unlock(), or init()/copyFrom()/destroy() member functions are typical examples of non-RAII classes.

9.9.1.2 RAII可实现Java中finally语句的功能

C++中没有finally语句,利用RAII我们可以实现同样的目的。

如:正确使用下面的RAII类可保证fclose在异常发生时被调用。

class File_handle {
    FILE* p;
public:
    File_handle(const char* n, const char* a)
        { p = fopen(n,a); if (p==0) throw Open_error(errno); }
    File_handle(FILE* pp)
        { p = pp; if (p==0) throw Open_error(errno); }

    ~File_handle() { fclose(p); }

    operator FILE*() { return p; }

    // ...
};

void f(const char* fn)
{
    File_handle f(fn,"rw");       // open fn for reading and writing
    // use file through f         // 发生异常时,局部对象f会被撤销,它的析构函数会自动调用!
}

参考:http://www.stroustrup.com/bs_faq2.html#finally

9.9.1.3 Limitation of RAII

用RAII技术可以很好地管理stack-allocated objects, 但无法管理dynamically allocated objects.
对于dynamically allocated objects我们可以使用 Smart pointer 来进行管理。

9.9.2 实现异常安全之——Smart pointer

C++98中实现了智能指针:std::auto_ptr

C++11 introduces following smart pointers:
std::shared_ptr
std::weak_ptr
std::unique_ptr

后文将介绍智能指针。

9.10 Tips

9.10.1 什么时候不要用异常

有些时候不应该使用异常,如:

  1. 对实时性要求很高的嵌入式系统中;
  2. 大量使用 naked pointer 这种原始方式来进行资源管理的老系统中。

参考:The C++ Programming Language, by Bjarne Stroustrup, 4th Edition, 13.1.5 When You Can't Use Exceptions

9.10.2 确保程序异常都被处理的

要捕获程序中所有异常,往往写如下代码:

int main()
try {
  // code
}
catch (My_error& me) {     // a My_error (user-defined exception class) happened
  //
}
catch (runtime_error& re) { // a runtine_error happened
  // we can use re.what()
}
catch (exception& e) {      // some standard-library exception happened
  // we can use e.what()
}
catch (...) {               // Some unmentioned exception happened
  //
}

10 Standard Template Library(STL)

The Standard Template Library (STL) consists of the iterator, container, algorithm, and function object parts of the standard library.

10.1 STL containers

The Containers library is a generic collection of class templates and algorithms that allow programmers to easily implement common data structures like queues, lists and stacks. There are three classes of containers – sequence containers, associative containers, and unordered associative containers – each of which is designed to support a different set of operations.

这里有个较好的STL containers总结:http://en.cppreference.com/w/cpp/container

10.1.1 各种Containers总结

可以把Containers分为四大类:Sequence containers/Associative containers/Unordered associative containers/Container adaptors,下面一一介绍它们。

Sequence containers implement data structures which can be accessed sequentially.

Table 8: STL Sequence containers
Sequence Container Comments
array static contiguous array (sine C++11)
vector dynamic contiguous array
deque double-ended queue
forward_list singly-linked list (sine C++11)
list doubly-linked list

Associative containers implement sorted data structures that can be quickly searched (O(log n) complexity).

Table 9: STL Associative containers
Associative Container Comments
set collection of unique keys, sorted by keys
map collection of key-value pairs, sorted by keys, keys are unique
multiset collection of keys, sorted by keys
multimap collection of key-value pairs, sorted by keys

Unordered associative containers implement unsorted (hashed) data structures that can be quickly searched (O(1) amortized, O(n) worst-case complexity).

Table 10: STL Unordered associative containers
Unordered (hashed) Container Comments
unordered_set collection of unique keys, hashed by keys (sine C++11)
unordered_map collection of key-value pairs, hashed by keys, keys are unique (sine C++11)
unordered_multiset collection of keys, hashed by keys (sine C++11)
unordered_multimap collection of key-value pairs, hashed by keys (sine C++11)

Container adaptors provide a different interface for sequential containers.

Table 11: STL Container adaptors
Container adaptors Comments
stack adapts a container to provide stack (LIFO data structure)
queue adapts a container to provide queue (FIFO data structure)
priority_queue adapts a container to provide priority queue

10.1.2 vector简单实现

下面是vector类的一个简单实现。

#include<iostream>

template <class T>
class myVector
{
private:
  T* _data;
  int _capacity;
  int _size;

public:
  myVector() {
    _data = NULL;
    _capacity = _size = 0;
  }

  myVector(int capacity) {
    _data = new T[capacity];
    _capacity = capacity;
    _size = 0;
  }

  T& operator[](int index) {
    return _data[index];
  }

  void push_back(const T & item) {
    if(_size == _capacity) {
      size_t new_capacity = _capacity * 2 + 1;
      T* newData = new T[new_capacity];

      for (size_t i = 0; i < _size; i++) {
        newData[i] = _data[i];
      }
      // 上面for循环可以copy代替: std::copy(_data, _data + _size, newData);
      // 但不要用memcpy代替: memcpy(newData, _data, _size *sizeof(T)); 代替,
      // 因为如果类定义了“赋值操作符”/“复制构造函数”,memcpy并不会调用它们进行相应操作。
      _capacity = new_capacity;
      delete []_data;
      _data = newData;
    }
    _data[_size++] = item;
  }

  int size() { return _size; }

};

class A{
private:
  int _member;
public:
  A() { _member = 0 ; }
  A(int i): _member(i){}
  A& operator=(const A &rhs) {
    _member = rhs._member;
    //std::cout << "Assignment Operator called" << std::endl;
    return *this;
  }
};

int main() {
  myVector<A> v;
  v.push_back(A(10));
  v.push_back(A(20));
  v.push_back(A(30));

  std::cout << "size is " << v.size() << std::endl;   //output: size is 3

  return 0;
}

10.2 STL iterators

Iterators are the glue that ties standard-library algorithms to their data. Conversely, you can say that iterators are the mechanism used to minimize an algorithm’s dependence on the data structures on which it operates:

cxx_stl_iterators.png

Figure 3: STL iterators

参考:http://en.cppreference.com/w/cpp/iterator

10.2.1 iterator什么情况下会失效

迭代器(iterator)是一个可以对其执行类似指针的操作的对象,我们可以将它理解成为一个指针。

下面以vector的插入操作来讨论迭代器失效的问题。
我们知道,vector元素在内存中是顺序存储,如果当前容器中有10个元素,现在又要添加一个元素到容器中,但是内存中紧跟在这10个元素后面没有一个空闲空间,而vector的元素必须顺序存储一边索引访问,所以我们不能在内存中随便找个地方存储这个元素。于是vector必须重新分配存储空间,用来存放原来的元素以及新添加的元素:存放在旧存储空间的元素被复制到新的存储空间里,接着插入新的元素,最后撤销旧的存储空间。这种情况发生,一定会导致vector容器的所有迭代器都失效。

#include<vector>
#include <iostream>
using namespace std;

int main()
{
  vector<int> v;
  v.push_back(1);
  v.push_back(2);

  vector<int>::iterator iter1 = v.begin();
  v.push_back(3);                        // 在vector中插入元素可能导致其重新分配存储空间,从而导致迭代器 iter1 失效!

  for(; iter1 != v.end(); iter1++) {     // Wrong!!! 不能使用iter1了!它可能失效了!
    std::cout << *iter1 << std::endl;
  }

  return 0;
}

一个解决办法是重新获取迭代器,即把for循环那行代码修改为:

  for(iter1 = v.begin(); iter1 != v.end(); iter1++) {

说明:在C++标准(ISO&IEC-14882-2003(E))中有对迭代器失效的说明,但比较分散,没有总结。
这里有一个比较好的总结:Iterator Invalidation Rules: http://kera.name/articles/2011/06/iterator-invalidation-rules/

10.3 STL algorithms

10.4 智能指针(Smart Pointer)

本节主要摘自:C++ Primer Plus第6版,16.2智能指针模板类

10.4.1 为什么需要智能指针

为什么需要智能指针呢?是为了防止内存泄露。考虑下面代码:

void remodel(std::string & str)
{
    std::string * ps = new std::string(str);
    //...
    if (weird_thing())
        throw exception();   // throw前没有写delete ps,可能有内存泄露
    delete ps;               // 如果忘记写delete ps,也会导致内存泄露
    return;
}

上面代码中,如果函数 weird_thing() 返回真,那么会抛出异常,return语句前的delete不会被执行,从而导致内存泄露。

当然,一个直观的解决解法是在抛出异常前增加delete语句:

    if (weird_thing()) {
        delete ps;
        throw exception();
    }

但问题在于我们经常会忘记这么写!不仅如此,return语句前的delete也经常被忘记。这都会导致内存泄露。

如果当函数 remodel() 终止(不管是正常返回,还是出现了异常而终止)时,指针ps指向的内存自动被释放,那该多好啊。

10.4.2 智能指针思想

在前面例子中,如果 ps 有一个析构函数(其功能是释放它指向的内存),我们知道析构函数会在 ps 过期时(局部变量超出作用域时会过期)自动被调用,这样就不用担心内存泄露了。问题在于 ps 只是一个常规指针,不是有析构函数的类对象。 智能指针的基本思想是:智能指针本身是一个对象,这样当智能指针过期时,它的析构函数(功能为删除它所指向的内存)会自动被调用,这样不用担心智能指针所指向的内存会泄露。

10.4.3 智能指针基本使用

C++内置有智能指针的实现。C++98中有auto_ptr(已经过时),C++11中有unique_ptr,shared_ptr和weak_ptr。

智能指针对象在很多方面都类似于普通指针。例如,假设ps是一个智能指针对象,则可以用 *ps 对它执行解除引用操作,用 ps->member 访问结构成员。

下面来介绍前三种智能指针的基本使用。

// smrtptrs.cpp -- using three kinds of smart pointers
// requires support of C++11 shared_ptr and unique_ptr
#include <iostream>
#include <string>
#include <memory>
class Report
{
private:
  std::string str;
public:
  Report(const std::string s) : str(s)
  { std::cout << "Object created!\n"; }
  ~Report() { std::cout << "Object deleted!\n"; }
  void comment() const { std::cout << str << "\n"; }
};

int main()
{
  {
    std::auto_ptr<Report> ps1 (new Report("using auto_ptr"));
    (*ps1).comment();
    ps1->comment();
  }  // 大括号结束了一个作用域,ps1对象会过期,ps1析构函数会被调用(它会删除相应的Report对象)。
  {
    std::shared_ptr<Report> ps2 (new Report("using shared_ptr"));
    ps2->comment();
  }  // 大括号结束了一个作用域,ps3对象会过期,ps2析构函数会被调用(它会删除相应的Report对象)。
  {
    std::unique_ptr<Report> ps3 (new Report("using unique_ptr"));
    ps3->comment();
  }  // 大括号结束了一个作用域,ps3对象会过期,ps3析构函数会被调用(它会删除相应的Report对象)。
  return 0;
}

上面程序将输出:

Object created!
using auto_ptr
using auto_ptr
Object deleted!
Object created!
using shared_ptr
Object deleted!
Object created!
using unique_ptr
Object deleted!

10.4.4 智能指针注意事项(auto_ptr的缺点)

为什么有这么多智能指针(auto_ptr, unique_ptr, shared_ptr等 )呢?为什么auto_ptr会deprecated呢?

先考虑下面的赋值语句:

auto_ptr<string> ps (new string("I reigned lonely as a cloud."));
auto_ptr<string> vocation;
vocation = ps;

上述赋值语句将完成什么工作呢?如果ps和vocation是常规指针,则两个指针将指向同一个string对象。这对智能指针来说是不可接受的,因为程序将试图删除同一个对象两次——一次是ps过期时,另一次是vocation过期时。要避免这种问题,有多种方法:
方法一:定义赋值运算符,使之执行深拷贝。这样两个指针将指向不同的对象,其中的一个对象是另一个对象的副本。
方法二:建立所有权(ownership)概念,对于特定的对象,只能有一个智能指针可拥有它,这样只有拥有对象的智能指针的构造函数会删除该对象。然后, 让赋值操作转让所有权。这就是用于auto_ptr和unique_ptr的策略 ,但unique_ptr的策略更严格。
方法三:创建智能更高的指针,跟踪引用特定对象的智能指针数。这称为引用计数(reference counting)。例如,赋值时,计数将加1,而指针过期时,计数将减1。 仅当最后一个指针过期时,才调用delete。这是shared_ptr采用的策略。
上面考虑的是赋值操作符,显然同样的策略也适用于复制构造函数。

auto_ptr和unique_ptr采用的都是“转让所有权”策略,但auto_ptr已经被deprecated。我们先通过实例来说明auto_ptr存在的问题。

// fowl.cpp -- auto_ptr a poor choice
#include <iostream>
#include <string>
#include <memory>
int main()
{
  using namespace std;
  auto_ptr<string> films[5] =
    {
      auto_ptr<string> (new string("Fowl Balls")),
      auto_ptr<string> (new string("Duck Walks")),
      auto_ptr<string> (new string("Chicken Runs")),
      auto_ptr<string> (new string("Turkey Errors")),
      auto_ptr<string> (new string("Goose Eggs"))
    };
  auto_ptr<string> pwin;
  pwin = films[2];                      // films[2] loses ownership
  cout << "The nominees for best avian baseball film are\n";
  for (int i = 0; i < 5; i++)
    cout << *films[i] << endl;          // 访问 *films[2] 时会 Segmentation fault
  cout << "The winner is " << *pwin << "!\n";
  cin.get();
  return 0;
}

上面程序的输出为:

The nominees for best avian baseball film are
Fowl Balls
Duck Walks
Segmentation fault

导致Segmentation fault的原因是下面语句将所有权从 films[2] 转让给 pwin:

  pwin = films[2];                      // films[2] loses ownership

从此, films[2] 不再引用该字符串,后面用for循环打印它时,发现它是空指针,导致了程序Segmentation fault。

这就是 废弃auto_ptr的主要原因:避免潜在的非法内存访问。

如果把前面程序中的auto_ptr换为shared_ptr,则程序会正常运行。因为shared_ptr仅当引用计数为0时,才真正调用引用对象的析构函数。

我们知道,和auto_ptr一样,unique_ptr采用的也是“转让所有权”策略,把前面例子中的auto_ptr换为unique_ptr会是什么结果呢?答案是:程序无法通过编译,编译器会禁止赋值语句 pwin = films[2]; ,在编译阶段就报错了(这显然比在运行阶段报错要更好),便于你发现。

什么情况下可以对unique_ptr对象赋值呢?请看下节内容。

10.4.5 unique_ptr为何优于auto_ptr

请看下面的语句:

auto_ptr<string> p1(new string("auto");  //#1
auto_ptr<string> p2;                     //#2
p2 = p1;                                 //#3

在语句#3中,p2接管string对象的所有权后,p1的所有权将被剥夺。前面说过,这是件好事,可防止p1和p2的析构函数试图删除同一个对象;但如果程序随后试图使用p1,这将是件坏事,因为p1不再指向有效的数据。
下面来看使用unique_ptr的情况:

unique_ptr<string> p3(new string("auto");  //#4
unique_ptr<string> p4;                     //#5
p4 = p3;                                   //#6

编译器认为语句#6非法,避免了p3不再指向有效数据的问题。因此,unique_ptr比auto_ptr更安全(编译阶段错误比潜在的程序崩溃更安全)。
但有时候,将一个智能指针赋给另一个并不会留下危险的悬挂指针。假设有如下函数定义:

unique_ptr<string> demo(const char * s)
{
  unique_ptr<string> temp(new string(s));
  return temp;
}

并假设编写了如下代码:

unique_ptr<string> ps;
ps = demo("Uniquely special");

demo()返回一个临时unique_ptr,然后ps接管了原本归返回的unique_ptr所有的对象,而返回的unique_ptr被销毁。这没有问题,因为ps拥有了string对象的所有权。但这里的另一个好处是,demo()返回的临时unique_ptr很快被销毁,没有机会使用它来访问无效的数据。换句话说,没有理由禁止这种赋值,神奇的是,编译器确实允许这种赋值!
总之, 程序试图将一个unique_ptr赋值给另一个时,如果源unique_ptr是个临时右值,编译器允许这么做;如果源unique_ptr将存在一段时间,编译器将禁止这么做(这就是unique_ptr比auto_ptr更安全之处)。 如:

using namespace std;
unique_ptr<string> pu1(new string "Hi ho!");
unique_ptr<string> pu2;
pu2 = pu1;                                    //#1 not allowed
unique_ptr<string> pu3;
pu3 = unique_ptr<string>(new string "Yo!");   //#2 allowed

上面的语句#1中,会留下悬挂的智能指针pu1,编译器在编译阶段会报错。语句#2中,不会留下悬挂的智能指针,编译器允许这种赋值。

10.4.5.1 unique_ptr可用于数组

相比于auto_ptr,unique_ptr还有另一个优点,它有一个可用于数组的变体。

std::unique_ptr<double[]> pda (new double[5]);   // 删除时将使用delete[]

说明:auto_ptr和shared_ptr都不能用于数组。

10.4.5.2 用std::move强制转移unique_ptr指向内存的所有权
#include <iostream>
#include <memory>

int main() {
  std::unique_ptr<int> p1(new int(5));
  //std::unique_ptr<int> p2 = p1;            // 编译阶段报错,禁止了p1成为悬挂指针
  std::unique_ptr<int> p3 = std::move(p1);   // 可用std::move强制转移所有权,现在那块内存归p3所有,p1成为了悬挂指针

  std::cout << *p3 <<std::endl;
  // std::cout << *p1 <<std::endl;           // p1是悬挂指针,在对p1重新赋值前,不要访问p1
}

10.4.6 选择智能指针

应使用哪种智能指针呢?
如果程序要使用多个指向同一个对象的指针,应选择shared_ptr。 这样的情况包括:(1) 有一个指针数组,并使用一些辅助指针来标示特定的元素,如最大的元素和最小的元素;(2) 两个对象包含都指向第三个对象的指针;(3) STL容器包含指针。
如果程序不需要多个指向同一个对象的指针,则可使用unique_ptr。

10.4.7 shared_ptr的“循环引用”问题及其解决办法(weak_ptr使用)

前面介绍过,shared_ptr采用“引用计数”策略来处理shared_ptr对象间的赋值问题。赋值时计数加1,智能指针过期时,计数将减1,当最后一个智能指针过期时才调用delete。

但当出现“循环引用”情况时,会出现内存泄露。 “循环引用”是指两个对象互相使用一个shared_ptr成员变量指向对方。循环引用出现的可能场景:如二叉树中父节点与子节点的循环引用,容器与元素之间的循环引用等。

// shared_ptr “循环引用”演示实例
#include <iostream>
#include <memory>
using namespace std;

class parent;
class children;

class parent
{
public:
    shared_ptr<children> children;
    ~parent() { std::cout <<"destroying parent\n"; }
};

class children
{
public:
    shared_ptr<parent> parent;
    ~children() { std::cout <<"destroying children\n"; }
};


void test()
{
    shared_ptr<parent> father(new parent());
    shared_ptr<children> son(new children());

    father->children = son;        // 这一行和下一行导致了“循环引用”
    son->parent = father;
}

int main()
{
    std::cout<<"begin test...\n";
    test();
    std::cout<<"end test.\n";
    return 0;
}

运行上面程序,会输出:

begin test...
end test.

从输出中,可看出parent和children的析构函数在test函数返回后并没有被调用。这就是由于parent对象和children对象互相引用,它们的引用计数都是1,不能自动释放,但此时(退出函数test后),这两个对象再也无法访问到了。这引起了“内存泄漏”。

怎么解决shared_ptr中的“循环引用”问题?可以使用弱引用weak_ptr解决这个问题, 弱引用weak_ptr只引用,不计数。若某块内存被shared_ptr和weak_ptr同时引用,当所有shared_ptr过期之后,不管还有没有weak_ptr引用该内存,内存也会被释放。所以weak_ptr不保证它指向的内存一定是有效的,在使用之前需要检查weak_ptr是否为空指针(使用expired方法)。

10.4.7.1 利用weak_ptr打破循环引用

只要把循环引用的一方使用弱引用,即可解除循环引用。对于上面的例子,只要把children的定义改为如下方式,即可解除循环引用:

class children
{
public:
    weak_ptr<parent> parent;
    ~children() { std::cout <<"destroying children\n"; }
};

最后值得一提的是,虽然通过弱引用指针可以有效的解除循环引用,但这种方式必须在程序员能预见会出现循环引用的情况下才能使用,也可以是说这个仅仅是一种编译期的解决方案,如果程序在运行过程中出现了循环引用,还是会造成内存泄漏的。

例子摘自:http://www.cnblogs.com/TianFang/archive/2008/09/20/1294590.html

10.4.8 智能指针的简单实现

下面给出一个基于引用计数(类似于shared_ptr)的智能指针的简单实现。

// file my_smart_ptr.h

template <typename T>
class my_smart_ptr {
private:
  int count;                               // 保存引用计数
  T *ptr;                                  // 保存底层保管的指针

public:
  my_smart_ptr(T*);                        // 构造函数,普通指针初始化智能指针
  my_smart_ptr(my_smart_ptr &);            // 拷贝构造函数(会对参数进行修改,没有声明为const)

  T* operator->();                         // 重载指针运算符
  T& operator*();                          // 重载解引用运算符
  my_smart_ptr& operator=(my_smart_ptr &); // 重载赋值运算符(会对参数进行修改,没有声明为const)

  ~my_smart_ptr();                         // 析构函数
};

// file my_smart_ptr.cpp
// 构造函数
template <typename T>
my_smart_ptr<T>::my_smart_ptr(T *p): count(1), ptr(p) { }

// 拷贝构造函数
template <typename T>
my_smart_ptr<T>::my_smart_ptr(my_smart_ptr &sp): count(++sp.count), ptr(sp.ptr)  { }

// 重载指针运算符
template <typename T>
T* my_smart_ptr<T>::operator->() {
  return ptr;
}

// 重载解引用运算符
template <typename T>
T& my_smart_ptr<T>::operator*() {
  return *ptr;
}

// 重载赋值运算符:左边的计数减1,右边计数加1,当左边计数为0时,释放内存。
template <typename T>
my_smart_ptr<T>& my_smart_ptr<T>::operator=(my_smart_ptr& sp) {
  ++sp.count;
  if (--count == 0) {         //自我赋值也能保证正确
    delete ptr;
  }
  this->ptr = sp.ptr;
  this->count = sp.count;
  return *this;
}

// 析构函数
template <typename T>
my_smart_ptr<T>::~my_smart_ptr() {
  if (--count == 0) {
    delete ptr;
  }
}

10.4.9 智能指针的简单总结

Table 12: C++智能指针
Name 说明
std::auto_ptr C++98中引入,现在已经Deprecated.
std::unique_ptr C++11中引入,对unique_ptr对象赋值会“转移所有权”
std::shared_ptr C++11中引入,对shared_ptr对象赋值会增加引用计数,有“循环引用”问题
std::weak_ptr C++11中引入,和shared_ptr配合使用,可解决“循环引用”问题

11 其它

11.1 命名空间和using声明

C++中可以用关键字namespace来创建一个命名空间,这样可以把符号限定在指定空间内,以减少命名冲突。
命名空间可以不连续:命名空间是累积的,一个命名空间可以在一个文件中不连续,也可以分散在多个文件中。

命名空间实例:

namespace MyNamespace {
  class MyClass {
  };
}

使用时,要加上命名空间名字(方法一):

MyNamespace::MyClass obj;

也可以用using先声明命名空间中的符号(方法二):

using MyNamespace::MyClass;

MyClass obj;

或者用using namespace引入命名空间的所有符号(方法三):

using namespace MyNamespace;

MyClass obj;

11.1.1 全局命名空间

定义在全局作用域的名字(在任意类、函数或命名空间外部声明的名字)是定义在全局命名空间中的。
全局作用域是隐含的,没有名字,可以用记号::member_name来引用全局命名空间的成员。

#include<iostream>

int a = 1;                         // 这个a定义在全局命名空间中。

int main() {
  int a = 2;
  std::cout << a << std::endl;
  std::cout << ::a << std::endl;   // 可以用 ::a 来引用全局命名空间中的a。
}

11.1.2 嵌套命名空间

命名空间可以嵌套。

#include<iostream>

namespace A {
  namespace B {
    int a = 1;
  }
}

int main() {
  std::cout << A::B::a << std::endl;  // 输出 1
}

11.1.3 未命名的命名空间(可代替C中的static)

命名空间可以是未命名的。
未命名的命名空间与其他命名空间不同,未命名的命名空间的定义局部于当前文件,从不跨越多个程序文件(每个文件都有自己的未命名的命名空间)。
未命名的命名空间中定义的名字只在包含该命名空间的文件中可见,这和C语言中的static声明类似。

#include<iostream>

namespace {       // unnamed namespace
  int a = 1;      // 其它文件中不能引用它,相当于 static int a = 1;
}

// int a = 2;     // ambiguous !!

int main() {
  std::cout << a << std::endl;    // 输出 1
  std::cout << ::a << std::endl;  // 输出 1
}

未命名的命名空间可以嵌套在另一个命名空间内部。如:

#include<iostream>

namespace A {
  namespace {
    int a = 1;
  }
}

int main() {
  std::cout << A::a << std::endl;
}

11.1.4 命名空间别名

可以用命名空间别名为命名空间再取一个名字。

语言如下:

namespace A = ABCDEF;    // 其中命名空间ABCDEF必须已定义。
namespace B = X::Y::Z;   // 用命名空间别名引用嵌套的命名空间。

11.1.5 头文件中不要使用using声明

头文件的内容会被预处理器复制到程序中,如果在头文件中放置using声明,就相当于包含这个头文件的每个程序中都放置了同一using声明,无论该程序是否需要using声明。

头文件通常只应该定义确定必要的东西,所以不要在头文件中使用using声明。 头文件中请直接使用全名,如std::string。

11.2 string类

11.2.1 sring类的Copy-On-Write

C++中string类Copy-On-Write技术的测试。

#include <string>
#include <stdio.h>
using namespace std;

int main(void)
{
    string str1 = "hello world";
    string str2 = str1;

    printf ("Sharing the memory:\n");
    printf ("\tstr1's address: %p\n", str1.c_str() );
    printf ("\tstr2's address: %p\n", str2.c_str() );

    str1[1]='q';  /* modify str1 */
    str2[1]='w';  /* modify str2 */

    printf ("After Copy-On-Write:\n");
    printf ("\tstr1's address: %p\n", str1.c_str() );
    printf ("\tstr2's address: %p\n", str2.c_str() );

    return 0;
}

g++ 4.9.2中测试结果:

Sharing the memory:
	str1's address: 0x13b8028
	str2's address: 0x13b8028
After Copy-On-Write:
	str1's address: 0x13b8058
	str2's address: 0x13b8028

修改str1和str2前,它们的地址是一样的,修改str1和str2后,有一个str1地址变了,str2地址没变。在这个例子中仅修改str1或str2中的一个,也肯定有一个地址会变化。

参考:
http://coolshell.cn/articles/12199.html
http://blog.csdn.net/haoel/article/details/24058

11.2.2 c_str() vs data()

从C++标准上的解释来看,只有一点区别:
c_str()返回的指针保证指向一个size() + 1长的空间,而且最后一个字符肯定 "\0";
而data()返回的指针则保证指向一个size()长度的空间,有没有null-terminate不保证,可能有,可能没有,取决于库的实现。

11.2.3 实现trim()

C++中没有现成的函数对string进行trim操作。

/*
 * Trim string. Both leading and trailing spaces(" \t\n\v\f\r") are removed.
 */
static string& trim(string& str){

    const char* white_space = " \t\n\v\f\r"; //isspace().
    str.erase(0, str.find_first_not_of(white_space));  // leading trim
    str.erase(str.find_last_not_of(white_space) + 1);  // tailing trim
    return str;
}

11.2.4 string类的基本实现

已知类String的原型如下所示,请实现类String的这些基本函数。

class String {
public:
  String(const char *str = "");
  String(const String &other);

  ~String();
  String & operator=(const String &other);

  bool operator==(const String &str);
  friend ostream & operator<<(ostream& o,const String &str);
private:
  char * m_data;
};

其实现和基本测试程序如下:

#include<iostream>
using std::ostream;

// 普通构造函数
String::String(const char *str) {
  if (str == NULL) {
    m_data = new char[1];
    *m_data = '\0';
  } else {
    int len = strlen(str);
    m_data = new char[len + 1];
    strcpy(m_data, str);
  }
}

// 析构函数
String::~String() {
  delete [] m_data;
}

// 拷贝构造函数
String::String(const String &other) {
  int len = strlen(other.m_data);
  m_data = new char[len + 1];
  strcpy(m_data, other.m_data);
}

// 赋值函数
String & String::operator=(const String &other) {
  // 检查自我赋值
  if (this != &other) {
    // 要先分配内存,再释放原有的内存资源
    // 这时基于异常安全的考虑(new失败时以前数据不会被破坏)
    char *temp = new char[strlen(other.m_data) + 1];
    strcpy(temp, other.m_data);

    delete [] m_data;
    m_data = temp;
  }

  return *this;
}

bool String::operator==(const String &str) {
  return strcmp(m_data,str.m_data) == 0;
}

ostream & operator<<(ostream &o,const String &str) {
  o << str.m_data;
  return o;
}

int main()
{
  String s = "cplusplus";
  String s1 = s;
  String s2 = "cplusplus";
  std::cout<<"s1 = "<<s1<< std::endl;
  std::cout<<"s2 = "<<s2<< std::endl;
  std::cout<< std::boolalpha<<(s1 == s2)<< std::endl;
  return 0;
}

12 不常用技术

12.1 嵌套类(Nested class)

可以在另一个类内部定义一个类,这样的类是嵌套类(Nested class),也称为嵌套类型(Nested type)。

嵌套类是独立的类,基本上与它的外围类不相关。嵌套类型的对象不具备外围类所定义的成员,同样,处置类的成员也不具备嵌套类所定义的成员。

12.2 局部类(Local class)

可以在函数体内部定义类,这样的类称为局部类(Local class)。

int a, val;
void foo(int val)
{
   static int si;
   enum Loc { a = 1024, b };
   // Bar is local to foo
   class Bar {               // 类Bar定义在函数foo内部,它是局部类。
   public:
       Loc locVal; // ok: uses local type name
       int barVal;
       void fooBar(Loc l = a)         // ok: default argument is Loc::a
       {
          barVal = val;      // error: val is local to foo
          barVal = ::val;    // ok: uses global object
          barVal = si;       // ok: uses static local object
          locVal = b;        // ok: uses enumerator
       }
   };
   // ...
}

13 C++ Tips

13.2 By Defalut, const Objects Are Local to a File in C++

C++中,在全局作用域声明的const变量是定义该对象的文件的局部变量。此变量只存在于那个文件中,不能被其他文件访问。通过指定const变量为extern,就可以在整个程序中(其它编译单元)中访问const对象。

先看一个普通的例子(不同编译单元中的非const的全局变量):

// file_1.cpp
int bufSize = 1024; //definition

// file_2.cpp
#include <iostream>
extern int bufSize; //declaration
int main()
{
    std::cout << bufSize <<std::endl;
}

上面例子可以成功编译(g++ file_2.cpp file_1.cpp)。

但我们想保护变量bufSize,防止它被修改,在它前面加个const限定符:

// There is linker error when resolve symbol (bufSize) in file_2.cpp
// file_1.cpp
const int bufSize = 1024; //By Defalut, const Objects Are Local to a File

// file_2.cpp
#include <iostream>
extern const int bufSize;
int main()
{
    std::cout << bufSize <<std::endl;
}

编译上面程序时(g++ file_1.cpp file_2.cpp),提示链接错误:
/tmp/ccmJrgV0.o: In function `main':
file_2.cpp:(.text+0x6): undefined reference to `bufSize'
collect2: error: ld returned 1 exit status
这是因为:使用const限制符定义的全局变量,默认只能在其定义的文件(file_1.cpp)中访问。

Resolution:
在定义const全局变量时,加上extern关键字,这样就可以在其它文件(file_2.cpp)中访问它:

// file_1.cpp
extern const int bufSize = 1024;

// file_2.cpp
#include <iostream>
extern const int bufSize;
int main()
{
    std::cout << bufSize <<std::endl;
}

参考:
C++ Primer, 4th, 2.4 const Qualifier
The C++ Programming Language, by Bjarne Stroustrup, 4th Edition, 15.2 Linkage

注意:
规则"By Defalut, const Objects Are Local to a File"仅限于C++,在C语言中不是这样的。 In C, a global const by default has external linkage.

下面两个C源码(和前面例子的C++源码类似),可以成功编译(gcc file_1.c file_2.c)。

// file_1.c
const int bufSize = 1024; //

// file_2.c
#include<stdio.h>

extern const int bufSize;
int main()
{
  printf("%d\n", bufSize);
}

13.3 placement new

Placement new allows you to construct an object on memory that's already allocated. This is useful when building a memory pool, a garbage collector or simply when performance and exception safety are paramount (there's no danger of allocation failure since the memory has already been allocated, and constructing an object on a pre-allocated buffer takes less time):

char *buf  = new char[sizeof(string)]; // pre-allocated buffer
string *p = new (buf) string("hi");    // placement new
string *q = new string("hi");          // ordinary heap allocation

参考:
http://stackoverflow.com/questions/222557/what-uses-are-there-for-placement-new
https://isocpp.org/wiki/faq/dtors#placement-new
http://www.stroustrup.com/bs_faq2.html#placement-delete
http://www.cplusplus.com/reference/new/operator new/

13.4 C和C++的不兼容处

一般地,C和C++是兼容的;但也有很多地方C和C++的行为是不同的。

如:对字符常量用sizeof运算的结果,在C语言中和sizeof(int)相同,而在C++中和sizeof(char)相同。

#include<stdio.h>

int main()
{
  char var1='a';
  printf("%zu\n", sizeof(var1));  // 输出1
  printf("%zu\n", sizeof('a'));   // 用C编译器编器,会输出1;用C++编译器编译,会输出4
}

Bjarne Stroustrup在"The C++ Programming Language, 44.3 C/C++ Compatibility"中列举了很多C和C++不兼容的地方。

13.5 new char(n) 和 new char[n] 的区别

new char(10)new char[10] 有什么区别?
new char(10) 相当于只申请一个字节的空间,它的值为ASCII为10的字符;而 new char[10] 是申请10个字符的空间。

13.6 delete指针后赋值NULL

delete指针后赋值NULL是比较好的编程习惯。

C++标准规定:delete空指针是合法的,没有副作用。
但是,delete p后,p并不会自动被置为NULL

问题来了,对一个非空指针delete后,若没有赋NULL,若被再次delete的话,有可能出现问题。

如下代码:

int *p = new int(3);
delete p;
delete p;

用编译后运行将出现问题。现将其改为:

int *p = new int(3);
delete p;
p = NULL;
delete p;

则不会出现问题(因为delete空指针是合法的),所以,为了避免出现问题,指针被delete之后应该赋值NULL。

总结:delete之后,如果不对原指针赋NULL,它就是个“野指针”(指向一块已被系统回收的内存)。而如果赋了NULL,即使它再多次被delete,也不会导致异常。

13.7 istream::getline vs std::getline

推荐使用std::getline,istream::getline更低级,处理的是naked character
参考:
http://stackoverflow.com/questions/2910836/how-do-i-read-long-lines-from-a-text-file-in-c/2911350#2911350

// istream::getline example
#include <iostream>     // std::cin, std::cout

int main () {
  char name[256], title[256];

  std::cout << "Please, enter your name: ";
  std::cin.getline (name,256);

  std::cout << "Please, enter your favourite movie: ";
  std::cin.getline (title,256);

  std::cout << name << "'s favourite movie is " << title;

  return 0;
}

参考:
http://www.cplusplus.com/reference/istream/istream/getline/

// extract to string
#include <iostream>
#include <string>

main ()
{
  std::string name;

  std::cout << "Please, enter your full name: ";
  std::getline (std::cin,name);
  std::cout << "Hello, " << name << "!\n";

  return 0;
}

参考:
http://www.cplusplus.com/reference/string/string/getline/

13.8 分析CSV文本

用std::getline即可,它的第3个参数可以指定分隔符。

例子:分析CSV文件的一行内容。

#include <iostream>
#include <sstream>
#include <string>

using namespace std;

int main() {

    string line = "a;b;c";
    istringstream iss(line);
    string tok;
    char delim = ';';
    while (getline(iss, tok, delim)) {
        cout << tok <<endl;
    }
}

运行上面程序,将输出:

a
b
c

13.9 为什么C++成员函数指针是16字节

一般地,64位机器上,指针占8个字节。但有个例外:C++成员函数的指针。
如,下面例子中对成员函数的指针求sizeof,结果会是16:

// 请在64位机器上测试
#include <iostream>

void fun1() {}

struct Foo {
    void bar() const { }
};

int main() {
    std::cout << sizeof(&fun1) << std::endl;       // 输出 8 (普通函数指针大小)
    std::cout << sizeof(&Foo::bar) << std::endl;   // 输出 16 (成员函数指针大小)
}

为什么C++成员函数的指针是16字节?请参考:http://www.oschina.net/translate/wide-pointers


Author: cig01

Created: <2011-05-22 Sun 00:00>

Last updated: <2017-12-25 Mon 12:14>

Creator: Emacs 25.3.1 (Org mode 9.1.4)