4.1 c++左值和右值、类型转换

左值和右值

  • c++的表达式不是右值就是左值。
  • 一个左值表达式的求值结果是一个对象或一个函数,然而以常量对象为代表的某些左值实际上不能作为赋值语句的左侧运算对象。此外,虽然某些表达式的求值结果是对象,但它们是右值而非左值。
  • 当一个对象被用作右值时,用的是对象的值(内容),当对象被用作左值的时候,用的是对象的身份(在内存中的位置)
  • 一个重要的原则是在需要右值的地方可以用左值来代替,但是不能把右值当作左值(也就是位置)使用。当一个左值被当作右值使用时,实际使用的是它的内容(值)。
    • 赋值运算符需要一个左值作为其左侧运算对象,得到的结果也仍然是一个左值。
    • 取地址符作用于一个左值运算对象,返回一个指向该运算对象的指针,这个指针是一个右值。
    • 内置解引用运算符、下标运算符、迭代器解引用运算符、string和vector的下标运算符的求值结果都是左值。
    • 内置类型和迭代器的递增递减运算符作用于左值运算对象,其前置版本所得的结果也是左值。
  • 使用关键字decltype时,左值和右值也各有不同。如果表达式的求值结果是左值,decltype作用于该表达式(不是变量)得到一个引用类型。
    • 假定p的类型是int*,因为解引用运算符生成左值,所以decltype(*p)的结果是int&。
    • 另一方面,因为取地址运算符生成右值,所以decltype(&p)的结果是int**,也就是说,结果是一个指向整型指针的指针。

类型转换

  • 在c++语言中,某些类型之间有关联。如果两种类型有关联,那么当程序需要其中一种类型的运算对象时,可以用另一种关联类型的对象或值来替代。如果两种类型可以相互转换,那么它们就是关联的。
int ival = 3.54 + 3;//编译器可能会报错损失了精度
  • 加法的两个运算对象不同,c++语言不会直接将两个不同类型的值相加,而是实现根据类型转换规则设法将运算对象的类型统一后再求值。上述的类型转换是自动执行的,因此它们被称作隐式类型转换。
  • 算术类型之间的隐式类型转换被设计的尽可能的避免损失精度。很多时候,如果表达式中既有整数类型的运算对象,又有浮点数类型的运算对象,整数会转换成浮点型。在上例中,3转换成double类型,然后执行浮点数加法,所得的结果类型是double
  • 接下来完成初始化,在初始化过程中,因为被初始化的对象的类型无法改变,所以初始值被转换成该对象的类型。在上例中,加法得到的double类型的结果转换成int类型,这个值被用来初始化ival。由double向int转换时忽略掉了小数部分,在上述表达式中,数值6被赋给了ival。
何时发生隐式类型转换
  • 在大多数情况下,比int类型小的整型值首先提升为较大的整数类型。
  • 在条件中,非布尔值转换成布尔类型。
  • 初始化过程中,初始值转换成变量的类型;在赋值语句中,右侧运算对象转换成左侧运算对象的类型
  • 如果算术运算或关系运算的运算对象有多种类型,需要转换成同一种类型。
  • 函数调用时也会发生类型转换。

算术转换

  • 算术转换的含义是把一种算术类型转换成另一种算术类型。
    • 算术转换的规则定义了一套类型转换的层次,其中运算符的运算对象将转换成最宽的类型。例如,如果一个运算对象的类型是long double,那么不论另外一个运算对象的类型是什么都会转换成long double。
    • 当表达式中既有浮点类型又有整数类型时,整数类型将转换成相应的浮点类型。
整型提升
  • 整型提升负则把小整数类型换成较大的整数类型。对于bool、char、signed char、unsigned char、short和unsigned short等类型来说,只要它们所有可能的值都存在int里,它们就会提升成int类型;否则,提升成unsigned int类型。
  • 较大的char类型提升成int、unsigned int、long、unsigned long、long long和unsigned long long中最小的一种类型,前提是转换后的类型要能容纳原类型所有可能的值。
无符号类型的运算对象
  • 如果某个运算符的运算对象类型不一致,这些运算对象将转换成同一种类型。但是如果某个运算对象的类型是无符号类型,那么转换的结果就要依赖于机器中各个整数类型的相对大小了。
  • 首先执行整型提升。如果结果的类型匹配,无需进行进一步的转换。如果两个运算对象的类型要么都是带符号的、要么都是无符号的,则小类型运算对象转换为较大的类型。
  • 如果一个运算对象是无符号类型、另外一个运算对象是带符号类型,而且其中的无符号类型不小于带符号类型,那么带符号的运算对象转换成无符号的。例如,假设两个类型分别是unsigned int 和int,则int类型的运算对象转换成unsigned int类型。需要注意的是,如果int类型为负值,它的转换将出现问题。
  • 如果带符号类型大于无符号类型,此时的转换结果依赖于机器。如果无符号类型的所有值都能存在该带符号类型中,则无符号类型的运算对象转换成带符号类型。如果不能,那么带符号类型的运算对象转换成无符号类型。例如,如果两个运算对象的类型分别为long和unsigned int,并且int和long的大小相同,则long类型的运算对象转换成unsigned int类型;如果long类型占用空间比int更多,则unsigned int类型的运算对象转换成long。

其它隐式类型转换

  • 数组转换成指针:在大多数用到数组的表达式中,数组自动转换成指向数组首元素的指针
int ia[10];//含有10个整数的数组
int *ip = ia;//ia转换成指向数组首元素的指针
  • 当数组被用作decltype关键字的参数,或者作为取地址符&、sizeof及typeid等运算符的运算对象时,上述转换不会发生。

  • 如果用一个引用来初始化数组,上述转换也不会发生。

  • 指针的转换:c++还规定了几种其他的指针转换方式,包括常量整数值0或者字面值nullptr能转换成任意指针类型;指向任意非常量的指针能转换成void*;指向任意对象的指针能转换成const void*.

  • 在有继承关系的类型间还有另外一种指针转换的方式。

  • 转换成布尔类型:存在一种从算术类型或指针类型向布尔类型自动转换的机制。如果指针或算术类型的值为0,转换结果为false;否则转换结果为true

  • 转换成常量:允许将指向非常量类型的指针转换成指向相应的常量类型的指针,对于引用也是这样。也就是说,如果T是一种类型,就能将指向T的指针或引用分别转换成指向const T的指针或引用。

int i;
const int &j = i;//非常量转换成const int的引用
const int *p = &i;//非常量的地址转换成const的地址
int &r = j, *q = p;//错误:不允许const转换成非常量
  • 相反的转换并不存在,因为它试图删除掉底层const

  • 类类型定义的转换:类类型能定义由编译器自动执行的转换,不过编译器每次只能执行一种类类型的转换。如果同时提出多个转换请求,这些请求将被拒绝。

string s, t = "a value";//字符串字面值转换成string类型
while(cin >> s)//while的条件部分把cin转换成布尔值

显式转换

  • 有时希望显式地将对象强制转换成另外一种类型,这种方法称作强制类型转换。
命名的强制类型转换
  • 一个命名的强制类型转换具有如下形式
cast-name<type>(expression);
  • 其中,type是转换的目标类型,而expression是要转换的值。如果type是引用类型,则结果是左值。
  • cast-name是static_cast、dynamic_cast、const_cast和reinterpret_cast中的一种。dynamic_cast支持运行时类型识别,cast_name指定了执行的是哪种转换。
ststic_cast
  • 任何具有明确定义的类型转换,只要不包含底层const,都可以使用static_cast.
int i,j;
double slop = static_cast<double>(j) / i;//进行强制类型转换以便执行浮点数除法
  • 当需要把一个较大的算术类型赋值给较小的类型时,static_cast非常有用。此时,强制类型转换告诉编译器,我们知道且不在乎潜在的精度损失。一般来说,如果编译器发现了一个较大的算术类型试图赋值给较小的类型,就会给出警告信息;但是当执行了显式类型转换后,警告信息就会被关闭了。
  • static_cast对于编译器无法自动执行的类型转换非常有用。例如,可以使用static_cast找回存在于void*指针中的值。
void *p = &d;//正确:任何非常量对象的地址都能存入void*
double *dp = static_cast<double*>(p);//正确:将void*转换回初始的指针类型
  • 把指针存放在void*中,且使用static_cast将其强制转换回原来的类型时,应该确保指针的值保持不变。也就是说,强制类型转换的原地址相等,因此必需确保转换后所得的类型就是指针所指的类型。类型一旦不符,将产生未定义的后果。
const_cast
  • const_cast只能改变运算对象的底层const
const char *pc;
char *p = const_cast<char*>(pc);//正确:但是通过p写值是未定义的行为
  • 对于将常量对象转换成非常量对象的行为,一般称其为"去掉const性质"。一旦去掉了某个对象的const性质,编译器就不再阻止我们对该对象进行写操作了。如果对象本身不是一个常量,使用强制类型转换获得写权限是合法的行为。然而如果对象是一个常量,再使用const_cast执行写操作就会产生未定义的后果。
  • 只有const_cast能改变表达式的常量属性,使用其它形式的命名强制类型转换改变表达式的常量属性都将引发编译器错误。同样的,也不能用const_cast改变表达式的类型
const char *cp;
char *q = static_cast<char*>(cp);//错误:static_cast不能转换掉cast性质
static_cast<string>(cp);//正确:字符串字面值转换成string类型
const_cast<string>(cp);//错误:const_cast只改变常量属性
  • const_cast常常用于有函数重载的上下文中。
reinterpreter_cast
  • reinterpret_cast通常为运算对象的位模式提供较低层次上的重新解释。假设有如下转换:
int *ip;
char *pc = reinterpret_cast<char*>(ip);
  • pc所指的真实对象是一个int而非字符,如果把pc当成普通的字符指针使用就可能再运行时发生错误,例如:
string str(pc);
  • 可能导致异常的运行时行为。
  • 使用reinterpret_cast是非常危险的,用pc初始化str的例子很好的证明了这一点。其中关键问题是类型改变了,但编译器没有给出任何警告或错误的提示信息。
  • 当用一个int的地址初始化pc时,由于显式的声称这种转换合法,所以编译器没法知道它实际存放的是指向int的指针。最终的结果是,在上例中虽然用pc初始化str没什么实际意义,甚至还可能引发更糟糕的后果,但仅从语法上而言,这种操作没错。
  • 查找这类问题的原因非常困难,如果将ip强制转换成pc的语句和用pc初始化string对象的语句分属不同文件就更是如此。
  • reinterpret_cast本质上依赖于机器。要想安全的使用reinterpret_cast必需对涉及的类型和编译器首先的转换过程非常了解。

建议:避免强制类型转换
强制类型转换干扰了正常的类型检查,强烈建议程序员避免使用强制类型转换。在有重载函数的上下文中使用const_cast无可厚非,但在其他情况下使用conat_cast也就意味着程序的某种设计缺陷。其它强制类型转换,都应该反复斟酌能否以其他方式实现相同的目标。就算实在无法避免,也应该尽量限制类型转换的作用域,并且记录对相关类型的所有假定,这样可以减少错误发生的机会。

旧式的强制类型转换
  • 在早期版本的c++语言中,显式的进行强制类型转换包含两种形式
type(expr);//函数形式的强制类型转换
(type) expr;//c语言风格的强制类型转换
  • 根据所涉及的类型不同,旧式的强制类型转换分别具有与const_cast、static_cast和reinterpret_cast相似的行为。在某处执行旧式的强制类型转换时,如果换成const_cast和static_cast也合法,则其行为与对应的命名转换一致。如果替换后不合法,则旧式强制类型转换执行与reinterpret_cast类似的功能的效果与使用reinterpret_cast一样
char *pc = (char*) ip;//ip是指向整数的指针

与命名的强制类型转换相比,旧式的强制类型转换从表现形式上来说不那么清晰明了,所以一旦转换过程出现问题,追踪起来也更加困难。

你可能感兴趣的:(C++,c++)