【内存分配】01.C内存分配/堆栈

【嵌入式八股】一、语言篇(本专栏)https://www.nowcoder.com/creation/manager/columnDetail/mwQPeM

【嵌入式八股】二、计算机基础篇https://www.nowcoder.com/creation/manager/columnDetail/Mg5Lym

【嵌入式八股】三、硬件篇https://www.nowcoder.com/creation/manager/columnDetail/MRVDlM

【嵌入式八股】四、嵌入式Linux篇https://www.nowcoder.com/creation/manager/columnDetail/MQ2bb0

内存分配

alt

结合【Linux内核驱动中内存资源管控】、【Linux内核驱动中用户/内核的堆栈】与【操作系统内存管理】一起看

C内存分配/堆栈

01.C内存分配

alt

#include <stdio.h>

const int    g_A       = 10;         //常量区 
int          g_B       = 20;         //数据段  
static int   g_C       = 30;         //数据段  
static int   g_D;                    //BSS段  
int          g_E;                    //BSS段  
char        *p1;                     //BSS段  

int main()
{
    int           local_A;            //栈  
    int           local_B;            //栈  
    static int    local_C = 0;        //BSS段(初值为0 等于没初始化,会放在BSS段 )  
    static int    local_D;            //数据段  
      
    char        *p3 = "123456";     //123456在代码段,p3在栈上  
  
    p1 = (char *)malloc( 10 );      //堆,分配得来得10字节的区域在堆区  
    char *p2 = (char *)malloc( 20 ); //堆上再分配,向上生长
    strcpy( p1, "123456" );         //123456放在常量区,编译器可能会将它与p3所指向的"123456"优化成一块  
    printf("hight address\n");  
    printf("-------------栈--------------\n");  
    printf( "栈,    局部变量,                           local_A, addr:0x%08x\n", &local_A );  
    printf( "栈,    局部变量,(后进栈地址相对local_A低)     local_B, addr:0x%08x\n", &local_B );  
    printf("-------------堆--------------\n");  
    printf( "堆,    malloc分配内存,             p2,     addr:0x%08x\n", p2 );  
    printf( "堆,    malloc分配内存,             p1,     addr:0x%08x\n", p1 );  
    printf("------------BSS段------------\n");  
    printf( "BSS段, 全局变量,       未初始化    g_E,     addr:0x%08x\n", &g_E, g_E );      
    printf( "BSS段, 静态全局变量,   未初始化,   g_D,      addr:0x%08x\n", &g_D );  
    printf( "BSS段, 静态局部变量,   未初始化,   local_C,  addr:0x%08x\n", &local_C);  
    printf( "BSS段, 静态局部变量,   未初始化,   local_D,  addr:0x%08x\n", &local_D);  
    printf("-----------数据段------------\n");  
    printf( "数据段,全局变量,       初始化      g_B,      addr:0x%08x\n", &g_B);  
    printf( "数据段,静态全局变量,    初始化,     g_C,     addr:0x%08x\n", &g_C);  
    printf("-----------代码段------------\n");  
    printf( " 常量区                只读const,  g_A,     addr:0x%08x\n\n", &g_A);  
    printf( " 程序代码,可反汇编看 objdump -d a.out \n");
    printf("low address\n");  
    return 0;
}
02.全局变量和局部变量的区别
  1. 作用域:全局变量的作用域是整个程序,它可以被程序中的任何函数调用和访问。而局部变量的作用域仅限于它所在的函数或代码块中,函数外部无法访问它。
  2. 存储位置:全局变量存储在静态存储区,而局部变量存储在栈中。
  3. 生命周期:全局变量储存在静态区,生命周期是整个程序执行期间,即在程序开始运行时创建,直到程序结束时才被销毁。而局部变量在栈中分配,生命周期仅限于其所在函数的执行期间,在函数被调用时才被创建,在函数返回时被销毁。
  4. 访问速度:由于全局变量存储在静态存储区,访问速度比局部变量要慢一些。
  5. 默认值:全局变量默认初始化为0或NULL,而局部变量不会自动初始化,其值由程序员决定。

一般来说,应该尽可能使用局部变量,以便控制变量的作用域和生命周期,并且避免全局变量带来的潜在问题。

03.全局变量和static变量的区别
  • 作用域

全局变量本身就是静态存储方式,静态全局变量也是静态存储方式。这两者在存储方式上并无不同。这两者的区别在于非静态全局变量的作用域是整个源程序,当一个源程序由多个原文件组成时,非静态的全局变量在各个源文件中都是有效的。

而静态全局变量则限制了其作用域,即只在定义该变量的源文件内有效,在同一源程序的其它源文件中不能使用它。由于静态全局变量的作用域限于一个源文件内,只能为该源文件内的函数公用,因此可以避免在其他源文件中引起错误。

04.内存分配方式
  1. 静态存储区分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static变量。
  2. 在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
  3. 从堆上分配。亦称动态内存分配。程序在运行的时候用malloc或new申请任意多少的内存,程序员自己负责在何时用free或delete释 放内存。动态内存的生存期由程序员决定,使用非常灵活,但如果在堆上分配了空间,就有责任回收它,否则运行的程序会出现内存泄漏,频繁地分配和释放不同大小的堆空间将会产生堆内碎块。
05.栈在C语言中有什么作用
  1. 函数调用:C语言中栈用来存储临时变量,临时变量包括函数参数和函数内部定义的临时变量。函数调用相关的函数返回地址,函数中的临时变量,寄存器等均保存在栈中,函数调用返回后从栈中恢复寄存器和临时变量。
  2. 多线程/进程:栈是多线程编程的基石,每一个线程都最少有一个自己专属的栈,用来存储本线程运行时各个函数的临时变量和维系函数调用和函数返回时的函数调用关系和函数运行场景。
  3. 中断处理
06.C语言函数参数压栈顺序是怎样的?

变量从下到上、函数参数从右至左

变量从下到上的入栈顺序的好处是最后压入的参数总是能够被函数找到,因为它就在堆栈指针的上方。

参数从右至左的入栈顺序的好处就是可以动态变化参数个数。自左向右的入栈方式,最前面的参数被压在栈底。除非知道参数个数,否则是无法通过栈指针的相对位移求得最左边的参数。这样就变成了左边参数的个数不确定,正好和动态参数个数的方向相反。因此,C语言函数参数采用自右向左的入栈顺序,主要原因是为了支持可变长参数形式。

07.调用函数时,哪些内容需要压栈?

函数调用过程栈的变化,返回值和参数变量哪个先入栈?

函数调用的压栈过

alt

  1. 压入调用方的函数参数

  2. 压栈返回地址,确保函数调用结束后能够正确的执行剩余代码。

  3. 保存栈底,更改栈底和栈顶

  4. 压栈保存的寄存器,局部变量,执行函数操作,返回值保存到寄存器

更具体一点看下文

函数调用过程中函数栈详解_芹泽的博客-CSDN博客_函数调用过程中函数栈详解

main为调用函数,sum为被调用函数

压入main函数参数,从右至左入栈

sum函数返回地址进行压栈

将main函数基指针入栈保存,保存栈底

创建sum函数的栈栈帧,更改栈底和栈顶

压栈保存的寄存器

将sum函数的局部变量放入[ebp-4]的空间内

执行sum函数操作

将sum函数返回值(变量所在的地址的内容)放入eax寄存器保存

将之前入栈的值重新返回给edi寄存器

销毁sum函数栈帧

栈中保存的main函数的基址赋值给ebp

把之前保存的函数返回地址(也就是main函数中下一条该执行的指令的地址)出栈

传入sum函数的参数已经不需要了,将esp指针上移

08.将临时变量作为返回值时的处理过程

临时变量,在函数调用过程中是被压到程序进程的栈中的,当函数退出时,临时变量出栈,即临时变量已经被销毁,临时变量占用的内存空间没有被清空,但是可以被分配给其他变量,所以有可能在函数退出时,该内存已经被修改,对于临时变量来说已经是没有意义的值了。

C语言里规定:16bit程序中,返回值保存在ax寄存器中,32bit程序中,返回值保持在eax寄存器中,如果是64bit返回值,edx寄存器保存高32bit,eax寄存器保存低32bit

由此可见,函数调用结束后,返回值被临时存储到寄存器中,并没有放到堆或栈中,也就是说与内存没有关系了。当退出函数的时候,临时变量可能被销毁,但是返回值却被放到寄存器中与临时变量的生命周期没有关系

如果我们需要返回值,一般使用赋值语句就可以了。

09.printf函数的实现原理是什么?
  1. 格式化字符串解析:当程序调用printf函数时,格式化字符串会被解析,找到其中的格式占位符并将其替换成对应的值,例如%d表示整数占位符,%f表示浮点数占位符等等。
  2. 参数压栈:printf函数的参数被压入栈中,按照从右往左的顺序压入,最后一个参数先入栈,第一个参数最后入栈
  3. 格式化字符串与参数匹配:解析完格式化字符串并将参数压入栈中后,程序会按照格式化字符串中的占位符顺序依次从栈中弹出对应的参数值,并将其转换成字符串形式,用于输出。
  4. 输出到标准输出或文件:最后,程序将格式化后的字符串输出到标准输出或指定的文件中。

printf的第一个被找到的参数就是那个字符指针,就是被双引号括起来的那一部分,函数通过判断字符串里控制参数的个数来判断参数个数及数据类型,通过这些就可算出数据需要的堆栈指针的偏移量了,下面给出printf("%d,%d",a,b);(其中a、b都是int型的)的汇编代码

.section
.data
string out = "%d,%d"
push b
push a
push $out
call printf

你会看到,参数是最后的先压入栈中,最先的后压入栈中,参数控制的那个字符串常量是最后被压入的,所以这个常量总是能被找到的。

10.对空栈和满栈的了解?什么是SP、什么是FP寄存器?

满堆栈(full stack,“F”)是指堆栈指针指向堆栈的最后一个已使用的地址或者满位置。

空堆栈(empty stack,"E")是指sp指向堆栈的第一个没有使用的地址或者空位置(也就是存下一个的时候就用它)。

SP寄存器是栈指针寄存器(Stack Pointer Register),用于指向栈顶元素的地址,通常在进入函数时用于保存现场,在函数返回时恢复现场。

FP寄存器是帧指针寄存器(Frame Pointer Register),也称为基址指针寄存器(Base Pointer Register),用于指向当前函数帧(stack frame)的基地址。在函数中,通过FP寄存器可以访问函数参数、局部变量和临时变量等信息。

11.堆与栈区别
  • 管理方式:对于栈来讲,是由编译器自动管理,无需我们手工控制;对于堆来说,释放工作由程序员控制,容易产生memory leak。

  • 空间大小:一般来讲在32位系统下,堆内存可以达到4G的空间,从这个角度来看堆内存几乎是没有什么限制的(堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。)但是对于栈来讲,一般都是有一定的空间大小的(栈:在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小

    在 Windows下,栈的大小是2M(也有的说是1M,总之是一个编译时就确定的常数),

    在VC6下面,默认的栈空间大小是1M。

    Linux下默认的用户栈空间大小是8MB,内核栈空间大小是8KB。Linux进程栈空间大小 - Tiehichi's Blog

  • 碎片问题:对于来讲,频繁的new/delete会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。栈不会存在这个问题,因为栈是先进后出的。

  • 分配方式堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由malloca函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行释放,无需手工实现。

  • 生长方向堆,向上生长,也就是向着内存地址增加的方向;栈,向下生长,是向着内存地址减小的方向增长。

  • 分配效率:栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高(只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出)。堆则是C/C++函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法(操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。) 在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多

记忆:管小片,方长效率

12.为什么栈的生长方向从上到下?

栈是一种常见的数据结构,它遵循"后进先出"的原则。栈的生长方向通常被定义为从高地址向低地址,这意味着栈底在较高的内存地址,而栈顶在较低的内存地址。

个人理解:系统上电后都是从内存零地址开始分配和使用内存空间的,栈要足够远离这些空间,以免引起错误。

防止踩踏内核数据。

这种栈的生长方向选择是出于一些技术和历史原因。在早期的计算机系统中,程序员使用汇编语言编写程序,并直接操作内存。栈的生长方向是由处理器硬件设计决定的。处理器使用栈指针寄存器来跟踪栈顶的位置。栈指针的初始值通常设置为栈的最高地址,然后通过不断递减来指向栈的新的栈顶位置

当调用函数时,函数的局部变量和其他相关数据被存储在栈帧中。栈帧是一个用于支持函数调用和返回的内存区域。每当函数被调用时,一个新的栈帧就会被创建并推入栈中,使栈顶指针下移。当函数执行完毕后,栈顶指针再次上移,将栈帧弹出栈。

由于栈的生长方向从高到低,函数调用时,新的栈帧会被添加到当前栈顶的下方,这样可以简单高效地管理栈上的数据。此外,栈的生长方向也有助于防止栈溢出问题的发生。当栈的生长超过分配给它的内存空间时,栈底和其他数据之间的空间将被用作栈溢出的标志,从而使程序可以检测到并采取相应的措施。

13.在局部数组中定义一个大数组可以吗?很大的数组,比如2048

不可以,会爆栈,栈溢出

在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,例如,在 WINDOWS下,栈的大小是2M(也有的说是1M,总之是一个编译时就确定的常数),在VC6下面,默认的栈空间大小是1M。当然,这个值可以修改。如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小

局部数组,具有局部作用域,当函数调用结束之后,数组也就被操作系统销毁了,即回收了他的内存空间。

解决方法,(解决局部大数组爆栈和局部作用域的问题)

  1. 定义成全局数组
  2. 加static放在静态存储区
char *fun()
{
      static char a[] = "hello,world";
      return a;
}
  1. 数组用malloc申请空间放在堆区

    定义一个指针指向这个数组,栈中只占用一个指针的大小

char *fun()
{
     char *a = (char*)malloc(sizeof(char)*100);
     a = "hello,world";
     return a;
}
14.为什么堆的空间不连续?

堆是C/C++函数库提供的,为了分配一块内存,库函数会按照一定的算法(操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。) 在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。

15.你觉得堆快一点还是栈快一点?

毫无疑问是栈快一点。

因为操作系统会在底层对栈提供支持,会分配专门的寄存器存放栈的地址,栈的入栈出栈操作也十分简单,并且有专门的指令执行,所以栈的效率比较高也比较快。

而堆的操作是由C/C++函数库提供的,在分配堆内存的时候需要一定的算法寻找合适大小的内存。并且获取堆的内容需要两次访问,第一次访问指针,第二次根据指针保存的地址访问内存,因此堆比较慢。

16.堆的动态申请释放时注意的点:orange:
  • 判断成功申请到空间
  • 释放后指向NULL(通过后面两道题更好理解)
#include <stdio.h>
#include <stdlib.h>
int main()
{
   char *pt;
   pt= (char *)malloc(10); //堆上申请空间(malloc的输入参数,是申请空间的字节数)
                           //成功 返回值是申请空间的地址,失败返回NULL;
   if(pt == NULL){ 
      printf("申请空间失败");
      return -1;
   }
   *pt=0x11;
   *(pt+1)=0x22;
   printf("%x %x %x ",pt[0],pt[1],pt[2]);
   free(pt); //释放空间,避免内存泄漏
   pt =NULL; //避免野指针,操作已释放的空间
   return 0;
}
17.给已free的指针赋值
//问: 输出结果是什么? 为什么
#include <stdio.h>
#include <stdlib.h>
void GetMemory(char **p,int num){                          
   *p=(char *)malloc(num); 
}  
int main(){
   char *str=NULL;
   GetMemory(&str,100);  
   strcpy(str,"hello");
   free(str);
   if(str!=NULL)
   {
      strcpy(str,"world");
   }   
   printf("\n str is %s",str);
}

答案:输出str is world。 进程中的内存管理是由库函数完成,不是由操作系统。 malloc时,库函数会向操作系统申请空间,并做记录。 但free时,库函数不会把空间马上还给操作系统,还能继续访问。 但后面可能这个空间分配给别人。 所以为避免误操作 free(str) 后需 str=NULL;

18.被free回收的内存是立即返还给操作系统吗?

不是的,被free回收的内存会首先被ptmalloc使用双链表保存起来,当用户下一次申请内存的时候,会尝试从这些内存中寻找合适的返回。这样就避免了频繁的系统调用,占用过多的系统资源。同时ptmalloc也会尝试对小块内存进行合并,避免过多的内存碎片。

19.malloc的底层是如何实现的?

深入 malloc 函数,带你真正理解内存分配! - 知乎 (zhihu.com)

在一般的操作系统中,进程启动时,操作系统会为该进程分配一块内存空间作为其虚拟内存空间。malloc() 函数就是在这块虚拟内存空间中分配内存的。当我们调用 malloc() 函数请求内存时,malloc() 函数会向操作系统申请一块足够大的虚拟内存空间。然后,malloc() 函数将该虚拟内存空间划分成若干块内存,其中一块被分配给用户使用,剩余的块被标记为未使用的状态,以便于下一次的 malloc() 函数调用使用。

malloc() 函数的具体实现方式可以有多种,其中比较常见的方式是采用堆(heap)来管理内存。堆是操作系统为进程分配的一块虚拟内存空间,其大小可以根据需要动态增长或缩小。malloc() 函数通过调用系统函数 sbrk() 或 mmap() 来向操作系统申请增加堆的大小,然后将堆分割成一些块,每个块大小可以根据用户请求的大小来灵活调整。当用户使用完内存后,可以通过调用 free() 函数将该块内存释放回堆中,以便于下一次的 malloc() 函数调用使用。

malloc函数的底层实现是操作系统有一个由可用内存块连接成的空闲链表(堆的底层实现)。调用malloc时,它将遍历该链表寻找足够大的内存空间,将该块一分为二(一块与用户申请的大小相等,另一块为剩下来的碎片,会返回链表),调用free函数时,内存块重新连接回链表。若内存块过于琐碎无法满足用户需求,则操作系统会合并相邻的内存块。由于堆的管理需要维护内存块的链表,因此堆的实现可能存在一些效率问题。为了提高堆的性能,一些优化技术被应用于堆的实现中,例如采用伙伴算法、分配器缓存、内存池等。

malloc是C语言中用于动态内存分配的函数。其底层实现可能因操作系统和编译器的不同而有所不同,但一般来说,它的工作原理如下:

  1. 当你首次调用malloc时,它会向操作系统请求一大块内存。这通常通过系统调用如sbrkmmap(在Unix-like系统中)完成。
  2. malloc会维护一个空闲内存块的列表。当你请求一块特定大小的内存时,malloc会在这个列表中查找足够大的内存块。
  3. 如果找到了足够大的内存块,malloc就会将其标记为已使用,并可能将其分割为更小的块。
  4. 如果没有找到足够大的内存块,malloc就会再次向操作系统请求更多的内存。
  5. 当你调用free函数时,内存块会被标记为可用,并可能与相邻的空闲块合并。
20.malloc最小分配的大小,malloc(1)分配多大?

malloc函数的最小分配大小通常取决于操作系统和编译器的实现。然而,通常情况下,malloc至少会分配一个字节的内存,因为这是C语言中最小的可寻址单元。

但是,实际上,由于内存对齐和管理开销的原因,malloc可能会分配比你请求的更大的内存块。例如,许多系统会将分配的内存块大小对齐到4或8字节的边界。此外,malloc还需要存储一些额外的信息,如内存块的大小和状态,这通常会在每个分配的内存块中占用一些额外的空间。

因此,虽然你可以请求malloc分配一个字节的内存,但实际分配的内存块可能会大于一个字节。

当你调用malloc(1)时,你是在请求分配1字节的内存。然而,实际分配的内存可能会大于1字节,这取决于操作系统和编译器的实现。

在大多数现代操作系统中,malloc会将分配的内存块大小对齐到一定的边界,通常是8或16字节。这是为了满足硬件的对齐要求,以及为了提高内存访问的效率。

此外,malloc还需要存储一些管理信息,如内存块的大小和是否已被分配。这些信息通常存储在每个分配的内存块的开头,因此会占用一些额外的空间。

因此,即使你只请求分配1字节的内存,实际分配的内存块的大小可能会大于1字节。具体的大小取决于你的系统和编译器的实现,但通常会在16字节到24字节之间。

21.malloc分配多大的内存的时候会需要向内核申请?

malloc函数在需要更多内存来满足请求时会向内核申请内存。具体的阈值取决于malloc的实现和系统的配置。

在GNU libc库的malloc实现中,有一个阈值叫做"MMAP_THRESHOLD"。对于大于这个阈值的内存请求,malloc会直接使用mmap系统调用向内核申请内存,而不是从malloc维护的空闲内存列表中分配。这个阈值默认是128KB,但可以通过mallopt函数进行调整。

对于小于这个阈值的内存请求,malloc会首先查看它的空闲内存列表是否有足够的内存可以满足请求。如果没有,malloc会使用sbrkbrk系统调用来增加程序的堆大小,从而获取更多的内存。

需要注意的是,这些细节可能会因操作系统和C库的不同而有所不同。例如,某些系统可能总是使用mmap来分配大内存块,而其他系统可能有不同的阈值或者完全不使用mmap

22.malloc和free的使用有哪些注意的?

在C语言中,mallocfree是用于动态内存分配和释放的函数。在使用它们时,有几个重要的注意事项:

  1. 检查malloc的返回值malloc函数在无法分配请求的内存时会返回NULL。因此,你应该总是检查malloc的返回值,以确保内存已成功分配。
c复制代码int *ptr = (int*) malloc(sizeof(int) * 10);
if (ptr == NULL) {
    printf("Memory allocation failed\n");
    return -1;
}
  1. 避免内存泄漏:每次使用malloc分配内存后,都应使用free来释放这些内存。如果你忘记释放内存,或者在释放内存后继续使用它,就会导致内存泄漏。
c复制代码free(ptr);
ptr = NULL; // 避免悬挂指针
  1. 不要重复释放内存:一旦你使用free释放了内存,就不应再次释放它。这会导致未定义的行为。

  2. 正确计算内存大小:在调用malloc时,你需要传递你想要分配的字节数。你应该使用sizeof运算符来计算你需要的内存大小,以避免分配过多或过少的内存。

23.在1G内存的计算机中能否通过malloc申请大于1G的内存?为什么?

可以 因为malloc函数是在程序的虚拟地址空间申请的内存,与物理内存没有直接的关系。虚拟地址与物理地址之间的映射是由操作系统完成的,操作系统可通过虚拟内存技术扩大内存。

24.在1G内存的计算机中能否malloc(1.2G)?为什么?

malloc能够申请的空间大小与物理内存的大小没有直接关系,仅与程序的虚拟地址空间相关。 程序运行时,堆空间只是程序向操作系统申请划出来的一大块虚拟地址空间。应用程序通过malloc申请 空间,得到的是在虚拟地址空间中的地址,之后程序运行所提供的物理内存是由操作系统完成的。 本题要申请空间的大小为 1.2G=(1024 * 1024*1024) * 1.2Byte转换为十六进制约为 4CCC CCCC ,这个数值已经超过了 int 类型的表示范围,但还在 unsigned 的表示范围。幸运的是 malloc 函数要求的参数为 unsigned 。见下面的示例代码。

#include <stdio.h> 
#include <stdlib.h> 
int main() {
    char*p;
    const unsigned k= 1024*1024*1024*1.2; 
        printf("%x\n",k); 
        p= (char *)malloc( k ); 
        if( p!=NULL ) 
            printf("OK");
    else 
        printf("error"); 
        return0; 
}
25.malloc能申请多大的空间?

想知道在一台机器上malloc能够 申请的最大空间到底是多少,可以使用下面的程序进行测试。

#include <stdio.h> 
#include <stdlib.h>
unsigned maximum = 1024*1024*1024; 
int main(int argc, char *argv[]) {
    unsignedblocksize[] = {1024*1024, 1024, 1}; 
    inti, count; 
    void* block; 
    for(i=0; i<sizeof(blocksize)/sizeof(unsigned); i++ ) {
        for( count = 1; ;count++ ) {
            block = malloc( maximum +blocksize[i]*count ); 
            if( block!=NULL ) 
            { 
                maximum= maximum + blocksize[i]*count; free(block );
            }
            else 
            { 
                break; 
            } 
        } 
    }
	printf("maximummalloc size = %u bytes\n", maximum); return0;
}

在当前正在使用的Windows环境中,可申请的最大空间超过1.9G。实际上,具体的数值会受到操作系统版本、程序本身的大小、用到的动态/共享库数量、大小、程序栈数量、大小等的影响,甚至每次运行的结果都可能存在差异,因为有些操作系统使用了一种叫做随机地址分布的技术,使得进程的堆空间变小。

26.程序在执行int main(int argc, char *argv[])时的内存结构,你了解吗?

参数的含义是程序在命令行下运行的时候,需要输入argc 个参数,每个参数是以char 类型输入的,依次存在数组里面,数组是 argv[],所有的参数在指针char * 指向的内存中,数组的中元素的个数为 argc 个,第一个参数为程序的名称。

当main函数被调用时,操作系统会将栈空间的一部分分配给该函数使用。argc和argv参数是通过栈空间传递给main函数的,它们通常被存储在栈的底部。main函数的局部变量也存储在栈空间中,它们的大小和数量可以根据程序需要动态地变化。函数调用过程中,每次调用都会在栈空间中创建一个新的栈帧,用于存储被调用函数的局部变量和返回地址。

在程序执行过程中,操作系统会根据程序的内存使用情况和系统的可用内存动态地调整进程地址空间的大小,以确保程序能够正常运行并不会耗尽系统的内存资源。

27.main函数的返回值有什么值得考究之处吗?

main函数的返回值是程序的退出状态码,用于指示程序的执行情况。在C语言中,main函数的返回值类型为int,可以返回任意整数值。

程序的退出状态码通常具有以下含义:

  • 返回0表示程序正常结束。
  • 返回正整数表示程序出现了错误或异常情况,具体的值可以表示不同的错误类型或错误码。
  • 返回负整数表示程序被另一个进程或操作系统中止,具体的值也可以表示不同的中止原因或中止码。

程序的退出状态码可以被其他程序或操作系统捕捉并处理,例如Shell脚本可以根据程序的退出状态码来执行不同的操作。因此,main函数的返回值是程序的一个重要部分,应该根据程序的需求来选择合适的返回值。

另外需要注意的是,如果main函数没有显式地返回任何值,编译器会默认返回0作为程序的退出状态码。因此,即使程序没有显示地指定返回值,也应该在main函数中显式地返回0以表示程序正常结束。

28.形参和实参的区别

形参出现在函数定义中,在整个函数体内都可以使用, 离开该函数则不能使用。

实参出现在主调函数中,进入被调函数后,实参变量也不能使用。

形参变量只有在被调用时才分配内存单元,在调用结束时, 即刻释放所分配的内存单元。因此,形参只有在函数内部有效。 函数调用结束返回主调函数后则不能再使用该形参变量。

29.结构体struct和联合体union的区别

内存分配方式:

  • 结构体struct中的各个成员会占用不同的内存空间,因此结构体的大小为各个成员大小的总和。
  • 联合体union中的各个成员共用同一块内存空间,因此联合体的大小为各个成员中占用内存最大的那个成员的大小。

数据存储方式:

  • 结构体struct中的各个成员在内存中是按照定义的顺序依次存储的,也就是说结构体的存储顺序与定义顺序相同。
  • 联合体union中的各个成员共用同一块内存空间,存储顺序是不确定的,取决于最后一次赋值的成员。

用途:

  • 结构体struct常用于描述一组相关的数据,例如一个人的姓名、年龄、性别等信息。
  • 联合体union常用于节省内存空间,例如可以用一个联合体表示一个变量可以是不同类型的数据。
30.以下程序中,主函数能否成功申请到内存空间?
#include<stdio.h>  
#include<stdlib.h>  
#include<string.h>  
void getmemory(char *p)  
{  
    p = (char *)malloc(100);  
    strcpy(p, "hello world");  
}  
int main()  
{  
    char *str = NULL;  
    getmemory(str);  
    printf("%s\n", str);  
    free(str);  
    return 0;  
} 

不能。

getmemory(str)没能改变str的值,因为传递给子函数的只是str的复制值NULL,main函数中的str一直都是 NULL。

正确的getmemory()如下:

①传递的是二重指针,即str的指针
void getmemory(char **p)   
{  
    *p = (char *)malloc(100);  
    strcpy(*p, "hello world");  
}  
②传递的是指针别名,即str的别名,C++中
void getmemory(char * &p)   
{  
    p = (char *)malloc(100);  
    strcpy(p, "hello world");  
}  
31.glibc的内存管理

glibc(GNU C Library)是Linux系统中常用的C语言标准库实现。在glibc中,内存管理是其中一个重要的组成部分,它提供了一些函数和机制来进行动态内存分配和释放。

以下是glibc中常用的内存管理函数:

  1. malloc:用于动态分配内存。它接受一个参数,即要分配的内存大小(以字节为单位),并返回一个指向分配内存区域的指针。如果分配失败,返回NULL

    void* malloc(size_t size);
    
  2. calloc:用于动态分配并初始化内存。它接受两个参数,分别是要分配的元素个数和每个元素的大小(以字节为单位)。它将分配的内存区域初始化为零,并返回一个指向分配内存区域的指针。

    void* calloc(size_t nmemb, size_t size);
    
  3. realloc:用于重新分配内存大小。它接受两个参数,一个是指向先前分配的内存区域的指针,另一个是要重新分配的大小(以字节为单位)。它将重新分配内存的大小,并返回一个指向重新分配内存区域的指针。

    void* realloc(void* ptr, size_t size);
    
  4. free:用于释放先前分配的内存。它接受一个指向要释放的内存区域的指针,并将该内存区域标记为可用。

    void free(void* ptr);
    

这些函数提供了基本的内存管理功能,允许在程序运行时动态地分配和释放内存。它们是在堆(Heap)上进行内存分配,与栈(Stack)上的自动变量不同。使用这些函数时,需要注意内存泄漏和越界访问等问题,确保正确地分配和释放内存,以避免资源浪费和程序错误。

此外,glibc还提供了其他一些内存管理相关的函数和特性,例如内存池(Memory Pool)和内存分配器(Allocator)。这些高级功能可以帮助提高内存分配和释放的性能和效率,但一般情况下,上述介绍的基本函数已经足够满足常规的内存管理需求。

32.malloc、realloc、calloc、alloca的区别

malloc函数:只分配内存空间,不进行初始化。

void* malloc(unsigned int num_size);
int *p = malloc(20*sizeof(int));申请20个int类型的空间;    

calloc函数:省去了人为空间计算;malloc申请的空间的值是随机初始化的,calloc申请的空间的值是初始化为0的;

void* calloc(size_t n,size_t size);
int *p = calloc(20, sizeof(int));

realloc函数:重新分配之前已经分配的内存空间。给动态分配的空间分配额外的空间,用于扩充容量。

void realloc(void *p, size_t new_size);

alloca是向栈申请内存,因此无需释放.

#嵌入式##八股#
【嵌入式八股】一、语言篇 文章被收录于专栏

查阅整理上千份嵌入式面经,将相关资料汇集于此,主要包括: 0.简历面试 1.语言篇【本专栏】 2.计算机基础 3.硬件篇 4.嵌入式Linux (建议PC端查看)

全部评论

相关推荐

双非坐过牢:非佬,可以啊10.28笔试,11.06评估11.11,11.12两面,11.19oc➕offer
点赞 评论 收藏
分享
点赞 2 评论
分享
牛客网
牛客企业服务