C++11 语言新特性
本文只是罗列 C++11 一些常见的,但不是很复杂的特性,所以就没展开篇幅讲。其他比较复杂的 C++11 特性,具体见 C++11 相关特性汇总。
1. nullptr 和 std::nullptr_t
- C++11 允许你使用 nullptr 取代 0或NULL
- 这个新特性特别能够帮助你在 “NULL 指针被解释为一个整数值”时避免误解
- nullptr 是个新关键字,它被 自动转换为各种指针类型,但不会被转换为任何整数类型
例如:
void f(int);
void f(void*);
f(0); // call f(int)
f(NULL); // call f(int) if NULL is 0
f(nullptr); // call f(void*)
2. 一致性初始化与初值列
一致性初始化
,面对任何初始化动作你可以使用相同语法,也就是使用大括号,例如:
int values[] {1, 2, 3, 4, 5};
std::vector<int> v {1, 2, 3, 4, 5};
std::vector<std::string> cities {"beijing", "shanghai"};
初值列
会强迫造成所谓值初始化,意思是即使某个 local 变量属于某种基础类型(通常会有不明确的初值)也会被初始化为0或 nullptr
int i; // i 可能是个随机值
int j{}; // j 被初始化成 0
int* p; // p 可能是个随机值
int* q{}; // q 会被初始化成 nullptr
bool b{}; // b 被初始化成 0
- 支持“用户自定义类型之初值列”的概念,提供 std::initializer_list<>,用来支持以一系列值进行的初始化,或者想处理一系列的值,例如:
void print(std::initializer_list<int> vals) {
for (auto v : vals) {
std::cout << v << std::endl;
}
}
print({ 1,2,3,4,4,5,6,7 });
3. range-based for 循环
- 一种崭新的 for 循环形式,可以逐一迭代某个给定的区间、数组、集合内的每一个元素,一般语法如下:
// decl是给定的 coll 集合中的每个元素的声明,针对这些元素,给定的 statement 会被执行。
for ( decl : coll ) {
statement
}
// 常规遍历
for (int i : {1, 2, 3, 4, 5}) {
std::cout << i << std::endl;
}
// 修改元素值,vector每个元素乘以3
std::vector<double> vecD;
...
for (auto& elem : vecD) {
elem *= 3;
}
// 建议的写法,可以避免调用每个元素的拷贝构造函数和析构函数
template <typename T>
void PrintElements(const T& coll) {
for (const auto& elem : coll) {
std::cout << elem << std::endl;
}
}
4. 新式的字符串字面量
- 原始字符串(raw string),以 R("开头,以")结尾,可以内包含line break,例如:
// 常规,两个反斜杠和一个n
"\\\\n"
// raw string
R("\\n")
- 原始字符串对于定义正则表达式特别有用
- 编码前缀,可以给字符串字面量定义一个特殊的字符编码前缀,如下:
// u8 定义一个 UTF-8 编码,字符串类型为 const char[N]
auto u8 = u8"你好";
// u 定义一个 UTF-16 编码,字符串类型为 const char16_t[N]
auto u = u"你好";
// U 定义一个 UTF-32 编码,字符串类型为 const char16_t[N]
auto U = U"你好";
// L 定义一个宽字符串字面量,字符串类型为 const wchar_t[N]
auto L = L"你好";
5. 关键字 noexcept
- C++11 提供了关键字noexcept,用来指明某个函数 无法 或 不打算 抛出异常,例如:
void foo() noexcept;
- 可以在 noexcept(...) 中指定一个Boolean条件,若符合条件就不抛出异常,通常指明使用 noexcept 而不带条件,其实就是 noexcept(true) 的一个简洁形式
6. 关键字 constexpr
- C++11 ,可以使用 constexpr 让
表达式
的结果确定在代码编译过程中,例如
constexpr int square(int x) {
return x*x;
}
float a[square(9)]; // a 有81个元素,在代码编译过程中就计算好了
7. 关键字 final
C++ 中增加了 final 关键字来限制某个类不能被继承,或者某个虚函数不能被重写。如果使用 final 修饰函数,只能修饰虚函数,并且要把final关键字放到类或者函数的后面。
class A
{
public:
A() {};
~A() {};
virtual void echo() {
std::cout << "Hello, i am A!" << std::endl;
}
};
class B final : public A
{
public:
B() {};
~B() {};
void echo() final {
std::cout << "Hello, i am B!" << std::endl;
}
};
// ERROR: 'C': cannot inherit from 'B' as it has been declared as 'final'
class C : public B
{
public:
C() {};
~C() {};
// ERROR: 'B::echo': function declared as 'final' cannot be overridden by
void echo() {
std::cout << "Hello, i am C!" << std::endl;
}
};
8. 关键字 override
override 关键字确保在派生类中声明的重写函数
与基类的虚函数
有相同的签名,同时也明确表明将会重写基类的虚函数,这样就可以保证重写的虚函数的正确性,也提高了代码的可读性,该关键字要写到方法的后面。使用方法如下:
class A
{
public:
A() {};
~A() {};
void sayHi() {
std::cout << "Hi, i am A!" << std::endl;
}
virtual void echo(int a) {
std::cout << "Hello, i am A!" << std::endl;
}
};
class B : public A
{
public:
B() {};
~B() {};
// ERROR: 'B::echo': method with override specifier 'override' did not override any base class methods
// 将会在编译期间检查重写虚函数类型是否正确
void echo() override {
std::cout << "Hello, i am B!" << std::endl;
}
// ERROR: 'B::sayHi': method with override specifier 'override' did not override any base class methods
// 只可以覆盖虚函数
void sayHi() override {
std::cout << "Hi, i am B!" << std::endl;
}
// OK
void echo(int a) override {
std::cout << "Hello, i am B!" << std::endl;
}
};
9. 关键字 using
通常我们使用 using 来引入一个命名空间使用,在 C++11,赋予了它额外的能力,就是定义类型的别名。
之前我们要重定义一个类型的别名,使用的是 typedef,例如:
typedef 旧的类型名 新的类型名;
// 使用举例
typedef std::vector<int> vecNumber;
// 结构体
typdef struct _A {
int aNumber;
}A, *PA;
// 枚举值
typedef enum _enumB {
enumNone = 0,
}enumB;
使用 using 定义别名的语法格式是这样的:
using 新的类型名 = 旧的类型名;
// 使用举例
using uint_t = int;
using vecNumber = std::vector<int>;
过去我们想将 typedef 和 模板(template)
结合使用,不是那么特别方便:
template <typename T>
typedef std::vector<T> vecT; // ERROR: a typedef template is illegal
// 不得不使用 class 来包装一下
#include <vector>
template <typename T>
class A
{
public:
A() {};
~A() {};
typedef std::vector<T> vecT;
private:
vecT m_vecT;
};
现在,使用 using 关键字可以直接做到,例如:
#include <vector>
template <typename T>
using vecT = std::vector<T>;
int main(void)
{
vecT<std::string> vecStr{ "Hello using keyword!" };
vecT<std::uint16_t> vecNum{ 1,2,3,4,5 };
return 0;
}
总结一下:
- using 语法和 typedef 目的是一样的,只是给某些类型定义了新的别名,但是并不会创建出新的类型。
- using 相较于 typedef 的优势在于定义函数指针别名时看起来更加直观,并且可以给模板定义别名。
10. 关键字 explicit
explicit 主要用于防止隐式转换,用于修饰构造函数、复制构造函数。
先看个例子:
class A
{
public:
A() {};
A(int nValue) :m_nValue(nValue) {};
A(const A& other) { m_nValue = other.m_nValue; };
~A() {};
private:
int m_nValue{ 0 };
};
int main(void)
{
A a = 1; // OK call A(int nValue)
A b = a; // OK call A(const A& other)
return 0;
}
上述的代码,A a = 1 和 A b =a 就是是隐式调用,它是自动完成的。有时候这么写可能看上去很奇怪,直接给一个对象赋值一个数字。如果想禁止这种隐式调用,explicit 关键字就派上了用场,该关键字使用方法,是放在类成员函数的前面,例如:
class A
{
public:
A() {};
explicit A(int nValue) :m_nValue(nValue) {};
explicit A(const A& other) { m_nValue = other.m_nValue; };
~A() {};
private:
int m_nValue{ 0 };
};
int main(void)
{
A a(1); // OK
A a = 1; // ERROR: 'initializing': cannot convert from 'int' to 'A', Constructor for class 'A' is declared 'explicit'
A b(a); // OK
A b = a; // ERROR: 'initializing': cannot convert from 'A' to 'A', Constructor for class 'A' is declared 'explicit'
return 0;
}
11. Template 新特性
- 自从 C++11 起, template 可以拥有 “接受个数不定的 template 实参”,称之为 variadic template。例如:
void print()
{
std::cout << "the end!!" << std::endl;
}
template <typename T, typename... Types>
void print (const T& firstArg, const Types&... args)
{
std::cout << firstArg << std::endl; // 打印第一个参数
print(args...); // 递归调用
}
// 调用
print(1, 1.2, "hello template");
如果传入1个或多个实参,上述的函数就会被调用,它会把第一实参区分开,允许第一个实参被打印,然后递归调用 print() 并传入其余实参,必须提供一个 非模板重载函数 print(),才能使得整个递归动作结束。
具体模板还有很多特性,目前模板用的比较少,用到的时候再补充:
- 非类型模板参数(Nonetype Template Parameter)
- 模板参数默认值 (Default Template Parameter)
- 关键字 typename 的新用法
- 类成员模板 (Member Template)
12. 带作用域的 Enumeration 类型
在过去,我们要全局定义枚举值,若名字相同便会产生冲突报错:
enum A1
{
enum_test = 0,
};
enum B1
{
enum_test = 0, // ERROR: 'enum_test': redefinition; previous definition was 'enumerator'
};
定义的枚举值还会被隐式转换为 int:
enum my_enum
{
enum0 = 0,
enum1,
};
int nNumber = enum0;
C++11 提供了一种 带作用域的 Enumeration 类型,其与老的方式的不同是,在 enum 之后指明关键字 class,例如:
enum class Car : char { a, b, c, d }; // a 默认等于 0,b = a+1,和之前一样
在使用的时候,也有变化,须写明作用域,如 Car::a
这个特性有如下好处:
- 绝对不会隐式转换至 int
- 可以明确定义底层类型,如上例子中的
char
并因此获得一个保证的大小(如果这里省略掉 “: char”,默认类型是 int)