CPP语言的历史和演化2

1 C++为何需要复制控制机制

像string和vector这种类,拥有指针成员。属于非平凡的类。

所谓的平凡的类就是所有成员都是值类型的,赋值就是拷贝副本,而不会有副作用。

像string这种类,有指针成员,拷贝就会导致乱套。

class string
{
public:
    int m_size;
    char* m_data;
};

这时候的普通拷贝会导致问题:

string a;
string b("123")
a = b;

两个对象的成员指针共同指向了同一块内存。不知道谁应该去释放这块内存,也不知道怎么保证不会重复释放。所以,为了解决这个问题,C++允许对类的赋值操作符进行重载,这是操作符重载的前身。也是C++遵从C语言值语义必然要遇到的问题。

Java解决这个问题是让所有对象的内存统一到堆上,而且不给程序员管理,让语言自己管理。所以提供了垃圾回收机制。

C++要兼容C(实际上is better C)就是想取代C。既然取代,就要接受兼容C的代码。过渡到C++,兼容C就必须要值语义,因为C是值语义。值语义就必然要遇到复制控制。只有这样才可以保证零开销。

就像经济学中的规则产生行为一样。

无论谁想定义一个更好的语言,都会产生新的问题,这是必然的。因为不可能存在完美的事物,你以为你超越了,其实只不过坑还没出现,而你制定的新规则就注定了有针对这种规则的“Bug”。只有事后诸葛亮,从来没听说过事前诸葛亮。

2 const和constexpr

这是一个非常有意思也非常有意义的题目。从这里我们可以看出很多东西,值得了解。

一开始C++标准是先有const的。后来才有了constexpr。单看这两个关键字的名字就很有意思。难道不多于吗?不多余。

首先,我们来看看const。const修饰一个变量,在这个变量被初始化之后,不可以改变它的值。虽然用const_cast也可以改,但至少这个明确麻烦的步骤就是在告诉开发人员这个变量我是不希望有人再去改变它的。

比如,

const int max_length = 255;               //编译时期赋值,赋值之后变量不可以再被改变
const int sz = get_current_buffer_size(); //运行时赋值,赋值之后变量不可以再被改变

以上我们关注的是一个变量,这个变量不管何时从哪里得到了一个值,一旦得到了,这个变量就一直是这个值了,不会再变了。这个需求是明确的,const也的确完成了它的使命。

但是,有时候呢,我们就是想从写代码的时候就要确定一个约定的值,这个值可能是我们根据业务场景定下来的,是我们大家共同遵守的,绝对不是来自于运行时计算得出来的。那样太不可靠了。尤其是一些嵌入式,或者针对某种具体硬件或者业务场景固定下来的数据,我们绝对不希望它有任何可能的不一致。

那么这时候我们需要的不再是一个变量不可以被修改,而是一个数值,这个数值代表着一种约定,这个数值可能出现在多个地方,要用一个东西来存储。更进步一,基于这个数值的运算结果是另一个具有同等性质的数值。

由于一开始就要知道这个值,那么就等于是写在代码里的硬编码数据,所以编译器就可以知道这个数据是不是一个硬编码数据。

比如,

constexpr int max_length = 255;  //编译时期确定数值
constexpr sz = particular_size();//函数是一个constexpr函数,里面可以理解是对一个constexpr变量的常数运算

好了。现在,我们来做一个假设,是不是当时如果早点都统一用constexpr就完事了呢?就可以避免const这个关键字的出现了呢?不是。

比如类的const成员函数,目的很明确,你调用这个对象的成员函数,对对象的状态是安全的,因为这个函数不能改变这个对象的成员变量。

这时候的对象当然没有必要从常量表达式这样一个角度去看待,因为const的目的很明确就是用来保护变量的。

这样以来C++增加了一个关键词,这个语言在扩展,扩展的时候没有打扰到原来的内容。

C++的扩展一向比较保守,因为他如果设计一个新的特性,导致原来的代码作废,编译报错,那就意味着历史财富要泡汤,这是绝对不允许的。因为那些程序可能都是常年不能停止的服务器程序。对程序的稳定性要求极高。比如飞机上的控制程序,银行,金融,等等。

反过来,第一眼看到我都要笑了。试想一下,是什么样的设计可以不顾之前的代码也要跳版?首先,历史代码少,如果历史代码都是关键代码,是宝贵的财富,说扔就扔,不管了?新,就是因为刚出来,负担轻,才可以说不要就不要。

C++也有废弃的语法特性,但是相对这种跳版,真的算是修修补补了。

3 C++是否需要引入垃圾回收机制

我有意的这样设计C++,使它不依赖于垃圾回收机制。

这是基于自己对垃圾回收系统的经验,我很害怕那种严重的时间和空间开销;也害怕由于实现和移植垃圾回收系统而带来的复杂性;还有,垃圾回收使得C++不适合做许多低层次的工作,而这却正是它的一个设计目标;

我喜欢垃圾回收的思想,但是我也确信,如果C++一开始就把垃圾回收加入,它可能早就胎死腹中了。

需要垃圾回收的理由一般如下:

1 对用户来说是方便的;

2 构造库更简单;

3 对某些应用来说更可靠;

反对的声音也很多,这里只列出最关键的一些方面:

1 垃圾回收带来的时间和空间开销,让运行在很多硬件上的应用无法负担;

2 垃圾回收可能意味着服务中断,对一些实时性要求很高的系统,这是无法接受的。比如,设备驱动、人机交互、操作系统、等等。

3 有些系统并没有那么多的资源。

4 垃圾回收要求不能有一些基本的C机制,比如指针运算,等。

5 有些垃圾回收机制让对象布局和创建有限制,这些都会使得C++与其他语言接口变得复杂。

还有很多支持的意见和反对的意见,不需要列出更多了。

我不存在任何幻想,以为给出一个C++的垃圾回收机制是一件很容易的事情。

4 C++标准委员会里都有哪些人

标准化绝对不是一件容易的事情。

在委员会里有各种各样的人。

有的人来这里就是为了维持现状;

有的人带着一个有关现状的想法,希望能够把时间拨回到几年之前;

有的人希望能与过去做彻底的决裂,设计出一个全新的语言;

有的人只关心某一个问题;有的人只关心某一类系统;

有的人在投票的时候完全按照他们雇主的脸色行事;

有的人只代表他们自己;

有的人带着有关程序设计和程序设计语言的理论观点;

也有的人希望的是今天就有一个标准,即使这意味着遗留下许多没有结论的问题;

有的人则除了一个完美的定义之外什么也不能接受;

有的人还认为C++完全是一个新的语言,几乎没有什么用户;

有的人则代表着在过去的十年里写成了百万行代码的用户;

如此等等。

这样,委员会的每一个成员都需要学会尊重那些看起来是异己的观点,学会妥协。这倒是很符合C++的精神。

5 C++语言设计准则

0 C++的目标:

0.0 C++应该使认真的程序员能够觉得编程变得更愉快了

0.1 C++是一种通用的程序设计语言:

是一种更好的C;

支持数据抽象;

支持面向对象的程序设计;

1 C++的演化与迭代准则:

1.0 C++的发展必须由实际问题推动;

1.1 不被牵扯到毫益的对完美的追求之中;

1.2 C++必须现在就是有用的;

不然的话,在没有一个有钱的爸爸在后面推动的情况下,这种争论、拖延、只会拖死C++,让它无法出现,更不要说大规模使用了。

1.3 每一种特性必须存在一种合理的明显的实现方式;

1.4 总提供一条转变的通路;

例如sprintf->sprintf_s通过编译器警告来过渡,而不是强制废弃前者

1.5 C++是一个语言,而不是一个完整的系统;

1.6 为每种应该支持的风格提供全面的支持;

1.7 不试图去强迫人做什么;

2 用C++ 设计的准则:

2.0 支持一致的设计概念;

总是可以通过类来表达抽象概念

2.1 为程序的组织提供各种机制;

2.2 直接说出你的意思;

2.3 所有特征都必须是能够负担的;

2.4 允许一个更有用的特征比防止各种误用更重要;

2.5 支持分别开发进行软件组合;

3 语言的技术性准则:

3.0 不隐式的违反静态类型系统;

3.1 为用户提供的类型提供和内部类型同样好的支持(值语义);

这个准则带来的影响最大。首先有人认为所有类对象都应该放到堆区就是不可以接受的。因为像复数complex这种类,不仅仅应该和int这种基础类型一样默认放到栈区。而且所有对象放到堆区那就意味着C++要彻底大改造成带有垃圾回收的语言,这种剧烈的变化严重违反了C++准则(1.2 C++必须现在就是有用的),是不可接受的。这样以来,C++就应该和C一样是值语义的。

值语义的语言要解决String 这种成员有指针的情形,

String a,b; a = b;

这种情况无法进行成功的复制,因为复制就意这共享(两个对象的底层指针指向同一处)那么就要有复制控制,所以C++提供了赋值操作符重载的机制。由于函数参数默认也是传值,

Fun(String s);

这就引发了复制构造函数。由于函数直接返回一个对象是自然的,也是方便的,所以这时候如果返回大的容器对象,值语义就比较糟糕了,

std::vector<int> Fun(void)
{
    ////
    return intArray;
}

这就引发了移动语义,std::move,移动赋值操作符重载,移动构造函数。

3.2 局不化是好事情;

3.3 避免顺序依赖性;

3.4 应该清除使用预处理程序的必要性;

4 低级程序设计支持准则:

4.0 使用传统的连接程序;

4.1 没有无故的与C的不兼容性;

4.2 在C++下面不为更低级语言留下空间,汇编除外;

4.3 0开销规则:对不用的东西不需要付出代价;

4.4 遇到有疑问的地方就提供手工控制的手段;

5 运算符重载催生了引用

这本书最大的价值在于还原了C++之所以是现在这个样子的历史由来。

里面很多语法特性是有先后顺序和依赖关系的,这才是最有意思的事情。对于理解这个语言的合理性和折中妥协有非常直接的帮助。

下面就开始介绍这其中互相关联的一些语言特性。

很多人要求C++应该有运算符重载的能力,因为这可以让代码看起来很舒服。

比如: Matrix a,b,c; a = b * c;

但是C++是沿用C的函数参数默认传值语义的,上面的代码本质上是下面的函数:

Matrix operator * (Matrix lhs, Matrix rhs);

但是上面的实现会发生拷贝,这对复杂对象来说是不可接受的,那就是要传指针:

Matrix operator (Matrix * lhs, Matrix * rhs);

这样以来乘法就会写成下面这样:

Matrix a,b,c; a = &b * &c;

上面这样一写的话,看起来就很丑了,没人会接受,太麻烦,重载的目标就是简洁优雅,整出这么个玩意儿出来,实在说不过去。何况上面的写法在C里面已经有意义了。那就只能让C++支持引用了:

Matrix operator * (const Matrix & lhs, const Matrix & rhs);

有了上面的引用语义,一开始的 a = b * c;就变得又优雅,又没有成本了。

有了引用,将引用作为返回值也是很重要的,尤其是像string这种类型的下标运算成员函数:

char& operator [ ] (int index);

string s;

s[i] = 'c';//这里的赋值变得简洁明了。

2

1.C++之父对于C语言的贡献

许多人将C++中的“不优雅”归结于对C的兼容,C++大牛们的几本书对于C近乎偏见的态度……似乎C是C++最失败的子集。但在本书中,C++之父实际上讲的很清楚了,他对于C是极其欣赏的,对于C的能力完全的承认(实际上早期C++许多的工作是对于C的强化和净化,并把完全兼容C作为强制性要求)。Stroustrup对于C的抱怨主要来源于两个方面——在C++兼容C的过程中遇到了不少设计实现上的麻烦;以及守旧的K&R C程序员对Stroustrup的批评。Stroustrup经常用这样的论据证明他对于C语言的修改和净化是正确的——C89、C99中许多的改进正是从C++中所引进。应该说Stroustrup身为C++之父,但对于C语言的贡献同样不可埋没。K&R的C语言著作,实际上也是使用的C++兼容的C(Stroustrup原话,我一定程度表示怀疑……)。

2.一些对C的“偏见”的来源

C++的书里经常出现“要用……,不要用……”之类的话,有些比较靠谱,属于Stroustrup在净化C的过程中的一些明智的措施,比如取消默认int,将fun()与fun(void)等同,变更K&R C的参数列表,有些感觉更偏重于他个人的Style。书中的一句话道出了真相,Stroustrup直言C++最大的竞争对手正是C,他的目的就是——C能做到的,我必须也能做到,而且要做的更好。正因为如此,Stroustrup才绞尽脑汁对于C中任何有可能造成麻烦的特性都做了处理,即便有些麻烦可以轻松避免,而这种做法很容易给人以“鸡蛋里挑骨头”的感觉。

3.对于程序效率和开发效率的侧重

现如今普遍的说法是C++编程开发效率远重要于程序效率,持这种观点的动不动就让反对者们去用汇编——“你要效率,就用汇编吧”,且不说此种说法极其幼稚(关于汇编与C/C++效率的问题本文不赘述,只提一句,绝大部分人写的汇编程序效率是绝对不如C的),更重要的是完全不符合Stroustrup的本意。

在讲述C++的演化时,Stroustrup多次强调C++的目标是“在保证效率与C语言相当的情况下,加强程序的组织性;能保证同样功能的程序,C++更短小”,这正是浅封装的核心思想,而不是现在某些人吹捧的过度设计——只要C++版本比结构高度优化的C版本源代码长(如果你的C程序有一长串if else和switch case,只能说明自己水平差),基本上就逃不出过度设计,事实上,现在的许多C++代码,如果用C改写,将会更短而不是更长!

书中的许多例子证明C++和C实际上效率几乎相同,C++早期的一个设计瑕疵导致相对于c语言3%的性能损失Stroustrup都无法忍受!如果觉得Stroustrup说得不具体,那就看Lippman的《深入详解C++对象模型》,定量的测试了C++各种情况下的效率与C的对比。

C++在其第二个版本中,引入了虚函数机制,这应该就是现代C++效率最大的瓶颈了。不要滥用虚函数,减少虚函数调用次数可明显增强效率,更不要按照java的方式去使用C++。当然,C/C++程序效率问题最大的原因仍是程序员的水平!