《Effective c++》读书笔记1

本章一共有四个条款:

  • 视c++为一个语言联邦
  • 尽量以constenuminline替换#define
  • 尽可能使用const
  • 确定对象被使用前已被初始化

视c++为一个语言联邦

  这个条款介绍c++总共由四个次语言部分组成,分别为:

  • C part of c++:这一点表示c++是以c为基础的,其中的区块(blocks)、语句(statements)、数组(arrays)和指针(pointers)都来自c。注意,c语言部分和专属于c++部分的初始化方式不一样。

  • Object-oriented c++: 这一部分即面向对象部分,包括封装、继承、多态和虚函数等等。

  • Template C++ :这是c++的泛型编程部分。

  • STL

尽量以const, enum, inline替换#define

  本条款也可称之为“宁以编译器替换预处理器”。#define与前三者的差别在于,它不被视为语言的一部分,在编译器处理源码之前就由预处理器来处理了,其所定义的名称不记入记号表。

  • 对于单纯常量,最好以const对象或enums替换#define
  • 对于形似函数的宏,最好改用inline函数替换#define

尽可能使用const

  对于关键字const,考虑其对指针的修饰:

const char* p=greeting;      //(1)
char const* p=greeting;      //(2)
char* const p=greeting;      //(3)

如果const出现在星号左边表示*p所指的字符串是常量,即不能对*p重新赋值,如(1)(2)式所示,且(1)(2)式的意义完全一样。如果const出现在星号右边表示指针p是常指针,即不能对p重新赋值使其指向其他的字符串。值得注意的是,在STL中迭代器是以指针为根据塑模出来的,所以迭代器的作用就像一个指针,声明迭代器为const与声明指针为const的含义一样,如果希望迭代器所指的东西为const则需要定义const_iterator,这里非常容易混淆!

  关于const关键词有一个很重要的概念,即const成员函数,并引出另外两个流行概念:bitwise constlogical constness.,其中bitwise const流派主张const成员函数不可以更改对象内任何non-static成员变量,考虑下述代码:

class CTextBlock
{
public:
    std::size_t length() const;             //(1)式

private:
    char* pText;                            //(2)式
    std::size_t textLength;                 //(3)式
    bool lengthIsValid;                     //(4)式
};

std::size_t CTextBlock::length() const
{
    if (!lengthIsValid)
    {
        textLength = std::strlen(pText);
        lengthIsValid = true;
    }
    char* temp= "Hello";
    pText =temp;                            //(5)式
    *pText = *temp;                         //(6)式
    return textLength;
}

其中(1)式的length()被声明为const成员函数,但是在函数的定义中对象的成员变量都被进行了重新赋值,所以上述代码无法通过编译。值得注意的是,(6)式改变了对象的某些bits,因为修改了指针指向的字符串,但是不幸的是它却能通过bitwise const的测试,事实上这就是所谓的logical constness。在有些时候,即使我们声明了const成员函数,我们也希望某些变量可以被重新赋值,可以通过关键字mutable释放掉bitwise constness约束。

class CTextBlock
{
public:
    std::size_t length() const;                     //(1)式

private:
    char* pText;                                    //(2)式
    mutable std::size_t textLength;                 //(3)式
    mutable bool lengthIsValid;                     //(4)式
};

std::size_t CTextBlock::length() const
{
    if (!lengthIsValid)
    {
        textLength = std::strlen(pText);
        lengthIsValid = true;
    }
    return textLength;
}

上述代码就可以正常通过编译。

在某些时候,我们可能在定义了一个const成员函数的同时也需要定义一个对应的non-const的成员函数,两者的功能代码可能会有大部分是重复的。为了避免重复代码,可以通过转型动作使得non-const成员函数调用const成员函数,注意不能反过来操作,因为会破坏const成员函数的bitwise constness约束。

class CTextBlock
{
public:
    const char& operator[](std::size_t position) const
    {
        ...
        ...
        ...
        return text[position];
    }

    char& operator[](std::size_t position)
    {
        return
            const_cast<char&>(static_cast<const CTextBlock&>(*this))[position];   //(1)式
    }
private:
    char* text;
};

其中(1)式经过了两次强制转型动作。

确定对象被使用前已先被初始化

  前面我们说过在c++的c语言部分和非c语言部分的初始化规则并不是相同的,在C part of C++中,如果初始化可能招致运行期成本那么就不保证发生初始化,一旦进入non-C parts of C++,规则就有些变化。这就是为什么array不保证其内容被初始化,而vector确有此保证。针对这种情况,一个保险的情况是:永远在使用对象之前先将它初始化。对于内置类型之外的任何其他东西,初始化责任落在构造函数身上,这里需要注意的是别混淆赋值初始化的区别。

class ABEntry
{
public:
    ABEntry(const std::string& name);

private:
    std::string theName;
};

ABEntry::ABEntry(const std::string& name) :theName(name)   //(成员初值列,初始化)
{
    //theName = name;                                      //(赋值操作)
}

上述代码展示了赋值操作与初始化的区别,c++规定,对象的成员变量的初始化动作发生在进入构造函数本体之前,所以在本例中,如果成员变量theName是以赋值的方式进行“初始化”,那么他实际上进行的是:先调用theName自身的默认构造函数为其设初值,再用name给它赋予新值。而如果直接使用成员初值列的方式对其进行初始化执行的操作是:利用nametheName进行拷贝构造。后一种方法比前一种方法高效很多,所以建议尽可能使用成员初值列的方式进行初始化,值得注意的是:成员变量的初始化顺序只与其声明顺序有关,而与其成员初值列的顺序无关

所谓编译单元是指产出单一目标文件的那些源码,基本上它是单一源码文件加上其所含入的头文件。如果涉及至少两个源码文件,每一个内含至少一个non-local static对象,且某个non-local static对象的初始化使用了另一个编译单元的某个non-local static对象,它所用的这个对象可能尚未被初始化,因为c++对“定义于不同编译单元内的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();
}
/*创建一个Directory对象*/
Directory tempDir();           //(1)式

上述代码中的(1)式,除非tfstempDir之前先被初始化,否则tempDir的构造函数会用到尚未初始化的tfs,但实际上这是无法保证的。解决这个问题的办法是:将每个non-local static对象 搬到自己的专属函数内,这些函数返回一个reference指向它所含的对象,然后用户调用这些函数。

/*第一个源码文件*/
class FileSystem
{
public:
    std::size_t numDisks() const;
};
FileSystem& tfs()
{
    static FileSystem fs;
    return fs;
}
/*第二个源码文件*/
class Directory
{
public:
    Directory();
};
Directory::Directory()
{
    std::size_t disks = tfs().numDisks();
}
Directory& tempDir()
{
    static Directory td;
    return td;
}

如上进行修改,调用的方式由直接使用tfstempDir改为tfs()tempDir()

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

  • 前言 把《C++ Primer》[https://book.douban.com/subject/25708312...
    尤汐Yogy阅读 9,729评论 1 51
  • 1 让自己习惯 C++ 条款01:视 C++ 为一个语言联邦 将C++视为一个由相关语言组成的联邦而非单一语言。在...
    暗夜望月阅读 484评论 0 1
  • 1.C和C++的区别?C++的特性?面向对象编程的好处? 答:c++在c的基础上增添类,C是一个结构化语言,它的重...
    杰伦哎呦哎呦阅读 10,048评论 0 45
  • 1. 让自己习惯C++ 条款01:视C++为一个语言联邦 为了更好的理解C++,我们将C++分解为四个主要次语言:...
    Mr希灵阅读 3,011评论 0 13
  • 01.为什么要写作,写作的目的是什么? 在写作上,我是一个新手。自然刚开始时完全是自嗨型写作,想到那里就写到那里。...
    跑者子牛阅读 181评论 2 6

友情链接更多精彩内容