《Effective C++》第一章笔记

第一章 让自己习惯C++

条款1:C++是一个语言联邦

C++可以看作是由四个次语言组成的集合体:

  • C:基本的C语言,区块、语句、预处理、内置数据类型、数组、指针等。
  • Object-Oriented C++:C with Classes诉求的,即封装、继承、多态、动态绑定等等。
  • Template C++:泛型编程部分,即编程不受限于某种具体类型。
  • STL:标准库,包含容器、迭代器、算法、函数对象等等。

当从某一个次语言切换到另一个时,会导致高效编程的规则发生变化。例如对于内置类型而言按值传递要比引用传递高效,但是对于用户自定义类型(class)而言由于构造函数和析构函数的存在,const-引用传递更好。 对于泛型编程中的模板尤为如此。

条款2:尽量以const、enum、inline替换#define

define预处理命令通常由两种用户:定义常量和定义宏,下面分别针对这两种用法进行优化:

  • 定义常量使用const或者enum替代
  • 定义宏使用inline函数替代

对于单纯的常量,最好以const或者enum替换#define

1
#define MAX 100

即C语言中常用于定义常量的形式。这种形式缺点:

  • 编译器无法检查是否错误。#define与预处理命令,是由预处理器来完成的,不是由编译器完成的。具体的工作就是在编译之前把文件中所有的MAX替换成100,因此MAX从未进入编译器的记号表。因此如果出问题的话编译器报错找不到是MAX的错误,最多找到是100的错误;

  • 影响目标代码大小。如果文件中有很多处MAX,那么就会用很多100代替MAX,即出现很多份100使得目标代码变大。

    改进方式一:使用const变量

    1
    const int Max = 100

    这样不仅使得Max变量进入编译器检查范围,并且只会有一份变量副本。

    注意事项:

    1.常量指针:由于常量定义一般放在头文件中以便被不同的源码使用,因此有必要把常量指针声明为const,不仅不允许通过指针修改变量,也不允许修改指针指向:

    1
    const char * const p = "sss" ;

    2.class专属常量:可以将常量的作用域限制在类内,这样常量就必须是class的成员,并且为了使得只存在一个副本而不是所有类对象都有一个常量副本,声明为static成员:

    1
    2
    3
    class C {
    static const int Max = 100
    };

    注意这是一个in-class声明式。只允许对于类内static整数常量进行。

    声明式和定义式:C++需要对任何使用的东西提供定义式,定义式使得编译器为此对象分配内存。声明式告诉编译器类型和名称。但是对于class专属常量并且为整数类型(int、char、bool),只要不取地址,就可以声明使用它们而无需定义。声明式也可以初始化,把声明、定义、初始化概念分开。

    注意有可能编译器不支持in-class声明式,即声明中初始化。这样就需要进行定义式:

    1
    const int Class::MAx = 1.35// 位于实现文件内

    而声明式随类定义位于h文件内。)

    (注意这里是static成员,与静态存储持续性的概念有区别,作为静态变量是会自动进行零初始化,即所有位都被设置为0)。

    改进方式二:class专属常量的另一种形式——enum

    可以使用如下形式代替class专属常量:

    1
    2
    3
    4
    class C {
    enum { Max = 100 };
    int arr[MAx];
    }

    因为枚举类型的数值可以当作int类型使用。

    枚举类型行为类似于#define,不允许取其地址,而const变量允许取地址,所以当不允许取地址时可用enum。

对于形似函数的宏最好用inline函数代替#define

1
2
3
4
5
#define MAX(a, b) f((a) > (b) ? (a) : (b));  // 以最大值调用函数f

int a = 5, b = 0;
MAX(++a, b); // 等价于((++a) > (b)) ? (++a) : (b); a累加两次
MAX(++a, b+10); // a累加1次

这导致a的递增次数与它和谁相比有关,不合理。

不如改成模板内联函数:

1
2
3
4
template <typename T>
inline void Max(const T &a, const T&b) {
f(a > b ? a : b); // 以较大值调用函数f
}

条款3:尽可能使用const

const作用

const语义上来说是定义了一种约束,即不允许修改。可以将const用于任何作用域内的对象、函数参数、函数返回类型、成员函数等。

编译器会帮助检查尝试修改的做法。

需要注意的是,* 之前表示修饰的是对象本身,表示对象本身不能被修改; * 之后修饰的是指针的指向,表示指向不可修改。

const在类型前还是在类型后作用都一样,比如:

1
2
const int a = 10
int const a = 10

const修饰函数返回值类型与参数类型

1.修饰返回值类型即不允许将对于函数返回值进行修改,尤其是返回一个用户自定义类型的对象时;

2.修饰函数参数与修饰正常对象没什么不同。(需要注意的是,对于参数是const的引用形式,如果把一个右值(即不能取地址的值)传递给它,编译器会尝试把这个右值转换为一个匿名变量,然后引用这个匿名变量)

const修饰成员函数

1.使得接口(函数方法)比较清晰,即声明了为const的方法表示不会对成员进行修改;

2.使得能够操作const对象。例如一个对象声明为const类型,那么就无法调用其一般接口,因为一般接口(函数方法)可能会尝试修改成员数据。这样即使一个普通接口没有修改成员无法调用。为了调用这些不修改成员的方法,就需要将这些方法声明为const。

什么时候会用到const对象呢?通常不会显式声明或定义一个const对象,一般是在函数参数中将对象设置为const类型的引用,以防止修改对象。)

两个函数只是常量性不同也是可以被重载的

比如重载自定义数组的[]运算符,对于const对象应该使得返回值类型为const T&或者const T,运算符方法应该为const;对于非const对象应该使得运算符返回值类型为T&,运算符方法不应为const。

即对于const对象和非const对象调用同一个方法(但是方法的常量性不同)会有两个不同的版本。

编译器强制实施比特位常量性,但是编程应该按照概念常量性

以类对象为例子:

比特位常量性指的是不允许改变类对象中成员的任何一位,编译器也是这么执行的。(修改成员数据都是通过this指针,通过对把this指针声明为const,导致无法通过this指针改变成员数据)。

概念(逻辑)常量性指的是虽然不能修改类成员,但是const成员函数可以修改它所处理的对象。这种情况只有在客户端侦测不出来时才可以。

客户端侦测不出来的const发生变化的情况:比如类成员中有一个指针,虽然不允许修改常量对象的指针变量,但是指针指向的位置却不受const的约束,因此客户端侦测不出来)

解决办法:可以使用mutable关键字,把某些成员声明为mutable,这样可以修改这些对象。比如string类对象有一个指针指向字符数组,即使把string对象声明为const也不影响修改字符数组,那么string类中的成员length应该声明为mutable,这样当字符数组改变时可以修改length这个成员。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class CTextBlock {
public:
std::size_t length() const;
private:
cahr *pText;
mutable std::size_t textLength; // 这两个成员变量总是可以被修改,即使在const成员函数内
mutable bool lengthIsValid;
};

std::size_t CTextBlock::length() const {
if (!lengthIsValid) {
textLength = std::strlen(pText); // 两个mutable成员变量允许在const成员函数内修改
lengthIsValid = true;
}

return textLength;
}

non-const成员函数调用const成员函数

这样做的目的是当const成员函数和non-const成员函数有着等价的实现时,减少重复代码,比如数组中的[]运算符。

可以使用非const成员函数调用const成员函数,因为非const成员函数本来就可以修改或者不修改成员,因此调用const成员函数是合理的;

但是反过来就不合理,因为const成员不能修改成员,因此不能调用非const成员函数。

非const成员函数调用const成员函数形式:

1
2
3
char& operator[](int index) {
return const_cast<char&>(static_cast<const ClassType&>(*this)[index]);
}

注意,这里存在两个转换

1.static_cast<ClassType&>(*this):安全转换,把非const对象转换为const对象,因为只有const对象才会使用const版本;

2.const_cast<char&>把const版本的运算符[]函数的返回值类型从const char&转换为char&类型。

条款4:确定对象使用前已经被初始化

内置类型

使用前一定要先初始化。其中使用cin读取输入也叫做初始化。(即初始化并不是在声明中才叫初始化)

用户自定义类型

对于内置类型以外的任何类型,初始化的工作由构造函数完成。

  • 尽量使用初始化列表而不是在构造函数内部进行赋值:C++规定在进入构造函数函数体之前成员变量已经创建,可以认为初始化(这个初始化的意思不是获得值,而是获得内存空间)在函数体之前完成(对于内置类型的成员不能保证一定在函数体之前获得初值)。因此应该使用初始化列表初始化成员,这样就避免了:先创建对象,再给对象进行赋值的时间消耗。
    值得注意的是:直接调用copy构造函数的效率比先调用default构造函数再调用copy assignment运算符的效率高!
    对于内置类型:其初始化和赋值的成本一样,但是为了一致性,最好使用初始化列表。
    对于const成员和引用类型的成员:只能在初始化列表中进行初始化!
    成员初始化顺序:与类中的声明顺序相同,所以初始化列表尽量按照成员声明顺序,防止后者对于前者有依赖。
    一个类多个构造函数有重复的成员初始化部分:可以将那些“初始化和赋值效率相同”的成员(其实就是内置类型)统一用一个私有方法实现,这样就可以使用构造函数调用这个私有方法实现内置类型的初始化操作。
    总是在初始化列表中列出所有成员变量:如果想要使用成员变量的默认构造函数,就让列表初始化括号内为空即可。

  • 不同编译单元中non-local static对象的初始化顺序:static 对象:指的是存储持续性为static的对象,即其声明周期从程序运行开始,直到程序结束。non-local static对象:指的是存储持续性为static,但是非局部(即不在函数内)的对象。编译单元:指的是产出单一文件目标的源码,一般就是一个.cpp文件加上它包含的几个头文件。
    C++对于定义于不同编译单元的non-local static对象的初始化次序无明确定义。
    (因为基本上也不可能进行顺序定义,例如多个编译单元的non-loacl static对象是由“模板隐式具体化“产生的,即不运行到类模板具体化的地方是不会产生这个类对象的。)
    解决方法是:将每个non-local static对象移动到自己的专属函数内,这个对象在函数内声明为static(仍能保证存储持续性),定义并初始化这个对象,然后使得函数返回一个指向该对象的引用。原理在于:C++保证函数内的local static对象会在”函数被调用期间“、”首次遇到该对象定义式“时被初始化。所以可以用函数调用(返回一个指向local static对象的引用)替换访问一个non-local static对象。(类似于设计模式中的单例模式)
    解决方法在多线程下存在的缺陷:因为这些函数内含static对象,所以使得它们在多线程中具有不确定性。(有可能一个函数在一个线程而另一个函数在另一个线程)会导致一件事等待另一件事,所以尽量在单线程启动阶段完成调用所有的返回局部静态变量引用的函数以解决初始化在多线程中进行竞速的关系。

总结

这一章主要是说明C++与C语言或者其它语言中一些不同的地方,包括:
1.常量、类内常量、类似函数的宏:使用const、enum、inline函数而非define;
2.在C++中尽量使用const
3.使用任何对象前一定要初始化