第5章 类 与 对 象 经典的编程方法称为面向过程的程序设计方法。在面向过程的程序设计中,使用函数实现各个功能模块,这些函数往往具有大量的形式参数,并且其中还调用了大量其他的函数。这需要程序设计人员花费大量的时间管理和分类这些函数。一种对这些函数分类的好办法为把这些函数按其处理的客观对象进行分类,例如,计算圆的周长的函数和计算圆的面积的函数,因为它们处理的客观对象都是圆,所以,将它们归为一类,同时,把它们的形式参数,即这些函数要处理的数据,也归到这一类中。通过这种方法,实现了对客观对象的属性(即数据)和方法(即函数)的封装。对于圆这个类而言,其属性就是它的半径,方法就是计算它的周长和计算它的面积,而且方法(即函数)不再需要形式参数,只需要对圆的属性进行操作。这样,就从面向过程的程序设计方法过渡到了面向对象的程序设计方法。 面向对象的程序设计方法具有三大特点: 封装、继承和多态。本章将介绍类与对象的概念与程序设计方法,体现了其封装特点; 第6章将介绍继承和多态。 自本章开始,类内部定义的变量将被称为属性或数据成员; 类内部定义的函数将被称为方法或方法成员。 本章的学习目标: 了解类在程序设计中的重大意义 掌握类的设计方法 熟练应用类与对象进行程序设计 掌握借助于指针对对象成员进行操作的方法 5.1结构体与类 C++语言设计者最初拓展C语言的结构体实现了封装特性,由于结构体实现了属性(即数据)的“封装”,只需要将方法(即函数)添加到结构体中,就可实现“封装”特性。但是,结构体中默认定义的属性和方法均是“公有”的,这里的“公有”的含义表示在结构体的外部,通过结构体定义的变量可以直接访问结构体中的全部属性和方法。 “封装”的真正含义在于客观对象的属性(或部分属性)是“私有”的,这里的“私有”的含义表示这些属性只能在结构体内部被其方法(或函数)访问,在结构体的外部无法访问。为了实现结构体的“封装”特性,引入了访问控制符private和public,分别表示私有访问属性和公有访问属性。 程序段61展示了结构体的封装特性。 视频讲解 程序段51结构体的用法实例 (1) 头文件main.h 1#pragma once 2struct Rectangle 3{ 4private: 5double w, h; 6public: 7Rectangle() 8{ 9w = 0; 10h = 0; 11} 12void setW(double a) 13{ 14w = a; 15} 16void setH(double b) 17{ 18h = b; 19} 20double area() 21{ 22return w * h; 23} 24double perimeter() 25{ 26return 2.0 * (w + h); 27} 28}; (2)主程序文件main.cpp 29#include <iostream> 30#include "main.h" 31using namespace std; 32 33int main() 34{ 35cout << "Input two numbers:"; 36double a, b; 37cin >> a >> b; 38Rectangle rect; 39rect.setW(a); 40rect.setH(b); 41cout << "Area = " << rect.area() << endl; 42cout << "Perimeter = " << rect.perimeter() << endl; 43} 在程序段51的头文件main.h中,第2~28行定义一个结构体类型Rectangle,用于抽象表示客观事物——矩形。其中,第4行的代码“private:”表示其后的语句(直到遇到第6行的public)均为私有访问属性,即第5行的语句“double w,h;”定义的双精度浮点型变量w和h均为私有属性,分别表示矩阵的宽和高。 第6行的代码“public:”表示其后的语句(直到遇到新的访问属性控制符或结构体的末尾)均具有公有访问属性。这里表示第7~27行的方法均为公有访问属性,即在结构体外部(通过结构体定义的变量)可以直接访问这些方法。 第7~11行的方法为Rectangle(),该方法与结构体同名,称为“构造”方法,一般用于初始化结构体的属性(即数据)成员。这里的第9行和第10行将数据成员w和h赋值为0。 第12~15行的方法属于set方法,这类方法具有参数,用于给结构体的数据成员赋值。第14行的语句“w=a;”将a赋给数据成员w。 第16~19行的方法也属于set方法,具有一个双精度浮点型参数b,第18行的语句“h=b;”将b赋给数据成员h。 第20~23行的方法area()方法用于计算长方形面积,第22行的语句“return w * h;”使用数据成员w和h计算长方形的面积,并返回这个面积。 第24~27行的方法perimeter()用于计算长方形的周长,第26行的语句“return 2.0 * (w+h);”使用数据成员w和h计算长方形的周长,并返回这个周长值。 在主程序文件main.cpp中,第30行的预编译指令“#include "main.h"”将头文件main.h包括在程序中。 在main()函数中,第35行的语句“cout << "Input two numbers: ";”输出提示信息“Input two numbers:”。 第36行的语句“double a,b;”定义两个双精度浮点型变量a和b。 第37行的语句“cin >> a >> b;”输入变量a和b的值。 第38行的语句“Rectangle rect;”定义结构体Rectangle类型变量rect。 第39行的语句“rect.setW(a);”调用公有方法setW()给私有数据成员(宽)赋值。 第40行的语句“rect.setH(b);”调用公有方法setH()给私有数据成员(高)赋值。 第41行的语句“cout << "Area=" << rect.area() << endl;”输出字符串"Area="和调用公有方法area()得到的矩形的面积。 第42行的语句“cout << "Perimeter=" << rect.perimeter() << endl;” 输出字符串"Perimeter="和调用公有方法perimeter()得到的矩形的周长。 程序段51的执行结果如图51所示。 图51程序段51的执行结果 上述程序段51中的结构体封装了数据成员和方法成员,这种做法比传统基于过程的模块化编程有很多优势: (1) 数据成员为私有成员,只能在结构体内部使用其方法访问这些成员,有效地保护了数据成员,避免了其被非法访问和修改; (2) 方法成员仅限于操作结构体的数据成员,方法的执行不受外部数据的干扰,更容易维护且更加健壮; (3) “封装”形式的程序更易于实现团队的协作和程序代码的拼装与移植,可大大加速软件的设计进程,大幅度缩短软件的开发周期。 这些优势也是面向对象编程优于面向过程编程的地方。 5.1.1类 为了与C语言的结构体兼容,除了拓展C语言结构体的定义外,C++语言定义了一种全新的数据类型——类,用class关键字定义。类是一种封装了数据成员和方法成员的数据类型。与结构体不同的是,类中默认定义的成员均为私有成员。在本书中,出于对严谨性的考虑,所有程序实例均使用了访问控制符(private、public和protected)显式地表示访问控制属性,其中,protected访问控制符在第6章介绍。 类类型的一般定义形式如下: class 类名 { private: 私有数据成员; 私有方法成员; protected: 保护型数据成员; 保护型方法成员; public: 公有数据成员; 公有方法成员; }; 在本书中,类名使用大写首字母的“大骆驼表示法”,即首字母大写且类名中完整的英文单词的首字母也大写,如“MyRectangle”; 类定义的变量(即对象或实例)使用全小写字母或首字母小写的“小骆驼表示法”,即首字母小写但变量名中完整的英文单词的首字母大写,如“myRectangle”。 类是一种数据类型,类似于整型int,在定义类类型时,并不为类分配存储空间。因此,类中的数据成员和方法成员的顺序可随意排列,即方法成员可以放在前面,而数据成员(尽管被方法成员使用)可以放在方法成员的后面。此外,类中的各种访问控制属性的顺序也可随意排列,或者分开放置,例如: class 类名 { private: 私有成员1; public: 公有成员1; private: 私有成员2; public: 公有成员2; }; 类定义的变量称为对象或实例,“类名对象名;”和“inta;”类似,这时系统将为“对象名”表示的对象分配存储空间。类定义对象只能在类的外部实现,因此,对象仅能访问类中的公有方法! 现在使用类类型替换结构体类型重新设计程序段51,如程序段52所示。 程序段52类的定义与用法实例 视频讲解 (1) 头文件main.h 1#pragma once 2class Rectangle 3{ 4private: 5double w, h; 6public: 7Rectangle() 8{ 9w = 0; 10h = 0; 11} 12Rectangle(double a,double b=1.0):w(a),h(b) 13{ 14} 15void setW(double a) 16{ 17w = a; 18} 19void setH(double b) 20{ 21h = b; 22} 23double area(); 24double perimeter(); 25}; 26double Rectangle::area() 27{ 28return w * h; 29} 30double Rectangle::perimeter() 31{ 32return 2.0 * (w + h); 33} (2)主程序文件main.cpp 34#include <iostream> 35#include "main.h" 36using namespace std; 37 38int main() 39{ 40cout << "Input two numbers:"; 41double a, b; 42cin >> a >> b; 43Rectangle rect(a,b); 44cout << "Area = " << rect.area() << endl; 45cout << "Perimeter = " << rect.perimeter() << endl; 46} 在程序段52的头文件main.h中,第2~25行定义了类Rectangle,这是一种数据类型。其中,第4行的“private:”表示第5行的语句“double w,h;”定义的数据成员w和h为私有属性,实际上从private开始到遇到其他的访问控制符前定义的成员均为私有成员。从第6行的“public:”开始至第25行类结束符,其中的方法只受到了public访问控制符的影响,表示第7~24行的方法均为公有方法。 第7~11行的Rectangle()方法为构造方法,第12~14行的Rectangle()方法也是构造方法,这两个构造方法是重载的关系。当类创建对象时,构造方法用于给对象的数据成员赋值。构造方法将在5.1.2节详细介绍。这里,第7~11行的构造方法将数据成员均赋值为0,第12~14行的构造方法将参数a和b分别赋给数据成员w和h。 第15~18行的setW()方法属于set()方法,将参数a赋给数据成员w。第19~22行的setH()方法也属于set()方法,将参数b赋给数据成员h。set()方法的特点为其名称约定由set和大写首字母的数据成员构成。 第23行的语句“double area();”声明了公有方法area()。 第24行的语句“double perimeter();”声明了公有方法perimeter()。 第23~24行的两个方法的实现部分放在了类Rectangle的外部,如第26~33行所示。类中的方法的实现可以放在类的内部,如第7~14行的构造方法和第15~22行的set()方法; 也可放在类的外部,如第23行和第24行的两个方法。 第26~29行为类Rectangle的area()方法的实现部分,这里的“Rectangle::area”表示类Rectangle中声明的方法area(),其中“: :”表示归属关系的域运算符。 第30~33行为类Rectangle的perimeter()方法的实现部分。 在主程序文件main.cpp的main()函数中,第43行的语句“Rectangle rect(a,b);”定义对象rect,并调用类Rectangle的构造方法(见第12行)使用a和b分别初始化对象rect的数据成员w和h。 第44行的语句“cout << "Area=" << rect.area() << endl;”输出字符串"Area=",并调用对象rect的公有方法area()得到长方形的面积。 第45行的语句“cout << "Perimeter=" << rect.perimeter() << endl;”输出字符串"Perimeter=",并调用对象rect的公有方法perimeter()得到长方形的周长。 程序段52的执行结果如图52所示。 图52程序段52的执行结果 5.1.2构造方法 在类的公有方法中,有一种和类名同名的方法,称为构造方法。在创建类时,如果没有显式的构造方法,类将自动创建一个无参数的构造方法。构造方法的作用在于给类中的数据成员赋初始值。 这里针对程序段52中的Rectangle类,介绍几种定义构造方法的重载形式。 1. 无参数的构造方法 无参数的构造方法由类名和一对圆括号“( )”构成,如程序段53中的第6行所示。 视频讲解 程序段53无参数的构造方法实例 1class Rectangle 2{ 3private: 4double w, h; 5public: 6Rectangle() 7{ 8w = 0; 9h = 0; 10} 11//其他语句 12} 下述语句将调用程序段53中第6行的无参数构造方法: Rectangle rect; 请注意,上述语句中没有圆括号“( )”。 建议每个类都应该设计一个无参数的构造方法。 2. 带参数的构造方法 带参数的构造方法如程序段54中的第11~15行的方法。 视频讲解 程序段54带参数的构造方法实例 1class Rectangle 2{ 3private: 4double w, h; 5public: 6Rectangle() 7{ 8w = 0; 9h = 0; 10} 11Rectangle(double a, double b) 12{ 13w = a; 14h = b; 15} 16//其他语句 17}; 第11~15行的方法与类名Rectangle同名,具有两个双精度浮点型参数,在方法中,第13~14行的语句分别将a和b赋给数据成员w和h。这样表述是为了叙述简单,切记,这种表述方式是不准确的!类只是一种数据结构,定义类时并不会为类分配存储空间。对于第11~15行的构造方法准确的表述应该是当应用类创建对象时,将赋给形式参数a和b的实际参数值分别赋给所创建的对象的数据成员w和h。在不引起歧义的情况下,本节在介绍构造函数时,使用给数据成员赋值这种简略说法。 在上述情况下,使用以下语句调用程序段54中第11~15行的有参数构造方法: Rectangle rect(3.0,4.0); 3. 带默认参数值的构造方法 构造方法中的参数可以使用默认值,如程序段55所示。 视频讲解 程序段55带默认参数值的构造方法 1class Rectangle 2{ 3private: 4double w, h; 5public: 6Rectangle(double a=1.0, double b=1.0) 7{ 8w = a; 9h = b; 10} 11//其他语句 12}; 在程序段55中,第6~10行为带默认参数值的构造方法,这里两个参数均有默认值,相当于默认的无参数构造方法,所以,这里的第6~10行的构造方法不能与程序段53中的构造方法共存。 调用程序段55中第6~10行带默认参数的构造方法的语句形如: Rectangle rect;//或Rectangle rect(3.0);或Rectangle rect(3.0,5.0); 其中,前两者相当于“Rectangle rect(1.0,1.0);”或“Rectangle rect(3.0,1.0);”。 如果构造方法只有部分参数带有默认值,则带有默认值的参数必须位于参数列表中没有默认值的参数的右边,如程序段56所示。 视频讲解 程序段56部分参数带默认值的构造方法 1class Rectangle 2{ 3private: 4double w, h; 5public: 6Rectangle() 7{ 8w = 0; 9h = 0; 10} 11Rectangle(double a, double b=1.0) 12{ 13w = a; 14h = b; 15} 16//其他语句 17}; 这里的第11~15行为只有参数b带有默认值的构造方法,与第6~10行的构造方法是重载关系。 调用程序段56中第11~15行的构造方法的语句形如: Rectangle rect(3.0);//或Rectangle rect(3.0,5.0); 其中,前者相当于“Rectangle rect(3.0,1.0);”。 4. 使用this指针的构造方法 带参数的构造方法中,参数名是构造方法的局部变量,参数名可以与数据成员名相同。当参数名与数据成员名相同时,构造方法中使用this指针区分参数和数据成员,如程序段57所示。 视频讲解 程序段57使用this指针的构造方法 1class Rectangle 2{ 3private: 4double w, h; 5public: 6Rectangle() 7{ 8w = 0; 9h = 0; 10} 11Rectangle(double w, double h) 12{ 13this->w = w; 14this->h = h; 15} 16//其他语句 17}; 在程序段57中,第11~15行的构造方法的两个参数名与数据成员名相同。当使用类定义一个对象时,this指针将作为对象本身的指针,第13行的“this>w”指的是数据成员w,第14行的“this>h”指的是数据成员h,从而第13行的语句“this>w=w;”表示将参数w赋给数据成员w,第14行的语句“this>h=h;”表示将参数h赋给数据成员h。 5. 使用列表初始化的构造方法 使用列表初始化的构造方法是最常用的构造方法,如程序段58所示。 视频讲解 程序段58使用列表初始化的构造方法 1class Rectangle 2{ 3private: 4double w, h; 5public: 6Rectangle() 7{ 8w = 0; 9h = 0; 10} 11Rectangle(double a,double b) :w(a), h(b) 12{ 13//其他语句 14} 15//其他语句 16}; 在程序段58中,第11~14行的构造方法为使用列表初始化的构造方法,这里使用“:”连接初始化列表“w(a),h(b)”,这个列表表示将参数a赋值给w,将参数b赋值给h。 使用列表初始化的构造方法中,可以使参数名与数据成员名相同,即程序段58中的第11~14行可以写作如下形式: 11Rectangle(double w, double h) :w(w), h(h) 12{ 13//其他语句 14} 在上述代码中,尽管可以在构造方法中放入“其他语句”,但实际上,一般来说,构造方法主要用于给类定义的对象的数据成员赋值,即在类定义对象时给其数据成员赋初始值。 5.1.3set()方法与get()方法 类中的构造方法在使用类创建对象时被调用以初始化对象中的数据成员,当类创建对象后,构造方法不再被调用,此时只能通过set()方法修改对象的私有数据成员,所以,一般地,类都需要set()方法。与set()方法相对的方法称为get()方法,通过get()方法可以得到类中的私有数据成员的值。注意,这里的set()方法和get()方法的称谓,是程序员的约定,set()方法一般由set()加上首字母大写的数据成员名命名,get()方法一般由get()加上首字母大写的数据成员名命名,如程序段59所示。 视频讲解 程序段59set()方法和get()方法实例 (1) 头文件main.h 1#pragma once 2class Rectangle 3{ 4private: 5double w, h; 6public: 7Rectangle(double w=0, double h=0) :w(w), h(h){} 8void setW(double w) 9{ 10if (w > 0) 11this->w = w; 12else 13this->w = 0; 14} 15void setH(double h) 16{ 17if (h > 0) 18this->h = h; 19else 20this->h = 0; 21} 22double getW() const 23{ 24return w; 25} 26double getH() const 27{ 28return h; 29} 30double area(); 31double perimeter(); 32}; 33double Rectangle::area() 34{ 35return w * h; 36} 37double Rectangle::perimeter() 38{ 39return 2.0 * (w + h); 40} (2)主程序文件main.cpp 41#include <iostream> 42#include "main.h" 43using namespace std; 44 45int main() 46{ 47cout << "Input two numbers:"; 48double a, b; 49cin >> a >> b; 50Rectangle rect; 51rect.setW(a); 52rect.setH(b); 53cout << "Width: " << rect.getW() << endl; 54cout << "Height: " << rect.getH() << endl; 55cout << "Area = " << rect.area() << endl; 56cout << "Perimeter = " << rect.perimeter() << endl; 57} 在程序段59的头文件main.h中,第7行的语句“Rectangle(double w=0,double h=0) : w(w),h(h){}”为构造方法,带有默认参数值(可作为默认的构造方法)。 第8~14行的语句为setW()方法,用形式参数w给数据成员w赋值。第10~13行为一个if结构,如果参数w大于0,则将w的值赋给数据成员w; 否则,将数据成员w的值设为0。 第15~21行的语句为setH()方法,用形式参数h给数据成员h赋值。第17~20行为一个if结构,如果参数h大于0,则将h的值赋给数据成员h; 否则,将数据成员h的值设为0。 第22~25行的语句为getW()方法,第22行的代码“double getW() const”中,const用于修饰getW(),表示该方法不能修改数据成员的值。这里的getW()方法返回数据成员w的值(见第24行)。 第26~29行的语句为getH()方法,第26行的代码“double getH() const”中,const用于修饰getH(),表示该方法不能修改数据成员的值。这里的getH()方法返回数据成员h的值(见第28行)。 在主程序文件main.cpp中的main()函数中,第50行的语句“Rectangle rect;”定义类Rectangle类型的对象rect,将调用默认的构造方法,将数据成员w和h均赋值为0。 第51行的语句“rect.setW(a);”调用对象rect的setW()方法将a赋给数据成员w。 第52行的语句“rect.setH(b);”调用对象rect的setH()方法将b赋给数据成员h。 第53行的语句“cout << "Width: " << rect.getW() << endl;”输出字符串"Width: "和调用getW()方法得到的长方形的宽。 第54行的语句“cout << "Height: " << rect.getH() << endl;” 输出字符串"Height: "和调用getH()方法得到的长方形的高。 程序段59的执行结果如图53所示。 图53程序段59的执行结果 5.1.4析构方法 与构造方法对立的方法称为析构方法。构造方法在类创建对象时对对象的数据成员进行实始化,而析构方法用于清除对象时清理对象占据的存储空间。大多数情况下,无须程序员设计析构方法,类本身有一个隐式的析构方法完成清理工作。但是,如果程序员使用了“char *”类型的数据成员,则需要编写显式的析构方法。 析构方法名是类名前添加符号“~”,而且,应尽量设为虚析构方法,即在析构方法前添加virtual关键字,具体原因在学习完第6章多态方面的内容后才会明白。 视频讲解 程序段510展示了析构方法的用法。 程序段510析构方法的用法实例 (1) 头文件main.h 1#pragma once 2#include <cstring> 3using namespace std; 4 5class Student 6{ 7private: 8char* name; 9char gender; 10public: 11Student(const char* str,char g) 12{ 13if (str != NULL) 14{ 15name = new char[strlen(str)+1]; 16strcpy_s(name,strlen(str)+1, str); 17} 18else 19name = NULL; 20gender = g; 21} 22char* getName() const 23{ 24return name; 25} 26char getGender() const 27{ 28return gender; 29} 30virtual ~Student() 31{ 32if (name != NULL) 33delete[] name; 34cout << "Destructor is called." << endl; 35} 36}; (2) 主程序文件main.cpp 37#include <iostream> 38#include "main.h" 39using namespace std; 40 41int main() 42{ 43Student st("Zhang Fei", 'M'); 44cout << "Name: " << st.getName() << endl; 45cout << "Gender: " << st.getGender() << endl; 46} 在程序段510的头文件main.h中,定义了类Student,其中,有两个私有数据成员,如第8行和第9行所示,为“char* name;”和“char gender;”,即指向字符的指针类型的变量name和字符类型的变量gender。 第11~21行为构造方法Student(),具有两个形式参数str和g。这里的“const char*”表示指向常量字符串的指针,const修饰上满足“就近”原则,例如,“char * const”类型表示指向字符串的常量指针。因此,“const char* p1”和“char * const p2”是不同的。前者表示定义一个指向常量字符串的指针p1,p1可以改变,但其指向的内容不可变; 后者表示定义一个指向字符串的常量指针p2,p2不可变,但其指向的内容可变。在这两个定义下,“*p1”不可赋值,“p1”可赋值; “*p2”可赋值,“p2”不可赋值。另外,“const char * const p3”表示定义一个指向常量字符串的常量指针p3,此时,“p3”和“*p3”均不可赋值。 在第11~21行的Student()构造方法内部,第13~19行为一个if结构,如果str不为空,则第15行的语句“name=new char[strlen(str)+1];”使用new开辟一个长度为strlen(str)+1个字节的空间给name,即name指向该存储空间。第16行的语句“strcpy_s(name,strlen(str)+1,str);”将字符串str复制到name指向的空间中,复制的长度为“strlen(str)+1”,这里“+1”不可少,因为strlen(str)只返回字符串的真实长度,忽略了字符串结束符'\0'。第20行的语句“gender=g;”将参数g赋给数据成员gender。 第22~25行为getName()方法,用于返回数据成员name。 第26~29行为getGender()方法,用于返回数据成员gender。 第30~35行为虚析构方法,其中第32行和第33行为一个if结构,如果数据成员name指针不为空,则调用第33行的语句“delete[] name;”释放它指向的存储空间。第34行的语句“cout << "Destructor is called." << endl;”输出提示信息“Destructor is called.”,这行语句仅用于测试。 在主程序文件main.cpp的main()函数中,第43行的语句“Student st("Zhang Fei",'M');”创建类Student类型的对象st,并用字符串"Zhang Fei"和字符'M'初始化数据成员。 第44行的语句“cout << "Name: " << st.getName() << endl;”输出字符串"Name: "和调用对象的getName()方法得到的数据成员name的值。 第45行的语句“cout << "Gender: " << st.getGender() << endl;” 输出字符串"Gender:"和调用对象的getGender()方法得到的数据成员gender的值。 程序段510的执行结果如图54所示。 图54程序段510的执行结果 此外,建议在类中尽可能避免使用“char *”类型的数据成员,对于字符串类型的数据成员,可以使用string类型。这样,可以避免在类中编写析构方法。 5.2对象与指针 类是一种数据类型,类定义的变量称为对象或实例。对象可以访问其中的公有方法,使用“.”作用符,形如“对象名.公有方法名(实际参数)”,如果没有“参数”,那么括号“( )”不可少(只有一个例外,就是构造方法在没有参数时,不能添加括号)。 可以定义类类型的指针,形式“类名*指针名”,这种指针可以指向该类定义的对象。程序段511介绍了指向对象的指针及其用法。 视频讲解 程序段511指向对象的指针实例 (1) 头文件main.h 1#pragma once 2class Rectangle 3{ 4private: 5double w, h; 6public: 7Rectangle(double w=0,double h=0):w(w),h(h){} 8void setWH(double w, double h) 9{ 10this->w = w; 11this->h = h; 12} 13double getW() const 14{ 15return w; 16} 17double getH() const 18{ 19return h; 20} 21}; (2) 主程序文件main.cpp 22#include <iostream> 23#include "main.h" 24using namespace std; 25 26int main() 27{ 28Rectangle rect[4]; 29Rectangle* r; 30for (int i = 0; i < 4; i++) 31{ 32rect[i].setWH(2 + i * i, 3 + 2 * i); 33} 34r=rect; 35for (int i = 0; i < 4; i++) 36{ 37cout << "Width: " << r->getW() << ", Height: " << r->getH() << endl; 38r++; 39} 40} 在程序段511的头文件main.h中,定义了类Rectangle,具有两个私有数据成员w和h(第4行和第5行),具有一个构造方法Rectangle()(第7行)、一个setWH()方法(第8~12行)、一个getW()方法(第13~16行)和一个getH()方法(第17~20行)。 在主程序文件main.cpp的main()函数中,第28行的语句“Rectangle rect[4];”定义了一个类Rectangle类型的对象数组rect,具有4个元素。 第29行的语句“Rectangle* r;”定义一个指向Rectangle类类型的指针r。 第30~33行为一个for结构,循环变量i从0按步长1递增到3,对于每个i,执行一次第32行的语句“rect[i].setWH(2+i * i,3+2 * i);”调用rect[i]对象的setWH方法给对象的私有数据成员w和h赋值。 第34行的语句“r=rect;”使指针r指向rect数组的首地址,也可写为“r=&rect[0];”。 第35~39行为一个for结构,循环变量i从0按步长1递增到3,对于每个i,执行一次第37行和第38行的语句,其中,第37行的语句“cout << "Width: " << r>getW() << ",Height: " << r>getH() << endl;”输出字符串"Width: "、指针r指向的对象的getW()方法返回的值、字符串",Height: "和指针r指向的对象的getH()方法返回的值; 第38行的语句“r++;”使指针r指向数组rect的下一个对象。这里的“r>getW()”为指向对象的指针调用对象中的公有方法的形式,使用“>”指向符号。 这里的第34~39行的代码可以替换为如下形式: 1r = rect; 2for (int i = 0; i < 4; i++) 3{ 4cout << "Width: " << r[i].getW() << ", Height: " << r[i].getH() << endl; 5} 上述代码中,使用“r[i].getW()”的形式访问对象的公有方法。这里的rect为一组数组,r为指向一维数组的指针,因此,指针r和数组名rect等价(除了数组名rect为常量不可赋值而指针r为变量可赋值外)。 程序段511的执行结果如图55所示。 图55程序段511的执行结果 5.3静态函数与友元函数 类的最大优点在于具有“封装”特性,使用类创建对象后,对象通过私有访问控制将数据成员保护起来,通过公有访问控制向外部提供实现特定功能“接口”的公有方法。但是,有两个“破坏”类的封装特性的途径,称为静态函数和友元函数(本书不介绍友元类)。建议尽可能少使用或者不使用静态函数和友元函数。 在类中的静态函数,在编译时就为其分配了存储空间,而类创建的对象是在运行时才分配存储空间,因此,静态函数属于类,而不属于类创建的对象。有一个说法,“类中的静态函数被类创建的对象共享”,这种说法也不妥。应将静态函数视为类所有的,直接使用“类名::静态函数名(实际参数)”这种形式调用静态函数。静态函数的作用主要体现在: 如果想设计一些特定功能的函数,例如数学函数,这些函数没有特定的物理对象相对应,那么这时可以考虑设计一个数学类,然后,将其中的所有函数均设计为静态函数(一般会带有大量参数),通过类名可以调用这些静态函数。 类的友元函数是一种“可怕”的函数,友元函数可以直接访问类的私有成员。相比于静态函数,友元函数没有任何优点,可能的作用在于通过“对象名.私有数据成员名”的方式访问对象中的私有数据成员(这种访问私有成员的方式本应是不合法的)。正是因为这个原因,本书没有介绍友元类。注意,类的友元函数不属于类,所以友元函数既可以放在“public:”公有部分,也可以放在“private:”私有部分。 程序段512介绍了静态函数和友元函数的用法。注意,静态函数不属于类创建的对象,它无法访问对象中的方法,也就是说,静态函数无法访问类中的私有成员和公有成员,只能访问类中的静态成员。应将静态函数与对象划清界限。 视频讲解 程序段512静态函数和友元函数实例 (1) 头文件main.h 1#pragma once 2class Circle 3{ 4private: 5double r; 6public: 7Circle(double r=0):r(r){} 8void setR(double r) 9{ 10this->r = r; 11} 12double getR() const 13{ 14return r; 15} 16double area() 17{ 18return 3.14 * r * r; 19} 20 21static double sarea(double a) 22{ 23return 3.14 * a * a; 24} 25friend double farea(Circle& c,double b) 26{ 27c.r = b; 28return c.area(); 29} 30}; (2) 主程序文件main.cpp 31#include <iostream> 32#include "main.h" 33using namespace std; 34 35int main() 36{ 37Circle c1; 38c1.setR(5.0); 39cout << "Radius: " << c1.getR() << endl; 40cout << "Area: " << c1.area() << endl; 41 42cout << "Area: " << Circle::sarea(6.0) << endl; 43cout << "Radius: " << c1.getR() << endl; 44cout << "Area: " << farea(c1, 7.0) << endl; 45cout << "Radius: " << c1.getR() << endl; 46} 在程序段512的头文件main.h中,第2~30行定义了一个类Circle,具有一个私有数据成员r,如第4行和第5行所示,r为双精度浮点型变量。 第6~19行为类Circle的公有方法,其中,第7行的代码“Circle(double r=0): r(r){}”为具有默认值的构造方法。 第8~11行为setR()方法,将参数r赋给数据成员r。 第12~15行为getR()方法,返回数据成员r的值。 第16~19行为area()方法,使用数据成员r返回圆的面积。 第21~24行为类的静态函数sarea(),具有一个双精度浮点型参数a,计算半径为a的圆的面积(第23行)。 第25~29行为类的友元函数farea(),具有两个参数,即Circle类类型的引用参数c和双精度浮点型参数b。第27行的语句“c.r=b;”将参数b赋给对象c的私有数据成员r。第28行的语句“return c.area();”调用对象c的area()方法得到圆的面积。 在主程序文件main.cpp的main()函数中,第37行的语句“Circle c1;”定义类Circle类型的对象c1。 第38行的语句“c1.setR(5.0);”调用对象c1的公有方法setR()将5.0赋给其数据成员r。 第39行的语句“cout << "Radius: " << c1.getR() << endl;”输出字符串"Radius: "和调用对象c1的公有方法getR()得到的圆的半径,即数据成员r的值,结合第38行可知,这里r的值为5.0。 第40行的语句“cout << "Area: " << c1.area() << endl;” 输出字符串"Area: "和调用对象c1的公有方法area()得到的圆的面积,即对应于r=5.0的圆的面积,为78.5。 第42行的语句“cout << "Area: " << Circle::sarea(6.0) << endl;”中,使用“类名::静态函数名(实际参数列表)”的方式调用了静态函数“Circle::sarea(6.0)”,返回半径为6.0的圆的面积,为113.04。 第43行的语句“cout << "Radius: " << c1.getR() << endl;”输出对象c1的数据成员r的值。这一条语句的执行结果表明第42行语句对对象c1的数据成员没有影响。 第44行的语句“cout << "Area: " << farea(c1,7.0) << endl;”调用了友元函数farea(),将对象c1和数值7.0作为其参数,得到半径为7.0的圆的面积,为153.86。 第45行的语句“cout << "Radius: " << c1.getR() << endl;”输出对象c1的私有数据成员r的值,将发现r的值为7(而非原来的5),是因为第44行的友元函数farea()的执行改变了对象c1的私有数据成员r。 程序段512的执行结果如图56所示。 图56程序段512的执行结果 5.4对象复制 在使用类定义一个新对象时可以使用它已定义的对象初始化,即将已定义的对象赋给同类型的新对象。 在C++语言中,存在着赋值和赋址两种方式。赋值是指将一个变量的值赋给另一个变量,这两个变量占有不同的存储空间,赋值完成后修改任一变量的值,另一个变量不受影响。赋址是指将一个变量的地址赋给另一个变量,这类变量一般为指针,这样两个变量指向相同的地址,修改它们中任一变量的“值”,都是修改它们共同指向的存储空间中的“值”,因此,修改其中一个变量的“值”将影响到另一个变量的“值”。 下面程序段513展示了赋值和赋址的情况。 视频讲解 程序段513赋值与赋址的不同情况实例 1#include <iostream> 2#include <cstring> 3#include <string> 4using namespace std; 5 6int main() 7{ 8int a, b; 9a = 30; 10b = a; 11cout << "a = " << a << endl; 12cout << "b = " << b << endl; 13a = 15; 14cout << "a = " << a << endl; 15cout << "b = " << b << endl; 16 17string s1, s2; 18s1 = "Hello"; 19s2 = s1; 20cout << "s1 = " << s1 << endl; 21cout << "s2 = " << s2 << endl; 22s1 = "World!"; 23cout << "s1 = " << s1 << endl; 24cout << "s2 = " << s2 << endl; 25 26int* p = &a; 27int* q; 28q = p; 29cout << "*p = " << *p << endl; 30cout << "*q = " << *q << endl; 31*p = 20; 32cout << "*p = " << *p << endl; 33cout << "*q = " << *q << endl; 34 35char* c1 = new char[20]; strcpy_s(c1, 6, "Hello"); 36char* c2; 37c2 = c1; 38cout << "c1 = " << c1 << endl; 39cout << "c2 = " << c2 << endl; 40strcpy_s(c1, 6, "World"); 41cout << "c1 = " << c1 << endl; 42cout << "c2 = " << c2 << endl; 43} 在程序段513的main()函数中,第8~15行为变量间赋值的情况,第8行定义整型变量a与b; 第9行将30赋值给变量a; 第10行将变量a赋值给变量b; 第11行和第12行输出变量a和b的值,此时两个变量均为30。然后,第13行将15赋给变量a; 第14行和第15行输出变量a和b的值,此时,变量a为15,变量b仍为30。 第17~24行为string类型的变量间赋值的情况。其中,第17行定义字符串变量s1和s2; 第18行将字符串“Hello”赋值给s1; 第19行将s1赋值给变量s2; 第20行和第21行输出变量s1和s2的值,此时,两者均为“Hello”; 第22行将“World!”赋给变量s1; 第23行和第24行输出s1和s2的值,此时,s1为“World!”,而s2为“Hello”。 第26~33行为变量间赋址的情况。其中,第26行定义指向整型变量的指针p,p指向整型变量a; 第27行定义指向整型变量的指针q; 第28行将指针p赋给指针q,此时两个指针都指向相同的地址(即&a); 第29行和第30行输出“*p”和“*q”的值,均为a的值,即15。第31行将“*p”赋为20; 第32行和第33行输出“*p”和“*q”的值,由于指针p和q均指向相同的地址,“*p”和“*q”的值相同,均为20。 第35~42行为变量间赋址的情况。其中,第35行定义指向字符的指针c1,并使用new操作符开辟长度为20字节的存储空间,然后,调用strcpy_s函数将字符串Hello赋值给指针c1指向的空间; 第36行定义指向字符的指针c2; 第37行将指针c1赋给c2; 第38行和第39行输出指针c1和c2的值,即输出这两个指针指向的字符串的值,由于两个指针指向同一地址,故均输出“Hello”; 第40行将字符串“World”赋值给指针c1指向的空间; 第41行和第42行输出指针c1和c2的值,即输出这两个指针指向的字符串的值,由于两个指针指向同一地址,此时均输出“World”。 程序段513的执行结果如图57所示。 图57程序段513的执行结果 由于类是一种数据类型,类定义的变量(即对象)也可以复制。如果类的私有成员全是赋值类型的变量,类定义的对象间可以互相赋值; 但若类的私有成员中有赋址类型的变量,则类定义的对象间不能直接赋值,这是因为当一个对象赋值给另一个对象时,两个对象中的赋址类型的数据成员指向同一个地址空间,这时如果修改了一个对象的这种赋址类型的数据成员,那么另一个对象的指向这个地址的赋址类型的数据成员的“值”也跟着变化,从而破坏了类的“封装”特性。 下面的程序段514就反映了这种情况,这种情况称为对象间的“浅复制”,这个程序在Visual Studio 2022下可编译通过,也可执行(但会有警告)。 视频讲解 程序段514对象间的浅复制实例 (1) 头文件main.h 1#pragma once 2#include <iostream> 3#include <cstring> 4using namespace std; 5 6class Student 7{ 8private: 9char * name; 10char gender; 11double score[5]; 12public: 13Student() 14{ 15name = NULL; 16gender = 'F'; 17for (int i = 0; i < 5; i++) 18score[i] = 0; 19} 20Student(const char * str, char g) 21{ 22name = NULL; 23if (str != NULL) 24{ 25name = new char[200]; 26strcpy_s(name, strlen(str) + 1, str); 27} 28gender = g; 29for (int i = 0; i < 5; i++) 30score[i] = 0; 31} 32void setScore(double *d) 33{ 34for (int i = 0; i < 5; i++) 35score[i] = *(d + i); 36} 37 38void setName(const char* str) 39{ 40if (strlen(str) < 200) 41{ 42strcpy_s(name, strlen(str) + 1, str); 43} 44else 45cout << "Error" << endl; 46} 47void getMember() const 48{ 49if (name != NULL) 50cout << "Name: "<<name << endl; 51cout << "Gender: " << gender << endl; 52cout << "Score: "; 53for (int i = 0; i < 5; i++) 54{ 55cout << score[i] << " "; 56} 57cout << endl; 58} 59virtual ~Student() 60{ 61if (name != NULL) 62delete[] name; 63} 64}; (2) 主程序文件main.cpp 65#include <iostream> 66#include "main.h" 67using namespace std; 68 69int main() 70{ 71Student s1("Zhang Fei",'M'), s2; 72double sc[5] = { 91.5,93.2,97.5,94.9,90.1 }; 73s1.setScore(sc); 74s1.getMember(); 75s2 = s1; 76s2.getMember(); 77cout << endl; 78 79s1.setName("Guan Yu"); 80s1.getMember(); 81s2.getMember(); 82} 在程序段514的头文件main.h中定义了类Student,在类Student中的私有成员中,使用了指向字符的指针name(第9行)。 由于类Student使用了指针类型的私有成员,在第20~31行的构造方法中,为指针name开辟了长度为200字节的空间(第25行),然后,将构造方法的参数str赋给name指向的空间。 由于类Student在构造方法中使用new操作符开辟了一块存储空间,在第59~63行添加了虚析构方法,将开辟的存储空间释放掉(第61行和第62行)。 类Student中其余的部分如下: 在私有数据成员中,定义了字符型的gender成员,表示学生的性别(第10行); 定义了双精度浮点型的一维数组score,保存学生的成绩(第11行)。在公有方法中,定义了一个默认构造方法(第13~19行); 定义了一个setScore()方法,用于向score成员赋值(第32~36行); 定义了一个setName()方法,用于向指针name指向的空间赋值(第38~46行); 定义了一个getMember()方法,用于显示数据成员的值(第47~58行)。 在主程序文件main.cpp的main()函数中,第71行的语句“Student s1("Zhang Fei",'M'),s2;”定义了类Student类型的对象s1和s2,同时,对s1进行了初始化。 第72行的语句“double sc[5]={ 91.5,93.2,97.5,94.9,90.1 };”定义了双精度浮点型一维数组sc,具有5个元素,并做了初始化。 第73行的语句“s1.setScore(sc);”调用对象s1的setScore()方法使用一维数组sc对对象s1的成员score进行赋值。 第74行的语句“s1.getMember();”调用对象s1的getMember()方法输出对象s1的各个数据成员的值。 第75行的语句“s2=s1;”将对象s1赋值给s2。 第76行的语句“s2.getMember();”调用对象s2的getMember()方法输出对象s2的各个数据成员的值。此时,对象s2和s1的数据成员的值相同。 第77行的语句“cout << endl;”输出回车换行符。 第79行的语句“s1.setName("Guan Yu");”调用对象s1的方法setName将对象s1中成员name指向的空间赋为“Guan Yu”(原来是“Zhang Fei”)。由于对象s2的数据成员name和对象s1的数据成员name指向相同的地址空间(共享同一存储空间),所以,针对对象s1的name的赋值,将影响到对象s2的name成员。 第80行语句“s1.getMember();”调用对象s1的getMember()方法输出对象s1的各个数据成员的值,其中name的输出为“Guan Yu”。 第81行的语句“s2.getMember();”调用对象s2的getMember()方法输出对象s2的各个数据成员的值。此时,对象s2中的成员name受到第79行语句的影响而输出“Guan Yu”。 程序段514的执行结果如图58所示。 图58程序段514的执行结果 为了修正程序段514的缺陷,即当类中有指针类型的数据成员时,必须添加一个“复制构造方法”,复制构造方法与其他的构造方法是重载的关系。针对程序段514的修改有以下两点: (1) 将程序段515所示的“复制构造方法”插入到程序段514的第31行和第32行之间。 视频讲解 程序段515复制构造方法 1Student(const Student& stu) 2{ 3cout << "Copy." << endl; 4name = NULL; 5if (stu.name != NULL) 6{ 7name = new char[200]; 8strcpy_s(name, strlen(stu.name) + 1, stu.name); 9} 10gender = stu.gender; 11for (int i = 0; i < 5; i++) 12score[i] = stu.score[i]; 13} 在程序段515中,复制构造方法的参数为“const Student& stu”即类Student的引用类型的参数,这里使用了const表示形式参数stu为常量,在复制构造方法内部不能被修改。调用复制构造方法的形式为“Student 目标对象(被复制的对象)”,结合第1行代码,这里的stu为“被复制的对象”。 第3行的语句“cout << "Copy." << endl;”输出一个提示信息“Copy.”,用于表示该复制构造方法被调用。 第7行和第8行的语句对“目标对象”中的name开辟一块新的存储空间,然后,将“被复制的对象”stu的name指向的内容复制到“目标对象”的name中。 这种使用了“复制构造方法”的情况,称为对象间的“深度”复制。 (2) 将程序段516中的主程序文件main.cpp替换程序段514中的main.cpp。 视频讲解 程序段516主程序文件main.cpp 1#include <iostream> 2#include "main.h" 3using namespace std; 4 5int main() 6{ 7Student s1("Zhang Fei", 'M'); 8double sc[5] = { 91.5,93.2,97.5,94.9,90.1 }; 9s1.setScore(sc); 10s1.getMember(); 11Student s2(s1); 12s2.getMember(); 13cout << endl; 14 15s1.setName("Guan Yu"); 16s1.getMember(); 17s2.getMember(); 18} 在程序段516中,使用了“Student s2(s1);”将对象s1赋值给对象s2,而不是使用“s2=s1”,这是因为赋值运算符“=”对于私有成员char *没有重载,运算符重载将在第7章介绍。 程序段514修改后的执行结果如图59所示。 图59程序段514修改后的执行结果 尽管采用深度“复制”可以完美解决类的私有数据成员为指针时的对象复制问题,但是建议初学者在创建类时尽量不使用指针,全部使用具有赋值特性的数据成员。这样,无须考虑深度复制的情况,即无须编写复制构造方法和析构方法。更重要的是,在绝大多数情况下,类中只需要使用具有赋值特性的数据成员。 下面程序段517实现了与程序段514完全相同的功能,但是仅使用了赋值特性的数据成员,且实现了深度复制(不用编写“复制构造方法”)。 视频讲解 程序段517类中仅使用赋值特性的数据成员(实现与程序段514相同的功能)实例 (1) 头文件main.h 1#pragma once 2#include <iostream> 3using namespace std; 4 5class Student 6{ 7private: 8string name; 9char gender; 10double score[5]; 11public: 12Student(string str="", char g=0) 13{ 14name = str; 15gender = g; 16for (int i = 0; i < 5; i++) 17score[i] = 0; 18} 19void setScore(double* d) 20{ 21for (int i = 0; i < 5; i++) 22score[i] = *(d + i); 23} 24void setName(string str) 25{ 26name = str; 27} 28void getMember() const 29{ 30cout << "Name: " << name << endl; 31cout << "Gender: " << gender << endl; 32cout << "Score: "; 33for (int i = 0; i < 5; i++) 34cout << score[i] << " "; 35cout << endl; 36} 37}; (2) 主程序文件main.cpp 38#include <iostream> 39#include "main.h" 40using namespace std; 41 42int main() 43{ 44Student s1("Zhang Fei", 'M'), s2; 45double sc[5] = { 91.5,93.2,97.5,94.9,90.1 }; 46s1.setScore(sc); 47s1.getMember(); 48cout << endl; 49s2 = s1; 50s2.getMember(); 51cout << endl; 52 53s1.setName("Guan Yu"); 54s1.getMember(); 55cout << endl; 56s2.getMember(); 57} 在程序段517的头文件main.h中,定义了类Student(第5~37行),包含私有数据成员字符串string类型的name(第8行)、字符类型的gender(第9行)和双精度浮点型的一组数组score(第10行)。还包含了公有方法: 第12~18行的构造方法Student()、第19~23行的setScore()方法、第24~27行的setName()方法和第28~36行的getMember()方法。这些数据成员和方法成员的含义与程序段514相同,不再赘述。 在程序段517的类Student中,私有数据成员没有使用指针等赋址类型的变量,因此,无须创建“复制构造方法”和析构方法,就可以实现深度复制,即一个对象赋值给另一个对象后,两个对象间不会有共享的存储空间,可认为两个对象互不相关。 主程序文件main.cpp的main()函数与程序段514的main()函数相同(只是这里的main()函数中多了第48行和第55行的语句“cout << endl;”),第49行的语句“s2=s1;”将对象s1赋给对象s2。然后,第53行的语句“s1.setName("Guan Yu");”将对象s1中的私有数据成员name赋值为“Guan Yu”。第54行的语句“s1.getMember();”调用对象s1的方法getMember()输出对象s1的数据成员。第56行的语句“s2.getMember();”调用对象s2的方法getMember()输出对象s2的数据成员。程序段517的执行结果如图510所示。由图510可知,第53行修改对象s1的数据成员name没有影响到对象s2中的数据成员name。 图510程序段517的执行结果 5.5本章小结 将所要实现的功能划分为小的功能模块,基于这些功能模块编写大量的函数,进而通过函数调用实现所需功能,这种软件设计方法称为面向过程的程序设计方法。在面向过程的程序设计方法中,函数是程序设计的中心。面向对象的程序设计方法是面向过程的程序设计方法的进化,在面向对象的程序设计方法中,类是程序设计的中心,将所要实现的功能的客体分成小的客体,基于这个小的客体抽象成数据结构——类,类定义的变量称为对象或实例,对应着小的客体,类中定义了所抽象的小的客体的属性(即数据成员)和方法(即函数),通过类定义的对象实现所需要的功能。面向对象的程序设计方法管理一个个的“类”,比起面向过程的程序设计方法管理一个个的函数而言,更加直观方便。 面向对象程序设计具有抽象、封装、继承和多态的优点。如何将一个客体抽象为类需要程序员不断地实践积累经验,这是面向对象程序设计思想的难点,特别是对于那些从面向过程的程序设计过渡到面向对象的程序设计的程序员而言。本章重点讨论了类的封装特性,介绍了类的定义、设计方法和封装技术(即访问控制技术),介绍了对象的使用方法,简述了借助对象访问类中公有方法的技巧,并介绍了静态函数和友元函数,讨论了对象复制的注意事项。第6章将讨论类的继承和多态特性。 对于C++语言入门者而言,建议在类中不使用赋址类型的数据成员,字符串使用string而不使用“char *”定义,不使用需要借助new操作符为数据成员开辟空间的数据成员和公有方法,这样可避免设计复制构造方法和析构方法。 习题 1. 将圆抽象为一个类,圆的半径和圆心坐标为类的私有数据成员,求圆的面积为类的公有方法成员,并编写构造方法、set方法和get方法。 2. 将三角形抽象为一个类,三角形的三个顶点坐标为类的私有数据成员,求三角形的周长为类的公有方法成员,并编写构造方法、set方法和get方法。 3. 编写一个学生类,其中学生的姓名、性别、学号和9门课的成绩(使用共用体类型)作为私有数据成员,输入和输出学生的信息作为公有方法成员。 4. 编写一个扇形类,扇形的圆心坐标、半径和圆心角作为类的私有数据成员,计算扇形的面积作为类的公有方法,并编写带默认参数的构造方法、set方法和get方法。然后,编写主程序计算一个扇形的面积。 5. 编写一个整数类,类的私有数据成员为两个整数,类的公有方法成员为实现两个整数间的四则运算,同时编写带默认参数的构造方法、set方法和get方法。 6. 编写一个复数类,类的私有数据成员为两个复数(用结构体类型表示),实现两个复数间的加、减、乘和除法运算作为类的4个公有方法成员,同时编写构造方法、set方法和get方法。编写主程序文件实现两个复数间的各种运算。