C++语言的设计与演化
本文是C++语言之父的《C++语言的设计与演化》一书的摘录笔记。
第1章 C++的史前时代
Simula
Simula
的类能以协程co-routine
的方式活动,很容易要求类的一个对象,与该类的其他对象以伪并行
的方式工作。
类的层次结构,可用于表述应用中的各种分层概念。
Simula
的类型系统的表达能力,以及编译系统捕捉类型错误的能力非常好。
整个程序的活动更像许多小的程序的组合,而不像一个整体的大程序,因而更容易写,更容易理解,也更容易排除其中的错误。
没有合适工具的条件下,不要去冲击一个问题。
C++的保护模型来自于访问权限许可、转让的概念;初始化与赋值的区分来自于对转让能力的思考;const
概念从读写保护机制中演化出来的;异常处理来自有关容错系统的设计经历。
一个系统的结构反映了创建它的那个组织的结构,当一个系统基本上是一个人的工作时,它就应该反映这个人的个人观点。
尊重人群而不尊重人群中的个体,实际上就是什么也不尊重。C++的许多决策根源于我对强迫人按照某种特定的方式行事的极度厌恶。
特别的,我绝不想人作为思想的牺牲品,不想通过一种有局限性的语言定义,去推行某种唯一的设计理念。
第2章 带类的C
当需要这类支持时,就应该通过库或者特定的扩充来支持。语言只负责提供对程序组织的一般性机制,而不应该去支持特定的应用领域。所以C++不在内部提供复数、字符串、或者矩阵类型,也不对并行性、持续性、分布式计算、模式匹配、文件系统操作等提供直接的支持。
带类的C不应该为了去掉C语言的 危险 或 丑陋 特性而付出效益方面的代价。
一个类Class就是一个类型。
在编译时,实现对成员的访问控制。
对于函数成员:要描述其返回类型、参数类型,类型检查机制需要这样的描述。
类声明、函数声明一般写在.h
文件里供调用方参考,它们的定义实现放在.cpp
中。这也意味着更容易进行分开编译。
与C一样,可以在栈、堆中分配对象。与C语言不同的是,为堆中的对象提供了new
与delete
等内置的操作。
法则:用户自定义类型 和 内置类型,与语言法则的关系应该是一样的,即有相同的创建规则和作用域规则。
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++出现的时间点。
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的一个对象是这样的:
p->g(2)
被编译后,就成了(*(p->vptr[1])) ( p, 2 )
这样的间接调用形式。
当初我没有把结构和类看成不同的概念。原因是我希望两者有一个统一的布局规则集合、一个统一的检索规则集合和一个统一的解析规则集合。我认为让struct
与class
作为同一个概念比较好,struct 就是class。
虚函数只能被派生类里的函数覆盖,覆盖的前提是要保持:同样的函数名、同样的参数和返回类型。
派生类里的名字将遮蔽基类中具有相同名字的任何对象或函数,这是由作用域规则推理而来的。
一种特征能够怎样被用好,比它可能怎样被用错更重要。
基本重载
class complex{
double re, im;
public:
friend complex operator+( complex, complex );
}
complex z3 = z1 + z2; // operator+( z1, z2 )
重载使简单的复数表达式可以被解析为函数调用。