普通函数
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);
}