C++11支持统一的初始化方案,使用 {}
可以对所有的内置类型和自定义类型初始化,可以加 =
,也可以不加。
int x1 = 1;
int x2 = { 2 };
int x3{ 3 };
int array1[]{ 1,2,3,4,5 };
int array2[5]{ 0 };
内置类型的初始化建议使用原来的。
主要是方便了对自定义类型的初始化,new多个对象的初始化:
对自定义类型的初始化,成员变量的权限需要是 public
struct Point
{
int _x;
int _y;
};
Point p{ 1,2 };
int* p1 = new int[3] {1, 2, 3};
对 Date 类的不同初始化方式:
class Date
{
public:
Date(int year = 0, int month = 0, int day = 0)
: _year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
Date d1(2022, 9, 19);
Date d2 = { 2022, 9, 19 };
Date d3{ 2022, 9, 19 };
Date* p3 = new Date[3]{ {2022,1,1},{2022,1,2},{2022,1,3} };
对STL容器的初始化:
vector<int> v1{ 1,2,3,4,5 };
list<int> lt1{ 1,2,3 };
set<int> s1{ 1,2,3 };
map<string, string> dict = { {"apple", "苹果"},{"banana", "香蕉"} };
map
可以使用两层 {}
进行初始化,内层是对 pair
的初始化,外层是整体对 map
的初始化
initializer_list
该类型由编译器根据初始化列表声明自动构造,初始化列表声明就用 {}
括起来的元素列表。
auto il = { 10, 20, 30 }; // the type of il is an initializer_list
它支持求长度 size()
,和迭代器访问 begin()
end()
STL 容器的成员变量是用来组织元素的,而非存储元素,并且成员变量往往是 private
权限。所以 {}
的原生语法不支持对这类容器进行初始化.
STL 容器之所以支持 {}
初始化,是因为增加了新的构造函数:
除了构造函数,其他一些成员函数,比如 set
的 insert()
也增加了 initializer_list
参数的函数重载
我们要让自己实现的 vector
支持列表初始化,也可以添加一个这样的构造函数:
vector(initializer_list<T> l)
: _start(nullptr)
, _finish(nullptr)
, _endofstorage(nullptr)
{
for (auto e : l)
push_back(e);
}
小问题:
vector<int> v1 = { 1,2,3,4 }; // ①
v1 = { 10,20,30,40 }; // ②
①是刚刚介绍的初始化列表构造,那么②是什么?
单参数的构造函数支持隐式类型转换,使用 explicit
可以阻止此类转换
这些在之前已经使用得很多了
具体介绍:引用|内联函数|auto|范围for|nullptr_世真的博客-CSDN博客
关键字 decltype
用于将变量的类型声明为表达式指定的类型。
// 打印 t1*t2 的类型
template<class T1, class T2>
void F(T1 t1, T2 t2)
{
decltype(t1 * t2) ret;
cout << typeid(ret).name() << endl;
}
C++11增加了一些新容器
右值引用的概念出来以后,我们之前学的引用应该叫做左值引用。
什么是左值和左值引用?
左值,是一个可以被取地址并且可以被赋值的数据的表达式(比如变量名、解引用的指针)。左值既可以出现在赋值号的左边也可以出现在赋值号的右边。特例:const修饰的变量不能赋值,但它依然是左值。
左值引用就是给左值取别名
什么是右值和右值引用?
常见的右值引用:
double x = 1.1, y = 2.5;
int&& rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = fmin(x, y);
右值不能被取地址和赋值,但是右值引用可以,这里的 rr1 rr2 rr3 都是可以被取地址和赋值的。如果你不想右值引用被修改,可以用 const int&& rr1
去引用。这说明,给右值取别名后,会导致右值被存储到特定的位置
左值引用总结:
权限只能缩小或平等,不能放大,这是之前学习引用的知识点,现在可以总结为以下两点:
右值引用总结:
右值引用只能引用右值,不能引用左值
右值引用可以引用 move 后的左值
move 是一个函数,可以把左值变成右值
我们知道,函数可以使用引用返回来减少拷贝,但是要保证变量出了作用域不会被销毁
在 string 的实现中,像 operator+
后置++
这样的函数返回值不能使用左值引用来减少拷贝,只能传值返回,增加了拷贝的开销。
那么,怎么解决这个问题呢?
解决方案:
传值返回会调用拷贝构造,我们可以从拷贝构造入手,
在 string 实现中增加一个右值引用构造(移动构造)
String(String&& s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
swap(s);
}
它和原来的const左值引用拷贝互不冲突,左值参数会调用左值拷贝,右值参数会选择调用最匹配的——移动构造。移动构造的代价比拷贝构造小,因为它本质上是一种资源转移。说白了,就是把临时对象偷过来直接用,避免了不必要的深拷贝。
传值返回时编译器做了什么?
有了右值引用,才算是彻底解决了函数返回需要深拷贝的问题。在 C++11 的 STL 中,也增加了移动构造。
不仅有移动构造,还有移动赋值:
String& operator=(String&& s)
{
swap(s);
return *this;
}
关于std::swap
std::swap
是需要拷贝临时对象的,效率较慢,所以各个容器通常会实现自己的swap。
在C++11中,swap的实现运用了右值引用,现在我们可以放心使用 std::swap
了
关于move:
int main()
{
String s1("hello");
String s2(move(s1));
cout << s1 << endl;
cout << s2 << endl;
return 0;
}
//结果:
//
//hello
如果你把 s1 move 了,再用生成的右值去构造 s2,那么就相当于把 s1 直接偷给了 s2,最后 s1 为空,s2 为 hello。所以一般不这样用。
如果只是单纯的 move,那么它的资源不会被转移:
int main()
{
String s1("hello");
move(s1);
cout << s1 << endl;
return 0;
}
//结果:
//hello
另外在 C++11 中,STL容器的 push_back
, insert
等插入操作也加入了右值引用的版本:
模板中的 &&
不代表右值引用,而是万能引用,其既能接收左值又能接收右值。但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值。这个过程也叫折叠
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;
}
template<class T>
void PerfectForward(T&& t)
{
Fun(t);
}
int main()
{
PerfectForward(10);
int a;
PerfectForward(a);
PerfectForward(move(a));
const int b = 9;
PerfectForward(b);
PerfectForward(move(b));
return 0;
}
//结果:
//左值引用
//左值引用
//左值引用
//const左值引用
//const左值引用
可以看到,传入的参数最后都变成了左值。那么要如何保持原来的性质呢?就需要下面学习的完美转发
forward:在传参的过程中保留对象原生类型属性
在函数模板里使用forward()把参数包起来,这样就能保证它原来的左值或右值的性质。
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;
}
template<class T>
void PerfectForward(T&& t)
{
Fun(forward<T>(t)); //
}
int main()
{
PerfectForward(10);
int a;
PerfectForward(a);
PerfectForward(move(a));
const int b = 9;
PerfectForward(b);
PerfectForward(move(b));
return 0;
}
//结果:
//右值引用
//左值引用
//右值引用
//const左值引用
//const右值引用
原来的C++类中,有6个默认构造函数:
C++11新增了两个:移动构造函数和移动赋值运算符重载
生成条件:
class Person
{
public:
Person(const char* name = "", int age = 0)
:_name(name)
, _age(age)
{}
private:
String _name;
int _age;
};
int main()
{
Person s1;
Person s2 = s1;
Person s3 = std::move(s1);
Person s4;
s4 = std::move(s2);
return 0;
}
//结果:
//拷贝构造
//移动构造
//拷贝构造
//拷贝赋值
即使不满足条件你也可以使用 default
强制生成,如:
Person(Person&& pp) = default;
Person& operator=(Person&& pp) = default;
delete
强制禁止生成默认函数:
Person(Person&& pp) = delete;
Person& operator=(Person&& pp) = delete;
继承与多态中的 final
与 override
关键字:
这些在 多态:虚函数,使用,多态的原理_世真的博客-CSDN博客_虚函数的多态调用 已有介绍。
我们知道,printf 函数传入的参数是可变的。在C语言中,这是靠宏来实现的。
到了C++11,我们可以轻易地使用可变参数模板来实现。
下面是一个基本可变参数的函数模板:
template<class ...Args>
void ShowList(Args... args)
{}
sizeof...()
可以计算一个包里有多少个参数。
例1:
传入任意类型任意个数的参数,打印参数个数
template<class ...Args>
void ShowList(Args... args)
{
cout << sizeof...(args) << endl;
}
int main()
{
ShowList(1, 'x', 5.2, true);
return 0;
}
//结果:
//4
例2:
实现print函数,依次打印传入的参数,以逗号分隔。
//退出条件
template<class T>
ostream& print(ostream& os, const T& t)
{
return os << t;
}
template<class T, class...Args>
ostream& print(ostream& os, const T& t, const Args&...rest)
{
os << t << ", ";
return print(os, rest...); //递归调用
}
int main()
{
print(cout, 'x', 5.6, 42);
return 0;
}
//结果:
//x, 5.6, 42
这里需要用到递归,第一个栈帧,t为’x’,rest参数包有两个参数,打印’x’。递归调用,第二个栈帧把上一层的rest包传入,rest包的第一个参数给t,后面的参数给新的参数包,新的rest参数包只剩一个参数。以此类推,当rest成为空包时,就会去调用最上面那个模板函数,因为此时它的参数是最匹配的,最后打印最后一个参数结束。
注意:
下面是一个典型的错误
template<class T, class...Args>
ostream& print(ostream& os, const T& t, const Args&...rest)
{
if (sizeof...(rest) == 0)
{
return os << t;
}
os << t << ", ";
return print(os, rest...); //错误C2672“print”: 未找到匹配的重载函数
}
递归的结束条件不能写在里面,因为 if 在运行时才判断的逻辑,编译时不会看,而模板实例化是在编译时进行的,当rest为空时,没有对应参数的模板可以实例化。
我们可以再提供一个单参数的函数来确保编译通过:
ostream& print(ostream& os)
{
return os;
}
template<class T, class...Args>
ostream& print(ostream& os, const T& t, const Args&...rest)
{
if (sizeof...(rest) == 0)
{
return os << t;
}
os << t << ", ";
return print(os, rest...);
}
但是这样写又和第一种写法差不多,还是不太推荐。
包扩展(展开):
上面也用到了,我们在形参声明时将...
写在前面,在实参调用是将 ...
写在了后面,后者其实就是包扩展(展开)
调用函数时,将 ...
写在函数后面表示让参数包里面的每一个参数都去调用函数,把 ...
写在函数里面表示调用一次函数,传入参数包里的所有参数。
vector::emplace_back
template <class... Args>
void emplace_back (Args&&... args);
在末端构造并插入元素
在 vector 的末尾插入一个新元素。这个新元素是使用 args 作为其构造函数的参数就地构造的。
现在我们能看懂,这个函数的参数是万能应用+可变参数模板。
这意味着我们可以一次传多个参数
以下分别使用 emplace_back
和 push_back
进行尾插:
int main()
{
list<pair<int, char>> mylist;
mylist.emplace_back(10, 'a'); // 直接调用pair的构造
mylist.emplace_back(20, 'b');
mylist.emplace_back(make_pair(30, 'c'));
mylist.push_back(make_pair(40, 'd'));
mylist.push_back({ 50, 'e' });
for (auto& e : mylist)
{
cout << e.first << ':' << e.second << endl;
}
return 0;
}
emplace_back
和 push_back
的区别在于,emplace_back
是直接构造,而 push_back
会先构造再移动构造,前者的效率略高一点。
int main()
{
list<pair<int, string>> mylist;
mylist.emplace_back(10, "apple"); //直接构造pair
mylist.emplace_back(make_pair(20, "banana")); //直接构造pair
mylist.push_back(make_pair(30, "orange")); //先构造再移动构造
mylist.push_back({ 40, "pear" }); //先构造再移动构造
return 0;
}
先前我们要想改变排序函数的比较逻辑,需要额外传入一个仿函数,排序的逻辑越多,需要写的类越多,很不方便。因此,在C++11中出现了lambda表达式。
lambda表达式实际上是一种匿名函数,并且总是写在局部(也可以写成全局)
[capture_list](parameters)mutable->returntype {statement}
各部分说明:
[capture_list]
:捕捉列表,可以捕捉上下文中的变量供lambda函数使用(parameters)
:参数列表,没有参数可以省略mutable
:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)->returntype
:返回值类型,可以省略{statement}
:函数体其中的 []
和 {}
都是不能省略的,但是可以为空,因此一个最简单的lambda表达式是:[]{}
捕获列表说明:
捕获列表声明了上下文中哪些数据可以被lambda函数使用,以及传递方式是传值还是传引用
[var]
:表示传值捕捉变量 var
,捕捉多个变量中间用 ,
隔开,默认传值捕获后带有const属性[=]
:表示传值捕捉所有父作用域中的变量(包括 this
)[&var]
:表示传引用捕捉变量 var
[&]
:表示传引用捕捉所有父作用域中的变量(包括 this
)注意:
父作用域指lambda表达式所在的这一层代码块
捕捉列表不允许变量重复传入,比如 [=, a]
,=
已经以值传递方式捕捉了所有变量,a
重复捕捉。
lambda 表达式之间不能互相赋值
因为底层每个lambda表达式都会被转换成一个仿函数类型,仿函数类的名称为 lambda+uuid,uuid对每个lambda表达式都是唯一的,这样其实每个lambda表达式之间的类型都不同,所以不能相互赋值。
但是lambda表达式可以赋值给相同类型的函数指针:
void(*PF)();
auto f = [] {cout << "hello" << endl; };
PF = f; // 可以赋值,但是不建议
Add
int a = 0, b = 1;
auto Add1 = [](int x, int y)->int {return x + y; };
cout << Add1(a, b) << endl; //1
// 可以省略返回类型
auto Add2 = [](int x, int y) {return x + y; };
cout << Add2(a, b) << endl; //1
因为lambda表达式本身是匿名的,所以要有一个对象来接收之后方可调用。
使用捕捉列表直接捕捉变量:
捕获上面的 a 和 b,后面的参数列表也可以省略
auto Add3 = [a, b] {return a + b; };
cout << Add3() << endl; //1
Swap
经典写法,传参:
auto Swap1 = [](int& x, int& y) {
int tmp = x;
x = y;
y = tmp;
};
Swap1(a, b);
cout << a << ' ' << b << endl;
使用捕获,注意加 &
表示传引用:
auto Swap2 = [&a, &b] {
int tmp = a;
a = b;
b = tmp;
};
Swap2();
cout << a << ' ' << b << endl;
如果父作用域的变量太多,你的lambda表达式需要捕捉的也多,可以用 [=]
和 [&]
来全部捕捉
// 传值捕捉
int c = 2, d = 3, e = 4, f = 5, g = 6, ret;
auto func1 = [=] {
return c + d * e / f + g;
};
cout << func1() << endl;
// 传引用捕捉
auto func2 = [&] {
ret = c + d * e / f + g;
};
func2();
cout << ret << endl;
还可以混合使用:
// ret传引用捕捉,其余的传值捕捉
auto func3 = [=, &ret] {
ret = c + d * e / f + g;
};
func3();
cout << ret << endl;
控制排序的比较逻辑,当成仿函数使用:
struct Goods
{
string _name; // 名字
double _price; // 价格
int _evaluate; // 评价
Goods(const char* str, double price, int evaluate)
: _name(str)
, _price(price)
, _evaluate(evaluate)
{}
};
void test1()
{
vector<Goods> v = { {"苹果",2.4,8},{"香蕉",3,4},{"橘子",1.3,5},{"梨",2.2,3} };
// 按价格升序排序
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {return g1._price < g2._price; });
for (auto& e : v)
{
cout << e._name << ' ' << e._price << ' ' << e._evaluate << endl;
}
}
//橘子 1.3 5
//梨 2.2 3
//苹果 2.4 8
//香蕉 3 4
lambda表达式用起来很方便,那么它能替代仿函数吗?
答案是不能,比如在模板参数方面,lambda表达式返回的是一个可调用对象,它无法作为模板实参。
到现在为止我们一共学习了三种可调用对象
下面写一个函数模板,其中一个函数参数用来传入这三种可调用对象:
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;
}
};
void test3()
{
// 函数名
cout << useF(f, 11.11) << endl;
// 函数对象
cout << useF(Functor(), 11.11) << endl;
// lamber表达式
cout << useF([](double d)->double { return d / 4; }, 11.11) << endl;
}
//count:1
//count:006463DC
//5.555
//count:1
//count:006463E8
//3.70333
//count:1
//count:006463EC
//2.7775
通过结果可以看出,这个函数模板被实例化了三份。效率较为低下。
包装器可以很好地解决这个问题
包装器的作用就是将返回值类型和参数类型相同的可调用对象的类型进行统一。
function - C++ Reference (cplusplus.com)
function是一个类模板,原型:
template <class T> function; // undefined
template <class Ret, class... Args> class function<Ret(Args...)>;
下面是对函数指针,仿函数对象,静态成员函数和非静态成员函数的包装
需要包含头文件
模板参数传参格式:
std::function<返回类型(参数类型列表)>
int f(int a, int b)
{
return a + b;
}
struct Functor
{
int operator()(int a, int b)
{
return a + b;
}
};
class Plus
{
public:
static int plusi(int a, int b)
{
return a + b;
}
double plusd(double a, double b)
{
return a + b;
}
};
void test4()
{
// 函数指针
std::function<int(int, int)> func1 = f;
cout << func1(1, 2) << endl; //3
// 函数对象
std::function<int(int, int)> func2 = Functor();
cout << func2(1, 2) << endl; //3
// 静态成员函数(需要指定类域)
std::function<int(int, int)> func3 = Plus::plusi;
cout << func3(1, 2) << endl; //3
// 非静态成员函数
std::function<double(Plus, double, double)> func4 = &Plus::plusd;
cout << func4(Plus(), 1.1, 2.1) << endl; //3.2
// lambda表达式
std::function<int(int, int)> func5 = [](int a, int b) {return a + b; };
cout << func5(1, 2) << endl; //3
}
尤其要注意非静态成员函数的包装,函数需要取地址,参数类型列表需要增加一个类名,因为成员函数有一个隐含的this指针,在调用的时候也要多传一个对象,因为要通过这个对象来调用成员函数。
150. 逆波兰表达式求值 - 力扣(LeetCode)
对这道题,我们有了一个新写法:
使用map映射运算符与对应的函数功能;map
,map的第二个模板参数用来传入包装器,指定了函数的返回类型和参数类型。接下来使用列表初始化,存入相应的pair,运算符和lambda表达式进行映射。
class Solution {
public:
int evalRPN(vector<string>& tokens) {
stack<long long> s;
map<string, function<long long(long long, long long)>> opFuncMap =
{
{"+", [](long long x, long long y) {return x + y; }},
{"-", [](long long x, long long y) {return x - y; }},
{"*", [](long long x, long long y) {return x * y; }},
{"/", [](long long x, long long y) {return x / y; }},
};
for (auto& str : tokens)
{
if (opFuncMap.count(str))
{
long long right = s.top();
s.pop();
long long left = s.top();
s.pop();
s.push(opFuncMap[str](left, right));
}
else
{
s.push(stoi(str));
}
}
return s.top();
}
};
把可调用对象用包装器包装一下,就可以解决刚开始那个问题:
void test3()
{
// 函数名
std::function<double(double)> func1 = f;
cout << useF(func1, 11.11) << endl;
// 函数对象
std::function<double(double)> func2 = Functor();
cout << useF(func2, 11.11) << endl;
// lamber表达式
std::function<double(double)> func3 = [](double d)->double { return d / 4; };
cout << useF(func3, 11.11) << endl;
}
//count:1
//count : 00AEF4D0
//5.555
//count : 2
//count : 00AEF4D0
//3.70333
//count : 3
//count : 00AEF4D0
//2.7775
可以看出,这次模板只实例化出了一份
还有一个问题,非静态成员函数的包装器需要额外传入一个参数来表示 this 指针,这就导致它的类型即使包装后也无法与其他可调用对象统一。
要解决这个问题就需要用到bind
bind - C++ Reference (cplusplus.com)
bind是一个函数模板,它可以用来调整可调用对象的参数。接收一个可调用对象,生成一个新的可调用对象来”适应“原对象的参数列表。
原型:
//simple(1)
template <class Fn, class... Args>
/* unspecified */ bind (Fn&& fn, Args&&... args);
//with return type (2)
template <class Ret, class Fn, class... Args>
/* unspecified */ bind (Fn&& fn, Args&&... args);
先传可调用对象,然后传参数包,其中第一个参数传入 Plus()
进行绑定,以后使用bind返回的对象来调用都自动传这个参数,后面两个参数 placeholders::_1, placeholders::_2
用来占位。表示你最后调用所需要传的参数以及传入的顺序。
class Plus
{
public:
int plusd(int a, int b)
{
return a + b;
}
};
void test4()
{
// 直接包装,3个参数
std::function<int(Plus, int, int)> func3 = &Plus::plusd;
cout << func3(Plus(), 1, 2) << endl; //3
// 使用bind,2个参数
std::function<int(int, int)> func4 = std::bind(&Plus::plusd, Plus(), placeholders::_1, placeholders::_2);
cout << func4(1, 2) << endl; //3
}
还可以使用bind改变参数顺序
int f(int a, int b)
{
return a - b;
}
void test4()
{
// 直接包装
std::function<int(int, int)> func5= f;
cout << func5(1, 2) << endl;
// 使用bind,将两个参数位置对调
std::function<int(int, int)> func6 = std::bind(f, placeholders::_2, placeholders::_1);
cout << func6(1, 2) << endl;
}
//-1
//1
调整顺序后1传给了 placeholders::_1
的位置,也就是b,2传给了 placeholders::_2
的位置,也就是a,结果为1。