第四章 表达式

第四章 表达式C 之表达式 表达式

大家好,欢迎来到IT知识分享网。

4.1 表达式基础

4.1.1 基本概念

  1. 表达式由一个或多个运算对象(operand)组成,对表达式求值将得到一个结果。
  2. 字面值和变量是最简单的表达式。
  3. 把运算符和运算对象结合起来可以生产较复杂的表达式。
  4. 运算符
    一元运算符:作用于一个运算对象的运算符,如取地址符(&)和解引用符
    二元运算符:作用于两个运算对象的运算符,如相等(==)和乘法(*)
    三元运算符:作用于三个运算对象的运算符,?:


  5. 运算符重载:为已存在的运算符赋予另外一层含义。
    当运算符作用于类类型的运算对象时,用户可以自行定义其含义。
    例:IO库的>>和<<,string对象、vector对象和迭代器使用的运算符。
    运算对象的个数、运算符的优先级、结合律都是无法改变的。


4.1.2 左值和右值

  1. C++表达式要么是左值,要么是右值。
  2. C语言:左值可以位于赋值语句的左侧、右值不能。C++语言中,要复杂得多。
    右值:取不到地址的表达式;
    左值:能取到地址的表达式;

  3. 常量对象为代表的左值不能作为赋值语句的左侧运算对象。
  4. 某些表达式的求值结果是对象,但它们是右值。
  5. 当一个对象被用作右值的时候,用的是对象的值(内存中的内容),当对象被用作左值的时候,用的是对象的身份(在内存中的位置)。
  6. 在需要右值的地方可以用左值来代替,但是不能把右值当成左值(也就是位置)使用。当一个左值被当成右值使用时,实际使用的是它的内容(值)。
  7. 如果表达式的求值结果是左值,decltype作用于该表达式(不是变量)得到一个引用类型。
  8. 用到左值的运算符:
    赋值运算符需要一个(非常量)左值作为其左侧运算对象,得到的结果也仍然是一个左值。
    取地址符作用于一个左值运算对象,返回一个指向该运算对象的指针,这个指针是一个右值。
    内置解引用运算符、下标运算符、迭代器解引用运算符、string和vector的下标运算符的求值结果都是左值。
    内置类型和迭代器的递增递减运算符作用于左值运算对象,其前置版本所得的结果也是左值。



4.1.3求值顺序

  1. 表达式 的计算结果,依赖运算符的优先级(precedence)、结合律(associativity) 以及运算对象的求值顺序(order of evaluation)。
  2. 大多数情况下,不会明确指定求值顺序。
int i = f1() * f2()//f1和f2将在乘法之前被调用,但是不知道谁前谁后 
  1. 对于没有指定执行顺序的运算符来说,如果表达式指向并修改了同一个对象,将会引发错误并产生未定义的行为。
int i = 0; cout << i << “ “ << ++i <<endl; //未定义的 
  1. 处理复合表达式的两条经验:
    拿不准的时候最好用括号来强制让表达式的组合关系符合程序逻辑的要求。
    如果改变了某个运算对象的值,在表达式的其他地方不要再使用这个运算对象。例外情况:当改变运算对象的子表达式本身就是另外一个子表达式的运算对象时该规则无效。如:*++i;

4.2 算术运算符

在这里插入图片描述

  1. 算术运算符的运算对象和求值结果都是右值。
  2. 除法运算,商一律向0取整,即直接切除小数部分(C++11)。
  3. (-m)/n和m/(-n)都等于-(m/n),m%(-n)等于m%n,(-m)%n等于-(m%n)。
  4. 如果m和n是整数且n非0,则(m/n)*n+m%n == m。
  5. 一元负号运算符,获取运算对象的副本(可能需要提升),返回其负值。
int i = 1024; int k = -i; //k是-1024 bool b = true; bool b2 = -b; //true,-b为-1,,是提升后的副本 
  1. 运算符%俗称“取余”或“取模”运算符,计算两个整数相除所得的余数。
int ival = 42; double dval = 3.14; ival % 12; //正确:运行结果是6 ival % dval; //错误 

4.3 逻辑和关系运算符

在这里插入图片描述

  1. 逻辑和关系运算符的运算对象和求值结果都是右值。
  2. 逻辑与运算符左侧运算对象是为了确保右侧运算对象求值过程的正确性和安全性。
  3. 关系运算符都满足左结合律。
  4. 进行比较运算时除非比较的对象是布尔类型,否则不要使用布尔字面值true和false作为运算对象。
  5. 测试一个算术对象或指针对象的真值:
if (val) { 
   } //如果val是任意的非0值,条件为真 if (!val) { 
   } //如果val是0,条件为真 

4.4 赋值运算符

  1. 赋值运算符的左侧运算对象必须是一个可修改的左值。
  2. 如果左侧运算对象是内置类型,那么初始化列表最多只能包含一个值,而且该值即使转换的话其所占空间也不应该大于目标类型的空间。
int i = 0, j = 0, k = 0; //初始化而非赋值 const int ci = i; //初始化而非赋值 1024 = k; //错误 i + j = k; //错误:算术表达式是右值 ci = k; //错误 k = 0; k = 3.14159; //结果为3 //C++11允许使用列表作为赋值语句右侧的运算对象 k = { 
    3.14 }; //错误:窄化转换 vector<int> vi; //初始为空 vi = { 
    0,1,2,3,4,5,6,7,8,9 }; 
  1. 赋值运算满足右结合律。
int ival, jval; ival = jval = 0; //正确:都被赋值为0 int* pval; ival = pval = 0; //错误:不能把指针赋值给int string s1, s2; s1 = s2 = "OK"; //字符串字面值"OK"转换成string对象 
  1. 因为赋值运算符的优先级低于关系运算符的优先级,所以在条件语句中,赋值部分通常应该加上括号。
int i; while ((i = get_value()) != 42) { 
    //其他处理... } 

4.5 递增和递减运算符

  1. 前置版本将对象本身作为左值返回,后置版本则将对象原始值的副本作为右值返回。
int i = 0, j; j = ++i; // j=1,i=1:前置版本,得到递增之后的值 j = i++; // j=1,i=2;后置版本,得到递增之前的值 
  1. 建议:除非必须,否则不用递增递减运算符的后置版本。前置版本把值加减1后直接返回改变了的运算对象,而后置版本需要将原始值存储下来以便于返回这个未修改的内容。前置版本少创建了一个临时对象。

4.6 成员访问运算符

  1. 点运算符和箭头运算符都可以访问成员
ptr->mem == (*ptr).mem 
  1. 箭头运算符作用于一个指针类型的运算对象,结果是一个左值。
  2. 点运算符分成两种情况:如果成员所属的对象是左值,那么结果是左值,反之是右值。

4.7 条件运算符

  1. cond?expr1:expr2
  2. 当条件运算符的两个表达式都是左值或能转换成同一种左值类型时,运算的结果是左值,否则运算的结果是右值。
  3. 条件运算符满足右结合律。
  4. 条件运算的嵌套最好别超过两到三层。
string finalgrade = (grade > 90) ? "high pass" : (grade < 60) ? "fail" : "pass"; 
cout << ((grade < 60) ? "fail" : "pass"); //输出fail或pass cout << (grade < 60) ? "fail" : "pass"; //输出1或0 //等价于 //cout << (grade < 60); //cout ? "fail" : "pass"; cout << grade < 60 ? "fail" : "pass"; //错误:试图比较cout和60 // 等价于 //cout << grade; //cout < 60 ? "fail" : "pass"; 

4.8 位运算符

在这里插入图片描述

  1. 位运算符作用于整数类型的运算对象,并把运算对象看成是二进制位的集合。
  2. 位运算符提供检查和设置二进制位的功能。
  3. 关于符号位如何处理没有明确的规定,所以强烈建议仅将位运算符用于处理无符号类型。
  4. 如果运算对象是“小整形”、则它的值会被自动提升。
  5. 二进制左移或右移,移出边界之外的位就被舍弃掉了。
  6. 移位运算符(又叫IO运算符)满足左结合律,优先级介于中间(低于算术运算符、高于关系运算符)。
//假设char占8位,int占32位 unsigned char bits = 0233; //八进制,二进制为: bits << 8; //bits提升为int型,然后向左移动8位 bits = 0227; // ~bits; unsigned char b1 = 0145; //0 unsigned char b2 = 0257; // b1 & b2; //24个高位都是0, 00 b1 | b2; //24个高位都是0, b1 ^ b2; //24个高位都是0, 

4.9 sizeof运算符

  1. sizeof运算符满足右结合律,其所得值是一个size_t类型的常量表达式。
  2. sizeof运算符返回一条表达式(表达式结果类型的大小)或一个类型名字所占的字节数。
  3. sizeof并不实际计算其运算对象的值。
  4. sizeof运算不会把数组转换成指针来处理。
Sales_data data, * p; sizeof(Sales_data); //存储Sales_data类型的对象所占的空间大小 sizeof data; //data的类型大小,即sizeof(Sales_data) sizeof p; //指针所占的空间大小 //sizeof和*优先级一样,并且满足右结合律 //所以下面等价于sizeof (*p) //p可以是无效指针,并不会执行解引用 sizeof *p; //p所指类型的空间大小,即sizeof(Sales_data) sizeof data.revenue; //Sales_data的revenue成员对应类型的大小 sizeof Sales_data::revenue; //另一种获取revenue大小的方式 // 使用作用域运算符获取类成员的大小。(C++11) //sizeof运算能够得到整个数组的大小 constexpr size_t sz = sizeof(ia) / sizeof(*ia); int arr2[sz]; //正确:sizeof返回一个常量表达式 

4.10 逗号运算符

  1. 逗号运算符含有两个运算对象,按照从左向右的顺序依次求值。结果是右侧表达式的值。
//逗号运算符经常被用在for循环当中 vector<int> ivec = { 
    1,2,3,4 }; vector<int>::size_type cnt = ivec.size(); //将把从size到1的值赋给ivec的元素 for (vector<int>::size_type ix = 0; ix != ivec.size(); ++ix, --cnt) ivec[ix] = cnt; 

4.11 类型转换

4.11.1 算术转换

bool flag; char cval; short sval; unsigned short unsval; int ival; unsigned int uival; long lval; unsigned long ulval; float fval; double dval; 3.14159L + 'a’; //‘a'提升成int,然后该int值转换成long double dval + ival; //ival转换成double dval + fval; //fval转换成double ival = dval; //dval转换成(切除小数部分后)int flag = dval; //如果dval是0,则flag是false,否则flag是true cval + fval; //cval提升成int,然后该int值转换成float sval + cval; //sval和cval都提升成int cval + lval; //cval转换成long ival + ulval; //ival转换成unsigned long unsval + ival; //根据unsigend short和int所占空间的大小进行提升 uival + lval; //根据unsigned int和long所占空间的大小进行转换 

4.11.2 其他隐式类型转换

  1. 数组转换成指针:当数组被用作decltype关键字的参数,或者作为取地址符(&)、sizeof及typeid等运算符的运算对象时,数组不会转换成指针。
  2. 指针的转换
    常量整数值0或者字面值nullptr能转换成任意指针类型。
    指向任意非常量的指针能转换成void*。
    指向任意对象的指针能转换成const void*。


  3. 转换成常量:允许将指向非常量类型的指针转换成指向相应的常量类型的指针,对于引用也是这样。但相反的转换并不存在,因为它试图删除掉底层const。
/*数组转换成指针*/ int ia[10];//含有10个整数的数组 int *ip = ia;//ia转换成指向数组首元素的指针 /*转换成布尔类型*/ char *cp = get_string(); if (cp) /*...*/ //如果指针cp不是0,条件为true while (*cp) /*...*/ //如果*cp不是空字符,条件为true /*转换成常量*/ 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转换成布尔值 

4.11.3 显式转换

  1. 旧式的强制转换
char *pc = (char*) ip;//ip是指向整型的指针,在这里与reinterpret_cast一样 
  1. 命名的强制类型转换: cast-name (expression)
    cast-name是static_cast、dynamic_cast、const_cast和reinterpret_cast中的一种。
  2. static_cast:任何具有明确定义的类型转换,只要不包含底层const,都可以使用static_cast。
    当需要把一个较大的算术类型赋值给较小的类型时,static_cast非常有用
static_cast对于编译器无法自动执行的类型转换也非常有用。 /*static_cast:只要不包含底层const,都可以使用*/ //进行强制类型转换以便指向浮点数除法 int i, j; double slope = static_cast<double>(j) / i; double d; void *p = &d;//正确:任何非常量对象的地址都能存入void* double *dp = static_cast<double*>(p);//正确 
  1. const_cast:只能改变运算对象的底层const。
    将常量对象转换成非常量对象,称为“去掉const性质”。
    只有const_cast能改变表达式的常量属性,使用其他形式的命名强制类型转换改变表达式的常量属性都将引发编译器错误。
    不能用const_cast改变表达式的类型。
    const_cast常常用于有函数重载的上下文中。



/*const_cast:只能改变运算对象的底层const*/ const char *pc; char *p = const_cast<char*>(pc); //正确:对象本身是const,通过p写值是未定义的行为 const char *cp; char *q = static_cast<char *>(cp); //static_cast不能转掉const性质 static_cast<string>(cp); //正确 const_cast<string>(cp); //错误 
  1. reinterpret_cast:通常为运算对象的位模式提供较低层次上的重新解释。
    reinterpret_cast本质上依赖于机器。
/*reinterpret_cast:为运算对象的位模式提供较低层次上的重新解释*/ int* ip; char* pc = reinterpret_cast<char*>(ip); //效果类似C风格的强制转换 
  1. dynamic_cast:和继承及运行时类型识别一起使用。
  2. 建议:避免强制类型转换,因为干扰了正常的类型检查。

免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://haidsoft.com/121838.html

(0)
上一篇 2025-10-21 11:26
下一篇 2025-10-21 11:33

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注微信