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

系列文章目录

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 
//值传递
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 
#include 
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 = ±
    //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 
#include 
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 
#include 
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 
#include 

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 
#include 

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 

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 
#include 

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

总结

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

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

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

你可能感兴趣的:(C++技能系列,c++,开发语言,linux)