《Effective C++》学习笔记(1)

1 让自己习惯 C++

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

将C++视为一个由相关语言组成的联邦而非单一语言。在某个次语言(sublanguage)中,各种守则与通例都倾向简单、直观易懂、并且容易记住。然而当你从一个次语言移往另一个次语言,守则可能改变。

  • C:说到底C++仍是以C为基础。区块,语句,预处理器,内置数据类型,数组,指针统统来自C。
  • Objective-Oriented C++:C with Classes所诉求的。这一部分是面向对象设计之古典守则在C++上的最直接实施。类,封装,继承,多态,virtual函数等等...
  • Template C++:C++的泛型编程部分
  • STL:template程序库。容器(containers),迭代器(iterators),算法(algorithms)以及函数对象(function objects)...

** note: **
C++高效编程守则视状况而改变,取决于你使用C++的哪一部分。


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

C++ 编译过程:预处理 --> 编译 --> 链接
预处理过程扫描源代码,对其进行初步的转换,产生新的源代码提供给编译器。检查包含预处理指令的语句和宏定义,并对源代码进行相应的转换。预处理过程还会删除程序中的注释和多余的空白字符。

“宁可以编译器替换预处理器”。就是尽量少用预处理。

  • 预处理器#define ASPECT_RATIO 1.653将所有出现ASPECT_RATIO的地方替换为1.653,ASPECT_RATIO可能并未进入记号表(symbol table)。因此,当出现错误时报的是1.653而不是ASPECT_RATIO,导致目标定位有问题,问题追踪有困难。如果使用变量,则可轻易地判断。
    此外,盲目地把ASPECT_RATIO替换为1.653可能会在目标码中出现多份1.653,改用常量绝不会出现相同情况。所以尽量定义为常量,const double ASPECT_RATIO = 1.653

  • 如果在数组初始化的时候,编译器需要知道数组的大小,且编译器(错误地)不允许使用“static整数型class常量”进行数组初始化,这时可以使用枚举类型enum来替代define。

class GamePlays{
private:
  static const int NumTurns = 5;      // static整数型class常量
  enum { NumTurns = 5 };             // 枚举
  int scores[NumTurns];
... ...
}
  • 宏看起来像函数,但不会招致函数调用带来的额外开销。如果你想获得高效,建议使用inline内联函数。

有了consts 、enums 和inlines,我们对预处理器(特别是#define) 的需求降低了,但并非完全消除。#include 仍然是必需品,而 #ifdef / #ifndef 也继续扮演控制编译的重要角色。目前还不到预处理器全面引退的时候,但我们要尽量限制预处理器的使用。

** note: **

  1. 对于单纯常量,最好以const对象或enum替换#define。
  2. 对于形似函数的宏,最好改用inline函数替换#define。

条款03:尽可能使用 const

const允许你告诉编译器和其他程序员某值应保持不变,只要“某值”确实是不该被改变的,那就该确实说出来。如果const修饰变量,则表示这个变量不可变;如果const修饰指针,表示指针指向的位置不可改变。

  • 和指针有关的const判断:
  1. 如果关键字const出现在星号左边,表示被指物事常量。const char *pchar const *p两种写法意义一样,都说明所致对象为常量。
  2. 如果关键字const出现在星号右边,表示指针自身是常量。
const char * p = "hello"; // *p的hello不可变, 与char const * p = "hello"等价
char * const p = "hello"; // 表示p的值不可变,即p不能指向其它位置
  • STL迭代器的const:
  1. 声明迭代器为const就像声明指针为const一样(即声明一个T* const指针),表示这个迭代器不得指向不同的东西,但它所指的东西的值可以改变。
  2. 如果想要迭代器所指的东西不可改变(即模拟一个const T*指针),使用const_iterator。
std::vector<int> vec;
const std::vector<int>::iterator iter = vec.begin(); //类似T* const
*iter = 10;  //没问题,改变iter所指物
++iter;      //错误!iter是const
std::vector<int>const_iterator cIter = vec.begin();  //类似const T*
*iter = 10;  //错误,*iter是const
iter++;      //没问题,可以改变iter
  • 令函数返回一个常量值,可以避免意外错误。
    如下代码,错把==写成=,一般程序对*号之后进行赋值会报错,但在自定义操作符面前不会(因为自定义*号后返回的是Rational对象实例的引用,可以拿来赋值,不会报错)。如果*不写成const,则下面的程序完全可以通过,但写成const之后,再对const进行赋值就出现问题了。函数的参数,如果无需改变其值,尽量使用const,这样可以避免函数中错误地将==等于符号误写为=赋值符号,而无法察觉。
class Rational { ... };
const Rational operator* (const Rational& lhs, const Rational& rhs);
...
Rational a, b, c;
if(a * b = c)...  //把==错写成=,比较变成了赋值
  • const作用于成员函数,有两个作用:
  1. 可以知道哪些函数可以改变对象内容,哪些函数不可以。
  2. 改善C++效率,通过pass by reference_to_const(const对象的引用)方式传递对象可改善C++效率。
    下面是常量函数与非常量函数的形式:
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[] */
TextBlock tb("hello");              //non-const 对象
cout<<tb[0]<<endl;    //调用的是non-const TextBlock::operator[]
tb[0] = 'x';          //没问题,写一个non-const对象
const TextBlock cTb("hello");    //const 对象
cout<<cTb[0]<<endl;   //调用的是const TextBlock:operator[]
cTb[0] = 'x';          //错误,写一个const对象

在C++中,只有被声明为const的成员函数才能被一个const类对象调用。

  • 成员函数是const意味着什么?
  1. bitwise const主张const成员函数不可以改变对象内任何non-static成员变量。但一个更改了“指针所指物”的成员函数虽然不能算const,但如果只有指针(而非其所指物)隶属于对象,那么称此函数为bitwise const不会引发编译器异议。
  2. logical const主张成员函数可以修改它所处理的对象内的某些bits,但要在客户端侦测不出的情况下才得如此。
    编译器默认执行bitwise。如果想要在const函数中修改non-static变量,需将变量声明为mutable(可变的)。
class TextBlock{
    private:
        char* pText;
        mutable std::size_t textLength;    // 即使在const成员函数内,
        mutable bool lengthIsValid;        // 这些成员变量也可能会被更改。
    public:
        ...
        std::size_t length() const; 
};
std::size_t TextBlock::length() const{
    if (!lengthIsValid){
        textLength = std::strlen(pText);  //加上mutable修饰后,便可以修改其值
        lengthIsValid = true;
    }
}
  • 避免const和non-const成员函数重复
    如果const和non-const成员函数功能相当、代码重复,编译时间、维护等会是一个大问题,这时就用non-const函数去调用const函数,但不能反过来。这是因为non-const函数可能会改变对象,const函数承诺不改变对象,const调用non-const就不安全了
class TextBlock{
    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 TextBlock&>(*this)[position]);
        } 
};

上面代码进行了两次转型:

  1. 第一次用static_cast来为*this添加const,这使接下来调用operator[ ]时得以调用const版本;
  2. 第二次则是用const_cast从const operator[]的返回值转除const,以符合non-const返回值类型。

** note: **

  1. 将某些东西声明为const可帮助编译器侦测出错误用法。const可被施加于任何作用域内的对象、函数参数、函数返回类型、成员函数本体。
  2. 编译器强制实施bitwise constness,但你编写程序时应该使用“概念上的常量性”;
  3. 当cosnt和non-const成员函数有着实质等价的实现时,令non-const版本调用const版本可避免代码重复。

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

  • 对内置类型(基本类型)手动进行初始化。

  • 内置类型以外的类型,初始化要靠构造函数,要确保每一个构造函数都将对象的每一个成员初始化。
    类的构造函数使用成员初值列(member initialization list),而不是在构造函数中进行赋值操作,这样通常效率更高。因为赋值的版本其实是先进行初始化再进行赋值,而成员初值列版本是直接进行初始化,这对于非内置类型(std::string等)来说显然后者效率更高。对于内置类型,其初始化和赋值的成本相同,但为了一致性最好也通过成员初值列来初始化。对于const和reference类型必须是初始化,赋值操作是不允许的。

  • base classes更早于derived classes被初始化,class的初值列成员变量的排列顺序与其声明顺序相同。

  • “不同编译单元内定义之non-local-static对象”的初始化次序。
    static对象,其寿命从被构造出来直到程序结束为止,包括global对象,定义于namespace作用域内的对象,在classes内、在函数内、以及在file作用域内被声明为static的对象。函数内的static对象被称为local static对象(因为它们对函数而言是local),其他static对象称为non-local static对象。

// FileSystem源文件 class FileSystem{ public: ... std::size_t numDisks() const; };
extern FileSystem tfs;
// Directory源文件,与FileSystem处于不同的编译单元
class Directory{
    public:
        Directory(params);
        ...
};
Directory::Directory(params){
    ...
    //调用未初始化的tfs会出现错误
    std::size_t disks = tfs.numDisks();
}

C++对“定义于不同编译单元内的non-local static对象”的初始化相对次序并无明确定义,因此,为防止A的初始化需要B,但B尚未初始化的错误,将每个non-local static对象搬到自己的专属函数内(该对象在此函数内被声明为static),然后用户调用这些函数,而不直接涉及这些对象。

class FileSystem { ... };
FileSystem& tfs(){
    static FileSystem fs;
    return fs;
}
class Directory { ... };
Directory::Directory(params){
    std::size_t disks = tfs().numberDisks();
}
Directory& tempDir(){
    static Directory td;
    return td;
}

经过上面的处理,将non-local转换了local对象,这样做的原理是:函数内的local static 对象会在"该函数被调用期间","首次遇上该对象之定义式"时被初始化,这样就保证了对象被初始化。使用函数返回的“指向static对象”的reference,而不再使用static对象本身。这样做的好处是不调用函数时,不会产生对象的构造和析构。但对多线程这样的方法会有问题。

** note: **

  1. 为内置对象进行手工初始化,因为C++不保证初始化它们;
  2. 构造函数最好使用成员初始化列表,而不要在构造函数本体内使用赋值操作。初始化列表列出的成员变量,其排列次序应该和它们在类中的声明次序相同;
  3. 为免除“跨编译单元之初始化次序”问题,请以local static对象替换non-local static对象。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容

  • C++运算符重载-下篇 本章内容:1. 运算符重载的概述2. 重载算术运算符3. 重载按位运算符和二元逻辑运算符4...
    Haley_2013阅读 1,435评论 0 49
  • 1. 让自己习惯C++ 条款01:视C++为一个语言联邦 为了更好的理解C++,我们将C++分解为四个主要次语言:...
    Mr希灵阅读 2,792评论 0 13
  • 接着上节 condition_varible ,本节主要介绍future的内容,练习代码地址。本文参考http:/...
    jorion阅读 14,778评论 1 5
  • 前言 把《C++ Primer》[https://book.douban.com/subject/25708312...
    尤汐Yogy阅读 9,512评论 1 51
  • (1) 浣衣池边纤手涤 非往昔佳人 泪落红妆散 (2) 徐徐春风梓木馨 黄昏落日是三金 羡煞众人心! (3) 雨夜...
    剑飞先森阅读 299评论 0 5