• C++编码优化(1):条款1~4


     "不要烟火不要星光,只要问问你内心的想法。"


            本栏仅仅 由对《Effictive C++》其中的一系列条款,掺杂着自己一些愚钝的理解而写的。

    ---前言

     条款01:

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

    在谈及上述好几个关键字 与define宏定义的关系,我们不得不谈谈他们各自 在编码时的使用情况。

    (1)小试牛刀地回顾

    ①const 与 enum 

    1. #include
    2. using namespace std;
    3. enum Color
    4. {
    5. Red,
    6. Blow,
    7. Black
    8. };
    9. int main()
    10. {
    11. const int a = 10;
    12. cout << "a:" << a << endl;
    13. cout << "Red:" << Red << endl;
    14. //Red = 3;
    15. //a = 3;
    16. return 0;
    17. }

     ②inline 与 define

    上述的enum、const 或许我们都在C中经常看到。 inline是个什么东西呢?

    inline是一种“用于实现”的关键字,而不是一种“用于声明”的关键字。

    想想一个场景:

            一个函数其代码量短小,但是很多地方都会调用,也就是会被反复调用的函数,必然带来的是 main函数为其 反复开辟、销毁的栈帧损耗。 那么如何避免开辟栈帧了? 

            在C中提供了一种宏函数调用的方式!

    1. int Add(int x, int y)
    2. {
    3. return x + y;
    4. }
    5. #define ADD(x,y) ((x)+(y))
    6. int main()
    7. {
    8. int x = 4;
    9. int y = 5;
    10. cout << "普通函数加法的总和为:" << Add(1, 2) << endl;
    11. cout << "宏函数加法的总和为:" << ADD(1, 2) << endl;
    12. return 0;
    13. }

    它们的结果完完全全一样的,但是宏函数可以避免 栈空间反复的 开辟、销毁。 

     那么 宏函数就可以肆无忌惮地乱使用了吗? 答案是肯定不是! 否则也不会有inline

    宏函数的缺陷 

    1.没有检查类型

    2.不支持调试

    3.使用复杂 

    当然前两个也无需多说。宏函数不支持调试的根本原因就在于,宏函数在预处理完之后已经完成了文本替换。

    其次,你也可以明显察觉到 当上述代码定义宏函数的时候 需要 加上很多括号。

    举个简单的例子:

    也许C++正是看到了define宏函数存在的缺点,因此设计了一个新的关键字 inline;

    并且还 不如像使用宏函数那么拘谨! 就当做一个普通的函数即可!

     (2)开胃菜

    ①错误信息与数据冗余

    上面的一节,仅仅做了一些要理解本条款的预备知识。

    书上也说,老师也说,预处理阶段要经历的几个步骤; 但是没什么感性的认识!我们怎么知道这些是什么个东东???

    去注释、宏替换、条件编译、头文件展开。

      

    尽管const、enum、宏定义 都作为一个语言常量! 都保障其数值的 常量性。

    但:

     ①如果 这个宏定义的数值(DEBUG) 在程序运行的 获得一个错误信息却得到一个"2"! 而非const 定义下的 DEBUG。如果这份代码不是你写的! 你肯定对这个数字"2"的来源无从下手! 因为其根本没有进入 记号表。

     ②此外,编译器看到 #define DEBUG 只会盲目地进行替换(它可不会太智能)。从而导致一份代码里 出现多份"2".相反 如果改用const int DEBUG 则只需要有一份记录即可。

    ②封装性

    C++引入类这个概念,对封装的要求 一定比C语言高得多。

    #define 与 const 

    注:

    枚举与const:

     

    上述不懂? 那就记住!

    1. 对于单纯常量,最好以const对象 或者 enums替换 define。

    2.形似函数的宏,最好用inline 替代。


    条款03:

    尽可能使用const 

    (1)小试牛刀地回顾

    const是一个非常神奇 且用途十分广泛的关键字。它既可以告诉 编译器,你是会否应该让某个变量保持不变,也是告诉程序员,哪个变量由const 修饰 是不变的!

    恐怕各位初学const的时候 尤其是到指针那章节! 一定会被这个const 弄得晕头转向。

    1. char greetring[] = "Hello"; //non-const pointer,non-const data
    2. char* p1 = greetring; //non-const pointer,const data
    3. const char* p2 = greetring; //const pointer,non-const data
    4. char* const p3 = greetring; //non-const pointer,const data
    5. const char* const p4 = greetring; //const pointer,const data
    6. ---样例取自《Effective C++》

    分清 const 到底是修饰的是 指针自身 还是指针所指向的物

            就是区分 const是在 " * " 的左边还是右边;

     (2)开胃菜

    ①const 与 迭代器 

     在C++中,STL大量使用迭代器(类似于指针一样,但不是真的指针)。

    可能接触过STL容器的,一定更为头疼里面const的 “漫天使用”。

    1. #include
    2. #include
    3. using namespace std;
    4. int main()
    5. {
    6. vector<int> v1;
    7. v1.push_back(1);
    8. vector<int> v2;
    9. v1.push_back(1);
    10. //1.如果你希望得到一个迭代器
    11. //指向的对象是 可以被修改 但是自己不能指向不同的东西
    12. //你就需要一个 T* const
    13. vector<int>::iterator it = v1.begin();
    14. cout << ++(*it) << endl;
    15. //2.如果你希望得到一个迭代器
    16. // 指向的对象是 不能被修改的!
    17. //你就需要 const T*
    18. vector<int>::const_iterator cit = v1.cbegin(); //v1.begin()
    19. cout << ++(*cit) << endl;
    20. return 0;
    21. }

    注:

    const迭代器 并不是给 迭代器+const!

    ②const 与 函数返回值

    有些函数 的声明 会让函数 就返回一个const 对象! 但是可能 有人会觉得多此一举。 

    多的也不用多说了。 很多时候 我们或许将“ == ” 写成了 " = " 很明显,如果你对该对象施加了const 那么很容易 编译器就会给我们立马 找出错误点! 而不是让我们盯着眼花缭乱的 代码胡乱翻找。 

    ③const对象、非const对象 与 成员函数 

    我们先来看看以下代码;

    1. #include
    2. #include
    3. using namepace std;
    4. class TextBlock
    5. {
    6. public:
    7. TextBlock(const char* str)
    8. :_text(str)
    9. {}
    10. //...
    11. const char& operator[](size_t pos)const //这两个是operator 函数重载
    12. {
    13. return _text[pos];
    14. }
    15. char& operator[](size_t pos)
    16. {
    17. return _text[pos];
    18. }
    19. private:
    20. string _text;
    21. };
    22. int main()
    23. {
    24. TextBlock tb("Hello");
    25. cout << tb[1] << endl;
    26. const TextBlock ctb("World");
    27. cout << ctb[1] << endl;
    28. return 0;
    29. }

            上述的境况 仅仅是operator[] 的返回类型以致,"由const 版 之 operator[] 返回的" const char& 进行赋值动作。

            同样,对于non-const成员 可以调用const成员函数 ,这显然是可以进容忍的! 上述问题的根本在于,const成员 去调用了 non-const成员函数。因此,const成员函数 

    当然! operator[]的返回值 一定是char& (reference to char) 而非 'char' (这样就变成了右值),否则 也无法通过编译。

    ④如何定义成员函数 const属性? 

            1.对于成员变量的const属性 那即是 —— 不能修改const修饰的成员变量,反之 non-const的成员变量 可以轻易地更改。

            2.因此一派(bitwise const)认为: 成员函数的const属性 也和成员变量的const属性一样。

    不能对 const成员函数里的 任何成员变量做任何修改! 显然,这很符合 毒地const常良性的定义。

    但事实是这样吗? 我们看看下面代码;

    1. #include
    2. using namespace std;
    3. class TextBlock
    4. {
    5. public:
    6. TextBlock(char* str)
    7. :_ptext(str)
    8. {}
    9. //...
    10. char& operator[](size_t pos) const
    11. {
    12. return _ptext[pos];
    13. }
    14. void Print()const
    15. {
    16. cout << _ptext << endl;
    17. }
    18. private:
    19. char* _ptext;
    20. };
    21. int main()
    22. {
    23. char ch[] = "hello";
    24. const TextBlock ctb(ch);
    25. char* pc = &ctb[0];
    26. cout << "更改前-----------" << endl;
    27. ctb.Print();
    28. *pc = 'W';
    29. cout << "更改后-----------" << endl;
    30. ctb.Print();
    31. return 0;
    32. }

    不仅如此,难道const成员函数里的 成员变量任何改变都不能容忍吗?

    logical constness就拥护:

    一个const成员函数可以修改它 所处理对象内的某些bit!但 这种情况 只允许出现在 检测不出来才得以如此。

    mutable(C++11 后引入的新宠)

    1. class TextBlock
    2. {
    3. public:
    4. TextBlock(char* str)
    5. :_ptext(str)
    6. {}
    7. //...
    8. size_t lenth()const
    9. {
    10. if (!lenghValid)
    11. {
    12. _textlength = strlen(_ptext);
    13. lenghValid = true;
    14. }
    15. return _textlength;
    16. }
    17. private:
    18. char* _ptext;
    19. mutable size_t _textlength; //释放 bitwise constness
    20. mutable bool lenghValid;
    21. };

    ⑤const 和 non-const避免重复 

    上面对于bitwise constness 一刀切的问题,mutable 可以解决。 但不能解决所有问题。

    回到最初的问题;

    C++11提供了一个新的方法,即:常量性转除(cast away constness);

    然而事实上,使用 转型(casting) 是一个糟糕的想法! 

    但是本例 中,仅仅是为了解决 代码冗余 给人带来的不快。

    1. class TextBlocK
    2. {
    3. public:
    4. const char& operator[](size_t pos)const
    5. {
    6. //..
    7. return text[pos];
    8. }
    9. char& operator[](size_t pos)
    10. {
    11. return //最外层是 去掉const属性
    12. const_cast<char&>(
    13. //加上const 属性 去调用 const版本的 operator[] 更加明确!
    14. static_cast<const TextBlocK&>(*this)[pos] //调用[]
    15. );
    16. }
    17. private:
    18. string text;
    19. };

            忽然,你拍一脑袋! 为什么不能让const const operator[] 版本去 复用 非const operator[]?

            显然! 你肯定不能被容忍这样干! 因为const成员函数 承诺不会改变 其所处理对象的任何状态,非const成员函数 则不会保证这样。

            同样,如果你非要这样干, 那不妨给const对象 const_cast 给它释放掉const 的属性。

    当然 这等于 “脱了裤子 ,打屁”。

            想说的也就是,非const对象 可以 去调用非const成员函数 也可以 去调用 const成员函数,因为 非const对象可以对自己的 状态 选择不做任何修改。

    上述不懂?那就记住!

     1.将某些变量声明为const 可以让编译器帮助你 检测错误。 其次 const可以施加于 作用域的任何对象,函数参数、函数返回的类型、成员函数自己身上。

     2.编译器多半强制实施 bitwise constness,mutable有时可为你提供一种解决方法。

     3.当const 与 non-const成员函数 有着实质性的等价实现时,令non-const成员函数 调用 const成员函数,未尝不是一个值得考虑的选择。


    条款4:

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

    (1)小试牛刀地回顾

    1. class Point
    2. {
    3. int x, y;
    4. //.....
    5. };
    6. int main()
    7. {
    8. Point p;
    9. return 0;
    10. }

    因此,处理这些的最佳办法就是: 永远在使用这些对象、使用这些内置类型之前 进行初始化。 

    (2)开胃菜 

    ①区分 初始化 和 赋值 

    这里提个问题:类的构造函数 是赋值 还是 初始化?

             C++规定,对象的成员变量初始化的动作发生在,进入构造函数本体之前。所以在ABEntry进入构造函数之前 其中等待Name、Address(自定义类型)已经 初始化好了。构造函数调用时,已经是对ABEntry 进行赋值 动作了。  但可以看到, 构造函数对Age(内置类型)就并没有那么友好, 里面是随机值!并没对其进行初始化! 

            对上述ABEntry的最佳的写法时,利用初始化列表(member initialzation list);从而替换 赋值的那些 琐碎的操作。 

       

            显然,第二个版本避免了 由default对它进行重新赋值的多余动作。而那些实参 都被初始化列表拿去作为 实参 进行构造拷贝了。 当然Name \ Address 分别为 name\address的 初值copy。

            当然,也有无参的构造函数(默认构造)。都受便 于编译器对于自定义类型的处理方式:

    ②初始化列表 与 const、reference 

    如果没有缺省参数,我们对于const 或者 reference 应该怎么初始化?  难道任让它们各自飞翔?

            对于内置类型,可能赋值、初始化的 代价不是很大! 但是 例如const 、 reference 它们一定就需要初值,而不是赋值!

             最简单的做法就是,所有成员变量的初始化 都交由初始化列表! "这样做有时候绝对必要,且往往比赋值要高 "

            但是,也有classes 拥有多个构造函数和多个成员变量,那样每个构造函数都 写初始化列表,未免有些 重复、无聊。 因此 ,对于 “初始化 与 赋值等价”的成员而言,也可以 给它们的赋值操作 抽象成一个 函数(通常为 private),往往使得 代码更加美观。 

           构造函数 无非只是一种 “伪初始化”,而不是初始化列表的“真初始化”那样 更可取。

    ③初始化列表 与  static

    (这里讨论的问题,更加倾向于 工程性的情况,可以进行选读)

    static声明的对象,其寿命是随程序的!

    我们往往称 一个 在函数内的static 对象为 local static 其他static对象为 non-local  static 

     如何解决 “定义于不同编译单元内的 non-local static对象 无明确定义次序”?

    简单来说,就是两个源码文件,各自内部都有一个 non-local static 的对象。 如果其中一个源码文件中的 non-local static的初始化 需要借助另外一个文件里的 non-local static对象, 但事实是,你根本不知道 要借助的该对象 是否已经初始化完毕了!  毕竟C++并没有该 明确的次序定义!

    我们来看看下面代码;

    1. class FileSystem
    2. {
    3. public:
    4. //...
    5. size_t numDisks() const; //统计Disks众多成员
    6. //...
    7. };
    8. extern FileSystem tfs; //这是一个全局变量 预备给客户使用的!
    9. class Directory
    10. {
    11. public:
    12. Directory()
    13. {
    14. size_t disks = tfs.numDisks();
    15. }
    16. };
    17. Directory tempDir;

     这样不管是 tempDir依赖tfs  还是之后tempDir被其他依赖。 都会在用其被调用的地方,首先被初始化。并且,不会再经历、调用non-local static 对象的 “仿真函数”,也绝不会引发 构造、析构带来的性能损耗。

    上述不懂?那就记住:
     1.为内置类型对对象进行手工初始化,因为C++不保证初始化他们。

     2.任何类的 成员变量初始化。最好都使用初始化列表。而不要在构造函数内就 进行赋值。并且,初始化列表里的排序,最好与变量声明的次序一致。

     3.为免除"跨编译单元之初始化次序"问题,请以local static 对象 替代 non-local static 对象。


    本篇也就到此为止。 此外,“每个条款的最后模块都也是该本书最后的总结

    希望对读者能有所帮助。感谢你的阅读~

  • 相关阅读:
    Vue-04-axios异步通信
    GitHub 忘记SSH密钥
    设计海报都有哪些好用的软件推荐
    Android学习笔记 75. 单元测试
    MathType需要安装一个较新版本的MT Extra(TrueType)字体解决办法
    【博客455】Linux网桥如何接管attach上来的设备的流量
    谈谈Kafka、ActiveMQ、RabbitMQ、RocketMQ 区别以及高可用实现
    C语言qsort()函数详细解析
    shell脚本入门-编写格式以及执行方式
    【LeetCode】5. Valid Palindrome·有效回文
  • 原文地址:https://blog.csdn.net/RNGWGzZs/article/details/128209106