【C++】第11章 C++高级主题

目录

11.1   强制类型转换

        11.1.1   static_cast

        11.1.2   reinterpret_cast

        11.1.3   const_cast

        11.1.4   dynamic_cast

11.2   运行时类型检查

11.3   智能指针 auto_ptr

        11.3.1   auto_ptr 托管指针

        11.3.2   auto_ptr 的成员函数

        11.3.3   auto_ptr的局限性

11.4   C++异常处理

        11.4.1   什么是 “异常处理” 

        11.4.2   C++异常处理基本语法

        11.4.3   能够捕获任何异常的 catch 语句

        11.4.4   异常的再抛出

        11.4.5   函数的异常声明列表

        11.4.6   C++标准异常类

11.5   名字空间

        11.5.1   什么是 “名字空间”

        11.5.2   让名字空间起到覆盖作用

        11.5.3   单独使用名字空间中的名字

        11.5.4   用名字空间避免重名

        11.5.5   无名名字空间

11.6   C++11 新特性概要

        11.6.1   智能指针 shared_ptr

        11.6.2   无序容器(哈希表)

        11.6.3   正则表达式

        11.6.4   Lambda 表达式

        11.6.5   auto 关键字和 decltype 关键字

        11.6.6   基于范围的 for 循环

        11.6.7   右值引用


11.1   强制类型转换

        用类型名做强制类型转换运算符的做法,,实是 C 语言的老式做法,C++为保持兼容而予以保留。在 C++ 中,引人了 4 种功能不同的强制类型转换运算符以进行强制类型转换:static_cast、reinterpret_cast、const_cast 和 dynamic_cast。

        强制类型转换是有一定风险的,有的转换并不一定安全。例如,把整型数值转换成指针,把基类指针转换成派生类指针,把一种函数指针转换成另一种函数指针,把常量指针转换成非常量指针等,都存在安全隐患。C++ 引入新的强制类型转换机制,主要是为了克服C语言式的强制类型转换的3个缺点:

        (1) 没有从形式上体现不同转换的功能和风险的不同。例如,将 int 强制转换成 double 是没风险的,将常量指针转换成非常量指针、把基类指针转换成派生类指针都是高风险的,而且后两者带来的风险不同(即可能引发不同种类的错误),C语言的强制类型转换形式对这些不同并不加以区分。

        (2) 将多态基类指针转换成派生类指针时,不检查安全性,即无法判断转换后的指针是否确实指向一个派生类对象。

        (3) 难以在程序中寻找到底什么地方进行了强制类型转换。强制类型转换是引发程序运行时错误的一个原因,因此在程序出错时,可能就会想查一下是不是有哪些强制类型转换出了问题。如果采用 C 语言的老式做法,要在程序中找出所有进行了强制类型转换的地方,显然是很麻烦的,因为这些转换没有统一的格式。而用 C++ 的方式,则只需要查找 “_cast” 字符串就可以了。甚至可以根据错误的类型,有针对性地专门查找某一种强制类型转换。例如,怀疑一个错误可能是用了reinterpret_cast 导致的,就可以只查找 “reinterpret_cast” 。

        C++强制类型转换运算符的用法如下:

                强制类型转换运算符<要转换到的类型>(待转换的表达式)

        例如:

                double d = static_cast<double>(3 * 5);        //将3*5的值转换成实数

        下面分别介绍4种强制类转换运算符。

        11.1.1   static_cast

        static_cast 用来进行比较 “自然” 和低风险的转换,如整型和实数型、字符型之间互相转换。另外,如果对象所属的类重载了强制类型转换运算符 T(如T是 int,int* 或别的什么类型名),那么 static_cast 也能用来进行对象到 T 类型的转换。

        static_cast 不能用来在不同类型的指针之间互相转换(基类指针和派生类指针之间的转换可以用它),也不能用于整型和指针之间的互相转换,当然也不能用于不同类型的引用之间的转换。因为这些转换属于风险比较高的。static_cast 用法示例如下:

#include <iostream>
using namespace std;
class A
{
	public:
		operator int() {return 1;}
		operator char* () {return NULL;}
};
int main()
{
	A a;
	int n; char *p = "New Dragon Inn";
	n = static_cast<int>(3.14);		//n的值变为3
	n = static_cast<int>(a);		//调用 a.operator int, n的值变为1
	p = static_cast<char*>(a);		//调用 a.operator char*, p的值变为NULL
	n = static_cast<int>(p);		//编译错误,static_cast不能将指针转换成整型
	p = static_cast<char*>(n);		//编译错误,static_cast 不能将整型转换成指针
	return 0;
}

        11.1.2   reinterpret_cast

        reinterpret_cast 用来进行各种不同类型的指针之间的转换、不同类型的引用之间的转换,以及指针和能容纳得下指针的整数类型之间的转换。转换的时候,执行的是逐个比特复制的操作。这种转换提供了很强的灵活性,但转换的安全性只能由程序员自己的细心来保证了。例如,程序员一定要把一个 int* 指针、函数指针或随便什么其他类型的指针转换成 string* 类型的指针,那也是可以的,至于以后用转换后的指针调用string类的成员函数,结果引发出错,那程序员只能自认倒霉了(C++ 标准不允许将函数指针转换成对象指针,但有些编译器,如 Visual Studio 2010,支持这种转换)。reinterpret_cast 用法示例如下:

#include <iostream>
using namespace std;
class A
{
  public:
	 int i;	
	 int j;
	 A(int n):i(n),j(n) { }
};
int main()
{
	  A a(100);
	  int &r = reinterpret_cast<int&>(a); //强行让 r 引用 a
	  r = 200;  //把 a.i 变成了 200
	  cout << a.i << "," << a.j << endl;  // 输出 200,100
	  int n = 300;
	  A *pa = reinterpret_cast<A*> ( & n); //强行让 pa 指向 n
	  pa->i = 400;	 // n 变成 400
 	  pa->j = 500;  //此条语句不安全,很可能导致程序崩溃
	  cout << n << endl;  // 输出 400
	  long long la = 0x12345678abcdLL;
	  pa = reinterpret_cast<A*>(la); //la太长,只取低32位0x5678abcd拷贝给pa
	  unsigned int u = reinterpret_cast<unsigned int>(pa);//pa逐个比特拷贝到u
	  cout << hex << u << endl;  //输出 5678abcd
	  typedef void (* PF1) (int);
	  typedef int (* PF2) (int,char *);
	  PF1 pf1;	  PF2 pf2;
	  pf2 = reinterpret_cast<PF2>(pf1); //两个不同类型的函数指针之间可以互相转换
}

        程序的输出结果:

                200,100
                400
                5678abcd

        第 19 行不安全,是因为在编译器看来,pa->j 的存放位置就是 n 后面的 4 个字节。本条语句会往这 4 个字节中写入 500。但我们不知道这 4 个字节是用来存放什么的,贸然往里面写人可能会导致程序错误甚至崩溃。当然运气好的话也可能什么事都没有。

        上面程序中的各种转换都是莫名其妙的,只是为了演示一下 reinterpret_cast 的能力而已。在编写黑客,病毒或反病毒等怪异程序时,也许会用到那样怪异的转换。reinterpret_cast 体现了 C++ 语言的设计思想是你爱干啥都行,但要为自己的行为负责。

        11.1.3   const_cast

        此运算符仅用来进行去除 const 属性的转换,它也是 4 个强制类型转换运算符中唯一能够去除 const 属性的。它用于将 const 引用转换成同类型的非 const 引用,将 const 指针转换成同类型的非 const 指针。例如:

                const string s= "Inception" ;
                string& p = const_cast<string&>(s);
                string* ps = const_cast<string*>(&s);        // &s 的类型是 const string*

        11.1.4   dynamic_cast

        用 reinterpret_cast 可以将多态基类(包含虚函数的基类)的指针强制转换为派生类的指针,但是这种转换不检查安全性,即不检查转换后的指针是否确实指向一个派生类对象 dynamic_cast 专门用于将多态基类的指针或引用强制转换为派生类的指针或引用,而且能够检查转换的安全性。对于不安全的指针转换,转换结果返回 NULL 指针。

        dynamic_cast 是通过 “运行时类型检查” 来保证安全性的。dynamic_cast 不能用于将非多态基类的指针或引用强制转换为派生类的指针或引用,这种转换没法保证安全性,只好用 reinterpret_cast 来完成。dynamic_cast 示例程序如下:

#include <iostream>
#include <string>
using namespace std;
class Base
{//有虚函数,因此是多态基类
	public:
	virtual ~Base() {}
};
class Derived:public Base { };
int main()
{
	Base b;
	Derived d;
	Derived* pd;
	pd = reinterpret_cast<Derived*> (&b);
	if(pd == NULL)
		//此处 pd 不会为 NULL, reinterpret_cast 不检查安全性,总是进行转换
		cout<< "unsafe reinterpret_cast" << endl;	//不会执行
	pd = dynamic_cast<Derived*> (&b);
	if(pd == NULL)//结果会是 NULL, 因为 &b 不是指向派生类对象, 此转换不安全
		cout << "unsafe dynamic_cast1" << endl;		//会执行
	pd = dynamic_cast<Derived*> (&d);				//安全的转换
	if(pd == NULL)									//此处pd不会为NLL
		cout << "unsafe dynamic_cast2" << endl;		//不会执行
	return 0;
}

        程序的输出结果:

                unsafe dynamic_cast1

        第 20 行,通过判断 pd 的值是否为 NULL,就能知道第 19 行进行的转换是否是安全的。第 23 行同理。

        如果上面的程序中出现了下面的语句:

                Derived & r = dynamic_cast<Derived&> (b);

        那该如何判断该转换是否安全呢?不存在空引用,因此不能通过返回值来判断转换是否安全。C++ 的解决办法是 dynamic_cast 在进行引用的强制转换时,如果发现转换不安全,就会抛出一个异常,通过处理异常,就能发现不安全的转换。在 11.2.6 节 “C++异常处理” 中的 “C++标准异常类” 中会讲到这一点。

11.2   运行时类型检查

        C++ 运算符 typeid 是单目运算符,可以在程序运行过程中获取一个表达式的值的类型。typeid 运算的返回值是一个 type_info 类的对象,里面包含了类型的信息。type_info 类在头文件  typeinfo 中定义的,一个 type_info 对象可以代表一种类型,它有成员函数 const char* name() const,可以返回 type_info 对象所代表的类的名字。

        两个 type_info 对象可以用 “==” 和 “!=” 进行比较。如果它们代表的类型相同,那么就算相等。

        下面是 typeid 和 type_info 的用法示例,输出来自用 Visual Studio 2010 编译的程序。用 Dev C++ 编译的程序,输出结果不同。

#include <iostream>
#include <typeinfo>	//要使用typeinfo,需要此头文件
using namespace std;
struct Base {};	//非多态基类
struct Derived:Base {};
struct Poly_Base {virtual void Func() {} };//多态基类
struct Poly_Derived:Poly_Base {};
int main()
{
	//基本类型
	long i; int* p = NULL;
	cout << "1) int is:" << typeid(int).name() << endl; 		//输出1) int is:int
	cout << "2) i is:" << typeid(i).name() << endl;				//输出2) i is:long
	cout << "3) p is:" << typeid(p).name() << endl;				//输出3) p is:int*
	cout << "4) *p is:" << typeid(*p).name() << endl ;			//输出4) *p is:int
	//非多态类型
	Derived derived;
	Base* pbase = &derived;
	cout << "5) derived is:" << typeid(derived).name() << endl;
		//输出5) derived is:struct Derived
	cout << "6) *pbase is:" << typeid(*pbase).name() << endl;
		//输出6) *pbase is:struct Base
	cout << "7) " << (typeid(derived) == typeid(*pbase)) << endl;//输出7)0
	//多态类型
	Poly_Derived polyderived;
	Poly_Base *ppolybase = &polyderived;
	cout << "8) polyderived is:" << typeid(polyderived).name() << endl;
		//输出8)polyderived is:struct Poly_Derived
	cout << "9) *ppolybase is:" << typeid(*ppolybase).name() << endl;
		//输出9) *ppolybase is:struct Poly_Derived
	cout<< "10) " << (typeid(polyderived) != typeid(*ppolybase)) << endl;//输出10)0
}

        第 21 行尽管 pbase 指向的是一个 Derived 对象,但是 typeid 运算无从知道这一点,因此实际上还是根据 “*pbase” 这个参数,在编译时就确定了其类型应该是 Base。

        第 29 行情况就不同了,因为 Poly_Base 是多态类,该类的对象以及该类的派生类的对象内部,前 4 个字节都是虚函数表的地址。不同的类的虚函数表不同,由虚函数表能够判断出对象是属于哪个类的。所以此处的 typeid 运算才是真正的运行时类型识别,其原理和多态实现的原理是类似的。

11.3   智能指针 auto_ptr

        要确保用 new 动态分配的内存空间在程序的各条执行路径都能被释放,是一件麻烦的事情。C++ 模板库中 memory 头文件中定义的智能指针,即 auto_ptr 模板,就是用来部分解决这个问题的。只要将 new 运算符返回的指针 p 交给一个 auto_ptr 对象 “托管”,就不必操心在哪里要写 “delete p” 了。实际上根本不需要写,托管 p 的 auto_ptr 对象在消亡的时候会自动执行 “delete p”。而且,该 auto_ptr 对象能像指针 p 一样使用,即假设托管 p 的 auto_ptr 对象称为 ptr,那么 *ptr 就是 p指向的对象。

        11.3.1   auto_ptr 托管指针

        通过 auto_ptr 的构造函数,可以让 auto_ptr 对象托管一个 new 运算符返回的指针,写法如下:
                auto_ptr<T> ptr(new T);        //T可以是 int,char,类型名等各种类型

        此后 ptr 就可以像 T* 类型的指针一样来使用,即 *ptr 就是用 new 动态分配的那个对象。请看下面的程序:

#include <iostream>
#include <memory>
using namespace std;
class A
{
	public:
		int i;
		A(int n):i(n) {};
		~A() {cout<<i<<" "<<"destructed"<<endl;}
};
int main()
{
	auto_ptr<A> ptr(new A(2));	// new出来的动态对象的指针,交给ptr托管
	cout<< ptr->i << endl;		//输出2
	ptr->i = 100;				//动态对象的i成员变量变为100
	A a(*ptr);					//*ptr就是前面new的动态对象
	cout<< a.i << endl;			//输出100
	a.i =20;
	return 0;
}

        程序的输出结果:

                2
                100
                20 destructed
                100 destructed

        第 3 行输出是因为局部变量 a 消亡,引发析构函数调用。第 4 行输出说明用 new 创建的动态对象被析构了。程序中没有编写 delete 语句,托管动态对象指针的 ptr 对象,消亡时自动 delete 了其托管的指针。

        只有指向动态分配的对象的指针,才能交给 auto_ptr 对象托管。将指向普通局部变量、全局变量的指针交给 auto_ptr 托管,编译不会有问题,但程序运行时会出错。因为不能 delete 一个并没有指向动态分配的内存空间的指针。

        用一个 auto_ptr 对象 p1 对另一个同类型的 auto_ptr 对象 p2 进行赋值或初始化,会导致 p1 托管的指针变成由 p2 托管,p1 不再托管任何指针(此时访问 *p1,就会导致程序出错)。

        11.3.2   auto_ptr 的成员函数

        类 auto_ptr<T> 有以下常用成员函数:

                r*release( ) ;

        解除对指针的托管,并返回该指针。解除对指针的托管并不会 delete 该指针。

                viod reset(T *p NULL);

        delete 原来托管的指针,托管新指针 p。若 p 为NULL,则执行后变成没有托管任何指针。

                *get() const;

        返回托管的指针。

        请再看一个 auto_ptr 的示例程序:

#include <iostream>
#include <memory>
using namespace std;
class A
{
	public:
		int i;
		A(int n):i(n){};
		A() {cout << i << " " << "destructed" << endl;}
};
int main()
{
	auto_ptr<A> ptr1(new A(2));	//A(2)由 ptr1托管
	auto_ptr<A> ptr2(ptr1);		//A(2)交由 ptr2托管,ptr1什么都不托管
	auto_ptr<A> ptr3;
	ptr3 = ptr2;				//A(2)交由ptr3托管,ptr2什么都不托管
	cout << ptr3->i << endl;		//输出2
	A* p = ptr3.release();		//p指向A(2),ptr3解除对A(2)托管
	ptr1.reset(p);				//ptr1重新托管A(2)
	cout << ptr1->i << endl;	//输出2
	ptr1.reset(new A(3));		//delete A(2),托管A(3),输出2 destructed
	cout<< "end" << endl;
	return O;					//程序结束,ptr1消亡时,会delete掉A(3)
}

        程序的输出结果:

                2
                2
                2 destructedend
                3 destructed

        11.3.3   auto_ptr的局限性

        auto_ptr 对象不能托管指向动态分配的数组的指针,这样的托管是没有效果的,如:

                auto_ptr<int> ptr(new int[200]);

        ptr 不能起到自动释放整个动态数组的功能。

        另外,auto_ptr 的复制构造函数会改变被复制的 auto_ptr 对象(即调用复制构造函数时的实参),所以不要把 auto_ptr 对象放到 STL 的容器中去。

11.4   C++异常处理

        11.4.1   什么是 “异常处理” 

        程序运行时常会碰到一些异常情况,例如,做除法的时候除数为 0;用户输入年龄的时候输入了一个负数;用 new 运算符动态分配空间的时候空间不够了导致无法分配;访问数组元素的时候下标越界了;要打开文件读取的时候文件却不存在,等等。对这些异常情况如果不能发现并加以处理,很可能会导致程序崩溃。所谓 “处理” 可以是给出错误提示信息然后让程序沿一条不会出错的路径继续执行;也可能是不得不结束程序,但在结束前做些必要的工作,如将内存中的数据写入文件、关闭打开的文件、释放动态分配的内存空间等。

        如果一旦发现异常情况就立即处理,未必妥当,因为在一个函数执行的过程中发生的异常,有的情况下由该函数的调用者来决定如何处理更合适。尤其像库函数这样提供给程序员调用,用以完成与具体应用无关的通用功能的函数,执行过程中对异常贸然进行处理,未必会符合调用它的程序的需要。此外,将异常分散在各处进行处理不利于代码的维护,尤其是对不同地方发生的同一种异常,都要编写相同的处理代码,也是一种不必要的重复和冗余。如果能在发生各种异常时,让程序都走到同一个地方,这个地方能够对异常进行集中处理,则程序就会容易编写、容易维护得多。
鉴于上述原因,C++ 引入了 “异常处理” 的机制。其基本思想就是:函数 A 在执行过程中发现异常时,可以不加处理,而只是 “抛出一个异常” 给 A 的调用者,假定称为函数 B。抛出异常而不加处理会导致函数 A 立即中止,此种情况下,函数 B 可以选择捕获 A 抛出的异常进行处理,也可以选择置之不理。如果置之不理,这个异常就会被抛给 B 的调用者,如果一层层的函数都不处理异常,最终异常会被抛给最外层的 main 函数。main 函数应该处理异常。如果 main 函数也不处理异常,那么程序就会立即异常地中止。

        11.4.2   C++异常处理基本语法

        C++ 通过 throw 语句和 try…catch 语句实现对异常的处理。throw 语句语法如下:

                throw 表达式;

        该语句抛出一个异常。异常是一个表达式,其值的类型可以是基本类型,也可以是类。

        try...catch 语句语法如下:

                try {
                        语句组
                }
                catch(异常类型) {
                        异常处理代码
                }
                …
                catch(异常类型) {
                        异常处理代码

                }

        catch 可以有多个,至少一个。

        不妨把 try 和其后的一对 “(}” 中的内容称为 “try 块”,把 catch 和其后的 “{}” 中的内容称为 “catch 块”。try…catch 语句执行的过程是:执行 try 块里面的语句,如果执行的过程中没有异常抛出,那么执行完后就执行最后一个 catch 块后面的语句,所有 catch 块里面的语句都不会被执行。如果 try 块执行的过程中抛出了异常,那么抛出异常后立即跳转到第一个 “异常类型” 和抛出的异常类型匹配的 catch 块里面去执行(称为异常被该 catch 块 “捕获”),执行完后再跳到最后一个catch 块后面继续执行。例如下面的程序段:

#include <iostream>
using namespace std;
int main()
{
	double m ,n;
	cin >> m >> n;
	try {
		cout << "before dividing." << endl;
		if( n == 0)
			throw -1; //抛出int类型异常
		else
			cout << m / n << endl;
		cout << "after dividing." << endl;
	}
	catch(double d) {
		cout << "catch(double) " << d <<  endl;
	}
	catch(int e) {
		cout << "catch(int) " << e << endl;
	}
	cout << "finished" << endl;
	return 0;
}

        程序运行结果如下:

                9 6(回车)
                before dividing.
                1.5
                after dividing.
                finished

        说明 n 不为 0 时, try 块中不会抛出异常。所以程序在 try 块正常执行完后,越过所有的catch 块继续执行,catch 块中的语句一个也不会执行。

        程序运行结果也可以是:

                9 0(回车)
                before dividing.
                catch(int) -1
                finished

        当 n 为 0 时,try 块中会抛出一个整型异常。抛出异常后,try 块立即停止执行。该整型异常会被类型匹配的第一个 catch 块捕获,即进入 “catch(int e)" 块执行,该 catch 块执行完毕后,程序继续往后执行,直到正常结束。

        如果抛出的异常没有被 catch 块捕获,例如,将 “catch(int e)” 改为 “catch(char e)” 的话,输入的 n 为 0 时,抛出的整型异常就没有被 catch 块捕获,这个异常也就得不到处理,那么程序就会立即中止,try…catch 后面的内容都不会被执行。

        11.4.3   能够捕获任何异常的 catch 语句

        如果希望不论抛出哪种类型的异常都能捕获,可以编写如下 catch 块:

                catch(...) {
                        ...
                }
        这样的 catch 块能够捕获任何还没有被捕获的异常。例如下面的程序段:

#include <iostream>
using namespace std;
int main()
{
	double m, n;
	cin >> m >> n;
	try {
		cout << "before dividing." << endl;
		if(n == 0)
			throw -1;	//抛出整型异常
		else if(m == 0)
			throw -1.0;	//抛出double型异常
		else
			cout << m/n << endl;
		cout << "after dividing." << endl;
	}
	catch(double d) {
	cout << "catch(double)" << d <<endl;
	}
	catch(...) {
		cout << "catch(...)" << endl;
	}
	cout << "finished" << endl;
	return 0;
}

        程序运行结果如下:

                9 0(回车)
                before dividing.
                catch( ...)
                finished

        n 为 0 时,抛出的整型异常被 “catch(...)” 捕获。

        程序的运行结果也可以是:

                0 6(回车)
                before dividing.
                catch(double) -1
                finished

        当 m 为 0 时,抛出一个 double 类型的异常。虽然 “catch(double)” 和 “catch(...)” 都能匹配该异常,但是 “catch(double)” 是第一个能匹配上的 catch 块,所以会执行它,而不会执行 “catch(...)” 这个块。

        由于 “catch(...)” 能匹配任何类型的异常,所以它后面的 catch 块实际上就不起作用了所以不要将它编写在别的catch块前面。

        11.4.4   异常的再抛出

        如果一个函数在执行的过程中,抛出的异常在本函数内就被 catch 块捕获并处理了,那么该异常就不会抛给这个函数的调用者(也称 “上一层的函数”);如果异常在本函数中没被处理,就会被抛给上一层的函数。请看下面的程序示例:

#include <iostream>
#include <string>
using namespace std;
class CException
{
	public:
		string msg;
		CException(string s):msg(s) {}
};
double Devide(double x, double y)
{
	if(y == 0)
		throw CException("devided by zero");
	cout << "in Devide" << endl;
	return x/y;
}
int CountTax(int salary)
{
	try {
	if(salary < 0)
		throw -1;
	cout << "counting tax" << endl;
	}
	catch(int) {
		cout << "salary<0" << endl;
	}
	cout << "tax counted" << endl;
	return salary*0.15;
}
int main()
{
	double f = 1.2;
	try {
		CountTax(-1);
		f = Devide(3,0);
		cout << "end of try block" << endl;
	}
	catch(CException e) {
		cout << e.msg << endl;
	}
	cout << "f=" << f << endl;
	cout << "finished" << endl ;
	return 0;
}

        程序的输出结果:

                salary<0
                tax counted
                devided by zero
                f=1.2
                finished

        CountTax 函数抛出的异常自己处理了,这个异常就不会继续被抛给调用者,即 main。所以 main 的 try 块中,CountTax 之后的语句还能正常执行,即会执行 “f=Devide(3,0);”。

        第 35 行,Devide 函数抛出了异常,自己却不处理,该异常就会被抛给 Devide 的调用者,即 main。抛出此异常后, Devide 函数立即结束,第 14 行不会被执行,函数也不会返回一个值,这从第 35 行中f的值不会被修改可以看出。

        Devide 中抛出的异常被 main 中的类型匹配的 catch 块捕获。第 38 行中的 e 对象是用复制构造函数初始化的。

        如果抛出的异常是派生类的对象,而 catch 块的异常类型是基类,那么这两者也是能匹配的。因为派生类对象也是基类对象。

        虽然函数也可以通过返回值或者传引用的参数通知调用者发生了异常,但采用这种方式的话,每次调用该函数都要去判断是否发生异常,在多处调用该函数的时候比较麻烦。有了异常处理机制,可以将这多处调用都写在一个 try 块里面,任何一处调用发生异常都会被匹配的 catch 块捕获并处理,就不需要每次调用完都判断是否发生异常了。

        有时,虽然在函数中对异常进行了处理,但是还是希望能够通知调用者,以便让调用者知道发生了异常,可以做进一步的处理。在 catch 块中抛出异常可以满足这种需要。例如:

#include <iostream>
#include <string>
using namespace std;
int CountTax(int salary)
{
	try {
		if( salary < 0 )
			throw string("zero salary");
		cout << "counting tax" << endl;
		
	}
	catch (string s ) {
		cout << "CountTax error : " << s << endl;
		throw; //继续抛出捕获的异常
	}
	cout << "tax counted" << endl;
	return salary * 0.15;
}
int main()
{
	double f = 1.2;
	try {
		CountTax(-1);
		cout << "end of try block" << endl;
	}
	catch(string s) {
		cout << s << endl;
	}
	cout << "finished" << endl;
	return 0;
}

        程序的输出结果:

                CountTax error:zero salary
                zero salary
                finished

        第 14 行 “throw;” 没有指明抛出什么样的异常,那么抛出的就是 catch 块捕获到的异常,即  string("zero salary")。所以这个异常会被 main 中的 catch 块捕获。

        11.4.5   函数的异常声明列表

        为了增强程序的可读性和可维护性,让程序员在使用一个函数时一眼就能看出这个函数可能会抛出什么异常,C++ 允许在函数声明和定义时加上它所能抛出的异常的列表。具体写法如下:

                void func() throw(int, double, A, B, C);

或者:

                void func() throw( int , double ,A,B,C) {…}

        上面的写法表明 func 可能抛出 int 类型,double 类型,以及 A、B、C 这 3 种类型的异常。异常声明列表可以在函数声明时编写,也可以在函数定义时编写。如果两处都编写,则两处应一致。

        如果异常声明列表编写为:

                void func() throw();

则说明 func 函数不会抛出任何异常。

        一个函数如果不交代能抛出哪些类型的异常,就可以抛出任何类型的异常。

        函数如果抛出了其异常声明列表中没有的异常,在编译时不会引发错误。在运行时,Dev C++ 编译出来的程序会出错。用 Visual Studio 2010 编译出来的程序则不会出错,异常声明列表实际上不起作用。

        11.4.6   C++标准异常类

        C++ 标准库中有一些类代表异常,这些类都是从 exception 类派生而来。常用的几个异常类如图 11.1 所示:

        bad_typeid、bad_cast、bad_alloc、ios_ base:: failure、out_of_range 都是 exception 类的派生类。C++ 程序在碰到某些异常时,即使程序中没有编写 throw 语句,也会自动抛出上述异常类的对象。这些异常类还都有名为 “what” 的成员函数,返回字符串形式的异常描述信息。使用这些异常类,需要包含头文件 stdexcept。下面分别介绍几个异常类,本节程序的输出以 Visual Studio 2010 为准,Dev C++ 编译的程序输出有所不同。

        1. bad_typeid

        使用 typeid 运算符时,如果其操作数是一个多态类的指针,而该指针值为NULL,则会抛出此异常。

        2. bad_cast
在用dynamic_cast进行从多态基类对象(或引用)到派生类的引用的强制类型转换时,如果转换是不安全的,则会抛出此异常。程序示例如下:

#include <iostream>
#include <stdexcept>
using namespace std;
class Base
{
	virtual void func() {}
};
class Derived:public Base
{
	public:
		void Print() {}
};
void PrintObj(Base & b)
{
	try {
		Derived & rd = dynanic_cast<Derived&> (b);
		//此转换若不安全,会抛出bad_cast异常
		rd.Print();
	}
	catch(bad_cast & e) {
		cerr << e.what() << endl;
	}
}
int main ()
{
	Base b;
	PrintObj(b);
	return 0;
}

        程序的输出结果:

                Bad dynamic_cast!

        在 PrintObj 函数中,通过 dynamic_cast 检测 b 是否引用的是一个 Derived 对象,如果是,就调用其 Print 成员函数,如果不是,就抛出异常,不会调用 Derived::Print。

        3. bad_alloc

        在用 new 运算符进行动态内存分配时,如果没有足够的内存,则会引发此异常。程序示例如下:

#include <iostream>
#include <stdexcept>
using namespace std;
int main()
{
	try {
	char* p = new char[0x7fffffff];	//无法分配这么多空间,会抛出异常
	}
	catch(bad_alloc & e) {
		cerr << e.what() << endl;
	}
	return 0;
}

        程序的输出结果:

                bad allocation
                ios_base::failure

        在默认的状态下,输人输出流对象不会抛出此异常。如果用流对象的 exceptions 成员函数设置了一些标志位,则在打开文件出错,读到了输入流的文件尾时,会抛出此异常。此处不再赘述。

        4. out_of_range

        用 vector 或 string 的 at 成员函数根据下标访问元素时,如果下标越界,就会抛出此异常。例如:

#include <iostream>
#include <stdexcept>
#include <vector>
#include <string>
using namespace std;
int main(
{
	vector<int> v(10);
	try {
	v.at(100) = 100;	//抛出out_of_range异常
	}
	catch(out_of_range & e){
		cerr << e.what() << endl;
	}
	string s = "hello";
	try {
		char c = s.at(100);	//抛出out_of_range异常
	}
	catch(out_of_range & e){
		cerr << e.what() << endl;
	}
	return 0;
}

        程序的输出结果:

                invalid vector<T>subscript
                invalid string position

        如果将 “v. at(100)” 换成 “v[100]”,将 “s. at(100)” 换成 “s[100]”,程序就不会引发异常了(但可能导致程序崩溃)。因为 at 成员函数中会检测下标越界并抛出异常,但是 operator[ ] 则不会。operator[ ] 相比 at 的好处就是不用判断越界,所以执行速度更快。

11.5   名字空间

        11.5.1   什么是 “名字空间”

        在多个程序员合作一个大型的 C++ 程序时,一个程序员起的某个全局变量名、类名,有可能和其他程序员起的名字重名。编写大型程序可能需要使用多个其他公司开发的类库或函数库,如果这些类库和函数库设计的时候都不考虑重名问题,那么同时使用两个不同的类库或函数库产品时,就会碰到无法解决的重名错误。重名主要有以下情况:

        (1) 全局变量名重名。

        (2) 全局函数重名,而且参数表还相同。

        (3) 自定义类型名重名,包括结构名、联合名、枚举名、typedef 的类型名以及类名的重名。

        (4) 模板名重名。

        用 C++ 编程应避免使用全局变量。全局变量都可以用类的静态成员变量替代,这样做就不存在全局变量重名的问题了。

        要解决后面 3 种重名的情况,可以使用 “名字空间”(namespace)的机制。下面所提到的 “名字”,指的就是自定义类型名、模板名或全局函数名。

        C++ 程序中的每个名字都是属于一个名字空间的。如果定义该名字的时候没有指定它属于哪个名字空间,那么它就属于 “全局名字空间”。本书前面的程序中,所有自己定义的名字都没有指定名字空间,因此都是属于全局名字空间。注意:整个程序只有一个全局名字空间,而不是每个 .cpp 文件都有各自的全局名字空间。

        程序员可以用 “namespace” 关键字来自己定义名字空间,写法如下:

                namespace 名字空间名
                {
                        程序片段
                }

        上述的结构称为一个 “namespace块”。namespace 块中可以包含各种名字的定义。

        注意:和类定义不同, namespace 块最后的 “}” 后面不要分号 “;”。这里所说的 “程序片段” 几乎可以包含任何东西,如多个类、多个全局函数的函数体、完整的模板、全局变量等。如果某个名字是在上面这样的 namespace 块中定义的,就称这个名字属于该名字空间。

        名字空间的定义可以不是连续的,即一个文件中可以有多个名字空间名相同的 namespace 块,每个块中的程序片段不一样。例如,一个 .cpp 文件或 . h 文件中内容可以如下:

                namespace group1
                {
                        class A { };
                }
                …
                namespace group1
                {
                        class B{};
                }

        那么,A、B都属于名字空间 group1。

        不妨将 “使用一个已经定义好的名字” 称为对该名字的引用。例如,调用一个函数、实例化一个模板或用类定义一个对象等,都算是对函数的名字、模板的名字或类的名字的引用。编译器碰到一个名字的引用时,如果这个名字没有被指明属于哪个名字空间,编译器就会在覆盖当前语句的名字空间中去寻找这个名字。全局名字空间总是覆盖所有的语句,而程序员自己定义的名字空间,在某些条件下会覆盖某些语句。下面看一个名字空间的简单例子:

namespace graphics
{

	class A{};		//A属于名字空间graphics
}
int main()
{
	A a;			//编译出错,A没有定义
	graphics::A b;	//OK,指明了A所属的名字空间
	return 0;
}

        在上面的程序中,类名 A 定义是在 graphics 名字空间中定义的,所以它属于名字空间 graphics,不属于全局名字空间。编译到第 7 行时,虽然前面定义了 graphics 名字空间,但是本行并没有被 graphics 名字空间覆盖,因此编译器只能在全局名字空间中寻找名字A,结果当然是找不到,于是报 “A 没有定义” 的错误。

        用 “名字空间名名字" 格式可以指明一个名字所属的名字空间。正如第 8 行,“graphics:A” 就指明了 A 是属于名字空间 graphics 的。虽然 graphics 名字空间还没有覆盖本行,但是编译器还是能据此在 graphics 名字空间中找到 A 的定义,因此编译没有问题。

        11.5.2   让名字空间起到覆盖作用

        使用下面的语句,可以使得一个名字空间起到覆盖作用:

                using namespace 名字空间名;

        这条语句能起作用的范围和标识符的作用域有些相似。如果它出现在所有的函数外面,那么它会使得名字空间覆盖其后的所有语句,即起作用的范围相当于全局变量的作用域;如果它出现在个函数内部,那么会使得名字空间覆盖从它开始直到包含它的最内层的 “{ }” 的右花括号 “}” 为止,这和局部变量的作用域是一致的。

        例如,前面看到了无数次的程序开头的 “using namespace std;”,这条语句就使得名字空间 “std” 覆盖了其后所有语句。std 是 C++ 标准模板库的名字空间,C++ 标准模板库中的标识符,如cin,cout,string,istream,vector,find 等,都是在该名字空间中定义的,都属于该名字空间。 “using namespace std;” 使得其后所有的语句都被 std 覆盖,因此程序中若是出现了 cin,cout,istream,vector 等名字,编译器就能在 std 名字空间中找到这些名字的定义。

        一条语句或一段程序可以同时被多个名字空间覆盖,编译器会在覆盖它的多个名字空间中寻找它所引用的名字的定义,如果都找不到,就会报错。请看下面的示例程序:

#include <iostream>
using namespace std;
namespace graphics
{
	class A{};
}
using namespace graphics;		//graphics会覆盖后面的内容
int main()
{
	A a;						//编译没问题,graphics已覆盖此处
	return 0;
}

        第 7 行使得名字空间 graphics 覆盖了其后所有的内容,编译到第 10 行时,编译器会到 graphics 名字空间中找到 A 的定义,因此没有问题。

        编译器在编译某条语句时,如果该语句引用的名字在同时覆盖该语句的不止一个名字空间中出现,那么编译器不知道应该用哪个名字空间的名字,因而会报二义性的错误(同名而参数表不同的函数不属于这种情况)。不过这一条也有例外。请看例程:

#include <iostream>
using namespace std;
class A {};
class B {};
namespace graphics
{
	class A{int v;};
	A a0;			//graphics名字空间的A
	B b;			//全局名字空间的B
}
using namespace graphics;
int main()
{
	A al;			//二义性错误,不知道是哪个名字空间的A
	graphics::A a2;	//引用graphics名字空间的A
	::A a3;			//引用全局名字空间的A
	return 0;
}

        程序中有全局名字空间的 class A,也有 graphics 名字空间的 class A。

        在 namespace 块的内部,全局名字空间中的名字会被这里定义的同名名字屏蔽。例如,上面程序中,定义 graphics 的 namespace 块内部也是全局名字空间的覆盖范围,但是在这里如果定义了和全局名字空间中重名的名字,全局名字空间中的名字自动被屏蔽,不会有二义性。所以第 8 行的 A 就是 graphics 中定义的 class A。

        但是在第 14 行,全局名字空间和 graphics 名字空间都覆盖了本行,而且都包含 A 这个名字,因此本行导致二义性的编译错误。

        第 16 行,名字前面加 “::” 而不编写名字空间,等于是指定该名字属于全局名字空间。

        11.5.3   单独使用名字空间中的名字

        如果不在程序中编写 “using namespace std;”,那么程序中的 cin,cout,vector 等都会没有定义;但是如果在使用它们时前面都加上 “std∵”,就不会有问题了,例如下面的程序,能够编译通过:

#include <iostream>
#include <vector>
int main()
{
	std::vector<int> v;
	std::vector<int>::iterator i = v.begin();
	std::cout << "Hello" << std::endl;
	std::cout << "World" << std::endl;
	return 0;
}

        有时,编写大程序时,程序员会因为担心和 std 中的名字发生重名。所以不想写 “usingnamespaces std;”,但这样的话,每个 std 里面的名字前面都要加上 “std”,显然非常麻烦。C++ 对此有解决办法。假设 s 是一个名字空间的名称,y 是一个名字,C++ 允许只在程序编写一次 “using s:y;”,此后碰到y,编译器就认为它应该属于名字空间 s。例如:

#include <iostream>
#include <vector>
using std::cout;
using std::vector;
using std::endl;
int main()
{
	vector<int> v;				//前面交代过,vector是属于std的
	vector<int>::iterator i = v.begin();
	cout << "Hello" << endl;	//前面交代过,cout和 endl 是属于std的
	cout << "World" << endl;
	return 0;
}

        11.5.4   用名字空间避免重名

        多个程序员合作的时候,会互相用到别人编写的类,模板、全局函数等。用名字空间避免重名的具体做法如下:

        将一个 .cpp 文件中需要提供给别人使用的名字的定义或声明写在一个 namespace 块里面,然后将该 namespace 块放在一个头文件中,供别的 .cpp 文件包含。namespace 块中可以包含类定义,但不能包含类的非内联成员函数的函数体;可以包含全局函数的声明,但不能包含全局函数的函数体,否则在链接的时候都会导致重复定义的错误。全局函数和成员丽数的函数体应放在一个同名的 namespace 块中,并将这个 namespace 块放在一个 .cpp 文件中。函数模板和类模板的全部内容都可以放在头文件的 namespace 块中,不用担心重复定义问题。

        下面的程序由 namcspacedemo. cpp、groupl.cpp 和 group2.cpp 组成。另外还有两个头文件  group1.h 和 group2.h。在这个程序中,定义了两个名字空间 group1和 group2,两个名字空间中包含的名字一模一样,通过名字空间,就能区别使用这些名字。

        group1.h 文件如下:

#include <iostream>
#include <vector>
using namespace std;
namespace group1
{//包含一个全局函数名、一个类名、一个函数模板名、一个类模板名
	void Func1();
	class A {
		public:
			A() {cout << "group1::A()" << endl;}
			void Print();		//函数体写在group1.cpp中
	};
	template <class T>
	void templateFunc(T a)
	{
		cout << a << "in group1::templateFunc()"<<endl;
	}
	template <class T>
	class templateCls
	{
		vector<T> v;
		public:
			void Append(const T & t);
	};
	template<class T>
	void templateCls<T>::Append( const T & t)
	{
		v.push_back(t);
		cout << t << "appended in group1::templateCls::append" << endl;
	}
}

        group2.h 和 group1.h 几乎一样,只是把 “group1” 改成了 “group2”:

        group2.h 文件如下:

#include <iostream>
#include <vector>
using namespace std;
namespace group2
{//包含一个全局函数名、一个类名、一个函数模板名、一个类模板名,它们和group1中的一样
	void Func1();
	class A {
		public:
			A() {cout << "group2::A()" << endl;}
			void Print();		//函数体写在group2.cpp中
	};
	template <class T>
	void templateFunc(T a)
	{
		cout << a << "in group2::templateFunc()"<<endl;
	}
	template <class T>
	class templateCls
	{
		vector<T> v;
		public:
			void Append(const T & t);
	};
	template<class T>
	void templateCls<T>::Append( const T & t)
	{
		v.push_back(t);
		cout << t << "appended in group2::templateCls::append" << endl;
	}
}

        group1.cpp 中包含了 group1.h 中 Func1 函数和 A::Print 函数的函数体:

        group1.cpp 文件如下:

#include <iostream>
#include "group1.h”
using namespace std;
namespace group1	//一个namespace分多段写是可以的
{
	void Func1() {cout << "gcoup1::Func1" << endl;}
	void A::Print() {cout << "group1::A::Print" << endl;}
}

        group2.cpp 和 group1.cpp 几乎一样,只是把 “group1” 改成了 “group2”:

        group2.cpp 文件如下:

#include <iostream>
#include "group2.h"
using namespace std;
namespace group2
{
	void Func1() {cout<< "group2::Func1" << endl;
	void A::Print() {cout << "group2::A::Print" << endl;}
}

        namespacedemo.cpp 如下:

#include "group1.h"
#include "group2.h"
using namespace std;	//此行写不写都一样
int main()
{
	group1::Func1();	//输出group1::Func1
	group1::A a1;		//输出group1::A()
	a1.Print();			//输出 group1::A::Print
	group1::templateFunc("Hello");	//输出Hello in group1::teaplateFunc()
	group1::templateCls<int> t1;
	t1.Append(100);		//输出100 appended in groupl::templateCls::Append
	group2::Func1();	//输出group2::Fune1
	group2::A a2;		//输出group2::A()
	a2.Print();			//输出group2::A::Print
	group2::templateFunc("Hello");//输出 Hello in group2::templateFunc()
	group2::templateCls<int> t2;
	t2.Append(100);		//输出100 appended in group2::templateCls::Append
}

        11.5.5   无名名字空间

        如果在一个 .cpp 文件中定义的某些函数、类、模板等,不想让别的文件使用,则可以把这些东西写在一个无名名字空间中,如下面的程序由 func.cpp 和 main. cpp 组成。

        func.cpp文件如下:

namespace		//无名名字空间
{
	void Func1(){)
}//无名名字空间自动覆盖其后的所有代码
void Func2() {
	Func1();	//此处被无名名字空间覆盖,因此Func1有定义
}

        main.cpp 文件如下:

void Func1();
void Func2();
int main()
{
	Func1();	//此语句链接时导致没有定义错误
	Func2();
	return 0;
}

        在 main.cpp 中对 Func1 和 Func2 都做了声明,因此编译不会出错。但是在链接的时候,编译器在全局名字空间中找不到 Func1,因此 main 中调用 Func1 的语句会导致出错。在 main 里面无法直接调用 Func1,因为它不属于全局名字空间,它所属的名字空间又没有名字,因此无法使用它。

        无名名字空间自动覆盖它所在的文件,因此在 func.cpp 中可以使用 Func1。

11.6   C++11 新特性概要

        2011年9月,C++ 标准委员会通过了最新的 C++ 标准,这个标准被称为 “C++0x" 或者 “C++11”。新标准对 C++ 做了大量的改进和扩充,有语法方面的,也有库方面的。这里只介绍一点在库方面最受欢迎的改进。

        C++11 在库方面的许多改进源自 Boost 库。Boost 是一个跨平台的开源 C++ 程序库其创建者很多是 C++ 标准委员会的成员。Boost 库内容涉及很多方面,如智能指针、多线程、数学库、随机数、正则表达式等。优秀的 C++ 程序员不能不了解 Boost。Boost 的官方网址是  www.boost.org。Dev C++ 4.9.9.2 内核的 gcc 编译器还不够新,对 C++11 的支持不如 Visual Studio 2010。故本节的程序都是在 Visual Studio 2010中编译的。

        C 发明人 Bjarne Stroustrup 有一个主页,网址是 http://www2.research. att. com/~bs/ 。里面的 C++0x FAQ 对 C++11 的新特性做了比较充分的说明。

        下面介绍部分C++11中的新特性。

        11.6.1   智能指针 shared_ptr

        shared_ptr 模板和 auto_ptr 有类似之处,它包括的成员函数和 auto_ptr 也大多类似。它相比auto_ptr 的改进之处在于,auto_ptr 对象独享对指针的托管权,而多个 shared_ptr 对象可以共同托管一个指针p,在最后一个托管 p 的 shared_ptr 对象消亡时,才会执行 “delete p”。

        要确保用 new 动态分配的内存空间在程序的各条执行路径都能被释放是一件麻烦的事情。 C++11 模板库的 memory 头文件中定义的智能指针,即 shared_ptr 模板,就是用来部分解决这个问题的。只要将 new 运算符返回的指针 p 交给一个 shared_ptr 对象 “托管”,就不必担心在哪里写 “deletep” 语句一实际上根本不需要编写这条语句,托管 p 的 shared_ ptr 对象在消亡时会自动执行 “delete p”。 而且,该 shared_ ptr 对象能像指针 p 一样使用,即假设托管 p 的 shared _ptr 对象叫作 ptr,那么 *ptr 就是 p 指向的对象。

        通过 shared_ptr 的构造函数,可以让 shared_ptr 对象托管一个 new 运算符返回的指针,写
法如下:

                shared_ ptr <T>ptr(new T);         // T可以是int、char类等各种类型

        此后,ptr 就可以像 T* 类型的指针一样使用,即 *ptr 就是用 new 动态分配的那个对象。

        多个shared_ptr对象可以共同托管一个指针 p,当所有曾经托管 p 的shared_ptr 对象都解除了
对其的托管时,就会执行 “delete p"。例如下面的程序:

#include <iostream>
#include <memory>
using namespace std;
class A
{
public:
	int i;
	A(int n):i(n) { };
	~A() { cout << i << " " << "destructed" << endl; }
};
int main()
{
	shared_ptr<A> sp1(new A(2)); //A(2)由sp1托管,
	shared_ptr<A> sp2(sp1);       //A(2)同时交由sp2托管
	shared_ptr<A> sp3;
	sp3 = sp2;   //A(2)同时交由sp3托管
	cout << sp1->i << "," << sp2->i <<"," << sp3->i << endl;
	A * p = sp3.get();      // get返回托管的指针,p 指向 A(2)
	cout << p->i << endl;  //输出 2
	sp1.reset(new A(3));    // reset导致托管新的指针, 此时sp1托管A(3)
	sp2.reset(new A(4));    // sp2托管A(4)
	cout << sp1->i << endl; //输出 3
	sp3.reset(new A(5));    // sp3托管A(5),A(2)无人托管,被delete
	cout << "end" << endl;
	return 0;
}

        程序的输出结果如下:

                2,2,2
                2 destructed
                end
                5 destructed
                4 destructed
                3 destructed

        可以用第 14 行及第 16 行的形式让多个 shared_ptr 对象托管同一个指针。这多个 shared_ptr 对象会共享一个对共同托管的指针的 “托管计数”。有 n 个 shared_ ptr 对象托管同一个指针 p ,则 p 的托管计数就是 n 。当一个指针的托管计数减为 0 时,该指针会被释放。shared_ptr 对象消亡或托管了新的指针,都会导致其原托管指针的托管计数减 1。

        第 20、21 行,shared_ptr 的 reset 成员函数可以使得对象解除对原托管指针的托管(如果
有的话),并托管新的指针。原指针的托管计数会减 1。

        输出的第 4 行说明,用 new 创建的动态对象 A(2) 被释放了。程序中没有写 delete 语句,
而 A(2) 被释放,是因为程序的第 23 行执行后,已经没有 shared_ptr 对象托管 A(2),于是
 A(2) 的托管计数变为 0。最后一个解除对 A(2) 托管的 shared_ptr 对象会释放 A(2)。

        main 函数结束时,spl、 sp2、sp3 对象消亡,各自将其托管的指针的托管计数减为 0,并
且释放其托管的指针,于是会有以下输出:

                5 destructed
                4 destructed 
                3 destructed

        只有指向动态分配的对象的指针才能交给 shared_ptr 对象托管。将指向普通局部变量、全
局变量的指针交给 shared_ ptr 托管,编译时不会有问题,但程序运行时会出错,因为不能析构
一个并没有指向动态分配的内存空间的指针。

        注意,不能用下面的方式使得两个 shared_ ptr 对象托管同一个指针:

                A* p=new A(10) ;
                shared_ ptr <A>sp1 (p) ,sp2 (p) ;

        sp1 和 sp2 并不会共享同一个对 p 的托管计数,而是各自将对 p 的托管计数都记为1(sp2
无法知道 p 已经被 sp1 托管过)。这样,当 sp1 消亡时要析构 p,sp2 消亡时要再次析构 p,这
会导致程序崩溃。

        11.6.2   无序容器(哈希表)

        C ++11 新增了四种所谓 “无序容器” ,分别是 unordered_map、unordered_set、unordered_multimap 和 unordered_muliset。 它们实现的都是哈希表。哈希表是一种能够实现快速查找的数据结构,查找时间比关联容器更快,大多数情况下时间复杂度都是O(1)。无序容器的用法和关联容器相似。例如下面关于 unordered _map 的程序:

#include <iostream>
#include <string>
#include <unordered_map>
using namespace std;
int main()
{
	unordered map<string, int> turingWinner;//图灵奖获奖名单
	turingWinner.insert(make_pair("Dijkstra", 1972));
	turingWinner.insert(make_pair("Scott", 1976));
	turingWinner.insert(make_pair("Wilkes", 1967));
	turingWinner.insert(make_pair("Hamming", 1968));
	turingWinner["Ritchie"] = 1983;
	string nane;
	cin >> name;	//输入姓名
	unordered_map<string, int>::iterator p = turingWinner.find(name);
	//根据姓名查获奖时间
	if(p != turingwinner.end())
		cout<< p->second;
	else
		cout << "Not Found" << endl;
return 0;
}

        程序运行结果可以是:

                Ritchie(回车)
                1983

        也可以是:

               Bush(回车)
                Not Found

        这个程序换成用 map 结果也--样。但是在元素很多的时候,用哈希表确实能比用关联容器
查找效率明显提高。

        11.6.3   正则表达式

        正则表达式(regular expression)用于非常灵活的字符串匹配、查找和替换。仅以匹配为例,正则表达式可以方便地完成类似下面的复杂匹配任务:“看一个字符串是否包含如下子串:该子串以一个长度不超过 10 的英文单词开头,接下来是 3 到 8 个数字,再接下来是一个任意的字符,然后是重复 3 次的长度为 4 的英文单词”。C++ 11中对正则表达式的支持是通过 regex 类,以及regex_ match、regex_search、regex_replace 等几个函数来完成的。请看示例程序:

#include <iostream>
#include <regex> //使用正则表达式须包含此文件
using namespace std;
int main()
{
	regex reg("b.?p.*k");
	cout << regex_match("bopggk",reg) <<endl;    //输出 1 表示匹配成功
	cout << regex_match("boopgggk",reg) <<endl;  //输出 0 表示匹配失败
	cout << regex_match("b pk",reg) <<endl;      //输出 1 表示匹配成功
	regex reg2("\\d{3}([a-zA-Z]+).(\\d{2}|N/A)\\s\\1");
	string correct="123Hello N/A Hello";
	string incorrect="123Hello 12 hello";
	cout << regex_match(correct,reg2) <<endl;    //输出 1 表示匹配成功
	cout << regex_match(incorrect,reg2) << endl; //输出 0 表示匹配失败
}

        第 6 行定义了一个正则表达式对象,其中包含的正则表达式是 "b. ?p.* k"。正则表达式描述了一种字符串的模式。表达式中 '. ' 代表任意一个字符,' * ' 代表出现零次或更多次,“+” 代表出现一次或更多次,'?' 代表出现零次或一次。 因此在第 7 行," bopggk" 是能够匹配这个正则表达式的,它以 'b' 开头,后面跟着出现了一次的某个字符 'o',然后是 'p',接下来出现了两次的某个字符 'g',再接下来是 'k'。在第 8 行," boopgggk" 不能匹配 reg,因为其中的 'o' 出现了两次,不符合 “b后面应该跟着某个出现了零次或一次的字符,然后再是p”

        这个模式。第 9 行请读者自己分析。

        第 10 行的正则表达式中,“\d"(别忘了 C++ 字符串中 '\' 要连写两次)代表数字,“{3}" 代表出现 3 次(即数字出现3次),“\s” 代表空格,“()” 中的内容是 “项”,“\1” 代表此处应出现第一项。这个正则表达式表示以下模式的字符串:3个数字,一个英文单词,任意一个字符,两个数字或 "N/A",空格,然后是第一项(即前面出现的那个英文单词)。

        按照这个模式,"123Hello N/A Hello" 能够匹配,而 "123Hello 12 hello" 不能,因为它最后出现的那个单词不是前面出现的 "Hello",差了一个字母。

        正则表达式很复杂,这里只能做一点极为皮毛的介绍,有需要的读者可自行查阅相关资料。

        11.6.4   Lambda 表达式

        使用 STL 的时候,往往会大量用到函数对,为此要编写很多函数对象类。有的函数对象类只定义了一个对象,而且这个对象也只使用了一次,那么编写这样的函数对象类就感觉有点浪费。而且,定义函数对象类的地方和使用函数对象的地方可能相隔较远,看到函数对象,想要查看其 operator() 成员函数到底是做什么的,也会比较麻烦。对于只使用一次的函数对象类,能不能直接在使用它的地方来定义呢?Lambda 表达式就能解决这个问题。使用 Lambda 表达式可以减少程序中的函数对象类的数目,使得程序更加优雅一些。

        Lambda表达式的定义形式如下:

                [外部变量访问方式说明符](参数表) -> 返回值类型
                {
                        语句组
                }

        其中,“外部变量访问方式说明符” 可以是 “=” 或 “&",表示 “{ }” 里面用到的,定义在 “{ }" 外面
的变量,在 “{ }” 中是否允许被改变。“=” 表示不允许,“&” 表示允许。当然,“{ }" 里面也可以不使用定义在外面的变量。“->返回值类型” 也可以没有。下面就是一个合法的 Lambda 表达式:

                [ = ](intx, int y)->bool { return x%10 < y%10; }

        Lambada 表达式实际上是一个函数,只是它没有名字。下面的程序段使用了上面的 Lambada 表达式:

                int a[4] = {11,2,33,4};
                sort(a,a+4,[=](intx, inty)->bool { return x%10 < y%10;});
                for_each(a,a+4,[ = ](int x) {cout<<x<" " ;});

        程序运行结果如下:

                11 2 33 4
        程序第 2 行使得数组 a 按个位数从小到大排序。具体的原理是 sort 在执行过程中,需要判断两个元索 x、y 的大小时,会以x、y作为参数,调用 Lambada 表达式所代表的函数,并根据返回值来判断 x、y 的大小。这样,就用不着专门编写一个函数对象类了。

        第 3 行,for_each 的第 3 个参数是一个 Lambda 表达式。for_ each 执行过程中会依次以每个元素作为参数调用它,所以每个元素都被输出了。

        下面看一个用到了外部变量的 Lambada 表达式的程序:

#include <iostream>
#include <algorithm>
using namespace std;
int main()
{
    int a[4] = {1,2,3,4};
    int total = 0;
    for_each(a, a+4, [&](int &x){total += x; x *= 2;});
    cout << total << endl;    //输出10
    for_each(a, a+4, [=](int x){cout << x << " ";);
    return 0;
}

程序运行结果如下:

        1 0
        2 4 6 8

        第 8 行,“[&]" 表示该 Lambada 表达式中用到的外部变量 total 是传引用的,其值可以在表达式执行过程中被改变(如果使用 “[=]",就会编译不过了)。该 Lambada 表达式每次被 for_each 执行,都将 a 中的一个元素累加到 total 上,然后将该元素加倍。

        实际上,“外部变量访问方式说明符” 还可以有更加复杂和灵活的用法。例如,[ =,&x, &y] 表示外部变量 x、y 的值可以被修改,其余外部变量不能被修改; [&, x, y] 表示除 x、y以外的外部量,值都可以被修改。

        例如下面的程序:

#include <iostream>
using namespace std;
int main()
{	
	int x = 100,y=200,z=300;
	auto ff  = [=,&y,&z](int n) {
		cout <<x << endl;
		y++; z++;
		return n*n;
	};
	cout << ff(15) << endl;
	cout << y << "," << z << endl;
}

        程序的输出结果如下:

                100
                225
                201,301

        第 6 行定义了一个变量 ff,ff 的类型是 auto,表示由编译器自动判断其类型(这也是 C++11 的新特性)。本行将一个 Lambda 表达式赋值给 ff,以后就可以通过 ff 来调用该 Lambda 表达式了。

        第 11 行通过 ff,以 15 作为参数 n 调用上面的 Lambda 表达式。该 Lambda 表达式指明,对于外部变量 y、z,可以修改其值;对于其他外部变量,例如 x,不能修改其值。因此在该表达式执行时,可以修改外部变量 y、z 的值,但如果出现试图修改 x 值的语句,就会编译出错。

        11.6.5   auto 关键字和 decltype 关键字

        可以用 auto 关键字定义变量,编译器会自动判断变量的类型。例如:

                auto i =100;                 //i是int
                auto p=new A() ;         // p是A*
                auto k =34343LL;       // k是1ong 1ong

        有时,变量的类型名特别长,使用 auto 就会很方便。例如:

                map<string, int, greater<string> > mp;
                for (auto i =mp. begin(); i != mp.end(); ++i)
                        cout << i->first << "," << i->second;

        编译器会自动判断出 i 的类型是 map<string, int, greater<string> >::iterator。

        decltype 关键字可以用于求表达式的类型。例如:

                int i;
                double t;
                struct A{double X;};
                const A* a=new A();
                decltype(a)x1;        // x1是A*
                decltype (i)x2;        //x2是int
                decltype(a->x)x3;  // x3是double

        在上面的例子中,编译器自动将 decltype(a) 等价为 “A*”,因为编译器知道 a 的类型是 “A*”。

        auto 和 decltype 可以一起使用。例如:

#include < iostream >
using namespace std;
struct A{
	int i;
	A(int ii):i(ii){}
};
A operator + (int n,const A & a)
{
	return A(a.i+n);
}
template <class T1,class T2>
auto add(T1 x,T2 y) -> decltype (x+y) {
	return x + y;
}
int main() {
	auto d = add(100,1.5);	 // d是double类型, d=101. 5
	auto k = add(100,A(1));	 // k是A类型,因为表达式“100 +A(1)”是A类型的
	cout << d << endl;
	cout << k.i << endl;
	return 0;
}

        程序的输出结果如下:

                101.5
                101

        第 12 行告诉编译器,add 的返回值类型是 “decltype (x+y)”, 即返回值的类型和 “x+y” 这个表达式的类型一致。编译器将 add 实例化时,会自动推断出 “x +y" 的类型。在 C++11 中,函数返回值若为 auto,则需要和 decltype 配合使用。在 C++14 中,则可以不用 decltype,例如下面的程序没有问题:

                auto add(int a,int b) {
                        int i=a +b;
                        return i;
                }

        11.6.6   基于范围的 for 循环

        C++11 引人了新的 for 循环的写法,在遍历整个数组或整个容器时,不再需要循环控制变量。例如:

#include <iostream>
#include <vector>
using namespace std;
struct A {	int n;	A(int i):n(i) {	} };
int main()  {
	int ary[] = {1,2,3,4,5};
	for(int & e: ary)  	//将ary中每个元素都乘以10
		e*= 10;
	for(int e : ary)	    //输出ary中所有元素
		cout << e << ",";
	cout << endl;
	vector<A> st(ary,ary+5);
	for(  auto & it: st)  //将st中每个元素都乘以10
		it.n *= 10;
	for(  A it: st)   //输出st中所有元素
		cout << it.n << ",";
	return 0;
}

        程序的输出结果如下:

                10,20,30,40,50,
                100 ,200 ,300, 400, 500,

        第 7 行可以理解为:要遍历整个数组 ary,e 代表数组中的每个元素。e 前面有 “&”, 表明 e 的值在循环中可以被修改。因此,循环中的 “e* =10” 就会导致数组中的每个元素都被乘以 10。而第 9 行的 e 也代表数组 ary 的每个元素,但其值在循环中不能被修改。

        第 13 行的 it 代表 st 中的每个元素,类型为 auto,编译器会自动判断其类型。

        11.6.7   右值引用

        能出现在赋值号左边的表达式称为 “左值”,不能出现在赋值号左边的表达式称为 “右值”。一般来说,左值是可以取地址的,右值则不可以。非 const 的变量都是左值。函数调用的返回值若不是引用,则该函数调用就是右值。前面所学的 “引用” 都是引用变量的,而变量是左值,因此它们都是 “左值引用”。

        C++11 新增了一种引用,可以引用右值,因而称为 “右值引用”。无名的临时变量不能出现在赋值号左边,因而是右值。右值引用就可以引用无名的临时变量。定义右值引用的格式如下:

                类型 && 引用名 = 右值表达式;

        例如:

                class A{};
                A & r1=A();           //错误,无名临时变量A ()是右值,因此不能初始化左值引用r1
                A && r2=A();         // 正确,因r2是右值引用

        引入右值引用的主要目的是提高程序运行的效率。有些对象在复制时需要进行深复制,深复制往往非常耗时。合理使用右值引用可以避免没有必要的深复制操作。例如下面的程序:

#include <iostream>
#include <string>
#include <cstring>
using namespace std;
class String
{
	public:
		char* str;
	String():str(new char[1]) {str[0]=0;}
	String(const char* s){
		str = new char[strlen(s)+1];
		strcpy(str,s);
	}
	String(const String & s){//复制构造函数
		cout << "copy constructor called" << endl;
		str = new char[strlen(s.str)+1];
		strcpy(str,s.str);
	}
	String & operator = (const String & s) {//复制赋值号
		cout << "copy operator = called" << endl;
		if(str !=s.str) {
			delete [] str;
			str = new char[strlen(s.str)+1];
			strcpy(str,s.str);
		}
		return *this;
	}
	String(String && s):str(s.str) {//移动构造函数
		cout << "move constructor called" << endl ;
		s.str = new char[1];
		s.str[0]=0;
	}
	String & operator = (String && s) {//移动赋值号
		cout << "move operator = called" << endl ;
		if(str !=s.str) {
			str=s.str;
			s.str = new char[1];
			s.str[0] =0;
		}
	return *this;
	}
	~String() {delete [] str;}
}; 
template <class T>
void MoveSwap(T & a,T & b) {
	T tmp(move(a));	//std::move(a) 为右值,这里会调用移动构造函数
	a = move(b) ;	//move(b)为右值,因此这里会调用移动赋值号
	b = move(tmp);	//move(tmp)为右值,因此这里会调用移动赋值号
}
int main()
{
	String s;
	s = String("this"); //调用移动赋值号
	cout << "****" << endl;
	cout << s.str << endl;
	String s1 = "hello", s2 = "world";
	MoveSwap(s1,s2);	//调用一次移动构造函数和两次移动赋值号
	cout<< s2.str << endl;
	return 0;
}

        程序的输出结果如下:

                move operator=called
                ****
                this
                move constructor called
                move operator=called
                move operator=called
                hello

        第 33 行重载了一个移动赋值号。它和第 19 行的复制赋值号的区别在于,其参数是右值引用。在移动赋值号函数中没有执行深复制操作,而是直接将对象的 str 指向了参数 s 的成员变量 str 指向的地方,然后修改 s.str 让它指向别处,以免 s.str 原来指向的空间被释放两次。该移动赋值号函数修改了参数,这会不会带来麻烦呢?答案是不会。因为移动赋值号函数的形参是一个右值引用,则调用该函数时,实参一定是右值。右值一般是无名临时变量,而无名临时变量在使用它的语句结束后就不再有用,因此其值即使被修改也没有关系。

        第 53 行,如果没有定义移动赋值号,则会导致复制赋值号被调用,引发深复制操作。临时无名变量 “String (" this" )” 是右值,因此在定义了移动赋值号的情况下,会导致移动赋值号被调用。移动赋值号使得 s 的内容和 String (" this" ) 一致,然而却不用执行深复制操作,因而效率比复制赋值号高。虽然移动赋值号修改了临时变量 String ( " this" ),但该变量在后面已无用处,因此这样的修改不会导致错误。

        第 46 行使用了 C ++11 中的标准模板 move。move 能接受一个 左值作为参数,返回该左值的右值引用。因此本行会用定义于第 28 行、以右值引用作为参数的移动构造函数来初始化 tmp。该移动构造函数没有执行深复制,将 tmp 的内容变成和 a 相同,然后修改 a。由于调用 MoveSwap 本来就会修改 a,所以 a 的值在此处被修改不会产生问题。第 47 行和第 48 行调用了移动赋值号,在没有进行深复制的情况下完成了 a 和 b 内容的互换。对比 Swap 函数的以下写法:

                template <class T>
                void Swap(T & a,T & b) {
                        T tmp(a);         //调用复制构造 函数
                        a=b;                //调用复制赋值号
                        b= tmp;           //调用复制赋值号
                }

        Swap 函数执行期间会调用一次复制构造函数,两次复制赋值号,即一共会进行三次深复
制操作。而利用右值引用,使用 MoveSwap,则可以在无须进行深复制的情况下达到相同的目
的,从而提高了程序的运行效率。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值