栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > C/C++/C#

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

C/C++/C# 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

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

一、左值、右值

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

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

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 allcaps(const vector &vs)
{
	vector tmp(vs);        复制vs
	return tmp;                    返回中间变量
}
vector vstr;        1   假设vstr是个含2000个字符串的数组,每个字符串元素长度为100
vector vstr1(vstr);   2   调用复制构造函数
vector 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<
转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/429581.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号