动态内存分配指根据需要向系统申请所需大小的空间,由于未在声明部分定义其为变量或者数组,不能通过变量名或者数组名来引用这些数据,只能通过指针来引用)
一:void 指针(无类型指针)
C99允许定义一个类型为void的指针变量,它可以指向任何类型的数据。
(1)void 指针作用
指针变量必须有类型,否则编译器无法知道如何解读内存块保存的二进制数据。但是,有时候向系统请求内存的时候,还不确定会有什么类型的数据写入内存,需要要先获得内存块,稍后再确定写入的数据类型。
这种情况下就可以使用 void 指针,它只有内存块的地址信息,没有类型信息,等到使用该块内存的时候,再向编译器补充说明,里面的数据类型是什么。
(2)void 指针特点
- void 指针与其他所有类型指针之间是互相转换关系,任一类型的指针都可以转为 void 指针,而 void 指针也可以转为任一类型的指针。
- 由于不知道 void 指针指向什么类型的值,所以不能用 * 运算符取出它指向的值(解引用)。
#include <stdio.h>
int main()
{
int a = 110;
double b = 3.14;
// void *类型指针,可以与其他类型的指针相互转换
// 将右侧int *类型地址,隐式转换为void *
void *ptr = &a;
// 将右侧double *类型的地址,隐式转换为void*
void *ptr1 = &b;
// 无类型指针不能取值
// printf("%d\n", *ptr);
// void * 类型指针 强转其他类型
int *p = (int *)ptr;
double *p1 = (double *)ptr1;
printf("%d %lf\n", *p, *p1);
return 0;
}
- 其他类型指针赋给 void指针,使用隐式转换即可,因为 void 指针不包含指向的数据类型的信息,通常是安全的。
- void 指针赋给其他类型指针,建议使用显式类型转换,这样更加安全,如果使用隐式类型转换,有些编译器会触发警告。
二:内存分配相关函数
头文件 <stdlib.h> 声明了四个关于内存动态分配的函数:
(1)malloc() 函数
malloc() 函数用于分配一块连续的内存空间。
函数原型:
void *malloc(size_t size);
返回值说明:
如果内存分配成功,返回一个void指针,指向新分配内存块的地址;如果分配失败(例如内存不足),返回一个空指针(NULL)。
参数说明:
size是要分配的内存块的大小,以字节为单位。
#include <stdio.h>
// 动态分类内存函数malloc,位于stdlib这个头文件当中
#include <stdlib.h>
int main()
{
// 在堆空间开了一个四字节内存大小
int *ptr = (int *)malloc(sizeof(int) * 1000);
// 每次用之前先判断申请内存是否充足
if (ptr == NULL)
{
printf("内存不足!!!!");
// 程序结束
return 1;
}
// 向申请哪个区域存储一个整数100
*ptr = 100;
printf("%d\n", *ptr);
// 每一次操作完以后,释放内存
free(ptr);
// //释放以后不要在操作【访问|赋值这块内存啦】!!!
printf("%d\n", *ptr);
return 0;
}
#include <stdio.h>
#include <stdlib.h>
typedef int Integer;
int main()
{
int n = 5;
// malloc参数:开辟空间大小->字节
int *array = (int *)malloc(n * sizeof(Integer));
// 判断申请内存是否成功
if (array == NULL)
{
printf("申请失败");
return 1;
}
// 申请内存成功
for (int i = 0; i < n; i++)
{
// *(array + i) = i * 10;
// 底下这种写法:与上面写法相同的
array[i] = i * 10;
}
// 查看存储是否成功
for (int j = 0; j < n; j++)
{
printf("%d\n", array[j]);
}
// 立马销毁
free(array);
return 0;
}
(2)calloc() 函数
calloc() 函数用于分配内存并将其初始化为零,它在分配内存块时会自动将内存中的每个字节都设置为零。
函数原型:
void *calloc(size_t numElements, size_t sizeOfElement);
返回值说明:
如果内存分配成功,返回一个 void 指针,指向新分配内存块的地址;如果分配失败(例如内存不足),返回一个空指针(NULL)。
参数说明:
numElements是要分配的元素的数量。
sizeOfElement是每个元素的大小(以字节为单位)。
#include <stdio.h>
#include <stdlib.h>
int main()
{
int n = 5;
// 两个参数:元素个数 每个元素占用字节
int *array = (int *)calloc(n, sizeof(int));
if (array == NULL)
{
printf("申请失败");
return 1;
}
// 申请这块'地',地址信息
printf("%p\n", array);
for (int i = 0; i < n; i++)
{
array[i] = i * 20;
}
// 访问
for (int i = 0; i < n; i++)
{
printf("%d\n", array[i]);
}
free(array);
return 0;
}
(3)realloc() 函数
realloc() 函数用于重新分配malloc() 或calloc() 函数所获得的内存块的大小。
函数原型:
void* realloc(void *ptr, size_t size);
返回值说明:
返回一个指向重新分配内存块的指针。如果内存重新分配成功,返回的指针可能与原始指针相同,也可能不同;如果内存分配失败,返回返回一个空指针(NULL)。
如果在原内存块上进行缩减,通常返回的原先的地址。
参数说明:
- ptr是要重新分配的内存块的指针。
- size是新的内存块的大小(以字节为单位)。
#include <stdio.h>
#include <stdlib.h>
#include <malloc.h>
int main()
{
// 申请一点空间
int *ptr = (int *)malloc(400);
// 判断一下
if (ptr == NULL)
{
printf("申请失败");
return 1;
}
// 成功开了400字节内存
printf("%d\n", _msize(ptr));
// 开辟空间不够--->800
ptr = realloc(ptr, 8000);
printf("%d\n", _msize(ptr));
free(ptr);
return 0;
}
(4)free() 函数
如果动态分配的内存空间没有被正确释放,这种情况称为内存泄漏,内存泄漏会导致系统中的可用内存逐渐减少,直到耗尽系统可用的内存资源。
free() 函数用于释放动态分配的内存,以便将内存返回给操作系统,防止内存泄漏。
函数原型:
void free(void *ptr);
返回值说明:
没有有返回值。
参数说明:
ptr是指向要释放的内存块的指针,ptr必须是malloc() 或calloc() 动态分配的内存块地址。
注意:
- 分配的内存块一旦释放,就不应该再次操作已经释放的地址,也不应该再次使用 free() 对该地址释放第二次。
- 如果忘记调用free()函数,会导致无法访问未回收的内存块,构成内存泄漏。
(5)内存分配应用实例
动态创建数组,输入5个学生的成绩,再定义一个函数检测成绩低于60分的,输出不合格的成绩。
#include <stdlib.h>
#include <stdio.h>
// 函数原型声明
void check(int *);
int main()
{
int *p;
// 在堆区开辟一个 5 * 4 的空间
p = (int *)malloc(5 * sizeof(int));
printf("请输入5个成绩:");
for (int i = 0; i < 5; i++)
{
scanf("%d", p + i);
}
check(p);
free(p); // 销毁 堆区 p 指向的空间
return 0;
}
// 函数定义
void check(int *p)
{
printf("\n不及格的成绩有: ");
for (int i = 0; i < 5; i++)
{
if (p[i] < 60)
{
printf(" %d ", p[i]);
}
}
}
内存分配的基本原则:
- 避免分配大量的小内存块。分配堆上的内存有一些系统开销,所以分配许多小的内存块比分配几个大内存块的系统开销大。
- 仅在需要时分配内存。只要使用完堆上的内存块,就需要及时释放它,否则可能出现内存泄漏。
- 总是确保释放已分配的内存。在编写分配内存的代码时,就要确定好在代码的什么地方释放内存。
三:预处理器
(1)基本介绍
预处理过程中会执行预处理指令,预处理指令以 # 号开头,用于指导预处理器执行不同的任务。
预处理指令具有如下特点:
1.预处理指令应该放在代码的开头部分。
2.预处理指令都以 # 开头,指令前面可以有空白字符(比如空格或制表符),# 和指令的其余部分之间也可以有空格,但是为了兼容老的编译器,一般不留空格。
// 推荐写法
#include <stdio.h>
// 不推荐写法
#include <stdio.h>
# include <stdio.h>
3.预处理指令都是一行的,除非在行尾使用反斜杠,将其折行。
#include <std\
io.h>
4.预处理指令不需要分号作为结束符,指令结束是通过换行符来识别的。
#include <stdio.h>; // 这里有分号会报错
#define PI 3.14; // 分号会成为 PI 的值的一部分
5.预处理指令通常不能写在函数内部,有些编译器的扩展允许将预处理指令写在函数里,但强烈不建议这么干。
int main ()
{
// 一般不允许写在这里
#include <stdio.h>
return 0;
}
(2)宏定义
宏定义,就是用一个标识符(宏名称)来表示一个替换文本,如果在后面的代码中出现了宏名称,预处理器会将它替换为对应的文本,称为宏替换或宏展开。
宏定义的基本语法形式如下:
#define 宏名称 替换文本
宏名称:宏的名称,是一个标识符,通常使用大写字母表示,以便与变量名区分开来。
替换文本:宏名称在代码中的每次出现都会被替换为这段文本。
前面的案例中,我们曾经使用宏定义来定义常量和布尔类型。
使用宏定义常量:
#include <stdio.h>
// 定义常量
#define PI 3.14
int main()
{
// 定义变量保存半径,值通过用户输入获取
double radius;
printf("请输入半径:");
scanf("%lf", &radius);
// 计算面积并输出
printf("圆的面积:%.2f", radius * PI * PI);
return 0;
}
使用宏定义数据类型:
#include <stdio.h>
// 宏定义
#define BOOL int
#define TRUE 1
#define FALSE 0
int main()
{
// 使用整型表示真假两种状态
// int isPass = 0;
// int isOk = 1;
// 借助于宏定义
BOOL isPass = FALSE;
BOOL isOk = TRUE;
if (isPass)
{
printf("Pass");
}
if (isOk)
{
printf("Ok");
}
return 0;
}
上面代码中使用宏定义声明了BOOL、TURE、FALSE,后面代码中出现BOOL会替换成int,出现TRUE会替换成1,出现FALSE替换成0。
宏定义的替换文本:
替换文本可以含任何字符,它可以是字面量、表达式、if 语句、函数等,预处理程序对它不作任何检查,直接进行文本替换,如有错误,只能在编译已被宏展开后的源程序时发现。
#include <stdio.h>
#define M (n * n + 3 * n)
#define PRINT_SUM printf("sum=%d", sum)
int main()
{
int n = 3;
int sum = 3 * M + 4 * M + 5 * M; // 宏展开 3*(n*n+3*n)+4*(n*n+3*n)+5*(n*n+3*n);
PRINT_SUM; // 宏展开 printf("sum=%d", sum)
return 0;
}
宏定义允许嵌套,在宏定义的替换文本中可以使用已经定义的宏名,在宏展开时由预处理程序层层替换。
#include <stdio.h>
#define PI 3.1415926
#define S PI *y *y
int main()
{
int y = 2;
printf("%f", S); // 宏替换变为 printf("%f", 3.1415926*y*y);
return 0;
}
取消宏定义:
如要取消宏定义使用#undef命令。
#include <stdio.h>
#define PI 3.14159
int main()
{
printf("PI=%f", PI);
return 0;
}
#undef PI // 取消宏定义
void func()
{
// printf("PI=%f", PI); //错误,这里不能使用到PI了
}
(3)带参数的宏定义
- C语言允许宏带有参数。在宏定义中的参数称为“形式参数”,在宏调用中的参数称为“实际参数”,这点和函数有些类似。
- 对带参数的宏,在展开过程中不仅要进行文本替换,还要用实参去替换形参。
- 带参宏定义的一般形式为#define宏名(形参列表)替换文本,在替换文本中可以含有各个形参。
- 带参宏调用的一般形式为:宏名(实参列表)。
#include <stdio.h>
// 定义一个宏,需要有'形参列表'
// 预处理指令#代码,末尾不能添加分号
// 注意事项:1:宏定义的时候形参之间可以有空格
// 2:宏名字与形参列表之间不能出现空格
#define MAX(a, b) a > b ? a : b
#define SQUARE(Y) (Y) * (Y)
int main()
{
printf("%d\n", MAX(10, 20));
printf("%d\n", SQUARE(66));
return 0;
}
注意:
- 带参宏定义中,形参之间可以出现空格,但是宏名和形参列表之间不能有空格出现。
- 在带参宏定义中,不会为形式参数分配内存,因此不必指明数据类型,而在宏调用中,实参包含了具体的数据,要用它们去替换形参,因此实参必须要指明数据类型。
- 在宏定义中,替换文本内的形参通常要用括号括起来以避免出错。
带参宏定义和函数的区别
- 宏展开仅仅是文本的替换,不会对表达式进行计算;宏在编译之前就被处理掉了,它没有机会参与编译,也不会占用内存。
- 函数是一段可以重复使用的代码,会被编译,会给它分配内存,每次调用函数, 就是执行这块内存中的代码。
四:文件包含
#include 指令用于引入标准库头文件、自定义头文件或其他外部源代码文件,以便在当前源文件中使用其中定义的函数、变量、宏等内容。
一个源文件可以导入多个头文件,一个头文件也可以被多个源文件导入。
标准库头文件、自定义头文件的扩展名都是 .h。
(1)包含标准库头文件
标准库头文件是系统提供的头文件,直接引入即可,像我们前面用过的 stdio.h、stdbool.h、string.h、time.h 等。引入标准库头文件需要使用尖括号将文件名包裹起来,格式如下:
#include <头文件名.h>
(2)包含自定义头文件
自定义头文件的文件名写在双引号里面, 格式如下:
#include "文件名.h"
建议把所有的常量、宏、系统全局变量和函数原型写在自定义的头文件中,在需要的时候随时引用这些头文件
(3)使用相对路径
如果自定义的头文件在源文件的同级目录或源文件所在目录的下级目录,使用 ./ 开头的路径,./ 可以省略。
如果自定义的头文件在源文件所在目录的上级或者更上级,使用 ../ 开头的路径。
参照以下目录结构。
├─ myheader05.h
└─ project
├─ inc
│ └─ myheader04.h
├─ myheader03.h
└─ src
├─ includes
│ └─ myheader02.h
├─ main.c
└─ myheader01.h
如果要在源文件 main.c 中引入以上自定义的头文件,需按照如下写法:
#include "myheader01.h" // 等价于 #include "./myheader01.h"
#include "includes/myheader02.h" // 等价于 #include "./myheader01.h"
#include "../myheader03.h"
#include "../inc/myheader04.h"
#include "../../myheader05.h"
需要注意的是,建议将头文件放置在源文件所在目录或子目录中,以保持项目的组织结构清晰。
(4)使用绝对路径
绝对路径是文件在文件系统中的完整路径,它从文件系统的盘符(Windows系统)或根目录(Linux系统、MacOS系统)开始,沿着文件系统的目录结构一直到达目标文件。
Windows 系统中使用绝对路径引入自定义头文件,示例如下:
#include "C:\Preparation\Embedded\01CLang\code\project\foo.h"
Linux系统或MacOS系统中使用绝对路径引入自定义头文件,示例如下:
#include "/usr/local/lib/foo.h"
需要注意的是,通常我们建议使用相对路径引入自定义头文件,因为相对路径更加灵活和可移植。
五:条件编译
(1)#if
#if...#endif指令用于预处理器的条件判断,满足条件时,内部的行会被编译,否则就被编译器忽略。
#include <stdio.h>
#if 1
int a = 100;
double kill = 3.14;
char ch = 'z';
#endif
#if 1 - 1
我爱你我的祖国!
哈哈哈哈哈哈
6666 int c = 100;
#endif
int main()
{
printf("%lf", kill);
return 0;
}
#if 0
const double pi = 3.1415; // 不会执行
#endif
上面示例中,#if后面的0,表示判断条件不成立。所以,内部的变量定义语句会被编译器忽略。#if 0这种写法常用来当作注释使用,不需要的代码就放在#if 0里面。
#if后面的判断条件,通常是一个表达式。如果表达式的值不等于0,就表示判断条件为真,编译内部的语句;如果表达式的值等于0,表示判断条件为伪,则忽略内部的语句。
#if...#endif之间还可以加入#else指令,用于指定判断条件不成立时,需要编译的语句。
#include <stdio.h>
// 定义宏常量
#define OK 0
#if OK
int a = 100;
char ch = 'z';
#else
double PI = 3.14;
int arr[] = {10, 20, 30, 50};
typedef struct
{
char *name;
} Students;
#endif
int main()
{
printf("%lf\n", PI);
return 0;
}
如果有多个判断条件,还可以加入#elif命令。
#if HAPPY_FACTOR == 0
printf("I'm not happy!\n");
#elif HAPPY_FACTOR == 1
printf("I'm just regular\n");
#else
printf("I'm extra happy!\n");
#endif
上面示例中,通过#elif指定了第二重判断。注意,#elif的位置必须在#else之前。如果多个判断条件皆不满足,则执行#else的部分。
没有定义过的宏,等同于0。因此如果UNDEFINED是一个没有定义过的宏,那么#if UNDEFINED为伪,而#if !UNDEFINED为真。
(2)#ifdef
#ifdef...#endif指令用于判断某个宏是否定义过。
有时源码文件可能会重复加载某个库,为了避免这种情况,可以在库文件里使用#define定义一个空的宏。通过这个宏,判断库文件是否被加载了。
#define EXTRA_HAPPY
上面示例中,EXTRA_HAPPY就是一个空的宏。
然后,源码文件使用#ifdef...#endif检查这个宏是否定义过。
#ifdef EXTRA_HAPPY
printf("I'm extra happy!\n");
#endif
上面示例中,#ifdef检查宏EXTRA_HAPPY是否定义过。如果已经存在,表示加载过库文件,就会打印一行提示。
#ifdef可以与#else指令配合使用。
#ifdef EXTRA_HAPPY
printf("I'm extra happy!\n");
#else
printf("I'm just regular\n");
#endif
上面示例中,如果宏EXTRA_HAPPY没有定义过,就会执行#else的部分。
#ifdef...#else...#endif可以用来实现条件加载。
#ifdef MAVIS
#include "foo.h"
#define STABLES 1
#else
#include "bar.h"
#define STABLES 2
#endif
上面示例中,通过判断宏MAVIS是否定义过,实现加载不同的头文件。
(3)#if defined
上一节的#ifdef指令,等同于#if defined。
#ifdef FOO
// 等同于
#if defined FOO
(4)#ifndef
#ifndef...#endif指令跟#ifdef...#endif正好相反。它用来判断,如果某个宏没有被定义过,则执行指定的操作。
#ifdef EXTRA_HAPPY
printf("I'm extra happy!\n");
#endif
#ifndef EXTRA_HAPPY
printf("I'm just regular\n");
#endif
上面示例中,针对宏EXTRA_HAPPY是否被定义过,#ifdef和#ifndef分别指定了两种情况各自需要编译的代码。
#ifndef常用于防止重复加载。举例来说,为了防止头文件myheader.h被重复加载,可以把它放在#ifndef...#endif里面加载。
#ifndef MYHEADER_H
#define MYHEADER_H
#include "myheader.h"
#endif
上面示例中,宏MYHEADER_H对应文件名myheader.h的大写。只要#ifndef发现这个宏没有被定义过,就说明该头文件没有加载过,从而加载内部的代码,并会定义宏MYHEADER_H,防止被再次加载。
(5)应用案例
具体要求
开发一个C语言程序,让它暂停 5 秒以后再输出内容"我爱你不问归期!",并且要求跨平台,在Windows和Linux下都能运行,如何处理。
提示
- Windows平台下的暂停函数的原型是void Sleep(DWORD dwMilliseconds),参数的单位是“毫秒”,位于<windows.h>头文件。
- Linux平台下暂停函数的原型是unsigned int sleep (unsigned int seconds),参数的单位是“秒”,位于 <unistd.h> 头文件。
#include <stdio.h>
// 条件编译
#if _WIN32
#include <windows.h>
#define SLEEP(TIME) Sleep(TIME * 1000);
#elif __LINUS__
#include <unistd.h>
#define SLEEP sleep
#endif
int main()
{
SLEEP(5);
printf("我爱你不问归期!");
return 0;
}
附录:
指令 | 说明 |
#include | 包含一个源代码文件 |
#define | 定义宏 |
#undef | 取消已定义的宏 |
#if | 如果给定条件为真,则编译下面代码 |
#ifdef | 如果宏已经定义,则编译下面代码 |
#ifndef | 如果宏没有定义,则编译下面代码 |
#elif | 如果前面的#if给定条件不为真,当前条件为真,则编译下面代码 |
#endif | 结束一个#if……#else条件编译块 |
本章内容到此结束,下一章为文件操作
关注我,一起学习嵌入式