C/C++ | 每日一练 (5)
💢欢迎来到张胤尘的技术站
💥技术如江河,汇聚众志成。代码似星辰,照亮行征程。开源精神长,传承永不忘。携手共前行,未来更辉煌💥
文章目录
- C/C++ | 每日一练 (5)
- 题目
- 参考答案
- 引用
- 引用和指针的区别
- 语法
- 使用方式
- 安全和易用性
- 底层差异
- 注意事项
- 引用的生命周期
- 常量引用
- 引用和函数参数
C/C++ | 每日一练 (5)
题目
什么是引用?引用和指针的区别是什么?
参考答案
引用
在 c++
中,引用是为变量提供了一个别名,使得对引用的操作等同于对原始变量的操作。
需要注意的是,引用必须在声明时初始化,并且一旦初始化后,就与它所引用的变量绑定在一起,不能改变引用的目标。
给出引用的定义,如下所示:
int a = 10;
int& ref = a; // ref 是 a 的引用
此时,ref
和 a
是同一个变量的两个名字,对 ref
的操作等同于对 a
的操作。
注意:本文章中的 “引用” 只讨论作为左值引用,不讨论右值引用的概念和差异。
引用和指针的区别
引用和指针虽然都可以用来操作变量,但它们在具体的使用方式上确实有很大的不同。
下面从:语法、使用方式、安全和易用性这几个方面进行说明。
语法
引用
-
引用在其声明时必须初始化,且不能改变引用的目标。
-
另外引用的语法类似于变量的声明,但需要在类型后面添加
&
符号。 -
引用没有自己的内存地址,它和所引用的变量共享同一个内存地址。
指针
- 指针可以不初始化(
nullptr
),也可以随时改变指向的目标。 - 指针的声明需要在类型前加
*
,并且需要通过解引用操作符*
来访问指针所指向的内容。 - 指针有自己的内存地址,存储的是目标变量的地址。
使用方式
引用
引用常用于函数参数传递(避免拷贝)和返回值(返回对象的别名)。
void increment(int &x)
{
x++; // 直接操作引用
}
int main()
{
int a = 10;
increment(a); // 传递引用
return 0;
}
指针
指针常用于动态内存分配(如 new
和 delete
)、链表等数据结构。
#include <iostream>
int main(int argc, char const *argv[])
{
int *a = new int();
*a = 10;
std::cout << *a << std::endl; // 10
delete a;
return 0;
}
安全和易用性
引用
- 引用更安全,因为它不能为
nullptr
,并且不能改变引用的目标。
int main()
{
int *ptr = nullptr; // 合法
// int &ref = nullptr; // 错误:引用不能为nullptr
int a = 10;
int b = 20;
int &ref = a;
ref = b; // 注意:这句话的含义并不是将ref重新指向b,而是将b的值赋值给a,因为ref是a的引用
return 0;
}
- 引用的使用方式更直观,代码更简洁。
指针
- 指针更为灵活,但同时也更容易出错,例如野指针(指向无效内存的指针)、空指针解引用。
- 指针使用时需要注意更多的边界检查,例如检查是否为
nullptr
。
底层差异
为了从底层更为深入的了解引用和指针的差异,下面给出一段代码,将这段代码编译成会汇编,从汇编的角度观察两者之间的区别,如下所示:
void increment(int& x) {
x = x + 1;
}
int main() {
int a = 10;
increment(a);
return a;
}
汇编代码如下所示:
$ cat test.s
_Z9incrementRi:
pushq %rbp # 建立_Z9incrementRi函数栈帧
movq %rsp, %rbp
movq %rdi, -8(%rbp) # 通过rdi寄存器传递,保存参数x的地址
movq -8(%rbp), %rax # 将x的地址加载到%rax
movl (%rax), %eax # 通过地址加载x的值
leal 1(%rax), %edx # x + 1,结果存储在%edx
movq -8(%rbp), %rax # 再次加载x的地址
movl %edx, (%rax) # 将结果写回到x的地址
nop
popq %rbp # 恢复栈帧
ret # 返回
main:
endbr64
pushq %rbp # 建立main函数栈帧
movq %rsp, %rbp
subq $16, %rsp
movq %fs:40, %rax
movq %rax, -8(%rbp)
xorl %eax, %eax
movl $10, -12(%rbp) # 初始化变量a = 10
leaq -12(%rbp), %rax # 获取a的地址
movq %rax, %rdi # 将a的地址传递给increment
call _Z9incrementRi # 调用increment函数
movl -12(%rbp), %eax # 将a的值加载到返回寄存器eax
movq -8(%rbp), %rdx # 检查栈
subq %fs:40, %rdx
je .L4
call __stack_chk_fail@PLT
.L4:
leave
ret # 返回
以上汇编代码并不完整,只保留核心代码逻辑。
从以上的代码中可以看出,引用在底层是通过指针实现的。c++
中的引用本质上是一个“隐藏的指针”,它通过地址直接操作变量。
注意事项
从之前的分析可以看出,引用是一个非常强大且实用的特性,但是在平时使用过程中也有一些使用上的注意事项和限制。
引用的生命周期
引用的生命周期必须与其绑定的对象一致,否则可能导致未定义行为。
#include <iostream>
int& getRef() {
int a = 10;
return a; // warning: reference to local variable ‘a’ returned [-Wreturn-local-addr]
}
int main() {
int &b = getRef();
std::cout << b << std::endl; // Segmentation fault (core dumped)
return 0;
}
在上面的代码中,a
是局部变量,函数返回后 a
的生命周期结束,返回的引用 b
此时指向了一个已经销毁的对象,打印的 b
会导致未定义的行为,从而报错段错误。
常量引用
Best Praetices:如果函数无须改变引用形参的值,最好将其声明为常量引用,以提高代码的安全性和效率。
#include <iostream>
void printVal(const int &x)
{
std::cout << x << std::endl;
}
int main(int argc, char const *argv[])
{
printVal(10);
return 0;
}
使用 const
引用可以避免不必要的拷贝,同时又保证在函数内部不会修改传入的对象。
引用和函数参数
使用引用作为函数参数时,需要确保传递的参数是有效的对象,不能传递字面量或临时对象(除非是 const
引用或者是右值引用)。
本文章不对右值引用进行讨论。
#include <iostream>
void printVal(int &x)
{
std::cout << x << std::endl;
}
int main(int argc, char const *argv[])
{
// printVal(10); // error: cannot bind non-const lvalue reference of type ‘int&’ to an rvalue of type ‘int’
return 0;
}
🌺🌺🌺撒花!
如果本文对你有帮助,就点关注或者留个👍
如果您有任何技术问题或者需要更多其他的内容,请随时向我提问。