文章目录
- 一、列表初始化
- 1.{}
- 2.initializer_list
- 二、声明
- 1.auto
- 2.deltype
- 三、右值与左值
- 1.基本概念
- 2.应用场景
- 1.左值引用
- 2.右值引用
- 3.完美转发
- 4.万能引用
- 四、新增默认成员函数
- 五、lambda表达式
- 1.基本语法
- 1.1捕捉列表
- 1.2参数列表
- 1.3返回类型
- 1.4函数体
- 2.底层原理
- 总结
一、列表初始化
1.{}
- 内置类型
int a1{ 1 };
int a2 = { 2 };
- 自定义类型
class Point
{
public:
Point(int a,int b)
:_a(a)
,_b(b)
{}
private:
int _a;
int _b;
};
int main()
{
//数组
int arr1[]{ 1,2,3 };//列表初始化
int arr2[] = { 1,2 };//数组初始化。
//自定义类型初始化
Point p1{ 1,2 };//这里是显示调用构造函数
Point p2 = { 1,2 };
//本质上是C++11支持多参数的隐式类型转换,编译器优化为直接构造。
//验证:在构造函数前面+explicit 语法会报错。
const Point& p4 = { 1,2 };
//多参数的隐式类型转换,生成一个匿名对象其具有常性,不可被修改。
int* ptr1 = new int[] {1, 2, 3};
Point* ptr2 = new Point[]{{1,2},{1,3}};
//外面的一层为数组,里面的为列表初始化。
return 0;
}
总结:
- 一切即可用{}进行初始化。
- {}支持多参数的隐式类型转换,也用于单参数的隐式类型转换。
- 对于自定义类型初始化不加 = 直接跟 {}——显示调用构造函数。
- 对于加 = 再跟{}初始化——隐式类型转换+编译器的优化 - > 调用构造函数(构造函数前加explicit验证)。
2.initializer_list
- initializer_list是编译器特殊支持的内置容器,不可单独实现,是一个常量数组。
说明:这是库里initializer_list的所有接口。
int main()
{
const char* str = "abcd";//常量区字符串。返回的是首字母的地址。
int arr[] = {1,2,3,4};//直接进行赋值。
initializer_list<int> lt;//此处是默认构造函数进行初始化
initializer_list<int> lt = { 1,2,3,4 };
//常量区数组,此处存于栈区。
//此处是编译器直接进行识别进行初始化,可以理解是直接内存赋值,
//与arr数组的赋值雷同。
return 0;
}
-
常见用途:作为构造函数的参数进行初始化/赋值
-
注意
vector<int> v = { 1,2,3,4 };
//隐式类型转换 + 编译器优化,调用initializer_list的构造函数进行初始化。
Point p = { 1,2 };
//隐式类型转换+编译器优化,调用相应的构造函数进行初始化。
- 接口实现
vector(initializer_list<T>& lt)
{
reserve(lt.size());
for(auto e : lt)
{
push_back(e);
}
//或者直接调用相应的迭代器区间进行初始化,不过得用现代写法再进行交换。
//注意:构造函数是不能够显示进行调用的!
//vector tmp(lt.begin(), lt.end());
//swap(tmp);
}
其它的接口类似。
二、声明
1.auto
详见:初始C++语法
举例:
int main()
{
auto p1 = malloc;
auto p2 = Point(1, 2);
auto p3 = { 1,2,3 };
auto p4 = "123";
auto p5 = 1;
auto str = string();
cout << typeid(p1).name() << endl;
cout << typeid(p2).name() << endl;
cout << typeid(p3).name() << endl;
cout << typeid(p4).name() << endl;
cout << typeid(p5).name() << endl;
cout << typeid(str).name() << endl;
cout << endl;
//typeid().name的返回值的类型为char const* ,即指向的内容不可被修改。
cout << typeid(typeid(p1).name()).name() << endl;
return 0;
}
补充:auto是在编译期间进行推导得出类型的,且右边必须有可推导的值,因此不可对类型进行声明。
2.deltype
- 可得出类型,且可以当做类型名进行使用。
template<class T>
class Func
{
T _a;
};
int main()
{
auto p1 = malloc;
decltype(p1) p2;
Func<decltype(p2)> p3;
return 0;
}
- 总结
- 推导出的类型可以进行声明。
- 推导出的类型可以当做函数参数。
用途:一些迭代器类型的声明,以及复杂指针的声明。
三、右值与左值
1.基本概念
- 右值:不能取地址,一般把内置类型的右值叫做纯右值,自定义类型的右值叫将亡值。
- 左值:能取地址。
- 左值引用:对左值起别名。
- 右值引用:对右值起别名。
- 引用:不管左值引用还是右值引用,都是对值起别名。
- 常见右值
int add(int x, int y)
{
return x + y;
}
int main()
{
int a = 0;
int b = 0;
//常见的右值
//表达式
a + b;
//函数的返回值
add(a, b);
//常量
10;
//验证:不能取地址。
//cout << &10 << &add(a, b) << &(a + b) << endl;
return 0;
}
特殊的左值:
int main()
{
//特殊的左值。
"abc";//返回的是首字符a的地址。
const char* p = "abc";
p[1];//这也是左值。
return 0;
}
- 左值引用
int main()
{
int a = 0;
int b = 0;
//左值引用直接对左值取别名
int& ref1 = a;
int& ref2 = b;
//const 左值引用可以引用右值
const int& ref3 = a + b;
const int& ref4 = 10;
return 0;
}
拓展:
const int& ref3 = 10;
int* ptr = (int*)&ref3;
*ptr = 100;
cout << &ref3 << endl;
cout << ref3 << endl;
//const左值引用引用右值,开空间存储右值,对const左值引用可以取地址,因此const左值引用为左值。
- 右值引用
int main()
{
int a = 0, b = 0;
int&& ref5 = 10;
int&& ref6 = a + b;
return 0;
}
- 拓展
int main()
{
int a = 0;
int&& ref = move(a);
//cout << &move(a) << endl;
//右值引用可以引用move之后的左值,move之后的结果为右值。
int&& ref1 = 10;
cout << &ref1 << endl;
//右值引用能取地址,说明右值引用为左值,对10开空间进行拷贝。
ref1++;
//右值引用可以进行修改,更说明其为左值。
int&& ref2 = forward<T>(ref1);//叫做完美转发,即保持右值属性,此处了解即可。
int&& ref3 = move(ref2);//将左值ref2转换为右值。
//说明:右值引用也可作为右值,需要进行处理。
return 0;
}
2.应用场景
1.左值引用
- 作为函数参数
- 作为返回值,前提是引用的值在出作用域不被销毁。
- 起到的作用都是减少拷贝。
缺陷:当返回一个局部的自定义的很大的对象时,只能用传值返回,不能用传引用返回。
说明:写一份string方便查看细节:
namespace MY_STL
{
class string
{
public:
typedef char* iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
string(const char* str = "")
:_size(strlen(str))
, _capacity(_size)
{
_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;
_size = s._size;
_capacity = s._capacity;
_str = new char[_capacity + 1];
strcpy(_str, s._str);
}
// 赋值重载
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
};
}
- 举例:
MY_STL::string func()
{
MY_STL::string str("xxxxxxxxxxxxxxxxxx");
return str;
}
int main()
{
//1.连续的拷贝构造进行优化。
MY_STL::string str = func();
//2.不连续的拷贝构造,进行逐步拷贝。
MY_STL::string str1;
str1 = func();
return 0;
}
1.图解
说明:编译器优化的结果是——在return之前就调用拷贝构造进行构造,因为return后就调用析构函数对空间进行销毁了。
2.图解
运行结果图解:
2.右值引用
前面我们举了左值引用的缺陷,那右值引用是来弥补这一个缺陷的,如何弥补呢?接着继续分析。
第一个问题:
void func(const int& a)
{
cout << "func(const int& a)" << endl;
}
void func(int && a)
{
cout << "func(int && a)" << endl;
}
int main()
{
int a = 0;
int b = 0;
func(a);
func(a + b);
return 0;
}
运行结果如何?构成重载么?如果构成,那是否存在歧义?
- 运行结果
结果很显然构成重载,且不存在歧义,函数调用会走最匹配的。
接着用上面的例子:
MY_STL::string func()
{
MY_STL::string str("xxxxxxxxxxxxxxxxxx");
return str;
}
int main()
{
MY_STL::string str1;
str1 = func();
return 0;
}
根据前面的思路分析,返回str时发生了一次拷贝构造,生成了匿名对象(右值),且其声明周期只在这一行,那么我们是否可以利用这一特性写出个右值引用版本的赋值呢?那关键就在于这是右值且声明周期只在当前一行
,因此我们可以直接换资源,我们顺走匿名对象的资源,之后让匿名对象带走不用的资源(析构函数)
。
根据这样我们写出来的赋值重载就是这样的:
void swap(string& s)
{
::swap(_str, s._str);
::swap(_size, s._size);
::swap(_capacity, s._capacity);
}
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移动赋值" << endl;
swap(s);
return *this;
}
- 补充:右值引用可以被修改,且默认识别为左值。
我们再来运行一下:
对比之前,这里就优化了一次深拷贝,在一定程度上提升了效率。
继续使用上面的例子再进行分析:
MY_STL::string func()
{
MY_STL::string str("xxxxxxxxxxxxxxxxxx");
return str;
}
int main()
{
//1.连续的拷贝构造进行优化。
MY_STL::string str = func();
return 0;
}
按理来说这里本该为拷贝构造生成匿名对象,再用匿名对象拷贝构造对象,但编译器看不惯连续的拷贝构造,因此直接优化为了一次拷贝构造,这里如何用右值进行优化呢?编译器直接将返回的str特殊处理成右值,然后用识别之后的右值,进行拷贝构造
,那如何写拷贝构造呢?其实跟赋值一样。
string(string&& s)
:_str(nullptr)
{
cout << "string(string&& s)——移动拷贝" << endl;
swap(s);
}
运行结果:
看结果,确实是编译器对返回值进行了优化,那为啥要进行这样的优化呢?第一点博主认为这样代码具有可移植性,第二点是不用再对之前的代码进行修改即在返回值处修改成move(str)。
此时我们再看之前的那个例子:
MY_STL::string func()
{
MY_STL::string str("xxxxxxxxxxxxxxxxxx");
return str;
}
int main()
{
//1.连续的拷贝构造进行优化。
MY_STL::string str = func();
return 0;
}
再看运行结果:
这里编译器也是做了优化的,对返回值直接识别成右值,直接将两次拷贝构造,优化为两次移动构造,这样处理对一些很大的自定义的值返回,带来了效率的极大的提升!并且写起来也变的更加轻松。
除此之外为了更好更深刻的理解问题,博主举一个例子:
int main()
{
const string& str = string("xxxxx");
string("xxxxx").push_back('a');
return 0;
}
这会引出来几个问题:
- 匿名对象具有常属性不可以被修改,但是这里却可以调用接口被修改。
- 匿名对象左值引用只能用const左值引用,之后就不可调用接口修改,因为是this指针被const修饰,这一点很容易理解。
- 匿名对象右值引用可以被修改,且修改的是匿名对象本身,而对内置类型则是修改其拷贝。
可以看出匿名对象确实是右值或者说是常量(const左值引用),但也确实可以被修改,其原因就在于这是C++委员会开的绿灯,其目的就在于通过右值引用修改其内部成员而完成资源的转换,进而实现移动赋值(语义)和移动拷贝(语义),而对内置类型的右值进行引用则是开空间存储(消耗不大),因此我们可以把右值引用的最终目的就是为了减少资源的拷贝进行提升效率。
总结一下关键:
- 对于内置类型的右值可以认为是常量绝对不可以被修改,而自定义类型的右值也认为是常量,但为了达到目的,开了绿灯因此可以被修改。
- 右值引用对于内置类型开空间拷贝,对自定义类型直接取别名,且右值引用之后的别名可以进行修改,且被识别为左值(方便修改),进而方便转资源,提升效率。
那右值引用是否具有右值属性呢?答案是肯定的。在有些情况下,是需要保持右值属性的。
比如:当我们在vector中push_back一个string。
int main()
{
MY_STL::vector < MY_STL::string> v;
v.push_back("xxxxx");//尾插一个匿名对象的string。
return 0;
}
因此push_back可优化成这样:
但是又涉及一个问题,虽然这里是右值引用,但这里会被识别为左值,进而调用赋值重载跟上面的一样,这要怎么办呢?也就是如何保持其右值的属性呢?
3.完美转发
完美转发就是来解决上面的问题,右值引用保持其右值的属性,左值引用保持其左值的属性。
那么最终的push_back就是:
void push_back(value_type&& val)
{
if (_finish == _end_of_storage)
{
size_t new_capacity = size() == 0 ? 4 : \
capacity() * 2;
//扩容
reserve(new_capacity);
}
*(_finish++) = forward<value_type>(val);//这样就叫做完美转发。
}
- 这样就能在插入数据时进一步优化效率。
4.万能引用
最后再简单的提及一个比较省力的小语法。
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(forward<T>(t));
}
int main()
{
PerfectForward(10); // 右值
int a;
PerfectForward(a); // 左值
PerfectForward(move(a)); // 右值
const int b = 8;
PerfectForward(b); // const 左值
PerfectForward(move(b)); // const 右值
return 0;
}
运行结果:
图解:
四、新增默认成员函数
C++11新增了两个默认成员函数,不过需要具备以下条件才会自动生成。
- 你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。
- 功能:默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
例:
class Person
{
public:
Person(const char* name = "", int age = 0)
:_name(name)
, _age(age)
{}
private:
MY_STL::string _name;
int _age;
};
int main()
{
Person s2;
Person s3("xxxxx",10);
s3 = move(s2);
return 0;
}
分析为什么要这样设计?关键在于默认生成的移动构造函数对内置类型是只拷贝字节的。也就是说我们在类内部定义一个指针,用个指针指向堆空间的资源,默认生成只完成浅拷贝,而不完成资源的交换!并且因为默认生成的析构函数不会对内置类型处理,因此还会造成内存泄漏 , 这就要求我们自己实现析构函数,但是写了析构函数就不会生成默认的移动构造了,这就又反过来要求我们自己写移动构造交换资源。
这样编译器选择生成的目的在于只要你写了实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个就会默认认为有需要深拷贝的内置成员,因此不会生成只会对内置类型完成按字节拷贝的默认移动构造。
- 对于移动赋值重载同理。
五、lambda表达式
lambda其实就是一个对象,有类似函数,可以在语句里面定义即使用。
在之前我们还学过函数指针和仿函数与之相同。
1.基本语法
基本语法:
[capture-list] (parameters) mutable -> return-type { statement}
//捕捉列表 参数 控制属性 返回类型 实现语句
先举个例子看看如何使用:
struct Goods
{
string _name; // 名字
double _price; // 价格
int _evaluate; // 评价
Goods(const char* str, double price, int evaluate)
:_name(str)
, _price(price)
, _evaluate(evaluate)
{}
};
int main()
{
vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2, 3 }, { "菠萝", 1.5, 4 } };
auto CompareNameLess = [](const Goods& x1, const Goods& x2)->bool
{ return x1._name < x2._name; };
auto CompareNameGreater = [](const Goods& x1, const Goods& x2)->bool
{ return x1._name > x2._name; };
sort(v.begin(), v.end(), CompareNameLess);
sort(v.begin(), v.end(), CompareNameGreater);
//也可以直接这样写:
sort(v.begin(), v.end(), [](const Goods& x1, const Goods& x2)->bool
{ return x1._name < x2._name; }
);
sort(v.begin(), v.end(), [](const Goods& x1, const Goods& x2)->bool
{ return x1._name > x2._name; }
);
//箭头以及后面的类型可省略,编译器会自动进行推导。
sort(v.begin(), v.end(), [](const Goods& x1, const Goods& x2)
{ return x1._name > x2._name; }
);
return 0;
}
这样使用其实有一定的好处,就是避免了有人使用仿函数或者函数指针时,定义的类或者函数名字不规范而导致代码的可读性极差,还要回去找定义才能进行理解, 花费的时间成本很大。
再来谈谈其具体的几个部分:
1.1捕捉列表
捕捉列表其实是用所在局部域的变量,因为其实lambda就是一个类的匿名对象,其定义的内容在底层是一个函数,又因为函数与函数之间的域是独立的,不可互相使用变量只能通过传参的形式或者使用全局变量的方式间接使用,这里的捕捉列表就是为了间接使用不同域之间的变量而诞生的。
- 捕捉值
- 捕捉指定值。
int main()
{
int x = 1, y = 2;
auto tmp = [x, y]()
{
//x = 0;并且此处是const的左值,不可被修改。
return x + y;
};
return 0;
}
这里的x与y进行值捕捉是加const的,因此不可被修改,如果非要修改呢?
很简单:在()后加 mutable。
这样就可以被修改了:
int main()
{
int x = 1, y = 2;
auto tmp = [x, y]() mutable
{
x = 0;
return x + y;
};
return 0;
}
说明: mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。
class Person {
public:
void modify()const
{
//const函数
//比如int float等类型,其值不可修改。
//指针的值不可被修改,内容是可以被修改的。
//引用底层是指针,其指向的内容是可以被修改的。
//static静态对象不受this指针的约束也可以被修改。
//age++;
//name = "aaaa";
nums++;
*name;
ref = 0;
_id = 0;
}
private:
int age;
int& ref = age;
char* name;
mutable int nums;
static int _id;
};
int Person::_id = 0;
- 捕捉所有值(如果有this则this也算进去)
int main()
{
int x = 1, y = 2;
auto tmp = [=]() mutable
{
x = 0;
return x + y;
};
return 0;
}
- 捕捉引用
- 捕捉指定引用
int main()
{
int x = 1,y = 2;
auto tmp = [&x,&y]()
{
return x + y;
};
return 0;
}
- 捕获所有引用(如果有this包含this)
int main()
{
int x = 1,y = 2;
auto tmp = [&]()
{
return x + y;
};
return 0;
}
注意
- 捕捉列表不允许变量重复传递,否则就会导致编译错误,比如x以值的形式捕捉过一回就不能用引用再进行捕获。
- 语法上捕捉列表可由多个捕捉项组成,并以逗号分割。比如:[=, &a, &b]:以引用传递的方式捕捉变量a和b。
- 在全局定义lambda表达式,其捕捉列表必须为空。
- lambda只能捕捉其所在局部作用域的变量。
- lambda之间不能能进行赋值,因为类型不同。
1.2参数列表
- 与普通的函数列表一样。
1.3返回类型
- 通常可通过实现体进行推导,所以一般情况下可以省略。
1.4函数体
- 写函数的实现功能。
2.底层原理
- 类型
int main()
{
auto tmp = []() {};
cout << typeid(tmp).name() << endl;
return 0;
}
运行结果:
可以看出其是一个类,至于这么长,博主猜测是采用了哈希函数的那套方法进行映射出了一段关键码,由此产生不同的类,这也间接说明了不同lambda表达式的类型各不相同。
- 反汇编
struct tmp1
{
bool operator()()
{
return 0;
}
};
int main()
{
auto tmp = []()->bool { return 0; };
tmp();
tmp1()();
return 0;
}
反汇编截图:
- 因此:底层是仿函数。
总结
今天的分享到此就结束了,下篇文章再见咯,如果有所帮助,不妨点个赞鼓励一下吧!