写在前面:
- 本系列专栏主要介绍C语言的相关知识,思路以下面的参考链接教程为主,大部分笔记也出自该教程。
- 除了参考下面的链接教程以外,笔者还参考了其它的一些C语言教材,笔者认为重要的部分大多都会用粗体标注(未被标注出的部分可能全是重点,可根据相关部分的示例代码量和注释量判断,或者根据实际经验判断)。
- 如有错漏欢迎指出。
参考教程:C语言程序设计从入门到进阶【比特鹏哥c语言2024完整版视频教程】(c语言基础入门c语言软件安装C语言指针c语言考研C语言专升本C语言期末计算机二级C语言c语言_哔哩哔哩_bilibili
一、程序环境及编译+链接
1、程序的翻译环境和执行环境
在ANSI C的任何一种实现中,存在两个不同的环境:
(1)第1种是翻译环境,在这个环境中源代码被转换为可执行的机器指令。
(2)第2种是执行环境,它用于实际执行代码。
2、翻译环境下的工作
(1)组成一个程序的每个源文件通过编译过程分别转换成目标代码(object code),每个目标文件由链接器(linker)捆绑在一起,形成一个单一而完整的可执行程序。
(2)链接器同时也会引入标准C函数库中任何被该程序所用到的函数,而且它可以搜索程序员个人的程序库,将其需要的函数也链接到程序中。
3、编译本身分为几个阶段
(1)预处理(预编译):预处理完成之后就停下来,预处理之后产生的结果都放在test.i文件中。(把#include包含的文件全部拷贝进文件,注释全部删除,#define全部代入替换)
(2)编译:经过语法分析、词法分析、语义分析和符号汇总(把在全局中的符号汇总,比如全局变量名及函数名)把C代码转换成汇编代码,编译完成之后就停下来,结果保存在test.s中。
(3)汇编:汇编完成之后就停下来,结果保存在test.o中。(把汇编代码转换为二进制/机器指令,利用编译阶段符号汇总的结果形成符号表)
(4)最后还要进行链接,包括合并段表以及符号表的合并和符号表的重定位(如上图,sum有两个地址,重定位会在其中选取一个有效的地址,然后进行合并),然后生成可执行程序.elf。
4、程序执行的过程
(1)程序必须载入内存中。
①在有操作系统的环境中,一般这个由操作系统完成。
②在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。
(2)程序的执行开始——调用main函数。
(3)开始执行程序代码,这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回地址,程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程一直保留他们的值。
(4)终止程序——正常终止main函数;也有可能是意外终止。
二、预处理(一)
1、C语言内置的预定义符号
__FILE__ //进行编译的源文件
__LINE__ //文件当前的行号
__DATE__ //文件被编译的日期
__TIME__ //文件被编译的时间
__STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义
2、#define 定义标识符
(1)用#define指令指定一个符号名称(例如PI)代表一个常量(例如3.1415),这个常量就是符号常量,它不占内存,在预编译后这个符号就不存在了。
(2)经过#define的指定后,文件从该行开始所有的(PI)都代表3.1415,对程序进行编译前,预处理器先对PI进行处理,把代码中所有PI全部置换为3.1415,在预编译后,符号常量就全部变成了字面常量3.1415。
(3)“#define 定义标识符”的好处:
①含义清楚。
②在需要改变程序中多出用到的同一个常量时,能做到“一改全改”,非常方便。
(4)#define指令不止可以定义符号常量,它可以把一大段内容(比如一个函数的调用)用一个简短的标识符代替,对程序进行编译前,预处理器会将代码中出现过的这个简短的标识符全部替换成那一大段内容。
(5)举例:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#define MAX 1000
#define reg register //为 register这个关键字,创建一个简短的名字
#define do_forever for(;;) //用更形象的符号来替换一种实现
#define CASE break;case //在写case语句的时候自动把 break写上。
// 如果定义的 stuff过长,可以分成几行写,除了最后一行外,每行的后面都加一个反斜杠(续行符)。
#define DEBUG_PRINT printf("file:%s\tline:%d\tdate:%s\ttime:%s\n" ,\
__FILE__,__LINE__ ,\
__DATE__,__TIME__ )
int main()
{
DEBUG_PRINT;
return 0;
}
3、#define 定义宏
(1)#define 机制包括了一个规定,它允许把参数替换到文本中,这种实现通常称为宏(macro)或定义宏(define macro)。
#define name( parament-list ) stuff
①其中parament-list是一个由逗号隔开的符号表,它们可能出现在stuff中。
②参数列表的左括号必须与name紧邻,如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。
③stuff中的参数以及整个stuff最好都加括号,这是为了防止运算符优先级引发若干问题。
④在代码中使用定义的宏(函数)时,格式为“name(parament-list)”。
(2)举例:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#define SQUARE(x) ((x) * (x)) //整个表达式以及每个参数最好都加括号
int main()
{
int a = 5;
printf("%d\n", 10 * SQUARE(a + 1));
//printf("%d\n", 10 * ((a + 1) * (a + 1)));
return 0;
}
4、#define 的替换规则
(1)在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号,如果是,它们首先被替换。
(2)替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被它们对应的值替换。
(3)最后再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号,如果是,就重复上述处理过程。
(4)需要注意的是:
①宏参数和#define 定义中可以出现其他#define定义的变量,但是对于宏,不能出现递归。
②当预处理器搜索#define定义的符号的时候,字符串常量的内容并不会被搜索。
5、#和##
(1)使用“#”可以把一个宏参数变成对应的字符串,具体方法是在stuff中的字符串参数的首部(双撇号内)添加一个“#”。
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#define PRINT(FORMAT, VALUE)(printf("the value of " #VALUE " is "FORMAT "\n", VALUE))
int main()
{
int i = 1;
PRINT("%d", i + 3);
return 0;
}
(2)使用“##”可以把位于它两边的符号合成一个符号,它允许宏定义从分离的文本片段创建标识符。
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#define ADD_TO_SUM(num, value) (value##num)
int main()
{
int sum = ADD_TO_SUM(5, 10);
printf("%d\n", sum);//105
return 0;
}
6、带副作用的宏参数
(1)当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么在使用这个宏的时候就可能出现危险,导致不可预测的后果。
(2)举例:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#define MAX(a, b) ( (a) > (b) ? (a) : (b) )
int main()
{
int x = 5;
int y = 8;
int z = MAX(x++, y++);
//z = ( (x++) > (y++) ? (x++) : (y++));
printf("x=%d y=%d z=%d\n", x, y, z); //x=6 y=10 z=9
return 0;
}
7、宏(函数)和普通函数的对比
(1)用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多,所以宏比函数在程序的规模和速度方面更胜一筹。
(2)更为重要的是函数的参数必须声明为特定的类型,所以函数只能在类型合适的表达式上使用;反之宏可以适用于整形、长整型、浮点型等可以用于>来比较的类型,宏是类型无关的。
(3)当然,和函数相比,宏也有劣势的地方:
①每次使用宏的时候,一份宏定义的代码将插入到程序中,除非宏比较短,否则可能大幅度增加程序的长度。
②宏是没法调试的。
③宏由于类型无关,也就不够严谨。
④宏可能会带来运算符优先级的问题,导致程容易出现错。
(4)宏有时候可以做函数做不到的事情,比如宏的参数可以出现类型,但是函数做不到。
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
#define MALLOC(num, type)\
(type *)malloc(num * sizeof(type))
int main()
{
//使用
MALLOC(10, int);//类型作为参数
//预处理器替换之后:
//(int *)malloc(10 * sizeof(int));
return 0;
}
8、#undef
这条指令用于移除一个宏定义,使用方式如下。
#undef NAME
//如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除。
三、预处理(二)
1、条件编译
(1)在编译一个程序的时候,如果要将一条语句(一组语句)编译或者放弃是很方便的,因为C语言有条件编译指令。
(2)调试性的代码(比如用printf输出一些中间变量)删除可惜,保留又碍事,这时就可以选择性地编译。
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#define __DEBUG__
int main()
{
int i = 0;
int arr[10] = { 0 };
for (i = 0; i < 10; i++)
{
arr[i] = i;
#ifdef __DEBUG__
printf("%d\n", arr[i]);//为了观察数组是否赋值成功。
#endif //__DEBUG__
}
return 0;
}
(3)常见的条件编译指令:
①单个分支的条件编译:
#if 常量表达式
//...
#endif
//常量表达式由预处理器求值,其逻辑和一般的if语句相同,表达式结果不为零即编译其下的内容
如:
#define __DEBUG__ 1
#if __DEBUG__
//..
#endif
②多个分支的条件编译:
#if 常量表达式
//...
#elif 常量表达式
//...
#else
//...
#endif
③判断是否被定义:
#if defined(symbol)
#ifdef symbol //如果有用#define定义symbol
#if !defined(symbol)
#ifndef symbol //如果没有用#define定义symbol(这条指令常用于防止头文件被重复包含)
//这四条指令都需接#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 //每条条件编译指令都需要配对一个“#endif”
2、offsetof宏的实现
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stddef.h> //使用offsetof需要包含此头文件
//写一个宏,计算结构体中某变量相对于首地址的偏移,并给出说明
#define OFFSETOF(type,member) (size_t)&(((type*)0)->member)
struct S
{
char c1;
int i;
char c2;
};
int main()
{
struct S s = { 0 };
printf("%d\n", OFFSETOF(struct S, c1));
printf("%d\n", OFFSETOF(struct S, i));
printf("%d\n", OFFSETOF(struct S, c2));
return 0;
}