![Visual C++从入门到精通(第5版)](https://wfqqreader-1252317822.image.myqcloud.com/cover/338/27563338/b_27563338.jpg)
5.1 类和对象
面向对象最大的特征就是提出了类和对象的概念。在以面向对象的方式开发应用程序时,将遇到的各种事物抽象为类,类中包含数据和操作数据的方法,用户通过实例化类对象来访问类中的数据和方法。举例来说,杯子是一个类,那么茶杯是该类的对象,酒杯也是该类的对象,玻璃杯、塑料杯同样都是杯子类的对象。本节将介绍有关类和对象的相关知识。
5.1.1 类的定义
C++语言中类和结构体类似,其中可以定义数据和方法。C++语言提供了class关键字定义类,其语法格式如下。
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P140_35444.jpg?sign=1738874034-Hs1Ryrwo8Mjxm1iV76bCewnZYI9E6NEJ-0-70030ab5f666cf032a488f73052caf47)
类的定义包含两部分,即类头和类体。类头由class关键字和类名构成;类体由一组大括号“{}”和一个分号“;”构成。类体中通常定义类的数据和方法,其中数据描述的是类的特征(也被称为属性);方法实际上是类中定义的函数,描述的是类的行为。下面的代码定义了一个CUser类。
【例5.1】 定义一个CUser类。
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P140_35464.jpg?sign=1738874034-5JoAJQZfI1GxsVDcqkrr6O10MnAwa7IW-0-f8235b57212a89839a8719879b97d6d4)
上述代码定义了一个CUser类,其中包含两个数据成员和一个方法。对于方法的定义,直接放在了类体中;此外,也可以将方法放在类体的外面进行定义。
【例5.2】 将方法放置在类体之外。
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P141_35588.jpg?sign=1738874034-I7YiNNBBYMq9ZpHidtUHBzglifHDXXlY-0-4d5e92bc235a7c6c4185bd95d2b8e8f4)
当方法的定义放置在类体外时,方法的实现部分首先是方法的返回值,然后是方法名称和参数列表,最后是方法体。
说明
当方法的定义放置在类体外时,方法名称前需要使用类名和域限定符“::”来标记方法属于哪一个类。
注意
在定义类的数据成员时,注意不能像定义不同变量一样进行初始化。
例如,下面的类定义是错误的。
【例5.3】 不能直接对类数据成员进行初始化。
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P141_35683.jpg?sign=1738874034-c5dacDjuLHpky0Xulx0WSqWTfLmRJHgx-0-294f5aea30c5fe28e1ab417dd3bef36b)
注意
在定义类时,不要忘记末尾的分号,这是初学者最容易也最经常犯的一个错误。
5.1.2 类成员的访问
类成员主要是指类中的数据成员和方法(方法也被称为成员函数)。在定义类时,类成员是具有访问限制的。C++语言提供了3个访问限定符用于标识类成员的访问,分别为public、protected和private。public成员也被称为公有成员,该成员可以在程序的任何地方进行访问。protected成员被称为保护成员,该成员只能在该类和该类的派生类(子类)中访问,除此之外,程序的其他地方不能访问保护成员。private成员被称为私有成员,该成员只能在该类中访问,派生类以及程序的其他地方均不能访问私有成员。如果在定义类时没有指定访问限定符,默认为private,如5.1.1节中定义的CUser类。
下面重新定义CUser类,将各个成员设置为不同的访问级别。
【例5.4】 设置类成员的不同访问级别。
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P142_35816.jpg?sign=1738874034-94s9eYcHobH8IV48tuZG55dvdCJjLxTQ-0-b0aa61df051d197f14f2a052e36161ba)
在上述代码中,读者需要注意的是GetUsername方法的定义,在方法的最后使用了const关键字。在定义类方法时,如果不需要在方法中修改类的数据成员,建议在方法声明的最后使用const关键字,表示用户不能在该方法中修改类的数据成员。例如,如果在GetUsername方法中试图修改m_Password成员或m_Username成员将是非法的。
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P143_36058.jpg?sign=1738874034-ILD6dx1StdJVYDdF4KjWCuamTUJO0e90-0-1b169dfbb63c044b3814bf6cd299afeb)
如果类中包含有指针成员,在const方法中不可以重新为指针赋值,但是可以修改指针所指向地址中的数据。例如:
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P143_36083.jpg?sign=1738874034-hcDbyXDaLsD54zKPU0wWKojDbIRjJQey-0-9cabc8cdd7617868429e06bd733fb47e)
在定义类后,需要访问类的成员。通常类成员(静态成员除外)的访问是通过对象实现的,对象被称为类的实例化。当程序中定义一个类时,并没有为其分配存储空间,只有当定义类的对象时,才分配存储空间。对象的定义与普通变量的定义是相同的,下面的代码定义了一个CUser类的对象user。
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P143_94732.jpg?sign=1738874034-lmN8Gbc4skDKI8GTzTmzjyk8rdaZOrtf-0-e67851a208675dd978d818df210fc2fa)
定义了类的对象后,即可访问类的成员。例如:
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P144_36132.jpg?sign=1738874034-sXB0GUkzpg7R5DxtyEQ3qZGMpGFH0tGc-0-946c3fd35d733838e6b4cb911cd92314)
类成员是具有访问权限的,如果类的外部访问私有或受保护的成员将出现访问错误。例如:
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P144_36152.jpg?sign=1738874034-m3nLn8DRruhlUKVhxNN6AWUrPCLONqxx-0-bb01c95f43e1d2e9265d8d7385b8d970)
上述代码试图访问CUser类的私有成员m_Password,产生了编译错误。
在定义类对象时,也可以将类对象声明为一个指针。例如:
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P144_36162.jpg?sign=1738874034-TvPrjY1UBFM49uJQQI5nFZokZZG0ovcE-0-cf035fd0be379e55158842bf77f4732a)
程序中可以使用new运算符来为指针分配内存。例如:
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P144_36167.jpg?sign=1738874034-Bb7nW3br43EPa2qef24yJIk0Q6GQ9DfN-0-4634dcc7bb46e4499650d67929134acf)
也可以写成如下形式:
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P144_36172.jpg?sign=1738874034-jaSnaOP6bwXW9pgyrYgwIGfMyDunOEOq-0-57d04d5f07eb31a8aa3c4d3b4f0fa473)
说明
如果类对象被定义为指针,需要使用“->”运算符来访问类的成员,而不能使用“. ”运算符来访问。
例如:
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P144_36177.jpg?sign=1738874034-polfdEVwZVqMYq9T3WBZtleUzWAD96ou-0-a82233c4a3099d3ae820fa4a64c880b4)
如果将类对象定义为常量指针,则对象只允许调用const方法。例如:
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P144_36202.jpg?sign=1738874034-0MhYrJ3Ij5B7gRdSsAYP5bZqcv8aSHk4-0-6db91a0501621125313cd3af019c7abd)
5.1.3 构造函数和析构函数
每个类都具有构造函数和析构函数。其中,构造函数在定义对象时被调用,析构函数在对象释放时被调用。如果用户没有提供构造函数和析构函数,系统将提供默认的构造函数和析构函数。
1. 构造函数
构造函数是一个与类同名的方法,可以没有参数、有一个参数或多个参数,但是构造函数没有返回值。如果构造函数没有参数,该函数被称为类的默认构造函数。下面的代码显式地定义了一个默认的构造函数。
【例5.5】 定义默认的构造函数。(实例位置:资源包\TM\sl\5\1)
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P145_36259.jpg?sign=1738874034-CtXB3DpaqxxTVKOCDMe5K94YRvnebTEV-0-635024f2fa58b7e91b9890fca86df515)
说明
如果用户为类定义了构造函数,无论是默认构造函数还是非默认构造函数,系统均不会提供默认的构造函数。
下面定义一个CUser类的对象,它将调用用户定义的默认构造函数。
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P145_36359.jpg?sign=1738874034-qWFJhloUU1z62lCcjfJFktLOu1MhWGYb-0-988286a615ad5153ca808c86b320b126)
执行上述代码,结果如图5.1所示。
从图5.1中可以发现,在定义user对象时,调用了默认的构造函数对数据成员进行了赋值。下面再定义一个非默认的构造函数,使用户能够在定义CUser类对象时为数据成员赋值。
【例5.6】 定义非默认的构造函数。(实例位置:资源包\TM\sl\5\2)
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P145_36389.jpg?sign=1738874034-aD2qj29r3GYFc72Pmn7IJPlkbmWhB1B8-0-9ec5555e7e056f7514fd718622ff3faa)
下面定义两个CUser对象,分别采用不同的构造函数。
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P146_36559.jpg?sign=1738874034-P7OkVzbmAHd0joezqjzrEfBtB0xLDNhA-0-a98ee554e324b0f4d5bd49813459951a)
执行上述代码,效果如图5.2所示。
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P146_36597.jpg?sign=1738874034-kvXlFtPIAJvCsqMOLkdeMk18V0mFkfY0-0-629407ab6fefc0f307f3ba44055a4f1e)
图5.1 默认构造函数
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P146_36600.jpg?sign=1738874034-RWCgQwq644gp5UaoIsyIGtMkWcutCUE5-0-523d236ea33764063b31ea53e17a19e9)
图5.2 非默认构造函数
说明
一个类可以包含多个构造函数,各个构造函数之间通过参数列表进行区分。
从图5.2所示的输出结果可以发现,语句“CUser Customer("SK","songkun");”调用了非默认的构造函数,通过传递两个参数初始化CUser类的数据成员。如果想要定义一个CUser对象的指针,并调用非默认构造函数进行初始化,可以采用如下形式。
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P147_94692.jpg?sign=1738874034-C0EvPGwmbPuEMKC8ntjHr0W9qyhBx61n-0-2a672e15c34bc07abcd8c99cb4e9cbd0)
在定义常量或引用时,需要同时进行初始化。5.1.1节中已经介绍了在类体中定义数据成员时,为其直接赋值是非法的,那么如果在类中包含常量或引用类型数据成员时该如何初始化呢?
类的构造函数通过使用“:”运算符提供了初始化成员的方法。
【例5.7】 在构造函数中初始化数据成员。
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P147_36636.jpg?sign=1738874034-xMbKLzUjAfHeNCer30LdfGOoRu5F0ZrW-0-5ce01bc0f0e4a8286e7563e0dd4a2ec4)
上述代码在定义CBook类的构造函数时,对数据成员m_Price和m_ChapterNum进行了初始化。
说明
编译器除了能够提供默认的构造函数外,还可以提供默认的复制构造函数。当函数或方法的参数采用按值传递时,编译器会将实际参数复制一份传递到被调用函数中,如果参数属于某一个类,编译器会调用该类的复制构造函数来复制实际参数到被调用函数。复制构造函数与类的其他构造函数类似,以类名作为函数的名称,但是其参数只有一个,即该类的常量引用类型。因为复制构造函数的目的是为函数复制实际参数,没有必要在复制构造函数中修改参数,因此参数定义为常量类型。
下面的代码为CBook类定义了一个复制构造函数。
【例5.8】 定义复制构造函数。(实例位置:资源包\TM\sl\5\3)
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P147_36696.jpg?sign=1738874034-PgYqahIOnhBHz5Bpammhq9f0buNmN1s9-0-bb697bbce0ffedcc514d16e26cd1b960)
下面定义一个函数,以CBook类对象为参数,演示在按值传递函数参数时调用了复制构造函数。
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P148_36828.jpg?sign=1738874034-ZwysyHMsnYGMdKCvP3cODzfKfhEOyzoK-0-53a18cdeb86eff244a88972139bce9e6)
执行上述代码,结果如图5.3所示。
从图5.3中可以发现,执行“CBook book;”语句时调用了构造函数,输出了“构造函数被调用”的信息。执行“OutputBookInfo(book);”语句首先调用复制构造函数,输出“复制构造函数被调用”的信息,然后执行OutputBookInfo函数,输出m_BookName成员的信息。
注意
如果对OutputBookInfo函数进行修改,以引用类型作为函数参数,将不会执行复制构造函数。
【例5.9】 引用类型作为函数参数。(实例位置:资源包\TM\sl\5\4)
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P148_14725.jpg?sign=1738874034-Mf3FLaDodmcDTdshPknOdA6HgOp7yuK9-0-c624e6d484aa9f972effd7ddd431bcaf)
执行上述代码,结果如图5.4所示。
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P149_37015.jpg?sign=1738874034-FHQUpB76kOtXqBROO1dk5xsv1Sp1S3sE-0-1852ad9dbb090fa38218a3955180d635)
图5.3 复制构造函数1
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P149_37016.jpg?sign=1738874034-XH4auGnHx7jUxdo5eOu64qfSIZomyMvO-0-fd278b28fbee298a4400a4b6925aee22)
图5.4 复制构造函数2
从图5.4中可以发现,复制构造函数没有被调用。因为OutputBookInfo函数是以引用类型作为参数,函数参数按引用的方式传递,直接将实际参数的地址传递给函数,不涉及复制参数,所以没有调用复制构造函数。
技巧
编写函数时,尽量按引用的方式传递参数,这样可以避免调用复制构造函数,极大地提高了程序的执行效率。
2. 析构函数
在介绍完构造函数后,下面介绍一下析构函数。析构函数在对象超出作用范围或使用delete运算符释放对象时被调用,用于释放对象占用的空间。如果用户没有显式地提供析构函数,系统会提供一个默认的析构函数。析构函数也是以类名作为函数名,与构造函数不同的是,在函数名前添加一个“~”符号,标识该函数是析构函数。析构函数没有返回值,甚至void类型也不可以;析构函数也没有参数,因此不能够重载。这是析构函数与普通函数最大的区别。下面为CBook类添加一个析构函数。
【例5.10】 定义析构函数。(实例位置:资源包\TM\sl\5\5)
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P149_36959.jpg?sign=1738874034-tWt7xBATazUaR6v8nvLu4XXO77wjxU9T-0-548ccaeba3c89c4afa60e131f9d02165)
为了演示析构函数的调用情况,定义一个CBook类对象。
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P149_36989.jpg?sign=1738874034-A6yrY1XoDsPA1uYUdsoAChzdPITiAZkG-0-4271b9e91100e58b58eeffdcdd8ffde0)
执行上述代码,结果如图5.5所示。
上述代码中定义了一个CBook对象book,当执行“CBook book;”语句时将调用构造函数输出“构造函数被调用”的信息;然后执行“printf("定义一个CBook类对象\n");”语句输出一行信息;最后在函数结束时,也就是book对象超出了作用域时调用析构函数释放book对象,因此输出了“析构函数被调用”的信息。
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P149_37037.jpg?sign=1738874034-McrJWQwNZf4Fvwo1geFYaJDjMU0EiTwA-0-f575bbb223121eae5d9a83f4b098d08c)
图5.5 析构函数
5.1.4 内联成员函数
在定义函数时,可以使用inline关键字将函数定义为内联函数。在定义类的成员函数时,也可以使用inline关键字将成员函数定义为内联成员函数。
说明
其实对于成员函数来说,如果其定义是在类体中,即使没有使用inline关键字,该成员函数也被认为是内联成员函数。
例如,定义一个内联成员函数。
【例5.11】 定义内联成员函数。
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P150_37054.jpg?sign=1738874034-yn60hc55apWxF7pJpV3tWOPpApovDQfx-0-ec0fcc5502c2ffd563f19fc9d0c919ee)
在上述代码中,GetUsername函数即为内联成员函数,因为函数的定义处于类体中。此外,还可以使用inline关键字表示函数为内联成员函数。
【例5.12】 使用inline关键字定义内联成员函数。
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P150_37109.jpg?sign=1738874034-ZcG6m1wjvylPMSaSc8s46FGr09GiNoC1-0-48bbdb1f1e0859173c1716877004c7ea)
此外,还可以在类成员函数的实现部分使用inline关键字标识函数为内联成员函数。例如:
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P150_37169.jpg?sign=1738874034-dPYu8WZheiH8arWA01CGnIEmqxqTTPye-0-e76d3a75ceab36ee0642b4ff99bd129c)
说明
对于内联成员函数来说,程序会在函数调用的地方直接插入函数代码,如果函数体语句较多,将会导致程序代码膨胀。因此,将类的析构函数定义为内联成员函数,可能会导致潜在的代码膨胀。
分析下面的代码(假设CBook类的析构函数为内联成员函数)。
【例5.13】 将类的析构函数定义为内联成员函数,可能会导致潜在的代码膨胀。
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P151_37261.jpg?sign=1738874034-CiFSdO3lIE586xVWzQ2MxjY40LrFucrD-0-1b86376eebd1f38c5208491ccf2b82ae)
由于CBook类的析构函数是内联成员函数,因此上述代码在每一个return语句之前,析构函数均会被展开。因为return语句表示当前函数调用结束,所以book对象的生命期也就结束了,自然调用其析构函数。
根据上述分析,main函数中switch语句的编写是非常不明智的,下面对其进行修改,将return语句替换为break语句。
【例5.14】 switch语句的注意事项。
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P151_37366.jpg?sign=1738874034-JO2bXY19wlJ0VNQyNAOGecBF7tk4g5gM-0-b299d23250093f364db1be160a8cd1c8)
通过修改switch语句,避免了可能产生的代码膨胀,这也是使用switch语句应该注意的事项。
5.1.5 静态类成员
本节之前所定义的类成员都是通过对象来访问的,不能通过类名直接访问。如果将类成员定义为静态类成员,则允许使用类名直接访问。静态类成员是在类成员定义前使用static关键字标识。
【例5.15】 定义静态数据成员。
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P152_37499.jpg?sign=1738874034-6Dj8j8FhPXGomRl8zZFpxkA6AiQxgksI-0-2a5abb1ad49ec46dd03eafcca9d9223e)
在定义静态数据成员时,通常需要在类体外部对静态数据成员进行初始化。例如:
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P152_37524.jpg?sign=1738874034-T3WAJPnUi9VHgwpd1kgfkgTW8UQ5Lzvn-0-33f2df9bfaf7e5da77ccadb02566b3b5)
对于静态数据成员来说,不仅可以通过对象访问,还可以直接使用类名访问。例如:
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P152_37529.jpg?sign=1738874034-YogmGJlrvMaZY3w5SYLdEouazxxP3qly-0-2e1a5eb6def074cbf4413582fa0108e9)
在一个类中,静态数据成员是被所有的类对象所共享的。这就意味着无论定义多少个类对象,类的静态数据成员只有一份;同时,如果某一个对象修改了静态数据成员,其他对象的静态数据成员(实际上是同一个静态数据成员)也将改变。
【例5.16】 类对象共享静态数据成员。(实例位置:资源包\TM\sl\5\6)
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P153_37580.jpg?sign=1738874034-IUvzGmccMxj3qhTsYtT3JnBfGbjxt7dF-0-2fb044059a0568af772b4e4d4f682640)
执行上述代码,结果如图5.6所示。
由于静态数据成员m_Price被所有CBook类对象所共享,因此Book对象修改了m_Price成员,将影响到vcBook对象对m_Price成员的访问。
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P153_37709.jpg?sign=1738874034-wXXsBKHsJn7nv10mLmziHz8hbD7GrkML-0-05371b71aec0078d9b2ebc10a62f24dd)
图5.6 静态数据成员
对于静态数据成员,还需要注意以下几点。
(1)静态数据成员可以是当前类的类型,而其他数据成员只能是当前类的指针或引用类型。
在定义类成员时,对于静态数据成员,其类型可以是当前类的类型,而非静态数据成员则不可以,除非数据成员的类型为当前类的指针或引用类型。
【例5.17】 静态数据成员可以是当前类的类型。
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P153_37620.jpg?sign=1738874034-eK6nzfYpdirOjgKUXKFwXnpmfc2uV5Wz-0-455a91e8368089b593039c6f1ee59164)
(2)静态数据成员可以作为成员函数的默认参数。
在定义类的成员函数时,可以为成员函数指定默认参数,其参数的默认值也可以是类的静态数据成员,但是普通的数据成员则不能作为成员函数的默认参数。
【例5.18】 静态数据成员可以作为成员函数的默认参数。
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P153_37660.jpg?sign=1738874034-UkbzMvZcsBBBu50QvCv7pZz7dmUYsWez-0-f935e69b9581dff01da0c04690ca22da)
在介绍完类的静态数据成员后,下面介绍类的静态成员函数。定义类的静态成员函数与定义普通的成员函数类似,只是在成员函数前添加static关键字。例如:
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P154_37746.jpg?sign=1738874034-40INATsUA58C9daJpYRJDtbp98HpP5Oa-0-ab89ce2df8107a161c83254fbf810bca)
类的静态成员函数只能访问类的静态数据成员,而不能访问普通的数据成员。例如:
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P154_37751.jpg?sign=1738874034-KO6rKDqKVVXcK6W1wJWGsA6mPpAiTor8-0-f06a2992722c72216ed2f35e3b8ccf02)
在上述代码中,语句“printf("%d\n",m_Pages);”是错误的,因为m_Pages是非静态数据成员,不能在静态成员函数中访问。
注意
静态成员函数不能定义为const成员函数,即静态成员函数末尾不能使用const关键字。
例如,下面静态成员函数的定义是非法的。
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P154_37806.jpg?sign=1738874034-oF1yCxxnVts6oKnZskrEx2sgbizyhUpO-0-b419cf778aae07f72ff8bd27d7f2a1ab)
在定义静态成员函数时,如果函数的实现代码处于类体之外,则在函数的实现部分不能再标识static关键字。例如,下面的函数定义是非法的。
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P154_37811.jpg?sign=1738874034-TxXpbyfzI1TZfCcdFRFZTQO8T30eOyjy-0-77ed2fd42218bb3d6c58ca79c05e7f63)
上述代码如果去掉static关键字则是正确的。
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P154_37831.jpg?sign=1738874034-k77YQ6XyRz6ZzorLFe9YCROpvSSjS8lP-0-9031e40d48c473626b63073fbe58738c)
5.1.6 隐藏的this指针
对于类的非静态成员,每一个对象都有自己的一份备份,即每个对象都有自己的数据成员和成员函数。
【例5.19】 每一个对象都有自己的一份备份。(实例位置:资源包\TM\sl\5\7)
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P155_37887.jpg?sign=1738874034-wDWjbAvEVv05D8MArsfIPtau86yxGBEb-0-bb0738e177a76fb9e3a2e6dd31cce66b)
执行上述代码,结果如图5.7所示。
从图5.7中可以发现,vbBook和vcBook两个对象均有自己的数据成员m_Pages,在调用OutputPages成员函数时,输出的均是自己的数据成员。在OutputPages成员函数中只是访问了m_Pages数据成员,那么每个对象在调用OutputPages方法时是如何区分自己的数据成员的呢?答案是通过this指针。在每个类的成员函数(非静态成员函数)中都隐含包含一个this指针,指向被调用对象的指针,其类型为当前类类型的指针类型(在const方法中,为当前类类型的const指针类型)。当vbBook对象调用OutputPages成员函数时,this指针指向vbBook对象;当vcBook对象调用OutputPages成员函数时,this指针指向vcBook对象。在OutputPages成员函数中,用户可以显式地使用this指针访问数据成员。例如:
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P155_37997.jpg?sign=1738874034-pob8S6koRdG1hvmN4TFycG687B7hPD75-0-66bccac470e18761978b982bac528a6c)
图5.7 访问对象的数据成员
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P155_37977.jpg?sign=1738874034-DXr4me4XkIrxBckn2ppWFOspxMsebtRb-0-8e0627f643b6191c4896658ed63e8058)
实际上,编译器为了实现this指针,在成员函数中自动添加了this指针用于对数据成员或方法的访问,类似于上面的OutputPages方法。
说明
为了将this指针指向当前调用对象,并在成员函数中能够使用,在每个成员函数中都隐含包含一个this指针作为函数参数,并在函数调用时将对象自身的地址隐含作为实际参数传递。
以OutputPages成员函数为例,编译器将其定义为:
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P156_38014.jpg?sign=1738874034-6ItSHAKWwTwDP9g3HEzSHmqD85ZArfft-0-748fb7fe34d2c4dd3c975f515978bb1d)
在对象调用成员函数时,传递对象的地址到成员函数中。以“vc.OutputPages();”语句为例,编译器将其解释为“vbBook.OutputPages(&vbBook);”。这样就使得this指针合法,并能够在成员函数中使用。
5.1.7 运算符重载
定义两个整型变量后,可以对两个整型变量进行加运算。如果定义两个类对象,它们能否进行加运算呢?答案是不可以。两个类对象是不能直接进行加运算的,因此下面的语句是非法的。
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P156_38034.jpg?sign=1738874034-d0ZkGXy3jFBr0hHgC4fBaGthYBr4Jzla-0-f7d912688b50cbfb954e9fd291ff92bb)
但是,如果对“+”运算符进行重载,则可以实现两个类对象的加运算。
说明
运算符重载是C++语言提供的一个重要特性,允许用户对一些编译器提供的运算符进行重载,以实现特殊的含义。
下面以为CBook类添加运算符重载函数为例来演示如何进行运算符重载。
【例5.20】 为类添加运算符重载函数。
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P156_38044.jpg?sign=1738874034-DwwaRDvmIMYXocnUdALQrpSt7NoFpcWu-0-46b5ea2e5977fd51cd778e47923e5900)
在上述代码中,为CBook类实现了“+”运算符的重载。运算符重载需要使用operator关键字,其后是需要重载的运算符,参数及返回值根据实际需要来设置。通过为CBook类实现“+”运算符重载,允许用户实现两个CBook对象的加运算。例如:
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P157_38167.jpg?sign=1738874034-udvdGFO50f35FnpSjcSxxFUbClyD7sz8-0-ca3341a5c30a49b4e0e33c36e5fcad72)
如果用户想要实现CBook对象与一个整数相加,可以通过修改重载运算符的参数来实现。例如:
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P157_38177.jpg?sign=1738874034-D9ABvJ0gwnU6D7Co9wNu5mOJkF3Ht6Rt-0-50e53946fc769567d5a848a3b5d3be2e)
通过修改运算符的参数为整数类型,可以实现CBook对象与整数相加。如下面的代码是合法的:
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P157_38207.jpg?sign=1738874034-lUdBPu1z313JEnYH7Jcw0RWwVwm6GCfW-0-53722e77eb64bcf67f3d737a52d09268)
两个整型变量相加,用户可以调换加数和被加数的顺序,因为加法符合交换律。但是,通过重载运算符实现两个不同类型的对象相加则不可以,因此下面的语句是非法的。
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P157_38227.jpg?sign=1738874034-aKYhf9EsGodz691mn6cYaajVzsQP3N7K-0-0e5df6191f5f7efbe33a08d2a4922925)
对于“++”和“--”运算符,由于涉及前置运算和后置运算,在重载这类运算符时如何区分是前置运算还是后置运算呢?默认情况下,如果重载运算符没有参数,则表示是前置运算。例如:
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P157_38232.jpg?sign=1738874034-JxO0d6Vjj9DX5DO51zOJBxnVzIAWE1nL-0-d3732631a89e77bed8fb591d0000c215)
如果重载运算符使用了整数作为参数,则表示是后置运算。此时的参数值可以被忽略,它只是一个标识,标识后置运算。
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P157_38252.jpg?sign=1738874034-PRpZE0PhyGeFrlPpjk3nwaQ0Zg2xif9H-0-b3fc4aedf51318ef39a7fff7e2b0952a)
默认情况下,将一个整数赋值给一个对象是非法的,可以通过重载赋值运算符“=”将其变为合法的。例如:
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P158_38293.jpg?sign=1738874034-9r74vYncCr8nJX4P3Z21H5fy7Uyr9Jhx-0-565f7cc578a7158ec65d624e3d0a1319)
通过重载赋值运算符,可以进行如下形式的赋值。
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P158_38313.jpg?sign=1738874034-WHfvqzAT5iTVDVhHdJ4meOTEiU5R1pRP-0-059636b8b13343fbd4fdc7196caaf494)
此外,用户还可以通过重载构造函数将一个整数赋值给一个对象。例如:
【例5.21】 通过重载构造函数将一个整数赋值给一个对象。
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P158_38323.jpg?sign=1738874034-7akMP0fcZNZuSElIUuzjieilknyDFUOj-0-d92502211abfb6d98470bf5eb69b564d)
上述代码定义了一个重载的构造函数,以一个整数作为函数参数,这样同样可以将一个整数赋值给一个CBook类对象。例如:
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P158_38408.jpg?sign=1738874034-xZ4GXExLvXVmrAvTnSJXqM9rRwRlM9sF-0-dd0903073f6638d67cb89d54b7f14bc0)
语句“vbBook = 200;”将调用构造函数CBook(int page)重新构造一个CBook对象,将其赋值给vbBook对象。
说明
无论是重载赋值运算符还是重载构造函数,都无法实现反向赋值,即将一个对象赋值给一个整型变量。
为了实现将一个对象赋值给一个整型变量的功能,C++提供了转换运算符。
【例5.22】 转换运算符。
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P158_38418.jpg?sign=1738874034-XvYnyP1jgKt0pk3WIIplltqyQ4pyWCOo-0-9a1d83765d77cea66b121d5231a687a6)
上述代码在定义CBook类时定义了一个转换运算符int,用于实现将CBook类赋值给整型变量。转换运算符由关键字operator开始,其后是转换为的数据类型。在定义转换运算符时,注意operator关键字前没有数据类型,虽然转换运算符实际返回了一个转换后的值,但是不能指定返回值的数据类型。下面的代码演示了转换运算符的应用。
【例5.23】 转换运算符的应用。(实例位置:资源包\TM\sl\5\8)
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P159_38517.jpg?sign=1738874034-jnorYZ41F1Rjsu3gjIqIyEBMfXZNTNxt-0-0442e0e9d3cb2223bfa838856e081b34)
执行上述代码,结果如图5.8所示。
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P159_38556.jpg?sign=1738874034-lQRsDCrKps7Uh3LhWRQJWDDbXkhIbaps-0-c80de099cadac98034574b0ce6b38757)
图5.8 转换运算符
从图5.8中可以发现,语句“int page = vbBook;”是将vbBook对象的m_Pages数据成员赋值给了page变量,因此page变量的值为300。
用户在程序中重载运算符时,需要遵守如下规则和注意事项。
(1)并不是所有的C++运算符都可以重载。
C++中的大多数运算符都可以进行重载,但是“::”“?”“:”和“. ”运算符不能够被重载。
(2)运算符重载存在如下限制。
不能构建新的运算符。
不能改变原有运算符操作数的个数。
不能改变原有运算符的优先级。
不能改变原有运算符的结合性。
不能改变原有运算符的语法结构。
(3)运算符重载遵循以下基本准则。
一元操作数可以是不带参数的成员函数,或者是带一个参数的非成员函数。
二元操作数可以是带一个参数的成员函数,或者是带两个参数的非成员函数。
“=”“[]”“->”和“()”运算符只能定义为成员函数。
“->”运算符的返回值必须是指针类型或者能够使用“->”运算符类型的对象。
重载“++”和“--”运算符时,带一个int类型参数,表示后置运算,不带参数表示前置运算。
5.1.8 友元类和友元方法
类的私有方法,只有在该类中允许访问,其他类是不能访问的。在开发程序时,如果两个类的耦合度比较紧密,在一个类中访问另一个类的私有成员会带来很大的方便。C++语言提供了友元类和友元方法(或者称为友元函数)来实现访问其他类的私有成员。当用户希望另一个类能够访问当前类的私有成员时,可以在当前类中将另一个类作为自己的友元类,这样在另一个类中即可访问当前类的私有成员。
【例5.24】 定义友元类。
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P160_38573.jpg?sign=1738874034-kAh9jP5DXwnm8W2wv4GTW5yQOng9tZmy-0-46074246a1d9746c9b1dbd56d0a3892f)
在上述代码中,定义CItem类时使用了friend关键字将CList类定义为CItem类的友元,这样CList类中的所有方法就都可以访问CItem类中的私有成员了。在CList类的OutputItem方法中,语句“m_Item.OutputName()”演示了调用CItem类的私有方法OutputName。
在开发程序时,有时需要控制另一个类对当前类的私有成员的访问。例如,只允许CList类的某个成员访问CItem类的私有成员,而不允许其他成员函数访问CItem类的私有数据,此时可以通过定义友元函数来实现。在定义CItem类时,可以将CList类的某个方法定义为友元方法,这样就限制了只有该方法允许访问CItem类的私有成员。
【例5.25】 定义友元方法。(实例位置:资源包\TM\sl\5\9)
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P161_38758.jpg?sign=1738874034-j4SbGXmzC6B9XRWwp2mfstsAciME0Wgi-0-bc57ceefdca72dcbe6e4880a63725aef)
在上述代码中,定义CItem类时使用了friend关键字将CList类的OutputItem方法设置为友元函数,在CList类的OutputItem方法中访问了CItem类的私有方法OutputName。执行上述代码,结果如图5.9所示。
注意
对于友元函数来说,不仅可以是类的成员函数,还可以是一个全局函数。
下面的代码在定义CItem类时,将一个全局函数定义为友元函数,这样在全局函数中即可访问CItem类的私有成员。
【例5.26】 将全局函数定义为友元函数。(实例位置:资源包\TM\sl\5\10)
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P162_39031.jpg?sign=1738874034-wHJQCnlW6DTsw3QBDTftwtvQprQktXQA-0-a619a05f9a071eac55fcee03d6af4769)
执行上述代码,结果如图5.10所示。
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P163_39309.jpg?sign=1738874034-Jyscsuf1jjPsJmdTRENNu20KPRWcUzvK-0-31904d55c78b4186997b3849749fe00e)
图5.9 友元函数
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P163_39312.jpg?sign=1738874034-sZGeW3h7vxQRHXALHAtKh5lP60LBL1Oy-0-e7bd21cf061180e848a4c990123c6fbe)
图5.10 全局友元函数
5.1.9 类的继承
继承是面向对象的主要特征(此外还有封装和多态)之一,它使一个类可以从现有类中派生,而不必重新定义一个新类。例如,定义一个员工类,其中包含员工ID、员工姓名、所属部门等信息;再定义一个操作员类,通常操作员属于公司的员工,因此该类也包含员工ID、员工姓名、所属部门等信息,此外还包含密码信息、登录方法等。如果当前已经定义了员工类,则在定义操作员类时可以从员工类派生一个新的员工类,然后向其中添加密码信息、登录方法等即可,而不必重新定义员工ID、员工姓名、所属部门等信息,因为它已经继承了员工类的信息。下面的代码演示了操作员类是如何继承员工类的。
【例5.27】 类的继承。(实例位置:资源包\TM\sl\5\11)
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P163_39241.jpg?sign=1738874034-z3m9BMt4OsBlvInUfJXnLKdOUAsVRmI0-0-bd44d1727f1f1558f6d509621358c7fc)
上述代码在定义COperator类时使用了“:”运算符,表示该类派生于一个基类;public关键字表示派生的类型为公有型;其后的CEmployee表示COperator类的基类,也就是父类。这样,COperator类将继承CEmployee类的所有非私有成员(private类型成员不能被继承)。
说明
当一个类从另一个类继承时,可以有3种派生类型,即公有型(public)、保护型(protected)和私有型(private)。派生类型为公有型时,基类中的public数据成员和方法在派生类中仍然是public,基类中的protected数据成员和方法在派生类中仍然是protected。派生类型为保护型时,基类中的public、protected数据成员和方法在派生类中均为protected。派生类型为私有型时,基类中的public、protected数据成员和方法在派生类中均为private。
下面定义一个操作员对象,演示通过操作员对象调用操作员类的方法以及调用基类—员工类的方法。
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P164_35689.jpg?sign=1738874034-nJnmO3zQBWO4tUfjkqmPGeVMN6gFjCgJ-0-26261fc775e92db530d473e56b22b86c)
执行上述代码,结果如图5.11所示。
注意
用户在父类中派生子类时,可能存在一种情况,即在子类中定义了一个与父类的方法同名的方法,称之为子类隐藏了父类的方法。
例如,重新定义COperator类,添加一个OutputName方法。
【例5.28】 子类隐藏了父类的方法。(实例位置:资源包\TM\sl\5\12)
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P165_39521.jpg?sign=1738874034-SlJQRSMXgY1IIOEywHQQ4ZQEIAAe5ZOM-0-36c7407354fb3ce94ed51e33a3ffb29b)
定义一个COperator类对象,调用OutputName方法。
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P165_39636.jpg?sign=1738874034-Pb2DWk2wyVn4icDWcSn9FHk1d9sVl6N5-0-1e3829fe2ee654773ea4042c4307b350)
执行上述代码,结果如图5.12所示。
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P165_39687.jpg?sign=1738874034-rJnHO0qtzshP2EQHOmMyRLbNginr3PkP-0-554d68aabc47b1e0d8efe2748645fc8a)
图5.11 访问父类方法
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P165_39688.jpg?sign=1738874034-FR2X6VCl3geLDEN33ihd8AYLiKfVBMpo-0-d554a611f4313184bc3a3c692b627ebb)
图5.12 隐藏基类方法
从图5.12中可以发现,语句“optr.OutputName();”调用的是COperator类的OutputName方法,而不是CEmployee类的OutputName方法。如果用户想要访问父类的OutputName方法,需要显式使用父类名。例如:
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P166_39705.jpg?sign=1738874034-iuM2TtZTUybraDSvVOCd66lxoQ09qZYT-0-bb024280eb44698f9501a2da9474ba31)
如果子类隐藏了父类的方法,则父类中所有同名的方法(重载方法)均被隐藏。因此,例5.29中黑体部分代码的访问是错误的。
【例5.29】 如果子类隐藏了父类的方法,则父类中所有同名的方法均被隐藏。
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P166_39725.jpg?sign=1738874034-jbzMx19Q6vfcyj2j4eTLPn52K59ApzW3-0-a72ce0e826d9894d8d2c1d531131ace1)
在上述代码中,CEmployee类中定义了重载的OutputName方法,而在COperator类中又定义了一个OutputName方法,导致父类中的所有同名方法被隐藏。因此,语句“optr.OutputName("MR");”是错误的。如果用户想要访问被隐藏的父类方法,依然需要指定父类名称。例如:
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P167_45678.jpg?sign=1738874034-dzj1pIJjjgGgn7hJeB2omaTxXjPXJU4I-0-a3e54f5bf2fa442194578e1f69d86a1b)
在派生完一个子类后,可以定义一个父类的类型指针,通过子类的构造函数为其创建对象。例如:
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P167_40025.jpg?sign=1738874034-IvfU2uwLQ98CKw9D0rx6ZknrlAi437Xi-0-9fd3fd2f487c742cdcb12860761e4f8a)
如果使用pWorder对象调用OutputName方法,如执行“pWorker->OutputName();”语句,则执行的是CEmployee类的OutputName方法还是COperator类的OutputName方法呢?答案是调用CEmployee类的OutputName方法。编译器对OutputName方法进行的是静态绑定,即根据对象定义时的类型来确定调用哪个类的方法。由于pWorker属于CEmployee类型,因此调用的是CEmployee类的OutputName方法。那么是否有方法将“pWorker->OutputName();”语句改为执行COperator类的OutputName方法呢?通过定义虚方法可以实现这一点。
说明
在定义方法(成员函数)时,在方法的前面使用virtual关键字,该方法即为虚方法。使用虚方法可以实现类的动态绑定,即根据对象运行时的类型来确定调用哪个类的方法,而不是根据对象定义时的类型来确定调用哪个类的方法。
下面的代码修改了CEmployee类的OutputName方法,使其变为虚方法。
【例5.30】 利用虚方法实现动态绑定。(实例位置:资源包\TM\sl\5\13)
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P167_40030.jpg?sign=1738874034-9IjHyKJpTll7ISchkHiWkIllXSBgM26T-0-c219a89d1a2e5a917e378f80d541244a)
在上述代码中,CEmployee类中定义了一个虚方法OutputName,在子类COperator类中改写了OutputName方法,其中COperator类中的OutputName方法仍为虚方法,即使没有使用virtual关键字。下面定义一个CEmployee类型的指针,调用COperator类的构造函数构造对象。
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P168_40193.jpg?sign=1738874034-r2v1qqiWNP1ngjWIzQRsOaVVQj95TCIu-0-8b7844ec632f1fc1fb917d5c4a752ffd)
执行上述代码,结果如图5.13所示。
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P168_40231.jpg?sign=1738874034-u7kQSGwhx0sv7sQDeymEiVKbDzKLz7wu-0-e25afb6500a77d2aed741d767e9cf31b)
图5.13 虚方法
从图5.13中可以发现,“pWorker->OutputName();”语句调用的是COperator类的OutputName方法。
注意
在C++语言中,除了能够定义虚方法外,还可以定义纯虚方法,也就是通常所说的抽象方法。一个包含纯虚方法的类被称为抽象类,抽象类是不能够被实例化的,通常用于实现接口的定义。
下面的代码演示了纯虚方法的定义。
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P169_40249.jpg?sign=1738874034-SAndl2t0FkJ9Ro90SaxpPFXQCpWj4Zv4-0-fa7334a0f50c4f2bd5c900b5315c68c9)
在上述代码中,为CEmployee类定义了一个纯虚方法OutputName。纯虚方法的定义是在虚方法定义的基础上在末尾添加“= 0”。包含纯虚方法的类是不能够实例化的,因此下面的语句是错误的。
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P169_40294.jpg?sign=1738874034-aJVHq34yJZdOWxrxXukNLOc3ZGXXqFaQ-0-eae7d480059995b5ad12c8239e4f1e21)
抽象类通常用于作为其他类的父类,从抽象类派生的子类如果也是抽象类,则子类必须实现父类中的所有纯虚方法。
【例5.31】 实现抽象类中的方法。(实例位置:资源包\TM\sl\5\14)
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P169_40299.jpg?sign=1738874034-6cnReQ7Gov2LJYvcYVeM2KT7ORFtSyzG-0-1ff2b4e7f244e8c642e12d58ecb19f61)
上述代码从CEmployee类派生了两个子类,即COperator和CSystemManager。这两个类分别实现了父类的纯虚方法OutputName。下面编写一段代码演示抽象类的应用。
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P170_40459.jpg?sign=1738874034-OaEvb0YB8dO3rE6bYUlHBPH3gQaHMlBX-0-5fa635aef1254652fd2b9e59ec6f8002)
执行上述代码,结果如图5.14所示。
从图5.14中可以发现,同样的一条语句“pWorker->OutputName();”,由于pWorker指向的对象不同,其行为也不同。
下面分析一下子类对象的创建和释放过程。当从父类派生一个子类后,定义一个子类的对象时,它将依次调用父类的构造函数、当前类的构造函数来创建对象。在释放子类对象时,先调用的是当前类的析构函数,然后是父类的析构函数。下面的代码说明了这一点。
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P170_40614.jpg?sign=1738874034-OHHbwcpTPaRKNcbLjfyWP64GtW3bQQvu-0-47ee95ff096332bd811cef0a4c1028a6)
图5.14 纯虚方法
【例5.32】 子类对象的创建和释放过程。(实例位置:资源包\TM\sl\5\15)
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P170_40524.jpg?sign=1738874034-BVZNW1f5Z7Rm8NFVqHMCc4frnhlcsDgt-0-b6bcb0b42486f7c98755531fd1211d34)
执行上述代码,结果如图5.15所示。
从图5.15中可以发现,在定义COperator类对象时,调用的是父类CEmployee的构造函数,然后是COperator类的构造函数。子类对象的释放过程则与其构造过程恰恰相反,先调用自身的析构函数,然后再调用父类的析构函数。
在分析完对象的构建、释放过程后,考虑这样一种情况—定义一个基类类型的指针,调用子类的构造函数为其构建对象,当对象释放时,是先调用父类的析构函数还是先调用子类的析构函数,再调用父类的析构函数呢?答案是如果析构函数是虚函数,则先调用子类的析构函数,然后再调用父类的析构函数;如果析构函数不是虚函数,则只调用父类的析构函数。可以想象,如果在子类中为某个数据成员在堆中分配了空间,父类中的析构函数不是虚方法,上述情况将使子类的析构函数不会被调用,其结果是对象不能被正确地释放,导致内存泄漏的产生。因此,在编写类的析构函数时,析构函数通常是虚函数。
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P171_40756.jpg?sign=1738874034-6nQUJZtcUNfhRZysgEUJq9sFT0BWWsjw-0-bd0200cedf42c5f0bda14f3af7bad17e)
图5.15 构造函数调用顺序
说明
前面所介绍的子类的继承方式属于单继承,即子类只从一个父类继承公有的和受保护的成员。与其他面向对象语言不同,C++语言允许子类从多个父类继承公有的和受保护的成员,称之为多继承。
例如,鸟能够在天空飞翔,鱼能够在水里游,而水鸟既能够在天空飞翔,又能够在水里游,则在定义水鸟类时,可以将鸟和鱼同时作为其基类。下面的代码演示了多继承的应用。
【例5.33】 实现多继承。(实例位置:资源包\TM\sl\5\16)
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P171_40716.jpg?sign=1738874034-99XvFPtADJgButp6rG923c4RpN1wzxs1-0-4bcc104509fb08080b203436b69fc75d)
执行上述代码,结果如图5.16所示。
上述代码定义了鸟类CBird和鱼类CFish,然后从鸟类和鱼类派生了一个子类—水鸟类CWaterBird,水鸟类自然继承了鸟类和鱼类所有公有和受保护的成员,因此CWaterBird类对象能够调用FlyInSky和SwimInWater方法。在CBird类中提供了一个Breath方法,在CFish类中同样提供了Breath方法,如果CWaterBird类对象调用Breath方法,将会执行哪个类的Breath方法呢?答案是将会出现编译错误,编译器将产生歧义,不知道具体调用哪个类的Breath方法。为了让CWaterBird类对象能够访问Breath方法,需要在Breath方法前具体指定类名。例如:
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P172_40953.jpg?sign=1738874034-NLaS8u0d1qv0VNKyNTHXbk1cf9Rd4mOl-0-94559cfe0735dfe5193ea1dfdf82a136)
图5.16 多继承
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P172_40943.jpg?sign=1738874034-MBVgyK3mECkY7KU3orGmm5PFDZEr8xW5-0-7adc73878a5e55066513e919f485c3d6)
在多继承中存在这样一种情况,假如CBird类和CFish类均派生于同一个父类,如CAnimal类,那么当从CBird类和CFish类派生子类CWaterBird时,在CWaterBird类中将存在两个CAnimal类的备份。能否在派生CWaterBird类时,使其只存在一个CAnimal基类?为了解决该问题,C++语言提供了虚继承的机制。下面的代码演示了虚继承的使用。
【例5.34】 虚继承。(实例位置:资源包\TM\sl\5\17)
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P173_40970.jpg?sign=1738874034-0np8udN43jAlzuFDVEJlq2QholqFoUr7-0-6f838cb8b410c1fbaa19eb8a4e0cbd23)
执行上述代码,结果如图5.17所示。
在上述代码中,定义CBird类和CFish类时使用了关键字virtual从基类CAnimal派生而来。实际上,虚继承对于CBird类和CFish类没有多少影响,但是却对CWaterBird类产生了很大影响。CWaterBird类中不再有两个CAnimal类的备份,而只存在一个CAnimal的备份,图5.17充分说明了这一点。
通常在定义一个对象时,先依次调用基类的构造函数,最后才调用自身的构造函数。但是对于虚继承来说,情况有些不同。在定义CWaterBird类对象时,先调用基类CAnimal的构造函数,然后调用CBird类的构造函数,这里CBird类虽然为CAnimal的子类,但是在调用CBird类的构造函数时将不再调用CAnimal类的构造函数,此操作被忽略了,对于CFish类也是同样的道理。
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P174_41302.jpg?sign=1738874034-cSJwu5CCPNtbnfu4MTZtnS4JjzQYmrmP-0-f936f509669119e258ce10e488705c77)
图5.17 虚继承
说明
在程序开发过程中,多继承虽然带来了很多方便,但是很少有人愿意使用它,因为多继承会带来很多复杂的问题,并且多继承能够完成的功能,通过单继承同样也可以实现。如今流行的C#、Delphi和Java等面向对象语言只采用了单继承,而没有提供多继承的功能是经过设计者充分考虑的。因此,读者在开发应用程序时,如果能够使用单继承实现,尽量不要使用多继承。
5.1.10 类域
在定义类时,每个类都存在一个类域,类的所有成员均处于类域中。当程序中使用点运算符(.)和箭头运算符(->)访问类成员时,编译器会根据运算符前面的对象的类型来确定其类域,并在其类域中查找成员。如果使用域运算符(::)访问类成员,编译器将根据运算符前面的类名来确定其类域,查找类成员。当用户通过对象访问一个不属于类成员的“成员”时,编译器将提示其“成员”没有在类中定义,因为在类域中找不到该成员。
在类中如果有自定义的类型,则自定义类型的声明顺序是很重要的。在定义类的成员时如果需要使用自定义类型,通常将自定义类型放置在类成员定义的前方,否则将出现编译错误。例如,下面的类定义将是非法的。
【例5.35】 自定义类型应放置在类成员定义的前方。
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P175_41319.jpg?sign=1738874034-gkFfI3RYaSIbwosv0R4Q7kfYHLLWEyJN-0-773291dfadb1576e0ccd0ed5c79e3a34)
上述代码中应将自定义类型UINT的定义放置在m_Wage数据成员定义的前方。下面的类定义是正确的。
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P175_41384.jpg?sign=1738874034-ShJka4lfykOQDVMMnm12yuEq1rTp596M-0-3b2eeb635f46f3cfbb462a2231c33bb3)
说明
如果在类中自定义了一个类型,在类域内该类型将被用来解析成员函数参数的类型名。
5.1.11 嵌套类
C++语言允许在一个类中定义另一个类,称之为嵌套类。例如,下面的代码在定义CList类时,在内部又定义了一个嵌套类CNode。
【例5.36】 定义嵌套类。
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P176_41480.jpg?sign=1738874034-RtIZAPxpvt7shDrkF0qxTCMlH7HILa25-0-33d04dfa2b74929054895a4751dea1de)
在上述代码中,嵌套类CNode中不仅定义了一个私有成员m_Tag,还定义了一个公有成员m_Name。对于外围类CList来说,通常它不能够访问嵌套类的私有成员,虽然嵌套类是在其内部定义的。但是,上述代码在定义CNode类时将CList类作为自己的友元类,这便使得CList类能够访问CNode类的私有成员。
说明
对于内部的嵌套类来说,只允许其在外围的类域中使用,在其他类域或者作用域中是不可见的。
例如,下面的定义是非法的。
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P176_14785.jpg?sign=1738874034-seMWi7GvEnnfYOjS5UQjr1oH8FMVKMyk-0-dfbdc8a460b519a76b68ac458105246e)
上述代码在main函数的作用域中定义了一个CNode对象,导致CNode没有被声明的错误。对于main函数来说,嵌套类CNode是不可见的,但是可以通过使用外围的类域作为限定符来定义CNode对象。下面的定义是合法的。
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P177_41664.jpg?sign=1738874034-xXCiyTxUhZifJaDhzhhsqgFy8OoyEvv1-0-0a67867716262211e1c7b8f16cb45987)
上述代码通过使用外围类域作为限定符访问到了CNode类。但是这样做通常是不合理的,也是有限制条件的。因为既然定义了嵌套类,通常不允许在外界访问,这违背了使用嵌套类的原则。其次,在定义嵌套类时,如果将其定义为私有的或受保护的,即使使用外围类域作为限定符,外界也无法访问嵌套类。
5.1.12 局部类
类的定义也可以放置在函数中,这样的类称为局部类。
【例5.37】 定义局部类。
![](https://epubservercos.yuewen.com/771DC2/15825992505222206/epubprivate/OEBPS/Images/Figure-P177_41689.jpg?sign=1738874034-zEPlm9qDkYw11IyWCNqvowd4z8nZtt0w-0-f3d926af35464f724ac14e78e0c7f98c)
上述代码在LocalClass函数中定义了一个CBook类,该类被称为局部类。对于局部类CBook来说,在函数之外是不能够被访问的,因为局部类被封装在了函数的局部作用域中。在局部类中,用户也可以再定义一个嵌套类,其定义形式与5.1.11节介绍的嵌套类完全相同,在此不再赘述。