十八万字整理C/C++、嵌入式软开 常见面试题汇总21

十八万字吐血整理的C/C++、嵌入式常见面试题!!!!

欢迎订阅,希望能点个赞!!!!

正在持续更新!!!!!欢迎探讨!!!

完整专栏地址:https://blog.nowcoder.net/zhuanlan/gmPWX0


相关知识点都能零星在网上找到,这个文章系列将目前遇到的所有常见面试问题进行一个汇总。

文中很多资料避免不了从网上或是其他复习资料里收集整理,十分感谢前辈的辛勤付出,如果存在侵权请一定联系我进行删除

也有相当一部分是本人在经历提前批以及秋招的过程中遇到和验证过的。


系列文章PDF下载地址:《最全C_C++及嵌入式软开面试题宝典.pdf》



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

1.虚拟内存管理负责从进程的虚拟地址空间分配虚拟页,sys_brk负责用来扩大或收缩堆,sys_mmap负责从内存映射区域分配虚拟页,sys_munmap用来释放虚拟页。

2.进程第一次访问虚拟页的时候触发页处理异常,直接从页处理申请物理内存,然后映射到虚拟内存的页表。

3.页分配器负责分配物理页,当前使用的页分配器是伙伴分配器。内核空间提供把页划分为小内存块分配的块分配器,提供分配内存的接口kmalloc(),和释放内存的接口kfree()

4.不连续页分配器提供分配内存的接口vmalloc()和释放内存接口vfree(),在内存碎片化的时候,申请连续物理页的成功率很低,可以申请不连续的物理页,映射到连续的虚拟页,即虚拟地址连续,页物理地址不连续。

112、mallocfree的实现原理?

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、mallocrealloccalloc的区别

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 所指的内存地址的起始位置。你可以用这种方法复制任何类型的值(例如:intdouble,结构或结构数组),如果srcdst以任何形式出现了重叠,它的结果将是未定义的。

实现代码:

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) 的值小于 lendst 数组就用额外的 NUL 字节填充到 len 长度。如果 strlensrc)的值大于或等于 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++、嵌入式常见面试题!!!!还在持续更新中!!! 这个专栏写完了,再po上自己亲手敲的笔试编程题整理。

全部评论

相关推荐

10-15 09:13
已编辑
天津大学 soc前端设计
点赞 评论 收藏
分享
Noob1024:一笔传三代,人走笔还在
点赞 评论 收藏
分享
评论
1
1
分享
牛客网
牛客企业服务