Effective C++学习(1)—— 让自己习惯C++

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

C++是一个多重泛型编程语言,一个同时支持过程形式、面对对象形式、函数形式、泛型形式、元编程形式的语言。为了理解C++,必须认识其主要的次语言。包含四个:

C

C++以C为基础。区块、语句、预处理器、内置数据类型、数组、指针等统统来自C。C语言的局限:没有模板、没有异常、没有重载。。。

Object-Oriented C++

C with Classes所诉求的,classes(包括构造函数和析构函数),封装、继承、多态、虚函数等等。

Templae C++

C++的泛型编程。

STL

STL是个template程序库,容器、迭代器、算法以及函数对象的规约有极佳的配合与协调。

请记住:

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

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

宁可以编译器替换预处理器

#define ASPECT_RATIO 1.653
const double AspectRadio = 1.653;

使用常量可能比使用#define获得导致较小量的代码,因为预处理器“盲目将ASPECT_RATIO替换为1.653”可能导致目标代码出现多份1.653,若改为常量AspectRadio绝不会出现相同情况。

  • 用常量替换#define有两种特殊的情况

第一定义常量指针

const char* const authorName = “Scott Meyers”;
const std::string authorName("Scott Meyers");

第二class的专属常量

class GamePlayer {
private:        
    static const int NumTurns = 5;    //常量声明式,旧式的编译器可能不支持
};

在非头文件中放入如下定义式

const int GamePlayer::NumTurms;

声明时获取了初值,因此定义时不可以再设初值。

不能用#define创建一个class的专属常量,因为#define并不重视作用域。因此#define不能提供任何封装性,没有所谓的private #define这样的东西。

对于旧式编译器不支持在class内初值设定,可改用所谓的“the enum hack”的补偿做法。

class GamePlayer {
private:
    enum { NumTurns = 5 };
    int scores[NumTruns];
};
  • #define误用情况是用它实现宏。

#define CALL_WITH_MAX(a, b)  f((a) > (b) ? (a) : (b))

必须记住宏中的所有实参加上小括号。

int a = 5, b = 0;
CALL_WITH_MAX(++a, b);          //a被累加两次
CALL_WITH_MAX(++a, b+10);       //a被累加一次

在这里,调用f之前,a的递增次数竟然取决与“它被拿来和谁比较”!

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

请记住:

        对于单纯常量,最好以const对象或enum替换#define

        对于形似函数的宏,最好改用inline函数替换#define

条款03:尽可能使用const

可以用它在class外部修饰global或namespace作用域中的常量,或修饰文件、函数、或区块作用域中被声明为static的对象。也可以修改class内部的static和非static成员变量。面对指针可以指出指针自身、指针所指物,或者两者都是const。

const关键字出现在星号左边,表示被指物是常量;如果出现在星号的右边,表示指针自身是常量;如果出现在星号两边,表示被指物和指针两者都是常量。

  • STL迭代器

const std::vector<int>::iterator iter = vec.begin();
*iter = 10;               //没问题,改变iter所指物
++iter;                   //错误!iter是const

std::vector<int>::const_iterator cIter = vec.begin();
*cIter = 10;            //错误!*cIter是const
++cIter;                //没问题,改变cIter
  • const最具威力的用法是对函数声明时的应用

在函数声明式内,const可以和函数返回值、各参数、函数自身(如果是成员函数)。

class Rational { ... };
const Rational operator*(const Rational& lhs, const Rational& rhs);
Rational a,b,c;
if(a*b = c) ...

除非有需要改动参数或者local对象,否则请将它们声明为const。只不过多打了6个字符,却可以省下脑人的错误,像是“想要键入'=='却意外键成'='”的错误。

  • const成员函数

将const实施于成员函数的目的,是为了确认该成员函数可作用于const对象身上,基于两个理由。第一,它们使用class接口比较容易被理解。第二,它们使“操作const对象”成为可能。改善C++程序效率的一个根本办法是以pass by reference-to-const方式传递对象,而此技术可行的前提是,我们有const成员函数可以用来处理取得(并经修饰而成)的const对象。

  • 两个成员函数如果只是常量属性(constness)不同,可以被重载
class TextBlock {
public:
    const char& operator[](std::size_t position) const    //operator[] for const 对象
    { return text[position]; }
    char& operator[](std::size_t position)                //operator[] for non-const 对象
    { return text[position]; }
};

TextBlock的operator[]可以这么使用:

TextBlock tb("Hello");
std::cout << tb[0];
const TextBlock ctb("World");
std::cout << ctb[0];

只要重载operator[]并对不同的版本给予不用的返回类型,就可以令const和non-const获得不同的处理:

tb[0] = 'x';                //没问题 写一个 no-const TextBlock
ctb[0] = 'x';               //错误!写一个const TextBlock
  • bitwise constness 和 logical constness
class TextBlock {
public:
    char& operator[] (std::size_t position) const       //bitwise const声明,但是不适当
    { return pText[position]; }
private:
    char* pText;
};

该函数声明为const成员函数,却返回了一个reference指向对象的内部值。

const TextBlock ctb("Hello");     //声明一个常量对象
char* pc = &ctb[0];               //调用const operator[]获取一个指针,指向ctb的数据
*pc = 'J';                        //ctb现在有了"Jello"这样的内容

这种情况导出所谓的logical constness。

class CTextBlock {
public:
    std::size_t length() const;
private:
    char* pText;
    std::size_t textLength;          //最近一次计算文本区块长度
    bool lengthIsValid;              //目前的长度是否有效
};

std::size_t CTextBlock::length() const
{
    if(!lengthIsValid) {
        textLength = std::strlen(pText);   //错误!在const成员函数内不能赋值给textLength
                                           //和lengthIsValid
        lengthIsValid = true;
    }
    return textLength;
}

解决办法:利用C++的一个关于const相关的摆动场mutable关键字。

class CTextBlock {
public:
    std::size_t length() const;
private:
    char* pText;
    mutable std::size_t textLength;        //这些成员变量可能总是被更改,即使在const成员函数内
    mutable bool lengthIsValid;
};

std::size_t CTextBlock::length() const
{
    if(!lengthIsValid) {
        textLength = std::strlen(pText);   //现在可以这样
        lengthIsValid = true;
    }
    return textLength;
}

mutable是个解决办法,但它不能解决所有const相关问题。比如函数内想做一些边界检查、日志数据访问,数据完整性检验等等。

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;
};

如上,存在代码重复以及伴随的编译时间、维护、代码膨胀等问题。当然,可以将边界检查等代码移到另一个成员函数并且令两个operator[]调用它。但还是重复了一些代码,例如函数调用,两次return语句等。

class TextBlock {
public:
    const char& operator[] (std::size_t position) const
    {
        ...               //边界检查
        ...               //日志数据访问
        ...               //检查数据完整性
        return text[position];
    }
    const char& operator[] (std::size_t position) const
    {
        //将operator[]返回值的const转除,为*this加上const调用const oprerator[]
        return const_cast<char &>(static_cast<const TextBlock &>(*this)operator[position]);
    }
private:
    std::string text;
};

请记住:

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

编译器强制实施bitwise constness,但你编写程序时应该使用“概念上的常量性”。

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

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

永远在使用对象之前先将它初始化。

  • 使用成员初始化列表

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::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;
}

ABEntry::ABEntry(const std::string& name, const std::string& address,
                   const std::list<PhoneNumber>& phones)
    :theName(name),                  //是初始化
     theAddress(address),
     thePhones(phones),
     numTimesConsulted(0)
    
{ }

C++有着十分固定的“成员初始化次序。base classes更早于其derived classed被初始化,而class的成员变量总是以其声明次序被初始化。

  • 不同编译单元内定义之non-local static对象的初始化次序无明确定义。

class FileSystem {
public:
    std::size_t numDisks() const;
};
extern FileSystem tfs;                      //预备给客户使用的对象

class Directory {
public:
    Directory();
};

Directory::Directory(){
    std::size_t disks = tfs.numDisks();     //使用tfs对象
}

Directory tempDir;

除非tfs在tempDir之前初始化,否则tempDir的构造函数会用到尚未初始化的tfs。

解决办法,将每个non-local static对象搬到自己的专属函数内。这是Singleton模式的一个常见的实现手法。

class FileSystem { ... };
FileSystem tfs() {
    static FileSystem fs;
    return fs;
}

class Directory { ... };
Directory::Direcotory {
    ...
    std::size_t disks = tfs().numDisks();
    ...
}

Directory& tempDir() {
    static Directory td;
    return td;
}


请记住:

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

构造函数最好使用成员初始化列表,而不要在构造函数本体内使用赋值操作。初始化列表列出的,成员变量,其排列次序应该和它们在class中的声明次序相同。

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

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值