c++入门笔记(15)引用

什么是引用?

引用就是别名
当定义引用的时候,一定要对引用初始化

    int num;
    int &mum = num; //mum是num的别名

mum是num的别名,这两个变量是一个变量,只不过名字不同而已。
这就好像李四有个外号叫李大嘴,大家称呼李四指的是李四这个人,称呼李大嘴也是指的李四这个人。

所以,我们对mum的操作实际就是对num的操作。

注:别名mum前面的符号&不是取址运算符,而是引用运算符。虽然它们符号相同,但功能却不一样。

引用运算符一般用在别名的初始化上。而取址运算符则可以用在任何时候。

例:

#include <iostream>

int main(){

    int num;
    int &mum = num; //mum是num的别名,这两个变量是一个变量,只不过名字不同而已

    num = 999;

    std::cout << "mum:\t" << mum << std::endl;

    mum = 0;

    std::cout << "num:\t" << num << std::endl;


    return 0;
}

输出:

mum:    999
num:    0

引用的地址

我们查看一下别名的地址,就会明白为什么对别名的操作就是对原名的操作。

别名和原名的地址是相同的。

例:

#include <iostream>

int main(){

    int a;
    int &ra = a;
    a = 999;
    std::cout << "&a:\t" << &a << std::endl;    //输出原名地址
    std::cout << "&ra:\t" << &ra << std::endl;  //输出别名地址


    return 0;
}

输出:

&a:     0x28fea8
&ra:    0x28fea8

别名和原名的地址是相同的。


引用就是别名的常量

假如你定义了某个变量的别名,那么该别名就永远属于这个变量,它会忠心耿耿地跟随该变量,不会变成别的变量的别名。

例:

#include <iostream>

int main(){

    int a;
    int &ra = a;    //ra是a的别名
    a = 999;

    std::cout << "&a:\t" << &a << std::endl;
    std::cout << "&ra:\t" << &ra << std::endl;

    int b = 888;

    ra = b;         //把b里的值赋给别名,变量b并没有把自身的地址赋给ra别名
    std::cout << std::endl;
    std::cout << "&a:\t" << &a << std::endl;
    std::cout << "&ra:\t" << &ra << std::endl;
    std::cout << "&b:\t" << &b << std::endl;

    std::cout << std::endl;
    std::cout << "a:\t" << a << std::endl;
    std::cout << "ra:\t" << ra << std::endl;
    std::cout << "b:\t" << b << std::endl;

    ra = 1;

    std::cout << std::endl;
    std::cout << "a:\t" << a << std::endl;
    std::cout << "ra:\t" << ra << std::endl;
    std::cout << "b:\t" << b << std::endl;

    return 0;
}

输出:

&a:     0x28fea8
&ra:    0x28fea8

&a:     0x28fea8
&ra:    0x28fea8
&b:     0x28fea4

a:      888
ra:     888
b:      888

a:      1
ra:     1
b:      888

在这个实例中,我们将ra定义为a的别名。
这样ra这个别名就只属于变量a,它不会变成b的别名。
变量b只能将自己的值赋给这个ra,它不能改变ra的地址,比如说它不能让ra变成自己的别名。

因此这个ra又可看作是个别名常量,它是a的别名这个身份我们无法更改,我们能改变的只是它所引用的值。


引用对象

我们也可以定义一个对象的别名。
注:我们不能定义一个类的别名。因为类它没有具体的内存地址,所以我们不能定义一个它的别名。

别名必须初始化,不能通过赋值的方式绑定别名。。

例:

#include <iostream>

class Human
{
public:
    Human() {}
    inline int Get() const {return i;}
    inline void Set(int x) {i = x;}

private:
    int i;
};

int main(){

    Human Mike;
    Human &rMike = Mike;    //对象的别名

    rMike.Set(11);
    std::cout << rMike.Get() << std::endl;

    return 0;
}

输出:

11

空引用

我们知道指针进行删除操作后,需要将它们赋为空,引用却不需要这么做,这是因为引用是原来对象的别名,假如该对象存放在栈中,那么在对象超出作用域时别名会和对象一起消失。

假如该对象存入在堆中,由于堆中内存空间必须使用指针来访问,因此用不着别名,即使再定义一个该指针的别名,那么将指针删除并赋空之后,该指针的别名中的地址也相应的赋空了。


按值传递

例:

#include <iostream>

void swap1(int a, int b);   //按值传递参数

int main(){

    int a = 5, b = 10;
    std::cout << "在主函数中,交换前, a:\t" << a  << "\tb:\t" << b << std::endl;
    std::cout << std::endl;

    swap1(a , b);   //按值传递的交换函数

    std::cout << std::endl;
    std::cout << "在主函数中,交换前, a:\t" << a  << "\tb:\t" << b << std::endl;

    return 0;
}

void swap1(int a, int b){

    std::cout << "swap1函数中,交换前,a:\t" << a  << "\tb:\t" << b << std::endl;

    int c;  //交换
    c = a;
    a = b;
    b = c;

    std::cout << "swap1函数中,交换后,a:\t" << a  << "\tb:\t" << b << std::endl;
}

输出:

在主函数中,交换前, a: 5       b:      10

swap1函数中,交换前,a: 5       b:      10
swap1函数中,交换后,a: 10      b:      5

在主函数中,交换前, a: 5       b:      10

swap1函数交换的是main函数中a和b的副本的值
,而不是a和b本身。
也就是说在main函数中定义的a和b的备份的值。

那么为什么swap1函数不直接交换a和b本身,却去交换它们副本的值呢?
这是因为当我们直接将a和b传递给swap1函数时,这样的传递方式是按值传递

假如将a和b按值传递给swap1函数,那么编译器会自动在栈中创建a和b的拷贝,然后将a和b的拷贝传递给swap1函数。
在swap1函数中对a和b的拷贝进行交换。因此我们看到的输出语句,a和b确实进行了交换,只不过交换的是a和b的副本。

由于交换的是a和b的副本,并不是a和b本身,所以在swap1函数结束后,输出的值显示main函数中的a和b并没有改变。


按址传递

地址的方式传递。

#include <iostream>

void swap1(int *a, int *b);   //按址传递参数

int main(){

    int a = 5, b = 10;
    std::cout << "在主函数中,交换前, a:\t" << a  << "\tb:\t" << b << std::endl;
    std::cout << std::endl;

    swap1(&a , &b);   //按址传递的交换函数

    std::cout << std::endl;
    std::cout << "在主函数中,交换前, a:\t" << a  << "\tb:\t" << b << std::endl;

    return 0;
}

void swap1(int *a, int *b){

    std::cout << "swap1函数中,交换前,a:\t" << *a  << "\tb:\t" << *b << std::endl;

    int c;  //交换
    c = *a;
    *a = *b;
    *b = c;

    std::cout << "swap1函数中,交换后,a:\t" << *a  << "\tb:\t" << *b << std::endl;
}

输出:

在主函数中,交换前, a: 5       b:      10

swap1函数中,交换前,a: 5       b:      10
swap1函数中,交换后,a: 10      b:      5

在主函数中,交换前, a: 10      b:      5

我们将a和b的地址传递给swap1函数以后,我们就可以在swap1函数中通过a和b的地址直接访问到主函数中的a和b。
由于可直接通过地址访问到主函数中的a和b,就可以使用*星号修改主函数a和b的值 。

所以在调用swap1函数以后,主函数a和b的值就交换成功了。


按别名传递

把指针作为函数的接收参数虽然能够正常使用,但是它却不易阅读,而且很难使用。

按别名传递参数。
例:

#include <iostream>

void swap1(int &a, int &b);   //按别名传递参数

int main(){

    int a = 5, b = 10;
    std::cout << "在主函数中,交换前, a:\t" << a  << "\tb:\t" << b << std::endl;
    std::cout << std::endl;

    swap1(a , b);   //按别名传递的交换函数

    std::cout << std::endl;
    std::cout << "在主函数中,交换前, a:\t" << a  << "\tb:\t" << b << std::endl;

    return 0;
}

void swap1(int &a, int &b){

    std::cout << "swap1函数中,交换前,a:\t" << a  << "\tb:\t" << b << std::endl;

    int c;  //交换
    c = a;
    a = b;
    b = c;

    std::cout << "swap1函数中,交换后,a:\t" << a  << "\tb:\t" << b << std::endl;
}

输出:

在主函数中,交换前, a: 5       b:      10

swap1函数中,交换前,a: 5       b:      10
swap1函数中,交换后,a: 10      b:      5

在主函数中,交换前, a: 10      b:      5

swap1函数接收参数为两个别名,这样我们在调用swap1函数时直接将a和b传递即可,
由于别名即自身,所以对a和b的操作,是对主函数中a和b的操作。

或者说参数中的a和b,即主函数中的a和b。


让函数返回多个值

我们知道函数只能返回一个值,那么假如有的时候我们需要函数返回多个值时该怎么办?

指针或者引用可以帮助我们解决这个问题,我们使用别名或者指针的方式传递给函数一个以上的变量,在函数体中将需要返回的值赋给这些变量,由于使用引用或者指针传递变量允许函数改变原来的变量。
因此这些在函数体中被修改的变量均可以看做是已经被该函数返回的值。

例:演示使用指针一下返回3个值的操作。

#include <iostream>

int func(int a, int *b, int *c); //使用指针返回3个值

int main(){

    int a = 1, b = 2, c = 3;
    std::cout << "主程序,调用func函数前...\n";
    std::cout << "a:\t" << a << "\nb:\t" << b << "\nc:\t" << c << std::endl;
    std::cout << "\n";

    a = func(a, &b, &c);

    std::cout << "\n主程序,调用func函数后...\n";
    std::cout << "a:\t" << a << "\nb:\t" << b << "\nc:\t" << c << std::endl;

    return 0;
}

int func(int a, int *b, int *c){

    std::cout << "func函数中,计算前...\n";

    std::cout << "a:\t" << a << "\nb:\t" << *b << "\nc:\t" << *c << std::endl;

    a = a+1;        //a自加1
    *b = (*b) * (*b);   //b的平方
    *c = (*c) *(*c) * (*c); //c的立方

    std::cout << "func函数中,计算后...\n";
    std::cout << "a:\t" << a << "\nb:\t" << *b << "\nc:\t" << *c << std::endl;

    return a;
}

输出:

主程序,调用func函数前...
a:      1
b:      2
c:      3

func函数中,计算前...
a:      1
b:      2
c:      3
func函数中,计算后...
a:      2
b:      4
c:      27

主程序,调用func函数后...
a:      2
b:      4
c:      27

我们也可以把a作为返回的判断值,把*b和 *c作为运算的返回值,用该种方法我们可以实现汇报执行程序时的非法操作信息。

拉下来的程序就是演示了这一点:
例:

#include <iostream>

int func(int a, int *b, int *c); //使用指针返回3个值

int main(){

    int a, b, c;
    int check;
    std::cout <<"请输入要进行运算的数字,\n";
    std::cout << "您输入的数字将作为 圆的半径 和 正方形的边长:";
    std::cin >> a;

    check = func(a, &b ,&c);  //用指针的方式传值
    if( check ){    //判断函数返回值 是1还是0
        std::cout << "输入的数字超过计算范围!\n";
    }
    else{   //如果是0的话
        std::cout << "圆的面积为:" << b << std::endl;
        std::cout << "正方形的面积为:" << c << std::endl;
    }

    return 0;
}

int func(int a, int *b, int *c){

    if(a>20000){
        a = 1;      //a作为返回判断值
    }
    else{
        *b = a * a * 3.14;
        *c = a * a;

        a = 0;      //a作为返回判断值
    }

    return a;
}

输出: 输入20001

请输入要进行运算的数字,
您输入的数字将作为 圆的半径 和 正方形的边长:20001
输入的数字超过计算范围!

输出: 输入100

请输入要进行运算的数字,
您输入的数字将作为 圆的半径 和 正方形的边长:100
圆的面积为:31400
正方形的面积为:10000

注:通过指针返回的两个值 不是通过返回机制来得到的,而是通过改变函数指针参量*b和 *c所指向的内存区域中的值来实现的。


用引用来返回多值

用引用重写上面代码:

#include <iostream>

int func(int a, int b, int c); //使用引用返回值

int main(){

    int a, b, c;
    int check;
    std::cout <<"请输入要进行运算的数字,\n";
    std::cout << "您输入的数字将作为 圆的半径 和 正方形的边长:";
    std::cin >> a;

    check = func(a, b ,c);  //使用引用返回值
    if( check ){    //判断函数返回值 是1还是0
        std::cout << "输入的数字超过计算范围!\n";
    }
    else{   //如果是0的话
        std::cout << "圆的面积为:" << b << std::endl;
        std::cout << "正方形的面积为:" << c << std::endl;
    }

    return 0;
}

int func(int a, int *b, int *c){

    if(a>20000){
        a = 1;      //a作为返回判断值
    }
    else{
        b = a * a * 3.14;
        c = a * a;

        a = 0;      //a作为返回判断值
    }

    return a;
}

输出: 输入20001

请输入要进行运算的数字,
您输入的数字将作为 圆的半径 和 正方形的边长:20001
输入的数字超过计算范围!

输出: 输入100

请输入要进行运算的数字,
您输入的数字将作为 圆的半径 和 正方形的边长:100
圆的面积为:31400
正方形的面积为:10000

采用别名的方式传递参数使程序更加明了和易于掌据。


按值传递对象

按值传递在向函数传递一个对象时,会像传递变量那样建立一个该对象的拷贝,而从函数返回一个对象时,也要建立这个被返回的对象的一个拷贝。

这样假如该对象的数据非常多时,这种拷贝带来的内存开销是相当可观的。比如说该对象拥有1000多个double型成员变量,每个double型变量占据8个字节,1000个就要占据8000个字节,每次通过值传递的方式给函数传递该对象,都要在栈中复制该对象,占用8000个字节的栈内空间,而返回该对象,又要在栈中复制一次,这样就又要占用8000个字节的内存空间。
我们知道栈的内存只有2M大小,8000个字节占用8K,那么仅仅传递该对象就占用了栈内16K字节的空间。
并且别的对象想要访问该对象的8000个数据成员的时候,也要同样采取复制的方式,那么系统的开销将无法估算了。

然而,按值传递所付出的开销远不如此,由于在传递过程中需要复制对象,因此会默认调用复制构造函数,该函数的作用就是创建某个对象的临时副本。只要在栈中创建临时拷贝都会自动调用复制构造函数即可。

而当函数返回时,传递该对象时创建的该对象的副本会被删除,这时候又会自动调用该对象的析构函数来释放内存。
假设返回的仍然是该对象,并且仍旧采用按值传递的方式,那么就又会调用复制构造函数建立一个该对象的临时副本,当该值被成功返回给调用程序后,然后再调用该对象的析构函数删除临时拷贝并释放内存。

我们看到复制构造函数和析构函数一连被执行了两次,这无疑会增加系统的开销。我们用一个实例来演示一下按值传递一个对象的复制与删除过程。

#include <iostream>

class A
{
public:
    A() {std::cout << "执行构造函数创建一个对象\n";}
    A(A&) {std::cout << "执行复制构造函数创建该对象的副本\n"; } //复制构造函数 参数是类A对象的别名
    ~A() {std::cout << "执行析构函数删除该对象\n";}
};

A func(A one){      //按值传递对象
    return one;
}


int main(){

    A a;    //定义了一个类A的对象a
    func(a);    //按值传递对象

    return 0;
}

输出:

执行构造函数创建一个对象
执行复制构造函数创建该对象的副本
执行复制构造函数创建该对象的副本
执行析构函数删除该对象
执行析构函数删除该对象
执行析构函数删除该对象
A a;    //定义了一个类A的对象a

1.创建一个对象会自动调用构造函数。
所以输出:“执行构造函数创建一个对象”

func(a);    //按值传递对象

2.接下来将对象a按值传递到func函数中。
这时会自动调用复制构造函数创建对象a的一个副本。
然后将这个副本传递到func函数中去。
所以输出:“执行复制构造函数创建该对象的副本”
以证明复制构造函数被调用。

A func(A one)

3.当我们将这个副本传递到func函数中去,这样程序转到这一行来执行。

return one;

4.由于返回方式也是按值返回。
又会调用复制构造函数。
再次创建一个返回值one的副本。
所以输出:“执行复制构造函数创建该对象的副本”

5.由于func函数的返回值没有赋给任何对象,因此这个返回的临时对象也就被丢弃了。
这时自动调用析构函数来释放这个临时对象所占用的内存。
所以输出:执行析构函数删除该对象

6.由于func函数已经结束了,因此在它内部拷贝的临时对象one也就被系统删除了。
对象被删除会自动调用该对象的析构函数来释放内存。
所以输出:“执行析构函数删除该对象”

7.最后,main函数结束,对象a的生命也结束了。
这样又会调用析构函数,来释放对象a的内存。
所以输出: “执行析构函数删除该对象”


通过这个程序我们就可以观察到,将一个对象按值传递给一个函数,会调用2次复制构造函数和2次析构函数。这样系统的开销是很大的。


用指针来传递对象

用指针来传递对象可以解决用值传递对象开销大的问题。
我们将对象a的地址传递给func函数,就可避免调用复制构造函数。

#include <iostream>

class A
{
public:
    A() {std::cout << "执行构造函数创建一个对象\n";}
    A(A&) {std::cout << "执行复制构造函数创建该对象的副本\n"; } //复制构造函数 参数是类A对象的别名
    ~A() {std::cout << "执行析构函数删除该对象\n";}
};

A func(A *one){      //按址传递对象
    return *one;     //返回一个对象
}

int main(){

    A a;    //定义了一个类A的对象a
    func(&a);    //按址传递对象

    return 0;
}

输出:

执行构造函数创建一个对象
执行复制构造函数创建该对象的副本
执行析构函数删除该对象
执行析构函数删除该对象

由于复制构造函数没有执行,因此也不用调用析构函数来删除复制构造函数创建的对象。

return *one;     //返回一个对象

由于返回的是对象,而不是地址,所以这种返回方式是按值返回。
按值返回的话就会调用复制构造函数

如果不想调用复制构造函数,那么就返回一个一址,返回类型是指向类A对象的指针既可。
如:

#include <iostream>

class A
{
public:
    A() {std::cout << "执行构造函数创建一个对象\n";}
    A(A&) {std::cout << "执行复制构造函数创建该对象的副本\n"; } //复制构造函数 参数是类A对象的别名
    ~A() {std::cout << "执行析构函数删除该对象\n";}
};

A* func(A *one){      //按址传递对象 返回值类型是指向类A对象的指针
    return one;    //返回一个地址
}

int main(){

    A a;    //定义了一个类A的对象a
    func(&a);    //按址传递对象

    return 0;
}

输出:

执行构造函数创建一个对象
执行析构函数删除该对象

由于复制构造函数没有执行,所以也不用调用析构函数删除复制构造函数创建的对象,一举避免了两个函数的调用。


用const指针来传递对象

按址传递对象虽然可以避免调用复制构造函数和析构函数,但是由于它得到了该对象的内存地址,可以随时修改对象的数据。所以它实际上是破坏了按值传递的保护机制。

不过我们仍然对此有解决办法,那就是用const指针来接受对象,这样就可以防止任何试图对该对象所进行的操作行为,并且保证返回一个不被修改的对象。

#include <iostream>

class A
{
public:
    A() {std::cout << "执行构造函数创建一个对象\n";}
    A(A&){std::cout << "执行复制构造函数创建该对象的副本\n";}
    ~A() {std::cout << "执行析构函数删除该对象\n";}
    inline void Set(int i) {x = i;}
    inline int Get()const {return x;}
private:
    int x;
};

const A* const func(const A *const one){   //指针one指向的对象不可修改 指针one不可修改 返回值类型const表示返回值one指针是不可被修改的 one指针所指向的对象也是不可修改的
    one->Get();
    //one->Set(99);
    //one++;
    return one;
}

int main(){
    A a;
    a.Set(11);
    const A *const p = func(&a);    //p为指向常量的常指针

    std::cout << "p:\t" << p << std::endl;
    std::cout << "a:\t" << &a << std::endl;
    std::cout << p->Get() << std::endl;

    return 0;
}

输出:

执行构造函数创建一个对象
p:      0x28fea8
a:      0x28fea8
11
执行析构函数删除该对象
const A* const func(const A *const one)

这样就保证了传递进来的数据不被修改,
同时又保证了返回的数据也不会被修改。

我们将函数的返回值和接收参数都定义为const,就可保证函数内不可修改原始值,同时避免利用返回值对原始值进行修改。
所以加上这个const,实际上是为了实现按值传递的保护机制,同时又避免按值传递的开销,因为不用再调用复制构造函数。

注:指向常量的常指针,只是限制我们用该指针修改它指向的对象的值,但是它并不会改变原始对象的属性。

但是加上const很麻烦。可以用一种更简便的方法:按别名传递对象。


按别名传递对象

由于引用不能重新分配去引用另一个对象,它始终是常量,所以我们不用将它设置为常量。

#include <iostream>

class A
{
public:
    A() {std::cout << "执行构造函数创建一个对象\n";}
    A(A&){std::cout << "执行复制构造函数创建该对象的副本\n";}
    ~A() {std::cout << "执行析构函数删除该对象\n";}
    inline void Set(int i) {x = i;}
    inline int Get()const {return x;}
private:
    int x;
};

 A& func( A &one){  //参数类型 对象别名 参数返回值类型 对象别名
    one.Get();
    return one; //返回对象别名
}

int main(){
    A a;
    a.Set(11);
    A &p = func(a);  //用别名接收func函数的返回的别名

    std::cout << "别名p:\t" << &p << std::endl;
    std::cout << "a:\t" << &a << std::endl;
    std::cout << p.Get() << std::endl;

    b.Set(33);  //别名可以改变原名成员的值
    std::cout << b.Get() << std::endl;
    return 0;
}

输出:

执行构造函数创建一个对象
别名p:   0x28fea8
a:      0x28fea8
11
11
执行析构函数删除该对象

我们看到按别名返回对象时,并没有调用复制构造函数,因为返回的并不是一个对象,而是对象a的别名,所以不用调用复制构造函数。从而避免了系统的开销。因此它实现了指针的功能,但是与指针不同的是,它所引用的这个对象还是可以改变的。


返回值类型为常量别名,接收这个返回值的也是常量别名,这样这个别名就不会修改原名的成员值了。

#include <iostream>

class A
{
public:
    A() {std::cout << "执行构造函数创建一个对象\n";}
    A(A&){std::cout << "执行复制构造函数创建该对象的副本\n";}
    ~A() {std::cout << "执行析构函数删除该对象\n";}
    inline void Set(int i) {x = i;}
    inline int Get()const {return x;}
private:
    int x;
};

const A& func( A &one){  //参数类型 对象别名 参数返回值类型 对象别名
    one.Get();
    return one; //返回对象别名
}

int main(){
    A a;
    a.Set(11);
    A const &b = func(a);  //别名常量

    std::cout << "p:\t" << &b << std::endl;
    std::cout << "a:\t" << &a << std::endl;
    std::cout << b.Get() << std::endl;

    //b.Set(33);  //别名常量不可改变原名成员的值
    //std::cout << b.Get() << std::endl;
    return 0;
}

输出:

执行构造函数创建一个对象
p:      0x28fea8
a:      0x28fea8
11
执行析构函数删除该对象

到底是使用引用还是指针

既然引用实现了指针的功能,而且使用起来更加方便,为什么还要指针呢?
这是因为指针可以为空,但是引用不能为空,指针可以被赋值,但是引用只可以被初始化,不可以被赋为另一个对象的别名。
如果你想使一个变量记录不同对象的地址,那么就必须使用指针。

另外,在堆中创建一块内存区域,必须要用指针来指向它,否则该区域就会变成无法访问的内存空间。
当然我们也可以使用引用来引用指向内存空间的指针。

int *p = new int;
int &r = p;

这样,引用r就是指针p的别名。

int *p = new int;
int &r = *p;

这样,这个引用r就变成了用指针p读取到的值的别名。
我们可以通过这个r来修改保存在堆中空间的数据。
如:

#include <iostream>

int main(){

    int *p = new int;
    int &r = *p;
    r = 4;  //将4保存到堆中空间
    std::cout << "堆中空间的值是:" << *p << std::endl;
    return 0;

输出:

堆中空间的值是:4

那么这个4就自动保存到堆中空间中。

注:我们不可以直接用引用来指向堆中新建的空间,因为引用只是个别名,它不可以作为指针来使用。
错误例子:

int &r = new int;   //错误例子

正确例子:

#include <iostream>

int main(){

    int *p = new int;
    if(p!=NULL){        //测试p,假如不为空,表示空间创建成功。
        int &r = *p;    //将r初始化为p指向的内存空间中数据的别名
        r = 5;          //直接向堆中该空间赋值
        std::cout << *p << std::endl;
    }
    return 0;
}

输出:

5

注:r是在大括号中创建的,所以它的作用域只在大括号内。


指针与引用的区别:
指针可以为空,引用不能为空。
指针可以被赋值,引用不能被赋值。
指针可以指向堆中空间,引用不可以指向堆中空间。


引用和指针可以一块用

int *func(int &one, int *two, int x);

该行语句声明了一个func函数,该函数有三个参数,
第一个是int型变量的别名one,
第二个是指向int型变量的指针two,
第三个是整型参数x,
该函数返回一个指向int型变量的指针。


int *r, ra;

该行只有r是int型指针,ra是int型。


引用容易犯的错误

与指针一样,引用使用不当也会出现致命性的错误。
我们知道引用是对象的别名,那么假如这个对象不存在了,使用这个对象的别名会产生什么样的后果呢?

错误例子:

#include <iostream>
//错误例子,此程序运行崩溃
class A
{
public:
    A(int i) {x = i;}
    int Get() const {return x;}
    int x;
};

A& func(){  //返回值是一个对象的别名
    A a(23);
    return a;
}

int main(){

    A &r = func(); //func函数返回对象a的别名
    std::cout << r.Get() << std::endl;
    return 0;
}

由于对象a是局部对象,因此当函数func结束后,局部对象a也就被删除了。
由于对象a消失了,所以func()函数返回的其实是一个并不存在对象的别名。

&r是一个并不存在的别名,用这个不存在的对象来调用对象的函数get(),该函数会返回一个并不存在的对象的x成员。
由于这个对象不存在,所以它的x成员也不存在。
因此崩溃。


正确例子

#include <iostream>
//正确例子
class A
{
public:
    A(int i) {x = i;}
    int Get() const {return x;}
    int x;
};

A func(){  //按值返回
    A a(23);
    return a;
}

int main(){

    A r = func(); //接值返回 会调用复制构造函数
    std::cout << r.Get() << std::endl;
    return 0;
}

输出:

23

假如我们不创建这样的复制构造函数,编译器也会为我们提供一个默认复制构造函数。
这个默认的复制构造函数实现了同样的功能,即根据一个对象创建一个新对象,然后将旧对象的成员复制给新对象。

这里之所以创建这样一个复制构造函数,只是为了在这个函数中增加一条输出语句。而之所以加上这条输出语句,则是为了说明按值返回对象时会自动调用默认复制构造函数来创建该对象的副本。


对于引用而言,如果引用的是一个临时变量,那么这个临时变量的生存期会不少于这个引用的生存期。

但是指针就没有这个特性,假如将对象a的副本的地址赋给一个指针,那么在func函数返回对象a的副本的时候,就可以析构这个对象a的副本。


引用一个按值返回的堆中对象

此章以下看不懂,暂留 。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值