C语言:指针

动态内存分配


要实现动态内存分配,需要使用标准C库提供的库函数,我们所说的动态内存分配,其实就是在堆区申请内存(此时的内存回收需要程序员自身来维护)

常用函数

malloc
  • 头文件:#include <stdlib.h>

  • 函数原型:

  void* malloc(size_t size);
  • 功能:分配指定字节数的内存到堆区,返回指向内存块首地址的指针。内存内容未初始化(随机值)。

  • 参数:

    • size:要分配的内存大小(字节),这里的size_t是数据类型 unsigned long int的别名。

  • 返回值:

    • 成功:返回内存指针(申请到的内存的首地址)

    • 失败:失败返回NULL

  • 示例:

     /*************************************************************************
       > File Name:    demo01.c
       > Author:       久念昕
       > Description:  
       > Created Time: 2025-07-31 09:31:49
      ************************************************************************/
     #include <stdio.h>
     #include <stdlib.h>
     #include <string.h>
     ​
     int main(int argc, char *argv[])
     {
         // 新建一个指针变量,用来接收内存分配后返回的堆内存首地址
         int *p = malloc(sizeof(int)); // 4字节 这里尽量不使用常量
         
         // 对于指针的使用,一定要进行有效性校验,防止野指针
         if(p == NULL){ // 等价于 !p
             perror("内存申请失败!");// 这个函数用于向控制台输出异常信息
             return -1;
         }
         
         // malloc申请的内存空间,默认填充的是随机值,需要我们手动清零
         memset(p,0,sizeof(int)); // 对指定空间赋值0,支持批量赋值
         
         // 向这块空间赋值
         *p = 100;
         // 访问这块空间
         printf("%d\n", *p);
         
         // 堆空间使用完毕,一定要释放内存,此时需要程序员写代码释放
         free(p);
         
         // 如果指针对应的内存被释放,此时指针需要置空,防止产生空悬指针
         p = NULL;
         
         
         return 0;
     }
  • 注意事项:

    • 分配内存后需要手动初始化内存,推荐memsetcalloc

    • 内存空间连续,不可越界访问

calloc
  • 头文件:#include <stdlib.h>

  • 函数原型:

      void* calloc(size_t nitems, size_t size);

  • 功能:动态分配内存,并初始化为0

  • 参数:

    • nitems:元素个数

    • size:每个元素的字节大小

  • 返回值:

    • 成功:返回内存指针(申请到的内存的首地址)

    • 失败:失败返回NULL

  • 示例:

     /*************************************************************************
       > File Name:    demo01.c
       > Author:       久念昕
       > Description:  
       > Created Time: 2025-07-31 09:50:17
      ************************************************************************/
     #include <stdio.h>
     #include <stdlib.h>
     #include <string.h>
     ​
     #define LEN 5
     ​
     int main(int argc, char *argv[])
     {
         // 创建一个指针变量,用来接收内存分配后返回的堆内存地址
         int *arr = calloc(LEN, sizeof(int)); // 等价于 int *arr = malloc(LEN * sizeof(int); memset(arr,0,LEN * sizeof(int));
         
         // 对于指针的使用,一定要进行非空检验,防止野指针
         if (arr == NULL) // 等价于 !arr
         {
             perror("内存申请失败!");
             return -1;
         }
         
         // 使用for循环快速赋值
         for (int i = 0; i < LEN; i++)
         {
             if(i % 2 == 0) continue;
             arr[i] = (i+1)*10;
         }
         
         // 遍历数组
         int *p = arr;
         for (; p < arr + LEN; p++)
         {
             printf("%-6d", *p);
         }
         printf("\n");
         
         // 内存使用完毕,释放内存
         free(arr);
         // 置空,防止产生空悬指针
         arr = p = NULL;
         
         return 0;
     }
  • 使用场景:为数组分配内存时更安全高效。

realloc
  • 头文件:#include <stdlib.h>

  • 函数原型:

      void* realloc(void* ptr, size_t size);
  • 功能:调整已分配内存块的大小,可能迁移数据到新地址。扩容后,空余位置是随机值,需要手动清零。

  • 原理:

    • 当前内存空间后面的剩余空间足够扩容,就在原空间基础上进行扩容,返回原地址;

    • 当前内存空间后面的剩余空间不够扩容,会重新开辟一块空间,将原空间数据按顺序拷贝后,销毁原空间,返回新地址。扩容后的内存空间中的数据是随机的。需要手动清零。

  • 参数:

    • ptr:原内存指针(需要扩容的内存空间的指针,这个ptr的来源(malloc/calloc/realloc))

    • size:指定重新分配内存的大小(字节)

  • 返回值:

    • 成功:返回内存指针(申请到的内存的首地址)

    • 失败:失败返回NULL

  • 示例:

     /*************************************************************************
       > File Name:    demo03.c
       > Author:       久念昕
       > Description:  
       > Created Time: 2025-07-31 10:21:26
      ************************************************************************/
     #include <stdio.h>
     #include <stdlib.h>
     #include <string.h>
     ​
     #define LEN 5
     #define NEW_LEN 8
     #define NEW_LEN_MIN 3
     ​
     int main(int argc, char *argv[])
     {
         // 创建一个指针,指向堆内存分配的内存空间
         int *arr = (int*)calloc(LEN, sizeof(int));
         
         // 指针的非空检验,防止野指针
         if (!arr){
             perror("内存申请失败!");
             return -1;
         }
         
         // 使用for循环赋值
         for (int i = 0; i < LEN; i++)
         {
             arr[i] = (i+1)*10;
         }
         
         // 遍历数组
         int *p = arr;
         for (; p < arr + LEN; p++) printf("%-6d", *p); printf("\n");
         
         // 扩容数组
         int *temp = (int*)realloc(arr, NEW_LEN * sizeof(int)); // 扩容出来的空间,默认是随机值
         // 非空校验,防止野指针
         if(!temp){
             perror("内存申请失败!");
             free(arr); // 扩容失败,回收原空间
             return -1;
         }
         
         // 更新数组指针
         arr = temp;
         // 对扩容部分清零
         memset(arr+LEN,0,(NEW_LEN - LEN) * sizeof(int));
         
         // 修改数组元素
         arr[7] = 666;
         
         // 遍历数组
         for (int i = 0; i < NEW_LEN; i++) printf("%-6d", *(arr+i)); printf("\n");
         
         // 缩容数组
         temp = (int*)realloc(arr, 0); // 此时,会回收掉内存,等价于free(arr);
         // 非空校验,防止野指针
         if(!temp){
             perror("内存申请失败!");
             return -1;
         }
         
         // 更新数组指针
         arr = temp;
     ​
         // 遍历数组
         for (int i = 0; i < NEW_LEN; i++) printf("%-6d", *(arr+i)); printf("\n");
         
         // 释放内存
         free(arr);
         arr = NULL;
         p = NULL;
         
         return 0;
     }
  • 注意事项:

    • 分配后需要手动初始化内存,推荐memset

    • 必须用临时变量接收返回值,避免直接覆盖原指针导致内存泄漏。

    • size为0,等效于free(ptr)

free
  • 头文件:#include <stdlib.h>

  • 函数原型

void free(void* ptr);
  • 功能:释放动态分配的内存。

  • 注意事项:

    • 只能释放一次,重复释放会导致程序崩溃。

    • 释放后应将指针置为NULL,避免野指针。

    • 栈内存由系统自动释放,无需手动free

内存操作


我们对于内存操作需要依赖于string.h头文件中相关的库函数。

常用函数


内存填充
  • 头文件:#include <string.h>

  • 函数原型:

     void* memset(void* s, int c, size_t n);
  • 函数功能:将内存块s的前n个字节填充为c,一般用于初始化或者清零操作。

  • 参数说明:

    • s:目标内存首地址

    • c:填充值(以unsigned  char形式处理(0~255))

    • n:填充字节数

  • 返回值:

    • 成功:返回s的指针

    • 失败:返回NULL

  • 注意事项:

    • 常用于动态初始化,c通常设置为0(清零)

    • 按字节填充,非整型初始化需要谨慎(如填充int数组时,0是安全的)

  • 案例:

     /*************************************************************************
       > File Name:    demo04.c
       > Author:       久念昕
       > Description:  
       > Created Time: 2025-07-31 11:03:23
      ************************************************************************/
     #include <stdio.h>
     #include <stdlib.h>
     #include <string.h>
     ​
     #define LEN 4
     ​
     int main(int argc, char *argv[])
     {
         // 在堆内存申请4个int的连续空间
         int *p = (int*)malloc(LEN * sizeof(int));
         
         // 非空校验
         if (!p){
             perror("内存申请失败!");
             return -1;
         }
         
         // 初始化堆内存空间,填充0,我们也可以称作 清零操作
         memset(p, 0, LEN * sizeof(int));
         
         // 测试输出
         printf("%d,%d\n", p[1], *(p+1));
         
         // 释放内存
         free(p);  // 等价于 realloc(p,0);
         
         // 对指针置空,防止空悬指针
         p = NULL;
         
         return 0;
     }
内存拷贝
  • 头文件:#include <string.h>

  • 函数原型:

    • 源与目标内存无重叠时使用

      void* memcpy(void* dest, const void* src, size_t n);
    • 安全处理内存重叠

      void* memmove(void* dest, const void* src, size_t n);
  • 函数功能:将src的前n个字节拷贝到dest

  • 参数说明:

    • dest:目标内存首地址,支持指针偏移

    • src:源内存首地址,支持指针偏移

    • size_t n:拷贝的字节数

  • 返回值:

    • 成功:返回dest的首地址

    • 失败:返回NULL

  • 注意事项:

    • memmove能正确处理内存重叠,推荐优先使用

    • 确保目标内存足够大,避免溢出。

  • 示例:

    /*************************************************************************
      > File Name:    demo05.c
      > Author:       久念昕
      > Description:  
      > Created Time: 2025-07-31 11:23:20
     ************************************************************************/
    #include <stdio.h>
    #include <string.h>
    
    int main(int argc, char *argv[])
    {
    	// 准备两个数组,用来存储源和目标
    	int src[4] = {11,22,33,44}; // 拷贝 22,33  从src+1开始,拷贝2个sizeof(int)
    	int dest[6] = {111,222,333,444,555,666}; // 目的地 dest+1,拷贝后:111,22,33,444,555,666
    	
    	register int i = 0;
    	printf("拷贝前:");
    	for (i = 0; i < 6; i++) printf("%-6d", dest[i]);
    	
    	// 进行拷贝
    	// memcpy(dest+1,src+1,2 * sizeof(int));
    	memmove(dest+1, src+1, 2 * sizeof(int));
    	
    	printf("\n拷贝后:");
    	for (i = 0; i < 6; i++) printf("%-6d", dest[i]); printf("\n");
    	
    	return 0;
    }

思考:什么是内存重叠?

内存比较
  • 头文件:#include <string.h>

  • 函数原型:

    int memcmp(const void* s1, const void* s2, size_t n);   
  • 函数功能:比较s1s2的前n个字节

  • 返回值:

    • 0:内存内容相同

    • >0s1中第一个不同字节大于s2

    • <0s1中第一个不同字节小于s2

  • 注意事项:比较按字节进行,非字符串需确保长度一致(总字节数一致)。

  • 示例:

    /*************************************************************************
      > File Name:    demo06.c
      > Author:       久念昕
      > Description:  
      > Created Time: 2025-07-31 11:41:20
     ************************************************************************/
    #include <stdio.h>
    #include <string.h>
    
    int main(int argc, char *argv[])
    {
    	// 准备两个测试用的数组
    	int src[] = {111,22,33,44};
    	int dest[] = {111,22,10,44,555,666};
    	
    	// 进行比较
    	int result = memcmp(src, dest, 4 * sizeof(int)); 
    	printf("%d与%d的比较结果是%d\n",*src, *dest, result);
    	
    	return 0;
    }
内存查找
  • 头文件:#include <string.h>

  • 函数原型:

    • 正向查找,C语言标准库函数

      void* memchr(const void* s, int c, size_t n);
    • 逆向查找,这个不是C语言标准库函数,属于GNU扩展

      void* memrchr(const void* s, int c, size_t n);
  • 函数功能:在s的前n个字节中查找字符c

  • 返回值:

    • 成功:返回找到内容对应的地址

    • 失败:返回NULL

  • 注意事项:

    • memrchr是GNU扩展函数,需手动声明(只要不是C语言标准提供,编译的时候都需要手动声明或链接)

    • 查找单位为字节值,非整型数据需要注意内存布局

  • 示例:

    /*************************************************************************
      > File Name:    demo07.c
      > Author:       久念昕
      > Description:  
      > Created Time: 2025-07-31 14:24:24
     ************************************************************************/
    #include <stdio.h>
    #include <string.h>
    // #include <stddef.h>
    
    // 逆向查找函数 memrchr是GNU扩展函数,这个函数需要额外声明
    extern void* memrchr(const void*, int, size_t);
    
    //void* memrchr(const void* s, int c, size_t n) {
    //	const unsigned char* ptr = (const unsigned char*)s + n;
    //	while (n--) {
    //		if (*--ptr == (unsigned char)c) {
    //			return (void*)ptr;
    //		}
    //	}
    //	return NULL;
    //}
    
    int main(int argc, char *argv[])
    {
    	// 准备一个测试数组
    	char str[] = {'A','B','C','B'};
    	
    	// 查找字符 B
    	char* first = (char*)memchr(str, 'B', sizeof(str)); // 返回 低地址对应的字符地址
    	char* last  = (char*)memrchr(str, 'B', sizeof(str)); // GNU扩展函数,返回 高地址对应的字符地址
    	
    	printf("first=%p,last=%p\n", first, last);// first=0x7fff65a47875,last=0x7fff65a47877
    	printf("第1个B的位置:%ld\n", first - str);// 1
    	printf("最后1个B的位置:%ld\n", last - str);// 3
    	
    	return 0;
    }

综合案例:学生成绩管理系统v2.0


  • 需求:

    要求实现一个基于指针的学生成绩管理系统,具体功能如下:

    1. 添加学生信息:输入学号和三门成绩,存储到数组中。

    2. 显示所有学生信息:遍历数组,输出每个学生的学号和成绩。

    3. 计算每个学生的平均分和总分:遍历数组,计算每行的总分和平均分。

    4. 根据某科成绩排序:用户选择科目,然后按该科成绩排序,可以升序或降序。

    5. 查找学生信息:按学号查找,显示该生的成绩和平均分。

    6. 退出程序。

  • 代码:

    /*************************************************************************
      > File Name:   scoresMgr.c 
      > Author:       久念昕
      > Description:  学生成绩管理系统v2.0
      > Created Time: 2025-07-31 15:21:00
     ************************************************************************/
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    
    #define MAX_STUDENTS 50    // 最大学生数
    #define COURSE_NUM 3       // 课程科目数量
    #define ID_LENGTH 4        // 学号长度
    
    /** 函数原型声明 **/
    // 添加学生信息
    void addStudent(int (*scores)[COURSE_NUM], char (*ids)[ID_LENGTH], int *count);
    // 显示所有记录
    void displayAll(int (*scores)[COURSE_NUM], char (*ids)[ID_LENGTH], int count);
    // 查看统计信息
    void showStatistics(int (*scores)[COURSE_NUM], char (*ids)[ID_LENGTH], int count);
    // 成绩排序
    // 查找学生
    // 校验学号 1-校验合格  0-校验不合格
    int validateId(char *id);
    
    /**
     * @brief 入口函数
     * 
     * @param argc 
     * @param argv 
     * 
     * @return 
     */
    int main(int argc, char *argv[])
    {
    	int choice;               // 用户菜单选择
    	int studentCount = 0;     // 记录当前学生的数量
    	
    	// 学生数据存储(二维数组)
    	char studentIds[MAX_STUDENTS][ID_LENGTH];       // 学生学号
    	int scores[MAX_STUDENTS][COURSE_NUM];           // 学生成绩
    	
    	// 主循环
    	do{
    		// 系统菜单界面
    		printf("\033[1;32m+-----------------------+\033[0m\n"); // 绿色边框
    		printf("\033[1;32m| \033[1;33m学生成绩管理系统 v2.0\033[1;32m |\033[0m\n"); 
    		printf("\033[1;32m| \033[1;33m作者:冯鹏飞\033[1;32m          |\033[0m\n"); 
    		printf("\033[1;32m+-----------------------+\033[0m\n");
    		printf("\033[31m1. 添加学生信息\033[0m\n");
    		printf("\033[31m2. 显示所有记录\033[0m\n");
    		printf("\033[31m3. 查看统计信息\033[0m\n");
    		printf("\033[31m4. 成绩排序\033[0m\n");
    		printf("\033[31m5. 查找学生\033[0m\n");
    		printf("\033[31m6. 退出系统\033[0m\n");
    		printf("\n请输入您的选择: ");
    		scanf("%d", &choice);
    		
    		switch (choice) {
    		case 1: // 添加学生信息
    			addStudent(scores, studentIds, &studentCount);
    			break;
    		case 2: // 显示所有记录
    			displayAll(scores, studentIds, studentCount);
    			break;
    		case 3: // 查看统计信息
    			showStatistics(scores, studentIds, studentCount);
    			break;
    		case 4: // 成绩排序
    			printf("该功能未开放!\n");
    			break;
    		case 5: // 查找学生
    			printf("该功能未开放!\n");
    			break;
    		case 6: // 退出系统
    			printf("系统已退出,感谢您的使用!\n");
    			return 0;
    		default:
    			//TODO
    			break;
    		}
    	}while(1);
    	
    	return 0;
    }
    
    /**
     * @brief 添加学生信息
     * 
     * @param scores 学生成绩数组
     * @param ids    学生学号数组
     * @param count  当前学生数量
     */
    void addStudent(int (*scores)[COURSE_NUM], char (*ids)[ID_LENGTH], int *count)
    {
    	// 校验存储空间是否已满
    	if (*count >= MAX_STUDENTS)
    	{
    		printf("错误信息:存储空间已满!\n");
    	    return;
    	}
    	
    	printf("\n--- 添加学生信息 ---\n");
    	
    	// 创建一个数组,用来存放学生学号
    	char tempId[ID_LENGTH + 1]; // 控制台输入的字符串以\0结尾
    	
    	// 学号验证
    	do{
    		printf("请输入4位学号:");
    		scanf("%4s", tempId);
    		// 清空缓冲区
    		while(getchar() != '\n');
    	}while(!validateId(tempId));
    	
    	// 检查学号是否存在
    	register int i;
    	for (i = 0; i < *count; i++)
    	{
    		// 使用内存比较函数,比较两块内存中的数据是否相等
    		if (memcmp(ids[i], tempId, ID_LENGTH) == 0)
    		{
    			printf("该学号已存在!\n");
    			return;
    		}
    	}
    	// 向数组中存入学号
    	memcpy(ids[*count], tempId, ID_LENGTH);
    	
    	// 输入成绩
    	printf("请输入%d门课程成绩(0~100):\n", COURSE_NUM);
    	
    	for (i = 0; i < COURSE_NUM;)
    	{
    		printf("课程%d:", i + 1);
    		// 1.非法字符校验
    		int tempScore = scanf("%d", &scores[*count][i]);
    		if (tempScore != 1)
    		{
    			printf("成绩无效:请重新输入!\n");
    			while(getchar() != '\n'); // 清空缓冲区
    			continue;
    		}
    		// 2.输入范围校验
    		if (scores[*count][i] < 0 || scores[*count][i] > 100)
    		{
    			printf("成绩无效:请重新输入!\n");
    			continue;
    		}
    		i++;  // i++ 写在这里,会受到continue的影响
    	}
    	
    	// 更新序号,管理当前学生人数
    	(*count)++;
    	printf("学生信息添加成功!\n");
    }
    
    /**
     * @brief 显示所有记录
     * 
     * @param scores 
     * @param ids 
     * @param count 
     */
    void displayAll(int (*scores)[COURSE_NUM], char (*ids)[ID_LENGTH], int count)
    {
    	printf("\n--- 学生成绩列表 ---\n");
    	// 校验是否存在数据
    	if (count == 0)
    	{
    		printf("暂无学生数据!\n");
    		return;
    	}
    	
    	// 表格数据
    	// 表头 (学号,语文,数学,英语)
    	printf("%s\t%s\t%s\t%s\n","学号","语文","数学","英语");
    	// 数据
    	for (int i = 0; i < count; i++) // 人
    	{
    		// 学号
    	    printf("%.4s\t",ids[i]);
    		// 成绩
    		for (int j = 0; j < COURSE_NUM; j++) // 科目
    		{
    			printf("%d\t", *(*(scores + i) + j)); // scores[i][j]
    		}
    		printf("\n");
    	}
    	printf("\n");
    }
    
    /**
     * @brief 查询统计信息
     * 
     * @param scores 学生成绩数组
     * @param ids    学生学号数组
     * @param count  当前学生数
     */
    void showStatistics(int (*scores)[COURSE_NUM], char (*ids)[ID_LENGTH], int count)
    {
    	// 校验是否存在学生
    	if (count == 0)
    	{
    		printf("暂无学生数据!\n");
    		return;
    	}
    	
    	// 创建一个数组,用来存储每一科总分
    	int courseTotal[COURSE_NUM] = {0};
    	// 创建一个数组,用来存储每一科最高分
    	int courseMax[COURSE_NUM] = {0};
    	// 创建一个数组,用来存储每一科最低分
    	int courseMin[COURSE_NUM] = {100,100,100};
    	
    	// 遍历:计算每一科总分,最高分,最低分
    	for (int i = 0; i < count; i++) // 遍历得到每一个学生,行
    	{
    		for (int j = 0; j < COURSE_NUM; j++) // 遍历得到每一个学生的每一门成绩,列
    		{
    			// 获取每一个成绩
    			int score = scores[i][j];
    			// 单科总分
    			courseTotal[j] += score;
    			// 单科最高分
    			if (score > courseMax[j]) courseMax[j] = score;
    			// 单科最低分
    			if (score < courseMin[j]) courseMin[j] = score;
    		}
    	}
    	
    	// 输出信息
    	printf("\n--- 课程统计信息 ---\n");
    	char *courses[] = {"语文","数学","英语"};
    	for (int i = 0; i < COURSE_NUM; i++)
    	{
    		printf("%s:\n",courses[i]); // 语文 数学 英语
    		printf("  平均分:%.2f\n", (float)courseTotal[i] / count); 
    		printf("  最高分:%d\n", courseMax[i]);
    		printf("  最低分:%d\n", courseMin[i]);
    	}
    }
    
    /**
     * @brief 学号校验
     * 
     * @param id 学号指针
     * 
     * @return 1-合法,0-非法
     */
    int validateId(char *id)
    {
    	char *p = id;
    	int len = 0;
    	// 1.校验学号是否是数字
    	while (*p && len < ID_LENGTH)
    	{
    		// 校验输入的是否是数字
    		if(!(*p >= '0' && *p <= '9'))
    		{
    			printf("学号必须为数字!\n");
    			return 0;// 不合法
    		}
    		p++;
    		len++;
    	}
    	// 2. 校验学号的位数是否满足
    	if (len != ID_LENGTH || *p != '\0')
    	{
    		printf("学号必须为4位!\n");
    		return 0;
    	}
    	
    	return 1;
    	
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值