C 语言高频考点总结:原码 / 结构体 / 内存管理等 10 大核心知识点
引言
C 语言作为编程入门和底层开发的核心语言,其基础概念(如内存表示、数据类型、内存管理)是面试和笔试的高频考点。本文整理了 10 个最核心的 C 语言知识点,涵盖原码反码补码、结构体与联合体、内存泄露等关键内容,适合初学者夯实基础或开发者复习回顾,每个知识点均以 “概念 + 示例 + 对比” 的形式呈现,通俗易懂。
目录
- 原码、反码、补码的规则
- 结构体(struct)和联合体(union)的区别
- C 语言中数据类型的分类
- 数组的三大特性
- 关系运算符有哪些?
- extern 和 static 的区别
- const 的用法(含指针修饰场景)
- 动态开辟内存不在堆区的函数:alloca
- 内存泄露的常见原因
- 如何防止头文件重复包含
一、原码、反码、补码的规则
计算机通过原码、反码、补码表示带正负的整数,核心目的是简化加减运算(尤其是负数运算),三者的规则差异如下:
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) - 优势:
- 0 仅有一种表示:
0000 0000(无 + 0/-0 之分); - 减法转加法:
a - b = a + (-b的补码),无需单独设计减法电路,硬件简化。
- 0 仅有一种表示:
二、结构体(struct)和联合体(union)的区别
结构体和联合体均用于 “打包数据”,但核心逻辑完全不同,可通过 “组合柜” 与 “共享抽屉柜” 的比喻理解:
1. 核心定义对比
| 特性 | 结构体(struct) | 联合体(union) |
|---|---|---|
| 内存分配 | 分块存储,每个成员有独立内存 | 共享内存,所有成员共用一块空间 |
| 整体大小 | 所有成员内存之和(含内存对齐) | 等于最大成员的内存大小 |
| 访问方式 | 成员可同时存在、同时访问 | 同一时间仅能使用一个成员(会覆盖) |
| 核心逻辑 | “共存”:所有成员同时有效 | “互斥”:仅一个成员当前有效 |
2. 典型用途
- 结构体:适合管理 “需同时生效” 的关联数据,如学生信息(学号、姓名、成绩):
// 结构体示例:学生信息 struct Student { int id; // 学号(独立内存) char name[20];// 姓名(独立内存) float score; // 成绩(独立内存) }; - 联合体:适合 “节省内存” 或 “多方式解读同一块内存”,如:
- 场景 1:数据不同时使用(如存储 “性别(char)” 或 “年龄(int)”,无需同时存);
- 场景 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 == 5 | 0(假,3 不等于 5) |
!= | 不等于 | 3 != 5 | 1(真,3 不等于 5) |
> | 大于 | 5 > 3 | 1(真,5 大于 3) |
< | 小于 | 5 < 3 | 0(假,5 小于 3) |
>= | 大于等于 | 3 >= 3 | 1(真,3 等于 3) |
<= | 小于等于 | 5 <= 3 | 0(假,5 不小于 3) |
注意事项
- 区分
==(等于)和=(赋值):if (a == 5)是判断,if (a = 5)是赋值(恒为真,易出错); - 关系运算结果可直接赋值给变量:
int res = (3 > 5);(res 值为 0)。
六、extern 和 static 的区别
extern和static均用于修饰变量 / 函数,但作用完全相反:一个 “共享”,一个 “隐藏”。
1. 作用域不同(核心区别)
-
extern:用于 “跨文件共享”,声明 “在其他文件定义的变量 / 函数”,当前文件可使用。示例:
- 在
a.c中定义全局变量:int num = 10;(定义); - 在
b.c中用extern声明:extern int num;(声明,不定义); - 此时
b.c可直接使用num(值为 10)。
- 在
-
static:用于 “限制作用域”,修饰的变量 / 函数仅能在当前文件使用,其他文件无法访问。示例:
- 在
a.c中定义静态全局变量:static int num = 10;; - 在
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.h,b.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.h,b.h又包含a.h的情况(即使加保护符,也可能导致声明不完整)。
总结
本文整理了 C 语言中 10 个核心知识点,涵盖内存表示(原码 / 反码 / 补码)、数据结构(结构体 / 联合体 / 数组)、内存管理(extern/static/const/ 内存泄露)、语法细节(关系运算符 / 头文件保护),均为面试高频考点。
建议初学者结合代码示例实操,加深理解;复习者可重点关注 “结构体与联合体区别”“const 修饰指针”“内存泄露原因” 等易混淆知识点。如有疑问,欢迎在评论区交流!

1723

被折叠的 条评论
为什么被折叠?



