目录
1. 程序内存
1.1 程序运行时的内存
在学习C语言时,我们经常需要为一个变量来分配一块内存空间,不同类型的变量所占用的内存空间也是不一样的,而每一个程序内部都有各种代码和数据需要存储在内存上。因此,了解内存空间的分配对我们理解程序的运行是极为有利的,下图介绍了Linux x86-64运行时的内存映像
对于一个C程序,操作系统将会针对不同的数据类型在不同的内存区域上开辟空间
- 静态变量及全局变量
静态变量和全局变量被存储在数据段 ,在程序的整个运行周期都不销毁。其中,如果变量未初始化,则存放在.bss段,初始化的变量存放在.data段
#include <stdio.h>
int a = 10; //全局变量(初始化)-.bss
int main()
{
static int a; //static修饰的静态变量(未初始化)-.data
return 0;
}
- 常量
常量存放在代码段中的.rodata段,对于存放在这一块的常量是不可以被修改的,其状态为只读(read only data),但是注意并不是所有的常量都是放在常量数据段的,其特殊情况如下:
- 有些立即数与指令编译在一起直接放在代码段(text段)
- 对于字符串常量,编译器会去掉重复的常量,让程序的每个字符串常量只有一份
- 有些系统中rodata段是多个进程共享的,目的是为了提高空间的利用率
#include <stdio.h>
int main()
{
printf("%s", "test"); //这里的字符串test就是一个常量字符串,存放在.rodata中
return 0;
}
- 代码
可执行的代码和部分的只读常量存放在代码段的.text段,它与rodata段的主要不同是,text段是可以执行的,而且不被不同的进程共享
- 临时变量和函数参数
在内存中有一个部分称为栈区(stack) ,用来保存临时变量以及函数参数,栈区变量的生存周期是在入栈前(函数开始被调用)获得内存空间,而在出栈时(函数结束调用)释放内存空间。值得注意的是,栈的增长是由高地址向地址值增长的,并由栈指针%rsp来维护栈顶的地址
int main()
{
int a = 10; //临时变量,存放在栈区
test(a); //函数参数,存放在栈区
return 0;
}
- 由内存分配函数申请的空间
在内存中有一个区域是专门服务于程序的,当程序需要动态地申请内存时,操作系统就会在这个部分开辟空间供程序使用——堆(heap)是最自由的一种内存,它完全由程序来负责内存的管理,包括什么时候申请,什么时候释放,而且对它的使用也没有什么大小的限制。在C/C++中,用malloc函数和new申请的内存都存在于heap段中
int main()
{
int* p = (int*)malloc(sizeof(int) * 4); //用malloc函数申请的4个int类型大小的空间-在堆区
return 0;
}
1.2 内存开辟的方式
在C语言中,一般有两种开辟内存的方式
1. 静态开辟内存
int main()
{
int a = 10;
float b = 5.0;
char c[5] = { 0 };
return 0;
}
类似于这种语句,由于具有给定的数据类型,因此能够直接在内存中分配相应大小的空间来存储数据。比如int a = 10,就在内存中的栈区开辟了4个字节用于存放数据10;char c[5] = {0},在栈区开辟了10个字节的空间。这些空间的开辟有两个特点:
- 空间开辟大小是固定的
- 数组在申明的时候,必须指定数组的长度,它所需要的内存在编译时分配
但这种内存开辟是有一定局限性的,考虑下面一种情况
程序需要不断更新数据,例如使用一个能存放10个整型的数组,在后续需要存入更多的数据,这时无法再去修改源程序,而如果开辟数组时将其长度指定得过大,又会造成内存的浪费,因此我们需要根据实际情况,动态的增长内存空间
2. 动态开辟内存
使用内存分配函数能够实现动态地开辟空间,下面将会介绍动态内存函数的使用
2. 动态内存函数
2.1 malloc
首先要介绍的第一个函数是malloc,函数定义为
void* malloc (size_t size);
这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针
malloc的参数是一个无符号的size,这个size由程序来指定,其意义是在堆区分配空间的大小,如果size为10,操作系统会为程序在堆区分配一个大小为10字节的空间,并将使用权交给程序
malloc的返回值是一个无类型的指针,它指向分配好的空间的起始地址,通常在使用时需要根据数据类型进行强制转化
int main()
{
int* p = (int*)malloc(sizeof(int) * 4);
if(NULL != p)
{
//使用
}
return 0;
}
这个代码说明
- 开辟了是4个int类型的大小——16个字节
- 需要将指针强制转化成int* 类型的指针再使用
- 在使用该内存空间之前,先要判断是否分配成功再使用
注意事项
- 如果开辟成功,则返回一个指向开辟好空间的指针
- 如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查
- 返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定
- 如果参数 size 为0,malloc的行为是标准是未定义的,取决于编译器
2.2 free
C语言提供了另外一个函数free,专门是用来做动态内存的释放和回收的,函数定义为
void free (void* ptr);
free函数是用来回收动态分配的内存空间的 ,参数ptr是指向动态分配空间首地址的指针
一般来说,在程序结束后,操作系统会自动释放在之前为程序动态开辟的内存空间,但如果程序出错或,会导致操作系统无法将内存空间收回,造成内存泄漏 ,因此,当内存分配的空间不再使用时,程序员使用free函数主动释放动态分配的空间是一种很好的编码习惯
int main()
{
int* p = (int*)malloc(sizeof(int) * 4);
free(p); //释放掉动态申请的空间p
p = NULL;
return 0;
}
注意事项
- 如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的
- 如果参数 ptr 是NULL指针,则函数什么事都不做
- 在free空间之后,将指针置空是很好的编码习惯,防止造成野指针的错误
2.3 calloc
C语言还提供了一个函数叫 calloc , calloc 函数也用来动态内存分配。函数定义为
void* calloc (size_t num, size_t size);
- 函数的功能是为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为0
- 与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0
如果需要对开辟的空间进行初始化,通常使用calloc函数
2.4 realloc
realloc函数能够在malloc和colloc函数动态开辟的空间的基础上,接着开辟空间,函数定义为
void* realloc (void* ptr, size_t size);
参数ptr是之前就动态开辟好的内存空间的起始地址,参数size是接着要开辟空间的大小。返回值是改变后的空间的起始地址,下面会详细讲述
考虑下面的情况
- 使用malloc分配100个字节的空间,空间过大产生浪费,可以使用realloc将空间调整到10个字节,并将原地址返回
- 使用malloc分配100个字节的空间,空间过小不够存放数据,可以使用realloc将空间调整到200个字节,这个调整基于原地址空间后面有100个字节,那么将原地址返回
- 同样是上面的情况,原地址空间后面连续的空间不足100个字节,realloc函数重新在内存中找到一块大小足够的新空间并开辟,将原空间释放,返回新的内存地址
int main()
{
int* p = (int*)calloc(4, sizeof(int));
if (p != NULL)
{
p = (int*)realloc(p, sizeof(int) * 5);
}
return 0;
}
上述代码先动态分配16个字节的空间,之后使用realloc在p后面分配20个字节的空间,因此这一块连续空间的大小为36个字节
3. 常见的动态内存错误
3.1 对NULL指针的解引用操作
void test()
{
int* p = (int*)malloc(INT_MAX / 4);
*p = 20; //如果p的值是NULL,就会有问题
free(p);
}
3.2 对动态开辟空间的越界访问
void test()
{
int i = 0;
int* p = (int*)malloc(10 * sizeof(int));
if (NULL == p)
{
exit(EXIT_FAILURE);
}
for (i = 0; i <= 10; i++)
{
*(p + i) = i; //当i是10的时候越界访问
}
free(p);
}
3.3 对非动态开辟内存使用free释放
void test()
{
int a = 10;
int* p = &a;
free(p); //报错
}
3.4 使用free释放一块动态开辟内存的一部分
void test()
{
int* p = (int*)malloc(100);
p++;
free(p); //p不再指向动态内存的起始位置
}
3.5 对同一块动态内存多次释放
void test()
{
int* p = (int*)malloc(100);
free(p);
free(p);//重复释放
}
3.6 动态开辟内存忘记释放(内存泄漏)
void test()
{
int* p = (int*)malloc(100);
if (NULL != p)
{
*p = 20;
}
}
int main()
{
test();
while (1);
}
4. 通讯录
使用内存的动态分配,实现动态版本的通讯录,代码如下:
contact.h
#pragma once
#include <stdio.h>
#include <string.h>
#include <assert.h>
#include <stdlib.h>
//类型的声明
#define MAX 1000
#define NAME_MAX 20
#define SEX_MAX 5
#define TELE_MAX 12
#define ADDR_MAX 30
#define DEFAULT_SZ 3
//定义一个结构体, 包含联系人信息
typedef struct PeoInfo
{
char name[NAME_MAX];
char sex[SEX_MAX];
int age;
char tele[TELE_MAX];
char addr[ADDR_MAX];
}PeoInfo;
//定义一个结构体, 作为通讯录(静态的数组版本)
//typedef struct Contact
//{
// PeoInfo* data[MAX]; //可以存放1000个人的信息
// int sz; //记录通讯录中已经保存的信息个数
//}Contact;
//动态版本
typedef struct Contact
{
PeoInfo* data; //指向动态分配的空间
int sz; //记录通讯录中已经保存的信息个数
int capacity; //记录通讯录当前的最大容量
}Contact;
//函数的声明
//初始化通讯录
void InitContact(Contact* pc);
//销毁通讯录
void DestoryContact(Contact* pc);
//增容的函数实现
void CheckCapacity(Contact* pc);
//增加联系人的信息
void AddContact(Contact* pc);
//打印通讯录中的信息
void PrintContact(const Contact* pc);
//删除指定联系人
void DelContact(Contact* pc);
//查找指定联系人
void SearchContact(const Contact* pc);
//修改指定联系人的信息
void ModifyContact(Contact* pc);
//排序联系人信息
void SortConcatc(Contact* pc);
//清空通讯录
void EmptyContact(Contact* pc);
contact.c
#include "contact.h"
//初始化通讯录的函数实现
void InitContact(Contact* pc)
{
assert(pc);
pc->sz = 0;
pc->capacity = DEFAULT_SZ;
pc->data = (PeoInfo*)malloc(pc->capacity * sizeof(PeoInfo));
//如果malloc失败
if (pc->data == NULL)
{
perror("InitContact::malloc");
return;
}
memset(pc->data, 0, pc->capacity * sizeof(PeoInfo));
}
//销毁通讯录的函数实现
void DestoryContact(Contact* pc)
{
free(pc->data);
pc->data = NULL;
pc->sz = 0;
pc->capacity = 0;
printf("销毁成功\n");
}
//增容的函数实现
void CheckCapacity(Contact* pc)
{
if (pc->sz == pc->capacity)
{
PeoInfo* tmp = (PeoInfo*)realloc(pc->data, (pc->capacity + 2) * sizeof(PeoInfo)); //每次增容两个
if (tmp != NULL)
{
pc->data = tmp;
}
else
{
perror("CheckCapacity::realloc");
}
pc->capacity += 2;
printf("增容成功\n");
}
}
//增加联系人的函数实现
void AddContact(Contact* pc)
{
assert(pc);
CheckCapacity(pc); //每次要添加联系人, 先检查容量是否已满
//录入信息
printf("请输入名字:>");
scanf("%s", pc->data[pc->sz].name);
printf("请输入年龄:>");
scanf("%d", &(pc->data[pc->sz].age));
printf("请输入性别:>");
scanf("%s", pc->data[pc->sz].sex);
printf("请输入电话:>");
scanf("%s", pc->data[pc->sz].tele);
printf("请输入地址:>");
scanf("%s", pc->data[pc->sz].addr);
pc->sz++;
printf("添加成功\n");
}
//打印通讯录的函数实现
void PrintContact(const Contact* pc)
{
assert(pc);
int i = 0;
printf("%-20s %-5s %-5s %-12s %-30s\n", "姓名", "年龄", "性别", "电话", "地址");
for (int i = 0;i < pc->sz;i++)
{
printf("%-20s %-5d %-5s %-12s %-30s\n", pc->data[i].name, pc->data[i].age, pc->data[i].sex, pc->data[i].tele, pc->data[i].addr);
}
}
//通讯录中查找的函数实现
int FindByName(const Contact* pc, char name[])
{
assert(pc);
int i = 0;
for (i = 0;i < pc->sz;i++)
{
if (0 == strcmp(pc->data[i].name, name))
{
return i;
}
}
return -1;
}
//删除通讯录信息的函数实现
void DelContact(Contact* pc)
{
assert(pc);
//无有效信息
if (pc->sz == 0)
{
printf("通讯录已空, 无法删除\n");
return;
}
//删除联系人的函数实现
char name[NAME_MAX] = { 0 };
printf("请输入要删除人的名字:>");
scanf("%s", name);
int pos = FindByName(pc, name);
//1.无效名字
if (pos == -1)
{
printf("要删除的人不存在\n");
return;
}
//2.删除
int j = 0;
for (j = pos;j < pc->sz - 1;j++)
{
pc->data[j] = pc->data[j + 1];
}
pc->sz--;
printf("删除成功\n");
}
//查找联系人的函数实现
void SearchContact(const Contact* pc)
{
assert(pc);
char name[NAME_MAX] = { 0 };
printf("请输入要查找人的名字:>");
scanf("%s", name);
int pos = FindByName(pc, name);
if (pos == -1)
{
printf("要查找的人不存在\n");
return;
}
//找到了, 打印pos位置上联系人的信息
printf("%-20s %-5s %-5s %-12s %-30s\n", "姓名", "年龄", "性别", "电话", "地址");
printf("%-20s %-5d %-5s %-12s %-30s\n", pc->data[pos].name, pc->data[pos].age, pc->data[pos].sex, pc->data[pos].tele, pc->data[pos].addr);
}
//修改指定联系人的信息函数实现
void ModifyContact(Contact* pc)
{
assert(pc);
char name[NAME_MAX] = { 0 };
printf("请输入要修改信息人的名字:>");
scanf("%s", name);
int pos = FindByName(pc, name);
if (pos == -1)
{
printf("要修改信息人不存在\n");
return;
}
//修改pos位置上联系人的信息
printf("请重新输入名字:>");
scanf("%s", pc->data[pos].name);
printf("请重新输入年龄:>");
scanf("%d", &(pc->data[pos].age));
printf("请重新输入性别:>");
scanf("%s", pc->data[pos].sex);
printf("请重新输入电话:>");
scanf("%s", pc->data[pos].tele);
printf("请重新输入地址:>");
scanf("%s", pc->data[pos].addr);
printf("修改成功\n");
}
//排序联系人信息的函数实现
void SortConcatc(Contact* pc)
{
assert(pc);
//按照姓氏的首字母排序
for (int i = 0;i < pc->sz - 1;i++)
{
for (int j = 0;j < pc->sz - 1 - i;j++)
{
if (strcmp(pc->data[j].name, pc->data[j + 1].name) > 0)
{
PeoInfo tmp = pc->data[j];
pc->data[j] = pc->data[j + 1];
pc->data[j + 1] = tmp;
}
}
}
printf("排序完成\n");
}
//清空通讯录的函数实现
void EmptyContact(Contact* pc)
{
assert(pc);
pc->sz = 0;
printf("清空成功\n");
}
test.c
#include "contact.h"
enum Option
{
EXIT,
ADD,
DEL,
SEARCH,
MODIFY,
SORT,
PRINT,
EMPTY
};
void menu()
{
printf("*************************\n");
printf("*** 1.add 2.del ***\n");
printf("*** 3.search 4.modify ***\n");
printf("*** 5.sort 6.print ***\n");
printf("*** 7.empty 0.exit ***\n");
printf("*************************\n");
}
void test()
{
int input = 0; //记录用户输入
//创建通讯录
Contact con;
//初始化通讯录
InitContact(&con);
do
{
menu();
printf("请选择:>");
scanf("%d", &input);
switch (input)
{
case ADD:
AddContact(&con);
break;
case DEL:
DelContact(&con);
break;
case SEARCH:
SearchContact(&con);
break;
case MODIFY:
ModifyContact(&con);
break;
case SORT:
SortConcatc(&con);
break;
case PRINT:
PrintContact(&con);
break;
case EMPTY:
EmptyContact(&con);
break;
case EXIT:
DestoryContact(&con);
printf("退出通讯录\n");
break;
default:
printf("选择错误\n");
break;
}
} while (input);
}
int main()
{
test();
return 0;
}