《Effective C++》读书笔记(一):让自己习惯C++(条款01~04)

2023-04-27 19:58:15 浏览数 (2)


1.视C 为一个语言联邦

当看到这一小节的时候,让我重新认识了一下C 了。很多人在说起C 的时候,都只会说C 是建立在C基础上的一个面向对象的语言,而老师会加上一句:C 是一个面向过程同时,也面向对象的语言,因为它包容C。

而在这一小节中,作者Meters修正了我对C 的认识:C 经过多年的发展,已经是一个多重范型编程语言。即同时支持过程形式、面向对象形式、函数形式、泛型形式和元编程形式的语言。这些形式组成了如今是C ,因此我们可以把C 视为一个由众多语言组成的语言联邦。

在C 语言联邦中,重要的次语言有以下四个:

C语言,面向对象,泛型和STL

因此,我个人认为,我们在学习C 的时候,如果不熟练这四种重要的次语言,那就不能说我们熟练C 了。

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

2.1 使用const来替换#define的缘由

使用const来替换#define,是因为宏定义的记号名称在编译器开始处理源码的时候就已经被预处理器移走,使得我们自己定义的记号名称不能进入记号表,导致后续运用这个常量的时候出现编译错误让我们满脸疑惑。

2.2 使用enum替换#define的缘由

类中静态整型常量在一些编译器中是不允许在来中初始化的,但是当我们需要在类中使用这个常量的时候。或许我们会想到使用#define来创建一个class专属常量,但是这是不可以的,因为#define并不重视作用域,没有任何封装性!此时就可以使用枚举enum。

代码语言:javascript复制
class A
{
private:
	enum{Num = 5};
	int a[Num];
};

int main()
{
	A a;
	return 0;
}

在类中定义一个静态常量成员的原因: ①让常量的作用域限制于类中   ②确保此常量只有一份,因此使用static

2.3 使用inline替换#define的缘由

“对于形似函数的宏,最好改用inline函数替换#define”

内联函数会像宏一样,在编译的时候会展开,不会有函数栈帧的额外开销。对于一些函数来说,我们想要提高效率,使用宏来定义固然是可以提高效率,但是使用宏可能会导致计算出来的结果是不对的!

比如下面这段代码:

代码语言:javascript复制
#define Compare_ab(a,b) ((a)>(b)?(a):(b))
int main()
{
	int a = 1, b = 5;
	Compare_ab(  a, b);  //a会被累加两次 
	Compare_ab(  a, b 5); //a会被累加1次
	
	return 0;
}

将a拿进去算的时候,会在前面判断的时候算一次,在后面算一次。这显然不符合我们的预期。因此,我们如果想要得到拥有宏一样的效率,也想得到正确的结果,那么就使用内联函数。

代码语言:javascript复制
inline int f(int a, int b)
{
	return a > b ? a : b;
}
int main()
{
	int a = 1, b = 5;
	//Compare_ab(  a, b);  //a会被累加两次 
	//Compare_ab(  a, b 5); //a会被累加1次
	f(a, b);
	
	return 0;
}

3.尽可能使用const

3.1 const关键字在星号的左右

代码语言:javascript复制
char greeting[] = "Hello";
char* p = greeting;   //指针p的指向可以改变,指针p指向的内容可以改变
const char* p = greeting; //指针p的指向可以改变,指针p指向的内容不可被改变
char * const p = greeting; //指针p的指向不可以改变,指针p指向的内容可以被改变
const char* const p = greeting; //指针p的指向不可以改变,指针p指向的内容不可以被改变

const出现在星号的左边,代表着被指物是常量。const出现在星号的右边,代表指针本身是常量。如果const出现在星号的两边,说明被指物和指针本身都是常量。

3.2 让函数返回常量值

函数返回一个常量值,可以避免一些意外。如:

代码语言:javascript复制
class Rational{...};
//乘法运算符重载
const Rational operator* (const Rational& lhs, const Rational& rhs);

意外:
Rational a,b,c;
(a * b) = c;//在a和b相乘的结果上调用operator=,或许本意是作比较,而非赋值。

3.3 const成员函数

用const修饰成员函数的目的有两个:

①让const对象能够使用该函数。②让接口容易被理解,理解哪些内容不能被改,哪些可以被该。

在成员函数中,比如operator[]的重载,如果函数返回值不加const,也不是引用返回,那么这样是错误的:

a[0] = 'x';

因为如果函数的返回类型是一个内置类型,那么改动函数返回值是不合法的。

4. 确定对象被使用前已经被初始化

先来说说几个概念:

①static对象:生命周期随程序的结束而结束,这种对象包括全局global对象、定义在namespce作用域内的对象、在类内,在函数内、以及在文件file作用域内被声明为static的对象。 ②local static对象和non-local static对象:函数内的静态static对象就是local static对象(因为对于函数来说就是local局部的),其它static对象都是非局部静态non-local static对象。

条款04的重要三个点:

①最好对类中的成员变量进行初始化,即使用初始化列表初始化,在构造函数的主体内,那不是初始化,那叫赋值。顺带提一嘴,对于const、static的成员变量,就必须在初始化列表中初始化。自定义类型会调用它的构造函数,内置类型或内置型对象必须手工初始化。 ②初始化列表的初始化顺序是按照成员变量在声明时的顺序来的,并非是在初始化列表中的顺序! ③跨编译单元的初始化次序问题:

在不同编译单元中定义的non-loacl static对象的初始化次序在C 中没有明确的定义。什么意思呢?举个例子:

在在一个源码文件A中写出了一个类:

代码语言:javascript复制
class FileSystem {
public:
    //...
    std::size_t numDisks() const;
    //...
};
extern FileSystem tfs;//给用户使用的对象

在源码文件B中,调用源码文件A的对象:

代码语言:javascript复制
class Directory {
public:
        Directory(data);
        //...
};
Directory::Directory(data)
{
    //...
    std::size_t disks = tfs.numDisks();//使用源码文件A中的对象
    //...
}

//创建Directory对象
Directory tempDir(data);

问题来了,我们读这代码的时候,都认为的初始化顺序理所当然的是先初始化tfs,然后再去初始化tempDir。但是前面说了,对于这种跨编译单元的non-local static对象,它们的初始化顺序是没有确定下来的。

幸运的是,我们可以解决这个问题,办法就是:专门为non-local static对象写一个函数,然后引用返回这个对象!这样,用户调用这个函数,从而拿到了这个对象。换句话来说,就是用local static对象替换non-local static对象。

原理是:C 中,函数内的local static对象会在该函数被调用的时候,并且首次遇上该static对象的定义的时候就会将其初始化!这样,就能保证拿到的tfs是已经被初始化了的。

其实我们已经看出,这是单例模式的一种手法!

代码语言:javascript复制
class FileSystem {
public:
    //...
    std::size_t numDisks() const;
};
FileSystem& tfs()
{
    static FileSystem fs;//在首次遇到fs的时候,对它进行初始化
    return fs;
}

class Directory {
public:
    Directory(int data = 0);
};
Directory::Directory(int data = 0)
{
    //...
    std::size_t disks = tfs().numDisks();
    //...
};
Directory& tempDir()
{
    static Directory td;//在首次遇到td的时候,对它进行初始化
    return td;
}

Directory t(1);

0 人点赞