C++ 参数传递浅析

概述

众所周知C++参数传递有三种,分别问值传递、指针传递、引用传递。其实这只是三种表现形式罢了,归根结底其实本质上就只有一种,那就是实参初始化形参。我曾经一度以为传参实际上就是替换,彼时还是个没完全理解赋值和初始化真谛的萌新(虽然现在也没出新手村,哈哈)。

值传递(pass-by-value)

什么是变量?

在讨论值传递之前我们现在自问下什么是变量?有人说变量不是很简单么,比如说

 int a; 		// a 就是个int类型变量,只是a没有初始化罢了
 a = 1; 		// 此时 a 被赋值,已有初始值, 但这并不是初始化
 int b = 2;		// b被声明同时被初始化
 int c = b;		// 用 b 来初始化 c

那么变量是什么呢?变量就是内存单元的名字,这块内存单元存放的数据可以通过变量名来获取,称作变量的值。可以通过取地址(&)运算符来获取内存单元的地址,也就是变量的地址

int main()
{
    int a;
    a = 1;
    int b = 2;
    int c = b;

    std::cout << "&a: " << &a << std::endl 
    << "&b: " << &b << std::endl 
    << "&c: " << &c << std::endl;
}

/*
*    (以下是输出部分)
*       &a: 00AFF978
*       &b: 00AFF96C
*       &c: 00AFF960
*/

我们可以从输出的部分得知三个变量的地址是不同的,用一个变量去初始化另一个变量这两个变量的地址是不同的,将这个引申到函数参数值传递的情况。

值传递的例子

#include <iostream>

void FuncByValue(int nValue)
{
    nValue++;
    std::cout << "执行中 FuncByValue(int) 形参地址: " << &nValue << ", 形参自增之后的值:" << nValue << std::endl;
}

int main()
{
    int nTestValue = 1;
    std::cout << "调用前 FuncByValue(int) 实参地址: " << &nTestValue << ", 实参值:" << nTestValue << std::endl;
    FuncByValue(nTestValue);
    std::cout << "调用后 FuncByValue(int) 实参地址: " << &nTestValue << ", 实参值:" << nTestValue << std::endl;
}
/*
*    (以下是输出部分)
*     调用前 FuncByValue(int) 实参地址: 003BFA90, 实参值:1
*     执行中 FuncByValue(int) 形参地址: 003BF9A4, 形参自增之后的值:2
*     调用后 FuncByValue(int) 实参地址: 003BFA90, 实参值:1
*/

由上例可知,实参nTestValue在调用函数前后地址并没有变化,形参和实参的地址不相同,说明了形参是实参的一份拷贝,是一个独立的变量,两者包含了不同的内存单元。 实参初始化形参后,形参的值改变了,在函数调用结束时实参的值并没有发生相应的变化,即通过值传递参数的方式,形参的改变并不能影响实参的改变

指针传递(pass-by-pointer)

什么是指针?

我对指针的印象来源于我的大学C语言老师,当时在上指针那一章节的时候他在黑板上写了一句: 指针 = 地址 ,写完之后他把书一扔,然后说:“没了,我已经教完了”。当时听完直接愣住了(内心os 啊?),这个我预习的时候可不一样,指针可是很难的啊,怎么就一句话就没了。老师是幽默风趣的,也是渊博的,这点我从未怀疑,他平时上课经常性的上一半就开始讲他的故事,包括他的一些风流史(诸如去某地见女网友啥的),他是一个挺有意思的一个人,也正因为他这种教学风格也让我愿意学习,而不是换个地方玩手机。言归正传,指针 = 地址 这句确实无敌, 指针的全称是指针变量,只是简称指针罢了,指针变量跟整型变量等各种变量没有本质上的差别(都是变量肯定是本质上都一样,不然如何归类到变量!)。指针变量和常规变量不同的地方在于他们的类型和值的含义。我们来浅析一下:

int main()
{
    int nTestValue = 1;
    int* pValue = nullptr; 
    pValue = &nTestValue;
    
    std::cout << "nTestValue的地址: " << &nTestValue 
        << ",nTestValue的值: " << nTestValue << std::endl
        <<  "pValue的值: " << pValue 
        << ", pValue所指向的值: " << *pValue << std::endl
        << "pValue本身的地址值: " << &pValue << std::endl;

	delete pValue;
	pValue = nullptr;
}

/*
*    (以下是输出部分)
*       nTestValue的地址: 0077FA54, nTestValue的值: 1
*       pValue的值: 0077FA54, pValue所指向的值: 1
*       pValue本身的地址值: 0077FA48
*/

首先我们来套用“指针 = 地址” 这个概念性公式:
int* pValue = nullptr; 声明了一个整型指针变量 pValue
pValue = &nTestValue; 把整型变量nTestValue的地址赋值给 pValue

后面一句是不是就是概念“指针 = 地址”的完美诠释。这样就可以理解为指针里面包含了一个内存单元的地址值,这个内存单元本身是有数据存在的。被变量 nTestValue 命名的这块内存单元,pValue 指向了它,换句话说 pValue 不管你这块内存单元以前叫啥名字,在我这我只要知道了你的地址就行了,我(pValue)通过间接寻址可以获取/修改这一内存单元的数据,这也是指针的灵活性的体现。

对于整型变量nTestValue 来说,通过变量名 (nTestValue )可以访问该内存单元的数据 1,通过取地址符号(&nTestValue )可以获取变量 nTestValue 的地址;而对于指针变量 pValue 来说,通过间接寻址运算符(*pValue )来获取内存单元的数据1,即返回的是该地址的变量的值,通过变量名(pValue)来获取pValue所包含的地址,指针变量也是变量同样也可以用取地址符来获取pValue本身的地址,即&pValue。

指针传递的例子

#include <iostream>

void FuncByPoint(int* pValue)
{
    (*pValue)++;
    std::cout << "执行中 FuncByPoint(int*) 形参地址: " << pValue << ", 形参自增之后的值:" << *pValue;
    pValue++;
    std::cout << ", 指针自增之后的地址: " << pValue << std::endl;
}

int main()
{
    int nTestValue = 1;
    int* pValue = &nTestValue;

    std::cout << "调用前 FuncByPoint(int*) 实参地址: " << pValue << ", 实参值:" << *pValue << std::endl;
    FuncByPoint(pValue);
    std::cout << "调用后 FuncByPoint(int*) 实参地址: " << pValue << ", 实参值:" << *pValue << std::endl;
    
	delete pValue;
	pValue = nullptr;
}

/*
*                   (以下是输出部分)
*       调用前 FuncByPoint(int*) 实参地址: 006FF778, 实参值:1
*       执行中 FuncByPoint(int*) 形参地址: 006FF778, 形参自增之后的值:2, 指针自增之后的地址: 006FF77C
*       调用后 FuncByPoint(int*) 实参地址: 006FF778, 实参值:2
*/

由上例可知实参 pValue 的值在传入函数前后均未发生改变,形参和实参的值是一样的(此处指的是地址),指针传递的是一个地址值,相当于用一个指针去初始化另一个指针,形参和实参都指向同一个内存单元。形参的指针自增后,地址值发生了变化,但是不影响实参的地址,因为这是两个指针,所以说明指针传递本身也是一种值传递,只不过传递的不是寻常的值而是地址而已。通过间接寻址运算符来修改指针所指向的内存单元的数据,虽然这一步操作时发生在函数体内的(形参完成的),但是内存单元里面的数据确实是已经被修改了,所以在函数体结束之后修改依然有效,这就是为什么调用后实参的数据也被修改了的原因。因为实参pValue 从始至终都指向 nTestValue所命名的内存单元,只是这个内存单元内的值被函数形参的指针修改了。通过指针传递参数的方式,形参的改变并不会影响实参的改变(注意指针是地址),但改变形参指向的内存单元的数据时,实参所指的内存单元的数据也会发生改变。

引用传递(pass-by-reference)

什么是引用?

一句话概括 “引用 = 别名”。引用就是给一个变量取别名,C/C++ 可以通过typedef给表达式取一个类型别名,但两者还是有区别的,引用是个别名但同样也是个变量,且是一种类型;通过typedef定义的只是一个类型别名,此处并不是产生一个新的类型,大多数情况下是用来简写复杂的类型。

int main()
{
    int nTestValue = 1;
    int nOtherValue = 2;
 
    int& nRefValue = nTestValue;		
    nRefValue = nOtherValue;			
 }

上面例子运行没问题,是不是有点诧异?造成诧异的原因可能是因为理解错了!!!首先引用声明时必须要初始化,引用只会在这个时候发生绑定,且只发生一次绑定不可更改绑定,其他任何时候都只是给引用变量赋值罢了。执行完之后变量 nTestValue 的值也是 2,nRefValue = nOtherValue;相当于是 nTestValue = nOtherValue;

引用和指针的异同

  1. 引用声明时必须初始化(声明即绑定),指针可以不初始化(野指针,不安全操作)
  2. 引用绑定后不可修改,指针可以随意改变指向
  3. 引用不能为空,指针可以为空
  4. 引用的创建和销毁不会调用对象的构造和析构函数(引用的高效性)
  5. 引用底层是用指针来实现的

个人浅析(感兴趣的可以另行深入研究):
(1). 引用声明时为什么必须初始化?因为引用是给另一个变量取别名,如果在声明时不绑定被取名对象变量何来引用呢?引用概念使然。指针只要在使用前赋值就行了。
(2). 引用绑定后就不能修改了,这是为什么?个人理解指针是弱绑定关系,引用是强绑定关系,如果可以修改的话,那还要指针干什么。(这段解释有点牵强了,不知道有没有懂哥有更充分的依据,评论区告知!)
(3). 依旧回归引用的概念。
(4). 这个是引用最重要的特性。产生了新的对象时构造函数才会被调用,引用的创建并不会产生新的对象,所以就不会调用构造函数。同理引用的销毁也不会删除被引用的对象,也就不会调用析构函数
(5). 在汇编代码中,两者处理方式都是一样的, 引用可以理解为需要初始化的指针常量,仅通过结果逆向推导出来的,并未深究。
在这里插入图片描述

引用传递的例子

#include <iostream>

void FuncByReference(int& nValue)
{
    nValue++; 
    std::cout << "执行中 FuncByReference(int&) 形参地址: " << &nValue << ", 形参自增之后值:" << nValue << std::endl;
}

int main()
{
    int nTestValue = 1;
    int& nRefValue = nTestValue;

    std::cout << "调用前 FuncByReference(int&) 实参地址: " << &nRefValue << ", 实参值:" << nRefValue << std::endl;
    FuncByReference(nRefValue);
    std::cout << "调用后 FuncByReference(int&) 实参地址: " << &nRefValue << ", 实参值:" << nRefValue << std::endl;
}

/*
*                   (以下是输出部分)
*       调用前 FuncByReference(int&) 实参地址: 00AFF8AC, 实参值:1
*       执行中 FuncByReference(int&) 形参地址: 00AFF8AC, 形参自增之后值:2
*       调用后 FuncByReference(int&) 实参地址: 00AFF8AC, 实参值:2
*/

由上例可知实参 nRefValue 在传入函数前后地址均未发生改变,说明形参和实参共用同一块内存单元。在函数体内对形参nValue自增的操作相对于是对实参 nRefValue 自增,在函数运行结束之后,实参的值依然被改变了。即通过引用传递参数的方式,形参的改变能影响实参的改变

写在最后

小结

1. 通过值传递参数的方式,形参的改变并不能影响实参的改变
2. 通过指针传递参数的方式,形参的改变并不会影响实参的改变(注意指针是地址),但改变形参指向的内存单元的数据时,实参所指的内存单元的数据也会发生改变。
3. 通过引用传递参数的方式,形参的改变能影响实参的改变

因本人水平有限,错误之处在所难免,欢迎评论区交流指正。

  • 29
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
当我们将函数作为参数传递给另一个函数时,我们可以使用函数指针或函数对象(函数器)来实现。 1. 函数指针: 使用函数指针,我们首先需要定义一个与要传递的函数具有相同签名(参数和返回值类型)的函数指针类型。然后,我们可以将函数的名称赋给指针,并将该指针作为参数传递给另一个函数。 例如,假设我们有一个函数 `void foo(int)`,我们希望将其作为参数传递给另一个函数 `void bar(void (*func)(int))`,则可以这样做: ```cpp void foo(int x) { // 函数体 } void bar(void (*func)(int)) { // 调用函数指针所指向的函数 func(42); } int main() { // 将函数指针作为参数传递给另一个函数 bar(foo); return 0; } ``` 2. 函数对象(函数器): C++中的函数对象是可调用对象,可以像函数一样被调用。我们可以定义一个类,并在该类中实现 `operator()` 运算符重载。然后,我们可以创建该类的对象,并将该对象作为参数传递给另一个函数。 例如,假设我们有一个函数对象类 `Foo`,我们希望将其作为参数传递给另一个函数 `void bar(Foo)`,则可以这样做: ```cpp class Foo { public: void operator()(int x) { // 函数体 } }; void bar(Foo func) { // 调用函数对象 func(42); } int main() { // 将函数对象作为参数传递给另一个函数 Foo foo; bar(foo); return 0; } ``` 无论是使用函数指针还是函数对象,我们都可以将函数作为参数传递给其他函数,并在接受参数的函数中调用该函数。这样可以实现更灵活的代码设计和功能扩展。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一生要强的男人.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值