C++的55个条款——让自己习惯C++

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/fancynece/article/details/79557179

让自己习惯C++


条款01:将C++看做一个语言联邦

今天的C++已经是个多重范型编程语言,一个同时支持过程形式、面向对象形式、泛型形式、元编程形式的语言。

理解它的最佳方法,就是将C++视为一个由相关语言组成的联邦而非单一语言。在其某个次语言中,各种守则都倾向简单、直观易懂。然而当你从一个次语言移往另一个次语言,守则可能改变。而C++中,共有四个次语言

  • C:说到底C++是以C为基础。区块、语句、预处理器、内置数据类型、数组、指针都来自于C。
  • C with Class:这一部分是面向对象设计之古典法则在C++上的最直接实施。
  • Template C++: 是C++的泛型编程部分。
  • STL:STL是个template程序库,它对容器、迭代器、算法、函数对象的规约有极佳的紧密配合与协调。

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

这个条款也可以说成:尽量以编译器替换预处理器

#define ASPECT_RATIO 1.653            //宏定义

const double AspectRatio = 1.653;     //以常量替换宏定义

在上述代码中,预处理器在编译器之前将ASPECT_RATIO全部替换为1.653,因此ASPECT_RATIO这个名字编译器无法找到,没有进入记号表内。因此若是宏被定义在一个非我们自己所写头文件内,追踪它也会花上很长的时间。

当我们以常量替换宏定义时,由于AspectRatio是一个常量,一定会被编译器看到,也会进入记号表内。

当我们以常量替换#define时,有两种特殊情况:

① 定义常量指针。一般情况下,常量定义需要放在头文件内,以便被不同的源码含入。因此必须要将指针定义为常量指针才安全。

const char* const s = "Hello,fancy!";

const string s("Hello,fancy!");     //一般情况下,使用const修饰string对象比char指针更好。

② 定义class专属常量。若一个常量是某个类专属的,则它要声明为类的成员,并且多个类共有一个即可,因此将它声明为static。

class GamePlayer{
private:
    static const int num = 5;   //类的专属常量声明式
    int scores[num];
};

需要注意的是,上式仅仅是常量num的声明式而非定义式。在C++中,要求我们对所使用的任何东西提供一个定义式,但在这里是个例外。如果我们定义了一个整数类型(int bool char)的类专属常量,只要我们不取它们的地址,则无需定义。否则,应当在相应的.cpp文件中给出如下定义式。

const int GamePlayer::num;   //由于已经赋值,因此不能再次赋值

旧式编译器不允许const修饰的常量在声明时获得初值,倘若我们必须要如此,如类GamePlayer中,需要num来初始化数组scores,此时可以用enum来代替。因为一个属于枚举类型的数值可以当做int被使用

class GamePlayer{
private:
    enum { num = 5 };   //令num成为5的记号名称
    int scores[num];
};

enum的行为比较像#define而不是const。#define和enum不会为常量分配内存,因此无法获取常量的地址,因而不能被指针或引用绑定。而一般情况下,const也不会为常量分配内存,但如果有指针和引用绑定const对象时,会为它分配内存。

因此,若想常量被指针或引用绑定,则用const;若禁止常量被指针或引用绑定,则用enum

另一个常见的#define误用是形似函数的宏。

#define MAX(a,b) f((a) > (b) ? (a) : (b))        //所有实参都要加上小括号

int a = 5,b = 0;
MAX(++a,b);        //a自增两次
MAX(++a,b + 10);  //a自增一次

由上述代码可见,对形似函数的宏的调用常常会得到错误的结果。此时我们可以用inline函数替代#define。

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

条款总结:

  • 对于单纯常量,最好以const(可被指针或引用绑定)或enum代替#define
  • 对于形似函数的宏,最好改用inline函数代替#define

条款03:尽可能使用const

只要某个值需要保持不变,我们就应该用const来修饰它。

迭代器与const

STL迭代器是以指针为根据塑造出来的。我们可以将迭代器理解为一个 T* 的指针。声明迭代器为const就像声明指针为const一样,即T* const,此时迭代器只可绑定固定对象,而对象内容可修改。当我们禁止迭代器指向内容修改时,应该用只读迭代器const_iterator,即const T*

vector<int> vec;

const vector<int>::iterator it1 = vec.begin();   //it1为常量迭代器
vector<int>::iterator const it2 = vec.begin();   //it2为常量迭代器
*it1 = 10;    //正确
++it1;       //错误

vector<int>::const_iterator it3 = vec.begin();  //it3为只读迭代器
*it3 = 10;   //错误
++it3;       //正确
函数与const

const最具威力的用法是在函数上的应用。在一个函数的声明式中,const可以和函数返回值、各参数、函数自身产生关联。

函数返回值、形参与const

令函数返回值为const,可以降低用户程序员书写代码时的错误。

class Rational { …… };
const Rational operator*(const Rational& lhs,const Rational& rhs);

if( a * b = c );     //想要比较却输入错误

如上述代码所示,我们将乘运算符的返回值以const修饰,因为想要避免不合法的应用,如if( a * b = c),若返回值不以const修饰,将会对其赋值产生很大错误。

对于函数形参而言,我们应该把所有不需要被改变的形参都声明为const。

const成员函数

当我们需要成员函数处理const对象时,便要用const修饰成员函数。我们之前已经了解到,只有类的const成员函数可以被类的常量对象调用。const成员函数非常重要,因为它可以指出哪个函数可以更改对象哪个函数不可以,而且使操作const对象成为可能。

对于const成员函数是什么样的函数,有bitwise constness(物理常量) 和 logical constness(逻辑常量)两种概念 。

bitwise const认为,只有当 成员函数不会更改对象的任何成员变量 时才是const,这也是编译器对常量性的定义。

class CTextBlock{
public:
    char& operator[](size_t position) const           //bitwise const成员函数
    {
        return pText[position];
    }
private:
    char* pText;
};

const CTextBlock ctb("Hello");
char *p = ctb[0];
*p = 'J';

但是,如上述代码所示,当我们有一个指针成员变量时,更改指针所指向内存的内容,不改变指针,编译器不会报错。operator[]函数声明为const函数,函数体内部没有改变成员变量的值,但它的返回类型是char&,返回pText所指向的某个单元,由使用代码可以更改其指向内存的内容。也就是说我们创建了常量对象ctb,并且调用了它的常函数,但最终依然改变了ctb。

这种情况导出了logical const,它认为一个 const成员函数可以修改它所处理的对象内的某些bits ,但只有在客户端侦测不出的情况下才能这样。

class CTextBlock{
public:
    ...
    size_t length() const;
private:
    char* pText;
    size_t textLength;
    bool lengthIsValid;      //文本块长度是否有效
};

size_t CTextBlock::length() const{
    if(!lengthIsValid)
    {
        textLength = strlen(pText);      //错误,不能给它们赋值
        lengthIsValid = true;
    }
    return textLength;
}

如上述代码所示,我们想要获取一个常量文本块对象的长度,将length()声明为const成员函数,而在当前文本块长度无效的情况下,又不可更改成员变量lenthIsValid和textLength。解决这个问题的办法是用mutable(可变的)关键字,释放掉对非静态成员的物理常量约束。

class CTextBlock{
public:
    ...
    size_t length() const;
private:
    char* pText;
    mutable size_t textLength;
    mutable bool lengthIsValid;      //可变的
};

size_t CTextBlock::length() const{
    if(!lengthIsValid)
    {
        textLength = strlen(pText);      //正确
        lengthIsValid = true;
    }
    return textLength;
}
在const和非const成员函数中避免重复

举个例子,假设TextBlock内的operator[] 需要执行边界检验、记录访问信息、数据完善性检验等,将这些所有同时放入const成员函数和非const成员函数中,会有大量的重复操作。

class TextBlock{
public:
    ...
    const char& operator[](size_t position) const
    {
        //边界检验
        //记录访问信息
        //数据完善性检验
        return text[position];
    }
    char& operator[](size_t position)
    {
        //边界检验
        //记录访问信息
        //数据完善性检验
        return text[position];
    }
private:
    string text;
};

当我们看到这些重复代码时,自然会想到,新建一个private函数,将共同操作放入函数内部,再调用该函数。但是函数调用和返回语句也出现了重复。

正确的做法是,实现一个operator[]一次,令其中一个调用另外一个,这促使我们将常量性转除。上述代码中,const成员函数做了非const成员函数该做的一切,除了返回值是个常量。因此我们可以在非const成员函数中调用const成员函数。

class TextBlock{
public:
    ...
    const char& operator[](size_t position) const
    {
        //边界检验
        //记录访问信息
        //数据完善性检验
        return text[position];
    }
    char& operator[](size_t position)
    {
        return const_cast<char&>(                //3.将operator[]返回值的const转除
            static_cast<const TextBlock&>(*this) //1.令*this转换为const
                [position]);                     //2.调用const operator[]  
    }
private:
    string text;
};

在上述代码中,我们使用了两次显示类型转换,第一次是使用static_cast将*this转换为const *this,以使非const函数不会递归调用自身。第二次是使用const_cast将const成员函数的返回类型转换为char&(也就是转除const)。

条款总结:

  • 将某些东西声明为const可帮助编译器侦测出错误用法。
  • 编译器强制实施bitwise const,但编写程序时我们应使用logical const。
  • 当const成员函数和非const成员函数有等价的实现时,令非const成员函数调用const。

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

我们知道,对象被定义才可以被使用。但是在一些情况下,对象会被默认初始化;而在另一些情况下, 对象不一定被默认初始化。一般而言,C++中属于C的部分不一定会被默认初始化,不属于C的部分会被默认初始化。比如数组不会默认初始化,而vector会默认初始化。

最好的处理方法就是:在对象使用之前先将它初始化。初始化内置类型时,手工完成;初始化类类型时,由构造函数完成,确保构造函数将每一个成员变量初始化。重要的是不要混淆赋值和初始化

class PhoneNumber{...};
class ABEntry{
public:
    ABEntry(const string& name,const string address,
            const list<PhoneNumber>& phones);
private:
    string theName;
    string theAddress;
    list<PhoneNumber> thePhone;
    int numTimeConsulted;
};

ABEntry::ABEntry(const string& name,const string address,
                const list<PhoneNumber>& phones){
    theName = name;
    theAddress = address;
    thePhone = phones;
    numTimeCousulted = 0;
}

在上述代码中,构造函数的行为是赋值行为,而不是初始化行为,我们不建议这样写。它要先调用成员变量的默认构造函数进行初始化,再调用成员变量的拷贝赋值运算符进行赋值,因此效率很低。

ABEntry::ABEntry(const string& name,const string address,
                const list<PhoneNumber>& phones):
                    theName (name),theAddress(address),
                    thePhone(phones),numTimeCousulted(0){}

这才是构造函数的正确写法,对成员的初始化应放在函数体外执行,对每个成员变量调用其拷贝构造函数进行初始化。而对于const及引用成员,必须使用构造函数进行初始化,不能赋值。总而言之,我们最好在构造函数初始值列表中列出所有成员。

非局部静态变量的初始化次序

所谓static对象,它从被构造出来一直到程序结束才被销毁。局部静态对象是指函数内的static对象,非局部静态对象包括global对象、namespace内的对象、在class内、在file内被声明为static的对象。

C++对定义于不同编译单元的非局部静态变量没有确定的初始化次序。因此,我们要尽量使用局部static变量和类内static变量,避免其他的非局部静态变量。

条款总结:

  • 为内置类型手动初始化,因为C++不保证初始化它们。
  • 为类类型编写构造函数,将所有成员变量按声明次序列入构造函数初始值列表。
  • 尽量使用局部static变量(函数体内的)及类内static变量,避免使用其他非局部静态变量。

猜你喜欢

转载自blog.csdn.net/fancynece/article/details/79557179