• 嵌入式软件工程师面试题——2025校招社招通用(十五)


    说明:
    总结到这里C++基础部分级面试题就完结了,后面前面几篇写的优点混乱,后面几篇都以理论为主。在后面会出一个牛客选择题的训练营以及力扣牛客的算法题 题目推荐,等博主刷完牛客C/C++的所有题目后把易错题目做一个整理。 下一次更新其他模块的面试题,嵌入式相关的C语言题目,驱动开发,应用编程,通信协议也会更新的。希望大家继续支持

    • 面试群,群号: 228447240
    • 面试题来源于网络书籍,公司题目以及博主原创或修改(题目大部分来源于各种公司);
    • 文中很多题目,或许大家直接编译器写完,1分钟就出结果了。但在这里博主希望每一个题目,大家都要经过认真思考,答案不重要,重要的是通过题目理解所考知识点,好应对题目更多的变化;
    • 博主与大家一起学习,一起刷题,共同进步;
    • 写文不易,麻烦给个三连!!!

    1.C++11中的智能指针你了解多少?

    答案:
    C++11 引入了三种主要的智能指针类型:unique_ptr、shared_ptr 和 weak_ptr。
    unique_ptr:
    unique_ptr 是一种独占所有权的智能指针,它确保只有一个指针可以访问和管理所指向的对象。
    unique_ptr 不能被拷贝,但可以通过移动语义进行转移所有权。
    当 unique_ptr 超出作用域或被显式地释放时,它会自动删除所管理的对象。
    unique_ptr 是最轻量级的智能指针,适用于需要独占所有权的情况。
    原理: 简单粗暴的防拷贝,下面简化模拟实现了一份UniquePtr来了解它的原理

    // 模拟实现一份简答的UniquePtr,了解原理
    template<class T>
    class UniquePtr
    {
    public:
     UniquePtr(T * ptr = nullptr) 
     : _ptr(ptr)
     {}
     ~UniquePtr() 
     {
      	if(_ptr)
     	delete _ptr;
     }
     T& operator*() {return *_ptr;}
     T* operator->() {return _ptr;}
     
    private:
     // C++98防拷贝的方式:只声明不实现+声明成私有
     UniquePtr(UniquePtr<T> const &);
     UniquePtr & operator=(UniquePtr<T> const &);
     
     // C++11防拷贝的方式:delete
     UniquePtr(UniquePtr<T> const &) = delete;
     UniquePtr & operator=(UniquePtr<T> const &) = delete;
     
    private:
     T * _ptr;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    shared_ptr:
    shared_ptr 是一种共享所有权的智能指针,它可以被多个指针共同管理同一个对象。
    shared_ptr 使用引用计数来跟踪有多少个指针共享对象,当引用计数为零时,对象会被自动删除。
    shared_ptr 可以被拷贝和赋值,每个拷贝都会增加引用计数。
    shared_ptr 具有较大的开销,因为需要维护引用计数,但它提供了方便的共享对象所有权的方式。
    原理: 是通过引用计数的方式来实现多个shared_ptr对象之间共享资源。

    // 模拟实现一份简答的SharedPtr,了解原理
    #include 
    #include 
    
    template <class T>
    class SharedPtr
    {
    public:
     SharedPtr(T* ptr = nullptr)
     : _ptr(ptr)
     , _pRefCount(new int(1))
     , _pMutex(new mutex)
     {}
     ~SharedPtr() {Release();}
     SharedPtr(const SharedPtr<T>& sp)
     : _ptr(sp._ptr)
     , _pRefCount(sp._pRefCount)
     , _pMutex(sp._pMutex)
     {
     AddRefCount();
     }
     // sp1 = sp2
     SharedPtr<T>& operator=(const SharedPtr<T>& sp)
     {
     //if (this != &sp)
     if (_ptr != sp._ptr)
     {
     // 释放管理的旧资源
     Release();
     // 共享管理新对象的资源,并增加引用计数
     _ptr = sp._ptr;
     _pRefCount = sp._pRefCount;
     _pMutex = sp._pMutex;
     
     AddRefCount();
     }
     return *this;
     }
     T& operator*() {return *_ptr;}
     T* operator->() {return _ptr;}
     int UseCount() {return *_pRefCount;}
     T* Get() { return _ptr; }
     void AddRefCount()
     {
     // 加锁或者使用加1的原子操作
     _pMutex->lock();
     ++(*_pRefCount);
      _pMutex->unlock();
     }
    private:
     void Release()
     {
     bool deleteflag = false;
     
     // 引用计数减1,如果减到0,则释放资源
     _pMutex.lock();
     if (--(*_pRefCount) == 0)
     {
     delete _ptr;
     delete _pRefCount;
     deleteflag = true;
     }
     _pMutex.unlock();
     
     if(deleteflag == true)
     delete _pMutex;
     }
    private:
     int* _pRefCount; // 引用计数
     T* _ptr; // 指向管理资源的指针 
     mutex* _pMutex; // 互斥锁
    };
    int main()
    {
     SharedPtr<int> sp1(new int(10));
     SharedPtr<int> sp2(sp1);
     *sp2 = 20;
     cout << sp1.UseCount() << endl;
     cout << sp2.UseCount() << endl;
     SharedPtr<int> sp3(new int(10));
     sp2 = sp3;
     cout << sp1.UseCount() << endl;
     cout << sp2.UseCount() << endl;
     cout << sp3.UseCount() << endl;
     sp1 = sp3;
     cout << sp1.UseCount() << endl;
     cout << sp2.UseCount() << endl;
     cout << sp3.UseCount() << endl;
     return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91

    weak_ptr:
    weak_ptr 是一种弱引用的智能指针,它指向 shared_ptr 管理的对象,但不增加引用计数。
    weak_ptr 主要用于解决 shared_ptr 的循环引用问题,避免内存泄漏。
    weak_ptr 可以通过 lock() 函数获取一个 shared_ptr 对象,如果对象仍然存在,则返回一个有效的 shared_ptr,否则返回一个空的shared_ptr。

    template <typename T>
    class weak_ptr
    {
    public:
        weak_ptr() : ptr_(nullptr), ref_count_(nullptr) {}
    
        weak_ptr(const shared_ptr<T>& shared) : ptr_(shared.ptr_), ref_count_(shared.ref_count_)
        {
            if (ref_count_)
            {
                ref_count_->weak_count_++;
            }
        }
    
        ~weak_ptr()
        {
            reset();
        }
    
        weak_ptr(const weak_ptr<T>& other) : ptr_(other.ptr_), ref_count_(other.ref_count_)
        {
            if (ref_count_)
            {
                ref_count_->weak_count_++;
            }
        }
    
        weak_ptr<T>& operator=(const weak_ptr<T>& other)
        {
            if (this != &other)
            {
                reset();
                ptr_ = other.ptr_;
                ref_count_ = other.ref_count_;
                if (ref_count_)
                {
                    ref_count_->weak_count_++;
                }
            }
            return *this;
        }
    
        void reset()
        {
            if (ref_count_)
            {
                ref_count_->weak_count_--;
                if (ref_count_->weak_count_ == 0 && ref_count_->shared_count_ == 0)
                {
                    delete ref_count_;
                    delete ptr_;
                }
            }
            ptr_ = nullptr;
            ref_count_ = nullptr;
        }
    
        shared_ptr<T> lock() const
        {
            if (expired())
            {
                return shared_ptr<T>();
            }
            return shared_ptr<T>(*this);
        }
    
        bool expired() const
        {
            return (ref_count_ == nullptr || ref_count_->shared_count_ == 0);
        }
    
    private:
        T* ptr_;
        shared_count* ref_count_;
    };
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76

    注意: auto_ptr并不是C++11提出的,而是 C++ 98 中产生的第一个智能指针。已不建议使用。

    2.vector与list的区别与应用?怎么找某vector或者list的倒数第二个元素

    答案:
    1.vector数据结构 vector和数组类似,拥有一段连续的内存空间,并且起始地址不变。因此能高效的进行随机存取,时间复杂度为o(1);但因为内存空间是连续的,所以在进行插入和删除操作时,会造成内存块的拷贝,时间复杂度为o(n)。

    另外,当数组中内存空间不够时,会重新申请一块内存空间并进行内存拷贝。连续存储结构:vector是可以实现动态增长的对象数组,支持对数组高效率的访问和在数组尾端的删除和插入操作,在中间和头部删除和插入相对不易,需要挪动大量的数据。

    它与数组最大的区别就是vector不需程序员自己去考虑容量问题,库里面本身已经实现了容量的动态增长,而数组需要程序员手动写入扩容函数进形扩容。

    2.list数据结构 list是由双向链表实现的,因此内存空间是不连续的。只能通过指针访问数据,所以list的随机存取非常没有效率,时间复杂度为o(n);但由于链表的特点,能高效地进行插入和删除。非连续存储结构:list是一个双链表结构,支持对链表的双向遍历。每个节点包括三个信息:元素本身,指向前一个元素的节点(prev)和指向下一个元素的节点(next)。因此list可以高效率的对数据元素任意位置进行访问和插入删除等操作。由于涉及对额外指针的维护,所以开销比较大。

    区别:
    1.vector的随机访问效率高,但在插入和删除时(不包括尾部)需要挪动数据,不易操作。
    2.list的访问要遍历整个链表,它的随机访问效率低。但对数据的插入和删除操作等都比较方便,改变指针的指向即可。
    3.从遍历上来说,list是单向的,vector是双向的。
    4.vector中的迭代器在使用后就失效了,而list的迭代器在使用之后还可以继续使用。
    5.list不提供随机访问,所以不能用下标直接访问到某个位置的元素,要访问list里的元素只能遍历,不过你要是只需要访问list的最后N个元素的话,可以用反向迭代器来遍历

    3.Vector如何释放空间?

    答案:
    由于vector的内存占用空间只增不减,比如你首先分配了10,000个字节,然后erase掉后面9,999个,留下一个有效元素,但是内存占用仍为10,000个。所有内存空间是在vector析构时候才能被系统回收。empty()用来检测容器是否为空的,clear()可以清空所有元素。但是即使clear(),vector所占用的内存空间依然如故,无法保证内存的回收。

    如果需要空间动态缩小,可以考虑使用deque。

    如果使用vector,可以用swap()来帮助你释放多余内存或者清空全部内存。

    vector(Vec).swap(Vec); //将Vec中多余内存清除; 
    vector().swap(Vec); //清空Vec的全部内存;
    
    • 1
    • 2

    3.map插入方式有哪几种?

    答案:
    1.用insert函数插入pair数据

    mapStudent.insert(pair<int, string>(1, "student_one")); 
    
    • 1

    2.用数组方式插入数据

    mapStudent[1] = "student_one"; 
    
    • 1

    3.在insert函数中使用make_pair()函数

    mapStudent.insert(make_pair(1, "student_one")); 
    
    • 1

    4.用insert函数插入value_type数据

    mapStudent.insert(map<int, string>::value_type (1, "student_one"));
     
    
    • 1
    • 2

    4.STL中unordered_map(hash_map)和map的区别,hash_map如何解决冲突以及扩容

    答案:

    1. unordered_map和map类似,都是存储的key-value的值,可以通过key快速索引到value。不同的是unordered_map不会根据key的大小进行排序,
    2. 存储时是根据key的hash值判断元素是否相同,即unordered_map内部元素是无序的,而map中的元素是按照二叉搜索树存储,进行中序遍历会得到有序遍历。
    3. 使用时map的key需要定义operator<。而unordered_map需要定义hash_value函数并且重载operator==。但是很多系统内置的数据类型都自带这些
    4. 那么如果是自定义类型,那么就需要自己重载operator<或者hash_value()了。
    5. 如果需要内部元素自动排序,使用map,不需要排序使用unordered_map
    6. unordered_map的底层实现是hash_table
    7. hash_map底层使用的是hash_table,而hash_table使用的开链法进行冲突避免,所有hash_map采用开链法进行冲突解决
    8. 什么时候扩容:当向容器添加元素的时候,会判断当前容器的元素个数,如果大于等于阈值—即当前数组的长度乘以加载因子的值的时候,就要自动扩容啦。
    9. 扩容(resize):就是重新计算容量,向HashMap对象里不停的添加元素,而HashMap对象内部的数组无法装载更多的元素时,对象就需要扩大数组的长度,以便能装入更多的元素

    5.STL中的allocator、deallocator

    答案:

    1. 第一级配置器直接使用malloc()、free()和relloc(),第二级配置器视情况采用不同的策略:当配置区块超过128bytes时,视之为足够大,便调用第一级配置器;当配置器区块小于128bytes时,为了降低额外负担,使用复杂的内存池整理方式,而不再用一级配置器;
    2. 第二级配置器主动将任何小额区块的内存需求量上调至8的倍数,并维护16个free-list,各自管理大小为8~128bytes的小额区块;
    3. 空间配置函数allocate(),首先判断区块大小,大于128就直接调用第一级配置器,小于128时就检查对应的free-list。如果free-list之内有可用区块,就直接拿来用,如果没有可用区块,就将区块大小调整至8的倍数,然后调用refill(),为free-list重新分配空间;
    4. 空间释放函数deallocate(),该函数首先判断区块大小,大于128bytes时,直接调用一级配置器,小于128bytes就找到对应的free-list然后释放内存。

    6.基类的虚函数表存放在内存的什么区,虚表指针vptr的初始化时间

    答案:
    首先整理一下虚函数表的特征:

    • 虚函数表是全局共享的元素,即全局仅有一个,在编译时就构造完成
    • 虚函数表类似一个数组,类对象中存储vptr指针,指向虚函数表,即虚函数表不是函数,不是程序代码,不可能存储在代码段
    • 虚函数表存储虚函数的地址,即虚函数表的元素是指向类成员函数的指针,而类中虚函数的个数在编译时期可以确定,即虚函数表的大小可以确定,即大小是在编译时期确定的,不必动态分配内存空间存储虚函数表,所以不在堆中

    根据以上特征,虚函数表类似于类中静态成员变量.静态成员变量也是全局共享,大小确定,因此最有可能存在全局数据区,测试结果显示:

    虚函数表vtable在Linux/Unix中存放在可执行文件的只读数据段中(rodata),这与微软的编译器将虚函数表存放在常量段存在一些差别。
    由于虚表指针vptr跟虚函数密不可分,对于有虚函数或者继承于拥有虚函数的基类,对该类进行实例化时,在构造函数执行时会对虚表指针进行初始化,并且存在对象内存布局的最前面。

    一般分为五个区域:栈区、堆区、函数区(存放函数体等二进制代码)、全局静态区、常量区。
    C++中虚函数表位于只读数据段(.rodata),也就是C++内存模型中的常量区;而虚函数则位于代码段(.text),也就是C++内存模型中的代码区。

    7.将字符串“hello world”从开始到打印到屏幕上的全过程?

    答案:

    1. 用户告诉操作系统执行HelloWorld程序(通过键盘输入等)
    2. 操作系统:找到helloworld程序的相关信息,检查其类型是否是可执行文件;并通过程序首部信息,确定代码和数据在可执行文件中的位置并计算出对应的磁盘块地址
    3. 操作系统:创建一个新进程,将HelloWorld可执行文件映射到该进程结构,表示由该进程执行helloworld程序。
    4. 操作系统:为helloworld程序设置cpu上下文环境,并跳到程序开始处。
    5. 执行helloworld程序的第一条指令,发生缺页异常
    6. 操作系统:分配一页物理内存,并将代码从磁盘读入内存,然后继续执行helloworld程序
    7. helloword程序执行puts函数(系统调用),在显示器上写一字符串
    8. 操作系统:找到要将字符串送往的显示设备,通常设备是由一个进程控制的,所以,操作系统将要写的字符串送给该进程
    9. 操作系统:控制设备的进程告诉设备的窗口系统,它要显示该字符串,窗口系统确定这是一个合法的操作,然后将字符串转换成像素,将像素写入设备的存储映像区
    10. 视频硬件将像素转换成显示器可接收和一组控制数据信号
    11. 显示器解释信号,激发液晶屏
    12. 终于,我们在屏幕上看到了HelloWorld

    8.哪些函数不能是虚函数?把你知道的都说一说

    答案:

    1. 构造函数,构造函数初始化对象,派生类必须知道基类函数干了什么,才能进行构造;当有虚函数时,每一个类有一个虚表,每一个对象有一个虚表指针,虚表指针在构造函数中初始化;
    2. 内联函数,内联函数表示在编译阶段进行函数体的替换操作,而虚函数意味着在运行期间进行类型确定,所以内联函数不能是虚函数;
    3. 静态函数,静态函数不属于对象属于类,静态成员函数没有this指针,因此静态函数设置为虚函数没有任何意义。
    4. 友元函数,友元函数不属于类的成员函数,不能被继承。对于没有继承特性的函数没有虚函数的说法。
    5. 普通函数,普通函数不属于类的成员函数,不具有继承特性,因此普通函数没有虚函数。

    9.带参宏与带参函数的区别

    答案:

    类型带参宏带参函数
    处理时间编译时运行时
    参数类型需定义
    程序长度变长不变
    占用存储空间
    运行时间不占运行时间调用和返回占
  • 相关阅读:
    十三)Stable Diffussion使用教程:Lora训练
    uniapp手机一键登录,微信授权登陆
    分布式锁最实用解决方案--redisson分布式锁(一)
    贪吃蛇游戏制作
    三年后端开发: 拿下阿里 / 腾讯 / 美团等四个大厂的 Offer 后,总结如下
    【软件质量与软件测试 软件质量标准】
    【案例】从kaggle的房价预测模型案例中了解深度学习模型,即如何学习深度学习模型
    对于IT互联网行业来说,家觉得学历重要还是能力?
    idea设置字体大小快捷键 Ctrl+鼠标上下滑 字体快捷键缩放设置
    智能问答(Question Answering)的主要研究方向
  • 原文地址:https://blog.csdn.net/weixin_45257157/article/details/134421413