C++ 八股¶
复习一下C++
Q: Vector¶
C++ 中vector的底层实现?64位条件下vector容器本身的大小是多少,为什么?
A: 大小: vector由一个data指针(相当于数组头指针)、一个size和一个capacity组成。指针指向动态分配的内存,size表示当前元素个数,capacity表示分配的内存大小。64位条件下,vector容器本身的大小是24字节(8+8+8),因为指针占8字节,size和capacity各占8字节,总体为24 + capacity * sizeof(T)字节。
结构: vector实际是泛型的动态类型顺序表,因此底层是一段连续的内存空间,用三个指针指向内存空间,start,finish,end_of_storage 然后用他们来实现以下几种方法:begin(),end(),size(),capacity(),empty(),push_back(),pop_back()等。
扩容: 当 vector 的大小和容量相等(size==capacity)也就是满载时,如果再向其添加元素,那么 vector 就需要扩容。
vector 容器扩容的过程需要经历以下 3 步: 1. 完全弃用现有的内存空间,重新申请更大的内存空间(VS2015中以1.5倍扩容,GCC以2倍扩容。扩容倍数为2时,时间上占优势;扩容倍数为1.5时,空间上占优势。) 2. 将旧内存空间中的数据,按原有顺序移动到新的内存空间中; 3. 最后将旧的内存空间释放。
Q: 多线程Vector¶
多线程下同时修改一个vector会有什么问题?如何解决?
A:在多线程环境下,如果多个线程同时修改同一个 std::vector,会遇到以下几个问题:
数据竞态
- 问题:多个线程同时修改 std::vector(如插入、删除或修改元素)时,如果没有同步机制,可能会导致数据不一致或程序崩溃。例如,一个线程正在向 vector 添加元素,而另一个线程在同一时刻也在进行修改操作(如删除或修改元素),可能会导致访问非法内存或不一致的状态。
- 解决方法:使用同步机制(如互斥锁 std::mutex)来保证在同一时刻只有一个线程访问 vector。
内存访问冲突
- 问题:std::vector 内部是动态分配内存的,在进行扩容时,它可能会重新分配更大的内存区域,并将数据复制到新位置。如果有线程正在访问或修改 vector,而另一个线程执行扩容,可能导致内存访问冲突、指针悬挂或程序崩溃。
- 解决方法:保证在对 vector 执行扩容时,其他线程不对其进行读写操作。可以通过锁保护整个 vector 或使用其他同步方式。
不确定的迭代器行为
- 问题:如果一个线程正在迭代 vector,而另一个线程在同一时间修改它(例如,增加或删除元素),则迭代器可能变得无效,导致访问越界或未定义行为。
- 解决方法:避免在多线程中同时进行迭代操作和修改操作。如果必须进行修改,使用锁或其他同步机制确保迭代和修改操作不冲突。
解决方法:使用互斥锁
使用 std::mutex 来保护对 std::vector 的访问,使得每次只能有一个线程操作 vector,其他线程必须等待。下面是一个使用 std::mutex 来同步访问 vector 的示例:
示例(C++):
#include <iostream>
#include <vector>
#include <thread>
#include <mutex>
std::vector<int> data;
std::mutex mtx; // 互斥锁
// 修改vector的函数
void modifyVector(int value) {
std::lock_guard<std::mutex> lock(mtx); // 使用锁保护对vector的修改
data.push_back(value);
}
// 读取vector的函数
void readVector() {
std::lock_guard<std::mutex> lock(mtx); // 使用锁保护对vector的读取
for (int num : data) {
std::cout << num << " ";
}
std::cout << std::endl;
}
int main() {
std::thread t1(modifyVector, 10);
std::thread t2(modifyVector, 20);
std::thread t3(readVector);
t1.join();
t2.join();
t3.join();
return 0;
}
解释:
- std::lock_guard<std::mutex>:这是一种 RAII 风格的锁,确保每次访问 vector 时都能自动获得锁,并且在操作完成后释放锁,避免手动管理锁。
- 同步访问:在修改和读取 vector 时,所有线程都需要获取锁,保证同一时间只有一个线程可以访问 vector。
解决方案总结:
- 使用
std::mutex锁定vector:确保每次访问vector时,只有一个线程可以操作。 - 避免在多线程中同时修改和遍历
vector:操作和读取vector时,都要加锁,以防止数据不一致和内存冲突。 - 如果需要频繁读写,考虑使用其他同步机制:例如 读写锁(
std::shared_mutex),允许多个线程并行读取,而写入时仍然是独占访问。
这样通过使用同步机制可以有效地避免并发访问 vector 时的数据竞态问题,保证程序的正确性和稳定性。
Q: C++11新特性¶
C++11有哪些重要的新特性?
A: 主要新特性包括:
-
auto关键字: 自动类型推导
auto x = 10; // x为int类型 auto y = 3.14; // y为double类型 auto z = "hello"; // z为const char*类型 -
nullptr: 空指针常量,替代NULL
int* p = nullptr; // 比NULL更安全 -
范围for循环: 简化容器遍历
vector<int> v = {1, 2, 3, 4, 5}; for (const auto& x : v) { cout << x << " "; } -
右值引用和移动语义: 提高性能,减少拷贝
string&& rref = move(str); // 右值引用 -
Lambda表达式: 匿名函数
auto lambda = [](int x) { return x * 2; }; -
智能指针: 自动内存管理
unique_ptr<int> ptr = make_unique<int>(42); shared_ptr<int> sptr = make_shared<int>(42); -
列表初始化: 统一初始化语法
vector<int> v{1, 2, 3, 4, 5}; int arr[]{1, 2, 3, 4, 5}; -
decltype: 类型推导
int x = 10; decltype(x) y = 20; // y的类型为int -
constexpr: 编译期常量表达式
constexpr int factorial(int n) { return n <= 1 ? 1 : n * factorial(n - 1); } -
线程支持库: 标准多线程支持
#include <thread> std::thread t([]{ cout << "Hello from thread\n"; });
Q: 引用(&)和右值引用(&&)¶
左值引用和右值引用的区别?什么是移动语义?
A:
左值引用(&): - 绑定到左值(有名字的对象,可以取地址) - 延长对象生命周期 - 不能绑定到临时对象(右值)
int x = 10;
int& lref = x; // 正确,绑定到左值
// int& lref2 = 20; // 错误,不能绑定到右值
右值引用(&&): - 绑定到右值(临时对象,不能取地址) - 支持移动语义,避免不必要的拷贝 - 可以"窃取"临时对象的资源
int&& rref = 20; // 正确,绑定到右值
string&& s = string("hello"); // 绑定到临时对象
移动语义: 移动构造函数和移动赋值运算符,"窃取"资源而不是拷贝
class MyString {
char* data;
size_t size;
public:
// 移动构造函数
MyString(MyString&& other) noexcept
: data(other.data), size(other.size) {
other.data = nullptr; // 窃取资源
other.size = 0;
}
// 移动赋值运算符
MyString& operator=(MyString&& other) noexcept {
if (this != &other) {
delete[] data;
data = other.data;
size = other.size;
other.data = nullptr;
other.size = 0;
}
return *this;
}
};
完美转发:
template<typename T>
void wrapper(T&& arg) {
func(std::forward<T>(arg)); // 保持参数的值类别
}
Q: static关键字¶
static关键字的不同用法和作用?
A:
1. 静态局部变量: - 只初始化一次,程序结束时销毁 - 保持函数调用间的状态
int counter() {
static int count = 0; // 只初始化一次
return ++count;
}
// 每次调用counter(),count都会递增
2. 静态全局变量: - 限制在当前文件内可见 - 内部链接性
static int global_var = 10; // 只在当前文件可见
3. 静态成员变量: - 属于类而不是对象 - 所有对象共享同一份 - 需要在类外定义
class MyClass {
static int static_var; // 声明
public:
static int getStaticVar() { return static_var; }
};
int MyClass::static_var = 0; // 定义
4. 静态成员函数: - 不依赖于对象实例 - 不能访问非静态成员 - 可以通过类名直接调用
class Math {
public:
static int add(int a, int b) {
return a + b;
}
};
// 调用: Math::add(1, 2)
Q: const关键字¶
const的各种用法和作用?
A:
1. 常量变量:
const int x = 10; // x不能被修改
const int* p1 = &x; // 指向常量的指针,不能通过p1修改值
int* const p2 = &y; // 常量指针,p2不能指向其他地址
const int* const p3 = &x; // 常量指针指向常量
2. 常量成员函数: - 不修改对象状态 - const对象只能调用const成员函数
class MyClass {
int value;
public:
int getValue() const { // const成员函数
return value; // 不能修改成员变量
}
void setValue(int v) { // 非const成员函数
value = v;
}
};
const MyClass obj;
obj.getValue(); // 正确
// obj.setValue(10); // 错误,const对象不能调用非const函数
3. 常量引用:
const int& ref = x; // 常量引用,不能通过ref修改x
4. 返回值const:
const string& getName() const { // 返回常量引用
return name;
}
5. mutable关键字: 允许在const函数中修改特定成员
class Cache {
mutable int cache_hits; // 可在const函数中修改
public:
int getValue() const {
++cache_hits; // 正确,mutable成员
return value;
}
};
Q: 多态¶
C++中的多态机制是什么?静态多态和动态多态的区别?
A:
多态定义: 同一接口,不同实现。允许使用基类指针或引用调用派生类的方法。
静态多态(编译期多态):
- 函数重载
- 运算符重载
- 模板
// 函数重载
void print(int x) { cout << "int: " << x << endl; }
void print(double x) { cout << "double: " << x << endl; }
// 模板
template<typename T>
void func(T x) { cout << x << endl; }
动态多态(运行期多态): 通过虚函数和继承实现
class Animal {
public:
virtual void makeSound() { // 虚函数
cout << "Animal sound" << endl;
}
virtual ~Animal() = default; // 虚析构函数
};
class Dog : public Animal {
public:
void makeSound() override { // 重写虚函数
cout << "Woof!" << endl;
}
};
class Cat : public Animal {
public:
void makeSound() override {
cout << "Meow!" << endl;
}
};
// 使用多态
Animal* animals[] = {new Dog(), new Cat()};
for (auto* animal : animals) {
animal->makeSound(); // 运行时决定调用哪个版本
}
虚函数实现原理: - 每个包含虚函数的类都有虚函数表(vtable) - 每个对象都有虚函数表指针(vptr) - 通过vptr找到vtable,再找到对应的函数
纯虚函数和抽象类:
class Shape {
public:
virtual double getArea() = 0; // 纯虚函数
virtual ~Shape() = default;
};
// Shape是抽象类,不能实例化
虚析构函数的重要性:
class Base {
public:
virtual ~Base() { // 虚析构函数
cout << "Base destructor" << endl;
}
};
class Derived : public Base {
public:
~Derived() {
cout << "Derived destructor" << endl;
}
};
Base* ptr = new Derived();
delete ptr; // 正确调用Derived析构函数,再调用Base析构函数
Q: HTTP协议¶
HTTP请求和响应的格式是什么?
A:
HTTP请求格式:
GET /index.html HTTP/1.1 // 请求行:方法 路径 版本
Host: www.example.com // 请求头
User-Agent: Mozilla/5.0
Accept: text/html
Content-Length: 0
[请求体] // 可选,GET通常为空
HTTP响应格式:
HTTP/1.1 200 OK // 状态行:版本 状态码 状态描述
Content-Type: text/html // 响应头
Content-Length: 1234
Server: Apache/2.4
<html> // 响应体
<body>Hello World</body>
</html>
常见HTTP方法: - GET: 获取资源 - POST: 提交数据 - PUT: 更新资源 - DELETE: 删除资源 - HEAD: 获取头部信息 - OPTIONS: 获取支持的方法
常见状态码: - 2xx: 成功 - 200 OK: 请求成功 - 201 Created: 资源创建成功 - 3xx: 重定向 - 301 Moved Permanently: 永久重定向 - 302 Found: 临时重定向 - 4xx: 客户端错误 - 400 Bad Request: 请求错误 - 401 Unauthorized: 未授权 - 404 Not Found: 资源未找到 - 5xx: 服务器错误 - 500 Internal Server Error: 服务器内部错误 - 503 Service Unavailable: 服务不可用
HTTP头部字段:
// 请求头
Host: www.example.com // 目标主机
User-Agent: browser-info // 客户端信息
Accept: text/html // 可接受的内容类型
Cookie: session=abc123 // Cookie信息
// 响应头
Content-Type: text/html // 内容类型
Content-Length: 1234 // 内容长度
Set-Cookie: session=xyz789 // 设置Cookie
Cache-Control: max-age=3600 // 缓存控制
Q: C++锁机制¶
C++中有哪些锁类型?它们的区别和使用场景是什么?
A:
1. 互斥锁(mutex): 最基本的锁,同一时间只能有一个线程获取
#include <mutex>
std::mutex mtx;
void criticalSection() {
mtx.lock(); // 手动加锁
// 临界区代码
mtx.unlock(); // 手动解锁
}
// 推荐使用RAII方式
void safeCriticalSection() {
std::lock_guard<std::mutex> lock(mtx); // 自动加锁
// 临界区代码
} // 自动解锁
2. 递归锁(recursive_mutex): 允许同一线程多次获取同一个锁
std::recursive_mutex rec_mtx;
void recursiveFunction(int n) {
std::lock_guard<std::recursive_mutex> lock(rec_mtx);
if (n > 0) {
recursiveFunction(n - 1); // 同一线程再次获取锁
}
}
3. 定时锁(timed_mutex): 支持超时的互斥锁
std::timed_mutex tm_mtx;
void timedLockExample() {
// 尝试在1秒内获取锁
if (tm_mtx.try_lock_for(std::chrono::seconds(1))) {
// 成功获取锁
// 临界区代码
tm_mtx.unlock();
} else {
// 超时,处理失败情况
}
}
4. 读写锁(shared_mutex, C++17): 允许多个线程同时读,但写操作独占
#include <shared_mutex>
std::shared_mutex rw_mtx;
int shared_data = 0;
// 读操作
void reader() {
std::shared_lock<std::shared_mutex> lock(rw_mtx); // 共享锁
int value = shared_data; // 多个线程可以同时读
}
// 写操作
void writer() {
std::unique_lock<std::shared_mutex> lock(rw_mtx); // 独占锁
shared_data = 42; // 只有一个线程可以写
}
5. 条件变量(condition_variable): 用于线程间同步,等待特定条件
#include <condition_variable>
std::mutex cv_mtx;
std::condition_variable cv;
bool ready = false;
// 等待线程
void waiter() {
std::unique_lock<std::mutex> lock(cv_mtx);
cv.wait(lock, []{ return ready; }); // 等待ready为true
// 条件满足后继续执行
}
// 通知线程
void notifier() {
{
std::lock_guard<std::mutex> lock(cv_mtx);
ready = true;
}
cv.notify_one(); // 通知一个等待线程
// cv.notify_all(); // 通知所有等待线程
}
6. 自旋锁(atomic_flag): 轻量级锁,使用忙等待
#include <atomic>
class SpinLock {
std::atomic_flag flag = ATOMIC_FLAG_INIT;
public:
void lock() {
while (flag.test_and_set(std::memory_order_acquire)) {
// 忙等待
}
}
void unlock() {
flag.clear(std::memory_order_release);
}
};
锁的RAII封装类:
lock_guard: 最简单的RAII锁封装
{
std::lock_guard<std::mutex> lock(mtx);
// 自动加锁,作用域结束自动解锁
}
unique_lock: 更灵活的锁封装
std::unique_lock<std::mutex> lock(mtx);
// 可以手动解锁
lock.unlock();
// 可以重新加锁
lock.lock();
// 可以转移所有权
std::unique_lock<std::mutex> lock2 = std::move(lock);
shared_lock: 共享锁封装(C++14)
std::shared_lock<std::shared_mutex> lock(rw_mtx);
// 允许多个线程同时持有
死锁预防:
1. 避免嵌套锁:
// 危险:可能死锁
void thread1() {
std::lock_guard<std::mutex> lock1(mtx1);
std::lock_guard<std::mutex> lock2(mtx2);
}
void thread2() {
std::lock_guard<std::mutex> lock1(mtx2); // 顺序相反
std::lock_guard<std::mutex> lock2(mtx1);
}
2. 使用std::lock同时获取多个锁:
void safeLocking() {
std::lock(mtx1, mtx2); // 同时获取多个锁
std::lock_guard<std::mutex> lock1(mtx1, std::adopt_lock);
std::lock_guard<std::mutex> lock2(mtx2, std::adopt_lock);
}
3. 使用scoped_lock(C++17):
void scopedLocking() {
std::scoped_lock lock(mtx1, mtx2); // 自动处理多锁获取
}
性能考虑:
锁的开销比较: - atomic操作 < 自旋锁 < 互斥锁 < 读写锁
选择建议: - 短临界区: 使用自旋锁或atomic - 长临界区: 使用互斥锁 - 读多写少: 使用读写锁 - 需要超时: 使用timed_mutex - 递归调用: 使用recursive_mutex
最佳实践:
class ThreadSafeCounter {
mutable std::mutex mtx; // mutable允许在const函数中使用
int count = 0;
public:
void increment() {
std::lock_guard<std::mutex> lock(mtx);
++count;
}
int getValue() const {
std::lock_guard<std::mutex> lock(mtx);
return count;
}
// 返回副本而不是引用,避免锁外访问
int getValueSafe() const {
std::lock_guard<std::mutex> lock(mtx);
return count; // 返回副本
}
};
无锁编程(Lock-Free): 使用atomic操作避免锁
#include <atomic>
class LockFreeCounter {
std::atomic<int> count{0};
public:
void increment() {
count.fetch_add(1, std::memory_order_relaxed);
}
int getValue() const {
return count.load(std::memory_order_relaxed);
}
};