ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

【C 语言】操作符详解(下):逗号、下标、结构体、类型转换与表达式求值

【C 语言】操作符详解(下):逗号、下标、结构体、类型转换与表达式求值 星光编译者· 个人主页 学习专栏《C/C 成长笔记》·《Linux 实践手册》·《数据结构与算法》 向云端飞扬编译属于自己的代码星河。☕ 写在开篇你好这里是星光编译者。这里记录我在C/C、Linux、数据结构与算法学习中遇到的真实问题、亲手验证过的代码以及那些容易被忽略的实现细节。比起简单罗列结论我更愿意从问题出发把一个知识点的来由讲清楚把“为什么会这样”和“应该怎样解决”说明白让每一次踩坑都沉淀成可以复用的经验。如果这篇记录能帮你少绕一点路或让某个模糊的地方忽然变得清晰那么这次分享便有了意义。愿我们在一次次阅读、编译与调试中稳步向前慢慢搭起属于自己的技术世界。本文定位接续上篇的位运算读懂逗号表达式、[]、函数调用()、结构成员访问.与-再解决更核心的问题表达式被怎样分组并不等于各部分按什么时间顺序执行。学习目标会计算逗号表达式能正确访问数组与结构体成员理解优先级、结合性、整数提升和通常算术转换识别未定义行为与未指定求值顺序并把复杂表达式改成可预测的代码。阅读说明根据课程 PDF《第10讲操作符详解》第 611 节整理。上篇已解释进制、补码、移位与位运算。本篇按 C11 讨论规则对讲义中的示例做了必要的语法修正和边界说明。文章目录一、单目操作符一个操作数多种角色二、逗号表达式明确规定的从左到右三、下标访问与函数调用符号相近规则不同四、结构成员访问.与-怎么选五、优先级与结合性解决的是分组六、整数提升与通常算术转换七、表达式求值分组之外还要看求值顺序八、把风险表达式改写成可预测的代码九、高频问答与练习总结一、单目操作符一个操作数多种角色“单目”只表示它处理一个操作数并不表示只有一种用途。课程列出!、、--、、*、一元、一元-、~、sizeof和类型转换(类型)等。写法含义例子!x逻辑非零变真、非零变假!0为1x/x增加 1返回值时机不同前置返回更新后的值后置返回原值--x/x--减少 1返回值时机不同同上x获取对象地址int *p x;*p通过指针访问对象*p 10;~x按位取反~0Usizeof x求类型或表达式结果类型的大小结果类型为size_t(double)x显式转换类型让除法按浮点数计算例如intx3;intax;/* x4a4 */intbx;/* b4之后 x5 */这里的前置和后置区别只说明表达式的值。不要据此推断它与同一个大表达式里其他子表达式的执行先后更不要把x x当成普通算术题。sizeof也有一个常见陷阱它通常不会求值其操作数表达式可变长数组的相关情况是例外但会根据类型决定字节数。sizeof array对真实数组得到整个数组大小数组作为函数形参时会调整为指针类型不能再用sizeof 参数得到原数组长度。二、逗号表达式明确规定的从左到右逗号操作符把两个表达式连接起来先求左边并完成其副作用再求右边整个逗号表达式的值是右边表达式的值。多个逗号连续出现时按左结合分组可以按从左到右跟踪。inta1;intb2;intc(ab,ab10,a,ba1);逐项计算步骤表达式状态或结果1a b1 2为假该结果被丢弃2a b 10a 123a值为12仍被丢弃4b a 1b 13也是整个表达式的值因此最后a 12、b 13、c 13。括号不可随意去掉因为逗号操作符的优先级很低。2.1 有逗号不一定就是逗号操作符printf(%d %d\n,a,b);intnumbers[]{1,2,3};函数实参之间、初始化列表项之间的逗号是分隔符。它们不享有逗号操作符“左边完成后才求右边”的规则。尤其是函数f(left(), right())两个实参表达式的求值先后没有由书写顺序指定。若确实需要强制先后写成独立语句更直接intleft_valueleft();intright_valueright();f(left_value,right_value);讲义还把读取、计数与判断压进一个while条件里。语法上可用逗号表达式实现但把副作用拆开通常更容易调试for(;;){intvalueget_val();count_val(value);if(value0)break;/* 处理 value */}这段写法仍需按实际业务定义处理结束标记或错误返回值但每步执行时机一眼可见。三、下标访问与函数调用符号相近规则不同3.1[]从起点找到一个元素intarr[10]{0};arr[9]10;printf(%d\n,arr[9]);下标表达式arr[9]可按*(arr 9)理解从arr指向的首元素位置移动 9 个元素再访问它。合法下标是0到9。越界不是“读到下一个格子也许能用”而是未定义行为。intarr[10]{0};/* arr[10] 1; 错误最后一个合法元素是 arr[9] */下标里也可以是计算表达式如arr[i 1]但要确保计算出的最终下标仍在有效范围内。3.2()调用函数实参个数可为零#includestdio.hvoidgreet(void){puts(hello);}voidprint_number(intvalue){printf(%d\n,value);}intmain(void){greet();print_number(10);return0;}greet()的实参个数是0这也是合法的函数调用。函数调用前会先准备实参然后进入函数体但多个实参之间的求值顺序没有由它们在括号中的左右位置规定。需要先后关系时分别算好再传入。四、结构成员访问.与-怎么选一个int不够描述一名学生还可能需要姓名、年龄、学号等。结构体把这些不同类型的成员组织成一个新类型。structStudent{charname[20];intage;unsignedintid;};/* 结构类型声明末尾的分号不能漏 */声明并初始化一个变量structStudentstudent{Zhang,20,1001U};printf(%s %d %u\n,student.name,student.age,student.id);4.1 有结构体对象用.student.age访问的是student这个结构体对象里的成员age。C11 还支持指定初始化器structStudentstudent{.id1001U,.age20,.nameZhang};这样初始化时可不按声明顺序书写未明确初始化的成员会按初始化规则处理。指定初始化器是 C99 起的标准 C 语法。4.2 有结构体指针用-structStudentstudent{Zhang,20,1001U};structStudent*ptrstudent;ptr-age21;printf(%d\n,(*ptr).age);/* 21 */ptr-age等价于(*ptr).age。后者的括号非常重要.的优先级高于一元*若写成*ptr.age会先尝试访问ptr.age但ptr是指针不是结构体对象。4.3 把结构体传入函数值与地址不同#includestdio.hstructPoint{intx;inty;};voidprint_point(structPointpoint){printf((%d, %d)\n,point.x,point.y);}voidmove_right(structPoint*point){if(point!NULL)point-x1;}intmain(void){structPointp{3,4};print_point(p);move_right(p);print_point(p);return0;}print_point(p)接收一份结构体值move_right(p)接收原对象的地址可以修改原对象。指针解引用之前先确认它满足函数约定这里用NULL检查展示基本防护。五、优先级与结合性解决的是分组优先级与结合性回答的是一个表达式按什么结构解释。例如34*5/* 解释为 3 (4 * 5)结果 23 */5*6/2/* 解释为 (5 * 6) / 2结果 15 */ab7/* 解释为 a (b 7) */*、/优先级高于同一级的*、/左结合赋值操作符右结合。因此这些表达式如何分组可以确定。不过“(5 * 6) / 2左结合”并不是一条“编译器必须先读变量5再读6”的求值时间表。操作数若带函数调用或等副作用就必须另外检查求值顺序规则。常见的部分层级如下越往下优先级越低这张表只用于阅读不建议靠背完整表写出复杂表达式。层级操作符结合方向1后缀 --、调用()、下标[]、.、-左到右2前缀 --、一元 -、! ~、取地址、间接访问*、sizeof、类型转换右到左3* / %左到右4 -左到右5 左到右6 左到右7 !左到右8按位与左到右9按位异或^左到右10按位或|左到右11逻辑与左到右12逻辑或||左到右13条件?:右到左14及、-等复合赋值右到左15逗号操作符,左到右括号是给读者的路标当分组不明显时主动加括号。它能改变或明确分组但不能给普通算术操作数凭空增加求值顺序保证。六、整数提升与通常算术转换写char char结果不一定还是char。C 的整数提升会先把较窄的整数类型提升到至少int或unsigned int再参与运算。6.1 整数提升小类型先“长大”signedcharleft100;signedcharright20;intsumleftright;/* 两个操作数先提升为 int结果为 120 */对char、signed char、unsigned char、short等若int能表示该类型的所有值就提升为int否则提升为unsigned int。因此不能简单背成“有符号补符号位、无符号补零”并认为那就是完整语言规则。而且普通char的有符号性由实现决定。若示例需要明确讨论负值写signed char需要明确讨论字节中的非负值写unsigned char。signedchara100;signedcharb20;signedcharresult(signedchar)(ab);上面的加法先在提升后的类型中计算赋回小类型时再发生转换。若结果超出目标有符号类型可表示范围转换结果由实现定义不能通用地说成“只截掉高位”。这里120在常见 8 位signed char范围内但示例是否可移植仍应根据实现的类型范围判断。6.2 通常算术转换两边类型不同怎么办当算术或比较操作需要共同类型时会按一组规则转换操作数。浮点和整数相遇时通常向浮点类型转换两个整数相遇时还要看整数转换等级与有符号/无符号范围不能只把类型名排成一条“谁排得高就转成谁”的链。一个很典型的例子intnegative-1;unsignedintpositive1U;if(negativepositive)puts(negative is smaller);在这里两者是同等级的int与unsigned intnegative会转换成unsigned int-1转换后的值是UINT_MAX所以比较结果为假。这常让只按数学整数直觉阅读的人吃惊。需要表达“负数一定小于非负数”时可以先显式处理负值再在适当范围内比较if(negative0||(unsignedint)negativepositive){/* 条件成立 */}||的短路保证当negative 0为真时右边不求值。若为假negative已是非负转换为unsigned int不会改变其数学值。七、表达式求值分组之外还要看求值顺序课程最后一节的真正难点不是记住每种操作符优先级而是区分下面三件事分组表达式按什么结构解释由语法、优先级和结合性决定。求值顺序多个子表达式按什么时间先后计算必须由专门的规则决定。副作用是否冲突若对同一对象的修改与另一次修改或读取没有必要的先后约束可能是未定义行为。7.1a*b c*d e*f结构确定时间顺序未全定分组相当于(a*b c*d) e*f。每个*的结果要用于相应的但这不意味着编译器必须在第一次加法前把三个乘法全做完。若af都是普通稳定数值这通常不影响最终数学结果若换成带副作用的函数调用就应仔细检查。7.2c --c未定义行为不是“结果不稳定”intc5;intresultc--c;/* 不要这样写 */的左右操作数没有规定先后表达式一边读取c另一边又修改c两者没有必要的顺序关系。在 C11 中属于未定义行为。未定义行为不能靠“我的编译器输出 9”或“加一对括号”变成合法题目。intc5;--c;intresultcc;/* 定义明确result 8 */课程里的(i) (i) (i)以及同一大表达式里多次i--、--i、i、i也有同类问题。不同编译器出现不同输出只是症状不能把某一个输出当成标准答案。7.3 多次调用会修改状态的函数先后未指定结果也可能变把课程示例里的头文件拼写修正为stdio.h后来看#includestdio.hintnext_value(void){staticintcount1;returncount;}intmain(void){intanswernext_value()-next_value()*next_value();printf(%d\n,answer);return0;}这里调用各自完整执行但三个调用谁先执行没有指定。它们依次可能返回2、3、4而这三个返回值分配给减法左操作数、乘法两操作数的方式可能不同乘法分组明确不等于调用次序明确。此例适合归为未指定的求值顺序导致结果可能不同不要与c --c的未定义行为混成同一类。想要确定的业务含义就写清调用顺序intfirstnext_value();intsecondnext_value();intthirdnext_value();intanswerfirst-second*third;/* 2 - 3 * 4 -10 */7.4 哪些操作符给出了先后保证形式保证left, right逗号操作符左操作数完成后才求右操作数left right左边先求若为假右边不求left || right左边先求若为真右边不求condition ? yes : no先求条件只求选中的分支独立的两个完整语句前一语句完成再执行后一语句注意函数实参列表里的逗号不属于表中的逗号操作符。普通、*、-与括号也没有提供“从左往右逐项执行”的承诺。八、把风险表达式改写成可预测的代码分析一个复杂表达式可以按照这四步走画出分组先根据语法、优先级和结合性加上帮助阅读的括号。标记每个读取与修改尤其找、--、赋值、函数调用、volatile访问等。寻找明确的先后关系逗号操作符、逻辑短路、条件操作符、语句边界等能否约束这些副作用若业务需要唯一顺序拆成语句让编译器和读者都无须猜测。例如想“读取旧值再递增最后与另一个数相加”intold_valuecounter;counter1;intresultold_valuebonus;这比把读取、修改和求和挤进一行更稳妥。代码行数多一点检查成本却低很多。九、高频问答与练习Q1a b 7为什么能连续赋值赋值操作符右结合分组为a (b 7)。先把7赋给b赋值表达式再产生相应值供外层赋值使用。这里同一个对象没有冲突修改。Q2加括号能修好i i吗不能。(i) (i)只是更明确地写出分组没有给两次修改建立顺序。拆成独立语句。Q3ptr-x与(*ptr).x是否完全一样就成员访问而言两者等价。前提是ptr指向有效的相应结构体对象空指针或失效指针不能因为用了-就安全。Q4sizeof的单位是什么结果以字节为单位C 的“字节”是char的大小sizeof(char) 1。一个字节包含多少位可查CHAR_BIT不能对所有实现都默认 8 位。动手练习判断int x 1, y 2; int z (x 3, y x, y);执行后的三个值。int arr[4] {10, 20, 30, 40};arr[3]与arr[4]哪个有效对struct Point p和struct Point *q p分别写出访问x的表达式。解释为什么f(g(), h())不保证先调用g并改写成保证g先执行的代码。int x 4; int y x x;能否推算y为什么参考答案第 1 题x4、y6、z6第 2 题只有arr[3]有效第 3 题p.x与q-x第 4 题先执行int gv g();再执行int hv h();最后f(gv, hv);第 5 题不能读取与修改同一对象缺少必要顺序行为未定义。总结操作符学习可以分两层先知道每个符号做什么再知道类型转换、分组和求值顺序如何共同影响结果。[]、()、.、-解决访问与调用逗号操作符提供真正的先后整数提升和通常算术转换改变运算类型优先级与结合性只解决表达式的结构。遇到多个副作用时拆成清楚的语句通常就是最好的答案。资料课程 PDF《第10讲操作符详解》ISO C11 工作草案 N1570 的表达式、整数提升与通常算术转换条款。本文示例和示意图为重新整理与绘制。
返回列表