大道至简---C++的三大特性详解

#【投稿赢 iPhone 17】「我的第一个开源项目」故事征集:用代码换C位出道!#

C++特性

C++有三大特性:多态、继承、封装

1、多态

1.1、什么是多态

同一个接口(函数调用),在不同对象中表现出不同的行为。

因此我们可以将多态分为两种:

多态类型
编译时多态(静态多态)
运行时多态(动态多态)
1.1.1、编译时多态(静态多态)

在C++中主要靠函数的重载或者模板来实现

void Print(int x) { std::cout << "int: " << x << "\n"; }

void Print(double x) { std::cout << "double: " << x << "\n"; }

int main() {
    Print(3);     // 调用第一个
    Print(3.14);  // 调用第二个
}

1.1.2、运行时多态(动态多态)

动态多态主要靠 继承 + 虚函数(virtual function) 实现。
主要有下面三个特征:

1、基类有一个或多个 virtual 函数;

2、子类重写(override)这些函数;

3、通过 基类指针或引用 调用时,调用的是“实际对象”的函数。

下面我们看一个demo

#include <iostream>
using namespace std;

class Animal
{
public:
    virtual void Speak() // 虚函数
    {
        cout << "Animal sound\n";
    }

    void TestFunc()
    {
        cout << "TestFunc!\n";
    }
};

class Dog : public Animal
{
public:
    void Speak() override // 重写
    {
        cout << "Woof!\n";
    }
};

class Cat : public Animal
{
public:
    void Speak() override
    {
        cout << "Meow!\n";
    }
};

int main()
{
    Animal *a1 = new Dog();
    Animal *a2 = new Cat();

    a1->Speak(); // 输出 "Woof!"
    a2->Speak(); // 输出 "Meow!"

    a1->TestFunc();
    a2->TestFunc();

    delete a1;
    delete a2;
}

可以看到即使我们用的是 Animal*(基类指针),程序在运行时仍能根据对象的“真实类型”调用对应的函数。因为virtual 关键字告诉编译器,这个函数的具体实现要在运行时再确定。编译器因此在对象中建立一个“虚函数表(vtable)”,在运行时动态查找函数。因此即使你用 Dog* 去调用,也永远会执行Animal::Speak() —— 因为编译器在 编译期 就决定了函数调用地址。

什么是虚函数表(vtable)
当一个类中出现了 至少一个 virtual 函数 时,编译器会为这个类自动生成一个隐藏的“虚函数表”,简称 vtable。可以把它想象成:一个“函数指针数组”,里面存放着类中虚函数的实际地址。

#include <iostream>
using namespace std;

class Animal {
public:
    virtual void Speak() { cout << "Animal sound\n"; }
    virtual void Eat()   { cout << "Animal eat\n"; }
};

class Dog : public Animal {
public:
    void Speak() override { cout << "Dog bark\n"; }
};

Animal 类有一个虚表,里面存两项:

vtable(Animal):
[0] -> Animal::Speak()
[1] -> Animal::Eat()

Dog 类继承自 Animal,重写了 Speak(),所以它的表是:

vtable(Dog):
[0] -> Dog::Speak()    // 被重写
[1] -> Animal::Eat()   // 继承原函数
1.1.3、对象与 vptr(虚表指针

每个包含虚函数的对象实例中,编译器都会偷偷加一个隐藏的成员变量,叫 vptr(Virtual Table Pointer)。这个 vptr 指向该对象所属类的 vtable。
所以每个对象都知道“该调用哪张表”。

Dog d;
Animal* a = &d;
a->Speak();

执行时的过程是:

1、a 是一个 Animal* 指针;

2、但它实际上指向一个 Dog 对象;

3、编译器生成的代码是:“通过 a 的 vptr,找到虚表,然后调用表中第 0 个函数”;

4、在 Dog 的 vtable 中,第 0 个函数是 Dog::Speak();

5、最终调用了 Dog::Speak()。

a (Animal*) ---> [对象内存布局]
                  ├── vptr ---> vtable(Dog)
                                    ├── [0] -> Dog::Speak()
                                    ├── [1] -> Animal::Eat()

元素名称功能
virtual虚函数关键字让函数支持运行时绑定
vtable虚函数表存放虚函数地址
vptr虚表指针每个对象中隐藏的指针,指向所属类的 vtable
运行时调用过程动态绑定通过 vptr 查表,找到实际函数地址调用
1.1.4、基类应当有虚析函数

基类应当有虚析函数,这个是非常重要的一点,我们举个例。

#include <iostream>
using namespace std;

class Animal
{
public:
    ~Animal()
    {
        cout << "This is the destructor of Animal!\n";
    }

public:
    virtual void Speak() // 虚函数
    {
        cout << "Animal sound\n";
    }
};

class Dog : public Animal
{
public:
    ~Dog()
    {
        cout << "This is the destructor of Dog!\n";
    }

public:
    void Speak() // 重写
    {
        cout << "Woof!\n";
    }
};

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

    a1->Speak(); // 输出 "Woof!"

    delete a1;
    return 0;
}

运行结果

Woof!
This is the destructor of Animal!

可以看到在析构时候只调用了父类的析构函数并没有调用派生类的析构函数。
修改一下demo

#include <iostream>
using namespace std;

class Animal
{
public:
    Animal()
    {
        cout << "This is the constructor of Animal!\n";
    }
    virtual ~Animal()
    {
        cout << "This is the destructor of Animal!\n";
    }

public:
    virtual void Speak() // 虚函数
    {
        cout << "Animal sound\n";
    }

private:
};

class Dog : public Animal
{
public:
    ~Dog() override
    {
        cout << "This is the destructor of Dog!\n";
    }
    Dog()
    {
        cout << "This is the constructor of Dog!\n";
    }

public:
    void Speak() override // 重写
    {
        cout << "Woof!\n";
    }
};

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

    a1->Speak(); // 输出 "Woof!"

    delete a1;
    return 0;
}

运行结果

This is the constructor of Animal!
This is the constructor of Dog!
Woof!
This is the destructor of Dog!
This is the destructor of Animal!

可以看到先执行了基类的构造函数再执行了派生类的构造函数,在析构则先执行了派生类的析构函数再执行基类的析构函数。

我们修改一下实例化

Dog *a1 = new Dog();

再执行代码

This is the constructor of Animal!
This is the constructor of Dog!
Woof!
This is the destructor of Dog!
This is the destructor of Animal!

可以看到两次输出一模一样。
首先我们要明白一个道理:多态的意义 —— “一个指针可以指向不同类型的对象”。

Dog *a1 = new Dog();

这种方式就失去了多态的意义

情况指针类型多态性能否指向其它派生类主要用途
Dog* a1 = new Dog();派生类指针❌ 否❌ 只能指向 Dog用于直接操作具体类型
Animal* a1 = new Dog();基类指针✅ 是✅ 可指向 Dog、Cat、Bird…用于多态接口编程
1.1.5、抽象类(Abstract Class)

抽象类:指的是包含至少一个纯虚函数的类。

特征描述
包含纯虚函数✅ 是抽象类
不能被实例化❌ 不能创建对象
可以定义指针/引用✅ 可以用作基类接口
派生类必须实现所有纯虚函数✅ 否则派生类也抽象

纯虚函数 vs 普通虚函数 对比表

特性普通虚函数纯虚函数
是否有实现✅ 可以有❌ 没有(=0)
是否强制派生类重写❌ 否✅ 是
所在类是否能实例化✅ 可以❌ 不行(抽象类)
是否存在 vtable✅ 有✅ 也有(只不过函数项为空指针)

2、继承

2.1、什么是继承

继承就是一个类从另一个类获得成员变量和成员函数的机制。

被继承的类叫 基类(Base class) 或 父类(Parent class);

继承它的类叫 派生类(Derived class) 或 子类(Child class)。

简单理解:“子类继承父类的特征和行为,还可以扩展自己的新功能。”

举例个简单的例子

#include <iostream>
using namespace std;

class Animal   // 基类
{
public:
    void Eat() {
        cout << "Animal is eating\n";
    }
};

// Dog 从 Animal 继承
class Dog : public Animal   // 派生类
{
public:
    void Bark() {
        cout << "Dog is barking\n";
    }
};

int main() {
    Dog d;      // 创建派生类对象
    d.Eat();    // ✅ 继承自 Animal
    d.Bark();   // ✅ 自己的函数
}

运行结果

Animal is eating
Dog is barking
2.1.1、继承的语法
class 派生类名 : 继承方式 基类名
继承方式含义派生类中基类成员的可见性变化
public公有继承publicpublicprotectedprotectedprivateprivate
protected保护继承publicprotectedprotectedprotectedprivateprivate
private私有继承publicprivateprotectedprivateprivateprivate
2.1.2、继承的类型

1、单继承(Single Inheritance): 一个子类只继承一个父类:

class Dog : public Animal {};

2、多重继承(Multiple Inheritance): 一个子类继承多个父类:

class Swimmer {
public:
    void Swim() { cout << "Swimming\n"; }
};

class Flyer {
public:
    void Fly() { cout << "Flying\n"; }
};

class Duck : public Swimmer, public Flyer {};  // 🦆 会游又会飞

2.1.3、继承中的构造与析构顺序

demo

class Base {
public:
    Base()  { cout << "Base constructor\n"; }
    ~Base() { cout << "Base destructor\n"; }
};

class Derived : public Base {
public:
    Derived()  { cout << "Derived constructor\n"; }
    ~Derived() { cout << "Derived destructor\n"; }
};

int main() {
    Derived d;
}

运行结果

Base constructor
Derived constructor
Derived destructor
Base destructor

💡 构造:从父到子

💡 析构:从子到父

2.1.3、public继承
#include <iostream>
using namespace std;

class Animal   // 基类
{
public:
    void Eat() {
        cout << "Animal is eating\n";
    }

protected:
    int Number = 1;

private:
    int Type = 0;
};

// Dog 从 Animal 继承
class Dog : public Animal   // 派生类
{
public:
    void Bark() {
        cout << "Dog is barking\n";
        cout << "Number is "<<Number<<endl; // <---内部可以访问基类中的protected属性成员
        // cout << "Type is "<<Type<<endl;   <---无法访问基类中的private属性成员 编译就报错
    }
};

int main() {
    Dog d;      // 创建派生类对象
    d.Eat();    // ✅ 继承自 Animal // <---外部可以直接访问基类中的public属性成员
    d.Bark();   // ✅ 自己的函数
}

运行结果

Animal is eating
Dog is barking
Number is 1
2.1.3、protected继承
#include <iostream>
using namespace std;

class Animal   // 基类
{
public:
    void Eat() {
        cout << "Animal is eating\n";
    }

protected:
    int Number = 1;

private:
    int Type = 0;
};

// Dog 从 Animal 继承
class Dog : protected Animal   // 派生类
{
public:
    void Bark() {
        cout << "Dog is barking\n";
        cout << "Number is "<<Number<<endl; // <---内部可以访问基类中的protected属性成员,protected继承,基类中的protected再派生类中可见性仍为protected
        // cout << "Type is "<<Type<<endl;     <---无法访问基类中的private属性成员 编译就报错
        Eat();                              // <---因为是protected继承,基类的中的public再派生类中可见性变成了protected,只有派生类内部可以访问。
    }
};

int main() {
    Dog d;      // 创建派生类对象
    // d.Eat();    // ✅ 继承自 Animal // <---因为是protected继承,基类中的的public再派生类中可见性变成了protected,外部无法访问。
    d.Bark();   // ✅ 自己的函数
}

运行结果

Dog is barking
Number is 1
Animal is eating
2.1.4、private继承
#include <iostream>
using namespace std;

class Animal   // 基类
{
public:
    void Eat() {
        cout << "Animal is eating\n";
    }

protected:
    int Number = 1;

private:
    int Type = 0;
};

// Dog 从 Animal 继承
class Dog : private Animal   // 派生类
{
public:
    void Bark() {
        cout << "Dog is barking\n";
        cout << "Number is "<<Number<<endl; // <---内部可以访问基类中的protected属性成员,private继承,基类中的protected再派生类中可见性为private,只有派生类内部可以访问。
        // cout << "Type is "<<Type<<endl;     <---无法访问基类中的private属性成员 编译就报错
        Eat();                              // <---因为是private继承,基类的中的public再派生类中可见性变成了private,只有派生类内部可以访问。
    }
};

int main() {
    Dog d;      // 创建派生类对象
    // d.Eat();    // ✅ 继承自 Animal // <---因为是protected继承,基类中的的public再派生类中可见性变成了private,外部无法访问。
    d.Bark();   // ✅ 自己的函数
}

运行结果

Dog is barking
Number is 1
Animal is eating
2.1.5、多重继承的菱形问题

假设我们有以下继承结构 👇

      Animal
       /  \
   Dog      Cat
       \  /
       Tiger

假设有下述代码

#include <iostream>
using namespace std;

class Animal {
public:
    int age = 1;
};

class Dog : public Animal {
};

class Cat : public Animal {
};

class Tiger : public Dog, public Cat {
public:
    void Show() {
        cout << "Dog::age = " << Dog::age << endl;
        cout << "Cat::age = " << Cat::age << endl;
    }
};

int main() {
    Tiger t;
    t.Show();
}

运行结果

Dog::age = 1
Cat::age = 1

如果修改一下代码

    void Show() {
        cout << "age = " << age << endl;
    }

编译都无法通过

cl.cpp:18:29: error: reference to ‘age’ is ambiguous
         cout << "age = " << age << endl;
                             ^
cl.cpp:6:15: note: candidates are: int Animal::age
     int age = 1;
               ^
cl.cpp:6:15: note:                 int Animal::age

❌ 因为 Tiger 从 Dog 和 Cat 都继承了 Animal,所以 Tiger 内部有两个 Animal 子对象,导致成员访问“二义性”(ambiguous)。

Tiger
 ├── Dog
 │    └── Animal (一份)
 └── Cat
      └── Animal (另一份)

Tiger 里有两个独立的 Animal,数据重复、访问冲突。

为了解决这种问题C++引入了虚继承(virtual inheritance)

#include <iostream>
using namespace std;

class Animal {
public:
    int age = 1;
};

class Dog : virtual public Animal {};
class Cat : virtual public Animal {};

class Tiger : public Dog, public Cat {
public:
    void Show() {
        cout << "age = " << age << endl;
    }
};

int main() {
    Tiger t;
    t.Show();
    t.age = 5;
    t.Show();
    return 0;
}

输出

age = 1
age = 5

写 virtual public Animal 时,编译器会让 所有虚继承的派生类共享同一个 Animal 实例。结构如下:

Tiger
 ├── Dog
 ├── Cat
 └── Animal (共享的一个实例)

✅ 所有路径上的基类 Animal 都指向同一份对象。
✅ 再也不会出现重复拷贝或访问冲突。

再看一下多重继承的菱形问题出现时候的构造顺序

#include <iostream>
using namespace std;

class Animal {
public:
    Animal() { cout << "Animal()\n"; }
};

class Dog : public Animal {
public:
    Dog() { cout << "Dog()\n"; }
};

class Cat : public Animal {
public:
    Cat() { cout << "Cat()\n"; }
};

class Tiger : public Dog, public Cat {
public:
    Tiger() { cout << "Tiger()\n"; }
};

int main() {
    Tiger t;
}

运行结果

Animal()
Dog()
Animal()
Cat()
Tiger()

加上虚继承

#include <iostream>
using namespace std;

class Animal {
public:
    Animal() { cout << "Animal()\n"; }
};

class Dog : virtual public Animal {
public:
    Dog() { cout << "Dog()\n"; }
};

class Cat : virtual public Animal {
public:
    Cat() { cout << "Cat()\n"; }
};

class Tiger : public Dog, public Cat {
public:
    Tiger() { cout << "Tiger()\n"; }
};

int main() {
    Tiger t;
}

运行结果

Animal()
Dog()
Cat()
Tiger()

这样上诉说所的“路径上的基类 Animal 都指向同一份对象。”就很清晰了。

2.1.6、继承与组合(composition)的区别 —— “is-a” vs “has-a”

概念对比

特性继承(Inheritance)组合(Composition)
关系描述“is-a”“has-a”
对象关系子类 父类的一种包含 另一个类作为成员
访问父类可继承父类的成员、方法只能通过成员对象访问
多态支持✅ 可使用虚函数实现多态❌ 不直接支持,需要间接封装接口
适用场景类型扩展,接口共享拥有关系,功能复用、组合对象

继承 = is-a
派生类继承基类

class Animal {
public:
    virtual void Speak() { std::cout << "Animal sound\n"; }
};

class Dog : public Animal { // Dog 是 Animal
public:
    void Speak() override { std::cout << "Woof!\n"; }
};

✅ Dog is-an Animal

可以使用基类指针或引用指向它:Animal* a = new Dog();

多态特性生效

组合 = has-a
一个类拥有另一个类作为成员,例如:

class Engine {
public:
    void Start() { std::cout << "Engine starts\n"; }
};

class Car {    // Car 有 Engine
private:
    Engine engine;  // 组合
public:
    void Start() { engine.Start(); }
};

3、封装

3.1、什么是封装

封装就是: 把数据(成员变量)和操作数据的函数(成员函数)绑定在一起,并控制外部访问权限。

换句话说,封装就是“把对象的内部实现隐藏起来,只暴露必要的接口”,外界只能通过接口访问数据,而不能直接修改内部状态。

封装的实现方式

修饰符类内访问派生类访问类外访问
private
protected
public

演示demo

#include <iostream>
using namespace std;

class Animal {
public:
    void SetAnimalType(const std::string &type)
    {
        mAnimalType = type;
    }
    std::string GetAnimalType()
    {
        return mAnimalType;
    }

private:
    std::string mAnimalType;
};


int main()
{
    Animal a;
    a.SetAnimalType("cat");
    std::cout<<"Animal type is : "<<a.GetAnimalType()<<std::endl;
    return 0;
}

外部通过SetAnimalTypeGetAnimalType接口操作mAnimalType。不直接操作mAnimalType,这就是封装。

封装的优点

1、数据隐藏:防止外部直接操作内部数据,避免错误或不安全的修改。

2、接口控制:只暴露必要的接口,其他实现细节隐藏,提高模块化。

3、易于维护:内部实现可修改而不影响外部代码。

4、增强安全性:可以在接口中加入校验逻辑,保证对象状态合法。

结语

感谢您的阅读,如有问题可以私信或评论区交流。
^ _ ^

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值