一.C++的发展历史
C++11是C++的第二个主要版本,从C++98起的重要更新,引入了大量更改,从C++11起C++规律的进行每3年更新一次。
二.列表初始化
2.1 C++98和C++11中的 { }
传统的C++98中使用 { } 来进行列表初始化,结构体函数体都使用此类方法,到了C++11中,对对象进行初始化时,可以省略 = 直接使用 { } 进行初始化。
2.2 C++11中的initializer_list
上面的 { }列表初始化已经很方便了,但是面对容器时,仍有些麻烦,例如vector,我们需要对数一个一个进行书写。initializer_list这个类底层是一个数组,保存起始和末尾位置的指针,这样我们可以一次性传给容器中,而无需考虑自身是否因空间增大要重新扩容等因素。
vector(initializer_list<T> l) { for (auto e : l) push_back(e) }
如图可以直接传给容器。
int main() { std::initializer_list<int> mylist; mylist = { 10, 20, 30 }; cout << sizeof(mylist) << endl; // 这⾥begin和end返回的值initializer_list对象中存的两个指针 // 这两个指针的值跟i的地址跟接近,说明数组存在栈上 int i = 0; cout << mylist.begin() << endl; cout << mylist.end() << endl; cout << &i << endl; return 0; } //8 //00EFF4B0 //00EFF4BC //00EFF824
initializer_list类的底层是数组,存储了两个指针分别指向开头和结尾地址,如图,sizeof的输出为8为两个int指针,当我们输出begin和end时,发现与i的地址非常相近,说明他们都是存储在栈上的。
三.右值引用和移动语义
3.1左值和右值
左值和右值都是表示数据的表达式,左值具有持久性,我们可以访问他的地址,一般是一些变量。右值是一些字面值常量,和表达式求值过程中创建的临时对象。
下面举例一些左值和右值的例子
int main()
{
// 左值:可以取地址
// 以下的p、b、c、*p、s、s[0]就是常⻅的左值
int* p = new int(0);
int b = 1;
const int c = b;
*p = 10;
string s("111111");
s[0] = 'x';
cout << &c << endl;
cout << (void*)&s[0] << endl;
// 右值:不能取地址
double x = 1.1, y = 2.2;
// 以下⼏个10、x + y、fmin(x, y)、string("11111")都是常⻅的右值
10;
x + y;
fmin(x, y);
string("11111");
//cout << &10 << endl;
//cout << &(x+y) << endl;
//cout << &(fmin(x, y)) << endl;
//cout << &string("11111") << endl;
return 0;
}
如上代码所示,所以通常我们区分左右值的办法就是看它是否能取地址。
3.2 左值引用与右值引用
左值引用int &r = x,右值引用int &&r = y。左值引用就是给左值取别名,同理右值引用就是给右值取别名。
左值引用不能直接引用右值,需要添加const来修饰,使左值变为常量 ,而右值也不能直接引用左值,需要添加move(左值)修饰。
int main() { int a; const int& r1 = 10;//若直接为 int& r1 = 10 就会报错 int&& r2 = move(a);//若直接为 int &&r2 = a 就会报错 return 0; }
当右值变量右值引用右值时这个右值将带有左值变量的属性
int main() { int&& r1 = 10; //r1是右值变量,&&右值引用,10右值后,r1带有左值属性。 return 0; }
3.3 左值和右值的参数匹配
在函数调用时,通过调用不同的左值和右值,会相应调用不同的重载函数,这里在STL容器接口中有体现,下面我们来分析一下不同参数调用到的不同接口。
void f(int& x) { std::cout << "左值引⽤重载 f(" << x << ")\n"; } void f(const int& x) { std::cout << "到 const 的左值引⽤重载 f(" << x << ")\n"; } void f(int&& x) { std::cout << "右值引⽤重载 f(" << x << ")\n"; } int main() { int i = 1; const int ci = 2; f(i); // 调⽤ f(int&) f(ci); // 调⽤ f(const int&) f(3); // 调⽤ f(int&&),如果没有 f(int&&) 重载则会调⽤ f(const int&) f(std::move(i)); // 调⽤ f(int&&) // 右值引⽤变量在⽤于表达式时是左值 int&& x = 1; f(x); // 调⽤ f(int& x) f(std::move(x)); // 调⽤ f(int&& x) return 0; }
如上图f函数分别重载构造了左值引用,const 左值引用,以及右值引用。i 作为左值变量相应的调用左值引用;ci是const int类型的变量,相应调用const的左值引用;3为右值调用右值引用;i 为左值在move之后变为右值调用右值引用;x为右值在右值引用右值后它带有左值属性 ,调用左值引用;相应的move(x)变为右值属性调用右值引用。
3.4 右值引用和移动语义的使用场景
3.4.1 移动构造和移动赋值
移动构造和拷贝构造类似,是一种构造函数,但是不同的是他要求参数是右值引用;移动赋值是一个赋值运算符重载,类似赋值函数,但是要求赋值参数需是右值引用。
与拷贝构造和赋值类似,移动构造和移动赋值的目的就是窃取对象资源,实现深拷贝从而提高效率。
namespace wu
{
class string
{
public:
typedef char* iterator;
typedef const char* const_iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
const_iterator begin() const
{
return _str;
}
const_iterator end() const
{
return _str + _size;
}
string(const char* str = "")
:_size(strlen(str))
, _capacity(_size)
{
cout << "string(char* str)-构造" << endl;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
void swap(string& s)
{
::swap(_str, s._str);
::swap(_size, s._size);
::swap(_capacity, s._capacity);
}
string(const string& s)
:_str(nullptr)
{
cout << "string(const string& s) -- 拷⻉构造" << endl;
reserve(s._capacity);
for (auto ch : s)
{
push_back(ch);
}
}
// 移动构造
string(string&& s)
{
cout << "string(string&& s) -- 移动构造" << endl;
swap(s);
}
string& operator=(const string& s)
{
cout << "string& operator=(const string& s) -- 拷⻉赋值" <<
endl;
if (this != &s)
{
_str[0] = '\0';
_size = 0;
reserve(s._capacity);
for (auto ch : s)
{
push_back(ch);
}
}
return *this;
}
// 移动赋值
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移动赋值" << endl;
swap(s);
return *this;
}
~string()
{
cout << "~string() -- 析构" << endl;
delete[] _str;
_str = nullptr;
}
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
void reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
if (_str)
{
strcpy(tmp, _str);
delete[] _str;
}
_str = tmp;
_capacity = n;
}
}
void push_back(char ch)
{
if (_size >= _capacity)
{
size_t newcapacity = _capacity == 0 ? 4 : _capacity *
2;
reserve(newcapacity);
}
_str[_size] = ch;
++_size;
_str[_size] = '\0';
}
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
const char* c_str() const
{
return _str;
}
size_t size() const
{
return _size;
}
private:
char* _str = nullptr;
size_t _size = 0;
size_t _capacity = 0;
};
}
这里我们提供了一个自己手动实现的string类对象,这里并不是重点后续就不再附上此代码。我们的重点在于传值是调用了什么构造。
int main()
{
wu::string s1("xxxxx");
// 拷⻉构造
wu::string s2 = s1;
// 构造+移动构造,优化后直接构造
wu::string s3 = wu::string("yyyyy");
// 移动构造
wu::string s4 = move(s1);
cout << "******************************" << endl;
return 0;
}
第一个s1没有什么疑问,直接进行构造;s2拷贝构造s1;第三个先构造出 “yyyy” 为右值,在调用移动构造,最后编译器直接优化为直接构造,第四个s1为左值move(s1)后带有右值属性调用移动构造。
3.4.2 右值引用和移动语义在传参中的提效
在C++11后容器的push和insert新增了右值引用的接口。当实参是一个左值时,调用拷贝构造进行拷贝;当实参是一个右值时,调用移动构造进行拷贝。
int main()
{
std::list<wu::string> lt;
wu::string s1("111111111111111111111");
lt.push_back(s1);
cout << "*************************" << endl;
lt.push_back(wu::string("22222222222222222222222222222"));
cout << "*************************" << endl;
lt.push_back("3333333333333333333333333333");
cout << "*************************" << endl;
lt.push_back(move(s1));
cout << "*************************" << endl;
return 0;
}
//string(char* str) - 构造
//string(const string & s) --拷⻉构造
//* ************************
//string(char* str) - 构造
//string(string && s) --移动构造
//~string() --析构
//* ************************
//string(char* str) - 构造
//string(string && s) --移动构造
//~string() --析构
//* ************************
//string(string && s) --移动构造
//* ************************
//~string() --析构
//~string() --析构
//~string() --析构
//~string() --析构
//~string() --析构
第一个先构造了s1变量,push_back拷贝一份s1调用拷贝构造;第二个先构造“22222”为右值,随后调用移动构造;第三个直接在push_back中调用构造,完成移动构造;最后一个move转为右值调用移动构造。
3.5 引用折叠
通过模板或typedef中的类型构成引用的引用时,此时存在一个引用折叠的规则:右值引用的右值引用折叠成右值引用,其余的都折叠成为左值引用。
int main()
{
typedef int& lref;
typedef int&& rref;
int n = 0;
lref& r1 = n; // r1 的类型是 int&
lref&& r2 = n; // r2 的类型是 int&
rref& r3 = n; // r3 的类型是 int&
rref&& r4 = 1; // r4 的类型是 int&&
return 0;
}
左值+左值=左值,左值+右值=左值,右值+左值=左值,右值+右值=右值。
template<class T>
void f1(T& x)
{}
// 由于引⽤折叠限定,f2实例化后可以是左值引⽤,也可以是右值引⽤
template<class T>
void f2(T&& x)
{}
int main()
{
int n = 0;
// 没有折叠->实例化为void f1(int& x)
f1<int>(n);
f1<int>(0); // 报错
// 折叠->实例化为void f1(int& x)
f1<int&>(n);
f1<int&>(0); // 报错
// 折叠->实例化为void f1(int& x)
f1<int&&>(n);
f1<int&&>(0); // 报错
// 折叠->实例化为void f1(const int& x)
f1<const int&>(n);
f1<const int&>(0);
// 折叠->实例化为void f1(const int& x)
f1<const int&&>(n);
f1<const int&&>(0);
// 没有折叠->实例化为void f2(int&& x)
f2<int>(n); // 报错
f2<int>(0);
// 折叠->实例化为void f2(int& x)
f2<int&>(n);
f2<int&>(0); // 报错
// 折叠->实例化为void f2(int&& x)
f2<int&&>(n); // 报错
f2<int&&>(0);
return 0;
}
n为左值,在f1中可以调用int,int&,int&&(右值+左值=左值),const int&,const int&&。0为右值,在f1中可以调用const int&(const 可以对左值进行常量化),const int&&。
在f2中,n可以调用int& (左值+右值=左值),不可以调用int(若调用则n变为右值)。0可以调用int,int&&(右值+右值=右值)。
四. 可变参数模板
4.1 基本语法原理
C++11支持可变参数模板,也就是说支持可变数量参数的函数和类,被称为参数包。
template <class ...Args>
void Print(Args&&... args)
{
cout << sizeof...(args) << endl;
}
int main()
{
double x = 2.2;
Print(); // 包⾥有0个参数
Print(1); // 包⾥有1个参数
Print(1, string("xxxxx")); // 包⾥有2个参数
Print(1.1, string("xxxxx"), x); // 包⾥有3个参数
return 0;
}
若要使用参数包需先进行模板设置,...Args,上面的代码计算了参数包的大小,参数包会根据包内参数数量的不同类型的不同,变成不同的模板类型函数,使函数的使用更加灵活多变。
4.2 包扩展
参数包还支持包扩展,可以将包传给不同的函数递归进行拆包操作。
void ShowList()
{
// 编译器时递归的终⽌条件,参数包是0个时,直接匹配这个函数
cout << endl;
}
template <class T, class ...Args>
void ShowList(T x, Args... args)
{
cout << x << " ";
// args是N个参数的参数包
// 调⽤ShowList,参数包的第⼀个传给x,剩下N-1传给第⼆个参数包
ShowList(args...);
}
// 编译时递归推导解析参数
template <class ...Args>
void Print(Args... args)
{
ShowList(args...);
}
int main()
{
Print(1, string("xxxxx"), 2.2);
return 0;
}
如上图,print有三个参数,被装到包里,把包传给showlist函数,showlist函数进行解包操作,showlist通过调用递归进行解包,当包为空时调用showlist()结束。
template <class T>
const T& GetArg(const T& x)
{
cout << x << " ";
return x;
}
template <class ...Args>
void Arguments(Args... args)
{}
template <class ...Args>
void Print(Args... args)
{
// 注意GetArg必须返回或者到的对象,这样才能组成参数包给Arguments
Arguments(GetArg(args)...);
}
int main()
{
Print(1, string("xxxxx"), 2.2);
return 0;
}
如上图,Arguments函数体为空,它的主要作用是将参数转换为参数包,GetArg负责将参数包传来的参数进行解包处理,最后完成Print函数。
4.3 emplace接口
emplace接口均为模板可变参数,功能上兼容push和insert,他可以通过判断左右值调用不同的构造拷贝方式。
#include<list>
// emplace_back总体⽽⾔是更⾼效,推荐以后使⽤emplace系列替代insert和push系列
int main()
{
list<wu::string> lt;
// 传左值,跟push_back⼀样,⾛拷⻉构造
wu::string s1("111111111111");
lt.emplace_back(s1);
cout << "*********************************" << endl;
// 右值,跟push_back⼀样,⾛移动构造
lt.emplace_back(move(s1));
cout << "*********************************" << endl;
// 直接把构造string参数包往下传,直接⽤string参数包构造string
// 这⾥达到的效果是push_back做不到的
lt.emplace_back("111111111111");
cout << "*********************************" << endl;
list<pair<wu::string, int>> lt1;
// 跟push_back⼀样
// 构造pair + 拷⻉/移动构造pair到list的节点中data上
pair<wu::string, int> kv("苹果", 1);
lt1.emplace_back(kv);
cout << "*********************************" << endl;
// 跟push_back⼀样
lt1.emplace_back(move(kv));
cout << "*********************************" << endl;
// 直接把构造pair参数包往下传,直接⽤pair参数包构造pair
// 这⾥达到的效果是push_back做不到的
lt1.emplace_back("苹果", 1);
cout << "*********************************" << endl;
return 0;
}
第一个先构造出s1,再将s1拷贝给lt;第二个判断出为右值直接走移动构造;第三个直接进行构造;第四个先构造出kv,再调用拷贝构造;第五个括号内为右值直接调用移动构造;最后一个直接调用构造。
五. 新的类功能
5.1 默认的移动构造和移动赋值
C++有默认生成的6个成员函数,构造函数/析构函数/拷贝构造函数/拷贝赋值重载/取地址重载/const 取地址重载。当成员列表中,没有实现析构函数/拷贝构造函数/拷贝赋值函数时(三个函数都不存在),成员列表就会默认生成移动构造函数和移动赋值重载函数。
class Person
{
public:
Person(const char* name = "", int age = 0)
:_name(name)
, _age(age)
{}
/*Person(const Person& p)
:_name(p._name)
,_age(p._age)
{}*/
/*Person& operator=(const Person& p)
{
if(this != &p)
{
_name = p._name;
_age = p._age;
}
return *this;
}*/
/*~Person()
{}*/
private:
wu::string _name;
int _age;
};
int main()
{
Person s1;
Person s2 = s1;
Person s3 = std::move(s1);
Person s4;
s4 = std::move(s2);
return 0;
}
//string(char* str) - 构造
//string(const string & s) --拷⻉构造
//string(string && s) --移动构造
//string(char* str) - 构造
//string & operator=(string && s) --移动赋值
//~string() --析构
//~string() --析构
//~string() --析构
//~string() --析构
如上图,当我们屏蔽了析构/拷贝构造/拷贝赋值函数时,当值为右值时,会默认调用移动构造和移动赋值。
六. lambda表达式
6.1 lambda表达式语法
lambda表达式我们通常用auto的类型来接收,如下面的式子。
int x = 0;
// 捕捉列表必须为空,因为全局变量不⽤捕捉就可以⽤,没有可被捕捉的变量
auto func1 = []()
{
x++;
};
int main()
{
// 只能⽤当前lambda局部域和捕捉的对象和全局对象
int a = 0, b = 1, c = 2, d = 3;
auto func1 = [a, &b]
{
// 值捕捉的变量不能修改,引⽤捕捉的变量可以修改
//a++;
b++;
int ret = a + b;
return ret;
};
cout << func1() << endl;
// 隐式值捕捉
// ⽤了哪些变量就捕捉哪些变量
auto func2 = [=]
{
int ret = a + b + c;
return ret;
};
cout << func2() << endl;
// 隐式引⽤捕捉
// ⽤了哪些变量就捕捉哪些变量
auto func3 = [&]
{
a++;
c++;
d++;
};
func3();
cout << a << " " << b << " " << c << " " << d << endl;
// 混合捕捉1
auto func4 = [&, a, b]
{
//a++;
//b++;
c++;
d++;
return a + b + c + d;
};
func4();
cout << a << " " << b << " " << c << " " << d << endl;
// 混合捕捉1
auto func5 = [=, &a, &b]
{
a++;
b++;
/*c++;
d++;*/
return a + b + c + d;
};
func5();
cout << a << " " << b << " " << c << " " << d << endl;
// 局部的静态和全局变量不能捕捉,也不需要捕捉
static int m = 0;
auto func6 = []
{
int ret = x + m;
return ret;
};
// 传值捕捉本质是⼀种拷⻉,并且被const修饰了
// mutable相当于去掉const属性,可以修改了
// 但是修改了不会影响外⾯被捕捉的值,因为是⼀种拷⻉
auto func7 = [=]()mutable
{
a++;
b++;
c++;
d++;
return a + b + c + d;
};
cout << func7() << endl;
cout << a << " " << b << " " << c << " " << d << endl;
return 0;
}
6.2 lambda的应用
lambda表达式相比于仿函数需要定义一个类的方法来说,会更加的简便好用。
struct Goods
{
string _name; // 名字
double _price; // 价格
int _evaluate; // 评价
// ...
Goods(const char* str, double price, int evaluate)
:_name(str)
, _price(price)
, _evaluate(evaluate)
{}
};
struct ComparePriceLess
{
bool operator()(const Goods& gl, const Goods& gr)
{
return gl._price < gr._price;
}
};
struct ComparePriceGreater
{
bool operator()(const Goods& gl, const Goods& gr)
{
return gl._price > gr._price;
}
};
int main()
{
vector<Goods> v = { { "苹果", 2.1, 5 }, { "⾹蕉", 3, 4 }, { "橙⼦", 2.2, 3
}, { "菠萝", 1.5, 4 } };
// 类似这样的场景,我们实现仿函数对象或者函数指针⽀持商品中
// 不同项的⽐较,相对还是⽐较⿇烦的,那么这⾥lambda就很好⽤了
sort(v.begin(), v.end(), ComparePriceLess());
sort(v.begin(), v.end(), ComparePriceGreater());
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
return g1._price < g2._price;
});
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
return g1._price > g2._price;
});
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
return g1._evaluate < g2._evaluate;
});
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
return g1._evaluate > g2._evaluate;
});
return 0;
}
如上图,我们需要比较成员之间的大小,通过sort加上compar函数进行比较。若我们不使用lambda表达式,则需要在类外定义一个函数实现功能比较,而lambda表达式则可以直接在括号内进行函数表达式书写。相对于仿函数,lambda表达式更加的明显,可以更清楚的知道函数需要实现的功能。
6.3 lambda的原理
lambda表达式的底层是通过仿函数实现的,在底层编译器会给lambda表达式命名。
七. 包装器
7.1 function
首先调用function包装器前,需要添加头文件<functional>,function可以调用函数,调用类,调用lambda表达式,调用类中的函数。调用了包装器相当于包装了一个函数功能,可以方便我们进行调用。
#include<functional>
int f(int a, int b)
{
return a + b;
}
struct Functor
{
public:
int operator() (int a, int b)
{
return a + b;
}
};
class Plus
{
public:
Plus(int n = 10)
:_n(n)
{}
static int plusi(int a, int b)
{
return a + b;
}
double plusd(double a, double b)
{
return (a + b) * _n;
}
private:
int _n;
};
int main()
{
// 包装各种可调⽤对象
function<int(int, int)> f1 = f;
function<int(int, int)> f2 = Functor();
function<int(int, int)> f3 = [](int a, int b) {return a + b; };
cout << f1(1, 1) << endl;
cout << f2(1, 1) << endl;
cout << f3(1, 1) << endl;
// 包装静态成员函数
// 成员函数要指定类域并且前⾯加&才能获取地址
function<int(int, int)> f4 = &Plus::plusi;
cout << f4(1, 1) << endl;
// 包装普通成员函数
// 普通成员函数还有⼀个隐含的this指针参数,所以绑定时传对象或者对象的指针过去都可以
function<double(Plus*, double, double)> f5 = &Plus::plusd;
Plus pd;
cout << f5(&pd, 1.1, 1.1) << endl;
function<double(Plus, double, double)> f6 = &Plus::plusd;
cout << f6(pd, 1.1, 1.1) << endl;
cout << f6(pd, 1.1, 1.1) << endl;
function<double(Plus&&, double, double)> f7 = &Plus::plusd;
cout << f7(move(pd), 1.1, 1.1) << endl;
cout << f7(Plus(), 1.1, 1.1) << endl;
return 0;
}
function<int(int, int)> f1 = f中 <int>为返回类型,()括号内是调用参数的类型,若包装的为静态成员,取类型时不需要添加取地址符号,若包装的为类内的成员函数,则()内首位需要包含类名指针。类可以通过左值和右值区分模板。
f1调用的是f函数,f2调用的是Functor仿函数,f3调用的是lambda表达式,f4调用的为类中的静态函数(成员函数在指定类域需要加&),f5调用为类中的函数,f6调用plus类,f7调用右值模板。