C++11常用语法
一、列表初始化
1. {}初始化
eg1:C++98中,使用{}进行统一的初始化方式
struct Point
{
int _x;
int _y;
};
int main()
{
int arry1[] = { 1, 2, 3, 4, 5 };
int arry2[5] = { 0 };
Point p = { 1, 2 };
return 0;
}
C++11扩大了用{}扩起的列表的使用范围(所有的内置类型和自定义类型)。使用列表初始化可以添加等号,也可以不添加
eg2:
struct Point
{
int _x;
int _y;
};
int main()
{
//这是一个C++传统的初始化方式,使用等号(=) 将1赋值给x1变量。这被称为赋值初始化。
int x1 = 1;
//这是一种使用大括号({}) 的初始化方式,被称为直接列表初始化。
//在C++11中引入了这个新的初始化语法,它提供了更严格的类型检查并可以防止某些类型转换。
int x2{ 1 };
//这是将直接列表初始化({}) 与赋值初始化(=) 结合在一起的方式。这种形式也是有效的。
int x3 = { 1 };
//这是使用圆括号(()) 的初始化方式,被称为函数形式初始化。
//这种方式类似于赋值初始化,但使用圆括号而不是等号。
int x4(1);
cout << x1 << " " << x2 << " " << x3 << " " << x4 << endl;
int arry1[]{ 1, 2, 3, 4, 5 };
int arry2[5]{ 0 };
Point p{ 1, 2 };
//C++列表初始化也可以适用于new表达式中
int* pa = new int[4]{ 1 }; //开辟四个int类型的空间,按照列表初始化,其余的初始化成0
return 0;
}
eg3:创建对象时可以利用列表初始化调用构造函数初始化
class Date
{
public:
Date(int year, int month, int day)
:_year(year)
,_month(month)
,_day(day)
{
cout << "Date(int year, int month, int day)" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2023, 10, 1); //old style
//C++支持的列表初始化,这里会调用构造函数初始化
Date d2{ 2023, 10, 11 }; //new style
Date d3 = { 2023, 10, 21 };
return 0;
}
eg4:自定义对象各种初始化方式。当构造函数被explicit修饰时p2和r初始化出错(不能进行隐式类型转换)
struct Point
{
//explicit Point(int x, int y)
Point(int x, int y)
: _x(x), _y(y)
{
cout << "Point(int x, int y)" << endl;
}
int _x;
int _y;
};
int main()
{
// 以下四个本质都是调用构造函数
// 这是使用括号语法创建Point结构体对象p1的构造方式。
// 通过调用Point结构体的构造函数,将参数(0, 0)传递给构造函数的参数x和y。 类似函数
Point p1(0, 0);
// 这是使用大括号初始化的方式创建Point结构体对象p2的构造方式。
// 它利用大括号列表直接初始化Point结构体对象的成员变量x和y。
Point p2 = {1, 1}; // 多参数构造函数,隐士类型转换
// 这是C++11中引入的直接列表初始化的方式创建Point结构体对象p3的构造方式。
// 通过大括号列表,直接将参数2和3传递给Point结构体的构造函数。
Point p3{2, 3};
// 这是创建Point结构体对象的常量引用r的构造方式。
// 它使用大括号列表直接初始化结构体对象,并将其绑定到常量引用r上
const Point &r = {3, 3};
return 0;
}
2. std::initializer_list
- 列表初始化的类型
eg1:列表初始化是有类型的,根据列表的数据不同,生成的模板类也不同
int main()
{
//这里和数组没有半毛缘关系,而且别忘了auto不能定义数组
auto il = { 10, 20, 30 };
cout << typeid(il).name() << endl;
return 0;
}
//运行结果:
//class std::initializer_list<int>
列表初始化中的数据类型是int类型,所以列表的类型就是:class std::initializer_list<int>
- 使用场景
- 一般作为构造函数的参数,C++11对STL中的不少容器增加
- 也可以用operator=的参数,这样就可以用{}赋值
eg2:使用场景
int main()
{
//使用场景1
auto il = { 1,2,3,4 };
//下面两种使用的语法一样
vector<int> v(il);
vector<int> v1 = { 1,2,3,4 };
list<int> lt(il);
cout << typeid(il).name() << endl;
//这里{"sort", "排序"}会先初始化构造一个pair对象
map<string, string> dict = { {"sort", "排序"}, {"insert", "插入"} };
//使用场景2:赋值运算符重载
//使用大括号对容器赋值
vector<int> v2;
v2 = { 10, 20, 30 };
//一样的道理
v2 = il;
return 0;
}
3. 在模拟实现的vector中支持列表初始化
让之前模拟实现的vector也支持{}初始化和赋值运算符重载
eg:
namespace kpl
{
template <class T>
class vector
{
typedef T *iterator;
public:
// 对于 vector 的构造函数vector(initializer_list<value_type> il, const allocator_type& alloc = allocator_type()),
// 参数 il 是 initializer_list<value_type> 类型的对象,表示一个元素列表。
// initializer_list 是用于初始化 STL 容器的一种特殊类型。在这个构造函数中,il 并不需要被标记为 const,
// 因为它允许对 il 中的元素进行修改。
vector(initializer_list<T> l)
{
_start = new T[l.size()];
_finish = _start + l.size();
_endofstorage = _start + l.size();
iterator vit = _start;
typename initializer_list<T>::iterator lit = l.begin();
while (lit != l.end())
{
*vit++ = *lit++;
}
}
vector<T> &operator=(initializer_list<T> l)
{
vector<T> tmp(l);
std::swap(_start, tmp._start);
std::swap(_finish, tmp._finish);
std::swap(_endofstorage, tmp._endofstorage);
return *this;
}
private:
iterator _start;
iterator _finish;
iterator _endofstorage;
};
}
int main()
{
auto il = {1, 2, 3, 4};
kpl::vector<int> v(il); //构造
kpl::vector<int> v1 = {2,3,4,5}; //构造
v1 = il; //赋值运算符重载
return 0;
}
二、声明方式
C++11提供了多种简化声明的方式
1. auto
这个auto关键字博客讲到了该内容,包括范围for循环的使用,这里就不在多讲
2. decltype
作用:推出变量类型,在定义变量,例如指针(可以不赋值)。做模板实参
eg:
template<class T1, class T2>
void f(T1 t1, T2 t2)
{
decltype(t1 * t2) ret; //int
cout << typeid(ret).name() << endl;
}
int main()
{
const int x = 1;
double y = 2.2;
decltype(x * y) ret; //ret的类型是double
decltype(&x) p; //p的类型是int const*
cout << typeid(ret).name() << endl;
cout << typeid(p).name() << endl;
f(1, 'a');
return 0;
}
3. nullptr
C++11nullptr这篇博客对nullptr也进行了详细介绍
三、STL中的变化
1. 容器部分
红色框起来的就是C++11新增的容器,其中比较有作用的是unordered_map和unordered_set

2. 接口部分
-
迭代器(用处不大)

-
所有容器都支持{}列表初始化的构造函数和赋值运算符重载。(这里列出vector的接口)

-
所有容器新增了emplace系列(性能提升),针对不同的容器新增的emplace接口也有区别,这里列出vector的接口(这里就没头插,原因也就是效率低下没必要提供,list就会提供)。涉及右值引用和模板的可变参数(后面讲)

-
移动语义和移动赋值

四、右值引用和移动语义
1. 左值引用和右值引用概念
无论是左值引用还是右值引用,都是给对象取别名
- 左值:表示数据的表达式(eg:变量名和解引用的指针)。我们可以获取它的地址,一般可以对它赋值。 左值可以出现赋值符号的左边,右值不能出现在赋值符号的左边。左值引用就是给左值的引用,给左值取别名
eg1:左值
int main()
{
// 以下ptr *ptr b c p *p都是左值
int *ptr = new int(0);
int b = 1;
const int c = 2;
const char *p = "xxxxx";
// 以下都是对上面左值的引用
int *&rp = ptr;
int &rb = b;
const int &rc = c;
int &pvalue = *ptr;
return 0;
}
- 右值:表示数据的表达式(eg:字面常量,表达式返回值,函数返回值等)。右值不能取地址,可以出现在赋值符号的右边,但是不能出现在赋值符号的左边。右值引用就是对右值的引用,给右值取别名
eg2:右值
int fmin(int a, int b)
{
return a < b ? a : b;
}
int main()
{
double x = 1.1, y = 2.2;
// 以下几个都是常见的右值
10;
x + y;
fmin(x, y);
// 右值不能取地址
// cout << &10 << endl; //err
// cout << &(x + y) << endl; //err
// cout << &(fmin(x, y)) << endl; //err
// 以下几个都是对右值的右值引用
int &&rr1 = 10;
double &&rr2 = x + y;
double &&rr3 = fmin(x, y);
//右值不能在赋值符号的左边
// x + y = 1; //err
return 0;
}
注:右值是不能取地址的,但是右值引用是可以取地址的(因为右值取别名后,就会存储到特定的位置),所以上面的例子中10不能取地址,但是rr1可以取地址,也可以修改rr1
eg3:右值引用的特性
int main()
{
double x = 1.1, y = 2.2;
int &&rr1 = 10;
const double &&rr2 = x + y;
cout << rr1 << endl; //10
rr1 = 20;
cout << rr1 << endl; //20
//因为+const了所以rr2不能被修改
// rr2 = 5.5;
return 0;
}
2. 左值引用和右值引用比较
- 左值引用
- 单纯的左值引用只能引用左值,不能引用右值
- const左值引用可以引用左值,也可以引用右值
eg1:左值引用
int main()
{
//左值引用引用左值,不能引用右值
int a = 10;
int &ra1 = a; //ra为a的别名
// int &ra2 = 10; //10为右值不能被左值引用
// const左值引用可以引用左值,也可以引用右值
const int &ra3 = 10; // ok
const int &ra4 = a; // ok
return 0;
}
- 右值引用
- 单纯的右值引用只能引用右值,不能引用左值
- 右值引用可以引用move以后的左值
eg2:右值引用
int main()
{
// 右值引用引用右值,不能引用左值
int a = 10;
int &&r1 = 10; // ok
// int &&r2 = a; //右值引用不能引用左值 无法将右值引用绑定到左值
// 右值引用可以引用move以后的左值
int &&r3 = std::move(a);
return 0;
}
3. 右值引用使用场景和意义
使用场景:
- 做参数
- 做返回值
价值:减少拷贝
内置类型的右值:纯右值
自定义类型的右值:将亡值
①场景1:自定义类型深拷贝的类,要传值返回
注:eg1中这个包含在kpl命名空间的string类后面的例子一直使用
- 问题:传值返回代价很大
eg1:以前是不能用引用返回的,因为局部变量返回时生命周期就到了,但是使用传值返回代价很大。
namespace kpl
{
class string
{
public:
string(const char* str = "")
:_size(strlen(str))
, _capacity(_size)
{
cout << "string(char* str)" << endl;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
// s1.swap(s2)
void swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
// 拷贝构造
string(const string& s)
:_str(nullptr)
{
cout << "string(const string& s) -- 深拷贝" << endl;
string tmp(s._str);
swap(tmp);
}
// 赋值重载 —— 使用的现代写法,要调用拷贝构造
string& operator=(const string& s)
{
cout << "string& operator=(string s) -- 深拷贝" << endl;
string tmp(s);
swap(tmp);
return *this;
}
~string()
{
delete[] _str;
_str = nullptr;
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
}
kpl::string func()
{
kpl::string str("xxxxxxxxxxxxxxxxxxxxxxxxxxxxx");
//...
return str;
}
int main()
{
//算上了函数部分的一次构造
//这里会经过一个构造 (编译器优化的结果),但是实际上,这里要经过构造,然后两次拷贝构
kpl::string ret1 = func();
//这里会经过两次构造+一次赋值运算符重载(其中一次拷贝构造,根据返回值优化被优化掉了)
//当然这里调用的是赋值运算符重载的现代写法,会多调用拷贝构造
kpl::string ret2;
ret2 = func();
return 0;
}
eg1图解:上述代码的一部分,传值返回的代价很大

eg2:左值引用和右值引用函数重载
// 是否构成重载——构成重载
void func(const int& r)
{
cout << "void func(const int& r)" << endl;
}
void func(int&& rr)
{
cout << "void func(int&& rr)" << endl;
}
int main()
{
int a = 0;
int b = 1;
func(a);
//它会走更匹配的一项,有右值引用就会走右值版本
func(a + b);
return 0;
}
//运行结果:
//void func(const int& r)
//void func(int&& rr)
- 右值引用和移动语义(移动拷贝和移动赋值) 解决传值返回代价大的问题
eg1:在定义实现的string类中实现了两个成员函数:移动拷贝和移动赋值(符合函数重载)
// 移动拷贝
string(string &&s)
: _str(nullptr)
{
cout << "string(string&& s) -- 移动拷贝" << endl;
swap(s);
}
// 移动赋值
string &operator=(string &&s)
{
cout << "string& operator=(string && s) -- 移动拷贝" << endl;
swap(s);
return *this;
}

移动语义:
- 移动构造:本质是将参数右值的资源窃取过来,占为己有,那就不用深拷贝了(窃取别人的资源构造自己)。移动构造中没有开新空间,没有拷贝数据,所以效率变高了。
- 移动赋值:同移动构造一个道理。移动语义会把自身不要的数据扔给右值销毁,而自己霸占原来右值的资源
eg2:这个是有编译器优化的例子
//string类使用的是上面模拟实现的
kpl::string func()
{
kpl::string str("xxxxxxxxxxxxxxxxxxxxxxxxxxxxx");
//...
return str;
}
int main()
{
kpl::string ret1 = func();
return 0;
}
eg2图解:所以通过右值引用和移动拷贝,最终这段传值返回的代码只需要一次移动拷贝(浅拷贝)

eg3:编译器不优化
kpl::string func()
{
kpl::string str("xxxxxxxxxxxxxxxxxxxxxxxxxxxxx");
//...
return str;
}
int main()
{
kpl::string ret2;
ret2 = func();
return 0;
}
eg3图解:通过右值引用和移动语义,最终这段传值返回的代码只需要一次移动拷贝和一次移动赋值

eg4:move——正常的深拷贝环境下只能用深拷贝,只有将亡值才能使用移动语义
int main()
{
kpl::string str1("1111111111111111111111111111111");
move(str1);
kpl::string str2 = str1; //深拷贝
kpl::string str3 = move(str1); //移动拷贝
return 0;
}
图解eg4:

②场景2:容器的插入接口
容器的插入接口,如果插入的对象是右值,可以利用移动构造,转移资源给数据结构中的对象
int main()
{
list<kpl::string> lt;
kpl::string s1("1111111111111111"); //构造
lt.push_back(s1);
cout << endl;
kpl::string s2("111111111111111111111"); //构造
lt.push_back(move(s2));
cout << endl;
lt.push_back("22222222222222222222222"); //构造+移动拷贝
return 0;
}
图解:

STL容器插入接口函数也增加了右值引用版本,举list容器的例子:

小结
- 浅拷贝的类:移动构造不用实现,没什么需要转移的资源,直接拷贝即可。传值返回拷贝代价也不大
- 左值引用和右值引用的核心价值:
- 左值:减少拷贝,调高效率
- 右值:进一步减少拷贝,弥补左值引用没有解决的场景。eg:传值返回
4. 完美转发
①右值引用变量的属性是左值
右值引用变量的属性本质还是左值
eg1:
int main()
{
int &&r = 10;
cout << &r << endl;
r++;
cout << &r << endl;
cout << r << endl;
return 0;
}
//运行结果:
// 0x7ffda8ae2914
// 0x7ffda8ae2914
// 11
eg1解释:
- 我们知道右值本身是不能修改的,也是不能取地址的
- 但是eg1中右值的引用是可以修改的,也是可以取地址的。这就代表右值引用变量本身是左值
eg2:上文中右值引用使用场景和意义那一节的例子

eg3:也是在上文中留了一个坑,场景2中

②万能引用
万能引用
- 概念:模板中的
&&- 作用:左右值都能接收
eg:
template<typename T>
void PerfectForward(T&& t)
{
Fun(t);
}
图解:

万能引用虽然能解决接收左值和右值的能力,但是后续使用中都变成了左值
③完美转发
完美转发:在传参的过程中保留对象原生类型的属性
eg1:问题
void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
// 万能引用:既可以接收左值,又可以接收右值,const类型也可以接收,T会在函数内转化
// 实参左值,他就是左值引用(引用折叠)
// 实参右值,他就是右值引用
template<typename T>
void PerfectForward(T&& t)
{
Fun(t);
}
int main()
{
PerfectForward(10); // 右值
int a;
PerfectForward(a); // 左值
PerfectForward(std::move(a)); // 右值
const int b = 8;
PerfectForward(b); // const 左值
PerfectForward(std::move(b)); // const 右值
return 0;
}
// 运行结果:
// 左值引用
// 左值引用
// 左值引用
// const 左值引用
// const 左值引用
eg1的代码我们想的是谁调用谁的Fun函数,但是最终运行结果全是左值,就是因为右值引用的属性本质是左值
解决:完美转发

eg2:完美转发的引用场景
修改之前模拟实现的list类
修改的部分:

增加了完美转发的模拟实现的list完整版
测试代码:这里调用的是模拟实现的string类和list类,string类使用上文场景1的那个string
int main()
{
kpl::list<kpl::string> lt;
kpl::string s1("1111111111111111"); //构造
lt.push_back(s1);
cout << endl;
kpl::string s2("111111111111111111111"); //构造
lt.push_back(move(s2));
cout << endl;
lt.push_back("22222222222222222222222"); //构造+移动拷贝
return 0;
}
// 运行结果:
// string(char* str)
// string(string&& s) -- 移动拷贝
// string(char* str)
// string(const string& s) -- 深拷贝
// string(char* str)
// string(string&& s) -- 移动拷贝
// string(char* str)
// string(string&& s) -- 移动拷贝
五、新的类功能
1. 默认成员函数
- 在之前的介绍中C++类中有6个默认成员函数:构造函数、析构函数、拷贝构造函数、拷贝赋值重载、取地址重载和const 取地址重载。主要是前四个,后面两个用处不大
- C++11 新增了两个:移动构造函数和移动赋值运算符重载。
注:- 如果没有自己实现移动构造函数或移动赋值重载函数,且没有自己实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造或默认移动赋值。默认生成的移动构造函数或默认移动赋值函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造或移动赋值,如果实现了就调用移动构造或移动赋值,没有实现就调用拷贝构造或拷贝赋值。
- 一般情况,需要实现析构函数、拷贝构造和拷贝赋值都是需要管理资源的深拷贝的类,那也就需要开发者自己实现移动语义。如果不需要管理某些资源,很明显也不需要实现这些成员函数,直接使用默认生成的成员函数就可以。
- 如果提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。
原因:编译器认为开发者显式定义移动操作意味着他们可能希望对这些操作进行细粒度的控制,通常也意味着该类管理着一些资源(如动态分配的内存、文件句柄等),开发者希望控制这些资源在对象移动时的行为,而不是依赖默认的拷贝。
eg:自己实现一个简单的string类,因为这样方便观察测试结果
//这一节的测试代码,都会调用这个string类。便于观察
namespace kpl
{
class string
{
public:
string(const char *str = "")
: _size(strlen(str)), _capacity(_size)
{
cout << "string(char* str)" << endl;
}
// 拷贝构造
string(const string &s)
: _str(nullptr)
{
cout << "string(const string& s) -- 深拷贝" << endl;
}
// 赋值重载
string &operator=(const string &s)
{
cout << "string& operator=(string s) -- 深拷贝" << endl;
return *this;
}
// 移动拷贝
string(string &&s)
: _str(nullptr)
{
cout << "string(string&& s) -- 移动拷贝" << endl;
}
// 移动赋值
string &operator=(string &&s)
{
cout << "string& operator=(string && s) -- 移动赋值" << endl;
return *this;
}
~string()
{}
private:
char *_str;
size_t _size;
size_t _capacity;
};
}
eg1:显示实现析构函数 、拷贝构造、拷贝赋值重载
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:
kpl::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(const string& s) -- 深拷贝
// string(char* str)
// string& operator=(string s) -- 深拷贝
eg1分析:我们在main中调用移动语义,但是最后的结果还都是深拷贝。原因:我们显示实现析构函数 、拷贝构造、拷贝赋值重载,所以没有默认生成移动语义,所以也不能调用kpl::string中的移动语义
eg2:没有显示实现析构函数 、拷贝构造、拷贝赋值重载
class Person
{
public:
Person(const char *name = "", int age = 0)
: _name(name), _age(age)
{}
private:
kpl::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) -- 移动赋值
eg2分析:因为我们没有显示实现析构函数 、拷贝构造、拷贝赋值重载,所以编译器全都是默认生成的默认成员函数,当需要调用移动语义的时候,默认生成的移动语义,会调用自定义类型的移动语义
eg3:如果提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值
class Person
{
public:
Person(const char *name = "", int age = 0)
: _name(name), _age(age)
{
}
// 下一个内容就是这个语法:强制生成默认函数
Person(Person &&p) = default;
private:
kpl::string _name;
int _age;
};
int main()
{
Person s1;
// Person s2 = s1; // error 因为定义了移动构造,使用所以编译器不会生成默认的拷贝构造
Person s3 = std::move(s1);
Person s4;
// s4 = std::move(s2); // error 因为定义了移动构造,使用所以编译器不会生成默认的拷贝赋值
return 0;
}
2. 强制生成默认函数——default
更好的控制要使用的默认成员函数。假设你要使用某个默认的成员函数,但是因为一些原因这个函数没有默认生成。
eg:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用default关键字显示指定默认移动构造生成。
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(Person &&p) = default;
private:
kpl::string _name;
int _age;
};
int main()
{
Person s1;
Person s2 = s1;
Person s3 = std::move(s1);
return 0;
}
// 运行结果:
// string(char* str)
// string(const string& s) -- 深拷贝
// string(string&& s) -- 移动拷贝
3. 禁止生成默认函数——delete
- 更好的控制要使用的默认成员函数,不仅在生成方面还有限制方面。限制生成某些默认成员函数,在C++98中,是该函数设置成private,并且只声明不定义,这样只要其他人想要调用就会报错。在C++11中只需在该函数声明加上=delete即可。
- =delete作用是:指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数
eg:
class Person
{
public:
Person(const char *name = "", int age = 0)
: _name(name), _age(age)
{
}
Person(const Person &p) = delete;
private:
string _name;
int _age;
};
int main()
{
Person s1;
Person s2 = s1; // error 拷贝构造被delete了
// 类有被删除或不可访问的拷贝构造函数或拷贝赋值操作符,编译器将不会生成移动构造函数或者移动赋值。
Person s3 = std::move(s1); // error
return 0;
}
4. final和override
继承和多态中的final与override关键字也属于C++11中新的类功能,在多态中进行了详细介绍
六、可变参数模板
1. 可变参数模板
C++98/03中,类模板和函数模板中只能传固定数量的模板参数,而C++11的新特性可变参数模板可以接受可变参数的函数模板和类模板。这里只做基础介绍
- 基本可变参数的函数模板:
// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包:Args... args 这个args参数包可以包含0-N(N>=0)个模板参数
template<class ...Args>
void ShowList(Args... args)
{}
- 获取参数包的值:展开参数包
我们想要使用参数包中的参数就得把参数从参数包里取出,只能通过展开的方式来获取参数包的每个参数
eg1:可以理解的展开方式
template<class ...Args>
void ShowList(Args... args)
{
//语法使用也很奇怪
cout << sizeof...(args) << endl; //获取参数包中的参数个数
// 虽然这种方式很好理解,但是语法不支持这样的参数包展开方式。
//for (size_t i = 0; i < sizeof...(args); i++)
//{
// cout << args[i] << endl;
//}
}
int main()
{
ShowList(1, 2, 'a');
ShowList(1, 2, 'a', 'f');
return 0;
}
//运行结果:
//3
//4
eg2:递归函数方式展开参数包
//递归终止函数(无参调用),和下面的展开函数构成重载
void ShowList()
{
cout << endl;
}
//展开函数
template<class T, class ...Args>
void ShowList(T val, Args... args)
{
cout << val << " ";
ShowList(args...);
}
int main()
{
ShowList();
ShowList(1);
ShowList(1, 2);
ShowList(1, 2, 3.3);
ShowList(1, string("sdada"));
return 0;
}
//运行结果:
//
// 1
// 1 2
// 1 2 3.3
// 1 sdada
eg3:逗号表达式展开参数包
void ShowList()
{
cout << endl;
}
template<class T>
int PrintArg(T t)
{
cout << t << " ";
return 0;
}
template<class ...Args>
void ShowList(Args... args)
{
// 使用参数包来初始化数组时,编译器会先展开参数包,然后将展开后的参数用于数组的初始化。
// int arr[] = { (PrintArg(args), 0)... };
// 上面是利用逗号表达式,其实也可以利用调用函数的返回值
int arr[] = { PrintArg(args)... };
cout << endl;
}
int main()
{
ShowList(); //注意:没有参数需要单独写一个函数进行处理
ShowList(1);
ShowList(1, 3.3);
ShowList(1, string("sdada"));
return 0;
}
// 运行结果:
//
// 1
// 1 3.3
// 1 sdada
eg3文字叙述:
这里使用了C++11的包扩展语法(pack expansion)来初始化一个数组。这里的数组arr的大小实际上是在编译时确定的,取决于参数包args中元素的数量。{PrintArg(args)…}将会展开成{PrintArg(arg1), PrintArg(arg2), PrintArg(arg3) etc… },最终会创建一个元素值都为0的数组int arr[sizeof…(Args)]。在创建数组的过程中会先执行PrintArg(args)打印出参数,也就是说在构造int数组的过程中就将参数包展开了,这个数组的目的纯粹是为了在数组构造的过程展开参数包
- 简单应用参数包:
eg:
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
: _year(year), _month(month), _day(day)
{
cout << "Date构造" << endl;
}
Date(const Date &d)
: _year(d._year), _month(d._month), _day(d._day)
{
cout << "Date拷贝构造" << endl;
}
private:
int _year;
int _month;
int _day;
};
template <class ...Args>
Date* Create(Args... args)
{
Date* ret = new Date(args...);
return ret;
}
int main()
{
//直接传参到构造中
Date* p1 = Create();
Date* p2 = Create(2023);
Date* p3 = Create(2023, 9);
Date* p4 = Create(2023, 9, 27);
Date d(2023, 1, 1);
Date* p5 = Create(d); //这里识别出来是Date类型直接构造
// 两次拷贝构造,一次传参给Create时,一次new时
return 0;
}
// 运行结果:
// Date构造
// Date构造
// Date构造
// Date构造
// Date构造
// Date拷贝构造
// Date拷贝构造
2. emplace
我们在上面说到容器新增了emplace系列,让性能得到提升。本质也是进行插入操作
这里以list容器举例:

emplace_back函数的声明:
template <class... Args> //emplace系列的接口,支持模板的可变参数,并且是万能引用
void emplace_back (Args&&... args);
eg1:用法上
int main()
{
list<pair<int, char>> lt;
//emplace_back支持可变参数,拿到构建pair对象的参数后自己去创建对象
lt.emplace_back(10, 'a');
lt.emplace_back(20, 'b');
lt.emplace_back(make_pair(30, 'c'));
//lt.emplace_back({ 40, 'd' }); //error
//lt.push_back(10, 'a'); //error
lt.push_back(make_pair(40, 'd'));
lt.push_back({ 50, 'e' });
for (auto& e : lt)
{
cout << e.first << " " << e.second << endl;
}
return 0;
}
eg2:效率上的区别
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
: _year(year), _month(month), _day(day)
{
cout << "Date构造" << endl;
}
Date(const Date &d)
: _year(d._year), _month(d._month), _day(d._day)
{
cout << "Date拷贝构造" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
// emplace_back是直接构造
// push_back是先构造,再移动构造。因为有移动构造的存在其实效率差别不大。
std::list<std::pair<int, string>> mylist;
mylist.emplace_back(10, "sort");
mylist.push_back(make_pair(30, "sort"));
// 对于内置类型,效率有些差别
std::list<Date> lt;
Date d(2023, 9, 27);
// 只能传日期类对象
lt.push_back(d);
// 传日期类对象
lt.emplace_back(d);
// 传日期类对象的参数包
// 参数包,一路往下传,直接去构造日期类对象,减少了构造次数
lt.emplace_back(2023, 9, 27);
return 0;
}
// 运行结果:
// Date构造
// Date拷贝构造
// Date拷贝构造
// Date构造
七、lambda表达式
1. 引入
eg1:在C++98中我们可以使用sort对内置类型集合中的元素进行排序
int main()
{
int array[] = {4, 1, 8, 5, 3, 7, 0, 9, 2, 6};
// 默认按照小于比较,排出来结果是升序
std::sort(array, array + sizeof(array) / sizeof(array[0]));
// array: 0 1 2 3 4 5 6 7 8 9
// 如果需要降序,需要改变元素的比较规则
std::sort(array, array + sizeof(array) / sizeof(array[0]), greater<int>());
// array: 9 8 7 6 5 4 3 2 1 0
return 0;
}
eg2:对自定义类型排序,需要用户定义排序时的比较规则
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& g1, const Goods& g2)
{
return g1._price < g2._price;
}
};
struct ComparePriceGreater
{
bool operator()(const Goods& g1, const Goods& g2)
{
return g1._price > g2._price;
}
};
struct CompareEvaluateLess
{
bool operator()(const Goods& g1, const Goods& g2)
{
return g1._evaluate < g2._evaluate;
}
};
struct CompareEvaluateGreater
{
bool operator()(const Goods& g1, const Goods& g2)
{
return g1._evaluate > g2._evaluate;
}
};
int main()
{
vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2, 3 }, { "菠萝", 1.5, 4 } };
// 根据价格——升序
sort(v.begin(), v.end(), ComparePriceLess());
// 根据价格——降序
sort(v.begin(), v.end(), ComparePriceGreater());
// 根据评价——升序
sort(v.begin(), v.end(), CompareEvaluateGreater());
// 根据评价——降序
sort(v.begin(), v.end(), CompareEvaluateLess());
return 0;
}
每次比较的逻辑不一样都要再去实现多个类,对于编程者来说极其不便。
eg3:lambda表达式 (借用eg2的类Goods)
int main()
{
vector<Goods> v = {{"苹果", 2.1, 5}, {"香蕉", 3, 4}, {"橙子", 2.2, 3}, {"菠萝", 1.5, 4}};
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; }); // 根据评价——降序
}
eg3中就是使用C++11中的lambda表达式解决,不需要编程者再写一堆的仿函数。这里也能看出lambda表达式其实是一个匿名函数
2. lambda
匿名函数——lambda
①语法
- 语法:
[capture-list](parameters)mutable -> return-type{ statement };- 说明:
- [capture-list]:捕捉列表。作用:捕捉上下文中的变量供lambda函数使用。 注:编译器根据
[]来判断接下来的代码是否为lambda函数。- (parameters):参数列表。与普通函数的参数列表一样,如果不需要参数传递,则可以连
()一起省略- mutable:默认情况,lambda函数是一个const函数,mutable可以取消常量性,在lambda函数中变量就可以修改了,但是不影响外面(eg中讲)。使用mutable,参数列表不能省略(即使没有参数)
-> return-type:返回值类型。没有返回值可以省略,返回值类型明确也可以省略,由编译器推导- { statement }:函数体。操作参数和捕获的变量
注:C++11中最简单的lambda函数[]{},该函lambda函数不做任何事情
②使用
- eg1:基本使用
struct Goods
{
string _name; //名字
double _price; //价格
int _evaluate; //平价
Goods(const char* str, double price, int evaluate)
:_name(str)
,_price(price)
,_evaluate(evaluate)
{}
};
int main()
{
// 局部的匿名函数对象 —— less
// auto less = [](int x, int y) -> bool // 返回值类型可以直接省略
auto less = [](int x, int y){
return x < y;
};
cout << less(1, 2) << endl; // 真
vector<Goods> v = {{"苹果", 2.1, 5}, {"香蕉", 3, 4}, {"橙子", 2.2, 3}, {"菠萝", 1.5, 4}};
//返回值没有省略
auto goodsPriceLess = [](const Goods &x, const Goods &y) -> bool{
return x._price < y._price;
};
sort(v.begin(), v.end(), goodsPriceLess); // 按照价格的升序
//返回值省略
auto goodsEvaluateLess = [](const Goods &x, const Goods &y){
return x._evaluate < y._evaluate;
};
sort(v.begin(), v.end(), goodsEvaluateLess); // 按照评价的升序
// 按照评价的降序 直接使用这个匿名函数
sort(v.begin(), v.end(), [](const Goods &x, const Goods &y){
return x._evaluate > y._evaluate;
});
return 0;
}
- eg2:参数列表
int main()
{
int a = 0, b = 2;
double rate = 3.14;
auto add1 = [](int x, int y) -> int
{
return x + y;
};
cout << "a + b = " << add1(a, b) << endl;
auto add2 = [](int x, int y)
{
return x + y;
};
cout << "(lambda表达式也可以隐式类型转换:) a + rate = " << add2(a, rate) << endl; // rate传给int类型的y,隐式类型转换
auto add3 = [](int x, double y)
{
return x + y;
};
cout << "a + rate = " << add3(a, rate) << endl;
return 0;
}
// 运行结果:
// a + b = 2
// (lambda表达式也可以隐式类型转换:) a + rate = 3
// a + rate = 3.14
- eg3:捕捉列表简单使用,引用传参
void func()
{
cout << "func()" << endl;
}
int main()
{
int a = 0, b = 2;
double rate = 3.14;
// 当我们在参数列表传了两个变量,这时又需要使用第三个变量,也可以继续传参,
// 但是这里可以使用捕捉列表捕捉
auto add = [rate](int x, int y){
return (x + y) * rate;
};
cout << add(a, b) << endl; //6.28 自动推导返回类型
// auto function = [](int x, int y){
// add(); /*error 不能直接调用局部的add函数*/
// };
cout << "swap before:" << a << " " << b << endl;
auto swap = [add](int &x, int &y){ /*交换引用传参即可*/
int tmp = x;
x = y;
y = tmp;
// 使用捕捉列表捕捉要调用的局部函数
cout << add(x, y) << endl; //ok
// 全局函数可以直接调用,不需要捕捉
func(); //ok
};
swap(a, b);
cout << "swap after:" << a << " " << b << endl;
return 0;
}
//运行结果:
// 6.28
// swap before:0 2
// 6.28
// func()
// swap after:2 0
- mutable和捕捉列表
注:mutable关键字与捕获列表有关,与参数列表无关。默认情况下,按值捕获的变量在lambda表达式中是只读的。使用mutable关键字,可以在lambda的函数体内部修改这些变量。
int main()
{
int x = 0, y = 2;
cout << x << " " << y << endl;
// auto swap = [x, y]() {
// //不添加mutable,x和y不能改变。mutable取消捕捉列表中变量的常属性
// int tmp = x;
// x = y;
// y = tmp;
// };
auto swap = [x, y]()mutable {
//mutable可以让内部的x和y改变,但是不会影响外面的x,y
//[var]:值传递的方式捕获,这里只是外面变量的副本
int tmp = x;
x = y;
y = tmp;
};
swap();
cout << x << " " << y << endl; //并没有交换
//想要成功交换需要引用的捕捉方式
auto swap1 = [&x, &y]() {
int tmp = x;
x = y;
y = tmp;
};
swap1();
cout << x << " " << y << endl; //进行了交换
return 0;
}
// 运行结果:
// 0 2
// 0 2
// 2 0
- 捕获列表
int main()
{
int a = 1, b = 2, c = 3, d = 4;
const int con = 1;
cout << &con << endl;
//[]
auto func = [&, a] {
//引用的方式捕捉所有父作用域的变量,除了a
//a使用传值的方式捕捉
//a++; //err 表达式必须是可以修改的左值
b++;
c++;
d++;
//con++; //不可修改
};
func();
return 0;
}
3. 函数对象与lambda
仿函数:函数对象。可以像函数一样使用的对象,就是在类中重载operator()运算符的类对象
eg:
class Rate
{
public:
Rate(double rate)
: _rate(rate)
{}
double operator()(double money, int year)
{
return money * _rate * year;
}
private:
double _rate;
};
// 实际再底层,编译器对lambda的处理方式,完全就是按照函数对象的方式进行处理,
// 如果定义一个lambda表达式,编译器会自动生成一个类,该类中重载operator()
int main()
{
//函数对象
double rate = 3.14;
Rate r1(rate);
r1(10000, 2);
//lambda
auto r2 = [=](double money, int year)->double { return money * rate * year; };
r2(10000, 2);
return 0;
}
vs2019中上例中的汇编代码:

4. 小结
捕获列表说明了上下文中那些数据可以被lambda使用,是使用传值的方式还是引用的方式捕获
- [var]:表示值传递方式捕捉变量var
- [=]:表示值传递方式捕获所有父作用域中的变量(包括this)
- [&var]:表示引用传递捕捉变量var
- [&]:表示引用传递捕捉所有父作用域中的变量(包括this)
- [this]:表示值传递方式捕捉当前的this指针
注意:
- 父作用域:lambda表达式被定义的那个作用域。这通常是一个函数体、类的成员函数体或其他能够定义lambda表达式的作用域。
- 捕捉列表可由多个捕捉项组成,以逗号分割。
eg:
a. [=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量
b. [&,a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量- 捕捉列表不允许变量重复传递,否则就会导致编译错误。
eg:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复- 在块作用域以外的lambda函数捕捉列表必须为空。
解释:块作用域通常指的是由一对花括号 {} 定义的代码块。在块作用域之外定义lambda表达式(例如,在全局作用域或命名空间作用域中),因为在块作用域之外,通常没有局部变量可供捕获,全局或命名空间作用域的变量通常是通过参数等方式传递给函数的,而不是通过捕获。- 在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都会导致编译报错。
- lambda表达式之间不能相互赋值。原因:类型有区别
lambda表达式之间不能相互赋值的原因:eg
typedef void (*PF)();
int main()
{
auto f1 = []{cout << "hello" << endl;};
auto f2 = []{cout << "hello" << endl;};
// f1 = f2 //error 因为f1和f2的类型不同
cout << typeid(f1).name() << endl;
cout << typeid(f2).name() << endl;
auto f3(f1);
f3(); //可以调用拷贝构造
cout << typeid(f3).name() << endl;
PF pf = f1; //可以赋值给函数指针
pf();
cout << typeid(pf).name() << endl;
return 0;
}
//运行结果:g++运行结果
// Z4mainEUlvE_
// Z4mainEUlvE0_
// hello
// Z4mainEUlvE_
// hello
// PFvvE
八、包装器
1. function
function包装器也叫作适配器。C++中的function本质是一个类模板。std::function在头文件<functional>中。
类模板原型:
template <class Ret, class... Args>
class function<Ret(Args...)>;
模板参数说明:
1. Ret: 被调用函数的返回类型
2. Args…:被调用函数的形参
用法:
// ()外的double是返回类型,()里的double是参数可以传参数(类模板原型中模板参数就是参数包)
function<double(double)> f2 = [](double d)->double { return d / 4; };
eg:function使用例子
#include <iostream>
#include <functional>
using namespace std;
template<class F, class T>
T useF(F f, T x)
{
static int count = 0;
cout << "count:" << ++count << endl;
cout << "count:" << &count << endl;
return f(x);
}
double f(double i)
{
return i / 2;
}
struct Functor
{
double operator()(double d)
{
return d / 3;
}
};
int main()
{
// 把f换成lamber表达式对象、或者函数对象每一种都会模板实例化一份
cout << useF(f, 11.1) << endl;
// 包装器 -- 可调用对象的类型:函数名(函数指针)、函数对象(仿函数对象)、lambda表达式对象
// 三次调用也只实例化一份。
function<double(double)> f1 = f;
function<double(double)> f2 = [](double d)->double { return d / 4; };
function<double(double)> f3 = Functor();
// 也可以这样玩,虽然是三种不同的调用对象类型,但是可以在一个vector中
// vector<function<double(double)>> v = { f1, f2, f3 };
vector<function<double(double)>> v = { f, [](double d)->double { return d / 4; }, Functor() };
return 0;
}
2. bind
bind包装器(适配器),是一个函数模板,接受一个可调用对象,返回一个可调用对象(和所接受可调用对象的区别是参数间的位置发生变化——简单理解)。std::bind函数定义在头文件<functional>中。
函数模板原型:
template <class Fn, class... Args>
bind (Fn&& fn, Args&&... args);
template <class Ret, class Fn, class... Args>
bind (Fn&& fn, Args&&... args);
使用时语法格式:
auto newCallable = bind(callable,arg_list);// auto可以使用function代替
1. newCallable本身是一个可调用对象(函数名、函数对象、lambda表达式)
2. arg_list是一个逗号分隔的参数列表,对应给定的callable的参数。当我们调用newCallable时,
newCallable会调用callable,并传给它arg_list中的参数。
eg1:bind在普通函数中的使用
#include <iostream>
#include <functional>
using namespace std;
int Sub(int a, int b)
{
return a - b;
}
double PPlus(int a, double rate, int b)
{
return rate * (a + b);
}
int main()
{
// placeholders是一个命名空间。如果还有更多的参数_n即可
function<int(int, int)> rSub = bind(Sub, placeholders::_1, placeholders::_2);
cout << rSub(10, 5) << endl; // output: 5
function<int(int, int)> rSub1 = bind(Sub, placeholders::_2, placeholders::_1);
// 10 会传给Sub中的b即placeholders::_2,5则会传给placeholders::_1即a
cout << rSub1(10, 5) << endl; // output: -5
// 其实可以给Plus函数一个默认参数,但是这样更灵活
function<double(int, int)> Plus1 = bind(Plus, placeholders::_1, placeholders::_2, 4.0);
function<double(int, int)> Plus2 = bind(Plus, placeholders::_1, placeholders::_2, 4.2);
cout << Plus1(5, 3) << endl; // output:: 32
cout << Plus2(5, 3) << endl; // output:: 33.6
// placeholders::_1和placeholders::_2不干扰确定的值
function<double(int, int)> PPlus1 = bind(PPlus, placeholders::_1, 4.0, placeholders::_2);
// 5传给placeholders::_1 3传给placeholders::_2
cout << PPlus1(5, 3) << endl; // output:: 32
return 0;
}
eg2:bind在类中成员函数中的使用
#include <iostream>
#include <functional>
using namespace std;
class SubType
{
public:
static int sub(int a, int b)
{
return a - b;
}
int ssub(int a, int b, int rate)
{
return (a - b) * rate;
}
};
int main()
{
// 静态成员函数直接访问(调用对象可以不取地址)
function<double(int, int)> Sub1 = bind(&SubType::sub, placeholders::_1, placeholders::_2);
cout << Sub1(1, 2) << endl; // output:-1
// 类成员函数 要多传一个类对象地址或者匿名对象(调用对象要取地址)
SubType st;
function<double(int, int)> Sub2 = bind(&SubType::ssub, &st, placeholders::_1, placeholders::_2, 3);
cout << Sub2(1, 2) << endl; // output:-3
function<double(int, int)> Sub3 = bind(&SubType::ssub, SubType(), placeholders::_1, placeholders::_2, 3);
cout << Sub3(1, 2) << endl; // output:-3
return 0;
}

2051

被折叠的 条评论
为什么被折叠?



