目录
1. 预处理指令
1.1 #define
- 宏和宏的值可以在编译时指定,编译时需要加 -D 选项
#include <stdio.h>
#define PI 3.14
int main(void)
{
double girth;
girth = 2 * PI * RADIUS;
printf("girth = %lf\n",girth);
return 0;
}
编译:
Desktop Linraffe$ gcc -D RADIUS=10 macro.c -o marcro
- 宏可以实现类似函数功能
#include <stdio.h>
#define PI 3.14
#define GIRTH(RADIUS) 2*PI*RADIUS
int main(void)
{
double radius;
printf("input radius:");
scanf("%lf",&radius);
printf("girth = %lf\n",GIRTH(radius));
return 0;
}
但是,宏函数与函数是有很大区别的,函数的计算有自己的存储单元保存,而宏函数的实质是文本替换,以下面的程序为例:
执行以下程序,输出结果是什么:
#include <stdio.h>
#define SUB(I,J) I - J
int sub(int i,int j){
return i - j;
}
int main(void)
{
printf("SUB(4,3) = %d\n",SUB(4,3));
printf("sub(4,3) = %d\n",sub(4,3));
printf("SUB(5,SUB(4,3)) = %d\n",SUB(5,SUB(4,3)));
printf("sub(5,sub(4,3)) = %d\n",sub(5,sub(4,3)));
return 0;
}
编译执行结果:
SUB(4,3) = 1
sub(4,3) = 1
SUB(5,SUB(4,3)) = -2
sub(5,sub(4,3)) = 4
以上代码可以比价出函数与宏函数的区别,通过与编译可以更明显得看出他们的区别:
macro.i 预处理文件
int main(void)
{
printf("SUB(4,3) = %d\n",4 -3);
printf("sub(4,3) = %d\n",sub(4,3));
printf("SUB(5,SUB(4,3)) = %d\n",5 - 4 - 3);
printf("sub(5,sub(4,3)) = %d\n",sub(5,sub(4,3)));
return 0;
}
注意:宏和宏函数的实质都是文本的替换
3. 宏函数有多个参数时和普通函数的形参一样用逗号分开
4. 宏函数的参数没有数据类型,参数不一定是数字,字符也可以
1.2 宏操作符 - #
#是一个宏操作符,它可以把宏的参数转换为字符串字面值,例如:#define A(n) #n,那么A(abc)预处理时会被替换为"abc"
#include <stdio.h>
#define BE_STRING(s) #s
int main(void)
{
printf("BE_STRING(giraffe) = %s\n",BE_STRING(giraffe));
return 0;
}
编译执行结果:
BE_STRING(giraffe) = giraffe
1.3 宏操作符 - ##
##是一个宏操作符,它可以把一个代表标识符的参数和其他内容连接形成新的标识符
#include <stdio.h>
#define POINTER(n) p_##n
int main(void)
{
int num;
int *POINER(num) = # //等价于int * p_num = #
return 0;
}
预编译后生成macro.i,内容如下:
int main(void)
{
int num;
int *POINER(num) = #
return 0;
}
1.4 条件编译
条件编译可以在编译时从几组语句中选择一组编译而忽略其他组语句
条件编译结构:
- #ifdef/#ifndef 宏名称
语句组1…
#else
语句组2…
#endif
举例:
#include <stdio.h>
int main(void)
{
#ifdef GIRAFFE
printf("giraffe\n");
#else
printf("unicorn\n");
#endif
return 0;
}
编译和输出结果:
Desktop Linraffe$ gcc -D GIRAFFE macro.c -o macro
Desktop Linraffe$ macro
giraffe
Desktop Linraffe$ gcc macro.c -o macro
Desktop Linraffe$ macro
unicorn
- #if defined(宏名称)或逻辑表达式,如!defined(宏名称)&&!defined(宏名称)
#elif defined(宏名称)或逻辑表达式
#else
#endif
举例:
#include <stdio.h>
int main(void)
{
#if defined(GIRAFFE)
printf("giraffe\n");
#elif !defined(UNICORN) && !defined(FOX)
printf("zebra\n");
#elif !defined(UNICORN)
printf("fox\n");
#else
printf("unicorn\n");
#endif
return 0;
}
编译和输出结果:
esktop Linraffe$ gcc macro.c -D UNICORN -o macro
Desktop Linraffe$ macro
unicorn
Desktop Linraffe$ gcc macro.c -D FOX -o macro
Desktop Linraffe$ macro
fox
1.5 块注视
快速注释掉一整块代码:
#if 0
...
...
#endif
2. extern关键字
执行代码输出结果是什么?
Myextern.c
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
int num;
int main()
{
void Myfunc(void);
num = 1;
printf("parent ps:\tnum = %d\n",num);
printf("parent ps:\t");
Myfunc();
pid_t pid = fork();
if(-1 == pid){
perror("fork");
}
else if(0 == pid){
printf("child ps:\t");
Myfunc();
printf("child ps:\t");
Myfunc();
exit(0);
}
wait(NULL);
printf("parent ps:\t");
Myfunc();
printf("parent ps:\tnum = %d\n",num);
return 0;
}
func.c
#include <stdio.h>
void Myfunc(void)
{
extern int num;
printf("in func num = %d\n",num);
num++;
return;
}
编译执行结果:
Desktop Linraffe$ gcc Myextern.c func.c -o Myextern
Desktop Linraffe$ Myextern
parent ps: num = 1
parent ps: in func num = 1
child ps: in func num = 2
child ps: in func num = 3
parent ps: in func num = 2
parent ps: num = 3
以上代码本来想说明跨文件使用全局变量时,未定义全局变量的文件需要使用extern声明这个全局变量,这个不错,但是在多进程时共享全局变量时翻车了。
注意:关于extern关键字:
- 跨文件使用全局变量时,未定义全局变量的文件需要使用extern关键字声明该全局变量
- extern声明的全局变量不再次创建新的内存空间,因此extern对变量的声明都可放在头文件中
- 局部变量不可跨文件使用(加extern无效,编译报错)
- static修饰的变量/函数被限制在本文件中,也不可以跨文件(加extern无效,编译报错)
- 子进程会继承父进程的全局变量值,但是父子进程并不共享该全局变量(这个同名的全局变量,对于父子进程是两个不同的存储区,而不是同一个) //通过上面的代码验证
3. static关键字
注意:关于static关键字
- 对于局部变量,static关键字将变量的生命周期从栈帧延长到整个进程,但是作用域不变,在函数栈帧之外该变量不可见
- 对于全局变量,static关键字不影响其原先的生命周期,但是限制其作用域在本文件中(不允许其跨文件使用,即,禁止extern对其修饰)
- 对于函数,static关键字同样将函数的作用域限制在本文件中,不允许其他文件调用该函数
4. volatile关键字
volatile关键字修饰的变量不允许编译器对其优化,每次对volatile修饰变量的操作直接通过寄存器,绝不通过缓存,保证数据实时性
volatile关键字详解参考
5. 结构体
- 对于C语言结构体成员不能是函数(C++可以)
- 结构体的声明不开辟内存空间,适合放在头文件中
- 创建自定义结构体数据类型时的一般方法(为了简化定义结构体数据类型变量):
typedef struct{
...//成员变量
} 类型名_t; (比如id_t、detail_t...)
- 相同类型的结构体变量可以直接互相赋值(如,sc = sd,数组不行)
5.1 数据对齐、数据补齐
5.1.1 数据对齐
执行以下程序结果是多少?
#include <stdio.h>
int main()
{
struct align{
char buf[2];
int num;
};
printf("sizeof(char) * 2 = %lu\n",sizeof(char) * 2);
printf("sizeof(int) = %lu\n",sizeof(int));
printf("sizeof(struct align) = %lu\n",sizeof(struct align));
return 0;
}
编译执行结果:
sizeof(char) * 2 = 2
sizeof(int) = 4
sizeof(struct align) = 8
为什么align类型结构体的大小不是6而是8?原因是结构体的子存储区(即,成员变量对应的存储区)需要遵守数据对齐原则
注意:
数据对齐:计算机通过按字节合并几个存储单元组成一个存储区来存放不同类型数据(比如,合并4个字节存储单元组成一个存储区来存放int型数据),但是存储单元的合并是有规则的:
相互合并存储单元的基数是4个字节,比如:
struct{
char buf[2];
int num;
};
数组buf需要2个字节的存储区,但是实际分配给buf的是4个字节。num的首地址不能取为buf的第三个字节的地址,而是第五个地址开始,尽管buf并没有使用第三、第四的字节。这就是程序输出结果为8的原因
存储区的地址一定是它自身大小的整数倍(特例,double存储区的地址只需要时4的整数倍即可),这种规则叫数据对齐,数据对齐可能造成结构体的字存储区之间有空隙
5.1.2 数据补齐
执行以下代码的输出结果是什么?原因是结构体的子存储区(即,成员变量对应的存储区)还需要遵守数据补齐原则
#include <stdio.h>
int main()
{
struct align{
char ch1;
int num;
char ch2;
};
printf("sizeof(char) = %lu\n",sizeof(char));
printf("sizeof(int) = %lu\n",sizeof(int));
printf("sizeof(struct align) = %lu\n",sizeof(struct align));
return 0;
}
编译执行结果:
sizeof(char) = 1
sizeof(int) = 4
sizeof(struct align) = 12
按照数据对齐原则,为什么align的大小不是9?
注意:
数据补齐:结构体存储区大小必须是它所包含的占空间最大的内置类型子存储区大小的整数倍(特例,如果最大的内置类型是double,则结果体存储区的大小之需要是4的整数倍)。数据补齐可能造成结构体最后几个字节的浪费
6. 枚举类型
- 枚举类型存储区就是整数类型存储区(对C语言而言,C++不是)
- 枚举类型存储区里只能放有限多个整型数据
- 枚举类型也需要先声明然后才能使用,声明枚举类型的时候需要提供几个名称,计算机为每个名称分配一个整数。只有这些整数才能记录到这种枚举类型的存储区里不同枚举类型所能记录的整数范围不同
- 声明枚举类型需要使用enum关键字
- 枚举类型数据定义初始化形式:
enum{SPRING,SUMMER,AUTUMN,WINTER};(完整的定义形式:enum season{SPRING,SUMMER,AUTUMN,WINTER};但是,一般采用简写)
7. 回调函数(callback)
回调函数:会作被为函数实际参数使用的函数,回调函数的作用是使得通用功能函数和特定功能函数相互独立,在需要时才配合使用
#include <stdio.h>
void func_1(void){
printf("giraffe\n");
return;
}
void func_2(void){
printf("unicorn\n");
return;
}
void loop(int count,void (*func_p)(void)){
for(; count > 0; count-- ){
func_p();
}
}
int main()
{
void (*func_p)(void);
loop(2,func_1);
loop(5,func_2);
return 0;
}
以上,通用功能函数loop只负责循环,至于循环的具体内容,需要传入的回调函数来实现,这里体现回调函数将通用功能函数与特殊功能函数剥离的用途
8. 文本文件、二进制文件
文件都是采用二进制方式记录数据
如果文件里所有二进制数据都有对应的字符这种文件叫文本文件
除了文本文件以外的文件都叫做二进制文件
文本文件和二进制文件分别采用不同的方式进行操作(但是,文本文件也可以当作二进制文件进行操作)
文本文件和二进制文件区别详解
9. 文件的打开方式(fopen(3)的第二个参数)
" r ":只读,如果文件不存在打开失败
" r+ “:比” r "多了修改功能
" w ":只写,不能查看文件;只能从文件开头开始修改;
如果文件不存在就创建文件;如果文件存在就清空文件内容
" w+ “:比” w "多了查看功能
" a ":只能修改不能查看;修改方式是在文件原有内容
后追加新内容;如果文件不存在就创建文件;如果文件与存在,则不修改文件原有内容
" a+ “:比” a "多了查看功能
" b " :可以和上述任何一种打开方式混合使用,表示fopen(3)以二进制方式操作文件(而不是文本方式操作文件)
10. fprintf(3)、fscanf(3)
fprintf(3):从存储区读数据到指定文件
使用方式:fprintf(3)的第一个参数是要操作文件的文件指针,其余参数和语意和printf(3)的参数相同
fscanf(3):从文件读数据到指定存储区
使用方式:fscanf(3)的第一个参数是要操作文件的文件指针,其余参数和语意和scanf(3)的参数相同