字节跳动C++开发一面
1. C++中的智能指针有哪些?它们的区别和使用场景是什么?
答案:
三种智能指针:
1. unique_ptr(独占所有权)
- 独占资源,不可复制,只能移动
- 开销最小,性能最好
- 使用场景:明确单一所有者的资源管理
std::unique_ptr<int> p1(new int(10)); std::unique_ptr<int> p2 = std::move(p1); // 转移所有权
2. shared_ptr(共享所有权)
- 引用计数,多个指针共享同一资源
- 最后一个指针销毁时释放资源
- 线程安全(引用计数操作)
- 使用场景:多个对象共享资源
std::shared_ptr<int> p1 = std::make_shared<int>(10); std::shared_ptr<int> p2 = p1; // 引用计数+1
3. weak_ptr(弱引用)
- 不增加引用计数,不影响对象生命周期
- 解决shared_ptr循环引用问题
- 使用前需要lock()转换为shared_ptr
std::weak_ptr<int> wp = sp;
if(auto p = wp.lock()) { // 检查对象是否存在
// 使用p
}
对比表:
所有权 |
独占 |
共享 |
不拥有 |
可复制 |
✗ |
✓ |
✓ |
引用计数 |
无 |
有 |
不增加计数 |
开销 |
最小 |
较大 |
小 |
2. 虚函数的实现原理是什么?虚函数表在内存中如何布局?
答案:
实现原理:
- 每个包含虚函数的类都有一个虚函数表(vtable)
- 每个对象有一个**虚函数指针(vptr)**指向虚函数表
- 通过vptr查表实现动态绑定
内存布局:
class Base {
int a;
virtual void func1() {}
virtual void func2() {}
};
// 对象内存:[vptr][a]
// vtable: [&Base::func1][&Base::func2]
class Derived : public Base {
int b;
void func1() override {} // 覆盖
};
// 对象内存:[vptr][a][b]
// vtable: [&Derived::func1][&Base::func2]
关键点:
- vptr通常位于对象内存的开头
- 虚函数表在编译期生成,存储在只读数据段
- 构造函数中会设置vptr
- 多继承时可能有多个vptr
性能影响:
- 额外的指针开销(8字节)
- 函数调用需要两次间接寻址
- 无法内联优化
3. 左值和右值的区别?什么是移动语义?
答案:
左值(lvalue)vs 右值(rvalue):
- 左值:有名字,可取地址,持久存在 变量、数组元素、返回左值引用的函数
- 右值:临时对象,不可取地址,即将销毁 字面量、临时对象、返回值
int a = 10; // a是左值,10是右值 int b = a + 1; // b是左值,a+1是右值
移动语义(Move Semantics):
- C++11引入,避免不必要的拷贝
- 通过"窃取"资源而非复制来转移所有权
- 使用右值引用
&&实现
移动构造函数:
class String {
char* data;
public:
// 拷贝构造(深拷贝)
String(const String& s) {
data = new char[strlen(s.data) + 1];
strcpy(data, s.data);
}
// 移动构造(转移所有权)
String(String&& s) noexcept {
data = s.data; // 窃取资源
s.data = nullptr; // 置空源对象
}
};
std::move的作用:
- 将左值转换为右值引用
- 不移动任何东西,只是类型转换
String s1("hello");
String s2 = std::move(s1); // 调用移动构造
使用场景:
- 容器元素的插入/删除
- 返回局部对象
- 资源管理类(unique_ptr)
4. const关键字的作用?const成员函数能修改成员变量吗?
答案:
const的多种用法:
1. 修饰变量
const int a = 10; // 常量 int const b = 20; // 同上 const int* p1; // 指向常量的指针(不能改*p1) int* const p2; // 常量指针(不能改p2) const int* const p3; // 都不能改
2. 修饰函数参数
void func(const string& s); // 避免拷贝,防止修改
3. 修饰成员函数
class A {
int value;
public:
int getValue() const { // 承诺不修改成员变量
return value;
}
};
const成员函数能否修改成员变量?
一般情况:不能
void func() const {
value = 10; // 编译错误
}
例外:mutable关键字
class Cache {
mutable int access_count; // 可在const函数中修改
public:
int getData() const {
access_count++; // 允许
return data;
}
};
const对象只能调用const成员函数:
const A obj; obj.getValue(); // OK obj.setValue(5); // 错误(如果setValue不是const)
5. 什么是RAII?如何实现一个线程安全的单例模式?
答案:
RAII(Resource Acquisition Is Initialization):
- 资源获取即初始化
- 利用对象生命周期管理资源
- 构造函数获取资源,析构函数释放资源
典型应用:
// 智能指针
std::unique_ptr<int> p(new int(10)); // 自动释放
// 互斥锁
std::lock_guard<std::mutex> lock(mtx); // 自动解锁
// 文件操作
std::ifstream file("data.txt"); // 自动关闭
线程安全的单例模式:
方法1:C++11局部静态变量(推荐)
class Singleton {
private:
Singleton() {}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
public:
static Singleton& getInstance() {
static Singleton instance; // C++11保证线程安全
return instance;
}
};
方法2:双重检查锁(DCLP)
class Singleton {
private:
static std::atomic<Singleton*> instance;
static std::mutex mtx;
public:
static Singleton* getInstance() {
Singleton* tmp = instance.load(std::memory_order_acquire);
if (tmp == nullptr) {
std::lock_guard<std::mutex> lock(mtx);
tmp = instance.load(std::memory_order_relaxed);
if (tmp == nullptr) {
tmp = new Singleton();
instance.store(tmp, std::memory_order_release);
}
}
return tmp;
}
};
为什么C++11局部静态变量是线程安全的?
- 编译器保证初始化时的互斥
- 只初始化一次
- 简洁高效
6. vector和list的区别?什么时候用vector,什么时候用list?
答案:
底层实现:
- vector:动态数组,连续内存
- list:双向链表,非连续内存
性能对比:
随机访问 |
O(1) |
O(n) |
头部插入 |
O(n) |
O(1) |
尾部插入 |
O(1)均摊 |
O(1) |
中间插入 |
O(n) |
O(1) |
内存占用 |
小 |
大(额外指 |
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
C++八股文全集 文章被收录于专栏
本专栏系统梳理C++技术面试核心考点,涵盖语言基础、面向对象、内存管理、STL容器、模板编程及经典算法。从引用指针、虚函数表、智能指针等底层原理,到继承多态、运算符重载等OOP特性从const、static、inline等关键字辨析,到动态规划、KMP算法、并查集等手写实现。每个知识点以面试答题形式呈现,注重原理阐述而非冗长代码,帮助你快速构建完整知识体系,从容应对面试官提问,顺利拿下offer。
