C语言-内存管理、输入输出缓冲区、字符串
一、C语言的堆内存管理:
堆内存:
是进程的一个内存段(text
、data
、bss
、heap
、stack
),由程序员手动管理。
特点就是足够大,缺点就是使用麻烦,比较危险。
使用堆内存的原因:
- 随着程序变复杂,数据量开始变多。
- 其它内存段的申请和释放不受控制,堆内存的申请释放受程序员控制。
堆内存的使用:C语言中没有管理堆内存的语句,C标准库中提供一套管理堆内存的函数,这些函数底层封装了各操作系统的堆内存管理接口,所以可以跨平台使用,这些函数声明在 stdlib.h
头文件中。
malloc函数:
/**
* 功能:向malloc申请连续的size字节的堆内存块
* @size:
* 要申请的内存块字节数
* 如果申请数组形式的内存块,size=sizeof(数组元素类型)*数组长度
* 返回值:
* 如果申请成功,则返回内存块首地址,绝大多数情况是成功
* 失败申请失败,则返回NULL,例如现在有堆内存无法满足size个字节的需求
*/
void *malloc(size_t size);
注意:
1、使用malloc
申请到的内存块,里面的内容是不确定的,malloc
不会帮我们初始化,可以使用bzero
,memset
函数进行初始化。
2、如果size等于0,返回NULL
或唯一个的地址,并且该地址可以通过free
释放而不出错,但不能使用它指向的内存。
3、通过一次malloc
申请出来的内存,如果成功时该内存段必定连续,如果分多次malloc
申请多段内存段,每段内存段之间不一定连续
calloc函数:
/**
* 功能:申请nmemb个size个字节的内存块,专门用于申请数组型的内存块。
* @nmemb:数组的长度
* @size:数组元素的字节数
* 返回值:与malloc相同
* 注意:使用calloc申请的内存块,所有字节会被初始化0。
*/
void *calloc(size_t nmemb, size_t size);
注意:
1、calloc
所申请也是一块连续的内存块,所以nmemb
和size
的参数位置可以调换,就相当于calloc
内部调用了malloc
函数,只是比malloc
多了初始化步骤,而且比malloc
的可读性更高。
2、malloc
比calloc
申请内存的速度快,或者使用malloc
+bzero
配合。
free函数:
/**
* 功能:释放堆内存
* @ptr:要释放的内存块的首地址,它必须是malloc、calloc函数的返回值
*/
void free(void *ptr);
注意:
1、free
释放的是使用权,只破坏内存块的一部分内存,大部分数据还在,这样设计的原因是释放速度比较快,就像在硬盘上删除文件一样,只是把存储文件那片区域的使用释放旧,数据还存储在磁盘上,这也是我们能进行数据恢复的原因。
2、free
的参数可以是空指针,不会出现错误,也不会执行任何操作,这也是空指针比野指针安全的原因。
3、如果内存被重复释放则会出现double free or corruption (fasttop)
,程序会异常停止,所以在第一次释放内存后,要把与堆内存配合的指针及时的赋值为空,防止重复释放产生的错误。
realloc函数:
/**
* 功能1:把已有的堆内存块调小
* ptr是malloc、calloc、realloc的返回值,也就已有堆内存块首地址
* size < oldsize 此时不需要关心realloc的返回值
*/
/**
* 功能2:把已有的堆内存块调大
* ptr是malloc、calloc、realloc的返回值,也就已有堆内存块首地址
* 情况1:如果ptr后续的内存没有被占用,realloc会在ptr的基础上进行扩大
* 情况2:如果ptr后续的内存已经被占用,realloc会重新分配一块符合要求的内存块,并把ptr上的内容拷贝到新的内存块,然后释放ptr,再返回新内存块的首地址
* 使用此功能时,我们必须重新接收realloc函数的返回值,我们无法预料realloc执行的是情况1还是情况2。
*/
/**
* 功能3:释放内存
* ptr是malloc、calloc、realloc的返回值
* 0==size,此时realloc的功能就相当于free
*/
/**
* 功能4:申请内存
* ptr == NULL , size > 0, 此时的功能就相当于malloc
*/
void *realloc(void *ptr, size_t size);
注意:虽然realloc
具有释放和申请堆内存的功能,但我们一般不使用,而是直接使用malloc
和free
,主要使用的是realloc
的调整堆内存块大小的功能。
总结: 使用堆内存只需要掌握malloc
和free
函数即可,对于calloc
和realloc
函数了解即可。
堆内存越界时为什么超过135160才会出现段错误?
#include <stdio.h>
#include <stdlib.h>
int main() {
char* ptr = malloc(1);
// 只要越界的不超过135160,就不会出现段错误
printf("%c\n", ptr[135160]);
printf("%c\n", ptr[4096 * 33 - 9])
}
- 当程序首次向
malloc
申请内存时,此时malloc
手里没有堆内存可分配,malloc
会向操作系统申请堆内存,操作系统会一次性分配33页内存交给malloc
管理(一页内存=4096个字节),之后再向malloc申请内存时,malloc会从这33页内存中分配给调用者。但这不意味着可以越界访问,因为malloc
把使用分配给"其他人",这样会产生脏数据。 - 使用
malloc
申请的每个内存块前面会有4~12个字节的空隙,malloc
会根据所申请的内存块的大小自动调整空隙的大小。 - 内存块前面的空隙有两部分:
- 空隙的前0~8字节:用于内存对齐(目的提高内存的访问速度)
- 空隙的末尾4字节:也就是内存块前面的4字节,存储首
malloc
的管理信息,这块信息被破坏会影响后续malloc
、free
、printf
、scanf
函数的使用。
注意:堆内存越界的后果?
135160会出现段错误的原因:操作系统交给malloc
33页内存(135168个字节),可访问的范围是**0135167**,`malloc`会预留8个字节的空隙,返回给程序的是33页内存的第9个字节的地址(33页内存还剩135160个字节),所以可访问的范围是0135159,只要在这个范围就不会出现段错误。
优点:
1、避免了频繁打扰操作系统,而影响操作系统的速度。
2、段错误产生的原因是被操作系统发现非法使用内存,所以我们使用malloc分配的内存越界时,只要不超过33页范围就不会产生段错误。
使用堆内存越界的后果:
1、越界使用的是空隙的空闲字节,一切正常,可以安全访问。
int main() {
int* p = malloc(4);
p[0] = 123; // 申请到的内存块
p[1] = 456; // 空闲
p[2] = 789; // 空闲
}
2、越界破坏了malloc的管理信息,会影响后续malloc、free、scanf、printf函数的使用。
int main() {
int* p = malloc(4);
p[3] = 0; // 存储着malloc的管理信息,后续无法继续申请堆内存
p[-1] = 0; // 存储着malloc的管理信息,p内存块无法释放
}
3、越界使用malloc还未分配出去的内存,虽然不会产生段错误,但后续malloc把它分配出去后,可能会产生脏数据。
4、超出33页范围,就产生段错误。
内存碎片:
已经释放了使用权的内存,但无法被malloc
再次分配出去,这种内存叫内存碎片。
int *p1 = malloc(4);
int *p2 = malloc(4);
int *p3 = malloc(4);
free(p2);
// 此时p2已经被释放,但无法再次分配给p4,p2就是内存碎片,如果后续不再分配较小的内存块,p2可能一直接内存碎片
int *p4 = malloc(16);
// 此时p2就有可能被再次分配出来,它就不是内存碎片,
int *p5 = malloc(4);
// 如果p1或p3被释放,p2就不是内存碎片了
free(p1);
free(p3);
内存碎片产生的原因:内存和释放、分配时间、大小不协调导致的。一块内存碎片,只是短时间内是碎片,过一段时间它可能就不是碎片了,所以内存碎片不是绝对的。
如何减少内存碎片:
前提:内存碎片只能尽量减少,无法杜绝。
1、尽量使用栈内存(要了解栈内存的特性,要知道栈内存的使用上限,ulimit -s 、ulimit -s)。
2、尽量分配大块内存自己管理。
3、按照分配的顺序,逆序释放,把堆内存当栈进行管理。
4、内存碎片整理
内存泄漏:
内存已经不再使用,但无法被释放的内存叫内存泄漏。
void func(int num) {
int *p = malloc(num);
if (条件) return;
free(p);
}
int main(void) {
while (1) {
func(4);
}
free(p)
}
但这不是最严重的,严重的是反复的内存泄漏,例如:一个函数执行需要分配一块堆内存,等它执行完毕后堆内存没有被释放,等再次调用这个函数时,它又重新分配堆内存,又没有释放,反复这样会导致可用的内存越来越少,系统、程序会变得越来越慢、卡、死机。
注意:程序一旦结束属于它的资源都会被操作系统回收。但不是所有程序都适用该方法来回收资源。
产生内存泄漏的原因:
1、只写的内存分配语句,而忘记写内存释放语句,可能是粗心大意,也可能是以为别人会释放。
2、写了内存释放语句,但由于执行流程、执行条件设计有问题,导致释放语句没有执行。
3、与堆内存配合的指针被破坏,改变了指向,导致free语句执行无效。
int *p = malloc(4);
*p = 100;
p = NULL;
free(p);
如何减少内存泄漏:
- 按规则分配、释放内存:
- 自用:谁申请谁释放,分配语句和释放语句成对出现。
- 共用:谁知道该释放谁释放,项目组中负责分配和负责释放的人要进行对接。
- 封装
malloc
和free
函数,记录每一次的分配和释放的内存块地址,通过对比记录,就可以发现是否有内存泄漏。- 使用
const
保护与堆内存配合的指针变量,防止指针被破坏。
void* myMalloc(size_t size) {
void* ptr = malloc(size);
printf("debug: my_malloc:%p\n",ptr);
return ptr;
}
void myFree(void *ptr) {
printf("debug: my_free:%p\n",ptr); // 可以记录到日志
free(ptr);
}
// 使用const保护与堆内存配合的指针变量,防止指针被破坏
int* const p = malloc(4);
p = NULL; // 无法修改
如何判断和定位内存泄漏:
1、查看内存的使用情况
windows 任务管理器 、Linux ps -aux
命令
大致确定是哪个进程发生了内存泄漏
2、使用检查内存泄漏的工具:
sudo apt-get update #更新软件源
sudo apt install valgrind # 安装该工具的命令
# valgrind是一套Linux下的仿真调试工具集
# memcheck是其中一个工具,可以检查程序中的内存问题,如泄漏、越界、非法指针等。可以检测:
# 使用未初始化的内存
# 读/写已经被释放的内存
# 读/写内存越界
# 读/写不恰当的内存栈空间
# 内存泄漏
# 使用malloc和free不匹配等
#使用该工具检测程序:
valgrind --tool=memcheck --leak-check=yes ./a.out
--tool=<name>指定要使用的工具,默认为memcheck
--leak-check=yes|no 是否对内存泄漏给出详细信息
3、根据封装的malloc和free记录到日志的信息进行比对
内存泄漏和内存碎片的危害:
前提:当程序结束时,操作系统会把分配它的所有资源全部回收(包括系统分配给程序的堆内存),所以当程序结束时,内存碎片和内存泄漏就会消失,这也是为什么软件、系统重启能解决很多问题。
注意:服务器端的程序一般需要7*24小时运行,不能随意结束。
客户端的程序可以随意的关闭、重启系统和软件,所以即使发生内存碎片和内存泄漏,也问题不大,但如果是服务端的程序,即使只有少量的内存泄漏和内存碎片,长年累月下来也会导致系统可用的内存越来越少,系统、程序会变得越来越慢、卡、死机。
说明:想要共享指针变量,必须传递二级指针
void createMem(void** p, size_t n) {
*p = malloc(n);
printf("create_mem:%p\n",*p);
if (NULL == *p) {
printf("malloc error\n");
}
}
int main() {
int* p = NULL;
createMem(&p,40);
printf("--------%p\n",p);
for (int i = 0; i < 10; ++i) {
p[i] = i;
//printf("--------\n");
printf("%d%c", p[i], " \n"[i == 9]);
}
free(p);
p = NULL;
}
常用的内存操作函数:
void bzero(void *s, size_t n);
功能:把内存块s的n个字节,赋值为0。
void *memset(void *s, int c, size_t n);
功能:把内存块s的n个字节,赋值为c(0~255)
void *memcpy(void *dest, const void *src, size_t n);
功能:从src内存块拷贝n个字节的内容到dest内存块
void *memmove(void *dest, const void *src, size_t n);
功能:与memcpy相同,不同的是当dest与src重叠时,该函数能正常工作,memcpy行为不确定
int memcmp(const void *s1, const void *s2, size_t n);
功能:比较s1和s2内存块的n个字节
s1 > s2 返回1
s1 < s2 返回-1
s1 == s2 返回0
二、缓冲区
输出缓冲区:
当我们使用标准库的输出系列函数打印数据到屏幕,数据并不会立即显示到屏幕上,而先存储到一块内存中,我们把这块内存称为输出缓冲区,等满足相关条件后,再从缓冲区中显示到屏幕,相关条件有:
- 从输出状态切换到输入状态。
- 缓冲区满了,1k=1024个字节,系统会把缓冲区中所有数据一起显示到屏幕了。
- 程序正常结束时,系统会把缓冲区中所有数据一起显示到屏幕了。
- 遇到
'\n'
时,'\n'
前面的数据会立即显示到屏幕上。 - 调用
fflush(stdout)
强制刷新,会把立即输出缓冲区中所有数据一起显示到屏幕了。
总结:缓冲区机制的目的是为了提高输入输出效率
输入缓冲区:
当我们从终端输入数据给程序时,系统并没有立即把数据交给程序读取,而先存储到了一块内存中,我们这块内存称为输入缓冲区,直到我们按下Enter键时,系统才会把缓冲区中的数据给程序读取。
当我们输入的数据过多,或者类型不匹配,标准的输入系列函数就会读取失败,或只读取一部分,剩余的数据就会残留缓冲区中,影响后续数据的输入,当我们发现这情况情况后,应先清理输入缓冲区,后续的数据才能正常输入
清理输入缓冲区的方式:
// 方法1:
while('\n' != getch()); // 清空缓冲区,直到按下回车结束
// 方法2:正则表达式
scanf("%*[^\n]"); // 从缓冲区中读取任意类型数据并丢弃,直到遇到'\n'
scanf("%*c"); // 从换乘区中读取一个字符并丢弃
// 方法3:
stdin->_IO_read_ptr = stdin->_IO_read_end;
// 设置输入缓冲区的位置指针到缓冲区末尾,此时缓冲区会被操作系统自动清空
注意:
1、方法3只能在Linux系统中使用
2、如果输入缓冲区中本来就没有垃圾数据,使用方法1和方法2就需要你手动多输入一个'\n'作为垃圾数据,程序才能往下走
三、字符串:
字符:
字符就是符号或图案,但在计算机中以整数形式存在,当需要显示时,会根据ASCII表中的对应关系显示出相应的符号或图案。
在C语言中使用char类型的变量存储字符的ASCII码值,也就是使用整数进行模拟字符,标准的ASCII码表
的范围是:0 ~ 127
,共128个字符,其他的语种,使用-128 ~ -1
进行设计字符编码,比如中文的汉字,使用的是2~3字节存储一个汉字。
重要的字符:
'\0' ASCII值是 0 空字符 字符串的结束标志
'0' ASCII值是 48
'A' ASCII值是 65
'a' ASCII值是 97
输出:
printf("%c", ASCII值);
putchar(ASCII值);
字符的输入:
char ch;
scanf("%c",&ch);
ch = getchar();
注意:当先输入数值型数据(整数形、浮点型),再输入字符型数据时,前一次的输入会残留一个'\n'或空格,影响字符型数据的输入,是缓冲区在影响字符的输入
解决方法:
// 方法1:增加一个空白字符的接收函数
scanf("%*c");
getch();
getchar();
// 方法2:在%c前面增加一个空格
scanf(" %c");
// 方法3:全部清空输入缓冲区
stdin->_IO_read_ptr = stdin->_IO_read_end;
判断字符类型的函数:
函数名 | 函数功能 |
---|---|
isalnum() | 当字母或数字字符时, 返回真值 |
isalpha() | 当字母字符时, 返回真值 |
iscntrl() | 当控制字符时, 返回真值 |
isdigit() | 当数字字符时, 返回真值 |
isgraph() | 当非空格可打印字符时, 返回真值 |
islower() | 当小写字母字符时, 返回真值 |
isprint() | 当可打印字符时, 返回真值 |
ispunct() | 当标点字符时, 返回真值 |
isspace() | 当空格字符时, 返回真值 |
isupper() | 当大写字母字符时, 返回真值 |
isxdigit() | 当十六进制字符时, 返回真值 |
串型结构:
由若干个相同类型的数据组成顺序表(数组),在数据的末尾有一个结束标志,在使用这种数组时,可以不关心数组的长度。并且串型结构的处理都是批量性的
#include <stdio.h>
void showString(int arr[]) {
for (int i = 0; arr[i] != ~0; ++i) {
printf("%d ",arr[i]);
}
}
int main() {
int arr[] = {33, 5, 0, 63, 34, 23, 5, ~0, 32, 23, 56};
showString(arr);
}
字符串:
由字符类型组成的串型结构,它的结束标志是'\0'
,使用它可以存储单词、句子、文章、汉字等更丰富的信息,一般使用char
类型的数组存储。
// 定义字符串时,要为'\0'预留位置
char arr1[] = {'H','e','l','l','o','\0'};
char arr2[10] = {'H','e','l','l','o'};
字符串字面值:
- "由双引号包括着的若干个字符"
- 它是以常量字符数组的形式存在,末尾隐藏着一个
'\0'
。 - 它们会被存储在
text内存段
,一旦强行修改就会出现段错误。 - 使用指针指向字符串字面值时,一定要用
const
加以保护,防止出现段错误,宁可出现编译时的错误,也不要出现运行时的错误。 - 编译器会优化它的存储,相同的字符串字面值,只会存储一份在text内存段中。
- 最常用的是用它给字符数组初始化,
char arr[] = "hello"
编译器会自动拷贝字符串到数组的内存中(包括'\0'
),完成初始化就有了两份字符串存储在内存中,一份存储在stack
\data
,另一份还存储在text
。
注意:使用字符串字面值给字符数组赋值,只能在定义字符数组时使用,这是编译器帮忙完成拷贝的,在完成字符数组的定义后,只能使用strcpy
函数对字符串进行赋值。
字符串的输出:
printf("%s",字符串的首地址);
puts(字符串的首地址); // 输出完字符串后会再输出一个\n
字符串的输入:
scanf("%s",存储字符串的首地址);
缺点:不能输入带有空格的字符串
char *gets(char *s);
返回值:就是s,为了链式调用
缺点:直接从终端中接收字符数据,遇到'\n',可以接收空格字符,但是它不检查数据的长度跟存储空间的关系,所以很容易接收过长产生段错误、脏数据,官方编译器不建议使用该函数,会产生警告
char *fgets(char *s, int size, FILE *stream);
功能:可以从指定文件stream中读取不超过size - 1个字符会自动在末尾添加'\0',并存储到s中,返回值也是s,为了链式调用
stream: 数据的来源,写stdin即可 stdout 一切皆文件
size:最多只能读取size-1个字符,必定会为'\0'预留位置
缺点1:如果输入的字符个数不足size - 1个时,会把最后输入的'\n'一起接收
缺点2:如果输入的字符个数超过size - 1个时,超出部分的字符数据会继续残留在输入缓冲区中,会继续影响后序的输入
解决方法:
char usr[6] = {};
printf("请输入字符串:");
fgets(usr, 6, stdin);
int len = -1;
// 计算出'\0'下标为len
while(usr[++len]);
// 检查'\0'前面是否是'\n'
if('\n' == usr[len-1]) {
// 证明输入不足size-1
usr[len-1] = '\0';
} else {
// 证明输入超过size-1个,\n在缓冲区中,有残留
// 清理输入缓冲区
//while('\n' != getch());
stdin->_IO_read_ptr = stdin->_IO_read_end;
}
操作字符串的常用函数:
size_t strlen(const char *s);
功能:计算字符串的长度,不包括'\0'
char *strcpy(char *dest, const char *src);
功能:把字符串src拷贝到dest处,相当于 = 运算符
注意:会把src末尾的'\0'一起拷贝过来
char *strcat(char *dest, const char *src);
功能:把src字符串追加到dest的末尾 相当于 += 运算符
注意:从dest的\0开始追加src,并且会把src的\0一起追加过来
int strcmp(const char *s1, const char *s2);
功能:按字典序比较两个字符串
s1 > s2 返回正数
s1 < s2 返回负数
s1 == s2 返回0
逐个字符进行比较,一旦出结果立即结束,后面的不再比较
注意:strlen
与sizeof
的区别
字符串相关函数:
int atoi(const char *nptr);
功能:字符串转int类型
long atol(const char *nptr);
功能:字符串转long类型
long long atoll(const char *nptr);
功能:字符串转long long类型
double atof(const char *nptr);"2.4"
功能:字符串转double类型
char *strstr(const char *haystack, const char *needle);
功能:查找haystack中是否存在needle
返回值:needle第一次在haystack出现的位置,如果找不到返回NULL
"abcdefcd" "cad"
char *strchr(const char *s, int c);
功能:查找字符串s中是否有字符c。
返回值:c在s中第一次出现的位置,如果找不到返回NULL。
int sprintf(char *str, const char *format, ...);
功能:把任意类型的数据输出到str中 把任意类型的数据拼接成字符串
返回值:字符串str的长度
int sscanf(const char *str, const char *format, ...);
功能:从str中读取任意类型数据 从字符串中解析任意类型的数据
返回值:成功读取到的变量个数
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)