What & How & Why

差别

这里会显示出您选择的修订版和当前版本之间的差别。

到此差别页面的链接

两侧同时换到之前的修订记录前一修订版
后一修订版
前一修订版
cs:programming:cpp:courses:cpp_basic_deep:chpt_13 [2024/11/14 02:33] – [箭头运算符] codingharecs:programming:cpp:courses:cpp_basic_deep:chpt_13 [2024/11/20 13:01] (当前版本) – [using 与重写] codinghare
行 209: 行 209:
     return 0;     return 0;
 } }
 +</code>
 +===类型转换运算符===
 +  * 用于自定义类类型与其他类型之间的转换
 +  * 无显式的返回类型
 +  * 需要被定义为 ''const''
 +    * 类型转换不涉及内容的修改,因此应该被定义为 ''const''
 +    * 涉及常量的类型转换需要重载为 ''const'' 
 +  * 写法:
 +<code cpp>
 +operator type() const {}
 +</code>
 +==避免歧义==
 +需要注意的是,除了自定义的类型转换运算符,程序中很可能还存在其他的重载,这些重载也可能具有(隐式)类型转换的功能,比如:
 +<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>
 +对此,我们可以使用 ''explicit'' 关键字禁止其中一个重载进行隐式转换:
 +<code cpp>
 +explicit operator int() const { return val; }
 +</code>
 +==避免隐式转换带来的其他问题==
 +隐式转换还可能带来一些未知的结果。比如下面的例子:
 +<code cpp>
 +// 使用 cin 输出,逻辑上是不合理的
 +// 此处,cin 的隐式转换为 istream -> bool -> int
 +// 根据 cin 是否有效,最终的表达式为 0 << 3 或是 1 << 3
 +// 也就是非法的输出操作通过隐式转换转变为了合法的移位操作
 +std::cin << 3;
 +</code>
 +这种情况依然可以使用 ''explict'' 处理
 +<WRAP center round info 100%>
 +''explict'' 存在例外。当 ''explict'' 修饰的表达式作为条件语句的判断条件时,编译器会将其自动转换为 ''bool'' 类型。该行为不受 ''explicit'' 关键字约束。
 +</WRAP>
 +===关系运算符的重载===
 +C++ 20 中:
 +  * 只需定义 ''=='' 的重载,编译器会自动推导 ''!='' 的重载(3-way comparsion 同理)
 +  * ''=='' 会尝试对 operand 进行自动换位,来匹配用户换位的结果,比如
 +<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;
 +</code>
 +====类的继承====
 +===基础概念===
 +  * 基类(//Base//): 被继承的类
 +  * 派生类(//Derive//): 继承的类
 +  * 继承(//Inheritance//)://is-a// 关系,指派生类是基类的一种。
 +==继承的三种方式==
 +  * public:父类的内容对所有人可见
 +  * private:父类的内容只对自己可见
 +  * protect:父类的内容对自己以及其子类可见
 +<WRAP center round info 100%>
 +  * ''class'' 默认的继承方式是 ''private'',''struct'' 默认的继承方式是 ''public''
 +  * 继承方式的部分不算做声明的一部分('':public Base'')这一部分
 +</WRAP>
 +==继承的使用==
 +可以通过基类的指针和引用指向派生类的对象:
 +<code cpp>
 +Derive d;
 +Base& ref = d;
 +Base* ptr = &d;
 +</code>
 +==被继承类的类型==
 +  * 静态类型:父类指针/引用在编译时的类型,通常为父类的类型
 +  * 动态类型:父类指针/引用在运行期时的类型,通常为其指向的派生类的类型
 +==派生类的 scope==
 +  * 派生类的 scope 属于基类的一部分
 +  * 派生类的 scope 处于基类 scope 的内部,遵循内部 name 隐藏外部 name 的原则
 +  * name 查找原则也是自内向外
 +==派生类中调用基类的构造函数==
 +  * 默认情况下派生类会委托基类中的**默认构造函数**对继承自基类的部分**先行**进行构造
 +  * 如果基类中缺乏默认构造函数,那么需要在派生类中显式的调用基类的构造函数
 +  * 对基类的构造必须在初始化阶段:派生类委托基类构造应该在初始化列表中,而不是在函数体中(函数体中的处理处于赋值阶段)
 +===虚函数===
 +当基类(或派生类)中某个函数被定义为虚函数时,意味着该函数可以通过基类的**指针或者引用**对其进行调用。其作用是为了实现**运行期的动态多态**:也就是说,根据调用者的动态类型不同,对应的虚函数可能存在不同的实现方式。这些不同的实现方式通过**重写**(//override//)实现。一些注意事项:
 +  * 使用 ''vritual'' 关键字定义
 +  * 非静态、非构造函数可以声明为虚函数
 +==虚函数与虚表==
 +虚函数之所以能在运行期与特定类型的对象进行绑定,原因是其维护了一个被称为虚表(//vitrual table//)的数组。该数组会在派生类对象实例化时生成,包含两部分重要的信息:
 +  * 派生类的类型信息
 +  * 派生类中所有的虚函数的信息
 +虚函数的动态绑定会根据这个表中的信息完成。在派生类中,只要定义了虚函数的重写,那么虚表中对应的同名函数的绑定,更新就会到当前的派生类上。
 +<WRAP center round box 100%>
 +虚函数通过虚表达实现 C++ 中动态多态的功能:在不改变函数签名的情况下,根据实例化的对象来选择对应的函数。
 +</WRAP>
 +===纯虚函数===
 +纯虚函数是作为类的接口而存在的。与虚函数不同,纯虚函数没有实现。几个特点:
 +  * 其所在的类被称为抽象类,无法进行实例化。
 +  * 父类中有纯虚函数时,所有继承者重写出实现版本
 +纯虚函数通过以下方式定义:
 +<code cpp>
 +virtual returnType Func(parameter_list) = 0;
 +</code>
 +===继承与特殊成员函数===
 +  * 派生类中合成的特殊成员(构造,拷贝构造,赋值)会**隐式**调用基类中对应成员
 +  * 派生类中的析构函数会调用基类的析构函数
 +  * 派生类中的构造函数会调用基类中的**默认构造函数**
 +  * 显式定义的特殊成员可能会需要显式的调用基类对应成员
 +==构造与析构的顺序==
 +  * 构造:先构造基类成员
 +  * 析构:先析构派生类成员
 +====补充知识====
 +===继承方式的影响===
 +影响的实际是被继承成员的访问权限:
 +  * ''public'' 继承:所有继承自父类的成员访问权限都不会发生改变
 +    * 实际上代表了 ''is-a'' 关系,因为继承了所有的行为
 +  * ''protect'' 继承:继承自父类中的,所有成员权限高于 ''protected'' 的,都会变为 ''protected''
 +    * 实际上代表了派生类的实现是基于基类的这样一种关系
 +  * ''private'' 继承:继承自父类中的,所有成员权限高于 ''private'' 的,都会变为 ''private''
 +===using 与继承===
 +  * 使用 ''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;
 +}
 +</code>
 +<WRAP center round box 100%>
 +对派生类不可见的成员(基类中的 ''private'' 类型成员),''using'' 无法改变其权限
 +</WRAP>
 +
 +
 +==using 与成员函数==
 +  * ''using'' 关键字也可以作用于基类的成员函数(构造函数除外),使用方法:
 +<code cpp>
 +// 在派生类中使用
 +using Base::func;
 +</code>
 +  * 引入的是**函数名**:所有同名的函数都会被引入,在派生类中通过函数匹配规则来调用:比如上面的例子中:
 +<code cpp>
 +// 所有名字为 func 的函数都会被引入派生类
 +void func(int);
 +void func(int,int);
 +// 派生类中根据函数匹配调用
 +// ...
 +Derived d;
 +d.func(1); // 调用 func(int)
 +d.func(2,2); // 调用 func(int, int)
 +</code>
 +  * 如果派生类中存在同名函数的定义,那么会**隐藏**所有引入的,基类的同名函数:
 +<code cpp>
 +sturct Derived : public Base
 +{
 +    void func(int, int, int) {//...}
 +};
 +// 错误,无法找到匹配函数
 +d.func(1);
 +</code>
 +<WRAP center round box 100%>
 +这点同样适应于构造函数。通常情况下,如果派生类中没有引入新的数据成员,那么可以使用 ''using'' 直接 “借” 基类的构造函数逻辑使用。
 +但当派生类中定义了构造函数时,派生类的初始化则会调用派生类的构造函数。两者达到的效果相同,但的路径不同:
 +  * ''using Base::Cstr'':使用 ''Base::Cstr()'' 构造
 +  * 派生类中定义了构造函数时:通过 ''Derived::Cstr()'' -> 调用 ''Base::Cstr()'' 完成派生类对象的构造
 +</WRAP>
 +==using 与重写==
 +  * ''using'' 不会改变虚函数的修饰(''using'' 的优先级低于重写)
 +  * 如果希望使用 ''using'' 实现**改变权限下的重写**:
 +    * 最好的办法是使用虚函数的特性,对**特定的重载**进行重写(带函数签名的,比如 ''func(int)'',而不是 ''func''
 +    * 也可以通过在派生类中使用函数隐藏来进行重写
 +===基类指针与容器===
 + 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::vector<std::shared_ptr<Base>> vec;
 +    
 +    // 使用 vec 通过 new 返回的指针,存储 Base 的不同派生对象
 +    vec.emplace_back(new DerivedI(1));
 +    vec.emplace_back(new DerivedD(3.14));
 +    
 +    for (auto &obj : vec)
 +    {
 +        std::cout << obj->getValue() << " ";
 +    }
 +    std::cout << std::endl;
 +    return 0;
 +};
 +</code>
 +<WRAP center round box 100%>
 +可以看出来这种实现是有局限性的:虚函数返回的是派生类的公共类型:''int'' 可以转换成 ''double''。如果这种公共类型不存在,
 +那么这种实现也是不可能的。
 +</WRAP>
 +===多重继承与虚继承===
 +  * 虚继承:以 ''virtual'' 的方式继承
 +<code cpp>
 +Class D1 : virtual public Base { .... };
 +</code>
 +  * 解决的问题:菱形继承带来的数据成员重复的问题,保证最终继承者的数据成员不会因为多重继承而翻倍。
 +===空基类优化===
 +==空类的大小为 1==
 +空类的大小被定义为 ''1'',是因为寻址的需求。假设存在一个该类类型的数组,则其寻址是基于起始地址 + 类大小 * 元素数量来计算的:
 +<code cpp>
 +ClassType a[2];
 +a[1] -> a[0 + 1] -> address(a[0]) + 1 * sizeof(ClassType)
 +</code>
 +这种情况下,如果空类大小为 ''0'',则会导致 ''a[0]'' 和 ''a[1]'' 的地址相同。C++ 不允许存在两个地址相同但逻辑上不同的单元。
 +==空类的问题以及传统解决方案==
 +有几个前提条件:
 +  * 成员函数不占用类的空间
 +  * 根据计算机的不同,类中元素占用空间不足字的,会进行内存对齐:比如空类 ''1'' 和 ''int'' 成员的组合,大小为 ''5'' 字节,但占用 ''8'' 字节空间
 +根据上述信息,因为这个 ''1'' 的空间占用,我们为上述组合付出的代价是空间占用翻倍。传统的解决方案是将函数放置到基类中,
 +进行继承。在这种情况下,编译器会进行空基类优化,忽略空基类的大小:
 +<code cpp>
 +struct Base { // some funcs ... }; // empty class
 + 
 +// obj = 4 bytes
 +struct Derived1 : Base
 +{
 +    int i;
 +};
 +</code>
 +== C++20 的解决方案==
 +上述解决方案的问题在于,public 继承的意义是描述 //is-a// 关系,但明显该类关系不是。C++ 20 提供了一种 ''no_unique_address'' 的类型
 +用于描述空类。被该类类型定义的空类大小为 ''0''。因此,相较于继承,我们可以将函数类直接作为数据成员放置到新类中调用,而不用付出额外的空间成本:
 +
 +<code cpp>
 +struct Empty {}; // empty class
 + 
 +struct X
 +{
 +    int i;
 +    [[no_unique_address]] Empty e;
 +};
 </code> </code>