C++ 基础
- 引用和指针之间的区别?
- 堆栈和堆中的内存分配有何区别?
- 存在哪些类型的智能指针?
- unique_ptr 是如何实现的?我们如何强制在 unique_ptr 中仅存在一个对象所有者?
- shared_ptr 如何工作?对象之间如何同步引用计数器?
- 我们可以复制unique_ptr或者将其从一个对象传递到另一个对象吗?
- 什么是右值和左值?
- 什么是 std::move 和 std::forward()
面向对象编程(OOP)
- 访问某些类的私有字段的方法?
- 一个类可以继承多个类吗?
- 静态字段是否在类构造函数中初始化?
- 构造函数/析构函数中会抛出异常吗?如何防止这种情况发生?
- 什么是虚方法?
- 为什么我们需要虚拟析构函数?
- 抽象类和接口的区别?
- 构造函数可以是虚拟的吗?
- 关键字 const 如何用于类方法?
- 如何保护对象不被复制?
STL 容器
- 向量和列表之间的区别?
- 地图和无序地图之间的区别?
- 调用 push_back() 时向量中的迭代器无效吗?
如何修改你的类以便与 map 和 unordered_map 一起使用?
主题
- 进程和线程之间的区别?
- 同一根线程可以重复运行吗?
- 同步线程的方法?
- 什么是死锁?
字节跳动中台、后端研发面试题
一面经常提到的问题
1.手撕lru
2.并发编程
3.函数式编程
4.无锁队列
5.CAP
6.线程池,进程池
7.大文件如何对字符串排序
8.线程同步
9.进程状态
10.进程调度、多进程开发,多线程开发
11.线程通信 + 2道场景题选择进程通信方式
12.raws
13.ocket与socket区别
14.设计高性能服务
15.nginx反向代理 epoll,nginx工作模式实现原理
16.pmtu,sendfile,io status等等
17.手撕memcpy 考虑内存重叠与copy效率
二面
1.自我介绍+项目介绍
2.mmap
3.软链接与硬链接
4.free命令 cache与buffer区别
5.数据库 列与行数据库区别
6.线程安全
7.单核线程安全
8.事务
9.隔离性,隔离级别
10.索引,合并
11.http cookie与session
12.跨域请求
13.最大上升子序列个数
14.函数调用过程
15.static c/c++区别
16.项目中的一些问题
17.tcp与udp
18.tcp深挖
19.进程与线程
20.字符串匹配
小米C++相关岗位面试题
1.自我介绍
2.项目介绍
3.实习介绍
4.C++11的新特性
5.懂rapidjson的底层原理?rapidjson使用的过程中遇到过那些bug?
6.判断链表是否有环,求环的入口节点
7.多态实现原理?虚函数指针具体怎么找虚函数表?
8.如何创建进程?
9.进程布局?堆栈全局代码那些区
10.函数压栈过程,包括函数带参数
11.”hello world"存在那个区?
12.线程私有区和共享区
13.是个全局变量但我又想是线程独有的怎么办?
14.tcp三次握手、流量控制、拥塞控制、MSS那一套
15.timewait
16.客户端握手发送SYN,TCP状态机,client变成什么状态?
17.相交链表
C++ 基础
1、引用和指针之间的区别?
引用和指针是C++中两种重要的间接访问机制。引用实质上是一个别名,它在声明时必须初始化,一旦绑定就不能改变。引用不能为空,也不能建立引用的引用,这使得它比指针更安全。引用在内存中实际上是作为一个常量指针实现的,但它对程序员隐藏了指针的复杂性。指针则是一个变量,它存储了另一个变量的内存地址。指针可以为空,可以改变它所指向的对象,也可以进行指针运算。指针的灵活性使它成为一个强大的工具,但同时也容易导致程序错误,如悬空指针和内存泄漏。
2、堆栈和堆中的内存分配有何区别?
栈内存的分配和释放是由编译器自动完成的。当变量超出其作用域时,栈内存会自动释放。栈内存的分配和释放速度很快,因为它使用简单的指针移动来实现。栈内存的大小是固定的,在程序运行时就已确定。
堆内存则是动态分配的,由程序员手动管理或使用智能指针等工具管理。堆内存的生命周期不受作用域限制,可以在需要时分配,在不需要时释放。堆内存的大小理论上只受系统可用内存的限制。但堆内存的分配和释放相对较慢,且容易产生内存碎片。
3、存在哪些类型的智能指针?
智能指针类型: C++11定义了三种智能指针。unique_ptr实现了独占式拥有概念,它保证一个对象只被一个指针拥有。shared_ptr允许多个指针指向同一个对象,通过引用计数机制来管理对象的生命周期。weak_ptr是一种弱引用,它不会增加引用计数,主要用于解决shared_ptr可能产生的循环引用问题。
auto_ptr是C++98引入的智能指针,但由于其危险的拷贝语义已在C++11中被弃用。每种智能指针都有其特定的使用场景,选择合适的智能指针可以大大降低内存管理的复杂性。
4、unique_ptr 是如何实现的?我们如何强制在 unique_ptr 中仅存在一个对象所有者?
unique_ptr的核心思想是独占式拥有。它通过删除拷贝构造函数和拷贝赋值运算符来保证一个对象只能被一个unique_ptr拥有。它支持移动语义,允许在保证安全的情况下转移对象的所有权。
unique_ptr内部包含一个原始指针和一个删除器。删除器是一个可调用对象,负责在unique_ptr析构时释放所管理的资源。unique_ptr的实现非常轻量,在大多数情况下不会带来任何额外的开销。
5、shared_ptr 如何工作?对象之间如何同步引用计数器?
shared_ptr使用引用计数来追踪有多少个shared_ptr共享同一个对象。当引用计数增加时(比如拷贝构造),计数器加一;当引用计数减少时(比如shared_ptr析构),计数器减一。当计数器降为零时,管理的对象会被删除。
为了保证线程安全,引用计数的更新必须是原子操作。shared_ptr内部实际上包含两个指针:一个指向管理的对象,另一个指向控制块。控制块包含引用计数、删除器等信息。这种实现方式使shared_ptr的大小是原始指针的两倍。
6、我们可以复制unique_ptr或者将其从一个对象传递到另一个对象吗?
由于unique_ptr强制独占所有权,所以它不能被复制。但它可以通过std::move来转移所有权。在转移后,原来的unique_ptr会变为空指针,而目标unique_ptr获得对象的所有权。
这种设计使得unique_ptr可以方便地在函数间传递对象的所有权。例如,factory函数可以返回一个unique_ptr,调用者就获得了返回对象的所有权。unique_ptr也可以存储在容器中,但必须使用移动语义来操作。
7、什么是右值和左值?
在C++中,左值是一个位置值,它标识一个持久的对象。左值有一个地址,可以取地址操作符作用于它。典型的左值包括变量名、解引用的指针等。
右值表示临时值,它不能取地址。右值可以分为纯右值(pr-value)和将亡值(x-value)。纯右值是临时的、不具名的值,如字面常量。将亡值是即将被销毁的值,比如即将被移动的对象。
8、什么是 std::move 和 std::forward()
std::move和std::forward: std::move是一个工具函数,它将一个左值强制转换为右值引用,使得我们可以调用移动构造函数或移动赋值运算符。这在需要显式地指明要进行移动操作时非常有用。
std::forward用于完美转发,它保持参数的值类别(左值或右值)不变。在模板编程中,std::forward可以确保参数按照其原始类型被转发,这对于通用库的实现非常重要。
9、访问某些类的私有字段的方法?
访问私有字段的方法: 在C++中,有几种合法的方式可以访问类的私有成员。最常用的是通过友元(friend)声明,可以是友元函数或友元类。友元声明允许外部代码访问类的私有成员。
另一种方式是通过类的公共接口方法来间接访问私有成员。这是面向对象设计中推荐的方式,因为它维护了类的封装性。还可以使用嵌套类,因为嵌套类可以访问外部类的所有成员。
10、一个类可以继承多个类吗?
类的多重继承: C++支持多重继承,即一个类可以同时继承多个基类。但这可能导致菱形继承问题,即一个类通过不同的路径继承了同一个基类的多个实例。
为了解决这个问题,C++引入了虚继承。虚继承确保共同基类只有一个实例。虽然C++支持多重继承,但在实际开发中应该谨慎使用,因为它可能增加代码的复杂性。
11、静态字段是否在类构造函数中初始化?
静态字段的初始化: 静态成员变量属于类而不是对象,它们不在构造函数中初始化。静态成员需要在类外进行定义和初始化,除非是整型或枚举类型的const static成员,这种情况可以在类内初始化。
这样设计的原因是为了避免静态成员被多次初始化。在程序的整个生命周期中,静态成员只需要初始化一次。静态成员的初始化发生在程序开始执行之前。
12、构造函数/析构函数中会抛出异常吗?如何防止这种情况发生?
构造函数/析构函数的异常: 构造函数可以抛出异常,这通常用于指示初始化失败。但析构函数应该避免抛出异常,因为如果在异常处理过程中析构函数抛出异常,程序会立即终止。
为了防止构造函数抛出异常,可以使用try-catch块包装可能抛出异常的代码,或者使用初始化列表来确保资源获取。对于析构函数,应该将其声明为noexcept,并在内部处理所有可能的异常。
13、什么是虚方法?
虚方法: 虚方法是C++实现运行时多态的机制。当基类中声明一个函数为virtual时,派生类可以重写这个函数。在运行时,程序会根据对象的实际类型来调用适当的函数版本。
虚函数通过虚函数表(vtable)来实现。每个包含虚函数的类都有一个vtable,其中存储了该类虚函数的地址。每个对象都包含一个指向vtable的指针(vptr)。这种机制允许在运行时动态绑定函数调用。
14、为什么我们需要虚拟析构函数?
虚析构函数的必要性: 当通过基类指针删除派生类对象时,如果析构函数不是虚函数,则只会调用基类的析构函数,而不会调用派生类的析构函数。这会导致资源泄漏。
因此,当一个类可能作为基类时,其析构函数应该声明为虚函数。这确保了在删除对象时,无论使用什么类型的指针,都能正确调用整个继承链上的析构函数。
15、抽象类和接口的区别?
抽象类和接口的区别: 抽象类是包含至少一个纯虚函数的类。它不能被实例化,只能作为基类使用。抽象类可以包含普通成员函数和数据成员,这些可以在派生类中直接使用。
接口是一种特殊的抽象类,它只包含纯虚函数。在C++中,接口通常被实现为所有函数都是纯虚函数的抽象类。接口定义了一个对象能够做什么,而不规定如何做。
16、构造函数可以是虚拟的吗?
构造函数的虚拟性: 构造函数不能是虚函数。这是因为在调用构造函数时,对象还没有被完全构造,vptr还没有被初始化。因此不能使用虚函数机制。
如果需要根据运行时条件创建不同类型的对象,可以使用工厂模式。工厂模式通过一个静态成员函数来创建对象,这个函数可以根据参数返回不同类型的对象。
17、关键字 const 如何用于类方法?
const关键字在类方法中的使用: const成员函数承诺不会修改对象的状态。它们可以被const对象调用,这提供了一种编译时的类型安全机制。const成员函数不能调用非const成员函数。如果需要在const成员函数中修改某些成员变量,可以将这些变量声明为mutable。mutable允许在const成员函数中修改特定的成员变量,这通常用于缓存等不影响对象逻辑状态的场合。
18、如何保护对象不被复制?
防止对象被复制: 有几种方式可以防止对象被复制。最现代的方式是使用=delete来删除拷贝构造函数和拷贝赋值运算符。这会在编译时阻止任何复制操作。
另一种方式是将拷贝构造函数和拷贝赋值运算符声明为private,并且不提供实现。这也可以防止复制,但错误信息可能不如=delete清晰。
19、向量和列表之间的区别?
vector和list的区别: vector是一种连续存储的容器,它在内存中分配一块连续的空间。这使得vector支持随机访问,但在中间插入或删除元素时需要移动后续元素。当vector需要更多空间时,它会重新分配一个更大的连续空间。
list是一种链式存储的容器,它的元素可以分散在内存的不同位置。list不支持随机访问,但在任何位置插入或删除元素都很快,因为只需要修改相关节点的指针。list的每个元素都需要额外的内存来存储指针。
20、map和unordered_map之间的区别?
map和unordered_map的区别: map基于红黑树实现,它保持键值对按键的顺序存储。这使得map的查找、插入和删除操作的时间复杂度都是O(log n)。map占用的内存较少,而且可以按序遍历。
unordered_map基于哈希表实现,它不保持任何顺序。在理想情况下,查找、插入和删除操作的时间复杂度都是O(1)。但unordered_map需要额外的内存来存储哈希表,而且可能需要处理哈希冲突。
21 调用 push_back() 时向量中的迭代器无效吗?
vector的push_back和迭代器:
当向vector调用push_back时,迭代器的有效性取决于是否发生了内存重新分配。如果vector当前的capacity足够容纳新元素,那么push_back不会导致迭代器失效。但如果capacity不足,vector会分配一个更大的内存块(通常是当前大小的1.5或2倍),并将所有元素复制到新位置,这时之前的所有迭代器都会失效。
为了避免迭代器失效的问题,有以下几种方法:
- 预先使用reserve分配足够的空间
- 在push_back后重新获取迭代器
- 使用索引而不是迭代器来遍历vector
- 如果必须使用迭代器,可以先完成迭代器操作,再进行push_back
在实际编程中,建议在知道vector大致容量的情况下,先调用reserve预分配空间。这不仅能避免迭代器失效,还能提高程序性能,因为减少了内存重新分配的次数。
22、如何修改你的类以便与 map 和 unordered_map 一起使用?
修改类使其可用于map和unordered_map:
要让自定义类可以作为map的键,必须为该类实现小于运算符(operator<)。这是因为map内部使用红黑树来组织数据,需要通过比较运算来确定元素的位置。operator<必须满足严格弱序关系,即具有传递性且不能出现等价关系。
对于unordered_map,要求更复杂:
- 需要实现相等运算符(operator==)
- 需要提供一个特化的std::hash模板,或者自定义哈希函数
实现示例:
class MyClass {
int id;
string name;
public:
// 为map实现小于运算符
bool operator<(const MyClass& other) const {
if (id != other.id) return id < other.id;
return name < other.name;
}
// 为unordered_map实现相等运算符
bool operator==(const MyClass& other) const {
return id == other.id && name == other.name;
}
};
// 为unordered_map特化hash模板
namespace std {
template<>
struct hash<MyClass> {
size_t operator()(const MyClass& obj) const {
return hash<int>()(obj.id) ^ hash<string>()(obj.name);
}
};
}
哈希函数的质量会直接影响unordered_map的性能。好的哈希函数应该能够均匀分布键值,避免产生过多的哈希冲突。在选择容器时,如果需要保持元素顺序就用map,如果需要最快的查找性能就用unordered_map。
23、同步线程的方法?
在C++中,有多种线程同步机制可以确保多线程程序的正确性:
- 互斥锁(mutex):
最基本的同步机制,用于保护共享资源。C++提供了多种互斥锁:
- std::mutex:标准互斥锁
- std::recursive_mutex:允许同一线程多次加锁
- std::timed_mutex:支持超时的互斥锁
- std::shared_mutex:读写锁,允许多个读者或一个写者
- 条件变量(condition_variable):
用于线程间的通信和同步,典型用法是等待某个条件成立:
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
// 消费者线程
{
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []{ return ready; });
// 处理数据
}
// 生产者线程
{
std::lock_guard<std::mutex> lock(mtx);
ready = true;
cv.notify_one();
}
-
原子操作:
对于简单的数据类型,使用std::atomic可以避免显式的锁定:
std::atomic counter{0};
counter++; // 原子操作,线程安全 -
读写锁:
当读操作远多于写操作时,使用std::shared_mutex可以提高并发性:
std::shared_mutex rwlock;
// 读者
{
std::shared_lock<std::shared_mutex> lock(rwlock);
// 读取数据
}
// 写者
{
std::unique_lock<std::shared_mutex> lock(rwlock);
// 修改数据
}
选择合适的同步机制取决于具体场景:
- 简单的共享资源保护用mutex
- 需要线程间通信用condition_variable
- 简单数据类型的并发访问用atomic
- 读多写少的场景用shared_mutex
5.避免死锁一般通过以下方法:
- 始终按照相同的顺序获取锁
- 使用std::lock同时获取多个锁
- 避免在持有锁时调用用户代码
- 使用RAII风格的锁管理