第3章〓关联与连接 客观世界中不仅存在大量的事物,还存在事物之间的相互作用,事物及其相互作用才创造了丰富多彩的大千世界。 第2章主要从编程实现角度介绍了类的封装和对象的职责,现在又回到客观世界中,继续讨论客观事物之间的关系。 视频讲解 3.1关联与连接的概念 关联和连接是面向对象思想中的两个基本概念。连接(Link)是对客观事物之间的关系的抽象,是客观事物之间的关系在计算机世界中的反映; 关联(Association)是对连接的抽象,是一种关系在计算机世界中的反映。 语文中,使用陈述语句描述客观事物及其相互关系,陈述语句主要采用“主谓宾”格式,其中,主语和宾语使用名词,表示客观事物; 谓语为动词,表示客观事物之间的关系。 例如,张三喜欢足球,李四喜欢篮球,王五喜欢篮球,陈述了一个人与一种体育运动之间的对应关系。人与体育运动之间的对应关系有很多种,这里只陈述了其中一种称为“喜欢”的对应关系,即人“喜欢”体育运动。 数学中,使用元素之间的对应关系来描述客观事物及其相互关系。例如,张三、李四、王五是集合“人”中的元素,足球、篮球是集合“体育运动”中的元素,“喜欢”是集合“人”和“体育运动”中元素之间的对应关系,这种对应关系如图3.1所示。 图3.1人“喜欢”体育运动的语义 图3.2“人喜欢体育运动”的类图 如图3.1所示,“人喜欢体育运动”中的“喜欢”表示了一种对应关系,可使用类图表示这种对应关系。表示“人喜欢体育运动”的类图,如图3.2所示。 如图3.2所示的类图,表达的意思是“Person like Sport”,翻译为中文是“人喜欢体育运动”,其数学上的语义如图3.1所示,表示集合“人”和“体育运动”之间的一种对应关系。 面向对象思想中,将类与类之间的一种对应关系称为一个关联,其中的一个对应称为该关联的一个连接。 如图3.2中,like是类Person和Sport之间的一个关联。关联like中包含Person对象与Sport对象之间的多个对应,每个对应称为关联like的一个连接。如图3.1中,(张三,足球)、(李四,篮球)和(王五,篮球)是关联like中的三个连接,分别表示张三喜欢足球、李四喜欢篮球、王五喜欢篮球。 3.2关联的实现 对应关系有一对一、多对一和多对多3种类型,对应关系的类型对计算机中的实现有重要影响,因此,数学中专门讨论了对应关系的3种类型,并为计算机中的实现提供了理论基础。 对应关系的3种类型中,可将一对一关系视为特殊的多对一关系,因此,只需讨论多对一和多对多两种关联的实现方法。 3.2.1使用指针实现多对一关联 一般需要结合具体的应用场景分析对应关系的类型。例如,如果需要回答“每个人最喜欢的体育 图3.3最喜欢的体育运动 运动是什么?”,可将人与体育运动的对应关系类型视为“多对一”关系,即一个人喜欢一种体育运动,一种体育运动可被多个人喜欢,其类图如图3.3所示。 图3.3中,将“一个人喜欢一种体育运动”中的“一种”标注在Sport端,并表示为数字“1”。将“一种体育运动可被多个人喜欢”中的“多个”标注在Person端,并表示为符号“*”。 关联like从Person端关联到Sport端,具有方向性,将表示方向的箭头称为导航(Navigation),将关联的“端”称为角色(Role),将表示对应数目的“1”和“*”称为重数(Multiplicity)。 关联的导航、角色和重数对编程实现有很大影响,首先需要明确标注这些信息,然后为关联中的每个类增加属性和成员函数。关联like及其中的类,如图3.4所示。 图3.4关联like及其中的类 类Person有属性sport,属性sport不是“人”的属性,而是用于存储关联like中的一个连接,其数据类型为指向Sport的指针,指向最喜欢的运动。 实际上,类Person的对象中使用成员指针sport指向一个类Sport的对象,表示多对一关联like中的一个连接。使用成员指针表示多对一关联中的连接,示例代码如例3.1所示。 【例3.1】使用成员指针表示多对一关联中的连接。 // Sport.h class Sport { public: Sport(); Sport(char * pName); void print(); ~Sport(); private: char name[20]; }; // Person.h #include "Sport.h" class Person { public: Person(); Person(char * pName); ~Person(); void like(Sport* likeSport); Sport* likedSport(); void print(); private: char name[20]; Sport* sport; //指向最喜欢的运动 }; //Sport.cpp #include #include #include "Sport.h" using namespace std; Sport::Sport(){ } Sport::Sport(char * pName){ strncpy(name, pName, sizeof(name)); name[sizeof(name)-1] = '\0'; } void Sport::print(){ cout << name << endl; } Sport::~Sport(){ } 类Person中,声明了void like(Sport*likeSport)和Sport*likedSport()两个成员函数,其中,like()成员函数的功能为将最喜欢的体育运动likeSport的地址存储到属性sport。likedSport()成员函数返回最喜欢的体育运动likeSport的地址。这两个成员函数主要维护了关联like中的连接,示例代码如例3.2所示。 【例3.2】维护多对一关联中的连接。 //Person.cpp #include #include #include "Sport.h" #include "Person.h" using namespace std; Person::Person(){ } Person::Person(char*pName){ strncpy(name, pName, sizeof(name)); name[sizeof(name)-1] = '\0'; } Person::~Person(){ } void Person::like(Sport*likeSport){ sport = likeSport; } Sport* Person::likedSport(){ return sport; } void Person::print(){ cout << name; } //app.cpp #include #include "Sport.h" #include "Person.h" using namespace std; void main(){ Sport s1("足球"); Sport s2("篮球"); Sport s3("乒乓球"); Person p1("张三"); p1.like(&s1); p1.print(); cout << "最喜欢"; p1.likedSport()->print(); Person p2("李四"); p2.like(&s2); p2.print(); cout << "最喜欢"; p2.likedSport()->print(); Person p3("王五"); p3.like(&s2); p3.print(); cout << "最喜欢"; p3.likedSport()->print(); } 图3.5表达式p1.likedSport()>print() 的计算顺序 例3.2中,表达式语句p1.like(&s1)调用类Person的like()成员函数,设置了张三最喜欢的足球,建立了类Person的对象p1与类Sport的对象s1之间的一个连接。 表达式p1.likedSport()>print()包含4个运算,其计算顺序如图3.5所示。 如图3.5所示,表达式p1.likedSport()>print()的语义为: ①计算p1.likedSport()中的点运算,选择类Person的likedSport()成员函数; ②调用Person::likedSport()成员函数,返回一个指向Sport对象s1的指针&s; ③计算选择运算(>),选择类Sport的print()成员函数; ④调用Sport::print()成员函数,输出张三最喜欢的“足球”。 例3.2中,先创建了类Sport的3个对象,然后创建类Person的对象p1("张三"),并执行函数调用p1.like(&s1)设置喜欢的体育运动项目s1,最后输出。程序运行过程如图3.6所示。 main()函数中,创建了类Person的3个对象,类Sport的3个对象,建立了关联like中的3个连接。创建的对象及其连接,如图3.7所示。 如图3.7所示,对象p2和p3连接到同一个对象s2,即“篮球”项目,符合实际情况。 例3.2程序的输出结果如下。 张三最喜欢足球 李四最喜欢篮球 王五最喜欢篮球 使用指针实现多对一关系,能够准确表示多对一关联的语义,是编程中最常用的方法之一。 图3.6例3.1程序运行过程 图3.7例3.1中创建的对象及其连接 3.2.2使用指针数组实现多对多关联 如果想知道一个人喜欢的所有体育运动项目,则人和体育运动之间的关联就是多对多类型,可以使用指针数组来存储这种关系。为了简单明了,假设一个人最多喜欢3项体育运动,多对多关联如图3.8所示。 图3.8多对多关联 如图3.8所示的类图中,Sport 端的重数为3,表示“一个人最多喜欢3项体育运动”,并使用一个指向Sport对象的指针数组sport[3]表示关联like中的连接。使用指针数组表示多对多关联中的连接,示例代码如例3.3所示。 【例3.3】使用指针数组表示多对多关联中的连接。 // Person.h #include "Sport.h" class Person { public: Person(); Person(char*pName); ~Person(); bool like(Sport*likeSport); Sport**likedSport(); void print(); private: char name[20]; Sport*sport[3]; //指向喜欢的3项运动 }; 使用指针数组sport表示多对多关联like中的连接,需要根据这种表示方法调用与此相关的成员函数,以正确维护关联like中的连接,示例代码如例3.4所示。 【例3.4】维护多对多关联中的连接。 //Person.cpp #include #include #include "Sport.h" #include "Person.h" using namespace std; Person::Person(){ for (int i = 0; i < 3; i++) sport[i] = NULL; //设置为空,表示没有指向对象 } Person::Person(char*pName){ strncpy(name, pName, sizeof(name)); name[sizeof(name)-1] = '\0'; for (int i = 0; i < 3; i++) sport[i] = NULL; } Person::~Person(){ } bool Person::like(Sport*likeSport){ int i=0; while (i<3&&sport[i]) i++; if (i < 3){ sport[i] = likeSport; return true; } else{ return false; //超过3项返回错误 } } Sport**Person::likedSport(){ return sport; } void Person::print(){ cout << name; cout << "喜欢"; for (int i = 0; i < 3; i++) sport[i]->print(); } 主要调用了like()和likedSport()成员函数。likedSport()成员函数的原型调整为Sport** likedSport(),返回一个指针数组。like()成员函数的原型调整为bool like(Sport*likeSport),每次调用传递一项体育运动项目,可多次调用,如果超过3项就返回false,表示存储失败。 //app.cpp //#include "Sport.h" //#include "Person.h" using namespace std; void main(){ Sport s1("足球"); Sport s2("篮球"); Sport s3("乒乓球"); Sport s4("跳高"); Person p1("张三"); p1.like(&s1); p1.like(&s2); p1.like(&s3); p1.like(&s4); //超过3项不存储 p1.print(); Person p2("李四"); p2.like(&s4); p2.like(&s3); p2.like(&s2); p2.like(&s1); //超过3项不存储 p2.print(); } 例3.4中,创建了2个Person对象,4个Sport对象。Person对象和Sport对象之间是多对多对应关系,其多对多对应关系如图3.9所示。 图3.9多对多对应关系 例3.4程序的输出结果如下。 张三喜欢足球 篮球 乒乓球 李四喜欢跳高 乒乓球 篮球 上面介绍了使用指针或指针数组表示关联的两种方法,这两种表示方法都能够准确地表示关联的语义。 但很多面向对象程序设计语言不支持或不建议使用指针,怎么办? 解决的办法是将指针改为引用,即使用对象的引用来实现关联。从本质上讲,引用是对指针的封装,引用的内部实现仍然使用指针,但引用更加安全。 可将上述示例程序中的指针修改为引用,使用引用或引用数组来实现关联。建议读者尝试一下,一定会发现很多问题,从而对学习编程有更深刻的体会。 视频讲解 3.3组合与聚合关联 关联分为一般关联(Association)、聚合(Aggregation)和组合(Composition )3种类型。一般关联,主要描述客观事物之间的相互关系,前面已经进行了介绍。 在介绍聚合和组合两个概念前,先讨论观察事物内部构成的思维方式。按照“一个客观事物由更小的客观事物构成的”观点,可将一个客观事物视为一个“整体”,将构成这个事物的更小事物视为“整体”的一个“部分”。 例如,一个人由头、躯体、肢体组成。具体地说,一个人是一个整体,包含一个头、一个躯体和四肢等部分,而每个部分也可以再细分。例如,头可分为眉、眼、耳、鼻、口等五官,包含两条眉、两只眼、两只耳、一只鼻、一张口。人体的构成,如图3.10所示。 例如,汽车由很多系统组成,包含很多零部分,主要有发动机、车轮等,可用类图描述汽车与发动机、轮子之间的关系,其类图如图3.11所示。 图3.10人体的构成 图3.11汽车的类图 如图3.11所示,汽车包含发动机和车轮,一辆汽车有一台发动机,有4个车轮。一台发动机可安装到一辆汽车,一个车轮可安装到一辆汽车,但安装到一辆汽车后就不能同时安装到另外一辆汽车。 每个事物都有自己的生命周期,不仅要观察每个事物由哪些“部分”构成,还要观察每个“部分”的生命周期与事物的生命周期是否同步。 当一个人出生时,这个人就有了头、躯体、肢体,当一个去世时,这个人的头、躯体、肢体同时死亡,因此,人这个“整体”与头、躯体、肢体等“部分”具有相同的生命周期。 可以将一辆汽车的发动机和车轮从该汽车上拆卸下来,再安装到另外一辆汽车上,显然,汽车这个“整体”与其“部分”具有不同的生命周期。 在面向对象程序设计中,组合关联和聚合关联都是描述“整体”与“部分”之间的构成关系,并将具有相同生命周期的构成关系称为组合关联,将具有不同生命周期的构成关系称为聚合关联。 图3.10中,使用组合关联来描述人的内部结构; 图3.11中,使用聚合关联来描述汽车的内部结构。 3.3.1使用对象实现组合关联 当一个学生入学时,学校为每个学生分配一个唯一的学号,学生与学号之间具有组合关联的特点,可使用组合关联描述学生与学号之间的关系。学生和学号之间的组合关联,如图3.12所示。 图3.12学生和学号之间的组合关联 一般使用对象表示多对一组合关联中的连接。例如,类Student中,声明了属性studentID,其数据类型为类StudentID,用于存储类StudentID的一个对象,表示多对一组合关联studentID中的一个连接,示例代码如例3.5所示。 【例3.5】使用对象表示多对一组合关联的连接。 //StudentID.h class StudentID { public: StudentID(); StudentID(int id); ~StudentID(); void print(); private: bool isValid(void); int value; }; //StudentID.cpp #include #include #include "StudentID.h" using namespace std; StudentID::StudentID(){ cout << "\t" << "调用构造函数 StudentID()" << endl; } StudentID::StudentID(int id){ cout << "\t"<<"调用构造函数 StudentID(" << id << ")" << endl; if (isValid()) value = id; } StudentID::~StudentID(){ cout << "析构StudentID:" << value< #include #include "StudentID.h" #include "Student.h" using namespace std; Student::Student(){ cout << "调用构造函数 Student()" << endl << endl; } Student::Student(int id) :studentID(id)//增加了新的语法 { cout << "调用构造函数 Student(" << id << ")" << endl << endl; } Student::Student(int id, char * pName) :studentID(id) { cout << "调用构造函数 Student(" << id << "," << pName << ")" << endl << endl; strncpy(name, pName, sizeof(name)); name[sizeof(name)-1] = '\0'; } Student::Student(int id, char * pName, int xHours, float xgpa) :studentID(id) { cout << "调用构造函数 Student(" << id << "," << pName << "," << xHours << "," << xgpa << ")" << endl << endl; strncpy(name, pName, sizeof(name)); name[sizeof(name)-1] = '\0'; semesHours=xHours; gpa = xgpa; } Student::~Student() { cout << "析构Student:"; studentID.print(); } Student的构造函数中,使用冒号(:)语法同步Student对象与成员对象studentID的生命周期。例如,语句Student::Student(int id):studentID(id),冒号的前面是构造函数的原型Student::Student(int id),而后面的studentID(id)规定了创建成员对象studentID的方法,即创建Student对象的过程中,按照StudentID studentID(id)的语义创建成员对象studentID。 //app.cpp #include "StudentID.h" #include "Student.h" void main() { Student s1(210101,"Randy"); Student s2(210102, "Randy"); Student s3(210103, "Jenny",10,3.5); } main()函数中使用3条语句创建了类Student的3个对象。其中,语句Student s1(210101,"Randy")创建学生Student的对象s1,对象s1的创建过程如图3.13所示。 图3.13对象s1的创建过程 如图3.13所示,语句Student s1(210101,"Randy")创建学生Student的对象s1时,先按照声明顺序依次给成员变量name[20]、semesHours、gpa和studentID分配内存,然后再执行函数调用Student(210101,"Randy")初始化对象s1。 执行函数调用Student(210101,"Randy")过程中,先将实参210101和"Randy"分别传递给形参id和pName,然后执行函数调用studentID.StudentID(id)初始化成员对象studentID,最后执行类Student的构造函数的函数体。对象s1的物理结构如图3.14所示。 图3.14对象s1的物理结构 如图3.14所示,对象s1的内存中包含对象s1.studentID的内存,对象s1.studentID的内存中包含s1.studentID.value的内存,其中的值为学号210101。 创建和删除对象s1过程中,都将对象视为一个整体来管理,并没有区分是成员对象还是成员变量。这样,程序员就不需要关心对象的内部结构,不需要考虑哪些是成员变量,哪些是成员对象,都可以当成变量来处理,使用非常方便。 例3.6中,总共创建了类Student的3个对象和StudentID的3个对象,每个Student对象包含一个StudentID对象,表示组合关联studentID中的一个连接。组合关联studentID中的连接如图3.15所示。 图3.15组合关联studentID中的连接 例3.6程序的输出结果如下。 调用构造函数 StudentID(210101) 调用构造函数 Student(210101,Randy) 调用构造函数 StudentID(210102) 调用构造函数 Student(210102,Randy) 调用构造函数 StudentID(210103) 调用构造函数 Student(210103,Jenny,10,3.5) 析构Student:210103 析构StudentID:210103 析构Student:210102 析构StudentID:210102 析构Student:210101 析构StudentID:210101 从逻辑上讲,类Student的对象中的成员对象studentID,也是一个对象,创建时也要调用类StudentID的构造函数,在删除时也要调用其析构函数。删除对象的步骤是,先调用类StudentID的析构函数,再调用类Student的析构函数,最后回收类Student的对象的所有内存。 例3.6中,创建了类Student的对象及其类StudentID的对象,在删除类Student的对象时,也同时删除了类StudentID的对象,同步了类Student的对象及其类StudentID对象的生命周期,实现了多对一组合关联的语义。 3.3.2使用指针实现组合关联 除了使用对象实现组合关联外,还可使用指向对象的指针实现组合关联,但需要编写代码同步对象的生命周期。 例如,可使用指针实现类Person和Tdate之间的组合关联,但需要在构造函数和析构函数中编写代码以同步对象的生命周期。使用指针存储组合关联中的连接,如图3.16所示。 图3.16使用指针存储组合关联中的连接 如图3.16所示的类Person中,声明了一个属性birthday,用于指向组合关联birthday中连接的Tdate对象。Person对象的内存中只存储了指针birthday,而没有存储其指向的Tdate对象。 因连接的Tdate对象不在Person对象的内存,创建Person对象时系统不会自动创建连接的Tdate对象,删除Person对象时系统也不会自动删除连接的Tdate对象,因此,需要将创建Tdate的职责赋予Person的构造函数和拷贝构造函数,删除Tdate的职责赋予Person的析构函数,以实现组合关联的语义。使用指针实现组合关联的示例代码如例3.7所示。 【例3.7】使用指针实现组合关联。 #include #include "Tdate.h" #include using namespace std; class Person { public: Person(){}; Person(char n[],Tdate d) { strncpy(name, n, sizeof(name)); name[sizeof(name)-1] = '\0'; birthday = new Tdate(d); //创建一个新的Tdate对象 }; ~Person(){ delete birthday; //删除连接的Tdate对象 }; void print(){ cout << "Person:"<print(); }; Tdate getBirthday() const{ return Tdate(*birthday); //返回一个新对象而没有返回指针birthday,以保证安全 } Person(const Person& oldPerson){ memcpy(this, &oldPerson, sizeof(Person)); // 将oldPerson的内存中的数据复制到内存 birthday = new Tdate(oldPerson.getBirthday()); //创建一个新的Tdate对象 }; private: Tdate* birthday; //使用指针表示多对一组合关联 char name[20]; }; 拷贝构造函数Person(const Person& oldPerson)中,语句memcpy(this, &oldPerson, sizeof(Person))将oldPerson的内存中的数据复制到当前对象的内存中,其中只复制了指向连接对象的指针,需要为当前Person对象重新创建一个Tdate对象。语句birthday = new Tdate(oldPerson.getBirthday())就实现了一功能。 Person fn(Person p){ return p; } Person* fnPtr(Person* p){ return p; } void main(){ Tdate d1(1, 2, 2000); Tdate d2(1, 2, 2021); cout << "****Person对象****" << endl; Person p1("张三",d1); Person p2("李四", d1); cout << "****传递*Person对象****" << endl; fn(p1).print(); cout << "****传递*Person对象指针****" << endl; fnPtr(&p2)->print(); cout << "****main语句结束****" << endl; } 例3.7程序的输出结果如下。 构造:00CFF710-> 2/1/2000 构造:00CFF6FC-> 2/1/2021 ****Person对象**** 拷贝构造:00CFF58C-> 2/1/2000 拷贝构造:01059D30-> 2/1/2000 析构:2/1/2000 拷贝构造:00CFF58C-> 2/1/2000 拷贝构造:0105BA58-> 2/1/2000 析构:2/1/2000 ****传递*Person对象**** 拷贝构造:0105BAA0-> 2/1/2000 拷贝构造:0105BAE8-> 2/1/2000 析构:2/1/2000 Person:张三,2/1/2000 析构:2/1/2000 ****传递*Person指针**** Person:李四,2/1/2000 ****main语句结束**** 例3.7中创建了2个Person对象,4个Tdate对象。2个Person对象分别连接到其中的2个Tdate对象,创建的对象及其连接如图3.17所示。 图3.17例3.7创建的对象及其连接 创建2个Person对象时,同时在堆中创建了2个无名Tdate对象,删除这两个Person对象时,也同时删除了堆中的2个无名Tdate对象。Person对象的创建和删除过程如图3.18所示。 图3.18Person对象的创建和删除过程 fn(p1).print()和fnPtr(&p2)>print()两条语句,演示了采用传值和传地址两种方式传递Person对象的过程。函数调用fn(p1)中,调用了拷贝构造函数传递和返回Person对象。其中,创建和删除了连接的Tdate对象,而函数调用fnPtr(&p2)中,只传递和返回了Person对象的地址,效率明显高于函数调用fn(p1)。 类Person中,类Tdate主要起到数据类型的作用,这就要求类Tdate的对象具有更好的计算特性。 作为数据类型使用的类,主要是为了计算,一般不会关注其具体的业务功能。 在组合关联中,由于“整体”和“部分”的生命周期相同,在创建“整体”时需要同时创建“部分”,在删除“整体”时需要同时删除“部分”,创建和删除“部分”的职责自然而然地赋予了构造函数和析构函数。 3.3.3使用代码实现聚合关联 聚合关联中,“整体”和“部分”都有自己的生命周期,由于“整体”和“部分”的生命周期不同,一般不会使用成员对象表示聚合关联中的连接,而使用成员指针表示聚合关联中的连接。 例如,图3.11中,汽车与发动机、车轮之间的关系都是聚合关联,可使用指针表示汽车与发动机之间的连接,使用指针数组表示汽车与车轮之间的多个连接。汽车及其部件如图3.19所示。 图3.19汽车及其部件 如图3.19所示,类Motor和Wheel中,都设计了属性serialNumber,该属性用于存储产品序列号,以保证计算机中每一个对象都与实际的汽车部件一一对应。类Motor和Wheel的代码如例3.8所示。 【例3.8】类Motor和Wheel。 // Motor.h class Motor { public: Motor(int sn,float fPower); ~Motor(); void print(); private: int serialNumber; //产品序列号 float power; //发动机的排量 }; // Motor.cpp #include #include "Motor.h" using namespace std; Motor::Motor(int sn,float fPower){ cout << "调用构造函数 Motor(" << sn << "," << fPower << ")" << endl ; serialNumber = sn; power = fPower; } Motor::~Motor(){ cout << "析构Motor:" << serialNumber << endl; } void Motor::print(){ cout << serialNumber << "," << power ; } // Wheel.h class Wheel { public: Wheel(int sn,float fSize); ~Wheel(); void print(); private: int serialNumber; //产品序列号 float size; //车轮大小 }; //Wheel.cpp #include "Wheel.h" #include using namespace std; Wheel::Wheel(int sn, float fSize){ cout << "调用构造函数 Wheel(" << sn <<","<< fSize << ")" << endl ; serialNumber = sn; size = fSize; } Wheel::~Wheel(){ cout << "析构Wheel:" << serialNumber << endl; } void Wheel::print(){ cout << serialNumber << "," << size ; } 类Car中设计了wheel和motor两个属性,这两个属性分别表示两个聚合关联wheel和motor,其中,属性wheel是一个指针数组,共有4个指针,分别对应一辆汽车的4个车轮; 属性motor是指向类Motor的一个对象,对应汽车的发动机。 类Car中还设计了5个成员函数,其中,Car(Motor& mMotor, Wheel* aWheel[])构造函数有两个参数,第一个参数是Motor的引用,用于传递代表发动机的对象; 第二个参数是一个指针数组,用于传递代表车轮的4个对象,其功能相当于将一台发动机和4个车轮组装成一辆汽车,以模拟汽车的组装工序。 Motor&set(Motor&depart)成员函数的功能为替换汽车的发动机,将通过参数depart传递的发动机安装(set)到汽车上,并以引用方式返回拆卸下来的发动机。Wheel&set(Wheel&depart, int position) 成员函数的功能为更换一个车轮,将通过参数depart传递的一个车轮安装(set)在汽车的位置(position)上,并以引用方式返回拆卸下来的车轮。这两个成员函数模拟了更换汽车部件的实际场景。类Car的代码如例3.9所示。 【例3.9】类Car。 // Car.h #include"Wheel.h" #include "Motor.h" class Car { public: Car(Motor& mMotor, Wheel** aWheel); ~Car(); Motor& set(Motor& depart); Wheel& set(Wheel& depart, int position); void print(); private: Wheel* wheel[4]; //指向4个车轮 Motor* motor; //指向发动机 }; 其中,Wheel* wheel[4]声明了一个指针数组,用于指向代表4个车轮的对象。Motor* motor声明了一个指针,用于指向代表发动机的对象。 // Car.cpp #include "Motor.h" #include "Wheel.h" #include "Car.h" #include using namespace std; Car::Car(Motor& mMotor, Wheel* aWheel[]) :motor(&mMotor) { cout << "调用构造函数 Car("; mMotor.print(); cout<< ")" << endl; for (int i = 0; i < 4; i++){ wheel[i] = aWheel[i]; } } Car::~Car(){ cout << "析构Car:" << endl; for (int i = 0; i < 4; i++){  //删除4个车轮对象 delete wheel[i]; } delete motor;  //删除发动机对象 } Motor& Car::set(Motor& depart){ Motor& rt = *motor; motor = &depart; return rt; } Wheel& Car::set(Wheel& depart, int position){ Wheel& rt = *wheel[position]; wheel[position] = &depart; return rt; } void Car::print(){ cout << "汽车\t发动机:"; motor->print(); cout << "\t\t车轮:"; for (int i = 0; i < 4; i++){ wheel[i]->print(); cout << "|"; } cout << endl; } 在制造汽车的过程中,最后将已制造出的各种部件组装成一辆汽车,因此,类Car的构造函数中没有创建Motor 和Wheel的对象,只存储了表示连接的指针。但当一辆汽车报废时,需要报废汽车的所有部件,因此,析构函数~Car()中,删除了代表发动机和4个车轮的对象。 参考汽车的装配场景,编程一个主程序,示例代码如例3.10所示。 【例3.10】主程序。 //CarApp.cpp #include "Motor.h" #include "Wheel.h" #include "Car.h" #include using namespace std; void main(){ cout << "***********创建发动机***********" << endl; Motor& m1 = *(new Motor(101,1.6));  //为堆中的对象取了一个名称,以便后面使用 Motor& m2 = *(new Motor(102,2.0)); Motor& m3 = *(new Motor(103,1.6)); cout << "***********创建车轮***********" << endl; const int len = 10; Wheel* wBase[len]; for (int i = 0; i < len; i++){ wBase[i] = new Wheel(201+i,10); } cout << "***********创建汽车***********" << endl; Car& c1 = *(new Car(m1, wBase)); c1.print(); Car& c2 = *(new Car(m2, wBase+4)); c2.print(); cout << "***********更换并报废部件***********" << endl; Wheel&w1 = c1.set(*wBase[8], 1); //换第一个车轮 delete &w1;  //报废车轮,删除它 Motor&t = c1.set(m3); //换发动机 delete &t;   //报废发动机,删除它 cout << "***********报废汽车***********" << endl; delete &c1; delete &c2; cout << "***********删除库存中没有使用的车轮***********" << endl; delete wBase[9]; } CarApp.cpp中,负责创建类Car、Motor和Wheel的对象,也负责删除没有安装到汽车上的Motor和Wheel对象。类Car、Motor和Wheel的对象都存储在堆区中,需要通过代码来创建和删除这些对象。 图3.20表达式Motor& m1= *(new Motor(101,1.6))的计算顺序 表达式语句Motor&m1=*(new Motor(101,1.6))的作用是在堆区中创建Motor的一个无名对象,并给它取一个别名,其计算顺序如图3.20所示。 如图3.20所示,表达式Motor& m1 = *(new Motor(101,1.6))中包含4个步骤: ①定义Motor对象的一个引用m1,从本质上讲,可将变量或对象的定义视为一种运算,事实上很多面向对象程序设计语言也是这样做的; ②在堆中创建Motor的一个无名对象,并返回这个对象的指针; ③取指针的对象,即堆中创建的无名对象; ④将引用m1初始化为无名对象,即使用m1命名无名对象。 main()函数中,先在堆中创建了类Motor的3个对象m1、m2和m3,然后创建了类Wheel的10个对象,并用指针数组存储10个对象的指针,最后创建了类Car的两个对象c1和c2。汽车和部件的对象及其连接,如图3.21所示。 图3.21汽车和部件的对象及其连接 图3.22表达式Wheel&w1= c1.set(*wBase[8],1)的计算顺序 创建了类Car的两个对象c1和c2后,使用表达式Wheel&w1 = c1.set(*wBase[8],1)更换汽车c1第1个位置上的车轮,其中,c1.set(*wBase[8],1)是一个函数调用,调用了类Car的Wheel& Car::set(Wheel& depart, int position)成员函数,其通过引用方式返回一个无名的Wheel对象。为了访问返回的对象,为返回的对象定义了一个引用w1。该表达式的计算顺序如图3.22所示。 如图3.22所示,表达式Wheel&w1=c1.set(*wBase[8],1)中包含6个步骤,其中,第①步定义了Wheel对象的一个引用w1; 第⑤步,函数调用c1.set(*wBase[8],1)返回了一个对象的引用,返回的引用没有名称; 第⑥步,为返回的对象定义了一个引用w1。 后面的语句delete &w1中,使用定义的引用w1删除函数调用c1.set(*wBase[8],1)返回的无名对象。 例3.10程序的输出结果如下。 ***********创建发动机*********** 调用构造函数 Motor(101,1.6) 调用构造函数 Motor(102,2) 调用构造函数 Motor(103,1.6) ***********创建车轮*********** 调用构造函数 Wheel(201,10) 调用构造函数 Wheel(202,10) 调用构造函数 Wheel(203,10) 调用构造函数 Wheel(204,10) 调用构造函数 Wheel(205,10) 调用构造函数 Wheel(206,10) 调用构造函数 Wheel(207,10) 调用构造函数 Wheel(208,10) 调用构造函数 Wheel(209,10) 调用构造函数 Wheel(210,10) ***********创建汽车*********** 调用构造函数 Car(101,1.6) 汽车 发动机:101,1.6 车轮:201,10|202,10|203,10|204,10| 调用构造函数 Car(102,2) 汽车 发动机:102,2 车轮:205,10|206,10|207,10|208,10| ***********更换并报废部件*********** 析构Wheel:202 析构Motor:101 ***********报废汽车*********** 析构Wheel:201 析构Wheel:209 析构Wheel:203 析构Wheel:204 析构Motor:103 析构Car: 析构Wheel:205 析构Wheel:206 析构Wheel:207 析构Wheel:208 析构Motor:102 析构Car: ***********删除库存中没有使用的车轮*********** 析构Wheel:210 可对照图3.21并结合例3.10程序的输出结果,分析对象的删除过程。 在“更换并报废部件”过程中,删除序列号为202的Wheel对象和序列号为101的 Motor对象。在“报废汽车”过程中,Car的构造函数中删除了连接到的4个Wheel对象和1个Motor对象,此时,总共删除了9个Wheel对象、3个 Motor对象和2个Car对象,还剩下序列号为210的1个Wheel对象没有删除。最后增加了一条语句删除它。这样就删除了程序中创建的所有对象。 示例中,CarApp.cpp的代码最复杂,其次是Car.cpp中的代码,其他源文件中的代码都比较简单,需要仔细阅读CarApp.cpp和Car.cpp中的代码。 总之,实现聚合关系的一般方法是使用指针来表示聚合关系中的连接,通过代码来管理各个对象的生命周期。方法很简单,但需要程序员单独管理各个对象,工作量大,非常烦琐,特别是在需要管理的对象成百上千时,工作量和工作难度会超过人工能力的范围。 为了解决这个问题,常常会编写一些程序来专门管理内存中的对象,以减轻程序的负担。例如,在上述示例中,增加两个类MotorBase和WheelBase专门管理对象。类MotorBase负责创建和删除类Motor的所有对象,承担管理Motor对象的职责。类MotorBase负责创建和删除类Wheel的所有对象,承担管理Wheel对象的职责。类Car只负责使用这些对象,承担使用这些对象的职责,这三个类各司其职、分工合作,共同完成所需完成的任务。专门管理对象的类MotorBase和WheelBase如图3.23所示。 图3.23专门管理对象的类MotorBase和WheelBase 面向对象程序设计中,管理程序中的对象是很重要的基础工作,在学习面向对象程序设计过程中需要掌握相关的技术,理解其原理。只有这样,才能理解系统提供的自动管理对象功能,例如,理解Java语言中提供的自动清理对象功能。 视频讲解 3.4深入理解类及其对象 类与对象是面向对象程序设计的基础,封装类并给其对象赋予相应的职责是面向对象程序设计的主要工作。下面从编程实现视角分析类中隐含的(组合)关联及其连接,讨论对象的内部结构,探讨类及其对象的本质。 例如,表示学生的类Student,具有自己的属性name和gpa,存在与学号之间的组合关联studentID,其类图如图3.24所示。 图3.24简化后的学生类 使用语句Student s1(210101,"Randy",3.5)创建类Student的对象s1,对象s1用于表示客观世界中的一个学生Randy,可按照映射来理解对象s1与其属性值之间的对应关系。对象s1中的映射如图3.25所示。 图3.25Student对象s1(210101,"Randy",3.5)中的映射 如图3.25所示,客观世界中的学生可视为一个集合,每一个学生都视为学生集合中的一个元素。同样,计算机世界中的类Student和类StudentID也可视为一个集合,它们的对象就是集合中的元素,数据类型char、int和float也可视为一个集合,每一个值就是数据类型中的一个元素。 使用对象s1表示客观世界中的一个学生Randy,可理解为,集合Student中的一个元素s1映射到学生集合中的元素Randy。此外,将集合Student中元素s1映射到集合float中的元素3.5,并用元素3.5表示学生Randy的绩点; 将元素s1映射到集合StudentID中的元素s1.studentID,再映射到集合int中的一个元素,并用元素210101表示学生Randy的学号。 元素s1映射到集合char中的多个元素,相对复杂一些,其中包含5个映射,使用一个数组name来存储这5个映射,并通过数组的下标映射到集合char中的5个元素。对象s1中的映射,如图3.26所示。 图3.26对象s1中的映射 图3.26中,使用组合连接描述了对象s1到其属性值的映射,箭头描述了映射的方向。语句Student s2(210103,"Jenny",3.0)创建了对象s2,对象s2中的映射,如图3.27所示。 图3.27对象s2中的映射 比较分析如图3.26和图3.27所示的对象图,可将其中的对象抽象为类,将其中的连接抽象为组合关联,绘制出一张类图,这张类图描述了类Student中包含的映射关系。类Student中包含的映射关系如图3.28所示。 图3.28类Student中包含的映射关系 分析对比如图3.24和图3.28所示的两张类图会发现,类的每个属性都描述了一种映射关系,其对象中的每个属性值都描述了一种映射关系中的一个映射。 类的一个属性描述了一种映射关系,对象的一个属性值描述了其中的一个映射。 经过前面的讨论,要理解如图3.24所示的类Student,需要将其细化为如图3.28所示的类图,并按照细化后的类图联想到如图3.25所示的实际场景,才能真正理解类Student的语义。 同样,也应该按照这个思路理解对象图。例如,在理解描述对象s1的对象图时,需要按照如图3.26所示的对象图来理解,并再联想到如图3.25所示的实际场景,才能真正理解对象s1的语义。描述对象s1的对象图,如图3.29所示。 图3.29对象s1的对象图 计算机世界中的对象怎样表示客观世界中的事物?成员函数调用怎样表示客观事物的行为?这是两个基本问题,建议读者带着这两个基本问题重读本章前面的内容,相信会有自己的答案。 3.5字符串 字符串是使用最多的数据,没有之一。字符串最终都是用字符数组来存储,下面先讨论字符数组的语义,再介绍封装字符串类的方法。 3.5.1数组中的概念及其关系 一个数组Array包含多个元素element,每个元素element都有相同的数据类型Type,数组Array与数据类型Type之间属于组合关联,元素element就是这个组合关联的名称。数组Array还有一个数组名arrayName,数组名arrayName是一个指针ptr(地址),指向数组Array。 上述描述中,使用了数组Array、数据类型Type和指针ptr 3个概念,并用元素element和数组名arrayName描述了这3个概念之间的关系。数组中的概念及其关系,如图3.30所示。 图3.30数组中的概念及其关系 希望按照如图3.30所示的关系,可通过指针ptr访问到数组,通过数组可访问到数组中元素,通过数组元素最终访问到元素值,即从一个指针ptr最终映射到一个元素值。 但有一个问题,数组与元素之间是一对多关系,一个数组不能映射到其中的一个元素。为了解决这个问题,规定了数组中的元素是有序的,并使用序号来确定访问哪个元素。这就是数组下标运算的由来。 3.5.2字符数组的语义 3.3.2节示例中介绍了类Person,下面仍然以它为例继续讨论字符数组的语义。 如图3.16所示,类Person的属性name是一个字符数组,字符数组name[20]作为一个整体存储在其对象的内存中。按照如图3.30所示的概念及其关系,可使用组合关联表示类Person到字符数组、字符数组到数据类型char之间的映射关系,细化属性name的语义。属性name隐含的组合关联如图3.31所示。 图3.31类Person的属性name隐含的组合关联 如图3.31所示,使用组合关联element将20个字符组合连接成字符数组charArray,再使用组合关联name将字符数组charArray组合连接到Person对象,20个字符最终成为Person对象的一部分。 在实际应用中,姓名不可能刚好都是20个字符。为了解决这个问题,提出了两种解决办法。第一种解决办法是,允许比规定的20个字符少,并增加一个字符串的结束标志'\0',结束标志前面的元素才有对应字符,这样会浪费后面的内存。如图3.16所示类Person中,就是选择了这种解决办法。 第二种解决办法是,使用动态数组存储姓名中的字符。一个姓名中有多少个字符就分配多少个字节。这个办法能够充分利用内存,但也存在一个问题,编译器自动管理的内存必须是固定长度的,不能是动态变化的,因此,需要程序员编写代码来动态管理数组的内存。 动态字符数组的指针是固定长度的,可以嵌套到Person对象的内存,并让系统自动管理。存储字符数组指针的类Person如图3.32所示。 图3.32存储字符数组指针的类Person 如图3.32所示的类图,清晰地表示出一个人到一个字符之间的映射关系。例如,姓名为Randy的Person对象p中,对象p通过属性name映射到指针address,指针address映射到一个字符数组A,字符数组A映射到其中的一个元素A[i],元素A[i]再映射到“Randy”中的一个字符。总共需要经过4个映射,才能访问姓名中的一个字符。Person对象p到字符的映射过程如图3.33所示。 图3.33Person对象p到字符的映射过程 在实际应用中,如果将属性name直接存储指向字符数组的指针,一般会省略图3.32中的类ptrChar和charArray及其映射关系,但从省略的类图中也要读出这层语义。 属性name中存储指向字符数组的指针时,系统不会自动管理指向的字符数组,而需要通过编程来同步Person对象和字符数组charArray的生命周期,实现组合关联的语义。具体编程方法可参考3.3节。 3.5.3自定义字符串类myString 为了方便使用,可将字符数组封装为一个类myString,并赋予类myString存储和管理字符串的职责。 可将如图3.32所示的类ptrChar组合关联到类myString,用于存储字符数组的地址,并增加表示字符数组大小的属性len。类myString及其组合关联如图3.34所示。 图3.34类myString及其组合关联 如图3.34所示,类myString中封装了两个属性,属性ptrCharArray用于存储动态字符数组的地址,属性len用于存储字符串的长度。 类myString中封装了6个成员函数,其中,myString(char s[])构造函数负责创建并初始化字符数组,~myString()析构函数负责删除字符数组,通过构造函数和析构函数同步类myString与字符数组的生命周期,实现组合关联ptrCharArray的语义。类myString的代码如例3.11所示。 【例3.11】字符串类myString。 // myString .h class myString { public: myString(char s[]); ~myString(); void print()const; int getLen()const; char* getString()const; myString(const myString& oldMyString); private: char* ptrCharArray; int len; }; // myString.cpp #include "myString.h" #include using namespace std; myString::myString(char s[]){ len = strlen(s); ptrCharArray = new char[len + 1]; strncpy(ptrCharArray, s, len + 1); ptrCharArray[len] = '\0'; } myString::~myString(){ delete ptrCharArray; } void myString::print()const{ cout << ptrCharArray << endl; } int myString::getLen()const { return len; } const char* myString::getString()const{ return ptrCharArray; } myString::myString(const myString& oldMyString){ len = oldMyString.getLen(); ptrCharArray = new char[len + 1]; strncpy(ptrCharArray, oldMyString.getString(), len + 1); } 类myString的代码中,getLen()成员函数返回字符串的长度,getString()成员函数返回字符串的指针,但这个指针指向的是const char*,不允许通过这个指针修改字符数组中的字符,以保证安全。 类Person中,可以像使用基本数据类型一样使用类myString,像变量一样使用类myString的对象。类Person中使用类myString的主要代码如例3.12所示。 【例3.12】类Person中使用类myString。 // Person.cpp #include "myString.h" #include using namespace std; class Person { public: Person(){} Person(myString n) :name(n.getString()){} void print(){ cout << "Person:"; name.print(); } Person(const Person& oldPerson) :name(oldPerson.name) {} private: myString name; //表示到myString的组合关联 }; 类Person的代码中,myString name表示到myString的组合关联,代码name(n.getString())维护这个组合关联。代码简洁,充分体现了类myString的优越性。 Person fn(Person p){ return p; } myString fn(myString p){ return p; } void main(){ cout << "****创建myString对象****" << endl; myString s1("张三"); s1.print(); myString s2(s1); s2.print(); cout << endl << "****传递*myString对象****" << endl; fn(s1).print(); cout << endl << "****创建Person对象****" << endl; Person p1("李四"); p1.print(); Person p2(s1); p2.print(); cout << endl << "****传递*Person对象****" << endl; fn(p1).print(); cout << endl << "***堆中创建*Person对象****" << endl; Person* p3 = new Person("王五"); p3->print(); cout << "****main语句结束****" << endl; } 例3.12程序的输出结果如下。 ****创建myString对象**** 张三 张三 ****传递*myString对象**** 张三 ****创建Person对象**** Person:李四 Person:张三 ****传递*Person对象**** Person:李四 ***堆中创建*Person对象**** Person:王五 ****main语句结束**** 类myString封装了字符数组,编写主程序和类Person代码的程序员,不需要了解类myString的内部实现,只需将类myString当成数据类型来使用,这样明显降低了编程难度,提高了编程效率。 视频讲解 3.6应用举例: 链表 链表是常用的数据结构,常常用于动态管理创建的对象。例如,使用链表管理学生,其类图如图3.35所示。 图3.35使用链表管理学生 在如图3.35所示的类图中,总共有3个关联。关联pFirst和pNext是自关联,即类Student关联到它自己,其连接是一个Student对象到另一个Student对象。关联pFirst是静态的,使用一个静态指针static Student* pFirst来表示,类Student的所有对象共享指针变量pFirst,用于指向链表中第一个Student对象。使用一个指针Student* pNext表示关联pNext,类Student的每个对象都有一个指针变量pNext,用于指向链表中的下一个Student对象。 关联name是一个组合关联,其中的类myString是在3.5节中封装的,这里直接使用,用于存储学生的姓名。使用链表管理学生,示例代码如例3.13所示。 【例3.13】使用链表管理学生。 #include "myString.h" #include "Student.h" #include using namespace std; class Student { public: static int number(void); static Student* findname(char* pName); Student(char * pName); ~Student(); private: myString name;  //存储组合关联name中的连接 Student* pNext;  //指向链表中的下一个Student对象 static Student* pFirst;  //指向链表中的第一个Student对象 static int noOfStudents;  //存储学生人数 }; int Student::noOfStudents = 0; Student* Student::pFirst = NULL; int Student::number(void){ return noOfStudents; } Student* Student::findname(char* pName){ for (Student* pS = pFirst; pS; pS = pS->pNext) if (strcmp(pS->name.getString(), pName) == 0) return pS; return NULL; } Student::Student(char * pName) :name(pName) { cout << "插入:" << this->name.getString() << endl; pNext = pFirst; //每新建一个结点(对象),就将其挂在链首 pFirst = this; } Student::~Student(){ cout << "删除:" << this->name.getString() << endl; if (pFirst == this){ //如果要删除链首结点,则只要链首指针指向下一个 pFirst = pNext; return; } for (Student* pS = pFirst; pS; pS = pS->pNext){ if (pS->pNext == this){ //找到时,pS指向当前结点的结点 pS->pNext = pNext; //pNext即this->pNext return; } } } void main(){ Student s1("Randy"); new Student("Jenny"); Student s2("Kinsey"); cout << "查找Jenny:" ; Student* pS1 = Student::findname("Jenny"); if (pS1) cout << "ok." << endl; else cout << "no find." << endl; delete pS1; cout << "查找Jenny:" ; Student* pS2 = Student::findname("Jenny"); if (pS2) cout << "ok." << endl; else cout << "no find." << endl; } main()函数中,总共创建了3个Student对象,构成一个链表,其中,语句new Student("Jenny")在堆中创建了一个无名对象。3个Student对象构成的链表如图3.36所示。 图3.363个Student对象构成的链表 语句Student*pS2=Student::findname("Jenny")在链表中按照姓名查找,Student::findname("Jenny")返回链表中的无名对象。语句delete pS1删除找到的无名对象,删除对象后的链表如图3.37所示。 图3.37删除无名对象后的链表 例3.13程序的输出结果如下。 插入:Randy 插入:Jenny 插入:Kinsey 查找Jenny:ok. 删除:Jenny 查找Jenny:no find. 删除:Kinsey 删除:Randy 例3.13中的类 Student中,没有定义拷贝构造函数,默认拷贝构造函数不会将创建的Student对象插入到链表,因此,如果将Student对象作为函数的参数或返回值,函数调用过程中的中间对象没有插入到链表中。 读者可以编写代码,测试函数调用过程中能否正确传递Student对象,评价Student对象参与计算的能力。实际上,这是一个比较复杂的问题,需要平衡多方面的因素,值得深入分析。 小结 本章从客观事物之间的关系出发,主要学习了关联及连接的概念以及描述事物之间关系的方法,重点学习了使用组合关联描述客观事物的内部结构,以及一般关联、组合关联和聚合关联的编程实现技术和方法,最后学习了字符串和链表两个应用案例。 学习了关联及连接的概念,举例说明了使用指针和指针数组实现多对一、多对多关联的编程方法和技术。希望读者能够理解关联及连接的概念,掌握使用关联的方法和编程技术。 学习了组合关联和聚合关联的概念,举例说明了使用组合关联描述事物内部构成的方法,以及组合关联和聚合关联的表示方式和编程技术。希望读者能够理解组合关联和聚合关联的概念,掌握其使用方法和编程技术。 最后学习了字符串和链表两个应用案例,希望读者能够从关联角度理解字符串和链表等复杂数据结构,了解封装复杂数据结构的思路,掌握编程实现的方法。 练习 1. 饭厅中拟放置1张饭桌和8个凳子,并抽象出了饭厅、饭桌和凳子3个类及其关系,如图3.38所示。请使用集合和映射描述其中的类及其关系。 图3.38饭厅、饭桌和凳子及其关系 2. 结合实际情况完善图3.38中的类图,增加描述饭厅、饭桌和凳子特性的属性,给每个类赋予适当的职责并增加相应的成员函数,最后编程实现。 3. 3.2节中使用指针表示关联,为了提高代码的安全性,请使用引用表示关联,并改写其中的例程代码。 4. 按照如图3.39所示的类图编写程序。要求使用动态数组存储一个人的姓名,类Person的对象负责管理其中存储的姓名,并编程实现。 图3.39使用动态数组存储姓名 其中,char为基本数据类型,charArray为动态字符数组,代码中可以不声明类charArray。 5. 例3.12中创建了类Person的对象p1和p2,请使用对象图描述对象p1和p2的结构及其数据,并使用映射表示对象p1与其数据之间的关系。 6. 例3.12中使用表达式fn(p1).print()检测类Person对象参与计算的能力,请使用时序图描述执行表达式fn(p1).print()的过程。 7. 例3.13中使用链表管理类Student的对象,请使用时序图描述程序的执行过程。 8. 每学期选修课程时需要先认真阅读本专业的培养方案,分析需要学习的课程以及相关规定,然后再根据自己的具体情况从开设的课程中选择适当的课程,以保证毕业时能够修完规定的课程,取得要求的学分。选课的一般步骤为: ①根据所学的专业找到培养方案,根据自己的情况从本专业的培养方案中明确所执行的培养方案,确定需要学习的所有课程; ②根据学习课程的进度从下学期开设课程中选择修读的课程。选课中涉及的专业、培养方案、课程等主要因素及其关系如图3.40所示。 图3.40选课中涉及的主要因素及其关系 (1) 按照集合和映射等数学思维分析所在学校的本专业培养方案,修改完善如图3.40所示的类图,然后选择关联的表示方式以及属性的数据类型,最终设计出包含类、关联以及属性的类图,以描述选课场景。 (2) 分析所在学校的选课系统,使用时序图分解其中的核心功能,确定每个类的主要职责,最终设计出成员函数。 (3) 按照前面的设计编写程序,并调试通过。 为了便于理解,在设计时可采用中文命名类及属性和方法,在编写代码前再转换为英文。