Effective C++摘要《第7章:杂项 真正认识C++》20090210

===条款45: 弄清C++在幕后为你所写、所调用的函数===
一个空类什么时候不是空类? ---- 当C++编译器通过它的时候。如果你没有声明下列函数,体贴的编译器会声明它自己的版本。
这些函数是:一个拷贝构造函数,一个赋值运算符,一个析构函数,一对取址运算符。
另外,如果你没有声明任何构造函数,它也将为你声明一个缺省构造函数。所有这些函数都是公有的。
换句话说,如果你这么写:
class Empty{};
和你这么写是一样的:
class Empty {
public:
  Empty();                        // 缺省构造函数
  Empty(const Empty& rhs);        // 拷贝构造函数

  ~Empty();                       // 析构函数 ---- 是否
                                  // 为虚函数看下文说明
  Empty&
  operator=(const Empty& rhs);    // 赋值运算符

  Empty* operator&();             // 取址运算符
  const Empty* operator&() const;
};

缺省构造函数和析构函数实际上什么也不做,它们只是让你能够创建和销毁类的对象,注意,生成的析构函数一般是非虚拟的
缺省取址运算符只是返回对象的地址
至于拷贝构造函数和赋值运算符,官方的规则是:缺省拷贝构造函数(赋值运算符)对类的非静态数据成员进行 "以成员为单位的" 逐一拷贝构造(赋值)。
即,如果m是类C中类型为T的非静态数据成员,并且C没有声明拷贝构造函数(赋值运算符),m将会通过类型T的拷贝构造函数(赋值运算符)被拷贝构造(赋值)---- 如果T有拷贝构造函数(赋值运算符)的话。如果没有,规则递归应用到m的数据成员,直至找到一个拷贝构造函数(赋值运算符)或固定类型(例如,int,double,指针,等)为止。默认情况下,固定类型的对象拷贝构造(赋值)时是从源对象到目标对象的 "逐位" 拷贝。
但通常,编译器生成的赋值运算符要想如上面所描述的那样工作,与此相关的所有代码必须合法且行为上要合理。如果这两个条件中有一个不成立,编译器将拒绝为你的类生成operator=,你就会在编译时收到一些诊断信息。
template<class T>
class NamedObject {
public:
  // 这个构造函数不再有一个const名字参数,因为nameValue
  // 现在是一个非const string的引用。char*构造函数
  // 也不见了,因为引用要指向的是string
 NamedObject(string& name, const T& value):nameValue(name),objectValue(value)
 {}


private:
  string nameValue;
  const T objectValue;         // 现在为const
};

string newDog("Persephone");
string oldDog("Satch");

NamedObject<int> p(newDog, 2);      // 正在我写本书时,我们的
         // 爱犬Persephone即将过
         // 她的第二个生日
NamedObject<int> s(oldDog, 29);     // 家犬Satch如果还活着,
         // 会有29岁了(从我童年时算起)
NamedObject<int> t = p; //OK
p = s;            // p中的数据成员将会发生些什么呢?编译不通过!
上述中p=s编译不能通过,VC编译器提示信息"“operator =”函数在“NamedObject<T>”中不可用"
原因在于数据成员objectValue为const类型,const数据在初始化后就不能改变
同样,如果将nameValue类型改为引用string& nameValue,那么也会导致编译不通过
因为引用则总是指向在初始化时被指定的对象,以后不能改变。
===条款46: 宁可编译和链接时出错,也不要运行时出错===
通常,对设计做一点小小的改动,就可以在编译期间消除可能产生的运行时错误。这常常涉及到在程序中增加新的数据类型
class Date {
public:
  Date(int day, int month, int year);
};
改为
class Date {
public:
  Date(int day, const Month& month, int year);
};
从而去除Date中运行时对Month的检查,但要新增累Month
class Month {
public:
  static const Month Jan() { return 1; }
  static const Month Feb() { return 2; }
  ...
  static const Month Dec() { return 12; }

  int asInt() const           // 为了方便,使Month
  { return monthNumber; }     // 可以被转换为int
private:
  Month(int number): monthNumber(number) {}
  const int monthNumber;
};
这个设计在几个方面的特点综合确定了它的工作方式。
1、Month构造函数是私有的。这防止了用户去创建新的month。可供使用的只能是Month的静态成员函数返回的对象,再加上它们的拷贝。
2、每个Month对象为const,所以它们不能被改变(否则,很多地方会忍不住将一月转换成六月,特别是在北半球)。
3、到Month对象的唯一办法是调用函数或拷贝现有的Month(通过隐式Month拷贝构造函数)
这样,就可以在任何时间任何地方使用Month对象;不必担心无意中使用了没有被初始化的对象。
Month m(Month::Feb());
Date d(10, m, 2009);
有了这些类,用户几乎不可能指定一个非法的month,甚至完全不可能 ---- 如果不出现下面这种可恶的情况的话:
Month *pm;                 // 定义未被初始化的指针
Date d(1, *pm, 1997);      // 使用未被初始化的指针!
将检查从运行时转移到编译或链接时一直是值得努力的目标,只要实际可行,就要追求这一目标。这样做的奖赏是,程序会更小,更快,更可靠。如Date构造函数对它的Month参数就可以免于合法性检查
===条款47: 确保非局部静态对象在使用前被初始化===
非局部静态对象指的是这样的对象:
· 定义在全局或名字空间范围内(例如:theFileSystem和tempDir),
· 在一个类中被声明为static,或,
· 在一个文件范围被定义为static。
对于不同被编译单元中(比如不同的cpp)的非局部静态对象,你一定不希望自己的程序行为依赖于它们的初始化顺序,因为你无法控制这种顺序。让我再重复一遍:你绝对无法控制不同被编译单元中非局部静态对象的初始化顺序。
附:什么是编译单元 ?
当一个c或cpp文件在编译时,预处理器首先递归包含头文件,
形成一个含有所有 必要信息的单个源文件,这个源文件就是一个编译单元。
这个编译单元会被编译成为一个与cpp 文件名同名的目标文件 。
连接程序把不同编译单元中产生的符号联系起来,构成一个可执行程序。
class FileSystem { ... };            // 在个类在你
                                     // 的程序库中
FileSystem theFileSystem;            // 程序库用户
                                     // 和这个对象交互
假设某个程序库的用户创建了一个类,表示文件系统中的目录。很自然地,这个类使用了theFileSystem:
class Directory {                    // 由程序库的用户创建
public:
  Directory();
  ...
};

Directory::Directory()
{
  通过调用theFileSystem的成员函数
  创建一个Directory对象;
}
进一步假设用户想为临时文件专门创建一个全局Directory对象:
Directory tempDir;                  // 临时文件目录

现在,初始化顺序的问题变得很明显了:除非theFileSystem在tempDir之前被初始化,否则,tempDir的构造函数将会去使用还没被初始化的theFileSystem。
但顺序是无法控制的!

解决办法:
确保非局部静态对象在使用前被初始化的问题像蝴蝶效应一样,对你的实现细节十分敏感。但是,如果你不强求一定要访问 "非局部静态对象",而愿意访问具有和非局部静态对象 "相似行为" 的对象(不存在初始化问题),难题就消失了。取而代之的是一个很容易解决的问题,甚至称不上是一个问题。这种技术 ---- 有时称为 "单一模式"
首先,把每个非局部静态对象转移到函数中,声明它为static。
其次,让函数返回这个对象的引用。这样,用户将通过函数调用来指明对象。换句话说,用函数内部的static对象取代了非局部静态对象。
这个方法基于这样的事实:虽然关于 "非局部" 静态对象什么时候被初始化,C++几乎没有做过说明;但对于函数中的静态对象(即,"局部" 静态对象)什么时候被初始化,C++明确指出:它们在函数调用过程中初次碰到对象的定义时被初始化。所以,如果你不对非局部静态对象直接访问,而用返回局部静态对象引用的函数调用来代替,就能保证从函数得到的引用指向的是被初始化了的对象。
class FileSystem { ... };            // 同前
FileSystem& theFileSystem()          // 这个函数代替了
{                                    // theFileSystem对象
  static FileSystem tfs;             // 定义和初始化
                                     // 局部静态对象
                                     // (tfs = "the file system")

  return tfs;                        // 返回它的引用
}

class Directory { ... };             // 同前

Directory::Directory()
{
  同前,除了theFileSystem被
  theFileSystem()代替;
}

Directory& tempDir()                 // 这个函数代替了
{                                    // tempDir对象
  static Directory td;               // 定义和初始化
                                     // 局部静态对象
  return td;                         // 返回它的引用
}
===条款48: 重视编译器警告===
class B {
public:
  virtual void f() const;
};

class D: public B {
public:
  virtual void f();
};
本来是想用D::f重新定义虚函数B::f,但有个错误:在B中,f是一个const成员函数,但在D中没有被声明为const。
据我所知,有个编译器会这么说:
warning: D::f() hides virtual B::f()
对于这条警告,很多缺乏经验的程序员会这样自言自语,"D::f当然会隐藏B::f ---- 本来就应该是这样!" 错了。编译器想告诉你的是:声明在B中的f没有在D中重新声明,它被完全隐藏了
===条款49: 熟悉标准库===
1、在归纳标准库中有些什么之前,需要介绍一下它是如何组织的
标准委员会为包装了std的那部分标准库构件创建新的头文件名。生成新头文件的方法仅仅是将现有C++头文件名中的 .h 去掉
所以<iostream.h>变成了<iostream>,<complex.h>变成了<complex>,等等。
对于C头文件,采用同样的方法,但在每个名字前还要添加一个c。所以C的<string.h>变成了<cstring>,<stdio.h>变成了<cstdio>,等等
下面是C++头文件的现状:
· 旧的C++头文件名如<iostream.h>将会继续被支持,尽管它们不在官方标准中。这些头文件的内容不在名字空间std中。
· 新的C++头文件如<iostream>包含的基本功能和对应的旧头文件相同,但头文件的内容在名字空间std中。(在标准化的过程中,库中有些部分的细节被修改了,所以旧头文件和新头文件中的实体不一定完全对应。)
· 标准C头文件如<stdio.h>继续被支持。头文件的内容不在std中。
· 具有C库功能的新C++头文件具有如<cstdio>这样的名字。它们提供的内容和相应的旧C头文件相同,只是内容在std中。
因此
<string.h>是旧的C头文件,对应的是基于char*的字符串处理函数;
<cstring>是对应于旧C头文件的std版本;
<string>是包装了std的C++头文件,对应的是新的string类。
2、关于标准库,需要知道的第二点是,库中的一切几乎都是模板。
iostream
iostream帮助你操作字符流,但什么是字符?是char吗?是wchar_t?是Unicode字符?一些其它的多字节字符?没有明显正确的答案,所以标准库让你去选。所有的流类(stream class)实际上是类模板,在实例化流类的时候指定字符类型。例如,标准库将cout类型定义为ostream,但ostream实际上是一个basic_ostream<char>类型定义(typedef )。
string不是类,它是类模板:类型参数限定了每个string类中的字符类型。
complex不是类,它是类模板:类型参数限定了每个complex类中实数部分和虚数部分的类型。
vector不是类,它是类模板。
...
在标准库中你无法避开模板,但如果只是习惯于和char类型的流和字符串打交道,通常可以忽略它们。
这是因为,对这些组件的char实例,标准库都为它们定义了typedef,这样你就可以在编程时继续使用cin,cout,cerr等对象,以及istream,ostream,string等类型,不必担心cin的真实类型是basic_istream<char>以及string的真实类型是basic_string<char>。
traits:
对于string,可以基于"它所包含的字符类型" 确定它的参数,但不同的字符集在细节上有不同,例如,特殊的文件结束字符,拷贝它们的数组的最有效方式,等等。这些特征在标准中被称为traits,它们在string实例中通过另外一个模板参数指定。
这里有一个basic_string模板的完整声明,以及建立在它之上的string类型定义(typedef);
namespace std {
  template<class charT,
           class traits = char_traits<charT>,
           class Allocator = allocator<charT> >
     class basic_string;

  typedef basic_string<char> string;
}

标准C++库中有哪些主要组件:
· 标准C库。

· Iostream。和 "传统" Iostream的实现相比,它已经被模板化了,继承层次结构也做了修改,增强了抛出异常的能力,可以支持string(通过stringstream类)和国际化(通过locales ---- 见下文)。当然,你期望Iostream库所具有的东西几乎全都继续存在。也就是说,它还是支持流缓冲区,格式化标识符,操作子和文件,还有cin,cout,cerr和clog对象。这意味着可以把string和文件当做流,还可以对流的行为进行更广泛的控制,包括缓冲和格式化。

· String。string对象在大多数应用中被用来消除对char*指针的使用。它们支持你所期望的那些操作(例如,字符串连接,通过operator[]对单个字符进行常量时间级的访问,等等),它们可以转换成char*,以保持和现有代码的兼容性,它们还自动处理内存管理。一些string的实现采用了引用计数(参见条款M29),这会带来比基于char*的字符串更佳的性能(时间和空间上)。

· 容器。不要再写你自己的基本容器类!标准库提供了下列高效的实现:vector(就象动态可扩充的数组),list(双链表),queue, stack,deque,map,set和bitset。唉,竟然没有hash table(虽然很多制造商作为扩充提供),但多少可以作为补偿的一点是, string是容器。这很重要,因为它意味着对容器所做的任何操作(见下文)对string也适用。

· 算法。标准容器当然好,如果存在易于使用它们的方法就更好。标准库就提供了大量简易的方法(即,预定义函数,官方称为算法(algorithm) ---- 实际上是函数模板),其中的大多数适用于库中所有的容器 ---- 以及内建数组(built-in arrays)!
标准算法有for_each(为序列中的每个元素调用某个函数),find(在序列中查找包含某个值的第一个位置),count_if(计算序列中使得某个判定为真的所有元素的数量),equal(确定两个序列包含的元素的值是否完全相同),search(在一个序列中找出某个子序列的起始位置),copy(拷贝一个序列到另一个),unique(在序列中删除重复值),rotate(旋转序列中的值),sort(对序列中的值排序)。等等

· 对国际化的支持。不同的文化以不同的方式行事。
支持国际化最主要的构件是facets和locales。facets描述的是对一种文化要处理哪些特性,包括排序规则(即,某地区字符集中的字符应该如何排序),日期和时间应该如何表示,数字和货币值应该如何表示,怎样将信息标识符映射成(自然的)明确的语言信息,等等。locales将多组facets捆绑在一起。

· 对数字处理的支持。C++库为复数类(实数和虚数部分的精度可以是float,double或long double)和专门针对数值编程而设计的特殊数组提供了模板。

· 诊断支持。标准库支持三种报错方式:C的断言(参见条款7),错误号,例外。为了有助于为例外类型提供某种结构,标准库定义了下面的例外类(exception class)层次结构:
                                                       |---domain_error
                     |----- logic_error<---  |---invalid_argument
                     |                                 |---length_error
                     |                                 |---out_of_range
exception<--|                                         
                     |                                 |--- range_error
                     |-----runtime_error<--|---underflow_error
                                                       |---overflow_error

logic_error(或它的子类)类型的例外表示的是软件中的逻辑错误。理论上来说,这样的错误可以通过更仔细的程序设计来防止。runtime_error(或它的子类)类型的例外表示的是只有在运行时才能发现的错误。
可以就这样使用它们,可以通过继承它们来创建自己的例外类,或者可以不去管它。没有人强迫你使用它。

标准库中容器和算法这部分一般称为标准模板库(STL)
STL中实际上还有第三个构件 ---- 迭代(Iterator)

===条款50: 提高对C++的认识===
C++中有很多 "东西":C,重载,面向对象,模板,例外,名字空间。这么多东西,有时让人感到不知所措。怎么弄懂所有这些东西呢?
C++之所以发展到现在这个样子,在于它有自己的设计目标。理解了这些设计目标,就不难弄懂所有这些东西了。
C++最首要的目标在于:
· 和C的兼容性。很多很多C还存在,很多很多C程序员还存在。C++利用了这一基础,并建立在 ---- 我是指 "平衡在" ---- 这一基础之上。
· 效率。作为C++的设计者和第一个实现者,Bjarne Stroustrup从一开始就清楚地知道,要想把C程序员争取过来,就要避免转换语言会带来性能上的损失,否则他们不会对C++再看第二眼。结果,他确信C++在效率上可以和C匹敌 ---- 二者相差大约在5%之内。
· 和传统开发工具及环境的兼容性。各色不同的开发环境到处都是,编译器、链接器和编辑器则无处不在。从小型到大型的所有开发环境,C++都要轻松应对,所以带的包袱越轻越好。想移植C++?你实际上移植的只是一种语言,并利用了目标平台上现有的工具。(然而,往往也可能带来更好的实现,例如,如果链接器能被修改,使得它可以处理内联和模板在某些方面更高的要求)
· 解决真实问题的可应用性。C++没有被设计为一种完美的,纯粹的语言,不适于用它来教学生如何编程。它是设计为专业程序员的强大工具,用它来解决各种领域中的真实问题。真实世界都有些磕磕碰碰,因此,程序员们所依赖的工具如果偶尔出点问题,也不值得大惊小怪。
以上目标阐明了C++语言中大量的实现细节,如果没有它们作指导,就会有摩擦和困惑。为什么隐式生成的拷贝构造函数和赋值运算符要象现在这样工作呢,尤其是指针(参见条款11和45)?因为这是C对struct进行拷贝和赋值的方式,和C兼容很重要。为什么析构函数不自动被声明为virtual(参见条款14),为什么实现细节必须出现在类的定义中(参见条款34)呢?因为不这样做就会带来性能上的损失,效率很重要。为什么C++不能检测非局部静态对象之间的初始化依赖关系(参见条款47)呢?因为C++支持单独编译(即,分开编译源模块,然后将多个目标文件链接起来,形成可执行程序),依赖现有的链接器,不和程序数据库打交道。所以,C++编译器几乎不可能知道整个程序的一切情况。最后一点,为什么C++不让程序员从一些繁杂事务如内存管理(参见条款5-10)和低级指针操作中解脱出来呢?因为一些程序员需要这些处理能力,一个真正的程序员的需要至关重要。

 

 

你可能感兴趣的:(Effective C++摘要《第7章:杂项 真正认识C++》20090210)