| 网站首页 | 业界新闻 | 小组 | 威客 | 人才 | 下载频道 | 博客 | 代码贴 | 在线编程 | 编程论坛
欢迎加入我们,一同切磋技术
用户名:   
 
密 码:  
共有 2563 人关注过本帖, 1 人收藏
标题:C++标准库的学习笔记 -- 语言支持库
取消只看楼主 加入收藏
pangding
Rank: 19Rank: 19Rank: 19Rank: 19Rank: 19Rank: 19
来 自:北京
等 级:贵宾
威 望:94
帖 子:6784
专家分:16751
注 册:2008-12-20
结帖率:96%
收藏(1)
 问题点数:0 回复次数:4 
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 A
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 编辑 ]
搜索更多相关主题的帖子: 东西 学习 委员会 影响 
2012-03-31 02:03
pangding
Rank: 19Rank: 19Rank: 19Rank: 19Rank: 19Rank: 19
来 自:北京
等 级:贵宾
威 望:94
帖 子:6784
专家分:16751
注 册:2008-12-20
收藏
得分:0 
实现特征:
    移植是个很复杂的话题。与 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>
#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;
}
    C 是使用宏,而 C++ 使用的是模版类。所有能在 C++ 做的,基本都能从 C 中取得,我在之后注释里写的就是与它等价的宏名。这样也许能方便熟悉 C 式处理方法的人快速理解相关函数的意思。

    这个模版类只对基本类型有效,基本类型就是指各种有无符号的,或长或短的整形,宽窄字符型,布尔型,浮点型等等。可以测试 is_specialized 来判断是不是基本类型 。如果不是基本类型(即使是标准库中的数据类型也可能不是基本类型),则对相关函数的调用,不是返回 0 就是返回 false。
程序代码:
#include <iostream>
#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;
}
    要讲解这个模版类,用 double 举例似乎更有意义:
程序代码:
#include <iostream>
#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;
}
    这个类还有其它一些成员,大家可以自己看看头文件,不过别忘了你用的编译器有可能会对标准做扩展,因此头文件里有的不一定都是标准要求的。有些函数只对浮点数有效,用 int 来弄也能输出结果,但不一定有意义。还有些函数,对 int 输出值的意义与 double 有些区别。但逐个介绍太细了,而且好多东西,我觉得有兴趣的话,自己研究一下基本就能搞明白。
    有关IEE754标准的事,以前有论坛里很多人发帖讨论过了,大家可以自己搜一搜。我这里一言半语肯定是说不清楚。论坛里有一篇讲IEEE754标准的帖子,我觉得非常详细,大家可以看看:https://bbs.bccn.net/thread-44500-1-1.html
如果大家熟悉 IEEE754标准,这里输出的这些值其实自己都能算出来。但是可想而知,直接获取能方便多少。

[ 本帖最后由 pangding 于 2012-4-15 17:56 编辑 ]
2012-03-31 02:07
pangding
Rank: 19Rank: 19Rank: 19Rank: 19Rank: 19Rank: 19
来 自:北京
等 级:贵宾
威 望:94
帖 子:6784
专家分:16751
注 册:2008-12-20
收藏
得分:0 
动态内存管理:   
    提到动态内存管理,大家都很熟悉。就是指的new表达式,和delete表达式。虽然我这篇文章以介绍标准库的组成为主,但有关new 和 delete 的语法我也稍带着介绍一下。   
    new用来动态分配对象的储存空间,并返回指向该对象的一个指针。如果用来申请数组,那么返回的是指向第一个元素的指针。可以在创建对象的同时,提供一个初始化列表以调用对象的构造函数。即使是内置对象也可以使用这种语法,这在很多时候这可以提供方便。但也有限制,因为申请数组时不能使用这种初始化方法,后面我会介绍这种情况的解决办法。
程序代码:
#include <iostream>
#include <new>
using namespace std;

int main()
{
    int *p = new int(5);    // 申请一个 int,并初始化为 5。
    int *q = new int[5];    // 申请一个有五个元素的 int 数组。注意,此时没有简单的方法可以一并指明数组的初值。
    int a[5] = {1, 2, 3, 4, 5};     // 对比,非动态申请的数组则有一种语法机制可以简单地赋初值。

    cout << *p << endl;     // 输出:5
    cout << q[0] << endl;   // 输出:0。内置元素的默认初值都是 0, 0.0, false 之类的。
    cout << a[1] << enld;   // 输出:1

    return 0;
} 
    new也可以用来申请组合类型。比如指针或者函数指针等等,但语法非常复杂,而且有时会出现一些比较有意思的意外。使用typedef 是个很好的习惯。下面也是用例子来讲解:
程序代码:
#include <iostream>
#include <new>
using namespace std;

int a = 5;                // 先定义一个整数。
int f() { return 0; }     // 再定义一个参数列表为空并返回 int 的函数。函数的原型貌似不能比这个更简单了。

int main()
{
    int *p = &a;          // 自己定义一个指向 a 的指针。
    cout << *p << endl;   // 输出:5。通过指针输出 a 的值,没问题。

 

    // 动态申请一个 int *,并试着用上个例子讲的语法点赋初值。
    // 但要注意,new返回的是一个指向 int * 的指针,所以是二级指针。
    int **q = new int * (&a);    // 写成 new (int *) (&a) 似乎可读性更好一点。但好吧,这样写也没问题。
    cout << **q << endl;         // 输出:5。通过这个申请来的指针输出 a 的值。


    // 自己定义一个函数指针,并指向 f。如果这个语法也不是很明白的话,就需要先去看看书了。
    int (*fp)() = &f;            // 函数指针的语法点对初学者来说的确比较难,不熟悉也很正常。
    cout << fp() << endl;        // 输出:0。试着通过函数指针调用函数,一切正常。 


    int (**fq)();                // 声明一个二级函数指针。 
    // fq = new int (*)() (&f);  // error 。想照搬 int* 的那套写法就会出语法错误。因为 new运算符 的优先级非常高。
                                 // 这个语句会被解释成:(new int) (*)() (&f),但这是说不通的。
                                 // 上面那个 int * 的例子,会被解释成 (new int *) (&a)。正好和咱们要求的语义一样。
    fq = new (int (*)()) (&f);   // 正确的写法要多加一对括号。

    cout << (*fq)() << endl;     // 输出:0。可以通过申请来的函数指针调用函数。

    return 0;
} 
    大家不难发现,这些写法极为晦涩。比如这个语句想做什么:fp= new ( int * (*[5])(double) );
答案是:该语句在申请一个五元素数组,数组里的每个元素都是一个函数指针,这些指针所指函数的参数列表应为一个 double,且返回一个 int 指针。给大家留个思考题:fp 应该声明成何种类型?   
    组合可以产生无限的复杂,即使琢磨一会能看明白这些语句,也不推荐这么写。使用 typedef 对可读性的贡献是巨大的。对比下面这个例子,是使用typedef 简化之后的版本:
程序代码:
#include <iostream>
#include <new>
using namespace std;

int f() { return 0; }

int main()
{
    typedef int (*fptr_t)();

    fptr_t *fp = new fptr_t(&f);
    cout << (*fp)() << endl;

    return 0;
} 

    可以用 new申请多维数组。但只允许第一维是整型表达式,而后续的都要求是整型常量。这个要求主要是为了能在编译时确定类型。newint 和 new int[5] 的返回类型都是int * 这没问题,但 newint[2][3]申请的是二维数组,可以理解成它申请的是一个二元素的数组,而这个数组中的每个元素又都是一个三元素的数组(这正是多维数组的意思),所以它的返回类型是int(*)[3]。可以看到除了第一维以外,其它几维都会被用来确实返回类型。而这个类型必须要在编译时就能确实,所以只能使用整型常量。
程序代码:
#include <iostream>
#include <new>
using namespace std; 

int main()
{
    int n = 2, m = 3;
    const int l = 3; 

    void * p = new int[2][3]; // OK
    void * q = new int[n][m]; // error
    void * r = new int[n][l]; // OK 

    return 0;
} 
   
    new 表达式和delete 表达式会引发函数调用。标准称它调用的函数为分配函数(allocation function),即指全局函数 operatornew 和 operatornew[];和解分配函数(deallocation functions),指对应的delete函数。它们会分别在申请和释放单一对象或数组对象时被调用。其函数原型是:
程序代码:
void* operator new   (std::size_t) throw(std::bad_alloc);
void* operator new[] (std::size_t) throw(std::bad_alloc);

void  operator delete   (void*) throw();
void  operator delete[] (void*) throw();
    这是标准库中唯一一组不在名空间std 里的函数。   
    这组函数的第一个参数不允许使用默认参数。   
    它们可以被重载成模板函数,但这时必须它的调用参数必须多于一个,这些后加的参数可以使用模板定义的类型,但第一个参数和返回参数的类型不能改变。
   
    可以通过重载这些函数,更改new 表达式和 delete表达式的行为。另外如果用来申请的对象是个类,那么编译器会先在这个类内寻找分配和解分配函数,如果没有才会调用全局的版本。想强制调用全局版本,应该在表达式前加:: 限定。
程序代码:
#include <iostream>
#include <new>
using namespace std;

// 由于有异常处理机制,因此 new 即使分配失败一般也不主张返回 NULL。我这里只为了演示。
// 这不是语法错误,但也许你的编译会给一个警告。
void *operator new(size_t n) throw()
{ cout << "global new()" << endl; return NULL; }

void operator delete(void *p) throw()
{ cout << "global delete()" << endl; }

class A {
public:
    void *operator new(size_t) throw()
    { cout << "A::new()" << endl; return NULL; } 

    void operator delete(void *) throw()
    { cout << "A::delete()" << endl; }
};

int main()
{
    int *p = new int; // 全局 new()。
    delete p; 

    A* q = new A;     // 类A的new()。
    delete q; 

    q = ::new A;      // 强制使用全局 new()。
    ::delete q; 

    return 0;
} 
    new A 这样的表达式,会调用new(sizeof(A))。如果使用 newA[5],标准规定传去 new 的参数至少是sizeof(A)*5,更大的值也是有可能的,比如用来记录一些有关的数据结构。至于new() 的底层实现是否使用 C库函数 malloc 和realloc 则没有规定。不过标准对new 还有一系列的描述,使得它的行为和malloc 之类的函数相兼容。它与C 式的分配函数的主要区别是,当申请空间的长度为零时,即使用new int[0] 这样的表达式,也不会返回NULL。而 C 标准则没有强制规定这一点。不论C 还是 C++这种情况申请来的指针所指的空间都不应该使用,不过可以用来释放。gcc并没有用 malloc 来实现new。

    默认情况下 new 在失败时,会抛出标准分配异常:bad_alloc。它是标准异常 exception 的派生类。可以使用 nothrow 来抑制异常的抛出,此时在分配失败后会返回 NULL 指针。bad_alloc (一个类)和 nothrow (一个nothrow_t 的常量对象)都定义在 <new> 里。
程序代码:
#include <iostream>
#include <new>
using namespace std;

struct A { int a[1024]; };     // 定义一个比较大的对象。一个 A 的对象要占用 4k 的空间。

int main()
{
    cout << sizeof(A) << endl; // 输出:4096

    try {
        for (;;)               // 无限申请,并抛弃返回地址。
            new A;             // 不要紧,申请的空间在程序结束运行之后会由操作系统自动回收。
    }
    catch (bad_alloc &e) {      // 最终内存耗尽,申请肯定会失败。这时会抛出 bad_alloc。
        cout << e.what() << endl;
    }
    catch(...) {}

    // 用 nothrow 来抑制异常的抛出。因为内存已经耗尽,调用一次就可以了。分配失败会返回 NULL。
    void *p = new(nothrow) A;
    if (p == NULL) cout << "allocation failed" << endl;

    return 0;
}
   
    有时不需要自己重载 operator new。而是写一个函数,通常称为 new handler,来影响 new 的行为。一般来说这个函数可以尝试释放一些自己已经没用的空间,或者干脆就是等待一段时间。如果可以成功释放一些空间,或者等上一会,就可能会有足够的内存来完成这次申请。自己写完这个函数要用 set_new_handler 注册。第一次调用 set_new_handler 时会返回 NULL 指针,而随后的调用则会返回上一次注册的函数。标准库里很多 set 类的函数都有这样的性质。
    new 的默认执行逻辑如下:首先尝试申请指定的空间。如果申请成功则将申请空间的地址返回给调用者,如果申请失败则检查是否有 new handler 被注册。如果没有注册,则抛出 bad_alloc(或者根据要求返回 NULL 指针)。如果有注册,则调用一次 new handler,并在 new handler 返回后继续尝试申请空间。这种尝试会不断持续下去,直到申请成功或者某次调用 handler 失败。
程序代码:
#include <iostream>
#include <new>
using namespace std;

struct A { int a[1024]; };

void f()
{
    static int i = 5;

    if (i-- > 0)
        cout << "calling new handler" << endl;
    else
        throw bad_alloc();
}

int main()
{
    set_new_handler(&f);

    try {
        for (;;) new A;
    }
    catch (bad_alloc &e) {
        cout << e.what() << endl;
    }

    return 0;
}

    一直没有提 delete 的事,因为 delete 的逻辑要简单得多,合法的调用不会失败,也不会抛出异常。delete 用于释放 new 申请的空间,delete[] 用于释放 new[] 申请的空间。delete 也可以作用于空指针,但其实没有什么任何效果。用其它指针来调用 delete 是非法的,另外已经 delete 过的指针也不能再次用于 delete。可见 delete 也有一些限制,但这些限制貌似都显而易见。

    另外,还有一种称作 替换形式(placement forms) 的 new 表达式。这种 new 表达式并不申请空间,而是在一个已知的地址上执行 new 的其余动作。这种功能使得 new 可以和 malloc 等等这些 C 式分配函数相互配合。一般来说我并不推荐使用这种形式,但这种形式的 new 表达式在 STL 里非常多见。一般使用这种方式的 new 是出于效率上的考虑,大家也看到了 new 默认的执行逻辑非常复杂,而且调用 new 还要伴随大量的异常处理。如果我手里已经有一块内存可用,那么我就完全没必要把它释放掉之后再申请。或者很多时候,你有其它方法可以更灵活轻巧地获得空间,那么也可以选择用其它方法先获得空间,然后再用替换形式的 new 来初始化它。我只举个简单的例子,有兴趣的朋友可以自己查资料(也许你在自己的 STL 头文件里就能看到这种形式)。
程序代码:
#include <iostream>
#include <new>
using namespace std;

struct A { int a[1024]; };

int main()
{
    // 既然是讲解 C++,那么我还是使用标准方式来申请空间。
    void *p = operator new(sizeof(A));
   
    A *q = new(p) A;      // 在 p 指向的空间上执行 new A。
    /* ... */             // 使用这些空间。

    operator delete(p);   // 没用之后释放掉。

    return 0;
}

    另外还有几点我想讲一下的。虽然标准里并没有规定,但各种实现都要考虑效率。事实上由于系统调用的速度比普通函数要慢很多,因为从用户态切换到内核态要做很多有关特权级的审查,即使审查通过也还要保存用户状态,并恢复内核的一些数据结构。因此在实现时,C++ 的运行库可能会一次向系统索要超过用户申请的空间,以便之后数次调用可以直接提供。另外,所谓释放空间也不是将空间返还给操作系统,还是返还给 C++ 运行库来管理,以便之后的申请可以再度使用。事实上,也没有简单的方法可以将运行库持有的内存推还给操作系统。STL 由于经常涉及大量的数据结构操作,很多实现为了效率,又在 C++ 运行库之上加了一层内存管理器,以加速小片内存的查找与回收。使用 gcc 的朋友可以查看 bits/allocator.h,以获得实现相关的更多细节。
    在内存的分配与回收算法上,人们下了很多功夫来研究,也取得了很多成果。有兴趣的朋友请自行查阅资料。


[ 本帖最后由 pangding 于 2012-4-19 21:38 编辑 ]
2012-03-31 02:07
pangding
Rank: 19Rank: 19Rank: 19Rank: 19Rank: 19Rank: 19
来 自:北京
等 级:贵宾
威 望:94
帖 子:6784
专家分:16751
注 册:2008-12-20
收藏
得分:0 
更新点内容。

每次没觉得写多少,发上来一看还是挺多的。看上好像很费眼睛,希望大家的浏览器可以把字调大~~
2012-04-15 00:36
pangding
Rank: 19Rank: 19Rank: 19Rank: 19Rank: 19Rank: 19
来 自:北京
等 级:贵宾
威 望:94
帖 子:6784
专家分:16751
注 册:2008-12-20
收藏
得分:0 
再更新。现在标准库中有关语言支持的部分我基本上就介绍完了。

考虑到看的人里可能初学者很多,我后来举的例子有部分内容甚至是为了展示一些语法,并且给了更细致的讲解。
我希望的是,只要通学过一遍 C++ 的人,应该就能看懂。但是毕竟我的目的不是为了教大家语法,所以比如想看我最后写的有关 动态内存分配 的内容,起码 new 的最一般用法还是要会的。虽然最基本的语法我也介绍了,但大家不应该期望能用我介绍的东西来学习语法。

总的来说这些文章还是准备给有一定基础的人看的,不过我确信大家可以从中学到很多书本上学不到的东西,而且我还指明了不少可以搜集资料、进阶学习的方向。
但无论如何,文章确实写的又臭又长。没有精力和兴趣的人,或者是刚刚开始学 C++ 的人,无视也是一个很好的选择。
2012-04-15 18:33
快速回复:C++标准库的学习笔记 -- 语言支持库
数据加载中...
 
   



关于我们 | 广告合作 | 编程中国 | 清除Cookies | TOP | 手机版

编程中国 版权所有,并保留所有权利。
Powered by Discuz, Processed in 0.035798 second(s), 8 queries.
Copyright©2004-2024, BCCN.NET, All Rights Reserved