Lazy loaded image
考研
C++ 学习笔记——七、类的高级特性
字数 7010阅读时长≈ 18 分钟
2026-10-15
2026-10-15

目录:点我

一、运算符重载

类的重载特性允许将C++现有的运算符的功能改为其他形式,举个栗子:
此时要实现两个Time对象相加,需要调用成员方法Sum,举个栗子:
而使用重载方式则更加简单,举个栗子:
此时可以使用下面两种方法实现两个对象的相加方法:
可重载运算符:
||||||| |-|-|-|-|-|-| |+|-||/|%|^| |&|||~=|!|=|<| |>|+=|-=|=|/=|%=| |^=|&=||=|<<|>>|>>=| |<<=|==|!=|<=|>=|&&| ||||++|–|,|->|->*| |()|[]|new|delete|new []|delete []|
+
-
*
/
%
^
&
|
~=
~
=
<
>
+=
-=
*=
/=
%=
^=
&=
|=
<<
>>
>>=
<<=
==
!=
<=
>=
&&
||
++
—
->
->*
()
[]
new
delete
new []
delete []
注意:
  • 重载运算符不能改变需要的操作数个数,例如加法运算需要两个操作数,不能仅使用一个操作数完成加法运算。
    • 重载运算符不改变运算符原有的优先级。
    • 不能创建新的运算符(只可重载标准运算符)。
    • 不能重载以下运算符:
      • sizeof:sizeof运算符
      • .:成员运算符
      • .*:成员指针运算符
      • :::作用域解析运算符
      • ?::条件运算符
      • typeid:一个RTTI运算符
      • const_cast:强制类型转换运算符
      • dynamic_cast:强制类型转换运算符
      • reinterpret_cast:强制类型转换运算符
      • static_cast:强制类型转换运算符
    • sizeof:sizeof运算符
    • .:成员运算符
    • .*:成员指针运算符
    • :::作用域解析运算符
    • ?::条件运算符
    • typeid:一个RTTI运算符
    • const_cast:强制类型转换运算符
    • dynamic_cast:强制类型转换运算符
    • reinterpret_cast:强制类型转换运算符
    • static_cast:强制类型转换运算符
    • 以下运算符只能通过成员函数重载:
      • =:赋值运算符
      • ():函数调用运算符
      • []:下标运算符
      • >:通过指针访问类成员的运算符
    • =:赋值运算符
    • ():函数调用运算符
    • []:下标运算符
    • >:通过指针访问类成员的运算符
    使用一个参数就可以调用的构造函数定义了从参数类型到类类型的转换:
    对于隐式的转换可能是不安全的,因此可以使用关键字 explicit 将其禁止,但是显示调用仍然是可行的:

    二、友元函数

    1. 介绍

    在为类重载二元运算符时,常常需要友元,举个栗子:
    为解决上述问题,可以考虑使用非成员函数,举个栗子:
    但是这样又出现了新的问题,由于对象的成员变量通常是私有的,因此不能直接用非成员函数访问。
    为了解决这个问题,便出现了友元函数。

    2. 使用友元

    举个栗子:
    该原型意味着下面两点:
    • 虽然 operator*() 函数是在类声明中声明的,但它不是成员函数,因此不能使用成员运算符来调用。
    • 虽然 operator*() 函数不是成员函数,但它与成员函数的访问权限相同。
    接下来编写函数定义,由于它不是成员函数,因此不需要使用Time::限定符,并且不需要使用关键字friend:
    通过上述定义,就解决了非成员函数访问私有变量的问题,下述代码可以正常进行转换:
    需要注意的是,对于成员重载函数来说,只需要一个参数,因为另一个参数会通过 this 指针传递;而对于友元重载函数来说,需要两个参数,即需要显示的指出对象。

    三、类和动态内存分配

    1. 静态成员

    类声明中不能初始化静态成员变量,因为声明描述了如何分配内存,但并不能分配内存,因此需要在类声明之外使用单独的语句来进行初始化,所有对象的静态变量共享同一个内存单元:
    类中还可以存在静态成员函数,即将成员函数声明为静态的(关键字 static),其有两个特性:
    • 不能通过对象调用静态成员函数,也不能对其使用 this 指针,如果其在共有部分声明,可以使用域解析运算符调用;
    • 静态成员函数不与特定的对象相关联,因此只能使用静态数据成员。

    2. 返回类型

    • 返回指向 const 对象的引用效率更高,因为不需要使用拷贝构造函数: cpp const Vector & Fun(const Vector & v) { return v; }
    • 返回指向非 const 对象的引用通常用于重载复制运算符以及与 cout 一起使用的 << 运算符,前者旨在提高效率,后者则必须这样做:
      • 返回对象通常为被调用函数中的局部变量,若使用引用则会出错,因为局部变量已被销毁:
        • 返回 const 对象通常用于防止出错:
          • 3. 指向对象的指针

            对于析构函数,调用顺序如下:
            1

        3. 指向对象的指针

        对于析构函数,调用顺序如下:
        1
        • 如果对象是动态变量,则当执行完定义该对象的程序块时调用析构函数;
        • 如果对象是静态变量,则在程序结束时调用析构函数;
        • 如果对象是 new 创建的指针,仅当使用 delete 删除对象时调用析构函数(或者程序结束)。

        4. 成员初始化列表

        对于构造函数可以使用如下方法:
        • 这种格式只能用于构造函数;
        • 必须用这种格式初始化非静态 const 数据成员(C++ 11 之前);
        • 必须用这种格式初始化引用数据成员;
        • 被初始化的顺序与定义顺序相同,与列表顺序无关。

        四、继承

        • 可以在已有类的基础上添加功能;
        • 可以给类添加数据;
        • 可以修改类方法的行为。

        1. 介绍

        类的继承特性,顾名思义,就是在某个类的基础上新增一部分功能从而实现新的类,举个栗子:
        上述继承的方法为公有继承(public),子类可以继承父类的成员数据和函数,并且是按照父类的权限进行继承,例如父类的数据为私有类型,子类继承后也为私有类型。
        子类不能直接访问父类的私有成员,必须通过父类的方法进行访问。
        由于在创建子类对象的时候,需要先创建父类对象,因此构造函数的使用有了新的要求,举个栗子:
        上述代码显式的调用了构造函数,根据继承的特性,对于构造函数与析构函数的调用过程如下:
        • 父类构造函数
        • 子类构造函数
        • 子类析构函数
        • 父类析构函数
        相似的,继承时也可以使用拷贝构造函数:
        并且可以使用成员初始化列表语法:
        可以使用派生类对象来初始化基类对象:
        对于形参是基类引用的构造函数,可以引用派生类,由于隐式拷贝构造函数的存在,也可以进行赋值:
        派生类可以使用域解析运算符调用父类的公有方法:

        2. 多态公有继承

        派生类对象使用基类的方法时,可能希望同一个方法在派生类和基类中的行为是不同的,也就是说同样的方法应当取决于调用该方法的对象,这种行为称为多态,有两种重要机制可用于实现多态公有继承:
        • 在派生类中重新定义基类的方法;
        • 使用虚方法。
        要讨论多态与继承的问题,首先需要了解一个例子:
        上述代码定义了一个父类与两个子类,现在我们尝试使用两个子类的area()方法:
        输出结果如下:
        程序输出的结果是父类方法 area() 的调用结果,这说明对象的定义决定了它选择哪个版本来输出(定义为父类 shape ,所以输出为父类的版本),这是因为调用的函数 area() 被编译器设置为基类中的版本,这就是所谓的静态多态,或静态链接——函数调用在程序执行前就准备好了。有时候这也被称为早绑定,因为 area() 函数在程序编译期间就已经设置好了,而编译的时候发现它的定义为父类,因此选择父类的版本。
        解决这个问题的方法很简单,只需要让编译器不要进行早绑定即可,也就是所谓的动态链接或后期绑定,方法如下:
        即将父类的函数 area()设 定为虚函数,此时的输出为:
        这是因为 virtual 关键字将静态链接(早绑定)改为了动态链接(后期绑定),使程序在运行时再进行选择,此时编译器分析被引用的对象实际类型后,选择采用派生类的版本。该效果同样适用于被指针指向的对象。
        假设现在想要在基类中定义虚函数,以便在派生类中重新定义该函数更好地适用于对象,但是在基类中又不想对虚函数给出有意义的实现,这个时候就会用到纯虚函数,我们可以把基类中的虚函数 area() 改写如下:
        =0 就是在告诉编译器,函数没有主体,上面的虚函数是纯虚函数,值得注意的是,若使用 =0 表示纯虚函数,可以将前面的 virtual 省略。在 C++ 中,纯虚函数也可以有自己的定义内容,不需要一定为空。
        由于这种特性的存在,还可以用于析构函数,即虚析构函数:
        如上述指针 p ,倘若析构函数不是虚的,那么程序结束后会执行父类的析构函数,而不是派生类的析构函数,这样可能导致内存泄漏。反之若析构函数是虚的,那么程序结束后系统会判断指针所指对象的具体类型,从而调用派生类的析构函数。

        3. 继承方式

        特征
        公有继承
        保护继承
        私有继承
        公有成员变成
        派生类的公有成员
        派生类的保护成员
        派生类的私有成员
        保护成员变成
        派生类的保护成员
        派生类的保护成员
        派生类的私有成员
        私有成员变成
        只能通过基类接口访问
        只能通过基类接口访问
        只能通过基类接口访问
        能否隐式向上转换
        是
        是(但只能在派生类中)
        否
        其中 protected 比较特别,它只在派生关系中发挥作用。对于外部世界来说,类的 protected 成员与 private 成员相似,均不可访问;对于派生类来说,protected 成员与 public 成员相似,可以被派生类访问。

        4. 联编

        程序调用函数时,将使用哪个可执行代码块是由编译器来决定的,将源代码中的函数调用解释为执行特定的函数代码块这个过程被称为函数名联编(binding)。在 C++ 中,由于函数重载的存在,编译器必须查看函数参数以及函数名才能确定使用哪个版本。若这个过程在编译时完成,则称为静态联编(static binding),又称为早期联编(early binding)。由于虚函数的存在,因此可将该过程推迟到程序运行阶段,来选择正确的虚方法的代码,这种方式称为动态联编(dynamic binding),又称为晚期联编(late binding)。
        由于虚方法的存在,导致指针和引用具有兼容性,即派生类引用或指针转换为基类引用或指针,这被称为强制转换(upcasting),这使公有继承不需要进行显示类型转换。这种方式不会出错的原因是子类是父类的扩充,对父类的成员拥有完全控制权(包括通过拥有方法控制私有成员),因此是可行的。
        相反的过程称为向下强制转换(downcasting),如果不采用显示类型转换,这种转换是不允许的。

        Ⅰ. 为什么有两种类型的联编?为什么默认为静态联编?

        • 在效率方面,为使程序能够在运行阶段进行决策,必须采取一些方法来跟踪基类指针或引用指向的对象类型,这额外增加了处理开销。而对于不作为基类的类、不重定义基类方法的类,均不需要动态联编,因此静态联编更合理,效率也更高。
        • 在概念模型方面,设计类时,可能包含一些不在派生类重新定义的成员函数,因此采用静态联编效率更高,且指出不要重新定义该函数。因此仅将哪些预期将被重新定义的方法声明为虚的是更有效的方式。

        Ⅱ. 虚函数的工作原理

        编译器处理虚函数的方法是:给每一个对象添加一个隐藏成员,隐藏成员中保存了一个指向函数地址数组的指针,这种数组称为虚函数表(virtual function table, vtbl),里面存储了为类对象进行声明的虚函数的地址。例如基类对象包含一个指向基类中所有虚函数的地址表的指针;派生类对象将包含一个指向独立地址表的指针。如果派生类提供了虚函数的新定义,该虚函数表将保存新函数的地址;如果没有新定义,该虚函数表将保存函数原始版本的地址。如果派生类定义了新的虚函数,则该函数的地址也将被添加到虚函数表中,由于对象中保存的是指针,因此不论虚函数是多是少,都只需要在对象中添加一个地址成员。总结一下就是:
        那么两个类中均存在虚函数表:
        Dad 成员方法的含义
        表地址:2008
        …
        …
        fun1
        4064
        fun2
        6400
        Son 成员方法的含义
        表地址:2096
        …
        …
        fun1
        6820
        fun2
        7280
        此时两个类所创建的对象中存在指针:
        指针字段含义|
        vptr
        …
        …
        dad
        2008
        son
        2096
        此时运行以下代码,将分为四个步骤执行:
        • 获取指针所指向的 vptr 地址;
        • 前往 vptr 地址表(2096);
        • 获取函数 fun1 的地址;
        • 前往函数 fun1 的地址(6820),并执行。

        Ⅲ. 注意事项

        • 基类中声明的虚函数,在派生类及其派生类中也是虚的;
        • 如果使用指向对象的引用或指针来调用虚方法,程序将使用为对象类型定义的方法,而不使用为引用或指针类型定义的方法;
        • 如果定义的类被用作基类,则应将那些要在派生类中重新定义的类方法声明为虚的;
        • 构造函数不能是虚函数,因为派生类会调用基类和自己的构造函数;
        • 析构函数应当为虚函数,除非类不作为基类;
        • 友元不能是虚函数,因为友元不是类成员;
        • 如果派生类没有重新定义函数,将使用基类的版本;
        • 如果派生类位于派生链中,将使用最新的虚函数版本;
        • 以上两条不适用于被隐藏的方法:
          • 此时新定义函数 fun 不接受任何参数,由于同名且不生成两个重载版本,导致基类方法被隐藏。因此如果重新定义基类方法,因保证函数原型相同;但如果返回类型是基类引用或指针,则可以修改为指向派生类的引用或指针,这种特性被称为返回类型协变(covariance of return type),因为允许返回类型随类类型的变化而改变:
            这种例外只适用于返回值,而不适用于参数。
        • 如果基类声明被重载了,则应在派生类中重新定义所有的基类版本:
          • 构造函数、析构函数、赋值运算符(=)、友元函数均不能被继承。
          • 派生类要想使用基类的友元函数,需要通过对象的基类指针或引用来访问,因为编译器会将派生类强制转换为基类后再调用友元函数。

          5. 抽象基类

          假设目前有一个椭圆类,现在需要构造一个圆类,由于椭圆类相对于圆类存在许多冗余成员,因此直接继承会造成空间的浪费,而重新定义则又浪费已有实现的部分代码。另一种解决办法是从椭圆类和圆类中抽象出共性,并将其放入一个单独的类 ABC 中,这样便可以使用基类指针同时管理椭圆类和圆类的对象,由于一些方法在类 ABC 中不可能实现,因此通过使用纯虚函数来提供,而这样的类通常不能创建对象。也就是说,包含纯虚函数的类只能用作基类。
          纯虚函数允许有定义,但是派生类必须重写纯虚函数,从而确保派生的所有组件都至少支持 ABC 指定的功能:

          6. 动态内存分配

          如果基类使用动态内存分配,并重新定义赋值和拷贝构造函数,对派生类会产生怎样的影响?

          Ⅰ. 派生类不使用 new

          • 析构函数:由于派生类没使用 new ,因此可以采用默认的析构函数。派生类的析构函数执行完自己的代码后会自动调用基类的析构函数,因此能够妥善处理基类 new 的内存,因此派生类不需要显示定义析构函数。
          • 拷贝构造函数:由于派生类没使用 new ,因此使用默认的拷贝构造函数可以完成对成员变量的复制。对于继承得到的基类成员变量,则会采用基类中显示定义的拷贝构造函数,因此也能妥善处理这部分成员变量,因此派生类不需要显示定义拷贝构造函数。
          • 赋值运算符:与拷贝构造函数相同,因此也不需要显示定义赋值函数。

          Ⅱ. 派生类使用 new

          • 析构函数:由于派生类使用了 new ,因此需要显示定义析构函数来 delete 对应的内存空间。
          • 拷贝构造函数:对于基类的数据,会调用基类的拷贝构造函数;对于派生类自己的变量,则需要显示的定义拷贝构造函数来处理:
            • 赋值运算符:需要显式定义并用基类的赋值运算符处理:
              原创存证 · 本地哈希 · e30ec499f7d1...
              算法:SHA-256
              时间:2026-10-15
              哈希:e30ec499f7d15f34a055b053e5989985a8591c87277650579bbddc3b070ea807

              该信息用于辅助证明当前内容版本,不等同于法律公证或版权登记。


              评论
              Loading...