《Effective C++》 读书笔记(详细总结55条款)上篇

本文是《Effective C++》的读书笔记上篇,主要涵盖了C++语言联邦的理解、const、enum、inline的使用、对象初始化、构造、析构与赋值运算的注意事项,以及资源管理和设计原则等内容。强调了使用const、枚举和inline替换#define,避免异常逃离析构函数,以及智能指针在资源管理中的重要性。
摘要由CSDN通过智能技术生成

文章阅读时间可能会超过1小时

一、让自己习惯 C++

1、视 C++ 为一个语言联邦

怎么理解这句话,就是不要把C++理解为一个单一的语言,它有很多个语言规则的联邦组成,例如C++ 支持多重泛型编程,既面向过程又面向对象、泛型形式、元编程形式,(看不懂不用管,你只要知道它不单一,有很多知识就行了)

下面从四个方面来说
(1) C++ 以 C 为基础,很多语句语法都没有改变,只是进行了扩展,C++ 解决问题的时候,可以理解为 C 的高效解法

(2) 面向对象,有构造函数,析构函数,继承、封装、多态、虚函数动态绑定,这是不同于C 语言的部分

(3) 泛型编程、可以理解为代码的模型,但是代码本身并不占用内存空间,只有在实例化的时候,才分配内存,所以可以提高代码复用性

(4) STL 模板程序库,是C++ 标准库组件之一,有容器,算法、迭代器、适配器、空间配置器、仿函数。其中空间配置器为容器在内存中分配空间,算法提供了排序、查找、删除等操作;迭代器方便用户进行遍历容器内部的元素,迭代器也可以和算法在一起使用;仿函数也是和算法在一起使用,仿函数是一个类,可以理解为用户自定义规则;适配器是通过接口的一些特性,把它转为另一个容器

然后我们再回头看那句话,C++ 是一个语言联邦,它就是根据这4个次语言组成,每一个次语言都有自己要遵守的规则。

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

这句话说的是什么?

#define 这玩意不好使,尽量不要使用,因为它是在预处理器执行的,不属于语言的一部分,而 const 、enum、inline 是在编译的时候执行的

那他们之间有啥区别?

举个例子

 #define num 1.653
 const double num = 1.653;

第一行语句在预处理执行,num 这个记号不会被编译器看见,不会进入符号表,如果把第一行语句放入头文件,那么后序代码有问题需要进行调试的话,别人看你的代码,会对 1.653 很懵逼,谁知道它哪冒出来的,或者你看别人的代码会一头雾水.

还有一个缺点是,#define 进行宏定义的时候容易导致歧义,如果括号使用不当,最后的结果可能不是预想的结果

建议做法:使用一个常量替换宏,例第二行语句,这两行语句意思和目的是一样的

关于这个建议还需要注意两个地方
(1) const 定义一个指针,如果把它写在头文件,需要写两次 const ,例 const int* const a = 10; 这样指针指向和指向的值就会被确定。

(2) const 变量可以使 class 的专属常量,确保它的作用域限制于 class 内,可以加 static 修饰,保证最多提供一个实体;但是我们无法使用 #define 创建一个class 常量,因为 #define 并不是重视作用域,宏一旦被定义,在后面编译过程中有效(除非被 #undef)

最后再补充一点 static 变量一般情况下是在类外定义赋值,类中只是声明一下,但是遇到下面情况怎么办?

class GamePlayer{
   
private:
	static const int NumTerns;
	int score[NumTerns];
	...
};

编译期间要知道数组的大小,否则编译不通过(有的编译器可以在类内定义的同时可以赋值,但是对于旧编译器不可以在类内定义,这时候怎么办?)

看看条款,我们就知道需要 enum 枚举

class GamePlayer{
   
private:
	static const int NumTerns;
	enum{
   
		NumTerns = 5 // 令 NumTerns 成为 5 的记号名称
	};
	int score[NumTerns];
	...
};

这样就可以完美解决问题了

对于 inline 关键是替换宏函数的,因为宏函数没有参数类型检查,出错了也很难追踪到,所以使用 inline 建议编译器把函数定义为内联函数

小总结:使用 const,enum 替换宏常量,使用 inline 替换宏函数

3、尽可能使用 const

使用 const 可以保证一定的安全性

const 有啥作用,使用范围?
(1) 在类外修饰全局变量,常量,或者修饰函数,区块作用域中被声明为 static 的变量

(2) 在类内可以修饰声明为 static 成员变量和非static 成员变量

(3) 面对指针,可以修饰指针本身或者指针所指物

(4) STL 迭代器作用像一个 T* 指针,声明迭代器为 const 就相当于声明指针 T* const ,表示指针的指向不可以改变,但是所指物可以改变,如果是一个 const_iterator 表示所指物不可以改变,即相当于const T* 或者 T const * (二者本身没有区别)

(5) const 修饰函数的时候,可以保证安全性,比如返回值不可以被修改,或者修饰 this 指针,可以保证意外或者恶意修改

未完待补充… 原文中还有许多例子,我只能理解这么多了…

4、确定对象被使用前以被初始

因为读取未初始化值可能发生不明确行为,有的平台可能直接导致程序终止了…

所用永远在对象使用之前初始化它

int x = 0;
const char * text = "A C-style string";

//读取input stream 的方式进行初始化
double d; std::cin>>d;

对于内置类型以外的,可以使用构造函数进行初始化

需要注意的是:构造函数体里面叫赋值,初始化列表中才叫初始化,使用初始化列表比较高效,因为其只调用一次拷贝构造函数,而不需要调用构造函数再调用赋值操作符重载函数.

而且有的变量必须要在初始化列表中进行,比如引用类型,const 类型,因为在构造函数体内的是赋值,因为这些变量不可以被赋值.

所以不管是什么变量,一股脑把它放到成员初始化列表中进行初始化,就好了

注意:成员变量的初始化顺序是和声明顺序一致的,和初始化列表中的顺序没有半毛钱关系.

最后一点,跨编译单元的初始化次序问题,怎么确定?
个人觉得还是挺重要的,举个例子

// 服务器建立
class FileSystem{
   
public:
	// ...
	std::size_t numDisk()const; // 成员函数
	// ....
};
extern FileSystem tfs; // 准备给客户使用的对象

// 客户建立
class Directory{
   
public:
	Directory(params);
	~Directory();
};
Directory::Directory(params){
   
	//...
	// 问题就在这一行语句,你怎么确定 tfs 已经被初始化了?
	std::size_t disks = tfs.numDisk();
	//...
}

// 客户决定创建一个对象来存放临时文件
void main(){
   
	Directory tempDir(params);
}

C++ 对于不同编译单元的对象初始化次序无法得知,但是可以通过一个设计,解决问题,也就是单例模式

解决办法:把非 static 对象设置为 static 成员变量,这样类在创建的时候,就已经初始化了,所以当客户去使用这个对象的时候,是没有风险的!

如下

//=============================================
// 高效做法
class FileSystem{
   ...};

// 不会引发构造函数
FileSystem& tfs(){
   
	static FileSystem fs;
	return fs;
}

class Directory{
   ...};
Directory::Directory(params){
   
	// ..
	std::size_t disks = tfs().numDisk();
	//...	
}

Directory& tempDir(){
   
	static Directory td;
	return td;
}

二、构造、析构、赋值运算

5、了解 C++ 默认编写调用的函数

创建一个类,经过C++编译器处理后,如果自己没有声明,那 C++ 编译器会为其生成默认的构造函数、拷贝构造函数、non-virtual 析构函数、赋值操作符重载,取地址操作和 const 类的取地址操作符。

如果自己声明这些函数,则编译器不会再创建对应的函数,只要当这些函数需要的时候(被调用时)才会被创建出来

为什么编译器要默认创建这些函数,有什么意义,又有什么缺点?
原因1:因为对象的实例化是很常见的事,调用默认函数比较方便
原因2:可以管理栈对象,栈对象的赋值和拷贝构造一般情况不会出大问题,但是对于堆上的对象需要用户自己去创建.

缺点:拷贝构造、赋值操作符这些函数都是浅拷贝的形式,所以当有不符合规则的情况下,编译器禁止调用赋值操作符和拷贝构造,另外如果用户进行 new 申请了空间,一定不要使用默认的成员函数,需要自己创建.

举例:

template<class T>

class NameObject{
   
public:
	// 注意这是一个引用类型
	NameObject(std::string& name,const T& value){
   

	}
private:
	std::string& name;
	const T objectValue;
};

int main(){
   
	std::string newDog("Persphone");
	std::string oldDog("Starch")
	NameObject<int>p (newDog,2);
	NameObject<
  • 8
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

程序猿的温柔香

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

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

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

打赏作者

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

抵扣说明:

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

余额充值