返回介绍

2.2 第二个十年

发布于 2024-08-19 12:44:37 字数 17280 浏览 0 评论 0 收藏 0

ANSI C++ 委员会是 1989 年 12 月在华盛顿特区的一次会议上成立的,距离第一次使用带类的 C这个名称仅仅 10 年多的时间。大约有 25 名 C++ 程序员出席了会议。我出席了会议,还有另外一些近些年来依然活跃的 ISO C++ 标准委员会成员当时也在。

经过了惯例性的、大约十年的工作,该委员会终于发布了第一个标准:C++98。我和许多其他人自然更愿意更快地输出一个标准,但是委员会规则、过度的雄心和各种各样的延迟使我们在时间表方面与 Fortran、C 和其他正式标准化的语言站在了同一起跑线上。

形成 C++98 的工作是 HOPL3 论文的核心 [Stroustrup 2007],所以这里我只简单总结一下。

2.2.1 语言特性

C++98 的主要语言特性是

  • 模板——无约束的、图灵完备的、对泛型编程的编译期支持,在我早期工作(§2.1)的基础上进行了许多细化和改进;这项工作仍在继续(§6)。
  • 异常——一套在单独(不可见的)路径上返回错误值的机制,由调用方栈顶上的在别处的代码处理;见(§7)。
  • dynamic_casttypeid——一种非常简单的运行期反射形式(运行期类型识别,又名 RTTI)。
  • namespace——允许程序员在编写由几个独立部分组成的较大程序时避免名称冲突。
  • 条件语句内的声明——让写法更紧凑和限制变量作用域。
  • 具名类型转换——(static_castreinterpret_castconst_cast):消除了 C 风格的类型转换中的二义性,并使显式类型转换更加显眼。
  • bool:一种被证明非常有用和流行的布尔类型;C 和 C++ 曾经使用整数作为布尔变量和常量。

让我们看一个简单的 C++98 例子。dynamic_cast 是面向对象语言中常被称为类似是某种的概念的 C++ 版本:

void do_something(Shape* p)
{
    if (Circle* pc = dynamic_cast<Circle*>(p)) { // p 是某种 Circle?
        // ... 使用 pc 指向的 Circle ...
    }
    else {
        // ... 不是 Circle,做其他事情 ...
    }
}

dynamic_cast 是一个运行期操作,依赖于存储在 Shape 的虚拟函数表中的数据。它通用、易用,并且与其他语言类似的功能一样高效。然而,dynamic_cast 变得非常不受欢迎,因为它的实现往往是复杂的,特殊情况下手动编码可能更高效(可以说这导致 dynamic_cast 违反了零开销原则)。在条件语句里使用声明很新颖,不过当时我认为我只是沿用了 Algol68 里的这个主意而已。

一种更简单的变种是使用引用而不是指针:

void do_something2(Shape& r)
{
    Circle& rc = dynamic_cast<Circle&>(r);  // r 是某种 Circle!
    // ... 使用 rc 引用的 Circle ...
}

这简单地断言 r 指代一个 Circle,如果不是则抛出一个异常。思路就是,错误能够在本地被合理地处理时,使用指针和测试,如果不能则依赖引用和异常。

C++98 中最重要的技术之一是 RAII(Resource Acquisition Is Initialization, 资源获取即初始化)。那是我给它取的一个笨拙的名字,想法就是每个资源都应该有一个所有者,它由作用域对象表示:构造函数获取资源、析构函数隐式地释放 它。这个想法出现在早期的带类的 C中,但直到十多年后才被命名。这里有一个我经常使用的例子,用来说明并非所有资源都是内存:

void my_fct(const char* name)  // C 风格的资源管理
{
    FILE* p = fopen(name, "r");  // 打开文件 name 来读取
    // ... 使用 p ...
    fclose(p);
}

问题是,如果(在 fopen()fclose() 的调用之间)我们从函数 return 了,或者 throw 了一个异常,或者使用了 C 的 longjmp,那么 p 指向的文件句柄就泄漏了。文件句柄泄漏会比内存泄漏更快地耗尽操作系统的资源。这个文件句柄是非内存资源的一个例子。

解决方案是将文件句柄表示为带有构造函数和析构函数的类:

class File_handle {
    FILE* p;
public:
    File_handle(const char* name,const char* permissions);  // 打开文件
    ~File_handle();  // 关闭文件
    // ...
};

我们现在可以简化我们的用法:

void my_fct2(const char* name)  // RAII 风格的资源管理
{
    File_handle p(name,"r");    // 打开文件 name 来读取
    // ... 使用 p ...
} // p 被隐式地关闭

随着异常的引入,这样的资源句柄变得无处不在。特别的,标准库文件流就是这样一个资源句柄,所以使用 C++98 标准库,这个例子变成:

void my_fct3(const string& name)
{
    ifstream p(name);    // 打开文件 name 来读取
    // ... 使用 p ...
} // p 被隐式的关闭

请注意,RAII 代码不同于传统的函数使用,它允许在库中一劳永逸地定义清理内存,而不是程序员每次使用资源时都必须记住并显式编写。至关重要的是,正确和健壮的代码更简单、更短,并且至少与传统风格一样高效。在接下来的 20 年里,RAII 已遍布 C++ 库。

拥有非内存资源意味着垃圾收集本身不足以进行资源管理。此外,RAII 加上智能指针(§4.2.4)消除了对垃圾收集的需求。另见(§10.6)。

2.2.2 标准库组件

C++98 标准库提供了:

  • STL——创造性的、通用的、优雅的、高效的容器、迭代器和算法框架,由 Alexander Stepanov 设计。
  • 特征(trait)——对使用模板编程有用的编译期属性集(§4.5.1)。
  • string——一种用于保存和操作字符序列的类型。字符类型是一个模板参数,其默认值是 char
  • iostream——由 Jerry Schwartz 和标准委员会精心制作,基于我 1984 年的简单的数据流,处理各种各样的字符类型、区域设置和缓冲策略。
  • bitset——一种用于保存和操作比特位集合的类型。
  • locale——用来处理不同文化传统的精致框架,主要与输入输出有关。
  • valarray——一个数值数组,带有可优化的向量运算,但遗憾的是,未见大量使用。
  • auto_ptr——早期的代表独占所有权的指针;在 C++11 中,它被 shared_ptr(共享所有权)和 unique_ptr(独占所有权)(§4.2.4)替代。

毫无疑问,STL 框架是最为重要的标准库组件。我认为可以说,STL 和它开创的泛型编程技术挽救了 C++,使它成长为一种有活力的现代语言。像所有的 C++98 功能一样,STL 在其他地方已经有了广泛的描述(例如 [Stroustrup 1997, 2007]),所以在这里我只会给出一个简单的例子:

void test(vector<string>& v, list<int>& lst)
{
    vector<string>::iterator p
        = find_if(v.begin(), v.end(), Less_than<string>("falcon"));
    if (p != v.end())  {  // p 指向 'falcon'
        // ... 使用 *p ...
    }
    else {                // 没找到 'falcon'
        // ...
    }

    list<int>::iterator q
        = find_if(lst.begin(), lst.end(), Greater_than<int>(42));
    // ...
}

标准库算法 find_if 遍历序列(由 begin/end 定界)寻找谓词为真的元素。该算法在三个维度上都是通用的:

  • 序列元素的存储方式(这里是 vectorlist
  • 元素的类型(这里是 stringint
  • 用于确定何时找到元素的谓词(此处为 Less_thanGreater_than

注意这里没有用到任何面向对象的方法。这是依赖模板的泛型编程,有时也被称为编译期多态。

模板的写法仍然很原始,但是从 2017 年左右开始,我可以使用 auto(§4.2.1)、范围(§9.3.5)和 lambda 表达式(§4.3.1)来简化代码:

void test2(vector<string>& v, list<int>& lst)
{
    auto p = find_if(v,[](const string& s) { return s<"falcon"; })
    if (p!=v.end()) {
        // ...
    }
    // ...
    auto q = find_if(lst,[](int x) { return x>42; })
    if (q!=lst.end()) {
        // ...
    }
    // ...
}

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文