本站首页    管理页面    写新日志    退出

The Neurotic Fishbowl

[好文转载]vc 专题
莹莹 发表于 2006/7/25 11:51:30

 500)this.width=500'>真是好文章,可惜不知道原创高人姓氏,姑且拿来为大家共赏:500)this.width=500'>  500)this.width=500'> 编程心得 尽量用<iostream>而不用<stdio.h> 是的,scanf和printf很轻巧,很高效,你也早就知道怎么用它们,这我承认。但尽管他们很有用,事实上scanf和printf及其系列还可以做些改进。尤其是,他们不是类型安全的,而且没有扩展性。因为类型安全和扩展性是C++的基石,所以你也要服从这一点。另外,scanf/printf系列函数把要读写的变量和控制读写格式的信息分开来,就象古老的FORTRAN那样。是该向五十年代说诀别的时候了! 不必惊奇,scanf/printf的这些弱点正是操作符>>和<<的强项: int i;Rational r;// r 是个有理数 ... cin >> i >> r;cout << i << r; 上面的代码要通过编译,>>和<<必须是可以处理Rational类型对象的重载函数(可能要通过隐式类型转换)。如果没有实现这样的函数,就会出错(处理int不用这样做,因为它是标准用法)。另外,编译器自己可以根据不同的变量类型选择操作符的不同形式,所以不必劳你去指定第一个要读写的对象是int而第二个是Rational。 另外,在传递读和写的对象时采用的语法形式相同,所以不必象scanf那样死记一些规定,比如如果没有得到指针,必须加上地址符,而如果已经得到了指针,又要确定不要加上地址符。这些完全可以交给C++编译器去做。编译器没别的什么事好做的,而你却不一样。最后要注意的是,象int这样的固定类型和象Rational这样的自定义类型在读写时方式是一样的。而你用sacnf和printf试试看! 你所写的表示有理数的类的代码可能象下面这样: class Rational {public: Rational(int numerator = 0, int denominator = 1);  ... private: int n, d;// 分子,分母 friend ostream& operator<<(ostream& s, const Rational& );}; ostream& operator<<(ostream& s, const Rational& r){ s<< r.n << '/' << r.d; return s;} 上面的代码涉及到operator<<的一些微妙(但很重要)的用法,这在本书其他地方详细讨论。例如:上面的operator<<不是成员函数(条款19解释了为什么),而且,传递给operator<<的不是Rational对象,而是定义为const的对象的引用(参见条款22)。operator>>的声明和实现也类似。 尽管我不大愿意承认,可有些情况下回到那些经过证明而且正确的老路上去还是很有意义的。第一,有些iostream的操作实现起来比相应的C stream效率要低,所以不同的选择会给你的程序有可能(虽然不一定,参见条款M16)带来很大的不同。但请牢记,这不是对所有的iostream而言,只是一些特殊的实现;参见条款M23。第二,在标准化的过程中,iostream库在底层做了很多修改(参见条款49),所以对那些要求最大可移植性的应用程序来说,会发现不同的厂商遵循标准的程度也不同。第三,iostream库的类有构造函数而<stdio.h>里的函数没有,在某些涉及到静态对象初始化顺序的时候,如果可以确认不会带来隐患,用标准C库会更简单实用。 iostream库的类和函数所提供的类型安全和可扩展性的价值远远超过你当初的想象,所以不要仅仅因为你用惯了<stdio.h>而舍弃它。毕竟,转换到iostream后,你也不会忘掉<stdio.h>。 顺便说一句,本条款的标题没有打印错;我确实说的是<iostream>而非<iostream.h>。从技术上说,其实没有<iostream.h>这样的东西——标准化委员会在简化非C标准头文件时用<iostream>取代了它。他们这样做的原因在条款49进行了解释。还必须知道的是,如果编译器同时支持 <iostream>和<iostream.h>,那头文件名的使用会很微妙。例如,如果使用了#include <iostream>, 得到的是置于名字空间std(见条款28)下的iostream库的元素;如果使用#include <iostream.h>,得到的是置于全局空间的同样的元素。在全局空间获取元素会导致名字冲突,而设计名字空间的初衷正是用来避免这种名字冲突的发生。还有,打字时<iostream>比<iostream.h>少两个字,这也是很多人用它的原因。:)500)this.width=500'>  VC心得 尽量用const和inline而不用#define 这个条款最好称为:“尽量用编译器而不用预处理”,因为#define经常被认为好象不是语言本身的一部分。这是问题之一。再看下面的语句: #define ASPECT_RATIO 1.653 编译器会永远也看不到ASPECT_RATIO这个符号名,因为在源码进入编译器之前,它会被预处理程序去掉,于是ASPECT_RATIO不会加入到符号列表中。如果涉及到这个常量的代码在编译时报错,就会很令人费解,因为报错信息指的是1.653,而不是ASPECT_RATIO。如果ASPECT_RATIO不是在你自己写的头文件中定义的,你就会奇怪1.653是从哪里来的,甚至会花时间跟踪下去。这个问题也会出现在符号调试器中,因为同样地,你所写的符号名不会出现在符号列表中。解决这个问题的方案很简单:不用预处理宏,定义一个常量: const double ASPECT_RATIO = 1.653; 这种方法很有效。但有两个特殊情况要注意。首先,定义指针常量时会有点不同。因为常量定义一般是放在头文件中(许多源文件会包含它),除了指针所指的类型要定义成const外,重要的是指针也经常要定义成const。例如,要在头文件中定义一个基于char*的字符串常量,你要写两次const: const char * const authorName = "Scott Meyers"; 关于const的含义和用法,特别是和指针相关联的问题,参见条款21。 另外,定义某个类(class)的常量一般也很方便,只有一点点不同。要把常量限制在类中,首先要使它成为类的成员;为了保证常量最多只有一份拷贝,还要把它定义为静态成员: class GamePlayer {private: static const int NUM_TURNS = 5; // constant eclaration  int scores[NUM_TURNS];  // use of constant ...}; 还有一点,正如你看到的,上面的语句是NUM_TURNS的声明,而不是定义,所以你还必须在类的实现代码文件中定义类的静态成员: const int GamePlayer::NUM_TURNS; // mandatory definition;  // goes in class impl.file 你不必过于担心这种小事。如果你忘了定义,链接器会提醒你。 旧一点的编译器会不接受这种语法,因为它认为类的静态成员在声明时定义初始值是非法的;而且,类内只允许初始化整数类型(如:int, bool, char 等),还只能是常量。在上面的语法不能使用的情况下,可以在定义时赋初值: class EngineeringConstants { // this goes in the classprivate:  // header file static const double FUDGE_FACTOR; ...}; // this goes in the class implementation file const double EngineeringConstants::FUDGE_FACTOR = 1.35; 大多数情况下你只要做这么多。唯一例外的是当你的类在编译时需要用到这个类的常量的情况,例如上面GamePlayer::scores数组的声明(编译过程中编译器一定要知道数组的大小)。所以,为了弥补那些(不正确地)禁止类内进行整型类常量初始化的编译器的不足,可以采用称之为“借用enum”的方法来解决。这种技术很好地利用了当需要int类型时可以使用枚举类型的原则,所以GamePlayer也可以象这样来定义: class GamePlayer {private: enum { NUM_TURNS = 5 } // "the enum hack" — makes // NUM_TURNS a symbolic name  // for 5 int scores[NUM_TURNS];// fine}; 除非你正在用老的编译器(即写于1995年之前),你不必借用enum。当然,知道有这种方法还是值得的,因为这种可以追溯到很久以前的时代的代码可是不常见的哟。 回到预处理的话题上来。另一个普遍的#define指令的用法是用它来实现那些看起来象函数而又不会导致函数调用的宏。典型的例子是计算两个对象的最大值: #define max(a,b) ((a) > (b) ? (a) : (b)) 这个语句有很多缺陷,光想想都让人头疼,甚至比在高峰时间到高速公路去开车还让人痛苦。无论什么时候你写了象这样的宏,你必须记住在写宏体时对每个参数都要加上括号;否则,别人调用你的宏时如果用了表达式就会造成很大的麻烦。但是即使你象这样做了,还会有象下面这样奇怪的事发生: int a = 5, b = 0;max(++a, b);// a 的值增加了2次max(++a, b+10); // a 的值只增加了1次 这种情况下,max内部发生些什么取决于它比较的是什么值!幸运的是你不必再忍受这样愚笨的语句了。你可以用普通函数实现宏的效率,再加上可预计的行为和类型安全,这就是内联函数(见条款33): inline int max(int a, int b) { return a > b ? a : b; }不过这和上面的宏不大一样,因为这个版本的max只能处理int类型。但模板可以很轻巧地解决这个问题: template<class T>inline const T& max(const T& a, const T& b){ return a > b ? a : b; } 这个模板产生了一整套函数,每个函数拿两个可以转换成同种类型的对象进行比较然后返回较大的(常量)对象的引用。因为不知道T的类型,返回时传递引用可以提高效率。 顺便说一句,在你打算用模板写象max这样有用的通用函数时,先检查一下标准库,看看他们是不是已经存在。比如说上面说的max,你会惊喜地发现你可以后人乘凉:max是C++标准库的一部分。有了const和inline,你对预处理的需要减少了,但也不能完全没有它。抛弃#include的日子还很远,#ifdef/#ifndef在控制编译的过程中还扮演重要角色。预处理还不能退休,但你一定要计划给它经常放长假。   500)this.width=500'> 写VC好习惯1 尽量用new和delete而不用malloc和free malloc和free(及其变体)会产生问题的原因在于它们太简单:他们不知道构造函数和析构函数。 假设用两种方法给一个包含10个string对象的数组分配空间,一个用malloc,另一个用new:     string *stringarray1 =static_cast<string*>(malloc(10 * sizeof(string))); string *stringarray2 = new string[10]; 其结果是,stringarray1确实指向的是可以容纳10个string对象的足够空间,但内存里并没有创建这些对象。而且,如果你不从这种晦涩的语法怪圈(详见条款m4和m8的描述)里跳出来的话,你没有办法来初始化数组里的对象。换句话说,stringarray1其实一点用也没有。相反,stringarray2指向的是一个包含10个完全构造好的string对象的数组,每个对象可以在任何读取string的操作里安全使用。 假设你想了个怪招对stringarray1数组里的对象进行了初始化,那么在你后面的程序里你一定会这么做: free(stringarray1);delete [] stringarray2;// 参见条款5:这里为什么要加上个"[]" 调用free将会释放stringarray1指向的内存,但内存里的string对象不会调用析构函数。如果string对象象一般情况那样,自己已经分配了内存,那这些内存将会全部丢失。相反,当对stringarray2调用delete时,数组里的每个对象都会在内存释放前调用析构函数。 既然new和delete可以这么有效地与构造函数和析构函数交互,选用它们是显然的。 把new和delete与malloc和free混在一起用也是个坏想法。对一个用new获取来的指针调用free,或者对一个用malloc获取来的指针调用delete,其后果是不可预测的。大家都知道“不可预测”的意思:它可能在开发阶段工作良好,在测试阶段工作良好,但也可能会最后在你最重要的客户的脸上爆炸。 new/delete和malloc/free的不兼容性常常会导致一些严重的复杂性问题。举个例子,<string.h>里通常有个strdup函数,它得到一个char*字符串然后返回其拷贝: char * strdup(const char *ps); // 返回ps所指的拷贝在有些地方,c和c++用的是同一个strdup版本,所以函数内部是用malloc分配内存。这样的话,一些不知情的c++程序员会在调用strdup后忽视了必须对strdup返回的指针进行free操作。为了防止这一情况,有些地方会专门为c++重写strdup,并在函数内部调用了new,这就要求其调用者记得最后用delete。你可以想象,这会导致多么严重的移植性问题,因为代码中strdup以不同的形式在不同的地方之间颠来倒去。 c++程序员和c程序员一样对代码重用十分感兴趣。大家都知道,有大量基于malloc和free写成的代码构成的c库都非常值得重用。在利用这些库时,最好是你不用负责去free掉由库自己malloc的内存,并且/或者,你不用去malloc库自己会free掉的内存,这样就太好了。其实,在c++程序里使用malloc和free没有错,只要保证用malloc得到的指针用free,或者用new得到的指针最后用delete来操作就可以了。千万别马虎地把new和free或malloc和delete混起来用,那只会自找麻烦。 既然malloc和free对构造函数和析构函数一无所知,把malloc/free和new/delete混起来用又象嘈杂拥挤的晚会那样难以控制,那么,你最好就什么时候都一心一意地使用new和delete吧 500)this.width=500'> 写VC好习惯--内存管理 c++中涉及到的内存的管理问题可以归结为两方面:正确地得到它和有效地使用它。好的程序员会理解这两个问题为什么要以这样的顺序列出。因为执行得再快、体积再小的程序如果它不按你所想象地那样去执行,那也一点用处都没有。“正确地得到”的意思是正确地调用内存分配和释放程序;而“有效地使用”是指写特定版本的内存分配和释放程序。这里,“正确地得到”显得更重要一些。 然而说到正确性,c++其实从c继承了一个很严重的头疼病,那就是内存泄露隐患。虚拟内存是个很好的发明,但虚拟内存也是有限的,并不是每个人都可以最先抢到它。 在c中,只要用malloc分配的内存没有用free返回,就会产生内存泄露。在c++中,肇事者的名字换成了new和delete,但情况基本上是一样的。当然,因为有了析构函数的出现,情况稍有改善,因为析构函数为所有将被摧毁的对象提供了一个方便的调用delete的场所。但这同时又带来了更多的烦恼,因为new和delete是隐式地调用构造函数和析构函数的。而且,因为可以在类内和类外自定义new和delete操作符,这又带来了复杂性,增加了出错的机会。下面的条款(还有条款m8)将告诉你如何避免产生那些普遍发生的问题。 500)this.width=500'>  对应的new和delete要采用相同的形式 下面的语句有什么错? string *stringarray = new string[100]; ... delete stringarray; 一切好象都井然有序——一个new对应着一个delete——然而却隐藏着很大的错误:程序的运行情况将是不可预测的。至少,stringarray指向的100个string对象中的99个不会被正确地摧毁,因为他们的析构函数永远不会被调用。 用new的时候会发生两件事。首先,内存被分配(通过operator new 函数,详见条款7-10和条款m8),然后,为被分配的内存调用一个或多个构造函数。用delete的时候,也有两件事发生:首先,为将被释放的内存调用一个或多个析构函数,然后,释放内存(通过operator delete 函数)。对于 delete来说会有这样一个重要的问题:内存中有多少个对象要被删除?答案决定了将有多少个析构函数会被调用。 这个问题简单来说就是:要被删除的指针指向的是单个对象呢,还是对象数组?这只有你来告诉delete。如果你在用delete时没用括号,delete就会认为指向的是单个对象,否则,它就会认为指向的是一个数组: string *stringptr1 = new string;string *stringptr2 = new string[100]; ... delete stringptr1;// 删除一个对象delete [] stringptr2;// 删除对象数组 如果你在stringptr1前加了"[]"会怎样呢?答案是:那将是不可预测的;如果你没在stringptr2前没加上"[]"又会怎样呢?答案也是:不可预测。而且对于象int这样的固定类型来说,结果也是不可预测的,即使这样的类型没有析构函数。所以,解决这类问题的规则很简单:如果你调用new时用了[],调用delete时也要用[]。如果调用new时没有用[],那调用delete时也不要用[]。 在写一个包含指针数据成员,并且提供多个构造函数的类时,牢记这一规则尤其重要。因为这样的话,你就必须在所有初始化指针成员的构造函数里采用相同的new的形式。否则,析构函数里将采用什么形式的delete呢?关于这一话题的进一步阐述。 这个规则对喜欢用typedef的人来说也很重要,因为写typedef的程序员必须告诉别人,用new创建了一个typedef定义的类型的对象后,该用什么形式的delete来删除。举例如下: typedef string addresslines[4]; //一个人的地址,共4行,每行一个string    //因为addresslines是个数组,使用new:string *pal = new addresslines; // 注意"new addresslines"返回string*, 和    // "new string[4]"返回的一样delete时必须以数组形式与之对应:delete pal;// 错误!delete [] pal;// 正确 为了避免混乱,最好杜绝对数组类型用typedefs。这其实很容易,因为标准c++库包含有stirng和vector模板,使用他们将会使对数组的需求减少到几乎零。举例来说,addresslines可以定义为一个字符串(string)的向量(vector),即addresslines可定义为vector<string>类型。 500)this.width=500'>  析构函数里对指针成员调用delete 大多数情况下,执行动态内存分配的的类都在构造函数里用new分配内存,然后在析构函数里用delete释放内存。最初写这个类的时候当然不难做,你会记得最后对在所有构造函数里分配了内存的所有成员使用delete。 然而,这个类经过维护、升级后,情况就会变得困难了,因为对类的代码进行修改的程序员不一定就是最早写这个类的人。而增加一个指针成员意味着几乎都要进行下面的工作:    ·在每个构造函数里对指针进行初始化。对于一些构造函数,如果没有内存要分配给指针的话,指针要被初始化为0(即空指针)。    ·删除现有的内存,通过赋值操作符分配给指针新的内存。    ·在析构函数里删除指针。 如果在构造函数里忘了初始化某个指针,或者在赋值操作的过程中忘了处理它,问题会出现得很快,很明显,所以在实践中这两个问题不会那么折磨你。但是,如果在析构函数里没有删除指针,它不会表现出很明显的外部症状。相反,它可能只是表现为一点微小的内存泄露,并且不断增长,最后吞噬了你的地址空间,导致程序夭折。因为这种情况经常不那么引人注意,所以每增加一个指针成员到类里时一定要记清楚。 另外,删除空指针是安全的(因为它什么也没做)。所以,在写构造函数,赋值操作符,或其他成员函数时,类的每个指针成员要么指向有效的内存,要么就指向空,那在你的析构函数里你就可以只用简单地delete掉他们,而不用担心他们是不是被new过。 当然对本条款的使用也不要绝对。例如,你当然不会用delete去删除一个没有用new来初始化的指针,而且,就象用智能指针对象时不用劳你去删除一样,你也永远不会去删除一个传递给你的指针。换句话说,除非类成员最初用了new,否则是不用在析构函数里用delete的。 说到智能指针,这里介绍一种避免必须删除指针成员的方法,即把这些成员用智能指针对象来代替,比如c++标准库里的auto_ptr。 500)this.width=500'>  预先准备好内存不够的情况 operator new在无法完成内存分配请求时会抛出异常(以前的做法一般是返回0,一些旧一点的编译器还这么做。你愿意的话也可以把你的编译器设置成这样。关于这个话题我将推迟到本条款的结尾处讨论)。大家都知道,处理内存不够所产生的异常真可以算得上是个道德上的行为,但实际做起来又会象刀架在脖子上那样痛苦。所以,你有时会不去管它,也许一直没去管它。但你心里一定还是深深地隐藏着一种罪恶感:万一new真的产生了异常怎么办? 你会很自然地想到处理这种情况的一种方法,即回到以前的老路上去,使用预处理。例如,c的一种常用的做法是,定义一个类型无关的宏来分配内存并检查分配是否成功。对于c++来说,这个宏看起来可能象这样: #define new(ptr, type)  try { (ptr) = new type; } catch (std::bad_alloc&) { assert(0); } (“慢!std::bad_alloc是做什么的?”你会问。bad_alloc是operator new不能满足内存分配请求时抛出的异常类型,std是bad_alloc所在的名字空间的名称。“好!”你会继续问,“assert又有什么用?”如果你看看标准c头文件<assert.h>(或与它相等价的用到了名字空间的版本<cassert>,就会发现assert是个宏。这个宏检查传给它的表达式是否非零,如果不是非零值,就会发出一条出错信息并调用abort。assert只是在没定义标准宏ndebug的时候,即在调试状态下才这么做。在产品发布状态下,即定义了ndebug的时候,assert什么也不做,相当于一条空语句。所以你只能在调试时才能检查断言(assertion))。 new宏不但有着上面所说的通病,即用assert去检查可能发生在已发布程序里的状态(然而任何时候都可能发生内存不够的情况),同时,它还在c++里有另外一个缺陷:它没有考虑到new有各种各样的使用方式。例如,想创建类型t对象,一般有三种常见的语法形式,你必须对每种形式可能产生的异常都要进行处理: new t;new t(constructor arguments);new t[size]; 这里对问题大大进行了简化,因为有人还会自定义(重载)operator new,所以程序里会包含任意个使用new的语法形式。 那么,怎么办?如果想用一个很简单的出错处理方法,可以这么做:当内存分配请求不能满足时,调用你预先指定的一个出错处理函数。这个方法基于一个常规,即当operator new不能满足请求时,会在抛出异常之前调用客户指定的一个出错处理函数——一般称为new-handler函数。(operator new实际工作起来要复杂一些) 指定出错处理函数时要用到set_new_handler函数,它在头文件<new>里大致是象下面这样定义的: typedef void (*new_handler)();new_handler set_new_handler(new_handler p) throw(); 可以看到,new_handler是一个自定义的函数指针类型,它指向一个没有输入参数也没有返回值的函数。set_new_handler则是一个输入并返回new_handler类型的函数。 set_new_handler的输入参数是operator new分配内存失败时要调用的出错处理函数的指针,返回值是set_new_handler没调用之前就已经在起作用的旧的出错处理函数的指针。 可以象下面这样使用set_new_handler: // function to call if operator new can't allocate enough memoryvoid nomorememory(){ cerr << "unable to satisfy request for memory\n"; abort();} int main(){ set_new_handler(nomorememory); int *pbigdataarray = new int[100000000];  ... } 假如operator new不能为100,000,000个整数分配空间,nomorememory将会被调用,程序发出一条出错信息后终止。这就比简单地让系统内核产生错误信息来结束程序要好。(顺便考虑一下,假如cerr在写错误信息的过程中要动态分配内存,那将会发生什么...) operator new不能满足内存分配请求时,new-handler函数不只调用一次,而是不断重复,直至找到足够的内存。实现重复调用的代码,这里我用描述性的的语言来说明:一个设计得好的new-handler函数必须实现下面功能中的一种。    ·产生更多的可用内存。这将使operator new下一次分配内存的尝试有可能获得成功。实施这一策略的一个方法是:在程序启动时分配一个大的内存块,然后在第一次调用new-handler时释放。释放时伴随着一些对用户的警告信息,如内存数量太少,下次请求可能会失败,除非又有更多的可用空间。    ·安装另一个不同的new-handler函数。如果当前的new-handler函数不能产生更多的可用内存,可能它会知道另一个new-handler函数可以提供更多的资源。这样的话,当前的new-handler可以安装另一个new-handler来取代它(通过调用set_new_handler)。下一次operator new调用new-handler时,会使用最近安装的那个。(这一策略的另一个变通办法是让new-handler可以改变它自己的运行行为,那么下次调用时,它将做不同的事。方法是使new-handler可以修改那些影响它自身行为的静态或全局数据。)    ·卸除new-handler。也就是传递空指针给set_new_handler。没有安装new-handler,operator new分配内存不成功时就会抛出一个标准的std::bad_alloc类型的异常。    ·抛出std::bad_alloc或从std::bad_alloc继承的其他类型的异常。这样的异常不会被operator new捕捉,所以它们会被送到最初进行内存请求的地方。(抛出别的不同类型的异常会违反operator new异常规范。规范中的缺省行为是调用abort,所以new-handler要抛出一个异常时,一定要确信它是从std::bad_alloc继承来的。想更多地了解异常规范。)    ·没有返回。典型做法是调用abort或exit。abort/exit可以在标准c库中找到(还有标准c++库)。 上面的选择给了你实现new-handler函数极大的灵活性。 处理内存分配失败的情况时采取什么方法,取决于要分配的对象的类: class x {public: static void  outofmemory();  ... }; class y {public: static void outofmemory();  ... }; x* p1 = new x; // 若分配成功,调用x::outofmemoryy* p2 = new y; // 若分配不成功,调用y::outofmemory c++不支持专门针对于类的new-handler函数,而且也不需要。你可以自己来实现它,只要在每个类中提供自己版本的set_new_handler和operator new。类的set_new_handler可以为类指定new-handler(就象标准的set_new_handler指定全局new-handler一样)。类的operator new则保证为类的对象分配内存时用类的new-handler取代全局new-handler。 假设处理类x内存分配失败的情况。因为operator new对类型x的对象分配内存失败时,每次都必须调用出错处理函数,所以要在类里声明一个new_handler类型的静态成员。那么类x看起来会象这样: class x {public: static new_handler set_new_handler(new_handler p); static void * operator new(size_t size); private: static new_handler currenthandler;}; 类的静态成员必须在类外定义。因为想借用静态对象的缺省初始化值0,所以定义x::currenthandler时没有去初始化。 new_handler x::currenthandler; //缺省设置currenthandler为0(即null)类x中的set_new_handler函数会保存传给它的任何指针,并返回在调用它之前所保存的任何指针。这正是标准版本的set_new_handler所做的: new_handler x::set_new_handler(new_handler p){ new_handler oldhandler = currenthandler; currenthandler = p; return oldhandler;} 最后看看x的operator new所做的:1. 调用标准set_new_handler函数,输入参数为x的出错处理函数。这使得x的new-handler函数成为全局new-handler函数。注意下面的代码中,用了"::"符号显式地引用std空间(标准set_new_handler函数就存在于std空间)。 2. 调用全局operator new分配内存。如果第一次分配失败,全局operator new会调用x的new-handler,因为它刚刚(见1.)被安装成为全局new-handler。如果全局operator new最终未能分配到内存,它抛出std::bad_alloc异常,x的operator new会捕捉到它。x的operator new然后恢复最初被取代的全局new-handler函数,最后以抛出异常返回。 3. 假设全局operator new为类型x的对象分配内存成功,, x的operator new会再次调用标准set_new_handler来恢复最初的全局出错处理函数。最后返回分配成功的内存的指针。 c++是这么做的: void * x::operator new(size_t size){ new_handler globalhandler =  // 安装x的new_handler      std::set_new_handler(currenthandler);  void *memory; try {  // 尝试分配内存 memory = ::operator new(size);} catch (std::bad_alloc&) {  // 恢复旧的new_handler std::set_new_handler(globalhandler);       throw; // 抛出异常} std::set_new_handler(globalhandler); // 恢复旧的new_handler return memory;} 如果你对上面重复调用std::set_new_handler看不顺眼,可以除去它们。 使用类x的内存分配处理功能时大致如下: void nomorememory();// x的对象分配内存失败时调用的new_handler函数的声明 x::set_new_handler(nomorememory);  // 把nomorememory设置为x的  // new-handling函数x *px1 = new x;  // 如内存分配失败,  // 调用nomorememorystring *ps = new string;  // 如内存分配失败,调用全局new-handling函数  x::set_new_handler(0);  // 设x的new-handling函数为空  x *px2 = new x;  // 如内存分配失败,立即抛出异常  // (类x没有new-handling函数) 你会注意到,处理以上类似情况,如果不考虑类的话,实现代码是一样的,这就很自然地想到在别的地方也能重用它们。继承和模板可以用来设计可重用代码。在这里,我们把两种方法结合起来使用,从而满足了你的要求。 你只要创建一个“混合风格”(mixin-style)的基类,这种基类允许子类继承它某一特定的功能——这里指的是建立一个类的new-handler的功能。之所以设计一个基类,是为了让所有的子类可以继承set_new_handler和operator new功能,而设计模板是为了使每个子类有不同的currenthandler数据成员。这听起来很复杂,不过你会看到代码其实很熟悉。区别只不过是它现在可以被任何类重用了。 template<class t> // 提供类set_new_handler支持的class newhandlersupport { // 混合风格”的基类public: static new_handler set_new_handler(new_handler p); static void * operator new(size_t size); private: static new_handler currenthandler;}; template<class t>new_handler newhandlersupport<t>::set_new_handler(new_handler p){ new_handler oldhandler = currenthandler; currenthandler = p; return oldhandler;} template<class t>void * newhandlersupport<t>::operator new(size_t size){ new_handler globalhandler =  std::set_new_handler(currenthandler); void *memory; try {  memory = ::operator new(size); } catch (std::bad_alloc&) {  std::set_new_handler(globalhandler);  throw; }  std::set_new_handler(globalhandler); return memory;}// this sets each currenthandler to 0 template<class t>new_handler newhandlersupport<t>::currenthandler;有了这个模板类,对类x加上set_new_handler功能就很简单了:只要让x从newhandlersupport<x>继承:// note inheritance from mixin base class template. (see// my article on counting objects for information on why// private inheritance might be preferable here.)class x: public newhandlersupport<x> { ...  // as before, but no declarations for};  // set_new_handler or operator new 使用x的时候依然不用理会它幕后在做些什么;老代码依然工作。这很好!那些你常不去理会的东西往往是最可信赖的。 使用set_new_handler是处理内存不够情况下一种方便,简单的方法。这比把每个new都包装在try模块里当然好多了。而且,newhandlersupport这样的模板使得向任何类增加一个特定的new-handler变得更简单。“混合风格”的继承不可避免地将话题引入到多继承上去。 1993年前,c++一直要求在内存分配失败时operator new要返回0,现在则是要求operator new抛出std::bad_alloc异常。很多c++程序是在编译器开始支持新规范前写的。c++标准委员会不想放弃那些已有的遵循返回0规范的代码,所以他们提供了另外形式的operator new(以及operator new[])以继续提供返回0功能。这些形式被称为“无抛出”,因为他们没用过一个throw,而是在使用new的入口点采用了nothrow对象: class widget { ... }; widget *pw1 = new widget;// 分配失败抛出std::bad_alloc if if (pw1 == 0) ... // 这个检查一定失败 widget *pw2 = new (nothrow) widget;  // 若分配失败返回0 if (pw2 == 0) ... // 这个检查可能会成功 不管是用“正规”(即抛出异常)形式的new还是“无抛出”形式的new,重要的是你必须为内存分配失败做好准备。最简单的方法是使用set_new_handler,因为它对两种形式都有用。 500)this.width=500'>  写operator new和operator delete时要遵循常规 自己重写operator new时(条款10解释了为什么有时要重写它),很重要的一点是函数提供的行为要和系统缺省的operator new一致。实际做起来也就是:要有正确的返回值;可用内存不够时要调用出错处理函数(见条款7);处理好0字节内存请求的情况。此外,还要避免不小心隐藏了标准形式的new,不过这是条款9的话题。 有关返回值的部分很简单。如果内存分配请求成功,就返回指向内存的指针;如果失败,则遵循条款7的规定抛出一个std::bad_alloc类型的异常。 但事情也不是那么简单。因为operator new实际上会不只一次地尝试着去分配内存,它要在每次失败后调用出错处理函数,还期望出错处理函数能想办法释放别处的内存。只有在指向出错处理函数的指针为空的情况下,operator new才抛出异常。 另外,c++标准要求,即使在请求分配0字节内存时,operator new也要返回一个合法指针。(实际上,这个听起来怪怪的要求确实给c++语言其它地方带来了简便) 这样,非类成员形式的operator new的伪代码看起来会象下面这样:void * operator new(size_t size)        // operator new还可能有其它参数{                                          if (size == 0) {                      // 处理0字节请求时,    size = 1;                           // 把它当作1个字节请求来处理  }                                       while (1) {    分配size字节内存;     if (分配成功)      return (指向内存的指针);     // 分配不成功,找出当前出错处理函数    new_handler globalhandler = set_new_handler(0);    set_new_handler(globalhandler);     if (globalhandler) (*globalhandler)();    else throw std::bad_alloc();  }} 处理零字节请求的技巧在于把它作为请求一个字节来处理。这看起来也很怪,但简单,合法,有效。而且,你又会多久遇到一次零字节请求的情况呢? 你又会奇怪上面的伪代码中为什么把出错处理函数置为0后又立即恢复。这是因为没有办法可以直接得到出错处理函数的指针,所以必须通过调用set_new_handler来找到。办法很笨但也有效。 条款7提到operator new内部包含一个无限循环,上面的代码清楚地说明了这一点——while (1)将导致无限循环。跳出循环的唯一办法是内存分配成功或出错处理函数完成了条款7所描述的事件中的一种:得到了更多的可用内存;安装了一个新的new-handler(出错处理函数);卸除了new-handler;抛出了一个std::bad_alloc或其派生类型的异常;或者返回失败。现在明白了为什么new-handler必须做这些工作中的一件。如果不做,operator new里面的循环就不会结束。 很多人没有认识到的一点是operator new经常会被子类继承。这会导致某些复杂性。上面的伪代码中,函数会去分配size字节的内存(除非size为0)。size很重要,因为它是传递给函数的参数。但是大多数针对类所写的operator new(包括条款10中的那种)都是只为特定的类设计的,不是为所有的类,也不是为它所有的子类设计的。这意味着,对于一个类x的operator new来说,函数内部的行为在涉及到对象的大小时,都是精确的sizeof(x):不会大也不会小。但由于存在继承,基类中的operator new可能会被调用去为一个子类对象分配内存:class base {public:  static void * operator new(size_t size);  ...}; class derived: public base       // derived类没有声明operator new{ ... };                         //  derived *p = new derived;        // 调用base::operator new 如果base类的operator new不想费功夫专门去处理这种情况——这种情况出现的可能性不大——那最简单的办法是把这个“错误”数量的内存分配请求转给标准operator new来处理,象下面这样:void * base::operator new(size_t size){  if (size != sizeof(base))             // 如果数量“错误”,让标准operator new    return ::operator new(size);        // 去处理这个请求                                        //    ...                                   // 否则处理这个请求} “停!”我听见你在叫,“你忘了检查一种虽然不合理但是有可能出现的一种情况——size有可能为零!”是的,我没检查,但拜托下次再叫出声的时候不要这么文绉绉的。:)但实际上检查还是做了,只不过融合到size != sizeof(base)语句中了。c++标准很怪异,其中之一就是规定所以独立的(freestanding)类的大小都是非零值。所以sizeof(base)永远不可能是零(即使base类没有成员),如果size为零,请求会转到::operator new,由它来以一种合理的方式对请求进行处理。(有趣的是,如果base不是独立的类,sizeof(base)有可能是零,详细说明参见"my article on counting objects")。 如果想控制基于类的数组的内存分配,必须实现operator new的数组形式——operator new[](这个函数常被称为“数组new”,因为想不出"operator new[]")该怎么发音)。写operator new[]时,要记住你面对的是“原始”内存,不能对数组里还不存在的对象进行任何操作。实际上,你甚至还不知道数组里有多少个对象,因为不知道每个对象有多大。基类的operator new[]会通过继承的方式被用来为子类对象的数组分配内存,而子类对象往往比基类要大。所以,不能想当然认为base::operator new[]里的每个对象的大小都是sizeof(base),也就是说,数组里对象的数量不一定就是(请求字节数)/sizeof(base)。关于operator new[]的详细介绍参见条款m8。 重写operator new(和operator new[])时所有要遵循的常规就这些。对于operator delete(以及它的伙伴operator delete[]),情况更简单。所要记住的只是,c++保证删除空指针永远是安全的,所以你要充分地应用这一保证。下面是非类成员形式的operator delete的伪代码:void operator delete(void *rawmemory){  if (rawmemory == 0) return;    file://如/果指针为空,返回                                 //    释放rawmemory指向的内存;   return;} 这个函数的类成员版本也简单,只是还必须检查被删除的对象的大小。假设类的operator new将“错误”大小的分配请求转给::operator new,那么也必须将“错误”大小的删除请求转给::operator delete: class base {                       // 和前面一样,只是这里声明了public:                            // operator delete  static void * operator new(size_t size);  static void operator delete(void *rawmemory, size_t size);  ...}; void base::operator delete(void *rawmemory, size_t size){  if (rawmemory == 0) return;      // 检查空指针   if (size != sizeof(base)) {      // 如果size"错误",    ::operator delete(rawmemory);  // 让标准operator来处理请求    return;                          }   释放指向rawmemory的内存;   return;} 可见,有关operator new和operator delete(以及他们的数组形式)的规定不是那么麻烦,重要的是必须遵守它。只要内存分配程序支持new-handler函数并正确地处理了零内存请求,就差不多了;如果内存释放程序又处理了空指针,那就没其他什么要做的了。至于在类成员版本的函数里增加继承支持,那将很快就可以完成。 500)this.width=500'>  避免隐藏标准形式的new 因为内部范围声明的名称会隐藏掉外部范围的相同的名称,所以对于分别在类的内部 和全局声明的两个相同名字的函数f来说,类的成员函数会隐藏掉全局函数: void f();                             // 全局函数 class x {public:  void f();                           // 成员函数}; x x; f();                                  // 调用 f x.f();                                // 调用 x::f 这不会令人惊讶,也不会导致混淆,因为调用全局函数和成员函数时总是采用不同的 语法形式。然而如果你在类里增加了一个带多个参数的operator new函数,结果就有 可能令人大吃一惊。 class x {public:  void f();   // operator new的参数指定一个  // new-hander(new的出错处理)函数  static void * operator new(size_t size, new_handler p);}; void specialerrorhandler();          // 定义在别的地方 x *px1 =  new (specialerrorhandler) x;       // 调用x::operator new x *px2 = new x;                      // 错误! 在类里定义了一个称为“operator new”的函数后,会不经意地阻止了对标准new的访 问。条款50解释了为什么会这样,这里我们更关心的是如何想个办法避免这个问题。 一个办法是在类里写一个支持标准new调用方式的operator new,它和标准new做同样 的事。这可以用一个高效的内联函数来封装实现。 class x {public:  void f();   static void * operator new(size_t size, new_handler p);   static void * operator new(size_t size)  { return ::operator new(size); }}; x *px1 =  new (specialerrorhandler) x;      // 调用 x::operator                                    // new(size_t, new_handler) x* px2 = new x;                     // 调用 x::operator                                    // new(size_t) 另一种方法是为每一个增加到operator new的参数提供缺省值(见条款24): class x {public:  void f();   static    void * operator new(size_t size,                // p缺省值为0                        new_handler p = 0);         // }; x *px1 = new (specialerrorhandler) x;               // 正确 x* px2 = new x;                                     // 也正确 无论哪种方法,如果以后想对“标准”形式的new定制新的功能,只需要重写这个函数。 调用者重新编译链接后就可以使用新功能了。 500)this.width=500'>  如果写了operator new就要同时写operator delete 让我们回过头去看看这样一个基本问题:为什么有必要写自己的operator new和operator delete? 答案通常是:为了效率。缺省的operator new和operator delete具有非常好的通用性,它的这种灵活性也使得在某些特定的场合下,可以进一步改善它的性能。尤其在那些需要动态分配大量的但很小的对象的应用程序里,情况更是如此。 例如有这样一个表示飞机的类:类airplane只包含一个指针,它指向的是飞机对象的实际描述(此技术在条款34进行说明): class airplanerep { ... };      // 表示一个飞机对象                                // class airplane {public:  ...private:  airplanerep *rep;             // 指向实际描述}; 一个airplane对象并不大,它只包含一个指针(正如条款14和m24所说明的,如果airplane类声明了虚函数,会隐式包含第二个指针)。但当调用operator new来分配一个airplane对象时,得到的内存可能要比存储这个指针(或一对指针)所需要的要多。之所以会产生这种看起来很奇怪的行为,在于operator new和operator delete之间需要互相传递信息。 因为缺省版本的operator new是一种通用型的内存分配器,它必须可以分配任意大小的内存块。同样,operator delete也要可以释放任意大小的内存块。operator delete想弄清它要释放的内存有多大,就必须知道当初operator new分配的内存有多大。有一种常用的方法可以让operator new来告诉operator delete当初分配的内存大小是多少,就是在它所返回的内存里预先附带一些额外信息,用来指明被分配的内存块的大小。也就是说,当你写了下面的语句, airplane *pa = new airplane; 你不会得到一块看起来象这样的内存块:     pa——> airplane对象的内存 而是得到象这样的内存块:     pa——> 内存块大小数据 + airplane对象的内存 对于象airplane这样很小的对象来说,这些额外的数据信息会使得动态分配对象时所需要的的内存的大小翻番(特别是类里没有虚拟函数的时候)。 如果软件运行在一个内存很宝贵的环境中,就承受不起这种奢侈的内存分配方案了。为airplane类专门写一个operator new,就可以利用每个airplane的大小都相等的特点,不必在每个分配的内存块上加上附带信息了。 具体来说,有这样一个方法来实现你的自定义的operator new:先让缺省operator new分配一些大块的原始内存,每块的大小都足以容纳很多个airplane对象。airplane对象的内存块就取自这些大的内存块。当前没被使用的内存块被组织成链表——称为自由链表——以备未来airplane使用。听起来好象每个对象都要承担一个next域的开销(用于支持链表),但不会:rep域的空间也被用来存储next指针(因为只是作为airplane对象来使用的内存块才需要rep指针;同样,只有没作为airplane对象使用的内存块才需要next指针),这可以用union来实现。 具体实现时,就要修改airplane的定义,从而支持自定义的内存管理。可以这么做: class airplane {           // 修改后的类 — 支持自定义的内存管理public:                    //    static void * operator new(size_t size);   ... private:  union {    airplanerep *rep;      // 用于被使用的对象    airplane *next;        // 用于没被使用的(在自由链表中)对象  };   // 类的常量,指定一个大的内存块中放多少个  // airplane对象,在后面初始化  static const int block_size;   static airplane *headoffreelist; }; 上面的代码增加了的几个声明:一个operator new函数,一个联合(使得rep和next域占用同样的空间),一个常量(指定大内存块的大小),一个静态指针(跟踪自由链表的表头)。表头指针声明为静态成员很重要,因为整个类只有一个自由链表,而不是每个airplane对象都有。 下面该写operator new函数了: void * airplane::operator new(size_t size){  // 把“错误”大小的请求转给::operator new()处理;  // 详见条款8  if (size != sizeof(airplane))    return ::operator new(size);   airplane *p =           // p指向自由链表的表头     headoffreelist;       //    // p 若合法,则将表头移动到它的下一个元素  //   if (p)    headoffreelist = p->next;   else {    // 自由链表为空,则分配一个大的内存块,    // 可以容纳block_size个airplane对象    airplane *newblock =      static_cast<airplane*>(::operator new(block_size *                                            sizeof(airplane)));     // 将每个小内存块链接起来形成一个新的自由链表    // 跳过第0个元素,因为它要被返回给operator new的调用者    //     for (int i = 1; i < block_size-1; ++i)      newblock[i].next = &newblock[i+1];     // 用空指针结束链表    newblock[block_size-1].next = 0;     // p 设为表的头部,headoffreelist指向的    // 内存块紧跟其后    p = newblock;    headoffreelist = &newblock[1];  }   return p;} 如果你读了条款8,就会知道在operator new不能满足内存分配请求时,会执行一系列与new-handler函数和例外有关的例行性动作。上面的代码没有这些步骤,这是因为operator new管理的内存都是从::operator new分配来的。这意味着只有::operator new失败时,operator new才会失败。而如果::operator new失败,它会去执行new-handler的动作(可能最后以抛出异常结束),所以不需要airplane的operator new也去处理。换句话说,其实new-handler的动作都还在,你只是没看见,它隐藏在::operator new里。 有了operator new,下面要做的就是给出airplane的静态数据成员的定义: airplane *airplane::headoffreelist;                                               const int airplane::block_size = 512;                                             没必要显式地将headoffreelist设置为空指针,因为静态成员的初始值都被缺省设为0。block_size决定了要从::operator new获得多大的内存块。 这个版本的operator new将会工作得非常好。它为airplane对象分配的内存要比缺省operator new更少,而且运行得更快,可能会快2次方的等级。这没什么奇怪的,通用型的缺省operator new必须应付各种大小的内存请求,还要处理内部外部的碎片;而你的operator new只用操作链表中的一对指针。抛弃灵活性往往可以很容易地换来速度。 下面我们将讨论operator delete。还记得operator delete吗?本条款就是关于operator delete的讨论。但直到现在为止,airplane类只声明了operator new,还没声明operator delete。想想如果写了下面的代码会发生什么: airplane *pa = new airplane;        // 调用                                    // airplane::operator new... delete pa;                          // 调用 ::operator delete 读这段代码时,如果你竖起耳朵,会听到飞机撞毁燃烧的声音,还有程序员的哭泣。问题出在operator new(在airplane里定义的那个)返回了一个不带头信息的内存的指针,而operator delete(缺省的那个)却假设传给它的内存包含头信息。这就是悲剧产生的原因。 这个例子说明了一个普遍原则:operator new和operator delete必须同时写,这样才不会出现不同的假设。如果写了一个自己的内存分配程序,就要同时写一个释放程序。(关于为什么要遵循这条规定的另一个理由,参见article on counting objects一文的the sidebar on placement章节) 因而,继续设计airplane类如下: class airplane {        // 和前面的一样,只不过增加了一个public:                 // operator delete的声明  ...   static void operator delete(void *deadobject,                              size_t size); }; // 传给operator delete的是一个内存块, 如果// 其大小正确,就加到自由内存块链表的最前面// void airplane::operator delete(void *deadobject,                               size_t size){  if (deadobject == 0) return;         // 见条款 8   if (size != sizeof(airplane))     {  // 见条款 8    ::operator delete(deadobject);    return;  }   airplane *carcass =    static_cast<airplane*>(deadobject);   carcass->next = headoffreelist;  headoffreelist = carcass;} 因为前面在operator new里将“错误”大小的请求转给了全局operator new(见条款8),那么这里同样要将“错误”大小的对象交给全局operator delete来处理。如果不这样,就会重现你前面费尽心思想避免的那种问题——new和delete句法上的不匹配。 有趣的是,如果要删除的对象是从一个没有虚析构函数的类继承而来的,那传给operator delete的size_t值有可能不正确。这就是必须保证基类必须要有虚析构函数的原因,此外条款14还列出了第二个、理由更充足的原因。这里只要简单地记住,基类如果遗漏了虚拟构函数,operator delete就有可能工作不正确。 所有一切都很好,但从你皱起的眉头我可以知道你一定在担心内存泄露。有着大量开发经验的你不会没注意到,airplane的operator new调用::operator new 得到了大块内存,但airplane的operator delete却没有释放它们。内存泄露!内存泄露!我分明听见了警钟在你脑海里回响。 但请仔细听我回答,这里没有内存泄露! 引起内存泄露的原因在于内存分配后指向内存的指针丢失了。如果没有垃圾处理或其他语言之外的机制,这些内存就不会被收回。但上面的设计没有内存泄露,因为它决不会出现内存指针丢失的情况。每个大内存块首先被分成airplane大小的小块,然后这些小块被放在自由链表上。当客户调用airplane::operator new时,小块被自由链表移除,客户得到指向小块的指针。当客户调用operator delete时,小块被放回到自由链表上。采用这种设计,所有的内存块要不被airplane对象使用(这种情况下,是由客户来负责避免内存泄露),要不就在自由链表上(这种情况下内存块有指针)。所以说这里没有内存泄露。 然而确实,::operator new返回的内存块是从来没有被airplane::operator delete释放,这个内存块有个名字,叫内存池。但内存泄漏和内存池有一个重要的不同之处。内存泄漏会无限地增长,即使客户循规蹈矩;而内存池的大小决不会超过客户请求内存的最大值。 修改airplane的内存管理程序使得::operator new返回的内存块在不被使用时自动释放并不难,但这里不会这么做,这有两个原因: 第一个原因和你自定义内存管理的初衷有关。你有很多理由去自定义内存管理,最基本的一条是你确认缺省的operator new和operator delete使用了太多的内存或(并且)运行很慢。和采用内存池策略相比,跟踪和释放那些大内存块所写的每一个额外的字节和每一条额外的语句都会导致软件运行更慢,用的内存更多。在设计性能要求很高的库或程序时,如果你预计内存池的大小会在一个合理的范围之内,那采用内存池的方法再好不过了。 第二个原因和处理一些不合理的程序行为有关。假设airplane的内存管理程序被修改了,airplane的operator delete可以释放任何没有对象存在的大块的内存。那看下面的程序: int main(){  airplane *pa = new airplane;     // 第一次分配: 得到大块内存,                                   // 生成自由链表,等   delete pa;                       // 内存块空;                                   // 释放它   pa = new airplane;               // 再次得到大块内存,                                   // 生成自由链表,等   delete pa;                       // 内存块再次空,                                   // 释放   ...                              // 你有了想法...   return 0;} 这个糟糕的小程序会比用缺省的operator new和operator delete写的程序运行得还慢,占用还要多的内存,更不要和用内存池写的程序比了。 当然有办法处理这种不合理的情况,但考虑的特殊情况越多,就越有可能要重新实现内存管理函数,而最后你又会得到什么呢?内存池不能解决所有的内存管理问题,在很多情况下是很适合的。 实际开发中,你会经常要给许多不同的类实现基于内存池的功能。你会想,“一定有什么办法把这种固定大小内存的分配器封装起来,从而可以方便地使用”。是的,有办法。虽然我在这个条款已经唠叨这么长时间了,但还是要简单介绍一下,具体实现留给读者做练习。 下面简单给出了一个pool类的最小接口(见条款18),pool类的每个对象是某类对象(其大小在pool的构造函数里指定)的内存分配器。 class pool {public:  pool(size_t n);                      // 为大小为n的对象创建                                       // 一个分配器   void * alloc(size_t n)  ;            // 为一个对象分配足够内存                                       // 遵循条款

阅读全文(1329) | 回复(0) | 编辑 | 精华

 



发表评论:
昵称:
密码:
主页:
标题:
验证码:  (不区分大小写,请仔细填写,输错需重写评论内容!)

 
 



The Neurotic Fishbowl

.: 公告

暂无公告...


Bloginess

«Mar.2026»
1234567
891011121314
15161718192021
22232425262728
293031

.: 我的分类(专题)

首页(49)
我的相册(9)
工作日志(2)
爱的痕迹(3)
好文转载(27)
成长记录(3)
我的音乐(0)
news(3)
综合娱乐(2)


In the Bowl

.: 最新日志

大起大落
他的来信
伤逝
wi_fi
不想说毕业
微微疼痛
100 Best Places to W
ini file class
log file class
类的继承与组合


.: 最新回复

回复:构造函数与析构函数
回复:C++从零开始之结构(9)


The Fishkeeper
blog名称:Natural Pink
日志总数:49
评论数量:12
留言数量:0
访问次数:121247
建立时间:2006年7月25日



Text Me

.: 留言板

签写新留言


Other Fish in the Sea

.: 链接


喜欢的博客:

http://nfox.spaces.live.com/
http://gaowei.cnblogs.com/

http://wang2fan2.spaces.live.com/

http://www.itepub.com/html/xiazaizhongxin/index.html




站点首页 | 联系我们 | 博客注册 | 博客登陆

Sponsored By W3CHINA
W3CHINA Blog 0.8 Processed in 0.043 second(s), page refreshed 144824656 times.
《全国人大常委会关于维护互联网安全的决定》  《计算机信息网络国际联网安全保护管理办法》
苏ICP备05006046号