《Effective C++》第1章 让自己习惯C++-读书笔记

章节回顾:

《Effective C++》第1章 让自己习惯C++-读书笔记

《Effective C++》第2章 构造/析构/赋值运算(1)-读书笔记

《Effective C++》第2章 构造/析构/赋值运算(2)-读书笔记

《Effective C++》第3章 资源管理(1)-读书笔记

《Effective C++》第3章 资源管理(2)-读书笔记

《Effective C++》第4章 设计与声明(1)-读书笔记

《Effective C++》第4章 设计与声明(2)-读书笔记

《Effective C++》第5章 实现-读书笔记

《Effective C++》第8章 定制new和delete-读书笔记


 

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

为了理解C++,你必须认识其主要的次语言。总共有四个:

(1)C

C++仍是以C为基础。

(2)Object-Oriented C++
classes(类)(包括构造函数和析构函数),encapsulation(封装),inheritance(继承), polymorphism(多态),virtual functions (dynamic binding)(虚拟函数(动态绑定))等。

(3)Template C++

这是C++的generic programming(泛型编程)部分

(4)STL

STL 是一个 template library(模板库)。

C++并不是一个带有一组守则的一体语言:它是四个次语言组成的联邦政府,每个次语言都有自己的规约。记住这四个次语言你就会发现C++容易了解多了。

请记住:C++高效编程守则视状况而变化,取决于你使用C++的哪一部分。

-------------------------------------------------------------------------------------------------------------------

 

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

该条款最好称为:“尽量用编译器而不用预处理”,因为#define不被视为语言的一部分。

#define ASPECT_RATIO 1.653

编译器永远也看不到ASPECT_RATIO这个符号名,因为在源码进入编译器之前,它会被预处理程序去掉,于是ASPECT_RATIO不会加入到符号列表中。

解决的办法是:不用预处理宏,定义一个常量:

const double AspectRatio = 1.653;    //大写名称通常用于宏,所以这里改变名称写法

说明:

(1)作为语言常量,AspectRatio会被编译器看到,记入符号表。

(2)使用常量可能比使用#define导致较小量的码。因为预处理器盲目地用1.653置换ASPECT_RATIO导致目标代码中存在多个1.653的拷贝。如果使用常量AspectRatio,就不会产生多于一个的拷贝。

 

要把常量限制在类中,首先要使它成为类的成员;为了保证常量最多只有一份拷贝,还要把它定义为静态成员:

class GamePlayer 
{
private:
    static const int NUM_TURNS = 5;            // constant declaration 
    int scores[NUM_TURNS];                    // use of constant
    ...
};

说明:

(1)语句是NUM_TURNS的声明,而不是定义。

(2)C++要求对所使用的任何东西提供一个定义式。如果它是class专属常量且为static整数类型(例如:ints,chars,bools等),只要不取它们的地址可以只声明并使用而无须定义。

(3)NUM_TURNS的定义如下:

const int GamePlayer::NUM_TURNS;

由于class常量已在声明时获得初值,因此定义时可不设初值。

(4)没有办法使用#define来创建一个类属常量,因为#defines不考虑作用域。一旦宏被定义,它就在其后编译过程中有效(除非后面某处存在#undefed)。

 

旧一点的编译器认为类的静态成员在声明时定义初始值是非法的。可以在定义赋值:

class EngineeringConstants                // header file
{ 
private:        
    static const double FUDGE_FACTOR;
    ...
};
// this goes in the class implementation file
const double EngineeringConstants::FUDGE_FACTOR = 1.35;

如果在编译器需要FUDGE_FACTOR的值例如,作为数组维数,是不行的。因为编译器必须在编译器间知道数组的大小。可以用enum解决:

class GamePlayer 
{
private:
    enum { NUM_TURNS = 5 };                    
    int scores[NUM_TURNS];
};

说明:取一个enum地址是非法的。

 

一个普遍的#define指令的用法是用它来实现那些看起来像函数而又不会导致函数调用的宏。

#define max(a,b) ((a) > (b) ? (a) : (b))

注意:写宏时要对每个参数都要加上括号,否则会造成调用麻烦。但也会造成下面的错误:

int a = 5, b = 0;
max(++a, b);                // a 的值增加了2次
max(++a, b+10);                // a 的值只增加了1次

你可以用普通函数实现宏的效率,再加上可预计的行为和类型安全。

template<typename T>
inline const T& max(const T& a, const T& b)
{ 
    return a > b ? a : b; 
}

请记住:

(1)对于单纯常量,最好以const或enums替换#defines。

(2)对于形似函数的宏,最好改用inline函数替换#defines。

-------------------------------------------------------------------------------------------------------------------

 

条款03:尽可能使用const

const允许你告诉编译器某值保持不变,并获得编译器帮助,确保这条约束不被违反。在classes的外部,可以将它用于 global(全局)或namespace(命名空间)范围的 constants(常量),或修饰文件、函数、或区块作用域中被声明为static对象。修饰classes内部的static和non-static成员。修饰指针自身,指针所指物。

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

说明:当指针指向的内容为常量时,const放在类型之前和类型之后意义相同。

void f1(const Widget *pw); // f1 takes a pointer to a constant Widget object
void f2(Widget const *pw); // so does f2

对于STL迭代器来说:

const std::vector<int>:: iterator iter = vec.begin();    // iter acts like a T* const
*iter = 10;                                                // OK, changes what iter points to
++iter;                                                    // error! iter is const
std::vector<int>:: const_iterator cIter = vec.begin();    // cIter acts like a const T*    
*cIter = 10;                                            // error! *cIter is const
++cIter;                                                // fine, changes cIter

 

const 成员函数

将const实施于成员函数的目的是确认该成员函数可作用于const对象身上。两个函数如果只是常量性不同,可以被重载。

class TextBlock {
public:
    ...
    const char& operator[] (std::size_t position) const // operator[] for const objects
    { return text[position]; } 
    char& operator[] (std::size_t position)                // operator[] for non-const objects
    { return text[position]; } 
private:
    std::string text;
};

TextBlock tb("Hello");
const TextBlock ctb("World");
std::cout << tb[0]; // fine — reading a non-const TextBlock
tb[0] = 'x';        // fine — writing a non-const TextBlock
std::cout << ctb[0]; // fine — reading a const TextBlock
ctb[0] = 'x';        // error! — writing a const TextBlock

为了避免重复,可以利用转型修改代码。根据const版本的operator[]实现其non-const版本。

class TextBlock {
public:
    ...
        const char& operator[](std::size_t position) const 
    {
        ...
            ...
            ...
            return text[position];
    }
    char& operator[](std::size_t position)        // now just calls const op[]
    {
        return
            const_cast<char&>(                    // cast away const on
                                                // op[]'s return type;
            static_cast<const TextBlock&>(*this) // add const to *this's type;
            [position]                            // call const version of op[]
        );
    }
    ...
};

注意:令const 版本调用non-const版本来避免重复是错误的。一个 const成员函数承诺绝不会改变它的逻辑状态,但是一个non-const成员函数不会做这样的承诺。从一个const成员函数调用一个non-const成员函数,将面临承诺不会变化的对象被改变的风险。即const成员函数调用non-const成员函数是一种错误行为。

请记住:

(1)将某些东西声明为const可帮助编译器侦测出错误用法const可被施加于任何作用域内的对象、函数参数、函数返回类型、成员函数本体。

(2)当const和non-const成员函数有着实质等价的实现时,令non-const版本调用const版本可避免代码重复。

-------------------------------------------------------------------------------------------------------------------

 

条款04:确定对象被使用前已先被初始化

读取一个未初始化的值会引起未定义行为。因此,永远在使用对象之前先将它初始化。对于无任何成员的内置类型,你必须手工完成此事。

int x = 0;                                // manual initialization of an int
const char * text = "A C-style string"; // manual initialization of a pointer 
double d;                                 
std::cin >> d;                    // "initialization" by reading from an input stream

对于内置类型以外的东西,由构造函数初始化,确保将对象的每一个成员都初始化。重要的是不要把赋值和初始化混淆

class PhoneNumber { ... };
class ABEntry {                                    // 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 theAddress;
    std::list<PhoneNumber> thePhones;
    int num TimesConsulted;
};
ABEntry::ABEntry(const std::string& name, const std::string& address,
    const std::list<PhoneNumber>& phones)
{
    theName = name;                    // these are all assignments,
    theAddress = address;            // not initializations
    thePhones = phones;
    numTimesConsulted = 0;
}

这样做虽然使得ABEntry对象具有了你所期待的值,但不是最好的做法。C++规定对象的成员变量的初始化动作发生在进入构造函数本体之前。效率较高的写法是:

ABEntry::ABEntry(const std::string& name, const std::string& address,
    const std::list<PhoneNumber>& phones)
    : theName(name) ,
    theAddress(address) ,            // these are now all initializations
    thePhones(phones) ,
    numTimesConsulted(0)
{}                                    // the ctor body is now empty

说明:

(1)版本1首先调用default构造函数为theName,theAddress和thePhones设初值,然后立即再对它们赋予新值。default构造函数的一切作为因此浪费了。

(2)版本2的成员初始化列表的做法避免了这个问题。初始化列表中针对各个成员变量而设的实参,被拿去作为各成员变量构造函数的实参。theName以name为初值进行拷贝构造,theAddress以address为初值进行拷贝构造, thePhones以phones为初值进行拷贝构造。

(3)对于大多数类型来说,只调用一次拷贝构造函数的效率比先调用一次default构造函数再调用一次copy assignment operator(拷贝赋值运算符)的效率要高(有时会高很多)。

(4)对于内置类型对象如numTimesConsulted,其初始化和赋值成本相同,但为了一致性最好也通过成员初始化列表来初始化。

(5)当想要构造一个default构造函数时,也可以使用成员初始化列表。

ABEntry::ABEntry()
    :theName() ,                // call theName's default ctor;
    theAddress() ,                // do the same for theAddress;
    thePhones() ,                // and for thePhones;
    numTimesConsulted(0)        // but explicitly initialize
{}

 当然,编译器会为用户自定义类型成员变量自动调用default构造函数,如果那些成员变量没有在成员初始化列表中被指定初值。

(6)如果成员变量是const或引用,即使内置类型也一定需要初值,不能被赋值。

(7)C++有固定的成员初始化次序:基类早于派生类,class成员变量总是以其声明次序被初始化。

 

static对象初始化问题:

所谓static对象,其寿命从构造出来直到程序结束为止。包括:global对象、定义于namespace作用域内的对象、在class内、函数内、以及在file作用域内被声明为static的对象。函数内的static对象称为local static对象,其他static对象称为non-local static对象。程序结束时static对象会被自动销毁,它们的析构函数会在main()结束时被自动调用。

问题是:如果某编译单元内的某个non-local static对象的初始化动作使用了令一编译单元内的某个non-local static对象,它所用到的这个对象可能尚未被初始化。C++对“定义于不同编译单元内的non-local static对象”的初始化次序并无明确要求

改进方法:将每个non-local static对象搬到static函数中。C++保证,函数内的local static对象会在“该函数被调用期间”首次遇上该对象之定义式时被初始化

class FileSystem { ... };                // as before
FileSystem& tfs()                        // this replaces the tfs object; it could be
{                                        // static in the FileSystem class
    static FileSystem fs;                // define and initialize a local static object
    return fs;                            // return a reference to it
}
class Directory { ... };                // as before
Directory::Directory(params)            // as before, except references to tfs are
{                                        // now to tfs()
    ...
        std::size_t disks = tfs().numDisks();
    ...
}
Directory& tempDir()                    // this replaces the tempDir object; it
{                                        // could be static in the Directory class
    static Directory td;                // define/initialize local static object
    return td;                            // return reference to it
}

请记住:

(1)为内置类型对象进行手工初始化,因为C++不保证初始化它们。

(2)构造函数最好使用初始化列表,而不要在构造函数体内使用赋值操作。初始化列表中列出的成员变量排列次序与class中声明次序相同。

(3)为免除“跨编译单元之初始化次序”问题,请以local static对象替换non-local static对象。

-------------------------------------------------------------------------------------------------------------------

posted @ 2015-04-16 09:26  QingLiXueShi  阅读(1931)  评论(2编辑  收藏  举报