文章目录
小拓展:C语言中int的正确使用姿势
上一节已经讲过,由于C语言中,整型的实际长度和范围不固定的问题,会导致C语言存跨平台移植的兼容问题,因此,C99标准中引入了stdint.h
头文件,有效的解决了该问题。
#include<stdio.h>
#include<stdint.h>
int main(void){
// 使用stdint.h中定义的类型表示整数
int8_t a = 0;
int16_t b = 0;
int32_t c = 0;
int64_t d = 0;
// 前面加u,表示unsigned,无符号
uint32_t e = 0;
printf("int8 size is %d\n",sizeof(int8_t));
printf("int16 size is %d\n",sizeof(int16_t));
printf("int32 size is %d\n",sizeof(int32_t));
printf("int64 size is %d\n",sizeof(int64_t));
printf("uint32 size is %d\n",sizeof(uint32_t));
}
打印结果:
int8 size is 1
int16 size is 2
int32 size is 4
int64 size is 8
uint32 size is 4
int8_t
即表示8位整型,同理,int64_t
就是64位整型,类型定义明确清晰,且能兼容多种平台。以上代码,使用32位编译器,编译成32位系统下的程序后,运行得到的结果依然不变。这里一定会有朋友质疑,为什么32位的系统下,还能表示并使用int64
这种64位的整型?这当然就是stdint.h
库给我们带来的便利了,简单说一下原理,如果当前平台的是32位的,那么经过组合,我们可以使用两个32位拼起来,不就能表示64位了吗?同理,即使是8位的CPU,经过这种拼合思路,照样能表示64位!当然,聪明人一眼就看出了弊端,使用这种拼合的方式,数据需要经过组合转换,处理也更加复杂,同时还会带来性能的损失,但是C99标准库已经为我们处理好了一切,虽然付出了一定的性能损失,但是成功的实现了C语言整型的跨平台兼容,这样的损失是完全值得的。
由于stdint.h
头文件是C99标准引入的新特性,前面也说过微软的VC编译器不支持C99,那是不是VC就不能用了呢?好东西,当然人人眼馋,微软虽然表面上说不支持C99,但是这种有用的特性还是会引入,因此VS2010也引入了stdint.h
头文件,在VS2010及其以后的版本中,可以放心使用。但是要注意,只是引入了这个新特性,而不是支持C99。这里就要吐槽了,目前还在使用VC6.0教学的,还是上个世纪的人么?说和工具没关系的这些人,害人匪浅。
语法基础
表达式
与其他编程语言不同,C语言强调表达式而不是语句。表达式就如同计算值的公式,通过运算符把变量和常量组合起来。
算术运算符
主要包括加减乘除
+
、-
、*
、/
求余数,即取模运算 %
二元的算术运算还包括自增和自减
++
、--
自增和自减运算符可以作为前缀或后缀使用,如下
int i = 0;
i++;
++i;
那么i++
和++i
的区别是什么呢?
关于这两者的区别,某些教材和网上一些资料是这样解释的,++
做前缀,是先让i
加1,做后缀则后加1,既在下一行代码前i
被加1。类似这种说法其实是不准确的,甚至是错误的,理解太过于表面,只是对现象的概括而已。这里咱们就一次把这个问题彻底搞明白,永不犯迷糊。
前面已经说了,C语言强调的是表达式而不是语句,那么表达式和语句有什么区别呢?我个人认为其中一个区别就是表达式整体一定有一个值,而语句可以没有返回值。有其他编程基础的朋友一定清楚所谓返回值的概念,那么就是说表达式一定有一个返回值,或者应该说是表达式整体的值。
i++
作为一个表达式,那么他的表达式的值是什么呢?其实我们可以用一个变量来保存表达式的值int r = i++;
int i = 0;
int r = i++;
printf("r=%d\n",r);
可以看到,表达式的r
值为0。这个例子就很清楚了,所谓表达式的值,其实就是(i++)
整体的一个值,它是一个独立的值。再运行下面的例子
int i = 0;
int r = ++i;
printf("r=%d\n",r);
可以看到,此时,表达式(++i)
整体的值r
变成了1。
来总结一下
-
当
++
作为后缀时,自增表达式整体的值等于该变量初始值。如上例中int r = i++;
,表达式整体的返回值r
等于i
的初始值,而i
未做自增运算前的初始值是0,所以r
就是0。但是要注意,表达式一旦运行,i
的值就会立刻发生变化,因此(i++)
中,i
的值是1 -
当
++
作为前缀时,自增表达式整体的返回值等于该变量运算之后的值。如上例中int r = ++i;
,r
的值等于(++i)
表达式运算之后i
的实际值。
因此,遇到复杂的自增运算符时,只需要问自己两个问题,自增变量的值是几?表达式整体的返回值又是几?下面我们看一个很常见的问题,问i
和j
打印的值各是几?
int i = 0;
int j = i++ + ++i;
printf("i=%d, j=%d\n",i,j);
按照我们上面讲的知识来分解,先把式子拆分成(i++) + (++i);
,(i++)
这个表达式整体的值是0,但此时i
的值已经变成1了。而在(++i)
这个表达式中,i
的值则是1 + 1
,所以执行(++i)
后,i
的值为2,那么j
的值也就是0 + 2
大家千万要记住,不管是i++
也好,++i
也罢,变量i
的值都会立刻增加,所以只看i
的值,这两者是没有区别的,它的区别在我们说的另一个概念上,也就是所谓的表达式的返回值。
好了,授人以鱼不如授人以渔,如何证明我说的就是对的,别人的是错误的呢?C语言就是有一个好处,一切纷繁复杂的表象都能回归事物的本质。因为C语言与汇编语言是一一对应的,因此我们只需要查看C语言翻译成汇编语言后,在计算机内部到底发生了什么就能掌握真理,而无需人云亦云。
为了让生成的汇编语言更简单,我们去除头文件,编写最简单的代码test.c
int main(void){
int i = 0;
i++ + ++i;
return 0;
}
打开cmd命令行,使用gcc命令生成汇编源码,这里学习一个新的gcc参数-S
gcc -S test.c
打开生成的test.s
文件,这里截取关键部分如下:
call __main
movl $0, -4(%rbp)
movl -4(%rbp), %eax
addl $1, %eax
movl %eax, -4(%rbp)
addl $1, -4(%rbp)
movl $0, %eax
addq $48, %rsp
popq %rbp
ret
这里call __main
相当于main函数入口,ret
相当于return 0
,这之间一段也就对应我们的两行C语言代码。特别说明一下,这里使用的gnu的工具链生成的是AT&T的x86-64汇编代码,而非大家熟悉的intel 80386汇编。高校教的汇编语言都是intel x86的32位汇编,因此学过汇编的人可能也会感觉非常陌生。实际上这段汇编非常简单,并不需要有什么汇编基础。
简单解释一下指令
movl
对应80386汇编中的mov
指令,是单词move
的缩写,表示传递数据,addl
则对应add
指令,表示加法器。这里的-4(%rbp)
表示的是一个内存地址,eax
则是32位对应的8个寄存器中的第一个。
movl $0, -4(%rbp)
这句表示把一个常量0存到一个内存地址中,对应int i = 0;
此后,-4(%rbp)
这个地址就代指变量i
movl -4(%rbp), %eax
这句表示将变量i
中的值取出来放到一个名叫eax
的寄存器中。addl $1, %eax
则对应i++
,表示将常量1与寄存器eax
的值相加,然后存到eax
中,那么此时eax
的值就是1。紧接着movl %eax, -4(%rbp)
,表示将寄存器eax
的值刷新到变量i
中,故而i++
后,i
的值立刻发生改变。
然后是addl $1, -4(%rbp)
,这句对应的C语言代码是++i
,它表示将常量1直接与变量i
的值相加,结果仍然保存到变量i
中,那么此时就是1+1
,故而变量i
最后等于2。
到这里,其实汇编代码就结束了,并没有将(i++)
的整体结果与(++i)
的整体结果做最后的求和,这是因为我们没有用一个 变量来保存他们的和,所以编译器对C语言代码进行了优化,既然我们不需要结果,它干脆就不计算了。
现在修改代码,并再次生成汇编代码
int main(void){
int i = 0;
int j = i++ + ++i;
return 0;
}
这次生成的汇编代码稍复杂,简单说明一下,edx
、eax
都是32位通用寄存器,rax
则是64位寄存器,在此处,可以把rax
和eax
等同,可以看做是同一个寄存器。那么leal 1(%rax), %edx
则表示,将寄存器rax(即eax)
中的值加1,然后存到edx
寄存器中。-4(%rbp)
和-8(%rbp)
分别是变量i
和变量j
的内存地址,可以指代这两个变量。
通过上述汇编代码,我们可以清晰的发现,无论是i++
还是++i
,变量i
的值都会立刻被改变。
最后,关于i++
和++i
的辟谣:
有一些陈旧的资料中指出,++i
的性能要比i++
更好,因为它是直接在内存中加1,在for
循环中,推荐使用++i
。让我们再次编写C代码,生成汇编代码来验证这个观点
int main(void){
int i = 0;
int j = 0;
i++;
++j;
}
汇编代码
call __main
movl $0, -4(%rbp)
movl $0, -8(%rbp)
addl $1, -4(%rbp)
addl $1, -8(%rbp)
movl $0, %eax
addq $48, %rsp
popq %rbp
ret
可以看到,i++;
和++j;
生成的汇编代码一模一样,不存在谁性能更好的说法。现代编译器中,都已做了优化处理,因此你喜欢写那种风格都没问题。
关系运算符
用于比大小的一些运算,其中==
表示两者相等
<
、<=
、>
、>=
、==
逻辑运算符
这是任何一种编程语言都具备的,如下,表示逻辑与或非
&&
、||
、!
赋值运算符
=
表示赋值运算符,在C语言中,存在左值和右值的概念。简单说,=
左边的叫左值,右边的叫右值。左值只能是计算机内存中的对象,而不能是常量或计算的结果。例如变量可以成为左值,而像5
、i + 2
这样的不能做左值。
注意,重点来了,C语言中=
运算符存在赋值陷阱!
首先看C语言的连环赋值语法
int i,j,k;
i = j = k = 0;
=
遵循右结合,所有它等价于i = (j = (k = 0))
,也就是说0先赋值给k
,然后k
的值再赋值给j
,以此类推。Ok,这样是没问题的。
再看如下代码
int i;
float j;
j = i = 6.1f;
则j
最终的值变成了6.0
,这就是赋值陷阱。也就是说=
存在类型自动转换的问题,值传递给i
时,自动转化为int型,丢弃了小数部分。
除此外,赋值运算符还存在复合用法如下
int8_t a = 0;
int16_t b = 0;
int32_t c = 0;
int64_t d = 0;
a += 1; // 等价于 a = a + 1
b -=1; // 等价于 a = a - 1
c *= 1; // 等价于 a = a * 1
d /=2; // 等价于 a = a / 1
d %=2; // 等价于 a = a % 1
运算符优先级
这里给出一个简单常见的优先级顺序
优先级 | 类型 | 符号 |
---|---|---|
1 | 自增自减(后缀) | i++ 或i-- |
2 | 自增自减(前缀) | ++i 或--i |
3 | 乘法类 | * / % |
4 | 加法类 | + - |
5 | 赋值类 | = += -= …… |
分支与循环
条件分支
C语言的条件分支与其他语言相似
if-else
分支,如下结构,这是Linux C语言推荐的代码范式,即将一个花括号紧跟小括号之后,写在同一行。
if (1 > 0){
// do something
}else{
// do something
}
if
后面的条件表达式中存在陷阱,在C语言中没有布尔类型,使用0和非0来表示false和true。因此很多人会想当然的以为0是false,大于0就是true,实际上,-1
也是true,要注意,是一切非0值,包括小数也是true。
当if-else
中只有一句时,语法上是可以省略花括号的,但是不建议这样,尤其包含嵌套的if语句时。C语言语法比较自由,正是如此,才更应该遵守规范。始终写上花括号,养成良好的编程规范,使代码易于阅读和维护。
if(a>b) max=a;
else max=b;
// 或者放两行
if(a>b)
max=a;
else
max=b;
多重条件的复合判断
if(/*条件1*/){
//语句块1
} else if(/*条件2*/){
//语句块2
} else if(/*条件3*/){
//语句块3
}else{
//语句块n
}
当复合的条件过多时,直接使用if - else if - else
会显得代码冗长,因此C语言也提供了另一种语法编写选择分支,与Java、JavaScript等语言的switch相同
int a = 1;
switch(a){
case 1:
printf("Monday\n");
break;
case 2:
printf("Tuesday\n");
break;
case 3:
printf("Wednesday\n");
break;
case 4:
printf("Thursday\n");
break;
case 5:
printf("Friday\n");
break;
case 6:
printf("Saturday\n");
break;
case 7:
printf("Sunday\n");
break;
default:
printf("error\n");
break;
}
需要注意,case
后面必须是一个整数,或者是结果为整数的表达式,但不能包含任何变量。
循环
while
最简单的循环当是while
循环
while(/*表达式*/){
//语句块
}
int i=1, sum=0;
while( i<=100 ){
sum+=i;
i++;
}
除此外,还存在while
循环的变体,do - while
循环
do{
//语句块
}while(/*表达式*/);
//---------------------------------
int i=1, sum=0;
do{
sum+=i;
i++;
}while(i<=100);
do-while
循环与while
循环的不同在于,它会先执行“语句块”,然后再判断表达式是否为真,如果为真则继续循环;如果为假,则终止循环。因此,do-while 循环至少要执行一次“语句块”。再使用do-while
循环时,要记住,while(i<=100);
的小括号后面必须跟一个分号。
for
C语言中更常用的可能是for
循环
for 循环的一般形式
for(表达式1; 表达式2; 表达式3){
语句块
}
- 先执行“表达式1”。
- 再执行“表达式2”,如果它的值为真(非0),则执行循环体,否则结束循环。
- 执行完循环体后再执行“表达式3”。
- 重复执行步骤 2 和 3,直到“表达式2”的值为假,就结束循环。
// 使用for循环,进行等差数列求和
int sum=0;
for(int i=1; i<=100; i++){
sum+=i;
}
printf("%d\n",sum);
for 循环中的三个表达式都是可选项,都可以省略,但分号必须保留。
int i = 1, sum = 0;
for( ; i<=100; i++){
sum+=i;
}
// 省略两个
for( ; i<=100 ; ){
sum=sum+i;
i++;
}
// 全部省略,表示死循环,等同于while(1){}
for( ; ; ){
// do something
}
实际上,for循环的灵活用法,完全可以替代while循环。另外,for循环中也能使用逗号表达式,当循环体只有一行时,亦可省略花括号
//表达式1 和 表达式3都是一个逗号表达式,即用逗号连接了两个表达式。
for( i=0,j=100; i<=100; i++,j-- ) k=i+j;
控制循环
在适当的时候,我们需要退出循环或跳过本次循环,这时候就需要控制循环。
控制循环通常使用break
和continue
关键字。
当break
关键字用于 while、for 循环时,会终止循环而执行整个循环体后面的代码。break 关键字通常和 if 语句一起使用,即满足条件时便跳出循环
int i=1, sum=0;
while(1){ //死循环
sum+=i;
i++;
if(i>100) break; //满足条件退出循环
}
continue
的作用是跳过本次循环中剩余的语句而强制进入下一次循环。它只用在 while、for 循环中,常与 if 条件语句一起使用
// 打印奇数
for(int i=1; i<=100; i++){
if(i%2 == 0){ // 遇到偶数时跳过
continue;
}
printf("%d\n",i);
}