c++

以下是更详细的 C++ 面试题集,涵盖核心考点,每个题目均包含代码示例、深入解析及易错点提示,适合面试冲刺复习:

一、基础语法与关键字

1. const 关键字的深层用法(含代码示例)

题目const 在不同场景下的作用,如何区分 const int*int* constconst 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/deletemalloc/free 的本质区别

题目 :从底层实现、功能、安全性等方面对比 newmalloc答案

维度 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;
}

易错点

  • 混用 newfreemallocdelete(例如 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. 分配一块更大的内存(通常是原容量的 1.5 倍或 2 倍);
    2. 将原数据拷贝到新内存;
    3. 释放原内存;
    4. 更新指针指向新内存。
  • 迭代器失效场景

    • 扩容时(原内存释放,迭代器指向无效地址);
    • 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 新特性(移动语义、智能指针)。
相关推荐
有趣的我7 小时前
关于stub和mock
c++
Yupureki7 小时前
从零开始的C++学习生活 18:C语言复习课(期末速通)
c语言·数据结构·c++·学习·visual studio
永远有缘8 小时前
四种编程语言常用函数对比表
java·开发语言·c++·python
C++_girl8 小时前
c++、java/python语言有什么区别?为什么c++更快?
java·开发语言·c++
艾莉丝努力练剑8 小时前
【Linux权限 (二)】Linux权限机制深度解析:umask如何决定默认权限与粘滞位的妙用
大数据·linux·服务器·c++·ubuntu·centos·1024程序员节
彩妙不是菜喵8 小时前
基于C语言上,面向对象语言:C++基础(学完C语言后再看)
c语言·开发语言·c++
夜晚中的人海9 小时前
【C++】位运算算法习题
开发语言·c++·算法
枫叶丹49 小时前
【Qt开发】布局管理器(一)-> QVBoxLayout垂直布局
开发语言·c++·qt
superior tigre9 小时前
(huawei)5.最长回文子串
c++·算法