Effective C++【六】(Scott Meyers著 侯捷译)

Categories: 工具语言, 读书
Comments: No Comments
Published on: 2011 年 12 月 01 日

杂项讨论

条款45:清楚知道C++(编译器)默默为我们完成和调用哪些函数
声明一个类后什么都不写,如果程序在需要的时候,编译器会体贴的为你声明一个default constructor、一个copy constructor、一个destructor、一个assignment运算符和一对address of(取址)运算符。

class Empty{};

和你这么写是一样的:

class Empty {
public:
Empty(); // 缺省构造函数
Empty(const Empty& rhs); // 拷贝构造函数
~Empty(); // 析构函数 ---- 是否为虚函数看下文说明
Empty& operator=(const Empty& rhs); // 赋值运算符
Empty* operator&(); // 取址运算符
const Empty* operator&() const;
};

对于copy constructor 和assignment运算符,官方规定是:缺省的copy constructor或(assignment运算符)对该class的nonstatic data members执行memberwise copy constructor(或assignment)动作。也就是说,如果m是class c中的一个型别T的nonstatic data member,而c没有声明copy constructor或assignment运算符,那么m会调用T所定义的copy constructor或assignment运算符来进行copy constructor或assignment动作。如果T没有定义那些函数,这一规则递归施行于m的data members身上,知道找到一个copy constructor或assignment运算符或遇到内建型别(int,double,指针等)为止。缺省情况下各种内建型别的对象系以bitwise copy方式进行copy constructor或assignment。对继承体系中的classes而言,这个规则将施行于继承体系的每一层,所以用户自定义的copy constructors和assignment运算符会在它们被声明的那一层被调用。

如果你打算为一个“内涵reference member”的class支持assignment动作,你必须自行定义assignment运算符。对于“内含const members”的class,编译器亦有类似行为。修改const members是不合法的,所以编译器在一个隐式产生的assignment运算符中不知道该如何对待它们。如果base classes将标准的assignment运算符声明成private,编译器也会拒绝为其derived class产生assignment运算符。

条款46:宁愿编译和链接时出错,也不要执行时才错
除了少数会引起c++抛出异常信息exceptions的事态(如内存不足)外,“执行期错误”的概念和c++之间的疏离程度就像和c一样。即没有侦测是否溢位(underflow,overflow),是否除0,也没检查是否越过数组边界。一旦过了编译器那关,剩下的就全靠自己了,再也没有声明可以保护你的了。
对于运行时错误来说,情况大不一样。在某次运行期间程序没有产生任何运行时错误,你就能确信另一次不同的运行期内不会产生错误吗?比如:在另一次运行中,你以不同的顺序做事,或者采用不同的数据,或者运行更长或更短时间,等等。你可以不停地测试自己的程序直到面色发紫,但你还是不能覆盖所有的可能性。因而,运行时发现错误比在编译链接期间检查错误更不能让人放心。

通常,对设计做一点小小的改动,就可以在编译期间消除可能产生的运行时错误。这常常涉及到在程序中增加新的数据类型(参见条款M33)。例如,假设想写一个类来表示时间中的日期,最初的做法可能象这样:

class Date {
public:
Date(int day, int month, int year);
...
};

准备实现这个构造函数,面临的一个问题是对day和month值的合法性检查。让我们来看看,对于传给month的值来说,怎么做可以免于对它进行合法性检查呢?一个明显的办法是采用枚举类型而不用整数:
enum Month { Jan = 1, Feb = 2, ... , Nov = 11, Dec = 12 };
class Date {
public:
Date(int day, Month month, int year);
...
};

遗憾的是,这不会换来多少好处,因为枚举类型不需要初始化:
Month m;
Date d(22, m, 1857); // m是不确定的
所以,Date构造函数还是得验证month参数的值。

既想免除运行时检查,又要保证足够的安全性,你就得用一个类来表示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;
};
class Date {
public:
Date(int day, const Month& month, int year);
...
};

这个设计在几个方面的特点综合确定了它的工作方式。首先,Month构造函数是私有的。这防止了用户去创建新的month。可供使用的只能是Month的静态成员函数返回的对象,再加上它们的拷贝。第二,每个Month对象为const,所以它们不能被改变(否则,很多地方会忍不住将一月转换成六月,特别是在北半球)。最后一点,得到Month对象的唯一办法是调用函数或拷贝现有的Month(通过隐式Month拷贝构造函数 ---- 见条款45)。这样,就可以在任何时间任何地方使用Month对象;不必担心无意中使用了没有被初始化的对象。(否则就可能有问题。条款47进行了说明)

有了这些类,用户几乎不可能指定一个非法的month,甚至完全不可能 ---- 如果不出现下面这种可恶的情况的话:
Month *pm; // 定义未被初始化的指针
Date d(1, *pm, 1997); // 使用未被初始化的指针!
但这种情况所涉及的是另一个问题,即通过未被初始化的指针取值,其结果是不可确定的。(参见条款3,看看我对 "不确定行为" 的感受)遗憾的是,我没有办法来防止或检查这种异端行为。但是,如果假设这种情况永远不会发生,或者如果我们不考虑这种情况下软件的行为,Date构造函数对它的Month参数就可以免于合法性检查。另一方面,构造函数还是必须检查day参数的合法性 ---- 九月,四月,六月和十一月各有多少天呢?

Date的例子将运行时检查用编译时检查来取代。你可能想知道什么时候可以使用链接时检查。实际上,不是经常这么做。C++用链接器来保证所需要的函数只被定义一次(参见条款45,"需要" 一个函数会带来什么)。它还使用链接器来保证静态对象(参见条款47)只被定义一次。你可以用同样的方法使用链接器。例如,条款27说明,对于一个显式声明的函数,如果想有意禁止对它进行定义,链接器检查就很有用。

但不要过于强求。想消除所有的运行检查是不切实际的。例如,任何允许交互式输入的程序都要进行输入验证。同样地,某个类中如果包含需要执行上下限检查的数组,每次访问数组时就要对数组下标进行检查。尽管如此,将检查从运行时转移到编译或链接时一直是值得努力的目标,只要实际可行,就要追求这一目标。这样做的奖赏是,程序会更小,更快,更可靠。

条款47:使用nonlocal static objects之前先确定它已有初值
每当你有nonlocal static objects,而他们定义于不同的编译单元,且其行为视他们“是否以某种正确次序初始化”而定时,这样的问题就会产生。所谓nonlocal static objects是指
1)、定义于global或namespace scope;
2)、在某个class内声明static;
3)、在某个file scope内定义为static。
改变这种情况有一个办法,Singleton pattern技术,首先将一个non-local static object 转移到一个他专属的函数中,在这个函数中声明成static,接下来这个函数传回一个reference,它指向内含的那个object。Clients不再直接调取static object,而是改掉函数。这样你可以在该函数内作出初始化。这里提一下,慎用inline,条款33有讲为什么。这样做之后你可以很容易的控制各个对象的初始化顺序。

条款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中重新声明,它被完全隐藏了(参见条款50:为什么这样)。忽视这条编译器警告几乎肯定会导致错误的程序行为。你会不停地调试去找原因,而这个错误实际上早就被编译器发现了。

当然,在对某个编译器的警告信息积累了经验之后,你会真正理解不同的信息所表示的含义(唉,往往和它们表面看上去的意思不同)。一旦有了这些经验,你会对很多警告不予理睬。这没问题,但重要的是,在忽略一个警告之前,你一定要准确理解它想告诉你的含义。

只要谈到警告,就要想到警告是和编译器紧密相关的,所以在编程时不要马马虎虎,寄希望于编译器为你找出每一条错误。例如上面隐藏了函数的那段代码,当它通过不同的(但使用很广泛的)编译器时可能不会产生警告。编译器是用来将C++转换成可执行格式的,并不是你的私人保镖。你想得到那样的安全?去用Ada吧。

另外需要强调一点:不要依赖编译器替你寻找错误或者不安全的地方!编译器的作用是帮你转换为可执行。

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

总的来讲,很多,下去自己看吧。。。。

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

总的来讲,你不了解的c++还很多,请查看枯燥无味的c++国际标准规格书

我猜你可能也喜欢:

No Comments - Leave a comment

Leave a comment

电子邮件地址不会被公开。 必填项已用*标注

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>


Welcome , today is 星期日, 2017 年 12 月 17 日