程序员C语言快速上手——基础篇(三) 待我称王封你为后i 2022-01-23 00:19 282阅读 0赞 ### 文章目录 ### * 小拓展:C语言中int的正确使用姿势 * 语法基础 * * 表达式 * * 算术运算符 * 关系运算符 * 逻辑运算符 * 赋值运算符 * 运算符优先级 * 分支与循环 * * 条件分支 * 循环 * * while * for * 控制循环 * 欢迎关注我的公众号:编程之路从0到1 **本专栏已发布配套视频内容,请查看公众号视频专辑《程序员的C》** # 小拓展: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。 来总结一下 1. 当`++`作为后缀时,自增表达式整体的值等于该变量初始值。如上例中`int r = i++;`,表达式整体的返回值`r` 等于`i`的初始值,而`i`未做自增运算前的初始值是0,所以`r`就是0。但是要注意,表达式一旦运行,`i`的值就会立刻发生变化,因此`(i++)`中,`i`的值是1 2. 当`++`作为前缀时,自增表达式整体的返回值等于该变量运算之后的值。如上例中`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; } ![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9hcmN0aWNmb3guYmxvZy5jc2RuLm5ldA_size_16_color_FFFFFF_t_70] 这次生成的汇编代码稍复杂,简单说明一下,`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 ### 运算符优先级 ### 这里给出一个简单常见的优先级顺序 <table> <thead> <tr> <th>优先级</th> <th>类型</th> <th>符号</th> </tr> </thead> <tbody> <tr> <td>1</td> <td>自增自减(后缀)</td> <td><code>i++</code> 或<code>i--</code></td> </tr> <tr> <td>2</td> <td>自增自减(前缀)</td> <td><code>++i</code> 或<code>--i</code></td> </tr> <tr> <td>3</td> <td>乘法类</td> <td><code>*</code> <code>/</code> <code>%</code></td> </tr> <tr> <td>4</td> <td>加法类</td> <td><code>+</code> <code>-</code></td> </tr> <tr> <td>5</td> <td>赋值类</td> <td><code>=</code> <code>+=</code> <code>-=</code> ……</td> </tr> </tbody> </table> ## 分支与循环 ## ### 条件分支 ### 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. 先执行“表达式1”。 2. 再执行“表达式2”,如果它的值为真(非0),则执行循环体,否则结束循环。 3. 执行完循环体后再执行“表达式3”。 4. 重复执行步骤 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); } # 欢迎关注我的公众号:编程之路从0到1 # ![编程之路从0到1][0_1] [watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9hcmN0aWNmb3guYmxvZy5jc2RuLm5ldA_size_16_color_FFFFFF_t_70]: /images/20220123/9c550ec5b86640e4b79f8f8e380ef1dd.png [0_1]: /images/20220123/28dad688ce3c485db836ae4a3287ea5e.png
相关 程序员C语言快速上手——工程篇(十三) 文章目录 C语言工程构建 shell脚本(bat脚本) Makefile 脚本 基本语法规则 补 女爷i/ 2023年10月11日 13:11/ 0 赞/ 48 阅读
相关 程序员C语言快速上手——基础篇(三) 文章目录 小拓展:C语言中int的正确使用姿势 语法基础 表达式 算术运算符 关系运算符 待我称王封你为后i/ 2022年01月23日 00:19/ 0 赞/ 283 阅读
相关 程序员C语言快速上手——高级篇(九) 文章目录 高级篇 结构体 背景 结构体的声明与使用 结构体变量的初始化 太过爱你忘了你带给我的痛/ 2021年12月09日 20:55/ 0 赞/ 352 阅读
还没有评论,来说两句吧...