多态
概述
- 多态性(
Polymorphism
)是面向对象编程的一个核心概念,它允许同一个接口调用在不同对象上执行不同的操作 - 在
C++
中,多态性主要通过继承和虚函数实现 - 多态性使代码更具灵活性和可扩展性
- 因为你可以编写更通用的代码,依赖于抽象基类而不是具体的派生类
多态的类型
- 编译时多态(静态多态):
- 通过函数重载和运算符重载实现
- 运行时多态(动态多态):
- 通过继承和虚函数实现
编译时多态
- 编译时多态性在编译阶段就已经确定
- 主要通过函数重载和运算符重载来实现
- 函数重载
- 同一作用域内可以有多个同名函数,只要它们的参数列表不同
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
#include <iostream> void print(int i) { std::cout << "Integer: " << i << std::endl; } void print(double d) { std::cout << "Double: " << d << std::endl; } int main() { print(5); // 调用 void print(int i) print(3.14); // 调用 void print(double d) return 0; } |
- 运算符重载
- 可以为类自定义运算符行为
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
#include <iostream> class Complex { public: double real, imag; Complex(double r, double i) : real(r), imag(i) {} Complex operator+(const Complex& other) const { return Complex(real + other.real, imag + other.imag); } }; int main() { Complex c1(1.0, 2.0); Complex c2(3.0, 4.0); Complex c3 = c1 + c2; // 使用重载的运算符+ std::cout << "c3: " << c3.real << " + " << c3.imag << "i" << std::endl; return 0; } |
运行时多态
- 运行时多态性在程序运行时通过虚函数表(
vtable
)实现- 基类定义虚函数,派生类可以重写这些虚函数
- 通过基类指针或引用调用虚函数时,根据实际对象类型调用相应的派生类实现
- 总结来说:
- 运行时多态是通过虚函数和动态绑定来实现的
虚函数
- 虚函数允许在基类中定义接口,由派生类提供具体实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
#include <iostream> class Base { public: virtual void show() const { std::cout << "Base show()" << std::endl; } virtual ~Base() {} }; class Derived : public Base { public: void show() const override { std::cout << "Derived show()" << std::endl; } }; void display(const Base& obj) { obj.show(); // 动态绑定,调用实际对象的 show() } int main() { Base base; Derived derived; display(base); // 调用 Base::show() display(derived); // 调用 Derived::show() return 0; } |
虚函数表
- 虚函数表(
vtable
)是编译器维护的一种数据结构,用于实现运行时多态 - 每个包含虚函数的类都有一个虚函数表,其中存储了虚函数的地址
- 对象的虚函数指针(
vptr
)指向这个表 - 通过基类指针调用虚函数时,程序会通过
vptr
查找实际函数地址并调用
- 对象的虚函数指针(
- 在
C++
中,每个包含虚函数的类在实例化对象时,编译器会在对象的构造函数中插入代码来初始化虚函数指针(vptr
)- 虚函数指针指向虚函数表(
vtable
),虚函数表中包含了该类的虚函数的地址 - 当通过基类指针或引用调用虚函数时,程序会通过
vptr
找到对应的vtable
,并从中获取实际要调用的函数地址,从而实现动态多态
- 虚函数指针指向虚函数表(
- 总结来说:
- 编译器会在编译期为每一个有虚函数的类确定一个虚函数表
- 类里面的虚函数的地址会按声明顺序存到这个虚函数表里
- 然后在运行时,在对象构造的时候,会动态得将
vptr
指向这个类对象动态类型对应的虚函数表的地址
虚函数与虚函数表示例
- 示例代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
#include <iostream> class Base { public: virtual void show() { std::cout << "Base show()" << std::endl; } virtual ~Base() {} }; class Derived : public Base { public: void show() override { std::cout << "Derived show()" << std::endl; } }; int main() { Base* b = new Derived(); b->show(); // 调用的是 Derived::show() delete b; return 0; } |
- Base类的虚函数表:
1 2 3 |
Base_vtable: .quad Base::show .quad Base::~Base |
- Derived类的虚函数表:
1 2 3 |
Derived_vtable: .quad Derived::show .quad Derived::~Derived |
- 在构造函数中初始化
vptr
1 2 3 4 5 6 7 |
Base::Base() { this->vptr = &Base_vtable; } Derived::Derived() { this->vptr = &Derived_vtable; } |
- 虚函数调用:
- 通过对象
b
的vptr找到虚函数表(vtable
) - 在虚函数表中查找
show
函数的地址 - 调用虚函数表中
show
函数的地址,实际调用Derived::show()
- 通过对象
虚函数表原理
- 内存布局和虚函数表(
vtable
)- 每个包含虚函数的类都有一个虚函数表(
vtable
),这个表中存储了该类的所有虚函数的地址 - 虚函数表是在编译期确定的
- 每个对象都有一个隐藏的指针,称为虚函数指针(
vptr
),指向这个虚函数表 - 这个指针
vptr
的初始化是在执行对象的构造函数时,编译器插入了代码进行初始化的,让它执行本类的虚函数表 - 而类的所有对象共用一张虚函数表
- 每个包含虚函数的类都有一个虚函数表(
- 当你使用派生类对象初始化一个基类指针时,会发生隐式向上转型(
implicit upcasting
)- 这意味着派生类对象被视为基类对象,并且基类指针只能访问基类中定义的成员
- 对象内存布局:
- 对于一个包含虚函数的类,其对象内存布局中通常包含以下部分:
- 虚函数指针(
vptr
):指向该对象所属类的虚函数表(vtable
) - 类的其他成员变量
i
,j
1 2 3 |
vptr i j |
- 生隐式向上转型
- 即使一个派生类对象被隐式向上转型为基类指针,指针本身仍然指向的是派生类对象
- 在派生类对象的内存布局中,虚函数指针(
vptr
)仍然指向派生类的虚函数表(vtable
) - 因此,通过基类指针调用虚函数时,程序会使用对象的虚函数指针,找到派生类的虚函数表,从而调用派生类的虚函数实现
虚函数指针
- 虚函数指针(
vptr
)的位置:- 虚函数指针(
vptr
)通常是对象内存布局中的第一个成员,指向该对象所属类的虚函数表(vtable
)
- 虚函数指针(
- 虚函数表(
vtable
)的位置:- 虚函数表(
vtable
)是一个类级别的数据结构,不是对象的一部分 - 每个包含虚函数的类有一个虚函数表,存储在全局或静态存储区
- 虚函数表(
type_info
type_info
是C++
标准库中的一个类,用于在运行时描述类型信息- 它是
RTTI
(Run-Time Type Information
,运行时类型信息)的一部分,主要用于在运行时进行类型识别和检查 type_info
对象通常与typeid
运算符一起使用,typeid
运算符用于获取对象或类型的type_info
type_info
对象通常由编译器生成,并在程序的全局静态存储区中维护- 每个类型对应一个唯一的
type_info
对象 type_info
对象是与类关联的,而不是与类的实例关联的
也就是说,每个类型的实例化对象的type_info
数据是一样的- 编译器确保每种类型在程序中只存在一个
type_info
对象
- 每个类型对应一个唯一的
- 使用场景
- 动态类型识别:在多态场景中,通过基类指针或引用获取实际对象的类型
- 异常处理:在捕获异常时,通过
typeid
获取异常对象的类型信息,进行特定类型的异常处理 - 类型比较和排序:在泛型编程中,可以使用
type_info
进行类型比较和排序,确保模板参数类型符合预期
type_info
是否在虚函数的调用过程中起到作用?- 在通过基类指针调用虚函数的过程中,
type_info
本身并不起直接作用 - 虚函数调用依赖于虚函数表(
vtable
)和虚函数指针(vptr
),而不是type_info
- 在通过基类指针调用虚函数的过程中,
纯虚函数和抽象类
- 抽象类是不能实例化的类,通常用于定义接口
- 包含一个或多个纯虚函数的类就是抽象类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
#include <iostream> class Shape { public: virtual void draw() const = 0; // 纯虚函数 virtual ~Shape() {} }; class Circle : public Shape { public: void draw() const override { std::cout << "Drawing Circle" << std::endl; } }; class Square : public Shape { public: void draw() const override { std::cout << "Drawing Square" << std::endl; } }; int main() { Circle circle; Square square; Shape* shape1 = &circle; Shape* shape2 = □ shape1->draw(); // 调用 Circle::draw() shape2->draw(); // 调用 Square::draw() return 0; } |
动态绑定
- 总结就是:
- 通过基类的指针或引用去调用和派生类同名的函数时,根据基类指针或引用所指对象的动态类型来确定
vptr
指向哪个虚函数表,然后根据偏移量在表中拿到了目标函数地址
- 通过基类的指针或引用去调用和派生类同名的函数时,根据基类指针或引用所指对象的动态类型来确定
静态类型
- 变量在声明时的类型(编译时已知)
- 编译阶段
- 如
Base* ptr = new Derived;
中Base*
- 静态绑定(如非虚函数调用、默认参数选择)
- 若基类和派生类的虚函数有不同默认参数,调用时由静态类型决定参数值。例如:
- 这就是所谓的静态类型决定默认参数:
1 2 3 4 5 |
class Base { virtual void func(int x = 1) { ... } }; class Derived : public Base { void func(int x = 2) override { ... } }; Base* ptr = new Derived; ptr->func(); // 使用 Base 的默认参数 x=1,但调用 Derived 的 func 实现 [3](@ref) |
动态类型
- 对象实际指向的类型(运行时确定)
- 运行阶段
- 如
ptr
实际指向Derived
对象 - 动态绑定(虚函数调用、
dynamic_cast
等
- 虚函数调用根据动态类型选择派生类实现,例如:
- 这就是所谓的动态类型决定函数实现:
1 2 |
Shape* shape = new Circle; shape->draw(); // 调用 Circle::draw(),而非 Shape::draw() [3,5](@ref) |
向上转型
-
向上转型(
upcasting
)是将派生类的指针或引用隐式地转换为基类的指针或引用的过程- 这在
C++
中是安全的,因为派生类对象包含了基类的所有成员和行为 - 不会发送截断现象
- 因此,基类指针或引用可以访问基类的成员函数和数据成员,但不能访问派生类特有的成员
- 这在
-
原理:
- 指针/引用仅“看待”对象为基类部分,但实际对象仍是完整的派生类对象
-
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
#include <iostream> class Base { public: void baseFunction() { std::cout << "Base function" << std::endl; } virtual void show() { std::cout << "Base show()" << std::endl; } virtual ~Base() {} }; class Derived : public Base { public: void derivedFunction() { std::cout << "Derived function" << std::endl; } void show() override { std::cout << "Derived show()" << std::endl; } }; void callShow(Base& b) { b.show(); // 调用动态绑定的虚函数 } int main() { Derived d; Base* basePtr = &d; // 向上转型 Base& baseRef = d; // 向上转型 // 基类指针调用基类函数 basePtr->baseFunction(); // 基类指针调用虚函数(动态绑定) basePtr->show(); // 基类引用调用基类函数 baseRef.baseFunction(); // 基类引用调用虚函数(动态绑定) baseRef.show(); // 调用接受基类引用的函数 callShow(d); // 错误:基类指针不能调用派生类特有的函数 // basePtr->derivedFunction(); return 0; } // res Base function Derived show() Base function Derived show() Derived show() |
- 关于内存布局
- 即使发生向上转型,派生类对象的内存布局和虚函数表(
vtable
)保持不变 - 基类指针或引用指向的是派生类对象,因此虚函数指针(
vptr
)仍然指向派生类的虚函数表,从而实现虚函数的动态绑定
- 即使发生向上转型,派生类对象的内存布局和虚函数表(
- 内存布局实例:
1 2 3 4 5 6 7 8 9 10 11 |
class Base { public: virtual void show(); int baseData; }; class Derived : public Base { public: void show() override; int derivedData; }; |
1 2 3 4 |
Base 对象: vptr -> Base vtable baseData |
1 2 3 4 5 |
Derived 对象: vptr -> Derived vtable baseData derivedData |
向下转型
-
将基类指针/引用转回派生类指针/引用
- 如果需要访问派生类特有的数据成员或成员函数,需要进行向下转型
-
static_cast
- 必须确保基类指针/引用实际指向的是目标派生类对象
- 若对象类型不匹配,导致未定义行为
1 2 |
Derived* pd1 = static_cast<Derived*>(pb); // 安全(若 pb 实际指向 Derived) Derived* pd2 = static_cast<Derived*>(new Base); // 不安全!UB |
dynamic_cast
(推荐方式)- 基类必须有虚函数(启用
RTTI
) - 若转换失败,返回
nullptr
(指针)或抛出异常(引用)
- 基类必须有虚函数(启用
1 2 3 4 5 6 |
Derived* pd3 = dynamic_cast<Derived*>(pb); if (pd3) { // 转换成功 } else { // 转换失败(pb 实际指向的不是 Derived 或它的派生类) } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
#include <iostream> class Base { public: int baseData; virtual void show() { std::cout << "Base show()" << std::endl; } virtual ~Base() {} }; class Derived : public Base { public: int derivedData; void show() override { std::cout << "Derived show()" << std::endl; } void derivedFunction() { std::cout << "Derived function" << std::endl; } }; int main() { Derived d; d.baseData = 1; d.derivedData = 2; Base* basePtr = &d; // 向上转型 basePtr->show(); // 调用 Derived::show() // 向下转型以访问派生类特有的成员 Derived* derivedPtr = dynamic_cast<Derived*>(basePtr); if (derivedPtr) { derivedPtr->derivedFunction(); std::cout << "derivedData: " << derivedPtr->derivedData << std::endl; } else { std::cout << "Failed to cast to Derived*" << std::endl; } return 0; } |
截断现象
- 仅发生在值拷贝时
- 当派生类对象被赋值给基类对象(而非指针/引用)时,派生类独有的部分会被丢弃
1 2 |
Derived d; Base b = d; // 对象截断:b 是 Base 类型,无法保留 Derived 的成员 |
其他问题
-
编译时多态,本质是编译器在生成汇编指令时生成了多份吗
- 编译时多态(静态多态)在本质上是编译器在编译阶段根据函数重载或运算符重载生成了不同的函数或运算符的多个版本
- 在编译时,编译器能够确定调用的是哪个具体的函数或运算符版本
因此在生成汇编指令时,会生成针对每个重载版本的不同代码 - 这意味着,函数的不同版本在编译期间被静态地解析,并且每个版本都有自己的汇编代码实现
-
在抽象类里面,可以加非虚函数实现吗
- 在抽象类中可以添加非虚函数的实现,目的是在保持抽象类接口规范的同时,提供可复用的基础功能
-
在抽象类里面,可以定义数据成员吗
- 抽象类通过数据成员为所有派生类提供统一的属性定义,避免重复代码
类型转换
dynamic_cast
- 用途:
- 用于多态类型的安全向下转型
- 特点:
- 在运行时进行类型检查,确保转换的安全性
- 如果转换失败,返回
nullptr
(对于指针)或抛出std::bad_cast
异常(对于引用)
- 所谓运行期进行类型检查:
- 编译器会在生成汇编代码时插入相应的类型检查相关的汇编指令,以保证在运行时对类型进行验证
- 这些验证通常通过虚表(
vtable
)和运行时类型信息(RTTI
)来实现
- 使用条件:
- 要求基类有至少一个虚函数,以支持
RTTI
(运行时类型信息)
- 要求基类有至少一个虚函数,以支持
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
#include <iostream> class Base { public: virtual ~Base() {} }; class Derived : public Base { public: void derivedFunction() { std::cout << "Derived function" << std::endl; } }; int main() { Base* basePtr = new Derived(); Derived* derivedPtr = dynamic_cast<Derived*>(basePtr); if (derivedPtr) { derivedPtr->derivedFunction(); } else { std::cout << "Failed to cast to Derived*" << std::endl; } delete basePtr; return 0; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
; 省略了其他部分,仅关注 dynamic_cast 部分 ; 假设 basePtr 已经指向一个 Derived 对象 mov rax, [basePtr] ; 加载 basePtr 的值 mov rcx, [rax] ; 加载对象的虚表指针(vptr) ; 运行时检查 cmp [rcx], offset Derived_vtable ; 比较虚表指针与 Derived 类的虚表地址 jne dynamic_cast_failed ; 如果不匹配,跳转到 dynamic_cast_failed ; 转换成功 mov rax, [basePtr] ; 将 basePtr 赋值给 derivedPtr jmp dynamic_cast_done ; 跳转到 dynamic_cast_done dynamic_cast_failed: xor rax, rax ; 转换失败,返回 nullptr dynamic_cast_done: ; 继续执行后续代码 |
static_cast
- 用途:
- 用于已知类型关系的转换,包括向上转型和向下转型
- 已知类型关系包括:
- 基类和派生类之间的转换:基类指针或引用转换为派生类指针或引用,反之亦然
- 基本类型之间的转换:如整数到浮点数的转换,反之亦然
- 指针类型之间的转换:如
void*
到具体类型指针的转换
- 特点:
- 在编译时进行类型检查(比如检查两个类是不是继承关系),但不进行运行时检查
- 如果类型不匹配,可能导致未定义行为
- 使用条件:
- 要求在编译时能够确定类型转换的合理性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
#include <iostream> class Base { public: virtual ~Base() {} }; class Derived : public Base { public: void derivedFunction() { std::cout << "Derived function" << std::endl; } }; int main() { Base* basePtr = new Derived(); Derived* derivedPtr = static_cast<Derived*>(basePtr); // 注意:如果 basePtr 实际上不是指向 Derived 对象,可能导致未定义行为 derivedPtr->derivedFunction(); delete basePtr; return 0; } |
reinterpret_cast
- 用途:
- 用于进行位级别的类型转换,通常在底层编程中使用
- 特点:
- 不进行任何类型检查,直接将对象的位模式重新解释为另一类型。如果用于不相关的类型,可能导致未定义行为
- 所谓位级别的类型转换:
- 指直接将一个对象的内存表示(即位模式)解释为另一种类型,而不对数据本身进行任何转换或解释
- 这种转换仅仅改变了编译器对该内存块的解释方式,而不改变内存中的实际位模式
- 具体解释在
tip 6
- 使用条件:
- 通常用于非常低级别和特定场景下的转换,不推荐在常规代码中使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
#include <iostream> class Base { public: virtual ~Base() {} }; class Derived : public Base { public: void derivedFunction() { std::cout << "Derived function" << std::endl; } }; int main() { Base* basePtr = new Derived(); Derived* derivedPtr = reinterpret_cast<Derived*>(basePtr); // 注意:如果 basePtr 实际上不是指向 Derived 对象,可能导致未定义行为 derivedPtr->derivedFunction(); delete basePtr; return 0; } |
- 应用场景
- 示例
- 会按照转换的目标类型去从地址去读取目标类型的相应字节数
- 并不会改变内存中原有的二进制的数据
- 但是会按照转换的目标类型去解释这些二进制位
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
#include <iostream> #include <iomanip> int main() { int num = 42; // 42 在内存中的二进制表示为 0x0000002A float* floatPtr = reinterpret_cast<float*>(&num); // 将 int* 转换为 float* std::cout << "num (as int): " << num << std::endl; std::cout << "num (as float): " << *floatPtr << std::endl; // 打印 num 的二进制表示 std::cout << "Binary representation of num: "; for (int i = sizeof(int) - 1; i >= 0; --i) { std::cout << std::bitset<8>(reinterpret_cast<unsigned char*>(&num)[i]) << ' '; } std::cout << std::endl; return 0; } |
const_cast
- 用途:
- 是一个特殊的类型转换运算符
- 用于移除或添加
const
或volatile
限定符 - 它的主要作用是改变编译器对对象的类型视图,使得可以通过非
const
的方式访问对象
- 特点:
- 仅用于修改对象的常量性,不改变对象的类型
- 原理:
const_cast
并不是通过修改常量的存放位置来实现其功能的- 实际上,
const_cast
只是改变了编译器对对象的类型视图,使得可以通过非const
的方式访问对象 - 然而,使用
const_cast
可能是一件危险的事情,见tip 5
- 使用条件:
- 需要改变对象的常量性时使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
#include <iostream> void print(const int& value) { int& nonConstValue = const_cast<int&>(value); nonConstValue = 42; // 修改了原始对象的值 std::cout << "Value: " << value << std::endl; } int main() { int x = 10; print(x); // 打印并修改 x std::cout << "x: " << x << std::endl; // x 被修改为 42 return 0; } |
- 使用
const_cast
其实是一件危险的事情,如下例:- 编译器可能会对常量进行优化,将其直接嵌入指令中
- 因为
.rodata
段通常是只读的,尝试修改该段中的数据可能会导致程序崩溃或引发其他严重错误
1 2 3 4 5 6 7 8 9 10 11 |
#include <iostream> void exampleFunction() { const int constantValue = 10; // 存储在栈上,但可能会被优化为常量存储在只读数据段 std::cout << "constantValue: " << constantValue << std::endl; } int main() { exampleFunction(); return 0; } |
- 建议:
- 仅用于非常量对象:确保使用
const_cast
仅用于原本就不是常量的对象 - 避免修改全局或静态常量:全局或静态常量通常存储在只读数据段,修改它们是危险的
- 文档和注释:在代码中使用
const_cast
时,添加适当的注释,解释为何需要移除const
限定符以及这样做的安全性
- 仅用于非常量对象:确保使用
关于数据段和BSS段
概述
- 数据段(
Data Segment
):- 存放已初始化的全局和静态变量,以及只读数据段中的常量
BSS
段(Block Started by Symbol
):- 存放未初始化的全局和静态变量,这些变量在程序启动时被初始化为零
常量位置
- 全局常量:通常存放在只读数据段(
.rodata
) - 局部常量:存放在栈上
- 静态常量:通常存放在只读数据段(
.rodata
)
示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
#include <iostream> const int globalConst = 42; // 只读数据段 int globalVar = 0; // 数据段 void exampleFunction() { const int localConst = 1; // 栈 static const int staticConst = 2; // 只读数据段 static int staticVar = 3; // 数据段 } int main() { exampleFunction(); return 0; } |
编译器对对象的类型视图
概述
- 编译器对对象的类型视图指的是编译器在编译阶段如何理解和处理变量的类型信息
- 类型视图包括变量的类型、限定符(如
const
和volatile
)、以及该类型的相关操作和限制 - 通过类型视图,编译器能够进行类型检查、类型转换和优化代码生成等工作
类型视图的基本概念
- 类型(
Type
):- 变量的基本属性,包括内置类型(如
int
、float
)和用户定义类型(如struct
、class
)
- 变量的基本属性,包括内置类型(如
- 限定符(
Qualifiers
):- 修饰类型的属性,包括
const
(常量性)和volatile
(易变性)
- 修饰类型的属性,包括
- 类型检查:
- 编译器在编译阶段检查变量和表达式的类型是否匹配
- 类型转换:
- 编译器在类型视图的基础上进行合法的类型转换,包括隐式转换和显式转换
本文为原创文章,版权归Aet所有,欢迎分享本文,转载请保留出处!
你可能也喜欢
- ♥ Spdlog记述:二07/09
- ♥ C++_函数模板、类模板、特化、模板元编程、SFINAE、概念06/22
- ♥ Deelx正则引擎使用12/24
- ♥ C++标准模板库编程实战_关联容器12/07
- ♥ C++_友元、联合体、内联、static、指针、深浅拷贝06/21
- ♥ COM组件_303/07