Effective C++ 3nd 笔记——让自己习惯C++

本文探讨了C++编程中的一些重要技巧,包括使用const、enum和inline替代宏定义以提高代码质量和效率;阐述了成员初始化列表的重要性及其在构造函数中的正确应用;并讨论了静态对象初始化次序问题及解决方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Effective C++ 3nd 笔记——让自己习惯C++

尽量以const,enum,inline替换#define

宏定义在预处理阶段就进行了字符串替换,于是你的宏名有可能没进入符号表;

对于浮点数 ( f l o a t ) (float) (float)来说,使用const比使用宏定义可以导致较小的代码量,因为预处理器盲目的进行宏替换,这可能导致在程序中出现多份目标码。而用const则不会

变量的声明就是将变量的别名告诉编译器,并不带有细节信息

enum hack的好处:可以在某些不支持类数据成员在类内初始化的编译器(一般比较老旧)的情况下对数据成员进行类内初始化(其理论基础就是会把它作为一个枚举常量);可以防止用户取得某些数据的地址(#define也可以);不会导致非必要的内存分配

请记住:

  • 对于单纯常量,最好以const对象或enums替换#define
  • 对于形似函数的宏,最好改用inline函数替换#define

尽可能使用const

声明STL迭代器为const就像声明指针为const一样,即 T* const;如果想声明为 const T*,则需要使用const_iterator

const最具威力的用法是面对函数声明时的应用,令函数返回一个常量值,往往可以降低因客户错误而造成的以外,而又不至于放弃安全性和高效性

对于不需要改动的参数或本地对象,最好声明为const,这可以避免很多烦恼

成员函数如果是const意味着什么?这有两个流派概念:bitwise constness(physical constness)和logical constness
前者是成员函数不更改对象内的任何一个bit,这也是C++对常量性的定义。但有许多成员函数不具备十足的const性,但也能通过bitwise测试。更具体的说,一个更改了 “指针所有物” 的成员函数虽然不能算是const,但如果只有指针隶属于对象,那么此函数不会引发bitwise编译器错误,考虑以下代码:

class CTextBlock{
public:
	...
	char &operator[](std::size_t position) const  // bitwise 声明,但其实不当
	{ return pText[position]; }
	
private:
	char *pText;
};
// 这个 class 不适当的将其 operator[]声明为 const 成员函数,而该函数返却返回一个 
// reference 指向对象内部值,则产生以下情况

const CTextBlock cctb("Hello");  // 声明一个常量对象
char *pc = &cctb[0];  // 调用[]取得一个指针,指向cctb的数据

*pc = 'J';  // 现在cctb = "Jello",虽然cctb为const,但实际上我们还是改变了它的值

这种情况就导出了logical constness。主张一个const成员函数可以修改它处理的对象内的某些bit,但只有在客户端侦测不出的情况下才能如此

使用mutable可以释放掉non-static成员变量的bitwise constness约束

可以使用类型转换避免const和non-const成员函数的代码重复,可以使用const_cast将原const成员函数的const性质移除,但要考虑代码的安全情况;non-const函数可以调用const函数,反之则不行

请记住:

  • 将某些东西声明为const可帮助编译器侦测出错误用法。const可被施加于任何作用域内的对象、函数参数、函数返回类型、成员函数本体
  • 编译器强制实施bitwise constness,但你编写程序时应该使用 “概念上的常量性”
  • 当const和non-const成员函数有着实质等价的实现时,令non-const版本调用const版本可避免代码重复

确定对象被使用前已先被初始化

首先我们要分清楚赋值和初始化,考虑以下代码:

class PhoneNumber{ ... };  // 随便声明一个类
class ABEntry{
public:
	// 定义类的构造函数
	ABEntry(const std::string &name, const std::string &address, const std::list<PhoneNumber> &phones){}

private:
	std::string thename;
	std::string theAddress;
	std::list<PhoneNumber> thePhones;
	int numTimesConsulted;
};

ABEntry::ABEntry(const std::string &name, const std::string &address, const std::list<PhoneNumber> &phones){
	// 这些都是赋值,而非初始化
	theName = name;
	theAddress = address;
	thePhones = phones;
	numTimesConsulted = 0;
}

这样虽然可以让ABEntry里的成员变量带有你期望的值,但这并不是最佳做法。C++规定,对象的初始化发生在进入构造函数本体之前,在ABEntry构造函数类,这些成员函数都不是被初始化,而是被赋值。

ABEntry构造函数的一个较佳写法是通过成员初始化列表(member initialization list)替换赋值动作

使用成员初始化列表的结果和上面的结果相同,但效率更高。上面基于赋值的版本,首先会调用默认构造函数为成员变量设初值,然后再立刻对它们附上你所期望的值。这样的话,构造函数所做的一切就浪费了。使用成员初始化列表可以避免这一问题,因为初始化列表中针对各个成员变量而设的实参,被拿去作为各成员变量的构造函数的实参

但需要注意的是,对于内置类型成员(int, double, float…)比如ABEntry类中的numTimesConsulted,其初始化和赋值的成本相同,但为了一致性最好也通过成员初始化列表来进行初始化;如果成员变量是const或references,就一定需要初值,不能被赋值。顺带一提,成员变量的初始化次序只和其的声明次序有关,与在初始化列表中的次序无关。

解决完上面的问题之后,还剩下最后一个问题:不同编译单元内定义的non-local static对象的初始化次序
什么是编译单元:所谓编译单元是指产出单一目标文件的那些源码。基本上它是单一源码加上其所含入的头文件

这个问题至少涉及到两个源码文件。真正的问题是:

如果某编译单元内的某个non-local static对象的初始化动作使用了另一编译单元内的某个non-local static对象,它所用到的这个对象可能尚未被初始化,因为**C++对 “定义于不同编译单元内的non-local static对象” 的初始化次序无明确定义*。考虑以下代码:

// ---------------------------------文件1---------------------------------------

class FileSystem{  // 来自你的程序库
public:
	...
	std::size_t numDisks() const;  // 众多成员函数之一
	...
};
extern FileSystem tfs;  // 定义一个FileSystem对象,用extern修饰表示其可以在别处引用

// ---------------------------------文件2---------------------------------------

class Directory{
public:
	Directory( params );  // 构造函数
	...
};
Directory::Drectory( parmas ){
	...
	std::size_t disks = tfs.numDisks();  // 使用tfs对象
	...
};
Directory tempDir( parmas );  // 创建一个Directory对象

此时就会出现问题,除非 tfs 在 tempDir 之前先被初始化,否则 tempDir 的构造函数会用到尚未初始化的 tfs。但 tfs 和 tempDir是不同的人在不同的时间在不同的源码文件中建立的,如何保证 tfs 会先于 tempDir 初始化呢?答案是:无解

多个编译单元内的non-local static对象经由 “模板隐式具现化,implicit template instantiations”形成,不但不可能决定正确的初始化次序,甚至往往不值得寻找 “可决定正确次序” 的特殊情况

幸运的是一个小小的设计便可以完全消除这个问题。唯一需要做的是:将每个non-local static对象搬到自己的专属函数内(该对象在此函数内被声明为static)。这些函数返回一个reference指向它所含的对象,然后用户调用这些函数,而不直接指涉这些对象。换句话说,non-local static对象被local static对象替换了。这是 S i n g l e t o n Singleton Singleton(单例设计)模式的一个常见手法

这个手法的基础在于:C++保证,函数内的local static对象会在 “该函数被调用期间,首次遇上该对象的定义” 时被初始化。所以如果你以函数调用替换直接访问non-local static对象,你就获得了保证,保证你所获得的那个reference将指向一个历经初始化的对象。作用于上述代码有以下效果:

// ---------------------------------文件1---------------------------------------

class FileSystem{ ... };  // 同前
FileSystem &tfs(){  // 用函数来替换tfs对象
	static FileSystem fs;
	return fs;
}

// ---------------------------------文件2---------------------------------------

class Directory{ ... };  // 同前
Directory::Directory( parmas ){
	...
	std:::size_t disks = tfs().numDisks();
	...
}
Directory &tempDir(){  // 用来替换tempDir对象
	static Directory td;
	return td;
}

但是从另一个角度看,这些函数 “内含static” 对象的事实使它们在多线程系统中带有不确定性。任何一种non-const static对象,不论它是local或non-local,在多线程环境下 “等待某事发生” 都会有麻烦。处理这个麻烦的一种做法是:在程序的单线程启动阶段手工调用所有reference-returning函数,这可消除与初始化有关的 “竞速形式”。

运用reference-returning函数防止 “初始化次序问题”,前提是其中有着一个对对象而言合理的初始化次序

请记住:

  • 为内置型对象进行手工初始化,因为C++不保证初始化它们
  • 构造函数最好使用成员初始化列表,而不要在构造函数本体内使用赋值操作。初始化列表列出的成员变量,其排列次序应该和它们在类中的声明次序一致
  • 为免除 “跨编译单元的初始化次序” 问题,请以local static对象替换non-local static对象
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

JallinRichel

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值