关闭

effective C++ 1.让自己习惯C++

419人阅读 评论(0) 收藏 举报
分类:
一.让自己习惯C++


条款01:视C++为一个语言联邦


为了更好的理解C++,我们将C++分解为四个主要次语言:
C:说到底C++仍是以C为基础。区块,语句,预处理器,内置数据类型,数组,指针统统来自C。
Object-Oreinted C++:这一部分是面向对象设计之古典守则在C++上的最直接实施。类,封装,继承,多态,virtual函数等等...
Template C++:这是C++泛型编程部分。
STL:STL是个template程序库。容器(containers),迭代器(iterators),算法(algorithms)以及函数对象(function objects)...


请记住:
这四个次语言,当你从某个次语言切换到另一个,导致高效编程守则要求你改变策略。C++高效编程守则视状况而变化,取决于你使用C++的哪一部分。






条款02:尽量以const,enum,inline替换#define


这个条款或许可以改为“宁可 以编译器替换预处理器”,因为或许#define不被视为语言的一部分。即尽量少用预处理。


编译过程:.c文件--预处理-->.i文件--编译-->.o文件--链接-->bin文件
预处理过程扫描源代码,对其进行初步的转换,产生新的源代码提供给编译器。检查包含预处理指令的语句和宏定义,并对源代码进行相应的转换。预处理
过程还会删除程序中的注释和多余的空白字符。可见预处理过程先于编译器对源代码进行处理。预处理指令是以#号开头的代码行。


例:#define ASPECT_RATIO 1.653
记号名称ASPECT_RATIO也许从未被编译器看见,也许在编译器开始处理源代码之前它就被预处理器移走了。即编译源代码时ASPECT_RATIO已被1.653取代,
导致运用此常量但获得一个变异信息也许会提到1.653,但你不知道1.653来自何处,将因追踪它而浪费时间。这个问题也可能在记号式调试器中,原因相同:ASPECT_RATIO可能并未进入记号表(symbol table)。


替换:const double AspectRatio = 1.653;
好处应该有:作为一个语言常量,AspectRatio 肯定会被编译器看到,当然会进入记号表内。多了类型检查,因为#define 只是单纯的替换,而这种替换在目标码中可能出现多份1.653;改用常量绝不会出现相同情况。


常量替换#define两点注意:
1.定义常量指针:
由于常量定义式通常被放在头文件内(以便被不同的源码含入),因此有必要将指针(而不只是指针所指之物)声明为const。
const char *authorName = “Shenzi”;//char*-based字符串,必须写两次const
    cosnt std::string authorName("Shenzi");//string-based比char*-based适宜
2.定义class专属常量:
为了将常量的作用域限制于class内,你必须让他成为class的一个成员;而为确保此常量至多只有一份实体,你必须让他成为一个static成员。
class GamePlayer{
private:
static const int NumTurns = 5;//常量声明式
int scores[NumTurns];//使用常量
};
然而NumTurns的声明式并非定义式。通常C++要求你对你所使用的任何东西提供一个定义式,但如果他是个class专属常量又是static且为整形(如int,char,bool),则需特殊处理。只要不取他们的地址,你就可以声明并使用他们而无需提供定义式。但如果你取某个class专属常量的地址,或你的编译器坚持看到一个定义式,你就必须在一个实现文件而非头文件中提供定义:const int GamePlayer::NumTurns;//NumTurns的定义,由于class常量已在声明时获得初值,因此定义时不可以再设初值。


无法利用#define创建一个class专属常量,因为#define并不重视作用域。一旦被宏定义,他就在其后面的编译过程中有效。


旧的编译器也许不支持上述语法,他们不允许static成员在声明式上获得初值。则可以将初值放在定义式:
class CostEstimate{
private:
static const double Fudge;//定义常量
...
};
const double CostEstimate::Fudge= 1.35;
但如果你在class编译期间需要一个class常量值,例如上述GamePlayer::scores[NumTurns]中,必须在编译期间知道数组的大小。此时可以用“the enum hack”补偿法。其理论基础是:“一个属于枚举类型的数值可权充int被使用”,于是定义如下:
class GamePlayer{
private:
enum {NumTurns = 5;};//令NumTurns成为5的一个记号名称。
int scores[NumTurns];//没问题
};
enum行为更像#define而不像const,原因如下:
取一个const的地址是合法的,但取一个enum的地址就不合法,而取一个#define的地址通常也不合法。如果你不想让别人获取一个pointer或reference指向你的某个整数常量,enum可以帮助你实现这个约束。

另一个常见#define无用的情况是它实现宏。宏看起来像函数,但不会招致函数调用带来的额外开销。
例: #define CALL_WITH_MAX(a,b)    f((a) > (b)) ? (a) : (b))//无论何时当你写出这种宏,你必须记住宏中所有实参加上小括号,否则易出错。
但即使加了小括号,也会变得不可思议,如下:
int a=5,b=6;
CALL-WITH_MAX(++a,b);//a被累加两次
CALL_WITH_MAX(++a,b+10);//a被累加一次
a的递增竟然取决于“它和谁比较”!

解决办法:template inline函数

template<typename T>
inline void callWithMax(const T & a,const T & b)
{
f(a>b?a:b);
}


请记住:
对于单纯常量,最好以const对象或enum替换#define;
对于形似函数的宏,最好改用inline函数替换#define。   






条款3:尽可能使用const
const允许你告诉编译器和其他程序员某值应保持不变,只要“某值”确实是不该被改变的,那就该确实说出来。


面对指针:
如果关键字const出现在星号左边,表示被指物事常量。const char *p和char const *p两种写法意义一样,都说明所致对象为常量;
如果关键字const出现在星号右边,表示指针自身是常量。
char greeting[] = "Hello";
        char *p = greeting;    //指针p及所指的字符串都可改变;
        const char *p = greeting;    //指针p本身可以改变,如p = &Anyother;p所指的字符串不可改变;
        char * cosnt p = greeting;    //指针p不可改变,所指对象可改变;
        const char * const p = greeting;    //指针p及所致对象都不可改变
STL迭代器以指针模拟:
const std::vector<int>::interator iter = vec.begin();//作用像T *const, ++iter 错误:iter是const
        std::vector<int>::const_iterator cIter = vec.begin();//作用像const T*,*cIter = 10 错误:*cIter是const


面对函数:const可以与返回值,各参数,函数本身产生关联
1.const函数返回值:往往可以降低因客户错误而造成的意外,而不至于放弃安全性和高效性。
class Rational {...};
const Rational operator* (const Rational &lhs, cosnt Rational &rhs);//防止与内置类型不兼容,如(a*b)=c;


2.const函数参数:和local const对象一样,除非你有需要改动参数或local变量,否则请将他们声明为const。


3.const成员函数:将const实施于成员函数的目的是为了确认该成员函数可作用与const对象身上。


const成员函数使class接口比较容易被理解(可以知道哪个函数可以改动对象而哪个函数不能),它们使“操作const对象”称为可能(如pass by reference-t-const方式传递对象);
两个成员函数如果只是常量性不同,可以被重载。
自己的建议:如果成员函数为const,若返回值与类成员有关,则其返回值也应该是const。如下:
class CTexrBlock{
public:
...
char& operator[]{std::size_t position} const
{return pText[position];}
private:
char * pText;
};
const CTextBlock cctb("Hello");
char * pc =&cctb[0];
*pc = 'J'; //此时cctb变成“Jello”,const成员函数没有起到作用。


const成员函数和non-const成员函数避免重复:
如果const成员函数和一个non-const成员函数重载 ,并且代码较长,使用non-const成员函数调用const成员函数。不可以使用const成员函数调用


non-const成员函数,因为对象有可能被改动。举例如下:
class TextBlock{
public:
...
const char& operator[](std::size_t position) const
{
...
return text[position];
}

char& operatoe[](std::size_t position)
{
return const_cast<char&>(//将op[]返回值的const转除
static_cast<const TextBlock&>(*this)//为*this加上const调用const op[],
[position]);     //将non-const对象转为const对象是安全转型
}

}




请记住:
将某些东西声明为const可帮助编译器侦测出错误用法。const可被施加于任何作用域内的对象、函数参数、函数返回类型、成员函数本体;
当cosnt和non-const成员函数有着实质等价的实现时,令non-const版本调用const版本可避免代码重复。






条款04:确定对象被使用前已先被初始化
1.永远在使用对象之前先将它初始化。对于无任何成员的内置类型,你必须手工完成此事。至于内置类型以外的任何其它东西,初始化责任落在构造函数身上,确保每一个构造函数都将对象的每一个成员初始化。


2.赋值与初始化:
C++规定,对象的成员变量的初始化动作发生在进入构造函数本体之前。应将成员变量的初始化置于构造函数的初始化列表中,因为初始化列表中针对各成员变量而设的实参,被拿去作为个成员变量之构造函数的实参。
class PhoneNumber{...};
class ABEntry{
public:
ABEentry(const std::string & name,const std::string & address,
const std::list<PhoneNumber> & phones);
private:
std::string theName;
std::string theAddress;
std::list<PhoneNumber> thePhones;
int numTimesConsulted;
};


ABEntry::ABEntry(const std::string& name, const std::string& address,
                                     const std::list<PhoneNumber>& phones)

theName = name;                //这些都是赋值,而非初始化
theAddress = address;          //这些成员变量在进入函数体之前,他们的默认构造函数会被自动调用,接着又调用赋值函数,
thePhones = phones;            //也就是说要经过两次的函数调用。            
numTimesConsulted = 0;//但这对numTimesConsulted 不为真,因为它属于内置类型,不保证在你所看到的那个赋值动作的时间点之前获得初值



ABEntry::ABEntry(const std::string& name, const std::string& address,
                                    const std::list<PhoneNumber>& phones) 
        : theName(name),                    //这些才是初始化 
        theAddress(address),                //这些成员变量只用相应的值进行拷贝构造函数,所以通常效率更高。
        thePhones(phones),
        numTimesConsulted(0)
{    } 
所以,对于非内置类型变量的初始化应在初始化列表中完成,以提高效率。而对于内置类型对象,如numTimesConsulted(int),其初始化和赋值的成本相同,但为了一致性最好也通过成员初始化表来初始化。如果成员变量时const或reference,它们就一定需要初值,不能被赋值。
有些类拥有多个构造函数,每个构造函数都有自己的成员初值列,这种情况下可以改用他们的赋值操作,并将那些赋值操作移往某个函数(通常是private),供所有构造函数用。这种做法在“成员变量的初值系由文件或数据库读入”时特别有用。


晦涩错误:指两个成员变量的初始化带有次序性。例如初始化数组时需要制定大小,因此代表大小的那个成员变量必须先有初值。



3.不同编译单元内定义的non-local static对象:
static对象:global对象,定义于namespace作用域的对象,在class内,在函数内,以及在file作用域内被声明为static的对象。
函数内的static对象被称为local static对象,其他static对象被称为non-local satic对象。编译单元:产出单一目标文件的那些源码,基本上它是单一源码文件加上其含入的头文件。


我们关心的问题:含有两个编译单元源码文件,每一个内至少有一个non-local static对象,如果某编译单元的某个non-local static对象的初始化动作使用了另外一个编译单元的某个non-local static对象,他所用到的这个对象可能尚未初始化,因为C++对“定义于不同编译单元内的non-local static对象”的初始化次序并无明确定义。
A:
class FileSystem{
public:
...
std::size_t numDisk() const;
...
};
extern FileSystem tfs;


B:
class Directory{
public:
Directory(params);
...
};
Directory::Directory(params){
...
std:;size_t disks = tfs.numDisks();
...
}


Directory temDir(params); //此时除非tfs在temDir之前被初始化,否则temDir的构造函数会用到尚未初始化的tfs。


解决办法:将每个non-local static对象搬到自己的专属函数内(该对象在此函数内为static)。这些函数返回一个reference指向它所含的对象。然后用户调用这些函数,而不直接指涉这些对象。换句话说non-local static 对象向被local static对象替换了。这是Singleton模式的一个常见实现手法。
这个手法的基础在于:C++保证,函数内的local static对象会在“该函数被调用期间”“首次遇上该对象的定义式”时初始化。所以如果你以“函数调用”(返回一个reference指向local static对象)替换“直接访问non-local static对象”,你可以保证你获得的那个reference指向一个已初始化的对象。
如下:
A:
class FileSystem{
public:
...
std::size_t numDisk() const;
...
};
FileSystem & tfs(){
static FileSystem fs;
return fs;
}
B:
class Directory{
public:
Directory(params);
...
};
Directory::Directory(params){
...
std::size_t disks = tfs().numDisks();
...
}
Directory & temDir(){
static Directory td;
return td;
}
这样淡村行的reference-returnn是他们成为绝佳的inlining候选人。


请记住:
为内置对象进行手工初始化,因为C++不保证初始化它们;
构造函数最好使用成员初始化列表,而不要在构造函数本体内使用赋值操作。初始化列表列出的成员变量,其排列次序应该和它们在类中的声明次序相同;
为免除“跨编译单元的初始化次序”问题,请以local static对象替换non-local static对象。
0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:178037次
    • 积分:3721
    • 等级:
    • 排名:第8646名
    • 原创:205篇
    • 转载:12篇
    • 译文:1篇
    • 评论:21条
    博客专栏
    最新评论