C++ 基础
Q1:C++ 和 C 的区别?
主要区别:
-
面向对象编程
- C++ 支持面向对象编程(封装、继承、多态)
- C 是过程式编程语言
-
函数重载
- C++ 支持函数重载
- C 不支持函数重载
-
命名空间
- C++ 有命名空间(namespace)
- C 没有命名空间
-
引用
- C++ 支持引用(reference)
- C 只有指针
-
异常处理
- C++ 支持异常处理(try-catch)
- C 没有异常处理机制
-
内存管理
- C++ 有 new/delete 操作符
- C 使用 malloc/free
-
类型检查
- C++ 类型检查更严格
- C 类型检查相对宽松
Q2:C++ 中 struct 和 class 的区别?
主要区别:
-
默认访问权限
struct默认成员是publicclass默认成员是private
-
默认继承方式
struct默认继承是publicclass默认继承是private
-
其他方面
- 功能基本相同,都可以有构造函数、析构函数、成员函数等
struct在 C++ 中主要用于兼容 C,或者用于简单的数据聚合
// 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++ 提供了四种类型转换操作符:
- static_cast
- 用于基本类型转换、父子类指针转换
- 编译时检查,不保证运行时安全
int i = 10;
double d = static_cast<double>(i);
Base* base = new Derived();
Derived* derived = static_cast<Derived*>(base); // 不安全,需要确保类型正确
- dynamic_cast
- 用于多态类型之间的转换
- 运行时检查,转换失败返回 nullptr(指针)或抛出异常(引用)
- 只能用于有虚函数的类
Base* base = new Derived();
Derived* derived = dynamic_cast<Derived*>(base);
if (derived != nullptr) {
// 转换成功
}
- const_cast
- 用于移除 const 或 volatile 属性
- 主要用于函数重载场景
const int* p = &i;
int* q = const_cast<int*>(p);
- reinterpret_cast
- 用于低级别的类型转换
- 非常危险,不保证可移植性
- 主要用于指针和整数之间的转换
int* p = new int(10);
long addr = reinterpret_cast<long>(p);
内存管理
Q4:new/delete 和 malloc/free 的区别?
主要区别:
-
类型安全
new/delete是操作符,类型安全malloc/free是函数,需要手动计算大小
-
构造函数/析构函数
new会调用构造函数delete会调用析构函数malloc/free不会调用构造函数/析构函数
-
返回值
new返回具体类型指针malloc返回void*,需要强制转换
-
失败处理
new失败抛出std::bad_alloc异常malloc失败返回NULL
-
重载
new/delete可以重载malloc/free不能重载
// 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:什么是内存泄漏?如何避免?
内存泄漏:
程序在运行过程中,动态分配的内存没有被正确释放,导致内存无法被再次使用。
常见原因:
- 使用
new分配内存后忘记delete - 异常导致
delete未执行 - 循环引用(智能指针)
避免方法:
- 使用智能指针
std::unique_ptr:独占所有权std::shared_ptr:共享所有权std::weak_ptr:弱引用,解决循环引用
// 使用 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 避免循环引用
};
- RAII(Resource Acquisition Is Initialization)
- 资源获取即初始化
- 在对象构造时获取资源,在析构时释放资源
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; }
};
- 使用容器
- 优先使用 STL 容器(vector、string 等),自动管理内存
Q6:堆和栈的区别?
主要区别:
-
分配方式
- 栈:由编译器自动分配和释放
- 堆:由程序员手动分配和释放
-
速度
- 栈:速度快,直接操作栈指针
- 堆:速度慢,需要查找合适的内存块
-
大小限制
- 栈:大小有限(通常几MB)
- 堆:大小受限于系统可用内存
-
内存碎片
- 栈:不会产生内存碎片
- 堆:可能产生内存碎片
-
生命周期
- 栈:局部变量,函数结束时自动释放
- 堆:需要手动释放,否则可能内存泄漏
void example() {
int stack_var = 10; // 栈上分配
int* heap_var = new int(10); // 堆上分配
// stack_var 在函数结束时自动释放
// heap_var 需要手动 delete,否则内存泄漏
delete heap_var;
}
指针和引用
Q7:指针和引用的区别?
主要区别:
-
初始化
- 引用必须在定义时初始化,且不能改变指向
- 指针可以先定义后初始化,可以改变指向
-
空值
- 引用不能为空,必须指向有效对象
- 指针可以为空(nullptr)
-
操作
- 引用是别名,使用起来像普通变量
- 指针需要解引用(*)才能访问指向的对象
-
内存
- 引用不占用额外内存(编译器优化)
- 指针占用内存(存储地址)
-
多级
- 引用没有多级引用
- 指针可以有多级指针
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):
未初始化的指针,指向未知内存地址。
int* p; // 野指针,未初始化
*p = 10; // 危险!可能崩溃
悬空指针(Dangling Pointer):
指向已经被释放的内存地址的指针。
int* p = new int(10);
delete p; // 释放内存
*p = 20; // 危险!p 现在是悬空指针
p = nullptr; // 好的做法:释放后置空
避免方法:
- 初始化指针为
nullptr - 释放后立即置空
- 使用智能指针
面向对象
Q9:C++ 中的多态是如何实现的?
多态分为两种:
-
编译时多态(静态多态)
- 函数重载
- 模板
-
运行时多态(动态多态)
- 虚函数
- 通过虚函数表(vtable)实现
虚函数表机制:
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):
- 有函数体,可以被子类重写
- 子类可以不重写
class Base {
public:
virtual void func() {
cout << "Base::func" << endl;
}
};
纯虚函数(Pure Virtual Function):
- 没有函数体,用
= 0表示 - 包含纯虚函数的类是抽象类,不能实例化
- 子类必须实现纯虚函数
class Base {
public:
virtual void func() = 0; // 纯虚函数
};
class Derived : public Base {
public:
void func() override {
cout << "Derived::func" << endl;
}
};
// Base b; // 错误:抽象类不能实例化
Derived d; // 正确
Q11:构造函数和析构函数可以是虚函数吗?
构造函数:
- 不能是虚函数
- 原因:对象在构造时,虚函数表还未建立,无法通过虚函数表调用
析构函数:
- 应该是虚函数(如果类会被继承)
- 原因:确保通过基类指针删除派生类对象时,能正确调用派生类的析构函数
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 倍)
- 将旧数据拷贝到新内存,释放旧内存
关键点:
-
容量(capacity)和大小(size)
size():当前元素个数capacity():当前容量
-
扩容机制
- 扩容时,所有迭代器、指针、引用都会失效
- 扩容代价高,需要拷贝所有元素
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(扩容)
- 时间复杂度
- 随机访问:O(1)
- 尾部插入:平均 O(1),最坏 O(n)(需要扩容)
- 中间插入:O(n)
Q13:map 和 unordered_map 的区别?
主要区别:
-
底层实现
map:红黑树(平衡二叉搜索树)unordered_map:哈希表
-
有序性
map:有序(按 key 排序)unordered_map:无序
-
时间复杂度
map:查找、插入、删除 O(log n)unordered_map:平均 O(1),最坏 O(n)
-
内存占用
map:相对较少unordered_map:相对较多(需要维护哈希表)
-
key 要求
map:key 需要支持<操作符unordered_map:key 需要支持哈希函数和==操作符
// 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:
- 插入元素:可能导致所有迭代器失效(扩容时)
- 删除元素:被删除元素之后的迭代器失效
vector<int> v = {1, 2, 3, 4, 5};
auto it = v.begin() + 2;
v.erase(it); // it 失效,不能再使用
list/deque:
- 插入元素:不会导致迭代器失效
- 删除元素:只有被删除元素的迭代器失效
map/set:
- 插入元素:不会导致迭代器失效
- 删除元素:只有被删除元素的迭代器失效
map<int, int> m = {{1, 1}, {2, 2}, {3, 3}};
auto it = m.find(2);
m.erase(it); // it 失效
模板和泛型编程
Q15:函数模板和类模板的区别?
函数模板:
- 用于生成函数
- 编译器根据调用时传入的参数类型自动推导
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
类模板:
- 用于生成类
- 使用时需要显式指定类型参数
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 在模板中的区别?
在模板参数中:
typename和class可以互换使用typename更语义化,表示类型
template<typename T> // 或 template<class T>
class MyClass {
// ...
};
在模板内部:
typename用于告诉编译器后面的标识符是类型class不能用于此场景
template<typename T>
class MyClass {
typename T::iterator it; // 必须使用 typename
// class T::iterator it; // 错误
};
多线程和并发
Q17:C++11 中的多线程支持?
主要特性:
- std::thread
- 创建和管理线程
#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(); // 等待线程结束
- std::mutex
- 互斥锁,保护共享资源
#include <mutex>
std::mutex mtx;
int counter = 0;
void increment() {
std::lock_guard<std::mutex> lock(mtx); // 自动加锁解锁
counter++;
}
- std::condition_variable
- 条件变量,用于线程间通信
#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; });
}
- std::atomic
- 原子操作,无需加锁
#include <atomic>
std::atomic<int> counter(0);
counter++; // 原子操作
Q18:什么是死锁?如何避免?
死锁:
多个线程互相等待对方释放锁,导致所有线程都无法继续执行。
死锁产生的四个必要条件:
- 互斥条件:资源不能被多个线程同时使用
- 请求与保持:线程持有资源的同时请求其他资源
- 不剥夺条件:资源不能被强制释放
- 循环等待:存在循环等待链
避免方法:
- 按顺序加锁
- 所有线程按相同顺序获取锁
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);
// ...
}
- 使用超时锁
- 使用
try_lock_for或try_lock_until
- 使用
std::timed_mutex mtx;
if (mtx.try_lock_for(std::chrono::seconds(1))) {
// 获取锁成功
mtx.unlock();
} else {
// 超时,避免死锁
}
- 避免嵌套锁
- 尽量减少锁的嵌套使用
常见算法题
Q19:实现一个单例模式
懒汉式(线程安全):
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 线程安全的懒汉式(推荐):
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:
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 的多种用法:
- const 变量
- 常量,不能修改
const int a = 10;
// a = 20; // 错误
- const 指针
const int* p:指向常量的指针(不能通过 p 修改指向的值)int* const p:常量指针(不能改变指向)const int* const p:指向常量的常量指针
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; // 错误
- const 成员函数
- 不能修改成员变量(mutable 除外)
class MyClass {
int value;
public:
int getValue() const { // const 成员函数
// value = 10; // 错误
return value;
}
};
- const 引用
- 不能通过引用修改对象
const int& ref = a;
// ref = 20; // 错误
Q22:static 关键字的作用?
static 的多种用法:
- static 局部变量
- 生命周期延长到程序结束
- 只初始化一次
void func() {
static int count = 0; // 只初始化一次
count++;
cout << count << endl;
}
- static 成员变量
- 属于类,不属于对象
- 所有对象共享
class MyClass {
public:
static int count; // 声明
};
int MyClass::count = 0; // 定义
- static 成员函数
- 属于类,不属于对象
- 不能访问非静态成员
class MyClass {
static int count;
public:
static void increment() {
count++; // 可以访问静态成员
}
};
- static 全局变量/函数
- 限制作用域为当前文件
Q23:volatile 关键字的作用?
volatile 的作用:
- 告诉编译器不要优化该变量
- 每次访问都从内存读取,不缓存到寄存器
- 主要用于多线程、中断处理、硬件寄存器访问
volatile int flag = 0;
void thread1() {
while (flag == 0) { // 每次循环都从内存读取 flag
// 等待
}
}
void thread2() {
flag = 1; // 修改 flag
}
Q24:inline 关键字的作用?
inline 的作用:
- 建议编译器将函数内联展开
- 减少函数调用开销
- 编译器可能忽略 inline 建议
使用场景:
- 短小的函数
- 频繁调用的函数
inline int add(int a, int b) {
return a + b;
}
int result = add(1, 2); // 可能被展开为:int result = 1 + 2;
注意:
- 定义在类内的成员函数默认是 inline
- 模板函数通常也是 inline
Q25:C++11 的新特性?
主要新特性:
- auto 和 decltype
- 自动类型推导
auto i = 10; // int
auto d = 3.14; // double
decltype(i) j = 20; // j 的类型与 i 相同
- 范围 for 循环
vector<int> v = {1, 2, 3, 4, 5};
for (auto& item : v) {
cout << item << endl;
}
- lambda 表达式
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; };
-
智能指针
unique_ptr、shared_ptr、weak_ptr
-
右值引用和移动语义
class MyClass {
public:
MyClass(MyClass&& other) { // 移动构造函数
// 移动资源
}
};
- nullptr
- 替代 NULL
int* p = nullptr; // 而不是 NULL
- 统一初始化
vector<int> v{1, 2, 3, 4, 5};
int arr[]{1, 2, 3};
总结
C++ 是一门功能强大但复杂的语言,掌握这些基础知识对于面试和实际开发都非常重要。重点需要理解:
- 内存管理:理解堆栈、指针、引用、智能指针
- 面向对象:封装、继承、多态、虚函数
- STL:常用容器的底层实现和特性
- 模板:函数模板和类模板的使用
- 多线程:线程安全、锁、死锁避免
- C++11 新特性:现代 C++ 的编程方式
在实际开发中,应该优先使用现代 C++ 特性(智能指针、RAII、STL 容器等),避免手动管理内存,提高代码的安全性和可维护性。
Originally published on mlangTse's Blog. View source