Effective C++系列笔记1-4

Effective C++系列笔记,第一章,第1-4小节

Posted by CongYu on February 2, 2021

Effective C++

Effective C++ 1-4

Created 2021.02.20 by William Yu; Last modified: 2021.02.21-V1.0.0

Contact: windmillyucong@163.com

Copyleft! 2021 William Yu. Some rights reserved.


References

本文内容:《Effective C++》阅读笔记,总共9个章节,55小节。

Ch1 习惯c++

L1: 将c++看做一个语言联邦

从4个方面去看待c++

c

以c语言为基础。包含了c的特性:

  • 区块 blocks
  • 语句 statements
  • 预处理 preprocessor
  • 内置数据类型
  • 数组
  • 指针

Object-Oriented

简单来讲就是面向对象

  • class
    • 构造函数
    • 析构函数
    • 封装
    • 继承
    • 多态
    • virtual 函数 (动态绑定)

Template

泛型编程

STL

是一个template标准库

  • 容器
  • 迭代器
  • 算法

L2: 少用#define

  • 尽量少用预处理器,多用编译器
  • 使用const, enum, inline 替换 #define
    • 对于常量,使用const, enum替换#define
    • 对于形似函数的宏,使用inline函数替换#define

const、inline

  • For example:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    #define Pi 3.1415926   //bad
    const double Pi = 3.1415926;  //good
      
      
    #define MAX(a,b) f((a)>(b) ? (a):(b)) //bad
    template<typename T>
    inline void MAX(const T& a, const T& b){   //good
        f(a > b ? a : b);
    }
    

类内的静态常量

class专属常量

  • 需求

    • 将常量的作用域限制于class内 -> 定义为类内成员
    • 确保此常量只有一份实体 -> 定义为static成员
1
2
3
4
5
6
  // xxx.h
  class GamePlayer{
    private:
      static const int NumTurns = 5;    // 成员常量声明式
      ...
  }

注意:

  • 上面你只看到了NumTurns的声明式,而非定义式

  • 如果是 class 专属常量,且为static,且为整数类型(int,char,bool),只要不取他们的地址,可以在只声明不提供定义式的情况下使用

  • 但是如果要取地址,就必须另外提供一个定义式:

    1
    2
    
    // xxx.cc
    const int GamePlayer::NumTurns;    //这才是定义式
    
  • 定义式放在实现文件里面,不要放在头文件里面

  • 声明时获得初始值,定义时不可再设初始值 -> in-class 初值设定

  • 旧的编译器可能不支持in-class 初值设定。也可以 定义时设置初值,声明时不设置初值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    // xxx.h
    class GamePlayer{
      private:
        static const int NumTurns;
        ...
    }
        
    // xxx.cc
    const int GamePlayer::NumTurns = 5;  // 定义时给初值
    
    • 但是有个例外:class编译期间需要的常量(比如某个数组的大小由某常量给出),而旧式编译器不支持”in-class 初值设定”,如何解决? -> “enum hack”补偿

      1
      2
      3
      4
      5
      6
      7
      8
      
      // xxx.h
      class GamePlayer{
        private:
          enum {NumTurns = 5};
          int scores[NumTurns];
          ...
      }
            
      

enum hack

以上引出了enum_hack的使用场景

  • enum hack的优点
    • 可以提供一种整数常量,无法被别人获取到一个pointer或reference指向该常量
      • 这一点上,enum和#define一样,无法取地址,于是不会造成不必要的内存分配
      • 但是const却可以被取地址
    • 实用主义

L3: 多使用const

  • const提供常量约束
  • 如果某些对象是确定不变的,编程的时候就立即用const约束

const修饰指针

  • const 可以修饰指针自身 -> *号后有const
  • const 可以修饰指针所指物 -> *号前有const
  • 或者两者都修饰 -> *号前后都有const
1
2
3
4
5
  char* p1 = greeting1;              // non-const pointer, non-const data
  const char* p2 = greeting2;        // non-const pointer, const data
  char const * p2 = greeting2;      // 和上一行一样,两种写法都可以
  char* const p3 = greeting3;        // const pointer, non-const data
  const char* const p4 = greeting4;  // const pointer, const data

for example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// L3_const.cpp
/********************************************************************************
 * const 修饰指针
 */
int main(){
  
  char greeting1[] = "hello";
  char greeting2[] = "hello";
  char greeting3[] = "hello";
  char greeting4[] = "hello";

  char* p1 = greeting1;              // non-const pointer, non-const data
  const char* p2 = greeting2;        // non-const pointer, const data
  char* const p3 = greeting3;        // const pointer, non-const data
  const char* const p4 = greeting4;  // const pointer, const data

  p1 += 1;   // p1 = 0x7fffa0"hello" 变为 p1 = 0x7fffa1"ello", greeting = "hello"
  *p1 += 1;  // greeting = "hfllo", p1 = "fllo"

  p2 += 1;  // p2 = 0x7fffb1"ello", greeting = "hello"
  // *p2 += 1; // 编译不通过

  // p3 += 1; // 编译不通过
  *p3 += 1;  // greeting = "iello", p3 = 0x7fffc0"iello"

  // p4 += 1;  // 编译不通过
  // *p4 += 1; // 编译不通过

  return 0;
}

const修饰迭代器

  • STL 迭代器相当于 T* 指针,所以const修饰指针和修饰迭代器差不多
1
2
  const std::vector<int>::iterator iter = vec.begin();    // iter相当于T* const   // const iter, non-const data
  std::vector<int>::const_iterator const_iter = vec.begin();  //const_iter相当于const T*  // non-const iter, const data
  • STL迭代器提供了一个const_iterator类,实现non-const iter, const data

const修饰函数声明

1. 函数返回const常量

const写在最左边

1
2
3
4
5
6
7
8
9
10
11
12
const int f_add(int a, int b){
  return a + b;
}

int a = 4,b = 3;
if (f_add(a + b) = 10){  // 避免出现这样的笔误,本来应该写==判断,但是却写成了=赋值操作。如果f_add的返回不限定为const,这样的笔误很难发现
    ...
} 

if (f_add(a + b) == 10){  
    ...
} 

一点小tips:

当然,针对上面所讲的这种笔误,更好的操作是:所有出现 == 判断的地方,将值放在左边

1
2
3
4
5
6
if (f_add(a + b) == 10){  // bad
    ...
} 
if (10 == f_add(a + b)){  // good
    ...
} 
2. const成员函数

如果一个成员方法里面的操作是不会改变成员变量的,那么我们应该务必将它限定为const

好处:

  • class的接口可以非常容易理解:哪些是会改变对象内容的,哪些是不会改变对象内容的。一目了然。
1
2
3
4
5
6
7
8
9
10
11
class TestClass{
 public:
	int GetNum() const { // 限定const成员函数
		nums_++; // 这种操作会在编译时报错
		return nums_;
	}

 private:
	int nums_;
}

const修饰类

const修饰类主要涉及以下几个方面:

1. const对象
  • 当一个对象被声明为const时,该对象的状态不能被修改
  • const对象只能调用const成员函数
1
2
3
4
5
6
7
8
9
10
11
class MyClass {
public:
    void normalFunction() { /* 可以修改成员变量 */ }
    void constFunction() const { /* 不能修改成员变量 */ }
private:
    int value_;
};

const MyClass obj;  // const对象
obj.constFunction();    // 正确:可以调用const成员函数
// obj.normalFunction(); // 错误:不能调用非const成员函数
2. const成员函数的重载
  • 可以基于const性质重载成员函数
  • 编译器会根据调用对象是否为const来选择合适的版本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class TextBlock {
public:
    const char& operator[](std::size_t position) const {  // const版本
        return text[position];
    }
    
    char& operator[](std::size_t position) {  // non-const版本
        return const_cast<char&>(
            static_cast<const TextBlock&>(*this)[position]  // 调用const版本
        );
    }
private:
    std::string text;
};
3. mutable关键字
  • 有时候需要在const成员函数中修改某些成员变量
  • 使用mutable关键字可以让成员变量在const成员函数中也能被修改
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class CacheClass {
public:
    int getValue() const {
        if (!cached_) {
            cache_value_ = expensiveComputation();  // 可以修改mutable成员
            cached_ = true;
        }
        return cache_value_;
    }
private:
    mutable int cache_value_;
    mutable bool cached_ = false;
    
    int expensiveComputation() const { return 42; }
};

L4: 确定对象被使用前已经被初始化

  • 永远在使用对象之前先将其初始化
    • 对于C++内置数据类型(如int, double, string),手动完成初始化
    • 对于其他,在构造函数进行初始化,确保每一个构造函数都将对象的每一个成员初始化

初始化的方法:成员初值列

初始化和赋值的区别
  • 对象成员变量的初始化动作发生在进入构造函数本体之前,成员初值列 member initialization list
  • 在构造函数之内的,都是赋值而非初始化

for example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Point2d{
  public:
	Point2d(const float x, const float y);
  private:
    float x_;
    float y_;
};

Point2d::Point2d(const float x, const float y){
    x_ = x;   // 赋值
    y_ = y;
}

Point2d::Point2d(const float x, const float y)
   	:x_(x),y_(y)  // 初始化
{
   // 构造函数本体不用做什么       
}
  • 成员初值列的效率通常较高,是copy构造

    • 对于大多数类型而言,先调用default构造,再调用copy assignment操作,对比 单只调用一次copy构造函数,后者要高效得多
    • 对于c++内置的简单对象,初始化和赋值成本相同,但是依旧建议使用成员初值列
  • 成员初值列也支持default构造,只要指定(nothing)作为初始化实参即可:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    
    class Circle{
      public:
        Circle(const Point2d& point, const float r);
      private:
        Point2d point_;
        float r_;
       	float think_;
    };
      
    Circle::Circle(const Point2d& point, const float r)
    	:point_(),     // 调用 point_的默认构造函数
    	 r_(r),        // 调用 copy构造
         think_(0.5)   // 显式初始化
    {
    }
    
  • 建议总是在初值列中列出所有成员变量,但是有下面这种例外情形:一个类拥有多个构造函数时

当类拥有多个构造函数

问题:

  • 当class拥有多个构造函数的时候,每个构造函数都会有自己的成员初值列,这会造成重复。

解决:

  • 这种情况下,可以在初值列中忽略一些成员变量的初始化
  • 忽略哪些呢?忽略那些c++内置的简单对象的初始化 (因为他们初始化与赋值的成本一样)
  • 对没有初始化的被忽略的成员变量改用赋值,进行”伪初始化”
  • 将这些赋值操作移到某个函数内(通常为private, 命名为Init…)
  • 然后在所有的构造函数内调用

Tips:

  • 这种操作通常发生在”成员变量的初值由参数文件或者数据库读入”的情形下

成员初始化次序

  • c++有固定的成员初始化顺序
    • base class 早于 derived class
    • class内的成员变量按照其声明顺序初始化
  • 但是对于static对象,该如何分析?

For example

1
code: pc/sync/c++/basic/Effective_c++/L4.cpp
static对象的初始化次序

static对象的分类:

  1. global对象 - 在global或namespace作用域内的对象
  2. 定义在class内的static对象
  3. 定义在函数内的static对象 - local static对象
  4. 定义在file作用域内的static对象

初始化次序问题:

  • 同一编译单元内:static对象按照定义顺序初始化
  • 不同编译单元间:初始化顺序是未定义的(这就是”static initialization order fiasco”)

问题示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// file1.cpp
class FileSystem {
public:
    std::size_t numDisks() const { return 10; }
};
FileSystem tfs;  // global static对象

// file2.cpp  
class Directory {
public:
    Directory() {
        std::size_t disks = tfs.numDisks();  // 可能tfs还未初始化!
    }
};
Directory tempDir;  // 依赖于tfs的global static对象

解决方案:使用local static对象

将non-local static对象替换为local static对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// file1.cpp
class FileSystem {
public:
    std::size_t numDisks() const { return 10; }
};

FileSystem& tfs() {  // 返回reference指向local static对象
    static FileSystem fs;  // 定义并初始化local static对象
    return fs;
}

// file2.cpp
class Directory {
public:
    Directory() {
        std::size_t disks = tfs().numDisks();  // 现在安全了
    }
};

Directory& tempDir() {  // 同样使用local static
    static Directory td;
    return td;
}

local static对象的优点:

  1. 延迟初始化:只有在函数第一次被调用时才初始化
  2. 线程安全:C++11开始,local static对象的初始化是线程安全的
  3. 避免初始化顺序问题:通过函数调用控制初始化时机

注意事项:

  • 这种技术的基础是:C++保证函数内的local static对象会在该函数被调用期间首次遇到该对象定义时被初始化
  • 如果从未调用该函数,就绝不会引发构造和析构成本
  • 多线程环境下需要考虑线程安全问题(C++11之前需要手动处理)

最佳实践:

  • 最好使用local static对象替换non-local static对象

Contact

Feel free to contact me windmillyucong@163.com anytime for anything.


License

CC0