📃博客主页: 小镇敲码人
💚代码仓库,欢迎访问
🚀 欢迎关注:👍点赞 👂🏽留言 😍收藏
🌏 任尔江湖满血骨,我自踏雪寻梅香。 万千浮云遮碧月,独傲天下百坚强。 男儿应有龙腾志,盖世一意转洪荒。 莫使此生无痕度,终归人间一捧黄。🍎🍎🍎
❤️ 什么?你问我答案,少年你看,下一个十年又来了 💞 💞 💞
【C++进阶】之C++11的简单介绍
- C++11简介
- 列表初始化
- {}初始化
- std::initializer_list容器
- 文档介绍
- initializer_list容器的底层
- std::initializer_list容器的使用
- 与类型相关的两个新特性
- 变量名推导
- decltype关键字
- typeid介绍
- type_info类
- nullptr
- 重载函数
- STL中的一些变化
- 增加了一些新容器
- 老容器增加了一些新的方法
- pair容器的模拟实现
- 右值引用和移动构造
- 左值引用和右值引用
- 左值引用和右值引用的比较
- 引用的底层原理
- 右值引用存在的价值
- 移动构造
- 左值引用为什么无法完全解决拷贝的问题
- 右值引用引用右值的几种情况
- 引用的不是指针
- 引用的是指针变量
- 右值引用引用左值
- 完美转发
- 完美转发的应用场景
- 万能引用
C++11简介
列表初始化
{}初始化
C++98中,标准允许使用花括号对数组和结构体对象进行初始化。
C++11之后,扩大了{}
的使用范围,可以对所有的内置类型和自定义类型使用{}
,还可以省略=
。
new
对象的时候初始化也可以使用{}
。
看下面这段代码,思考使用{}
直接初始化自定义类的原理:
#include<iostream>
#include<vector>
#include<map>
#include<string>
#include<stack>
using namespace std;
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;
}
Date& operator=(const Date& x)
{
cout << "operator=(const Date & x)" << endl;
return *this;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date y = { 2024,9,17 };
}
运行结果:
只调用了构造函数,这是优化后的结果。
那直接构造自定义类型中,{1,2,3,4,5,6,7是什么类型,调用的是什么构造函数呢?
std::initializer_list容器
文档介绍
点击跳转
从上面的文档我们能得到关于initializer_list
以下信息:
- 这个类型的对象是被编译器自动的创建的,是一个用逗号分割被花括号包裹的元素列表。
- 使用这个对象的时候,应该包含头文件。
- 初始化列表的对象会被自动的构建,底层是一个
const T
类型的数组,但是这个对象只有使用权,并不拥有底层的数组,也就是它们的空间不归这个对象管理和释放。 - 只使用这个初始化列表对象为参数的构造函数是一个特殊的构造函数,当使用初始化器列表构造函数语法时,初始化器列表构造器优先于其他构造函数,不会走隐式类型转换。
看下面这段代码验证上述信息:
#include<iostream>
using namespace std;
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;
}
template<class T>
Date(const initializer_list<T>& x)
{
auto it = x.begin();
_year = *it;
it++;
_month = *it;
it++;
_day = *it;
cout << "Date(const initializer_list<T>& x)" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date y = { 2024,9,17 };
}
当以初始化列表初始化Date
类时,当存在带参构造函数和初始化列表类型的构造函数时,编译器会优先调用初始化列表构造函数:
只存在带参构造函数,编译器会去调用带参构造函数:
这是多参数的隐式类型转化,编译器会去找有没有对应数量的参数以及相同的类型的构造函数。
如果数量不符或者类型不一致就不能隐式类型转化。
底层直接调用带参构造,并没有先构造为initializer_list
类型,所以这里不包含头文件也可以。
这里实际上也是编译器优化的结果,应该先构造为initializer_list
类型再去构造出一个Date
类的对象,最后调用赋值构造函数,这里直接优化为调用带参的Date
构造函数。
initializer_list容器的底层
看下面这段代码:
#include<iostream>
using namespace std;
int main()
{
std::initializer_list<int> x = { 1,2,3,4,5 };
cout << sizeof(x) << endl;
return 0;
}
运行结果:
这个类的大小为8,实际上,这个类的成员变量是两个指针,一个指向const T*
类型的数组首元素,一个指向最后一个元素的下一个元素处。就像begin
,end
一样。
如果用一个initializer_list
容器去初始化另外一个,它们的_First和_Last应该是一样的:
官方文档里面已经谈到过:
initializer_list
这个类是没有析构函数的,也就是它并不负责数组的空间管理,它只是用来初始化别的容器的。所以不用担心重复析构的问题。
我们在反汇编里面并没有看见调用malloc
函数和new []
,应该是直接在栈上开的局部数组。如果是字符串字面量,则是在只读数据段。
std::initializer_list容器的使用
std::initializer_list
容器主要用来初始化我们自定义的类,像vector
、List
等容器了以initializer_list
为参数的构造函数,我们下面来支持一下我们自己实现的vector
类的{}
构造,给它增加以initializer_list
为参数的构造函数,让它支持这个功能。
这是没有增加初始化列表构造函数,我们自己实现的vector
函数不支持{}
直接初始化:
增加初始化列表构造函数:
template<class T>
vector(std::initializer_list<T> x)
{
resize(x.size());
iterator it = begin();
for (auto data : x)
{
*it = data;
it++;
}
}
运行结果:
与类型相关的两个新特性
变量名推导
auto
关键字的出现给我们提供了很多遍历。像某些时候如果变量的类型名很长,我们不想写,就可以用auto
关键字,让编译器去帮助我们推导。
auto 关键字用于自动类型推导,编译器会根据初始化表达式自动推断变量的类型。这意味着,在编写代码时,你不需要显式地指定变量的类型,编译器会在编译时为你做这件事。
auto
关键字工作在编译时。使用 auto 可以使代码更加简洁,尤其是在处理复杂的类型或者模板编程时,例如:
#include<unordered_map>
#include<queue>
#include<vector>
int main()
{
std::unordered_map<int, int> mp;
std::unordered_map<int, int>::iterator it = mp.begin();
auto it = mp.begin();
return 0;
}
auto
关键字不建议做函数的返回值和函数的参数,可读性不好。
decltype关键字
decltype
表达式可以将变量声明为指定类型。
#include<iostream>
using namespace std;
int main()
{
int a = 0;
decltype(&a) ret1 = nullptr;
const int b = 2;
double c = 2.1;
decltype(b * c) ret2 = 0;
const int& d = a;
decltype(d) ret3 = 0;
cout << typeid(ret1).name() << endl;
cout << typeid(ret2).name() << endl;
cout << typeid(ret3).name() << endl;
}
运行结果:
只会声明为表达式里面值的基本类型,不会加修饰像&
、const
等。
typeid介绍
#include <iostream>
#include <typeinfo>
class Base {
public:
virtual void func() {} // 引入虚函数以支持动态类型识别
};
class Derived : public Base {
};
int main() {
Derived d;
Base* bPtr = &d;
Base& bRef = d;
std::cout << typeid(bPtr).name() << std::endl; // 可能输出 "PBase"(指针类型),具体取决于编译器
std::cout << typeid(*bPtr).name() << std::endl; // 输出 "Derived",因为bPtr指向Derived类型的对象
std::cout << typeid(bRef).name() << std::endl; // 输出 "Derived",因为bRef是Derived类型的引用
return 0;
}
运行结果:
通过调试我们可以看见,确实存在这个虚函数表,并且Base
对象和Derived
对象的虚函数表的里面会有一些关于类型的信息。具体可以看博主这篇博客
type_info类
type_info
类是一个特殊的类,它不是给用户使用的,所以我们无法调用它的构造函数,它是用于存储类型相关的信息的。std::type_info
对象总是在运行时被访问,但其所表示的类型信息可能在编译时就已经确定了(对于非多态类型),也可能在运行时才能确定(对于多态类型)。
我们只能通过typeid
关键字来获得 type_info
类型,它是const type_info &
类型。
#include <iostream>
#include <typeinfo>
using namespace std;
int main()
{
int a = 0;
const type_info& ti = typeid(a);
cout << ti.name() << endl;
return 0;
}
运行结果:
nullptr
这个关键字我们很早就在使用了,它是一个新的类型用来表示空指针,了解决C语言
NULL
的一些问题。
这是C语言中的NULL
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
NULL
是一个宏,是一个整数常量,由于它既可以赋值给整数类型变量,也可以赋值给指针变量,指针和整数混合使用的时候,容易产生类型安全的问题,也会造成代码可读性不好。
但是nullptr
则不同,它有独立的类型:std::nullptr_t
,不会造成NULL
这样的问题。
#include<iostream>
using namespace std;
int main()
{
int* aPtr = NULL;
int a = NULL;
cout << a << endl;
int* bPtr = nullptr;
cout << typeid(nullptr).name() << endl;
}
运行结果:
当nullptr
转成指针类型时就会采用正确的空指针值。而不能赋值给其它类型。
重载函数
使用NULL
给重载函数传值,就会出现问题。
#include<iostream>
using namespace std;
void f(int)
{
cout << "void f(int)" << endl;
}
void f(int*)
{
cout << "void f(int*)" << endl;
}
int main()
{
f(NULL);
return 0;
}
运行结果:
这里我们的本意可能是传给int*
为参数的f
函数,编译器却调用了f(int)
,可能是指针类型需要强转。
使用nullptr
就不会出现这种问题:
但是此时如果传nullptr
,出现一个更符合它的重载函数,不用转成指针类型,编译器就会就先调用它:
STL中的一些变化
增加了一些新容器
array
、forward_list
、unordered_map
、unordered_set
就是C++11中新增的容器。最有用的两个就是最后面两个,它们的底层是哈希表,前面我们已经具体的介绍过了。
老容器增加了一些新的方法
rbegin
,rend
方法,这些是反向迭代器,用到的不多。- 新增列表初始化构造函数。
- 新增
push_back
、insert
、emplace_back
等方法的右值引用版本,极大的提高了效率。 - 增加了移动构造,移动赋值,也可以减少拷贝,提高效率。
pair容器的模拟实现
前面我们一直使用过
pair
容器,没有模拟实现,今天我们来模拟实现以下pair
容器。
namespace my_std
{
template<class T1,class T2>
class pair
{
public:
pair()
{
}
pair(const T1& a, const T2& b)
:_first(a),
_second(b)
{
}
template<class U, class V>
pair(const pair<U, V>& pr)
:_first(pr._first),
_second(pr._second)
{
}
~pair(){}
public:
T1 _first;
T2 _second;
};
}
测试函数:
#include<iostream>
#include<vector>
#include<map>
using namespace std;
namespace my_std
{
template<class T1,class T2>
class pair
{
public:
pair()
{
}
pair(const T1& a, const T2& b)
:_first(a),
_second(b)
{
cout << typeid(T1).name() << endl;
}
template<class U, class V>
pair(const pair<U, V>& pr)
:_first(pr._first),
_second(pr._second)
{
}
~pair(){}
public:
T1 _first;
T2 _second;
};
}
int main()
{
my_std::pair<string, string> kv1("sort", "排序");
my_std::pair<string, string> kv2("string", "字符串");
my_std::pair<const char*, const char*> kv3("sort", "排序");
my_std::pair<const string, string> kv4(kv3);
cout << kv1._first << ": " << kv1._second << endl;
cout << kv2._first << ": " << kv2._second << endl;
cout << kv3._first << ": " << kv3._second << endl;
cout << kv4._first << ": " << kv4._second << endl;
}
运行结果:
右值引用和移动构造
左值引用和右值引用
- 左值和左值引用
我们在之前的博客中也谈到过引用,那就是左值引用,是在C++11之前出现的,左值就是一个表示数据的表达式(像变量名、解引用的指针),它可以出现在等号的左边,我们可以对左值取地址,像
const
修饰的变量虽然我们不能修改它,但是可以取地址,所以它也是左值。
int main()
{
//左值
int a = 3;
int* ptr = &a;
double b = 2.3;
//左值引用
double& b_ = b;
int*& ptr_ = ptr;
int& a_ = a;
}
- 右值和右值引用
右值也是一个表达数据的表达式(函数表达式的返回值(
不能是左值引用
),字面常量,表达式返回值),但是它只能出现在表达式的右边,而不能出现表达式的左边,也不能被修改。对右值进行取别名就叫做右值引用。
上面是比较官方的定义,实际上,右值和左值最大的不同除了不能对右值进行修改,还有一点就是不能对右值取地址。
右值(C++11对右值进行了细分):
- 纯右值(内置类型的右值):10/a+b。
- 将亡值(自定义类型的右值):函数传值返回的返回值/
obj++
/to_string(1234)
.
#include<iostream>
using namespace std;
int* a = (int*)malloc(sizeof(int));
int*& f()
{
return a;
}
int* ff()
{
return a;
}
int main()
{
int x = 0;
int y = 3;
//左值
9;
x + y;
*f() = 3;//f()是左值
int*& left_ = ff();//ff()是右值,对右值不能普通的左值引用
cout << *f() << endl;
//左值引用
}
#include<iostream>
using namespace std;
int* a = (int*)malloc(sizeof(int));
int*& f()
{
return a;
}
int* ff()
{
return a;
}
int main()
{
int x = 0;
int y = 3;
//右值
9;
x + y;
*f() = 3;//f()是左值
//int*& left_ = ff();//ff()是右值,对右值不能普通的左值引用
cout << *f() << endl;
//右值引用
int&& r1 = 9;
int*&& r2 = ff();
int&& r3 = x + y;
}
运行结果:
但是const
修饰的左值引用可以引用右值,对于指针来说,const
必须修饰ptr
也就是引用变量本身,而不是它指向的内容*ptr
。
左值引用和右值引用的比较
上面代码示例验证了const
左值引用可以引用右值,其实const
左值引用也可以引用左值。
#include<iostream>
using namespace std;
int main()
{
int x = 0;
int y = 3;
//右值
9;
//常量的左值引用可以对右值引用
int const& l1 = 9;
//const修饰的左值引用也可以引用左值
const int& l2 = x;
}
- 右值引用只能引用右值,但是它可以引用经过
move
的左值。
#include<iostream>
using namespace std;
int main()
{
int x = 0;
int y = 3;
//右值
9;
x + y;
int&& r1 = 9;
int&& r2 = move(x);
return 0;
}
move
函数可以将左值转成右值,关于这个函数的其它细节,我们到后面内容详谈。
引用的底层原理
在语法层面上,我们认为,左值引用是给左值引用取别名,右值引用是给右值取别名,但底层引用的原理实际上和指针相似。
#include<iostream>
using namespace std;
int main()
{
int a = 0;
int& l = a;
l = 3;
int* ptr = &a;
*ptr = 3;
cout << "--------------------------------" << endl;
int&& r = 100;
r = 10;
return 0;
}
右值引用存在的价值
右值引用主要解决左值引用没有解决的问题。
很多情况下,使用左值引用可以减少不必要的拷贝,这极大的提高了资源的利用率,但是还有时候,由于返回的变量是临时变量所以我们无法左值引用返回,就需要拷贝,C++11中,右值引用的出现就是为了解决这一问题:
string string::substr(size_t pos, size_t len)
{
string s;
if (len == string::npos || pos + len > size_)
{
char* tmp = new char[size_ - pos + 1];//给\0也要开空间
memcpy(tmp, str_ + pos, size_ - pos + 1);
s = tmp;
}
else
{
char* tmp = new char[len + 1];
memcpy(tmp, str_ + pos, len);
tmp[len] = '\0';
s = tmp;
}
return s;
}
这里我们使用的是我们自己实现的string
容器:
#include"string.h"
int main()
{
my_string::string s1 = "xxxxxxxxxx";
cout << "------------------------------------------------------------" << endl;
my_string::string s2 = s1.substr(0);
return 0;
}
移动构造
由于有了右值引用,所以我们的构造函数也多了移动构造,移动构造的作用就是转移右值的资源,减少了不必要的拷贝:
// 移动构造
string::string(string&& s)
:str_(nullptr)
, size_(0)
, capacity_(0)
{
cout << "string(string&& s) -- 移动语义" << endl;
swap(s);
}
我们可以验证一下,使用移动构造是否成功将substr
方法中的局部变量s
的资源窃取,从而避免了拷贝:
左值引用为什么无法完全解决拷贝的问题
上述问题中,解决拷贝问题的关键在于移动构造,移动构造的参数是右值引用,而移动构造解决拷贝问题的关键又在于它可以转移右值的资源。
const
修饰的左值引用也可以引用右值,为什么不把右值传给它,然后转移其资源呢?
- 无法转移,因为它被
const
修饰,无法修改它里面的值。
右值引用引用右值的几种情况
引用的不是指针
但是右值引用不同,右值引用给右值起别名之后,变量本身不再具有常性,反而变得可以修改,所以我们可以转移其资源,移动构造我们可以变相的认为延长了它右值的生命周期(因为转移后它的资源还存在),右值引用功不可没。
右值引用变相的让右值变得可以修改。
#include<iostream>
using namespace std;
int* f()
{
int* ptr = (int*)malloc(sizeof(int));
*ptr = 3;
return ptr;
}
int main()
{
f() = (int*)2;
return 0;
}
ptr
会拷贝一个临时对象存储ptr
的值,这个值作为返回值返回,具有常性,不能修改:
我们将其作为参数或者右值传给右值引用变量就可以修改了,因为这个变量具有左值属性。
#include<iostream>
using namespace std;
int* f()
{
int* ptr = (int*)malloc(sizeof(int));
return ptr;
}
int main()
{
int*&& r1 = f();
r1 = (int*)0;
int* ptr_ = f();
ptr_ = (int*)0;
return 0;
}
这里ptr_
和r1
都可以修改,貌似它们是一样的其实不一样,前者是对右值的引用,在语法层面是对其起别名,也就是对那个函数返回的临时变量起别名,少了一次拷贝。但是r1
是把临时变量拷贝一份。
对于右值引用
可以修改,我们无需重点关注这个,因为它最重要的应用不在这里,而是在移动构造里面的转移资源,借助右值引用减少了拷贝,这个我们上面已经重点介绍过,不再重复赘述。
- 有小伙伴可能会这样写代码:
#include<iostream>
using namespace std;
int*&& f()
{
int* ptr = (int*)malloc(sizeof(int));
return move(ptr);
}
int main()
{
f() = (int*)0;
return 0;
}
这里虽然函数的返回值是右值引用类型,但是它仍然还是一个右值,因为这个右值返回后还没有来得及保存,没有赋值给一个普通变量或者右值引用的变量,它仍然具有右值属性,不能修改。所以我们要少使用右值引用类型作为返回值,因为这容易让人被误导,认为它没有被接收前可以修改,它的作用也不大,右值引用最常见的应用还是移动构造。
引用的是指针变量
此时如果引用的是指针变量,并且这个指针变量指向的内容具有常性,我们使用右值引用,只能让这个指针变量本身变得可修改,并不能让其指向的内容可修改。
下面是一段代码:
#include <iostream>
using namespace std;
int main()
{
int num = 3;
int const* ptr = #
int const * &&r1 = move(ptr);//如果不加*修饰r1指向的内容,程序就会报错
//*r1 = 4;不可修改r1指向的内容
r1 = nullptr;//r1可修改
return 0;
}
运行结果:
不加const
修饰r1
变量指向的内容,程序就会报错:
右值引用引用左值
右值引用也可以引用左值,就是我们前面提到的将左值转化为右值的函数move
,这个函数并不移动任何资源,只是简单的将左值转化为右值:
#include"string.h"
int main()
{
my_string::string s1 = "xxxxxxxxxx";
my_string::string s2(s1);
my_string::string s3(move(s1));
return 0;
}
这里在移动构造s3
后,s1
内部的资源会被搬空。
完美转发
右值引用变量在接收一个右值之后,它的属性没有保持和右值一样,而是一个左值的属性,如果我们想在参数的传递过程中一直保持右值属性,就需要学习完美转发。
完美转发所依赖的语法之一万能引用:
template<class T>
void forward(T&& x)
{
}
模板类型T+&&,所构成的类型T&&
叫做万能引用,它既能接收左值又能接收右值,但不能维持它们原先的属性(const
、左值\右值)。
下面一段代码来验证我们的结论:
#include<iostream>
using namespace std;
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 forward_(T&& x)
{
Func(x);//转发
}
int main()
{
int num = 3;
const int& l1 = num;
int& l2 = num;
forward_(l1);
forward_(l2);
forward_(move(l2));
forward_(move(l1));
return 0;
}
运行结果:
当我们使用完美转发之后,就可以保持变量对应的属性了:
#include<iostream>
using namespace std;
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 forward_(T&& x)
{
Func(forward<T>(x));//转发
}
int main()
{
int num = 3;
const int& l1 = num;
int& l2 = num;
forward_(l1);
forward_(l2);
forward_(move(l2));
forward_(move(l1));
return 0;
}
运行结果:
完美转发的应用场景
C++给一些模板类的push_back
、insert
等函数增加了右值引用版本,这样也可以减少容器中T
类型的深拷贝,因为T
可能是string
这种需要深拷贝的类型。右值传给万能引用参数之后,又变成左值了,这个时候,如果我们还要赋值或者调用其它的函数,需要保持其原有属性才能减少拷贝,这个时候可以使用完美转发或者move
,但是使用move
要注意,使用move
后你的最开始的那个拥有资源的变量,就不安全了,因为它的资源有被窃取的风险,所以要少使用move
。
万能引用
万能引用作为函数形式参数,既可以接收左值,又可以接收右值。为了把它和单纯的模板参数的右值引用区分,通常有如下规定:
- 万能引用必须要在函数方法前加模板参数,无论这个模板参数有没有出现,否则就是右值引用,不是万能引用:
#include"string.h"
#include<iostream>
using namespace std;
template<class T>
class A
{
public:
void Func(T&& x)//普通右值引用
{
cout << "void Func(T&& x)" << endl;
}
};
int main()
{
my_string::string s1 = "xxxxxx";
A<my_string::string>().Func(s1);
A<my_string::string>().Func(my_string::string("ssssss"));
return 0;
}
Func
函数此时是右值引用:
- 在函数名前面加上模板参数才是万能引用:
#include"string.h"
#include<iostream>
using namespace std;
template<class T>
class A
{
public:
template<class T>
void Func(T&& x)//万能引用
{
cout << "void Func(T&& x)" << endl;
}
};
int main()
{
my_string::string s1 = "xxxxxx";
A<my_string::string>().Func(s1);
A<my_string::string>().Func(my_string::string("ssssss"));
return 0;
}
- 当万能引用和
const 左值引用
作为函数重载的不同参数出现,编译器会去调用更符合要求的函数:
#include"string.h"
#include<iostream>
using namespace std;
template<class T>
class A
{
public:
void Func(const T& x)
{
cout << "void Func(const T & x)" << endl;
}
template<class T>
void Func(T&& x)
{
cout << "void Func(T&& x)" << endl;
}
};
int main()
{
my_string::string s1 = "xxxxxx";
A<my_string::string>().Func(s1);
return 0;
}
运行结果:
如果加上const
就会去调用前者,因为它更加符合要求:
- 本人知识、能力有限,若有错漏,烦请指正,非常非常感谢!!!
- 转发或者引用需标明来源。