💓博主CSDN主页:麻辣韭菜💓
⏩专栏分类:C++修炼之路⏪
🚚代码仓库:C++高阶🚚
🌹关注我🫵带你学习更多C++知识
🔝🔝
1.C++ 11 简介
目录
1.C++ 11 简介
2. 统一的列表初始化
2.1 {}初始化
2.2 initializer_list
3.1 auto
3.2 decltype
4.右值引用和移动语义
4.2 左值引用与右值引用比较
4.3 右值引用使用场景和意义
4.4 右值引用引用左值及其一些更深入的使用场景分析
5.完美转化
5.1模板中的&& 万能引用(引用折叠)
5.2完美转发实际中的使用场景:
C++11是C++编程语言的一个主要更新版本,它在2011年被国际标准化组织(ISO)正式批准为ISO/IEC 14882:2011标准。这个版本引入了许多新特性和改进,旨在提高语言的表达能力、性能和易用性。以下是C++11的一些关键特性:
自动类型推断(auto关键字):允许编译器自动推断变量的类型,简化了代码编写,尤其是在模板元编程中。
基于范围的for循环:提供了一种更简洁的方式来遍历容器。
lambda表达式:允许在代码中创建匿名函数,这在算法编程中非常有用。
智能指针:如
std::unique_ptr
和std::shared_ptr
,提供了自动内存管理,减少了内存泄漏的风险。右值引用和移动语义:允许更高效的资源管理,特别是对于临时对象。
变长模板参数:允许模板函数和类接受任意数量的模板参数。
委托构造函数:允许构造函数之间相互委托。
继承构造函数:允许派生类继承基类的构造函数。
线程库:引入了一个新的线程库,支持多线程编程。
原子操作和内存模型:提供了对多线程编程的支持,包括原子操作和内存顺序控制。
用户定义的字面量:允许用户定义新的字面量后缀。
静态断言:可以在编译时检查条件。
类型特征和类型遍历:提供了类型信息查询和类型操作的能力。
统一的初始化列表:简化了对象的初始化。
新的字符串字面量:如u8"",支持UTF-8编码。
匿名结构体和类:允许在声明中嵌套结构体和类。
属性(属性修饰符):如[[deprecated]],允许标记某些特性为已弃用。
正则表达式库:提供了正则表达式的处理能力。
C++11的这些特性使得C++语言更加强大和灵活,同时也使得编写的代码更加简洁和易于维护。
2. 统一的列表初始化
2.1 {}初始化
#include <iostream>
using namespace std;
struct Point
{
int _x;
int _y;
};
int main()
{
int x1 = 1;
int x2 = { 2 };
// int x4(1);
// 可以省略赋值符号
int x3{ 3 };
int array1[]{ 1, 2, 3, 4, 5 };
int array2[5]{ 0 };
Point p{ 1, 2 };
// C++11中列表初始化也可以适用于new表达式中
int* pa = new int[4]{ 0 };
return 0;
}
C++11 扩大了用大括号括起的列表 ( 初始化列表 ) 的使用范围,使其可用于所有的内置类型和用户自定义的类型,使用初始化列表时,可添加等号 (=) ,也可不添加 。
我个人感觉还是添加等号(=),比较好。
class Date
{
public:
//explicit Date(int year, int month, int day)
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, 1, 1); // old style
// C++11支持的列表初始化,这里会调用构造函数初始化
Date d2 = { 2024, 5, 20 };
Date d3 { 2024, 5, 21 };
return 0;
}
创建对象时也可以使用列表初始化方式调用构造函数初始化
2.2 initializer_list
它提供了一种便捷的方式来接收和使用初始化列表(initializer lists)。初始化列表是一组用花括号 {}
包围的初始化值。
在 C++11 之前,当你需要初始化一个容器或对象时,你通常需要使用单独的参数或者构造函数的多个参数。std::initializer_list
的引入使得你可以一次性传递一个值的列表,这在初始化数组、容器或自定义类型时非常有用。
#include <initializer_list>
#include <iostream>
class MyClass {
public:
MyClass(std::initializer_list<int> init) {
for (auto value : init) {
values.push_back(value);
}
}
void print() const {
for (auto value : values) {
std::cout << value << " ";
}
std::cout << std::endl;
}
private:
std::vector<int> values;
};
int main() {
MyClass obj1 = {1, 2, 3, 4, 5}; // 使用列表初始化 MyClass
obj1.print(); // 输出: 1 2 3 4 5
MyClass obj2{6, 7, 8, 9, 10}; // 直接初始化 MyClass
obj2.print(); // 输出: 6 7 8 9 10
return 0;
}
std::initializer_list作为参数的构造函数,这样初始化容器对象就更方便了。也可以作为operator=
class Date
{
public:
//explicit Date(int year, int month, int day)
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()
{
vector<int> v1 = { 1,2,3,4,5 };
vector<int> v2 = { 10,20,30 };
vector<int> v3 = { 10,20,30,1,1,2,2,2,2,2,2,1,1,1,1,1,1,1,1,2,1,1,2 };
list<int> lt1 = { 1,2,3,4,5 };
list<int> lt2 = { 10,20,30 };
auto i1 = { 10,20,30,1,1,2,2,2,2,2,2,1,1,1,1,1,1,1,1,2,1,1,2 };
auto i2 = { 10,20,30 };
cout << typeid(i1).name() << endl;
cout << typeid(i2).name() << endl;
initializer_list<int>::iterator it1 = i1.begin();
initializer_list<int>::iterator it2 = i2.begin();
cout << it1 << endl;
cout << it2 << endl;
//*it1 = 1;
initializer_list<int> i3 = { 10,20,30 };
initializer_list<int>::iterator it3 = i3.begin();
cout << it3 << endl;
Date d1(2023,5,20);
Date d2(2023,5,21);
// initializer_list<Date>
vector<Date> vd1 = {d1, d2};
vector<Date> vd2 = { Date(2023,5,20), Date(2023,5,21) };
vector<Date> vd3 = { {2023,5,20}, {2023,5,20} };
map<string, string> dict = { {"sort", "排序"},{"string", "字符串"},{"Date", "日期"} };
pair<string, string> kv1 = { "Date", "日期" };
pair<string, string> kv2 { "Date", "日期" };
return 0;
}
3.1 auto
int main()
{
int i = 10;
auto p = &i;
auto pf = strcpy;
cout << typeid(p).name() << endl;
cout << typeid(pf).name() << endl;
map<string, string> dict = { {"sort", "排序"}, {"insert", "插入"} };
//map<string, string>::iterator it = dict.begin();
auto it = dict.begin();
return 0;
}
3.2 decltype
关键字decltype将变量的类型声明为表达式指定的类型。
int main()
{
const int x = 1;
double y = 2.2;
cout << typeid(x * y).name() << endl;
decltype(x * y) ret; // ret的类型是double
decltype(&x) p; // p的类型是const int*
cout << typeid(ret).name() << endl;
cout << typeid(ret).name() << endl;
cout << typeid(p).name() << endl;
// vector存储的类型跟x*y表达式返回值类型一致
// decltype推导表达式类型,用这个类型实例化模板参数或者定义对象
vector<decltype(x* y)> v;
return 0;
}
4.右值引用和移动语义
什么是左值?什么是左值引用?
int main()
{
// 以下的p、b、c、*p都是左值
int* p = new int(0);
int b = 1;
const int c = 2;
// 以下几个是对上面左值的左值引用
int*& rp = p;
int& rb = b;
const int& rc = c;
int& pvalue = *p;
return 0;
}
int main()
{
double x = 1.1, y = 2.2;
// 以下几个都是常见的右值
10;
x + y;
fmin(x, y);
// 以下几个都是对右值的右值引用
int&& rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = fmin(x, y);
// 这里编译会报错:error C2106: “=”: 左操作数必须为左值
10 = 1;
x + y = 1;
fmin(x, y) = 1;
return 0;
}
int main()
{
double x = 1.1, y = 2.2;
int&& rr1 = 10;
const double&& rr2 = x + y;
rr1 = 20;
rr2 = 5.5; // 报错
return 0;
}
4.2 左值引用与右值引用比较
int main()
{
// 左值引用只能引用左值,不能引用右值。
int a = 10;
int& ra1 = a; // ra1为a的别名
//int& ra2 = 10; // 编译失败,因为10是右值
// const左值引用既可引用左值,也可引用右值。
const int& ra3 = 10;
const int& ra4 = a;
return 0;
}
int main()
{
// 右值引用只能右值,不能引用左值。
int&& r1 = 10;
// error C2440: “初始化”: 无法从“int”转换为“int &&”
// message : 无法将左值绑定到右值引用
int a = 10;
int&& r2 = a;
// 右值引用可以引用move以后的左值
int&& r3 = std::move(a);
return 0;
}
4.3 右值引用使用场景和意义
既然左值引用可以引用左值和引用右值,那C++11搞出右值引用岂不是多此一举?不要忘了左值引用还有短板没有解决。
我们通过代码来演示右值引用的意义
通过上面的代码示例我们可以看到 传值a+b会调用参数是右值引用的func函数。当然有人觉得这不能说明右值引用的意义。那如果传值是个自定义类型的参数比如string这样的容器又或者unordered_map 这样的?
比如下面这种场景
#pragma once
#include <iostream>
#include <list>
#include <vector>
#include <cassert>
#include <map>
using namespace std;
namespace gx
{
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;
}
string operator+(char ch)
{
string tmp(*this);
tmp += ch;
return tmp;
}
const char* c_str() const
{
return _str;
}
private:
char* _str;
size_t _size;
size_t _capacity; // 不包含最后做标识的\0
};
}
#include "String.h"
int main()
{
gx::string s1("hello world");
gx::string s2(s1);
gx::string s3 = (s2 + '!');
return 0;
}
s2+'!' 是个右值 右值又称为将亡值。将亡值也就意味着执行完这条语句。s2+'!'所在的空间会被析构函数回收。那我们这时再拷贝一个和它一样的大的空间岂不是浪费了?能不能把它的空间直接给s3?
当然可以 我们在头文件String.h增加这段代码
// 移动构造
string(string&& s)
:_str(nullptr)
,_size(0)
,_capacity(0)
{
cout << "string(string&& s) -- 移动构造" << endl;
swap(s);
}
移动构造代价就比深拷贝的代价低了很多。效率就变高了!
之前的学习我们知道左值引用使用场景:
一是引用传参二是引用返回
但是引用返回如果是函数内的局部对象,是不能用引用返回的。
这就意味局部对象返回时是要拷贝的。那如果返回的局部对象是一颗红黑树?那拷贝就极大降低了效率。
举个例子 string 的to_string这个函数。
gx::string to_string(int value)
{
bool flag = true;
if (value < 0)
{
flag = false;
value = 0 - value;
}
gx::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;
}
那如果是C++11之后的编译器呢?
编译器直接优化成移动构造
// 移动赋值
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移动语义" << endl;
swap(s);
return *this;
}
C++11后STL中的容器都是增加了移动构造和移动赋值:
这里就列举移动构造感兴趣的可以去cplusplus官网看看。
4.4 右值引用引用左值及其一些更深入的使用场景分析
int main()
{
gx::string s1("hello world");
// 这里s1是左值,调用的是拷贝构造
gx::string s2(s1);
// 这里我们把s1 move处理以后, 会被当成右值,调用移动构造
// 但是这里要注意,一般是不要这样用的,因为我们会发现s1的
// 资源被转移给了s3,s1被置空了。
gx::string s3(std::move(s1));
return 0;
}
STL容器插入接口函数也增加了右值引用版本:
以链表为例
从上面运行结果来看,减少了深拷贝。那就意味着效率就提高了。当然move以后也有风险。s1就悬空了没有了。
一般来说我们写法不会用move而是下面这种
那如果是C++98 那就是拷贝构造。而11之后就是移动构造。
总结:
左值引用减少拷贝,提高效率
右值引用也是减少拷贝,提高效率
但是他们的角度不同,左值引用是直接减少拷贝
右值引用是间接减少拷贝,识别出是左值还是右值,如果是右值,则不再深拷贝,直接移动拷贝!提高效率。
5.完美转化
5.1模板中的&& 万能引用(引用折叠)
模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力,
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;
}
先看上面一段代码的 按照常理 PerfectForward(std::move(a)); PerfectForward(std::move(b));
是会调用void Fun(int&& x) { cout << "右值引用" << endl; }和void Fun(const int&& x) { cout << "const 右值引用" << endl; }函数
运行结果却都是左值引用?这是为什么?
因为右值本身就具有常性。不可修改 PerfectForward这个函数参数是万能引用 当识别是右值时,编译器发现是右值时,会开辟一段空间,而这个开辟的空间会和传参的值进行资源转换。而在转换的过程中会出现属性丢失。也就是说原本的右值属性变成了左值属性。而这时在fun这函数进行传参t时,而这个t就变成左值。那调用时就会调用参数是左值的fun
那如何解决这个问题?
那就需要用到完美转发 forward<>()
std::forward<T>(t)在传参的过程中保持了t的原生类型属性。
5.2完美转发实际中的使用场景:
就比如list 我们使用push_back 时,我们后来又实现了insert这个插入函数
如果push_back函数套用insert这个函数就会出现上面的情况。传参时出现属性丢失。
原本是右值的移动构造就会变成拷贝构造。 效率就降低了。
template<class T>
struct ListNode
{
ListNode* _next = nullptr;
ListNode* _prev = nullptr;
T _data;
};
template<class T>
class List
{
typedef ListNode<T> Node;
public:
List()
{
_head = new Node;
_head->_next = _head;
_head->_prev = _head;
}
void PushBack(T&& x)
{
//Insert(_head, x);
Insert(_head, std::forward<T>(x));
}
void PushFront(T&& x)
{
//Insert(_head->_next, x);
Insert(_head->_next, std::forward<T>(x));
}
void Insert(Node* pos, T&& x)
{
Node* prev = pos->_prev;
Node* newnode = new Node;
newnode->_data = std::forward<T>(x); // 关键位置
// prev newnode pos
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = pos;
pos->_prev = newnode;
}
void Insert(Node* pos, const T& x)
{
Node* prev = pos->_prev;
Node* newnode = new Node;
newnode->_data = x; // 关键位置
// prev newnode pos
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = pos;
pos->_prev = newnode;
}
private:
Node* _head;
};
int main()
{
List<gx::string> lt;
lt.PushBack("1111");
lt.PushFront("2222");
return 0;
}
如果我们把任意一个关键位置的forward的去掉,都会是拷贝构造。
右值引用非常的绕,希望大家下去可以多看几遍。有什么不懂的可以私信我。关注我带你学习更多C++知识。下节预告可变参数模板、lambda。也是非常重要!!!