您的位置:首页 > 编程语言 > C语言/C++

C++ premier -- 异常、命名空间以及多重继承

2012-08-19 14:57 363 查看
放了两个星期的暑假,回来久久不能进入状态。也或许是这一章对编程经验的要求远高于我目前的水平,很难静下心来看下去。总结也可能会因此做得很不到位。Anyway,有些东西就先记在这里吧。
第17章起就是高级主题了,主要涉及大型程序中使用的工具,包括异常的使用、命名空间以及多重继承。

1.异常

通过异常我们可以将问题的检测和问题的解决分享,这样程序的问题检测部分可以不必了解如何处理问题。

1.1抛出异常对象throw

在C++的异常处理中,需要由问题检测部分抛出一个对象给处理代码,通过这个对象的类型和内容,两个部分能够就出现了什么错误进行通信,这就是异常对象。异常以类似于将实参传给函数的方式抛出和捕获。

执行throw的时候,不会执行跟在throw后面的语句,而是将控制从throw转移到匹配的catch,该catch可以是同一函数中局部的catch,也可以在直接或间接调用发生异常的函数的另一个函数中。控制从一个地方传到另一个地方,这有两个重要含义:

(1)沿着调用链的函数提早退出。

(2)一般而言,在处理异常的时候,抛出异常的块中的局部存储不存在了。

异常对象由编译器管理,而且保证驻留在可以被激活的任意catch都可以访问的空间。

当抛出一个表达式的时候,被抛出对象的静态编译时类型将决定异常对象的类型。也就是说,假如抛出的对象是基类的解引用,而该指针实际指向继承类,该对象会被分割,成为事实上的基类对象。

抛出异常的时候,交暂停当前函数的执行,开始查找匹配的catch子句。首先检查throw本身是否在try块内部,如果是,检查与该try相关的catch子句,看是否其中之一与被抛出对象相匹配。如果找到匹配的catch,就处理异常;如果找不到,就退出当前函数(释放当前函数的内存并撤销局部对象),并且继续在调用函数中查找。如果对抛出异常的函数的请用是在try块中,则检查与该try相关的catch子句。如果找到匹配的catch,就处理异常;如果找不到匹配的catch,调用函数也退出,并且继续在调用这个函数的函数中查找,这个过程称之为“栈展开”。

如果一个块直接分配资源,而且在释放资源之前发生异常,在栈展开期间将不会释放该资源。

1.2捕获异常catch

catch子句中的异常说明符看起来像只包含一个形参的形参表,异常说明符是在其后跟一个可选形参名的类型名。类型必须是完全类型,不能向前声明。

在查找匹配的catch期间,找到的catch不必是与异常最匹配的那个catch,相反,将选中第一个可以处理该异常的catch。异常与catch说明符匹配的规则只允许以下转换:

(1)允许从非const到const的转换。也就是说,非const对象的throw可以与指定接受const引用的catch匹配。

(2)允许从派生类类型到基类类型的转换

(3)将数组转换为指向数组类型的指针,将函数转换为指向函数类型的适当指针。

通常,如果catch子句处理因继承而相关的类型的异常,它就应该将自己的形参定义为引用。(其规则与一般函数一样)

如果单个catch不能完全处理一个异常,在进行了一些校正行动之后,catch可能确定该异常必须由函数调用链中更上层的函数来处理,catch可以通过重新抛出将异常传递给调用链中更上层的函数。

1.3 函数测试块与构造函数

构造函数函数体内部的catch子句不能处理在处理构造函数初始化式时可能发生的异常。构造函数要处理来自构造函数初始化式的异常,唯一的方法是将构造函数编写为函数测试块。
template<class T> Handle<T>::Handle(T *p)
try: ptr(p), use(new size_t(1)){
//empty function body
}catch( const std::bad_alloc& e ){
handle_out_of_memory(e);
}
1.4用类管理资源分配

异常安全:异常安全意味着,即使发生异常,程序也有正确操作。在这种情况下,“安全”来自于保证“如果发生异常,被分配的任何资源都能适当地释放”。

例如下面的函数:
void exercise(int* b, int* e){
vector<int> v(b,e);
int *p=new int[v.size()];
ifstream in("ints");
//exception occurs
//...
delete p;	//p will not be released
}
用类管理资源分配的例子如下:
class Resource{
public:
Resource( size_t t ):r(new int[t]){}
~Resource(){
if(r)
delete r;
}
private:
int* r;
};

void exercise( int *b, int *e ){
vector<int> v(b,e);
Resource res(v.size());
ifstream in("ints");
//now if exceptions occur here
//destructor of res will be invoked
//to release the pointer in res
...
}

标准库中提供的auto_ptr是“资源分配即初始化”技术的例子。

2. 命名空间

命名空间的作用是限定作用域。在命名空间外部使用命名空间的成员时,必须使用限定名namespace_name::member_name。命名空间可以是不连续的,一个命名空间的分离部分可以分散在多个文件中,由此可以用分离的接口文件和实现文件构成命名空间。

未命名的命名空间在定义时没有给定名字。未命名的命名空间以关键字namespace开头,后面直接将花括号。它可以在给定文件中不连续,但是不能跨越文件,每个文件有自己的未命名空间。

未命名的命名空间用于声明局部于文件的实体。在未命名的命名上定义的变量在程序开始时创建,在程序结束之前一直存在。(它将取代C中的static声明)

除了使用限定名namespace_name::member_name用引用命名空间的成员外,还可以使用using声明、using指示及命名空间别名的方法。

using声明一次只引入一个命名空间成员,如:
using std::map;
using std::string;
其作用域从using声明点开始,直到包含该using声明的作用域的末尾。

可以将较短的同义词与命名空间名字相关联,如:
namespace cplusplus_primer{ /* ... */ }
namespace primer = cplusplus_primer;
namespace Qlib = cplusplus_primer::QueryLib;
using指示使用如下:

using namespace std;

using指示将命名空间成员提升到包含命名空间本身和using指示的最近作用域。例如:
namespace A{
int i, j;
}
void f(){
using namespace A;	//injects names from A into the global scope.
cout<<i*j<<endl;
//...
}
类、命名空间和作用域

考虑下面的简单程序:
std::string s;
//calls std::getline(std::istream&, const std::string& )
getline(std::cin, s);

接受类类型形参(或类类型指针及引用形参)的函数(包含重载符),以及与类本身定义在同一命名空间中的函数(包括重载函数),在用类类型对象(或类类型的引用及指针)作为实参的时候是可见的。

当编译器在看到getline函数的使用getline(std::cin,s)的时候,它在当前作用域、包含调用的作用域以及定义cin的类型和string类型的命名空间中查找匹配的函数。因此它在std空间中查找并找到getline函数。
"当一个类声明友元函数的时候,函数的声明不必是可见的。如果不存在可见的声明,那么,友元声明具有将该函数或类的声明放入外围作用域的效果。如果类在命名空间内部定义,则没有另外声明的友元函数在同一命名空间中声明。"

这段话我读了很多遍,也不是很明白它的意思。友元声明将已命名的类或非成员函数引入到外围作用域中,意思应该是说,友元虽然在类内部声明或定义,但是他的作用域跟类的作用域一样(我的意思是说,它可以使用类的私有成员,我们可以像使用类一样在外围作用域使用函数)。如果我们在命名空间内定义类,那么友元函数也应该在同一命名空间声明,那么,这个“没有另外声明的友元函数”中,“没有另外声明”是什么意思呢?

命名空间对函数匹配有两个影响,一是using声明或using指示可以将函数加到候选集合。另外一种,有一个或多个类类型形参的函数的名字查找包括定义每个形参类型的命名空间,为查找候选函数而查找定义形参类(以及定义其基类)的每个命名空间,将那些命名空间中任意与被调用函数名字相同的函数加入候选集合。例如:
namespace NS{
class Item_base{ /*... */ }
void display( const Item_base& ){}
}

class Bulk_item : public NS::Item_base{ }

int main(){
Bulk_item book1;
display(book1);
return 0;
}

display函数的实参book1具有类类型Bulk_item。display调用的候选函数不仅是在调用display函数的地方其声明可见的函数,还包括声明 Bulk_item类及其基类Item_base的命名空间中的函数。

3. 多重继承与虚继承

多重继承跟一般的继承没有太大区别,然而多个基类可能导致二义性。例如不同基类同时含有相同名字的成员,此时不论该成员是否可见,都会能查找名字,然后再判断声明的合法性。例如下面的代码:
struct Base1{
Base1():ival(-11),dval(0), cval('b'){}
void print( int )const;
protected:
int ival;
double dval;
char cval;
private:
int *id;
};

struct Base2{
Base2(){}
void print( double )const;
protected:
double fval;
private:
double dval;
};

struct Derived : public Base1{
Derived():dval(0),sval("eva"){}
void print( std::string ) const;
protected:
std::string sval;
double dval;
};

struct MI: public Derived, public Base2{
MI(){
ival = new int(11);
dvec.push_back(1);
dvec.push_back(2);
}
void print( std::vector<double> );
void bar();
void foobar( double );
protected:
int *ival;
std::vector<double> dvec;
};

void MI::bar(){
int sval;
//dval = 3.14;		//此时编译出错,dval在Derived及Base2中均有定义
//虽然dval在Base2中是私有成员,然而编译器先找到了这两个匹配的声明,
//已经出现了歧义。
cval = 'a';
//id = 1;
fval = 0;
sval = *ival;
}
虚继承是一种机制,类通过虚继承指出它希望共享其虚基类的状态。在虚继承下,对给定虚基类,无论该类在派生层次中作为虚基类出现多少次,只继承一个共享的基类子对象。

非虚继承的时候,我们通过向上逐步初始化各基类实现继承类的初始化,然而虚继承由于只继承一个共享的基类子对象,这使继承类的初始化变得复杂了起来:

在虚派生中,由最低层派生类的构造函数初始化虚基类。

无论虚基类出现在继承层次的任何地方,总是在构造非虚基类之前构造虚基类。

http://philoscience.iteye.com/category/176811
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: