Linux C编程笔记(1)

作者 by adtxl / 2022-07-17 / 暂无评论 / 180 个足迹

只记录一些之前新学到的,之前没注意到的东西

1. 常量、变量和表达式

  1. 在C语言中,整数除法取的既不是Floor也不是Ceiling,无论操作数是正是负,总是把小数部分截掉,在数轴上向零的方向取整。
  2. 只有分配存储空间的变量声明才叫变量定义。

2. 简单函数

  1. 把函数名也看成属于一种类型,即函数类型。
  2. 函数的隐式声明,如果一个函数的返回值是int,参数类型是void,在未提前声明就使用的话,编译器会自动生成函数的隐式声明,编译不会报错。
  3. 局部变量可以用类型相符的任意表达式来初始化,而全局变量只能用常量表达式初始化。程序开始运行时要用适当的值来初始化全局变量,所以初始值必须保存在编译生成的可执行文件中,因此初始值在编译时就要计算出来。下面这种定义也是不合法的
int minute = 360;
int hour = minute/60;

虽然在编译时计算出hour的初始值是可能的,但minute/60不是常量表达式,不符合语法规定,所以编译器直接报错退出,而不去计算这个初始值。

  1. 如果全局变量定义时不初始化则其初始值是0,如果局部变量在定义时不初始化,是个随机值,所以,局部变量在使用前一定要先赋值,如果基于一个不确定的值做后续计算肯定会引入bug。

3. 分支语句

  1. %运算符的结果总是与被除数同号。

  2. 浮点型的精度有限,不适合用==做精确比较。

  3. 使用switch语句需要注意以下几点:1.case后面的表达式必须是常量表达式,这个值和全局变量的初始值一样必须是在编译时计算出来的; 2.case后面必须是整型常量表达式,不能是浮点数; 3.进入case后如果没有遇到break语句就会一直往下执行,后面其他case或default分支的语句也会被执行到,直到遇到break,或者执行到整个switch语句块的末尾。通常每个case后面都要加上break语句,有时也会故意不加break。

4. 深入理解函数

  1. 递归和循环是等价的,用循环能做的事用递归都能做,反之亦然。

5. 循环语句

  1. 如果把i++这个表达式看作一个函数调用,传入一个参数返回一个值,返回值就等于参数值(而不是参数值加1),此外也产生一个Side Effect,就是把变量i的值增加了1,它和++i的的区别就在于返回值不同。
  2. for (控制表达式1;控制表达式2;控制表达式3;) 语句:对于for循环,执行continue语句之后首先计算控制表达式3,然后测试控制表达式2,如果值为真则继续执行下一次循环。
  3. 在多层循环或switch嵌套的情况下,break只能跳出最内层的循环或switch,continue也只能终止最内层循环并回到该循环的开头。
  4. goto语句,即实现无条件跳转。我们知道break只能跳出最内层的循环,如果在一个嵌套循环中遇到某个错误条件需要立即跳出最外层循环做出错处理,就可以用goto语句。

6. 结构体

  1. 结构体变量之间允许用赋值运算符,同时允许用一个结构体变量初始化另一个结构体变量,例如
struct complex_struct {double x, y;};
struct complex_struct z1 = {3.0, 4.0};
struct complex_struct z2 = z1;

同样地,z2必须是局部变量才能用z1的值来初始化。

7. 数组

  1. 数组不能相互赋值或初始化。既然不能相互赋值,也就不能用数组类型作为函数的参数或返回值。当数组类型做右值使用时,自动转换成指向数组首元素的指针。
  2. scanf的功能是等待用户的输入并回车。

8. 计算机中数的表示

  1. 1's Complement表示法。
    负数用1的补码(1's Complement)表示,减法转换成加法,计算结果的最高位如果有进位则要加回到最低位上去。取1的补码就是把每个位取反,所以1的补码也称为反码。
    00001000-00000100->00001000+(-00000100)->00001000+11111011->00000011 进1 -> 高位进的1加到低位上去,结果为00000100
    美中不足的是0的表示仍然不唯一,既可以表示成11111111也可以表示为00000000,为了解决这最后一个问题,我们引入2's Complement。

  2. 2's Complement表示法
    2's Complement表示法规定:正数不变,负数先取反码再加1.
    2's Complement表示法的计算规则有些不同:减法转换成加法,忽略计算结果最高位的进位,不必加回到最低位上去。
    在相加过程中最高位产生的进位和次高位产生的进位如果相同则没有溢出,如果不同则表示有溢出。

9. 数据类型详解

  1. signed int和unsigned int可以简写为signed和unsigned
  2. 整数常量还可以在末尾加u或U表示unsigned,加l或L表示long,加ll或LL表示long long,例如0x1234U、98765ULL等。但事实上,u、l、ll这几种后缀和上面讲的unsigned、long、long long关键字并不是一一对应的。

10. 运算符详解

  1. 掩码(Mask)。如果要对一个整数中的某些位进行操作,怎样表示这些位在整数中的位置呢?可以用掩码(Mask)来表示。比如掩码0x0000ff00表示对一个32位整数的8~15位进行操作。
  2. a += 1相当于a = a + 1。但有一点细微的差别,前者对表达式a只求值一次,而后者只求值两次,如果a是一个复杂的表达式,求值一次和求值两次的效率是不同的。
  3. sizeof是一个特殊的运算符,它有两种形式:"sizeof 表达式"和"sizeof(类型名)"。这个运算符很特殊,"sizeof 表达式"中的子表达式并不求值,而只是根据类型转换规则求得子表达式的类型,然后把这种类型所占的字节数作为整个表达式的值。有人喜欢写成sizeof(表达式)也是可以的。例如:
int a[12];
printf("%u\n", sizeof a/sizeof a[0];

注意sizeof a中的a做左值,表示整个数组,而不是做右值转换成指向首元素的指针。

11. 汇编与C之间的关系

  1. 关键字union定义一种新的数据类型,称为联合体,其语法类似于结构体。一个联合体的各个成员占用相同的内存空间,联合体的长度等于其中最长成员的长度(不绝对相等,可能需要对齐)。
  2. 在c语言中用volatile限定符修饰变量,也就是告诉编译器,即使在编译时指定了优化选项,每次读取这个变量时仍然要老老实实地从内存读取,每次写这个变量也仍然要老老实实地写回内存,不能省略任何步骤。

12. 预处理

  1. 函数式宏定义

以前我们用过的#define N 20#defines STR "hello world"这种宏定义称为变量式宏定义,宏定义名可以像变量一样在代码中使用。另外一种宏定义可以像函数调用一样在代码中使用,称为函数式宏定义,例如

#define MAX(a, b) ((a) > (b)?(a) : (b))
k = MAX(i&0x0f, j&0x0f)

注意函数式宏定义和真正的函数调用的区别:

  • 函数式宏定义的参数没有类型,预处理器只负责进行形式上的替换,而不做参数类型检查,所以传参时要格外小心
  • 调用真正的函数和调用函数式宏定义的代码编译生成的指令不同。
  • 定义这种宏要格外小心,注意括号。
  • 调用参数时先求实参表达式的值再传递给形参,如果实参表达式有Side Effect,那么这些Side Effect只发生一次,使用宏定义则不一定。
  1. # ##运算符和可变参数

###是两个预处理运算符(注意不是C语言表达式的运算符),在函数式宏定义中#运算符后面应该跟一个形参(#和形参之间可以有空格或Tab),用于创建字符串字面值,例如:

#define STR(s) # s
STR(hello     world)

用cpp命令预处理之后是"hello world",预处理用引号把实参括起来成为一个字符串字面值,并且实参中的连续多个空白字符被替换成一个空格。再比如:

#define STR(s) #s
fputs(STR(strncmp("ab\"c\0d", "abc", '\4"') == 0) STR(: @\n), s);

预处理之后是fputs("strncmp(\"ab\\\"c\\0d\", \"abc\", '\\4\"') == 0" ": @\n", s);
注意如果实参中包含字符常量或字符串字面值,则宏展开之后字符串的界定符"要替换成\",字符串常量或字符串字面值中的\" 要替换成\\\"

在宏定义中可以使用##运算符把前后两个预处理Token连接成一个预处理Token。和#运算符不同,##运算符不仅限于函数式宏定义,变量式宏定义也可以用。例如

#define CONTACT(a, b) a##b
CONTACT(con, cat)

预处理后是concat。

我们知道printf函数带有可变参数,函数式宏定义也可以带可变参数,同样是在参数列表中用...表示可变参数。例如:

#define showlist(...) printf(#__VA_ARGS__)
#define report(test, ...) ((test)?printf(#test):printf(__VA_ARGS__))
showlist(The first, second, and third items.);
report(x>y, "x is %d but y is %d", x, y);

预处理之后变成

printf("The first, second, and third items.");
((x>y)?printf("x>y"):printf("x is %d but y is %d", x, y));

宏定义中可变参数的部分用__VA_ARGS__表示,在宏展开时和...对应的几个实参可以看成一个实参来替换掉__VA_ARGS__

gcc有一种扩展语法,如果##运算符用在__VA_ARGS__前面,除了起连接Token的作用之外还有一种特殊用法。例如,

#define DEBUGP(format, ...) printk(format, ## __VA_ARGS__)

这个函数式宏定义可以这样调用:DEBUG("info no. %d", 1),也可以这样调用:DEBUGP("info")。后者相当于可变参数部分传了一个空参数,但展开之后并不是printk("info",),而是printk("info"),当__VA_ARGS__是空参数时。##运算符把它前面的逗号“吃”掉了。

  1. 如果在一个编译单元重复定义一个宏,c语言规定这些重复定义的宏必须一模一样(可以在定义中间包含空格、Tab,注释)。如果需要重新定义一个宏,和原来的定义不同,可以先用#undef取消原来的定义。重复取消一个宏的定义不算错。

13. 指针

  1. 用一个指针给另一个指针赋值时要注意,两个指针必须是同一类型的。在我们的例子中,pi是int*型的,pc是char*型的,pi=pc;这样赋值就是错误的,但是可以通过先强制类型转换然后赋值。pi=(int*)pc;。现在pi指向的地址和pc一样,但是通过*pc只能访问到一个字节,而通过*pi可以访问到4个字节。

2.通用指针,void*类型,可以转换成任意其他类型的指针,任意其他类型的指针也可以转换成通用指针,可以转换成任意其他类型的指针,任意其他类型的指针也可以转换成通用指针。注意,只能定义void*类型的指针而不能定义void类型的变量,因为void*指针和别的指针一样都占4个字节。void*指针通常用于参数传参和传返回值。

独特见解