2 如何写出一个好类
2.1final
C++11 新增了一个特殊的标识符“final”(注意,它不是关键字),把它用于类定义,就可以显式地禁用继承,防止其他人有意或者无意地产生派生类。无论是对人还是对编译器,效果都非常好,我建议你一定要积极使用。
class DemoClass final // 禁止任何人继承我
{
...
};
在必须使用继承的场合,建议你只使用 public 继承,避免使用 virtual、protected,因为它们会让父类与子类的关系变得难以捉摸,带来很多麻烦。当到达继承体系底层时,也要及时使用“final”,终止继承关系。
class Interface // 接口类定义,没有final,可以被继承
{ ... };
class Implement final : // 实现类,final禁止再被继承
public Interface // 只用public继承
{ ... };
2.2 default
C++ 里类的四大函数你一定知道吧,它们是构造函数、析构函数、拷贝构造函数、拷贝赋值函数。C++11 因为引入了右值(Rvalue)和转移(Move),又多出了两大函数:转移构造函数和转移赋值函数。
所以,在现代 C++ 里,一个类总是会有六大基本函数:三个构造、两个赋值、一个析构。好在 C++ 编译器会自动为我们生成这些函数的默认实现,省去我们重复编写的时间和精力。但我建议,对于比较重要的构造函数和析构函数,应该用“= default”的形式,明确地告诉编译器(和代码阅读者):“应该实现这个函数,但我不想自己写。”这样编译器就得到了明确的指示,可以做更好的优化。
class DemoClass final
{
public:
DemoClass() = default; // 明确告诉编译器,使用默认实现
~DemoClass() = default; // 明确告诉编译器,使用默认实现
};
这种“= default”是 C++11 新增的专门用于六大基本函数的用法,相似的,还有一种“= delete”的形式。它表示明确地禁用某个函数形式,而且不限于构造 / 析构,可以用于任何函数(成员函数、自由函数)。
比如说,如果你想要禁止对象拷贝,就可以用这种语法显式地把拷贝构造和拷贝赋值“delete”掉,让外界无法调用。
class DemoClass final
{
public:
DemoClass(const DemoClass&) = delete; // 禁止拷贝构造
DemoClass& operator=(const DemoClass&) = delete; // 禁止拷贝赋值
};
因为 C++ 有隐式构造和隐式转型的规则,如果你的类里有单参数的构造函数,或者是转型操作符函数,为了防止意外的类型转换,保证安全,就要使用“explicit”将这些函数标记为“显式”。
class DemoClass final
{
public:
explicit DemoClass(const string_type& str) // 显式单参构造函数
{ ... }
explicit operator bool() // 显式转型为bool
{ ... }
};
2.3 常用技巧
C++11 里还有很多能够让类更优雅的新特性,这里我从“投入产出比”的角度出发,挑出了三个我最喜欢的特性,给你介绍一下,让你不用花太多力气就能很好地改善代码质量。
2.3.1m 第一个是“委托构造”(delegating constructor)。
如果你的类有多个不同形式的构造函数,为了初始化成员肯定会有大量的重复代码。为了避免重复,常见的做法是把公共的部分提取出来,放到一个 init() 函数里,然后构造函数再去调用。这种方法虽然可行,但效率和可读性较差,毕竟 init() 不是真正的构造函数。
在 C++11 里,你就可以使用“委托构造”的新特性,一个构造函数直接调用另一个构造函数,把构造工作“委托”出去,既简单又高效。
class DemoDelegating final
{
private:
int a; // 成员变量
public:
DemoDelegating(int x) : a(x) // 基本的构造函数
{}
DemoDelegating() : // 无参数的构造函数
DemoDelegating(0) // 给出默认值,委托给第一个构造函数
{}
DemoDelegating(const string& s) : // 字符串参数构造函数
DemoDelegating(stoi(s)) // 转换成整数,再委托给第一个构造函数
{}
};
2.3.2. 第二个是“成员变量初始化”(In-class member initializer)。
如果你的类有很多成员变量,那么在写构造函数的时候就比较麻烦,必须写出一长串的名字来逐个初始化,不仅不美观,更危险的是,容易“手抖”,遗漏成员,造成未初始化的隐患。
而在 C++11 里,你可以在类里声明变量的同时给它赋值,实现初始化,这样不但简单清晰,也消除了隐患。
class DemoInit final // 有很多成员变量的类
{
private:
int a = 0; // 整数成员,赋值初始化
string s = "hello"; // 字符串成员,赋值初始化
vector<int> v{1, 2, 3}; // 容器成员,使用花括号的初始化列表
public:
DemoInit() = default; // 默认构造函数
~DemoInit() = default; // 默认析构函数
public:
DemoInit(int x) : a(x) {} // 可以单独初始化成员,其他用默认值
};
2.3.3. 第三个是“类型别名”(Type Alias)。
C++11 扩展了关键字 using 的用法,增加了 typedef 的能力,可以定义类型别名。它的格式与 typedef 正好相反,别名在左边,原名在右边,是标准的赋值形式,所以易写易读。
using uint_t = unsigned int; // using别名
typedef unsigned int uint_t; // 等价的typedef
在写类的时候,我们经常会用到很多外部类型,比如标准库里的 string、vector,还有其他的第三方库和自定义类型。这些名字通常都很长(特别是带上名字空间、模板参数),书写起来很不方便,这个时候我们就可以在类里面用 using 给它们起别名,不仅简化了名字,同时还能增强可读性。
class DemoClass final
{
public:
using this_type = DemoClass; // 给自己也起个别名
using kafka_conf_type = KafkaConfig; // 外部类起别名
public:
using string_type = std::string; // 字符串类型别名
using uint32_type = uint32_t; // 整数类型别名
using set_type = std::set<int>; // 集合类型别名
using vector_type = std::vector<std::string>;// 容器类型别名
private:
string_type m_name = "tom"; // 使用类型别名声明变量
uint32_type m_age = 23; // 使用类型别名声明变量
set_type m_books; // 使用类型别名声明变量
private:
kafka_conf_type m_conf; // 使用类型别名声明变量
};
类型别名不仅能够让代码规范整齐,而且因为引入了这个“语法层面的宏定义”,将来在维护时还可以随意改换成其他的类型。比如,把字符串改成 string_view(C++17 里的字符串只读视图),把集合类型改成 unordered_set,只要变动别名定义就行了,原代码不需要做任何改动。