一、面向对象三大特性

C++ 是一门支持面向对象编程的语言。面向对象主要有三大特性:

1. 封装
2. 继承
3. 多态

这三个概念是 C++ 面试中非常高频的基础问题。


二、封装

封装就是把数据和操作数据的函数放在一个类里面,并通过访问权限控制外部是否可以直接访问这些数据。

C++ 中常见的访问权限有:

public:公有成员,类外可以访问
protected:保护成员,类外不能访问,子类可以访问
private:私有成员,类外不能访问,子类也不能直接访问

1. 封装示例

#include <iostream>
#include <string>
using namespace std;

class Student {
private:
    // 私有成员变量,类外不能直接访问
    string name;
    int age;

public:
    // 设置姓名
    void setName(string n) {
        name = n;
    }

    // 获取姓名
    string getName() {
        return name;
    }

    // 设置年龄
    void setAge(int a) {
        // 可以在函数中加入限制条件,保证数据合理
        if (a > 0 && a < 150) {
            age = a;
        } else {
            cout << "年龄不合法" << endl;
        }
    }

    // 获取年龄
    int getAge() {
        return age;
    }
};

int main() {
    Student stu;

    // 不能直接访问 private 成员
    // stu.name = "Tom";  // 错误

    // 通过 public 函数间接访问和修改数据
    stu.setName("Tom");
    stu.setAge(20);

    cout << stu.getName() << endl;
    cout << stu.getAge() << endl;

    return 0;
}

2. 封装面试总结

面试时可以这样回答:

封装就是把数据和操作数据的方法放到一个类中,并通过访问权限控制外部访问。封装可以隐藏内部实现细节,提高代码安全性和可维护性。比如把成员变量设置为 private,外部只能通过 public 方法访问,这样可以在方法中加入数据检查,避免非法赋值。


三、继承

继承表示一个类可以复用另一个类已有的成员。被继承的类叫父类或基类,继承得到的新类叫子类或派生类。

1. 继承示例

#include <iostream>
using namespace std;

// 父类
class Animal {
public:
    void eat() {
        cout << "动物会吃东西" << endl;
    }
};

// 子类继承父类
class Dog : public Animal {
public:
    void bark() {
        cout << "狗会叫" << endl;
    }
};

int main() {
    Dog dog;

    // 子类可以使用自己的成员函数
    dog.bark();

    // 子类也可以使用从父类继承来的成员函数
    dog.eat();

    return 0;
}

在这段代码中,Dog 继承了 Animal,所以 Dog 对象可以调用 Animal 中的 eat() 函数。

2. 继承的作用

继承的主要作用是代码复用和扩展。

例如:

Animal 表示通用动物
Dog 表示狗
Cat 表示猫

狗和猫都有动物的共同特征,所以可以把共同部分放到 Animal 中,然后让 DogCat 去继承。

3. 继承面试总结

面试时可以这样回答:

继承是面向对象的重要特性,子类可以复用父类已有的成员,也可以在父类基础上扩展新的功能。继承可以减少重复代码,提高代码复用性。但是继承关系不能乱用,只有当两个类之间确实存在“is-a”的关系时,才适合使用继承。


四、多态

多态的意思是“同一个接口,在不同对象上表现出不同的行为”。

C++ 中多态主要分为两种:

1. 编译时多态:函数重载、模板
2. 运行时多态:虚函数

面试中问到的多态,通常重点指运行时多态,也就是虚函数实现的多态。

1. 没有 virtual 的情况

#include <iostream>
using namespace std;

class Animal {
public:
    void speak() {
        cout << "动物在叫" << endl;
    }
};

class Dog : public Animal {
public:
    void speak() {
        cout << "狗在汪汪叫" << endl;
    }
};

int main() {
    Animal* animal = new Dog();

    // 没有 virtual,调用的是父类的 speak
    animal->speak();

    delete animal;

    return 0;
}

输出结果:

动物在叫

虽然 animal 指向的是 Dog 对象,但是因为 speak() 不是虚函数,所以调用的是父类的 speak()


五、虚函数

虚函数就是在成员函数前面加上 virtual 关键字。

有了虚函数之后,父类指针或引用指向子类对象时,调用同名函数会根据对象的真实类型决定调用哪个函数。

1. 虚函数示例

#include <iostream>
using namespace std;

class Animal {
public:
    // virtual 表示这是一个虚函数
    virtual void speak() {
        cout << "动物在叫" << endl;
    }
};

class Dog : public Animal {
public:
    // 子类重写父类的虚函数
    void speak() override {
        cout << "狗在汪汪叫" << endl;
    }
};

class Cat : public Animal {
public:
    // 子类重写父类的虚函数
    void speak() override {
        cout << "猫在喵喵叫" << endl;
    }
};

int main() {
    Animal* animal1 = new Dog();
    Animal* animal2 = new Cat();

    // 父类指针指向 Dog 对象,调用 Dog 的 speak
    animal1->speak();

    // 父类指针指向 Cat 对象,调用 Cat 的 speak
    animal2->speak();

    delete animal1;
    delete animal2;

    return 0;
}

输出结果:

狗在汪汪叫
猫在喵喵叫

这就是运行时多态。

2. override 的作用

在子类重写父类虚函数时,建议加上 override

void speak() override {
    cout << "狗在汪汪叫" << endl;
}

override 的作用是告诉编译器:这个函数是重写父类的虚函数。

如果函数名、参数列表写错了,编译器会报错,帮助我们提前发现问题。

3. 虚函数面试总结

面试时可以这样回答:

虚函数是实现 C++ 运行时多态的基础。在父类函数前加上 virtual 后,如果子类重写该函数,那么通过父类指针或引用调用该函数时,会根据对象的真实类型调用对应的子类函数。这样可以实现同一个接口,不同对象有不同表现。


六、虚函数表和虚函数指针

面试中有时会继续问:虚函数的底层原理是什么?

简单来说:

有虚函数的类,编译器通常会为它生成一张虚函数表。
对象内部通常会有一个虚函数指针,指向对应类的虚函数表。
调用虚函数时,会通过虚函数指针找到虚函数表,再找到真正要调用的函数。

1. 简单理解

例如:

class Animal {
public:
    virtual void speak() {
        cout << "动物在叫" << endl;
    }
};

class Dog : public Animal {
public:
    void speak() override {
        cout << "狗在汪汪叫" << endl;
    }
};

可以简单理解为:

Animal 类有自己的虚函数表,里面存 Animal::speak 的地址。
Dog 类也有自己的虚函数表,里面存 Dog::speak 的地址。

当父类指针指向子类对象时:

Animal* animal = new Dog();
animal->speak();

程序会根据对象内部的虚函数指针,找到 Dog 的虚函数表,然后调用 Dog::speak()

2. 虚函数表面试总结

面试时可以这样回答:

C++ 的虚函数通常通过虚函数表和虚函数指针实现。含有虚函数的类会有虚函数表,对象中通常会有虚函数指针。调用虚函数时,会通过对象的虚函数指针找到对应的虚函数表,再根据函数位置找到真正要调用的函数。所以虚函数可以在运行时决定调用父类函数还是子类函数。


七、虚析构函数

虚析构函数是 C++ 面试中非常高频的考点。

如果一个类要作为基类,并且可能通过父类指针删除子类对象,那么父类析构函数应该写成虚函数。

1. 没有虚析构函数的问题

#include <iostream>
using namespace std;

class Base {
public:
    Base() {
        cout << "Base 构造函数" << endl;
    }

    // 普通析构函数,不是虚函数
    ~Base() {
        cout << "Base 析构函数" << endl;
    }
};

class Derived : public Base {
public:
    Derived() {
        cout << "Derived 构造函数" << endl;
    }

    ~Derived() {
        cout << "Derived 析构函数" << endl;
    }
};

int main() {
    Base* p = new Derived();

    // 通过父类指针删除子类对象
    // 如果父类析构函数不是 virtual,可能只调用 Base 析构函数
    delete p;

    return 0;
}

这种情况下,可能只调用父类析构函数,而子类析构函数没有被正确调用。如果子类中申请了资源,就可能导致资源泄漏。

2. 正确写法:父类析构函数加 virtual

#include <iostream>
using namespace std;

class Base {
public:
    Base() {
        cout << "Base 构造函数" << endl;
    }

    // 父类析构函数写成虚函数
    virtual ~Base() {
        cout << "Base 析构函数" << endl;
    }
};

class Derived : public Base {
public:
    Derived() {
        cout << "Derived 构造函数" << endl;
    }

    ~Derived() {
        cout << "Derived 析构函数" << endl;
    }
};

int main() {
    Base* p = new Derived();

    // 父类析构函数是虚函数时,会先调用子类析构,再调用父类析构
    delete p;

    return 0;
}

正常析构顺序是:

Derived 析构函数
Base 析构函数

3. 为什么析构顺序是先子类后父类?

构造对象时,先构造父类部分,再构造子类部分。

析构对象时,顺序正好相反,先析构子类部分,再析构父类部分。

可以这样记:

构造:先父后子
析构:先子后父

4. 虚析构函数面试总结

面试时可以这样回答:

如果一个类要作为基类使用,并且可能通过父类指针删除子类对象,那么父类析构函数必须写成 virtual。否则 delete 父类指针时,可能只调用父类析构函数,而不调用子类析构函数,导致子类资源没有释放,出现内存泄漏。虚析构函数可以保证先调用子类析构函数,再调用父类析构函数。


八、纯虚函数和抽象类

纯虚函数是在虚函数后面加 = 0

含有纯虚函数的类叫抽象类。

抽象类不能直接创建对象,只能被子类继承,并要求子类实现纯虚函数。

1. 纯虚函数示例

#include <iostream>
using namespace std;

class Shape {
public:
    // 纯虚函数
    // 表示不同图形都应该有 area 方法,但具体怎么算由子类决定
    virtual double area() = 0;

    // 抽象类作为基类时,析构函数建议写成 virtual
    virtual ~Shape() {}
};

class Circle : public Shape {
private:
    double radius;

public:
    Circle(double r) {
        radius = r;
    }

    // 实现父类的纯虚函数
    double area() override {
        return 3.14 * radius * radius;
    }
};

class Rectangle : public Shape {
private:
    double width;
    double height;

public:
    Rectangle(double w, double h) {
        width = w;
        height = h;
    }

    // 实现父类的纯虚函数
    double area() override {
        return width * height;
    }
};

int main() {
    Shape* s1 = new Circle(2.0);
    Shape* s2 = new Rectangle(3.0, 4.0);

    cout << "圆的面积:" << s1->area() << endl;
    cout << "矩形的面积:" << s2->area() << endl;

    delete s1;
    delete s2;

    return 0;
}

2. 抽象类的作用

抽象类主要用来定义统一接口。

例如所有图形都有面积,但是不同图形面积计算方式不同,所以可以把 area() 定义为纯虚函数,让具体子类去实现。

3. 纯虚函数面试总结

面试时可以这样回答:

纯虚函数是在虚函数后面加 = 0,含有纯虚函数的类叫抽象类。抽象类不能直接实例化,主要用于定义接口。子类继承抽象类后,必须实现纯虚函数,否则子类仍然是抽象类。


九、函数重载、重写和隐藏

这三个概念也经常和虚函数一起考。

1. 函数重载

函数重载发生在同一个作用域中,函数名相同,但是参数列表不同。

#include <iostream>
using namespace std;

class Printer {
public:
    void print(int x) {
        cout << "打印整数:" << x << endl;
    }

    void print(string s) {
        cout << "打印字符串:" << s << endl;
    }
};

int main() {
    Printer p;

    p.print(10);
    p.print("hello");

    return 0;
}

2. 函数重写

函数重写发生在父类和子类之间,要求父类函数是虚函数,子类函数的函数名、参数列表、返回值类型要匹配。

class Base {
public:
    virtual void show() {
        cout << "Base show" << endl;
    }
};

class Derived : public Base {
public:
    void show() override {
        cout << "Derived show" << endl;
    }
};

3. 函数隐藏

函数隐藏也发生在父类和子类之间。如果子类定义了和父类同名的函数,即使参数不同,也可能隐藏父类同名函数。

#include <iostream>
using namespace std;

class Base {
public:
    void show(int x) {
        cout << "Base show int: " << x << endl;
    }
};

class Derived : public Base {
public:
    // 子类定义了同名函数 show
    // 会隐藏父类中的 show(int)
    void show() {
        cout << "Derived show" << endl;
    }
};

int main() {
    Derived d;

    d.show();

    // d.show(10);  // 错误,父类 show(int) 被隐藏了

    return 0;
}

4. 三者区别总结

重载:同一作用域,函数名相同,参数不同。
重写:父子类之间,父类是虚函数,子类重新实现。
隐藏:父子类之间,子类同名函数隐藏父类同名函数。

十、面试高频问题整理

1. 面向对象三大特性是什么?

面向对象三大特性是封装、继承和多态。

封装是隐藏内部实现,通过接口访问数据。

继承是子类复用父类已有功能,并在此基础上扩展。

多态是同一个接口在不同对象上表现出不同的行为,C++ 中运行时多态主要通过虚函数实现。


2. 什么是虚函数?

虚函数是在成员函数前加 virtual 的函数。它可以被子类重写,并且通过父类指针或引用调用时,会根据对象的真实类型决定调用父类函数还是子类函数。


3. C++ 多态如何实现?

C++ 运行时多态主要通过虚函数实现。底层通常依靠虚函数表和虚函数指针。含有虚函数的类会有虚函数表,对象中会有虚函数指针。调用虚函数时,会通过虚函数指针找到对应的虚函数表,再调用实际对象对应的函数。


4. 为什么基类析构函数要写成 virtual?

如果通过父类指针删除子类对象,而父类析构函数不是虚函数,可能只调用父类析构函数,不调用子类析构函数,导致子类资源没有释放。

所以作为基类使用的类,析构函数通常要写成虚函数。


5. 构造函数和析构函数的调用顺序是什么?

构造时,先调用父类构造函数,再调用子类构造函数。

析构时,先调用子类析构函数,再调用父类析构函数。

简单记忆:

构造:先父后子
析构:先子后父

6. 构造函数可以是虚函数吗?

构造函数不能是虚函数。

因为对象在构造过程中,虚函数机制还没有完整建立,对象类型还没有完全形成,所以构造函数不能声明为虚函数。

但是析构函数可以是虚函数,并且基类析构函数经常需要写成虚函数。


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

纯虚函数是在虚函数后面加 = 0。含有纯虚函数的类叫抽象类。

抽象类不能直接创建对象,主要用于定义接口。子类继承抽象类后,需要实现纯虚函数,否则子类仍然是抽象类。


十一、总结

本文主要整理了 C++ 面试中面向对象相关的高频知识点,包括封装、继承、多态、虚函数、虚函数表、虚析构函数、纯虚函数和抽象类。

封装主要是隐藏内部数据,通过公开接口访问,提高代码安全性和可维护性。

继承主要用于代码复用和功能扩展,子类可以继承父类已有成员。

多态表示同一个接口在不同对象上有不同表现,C++ 中运行时多态主要通过虚函数实现。

虚函数的底层通常通过虚函数表和虚函数指针实现。父类指针或引用调用虚函数时,会根据对象真实类型调用对应函数。

虚析构函数是面试重点。如果一个类作为基类使用,并且可能通过父类指针删除子类对象,那么父类析构函数应该声明为 virtual,避免子类析构函数不被调用。

简单记忆:

封装:隐藏数据,提供接口。
继承:复用代码,扩展功能。
多态:同一接口,不同表现。
虚函数:实现运行时多态。
虚析构函数:保证通过父类指针删除子类对象时析构完整。
纯虚函数:定义接口,形成抽象类。

面试中回答这类问题时,最好不要只背概念,要结合代码说明父类指针指向子类对象、虚函数调用、虚析构函数释放资源这些典型场景。

0voice · GitHub

更多推荐