【最全】C面试题

1.基本数据类型和范围

C语言的基本数据类型包括charintfloatdouble

  • char:通常用于存储字符,其范围取决于编译器和系统,但通常为-128到127(以ASCII码为例)。

  • int:用于存储整数,其范围取决于编译器和操作系统,通常为-32,768到32,767(以16位整数为例)。

  • floatdouble:用于存储浮点数,其范围非常大,但具体数值取决于IEEE 754标准。

2.计算机如何存储浮点数的

C++中,浮点数的存储分为两种,float(单精度)和double(双精度)。

float占4字节,32位,double占8字节,64位,二者在内存中的存储形式如下:

无论是单精度还是双精度在存储中都分为三个部分:

   1. 符号位(Sign) : 0代表正,1代表为负
   2. 指数位(Exponent):用于存储科学计数法中的指数数据,并且采用移位存储
   3. 尾数部分(Mantissa):尾数部分
​
     存储规定:

按照国际标准IEEE 754,任意一个进制浮点数V可以表现成下面的形式:

V = (-1)^S * M * 2^E

    其中 (-1)^S 表示有效数字,当S为0时,V为正数,S为1时,V是负数。
    M表示有效数字,范围规定:大于1,小于2。
    2^E表示的是指数位。
    要将一个浮点数写成进制形式:一定需要S、M、E的值。

例如: 5.5 进制形式:

先将整数部分与小数部分分开换算: ​ 整数: 101 ​ 小数: 0.1 小数点后面 2^-1 = 1/2 = 0.5

    由此可以看出:V = (-1)^S * M * 2^E

5.5 --> 101.1 —> 1.011 * 2^2 ​ 所以 S = 0,M = 1.011,E = 2;

因为以上知道,

1.M是有效数字,范围:1<=M<2,在计算机内存保存M时,默认这个数第一位总是1,可以被舍去(在读取的时候,再把第一位加上),只保 留小数点后面的;如:当保留 101.1 时:只保存了 011,为了更方便理解.

2.E 是一个无符号数字(unsigned int)。 因为在8位的时候,其有范围0 ~ 255;而在11位的时候,其范围是 0 ~ 2047,但是由科学计数法知道,这时可以有负数的,(当存入 0.5 时,E = -1),所以在IEEE 754规定:存入内存时E的真实值需要加上一个中间值,对于8位来说其中间值是 127 ,对于11位来说其中间值是 1024. 对于指数E从内存中取出分为三种: 1.当E全为0时:也就是说指数E的真实值等于 -127,有效数字M不再加上1,而是0.xxxxxx这样的小数,这样做的目的是为了表示这样几乎很小很小的数,接近与0。 2.当E全为1时:也就是说这样的数表示无穷大(正负取决于S)。 3.当E有1有0时:即指数E的计算值减去127(或1024)得到真实值,然后由规定正数部分必须为1,(即M取出后加1)。

3.全局变量、局部变量、模块变量在内存空间中如何存放?

全局变量在全局空间分配。

局部变量在栈空间分配。

模块变量(通常指静态变量)也在全局空间分配,但其作用范围限制在定义它的模块内

4.内存详细分区

内存(在计算机科学中通常指的是主存储器或随机存取存储器,即RAM)在逻辑上可以被划分为几个不同的区域或段,这些区域或段用于不同的目的和存储不同的数据类型。然而,具体的分区方式可能因操作系统、处理器架构和应用程序的不同而有所差异。以下是一些常见的内存分区:

  1. 代码区(Code or Text Segment)

    • 存储程序执行的机器代码(即编译后的二进制代码)。

    • 通常是只读的,以防止程序意外修改自己的代码。

  2. 数据区(Data Segment)

    • 通常分为两个子区:初始化数据区(Initialized Data)和未初始化数据区(Uninitialized Data 或 BSS)。

    • 初始化数据区:存储程序中已明确赋值的全局变量和静态变量。

    • 未初始化数据区(BSS):存储未明确赋值的全局变量和静态变量,这些变量在程序开始执行前被初始化为零(或空)。

  3. 堆区(Heap)

    • 用于动态内存分配。当程序员使用如 malloccallocnew 等函数在运行时请求内存时,这些内存通常从堆中分配。

    • 程序员负责在不再需要这些内存时释放它们(使用如 freedelete 等函数),否则可能会导致内存泄漏。

  4. 栈区(Stack)

    • 用于存储局部变量和函数调用的信息(如返回地址、参数等)。

    • 由编译器自动分配和释放,其操作方式遵循“后进先出”(LIFO)的原则。

  5. 静态存储区

    • 通常与数据区重叠,但强调存储的是静态数据(即全局变量和静态变量)。

    • 这些变量的生命周期是整个程序的执行期间。

  6. 映射文件或共享内存区

    • 在某些操作系统中,可以将文件或其他对象的内存映射到进程的地址空间中。

    • 共享内存允许多个进程访问同一块物理内存,从而实现进程间的通信和数据共享。

  7. 其他内存区

    • 根据特定的操作系统、处理器架构或应用程序需求,还可能存在其他特定的内存区,如内存映射的I/O设备、图形缓冲区等。

5.内存的最小存储单位和最小计量单位分别是什么?

  • 内存的最小存储单位是二进制位(bit)。

  • 内存的最小计量单位是字节(byte),通常8位组成1字节。

6.函数指针与递归

  • 什么是函数指针?

    • 函数指针是指向函数的指针变量,可以用来调用函数或传递函数作为参数。

  • 解释递归函数

    • 递归函数是一个可以调用自身的函数。递归函数是一个可以调用自身的函数。在递归函数中,函数会反复调用自己,直到满足终止条件为止。

7.什么是结构体?

  • 结构体是一种用户自定义的数据类型,它可以包含多个不同类型的变量,这些变量可以通过一个结构名来访问。

8.C语言中如何进行字符串操作?

  • C语言提供了一系列字符串库函数,如strcpy()strcat()strlen()等,用于复制、连接、计算字符串长度等操作。

9.结构体为什么要内存对齐?

  • 结构体内存对齐是计算机内存管理中的一个重要概念,它对于程序的性能和正确性有着重要影响。以下是结构体内存对齐的主要原因:

  • 性能优化: CPU访问内存时,通常会以一定的块大小(例如4字节、8字节等)为单位进行读取。如果结构体中的数据成员没有对齐到这样的块边界,CPU可能需要执行额外的操作(如两次内存访问)来读取一个成员,这称为“未对齐访问”。未对齐访问会降低CPU的访问效率,增加程序的执行时间。 内存对齐可以确保结构体中的数据成员都位于合适的块边界上,从而提高CPU的访问效率。

  • 硬件限制: 某些硬件平台对内存访问有严格的对齐要求。如果结构体中的数据成员没有正确对齐,硬件可能会抛出异常或产生错误的结果。 通过内存对齐,可以确保结构体中的数据成员满足硬件平台的对齐要求,从而避免潜在的硬件问题。

  • 可移植性: 不同的编译器和硬件平台可能对内存对齐有不同的要求。如果不进行内存对齐,同一个结构体在不同的编译器或硬件平台上可能会有不同的内存布局和大小。 通过使用标准的数据类型和编译器特定的对齐指令(如#pragma pack),可以确保结构体在不同平台上的内存布局和大小一致,从而提高程序的可移植性。

  • 空间利用率: 虽然内存对齐可能会增加结构体所占用的总内存空间(因为编译器会在成员之间插入填充字节以确保对齐),但在某些情况下,这可以提高内存访问的效率并减少缓存未命中的可能性。 此外,合理的内存对齐还可以帮助减少内存碎片,提高内存管理的效率。

  • 简化编译器实现: 对于编译器来说,处理未对齐的内存访问需要额外的逻辑和复杂性。通过要求结构体进行内存对齐,可以简化编译器的实现并减少潜在的错误。 综上所述,结构体内存对齐是为了提高程序的性能、确保硬件兼容性、提高可移植性、优化空间利用率以及简化编译器实现。在编写涉及结构体的代码时,应该考虑内存对齐的问题,并遵循相关的编程规范和最佳实践。

10.结构体与联合体的区别?

  • 什么是结构体?

    • 结构体是一种用户自定义的数据类型,用于将不同类型的数据项组合成一个单独的数据单元。

  • 什么是联合体?

    • 联合体(union)是一种特殊的数据结构,允许在相同的内存位置存储不同类型的数据。但联合体的大小至少是它最大成员的大小。

11.C语言中如何进行文件操作?

  • C语言提供了一系列文件操作函数,如fopen()fclose()fread()fwrite()等,用于打开、关闭、读取和写入文件。

12.如何在C语言中实现动态内存分配?

  • 动态内存分配是指在程序运行时根据需要动态地分配内存空间。C语言中的动态内存分配函数包括malloc()calloc()realloc(),通过这些函数可以在程序运行时根据需求分配或释放内存。

13.malloc底层分配的原理

  • 基本概念 malloc的作用是在堆区(heap)上分配指定大小的连续内存空间,并返回指向该空间的指针。 堆区是程序运行时动态分配内存的区域,与静态区(存放全局变量、静态变量等)、栈区(存放局部变量、函数参数等)不同。

  • 分配步骤: 初始化内存池:malloc首次调用时,通常会初始化内存池。内存池是预先分配的一大块内存空间,用于满足后续内存分配请求。初始化过程包括从操作系统请求内存(如使用sbrk或mmap系统调用),并建立数据结构来跟踪可用的内存块(称为free list)。 查找合适的内存块:当malloc收到内存分配请求时,它会在free list中查找一个大小满足需求的内存块。内存块查找策略可能有所不同,如首次适配(first fit)、最佳适配(best fit)或最差适配(worst fit)等。策略选择会影响内存分配的性能和内存碎片化程度。如果找不到足够大小的内存,它会从新向操作系统申请,申请大小小于128KB用brk,大于128KB时用mmap。( "brk"是"break"的缩写,它是一个系统调用,用于改变程序的堆空间大小。) 分割内存块:如果找到的内存块大小远大于请求的内存大小,malloc可能会将其分割成两部分。一部分用于满足当前请求,另一部分保留在free list中以供后续分配使用。 更新数据结构:malloc将找到的内存块从free list中移除,并更新相关的数据结构。此外,malloc通常会在返回的内存块前附加一些元数据(如内存块大小),以便于后续的内存释放(free)和重新分配(realloc)操作。 返回内存块地址:malloc返回分配的内存块地址,供程序使用。需要注意的是,分配的内存块内容可能是未初始化的,需要在使用前进行适当的初始化操作

    所以 malloc 申请内存的时候,会有两种方式向操作系统申请堆内存。 方式一:通过 brk() 系统调用从堆分配内存 方式二:通过 mmap() 系统调用在文件映射区域分配内存;

    1.、brk() 函数与mmap()函数 如果用户分配的内存小于 128 KB,则通过 brk() 申请内存; 如果用户分配的内存大于 128 KB,则通过 mmap() 申请内存;

    方式一、实现的方式很简单,就是通过 brk() 函数将「堆顶」指针向高地址移动,获得新的内存空间。

    malloc 通过 brk() 方式申请的内存,free 释放内存的时候,并不会把内存归还给操作系统,而是缓存在 malloc 的内存池中,待下次使用,这样就可以重复使用。

    优点 malloc 通过 brk() 系统调用在堆空间申请内存的时候,由于堆空间是连续的,所以直接预分配更大的内存来作为内存池,当内存释放的时候,就缓存在内存池中。 等下次在申请内存的时候,就直接从内存池取出对应的内存块就行了,而且可能这个内存块的虚拟地址与物理地址的映射关系还存在,这样不仅减少了系统调用的次数,也减少了缺页中断的次数,这将大大降低 CPU 的消耗 缺点:由于申请的内存没有归还系统,在内存工作繁忙时,频繁的内存分配和释放会造成内存碎片。brk()方式之所以会产生内存碎片,是由于brk通过移动堆顶的位置来分配内存,并且使用完不会立即归还系统,重复使用,如果高地址的内存不释放,低地址的内存是得不到释放的。

    方式二 、通过 mmap() 系统调用中「私有匿名映射」的方式,在文件映射区分配一块内存,也就是从文件映射区“偷”了一块内存。如下图:

    优点 1、对文件的读取操作跨过了页缓存,减少了数据的拷贝次数,用内存读写取代了I/O读写,提高了读取的效率。 2、实现了用户空间和内核空间的高校交互方式,两空间的各自修改操作可以直接反映在映射的区域内,从而被对方空间及时捕捉。 3、提供进程间共享内存及互相通信的方式。不管是父子进程还是无亲缘关系进程,都可以将自身空间用户映射到同一个文件或者匿名映射到同一片区域。从而通过各自映射区域的改动,打到进程间通信和进程间共享的目的。 缺点 申请内存的操作应该避免频繁的系统调用,如果都用 mmap 来分配内存,等于每次都要执行系统调用。 另外,因为 mmap 分配的内存每次释放的时候,都会归还给操作系统,于是每次 mmap 分配的虚拟地址都是缺页状态的,然后在第一次访问该虚拟地址的时候,就会触发缺页中断。 频繁通过 mmap 分配的内存话,不仅每次都会发生运行态的切换,还会发生缺页中断(在第一次访问虚拟地址后),这样会导致 CPU 消耗较大。

  • mmap实现原理 普通读写与mmap对比 在unix/linux平台下读写文件,一般有两种方式。第一种是首先open文件,接着使用read系统调用读取文件的全部或一部分。于是内核将文件的内容从磁盘上读取到内核页高速缓冲(也即pageCache),再从内核高速缓冲读取到用户进程的地址空间。而写的时候,需要将数据从用户进程拷贝到内核高速缓冲,然后在从内核高速缓冲把数据刷到磁盘中,那么完成一次读写就需要在内核和用户空间之间做四次数据拷贝。而且当多个进程同时读取一个文件时,则每一个进程在自己的地址空间都有这个文件的副本,这样也造成了物理内存的浪费

mmap 是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用 read、write 等系统调用函数。

mmap内存映射实现过程 (一)进程启动映射过程,并在虚拟地址空间中为映射创建虚拟映射区域** 1、进程在用户空间调用库函数mmap,原型:void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset); 2、在当前进程的虚拟地址空间中,寻找一段空闲的满足要求的连续的虚拟地址 3、为此虚拟区分配一个vm_area_struct结构,接着对这个结构的各个域进行了初始化 4、将新建的虚拟区结构(vm_area_struct)插入进程的虚拟地址区域链表或树中 (二)调用内核空间的系统调用函数mmap(不同于用户空间函数),实现文件物理地址和进程虚拟地址的一一映射关系 5、为映射分配了新的虚拟地址区域后,通过待映射的文件指针,在文件描述符表中找到对应的文件描述符,通过文件描述符,链接到内核“已打开文件集”中该文件的文件结构体(struct file),每个文件结构体维护着和这个已打开文件相关各项信息。 6、通过该文件的文件结构体,链接到file_operations模块,调用内核函数mmap,其原型为:int mmap(struct file *filp, struct vm_area_struct *vma),不同于用户空间库函数。 7、内核mmap函数通过虚拟文件系统inode模块定位到文件磁盘物理地址。 8、通过remap_pfn_range函数建立页表,即实现了文件地址和虚拟地址区域的映射关系。此时,这片虚拟地址并没有任何数据关联到主存中。 (三)进程发起对这片映射空间的访问,引发缺页异常,实现文件内容到物理内存(主存)的拷贝 注:前两个阶段仅在于创建虚拟区间并完成地址映射,但是并没有将任何文件数据的拷贝至主存。真正的文件读取是当进程发起读或写操作时。 9、进程的读或写操作访问虚拟地址空间这一段映射地址,通过查询页表,发现这一段地址并不在物理页面上。因为目前只建立了地址映射,真正的硬盘数据还没有拷贝到内存中,因此引发缺页异常。 10、缺页异常进行一系列判断,确定无非法操作后,内核发起请求调页过程。 11、调页过程先在交换缓存空间(swap cache)中寻找需要访问的内存页,如果没有则调用nopage函数把所缺的页从磁盘装入到主存中。 12、之后进程即可对这片主存进行读或者写的操作,如果写操作改变了其内容,一定时间后系统会自动回写脏页面到对应磁盘地址,也即完成了写入到文件的过程。

mmap 的适用场景 mmap 的适用场景实际上非常受限,在如下场合下可以选择使用 mmap 机制: 多个线程以只读的方式同时访问一个文件,这是因为 mmap 机制下多线程共享了同一物理内存空间,因此节约了内存。案例:多个进程可能依赖于同一个动态链接库,利用 mmap 可以实现内存仅仅加载一份动态链接库,多个进程共享此动态链接库。 mmap 非常适合用于进程间通信,这是因为对同一文件对应的 mmap 分配的物理内存天然多线程共享,并可以依赖于操作系统的同步原语; mmap 虽然比 sendfile 等机制多了一次 CPU 全程参与的内存拷贝,但是用户空间与内核空间并不需要数据拷贝,因此在正确使用情况下并不比 sendfile 效率差

14.malloc()calloc()realloc()的区别

** malloc()**

malloc() 函数用于分配指定字节数的内存块。它返回一个指向该内存块的指针,如果内存分配失败,则返回 NULL。使用 malloc() 分配的内存区域的内容是未初始化的,这意味着内存区域可能包含垃圾值。

calloc()

calloc() 函数也用于分配内存,但它同时初始化分配的内存块为零。此外,它接受两个参数:要分配的元素的数量和每个元素的大小。

realloc()

realloc() 函数用于更改先前分配的内存块的大小。它接受两个参数:一个指向要重新分配的内存块的指针和新的大小(以字节为单位)。

15.什么是内存泄漏?如何避免?

  • 内存泄漏是指程序在运行时动态分配的内存未被正确释放,导致可用内存逐渐减少。

  • 避免内存泄漏的方法包括在不再需要动态分配的内存时使用free()函数释放,并确保每个malloc()calloc()realloc()调用都有对应的free()调用。

16.内存池

内存池(Memory Pool)是一种高效的内存分配机制,通过预先申请并管理一定数量的内存块,以避免频繁的内存分配和释放操作带来的开销,减少内存碎片,提高内存使用效率。以下是对内存池的详细简述:

  1. 基本概念

  • 内存池在真正使用内存之前,会预先申请分配一定数量的、大小相等的内存块作为备用。

  • 当有新的内存需求时,直接从内存池中分配出内存块,而不需要每次都向操作系统申请。

  1. 工作原理

  • 初始化:从操作系统申请一块连续的物理内存作为内存池。

  • 分区:将内存池按照固定大小分成多个内存块。

  • 管理:使用链表、栈或其他数据结构将内存块连接起来,形成一个内存块池。

  • 分配:当需要分配内存时,从内存块池中获取一个内存块,并将其标记为已分配。

  • 释放:当不再需要该内存块时,将其标记为未分配,并放回到内存块池中。

  • 扩展:当内存块池中无可用内存块时,可以选择动态扩展内存池。

  1. 优点

  • 提高性能:减少了频繁的内存分配和释放操作,降低了内存分配器的调用次数,提高了性能。

  • 减少内存碎片:通过预先分配固定大小的内存块,避免了由于内存块大小不定而导致的内存碎片问题。

  • 便于管理:内存池中的内存块以块的形式进行管理,便于进行分配和回收。

  1. 应用场景

  • 需要频繁分配和释放小块内存的情况,如字符串的拼接、复制和删除等操作。

  • 需要大量内存分配和释放的情况,如数据处理、文件读写等操作。

  • 需要长时间运行的程序,如服务器、游戏等,需要稳定、高效的内存管理。

  • 需要减少内存碎片的情况,如嵌入式系统、实时操作系统等。

  1. 类型

  • 固定大小内存池:申请的内存单元是固定大小,例如提供大小为8 Byte、16 Byte和24 Byte的内存单元。优点是效率高,便于管理和分配;缺点是可能造成内存空间的浪费。

  • 可变大小内存池:根据申请的大小进行分配,实行“要多少就给多少”的内存分配方法。优点是充分利用内存空间,不会造成太多的资源浪费;缺点是效率低、内存池管理困难。

  1. 注意事项

  • 当使用内存池时,需要注意内存块的分配和回收策略,避免内存泄漏和内存碎片问题。

  • 根据应用程序的需求和特性,选择合适的内存池类型和实现方式。

  • 在某些情况下,可能需要重载newdelete等内存分配和释放函数,以与内存池进行结合使用。

#include <iostream>  
#include <vector>  
#include <cstdlib>  
  
// 内存块的结构  
struct MemoryBlock {  
    // 指向下一个空闲块的指针(用于链表)  
    MemoryBlock* next;  
};  
  
// 内存池类  
class MemoryPool {  
private:  
    // 空闲内存块链表头  
    MemoryBlock* freeList;  
    // 内存块的大小(包括结构体本身的大小)  
    size_t blockSize;  
    // 已分配的内存块数量  
    size_t allocatedBlocks;  
    // 预先分配的内存块数量  
    size_t poolSize;  
  
    // 分配一个新的内存块  
    MemoryBlock* allocateBlock() {  
        // 这里简化处理,直接分配内存,实际应用中可能需要更复杂的内存管理  
        MemoryBlock* block = static_cast<MemoryBlock*>(std::malloc(blockSize));  
        if (!block) {  
            throw std::bad_alloc();  
        }  
        return block;  
    }  
  
public:  
    // 构造函数  
    MemoryPool(size_t blockSize, size_t poolSize)   
        : freeList(nullptr), blockSize(sizeof(MemoryBlock) + blockSize), allocatedBlocks(0), poolSize(poolSize) {  
        // 预先分配内存块  
        for (size_t i = 0; i < poolSize; ++i) {  
            MemoryBlock* block = allocateBlock();  
            block->next = freeList;  
            freeList = block;  
        }  
    }  
  
    // 析构函数  
    ~MemoryPool() {  
        // 释放所有内存块  
        while (freeList) {  
            MemoryBlock* block = freeList;  
            freeList = freeList->next;  
            std::free(block);  
        }  
    }  
  
    // 从内存池中分配内存  
    void* allocate() {  
        if (!freeList) {  
            // 如果没有空闲块,可以选择抛出异常或动态扩展内存池  
            throw std::bad_alloc();  
        }  
  
        // 从空闲链表头部取出一个块  
        MemoryBlock* block = freeList;  
        freeList = freeList->next;  
        ++allocatedBlocks;  
  
        // 返回指向用户数据的指针(跳过MemoryBlock结构体本身)  
        return reinterpret_cast<char*>(block) + sizeof(MemoryBlock);  
    }  
  
    // 释放内存到内存池  
    void deallocate(void* ptr) {  
        if (!ptr) {  
            return;  
        }  
  
        // 计算回MemoryBlock结构体的指针  
        MemoryBlock* block = reinterpret_cast<MemoryBlock*>(reinterpret_cast<char*>(ptr) - sizeof(MemoryBlock));  
  
        // 将块添加到空闲链表头部  
        block->next = freeList;  
        freeList = block;  
        --allocatedBlocks;  
    }  
  
    // 获取已分配的内存块数量  
    size_t getAllocatedBlocks() const {  
        return allocatedBlocks;  
    }  
  
    // 获取空闲的内存块数量  
    size_t getFreeBlocks() const {  
        return poolSize - allocatedBlocks;  
    }  
};  
  
int main() {  
    try {  
        // 创建一个内存池,每个块大小为1024字节,预分配10个块  
        MemoryPool pool(1024, 10);  
  
        // 分配内存  
        void* ptr1 = pool.allocate();  
        void* ptr2 = pool.allocate();  
  
        // ... 使用ptr1和ptr2指向的内存 ...  
  
        // 释放内存  
        pool.deallocate(ptr1);  
        pool.deallocate(ptr2);  
  
        // 输出已分配和空闲的内存块数量  
        std::cout << "Allocated blocks: " << pool.getAllocatedBlocks() << std::endl;  
        std::cout << "Free blocks: " << pool.getFreeBlocks() << std::endl;  
    } catch (const std::bad_alloc& e) {  
        std::cerr << "Out of memory!" << std::endl;  
    }  
  
    return 0;  
}
这个示例代码展示了如何创建一个简单的固定大小内存池。内存池在初始化时预先分配了一定数量的内存块,并通过链表管理这些内存块。allocate方法用于从内存池中分配内存,deallocate方法用于将内存释放回内存池。内存池还提供了获取已

17.malloc 分配的空间是连续的嘛

malloc 函数在 C 和 C++ 中用于动态内存分配。其底层实现通常依赖于操作系统和具体的 C 库(如 glibc, BSD libc, MSVC 的运行时库等)。然而,从一般意义上讲,当 malloc 分配一块内存时,它试图为用户提供一个连续的内存块。

这里的“连续”是指从逻辑上或虚拟地址空间上看,分配的内存块是连续的。但实际上,物理内存可能不是连续的,因为现代操作系统使用虚拟内存管理,可以将不连续的物理内存页映射到连续的虚拟地址空间。

以下是关于 malloc 的一些重要点:

  1. 内存碎片:随着程序运行,频繁的 mallocfree 调用可能会导致内存碎片。这意味着即使有很多未使用的内存,但由于它们被分割成小的、不连续的部分,malloc 可能无法为一个大请求提供足够的连续内存。

  2. 内存对齐malloc 分配的内存通常满足一定的对齐要求,这对于某些硬件操作(如 SIMD 指令)是必需的。

  3. 内存池:为了优化小对象的分配和释放,一些 malloc 实现使用内存池。这意味着它们会预先分配一大块内存,并在其中管理小对象的分配和释放,以减少与操作系统的交互次数。

  4. 多线程和并发:在多线程环境中,malloc 实现通常是线程安全的,并使用各种技术(如锁、原子操作、TLS 等)来确保并发访问时的正确性。

  5. 内存管理工具:除了 mallocfree 之外,还有一些更高级的内存管理工具(如 jemalloc, tcmalloc 等),它们提供了更好的性能和特性。

总之,虽然 malloc 在逻辑上为用户提供连续的内存块,但实际的物理内存布局可能不是连续的,这取决于操作系统的虚拟内存管理和其他因素。

18.静态数组与动态数组

静态数组(Static Array)和动态数组(Dynamic Array)在编程中是两种不同的数组类型,它们在内存分配、大小和生命周期等方面有所不同。

静态数组

静态数组是在编译时分配固定大小的数组。一旦定义,其大小就不能改变。它们通常在栈上分配(尽管在某些情况下,如C语言中的全局数组,它们可能在静态存储区或数据段中分配)。

特点

  • 大小固定:在定义时必须指定数组的大小,之后不能改变。

  • 分配在栈上(通常):对于局部变量,它们通常在栈上分配,这意味着它们的生命周期与它们所在的函数或代码块相同。

  • 高效访问:由于元素在内存中是连续存储的,因此访问静态数组中的元素通常非常快。

示例(C语言):

c复制代码
​
int staticArray[10]; // 声明一个大小为10的静态数组

动态数组

动态数组是一种可以在运行时改变大小的数组。它们通常在堆上分配,并使用某种机制(如指针和重新分配内存)来管理其大小。

特点

  • 大小可变:可以在运行时动态地增加或减少数组的大小。

  • 分配在堆上:通常使用new(C++)、malloc(C)或类似的函数在堆上分配内存。这意味着动态数组的生命周期可以跨越多个函数或代码块,直到显式释放内存。

  • 可能存在性能开销:由于动态数组可能需要重新分配内存和复制元素,因此在增加数组大小时可能会产生一些性能开销。

  • 访问效率:与静态数组相同,动态数组中的元素在内存中是连续存储的(在重新分配之后),因此访问效率通常很高。

示例(C++):

std::vector<int> dynamicArray; // 声明一个动态数组(实际上是一个vector)  
dynamicArray.push_back(1);     // 添加元素到动态数组  
// ... 可以继续添加元素或删除元素 ...

在C++中,std::vector是一种非常常用的动态数组实现,它提供了方便的接口来管理数组的大小和元素。然而,在C语言中,没有内置的动态数组类型,但可以使用指针和malloc/free函数来手动管理动态内存分配。

19.编译与预处理

  • 描述一下gcc的编译过程?

    • gcc编译过程分为预处理、编译、汇编、链接四个阶段。预处理包括头文件包含、宏替换、条件编译、删除注释等;编译将预处理好的文件转换成汇编文件;汇编将汇编文件转换成二进制目标文件;链接将项目中的各个二进制文件、所需库和启动代码链接成可执行文件。

  • 什么是预处理器?

    • 预处理器是一个独立于编译器的程序,对源代码进行预处理,包括宏展开、条件编译等操作。预处理器会在源文件被编译之前执行。

20.什么是指针?

  • 指针是C语言中一种特殊的数据类型,用于存储其他数据类型的内存地址。通过指针可以直接访问和修改内存中的数据。

  • 指向常量的指针和常指针的区别?

    • 指向常量的指针(如const int *p)所指向的内容不能被修改,但指针本身可以指向其他地址。

    • 常指针(如int *const p)指向的内容可以被修改,但指针本身不能再指向其他地址。

    • 什么是野指针?如何避免?

    • 野指针是指已被释放或未初始化的指针,访问野指针可能导致程序崩溃或不可预测的行为。避免野指针的方法包括在释放指针后将其置为NULL,确保在使用指针前已正确初始化等。

    • 野指针和悬空指针

      • 野指针指的是未初始化的指针,指向未知的内存地址。->及时初始化或者赋空值

      • 悬空指针指的是释放所指向空间后没有指向空的指针。->释放空间后及时赋空

    • 指针和引用的区别

      1. 可变性:指针可以改变它所指向的内存地址,而引用一旦初始化就不能再改变。

      2. 可空性:指针可以为空(nullptr),而引用必须在定义时立即初始化,不能为空。

      3. 解引用:要访问指针所指向的数据,你需要使用解引用操作符(*),而引用则不需要。

      4. 安全性:由于指针可以进行算术运算和可以被设置为空,它们在某些情况下可能导致安全问题,如野指针和内存泄漏。而引用则更安全,因为它们不能为空且不能被重新赋值。

      5. 用途:指针通常用于动态内存分配、数据结构(如链表、树等)的实现以及低级编程任务。而引用则更常用于函数参数传递(特别是当需要修改函数外部变量的值时)和大型对象的传递(以避免复制的开销)。

21.头文件和库文件的作用?

  • 头文件包含一组函数声明、宏定义和结构体声明等信息,用于在源文件中引用。库文件则包含已编译的函数实现和数据,通过链接库文件可以在程序中使用这些函数和数据。

22.数组和结构体的区别

数组是一组相同类型的数据元素的集合,这些数据元素在内存中按照一定的顺序连续存储。数组的每个元素都通过索引来访问,索引通常是连续的整数。

结构体是一种复合数据类型,允许你将不同类型的数据项组合成一个单一的类型。结构体中的每个数据项(称为成员或字段)可以是不同的类型,并且可以有不同的名称。

数组与结构体的区别

  1. 数据类型:数组的所有元素都是同一类型,而结构体可以包含多种不同类型的成员。

  2. 存储方式:数组的元素在内存中连续存储,而结构体的成员在内存中按照它们各自的类型和顺序存储(不一定连续)。

  3. 大小:数组的大小在定义时是固定的(静态数组),而结构体的大小取决于其成员的类型和数量,可以在定义时确定也可以在运行时动态变化(如果结构体中包含动态数组或指针)。

  4. 用途:数组主要用于存储和处理大量同类型的数据,而结构体主要用于表示具有多个属性的对象或实体。

  5. 访问方式:数组通过索引访问其元素,而结构体通过成员名访问其成员。

23.数组和链表的区别

数组是一组相同类型的数据元素的集合,这些数据元素在内存中按照一定的顺序连续存储。数组的每个元素都通过索引来访问,索引通常是连续的整数。

链表是一种线性数据结构,它的元素(称为节点)在内存中不是连续存储的,而是通过指针或引用链接在一起。每个节点包含数据部分和指向下一个节点的指针部分。

数组与链表的比较

  • 访问方式:数组通过索引直接访问元素,时间复杂度为O(1);链表需要从头节点开始逐个遍历节点,时间复杂度为O(n)。

  • 插入和删除操作:在数组的前端或中间插入或删除元素时,可能需要移动大量元素,时间复杂度较高;而在链表中插入或删除节点通常只需要改变相关节点的指针,时间复杂度较低。

  • 空间利用率:数组的元素在内存中连续存储,空间利用率高;链表由于需要存储指针或引用,空间利用率可能较低。

  • 适用场景:数组适用于需要频繁访问元素且元素数量固定不变的场景;链表适用于需要动态改变元素数量且插入和删除操作频繁的场景。

24.define和inline区别?(宏和内联)

  • define是C中就存在的,作用是预处理;inline是C++中新引入的关键字,用来在函数被调用的位置进行函数展开。

  • 宏在预处理阶段完成替换,相当于代码插入的操作;内联函数本质上是一个函数,在编译时完成在函数调用位置的展开,相比之下inline是具有类型检查和返回值检查的。

  • inline只是提供给编译器的一个优化建议,是否使用内联由编译器决定;而宏会强制进行代码的替换。

  • 宏定义参数没有类型,不进行类型检查,需要注意括号,可能产生歧义;内联函数参数具有类型,需要检查类型,不会产生歧义。

25.define和typedef区别?

  • define预处理阶段完成替换,typedef编译阶段进行替换。

  • define没有类型,只是简单的替换,而typedef会进行类型检查

  • define不是语句,不需要加分号,typedef需要加分号

  • 二者对指针的定义区别很大。

  • 一般define用来定义常量或者书写复杂的内容;typedef一般用于给类型起别名

26.define和const区别?

  • define预处理阶段,const编译阶段

  • define不进行类型检查,const会进行类型检查

  • define定义的常量是全局的,const有自己的作用域

  • define只是简单的文本替换,需要注意表达式的括号使用,而且没有实际的变量存储,不占用内存,而const定义的常量是要占用内存空间的。

27.声明和定义的区别

  • 声明仅仅是把变量的声明的位置及类型提供给编译器,并不分配内存空间;定义要在定义的地方为其分配存储空间。

  • 相同变量可以在多处声明(外部变量extern),但只能在一处定义。

28.C语言中的breakcontinue语句有什么不同?

break语句和continue语句都是控制流语句,但它们的作用不同:

break语句用于终止循环(forwhiledo-while循环)或switch语句块的执行,并跳出当前的循环或switch语句。

continue语句用于提前结束当前循环迭代,跳过循环体余下的语句,直接开始下一轮循环。

29.请解释一下C语言中的指针数组和数组指针。它们有什么不同?

指针数组: 指针数组是一个数组,其中的每个元素都是一个指针。这意味着每个元素可以指向一个不同的内存位置。这些指针可以指向不同类型的数据,如整数、字符、结构体等。通常,指针数组用于存储一组指针,每个指针可以指向一个独立的数据对象。

数组指针: 数组指针是一个指针,它指向一个数组。数组指针本身并不存储数据,而是指向一个数组的首元素。数组指针可以通过指针算术运算遍历数组的元素。数组指针通常用于在函数中传递数组,或者用于动态分配多维数组。

30.什么是C语言中的位运算符?请解释一下&、|和^运算符。

按位与(&)运算符: 按位与运算符将两个操作数的对应位进行逻辑与操作。如果两个对应位都为1,则结果位为1,否则为0。

按位或(|)运算符: 按位或运算符将两个操作数的对应位进行逻辑或操作。如果两个对应位中至少有一个为1,则结果位为1,否则为0。

按位异或(^)运算符: 按位异或运算符将两个操作数的对应位进行逻辑异或操作。如果两个对应位不相同,则结果位为1,否则为0。

31.C语言中Volatile的作用

在C语言中,volatile关键字用于告诉编译器不要对该变量的访问进行优化,即每次对该变量的读写都应当直接从其内存地址中读取或写入,而不是从CPU的寄存器中读取或写入。这是因为volatile变量可能会被程序之外的因素(如硬件、中断服务程序等)所修改,而编译器在不知道这些外部修改的情况下,可能会做出一些错误的优化。

以下是volatile关键字的一些主要用途:

  1. 硬件寄存器访问:当C代码需要直接访问硬件寄存器时,这些寄存器的内容可能会在任何时候被硬件修改。使用volatile可以确保每次读取或写入都是从/到实际的寄存器地址,而不是从可能被编译器优化的临时存储位置。

  2. 中断服务程序:在中断服务程序中,一个变量可能会被中断处理程序修改。如果这个变量不是volatile的,那么编译器可能会将其保存在寄存器中,从而不会看到中断服务程序对其所做的修改。

  3. 多线程/多任务环境:在多线程或多任务环境中,一个线程可能会修改一个变量,而另一个线程可能会读取这个变量。使用volatile可以确保每次读取都是从内存中读取最新的值,而不是从可能被编译器优化的临时存储位置。但请注意,在多线程环境中,仅仅使用volatile并不足以保证线程安全。对于复杂的线程间同步和通信,通常需要使用更高级的同步原语,如互斥锁、条件变量等。

  4. 内存映射的I/O:当使用内存映射的I/O时(即将设备的I/O端口映射到内存地址),对这些内存地址的访问应当使用volatile,以确保每次读写都是从实际的设备地址中进行的。

需要注意的是,虽然volatile可以确保对变量的每次访问都是直接从其内存地址中进行的,但它并不能保证操作的原子性。也就是说,如果一个操作(如自增操作)包含多个步骤(如读取-修改-写入),那么在这个操作进行的过程中,变量可能会被其他线程或中断服务程序修改,从而导致数据的不一致。在这种情况下,可能需要使用更复杂的同步机制来确保操作的原子性。

  • 43
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

爱编程的小猴

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值