从0学起的C语言笔记(八、预处理、动态库和静态库)
文章目录
程序中有使用指针,结构体等知识,但要求不高,请大概先行了解后在进行学习
C语言的编译过程
gcc -E 文件名.c -o 文件名.i 预处理
gcc -S 文件名.i -o 文件名.s 编译
gcc -C 文件名.s -o 文件名.o 汇编
gcc 文件名.o -o 文件名.exe 链接
- 预编译
将.c头文件,宏定义展开,生成.i的文件 - 编译
将预处理之后的.i文件编译为.s的汇编文件 - 汇编
将.s的汇编文件生成.o的目标文件,
注意:linux与windows生成.obj的目标文件,简化为.o文件,mac生成.out的目标文件,也简化为.o文件,具体过程见扩展还没写.这里不做赘述,不是这里的重点 - 链接
具体主要用于window生成.exe,linux中.o文件就为“一个32位ELF的文件,类型是 relocatable ,就是可重定位。所以目标文件在linux又叫做可重定位文件
# include
#include < > //用尖括号包含的文件是在系统指定目录下的文件
#include " " //用引号包含的是先在当前目录下寻找,
//如果没有就在系统指定文件下寻找
例如
#include<stdio.h>
#include"myself.h"
#include"/usr/biff/myself.h"
集成开发环境中(IDE)也有标准路径和系统头文件的路径。一般的,都提供具体可选项,用于指定查找路径。
在UNIX中,使用双引号意味着优先查找本地目录,具体查找那个目录,这取决于编译器的设定。有些编译器会优先查找源文件所在目录,有些会优先查找当前工作目录。
示例与分析
头文件
names.h
//常量
#include<string>
#define SLEN 32
//结构体声明————————下一课笔记的内容
struct names
{
char first[SLEN];
char last[SLEN];
}
//类型定义
typedef struct names name;
//函数原型
void get_name(names *);
void show_name(const name *);
char * s_geta(char * st, int n);
头文件一般包含:
#define指令,结构体声明,typedef和函数原型。
注意:这些内容是编译器在创建可执行代码时所需要的数据,而不是可执行代码。为了便于学习这个源代码过于简单,通常的还有#ifndef和#define 防止多次包含头文件。稍后介绍。
源文件
names.c
#include<stdio.h>
#include"names.h" //引入头文件
//函数定义
void get_names(name * pn)
{
printf("请输入你的第一个名字");
s_geta(pn->first,SLEN);
printf("请输入你的下一个名字");
s_geta(pn->last,SLEN);
}
void show_names(const name * pn)
{
printf("%s, %s", pn->first,pn->last);
}
char * s_gets(char * st, int n)
{
char * ret_val;
char * find;
ret_val = fgets(st, n, stdin);
if(ret_val)
{
find = strchr(st, '\n');
if(find)
*find = '\0';
else
while(getchar() != '\n')
continue;
}
return ret_val;
}
注解:在这里get_names()函数通过s_gets()函数调用了fgets()函数,是为了避免目标数组溢出。
主函数文件
main.c
#include <stdio.h>
#include "names.h"
//一定记住names.c文件的位置
int main()
{
name candidate;
get_names(&candidate);
printf("感谢");
show_names(&&candidate);
printf("使用这个程序");
return 0;
}
注意要点
- 两个源文件都要使用 names 类型结构,所以他们都必须包含 names.h 头文件。
- 必须编译和链接 names.c 和 main.c 源代码文件。
- 声明和指令放在 names.h 头文件中,函数定义放在names.c源代码文件中。
#define
明示常量:#define
类对象宏
有些有宏代表值的宏,这一类宏被称为类对象宏
示例与分析
程序
请务必要输入编译器编译后理解!!!
#include<stdio.h>
#define TWO 2
#define OW "当你编出一个程序 ,便能立即看到你的思想的实现!\
所有的事情以一种非常有趣的方式联系在了一起,也\
正是这一类的东西促使我进入这一领域。"
#define FOUR TWO*TWO
#define FX printf("X is %d。\n",x);
#define FMT "X is %d.\n"
int main()
{
int x = TWO;
FX;
x = FOUR;
printf(FMT,x);
printf("%s\n",OW);
printf("TWO:OW\n");
return 0;
}
预处理器指令从#开始执行,到后面的第一个换行符为止。
每一个#define都有三部分组成
- #define指令本身
- 选定的缩写,也就是宏。有些宏代表值(如本例),这一类宏被称为类对象宏。宏的名称中不允许有空格,遵循C的变量定义规则
- (指令剩余部分)称为替换体或替换列表
#define | FX | printf(“X is %d。\n”,x); |
---|---|---|
预处理器指令 | 宏 | 替换体 |
记号(了解)
从技术角度,可以把宏的替换体看作记号(token)型字符串,“不是字符型字符串”。C语言预处理中记号是宏定义中单独的“词”。利用空格把这些东西分开。
示例与分析
#define FOUR 2*2
该宏定义有一个记号——“2*2”
#define SIX 3 * 2
该宏定义有三个记号——“3“,”*”,“2”
替换体中如果有多个空格,字符型和记号型处理方式不同,考虑如下
#define EIGHT 4 * 8
如果预处理器把该替换体看作是字符型字符串,将用4 * 8代替EIGHT。即,额外的空格完全是替换体的一部分。如果预处理器把该替换体看作是记号型字符串,则用3个记号 4 * 8 代替EIGHT。
换言之,解释字符型字符串会把空格记为替换体的一部分;记号型字符串会把空格记为多个记号的分隔符。
常量重定义
概念:
假设先把LIMIT定义为21,后来在该文件中又定义为26。这一过程被称之为重定义常量
编程时,头文件中引用头文件,定义全局变量,一定要慎重考虑
不同的实现方式采用不同的重定义方案。除非新旧定义相同,否则这些定义会被编译器认定是错误。还有一些实现方式允许重定义,但会给出警告。
ANSI标准采用第一种结局方案,只有新定义和旧定义完全相同才被允许重定义。
例:
具有相同的定义意味着替换体中的记号必须相同,且顺序相同。
示例与分析
#define SIX 2 * 3
#define SIX 2 * 3
这两条定义都有相同三个记号,额外的空格不算替换体的一部分。
#define SIX 2*3
这条定义只用一个记号,与前两个不同。如果需要重新定义,就需要使用 #undef 指令
若确实需要重定义常量,则使用 const 关键字和作用域规则更容易一些
#define MAX 20
#include "myself.h"
#undef MAX
只有myself.h中的函数可以使用宏
在#define中使用参数
概念
#define S(a,b) a*b
注意带参宏的形参 a 和 b 没有类型名,
S(2,4) 将来在预处理的时候替换成 实参替代字符串的形参,其他字符保留,2*4
示例与分析
该示例有些问题,请复制执行后再仔细阅读分析
#include<stdio.h>
#define S(X) X*X
#define PR(X) printf("这一宏的返回值为%d",X)
int main()
{
int z;
int x = 5;
printf("X的返回值1为%d",x);
z = S(x);
printf("S(x): ");
PR(z);
z = S(2);
printf("S(2): ");
PR(2);
printf("S(x+2): ");
PR(S(x+2));
printf("100/S(x): ");
PR(100/S(x));
printf("X的返回值2为%d",x);
printf("S(++x)");
PR(S(++x));
printf("X的返回值2为%d",x);
return 0;
}
执行结构如下
前两行的输出与预期相同。但是,,,这个后面的东西就看起来又点放飞自我了。
在解释之前先说几句:
- 编译器换值,不计算!!!!
所以第三行,17 的计算方式为 x + 2 * x + 2 = 5 + 2 * 5 + 2 = 5 + 10 + 2 = 17。第四行100的计算方式为100/5*5=100。
第六行 42的计算方式为 ++x*++x 两次自增。
第一次在乘号之前 (5 + 1)*++x ,第二次在乘号之后(5 + 1)*(6 + 1) = 42。但是,由于标准并没有对这类运算顺序做出规定。所以,有的编译器输出为42,也有一些在乘法顺序之前,输出为 7*7=49。
在C的标准中对此种行为定义为未定义行为。无论哪种情况,x初始值为5,最终结果都是x = 7。这也是不被允许的。
注意要点
我们在使用时要避免 ++x 被作为宏参数!
带参宏和带参函数的区别
带参宏被调用多少次就会展开多少次,执行代码的时候没有函数调用的过程,不需要压栈弹栈。
所以带参宏,是浪费了空间,因为被展开多次,节省时间。 带参函数,代码只有一份,存在代码段,调用的时候去代码段取指令,调用的时候要,压栈弹栈。有个调用的过程。
所以说,带参函数是浪费了时间,节省了空间。
带参函数的形参是有类型的,带参宏的形参没有类型名。
条件编译
可以使用指令创建条件编译。告诉编译器根据编译时的条件执行或者忽略信息和代码。
#ifdef、#else和#endif指令
#ifdef MAVIS
#include "horse.h"
#define STABLES 5
#else
#include "cow.h"
#define STABLES 15
#endif
#ifdef指令:假设在#ifdef MAVIS之前已经定义了MAVIS,则执行#ifdef与#else之间的语句,#include “horse.h” 和 #define STABLES 5语句;假设在#ifdef MAVIS之前没有定义定义了MAVIS,则执行#else与#endif之间的语句,则执行#include “cow.h” 和 #define STABLES 15语句。
静态库
- 动态编译 动态编译使用的是动态库文件进行编译 gcc hello.c -o hello 默认的咱们使用的是动态编译方法
- 静态编译 静态编译使用的静态库文件进行编译 gcc -static hello.c -o hello
- 静态编译和动态编译区别 1:使用的库文件的格式不一样
动态编译使用动态库,静态编译使用静态库
注意:
静态编译要把静态库文件打包编译到可执行程序中。
动态编译不会把动态库文件打包编译到可执行程序中,它只是编译链接关系
示例
mytest.c
#include <stdio.h>
#include "mylib.h"
int main(int argc, char *argv[])
{
int a=10,b=20,max_num,min_num;
max_num=max(a,b);
min_num=min(a,b);
printf("max_num=%d\n",max_num);
printf("min_num=%d\n",min_num);
return 0;
}
mylib.c
int max(int x,int y)
{
return (x>y)?x:y;
}
int min(int x,int y)
{
return (x<y)?x:y;
}
mylib.h
extern int max(int x,int y);
extern int min(int x,int y);
以linux为例简单了解静态库的建立过程
- 制作静态态库:
gcc -c mylib.c -o mylib.o
ar rc libtestlib.a mylib.o
注意:静态库起名的时候必须以 lib 开头以.a 结尾 - 编译程序:
gcc -static mytest.c libtestlib.a -o mytest - 编译程序的命令
gcc -static mytest.c –o mytest -ltestlib
结语
非常感谢您能看到这里,下一次写C语言的又一大难点——指针(1)