一、特性总览表
中文翻译 | 英文名称 |
---|---|
_cplusplus宏 | _cplusplus macro |
对齐支持 | alignment support |
通用属性 | general attribute |
原子操作 | atomic operation |
auto类型推导(初始化类型推导) | auto(type deduction from initialize) |
C99特性 | C99 |
强类型枚举 | enum class(scoped and strongly typed enums) |
复制及再抛出异常 | copy and rethrow exception |
常量表达式 | constexpr |
decltype | decltype |
函数的默认模板参数 | default template parameters for function |
显示默认和删除的函数(默认的控制) | defaulted and deleted functions (control of defaults) |
委托构造函数 | delegating constructors |
并行动态初始化和析构 | Dynamic initialization and destruction witn concurrency |
显示转换操作符 | explicit conversion operators |
扩展的friend语法 | extended friend syntax |
扩展的整型 | extended integer types |
外部模板 | extern templates |
一般化的SFINAE规则 | generalized SFINAE rules |
统一的初始化语法和语义 | uniform initialization syntax and semantics |
非受限联合体 | unrestricted union |
用户定义的字面量 | user-defined literals |
变长模板 | variadic templates |
类成员初始化 | in-class member initializers |
继承构造函数 | inherited constructors |
初始化列表 | initializer lists |
Lambda函数 | lambda |
局部类型作模板参数 | local classes as template arguments |
long long 整型 | long long integers |
内存模型 | memory model |
移动语义(参见右值引用) | move semantics(see rvalue references) |
内联名字空间 | Inline namespace |
防止类型收窄 | preventing narrowing |
指针空值 | nullptr |
POD | POD(plain old data) |
基于范围的for语句 | range-based for statement |
原生字符串字面量 | raw string literals |
右值引用 | rvalue references |
静态断言 | static assertions |
追踪返回类型语法 | trailing return types syntax |
模板别名 | template aias |
线程本地的存储 | thread-local storage |
Unicode | Unicode |
二、部分特性的介绍
1、long long 整型
long long 整型有两种:long long和unsigned long long。在C++11中,标准要求以long long整型可以在不同平台上有不同的长度,但至少有64位。我们在写常数字面量时,可以使用LL后缀(或是ll)表示一个long long 类型的字面量,而ULL(或是ull、Ull、uLL)则表示一个unsigned long long 类型的字面量。如:
long long int lli = -9000000000000000000LL;
unsigned long long ulli = -9000000000000000000ULL;
在C++11中,有很多与long long等价的类型,比如:long long int 、signed long long 、signed long long int;而unsigned long long 和unsigned long long int则是等价的。
查看<climits>或<limits.h>中的宏来了解long long 整型的最值,共有三个:LLONG_MIN、LLONG_MAX、ULLONG_MAX,如下:
#include<limits>
#include<iostream>
#include<stdio.h>
using namespace std;
int main() {
long long ll_min = LLONG_MIN;
long long ll_max = LLONG_MAX;
unsigned long long ull_max = ULLONG_MAX;
cout << "long long最小值为:" << ll_min << endl;
cout << "long long最大值为:" << ll_max << endl;
cout << "unsigned long long最大值为:" << ull_max << endl;
return 0;
}
输出结果为:
2、右值引用:移动语义和完美转发
2.1、指针成员与拷贝构造
编写C++程序时,如果在类里有指针成员的话,要特别小心拷贝构造函数的编写,稍不注意,就会造成内存泄漏。若是进行了浅拷贝,就可能会导致内存泄漏,比如:
#inlude<iostream>
using namespace std;
class A{
public:
A () : d(new int(0))
{}
~A(){
delete d;
}
int *d;
};
int main(){
A a;
A b(a);
cout<<*a.d<<endl;//0
cout<<*b.d<<endl;//0
}//异常析构
说明:
定义了一个含有指针变量d的类A;
该成员d在构造时会接受一个new操作分配堆内存返回的指针,而在析构时会被delete操作释放分配的堆内存;
在main函数中,声明了类的对象a,又用a初始化了对象b,此处会调用类A的拷贝构造函数。此处调用的是由编译器隐式生成的拷贝构造函数,作用是类似于memcopy的按位拷贝;
此时就产生了一个问题:a.b和b.d指向了同一块堆内存;
在main函数作用域结束时,会调用析构函数,对象a和对象b的析构函数会被依次调用;
当其中一个析构完成时(如当对象b先被析构,b.d先被delete),指向的堆内存就已经被释放了,而此时a.b指针就不再指向一个有效的内存,无资源可释放,变成了一个悬挂指针。这就会导致内存泄漏。
#inlude<iostream>
using namespace std;
class A{
public:
A () : d(new int(0))
{}
A (A &h) : d(new int(*h.d))
{}
~A(){
delete d;
}
int *d;
};
int main(){
A a;
A b(a);
cout<<*a.d<<endl;//0
cout<<*b.d<<endl;//0
}//正常析构
说明:
给类A显示地添加了一个拷贝构造函数,从堆中分配新内存,将分配来的内存的指针给d,又使用*(h.d) 对*d进行初始化,这样a.d和b.d就指向了两块不同的内存,避免了析构时出现悬挂指针。
浅拷贝:
只是简单地把原始对象的内存内容按位复制到新的对象中,包括指针的复制,而不会创建新的内存空间,这就意味着原始对象和新创建的对象会共享同一块内存,对一个对象的修改可能会影响到另一个对象。但不意味着不能使用浅拷贝,在不涉及到动态内存分配时,可使用浅拷贝。
深拷贝:
在创建新对象后,会把原始对象的所有数据成员复制到新对象中,包括动态内存分配。这就意味着在进行深拷贝之后,原始对象和新对象是完全独立的,对一个对象的修改不会影响到另一个对象。通常在涉及到动态内存分配时使用深拷贝,如对象中包含指针或动态数组等。
2.2、移动语义
拷贝构造函数中为指针成员分配新的内存再进行内容拷贝的做法在C++编程中几乎是不可避免的,不过在一些时候,我们确实不需要这样的拷贝构造语义。如果一个对象中有堆区资源,需要编写拷贝构造函数和赋值函数,实现深拷贝,深拷贝把对象中的堆区资源复制了一份,如果源对象是临时对象,拷贝完就没什么作用的话,就会被析构,那这样会造成无意义的资源申请和释放操作。如果能够直接使用源对象拥有的资源,节省资源申请和释放的时间,又要避免浅拷贝,就需要用到移动语义。
#include <iostream>
#include <vector>
class LargeObject {
public:
LargeObject() {
// 构造大型对象
std::cout << "Constructing LargeObject" << std::endl;
}
LargeObject(const LargeObject& other) {
// 拷贝构造函数
std::cout << "Copying LargeObject" << std::endl;
}
LargeObject(LargeObject&& other) noexcept {
// 移动构造函数
std::cout << "Moving LargeObject" << std::endl;
}
~LargeObject() {
// 析构函数
std::cout << "Destructing LargeObject" << std::endl;
}
};
int main() {
std::vector<LargeObject> vec;
// 添加临时对象
vec.push_back(LargeObject());
return 0;
}
2.3、左值、右值与右值引用
在移动语义的构造时,有非常重要的一点就是,其参数是一个右值(或右值引用),这就得对左右值和左右值引用进行学习。
2.3.1、定义
左值:locator value,简写为lvalue,可以为存储在内存中、有明确存储地址(可寻址)的数据;
例如:
//a、p、*p、b都为左值
int a = 3;
int *p = &a;
*p;
const int b = 2;
右值:read value,简写为rvalue,指的是那些可以提供数据值的数据。右值不能出现在赋值符号左边且不能取址。
例如:
double x = 1.3 , y = 3.8;
10;//字面常量
x + y;//表达式返回值
fmin(x,y);//传值返回函数的返回值
区分左值和右值的关键点是:能否取址。
2.3.2、左值引用
左值引用就是对左值的引用,给左值取别名。
int & ra = a;
int *& rp = p;
int & r = *p;
const int & rb = b;
总结:
- 左值引用只能引用左值,不能引用右值;
- 但const左值引用既可以引用左值,也可以引用右值。
// 1.左值引用只能引用左值
int t = 8;
int& rt1 = t;
//int& rt2 = 8; // 编译报错,因为8是右值,不能直接引用右值
// 2.但是const左值引用既可以引用左值
const int& rt3 = t;
const int& rt4 = 8; // 也可以引用右值
const double& r1 = x + y;
const double& r2 = fmin(x, y);
2.3.3、右值引用
右值引用是C++11新增的特性。
右值引用就是对右值的引用,给右值取别名。
int&& rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = fmin(x,y);
注意:
- 右值引用引用右值,会使右值被存储到特定的位置,也就是说,右值引用变量其实是左值,可以对它取址和复制(const右值引用变量可以取址,但不能赋值),当然,取址是指取变量空间的地址,因为右值是不能取址的。
- 比如:
double&& rr2 = x + y; &rr2; rr2 = 6.3;
右值引用rr2引用右值x+y后,该表达式的返回值会被存储到特定的位置,对rr2取址,也就是取返回值存储位置的地址,也可以修改rr2的值;
-
再比如:
const double && rr3 = x + y; &rr4;
可以对rr4取址,但是不能修改rr4的值。
总结:
- 右值引用只能引用右值,不能直接引用左值;
- 右值引用可以引用被move的左值。(move的作用为将一个左值强制转化为右值,以实现移动语义。)
// 1.右值引用只能引用右值
int&& rr1 = 10;
double&& rr2 = x + y;
const double&& rr3 = x + y;
int t = 10;
//int&& rrt = t; // 编译报错,不能直接引用左值
// 2.但是右值引用可以引用被move的左值
int&& rrt = std::move(t);
int*&& rr4 = std::move(p);
int&& rr5 = std::move(*p);
const int&& rr6 = std::move(b);
2.3、完美转发
2.3.1、引用折叠
引用折叠的规则如下:
- 左值引用与左值引用的折叠:当左值引用与左值引用结合时,结果仍为左值引用。具体来说,以下几种情况都会折叠为左值引用:
T& &折叠为T& T& &&折叠为T& T& const &折叠为T& const(保留const,表示引用本身不可变,但所引用的对象可变)
- 右值引用与右值引用的折叠:当右值引用与右值引用结合时,结果是右值引用:
T&& &&折叠为T&&
模板类型T | 实际类型R | 最终推导类型 |
---|---|---|
T& | R | R& |
T& | R& | R& |
T& | R&& | R& |
T&& | R | R&& |
T&& | R& | R& |
T&& | R&& | R&& |
从上表中可以发现:只要有左值引用参与,最终推导类型都为左值引用。
万能引用:格式与右值引用类似(都是两个&),无论传入什么值还是引用都能接收,并且通过引用折叠的方式给其确定一个确切的类型。
2.3.2、完美转发
C++11中完美转发是在万能引用的基础上进行优化,对于一个对象,传入什么类型,就按照什么类型进行转发。
示例分析:
std::vector<std::string> v;
//参数为左值引用
void func_push(const std::string& str)
{
std::cout << "左(const std::string& str)" << std::endl;
v.push_back(str);
}
//参数为右值引用
void func_push(std::string&& str)
{
std::cout << "右(std::string&& str)" << std::endl;
v.push_back(std::move(str));
}
//参数为万能引用的模板函数
template<class T>
void func(T&& t)
{
//调用func_push()函数
func_push(t);
}
//测试
void test3()
{
std::string s = "123";
func(s);
func(std::move(s)); //std::move(s)是一个亡值表达式, 返回的是一个右值传入给func函数
}
运行结果为:
分析:
- s为一个左值,经过模板T的推导,t是一个左值引用。所以会运行func_push(const std::string& str);
- 而move(s)的作用是将左值强制转化为右值,那按照引用折叠进行推导,t应该是一个右值引用才对,但根据运行结果来看,还是执行的左值引用的函数,原因是:t虽然是个右值引用,但是它本身是一个左值,所以依然会调用左值引用的函数。
我们希望的是左值引用就调用左值引用函数,右值引用就调用右值引用的函数,现在达不到目的。若是再次强行将模板函数里的t转换为右值,即:
template<class T>
void func(T&& t)
{
// func_push(t); //
func_push(std::move(t));
}
void test3()
{
std::string s = "123";
func(s);
func(std::move(s));
}
运行结果为:
很明显,因为经过move()后,两个都会调用右值引用的函数,那么现在就轮到完美转发出场了。
格式:template <class T> forward<T>()
#include<iostream>
#include<vector>
std::vector<std::string> v;
void func_push(const std::string& str)
{
std::cout << "左(const std::string& str)" << std::endl;
v.push_back(str);
}
void func_push(std::string&& str)
{
std::cout << "右(std::string&& str)" << std::endl;
v.push_back(std::move(str));
}
template<class T>
void func(T&& t)
{
func_push(std::forward<T>(t));//加入完美转发
}
int main()
{
std::string s = "123";
func(s);
func(std::move(s)); //std::move(s)是一个亡值表达式, 返回的是一个右值传入给func函数
}
运行结果为:
完美转发的源码如下:
template<typename _Tp>
constexpr _Tp&& forward(typename std::remove_reference<_Tp>::type& __t) noexcept
{
return static_cast<_Tp&&>(__t);
}
template<typename _Tp>
constexpr _Tp&& forward(typename std::remove_reference<_Tp>::type&& __t) noexcept
{
static_assert(!std::is_lvalue_reference<_Tp>::value, "template argument substituting _Tp is an lvalue reference type");
return static_cast<_Tp&&>(__t);
}
template<typename _Tp>
struct remove_reference
{
typedef _Tp type;
};
template<typename _Tp>
struct remove_reference<_Tp&>
{
typedef _Tp type;
};
template<typename _Tp>
struct remove_reference<_Tp&&>
{
typedef _Tp type;
};
3、可变参数模板
3.1、基本语法
C++11的可变参数模板对参数进行了高度泛化,可以表示任意数目、任意类型的参数,其语法为:在class或typename后面加上省略号“...”。
例如:(其中T叫做模板参数包,args叫做函数参数包)
#include<iostream>
#include<vector>
using namespace std;
template<class ...T>
void func(T ...args) {
cout << "参数包中参数的个数为:" << sizeof ...(args) << endl;
}
int main() {
func();//参数包里0个参数
func(1, 2, 3);//参数包里3个int型的实参
func(1, "hello", 2.0);//参数包里一个int型的实参,一个char型的实参,一个double型的实参
return 0;
}
运行结果为:
省略号的作用:
- 声明一个包含0到任意个模板参数的参数包;
- 在模板定义的右边,可以将参数包展开为一个个独立的参数
3.2、参数包的展开
3.2.1、递归函数
C++11可以使用递归函数的方式展开参数包,获得可变参数的每个值。通过递归函数展开参数包,需要提供一个参数包展开的函数和一个递归终止函数。
例如:
#include<iostream>
#include<vector>
using namespace std;
//最终递归函数
void print() {
cout << "empty" << endl;
}
//展开函数
template<typename T, typename ...Args>
void print(T head, Args ...args) {
cout << head << ",";
print(args...);
}
int main() {
print(1,2,3,4);
return 0;
}
运行结果为:
参数包Args...在展开的过程中递归调用自己,每调用一次,参数包中的参数就会少一个 ,直到所有的参数都展开为止。当没有参数时就会调用非模板函数print()来终止递归过程。
3.2.2、逗号表达式
通过逗号表达式的方式展开参数包是不需要设置终止递归函数的,其直接在expand函数体中展开。
例如:(print()不是终止函数,只是一个处理参数包中每一个参数的函数)
#include<iostream>
#include<vector>
using namespace std;
template<class T >
void printf(T t) {
cout << t<< endl;
}
//展开函数
template<class ...Args>
void print(Args ...args) {
int arr[] = { (printf(args),0)... };
cout << endl;
}
int main() {
print(1,2,3,4);
return 0;
}
运行结果为:
4、Lambda表达式
4.1、概念
Lambda表达式实际上是定一个匿名的函数,该类由编译器lambda创建,这个函数被隐式地定义为内联,可以捕获一定范围内的变量。因此,调用lambda表达式相当于直接调用它的operator()函数,这个函数可以被编译器内联优化,其语法如下:
[capture](params)mutable->return-type{statement}
其中:
- [capture]:为捕获列表,捕获上下文变量供lamba使用,[]表示一个lambda的开始,不可省略,编译器会根据该符号来判断接下来的代码是不是lambda表达式。
- (params):为参数列表,与普通函数的参数列表一致,如果不需要传递参数,则可以连括号一起省略。可以按值和按引用两种方式传参。
- mutable:为修饰符,默认情况下,lambda函数总是一个const函数,mutable可以取消常量-性。注意:在使用mutable时,参数列表不可以省略。
- ->return->type:是返回值类型。当函数返回值为void,或者函数体只有一处return的地址时,这部分可以省略,因为只有一处return时,编译器可以自动推断出返回类型。
- {statement}:函数体,内容和普通函数一样,除了可以使用参数外,还可以使用所捕获的变量。此部分函数体可以为空,但不能省略。
4.2、使用方法
Lambda表达式与普通函数最大的区别就是其可以通过捕获列表来访问一些上下文中的数据。
- [var]:表示值传递方式捕获变量var;
- [=]:表示值传递方式捕获所有父作用域的变量(包括this);
- [&var]:表示引用传递捕获变量var;
- [&]:表示引用传递方式捕获所有父作用域的变量;
- [this]:表示值传递方式捕获当前的this指针。
值传递示例:
#include<iostream>
using namespace std;
int main() {
int x = 5;
[x]() {
cout << "捕获到的变量为:" << x << endl;
}();
cout << x+1 << endl;
return 0;
}
运行结果为:
注意:值传递时,lambda表达式是无法对其进行修改的。
要进行修改,就会报错:
引用传递示例:
#include<iostream>
using namespace std;
int main() {
int x = 5;
[&x]() {
cout << "捕获到的变量为:" << x++ << endl;
}();
cout << x+1 << endl;
return 0;
}
运行结果为:
注意:引用传递时可以对其值进行修改,并且在lambda内部修改了后,外部的值也会发生改变。
用修饰符mutable的值传递示例:
#include<iostream>
using namespace std;
int main() {
int x = 5;
[x]()mutable {
cout << "捕获到的变量为:" << ++x<< endl;
}();
cout << x<< endl;
return 0;
}
运行结果为:
可以看到,加了修饰符mutable之后,可以对值进行修改,而且在lambda内部修改的值,外部没有发生改变。
有返回值:
#include<iostream>
using namespace std;
int main() {
const int ret = [] () {return 1024;}();
/*const int ret = []()->int{return 1024;}();*/
cout << ret << endl;
return 0;
}
运行结果为:
4.3、作用
Lambda表达式的类型被定义为“闭包”的类,其通常用于STL库中,在某些场景下可用于简化仿函数的使用,同时lambda表达式作为局部函数,也会提高复杂代码的开发速度,轻松在函数内重用代码,无需费心设计接口。