![]() |
#2
pangding2012-03-31 02:07
实现特征:
移植是个很复杂的话题。与 windows 不同,Unix 更加开放,它可以运行在各种各样的平台上,并且有大量的变种。充分考虑移植性的程序,也许可以轻松的在各种 Unix 上运行,否则移植就会有很多阻力。曾经的 windows 只能在固定的平台上运行,但也多少会面临一个软件是否可以在 win7, xp, 甚至更老的系统上兼容的问题。而且现在的 windows 也开始尝试在更多的平台上运行了,那么要想开发出在各个平台上都能运行的程序,也许也得开始考虑更多的问题。不过现在好多软件为了移植也开始在网络这个平台上下功夫。时代变化的很快,很多东西都往前走了很远。 抛开复杂的移植性问题,大家也都知道,即使在同一台电脑同一个操作系统上不同的编译器,甚至在同一个编译器的不同选项控制下,编译出来的程序也会执行出不同的结果。有些情况确实是在滥用语言,比如大家经常问的很多有关 ++ 的问题,其实就属于这类。在任何编译器下都应该禁止使用这样的语句。但也有些是受限于系统(凡是提到系统这个词,很多时候并不是指操作系统(OS),而是指编译器的整个体系。如果我是在指OS,我尽量说全了,但一般大家从上下文还是能推出来的吧。毕竟在很多讲C++书籍上看到的系统往往也不是指OS),比如 sizeof 表达式的结果就往往会有所不同。 实现特征(Implementation properties)这个部分就是 C++ 语言提供的一种机制,使得我们在编译时有能力获得有关该实现的一些描述。其实我对这部分的内容也很陌生,基本上是在写这篇文章的时候才学的。因为涉及的很多东西,用 C 语言里的那套东西都能解决。如果大家熟悉 C 里面的那套处理方法,我认为可以不用管 C++ 的这套东西。当然 C++ 也有它自己的特色。有关这方面的东西原理都很简单,但很细致。细节的介绍不是这篇文章的重点,再次指出,有兴趣的朋友可以自行查阅相关资料。 很多时候,只知道 sizeof(int) 的值是不够的。因为即使长度相等,由于在不同平台上使用的补码方式不同,它们能表示的范围也很可能不一样。更不要说 double 这些类型了。并且就算自己能够用 sizeof 的值推算出 int 的上下限(利用if 测试一下补码的形式就可以保证移植性),直接获知 int 能表示的范围依然会很有帮助,更何况还有 double 这样的类型。而事实上这也确实能做到。对比 C 与 C++ 的做法: ![]() #include <stdio.h> #include <limits.h> int main(int argc, char *argv[]) { printf("%d\n", INT_MAX); // 输出:2147483647(不同的实现可能会有不同的结果) printf("%d\n", INT_MIN); // 输出:-2147483648 return 0; } ![]() #include <iostream> C 是使用宏,而 C++ 使用的是模版类。所有能在 C++ 做的,基本都能从 C 中取得,我在之后注释里写的就是与它等价的宏名。这样也许能方便熟悉 C 式处理方法的人快速理解相关函数的意思。#include <limits> using namespace std; int main() { numeric_limits<int> i; cout << i.max() << endl; // INT_MAX cout << i.min() << endl; // INT_MIN return 0; } 这个模版类只对基本类型有效,基本类型就是指各种有无符号的,或长或短的整形,宽窄字符型,布尔型,浮点型等等。可以测试 is_specialized 来判断是不是基本类型 。如果不是基本类型(即使是标准库中的数据类型也可能不是基本类型),则对相关函数的调用,不是返回 0 就是返回 false。 ![]() #include <iostream> 要讲解这个模版类,用 double 举例似乎更有意义:#include <limits> #include <complex> using namespace std; int main() { numeric_limits< complex<double> > c; if (c.is_specialized != true) // 条件成立 cout << "not a fundamental standard type" << endl; cout << c.max() << endl; // 输出:(0,0) cout << c.min() << endl; // 输出:(0,0) return 0; } ![]() #include <iostream> 这个类还有其它一些成员,大家可以自己看看头文件,不过别忘了你用的编译器有可能会对标准做扩展,因此头文件里有的不一定都是标准要求的。有些函数只对浮点数有效,用 int 来弄也能输出结果,但不一定有意义。还有些函数,对 int 输出值的意义与 double 有些区别。但逐个介绍太细了,而且好多东西,我觉得有兴趣的话,自己研究一下基本就能搞明白。#include <limits> using namespace std; int main() { numeric_limits<double> d; if (d.is_specialized == true) // 成立 cout << "a fundamental standard type" << endl; // 测试实现是否遵循 IEC559(也叫IEEE754)标准。 // 这个标准定义了一种浮点数的表示方法,应用非常广。在绝大多数平台上都应该为 true。 if (d.is_iec559 == true) cout << "conforming to IEC559" << endl; // double 能表示的最大值和规格化(normalized )最小值。 cout << d.max() << endl; // DBL_MAX ,输出:1.79769e+308 cout << d.min() << endl; // DBL_MIN ,输出:2.22507e-308 // 如果支持非规格化浮点数,输出非规格化最小值。否则其值与 min() 相同。 if (d.has_denorm == denorm_present) cout << d.denorm_min() << endl; // 输出:4.94066e-324 // 进位基数(显然一般都是2) 和 // 精度数(就是IEEE754里定义的p的值),其值是 double 的 64 位中用来表示尾数的位数+1。 // 和科学计数法里的有效数字的概念比较接近。可以理解成以二进制表示的话,小数点后可以精确到多少位。 // 遵循IEEE754标准的话,其值应该是53。 // 以十进制表示是15。也可以理解成,double 表示的数可以精确到小数点后15位。 cout << d.radix << endl; // FLT_RADIX ,输出:2 cout << d.digits << endl; // DBL_MANT_DIG ,输出:53 cout << d.digits10 << endl; // DBL_DIG ,输出:15 // 指数的取值范围。64位中剩余的用来表示指数的那几位数可能的取值范围。 // 注意它们与 min() 和 max() 输出结果之间的联系。(尤其是 exponent10 的联系) cout << d.min_exponent << endl; // DBL_MIN_EXP ,输出:-1021 cout << d.min_exponent10 << endl; // DBL_MIN_10_EXP ,输出:-307 cout << d.max_exponent << endl; // DBL_MAX_EXP ,输出:1024 cout << d.max_exponent10 << endl; // DBL_MAX_10_EXP ,输出:308 // 重要性误差与舍入误差限 // 前者是指系统能表示的比1大的最接近1的数与1的差。 // 说着挺绕口,就是说介于1和1+e之间的数,一定会被舍入到1或者1+e上。 // 后者是说,在发生舍入的情况下,最大会有多少误差。(但其实绝大多数情况不会误差到这么多) cout << d.epsilon() << endl; // DBL_EPSILON ,输出:2.22045e-16 cout << d.round_error() << endl; // 输出:0.5 // 舍入方式。可能的取值是 -1, 0, 1, 2, 3。(其实是一个 enum,有各自的名字) // -1(round_indeterminate ) 表示舍入方式未定。 // 0-3分别表示向零舍入,就近舍入,向上舍入,和向下舍入。 cout << d.round_style << endl; // FLT_ROUNTDS ,输出:1,即round_to_nearest 。 return 0; } 有关IEE754标准的事,以前有论坛里很多人发帖讨论过了,大家可以自己搜一搜。我这里一言半语肯定是说不清楚。论坛里有一篇讲IEEE754标准的帖子,我觉得非常详细,大家可以看看:https://bbs.bccn.net/thread-44500-1-1.html 如果大家熟悉 IEEE754标准,这里输出的这些值其实自己都能算出来。但是可想而知,直接获取能方便多少。 [ 本帖最后由 pangding 于 2012-4-15 17:56 编辑 ] |
首先应该声明一下,我现在介绍的内容是遵循 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 的输出分别是:#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 处理。
}
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;
}
#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;
}
#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 编辑 ]