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

说明:
总结到这里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;
};

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

// 模拟实现一份简答的SharedPtr,了解原理
#include <thread>
#include <mutex>

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;
}

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_;
};

注意: 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的全部内存;

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

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

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

2.用数组方式插入数据

mapStudent[1] = "student_one"; 

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

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

4.用insert函数插入value_type数据

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

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.带参宏与带参函数的区别

答案:

类型带参宏带参函数
处理时间编译时运行时
参数类型需定义
程序长度变长不变
占用存储空间
运行时间不占运行时间调用和返回占
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值