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

系列文章目录

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

总结

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

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

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

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
### 回答1: memset()函数是C/C++语言中的一个库函数,可以用于将一段内存空间的内容设置为指定的值。 memset()函数的原型为: ```c void *memset(void *s, int c, size_t n); ``` 其中,s是指向要设置的内存的指针;c是要设置的值,通常是一个无符号字符或者零;n是要设置的内存空间的大小。 memset()函数的作用是将一段内存空间的每个字节都设置为相同的值。可以用于初始化内存,或者将内存中的内容清零。 使用memset()函数需要注意以下几点: 1. memset()函数只能用于字符数据类型或者无符号整型数据类型,即只能设置1字节大小的值。 2. 使用memset()函数时,需要知道要设置的内存空间的大小,以防止超出边界进行内存越界操作。 3. memset()函数是按字节进行设置的,所以对于非字符类型数据(如整数或浮点数),可能造成数据不符合预期。 示例代码: ```c #include <string.h> int main() { int arr[5]; memset(arr, 0, 5 * sizeof(int)); // 将arr内存空间设置为0 char str[20]; memset(str, 'A', 19); // 将str内存空间设置为'A' str[19] = '\0'; // 末尾添加'\0',形成一个字符串 return 0; } ``` 总之,memset()函数是一个能快速设置内存空间内容的函数,可以方便地进行内存初始化和内存清零操作。 ### 回答2: memset()函数是C语言中的一个库函数,其原型如下: void *memset(void *ptr, int value, size_t num); memset()函数的作用是将指定内存空间的值设置为指定的值。其中,ptr表示要设置的内存空间的起始地址,value表示要设置的值,num表示要设置的字节数。 memset()函数返回值为void指针类型,即可以接受任何类型的指针使用memset()函数可以在一次调用中批量设置内存空间的值,提高效率和代码的简洁度。 例如,下面的代码片段就是使用memset()函数将数组arr中的所有元素设置为0: int arr[10]; memset(arr, 0, sizeof(arr)); 由于memset()函数设置的是字节数据,所以在设置非字符类型数据时需要小心。以一个int型数组arr为例,使用memset()函数将其所有元素设置为-1可能会出现错误。因为在某些机器上,-1的二进制表示并不是所有字节全为1,这会导致memset()函数设置的结果并非预期。 对于字符数组或字符串,可以使用memset()函数设置为0,即'\0',也可以使用strcpy()函数进行单个字符赋值,这样更为安全可靠。 总之,memset()函数是一个实用的函数,可以批量设置内存空间的值,提高代码的执行效率和简洁度。在使用时,需要注意数据类型和数据源的合法性,以避免出现错误。 ### 回答3: memset()函数是C/C++语言中的一个库函数,主要用来对一段指定内存空间进行初始化。 其函数原型为: void* memset(void* ptr, int value, size_t num); 第一个数ptr是一个指向某一块内存区域的指针,用来指定待初始化的内存空间。 第二个数value是一个整数值,用来指定待初始化的值,其中最常用的是0。 第三个数num是一个整数值,用来指定待初始化的内存空间的字节数。 memset()函数的作用是将ptr指向的内存空间中的每个字节都设置为value指定的值。一般来说,value为0时,可以用来将内存空间清零。 memset()函数通常用于对数组、字符串或结构体等数据类型的初始化。例如,当我们声明一个数组或字符串后,需要将其所有元素或字符都初始化为0,可以使用memset()函数。 以下是一个使用memset()函数进行数组初始化的示例: int num[5]; memset(num, 0, sizeof(num)); 以上代码将会把num数组的所有元素都初始化为0。 需要注意的是,memset()函数只能用来设置每个字节的值,并不能对较大的数据块进行初始化。此外,在使用memset()函数时,需要确保待初始化的内存空间不受限制并且是可访问的,否则可能会引发错误。 总结起来,memset()函数是C/C++中常用的一个函数,主要用来对一段指定的内存空间进行初始化,提高程序的可读性和可维护性。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Allen.Su

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

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

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

打赏作者

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

抵扣说明:

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

余额充值