【C++ const与非const、左值与右值、移动语义】

一、左值、右值

左值右值是相对于等号表达式说的,左值是可以出现在等号表达式左边也可以出现在右边,右值是只能出现在等号表达式的右边。

有什么区别呢?明显的来说,等号表达式左边是可以被赋值的也就是可以被修改的,那么类似于常量这种,是不能放在左边的,只能放在右边;右值是不能被修改的,但是对于自定义类型来说,可以通过成员函数进行修改。

3=a;               3为常量,不能作为左值,只能为右值
a=33作为右值

可以将 L-valueL, 理解成 Location,表示定位,地址。将 R-valueR理解成Read,表示读取数据。现在的计算机数据放在内存。内存有两个很基本的属性:内存地址和内存里面放的数据。想象完全一样的箱子。每个箱子有个编号,用来区分到底是哪个箱子,箱子里面可以放东西。内存地址相当于箱子的编号,内存的数据,相当于箱子里面放的东西。

变量名编译之后,会映射成内存地址。看看a = b的含义。其实就是 将 b地址内存里面的数据,放到a地址内存中。

左值是代表一个内存地址值(可以由用户访问的内存单元),并且通过这个内存地址,就可以对内存进行读并且写(主要是能写)操作;这也就是为什么左值可以被赋值的原因了。

相对应的还有右值:当一个符号或者常量放在操作符右边的时候,计算机就读取他们的“右值”,也就是其代表的真实值。简单来说就是,左值相当于地址值,右值相当于数据值。右值指的是引用了一个存储在某个内存地址里的数据。

int a,b;
b=0;
a=b;

以上代码很简单,我们看看它代表什么意思:首先定义ab。然后对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引用证明可以通过pbb进行修改,冲突了,所以是错的。
但是可以这么写: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计算得到的结果,这个结果一般放在一个临时的空间中,那么xy的值再修改影响不到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 ,两者是冲突的。

  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值