目录
7.新增加容器--静态数组array、forward_list、unordered系列
8.9 vector新增的emplace_back究竟有何神奇之处
1.C++11简介
2.列表初始化
2.1 c++98中{}的初始化问题
在c++98中,标准运行使用花括号{}对数组元素进行统一的列表初始值设定,比如
int array1[] = {1,2,3,4,5};
int array2[5] = {0};
对于一些自定义的类型,却无法使用这样的初始化,比如:
vector<int> v{1,2,3,4,5};
就无法通过编译,导致每次定义vector时,都需要先把vector定义出来,然后使用循环对其赋初始值,非常不方便。c++11扩大了用大括号括起来的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自定义类型,使用初始化列表时,可添加=,也可不添加
2.2 内置类型的列表初始化
int main()
{
// 内置类型变量
int x1 = {10};
int x2{10};
int x3 = 1+2;
int x4 = {1+2};
int x5{1+2};
// 数组
int arr1[5] {1,2,3,4,5};
int arr2[]{1,2,3,4,5};
// 动态数组,在C++98中不支持
int* arr3 = new int[5]{1,2,3,4,5};
return 0;}
注意:列表初始化可以在{}之前使用=,其效果与不使用=没有什么区别
2.3 自定义类型的列表初始化
1.标准库支持单个对象的列表初始化
class Point
{
public:
Point(int x = 0, int y = 0): _x(x), _y(y)
{}
private:
int _x;
int _y;
};
int main()
{
// 标准容器
vector<int> v{1,2,3,4,5};
map<int, int> m{{1,1}, {2,2,},{3,3},{4,4}};
Pointer p{ 1, 2 };
return 0;
}
2.多个对象的列表初始化
如果我们自己实现一个类,比如类似vector之类的,像传多少就传多少
这时候就需给该类(模板类)添加一个带有initializer_list类型参数的构造函数即可。注意:initializer_list是系统自定义的类模板,该类模板中主要有三个方法:begin()、end()迭代器以及获取区间中元素个数的方法size()。
#include <initializer_list>
template<class T>
class Vector {
public:
// ...
Vector(initializer_list<T> l): _capacity(l.size()), _size(0)
{
_array = new T[_capacity];
for(auto e : l)
_array[_size++] = e;
}
Vector<T>& operator=(initializer_list<T> l) {
delete[] _array;
size_t i = 0;
for (auto e : l)
_array[i++] = e;
return *this;
}
// ...
private:
T* _array;
size_t _capacity;
size_t _size;
}
因为你不知道想要传多少个,所以c++11提供了一个类,你只需要重载一下你的构造函数,如果使用了初始化列表就会去调用这个构造函数
3 变量类型推导
3.1 为什么需要类型推导
因为有时候在定义一个变量的时候,不清楚或者变量的类型太长太复杂,就可以使用auto去帮我们自动推导类型
// 使用迭代器遍历容器, 迭代器类型太繁琐
std::map<std::string, std::string>::iterator it = m.begin();
while(it != m.end())
{
cout<<it->first<<" "<<it->second<<endl;
++it;
}
3.2 decltype类型推导
auto使用的前提是必须要对auto声明的类型进行初始化,比如这里的m.begin就是一个迭代器类型,但有时候可能需要根据表达式完成之后的结果进行推导,因为编译期间,代码不会运行,此时auto也就无能为力了
decltype是编译期间进行推演,代码不会运行,仅仅进行推演类型,比如下面的a+b不会执行a+b
typeid是用来查看某个变量的类型的,不能用其返回结果来定义变量
dynamic_cast只能应用于含有虚函数的继承体系中
1.推演表达式类型作为变量的定义类型
int main()
{
int a = 10;
int b = 20;
// 用decltype推演a+b的实际类型,作为定义c的类型
decltype(a+b) c;
cout<<typeid(c).name()<<endl;
return 0;
}
2.推演函数返回值的类型
void* GetMemory(size_t size)
{
return malloc(size);
}
int main()
{
// 如果没有带参数,推导函数的类型
cout << typeid(decltype(GetMemory)).name() << endl;
// 如果带参数列表,推导的是函数返回值的类型,注意:此处只是推演,不会执行函数
cout << typeid(decltype(GetMemory(0))).name() <<endl;
return 0;
}
4.范围for
https://blog.csdn.net/Laydya/article/details/145682543
5.final与override
提供严格检查是否完成重写的
https://blog.csdn.net/Laydya/article/details/148176755
6.默认成员函数的控制
default:指示编译器生成默认构造函数等,关于类的默认函数
delete:指示编译器删除某个关于类的默认函数
class A
{
public:
A(int a): _a(a)
{}
// 显式缺省构造函数,由编译器生成
A() = default;
// 在类中声明,在类外定义时让编译器生成默认赋值运算符重载
A& operator=(const A& a);
private:
int _a;
};
A& A::operator=(const A& a) = default;
int main()
{
A a1(10);
A a2;
a2 = a1;
return 0;
}
class A
{
public:
A(int a): _a(a)
{}
// 禁止编译器生成默认的拷贝构造函数以及赋值运算符重载
A(const A&) = delete;
A& operator(const A&) = delete;
private:
int _a;
};
int main()
{
A a1(10);
// 编译失败,因为该类没有拷贝构造函数
// A a2(a1);
// 编译失败,因为该类没有赋值运算符重载
A a3(20);
a3 = a2;
return 0;
}
在c++98当中,把构造函数等直接设置成私有也行,c++11更简单,直接后面=delete
注意:避免删除函数和explicit一起使用,explicit 是为了避免意外的隐式转换导致逻辑错误
这个explicit只能修饰构造函数

7.新增加容器--静态数组array、forward_list、unordered系列
array:静态数组
forward_list:单向不带头链表
unordered系列在专栏当中已经重点讲解,可配合数据结构了解底层结构
8.右值引用(重点)
8.1概念
c++98提出了引用的概念,引用即别名,引用变量与其引用实体公用同一块内存空间,而引用的底层是通过指针来实现的,使用引用可以提高程序的可读性
https://blog.csdn.net/Laydya/article/details/145669638
为了提高程序的运行效率,c++11中引入了右值引用,右值引用也是别名,但其只能对右值进行引用
//左值
void Swap(int& left, int& right)
{
int temp = left;
left = right;
right = temp;
}
int main()
{
int a = 10;
int b = 20;
Swap(a, b);
}
//右值
int Add(int a, int b)
{
return a + b;
}
int main()
{
const int&& ra = 10;
// 引用函数返回值,返回值是一个临时变量,为右值
int&& rRet = Add(10, 20);
return 0;
}
为了与c++98中的引用进行区分,c++11将该种方式称之为右值引用
8.2.左值与右值
可以放在=左边的,或者能够取地址的称之为左值
只能放在=右边的,或者不能取地址的称之为右值
但这个说法不准确
int g_a = 10;
// 函数的返回值结果为引用
int& GetG_A()
{
return g_a;
}
int main()
{
int a = 10;
int b = 20;
// a和b都是左值,b既可以在=的左侧,也可在右侧,
// 说明:左值既可放在=的左侧,也可放在=的右侧
a = b;
b = a;
const int c = 30;
// 编译失败,c为const常量,只读不允许被修改
//c = a;
// 因为可以对c取地址,因此c严格来说不算是左值
cout << &c << endl;
// 编译失败:因为b+1的结果是一个临时变量,没有具体名称,也不能取地址,因此为右值
//b + 1 = 20;
GetG_A() = 100;
return 0;
}
因此关于左值与右值的区分不是很好区分,一般认为:
1. 普通类型的变量,因为有名字,可以取地址,都认为是左值。
2. const修饰的常量,不可修改,只读类型的,理论应该按照右值对待,但因为其可以取地址(如果只是const类型常量的定义,编译器不给其开辟空间,如果对该常量取地址时,编译器才为其开辟空间),C++11认为其是左值。
3. 右值通常是常量(注意不是const等修饰的,一般是1,2这种数字)、表达式、函数返回值等临时对象。
4. 左值通常是变量(注意const等修饰的是左值)左值一般都可以修改,取地址等操作。
int& a=10;//这样不行的,10是常量,是右值,不能左值引用
8.3 纯右值和将亡值
右值分为:纯右值和将亡值
纯右值:(传统的右值)基本内置类型的常量或临时变量、函数返回值等(函数返回值的本质就是一个临时变量)
将亡值:自定义类型的临时对象(c++11主要是为了解决这个)(有身份有地址的对象即将要被销毁了)
8.4 引用与右值引用比较
int main()
{
// 普通类型引用只能引用左值,不能引用右值
int a = 10;
int& ra1 = a; // ra为a的别名
//int& ra2 = 10; // 编译失败,因为10是右值
const int& ra3 = 10;
const int& ra4 = a;
return 0;
}
两个概念:一个是普通引用 int& 一个是const引用 const int&
普通引用只能引用左值,不能引用右值
const引用能够引用左值,也能够引用右值
int main()
{
// 10纯右值,本来只是一个符号,没有具体的空间,
// 右值引用变量r1在定义过程中,编译器产生了一个临时变量,r1实际引用的是临时变量
int&& r1 = 10;
r1 = 100;
int a = 10;
int&& r2 = a; // 编译失败:右值引用不能引用左值
return 0;
}
右值引用只能够引用右值,在引用右值的过程当中,给10这个变量开辟临时变量有实际空间,r1是这个临时变量的别名,它的生命周期会被延长至和右值引用r1相同(原本临时对象用完就销毁,绑定到右值引用后会和引用一起存活到作用域结束)。注意:这个临时变量是编译器 “隐式创建” 的匿名对象,你无法直接通过名字访问它,但右值引用r1是它的 “别名”—— 所以r1 = 100;实际是修改这个临时变量的值。
注意:有const右值引用,但意义不大,它的功能完全可以使用const左值引用代替
总结:左值和右值的概念,引用和右值引用的用法
问题:既然c++98中的const类型引用即可以引用左值,也可以引用右值,为什么c++11还要复杂的提出右值引用呢?
8.5 值的形式返回对象的缺陷
如果一个类当中涉及到资源管理(比如你new出来的堆空间),用户必须显示提供拷贝构造、赋值运算符重载已经析构函数,否则编译器将会自动生成一个默认的,此时就会出现浅拷贝问题
class String
{
public:
String(char* str = "")
{
if (nullptr == str)
str = "";
_str = new char[strlen(str) + 1];
strcpy(_str, str);
}
String(const String& s)
: _str(new char[strlen(s._str) + 1])
{
strcpy(_str, s._str);
}
String& operator=(const String& s)
{
if (this != &s)
{
char* pTemp = new char[strlen(s._str) +1];
strcpy(pTemp, s._str);
delete[] _str;
_str = pTemp;
}
return *this;
}
String operator+(const String& s)
{
char* pTemp = new char[strlen(_str) + strlen(s._str) + 1];
strcpy(pTemp, _str);
strcpy(pTemp + strlen(_str), s._str);
String strRet(pTemp);
return strRet;
}
~String()
{ if (_str) delete[] _str;}
private:
char* _str;
};
int main()
{
String s1("hello");
String s2("world");
String s3(s1+s2);
return 0;
}
上述代码完全可以运行,但是有个地方可以优化,这里的operator+

这里返回是以值的方式返回,比如会进行创建临时对象,临时对象的创建就需要调用拷贝构造函数,最后销毁strRet,临时对象赋值给s3后临时对象才销毁,strRet、临时对象、s3每个对象创建后,都有自己独立的空间,而空间中存放内容也都相同,相当于创建了三个内存完全相同的对象,对于空间是一种浪费,程序的效率也会降低,而且临时对象确实作用不是很大,那么就需要想出来一种策略去优化这种情况
8.6 移动语义
c++11提出了移动语义的概念,即:将一个对象中资源移动到另一个对象中的方式,可以有效缓解该问题

在c++中如果需要实现移动语义,必须使用右值引用,上述String类增加移动构造
String(String&& s)
: _str(s._str)
{
s._str =nullptr;
}
C++11 规定:函数返回的局部对象(值返回)会被隐式转换为将亡值—— 本质是告诉编译器 “这个对象马上要销毁了,可以安全地窃取它的资源”。
因为strRet对象的生命周期在创建好临时对象后就结束了,即将亡值,C++11认为其为右值,在用strRet构造临时对象时,就会采用移动构造,即将strRet中资源转移到临时对象中。而临时对象也是右值,因此在用临时对象构造s3时,也采用移动构造,将临时对象中资源转移到s3中,整个过程,只需要创建一块堆内存即可,既省了空间,又大大提高程序运行的效率。
注意:
1. 移动构造函数的参数千万不能设置成const类型的右值引用,因为资源无法转移而导致移动语义失效。
2. 在C++11中,仅当类中没有显式定义拷贝 / 析构时,。编译器会为类默认生成一个移动构造,该移动构造为浅拷贝,因此当类中涉及到资源管理时,用户必须显式定义自己的移动构造。
8.7 右值引用左值
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> v1 = {1,2,3,4}; // v1 是左值
// 1. 移动构造:用 std::move(v1) 将 v1 转为右值
vector<int> v2 = std::move(v1);
// 此时 v1 被“掏空”(资源被窃取),但仍是合法的空对象
// 2. 移动赋值:同样触发移动语义
vector<int> v3;
v3 = std::move(v2); // v2 被掏空,v3 获得资源
return 0;
}
std::move(v1)表达式:只是一个 “临时的将亡值”—— 这个表达式的结果是右值引用类型(T&&),但它是 “匿名的”,仅在使用它的那一刻(比如赋值、传参)被视为将亡值,用完后就失效了。
一般来说不这样使用move,一般都是为了解决刚刚的strRet这种返回值一直进行拷贝构造导致效率低下的场景
所以c++11中对于所有的容器基本都实现了两个版本

一个是左值引用,一个是右值引用(一般不使用const,因为右值一般需要改变,比如之前的你需要把s._str=nullptr)
右值引用的作用是针对一些右值,push_back会调用他们的移动构造去转移资源,所以可以提高效率
总结:右值引用做参数和做返回值,减少拷贝的本质是利用了移动构造和移动赋值,左值引用和右值引用本质的作用都是减少拷贝,右值引用的本质可以认为是弥补左值引用不足的地方
左值引用:
做参数:const T&x,这里即可以接受左值也可以接受右值,因为是引用所以是别名可以减少拷贝
做返回值:T&f(),但要注意这个返回值出作用域是否还存在
右值引用
做参数:T&&x,解决的是左值中不能使用移动构造,因为你push_back()内部在赋值的时候使用构造函数,如果是左值,就会调用拷贝构造,右值可以使用移动构造
做返回值:左值能够做返回值是因为你外部有变量,这个变量的周期不是在函数内部所以可以使用左值引用返回,但右值解决的是外部没有,比如T ret=f(),需要使用一个变量来接受返回值,此时可以减少拷贝(注意左值是因为你出来函数作用域这个变量的生命周期还在)
8.8 完美转发
完美转发是指在函数模板中,完全依照模板的参数的类型,将参数传递给函数模板中调用给的另一个函数
void Fun(int& x){cout<<"lvalue ref"<<endl;}
void Fun(int &&x){cout<<"rvalue ref"<<endl;}
void Fun(const int &x){cout << "const lvalue ref" << endl;}
void Fun(const int &&x){cout << "const rvalue ref" << endl;}
template<typename T>
void PerfectForward(T &&t){Fun(std::forward<T>(t));}
int main()
{
PerfectForward(10); // rvalue ref
int a;
PerfectForward(a); // lvalue ref
PerfectForward(std::move(a)); // rvalue ref
const int b = 8;
PerfectForward(b); // const lvalue ref
PerfectForward(std::move(b)); // const rvalue ref
return 0;
}
int main()
{
string s
注意,这里的参数是T&&,当类型是一个模板的时候,加上&&那就是万能引用,这样是可以接受左值也可以接受右值的,会根据传入的是左值还是右值来进行初始化
比如传10,10是右值,那就是int&&,所以T就会实例化成int,那最后是变为int&&
而std::forward的作用是 “完美转发”—— 将万能引用接收到的实参,以其原始的左值 / 右值属性传递给后续函数(这里是Fun)。
因为右值引用会在第二次调用之后的参数传递过程中属性丢失,需要使用完美转发来保证其特性
注意:对于所有的引用函数,一旦传参,无论是左值还是右值,在函数内部都会变成左值,因为你传参过来,这个变量有了名字并且可寻址,所以函数内部会变成左值,如果不使用完美转发,就会一直是左值
总结:c++11中右值引用主要有以下作用
1.实现移动语义(移动构造与移动赋值)
2.给中间临时变量取别名
3.实现完美转发
8.9 vector新增的emplace_back究竟有何神奇之处
有人说emplace_back比push_back更高效,这种说法是完全不准确的,具体看应用场景


针对c++11,push_back实现了右值引用
针对场景一:已经构造好的对象,比如string s1,你传入push_back和emplace_back是一样的效率
针对场景二:直接传入参数,比如push_back("右值"),emplace_back("右值"),这个来说是emplace效率更高,因为你传参的时候需要调用一次string的构造函数构造出临时对象,然后再传给右值还需要调用移动构造,emplace_back不需要传参的时候进行构造函数构造出临时对象,就单纯的传参,然后再函数内部进行一次构造函数即可,全程没有临时对象,所以高效在于减少了临时对象
比如:


这里不看最后一次拷贝构造,最后一次是因为扩容,需要把原来的空间拷贝到新空间
这里明显多了一次移动构造
怎么实现的???
template <typename... Args> // 可变参数模板:接收任意参数
void emplace_back(Args&&... args) { // 万能引用:转发参数
if (_size >= _capacity) reserve(...); // 扩容检查
// 关键:在 vector 的内存上,用传入的 args 直接构造 Person
new (reinterpret_cast<void*>(&_data[_size])) Person(std::forward<Args>(args)...);
++_size;
}
可以看出,emplace_back使用了可变参数模板:接受任意参数,函数内部使用了完美转发直接调用构造函数构造person然后存在对应的内存上
emplace_back接受的是参数,也就是person构造函数的参数,也就是string 和int,不是person类型的对象,函数内部的new是定位new,直接通过完美转发和定位new调用person的构造函数,把args转发给Person的构造函数,在vector的内存地址(&_data[_size])上构造
emplace版本的特点就是有模板的可变参数的特点
9. lambda表达式
9.1 c++98中的一个例子
在c++98中,如果想要对一个数据集合中的元素进行排序,可以使用std::sort方法
#include<algorithm>
#include <functional>
int main()
{
int array[] = {4,1,8,5,3,7,0,9,2,6};
// 默认按照小于比较,排出来结果是升序
std::sort(array, array+sizeof(array)/sizeof(array[0]));
// 如果需要降序,需要改变元素的比较规则
std::sort(array, array + sizeof(array) / sizeof(array[0]), greater<int>());
return 0;
}
如果待排序元素为自定义类型,需要用户定义排序时的比较规则
对于一个类(商品类)-->要排序---->按什么排
如果你在类内重载operator > = <等,你这次按价格,下次按数量又要去改代码,所以你写了一个仿函数,针对不同的比较方式写不同的仿函数(但需要很多个,而且依赖命名,写函数也行)
struct Goods
{
string _name;
double _price;
};
struct Compare
{
bool operator()(const Goods& gl, const Goods& gr)
{
return gl._price <= gr._price;
}
};
int main()
{
Goods gds[] = { { "苹果", 2.1 }, { "相交", 3 }, { "橙子", 2.2 }, {"菠萝", 1.5} };
sort(gds, gds+sizeof(gds) / sizeof(gds[0]), Compare());
return 0;
}
9.2 lambda表达式
int main()
{
Goods gds[] = { { "苹果", 2.1 }, { "相交", 3 }, { "橙子", 2.2 }, {"菠萝", 1.5} };
sort(gds, gds + sizeof(gds) / sizeof(gds[0]), [](const Goods& l, const Goods& r)
->bool
{
return l._price < r._price;
});
return 0;
}
上述代码就是使用c++11中的lambda表达式来解决的,可以看出lambda表达式实际是一个匿名函数
9.3 lambda表达式语法
ambda表达式书写格式:[capture-list] (parameters) mutable -> return-type { statement }
1. lambda表达式各部分说明
[capture-list] : 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。
(parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起
省略
mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修
饰符时,参数列表不可省略(即使参数为空)。
->returntype:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分
可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
{statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量
注意: 在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空。
因此C++11中最简单的lambda函数为:[]{}; 该lambda函数不能做任何事情。
int main()
{
// 最简单的lambda表达式, 该lambda表达式没有任何意义
[]{};
// 省略参数列表和返回值类型,返回值类型由编译器推导为int
int a = 3, b = 4;
[=]{return a + 3; };
// 省略了返回值类型,无返回值类型
auto fun1 = [&](int c){b = a + c; };
fun1(10)
cout<<a<<" "<<b<<endl;
// 各部分都很完善的lambda函数
auto fun2 = [=, &b](int c)->int{return b += a+ c; };
cout<<fun2(10)<<endl;
// 复制捕捉x
int x = 10;
auto add_x = [x](int a) mutable { x *= 2; return a + x; };
cout << add_x(10) << endl;
return 0;
}
通过上述例子可以看出,lambda表达式实际上可以理解为无名函数,该函数无法直接调用,如果想要直接调用,可借助auto将其赋值给一个变量。
2.捕获列表说明
void (*PF)();
int main()
{
auto f1 = []{cout << "hello world" << endl; };
auto f2 = []{cout << "hello world" << endl; };
// 此处先不解释原因,等lambda表达式底层实现原理看完后,大家就清楚了
//f1 = f2; // 编译失败--->提示找不到operator=()
// 允许使用一个lambda表达式拷贝构造一个新的副本
auto f3(f2);
f3();
// 可以将lambda表达式赋值给相同类型的函数指针
PF=f2;
PF();
return 0;
}
9.4 函数对象与lambda表达式
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);
// lamber
auto r2 = [=](double money, int year)->double{return money*rate*year; };
r2(10000,2);
return 0;
}
从上述使用方法看,函数对象与lambda表达式完全一样
函数对象将rate作为其成员变量,在定义对象时给出初始值即可
lambda表达式通过捕获列表可以直接将该对象捕获到

1435

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



