💢欢迎来到张胤尘的技术站
💥技术如江河,汇聚众志成。代码似星辰,照亮行征程。开源精神长,传承永不忘。携手共前行,未来更辉煌💥
文章目录
- 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; // 10delete a;return 0;
}
安全和易用性
引用
- 引用更安全,因为它不能为
nullptr
,并且不能改变引用的目标。
int main()
{int *ptr = nullptr; // 合法// int &ref = nullptr; // 错误:引用不能为nullptrint 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, %rbpmovq %rdi, -8(%rbp) # 通过rdi寄存器传递,保存参数x的地址movq -8(%rbp), %rax # 将x的地址加载到%raxmovl (%rax), %eax # 通过地址加载x的值leal 1(%rax), %edx # x + 1,结果存储在%edxmovq -8(%rbp), %rax # 再次加载x的地址movl %edx, (%rax) # 将结果写回到x的地址noppopq %rbp # 恢复栈帧ret # 返回
main:endbr64 pushq %rbp # 建立main函数栈帧movq %rsp, %rbpsubq $16, %rspmovq %fs:40, %raxmovq %rax, -8(%rbp)xorl %eax, %eaxmovl $10, -12(%rbp) # 初始化变量a = 10leaq -12(%rbp), %rax # 获取a的地址movq %rax, %rdi # 将a的地址传递给incrementcall _Z9incrementRi # 调用increment函数movl -12(%rbp), %eax # 将a的值加载到返回寄存器eaxmovq -8(%rbp), %rdx # 检查栈subq %fs:40, %rdxje .L4call __stack_chk_fail@PLT
.L4:leaveret # 返回
以上汇编代码并不完整,只保留核心代码逻辑。
从以上的代码中可以看出,引用在底层是通过指针实现的。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;
}
🌺🌺🌺撒花!
如果本文对你有帮助,就点关注或者留个👍
如果您有任何技术问题或者需要更多其他的内容,请随时向我提问。