C++复习二,继承与多态

🚀 C++ 继承与多态完全指南:从继承本质到虚函数表底层揭秘

📌 阅读提示 :本文系统梳理 C++ 两大核心面向对象特性------继承多态,从语法入门到编译器底层原理,搭配 30+ 代码实例与易错点提醒,适合正在学习或复习 C++ 面向对象的同学食用~


📖 文章目录


一、继承的本质与基本使用

🎯 什么是继承?

当多个类之间存在重复的成员变量或成员函数时,我们可以把这些公共部分抽取到一个父类(基类)中,然后让其他类继承它,从而实现代码复用。

💡 继承的核心思想:子类(派生类)拥有父类(基类)的成员,并可以在此基础上扩展自己的独特成员。

📝 基础示例

cpp 复制代码
#include <iostream>
using namespace std;

class Person {
public:
    Person(string d = "dsd")
        : _name(d) { }
    string _name;
};

class Student : public Person {
public:
    Student(int a = 10, string s = "jsf")
        : _num(a)
        , Person(s) { }
    int _num;
};

class Teacher : public Person {
public:
    Teacher(int b = 20, string s = "djaq")
        : _id(b)
        , Person(s) { }
    int _id;
};

int main() {
    // Teacher 和 Student 都共用 Person 这个类,实现了代码的简化
    Teacher a(20, "yinli");
    Student b(30, "xiaoliao");
    cout << a._name << ' ' << a._id << endl;
    cout << b._name << ' ' << b._num << endl;
    return 0;
}

⚠️ 易错点:子类构造函数必须在初始化列表中显式调用父类构造函数(如果父类没有默认构造函数的话),否则编译报错!


二、继承方式的访问权限规则

🔑 三种继承方式与三种访问限定符

继承方式 父类 public 成员 父类 protected 成员 父类 private 成员
public 继承 子类中为 public 子类中为 protected 子类中不可见
protected 继承 子类中为 protected 子类中为 protected 子类中不可见
private 继承 子类中为 private 子类中为 private 子类中不可见

📌 核心规则

  1. 继承方式的符号与访问限定符关键字相同public / protected / private
  2. 父类的 private 成员在子类中完全不可见(但确实被继承到了子类内存中)
  3. protected 是为继承而生的------类外不可访问,但子类内部可以访问
cpp 复制代码
struct A {
    int _a;           // public
protected:
    int _name;        // 子类可访问,类外不可
private:
    int _score;       // 子类也不可访问
};

struct B : public A {
    void func() {
        _name = 20;   // ✅ A 中的 protected 成员可以访问
        // _score = 10;  // ❌ A 中的 private 成员不可以访问
    }
};

int main() {
    B a;
    a._a = 20;        // ✅ public 成员类外可访问
    // a._name = 30;  // ❌ protected 类外不可访问
    // a._score = 30; // ❌ private 类外不可访问
    return 0;
}

🔢 子类访问权限计算公式

复制代码
子类中成员的访问权限 = min(父类成员的访问限定符, 继承方式)

例如:

  • protected 成员 + public 继承 → 子类中为 protected
  • protected 成员 + private 继承 → 子类中为 private
  • private 成员 + public 继承 → 子类中不可见

⚠️ 易错点

  • class 的默认继承方式是 privatestruct 的默认继承方式是 public
  • 日常开发几乎总是使用 public 继承

三、类模板的继承

模板类也可以被继承,但调用父类模板中的函数时需要指定作用域

cpp 复制代码
#include <vector>

namespace gal {
    template<class T>
    class Stack : vector<T> {
    public:
        void push(const T& x) {
            vector<T>::push_back(x);  // 必须指定作用域
        }
    };
}

int main() {
    gal::Stack<int> st;
    st.push(4);
    return 0;
}

💡 原理 :模板中的函数在实例化前不存在,所以编译器无法自动找到。当创建 Stack<int> 变量时,T 被确定为 int,此时 vector<T>::push_back 才被实例化。
⚠️ 易错点 :模板继承中调用父类的模板成员函数,必须用 父类<T>::函数名 的方式指定作用域,否则编译报错!


四、父类与子类的赋值兼容转换(切片)

✂️ 什么是切片?

子类对象可以赋值给父类对象 (包括指针和引用),反之不行------这个过程叫做切片(Slicing)

cpp 复制代码
struct A {
    A(int a = 30, int b = 90)
        : _a(a), _b(b) { }
    int _a = 20;
    int _b = 30;
};

struct B : A {
    B(int a = 50, int b = 70)
        : _c(a), A(a, b) { }
    int _c = 40;
};

int main() {
    B b(60, 70);
    A a(40, 60);
    
    a = b;                 // ✅ 子类对象赋值给父类对象(切片)
    cout << a._a << ' ' << a._b << endl;  // 输出: 60 70
    
    A* p = &b;             // ✅ 子类地址给父类指针
    A& q = b;              // ✅ 子类引用给父类引用
    
    return 0;
}

📐 本质b 中属于 A 的那块内存被"切"下来给 a 使用,因此也叫切片。

🔄 反向转换

父类的指针/引用可以通过强制类型转换转回子类类型(前提是该指针原本就指向子类对象):

cpp 复制代码
A* p = new B;            // 父类指针指向子类对象
B* bp = (B*)p;           // 强制转回子类指针(不安全,建议用 dynamic_cast)

⚠️ 易错点 :只能转指针/引用,不能值拷贝 !另外这种强转是不安全的,实际项目中推荐使用 dynamic_cast 并检查结果。


五、继承中的作用域与隐藏规则

🔍 核心规则

子类和父类是不同的作用域 。当子类与父类有同名变量或函数时,父类的会被隐藏(Hide)

cpp 复制代码
struct A {
    int _a = 30;
};

struct B : A {
    int _a = 40;
};

int main() {
    B a;
    cout << a._a << endl;        // 输出: 40(B 中的 _a)
    cout << a.A::_a << endl;     // 输出: 30(指定作用域访问 A 中的 _a)
}

🔥 经典易错题

cpp 复制代码
class A {
public:
    void fun() {
        cout << "func()" << endl;
    }
};

class B : public A {
public:
    void fun(int i) {
        cout << "func(int i)" << i << endl;
    }
};

int main() {
    B b;
    b.fun(10);   // ✅ 调用 B::fun(int)
    b.fun();     // ❌ 编译报错!A::fun() 被隐藏了
    return 0;
}

⚠️ 易错点 :A 和 B 中的 fun 不构成重载 (因为是两个作用域),而是隐藏关系b.fun() 找不到匹配的函数所以报错。要调用 A 的 fun 必须写成 b.A::fun()


六、子类默认成员函数的生成规则

1️⃣ 默认构造函数

子类的默认构造函数会自动做三件事:

  1. 对内置类型成员不进行明确初始化(随机值)
  2. 对类类型成员自动调用其默认构造函数
  3. 将父类视为整体,调用其默认构造函数

2️⃣ 显式构造函数

cpp 复制代码
struct Father {
    Father(string a)
        : _name(a) { }
    string _name;
};

struct Child : Father {
    Child(string s, int a, string h)
        : Father(s)     // 显式调用父类构造函数(必须放初始化列表)
        , _score(a)
        , _gal(h) { }
    int _score;
    string _gal;
};

int main() {
    Child c("galgame", 10, "love");
    cout << c._gal << c._name << c._score << endl;
    return 0;
}

⚠️ 易错点 :如果父类没有默认构造函数,子类必须在初始化列表中显式调用父类构造函数,否则编译报错!

3️⃣ 拷贝构造与赋值重载

cpp 复制代码
struct Person {
    Person(string s) : _s(s) { }
    string _s;
};

struct Student : Person {
    Student(string s1, string s2, int n)
        : Person(s1), _s1(s2), _num(n) { }
    
    // 拷贝构造函数
    Student(const Student& x)
        : Person(x)          // 利用切片,子类对象可传给父类引用
        , _num(x._num)
        , _s1(x._s1) { }
    
    // 赋值运算符重载
    Student& operator=(const Student& x) {
        if (this != &x) {
            Person::operator=(x);  // ⚠️ 必须指定作用域!因为 operator= 被隐藏了
            _num = x._num;
            _s1 = x._s1;
        }
        return *this;
    }
    
    int _num;
    string _s1;
};

⚠️ 易错点 :赋值运算符重载中,调用父类的 operator= 必须写 Person::operator=(x),因为子类的 operator= 隐藏了父类的 operator=

4️⃣ 析构函数

所有类的析构函数名都会被编译器处理成 destructor(),因此子类和父类的析构函数也构成隐藏关系

正常情况下不需要手动写析构------编译器生成的析构函数会自动调用父类和类类型成员的析构。

⚠️ 重要警告 :无论你在显式析构函数中是否调用了父类的析构,编译器在析构函数结束前都会自动再调用一次父类和类类型成员的析构 。因此绝对不要在析构函数中手动调用父类析构,否则会 double free!


七、继承中的三个重要知识点

🔒 1. 禁止父类被继承的方式

方式一:将父类的构造函数私有化

cpp 复制代码
struct A {
private:
    A() { }         // 构造函数私有
    int _a = 30;
};

struct B : A {      // 创建 B 变量时会报错(无法调用 A 的构造)
    int _b = 80;
};
// 不创建 B 的变量就不会报错,但失去了实际意义

方式二 :使用 final 关键字(C++11)

cpp 复制代码
struct A final {     // A 不能被任何类继承
    A() { }
    int _a = 30;
};

struct B : A {       // ❌ 编译报错!无论是否创建 B 的变量
    int _b = 80;
};

💡 建议 :用 final 更直观、更安全。

🤝 2. 友元关系不能被继承

cpp 复制代码
struct A {
    friend void func();
private:
    int _a = 20;
    int _b = 40;
};

struct B : A {
private:
    int _c = 30;
};

void func() {
    A a; B c;
    cout << a._a << ' ' << a._b << endl;  // ✅ func 是 A 的友元
    // cout << c._c << endl;              // ❌ func 不是 B 的友元!
}

⚠️ 易错点:"你爸的朋友不一定是你的朋友"------友元关系只对声明它的类有效,子类不继承。

📊 3. 父类中的 static 成员:子类与父类共用

cpp 复制代码
struct A {
    static int a;
    int b = 40;
};

struct B : A {
    int b = 30;
};

int A::a = 10;

int main() {
    A a;
    B b;
    
    cout << A::a << endl;    // 10
    cout << B::a << endl;    // 10 (同一个值)
    cout << &a.a << endl;    // 0x...
    cout << &b.a << endl;    // 0x... (同一个地址!)
    
    // 但非 static 成员是各自独立的
    cout << &a.b << endl;    // 不同地址
    cout << &b.b << endl;    // 不同地址
    return 0;
}

💡 结论static 成员在继承体系中是共用 的(同一个变量),而非 static 成员是各自独立的。


八、单继承与多继承(含菱形继承)

📐 基本概念

cpp 复制代码
struct Person {
    string s;
};

struct Teacher {
    string h;
};

// 单继承:一个子类只有一个直接父类
struct Student : Person {
    string d;
};

// 多层单继承
struct Man : Student {
    string l;
};

// 多继承:一个子类有多个直接父类
struct Assistant : public Person, public Teacher {
    string c;
};

💎 菱形继承问题

复制代码
        Person
       /      \
   Student   Teacher
       \      /
      Assistant

两个经典问题

问题 说明
数据冗余 Person 的数据在 Assistant 中有两份
二义性 调用 Person 的成员时不明确是哪一份
cpp 复制代码
struct Person {
    string _name = "fs";
};

struct Student : Person {
    string _big = "op";
};

struct Teacher : Person {
    string _small = "jj";
};

struct Assistant : Student, Teacher {
    string _galgame = "oo";
};

int main() {
    Assistant a;
    // cout << a._name << endl;  // ❌ 二义性!不明确是哪个 _name
    cout << a.Student::_name << endl;  // 指定作用域
    cout << a.Teacher::_name << endl;  // 指定作用域
    return 0;
}

🔧 解决方案:虚继承 virtual

cpp 复制代码
struct Student : virtual public Person { };
struct Teacher : virtual public Person { };
struct Assistant : Student, Teacher { };
// 此时 Student 和 Teacher 共用同一份 Person

⚠️ 易错点

  • 虚继承两边都要写 virtual,否则另一边的 virtual 无效
  • Assistant 构造时直接调用 Person 的构造函数 ,而 StudentTeacher 中对 Person 的构造调用会被编译器跳过
  • 实际开发中尽量避免设计出菱形继承

📏 多继承下的指针偏移问题

cpp 复制代码
class Base1 { public: int _b1; };
class Base2 { public: int _b2; };
class Derive : public Base1, public Base2 { public: int _d; };

int main() {
    Derive d;
    Base1* p1 = &d;
    Base2* p2 = &d;
    Derive* p3 = &d;
    // 结果:p1 == p3 != p2
    // Base2 的地址比 Base1 的地址多了一个 Base1 大小(指针偏移)
    return 0;
}

🧠 原理 :多继承时,不同父类在子类内存中的位置不同,p2 实际上指向的是 dBase2 部分的起始地址。


九、继承 vs 组合:is-a 与 has-a

cpp 复制代码
// 继承(is-a):Stack "是"一种 List
struct Stack : public List { };

// 组合(has-a):Stack "有"一个 List
struct Stack {
    List it;
};
特性 继承(is-a) 组合(has-a)
关系 "是"的关系 "有"的关系
耦合度 较高 较低
白盒/黑盒 白盒复用(可见父类实现) 黑盒复用(只使用接口)
优先度 次要选择 优先选择

💡 设计原则能使用组合就优先使用组合,只有在确实存在"is-a"关系时才使用继承。


十、多态的基本概念

🎭 什么是多态?

多态就是一个事物有多种形态。C++ 中分为:

类型 时机 例子
静态多态 编译时 函数重载、函数模板
动态多态 运行时 虚函数 + 父类指针/引用调用
cpp 复制代码
// 静态多态:编译时确定调用哪个函数
void print(int x)    { cout << "int: " << x << endl; }
void print(double x) { cout << "double: " << x << endl; }

// 动态多态:运行时确定调用哪个函数(后面详细讲)

十一、虚函数与多态的实现条件

✅ 实现动态多态的两个条件

  1. 必须是父类的指针或引用去调用虚函数
  2. 父类中必须有虚函数,且子类对该虚函数完成重写(覆盖)

📝 重写(覆盖)的定义

子类和父类分别有一个返回值类型、函数名、参数列表完全相同的虚函数,子类的虚函数才能实现对父类虚函数的覆盖。

cpp 复制代码
struct A {
    virtual void func() {
        cout << "A" << endl;
    }
};

struct B : A {
    virtual void func() {   // 重写 A 的 func
        cout << "B" << endl;
    }
};

struct C : A {
    virtual void func() {   // 重写 A 的 func
        cout << "C" << endl;
    }
};

// 必须是父类的指针
void put(A* a) {
    a->func();              // 调用的是传入对象实际类型的 func
}

int main() {
    B b;
    C c;
    put(&b);   // 输出: B
    put(&c);   // 输出: C
    return 0;
}

⚠️ 注意 :子类的虚函数可以不加 virtual(但不规范,不推荐)。原因后文虚函数表部分会解释。

🔥 进阶习题:缺省值与多态

cpp 复制代码
class A {
public:
    virtual void func(int val = 1) { cout << "A->" << val << endl; }
    virtual void test() { func(); }
};

class B : public A {
public:
    void func(int val = 0) { cout << "B->" << val << endl; }
};

int main() {
    A* p = new B;
    p->test();    // 输出: B->1  ← 注意缺省值是 1 不是 0!
    return 0;
}

🧠 解析

  1. p->test() 调用 A::test(),其 this 指针是 A* 类型
  2. test() 内部调用 func(),由于 func 是虚函数且 this 实际指向 B 对象,触发多态
  3. 重写的本质是父类将虚函数的声明(包括缺省值)"复制"到子类------缺省值不会被重写覆盖
  4. 因此调用 B::func 时缺省值仍是父类的 1,输出 B->1
    ⚠️ 易错点缺省值在编译时绑定 ,取决于调用者的静态类型,而非实际对象类型!

十二、协变(Covariant)

协变允许重写时返回值类型不同 ------但必须是返回本类或子类的指针/引用

cpp 复制代码
class A { };
class B : public A { };

class Person {
public:
    virtual A* BuyTicket() {
        cout << "买票-全价" << endl;
        return nullptr;
    }
};

class Student : public Person {
public:
    virtual B* BuyTicket() {    // 返回值 B* 是 A* 的子类 → 构成协变
        cout << "买票-打折" << endl;
        return nullptr;
    }
};

void Func(Person* ptr) {
    ptr->BuyTicket();
}

int main() {
    Person ps;
    Student st;
    Func(&ps);   // 输出: 买票-全价
    Func(&st);   // 输出: 买票-打折
    return 0;
}

十三、析构函数的重写

❓ 为什么要让析构函数成为虚函数?

cpp 复制代码
struct A {
    // ~A() { }               // ❌ 不加 virtual 会出问题!
    virtual ~A() {            // ✅ 加上 virtual
        cout << "~A()" << endl;
    }
};

struct B : A {
    virtual ~B() {
        cout << "~B()" << endl;
    }
};

int main() {
    A* a = new A;
    A* b = new B;    // 父类指针指向子类对象
    delete a;        // 输出: ~A()
    delete b;        // 输出: ~B() 换行 ~A()  ← 正确调用了子类析构
    return 0;
}

🧠 原理 :析构函数的本质是在外层包了一层 destructor 函数。当父类析构为虚函数,delete 时会构成多态调用,正确调到子类的析构函数。
⚠️ 重要规则只要一个类有可能作为父类被继承,就应该将析构函数声明为虚函数!


十四、override 与 final 关键字

override:检查是否正确重写

cpp 复制代码
class Car {
public:
    virtual void Drive() { }
};

class Benz : public Car {
public:
    virtual void Drive() override {    // 拼写错误!父类是 Dirve,不是 Drive
        cout << "Benz-舒适" << endl;   // ❌ 编译报错:没有重写任何虚函数
    }
};

🔒 final:禁止虚函数被进一步重写

cpp 复制代码
class Car {
public:
    virtual void Drive() final { }     // 该虚函数不能被任何子类重写
};

class Benz : public Car {
public:
    virtual void Drive() {             // ❌ 编译报错!
        cout << "Benz-舒适" << endl;
    }
};

十五、重载、重写、隐藏三者辨析

特性 重载(Overload) 重写(Override) 隐藏(Hide)
作用域 同一个作用域 父类和子类之间 父类和子类之间
函数名 相同 相同 相同
参数 不同 必须相同 任意
返回值 可同可不同 必须相同(协变除外) 任意
virtual 任意 必须是虚函数 不构成重写时即为隐藏
关系 并列关系 覆盖关系 遮蔽关系

💡 判断口诀

  • 同一作用域 + 同名 + 不同参数 → 重载
  • 父子类 + 同名 + 同参数 + virtual → 重写
  • 父子类 + 同名 + 不构成重写 → 隐藏(包括同名成员变量)

十六、纯虚函数与抽象类

📌 定义

cpp 复制代码
virtual void func() = 0;    // 纯虚函数:只声明,不实现

有纯虚函数的类叫抽象类 ,抽象类不能实例化对象

cpp 复制代码
class Car {
public:
    virtual void Drive() = 0;    // 纯虚函数
};

class Benz : public Car {
    // 继承了纯虚函数但没有重写 → Benz 仍是抽象类
};

class BMW : public Car {
public:
    virtual void Drive() {       // 重写纯虚函数
        cout << "BMW-操控" << endl;
    }
};

int main() {
    // Car car;      // ❌ 编译报错:无法实例化抽象类
    // Benz benz;    // ❌ 编译报错:无法实例化抽象类
    
    Car* pBMW = new BMW;    // ✅ 抽象类可以声明指针
    pBMW->Drive();          // 输出: BMW-操控
    return 0;
}

💡 设计意义 :强制子类必须实现特定接口,类似于 Java 中的 interface


十七、多态的底层原理:虚函数表

📊 虚函数表(Virtual Table / vtable)

每个包含虚函数的类都有一个虚函数表指针(_vfptr ,指向一个函数指针数组,数组中存储了该类所有虚函数的地址。

cpp 复制代码
class Base {
public:
    virtual void Func1() {
        cout << "Func1()" << endl;
    }
protected:
    int _b = 1;
    char _ch = 'x';
};

int main() {
    Base b;
    cout << sizeof(b) << endl;   // 输出: 12(32位下)
    // _vfptr(4) + _b(4) + _ch(1) + 对齐(3) = 12
    return 0;
}

🔍 多态调用的底层流程

cpp 复制代码
struct A {
    virtual void func() { cout << "A" << endl; }
    int _a;
};

struct B : A {
    virtual void func() { cout << "B::A" << endl; }
    int _b;
};

void fun(A* _a) {
    _a->func();    // 1. 判断 func 是虚函数
                   // 2. 通过 _a 找到对象的 _vfptr
                   // 3. 在虚函数表中查找 func 的地址
                   // 4. 调用该地址的函数
}

📐 虚函数调用 vs 常规函数调用的汇编区别

复制代码
虚函数调用:
    mov  eax, [ptr]       ; 取虚表指针
    mov  edx, [eax]       ; 取虚表中函数地址
    call edx              ; 间接调用(运行时确定地址)

常规函数调用:
    call 0x00401000       ; 直接调用(编译时确定地址)

🧠 关键区别 :虚函数是 call 一个寄存器中的地址(间接调用),常规函数是直接 call 一个编译时确定的地址。


十八、动态绑定与静态绑定

绑定类型 时机 条件 方式
静态绑定 编译时 不满足多态条件 直接 call 函数地址
动态绑定 运行时 满足多态条件 通过虚函数表间接 call
cpp 复制代码
// 静态绑定示例
B b;
b.func();        // 编译时就确定调用 B::func 的地址

// 动态绑定示例
A* p = new B;
p->func();       // 运行时通过虚表查找 func 的地址

十九、虚函数表进阶知识点

📋 知识点汇总

  1. 同类型的虚函数表相同 ------同类型变量的 _vfptr 指向同一个虚表
  2. 不同类型的虚表相互独立
  3. 子类虚表的生成过程
    • ① 拷贝父类的虚函数表
    • ② 将自身重写的虚函数地址覆盖对应位置
    • ③ 追加自身新增的虚函数地址
  4. 虚函数表存储在常量区
cpp 复制代码
int main() {
    int i = 0;
    static int j = 1;
    int* p1 = new int;
    const char* p2 = "xxxxxxxx";
    
    printf("栈:     %p\n", &i);
    printf("静态区: %p\n", &j);
    printf("堆:     %p\n", p1);
    printf("常量区: %p\n", p2);
    
    Base b;
    Derive d;
    printf("Base虚表地址:   %p\n", *(int*)&b);
    printf("Derive虚表地址: %p\n", *(int*)&d);
    // 虚表地址和常量区地址接近,说明虚表在常量区
    
    return 0;
}
  1. virtual 只能在成员函数声明处加,类外实现时不能加
  2. 静态成员函数不能是虚函数
cpp 复制代码
A* a = new B;
a->func();     // ✅ 有 this 指针,能通过 this 找到虚表

A::func();     // ❌ 静态函数没有 this 指针,无法找到虚表
  1. 多继承时可能有多个虚表,但只有第一个虚表能正常使用,新增虚函数地址存在第一个虚表末尾

易错点 :模板是编译时多态 (静态),虚函数是运行时多态(动态),二者原理完全不同。


二十、总结与学习建议

🗺️ 知识图谱

复制代码
C++ 面向对象三大特性
│
├── 继承(Inheritance)
│   ├── 基本概念与语法
│   ├── 访问权限规则
│   ├── 切片(赋值兼容)
│   ├── 作用域与隐藏
│   ├── 默认成员函数
│   ├── 单/多/菱形继承
│   └── 继承 vs 组合
│
└── 多态(Polymorphism)
    ├── 静态多态(重载/模板)
    ├── 动态多态(虚函数)
    │   ├── 实现条件
    │   ├── 重写/协变
    │   ├── override/final
    │   ├── 纯虚函数/抽象类
    │   └── 析构函数重写
    └── 底层原理
        ├── 虚函数表(vtable)
        ├── 动态绑定 vs 静态绑定
        └── 虚表的内存分布

核心要点回顾

主题 核心规则
访问权限 子类权限 = min(父类权限, 继承方式)
切片 子类 → 父类(值/指针/引用),反之需强转
隐藏 父子类同名即隐藏,需指定作用域
菱形继承 virtual 虚继承解决二义性
多态条件 父类指针/引用 + 虚函数重写
虚函数表 每个有虚函数的类一个虚表,存储在常量区
缺省值 编译时绑定到静态类型,不参与多态
设计原则 优先组合,析构函数建议加 virtual

写在最后:继承和多态是 C++ 面向对象的灵魂,理解它们不仅需要记住语法,更需要理解背后的内存模型和编译器行为。希望这篇指南能帮你建立起完整的知识体系~

相关推荐
小小de风呀1 小时前
de风——【从零开始学C++】(十一):list的基本使用和模拟实现
开发语言·c++·list
陌路201 小时前
C++高级进阶--夯实进阶基础(1)
开发语言·c++
郝学胜-神的一滴3 小时前
中级OpenGL教程 008:精准控制高光光斑大小与强度
c++·unity·godot·three.js·图形学·opengl·unreal
牢姐与蒯3 小时前
c++数据结构之c++11(一)
数据结构·c++
折戟不必沉沙3 小时前
构造和析构函数能否是虚函数?能否调用虚函数?
c++
-To be number.wan4 小时前
算法日记 | STL- sort排序
c++·算法
不想写代码的星星4 小时前
编译期策略模式:当模板成为策略容器
c++
啦啦啦啦啦zzzz4 小时前
数据结构:平衡二叉树
数据结构·c++·二叉树
玖釉-4 小时前
Vulkan 中 Shader 的 vert、frag、mesh、comp 全面解析:作用、关系、特点与工程实践
开发语言·c++·windows·算法·图形渲染