初识 C++ 函数参数引用和指针及左值和右值

 普通函数

double up(double x) { return 2.0* x; }

double a = 3;

double b = up(a);

这种情况是值传递,double x 是函数形参,a是函数实参。在执行double b = up(a)语句时,先开辟存储临时变量x的位置,把a的值拷贝到x位置,在开辟一个临时temp位置,存储函数返回值2*x。运行到 = 时把temp位置的值拷贝到变量b的位置。这个过程的好处和不足:

好处:a本身得到了保护,因为up函数运行过程中,使用的是x临时变量,并不对a变量本身进行操作,a的值不会再函数中进行改变。

不足:1,如果需要在函数中修改a,是不能实现的,函数中修改的是a的临时拷贝x,和a没有关系,并且调用结束后,x自动销毁

          2,如果函数参数是个大的结构体,就会因为多次拷贝增大程序的运行时间。

 还有个隐形的好处,因为x是a的拷贝,如果a的类型和x不符,编译器可以支持隐式类型转换,比如下面这些代码都是可以执行的。从下面r0这些调用方法看,编译器能进行类型转化的都会转化成int 类型进行使用,这个貌似是理所当然的, 编译器理应为工程师提供支持。我们看一下指针作为参数的情况。

double up(double x) { return 2.0* x; }

double a = 3;

double b = up(a);

void r0(int rx) { cout << "r0: " << rx << "\t"; cout << &rx << endl; }

void test00()

{

double w = 10.1;

r0(4);

r0(up(w));

r0(up(4));

r0(w + 1);

r0(w);

r0(4);

r0(5.1);

}

指针作为参数:

对于上面值传递作为参数的函数定义方式,存在的两个不足,考虑用指针作为参数进行传递,不传递值本身,而是传递变量的地址,这样就可以在函数内根据变量地址对变量进行操作了,可以修改变量,并且调用函数时不用建立变量副本,提升效率。

但是这样也有不足:

1,很多表达式没有办法作为函数参数使用,以下注释掉的调用都不行,就是只有r(p)、r(p+1)可以,(不允许作为参数传递的:比如常数,函数返回值。。。,具体都什么样的值不可以作为参数,这个在后面讨论)

2,函数内部每次使用都需要加*解引用,如果使用很多次,会带来工作量增大,并容易出错

3,C 语言允许对指针进行类似p+1 的操作,p+1 操作会根据指针本身类型指向下一个内容地址,那个地址是什么我们不知道,这个会导致完全不可控(通常说C语言中指针使用很危险,这个是主要原因),所有实际上以下对于r()函数的调用中,只有r(p)这一个表达式时有效的。

4,因为指针传递没有开辟新的空间,所以也不能进行类型转换,w是int 型的,他的指针才可以作为r()的参数。double 类型变量的指针无法使用。

指针作为参数的好处:

就是对应值传递的两个不足,1,可以对参数进行修改;2,减少了多余的复制。

void r(int *rx) { cout << "r0: " << *rx  << "\t"; cout << rx << endl; }

void test01()

{

int w = 10;

int *p = &w;

cout << "&w: " << &w << endl;

//r(&4);

//r(&up(w));

//r(up(4));

r(p);

r(p+1);

//r(4);

//r(5.1);

}

引用作为参数:

Int a = 10;

Int &b = a;

引用概念:以上b作为a的引用,可以对b进行赋值 ,b=3, 这样a也变成3. 这样看来b 是a的别名,b和a共同控制同一个内存位置。

引用本质上是指针常量: Int &b = a相当于int * const b = &a,同时 b = 3 相当于*b = 3 . 在使用引用时,编译器自动解引用,不需要程序员在b前加*,这样可以减少写代码的工作量,并且防错。(指针常量给了引用另一个特性,定义了引用不可在修改为其他的引用,b是a的别名,不能在修改成c的别名。)

 引用本身是指针,这样引用作为函数参数就可以实现指针的效果,即传递地址,减少函数调用时的值拷贝,同时可以在函数中对变量进行修改。下面这段函数代码看,引用作为参数的函数,函数调用时允许使用的参数和指针是一样的。(指针多一个可以使用 r(p+1),但这种调用只是语法上的允许,作为指针p+1 本身是没有意义的。)

void r2(double &rx) { cout << "r2: " << rx << endl; cout << &rx << endl; }

void test03()

{

double w = 10.1;

cout << "&w: " << &w << endl;

//r2(4);

//r2(up(w));

//r2(up(4));

r2(w);

//r2(w + 1);

//r2(4);

//r2(5.1);

}

对于以上留下的问题:指针和引用作为参数的函数,什么样的表达式(变量)允许作为参数:

因为参数本身是个指针(引用也是指针),所以参数必须是一个具有内存固定地址的表达式,w是初始化的变量,有固定的内存位置;4 是一个常数,内存中不存在;up(w)是函数的返回值,仅在函数调用时保存至临时开辟的内存中,没有固定位置;w+1 也没有固定位置。

基于这些差异,引入概念左值和右值。不准确的说法左值是 “= ”左边的值,右值是 “= ”右边的值。double w = 10.1语句中 w是左值,10是右值。这是C++ 98 对于左、右值的定义,比较容易理解,但是对程序编写没有什么用。

C++ 左右值的定义

左值:具有特定内存位置的值(具名对象),具有相对稳定的内存地址和较长的生命周期;

右值:不指向特定内存位置的匿名值,存在周期很短,通常是暂时性的。

这个定义和上面说的 在“=” 左右的定义不矛盾。基于上面特性来说,可以取地址的是左值,不能取地址的是右值(这个时判断表达式是左值还是右值的基本方法)。

通常的左值,变量名、函数名。。。

通常的右值,常数、函数返回值、逻辑表达式。。。

特别的:i++ 是右值(临时开辟的空间);++i 是左值(对i进行直接修改,是i的地址);

             “hello” 是左值,在内存中有特定的空间,可以取地址

返回到原来的问题,对于使用引用和指针作为参数的函数,调用函数只能传递左值,如r2(w) 是有效的;不能传递右值,因为右值没有固定的地址。

(对于引用作为参数的函数,本来编译器也有两种选择,传递类型匹配的参数就直接使用,传递类型不匹配的参数或右值,编译器自动开辟一个临时内存存储实参,这样可以让引用作为参数的函数适用性和值传递的函数接近,可以接收各种可能的数据做参数,编译器不报错误。但是这样做本来程序设计认为传递的是引用,但是因为开辟了新内存,导致原来的变量没有得到修改,既不报错又没实现函数对变量修改的功能,这样造成混乱,所有后来对这种函数进行了严格限制,不在自动为程序开辟内存,必须严格匹配)

void r0(int rx) {}

void r(int *rx){}

void r2(double &rx)

左值作为参数

Y

Y

Y

右值作为参数

Y

N

N

高效(避免变量拷贝)

N

Y

Y

变量本身修改

N

Y

Y

没有野指针风险

Y

N

Y

上面这个表格看到采用引用作为参数,比用指针和值传递的方式的优势,但是没有解决右值作为参数的情况。

常量引用作为参数:

将引用进一步定义为常量,相当于告诉编译器函数需要传递引用,但是不需要对引用的值进行修改。也就是相对于void r0(int rx) {}的两个不足,只需要解决提高效率(减少拷贝)就可以了。

这样使用相对于上面的只能传递左值引用,会带来便利,如果传递的是右值,系统可以自动开辟内存存储变量,降低了程序编写的困难,同时在传递符合类型的引用时又可以提高效率。

这段代码可以看到,在使用这样的函数时void r1(const double &rx) {} ,使用r1(w)调用,函数内的变量rx的地址和变量w时一样的,不需要拷贝;使用r1(d)进行调用时,rx的地址和变量d不一样,编译器自动开辟了一个地址保存d的拷贝,函数内使用这个临时拷贝。这并不影响任何结果,因为函数初始定义了参数时const类型的,已经提前告诉编译器不需要对参数进行修改。

void r1(const double &rx) { cout << "r1: " << rx << "\t"; cout << &rx << endl; }

void test02()

{

double w = 10.1;

int d = 5;

cout << "&w: " << &w << "\t" << "&d: " << &d << endl;

r1(w);

r1(w + 1);

r1(up(w));

r1(4);

r1(w);

r1(d);

}

void r0(int rx) {}

void r(int *rx){}

void r2(double &rx)

void r1(const double &rx) {}

左值作为参数

Y

Y

Y

Y

右值作为参数

Y

N

N

Y

高效(避免变量拷贝)

N

Y

Y

Y

变量本身修改

N

Y

Y

N

没有野指针风险

Y

N

Y

Y

现在看const 引用这种方式,对比原来的值传递,虽然只有一个优化,但没有任何不足。

这样就以上四种方式来说,只要使用引用和const 引用两种方式就可以了,相对于值传递和指针方式来说,都是可以绝对优化的。

但是综合这两种方式,还是有一种情况没有解决,对于右值又需要修改变量的情况怎么办?这可能本身就是一个伪命题,因为根据前面的定义,右值时临时值,没有固定的内存空间,也就不需要修改了。对于常规理解的常数、函数返回值、逻辑表达式这类表达式来说,显然是不需要作为变量修改的,也就没有需要修改右值这个问题。

但是C++ 又引入了一个将亡值,从下面可以看出将亡值是右值,但是又属于泛左值。所以需要不同于上面的引用和常量引用之外的方式,处理将亡值。

下面讨论右值引用和将亡值。

接着上面问题,如果我们需要将右值传入函数,并且要在函数中修改他,以上方法都是办不到的。

右值引用:

就是考虑将右值作为函数参数使用,同时又不对参数进行const 的限定方案。void r3(double &&rx) {} 其中两个&&表示函数参数为右值引用。

看到以下代码中只有r3(w)是不可用的。d在采用r3(std::move())方式可以使用,r3(std::move(d))调用中函数内的参数rx的地址和d 的地址是同一个,参数没有被复制。r3(std::move(w)),也可以调用,只是这种引用会重新开辟内存保存一个double 临时变量赋值5.  ( std::move() 是std 里面的一个函数,把一个左值变成右值,相当于告诉编译器,我需要把 w 变量对应的内存位置变成临时位置,把一个左值变成将亡值。)

总之采用这种参数定义方式,函数调用时传入的参数是右值。这种方式不用const 修饰,使得传入的量可以在函数中修改(下面例子中没有使用这个特性)。下面的这种右值引用作为参数的函数其实没有什么意义,只是可以理解右值引用的概念,不是右值引用作为函数参数的通常用法。

void r3(double &&rx) { cout << "r3: " << rx << "\t" << &rx << endl; }

void test04()

{

double w = 10;

int d = 5;

cout << "&w: " << &w << endl;

cout << "&d: " << &d << endl;

r3(std::move(w));

r3(std::move(d));

r3(w + 1);

r3(up(w));

r3(4);

//r3(w);

}

void r0(int rx) {}

void r(int *rx){}

void r2(double &rx)

void r1(const double &rx) {}

void r3(double &&rx)

左值作为参数

Y

Y

Y

Y

N

右值作为参数

Y

N

N

Y

Y

高效(避免变量拷贝)

N

Y

Y

Y

Y

变量本身修改

N

Y

Y

N

Y

没有野指针风险

Y

N

Y

Y

Y

在完善了右值引用功能,通过引用、const 引用、右值引用 三种定义函数参数的方式,实际上就实现了在高效的前提下处理各种参数需求。

问题是右值引用在什么情况下才会用到?

大多数情况下,使用右值引用作为函数参数,主要是用于实现移动构造。

下面这个自定义类,有4种构造函数,其中最后一种是移动构造,在把已有的类对象转化为另一个类对象,原对象又不需要保留时可以用移动构造实现,这样不用进行拷贝。

另一种,如果存在一个函数的返回值时Mycalss 类型的,如下Myclass moveclass(Myclass & cls) ,这段代码Myclass d(moveclass(b))在实例化Myclass d时,将调用Myclass(Myclass && cls) 直接将函数返回值的地址给d用,减少函数返回值最为临时值,在拷贝到d的构造和析构工作过程。

右值引用函数主要用作大型数据实现移动拷贝,提高程序效率。

    class Myclass
    {
    public:
        int m_a;
        char * chr;
        Myclass(){ std::cout << "default instruct func" << endl; }
        Myclass(int x, char name) { 
            m_a = x; 
            chr = new char [m_a]; 
            for (int i = 0; i < m_a; i++) { chr[i] = name; }
            std::cout << "normal instruct func" << endl; }
        Myclass(Myclass & cls)
        {
            m_a = cls.m_a;
            chr = new char[m_a];
            for (int i = 0; i < m_a; i++) { chr[i] = cls.chr[i]; }
            std::cout << "cppy instruct func" << endl; }
        Myclass(Myclass && cls) {
            m_a = cls.m_a;
            chr = cls.chr;
            cls.chr = nullptr;
            cls.chr = 0;
            std::cout << "move instruct func" << endl;
        }
        ~Myclass() { cout << "destruct func " << endl; }
    };
    Myclass moveclass(Myclass & cls) { return cls; }

    void test05()
    {
        Myclass a;
        Myclass b(3, 'b');
        Myclass c(b);
        Myclass d(std::move(b));
        moveclass(d);
    }

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值