一、左值、右值
左值右值是相对于等号表达式说的,左值是可以出现在等号表达式左边也可以出现在右边,右值是只能出现在等号表达式的右边。
有什么区别呢?明显的来说,等号表达式左边是可以被赋值的也就是可以被修改的,那么类似于常量这种,是不能放在左边的,只能放在右边;右值是不能被修改的,但是对于自定义类型来说,可以通过成员函数进行修改。
3=a; 3为常量,不能作为左值,只能为右值
a=3; 3作为右值
可以将 L-value
的L
, 理解成 Location
,表示定位,地址。将 R-value
的 R
理解成Read
,表示读取数据。现在的计算机数据放在内存。内存有两个很基本的属性:内存地址和内存里面放的数据。想象完全一样的箱子。每个箱子有个编号,用来区分到底是哪个箱子,箱子里面可以放东西。内存地址相当于箱子的编号,内存的数据,相当于箱子里面放的东西。
变量名编译之后,会映射成内存地址。看看a = b
的含义。其实就是 将 b
地址内存里面的数据,放到a
地址内存中。
左值是代表一个内存地址值(可以由用户访问的内存单元),并且通过这个内存地址,就可以对内存进行读并且写(主要是能写)操作;这也就是为什么左值可以被赋值的原因了。
相对应的还有右值:当一个符号或者常量放在操作符右边的时候,计算机就读取他们的“右值”,也就是其代表的真实值。简单来说就是,左值相当于地址值,右值相当于数据值。右值指的是引用了一个存储在某个内存地址里的数据。
int a,b;
b=0;
a=b;
以上代码很简单,我们看看它代表什么意思:首先定义a
,b
。然后对b赋值,此时计算机取b
的左值,也就是这个符号代表的内存位置即内存地址值,计算机取0
的右值,也就是数值0
;然后给a
赋值为b
,此时取b
的右值给a
的左值;
所以说,b
的左值、右值是根据他的位置来说的;
L-value中的L指的是Location,表示可寻, R-value中的R指的是Read,表示可读。
左值和右值是相对于赋值表达式而言的。左值表达式可以分为可读写的左值和只读左值。右值是可以出现在赋值表达式右边的表达式,他可以是不占据内存空间的临时量或字面量,可以是不具有写入权的空间实体。如
int a=3;
const int b=5;
a=b+2; a是左值,b+2是右值
b=a+2; 错!b是只读的左值但无写入权,不能出现在赋值符号左边
(a=4)+=28; a=4是左值表达式,28是右值,+=为赋值操作符
传统C++引用都是引用到一个左值,而不是右值,毕竟右值不能获得地址。
const int b=101;
明显b
是个右值,所以不能这样:int &pb=b;
因为b
是个不能修改的右值,而pb
引用证明可以通过pb
对b
进行修改,冲突了,所以是错的。
但是可以这么写:const int &pb=b;
没毛病,所以C++11新增了一个叫做 右值引用 的特性int &&pb=b;
,用两个&&
专门去引用右值。
int x=10;
int y=23;
int &&r1=13; 右值引用
int &&r2=x+y;
r2
关联的是x+y
计算得到的结果,这个结果一般放在一个临时的空间中,那么x
和y
的值再修改影响不到r2
;假设有int a=b+c;
那么b+c
的值将会被放在一个临时空间中,然后这个值被复制给a
的空间,然后这个临时空间就会被销毁;
那么我们来看看这句话int &&r2=x+y
;x+y
的临时空间会被销毁吗?不会的,因为右值引用导致该右值被储存的特定的位置!并且可以获得这个地址!
也就是说int a=2;
那么我们不能对2
进行取地址&
操作,因为2
的地址是个临时空间,会被销毁,;但是 int &&a=2
;那么这个2
将会被放到一个特定空间,不会被销毁,我们虽然不能对这个2
进行取地址操作,但是我们可以对关联它的a
进行取地址操作!说到底, 就是将一个右值(本来不能取地址)和某个特定的地址关联起来,使得可以通过这个地址访问它!
二、右值引用与移动语义
那么右值引用到底有什么用呢?刚才提到过,它可以将某个右值(通常是临时对象,常量)和特定地址关联起来,也就是延长了这个右值的生命周期(本来它要被销毁的),我们可以通过这个地址访问它。
借助于这个特性,我们来看看下列代码,
vector<string> allcaps(const vector<string> &vs)
{
vector<string> tmp(vs); 复制vs
return tmp; 返回中间变量
}
vector<string> vstr; 1 假设vstr是个含2000个字符串的数组,每个字符串元素长度为100
vector<string> vstr1(vstr); 2 调用复制构造函数
vector<string> vstr2(allcaps(vstr)); 3
上述代码:假设第一句话建立一个长度2000
,每个元素长度为100
的数组,第二句话将首先调用vector
类的复制构造函数,对新对象vstr1
数组进行复制,过程中string
对象又会调用string
类的复制构造函数对每个元素进行复制;
那么第三句话呢?由于allcaps
函数返回的是一个临时对象,它很大,有2000*100
个字符,第三句话将会根据这个临时对象重复第二句话的工作,将这么大的数据复制到一个新的空间并把它命名为vstr2
,然后销毁临时对象tmp
。那么更理想的是,直接将这个临时对象改名为vstr2
并且不销毁它,就避免了将这么大的数据来回移动要花费的时间。 这就是移动语义:避免了原始数据的移动,只是修改了记录。
所以说我们只需要将这个临时对象关联到一个地址上即可,这不就是右值引用吗?
---------------------------------------------移动语义示例--------------------------------------------------------------------
class Useless
{
private:
int n;
char * pc; 使用指针指向内容,需要深度赋值
static int ct; 静态变量,所有同类对象共用一个
public:
Useless(); 默认构造函数
explicit Useless(int k); 构造函数
Useless(int k, char ch); 构造函数
Useless(const Useless & f); 复制构造函数
Useless(Useless && f); 移动构造函数
~Useless(); 析构函数
Useless operator+(const Useless & f)const;
};
Useless::Useless(const Useless & f): n(f.n) 复制构造函数
{
++ct;
pc = new char[n];
for (int i = 0; i < n; i++)
pc[i] = f.pc[i];
}
Useless::Useless(Useless && f): n(f.n) 移动构造函数,这里采用右值引用
{
++ct;
pc = f.pc; // steal address
f.pc = nullptr;
}
Useless::~Useless()
{
delete [] pc;
}
上述代码:类使用了new
进行动态内存分配,析构函数要使用delete[],
否则造成内存泄露;复制构造函数没什么可说的,就是正常的;看看移动构造函数,没有进行内容的移动,就是将新对象的指针指向了临时的这个地址上(右值引用的f
对象),值得注意的是将临时对象的指针变为空,因为新对象和这个临时对象都将指向一个地址,在对象析构时对同一个地址进行两次delete会出错,所以将不用的临时对象的指针指向空即可。
同样的,C++11还 默认的提供了移动赋值运算符(上述的移动构造函数也默认提供)
-------------------------------------------------移动赋值运算符--------------------------------------------------------------
Useless & Useless::operator=(const Useless & f) // copy assignment
{
if (this == &f) 如果是a=a这种情况,那么直接返回,不单独列出来这种情况的话,下列代码会先把原数据删除,在复制新数据
return *this; 但是这种情况源数据和新数据是一个数据,会出错
delete [] pc;
n = f.n;
pc = new char[n]; 重新开辟地址储存
for (int i = 0; i < n; i++)
pc[i] = f.pc[i];
return *this;
}
Useless & Useless::operator=(Useless && f) // 移动赋值运算符
{
if (this == &f)
return *this;
delete [] pc;
n = f.n;
pc = f.pc; 直接指向这个引用,更改记录,不改地址
f.n = 0;
f.pc = nullptr;
return *this;
}
三、const与非const
const
代表不变的,被const
所修饰的值是不能被修改的,大大的提高了数据的安全性。
3.1 指针或引用使用const
下列代码,第一句话:表示不能通过指针pa
修改pa
指向的值,如果这里有个int *p=&a
,那么可以通过指针p
修改a
(如果a
没有被const
修饰);
第二句话:指针pa1
的指向不能变,这句话只能这么写,不能分开写成int *const pa1;pa1=&a;
因为指针pa1
从一定义下来,指针的指向就不能变,也就是 声明的同时进行初始化!
第三句话:将b
定义成不能修改的。
第四句话没问题,const
变量只能使用const
指针来指向。
第五第六句话:已经声明了b
是个不能修改的量,这时又用普通指针指向它,代表着可以通过指针修改它,这就矛盾了,所以是非法的操作。
int a=10;
const int *pa=&a; 1
int *const pa1=&a; 2
const int b=15; 3
const int *pb=&b; 4
int *const pb1=&b; 5 非法操作
int *pb2=&b; 6 非法操作
3.2 函数形参const,实参赋值问题
函数调用时,函数的形参如果是直接修改原值的类型(引用,指针),那么非const
形参无法接受const
类型实参,原因看上述第五句话。
若为值传递类型,这时会使用实参的副本,那么非const
形参可以接受const
实参和非const
实参。
const int b=5;
const int *pb=&b;
int fun1(int n); 采用的是值传递,使用的是实参的副本, fun1(b) 正确
int fun2(int &n); 引用类型,使用的是实参的本身,不能接受const类型 fun2(b) 错误
int fun3(int *n); 指针类型,使用的是实参本身,不能接受const类型 fun3(pb) 错误
3.3 类与const
const
修饰类的成员,这个成员表示不可修改的常量,意味着在对象创建的同时要对它进行初始化,否则一旦对象先创建,就不能赋值给它了,所以需要使用初始化列表进行初始化。
class A
{
public:
const int nValue; 成员常量不能被修改
…
A(int x): nValue(x) { } ; 构造函数,只能在初始化列表中赋值
}
我们来看下列类:定义了一个const
类对象test
,说到底,类是用户自定义类型,大部分的性质和基本类型没区别;这里的test
不能修改,在调用show
方法时,会报错,因为无法保证show()
函数不对test
对象进行修改!所以我们需要在show()
方法原型处后置加上const
,保证这个成员函数不修改对象。(函数原型与函数定义都要加,这里代码原型和定义一起了)
class A
{
public:
int a;
A(int a_):a(a_){};
void show() {cout<<a<<endl;} 1 换成void show() const {cout<<a<<endl;} 2
}
cosnt A test(10);
test.show(); 第一种写法报错,因为test对象不课修改的const类型,不能保证show方法不修改数据;第二种写法合法,保证不修改对象
const 数据成员
const 数据成员只在某个对象生存期内是常量,而对于整个类而言却是可变的。因为类可以创建多个对象,不同的对象其 const 数据成员的值可以不同。所以不能在类声明中初始化 const 数据成员,因为类的对象未被创建时,编译器不知道const 数据成员的值是什么。
const 数据成员的初始化只能在类的构造函数的初始化表中进行。要想建立在整个类中都恒定的常量,应该用类中的枚举常量来实现,或者static const。
static 数据成员
static 数据成员目的是作为类作用域的全局变量,被类里的所有对象共享,即使没有创建任何对象,该成员也存在。static成员变量不能在构造函数初始化列表中初始化,因为它不属于某个对象。在类的内部只是声明,定义必须在类定义体的外部,并且不能在函数体内,通常在类外定义时初始化,或者使用静态函数初始化。借用 gcc 的话:ISO C++ forbids in-class initialization of non-const static member
注意:static 成员变量的内存空间既不是在声明类时分配,也不是在创建对象时分配,而是在编译时在静态数据区分配内存,到程序结束时才释放。
const static 数据成员
const static 数据成员被一个类的所有对象共享,常量,可以在类内定义处初始化,也可以在类外初始化。
const 成员函数
const 成员函数主要是防止修改对象的成员变量(mutable 修饰的成员变量,static 变量除外)。即const成员函数不能修改成员变量的值,但可以访问成员变量。注意 const 成员函数只能保证不修改当前 this 指针所指的对象的成员变量,若通过参数传递进来有别的对象名,是可以修改其成员变量的,还有就是在 const 成员函数里通过 const_cast 移除 *this 的 const 特性后调用一些非 const 成员函数也有可能会改变 *this 对象的成员变量,虽然这种做法其实是错误的。
static 成员函数
static成员函数主要目的是作为类作用域的全局函数,不能访问类的非静态数据成员。类的静态成员函数没有this指针,这导致:
1、静态成员函数可以直接访问类的静态数据和函数成员,而访问非静态成员必须通过参数传递的方式得到一个对象名,然后通过对象名来访问,与其不同的是非静态成员函数可以任意地(非)静态成员函数和(非)静态数据成员;
2、不能被声明为virtual。
注意: 与数据成员不同,static 与 const 不能同时修饰成员函数
原因:const 修饰符用于表示函数不能修改成员变量的值,该函数必须是含有 this指针 的类成员函数,函数调用方式为 __thiscall ,而 static 函数是不含有 this指针 的,调用规约是 __cdecl 或 __stdcall ,两者是冲突的。