C++初学者指南第一步—12.引用
文章目录
- C++初学者指南第一步---12.引用
- 1. 功能(和限制)
- 1.1 非常量引用
- 1.2 常量引用
- 1.3 auto引用
- 2.用法
- 2.1 范围for循环中的引用
- 2.2 常量引用的函数形参
- 2.3 非常量引用的函数形参
- 2.4 函数参数的选择:copy / const& / & ?
- 2.5 避免输出型参数
- 3.绑定规则
- 3.1 右值和左值
- 3.2 引用绑定规则
- 4. 陷阱
- 4.1 永远不要返回函数内部的对象引用!
- 4.2 小心引用向量元素!
- 4.3 避免生命周期延长!
1. 功能(和限制)
1.1 非常量引用
int i = 2;
int& ri = i; // 引用 i
ri 和 i 都指向相同对象/内存位置。
cout << i <<'\n'; // 2
cout << ri <<'\n'; // 2
i = 5;
cout << i <<'\n'; // 5
cout << ri <<'\n'; // 5
ri = 88;
cout << i <<'\n'; // 88
cout << ri <<'\n'; // 88
运行上面代码
- 引用不能为“null”,即它们必须始终指向一个对象。
- 引用必须始终指向相同的内存位置。
- 引用类型必须与被引用对象的类型一致。
int i = 2;
int k = 3;
int& ri = i; // 引用 i
ri = k; // 将 k 的值赋给 i(ri 的目标)
int& r2; // 编译错误: 引用必须初始化
double& r3 = i; // 编译错误:类型必须相同
1.2 常量引用
= 对象的只读访问
int i = 2;
int const& cri = i; // i 的常量引用
- cri 和 i 都指向相同对象/内存位置。
- 但 const 表示 i 的值无法通过 cri 更改。
cout << i <<'\n'; // 2
cout << cri <<'\n'; // 2
i = 5;
cout << i <<'\n'; // 5
cout << cri <<'\n'; // 5
cri = 88; // 编译错误: 常量!
运行上面代码
1.3 auto引用
引用类型是从赋值符的右侧推导出来的。
int i = 2;
double d = 2.023;
double x = i + d;
auto & ri = i; // ri: int &
auto const& crx = x; // crx: double const&
运行上面代码
2.用法
2.1 范围for循环中的引用
std::vector<std::string> v;
v.resize(10);
// 修改vector中的元素:
for (std::string & s : v) { cin >> s; }
// 只读访问 vector 中的元素:
for (std::string const& s : v) { cout << s; }
// 修改:
for (auto & s : v) { cin >> s; }
// 只读访问:
for (auto const& s : v) { cout << s; }
2.2 常量引用的函数形参
只读访问 ⇒ const&
- 避免高开销的副本
- 向函数的用户清楚地传达只读意图
示例:计算中位数的函数
需要从向量中读取值!
错误:通过复制⇒值传递 | 正确:通过 const& ⇒ 没有副本 |
int median (vector); auto v = get_samples(“huge.dat”); auto m = median(v); // 运行时间和内存开销很大! | int median (vector const&); auto v = get_samples(“huge.dat”); auto m = median(v); // 不复制 ⇒ 没有开销! |
示例:混合传递(按引用 + 按值)
incl_first_last ({1,2,4},{6,7,8,9}) → {1,2,4,6,9}
incl_first_last ({1,2,4},{6,7,8,9}) → {1,2,4,6,9}
该实现是在第一个向量的本地副本 ‘x’ 上进行操作,并且通过常量引用 ‘y’ 从第二个向量中读取:
auto incl_first_last (std::vector<int> x, std::vector<int> const& y) {
if (y.empty() return x;
// append to local copy 'x'
x.push_back(y.front());
x.push_back(y.back());
return x;
}
2.3 非常量引用的函数形参
示例:交换两个变量值的函数
void swap (int& i, int& j) {
int temp = i; // copy i's value to temp
i = j; // copy j's value to i
j = temp; // copy temp's (i's original value) to j
}
int main () {
int a = 5;
int b = 3;
swap(a,b);
cout << a << '\n' // 3
<< b << '\n'; // 5
}
运行上面代码
注意:可以使用 std::swap 来交换对象的值(#include )。它可以像上面的功能一样使用,但是可以避免对于支持移动语义的对象(如std::vector)的大开销的临时复制(它的实现将在移动语义章节中解释)。
虽然在某些情况下非 const 引用可能很有用,但总体上你还是应该避免使用这种输出参数(查看下面的内容获取更多细节)。
2.4 函数参数的选择:copy / const& / & ?
void read_from (int); // 基本类型按值传递即可
void read_from (std::vector<int> const&);
void copy_sink (std::vector<int>);
void write_to (std::vector<int> &);
从可以廉价复制的对象读取(所有基本类型)⇒ 值传递
如:
double sqrt (double x) { … }
从内存占用量较大(> 64位)的对象中读取内存时 ⇒ 用const &传递
如:
void print (std::vector<std::string> const& v) {
for (auto const& s : v) { cout << s << ' '; }
}
在函数内部需要复制的内容 ⇒ 值传递
按值传递而不是在函数内显式复制。 其原因将在更高级的文章中解释。
如:
auto without_umlauts (std::string s) {
s.replace('ö', "oe"); // modify local copy
…
return s; // return by value!
}
写入到函数外部对象 ⇒ 由非常量&传递
(尽管它们在某些情况下可能很有用,但总的来说,你应该避免使用这种输出参数,请参阅下面内容。)
如:
void swap (int& x, int& y) { … }
2.5 避免输出型参数
像这样有非const引用参数的函数:
void foo (int, std::vector<int>&, double);
可能会在调用位置造成混乱/歧义:
foo(i, v, j);
- 哪个参数 (i, v, j) 改变了,哪个保持不变?
- 引用的对象是如何以及何时更改的,它是否被更改了?
- 引用参数只充当输出(函数只向它写入数据)还是同时充当输入(函数也从它读取数据)?
⇒ 一般来说很难调试和推理!
示例:一个只会造成混乱的接口
void bad_minimum (int x, int& y) {
if (x < y) y = x;
}
int a = 2;
int b = 3;
bad_minimum(a,b);
// 哪个变量再次保存了较小的值?
3.绑定规则
3.1 右值和左值
左值 = 我们可以获取内存地址的表达式
- 指向持久存在内存中的对象
- 一切有名称的东西(变量、函数参数……)
右值 = 我们无法获取内存地址的表达式
- 字面值(123,“string literal”,…)
- 临时的运行结果
- 从函数返回的临时对象
int a = 1; // a 和b 都是左值
int b = 2; // 1 和 2 都是右值
a = b;
b = a;
a = a * b; // (a * b)表达式的结果是右值
int c = a * b; // OK,右值可以赋值给左值
a * b = 3; // 编译错误:不能赋值给右值
std::vector<int> read_samples(int n) { … }
auto v = read_samples(1000);
3.2 引用绑定规则
& | 只能绑定到左值 |
const& | 可以绑定到const左值和右值 |
4. 陷阱
4.1 永远不要返回函数内部的对象引用!
只有在被引用对象的生命周期长于函数的情况下才有效!
4.2 小心引用向量元素!
警告:std::vector 中的元素引用可能在改变向量元素数量的任何操作之后失效!
vector<int> v {0,1,2,3};
int& i = v[2];
v.resize(20);
i = 5; // 未定义行为:原始内存可能已经释放。
悬空引用 = 指的是指向一个不再有效的内存位置的引用。
std::vector 存储元素的内部内存缓冲区在某些向量操作期间可以被替换为新的缓冲区,因此对旧缓冲区的任何引用可能会变得悬空。
4.3 避免生命周期延长!
引用可以延长临时对象(右值)的生命周期。
auto const& r = vector<int>{1,2,3,4};
⇒ 向量对象的生命周期被引用 r 延长了
从函数返回的对象呢?
std::vector<std::string> foo () { … }
以值传递(推荐)
vector<string> v1 = foo();
auto v2 = foo();
不推荐:忽略它→立即销毁
foo()
不推荐:获取对它的常量引用 ⇒ 临时对象的生命周期被延长
...只要这个引用还存在
vector<string> const& v3 = foo();
auto const& v4 = foo();
禁止:不要引用它的成员
返回对象的成员(这里指向量的内容)不能延长生命周期!
string const& s = foo()[0]; // 悬空引用!
cout << s; // 未定义行为
不要通过引用来延长生命周期!
- 容易造成混淆
- 容易写出错误
- 没有真正的好处
只需按值返回对象。 这对于现代C ++ 中的大多数函数和类型来说并不涉及大开销的复制,尤其是在C++17及更高版本中。
附上原文链接
如果文章对您有用,请随手点个赞,谢谢!^_^