C++11新标准
稳定性
兼容性
易用性
高性能
安全性
多核编程
long long
类型
新增了类型long long
和unsigned long long
,以支持64位(或更宽)的整型。
在VS中,int
和long
都是4字节,long long
是8字节。
在Linux中,int
是4字节,long
和long long
是8字节。char16_t
和char32_t
类型
新增了类型char16_t
和char32_t
,以支持16位和32位的字符。
意义不大,好像没什么人用,连demo
程序都找不到。- 原始字面量
在《45、C++11的原始字面量》中有详细介绍。 - 统一的初始化(列表)
C++11丰富了大括号的使用范围,用大括号括起来的列表(统一的初始化列表)可以用于所有内置
类型和用户自定义类型。使用统一的初始化列表时,可以添加等号(=),也可以不添加:
int x={5};
double y{2.74};
short quar[5]{4,5,2,76,1};
统一的初始化列表也可以用于new表达式中:
int *ar=new int[4]{2,4,6,7};
还有结构体:
Girl g1(3,"西施”);//C++98风格
Girl g2(5,"冰冰”);//C++11风格
Girl g3(8,"冰冰”);//C++11风格
STL容器提供了将initializer_list
模板类作为参数的构造函数:
vector<int> v1(10);//把v1初始化为10个元素。
vector<int> v2{10};//把v2初始化为1个元素,这个元素的值是10。
vector<int> v2{3,5,8};//把v3初始化为3个元素,值分别是3、5、8。
头文件<initializer_list>
提供了对模板类initializer_list
的支持,这个类包含成员函数begin()
和end()
。除了用于构造函数外,还可以将initializer_list
用于常规函数的参数:
#include <iostream>
#include <vector>
#include <cmath>
#include <fstream>//ifstream 类需要包含的头文件
#include<string>
#include<cassert>
#include<initializer_list>
using namespace std;
double sum(std::initializer_list<double> il) {
double total = 0;
for (auto it = il.begin();it != il.end();it++) {
total = total + *it;
}
return total;
}
int main() {
//double total=sum(3.14,5.20,8);//错误,如果没有大括号,这是三个参数
double total = sum({ 3.14,5.20,8 });//正确,有大括号,这是一个参数
std::cout << "total=" << total << endl;
}
-
自动推导类型auto
-
decltype关键字
-
函数后置返回类型
-
模板别名
-
空指针nullptr
空指针是不会指向有效数据的指针。以前C/C++
用0表示空指针,这带来了一些问题,这样的话,既可以表示指针常量,又可以表示整型常量。
C++11新增了关键字nullptr,用于表示空指针;它是指针类型,不是整型类型。
为了向后兼容,C++11仍允许用О来表示空指针,因此表达式nullptr==O为true。
使用nullptr提供了更高的类型安全。例如,可以将0传递给形参为int的函数,但是,如果将null
ptr传递给这样的函数,编译器将视为错误。
因此,出于清晰和安全考虑,请使用nullptr -
智能指针
-
异常规范方面的修改
-
强类型枚举(枚举类)
传统的C++
枚举提供了一种创建常量的方式,但类型检查比较低级。还有,如果在同一作用域内定义的两个枚举,它们的成员不能同名。
针对枚举的缺陷,C++11标准引入了枚举类,又称强类型枚举。
声明强类型枚举非常简单,只需要在enum后加上关键字class。
例如:
enum e1{red,green};
enum class e2{red,green,blue};
enum class e3{red,green,blue,yellow};
使用强类型枚举时,要在枚举成员名前面加枚举名和:,以免发生名称冲突,如: e2::red,e3:blue
强类型枚举默认的类型为int
,也可以显式地指定类型,具体做法是在枚举名后面加上:type
,type
可以是除wchar_t
以外的任何整型。
例如:
enum class e2::char {red,green,blue};
-
explicit关键字
C++支持对象自动转换,但是,自动类型转换可能导致意外。为了解决这种问题,C++11引入了explicit
关键字,用于关闭自动转换的特性。 -
类内成员初始化
在类的定义中初始化成员变量。
class Girl{
private:
int m_bh=20;//年龄
string m_name="美女";//姓名
char m_xb='X';//性别
public:
Girl(int bh,string name):m_bh(bh),m_name(name){}
}
-
基于范围的for循环
-
新的STL容器
array
(静态数组)
array
的大小是固定的,不像其它的模板类,但array
有begin()
和end()
成员函数,程序员可以array
对象使用STL
算法。forward_list
(单向链表)unordered_map
、unordered_multimap
、unordered_set
、unordered_multiset
(哈希表)
-
新的STL方法(成员函数)
- C++11新增了的方法cbegin()、cend()、crbegin()、crend(),这些方法将元素视为const。e
- iterator emplace (iterator pos,…);//在指定位置插入一个元素,.….用于构造元素,返回指
向插入元素的迭代器。 - 更重要的是,除了传统的拷贝构造函数和赋值函数,C++11新增了移动构造函数和移动赋值函
数。
-
摒弃export
C++98新增了export 关键字,C++11不再使用,但仍保留它作为关键字,供以后使用。 -
嵌套模板的尖括号
为了避免与运算符>>
混淆,C++要求在声明嵌套模板时使用空格将尖括号分开:
vector<list<int> > v1;//两个>之间必须加空格。
C++11不再这样要求:
vector<list<int> > v2;//两个>之间不必加空格。
-
final关键字
final关键字用于限制某个类不能被继承,或者某个虚函数不能被重写。
final关键字放在类名或虚函数名的后面。 -
override关键字
在派生类中,把 override放在成员函数的后面,表示重写基类的虚函数,提高代码的可读性。
在派生类中,如果某成员函数不是重写基类的虚函数,随意的加上override关键字,编译器会报错。 -
数值类型和字符串之间的转化
传统方法用sprintf()
和snprintf()
函数把数值转换为char*字符串;用atoi()
、atol()
、atof()
把char *
字符串转换为数值。
C++11提供了新的方法,在数值类型和string字符串之间的转换。
1. 数值转换为字符串
使用to_string()
函数可以将各种数值类型转换为string
字符串类型,这是一个重载函数,在头文件<string>
中声明,函数原型如下:string to_string (int val); string to_string (long val); string to_string (long long val); string to_string (unsigned val); string to_string (unsigned long val); string to_string (unsigned long long val); string to_string (float val); string to_string (double val); string to_string (long double val);
- 字符转换为串数值
在C++中,数值类型包括整型和浮点型,针对于不同的数值类型提供了不同的函数在头文件<string>
中声明,函数原型如下:
int stoi( const string& str, size_t* pos = nullptr, int base = 10 ); long stol( const string& str, size_t* pos = nullptr, int base = 10 ); long long stoll( const string& str, size_t* pos = nullptr, int base = 10 ); unsigned longstoul( const string& str, size_t* pos = nullptr, int base = 10 ); unsigned long long staull( const string& str, size_t* pos = nullptr, int base = 10 ); float stof( const string& str, size_t* pos = nullptr ); double stod( const string& str, size_t* pos = nullptr ); long double stold( const string& str, size_t*pos = nullptr );
- 字符转换为串数值
-
静态断言static_assert
-
常量表达式constexpr
const关键字从功能上来说有双重语义:只读变量和修饰常量。
示例:
void func(const int len1){
//len1是只读变量,不是常量。
int array[len1]={0};//VS会报错,Linux平台的数组长度支持变量,不会报错。
const int len2 = 8;
int array1[len2]={0};//正确,len2是常量。
}
C++11标准为了解决const关键字的双重语义问题,保留了const表示“只读”的语义,而将“常量”的语义划分给了新添加的constexpr关键字。
所以,C++11标准中,建议将const和constexpr的功能区分开,表达“只读”语义的场景用const,表达“常量”语义的场景用constexpr。
- 默认函数控制=default 与=deletec
委托构造和继承构造
C++11标准新增了委托构造和继承构造两种方法,用于简化代码。
委托构造
在实际的开发中,为了满足不同的需求,一个类可能会重载多个构造函数。多个构造函数之间可能会有重复的代码。例如变量初始化,如果在每个构造函数中都写一遍,这样代码会显得臃肿。
委托构造就是在一个构造函数的初始化列表中调用另一个构造函数。
#include <iostream>
#include <vector>
#include <cmath>
#include <fstream>//ifstream 类需要包含的头文件
#include<string>
#include<cassert>
#include<initializer_list>
using namespace std;
class AA {
private:
int m_a;
int m_b;
double m_c;
public:
//有一个参数的构造函数,初始化m_c;
AA(double c) {
m_c = c + 3;//初始化m_c
cout << "AA(double c)" << endl;
}
//有两个参数的构造函数,初始化m_a和m_v
AA(int a, int b) {
m_a = a + 1;//初始化m_a
m_b = b + 2;//初始化m_b
cout << "AA(int a,int b)" << endl;
}
//构造函数,这里有委托构造,委托上面AA(a,b)
AA(int a, int b, const string& str):AA(a,b) {
cout << "m_a" << m_a << ",m_b" << m_b << ",str=" << str << endl;
}
//构造函数,这里有委托构造,委托上面AA(c)
AA(double c, const string& str):AA(c) {
cout << "m_c=" << m_c << ",str=" << str << endl;
}
};
int main() {
AA a1(10, 20, "我是一只傻傻鸟.");
//AA a2(3.8, "我是一只小小鸟");
}
注意:
不要生成环状的构造过程。
一旦使用委托构造,就不能在初始化列表中初始化其它的成员变量。
继承构造
在C++11之前,派生类如果要使用基类的构造函数,可以在派生类构造函数的初始化列表中指定。在《126、如何构造基类》中有详细介绍。
C++11推出了继承构造(Inheriting Constructor),在派生类中使用using来声明继承基类的构
造函数。
#include <iostream>
#include <vector>
#include <cmath>
#include <fstream>//ifstream 类需要包含的头文件
#include<string>
#include<cassert>
#include<initializer_list>
using namespace std;
class AA {
public:
int m_a;
int m_b;
AA(int a) : m_a(a) { cout << "AA(int a)" << endl; }
AA(int a, int b) :m_a(a), m_b(b) { cout << "AA(int a,int b)" << endl; }
};
class BB :public AA//派生类
{
public:
double m_c;
using AA::AA;//使用基类的构造函数
//有三个参数的构造函数,调用A(a,b)初始化m_a和m_b,同时初始化m_c
BB(int a, int b, double c) :AA(a, b), m_c(c) {
cout << "BB(int a,int b,double c)" << endl;
}
void show() {
cout << "m_a=" << m_a << ",m_b" << m_b << ",m_c=" << m_c << endl;
}
};
int main() {
//将使用基类有一个参数的构造函数,初始化m_a
BB b1(10);
b1.show();
//将使用基类有两个参数的构造函数,初始化m_a和m_b
BB b2(10, 20);
b2.show();
//将使用派生类有三个参数的构造函数,调用AA(a,b)初始化m_a和m_b,同时初始化m_c
BB b3(10, 20, 10.58);
b3.show();
}
lambda函数
lambda 函数是C++11标准新增的语法糖,也称为lambda表达式或匿名函数。
lambda 函数的特点是:距离近、简洁、高效和功能强大。
示例:
[](const int& no) -> void { cout <<"亲爱的"<<no << "号:我是一只傻傻鸟。\n";
语法:
捕获列表 参数列表 函数选项 返回类型 函数体
[capture list] (parameters) mutable noexcept ->return type {statement}
来看这个例子:lamdba也可以像普通函数一样使用。
#include <iostream>
#include <vector>
#include <cmath>
#include <fstream>//ifstream 类需要包含的头文件
#include<string>
#include<algorithm>
#include<cassert>
#include<initializer_list>
using namespace std;
//表白函数
void zsshow(const int& no) {
cout << "亲爱的" << no << "号:我是一只傻傻鸟.\n";
}
//表白仿函数
class czs {
public:
void operator()(const int& no) {
cout << "亲爱的" << no << "号:我是一只傻傻鸟.\n";
}
};
int main() {
vector<int>vv = { 5,8,3 };
//第三个参数是普通函数
for_each(vv.begin(), vv.end(), zsshow);
//第三个参数是仿函数
for_each(vv.begin(), vv.end(), czs());
//第三个参数是lambda表达式
for_each(vv.begin(), vv.end(),
[](const int& no) {
cout << "亲爱的" << no << "号:我是一只傻傻鸟.\n";
}
);
//或者说也可以这样写
auto f = [](const int& no) {
cout << "亲爱的" << no << "号:我是一只傻傻鸟.\n";
};
for_each(vv.begin(), vv.end(), f);
//lamdba也可以像普通函数一样使用
f(338);
}
- 参数列表
参数列表是可选的,类似普通函数的参数列表,如果没有参数列表,(可以省略不写。与普通函数的不同:- lambda函数不能有默认参数。
- 所有参数必须有参数名。
- 不支持可变参数。
- 返回类型
用后置的方法书写返回类型,类似于普通函数的返回类型,如果不写返回类型,lambda 会根据函
数体中的代码推断出来。
如果有返回类型,建议显式的指定,自动推断可能与预期不一致。 - 函数体
类似于普通函数的函数体。 - 捕获列表
通过捕获列表,lambda函数可以访问的父作用域中的非静态局部变量(静态局部变量可以直接访
问)。
捕获列表在书写在[]
中,与函数参数的传递类似,捕获方式也可以是值或者引用。
例如:
只有将ii和dd放在捕获列表中才能访问。#include <iostream> #include <vector> #include <cmath> #include <fstream>//ifstream 类需要包含的头文件 #include<string> #include<algorithm> #include<cassert> #include<initializer_list> using namespace std; int main() { int ii = 3; double dd = 8.8; auto f = [ii, dd](const int& no) { //只有将ii和dd放在捕获列表中才能访问 cout << "ii=" << ii << endl; cout << "dd=" << dd << endl; cout << "亲爱的" << no << "号:我是一只小小鸟\n"; }; f(8); }
以下列出了不同的捕获列表的方式。
#include <iostream>
#include <vector>
#include <cmath>
#include <fstream>//ifstream 类需要包含的头文件
#include<string>
#include<algorithm>
#include<cassert>
#include<initializer_list>
using namespace std;
int main() {
int ii = 3;
double dd = 8.8;
auto f = [&](const int& no) {
//只有将ii和dd放在捕获列表中才能访问
cout << "ii=" << ++ii << endl;
cout << "dd=" << ++dd << endl;
cout << "亲爱的" << no << "号:我是一只小小鸟\n";
};
f(8);
}
右值引用
左值、右值
在C++中,所有的值不是左值,就是右值。左值是指表达式结束后依然存在的持久化对象,右值是指表达式结束后就不再存在的临时对象。有名字的对象都是左值,右值没有名字。
还有一个可以区分左值和右值的便捷方法:看能不能对表达式取地址,如果能,则为左值,否则为右值。
C++11扩展 了右值的的概念,将右值分为了纯右值和将亡值。
- 纯右值: a)非引用返回的临时变量; b)运算表达式产生的结果;c)字面常量(C风格字符串除外,它是地址)。
- 将亡值:与右值引用相关的表达式,例如︰将要被移动的对象、
T&&
函数返回的值、std:.move()
的返回值、转换成T&&
的类型的转换函数的返回值。
不懂纯右值和将亡值的区别其实没关系,统一看作右值即可,不影响使用。
例如:
class AA{
int m_a;
};
AA getTemp(){
return AA();
}
int ii=3;//ii是左值,3是右值
int jj=ii+8;//jj是左值,ii+8是右值
AA aa=getTemp();//aa是左值,getTemp()是返回的右值(临时变量)
左值引用,右值引用
C++98中的引用很常见,就是给变量取个别名,在C++11中,因为增加了右值引用(rvalue reference)的概念,所以C++98中的引用都称为了左值引用(Ivalue reference)。
右值引用就是给右值取个名字。
语法:数据类型&& 变量名=右值
例如:
#include <iostream>
#include <vector>
#include <cmath>
#include <fstream>//ifstream 类需要包含的头文件
#include<string>
#include<algorithm>
#include<cassert>
#include<initializer_list>
using namespace std;
class AA {
public:
int m_a = 9;
};
AA getTemp() {
return AA();
}
int main() {
int&& a = 3;//3是右值
int b = 8;//b是左值
int&& c = b + 5;//b+5是右值
AA&& aa = getTemp();//getTemp()返回的值是右值(临时变量)
cout << "a=" << a << endl;
cout << "c=" << c << endl;
cout << "aa.m_a=" << aa.m_a << endl;
}
getTemp()
的返回值本来在表达式语句结束后其生命也就该终结了(因为是临时变量),而通过右值引用重获了新生,其生命周期将与右值引用类型变量aa
的生命周期一样,只要aa
还活着,该右值临时变量将会一直存活下去。
引入右值引用的主要目的是实现移动语义。
左值引用只能绑定(关联、指向)左值,右值引用只能绑定右值,如果绑 定的不对,编译就会失
败。
但是,常量左值引用却是个奇葩,它可以算是一个万能的引用类型,它可以绑定非常量左值、常量左值、右值,而且在绑定右值的时候,常量左值引用还可以像右值引用一样将右值的生命期延长,缺点是,只能读不能改。
int a=1;
const int& ra=a;//a是非常量左值
const int b=1;
const int& rb=b;//b是常量左值
const int& rc=1;//1是右值
为什么要有右值引用
1)避免拷贝,提高性能,实现move()
右值中的数据可以被安全移走这一特性使得右值被用来表达移动语义。以同类型的右值构造对象时,需要以引用形式传入参数。右值引用顾名思义专门用来引用右值,左值引用和右值引用可以被分别重载,这样确保左值和右值分别调用到拷贝和移动的两种语义实现。对于左值,如果我们明确放弃对其资源的所有权,则可以通过std::move()来将其转为右值引用。std::move()实际上是static_cast<T&&>()的简单封装。
2)避免重载参数的复杂性,实现forward()
C++11之前,移动语义的缺失是C++最令人诟病的问题之一。举个栗子:
问题一:如何将大象放入冰箱?
这个答案是众所周知的。首先你需要有一台特殊的冰箱,这台冰箱是为了装下大象而制造的。你打开冰箱门,将大象放入冰箱,然后关上冰箱门。
问题二:如何将大象从一台冰箱转移到另一台冰箱?
普通解答:打开冰箱门,取出大象,关上冰箱门,打开另一台冰箱门,放进大象,关上冰箱门。
2B解答:在第二个冰箱中启动量子复制系统,克隆一只完全相同的大象,然后启动高能激光将第一个冰箱内的大象气化消失。
等等,这个2B解答听起来很耳熟,这不就是C++中要移动一个对象时所做的事情吗?
“移动”,这是一个三岁小孩都明白的概念。将大象(资源)从一台冰箱(对象)移动到另一台冰箱,这个行为是如此自然,没有任何人会采用先复制大象,再销毁大象这样匪夷所思的方法。C++通过拷贝构造函数和拷贝赋值操作符为类设计了拷贝/复制的概念,但为了实现对资源的移动操作,调用者必须使用先复制、再析构的方式。否则,就需要自己实现移动资源的接口。
为了实现移动语义,首先需要解决的问题是,如何标识对象的资源是可以被移动的呢?这种机制必须以一种最低开销的方式实现,并且对所有的类都有效。C++的设计者们注意到,大多数情况下,右值所包含的对象都是可以安全的被移动的。
右值(相对应的还有左值)是从C语言设计时就有的概念,但因为其如此基础,也是一个最常被忽略的概念。不严格的来说,左值对应变量的存储位置,而右值对应变量的值本身。C++中右值可以被赋值给左值或者绑定到引用。类的右值是一个临时对象,如果没有被绑定到引用,在表达式结束时就会被废弃。于是我们可以在右值被废弃之前,移走它的资源进行废物利用,从而避免无意义的复制。被移走资源的右值在废弃时已经成为空壳,析构的开销也会降低。
右值中的数据可以被安全移走这一特性使得右值被用来表达移动语义。以同类型的右值构造对象时,需要以引用形式传入参数。右值引用顾名思义专门用来引用右值,左值引用和右值引用可以被分别重载,这样确保左值和右值分别调用到拷贝和移动的两种语义实现。对于左值,如果我们明确放弃对其资源的所有权,则可以通过std::move()来将其转为右值引用。std::move()实际上是static_cast<T&&>()的简单封装。
转自
移动语义
如果一个对象中有堆区资源,需要编写拷贝构造函数和赋值函数,实现深拷贝。
深拷贝把对象中的堆区资源复制了一份,如果源对象(被拷贝的对象)是临时对象,拷贝完就没什么用了,这样会造成没有意义的资源申请和释放操作。如果能够直接使用源对象拥有的资源,可以节省资源申请和释放的时间。C++11新增加的移动语义就能够做到这一点。
实现移动语义要增加两个函数:移动构造函数和移动赋值函数。
移动构造函数的语法:
类名(类名&& 源对象){…}
移动赋值函数的语法:
类名& operator=(类名&& 源对象){…}
#include <iostream>
#include <vector>
#include <cmath>
#include <fstream>//ifstream 类需要包含的头文件
#include<string>
#include<algorithm>
#include<cassert>
#include<initializer_list>
using namespace std;
class AA {
public:
int* m_data = nullptr;//数据成员,指向堆区资源的指针
AA() = default;//启动默认构造函数
void alloc() {//给成员函数m_data分配内存
m_data = new int;//分配内存
memset(m_data, 0, sizeof(int));//初始化已分配的内存
}
AA(const AA& a) {//拷贝构造函数
cout << "调用了拷贝构造函数\n";//显示自己被调用的日志
if (m_data == nullptr) alloc();//如果没有分配,就分配
memcpy(m_data, a.m_data, sizeof(int));//把数据从源对象中拷贝过来
}
AA(AA&& a) {//移动构造函数
cout << "调用了移动构造函数\n";//显示自己被调用的日志
if (m_data != nullptr) delete m_data;//如果已经分配,先释放掉
m_data = a.m_data;//把资源从源对象中转移过来
a.m_data = nullptr;//把源对象中的指针置空
}
AA& operator=(const AA& a) {//赋值函数
cout << "调用了赋值函数\n";//显示自己被调用了
if (this == &a) return *this;//避免自我赋值
if (m_data == nullptr) alloc();//如果没有分配,就分配
memcpy(m_data, a.m_data, sizeof(int));//把数据从源对象中拷贝过来
return *this;
}
AA& operator=(AA&& a) {//赋值函数
cout << "调用了移动赋值函数\n";//显示自己被调用了
if (this == &a) return *this;//避免自我赋值
if (m_data != nullptr) delete m_data;//如果已经分配,先释放掉
m_data = a.m_data;//把资源从源对象中转移过来
a.m_data = nullptr;//把源对象中的指针置空
return *this;
}
~AA() {
if (m_data != nullptr) {
delete m_data;
m_data = nullptr;
}
}
};
int main() {
AA a1;//创建对象a1。
a1.alloc();//分配堆区资源。
*a1.m_data = 3;//给堆区内存赋值。
cout << "a1.m_data=" << *a1.m_data << endl;
AA a2 = a1;//将调用拷贝构造函数。
cout << "a2.m_data=" << *a2.m_data << endl;
AA a3;
a3 = a1;//将调用赋值函数。
cout << "a3.m_data=" << *a3.m_data << endl;
auto f = [] {AA aa;aa.alloc();*aa.m_data = 8;return aa;};//返回AA类对象的lambda函数
AA a4=f();//lambda函数返回临时对象,是右值,将调用移动构造函数
cout << "a4.m_data=" << *a4.m_data << endl;
AA a6;
a6 = f();//调用移动赋值函数
cout << "a6.m_data=" << *a6.m_data << endl;
}
注意:
- 对于一个左值,会调用拷贝构造函数,但是有些左值是局部变量,生命周期也很短,能不能也移动而不是拷贝呢?C++11为了解决这个问题,提供了
std:move()
方法来将左值转义为右值,从而方便使用移动语义。它其实就是告诉编译器,虽然我是一个左值,但不要对我用拷贝构造函数,用移动构造函数吧。左值对象被转移资源后,不会立刻析构,只有在离开自己的作用域的时候才会析构,如果继续使用左值中的资源,可能会发生意想不到的错误。#include <iostream> #include <vector> #include <cmath> #include <fstream>//ifstream 类需要包含的头文件 #include<string> #include<algorithm> #include<cassert> #include<initializer_list> using namespace std; class AA { public: int* m_data = nullptr;//数据成员,指向堆区资源的指针 AA() = default;//启动默认构造函数 void alloc() {//给成员函数m_data分配内存 m_data = new int;//分配内存 memset(m_data, 0, sizeof(int));//初始化已分配的内存 } AA(const AA& a) {//拷贝构造函数 cout << "调用了拷贝构造函数\n";//显示自己被调用的日志 if (m_data == nullptr) alloc();//如果没有分配,就分配 memcpy(m_data, a.m_data, sizeof(int));//把数据从源对象中拷贝过来 } AA(AA&& a) {//移动构造函数 cout << "调用了移动构造函数\n";//显示自己被调用的日志 if (m_data != nullptr) delete m_data;//如果已经分配,先释放掉 m_data = a.m_data;//把资源从源对象中转移过来 a.m_data = nullptr;//把源对象中的指针置空 } AA& operator=(const AA& a) {//赋值函数 cout << "调用了赋值函数\n";//显示自己被调用了 if (this == &a) return *this;//避免自我赋值 if (m_data == nullptr) alloc();//如果没有分配,就分配 memcpy(m_data, a.m_data, sizeof(int));//把数据从源对象中拷贝过来 return *this; } AA& operator=(AA&& a) {//赋值函数 cout << "调用了移动赋值函数\n";//显示自己被调用了 if (this == &a) return *this;//避免自我赋值 if (m_data != nullptr) delete m_data;//如果已经分配,先释放掉 m_data = a.m_data;//把资源从源对象中转移过来 a.m_data = nullptr;//把源对象中的指针置空 return *this; } ~AA() { if (m_data != nullptr) { delete m_data; m_data = nullptr; } } }; int main() { AA a1;//创建对象a1。 a1.alloc();//分配堆区资源。 *a1.m_data = 3;//给堆区内存赋值。 cout << "a1.m_data=" << *a1.m_data << endl; AA a2 = move(a1);//将调用拷贝构造函数。 cout << "a2.m_data=" << *a2.m_data << endl; AA a3; a3 = a1;//将调用赋值函数。 cout << "a3.m_data=" << *a3.m_data << endl; //auto f = [] {AA aa;aa.alloc();*aa.m_data = 8;return aa;};//返回AA类对象的lambda函数 //AA a4=f();//lambda函数返回临时对象,是右值,将调用移动构造函数 //cout << "a4.m_data=" << *a4.m_data << endl; //AA a6; //a6 = f();//调用移动赋值函数 //cout << "a6.m_data=" << *a6.m_data << endl; }
- 如果没有提供移动构造/赋值函数,只提供了拷贝构造/赋值函数,编译器找不到移动构造/赋值函
数就去寻找拷贝构造/赋值函数。 - C++11中的所有容器都实现了移动语义,避免对含有资源的对象发生无谓的拷贝。
- 移动语义对于拥有资源(如内存、文件句柄)的对象有效,如果是基本类型,使用移动语义没有意义。
完美转发
在函数模板中,可以将自己的参数“完美”地转发给其它函数。所谓完美,即不仅能准确地转发参
数的值,还能保证被转发参数的左、右值属性不变。
C++11标准引入了右值引用和移动语义,所以,能否实现完美转发,决定了该参数在传递过程使用的是拷贝语义(调用拷贝构造函数)还是移动语义(调用移动构造函数)。
我们想执行的目的就是:写一个函数让他能自动根据实参来调用相应的左值和右值版本的函数
下面是比较笨的方法:我们用了两个模板函数
#include <iostream>
#include <vector>
#include <cmath>
#include <fstream>//ifstream 类需要包含的头文件
#include<string>
#include<algorithm>
#include<cassert>
#include<initializer_list>
using namespace std;
void func1(int& ii) {//如果参数值左值,调用此函数
cout << "参数的左值=" << ii << endl;
}
void func1(int&& ii) {//如果参数值右值,调用此函数
cout << "参数的右值=" << ii << endl;
}
template<typename T>
void func2(T& ii) {
func1(ii);
}
template<typename T>
void func2(T&& ii) {
func1(move(ii));
}
int main() {
int ii = 3;
func2(ii);//将调用左值函数
func2(8);//将调用右值函数
}
为了支持完美转发,C++11提供了以下方案:(2个步骤)
- 如果模板中(包括类模板和函数模板)函数的参数书写成为
T&&
,那么,函数既可以接受左值
引用,又可以接受右值引用。 - 提供了模板函数
std:forward<T>(参数)
,用于转发参数,如果参数是一个右值,转发之后仍是右值引用;如果参数是一个左值,转发之后仍是左值引用。
#include <iostream>
#include <vector>
#include <cmath>
#include <fstream>//ifstream 类需要包含的头文件
#include<string>
#include<algorithm>
#include<cassert>
#include<initializer_list>
using namespace std;
void func1(int& ii) {//如果参数值左值,调用此函数
cout << "参数的左值=" << ii << endl;
}
void func1(int&& ii) {//如果参数值右值,调用此函数
cout << "参数的右值=" << ii << endl;
}
//1. 如果模板中(包括类模板和函数模板)函数的参数书写成为`T& & `,
// 那么,函数既可以接受左值引用,又可以接受右值引用。
//2. 提供了模板函数std:forward<T>(参数),
//用于转发参数,如果参数是一个右值,转发之后仍是右值引用;如果参数是一个左值,转发之后仍是左值引用。
template<typename TT>
void func(TT&& ii) {
func1(forward<TT>(ii));
}
int main() {
int ii = 3;
func(ii);//将调用左值函数
func(8);//将调用右值函数
}
这样就可以了:
可变参数模板
可变参数模版是C++11新增的最强大的特性之一,它对参数进行了泛化,能支持任意个数、任意数据类型的参数。
例如:
#include <iostream>
#include <vector>
#include <cmath>
#include <fstream>//ifstream 类需要包含的头文件
#include<string>
#include<algorithm>
#include<cassert>
#include<initializer_list>
using namespace std;
//递归终止时调用的非模板函数,函数名要与展开参数包的递归函数模板
void print() {
cout << "递归终止\n";
}
template<typename T,typename ...Args>
void print(T arg, Args... args) {
cout << "参数:" << arg << endl;//显示本次展开的参数
print(args...);//递归调用自己继续展开参数
}
int main(void) {
print("金莲", 4, "西施");
print("冰冰", 8, "西施", 3);
}
上面代码需要注意的是,必须要有一个递归终止时调用的非模板函数。
这个是比较完整的代码:
#include <iostream>
#include <vector>
#include <cmath>
#include <fstream>//ifstream 类需要包含的头文件
#include<string>
#include<algorithm>
#include<cassert>
#include<initializer_list>
using namespace std;
template<typename T>
void show(T girl) {//向超女表白的函数,参数可能是超女编号,也可能是姓名,所以用T。
cout << "亲爱的" << girl << ",我是一只小小鸟" << endl;
}
//递归终止时调用的非模板函数,函数名要与展开参数包的递归函数模板
void print() {
cout << "递归终止\n";
}
template<typename T,typename ...Args>
void print(T arg, Args... args) {
cout << "参数:" << arg << endl;//显示本次展开的参数
show(arg);//把参数用于表白
cout << "还有" << sizeof...(args) << "个参数未展开" << endl;//显示未展开的参数的个数
print(args...);//递归调用自己继续展开参数
}
int main(void) {
//print("金莲", 4, "西施");
print("冰冰", 8, "西施", 3);
}
这个我们只需要到时候会用就行。
下面的代码第一个参数不是可变参数,后面才是。
#include <iostream>
#include <vector>
#include <cmath>
#include <fstream>//ifstream 类需要包含的头文件
#include<string>
#include<algorithm>
#include<cassert>
#include<initializer_list>
using namespace std;
template<typename T>
void show(T girl) {//向超女表白的函数,参数可能是超女编号,也可能是姓名,所以用T。
cout << "亲爱的" << girl << ",我是一只小小鸟" << endl;
}
//递归终止时调用的非模板函数,函数名要与展开参数包的递归函数模板
void print() {
cout << "递归终止\n";
}
template<typename T,typename ...Args>
void print(T arg, Args... args) {
cout << "参数:" << arg << endl;//显示本次展开的参数
show(arg);//把参数用于表白
cout << "还有" << sizeof...(args) << "个参数未展开" << endl;//显示未展开的参数的个数
print(args...);//递归调用自己继续展开参数
}
template<typename ...Args>
void func(const string& str, Args...args) {
cout << str << endl;//表白之前喊个口号
print(args...);//展开可变参数包
cout << "表白完毕\n";
}
int main(void) {
//print("金莲", 4, "西施");
//print("冰冰", 8, "西施", 3);
func("我是超级大衰哥.", "冰冰", 8, "西施", 3);
}
时间操作chrono库
C++11提供了chrono模版库,实现了一系列时间相关的操作(时间长度、系统时间和计时器)。
头文件:#include <chrono>
命名空间:std:chrono
- 时间长度
duration模板类用表示一段时间(时间长度、时钟周期),如:1小时、8分钟、5秒。
duration的定义如下:
为了方便使用,定义了一些常用的时间长度,比如:时、分、秒、毫秒、微秒、纳秒,它们都位于template<class Rep, class Period = std:ratio<1,1>> class duratione { .... }
std:chrono
命名空间下,定义如下:
using hours = duration<Rep, std:ratio<3600>>//小时
using minutes = duration<Rep, std::ratio<60>>//分钟
using seconds = duration<Rep>//秒
using milliseconds = duration<Rep, std:milli>//毫秒
using microseconds = duration<Rep, std::micro>//微秒
using nanoseconds = duration<Rep, std:nano>//纳秒
注意:
- duration模板类重载了各种算术运算符,用于操作duration对象。
- duration模板类提供了count()方法,获取duration对象的值。
#include <iostream>
#include <vector>
#include <cmath>
#include <fstream>//ifstream 类需要包含的头文件
#include<string>
#include<algorithm>
#include<cassert>
#include<initializer_list>
#include<chrono>
using namespace std;
int main() {
chrono::hours t1(1);//1小时
chrono::minutes t2(60);//60分钟
chrono::seconds t3(60 * 60);//60*60秒
chrono::milliseconds t4(60 * 60 * 1000);//60 * 60 * 1000毫秒
if (t1 == t2) cout << "t1==t2" << endl;
if (t1 == t3) cout << "t1==t3" << endl;
if (t1 == t4) cout << "t1==t4" << endl;
//获取时钟周期的值,返回int整数
cout << "t1=" << t1.count() << endl;
cout << "t2=" << t2.count() << endl;
cout << "t3=" << t3.count() << endl;
cout << "t4=" << t4.count() << endl;
chrono::seconds t7(1);//1秒
chrono::milliseconds t8(1000);//1000毫秒
chrono::microseconds t9(1000 * 1000);//1000 * 1000微秒
chrono::nanoseconds t10(1000 * 1000 * 1000);//1000 * 1000 * 1000纳秒
if (t7 == t8) cout << "t7==t8" << endl;
if (t7 == t9) cout << "t7==t9" << endl;
if (t7 == t10) cout << "t7==t10" << endl;
//获取时钟周期的值,返回int整数
cout << "t7=" << t7.count() << endl;
cout << "t8=" << t8.count() << endl;
cout << "t9=" << t9.count() << endl;
cout << "t10=" << t10.count() << endl;
}
- 系统时间
system_clock
类支持了对系统时钟的访问,提供了三个静态成员函数:
//返回当前时间的时间点。
static std:chrono:time_point<std::chrono::system_clock> now() noexcept;
//将时间点time_point类型转换为std:time_t类型。
static std:time_t to_time_t(const time_point& t) noexcept;
//将std:time_t类型转换为时间点time_point类型。
static std:chrono:system_clock:time_point from_time_t( std:time_t t) noexcept;
例如:
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <vector>
#include <cmath>
#include <fstream>//ifstream 类需要包含的头文件
#include<string>
#include<algorithm>
#include<cassert>
#include<initializer_list>
#include<chrono>
#include<iomanip>
using namespace std;
int main() {
//静态成员函数chrono::system_clock::now()用于获取系统时间。(C++时间)
chrono::time_point<chrono::system_clock> now=chrono::system_clock::now();
//静态成员函数chrono::system_clock::to_time_t()把系统时间转换为time_t。(UTC时间)
time_t t_now = chrono::system_clock::to_time_t(now);
//std::localtime()函数把time_t转换成本地时间。
// localtime()不是线程安全的,VS用localtime_s代替,Linux用localtime_r代替。
tm* tm_now = std::localtime(&t_now);
//格式化输出本地时间
cout << put_time(tm_now, "%Y-%m-%d %H:%M:%S") << endl;
cout << put_time(tm_now, "%Y-%m-%d") << endl;
cout << put_time(tm_now, "%H:%M:%S") << endl;
cout << put_time(tm_now, "%Y%m%d%H%M%S") << endl;
}
我们之间照搬上面步骤就行。
3. 计时器
steady_clock
类相当于秒表,操作系统只要启动就会进行时间的累加,常用于耗时的统计(精确到纳秒)。
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <vector>
#include <cmath>
#include <fstream>//ifstream 类需要包含的头文件
#include<string>
#include<algorithm>
#include<cassert>
#include<initializer_list>
#include<chrono>
#include<iomanip>
using namespace std;
int main() {
//静态成员函数chrono::steady_clock::now()获取开始的时间点。
chrono::steady_clock::time_point start = chrono::steady_clock::now();
//执行一些代码,让他消耗时间
cout << "计时开始...\n";
for (int i = 0;i < 100000000;i++) {
}
cout << "计时结束.....\n";
//静态成员函数chrono::steady_clock::now()获取结束的时间点。
chrono::steady_clock::time_point end = chrono::steady_clock::now();
//计算消耗的时间。
auto dt = end - start;
cout << "耗时: " << dt.count() << "纳秒(" << (double)dt.count() / (1000 * 1000 * 1000) << "秒)";
}