你好,C++(32) 类是对现实世界的抽象和描述 6.2.1 类的声明和定义
6.2 类:当C++爱上面向对象
类这个概念是面向对象思想在C++中的具体体现:它既是封装的结果,同时也是继承和多态的载体。因此,要想学习C++中的面向对象程序设计,也就必须从“类”开始。
6.2.1 类的声明和定义
面向对象思想把现实世界中的所有事物都看成是对象,而类是对所有相同类型对象的抽象,是对它们总体的一个描述。比如,学校有很多老师,张老师、李老师、王老师,虽然每个老师各不相同,是不同的对象个体。但他们都是老师这一类型的对象,有着共同的属性(都有姓名、职务)和相同的行为(都能上课、批改作业)。我们把某一类型对象的共同属性和相同行为抽象出来,分别用变量和函数加以描述,然后把这些变量和函数用类这个概念封装起来,就成了可以用来描述这一类对象的新的数据类型,这种新的数据类型因此也被称为类。在C++中,声明一个类的语法格式如下:
class 类名 { public : // 公有成员,通常用来描述这类对象的相同行为 protected: // 保护型成员 private: // 私有成员,通常用来描述这类对象的共同属性 }; // 注意这里有个分号表示类的结束
其中,class是C++中用以声明类的关键字,其后跟所要声明的类的名字,通常是某个可以概括这一类对象的名词。其命名规则类似于之前介绍的变量名命名规则。这里,我们要定义一个类来描述“老师”这类对象,所以我们用“Teacher”作为这个类的名字。
在后面的章节中,我们还将学到C++中的类还有基类与派生类之分,它是面向对象思想的继承机制在类当中的体现。如果这个类是从某个基类继承而来的,我们在“class 类名”后面还要加上这个类的继承方式(public、protected或private)以及它所继承的基类的名字。这样,声明一个类的语法格式相应地就变为:
class 类名:继承方式 基类名 { // 成员变量和成员函数的声明… };
如果某个类没有继承关系,则类声明中的继承方式可以省略。这里的Teacher类本身就是基类,并不是由其他类继承而来,所以这里继承方式应当省略。
完成类的名字及继承关系的定义后,就可以开始在类的主体中描述这个类的属性和行为了。对象的属性属于数据,所以我们在类声明中定义一些变量来描述对象的属性。比如,“老师”这类对象拥有姓名这个属性,所以我们就可以定义一个string类型的变量strName来描述。这些变量描述了对象的属性,成为了这个类整体的一部分,所以也被称为成员变量。
最佳实践:在类声明中给成员变量初始值
如果类的某些成员变量具有初始值,我们可以在类中声明这些成员变量的同时给它一个初始值,这样在运行期间,类就可以在进入构造函数之前,直接使用这个初始值完成相应成员变量的初始化。例如:
class Teacher { // 具有初始值的成员变量 protected: // 用字符串常量“Name”作为成员变量m_strName的初始值 string m_strName = “Name”; // 姓名 private: // 用常数2000作为成员变量m_unBaseSalary的初始值 unsigned int m_unBaseSalary = 2000; };
在这段代码中,我们用两个常量分别作为类的两个成员变量m_strName和m_unSalary的初始值。经过这样的声明之后,在创建这个类的对象时,无需在构造函数中进行额外的初始化,它的这两个成员变量就会拥有相应的初始值。这一特性可以用于那些所有类的对象都拥有相同初始值的情况,比如所有“Teacher”对象的“m_unBaseSalary”(基本工资)都是2000元。
除了声明成员变量来描述对象的属性之外,对象的另外一个重要组成部分就是它的行为。在C++中,我们用函数来描述一个行为动作。同样,我们也将函数引入类中成为它的成员函数,用来描述类对象的行为。比如,一个“老师”对象有备课的行为动作,我们就可以为老师这个类添加一个PrepareLesson()函数,在这个函数中可以对老师备课动作进行具体的定义。类的构成如图6-7所示。
图6-7 类的构成
最佳实践:为类设计对程序员友好的接口
我们所设计的类不仅供我们自己使用,更多时候它还会提供给其他程序员使用,以达到代码复用或者实现团队协作的目的。这时,类的接口设计的好坏,将会影响到他人能否正确并轻松地使用我们所设计的类。因此,它也成为了衡量一个程序员水平高低的重要标准。
类的接口,就像类的使用说明书一样,是向类的使用者说明它所需要的资源及它所能够提供的服务等。只要类的接口对程序员友好,从接口就可以轻松地知道如何正确地使用这个类。要做到这一点,应当遵守下面这些设计原则。
l 遵循变量与函数的命名规则
成员变量也是变量,成员函数也是函数。所以,作为类的接口的它们,在命名的时候也同样应该遵守普遍的命名规则,让它们的名字能够准确而简洁地表达它们的含义。
l 简化类的视图
接口,代表了类所能够向用户提供的服务。所以,在进行类的接口设计时,只需要将必要的成员函数公有(public)就可以了,使用受保护的(protected)或者私有的(private)成员来向用户隐藏不必要的细节。因为隐藏了用户不应该访问的内容,自然也就减少了用户犯错误的机会。
l 使用用户的词汇
类设计出来最终是让用户使用的,所以在设计类的接口时,应该从用户的角度出发,使用用户所熟悉的词汇,让用户在阅读类的接口时,不需要学习新的词汇或概念,这样可以平滑用户的学习曲线,让我们的类使用起来更容易。
除了在类中定义变量和函数来表示类的属性和行为之外,还可以使用public、protected及private这三个关键字来对这些成员进行修饰,指定它们的访问级别。按照访问级别的不同,类的所有成员被分成了三个部分。通常,使用public修饰的成员外界可以访问,我们会在public部分定义类的行为,提供公共的函数接口供外部访问;使用protected修饰的成员只有类自己和它的派生类可以访问,所以在protected部分,我们可以定义遗传给下一代子类的属性和行为;最后private修饰的成员只有类自己可以访问,所以在private部分,我们可以定义这个类所私有的属性和行为。关于类的继承方式和访问控制,稍后将进行详细介绍。这里先来看一个实际的例子。例如,要定义一个类来描述老师这一类对象,通过对这类对象的抽象,我们发现老师这类对象拥有只有自己和子类可以访问的姓名属性和大家都可以访问的上课行为。当然,老师还有很多其他属性和行为,这里根据需要作了简化。最后,我们使用面向对象的封装机制,将这些属性和行为捆绑到一起,就有了老师这个类的声明。
// 老师 class Teacher { // 成员函数 // 描述对象的行为 public: // 公有部分,供外界访问 void GiveLesson(); // 上课 // 成员变量 // 描述对象的属性 protected:// 受保护部分,自己和子类访问 string m_strName; // 姓名 private: };
通过这段代码,我们声明了一个Teacher类,它是所有老师这种对象的一个抽象描述。这个类有一个public关键字修饰的成员函数GiveLesson(),它代表老师这类对象拥有大家都可以访问的行为——上课。它还有一个protected关键字修饰的变量m_strName,表示老师这类对象的只有它自己和子类可以访问的属性——姓名。这样,通过在一个类中声明函数描述对象的行为,声明变量描述对象的属性,就完整地声明了一个可以用于描述某类对象的类。
完成类的声明之后,我们还需要对类的行为进行具体的定义。类成员函数的具体定义可以直接在类中声明成员函数的时候同时完成:
class Teacher { // 成员函数 // 描述对象的行为 public: // 声明成员函数的同时完成其定义 void GiveLesson() { cout<<"老师上课。"<<endl; }; //… };
更多时候,我们只是将类的声明放在头文件(比如,Teacher.h文件)中,而将成员函数的具体实现放在类的外部定义,也就是相应的源文件(比如,Teacher.cpp)中。在类的外部定义类的成员函数时,我们需要在源文件中引入类声明所在的头文件,并且在函数名之前还要用“::”域操作符指出这个函数所属的类。例如:
#ifndef _TEACHER_H // 定义头文件宏,防止头文件被重复引入 #define _TEACHER_H // 在稍后的7.3.1小节中会有详细介绍 // Teacher.h 类的声明文件 class Teacher { // … public: void GiveLesson(); // 只声明,不定义 }; #endif // Teacher.cpp 类的定义文件 // 引入类声明所在的头文件 #include "Teacher.h" // 在Teacher类外完成成员函数的定义 void Teacher::GiveLesson() { cout<<"老师上课。"<<endl; }
这里可以看到,成员函数的定义跟普通函数并无二致,同样都是用函数来完成某个动作,只是成员函数所表示的是某类对象的动作。例如,这里只是输出一个字符串表示老师上课的动作。当然,在实际应用中,类成员函数还可以对成员变量进行访问,所完成的动作也要比这复杂得多。
知道更多:C++中用以声明类的另一个关键字——struct
在C++中,要声明一个类,除了使用正牌的“class”关键字之外,之前在3.8节中介绍过的用来定义结构体的“struct”关键字也同样可以用来声明一个类。在语法上,“class”和“struct”非常相似,两者都可以用来声明类,而两者唯一的区别就是,在没有指定访问级别的默认情况下,用“class”声明的类当中的成员是私有的(private),而用“struct”声明的类当中的成员是公有的(public)。例如:
// 使用“struct”定义一个Rect类 struct Rect { // 没有访问权限说明 // 类的成员函数,默认情况下是公有的(public) int GetArea() { return m_nW * m_nH; } // 类的成员变量,默认情况下也是公有的(public) int m_nW; int m_nH; };
这里,我们使用“struct”声明了一个Rect类,因为没有使用public等关键字显式地指明类成员的访问控制,在默认情况下,类成员都是公有的,所以可以直接访问。例如:
Rect rect; // 直接访问成员变量 rect.m_nH = 3; rect.m_nW = 4; // 直接访问成员函数 cout<<"Rect的面积是:"<<rect.GetArea()<<endl;
这两个关键字的默认访问控制要么过于保守,要么过于开放,这种“一刀切”的方式显然无法适应于所有情况。所以无论是使用“class”还是“struct”声明一个类,我们都应该在声明中明确指出各个成员的合适的访问级别,而不应该依赖于关键字的默认行为。
“class”和“struct”除了上面这点在类成员默认访问级别上的差异之外,从“感觉”上讲,大多数程序员都认为它们仍有差异:“struct”仅像一堆缺乏封装的开放的内存位,更多时候它是用以表示比较复杂的数据;而“class”更像活的并且可靠的现实实体,它可以提供服务、有牢固的封装机制和定义良好的接口。既然大家都这么“感觉”,那么仅仅在类只有很少的方法并且有较多公有数据时,才使用“struct”关键字来声明类;否则,使用“class”关键字更合适。