环境:CLion2021.3;64位macOS Big Sur
在ANSI C的任何一种实现中,存在两个不同的环境。第一种是翻译环境,在这个环境中源代码被转换为可执行的机器指令;第二种是执行环境,它用于实际执行代码。
test.c --> test.exec的过程依赖的是翻译环境(当前为CLion),运行test.exec所依赖的是运行环境,产生运行结果。
程序的翻译环境
翻译环境工作流程:工程中的每个.c源文件单独经过编译器处理,变成.o(linux下的目标文件)文件,然后链接器会将所有的目标文件和链接库(.lib静态库)链接在一起生成可执行程序(exec)。因此,翻译环境可分为两部分:编译(依靠编译器)和链接(依靠链接器)
其中编译分为三个步骤:
(1)预编译(预处理),通过gcc test.c -E > test.i将预处理的后的文件重定向到test.i中,便于查看。通过查看stdio.h中的内容,说明了在预处理阶段完成了:
1.头文件的包含,其实就是将头文件中的内容拷贝到相应位置;
2.#define定义的符号和宏的替换,因此后期程序在编译,汇编的时候也是看不到的,后来程序运行也是看不到的,就不便于调试;
3.注释删除,因此程序中无论有多少注释,都不会给程序带来负担。
上述这些其实都是文本操作,也就是说,预处理阶段实际上是完成了一些文本操作。
(2)编译,通过gcc test.i(test.c也可以) -S对文件进行编译,生成test.s文件,把C语言代码转化为汇编代码,编译阶段完成了:
1.语法分析
2.词法分析
3.语义分析
4.符号汇总:将.c中的全局符号汇总,分别汇总
(3)汇编,通过gcc test.s -c对编译生成的.s文件处理,生成test.o文件(即目标文件),-c意思是汇编之后停止,不要进行下一步。test.o是elf格式的,可以使用objdump查看这个文件,汇编阶段完成了:
1.生成符号表:将编译中汇总的符号以及地址放在符号表中
2.把汇编代码转换成了机器指令(二进制指令)
接下来就是链接,链接就是将多个目标文件(.o)和链接库进行链接,生成可执行程序(.out,也是elf格式的)。
链接分为两个步骤:
(1)合并段表:将所有.o文件的相同段链接在一起生成.out文件
(2)符号表的合并和重定位:汇编生成的符号表中可能会包含一些无效的信息,因此需要去除无效信息保留有效的符号和地址来生成最终的符号表,运行时通过符号表中的地址就可以找到相应的符号(如函数等)。
程序的执行环境
程序执行的过程:
(1)程序必须载入到内存中。在有操作系统的环境中,一般由操作系统来完成。在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。
(2)程序的执行开始。接着便调用main函数
(3)开始执行程序代码。这个时候程序将使用一个运行时堆栈,又叫函数栈帧(stack),存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程中一直保留他们的值。
(4)终止程序。正常终止main函数,也有可能是意外终止。
预定义符号可以用作日志:
符号名 | 作用 |
---|---|
__FILE__ | 此句代码所在的源文件的路径 |
__LINE__ | 此句代码所在的行 |
__DATE__ | 执行此句代码的日期 |
__TIME__ | 执行此句代码的时间 |
__FUNCTION__ | 此句代码所在的函数名 |
__STDC__ | 遵循ANSI标准的话值为1,否则为定义 |
FILE *pf = fopen("log.txt", "w");
if (pf == NULL) {
perror("fopen");
return 1;
}
fprintf(pf, "%-65s %-22s %-19s %-15s %-15s %-15s %-10s\n","文件路径","日期","时间","所在函数","所在行","标准定义","循环次数");
for (int i = 0; i < 10; ++i) {
fprintf(pf, "%-60s %-19s %-17s %-10s %-10d %-10d %-10d\n", __FILE__, __DATE__, __TIME__, __FUNCTION__, __LINE__, __STDC__, i);
}
fclose(pf);
pf = NULL;
#define定义的符号和宏,在预处理阶段就原封不动的替换到相应位置。至于是否加分号,尽量不加,除非必须,因为会原封不动地替换,容易出错。
//#define定义符号
#define MAX 1000
#define reg register //为register这个关键字创建一个简短的名字
#define do_forever for(;;) //用形象的符号来替换一种实现
#define CASE break;case //在写case语句的时候自动把break写上
//定义的代码过长,可以分成几行写,除了最后一行,每行的后面都加一个反斜杠(续行符)
#define DEBUG_PRINT printf("file:%s\tline:%d\t \
date%s\ttime%s\n", \
__FILE__,__LINE__, \
__DATE__,__TIME__)
int main()
{
int n = 0;
switch (n) {
case 1:
CASE 2://使用定义的CASE,避免了写break
CASE 3:
; }
return 0;
}
#define name(parament-list) stuff
#define SQUARE(X) X*X//注意,这样定义有严重的问题
int main()
{
printf("%d\n", SQUARE(3));
printf("%d\n", 3 * 3);//上一行代码在预处理阶段就被替换成了此行代码
printf("%d\n", SQUARE(3 + 1));
printf("%d\n", 3 + 1 * 3 + 1);//参数不经任何处理,直接传递,传递后再计算,因此应定义为#define SQUARE(X) ((X)*(X))
return 0;
}
#define PRINT(X) printf("the value of "#X" is %d\n",X)
#define PRINT2(X, FORMAT) printf("the value of "#X" is "FORMAT"\n",X)
#define CAT(X, Y) X##Y
int main()
{
//#
printf("hello world\n");
printf("hello " "world\n");//两个字符串会拼接在一起,打印hello world
int a = 10;
PRINT(a);//the value of a is 10
// printf("the value of""a""is %d\n",a);//等价于上一行:#的作用是把一个宏参数变成对应的字符串。替换后#X变成了X所对应的字符串即"a"
int b = 20;
PRINT(b);//the value of b is 20
int c = 30;
PRINT(c);//the value of c is 30
float f = 3.14f;
PRINT2(f, "%f");
//##
int fudan2023 = 100;
printf("%d\n", CAT(fudan, 2023));
// printf("%d\n", fudan2023);//##作用就是将两个参数连接起来合成了一个符号,变成了fudan2023
return 0;
}
#define MAX(X, Y) ((X) > (Y)?(X):(Y))
int Max(int x, int y) {
return x > y ? x : y;
}
int main()
{
int a = 5;
int b = 8;
int m = MAX(a++, b++);//这种参数就会有副作用,因为会使原变量发生变化,小心使用
// int m = ((a++) > (b++) ? (a++) : (b++));等价于上一行
// 上一行的a++执行了一次,而b++执行了两次:问号前a++和b++都执行了,问号后只执行了b++,因此b为10,a为6
int m2 = Max(a, b);
printf("m = %d\n", m);//9
printf("a = %d\n", a);//6
printf("b = %d\n", b);//10
return 0;
}
#define MALLOC(num, type) (type*)malloc(num*sizeof(type))
int main()
{
int *p = MALLOC(10,int);//可以传递类型,这样使用可以更人性化一些
return 0;
}
命名约定:一般来说,宏名全部大写,函数名不要全部大写。
属性 | #define定义宏 | 函数 |
---|---|---|
代码长度 | 每次使用时,宏代码都会被插入到程序中。除了非常小的宏之外,程序的长度会大幅度增长 | 函数代码只出现于一个地方;每次使用这个函数时,都调用那个地方的同一份代码 |
执行速度 | 更快 | 存在函数的调用和返回的额外开销,所以相对慢一些 |
操作符优先级 | 宏参数的求值是在所有周围表达式的上下文环境里,除非加上括号,否则邻近操作符的优先级可能会产生不可预料的后果,所以建议宏在书写的时候多些括号 | 函数参数只在函数调用的时候求值一次,它的结果值传递给函数。表达式的求值结果更容易预测 |
带有副作用的参数 | 参数可能被替换到宏体中的多个位置,所以带有副作用的参数求值可能会产生不可预料的结果 | 函数参数只在传参的时候求值一次,结果更容易控制 |
参数类型 | 宏的参数与类型无关,只要对参数的操作是合法的,它就可以使用于任何参数类型 | 函数的参数是与类型有关的,如果参数的类型不同,就需要不同的函数,即使他们执行的任务是不同的 |
调试 | 宏是不方便调试的 | 函数是可以逐语句调试的 |
递归 | 宏是不能递归的 | 函数是可以递归的 |
int main()
{
int a = MAX;
#undef MAX;//移除宏定义,导致下边的宏不可用
printf("%d\n",MAX);//err
return 0;
}
在mac终端下尝试运行下面的代码,很明显,正常运行是无法通过的,因为m未定义:
使用命令行参数,即可正常编译运行
gcc test.c -D m=10// -D m=10:定义m=10 -a -D等是命令行参数
./a.out// 执行
当你希望有一些语句在满足条件时才编译的话,就需要用到一些条件编译指令,常用的条件编译指令有:
#if 常量表达式
//需要条件编译的语句
#endif
int main()
{
#if 1//1为真,执行;若为假(0),则不执行。
printf("hello");
#endif
return 0;
}
#if 常量表达式
//..
#elif 常量表达式
//..
#else
//..
#endif
int main()
{
#if 1==1
printf("hello");
#elif 1==1//由于第一个条件为真,其他所有的都不会执行,即使为真。就是说只能执行一个分支
printf("nihao");
#else
printf("hh");
#endif
return 0;
}
#ifdef PRINTF //没有#define PRTINF,因此下边一组语句不编译。只要定义了就行,不判断值
printf("hello");
#endif
#if defined(PRINTF)//与上一种等价
printf("hello2");
#endif
//不定义参与编译,与上一种情况相反
#ifndef PRINTF //定义了PRTINF,因此下边一组语句不编译。
printf("world");
#endif
#if !defined(PRINTF)//与上一种等价
printf("world2");
#endif
#if defined(OS_UNIX)
#ifdef OPTION1
unix_version_option1();
#endif
#ifdef OPTION2
unix_version_option2();
#endif
#elif defined(OS_MSDOS)
#ifdef OPTION2
msdos_version_option2();
#endif
#endif
头文件包含的内容:头文件的包含,类型的定义,函数的声明。
全局变量的定义和声明都放在头文件中,可能会导致重复定义的错误,不可取。
#include "filename"
#include
防止头文件被重复包含的方式:
(1)头文件中加入#pragma once
(2)头文件中使用条件编译:
#ifndef __TEST_H__
#define __TEST_H__
内容..
#endif
推荐一本书《C语言深度解剖》,里边有详细介绍,这里就不说了。