以下是更详细的 C++ 面试题集,涵盖核心考点,每个题目均包含代码示例、深入解析及易错点提示,适合面试冲刺复习:
一、基础语法与关键字
1. const 关键字的深层用法(含代码示例)
题目 :const 在不同场景下的作用,如何区分 const int*、int* const 和 const int* const?答案:
-
const修饰变量:变量只读,编译期检查,内存不可修改(强制修改会触发未定义行为)。 -
const修饰指针:const int* p:指针指向的内容不可改(*p = 2错误),指针本身可改(p = &a合法)。int* const p:指针本身不可改(p = &a错误),指向的内容可改(*p = 2合法)。const int* const p:指针本身和指向的内容均不可改。
-
const修饰类成员函数:函数体内不能修改非mutable成员变量,也不能调用非const成员函数。
代码示例:
cpp
运行
ini
class A {
public:
int x;
mutable int y; // 可被 const 成员函数修改
void func() const {
// x = 10; 错误:const 函数不能修改非 mutable 成员
y = 20; // 正确:mutable 成员可修改
}
};
int main() {
const int a = 5;
// a = 10; 错误:const 变量不可修改
int b = 10;
const int* p1 = &b; // 指向内容不可改
// *p1 = 15; 错误
p1 = &a; // 正确:指针本身可改
int* const p2 = &b; // 指针本身不可改
*p2 = 15; // 正确:指向内容可改
// p2 = &a; 错误
return 0;
}
易错点:
- 混淆指针常量和常量指针的语法("左定值,右定向":
const在*左边则指向内容不可改,在右边则指针本身不可改)。 - 在
const成员函数中试图修改非mutable成员,或调用非const成员函数。
2. static 在类中的完整用法
题目 :static 修饰类成员变量和成员函数的特点,如何初始化?答案:
-
静态成员变量:
- 属于类而非对象,所有对象共享同一份内存。
- 必须在类外初始化(类内仅声明),初始化时不加
static。 - 可通过
类名::变量名或对象访问(对象访问本质还是类共享)。
-
静态成员函数:
- 无
this指针,只能访问静态成员(变量 / 函数),不能访问非静态成员。 - 可通过
类名::函数名直接调用,无需实例化对象。
- 无
代码示例:
cpp
运行
c
class Counter {
private:
static int count; // 类内声明
public:
Counter() { count++; }
static int getCount() { // 静态成员函数
return count;
}
};
int Counter::count = 0; // 类外初始化(必须!)
int main() {
Counter c1, c2;
cout << Counter::getCount() << endl; // 输出 2(通过类名调用)
cout << c1.getCount() << endl; // 输出 2(通过对象调用)
return 0;
}
易错点:
- 忘记在类外初始化静态成员变量,导致链接错误(
undefined reference to Counter::count)。 - 静态成员函数试图访问非静态成员(编译错误)。
二、面向对象核心
1. 多态的实现原理(虚函数表)
题目 :C++ 多态如何实现?虚函数表(vtable)的作用是什么?答案 :多态通过虚函数 和动态绑定实现,核心是虚函数表(vtable):
- 类中声明虚函数时,编译器会为该类生成一个虚函数表(存储虚函数地址的数组)。
- 类的每个对象会包含一个虚表指针(vptr),指向该类的虚函数表。
- 子类继承父类时,会复制父类的虚函数表,若重写父类虚函数,则替换表中对应函数的地址。
- 调用虚函数时,通过对象的 vptr 找到虚表,再调用对应函数(运行时确定调用哪个版本,即动态绑定)。
代码示例:
cpp
运行
csharp
class Base {
public:
virtual void func() { cout << "Base::func()" << endl; } // 虚函数
};
class Derived : public Base {
public:
void func() override { cout << "Derived::func()" << endl; } // 重写
};
int main() {
Base* p = new Derived(); // 父类指针指向子类对象
p->func(); // 输出 Derived::func()(动态绑定)
delete p;
return 0;
}
易错点:
- 误认为非虚函数也能实现多态(非虚函数是静态绑定,编译时根据指针类型确定调用版本)。
- 子类重写时参数列表或返回值不匹配(此时不算重写,而是隐藏父类函数,多态失效)。
2. 析构函数为何要设为虚函数?
题目 :析构函数不设为虚函数会导致什么问题?举例说明。答案 :若父类析构函数非虚函数,当用父类指针指向子类对象并 delete 时,只会调用父类析构函数,子类资源无法释放,导致内存泄漏。设为虚函数可保证析构函数动态绑定,先调用子类析构,再调用父类析构。
代码示例(错误情况) :
cpp
运行
arduino
class Base {
public:
~Base() { cout << "Base 析构" << endl; } // 非虚析构
};
class Derived : public Base {
private:
int* data;
public:
Derived() { data = new int[10]; }
~Derived() {
delete[] data;
cout << "Derived 析构" << endl; // 不会被调用!
}
};
int main() {
Base* p = new Derived();
delete p; // 仅输出 "Base 析构",Derived 的 data 内存泄漏
return 0;
}
正确做法:将父类析构函数声明为虚函数:
cpp
运行
arduino
class Base {
public:
virtual ~Base() { cout << "Base 析构" << endl; } // 虚析构
};
// 子类析构函数自动为虚函数(无需显式加 virtual)
// delete p 时会先调用 Derived 析构,再调用 Base 析构,无内存泄漏
易错点:
- 仅子类析构函数设为虚函数(无效,需从父类开始声明虚析构)。
- 认为 "只有存在继承时才需要虚析构"(正确,但只要可能通过父类指针删除子类对象,就必须设虚析构)。
三、内存管理
1. new/delete 与 malloc/free 的本质区别
题目 :从底层实现、功能、安全性等方面对比 new 和 malloc。答案:
| 维度 | new/delete |
malloc/free |
|---|---|---|
| 性质 | C++ 运算符 | C 库函数 |
| 类型检查 | 有(返回对应类型指针) | 无(返回 void*,需手动转换) |
| 构造 / 析构 | 自动调用构造 / 析构函数 | 仅分配 / 释放内存,不调用 |
| 内存不足 | 抛出 bad_alloc 异常 |
返回 NULL |
| 重载 | 可重载 operator new |
不可重载 |
代码示例:
cpp
运行
c
class A {
public:
A() { cout << "A 构造" << endl; }
~A() { cout << "A 析构" << endl; }
};
int main() {
// new/delete:自动调用构造/析构
A* p1 = new A(); // 输出 "A 构造"
delete p1; // 输出 "A 析构"
// malloc/free:不调用构造/析构
A* p2 = (A*)malloc(sizeof(A)); // 无输出(未构造)
free(p2); // 无输出(未析构),对象处于未初始化状态
return 0;
}
易错点:
- 混用
new和free、malloc和delete(例如free(new A())会导致析构函数不被调用,内存泄漏)。 - 对数组使用
delete而非delete[](delete[]会调用每个元素的析构函数,delete仅调用第一个,导致泄漏)。
2. 智能指针的循环引用问题及解决
题目 :shared_ptr 循环引用会导致什么问题?如何用 weak_ptr 解决?答案:
- 循环引用 :两个对象互相持有
shared_ptr指向对方,导致引用计数永远不为 0,对象无法释放,内存泄漏。 - 解决方法 :将其中一个指针改为
weak_ptr(弱引用,不增加引用计数)。
代码示例(循环引用问题) :
cpp
运行
arduino
#include <memory>
class B; // 前置声明
class A {
public:
shared_ptr<B> b_ptr;
~A() { cout << "A 析构" << endl; }
};
class B {
public:
shared_ptr<A> a_ptr; // B 持有 A 的 shared_ptr
~B() { cout << "B 析构" << endl; }
};
int main() {
shared_ptr<A> a(new A());
shared_ptr<B> b(new B());
a->b_ptr = b; // A 持有 B 的 shared_ptr
b->a_ptr = a; // 循环引用!
// 离开作用域时,a 和 b 的引用计数均为 1(未减到 0),析构函数不调用
return 0;
}
解决代码(用 weak_ptr) :
cpp
运行
arduino
class B {
public:
weak_ptr<A> a_ptr; // 改为弱引用,不增加计数
~B() { cout << "B 析构" << endl; }
};
// 此时 a 和 b 离开作用域时,引用计数减为 0,析构函数正常调用
易错点:
- 过度使用
shared_ptr而忽略循环引用风险(需根据 ownership 合理选择智能指针)。 - 用
weak_ptr直接访问对象(需先通过lock()转换为shared_ptr,检查对象是否存活)。
四、STL 容器与算法
1. vector 的扩容机制与迭代器失效
题目 :vector 扩容时发生了什么?哪些操作会导致迭代器失效?答案:
-
扩容机制 :
vector是动态数组,内存连续。当插入元素导致容量不足时,会:- 分配一块更大的内存(通常是原容量的 1.5 倍或 2 倍);
- 将原数据拷贝到新内存;
- 释放原内存;
- 更新指针指向新内存。
-
迭代器失效场景:
- 扩容时(原内存释放,迭代器指向无效地址);
erase操作(删除位置后的迭代器失效);push_back/insert可能导致扩容,间接使迭代器失效。
代码示例(迭代器失效) :
cpp
运行
arduino
#include <vector>
int main() {
vector<int> v = {1, 2, 3};
auto it = v.begin();
v.push_back(4); // 可能触发扩容,it 失效
// *it = 10; 未定义行为(可能崩溃)
// erase 导致迭代器失效
it = v.erase(v.begin()); // erase 返回下一个有效迭代器
// 正确做法:用返回值更新迭代器
return 0;
}
易错点:
- 扩容后继续使用旧迭代器(需重新获取迭代器)。
erase后未更新迭代器(正确用法:it = v.erase(it))。
五、C++11 及以上新特性
1. 移动语义与右值引用
题目 :什么是移动语义?std::move 的作用是什么?答案:
- 移动语义:解决临时对象(右值)的拷贝开销,通过 "窃取" 临时对象的资源(如内存),避免深拷贝。
- 右值引用 (
&&):绑定到右值(临时对象、字面量),是移动语义的基础。 std::move:将左值强制转换为右值引用,使对象可被移动(本身不移动资源,仅改变值类别)。
代码示例:
cpp
运行
arduino
class String {
private:
char* data;
public:
// 构造函数
String(const char* s) {
data = new char[strlen(s) + 1];
strcpy(data, s);
}
// 移动构造函数(窃取右值资源)
String(String&& other) noexcept : data(other.data) {
other.data = nullptr; // 原对象资源置空,避免析构时重复释放
}
~String() { delete[] data; }
};
int main() {
String s1("hello");
String s2 = std::move(s1); // 调用移动构造,s1 资源被窃取(s1.data 变为 nullptr)
return 0;
}
易错点:
- 移动后使用原对象(原对象处于 "有效但未定义" 状态,通常不应再使用)。
- 误认为
std::move会立即移动资源(实际仅允许移动,是否移动取决于是否有移动构造函数)。
总结
以上题目覆盖了 C++ 面试的核心考点,重点关注:
- 内存管理(智能指针、泄漏风险);
- 面向对象特性(多态实现、虚函数表);
- STL 容器的底层原理(扩容、迭代器);
- C++11 新特性(移动语义、智能指针)。