C语言内存布局与变量存储全解析

C语言程序内存布局

面试的重点:

  • 堆区 :用于动态内存分配,由程序员手动管理(malloc/free),向高地址方向增长。

  • 栈区 :用于存储局部变量、函数参数和返回地址,自动管理,向低地址方向增长。

变量的分类

全局变量

所有函数外部定义的变量

cpp 复制代码
#include <stdio.h>

int globalVar = 100;  // 全局变量

void function() {
    printf("在function中,globalVar = %d\n", globalVar);
    globalVar = 200;  // 修改全局变量
}

int main() {
    printf("程序开始,globalVar = %d\n", globalVar);
    
    function();
    
    printf("调用function后,globalVar = %d\n", globalVar);  // 输出200
    
    return 0;
}

局部变量

函数内部或代码块内部定义的变量

默认为自动存储类型(auto),函数调用时创建,函数返回时销毁

如果局部变量与全局变量同名,在函数内部优先使用局部变量

cpp 复制代码
#include <stdio.h>

int x = 30; //全局变量

void function() {
    int x = 10;  // 局部变量,必须要先初始化
    printf("函数内部 x = %d\n", x);
    
    {
        int y = 20;  // 代码块内的局部变量
        printf("代码块内部 y = %d\n", y);
    }
    
    // printf("y = %d\n", y);  // 错误:y在这里不可见
}

int main() {
    int x = 5;  // main函数的局部变量
    printf("main函数内 x = %d\n", x);
    
    function();
    
    printf("调用function后,main函数内 x = %d\n", x);  // x仍然是5
    
    return 0;
}

存储的关键字

auto

cpp 复制代码
void function() {
    auto int x = 10;  // 等同于 int x = 10;
    // ...
}

stastic

1.用于变量
静态局部变量
cpp 复制代码
#include <stdio.h>

void counter() {
    static int count = 0;  // 静态局部变量
    count++;
    printf("函数被调用了 %d 次\n", count);
}

int main() {
    counter();  // 输出:函数被调用了 1 次
    counter();  // 输出:函数被调用了 2 次
    counter();  // 输出:函数被调用了 3 次
    return 0;
}
静态全局变量
cpp 复制代码
//仅限于定义它的源文件内,内部链接,其他源文件不能访问


// file1.c
static int staticGlobalVar = 100;  // 静态全局变量

void function1() {
    printf("staticGlobalVar = %d\n", staticGlobalVar);
}

// file2.c
extern int staticGlobalVar;  // 错误:无法访问file1.c中的静态全局变量

void function2() {
    // 无法访问staticGlobalVar
}
用于函数

当static用于函数时,它将函数的链接属性从外部链接(external linkage)改为内部链接(internal linkage)。

其他源文件无法通过函数声明来访问该函数

cpp 复制代码
// file1.c
static void privateFunction() {  // 静态函数
    printf("这是一个私有函数\n");
}

void publicFunction() {  // 普通函数
    privateFunction();  // 可以在同一文件中调用
}

// file2.c
extern void privateFunction();  // 错误:无法访问file1.c中的静态函数

void anotherFunction() {
    // privateFunction();  // 错误:无法调用
}

static函数的优点

  • 隐藏实现细节:将辅助函数声明为static可以隐藏实现细节,只暴露必要的接口

  • 避免命名冲突:防止不同源文件中的同名函数冲突

  • 提高代码安全性:限制函数的访问范围,减少意外调用

extern

用于声明一个在其他源文件中定义的变量或函数

用于变量

extern关键字告诉编译器,该变量在其他地方已经定义,只是在当前文件中声明。

cpp 复制代码
// file1.c
int globalVar = 100;  // 定义全局变量

// file2.c
extern int globalVar;  // 声明外部变量

void function() {
    printf("globalVar = %d\n", globalVar);
    globalVar = 200;  // 修改全局变量
}
用于函数

在C语言中,函数默认具有外部链接属性,可以被其他源文件访问。使用extern关键字可以显式声明一个外部函数

cpp 复制代码
// file1.c
void utilityFunction() {
    printf("这是一个实用函数\n");
}

// file2.c
extern void utilityFunction();  // 声明外部函数(可以省略extern)

void anotherFunction() {
    utilityFunction();  // 调用在file1.c中定义的函数
}

extern关键字用于函数的作用

  • 显式声明外部函数:明确指出函数在其他源文件中定义

  • 提高代码可读性:使代码更加清晰,表明函数来自外部

  • 解决前向引用问题:在函数定义之前使用函数

register(单片机会用)

register关键字建议编译器将变量存储在CPU寄存器中,以提高访问速度。

cpp 复制代码
void function() {
    register int counter;  // 建议将counter存储在寄存器中
    for(counter = 0; counter < 1000; counter++) {
        // 频繁访问counter
    }
}//单片机里会遇到

现代编译器通常会自动进行寄存器优化,register关键字的作用不如以前明显。

const(重点)

const关键字用于声明一个常量,即一旦初始化后就不能再修改的变量。

cpp 复制代码
const int MAX_SIZE = 100;
// MAX_SIZE = 200;  // 错误:不能修改const变量
与指针使用
常量指针

指向常量的指针 :指针指向的内容不能通过该指针修改

cpp 复制代码
const int *p;  

指针常量

指针本身的值(即指向的地址)不能修改(指针本身是常量)

cpp 复制代码
int * const p;

指向常量的常量指针

cpp 复制代码
//内存和地址都不能修改
const int * const p;

用于参数

const关键字可以用于函数参数,表示函数不会修改传入的参数

cpp 复制代码
void printArray(const int arr[], int size) {
    for (int i = 0; i < size; i++) {
        printf("%d ", arr[i]);
        // arr[i] = 0;  // 错误:不能修改const数组元素
    }
    printf("\n");
}

用于函数返回值

cpp 复制代码
const char* getVersion() {
    return "v1.0.0";  // 返回一个不应被修改的字符串
}

总结:变量的存储区域比较

|----------|-----------|----------|----------------------|
| 存储区域 | 存储内容 | 生命周期 | 特点 |
| 栈区 | 局部变量、函数参数 | 函数调用期间 | 自动分配和释放,速度快 |
| 堆区 | 动态分配的内存 | 手动控制 | 需要手动管理,使用malloc/free |
| 数据段 | 全局变量、静态变量 | 整个程序运行期间 | 自动初始化为0 |
| 代码段 | 程序的可执行代码 | 整个程序运行期间 | 只读 |

内联函数

C99标准引入了inline关键字,用于定义内联函数。内联函数是一种特殊的函数,编译器会尝试将其调用处直接替换为函数体,而不是生成函数调用指令。

好处:

  1. 减少函数调用开销:避免了函数调用的压栈、跳转和返回等操作

  2. 适用于简短、频繁调用的函数:对于复杂函数,内联可能不会带来性能提升

  3. 只是对编译器的建议:编译器可能会忽略inline关键字,不进行内联

  4. 可能增加代码体积:因为函数代码被复制到每个调用处

cpp 复制代码
#include <stdio.h>

// 定义内联函数
static inline int max(int a, int b) { //根据C99标准,需要使用static inline组合
    return a > b ? a : b;
}

int main() {
    int x = 10, y = 20;
    
    // 调用内联函数
    int result = max(x, y);
    
    printf("最大值是: %d\n", result);
    
    return 0;
}

内联函数与宏定义

内联函数和宏定义都可以避免函数调用开销,但内联函数有以下优点:

  1. 类型安全:内联函数会进行类型检查,宏定义不会

  2. 求值一次:内联函数的参数只会被求值一次,避免了宏定义可能导致的多次求值问题

  3. 可以使用局部变量:内联函数可以定义局部变量,宏定义不能

  4. 可以使用条件语句和循环:内联函数可以包含复杂的控制结构

内存分配与释放函数

malloc - 内存分配

cpp 复制代码
void *malloc(size_t size);
  • 功能:分配指定字节数的内存空间

  • 参数

    • size:要分配的字节数
  • 返回值:指向分配的内存的指针,如果分配失败则返回NULL

  • 注意:分配的内存内容是未初始化的

realloc - 重新分配内存

cpp 复制代码
void *realloc(void *ptr, size_t size);
  • 功能:调整之前分配的内存块的大小

  • 参数

    • ptr:之前分配的内存块的指针(如果为NULL,则等同于malloc)

    • size:新的内存块大小(如果为0且ptr不为NULL,则等同于free)

  • 返回值:指向新分配内存的指针,如果分配失败则返回NULL

  • 注意

    • 如果新的大小大于原来的大小,额外的内存是未初始化的

    • 如果返回的指针与ptr不同,原来的内存块会被自动释放

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>

int main() {
    // 初始分配5个整数的空间
    int *numbers = (int *)malloc(5 * sizeof(int));
    
    if (numbers == NULL) {
        printf("内存分配失败\n");
        return 1;
    }
    
    // 初始化数组
    for (int i = 0; i < 5; i++) {
        numbers[i] = i * 10;
    }
    
    // 重新分配为10个整数的空间
    int *new_numbers = (int *)realloc(numbers, 10 * sizeof(int));
    
    if (new_numbers == NULL) {
        printf("内存重新分配失败\n");
        free(numbers);
        return 1;
    }
    
    numbers = new_numbers;
    
    // 初始化新增的元素
    for (int i = 5; i < 10; i++) {
        numbers[i] = i * 10;
    }
    
    // 使用扩展后的数组
    printf("重新分配后的数组: ");
    for (int i = 0; i < 10; i++) {
        printf("%d ", numbers[i]);
    }
    printf("\n");
    
    // 释放内存
    free(numbers);
    
    return 0;
}

free

cpp 复制代码
void free(void *ptr);
  • 功能:释放之前由malloc、calloc或realloc分配的内存

  • 参数

    • ptr:要释放的内存块的指针(如果为NULL,则不执行任何操作)
  • 返回值:无

  • 注意

    • 释放后的内存不能再被访问

    • 同一块内存不能被释放两次(双重释放)

    • 只能释放由malloc、calloc或realloc分配的内存

内存操作常见问题

cpp 复制代码
void memoryLeak() {
    int *p = (int *)malloc(sizeof(int));
    *p = 10;
    // 函数结束时没有调用free(p),导致内存泄漏
}

//解决方法:确保每次malloc、calloc或realloc都有对应的free。



int *p = (int *)malloc(sizeof(int));
*p = 10;
free(p);
*p = 20;  // 错误:使用已释放的内存


//解决方法:释放内存后将指针设置为NULL。
int *p = (int *)malloc(sizeof(int));
*p = 10;
free(p);
p = NULL;  // 防止使用已释放的内存


char *str = (char *)malloc(5);
strcpy(str, "Hello, World!");  // 错误:写入超过分配大小的数据

//解决方法:分配足够大的内存空间
char *str = (char *)malloc(15);
strcpy(str, "Hello, World!");  // 正确:分配了足够的空间
相关推荐
天赐学c语言3 天前
12.2 - LRU缓存 && C语言内存布局
c++·算法·lru·内存布局
点云SLAM18 天前
C++ 中的栈(Stack)数据结构与堆的区别与内存布局(Stack vs Heap)
开发语言·数据结构·c++·内存布局·栈数据结构·c++标准算法·heap内存分配
码字的字节4 个月前
深入理解Java内存与运行时机制:从对象内存布局到指针压缩
java·jvm·内存布局·指针压缩
同勉共进8 个月前
虚函数表里有什么?(三)——普通多继承下的虚函数表
c++·多继承·虚函数表·内存布局·rtti·non-virtual thunk·__vmi_class_type_info
太阳伞下的阿呆1 年前
Java内存布局
jvm·内存对齐·内存布局·压缩指针
知来者逆2 年前
Rust开发——数据对象的内存布局
开发语言·后端·rust·内存对齐·内存布局