以下是更详细的 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 新特性(移动语义、智能指针)。