前言
就准备开始 PE 了,先说说宏定义和堆,这俩是肯定要在学习的过程中遇到的,有了这俩基础后再学习一下下篇文章的 C 语言读写文件,那就可以自己写个小工具了
课堂
C 语言的执行步骤
这个看过一点汇编的就知道,C 语言这个执行的步骤大致分为 C –> 替换 –> 编译 –> 链接 –> 装入内存 –> 执行,而为啥要说这个呢,因为今天的宏定义所在的过程是替换这个阶段,也就是说在编译之前,就替换完了,要替换的东西不在常量区,它就在代码区,所以大家一定不要搞混了
无参数的宏定义
这个直接举个例子哇,就是记住就行,唯一需要理解的可能就是上面说的那句话,替换在编译之前就已经完成了
#define DEBUG 1
void Function(){
//....
if(DEBUG)
printf("测试信息");
}
我们平时写代码时,如果代码数量很庞大,那么肯定少不了在中间要输出测试一下结果是否正确,但是如果我写了成千个测试 printf,那么最后发布程序时,不可能一个一个修改,那么此时只要在每一条测试 printf 语句前加 if,然后使用宏定义把 DEBUG 的值赋值为 1 供测试执行,发布时再将 DEBUG 值改为 0 即可
-
只作字符序列的替换工作,不作任何语法的检查
-
如果宏定义不当,错误要到预处理(替换)之后的编译阶段才能发现
带参数的宏定义
这个就有点像函数,你看下面两张图实现的功能其实都是一样的
#define MAX(A,B) ((A) > (B)?(A):(B))
void Func(){
p = 1;
q = 2;
int x = MAX(p,q);
}
有点像定义函数一样,但是只是将标识符(参数表)用字符序列替换,那为啥要这么做呢?因为这么做确实一定意义上是可以节省空间的,因为函数一旦定义无论如何都要去开辟一块内存空间,但是这个只是单纯意义上的替换,不用额外给 MAX 开辟一块内存,而是在替换时即编译器,直接用宏定义时后面的字符序列来替换 MAX 部分,而且参数也可以很好的传递;不像函数,需要单独开辟一块内存,来存储函数信息
宏的注意事项
头文件
说这个的目的其实就是让写代码的时候更方便一点,然后引出一个重复包含的问题,看看怎么解决它
头文件的使用:
步骤一:
void Function()
{
printf("Hello World!");
}
int main(int argc, char* argv[])
{
Function();
return 0;
}
可以执行
如果换成:
int main(int argc, char* argv[])
{
Function();
return 0;
}
void Function()
{
printf("Hello World!");
}
不能执行!
解决办法:新增头文件(.h),在.h文件中对函数进行说明.
如:
.c文件: .h文件
void Function() void Function();
{
printf("Hello World!");
}
那咋去同时新建一个代码文件和一个头文件呢
OK 然后就在头文件说明就行
重复包含问题
解决方案(在 z.h 中加上下面两句):
#if !defined(ZZZ) //宏定义
#define ZZZ
struct Student{
int level;
};
#endif
比如现在我在 x.h 中包含了以后,在 y.h 文件中包含时就可以加上一个判断宏定义
这句话的意思可以这样去理解,如果 ZZZ 已经存在了,就不再声明,ZZZ 相当于一个编号,唯一的,越复杂越好,(但是一般不用我们来写)。而且没有这么简单,重复包含问题还要其他解决方案,后面遇到会说明。就是一开始没有 ZZZ,但如果不存在 ZZZ 就会创建一个 ZZZ,听的听绕的,但就是这样,逻辑上说的通
我们看看编译器是怎么帮我们解决重复包含问题的:(下面是 stdafx.h 头文件中的内容)
第一、二行就是使用我们说的:解决方案定义了一个编号,如果定义了就不定义了
#if _MSC_VER > 1000 #program once:表示如果编译器的版本>1000,那么#program once就有意义,而#program once的作用和最开始的!define作用是一样的,可以没有#if _MSC_VER > 1000 #program once,但是不能没有一二两行,因为#program的对编译器的兼容性不好,可能会失败
动态分配内存
OKK 终于到这个堆了,一种新的存储数据的地方,我们前面学过静态申请内存,比如 int 一个 a,或者是 char b[100](数组的时候就说过里面这个值必须是确定的),但是现在如果我要存的数我不确定个数,就不能使用静态申请内存分配固定大小的内存,而是要使用动态申请内存
malloc 函数的使用
malloc 函数的作用:C 库函数,分配所需的内存空间,并返回一个指向它的指针,如果内存空间不够,则返回 NULL
声明
#include "stdlib .h"
void* malloc(size_t size)
size_t是一个宏定义类型,就表示一个无符号整数类型,是sizeof关键字的结果。字节为单位
void*就表示任何类型的指针,因为使用malloc动态申请内存,最后要返回一个指向整块内存的指针,但是不知道具体是一个什么类型的指针,那么返回值类型使用void*,就表示是任何类型的指针,因此宽度就不确定了,所以void*类型指针无法做++,--等运算。所以void*相当于一个临时的类型,占4字节位置,遵循语法规则,到底是什么类型的指针用,就要在使用的时候强转成对应的类型指针即可
这个参数就是个 unsigned int
使用 malloc 动态申请内存
这个是海东老师自己写的,是他的个人习惯,很完善很安全
//在堆中申请内存,分配128个int
int* ptr = (int *)malloc(sizeof(int)*128); //假设这块内存要给一个int型数组使用,将void*强转int*
//无论申请的空间大小,一定要进行校验,判断是否申请成功
if(ptr == NULL){
return 0;
}
//初始化分配的内存空间,将分配的这片内存中全设为0(可以不用加,这里是害怕这块内存中有别人留下的数据)
memset(ptr,0,sizeof(int)*128);
//使用内存
*(ptr) = 1; //使用指针来操作指向的内存中的数据
//使用完毕,释放申请的堆空间
free(ptr);
//将指针设置为NULL。因为这次我使用了ptr指针,我用完之后ptr应该还是指向了最后的内存中的地址,如果有坏蛋尝试使用了ptr指针,即用完后又使用了ptr指针,那很可能把原先指向的内存中的其他数据给读出来了,不安全。如果设置了NULL,后面不小心使用ptr,会报错
ptr = NULL;
提出内存泄露的问题:
我们平时如果在函数外定义一个变量,分配的内存在全局区;在函数内定义一个变量,分配的内存在堆栈;使用完这个变量,也不用我们手动的去释放分配的内存空间,因为堆栈平衡等原因,使用完后这些内存中的数据就变成了垃圾,下一次再使用赋初始值覆盖这块内存中的数据即可
但是现在如果我们使用 malloc 函数动态申请内存,分配的内存空间在堆中,堆有一个特点,如果此时一个数据占用了堆中的某块内存,那么操作系统就会记住这块内存已经分配出去了,其他数据就不能占用了,要么等待释放、要么此 exe 程序退出后,其他的数据才能再使用这块内存
但是像服务器上运行的程序,会长时间运行,使用 malloc 函数申请内存,如果使用完没有释放,就会造成这块内存一直被占用,当数据庞大时,会将堆全部占住,最后内存占用率会很高,程序就会奔溃,这就是内存泄露问题(堆)。所以一定要释放内存!
那 malloc 最多能申请多大的内存呢?学过操作系统知道,如果一个 32 位计算机,任何一个.exe 程序运行时都会分配 4GB 的虚拟内存,2GB 是系统区,2GB 是用户区,系统区我们不能轻易使用(后面学中级课程的时候,就会学操作任意内存地址)
作业
emmm 这节课的作业还是比较麻烦的,下篇文章说一下 C 语言怎么操作文件,说完之后再看这个作业就会轻松好多,所以作业的解答就放到下篇文章中啦