本 Wiki 开启了 HTTPS。但由于同 IP 的 Blog 也开启了 HTTPS,因此本站必须要支持 SNI 的浏览器才能浏览。为了兼容一部分浏览器,本站保留了 HTTP 作为兼容。如果您的浏览器支持 SNI,请尽量通过 HTTPS 访问本站,谢谢!
这里会显示出您选择的修订版和当前版本之间的差别。
两侧同时换到之前的修订记录前一修订版后一修订版 | 前一修订版 | ||
cs:programming:cpp:courses:cpp_basic_deep:chpt_13 [2024/11/13 12:28] – [自增自减运算符] codinghare | cs:programming:cpp:courses:cpp_basic_deep:chpt_13 [2024/11/20 13:01] (当前版本) – [using 与重写] codinghare | ||
---|---|---|---|
行 154: | 行 154: | ||
return ret; | return ret; | ||
} | } | ||
+ | </ | ||
+ | ===解引用 / 箭头运算符=== | ||
+ | 两者都是模拟指针的行为。 | ||
+ | ==解引用运算符== | ||
+ | * 返回:解引用后内容类型的**引用**(与下标运算符类似,目的是为了写操作) | ||
+ | * 基于第一条的原因,因此也需要引入 '' | ||
+ | <code cpp> | ||
+ | int& Str:: | ||
+ | { | ||
+ | return *ptr; | ||
+ | } | ||
+ | </ | ||
+ | ==箭头运算符== | ||
+ | 箭头运算符的重载是一个通过递归的方式访问最终数据成员的过程: | ||
+ | * Base case:返回类型是类类型的**指针**(// | ||
+ | * Normal case:返回类型是普通类类型 | ||
+ | <WRAP center round box 100%> | ||
+ | 当使用箭头运算符的重载时,只要返回值不是指针类型,则编译器会尝试在当前对象中继续寻找 '' | ||
+ | </ | ||
+ | 实现上,箭头运算符的重载有返回类型,但没有参数 | ||
+ | <code cpp> | ||
+ | // 返回指针(Base case) | ||
+ | Str* Str:: | ||
+ | { | ||
+ | return this; | ||
+ | } | ||
+ | // 返回对象(Normal case) | ||
+ | // 编译器会到 Str2 中继续寻找 -> 的定义 | ||
+ | Str2 Str:: | ||
+ | { | ||
+ | return Str2; | ||
+ | } | ||
+ | </ | ||
+ | | ||
+ | 函数调用运算符允许将对象变为**可调用对象**,即 '' | ||
+ | <code cpp> | ||
+ | struct Str | ||
+ | { | ||
+ | Str(int* p):ptr(p){} | ||
+ | int operator() (int x, int y, int z) | ||
+ | { | ||
+ | return *ptr + x + y + z; | ||
+ | } | ||
+ | | ||
+ | int* ptr; | ||
+ | }; | ||
+ | |||
+ | int main(int argc, char const *argv[]) | ||
+ | { | ||
+ | int x = 100; | ||
+ | Str StrPtr(& | ||
+ | // 打印 100 + 1 + 2 + 3 的结果 | ||
+ | std::cout << StrPtr(1, | ||
+ | return 0; | ||
+ | } | ||
+ | </ | ||
+ | ===类型转换运算符=== | ||
+ | * 用于自定义类类型与其他类型之间的转换 | ||
+ | * 无显式的返回类型 | ||
+ | * 需要被定义为 '' | ||
+ | * 类型转换不涉及内容的修改,因此应该被定义为 '' | ||
+ | * 涉及常量的类型转换需要重载为 '' | ||
+ | * 写法: | ||
+ | <code cpp> | ||
+ | operator type() const {} | ||
+ | </ | ||
+ | ==避免歧义== | ||
+ | 需要注意的是,除了自定义的类型转换运算符,程序中很可能还存在其他的重载,这些重载也可能具有(隐式)类型转换的功能,比如: | ||
+ | <code cpp> | ||
+ | // 同时定义了 class 到 int 的转换,以及 class 与 int 的加法 | ||
+ | // int() 可以转换 | ||
+ | operator int() const { return val; } | ||
+ | friend Str operator + (const Str& lhs, const Str& rhs); | ||
+ | |||
+ | // 存在两个候选者 | ||
+ | // 4 可以通过 int() 转换为 Str, 也可以通过 + 转换为 Str,因此导致歧义 | ||
+ | Str obj(100); | ||
+ | std::cout << (obj + 4); | ||
+ | </ | ||
+ | 对此,我们可以使用 '' | ||
+ | <code cpp> | ||
+ | explicit operator int() const { return val; } | ||
+ | </ | ||
+ | ==避免隐式转换带来的其他问题== | ||
+ | 隐式转换还可能带来一些未知的结果。比如下面的例子: | ||
+ | <code cpp> | ||
+ | // 使用 cin 输出,逻辑上是不合理的 | ||
+ | // 此处,cin 的隐式转换为 istream -> bool -> int | ||
+ | // 根据 cin 是否有效,最终的表达式为 0 << 3 或是 1 << 3 | ||
+ | // 也就是非法的输出操作通过隐式转换转变为了合法的移位操作 | ||
+ | std::cin << 3; | ||
+ | </ | ||
+ | 这种情况依然可以使用 '' | ||
+ | <WRAP center round info 100%> | ||
+ | '' | ||
+ | </ | ||
+ | ===关系运算符的重载=== | ||
+ | C++ 20 中: | ||
+ | * 只需定义 '' | ||
+ | * '' | ||
+ | <code cpp> | ||
+ | bool operator ==(Str obj, int val) { return obj.x == val; } | ||
+ | // 可以调用 | ||
+ | obj == 3; | ||
+ | // C++ 20 之前错误,无法找到匹配的函数 | ||
+ | // C++ 20 编译通过 | ||
+ | // 编译器会自动尝试 bool operator ==(int val, Str obj) | ||
+ | 3 == obj; | ||
+ | </ | ||
+ | ====类的继承==== | ||
+ | ===基础概念=== | ||
+ | * 基类(// | ||
+ | * 派生类(// | ||
+ | * 继承(// | ||
+ | ==继承的三种方式== | ||
+ | * public:父类的内容对所有人可见 | ||
+ | * private:父类的内容只对自己可见 | ||
+ | * protect:父类的内容对自己以及其子类可见 | ||
+ | <WRAP center round info 100%> | ||
+ | * '' | ||
+ | * 继承方式的部分不算做声明的一部分('': | ||
+ | </ | ||
+ | ==继承的使用== | ||
+ | 可以通过基类的指针和引用指向派生类的对象: | ||
+ | <code cpp> | ||
+ | Derive d; | ||
+ | Base& ref = d; | ||
+ | Base* ptr = &d; | ||
+ | </ | ||
+ | ==被继承类的类型== | ||
+ | * 静态类型:父类指针/ | ||
+ | * 动态类型:父类指针/ | ||
+ | ==派生类的 scope== | ||
+ | * 派生类的 scope 属于基类的一部分 | ||
+ | * 派生类的 scope 处于基类 scope 的内部,遵循内部 name 隐藏外部 name 的原则 | ||
+ | * name 查找原则也是自内向外 | ||
+ | ==派生类中调用基类的构造函数== | ||
+ | * 默认情况下派生类会委托基类中的**默认构造函数**对继承自基类的部分**先行**进行构造 | ||
+ | * 如果基类中缺乏默认构造函数,那么需要在派生类中显式的调用基类的构造函数 | ||
+ | * 对基类的构造必须在初始化阶段:派生类委托基类构造应该在初始化列表中,而不是在函数体中(函数体中的处理处于赋值阶段) | ||
+ | ===虚函数=== | ||
+ | 当基类(或派生类)中某个函数被定义为虚函数时,意味着该函数可以通过基类的**指针或者引用**对其进行调用。其作用是为了实现**运行期的动态多态**:也就是说,根据调用者的动态类型不同,对应的虚函数可能存在不同的实现方式。这些不同的实现方式通过**重写**(// | ||
+ | * 使用 '' | ||
+ | * 非静态、非构造函数可以声明为虚函数 | ||
+ | ==虚函数与虚表== | ||
+ | 虚函数之所以能在运行期与特定类型的对象进行绑定,原因是其维护了一个被称为虚表(// | ||
+ | * 派生类的类型信息 | ||
+ | * 派生类中所有的虚函数的信息 | ||
+ | 虚函数的动态绑定会根据这个表中的信息完成。在派生类中,只要定义了虚函数的重写,那么虚表中对应的同名函数的绑定,更新就会到当前的派生类上。 | ||
+ | <WRAP center round box 100%> | ||
+ | 虚函数通过虚表达实现 C++ 中动态多态的功能:在不改变函数签名的情况下,根据实例化的对象来选择对应的函数。 | ||
+ | </ | ||
+ | ===纯虚函数=== | ||
+ | 纯虚函数是作为类的接口而存在的。与虚函数不同,纯虚函数没有实现。几个特点: | ||
+ | * 其所在的类被称为抽象类,无法进行实例化。 | ||
+ | * 父类中有纯虚函数时,所有继承者重写出实现版本 | ||
+ | 纯虚函数通过以下方式定义: | ||
+ | <code cpp> | ||
+ | virtual returnType Func(parameter_list) = 0; | ||
+ | </ | ||
+ | ===继承与特殊成员函数=== | ||
+ | * 派生类中合成的特殊成员(构造,拷贝构造,赋值)会**隐式**调用基类中对应成员 | ||
+ | * 派生类中的析构函数会调用基类的析构函数 | ||
+ | * 派生类中的构造函数会调用基类中的**默认构造函数** | ||
+ | * 显式定义的特殊成员可能会需要显式的调用基类对应成员 | ||
+ | ==构造与析构的顺序== | ||
+ | * 构造:先构造基类成员 | ||
+ | * 析构:先析构派生类成员 | ||
+ | ====补充知识==== | ||
+ | ===继承方式的影响=== | ||
+ | 影响的实际是被继承成员的访问权限: | ||
+ | * '' | ||
+ | * 实际上代表了 '' | ||
+ | * '' | ||
+ | * 实际上代表了派生类的实现是基于基类的这样一种关系 | ||
+ | * '' | ||
+ | ===using 与继承=== | ||
+ | * 使用 '' | ||
+ | <code cpp> | ||
+ | struct Base | ||
+ | { | ||
+ | public: | ||
+ | int pub; | ||
+ | private: | ||
+ | int pri; | ||
+ | protected: | ||
+ | int pro; | ||
+ | }; | ||
+ | |||
+ | struct Derived: public Base | ||
+ | { | ||
+ | // inaccessiable 的成员(父类 Private 成员)无法修改权限 | ||
+ | public: | ||
+ | // 修改继承的 Protected 成员权限为 public | ||
+ | using Base::pro; | ||
+ | private: | ||
+ | // 修改继承的 public 成员权限为 为 private | ||
+ | using Base::pub; | ||
+ | }; | ||
+ | |||
+ | int main() | ||
+ | { | ||
+ | Derived d; | ||
+ | // 修改后可访问 | ||
+ | d.pro; | ||
+ | // 修改后无法访问 | ||
+ | d.pub; | ||
+ | } | ||
+ | </ | ||
+ | <WRAP center round box 100%> | ||
+ | 对派生类不可见的成员(基类中的 '' | ||
+ | </ | ||
+ | |||
+ | |||
+ | ==using 与成员函数== | ||
+ | * '' | ||
+ | <code cpp> | ||
+ | // 在派生类中使用 | ||
+ | using Base::func; | ||
+ | </ | ||
+ | * 引入的是**函数名**:所有同名的函数都会被引入,在派生类中通过函数匹配规则来调用:比如上面的例子中: | ||
+ | <code cpp> | ||
+ | // 所有名字为 func 的函数都会被引入派生类 | ||
+ | void func(int); | ||
+ | void func(int, | ||
+ | // 派生类中根据函数匹配调用 | ||
+ | // ... | ||
+ | Derived d; | ||
+ | d.func(1); // 调用 func(int) | ||
+ | d.func(2, | ||
+ | </ | ||
+ | * 如果派生类中存在同名函数的定义,那么会**隐藏**所有引入的,基类的同名函数: | ||
+ | <code cpp> | ||
+ | sturct Derived : public Base | ||
+ | { | ||
+ | void func(int, int, int) {//...} | ||
+ | }; | ||
+ | // 错误,无法找到匹配函数 | ||
+ | d.func(1); | ||
+ | </ | ||
+ | <WRAP center round box 100%> | ||
+ | 这点同样适应于构造函数。通常情况下,如果派生类中没有引入新的数据成员,那么可以使用 '' | ||
+ | 但当派生类中定义了构造函数时,派生类的初始化则会调用派生类的构造函数。两者达到的效果相同,但的路径不同: | ||
+ | * '' | ||
+ | * 派生类中定义了构造函数时:通过 '' | ||
+ | </ | ||
+ | ==using 与重写== | ||
+ | * '' | ||
+ | * 如果希望使用 '' | ||
+ | * 最好的办法是使用虚函数的特性,对**特定的重载**进行重写(带函数签名的,比如 '' | ||
+ | * 也可以通过在派生类中使用函数隐藏来进行重写 | ||
+ | ===基类指针与容器=== | ||
+ | C++ 可以通过多态(基类指针)可以(有限)实现容器存储以及访问不同类型的对象: | ||
+ | <code cpp> | ||
+ | struct Base | ||
+ | { | ||
+ | // 访问内容函数 | ||
+ | virtual double getValue() = 0; | ||
+ | // 使用基类指针访问派生类时,释放堆资源必须声明虚析构 | ||
+ | virtual ~Base() = default; | ||
+ | }; | ||
+ | |||
+ | struct DerivedI: public Base | ||
+ | { | ||
+ | DerivedI(int x):val(x) {} | ||
+ | // 注意这里的 double,限制在这里 | ||
+ | double getValue() override { return val; }; | ||
+ | int val; | ||
+ | }; | ||
+ | |||
+ | struct DerivedD: public Base | ||
+ | { | ||
+ | DerivedD(double x): val(x) {} | ||
+ | double getValue() override { return val; }; | ||
+ | double val; | ||
+ | }; | ||
+ | int main(int argc, char const *argv[]) | ||
+ | { | ||
+ | // 使用智能指针作为基类指针 | ||
+ | std:: | ||
+ | | ||
+ | // 使用 vec 通过 new 返回的指针,存储 Base 的不同派生对象 | ||
+ | vec.emplace_back(new DerivedI(1)); | ||
+ | vec.emplace_back(new DerivedD(3.14)); | ||
+ | | ||
+ | for (auto &obj : vec) | ||
+ | { | ||
+ | std::cout << obj-> | ||
+ | } | ||
+ | std::cout << std::endl; | ||
+ | return 0; | ||
+ | }; | ||
+ | </ | ||
+ | <WRAP center round box 100%> | ||
+ | 可以看出来这种实现是有局限性的:虚函数返回的是派生类的公共类型:'' | ||
+ | 那么这种实现也是不可能的。 | ||
+ | </ | ||
+ | ===多重继承与虚继承=== | ||
+ | * 虚继承:以 '' | ||
+ | <code cpp> | ||
+ | Class D1 : virtual public Base { .... }; | ||
+ | </ | ||
+ | * 解决的问题:菱形继承带来的数据成员重复的问题,保证最终继承者的数据成员不会因为多重继承而翻倍。 | ||
+ | ===空基类优化=== | ||
+ | ==空类的大小为 1== | ||
+ | 空类的大小被定义为 '' | ||
+ | <code cpp> | ||
+ | ClassType a[2]; | ||
+ | a[1] -> a[0 + 1] -> address(a[0]) + 1 * sizeof(ClassType) | ||
+ | </ | ||
+ | 这种情况下,如果空类大小为 '' | ||
+ | ==空类的问题以及传统解决方案== | ||
+ | 有几个前提条件: | ||
+ | * 成员函数不占用类的空间 | ||
+ | * 根据计算机的不同,类中元素占用空间不足字的,会进行内存对齐:比如空类 '' | ||
+ | 根据上述信息,因为这个 '' | ||
+ | 进行继承。在这种情况下,编译器会进行空基类优化,忽略空基类的大小: | ||
+ | <code cpp> | ||
+ | struct Base { // some funcs ... }; // empty class | ||
+ | |||
+ | // obj = 4 bytes | ||
+ | struct Derived1 : Base | ||
+ | { | ||
+ | int i; | ||
+ | }; | ||
+ | </ | ||
+ | == C++20 的解决方案== | ||
+ | 上述解决方案的问题在于,public 继承的意义是描述 //is-a// 关系,但明显该类关系不是。C++ 20 提供了一种 '' | ||
+ | 用于描述空类。被该类类型定义的空类大小为 '' | ||
+ | |||
+ | <code cpp> | ||
+ | struct Empty {}; // empty class | ||
+ | |||
+ | struct X | ||
+ | { | ||
+ | int i; | ||
+ | [[no_unique_address]] Empty e; | ||
+ | }; | ||
</ | </ |