目录
前言
一、本节学习内容概要
1.1 本节主要学习什么?
1.2 vector 和普通数组有什么区别?
1.3 vector 的基本内存理解
二、vector 的遍历:auto、auto& 和 const auto&
2.1 使用范围 for 遍历 vector
2.2 auto d:会复制元素
2.3 auto& d:引用原来的元素,可以修改 vector
2.4 const auto& d:只读访问,避免复制
三、vector 作为函数参数:传值会复制
3.1 vector 作为参数传值
3.2 传值时,函数内部是新的 vector
3.3 vector 传值复制的是什么?
3.4 return vdatas 为什么可以?
3.5 为什么返回后的地址可能和函数内部一样?
四、move 移动语义:减少 vector 数据复制
4.1 std::move 的基本使用
4.2 move 本身并不搬数据
4.3 使用 move 后,原来的 vector 会怎么样?
4.4 move 前后的地址变化
4.5 move 的优点和代价
五、vector 引用传参与返回值
5.1 vector 引用传参不会复制
5.2 引用传参时,函数内部地址不变
5.4 如果想返回引用,返回值也要写引用
六、完整代码示例
七、总结
7.1 使用 vector 需要引入头文件
7.2 vector 是动态数组容器
7.3 for(auto d : vdatas) 会复制元素
7.4 for(auto& d : vdatas) 可以修改原元素
7.5 只读遍历推荐 const auto&
7.6 vector 传值会复制但是return vector和外面相同
7.8 move 可以减少 vector 内部数据复制
7.9 引用传参不会复制并不代表返回也不复制
前言
上一节我们学习了函数与普通数组、数组引用,知道了一个非常重要的结论:
普通数组作为函数参数时,会退化为指针。
所以普通数组传入函数后,函数内部无法直接通过sizeof得到数组真实大小,一般需要额外传入size。
但是在 C++ 中,除了传统数组以外,更常用的是:
vectorvector可以简单理解为 C++ 提供的动态数组容器。它可以自动管理内存,支持动态扩容,也可以通过.size()获取元素个数。
本节就继续围绕函数展开,重点讲解:
- vector 作为函数参数时会不会复制?
- vector 作为返回值时能不能返回?
- vector 引用传参有什么好处?
- for(auto d : vdatas) 和 for(auto& d : vdatas) 有什么区别?
- move(vdatas) 到底做了什么?
- 引用传参和 move 有什么区别?
一、本节学习内容概要
1.1 本节主要学习什么?
本节主要围绕vector和函数之间的关系展开。
主要内容包括:
- vector 的基本使用
- vector 的内存结构
- vector 作为函数参数
- vector 作为函数返回值
- vector 引用传参
- vector 和 move 移动语义
在 C++ 中,如果想使用vector,需要引入头文件:
#include <vector>定义一个vector<int>数组:
vector<int> vdatas{ 11,22,33,4,5 };这里的vdatas可以理解为一个动态数组对象,里面保存了 5 个int类型的数据。
1.2 vector 和普通数组有什么区别?
普通数组定义后,大小一般固定。
例如:
int datas[5] = { 11,22,33,4,5 };这个数组大小是固定的,不能像vector那样方便地动态增加元素。
而vector可以这样写:
vector<int> vdatas{ 11,22,33,4,5 };也可以继续添加元素:
vdatas.push_back(100); vdatas.push_back(200);并且可以通过:
vdatas.size()直接获取元素个数。
这比普通数组方便很多。
1.3 vector 的基本内存理解
定义:
vector<int> vdatas{ 11,22,33,4,5 };可以简单理解为:
- vdatas 是一个 vector 对象;
- 这个对象本身一般定义在栈区;
- 对象内部保存了指向堆区数据的指针;
- 真正的 11、22、33、4、5 一般存放在堆区连续空间中。
也就是说,vector对象本身并不直接把所有元素都塞在对象里面。
它内部更像保存了几个关键信息:
- 指向数据起始位置的指针
- 当前元素个数
- 当前容量大小
可以通过:
vdatas.data()查看vector内部元素存储空间的首地址。
例如:
vector<int> vdatas{ 11,22,33,4,5 }; cout << "main :" << vdatas.data() << endl;这里打印的是vector内部数组空间的首地址,也就是第一个元素所在的地址。
二、vector 的遍历:auto、auto& 和 const auto&
2.1 使用范围 for 遍历 vector
遍历vector最常见的方式是范围 for:
vector<int> vdatas{ 11,22,33,4,5 }; for (auto d : vdatas) { cout << d << " "; }输出结果:
11 22 33 4 5这里:
auto d表示每次从vdatas中取出一个元素,然后复制一份给d。
因为这里元素类型是int,所以:
auto d实际就是:
int d2.2 auto d:会复制元素
代码示例:
for (auto d : vdatas) { d = 100; }这段代码不会修改vdatas中原来的元素。
原因是:
d 是 vdatas 中元素的副本。
也就是说:
- 每次遍历时,只是把
vdatas中的元素复制一份给d。- 修改
d,只是在修改副本,不会影响原来的vector。
例如:
vector<int> vdatas{ 11,22,33,4,5 }; for (auto d : vdatas) { d = 100; } for (auto d : vdatas) { cout << d << " "; }输出仍然是:
11 22 33 4 52.3 auto& d:引用原来的元素,可以修改 vector
如果想修改vector中原来的元素,需要使用引用:
for (auto& d : vdatas) { d = 100; }这里:
auto& d表示d是vdatas中元素的引用。
修改d,就是修改原数组中的元素。
例如:
vector<int> vdatas{ 11,22,33,4,5 }; for (auto& d : vdatas) { d = 100; } for (auto d : vdatas) { cout << d << " "; }输出结果:
100 100 100 100 100所以:
- auto d 是复制元素;
- auto& d 是引用元素。
2.4 const auto& d:只读访问,避免复制
如果只是想读取元素,不想修改元素,可以写成:
for (const auto& d : vdatas) { cout << d << " "; }这里:
const auto& d表示只读引用。
它有两个好处:
- 不会复制元素;
- 不能修改元素。
对于int这种小类型来说,复制成本很低,auto d和const auto& d性能差别不大。
因为:
int 通常是 4 字节; 指针在 64 位程序中通常是 8 字节。但是如果vector里面存的是大对象,例如:
vector<string> vector<MyClass>这时使用:
const auto& d就可以避免每次遍历都复制一个大对象。
所以常见习惯是:
- 只读遍历大对象:const auto&
- 需要修改元素:auto&
- 小类型只读:auto 或 const auto& 都可以
三、vector 作为函数参数:传值会复制
3.1 vector 作为参数传值
先看一个函数:
vector<int> TestVector(vector<int> vdatas) { cout << "===begin TestVector===" << endl; cout << "vdatas :" << vdatas.data() << endl; cout << vdatas.size() << endl; for (auto& d : vdatas) cout << d << " "; cout << endl; cout << "===end TestVector===" << endl; return vdatas; }函数参数是:
vector<int> vdatas这是传值。
也就是说,调用函数时,会把外面的vector复制一份给函数内部的vdatas。
3.2 传值时,函数内部是新的 vector
调用代码:
vector<int> vdatas{ 11,22,33,4,5 }; cout << "main :" << vdatas.data() << endl; auto rdatas = TestVector(vdatas); cout << "rdatas :" << rdatas.data() << endl;可能输出类似:
main :00000196F0525150 ===begin TestVector=== vdatas :00000196F05251F0 5 11 22 33 4 5 ===end TestVector=== rdatas :00000196F05251F0注意看:
main : 00000196F0525150 vdatas : 00000196F05251F0这两个地址不同。
说明:
main 中的 vdatas 和函数中的 vdatas 不是同一个对象内部的数据空间。
也就是说:
vector<int> TestVector(vector<int> vdatas)这种写法会复制一份vector数据。
3.3 vector 传值复制的是什么?
这里要分清楚两件事:
vector 对象本身 vector 内部管理的堆区数组当写:
TestVector(vdatas);因为参数是传值:
vector<int> vdatas所以:
- 会创建一个新的
vector对象。- 这个新的
vector对象内部,也会有自己的堆区数组空间。- 因此原来的
vdatas.data()和函数内部的vdatas.data()地址不同。
可以理解为:
main 中有一个 vector; 函数参数中又复制出来一个 vector; 两个 vector 各自管理自己的堆区数组。所以普通传值会有复制开销
对于只有 5 个int的小数组来说,这个开销不大。
但如果vector中有很多数据,比如几十万、几百万个元素,那么复制成本就会明显增加。
3.4 return vdatas 为什么可以?
函数中写:
return vdatas;这里返回的是函数内部的局部vector对象。
前面学习普通数组时,我们说过:
不能返回函数内部局部数组的地址。
例如:
int* Test() { int arr[10]; return arr; // 错误 }但是vector不一样。
下面这种写法是可以的:
vector<int> TestVector(vector<int> vdatas) { return vdatas; }原因是:
这里返回的是 vector 对象本身,不是返回局部对象的地址。
函数返回时,C++ 会把这个vector对象作为返回值交给外面。
现代 C++ 编译器通常会进行优化,例如:
返回值优化 移动构造所以返回vector并不一定会真的完整复制一份所有元素。
3.5 为什么返回后的地址可能和函数内部一样?
在运行结果中可能看到:
vdatas :00000196F05251F0 rdatas :00000196F05251F0也就是说:
函数内部 vdatas.data() 和 外面 rdatas.data() 打印出来的地址一样。
这是因为返回时可能发生了:
移动构造
也可以理解为:
函数内部 vdatas 管理的堆区数组,被移动给了外面的 rdatas。
这时不会重新复制 5 个元素,而是把内部数组资源转交给返回值对象。
所以:
return vdatas;
- 虽然返回的是局部对象,但这是安全的。
- 因为返回的不是局部对象地址,而是把对象的资源交给了外部对象。
四、move 移动语义:减少 vector 数据复制
4.1 std::move 的基本使用
如果想避免第一次传参时复制vector,可以使用:
move代码示例:
vector<int> vdatas{ 11,22,33,4,5 }; auto mdatas = TestVector(move(vdatas));这里:
move(vdatas)表示把vdatas转成右值,让函数参数可以通过移动构造接收它的资源。
注意,使用move需要引入:
#include <utility>不过很多时候其他头文件可能间接包含了它,但规范写法建议加上:
4.2 move 本身并不搬数据
很多初学者会误以为:
move(vdatas)这一句本身就把数据搬走了。
其实不是。
std::move本身主要做的是:
强制类型转换。它把一个左值转换成右值引用,让后续代码可以调用移动构造或者移动赋值。
真正发生资源转移的是:
vector 的移动构造函数例如:
auto mdatas = TestVector(move(vdatas));进入函数时,参数:
vector<int> vdatas会通过移动构造创建。
此时原来vdatas内部管理的堆区数组资源,可能会被直接转移给函数参数。
这样就不用复制所有元素了。
4.3 使用 move 后,原来的 vector 会怎么样?
看代码:
vector<int> vdatas{ 11,22,33,4,5 }; auto mdatas = TestVector(move(vdatas)); cout << "vdatas :" << vdatas.size() << endl;很多编译器中可能输出:
vdatas :0这说明原来的vdatas数据资源已经被移动走了。
但是要注意,更严谨地说:
被 move 之后的 vector 仍然是一个有效对象, 但是它里面的内容处于未指定状态。也就是说,移动后的vdatas仍然可以:
析构; 重新赋值; 调用 size(); 调用 clear(); 继续 push_back()。但是不要再依赖它原来的内容。
常见编译器下,vdatas.size()很可能是 0。
4.4 move 前后的地址变化
代码示例:
vector<int> vdatas{ 11,22,33,4,5 }; cout << "main :" << vdatas.data() << endl; auto mdatas = TestVector(move(vdatas)); cout << "mdatas :" << mdatas.data() << endl; cout << "vdatas :" << vdatas.size() << endl;可能输出:
main :00000196F0525150 ===begin TestVector=== vdatas :00000196F0525150 5 11 22 33 4 5 ===end TestVector=== mdatas :00000196F0525150 vdatas :0这里可以看到:
main 中原始 vdatas.data() 函数内部 vdatas.data() 返回后的 mdatas.data()可能是同一个地址。
这说明:
堆区数组资源从原 vdatas 移动到了函数参数; 函数参数返回时,又移动到了 mdatas。整个过程中,元素数据本身没有被重新复制。
4.5 move 的优点和代价
move的优点:
减少大对象复制; 避免大量元素重新分配和拷贝; 适合资源转移场景。例如一个很大的vector:
vector<int> bigData(1000000);如果传值复制,可能要复制 100 万个int。
如果使用移动语义,就可以只转移内部资源,效率更高。
但是move也有代价:
原来的对象资源被转移后,不应该继续依赖原来的数据。例如:
auto mdatas = TestVector(move(vdatas));这之后,不建议继续使用vdatas原来的元素内容。
可以重新给它赋值:
vdatas = { 1,2,3 };或者重新添加元素:
vdatas.push_back(100);这样是可以的。
五、vector 引用传参与返回值
5.1 vector 引用传参不会复制
如果不想复制vector,最常见的方法是引用传参。
例如:
vector<int> TestVectorRef(vector<int>& vdatas) { cout << "===begin TestVectorRef===" << endl; cout << "vdatas :" << vdatas.data() << endl; cout << vdatas.size() << endl; for (auto& d : vdatas) cout << d << " "; cout << endl; cout << "===end TestVectorRef===" << endl; return vdatas; }参数是:
vector<int>& vdatas这表示:
vdatas 是外部 vector 的引用。函数内部并没有创建新的vector数据副本。
5.2 引用传参时,函数内部地址不变
调用代码:
vector<int> vdatas{ 11,22,33,4,5 }; cout << "main :" << vdatas.data() << endl; auto mdatas = TestVectorRef(vdatas); cout << "mdatas :" << mdatas.data() << endl; cout << "vdatas :" << vdatas.size() << endl;可能输出:
------------ref------------ main :000002CD7B035A60 ===begin TestVectorRef=== vdatas :000002CD7B035A60 5 11 22 33 4 5 ===end TestVectorRef=== mdatas :000002CD7B035B00 vdatas :5注意:
main :000002CD7B035A60 vdatas :000002CD7B035A60这两个地址一样。
说明引用传参时,函数内部操作的就是外面的那个vector。
但是:
mdatas :000002CD7B035B00这个地址变了。
- 原因是函数返回值类型是:vector<int>
- 而不是:vector<int>&
所以:
return vdatas;虽然vdatas是引用参数,但是返回时返回的是一个新的vector对象。
也就是说:
引用传参不复制; 但是按值返回会复制或移动生成新的返回对象。5.3 如果只读,推荐 const vector<int>&
如果函数内部只是读取vector,不修改它,推荐写成:
void PrintVector(const vector<int>& vdatas) { for (const auto& d : vdatas) { cout << d << " "; } cout << endl; }这里:
const vector<int>& vdatas有两个好处:
不会复制 vector; 函数内部不能修改原 vector。这在实际工程中非常常见。
例如:
void PrintVector(const vector<int>& vdatas);表示这个函数只是查看数据,不会修改数据。
5.4 如果想返回引用,返回值也要写引用
如果确实想返回原来的vector,可以写成:
vector<int>& TestVectorRefReturn(vector<int>& vdatas) { return vdatas; }调用:
vector<int> vdatas{ 11,22,33,4,5 }; auto& r = TestVectorRefReturn(vdatas); cout << r.data() << endl;这里:
auto& r也要写引用。
如果写成:
auto r = TestVectorRefReturn(vdatas);那么还是会复制一份。
所以要记住:
函数返回引用,接收时也要用引用,才能继续保持引用关系。
但是返回引用也要注意生命周期。
可以返回:
- 外部传进来的 vector 引用;
- 全局 vector 引用;
- static vector 引用。
不能返回:
函数内部普通局部 vector 的引用。错误示例:
vector<int>& Test() { vector<int> temp{ 1,2,3 }; return temp; // 错误,temp 离开函数后会销毁 }这个问题和前面讲的“不能返回栈区变量的引用”本质是一样的。
六、完整代码示例
#include <iostream> #include <vector> #include <utility> using namespace std; // 函数与 vector 数组和引用 vector<int> TestVector(vector<int> vdatas) { cout << "===begin TestVector===" << endl; cout << "vdatas :" << vdatas.data() << endl; cout << vdatas.size() << endl; for (auto& d : vdatas) cout << d << " "; cout << endl; cout << "===end TestVector===" << endl; return vdatas; // 返回 vector 对象,现代 C++ 中通常会移动或优化 } vector<int> TestVectorRef(vector<int>& vdatas) { cout << "===begin TestVectorRef===" << endl; cout << "vdatas :" << vdatas.data() << endl; cout << vdatas.size() << endl; for (auto& d : vdatas) cout << d << " "; cout << endl; cout << "===end TestVectorRef===" << endl; return vdatas; // 返回值是 vector<int>,所以会产生新的返回对象 } void PrintVector(const vector<int>& vdatas) { for (const auto& d : vdatas) { cout << d << " "; } cout << endl; } int main() { vector<int> vdatas{ 11,22,33,4,5 }; cout << "main :" << vdatas.data() << endl; auto rdatas = TestVector(vdatas); cout << "rdatas :" << rdatas.data() << endl; auto mdatas = TestVector(move(vdatas)); cout << "mdatas :" << mdatas.data() << endl; cout << "vdatas size :" << vdatas.size() << endl; { cout << "------------ref------------" << endl; vector<int> vdatas{ 11,22,33,4,5 }; cout << "main :" << vdatas.data() << endl; auto rdatas = TestVectorRef(vdatas); cout << "rdatas :" << rdatas.data() << endl; cout << "vdatas size :" << vdatas.size() << endl; } return 0; }七、总结
本节主要学习了函数与vector、引用、移动语义之间的关系。
| 情况 | 调用方式 | 函数参数 | 函数返回值 | main 和函数内部地址 | return 后地址 |
|---|---|---|---|---|---|
| 普通传值 | TestVector(vdatas) | vector<int> vdatas | vector<int> | 不同 | 通常和函数内部相同 |
| move 传值 | TestVector(move(vdatas)) | vector<int> vdatas | vector<int> | 通常相同 | 通常和函数内部相同 |
引用传参, 按值返回 | TestVectorRef(vdatas) | vector<int>& vdatas | vector<int> | 相同 | 通常不同 |
引用传参, 返回引用 | TestVectorRefReturn(vdatas) | vector<int>& vdatas | vector<int>& | 相同 | 相同 |
- 普通传值:进函数会复制,所以 main 和函数内部地址不同;
- move 传值:资源被移动进去,所以 main 原地址、函数内部地址、返回地址通常相同;
- 引用传参:进函数不复制,所以 main 和函数内部地址相同;
- 引用传参但按值返回:return 会生成新对象,所以返回地址不同;
- 引用传参并返回引用:从头到尾都是同一个对象,所以三个地址都相同。
- 看参数有没有 &,决定进函数时是否复制;
- 看返回值有没有 &,决定 return 出来后是否还是同一个对象。
7.1 使用 vector 需要引入头文件
#include <vector>如果使用move,建议引入:
#include <utility>7.2 vector 是动态数组容器
定义:
vector<int> vdatas{ 11,22,33,4,5 };可以简单理解为:
- vdatas 对象本身通常在栈区;
- 真正的元素数据通常在堆区;
- vdatas 内部保存了指向堆区数据的指针。
可以通过:
vdatas.data()查看内部数组空间首地址。
7.3 for(auto d : vdatas) 会复制元素
for (auto d : vdatas)这里d是元素副本。
修改d不会影响原来的vector。
7.4 for(auto& d : vdatas) 可以修改原元素
for (auto& d : vdatas)这里d是元素引用。
修改d就是修改vdatas中的元素。
7.5 只读遍历推荐 const auto&
for (const auto& d : vdatas)这种写法既不会复制元素,也不会修改元素。
对于大对象更推荐这样写。
7.6 vector 传值会复制但是return vector和外面相同
vector<int> TestVector(vector<int> vdatas)这种写法会把外面的vector复制一份到函数内部。
所以函数内部:
vdatas.data()和外面的:
main 中 vdatas.data()通常不同。
return vdatas;虽然vdatas是函数内部变量,但是返回的是对象本身,不是返回局部变量地址。
现代 C++ 中通常会通过返回值优化或移动语义减少复制。
7.8 move 可以减少 vector 内部数据复制
auto mdatas = TestVector(move(vdatas));move本身只是类型转换,真正转移资源的是vector的移动构造。
移动后,原来的vdatas仍然是有效对象,但是内容不要再依赖。
7.9 引用传参不会复制并不代表返回也不复制
vector<int> TestVectorRef(vector<int>& vdatas)这里参数是引用,函数内部操作的就是外面的vector。
所以函数内部vdatas.data()和外部vdatas.data()一样。
如果函数返回值是:
vector<int>那么:
return vdatas;仍然会生成一个新的返回对象。
如果想返回引用,要写成:
vector<int>& Test(vector<int& vdatas)正确写法是:
vector<int>& Test(vector<int>& vdatas) { return vdatas; }并且接收时也要写:
auto& r = Test(vdatas);