C语言每日十题

C 语言高频考点总结:原码 / 结构体 / 内存管理等 10 大核心知识点

引言

C 语言作为编程入门和底层开发的核心语言,其基础概念(如内存表示、数据类型、内存管理)是面试和笔试的高频考点。本文整理了 10 个最核心的 C 语言知识点,涵盖原码反码补码、结构体与联合体、内存泄露等关键内容,适合初学者夯实基础或开发者复习回顾,每个知识点均以 “概念 + 示例 + 对比” 的形式呈现,通俗易懂。

目录

  1. 原码、反码、补码的规则
  2. 结构体(struct)和联合体(union)的区别
  3. C 语言中数据类型的分类
  4. 数组的三大特性
  5. 关系运算符有哪些?
  6. extern 和 static 的区别
  7. const 的用法(含指针修饰场景)
  8. 动态开辟内存不在堆区的函数:alloca
  9. 内存泄露的常见原因
  10. 如何防止头文件重复包含

一、原码、反码、补码的规则

计算机通过原码、反码、补码表示带正负的整数,核心目的是简化加减运算(尤其是负数运算),三者的规则差异如下:

1. 原码:最直观的表示方式

  • 规则:最高位为符号位(0 = 正数,1 = 负数),剩余位表示数值的二进制本身。
  • 示例(8 位二进制)5(原码):0000 0101-5(原码):1000 0101
  • 问题:0 有两种表示(+0:0000 0000-0:1000 0000),且减法需单独处理符号位,硬件实现复杂。

2. 反码:过渡形式

  • 规则
    • 正数反码 = 原码(完全一致);
    • 负数反码 = 符号位不变,其余位 “0 变 1、1 变 0”(按位取反)。
  • 示例(8 位二进制)5(反码):0000 0101(与原码相同)-5(反码):1111 1010(原码 1000 0101 取反,符号位不变)
  • 问题:仍未解决 “0 有两种表示” 的问题,实际开发中极少直接使用。

3. 补码:计算机实际使用的表示

  • 规则
    • 正数补码 = 原码 = 反码;
    • 负数补码 = 其反码 + 1(末尾加 1,若有进位则进位)。
  • 示例(8 位二进制)5(补码):0000 0101-5(补码):1111 1011(反码 1111 1010 + 1)
  • 优势
    1. 0 仅有一种表示:0000 0000(无 + 0/-0 之分);
    2. 减法转加法:a - b = a + (-b的补码),无需单独设计减法电路,硬件简化。

二、结构体(struct)和联合体(union)的区别

结构体和联合体均用于 “打包数据”,但核心逻辑完全不同,可通过 “组合柜” 与 “共享抽屉柜” 的比喻理解:

1. 核心定义对比

特性结构体(struct)联合体(union)
内存分配分块存储,每个成员有独立内存共享内存,所有成员共用一块空间
整体大小所有成员内存之和(含内存对齐)等于最大成员的内存大小
访问方式成员可同时存在、同时访问同一时间仅能使用一个成员(会覆盖)
核心逻辑“共存”:所有成员同时有效“互斥”:仅一个成员当前有效

2. 典型用途

  • 结构体:适合管理 “需同时生效” 的关联数据,如学生信息(学号、姓名、成绩):
    // 结构体示例:学生信息
    struct Student {
        int id;       // 学号(独立内存)
        char name[20];// 姓名(独立内存)
        float score;  // 成绩(独立内存)
    };
    
  • 联合体:适合 “节省内存” 或 “多方式解读同一块内存”,如:
    1. 场景 1:数据不同时使用(如存储 “性别(char)” 或 “年龄(int)”,无需同时存);
    2. 场景 2:解读同一块二进制(如将 4 字节内存同时视为 “int” 或 “float”):
    // 联合体示例:多方式解读同一块内存
    union Data {
        int num;    // 整数形式
        float fnum; // 浮点形式
    };
    // 使用:num和fnum共用4字节内存
    union Data d;
    d.num = 10;      // 此时fnum值为随机(被覆盖)
    d.fnum = 3.14f;  // 此时num值被覆盖
    

三、C 语言中数据类型的分类

C 语言数据类型分为 4 大类,覆盖所有变量 / 数据的存储需求:

1. 基本类型(不可拆分,最基础)

  • 整数类型:存储整数,按内存大小区分:
    • int:最常用(4 字节,如int age = 18;);
    • short:短整型(2 字节,存较小整数);
    • long:长整型(4/8 字节,存较大整数)。
  • 字符类型char(1 字节,存储单个字符,本质是 ASCII 码整数):
    char c1 = 'a';  // 对应ASCII码97
    char c2 = '+';  // 对应ASCII码43
    
  • 浮点类型:存储带小数点的数,按精度区分:
    • float:单精度浮点(4 字节,精度较低);
    • double:双精度浮点(8 字节,精度更高,推荐优先用)。

2. 构造类型(复合类型,由基本类型组合而成)

  • 数组:同类型数据的连续集合(如存储 5 个学生成绩):
    int scores[5] = {90, 85, 95, 88, 92}; // 5个int类型,连续存储
    
  • 结构体(struct):不同类型数据的组合(见第二部分);
  • 联合体(union):共享内存的数据组合(见第二部分);
  • 枚举(enum):定义有名字的常量(如性别、状态):
    enum Gender { MALE, FEMALE }; // MALE=0,FEMALE=1(默认)
    enum Gender g = MALE;
    

3. 指针类型(存储内存地址)

专门用于存储变量 / 函数的内存地址,格式为类型* 变量名,如:

int a = 10;
int* p = &a;  // p存储a的内存地址(指针类型)

4. 空类型(void)

表示 “无具体类型”,仅用于 3 种场景:

  • 函数无返回值:void func();
  • 函数无参数:void func(void);
  • 通用指针:void* p;(可指向任意类型,需强转后使用)。

四、数组的三大特性

数组是 C 语言中最基础的 “批量存储” 结构,核心特性有 3 个,决定了其使用场景:

1. 元素类型必须相同

数组内所有元素的类型一致,不能混合存储。例如:

  • 正确:int arr[3] = {1, 2, 3};(全为 int);
  • 错误:int arr[3] = {1, 'a', 3.14};(混合 int、char、float)。

2. 内存连续存储

数组的所有元素在内存中 “紧挨着存放”,无间隙。例如int arr[3] = {1,2,3}的内存布局:

  • 假设 arr 首地址为 0x100,则:arr[0](1)存于 0x100~0x103,arr[1](2)存于 0x104~0x107,arr[2](3)存于 0x108~0x10B。
  • 优势:通过 “首地址 + 索引” 快速定位元素(arr[i] = 首地址 + i*元素大小)。

3. 长度固定

数组定义时必须确定长度,且后续无法修改(不能动态增删元素)。例如:

  • 定义时指定长度:int arr[5];(长度 5,固定);
  • 错误操作:定义后试图 “扩容” 为arr[6](编译报错)。
  • 若需动态长度,需用malloc等函数手动分配内存。

五、关系运算符有哪些?

关系运算符用于 “比较两个值的关系”,结果仅为1(真)或0(假),共 6 个:

运算符名称作用示例结果说明
==等于3 == 50(假,3 不等于 5)
!=不等于3 != 51(真,3 不等于 5)
>大于5 > 31(真,5 大于 3)
<小于5 < 30(假,5 小于 3)
>=大于等于3 >= 31(真,3 等于 3)
<=小于等于5 <= 30(假,5 不小于 3)

注意事项

  • 区分==(等于)和=(赋值):if (a == 5)是判断,if (a = 5)是赋值(恒为真,易出错);
  • 关系运算结果可直接赋值给变量:int res = (3 > 5);(res 值为 0)。

六、extern 和 static 的区别

externstatic均用于修饰变量 / 函数,但作用完全相反:一个 “共享”,一个 “隐藏”。

1. 作用域不同(核心区别)

  • extern:用于 “跨文件共享”,声明 “在其他文件定义的变量 / 函数”,当前文件可使用。示例:

    1. a.c中定义全局变量:int num = 10;(定义);
    2. b.c中用extern声明:extern int num;(声明,不定义);
    3. 此时b.c可直接使用num(值为 10)。
  • static:用于 “限制作用域”,修饰的变量 / 函数仅能在当前文件使用,其他文件无法访问。示例:

    1. a.c中定义静态全局变量:static int num = 10;
    2. b.c中即使写extern int num;,也无法访问num(编译报错)。
  • 补充:static修饰局部变量时,作用域仍在函数内,但生命周期变为 “整个程序运行期间”(普通局部变量函数结束后销毁):

    void func() {
        static int count = 0; // 仅第一次调用初始化,后续保留值
        count++;
        printf("count: %d\n", count);
    }
    // 调用两次:第一次输出1,第二次输出2(普通局部变量会输出1、1)
    func();
    func();
    

2. 内存分配不同

  • extern:仅 “声明”,不分配内存(内存由定义处分配);
  • static:既 “声明” 又 “分配内存”(全局 static 编译时分配,局部 static 第一次调用时分配)。

七、const 的用法

const的核心作用是 “限制变量 / 指针的修改”,提升代码安全性和可维护性,主要有 4 种用法:

1. 修饰普通变量:变为 “常量”

  • 规则:定义时必须初始化,后续无法修改。
  • 示例:
    const int a = 10; // 正确:初始化
    a = 20;           // 错误:试图修改const变量(编译报错)
    
  • 优势:防止代码中 “误修改” 关键值(如 π、数组长度)。

2. 修饰指针:分两种场景(重点)

const修饰指针时,位置不同,限制对象不同:

  • 场景 1:const 修饰指针指向的内容(内容不可改)格式:const 类型* 指针名 或 类型 const* 指针名(两者等价)。示例:

    int a = 10, b = 20;
    const int* p = &a; // p指向的内容(a)不可改
    *p = 30;           // 错误:不能修改指向的内容
    p = &b;            // 正确:可以修改指针指向的地址
    
  • 场景 2:const 修饰指针本身(指针不可改)格式:类型* const 指针名。示例:

    int a = 10, b = 20;
    int* const p = &a; // 指针p本身不可改
    p = &b;            // 错误:不能修改指针指向的地址
    *p = 30;           // 正确:可以修改指向的内容(a变为30)
    
  • 场景 3:两者都修饰(内容和指针均不可改)格式:const 类型* const 指针名。示例:

    const int* const p = &a;
    *p = 30; // 错误:内容不可改
    p = &b;  // 错误:指针不可改
    

3. 修饰函数参数

  • 规则:限制函数内部不能修改参数的值(或参数指向的内容)。
  • 示例 1:修饰普通参数(防止修改参数值):
    void func(const int x) {
        x = 10; // 错误:不能修改const参数
    }
    
  • 示例 2:修饰指针参数(防止修改指向的内容):
    void print(const int* p) {
        *p = 20; // 错误:不能修改p指向的内容
        printf("%d", *p); // 正确:仅读取
    }
    

4. 与 #define 的区别(扩展)

特性const#define
类型检查有(编译器会检查类型)无(仅文本替换,易出错)
作用域局部 / 全局(按定义位置)全局(从定义处到文件结束)
推荐度更高(安全性强)较低(仅用于简单宏)

八、动态开辟内存不在堆区的函数:alloca

通常动态内存分配(malloc/calloc/realloc)均在堆区,但alloca例外,它在栈区分配内存。

1. 核心特性

  • 分配区域:栈区(而非堆区);
  • 分配时机:运行时确定分配大小(符合 “动态” 特性);
  • 释放方式:无需手动free,函数执行结束后,栈帧销毁时自动回收;
  • 示例
    #include <alloca.h> // 需包含头文件(GCC支持)
    void func(int size) {
        // 动态分配size字节内存(栈区)
        int* p = (int*)alloca(size * sizeof(int));
        // 使用p...(无需free)
    } // 函数结束,p指向的内存自动回收
    

2. 注意事项

  • 非标准库函数alloca不是 C 标准库函数,属于编译器扩展(如 GCC、VS 支持,小众编译器可能不支持);
  • 可移植性差:因依赖编译器,跨平台项目(如 Windows→Linux)不推荐使用;
  • 栈溢出风险:栈区内存大小有限(通常几 MB),若alloca分配过大内存,易触发栈溢出。

九、内存泄露的原因

内存泄露是指 “动态分配的内存失去释放机会,导致系统无法回收”,常见原因有 6 种:

1. 未释放动态分配的内存(最常见)

使用malloc/calloc/realloc分配堆内存后,未调用free释放,内存一直被占用(直到程序退出)。示例(错误):

void func() {
    int* p = (int*)malloc(sizeof(int)); // 分配堆内存
    p = NULL; // 错误:p指向NULL,原内存地址丢失,无法free
} // 函数结束,内存泄露

2. 误用释放函数

  • 重复释放:同一块内存调用多次free(如free(p); free(p););
  • 释放非堆内存:释放栈变量(如int a; free(&a);)或未初始化的指针(如int* p; free(p););
  • 后果:触发内存管理混乱,可能导致程序崩溃或部分内存无法回收。

3. 引用计数错误

在手动管理引用计数的场景(如资源池),计数统计不准确:

  • 多增加一次引用(如 “引用 + 1” 后未 “引用 - 1”),导致计数始终不为 0,内存无法释放;
  • 少增加一次引用(如 “引用 - 1” 后未 “引用 + 1”),导致内存提前释放,引发野指针。

4. 循环引用

两个或多个对象互相持有对方的指针,且无外部引用指向它们,引用计数无法减到 0。示例:

struct A { struct B* b; };
struct B { struct A* a; };
// 循环引用:a指向b,b指向a
struct A* a = (struct A*)malloc(sizeof(struct A));
struct B* b = (struct B*)malloc(sizeof(struct B));
a->b = b;
b->a = a;
// 此时free(a)和free(b)均无法彻底释放(互相引用)

5. 全局变量持有内存

全局变量的生命周期与程序一致,若其持有动态内存且未在程序退出前释放,会导致泄露(长期运行的服务程序影响更严重)。示例(错误):

int* g_p = NULL;
void init() {
    g_p = (int*)malloc(sizeof(int)); // 全局变量持有内存
}
// 程序退出前未调用free(g_p),内存泄露

6. 异常 / 函数提前返回导致释放跳过

  • C 语言中:函数提前return,跳过free操作;
  • 带异常的语言(如 C++):抛出异常后,free未被执行。示例(错误):
void func() {
    int* p = (int*)malloc(sizeof(int));
    if (p == NULL) {
        return; // 错误:提前返回,跳过free(p)
    }
    // 使用p...
    free(p); // 若进入if分支,此句不执行
}

十、如何防止头文件重复包含

头文件重复包含(如a.h包含b.hb.h又包含a.h)会导致编译错误(如 “重复定义”),3 种解决方法:

1. 头文件保护符(最通用,跨编译器)

使用#ifndef#define#endif组合,确保头文件仅被包含一次。格式(以student.h为例):

#ifndef _STUDENT_H_  // 若_STUDENT_H_未定义
#define _STUDENT_H_  // 定义_STUDENT_H_

// 头文件内容(如结构体声明、函数声明)
struct Student {
    int id;
    char name[20];
};
void printStudent(struct Student s);

#endif // _STUDENT_H_  // 结束条件
  • 原理:第一次包含时,_STUDENT_H_未定义,执行内容并定义宏;后续包含时,宏已定义,直接跳过内容。

2. #pragma once(最简洁,编译器支持)

在头文件最开头加#pragma once,直接告诉编译器 “此头文件仅包含一次”。格式:

#pragma once  // 核心指令

// 头文件内容
struct Teacher {
    int id;
    char name[20];
};
  • 优势:代码简洁,无需手动定义宏;
  • 注意:非 C 标准指令,但主流编译器(GCC、VS、Clang)均支持,小众编译器可能不兼容。

3. 合理设计头文件(从源头减少重复)

  • 优先在.c 文件包含头文件:若头文件仅在某个.c文件中使用,不要在其他.h中包含;
  • 使用前置声明:若仅需 “声明”(无需 “定义”),用前置声明替代头文件包含。例如:无需包含teacher.h,直接声明struct Teacher;,避免循环包含:
    // 前置声明:无需包含teacher.h
    struct Teacher; 
    // 使用声明的结构体(如作为函数参数)
    void func(struct Teacher* t);
    
  • 避免循环包含:禁止a.h包含b.hb.h又包含a.h的情况(即使加保护符,也可能导致声明不完整)。

总结

本文整理了 C 语言中 10 个核心知识点,涵盖内存表示(原码 / 反码 / 补码)、数据结构(结构体 / 联合体 / 数组)、内存管理(extern/static/const/ 内存泄露)、语法细节(关系运算符 / 头文件保护),均为面试高频考点。

建议初学者结合代码示例实操,加深理解;复习者可重点关注 “结构体与联合体区别”“const 修饰指针”“内存泄露原因” 等易混淆知识点。如有疑问,欢迎在评论区交流!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值