Chapter 1

打印 上一主题 下一主题

主题 800|帖子 800|积分 2400

1 让自己习惯C++

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


  • C : C++以C为基础,block、语句、预处理器、内置数据类型、数组、指针都来自于C。当使用C++中的C成分工作时,没有模板(Template)、没有异常(Exceptions)、没有重载(overloading)。
  • Object-Oriented C++ : 也就是 C with classes,classes(包括构造函数和析构函数)、封装(encapsulation)、继承(inheritance)、多态(polymorphism)、virtual函数(动态绑定)......等等。
  • Template C++ : C++的泛型编程部分。
  • STL(Standard Template Library) : 对容器、迭代器、算法以及函数对象对的规约有极佳的紧密配合与协调。
*请记住 : *
​        1. 高效编程守则视状况而变化,取决于你使用C++的哪一部分。
条款 02 尽量以 const,enum,inline 替代 #define
  1. 例 : `#define ASPECT_RATIO 1.653`预处理器会将程序中的`ASPET_RATIO`记号全部替换成数值`1.653`,也就是这个记号不会进入记号表内,这样在程序出错时获取的错误信息会很难分析。
复制代码
​        当我们使用#define 实现宏时,更要小心,必须为宏中的所有实参加上小括号。例 :
  1. #define CALL_WITH_MAX(a,b) f((a) > (b) ? (a) : (b))//一定要加小括号,这里的例子还体现不出什么,注意加括号的位置就行
  2. int a = 5,b = 0;
  3. //当第一个参数(++a)大于第二个参数时,++a会被执行两次,这显然很容易出问题
  4. CALL_WITH_MAX(++a,b);
  5. CALL_WITH_MAX(++a,b+10);
复制代码

解决方法 :

  • 以一个常量替换上述宏。
    const double Aspect_ratio = 1.653; 作为一个语言常量,自然会进入记号表,这样在Debug的时候容易跟踪,且生成的代码量比较小,开支较小,因为宏定义是盲目地替换字段。
    以 const 代替 #define 时要注意两点 :
    ​        (1) 定义常量指针时,要注意顶层 const 和底层 const 的区别。顶层 const 是将 const 放在 '*' 左边,意思是指针指向的常量类型,所以该指针所指向的内容不能被修改;
    ​                 而底层 const 则是将 const 放在 '*' 右边,意思是我这个指针是一个常量指针,只能指向在初始化时的值,不能指向其他变量。
    ​        (2)定义 class 专属常量时,也就是类内 static 成员,例如
    1. class GamePlayer{
    2. private:
    3.     static const int NumTurns = 5;
    4.     int scores[NumTurns];
    5. };
    复制代码
    ​        注意,上例中的的static const int NumTurns = 5;        是声明式而不是定义式。C++要求要对使用的任何东西都要提供一个定义式。
    ​        但如果它是个 class 专属常量,又是 static 且为整数类型(integral type,例如int,char,bool),只要不取指针,则只需要提供声明式,若有些编译器不支持,则需要在实现文件中定义。(注意非整型必须定义,初值可以放在定义式中)
    1. //.h
    2. class CostEstimate{
    3. private:
    4.     static const double FudgeFactor;
    5. };
    6. //.c
    7. const double CostEstimate::FudgeFactor = 1.35;
    复制代码
    ​        另外,#define 不重视作用域(scope),在定义后编译过程中都有效。
  • 使用 enum hack
    这种做法比较像#define,同样不能取地址,同样会导致非必要的内存分配。
  • 对于“实现宏“,用template inline函数,同样是代码替换,但这种做法属于函数操作,一切按函数操作就行了,不用操心参数问题,同时遵守作用域和访问规则。
请记住:
​        1. 对于单纯常量,最好以 const 对象或 enum 替换 #define
2. 对于形似函数的宏(macros),最好改用 inline template 函数替代 #define
条款 03 尽可能使用const

​        顶层 const : 表示 const 修饰的类型为常量,特别地,对于指针类型,const 在 ‘*’ 右边时表示顶层指针,也就是该指针变量为常量,                                                 与该指针变量指向的对象是否为常量无要求。
  1. int i = 0;
  2. int *const p1 = &i;//const 在‘*’右边,表示p1只能指向i(顶层指针)
复制代码
​        底层 const : 与指针和引用类型有关,对于指针类型,当 const 在 ‘*’ 左边(一般写在类型左边)时,表示该指针指向的对象为常量类型,                                                  此时该指针可以指向别的对象,当不能通过指针解引用来更改所指对象的值。
​                                                  而对于引用类型,都是底层 const,也就是所引用的值为常量,但const引用可以绑定常量与非常量,这是一个特殊的例子。
  1. int i = 0;
  2. const int j = 1;
  3. const int &ref_i = i;//可以绑定非常量
  4. const int &ref_j = j;//可以绑定常量类型
  5. const int &ref = 10;//会创建临时变量,然后绑定
复制代码
​        const可以和函数的返回值、参数、函数自身产生关联,尽可能地使用它,可以让编译器帮你更快的发现错误!
const成员函数
​        将 const 作用在成员函数上,可以区分出常量对象和非常量对象所使用的不同版本的成员函数。const成员函数可以保证不修改类的成员变量。
  1. //.h
  2. class TextBlock{
  3. public:
  4.    // ...
  5.     const char& operator[](std::size_t position) const//const对象调用此函数
  6.     { return text[position]; }
  7.     char& operator[](std::size_t position)//非const对象调用此函数
  8.     { return text[position]; }
  9. private:
  10.     std::string text;
  11. };
  12. //.cpp
  13. TextBlock tb("Hello");
  14. std::cout << tb[0];//调用 char& operator[](std::size_t position)
  15. const TextBlock ctb("Hello");
  16. std::cout << ctb[0];//调用 const char& operator[](std::size_t position) const
复制代码
​        在C++中,除了内置类型外,初始化往往由构造函数完成。
​        因此我们强制规定 : 确保每一个构造函数都将对象中的每一个成员初始化。
​        注意不要混淆初始化和赋值这两种概念。
  1. //.h
  2. class CTextBlock{
  3. public:
  4.     //...
  5.     std::size_t length() const;
  6. private:
  7.     char* pText;
  8.     mutable std::size_t textLength;
  9.     mutable bool lengthIsVaild;
  10. };
  11. //.cpp
  12. std::size_t length() const
  13. {
  14.     if(!lengthIsVaild)
  15.     {
  16.         pText = "Hello";//错误,非mutable变量在const成员函数中禁止修改
  17.         textLength = std::strlen(pText);//允许修改
  18.         lengthIsvaild = true;//允许修改
  19.     }
  20.     return textLength;
  21. }
复制代码
​        该构造函数定义中的行为是赋值,因为在进入构造函数定义也就是大括号部分之前所有的成员变量已经被默认构造函数构造了。此时又        重新执行一遍赋值函数,一共两次动作,开销太大。
​        因此
  1. class TextBlock{
  2. public;
  3.     //...
  4.     const char& operator[](std::size_t position) const
  5.         {
  6.                 //...        //边界检查
  7.             //...        //日志数据访问
  8.             //...        //检验数据完整性
  9.             return text[position];
  10.         }
  11.    
  12. /*
  13.         //冗余版本
  14.         char& operator[](std::size_t position)
  15.         {
  16.                 //...        //边界检查
  17.             //...        //日志数据访问
  18.             //...        //检验数据完整性
  19.             return text[position];
  20.         }
  21. */
  22.     char& operator[](std::size_t position)
  23.     {
  24.         return const_cast<char&>(//去除返回值的const属性
  25.                         static_cast<const TextBlock>(*this)[position];//static_cast安全转型为const对象调用const版本,返回值为const char*
  26.         );
  27.     }
  28. private:
  29.     std::string text;
  30. };
复制代码
​        此时对于各个成员,只执行依次构造函数即可完成初始化。
​        另外,初始化的次序应该对应声明的次序。可以避免一些隐晦的错误。
现在我们关注“不同编译单元内定义的non-local static对象”的初始化次序 :

  • 编译单元 : 产出单一目标问价的源码。基本上是 单一源码文件 + 其所加入到的头文件。注意!编译器编译不同单元的次序无法确定!
  • static对象 : 寿命从被构造出来直到程序结束,程序结束时自动调用其析构函数。
  • non-local static对象 : 不在函数内的static对象。
  • local static对象 : 在函数内的static对象。
我们设想一种这样的情况 :
​        在一个编译单元内存在一个对象
  1. int x = 0;
  2. const char* text = "hello world";
  3. double d;
  4. std::cin >> d;
复制代码
​        在另一个编译单元内存在另一个对象
  1. //.h
  2. class PhoneNumber {...};
  3. class ABEntry{
  4. public:
  5.     ABEntry(const std::string& name, const std::string& address,
  6.                    const std::list<PhoneNumber>& phones);
  7. private:
  8.     std::string theName;
  9.     std::string theAddress;
  10.     std::list<PhoneNumber> thePhones;
  11.     int numTimesConsulted;
  12. };
  13. //.cpp
  14. ABEntry::ABEntry(const std::string& name, const std::string& address,
  15.                    const std::list<PhoneNumber>& phones)
  16. {
  17.     //赋值
  18.     theName = name;
  19.     theAddress = address;
  20.     thePhones = phones;
  21.     numTimesConsulted = 0;
  22. }
复制代码
因为不同编译单元编译的次序是不确定的,我们无法保证Directory对象定义之前,类FileSystem已经被编译,这样会出现错误。
解决方法 :
Singleton模式
因为C++保证 : 函数内的local static对象会在“该函数被调用期间”“首次遇上该对象的定义式”时被初始化。
  1. ABEntry::ABEntry(const std::string& name, const std::string& address,
  2.                    const std::list<PhoneNumber>& phones)
  3.         :theName(name),
  4.           theAddress(address),
  5.           thePhones(phones),
  6.           numTimesConsulted(0)
  7. { }
复制代码
因此可以保证调用tfs()时,只有FileSystem的定义式被编译器遇到时才给里面的fs对象初始化并返回。
但内含static对象在多线程系统中线程不安全。
*请记住 : *
​        1. 为内置型对象进行手工初始化,因为C++不保证初始化它们。
​        2. 构造函数最好使用成员初始化列表,而不要在构造函数本体内使用赋值操作,但如果有多个构造函数会产生冗余代码时,可以将具                有确定的默认初始化值的变量赋值封装进一个函数,其余还是使用列表初始化。同时,列表初始化的次序也应该与声明次序相同。
​        3. 为免除"跨编译单元的初始化次序"问题,请以 local static 对象替换 non-local static 对象。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

石小疯

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表