C++ 面向对象
介绍c++一下三大特性
简要回答
C++的三大特性就是:封装、继承、多态。 一句话概括就是:
封装:把数据和操作打包在一起,外人不能随便动,通过public,private,protected保护数据
继承:可以“复制+扩展”父类的内容,写代码更省力。
子类通过继承使用父类公开的数据,属性和方法,包括实现继承,接口继承和可视继承
多态:用“同一种方式”操作不同的对象,扩展性强。
详细回答
1.封装(Encapsulation)
封装是指把数据和操作数据的方法包装在类里,通过**访问权限(public、private、protected)**来控制对外的暴露。
好处是提高安全性,把实现细节藏起来,别人只能用你提供的接口,而不能直接改内部数据。
2.继承(Inheritance)
继承让一个类(子类)拥有另一个类(父类)的属性和行为,可以在原有功能的基础上做改进和拓展。
有了继承,代码可以复用,比如定义一个Animal类,再派生出Dog、Cat等具体动物类,不用重复写代码。
3.多态(Polymorphism)
多态指的是“同一种接口,不同的行为”。 主要有两种:
编译时多态(函数重载、运算符重载)运行时多态(虚函数+继承)
比如写了一个 speak() 接口,猫、狗调用它表现都不一样,这就是多态的威力。
其中编译时多态又叫静态多态,指的是在编译阶段就确定了函数调用的绑定关系。
典型方式有两个:函数重载和运算符重载
运行时多态又叫动态多态,指的是在程序运行时根据对象的实际类型来决定调用哪个函数。
原理是通过重写父类虚函数,生成虚函数表和虚函数指针,通过父类引用指向不同的子类对象选取不同的虚函数表进而实现多态。
代码示例
#include <iostream>
using namespace std;
// 封装
class Animal {
protected:
string name;
public:
Animal(string n) : name(n) {}
virtual void speak() { // 多态:虚函数
cout << name << " is making a sound." << endl;
}
};
// 继承
class Dog : public Animal {
public:
Dog(string n) : Animal(n) {}
void speak() override { // 重写父类方法,实现运行时多态
cout << name << " says: Woof!" << endl;
}
};
class Cat : public Animal {
public:
Cat(string n) : Animal(n) {}
void speak() override {
cout << name << " says: Meow!" << endl;
}
};
// 多态的体现
void animalSpeak(Animal* a) {
a->speak(); // 根据对象的真实类型调用不同的方法
}
int main() {
Dog d("Buddy");
Cat c("Kitty");
animalSpeak(&d); // 输出:Buddy says: Woof!
animalSpeak(&c); // 输出:Kitty says: Meow!
return 0;
}知识拓展
- 知识图解

依据 《C++ Primer (第5版)》 在第15章“面向对象编程”
- 面试官可能追问
- 编译时多态可以代替运行时多态吗?
答: 不完全可以。
编译时多态(如模板、函数重载)是在编译期确定函数调用,适用于参数类型、数量已知的情况。
而运行时多态(虚函数+继承)则是运行时根据对象真实类型做动态绑定,适用于“需要统一接口处理不同对象”的情况。
当对象的真实类型在编译时就能确定,并且想避免虚函数开销,可以用模板+策略模式模拟运行时多态效果。
- 虚函数调用的底层机制是怎样的?vtable 和 vptr 是怎么工作的?
答:虚函数背后有三样东西:虚函数表,虚指针,动态绑定过程。
虚函数表(vtable): 每个含虚函数的类在编译时都会生成一个vtable,里面存的是该类的虚函数地址列表。
虚指针(vptr): 每个对象内部都含一个隐藏的指针(vptr),指向它所属类的vtable。
动态绑定过程: 如:当用Animal* a = new Dog()时: 创建Dog对象时,会设置a->vptr指向Dog类的vtable。
当调用a->speak(),程序会通过vptr找到Dog类的vtable,调用里边的speak()地址。
- 拓展:
vtable 是类级别的(多个对象共享),vptr是对象级别的
- 能不用虚函数实现运行时多态吗?
答:可以,但要用其他技巧,比如函数指针、策略模式、std::function
简述一下 C++ 的重载和重写,以及它们的区别和实现方式。
简要回答
重载(Overload):函数名相同,参数不同,发生在同一个作用域(类内)。
即在同一个类里,函数名字一样,但参数不一样,编译器自己能分清。
重写(Override):派生类中重写父类的虚函数,函数名、参数、返回值完全相同,使用virtual 和 override 关键字。
即子类把父类的虚函数重新写一遍,运行时决定用哪一个。
详细回答
- 重载(函数重载 Function Overloading)
重载就是一个函数名可以用好几次,只要参数类型或者个数不一样就行。比如你写个 print 函数,可以传 int、float、string,各自有不同的实现,这就是重载。
它有以下三个特点
- 发生在同一个类里面
- 编译器在编译的时候能决定哪个用,叫编译期多态,属于静态绑
- 和返回值没有关系,不能光靠返回值来区分重载
- 重写(函数重写 Function Overriding)
重写就是子类对父类里的虚函数重新写一遍,达到“改写原来功能”的目的。
它有以下四个特点
- 必须有继承关系
- 父类函数要有virtual类型
- 子类函数和父类一模一样(名字、参数、返回值都不能改)
- 最好写上override,避免写错(c++11提倡这么写)
代码示例
- 重载
class Printer {
public:
void show(int x) {
std::cout << "打印 int: " << x << std::endl;
}
void show(double x) {
std::cout << "打印 double: " << x << std::endl;
}
};
// 同一个类,函数名一样,参数不一样,这就是重载- 重写
class Animal {
public:
virtual void speak() {
std::cout << "动物在叫" << std::endl;
}
};
class Dog : public Animal {
public:
void speak() override {
std::cout << "狗在叫:汪汪汪" << std::endl;
}
};
// 用父类指针调用:
Animal* a = new Dog();
a->speak(); // 输出:狗在叫:汪汪汪知识拓展
知识图解

什么是虚函数
答:virtual 是用来支持“多态”的关键字。它告诉编译器,这个函数将来可能会被“子类改写”,所以真正调用哪个版本,要在运行时决定。
- 面试官可能追问
- 重载能不能靠返回值来区分?
答: 不行,编译器光看返回值分不清哪个函数,得靠参数。
- override 有啥用?
答: 防止你写错函数签名,比如你拼错了参数,它能报错。
- 不加 virtual 会怎样?
答: 那就不是重写,调用的是父类的函数,不是子类的。 4. virtual 和 override 的区别?
答: virtual 是用在父类函数上,告诉编译器:“这个函数以后可能会被重写”; override 是用在子类函数上,告诉编译器:“我就是要重写你父类的函数”。
- virtual 和 override 哪个必须?
答: virtual 是前提,父类必须是虚函数;override 是附加保障,算是一种检查机制。
有哪些访问修饰符
C++提供了三个访问修饰符:public、private 和 protected。这些修饰符决定了类中的成员对外部代码的可见性和访问权限。
public 修饰符用于指定类中的成员可以被类的外部代码访问。公有成员可以被类外部的任何代码(包括类的实例)访问。
private 修饰符用于指定类中的成员只能被类的内部代码访问。私有成员对外部代码是不可见的,只有类内部的成员函数可以访问私有成员。
protected 修饰符用于指定类中的成员可以被类的派生类访问。受保护成员对外部代码是不可见的,但可以在派生类中被访问。
什么是多重继承?
一个类可以从多个基类(父类)继承属性和行为。在C++等支持多重继承的语言中,一个派生类可以同时拥有多个基类。
多重继承可能引入一些问题,如菱形继承问题, 比如当一个类同时继承了两个拥有相同基类的类,而最终的派生类又同时继承了这两个类时, 可能导致二义性和代码设计上的复杂性。为了解决这些问题,C++ 提供了虚继承, 通过在继承声明中使用 virtual 关键字,可以避免在派生类中生成多个基类的实例,从而解决了菱形继承带来的二义性。
#include <iostream>
class Animal {
public:
void eat() {
std::cout << "Animal is eating." << std::endl;
}
};
class Mammal : public Animal {
public:
void breathe() {
std::cout << "Mammal is breathing." << std::endl;
}
};
class Bird : public Animal {
public:
void fly() {
std::cout << "Bird is flying." << std::endl;
}
};
// 菱形继承,同时从 Mammal 和 Bird 继承
class Bat : public Mammal, public Bird {
public:
void navigate() {
// 这里可能会引起二义性,因为 Bat 继承了两个 Animal
// navigate 方法中尝试调用 eat 方法,但不明确应该调用 Animal 的哪一个实现
eat();
}
};
int main() {
Bat bat;
bat.navigate();
return 0;
}
#include <iostream>
class Animal {
public:
void eat() {
std::cout << "Animal is eating." << std::endl;
}
};
class Mammal : virtual public Animal {
public:
void breathe() {
std::cout << "Mammal is breathing." << std::endl;
}
};
class Bird : virtual public Animal {
public:
void fly() {
std::cout << "Bird is flying." << std::endl;
}
};
class Bat : public Mammal, public Bird {
public:
void navigate() {
// 不再存在二义性,eat 方法来自于共享的 Animal 基类
eat();
}
};
int main() {
Bat bat;
bat.navigate();
return 0;
}c++的多态如何实现
C++中的多态性是通过虚函数(virtual function)和虚函数表(vtable)来实现的。多态性允许在基类类型的指针或引用上调用派生类对象的函数,以便在运行时选择正确的函数实现。
- 基类声明虚函数:在基类中声明虚函数,使用
virtual关键字,以便派生类可以重写(override)这些函数。
class Shape {
public:
virtual void draw() const {
// 基类的默认实现
}
};- 派生类重写虚函数:在派生类中重写基类中声明的虚函数,使用
override关键字
class Circle : public Shape {
public:
void draw() const override {
// 派生类的实现
}
};- 使用基类类型的指针或引用指向派生类对象。
Shape* shapePtr = new Circle();- 调用虚函数:通过基类指针或引用调用虚函数。在运行时,系统会根据对象的实际类型来选择调用正确的函数实现。
shapePtr->draw(); // 调用的是 Circle 类的 draw() 函数- 虚函数表:编译器在对象的内存布局中维护了一个虚函数表,其中存储了指向实际函数的指针。这个表在运行时用于动态查找调用的函数。
成员函数/成员变量/静态成员函数/静态成员变量的区别
- 成员函数
- 成员函数是属于类的函数,它们可以访问类的成员变量和其他成员函数。
- 成员函数可以分为普通成员函数和静态成员函数。
- 普通成员函数使用对象调用,可以访问对象的成员变量。
- 普通成员函数的声明和定义通常在类的内部,但定义时需要使用类名作为限定符。
- 成员变量
- 成员变量是属于类的变量,存储在类的每个对象中。
- 每个对象拥有一份成员变量的副本,它们在对象创建时分配,并在对象销毁时释放。
- 成员变量的访问权限可以是
public、private或protected。
class MyClass {
public:
int memberVariable; // 成员变量的声明
void memberFunction() {
// 成员函数的实现
}
};- 静态成员函数
- 静态成员函数属于类而不是对象,因此可以直接通过类名调用,而不需要创建类的实例。
- 静态成员函数不能直接访问普通成员变量,因为它们没有隐含的
this指针。 - 静态成员函数的声明和定义也通常在类的内部,但在定义时需要使用类名作为限定符。
- 静态成员变量
- 静态成员变量是属于类而不是对象的变量,它们在所有对象之间共享。
- 静态成员变量通常在类的声明中进行声明,但在类的定义外进行定义和初始化。
- 静态成员变量可以通过类名或对象访问。
class MyClass {
public:
static int staticMemberVariable; // 静态成员变量的声明
static void staticMemberFunction() {
// 静态成员函数的实现
}
};
int MyClass::staticMemberVariable = 0; // 静态成员变量的定义和初始化什么是构造函数和析构函数?
- 构造函数
构造函数是在创建对象时自动调用的特殊成员函数。它的主要目的是初始化对象的成员变量,为对象分配资源,执行必要的初始化操作。构造函数的特点包括:
函数名与类名相同: 构造函数的函数名必须与类名相同,且没有返回类型,包括 void。
可以有多个构造函数: 一个类可以有多个构造函数,它们可以根据参数的类型和数量不同而重载。
默认构造函数: 如果没有为类定义任何构造函数,编译器会自动生成一个默认构造函数。默认构造函数没有参数,也可能执行一些默认的初始化操作。
class MyClass {
public:
// 默认构造函数
MyClass() {
// 初始化操作
}
// 带参数的构造函数
MyClass(int value) {
// 根据参数进行初始化操作
}
};- 析构函数
析构函数是在对象生命周期结束时自动调用的特殊成员函数。它的主要目的是释放对象占用的资源、执行必要的清理操作。析构函数的特点包括:
函数名与类名相同,前面加上波浪号
~: 析构函数的函数名为~ClassName,其中ClassName是类名。没有参数: 析构函数没有参数,不能重载,每个类只能有一个析构函数。
默认析构函数: 如果没有为类定义任何析构函数,编译器会自动生成一个默认析构函数,执行简单的清理操作。
class MyClass {
public:
// 析构函数
~MyClass() {
// 清理操作,释放资源
}
};C++构造函数有几种,分别什么作用
- 默认构造函数:没有任何参数的构造函数。如果用户没有为类定义构造函数,编译器会自动生成一个默认构造函数。默认构造函数用于创建对象时的初始化,当用户不提供初始化值时,编译器将调用默认构造函数。
class MyClass {
public:
// 默认构造函数
MyClass() {
// 初始化操作
}
};- 带参数的构造函数:接受一个或多个参数,用于在创建对象时传递初始化值。可以定义多个带参数的构造函数,以支持不同的初始化方式。
class MyClass {
public:
// 带参数的构造函数
MyClass(int value) {
// 根据参数进行初始化操作
}
};- 拷贝构造函数:用于通过已存在的对象创建一个新对象,新对象是原对象的副本。参数通常是对同类型对象的引用。
class MyClass {
public:
// 拷贝构造函数
MyClass(const MyClass &other) {
// 进行深拷贝或浅拷贝,根据实际情况
}
};- 委托构造函数:在一个构造函数中调用同类的另一个构造函数,减少代码重复。通过成员初始化列表或构造函数体内部调用其他构造函数。
class MyClass {
public:
// 委托构造函数
MyClass() : MyClass(42) {
// 委托给带参数的构造函数
}
MyClass(int value) {
// 进行初始化操作
}
};什么是虚函数和虚函数表?
- 虚函数
C++中的虚函数的作用主要是实现了多态的机制。虚函数允许在派生类中重新定义基类中定义的函数,使得通过基类指针或引用调用的函数在运行时根据实际对象类型来确定。这样的机制被称为动态绑定或运行时多态。
在基类中,通过在函数声明前面加上 virtual 关键字,可以将其声明为虚函数。派生类可以重新定义虚函数,如果派生类不重新定义,则会使用基类中的实现。
class Base {
public:
virtual void virtualFunction() {
// 虚函数的实现
}
};
class Derived : public Base {
public:
void virtualFunction() override {
// 派生类中对虚函数的重新定义
}
};- 虚函数表
虚函数的实现通常依赖于一个被称为虚函数表(虚表)的数据结构。每个类(包括抽象类)都有一个虚表,其中包含了该类的虚函数的地址。每个对象都包含一个指向其类的虚表的指针,这个指针被称为虚指针(vptr)。
当调用一个虚函数时,编译器会使用对象的虚指针查找虚表,并通过虚表中的函数地址来执行相应的虚函数。这就是为什么在运行时可以根据实际对象类型来确定调用哪个函数的原因。
虚函数和纯虚函数的区别
- 虚函数
有实现: 虚函数有函数声明和实现,即在基类中可以提供默认实现。
可选实现: 派生类可以选择是否覆盖虚函数。如果派生类没有提供实现,将使用基类的默认实现。
允许实例化: 虚函数的类可以被实例化。即你可以创建一个虚函数的类的对象。
调用靠对象类型决定: 在运行时,根据对象的实际类型来决定调用哪个版本的虚函数。
用
virtual关键字声明: 虚函数使用virtual关键字声明,但不包含= 0。
class Base {
public:
// 虚函数有实现
virtual void virtualFunction() {
// 具体实现
}
};- 纯虚函数
没有实现: 纯虚函数没有函数体,只有函数声明,即没有提供默认的实现。
强制覆盖: 派生类必须提供纯虚函数的具体实现,否则它们也会成为抽象类。
禁止实例化: 包含纯虚函数的类无法被实例化,只能用于派生其他类。
用
= 0声明: 纯虚函数使用= 0在函数声明末尾进行声明。为接口提供规范: 通过纯虚函数,抽象类提供一种接口规范,要求派生类提供相关实现。
class AbstractBase {
public:
// 纯虚函数,没有具体实现
virtual void pureVirtualFunction() = 0;
// 普通成员函数可以有具体实现
void commonFunction() {
// 具体实现
}
};什么是抽象类和纯虚函数?
抽象类是不能被实例化的类,它存在的主要目的是为了提供一个接口,供派生类继承和实现。抽象类中可以包含普通的成员函数、数据成员和构造函数,但它必须包含至少一个纯虚函数。即在声明中使用 virtual 关键字并赋予函数一个 = 0 的纯虚函数。
class AbstractShape {
public:
// 纯虚函数,提供接口
virtual void draw() const = 0;
// 普通成员函数
void commonFunction() {
// 具体实现
}
};纯虚函数是在抽象类中声明的虚函数,它没有具体的实现,只有函数的声明。通过在函数声明的末尾使用 = 0,可以将虚函数声明为纯虚函数。派生类必须实现抽象类中的纯虚函数,否则它们也会成为抽象类。
class AbstractShape {
public:
// 纯虚函数
virtual void draw() const = 0;
};简述一下虚析构函数,什么作用
虚析构函数是一个带有 virtual 关键字的析构函数。 主要作用是确保在通过基类指针删除派生类对象时,能够正确调用派生类的析构函数,从而释放对象所占用的资源。
通常,如果一个类可能被继承,且在其派生类中有可能使用 delete 运算符来删除通过基类指针指向的对象,那么该基类的析构函数应该声明为虚析构函数。
class Base {
public:
// 虚析构函数
virtual ~Base() {
// 基类析构函数的实现
}
};
class Derived : public Base {
public:
// 派生类析构函数,可以覆盖基类的虚析构函数
~Derived() override {
// 派生类析构函数的实现
}
};说说为什么要虚析构,为什么不能虚构造
在C++中,构造函数不能声明为虚函数(virtual)。这主要由对象的构造顺序决定:
vptr的初始化时机:当创建派生类对象时,基类部分的构造优先进行。在进入基类的构造函数体之前,编译器会自动初始化该对象基类部分的虚函数表指针(vptr),使其指向基类的虚函数表(vtable)。
顺序矛盾:虚函数机制(通过vptr和vtable进行动态绑定)依赖于一个已经部分构造好的对象(至少其vptr已被初始化)。而构造函数的工作正是创建和初始化这个对象本身。
根本原因:如果构造函数是虚函数,就意味着调用它需要通过vptr来查找vtable。但在构造函数执行之前,vptr本身尚未被初始化,这就产生了一个无法解决的悖论。因此,从语言层面禁止这种做法是唯一合理的选择。
依据《深度探索C++对象模型》书籍第5章 "构造、析构、拷贝语意学”这一节中详细描述了对象构造过程中的每一步,明确指出vptr的初始化操作是在构造函数体的代码执行之前由编译器插入代码完成的。
哪些函数不能被声明为虚函数?
常见的不不能声明为虚函数的有:普通函数(非成员函数),静态成员函数,内联成员函数,构造函数,友元函数。
- 构造函数:
构造函数在对象的创建期间调用,对象的类型在构造期间已经确定。因此,构造函数不能是虚函数,因为虚函数的动态绑定是在运行时实现的,而构造函数在对象还未创建完全时就会被调用。
- 普通函数
普通函数(非成员函数)只能被overload,不能被override,声明为虚函数也没有什么意思,因此编译器会在编译时绑定函数。
- 静态成员函数
静态成员函数对于每个类来说只有一份代码,所有的对象都共享这一份代码,他也没有要动态绑定的必要性。
- 友元函数
因为C++不支持友元函数的继承,对于没有继承特性的函数没有虚函数的说法。
- 内联成员函数
内联函数就是为了在代码中直接展开,减少函数调用花费的代价,虚函数是为了在继承后对象能够准确的执行自己的动作,这是不可能统一的。(再说了,inline函数在编译时被展开,虚函数在运行时才能动态的绑定函数)
内联函数是在编译时期展开,而虚函数的特性是运行时才动态联编,所以两者矛盾,不能定义内联函数为虚函数
深拷贝和浅拷贝的区别
主要区别在于如何处理对象内部的动态分配的资源。
- 深拷贝
深拷贝是对对象的完全独立复制,包括对象内部动态分配的资源。在深拷贝中,不仅复制对象的值,还会复制对象所指向的堆上的数据。
主要特点:
复制对象及其所有成员变量的值。
动态分配的资源也会被复制,新对象拥有自己的一份资源副本。
深拷贝通常涉及到手动分配内存,并在拷贝构造函数或赋值操作符中进行资源的复制。
class DeepCopyExample {
public:
int *data;
DeepCopyExample(const DeepCopyExample &other) {
// 手动分配内存并复制数据
data = new int(*(other.data));
}
~DeepCopyExample() {
// 释放动态分配的资源
delete data;
}
DeepCopyExample& operator=(const DeepCopyExample &other) {
// 复制数据
if (this != &other) {
delete data;
data = new int(*(other.data));
}
return *this;
}
};- 浅拷贝
浅拷贝仅复制对象的值,而不涉及对象内部动态分配的资源。在浅拷贝中,新对象和原对象共享相同的资源,而不是复制一份新的资源。
主要特点:
复制对象及其所有成员变量的值。
对象内部动态分配的资源不会被复制,新对象和原对象共享同一份资源。
浅拷贝通常使用默认的拷贝构造函数和赋值操作符,因为它们会逐成员地复制原对象的值。
class ShallowCopyExample {
public:
int *data;
// 使用默认拷贝构造函数和赋值操作符
};运算符重载
重载运算符函数,本质还是函数调用,所以重载后:
(1)可以是和调用运算符的方式调用,data1+data2
(2)也可以是调用函数的方式,operator+(data1, data2),这就要注意运算符函数的名字是“operator运算符”
在可以重载的运算符里有逗号、取地址、逻辑与、逻辑或
不建议重载:
逗号、取地址,本身就对类类型有特殊定义;逻辑与、逻辑或,有短路求值属性;逗号、逻辑与、或,定义了求值顺序。
运算符重载应该是作为类的成员函数or非成员函数(具体后面各小节会涉及)。有个对应知识点,
注意:
重载运算符,它本身是几元就有几个参数,对于二元的,第一个参数对应左侧运算对象,第二个参数对应右侧运算对象。而!类的成员函数的第一个参数隐式绑定了this指针,所以重载运算符如果是类的成员函数,左侧运算对象就相当于固定了是this。
建议非成员:
又因为要访问类的私有成员,多为类的友元。返回值iostream的引用,第一个参数iostream的引用,第二个参数,输出用const、输入非常量。输入的重载里注意判断是否成功,避免输入了不合预期的内容。
一些规则:
(1)算术和关系运算符建议非成员
因为这些运算符是对称性的,形参都是常量引用
(2)赋值运算符必须成员。复合赋值运算符建议成员
(3)下标运算符必须成员
返回访问元素的引用,建议两版本(常量、非常量)
(4)递增递减运算符,建议成员
因其会改变对象状态,后置与前置的区分——接受一个额外的不被使用的int类型形参,前置返回变后的对象引用,后置返回对象的原值(非引用);解引用(*)建议成员,因其与给定类型关系密切,箭头(->)必须成员。
函数调用运算符:
lambda是函数对象。编译器是将lambda表达式翻译为一个未命名类的未命名对象,‘[’捕获列表‘]’(参数列表){函数体} 对应类中重载调用运算符的参数列表、函数体,捕获列表的内容就对应类中的数据成员。所以捕获列表,值传递时,要拷贝并初始化那些数据成员,引用传递就是直接用。
