C++技能系列 ( 5 ) - 详解函数入参/返回参使用(值传递/引用传递/指针传递/智能指针传递)

news2024/11/25 10:30:21

系列文章目录

C++技能系列
Linux通信架构系列
C++高性能优化编程系列
深入理解软件架构设计系列
高级C++并发线程编程

期待动动小手,点击关注哦!!!

在这里插入图片描述

当你休息的时候,一定要想到别人还在奔跑。
When you rest, we must think about other people are still in the running.

详解函数入参/返回参使用(值传递/引用传递/指针传递/智能指针传递)

  • 系列文章目录
  • 一、值传递 - 【应用场景|实例分析】
  • 二、 指针传递 - 【应用场景|实例分析】
  • 三、引用传递 - 【应用场景|实例分析】
  • 四、std::thread 传递引用参数 - 【实例分析】
  • 五、智能指针传参 - 【实例分析】
  • 总结

什么场景下使用怎样的传递方式?比如函数的返回值、值传递、引用传递、指针传递、智能指针传递等这些如果搞不明白在实际项目开发中会遇到很多头疼的事。

一、值传递 - 【应用场景|实例分析】

1、值传递 - 应用场景

!!! 适用于传递简单的数据类型,如int、float、double等。传值是将参数的值传递给函数,函数内部会创建一个新的变量来存储该值,对该变量的修改不会影响原变量的值。

⚠️ !!! 虽然使用值传递是万能的, 但是传递当数据量大的对象时,会有拷贝的消耗,是不可取的。

2、值传递 - 说明

将实参的值(a、b)复制到形参(m、n)相应的存储单元中,即形参和实参分别占用不同的存储单元。

值传递的特点是单向传递,即主调函数被调用时给形参分配存储单元,把实参的值传递给形参,在调用结束后,形参的存储单元被释放,而形参值的任何变化都不会影响到实参的值,实参的存储单元仍保留并维持数值不变。

3、值传递 - 实例分析

#include <iostream>
//值传递
void TestValueTransmit(int x) {
    x += 1;
    std::cout << "x value:" << x << std::endl;
    std::cout << "x address:" << &x << std::endl;
    std::cout << "TestValueTransmit Func Done." << std::endl;
}
void Test() {
    int a = 2;
    TestValueTransmit(a);
    std::cout << "a value:" << a << std::endl;
    std::cout << "a address:" << &a << std::endl;
    std::cout << "Test Func Done. " << std::endl;
}

int main()
{
    Test(); //测试值传递
    return 0;
}

打印输出:

x value:3
x address:0x7ffee119f7fc
TestValueTransmit Func Done.
a value:2
a address:0x7ffee119f82c
Test Func Done. 

根据代码和结果我们可以知道的是值传传递的参数是有自己的内存的,并且当实参a把自己的值传递进去之后,对行参x是没有影响的,那么值传递则是等于把实参a的值赋给了行参x等于进行了一个赋值操作这就是值传递。

二、 指针传递 - 【应用场景|实例分析】

1、指针传递 - 应用场景

!!! 适用于传递数组、结构体等复杂的数据类型。指针传递是将参数的地址传递给函数,函数内部通过指针来访问该变量,对该变量的修改会影响原变量的值。

2、指针传递 - 说明

所谓的地址传递,指的就是函数的参数是数组名或者指针。传递的是数组的首地址或指针的值,而形参接收到的是实参的地址,即指向实参的存储单元,形参和实参占用相同的存储单元,所以形参和实参是相同的。形参并不存在存储空间,编译系统不为形参数组分配内存。因此在数组名或指针作函数参数时所进行的传送只是地址传送,形参在取得该地址之后,与实参共同拥有一段内存空间,形参的变化也就是实参的变化。

3、指针传递 - 实例分析

#include <iostream>
#include <thread>
class PersonModel
{
public:
    PersonModel() : age_(0){};
    ~PersonModel() = default;
    void SetName(std::string name){
        name_ = std::move(name);
    }
    std::string GetName(){
        return name_;
    }
    void SetAge(int age){
        age_ = age;
    }
    int GetAge() const{
        return age_;
    }

private:
    std::string name_;
    int age_;
};
void TestPointerTransmit(PersonModel *pmPtr) {

    pmPtr->SetName("AllenSu");
    pmPtr->SetAge(30);
    std::cout << "pmPtr name:" << pmPtr->GetName() << " age: " << pmPtr->GetAge() << std::endl;
    std::cout << "pmPtr address:" << pmPtr << std::endl;
    std::cout << "TestPointerTransmit Func Done." << std::endl;
}
void Test() {
    PersonModel pm;
    pm.SetName("Jack");
    pm.SetAge(21);
    
    //PersonModel *p = &pm;
    //TestPointerTransmit(p);
    
    TestPointerTransmit(&pm);
    std::cout << "pm name:" << pm.GetName() << " age: " << pm.GetAge() << std::endl;
    std::cout << "pm address:" << &pm << std::endl;
    std::cout << "Test Func Done." << std::endl;
}

int main()
{
    Test();

    return 0;
}

这是一个指针传递,可以明显的发现和值传递的差别,指针是存储地址的,当我们想要把pm的值传进TestPointerTransmit()函数时,我们传的是pm的地址,然后通过pm的地址,来获得pm的值,下面是结果:

pmPtr name:AllenSu age: 30
pmPtr address:0x7ffeef82f810
TestPointerTransmit Func Done.
pm name:AllenSu age: 30
pm address:0x7ffeef82f810
Test Func Done.

我们输出的跟值传递不同的是什么呢,很明显的是这次输出的地址比值传递多一个地址,那这个多的地址和pm的地址一模一样,可以说明的是指针传递的是地址,然后还有不同的是pm的值也被改变了,这就指针传递和值传递的不同。

三、引用传递 - 【应用场景|实例分析】

1、引用传递 - 应用场景

!!! 适用于传递对象、类等复杂的数据类型。引用传递是将参数的引用传递给函数,函数内部通过引用来访问该变量,对该变量的修改也会影响原变量的值。引用传递与指针传递相似,但使用起来更加简洁明了。

⚠️ !!! 直接访问实参的内存地址,此方法减少内存的拷贝,但是不想被传递函数修改实参值,需使用const& 引用行参。

2、引用传递 - 说明

形参相当于是实参的“别名”,对形参的操作其实就是对实参的操作,在引用传递过程中,被调函数的形式参数虽然也作为局部变量在栈中开辟了内存空间,但是这时存放的是由主调函数放进来的实参变量的地址。
形参的地址是实参地址的映射,即拥有不同的储存空间但是里面存放的地址相同。
被调函数对形参的任何操作都被处理成间接寻址,即通过栈中存放的地址访问主调函数中的实参变量。正因为如此,被调函数对形参做的任何操作都影响了主调函数中的实参变量。

3、引用传递 - 实例分析

#include <iostream>
#include <thread>
class PersonModel
{
public:
    PersonModel() : age_(0){};
    ~PersonModel() = default;
    void SetName(std::string name){
        name_ = std::move(name);
    }
    std::string GetName(){
        return name_;
    }
    void SetAge(int age){
        age_ = age;
    }
    int GetAge() const{
        return age_;
    }

private:
    std::string name_;
    int age_;
};
void TestQuoteTransmit(PersonModel &personModel) {

    personModel.SetName("AllenSu");
    personModel.SetAge(30);
    std::cout << "personModel name:" << personModel.GetName() << " age: " << personModel.GetAge() << std::endl;
    std::cout << "personModel address:" << &personModel << std::endl;
    std::cout << "TestPointerTransmit Func Done." << std::endl;
}
void Test() {
    PersonModel pm;
    pm.SetName("Jack");
    pm.SetAge(21);

    TestQuoteTransmit(pm);
    std::cout << "pm name:" << pm.GetName() << " age: " << pm.GetAge() << std::endl;
    std::cout << "pm address:" << &pm << std::endl;
    std::cout << "Test Func Done." << std::endl;
}

int main()
{
    Test();

    return 0;
}

在代码上是不是发现和值传递差不多,只是参数声明哪里比值传递的多了一个&符号其他的和值传递一样,但是就是在参数声明哪里多一个&符号,它就不是值传递,它的传递方式和值传递的是完全不一样的,所以在写参数声明时,要注意不要在你需要的引用传递时漏了一个&符号,它们的不同之处在哪里呢,我们看输出结果就知道了,下面是结果:

personModel name:AllenSu age: 30
personModel address:0x7ffeeebea810
TestPointerTransmit Func Done.
pm name:AllenSu age: 30
pm address:0x7ffeeebea810
Test Func Done.

可以发现的是personModel的值和pm的值是一样的,上面我们说指针传递时,是输出了一个personModel所指向地址的值,它的值和pm的值是一样,那么引用是不是和指针一样传的地址呢,其实不是的因为引用传递其实是等于把pm作为TestQuoteTransmit()函数的全局变量,为什么这样说呢,是因为personModel的地址和pm相同,然后personModel所做的所有操作都等于pm做的,这personModel像是pm的什么呢,这是名字不同,其他一样,personModel其实就是pm的一个别名,所以TestQuoteTransmit()函数对personModel的所有操作,都等于对pm进行,而personModel只是pm的另外一个标识。
那么有人对引用传递还有疑惑对吧,&在参数处是引用在所有非参数声明处都是获取某个变量的地址。还有就是引用可不可以解地址对吧,其实是不可以的。
*(解址符)的操作数必须是指针,意思只能对指针进行解址,对其他的类型是不能解址的。

四、std::thread 传递引用参数 - 【实例分析】

写代码的时候,担心std::thread传递引用时,会出现局部变量先被释放的情况。写了个测试代码看下流程。

#include <iostream>
#include <thread>
class PersonModel
{
public:
    PersonModel() : age_(0)
    {
        std::cout << "PersonModel()" << std::endl;
    }
    ~PersonModel()
    {
        std::cout << "~PersonModel()" << std::endl;
    }
    void SetName(std::string name){
        name_ = std::move(name);
    }
    std::string GetName(){
        return name_;
    }
    void SetAge(int age){
        age_ = age;
    }
    int GetAge() const{
        return age_;
    }

private:
    std::string name_;
    int age_;
};
void TestQuoteTransmit(PersonModel &personModel) {
    std::this_thread::sleep_for(std::chrono::seconds(3));
    personModel.SetName("AllenSu");
    personModel.SetAge(30);
    std::cout << "personModel address:" << &personModel << std::endl;
    std::cout << "TestPointerTransmit Func Done." << std::endl;
}
void Test() {
    PersonModel pm;
    std::cout << "pm address:" << &pm << std::endl;
    pm.SetName("Jack");
    pm.SetAge(21);
    //std::thread t(TestQuoteTransmit, std::ref(pm))
    std::thread t([&]{
        std::cout << "thread pm address:" << &pm << std::endl;
        TestQuoteTransmit(pm);
    });
    t.detach();

    std::cout << "Test Func Done." << std::endl;
}

int main()
{
    Test();

    std::this_thread::sleep_for(std::chrono::seconds(30));
    return 0;
}

~PersonModel()已经析构了,但是还能访问,结果如下:

PersonModel()
pm address:0x7ffeed0e3800
Test Func Done.
~PersonModel()
thread pm address:0x7ffeed0e3800
personModel address:0x7ffeed0e3800
TestPointerTransmit Func Done.

std::thread调用时,实际是进行了两次拷贝的。所以线程中访问的personModel,已经不是pm了,是经过拷贝过的新对象。
因此可以不用太担心局部变量被释放的情况。

五、智能指针传参 - 【实例分析】

shared_ptr进行引用传递,引用计数不变,内存正常释放。

#include <iostream>
#include <memory>

using namespace std;

class Base
{
public:
    Base(int num = 0):num(num) { cout << "Base::Base()" << endl; }
    ~Base() { cout << "Base::~Base()" << endl; }
    int num;
};

void func1(shared_ptr<Base>& sp) {
    cout << "func1 num: " << sp->num << " count: " << sp.use_count() << endl;
    sp->num++;
}

int main()
{
    auto sp = make_shared<Base>(10);
    func1(sp);
    cout << "main num: " << sp->num << " count: " << sp.use_count() << endl;
}

shared_ptr 进行值传递(传入shared_ptr),引用计数加一,内存释放正常

#include <iostream>
#include <memory>

using namespace std;

class Base
{
public:
    Base(int num = 0):num(num) { cout << "Base::Base()" << endl; }
    ~Base() { cout << "Base::~Base()" << endl; }
    int num;
};

void func1(shared_ptr<Base>& sp) {
    cout << "func1 num: " << sp->num << " count: " << sp.use_count() << endl;
    sp->num++;
}

int main()
{
    auto sp = make_shared<Base>(10);
    func1(sp);
    
    cout << "main num: " << sp->num << " count: " << sp.use_count() << endl;
}

shared_ptr进行值传递(传入unique_ptr),需要使用move转移所有权

#include <memory>

using namespace std;

class Base
{
public:
    Base(int num = 0):num(num) { cout << "Base::Base()" << endl; }
    ~Base() { cout << "Base::~Base()" << endl; }
    int num;
};

void func1(shared_ptr<Base> sp) {
    cout << "func1 num: " << sp->num << " count: " << sp.use_count() << endl;
    sp->num++;
}

int main()
{
    auto sp = make_unique<Base>(10);
    func1(std::move(sp));
}

传递后unique_ptr指针无法使用,如需继续使用,可以从函数返回

#include <iostream>
#include <memory>

using namespace std;

class Base
{
public:
    Base(int num = 0):num(num) { cout << "Base::Base()" << endl; }
    ~Base() { cout << "Base::~Base()" << endl; }
    int num;
};

auto func1(shared_ptr<Base> sp) {
    cout << "func1 num: " << sp->num << " count: " << sp.use_count() << endl;
    sp->num++;
    return sp;
}

int main()
{
    auto up = make_unique<Base>(10);
    auto sp = func1(std::move(up));
    cout << "main num: " << sp->num << " count: " << sp.use_count() << endl;
}

在unique_ptr的情况下,如果将unique_ptr传递给按值传递,则将unique_ptr的所有权传递给函数。

如果您将unique_ptr的引用传递给函数if,您只是希望函数使用指针,但您不希望将其所有权传递给函数。

shared_ptr对引用计数机制进行操作,因此在调用复制函数时计数总是递增,而在对析构函数进行调用时递减。

通过引用传递shared_ptr可以避免对任何一个进行调用,因此可以通过引用传递它。虽然通过值适当地递增和减少计数,但是对于大多数情况,shared_ptr的复制构造函数并不是非常昂贵,但在某些情况下它可能很重要,因此使用两者中的任何一个取决于情况。

总结

> 值传递:形参开辟内存空间,与形参不同的地址,不能改变值。(变量名的访问)

> 指针传递:形参不开辟内存空间,与形参相同的地址,能改变值。(地址的访问)

> 引用传递:形参开辟内存空间,与形参相同的地址,能改变值。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/668037.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

数据库相关

1、主要考点思维导图 2、如何设计一个关系型数据库 存储管理&#xff1a;数据逻辑关系转为物理存储关系。 缓存机制&#xff1a;优化执行效率。 SQL解析&#xff1a;将Sql语句进行解析。 日志管理&#xff1a;记录操作。 权限划分&#xff1a;多用户管理。 容灾机制&…

2、瑞丽-伯纳德对流的拉格朗日拟序结构(FTLE场结果对比)

在上篇博客中&#xff0c;我简单比较了瑞丽伯纳德对流的FTLE场&#xff0c;但是因为粒子追踪采用的是欧拉方法&#xff0c;所以精度不是很高&#xff0c; 因此与文献中的结果还是有些差别。 下面放一张文献中的FTLE场&#xff0c;参数与上篇文章是一致的&#xff0c;Ra 1e8;Pr…

《面试1v1》SpringBean生命周期

&#x1f345; 作者简介&#xff1a;王哥&#xff0c;CSDN2022博客总榜Top100&#x1f3c6;、博客专家&#x1f4aa; &#x1f345; 技术交流&#xff1a;定期更新Java硬核干货&#xff0c;不定期送书活动 &#x1f345; 王哥多年工作总结&#xff1a;Java学习路线总结&#xf…

python:使用Scikit-image对遥感影像进行边缘检测(edges)

作者:CSDN @ _养乐多_ 本文将介绍使用Scikit-image库中用于边缘检测特征提取的一些方法及其代码。方法包括 Canny边缘检测(canny),Sobel边缘检测(sobel),Scharr边缘检测(scharr), Roberts边缘检测(roberts),Prewitt边缘检测(prewitt),Farid边缘检测(farid),…

HJ48 从单向链表中删除指定值的节点

描述 输入一个单向链表和一个节点的值&#xff0c;从单向链表中删除等于该值的节点&#xff0c;删除后如果链表中无节点则返回空指针。 链表的值不能重复。 构造过程&#xff0c;例如输入一行数据为: 6 2 1 2 3 2 5 1 4 5 7 2 2 则第一个参数6表示输入总共6个节点&am…

Day24 实战篇 ——Jmeter通过JDBC测试实战

Day24 实战篇 ——Jmeter通过JDBC测试实战 文章目录 Day24 实战篇 ——Jmeter通过JDBC测试实战1、**业务级脚本开发**2、**接口级脚本开发**3、**JDBC脚本开发**4、**JMS Point-to-Poibt脚本开发**5、**Jmeter轻量级接口自动化测试框架**(了解就行)1、业务级脚本开发 登录脚…

《2023 年 React 生态》

大家好&#xff0c;我是 Chocolate。 前不久看到一篇不错的内容&#xff0c;来自于 The React Ecosystem in 2023&#xff0c;也结合自己今年使用的 React 生态总结一下。 本文并非视频演讲稿&#xff0c;和视频内容还是有一点点区别&#xff0c;视频内容相对来说会更加详细一…

ffmpeg(一) ffmpeg+QT开发环境搭建

1、开发库的选择 &#xff08;1&#xff09;音视频开发库 每个主流平台基本都有自己的音视频开发库&#xff08;API&#xff09;&#xff0c;用以处理音视频数据&#xff0c;比如&#xff1a; iOS&#xff1a;AVFoundation、AudioUnit 等 Android&#xff1a;MediaPlayer、Me…

Flink 学习九 Flink 程序分布式运行部署

Flink 学习九 Flink 程序分布式运行部署 1.Job 执行计划 层级说明备注StreamGraph用户代码生成的最初的图程序的运行流程图JobGraph将多个符合条件的节点多个符合条件的节点合并,减少序列化和反序列化ExecutionGraphJobGraph 的并行化调度层的核心数据结构PhysicalGraphJobMa…

【计算机组成原理】信息编码与数据表示

目录 一、进位计数制 二、信息编码 三、定点数的表示 四、校验码 五、浮点数的表示 一、进位计数制 整数部分&#xff1a; 二进制、八进制、十六进制 ---> 十进制&#xff1a;加权求和二进制 ---> 八进制&#xff1a;每三位分为一组&#xff0c;转为八进制…

CloFormer实战:使用CloFormer实现图像分类任务(一)

文章目录 摘要安装包安装efficientnet_pytorch安装timm安装 grad-cam 数据增强Cutout和MixupEMA项目结构计算mean和std生成数据集 摘要 论文翻译&#xff1a;https://blog.csdn.net/m0_47867638/article/details/131161083 官方源码&#xff1a;https://github.com/qhfan/CloF…

faceswap安装教程图文详解

Faceswap是一种人脸识别技术&#xff0c;可以将一个人的面部特征与另一个人的面部特征进行交换&#xff0c;从而创建出一个看起来像是两个人融合在一起的图像或视频。这项技术可以用于各种目的&#xff0c;包括艺术创作、电影制作、虚拟现实、安全监控等领域。Faceswap的实现方…

UE特效案例 —— 寒冰武器

一&#xff0c;环境配置 创建默认地形Landscape&#xff0c;如给地形上材质需确定比例&#xff1b;添加环境主光源DirectionalLight&#xff0c;设置相应的强度和颜色&#xff1b;PostProcessVolume设置曝光&#xff0c;设置Min/Max Brightness为1&#xff1b; 与关闭Game Sett…

怎样开始用selenium进行自动化测试?

如果您刚开始使用 Selenium 进行自动化测试&#xff0c;以下是建议的步骤。 1、安装 Selenium 首先&#xff0c;您需要安装 Selenium。Selenium 支持多种编程语言&#xff0c;如 Python、Java、C# 等。可以通过 pip 命令在 Python 中安装 Selenium&#xff1a; pip install …

CloFormer实战:使用CloFormer实现图像分类任务(二)

文章目录 训练部分导入项目使用的库设置随机因子设置全局参数图像预处理与增强读取数据设置Loss设置模型设置优化器和学习率调整算法设置混合精度&#xff0c;DP多卡&#xff0c;EMA定义训练和验证函数训练函数验证函数调用训练和验证方法 运行以及结果查看测试热力图可视化展示…

秀米编辑器(xiumi)+百度编辑器(Ueditor) 集成 :解决集成问题,秀米编辑器导出到百度编辑器格式问题,图片保存到自己的服务器(阿里云OSS)

1.集成前提条件&#xff1a; 1. 需要集成百度编辑器到环境中 2.https环境下才可以导出数据到百度编辑器&#xff0c;如果不是https环境&#xff0c;会出现错误 然后我们开始讲解如何集成&#xff1a; 2.引入资源&#xff1a; //百度编辑器需要修改的文件&#xff08;配置与原始…

测试入门第一步------编写接口测试用例

自动化始终只是辅助测试工作的一个手段&#xff0c;对于测试人员而言&#xff0c;测试基础和测试用例的设计才是核心。如果测试用例的覆盖率或者质量不高&#xff0c;那将这部分用例实现为自动化用例的意义也就不大了。 那么&#xff0c;接口测试用例应该怎么编写呢&#xff1…

Spring boot集成RabbitMq

Spring boot集成RabbitMq 一、搭建RabbitMq1.1 参考1.2 配置erlong的环境变量1.3 RabbitMQ对应的在注册表中的位置 二、使用教程2.1 打开服务端2.2 注意的问题2.3 Queue的包 三、git命令3.1 git remote3.2 git remote add origin "xxxx"3.3 git push -u origin maste…

使用esp32+micropython+microdot搭建web(http+websocket)服务器(超详细)第一部分

使用esp32micropythonmicrodot搭建web(httpwebsocket)服务器&#xff08;超详细&#xff09;第一部分 microdot文档速查 什么是Microdot?Microdot是一个可以在micropython中搭建物联网web服务器的框架micropyton文档api速查 Quick reference for the ESP32 先来个小demo先体…

数据仓库——原理+实战(一)

一、数据仓库概述 1. 数据仓库诞生原因 &#xff08;1&#xff09;历史数据积存&#xff08;存放在线上业务数据库中&#xff0c;当数据积压到一定程度会导致性能下降&#xff0c;所以需要将实用频率低的冷数据转移到数据仓库中&#xff09; &#xff08;2&#xff09;企业数…