Chinaunix首页 | 论坛 | 博客
  • 博客访问: 69213
  • 博文数量: 43
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 420
  • 用 户 组: 普通用户
  • 注册时间: 2014-06-27 15:04
个人简介

记录,分享

文章分类

全部博文(43)

文章存档

2017年(24)

2015年(1)

2014年(18)

我的朋友

分类: C/C++

2017-03-16 11:02:56

1.  异常处理

(1) 抛出类类型的异常

【1】异常可以是可传给非引用形参的任意类型的对象(包括内置类型)。异常对象由throw创建,并被初始化为被抛出的表达式结果的副本。异常对象将传给相应的catch,并且在完全处理了异常之后撤销。 异常对象通过复制throw表达式的结果而创建,故异常对象必须是可以复制的类类型。

void func(){
  ...
  throw xxx_ex();
}

try{
  func();
} catch(const xxx_ex& e){
 ...
}
【2】当抛出一个表达式的时候,被抛出的对象的静态编译时类型将决定异常对象的类型。故,如果通过解引用一个指针而抛出异常对象,若指针是一个指向派生类对象的基类类型指针,那么被抛出的对象将被分割,只抛出基类部分(实质是派生类引用转换成基类引用,作为基类复制构造函数的实参)。
    无论是抛出指针还是抛出指针的解引用,都有在处理异常时指针指向对象不存在的潜在风险,所以最好只通过复制传递异常对象。

(2).  栈展开 
  【1】栈展开期间,可能提前退出异常抛出点所在的作用域,该作用域的局部对象在异常抛出时会被释放,如果局部对象是类类型的,将自动调用其析构函数。
  【2】析构函数应该从不抛出异常 : 在为某个异常进行栈展开的时候,析构函数如果又抛出自己的未经处理的另一个异常,将会导致调用标准库terminate函数。一般而言,terminate函数将调用abort函数,强制从整个程序非正常退出。
  【3】与析构函数不同,构造函数可能会抛出异常。此时,对象只是部分被构造了,因为构造失败,要保证能适当地撤销已构造的成员。类似的,在初始化数组或者其他容器类型的时候,也可能发生异常,要保证能适当地撤销已构造的对象。
  【4】如果找不到匹配的catch,程序就调用库函数terminate。

(3).  捕获异常
【1】在catch子句列表中,最特殊的catch(形参的类型在异常继承层次底部)必须最先出现。即,带有因继承而相关的类型的多个catch子句,必须从最低派生类型到最高派生类型排序。
  
    除了以下几种情况外,异常的类型必须与catch说明符的类型完全匹配:
a. 允许从非const到const的转换。
b. 允许从派生类型到基类类型的转换。
c. 将数组转换为指向数组类型的指针,将函数转换为指向函数类型的适当指针。

【2】基类的异常说明符可以用于捕获派生类型的异常对象,而且,异常说明符的静态类型决定了cach子句可以执行的操作。
通常,如果catch子句处理因继承而相关的类型的异常,它就应该将自己的形参定义为引用,否则,一方面无法利用动态绑定,一方面有可能派生类型对象复制到基类类型对象导致异常对象被分割,只捕获到异常对象的基类部分(实质是派生类引用转换成基类引用,作为基类复制构造函数的实参)。

(4)  重新抛出

重新抛出是后面不跟类型或表达式的一个空throw,它只能出现在catch或从catch调用的函数中。 被抛出的异常是原来的异常对象,而不是catch形参!!!当catch形参是基类类型的时候,我们不知道由重新抛出表达式抛出的实际类型,该类型取决于异常对象的动态类型,而不是catch形参的静态类型。

(5) 捕获所有异常

捕获所有异常的catch子句形式为
catch(...){
  //TO DO
}

catch(...)常与重新抛出结合使用,作为重新抛出前的处理。如果catch(...)与其他catch子句结合使用,它必须是最后一个。

(6) 函数测试块与构造函数

    异常可能发生在构造函数中,或者发生在处理构造函数初始化式的时候。在进入构造函数体之前处理构造函数初始化式,构造函数体内部的catch子句不能处理在处理初始化式时发生的异常。
    为了处理来自构造函数初始化式的异常,必须将构造函数编写为函数测试块 , 形式如下:

class B {
private :  
    A a;
public :
    B(string s) try : a(s) {
        //TO DO
    } catch(const xxx_ex &e){
       //TO DO
    }
}

关键字try出现在成员初始化列表之前,catch子句既可以处理从成员初始化列表中抛出的异常,也可以处理构造函数体内部抛出的异常。

(8)  自动资源释放

通过定义一个类来封装资源的分配和释放,可以保证正确释放资源。这一技术常称为“资源分配即初始化”,简称RAII。

(9)  auto_ptr 类

通常,如果通过常规指针分配内存,而且在执行delete之前发生异常,就不会自动释放内存。如果使用一个auto_ptr对象来替代常规指针,就能在异常发生时自动释放动态分配的内存。auto_ptr类是接受一个类型形参的模版,它动态分配的对象提供异常安全,是一种“智能指针”。

使用:
auto_ptr ap;                //创建名为ap的未绑定的auto_ptr对象
auto_ptr ap(p);           //创建名为ap的auto_ptr对象,ap拥有指针p指向的对象。该构造函数为explicit
auto_ptr ap1(ap2);     //创建名为ap1的auto_ptr对象,ap1保存原来存储在ap2中的指针,ap2成为未绑定的auto_ptr对象(此时*ap2将导致运行时错误)
ap1 = ap2;                         //将所有权从ap2转给ap1.删除ap1指向的对象,并且使ap1指向ap2指向的对象,是ap2成为未绑定的。
~ap                                    //析构函数,删除ap指向的对象。
*ap                                     //返回对ap所绑定的对象的引用。
ap->                                  //返回ap保存的指针。
ap.reset(p)                        //如果p与ap的值不同,则删除ap指向的对象并将ap绑定到p
ap.release(p)                    //返回ap保存的指针并且使ap成为未绑定的
ap.get()                             //返回ap保存的指针

缺陷:
a.  auto_ptr只能用于管理从new返回的一个对象,不能管理动态分配的数组。
b. 当复制auto_ptr对象或者将它的值赋给其他auto_ptr对象的时候,将基础对象的所有权从原来的auto_ptr对象转给副本,原来的auto_ptr对象重置为未绑定状态。与其他复制或赋值操作不同,auto_ptr的复制和赋值操作改变右操作数(赋值的左右操作数都必须是可修改的左值),而且,赋值还删除左操作数原来指向的对象(左右对应对象不同),因此,不能将auto_ptr存储在标准库容器类型中。(auto_ptr的复制和赋值是破坏性操作)

测试auto_ptr对象:
要测试auto_ptr对象,必须使用它的get成员,该成员返回包含在auto_ptr对象中的基础指针:
if(ap.get()){
    //TO DO
}

reset操作:
auto_ptr与内置指针的另一个区别是,不能直接将一个地址(或者其他指针)赋给auto_ptr对象,相反,必须使用reset函数来改变指针。

【警告】
a.  不要使用auto_ptr对象保存指向静态分配对象的指针。
b.  永远不要使用两个auto_ptr对象指向同一对象,否则该对象内存将被释放两次,导致错误。
c.  不要使用auto_ptr对象保存指向动态分配数组的指针。
d.  不要将auto_ptr对象存储在容器中。

(10)  异常说明

异常说明指定,如果函数抛出异常,被抛出的异常将是包含在该说明中的一种,或者是从列出的异常中派生的类型。

【1】定义异常说明
异常说明跟在函数形参表之后(如果是const成员函数则跟在const限定符之后),一个异常说明在关键字throw后跟着一个有圆括号括住的异常类型列表(可能为空):
void func1(int i) throw (xxx_ex , yyy_err);
void func2() throw ();

如果一个函数没有指定异常说明,则该函数可以抛出任意类型的异常。如果异常类型列表为空,则表示该函数不抛出任何异常。

【2】违反异常说明
如果函数抛出了没有在其异常说明中列出的异常,就调用标准库函数unexpected。默认情况下, unexpected调用terminate函数,终止程序。

【3】因为不能在编译时检查异常说明,异常说明的应用通常是有限的。

【4】派生类的虚函数不能抛出比基类同名虚函数更多的异常。

(11) 函数指针的异常说明

异常说明是函数类型的一部分,这样,也可以在函数指针的定义中提供异常说明:
void (*pf) (int ) throw (runtime_error) ;

在使用另一指针初始化带异常说明的函数的指针,或者将后者赋值给函数地址的时候,两个指针的异常说明不必相同,但是,源指针的异常说明必须至少与目标指针一样严格。(P.598)


2.  命名空间

(1)命名空间的定义
命名空间定义以关键字namespace开始,后接命名空间的名字,定义体用花括号括起来,花括号后不能接分号。

【1】 在命名空间中定义的名字可以被命名空间中的其他成员直接访问,命名空间外部的代码必须指出名字定义在哪个命名空间中
【2】为避免每次都是用完全限定名namespace_name::member_name的形式,可以用using namespace_name::Typename化简命名空间成员的是用。
【3】命名空间可以再几个部分中定义,命名空间由它的分离定义部分的总和构成,命名空间是累积的。一个命名空间的分离部分可以分散在多个文件中,在不同文本文件中的命名空间定义也是累积的。如果命名空间的一部分需要定义在另一个文件中的名字,仍然必须声明该名字。
【4】在命名空间内部定义的函数可以使用同一命名空间中定义的名字的非限定名
namespace cpp_primer {
  std::istream& operator>>(std::istream& in , Sales_item& s)

也可以在命名空间定义的外部定义命名空间成员:
std::istream& cpp_primer::operator>>(std::istream &in , Sales_item& s)
一旦看到完全限定的函数名,就处于命名空间的作用域中,因此,形参表和函数体中的命名空间成员引用可以使用非限定名。

【5】全局命名空间是隐式的,可以引用作用域操作符引用全局命名空间的成员,全局命名空间没有名字,使用::member的形式引用其内部定义的成员。

(2)嵌套的命名空间

命名空间可以嵌套:外围命名空间中声明的名字被嵌套命名空间中同一名字的声明所屏蔽。嵌套命名空间内部定义的名字局部于该命名空间。外围命名空间之外的代码只能通过限定名引用嵌套命名空间中的名字(形如outer::inner::member)

(3)  未命名的命名空间

命名空间可以是未命名的,未命名的命名空间定义形如 namespace{...} ,关键字后直接接花括号。

未命名的命名空间的定义局部于特定文件,仅在该文件中可见,它可以不连续,但从不跨越多个文件,用于声明局部于文件的实体。在未命名的命名空间中定义的变量在程序开始时创建,在程序结束前一直存在。

如果在文件的最外层作用域中定义未命名的命名空间,那么,未命名的命名空间中的名字必须与全局作用域中的名字不同,否则名字冲突。

未命名的命名空间可以嵌套在另一命名空间内部,其中的名字按常规方法使用外围命名空间名字访问。

在标准C++引入命名空间以前,程序必须将名字声明为static,使它们局部于一个文件。这从c语言继承而来,c语言中,声明为static的局部实体在声明它的文件之外不可见。

(4)  命名空间成员的使用

【1】using声明: using xxx:yyy , 它可以出现在全局作用域、局部作用域或者命名空间作用域中。类作用域中的using声明局限于被定义类的基类中定义的名字。
【2】可用命名空间别名将较短名字与较长的命名空间名字相关联。
namespace sname = longnamespacename
可以用命名空间别名简化嵌套命名空间的书写
【3】using指示 : using namespace xxx。有时,在使用多个库的时候,使用using指示会重新引入名字冲突的所有问题。
namespace A{
  int a = 1;
  int b = 2;
  int c = 3;
}

int b = 4;

void func(){
  using namesapce A; 
  ++a;  //ok
  ++b;  //error
  int c = 6;  //ok
}

例子中,func中b的引用具有二义性,必须使用作用域操作符指出具体的引用。如果需要引用全局作用域中的b,则应使用::b,如果要引用命名空间A中的b,则应使用A::b

应该避免使用using指示,而使用using声明,减少注入到命名空间中的名字数目。通常,可以将using指示用在命名空间本身的实现文件中。

(5) 类、命名空间和作用域

  当在类作用域中使用名字时,首先在成员函数本身中查找,然后再类中查找,包括任意基类,只有在查找玩类之后,才检查外围作用域。当类包在命名空间中的时候,发生相同的查找:首先在成员中查找,然后在类中(包括基类)查找,再在外围作用域中查找,外围作用域中的一个或多个可以是命名空间。

【1】实参相关的查找和类类型形参
接受类类型形参(或类类型指针及引用形参)的函数(包括重载操作符),以及与类本身定义在同一命名空间中的函数(包括重载操作符),在用类类型对象(或类类型指针及引用)作为实参的时候是可见的。
例如,
std::string s;
getline(std::cin,s);

getline可以不加限制的引用,而无需using std::getline或std::getline(std:cin,s)。当编译器看到getline函数使用getline(std::cin,s)的时候,它在当前作用域、包含调用的作用域以及定义cin的类型和string类型的命名空间中查找匹配的函数。因此,它在std中查找并找到由string类型定义的getline函数。

目的:允许无需单独的using声明就可以使用概念上作为类接口组成部分的非成员函数。

【2】隐式友元声明与命名空间
如果不存在可见的声明,那么,友元声明具有将函数或类放入外围作用域的效果。如果类在命名空间内部定义,则没有另外声明的友元函数在同一命名空间中声明。

例如,
namespace A{
    class C{
        friend void f(const C&);
    };
}
因为该友元接受类类型实参并且与类隐式声明在同已命名空间中,所以使用它时可以无须使用显示命名空间限定符:
void f2(){
    A::C cobj;
    f(cobj);
}

(6)重载与命名空间
作为两个不同命名空间的成员的函数不能相互重载。

【1】候选函数与命名空间
当使用类类型作为实参调用函数时,为了确定候选函数,不但查找调用点所在作用域的同名函数,还查找定义形参类(及其基类)的每个命名空间,将那些命名空间中任意与被调用函数名字相同的函数加入候选集合,即使这些函数在调用点不可见。

例如,
namespace NS{
    class Item_base{ //TO DO...};
    void display(const Item_base&){ };
}

class Bulk_item : public NS::Item_base{ };

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

display函数的实参b具有类类型Bulk_item,display调用的候选函数不仅实在调用它的地方其声明可见的函数,还包括声明Bulk_item即Item_base的命名空间中的同名函数。因此NS::display(const Item_base&)被加到候选集合中。

【2】重载与using声明
当使用using声明引用命名空间中的一个函数名时,该声明包括重载函数的所以后版本。
如果using声明在已经有同名妾带有相同形参表的函数的作用域中引入函数,则using声明出错。否则,using定义给定名字的另一个重载实例,效果是增大候选函数集合。
【3】重载与using指示
using指示将命名空间成员提升到外围作用域,如果命名空间函数与命名空间所在作用域中声明的函数同名,就将命名空间成员加到重载集合中。
【4】跨越多个using指示的重载
如果存在多个using指示,则来自每个命名空间的名字成为候选集合的组成部分。

3.  多重继承与虚继承

(2)转换与多个基类
  在多重继承下,派生类的指针或者引用能转换为任意基类的指针或引用。这可能导致二义性:
假设类Video具有两个直接基类:Sound和Image,对于重载函数play
void play(const Sound&);
void play(const Image&);
使用Video的对象v调用play,play(v) 将导致编译错误

(4)多重继承下的类作用域
【1】多个基类可能导致二义性
假定类A是类A1,A2的派生类,A1,A2都定义了print成员函数,而A没有定义print,那么通过A的对象调用print将产生二义性,编译出错。如果在调用时指定A1::print还是A2::print,则可以避免二义性。
【2】二义性发生在名字查找的第一步:找到所有匹配的声明,第二步才是确定声明是否合法。因此,以下情形都不能避免二义性:
a. A1::print 和 A2::print具有不同形参表
b. A1::print是public成员,A2:print是private成员

【3】避免潜在二义性的最好方法:给派生类定义该函数的一个版本。

(5)虚继承

    虚继承是一种机制,类通过虚继承指出它希望共享其虚基类的状态。在虚继承下,对于给定虚基类,无论该类在派生层次中作为虚基类出现多少次,只继承一个共享的基类子对象。共享的基类子对象称为虚基类。
    通过在派生列表中包含关键字virtual设置虚基类:
class istream : public virtual ios{ ...}
class ostream : virtual public ios{ ...}

class iostream : public istream ,public ostream { ...} // iostream对象只拥有一个ios子对象

(7)特殊的初始化语义
    通常,每个类只初始化自己的直接基类。这一规则若应用到虚基类,则可能导致虚基类被重复初始化。因此,在虚派生中,由最底层的派生类的构造函数初始化虚基类。
    虽然由最底层派生类初始化虚基类,但是任何直接或间接继承虚基类的类一般也必须为该虚基类提供自己的初始化式。只要可以创建虚基类派生类类型的对象,该类就必须初始化自己的虚基类,这些初始化式只在创建中间类型的对象时使用。
阅读(383) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~