目录
一. 列表初始化
1)用法
2) initializer_list
小节:
二,简化声明
1) ,auto
2) ,decltype类
3),nullptr
三,范围for
四,C++11后,STL容器变化
五,左值与右值
1. 左值
2. 右值
3,左值与右值之间的比较
4. 右值引用使用场景
(1,将死右值的移动语义操作
(2. 编译器传值返回优化
(3,将亡值做参数
(4. 完美转发
5,默认成员函数
(1. 默认成员函数的强制生成
(2. 默认成员函数的禁止调用生成
嗨!收到一张超美的风景图,希望你每天都能顺心!
一. 列表初始化
首先,我们需要区分的是,什么是初始化列表与列表初始化。 前者是在类对象创建时,对成员变量进行初始化。后者是C++11优化后添加的新功能。
C++中为了满足泛型编程,基础类型如int, double等内置类型都被重写成了类。这些类被称为包装类,它们提供了一些额外的功能,比如重载运算符、提供类型转换等,以便更好地支持泛型编程。
从下面的用法,我们其实可以窥见一二:
1)用法
struct Point
{
int _x;
int _y;
};
int main()
{
int array1[] = { 1, 2, 3, 4, 5 };
int array2[5] = { 0 };
Point p = { 1, 2 };
return 0;
}
struct Point
{
int _x;
int _y;
};
int main()
{
// 内置类型
int x1 = 1;
int x2{ 2 };
int array1[]{ 1, 2, 3, 4, 5 };
int array2[5]{ 0 };
// C++11中列表初始化也可以适用于new表达式中
int* pa = new int[4]{ 0 };
//自定义类型
Point p{ 1, 2 };
map<string, string> z1 = { {"li", "a"}, {"ze", "b"} };
map<string, string> z2{ {"li", "a"}, {"ze", "b"} };
return 0;
}
2) initializer_list
其实这个{ }本质上是一个特殊类,叫:std::initializer_list
介绍文档:initializer_list - C++ Reference (cplusplus.com)
那自定义类型,比如:vector如何利用initializer_list来实现列表初始化的呢?? 下面是vector构造函数,C++11的构造方法,在下面我们能看到我们要找的构造
自己制作一个支持initializer_list的构造函数还是比较简单的:
但是我们也要注意:initializer_list中的变量是常量,在静态区,因此这些数据仅仅只能做数据值传递,无法修改。
小节:
C++11后,STL容器已经全部支持std::initializer_list列表初始化,作为构造函数的参数,使初始化容器对象就更方便了。
二,简化声明
1) ,auto
对于我们来说auto已经是老朋友了,在C++11之前,变量的类型必定要显式地指定。然而,C++11引入了auto关键字,允许编译器根据变量的初始化表达式来推断其类型。这意味着可以更加简洁地声明变量,减少了代码的冗余性和提高了代码的可读性。
例子 :
map<string, string> z1 = { {"li", "a"}, {"ze", "b"} };
// auto遍历
for (auto& e : z1)
{
cout << e.first << endl;
}
// 推导迭代器
auto it = z1.begin();
2) ,decltype类
decltype的主要作用是推断表达式的类型,并且保留表达式的const和引用属性(auto 不能保留)。这使得我们可以使用decltype来声明变量、函数返回类型,以及在模板中推断函数返回类型等。
在泛型编程中,decltype可以帮助我们避免重复输入类型信息,提高代码的可读性和可维护性。它还可以用于推断lambda表达式的返回类型,以及在模板元编程中进行类型推断。
总之,decltype类的意义:提供了一种灵活的类型推断机制,使得C++编程更加方便和高效。
3),nullptr
在C++11中添加了nullptr是为了解决空指针的歧义问题。在之前的C++版本中,使用NULL来表示空指针可能会引发一些问题,因为NULL可能被定义为0或者(void*)0,这样就会导致一些类型转换的问题。
使用nullptr可以明确地表示空指针,避免了歧义,同时也可以提高代码的可读性和安全性。
三,范围for
范围for我们在遍历数据时,经常使用,其底层原理是迭代器。有了范围for后,使用范围for循环可以更加直观和简洁地遍历容器或数组中的元素,而不需要手动管理迭代器或索引变量。这使得代码更易读、易维护,并且减少了出错的可能性。范围for循环也可以与自定义类型一起使用,只要该类型支持迭代操作。因此,范围for功能的添加使得C++代码更加现代化和易用。
四,C++11后,STL容器变化
前面所学的STL中,现在回望这些C++11才出现的容器。
还有一些比如:右值引用等等,这个我们后面细讲。
五,左值与右值
1. 左值
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;
}
传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,所以从现在开始我们之前学习的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名。
2. 右值
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;
}
3,左值与右值之间的比较
int main()
{
// 左值引用只能引用左值,不能引用右值。
int a = 10;
int& ra1 = a; // ra为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. 右值引用使用场景
聊使用场景前我们需要先更深入的了解右值引用: 右值是不可获取地址的,同时我们在用左值引用时,因为其不可修改性,还必须添加 const 才能接收。那我们思考下面代码
补充:
因此,我们可以这么理解右值引用是用来修改右值的工具,那什么场景下使用该工具? 请观察下面是简易实现的string类代码:
(1,将死右值的移动语义操作
namespace bit
{
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(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(){
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
};
}
我们用右值引用,从引用中区分出将死右值引用,同时重载赋值函数,移动语义,间接达到了减少一次拷贝的功能,解析如下:
(2. 编译器传值返回优化
当然这种优化跟编译器的版本,新旧有关,我们了解即可。
(3,将亡值做参数
(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(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;
}
但万能引用,却不一定“万能”,模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力,不代表就能表示左右值引用类型,引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值,我们希望能够在传递过程中保持它的左值或者右值的属性, 就需要用我们下面学习的完美转发。
头文件:utility
用万能引用接受过数据,尽量每次用完美转发的形式重新获取准确数据类型再作为参数。
5,默认成员函数
1. 没有自己实现 移动构造 函数,且析构函数 、拷贝构造、拷贝赋值重载 都未实现 。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝;自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
2. 没有自己实现 移动赋值重载 函数,且析构函数 、拷贝构造、拷贝赋值重载 都未实现 ,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝;自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。( 默认移动赋值跟上面移动构造完全类似)
如果你提供了移动构造 或 者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。
(1. 默认成员函数的强制生成
关键字: 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)
{
cout << "拷贝构造" << endl;
}
Person(Person&& p) = default; // 强制生成移动构造
Person& operator=(Person&& p) = default; // 移动赋值基于移动构造
private:
string _name;
int _age;
};
int main()
{
Person s1("1111", 2);
Person s2 = s1;
Person s3 = std::move(s1);
return 0;
}
(2. 默认成员函数的禁止调用生成
结语
本小节就到这里了,感谢小伙伴的浏览,如果有什么建议,欢迎在评论区评论,如果给小伙伴带来一些收获请留下你的小赞,你的点赞和关注将会成为博主创作的动力。