说明:
总结到这里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如何解决冲突以及扩容
答案:
- unordered_map和map类似,都是存储的key-value的值,可以通过key快速索引到value。不同的是unordered_map不会根据key的大小进行排序,
- 存储时是根据key的hash值判断元素是否相同,即unordered_map内部元素是无序的,而map中的元素是按照二叉搜索树存储,进行中序遍历会得到有序遍历。
- 使用时map的key需要定义operator<。而unordered_map需要定义hash_value函数并且重载operator==。但是很多系统内置的数据类型都自带这些
- 那么如果是自定义类型,那么就需要自己重载operator<或者hash_value()了。
- 如果需要内部元素自动排序,使用map,不需要排序使用unordered_map
- unordered_map的底层实现是hash_table
- hash_map底层使用的是hash_table,而hash_table使用的开链法进行冲突避免,所有hash_map采用开链法进行冲突解决
- 什么时候扩容:当向容器添加元素的时候,会判断当前容器的元素个数,如果大于等于阈值—即当前数组的长度乘以加载因子的值的时候,就要自动扩容啦。
- 扩容(resize):就是重新计算容量,向HashMap对象里不停的添加元素,而HashMap对象内部的数组无法装载更多的元素时,对象就需要扩大数组的长度,以便能装入更多的元素
5.STL中的allocator、deallocator
答案:
- 第一级配置器直接使用malloc()、free()和relloc(),第二级配置器视情况采用不同的策略:当配置区块超过128bytes时,视之为足够大,便调用第一级配置器;当配置器区块小于128bytes时,为了降低额外负担,使用复杂的内存池整理方式,而不再用一级配置器;
- 第二级配置器主动将任何小额区块的内存需求量上调至8的倍数,并维护16个free-list,各自管理大小为8~128bytes的小额区块;
- 空间配置函数allocate(),首先判断区块大小,大于128就直接调用第一级配置器,小于128时就检查对应的free-list。如果free-list之内有可用区块,就直接拿来用,如果没有可用区块,就将区块大小调整至8的倍数,然后调用refill(),为free-list重新分配空间;
- 空间释放函数deallocate(),该函数首先判断区块大小,大于128bytes时,直接调用一级配置器,小于128bytes就找到对应的free-list然后释放内存。
6.基类的虚函数表存放在内存的什么区,虚表指针vptr的初始化时间
答案:
首先整理一下虚函数表的特征:
- 虚函数表是全局共享的元素,即全局仅有一个,在编译时就构造完成
- 虚函数表类似一个数组,类对象中存储vptr指针,指向虚函数表,即虚函数表不是函数,不是程序代码,不可能存储在代码段
- 虚函数表存储虚函数的地址,即虚函数表的元素是指向类成员函数的指针,而类中虚函数的个数在编译时期可以确定,即虚函数表的大小可以确定,即大小是在编译时期确定的,不必动态分配内存空间存储虚函数表,所以不在堆中
根据以上特征,虚函数表类似于类中静态成员变量.静态成员变量也是全局共享,大小确定,因此最有可能存在全局数据区,测试结果显示:
虚函数表vtable在Linux/Unix中存放在可执行文件的只读数据段中(rodata),这与微软的编译器将虚函数表存放在常量段存在一些差别。
由于虚表指针vptr跟虚函数密不可分,对于有虚函数或者继承于拥有虚函数的基类,对该类进行实例化时,在构造函数执行时会对虚表指针进行初始化,并且存在对象内存布局的最前面。
一般分为五个区域:栈区、堆区、函数区(存放函数体等二进制代码)、全局静态区、常量区。
C++中虚函数表位于只读数据段(.rodata),也就是C++内存模型中的常量区;而虚函数则位于代码段(.text),也就是C++内存模型中的代码区。
7.将字符串“hello world”从开始到打印到屏幕上的全过程?
答案:
- 用户告诉操作系统执行HelloWorld程序(通过键盘输入等)
- 操作系统:找到helloworld程序的相关信息,检查其类型是否是可执行文件;并通过程序首部信息,确定代码和数据在可执行文件中的位置并计算出对应的磁盘块地址
- 操作系统:创建一个新进程,将HelloWorld可执行文件映射到该进程结构,表示由该进程执行helloworld程序。
- 操作系统:为helloworld程序设置cpu上下文环境,并跳到程序开始处。
- 执行helloworld程序的第一条指令,发生缺页异常
- 操作系统:分配一页物理内存,并将代码从磁盘读入内存,然后继续执行helloworld程序
- helloword程序执行puts函数(系统调用),在显示器上写一字符串
- 操作系统:找到要将字符串送往的显示设备,通常设备是由一个进程控制的,所以,操作系统将要写的字符串送给该进程
- 操作系统:控制设备的进程告诉设备的窗口系统,它要显示该字符串,窗口系统确定这是一个合法的操作,然后将字符串转换成像素,将像素写入设备的存储映像区
- 视频硬件将像素转换成显示器可接收和一组控制数据信号
- 显示器解释信号,激发液晶屏
- 终于,我们在屏幕上看到了HelloWorld
8.哪些函数不能是虚函数?把你知道的都说一说
答案:
- 构造函数,构造函数初始化对象,派生类必须知道基类函数干了什么,才能进行构造;当有虚函数时,每一个类有一个虚表,每一个对象有一个虚表指针,虚表指针在构造函数中初始化;
- 内联函数,内联函数表示在编译阶段进行函数体的替换操作,而虚函数意味着在运行期间进行类型确定,所以内联函数不能是虚函数;
- 静态函数,静态函数不属于对象属于类,静态成员函数没有this指针,因此静态函数设置为虚函数没有任何意义。
- 友元函数,友元函数不属于类的成员函数,不能被继承。对于没有继承特性的函数没有虚函数的说法。
- 普通函数,普通函数不属于类的成员函数,不具有继承特性,因此普通函数没有虚函数。
9.带参宏与带参函数的区别
答案:
类型 | 带参宏 | 带参函数 |
---|---|---|
处理时间 | 编译时 | 运行时 |
参数类型 | 无 | 需定义 |
程序长度 | 变长 | 不变 |
占用存储空间 | 否 | 是 |
运行时间 | 不占运行时间 | 调用和返回占 |