TOC
在2003年C++标准委员会曾经提交了一份技术勘误表(简称TC1),使得C++03这个名字已经取代了C++98称为C++11之前的最新C++标准名称。不过由于C++03(TC1)主要是对C++98标准中的漏洞进行修复,语言的核心部分则没有改动,因此人们习惯性的把两个标准合并称为C++98/03标准。
相比于C++98/03,C++11则带来了数量可观的变化,其中包含了约140个新特性,以及对C++03标准中约600个缺陷的修正,这使得C++11更像是从C++98/03中孕育出的一种新语言。基于此,C++11能更好地用于系统开发和库开发、语法更加泛华和简单化、更加稳定和安全,不仅功能更强大,而且能提升程序员的开发效率。
C++11增加的语法特性非常篇幅非常多,这里没办法一 一展开,所以本篇幅主要介绍实际中比较实用的语法。若要深层次去了解,可以去C++官方库查询学习:point_right:C++11官方库
在C++98中,标准允许使用花括号{}对数组或者结构体元素进行统一的列表初始值设定。
数组或者结构体对象后面接着{},{}里是要初始化的参数
{}初始化同样适用于new表达式
int* ptr1 = new int[4]{ 1,2,3,4 };
创建对象时也可以使用列表初始化方式调用构造函数初始化
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(2002, 12, 20);//()传参构造函数初始化
Date d2{ 2001,3,28 };//通过{}列表初始化的方式构造函数初始化
Date d3 = { 2000,11,24 };//也可以带=
return 0;
}
有时候我们会看到这样的初始化
vector<int> vt{ 1,2,3,4,5 };
list<int> lt{ 3,4,5,6,7 };
根据图中所示,由于Date是有多个参数的构造函数,所以可以通过传多个参数去初始化对象;但是vector和list是单个参数的构造函数,并且没有多个参数的构造函数,那是怎么做到下面的初始化呢?
使用初始化列表也只能进行固定参数的初始化,如果想要做到和 STL 一样有任意长度初始化的能力,可以使用 std::initializer_list 这个轻量级的类模板来实现。
initializer_list底层是一个常量数组,存放在常量区(不能随意修改),initializer_list就是对这个常量数组进行封装。
:point_right: initializer_list
initializer_list的特性
就像刚开始那样的初始化那样
vector<int> vt{ 1,2,3,4,5 };
list<int> lt{ 3,4,5,6,7 };
其其他容器支持列表初始化的原因是因为这些容器支持initializer_list类的构造
比如在vector这里,若vector没有实现initializer_list的构造,就不能实现列表初始化,并且识别为类型转化(是否是initializer_list->xxx的转换?)
实现了initializer_list构造函数就能用了
最好也增加一个用initializer_list为参数的赋值运算符重载函数,来支持对列表对象进行赋值。但实际上不需要。如果没有实现,那么编译器会走initializer_list构造函数
vector支持initializer_list初始化和赋值的简易代码如下
template<typename T>
class vector
{
public:
typedef T* iterator;
vector()//构造函数
//初始化列表-必要用的情况:const成员;没有默认构造的自定义类型成员
:_start(nullptr)
, _finish(nullptr)
, _endofstorage(nullptr){}
vector(const initializer_list<T>& il)//initializer_list构造函数
:_start(nullptr)
, _finish(nullptr)
, _endofstorage(nullptr)
{
reserve(il.size());
/* typename initializer_list<T>::iterator ilt = il.begin();
while (ilt != il.end())
{
push_back(*ilt);
ilt++;
}*/
for (auto& e : il)
{
push_back(e);
}
}
vector<T>& operator=(const initializer_list<T>& il)
{
vector<T> tmp(il);
std::swap(_start, tmp._start);
std::swap(_finish, tmp._finish);
std::swap(_endofstorage, tmp._endofstorage);
return *this;
}
size_t size()const
{
return _finish - _start;
}
size_t capacity() const//容量
{
return _endofstorage - _start;
}
iterator begin()
{
return _start;
}
iterator end()
{
return _finish;
}
void reserve(size_t n)//扩容
{
if (n > capacity())
{
size_t oldsize = size();
T* tmp = new T[n];//构造一个tmp
//memcpy-浅拷贝
//if (_start )//需要拷贝---有数据才要拷贝
//{
// //memcpy(tmp, _start, sizeof(T) * oldsize);浅拷贝·1
// tmp = _start;
// delete[] _start;
//}
if (_start)
{
for (size_t i = 0; i < oldsize; i++)
{
tmp[i] = _start[i];
}
delete[] _start;
}
_start = tmp;
//operator=-深拷贝
_finish = _start + oldsize;
_endofstorage = _start + n;
}
}
void push_back(const T& val)//尾插
{
if (_finish == _endofstorage)//容量为0或者满了都要扩容
{
size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
reserve(newcapacity);
}
*_finish = val;
++_finish;
}
void swap(vector<T>& v)//交换
{
std::swap(_start, v._start);
std::swap(_finish, v._finish);
std::swap(_endofstorage, v._endofstorage);
}
private:
iterator _start;//0的位置
iterator _finish;//最后一个成员变量的下一个位置
iterator _endofstorage;//
};
void testinlist1()
{
vector<int> vt1{ 1,2,3,4,5 };
for (auto e : vt1)
{
cout << e << " ";
}
cout << endl;
vt1 = { 10,20,30,40,50 };
for (auto e : vt1)
{
cout << e << " ";
}
cout << endl;
}
C++11之前(C++98)auto的用法为声明变量为自动变量,自动变量(Automatic Variable)指的是局部作用域变量,具体来说即是在控制流进入变量作用域时系统自动为其分配存储空间并在离开作用域时释放空间的一类变量,以上这个过程称为自动生命周期。
但变量默认拥有自动生命周期,所以这个用法是多余的。
//C++98auto的用法
int a = 10;//自动生命周期
auto int b = 10;//自动生命周期
所以在C++11的时候摒弃了之前的用法,取而代之的是auto用来推演变量类型:通过返回值的类型自动推演变量的类型
auto dou = 1.34;//返回值类型为double,auto推导变量dou的类型也为double
auto str = "jinitaimei";//返回值类型为string,auto推导变量str的类型也为string
list<int> lt;
lt.push_back(11);
lt.push_back(12);
lt.push_back(13);
lt.push_back(14);
list<int>::iterator it1 = lt.begin();//这里迭代器的类型是list<int>::iterator
cout << *it1 << endl;
auto it2 = lt.begin();//这里=的右边的返回值类型是list<int>::iterator,所以auto在等号在=左边自动推导变量it2的类型为list<int>::iterator
cout << *it2 << endl;
typeid(变量).name()获取变量的类型名;对于非引用类型,是在编译期间识别;对于引用类型,是在运行时识别
int a = 10;
cout << typeid(a).name() << endl; //变量a的类型:int
cout << typeid(typeid(a).name()).name() << endl; //变量名int的类型:const char *
const int b = 7;
cout << typeid(b).name() << endl; //加const不改变类型:int
int& c = a;
cout << typeid(c).name() << endl; //加引用&不改变类型:int
const int& f = a;
cout << typeid(f).name() << endl; //int
int* d;
cout << typeid(d).name() << endl; //int *
const int* e;
cout << typeid(e).name() << endl; //const int *
然而auto在有些场景就不能推导类型了,比如没有返回值接收的话,就不能自动推导变量的类型了;而typeid().name()只能用来打印类型名,这时就需要另外一个关键字来处理这些场景
关键字decltype将变量的类型声明为表达式指定的类型
用法:decltype(表达式)变量:把变量的类型推导为括号里表达式的类型
template<class T1,class T2>
void multis(T1 t1, T2 t2)
{
decltype(t1 * t2) ret;//这里ret没有接收返回值,所以不能用auto来推导ret的类型;但可以通过decltype来推导
cout <<"ret的类型为:" << typeid(ret).name() << endl;
}
int main()
{
int x = 6;
double y = 2.66;
multis(x, y);
return 0;
}
由于C++中NULL被定义成字面量0,这样就可能回带来一些问题,因为0既能指针常量,又能表示整形常量。所以出于清晰和安全的角度考虑,C++11中新增了nullptr,用于表示空指针。
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
模型:
底层是迭代器for(元素类型 元素对象:容器对象)
{
循环体
}
根据C++官网可以查到容器在C++11上的改动
array即数组,跟C++11之前的数组(c语言)最重要的区别:C++11之前的数组对于越界访问是抽查,而C++11的array对于越界访问更严格。但相比于vector的各自功能,array还是稍稍逊色
forward_list是单链表,带哨兵位的单向的单链表,每个节点只有一个指针next指向后一个节点,因此只能单向遍历|官网:point_right: forward_list
功能有如下图
大概实现可以看我这篇 C语言实现单向链表
unordered_map和unordered_set以及unordered_multimap和unordered_multiset在之前的博客有介绍过::point_right: unordered_map、unordered_set 。也可以去C++官方库看:point_right: unordered_set , unordered_map
final修饰的类不能被继承;final修饰的虚函数不能被重写;override用来判断虚函数是否完成了重写,在之前的篇幅中有提到过,这里就不细嗦
传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,所以从现在开始我们
之前学习的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名
左值是什么呢?左值有以下特性:
int* p = new int(1);
int*& rp = p;//对p的引用
int& p_value = *p;//对p解引用后的数据的引用
int x = 10;
int& rx = x;//对x的引用
const int y = 20;
const int& ry = y;//对y的引用
右值是什么?右值有以下特性
int x = 10;
int y = 90;
//以下是常见的右值
10//字面常量
x + y;//表达式返回值
min(x, y);//函数返回值
int &&pxy=x+y;//对x+y右值引用
那么左值引用和右值引用有什么关联呢?
int mini(int x, int y)
{
return x < y ? x : y;
}
int x = 10;
int y = 90;
//以下是右值
10;
x+y;
mini(x,y);
//////////////左值引用右值
//int&lret= x + y;//不能引用
const int&lret= x + y;//可以引用
const int& lmin = mini(x, y);//可以引用
//右值引用左值
//int&& rx1 = x;//不能引用
int&& rx2 = move(x);//move之后的左值可以右值引用
那好端端的有了引用(左值引用),为什么还要在C++11提出右值引用呢?
无论左值引用还是右值引用,功能之一都是减少拷贝,减少消耗
左值引用在以下场景不适用
template<class T>
const T& fun3(const T& object)
{
T ret;
ret.resize(object.size());
for (size_t i = 0; i < ret.size(); i++)
{
ret[i] = i;
}
//...... 这里对ret进行操作
return ret;//函数结束,参数ret的生命周期结束销毁了,一是参数ret传不出去,二是传引用返回给外面那层,此时再访问到参数ret算是越界访问了!
}
int main()
{
vector<int>v(10,0);
vector<int> ret3=fun3(v);
}
解决参数出了作用域传不出去这样的问题的办法有很多种,这里我罗列几种
通过输出型参数ret3,在函数fun3内参数ret赋值给retu,成功把参数ret传出来
template<class T>
void fun3(const T& object,T&retu)
{
T ret;
ret.resize(object.size());
for (size_t i = 0; i < ret.size(); i++)
{
ret[i] = i;
}
//...... 这里对ret进行操作
retu = ret;
}
int main()
{
vector<int>v(10,0);
vector<int> ret3;
fun3(v,ret3);
}
这种做法是C++11之前的普遍做法
这种做法至少进行一次拷贝构造(老旧的编译器会进行两次拷贝构造),针对内置类型消耗还算小,但针对自定义类型或者容器类参数比如vector<vector < vector > >>这种,消耗非常大,不推荐这样用。
template<class T>
T fun2( T& object)
{
T ret;
ret.resize(object.size());
for (size_t i = 0; i < ret.size(); i++)
{
ret[i] = i;
}
//...... 这里对ret进行操作
return ret;//传值返回
}
int main()
{
vector<int>v(10,0);
vector<int> ret2 = fun2(v);
}
针对上面的提到的参数出了函数作用域被销毁了,参数传不出去的问题,右值引用可以解决。
下面介绍右值引用的几大作用。
这里用到一个string类来介绍左值引用和值返回的不足之处。
namespace pjl
{
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);
}
// s1.swap(s2)
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;
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)
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; // 不包含最后做标识的\0
};
string to_string(int value)
{
bool flag = true;
if (value < 0)
{
flag = false;
value = 0 - value;
}
pjl::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()
{
pjl::string s1{ "xxxxx" };//左值
pjl::string ret1(s1);//s1为左值,用到拷贝构造
pjl::string s2{ "aaaaa" };
s2 = s1;//s1为左值,用到赋值重载
pjl::string s3(move(s1));//move之后的左值为右值
s3 = "bbbbb";//右值
return 0;
}
一般情况下,先是返回值拷贝构造一个临时对象,然后临时对象拷贝构造参数对象,这里一共拷贝构造了两次。这是老旧的编译器所做的
现在的编译器优化了效率,会跳过返回值拷贝临时对象这一步,直接用返回值拷贝构造参数对象,即一次拷贝构造
老旧的编译器会先用返回值拷贝构造临时对象,然后临时对象再赋值给参数对象
但现在的编译器优化了效率,跳过返回值拷贝构造临时对象这步,直接用返回值赋值给参数对象,这里的赋值重载也是一次拷贝构造(这里显示两次拷贝构造的原因是因为赋值重载函数用到了拷贝构造)
但是这里的to_string用的值返回,意味着返回值都需要用到拷贝构造,具有一定的损耗。当函数返回对象是一个局部变量时,倘若用引用返回,在函数销毁时返回对象也随之销毁,参数传不出去。右值引用能解决以上问题。
在上面的string类实现移动构造和移动赋值,能减少拷贝构造次数,减少损耗
如字面所述,移动+构造,若传的参数是右值,会将传入的右值的资源移动过来构造自己,避免了深拷贝,即移动的时候被移动的右值对象的资源会被转移。
// 移动构造
string(string&& s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
cout << "string(string&& s) -- 移动构造" << endl;
swap(s);
}
说明一下,这里移动构造函数是通过把右值对象的内容和string类成员内容进行交换,相比于string类的拷贝构造函数少了一次拷贝构造,降低了损耗。
老旧的编译器会先用返回值对象拷贝构造临时对象,然后用临时对象移动构造参数对象
现在的编译器优化了,跳过了返回值对象拷贝构造临时对象这步,直接用返回值对象移动构造参数对象
移动赋值也如字面意思,移动+赋值,若传的是右值对象,会将右值的资源移动过来赋值给自己,避免了深拷贝,且被移动的右值对象的资源会被转移
//// 移动赋值
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移动赋值" << endl;
swap(s);
return *this;
}
说明一下:这里移动赋值函数是将右值对象的内容和string类成员的内容进行交换,相比于拷贝赋值函数少了一次拷贝构造,降低了损耗。
老旧的编译器会先用返回值对象拷贝构造临时对象,然后用临时对象移动赋值给参数对象
现在的编译器优化了,跳过了返回值对象拷贝构造临时对象这步,直接用返回值对象移动赋值给参数对象
现在动态的来看移动构造
移动赋值
综上,移动构造和移动赋值的作用是通过移动右值的资源,减少了拷贝构造次数,减少了损耗
首先需要模板,然后在参数列表中是模板参数 &&
模板中的&&不代表右值引用,而是万能引用也称折叠引用,当传过来是左值时,&&折叠成&,当传过来是右值时,则是&&。其既能接收左值又能接收右值。
万能引用不仅能接收左值和右值,const左值和const右值也能接收。
下面代码通过万能引用接收左值,右值,const左值,const右值,实例化出不同类型的函数
void Func(int& x)
{
cout << "左值引用" << endl;
}
void Func(const int& x)
{
cout << "const 左值引用" << endl;
}
void Func(int&& x)
{
cout << "右值引用" << endl;
}
void Func(const int&& x)
{
cout << "const 右值引用" << endl;
}
template<class T>
void PerfectForward(T&& t)
{
Func(t);
}
int main()
{
int a = 10;
PerfectForward(a); //左值
PerfectForward(move(a)); //右值
const int b = 20;
PerfectForward(b); //const 左值
PerfectForward(move(b)); //const 右值
return 0;
}
根据传入参数的类型,推导出函数参数的类型然后实例化函数;例如传入参数a(左值),那么PerfectForward的参数t类型是int&
由于PerfectForward函数用的是万能引用,意味着通过PerfectForward函数按照传入参数的类型匹配调用相同类型的Func函数。但这里的参数里,左值和右值类型调用Func函数左值引用版本,const左值和const右值类型调用Func函数const左值引用版本
原因是右值被引用后会导致右值被放到特定的存储位置,因此该右值可以被取地址,也可以被修改,所以在函数PerfectForward后续的使用中右值会被识别成左值。
既经过一次参数传递后,参数的右值属性退化成了左值属性,因此在后续的使用中都退化成了左值。
为了防止右值被引用后退化成左值,这时候需要用到完美转发。
为了保证参数被引用后继续保持参数类型属性,需要在传参时用到完美转发
用法:std::forward<模板参数>(参数)
现在回过头来解决参数ret出了函数作用域,函数内的参数也随之销毁了,参数对象传不出去 的问题
template<class T>
const T& fun3( T&& object)
{
T ret = object;
for (size_t i = 0; i < ret.size(); i++)
{
ret[i] = i;
}
//...... 这里对ret进行操作
return ret;//函数结束,对象没有销毁,通过转移拷贝把对象传了回去
}
int main()
{
vector<int>v(10, 0);
vector<int> ret3 = fun3(v);//传了左值过去
int flag1 = 0;//标记位
原来的C++类中,有6个默认成员函数
依次是:构造函数,析构函数,拷贝构造函数,拷贝赋值函数,取地址重载和const取地址重载
前四个较为重要,默认成员函数是我们没有实现该函数而编译器默认生成。
现C++11新增了两个默认成员函数:移动拷贝函数和移动赋值运算符重载函数。
针对这两个函数需要注意:
这里通过一个简易的string函数和一个Perosn函数调用string
namespace pjl
{
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);
}
// s1.swap(s2)
void swap(string& s)
{
::swap(_str, s._str);
::swap(_size, s._size);
::swap(_capacity, s._capacity);
}
// 移动构造
//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;
//}
// 拷贝构造
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)
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
const char* c_str() const
{
return _str;
}
private:
char* _str=nullptr;
size_t _size=0;
size_t _capacity=0; // 不包含最后做标识的\0
};
}
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:
pjl::string _name;
int _age;
};
可以看到Person函数没有实现拷贝构造函数,析构函数和拷贝赋值函数,那么当右值参数传进来时,Person类则会默认生成移动拷贝构造函数和移动赋值函数
测试代码
int main()
{
Person s1="this is the test";//左值
Person s2 = std::move(s1);//传右值--构造
Person s3;
s3 = std::move(s1);//赋值
return 0;
}
当string类没有实现移动拷贝构造函数和移动赋值重载函数时,Person类生成的默认移动构造和移动赋值函数调用string类的拷贝构造函数和拷贝赋值函数
当string类实现了移动拷贝构造函数和移动赋值函数时,Person类生成的默认移动构造和移动赋值运算符重载函数调用string类的移动构造函数和移动赋值赋值函数
C++11可以更好的控制要使用的默认成员函数,假设要用都某个默认成员函数,但因为某些原因导致这个成员函数无法生成,这时可以用到关键字default。比如我们实现了拷贝构造函数,这时默认的移动构造函数就不会生成了,我们可以通过使用default关键字显示指定移动构造函数生成。
下面的代码生成了构造函数和拷贝构造,这时候想用到移动构造有两种方法,一是自己实现移动构造函数。二是生成默认的移动构造函数,但由于拷贝构造已经实现,所以编译器不会提供默认的移动构造函数,这时候可以使用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)
{}
private:
pjl::string _name;
int _age;
};
自己实现移动构造函数:由于对象p通过右值引用传进来后退化成了左值属性,p的成员_name是左值属性,且是自己实现的string类,所以Person类给 _name初始化调用的是对象p. _name的拷贝构造,因此这里需要给p的成员 _name用完美转发保持右值属性,以保后续调用移动构造
使用关键字default强制生成默认移动构造函数
想要能够限制某些默认函数的生成,在C++98中,是将函数权限设置为private,且只声明不实现,这样在外部调用时就会报错。在C++11只需要在函数声明上加上=delete即可。该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数
class A
{
public:
A() {}
A(const A& aa)//拷贝构造--用传过来的对象拷贝给自己,但是函数是浅析构,析构不到位报错
:p(aa.p)
{}
~A()
{
delete []p;//析构函数只析构一次,若对象不止一个int大小调用则报错
}
private:
int* p = new int[10];
};
这里我写里一个类A,类中成员对象是是一个int类型的指针,指向10int大小的空间。析构函数是析构1个int大小的空间。
//测试代码
int main()
{
A aa;
A bb(aa);
return 0;
}
通过测试,用对象aa去拷贝构造对象bb,拷贝构造时对象aa会创建10int大小的空间,而析构时只析构一个int大小的空间,导致内存泄漏报错。但这样是运行时被检查出来才报错,我想要的时运行前编译时报错。
其一:若我们不想给外部调用拷贝构造函数可以用在C++98的方法:将函数权限设置为private,且只声明实现,这样就能做到编译时报错。
其二:只写拷贝构造函数的声明且后接=delete表示该函数为删除函数即函数没有生成不能调用
lambda表达式书写格式:capture-list mutable -> return-type { statement}
capture-list :捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。必须写
(parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略。有就写
mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。
->returntype:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
{statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。必须写
这里写一个样例
或者是这样(由于捕捉列表没有使用,所以函数体内的参数都是由参数列表定义的,定义了几个参数,在后续调用lambda表达式中就需要传多少个参数)
我用lambda表达式实现了一个交换函数,意为通过lambda表达式把两个变量的值交换(函数体内的参数都是由捕捉列表提供的,所以参数列表不需要定义参数,后续调用lambda表达式也不需要传参)
通过捕捉列表把参数c、d捕捉,然后在函数体内进行交换,但是报错了,原因是此时的捕捉列表捕捉的是父作用域变量值的拷贝,具有常性无法改变且lambda函数总是一个const函数,可以在参数列表后加mutable表示取消参数的常性
添加mutable后运行,通过打印查看参数c、d在lambda表达式内是交换了,但是出了lambda表达式就又换了回来,原因是c、d变量给lambda表达式传的参数是值,改变值不会改变变量,现在改传引用
可以看到变量c、d进行了交换
可以看到变量c、d在lambda表达式下方的话会捕捉不到,原因为编译器具有往上找原则
lambda捕捉父作用域的变量的原则:
混合捕捉:
但是不能重复捕捉::=, a:=已经以值传递方式捕捉了所有变量,捕捉a重复
另外还有这些规则:
这里写了一个内含仿函数成员的类,main函数里实现了两个与Rate类中仿函数成员相同作用的lambda表达式,分别是r2和r3。然后main函数依次调用
class Rate
{
public:
Rate(double rate) : _rate(rate)
{}
double operator()(double money, int year)
{
return money * _rate * year;
}
private:
double _rate;
};
int main()
{
// 函数对象
double rate = 0.49;
Rate r1(rate);//传利率过去
r1(10000, 2);//调用仿函数
// lambda
auto r2 = [=](double monty, int year)->double {return monty * rate * year; };
r2(10000, 2);
auto r3 = [=](double monty, int year)->double {return monty * rate * year; };
r3(10000, 2);
return 0;
}
这里转到反汇编,可以看到在使用方式上,函数对象和lamba表达式完全一样,都是call相应的函数。实际上底层编译器对于lambda表达式的处理方式完全是按照函数对象(仿函数)处理,定义了一个lambda表达式,编译器会自动生成一个类,该类中重载了operator()
在C++98/03,类模板和参数模板只能含固定数量的模板参数,可变参数模板可以含0-N个模板参数
// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
template <class ...Args>
void ShowList(Args... args)
{}
上面参数args前面有。。。表示这是个可变模板参数,我们无法直接获得参数包args中的每个参数,只能通过展开参数包的方式来获取。
template<class T, class ...Args>
void showlist(const T& t)
{
cout << t << endl;//递归终止函数
}
template<class T,class ...Args>
void showlist(T value, Args ...args)
{
cout << value << " ";
showlist(args...);
}
int main()
{
showlist(1);
showlist(1,1.1);
showlist(1, 1.1, string("xxx"));
return 0;
}
展开传过来的参数包,从前往后依次遍历参数,遍历完一个(打印)然后把参数包剩余参数递归传递给showlist函数,当传递到最后一个参数的时候,此时调用的函数匹配递归终止函数。
另外,sizeof...(参数包)能计算出参数包里有多少个参数
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, 'T');
ShowList(1, 'T', std::string("fortunate"));
return 0;
}
把参数包传给showlist函数,showlist函数通过逗号表达式展开其参数包。expand函数中的逗号表达式:(printarg(args), 0),先执行printarg(args),printarg函数是用来处理参数包中的参数(打印参数),然后再得到逗号表达式后面的0。同时还通过C++11中的另一个特性—初始化列表,通过初始化列表来初始化一个变长数组{(printarg(args), 0)...}将会展开成((printarg(arg1),0),(printarg(arg2),0), (printarg(arg3),0), etc... ),这样就能依次遍历参数包里的参数,printarg函数依次处理参数,然后数组依次获得0。最终数组里的元素都为0,元素个数为参数包中的参数个数。这个数组纯粹是通过构造数组的同时展开参数包
在C++11中容器里的函数也有拓展到能使用到可变模板参数,这类函数称为empalce系列函数
list和vector里面都提供有emplace系列函数,支持模板的可变参数,并且支持万能引用,那么相对insert和emplace系列接口的优势在哪?
这里实现了一个string类,里面实现了拷贝构造(深拷贝)、移动构造。通过list的emplace_ back和push_back来比较区别
namespace pjl
{
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);
}
// s1.swap(s2)
void swap(string& s)
{
::swap(_str, s._str);
::swap(_size, s._size);
::swap(_capacity, s._capacity);
}
// 拷贝构造
string(const string& s)
{
cout << "string(const string& s) -- 深拷贝" << endl;
/*string tmp(s._str);
swap(tmp);*/
reserve(s._capacity);
strcpy(_str, s._str);
_size = s._size;
_capacity = s._capacity;
}
// 赋值重载
string& operator=(const string& s)
{
cout << "string& operator=(string s) -- 深拷贝" << endl;
string tmp(s);
swap(tmp);
return *this;
}
// 移动构造
string(string&& s)
{
cout << "string(const string& s) -- 移动拷贝" << endl;
swap(s);
}
// 移动赋值
string& operator=(string&& s)
{
cout << "string& operator=(string s) -- 移动赋值" << endl;
swap(s);
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];
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)
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
const char* c_str() const
{
return _str;
}
private:
char* _str = nullptr;
size_t _size = 0;
size_t _capacity = 0; // 不包含最后做标识的\0
};
string to_string(int value)
{
bool flag = true;
if (value < 0)
{
flag = false;
value = 0 - value;
}
pjl::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;
}
}
运行后可以看到,对于左值,emplace_back和push_back都是深拷贝;但对于右值,emplace是直接拿着参数对象去构造list的结点,而push_back先是构造一个临时对象,然后临时对象移动构造list的结点。构造+移动构造优化成构造,稍稍减少了一些消耗。但对于只有浅拷贝的类,构造+浅拷贝优化成构造,效率大大提升。
这里介绍的是function包装器。
function包装器 也叫作适配器。C++中的function本质是一个类模板,也是一个包装器。是对调用对象的包装,用同一种方法调用不同的对象。(统一了不同对象调用的用法)
function类模板原型
//std::function在头文件<functional>
// 类模板原型如下
template <class T> function; // undefined
template <class Ret, class... Args>
class function<Ret(Args...)>;
模板参数说明:Ret: 被调用函数的返回类型,Args…:被调用函数的形参
下面是使用
#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:
static int plusi(int a, int b)//类中静态成员
{
return a + b;
}
double plusd(double a, double b)
{
return a + b;
}
};
int main()
{
// 函数名(函数指针)
std::function<int(int, int)> func1 = f;
cout << func1(1, 2) << endl;
// 函数对象
std::function<int(int, int)> func2 = Functor();
cout << func2(1, 2) << endl;
// lamber表达式
std::function<int(int, int)> func3 = [](const int a, const int b) {return a + b; };
cout << func3(1, 2) << endl;
// 类的成员函数
std::function<int(int, int)> func4 = &Plus::plusi;
cout << func4(1, 2) << endl;
std::function<double(Plus, double, double)> func5 = &Plus::plusd;
cout << func5(Plus(), 1.1, 2.2) << endl;
return 0;
}
下面是对function包装器的使用。可以看到这里有一个useF模板函数,参数是两个模板参数。然后是对类中静态成员count进行++和取地址,最后返回第一个模板参数f的调用,传的参数是第二个模板参数x。
#include <functional>
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;
}
};
然后进行调用。第一个是将f函数作为对象传给useF函数,第二个是将Functor()类的匿名对象作为对象传给useF函数,第三个传递的是lambda表达式。
int main()
{
// 函数名
cout << useF(f, 11.11) << endl;
// 函数对象
cout << useF(Functor(), 11.11) << endl;
// lamber表达式
cout << useF([](double d)->double { return d / 4; }, 11.11) << endl;
return 0;
}
运行后看到useF函数的静态成员count的地址各不相同且count都只进行了一次++,意味着useF函数实例化出不同的函数体。传给useF的参数是f,Functor(),和lambda表达式,这些分别都是不同的类型,所以useF会被实例化出不同的对象。因此useF可能是函数名、函数指针、函数对象(仿函数对象)、也有可能是lamber表达式对象。
然而这三者的返回值类型相同(都是double),传递给useF函数的参数个数相同,形参类型相同,那么这里可以用包装器对这三个对象进行包装,然后通过function对象对这三者进行传参调用,这样就只会实例化出来一份useF函数。
原因是因为包装后,这三个可调用对象都是相同的function类型,因此最终只会实例化出一份useF函数,该函数的第一个模板参数的类型就是function类型的。
int main()
{
// 函数名
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;
return 0;
}
现在可以看到静态成员变量count的地址是相同的,且count++了三次说明只实例化了一份useF函数。
class Solution {
public:
int evalRPN(vector<string>& tokens) {
stack<int> st;
map<string,function<int(int,int)>>optomap={
{"+",[](int x,int y){return x+y;}},//包装的是lambda表达式
{"-",[](int x,int y){return x-y;}},
{"*",[](int x,int y){return x*y;}},
{"/",[](int x,int y){return x/y;}}
};
for(auto& e:tokens)
{
if(optomap.count(e)==0)//没找到字符--找到的是数字入-栈
{
st.push(stoi(e));
}else
{
int right=st.top();
st.pop();
int left=st.top();
st.pop();
st.push(optomap[e](left,right));
}
}
return st.top();
}
};
这里通过map建立了运算符和function类型的映射,而function类型实现的是运算符相应的操作,这里用的是lambda表达式,而函数指针,仿函数在这里也都能行。
function包装器的意义:
std::bind函数定义在头文件中,是一个函数模板,它就像一个函数包装器(适配器)。接受一个可调用对象(callable object),生成一个新的可调用对象(newcallable)来“适应”原对象的参数列表。
// 原型如下:
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);
可以这样理解:原先的可调用对象fun1通过bind函数适配器后,生成了一个新的可调用对象fun2
bind函数适配器的作用
//Plus、subFunc、Sub都是可调用对象
int Plus(int a, int b)
{
return a + b;
}
int SubFunc(int a, int b)
{
return a - b;
}
class Sub
{
public:
int sub(int a, int b)
{
return a - b * x;
}
private:
int x = 20;
};
int main()
{
//表示绑定函数plus 参数分别由调用 func1 的第一,二个参数指定
function<int(int, int)>fun1 = bind(Plus, placeholders::_1, placeholders::_2);
cout << fun1(2, 3) << endl;
return 0;
}
main函数里的 placeholders::1, placeholders是作用域,这种形如 _1 2等的参数是占位符,表示newcallable的参数,他们占据了传递给newcallable的参数位置,其 _n的n就对应newcallable的第n个位置的参数,比如 _1为newcallable的第一个参数, _2为newcallable的第二个参数,以此类推。
在这里callable是Plus,fun1是newcallable,Plus的参数由调用的fun1的第一,第二个参数决定。fun1把参数2,3传给Plus的a,b
int SubFunc(int a, int b)
{
return a - b;
}
int main()
{
// 调整参数的顺序
function<int(int, int)>fun2 = bind(SubFunc, placeholders::_2, placeholders::_1);
cout << fun2(7, 4) << endl;
return 0;
}
在这里callable是SubFunc,fun2是newcallable,fun2的第一个参数7传给 _1,第二个参数 4传给 _2。那么SubFunc的参数依次由调用的fun2的第二,第一个参数决定。fun2把参数4,7传给SubFunc的a,b。 占位符是几就接收第几个参数,占位符的位置决定传过来的参数的位置
class Sub
{
public:
int sub(int a, int b)
{
return a - b * x;
}
private:
int x = 20;
};
int main()
{
// 绑定固定参数
function<int(Sub, int, int)> fun3 = &Sub::sub;
cout<< "原先的包装器调用:" << fun3(Sub(), 10, 13) << endl;
function<int(int, int)>fun4 = bind(&Sub::sub, Sub(), placeholders::_1, placeholders::_2);
cout << "现在的bind绑定固定参数调用:" << fun4(10, 13) << endl;
return 0;
}
callable是Sub类中成员函数sub,所以在调用时需要传递Sub()匿名对象。但只要是通过包装器调用类中成员函数就需要传递Sub()匿名对象。现可以通过bind把Sub()匿名对象这个参数绑定在表达式中,后续参数不用传。
bind可以把参数绑定在表达式中,后续调用时不需要再次传参。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。