结构体是一些值得集合,这些值称为成员变量。结构体得每个成员可以是不同类型得变量。
语法:
struct tag
{
member-list;
}variable-list;
创建方法一:(普通创建)
struct Stu
{
char name[20];
char tele[20];
char sex[10];
int age;
}s4, s5, s6; //全局变量
struct Stu s3; //全局变量
int main()
{
struct Stu s1; // 创建结构体变量s1,局部变量
struct Stu s2;
return 0;
}
创建方法二:(匿名创建,不推荐用)
// 创建时省略了结构体标签,但是在后面必须跟结构体得名字
#include
#include
#include
struct
{
int a;
char b;
float c;
}x; // 由于tag被省略,这里必须跟结构体的名字
struct
{
int a;
char b;
float c;
}* psa; // 匿名结构体指针类型
// 结构体 *psa 和 x 虽然内容是一样的,但是编译器在执行时,会把他们当作两个不同类型来处理
struct
{
int a;
char b;
float c;
}a[20],*p;
int main()
{
x.a = 10;
x.b = 'A';
x.c = 3.14;
// 使用x访问结构体成员
printf("%d\n", x.a);
printf("%c\n", x.b);
printf("%.2f\n", x.c);
return 0;
}
结构体中不能包含一个类型为该结构本身的成员,这样会形成死递归,然后内存无限大,但是可以用结构体类型的指针来代替,这也是数据类型中链表概念在C当中的实现。
// 这个写法不行,错误示范,因为它的内存无限大,sizeof(struct Node)的大小没法计算
struct Node
{
int data;
struct Node n;
};
// 正确示范
struct Node
{
int data;
struct Node* next; // 结构体类型指针,存放下一个节点的地址
};
// 重命名结构体
typedef struct Node
{
int data;
struct Node* next;
}Node;
int main()
{
struct Node n1;
Node n2;
return 0;
}
struct T
{
double weight;
short age;
};
struct S
{
char c;
struct T st;
int a;
double d;
char arr[20];
};
int main()
{
struct S s = { 'c', {55.6, 30},100, 3.14, "hello bit"};
printf("%c %lf %d %d %lf %s\n", s.c, s.st.weight, s.st.age, s.a, s.d, s.arr);
return 0;
}
结构体对其规则:
1. 第一个成员在结构体变量偏移量为0的地址处。
2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大
对齐数(含嵌套结构体的对齐数)的整数倍。
注:对齐数 = 编译器默认的一个对其书数 与 该成员大小的较小值,VS中默认值为8,gcc没有默认对齐数。
内存对齐存在原因:
1. 平台原因(已知原因):不是所有的硬件平台都能访问任意地址上的任意数据;某些硬件平台只能在某些地址
处取某些特定类型的数据,否则抛出硬件异常。
2. 性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐内存,处理器
需要作两次内存访问;而对齐的内存访问仅需要一次访问。
总体来说,结构体的内存对齐是拿空间来换取时间的做法。
结构体设计注意:尽量让占用空间小的成员尽量集中在一起。
struct S1
{
char c;
int a;
char c1;
};
struct S2
{
char c;
char c1;
int a;
};
// 上面定义了两个结构体,他们除了c1的位置不一样,其他都一样,下面使用sizeof计算大小
int main()
{
struct S1 s1 = { 0 };
printf("%d\n", sizeof(s1)); // 结果:12
struct S2 s2 = { 0 };
printf("%d\n", sizeof(s2)); // 结果:8
return 0;
}
修改结构体的默认对齐数:
#pragma pack(4) // 预处理指令,设置默认对齐数为4,本来的大小是16
struct S2
{
char c;
double d;
};
int main()
{
struct S2 s2;
printf("%d\n", sizeof(s2)); // VS中结果为12
return 0;
}
#include
#include
#include
#include
struct S2
{
char c;
int i;
double d;
};
int main()
{
// offsetof,返回结构体成员相对于结构体的偏移量是多少,它是个宏
printf("%d\n", offsetof(struct S2, c));
printf("%d\n", offsetof(struct S2, i));
printf("%d\n", offsetof(struct S2, d));
return 0;
}
#include
#include
#include
#include
struct S
{
char c;
int i;
double d;
};
void Init(struct S* ps)
{
ps->c = 'a';
ps->i = 10;
ps->d = 3.1568;
}
Print(struct S tmp)
{
printf("%d %c %lf\n", tmp.i, tmp.c, tmp.d);
}
int main()
{
struct S s = { 0 };
Init(&s); // 对S的内容进行初始化
Print(s);
return 0;
}
位段的声明和结构是类似的,但是有两个不同:
1. 位段的成员必须是 int、unsigned int、singed int。
2. 位段的成员名后边有一个冒号和一个数字。
// 位段:其中的位指二进制位
struct A
{
int _a : 2; // 2代表着2个bit
int _b : 5; // 5个bit
int _c : 10; // 10个bit
int _d : 30; // 20个bit
};
// 加起来一共47bit,6个字节,但是实际打印是8个,那是因为位段开辟空间的时候也有内存分配
int main()
{
struct A s = { 0 };
printf("%d\n", sizeof(s));
return 0;
}
位段的内存分配
1. 位段的成员可以是 int、unsigned int、singed int或者是char(属于整型)类型。
2. 位段的空间是以4个字节或者1个字节的方式来开辟的。
3. 位段设计很多不确定因素,位段是不跨平台的,可移植程序应该避免使用位段。
位段的跨平台问题
1. int位段被当成有符号数和无符号数是不确定的。
2. 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机器会出问题)
3. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
4. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳第一个位段剩余位时舍弃剩余位还是利用不确定。
注: 跟结构相比,位段可以达到同样的效果,但是可以很好的节省空间,但是有跨平台的问题存在。
枚举就是列举,把可能的值一一列举。
例如:周一到周天、性别、月份。
enum Day
{
// 给常量赋一个初始值,但是不能改
Mon = 2,
Tues = 4,
Wed, // 这个就会沿着上面的初始值,这个就是 5
Thur,
Fri,
Sat,
Sun
};
enum Sex
{
// 枚举的可能取值--常量
MALE,
FEMALE,
SECRET
};
int main()
{
enum Sex s = MALE;
s = FEMALE;
printf("%d %d \n", MALE, SECRET); // 结果:0, 2
return 0;
}
可以使用 #define 定义常量,为什么还要使用枚举
1. 增加代码的可读性和可维护性
2. 和#define定义的标识符比较,枚举有类型检查,更加严谨
3. 放置了命名污染(封装)
4. 便于调试
5. 使用方法,一次可以定义多个常量
联合也是一种特殊的自定义类型,这种类型定义的变量也包含一系列的成员,特征是这些成员公用同一块空间,所以,
联合也叫做共用体。
union Un
{
char c;
int i;
};
int main()
{
union Un u;
printf("%d\n", sizeof(u)); // 结果:4 字节
printf("%p\n", &u); // 结果:00000001000FFB04
printf("%p\n", &(u.c)); // 结果:00000001000FFB04
printf("%p\n", &(u.i)); // 结果:00000001000FFB04
// 发现c和i公用一块空间,所以它才叫联合体(共用体)
return 0;
}
联合成员是共用一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小
计算计算机时大端还是小端存储,解法如下:
// 解法一
#include
#include
#include
#include
check_sys()
{
int a = 1;
// 返回1表示小端,返回0表示大端
return *(char*)&a;
}
int main()
{
// int a = 0x11 22 33 44;
// 高字节 低字节
// ---[][][][][][][][][][][][][][][][][][][][][]---
// 低地址 高地址
// ---[][][11][22][33][44][][][][][]--- 高字节放低地址,低字节放高地址 ==== 大端字节序存储模式
// ---[][][44][33][22][11][][][][][]--- 低字节放低地址,高字节放高地址 ==== 端字节序存储模式
int a = 1;
int ret = check_sys();
if (1 == ret)
{
printf("小端字节序");
}
else
{
printf("大端字节序");
}
return 0;
}
// 解法二
#include
#include
#include
#include
check_sys()
{
union Un
{
char c;
int i;
}u;
u.i = 1;
// 返回1:小端字节序,返回0:大端字节序
return u.c;
}
int main()
{
int ret = check_sys();
if (1 == ret)
{
printf("小端字节序");
}
else
{
printf("大端字节序");
}
return 0;
}
1. 联合的大小至少是最大成员的大小。
2. 当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。
已知内存开辟方式有:
int val = 20; // 在栈空间开辟四个字节
char arr[10] = {0}; // 在栈空间上开辟10个字节的连续空间
上述方式有两种特点
1. 空间开辟的大小是固定的。
2. 数组在申明的时候,必须指定数组的长度,它所需要的内存在编译时分配。
如果需要开辟的空间大小在程序运行时才知道,那么就需要用到动态内存了。
malloc 、free
void* malloc (size_t size);
malloc 函数向内存申请一块连续可用的空间,并返回指向这块空间的指针
1. 如果开辟成功,返回一个指向开辟好空间的指针
2. 如果开辟失败,返回一个NULL指针,因此,malloc的返回值一定要做检查
3. 返回值类型是 void *,所以malloc函数并不知道开辟空间的类型,具体由使用者自己决定
4. 入股哦参数size为0,malloc的行为是标准的还是未定义的,取决于编译器
void free(void* ptr);
free 函数用来释放动态开辟的内存
1. 如果参数 ptr 指向的空间不是动态开辟的,那 free 函数的行为是未定义的
2. 如果参数 ptr 是NULL指针,则函数什么事都不做
#include
#include
#include
#include
#include
int main()
{
// 申请 10 个整形的空间
int* p = malloc(10 * sizeof(int));
// int* p = (int*)malloc(10 * sizeof(int)); 如果有警告,前面加(int*)
if (p == NULL)
{
printf("错误信息:%s\n", strerror(errno)); // strerror(errno): 用来打印错误信息
}
else
{
// 正常使用空间
int i = 0;
for (i = 0; i < 10; i++)
{
*(p + i) = i;
}
for (i = 0; i < 10; i++)
{
printf("%d ", *(p + i));
}
}
// 当动态申请的空间不再使用时,就该还给操作系统,通过free函数可以还,或者程序结束时,也会主动归还
free(p);
p = NULL; // 防止野指针,虽然空间已经归还了,但是p还是指向那块地址
return 0;
}
calloc
void* callloc(size_t num, size_t size); 元素个数,每个元素的长度
1. 函数的功能是为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为0
2. 与 malloc 的区别只在于 calloc 会在返回地址前把申请的空间的每个字节初始化为全0
#include
#include
#include
#include
#include
int main()
{
int* p = (int*)calloc(10, sizeof(int)); // 它会初始化内存空间值为0,malloc不会初始化
if (p == NULL)
{
printf("%s\n", strerror(errno));
}
else
{
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", *(p + i));
}
}
// 释放空间
// free是来释放动态开辟的空间的
free(p);
p = NULL;
return 0;
}
realloc
void* realloc(void* ptr, size_t size);
参数:
ptr 要调整的内存地址
size 调整以后的大小
返回值 调整之后的内存起始位置
其他 在调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间
realloc 调整内存空间存在的两种情况
1. 原有空间之后有足够大的空间
2. 详见下面第二块代码···
1. 它的出现让动态内存管理更加灵活
2. 又是我们发现过去申请的空间太小了,有时候又会觉得申请的空间过大了,那么为了合理的使用内存,我们
一定会对内存的大小做灵活的调整,那么realloc函数就可以做到对动态开辟的内存大小的调整
3. 它可以实现和malloc同样的功能
#include
#include
#include
#include
#include
int main()
{
int* p = malloc(20);
if (p == NULL)
{
printf("%s\n", strerror(errno));
}
else
{
int i = 0;
for (i = 0; i < 5; i++)
{
*(p + i);
}
}
// 走到这个位置,就是在使用malloc开辟的20个字节的空间
// 假设这里20个字节不能满足使用要求,希望能够有40个字节的空间,这里就可以使用realloc来调整动态内存的空间
return 0;
}
#include
#include
#include
#include
#include
int main()
{
int* p = malloc(20);
if (p == NULL)
{
printf("%s\n", strerror(errno));
}
else
{
int i = 0;
for (i = 0; i < 5; i++)
{
*(p + i) = i;
}
}
// 走到这个位置,就是在使用malloc开辟的20个字节的空间
// 假设这里20个字节不能满足使用要求,希望能够有40个字节的空间,这里就可以使用realloc来调整动态内存的空间
// realloc使用注意事项
// 1. 如果 p 指向的空间之后有足够的空间可以追加,会直接追加,然后返回 p
// 2. 如果 p 指向的空间之后没有足够的空间可以追加,则realloc会重新找一块满足需求的新的空间开辟,并且把原来内存中的
// 数据拷贝过来,并释放原来指向的内存空间,最后返回新指向的空间的地址
// 3. 如果realloc开辟失败,会返回空指针,那么原来的哪个地址就丢掉了,所以最好给一个新指针来接收,或者判断一下是否为空指针
// 然后用老指针 p 去接收
int* p2 = realloc(p, 40);
if (p2 != NULL)
{
p = p2;
int i = 0;
for (i = 5; i < 10; i++)
{
*(p2 + i) = i;
}
for (i = 0; i < 10; i++)
{
printf("%d\n", *(p2 + i));
}
}
// 释放内存
free(p);
p = NULL;
return 0;
}
int main()
{
int* p = malloc(20); // 万一malloc失败了,p就被赋值为NULL,下面这段代码就属于非法操作(对空指针解引用)
int i = 0;
for (i = 0; i < 10; i++)
{
*(p + i) = i;
}
free(p);
p = NULL;
return 0;
}
int main()
{
int* p = malloc(5 * sizeof(int));
if (p == NULL)
{
return 0;
}
else
{
int i = 0;
for (i = 0;i < 10; i++) // 本来只有5个元素,但是这里访问了10个,一般会卡死,但是VS2022没什么反应
{
*(p + i) = i;
}
}
free(p);
p = NULL;
return 0;
}
int main()
{
int a = 10;
int* p = &a;
*p = 20;
free(p);
p = NULL;
return 0;
}
在C99中,结构中的最后一个元素允许是位置大小的数组,这就叫做柔性数组的成员
#include
#include
#include
#include
#include
struct S
{
int n;
int arr[]; // 未知大小的--柔性数组的成员--大小是可调整的
};
int main()
{
struct S s;
printf("%d\n", sizeof(s)); // 因为arr是柔性数组,所以这里不会包含柔性数组大小
struct S* ps = malloc(sizeof(struct S) + 5 * sizeof(int));
ps->n = 100;
int i = 0;
for (i = 0; i < 5; i++)
{
ps->arr[i] = i;
}
struct S* ptr = realloc(ps, 44);
if (ptr != NULL)
{
ps = ptr;
}
for (i = 5;i < 10; i++)
{
ps->arr[i] = i;
}
for (i = 0; i < 10; i++)
{
printf("%d ", ps->arr[i]);
}
free(ps);
ps = NULL;
return 0;
}
#include
#include
#include
#include
#include
struct S
{
int n;
int* arr;
};
int main()
{
struct S* ps = (struct S*)malloc(sizeof(struct S));
ps->arr = malloc(5 * sizeof(int));
int i = 0;
for (i = 0;i < 5; i++)
{
ps->arr[i] = i;
}
for (i = 0;i < 5; i++)
{
printf("%d ", ps->arr[i]);
}
// 调整大小
int* ptr = realloc(ps->arr, 10 * sizeof(int));
if (ptr != NULL)
{
ps->arr = ptr;
}
for (i = 5; i < 10;i++)
{
ps->arr[i] = i;
}
for (i = 0; i < 10;i++)
{
printf("%d ", ps->arr[i]);
}
free(ps->arr);
ps->arr = NULL;
free(ps);
ps = NULL;
return 0;
}
数据文件主要有 文本文件 和 二进制文件。
要求在外存上以ASSCII码的形式储存,则需要在储存前转换,以ASXII字符储存的文件就是文本文件。
数据在内存中以二进制形式存储,如果不加转换就输出到外存,就是二进制文件。
#include
#include
#include
#include
#include
int main()
{
int a = 10000;
FILE* pf = fopen("test.txt", "wb");
fwrite(&a, 4, 1, pf);
fclose(pf);
pf = NULL;
return 0;
}
ANSIC标准采用“缓冲文件系统”处理数据文件的,所谓缓冲文件系统是指系统自动地在内存中为程序每一个正在使用的
文件开辟一块“文件缓冲区”。从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上。如果
磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区,然后再从缓冲区逐个地将数据送到程序数据区。
缓冲区的大小根据C编译系统决定。
缓冲文件系统中,关键的概念是“文件类型指针”,简称“文件指针”。
每个被使用二点文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件名,文件状态,文件
当前位置等)。这些信息是保存在一个结构体变量中的。改结构体类型是有系统声明的,取名FILE。
FILE* pf
文件打开 fopen(const char * filename, const char * mode)
参数:filename---文件名
mode----打开模式
文件使用方式 | 含义 | 如果指定文件不存在 |
---|---|---|
r(只读) | 为了输入数据,打开一个已存在的文本 | 出错 |
w(只写) | 为了输出数据,打开一个文本文件 | 创建一个新文件 |
a(追加) | 向文本文件尾添加数据 | 出错 |
rb(只读) | 为了输入数据,打开一个二进制文件 | 创建一个新文件 |
wb(只写) | 为了输出数据,打开一个二进制文件 | 出错 |
ab(追加) | 向一个二进制文件尾添加数据 | 出错 |
r+(读写) | 为了读和写,打开一个文本文件 | 出错 |
w+(读写) | 为了读和写,创建一个新的文件 | 创建一个新文件 |
a+(读写) | 打开一个文件,在文件尾进行读写 | 创建一个新文件 |
rb+(读写) | 为了读和写打开一个二进制文件 | 出错 |
wb+(读写) | 为了读和写创建一个新的二进制文件 | |
ab+(读写) | 打开一个二进制文件,在文件尾进行读和写 | 创建一个新文件 |
#include
#include
#include
#include
#include
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
printf("%s \n", strerror(errno));
return 0;
}
fclose(pf);
pf = NULL;
return 0;
}
功能 | 函数名 | 适用于 |
---|---|---|
字符输入函数 | fgetc | 所有输入流 |
字符输出函数 | fputc | 所有输出流 |
文本行输入函数 | fgets | 所有输入流 |
文本行输出函数 | fputs | 所有输出流 |
格式化输入函数 | fscanf | 所有输入流 |
格式化输出函数 | fprintf | 所有输入流 |
二进制输入 | fread | 文件 |
二进制输出 | fwrite | 文件 |
写入文件(字符) fputc
#include
#include
#include
#include
#include
int main()
{
FILE* pfWrite = fopen("test.txt", "w");
if (pfWrite == NULL)
{
printf("%s \n", strerror(errno));
return 0;
}
// 写文件
fputc('a', pfWrite);
fputc('b', pfWrite);
fputc('c', pfWrite);
fclose(pfWrite);
pfWrite = NULL;
return 0;
}
读取文件(字符) fgetc
#include
#include
#include
#include
#include
int main()
{
FILE* pfRead = fopen("test.txt", "r");
if (pfRead == NULL)
{
printf("%s \n", strerror(errno));
return 0;
}
// 读文件
printf("%c", fgetc(pfRead));
printf("%c", fgetc(pfRead));
printf("%c", fgetc(pfRead));
fclose(pfRead);
pfRead = NULL;
return 0;
}
读取键盘输入,输出到屏幕 stdin stdout
#include
#include
#include
#include
#include
// 从键盘输入
// 输出到屏幕
// 键盘 - 标准输入设备 - stdin
// 屏幕 - 标准输出设备 - stdout
// 是一个程序默认打开的两个流设备
// stdin FILE*
// stdout FILE*
// stderr FILE*
int main()
{
int ch = fgetc(stdin); // 读取键盘输入
fputc(ch, stdout); // 输出到屏幕上
return 0;
}
读取文件(行) fgets
#include
#include
#include
#include
#include
int main()
{
char buf[1024] = { 0 }; // 储存读取的文件信息
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
printf("%s", strerror(errno));
return 0;
}
// 读文件---读取一行
fgets(buf, 1024, pf);
printf("%s", buf);
fgets(buf, 1024, pf);
printf("%s", buf);
// 关闭文件
fclose(pf);
pf = NULL;
return 0;
}
写入文件(行) fputs
#include
#include
#include
#include
#include
int main()
{
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
printf("%s", strerror(errno));
return 0;
}
// 写文件---写一行
fputs("hello\n", pf);
fputs("world", pf);
// 关闭文件
fclose(pf);
pf = NULL;
return 0;
}
标准输入,标准输出 fgets fputs gets puts
#include
#include
#include
#include
#include
int main()
{
// 从键盘读取一行
char buf[1024] = { 0 };
fgets(buf, 1024, stdin); // 从标准输入读取
fputs(buf, stdout); // 输出到标准输出流
// 和上面功能等价
gets(buf);
puts(buf);
return 0;
}
格式化的形式写文件 fprintf
#include
#include
#include
#include
#include
struct S
{
int n;
float score;
char aarr[10];
};
int main()
{
struct S s = { 100, 3.14f, "bit" };
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
return 0;
}
// 格式化的形式写入文件
fprintf(pf, "%d %f %s", s.n, s.score, s.aarr);
fclose(pf);
pf = NULL;
return 0;
}
格式化的形式读文件 fscanf
#include
#include
#include
#include
#include
struct S
{
int n;
float score;
char aarr[10];
};
int main()
{
struct S s = {0};
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
return 0;
}
// 格式化的形式读取文件(输入数据)
fscanf(pf, "%d %f %s", &(s.n), &(s.score), s.aarr);
printf("%d %f %s", s.n, s.score, s.aarr);
fscanf(stdin, "%d %f %s", &(s.n), &(s.score), s.aarr); // 读取标准输入
fprintf(stdout, "%d %f %s", s.n, s.score, s.aarr); // 打印标准输入
fclose(pf);
pf = NULL;
return 0;
}
实例
对比:scanf / fscanf / sscanf
printf / fprintf / sprintf
解答:
1. scanf 与 printf 是针对标准输入流/标准输出流的格式化输入/输出语句。
2. scanf 与 fprintf 是针对所有输入流/所有输出流的格式化输入/输出语句。
3. sscanf 是从字符串中读取格式化的数据,sprintf 是把格式化数据输出成(存储到)字符串。
程序的翻译环境、执行环境、运行环境
程序在翻译环境中,被转换额为可执行的机器指令,然后再执行环境中,实际执行代码。
编译和链接依赖的就是翻译环境。
在编译过程中,每个源文件中的 .c 文件都会单独经过翻译环境中的编译器进行单独处理,然后形成目标文件也就是
.obj 类型的文件,然后通过链接库链接起来,最后形成了可执行程序。
1. 组成一个程序的每个源文件通过编译过程分别转换成目标代码(object code)
2. 每个目标文件由连接器(linker)捆绑在一起,形成一个单一而完整的可执行程序
3. 链接器通过是也会引入标准C函数库中任何被该程序所用到的函数,而且它可以搜索程序员个人的程序库,将
其需要的函数也链接到程序中
编译本身也分为好几个阶段
1. 预处理/预编译阶段主要做文本操作
· 把头文件包含放在 .c 文件里
· 删除注释
· 把 #define 定义的常量全部替换成值
· 还有其他的,这里省略~~~
2. 编译阶段的操作(对test.i进行操作,上面图中写错了)
· 将 test.i 翻译成 test.s,实际上是把C代码翻译成汇编代码
· 主要过程是语法分析、词法分析、语义分析、符号汇总
3. 汇编阶段
· 将 test.c 汇编完成后,会生成 test.o 文件,也就是我们看到的 .obj 类型的文件
· 把汇编代码转换成立二进制指令,汇编阶段会 形成符号表,表中的符号和地址对应起来
链接器中主要做了如下事情
1. 合并段表: 每个 .o 文件都有 elf文件格式,然后把每个文件的特定段进行合并(exe文件也是elf格式的)
2. 符号表的合并和符号表的重定位: 把符号表进行合并,并把一些地址也进行合并
运行环境
程序的执行过程:
1. 程序必须载入内存中,这个一般由操作系统完成,在独立的环境中,程序的载入必须由手工安排,也可能通过
可执行代码置入只读内存来完成
2. 程序的执行便开始,紧接着调用 main 函数
3. 开始执行代码,这个时候程序将使用一个运行时堆栈(stack),储存函数的局部变量和返回地址,程序同时
也可以使用静态(static)内存,储存于静态内存中的变量在程序的整个执行过程已知保留他们的值
4. 中止程序,正常中止main函数,也有可能是意外中止
预定义符号 | 解释 |
---|---|
_ _FILE _ _ | 进行编译的源文件 |
_ _LINE _ _ | 文件当前行号 |
_ _DATE _ _ | 文件被编译的日期 |
_ _TIME _ _ | 文件被编译的时间 |
_ _STDC _ _ | 如果编译器遵循 ANSI C标准,其值为1,否则未定义 |
#include
#include
#include
#include
#include
int main()
{
// 写日志文件
int i = 0;
int arr[10] = { 0 };
FILE* pf = fopen("log.txt", "w");
for (i = 0; i < 10; i++)
{
arr[i] = i;
fprintf(pf, "file: %s line :%d date:%s time:%s i=%d\n", __FILE__, __LINE__, __DATE__, __TIME__, i);
printf("%s\n", __FUNCTION__); // 打印执行的函数
}
fclose(pf);
pf = NULL;
return 0;
}
预处理指令: #define、#include、#pragma pack(4)、#pragma、#if、#endif、#de、#ifdef
#define 定义标识符
语法: #define name stuff
注意: 后面千万不要加分号!!!!
#define MAX 100
#define STR "haha" // 预定义字符串
#define reg register // 为register关键字创建一个简短的名字
#define do_forever for(;;) // 用更形象的符号来替换一种实现
#define CASE break;case // 再写case语句的时候自动把break写上
// 如果定义二点stuff过长,可以分成几行写,每行后面都要加一个反斜杠(续行符)
#define DEBUG_PRINT printf("file:%s line:%d \
data:%s time:%s,\
__FILE__, __LINE__,\
__DATE__, __TIME__")
int main()
{
do_forever; // 这个就会死循环,和执行了 for(;;) 一样
return 0;
}
宏和函数的对比
#define 定义宏
#define name(parament-list) stuf 其中的 parament-list 是一个由逗号隔开的符号表,他们可能
出现在 stuff 中
注意: 参数的左括号必须与 name 紧邻,如果两者之间有任何空白存在,参数列表就会被解释为stuff一部分
#define SQUARE(x) x * x
这个宏接收一个参数x ,具体看下面示例:
#define 定义规则:
1. 调用宏时,首先检查参数,看看是否包含任何由 #define 定义的符号,如果是,他们首先被替换
2. 替换文本随后被插入到程序中原来文本的位置,对于宏、参数名被他们的值替换
3. 最后,再次扫描文件,看看它是否包含任何由 #define 定义的符号,如果有,重复上述过程
注意:
1. 宏参数和 #define 定义总可以出现其他 #define 定义的变量,但是宏不能出现递归
2. 当预处理器搜索 #define 定义的符号是,字符串常量的内容并不被搜索
#include
#define SQUARE(X) X*X // 这个就是宏
#define DOUBLE(X) X+X
int main()
{
int ret = SQUARE(5); // 这句话会被替换成 int ret = 5*5
printf("%d ", ret); // 结果: 25
int a = 5;
int ret2 = 10 * DOUBLE(a);
printf("%d ", ret2); // 结果: 55,如果想让结果变成100,在定义宏的时候加括号
return 0;
}
预处理操作符 # 和 ## 的介绍
这个不是 define 前面的那个 #,这里说的 # 是单独存在的
使用 # 把宏参数变成对应的字符串
#include
#include
#include
#include
#include
void print(int a)
{
printf("the value of a is %d\n", a);
}
int main()
{
int a = 10;
int b = 20;
// printf("the value of a is %d\n", a);
print(a);
print(b);
return 0;
}
// 与上一段代码是有关联的,这个是通过宏来实现,这一段代码做到了把宏的参数,插入到字符串了
#include
#include
#include
#include
#include
#define PRINT(X) printf("the value of "#X" is %d", X) // 这里的 "#X" 会被替换成 字符串
int main()
{
int a = 10;
int b = 20;
PRINT(a);
// printf("the value of ""a"" is %d\n", a)
PRINT(b);
// printf("the value of ""b"" is %d\n", b)
return 0;
}
## 可以把位于它两边的符号合成一个符号,它允许宏定义从分离的文本片段创建标识符
#define CAT(X, Y) X##Y
int main()
{
int lass84 = 2019;
printf("%d \n", lass84); // 结果: 2019
printf("%d \n", CAT(lass, 84)); // 结果: 2019,相当于把 lass 和 84 合成 lass84
return 0;
}
带有副作用的宏参数
当宏参数再宏的定义中出现超过一次的时候,如果参数带有副作用,那么使用宏的时候就会出现危险,导致结果
不可预测,副作用就是表达式求值时候出现的永久性效果,例如:
x+1: // 不带副作用
x++; //带有副作用
#define MAX(X, Y) (X)>(Y)?(X):(Y)
int main()
{
int a = 10;
int b = 20;
int max = MAX(a++, b++); // 这个就会产生副作用
printf("%d\n", max); // 21
printf("%d\n", a); // 11
printf("%d\n", b); // 22
return 0;
}
宏和函数特别相像,但是宏一般用于简单计算,但是针对于上面的例子来说,可以写函数,也可以写宏,但是用宏更加
灵活一些,因为上面例子是整型计算,如果是浮点型,函数需要重写,宏不需要。并且,宏在汇编当中执行时,只需要
一行,但是用函数的话,需要准备参数,然后计算,再返回参数,所以宏没有调用和返回的开销。
优势:
1. 宏比函数从程序的规模和速度方面更胜一筹。
2. 宏时类型无关的(例如int比大小,float比大小)。
3. 红的参数可以出现类型,函数做不到(见下例)。
劣势:
1. 每次使用宏时,一份宏定义代码将插入至程序,除非宏比较短,否则会大幅度增加程序长度。
2. 宏时没法调试的。
3. 宏由于类型无关,也就不够严谨。
4. 宏可能会带来运算符优先级的问题,导致程序容易出错。
#define MALLOC(num, type) (type*)malloc(num*sizeof(type))
int main()
{
int* p = (int*)malloc(10 * sizeof(int));
int* p = MALLOC(10, int);
// int* p = (int*)malloc(10 * sizeof(int)); 这个代码跟宏里面表现的一模一样
return 0;
}
属性 | #define 定义宏 | 函数 |
---|---|---|
代码长度 | 每次使用宏时,一份宏定义代码将插入至程序,除非宏比较短,否则会大幅度增加程序长度 | 函数代码只出现于一个地方;每次使用这个函数时,都调用同一份代码 |
执行速度 | 更快 | 存在函数的调用和返回的额外开销,所以相对慢一点 |
操作符优先级 | 宏参数的求职实在所有周围表达式的上下文环境里,除非加上括号,否则邻近操作符的优先级可能会产生不可预料的后果,所以建议宏再书写的时候多谢括号 | 函数参数只在函数调用的时候求值一次,它的结果传递给函数,表达式求值的结果更容易预测 |
带有副作用的参数 | 参数可能被替换到宏体当中的多个位置,所以带有副作用二点参数求值可能会产生不可预料的结果 | 函数参数只在传参的时候求值一次,结果更容易控制 |
参数类型 | 宏的参数于类型无关,只要对参数的操作是合法的,它就可以使用于任何参数类型 | 函数的参数于类型有关,入股哦参数的类型不同,就需要不同的函数,及时他们执行的任务是不同的 |
调试 | 宏是不方便调试的 | 函数是可以逐语句调试的 |
递归 | 宏是不能递归的 | 函数是可以递归的 |
命名预定:
宏:把宏名全部大写
函数:函数名不要全部大写
命令行定义
许多的C编译器提供了一种能力,允许再命令行中定义符号,用于启动编译过程。例如:当我们根据同一个源文件
要编译处不同的一个程序的不同版本的时候,这个特性有点用处。(假定某个程序中声明了一个某个长度的数组
,如果机器内存有限,我们需要一个很小的数组,但是另外一个机器内存大写,我们需要一个数组能够大写)
例如:写了一段代码,程序中没有定义变量SZ,那么再编译的时候可以定义
gcc test.c -D SZ=100
预处理指令 #include
如果是库文件
#include
如果是自己定义的头文件
#include "add.h"
查找策略:现在源文件所在的目录瞎查找,如果该头文件未找到,编译器就像查找库函数头文件一样
在标准位置查找头文件,如果找不到就提示编译错误。
在Linux瞎标准头文件路径: /user/include
在引入自己写的头文件时,有可能会出现一个文件引入多次,这样的话就有可能会报错所以解决办法
来避免头文件重复引用,如下:
// 方法一
#ifndef __TEST_H_
#define __TEST_H_
int Add(int x, int y);
#endif
// 方法二
#pragma once
int Add(int x, int y);
预处理指令 #undef
这条指令用于移除一条宏定义,如果一个名字需要重新定义,那就需要先移除
条件编译
在编译一个程序时,如果要将一条语句(一组语句)编译或者放弃是很方便的,因为有条件编译指令
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,0 };
int i = 0;
for (i = 0; i < 10; i++)
{
arr[i] = 0;
// 方式一
#ifdef DEBUG // 这句话的意思是,如果DEBUG被定义过,就编译 printf ,如果没有定义过,就不参与编译
printf("%d ", arr[i]);
#endif
// 方式二
#if 1 // 这句话的意思是,如果条件为真,就编译 printf ,如果为假,就不参与编译
printf("%d ", arr[i]);
#endif
// 方式三
# if
# elseif
# else
// 方式四:判断是否被定义
#if defined(symbol)
#ifdef symbol
#if !defined(symbol)
#ifndef symbol
// 方式无:嵌套指令
#if defined(OS_UNIX)
#ifdef OPTION1
语句
#endif
#elif 后面省略
}
return 0;
}