1 让自己习惯C++

01: 语言联邦 (View C++ as a federation of languages)

如今的C++,抛开C++11、C++17等新特性暂且不谈,称得上是一门多重范型编程语言,它包括:

  • 面向过程(C语言)
  • 面向对象(Class)
  • 泛型(模板)
  • 元编程(用程序生成程序)
  • 函数式(非冯诺伊曼式)

前三个是C++的典型特性。
根据C++的语法特性,可以拆分成四门次语言,每门次语言都有自己的一套规则:

次语言C++语法
C语言作用域,预处理器,内置数据类型,数组,指针
面向对象封装,继承,多态(包括静态编译时多态和virtual虚函数动态绑定)
模板泛型编程(让强类型语言有一定的自由性),这个部分是大多数程序员经验最少的部分
STL容器,迭代器,算法,函数对象

02: 尽量用const、enum、inline代替 #define

#define定义的内容属于预处理器
const、enum、inline定义的内容属于编译器
这句话相当于“宁可使用编译器,也不要使用预处理器”。
假设有如下定义:

#define SOME_RATIO 1.653

如果有关于SOME_RATIO的编译错误信息,错误信息可能只会提到1.653,而不是变量名,这样不好定位错误所在。
下面就三种情况来讨论 #defineconst, enum, inline 替换的方法:

1. 作用域

#define 定义的变量的作用域在全局(除非被 #undef取消定义),而 const 可以定义 class 专属的常量。

// 头文件 GamePlayer.h 内
class GamePlayer {
private:
	static const int NumTurns = 5;		// 定义类的专属常量,定义式
	static const int MaxTurns;			// 声明类的专属常量,声明式
	int scores[NumTurns];				// 调用常量
};
// 实现文件 GamePlayer.cpp 内
const int GamePlayer::MaxTurns = 15;	// 定义类的专属常量,声明式
2. 无法取地址的常量

用枚举 enum 定义常量,称为 enum hack

// 头文件 GamePlayer.h 内
class GamePlayer {
private:
	enum { NumTurns = 5 };				// enum hack ! 生成了 5 的记号表
	int scores[NumTurns];				// enum 的地址是无法获取的,#define的地址通常也无法获取
};
3. 函数宏定义

函数宏定义即便使用如下带括号的参数,也存在不少问题。

#define FUNC(a, b) f((a) > (b) ? (a) : (b))

可以用 template inline 函数解决,避免使用宏定义可能出现的问题。

template<typename T>
inline void FUNC(const T& a, cosnt T& b) {
	f(a > b ? a : b);
}
4. 小结

constenuminline 的出现是我们对预处理器的需求降低了,但编译控制 #ifdef / #ifndef 、头文件包括 include 在编译中仍然有重要作用,只不过最好多让预处理器放放假。

  • 对于常量,可用 const 或者 enum 代替 #define
  • 对于函数,可用 template inline 代替 #define

03 尽可能使用 const (Use const whenever possible.)

const 既能修饰指针,又能修饰指针所指物。

char str[] = "Hello World!";
const char* p = str;			// 指针 non-const,所指物 const
char* const p = str;			// 指针 const,所指物 non-const
const char* const p = str;		// 指针 const,所指物 const
char const* p = str;			// 等价于第一种
  • const 在星号左边,被指物是常量
  • const 在星号右边,指针是常量
  • const 在星号两边都有,被指物和指针都是常量

迭代器和指针类似,const std::vector<int>::iterator ite; 相当于 T* const,迭代器是常量,不能指向其他元素;
std::vector<int>::const_iterator cIte; 相当于 const T*,被指元素是常量,迭代器可以用来遍历数组。

3.1 函数返回值为 const

令函数返回一个 const 常量,可以避免因客户错误造成的意外(预防一些不小心产生的错误,if 语句里的 == 错写成 =),比如下面的重载星号运算符。

class Num {...};
const Num operator* (const Num& lNum, const Num& rNum);
3.2 const 成员函数

使用 const 成员函数的作用:

  1. class 的接口更容易被区分,哪些可以改动对象,而哪些不行
  2. 使 操作 const 对象 成为可能,比如以传递对象的 const 引用的方式传递对象。

关于 const 成员函数的几个要点:
1)const 是函数类型的一部分,在实现部分也要带 const
2)const 关键字可以用于对重载函数的区分,改变成员函数的常量性可以实现重载
3)cosnt 成员函数只能调用 const 成员函数;不能更新类的成员变量,也不能调用该类的 non-cosnt 成员函数。
4)non-const 对象也可以调用 const 成员函数,但是如果有重载的 non-const 成员函数则会调用 non-const 成员函数

看下面的例子,重载 operator[] ,由于 TextBlock[] 可能出现在赋值符号左边,所以 non-const 重载函数必须返回const类型的引用

class TextBlock {
public:
	const char& operator[] (std::size_t position) const	// const 函数,返回值是 const 的引用
	{ return text[position]; }							// 只能用于'读操作'
	char& operator[] (std::size_t position)				// non-const 函数,和上个函数互为重载函数,必须返回引用
	{ return text[position]; }							// 可以用于'读操作'和'写操作'
private:
	std::string text;
};

const 成员函数禁止修改类内的任何一个 bit 位,而类的 static 成员变量存储在静态区,因此可以被 const 成员函数修改。另外,还可以通过 mutable 关键字,使 non-static 成员变量也可以被 const 成员函数修改。

class TextBlock {
public:
	std::size_t length() const;							// const 成员函数
	char& operator[] (std::size_t pos) const			// const 成员函数,*pText 所指物可能被修改
	{ return pText[pos]; }
private:
	char* pText;
	mutable std::size_t textLength;						// textLength 可以被 length() const 成员函数修改
};

仅仅保证在 const 函数中 class 内的任何一个 bit 位不被修改是不够的,在上面代码中,length() const 函数并没有保证逻辑上的 const 性,只保证了 bit 位的 const 性。因此有时需要结合 const 成员函数与 mutable 关键字共同保证两种 const性

3.3 避免 const 和 non-const 成员函数的代码重复

const 函数和 non-const 函数体内部可能有共同的边界检验、日志数据访问、检验数据完整性等操作,其中的代码重复引起的编译时间、维护、代码膨胀等问题会让人头痛。为了避免代码重复,存在这样的做法:令 non-const 函数调用 const 函数。这看上去是个骚操作,然而简单有效地避免了代码重复的问题,尽管这样的代码并不美观。

class TextBlock {
public:
	const char& operator[] (std::size_t pos) const
	{
		... // 边界检验、日志数据访问、检验数据完整性等操作
		return text[pos];
	}
	char& operator[] (std::size_t pos) 
	{
		return const_cast<char&>(						// 使用 const_cast 消除 const 属性
			static_cast<const TextBlock&>(*this)		// 使用 static_cast 为 *this 加上 const 属性,
				[position]								// 这样就会明确调用 operator[] () const 函数
		);
	}
};

注意,你可能会想为什么不令 const 函数去调用 non-const 函数呢?因为 non-const 函数体内可能修改了 class 内部的 bit,这时再用 const 函数调用 non-cast 函数是存在危险的。

3.4 小结

const 是个非常奇妙的东西,可用于指针和迭代器、指针和迭代器所指物、函数参数和返回类型、本地变量、成员函数,使用广泛,威力强大。

  • 尽可能使用 const 可以帮助编译器尽早检测错误用法,const 可被用于非常多的地方,指针、指针所指物、变量、函数参数、函数返回类型、成员函数本体
  • 尽管编译器强制实施了 bit const 性,但编写程序时还要注意逻辑上的 logical const 性
  • non-const 成员函数调用 const 成员函数可以避免代码重复

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

C++ 非常容易出错的问题就是初始化,由于变量未初始化引起的未知行为,产生莫名其妙的 BUG,会导致不可测的程序行为,以及令人不愉快的调试过程。这也是 C++ 和其他语言区别很大的一个地方,很多语言都不存在"无初值对象"。
比如,数组 [] 属于 C++ 中 C 的部分,初始化可能导致运行期成本(C 总是为了高效而生的),因此不保证初始化;
vector 属于 C++ 中 STL 的部分,它保证了 vector 的内容会被初始化 (可以说是为了减少错误而生)。
而对于除了内置类型之外的任何其他东西,可以简单地遵循一个规则:确保每个构造函数都将对象的每一个成员初始化,因为其他东西总是通过构造函数进行初始化。

4.1 构造函数的赋值和初始化

萌新程序员很容易写出这样的代码:

class PhoneNumber { ... };
class ABEntry {
public:
	ABEntry(const std::string& name, const std::string& address,
		const std::list<PhoneNumber>& phones)
	{
		m_Name = name;							// 这些都是赋值操作,并不是初始化
		m_Address = address;
		m_Phones = phones;
		m_numTimesConsulted = 0;
	}
private:
	std::string m_Name;
	std::string m_Address;
	std::list<PhoneNumber> m_Phones;
	int m_numTimesConsulted;
};

构造函数比较好的写法如下,使用成员初始化列表

ABEntry::ABEntry(const std::string& name, const std::string& address,
	const std::list<PhoneNumber>& phones) : m_Name(name),	// 初始化列表
	m_Address(address),
	m_Phones(phones),
	m_numTimesConsulted(0)
{}

或者也可以写成默认构造函数,建议将所有成员变量都写进初始化列表内,以免遗漏:

ABEntry::ABEntry()
	: m_Name(),					// class 类型会调用其默认构造函数
	m_Address(),				// 同上
	m_Phones(),					// 同上
	m_numTimesConsulted(0)		// 基础类型需要初始化
4.2 构造函数初始化的顺序
  1. 如果存在继承关系,先调用基类的构造函数,在调用派生类的构造函数;
  2. class 中,成员变量的初始化顺序和它的声明次序相同,因此在写初始化列表时最好也保持顺序一致

而对于 non-local static 对象(比如声明在某个文件内、namespace内),会出现不一样的问题。
假设有两个编译单元(可以看作两个不同的 .cpp 及其 .h 文件),
在 “A.cpp” 文件中声明了 Class A,并且有 non-loacl static 对象的声明: extern A aaa;
在 “B.cpp” 文件中声明了 Class BB bbb; 的构造函数想要调用 aaa 这个 non-local static 对象。
问题出现,C++ 是无法决定应该先调用 aaa 的构造函数还是 bbb 的构造函数。如果不幸,bbb 的构造函数调用前,aaa 的构造函数从未被调用过,灾难就发生了。
用一个简单的设计便可消除这个问题,
将每个 non-local static 对象都放入自己 Class 的某个专属函数内,并且这个专属函数返回的是 non-local static 对象的引用。用户间接地调用专属函数,而不直接访问 non-local static 对象。(有人可能灵光一闪,这不就是 Singleton 单例模式吗!)
这种设计不仅解决了 non-local static 对象的初始化顺序问题,而且如果该 non-local static 对象从未被调用过,其构造和析构函数也绝不会调用,也就是说减少了构造析构的成本,天下竟有如此神奇的好事。
aaabbb 使用该设计(暂且不考虑线程安全性和其他问题):

class A {
public:
	A() {}
	int getCount() { return 3; }
	A& getAaa()										// 通过专属函数才能获得 aaa 这个 non-local static 对象
	{
		static A aaa;
		return aaa;
	}
}; 
class B {
	B() { int n = aaa.getCount(); }					// 万事大吉,声明 B bbb; 时不用再愁初始化顺序的问题了 
};
4.3 小结

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

  • 对于内置基础类型,要手动初始化,因为 C++ 不保证初始化它们
  • 构造函数最好使用初始化列表的形式
  • 为了解决不同编译单元的初始化次序问题,用类似单例模式的设计方法
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值