大局

大局问题

什么是 C++?

C++ 是一种通用编程语言,偏向系统编程,它

它由ISO 标准定义,提供数十年的稳定性,并拥有庞大而活跃的用户社区。另请参见《C++ 程序设计语言》《在真实世界中演化一种语言:C++ 1991-2006》

另请参见 C++ 的发明时间发明原因

C++ 是一种实用的语言吗?

是的。

C++ 是一种实用的工具。它并不完美,但很有用。

在工业软件领域,C++ 被视为一种坚实、成熟、主流的工具。它拥有广泛的行业支持,这使其从整体业务角度来看“很好”。

C++ 是一种完美的语言吗?

不。

C++ 的设计目的不是为了展示一种完美的语言是什么样子。它的设计目的是成为解决现实世界问题的实用工具。它有一些缺点,所有实用的编程工具都有,但唯一适合反复修改直到完美的地方是在纯粹的学术环境中。这不是 C++ 的目标。

什么是零开销原则?

零开销原则是 C++ 设计的指导原则。它指出:你不需要的,就不会为此付出代价(时间或空间),并且:你所使用的,你无法用手写代码做得更好。

换句话说,不应向 C++ 添加任何会使现有代码(未使用新特性)更大或更慢的特性,也不应添加任何编译器生成的代码不如程序员不使用该特性所创建的代码好的特性。

类有什么了不起的?

类的存在是为了帮助您组织代码和推理程序。您可以大致等同地说,类的存在是为了帮助您避免犯错,并在您犯错后帮助您发现错误。通过这种方式,类极大地帮助了维护。

类是代码中一个想法、一个概念的表示。类的对象代表代码中该想法的特定示例。没有类,代码的读者将不得不猜测数据项和函数之间的关系——类使这些关系明确并被编译器“理解”。有了类,程序的更多高级结构将体现在代码中,而不仅仅是注释中。

一个设计良好的类向其用户呈现一个清晰简单的接口,隐藏其表示并避免其用户必须了解该表示。如果表示不应该被隐藏——例如,因为用户应该能够随意更改任何数据成员——您可以将该类视为“只是一个普通的旧数据结构”;例如

struct Pair {
    Pair(const string& n, const string& v) : name(n), value(v) { }
    string name, value;
};

请注意,即使是数据结构也可以受益于辅助函数,例如构造函数。在设计类时,考虑对于类的每个对象和在任何时候都为真(即不变式)的内容通常很有用。例如,vector 的不变式可以是 (a) 其表示包含指向多个元素的指针,以及 (b) 该元素数量存储在整数中。每个构造函数的工作是建立类不变式,以便每个成员函数都可以依赖它。每个成员函数在退出时必须保持不变式有效。这种思维对于管理资源(如锁、套接字和文件)的类特别有用。例如,文件句柄类将具有不变式,即它持有一个指向已打开文件的指针。文件句柄构造函数打开文件。析构函数释放构造函数获取的资源。例如,文件句柄的析构函数关闭由构造函数打开的文件

class File_handle {
public:
    File_handle(const char* n, const char* rw)
        { f = fopen(n,rw); if (f==nullptr) throw Open_failure(n); }
    ~File_handle() { fclose(f); } // destructor
    // ...
private:
    FILE* f;
};

如果您没有使用类编程,您会发现此解释的某些部分晦涩难懂,并且会低估类的有用性。寻找示例。像所有好的教科书一样,TC++PL 有很多示例;例如,请参阅标准库之旅。大多数现代 C++ 库(除其他外)由类组成,而库教程是寻找有用类的最佳地方之一。

面向对象有什么了不起的?

使用类和虚函数的面向对象技术是开发大型复杂软件应用程序和系统的重要方法。同样,使用模板的泛型编程技术也很重要。两者都是表达多态性的重要方式——分别在运行时和编译时。它们在 C++ 中配合得很好。

关于“面向对象”、“面向对象编程”和“面向对象编程语言”有很多定义。要了解 Stroustrup 对“面向对象”的看法,请阅读《为什么 C++ 不仅仅是一种面向对象编程语言》。话虽如此,面向对象编程是一种源自 Simula(大约 40 年前!)的编程风格,它依赖于封装、继承和多态性。在 C++(以及许多其他根植于 Simula 的语言)的上下文中,它意味着使用类层次结构和虚函数进行编程,以允许通过定义良好的接口操作各种类型的对象,并允许通过派生逐步扩展程序。

请参阅类有什么了不起的,以了解“普通类”有什么了不起的。将类组织成类层次结构的目的是表达类之间的层次关系,并利用这些关系来简化代码。

要真正理解 OOP,请寻找一些例子。例如,您可能有两个(或更多)具有公共接口的设备驱动程序

class Driver {  // common driver interface
public:
    virtual int read(char* p, int n) = 0; // read max n characters from device to p
                                          // return the number of characters read
    virtual bool reset() = 0;             // reset device
    virtual Status check() = 0;           // read status
};

Driver 只是一个接口。它没有数据成员,并且定义了一组纯虚函数。Driver 可以通过此接口使用,并且许多不同类型的驱动程序可以实现此接口

class Driver1 : public Driver { // a driver
public:
    Driver1(Register);          // constructor
    int read(char*, int n) override;    
    bool reset() override;
    Status check() override;
private:
    // implementation details, including representation
};

class Driver2 : public Driver { // another driver
public:
    Driver2(Register);
    int read(char*, int n) override;    
    bool reset() override;
    Status check() override;
private:
    // implementation details, including representation
};

请注意,这些驱动程序持有数据(状态),并且可以创建它们的对象。它们实现了 Driver 中定义的函数。我们可以想象一个驱动程序是这样使用的

void f(Driver& d)               // use driver
{
    Status old_status = d.check();  
    // ...
    d.reset();
    char buf[512];
    int x = d.read(buf,512);
    // ...
}

这里的关键是 f() 不需要知道它使用的是哪种驱动程序;它只需要知道它被传递了一个 Driver;也就是说,一个用于许多不同类型驱动程序的接口。我们可以这样调用 f()

void g()
{
    Driver1 d1(Register(0xf00));  // create a Driver1 for device
                                  // with device register at address 0xf00

    Driver2 d2(Register(0xa00));  // create a Driver2 for device
                                  // with device register at address 0xa00
    // ...
    int dev;
    cin >> dev;

    if (dev==1) 
        f(d1);  // use d1
    else
        f(d2);  // use d2
    // ...
}

请注意,当 f() 使用 Driver 时,正确的操作类型会在运行时隐式选择。例如,当 f() 传递 d1 时,d.read() 使用 Driver1::read(),而当 f() 传递 d2 时,d.read() 使用 Driver2::read()。这有时被称为运行时调度或动态调度。在这种情况下,f() 无法知道调用它的设备类型,因为我们是根据输入选择它的。

请注意,面向对象编程并非万能。“OOP”不简单意味着“好”——如果您的根本问题概念之间没有固有的层次关系,那么任何层次结构和虚函数都不会改善您的代码。OOP 的强大之处在于,许多问题都可以使用类层次结构有效地表达——OOP 的主要弱点是太多人试图将太多问题强行塞入层次结构中。并非每个程序都应该是面向对象的。作为替代方案,请考虑普通类泛型编程和独立函数(如数学、C 和 Fortran 中)。

如果您仍然想知道“为什么是面向对象?”,请同时考虑业务原因

软件行业正在成功地自动化许多过去需要手动完成的生活功能。此外,软件正在提高以前自动化设备的灵活性,例如,将许多现有设备的内部实现从机械转换为软件(时钟、汽车点火系统等),或从由电路控制转换为软件(电视、厨房电器等)。当然,软件已融入我们日常业务生活的各个方面——最初软件仅限于会计和财务,但现在它已嵌入运营、营销、销售和管理中——软件几乎无处不在。

这种惊人的成功不断地给软件开发组织的跟进能力带来压力。作为一个行业,软件开发一直未能满足对大型复杂软件系统的需求。是的,这种失败实际上是由于软件带来感知价值的能力的成功——它实际上是因为需求大于我们满足需求的能力。虽然我们软件人员可以坐下来为这种需求而沾沾自喜,但这个及其他所有学科的创新者和思想领袖都具有一个不可否认的特征:他们/我们不满足。作为一个行业,我们必须做得更好。好得多。好得超乎想象。

我们过去的成功促使用户提出更多要求。我们创造了一个市场饥饿,结构化分析、设计和编程技术未能满足。这要求我们创建更好的范式。事实上,有几种。

C++ 支持面向对象编程。C++ 也可以用作传统的命令式编程语言(“作为更好的 C”)或使用泛型编程方法。当然,这些方法各有优缺点;不要期望在使用一种技术时获得另一种技术的优势。(最常见的误解:如果您将 C++ 用作更好的 C,不要期望获得面向对象编程的优势。)

C++ 也支持泛型编程方法。最近,C++ 开始支持(而非仅仅允许)函数式编程方法。最优秀的程序员能够决定哪种方法最适合哪种情况,而不是试图将单一方法(“我最喜欢的方法”)强加给每个行业中每个地方的每个问题,而不考虑业务背景或赞助商的目标。

最重要的是,有时通过结合面向对象、泛型和函数式编程风格的特性可以获得最佳解决方案,而试图将自己限制在一种特定方法可能会导致次优解决方案。

泛型编程有什么了不起的?

使用模板的泛型编程技术是开发大型复杂软件应用程序和系统的重要方式。同样,面向对象技术也很重要。两者都是表达多态性的重要方式——分别在编译时和运行时。它们在 C++ 中配合得很好。

C++ 支持泛型编程。泛型编程是一种以不牺牲性能的方式最大化代码重用的软件开发方法。(“性能”部分并非严格必要,但高度期望。)

泛型编程是基于参数化的编程:您可以将类型参数化为另一种类型(例如,vector 及其元素类型),并将算法参数化为另一种算法(例如,排序函数与比较函数)。泛型编程的目标是将有用的算法或数据结构推广到其最通用和最有用的形式。例如,整数的 vector 很好,并且查找整数 vector 中最大值的函数也很好。然而,一个更好的泛型查找函数将能够查找任何类型 vector 中的元素,或者更好的是,在由一对迭代器描述的任何元素序列中查找

auto p = find(begin(vs), end(vs), "Grail"s); // vector<string> vs; p is vector<string>::iterator 

auto q = find(begin(vi), end(vi), 42);       // vector<int> vi;    q is vector<int>::iterator 

auto r = find(begin(ld), end(ld), 1.2);      // list<double> ld;   r is list<double>::iterator 

auto s = find(begin(ar), end(ar), 10);       // int ar[10];        s is int *

这些示例来自 STL(ISO C++ 标准库的容器和算法部分);如需简要介绍,请参阅 《标准库之旅》,摘自 《TC++PL》

泛型编程在某些方面比面向对象编程更灵活。特别是,它不依赖于层次结构。例如,intstring 之间没有层次关系。泛型编程通常比 OOP 更具结构性;事实上,用于描述泛型编程的一个常用术语是“参数多态”,而“特设多态”是面向对象编程的相应术语。在 C++ 的上下文中,泛型编程在编译时解析所有名称;它不涉及动态(运行时)调度。这使得泛型编程在运行时性能至关重要的领域占据主导地位。

请注意,泛型编程并非万能药。程序中有许多部分不需要参数化,并且有许多示例中运行时调度(OOP)更合适。

泛型组件非常容易使用,至少如果它们设计良好,并且它们倾向于隐藏大量复杂性。另一个有趣的特点是,它们倾向于使您的代码更快,特别是如果您使用它们更多。这创造了一个令人愉快的非权衡:当您使用这些组件为您完成繁琐的工作时,您的代码变得更小、更简单,您引入错误的机会更少,而且您的代码通常会运行得更快。

大多数开发人员不适合创建这些通用组件,但大多数人可以使用它们。幸运的是,通用组件是通用的,所以您的组织通常不需要创建很多。有许多现成的通用组件库。STL 就是这样的一个库。Boost 还有更多。

什么是多范式编程?

简而言之:与“编程”相同,根据需要结合使用不同的特性(特别是 OO 和泛型风格)。

在面向对象和泛型编程同时存在于同一语言中仍然是新事物的时候,“多范式编程”最初是一种花哨的说法,意思是“使用多种编程风格进行编程,每种风格发挥其最佳效果。”例如,当需要不同对象类型之间的运行时解析时使用面向对象编程,而当静态类型安全和运行时性能至关重要时使用泛型编程。当然,多范式编程的主要优势在于使用了不止一种范式(编程风格)的程序中,这样就很难通过将用支持不同范式的语言编写的部分组成一个系统来获得相同的效果。多范式编程最引人注目的案例是在不同范式的技术密切协作,编写出比单一范式下更优雅、更可维护的代码的地方。一个简单的例子是对多态类型对象的静态类型容器进行遍历

void draw_all(vector<Shape*>& vs)   // draw each element of a standard vector
{
    for_each(vs.begin(),vs.end(),[](Shape* p){ p->draw(); });
}

在这里,Shape 将是一个抽象基类,定义几何形状层次结构的接口。这个例子很容易推广到任何标准库容器

template<class C>
void draw_all(C& cs)    // draw each element of a standard container
{
    for_each(cs.begin(),cs.end(),[](Shape* p){ p->draw(); });
}

这是 OOP、GP、函数式还是传统结构化编程?以上所有:它是一个函数模板(GP),具有过程体(传统结构化),使用泛型算法(再次是 GP)和一个 lambda(函数式),它接受一个指向基类的指针并调用一个虚函数(OO)。关键是这都只是“编程”。

所以今天我们不应该再称之为“多范式编程”,而应该简单地称之为“编程”。这都是编程,只是像往常一样正确地组合使用各种语言特性。

C++ 比 Java 更好吗?(或 C#、C、Objective-C、JavaScript、Ruby、Perl、PHP、Haskell、FORTRAN、Pascal、Ada、Smalltalk 或任何其他语言?)

停止。这个问题产生的光远少于热。在发布此问题的任何变体之前,请阅读以下内容。

在 99% 的情况下,编程语言的选择主要由业务考量决定而非技术考量。真正重要的事情是:开发机器的编程环境可用性、部署机器的运行时环境可用性、运行时和/或开发环境的许可/法律问题、训练有素的开发人员的可用性、咨询服务的可用性以及企业文化/政治。这些业务考量通常比编译时性能、运行时性能、静态与动态类型、静态与动态绑定等因素发挥更大的作用。

那些在评估编程语言权衡时忽略(主导性的!)业务标准的人,会因判断力差而受到批评。要技术化,但不要成为技术呆子。业务问题确实主导技术问题,那些不认识到这一点的人注定会做出带来可怕业务后果的决策——他们对雇主是危险的。

最广为流传的比较往往是由某些语言 Z 的支持者撰写的,旨在证明 Z 比其他语言更好。鉴于 C++ 的广泛使用,它经常是 Z 的支持者希望证明其劣势的语言列表的首位。通常,此类论文由销售 Z 的公司作为营销活动的一部分“发布”或分发。令人惊讶的是,许多人似乎认真对待由销售 Z 的公司工作人员撰写的未经审查的论文,该论文“证明”Z 是最好的。一个问题是,此类比较中总是有一些真理。毕竟,没有哪种语言在所有方面都比其他语言更好。C++ 肯定不完美,但选择性地引用事实可能极具诱惑力,有时甚至完全误导。在查看语言比较时,请考虑作者是谁,仔细考虑描述是否真实和公平,以及比较标准本身是否对所有考虑的语言都公平。这并非易事。

Stroustrup 出于《C++ 的设计与演化》中给出的以下原因,拒绝将 C++ 与其他语言进行比较:

“几位审稿人要求我将 C++ 与其他语言进行比较。我已决定不这样做。因此,我重申了一个长期且坚定的观点:语言比较很少有意义,更少公平。对主要编程语言进行良好比较需要比大多数人愿意付出的更多努力,在广泛的应用领域有经验,严格保持超然和公正的观点,以及公平感。我没有时间,而且作为 C++ 的设计者,我的公正性永远不会完全可信。

我还担心在对语言进行认真尝试的比较中反复观察到的一个现象。作者努力做到公正,但由于只关注单一应用、单一编程风格或程序员中的单一文化而无可救药地存在偏见。更糟的是,当一种语言比其他语言更为人所知时,视角会发生微妙的转变:众所周知的语言的缺陷被认为是次要的,并提出了简单的变通方法,而其他语言的类似缺陷则被认为是根本性的。通常,在不太知名的语言中常用的变通方法,要么是进行比较的人根本不知道,要么被认为不令人满意,因为它们在更熟悉的语言中无法使用。

同样,关于知名语言的信息往往是完全最新的,而对于不太知名的语言,作者则依赖于几年前的信息。对于值得比较的语言,将三年前定义的语言 X 与最新实验实现中出现的语言 Y 进行比较既不公平也没有信息量。因此,我将对 C++ 以外的语言的评论限制在一般性和非常具体的评论中。”

话虽如此,对于各种人群和应用程序来说,C++ 被认为是编程语言的最佳选择。

C++ 为什么这么大?

C++ 不是一种为了教学而设计的微小语言,但人们最常将它与之比较的语言,如 C、Java、C#,也并非如此。与 Wirth 博士最初定义的 Pascal 相比,它们也庞大——这是有充分理由的。今天的编程世界比 30 年前复杂得多,现代编程语言反映了这一点。

C++ 没有一些人想象的那么大。按字数计算,C++、C# 和 Java 的语言规范(不包括标准库)目前的大小相差只有几个百分点。这反映出它们是通用主流语言,并且已经发展出相似的特性——auto/var 类型推导、范围 for 循环、lambda 函数、各种级别的泛型编程支持等等。这也反映了设计理论家所说的“问题领域中的本质复杂性”——现实世界中的复杂性,以及一种严肃的语言必须暴露的一切,从基本的操作系统差异到调用 C++ 库。

在某些情况下,C++ 直接支持(即在语言中)其他语言通过库支持的功能,因此语言部分会相对较大。另一方面,如果您想编写一个“典型的现代应用程序”,您需要考虑操作系统接口、GUI、数据库、Web 接口等,语言特性、库和编程约定和标准的总和会让编程语言显得渺小。在这种情况下,C++ 的大小可以成为一个优势,因为它更好地支持了良好的库。

最后,新手程序员能够了解所有语言的时代已经一去不复返了,至少对于广泛应用于工业领域的语言来说是如此。很少有人知道“所有 C 语言”或“所有 Java 语言”,而且这些人都不是新手。由此可知,没有人需要为新手不了解所有 C++ 语言而道歉。您必须做的是——在任何语言中——选择一个子集,开始编写代码,并逐渐学习更多语言、其库和其工具。对于我关于初学者如何学习 C++ 的建议,请参阅《编程:使用 C++ 的原理与实践》

谁使用 C++?

许多公司和政府机构都在使用。非常多。如果您正在使用其他语言(如 Java)的编译器或运行时,很有可能它也是用 C++ 实现的。

C++ 用户数量众多,无法有效计数,但估计有数百万。所有主要厂商都支持 C++。大量的开发人员(以及因此而来的大量可用支持基础设施,包括厂商、工具、培训等)是 C++ 的几个关键特性之一。

在 1980-1991 年间,用户数量每七个半月翻一番(参见 《C++ 的设计与演化》)。目前的增长率稳定且积极。IDC 在 2001 年估计 C++ 程序员数量为“约 300 万”;2004 年的数据为“超过 300 万”。这似乎是合理的,并表明持续增长。尤其是在 2010 年左右,C++ 重新获得了增长,因为移动和数据中心应用程序都将“每瓦性能”视为一个新的主流指标。

学习 C++ 需要多长时间?

这取决于您对“学习”的定义。如果您是一位 C 程序员,您可以在一天之内学会足够多的 C++,从而更有效地进行 C 风格的编程。

编程:使用 C++ 的原理与实践》一书已被用于帮助数千名大一新生(一年级学生)在一个学期内掌握 C++ 的基础知识及其支持的编程技术(特别是面向对象编程和泛型编程)。

另一方面,如果您想完全掌握所有主要的 C++ 语言结构、数据抽象、面向对象编程、泛型编程、面向对象设计等,您很容易花费一到两年时间——如果您不熟悉这些技术(例如,来自 Java 或 C#)。

那么,这就是学习 C++ 所需的时间吗?也许吧,但这又是我们为了成为更好的设计师和程序员而必须考虑的时间尺度。如果目标不是我们工作和思考系统构建方式的巨大改变,那为什么要费心学习一门新语言呢?与熟练弹奏钢琴或流利掌握一门外语(自然语言)所需的时间相比,学习一门新的、不同的编程语言和编程风格是容易的。

有关学习 C++ 的更多观察,请参阅D&EBjarne Stroustrup 很久以前写的一篇笔记

公司成功地教授行业标准的“短期课程”,将一个大学学期的课程压缩成一个 40 小时的工作周。但无论您在哪里接受培训,请确保课程包含实践元素,因为大多数人在有项目帮助概念“成形”时学得最好。但即使他们接受了最好的培训,他们也尚未准备好。

掌握 C++ 需要 6-12 个月的时间,特别是如果你以前没有进行过面向对象或泛型编程。如果开发者能够轻易接触到“本地”专家群体,则所需时间更少;如果缺乏“良好”的通用 C++ 类库,则所需时间更长。要成为能够指导他人的专家,大约需要 3 年。

有些人永远无法成功。除非你虚心受教并具有个人驱动力,否则你没有机会。在“虚心受教”方面,最起码你必须能够承认自己犯错了。在“驱动力”方面,最起码你必须愿意额外投入一些时间。记住:学习一些新知识比改变你的范式,即改变你的思维方式;改变你对善恶的观念;改变你的心智模型,要容易得多

应该做的两件事

不应该做的两件事

改进我的 C++ 程序的最佳方法是什么?

这取决于你如何使用它。大多数人低估了抽象类和模板。相反,大多数人严重滥用强制转换和宏。可以看看 Stroustrup 的论文书籍以获取灵感。抽象类和模板的一种思考方式是作为接口,它们比通过函数或单一根类层次结构更容易提供更清晰、更逻辑的服务呈现。有关具体示例和想法,请参阅本常见问题的其他部分。

我使用哪种编程语言重要吗?

是的,但不要指望奇迹。有些人似乎认为编程语言可以或者至少应该解决他们构建系统的大部分问题。他们注定永远寻找完美的编程语言,并反复失望。另一些人则将编程语言视为不重要的“实现细节”,并将其资金投入到开发过程和设计方法中。他们注定永远用 COBOL、C 和专有设计语言编程。一种好的语言——例如 C++——可以为设计师和程序员做很多事情,只要其优点和局限性得到清晰的理解和尊重。

从商业角度看 C++ 有哪些特点?

以下是从商业角度看 OO/C++ 的一些特点

virtual 函数(动态绑定)是 OO/C++ 的核心吗?

是也不是!OO 风格的动态多态性(通过调用虚函数获得)是 C++ 提供的实现多态性的两种主要方式之一,并且是您应该用于编译时无法确定的事物的方式。另一种是泛型编程风格的静态多态性(通过使用模板获得),您应该经常用于编译时已知的事物。它们是两种绝佳的口味,搭配起来味道更佳。

如果没有虚函数,C++ 就不是面向对象的。操作符重载和非virtual成员函数很棒,但它们毕竟只是更典型的 C 语言中将指向结构体的指针传递给函数的语法糖。标准库包含许多演示“泛型编程”技术的模板,它们也很棒,但virtual函数仍然是使用 C++ 进行面向对象编程的核心。

商业角度来看,如果没有 virtual 函数,从纯 C 切换到 C++ 几乎没有理由(暂时我们忽略泛型编程和标准库)。技术人员通常认为 C 和非 OO C++ 之间存在很大差异,但如果没有 OO,这种差异通常不足以证明培训开发人员、新工具等的成本是合理的。换句话说,如果我要建议一位经理是否从 C 切换到非 OO C++(即,切换语言但不切换范式),我可能会劝阻他或她,除非有令人信服的工具导向原因。从商业角度来看,OO 可以帮助使系统可扩展和适应,但仅仅 C++ 类的语法(没有 OO)甚至可能不会降低维护成本,而且它肯定会显著增加培训成本。

底线:没有 virtual 的 C++ 不是 OO。使用类但没有动态绑定的编程被称为“基于对象”,而不是“面向对象”。扔掉 virtual 函数等同于扔掉 OO。剩下的只有基于对象的编程,类似于最初的 Ada 语言(顺便说一下,更新的 Ada 语言支持真正的 OO,而不仅仅是基于对象的编程)。

注意:泛型编程不需要 virtual 函数。除其他外,这意味着您无法仅仅通过计算拥有的 virtual 函数数量来判断您使用了哪种范式。

我来自密苏里州。您能给我一个简单的理由,说明为什么 virtual 函数(动态绑定、动态多态)和模板(静态多态)会产生巨大的影响吗?

它们可以通过让代码调用在运行时(虚函数)或编译时(模板)提供的新代码来提高重用性。

在面向对象和泛型编程出现之前,重用是通过让新代码调用旧代码来实现的。例如,程序员可能会编写一些调用可重用代码(如 printf())的代码。

通过面向对象和泛型编程,重用也可以通过让代码调用代码来实现。例如,程序员可能会编写一些被一个由他们的曾曾祖父编写的框架调用的代码。不需要更改曾曾祖父的代码。事实上,对于带有虚函数的动态绑定,它甚至不需要重新编译。即使你只剩下目标文件,而曾曾祖父编写的源代码在 25 年前就丢失了,那个古老的目标文件也会调用新的扩展,而不会出现任何问题。

这就是可扩展性,这就是面向对象和泛型编程的强大可重用抽象。

C++ 是否向后兼容 ANSI/ISO C?

几乎是。另请参阅C 是 C++ 的子集吗

C++ 尽可能与 C 兼容,但不多于此。实际上,主要的区别在于 C++ 需要原型,并且 f() 声明了一个不带参数的函数(在 C 中,使用 f() 声明的函数可以传递任意数量的任意类型的参数)。

还有一些非常微妙的差异,例如 sizeof('x') 在 C++ 中等于 sizeof(char),但在 C 中等于 sizeof(int)。此外,C++ 将结构“标签”放在与其他名称相同的命名空间中,而 C 需要显式的 struct(例如,typedef struct Fred Fred; 的技术仍然有效,但在 C++ 中是多余的)。

C++ 为什么(几乎)兼容 C?

当 Stroustrup 发明 C++ 时,他希望 C++ 能与一门完整的语言兼容,该语言具有足够的性能和灵活性,即使是最严苛的系统编程任务也能胜任。他“非常害怕再造一门漂亮但有无意限制的语言。”有关历史细节,请参阅《C++ 的设计与演化》第 2.7 节。

当时,Stroustrup 认为 C 是可用的最佳系统编程语言。这在当时(1979 年)并不像后来那样显而易见,但 Stroustrup 身边有专家,如 Dennis Ritchie、Steve Johnson、Sandy Fraser、Greg Chesson、Doug McIlroyBrian Kernighan,他可以向他们学习并获得反馈。没有他们的帮助和建议,也没有 C,C++ 就不会诞生。

与反复出现的谣言相反,Stroustrup 从未被告知必须使用 C;也从未被告知不能使用 C。事实上,第一本 C++ 手册是根据 Dennis Ritchie 提供的 C 手册 troff 源代码演变而来的。贝尔实验室设计了许多新语言;至少在“研究”部门,没有强制语言偏见的规则。

C++ 是何时发明的?

Bjarne Stroustrup 于 1979 年开始研究后来成为 C++ 的语言。最初的版本被称为“带类的 C”。C++ 的第一个版本于 1983 年 8 月在 AT&T 内部使用。当年晚些时候使用了“C++”这个名称。第一个商业实现于 1985 年 10 月发布,同时出版了《C++ 程序设计语言》第一版。模板和异常处理后来在 1980 年代被包含进来,并在《C++ 参考手册附注》和《C++ 程序设计语言(第二版)》中进行了说明。

C++ 的当前定义是ISO C++ 标准,并在《C++ 程序设计语言(第四版)》中进行了描述。

您可以在《C++ 的设计与演化》《C++ 的历史:1979-1991》中找到更完整的历史时间轴和更详细的解释。

C++ 是为何发明的?

Stroustrup 想用 Simula67 所鼓励的风格编写高效的系统程序。为此,他在 C 中添加了更好的类型检查、数据抽象和面向对象编程功能。更普遍的目标是设计一种语言,开发人员可以使用它编写既高效又优雅的程序。许多语言迫使您在两者之间做出选择。

导致 Stroustrup 开始设计和实现 C++(最初称为“带类的 C”)的具体任务与在网络上分发操作系统功能有关。

您可以在《C++ 的设计与演化》中找到更详细的解释。另请参见《C++ 的历史:1979-1991》《在真实世界中演化一门语言:C++ 1991-2006》

C++ 这个名字从何而来?

D&E的第三章中,Stroustrup 写道

我选择 C++ 是因为它简短,有很好的解释,而且不是“形容词 C”的形式。

在 C 语言中,++ 根据上下文可以理解为“下一个”、“后继”或“递增”,尽管它总是发音为“加加”。C++ 这个名字和它的备选名称 ++C 是笑话和双关语的丰富来源——几乎所有这些在选择这个名字之前就已经为人所知和欣赏。C++ 这个名字是 Rick Mascitti 建议的。它于 1983 年 12 月首次使用,当时它被编辑到 [Stroustrup,1984] 和 [Stroustrup,1984c] 的最终版本中。

TC++PL的第一章中,Stroustrup 写道

C++(发音为“see plus plus”)这个名字是 Rick Mascitti 在 1983 年夏天创造的。这个名字表示 C 语言变化的演进性质;“++”是 C 的增量运算符。稍短的名称“C+”是语法错误;它也被用作一种不相关语言的名称。C 语义的鉴赏家认为 C++ 不如 ++C。这种语言不叫 D,因为它扩展了 C,并且它不试图通过删除功能来解决问题。对于 C++ 这个名字的另一种解释,请参阅 [Orwell,1949] 的附录。

C++ 中的“C”历史悠久。当然,它是 Dennis Ritchie 设计的语言的名称。C 的直接祖先是 Ken Thompson 设计的 BCPL 的解释器后代,称为 B。BCPL 由剑桥大学的 Martin Richards 在访问另一个剑桥的麻省理工学院时设计和实现。BCPL 继而是 Basic CPL,其中 CPL 是由剑桥大学和伦敦大学联合开发的一种相当大(就当时而言)且优雅的编程语言的名称。在伦敦人加入项目之前,“C”代表剑桥。后来,“C”正式代表组合。非正式地,“C”代表 Christopher,因为 Christopher Strachey 是 CPL 的主要推动力。

C++ 为什么允许不安全的代码?

也就是说,C++ 为什么支持可用于违反静态(编译时)类型安全规则的操作?

  • 直接访问硬件(例如,将整数视为设备寄存器的指针(地址))
  • 实现最佳运行时和空间性能(例如,未经检查地访问数组元素和未经检查地通过指针访问对象)
  • 与 C 兼容

话虽如此,当您实际上不需要这三个特性中的任何一个时,最好像避瘟疫一样避免不安全的代码

  • 不要使用强制类型转换
  • 将 C 风格的 [] 数组保留在接口之外(将它们隐藏在高性能函数和类的内部,在需要时使用,并使用适当的 stringvector 等编写程序的其余部分)
  • 避免使用 void*(如果确实需要,请将它们保留在低级函数和数据结构内部,并向用户提供类型安全的接口,通常是模板)
  • 避免使用 union
  • 如果您对指针的有效性有任何疑问,请改用智能指针
  • 不要使用“裸露的”newdelete(请改用容器、资源句柄等)
  • 不要使用 ... 风格的可变参数函数(“printf 风格”)
  • 除了 #include 保护之外,避免使用宏

几乎所有 C++ 代码都可以遵循这些简单的规则。请不要因为在 C 或 C++ 中编写 C 风格代码时无法遵循这些规则而感到困惑。

C++ 中为什么有些东西是未定义的?

因为机器不同,也因为 C 语言留下了许多未定义的地方。有关详细信息,包括“未定义”、“未指定”、“实现定义”和“格式良好”等术语的定义;请参阅 ISO C++ 标准。请注意,这些术语的含义与 ISO C 标准中的定义以及一些常见用法有所不同。当人们没有意识到并非所有人都共享定义时,可能会出现令人困惑的讨论。

这是一个正确但令人不满意的答案。像 C 一样,C++ 旨在直接高效地利用硬件。这意味着 C++ 必须以给定机器上的方式处理位、字节、字、地址、整数计算和浮点计算等硬件实体,而不是我们可能希望它们的方式。请注意,许多人们称为“未定义”的“事物”实际上是“实现定义”的,因此只要我们知道我们正在运行的机器,我们就可以编写完美指定的代码。整数的大小和浮点计算的舍入行为属于这一类。

考虑一个可能是最著名也是最臭名昭著的未定义行为示例

    int a[10];
    a[100] = 0; // range error
    int* p = a;
    // ...
    p[100] = 0; // range error (unless we gave p a better value before that assignment)

C++(和 C)中的数组和指针概念是机器内存和地址概念的直接表示,没有开销。指针上的基本操作直接映射到机器指令。特别是,不进行范围检查。进行范围检查会增加运行时和代码大小的成本。C 语言设计用于在操作系统任务中超越汇编代码,因此这是一个必要的决定。此外,C——与 C++ 不同——没有合理的方法来报告违规,如果编译器决定生成代码来检测它:C 中没有异常。C++ 遵循 C 是出于兼容性原因,也因为 C++ 直接与汇编程序竞争(在操作系统、嵌入式系统和某些数值计算领域)。如果您需要范围检查,请使用合适的检查类(vector、智能指针、string 等)。一个好的编译器可以在编译时捕获 a[100] 的范围错误,捕获 p[100] 则困难得多,并且通常不可能在编译时捕获所有范围错误。

未定义行为的其他示例源于编译模型。编译器无法检测单独编译的翻译单元中对象或函数的不一致定义。例如

    // file1.c:
    struct S { int x,y; };
    int f(struct S* p) { return p->x; }

    // file2.c:
    struct S { int y,x; }
    int main()
    {
        struct S s;
        s.x = 1;
        int x = f(&s);  // x!=s.x !!
        return 2;
    }

在 C 和 C++ 中,编译 file1.cfile2.c 并将结果链接到同一个程序中是非法的。链接器可以捕获 S 的不一致定义,但没有义务这样做(大多数也不会)。在许多情况下,捕获单独编译的翻译单元之间的不一致可能非常困难。一致使用头文件有助于最大程度地减少此类问题,并且有迹象表明链接器正在改进。请注意,C++ 链接器确实捕获了几乎所有与函数声明不一致相关的错误。

最后,我们遇到了单个表达式中看似不必要且相当烦人的未定义行为。例如

    void out1() { cout << 1; }
    void out2() { cout << 2; }

    int main()
    {
        int i = 10;
        int j = ++i + i++;  // value of j unspecified
        f(out1(),out2());   // prints 12 or 21
    }

j 的值是未指定的,以允许编译器生成最优代码。据称,这种自由与要求“通常的从左到右求值”之间产生的代码差异可能很大。领先的专家对此不以为然,但由于无数的编译器“在那里”利用了这种自由,并且有些人热情地捍卫这种自由,因此改变将很困难,并且可能需要数十年才能渗透到 C 和 C++ 世界的偏远角落。令人失望的是,并非所有编译器都警告 ++i+i++ 这样的代码。同样,参数的求值顺序也是未指定的。

有一种观点认为,有太多“事物”被保留为未定义、未指定、实现定义等。为了解决这个问题,ISO C++ 委员会成立了研究小组 12,以审查并建议对未定义、未指定和实现定义的行为进行广泛的收紧。

为什么可移植性如此重要?

成功的软件是长寿的;几十年的寿命并不少见。一个好的应用程序/程序通常会比它设计的硬件、它编写的操作系统、它最初使用的数据系统等寿命更长。通常,一个好的软件会比提供构建它所使用的基本技术的公司寿命更长。

通常,一个成功的应用程序/程序会有喜欢各种平台的客户/用户。随着用户群体的变化,理想平台的集合也会改变。被绑定到单一平台或单一供应商会限制应用程序/程序的潜在用途。

显然,完全的平台独立性与使用所有平台特定功能的能力不兼容。然而,您通常可以通过一个“薄接口”来近似应用程序的平台独立性,该接口将应用程序对其环境的视图表示为一个库。

C++ 已标准化吗?

是的。

C++ 标准已由 ISO(国际标准化组织)以及 INCITS(美国信息技术标准国家委员会)、BSI(英国标准协会)、DIN(德国国家标准组织)等多个国家标准组织最终确定并采用。ISO 标准于 1997 年 11 月经一致投票最终确定并采用,2003 年进行了小幅更新,2011 年进行了重大且有价值的更新。预计 2014 年还将发布另一组更新。

美国 C++ 委员会名为“PL22.16”。ISO C++ 标准化组织名为“WG21”。C++ 标准化过程中的主要参与者几乎包括了所有人:来自澳大利亚、加拿大、丹麦、芬兰、法国、德国、爱尔兰、日本、荷兰、新西兰、瑞典、英国和美国等国家的代表,以及来自约一百家公司和许多感兴趣的个人代表。主要参与者包括 AT&T、爱立信、Digital、Borland、惠普、IBM、英特尔、Mentor Graphics、微软、英伟达、硅谷图形、太阳微系统和西门子。

欲了解更多信息,请参阅ISO C++ 标准化页面,包括但不限于:

标准化委员会的成员有哪些?

另请参阅委员会页面

委员会由大量人员(约 200 人)组成,其中约 100 人每年参加两到三次为期一周的会议。此外,还有几个国家设有国家标准小组和会议。大多数成员通过参加会议、参与电子邮件讨论或提交论文供委员会审议来做出贡献。大多数成员都有朋友和同事提供帮助。从第一天起,委员会就有来自许多国家的成员,每次会议都有来自六到十二个国家的人员参加。最终投票由大约 20 个国家标准机构进行。因此,ISO C++ 标准化是一项相当庞大的工作,而不是一小撮有凝聚力的人致力于为“像他们自己一样的人”创建完美的语言。该标准是这群志愿者所能达成一致的,作为他们所能产生的最好的、所有人都能接受的成果。

当然,许多(但并非所有)这些志愿者的日常工作都专注于 C++:他们包括编译器编写者、工具开发者、库编写者、应用程序开发者、研究人员、图书作者、顾问、测试套件开发者等等。

以下是一些主要参与组织的非常不完整的列表:Adobe、Apple、Boost、Bloomberg、EDG、Google、HP、IBM、Intel、Microsoft、Oracle、Red Hat。

以下是一些您可能在文献或网络上遇到过的委员会成员的简短名单:Dave Abrahams, Matt Austern, Pete Becker, Hans Boehm, Steve Clamage, Lawrence Crowl, Beman Dawes, Francis Glassborow, Doug Gregor, Pablo Halpern, Howard Hinnant, Jaakko Jarvi, John Lakos, Alisdair Meredith, Jens Maurer, Jason Merrill, Sean Parent, P.J. Plauger, Tom Plum, Gabriel Dos Reis, Bjarne Stroustrup, Herb Sutter, David Vandevoorde, Michael Wong。向我们无法列出的 200 多位现任和前任成员致歉。此外,请注意各种论文的作者列表:标准是由(许多)个人编写的,而不是由一个匿名的委员会编写的。

通过查阅WG21 论文档案中列出的作者,您可以更好地了解所涉及的专业知识的广度和深度,但请记住,标准工作中还有许多主要贡献者并不常写作。

我在哪里可以获取 C++ 标准的副本?

请参阅 isocpp.org 的标准页面

C++98 和 C++03 有什么区别?

从程序员的角度来看,没有区别。C++03 标准的修订版是针对实现者的 bug 修复版本,以确保更大的一致性和可移植性。特别是,描述 C++98 和 C++03 的教程和参考资料除了编译器编写者和标准专家外,所有人都可互换使用。

C++98 和 C++0x 有什么区别?

C++98 和 C++11 的区别相同,因为 C++0x 最终变成了 C++11。C++0x 的 x 变成了十六进制;我们有了 C++0xB。:)

C++98 和 C++11 有什么区别?

请参阅C++11 新功能

请注意,C++ 语言将保持稳定,因为兼容性始终是一个主要关注点。委员会努力不破坏您的(符合标准的)代码。除了您可能不会注意到的一些边缘情况外,所有有效的 C++98 代码都是有效的 C++11 和 C++14 代码。

C++11 和 C++14 有什么区别?

请参阅C++14 新功能

请注意,C++ 语言将保持稳定,因为兼容性始终是一个主要关注点。委员会努力不破坏您的(符合标准的)代码。除了您可能不会注意到的一些边缘情况外,所有有效的 C++98 代码都是有效的 C++14 代码。

有哪些“面试问题”可以用来判断候选人是否真正了解他们的专业知识?

这个答案主要是为非技术经理和人力资源人员准备的,他们正努力做好C++候选人的面试工作。如果你是一名即将接受面试的C++程序员,并且你潜伏在这个常见问题(FAQ)中,希望能提前知道他们会问什么问题,从而避免不得不真正学习C++,那么你应该感到羞耻:花时间让自己在技术上变得胜任,你就无需试图“作弊”过活!

回到非技术经理/人力资源人员:显然,你完全有资格判断候选人是否与贵公司的文化“契合”。然而,市面上有足够的江湖骗子、冒牌货和装腔作势者,你真的需要与一位技术能力强的人合作,以确保候选人具备正确的技术水平。许多公司都曾因雇佣了友善但无能的庸才而蒙受损失——这些人虽然能回答一些生僻问题,但实际上却无能。要识破那些装腔作势者和冒牌货的唯一方法,就是请一位能提出深入技术问题的人与你一同面试。你根本无法独自做到这一点。即使我给你一堆“刁钻问题”,也无法识破那些坏家伙。

你的技术搭档可能不(而且通常不)够格根据个性或软技能来评价候选人,所以请不要放弃你作为决策过程中最终仲裁者的角色。但请不要以为你可以问几个C++问题,就能对候选人是否真正了解他们在技术上所谈论的内容有丝毫的了解。

话虽如此,如果你技术水平足够阅读C++常见问题(FAQ),你可以在这里找到许多不错的面试问题。本FAQ包含大量精品,能帮你去伪存真。本FAQ侧重于程序员“应该”做什么,而不是仅仅编译器“允许”他们做什么。有些事情在C++中可以做,但不应该做。本FAQ帮助人们区分这两者。

FAQ 中“某某是邪恶的”是什么意思?

意思是“某某”是你大多数时候应该避免的,但不是所有时候都应该避免的。例如,当你遇到“邪恶选择中最不邪恶的”情况时,你最终会使用这些“邪恶”的东西。这只是个玩笑,好吗?别太当真。

这个术语的真正目的(“啊哈,”我听你说道,“果然隐藏的动机!”;你说得对:确实有)是让新的C++程序员摆脱一些旧有的思维方式。例如,刚接触C++的C程序员往往会过度使用指针、数组和/或`#define`。常见问题解答将这些列为“邪恶”,是为了给新的C++程序员一个有力(且诙谐!)的正确推动。像“指针是邪恶的”这类荒谬说法的目标是让新的C++程序员相信C++真的不是“除了那些愚蠢的`//`注释之外,就像C一样。”

现在我们来认真一下。我并不是说宏、数组或指针和谋杀、绑架一样糟糕。好吧,也许指针可以算。(开玩笑!)所以别对“邪恶”这个词太激动:它本就应该听起来有点夸张。也别去寻找一个技术上精确的定义,来界定某个东西何时是“邪恶的”或何时不是:根本就没有。

被标记为“邪恶”(宏、数组、指针等)的项并非在所有情况下都总是坏的。当它们是备选方案中“最不坏”的选择时,使用它们

我有时会使用那些所谓的“邪恶”结构吗?

当然会!

一刀切的方案并不适用。现在,立刻拿出一支细头笔,在你的眼镜内侧写上:软件开发决策。“思考”不是一个贬义词。软件中很少有“从不……”和“总是……”的规则——那些无需思考就能应用的规则——那些在所有情况下、所有市场中都始终适用的规则——一刀切的规则。

简单来说,你不得不做出决定,而你决定的质量影响你软件的商业价值。软件开发是主要关于盲目遵循规则;它是一个思考、权衡和选择的问题。有时你将不得不在一堆糟糕的选项中做出选择。当这种情况发生时,你所能期望的最好结果是选择备选方案中最不坏的,即“邪恶”中较小的那一个。

偶尔使用被标记为“邪恶”的方法和技术。如果这让你感到不舒服,可以在心里把“邪恶”这个词改为“经常不理想”(但不要为了成为作家而辞掉你的日常工作:milquetoast 这样的词会让人昏昏欲睡 :-)

了解“好的面向对象”的技术定义重要吗?“好的类设计”呢?

你可能不喜欢这个答案,但简短的回答是,“不。”(但需注意的是,此答案是针对实践者,而非理论家。)

成熟的软件设计师在评估情况时,除了“好的OO”或“好的类设计”等技术标准外,还会依据业务标准(时间、金钱和风险)进行。这要困难得多,因为它除了技术问题外,还涉及业务问题(日程、人员技能、了解公司发展方向以便知道在软件中何处设计灵活性、愿意考虑未来变化的发生可能性——那些可能发生而非仅仅理论上可能的改变等)。然而,它能带来更有可能带来良好业务成果的决策。

作为一名开发者,你对雇主负有受信责任,即只进行那些有合理预期投资回报的投资。如果你在提出技术问题之余,提出业务问题,你所做的决策将带来随机且不可预测的业务后果。

不管你喜欢与否,这在实践中意味着你最好不要定义“好的类设计”和“好的面向对象”这类术语。事实上,我相信这些术语的精确、纯粹的技术定义可能是危险的,可能让公司付出金钱,最终甚至可能让人失去工作。这听起来很奇怪,但有一个很好的理由:如果这些术语以精确、纯粹的技术术语来定义,善意的开发者往往会在渴望满足这些“好”的纯粹技术定义时,忽视业务考量。

任何纯粹技术性的“好”定义,例如“好的面向对象”或“好的设计”,或任何其他可以不考虑日程、业务目标(以便我们知道在哪里投资)、预期未来变化、公司对未来投资的意愿、维护团队的技能水平等而进行评估的定义,都是危险的。这是危险的,因为它欺骗程序员以为他们在做出“正确”的决定,而实际上他们可能正在做出可怕后果的决定。或者那些决定可能不会带来可怕的商业后果,但重点是:当你在做决策时忽视商业考量,商业后果将是随机且有些不可预测的。这很糟糕。

这是一个简单的事实:业务问题主导技术问题,任何未能承认这一事实的“好”的定义都是不好的。

如果有人抱怨“FAQ”这个词有误导性,因为它强调问题而非答案,我们都应该开始使用不同的缩写,我该如何回应他们?

告诉他们长大了。

有些人想把“FAQ”这个词改成不同的缩写,比如强调答案而不是问题的缩写。然而,一个词或短语是由其用法定义的。许多人已经将“FAQ”理解为一个独立的词。把它看作一个概念的代号,而不是一个缩写。作为一个词,“FAQ”已经意味着常见问题答案的列表。

不要把这视为鼓励粗心地使用词语。恰恰相反。重点是清晰的沟通涉及使用大家已经理解的词语。就我们是否应该更改“FAQ”这个词进行争论是愚蠢的,也是浪费时间的。如果这个词尚未广为人知,那倒另当别论,但在这么多人已经理解它之后,再这样做就没有意义了。

一个(不完美的)类比:字符 `'\n'` 几乎普遍被认为是换行符,但如今很少有程序员使用配备电传打字机的电脑,能真正执行“换行”。现在没人再关心这些了;它就是换行符;接受它吧。而 `'\r'` 是回车符,即使你的电脑可能没有可以回的车。接受它吧。

另一个(不完美的)类比是RAII。多亏了Andy KoenigBjarne Stroustrup等人的出色工作,“RAII”这个名称在C++社区中广为人知。“RAII”代表了一个非常有价值的概念,你应该经常使用它然而,如果你将“RAII”作为一个缩写词进行剖析,并且(过于?)仔细地审视构成该缩写词的单词,你会意识到这些单词与概念并不完全匹配。谁在乎呢?!概念才是重要的;“RAII”仅仅是该概念的代号。

细节:如果你剖析RAII(资源获取即初始化)这个缩写词的单词,你会认为RAII是关于在初始化期间获取资源。然而,RAII的力量并非来自将获取初始化绑定,而是来自将回收销毁绑定。一个更精确的缩写词可能是RRID(资源回收即销毁),也许是DIRR(销毁即资源回收),但既然这么多人已经理解RAII,那么正确使用它远比抱怨这个术语重要。RAII是一个概念的代号;它作为缩写词的精确性是次要的。

(缩写词中真正重要的是它们听起来有多酷。显然“RRID”和“DIRR”因此更好!开玩笑的。)

因此,请将“FAQ”这个词视为一个已确立且广为人知的代号。一个词是由其用法定义的。