C++ 11 新增了类型 long long 和 unsigned long long ,以支持 64 位(或更宽)的整型;新增了类型 char16_t 和 char32_t ,以支持 16 位和 32 位的字符表示;还新增了“原始字符串”。
C++ 11 扩大了用大括号括起的列表(初始化列表)的适用范围,使其可用于所有内置类型和用户定义的类型(即类对象)。使用初始化列表时,可添加等号,也可不添加:
// 常规类型
int x = {5};
double y {2.75};
short quar[5] {4, 5, 2, 76, 1};
int * ar = new int [4] {2, 4, 6, 7}; // new表达式
// 类
class Stump {
private:
int roots;
double weight;
public:
Stump(int r, double w) : roots(r), weight(w) {}
};
Stump s1(3, 15.6); // old style
Stump s2{5, 43.4}; // c++11
Stump s3 = {4, 32.1}; //c++11
// 缩窄转换
char c1 = 1.57e27; // double to char, allow but undefined behavior
char c2 = 459585821 // int to char, allow but undefined behavior
// 防止缩窄转换
char c1 {1.57e27}; // double to char, not allow
char c2 = {459568434}; // int to char, out of range, not allow
// 允许转换为更宽的类型
char c1 {66}; // int to char, in range, allow
double c2 = {66}; // int to double, allow
// 模板
vector<int> a1(10); // 未初始化的,大小为10的向量
vector<int> a2{10}; // 初始化列表,大小为1,值为10;
vector<int> a3{4, 6, 1}; // 初始化列表,大小为3,值为4 6 1
C++ 11 提供了多种简化声明的功能:
auto maton = 112; // 自动推断 int
auto pt = &maton; // int *
double fm(double, int);
auto pf = fm; // double (*)(double, int)
decltype(x) y; // 将y类型转换为x的类型
double x; int n;
decltype(x*n) q; // double
decltype(&x) pd; // double *
// 模板常用方法
template<typename T, typename U>
void ef(T t, U u) {
decltype(T*U) tu; // 模板实例化时确定类型
...
}
// 返回类型后置
double f1(double, int); // 传统语法
auto f2(double, int) -> double; // 新语法,返回double
auto eff(T t, U u) -> decltype(T*U); // 常规用法
// 模板别名
typedef std::vector<std::string>::iterator itType; // 传统别名,不能用于模板部分具体化
using itType = std::vector<std::string>::iterator; // C++ 11新方法,可以用于模板部分具体化
template<typename T>
using arr12 = std::array<T, 12>; // 模板部分具体化
// 空指针
nullptr <=> 0
如果在程序中使用 new 从堆分配内存,等到不再需要时,应使用 delete 释放,新增智能指针帮助自动化完成该过程。C++ 11 摒弃了 auto_ptr ,新增了 unique_ptr 、shared_ptr 、weak_ptr 。
用于指出函数可能引发哪些异常:
// 传统方法,因不好用被C++11摒弃
void f501(int) throw(bad_dog); // 抛出bad_dog异常
void f733(long long) throw(); // 不会引发异常
// 第二种情况可能有意义,因此新增关键字noexcept
void f875(short, short) noexcept; // 不会引发异常
传统枚举:
为解决这些问题,新增枚举:
// 旧枚举
enum old {yes, no, matbe};
// 新枚举,需要显示指定枚举成员作用域
enum class new1 {never, sometimes, often, always};
enum struct new2 {never, lever, sever};
禁止隐式转换运算符:
class Plebe {
operator int() const;
explicit operator double() const;
};
Plebe a, b;
int n = a; // 隐式转换为int
double x = b; // 隐式转换为double,not allow
x = double(b); // 显示转换为double,allow
类内成员初始化:
class Session {
int mem1 = 10; // 类内初始化
double mem2 {1966,.54}; // 类内初始化
short mem3;
public:
Session(){}
Session(short s) : mem3(s) {}
Session(int n, double d, short s) : mem1(n), mem2(d), mem3(s) {}
};
可以使用等号和大括号初始化,但不能使用圆括号。通过类内初始化可以避免在构造函数内编写重复代码。
基于范围的 for 循环:
vector<int> v(10);
for(int i : v)
cout << i << endl;
新增 STL 容器:
容器 | 作用 |
---|---|
forward_list | 单向链表 |
unordered_map | 无序 map |
unordered_multimap | 无序、可重复 map |
unordered_set | 无序 set |
unordered_multiset | 无需、可重复 set |
array | 数组 |
新增 STL 方法:
方法 | 说明 |
---|---|
cbegin() | 指向容器第一个元素,并将元素视为 const |
cend() | 指向容器最后一个元素的后面,并将元素视为 const |
对 valarray 进行升级,添加了 begin() 和 end() ,使之可以被迭代器访问,从而能够将基于范围的 STL 算法用于该容器。
摒弃 export ,原本作用为让程序员能够将模板定义放在接口文件和实现文件中,其中前者包含原型和模板声明,后者包含模板函数和方法的定义。
新增尖括号的识别,避免了与 >> 运算符混淆。
左值是一个表示数据的表达式(变量名、解除引用的指针),最初只能出现在赋值语句左边,但是随着 const 引入,也可以出现在赋值语句右边:
int n;
int * pt = new int;
const int b = 101; // 不能修改b
int & rn = n; // 引用n,即取到了n的地址
int & rt = *pt; // 取到了指针的地址
const int & rb = b; // 取到了b的地址,但不能修改rb
右值则是一个表示运算的表达式(不能对其赋值),不能应用取地址符,这种方式更像是创建并保存了一个表达式结果的副本,并引用了这个副本:
int x = 10;
int y = 23;
int && r1 = 13;
int && r2 = x + y; // r2关联运算的结果,因此改变x或y不影响r2
double && r3 = sqrt(2.0);
double *p = &r3; // 可以取到右值的地址
vector<string> vstr(20000, string(1000)); // 2w个string,每个string有1k个char
vector<string> vstr_copy1(vstr);
vector
和 string
类都使用动态内存分配,因此它们必须定义使用某种 new
版本的拷贝构造函数。为初始化对象 vstr_copy1
,拷贝构造函数 vector
将使用 new
给 2 万个 string
对象分配内存,而每个 string
对象又将调用 string
的拷贝构造函数,该构造函数使用 new
为 1 千个字符分配内存。接下来全部的字符都将从 vstr
控制的内存种复制到 vstr_copy1
控制的内存中。这样的工作量巨大,并且存在问题:
vector<string> allcaps(const vector<string> & vs) {
vector<string> tmp;
// todo: tmp存储了vs全大写版本的数据
return tmp;
}
vector<string> vstr(20000, string(1000));
vector<string> vstr_copy1(vstr); // 拷贝vstr
vector<string> vstr_copy2(allcaps(vstr)); // 创建tmp变量,函数运行结束时删除tmp变量,然后返回其它的副本,非常浪费资源
解决这个问题的方法为不删除 tmp 变量,而是转移所有权,该操作称为移动语义(move semantics),这样避免了删除和拷贝原数据。要实现它,需要让编译器直到何时需要复制,何时不需要,这就是右值发挥作用的地方。可以定义两个构造函数,一个是常规拷贝构造函数,使用 const 左值引用作为参数,这个引用关联到左值实参,如语句 1 的 vstr ;另一个是移动构造函数,使用右值引用作为参数,该引用关联到右值实参,如语句 2 种 allcaps 函数的返回值 tmp 。拷贝构造函数可执行深复制,而移动构造函数只负责调整记录(转换所有权)。由于移动构造函数可能修改其实参,因此右值引用不应是 const 。
Useless::Useless(Useless && f) : n(f.n) { // 此处的f可以是运算表达式
pc = f.pc; // pc是一个指针(私有成员),指向现有数据
f.pc = nullptr; // 转换了所有权
f.n = 0; // 转换了所有权后原来的对象的长度变为0
}
Useless four(one + three); // 传递一个对象运算表达式,这将生成临时对象,然后调用有移动构造函数
虽然右值引用可支持移动语义,但是需要执行两个步骤来执行。
Useless two = one; // 拷贝构造函数
Useless our(one + three); // 移动构造函数
对象 one 是左值,因此与左值引用匹配,调用拷贝构造函数;one + three 是右值,与右值引用匹配,调用移动构造函数。适用于构造函数的移动语义考虑也适用于赋值运算符,即移动赋值运算符:
Useless & Useless::operator=(Useless && f) {
pc = f.pc; // pc是一个指针(私有成员),指向现有数据
f.pc = nullptr; // 转换了所有权
f.n = 0; // 转换了所有权后原来的对象的长度变为0
return *this;
}
与上一种情况相同,由于修改了源对象,因此不能使用 const 引用。
上述两个例子使用了右值,如果要使用左值,就需要强制移动:
Useless choices[10];
Useless best = choices[pick]; // 这种方式会复制一个副本给best
由于 choices[pick]
是左值,因此上述赋值语句将使用复制赋值运算符,而不是移动赋值运算符,但如果能让它看起来像右值,便将使用移动赋值运算符。为此可使用运算符 static_cast<>
将对象的类型强制转换为 Useless &&
,但 C++ 11 在 utility
头文件中提供了一种更简单的方式 std::move
:
Chunk one, two;
two = move(one);
如果定义了移动赋值运算符,则调用移动赋值运算符;如果没有定义则调用复制赋值运算符;如果复制赋值运算符也没有定义,则不允许上述赋值。
C++ 11 在原有的 4 个特殊成员函数(默认构造函数、复制构造函数、复制赋值运算符和析构函数)的基础上,新增了移动构造函数和移动赋值运算符。这些成员函数是编译器在各种情况下自动提供的。但是存在一些例外:
class Someclass {
public:
Someclass(Someclass &&);
Someclass() = default;
Someclass(const Someclass &) = default;
Someclass & operator=(const Someclass &) = default;
...
};
class Someclass {
public:
Someclass() = default;
Someclass(const Someclass &) = delete; // 禁止使用拷贝构造函数
Someclass & operator=(const Someclass &) = delete; // 禁止使用复制赋值运算符
Someclass(Someclass &&) = default;
Someclass & operator=(Someclass &&) = default;
Someclass & operator+(const Someclass &) const;
...
};
Someclass one, two;
Someclass three(one); // not allow
Someclass four(one + two); // allow
如果需要给类提供多个构造函数,其中部分构造函数的代码需要重用,C++ 11 允许在一个构造函数的定义中使用另一个构造函数,这被称为委托构造函数:
class Notes {
int k;
double x;
string st;
public:
Notes();
Notes(int);
Notes(int, double);
Notes(int, double, string);
};
Notes::Notes(int kk, double xx, string stt) : k(kk), x(xx), st(stt) {}
Notes::Notes() : Notes(0, 0.01, "oh") {}
Notes::Notes(int kk) : Notes(kk, 0.01, "Ah") {}
Notes::Notes(int kk, double xx) : Notes(kk, xx, "Uh") {}
为了进一步简化编码工作,C++ 11 还提供了一种让派生类能够继承基类构造函数的机制。C++ 98 提供了一种让名称空间中函数可用的语法:
namespace Box {
int fn(int) {...}
int fn(double) {...}
int fn(const char *) {...}
}
using Box::fn; // 使函数fn的所有重载版本都可用
也可以使用这种方法让基类所有非特殊成员函数对派生类可用:
class C1 {
...
public:
...
int fn(int j) {...}
double fn(double w) {...}
void fn(const char * s) {...}
};
class C2 : public C1 {
...
public:
...
using C1::fn;
double fn(double) {...};
};
...
C2 c2;
int k = c2.fn(3); // 使用C1的方法
double z = c2.fn(2.4); // 使用C2的方法
C++ 11 将这种方法用于构造函数,这让派生类继承基类的所有构造函数(除默认构造函数、复制构造函数和移动构造函数),但不会使用与派生类构造函数的特征标匹配的构造函数(理解为重写,因此不调用父类的方法而是使用派生类自己的)。
假设基类声明了一个虚方法,而要在派生类中提供不同的版本,这将覆盖旧版本。但是由之前的讨论可知,如果特征标不匹配,将隐藏而不是覆盖旧版本:
class Action {
int a;
public:
Action(int i = 0) : a(i) {}
int val() const {return a;}
virtual void f(char ch) const {cout << val() << ch << endl;} // 基类的虚函数
};
class Bingo : public Action {
public:
Bingo(int i = 0) : Action(i) {}
virtual void f(char * ch) const {cout << val() << ch << endl;} // 派生类的虚函数
};
此时由于派生类定义的虚函数参数为指针,而不是 char ,因此导致程序不能执行:
Bingo b(10);
b.f('@'); // 无法执行,因为基函数被隐藏
C++ 11 可使用虚说明符 override 在派生类中指出要覆盖一个虚函数:
virtual void f(char * ch) const override { // 此时将编译错误,因为虚方法的参数与基类不同
cout << val() << ch << endl;
}
如果想要禁止派生类覆盖特定方法,可以在基类中使用 final 虚说明符:
virtual void f(char ch) const final {
cout << val() << ch << endl;
}
由于虚说明符不是关键字,因此编译器将根据上下文来确定其是否具有特殊含义,在其他场景中可以作为常规标识符使用。
Lambda 函数是 λ 演算——一种定义和应用函数的数学系统,可以使用匿名函数替代函数指针和函数符。首先使用函数指针、函数符和 Lambda 函数来实现一个函数:判断有多少个元素能够被 3 整除。
vector<int> numbers(100) {...};
bool f3(int x) {return x % 3 == 0;}
count = count_if(numbers.begin(), numsbers.end(), f3); // 将函数指针传递给count_if
class f_mod {
private:
int dv;
public:
f_mod(int d = 1) : dv(d) {}
bool operator()(int x) {return x % dv == 0;}
};
count = count_if(numbers.begin(), numbers.end(), f_mod(3)); // 使用构造函数创建dv为3的对象,然后使用函数符判断
[](int x) {return x % 3 == 0;} // 首先用[]代替了函数名,其次没有声明返回类型,它会通过decltype自动推断,此处为bool,默认为void
count = count_if(numbers.begin(), numbers.end(), [](int x){return x % 3 == 0;})
需要注意的是,自动推断只有在一条返回语句组成时才有效,否则需要显式给出具体类型:[](double x)->double{int y= x; return x - y;}
如果需要多次使用同一个 Lambda 函数,可以给它指定一个名称,并且可以作为正常函数使用:
auto mod3 = [](int x) {return x % 3 == 0;}
bool result = mod3(z);
Lambda 函数可访问作用域内的任何动态变量,需将其放入 [] 内:
// 原始版本
count = count_if(numbers.begin(), numbers.end(), [](int x){return x % 3 == 0;})
// 修改版本
int y = 0, z = 0;
count = for_each(numbers.begin(), numbers.end(), [&y](int x){y += x % 3 == 0; z += x % 13 == 0;}) // 同时实现两种整除
包装器(wrapper,也叫适配器 adapter),用于给其他编程接口提供更一致或更合适的接口。
answer = ef(q);
ef 可以是函数名、函数指针、函数对象或有名称的 lambda 表达式,这些都为可调用的类型(callable type),由于可调用的类型丰富,因此可能导致模板的效率降低,下面是一个例子:
#include
using namespace std;
template <typename T, typename F>
T use_f(T v, F f) {
static int count = 0;
count++;
cout << "use_f count = " << count << ", &count = " << &count << ", ";
return f(v);
}
class Fp {
private:
double z_;
public:
Fp(double z = 1.0) : z_(z) {}
double operator()(double p) {return z_*p;}
};
class Fq {
private:
double z_;
public:
Fq(double z = 1.0) : z_(z) {}
double operator()(double q) {return z_ + q;}
};
double dub(double x) {return 2.0 * x;}
double square(double x) {return x * x;}
int main() {
double y = 1.21;
cout << use_f(y, dub); // use_f count = 1, &count = 0x402028, 2.42
cout << use_f(y, square); // use_f count = 2, &count = 0x402028, 1.4641
cout << use_f(y, Fp(5.0)); // use_f count = 1, &count = 0x402020, 6.05
cout << use_f(y, Fq(5.0)); // use_f count = 1, &count = 0x402024, 6.21
cout << use_f(y, [](double u){return u * u;}); // use_f count = 1, &count = 0x405020, 1.4641
cout << use_f(y, [](double u){return u + u / 2.0;}); // use_f count = 1, &count = 0x40501c, 1.815
}
可以发现,在每次调用中,模板参数 T 都被设置为类型 double ,但是 F 的类型却各不相同,下面进行分析:
use_f(y, dub);
:dub 是一个函数指针,因此 F 的类型为 double(*)(double)
;use_f(y, square);
:与上一个相同;use_f(y, Fp(5.0));
:F 的类型为 Fp ;use_f(y, Fq(5.0));
:F 的类型为 Fq;use_f(y, [](double u){return u * u;});
:F 为 lambda 表达式使用的类型;use_f(y, [](double u){return u + u / 2.0;});
:F 为 lambda 表达式使用的类型;因此可以发现同样的模板代码,生成了 5 个不同的实例,而由于这 6 个调用中的调用特征标均为 double(double) ,导致造成了浪费。模板 function 是在头文件 functional 中声明的,他从调用特征标的角度定义了一个对象,可用于包装调用特征标相同的函数指针、函数对象或 labmda 表达式:
function<double(char, int)> fdci; // 接收char和int参数,返回double值,可将函数指针、函数对象或labmda表达式赋给它
下面改写上一个例子:
int main() {
double y = 1.21;
function<double(double)> ef1 = dub;
function<double(double)> ef2 = square;
function<double(double)> ef3 = Fp(5.0);
function<double(double)> ef4 = Fq(5.0);
function<double(double)> ef5 = [](double u){return u * u;};
function<double(double)> ef6 = [](double u){return u + u / 2.0;};
cout << use_f(y, ef1); // use_f count = 1, &count = 0x402020, 2.42
cout << use_f(y, ef2); // use_f count = 2, &count = 0x402020, 1.4641
cout << use_f(y, ef3); // use_f count = 3, &count = 0x402020, 6.05
cout << use_f(y, ef4); // use_f count = 4, &count = 0x402020, 6.21
cout << use_f(y, ef5); // use_f count = 5, &count = 0x402020, 1.4641
cout << use_f(y, ef6); // use_f count = 6, &count = 0x402020, 1.815
}
此时只创建了一个实例,节省了资源,还可以进一步简化:
typedef function(double) fdd;
int main() {
double y = 1.21;
cout << use_f(y, fdd(dub));
cout << use_f(y, fdd(square));
...
}
另外可以对源函数进行修改:
template <typename T, typename F>
T use_f(T v, function<T(T)> f) {
static int count = 0;
count++;
cout << "use_f count = " << count << ", &count = " << &count << ", ";
return f(v);
}
cout << use_f<double>(y, dub);
cout << use_f<double>(y, Fp(5.0));
由于函数本身的类型不是 function
,因此在 use_f
后面使用了
来指出所需的具体化。
如果想要对一个函数传递任意的参数列表,根据之前所学的知识是对其进行重载,但是这样很难满足“任意”的需求,因此需要可变参数模板。
template<typename T>
void show_list0(T value) {
cout<<value<<",";
}
show_list0(2.15);
上述代码中有两个参数列表,模板参数列表中只包含 T ,函数参数列表只包含 value 。C++ 11 提供了一个用省略号表示的元运算符(mera-operator),可以声明表示模板参数包的标识符(类型列表),还能够声明表示函数参数包的标识符(值列表):
template<typename... Args>
void show_list1(Args... args) {...} // args的类型为Args
其中,Args 是一个模板参数包,而 args 是一个函数参数包:
show_list1('S', 80, "sweet", 4.5); // 参数包Args包含于函数调用中的参数匹配的类型:char int const char* double
template<typename... Args>
void show_list1(Args... args) {
show_list1(args...); // args展开为5, 'L', 0.5,因此导致无限循环
}
show_list1(5, 'L', 0.5); // 将3个参数封装到args中
上面的代码存在缺陷,会导致无限循环,正确的使用方法如下:
template<typename T, typaname... Args>
void show_list3(T value, Args... args) {
cout << value << ", ";
show_list3(args...);
}
int main() {
int n =14;
double x = 2.7;
string s = "example";
show_list3(n, x);
show_list3(x*x, '!', 7, mr); // 第一个实参导致T为double,后3个参数封装为args
return 0;
}
考虑到参数结束的情况,还可以对其进行改进,重新定义一个只接受一项参数的版本:
template<typename T>
void show_list3(T value) {
cout << value << "\n";
}
考虑到直接传值效率很低,因此可以使用引用:
show_list3(const Args&... args);