C++ 基础

Q1:C++ 和 C 的区别?

主要区别:

  1. 面向对象编程

    • C++ 支持面向对象编程(封装、继承、多态)
    • C 是过程式编程语言
  2. 函数重载

    • C++ 支持函数重载
    • C 不支持函数重载
  3. 命名空间

    • C++ 有命名空间(namespace)
    • C 没有命名空间
  4. 引用

    • C++ 支持引用(reference)
    • C 只有指针
  5. 异常处理

    • C++ 支持异常处理(try-catch)
    • C 没有异常处理机制
  6. 内存管理

    • C++ 有 new/delete 操作符
    • C 使用 malloc/free
  7. 类型检查

    • C++ 类型检查更严格
    • C 类型检查相对宽松

Q2:C++ 中 struct 和 class 的区别?

主要区别:

  1. 默认访问权限

    • struct 默认成员是 public
    • class 默认成员是 private
  2. 默认继承方式

    • struct 默认继承是 public
    • class 默认继承是 private
  3. 其他方面

    • 功能基本相同,都可以有构造函数、析构函数、成员函数等
    • struct 在 C++ 中主要用于兼容 C,或者用于简单的数据聚合
1
2
3
4
5
6
7
8
9
10
11
12
// struct 示例
struct Point {
int x, y; // 默认 public
Point(int x, int y) : x(x), y(y) {}
};

// class 示例
class Point {
int x, y; // 默认 private
public:
Point(int x, int y) : x(x), y(y) {}
};

Q3:C++ 中的四种类型转换?

C++ 提供了四种类型转换操作符:

  1. static_cast
    • 用于基本类型转换、父子类指针转换
    • 编译时检查,不保证运行时安全
1
2
3
4
5
int i = 10;
double d = static_cast<double>(i);

Base* base = new Derived();
Derived* derived = static_cast<Derived*>(base); // 不安全,需要确保类型正确
  1. dynamic_cast
    • 用于多态类型之间的转换
    • 运行时检查,转换失败返回 nullptr(指针)或抛出异常(引用)
    • 只能用于有虚函数的类
1
2
3
4
5
Base* base = new Derived();
Derived* derived = dynamic_cast<Derived*>(base);
if (derived != nullptr) {
// 转换成功
}
  1. const_cast
    • 用于移除 const 或 volatile 属性
    • 主要用于函数重载场景
1
2
const int* p = &i;
int* q = const_cast<int*>(p);
  1. reinterpret_cast
    • 用于低级别的类型转换
    • 非常危险,不保证可移植性
    • 主要用于指针和整数之间的转换
1
2
int* p = new int(10);
long addr = reinterpret_cast<long>(p);

内存管理

Q4:new/delete 和 malloc/free 的区别?

主要区别:

  1. 类型安全

    • new/delete 是操作符,类型安全
    • malloc/free 是函数,需要手动计算大小
  2. 构造函数/析构函数

    • new 会调用构造函数
    • delete 会调用析构函数
    • malloc/free 不会调用构造函数/析构函数
  3. 返回值

    • new 返回具体类型指针
    • malloc 返回 void*,需要强制转换
  4. 失败处理

    • new 失败抛出 std::bad_alloc 异常
    • malloc 失败返回 NULL
  5. 重载

    • new/delete 可以重载
    • malloc/free 不能重载
1
2
3
4
5
6
7
8
9
10
11
12
13
// new/delete 示例
int* p = new int(10);
delete p;

int* arr = new int[10];
delete[] arr;

// malloc/free 示例
int* p = (int*)malloc(sizeof(int));
free(p);

int* arr = (int*)malloc(sizeof(int) * 10);
free(arr);

Q5:什么是内存泄漏?如何避免?

内存泄漏:
程序在运行过程中,动态分配的内存没有被正确释放,导致内存无法被再次使用。

常见原因:

  1. 使用 new 分配内存后忘记 delete
  2. 异常导致 delete 未执行
  3. 循环引用(智能指针)

避免方法:

  1. 使用智能指针
    • std::unique_ptr:独占所有权
    • std::shared_ptr:共享所有权
    • std::weak_ptr:弱引用,解决循环引用
1
2
3
4
5
6
7
8
9
10
11
12
13
// 使用 unique_ptr
std::unique_ptr<int> p(new int(10));
// 自动释放,无需手动 delete

// 使用 shared_ptr
std::shared_ptr<int> p1 = std::make_shared<int>(10);
std::shared_ptr<int> p2 = p1; // 引用计数为 2

// 使用 weak_ptr 解决循环引用
struct Node {
std::shared_ptr<Node> next;
std::weak_ptr<Node> prev; // 使用 weak_ptr 避免循环引用
};
  1. RAII(Resource Acquisition Is Initialization)
    • 资源获取即初始化
    • 在对象构造时获取资源,在析构时释放资源
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class FileHandle {
FILE* file;
public:
FileHandle(const char* filename) {
file = fopen(filename, "r");
if (!file) throw std::runtime_error("Cannot open file");
}

~FileHandle() {
if (file) fclose(file); // 自动释放资源
}

FILE* get() { return file; }
};
  1. 使用容器
    • 优先使用 STL 容器(vector、string 等),自动管理内存

Q6:堆和栈的区别?

主要区别:

  1. 分配方式

    • 栈:由编译器自动分配和释放
    • 堆:由程序员手动分配和释放
  2. 速度

    • 栈:速度快,直接操作栈指针
    • 堆:速度慢,需要查找合适的内存块
  3. 大小限制

    • 栈:大小有限(通常几MB)
    • 堆:大小受限于系统可用内存
  4. 内存碎片

    • 栈:不会产生内存碎片
    • 堆:可能产生内存碎片
  5. 生命周期

    • 栈:局部变量,函数结束时自动释放
    • 堆:需要手动释放,否则可能内存泄漏
1
2
3
4
5
6
7
8
void example() {
int stack_var = 10; // 栈上分配
int* heap_var = new int(10); // 堆上分配

// stack_var 在函数结束时自动释放
// heap_var 需要手动 delete,否则内存泄漏
delete heap_var;
}

指针和引用

Q7:指针和引用的区别?

主要区别:

  1. 初始化

    • 引用必须在定义时初始化,且不能改变指向
    • 指针可以先定义后初始化,可以改变指向
  2. 空值

    • 引用不能为空,必须指向有效对象
    • 指针可以为空(nullptr)
  3. 操作

    • 引用是别名,使用起来像普通变量
    • 指针需要解引用(*)才能访问指向的对象
  4. 内存

    • 引用不占用额外内存(编译器优化)
    • 指针占用内存(存储地址)
  5. 多级

    • 引用没有多级引用
    • 指针可以有多级指针
1
2
3
4
5
6
7
8
9
10
11
12
13
int a = 10;
int b = 20;

// 引用
int& ref = a; // 必须初始化
ref = b; // 这是赋值,不是改变指向,a 的值变为 20
// int& ref2; // 错误:引用必须初始化

// 指针
int* ptr = &a; // 可以后初始化
ptr = &b; // 可以改变指向
ptr = nullptr; // 可以为空
int** pptr = &ptr; // 多级指针

Q8:什么是野指针和悬空指针?

野指针(Wild Pointer):
未初始化的指针,指向未知内存地址。

1
2
int* p;  // 野指针,未初始化
*p = 10; // 危险!可能崩溃

悬空指针(Dangling Pointer):
指向已经被释放的内存地址的指针。

1
2
3
4
int* p = new int(10);
delete p; // 释放内存
*p = 20; // 危险!p 现在是悬空指针
p = nullptr; // 好的做法:释放后置空

避免方法:

  1. 初始化指针为 nullptr
  2. 释放后立即置空
  3. 使用智能指针

面向对象

Q9:C++ 中的多态是如何实现的?

多态分为两种:

  1. 编译时多态(静态多态)

    • 函数重载
    • 模板
  2. 运行时多态(动态多态)

    • 虚函数
    • 通过虚函数表(vtable)实现

虚函数表机制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Base {
public:
virtual void func1() { cout << "Base::func1" << endl; }
virtual void func2() { cout << "Base::func2" << endl; }
void func3() { cout << "Base::func3" << endl; } // 非虚函数
};

class Derived : public Base {
public:
void func1() override { cout << "Derived::func1" << endl; }
void func2() override { cout << "Derived::func2" << endl; }
void func3() { cout << "Derived::func3" << endl; }
};

Base* b = new Derived();
b->func1(); // 输出:Derived::func1(多态)
b->func2(); // 输出:Derived::func2(多态)
b->func3(); // 输出:Base::func3(非虚函数,无多态)

虚函数表原理:

  • 每个有虚函数的类都有一个虚函数表
  • 对象中包含一个指向虚函数表的指针(vptr)
  • 调用虚函数时,通过 vptr 找到虚函数表,再调用对应的函数

Q10:虚函数和纯虚函数的区别?

虚函数(Virtual Function):

  • 有函数体,可以被子类重写
  • 子类可以不重写
1
2
3
4
5
6
class Base {
public:
virtual void func() {
cout << "Base::func" << endl;
}
};

纯虚函数(Pure Virtual Function):

  • 没有函数体,用 = 0 表示
  • 包含纯虚函数的类是抽象类,不能实例化
  • 子类必须实现纯虚函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Base {
public:
virtual void func() = 0; // 纯虚函数
};

class Derived : public Base {
public:
void func() override {
cout << "Derived::func" << endl;
}
};

// Base b; // 错误:抽象类不能实例化
Derived d; // 正确

Q11:构造函数和析构函数可以是虚函数吗?

构造函数:

  • 不能是虚函数
  • 原因:对象在构造时,虚函数表还未建立,无法通过虚函数表调用

析构函数:

  • 应该是虚函数(如果类会被继承)
  • 原因:确保通过基类指针删除派生类对象时,能正确调用派生类的析构函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Base {
public:
Base() { cout << "Base constructor" << endl; }
virtual ~Base() { cout << "Base destructor" << endl; } // 应该是虚函数
};

class Derived : public Base {
public:
Derived() { cout << "Derived constructor" << endl; }
~Derived() { cout << "Derived destructor" << endl; }
};

Base* b = new Derived();
delete b; // 如果析构函数不是虚函数,只会调用 Base 的析构函数,导致内存泄漏
// 输出:
// Base constructor
// Derived constructor
// Derived destructor // 正确调用
// Base destructor

STL 容器

Q12:vector 的底层实现原理?

vector 底层实现:

  • 动态数组,使用连续内存存储
  • 当容量不足时,会重新分配更大的内存(通常是原来的 2 倍)
  • 将旧数据拷贝到新内存,释放旧内存

关键点:

  1. 容量(capacity)和大小(size)

    • size():当前元素个数
    • capacity():当前容量
  2. 扩容机制

    • 扩容时,所有迭代器、指针、引用都会失效
    • 扩容代价高,需要拷贝所有元素
1
2
3
4
5
6
vector<int> v;
v.push_back(1); // size=1, capacity=1
v.push_back(2); // size=2, capacity=2
v.push_back(3); // size=3, capacity=4(扩容)
v.push_back(4); // size=4, capacity=4
v.push_back(5); // size=5, capacity=8(扩容)
  1. 时间复杂度
    • 随机访问:O(1)
    • 尾部插入:平均 O(1),最坏 O(n)(需要扩容)
    • 中间插入:O(n)

Q13:map 和 unordered_map 的区别?

主要区别:

  1. 底层实现

    • map:红黑树(平衡二叉搜索树)
    • unordered_map:哈希表
  2. 有序性

    • map:有序(按 key 排序)
    • unordered_map:无序
  3. 时间复杂度

    • map:查找、插入、删除 O(log n)
    • unordered_map:平均 O(1),最坏 O(n)
  4. 内存占用

    • map:相对较少
    • unordered_map:相对较多(需要维护哈希表)
  5. key 要求

    • map:key 需要支持 < 操作符
    • unordered_map:key 需要支持哈希函数和 == 操作符
1
2
3
4
5
6
7
8
9
10
11
12
13
// map:有序
map<int, string> m;
m[3] = "three";
m[1] = "one";
m[2] = "two";
// 遍历顺序:1, 2, 3

// unordered_map:无序
unordered_map<int, string> um;
um[3] = "three";
um[1] = "one";
um[2] = "two";
// 遍历顺序:不确定

Q14:迭代器失效的情况?

vector:

  • 插入元素:可能导致所有迭代器失效(扩容时)
  • 删除元素:被删除元素之后的迭代器失效
1
2
3
vector<int> v = {1, 2, 3, 4, 5};
auto it = v.begin() + 2;
v.erase(it); // it 失效,不能再使用

list/deque:

  • 插入元素:不会导致迭代器失效
  • 删除元素:只有被删除元素的迭代器失效

map/set:

  • 插入元素:不会导致迭代器失效
  • 删除元素:只有被删除元素的迭代器失效
1
2
3
map<int, int> m = {{1, 1}, {2, 2}, {3, 3}};
auto it = m.find(2);
m.erase(it); // it 失效

模板和泛型编程

Q15:函数模板和类模板的区别?

函数模板:

  • 用于生成函数
  • 编译器根据调用时传入的参数类型自动推导
1
2
3
4
5
6
7
template<typename T>
T max(T a, T b) {
return a > b ? a : b;
}

int i = max(10, 20); // T 推导为 int
double d = max(3.14, 2.71); // T 推导为 double

类模板:

  • 用于生成类
  • 使用时需要显式指定类型参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
template<typename T>
class Stack {
vector<T> data;
public:
void push(const T& item) {
data.push_back(item);
}
T pop() {
T item = data.back();
data.pop_back();
return item;
}
};

Stack<int> intStack;
Stack<string> stringStack;

Q16:typename 和 class 在模板中的区别?

在模板参数中:

  • typenameclass 可以互换使用
  • typename 更语义化,表示类型
1
2
3
4
template<typename T>  // 或 template<class T>
class MyClass {
// ...
};

在模板内部:

  • typename 用于告诉编译器后面的标识符是类型
  • class 不能用于此场景
1
2
3
4
5
template<typename T>
class MyClass {
typename T::iterator it; // 必须使用 typename
// class T::iterator it; // 错误
};

多线程和并发

Q17:C++11 中的多线程支持?

主要特性:

  1. std::thread
    • 创建和管理线程
1
2
3
4
5
6
7
8
9
10
11
#include <thread>
#include <iostream>

void func(int n) {
for (int i = 0; i < n; i++) {
std::cout << i << std::endl;
}
}

std::thread t(func, 10);
t.join(); // 等待线程结束
  1. std::mutex
    • 互斥锁,保护共享资源
1
2
3
4
5
6
7
8
9
#include <mutex>

std::mutex mtx;
int counter = 0;

void increment() {
std::lock_guard<std::mutex> lock(mtx); // 自动加锁解锁
counter++;
}
  1. std::condition_variable
    • 条件变量,用于线程间通信
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <condition_variable>

std::condition_variable cv;
std::mutex mtx;
bool ready = false;

void producer() {
std::lock_guard<std::mutex> lock(mtx);
ready = true;
cv.notify_one();
}

void consumer() {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []{ return ready; });
}
  1. std::atomic
    • 原子操作,无需加锁
1
2
3
4
#include <atomic>

std::atomic<int> counter(0);
counter++; // 原子操作

Q18:什么是死锁?如何避免?

死锁:
多个线程互相等待对方释放锁,导致所有线程都无法继续执行。

死锁产生的四个必要条件:

  1. 互斥条件:资源不能被多个线程同时使用
  2. 请求与保持:线程持有资源的同时请求其他资源
  3. 不剥夺条件:资源不能被强制释放
  4. 循环等待:存在循环等待链

避免方法:

  1. 按顺序加锁
    • 所有线程按相同顺序获取锁
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
std::mutex mtx1, mtx2;

// 线程1和线程2都按相同顺序加锁
void thread1() {
std::lock(mtx1, mtx2); // 同时锁定多个互斥锁
std::lock_guard<std::mutex> lock1(mtx1, std::adopt_lock);
std::lock_guard<std::mutex> lock2(mtx2, std::adopt_lock);
// ...
}

void thread2() {
std::lock(mtx1, mtx2); // 相同顺序
std::lock_guard<std::mutex> lock1(mtx1, std::adopt_lock);
std::lock_guard<std::mutex> lock2(mtx2, std::adopt_lock);
// ...
}
  1. 使用超时锁
    • 使用 try_lock_fortry_lock_until
1
2
3
4
5
6
7
std::timed_mutex mtx;
if (mtx.try_lock_for(std::chrono::seconds(1))) {
// 获取锁成功
mtx.unlock();
} else {
// 超时,避免死锁
}
  1. 避免嵌套锁
    • 尽量减少锁的嵌套使用

常见算法题

Q19:实现一个单例模式

懒汉式(线程安全):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Singleton {
private:
static Singleton* instance;
static std::mutex mtx;
Singleton() {} // 私有构造函数
Singleton(const Singleton&) = delete; // 禁止拷贝
Singleton& operator=(const Singleton&) = delete; // 禁止赋值

public:
static Singleton* getInstance() {
if (instance == nullptr) {
std::lock_guard<std::mutex> lock(mtx);
if (instance == nullptr) { // 双重检查
instance = new Singleton();
}
}
return instance;
}
};

Singleton* Singleton::instance = nullptr;
std::mutex Singleton::mtx;

C++11 线程安全的懒汉式(推荐):

1
2
3
4
5
6
7
8
9
10
11
12
class Singleton {
private:
Singleton() {}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;

public:
static Singleton& getInstance() {
static Singleton instance; // C++11 保证线程安全
return instance;
}
};

Q20:实现一个智能指针

简化版 unique_ptr:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
template<typename T>
class UniquePtr {
private:
T* ptr;

public:
explicit UniquePtr(T* p = nullptr) : ptr(p) {}

~UniquePtr() {
delete ptr;
}

// 禁止拷贝
UniquePtr(const UniquePtr&) = delete;
UniquePtr& operator=(const UniquePtr&) = delete;

// 允许移动
UniquePtr(UniquePtr&& other) noexcept : ptr(other.ptr) {
other.ptr = nullptr;
}

UniquePtr& operator=(UniquePtr&& other) noexcept {
if (this != &other) {
delete ptr;
ptr = other.ptr;
other.ptr = nullptr;
}
return *this;
}

T& operator*() const { return *ptr; }
T* operator->() const { return ptr; }
T* get() const { return ptr; }
bool operator!() const { return ptr == nullptr; }
};

其他重要知识点

Q21:const 关键字的作用?

const 的多种用法:

  1. const 变量
    • 常量,不能修改
1
2
const int a = 10;
// a = 20; // 错误
  1. const 指针
    • const int* p:指向常量的指针(不能通过 p 修改指向的值)
    • int* const p:常量指针(不能改变指向)
    • const int* const p:指向常量的常量指针
1
2
3
4
5
6
7
8
9
10
11
12
int a = 10, b = 20;
const int* p1 = &a; // 指向常量的指针
// *p1 = 30; // 错误
p1 = &b; // 可以

int* const p2 = &a; // 常量指针
*p2 = 30; // 可以
// p2 = &b; // 错误

const int* const p3 = &a; // 指向常量的常量指针
// *p3 = 30; // 错误
// p3 = &b; // 错误
  1. const 成员函数
    • 不能修改成员变量(mutable 除外)
1
2
3
4
5
6
7
8
class MyClass {
int value;
public:
int getValue() const { // const 成员函数
// value = 10; // 错误
return value;
}
};
  1. const 引用
    • 不能通过引用修改对象
1
2
const int& ref = a;
// ref = 20; // 错误

Q22:static 关键字的作用?

static 的多种用法:

  1. static 局部变量
    • 生命周期延长到程序结束
    • 只初始化一次
1
2
3
4
5
void func() {
static int count = 0; // 只初始化一次
count++;
cout << count << endl;
}
  1. static 成员变量
    • 属于类,不属于对象
    • 所有对象共享
1
2
3
4
5
6
class MyClass {
public:
static int count; // 声明
};

int MyClass::count = 0; // 定义
  1. static 成员函数
    • 属于类,不属于对象
    • 不能访问非静态成员
1
2
3
4
5
6
7
class MyClass {
static int count;
public:
static void increment() {
count++; // 可以访问静态成员
}
};
  1. static 全局变量/函数
    • 限制作用域为当前文件

Q23:volatile 关键字的作用?

volatile 的作用:

  • 告诉编译器不要优化该变量
  • 每次访问都从内存读取,不缓存到寄存器
  • 主要用于多线程、中断处理、硬件寄存器访问
1
2
3
4
5
6
7
8
9
10
11
volatile int flag = 0;

void thread1() {
while (flag == 0) { // 每次循环都从内存读取 flag
// 等待
}
}

void thread2() {
flag = 1; // 修改 flag
}

Q24:inline 关键字的作用?

inline 的作用:

  • 建议编译器将函数内联展开
  • 减少函数调用开销
  • 编译器可能忽略 inline 建议

使用场景:

  • 短小的函数
  • 频繁调用的函数
1
2
3
4
5
inline int add(int a, int b) {
return a + b;
}

int result = add(1, 2); // 可能被展开为:int result = 1 + 2;

注意:

  • 定义在类内的成员函数默认是 inline
  • 模板函数通常也是 inline

Q25:C++11 的新特性?

主要新特性:

  1. auto 和 decltype
    • 自动类型推导
1
2
3
auto i = 10;  // int
auto d = 3.14; // double
decltype(i) j = 20; // j 的类型与 i 相同
  1. 范围 for 循环
1
2
3
4
vector<int> v = {1, 2, 3, 4, 5};
for (auto& item : v) {
cout << item << endl;
}
  1. lambda 表达式
1
2
3
4
5
6
auto func = [](int a, int b) { return a + b; };
int result = func(1, 2);

// 捕获变量
int x = 10;
auto lambda = [x](int a) { return a + x; };
  1. 智能指针

    • unique_ptrshared_ptrweak_ptr
  2. 右值引用和移动语义

1
2
3
4
5
6
class MyClass {
public:
MyClass(MyClass&& other) { // 移动构造函数
// 移动资源
}
};
  1. nullptr
    • 替代 NULL
1
int* p = nullptr;  // 而不是 NULL
  1. 统一初始化
1
2
vector<int> v{1, 2, 3, 4, 5};
int arr[]{1, 2, 3};

总结

C++ 是一门功能强大但复杂的语言,掌握这些基础知识对于面试和实际开发都非常重要。重点需要理解:

  1. 内存管理:理解堆栈、指针、引用、智能指针
  2. 面向对象:封装、继承、多态、虚函数
  3. STL:常用容器的底层实现和特性
  4. 模板:函数模板和类模板的使用
  5. 多线程:线程安全、锁、死锁避免
  6. C++11 新特性:现代 C++ 的编程方式

在实际开发中,应该优先使用现代 C++ 特性(智能指针、RAII、STL 容器等),避免手动管理内存,提高代码的安全性和可维护性。