首页 > 编程语言 >C++ 参数传递浅析

C++ 参数传递浅析

时间:2024-03-21 10:03:14浏览次数:29  
标签:变量 int C++ 参数传递 引用 实参 nTestValue 浅析 指针

全文目录

概述

众所周知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. 通过引用传递参数的方式,形参的改变能影响实参的改变

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

标签:变量,int,C++,参数传递,引用,实参,nTestValue,浅析,指针
From: https://blog.csdn.net/weixin_41403580/article/details/136810011

相关文章

  • C++内置 new /delete 运算符浅析
    全文目录malloc()/free()原型解析简化版本用法举例new/delete静态/动态类型new/delete运算符原型常用但没有注意区分的例子使用new分配对象的生存期那new/delete都做什么事呢几个注意点写在最后malloc()/free()提到new/delete运算符就不得不说malloc()/f......
  • C++模板实现之谜:为何只能在头文件中?解密原因与高级分离技术
     概述:C++中模板必须在头文件中实现,因为编译器需要可见的实现以生成模板具体实例的代码。通过头文件,确保模板在每个编译单元中都能被正确展开,提高可维护性。在C++中,模板只能在头文件中实现的主要原因是编译器在使用模板时需要生成对应的代码,而这部分代码必须在编译时可见。以......
  • python中出现Microsoft Visual C++ 14.0 or greater is required
    我尝试下载了Microsoftvisualc++14.0,但是依然不管用,而且它是真的很大…… 直接安装相应依赖也不管用(可能其他人管用?)——condainstalllibpythonm2w64-toolchain-cmsys2链接:https://blog.csdn.net/qzzzxiaosheng/article/details/125119006 然后我有找到一个,看着描......
  • C++ 函数模板
    C++函数模板函数模板在C++中,函数模板是一种允许函数以一种类型无关的方式来操作的工具。它们使得函数能够处理不同类型的数据而不需要为每种类型编写重复的代码。函数模板的核心思想是“参数化类型”,这意味着在定义函数时,可以使用一个或多个通用类型参数,而在函数被调用时......
  • C++ 编程入门指南:深入了解 C++ 语言及其应用领域
    C++简介什么是C++?C++是一种跨平台的编程语言,可用于创建高性能应用程序。C++是由BjarneStroustrup开发的,作为C语言的扩展。C++为程序员提供了对系统资源和内存的高级控制。该语言在2011年、2014年、2017年和2020年进行了4次重大更新,分别为C++11、C++14、C+......
  • C++ 多重继承下的内存布局
    1.多重继承多重继承示例代码如下:classBase1{public:voidf0(){}virtualvoidf1(){}inta;};classBase2{public:virtualvoidf2(){}intb;};classDerived:publicBase1,publicBase2{public:voidd(){}voidf2(){}......
  • C++ <atomic>汇编语言实现原理
    C++<atomic>汇编语言实现原理问题我们先看一下这段代码:/**badcnt.c-Animproperlysynchronizedcounterprogram*//*$beginbadcnt*//*WARNING:Thiscodeisbuggy!*/#include"csapp.h"void*thread(void*vargp);/*Threadroutineprototype*//*......
  • C++ 模板入门详解
    目录0.模板引入1.函数模板 1.函数重载的缺点 2.函数模板的概念和格式2. 函数模板的实例化 2.1 隐式实例化:让编译器根据实参推演模板参数的实际类型 2.2 显式实例化:在函数名后的<>中指定模板参数的实际类型2.3函数模板参数的匹配规则 3.类模板 3.1类......
  • C++ 静态变量的初始化线程安全问题
    1.静态变量的初始化线程安全问题C++的局部static变量,是预先在静态存储区分配了内存,然后在第一次执行到这里的时候进行初始化。C++11规定了局部static变量的线程安全,实现上应该是类似std::call_once的实现,我估计基本上就是基于cas的spin-lock,这里当然可以根据编译器不同有不同......
  • 复试C++15真题_程序设计2_递归_输入字符串倒序转整形
    编写一个递归函数,功能为:输入一个字符串,输出一个整数值。例如输入 "1a2xcz34,5a!6" , 输出654321。一开始想不明白怎么写递归,于是我写了迭代的函数。意识到,递归的过程就是实现了迭代的循环,而循环内的操作本质没有太大差别。于是就写出来了:#include<iostream>usingnam......