文章目录
赋值运算符
C语言接受多重赋值,c=b=a=68 等同 c=(b=(a=68))。
类似 a=6+(c=3+8) 的赋值是合法的但可读性差。
对象、左值、右值
形如 bmw = 2022,赋值运算符左侧是一个变量名,右侧是赋给该变量的值,读作“把值2022赋给变量bmw”。
赋值运算的目的是把值储存到内存位置上, 用于储存值的存储区称为对象(object)。 左值(lvalue) 是用于标识对象的标识符(变量名就是一个标识符)或表达式。例如,使用变量名或指针表达式标识一个数据对象。因此,对象指的是实际的数据存储,而左值是用于标识或定位存储位置的标签。
早期C语言的左值有两个特点
1 它指定一个对象,所以引用内存中的地址
2 它可用在赋值运算符的左侧
新的C标准增加了const 限定符,用 const 创建的变量不可更改值,不满足第2项。因此新标准使用可修改的左值(modifiable lvalue) 用于标识可修改的对象。最新标准建议使用术语对象定位值(object locator value) 更好。
被称为项(如,赋值运算符左侧的项)的就是运算对象(operand)。运算对象是运算符操作的对象。赋值运算符的左侧运算对象应该是可修改的左值。
右值(rvalue) 指的是能赋值给可修改左值的量,且本身不是左值。右值可以是常量、变量或其他可求值的表达式。实际上,当前标准在描述这一概念时使用的是表达式的值(value of an expression),而不是右值。
算术运算符
+、- 也是符号运算符。- 运算符可用于改变一个值的代数符号(取一个数的相反数)
int rocky = -12;
int spmkey = -rocky; //取相反数
除法运算符 "/" 用于两个数相除,只要其中一个数为浮点数,结果就是浮点数。如果两个数都是整数,结果是整数(小数部分被丢弃)。
求模运算符 %(modules operator)给出左侧整数除以右侧整数的余数(remainder)。 如果左侧是负数,求模的结果就是负数,如果左侧是正数,求模的结果就是正数。C中,a%b 等同 a - (a/b) * b。
关系运算符
关系表达式的运算结果为布尔值。关系表达式成立为结果为真(1),不成立为假(0)。
可以把待比较的常量放在左侧有助于编译器捕获错误:5 == num 。因为有时会将 num==5 误写成 num=5,变成赋值操作。
由于浮点数在内存中存储的是一个近似值,所以不要直接使用==运算符判断两个浮点数是否相等。可以设定一个差值范围,两个浮点数差值在这个范围内就认为相等。
逻辑运算符
逻辑表达式的运算结果为布尔值。
对于&&和||,C语言规定先计算左边的子表达式,根据结果决定是否短路。
对于 x && y,x为假则不再计算y
对于 x || y,x为真则不再计算y
while((c = getchar()) != ' ' && c != '\n')
//先计算c=getchar()!=' ' 的值,如果为真,再计算c!=’\n’的值。
//如果为假,执行短路不再计算c!=’\n’的值。
if(number && 12/number == 2)
//如果number的值是0,那么&&左边子表达式为假,且不再对右边关系表达式求值。
//这样避免了把0作为除数的情况。
备选拼写 iso646.h头文件
C 是在美国用标准美式键盘开发的语言。在世界各地,并非所有的键盘都有和美式键盘一样的符号。所以C99标准新增了可代替逻辑运算符的拼写,它们被定义在 <ios646.h> 头文件中。如果在程序中包含该头文件,便可用and代替&&、or代替|| 、not代替!。
条件运算符
表达式1 ? 表达式2 : 表达式3
先计算表达式1的值
如果 表达式1 为真,再计算 表达式2 的值,条件表达式的值就是 表达式2 的值。
如果 表达式1 为假,再计算 表达式3 的值,条件表达式的值就是 表达式3 的值。
逗号运算符
逗号运算符有两个性质
1、保证被它分隔的子表达式从左往右求值
2、整个逗号表达式的值是最右侧项的值
x = (y = 3, (z = ++y + 2) + 5)
先把3赋给y,递增y 为4,然后把4加2之和6赋给z,接着加上5,最后把结果11赋给 x。
houseprice = 249, 500
等同于((houseprice = 249),500),整个表达式是一个逗号表达式,houseprice = 249 是他的子表达式。houseprice 的值是 249。
houseprice = (249, 500)
整个表达式是一个赋值表达式,(249, 500)是它的子表达式,(249, 500)的值是500,所以houseprice的值是500。
声明和函数参数列表使用的逗号只是分隔符不是运算符。逗号运算符常用在for循环中。
for(表达式1, 表达式2 ; ; 表达式1, 表达式2){ }
表达式
表达式(expression) 由运算符和运算对象组成(运算对象是运算符操作的对象,可以是常量、变量或函数调用返回值的组合)。最简单的表达式是一个单独的运算对象,复杂的表达式由其他子表达式(subexpression) 组成。
返回值不为void的函数,对它的正确调用也是表达式,表达式的值为函数返回值。
表达式的一个最重要的特性是,每个表达式都有一个值。
语句
一个典型的C程序
C语言有6种语句
- 表达式语句
- 复合语句
- 迭代语句
- 选择语句
- 标号语句
- 跳转语句
表达式语句
语句(statement) 是C程序的基本构建块。表达式末尾加上分号就是一条语句(即,表达式语句)。
legs=4 //只是一个表达式
legs=4; //是一个表达式语句
; //空语句
3 + 4; //没用的表达式语句
一条语句(至少是一条有用的语句)相当于一条完整的指令,但并不是所有的指令都是语句。 例如,x = 6 + (y = 5) , 该语句中的子表达式y = 5是一条完整的指令,但是它只是语句的一部分。
声明创建了名称和类型,并为其分配内存位置。但声明不是语句,去掉分号也不是表达式。
赋值和函数调用都是表达式语句。在描述时,应该说赋值表达式语句和函数表达式语句,而不是赋值语句和函数调用语句。
复合语句
复合语句(compound statement) 是由一对花括号以及可选的,位于花括号中的一些声明和语句组成的。复合语句也称为块(block)。在复合语句外部,可以将复合语句视为一条语句。
迭代语句
迭代语句用于重复执行相同的代码,迭代语句包括do语句、while语句、for语句。
选择语句
选择语句包含if语句和switch语句。选择语句用于改变程序原有的执行顺序和流程。
跳转语句
程序无条件的转到指定的位置执行,跳转语句包括:
- return:跳转到主调函数的下一行代码。
- break:在switch中,用来跳出整个switch。在循环体中,跳出最内层循环。
- continue:只能用在循环体中,用来跳过本次循环,提前进入下一次循环。
- goto:用来在函数内进行跳转。
标号语句
标号语句用于标识一个可以执行的程序入口,这个入口就是执行跳转和分支选择的目标,但标号本身不会改变程序的执行流程。
标号语句分为3种:(goto)标号语句、(default)标号语句、(case)标号语句。
(default)标号语句和(case)标号语句需要配合switch语句一起使用。
// goto标号语句与goto跳转语句
void fun() {
标识符 : 语句; //goto 标号语句
...
goto 标号名; //goto 跳转语句
}
/* 这里的标识符又叫标号名,标号名是唯一具有函数作用域的标识符。
如果标号名的后面只能以声明开始,可以在标号名的后面跟上一个空语句来解决这个尴尬 */
{
A : ; //(goto)标号语句
int y = 100;
}
A : x = x + 100;
A :
x = x + 100;
A :
x = x + 100;
y = y + 100; //(goto)标号语句是 A: x = x + 100;
A :
B :
x = x + 100; //这段代码总共有3条语句
// 1. x = x +100; 这是一条普通语句
// 2. B: x = x + 100; 这是一条标号语句
// 3. A: B: x = x + 100; 这是一条嵌套的标号语句
副作用
副作用(side effects) 是对对象或文件的修改。
C语言主要目的是对每一个表达式求值,副作用是求值过程中产生的。
表达式 states = 50 求值得50,副作用是给states 赋值 50。
表达式 i++ 求值得 i 自增前的值,副作用是 i 本身的值被+1。
调用 printf 函数,求值得返回值,副作用是显示字符。
有些表达式没有副作用;有些表达式既会产生一个值,也会产生副作用。
序列点
C99 标准文件在5.1.2.3讲述序列点
序列点(sequence point) 是程序执行的位置点,在该点上, 所有的副作用都在进入下一步之前发生。
定义序列点是为了尽量消除编译器解释表达式时的歧义。如果序列点还是不能解决某些歧义,C标准允许编译器的实现自由选择解释方式。
常见的序列点
完整表达式的结束处
所谓完整表达式(full expression),就是指这个表达式不是另一个更大表达式的子表达式。
- 表达式语句中的表达式
- do while/while/if/switch 控制表达式
- for循环中被分号分隔的三个表达式
- return 语句中的表达式
int a = 5, b;
b = a++ + 3; //b = a++ + 3是一个完整表达式而a++不是
while(guests++ < 10)
printf("%d\n", guests);
/*
表达式guests++ < 10是一个完整的表达式,所以该表达式的结束处就是一个序列点。
因此,C 保证了在程序转至执行 printf之前发生副作用(递增guests)。
*/
逗号表达式中逗号处
逗号运算符将几个表达式拼接成逗号表达式。“,” 产生序列点,“,” 左边的子表达式必须先求值并产生副作用,再继续处理右边的子表达式。整个逗号表达式的值是最右侧子表达式的值。
int x=2,y;
y = x++, x+1;
// “,”之前的x++必须先求值完毕且副作用(递增x)已经完成,才会计算x+1。最终y的值为4
&&和||运算符
逻辑与和逻辑非产生序列点。因为&&和||支持短路操作,必须先将&&左边的表达式求值完毕并发生副作用。
while( x++ < 10 && x + y < 20)
&&是一个序列点,必须先计算x++ < 10且副作用(递增x)已经发生。如果结果为真,再求x+y<20; 结果为假执行短路。这样保证了在对x + y < 20 求值之前,已经递增x 。
条件运算符 ? :
? 处产生序列点,? 左边的子表达式必须先求值完毕,再根据结果选择执行后边的子表达式。
其他序列点
函数调用的所有参数求值完毕时,在实际调用之前
一个变量的初始化完成时
continue 分号处
break 分号处
语句中的分号标记了一个序列点
运算符优先级、结合性
优先级、结合性和表达式求值顺序的关系。
1 优先级决定两个相邻运算符执行顺序
2 结合性规定相同优先级运算符执行顺序
12/3*2/5
根据运算符结合性(从左向右)对表达式分组:(((12/3)*2)/5)
6*12+20/5
根据运算符优先级,先计算6*12和20/5,但不确定先计算6*12还是20/5。
a*b + c*d + e*f
根据结合性分组 ((a*b + c*d) + e*f),能保证第一个+号比第二个早计算
a*b
c*d
a*b + c*d
e*f
a*b + c*d + e*f
或者
a*b
c*d
e*f
a*b + c*d
a*b + c*d + e*f
x=funa() + funb() + func();
C标准没有规定这三个函数谁会先执行,不同的编译器有不同的调用顺序,甚至相同的编译器不同的
版本会有不同的调用顺序。只要最终结果与调用次序无关,这个语句就是正确的。
C标准规定,在不违背操作符优先级和结合特性的前提下,两个序列点之间的执行顺序是任意的。这个规定是为了给编译器的优化留下空间。
有歧义的表达式
当一个表达式的值取决于编译器实现而不是C语言标准的时候,其中所做的任何处理都会不确定。
例1
函数调用必须先对参数求值,函数参数求值完毕是一个序列点,在此之前如何先对哪个参数求值是未定义的。
int num=5;
printf("%d %d\n ", num, num*num++);
使结果不确定的是num++的副作用何时完成。 表达式num++的值是5,副作用是递增num。
如果副作用在第一个num和第二个num取值之前完成,则输出变成了 "6, 6 * 5 "
如果副作用在第一个num和第二个num取值之后完成,则输出变成了 "5, 5 * 5 "
例2
int n=3;
int y = n++ + n++;
printf("%d %d\n", n, y);
1 编译器可以使用n的旧值(3)两次,然后把n递增两次,这使得y=6,n=5。
2 编译器使用n的旧值(3)一次,立即递增n ,再对另外一个n使用递增后的新值,
然后再递增n,这使得 y=7,n=5
例3
从数组b中复制前n个元素到数组a中
while(i<n)
a[i] = b[i++];
假设a[i]的地址将在右边i++自增操作前被求值,然而不同编译器的实现不同。
int main(void) {
int a[4] = { 0,0,0,0 }, b[4] = {1,2,3,4};
int i = 0;
while (i < 4)
a[i] = b[i++];
for (i = 0; i < 4; i++)
printf("a[%d] = %d\n", i, a[i]);
return 0;
}
例4
int main(void) {
int i = 1;
printf("%d, %d, %d\n", i++, i++, i++);
printf("i = %d\n", i);
i = 1;
printf("%d\n", i++ + i++ + i++);
printf("i = %d\n", i);
i = 1;
printf("%d\n", ++i + ++i + ++i);
printf("i = %d\n", i);
return 0;
}//++i + ++i + ++i的值和i的值都不能肯定
"i++ + i++ + i++" 这条语句执行完成以后i的值也不确定,可能很多编译器的结果确实是4。
例5
int fun() {
static int count = 1;
return ++count;
}
int main() {
int answer;
answer = fun() - fun() * fun();
printf("%d\n", answer);//输出多少?
return 0;
}
fun() - fun() * fun();
通过操作符的优先级可知:先算乘法,再算减法。但函数的调用先后顺序无法通过操作符的优先级确定。
例6
int main(){
int a = 100;
a = a++ / 3;
printf("%d\n",a);
}
许多人会觉得这么算:
int tmp = a;
a += 1;
a = tmp / 3; //最后输出33
在vs中是这么算的:
a = a / 3;
a += 1; //最后输出34
因为在表达式 a=a++/3 中=号的副作用比++的副作用先起效。副作用和运算符优先级没有任何关系,
虽然++运算一定要在除法和赋值运算之前执行,但++的副作用却可以延后执行。
遵循以下规则,很容易避免类似的问题:
1 如果一个变量出现在一个函数的多个参数中,不要对该变量使用递增或递减运算符
2 如果一个变量多次出现在一个表达式中,不要对该变量使用递增或递减运算符。
ANSI C / ISO C标准的描述是:
在上一个和下一个序列点之间,一个对象所保存的值至多只能被表达式计算修改一次。而且前一个值只能用于决定将要保存的值。
意思是:
在一个表达式中如果某个对象需要写入, 则在同一表达式中对该对象的访问应该只局限于直接用于计算将要写入的值。这条规则有效地限制了只有能确保在修改之前才访问变量的表达式为合法。
自增自减的前置后置区别
后置运算的优先级高于前置运算。如果只需要递增变量,那么前置比后置效率高。
- ++a表示取a的地址,增加它的内容,然后把值放在寄存器中
- a++表示取a的地址,把它的值放入寄存器,然后增加内存中的a的值
在操作的时候用到的值都是寄存器里面的。
前置和后置优先级有两种争议
1 某些资料给出前置运算和后置运算优先级一样,为二级运算符且右结合
2 个人觉得更好的是,后置运算优先级高于前置运算,为一级运算符且左结合
分析四个表达式,*++pt 、++*pt 、(*pt)++ 、*pt++
a. *++pt
等价 *(++pt),将pt递增1,取得pt被递增后指向的值
b. ++*pt
等价 ++(*pt),取得pt指向的值,将pt指向的值递增1
c. (*pt)++
等价 (*pt)++,取得pt指向的值,将pt指向的值递增1
d. *pt++
等价 *(pt++),将pt递增1,取得被pt递增前指向的值
上面四个表达式用第1第2种说法都能解释
对于*p++先算++,
按第1种说法是:*和++同级,且右结合。
按第2种说法是:++优先级比 *高。
p是一个指针
a. ++p[0]
b. p++[0]
c. p[0]++
第1种说法无法解释 b。 b是合法表达式,b等价于
tmp = p;
p += 1;
tmp[0];
sizeof(int)*p
sizeof(int)*p
若单纯按优先级规定,sizeof是一个运算符,和类型转换、解引用运算 * 都是单目运算符且同级,则这个式子就是先对p解引用,然后强制转换为int,然后进行sizeof运算。但实际的行为是对int类型进行sizeof然后乘以p。因为还有一条特殊规定:不能对一个不带括号的强制类型转换表达式做sizeof,否则强制类型转换的运算符会视为sizeof的参数,且sizeof如果是对类型做运算,必须加括号。
类型转换
不同类型数据之间进行混合运算时必然涉及到类型的转换问题。
类型升级(promotion):从较小类型转换为较大类型
类型降级(demotion):从较大类型转换为较小类型
自动类型转换
涉及两种类型的运算,较低级别的类型会转换为较高级别的类型,类型的级别从高至低依次是 long double、double、float、unsigned long long、long long、unsigned long、long、unsigned int、int。 例外的,当long 和 int 的大小相同时,unsigned int 比 long 的级别高。
如果 unsigned/signed char 和 unsigned/signed short 出现在表达式中,会被提升成 int。如果 short 与 int 的字节数相同,unsigned short 会被提升成 unsigned int。
赋值表达式计算的最终结果会被转换成被赋值变量的类型。这可能导致类型升级或降级。类型升级通常没有问题,类型降级有时不可测。类型降级通常出现在赋值表达式中。
- 目标类型是无符号整型,且待赋的值是正整数时,额外的高位将被忽略。例如, 如果目标类型是 8 位unsigned char, 待赋的值是原始值求模256。
- 目标类型是一个有符号整型,且待赋的值是整数,结果因实现而异,C 标准并未定义有符号类型的溢出规则
- 目标类型是一个整型,且待赋的值是浮点数,小数部分将被忽略。
在C语言中,调用一个不带原型声明的函数时,会对每个参数执行 “默认实际参数提升”(default argument promotions)。声明函数原型可避免这种参数提升。
同时,对可变长参数列表的每一个实际参数,也将执行参数提升。
提升规则:
- float类型的实际参数将提升到double
- signed/unsigned char、signed/unsigned short 类型的实际参数被提升成 int。如果 short 与 int 的字节数相同,unsigned short 会被提升成 unsigned int。
为什么提升?
int类型被认为是计算机处理整数类型时最高效的类型。 因此, 在short和int类型的大小不同的计算机中, 用int类型的参数传递速度更快。
整型提升
char 和short在表达式中自动提升为int。
正数提升高位补0
负数提升高位补1
强制类型转换运算符
在某个量的前面放置用圆括号括起来的类型名
int a = (int)3.5;
short s = (short)a;