目录

C++语言的设计与演化

本文是C++语言之父的《C++语言的设计与演化》一书的摘录笔记。

第1章 C++的史前时代

Simula

Simula的类能以协程co-routine的方式活动,很容易要求类的一个对象,与该类的其他对象以伪并行的方式工作。

类的层次结构,可用于表述应用中的各种分层概念。

Simula的类型系统的表达能力,以及编译系统捕捉类型错误的能力非常好。

整个程序的活动更像许多小的程序的组合,而不像一个整体的大程序,因而更容易写,更容易理解,也更容易排除其中的错误。

没有合适工具的条件下,不要去冲击一个问题。

C++的保护模型来自于访问权限许可、转让的概念;初始化与赋值的区分来自于对转让能力的思考;const概念从读写保护机制中演化出来的;异常处理来自有关容错系统的设计经历。

一个系统的结构反映了创建它的那个组织的结构,当一个系统基本上是一个人的工作时,它就应该反映这个人的个人观点。

尊重人群而不尊重人群中的个体,实际上就是什么也不尊重。C++的许多决策根源于我对强迫人按照某种特定的方式行事的极度厌恶。

特别的,我绝不想人作为思想的牺牲品,不想通过一种有局限性的语言定义,去推行某种唯一的设计理念。

第2章 带类的C

当需要这类支持时,就应该通过库或者特定的扩充来支持。语言只负责提供对程序组织的一般性机制,而不应该去支持特定的应用领域。所以C++不在内部提供复数、字符串、或者矩阵类型,也不对并行性、持续性、分布式计算、模式匹配、文件系统操作等提供直接的支持。

带类的C不应该为了去掉C语言的 危险 或 丑陋 特性而付出效益方面的代价。

一个类Class就是一个类型。

在编译时,实现对成员的访问控制。

对于函数成员:要描述其返回类型、参数类型,类型检查机制需要这样的描述。

类声明、函数声明一般写在.h文件里供调用方参考,它们的定义实现放在.cpp中。这也意味着更容易进行分开编译。

与C一样,可以在栈、堆中分配对象。与C语言不同的是,为堆中的对象提供了newdelete等内置的操作。

法则:用户自定义类型 和 内置类型,与语言法则的关系应该是一样的,即有相同的创建规则和作用域规则。

inline提示编译器,将函数在调用处展开,而不是频繁调用,这提升了程序运行时的效率。

struct A { int x, y; };
struct B { int x, y; };

按名字等价是C++类型系统的基石,而内存布局的相似性则保证了可以使用显式类型转换,以便能提供低级的转换服务。

在C++语言中,每个函数、变量、类型、常量等都应该恰好只有一个定义。

Simula提供了类、Algol 68提供了运算符重载、引用、以及在块里任何地方声明变量的能力、Ada提供了模板、异常。

运行时的保证

构建函数、析构函数是由编译程序识别和调用的,基本想法是让程序员能够建立起一种保证,以便其他成员函数都能够依赖这种保证。也被称为 不变式

  • 构建函数:建立起其他成员函数进行操作的环境基础。

  • 析构函数: 销毁这个环境,并释放它以前获得的所有资源。

允许定义多个构建函数很有实用价值,这也是C++重载机制的一个重要应用方面。

赋值的重载

=赋值的含义在C语言里语意是按位复制,对于自定义的类来说,肯定是不正确的。这种默认的赋值语义,实际上只是一种共享表示,而不是真正的副本。我对这个情况的反应,就是允许程序员自己描述类的复制意义。

可以通过重载运算符解决:

void X::operator=( Class X from ) { ... }

上面这种方式被认为是低效率的,这就引入了复制构建函数。

C++的诞生

C++出现的时间点。

WX20190213-154850.png

C++的用户是哪些人?

在带类的C的基础上引入了一些新特征,从而形成了C++。

  • 虚函数

  • 函数名和运算符重载

  • 引用

  • 常量

  • 用户可控制的自由空间存储区控制

  • 改进的类型检查

虚函数

一个抽象数据类型定义了一个黑盒子,为了添加某些新的用途,必须修改它的定义才能实现。

enum kind { circle, triangle, square };

class shape{
    point center;
    color col;
    kind k; // k 是必须的,以便draw函数能确定在处理什么形状

    public :
        point where(){return center;}
        void draw();
}

void shape::draw()
{
    switch( k )
    {
        case circle:
            // draw a circle
            break;
        case triangle:
            // draw a triangle
            break;
    }
}

像这样,draw这样的函数必须理解现存的所有形状。这样,每当有一种新形状被加入系统,所有这类函数的代码都必须扩充。如果你无法接触所有这些操作的源代码,就无法将一个新的形状加入到系统中。

Simula的继承机制提供了一种解决方案:

  • 首先需要提供一个类,用它定义所有形状的普遍性质

  • 对那些必须针对每个特定的类型,专门实现的函数,标明为virtual

class shape{
    point center;
    color col;
    public:
        point where() { return center; };
        virtual void draw();
}
  • 如果要定义一个特定类型,我们必须先说明它是一个形状,然后再描述它的特殊类型。circle被说成由基类shape派生的。

class circle : public shape{
    int radius;
    public:
        void draw(){ ... };
        void rotate(int) {};
}

对每个有虚函数的类都存在一个这样的数组,一般称为虚函数表vtbl。这些类的每个对象都包含一个隐藏指针vprt,指向该对象的类的虚函数表。

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); // 覆盖 A::g()
        virtual void m(B*);
}
class C : public B{
    public:
        int c;
        void h(double); // 覆盖 A::h()
        virtual void n(C*);
}

void f( C* p )
{
    p -> g(2);
}

类C的一个对象是这样的:

WX20190321-193840.png

p->g(2) 被编译后,就成了(*(p->vptr[1])) ( p, 2 )这样的间接调用形式。

当初我没有把结构和类看成不同的概念。原因是我希望两者有一个统一的布局规则集合、一个统一的检索规则集合和一个统一的解析规则集合。我认为让structclass作为同一个概念比较好,struct 就是class

虚函数只能被派生类里的函数覆盖,覆盖的前提是要保持:同样的函数名、同样的参数和返回类型。

派生类里的名字将遮蔽基类中具有相同名字的任何对象或函数,这是由作用域规则推理而来的。

一种特征能够怎样被用好,比它可能怎样被用错更重要。

基本重载

class complex{
    double re, im;
    public:
        friend complex operator+( complex, complex );
}

complex z3 = z1 + z2; // operator+( z1, z2 )

重载使简单的复数表达式可以被解析为函数调用。