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

2023-10-26

系列文章目录

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

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

在这里插入图片描述

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


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

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

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的复制构造函数并不是非常昂贵,但在某些情况下它可能很重要,因此使用两者中的任何一个取决于情况。

总结

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

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

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

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

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

随机推荐

  • MFC对话框添加菜单栏

    1 解决方案资源管理器 资源文件 rc双击 转至资源视图 2 资源视图 右击 rc 添加资源 Menu 新建 图形化界面编辑 3 图形化编辑完成后 切换到目标对话框的头文件 Dlg h 在类的成员定义处添加一个CMenu变量 比如 4 切换
  • uni-app中scroll-view不触发@scrolltolower事件(某些机型)

    uni app中scroll view不触发 scrolltolower事件 某些机型 背景 uni app中scroll view中用 scrolltolower的时候需要设置lower threshold的值 a 当设置为10的时候 某
  • C#调用C/C++DLL的两种方法

    C 调用C C DLL的两种方法 前言 方法1 常用的DLLImport方法 方法2 CLR封装法 前言 工作中 需要给客户提供公司产品的 NET动态库 但驱动工程师往往只提供C编译的库 这里就需要我做一些中间工作 方便客户使用我们的产品
  • IDEA中 yml文件不提示 和 怎么隐藏springboot 创建的文件隐藏不需要的文件

    1 隐藏你要隐藏的文件 输入文件的名称就可以了 2 duiyml文件不提示的问题 3 对比两个java文件改了什么
  • webpack4 之 splitChunks

    webpack4 之 splitChunks 简介 主要作用是提取公共代码 防止代码被重复打包 拆分过大的js文件 合并零散的js文件 SplitChunks 插件的作用就是通过配置让 Webpack 去帮你提取公共代码 chunks we
  • Js的基础

    js的语法结构 1 js是区分大小写的 var username 张三 var userName 李四 document write username document write userName 2 js的标识符和命名规则 标识符 用来
  • 【hello Linux】线程概念

    目录 1 线程概念的铺设 2 Linux线程概念 2 1 什么是线程 2 2 线程的优点 2 3 线程的缺点 2 4 线程异常 2 5 线程用途 3 Linux进程VS线程 4 Linux线程控制 4 1 POSIX线程库 4 2 创建线程
  • react-router-dom文档

    前言 本来体验下react router的 然后 去react router npm查看 发现了官方的提示如下 这个包为 React Router 提供了核心路由功能 但你可能不想直接安装它 如果您正在编写将在浏览器中运行的应用程序 您应该
  • 四个步骤,教你打造自媒体爆款标题。

    做自媒体的心里应该都会明白一篇文章或者是一个视频中标题的重要性 当然取一个好的标题也是有点难度的 但是不必担心 也会有一定的取标题的技巧 下面小编就跟大家讲讲一些取标题的建议 1 内容拟定好标题 在创作标题的时候还是要以文章的具体内容进行创
  • Android之登录注册——简易版

    今天 我要分享给大家的是Android中常见的一个的登录注册的案例 我这里写的是简易版 如果大家有更精彩的拓展 可以自行发挥哦 运行过程相信大家都已经心知肚明了 所以我在这里就直接发布代码了 其中有不理解的地方大家可以自行百度 也可以互相学
  • Unity 按钮点击生成物体

    1 生成button 2 新建脚本ButtonListener using System Collections using System Collections Generic using UnityEngine using UnityE
  • java运行错误排查汇总

    Jetty排错信息1 Failed startup of context o e j w WebAppContext java lang IllegalStateException Duplicate fragment name 原因及解决
  • Linux -- 构建KVM虚拟化环境

    硬件系统的配置 硬件和BIOS中虚拟化技术的支持是KVM运行的先决条件 在x86 64架构的处理器中 KVM必需的硬件虚拟化扩展分别为 Intel的虚拟化技术 Intel VT 和AMD的AMD V技术 一般在BIOS中 VT的选项通过 A
  • Github + Hexo 搭建个人博客

    文章目录 Github Hexo 搭建个人博客 快速搭建 安装Node js 添加国内镜像源 安装 Git 注册 Github 安装Hexo 连接Github与本地 写文章 发布文章 图片添加水印 修改样式 文章基本操作 发表文章 修改文章
  • matlab 三法求矩阵权重(算数平均法求权重,几何平均法求权重,特征值法求权重)

    三法求矩阵权重 例子为3 3的矩阵 可根据需要修改 算数平均法求权重 clc clear judge 1 6 9 7 8 9 6 1 9 7 8 7 7 9 1 w zeros 3 3 for i 1 3 for j 1 3 w i j j
  • C 结构体字节对齐规则

    参考 https blog csdn net xiaoxiangyuhai article details 79192781
  • mysql数据库加密密码如何解密_MySQL加密和解密实例详解

    MySQL加密和解密实例详解 数据加密 解密在安全领域非常重要 对程序员而言 在数据库中以密文方式存储用户密码对入侵者剽窃用户隐私意义重大 有多种前端加密算法可用于数据加密 解密 下面我向您推荐一种简单的数据库级别的数据加密 解密解决方案
  • merge与update区别---->你一定要看!

    merge与update区别 注 就因为这2个方法的区别还得我花了太多时间项目迟迟不能做完 但是让我解决了 学到了东西了 这是一段代码 public void updateData Object obj try tx this getSes
  • Mysql的七种join

    对于SQL的Join 在学习起来可能是比较乱的 我们知道 SQL的Join语法有很多inner的 有outer的 有left的 有时候 对于Select出来的结果集是什么样子有点不是很清楚 Coding Horror上有一篇文章 实在不清楚
  • C++技能系列 ( 5 ) - 详解函数入参/返回参使用(值传递/引用传递/指针传递/智能指针传递)

    系列文章目录 C 技能系列 Linux通信架构系列 C 高性能优化编程系列 深入理解软件架构设计系列 高级C 并发线程编程 期待动动小手 点击关注哦 当你休息的时候 一定要想到别人还在奔跑 When you rest we must thi