void*解引用_指针和引用:从函数传参角度深度理解

本文深入探讨了C++中的三种参数传递方式:值传递、指针传递和引用传递,并通过汇编代码揭示了它们在底层的工作原理。此外,还讨论了引用和指针的区别及其在实际编程中的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

 一般面试会问到指针和引用的区别,以及C++中涉及到几种传参的形式,即:以值传递(pass by value)、指针传递(pass by pointer)、引用传递(pass by reference)。

可以简单理解为:
 1、值传递不可改变实参的值,需要进行实参与形参之间的拷贝复制。
 2、指针和引用传递只需要复制其值(也就是地址),指针需要通过用户自己实现取地址&value和函数内部的解引用*ptr来实现实参值所指向对象的内容的改变。
 3、引用只需要用户进行初始化,绑定一个初始值/对象,编译器来帮助我们实现取地址和解引用,从而实现实参对应地址下内容的改变。

从汇编的角度

 为了验证上面所说的第2点和第3点,我们可以从汇编的角度来看看,是否是编译器帮我做了对应的事。


下面分别是值传递、指针、引用传递的swap函数

> g++ -S TestMain.cpp 生成.s的汇编码

//TestMain.cpp
#include
#define POINTER 0
using namespace std;//pass by value
void swapValue(int a,int b)
{int temp = a;
a = b;
b = temp;
#if POINTER
cout << "&a = " << &a << "\t" << "&b = " << &b << endl;
#endif
}//pass by reference
void swapRef(int &a,int &b)
{int temp = a;
a = b;
b = temp;
#if POINTER
cout << "&a = " << &a << "\t" << "&b = " << &b << endl;
#endif
}//pass by pointer
void swapPointer(int *a,int *b)
{int temp = *a;
*a = *b;
*b = temp;
#if POINTER
cout << "&a = " << a << "\t" << "&b = " << b << endl;
#endif
}int main()
{int a = 1,b = 4;
#if POINTER
swapValue(a,b);
swapValue(3,4); //编译通过
a = 1,b = 4;
swapRef(a,b);//swapRef(3,4); //编译报错
a = 1, b = 4;
swapPointer(&a,&b);return 0;
}
对应的汇编码:
_Z7swapRefRiS_:
.LFB1573:
.cfi_startproc
endbr64
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movq %rdi, -24(%rbp)
movq %rsi, -32(%rbp)
movq -24(%rbp), %rax
movl (%rax), %eax
movl %eax, -4(%rbp)
movq -32(%rbp), %rax
movl (%rax), %edx
movq -24(%rbp), %rax
movl %edx, (%rax)
movq -32(%rbp), %rax
movl -4(%rbp), %edx
movl %edx, (%rax)
nop
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE1573:
.size _Z7swapRefRiS_, .-_Z7swapRefRiS_
.globl _Z11swapPointerPiS_
.type _Z11swapPointerPiS_, @function
_Z11swapPointerPiS_:
.LFB1574:
.cfi_startproc
endbr64
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movq %rdi, -24(%rbp)
movq %rsi, -32(%rbp)
movq -24(%rbp), %rax
movl (%rax), %eax
movl %eax, -4(%rbp)
movq -32(%rbp), %rax
movl (%rax), %edx
movq -24(%rbp), %rax
movl %edx, (%rax)
movq -32(%rbp), %rax
movl -4(%rbp), %edx
movl %edx, (%rax)
nop
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE1574:
.size _Z11swapPointerPiS_, .-_Z11swapPointerPiS_
.globl main
.type main, @function
汇编代码看不懂,大佬说是一样,我粗略地看了下,基本相同,感兴趣的可以研究下汇编码。

从函数入栈出栈的角度:

 函数调用时,栈增长。增长的空间需要能够容纳函数的实参、返回值、函数体内声明的局部变量。

 函数返回后,栈撤销。这意味着函数在执行期间所需要的局部变量占据的空间全部随着栈撤销而不复存在。下图解释了函数调用返回过程中栈的变化。

46567031a627877bf77e8aa82807d323.png

 这也就理解了为什么下面代码编写时会出现问题:

int &  returnTmpAddress()
{
int tmp = 100;//warning C4172: 返回局部变量或临时变量的地址: tmp
//此时栈撤销,地址被收回,无法再返回地址
return tmp;
}

但是下面的就没有问题:

int returnValue()
{
int tmp = 100;
return tmp;
}
从编译器的角度

 无论是指针还是引用,都通过取地址、解引用来实现实参内容的改变。


减轻函数调用的开销
namespace MyTest{
typedef struct _Node {
_Node() = default;
_Node(const int & val){ value = val; };
_Node(const _Node & xstring)/*=delete*/=default;
_Node & operator = (const _Node & xstring) =delete;
_Node & operator = (_Node && xstring) = delete;

int value;
}Node;

//void testFuncCall(Node & node)
//{}

void testFuncCall(Node node)
}int main()
{
using namespace MyTest;
Node node(10);
testFuncCall(node);

return 0;
}


如果将拷贝构造函数设为=deleteint main()
{
using namespace MyTest;
Node node(10);
testFuncCall(node);//error C2280: “MyTest::_Node::_Node(const MyTest::_Node &)”: 尝试引用已删除的函数

return 0;
}

 说明传值必须调用拷贝构造函数。两种解决方案,一种是设置=移动拷贝函数,一种是设置传引用或指针

typedef struct _Node {
_Node() = default;
_Node(const int & val){ value = val; };
_Node(const _Node & xstring)/*=default*/=delete;
_Node(_Node && xstring)=default;
_Node & operator = (const _Node & xstring) =delete;
_Node & operator = (_Node && xstring) = delete;

int value;
}Node;

 再来详细说下引用和指针的区别:

指针和引用

1、引用是变量/对象的别名,共享内存地址
2、引用必须初始化,且一旦初始化,就不能更改

 这里指的不能改改,就是一旦绑定指定的对象空间,之后操作引用都是操作指定内存空间的值。可以把引用当做 void * const。

int a = 10;
int b = 20;
int c = 30;

int ref = a;

ref = b;//此时a=20;
ref = c;//a =30,b =20

 可以把引用想象称为顶层const 【void * const】

3、引用只是c++语法糖,可以看作编译器自动完成取地址、解引用的void * const,其在汇编上实现一样。

4、由于引用只是指针包装了下,所以也存在风险,比如如下代码:

int *a = new int;
int &b = *a;
delete a;
b = 12; // 对已经释放的内存解引用

5、引用由编译器保证初始化,使用起来较为方便(如不用检查空指针等)   我们使用指针之前需要对其进行判空操作,避免指向未知地址,而对未知地址进行解引用。

typedef struct _Node{
int value;
}Node;
std::ostream & operator (std::ostream & out, const Node * node){
out<value;return out;
}
Node * node;std::cout<std::endl;//error 不能通过编译
Node * node = nullptr;std::cout<std::endl;//runtime error

5、没有 void & const, 但是由 void const &引用。

 因为引用本身就是一旦初始化,就不可以改嫁,但是她可以改变所嫁的这个家庭的环境。 常量引用就都不可以改变const void &。

6、指针既有const void *,又有void * const ,还有const void * const。

7、有指针的引用,但是没有引用的指针。

 有void * &,但是没有 void & *。

8、指针和引用的自增(++)和自减含义不同。

 指针是指针运算, 而引用是代表所指向的对象对象执行++或--

9、尽量用引用代替指针

 references when possible pointer when needed

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值