一切皆可列表{ }初始化
在C++98,允许花括号{ } 对数组、结构体类型初始化。
class Data
{
public:
Data(int y, int m, int d)
:_y(y), _m(m), _d(d)
{}
private:
int _y;
int _m;
int _d;
};
int arr[4]={0,1,2,3};//列表初始化
Data d1={2024,03,21};//列表初始化
C++11允许通过{ } 初始化内置类型或者用户自定义类型。同时支持省去赋值=符号
class Data
{
public:
Data(int y, int m, int d) :_y(y), _m(m), _d(d)
{}
private:
int _y;
int _m;
int _d;
};
int main()
{
int a = 1;
int b = { 1 };//支持列表初始化
int c{ 1 }; //支持列表初始化,同时省略=
Data d1(2024, 03, 21);
Data d2={ 2024, 03, 21 };//支持列表初始化
Data d3{ 2024, 03, 21 };//支持列表初始化,同时省略=
Data* p1=new Data[]{{2023,03,21},{2023,03,22},{2023,03,23}};
return 0;
}
创建d2时,会先调用{2024 ,03,21}列表构造出一个临时对象,再用临时对象拷贝构造d2。
如何证明?对列表的对象取别名,只有加const后才能通过。
编译器一般将 构造+拷贝构造优化成—>直接构造
std::initializer_list
在C++11中,std::initializer_list
是一个模板类,它用于表示初始化列表。
当编译器遇到一个使用花括号{}
的初始化列表时,它会尝试构造一个std::initializer_list
对象,并将该对象传递给接受std::initializer_list
参数的函数或构造函数。
initializer_list类中存在俩个指针,begin和end。
initializer_list同时支持迭代器
initializer_list<int> il{ 1,2,3,4 };
initializer_list<int>::iterator it = il.begin();
while (it != il.end())
{
cout << *it << " ";
++it;
}
在C++11往后的各种容器中,都支持initializer_list构造
vector<int> v1{ 1,2,3,4 };
vector<string> v2{"apple","map","cat"};
initializer_list去构造vector的原理
vector(initializer_list<T> it)
{
reserve(it.size());
for (auto& e : it)
{
push_back(e);
}
}
initializer_list的数据是放在常量区,不可修改
auto自动推演
对于较长的类型,写起来比较复杂,就要auto帮助自动推演。
map<string, string> dict = { { "sort", "排序" }, { "insert", "插入" } };
//map<string, string>::iterator it = dict.begin();
auto it = dict.begin(); //简化代码
decltype
将对象声明为指定的类型;
typeid().name返回一个字符串,字符串的内容是类型。一般用于打印。
int x = 1;
double y = 3.14;
decltype(x * y) ret;
cout << "typeid(ret).name():" << typeid(ret).name()<< endl;
新容器
array
是一个静态数组,内存是放在栈上的。
array有俩个模板参数,第一个模板参数是类型,第二个是大小
array的设计初心是为了代替数组,因为数组的越界检查不严格,因此array是严格的断言。
但是在日常的使用,完全可以用vector代替。
int main()
{
array<int, 10> a1; //定义一个可存储10个int类型元素的array容器
array<double, 5> a2; //定义一个可存储5个double类型元素的array容器
return 0;
}
forward_list容器
forwar_list是一个单链表
在实际使用,forward_list的运用少于list
- forward_list只支持头插头删,尾插尾删的复杂度是O(N);
- forward_list支持inset_after,插入的时间复杂度是O(N);
- 删除指定元素要找前一个结点。复杂度是O(N);
unordered_map和unordered_set容器
底层是哈希表实现的map和set
右值引用
什么是左值?
数据的表达式,通常出现在等号的左边,是可以被取地址的。
int a = 1;
int* b = new int;
const int c = 2;
int d = a; //d也是左值
什么是右值?
字母常量,表达式的返回值,函数的返回值,通常出现在等号的右边
是不能被取地址的;
10; //常量
Add(10, 5);//函数返回值
5 + 1; //表达式返回值
- 右值的本质是一块临时变量,没有被存储起来,即将被销毁的。因此无法取到地址。
- 值得注意的是,函数的返回值如果返回的是一块实际存储的空间,那么就是不是右值。
左值引用和右值引用
在C++11新特性后,增加了右值引用的玩法。
左值就是给左值取别名,右值就是给右值取别名。本文着重介绍右值。
右值引用的符号 (类型&&)
10; //常量
Add(10, 5);//函数返回值
5 + 1; //表达式返回值
int&& rp1 = 10;
int&& rp2 = Add(10, 5);
int&& rp3 = 5 + 1;
注意:
一个右值被取别名后,就被存储到特定的位置上,就能通过别名修改右值的内容;
左值引用可以引用右值吗?
不能。左值是可以被修改的,右值是常量不可被修改。将右值给左值引用,涉及权限的放大。
给左值引用加上const后,就能引用右值。
int& rp = Add(10, 5);//出错
const int& rp = Add(10, 5);//正常
右值引用可以引用右值吗?
右值引用只能引用右值。
右值引用可以引用move后的左值。
10;//右值
int i = 10;//左值
int&& r = move(i);
主要原因:
- 语义不匹配:右值引用的设计初衷是为了支持移动语义,即从一个临时对象(右值)中“窃取”资源并转移到另一个对象。左值通常具有持久的存储位置,因此将其与移动语义相关联是不合适的。
- 避免意外行为:如果允许右值引用引用左值,那么开发者可能会不小心将左值当作右值来处理,从而导致资源被错误地移动而不是复制。这可能导致程序出现难以调试的错误。
move是一种资源转移,对于左值的资源转移,要非常的谨慎!
右值引用的场景
移动构造函数和移动赋值运算符:
就拿模拟实现的to_string函数的来说明
To_string函数体中的str是一个左值,出了函数作用域,就会被销毁。我们把这个即将销毁的值叫做 “将亡值” 。
这一条语句的执行过程是:str先构造出一个临时对象,再用临时对象拷贝构造出s对象(如果考虑优化,编译器会直接用str构造出s对象)
本文默认不考虑编译器的优化
如果str出了作用域就会被销毁,那么他的资源就被释放,反而重新拷贝出一份一模一样的资源,这是浪费效率的事情,如果在此时发生深拷贝,那么效率会更低。
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;
}
对于将亡值,编译器会优先调用移动拷贝和移动赋值。
右值通过move引用左值
通过move函数后,s1会被当成右值,调用移动构造函数,会将s1的资源转移到s2上,s1的内容会变为空。
总结:将亡值会调用移动构造和移动赋值来实例化对象
左值可以通过move转移资源,来调用移动构造和移动赋值。
万能引用
万能引用是指在模板中,即可以接收左值又可以接收右值。
template<class T>
void Fun(T&& x) //万能引用
{
cout << x << endl;
}
int main()
{
Fun(10);//传入右值
int i = 1;
Fun(i);//传左值
return 0;
}
调用模板函数,传入左值和右值都能被Fun函数接收,故针对模板类的&&是万能引用
右值引用的属性是什么?
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 PerfectForward(T&& t)
{
Func(t);
}
int main()
{
int a = 10;
PerfectForward(a); //左值
PerfectForward(move(a)); //右值
const int b = 20;
PerfectForward(b); //const 左值
PerfectForward(move(b)); //const 右值
return 0;
}
左值和右值都会被万能引用接收,而后非const调用的Func函数是左值引用。
const调用的Func函数是右值引用。
说明右值被右值引用接收后的属性是左值!
针对这一点需要注意的,假如模拟实现的vector容器push_back接收到右值,调用inset()函数(同样实现了右值版本),却发现调不动,因为右值被引用后的结果是左值。
要让insert也调用右值,就必须先move再调用insert。
完美转发
完美转发(Perfect Forwarding)是一种技术,它允许函数模板将其参数无损地转发给另一个函数,保持参数的原始值类别(lvalue或rvalue)和类型。这通常用于编写通用包装器或代理函数,例如标准库中的 std::forward
和 std::tie
。
template<class T>
void PerfectForward(T&& t)
{
Func(std::forward<T>(t));
}
有了完美转发之后,就不用担心出现移动构造+赋值的情况,就会出现移动构造+移动赋值,大大减小了资源的利用。