final标识符
C++11新增final标识符,把final用于类定义,就可以显示地禁用继承,防止有人有意或者无意创建派生类,这个标识符无论对人还是编译器,效果都很好,建议积极使用。例如:
class DemoClass final //禁止任何人继承
{...}
final也可以用于虚函数,禁止这个虚函数再被子类重载,这样就可以更精细地控制继承类的使用:
class Interface //接口类定义,没有final,可以被继承
{
virtual void f() = 0; //纯虚接口,没有final,可以被子类继承
...
};
class Abstract : public Interface //抽象类,没有final,可以被继承
{
virtual void f() final //虚函数,有final,禁止子类重载
{....}
};
在必须使用继承的场合,建议只使用public,当到达继承体系底层时,及时使用final,终止继承关系。
class Implement final : //实现类,final禁止其被继承
public Abstract
{...}
default/delete 函数
对于比较重要的改造函数和析构函数,应该用"=default"的形式明确地告诉编译器(和代码阅读者):“应该实现这个函数,但我不想自己写”。这样编译器就得到了明确的指示,可以更好地进行优化。
class DemoClass final
{
public:
DemoClass() = default;
~DemoClass() = default; //明确告诉编译器,使用默认实现
DemoClass(...) {...};
DemoClass(...) {...}; //其他形式的构造函数,不用默认实现
};
default函数只是简化了默认构造函数/析构函数的写法,我们仍然可以写出其他形式的构造/析构函数,所以它们的作用相当于为类提供了一个"保底"实现。default主要用于构造、赋值、析构。
与之相似的,"=delete"它表示明确地禁用某个函数形式,可以用于类内的任何函数。
class DemoClass final
{
public:
DemoClass(const DemoClass&) = delete; //禁止构造函数
DemoClass& operator=(const DemoClass) = delete; //禁止赋值函数
};
explicit函数
C++有隐式构造和隐式转型的规则,如果类里有单参数的构造函数,或者是转型操作符函数,为了防止意外的类型转换,保证安全,最好使用explicit将这些函数标记为"显示"。
class DemoClass final
{
public:
explicit DemoClass(const string_type& str) //显式单参数构造函数
{ ... }
explicit operator bool() //显示转型为bool
{ ... }
};
DemoClass obj = "string ctor"; //错误,不能隐式转换
bool b = obj; //错误,不能隐式转换
DemoClass obj = (DemoClass)"string ctor"; //正确,能显式转换
bool b = static_cast<bool>(obj); //正确的显示转换
if(obj) {...}
委托构造
如果类有多个不同显示的构造函数,为了初始化,成员肯定会有大量的重复代码。为避免重复,常见做法是把公共部分提取出来,放入init()函数里,然后用构造函数去调用,这种方法虽然可行,但效率和可读性差。
现在我们可以使用委托构造的新特性,即在一个构造函数中直接调用另一个构造函数,把构造工作委托出去,既简单又高效
class DemoClass final
{
public:
DemoClass(int x) : a(x)
{}
DemoClass() : DemoClass(0)
{}
explicit DemoClass(const std::string& str)
:DemoClass(stoi(s))
{}
explicit operator bool()
{
return true;
}
private:
int a; //成员变量
};
成员变量初始化
如果类有很多成员变量,那么在写构造函数时候比较麻烦,不仅不美观,还可能出错。
在现代C++里,我们可以在类里声明变量的同时给它赋值,实现初始化,这样不但清晰,也能消除隐患。
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) {} //也可以单独初始化成员变量,其他未被初始化的成员变量用默认值
};
静态成员变量初始化
对于类的静态成员变量来说,初始化有其特殊性。
如果是const static,C++允许直接在声明的时候初始化,如果是非const的静态成员变量,则要求必须在实现文件里,也就是"*.cpp"里单独再初始化(因为需要分配唯一的存储空间)。
class DemoInit final
{
public:
static const int x = 0;
static string prefix = "xx"; //无法编译
};
string DemoInit::prefix = "xx"; //必须在类外单独初始化
C++17的解决方案,加上一个inline内联关键字就可以,这样被称为内联变量:
class DemoInit final
{
public:
inline static string prefix = "xx"; //在C++17编译正常
}
使用内联变量,C++就可以保证:无论这个头文件被包含多少次,静态成员变量也只有唯一的一个全局实例。
类型别名
C++扩展了using 的用法,使它具备了typedef的能力,可以定义类型别名。
using uint_t = unsigned int; //使用using定义别名
typedef unsigned int uint_t; //等价的typedef语句
类中使用示例:
class DemoClass final
{
public:
using this_type = DemoClass; //给类本身起别名
using kafka_conf_type = KafkaConfig; //给外部类型起别名
public:
using string_type = std::string;
using uint32_tupe = 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_config;
}
auto关键字
auto的自动推导能力只能使用在初始化的场合,具体来说,就是赋值初始化或者使用花括号初始化,变量右边必须有一个表达式。
auto x = 0L; //自动推导为long
auto y = &x; //自动推导为long*
auto z {&x}; //自动推导为long*
auto err; //错误,没有赋值表达式,不知道什么类型
在类成员变量初始化时候,目前C++标准还不允许使用自动推导类型。
- auto总是推导出值类型,绝不会推导出引用类型
- auto可以附加const/volatile/*/&等类型修饰符,从而得到新的类型
- auto&&是特殊用法,总是推导出引用类型。
- auto一般会忽略掉顶层const,同时底层const会保留下来。设置一个类型为auto的引用时,初始值的顶层常量属性仍然保留
auto x = 10L; //auto 推导为long
auto &x1 = x; //auto 推导为long, x1是long&
auto *x2 = &x; //auto 推导为long, x1是long*
const auto &x3 = x; //auto 推导为long, x1是long*
auto x4 = &x3; //auto推导为const long*,x4是 const long*
auto &&x5 = x; //auto推导为long,x5是long&
auto就像一个占位符,它推导出的类型会因表达式和附带的修饰符而变化。如果不采用"auto&&"的形式,那么它最终推导出的结果绝对不会是引用,但可以是指针。
decltype关键字
decltype的形式很像函数,后面的圆括号里就是自带的、可用于计算类型的表达式(类似sizeof),其他方面就和auto一样,也能附加const/volatile/*/&来修饰。
因为它已经自带表达式了,所以不需要变量后面再有表达式了,也就是可以直接声明变量,不用赋值、初始化。
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与auto区别:decltype不仅能够推导出值类型,还能够推导出引用类型,也就是表达式的原始类型。
decltype(x2) x6 = x2; //x2是int&,x6是int&
auto x7 = x2; //x2是int&,x7是int
我们完全可以把decltype表达式看成一个真正的类型名,用在变量声明、函数参数/返回值、模板参数等任何类型能出现的地方——只不过这个类型是在编译阶段通过表达式"计算"得到的。
using int_ptr = decltype(&x); //int *
using int_ref = decltype(x)&; //int &
decltype(auto) c++14新增形式,这里的auto仍然起到了占位符的作用,从而机能精确推导类型,又能像auto一样便于使用:
int x = 0;
decltype(auto) x1 = (x); //推导为int&,因为(expr)是引用类型
decltype(auto) x2 = &x; //推导为int*
decltype(auto) x3 = x1; //推导为int&
使用auto
auto有一个“最佳实践”,就是用于"range-based for"(基于范围的for循环):不需要关心容器的元素类型、迭代器返回值、首末位置,就能非常轻松完成遍历操作。不过为了保证效率,最好使用"const auto&"或者"auto&",例如:
vector<int> v = {2, 5, 7, 9, 11}; //vector顺序容器
for(const auto& i : v){
cout << i << ",";
}
for(auto& i : v){
i++;
cout << i << ",";
}
// C++14之后,可以推导函数返回值
auto get_a_set()
{
std::set<int> s = {1, 2, 3};
return s; //返回一个容器对象
}
// C++17,增加结构化绑定功能
tuple x{1, "x"s, 0.1}; //3个元素的元组
auto [a, b, c] = x;//结构化绑定,取出内部的元素
assert(a == 1 && b == "x");
使用decltype
可用于比较复杂的声明
void (*signal(int signo, void (*func)(int)))(int)
可以使用以下方式:
using sig_func_ptr_t = decltype(&signal);