C++ 基础常见面试题总结(中)

✍️ Demo User·📅 2026年4月24日·👁 8 次阅读
c++开发
📚 系列:现代 C++ 学习之路

本文采用面试问答形式,系统梳理 C++ 中指针与引用、内存管理、面向对象的核心知识。⭐ 标记为高频重点题。


一、指针与引用

⭐ 指针和引用的区别?

特性指针引用
本质存储地址的变量变量的别名
是否可为空✅ 可以为 nullptr❌ 不能为空
是否可变✅ 可以改变指向❌ 绑定后不可更改
是否需要初始化可以不初始化必须在声明时初始化
内存占用占用指针大小(4/8 字节)通常不占额外空间(编译器优化)
多级支持多级指针 int**无多级引用
sizeof指针自身大小被引用对象的大小
使用方式需要 * 解引用直接使用
cpp
int a = 10;

// 指针
int* p = &a;
*p = 20;         // 通过解引用修改
p = nullptr;     // 可以改变指向

// 引用
int& r = a;      // 必须初始化
r = 30;          // 直接使用,等同于 a = 30
// int& r2;      // ❌ 编译错误:引用必须初始化

🌈 选择建议

  • 优先使用引用:更安全,语法更简洁
  • 使用指针:需要"可空"、"可变指向"或操作动态内存时

⭐ 什么是悬空指针(Dangling Pointer)和野指针(Wild Pointer)?

类型定义原因
悬空指针指向已释放内存的指针delete 后未置空
野指针未初始化的指针声明后未赋值
空指针值为 nullptr主动设置
cpp
// 悬空指针
int* p = new int(42);
delete p;           // 内存释放
// *p = 10;         // ❌ 未定义行为!p 成为悬空指针
p = nullptr;        // ✅ 好习惯:释放后置空

// 野指针
int* q;             // 未初始化,指向随机地址
// *q = 10;         // ❌ 未定义行为!

// 正确做法
int* safe = nullptr;  // 初始化为空

⚠️ 防范措施

  1. 指针初始化为 nullptr
  2. delete 后立即置 nullptr
  3. 使用智能指针替代原始指针

⭐ 什么是左值和右值?什么是左值引用和右值引用?

概念含义特征示例
左值(lvalue)有地址、可取地址的表达式可以出现在 = 左边变量、数组元素
右值(rvalue)临时值、不可取地址只能出现在 = 右边字面量、临时对象
cpp
int a = 10;       // a 是左值,10 是右值
int& lr = a;      // 左值引用:绑定到左值
// int& lr2 = 10; // ❌ 左值引用不能绑定到右值

int&& rr = 10;    // 右值引用(C++11):绑定到右值
// int&& rr2 = a; // ❌ 右值引用不能绑定到左值

const int& cr = 10;  // ✅ 常量左值引用可以绑定右值(特例)

⭐ 什么是移动语义(Move Semantics)?

移动语义(C++11)允许窃取临时对象的资源,避免不必要的深拷贝:

cpp
class BigData {
    int* data_;
    size_t size_;

public:
    // 拷贝构造:深拷贝,昂贵
    BigData(const BigData& other) : size_(other.size_) {
        data_ = new int[size_];
        std::copy(other.data_, other.data_ + size_, data_);
        std::cout << "Copy!\n";
    }

    // 移动构造:转移资源,高效
    BigData(BigData&& other) noexcept
        : data_(other.data_), size_(other.size_) {
        other.data_ = nullptr;   // 让源对象处于有效但空的状态
        other.size_ = 0;
        std::cout << "Move!\n";
    }
};

BigData CreateData() {
    BigData temp(1000);
    return temp;           // 触发移动构造(而非拷贝)
}

BigData a = CreateData();  // 输出 "Move!"

BigData b = a;             // 输出 "Copy!"(a 是左值)
BigData c = std::move(a);  // 输出 "Move!"(std::move 将左值转为右值引用)
// ⚠️ 此后 a 处于"已移动"状态,不应再使用其内容

std::move 的作用?

std::move 本身不移动任何东西,它只是将左值强制转换为右值引用,从而触发移动构造/移动赋值:

cpp
std::string s1 = "Hello";
std::string s2 = std::move(s1);  // s1 的内容被移动到 s2
// s1 现在是空字符串(或处于有效但未指定的状态)

std::vector<std::string> vec;
std::string s = "World";
vec.push_back(std::move(s));     // 移动而非拷贝

⚠️ 注意std::move 之后,原对象处于有效但未指定的状态,只能对其执行析构或重新赋值。

⭐ 什么是完美转发(Perfect Forwarding)?

完美转发通过 std::forward 保持参数的左值/右值属性:

cpp
template <typename T>
void Wrapper(T&& arg) {                   // 万能引用
    Process(std::forward<T>(arg));         // 完美转发
}

void Process(int& x)  { std::cout << "lvalue\n"; }
void Process(int&& x) { std::cout << "rvalue\n"; }

int a = 42;
Wrapper(a);     // 输出 "lvalue"(a 是左值,T 推导为 int&)
Wrapper(42);    // 输出 "rvalue"(42 是右值,T 推导为 int)

二、内存管理

new / deletemalloc / free 的区别?

特性new / deletemalloc / free
所属C++ 运算符C 标准库函数
类型安全✅ 返回具体类型指针❌ 返回 void*,需强转
构造/析构✅ 自动调用❌ 不调用
异常处理失败抛出 std::bad_alloc失败返回 NULL
可重载✅ 可以重载 operator new
cpp
// C++ 方式
int* p1 = new int(42);          // 分配 + 初始化
delete p1;                       // 调用析构 + 释放

int* arr = new int[10];          // 分配数组
delete[] arr;                    // 释放数组(必须用 delete[])

// C 方式
int* p2 = (int*)malloc(sizeof(int));  // 仅分配内存
*p2 = 42;                             // 需手动初始化
free(p2);                             // 仅释放内存

// 对象的区别最明显
class Foo {
public:
    Foo()  { std::cout << "Constructor\n"; }
    ~Foo() { std::cout << "Destructor\n"; }
};

Foo* f1 = new Foo();     // 输出 "Constructor"
delete f1;               // 输出 "Destructor"

Foo* f2 = (Foo*)malloc(sizeof(Foo));  // 不调用构造函数!
free(f2);                              // 不调用析构函数!

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

内存泄漏:动态分配的内存未被释放,导致程序持续占用内存。

cpp
// 典型泄漏场景
void Leak() {
    int* p = new int(42);
    // 忘记 delete p; → 内存泄漏!
}

void LeakOnException() {
    int* p = new int(42);
    DoSomething();          // 如果抛出异常
    delete p;               // 这行永远执行不到!
}

避免方法

方法说明
⭐ 使用智能指针unique_ptr / shared_ptr 自动管理生命周期
RAII 原则资源在构造时获取,析构时释放
容器管理vector 替代原始数组
工具检测Valgrind、AddressSanitizer
cpp
// ✅ 使用智能指针避免泄漏
void NoLeak() {
    auto p = std::make_unique<int>(42);
    DoSomething();  // 即使抛异常,p 也会自动释放
    // 不需要手动 delete
}

⭐ 智能指针有哪些?区别是什么?

类型所有权引用计数拷贝适用场景
unique_ptr独占❌ 禁止拷贝,可移动单一所有者
shared_ptr共享✅ 拷贝增加引用计数多个所有者
weak_ptr观察不增加计数打破循环引用
cpp
#include <memory>

// unique_ptr —— 独占所有权
auto up = std::make_unique<int>(42);
// auto up2 = up;           // ❌ 编译错误:禁止拷贝
auto up2 = std::move(up);   // ✅ 移动所有权

// shared_ptr —— 共享所有权
auto sp1 = std::make_shared<int>(100);
auto sp2 = sp1;              // 引用计数变为 2
std::cout << sp1.use_count();  // 2

// weak_ptr —— 弱引用(不增加引用计数)
std::weak_ptr<int> wp = sp1;
if (auto sp3 = wp.lock()) {   // 提升为 shared_ptr
    std::cout << *sp3;
}

shared_ptr 的循环引用问题是什么?如何解决?

cpp
class B; // 前向声明

class A {
public:
    std::shared_ptr<B> bPtr;
    ~A() { std::cout << "A destroyed\n"; }
};

class B {
public:
    std::shared_ptr<A> aPtr;  // ❌ 循环引用!
    ~B() { std::cout << "B destroyed\n"; }
};

void Problem() {
    auto a = std::make_shared<A>();
    auto b = std::make_shared<B>();
    a->bPtr = b;   // a → b,b 的引用计数 = 2
    b->aPtr = a;   // b → a,a 的引用计数 = 2
    // 函数结束后,a 和 b 的引用计数各减 1,变为 1
    // 永远不会减到 0 → 永远不会析构 → 内存泄漏!
}

解决方案:将其中一方改为 weak_ptr

cpp
class B {
public:
    std::weak_ptr<A> aPtr;   // ✅ 使用 weak_ptr 打破循环
    ~B() { std::cout << "B destroyed\n"; }
};

make_unique / make_shared 和直接 new 有什么区别?

cpp
// 方式一:直接 new
std::shared_ptr<int> sp1(new int(42));

// 方式二:make_shared(推荐)
auto sp2 = std::make_shared<int>(42);
特性new + 构造make_shared / make_unique
内存分配次数2 次(对象 + 控制块)1 次(对象和控制块合并)
异常安全可能泄漏✅ 安全
性能稍差更好
cpp
// 异常安全问题
Foo(std::shared_ptr<A>(new A()), std::shared_ptr<B>(new B()));
// 如果 new A() 成功,new B() 失败,A 的内存可能泄漏!

// 安全写法
Foo(std::make_shared<A>(), std::make_shared<B>());

什么是内存对齐?为什么需要?

CPU 访问对齐的内存更高效。编译器会在结构体成员之间插入填充字节

cpp
struct A {
    char a;    // 1 字节
    // 3 字节填充
    int b;     // 4 字节
    char c;    // 1 字节
    // 3 字节填充
};
// sizeof(A) = 12(而不是 6)

// 调整成员顺序可以减少填充
struct B {
    int b;     // 4 字节
    char a;    // 1 字节
    char c;    // 1 字节
    // 2 字节填充
};
// sizeof(B) = 8

🌈 建议:将成员按大小从大到小排列,减少内存浪费。


三、面向对象——类的基础

⭐ 面向对象的三大特性是什么?

特性含义C++ 实现
封装隐藏内部实现,暴露接口private / public / protected
继承子类复用父类的属性和行为class Derived : public Base
多态同一接口,不同行为虚函数 virtual + override

publicprotectedprivate 的区别?

访问权限类内部子类类外部
public
protected
private
cpp
class Base {
public:
    int pubVar;        // 任何地方可访问
protected:
    int proVar;        // 子类可访问
private:
    int priVar;        // 仅类内可访问
};

class Derived : public Base {
    void Foo() {
        pubVar = 1;    // ✅
        proVar = 2;    // ✅
        // priVar = 3; // ❌ 编译错误
    }
};

⭐ 构造函数有哪些种类?

cpp
class MyClass {
    int x_;
    std::string name_;

public:
    // 1. 默认构造函数
    MyClass() : x_(0), name_("") {}

    // 2. 参数构造函数
    MyClass(int x, const std::string& name) : x_(x), name_(name) {}

    // 3. 拷贝构造函数
    MyClass(const MyClass& other) : x_(other.x_), name_(other.name_) {}

    // 4. 移动构造函数(C++11)
    MyClass(MyClass&& other) noexcept
        : x_(other.x_), name_(std::move(other.name_)) {}

    // 5. 委托构造函数(C++11)
    MyClass(int x) : MyClass(x, "default") {}  // 委托给参数构造
};
触发时机调用的构造函数
MyClass a;默认构造
MyClass a(1, "hi");参数构造
MyClass b = a;拷贝构造
MyClass b(a);拷贝构造
MyClass b = std::move(a);移动构造
MyClass b = MyClass(1, "hi");参数构造(可能被优化,不调用移动)

⭐ 为什么构造函数要使用初始化列表?

cpp
class Example {
    const int id_;            // const 成员
    int& ref_;                // 引用成员
    std::string name_;

public:
    // ✅ 初始化列表(推荐)
    Example(int id, int& r, const std::string& name)
        : id_(id), ref_(r), name_(name) {}

    // ❌ 函数体内赋值(某些情况不可行)
    // Example(int id, int& r, const std::string& name) {
    //     id_ = id;     // ❌ const 成员不能赋值
    //     ref_ = r;     // ❌ 引用不能重新绑定
    //     name_ = name; // 可以,但效率低(先默认构造,再赋值)
    // }
};

必须使用初始化列表的场景

场景原因
const 成员const 初始化后不能修改
引用成员引用必须在初始化时绑定
没有默认构造函数的成员无法默认构造
基类构造函数需要传参给基类

⭐ 什么是深拷贝和浅拷贝?

浅拷贝深拷贝
行为复制指针的值(地址)复制指针指向的内容
风险两个对象共享同一块内存各自拥有独立内存
释放可能重复释放(崩溃)安全
默认编译器生成的拷贝构造就是浅拷贝需要手动实现
cpp
class ShallowCopy {
    int* data_;
public:
    ShallowCopy(int val) : data_(new int(val)) {}
    // 编译器默认生成的拷贝构造是浅拷贝:
    // ShallowCopy(const ShallowCopy& other) : data_(other.data_) {}
    // ⚠️ 两个对象的 data_ 指向同一块内存!

    ~ShallowCopy() { delete data_; }  // 第二次析构时会重复释放!
};

class DeepCopy {
    int* data_;
public:
    DeepCopy(int val) : data_(new int(val)) {}

    // 手动深拷贝
    DeepCopy(const DeepCopy& other) : data_(new int(*other.data_)) {}

    ~DeepCopy() { delete data_; }
};

⭐ 什么是 explicit 关键字?

explicit 防止隐式类型转换

cpp
class Foo {
public:
    Foo(int x) {}              // 允许隐式转换
};

class Bar {
public:
    explicit Bar(int x) {}     // 禁止隐式转换
};

Foo f1 = 42;        // ✅ 隐式转换:42 → Foo(42)
// Bar b1 = 42;     // ❌ 编译错误:explicit 禁止隐式转换
Bar b2(42);          // ✅ 显式构造
Bar b3 = Bar(42);    // ✅ 显式构造

🌈 建议:单参数构造函数都应该加 explicit,除非你确实需要隐式转换。

this 指针是什么?

this 是一个隐含的指针,指向当前对象自身:

cpp
class MyClass {
    int x_;
public:
    MyClass(int x) : x_(x) {}

    // this 的用途1:区分成员变量和参数
    void SetX(int x) { this->x_ = x; }

    // this 的用途2:返回自身引用(链式调用)
    MyClass& Add(int val) {
        x_ += val;
        return *this;
    }
};

MyClass obj(0);
obj.Add(1).Add(2).Add(3);  // 链式调用,x_ = 6

friend 友元的作用?

友元可以访问类的 privateprotected 成员:

cpp
class MyClass {
    int secret_ = 42;

    friend void PrintSecret(const MyClass& obj);  // 友元函数
    friend class FriendClass;                      // 友元类
};

void PrintSecret(const MyClass& obj) {
    std::cout << obj.secret_;  // ✅ 可以访问 private 成员
}

class FriendClass {
    void Access(const MyClass& obj) {
        std::cout << obj.secret_;  // ✅
    }
};

⚠️ 友元破坏封装性,应谨慎使用。常见场景:运算符重载、测试类。

static 成员变量和成员函数的特点?

cpp
class Counter {
    static int count_;        // 静态成员变量(所有对象共享)
public:
    Counter() { ++count_; }
    ~Counter() { --count_; }

    static int GetCount() {   // 静态成员函数
        // this->xxx;         // ❌ 静态函数没有 this 指针
        return count_;
    }
};

int Counter::count_ = 0;     // 必须在类外初始化

Counter a, b, c;
Counter::GetCount();          // 3(通过类名调用)
a.GetCount();                 // 也可以通过对象调用
特性静态成员变量静态成员函数
归属类,而非对象类,而非对象
共享所有对象共享一份
this❌ 没有 this 指针
访问ClassName::varClassName::Func()
初始化类外初始化
只能访问静态成员(不能访问非静态成员)

四、面向对象——继承

⭐ 三种继承方式的区别?

基类成员public 继承protected 继承private 继承
publicpublicprotectedprivate
protectedprotectedprotectedprivate
private不可访问不可访问不可访问

🌈 实际开发中,99% 使用 public 继承,表示 "is-a" 关系。

⭐ 继承中构造函数和析构函数的调用顺序?

构造顺序:基类 → 成员变量(按声明顺序)→ 派生类
析构顺序:派生类 → 成员变量(按声明逆序)→ 基类
cpp
class Base {
public:
    Base()  { std::cout << "Base ctor\n"; }
    ~Base() { std::cout << "Base dtor\n"; }
};

class Member {
public:
    Member()  { std::cout << "Member ctor\n"; }
    ~Member() { std::cout << "Member dtor\n"; }
};

class Derived : public Base {
    Member m_;
public:
    Derived()  { std::cout << "Derived ctor\n"; }
    ~Derived() { std::cout << "Derived dtor\n"; }
};

// 构造输出:Base ctor → Member ctor → Derived ctor
// 析构输出:Derived dtor → Member dtor → Base dtor

⭐ 什么是多重继承?有什么问题?

cpp
class A { public: void Foo() {} };
class B { public: void Foo() {} };
class C : public A, public B {};   // 多重继承

C c;
// c.Foo();     // ❌ 二义性:A::Foo 还是 B::Foo?
c.A::Foo();     // ✅ 显式指定
c.B::Foo();     // ✅ 显式指定

⭐ 菱形继承和虚继承?

      A
     / \
    B   C
     \ /
      D    ← D 继承 B 和 C,都来自 A → A 有两份!
cpp
class A { public: int x_ = 0; };
class B : public A {};
class C : public A {};
class D : public B, public C {};

D d;
// d.x_;         // ❌ 二义性:B::x_ 还是 C::x_?
d.B::x_ = 1;     // ✅ 但 A 存在两份

// 解决方案:虚继承
class B : virtual public A {};
class C : virtual public A {};
class D : public B, public C {};

D d2;
d2.x_ = 1;       // ✅ 只有一份 A 的成员

⚠️ 虚继承有额外开销(虚基类指针),应尽量避免菱形继承


五、面向对象——多态

⭐ 什么是多态?C++ 中如何实现多态?

多态:同一接口,不同对象产生不同行为。

类型实现方式时机示例
编译时多态函数重载、模板编译期Add(int) / Add(double)
运行时多态虚函数 + 继承运行期基类指针调用派生类方法
cpp
class Shape {
public:
    virtual ~Shape() = default;
    virtual double Area() const = 0;  // 纯虚函数
};

class Circle : public Shape {
    double r_;
public:
    explicit Circle(double r) : r_(r) {}
    double Area() const override { return 3.14159 * r_ * r_; }
};

class Rect : public Shape {
    double w_, h_;
public:
    Rect(double w, double h) : w_(w), h_(h) {}
    double Area() const override { return w_ * h_; }
};

// 运行时多态
void PrintArea(const Shape& shape) {
    std::cout << shape.Area() << "\n";  // 根据实际类型调用
}

Circle c(5);
Rect r(3, 4);
PrintArea(c);   // 78.5398
PrintArea(r);   // 12

⭐ 虚函数的实现原理(vtable)?

概念说明
虚函数表(vtable)每个含虚函数的类都有一个 vtable,存放虚函数的地址
虚指针(vptr)每个对象内部有一个 vptr,指向所属类的 vtable
调用过程通过 vptr → vtable → 找到实际函数地址 → 调用
对象内存布局:
┌──────────┐
│   vptr   │ → 指向 vtable
├──────────┤
│ 成员变量  │
└──────────┘

vtable:
┌──────────────────────┐
│ [0] → Derived::Func1 │
│ [1] → Base::Func2    │ (未重写,仍指向基类版本)
│ [2] → Derived::Func3 │
└──────────────────────┘

⚠️ 虚函数调用比普通函数多了一次间接寻址,有少量性能开销。

⭐ 为什么基类的析构函数必须是虚函数?

cpp
class Base {
public:
    ~Base() { std::cout << "Base dtor\n"; }          // ❌ 非虚析构
    // virtual ~Base() { std::cout << "Base dtor\n"; }  // ✅ 虚析构
};

class Derived : public Base {
    int* data_;
public:
    Derived() : data_(new int[100]) {}
    ~Derived() {
        delete[] data_;
        std::cout << "Derived dtor\n";
    }
};

Base* p = new Derived();
delete p;
// 非虚析构:只调用 Base::~Base(),Derived::~Derived() 不被调用 → 内存泄漏!
// 虚析构:先调用 Derived::~Derived(),再调用 Base::~Base() → 正确释放

规则只要一个类可能被继承,其析构函数就应该是 virtual 的。

⭐ 纯虚函数和抽象类是什么?

cpp
class AbstractBase {
public:
    virtual ~AbstractBase() = default;
    virtual void DoWork() = 0;       // 纯虚函数(= 0)
    virtual void Log() {             // 普通虚函数(有默认实现)
        std::cout << "Logging...\n";
    }
};

// AbstractBase obj;  // ❌ 不能实例化抽象类

class Concrete : public AbstractBase {
public:
    void DoWork() override {          // 必须实现纯虚函数
        std::cout << "Working!\n";
    }
};

Concrete obj;    // ✅ 可以实例化
概念说明
纯虚函数virtual void Foo() = 0;,无实现,子类必须重写
抽象类含有至少一个纯虚函数的类,不能实例化
接口类所有成员函数都是纯虚函数的类(C++ 没有 interface 关键字)

overridefinal 的作用?

cpp
class Base {
public:
    virtual void Foo(int x) {}
    virtual void Bar() {}
};

class Derived : public Base {
public:
    // override:显式声明重写,编译器会检查签名
    void Foo(int x) override {}     // ✅
    // void Foo(double x) override {}  // ❌ 编译错误:签名不匹配

    // final:禁止子类进一步重写
    void Bar() final {}
};

class SubDerived : public Derived {
    // void Bar() override {}  // ❌ 编译错误:Bar 是 final 的
};

// final 也可以用于类
class FinalClass final {};
// class Sub : public FinalClass {};  // ❌ 编译错误:不能继承 final 类

🌈 建议:重写虚函数时始终加 override,让编译器帮你检查错误。

虚函数可以是内联的吗?

  • 可以声明为 inline,但只有在编译期能确定调用对象类型时才会真正内联。
  • 通过基类指针/引用调用时,不会内联(需要运行时查 vtable)。
cpp
class Base {
public:
    inline virtual void Foo() { std::cout << "Base\n"; }
};

Base b;
b.Foo();          // 可能内联(编译期已知类型)

Base* p = new Derived();
p->Foo();         // 不会内联(需要运行时多态)

运算符重载的规则?

cpp
class Vector2D {
    double x_, y_;
public:
    Vector2D(double x, double y) : x_(x), y_(y) {}

    // 成员函数重载
    Vector2D operator+(const Vector2D& other) const {
        return {x_ + other.x_, y_ + other.y_};
    }

    // 前置 ++
    Vector2D& operator++() {
        ++x_; ++y_;
        return *this;
    }

    // 后置 ++(用 int 占位区分)
    Vector2D operator++(int) {
        Vector2D old = *this;
        ++(*this);
        return old;
    }

    // 下标运算符
    double& operator[](int idx) {
        return (idx == 0) ? x_ : y_;
    }

    // 输出运算符(必须是友元函数)
    friend std::ostream& operator<<(std::ostream& os, const Vector2D& v) {
        return os << "(" << v.x_ << ", " << v.y_ << ")";
    }
};

不能重载的运算符:: . .* ?: sizeof typeid


💬 评论

加载评论中...