C语言:第18天笔记

C语言:第18天笔记

内容提要

  • 动态内存分配
  • 内存操作

动态内存分配

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

常用函数
malloc

头文件 : #include <stdlib.h>

函数原型:

c 复制代码
void* malloc(size_t size);

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

参数:

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

返回值:

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

示例:

c 复制代码
#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;
}

注意事项:

  • 分配内存后需要手动初始化内存,推荐memset 或calloc
  • 内存空间连续,不可越界访问
calloc

头文件 : #include <stdlib.h>

函数原型:

c 复制代码
void* calloc(size_t nitems, size_t size);

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

参数:

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

返回值:

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

示例:

c 复制代码
#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>

函数原型:

c 复制代码
void* realloc(void* ptr, size_t size);

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

原理:

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

参数:

  • ptr : 原内存指针(需要扩容的内存空间的指针,这个ptr的来源(malloc/calloc/realloc ))
  • size : 指定重新分配内存的大小(字节)

返回值:

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

示例:

c 复制代码
#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>

函数原型:

c 复制代码
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是安全的)

案例:

c 复制代码
#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>

函数原型:

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

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

    c 复制代码
    void* memmove(void* dest, const void* src, size_t n);

函数功能 : 将src 的前n 个字节拷贝到dest

参数说明:

  • dest : 目标内存首地址,支持指针偏移
  • src : 源内存首地址,支持指针偏移
  • size_t n : 拷贝的字节数

返回值:

  • 成功: 返回dest 的首地址
  • 失败: 返回NULL

注意事项:

  • memmove 能正确处理内存重叠,推荐优先使用
  • 确保目标内存足够大,避免溢出。

示例:

c 复制代码
#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>

函数原型:

c 复制代码
int memcmp(const void* s1, const void* s2, size_t n);

函数功能: 比较s1 和s2 的前n 个字节

返回值:

  • 0 : 内存内容相同
  • 0 : s1 中第一个不同字节大于s2

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

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

示例:

c 复制代码
#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语言标准库函数

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

    c 复制代码
    void* memrchr(const void* s, int c, size_t n);

函数功能: 在s 的前n 个字节中查找字符

返回值:

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

注意事项:

  • memrchr 是GNU扩展函数,需手动声明(只要不是C语言标准提供,编译的时候都需要手动声明或链接)
  • 查找单位为字节值,非整型数据需要注意内存布局

示例:

c 复制代码
#include <stdio.h>
#include <string.h>
// #include <stddef.h>

// 逆向查找函数 memrchr是GNU扩展函数,这个函数需要额外声明
extern void* memrchr(const void*, int, size_t);


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. 退出程序。

代码:

c 复制代码
#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作者:zz\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;
}

章节作业

  1. 利用指针变量将一个数组中的数据反向输出。
  2. 利用指针变量计算下标为奇数的数组的和;
  3. 确认整型,字符型,浮点型指针变量的大小;
  4. 利用指针变量输出字符数组中的所有字符。
  5. 编写一个函数,用指针变量做参数,用于求出一个浮点型数组元素的平均值。
  6. 编写函数,要求用指针做形参,分别实现以下功能:
    (1)求一个字符串长度
    (2)在一个字符串中统计大写字母的个数
    (3)在一个字符串中统计数字字符的个数
  7. 编写函数,要求用指针做形参,实现将二维数组(行列相同)的进行转置(行列数据互换): int (*p)[N]
  8. 编写函数,要求用指针做形参,实现统计二维数组上三角中的0 的数量:
  9. 编写一个指针函数,返回二维数组中最大元素的地址。
  10. 面试题
    1)定义整形变量i;
    2)p为指向整形变量的指针变量;
    3)定义整形一维数组p,它有n 个整形元素;
    4)定义一维指针数组p,它有n个指向整形变量的指针元素;
    5)定义p为指向(含有n个整形元素的一维数组)的指针变量;
    6)p为返回整形函数值的函数;
    7)p为返回一个指针的函数,该指针指向整形数据;
    8)p为指向函数的指针变量,该函数返回一个整形值;
    9)p是一个指向整形指针变量的指针变量;
  11. 动态申请一个具有10个float类型元素的内存空间,从一个已有的数组中拷贝数据,并找出第一次出现 12.35 的下标位置,并输出。
  12. 动态申请一个整型数组,并给每个元素赋值,要求删除第3个元素;
  13. 动态申请一个整型数组,并给每个元素赋值,要求在第4个元素后插入100;
  14. 附加题【选做】: 编写3个函数,分别实现 memmove,memcmp,memchr的功能。
相关推荐
云天徽上23 分钟前
【数据可视化-96】使用 Pyecharts 绘制主题河流图(ThemeRiver):步骤与数据组织形式
开发语言·python·信息可视化·数据分析·pyecharts
你怎么知道我是队长1 小时前
C语言---编译的最小单位---令牌(Token)
java·c语言·前端
ReedFoley1 小时前
【笔记】动手学Ollama 第七章 应用案例 Agent应用
笔记
quaer2 小时前
print(2 ** 3)
开发语言·python
Tipriest_2 小时前
C++ csignal库详细使用介绍
开发语言·c++·csignal·信号与异常
kyle~3 小时前
C++---多态(一个接口多种实现)
java·开发语言·c++
芜青3 小时前
JavaScript手录18-ajax:异步请求与项目上线部署
开发语言·javascript·ajax
一个会的不多的人3 小时前
C# NX二次开发:面收集器控件和曲线收集器控件详解
开发语言·c#
Freak嵌入式3 小时前
一文速通 Python 并行计算:教程总结
开发语言·python
shuououo4 小时前
集成算法学习笔记
笔记·学习·算法