一切皆可列表{ }初始化
在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));
}
有了完美转发之后,就不用担心出现移动构造+赋值的情况,就会出现移动构造+移动赋值,大大减小了资源的利用。