组成一个程序的每个源文件通过编译过程分别转换成目标代码
每个目标文件由链接器(linker)捆绑在一起,形成一个单一而完整的可执行程序
链接器同时也会引入标准C函数库中任何被该程序所用到的函数,而且它可以搜索程序员个人 的程序库,将其需要的函数也链接到程序中
处于func.c文件
int g_val = 2016;
int sum(const int x, const int y)
{
return x + y;
}
处于test.c文件
#include
int main()
{
extern int sum(const int x, const int y);
extern int g_val;
printf("%d\n", g_val);
print("%d\n", sum(1,2));
return 0;
}
如何查看编译期间每一步发生了什么:(Linux系统下输入命令行)
1. 预处理 选项 gcc -E test.c -o test.i 预处理之后产生的结果都放在test.i文件中
2. 编译 选项 gcc -S test.c -o test.s 编译完成之后结果保存在test.s中
3. 汇编 选项 gcc -c test.c -o test.o 汇编完成之后结果保存在test.o中
程序执行过程:
1. 程序执行必须载入内存中。在有操作系统的环境中,一般这个由操作系统完成;在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成
2. 程序的执行开始。调用main函数
3. 开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回 地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程一直保留他们的值
4. 终止程序。正常终止main函数;也有可能是意外终止
__FILE__ 进行编译的源文件
__LINE__ 文件当前的行号
__DATE__ 文件被编译的日期(当前时间戳)
__TIME__ 文件被编译的时间
__STDC__ 如果编译器遵循ANSI C,其值为1,否则未定义
printf("file:%s line:%d\n", __FILE__, __LINE__);
printf("date:%d time:%d\n", __DATE__, __TIME__);
这些预定义符号都是语言内置的
#define NUM 1000
#define reg register 为 register这个关键字,创建一个简短的名字
#define do_forever for(;;) 用更形象的符号来替换一种实现
#define CASE break;case 在写case语句的时候自动把 break写上。
如果定义的stuff过长,可以分成几行写,除了最后一行外,每行的后面都加一个反斜杠(续行符)
#define DEBUG_PRINT printf("file:%s\tline:%d\t \
date:%s\ttime:%s\n" , \
__FILE__,__LINE__ , \
__DATE__,__TIME__ )
注意:在在define定义标识符的时候,不要在最后加上 ;
#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义 宏(define macro)
#define name( parament-list ) stuff
其中的 parament-list 是一个由逗号隔开的符号表,它们可能出现在stuff中
注意:
参数列表的左括号必须与name紧邻,如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分
代码1
#define SQUARE_1( x ) x * x
#define SQUARE_2( x ) (x) * (x)
int a = 5;
printf ("%d\n", SQUARE_1(a + 1));
printf ("%d\n", SQUARE_2(a + 1));
代码2
#define DOUBLE_1( x ) (x) + (x)
#define DOUBLE_2( x ) ((x) + (x))
int a = 5;
printf ("%d\n", DOUBLE_1(a) * 5);
printf ("%d\n", DOUBLE_2(a) * 5);
注意 :用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,
避免在使用宏时由于参数中 的操作符或邻近操作符之间不可预料的相互作用
1. 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换
2. 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换
3. 最后,再次对结果文件进行扫描,看看它是否包含任何有其他#define定义的符号,如果有,就重复上述处理过程
注意:
1. 宏不能出现递归
2. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索
#include
#define BSC //
#define BMC /*
#define EMC */
注意:EMC已经被是先被注释掉了,BMC成为空
int main()
{
BSC printf("hello world\n");
BMC printf("hello bit\n"); EMC
return 0;
}
结论:预处理期间:先执行去注释,再进行宏替换
#include
#define INIT_VALUE(a,b) do\
{ a = 0;\
b = 0;\
}while(0)
int main()
{
int flag = 0;
scanf("%d", &flag);
int a = 100;
int b = 200;
printf("before: %d, %d\n", a, b);
if(flag){
INIT_VALUE(a,b);
}
else{
printf("error!\n");
}
printf("after: %d, %d\n", a, b);
return 0;
}
结论:当需要宏进行多语句替换的时候, 推荐使用do-while-zero结构,如若可以,宏方面的东西,推荐少用
const char* p = "你好吗? ""我很好!\n";
printf("你好吗? ""我很好!\n");
printf("%s", p);
printf(p);
字符串是有自动连接的特点的
# 的作用:
# 可以一个宏参数变成对应的字符串
代码1:利用字符串是有自动连接的特点
#define PRINT(FORMAT, VALUE)\
printf("the value is "FORMAT"\n", VALUE);
PRINT("%d", 10);
代码2:使用 # ,把一个宏参数变成对应的字符串
int i = 10;
#define PRINT(FORMAT, VALUE)\
printf("the value of " #VALUE "is "FORMAT "\n", VALUE);
PRINT("%d", i+3);
## 的作用:
##可以把位于它两边的符号合成一个符号
#define ADD_TO_SUM(num, value) sum_##num += value;
int sum_1 = 1;
ADD_TO_SUM(1, 10);
printf("sum_1=%d\n", sum_1);
注意:这样的连接必须产生一个合法的标识符,否则其结果就是未定义的
#define MAX(a, b) ( (a) > (b) ? (a) : (b) )
x = 5;
y = 8;
z = MAX(x++, y++);
printf("x=%d y=%d z=%d\n", x, y, z);
把宏名全部大写
函数名不要全部大写
优点:
1. 增强代码的复用性
2. 提高性能
缺点:
1. 不方便调试宏(因为预编译阶段进行了替换)
2. 导致代码可读性差,可维护性差,容易误用
3.没有类型安全的检查
有哪些技术替代宏?
1. 常量定义:换用const enum
2. 短小函数定义:换用内联函数
这条指令用于移除一个宏定义
#undef NAME
如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除
例:
代码1
#include
#define M 10
int main()
{
#define N 100
printf("%d, %d\n", M, N);
printf("%d, %d\n", M, N);
printf("%d, %d\n", M, N);
#undef M //取消M
#undef N //取消N
从该位置开始,M,N,不再被识别
printf("%d, %d\n", M, N);
printf("%d, %d\n", M, N);
printf("%d, %d\n", M, N);
return 0;
}
代码2
int main()
{
#define Y X*2
#define X 3
printf("%d\n", Y);//6
#define X 2
printf("%d\n", Y);//4
return 0;
}
///
代码3
void print(void);
#include
int main()
{
#define X 3
#define Y X*2
print();//4
#undef X
#define X 2
print();//4
return 0;
}
void print(void)
{
printf("%d\n", Y);
}
//
代码4
int main()
{
#define X 3
#define Y X*2
#define X 2
printf("%d\n", Y);//4
return 0;
}
代码5
#include
int main()
{
#define X 3
#define Y X*2
printf("%d\n", Y);//6
#undef X
#define X 2
printf("%d\n", Y);//4
return 0;
}
结论:undef是取消宏的意思,可以用来限定宏的有效范围
#pragma warning(disable:4996) 禁止4996报错
#pragma pack() 设置默认对齐数与内存对齐强相关
Linux环境演示:gcc -D __DEF__=1 test.c -o test
int main()
{
#ifdef __DEF__
printf("defined\n");
#else
printf("not define\n");
#endif
return 0;
}
常见的条件编译指令:
#if 常量表达式
#endif 常量表达式
#else
#endif 有#if必带
多个分支的条件编译
#if 常量表达式
...
#elif 常量表达式
...
#else
...
#endif
判断是否被定义
#if defined(symbol)
#ifdef symbol
#if !defined(symbol)
#ifndef symbol
嵌套指令
#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
本质认识:
条件编译,其实就是编译器根据实际情况,对代码进行裁剪。而这里“实际情况”,取决于运行平台,代码本身的业务逻辑等
两个好处:
1. 可以只保留当前最需要的代码逻辑,其他去掉。可以减少生成的代码大小
2. 可以写出跨平台的代码,让一个具体的业务,在不同平台编译的时候,可以有同样的表现
本地文件包含
#include "filename"
查找策略:先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标
准位置查找头文件,如果找不到就提示编译错误
标准库文件包含
#include
查找策略:查找头文件直接去标准路径下去查找,如果找不到就提示编译错误
Linux环境的标准头文件的路径:
/usr/include
VS环境的标准头文件的路径:
D\Windows Kits\10\Include\10.0.20348.0\ucrt
注意按照自己的安装路径去找
库文件也可以使用 “” 的形式包含但是这样做查找的效率就低些,也不容易区分是库文件还是本地文件
每个头文件的开头写:
#ifndef __TEST_H__
#define __TEST_H__
头文件的内容
#endif //__TEST_H__
或者:
#pragma once
可以避免头文件的重复引入