二十万字C/C++、嵌入式软开面试题全集宝典六

2021/10/4 17:10:49

本文主要是介绍二十万字C/C++、嵌入式软开面试题全集宝典六,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

目录

101、 字节对齐有什么作用?

102、 C语言中#pragma用法

103、 new和malloc的区别?

104、 malloc/calloc/realloc三者之间的区别?

105、 delete p;与delete[]p,allocator

106、 new和delete的实现原理,delete是如何知道释放内存的大小?

107、 malloc申请的存储空间能用delete释放吗

108、 函数参数入栈的顺序

109、 堆和栈区别

110、 堆与栈的优点和缺点

111、 内核空间 虚拟内存管理

112、 malloc与free的实现原理?

113、 malloc、realloc、calloc的区别

114、 __stdcall和__cdecl的区别?

115、 手写字符串函数 strcat, strcpy, strncpy, memset, memcpy实现

116、 使用智能指针管理内存资源,RAII

117、 手写实现智能指针类

118、 结构体变量比较是否相等

119、 位运算

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


 

101、 字节对齐有什么作用?

字节对齐的作用不仅是便于cpu快速访问,同时合理的利用字节对齐可以有效地节省存储空间。
编译器中提供了#pragma pack(n)来设定变量以n字节对齐方式。n字节对齐就是说变量存放的起始地址的偏移量有两种情况:第一、如果n大于等于该变量所占用的字节数,那么偏移量必须满足默认的对齐方式,第二、如果n小于该变量的类型所占用的字节数,那么偏移量为n的倍数,不用满足默认的对齐方式。

 

102、 C语言中#pragma用法

1.#pragma message
#pragma message("消息文本") 当编译器遇到这条指令时,就在编译输出窗口中将消息文本打印出来。
2.#pragma code_seg
#pragma code_seg(["section-name"["section-class"]])
它能够设置程序中函数代码存放的代码段。当我们开发驱动程序时便就会使用到它。
3.#pragma once
只要在头文件的最开始加入这条指令就能够头文件被编译一次。
4.#pragma hdrstop
表示编译头文件到此为止,后面的头文件不进行预编译。
5.#pragma resouce
#pragma resouce"*.dfm"表示*.dfm文件中的资源加入工程。*.dfm中包括了外观定义。
6.#pragma warning
#pragma warning (disable:4507 34; once:4385; error:164) 等价于
#pragma warning (disable:4507 34) //不显示4507和30号警告信息
#pragma warning (once:4385) //4358号警告信息仅报告一次
#pragma warning (error:164) //把164号警告信息作为一种错误
7.#pragma comment
#pragma comment(...) 该指令将一个注释放入一个对象文件或可执行文件中,常用lib关键字帮我们链入一个库文件。
如:#pragma comment(lib,"user32.lib") 该指令用来将user32.lib库文件加入到本工程中。
8. #pragma pack
这条指令主要用作改变编译器的默认对齐方式。

103、 new和malloc的区别?

1.new/delete是C++关键字,需要编译器支持。malloc/free是库函数,需要头文件支持;
2.使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算。而malloc则需要显式地指出所需内存的尺寸。
3.new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符。而malloc内存分配成功则是返回void * ,需要通过强制类型转换将void*指针转换成我们需要的类型。
4.new内存分配失败时,会抛出bac_alloc异常。malloc分配内存失败时返回NULL。
5.new会先调用operator new函数,申请足够的内存(通常底层使用malloc实现)。然后调用类型的构造函数,初始化成员变量,最后返回自定义类型指针。delete先调用析构函数,然后调用operator delete函数释放内存(通常底层使用free实现)。malloc/free是库函数,只能动态的申请和释放内存,无法强制要求其做自定义类型对象构造和析构工作。

104、 malloc/calloc/realloc三者之间的区别?

1)void *malloc(size_t size);
size表示要分配的字节数,其中要检测空间是否开辟成功,开辟失败时返回0。
作用:在内存中分配一个元素被初始化为0的数组。
2)void *calloc(size_t num, size_t size);
num表示元素的个数,size表示每个元素的大小
返回值:返回一个指向所分配空间的void指针。
作用:重新分配内存块
3)void *realloc(void* memblock,size_t size);
memblock指向原先分配的内存块,size表示新的内存块的字节大小。
返回值:返回一个指向重新分配(可能移动了)的内存块的大小。
注意:堆上的内存需要用户自己来管理,动态malloc/calloc/realloc的空间,必须free掉,否则会造成内存泄漏

105、 delete p;与delete[]p,allocator

1.动态数组管理new一个数组时,[]中必须是一个整数,但是不一定是常量整数,普通数组必须是一个常量整数;
2.new动态数组返回的并不是数组类型,而是一个元素类型的指针;
3.delete[]时,数组中的元素按逆序的顺序进行销毁;
4.new在内存分配上面有一些局限性,new的机制是将内存分配和对象构造组合在一起,同样的,delete也是将对象析构和内存释放组合在一起的。allocator将这两部分分开进行,allocator申请一部分内存,不进行初始化对象,只有当需要的时候才进行初始化操作。

106、 new和delete的实现原理,delete是如何知道释放内存的大小?

1.new简单类型直接调用operator new分配内存;而对于复杂结构,先调用operator new分配内存,然后在分配的内存上调用构造函数;对于简单类型,new[]计算好大小后调用operator new;对于复杂数据结构,new[]先调用operator new[]分配内存,然后在p的前四个字节写入数组大小n,然后调用n次构造函数,针对复杂类型,new[]会额外存储数组大小;
○1new表达式调用一个名为operator new(operator new[])函数,分配一块足够大的、原始的、未命名的内存空间;
○2编译器运行相应的构造函数以构造这些对象,并为其传入初始值;
○3对象被分配了空间并构造完成,返回一个指向该对象的指针。
2.delete简单数据类型默认只是调用free函数;复杂数据类型先调用析构函数再调用operator delete;针对简单类型,delete和delete[]等同。假设指针p指向new[]分配的内存。因为要4字节存储数组大小,实际分配的内存地址为[p-4],系统记录的也是这个地址。delete[]实际释放的就是p-4指向的内存。而delete会直接释放p指向的内存,这个内存根本没有被系统记录,所以会崩溃。
3.需要在 new [] 一个对象数组时,需要保存数组的维度,C++ 的做法是在分配数组空间时多分配了 4 个字节的大小,专门保存数组的大小,在 delete [] 时就可以取出这个保存的数,就知道了需要调用析构函数多少次了。

107、 malloc申请的存储空间能用delete释放吗

不能,malloc /free主要为了兼容C,new和delete 完全可以取代malloc /free的。malloc /free的操作对象都是必须明确大小的。而且不能用在动态类上。new 和delete会自动进行类型检查和大小,malloc/free不能执行构造函数与析构函数,所以动态对象它是不行的。当然从理论上说使用malloc申请的内存是可以通过delete释放的。不过一般不这样写的。而且也不能保证每个C++的运行时都能正常。

108、 函数参数入栈的顺序

○1大多数编译器中,参数是从右向左⼊栈(原因在于采⽤这种顺序,是为了让程序员在使⽤C/C++的“函数参数⻓度可变”这个特性时更⽅便。如果是从左向右压栈,第⼀个参数(即描述可变参数表各变量类型的那个参数)将被放在栈底,由于可变参的函数第⼀步就需要解析可变参数表的各参数类型,即第⼀步就需要得到上述参数,因此,将它放在栈底是很不方便的。)
○2本次函数调用结束时,局部变量先出栈,然后是参数,最后是栈顶指针最开始存放的地址,程序由该点继续运⾏,不会产生碎⽚。

109、 堆和栈区别

1.管理方式:
○1栈由操作系统自动分配释放,无需我们手动控制,无需我们手工控制,⼀般保存的是局部变量和函数参数等。
○2堆由程序员管理,需要⼿动 new malloc delete free 进⾏分配和回收,如果不进⾏回收的话,会造成内存泄漏的问题。
2.空间大小:一般来讲在32位系统下,堆内存可以达到4G的空间,从这个角度来看堆内存几乎是没有什么限制的。但是对于栈来讲,一般都是有一定的空间大小的,例如,在VC6下面,默认的栈空间大小是1M(好像是,记不清楚了)。当然,我们可以修改: 打开工程,依次操作菜单如下:Project->Setting->Link,在Category 中选中Output,然后在Reserve中设定堆栈的最大值和commit。 注意:reserve最小值为4Byte;commit是保留在虚拟内存的页文件里面,它设置的较大会使栈开辟较大的值,可能增加内存的开销和启动时间。
3.碎片问题:对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列,他们是如此的一一对应,以至于永远都不可能有一个内存块从栈中间弹出,在他弹出之前,在他上面的后进的栈内容已经被弹出,详细的可以参考数据结构,这里我们就不再一一讨论了。
4.生长方向:
○1对于栈来讲,是连续的内存空间,它的生长方向是向下的,是向着内存地址减小的方向增长。比如在函数调⽤的时候,首先⼊栈的主函数的下⼀条可执⾏指令的地址,然后是函数的各个参数。
○2对于堆来讲,不连续的空间,实际上系统中有⼀个空闲链表,生长方向是向上的,也就是向着内存地址增加的方向,空间交⼤,较为灵活。
;当有程序申请的时候,系统遍历空闲链表找到第⼀个⼤于等于申请⼤⼩的空间分配给程序,⼀般在分配程序的时候,也会空间头部写⼊内存⼤⼩,⽅便 delete 回收空间⼤⼩。当然如果有剩余的,也会将剩余的插⼊到空闲链表中,这也是产⽣内存碎⽚的原因。
5.分配方式:堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由alloca函数进行分配,但是栈的动态分配和堆是不同的,它的动态分配是由编译器进行释放,无需我们手工实现。
6.分配效率:栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是C/C++函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。

110、 堆与栈的优点和缺点

1.堆的优缺点:堆得优点就是可以动态分配内存大小,生存期也不必告诉编译器,因为它是在运行中动态分配内存的;缺点就是由于是在运行时动态分配内存的,所以读取速度较慢。
2.栈的优缺点:栈的优点就是读取速度快,而且数据可以共享;缺点就是存在于栈中的数据大小及周期必须是确定的,缺乏灵活性。

111、 内核空间 虚拟内存管理

1.虚拟内存管理负责从进程的虚拟地址空间分配虚拟页,sys_brk负责用来扩大或收缩堆,sys_mmap负责从内存映射区域分配虚拟页,sys_munmap用来释放虚拟页。
2.进程第一次访问虚拟页的时候触发页处理异常,直接从页处理申请物理内存,然后映射到虚拟内存的页表。
3.页分配器负责分配物理页,当前使用的页分配器是伙伴分配器。内核空间提供把页划分为小内存块分配的块分配器,提供分配内存的接口kmalloc(),和释放内存的接口kfree()。
4.不连续页分配器提供分配内存的接口vmalloc()和释放内存接口vfree(),在内存碎片化的时候,申请连续物理页的成功率很低,可以申请不连续的物理页,映射到连续的虚拟页,即虚拟地址连续,页物理地址不连续。

112、 malloc与free的实现原理?

malloc采用的是内存池的管理方式(ptmalloc),ptmalloc 采用边界标记法将内存划分成很多块,从而对内存的分配与回收进行管理。
为了内存分配函数malloc的高效性,ptmalloc会预先向操作系统申请一块内存供用户使用,当我们申请和释放内存的时候,ptmalloc会将这些内存管理起来,并通过一些策略来判断是否将其回收给操作系统。
这样做的最大好处就是,使用户申请和释放内存的时候更加高效,避免产生过多的内存碎片。
1.在标准C库中,提供了malloc/free函数分配释放内存,这两个函数底层是由brk、mmap、,munmap这些系统调用实现的;
2.brk是将数据段(.data)的最高地址指针_edata往高地址推,mmap是在进程的虚拟地址空间中(堆和栈中间,称为文件映射区域的地方)找一块空闲的虚拟内存。这两种方式分配的都是虚拟内存,没有分配物理内存。在第一次访问已分配的虚拟地址空间的时候,发生虚拟中断,操作系统负责分配物理内存,然后建立虚拟内存和物理内存之间的映射关系;
3.malloc小于128k的内存,使用brk分配内存,将_edata往高地址推;malloc大于128k的内存,使用mmap分配内存,在堆和栈之间找一块空闲内存分配;brk分配的内存需要等到高地址内存释放以后才能释放,而mmap分配的内存可以单独释放。当最高地址空间的空闲内存超过128K(可由M_TRIM_THRESHOLD选项调节)时,执行内存紧缩操作(trim)。在上一个步骤free的时候,发现最高地址空闲内存超过128K,于是内存紧缩。
4.malloc是从堆里面申请内存,也就是说函数返回的指针是指向堆里面的一块内存。操作系统中有一个记录空闲内存地址的链表。当操作系统收到程序的申请时,就会遍历该链表,然后就寻找第一个空间大于所申请空间的堆结点,然后就将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。

113、 malloc、realloc、calloc的区别

1.malloc函数
void* malloc(unsigned int num_size);
int *p = malloc(20*sizeof(int));申请20个int类型的空间;
2.calloc函数
void* calloc(size_t n,size_t size);
int *p = calloc(20, sizeof(int));
省去了人为空间计算;malloc申请的空间的值是随机初始化的,calloc申请的空间的值是初始化为0的;
3.realloc函数
void realloc(void *p, size_t new_size);
给动态分配的空间分配额外的空间,用于扩充容量。

114、 __stdcall和__cdecl的区别?

1.__stdcall
__stdcall是函数恢复堆栈,只有在函数代码的结尾出现一次恢复堆栈的代码;在编译时就规定了参数个数,无法实现不定个数的参数调用;
2.__cdecl
__cdecl是调用者恢复堆栈,假设有100个函数调用函数a,那么内存中就有100端恢复堆栈的代码;可以不定参数个数;每一个调用它的函数都包含清空堆栈的代码,所以产生的可执行文件大小会比调用__stacall函数大。

115、 手写字符串函数 strcat, strcpy, strncpy, memset, memcpy实现

1. strcat
头文件:#include <string.h>
用法:函数原型如下
char *strcat(char *dst, char const *src);
strcat 函数要求 dst 参数原先已经包含了一个字符串(可以是空字符串)。它找到这个字符串的末尾,并把 src 字符串的一份拷贝添加到这个位置。如果 src 和 dst 的位置发生重叠,其结果是未定义的。编程者需要保证目标字符数组剩余的空间足以保存整个字符串。

char *strcat (char * dst, const char * src)
{
assert(NULL != dst && NULL != src); // 源码里没有断言检测
char * cp = dst;
while(*cp )
cp++; /* find end of dst */
while(*cp++ = *src++) ; /* Copy src to end of dst */
return( dst ); /* return dst */
}


2. strcpy
头文件:#include <string.h>
用法:strcpy 的函数原型如下:
char *strcpy(char *dst, const char *src);
函数把参数 src 字符串复制到 dst 参数,dst 字符串的结束符也会复制,如果参数 src 和 dst 在内存中出现叠,其结果是未定义的。由于 dst 参数将进行修改,所以它必须是个字符串数组或者是一个指向动态内存分配的数组指针,不能使用字符串常量。
需要注意的是:程序员必须保证目标字符串数组的空间足以容纳需要复制的字符串。如果多余的字符串比数组长,多余的字符仍被复制,它们将覆盖原先存储于数组后面的内存空间。

char *strcpy(char *dst, const char *src) // 实现src到dst的复制
{
if(dst == src) return dst; //源码中没有此项
  assert((dst != NULL) && (src != NULL)); //源码没有此项检查,判断参数src和dst的有效性
  char *cp = dst; //保存目标字符串的首地址
  while (*cp++ = *src++); //把src字符串的内容复制到dst下
  return dst;
}


3.memcpy
头文件:#include <string.h>
用法:memcpy 提供了一般内存的复制,即memcpy对于需要复制的内容没有限制,用途更广泛。
void *memcpy(void *dst, const void *src, size_t length);
从 src 所指的内存地址的起始位置开始,拷贝n个字节的数据到 dest 所指的内存地址的起始位置。你可以用这种方法复制任何类型的值(例如:int,double,结构或结构数组),如果src和dst以任何形式出现了重叠,它的结果将是未定义的。
实现代码:

void *memcpy(void *dst, const void *src, size_t length)
{
assert((dst != NULL) && (src != NULL));
  char *tempSrc= (char *)src; //保存src首地址
  char *tempDst = (char *)dst; //保存dst首地址
  while(length-- > 0) //循环length次,复制src的值到dst中
  *tempDst++ = *tempSrc++ ;
  return dst;
}

4.strcpy 和 memcpy 的主要区别:
复制的内容不同。strcpy只能复制字符串,而memcpy可以复制任意内容,例如字符数组、整型、结构体、类等。
复制的方法不同。strcpy不需要指定长度,它遇到被复制字符的串结束符"\0"才结束,所以容易溢出。memcpy则是根据其第3个参数决定复制的长度,遇到'\0'并不结束。
用途不同。通常在复制字符串时用strcpy,而需要复制其他类型数据时则一般用memcpy;
5.strncpy
头文件:#include <string.h>
函数原型如下:
char *strncpy(char *dst, char const *src, size_t len);
strncpy把源字符串的字符复制到目标数组,它总是正好向 dst 写入 len 个字符。如果 strlen(src) 的值小于 len,dst 数组就用额外的 NUL 字节填充到 len 长度。如果 strlen(src)的值大于或等于 len,那么只有 len 个字符被复制到dst中。这里需要注意它的结果将不会以NUL字节结尾。
实现代码:

char *strncpy(char *dst, const char *src, size_t len)
{
assert(dst != NULL && src != NULL); //源码没有此项
char *cp = dst;
while (len-- > 0 && *src != '\0')
*cp++ = *src++;
*cp = '\0'; //源码没有此项
return dst;
}


5. memset
头文件:#include <string.h>
函数原型如下:
void *memset(void *a, int ch, size_t length);
将参数a所指的内存区域前length个字节以参数ch填入,然后返回指向a的指针。在编写程序的时候,若需要将某一数组作初始化,memset()会很方便。
实现代码:

void *memset(void *a, int ch, size_t length)
{
assert(a != NULL);
void *s = a;
while (length--)
{
*(char *)s = (char) ch;
s = (char *)s + 1;
}
return a;
}

116、 使用智能指针管理内存资源,RAII

1.RAII全称是“Resource Acquisition is Initialization”,直译过来是“资源获取即初始化”,也就是说在构造函数中申请分配资源,在析构函数中释放资源。因为C++的语言机制保证了,当一个对象创建的时候,自动调用构造函数,当对象超出作用域的时候会自动调用析构函数。所以,在RAII的指导下,我们应该使用类来管理资源,将资源和对象的生命周期绑定。
2.智能指针(std::shared_ptr和std::unique_ptr)即RAII最具代表的实现,使用智能指针,可以实现自动的内存管理,再也不需要担心忘记delete造成的内存泄漏。毫不夸张的来讲,有了智能指针,代码中几乎不需要再出现delete了。

117、 手写实现智能指针类

1.智能指针是一个数据类型,一般用模板实现,模拟指针行为的同时还提供自动垃圾回收机制。它会自动记录SmartPointer<T*>对象的引用计数,一旦T类型对象的引用计数为0,就释放该对象。除了指针对象外,我们还需要一个引用计数的指针设定对象的值,并将引用计数计为1,需要一个构造函数。新增对象还需要一个构造函数,析构函数负责引用计数减少和释放内存。通过覆写赋值运算符,才能将一个旧的智能指针赋值给另一个指针,同时旧的引用计数减1,新的引用计数加1
2.一个构造函数、拷贝构造函数、复制构造函数、析构函数、移走函数;

118、 结构体变量比较是否相等

1.重载了 “==” 操作符

struct foo {
int a;
int b;
bool operator==(const foo& rhs) // 操作运算符重载
{
return( a == rhs.a) && (b == rhs.b);

}
};


2.元素的话,一个个比;
3.指针直接比较,如果保存的是同一个实例地址,则(p1==p2)为真;

119、 位运算

若一个数m满足 m = 2^n;那么k%m=k&(m-1);
位与相关性质和计算一个数的二进制表示中有多少个1的做法:
https://blog.csdn.net/qq_41687938/article/details/117324467

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

1、调用者函数把被调函数所需要的参数按照与被调函数的形参顺序相反的顺序压入栈中,即:从右向左依次把被调函数所需要的参数压入栈;
2、调用者函数使用call指令调用被调函数,并把call指令的下一条指令的地址当成返回地址压入栈中(这个压栈操作隐含在call指令中);
3、在被调函数中,被调函数会先保存调用者函数的栈底地址(push ebp),然后再保存调用者函数的栈顶地址,即:当前被调函数的栈底地址(mov ebp,esp);
4、在被调函数中,从ebp的位置处开始存放被调函数中的局部变量和临时变量,并且这些变量的地址按照定义时的顺序依次减小,即:这些变量的地址是按照栈的延伸方向排列的,先定义的变量先入栈,后定义的变量后入栈;



这篇关于二十万字C/C++、嵌入式软开面试题全集宝典六的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程