[Effective C++]条款20: 以pass-by-reference-to-const替换pass-by-value

204 阅读2分钟

首先了解一下 C/C++ 的参数传递包括:

- 1:值传递 (pass by value):将变量名作为实参和形参

首先计算出实参表达式的值,然后给对应的形参变量在堆栈中分配一个存储空间,该空间的大小等于改形参类型的,然后把算出的实参表达式的值放到形参对应的存储空间中,形成形参变量的初值,供被调用函数执行时使用。 值传递是单向的(实参->形参), 参数的值只能传入。 为了观察这一过程,我新建一个狗狗🐶类:

class dog : public People {
public:
    dog () {};
    ~dog() {};
    string dogName;
};

以及一个方法: void printDogInform(dog d);

void printDogInform(dog d)

{
    d.dogName = "Blue";

    cout << "Dog address in printDogInform:" << &d << endl;

    cout << "Dog name inprintDogInfor:" << d.dogName <<endl;
}

main 函数里:

int main(int argc, const char* argv[]) {

    dog d;

    d.dogName = "Pink";

    printDogInform(d);

    cout << "Dog address in main:" << &d << endl;

    cout << "Dog name in main:" << d.dogName <<endl;

    return 0;
}

运行工程得到了如下的输出:

**Dog address in printDogInform:0x16fdff560**

**Dog name inprintDogInfor:Blue**

**Dog address in main:0x16fdff5b8**

**Dog name in main:Pink**

可以看出main函数和printDogInform里的Dog地址是不一样的,成员变量dogName的值也是不一样的。 但是值传递意味着额外的内存开销💰,也比较低效率

- 2:指针传递 (pass by pointer)

形参为指向实参地址的指针, 当对形参的指向操作时就相当于对实参数本身进行操作. 还是以狗狗🐶类举例: 声明一个方法接受指针形式的参数 void printDogInform(dog *d);

void printDogInform(dog *d)

{
    d->dogName = "Blue";

    cout << "Dog address in printDogInform:" << d << endl;

    cout << "Dog name inprintDogInfor:" << d->dogName <<endl;

}

main 函数里

int main(int argc, const char* argv[]) {

    dog *d = **new** dog();

    d->dogName = "Pink";

    printDogInform(d);

    cout << "Dog address in main:" << d << endl;

    cout << "Dog name in main:" << d->dogName <<endl;

    return 0;
}

输出为:

Dog address in printDogInform:0x105962ba0

Dog name inprintDogInfor:Blue

Dog address in main:0x105962ba0

Dog name in main:Blue

- 3:引用传递 (pass by reference)

形参相当于实参数的别名, 对形参的操作就是对实参的操作,在引用传递的过程中,虽然被调函数的形式参数虽然也作为局部变量在栈中开辟了内存空间,但是这时存放的是由主调函数放进来的实参变量的地址。被调函数对形参的任何操作都被处理成间接寻址,即通过栈中存放的地址访问主调函数中的实参变量。正因为如此,被调函数对形参做的任何操作都影响了主调函数中的实参变量。 以狗狗🐶类为例,修改打印方法为酱紫: void printDogInform(dog &d);

void printDogInform(dog &d)

{
    d.dogName = "Blue";

    cout << "Dog address in printDogInform:" << &d << endl;

    cout << "Dog name inprintDogInfor:" << d.dogName <<endl;

}

main函数为酱紫:

    int main(int argc, const char* argv[]) {
    dog d;

    d.dogName = "Pink";

    printDogInform(d);

    cout << "Dog address in main:" << &d << endl;

    cout << "Dog name in main:" << d.dogName <<endl;

    return 0;

}

输出为:

Dog address in printDogInform:0x16fdff5b8

Dog name inprintDogInfor:Blue

Dog address in main:0x16fdff5b8

Dog name in main:Blue

可以看出来, 引用传参指针传参操作的都是实参本身,那么这两种方法到底有什么区别呢? 我认为使用指针传递容易写出bug, 如果我们在被调函数修改形参里的指针的内容,希望在main里面观测到修改之后的结果,有时候结果并不是我们想象的那样,首先看例子: 先修改 **void** printDogInform(dog *d)

void printDogInform(dog *d)
{
    dog *z = new dog();

    z->dogName = "Red";

    d = z;

    cout << "Dog address in printDogInform:" << d << endl;

    cout << "Dog name inprintDogInfor:" << d->dogName <<endl;
}

main函数

int main(int argc, const char * argv[]) {

    dog *d = new dog();

    d->dogName = "Pink";

    printDogInform(d);

    cout << "Dog address in main:" << d << endl;

    cout << "Dog name in main:" << d->dogName <<endl;
    return 0;
}

得到的输出为:

**Dog address in printDogInform:0x10075ffc0**

**Dog name in printDogInfor:Red**

**Dog address in main:0x10075ff70**

**Dog name in main:Pink**

从输出结果得知:

执行完方法之后,main里面狗狗🐶的名字是Pink,并不是我期待的Red,并未察觉到指针的修改, 跟我期待的结果不一致.因为指针传惨的本质是值传递,也就是传递的是实参地址的值,在被调方法内,形参的地址发生了变化,从而和输入脱离了联系.

首先我们看一下怎样fix这个问题:

可以把printDogInform(dog d),这个方法里的d = z修改为d = *z,如下所示

void printDogInform(dog *d)

{
    dog *z = new dog();

    z->dogName = "Red";

    *d = *z;

    cout << "Dog address in printDogInform:" << d << endl;

    cout << "Dog name inprintDogInfor:" << d->dogName <<endl;

}

输出结果为:

**Dog address in printDogInform:0x100561410**

**Dog name inprintDogInfor:Red**

**Dog address in main:0x100561410**

**Dog name in main:Red**

这样修改的就不是地址的值了,而是地址值的指针,依赖关系还在。上面的写法不太简洁,而且冷静分析虽然能避免这类bug,但是有没有更好的方法呢? 那就是使用引用传参数,如下所示:

void printDogInform(dog &d)
{
    dog z;

    z.dogName = "Red";

    d = z;

    cout << "Dog address in printDogInform:" << &d << endl;

    cout << "Dog name in printDogInform:" << d.dogName <<endl;
}

main函数里:

int main(int argc, const char* argv[]) {
    dog d;

    d.dogName = "Pink";

    printDogInform(d);

    cout << "Dog address in main:" << &d << endl;

    cout << "Dog name in main:" << d.dogName <<endl;

    return 0;
}

输出为:

Dog address in main:0x16fdff618**

Dog name in main:Red**

Dog address in main:0x16fdff618**

Dog name in main:Red**
三种传值方式比较效率:

定义三个函数, 分别为值传递,指针传递和引用传递:

void printDogInform1(dog d);

void printDogInform2(dog *d);

void printDogInform3(dog &d);
// value

void printDogInform1(dog d)
{
    d.dogName = "Blue";
}

// pointer
void printDogInform2(dog *d)
{
    d->dogName = "Red";
}

// reference
void printDogInform3(dog &d)
{
    d.dogName = "Red";
}

在main函数里面

int main(int argc, const char* argv[]) {
    dog d;
    d.dogName = "Pink";
    
    size_t begin1 = clock();
    for (size_t i = 0; i < 100000 ; ++i)
        printDogInform1(d);
    size_t end1 = clock();
    cout << "pass by value elapsed time:" << end1 - begin1 << endl;
    
    dog *d1 = new dog();
    size_t begin2 = clock();
    for (size_t i = 0; i < 100000 ; ++i)
        printDogInform2(d1);
    size_t end2 = clock();
    cout << "pass by pointer elapsed time:" << end2 - begin2 << endl;  

    size_t begin3 = clock();
    for (size_t i = 0; i < 100000 ; ++i)
        printDogInform3(d);
    size_t end3 = clock();
    cout << "pass by reference elapsed time:" << end3 - begin3 << endl;

输出为:

**pass by value elapsed time:1603**

**pass by pointer elapsed time:1051**

**pass by reference elapsed time:883**

通过这个例子,可以看出值传递耗时最多.

默认情况下,C++的函数调用是传值调用,也就是形参copy实参的内容. 引用和指针的不同点: 1.引用定义时必须初始化 而指针没有要求 2.引用在初始化时引用一个实体后,就不能引用其他实体;而指针可以在任何时候指向任何实体 3.没有NULL引用,有NULL指针。 4.没有多级引用,有多级指针 5.sizeof中含义不同:引用结果为实体的大小(1),指针始终是地址空间所占大小(c,一个占4字节) 6.引用++则实体增加1,指针++即指针向后偏移一个类型的大小 7.访问实体方式不同,指针需要解引用;引用不用,编译器自己处理 8.引用比指针更安全

reference:
reference: