C语言相关面试题

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 个阶段:

  1. 预处理(Preprocessing)

    • 处理#include:递归展开头文件内容。
    • 处理#define:替换宏定义(如#define PI 3.14替换为 3.14)。
    • 处理条件编译(#ifdef/#ifndef/#else/#endif)。
    • 输出:.i文件(预处理后的 C 代码)。
    • 命令:gcc -E source.c -o source.i
  2. 编译(Compilation)

    • 对预处理后的代码进行语法分析、语义分析、类型检查。
    • 将 C 代码转换为汇编代码。
    • 输出:.s文件(汇编代码)。
    • 命令:gcc -S source.i -o source.s
  3. 汇编(Assembly)

    • 将汇编代码转换为机器码(二进制指令)。
    • 输出:.o文件(目标文件,二进制格式)。
    • 命令:gcc -c source.s -o source.o
  4. 链接(Linking)

    • 将多个目标文件(.o)和库文件(如libc.so)合并,解析未定义的符号(如printf)。
    • 分配内存地址,生成可执行文件。
    • 输出:可执行文件(如a.out)。
    • 命令:gcc source.o -o program

15. 32 位和 64 位系统下各数据类型的字节数

不同系统中数据类型的字节数可能因编译器和架构略有差异,以下为常见情况:

数据类型32 位系统(字节)64 位系统(字节)关键说明
char11固定为 1 字节(ASCII 编码基础)
short22通常为 2 字节,与系统位数无关
int44主流编译器保持 4 字节(兼容历史代码)
long4864 位系统中扩展为 8 字节(与系统位数一致)
long long88C99 标准后固定为 8 字节
float44遵循 IEEE 754 标准(单精度浮点)
double88遵循 IEEE 754 标准(双精度浮点)
void*48指针宽度与系统位数一致(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. sizeofstrlen的区别

特性sizeofstrlen
本质运算符(编译期计算)库函数(运行期计算)
作用对象所有数据类型(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. 数组名作为类型、地址及对数组名取地址的区别

数组名的含义随上下文变化,核心区别如下:

  1. 数组名作为类型

    • sizeof(数组名)时,数组名代表整个数组,返回数组总字节数。
    • 示例:int arr[5]; sizeof(arr) → 20(5×4 字节)。
  2. 数组名作为地址

    • sizeof和取地址(&)外,数组名代表首元素的地址
    • 对数组名+1,偏移量为一个元素的大小
    • 示例:arr + 1 → 指向arr[1](偏移 4 字节,int类型)。
  3. 对数组名取地址(&数组名

    • 结果是整个数组的地址(类型为 “指向数组的指针”)。
    • &数组名 + 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 段自动初始化为 0BSS 段在程序加载时由操作系统统一清零,优化空间且保证安全性(避免随机值导致的不可预测行为)。
未初始化局部数组栈区默认为随机值栈区用于临时数据(函数参数、局部变量),分配 / 释放频繁。若每次清零会增加开销,因此编译器不自动初始化,保留栈中原有残留值。
静态局部数组数据段 / 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. 二维数组的初始化方式

二维数组初始化有多种方式,灵活适用于不同场景:

  1. 按行初始化:用大括号分组,每组对应一行。

    int arr1[2][3] = { {1, 2, 3}, {4, 5, 6} };
    
  2. 连续初始化:不分组,按顺序填充所有元素(按行优先)。

    int arr2[2][3] = {1, 2, 3, 4, 5, 6};  // 等价于arr1
    
  3. 部分初始化:未显式初始化的元素自动为 0。

    int arr3[2][3] = { {1, 2}, {4} };  // 结果:{1,2,0}, {4,0,0}
    
  4. 省略第一维:编译器可根据初始化内容推断行数。

    int arr4[][3] = { {1,2,3}, {4,5,6} };  // 自动推断为2行
    
  5. 指定下标初始化(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会越界)兼容(可安全使用strlenprintf("%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. 字符数组元素的遍历方式

字符数组遍历主要有两种方式,适用于不同场景:

  1. 逐个遍历(%c

    • 适用于所有字符数组(无论是否含'\0')。
    • 通过下标或指针访问每个元素。
    char arr[] = {'h','e','l','l','o'};
    for (int i = 0; i < 5; i++) {
        printf("%c", arr[i]);  // 输出:hello
    }
    
  2. 整体输出(%s

    • 仅适用于以'\0'结尾的字符数组(字符串)。
    • 从首元素开始,直到'\0'停止。
    char str[] = "hello";
    printf("%s\n", str);  // 输出:hello(自动识别'\0')
    

注意:若字符数组无'\0',用%s输出会导致越界(读取到随机内存)。

25. getsfgets获取字符串的区别

gets因安全问题已被弃用,fgets是更安全的替代方案:

特性getsfgets
安全性极度不安全:不限制输入长度,可能导致缓冲区溢出(黑客可利用此漏洞)。相对安全:通过参数指定最大读取长度,超出则截断。
输入来源仅能从标准输入(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. sizeofstrlen的补充对比

(结合更多场景的示例):

场景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用于调整动态分配内存的大小,需注意以下问题:

  1. 返回值处理

    • 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 {  // 失败:保留原指针,避免内存泄漏
        // 处理错误(如内存不足)
    }
    
  2. 参数含义

    • 第二个参数是新空间的总大小(非追加大小)。例如,原空间 10 字节,需追加 10 字节,则新大小为 20 字节。
  3. 原数据保留

    • 若重新分配成功,原数据会被复制到新空间(前min(原大小, 新大小)字节)。
  4. 失败处理

    • 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 访问效率):

  1. 基本对齐值

    • 每个类型的默认对齐值为其大小(如char=1,int=4,double=8)。
    • 编译器可能有最大对齐值(如 8 字节),超过则按最大值对齐。
  2. 成员对齐规则

    • 每个成员的起始地址必须是其对齐值的整数倍。
    • 若前一成员结束后空间不足,插入填充字节(Padding)。
  3. 结构体整体对齐

    • 总大小必须是所有成员中最大对齐值的整数倍(不足则在末尾填充)。
  4. 嵌套结构体

    • 嵌套结构体的对齐值为其内部最大成员的对齐值。

示例

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字节(无填充)

https://github.com/0voice

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值