C++标准库的学习笔记 -- 语言支持库
语言支持库(language support library)首先应该声明一下,我现在介绍的内容是遵循 2003 年的C++标准来的 。2011年 C++标准委员会对 C++标准做了大幅度的修改,也许会改变很多细节。但应该相信,很多我介绍的东西,并不会受到这次修约的影响。
从理论上讲,C++ 语言是一种形式逻辑,它是独立于任何一种实现的。但是无论如何,它最终得落实到实现上。标准中有很多细节,用以描述什么样的实现是被称作“遵循标准”的实现,但事实上没有完成遵循标准的实现。一方面表现为标准要求实现的东西,并没有实现,或者没有完全按照要求实现。(我印象里 C++ 标准里的有些特性规定的比较牵强,以至自始至终就没有任何一个版本的编译器是按要求实现的。但我现在也想不起来是哪条了。)
另一方面表现为编译器实现了很多标准并没有做要求的内容,这一般被称为扩展。对标准做扩展这种行为是标准允许的。但在扩展以遵循其它标准时(如 POSIX 标准)就不免有些地方会和C++标准相抵触,或者使得对 C++标准的实现很牵强。有些实现会尝试尽量遵循标准,但也有些实现对标准扭曲的很厉害。如 VC6.0 就选择了不实现标准的很多的内容,并且实现了的部分也有很多是与标准相抵触的。网上能找到很多文章阐述为什么不应该选择 VC6.0,我就不重复这些东西了。
概述:
语言支持库总的来说是指:提供数据描述和运行时支持相关的标准库部分。比如标准有很多内容推给实现自由发挥,其目的就是为了让 C 或者 C++ 可以在更多的平台上发挥更好的性能。最著名的比如 int 的长度就没有规定死,这样就使得在 16 位机上不必蹩脚地实现32位加法;或者浪费 32 位机的能力,只去运算16位的数值。但这样一来,为了编写可移植的程序,就需要一些手段来确定这些“可变”的内容。大家也许都熟知 sizeof 运算符,但语言提供我们的不只是这一种手段。这关这方面的内容,我会在下面 实现特征 一节讲解。我另一个准备细致讲解的是动态内存管理,因为这块大家的误区也比较多。
由于有些涉及实现的内容(我会尽量减少这些内容),那么就会和硬件,操作系统和编译器等等因素都有关系。限于我的知识,我只能以 intel 80x86 架构,Unix系统(可能会涉及少量 Linux 的特性) gcc/g++ 为例讲解。如果有人能指出我描述中的错误,或者能补充 Windows 系统的有关知识,我将非常感谢。
组成结构:
标准把语言支持库又划分为七部分:
类型定义(Types):<cstddef>
实现特征(Implementation properties ):<limits> , <climits> , <cfloat>
启动与终止(Start and termination ):<cstdlib>
动态内存管理(Dynamic memory management ):<new>
类型认证(Type identification ):<typeinfo>
异常处理(Exception handling ):<exception>
其它运行时支持(Other runtime support ):<cstdarg>, <csetjmp> , <ctime> , <csignal> , <cstdlib>
类型定义:
<cstddef> 里定义了一些宏和类型。比如宏 NULL表示空指针,宏offsetof 用于取POD成员的偏移量等 和类型 ptrdiff_t 用于表示指针类型的差,size_t 用于表示与机器相关的无符号整数之类的,我并不打算详细介绍。
只是标准中的有一条规定,我也是前几天才刚知道:标准规定宏 NULL 应该是由实现定义的指针常量。并强调可以是 0 或者 0L,但不能是 (void *)0。事实上我以前一直以为是 (void *)0 呢。看完这一条,我验证了,gcc (版本 2.8 以后)使用了内置的指针常量 __null。
启动与终止:
<cstdlib> 定义了宏 EXIT_SUCCESS 和 EXIT_FAILURE (没有规定它们的值取多少,但一般分别是 0 和 1)。定义了与终止程序有关的三个函数 abort atexit exit。
exit 和 abort 的区别主要是,exit 退出的时候会析构静态变量,和自动变量(换句话说就是 exit 也不管动态分配的变量,它们会在程序终止后由系统回收)。并按逆序调用 atexit 注册的函数。而 abort 不干这些事情。Main 函数执行到 return 语句时,相当于执行 exit。有关它们的其它细节请自己行查阅资料。
程序代码:
#include <cstdlib> #include <iostream> using namespace std; class A { public: A() { cout << "constructing class A" << endl; } ~A() { cout << "destructing class A" << endl; } }; class B { public: B() { cout << "constructing class B" << endl; } ~B() { cout << "destructing class B" << endl; } }; void f1() { cout << "calling f1()" << endl; } void f2() { cout << "calling f2()" << endl; } int main() { static A a; // 注意静态对象构造和函数注册的顺序。 atexit(&f1); // 标准要求执行 f1 的时候,已经存在的静态变量必须尚未析构。 static B b; // 而还未构造的静态变量必须已经析构完成。 atexit(&f2); // 注意函数注册的顺序。 atexit(&f1); // 一个函数可以注册数次,注册多次的函数依然按照注册的逆序被调用。 exit(0); // abort(); // 分别按 exit 和 abort 处理。 }exit 和 abort 的输出分别是:
constructing class A #先构造A,之后注册 f1,不过注册没有任何输出显示。
constructing class B #再构造B。
calling f1() #最后一次注册f1的时候,A,B都己存在,故此时 A,B应该保证都未析构。
calling f2() #按注册的反序,此时会调用f2。
destructing class B #B是在第一次 f1注册之后才创建的。要保证 f1 执行前完成析构。
calling f1()
destructing class A
constructing class B #再构造B。
calling f1() #最后一次注册f1的时候,A,B都己存在,故此时 A,B应该保证都未析构。
calling f2() #按注册的反序,此时会调用f2。
destructing class B #B是在第一次 f1注册之后才创建的。要保证 f1 执行前完成析构。
calling f1()
destructing class A
constructing class A
constructing class B
Aborted (core dumped) #abort的话输出就少多了。这行是 Linux 为意外终止的程序执行核心转储时输出的。它转储的内容一般可以送去给程序的开发者研究程序意外终止的原因。
constructing class B
Aborted (core dumped) #abort的话输出就少多了。这行是 Linux 为意外终止的程序执行核心转储时输出的。它转储的内容一般可以送去给程序的开发者研究程序意外终止的原因。
类型认证:
定义了 type_info 类(用以实现 typeid 表达式),和 bad_cast, bad_typeid 这两个异常。我觉得介绍库的话,就没必要介绍这些东西了,概念比较多,都是语法上的东西。typeid, dynamic_cast 提供了运行时检查机制,如果不熟悉相关的语法,请自行查阅资料。(如果确实不熟悉最好是把static_cast , reinterpret_cast , const_cast 也一并查查,它们很相关。)
我只提一点,也许很多其它书也会提到:尽量不要用 typeid(obj).name() 这样的表达式,因为它依赖与实现。标准只规定它必须返回一个尾缀空字符的字符串(NTBS, Null-Termiated Byte String),并且连这个字符串是宽窄字符串都没有做规定。可能的话,最好这样:
程序代码:
#include <iostream> #include <typeinfo> using namespace std; int main() { double a = 5.5; cout << typeid(a).name() << endl; // 输出依赖实现。 if (typeid(a) == typeid(double)) // 条件应该成立。 cout << "true" <<endl; return 0; }
异常处理:
定义了 exception 和 bad_exception 两个类。定义了两个类型(其实是个 typedef) unexpected_handler 和 terminate_handler 。和若干函数 set_unexpected , unexpected, set_terminate , terminate, uncaught_exception。概念比较多,很难详细介绍。另外,相信所有讲解异常处理的书籍都会细致讲解这些东西。
其它运行时支持:
包括可变参数列表、longjump、时间处理和信号处理。
longjump 机制一般在 C 语言里是用来做类似异常处理的,比较经典的应用可能是表达式解析。我觉得在 C++ 里得没有什么太大的用处了,就不介绍了。另外 C++ 里关于时间处理和信号处理几乎没有比 C 多什么东西。主要原因可能是这些功能严重依赖操作系统的能力,C++ 无法预计系统有多少能力来处理时间和信号,所以只能提出一些最基本的模型框架(毫无疑问,这个框架只能是无数年前 C 语言已经规范好了的东西)。如果需要更多对时间及信号的处理功能,就应该在一定程度上放弃移植性而使用系统调用。
至于可变参数列表(variable argument list ),其实也不是很常用。但有些函数需要这种功能,比如大家最熟悉的 printf。标准规定了 va_start, va_arg, va_end 三个宏,但 gcc 使它们成了内置功能,没有用宏来实现它们,我没觉得这样有什么不妥。另外,这三个宏的经典实现很容易从网上查到。它们的语法点不多,我举个例子就是了,详细资料自行百度:
程序代码:
#include <iostream> #include <cstdarg> using namespace std; double sum(int n, ...) // 求 n 个 double 的和。 { va_list ap; va_start(ap, n); // 以 n 为基准,建立一个可变参数列表。 double res = 0.0; for (int i = 0; i < n; i++) res += va_arg(ap, double); // 循环读取列表中的 double 并累加。 va_end(ap); // 析构这个列表。 return res; } int main() { cout << sum(3, 1.1, 2.2, 3.3) << endl; // 输出 6.6 cout << sum(5, 1.1, 1.1, 1.1, 1.1, 1.1) << endl; // 输出 5.5 return 0; }
[ 本帖最后由 pangding 于 2012-4-15 17:55 编辑 ]