目录
一、初始化列表
二、变量类型的推导
1、auto
2、decltype
三、右值引用
1、左值与右值
2、关于左值引用、右值引用的问题
1、左值引用可以引用右值吗?
2、右值引用可以引用左值吗?
3、右值引用之后的问题
3、移动构造、移动拷贝
1、引用的价值
2、移动构造、移动赋值
4、完美转发
5、补充说明
四、关于默认成员函数的关键字
1、final
2、override
3、default
4、delete
四、可变参数模板
1、概念
2、模板包的展开
3、STL中具体应用
五、lambda表达式
1. lambda表达式各部分说明
2. 捕获列表说明
3、lambda表达式底层实现
六、包装器
1、包装器
2、bind
3、具体应用
总结
一、初始化列表
在C++98标准中{ },仅仅应用于数组,struct, class等
int a = 10;
int b{10};
int c = {10};
这三种写法都是可以的,并且STL的所有容器都支持初始化列表的方式进行初始化
vector<int> v = {1, 2, 3, 4, 5, 55, 6, 7, 6, 4};
这样使用容器就会变得更加灵活,而所谓的初始化列表本质其实是在调用构造函数
以Date类举例
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(2022, 11, 20);
Date d2 = {2022, 11, 20};
return 0;
}
其实所谓的初始化列表是C++11添加的一种容器
我们使用typeid查看它的类型
auto it = {1, 2, 3, 4, 5, 6, 0, 7, 8, 9};
cout << typeid(it).name() << endl;
同时STL的每一个容器的构造函数都多了一项 initializer_list
原本只有单参数构造函数支持的隐式类型转换,有了初始化列表,多参数构造函数也支持了
总结:C++11以后,一切对象都可以使用初始化列表,但是建议普通对象还是使用以前的方式初始化,容器有需要可以使用初始化列表
二、变量类型的推导
1、auto
//常用场景
unordered_map<string, int> m;
unordered_map<string, int>::iterator it = m.begin();
auto it = m.begin();
我们正常要写迭代器,变量的类型太长,这时可以使用auto,简化代码,让编译器自动推断类型
2、decltype
乍一看decltype与auto十分的相似,但是它们具有本质的区别
auto是自动推导变量,推导之后,不能使用auto这个类型进行创建新的变量
而decltype是获取变量的类型,可以使用decltype创建变量
int x = 10;
auto y = 10.1;
decltype(x) z = 11.1;
cout << y << endl;
cout << z << endl;
auto推导y一定是一个浮点数类型,而z是一个int
三、右值引用
1、左值与右值
总结一句:左值是可以取地址的,左值不一定能够被修改,例如const修饰的变量
右值一般都是:字面量, 表达式返回值, 函数返回值,临时对象也可以被认为是右值
2、关于左值引用、右值引用的问题
1、左值引用可以引用右值吗?
左值是不能直接引用右值的,但是可以在左值引用前面加上const
template<class T>
void Func(const T& x)
{
}
x既可以接收左值也可以接收右值
2、右值引用可以引用左值吗?
答案是不能直接引用,但是可以引用move之后的左值
int a = 10;
int&& ra = move(a);
move 本意为 "移动",但该函数并不能移动任何数据,它的功能很简单,就是将某个左值强制转化为右值。
基于 move() 函数特殊的功能,其常用于实现移动语义。
3、右值引用之后的问题
也就是说右值引用之后,右值就会变成左值,这里与后面完美转发有关
3、移动构造、移动拷贝
1、引用的价值
引用主要是用来减少拷贝,提高效率
左值引用常用的场景:
1、做参数:a、减少拷贝,提高效率 b、做输出型参数
2、做返回值:a、减少拷贝,提高效率 b、引用返回,可以修改返回对象
只有左值引用很难处理以下场景
to_string的返回值是一个string对象,要直接返回它代价较高,如果返回引用,就会出现野指针问题,to_string内部一定会定义一个临时string对象,它出了作用域就会销毁
如果要想提高效率,只能使用输出型参数,传一个string&给to_string,但是是不符合使用习惯的。
C++11的右值引用一个重要功能就是要解决上面的问题。
2、移动构造、移动赋值
在C++11以后,将右值分为两类1、内置类型右值-纯右值 2、自定义类型右值-将亡值
为了更好的说明这个问题,我使用了之前实现的string
namespace ww
{
class string
{
public:
typedef char *iterator;
iterator begin()
{
return _str;
}
iterator end()
{
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)
{
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;
}
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];
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;
}
private:
char *_str;
size_t _size;
size_t _capacity;
};
string to_string(int value)
{
bool flag = true;
if (value < 0)
{
flag = false;
value = 0 - value;
}
string str;
while (value > 0)
{
int x = value % 10;
value /= 10;
str += ('0' + x);
}
if (flag == false)
{
str += '-';
}
std::reverse(str.begin(), str.end());
return str;
}
}
int main()
{
ww::string str = ww::to_string(-2345);
return 0;
}
在g++关闭所有优化的条件下,调用to_string会进行两次深拷贝
一次拷贝构造是to_string里面的临时对象拷贝给返回的临时对象,另一次拷贝是临时对象拷贝给在main函数栈帧的str,调用一次to_string就要深拷贝两次,这个代价就十分的巨大了
这时右值拷贝的作用就体现了,不过它不是直接起作用,而是间接起作用,实现方式就是移动构造和移动拷贝
在STL容器中添加移动构造,移动赋值
所谓的移动构造和移动赋值 ,其实是通过右值引用,因为只有将亡值会走移动构造,移动赋值
而将亡值除了作用域很快就会销毁,我们可以使新的类的指针指向将亡值的数据,将亡值的指针指向新构造的类,然后让将亡值的析构函数来处理新构造类的原始数据,这样就延长了资源的生命周期,而没有改变对象的声明周期。这样就极大的提高了效率,如果没有移动构造移动赋值,只能走拷贝构造,赋值重载,进行深拷贝,代价极大,而移动构造和移动赋值,只是更换指针指向方向,消耗几乎可以忽略不记,例如进行红黑树的拷贝也就成了现实,红黑树如果只进行深拷贝代价十分的大,要深拷贝整棵树,而移动构造和移动赋值可以只更改树根节点指针方向
// 移动构造
string(string &&s)
: _str(nullptr), _size(0), _capacity(0)
{
cout << "string(string&& s) -- 移动语义" << endl;
swap(s);
}
// 移动赋值
string &operator=(string &&s)
{
cout << "string& operator=(string&& s) -- 移动语义" << endl;
swap(s);
return *this;
}
只有将亡值才会走移动构造移动赋值,不是将亡值的还会走原来的拷贝构造赋值操作符重载
STL容器,插入接口C++11以后都提供了右值版本,插入过程中,如果传递的对象是右值,那么进行资源转移,减少拷贝
4、完美转发
template<class T>
void Func(T&& x)
{
}
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 <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;
}
template <typename T>
void PerfectForward(T &&t)
{
Fun(forward<T>(t));//使用完美转发,保持属性不变
}
5、补充说明
如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值
四、关于默认成员函数的关键字
1、final
class A final
{
private:
int _a;
};
class B : public A
{
private:
int _b;
};
2、override
class A
{
public:
virtual void show()
{
cout << _a << endl;
}
private:
int _a = 0;
};
class B : public A
{
public:
virtual void show() override
{
cout << _b << endl;
}
private:
int _b = 0;
};
3、default
class A
{
public:
A(const A& a)
{
_a = a._a;
}
private:
int _a = 0;
};
这时我们使用default强制生成默认构造函数
class A
{
public:
A(const A& a)
{
_a = a._a;
}
A() = default;
private:
int _a = 0;
};
4、delete
1、如何利用delete,定义一个只能在堆上开空间的类?
很显然,只能在堆上定义的类,一定要使用new,使用new就会调用构造函数,所以我们不能够修改构造函数,那么我们就把析构函数delete就可以了,在栈上的对象,出了作用域要销毁,就要调用析构函数,无法调用析构函数,就会报错,可是删除了析构函数,要怎么回收这个类呢?
我们只要自己手动写一个类似析构函数的成员函数,然后手动调用就好了
class A
{
public:
~A() = delete;
A()
{
_a = new int[10];
}
void destory()
{
delete[] _a;
operator delete(this);
}
private:
int *_a;
};
int main()
{
A a;
return 0;
}
先来看一下直接在栈上创建对象
接下来是在堆上创建对象
class A
{
public:
~A() = delete;
A()
{
_a = new int[10];
}
void destory()
{
delete[] _a;
operator delete(this);
}
private:
int *_a;
};
int main()
{
A* a = new A();
a->destory();
return 0;
}
注意:这里destory函数销毁自己使用的是operator delete ,不可以使用delete,因为delete会自动调用析构函数可是我们已经删除了析构函数,所以不可以使用delete,但是我们可以使用operator delete,它类似于free,不会调用析构函数
四、可变参数模板
1、概念
// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
template <class ...Args>
void ShowList(Args... args)
{}
我们可以使用sizeof来获取模板参数的个数
template<class ...Args>
void showList(Args...args)
{
cout << sizeof...(args) << endl;
}
int main()
{
showList(1,2,3,4,5,5,5,6);
showList();
showList(1,2);
return 0;
}
2、模板包的展开
template <class T, class... Args>
void showList(T value, Args... args)
{
cout << value << endl;
showList(args...);
}
//递归终止函数
template <class T>
void showList(T t)
{
cout << t << endl;
}
int main()
{
showList(1, 2, 3, 4, 5, 5, 5, 6);
// showList();
showList(1, 2, 3);
return 0;
}
template <class T>
void PrintArg(T t)
{
cout << t << " ";
}
//展开函数
template <class ...Args>
void ShowList(Args... args)
{
int arr[] = { (PrintArg(args), 0)... };
cout << endl;
}
int main()
{
ShowList(1);
ShowList(1, 'A');
ShowList(1, 'A', std::string("sort"));
return 0;
}
3、STL中具体应用
在某些场景下emplace的效率要高于insert
对于内置类型没有差别,自定义类型例如像unordered_map这样调用insert要插入键值对,或者其它类型对象的场景,效率更高
拿unordered_map举例,insert要插入一个pair,我们要么创建一个临时对象,或者make_pair,
五、lambda表达式
1. lambda表达式各部分说明
2. 捕获列表说明
3、lambda表达式底层实现
六、包装器
1、包装器
包装器算是C++11在具体后端开发使用的较多的特性
我们可以将函数通过包装器,包装起来,然后放入到map等容器中,根据实际情况来调用
std::function在头文件<functional>
// 类模板原型如下
template <class T> function; // undefined
template <class Ret, class... Args>
class function<Ret(Args...)>;
模板参数说明:
Ret: 被调用函数的返回类型
Args…:被调用函数的形参
int add(int a, int b)
{
return a + b;
}
struct Func
{
int operator()(int a, int b)
{
return a + b;
}
};
class Plus
{
public:
static int addi(int a, int b)
{
return a + b;
}
double addd(double a, double b)
{
return a + b;
}
};
这里有三种不同的函数,分别演示如何包装
function<int(int, int)> f1 = add;//没有什么特殊条件直接绑定
function<int(int, int)> f2 = Func();//仿函数要绑定对象
function<int(int, int)> f3 = Plus::addi;//静态成员函数指定类域
function<double(Plus, double, double)> f4 = &Plus::addd;//非静态成员函数需要取地址,并且传类名
2、bind
// 原型如下:
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);
3、具体应用
150. 逆波兰表达式求值 - 力扣(LeetCode)
class Solution {
public:
int evalRPN(vector<string>& tokens) {
stack<long long> st;
unordered_map<string, function<long long(long long, long long)>> mp =
{
{"+", [](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(const auto& e : tokens)
{
if(mp.count(e))
{
long long right = st.top();
st.pop();
long long left = st.top();
st.pop();
st.push(mp[e](left, right));
}
else
{
st.push(stoll(e));
}
}
return st.top();
}
};
总结
例如:以上就是今天要讲的内容,本文仅仅简单介绍了C++11相关特性