作为 C 语言学习者,想要真正掌握这门语言,不能只停留在语法表层,必须深入理解内存管理、指针操作、自定义类型等核心机制。本文结合实战代码,系统拆解 C 语言关键知识点,帮你打通从基础到进阶的学习脉络。

一、数据在内存中的存储机制

数据的存储是 C 语言的底层基础,直接影响程序的正确性和效率,核心要掌握整形、浮点型的存储规则及大小端字节序。

1.1 整形的存储:原码、反码、补码

计算机中整形数据以补码形式存储,原因是能统一符号位和数值域的处理,同时让加法和减法运算统一(CPU 只有加法器)。

  • 正数:原码、反码、补码完全相同
  • 负数:原码→符号位不变,数值位取反得到反码→反码 + 1 得到补码
#include <stdio.h>
int main() {
    int a = 20;  // 正数:原码=反码=补码=00000000 00000000 00000000 00010100
    int b = -10; // 原码:10000000 00000000 00000000 00001010
                // 反码:11111111 11111111 11111111 11110101
                // 补码:11111111 11111111 11111111 11110110
    printf("a的补码(十六进制):%x\n", a);  // 输出14
    printf("b的补码(十六进制):%x\n", b);  // 输出fffffff6
    return 0;
}

1.2 大小端字节序及判断

大小端是多字节数据在内存中的存储顺序,直接影响跨平台数据交互:

  • 大端模式:数据高位存内存低地址,低位存高地址
  • 小端模式:数据低位存内存低地址,高位存高地址(X86 架构默认)
// 判断当前机器字节序
#include <stdio.h>
int check_sys() {
    union {  // 共用体所有成员共享一块内存
        int i;
        char c;
    } un;
    un.i = 1;
    return un.c;  // 小端返回1,大端返回0
}
int main() {
    printf("当前机器字节序:%s\n", check_sys() ? "小端" : "大端");
    return 0;
}

1.3 浮点型的存储规则

浮点型(float、double)遵循 IEEE 754 标准,存储结构为:符号位S + 指数位E + 有效数字M

  • 32 位 float:1 位 S + 8 位 E + 23 位 M
  • 64 位 double:1 位 S + 11 位 E + 52 位 M
#include <stdio.h>
int main() {
    int n = 9;
    float *pFloat = (float *)&n;
    printf("n的值:%d\n", n);          // 输出9
    printf("*pFloat的值:%f\n", *pFloat); // 输出0.000000(整数9的补码解析为浮点型极小值)
    
    *pFloat = 9.0;
    printf("n的值:%d\n", n);          // 输出1091567616(浮点型9.0的存储二进制解析为整数)
    printf("*pFloat的值:%f\n", *pFloat); // 输出9.000000
    return 0;
}

二、指针进阶:从基础到实战应用

指针是 C 语言的灵魂,掌握字符指针、数组指针、函数指针等进阶用法,能大幅提升代码灵活性。

2.1 字符指针与字符串

字符指针既可以指向单个字符,也可以指向字符串常量(本质是指向字符串首字符地址)。

#include <stdio.h>
int main() {
    // 指向单个字符
    char ch = 'w';
    char *pc = &ch;
    *pc = 'W';
    printf("ch:%c\n", ch);  // 输出W
    
    // 指向字符串常量(注意:字符串常量不可修改,需加const)
    const char *pstr = "hello bit";
    printf("字符串:%s\n", pstr);  // 输出hello bit
    
    // 面试题:字符串常量存储在单独内存区域,多个指针指向同一字符串会共享内存
    const char *str3 = "hello bit";
    const char *str4 = "hello bit";
    char str1[] = "hello bit";
    char str2[] = "hello bit";
    printf("str3 == str4 ? %s\n", str3 == str4 ? "是" : "否");  // 是
    printf("str1 == str2 ? %s\n", str1 == str2 ? "是" : "否");  // 否
    return 0;
}

2.2 数组指针与二维数组传参

数组指针是指向数组的指针,常用于二维数组传参,需注意[]优先级高于*,需加括号保证p先与*结合。

#include <stdio.h>
// 二维数组传参:可直接用数组指针接收
void print_arr(int (*arr)[5], int row, int col) {
    for (int i = 0; i < row; i++) {
        for (int j = 0; j < col; j++) {
            printf("%d ", arr[i][j]);  // arr[i]等价于*(arr+i)
        }
        printf("\n");
    }
}
int main() {
    int arr[3][5] = {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15};
    print_arr(arr, 3, 5);  // 二维数组名表示首行地址,类型为int(*)[5]
    return 0;
}

2.3 函数指针与转移表

函数指针指向函数的地址,可用于实现转移表(如计算器),简化多分支逻辑。

#include <stdio.h>
// 四则运算函数
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int mul(int a, int b) { return a * b; }
int div(int a, int b) { return a / b; }

int main() {
    // 函数指针数组(转移表):存储4个运算函数的地址
    int (*p[5])(int, int) = {0, add, sub, mul, div};
    int input = 1, x, y, ret;
    
    while (input) {
        printf("1:add  2:sub  3:mul  4:div  0:退出\n");
        printf("请选择:");
        scanf("%d", &input);
        
        if (input >= 1 && input <= 4) {
            printf("请输入两个操作数:");
            scanf("%d %d", &x, &y);
            ret = (*p[input])(x, y);  // 通过函数指针调用函数
            printf("结果:%d\n", ret);
        } else if (input != 0) {
            printf("选择错误!\n");
        }
    }
    return 0;
}

三、字符串与内存函数实战

C 语言没有内置字符串类型,需通过库函数操作,核心要掌握字符串函数和内存函数的使用场景及注意事项。

3.1 字符串函数:strlen、strcpy、strcmp

  • strlen:返回字符串中\0前的字符个数(返回值为 size_t,无符号)
  • strcpy:拷贝字符串(源字符串必须以\0结束,目标空间需足够大且可修改)
  • strcmp:比较字符串(按 ASCII 值逐字符比较,返回 > 0、=0、<0 表示大小关系)
#include <stdio.h>
#include <string.h>
int main() {
    // strlen示例
    const char *str1 = "abcdef";
    const char *str2 = "bbb";
    printf("str1长度:%zu\n", strlen(str1));  // 6
    printf("str2长度:%zu\n", strlen(str2));  // 3
    // 注意:size_t无符号,strlen(str2)-strlen(str1)结果为正数
    if (strlen(str2) - strlen(str1) > 0) {
        printf("str2长度大于str1\n");  // 会执行
    }
    
    // strcpy示例
    char dest[20] = {0};
    char src[] = "hello world";
    strcpy(dest, src);
    printf("拷贝后dest:%s\n", dest);  // hello world
    
    // strcmp示例
    int cmp = strcmp("abc", "abd");
    printf("abc与abd比较结果:%d\n", cmp);  // <0(c的ASCII小于d)
    return 0;
}

3.2 内存函数:memcpy、memmove、memset

内存函数直接操作字节,不关心数据类型,适用于任意类型数据处理:

  • memcpy:拷贝 num 字节(源和目标空间重叠时结果未定义)
  • memmove:拷贝 num 字节(支持重叠空间,更安全)
  • memset:将内存块填充为指定值(按字节填充)
#include <stdio.h>
#include <string.h>
int main() {
    // memcpy拷贝数组
    int arr1[10] = {1,2,3,4,5,6,7,8,9,10};
    int arr2[10] = {0};
    memcpy(arr2, arr1, 20);  // 拷贝前5个int(20字节)
    printf("arr2前5个元素:");
    for (int i = 0; i < 5; i++) {
        printf("%d ", arr2[i]);  // 1 2 3 4 5
    }
    printf("\n");
    
    // memmove处理重叠空间
    int arr3[10] = {1,2,3,4,5,6,7,8,9,10};
    memmove(arr3+2, arr3, 16);  // 将前4个元素拷贝到第3-6位
    printf("arr3处理后:");
    for (int i = 0; i < 8; i++) {
        printf("%d ", arr3[i]);  // 1 2 1 2 3 4 7 8
    }
    printf("\n");
    
    // memset填充
    char str[10] = {0};
    memset(str, 'a', 5);  // 前5个字节填充为'a'
    printf("memset填充后:%s\n", str);  // aaaaa
    return 0;
}

四、动态内存管理:避免内存泄漏与错误

动态内存分配(堆区)解决了静态内存大小固定的问题,核心函数包括 malloc、calloc、realloc、free,需严格遵循使用规则。

4.1 动态内存函数使用

  • malloc:申请连续空间,不初始化
  • calloc:申请空间并初始化为 0
  • realloc:调整已申请空间大小(可能开辟新空间并拷贝数据)
  • free:释放动态申请的空间(必须配对使用,避免内存泄漏)
#include <stdio.h>
#include <stdlib.h>
int main() {
    // malloc申请空间
    int *p1 = (int *)malloc(5 * sizeof(int));
    if (p1 == NULL) {  // 必须检查申请是否成功
        perror("malloc failed");
        return 1;
    }
    for (int i = 0; i < 5; i++) {
        p1[i] = i;  // 赋值0-4
    }
    
    // calloc申请并初始化
    int *p2 = (int *)calloc(5, sizeof(int));
    if (p2 == NULL) {
        perror("calloc failed");
        free(p1);  // 避免内存泄漏
        return 1;
    }
    
    // realloc调整空间大小
    int *p3 = (int *)realloc(p1, 10 * sizeof(int));
    if (p3 != NULL) {
        p1 = p3;  // 调整成功,更新指针
        for (int i = 5; i < 10; i++) {
            p1[i] = i;  // 赋值5-9
        }
    }
    
    // 输出结果
    printf("p1调整后:");
    for (int i = 0; i < 10; i++) {
        printf("%d ", p1[i]);  // 0-9
    }
    printf("\n");
    
    // 释放空间(避免野指针)
    free(p1);
    p1 = NULL;
    free(p2);
    p2 = NULL;
    return 0;
}

4.2 常见动态内存错误

  1. 对 NULL 指针解引用(未检查申请结果)
  2. 越界访问动态内存
  3. 释放非动态申请的内存
  4. 多次释放同一块内存
  5. 忘记释放内存(内存泄漏)

五、文件操作:实现数据持久化

文件操作让数据能够存储在磁盘(持久化),核心流程为:打开文件→读写操作→关闭文件。

5.1 文件打开与关闭

使用 fopen 打开文件(返回 FILE * 指针),fclose 关闭文件(必须关闭,否则可能丢失数据):

  • 打开模式:"r"(只读)、"w"(只写)、"a"(追加)、"rb"(二进制读)等
#include <stdio.h>
int main() {
    // 打开文件("w"模式:文件不存在则创建,存在则清空)
    FILE *pf = fopen("test.txt", "w");
    if (pf == NULL) {  // 检查打开是否成功
        perror("fopen failed");
        return 1;
    }
    
    // 写入数据
    fputs("hello file operation\n", pf);
    fprintf(pf, "num: %d, str: %s\n", 100, "C语言");
    
    // 关闭文件
    fclose(pf);
    pf = NULL;  // 避免野指针
    return 0;
}

5.2 文件读写操作

  • 文本读写:fgetc/fputc(字符)、fgets/fputs(行)、fscanf/fprintf(格式化)
  • 二进制读写:fread/fwrite(适用于结构体等复杂数据)
#include <stdio.h>
typedef struct {
    char name[20];
    int age;
} Student;

int main() {
    // 二进制写入结构体
    Student s = {"张三", 20};
    FILE *pf = fopen("student.bin", "wb");
    if (pf == NULL) {
        perror("fopen failed");
        return 1;
    }
    fwrite(&s, sizeof(Student), 1, pf);
    fclose(pf);
    pf = NULL;
    
    // 二进制读取结构体
    Student s2 = {0};
    pf = fopen("student.bin", "rb");
    if (pf == NULL) {
        perror("fopen failed");
        return 1;
    }
    fread(&s2, sizeof(Student), 1, pf);
    printf("姓名:%s,年龄:%d\n", s2.name, s2.age);  // 张三 20
    
    fclose(pf);
    pf = NULL;
    return 0;
}

总结

C 语言的核心魅力在于对底层的掌控力,从内存存储到指针操作,从动态内存到文件处理,每个知识点都环环相扣。掌握这些内容不仅能写出高效、健壮的代码,更能培养底层编程思维。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐