1 生命周期和编程范式
C++生命周期包括:编码、预处理、编译和运行。尽量把BUG消除在前三个阶段。C++语言的五种范式:面向对象、泛型编程、函数式编程、模板元编程和面向过程。面向过程和面向对象是最基本的范式。
2 宏定义和条件编译
编译是预处理之后的阶段,输入的是经过预处理后的源码,输出是二进制可执行文件,可以用一些关键字或者指令改变编译器的行为。属性(attribute)就是给变量、函数、类等“贴”上一个编译阶段的“标签”,方便编译器识别处理。 C++11 里只定义了两个属性:“noreturn”和“carries_dependency”。C++14 增加了一个比较实用的属性“deprecated(废除的含义)”,用来标记不推荐使用的变量、函数或者类。
[[noreturn]] // 属性标签
int func(bool flag) // 函数绝不会返回任何值
{
throw std::runtime_error("XXX");
}
[[deprecated("deadline:2020-12-31")]] // C++14 or later,括号里的为编译标签
int old_func();
静态断言(static_assert)就是在编译阶段写程序,与动态断言assert不同的是,static_assert是一个关键字,而不是一个宏,且只在编译时生效。
//静态断言,传入的模板参数必须大于0
template<int N>
struct fib
{
static_assert(N >= 0, "N >= 0");
static const int value =
fib<N - 1>::value + fib<N - 2>::value;
};
//在泛型编程的时候,结合标准库里的“type_traits”来判断模板参数T的类型
static_assert(
is_integral<T>::value, "int");
static_assert(
is_pointer<T>::value, "ptr");
static_assert(
is_default_constructible<T>::value, "constructible");
static_assert 运行在编译阶段,只能看到编译时的常数和类型,看不到运行时的变量、指针、内存数据等,是“静态”的。
//错误用法
char* p = nullptr;
static_assert(p == nullptr, "some error."); // 错误用法
3 C++设计好的类的原则:① 思想上抽象与封装;②实现上少用继承(继承层数不要超过两层)和虚函数,类尽量简单、“短小精悍”,只负责单一的功能;③避免使用嵌套类,应该定义一个新的名字空间,把内部类都“提”到外面,降低原来类的耦合度和复杂度。
编码准则:①积极使用“final”关键字,显式地禁用继承,防止其他人有意或者无意地产生派生类。
class DemoClass final // 禁止任何人继承我
{ ... };
② 在必须使用继承的场合,建议只使用 public 继承,避免使用 virtual、protected。当到达继承体系底层时,要及时使用“final”,终止继承关系。
class Interface // 接口类定义,没有final,可以被继承
{ ... };
class Implement final : // 实现类,final禁止再被继承
public Interface // 只用public继承
{ ... };
③ 现代C++六大基本函数中比较重要的构造函数和析构函数,应该用“= default”的形式,明确地告诉编译器自己实现和优化。想要显示禁用六大函数则应使用“= delete”。
class DemoClass final
{
public:
DemoClass() = default; // 明确告诉编译器,使用默认实现
~DemoClass() = default; // 明确告诉编译器,使用默认实现
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
{ ... }
};
⑤ 为了初始化成员,避免写init()函数,使用委托构造。委托构造就是一个构造函数直接调用另一个构造函数。
class DemoDelegating final
{
private:
int a; // 成员变量
public:
DemoDelegating(int x) : a(x) // 基本的构造函数
{}
DemoDelegating() : // 无参数的构造函数
DemoDelegating(0) // 给出默认值,委托给第一个构造函数
{}
DemoDelegating(const string& s) : // 字符串参数构造函数
DemoDelegating(stoi(s)) // 转换成整数,再委托给第一个构造函数
{}
};
⑥ 成员变量初始化。在 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) {} // 可以单独初始化成员,其他用默认值
};
⑦ 类型别名。C++11 扩展了关键字 using 的用法,增加了 typedef 的能力,可以定义类型别名。
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; // 使用类型别名声明变量
};
4 自动类型推导
① auto。auto 总是推导出“值类型”,绝不会是“引用”;auto 可以附加上 const、volatile、*、& 这样的类型修饰符,得到新的类型。
auto x = 10L; // auto推导为long,x是long
auto& x1 = x; // auto推导为long,x1是long&
auto* x2 = &x; // auto推导为long,x2是long*
const auto& x3 = x; // auto推导为long,x3是const long&
auto x4 = &x3; // auto推导为const long*,x4是const long*
C++14 里,auto 还能够推导函数返回值。
auto get_a_set() // auto作为函数返回值的占位符
{
std::set<int> s = {1,2,3};
return s;
}
② decltype。decltype 不仅能够推导出值类型,还能够推导出引用类型,也就是表达式的“原始类型”。
int x = 0; // 整型变量
decltype(x) x1; // 推导为int,x1是int
decltype(x)& x2 = x; // 推导为int,x2是int&,引用必须赋值
decltype(x)* x3; // 推导为int,x3是int*
decltype(&x) x4; // 推导为int*,x4是int*
decltype(&x)* x5; // 推导为int*,x5是int**
decltype(x2) x6 = x2; // 推导为int&,x6是int&,引用必须赋值
C++14 增加了一个“decltype(auto)”的形式,既可以精确推导类型,又能像 auto 一样方便使用。
int x = 0; // 整型变量
decltype(auto) x1 = (x); // 推导为int&,因为(expr)是引用类型
decltype(auto) x2 = &x; // 推导为int*
decltype(auto) x3 = x1; // 推导为int&
定义类的时候,由于auto 被禁用了,可以使用 decltype 搭配别名任意定义类型,再应用到成员变量、成员函数上,变通地实现 auto 的功能。
class DemoClass final
{
public:
using set_type = std::set<int>; // 集合类型别名
private:
set_type m_set; // 使用别名定义成员变量
// 使用decltype计算表达式的类型,定义别名
using iter_type = decltype(m_set.begin());
iter_type m_pos; // 类型别名定义成员变量
};