C++:让自己习惯C++(Effective C++)

写在前面

这是对Effective C++这本书中的部分内容进行的总结以及代码实践,主要是记录一些对我印象深刻的,确实能改善程序的内容,和有需要实践验证加深印象的一部分实践和我自己的理解

视C++为一个语言联邦

C++发展许久一直到今天,已经是一个很丰富并且很成熟的语言,有很多冒险大胆的做法,要把C++看成一个联邦而不是单独的一个语言,联邦可以由很多部分组成,例如在C++中有

  1. C
  2. Object-Oriented C++:包括但不限于构造析构,封装,继承多态等等
  3. Template C++
  4. STL

你可以把C++看成是一个由四个次语言组成的联邦

C++高效编程手册视情况而变化,取决于用哪一部分


尽量用const enum inline替换#define

使用const的场景和好处:

使用#define有诸多不便,例如:

  1. 在编译的预处理阶段,会把#define定义的内容全部进行替换,也就是说define的内容其实根本就不在记号表中,因此如果此时出现了错误,调试信息是一个巨大的成本,十分不方便,而如果采用const常量来代替宏就很方便

  2. 如果使用#define,在预处理会全部被替换,因此就会导致目标码中出现多份同样的数字,而如果使用常量就只会在记号表中留下一份数据

// 宏定义有诸多不便
#define rate1 0.8
// 才用const常量可以有效解决很多不便
const double rate2 = 0.8;
  1. #define定义的内容的作用域大概率是全局,会直接进行全局的替换,除非使用其他预处理指令,也就是说#define是不可以被封装,也不可以在class中定义专属常量,而你只需要在class中使用static const就可以声明一个常量,在新标准中甚至可以给值,这样如果不到特殊场景你都可以不需要定义,直接使用它的声明即可

使用enum的场景和用处

假设有下面的代码:

class A
{
    
    
private:
	static const int _num = 10;
	int _proinf[_num];
};

在有些编译器中,编译器必须要知道数组的大小,而如果编译器此时不支持const常量作为数组大小,这时你可以选择enum作为替代方法:

class A
{
    
    
private:
   //static const int _num = 10;
   enum
   {
    
    
   	_num = 5
   };
   int _proinf[_num];
};

这样的操作是被允许的

inline代替宏函数

inline可以代替宏函数,好处在这里不进行说明了~

对于单纯的常量,最好用const和enum代替它
对于宏函数,用inline代替它

尽可能使用const

对于const修饰指针前后的问题较为简单,这里主要写一个新场景

给函数的返回值加const:

const Rational operator* (const Rational& lhs, const Rational& rhs);

这个函数返回了一个Rational类型的值,这是已经定义好的,问题在于这里为什么前面要加const?

其实是为了防止这样情况出现

Rational a, b, c;
...
(a*b) = c;

显然如果有了const,这里就会报错,因为常量是不能被修改的,这里也很好的说明了const的功能,在有些地方可以很好的避免出现奇怪的错误

const成员函数

这里涉及一些简单的权限问题,const是常量,而常量不能调用普通成员函数,这涉及到权限的扩大,因此只能调用const成员函数

const如果作为成员函数,那么就会引入两个概念,bitwise constness和logical constness,分别译为二进制位常量性和逻辑常量性

bitwise constness:
这个概念的意思是说,不改变对象内的任何一个bit,就是const,这也正是C++对常量性的定义,因此const成员函数不可以更改对象内任何non-static成员变量

但这个概念是有些问题的,假设类中的成员是一个指针,而这个指针也参与到了const成员函数中,返回的是引用

class CTextBlock
{
    
    
public:
	CTextBlock( char* p)
	{
    
    
		pText = p;
	}
	char& operator[](std::size_t position) const
	{
    
    
		return pText[position];
	}
private:
	char* pText;
};

int main()
{
    
    
	char str[] = "hello";
	const CTextBlock cctb(str);
	char* pc = &cctb[0];
	*pc = 'j';
	cout << str;
	return 0;
}

因此对于外界来说,你创建了一个常量对象并给了值,而且也只对它用const成员函数,但最后却能通过指针改变这当中的内容,于是就引出了logical constness

logical constness:
这个概念是说,一个const成员函数可以修改它所处理的对象内的某些bits,但只有在客户端侦测不到的地方才能这样

于是引入了mutable的概念,这个关键字可以作用在类的成员内,使得它们可以在const函数中被修改,而不是意味着规定const函数内的成员都不能被有任何bits的更改,事实上也不能完全保证

在const和non-const成员函数中避免重复

简单来说,这个修改程序的思想就是对于const成员函数和普通成员函数且这两个函数构成重载,如果两个函数实现的功能基本相同,那么就可以让普通成员函数通过一些加const和摘除const的方式,也用const函数完成函数本身的功能,这样的好处在于避免代码重复,但要时刻理解保持权限的概念

将某些东西声明为const可以帮助编译器侦测出错误的用法
const可以被施加于任何作用域内的对象,函数参数,函数返回类型,成员函数本体
编译器本身是强制进行bitwise constness,但是可以使用logical constness
当const和non-const成员函数有实质性的等价实现时,可以避免代码重复

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

这里提及的大部分是前面的一些基础内容:

为内置类型进行手工初始化,编译器不会对内置类型初始化
构造函数最好用参数初始化表,最好不用赋值,参数初始化表的顺序和private中相同
为避免跨编译单元之初始化次序的问题,要用local static代替non-local static对象

下面重点介绍:跨编译单元之初始化次序是什么

首先,要先知道extern这个关键字是干什么的

  1. 如果定义于函数,那么可以使b.cpp中用a.cpp的函数进行使用
  2. 如果定义于变量,可以使b.cpp中用a.cpp定义的变量

因为有上面的概念,因此才会出现跨编译单元的问题

现在我们利用上面的第二条来看问题,现在假定,在a.cpp文件中我们定义了变量并用extern修饰,而在b.cpp文件中直接对它进行使用了,那么问题来了,如果初始化的次序并非先a.cpp,再b.cpp,那岂不是就进行了一次使用未初始化的内容?这是一个有风险的操作

解决方案是,把non-local static对象搬到自己的专属函数内,就可以解决这个问题,原因在于,C++保证,函数内的local static对象会在该函数被调用的期间,首次遇到该对象的定义式的时候进行初始化,这意味着只要你走到这个static对象,这个对象其实是必然被初始化过的,因此用这样的解决方案,可以以函数调用的形式,保证你获得的那个引用一定是指向的被初始化过的内容

猜你喜欢

转载自blog.csdn.net/qq_73899585/article/details/131999938
今日推荐