Impossible is nothing  
  爱过知情重醉过知酒浓   花开花谢终是空   缘份不停留像春风来又走   女人如花花似梦
公告
日历
<2024年11月>
272829303112
3456789
10111213141516
17181920212223
24252627282930
1234567
统计
  • 随笔 - 8
  • 文章 - 91
  • 评论 - 16
  • 引用 - 0

导航

常用链接

留言簿(4)

随笔分类(4)

随笔档案(8)

文章分类(77)

文章档案(91)

相册

搜索

  •  

最新评论

阅读排行榜

评论排行榜

 

导读 Introduction

1.   所谓 declaration ,是用来将一个 object function class template 的类型告诉编译器,它不带细节信息。所谓 definition ,是用来将细节信息提供给编译器。 object 而言,其定义式是编译器为它配置内存的地点;对 function function template 而言,其定义式提供函数本体 function body class class template 而言,其定义式必须列出该 class template 的所有 members

2.   所谓 default constructor 指不需任何参数就可被调用的constructor,不是没有任何参数,就是每个参数都有默认值。通常在定义对象数组时,就会需要一个default constructor。如果该类没有提供default constructor,通常的做法是定义一个指针数组,然后利用new将每个指针一一初始化;在该方法无效的情况下,可以使用placement new方法。

3.   所谓 copy constructor 以某对象作为另一同种类型对象的初值,或许它最重要的用途就是用来定义何谓以 by value 方式传递和返回对象。事实上,只要编译器决定产生中介的临时性对象,就会需要一些 copy constructor 调用 动作,重点是: pass-by-value 便意味着调用“ copy constructor

4.   初始化 initialization 行为发生在对象初次获得一个值的时候。对于带有 constructors classes structs ,初始化总是经由调用某个 constructor 达成。对象的 assignment 动作发生于已初始化的对象被赋予新值的时候。纯粹从操作观点看, initialization assignment 之间的差异在于前者由 constructor 执行,后者由 operator = 执行。C++ 严格区分此二者,原因是上述两个函数所考虑的事情不同: constructors 通常必须检验其参数的有效性,而大部份 assignment 运算符不必如此,因为其参数已经构造完成,必然是合法的。另一方面, assignment 动作的目标对象并非是尚未建构完成的对象,而是可能已经拥有配置得来的资源。在新资源可被赋值过去之前,旧资源通常必须先行释放。

5.   C++ 的两个新特征:

bool 类型:其值不是true就是false,语言内建的关系运算符、条件判断式的返回类型都是bool。若编译器尚未实现该类型,有两种选择:

enum bool { false, true };

b ool int是不同类型,允许boolint间的函数重载,但内建关系运算符依然返回int

typedef int bool;

const bool false = 0;

const bool true = 1;

bool int成为同种类型,兼容于传统的C/C++语意,移植到支持bool的平台上后行为不变,但不允许intbool间的函数重载;

四个转型运算符:static_castconst_castdynamic_castreinterpret_cast。它们更容易在程序代码中被识别出来,编译器更容易诊断出错误的运用。

2002-6-23

改变旧有的 C 习惯    Shifting from C to C++

C 基本上只是 C++ 的一个子集,其许多技巧在 C++ 中已经不合时宜。例如以 reference to pointer 取代 pointer to pinter 。某些 C 习惯用法与 C++ 的精神相互矛盾。

条款 1 :尽量以 const liline 取代 #define (以 compiler 取代 preprocessor

理由 1 #define 定义的常量名称可能在编译之前就被 preprocessor 移走,因此不会出现于 symbol table 中,从而就没有机会被编译器看见。这样的结果是会给 debug 工作带来不便。不如改用 const 定义常量。

理由 2 #define 实现的带有实参的宏,虽然不必付出函数调用所需的成本,但用户使用时极易出错。不如使用 inline function

注意 1 .常量指针的定义,如: const char * const authorName = “Scott Meyers”

注意 2 class 专属常量,即一个 const static member ,要注意在 implementation 文件中定义它。

注意 3 .不能完全舍弃 preprocessor ,因为 #include #ifdef #ifudef 在编译控制过程中还扮演着重要角色。

条款 2 :尽量以 <iostream> 取代 <stdio.h>

尽管 scanf printf 可移植而且高效率,但是它们家族都还不够完美。尤其是它们都不具备 type-safe 性质,也都不可扩充。而 type safety extensibility 正是 C++ 的基石组成部分。再者, scanf printf 函数家族将变量与控制读写的格式化信息分离开来,读写形式不够简单统一。

注意 1 有些 iostream 的实现效率不如相应的 C stream 故不同选择可能会给程序带来很大的不同。这只是对一些特殊的实现而言;

注意 2 .在标准化的过程中, iostream 库在底层做了很多修改,所以对那些要求最大可移植性的应用程序来说,会发现不同版本遵循标准的程度也不同。

注意 3 iostream 库的类有构造函数而 <stdio.h> 里的函数没有,在某些涉及到静态对象初始化顺序的时候,如果可以确认不会带来隐患,用标准 C 库会更简单实用。

条款 3 尽量用 new delete 而不用 malloc free

malloc free constructors destructors 一无所知 由此引发的问题将是对象的初始化难度以及内存泄漏。将 malloc/free new/delete 混合使用其结果未定义 [1] ,会带来很多麻烦。

条款 4 :尽量使用 c++ 风格的注释形式

理由: C 的多行注释 /*…*/ 不支持嵌套使用。

内存管理   Memory Management

C++ 程序员而言,把事情作对,意味着正确使用 new delete 。而要让它们更有效率则意味着定制自己的 operator new operator delete

条款 5 :使用相同形式的 new delete

                   使用 new 时会有两件事情发生:内存通过 operator new 被配置,然后一个(或)多个 constructor(s) 针对此内存被调用。使用 delete 时也会有两件事情发生:一个(或)多个的 destructor(s) 针对此内存被调用,然后内存通过 operator delete 被释放。

                   delete 的最大问题在于不知道即将释放的内存内究竟存在多少对象,必须由程序员指出。故 delete 是否使用 [ ] 一定要与 new 是否使用 [ ] 保持一致,否则结果未定义,最大的可能是 memory leak

1 .含有 pointer data member 并供应多个 constructors class ,应在 constructors 中使用相同形式的 new( 包括不使用 new) pointer member 初始化。否则,在 destructor 中就不知道该使用什么形式的 delete

2 .最好不要对数组类型使用 typedef 动作!不然,要加倍小心以保证 delete new 的使用形式相同。

条款 6 :记得在 destructor 中以 delete 对付 pointer members

1 .在 class 中每加上一个 pointer member 时,几乎总要相应做以下每件事:

n          在每个 constructors 中将该指针初始化。若没有任何一个 constructor 会为该指针分配内存,那么该指针应初始化为 0

n          assignment 运算符中将该指针原有的内存删除,重新配置一块。

n          destructor delete 这个指针。

2 delete 一个 null pointer 是安全的,什么也不做。 delete 一个指向合法内存的 pointer 后,该 pointer 并不为 null pointer ,再次 delete pointer 将是非法操作。

3 .不要以 delete 来对付一个未曾以 new 完成初始化的 pointer 。除了 smart pointer objects 之外,几乎绝对不要 delete 一个传递而来的指针。

条款 7 :为内存不足的状况作准备

1.   operator new 无法配置出所需内存时将丢出一个 std::bad_alloc exception 。使用 ”nothrow objects” 形式的 new 则可以像老的编译器一样,直接返回 0

2.   operator new 在无法满足需求而丢出 exception 前会先调用 client 专属的错误处理函数,该函数通常称为 new-handler operator new 不断重复 调用 new-handler 函数,直至找到足够的内存为止。 client 必须调用 set_new_handler 来指定这个 new-handler 。在 <new> 中大致定义如下:

typedef void (*new_handler)();

new_handler set_new_handler(new_handler p) throw();         // 返回值指向上次指定的 new_handler

         可以利用 new-handler 实现一个简单的错误处理策略。

3.   一个设计良好的 new-handler 函数必须实现下面功能中的一种:

n          产生更多的可用内存。一个方法是:程序启动时分配大的内存块,在第一次调用 new-handler 时释放。释放时伴随着一些对用户的警告信息,如内存数量太少,下次请求可能会失败,除非又有更多的可用空间。

n          安装另一个不同的 new-handler 函数。如果当前的 new-handler 函数无法产生更多可用内存,它知道另一个 new-handler 可以提供更多的资源,这样它就可以安装另一个 new-handler 来取代它。另一个变通策略是让 new-handler 可以改变自己的运行行为,使得下次调用时可以做不同的事,方法是使 new-handler 可以修改那些影响它自身行为的 static global 数据。

n          卸除 new-handler 。也就是传递 null 指针给 set_new_handler 。没有安装 new-handler operator new 分配内存不成功时就会抛出一个标准的 std::bad_alloc 类型的异常。

n          抛出 std::bad_alloc 或从 std::bad_alloc 继承的其他类型的 exception 。这样的 exceptions 不会被 operator new 捕捉,所以它们会被送到最初提出内存请求的地方。抛出别的不同类型的 exception 会违反 operator new 的异常规范。规范中的缺省行为是调用 abort ,所以 new-handler 要抛出一个 exception 时,一定要确信它是从 std::bad_alloc 继承来的。

n          不返回,直接调用 abort exit abort exit 可以在标准 c 库和 c++ 库中找到。

4 要想为 class X 加上“ set_new_handler 支持能力 ,只需令 X 继承自 NewHandlerSupport class template

template<class T>           // "mixin-style" base class for class-specific

class NewHandlerSupport {       

public:                     // set_new_handler support

  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;

条款 8 :撰写 operator new operator delete 时应遵循的公约

撰写自己的内存管理函数时,其行为一定要与缺省行为保持一致。撰写 o perator new 公约:正确的返回值,内存不足时调用错误处理函数 new_handler ,处理好 0 字节内存请求,避免隐藏标准形式的 new 。撰写 o perator delete 公约:保证删除一个 null 指针永远是安全的。

1.   一个 non-member operator new 的伪码:

										
												void * operator new(size_t size)        // your operator new might
										
								
										
												{                                       // take additional params
										
								
										
												
														  if (size == 0) {                      // handle 0-byte requests
										
								
										
												
														    size = 1;                           // by treating them as
										
								
										
												
														  }                                     // 1-byte requests
										
								
										
												
														  while (1) {
										
								
										
												
														    
														attempt to allocate
														
																size
														
														bytes;
										
								
										
												
														    if (the allocation was successful)
										
								
										
												
														      return (a pointer to the memory);
										
								
										
												
														    new_handler globalHandler = set_new_handler(0);
										
								
										
												
														    set_new_handler(globalHandler);
										
								
										
												
														    if (globalHandler) (*globalHandler)();
										
								
										
												
														    else throw std::bad_alloc();
										
								
										
												
														  }
										
								
										
												
														}
												
										
								

2.   一个 non-member operator delete 的伪码

										
												
														void operator delete(void *rawMemory)
												
										
								
										
												
														{
												
										
								
										
												
														if (rawMemory == 0) return;    // do nothing if the null pointer is being deleted
												
										
								
										
												
														
																deallocate the memory pointed to by
														
														 rawMemory;
												
										
								
										
												
														return;
												
										
								
										
												
														
																     }
														
																
																
														
												
										
								

3.   operator new operator delete 可被 subclasses 继承 有可能一个 base class operator new operator delete 会被用来配置或释放其 derived class object 最好的办法是把错误数量的内存管理请求转给标准 operator new operator delete 来处理 像下面这样

										
												class Base {                       // same as before, but now
										
								
										
												public:                            // op. delete is declared
										
								
										
												
														  static void * operator new(size_t size);
										
								
										
												
														  static void operator delete(void *rawMemory, size_t size);
										
								
										
												
														  ...
										
								
										
												};
										
								
										
												void * Base::operator new(size_t size)
										
								
										
												{
										
								
										
												
														  if (size != sizeof(Base))             // if size is "wrong,"
										
								
										
												
														    return ::operator new(size);        // have standard operator new handle the request                                        
										
								
										
												
														  ...                                   // otherwise handle the request here
										
								
										
												
														}
														
																
																
														
												
										
								
										
												void Base::operator delete(void *rawMemory, size_t size)
										
								
										
												{
										
								
										
												
														  if (rawMemory == 0) return;      // check for null pointer
										
								
										
												
														  if (size != sizeof(Base)) {      // if size is "wrong,"
										
								
										
												
														    ::operator delete(rawMemory);  // have standard operator
										
								
										
												
														    return;                        // delete handle the request
										
								
										
												
														  }
										
								
										
												
														  
														deallocate the memory pointed to by rawMemory;
										
								
										
												
														  return;
										
								
										
												
														}
												
										
								

4.   撰写 member function operator new[ ] 唯一要记住的是配置生鲜内存 raw memory ,因为不知道数组中每个对象元素的大小,因此不可对数组尚未存在的对象做任何其它动作。

条款 9 避免遮掩了标准形式的 new

问题:内部范围声明的名称会隐藏掉外部范围的相同的名称,所以对于分别在类的内部和全局声明的两个相同名字的函数来说,类的成员函数会隐藏掉全局函数:

										
												class X {
										
								
										
												public:
										
								
										
												
														  void f(); 
										
								
										
												
														     // operator new allowing specification of a new-handling function
										
								
										
												
														  static void * operator new(size_t size, new_handler p);
										
								
										
												};
										
								
										
												void specialErrorHandler();                       // definition is elsewhere
										
								
										
												X *px1 =  new (specialErrorHandler) X;            // calls X::operator new
										
								
										
												X *px2 = new X;                                   // error!
										
								

方案 1 写一个 class 专属的 operator new ,令 它和标准 new 有相同的调用方式。可以用一个高效的 inline 函数封装实现:

										
												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); }
};

方案 2 operator new 的每一个额外参数提供缺省值:

										
												class X {
										
								
										
												public:
										
								
										
												
														  void f();
										
								
										
												
														  static void * operator new(size_t size, new_handler p = 0);
										
								
										
												};
												
														
														
												
										
								

条款10 如果写了 operator new 请对应写 operator delete

1.    自己撰写operator newoperator delete 通常是为了效率。因为缺省版的operator new是一种通用内存分配器,它须能分配任意大小的内存块,同样,operator delete也要可以释放任意大小的内存块。operator delete想知道释放的内存块有多大,就必须知道当初operator new分配的内存块有多大。一种常用的方法,就是在operator new返回的内存里预先附带一些额外信息,用来指明被分配的内存块的大小。

2.   以下是定制 operator new operator delete 的一段源代码:

										
												class Airplane {           // modified class 
										
										
										
												 now supports
										
								
										
												public:                    // custom memory management
										
								
										
												
														  static void * operator new(size_t size);
										
								
										
												
														     static void operator delete(void *deadObject, size_t size);
										
								
										
												
														  ...
										
								
										
												private:
										
								
										
												
														  union {
										
								
										
												
														    AirplaneRep *rep;      // for objects in use
										
								
										
												
														    Airplane *next;        // for objects on free list
										
								
										
												
														 };  
										
								
										
												
														  static const int BLOCK_SIZE;
										
								
										
												
														  static Airplane *headOfFreeList;
										
								
										
												};
										
								
										
												Airplane *Airplane::headOfFreeList;
										
								
										
												const int Airplane::BLOCK_SIZE = 512;
												
														
														
														
																
																
														
												
										
								
										
												void * Airplane::operator new(size_t size)
										
								
										
												{
										
								
										
												
														  if (size != sizeof(Airplane))
										
								
										
												
														    return ::operator new(size);
										
								
										
												
														  Airplane *p = headOfFreeList;       
										
								
										
												
														  if (p)
										
								
										
												
														    headOfFreeList = p->next;
										
								
										
												
														  else {
										
								
										
												
														    Airplane *newBlock =
										
								
										
												
														      static_cast<Airplane*>(::operator new(BLOCK_SIZE *sizeof(Airplane)));
										
								
										
												
														   
														 for (int i = 1; i < BLOCK_SIZE-1; ++i)
										
								
										
												
														      newBlock[i].next = &newBlock[i+1];
										
								
										
												
														    newBlock[BLOCK_SIZE-1].next = 0;    
										
								
										
												
														    p = newBlock;
										
								
										
												
														    headOfFreeList = &newBlock[1];
										
								
										
												
														  }
										
								
										
												
														  return p;
										
								
										
												}
										
								
										
												void Airplane::operator delete(void *deadObject, size_t size)
										
								
										
												{
										
								
										
												
														  if (deadObject == 0) return;         
										
								
										
												
														  if (size != sizeof(Airplane)){  
										
								
										
												
														    ::operator delete(deadObject);
										
								
										
												
														    return;
										
								
										
												
														  }
										
								
										
												
														  Airplane *carcass = static_cast<Airplane*>(deadObject);
										
								
										
												
														  carcass->next = headOfFreeList;
										
								
										
												
														  headOfFreeList = carcass;
										
								
										
												}
										
								
										
												3.   
										
										以上代码体现了内存池的概念,并非memory leak
								
										
												4.   
										
										如果要删除的对象是从一个没有虚析构函数的类继承而来的
										
										那传给operator deletesize_t值可能不正确。这就是必须保证基类必须要有虚析构函数的原因之一。
										
												
												
										
								

构造函数、析构函数和 Assignment 运算符

条款 11: 为需要动态分配内存的 class 声明一个 copy constructor 和一个 assignment 运算符

										
												1.   
										
										只要程序中有pass-by-value的动作,就会调用copy constructor实际不是这样,关键在编译器。
								
										
												2.   
										
										memberwise assignment动作产生的指针别名pointer aliasing问题,可能导致 memory leak以及指向同一块内存的指针被delete多次
										<
										该结果未定义>
										
								
										
												3.   
										
										针对别名问题的解决之道:如果class拥有任何指针,撰写自己的copy constructorassignment operator,在这些函数中,可以将指针所指之数据结构复制一份,使每个对象拥有属于自己的一份拷贝;也可以实现某种reference-counting策略,追踪记录指向某个数据结构的对象个数。
								

条款 12: constructor 中尽量以 initialization 取代 assignment

										
												1.   
										
										const members
										reference members不能够被赋值,只能够被初始化,必须使用initialization.
								
										
												2.   
										
										对象的构造分两个阶段:初始化data members和执行constructor。对于自定义类型member data,使用member initialization list成本只有一次constructor函数调用,而在constructor中使用operator = 会先调用default constructor,然后再调用assignment运算符。
								
										
												3.   
										
										内建类型的non-constnon-reference对象,initializationassignment并无操作上的差异。
								
										
												4.   
										
										static class member
										绝不应该在classconstructor中被初始化。
								

条款 13: 初始化列表中成员列出的顺序和它们在类中声明的顺序相同

1.   nonstatic data members class 内的初始化次序与它们的声明次序相同,而与它们在 member initialization list 中出现的次序完全无关;而它们的 destructors 则总是以相反的次序被调用。

2.   base class data members 永远在 derived class data members 之前初始化。如果使用多重继承, base classes 将以被继承的次序来初始化,同样与在 member initialization list 中出现的次序无关。

条款 14: 确定基类有虚析构函数

1.   当经由一个 base class pointer 删除一个 derived class object ,而此 base class 有一个 nonvirtual destructor 时,其结果未定义,最可能的情况是在执行期未调用 derived class destructor

2.   class 未含任何虚拟函数,往往意味着它并不是要被当作 base class 使用,此时令其 destructor virtual 通常只是浪费空间 —— 容纳不必要的 vptr 以及 vtbl

3.   有时希望将一个 class 定义为抽象类,但却没有适当的函数可选为 pure virtual function ,此时可声明一个 pure virtual destructor 。不过,必须为此 destructor 提供定义式!因为 virtual destructor 的运作方式是派生程度最深的 class ,其 destructor 最先被调用,然后是 base class destructor 被调用。这意味着编译器会对该抽象类的 destructor 产生一个调用操作。

4.   inline 函数并不是一个独立存在的函数,如果将一个 virtual destructor 声明为 inline ,虽可以避免函数调用所需的额外负担,但编译器将必须在某个地方产生一个 out-of-line 函数副本。 <vtbl 中相关项正是指向该副本 >

条款 15: operator = 返回 *this 的引用

理由:如果不这样做,就会妨碍 assignments 串链,或是妨碍隐式类型转换,或者兼而有之。

定义: classname & classname::operator = (const classname & rhs){……return *this;}

条款 16: operator = 中对所有 data members 赋值

1.   class 加入新的 data member 时, assignment 运算符应同步修改!

2.   derived class assignment 运算符有义务处理其 base class assignment 动作!两种途径:

Base::operator = (rhs);                      // 如果 assignment 运算符由编译器生成,有些编译器可能不支持;

static_cast<Base&>(*this) = rhs;     // 是指向 Base object reference ,不是 Base object 本身!

3.   实现 devived class copy constructors 时一定要确保调用 base class copy constructor 而不是 default constructor !这只需在 devived class 的成员初始化列表中为 base class 指定初值为 rhs 即可。

条款 17: operator = 中检查是否“自己给自己赋值”

理由:效率、确保正确性。

1.   assignment 运算符在为其左侧对象配置新资源时,通常必须先释放该对象已有的资源。如果不作该项检查,极有可能使用已被自己强行释放的资源。

2.   对象等同 object identity 的判定:两个对象是否有相同的值或相同的地址。

3.   别名 aliasing 问题和对象等同 object identity 问题特别容易发生在 reference pointer 身上。

4.   任何时候,在可能出现别名问题的函数中,必须将该可能性纳入考虑!

类与函数之设计与声明

         设计良好的 class ,是一种挑战。关键在于,使得自定义的 class 所导出的类型和语言内建的类型并无区别,这才是优秀的设计。

条款 18: 努力使接口神具而型微

1.   所谓“神具而型微”,是指通过该接口用户可以做他们合理要求的任何事情,但是尽可能让函数个数少,不会任何重复功能的 member functions

2.   接口函数愈多,客户愈不容易了解它;大型 class 接口的可维护性较差;长长的 class 定义,会导致长长的头文件,进一步导致长长的编译时间,从而影响整个项目的开发。

3.   对所有实用目的而言, friend 函数是 class 接口的一部分。这意味着 friend 函数也应该被纳入 class 接口的神具而型微考虑。

条款 19: 分清成员函数,非成员函数和友元函数

1.   区分成员函数,非成员函数和友元函数的规则:

虚拟函数必须是成员函数;

operator >> operator << 必须是非成员函数;

需要对最左端的参数进行类型转换的函数必须是非成员函数;

非成员函数如需访问 class 的非公有成员,必须成为 class 的友元函数;

其它情况下都声明为成员函数。

2.   比较先进的编译器会在必要的时刻为每个函数的每一个参数身上执行隐式类型转换 implicit type conversion ,但 explicit constructors 不能作为隐式类型转换使用。

3.   尽量避免 friend 函数。

条款 20: 避免 public 接口出现数据成员

理由:一致性、精确的存取控制、函数抽象性。

条款 21: 尽可能使用 const

1.   必须知道

char *p              = "Hello";          // non-const pointer,non-const data

const char *p         = "Hello";          // non-const pointer,const data

char * const p               = "Hello";          // const pointer, non-const data

const char * const p     = "Hello";          // const pointer, const data

2.   const 最具威力的用途是作用于函数声明之上,它可以用来修饰函数的返回值、个别参数,以及整个成员函数。 c onst member function 的目的是为了指明仅可由 const 对象调用。

3.   C++ 的一个重要性质 member functions 即使是常量性有所不同 也可以重载。

4.   mutable 关键词施行于 nonstatic const data member 之上 可以有效解放其 bitwise constness 方面的束缚。

条款 22: 尽量用 传引用 而不用 传值

1.   C 语言中的每样东西都是 passed by value C++ pass-by-value 当作缺省行为 除非另行指定 否则函数参数都是以实参的副本作为初值 而调用端所获得的亦是函数传回值的一个副本。

2.   对象以 by value 的方式传递,其实际意义是由该对象的 copy constructor 决定的,这可能会使 pass-by-value 成为代价很高的动作。同时,对象的 destructor 也必然会被调用。以 by reference 的方式传递参数,不会有任何的 constructor destructor 会被调用,因为没有必要产生任何新对象。它的另一个好处是可避免所谓的“切割 slicing 问题”。

3.   Passing by reference 是一件美妙的事情,但会导致某种复杂性。最知名的问题就是别名 aliasing 问题。

4.   reference 的底层几乎都是以指针完成,因此 passing by reference 通常意味着传递的是指针。对于小尺寸对象,传值可能比传引用的效率更高一些。

条款 23: 当你必须返回一个 object 时,不要试图返回一个 reference

所谓 reference 只是一个符号、一个名称,一个既有对象的名称。任何时候看到一个 reference ,都应该立即问自己,它的另一个名称是什么,那个“既有对象”是否存在,如果不存在,请不要试图返回 reference

条款 24: 在函数重载和参数缺省化之间谨慎抉择

一般而言,如果有合理的缺省值,而且只需要一个算法,那么最好是选择参数缺省化,否则使用函数重载。

条款 25: 避免对指针类型和数值类型进行重载

理由 C ++ 世界中有一种独特的情况,人们认为某个调用动作应该被视为模棱两可,可编译器却不这么认为,因为编译器必须作出抉择。

条款 26: 当心潜在的模棱两可 ambiguity 状态

有很多可能及各种情况,在程序和程序库中隐藏着模棱两可状态。一个好的软件开发这应该时刻保持警惕,将它的出现几率降至最低。

1

										
												class B;                    // forward declaration for class B
										
								
										
												class A {
										
								
										
												public:
										
								
										
												
														  A(const B&);              // an A can be constructed from a B
										
								
										
												};
										
								
										
												class B {
										
								
										
												public:
										
								
										
												
														  operator A() const;       // a B can be converted to an A
										
								
										
												};
										
								
										
												void f(const A&);
										
								
										
												B b;
										
								
										
												f(b);                       // error! 
										
										
										
												 ambiguous
										
								

2

										
												void f(int);
										
								
										
												void f(char);
										
								
										
												double d = 6.02;
										
								
										
												f(d);                         // error! 
										
										
										
												 ambiguous
										
								
										
												f(static_cast<int>(d));       // fine, calls f(int)
										
								
										
												f(static_cast<char>(d));      // fine, calls f(char)
										
								

3

										
												class Base1 {
										
								
										
												public:
										
								
										
												
														  int doIt();
										
								
										
												};
										
								
										
												class Base2 {
										
								
										
												
														
														
														
														
												
										
										
												
												
										
										
												
												
												

存取限制不能解除“因多继承而来的成员”的模棱两可状态!因此,即使此处 doIt 声明为 private 也不能改变模棱两可的事实。

public:
										
												
														  void doIt();
										
								
										
												};
										
								
										
												class Derived: public Base1,          // Derived doesn't declare
										
								
										
												
														               public Base2 {         // a function called doIt
										
								
										
												
														  ...
										
								
										
												};
										
								
										
												Derived d;
										
								
										
												d.doIt();                   // error! 
										
										
										
												 ambiguous
										
								
										
												d.Base1::doIt();            // fine, calls Base1::doIt
										
								
										
												d.Base2::doIt();            // fine, calls Base2::doIt
										
								

条款 27: 如果不想使用编译器暗自产生的 member functions 就显式拒绝它

将相应的member functions声明为  private而且不要定义它们。

条款 28: 尝试切割全局名字空间 global namespace

client 可以通过三种方式来访问名字空间里的符号:将名字空间中的所有符号全部引入某一用户空间 scope ;将个别符号引入某一用户空间;每次通过修饰符显式地使用某个符号。

类与函数之实现

条款 29 :避免传回内部数据的 handles

1.   不允许通过一个 const 对象直接或间接调用一个 non-const member function

2.   问题常常发生在返回一个指向内部 data pointer 或者 reference const member function 身上, client 可以通过这个暴露的 handle 来修改一个 const 对象的内部资料。这实质上违反了抽象性 abstraction 。解决问题的方法是要么让 member function 成为 non-const ,要么就不让它传回任何 handle

3.   non-const member function 传回 handles 也会导致麻烦,特别是返回暂时对象时。要知道:暂时对象的生命短暂,只能维持到“调用函数“之表达式结束时。此时,很可能出现 dangling handles 现象,因为该 handles 所指向的对象因暂时对象的死亡而消逝!

2002-6-23

条款 30 :避免成员函数返回这样的 non-const pointer reference ,它们指向比该函数的存取级别还要低的 members

1.   目的是拒绝让这些 members 的存取级别获得晋升从而让 clients 随意访问它们而不受存取级别的限制。

2.   特别注意:面对指针,要担心的不只是 data members ,还有 member functions ,因为函数有可能返回一个 pointer to member function ,而访问函数指针是没有存取限制的!

3.   如果一定要返回指向较低存取级别 members pointer reference ,请让它们成为 const

条款 31 :千万不要返回函数内局部对象或 new 得指针所指对象的 reference

1.   函数返回时,控制权离开函数,函数内部的局部对象会自动析构。

2.   如果函数返回 new 得指针的所指对象, client 必须在使用后 delete 通过 operator & 获得的对象指针。然而,有时这种指针是无法获得的,例如在一个链式表达式中,而且也没有理由强制 client 这样做。

3.   返回一个指向 local static 对象的 reference 同样无法正确运作。如果 someFunc 是这样的函数,那么 someFun(a) == someFun(b) 恒为真!

条款 32 :尽量延缓变量定义式的出现

1.   不止应该延缓变量的定义,直到非得使用该变量为止,而且应该直到能给予它一个初值为止。

2.   理由:可以改善效率、增加清晰度,还可以降低变量的说明需求。

2002-6-24

条款 33 :明智地运用 inlining

1.   不仅仅是免除函数调用成本,编译器最佳化机制通常用来浓缩那些不含函数调用动作的程序代码,所以当你 inlining 一个函数时,或许编译器就有能力在函数本体身上执行某种最佳化。

2.   inline 函数背后的整个观念是,将对此函数的调用动作以函数代码来取代,因此也导致了目标代码的增大。 inline 函数的定义几乎总是放在头文件中,这允许多个编译单元 translation units 得以含入相同的头文件并获得其中定义的 inline 函数。

3.   inline 指令就像 register 指令一样,只是对编译器的一种提示,而不是一个强制命令。一个表面上的 inline 函数,是否真是 inline ,必须视编译器而定。如果此函数被拒绝 inline 化,它将被视为一个 non-inline 函数,对此函数的调用动作就像正常函数调用动作一样被处理。在旧规则下,在每一个调用该函数的编译单元中的目标文件中都会产生一个该函数的定义,而且编译器视该函数为 static 从而消除连接时期的问题。此时,若该函数内还定义有 local static 变量,则该函数的每一个副本中都将拥有该变量的一份副本!在新规则下,不论牵扯的编译单元有几个,只有一个 out-of-line 的函数副本被产生出来。

4.   有时,编译器也会为一个已经成功 inline 的函数产生一个函数实体,比如在编译器或者程序员需要取该函数的地址时。

5.   inline 函数无法随着程序库的升级而升级,其改变将会导致所有用到它的程序全部重新编译。此外, inline 函数中的 static 对象常会展现反直观的行为,因此如果函数含有 static 对象,通常避免将它声明为 inline 。还有,大部分除错器 debugger 面对 inline 函数都无能为力。

6.   只要明智地运用, inline 函数会是每一个 C++ 程序员的无价之宝,作为软件开发者,其目标便是要识别出可以有效增进程序效率的百分之二十的程序代码然后将它 inline ,或是尽量拉扯直到每块代码都高效运作。

2002-6-27

条款 34 :将文件之间的编译依赖关系 compilation dependencies 降至最低

问题 由于在接口与实现分离上, C++ 表现的很不好: class 定义式中内含的不只是接口规格,还包括某些实现细节。之所以这样,是因为编译器看到一个 class object 定义式时,它必须知道要配置多少空间,唯一办法是询问 class 定义式。这样便在 class 的定义文件与其含入文件之间形成一种编译依赖,结果只要某个 class 或者其所倚赖的其它 classes 有任何一个改变了实现代码,那么含有或者使用该 class 的所有文件就必须重新编译。

1.   将编译依赖性最小化的关键技术在于以对 class 声明的依存性来取代对 class 定义的依存性,即让头文件尽可能自我满足,或至少让它依赖于 class 的声明而不要依赖于 class 定义。三项具体建议:

ü          如果 object references 或者 object pointers 可以完成任务,就不要使用 objects 。可以只靠一个类型声明就定义出指向该类型的 reference pointer ,但定义出某个类型的 object ,就要使用该类型的定义。

ü          尽量以 class 的声明取代 class 的定义。声明一个会用到某个 class 的函数,纵使它使用 by value 方式传递该 class 的参数或返回值,也是这样。此时,要提供该 class 定义的是 clients

ü          不要在头文件中再 include 其它头文件,除非不这样做就无法编译,把这个责任留给 clients 。尽可能手动声明所需要的 classes

2.   Handle classes Envelope classes 是将接口与实现分离的一项有效技术,它们将所有的函数调用转交给相应的 Body classes Letter classes ,由后者完成真正的工作。

3.   另一种不同于 Handle classes 的做法是 Protocol class 。根据定义, Protocol class 没有任何实现代码,其作用是为 derived classes 指定一个接口,它往往没有 data members ,也没有 constructors ,只有一个 virtual destructor 和一组用来表示接口的纯虚拟函数。 Protocol class clients 必须用 pointers references 来写程序,因为 Protocol classes 不可能被实体化。 Factory functions 或称为 virtual constructors 通常扮演 constructor 的角色,它们传回一个指针,指向动态配置而来的对象,该对象是真正被实体化的 derived classes 对象。最好让 Factory functions 成为 Protocol classes static 函数。

4.   实现 Protocol class 有两种最平常的机制:一种从 Protocol classes 继承接口规格,然后实现出接口中的所有函数;第二种则涉及到多重继承。

5.   不论 Handle classes Protocol classes 都无法获得 inline 函数的好处,所有实用性的 inline 函数都要求处理实现细节,而 Handle classes Protocol classes 的设计目的正是用来避免实现细节曝光。

2002-6-29

继承关系与面向对象设计 Inheritance and Object-Oriented Design

条款 35 :确定你的 public inheritance 模塑出 isa 关系

1.   C++ 完成面向对象程序设计,最重要的一个规则就是 public inheritance 意味着 isa 关系。如果令 class D public 形式继承了 class B ,便是告诉编译器:每一个类型为 D 的对象同时也是一个类型为 B 的对象,但反之并不成立。但请注意,这并不意味着 D 数组是一种 B 数组!

2.   可适用于所有软件的完美设计是不存在的。所谓最佳设计,取决于这个系统现在与将来希望做什么事。为继承体系添加多余的 classes ,就像在 classes 之间构造不正确的继承关系一样,都是不良的设计。

2002-6-30

条款 36 :区分接口继承 interface inheritance 和实现继承 implementation inheritance

1 有关接口继承 interface inheritance 和实现继承 implementation inheritance 的三条:

ü          声明一个纯虚拟函数的目的是为了让 derived classes 只继承其接口;

ü          声明一个非纯虚拟函数的目的是为了让 derived classes 继承该函数的接口和缺省行为;

ü          声明非虚拟函数的目的是为了让 derived classes 继承函数的接口及其实现。

2   可以为纯虚拟函数提供定义,也就是说可以未它提供一份实现代码。 C++ 并不认为这是错误的,不过只能静态地调用它,即必须指定其完整的 class 名称。它可以用来实现一种安全机制,为一般非纯虚拟函数提供更安全的缺省行为。

3.   非虚拟函数代表的意义是为不变性凌驾于变异性之上,所以不应该在 subclass 中重新定义它。

条款 37 :绝对不要重新定义继承而来的非虚拟函数

从实务的角度来看,非虚拟函数是静态绑定的,如果撰写 class D 并重新定义继承自 class B 的非虚拟函数,那么 D 对象很有可能展现出精神分裂的行径:当该函数被调用时,任何一个 D 对象都可能表现出 B D 的行为,决定因素不在对象本身,而在于指向该对象指指针当初的声明类型。

从理论的角度来看,所谓 public inheritance 意味着 isa 的关系,任何 D 对象都是一个 B 对象, B subclasses 一定会继承 mf 的接口与实现,如果 D 重新定义 mf ,设计就出现矛盾。                                                                                                                                                                    2002-7-1

条款 38 :绝对不要重新定义继承而来的缺省参数值

首先,重新定义一个继承而来的非虚拟函数永远是错误的行为;其次,虚拟函数是动态绑定的,而缺省参数值是静态绑定的,就是说可能会在调用一个定义于 derived class 内的虚拟函数时,却使用 base class 为它指定的缺省参数值! C++ 这样做完全是为了执行期的效率。

对象的静态类型,是程序声明它时所采用的类型;对象的动态类型,是对象目前所代表的类型,即动态类型可以表现出一个对象的行为模式,它可以在程序执行过程中改变。

                                                                                                                         2002-7-2

条款 39 :避免在继承体系中做向下转型 cast down 动作

1.   为了摆脱 downcasts ,不论花多少努力都是值得的,因为 downcasts 既丑陋又容易出错,而且还会导致程序代码难以理解、难以强化、难以维护。

2.   解决 downcast 的最佳办法是将转型动作以虚拟函数的调用取代,并让每一个虚拟函数有一个“无任何动作”的缺省实现代码,以便应用在并不想要施行该函数的任何 classes 身上;第二个办法是让类型更明确一些,使得声明式中的指针类型就是真正的指针类型。

3.   万不得已,请使用由 dynamic_cast 运算符提供的安全向下转型动作 sefe downcasting ,在转型成功时会传回一个隶属新类型的指针,如果失败则传回 null 指针。此时 downcasting 必然导致的 if-then-else 程序风格,比起使用虚拟函数,实在是拙劣之至,所以万非得已不要出此下策。

4.   任何时候不要写出根据对象类型判断的不同结果而做不同事情的代码!不要在程序中到处放条件判断式或 switch 语句,让编译器来做这件事吧。

                                                                                                                         2002-7-7

条款 40 :通过 layering 技术来模塑 has-a is-implemented-in-terms-of 关系

所谓 layering ,是以一个 class 为本,建立另一个 class ,并令所谓 layering class (外层)内含所谓 layered class (内层)对象作为 data member 。某些时候,两个 classes 不是 has-a 的关系,此时 public inheritance 并不适合它们,而 layering 技术则是实现 is-implemented-in-trms-of 关系的最佳选择。不过同时也会在这些 classes 之间产生了一个编译依存关系,利用条款 34 的技术可以很好的解决这个问题。

条款 41 :区分 inheritance templates

template 用来产生一群 classes ,其对象类型不会影响 class 的函数行为; inheritance 用于一群 classes 身上,其中,对象类型会影响 class 的函数行为。

条款 42 :明智地运用 private inheritance

1.   如果 classes 之间的继承关系是 private ,编译器通常不会自动将一个 derived class object 转换为一个 base class object 。由 private base class 继承而来的所有 members ,在 derived class 中都会变成 private 属性。

2.   private inheritance 意味着 implemented-in-terms-of ,使用这项技术的原因往往是想采用已经撰写于 base class 的某些程序代码,而不是因为 derived class base class 之间有任何概念关系存在,即 private inheritance 意味着继承实现部分,接口部分略去。

3.   对于 is-implemented-in-terms-of ,应该尽可能使用 layering ,只有在涉及到 protected member 或虚拟函数时才使用 private inheritance ,因为唯有通过继承,才得以取用 protected members ;唯有通过继承,才允许虚拟函数被重新定义。

4.   base class 舍弃 template 技术而使用泛型指针 void * 来有效地遏制代码膨胀现象,并通过将 constructors destructors 以及所有接口声明为 protected ,而将 data members 声明为 private 来阻止 clients 误用这个类;通过 private inheritance derived class 继承实现部分代码,再使用 template 技术来建立类型安全 type-safe 的接口,同时将所有接口声明为 inline 来减少执行期成本。这样的设计带来的是最高的效率和最高的类型安全性,是一项巧妙的技术!

2002-7-5

条款 43 明智地运用多继承 multiple inheritance MI

1.   MI 的根本问题是产生了一大堆但继承中不存在的复杂性。最根本的复杂性是模棱两可 ambiguity ,其次是继承时是否应该使用 virtual inheritance

2.   考虑一个 class 的两个 public base classes ,如果它们均有一个相同虚拟函数,那么在 derived class 使用这个函数时,必须明确指定这个函数属于哪个 base class ,同时还不能在 derived class 中改写这个函数,不过此时可以通过添加两个 classes 来将这个 class 继承体系中单一而模棱两可的函数名称一分为二为两个明确的、操作性质等同的名称。

3.   在钻石形继承体系中,通常要令顶层 class virtual base class ,这样底层 derived class object 中就不会内含多份顶层 class subobjects ,但也意味着增加了程序执行时间和空间的额外成本,因为 virtual base classes 常常是以对象指针来实现,而不是以对象本身来完成。由此可见,要在多继承的情况下完成有效的 class 设计,程序员似乎得拥有优秀的洞察力才行。然而,决定一个 base class 是否应该成为 virtual 缺乏一种完好的高阶定义,该决定通常只取决于“整个”继承体系的结构。在该结构尚未明朗之前,无法做出决定。

4.   非虚拟继承时, base class constructor 的实参是在下一层的 derived class 的成员初始表中指定,然而对于虚拟继承,实参是在派生程度最深 most derived classes 的成员初始表中指定。因此,对 virtual base 执行初始化动作的那个 class ,在继承体系中可能距离其 base 相当远,而一旦有新的 classes 加入此体系,初始化动作可能就要由别的 class 来担当。解决此问题的好办法是消除传递 constructor 实参至 virtual bases 的必要性。最简单的做法是避免 virtual base classes 拥有 data members

5.   考虑如下钻石形继承体系:

class A{ virtual void mf(){}; }

class B : virtual public A{}                                 // 继承默认的虚拟函数 mf

class C : virtual public A{virtual mf(){};}// 改写虚拟函数 mf

class D : public B, public C{}

代码 D *pd = new D; pb->mf(); 中, D 对象调用的 mf 会被编译器明确决议为 C::mf

6 造成 MI 这么难用是因为,要让所有细节以某种合理的方式共同运作,必然会伴随某种复杂性。其实这些复杂都源于 virtual base classes ,所以尽量避免使用 virtual bases 即钻石形继承体系。

2002-7-13

条款 44 :说出你的意思并了解你所说的每一句话

继承关系与面向对象关系中最重要的一些观念:

ü          共同的 base class 意味着共同的特征;

ü          public inheritance 意味着 isa

ü          private inheritance 意味着 is-implemented-in-terms-of

ü          layering 意味着 has-a is-implemented-in-terms-of

在牵涉到 public inheritance 时,以下几点才成立:

ü          纯虚拟函数意味着只有函数接口会被继承;

ü          一般虚拟函数意味着函数的接口及缺省实现代码会被继承;

ü          非虚拟函数意味着函数的接口和实现代码都会被继承。

2002-7-7

条款 45 :清楚知道 C++ 编译器默默为我们完成那些函数

1.   一个空的 class 在编译器处理过它之后就不再为空,如果你写: class Empty{}; 其意义相当于:

class Empty{

public:

Empty();

Empty(const Empty& rhs);

~Empty();

Empty& operator = (const Empty& rhs);

Empty* operator& ();

const Empty* operator& () const;

}

2.   这些函数只有在需要时,编译器才会定义它们。 default constructor destructor 不做任何事情,只是让你得以产生和摧毁这个 class 的对象,并提供一个便利场所让编译器放置一些用来完成幕后动作的代码;产生出来的 destructor 并非虚拟函数,除非这个 class 继承自一个拥有 virtual destructor base class ;缺省的 address-of 运算符只负责传回对象地址;对于缺省 copy constructor assignment 运算符,官方规则是对该 class nonstatic data members 执行 memberwise copy construction assignment 动作。

3.   编译器为 class 默默产生的 assignment 运算符只有在其函数代码不但合法而且合理的情况下才会有正确行为,否则编译器会拒绝产生一个 operator = ,并报告编译错误。因此,如果打算让内含 reference member const member class 支持 assignment 动作,必须自行定义 assignment 运算符。如果 base classes 将标准的 assignment 运算符声明为 private ,编译器也会拒绝为其 derived class 产生 assignment 运算符。

2002-7-8

杂项讨论 Miscellany

条款 46 :宁愿编译和连接时出错,也不要执行时才错

不做执行期检验工作,程序会更小更快;尽可能将错误侦测工作交给连接器来做,或最好是交给编译器来做,这样做的好处不仅是程序大小的降低和数度的增加,好包括可信度的提升。相反,在执行期侦测错误,比起在编译期或连接期捕捉它们要麻烦许多。通常,只需稍稍改变设计,就可以在编译期捕捉除可能的执行期错误,这往往要加入新的类型。

条款 47 :使用 non-local static objects 之前先确定它已有初值

所谓 non-local static objects ,是指定义于 global namespace scope class 或者 file scope 内的 static objects 。每当在不同的编译单元内定义有 non-local static objects 时,想要决定它们以适当的次序初始化是极度困难甚至无法办到的事情。最常见的形式是,在多个编译单元中, non-local static objects 被隐式的 template 具现化行为产生出来,这样不但不可能决定正确的初始化次序,甚至不值得我们寻找有可能决定正确次序的特殊情况。

解决问题的方案是不再取用 non-local static object ,而改用行为类似 non-local static objects 的对象。这项被称为 Singleton pattern 的技术很简单:首先,将每个 non-local static object 移到一个它专属的函数中,在那里将它声明为 static ;然后,令这个函数传回一个 reference ,指向这个 static object 。这项技术的关键是以函数内的 static objects 取代 non-local static objects ,其依据是 C++ 虽然对于何时初始化一个 non-local static object 没有任何明确表示,但它却非常明白的指出函数中的 static object 的初始化时机。使用这项技术可以保证所获得的 references 一定指向已经初始化妥当的对象,同时,如果从未调用这样的函数还可以避免对象的构造成本和析构成本。

2002-7-10

条款 48 :不要对编译器的警告信息视而不见

警告信息天生就和编译器相依相靠,所以轻率地依赖编译期为你找出程序错误,决不是什么好主意。编译器的用途基本上是将 C++ 代码转换为可执行代码,而不是一张安全网。

条款 49 :尽量让自己熟悉 C++ 标准程序库

1.   为了避免程序员使用的 class 名称或函数名称与标准程序库所提供的名称相冲突,标准程序库的每一样东西几乎都驻在 namespase std 之中。由此而来的问题是,世上有无可数计的 C++ 代码依赖那些已使用多年的准标准程序库,那些软件并不知道什么是 namespace 。以下是 C++ 表头文件的组织状态:

ü          旧有的C++头文件如<iostream.h>尽管不是官方标准,但有可能继续存在。这些头文件的内容不在namespace std中;

ü          新的C++头文件如<iostream>包含的基本功能和对应的旧头文件相同,但其内容在namespace std中。(在标准化的过程中,程序库中有些组件细节稍有修改,所以新旧头文件中的实体不一定完全对应。)

ü          标准C头文件如<stdio.h>继续被支持。这类头文件的内容不在std中。

ü          具有C库功能的新C++头文件具有如<cstdio>这样的名字。它们提供的内容和相应的旧C头文件相同,但全部放在std中。

2.   关于标准程序库,必须知道的第二件事是,几乎每一样东西都是templatestringcomplexvector都不是class而是class templatecin的真正类型是basic_istream<char>string的真正类型是basic_string<char>

3.   C++ 标准程序库内的主要组件:

ü          C 标准程序库,它有些小小的改变,但整体而言改变不大。

ü          iostreams 。和传统的iostream相比,它已被template化了。它的继承体系已被修改,内容被扩充以便可以抛出异常信息,同时它可以支持strings和多国文化。它依旧支持stream buffersformattersmanipulatorsfiles以及cincoutcerrclog等对象。

ü          strings

ü          containers C++ 标准库为vectorslistsqueuesstacksdequesmapssetsbitsets提供了高效的实现。同时,strings也是containers

ü          algorithms 。用以轻松操作标准containers,它们大部分都适用于所有containers以及语言内建的数组身上。

ü          国际化internationalization支持。其主要组件是facetslocales。前者描述某一文化的特殊字符集应该如何处理,包括校对规则、日期和时间表示法、信息代码与自然语言之间的映射关系等等。后者含有一整组facetsC++允许多个locales同时在程序库中起作用,所以同一个程序的不同部分可能会采用不同的规则。

ü          数值处理。

ü          诊断功能。标准程序库提供三种方法来记录错误:经由Cassertions、经由错误代码、经由异常信息。

2002-7-11

条款 50 :加强自己对 C++ 的了解

C++ 最主要的几个设计目标:与C兼容、高效率、与传统工具和开发环境兼容、解决问题的真正能力,它的目的是成为专业程序员在各种不同领域中解决真正问题的一个威力强大的工具。这些目标可以解释C++语言的许多来龙去脉。

2002-7-13

posted on 2006-06-03 17:50 笑笑生 阅读(412) 评论(0)  编辑 收藏 引用 所属分类: C++语言

只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理


 
Copyright © 笑笑生 Powered by: 博客园 模板提供:沪江博客