"不要烟火不要星光,只要问问你内心的想法。"
本栏仅仅 由对《Effictive C++》其中的一系列条款,掺杂着自己一些愚钝的理解而写的。
---前言
尽量以const、enum、inline 替换 #define
在谈及上述好几个关键字 与define宏定义的关系,我们不得不谈谈他们各自 在编码时的使用情况。
- #include
- using namespace std;
-
- enum Color
- {
- Red,
- Blow,
- Black
- };
-
- int main()
- {
- const int a = 10;
- cout << "a:" << a << endl;
- cout << "Red:" << Red << endl;
-
- //Red = 3;
- //a = 3;
-
- return 0;
- }
上述的enum、const 或许我们都在C中经常看到。 inline是个什么东西呢?
inline是一种“用于实现”的关键字,而不是一种“用于声明”的关键字。
想想一个场景:
一个函数其代码量短小,但是很多地方都会调用,也就是会被反复调用的函数,必然带来的是 main函数为其 反复开辟、销毁的栈帧损耗。 那么如何避免开辟栈帧了?
在C中提供了一种宏函数调用的方式!
- int Add(int x, int y)
- {
- return x + y;
- }
-
- #define ADD(x,y) ((x)+(y))
-
- int main()
- {
- int x = 4;
- int y = 5;
- cout << "普通函数加法的总和为:" << Add(1, 2) << endl;
- cout << "宏函数加法的总和为:" << ADD(1, 2) << endl;
- return 0;
- }
它们的结果完完全全一样的,但是宏函数可以避免 栈空间反复的 开辟、销毁。
那么 宏函数就可以肆无忌惮地乱使用了吗? 答案是肯定不是! 否则也不会有inline
1.没有检查类型
2.不支持调试
3.使用复杂
当然前两个也无需多说。宏函数不支持调试的根本原因就在于,宏函数在预处理完之后已经完成了文本替换。
其次,你也可以明显察觉到 当上述代码定义宏函数的时候 需要 加上很多括号。
举个简单的例子:
也许C++正是看到了define宏函数存在的缺点,因此设计了一个新的关键字 inline;
并且还 不如像使用宏函数那么拘谨! 就当做一个普通的函数即可!
上面的一节,仅仅做了一些要理解本条款的预备知识。
书上也说,老师也说,预处理阶段要经历的几个步骤; 但是没什么感性的认识!我们怎么知道这些是什么个东东???
去注释、宏替换、条件编译、头文件展开。
尽管const、enum、宏定义 都作为一个语言常量! 都保障其数值的 常量性。
但:
①如果 这个宏定义的数值(DEBUG) 在程序运行的 获得一个错误信息却得到一个"2"! 而非const 定义下的 DEBUG。如果这份代码不是你写的! 你肯定对这个数字"2"的来源无从下手! 因为其根本没有进入 记号表。
②此外,编译器看到 #define DEBUG 只会盲目地进行替换(它可不会太智能)。从而导致一份代码里 出现多份"2".相反 如果改用const int DEBUG 则只需要有一份记录即可。
C++引入类这个概念,对封装的要求 一定比C语言高得多。
#define 与 const
枚举与const:
上述不懂? 那就记住!
1. 对于单纯常量,最好以const对象 或者 enums替换 define。
2.形似函数的宏,最好用inline 替代。
尽可能使用const
const是一个非常神奇 且用途十分广泛的关键字。它既可以告诉 编译器,你是会否应该让某个变量保持不变,也是告诉程序员,哪个变量由const 修饰 是不变的!
恐怕各位初学const的时候 尤其是到指针那章节! 一定会被这个const 弄得晕头转向。
- char greetring[] = "Hello"; //non-const pointer,non-const data
- char* p1 = greetring; //non-const pointer,const data
- const char* p2 = greetring; //const pointer,non-const data
- char* const p3 = greetring; //non-const pointer,const data
- const char* const p4 = greetring; //const pointer,const data
- ---样例取自《Effective C++》
分清 const 到底是修饰的是 指针自身 还是指针所指向的物
就是区分 const是在 " * " 的左边还是右边;
在C++中,STL大量使用迭代器(类似于指针一样,但不是真的指针)。
可能接触过STL容器的,一定更为头疼里面const的 “漫天使用”。
- #include
- #include
- using namespace std;
- int main()
- {
- vector<int> v1;
- v1.push_back(1);
- vector<int> v2;
- v1.push_back(1);
-
- //1.如果你希望得到一个迭代器
- //指向的对象是 可以被修改 但是自己不能指向不同的东西
- //你就需要一个 T* const
- vector<int>::iterator it = v1.begin();
- cout << ++(*it) << endl;
-
- //2.如果你希望得到一个迭代器
- // 指向的对象是 不能被修改的!
- //你就需要 const T*
- vector<int>::const_iterator cit = v1.cbegin(); //v1.begin()
- cout << ++(*cit) << endl;
-
- return 0;
- }
const迭代器 并不是给 迭代器+const!
有些函数 的声明 会让函数 就返回一个const 对象! 但是可能 有人会觉得多此一举。
多的也不用多说了。 很多时候 我们或许将“ == ” 写成了 " = " 很明显,如果你对该对象施加了const 那么很容易 编译器就会给我们立马 找出错误点! 而不是让我们盯着眼花缭乱的 代码胡乱翻找。
我们先来看看以下代码;
- #include
- #include
- using namepace std;
- class TextBlock
- {
- public:
- TextBlock(const char* str)
- :_text(str)
- {}
- //...
- const char& operator[](size_t pos)const //这两个是operator 函数重载
- {
- return _text[pos];
- }
-
- char& operator[](size_t pos)
- {
- return _text[pos];
- }
- private:
- string _text;
- };
-
- int main()
- {
- TextBlock tb("Hello");
- cout << tb[1] << endl;
-
-
- const TextBlock ctb("World");
- cout << ctb[1] << endl;
-
- return 0;
- }
上述的境况 仅仅是operator[] 的返回类型以致,"由const 版 之 operator[] 返回的" const char& 进行赋值动作。
同样,对于non-const成员 可以调用const成员函数 ,这显然是可以进容忍的! 上述问题的根本在于,const成员 去调用了 non-const成员函数。因此,const成员函数
当然! operator[]的返回值 一定是char& (reference to char) 而非 'char' (这样就变成了右值),否则 也无法通过编译。
1.对于成员变量的const属性 那即是 —— 不能修改const修饰的成员变量,反之 non-const的成员变量 可以轻易地更改。
2.因此一派(bitwise const)认为: 成员函数的const属性 也和成员变量的const属性一样。
不能对 const成员函数里的 任何成员变量做任何修改! 显然,这很符合 毒地const常良性的定义。
但事实是这样吗? 我们看看下面代码;
- #include
- using namespace std;
- class TextBlock
- {
- public:
- TextBlock(char* str)
- :_ptext(str)
- {}
- //...
- char& operator[](size_t pos) const
- {
- return _ptext[pos];
- }
- void Print()const
- {
- cout << _ptext << endl;
- }
- private:
- char* _ptext;
- };
-
- int main()
- {
- char ch[] = "hello";
- const TextBlock ctb(ch);
- char* pc = &ctb[0];
- cout << "更改前-----------" << endl;
- ctb.Print();
- *pc = 'W';
- cout << "更改后-----------" << endl;
- ctb.Print();
- return 0;
- }
不仅如此,难道const成员函数里的 成员变量任何改变都不能容忍吗?
logical constness就拥护:
一个const成员函数可以修改它 所处理对象内的某些bit!但 这种情况 只允许出现在 检测不出来才得以如此。
mutable(C++11 后引入的新宠)
- class TextBlock
- {
- public:
- TextBlock(char* str)
- :_ptext(str)
- {}
- //...
- size_t lenth()const
- {
- if (!lenghValid)
- {
- _textlength = strlen(_ptext);
- lenghValid = true;
- }
-
- return _textlength;
- }
- private:
- char* _ptext;
- mutable size_t _textlength; //释放 bitwise constness
- mutable bool lenghValid;
- };
上面对于bitwise constness 一刀切的问题,mutable 可以解决。 但不能解决所有问题。
回到最初的问题;
C++11提供了一个新的方法,即:常量性转除(cast away constness);
然而事实上,使用 转型(casting) 是一个糟糕的想法!
但是本例 中,仅仅是为了解决 代码冗余 给人带来的不快。
- class TextBlocK
- {
- public:
- const char& operator[](size_t pos)const
- {
- //..
-
- return text[pos];
- }
-
- char& operator[](size_t pos)
- {
- return //最外层是 去掉const属性
- const_cast<char&>(
- //加上const 属性 去调用 const版本的 operator[] 更加明确!
- static_cast<const TextBlocK&>(*this)[pos] //调用[]
- );
- }
- private:
- string text;
- };
忽然,你拍一脑袋! 为什么不能让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成员函数,未尝不是一个值得考虑的选择。
确定对象被使用前 已先被初始化。
- class Point
- {
- int x, y;
- //.....
- };
-
- int main()
- {
- Point p;
- return 0;
- }
因此,处理这些的最佳办法就是: 永远在使用这些对象、使用这些内置类型之前 进行初始化。
这里提个问题:类的构造函数 是赋值 还是 初始化?
C++规定,对象的成员变量初始化的动作发生在,进入构造函数本体之前。所以在ABEntry进入构造函数之前 其中等待Name、Address(自定义类型)已经 初始化好了。构造函数调用时,已经是对ABEntry 进行赋值 动作了。 但可以看到, 构造函数对Age(内置类型)就并没有那么友好, 里面是随机值!并没对其进行初始化!
对上述ABEntry的最佳的写法时,利用初始化列表(member initialzation list);从而替换 赋值的那些 琐碎的操作。
显然,第二个版本避免了 由default对它进行重新赋值的多余动作。而那些实参 都被初始化列表拿去作为 实参 进行构造拷贝了。 当然Name \ Address 分别为 name\address的 初值copy。
当然,也有无参的构造函数(默认构造)。都受便 于编译器对于自定义类型的处理方式:
如果没有缺省参数,我们对于const 或者 reference 应该怎么初始化? 难道任让它们各自飞翔?
对于内置类型,可能赋值、初始化的 代价不是很大! 但是 例如const 、 reference 它们一定就需要初值,而不是赋值!
最简单的做法就是,所有成员变量的初始化 都交由初始化列表! "这样做有时候绝对必要,且往往比赋值要高 "
但是,也有classes 拥有多个构造函数和多个成员变量,那样每个构造函数都 写初始化列表,未免有些 重复、无聊。 因此 ,对于 “初始化 与 赋值等价”的成员而言,也可以 给它们的赋值操作 抽象成一个 函数(通常为 private),往往使得 代码更加美观。
构造函数 无非只是一种 “伪初始化”,而不是初始化列表的“真初始化”那样 更可取。
(这里讨论的问题,更加倾向于 工程性的情况,可以进行选读)
static声明的对象,其寿命是随程序的!
我们往往称 一个 在函数内的static 对象为 local static 其他static对象为 non-local static
简单来说,就是两个源码文件,各自内部都有一个 non-local static 的对象。 如果其中一个源码文件中的 non-local static的初始化 需要借助另外一个文件里的 non-local static对象, 但事实是,你根本不知道 要借助的该对象 是否已经初始化完毕了! 毕竟C++并没有该 明确的次序定义!
我们来看看下面代码;
- class FileSystem
- {
- public:
- //...
- size_t numDisks() const; //统计Disks众多成员
- //...
- };
-
- extern FileSystem tfs; //这是一个全局变量 预备给客户使用的!
-
-
- class Directory
- {
- public:
- Directory()
- {
- size_t disks = tfs.numDisks();
- }
- };
-
- Directory tempDir;
这样不管是 tempDir依赖tfs 还是之后tempDir被其他依赖。 都会在用其被调用的地方,首先被初始化。并且,不会再经历、调用non-local static 对象的 “仿真函数”,也绝不会引发 构造、析构带来的性能损耗。
上述不懂?那就记住:
1.为内置类型对对象进行手工初始化,因为C++不保证初始化他们。2.任何类的 成员变量初始化。最好都使用初始化列表。而不要在构造函数内就 进行赋值。并且,初始化列表里的排序,最好与变量声明的次序一致。
3.为免除"跨编译单元之初始化次序"问题,请以local static 对象 替代 non-local static 对象。
本篇也就到此为止。 此外,“每个条款的最后模块都也是该本书最后的总结”
希望对读者能有所帮助。感谢你的阅读~