C语言笔记----变量与堆栈

1.变量

1.1.局部变量

  • 定义:在函数或代码块内部定义的变量,仅在该函数或代码块内可见。
  • 作用域:仅限于函数或代码块内部,不能被其他函数访问。
  • 存储方式:存储在栈区(Stack),函数调用时分配,函数结束后释放。
  • 初始值:未初始化的局部变量值是随机的(垃圾值)。
  • 生命周期:函数调用时创建,函数返回后销毁

示例:

    1

    2

    3

    4

    5

    6

    7

    8

    9

   10

   11

   12

#include <stdio.h>

void func() {

    int x = 10;  // 局部变量

    printf("x = %d\n", x);

}

int main() {

    func();

    // printf("%d", x); // 错误:x 仅在 func() 内部可见

    return 0;

}

  •  注意:局部变量在函数执行完后被销毁,无法在函数外部访问。

1.2.全局变量

  • 定义:在函数外部定义的变量,对整个程序可见。
  • 作用域:整个程序(所有函数或文件都可以访问)。
  • 存储方式:存储在数据段(Data Segment),程序启动时分配,程序结束时释放。
  • 初始值:未初始化的全局变量默认初始化为 0。
  • 生命周期:从程序启动到程序结束

示例:

    1

    2

    3

    4

    5

    6

    7

    8

    9

   10

   11

   12

   13

#include <stdio.h>

int g_var = 100;  // 全局变量,默认初始化为 100

void func() {

    printf("g_var in func = %d\n", g_var);

}

int main() {

    printf("g_var in main = %d\n", g_var);

    func();

    return 0;

}

注意:

  • 全局变量可以被所有函数访问,但应尽量避免使用过多全局变量,以免影响程序的可读性和维护性。
  • 如果多个文件中有同名全局变量,可能会引起命名冲突

        全局变量:保存在内存的全局存储区中,占用静态的存储单元

        局部变量:保存在栈中,只有在所在函数被调用时才动态地为变量分配存储单元

1.3.静态变量

  • 定义:使用 static 关键字修饰的变量,可以是局部静态变量全局静态变量

局部静态变量

  • 作用域:仅限于函数内部,但不同于普通局部变量,它的值不会在函数调用结束后被销毁,而是保持原值。
  • 存储方式:存储在数据段(Data Segment),程序启动时分配,程序结束时释放(而不是栈区)
  • 初始值:未初始化的静态变量默认初始化为 0。
  • 生命周期:和全局变量一样,从程序开始到程序结束

示例:

    1

    2

    3

    4

    5

    6

    7

    8

    9

   10

   11

   12

   13

   14

#include <stdio.h>

void func() {

    static int count = 0;  // 局部静态变量

    count++;

    printf("count = %d\n", count);

}

int main() {

    func();  // count = 1

    func();  // count = 2

    func();  // count = 3

    return 0;

}

  • 注意:局部静态变量不会在函数结束后销毁,而是保留上次调用时的值

全局静态变量

  • 作用域:仅限于当前文件其他文件无法访问(限制了全局变量的作用域)。
  • 存储方式:存储在数据段,程序启动时分配,程序结束时释放。
  • 生命周期:从程序开始到程序结束。

示例:

    1

    2

    3

    4

    5

    6

    7

    8

    9

   10

   11

   12

#include <stdio.h>

static int g_static_var = 200;  // 全局静态变量

void func() {

    printf("g_static_var = %d\n", g_static_var);

}

int main() {

    func();

    return 0;

}

  • 注意: 区别于普通全局变量,全局静态变量不能在其他文件中访问。

1.4.寄存器变量

  • 定义:使用 register 关键字修饰的变量,建议存储在 CPU 寄存器,提高访问速度。
  • 作用域:仅限于函数或代码块内部。
  • 存储方式:存储在 CPU 寄存器(如果可用)或栈。
  • 初始值:未初始化的寄存器变量值是随机的。
  • 生命周期:函数调用时创建,函数返回后销毁。不能使用&取地址(因为可能存储在寄存器,而非内存)。

示例:

    1

    2

    3

    4

    5

    6

    7

    8

    9

   10

   11

  • }
  •     func();
  •     return 0;
  • int main() {
  • }
  •     register int x = 10;  // 建议存放在寄存器
  •     printf("x = %d\n", x);
  • void func() {
  • #include <stdio.h>
  • 注意:现代编译器会自动优化变量存储位置,因此 register 关键字使用较少。

1.5.总结

1.6.常量变量

  • 定义:用 const 关键字修饰的变量,其值不能被修改
  • 作用域:和普通变量一样,可以是局部的或全局的。
  • 存储方式:存储在数据段的只读区域(或者存储在栈中,取决于变量的作用域)
  • 生命周期:同普通变量一样。编译器会防止对其进行修改,但仍然占用内存

示例:

    1

    2

    3

    4

    5

    6

    7

    8

#include <stdio.h>

int main() {

    const int x = 10;  // x 是常量变量

    // x = 20; // 错误:不能修改 const 变量

    printf("x = %d\n", x);

    return 0;

}

注意:常量变量适用于不可变的值,比如 PI、物理常量等。

1.7.易变变量

  • 定义:用 volatile 修饰的变量,告诉编译器该变量的值可能会被外部修改,不要进行优化。
  • 特点:防止编译器优化,确保每次读取该变量时都会从内存中获取最新的值。适用于硬件寄存器、共享变量、多线程环境。

示例:

    1

    2

    3

    4

    5

    6

    7

    8

    9

#include <stdio.h>

volatile int flag = 1;  // flag 可能被外部改变,不能优化

void func() {

    while (flag) {

        // 可能会一直循环,直到某个外部事件修改 flag

    }

}

注意: volatile 适用于多线程或嵌入式编程中的寄存器变量,如 硬件 IO 端口、共享内存等。

1.8.外部变量

  • 定义:用 extern 关键字声明的变量,表示该变量在其他文件中定义当前文件只引用它,而不分配存储空间
  • 特点:
    • extern 变量用于多个 .c 文件共享同一个全局变量。
    • 不会分配内存,只是一个引用,实际的存储空间由定义它的文件分配。

示例:

file1.c(定义全局变量)

    1

    2

    3

    4

    5

    6

    7

#include <stdio.h>

int global_var = 100;  // 全局变量

void print_global_var() {

    printf("global_var = %d\n", global_var);

}

file2.c(使用 extern 变量)

    1

    2

    3

    4

    5

    6

    7

    8

#include <stdio.h>

extern int global_var;  // 声明外部变量

int main() {

    printf("global_var in file2 = %d\n", global_var);

    return 0;

}

注意:extern 变量常用于 模块化编程,使多个 .c 文件共享变量。

1.9.动态变量

  • 定义:用 malloc、calloc、realloc 在 堆区(Heap)动态分配的变量,需要手动 free 释放。
  • 特点:
    • 存储在堆区,手动管理内存。
    • 使用 malloc、calloc、realloc 动态分配,并用 free 释放。
    • 适用于大块数据存储、动态数组、链表等数据结构。

示例:

    1

    2

    3

    4

    5

    6

    7

    8

    9

   10

   11

   12

   13

   14

   15

   16

#include <stdio.h>

#include <stdlib.h>

int main() {

    int *p = (int *)malloc(sizeof(int));  // 分配一个整数的内存

    if (p == NULL) {

        printf("Memory allocation failed\n");

        return -1;

    }

    *p = 100;

    printf("Dynamic Variable: %d\n", *p);

    free(p);  // 释放内存

    return 0;

}

注意:动态变量适用于需要灵活管理内存的场景,如链表、数组等。

1.10.总结

2.堆栈

  • 堆栈是一段连续的存储空间。
  • 遵守后入先出,先入后出,后来居上Lastt in First out3、只能从顶部(瓶口)加入和读取数据
  • 主要动作:推入push,取出pull。
  • 堆栈可以来完成参数传递和返回值的传递(函数调用)
  • 堆栈可以用来保存局部变量、寄存器的值
  • 典型用处是中断,保存案发现场。

2.1.简介

  • 堆(Heap)和栈(Stack)是计算机内存管理中的两种主要区域,它们在数据存储和管理上有显著区别,主要体现在存储方式、访问方式、分配和释放方式等方面。

2.2.栈(Stack)

  • 存储内容:主要用于存储函数调用的局部变量、参数、返回地址等。
  • 分配方式:栈是由系统自动分配和释放的,遵循后进先出(LIFO, Last In First Out)的原则。
  • 访问速度:由于栈是连续的内存块,访问速度非常快。
  • 大小限制:栈的大小一般较小(通常在几MB级别),容易发生栈溢出(Stack Overflow),如递归调用过深。

示例:

    1

    2

    3

void func() {

    int a = 10;  // a 被分配在栈中

}

注意:在函数 func 执行时,a 会被分配到栈中,函数执行完毕后,a 也会自动释放。

2.3.堆(Heap)

存储内容:主要用于存储动态分配的对象或数据,生命周期由程序员控制。

分配方式:由程序员手动分配和释放,使用 malloc/free(C语言) 或 new/delete(C++),如果程序员忘记释放,可能导致内存泄漏

访问速度:由于堆内存是动态分配的,访问速度通常比栈慢一些,可能需要指针间接访问。

大小限制:堆的大小一般远大于栈,适合存放大量数据,但频繁分配和释放可能导致内存碎片化,降低性能。

示例:

    1

    2

    3

    4

    5

void func() {

    int* p = (int*)malloc(sizeof(int)); // p 指向的内存分配在堆中

    *p = 10; 

    free(p);  // 释放堆内存,避免内存泄漏

}

注意:如果 free(p); 被遗忘,程序会发生内存泄漏,导致可用内存逐渐减少。

2.4.总结

2.5.为什么栈在高地址,堆在低地址?

栈的特点是动态增长和收缩,它在程序执行期间频繁地进行函数调用和返回,需要高效地分配和释放内存。为了实现这种高效的内存分配和释放,栈采用了一种先进后出(Last-In-First-Out,LIFO)的数据结构。栈的内存分配和释放是通过调整栈指针来实现的,栈指针向高地址移动表示分配新的栈帧,向低地址移动表示释放栈帧。因此,将栈置于高地址空间可以方便地通过增减栈指针来分配和释放内存。

相反,堆的特点是动态分配和释放,需要更灵活地管理内存。堆的内存分配是通过调用动态内存分配函数(如malloc、new)来实现的,分配的内存块的大小和位置在运行时确定。由于堆的内存分配和释放不像栈那样频繁,因此堆可以位于较低的地址空间,以便为栈和其他静态分配的数据留出较高的地址空间。

2.6.堆和栈的区别

堆与栈实际上是操作系统对进程占用的内存空间的两种管理方式,主要有如下几种区别:

(1)管理方式不同。栈由操作系统自动分配释放,无需我们手动控制;堆的申请和释放工作由程序员控制,容易产生内存泄漏;

(2)空间大小不同。每个进程拥有的栈大小要远远小于堆大小。理论上,进程可申请的堆大小为虚拟内存大小,进程栈的大小 64bits 的 Windows 默认 1MB,64bits 的 Linux 默认 10MB;

(3)生长方向不同。堆的生长方向向上,内存地址由低到高;栈的生长方向向下,内存地址由高到低。

(4)分配方式不同。堆都是动态分配的,没有静态分配的堆。栈有 2 种分配方式:静态分配和动态分配。静态分配是由操作系统完成的,比如局部变量的分配。动态分配由alloca()函数分配,但是栈的动态分配和堆是不同的,它的动态分配是由操作系统进行释放,无需我们手工实现。

(5)分配效率不同。栈由操作系统自动分配,会在硬件层级对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是由C/C++提供的库函数或运算符来完成申请与管理,实现机制较为复杂,频繁的内存申请容易产生内存碎片。显然,堆的效率比栈要低得多。

(6)存放内容不同。栈存放的内容,函数返回地址、相关参数、局部变量和寄存器内容等。当主函数调用另外一个函数的时候,要对当前函数执行断点进行保存,需要使用栈来实现,首先入栈的是主函数下一条语句的地址,即扩展指针寄存器的内容(EIP),然后是当前栈帧的底部地址,即扩展基址指针寄存器内容(EBP),再然后是被调函数的实参等,一般情况下是按照从右向左的顺序入栈,之后是被调函数的局部变量,注意静态变量是存放在数据段或者BSS段,是不入栈的。出栈的顺序正好相反,最终栈顶指向主函数下一条语句的地址,主程序又从该地址开始执行。堆,一般情况堆顶使用一个字节的空间来存放堆的大小,而堆中具体存放内容是由程序员来填充的。

从以上可以看到,堆和栈相比,由于大量malloc()/free()或new/delete的使用,容易造成大量的内存碎片,并且可能引发用户态和核心态的切换,效率较低。栈相比于堆,在程序中应用较为广泛,最常见的是函数的调用过程由栈来实现,函数返回地址、EBP、实参和局部变量都采用栈的方式存放。虽然栈有众多的好处,但是由于和堆相比不是那么灵活,有时候分配大量的内存空间,主要还是用堆。

无论是堆还是栈,在内存使用时都要防止非法越界,越界导致的非法内存访问可能会摧毁程序的堆、栈数据,轻则导致程序运行处于不确定状态,获取不到预期结果,重则导致程序异常崩溃,这些都是我们编程时与内存打交道时应该注意的问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值