归去来兮

 

2008年12月30日

Effective C++读书笔记之三 :确定对象被使用前已先被初始化

关于对象初始化,c++似乎反复无常,例如:
int x;
在某些语境条件下保证初始化为0,而在另外一些语境下却并没有这个保证。而下边这种情况:
class Point{
int x,y;
}
;

Point p;
p的成员函数有的时候被初始化(0),有的时候没有被初始化。
现在,已经有了一些规则,对象的初始化何时会发生,何时不会发生。但是这些规则过于复杂,对我们的记忆有一定的挑战,哈哈。
通常如果使用C part of C++而且初始化可能招致运行期成本,那就不保证发生初始化。一旦进入non-C parts of C++,规则有一些变化。这就是为啥array(C part of C++)不保证其内容被初始化,而vector(STL part of C++)却有此保证。
我们的最佳处理状态是,在使用任何对象之前先将它初始化。对于无任何成员的内置类型,必须手动完成此事:
int x = 0;   //对int进行初始化;
const char *text = " A C-style string";   //对指针进行初始化;
double d;
std::cin 
>> d;   //采用input stream的方式完成初始化;
至于内置类型以外的其他类型,初始化的责任落在了构造函数身上。规则很简单:确保构造函数都将对象的每一个成员初始化。
这个规则很容易奉行,值得注意的是别混淆了赋值(assignment)和初始化(initialization)。考虑一个表现通讯簿的class:
class PhoneNumber{};
class ABEntry{    //Address Book Entry;
public:
ABEntry(
const std::string &name, const std::string &address, const std::list<PhoneNumber> &phones);
private:
std::
string theName;
std::
string tehAddress;
std::list
<PhoneNumber> thePhones;
int numTimesConsulted;
}
;

ABEntry::ABEntry(
const std::string &name, const std::string &address, std::list<PhoneNumber> &phones)
{//以下全是赋值(assignment),不是初始化(initialization);
theName = name;
theAddress 
= address;
thePhones 
= phones;
numTimesConsulted 
= 0;
}
ABEntry对象会带给你期望的值,但不是最佳做法。C++规定:对象成员变量的初始化动作发生在进入构造函数本体之前。在ABEntry构造函数内,theName, theAddress, thePhone都不是被初始化,而是被赋值。初始化的时间发生的更早,发生于这些成员函数的default构造函数被自动调用之时(比进入ABEntry构造函数本体的时间更早)。但这对numTimesConsulted不为真,因为他属于内置类型,不保证在你看到的那个赋值动作的时间点之前获得初值。
ABEntry构造函数的一个较好写法是,使用所谓的member initializatin list(成员函数初始列)替换赋值动作:
ABEntry::ABEntry(const std::string &name, const std::string &address,
 
const std::list<PhoneNumber> &phones)
: theName(name),
 theAddress(address),
 thePhones(phones), 
numTimesConsulted(
0)
{}//构造函数本体不需要任何动作
这个构造函数和上一个的结果相同,但是通常效率较高。基于赋值的那个版本首先调用default构造函数为theName, theAddress,和thePhones设立初值,然后立刻再对他们赋值。default的一切作为此刻都浪费了。成员初始列(member initianlization list)的做法避免了这一问题,因为初始列中针对各个变量设的实参,被拿去作为各个成员函数的实参。本例中theName以name为初值进行copy构造,theAddress以address为初值进行copy构造,thePhones以phones为初值进行copy构造。
对于大多数类型而言,比起先调用default构造函数再调用copy assignment操作符,单只调用一次copy构造函数是比较高效的,有事甚至高的多。对于内置类型,如numTimesConsulted,其初始化成本和赋值的成本相同,但为了一致性最好也通过成员初始列来进行初始化。同样道理,如果我们想default构造一个成员变量,也可以使用成员初始列,只要指定nothing作为初始化列就行了,假设ABEntry有一个无参构造构造函数,我们可以实现如下:
ABEntry::ABEntry()
:theName(),             
//调用theName的构造函数
theAddress(),           //调用theAddress的构造函数
thePhones(),            //调用thePhomes的构造函数
numTimesConsulted(0)    //记得将此内置类型显式定义为0
{}
此处记得一条规则,那就是在成员初始列中列出所有的成员变量,以免还得记住哪些成员变量可以无需初值。举个例子,如numTimesConsulted属于内置类型,如果(member initialization list)遗漏了他,他就没有初值,因而可能开启“不明确行为”的潘多拉盒子。
有些情况下,即使面对的是内置类型,也一定要使用初始列。如果成员变量是const或者reference,他们一定得需要赋初值,而不是赋值。为避免成员变量何时必须在成员初始列中初始化,何时不需要,最简单的做法就是:总是使用成员初始列。
C++有着十分固定的“成员初始化次序”。是的,次序总是相同的:base classes总是先于derived classes被初始化,而class的成员变量总是以其声明次序被初始化。让我们在看一下ABEntry,其theName永远先被初始化,然后是theAddress,之后thePhones,最后是numTimesConsulted,即使他们在member initialization list中以不同的顺序出现,也不会有任何影响。为了避免迷惑,你在member initialization list中条列各个成员时,最好总是以其声明次序为次序。
一旦我们已经 很小心的将内置型成员变量明确的加以初始化,而且也确保构造函数运用member initialization list初始化base classes和成员变量,那就剩下唯一的一件事情需要操心,就是:不同编译单元内定义之non-local static对象的初始化次序。
所谓static对象,其寿命从被构造出来直到程序结束为止,因此stack和heap_based对象都被排除。这里所说的static对象包括global对象,定义于namespace对象内的对象,在class内,在函数内,以及在file内被声明为static的对象。函数内的static对象称为local static对象,其他则成为non-local static对象,程序结束时static对象会自动销毁,也就是他们的析构函数会在main()结束时被调用。
所谓编译单元(translation unit)是指产生单一目标文件(single object file)的那些源码。基本上是单一源码文件加上其所包含的头文件。
现在,上面所涉及到的问题至少包括两个源码文件,每一个内含至少一个non-local static对象(也就是说对象是global,或者位于namespace内,或者class内,或者file作用域内被声明为static)。问题是:如果某个编译单元内的某个non-local static对象的初始化动作使用了另外一个编译单元内的non-local static对象,他所使用的这个对象可能没有初始化,因为C++对“不同编译单元内的non-local static 对象”的初始化次序并没有明确定义。
来个例子:
 假设有一个FileSystem class,它让互联网上的文件看起来像是本机。由于这个class使世界看起来像个单一文件系统,可能会产生一个特殊对象,位于global或namespace作用域内,象征单一文件系统:
class FileSystem{
public:

std::size_t numDisks() 
const;

}
;
extern FileSystem tfs;//预备给客户使用的对象,tfs代表"the file system"
FileSystem对象绝不是一个无关痛痒的对象,因此客户如果在theFileSystem对象构造完成前就使用他,会得到惨重的代价:
现在假设建立了一个class用以处理文件系统内部的目录。很自然,他们会用上theFileSystem的对象:
class Directory{
public:
Directory();

}
;
Directory::Directory()
{

std::size_t disks 
= tfs.numDisks();

}
进一步假设,这些客户决定创建一个directory对象,用来放置临时文件:
Directory tempDir(params);//为临时文件而做出的目录
现在,初始化的重要作用显示出来了,除非tfs在tempDir之前被初始化,否则tempDir的构造函数会用到尚未初始化的tfs。但是tfs和tempDir是不同的人在不同的时间创建出来的,他们是定义于不同编译单元内地non-local static对象,如何能确认tfs在tempDir之前先被初始化?
哦,这是无法确认的。
一个小小的设计可以改变这种情形:将每一个non-local static 对象搬到自己的专属函数内(该对象在此函数内被声明为static)。这些函数返回一个reference用来指向他所包含的对象。然后用户调用这些函数,而不是直接指涉这些对象。换句话就是non-local static对象被local static对象替换了。这个实现的基础在于:C++保证,函数内的non-local static 对象会在函数被调用期间首次遇上该对象之定义式时被初始化。所以如果以函数调用替换直接访问non-local static 对象,就获得了保证,保证所得的那个reference将指向一个经历初始化的对象。 此技术实现于上面的代码如下:
class FileSystem{};//同前
FileSystem &tfs()//这个函数用来替换tfs对象;他在FileSystem中可能是个static。
{
static FileSystem fs;
return fs;
}

class Directory{};
Directory::Directory()
{

std::size_t disks 
= tfs().numDisks();

}

Directory 
&tempDir()
{
static Directory td;
return td;
}
修改之后,我们只是使用的是tfs()和tempDir(),而不是tfs和tempDir。也就是使用的是指向static的reference而不是static对象自身。
这样的reference-returning函数往往十分单纯:第一行定义并初始化一个local static对象,第二行返回他。当然,他是绝佳的inline候选,特别是经常被调用的话。但是,另一方面,任何一种non-const static对象,不论是local或者non-local,在多线程下“等待某事发生都会有麻烦的。处理这个麻烦的一种做法是:在程序的单线程启动阶段手工调用所有的reference-returning函数(??),这可消除与初始化有关的race condition。
为避免对象初始化之前过早的使用他们,你需要做的是三件事:手工初始化内置的non-member对象;使用member initialization list;在初始化次序不确定下加强你的设计。


Things to remember:
1.Manually initialize objects of built-in type, because C++only sometimes initializes them itself;
2.In a constructor, prefer to use the member initialization list to assigenment inside the body of the constructor. List data member in the initialization list in the same order they're declared  in the class.
3.Avoid initialization order problems across translation units by replacing non-local static objects with local static objects.

posted @ 2008-12-30 11:06 Edmund 阅读(308) | 评论 (0)编辑 收藏

2008年12月9日

Effective C++读书笔记之二 :尽可能使用const

条款三:尽可能使用const(use const whenever possible)
const允许你指定一个语义约束,而编译器会强制实施这项约束。它允许你告诉编译器和其他程序员某值应该保持不变。有一条约束需要注意,那就是:如果const出现在*号的左边,那就是说被指物是常量;如果出现在星号右边,则表示指针本身是常量;如果出现在两边,则被指物和指针都是常量。如果被指物是常量,则关键字const写在类型的前面和类型之后,星号之前两种所表示的语义是相同的。例如下面这两种写法是一样的:
void f1(const Widget* pw);
void f2(Widget const * pw);
const也可用来修饰STL中的迭代器。声明迭代器为const就想声明指针为const一样(即T* const 指针),表示这个迭代器不得指向不同的东西。但它所指的东西的值是可以改变的。如果希望迭代器所指的东西不可改变(即模拟一个const T*指针),需要的是const_iterator:
std::vector<int> vec;
...
const std::vector<int>::iterator iter = vec.begin();// same as T* const
*iter = 10;                                                        //no problem
++iter;                                                              //wrong!!
std::vector<int>::const_iterator cIter = vec.begin();//same as const T*
*iter = 10;                                                             //wrong!!
++iter;                                                                  //no problem

const 最具威力(?)的用法是面对函数声明时的应用。在一个函数声明式内,const可以和函数返回值,各参数,函数自身(成员函数)产生关联。
令函数返回一个常量值,往往可以降低因客户错误而造成的意外,而又不至于放弃安全性和高效性。例如,考虑有理数的operator*声明式:
class Rational(){...};
const Rational operator* (const Rational & lhs, const Rational & rhs);
也许你会说为什么返回一个const对象?原因是如果不这样别人可能实现这样的暴行:
Rational a,b,c;
...
(a*b)=c;
下面,主要说明const作用于成员函数。
许多人都忽视了这么一个事实,那就是如果两个成员函数只是常量性不同,那么他们是可以重载的。考虑以下这个用来表示一大块文字的class:

class TextBlock{
public:

const char& operator[](std::size_t position) const
{return text[position];}
char& operator[](std::size_t position)
{return text[position];}
private:
std::
string text;
}
;
TextBlock的operator[]可以这么使用:
TextBlock tb(
"Hello");
std::cout 
<< tb[0];  //调用non-const 

const TextBlock ctb("Hello");
std::cont 
<< ctb[0]; //调用const

真是情形中const对象多用于passed by pointer-to-const或passed by reference-to-const的传递结果。上述的ctb太过于造作,下边这个比较真实:
void print (const TextBlocd& ctb)
{
  std::cout 
<< ctb[0];
  
}

只用重载operator[]并对不同的版本给予不同的返回类型,就可以令const和non-const获得不同的处理。
此处需要注意一点,non-const operator[]的返回类型是个reference to char,不是char。如果operator[]返回的是个char,下边的赋值就不能通过编译:
tb[0] = 'x'; //error c2106: ' = ' : left operand must be l-value
那是因为,如果函数的返回类型是个内置类型,那么改动函数的返回值从来就不合法。纵使合法,C++以by value返回对象这一事实(条款20)意味着改动的其实只是tb.text[0]的一个副本,不是tb.text[0]本身,那不是我们想要的结果。
下边来说说在const和non-const成员函数中避免重复
假设TextBlock(和CTextBlock)内的operator[]不单只是返回一个reference指向某字符,也执行边界检查、志记访问信息、甚至可能进行数据完整性检验。把所有这些同时放进const和non-const operator[]中,导致这样的一个怪物:
class TextBlock{
public:

const char& operator[](std::size_t position) const
{
      
//边界检查(bounds checking)
      //志记数据访问(log access data)
      //检验数据完整性(verify data integrity)
return text[position];
}

char& operator[](std::size_t position)
{
      
//边界检查(bounds checking)
      //志记数据访问(log access data)
      //检验数据完整性(verify data integrity)
return text[position];
}

private:
std::
string text;
}
;
其中代码的代码重复性及伴随的编译时间,维护,代码膨胀等问题真是令人头疼啊。当然了,将边界检查……等所有代码移植到另一个成员函数,并令两个版本的operator[]调用它,是可能的,但是还是重复了一些代码,例如函数调用,两次return语句等。
我们真正要做的,是实现operator[]的机能一次并使用它两次。也就是说,你必须使一个调用另一个。这促使我们将常量性转除(casting away constness)。
就一般而言,casting是一个糟糕的想法,在条款27中有详细的说明。然而代码重复也不是什么令人愉快的经验。本例中cosnt operator[]完全做掉了non-const版本该做的一切,唯一不同是其返回类型多了一个const资格修饰。这种情况下如果将返回值的const转除是安全的,因为不论谁调用non-const operator[]都一定首先有个non-const对象,否则就不能够调用non-const函数。所以令non-const operator[]调用其const兄弟是一个避免重复的安全做法:
class TextBlock{
public:

const char& operator[](std::size_t position) const
{



return text[position];
}

char& operator[](std::size_t position)
{
const_cast
<char&>(static_cast<const TextBlock&>
(
*this)[position]);
}


}
;
这里面有两个转型动作,而不是一个。我们打算让non-const operator[]调用const兄弟,但是non-const如果只是单纯调用operator[],会递归调用自己。为了避免无穷递归,我们必须明确指出调用的是const operator[]。因此,这里将*this从其原始类型TextBlock&转型为const TextBlock&。所以这里有两次转型:第一次用来为*this添加const,第二次则是从const operator[]的返回值中移除const。添加const的那一次转型强迫进行了一次安全转型,所以采用static_cast。移除const的那个动作只能由const_cast完成,没有其他选择。
下面来考虑一下反向的做法:令const来调用non-const以避免重复。这个不是我们应该做的。const成员函数承诺绝对不改变其对象的逻辑状态,non-const成员函数却没有这般承诺。如果在const函数内部调用了non-const函数,就是冒了这样的风险:你曾经承诺不改动的那个对象被改动了。这就是为什么“const成员函数调用non-const成员函数”是一种错误行为:因为对象有可能因此而被改动。反向调用才是安全的:non-const函数本来就可以对其对象做任何动作,所以在其中调用一个const成员函数并不会带来任何风险。

本条目总结:

Things to Remember

  • Declaring something const helps compilers detect usage errors. const can be applied to objects at any scope, to function parameters and return types, and to member functions as a whole.

  • Compilers enforce bitwise constness, but you should program using conceptual constness.

  • When const and non-const member functions have essentially identical implementations, code duplication can be avoided by having the non-const version call the const version.

posted @ 2008-12-09 23:00 Edmund 阅读(174) | 评论 (0)编辑 收藏

2008年12月8日

effectiv c++ 读书笔记之一

开始写effective c++的读书笔记。今天是条款2:尽量以const,enum,inline替换#define(prefer consts,enums,and inlines to #define.)
现在在维护代码的时候,前辈们大片大片的宏搞得我是那个晕头转向啊,真希望他们也看过本条款
1.Case:#define ASPECT_RATIO 1.653
   Recommendation:const double AspectRatio = 1.653;
Reason:   当使用ASPECT_RATIO但是获得一个编译错误信息时,可能你会很是发冏,因为这个错误信息也许会提到1.653而不是ASPECT_RATIO。如果ASPECT_RATIO定义在非你所写的头文件中,你更是因为追踪他而浪费时间。改为推荐的方式后,你找到的肯定是AspectRatio。当以常量替换#define时,有两种注意的情况,第一种是定义常量指针(const pointers)。由于常量定义式常放在头文件内,因此有必要将指针也声明为const。例如在一个头文件内定义一个常量的char*-based字符串,必须写const两次:
const char* const authorName = "Edmund";
这里采用string对象比其前辈char*-based更合适,
const std::string authorName("Edmund");
第二种是class专属常量。为了将常量的作用域限制在class内,你必须让他成为class的一个成员;而为确保此常量只有一个实体,则必须声明为static:
class GamePlayer{
private:
static const int NumTurns = 5;
int scores[NumTurns];
...
}
然而,你看到的是NumTurns的声明式而不是定义式,C++通常要求我们所使用的任何东西都要有一个定义式,但如果他是个class的专属常量而又是static且为整数类型(ints,chars,bools),则做特殊处理。只要不取他们的地址,你可以声明并使用他们而无需提供定义式。但如果取某个class专属常量的地址,或纵使不取地址而编译器却坚持要看到一个定义式,你就必须提供另外一个定义式:
const int GamePlayer::NumTurns;
由于NumTurns在声明时已经获得了初值,因此定义时不可以再设初值。此外,对所谓的“in-class初值设定”也只允许对整数常量进行。如果为非整型则可以采用下面的这种方式:
class CostEstimate{
private:
static const double FudgeFactor;
...
}

const double CostEstimate::FudgeFactor = 1.35;
当你在编译期需要一个class常量值,例如在上述GamePlayer::scores的数组声明中,此时如果编译器不允许“static整数型class常量”完成“in-class初值设定”,可采用enum来解决,其理论基础是“一个属于枚举类型的数值可权充ints被使用”,于是GamePlayer可定义如下:
class GamePlayer{
private:
enum{NumTurns = 5};
int scores[NumTurns];
...
};
注意:取一个const的值是合法的,但是取一个enum的值就是不合法的,取一个#define的值也是不合法的。如果你不想让别人获得一个pointer或者reference指向你的某个整数常量,enum可以帮助你实现这个约束。
下边继续说预处理器。另外一个常见的#define误用的情景是以他来实现宏,宏看起来像函数,但是不会招致函数调用带来的额外开销,例如:
#define CALL_WITH_MAX(a,b) f((a)>(b)?(a):(b))。他的缺点就不说了,替代方式:
template<class T> inline void callWithMax(const T& a, const T& b)
{
f(a > b?a : b);
}
本条目总结:
1.对于单纯常量,最好以const对象或者enums替换#defines;
2.对于形似函数的宏,最好改用inline函数替换#defines。




Ps:本文是第一次在cppblog上发表的文章,呵呵。很早就想在这上面写点了,但是不是忙这就是忙那,昨天下定决心,先把effective C++(3e)里面的55条读书笔记写在这上面。打算每天一个条目,这里面好多跟书上的句子一样,但是全是我自己敲进去的,不存在任何的paste。所写均是自己搞清楚的,不明白地方的暂时没有添加。

posted @ 2008-12-08 23:50 Edmund 阅读(210) | 评论 (0)编辑 收藏

仅列出标题  

导航

统计

常用链接

留言簿(1)

随笔分类

随笔档案

搜索

最新评论

阅读排行榜

评论排行榜