2.2 第二个十年
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_cast
和typeid
——一种非常简单的运行期反射形式(运行期类型识别
,又名 RTTI)。namespace
——允许程序员在编写由几个独立部分组成的较大程序时避免名称冲突。- 条件语句内的声明——让写法更紧凑和限制变量作用域。
- 具名类型转换——(
static_cast
、reinterpret_cast
和const_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
定界)寻找谓词为真的元素。该算法在三个维度上都是通用的:
- 序列元素的存储方式(这里是
vector
和list
) - 元素的类型(这里是
string
和int
) - 用于确定何时找到元素的谓词(此处为
Less_than
和Greater_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 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论