c语言中的内存知识

内存布局

任何一个程序,正常运行都需要内存资源,用来存放诸如变量、常量、函数代码等等。这些不同的内容,所存储的内存区域是不同的,且不同的区域有不同的特性。因此我们需要研究C语言进程的内存布局,逐个了解不同内存区域的特性。

每个C语言进程都拥有一片结构相同的虚拟内存,所谓的虚拟内存,就是从实际物理内存映射出来的地址规范范围,最重要的特征是所有的虚拟内存布局都是相同的,极大地方便内核管理不同的进程。例如三个完全不相干的进程p1、p2、p3,它们很显然会占据不同区段的物理内存,但经过系统的变换和映射,它们的虚拟内存的布局是完全一样的。

通过从实际物理内存中映射的这种方式,我们可以发现每一个进程它的结构都是一样的,这样就方便了我们对每个进程进行管理。

 下面我们将会更详细地来聊一聊各个区段地详细信息

栈内存

  • 什么东西存储在栈内存中?
    • 环境变量
    • 命令行参数
    • 局部变量(包括形参)
  • 栈内存有什么特点?
    • 空间有限,尤其在嵌入式环境下。因此不可以用来存储尺寸太大的变量。
    • 每当一个函数被调用,栈就会向下增长一段,用以存储该函数的局部变量。
    • 每当一个函数退出,栈就会向上缩减一段,将该函数的局部变量所占内存归还给系统。
  • 注意:
    栈内存的分配和释放,都是由系统规定的,我们无法干预。

环境变量

用于存储有关系统环境的信息,如路径、用户信息等。比如说在程序中一个和程序相同层次目录的文件的话不需要写出来文件完整的路径,因为在栈中的环境变量中存放了对应路径。

命令行参数

在C语言中,命令行参数是通过main函数的参数传递给程序的。main函数的签名通常有以下两种形式:

  1. int main(void)
  2. int main(int argc, char *argv[])

其中,第二种形式用于接收命令行参数:

  • argc(argument count)表示传递给程序的参数个数,包括程序名本身。
  • argv(argument vector)是一个指向字符串数组的指针,每个字符串表示一个参数。

这些参数在程序运行时会被传递到栈内存中。具体来说,当操作系统加载并执行程序时,会将命令行参数放在栈上,然后将相应的指针传递给main函数。

下面是一个简单的示例,演示如何在C语言中访问命令行参数:

#include <stdio.h>

int main(int argc, char *argv[]) {
    printf("Number of arguments: %d\n", argc);
    for (int i = 0; i < argc; i++) {
        printf("Argument %d: %s\n", i, argv[i]);
    }
    return 0;
}

假设编译后的程序名为program,如果在命令行中执行以下命令:

./program arg1 arg2 arg3

输出将会是:

Number of arguments: 4
Argument 0: ./program
Argument 1: arg1
Argument 2: arg2
Argument 3: arg3

这里是4的原因, ./program也被当成了字符串传入了进来

 

可以看到运行程序的时候跟着的那行命令中的所有内存都传递给了argc。可以看到这里第二个参数是一个指针数组,可以接收多个字符串。

我们同时也可以发现其实系统上来并不是直接进入到main函数来执行我们的程序,而是先执行了一些系统初始化程序,比如定义了argv数组并且可以用命令行参数给它赋值。随后系统才会调用我们的main函数。

下面三种写法等效

qt中也可以加命令行参数,在Commed line arguments这一栏中

局部变量

用于存储函数的局部变量、参数和返回地址等信息。包括我们的main函数和自定义函数

在调用的时候栈是动态发生变化的,当调用了一个新的函数时,这个函数中的局部变量和传入的参数都新加到栈中,当函数调用结束后,这块内存自动被释放。栈是一种先进后出的结构,比如我们的main函数最早进来的,main函数占用的栈空间是最后被释放的。

局部变量的作用域是当前块,并不是当前函数,举一个很常见的例子。for(int i=0;i<5;i++){}

1.for 循环头部中定义的变量的作用域是 for 循环内部的块,而不是 for 所在的函数.

2.所以当我们在这个for循环外面来使用这个局部变量,会报错,显示变量i没有定义。

数据段内存

静态数据

数据段用于存储静态数据(全局变量和静态局部变量)

C语言中,静态数据有两种:全局变量和静态变量,细分的话其实可以分为三种,即全局变量静态全局变量静态局部变量

  • 全局变量:定义在函数外部的变量,整个文件所有地方都可以访问。全局变量的定义全部文件可见,默认使用当前文件中可使用。可以通过extern使得其他文件中也可使用,什么意思呢?

就是说默认情况下,C语言中的全局变量具有外部链接(external linkage),这意味着它在整个程序的所有文件中都是可见的,在其他文件中不能重复定义这个变量,但是这个变量不能直接给其他文件来使用。其他文件要想使用这个全局变量,只能在其他文件中通过extern来声明一下才可使用。

  • 静态全局变量:定义在函数外部且被static修饰的变量,整个文件所有地方都可以访问。静态全局变量的定义当前文件可见,只能当前文件中可使用。因为定义是当前文件可见,所以其他文件中可以定义同名的静态全局变量。
  • 静态局部变量:定义在函数内部且被static修饰的变量,只有函数内部可以访问。静态局部变量的定义是仅仅当前块可见,只能当前块中可使用,所以其他块中可以定义同名的静态全局变量。

数据段和栈内存,以及堆内存不一样的是占用的空间大小不可变,数据段中的数据可以发生变化,但是总的空间不变,也不会出现空间释放。全局变量和静态局部变量在程序结束的时候才释放。寿命很长。

  • 为什么需要静态数据?
  1. 全局变量主要是对于全部文件都可见,可以让不同文件之间都能访问这个数据
  2. 如果目的是仅当前文件所有函数可访问就行,那么就定义静态全局变量,可以方便地让所有函数之间共享这个数据,比如在写一些练习题时,将数组定义成静态全局变量可以避免函数传递。
  3. 当我们希望一个函数退出后依然能保留局部变量的值,以便于下次调用时还能用时,静态局部变量可帮助实现这样的功能。 static修饰的变量仅对当前函数有作用,static修饰的变量仅仅能当前函数内使用。
  • 注意:
    • 若定义时未初始化,则系统会将所有的静态数据自动初始化为0
    • 静态数据初始化语句,只会执行一遍。
    • 静态数据从程序开始运行时便已存在,直到程序退出时才释放。
    • 静态数据固然功能强大,但是不要滥用,要合理选择,比如变量都写成全局变量,就会污染其他文件中命名空间

除此之外,static还可以修饰函数

  • static修饰函数:使之由各文件可见的函数,变成了本文件可见的静态函数。如果在一个文件中定义了一个函数,在其他文件中不能再定义一个同名的函数。这是因为函数默认具有外部链接,名称在整个程序中必须是唯一的。希望在其他文件中使用这个函数,那么在其他文件中需要使用 extern 关键字进行声明。并且加上static之后,链接属性变为内部链接,其他文件也不可见这个函数的定义了,也能声明同名函数
  • ok那就再来补充一点吧,刚刚无论是全局变量,还是函数想要在其他文件中使用的话就必须extern,那么一个文件中的多个函数和全局变量被其他函数使用,extern声明岂不是很麻烦,确实,所以也可以直接包含其他文件的头文件

数据段的.bss,.data,.rodata简单了解一下就行

使用extern示例:

static的总结

static关键字在C语言中有两个不同的作用:改变可见(作用)域和改变存储期。掌握时空之力,恐怖如斯。

  1. 将可见范围设定为标识符所在的文件:
    • 修饰全局变量:将全局变量变为静态全局变量,使得全局变量由原来的跨文件可见,变成仅限于本文件可见。
    • 修饰普通函数:使得函数由原来的跨文件可见,变成仅限于本文件可见。
  2. 将存储区域设定为数据段:
    • 修饰局部变量:使得局部变量由原来存储在栈内存,变成存储在数据段。变为静态局部变量但是这个变量的可见性是不变的,仍然只能在变量所处的块中可见

堆内存

堆内存的特点和使用示例

  1. 动态分配:堆内存可以在程序运行时使用库函数如malloccallocrealloc进行动态分配。
  2. 手动释放:程序员需要使用free函数手动释放不再需要的堆内存,否则会导致内存泄漏。
  3. 不自动回收:与栈内存不同,堆内存不会在函数返回时自动释放,需要显式释放。在堆中分配的变量,统统拥有自定义存储期。
  4. 灵活性:适用于大小和数量在编译时未知的数据结构。

申请堆内存的正确且完备的过程

用完后free及时归还,将对应指针置空!!!

堆中常用函数

  • 申请堆内存:malloc() / calloc()
  • 清零堆内存:bzero()
  • 释放堆内存:free()

注意: 

  • malloc()申请的堆内存,默认情况下是随机值,一般需要用 bzero() 来清零。
  • calloc()申请的堆内存,默认情况下是已经清零了的,不需要再清零。
  • 我认为calloc()用在申请结构体数组中很适合。申请简单清晰,并且申请空间大小合适。
  • free()只能释放堆内存,不能释放别的区段的内存。

对free释放内存的详细说明

  • 释放内存意味着将内存的使用权归还给系统。只是告诉系统我这块内存不用了,你可以将它分配给别人。
  • 释放内存并不会改变指针的指向。并且通过原来的指针仍然可以访问到这块内存,相当于虽然告诉系统我这块内存不同了,但仍保留了访问这块内存途经。所以为了避免再次用指针访问释放过的内存(危险行为),要立即将释放后的指针置空。
  • 释放内存并不会对内存做任何修改,更不会将内存清零。

对于malloc函数的详细使用请移步至:

c语言对指针和数组的进一步理解(字符串,数组和指针进一步探究,const关键字,函数指针,几个例题)-CSDN博客

对于calloc函数等函数详细使用请移步至: 

【C语言】calloc()函数详解(动态内存开辟函数)-CSDN博客

内存泄漏检测工具

linux下一款检测内存泄漏的工具:sudo apt-get install valgrind # 对于 Debian/Ubuntu 系统

当然qt下也可以通过按键直接来调用valgrind帮助检测。

 

相关推荐

  1. C语言动态内存管理

    2024-07-18 09:22:03       33 阅读
  2. C语言L / 数据在内存存储

    2024-07-18 09:22:03       40 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-07-18 09:22:03       67 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-07-18 09:22:03       72 阅读
  3. 在Django里面运行非项目文件

    2024-07-18 09:22:03       58 阅读
  4. Python语言-面向对象

    2024-07-18 09:22:03       69 阅读

热门阅读

  1. OPPO 2024届校招正式批笔试题-后端(C卷)

    2024-07-18 09:22:03       23 阅读
  2. Caffeine缓存

    2024-07-18 09:22:03       22 阅读
  3. GO语言用http包发送带json文本body的GET请求

    2024-07-18 09:22:03       21 阅读
  4. Ubuntu 20 安装 uwsgi 失败解决办法

    2024-07-18 09:22:03       20 阅读
  5. 构建艺术:在Gradle中配置父子项目的关系

    2024-07-18 09:22:03       25 阅读
  6. (79)组合环路--->(03)组合环路代码示例一

    2024-07-18 09:22:03       22 阅读
  7. npm 设置镜像

    2024-07-18 09:22:03       20 阅读
  8. https 单向认证和双向认证

    2024-07-18 09:22:03       20 阅读