1. 哪些情况会出现野指针?
野指针是指指向无效内存区域的指针,常见成因包括:
- 指针变量未初始化:定义指针时未赋值,其值为随机内存地址,操作此类指针会导致不可预测的错误。
int *p; // 未初始化的野指针 *p = 10; // 危险!操作随机地址
- 指针释放后未置空:使用
free()
释放动态分配的内存后,指针仍指向原地址(已回收),成为野指针。int *p = malloc(sizeof(int)); free(p); // 内存释放后,p未置空 *p = 20; // 危险!操作已释放的内存
- 指针操作超越变量作用域:指向局部变量的指针,在变量生命周期结束后(如函数返回),指针指向的内存已被释放。
int* func() { int a = 10; return &a; // a是局部变量,函数返回后内存释放 } int *p = func(); // p成为野指针
2. 描述内存分区
程序的内存分区可分为运行前和运行后两个阶段:
-
程序运行前(静态分区):
- 代码区(Text Segment):存储程序的机器指令,只读(防止意外修改),可共享(多个进程共享同一份代码)。
- 数据段(Data Segment):存储已初始化的全局变量、静态变量(包括
static
修饰的局部变量),程序结束后由系统释放。 - BSS 段(Block Started by Symbol):存储未初始化的全局变量和静态变量,程序加载时会自动初始化为 0,占用内存空间但不占用可执行文件大小。
-
程序运行后(动态分区):
-
内核空间(Kernel Space)
- 位置:最高地址区域(通常占虚拟地址空间的 1/2 或 1/4)。
- 用途:操作系统内核代码和数据,如进程管理、内存管理、设备驱动等。
- 特性:用户进程无法直接访问,需通过系统调用(如
syscall
)间接交互。
-
栈区(Stack)
- 存储:函数参数、局部变量(非
static
)、返回地址、栈帧信息(函数调用上下文)。 - 特性:
- 编译器自动分配释放(函数调用时创建,返回时销毁)。
- 先进后出(LIFO),从高地址向低地址生长。
- 大小固定(默认几 MB,可通过
ulimit
调整),溢出会导致栈崩溃。
- 存储:函数参数、局部变量(非
-
内存映射区(Memory Mapping Segment,mmap)
- 位置:栈区下方,堆区上方。
- 存储:
- 共享库(如
libc.so
等动态链接库)。 - 内存映射文件(磁盘文件直接映射到内存)。
- 匿名映射(如
malloc
分配大内存时使用)。
- 共享库(如
- 特性:大小动态变化,由
mmap()
系统调用管理,支持进程间共享。
-
堆区(Heap)
- 存储:动态分配的内存(
malloc
/new
申请的空间)。 - 特性:
- 程序员手动管理(需
free
/delete
释放,否则内存泄漏)。 - 从低地址向高地址生长,大小可动态扩展(受限于系统内存)。
- 程序员手动管理(需
- 存储:动态分配的内存(
-
BSS 段(Block Started by Symbol)
- 存储:未初始化的全局变量、未初始化的静态变量(
static
修饰的全局 / 局部变量)。 - 特性:程序加载时自动初始化为 0,不占用可执行文件磁盘空间(仅在内存中分配)。
- 存储:未初始化的全局变量、未初始化的静态变量(
-
数据段(Data Segment)
- 存储:已初始化的全局变量、已初始化的静态变量(
static
修饰的全局 / 局部变量)。 - 特性:可读可写,大小在编译时确定(存储在可执行文件中)。
- 存储:已初始化的全局变量、已初始化的静态变量(
-
代码段(Text Segment)
- 存储:程序的机器指令(二进制可执行代码)。
- 特性:只读(防止意外修改指令)、可共享(多个进程共享同一份代码)。
-
只读数据段(.rodata)
- 存储:字符串常量(如
"hello"
)、const
修饰的全局变量(只读数据)。 - 特性:只读(修改会导致段错误),部分系统将其归为代码段的子区域,或独立划分。
- 存储:字符串常量(如
-
3. 普通局部变量、普通全局变量、静态局部变量、静态全局变量的区别
类型 | 存储区域 | 初始化默认值 | 作用域 | 生命周期 | 访问范围 |
---|---|---|---|---|---|
普通局部变量 | 栈区 | 随机值 | 定义所在的复合语句({} 内) | 复合语句结束后释放 | 仅当前复合语句内 |
普通全局变量 | 数据段 / BSS 段 | 0 | 整个程序 | 程序运行结束后释放 | 所有源文件(需extern 声明) |
静态局部变量(static ) | 数据段 / BSS 段 | 0 | 定义所在的函数 | 程序运行结束后释放 | 仅当前函数内 |
静态全局变量(static ) | 数据段 / BSS 段 | 0 | 整个程序 | 程序运行结束后释放 | 仅当前源文件(限制作用域) |
示例:
int g_var; // 普通全局变量(BSS段,默认0)
static int sg_var = 10; // 静态全局变量(数据段,仅当前文件可见)
void func() {
int l_var; // 普通局部变量(栈区,默认随机值)
static int sl_var; // 静态局部变量(BSS段,默认0,生命周期同程序)
}
4. 指针和指针变量的区别
概念 | 本质 | 关键特征 |
---|---|---|
地址 | 二进制数字(内存单元编号) | 无类型,仅标识位置 |
指针 | 带类型信息的地址 | 包含地址 + 类型(决定访问方式和步长) |
指针变量 | 存储指针的变量 | 有自己的地址和类型(类型为 “指向某类型的指针”) |
- 指针:本质是内存单元的地址+类型(如
&a
的值),是一个数值,代表内存中的某个位置(类比 “门牌号”)。 - 指针变量:专门用于存储指针(地址)的变量(如
int* p
中的p
),是一个容器,其值为指针(类比 “保存门牌号的记事本”)。
示例:
int a = 10;
int* p = &a; // p是指针变量,存储的是a的地址(指针)
5. 指针和地址的区别
- 地址:仅表示内存单元的编号(无类型),是一个纯数值(如
0x7ffd9a5b9a4c
)。 - 指针:由地址和类型组成,不仅记录内存位置,还包含该位置数据的类型信息(决定了指针的 “步长”,即
p++
时移动的字节数)。
示例:
int a = 10;
int* p = &a; // 指针p:地址为&a,类型为int*(步长4字节)
char* q = (char*)&a; // 指针q:地址同样为&a,类型为char*(步长1字节)
6. int *p[5]
和int (*p)[5]
的区别
-
int *p[5]
:指针数组,本质是数组,包含 5 个int*
类型的指针(数组元素为指针)。int a = 1, b = 2, c = 3; int *p[3] = {&a, &b, &c}; // 数组p的每个元素都是int*指针 printf("%d", *p[0]); // 输出1(访问a的值)
-
int (*p)[5]
:数组指针,本质是指针,指向一个包含 5 个int
元素的数组(指针指向数组)。int arr[5] = {1,2,3,4,5}; int (*p)[5] = &arr; // p指向整个数组arr printf("%d", (*p)[0]); // 输出1(访问数组首元素)
关键区别:优先级不同,[]
高于*
,因此int *p[5]
先结合[]
形成数组,int (*p)[5]
通过()
强制*
与p
结合形成指针。
7. 描述int (*p)(int, int)
的含义
p
是函数指针,指向一个返回值为int
、参数为两个int
的函数。函数指针存储函数的入口地址,可通过指针调用函数。
示例:
int add(int a, int b) { return a + b; }
int subtract(int a, int b) { return a - b; }
int main() {
int (*p)(int, int); // 声明函数指针p
p = add; // p指向add函数
printf("%d", p(3, 2)); // 输出5(调用add)
p = subtract;
printf("%d", p(3, 2)); // 输出1(调用subtract)
return 0;
}
8. 描述int* (*p[5])(int, int)
的含义
p
是函数指针数组,本质是数组,包含 5 个函数指针,每个指针指向返回值为int*
、参数为两个int
的函数。
示例:
int* func1(int a, int b) { // 返回int*的函数
static int res;
res = a + b;
return &res;
}
int* func2(int a, int b) {
static int res;
res = a * b;
return &res;
}
int main() {
int* (*p[2])(int, int) = {func1, func2}; // 函数指针数组
printf("%d", *p[0](2, 3)); // 输出5(调用func1)
printf("%d", *p[1](2, 3)); // 输出6(调用func2)
return 0;
}
9. 函数指针变量作为函数参数的意义是什么?
函数指针作为参数,允许将函数逻辑作为数据传递,实现 “回调机制” 和 “行为动态化”,使函数更通用、灵活。
示例:通用排序函数(通过不同比较函数实现升序 / 降序)
// 排序函数(接收函数指针作为比较规则)
void sort(int* arr, int size, int (*cmp)(int, int)) {
for (int i = 0; i < size; i++) {
for (int j = i+1; j < size; j++) {
if (cmp(arr[i], arr[j]) > 0) { // 调用外部比较函数
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
}
}
// 比较函数:升序
int ascending(int a, int b) { return a - b; }
// 比较函数:降序
int descending(int a, int b) { return b - a; }
int main() {
int arr[] = {3,1,2};
sort(arr, 3, ascending); // 按升序排序
sort(arr, 3, descending); // 按降序排序
return 0;
}
10. 指针作为函数返回值时需要注意什么?
严禁返回函数内部局部变量的地址,因为局部变量在函数结束后会被释放,返回的地址将指向无效内存(野指针),操作会导致段错误或不可预测的结果。
错误示例:
int* bad_func() {
int a = 10; // 局部变量,函数结束后释放
return &a; // 错误!返回无效地址
}
正确做法:返回静态变量地址、全局变量地址或动态分配的内存地址(需手动释放)。
int* good_func1() {
static int a = 10; // 静态变量,生命周期同程序
return &a; // 安全
}
int* good_func2() {
int* p = malloc(sizeof(int)); // 动态分配内存
*p = 10;
return p; // 安全,需外部调用free(p)释放
}
11. const int *p
和 int *const p
的区别
-
const int *p
(指向常量的指针):const
修饰指针指向的数据,因此不能通过p
修改指向的数据,但指针的指向可以改变。
int a = 10, b = 20; const int *p = &a; // *p = 30; // 错误!不能修改指向的数据 p = &b; // 正确!可以改变指向
-
int *const p
(常量指针):const
修饰指针本身,因此指针的指向不能改变,但可以通过指针修改指向的数据。
int a = 10, b = 20; int *const p = &a; *p = 30; // 正确!可以修改指向的数据 // p = &b; // 错误!不能改变指向
-
扩展:
const int *const p
(指向常量的常量指针),既不能修改指向的数据,也不能改变指向。
12. #include <>
和 #include ""
的区别
#include <header.h>
:编译器优先到系统默认目录(如/usr/include
)寻找头文件,适用于标准库头文件(如stdio.h
)。#include "header.h"
:编译器优先到当前源文件所在目录寻找头文件,若未找到再到系统目录,适用于自定义头文件。
13. 内存中的最小存储单位及最小计量单位
- 最小存储单位:二进制位(bit),表示 1 个二进制数(0 或 1),是硬件电路能表示的最小单位。
- 最小计量单位:字节(Byte),1 字节 = 8 位,是内存寻址的基本单位(硬件设计和编程中以字节为单位管理内存)。
14. GCC 的编译过程
GCC 将源代码转换为可执行文件分为 4 个阶段:
-
预处理(Preprocessing):
- 处理
#include
:递归展开头文件内容。 - 处理
#define
:替换宏定义(如#define PI 3.14
替换为 3.14)。 - 处理条件编译(
#ifdef
/#ifndef
/#else
/#endif
)。 - 输出:
.i
文件(预处理后的 C 代码)。 - 命令:
gcc -E source.c -o source.i
。
- 处理
-
编译(Compilation):
- 对预处理后的代码进行语法分析、语义分析、类型检查。
- 将 C 代码转换为汇编代码。
- 输出:
.s
文件(汇编代码)。 - 命令:
gcc -S source.i -o source.s
。
-
汇编(Assembly):
- 将汇编代码转换为机器码(二进制指令)。
- 输出:
.o
文件(目标文件,二进制格式)。 - 命令:
gcc -c source.s -o source.o
。
-
链接(Linking):
- 将多个目标文件(
.o
)和库文件(如libc.so
)合并,解析未定义的符号(如printf
)。 - 分配内存地址,生成可执行文件。
- 输出:可执行文件(如
a.out
)。 - 命令:
gcc source.o -o program
。
- 将多个目标文件(
15. 32 位和 64 位系统下各数据类型的字节数
不同系统中数据类型的字节数可能因编译器和架构略有差异,以下为常见情况:
数据类型 | 32 位系统(字节) | 64 位系统(字节) | 关键说明 |
---|---|---|---|
char | 1 | 1 | 固定为 1 字节(ASCII 编码基础) |
short | 2 | 2 | 通常为 2 字节,与系统位数无关 |
int | 4 | 4 | 主流编译器保持 4 字节(兼容历史代码) |
long | 4 | 8 | 64 位系统中扩展为 8 字节(与系统位数一致) |
long long | 8 | 8 | C99 标准后固定为 8 字节 |
float | 4 | 4 | 遵循 IEEE 754 标准(单精度浮点) |
double | 8 | 8 | 遵循 IEEE 754 标准(双精度浮点) |
void* | 4 | 8 | 指针宽度与系统位数一致(32 位 / 64 位地址) |
示例:
#include <stdio.h>
int main() {
printf("char: %zu\n", sizeof(char)); // 1
printf("int: %zu\n", sizeof(int)); // 4(32/64位均如此)
printf("long: %zu\n", sizeof(long)); // 4(32位)/8(64位)
printf("void*: %zu\n", sizeof(void*)); // 4(32位)/8(64位)
return 0;
}
16. 关键字register
register
是存储类型说明符,用于建议编译器将变量存储在 CPU 寄存器中,以提高访问速度(寄存器访问速度远快于内存)。
特性:
- 建议性:编译器可忽略该建议(如寄存器不足或变量不适合存储在寄存器中)。
- 限制:
- 寄存器变量无内存地址,因此不能使用
&
取地址。 - 通常用于频繁访问的变量(如循环计数器)。
- 寄存器变量无内存地址,因此不能使用
- 适用场景:短期、高频使用的变量(如
for
循环中的i
)。
示例:
#include <stdio.h>
int main() {
register int i; // 建议将i存入寄存器(适合循环计数)
int sum = 0;
for (i = 0; i < 1000000; i++) {
sum += i;
}
printf("Sum: %d\n", sum);
// &i; // 错误!寄存器变量无法取地址
return 0;
}
17. sizeof
和strlen
的区别
特性 | sizeof | strlen |
---|---|---|
本质 | 运算符(编译期计算) | 库函数(运行期计算) |
作用对象 | 所有数据类型(int、数组、结构体等) | 仅以'\0' 结尾的字符串 |
计算内容 | 变量 / 类型占用的总内存字节数 | 字符串中'\0' 前的字符个数 |
是否包含终止符 | 是(如字符串数组包含'\0' ) | 否(仅计算有效字符) |
求值时机 | 编译期(结果为常量) | 运行期(遍历字符串) |
头文件依赖 | 无 | 需要<string.h> |
安全性 | 安全(无越界风险) | 若字符串无'\0' ,会导致未定义行为 |
示例:
#include <stdio.h>
#include <string.h>
int main() {
char arr1[] = "hello"; // 存储:'h','e','l','l','o','\0'(共6字节)
char arr2[] = {'h','e','l','l','o'}; // 无'\0'(共5字节)
char* ptr = arr1;
// sizeof示例
printf("sizeof(arr1): %zu\n", sizeof(arr1)); // 6(含'\0')
printf("sizeof(arr2): %zu\n", sizeof(arr2)); // 5(实际长度)
printf("sizeof(ptr): %zu\n", sizeof(ptr)); // 8(64位指针大小)
// strlen示例
printf("strlen(arr1): %zu\n", strlen(arr1)); // 5(不含'\0')
// printf("strlen(arr2): %zu\n", strlen(arr2)); // 危险!无'\0',结果随机
return 0;
}
18. 逻辑右移和算术右移的区别
右移操作将二进制位向右移动,两者的差异体现在高位填充规则上:
- 逻辑右移:
- 右侧溢出的位丢弃,左侧空缺补
0
。 - 适用于无符号数(视为纯二进制数)。
- 右侧溢出的位丢弃,左侧空缺补
- 算术右移:
- 右侧溢出的位丢弃,左侧空缺补符号位(正数补
0
,负数补1
)。 - 适用于有符号数(保持数值的符号不变)。
- 右侧溢出的位丢弃,左侧空缺补符号位(正数补
示例(8 位二进制):
// 正数:0000 1010(十进制10)
// 逻辑右移1位 → 0000 0101(5)
// 算术右移1位 → 0000 0101(5)(结果相同)
// 负数:1111 0110(十进制-10,补码表示)
// 逻辑右移1位 → 0111 1011(123,符号改变)
// 算术右移1位 → 1111 1011(-5,符号不变)
注意:C 语言中,无符号数默认逻辑右移,有符号数的右移方式由编译器决定(通常为算术右移)。
19. 数组名作为类型、地址及对数组名取地址的区别
数组名的含义随上下文变化,核心区别如下:
-
数组名作为类型:
- 用
sizeof(数组名)
时,数组名代表整个数组,返回数组总字节数。 - 示例:
int arr[5]; sizeof(arr)
→ 20(5×4 字节)。
- 用
-
数组名作为地址:
- 除
sizeof
和取地址(&
)外,数组名代表首元素的地址。 - 对数组名
+1
,偏移量为一个元素的大小。 - 示例:
arr + 1
→ 指向arr[1]
(偏移 4 字节,int
类型)。
- 除
-
对数组名取地址(
&数组名
):- 结果是整个数组的地址(类型为 “指向数组的指针”)。
- 对
&数组名 + 1
,偏移量为整个数组的大小。 - 示例:
&arr + 1
→ 指向数组末尾后一位(偏移 20 字节,5 个int
)。
示例:
#include <stdio.h>
int main() {
int arr[3] = {1, 2, 3};
printf("arr: %p\n", arr); // 首元素地址(如0x7ffd...a0)
printf("arr + 1: %p\n", arr + 1); // 首元素+1(偏移4字节,0x7ffd...a4)
printf("&arr: %p\n", &arr); // 整个数组的地址(与arr数值相同,0x7ffd...a0)
printf("&arr + 1: %p\n", &arr + 1); // 整个数组+1(偏移12字节,0x7ffd...aC)
return 0;
}
20. 为什么全局数组未初始化默认为 0,而局部数组未初始化默认是随机值?
差异源于存储区域的特性:
数组类型 | 存储区域 | 初始化规则 | 原因分析 |
---|---|---|---|
未初始化全局数组 | BSS 段 | 自动初始化为 0 | BSS 段在程序加载时由操作系统统一清零,优化空间且保证安全性(避免随机值导致的不可预测行为)。 |
未初始化局部数组 | 栈区 | 默认为随机值 | 栈区用于临时数据(函数参数、局部变量),分配 / 释放频繁。若每次清零会增加开销,因此编译器不自动初始化,保留栈中原有残留值。 |
静态局部数组 | 数据段 / BSS 段 | 未初始化时为 0 | 静态变量生命周期与程序一致,存储在静态区域,遵循 BSS 段清零规则。 |
动态分配数组 | 堆区 | 未初始化时为未定义值 | 堆区由程序员手动管理,malloc 不自动清零(calloc 会清零)。 |
21. 二维数组int arr[3][4]
的sizeof
计算
二维数组可视为 “数组的数组”,sizeof
计算遵循以下规则:
sizeof(arr)
:整个二维数组的总字节数 →3×4×4 = 48
字节(int
占 4 字节)。sizeof(arr[0])
:第一行一维数组的字节数 →4×4 = 16
字节。sizeof(arr[0][0])
:单个元素的字节数 → 4 字节。
因此:
sizeof(arr) / sizeof(arr[0])
→48 / 16 = 3
(行数)。sizeof(arr[0]) / sizeof(arr[0][0])
→16 / 4 = 4
(列数)。sizeof(arr) / sizeof(arr[0][0])
→48 / 4 = 12
(总元素数)。
22. 二维数组的初始化方式
二维数组初始化有多种方式,灵活适用于不同场景:
-
按行初始化:用大括号分组,每组对应一行。
int arr1[2][3] = { {1, 2, 3}, {4, 5, 6} };
-
连续初始化:不分组,按顺序填充所有元素(按行优先)。
int arr2[2][3] = {1, 2, 3, 4, 5, 6}; // 等价于arr1
-
部分初始化:未显式初始化的元素自动为 0。
int arr3[2][3] = { {1, 2}, {4} }; // 结果:{1,2,0}, {4,0,0}
-
省略第一维:编译器可根据初始化内容推断行数。
int arr4[][3] = { {1,2,3}, {4,5,6} }; // 自动推断为2行
-
指定下标初始化(C99):直接指定元素的行和列下标。
int arr5[2][3] = { [0][1] = 20, [1][2] = 60 }; // 结果:{0,20,0}, {0,0,60}
23. char arr1[] = {'h','e','l','l','o'};
与char arr2[] = "hello";
的区别
特性 | arr1 (逐个初始化) | arr2 (字符串初始化) |
---|---|---|
元素组成 | 'h','e','l','l','o' (共 5 个元素) | 'h','e','l','l','o','\0' (共 6 个元素) |
终止符 | 无'\0' | 自动添加'\0' (字符串结束标志) |
字符串函数兼容性 | 不兼容(如strlen 会越界) | 兼容(可安全使用strlen 、printf("%s") 等) |
示例:
#include <stdio.h>
#include <string.h>
int main() {
char arr1[] = {'h','e','l','l','o'};
char arr2[] = "hello";
printf("arr1长度(strlen):%zu\n", strlen(arr1)); // 随机值(无'\0')
printf("arr2长度(strlen):%zu\n", strlen(arr2)); // 5(正确)
printf("arr1输出(%s):", arr1); // 乱码(直到遇到随机的'\0')
printf("arr2输出(%s):%s\n", arr2); // hello(正确)
return 0;
}
24. 字符数组元素的遍历方式
字符数组遍历主要有两种方式,适用于不同场景:
-
逐个遍历(
%c
):- 适用于所有字符数组(无论是否含
'\0'
)。 - 通过下标或指针访问每个元素。
char arr[] = {'h','e','l','l','o'}; for (int i = 0; i < 5; i++) { printf("%c", arr[i]); // 输出:hello }
- 适用于所有字符数组(无论是否含
-
整体输出(
%s
):- 仅适用于以
'\0'
结尾的字符数组(字符串)。 - 从首元素开始,直到
'\0'
停止。
char str[] = "hello"; printf("%s\n", str); // 输出:hello(自动识别'\0')
- 仅适用于以
注意:若字符数组无'\0'
,用%s
输出会导致越界(读取到随机内存)。
25. gets
和fgets
获取字符串的区别
gets
因安全问题已被弃用,fgets
是更安全的替代方案:
特性 | gets | fgets |
---|---|---|
安全性 | 极度不安全:不限制输入长度,可能导致缓冲区溢出(黑客可利用此漏洞)。 | 相对安全:通过参数指定最大读取长度,超出则截断。 |
输入来源 | 仅能从标准输入(stdin )读取。 | 可从任意文件流(stdin 、文件等)读取。 |
终止符处理 | 自动丢弃输入末尾的换行符'\n' 。 | 保留输入末尾的换行符'\n' (若读取到)。 |
函数原型 | char* gets(char* buf); | char* fgets(char* buf, int size, FILE* stream); |
现代支持 | C11 标准中移除,多数编译器报错。 | 标准库函数,广泛支持。 |
示例:
#include <stdio.h>
int main() {
char buf[10];
// fgets使用示例(安全)
printf("输入字符串:");
fgets(buf, sizeof(buf), stdin); // 最多读取9个字符(留1位给'\0')
printf("读取结果:%s", buf); // 若输入过长,会截断并保留'\n'
return 0;
}
26. sizeof
和strlen
的补充对比
(结合更多场景的示例):
场景 | sizeof 结果 | strlen 结果 |
---|---|---|
字符数组char arr[10] | 10(数组总大小,与内容无关) | 未初始化时为随机值(无'\0' );初始化后为'\0' 前的字符数。 |
指针char* p = "abc" | 8(64 位指针大小) | 3(字符串"abc" 的长度) |
结构体struct {int a; char b;} | 8(考虑对齐填充,int 占 4,char 占 1,填充 3 字节) | 不适用(strlen 仅处理字符串) |
总结:sizeof
关注 “内存占用”,strlen
关注 “字符串有效长度”。
27. 使用realloc
追加堆区空间的注意事项
realloc
用于调整动态分配内存的大小,需注意以下问题:
-
返回值处理:
realloc
可能返回新地址(内存重新分配)或原地址(空间足够时),必须用变量保存返回值(原指针可能失效)。
int* p = malloc(10 * sizeof(int)); int* new_p = realloc(p, 20 * sizeof(int)); // 调整为20个int if (new_p != NULL) { // 成功:更新指针 p = new_p; } else { // 失败:保留原指针,避免内存泄漏 // 处理错误(如内存不足) }
-
参数含义:
- 第二个参数是新空间的总大小(非追加大小)。例如,原空间 10 字节,需追加 10 字节,则新大小为 20 字节。
-
原数据保留:
- 若重新分配成功,原数据会被复制到新空间(前
min(原大小, 新大小)
字节)。
- 若重新分配成功,原数据会被复制到新空间(前
-
失败处理:
- 若
realloc
失败,返回NULL
,但原内存块仍有效(需避免因未检查返回值而丢失原指针)。
- 若
28. 宏函数和普通函数的区别
宏函数(#define
定义)与普通函数在编译、调用等方面差异显著:
特性 | 宏函数(#define ) | 普通函数 |
---|---|---|
本质 | 编译期文本替换(无函数调用过程)。 | 编译后生成机器指令,通过函数调用执行。 |
类型检查 | 无类型检查:参数可任意类型,可能导致隐蔽错误。 | 严格的类型检查:参数类型不匹配时编译报错。 |
代码体积 | 每次调用都会复制代码,可能增大二进制文件大小(代码膨胀)。 | 代码只存在一份,调用时跳转执行,体积较小。 |
执行效率 | 无调用开销(替换后直接执行)。 | 有调用开销(栈帧创建、参数传递等)。 |
调试 | 宏替换后才进入编译,调试时无法单步执行宏函数。 | 可在调试器中设置断点,单步执行。 |
适用场景 | 简单、高频调用的代码(如求最大值#define MAX(a,b) ((a)>(b)?(a):(b)) )。 | 复杂逻辑、多语句实现的功能。 |
示例:
// 宏函数
#define ADD(a, b) ((a) + (b))
// 普通函数
int add(int a, int b) {
return a + b;
}
int main() {
int x = 3, y = 4;
printf("%d\n", ADD(x, y)); // 编译时替换为((3)+(4)) → 7
printf("%d\n", add(x, y)); // 函数调用 → 7
return 0;
}
29. 结构体对齐规则
结构体成员的内存布局需遵循对齐规则(提高 CPU 访问效率):
-
基本对齐值:
- 每个类型的默认对齐值为其大小(如
char
=1,int
=4,double
=8)。 - 编译器可能有最大对齐值(如 8 字节),超过则按最大值对齐。
- 每个类型的默认对齐值为其大小(如
-
成员对齐规则:
- 每个成员的起始地址必须是其对齐值的整数倍。
- 若前一成员结束后空间不足,插入填充字节(
Padding
)。
-
结构体整体对齐:
- 总大小必须是所有成员中最大对齐值的整数倍(不足则在末尾填充)。
-
嵌套结构体:
- 嵌套结构体的对齐值为其内部最大成员的对齐值。
示例:
struct Example {
char a; // 对齐值1(偏移0)
int b; // 对齐值4(需偏移至4,填充3字节)
char c; // 对齐值1(偏移8)
}; // 总大小:9 → 需对齐至最大对齐值4的倍数 → 12(末尾填充3字节)
常用__attribute__
对齐属性:
__attribute__((aligned(n)))
:强制按n
字节对齐(n
为 2 的幂)。struct Aligned { int x; } __attribute__((aligned(8))); // 总大小强制为8字节(即使int仅占4字节)
__attribute__((packed))
:取消填充,紧密排列(可能降低访问速度,但节省内存)。
struct Packed {
char a;
int b;
} __attribute__((packed)); // 总大小5字节(无填充)