cPP语言的设计和演化

1 The Design and Evolution of C++

2.3 类
2.5 连接模型
2.7 为什么是C
2.3 类

2.3 类

1.“既然C++里的class意味着用户定义类型,为什么我不直接称它为type呢?选择用class这个词的基本原因是我不想发明新术语。”(p.27)

2.“像C语言中一样,对象的分配可以有3种方式:在堆栈上(作为自动对象),在固定地址(静态对象),或者在自由存储区(在堆,或者说是动态存储区)。与C语言不同的是,C with Classes 为自由存储的分配和释放提供了特定的运算符new和delete。”(p.27)

[注释1]new 和 delete现在仍然是c++自由存储的分配和释放运算符

2.5 连接模型

1.“C++的this指针是Simula里THIS引用的翻版。有时人们会问这个问题:为什么this是一个指针而不是一个引用?为什么它被称为this而不是self?当this被引进C with Classes时,在这个语言中还根本没有引用机制。对于后一个问题,C++是从Simula而不是从Smalltalk那里借用的术语。”(p.34)

2.7 为什么是C

1.C的一级优点:

[1] C是很灵活的:可以把C用到几乎所有的应用领域,可以在C中使用几乎所以后的程序设计技术。这个语言没有什么内在限制,不排斥在其中写出任何特殊种类的程序。

[2] C是高效的:C的语义是‘低级的’,也就是说,C语言的基本概念直接反映了传统计算机的基本概念。因此,如果想让一个编译器和/或一个程序员去有效地使用硬件资源,使用C语言相对来说更容易一些。

[3] C是可用的:有了一台计算机,无论是最小的微型机还是最大的超级计算机,情况都一样,即那里有可用的、具有可接收质量的C 编译器,而且这个编译器能支持比较完全(可以接受)的C语言和库,并基本上符合标准。还有一些可以用的库和工具,这就程序员不需要从零去设计一个新系统。

[4] C是可移植的:C程序通常不能自动地从一种机器(或者一个操作系统)移植到另一种机器上,这种移植通常也不是很容易做的事情。但是不管怎么说,这件事通常是可以做的,即使软件的某些重要部分具有内在的机器依赖性,其移植工作困难的程度(从技术的角度或经济的角度)都是可以接受的。

与这些一级的优点相比,那些二级的缺陷,例如C语言的古怪语法、某些语言结构缺乏安全性等,都变得不那么重要了。(p.38)

2. “一个程序设计语言要服务于两种目的:它为程序员提供了一种载体,使他们能描述需要执行的动作:它还要提供一组概念,供程序员借助它们去思考什么东西是能做的。第一方面的理想是要求一种‘接近机器’的语言,使机器的所有重要方面都能简单而有效地处理,而且是以某种程序员比较容易看清楚的方式。C语言的设计主要就是遵循了这种想法。第二方面的理想是一种‘接近需要解决的问题’的语言,这将使求解领域的概念可以直接而简洁描述。加入C中,从而使创造出C++的那些机制的设计着眼点也就在这个方面。”(p.39)

2 The Design and Evolution of C++

3.5虚函数
3.5.1 对象布局模型
3.8 常量
3.11 次要特征
3.11.3 限定

3.5虚函数

3.5.1 对象布局模型

1.“把在一个类中定义的虚函数集合定义为一个指向函数的指针数组,这样,对一个虚函数的调用,也就变成了通过该数组而做的一些简单间接函数调用。对于每一个包含虚函数的类,都存在这样的一个数组,通常称为虚函数表或者vtbl。而这些类的每个对象都包含一个隐式指针,一般称为vprt,它指向该对象的类的虚函数表。”(p.66)

class A {
    int a;
public:
    virtual void f();
    virtual void g(int);
    virtual void h(double);
};
class B :public A {
public:
    int b;
    void g(int); //overrides A::g()
    virtual void m(B*)
};
class C : public B {
public:
    int c;
    void h(double)j; //overrides A::h()
    virtual void n(C*);
};

类C的一个对象看起来大概是这样:

3.8 常量

1.“在操作系统中,常常能见到人们用两个二进制位直接或间接地对一块存储区进行访问控制,其中用一个位指明某个用户能否在这里写,另一个指明该用户能否从这里读。我举得这种思想可以直接用到C++中,因此也考虑过允许把一个类型描述为readonly或者writeonly。”(p.78)

2.“直到现在,在C语言中还不能规定一个数据元素是只读的,也就是说,它的值必须保持不变。也没有任何办法去限制函数对传给它的参数的使用方式…readonly运算符可用于防止对某些位置的更新。它说明,对于所有访问这个位置的合法手段而言,只有那些不改变存储在这里的值的手段才是真正合法的。”(p.78)

3.“我在离开这次会议时得到的是同意(通过投票)把readonly引进C语言(而不是C with Classes 或者C++),但把它另外命名为了const。”(p.79)

4.“const来表示常数,可以成为宏的一种有用替代物,但要求全局const隐含地只在它所在的编译单元起作用。因为只有在这种情况下,编译器才能很容易推导出这些东西的值确实没有改变…把简单的const用到常量表达式中,并可以避免为这些常量分配空间。”(p.79)

[注释1]常量表达式中的值不必为其分配内存空间,与程序代码存储在一起。

5.“因为C语言中的const不能用在常量表达式中,这就使const在C语言中远没有在C++中那样有用。”(p.80)

3.11 次要特征

3.11.3 限定

1.“C++引进::表示类的成员关系,而将.保留专用于对象的成员关系,e.g. object.member,class::member。”(p.84)

3 The Design and Evolution of C++

8.2 C++库设计
8.2.1 库设计的折中
9.2 回顾
9.2.2 C++是不是一种统一的语言?
9.2.2.2 什么东西本应该排除在外?
12.2 普通基类
12.3 虚基类
13.2 抽象类
13.2.1 为处理错误而用的抽象类
13.2.2 抽象类型
8.2 C++库设计
8.2.1 库设计的折中

8.2 C++库设计

1.“程序员经常把注意力集中在语言特征上:我要不要使用inline函数?虚函数?多重继承?单根层次结构?抽象类?重载函数?这种关注根本就是错的。这些语言特征的存在只是为了支持更具体本质的折中…”(p.163)

[注释1] 特征是为设计服务的,先设计框架,再根据框架需要选择最合适的特征服务。不要为用特征而用特征,本末倒置。

2.“因为fprintf()实际上依赖于不经检查的参数,这些参数是根据格式串在运行时处理的,这样做当然不是类型安全的,还有‘当x具有像complex那样的用户定义类型时,就没有能‘使printf()理解’的方便方式(例如%s和%d)去描述x的输出格式。’”(p.166)

[注释2] C++不用C的printf()函数主要是因为有了用户自定义的类型,printf()无法理解这样的类型格式。

9.2 回顾

9.2.2 C++是不是一种统一的语言?

9.2.2.2 什么东西本应该排除在外?

1.“C++规模很大的基本原因在于它要支持以不止一种方式、不止一种程序设计范型去写程序。从某种观点看,C++实际上是将3个语言合为一体。

· 一个类似C的语言(支持低级程序设计)。

· 一个类似Ada的语言(支持抽象数据类型程序的设计)。

· 一个类似Simula的语言(支持面向对象的程序设计)。

· 将上述特征综合成一个有机整体所需要的东西。

我们当然可以在一个类似C语言里按照这些风格之中的任何一个来写程序,但是C没提供对数据抽象或面向对象程序设计的直接支持。而在另一方面,C++则是直接地支持了所有这些不同的方式。”(p.178~179)

[注释3] C++,语言集大成者,集少林、武当、峨眉等多派功夫于一身。

2.“选择适当的设计始终是一个问题。但是在大多数语言里,语言的设计者都已经为你做了选择。对于C++我就没有这么做,而是把选择的权力交给了你。对那些相信有唯一一种做事情的正确风格的人而言,这种灵活性自然是很讨厌的。这样做也会吓走了一些初学者和老师,他们可能觉得一个好语言就是那种在一个星期就能完全理解的东西。C++不是那样的一种语言,它的设计就是为了给专业人员提供一个工具箱。抱怨在这里的特征太多了,就像是一个‘门外汉’在窥视了一个室内装修工的工具箱之后,抱怨说根本不可能需要这么多种小锤子一样。”(p.179)

12.2 普通基类

1.“考虑多重继承最原始、最根本的原因很简单,就是想两个类能够以这种方式组合起来,使结果产生的类的对象的行为就像两个类中任何一个的对象…”(p.230)

12.3 虚基类

1.“一个类可能在一个继承的DAG(Directed Acyclic Graph,有向无环图)里出现不止一次:

class task : public link {};
class displayed : public link {};
class displayed_task : public displayed, public task {};

在这种情况下,类displayed_task的对象中就会出现两个link类的对象:task::link和displayed::link。从图形上可以看出,表示displayed_task所需要的子对象是:

…按照默认方式,一个基类出现两次将会用两个子对象表示。”(p.231)

2.“…必须有一种非方式去描述一个基类只能在最终派生的类里有一个对象,即使它曾经作为基类出现过多次。为了将这种应用与独立的多重继承分开,这样的基类就应该描述为虚的:

class AW : public virtual W {};
class BW : public virtual W {};
class CW : public AW, public BW {};

在类AW和BW之间共享了类W的唯一的一个对象;也就是说,作为由AW和BW派生CW的结果,在CW里只有一个W对象。除了只在派生类里提供一个对象之外,virtual基类在其他方面与非基类完全一样。

类W的‘虚’是AW和BW描述的那些派生的一种性质,而不是W自身的一种性质。在一个继承DAG里,每个virtual基类总表示同一个对象。

从图形上看:

(p.231~232)

3.“抽象类和虚基类的组合就是为了支持一种程序设计风格,它大致对应于某些Lisp系统里所是使用的混合子风格。这种风格就是,用一个抽象类来定义接口,而用若干个派生类提供实现。每个派生类(每个混合子)为完整的类(混合体,多继承)提供某些东西。按照可靠的报道,术语混合子源自MIT附近某一家冰激凌店,他们将坚果、葡萄干、胶糖块、小甜饼等加入冰激凌中。”(p.233)

13.2 抽象类

13.2.1 为处理错误而用的抽象类

1.“考虑经典的有关‘形状’的例子。在这里我们必须首先声明一个类shape,以便表示形状的最一般概念,在这个类里需要两个虚函数rotate()和draw()。很自然,根本就没有shape类的对象,只会有特殊形状的对象。”(p.248)

2.“C++规则规定虚函数,例如rotate()和draw()等,都必须在它们最先声明的类里定义。提出这个要求是为了保证传统的连接程序可以用于C++程序的连接,并保证不会出现调用无定义虚函数的情况…

class shape
{
    point center;
    color col;
public:
    point where() { return center;}
    void move(point p) { center=p; draw();}
    virtual void rotate(int)
        { error("cannot rotate"); abort();}
    virtual void draw()
        { error("cannot draw"); abort();}
};

这就保证了对一些情况将产生运行错误,例如在一个由shape派生的类里忘记定义draw()函数的简单错误,或者想建立一个‘普遍的’shape对象去使用它的愚蠢错误。即使没有这些错误,存储器中也可能到处散布着像shape这样的类产生的不必要的虚函数表(注意:有含虚函数类的定义,就会有相应的虚函数表),或者像rotate()和draw()这样永远都不会调用的函数。这种东西造成的开销也可能变得很可观。

解决的方案就是允许用户简单地说某个虚函数并没有定义,也就是说,它是一个‘纯的虚函数’(即无定义的虚函数)。要做到这点只需要写初始式=0:

class shape
{    
    point center;
    color col;
public:
    point where() {return center;}
    void move(point p) {center=point; draw();}
    virtual void rotate(int) = 0; //pure virtual
    virtual void draw() = 0;      //pure virtual
};

带有一个或几个纯虚函数的类是一个抽象类。抽象类只能用做其他类的基类,特别是不可能建立抽象类的对象。从一个抽象类派生的类必须或者给基类的纯虚函数提供定义,或者重新将它们定义为纯虚函数。”(p.248~249)

13.2.2 抽象类型

1.“如果你不希望某个表示出现在一个类里,那么就不要把它放到那里!请换个方式,把有关表示的描述推迟到某个派生类中。”(p.249)

2.“可以像下面定义T指针的set(集合):

class set
{
public:
    virtual void insert(T*) = 0;
    virtual void remove(T*) = 0;
    virtual int is_member(T*) = 0;
    virtual T* first() = 0;
    virtual T* next() = 0;
    virtual ~set() {}
};

…在这个上下文中并没有包含任何有关表示或者实现的细节。只有实际建立set对象的人才需要知道set是如何表示的。例如,给了:

class slist_set : public set, private slist
{
    slink* current_elem;
public:
    void insert(T*);
    void remove(T*);
    
    int is_memeber(T*);
    T* first();
    T* next();
    slist_set() : slist(), current_elem(0) {}
};

而后我们就可以建立slist_set的对象,它可以由那些根本不知道slist_set类的set用户们使用。例如:

void user1(set& s)
{
    for(T* p=s.first(); p; p=s.next())
    {
        //use p
    }
}
void user2()
{
    slist_set ss;
    //...
    user1(ss);
};

最重要的是,抽象类set类的一个用户,例如user1(),可以在不必包含定义了slist_set的头文件和如slist_set这样的类定义的情况下编译(包含set的头文件即可,大大减少编译量),而实际上它依赖那些东西。

如上所述,编译时将能捕捉到所有建立抽象类对象的企图。例如:

void f(set& s1)  //fine
{
    set s2;       //error:declaration of object of abstract class set.
    set* p = 0;   //fine
    set& s3 = s1; //fine
}

…一个抽象类就是一个纯粹的接口,对应的实现通过由它派生的类提供。这样就可以限制在做了修改后需要重新编译的范围,也可以限制编译一部分代码所需要的信息量。”(p.249~250)

4 The Design and Evolution of C++

13.2 抽象类
13.2.4 虚函数和构造函数
13.2.4.2 基类优先的构造
13.3 const成员函数
13.3.3 可变性与强制
13.4 静态成员函数
14.2.5 typeid()运算符
14.2.5.1 类type_info
15.1 引言
15.3 类模板
13.2 抽象类
13.2.4 虚函数和构造函数
13.2.4.2 基类优先的构造

1.“构造函数就是要建立起一个环境,使其他成员函数在其中操作。”(p.251~252)

2.“考虑下面这个可能引起混乱的例子:

class B 
{
public:
    int b;
    virtual void f();
    void g();
    //...
    B();
};
class D: public B
{
public:
    X x;
    void f();
    //...
    D();
};
B::B()
{
    b++; //undefined: B::b isn't yet initialized.
    f(); //calls: B::f(); not D::f().
}

如果你真的希望调用B自己的f(),那就应该将它明确地写成B::f()。

这个构造函数的行为方式与写常规成员函数可能的方式成鲜明的对比,因为常规成员函数可以依靠构造函数的正确行为:

void B::g()
{
    b++; //fine, since B::b is a member, B::B should have initialized it.
    f(); //calls: D::f() if B::g() is called for a D.
}

当一个调用出自某个D的B部分时,在B::B()和B::g()里的f()调用的是不同函数。”(p.252)

3.“如果让构造函数去调用覆盖函数,构造函数的用途将受到严重的限制,以至于我们根本无法合理地编写覆盖函数了。

在这里,基本的设计要点是,直到对一个对象的构造函数的运行结束之前,这个对象就一直像一个正在建造之中的建筑物:你必须忍受结构没有完工所带来的各种不便,常常需要依靠临时性的脚手架,必须时时当心在与危险环境相处的各种问题。一旦构造函数返回,编译程序和用户就都可以假定构造完成的对象能使用了。”(p.253)

13.3 const成员函数

1.“我们需要一种方法,使程序员可以说明哪些成员函数将更新其对象的状态,而哪些并不更新

class X    {
    int aa;
public:
    void update() { aa++; }
    int value() const {return aa;}
    void cheat() const { aa++; } //error: *this is const
}

声明为const的成员函数,如X::value(),被称为const成员函数,并保证不会修改对象的值。const成员函数可以用于const对象和非const对象,而非const成员函数,如X::update(),就只能用于非const对象…从技术上说,得到这种行为的方式就是要求X的非const成员函数里的this指针指向X,而让其const成员函数里的this指针只能指向const X。

(p.253~254)

2.“将一个对象声明为const,就是认为它具有从其构造函数完成到析构函数的开始之间的不变性。”(p.255)

13.3.3 可变性与强制

1.“有些人还是特别讨厌强制去掉const,因为它是一个强制,甚至更因为这种东西并不保证对所有情况都能工作…应该能描述一种绝不应该被认为是const的成员,即使它是某个const对象的成员时也是这样…初始建议提出用‘~const’作为‘绝不能是const’的记法。甚至整个概念的一些拥护者也认为这个记法太难看,所以把关键字mutable引进建议里,被ANSI/ISO委员会接受:

class XXX {
    int a;
    mutable int cnt;   // cnt will never be const
public:
    int f() const { cnt++; return a;}
    //...
};
    XXX var;        // var.cnt is writable (of course)
    const XXX cnst; // cnst.cnt is writable because XXX::cnt is declared mutable

(p.255~256)

2.“类的static数据成员是这样的一种成员,它只存在一个唯一的备份,而不像其他成员那样在每个对象中各有一个备份。因此,不需要引用特定对象就可以访问static成员。static成员可用于减少全局名称的数量,并且能把某个static成员在逻辑上属于哪个类的问题表述明确,还能实现对这些名称的访问控制。这种特性对于库的提供商都是非常重要的,因为它能够防止对全局名称空间的污染,并可以简化库代码的书写,也使同时使用多个库变得更加安全。”(p.256)

13.4 静态成员函数

1.“static成员函数并不关联任何特定对象,因而不需要用特定成员函数的语法进行调用。”(p.257)

2.“在某些情况下,类被简单地当作一种作用域来使用,把不放进来就是全局的名称放入其中,作为它的static成员,可以使这些名称不会污染全局的名称空间。”(p.257)

14.2.5 typeid()运算符

1.“可能需要确定一个对象的确切类型。也就是说,告诉说这个对象就是X类的对象,而不是只说,它是X类的或者某个由X类派生的类的对象。dynamic_cast做的是后一件事情。”(p.281)

2.“人们希望知道一个对象的确切类型,通常是他们因为想对这个对象的整体执行某种标准服务。”(p.281)

14.2.5.1 类type_info

1.“函数before()是为了使type_info信息能够排序,以便能通过散列表等方式访问它们。由before()定义的顺序关系和继承关系之间没有任何联系。进一步说,对不同的程序或者同一个程序的不同运行,我们都不能保证before()能产生同样的结果。在这个方面,before()与取地址运算符类似。”(p.282)

15.1 引言

1.“模板概念植根于对描述参数化容器类的愿望:异常来自于渴望为运行时错误的处理提供一种标准化方式。”(p.298)

15.3 类模板

1.“一个C++的参数化类型被称为一个类模板。类模板描述了可以如何构造出一些个别的类,其方式很像在类里描述如何构造起个别的对象。一个向量的模板类可以像下面这样声明:

template<class T> 
class vector {
    T* V;
    int sz;
public:
    vector(int);
    T& operator[](int);
    T& elem(int i){return v[i];}
};

前缀template说明了这里声明的是一个模板,它有一个类型为T的参数类型将在声明中使用。将其引入后,在模板的作用域里,T就可以像其他类型名称一样使用了。向量模板可以像下面这样引用:

vector<int> v1(20);
vector<complex> v2(30);
typedef vector<complex> cvec;  // make cvec a synonym for vector<complex>.
cvec v3(40);                   // v2 and v3 are of the same type.
void f()
{
    v1[3]=7;
    v2[3]=v3.elem(4)=complex(7,8);
}

与类的声明相比,声明一个类模板并不复杂多少。关键字class用于指明类型参数的类型部分,一是因为它以很清楚的词的形式出现;二是因为这样可以节约一个关键字。在这个上下文环境里,class的意思是‘任意类型’,而不仅是‘某种用户定义类型’。

在这里使用尖括号<…>而不使用圆括号(…),是为了强调模板参数具有不同的性质(它们将在编译时求值),也因为圆括号在C++里已经过度使用了。

引进关键字template使模板声明很容易看清楚,无论是对人,还是对工具,同时也为模板类和模板函数提供了一种共有的语法形式。

模板是为生成类型提供的一种机制。它们本身并不是类型,也没有运行时的表示形式,因此它们对于对象的布局没有任何影响。”(p.301-302)

2.“除了类型参数之外,C++也允许非类型的模板参数。这种机制基本上被看做是为容器类提供大小和限界所需的信息。例如:

template<class T, int i>
class Buffer{
    T v[i];
    int sz;
public:
    Buffer():sz(i) {}
    //...
};

在那些运行时间、效率和紧凑性非常紧要的地方,为了能与C语言的数组和结构竞争,这样的模板就非常重要了。传递大小信息允许实现者不使用自由空间。”(p.303)

3.“在模板的初始设计中,不允许用名称空间或模板作为模板的参数,这一限制是过于谨慎的又一案例。我现在想不出任何理由去禁止这种参数,它们无疑是很有用的。以类模板作为模板参数,已在1994年3月圣迭戈会议上获得通过。”(p.303)

5 The Design and Evolution of C++

15.6 函数模板
15.10 模板的实例化
15.10.4 查找模板定义
15.11.1 实现与界面的分离
15.11.3 对C++其他部分的影响
16.2 目标和假设
16.3 语法
16.5 资源管理
16.6 唤醒与终止
15.6 函数模板

1.“之所以引进函数模板,一是因为我们已经很清楚,需要有模板类的成员函数;二是因为如果没有这种东西,模板的概念看起来就不够完全。

//declaration of a template function:
template<class T> void sort(vector<T>&);
void f(vector<int>& vi, vector<String>& vs)
{
    sort(vi);  //sort(vector<int>& v);
    sort(vs);  //sort(vector<String>& v);
}
//definition of a template function:
template<class T> void sort(vector<T>& v)
/*
    Sort the elements into increasing order
    Algorithm: bubble sort (inefficient and obvious)
*/
{
    unsigned int n=v.size();
    
    for(int i=0; i<n-1; i++)
        for(int j=n-1; i<j; j--)
            if(v[j] < v[j-1]) {// swap v[j] and v[j-1]
                T temp = v[j];
                v[j] = v[j-1];
                v[j-1] = temp;
            }
}

(p.307~308)

2.“使用尖括号<…>而不用圆括号,是因为用户发现这样写法更容易读,也因为圆括号在C和C++中已经使用过度。”(p.314)

3.“一般说,从模板类出发的派生提供了一种可能性,使人可以剪裁基类的信息以适应派生类的需要。这提供了一些特别有力的组合模式,例如:

template<class T> class Base {/*...*/};
class Derived : public Base<Derived> {/*...*/};

通过这种技术,我们可以将派生类的信息嵌入基类之中。

(p.315~316)

15.10 模板的实例化

1.“在连接程序运行时,如果缺少了某些模板函数实例,它就会调用编译程序,从模板的源代码生成所缺的目标代码。这个过程反复进行,直到所有模板都完成了实例化。”(p.322~323)

2.“实例化请求具有如下形式:

template class vector<int>;                       //class
template int& vector<int>::operator[](int);       //member
template int convert<int, double>(double);        //function

我们将关键字template又一次用到了这里,因为不希望引进新关键字instantiate。模板声明与实例化请求是很容易区分的,模板定义的开始总是列出模板参数表template<,仅有template就表示是实例化请求。

(p.324)

15.10.4 查找模板定义

1.“按照传统,C++程序和C程序一样都是由一组文件构成的。这些文件被组合进一个个编译单元,许多文件依据规定被编译后连接到程序中。例如,.c文件是源文件,它们通过包含.h文件以获得程序其他部分的信息。编译程序从一些.c出发去生成目标文件,常常称为.o文件。程序的可执行版本就是简单地通过连接这些.o而得到的。档案(archive)和动态连接库使问题进一步复杂化了,但是并没有改变这个整体画面。

模板并不能很好地放进这个画面中,这也正是与模板实现有关的许多问题的根源。从一方面看,一个模板不仅仅是一段源代码(通过模板实例化产生的更像传统意义上的源代码),一些模板定义也不像是属于.c文件;从另一方面看,模板并不正好就是类型和界面信息,因此它们也不像是属于.h文件。”(p.331~332)

15.11.1 实现与界面的分离

1.“模板机制完全是编译时和连接时的机制,模板机制的任何部分都不需要运行时支持。这当然是经过深思熟虑的,但也遗留下一个问题:如何让从模板产生的(实例化出来的)类和函数能够依靠那些只有到了运行时才能知道的信息?与C++的其他地方一样,回答是使用虚函数。”(p.334)

15.11.3 对C++其他部分的影响

1.“一个模板参数可以是内部类型的,也可以是用户定义类型的。这就产生了一种持续的压力,要求用户定义类型无论是在外观上,还是在行为上都要尽可能与内部类型相仿。可惜,用户定义类型和内部类型不可能具有完全一样的行为,因为无法做到清除C语言内部类型的不规范性,而又不严重地影响与C语言的兼容性。在许多相对较次要的方面,内部类型也从模板带来的进步中有所获益。”(p.335)

16 异常处理

16.2 目标和假设

1.“要想提供一些功能使一个单独的程序就能从所有错误中恢复,这完全是一种误导,也会使错误处理策略变得非常复杂,其本身还会成为新的错误根源。”(p.339)

16.3 语法

1.

int f()
{
    try{            // start of try block
        return g();
    }
    catch(xxii){    // start of exception handler
                    // we get here only if 'xxii' occurs
            error("g() goofed:xxii");
            return 22;
    }
}
int g()
{
    try{            // start of try block
        return g();
    }
    catch(xxii){    // start of exception handler
                    // we get here only if 'xxii' occurs
        error("g() goofed: xxii");
        return 22;
    }
}
int g()
{
    //...
    if(something_wrong) throw xxii();    //throw exception
    //...
}

这里的try关键字完全是多余的,那些花括号{}也是一样的,除了真正需要在try块中或异常处理器中使用多个语句的情况之外…我曾经试者用catch表示抛出和捕捉两种操作,这完全可以做得符合逻辑,也具有内在的一致性,但我在给人们解释这种模式时却没有成功。

选择关键字throw,部分原因是比它更鲜明的词,例如raise和signal,都已经被C语言的标准库函数使用了。”(p.340)

16.5 资源管理

1.“资源应该以它们被分配的相反顺序进行释放,在典型情况下,这一点是非常重要的。这与局部对象通过构造函数创建,而后由析构函数销毁的行为方式极其相似。因此,我们可以将资源的请求和释放问题用一个带有构造函数和析构函数的类的对象来适当处理。”(p.342)

16.6 唤醒与终止

1.“在异常处理机制的设计期间,引起最大争议的问题是它究竟应该支持哪种语义,是终止语义还是唤醒语义。也就是说,异常处理器是否应该能提出请求,要求从异常的抛出点重新唤醒程序的执行…在后来的4年里我学到了许多其他东西,因此,C++的异常处理机制采纳了相反的观点,通常被称为‘终止模型’。”(p.344)

2.“终止比唤醒更好,这不是一种观点的问题,而是许多年的经验。唤醒是非常诱人的,却是站不住脚的。”(p.345)

6 The Design and Evolution of C++

17.1 引言
17.4 一个解决方案:名称空间
17.4.3 名称空间的别名
17.4.5.1 方便性与安全性
17.4.5.2 全局作用域
17.4.5.3 重载
17.4.5.5 名称空间是开放的
17.5.1 派生类
17.5.3 清除全局的static
17.6 与C语言的兼容性
18 Cpp

17.1 引言

1.“对所有不适合放进某个函数、某个struct或者某个编译单元的名称,C语言提供了一个统一的全局性的名称空间,这就带来了名称的冲突问题…在我设计类型安全的连接机制时…我注意到,对以下形式:

extern "C" {/*...*/}

的语法、语义和实现技术稍微做些修改,就能允许我们用以下形式:

extern XXX {/*...*/}

表示在XXX中声明的名称位于一个分离的作用域XXX中,要从其他作用域中访问就需要用限定的XXX::形式,和在类之外访问静态类成员完全一样。

(p.351)

17.4 一个解决方案:名称空间

1.“…访问名称空间成员采用访问类成员的传统记法:namespace_name::member_name。事实上,类作用域可以看成是名称空间作用域的一种特殊情况。”(p.355)

2.“考虑:

namespace A {
    void f(int);
    void f(char);
    class String {/*...*/};
}

在名称空间括号中声明的名称就是在名称空间A内部的内容,它们不会与全局的名称或者其他任何名称空间中的名称冲突。名称空间声明(包括定义)与全局声明具有完全相同的语义,只是它们名称的作用域被限制在名称空间内部。

程序员可以通过显示限定的形式直接使用这些名称:

A::String s1="Annemarie";
void g1()
{
    A::f(1);
}

换一种方式,我们也可以显示地让某个特定库中的个别名称不需要限定描述而直接使用,这通过使用声明完成。

using A::String;
String s2="Nicholas"; //meaning A::String
void g2()
{
    using A::f; //introduce local synonym for A's f
    f(2);       //meaning A::f
}

上述使用声明在一个局部作用域为它所提出的名称引进了一个同义词。

再换一种方式,我们也可以显式地要求一个特定库中所有的名称都直接被使用,不需要借助限定,这通过使用指示完成。

using namespace A;  //make all names from A accessible
String s3="Marian"; //meaning A::String
void g3()
{
    f(3);    //meaning A::f
}

上述使用指示并不为局部作用域引进任何新名称,而只是简单地使有关名称空间中的所有名称都变成可以访问的。”(p.356)

17.4.3 名称空间的别名

1.“可以通过为长的名称空间提供短的别名的方式来解决:

//use namespace alias to shorten names;
namespace ATT = American_Telephone_and_Telegraph;
ATT::String s3="asdf";
ATT::String s4="lkjh";

事实上,名称空间也能用于从多个名称空间组合出包含某些名称的界面。

namespace My_interface {
    using namespace American_Telephone_and_Telegraph;
    using My_own::String;
    using namespace OI;
    //resolve clash of definitions of 'Flags'
    //from OI and American_Telephone_and_Telegraph.
    typedef int Flags;
    //...
}

(p.359~360)

17.4.5.1 方便性与安全性

1.“使用声明向局部作用域中添加一些内容。使用指示则不添加任何内容,只是使一些名称能够被访问,例如:

namespace X {
    int i, j, k;
}
int k;
void f1()
{
    int i=0;
    using namespace X; //make names from X accessible
    i++;    //local i
    j++;    //X::j
    k++;    //error: X::k or global k?
    ::k++;  //the global k
    X::k++; //X's k
}
void f2()
{
    int i=0;
    using X::i; //error: i declared twice in f2()
    using X::j;
    using X::k; //hides global k
    i++;
    j++;    //X::j
    k++;    //X::k
}

这样也就维持了一种非常重要的性质: 局部声明的名称(无论是通过正常的局部声明所声明的,还是通过使用声明)都将遮蔽名称相同的非局部声明,而名称的任何非法重载都将在声明点被检查出来。

如上所示,在全局作用域中,在可访问方面并不给全局作用域任何超越名称空间的优先权,这也就为防止偶然的名称冲突提供了某种保护。

在另一方面,非局部的名称在它们的声明所在的上下文中查找和处理,就像其他非局部名称一样,特别是使用指示有关的错误都只在使用点检查…例如:

namespace A {
    int x;
}
namespace B {
    int x;
}
void f()
{
    using namespace A;
    using namespace B; //ok: no error here
    A::x++; //ok
    B::x++; //ok
    x++; //error: A::x or B::x ?
}

(p.362~363)

17.4.5.2 全局作用域

1.“::f的意思是‘在全局作用域中声明的那个f’,而X::f意味着‘在名称空间X中声明的那个f’。考虑:

int a;
void f()
{
    int a=0;
    a++;      //local a
    ::a++;    //global a
}

如果我们将它包裹到一个名称空间中,再加上另一个名称为a的变量,得到的是:

namespace X {
    int a;
    void f()
    {
        int a=0;
        a++;    //local a
        X::a++; //X::a
        ::a++;  //X::a or global a ? -- the global a!
    }
}

也就是说,用::限定意味着是‘全局’,而不是‘外面包裹的最近名称空间’。

请注意,使用指示并不在它所出现的作用域中声明任何名称:

namespace X {
    int a;
    int b;
    //...
}
using namespace X;  //make all names from X accessible
using X::b;         //declare local synonym for X::b
int i1=::a;         //error: no 'a' declared in global scope
int i2=::b;         //ok: find the local synonym for X::b

(p.363)

17.4.5.3 重载

1.“…依据常规的重载规则,允许跨名称空间的重载。考虑:

namespace A {
    void f(int);
    //...
}
using namespace A;
namespace B {
    void f(char);
    //...
}
using namespace B;
void g()
{
    f('a'); //calls B::f(char)
}

…在同一个作用域里显式地两次使用using namespace—对于新编写的软件,这是一种不应推荐的方式。

(p.364)

17.4.5.5 名称空间是开放的

1.“…可以在多个名称空间声明中将各种名称加进一个名称空间中。例如:

namespace A {
    int f(); //now A has member f()
};
namespace A {
    int g(); //now A has two members f() and g()
};

…允许名称空间的定义散布在多个头文件和源程序文件中。

(p.365~366)

17.5.1 派生类

1.“考虑一个老问题,一个类的成员将遮蔽其基类中具有同样名称的成员:

class B {
public:
    f(char);
};
class D: public B {
public:
    f(int);      //hides f(char)
};
void f(D& d)
{
    d.f('c');    //calls D::f(int)
}

…可以通过一个使用声明,将B的f()引入到作用域:

class B{
public:
    f(char);
};
class D: public B{
public:
    f(int);
    using B::f; //bring B::f into D to enable overloading
};
void f(D& d)
{
    d.f('c');  //calls D::f(char)!
}

(p.367)

17.5.3 清除全局的static

1.“允许匿名的名称空间:

#include <header.h>
namespace {
    int a;
    void f() {/*....*/}
    int g() {/*...*/}
}

除了不会与头文件中的名称产生重载之外,这种写法等价于:

#include <header.h>
static int a;
static void f() {/*...*/}
static int g() {/*...*/}<

简单说:

namespace {/*...*/}

等价于:

namespace unique_name {/*...*/}
using namespace unique_name;

在一个作用域中的各个匿名名称空间都共享同样的唯一名称。特别是,在一个编译单元中所有的全局匿名名称空间都是同一个名称空间的一部分,而且它们又与其他编译单元的匿名名称空间不同。

(p.369)

17.6 与C语言的兼容性

1.“具有C连接的函数也可以放进一个名称空间中:

namespace X {
    extern "C" void f(int);
    void g(int);
}

这就使具有C连接的函数使用就像是该名称空间的另一个成员。例如:

void h()
{
    X::f();
    X::g();
}

当然,在同一个程序中,不能在两个不同名称空间里存在两个同样名称的具有C连接的函数,因为它们将被解析到同一个C函数(重名错误)。C语言的不安全的连接规则将使这种错误很难发现。”(p.370)

18 Cpp C的预处理

1.“在C++从C语言那里继承来的功能、技术和思想中也包括C的预处理程序–Cpp。我原来就不喜欢Cpp,现在也不喜欢它…C++为#define的主要应用提供了const、inline、template、namespace等替换方式…C++没有为#include提供替代形式…留下的使#ifdef和#pragma。没有#pragma我也能活,因为我还没有见过一个自己喜欢的#pragma…我很愿意看到Cpp被废除。”(p.371~374)

本页共738段,21616个字符,40940 Byte(字节)