C++数组处理以及左值与右值探究

来源:互联网 发布:linux 挂载ntfs硬盘 编辑:程序博客网 时间:2024/06/14 08:16

C++对字符数组的处理和一般数组不同,如果不弄清楚,使用过程中就有可能犯迷糊。
那么究竟有什么不同呢?先看下一般数组的情况。

一般数组

    int a[4]{ 1,2,3,4 };    int b{ 2 };    cout << a << endl//输出第一个元素地址        << &a << endl//输出第一个元素地址        << &a[0] << endl //输出第一个元素地址        << *(&a) << endl//输出第一个元素地址        << &(*a) << endl//输出第一个元素地址        << *a << "  " << *&(*a) << "  " //输出第一个元素        <<  << "  " << a[0] << endl;//输出第一个元素

这段代码可以通过VS2013的编译。对于数组的操作无疑有两种,一是获取数组地址,二是操作数组元素,然而获取数组地址的最终目的还是操作数组元素。
〈1〉代码中获取第一元素地址用了5种方法,常用的只有一种cout<<a,第二元素的址就是a+1,第n个元素地址就是a+(n-1)。
〈2〉.如果第一元素的物理地址是0038FC60,那第二元素则是0038FC64,为什么不是0038FC61呢?
因为int类型数据占用4个字节,所以后一元素地址都比前一元素递增4,所以第n个元素物理地址实际是a+(n-1)*sizeof(int),其中sizeof(int)返回int类型数据的字节数。
〈3〉继续研究这段代码,cout<<&a的用法和字符数组是相同的,但是字符数组如果写成这样cout<<ch(假设char ch[10]{})那输出就是整个字符串。使用数组名称时,编译器自动将其隐式转换为指针,存储的地址是数组的首地址。
〈4〉*(&a) 和&(*a)其实就是a,因为间接寻址运算符“*”和取址运算符“&”互为逆运算,相互抵消还原为a。同理*&(*a)和**(&a)也是如此,从右至左运算,最后都*a,表达的都是第一个元素值。访问元素常用的方法还是a[0],第n个元素就是a[n-1]。
采用地址偏移法也能访问数组中的其他元素,如*(a+1)就是第二元素a[1].
〈5〉但是如果用&(a+1)获取第二元素地址就会出错,提示“&”操作的对象必须是左值。那么这里要讨论一下什么是左值和右值。

左值和右值

C++没有严格定义左值和右值,简单来说:
(1)左值是内存中持续存储数据的地址,右值是临时存储的表达式结果,因此,左值能够被寻址,而右值不能。
(2)左值可以出现在赋值运算符“=”的左边或右边,但右值只能出现在“=”右边,因此,左值的内存数据可以被修改,不能修改的表达式不是左值。
(3)只包含一个命名变量的表达式始终是左值,一般来说,由运算符连接的多变量表达式是右值。
(4)const 修饰的变量是左值,但它是唯一不能出现在“=”左边的左值。
a[0]=b, a[0],b都是左值
b=b+1, b+1是右值
a[0]=++b, ++b是左值
a[1]=b++, b++是右值
const int val{3};val是左值,必须初始化,不能出现在=左边,因此不能修改其值
为什么++b是左值而b++是右值呢?
a[0]=++b,b是自增后再传递给a[0],本质还是a[0]=b.
a[1]=b++,b先临时传递给a[1]后再自增,因此a[1]得到是b的临时原值,而不是b,这之后a[0]!=b。
经过这番探究之后,终于明了&(a+1)出错的原因。如果要获取第二个元素地址,直接使用a+1就行,或者&a[1],当然还可以用static_cast<void*>(a + 1),这个方法在后面研究字符数组时还要讲到,这方法对于一般数组作用不大,但对于字符数组却不可或缺。

字符数组

char buffer[]{"My name is Lily."};    cout << buffer << endl//My name is Lily        << &buffer << endl//“M”的物理地址        << buffer[0] << endl//My name is Lily        << &buffer[1] << endl//y name is Lily        <<buffer+1<<endl;//y name is Lily

输出结果写在注释里,对比一般数组有以几点发现:
<1>一般数组单独使用数组名称时,输出是第一元素地址,而字符数组单独单独使用数组名称时,输出是整个字符串。
<2>为方便描述,暂时用“_name”代替数组名称。一般数组,&_name[index]表示元素地址,而字符数组则表示为从_name[index]元素开始到结束的字符串。与第<1>有共通点,一般数组表示地址的,字符数组表示字符串。
<3>字符数组不能像一般数组一样使用_name+index(数组名称+索引) 得到元素地址,只能用static_cast<void*>(_name+index)
static_cast<void*>(&_name[index]).
<4>对于两类数组,_name[index]都表示访问索引为index的元素。
<5>&buffer+1表示紧跟字符数组后的一个地址,如例中字符数组长度为16+1个字节(16是可见的字符,1表示一个终止符’\0’,每个字符占用一个字节),因此&buffer+1对应的物理地址实际为&buffer+17.

总结
以上所写有点混乱,而且一些用法不是常用甚至从来不用的。从用法来总结:

操作 一般数组 字符数组 访问单个元素 _name[index] _name[index] 访问整个数组 遍历所有元素 使用数组名称_name 访问单个元素地址1 &_name[index] static_cast<void*>(&_name[index]) 访问单个元素地址 2 static_cast<void*>(_name+index) static_cast<void*>(_name+index) 整个数组地址 _name &_name

扩展:左值引用与右值引用

单纯的左右值并没有意义,但左传引用和右值引用却是个非常有趣的话题。要想比较清楚的了解这两个概念,必须要先说函数的参数传递。
函数参数传递
假设这个函数的原型所对应的定义是实现两数之和,那么调用这个函数时,实参是以按值传递”的方式传递给函数的。

#include<iostream>using namespace std;double sum(double, double);void main(){    double val1{ 21 };    double val2{ 31 };    cout << val2 << " ";    cout << sum(val1, val2) << " ";    cout << val2 << endl;}double sum(double rval1, double rval2){    return rval1 + rval2++;}

〈1〉按值传递
所谓按值传递,就是创建实参的副本,函数使用的就是这个副本而不是实参本身。因此,函数表面上所有针对实参的操作,实际操作的对象是实参的副本,与实参本身毫无关系。那么,函数想修改实参的值是不可能实现的,因为它修改的实参副本的值,函数结束后,这个副本会被销毁。如果函数想实现修改实参的功能,那么只能使用指针或者引用。

〈2〉指针
假设函数想实现两数之和,然后val2的值增加1,我们可以这样做:

#include<iostream>using namespace std;double sum(double, double*);void main(){    double val1{ 21 };    double val2{ 31 };    double* pval2{ &val2 };    cout << val2 << " ";    cout << sum(val1, pval2) << " ";    cout << val2 << endl;}double sum(double val, double* pval){    return val + (*pval)++;            //括号是必须的,后缀++的优先级比间接寻址运算符“*”高}

实际上这还是按值传递,函数创建了指针的副本,副本指针与实参指针指向同一个对象val2,因此副本指针解除引用后的自增运算,间接操作的对象就是val2.这种方式虽然可以实现我们的意图,但实则还是与按值传递并无二致。
按值传递存在很大的缺陷,创建副本会产生额外的系统开销,如果实参是一个拥有大量数据成员的类对象,那么按值传递的这个缺陷就会十分明显。
有没有一种方法,可以让函数直接使用实参本身呢?当然有,这种方法就是引用。
〈3〉左值引用
引用,分为左值引用和右值引用。
左值引用,通俗点就是别名,外号。例如:

double val2{31};          //val2是一个左值double& rval2{val2};             //定义val2的左值引用

rval2是val2的一个别名,它们是同一个变量。因此上面的函数可以这样改:

#include<iostream>using namespace std;double sum(double&, double&);void main(){    double val1{ 21 };    double val2{ 31 };    cout << val2 << " ";    cout << sum(val1, val2) << " ";    cout << val2 << endl;}double sum(double& rval1, double& rval2){    return rval1 + rval2++;}

对比其他两种情况,我们不再需要定义额外的变量,使用起来就跟按值传递时没什么两样,但因为函数调用时使用的是变量的别名(左值引用),也没创建副本的步骤,系统开销比以上两种情况都要小。
那么什么是右值引用呢?
〈4〉右值引用
右值是一个临时变量,通常是表达式的求值结果,不能常驻内存。下面定义一个右值引用:

double&& val{val1+val2};

事实上这样定义一个右值引用毫无意义,我们完全可以这样做:double val{val1+val2};而且double&& val{val1+val2}所定义的val本身是一个左值。当然我们不可能定义这样的变量double& val{val1+val2},无法用右值初始化一个左值引用。
似乎右值引用没什么用处了?当然不是,上面的定义仅仅表示右值引用的形式而已,右值引用的巨大作用体现在函数的参数传递中。我们重写sum()函数:

double sum(double&& rval1,double& rval2){return rval1 + rval2++;}

sum()第一个参数现在只能接受右值实参,而不能接受左值。
比如:sum(12+val1,val2)这样的调用,第一个形参直接引用12+val1计算出的结果所在的内存(临时存储)。对比下面左值引用的情形的形式:

val1+=12sum(val1,val2);                       //左值引用的形式

左值引用必须增加一步才能实现右值引用的效果,当然这也仅仅是演示右值引用在函数参数传递的应用而已,实际上并没有体现出右值引用的巨大作用。
正如左值引用一样,右值引用真正的用武之地也是对类的操作。

1 0
原创粉丝点击