C++语言程序设计 2第二讲——类与对象_第1页
C++语言程序设计 2第二讲——类与对象_第2页
C++语言程序设计 2第二讲——类与对象_第3页
C++语言程序设计 2第二讲——类与对象_第4页
C++语言程序设计 2第二讲——类与对象_第5页
已阅读5页,还剩175页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

第四章 类与对象,C+语言程序设计,本章主要内容,OOP的基本特点 类概念和声明 对象 构造函数 析构函数 内联成员函数 拷贝构造函数 类的组合,C+的要点:,一个思想:抽象(和分类)的思想; 三个概念:类、对象、消息; 三个特征:封装、继承、多态; 一个观念:函数服务于数据。 程序 = 对象 + 消息 ( 对象 = 数据结构 + 算法 ),概念的引入,首先来考察一个应用项目: 客户使用自动取款机完成一次取款。 A方案:面向过程的开发模式。 下面用 流程图 给出处理描述;,OOP的基本特点,(面向过程的程序设计的特征: 数据与对其进行的操作分离。),B方案:面向对象的开发模式。 分析: 取款过程所涉及的独立个体有: 取款机 取款人,OOP的基本特点,包括有:现金、纸卷属性;还应有显示菜单、取款操作、验证密码、查询余额、打印小票,如果还关心其它功能,则可以增加:取款机型号、外形尺寸、机器单价、钞票容量、语音提示、报警,包括有:插卡、输入密码、选择服务、取出钞票,面向过程的开发模式特点: 分解。人为地预先设定了处理过程,用分解后的模块线性地环环相扣地堆积起了程序,成品后的程序没了灵活性和可扩展性,只能够应用于固定处理流程的问题。适合于小规模程序开发。,OOP的基本特点,面向对象的开发模式特点: 不是割裂问题,反而是尽力保持事物的整体性。抽取其原本的特征,加以提炼,制造成了一个个类,类中罗列了一项项属性和行为,它们被妥善的封装起来。 程序结构就成了对象间的相互作用,由主控函数决定产生哪些对象,以及它们之间如何相互作用。,抽象,抽象是对具体对象(问题)进行概括,抽出这一类对象的公共性质并加以描述的过程。 集中注意力,只关注问题中那些在当前背景下最为重要的部分。不被事物的表象所迷惑。 先注意问题的本质及描述,其次是实现过程或细节。 数据抽象:描述某类对象的属性或状态(对象相互区别的物理量)。 代码抽象:描述某类对象的共有的行为特征或具有的功能。 抽象的实现:通过类的声明。,OOP的基本特点,去粗取精,由表及里;去掉个性,保留共性。,抽象用于OOP,“概念”的表述是抽象的范例。如:关键字概念是语言中专用于表示特定含义的名称,不可挪作他用。int是表示数据类型是整型的关键字。 这里共性是“专用于”“不可挪作他用”,个性是“特定含义”,什么含义没说,各关键字的含义各不相同。 上述概念给出了一个完全覆盖的表述,可以涵盖全部关键字。这就是抽象的威力,提炼int、float、char.共性的过程就是抽象,抽象的结果产生了概念。抽象是个由特殊到一般的加工提炼过程。 于是,事务抽象的结果就产生了类。,类,人们用对象的观点将同一类型的实体加以抽象:描述该实体的共有的属性(对象相互区别的物理量)以及行为特征。这种抽象的实现造就了类,从而实现了真正意义上的面向对象程序设计。 类是有着共同特征与行为、而状态各不相同的物体的总称。 对象是类的实现,是类的实例。 用C+术语来表达:类是一种类型。 类是表现数据高级形态的有力工具。,OOP的基本特点,封装,将抽象出的数据成员、代码成员相结合,视它们为一个整体。 完全开放的数据结构容易受到伤害。 目的是增强安全性和简化编程,使用者不必了解具体的实现细节,而只需要通过外部接口,以特定的访问权限使用类的成员。 封装的实现:使用类声明中的 。 体现了类的高内聚性。 类提供了一种特殊的作用域,OOP的基本特点,封装的内涵,将数据和为其服务的操作结合成整体整体性; 将包裹的内容分为可见部分与不可见部分,用访问权限加以控制安全性; 即使是可见部分,也仅提供函数原型或接口,而将实现隐藏起来 封闭性; 为类提供了“本能”操作生存性; 类内所有成员间的相互访问是完全自由的开放性及数据的共享性。,封装的目的,切断实体的组成部分与外界的私下联系,以免政出多门,统一归口由实体的“界面”与外界打交道。从而实现了实体的独立性、各组成部分协作的规范性。 I/O流函数库是C做得很成功的范例。它用FILE 结构封装了对流操作的实现细节,使得纷繁复杂的I/O操作变得对用户透明了。 C+将这种封装技术普遍化,上升成为OOP的主体技术。,OOP的基本特点,封装的语法,例: class Clock public: void SetTime(int NewH,int NewM, int NewS); void ShowTime(); private: int Hour,Minute,Second; ;,特定的访问权限。决定了外界是否可访问到它。,OOP的基本特点,c+中的类,类是具有相同属性和行为的一组对象的集合,它为属于该类的全部对象提供了统一的抽象描述,其内部包括属性和行为两个部分。 利用类可以实现数据的封装、隐藏、继承与派生。 利用类易于编写大型复杂程序,其模块化程度比C中采用函数更高。,类 和 对 象,“类”模型,程序模型则是若干个对象的相互作用 一堆“鸡蛋”。,类 和 对 象,可放置在类中的任何位置; 不可声明时初始化; 不可声明为extern、register,但可以是static 或 const。 应避免将数据成员放在公有接口中,因为那将使类丧失弹性和抽象性。 不要将类的数据成员与函数成员同名。,类 和 对 象,数据成员,与一般的变量声明相同,但需要将它放在类的声明体中。,Hour Minute Second,类 和 对 象,对象的内存布局,this,Clock myclock,Memory,此图告诉我们:成员函数并非放在对象中,不必要也不合理。他们都呆在code区中,供所有对象共享,是靠this与对象联系着。,成员函数,可以仅在类中说明原型,而在类外给出函数体实现,并在函数名前使用类名加以限定。 也可以直接在类中给出函数体,形成成员函数的隐含内联。 允许成员函数为重载函数和带默认形参值的函数。,类 和 对 象,对成员数据的使用不再遵循“先声明后使用”的原则,即可以放置在类中任意位置。 凡被调用的成员函数一定要有函数实现。,内联成员函数,为了提高运行时的效率,对于较简单的函数可以声明为内联形式。 内联函数体中不要含有复杂结构(如循环语句和switch语句)。 在类中声明内联成员函数的方式: 将函数体放在类的声明中隐式声明。 使用inline关键字显式声明。,类 和 对 象,内联成员函数举例隐式声明,class Point public: void Init(int initX,int initY) X=initX; Y=initY; int GetX() return X; int GetY() return Y; private: int X,Y; ;,类 和 对 象,内联成员函数举例显式声明,class Point public: void Init(int initX,int initY); int GetX(); int GetY(); private: int X,Y; ;,类 和 对 象,inline void Point:Init(int initX,int initY) X=initX; Y=initY; inline int Point:GetX() return X; int Point:GetY() / 非内连 return Y; ,25,问: 内联函数到底在何时、何处展开?,内联成员函数,上面的例子就是标准的内联函数的用法,使用inline修饰带来的好处我们表面看不出来,其实上会避免频繁调用函数对栈内存重复开辟所带来的消耗。 说到这里我们不得不说一下在c语言中广泛使用的#define。define的确也可以起到inline的这种作用,但define会产生副作用,尤其是不同类型参数所导致的错误。可见inline有更强的约束性和能够让编译器检查出更多错误的特性,在c+中是不推荐使用define的。,类 和 对 象,对象,类的对象是该类的某一特定实体,即类类型的变量。 定义对象时,系统会为每一个对象分配自己的存储空间,该空间只保存数据,函数代码是所有对象共享的。 声明形式: 类名 对象名; 例: Clock myClock;,类 和 对 象,类中成员的访问方式,所谓对成员的访问,是为成员的行为立的规矩,是封装性的体现。 有两种方式: 1. 类内访问是类中成员的相互访问 ,又称成员访问 用法:直接使用成员名。 如 在Init函数中访问私有成员:X=initX; 2. 类外访问是类的外界访问类中的成员,又称对象访问。是在类外访问类的public 属性的成员。 用法:使用“对象名.成员名” 如 myClock.GetX() ;,类 和 对 象,“类” 访问模型,私有成员,公有成员,类外访问(对象访问),内部成员间相互访问,类内访问,构造函数,构造函数的作用是在对象被创建时使用特定的值构造对象,或者说将对象初始化为一个特定的状态(接管了编译器的部分职责) 。 初始化:是指对象(变量)诞生的那一刻即得到原始值的过程。是动词。 若对象声明然后再赋值,会经历状态由不确定到确定的过程。 初始化则不然,不经历该过程,而是一步到位地确定对象。,构造函数和析构函数,构造函数,构造函数的工作内容: 申请内存空间(必做); 对创建的对象初始化(选做); 执行函数体(有的话) 。,构造函数和析构函数,构造函数的调用形式: 隐含调用,创建有名对象; 显式调用,创建无名对象;,构造函数的特点:,与类同名; 无返回类型; 通常是公有成员; 在对象创建时由系统自动调用,亦可由程序员显式调用,但不可由对象调用; 如果程序中未声明,则系统自动产生出一个默认形式的构造函数,它无参,函数体为空; 允许为内联函数、重载函数、带默认形参值的函数 是可以使用初始化列表的两函数之一(特权); 不可以是常函数,亦不可是虚函数; 天然具有类型转换功能,除非用explicit关闭。,构造函数和析构函数,构造函数的工作特点,既然构造函数是在对象被创建时使用特定的值构造对象的,那么它就要分配能容纳全部数据成员的空间 ,还要按次序将所有成员初始化,这次序就是程序声明类时各成员的书写的次序。 任何类的数据成员不管形态如何千奇百怪,这次序是铁定的、嵌套的。,构造函数和析构函数,无参构造函数调用,因为类的构造函数可以无参,还可以带默认参数值,这两种情况的调用都不必使用实参. class Clock public: Clock () . ; 创建对象:Clock c1; 此时创建的对象的数据成员的值是任意值,即仅开辟空间,没初始化成员。 此句不可写成:Clock c1() ; 因为这将与声明一个函数原型的语句相同,编译器无法区别。,构造函数和析构函数,构造函数可以显式调用,#include class tree /定义类tree public: tree(int i ):ages(i) /构造函数 void grow(int years) /计算树的年龄 ages+=years; void age() /显示树的年龄 cout“树的年龄为: “agesendl; private: int ages; /私有数据成员 ;,构造函数和析构函数,void main() int age,year; coutage; tree tree(age);/因为显式调用,对象名只好与类名相同 tree.age(); /显示树的年龄 coutyear; tree.grow(year); /计算树的年龄 tree.age(); /显示树的年龄 ,尽管允许对象名与类名相同,但绝不提倡!,C+允许在同一作用域内,对象名与类名相同,但不允许类名(或对象名)与函数名相同!同一作用域内对象名的优先级高于类名。,拷贝构造函数,拷贝构造函数是一种特殊的构造函数,其形参为本类的对象引用。 class 类名 public : 类名(形参);/构造函数 类名(类名 &对象名);/拷贝构造函数原型 . ; 类名: 类名(类名 &对象名) /拷贝函数的实现 函数体 ,拷贝构造函数,例4-2 拷贝构造函数定义,class Point public: Point(int xx=0,int yy=0)X=xx; Y=yy; Point(Point ,拷贝构造函数,Point:Point (Point ,例4-2 拷贝构造函数调用 1,当用类的一个对象去初始化该类的另一个对象时系统自动调用拷贝构造函数实现拷贝赋值。 void main(void) Point A(1,2); Point B(A); /拷贝构造函数被调用 coutB.GetX()endl; ,拷贝构造函数,例4-2拷贝构造函数调用 2,若函数的形参为类对象,调用函数时,实参赋值给形参,系统自动调用拷贝构造函数。例如: void fun1(Point p) coutp.GetX()endl; void main() Point A(1,2); fun1(A); /调用拷贝构造函数 ,拷贝构造函数,这也说明:拷贝构造函数的形参必须是引用。,例4-2拷贝构造函数调用 3,当函数的返回值是类对象时,系统自动调用拷贝构造函数。例如: Point fun2() Point A(1,2); /调用拷贝构造函数 return A; void main() Point B; B=fun2(); ,拷贝构造函数,如果一个函数返回值是对象,系统将产生一个同类型的临时对象来存放该返回值。把返回对象拷贝给临时对象,自己被析构。,所以,函数的返回值不能是:某临时对象的“引用”。即:对于临时对象,只在创建它的外部表达式范围内有效。,然后将临时对象赋值给主调函数的接收对象,赋值完毕,临时对象再析构。,拷贝构造函数调用的定律,凡是一个函数的形参为类的对象(用传值完成形实结合),则该函数的调用必然伴随着拷贝构造函数的自动调用。 凡是一个函数的返回值是个对象(而非引用或指针),该函数调用后必然自动调用拷贝构造函数将return的对象复制出去。(有的系统会优化这个过程),拷贝构造函数,关于临时对象,Student fn() / Student ms(“Randy”); return ms; void main() Student s; s=fn(); Student / ,临时对象此时还有效,分号之后便无效,此时引用的是临时对象,它的生命期已结束。错误!但编译并无错!,无名对象,无名对象:实际存在、但没有标识符的对象。 无名对象也要用构造函数创建; 临时对象仅是无名对象之一,但不同;临时对象短命,只到分号;而无名对象长命,至少长于分号。但对引用例外。 无名对象的几种用法: void fn(Student /创建对象数组, /前三句,既创建了临时又创建了无名对象。,同前,错。但有的C+编译器为了引用,将临时对象的生命期延长的与引用一样长,就不错了。,由临时对象引发的问题,string S1,S2; /创建对象 printf(“%s”,(const char *) /幽灵指针,此句竟然正确 ?! 侥幸。 Why?,原因出在:此句紧挨着上句。,关于拷贝构造函数,拷贝构造函数执行的功能是:用初值对象的每个数据成员的值去初始化将要建立的新对象的对应数据成员 对应初始化。 如果程序员没有为类声明拷贝构造函数,则编译器自己生成一个默认拷贝构造函数。此拷贝构造函数不仅仅申请里空间,还会将对象做“按位拷贝”。这是与构造函数不同之处。 若将拷贝构造函数设为私有,就切断了外界克隆对象的能力。程序员可以授权给别的成员函数完成此任务。,拷贝构造函数,提倡用引用作函数的形参,用引用作拷贝构造函数的形参是为了避免死循环。在别的函数里用引用作形参,是为了大大提高效率。 对于Clock类,若有这样一个非成员函数: Clock modif ( Clock c ,int H,int M,int S) c.setTime(H,M,S); return c; . Clock cc(11,45,56); cc = modif( cc,12,0,0) ; 这将非常低效,试分析此句都干了些什么?,void,Clock &c,析构函数,完成对象被删除前的一些清理工作(恰与构造函数对称)。这些清理工作不仅是销毁前的提示,更重要的是释放它所把持的资源(通过调用那些析构函数来完成)。 在对象的生存期结束的时刻系统自动调用它,然后再释放此对象所属的空间。 如果程序中未声明析构函数,编译器将自动产生一个默认的析构函数。 不接受任何参数,亦无法重载。 析构和构造函数的机制保证了正确地创建和销毁对象,这是数据安全的保证。,析构函数,析构函数的调用时机:, 或文件尾(请注意它们的区别); delete (有条件地); catch() goto(在跳出作用域时) 当中途退出程序时(比如使用exit()或abort()时)不调用析构函数。 是否需要自定义析构函数的依据是:对象是否占有需要释放的资源。其他理由皆可有可无。 资源是指堆内存、类聚集、进(线)程、网络、锁、外设等。,析构函数,关于构造函数的设计,如果一个类含有带默认值的构造函数,一定不要再为该类提供无参构造函数。 Why? 如果一个类重写了其他形式的构造函数,一定要为该类提供无参构造函数。 Why? 编译器会检查是否将对象的定义放在了有可能被goto跳过的地方,若是则会报错。 Why?,关于初始化,对象的初始化就是在对象创建时,使用初始化列表直接填充对象的内存单元,没有中间的转换,也不产生临时对象。 对象的初始化是由构造函数承担的;数据成员的初始化是由初始化列表承担的,初始化列表是抢先在函数体执行之前就执行的;这是两个层次的初始化。 有人说,我在构造函数的第一行就对数据成员赋值,是否算初始化?答曰,不算。 因为赋值是对已有对象的更改,是初始化后的事,初始化已经完成了。 对象的初始化只能进行一次,赋值却可以进行多次。,关于赋值函数,每个类都有个赋值成员函数(operator =); 它的功能是将本类型的对象用已有对象更新; 它应是公有成员函数; 若定义类时没显式给出,系统将补一个,叫默认赋值函数; 默认的赋值函数的功能是将对象的数据成员逐个对应赋值,而不分辨赋值的后果; 凡类中含有指针做成员(引用不可能),则定要重写拷贝构造函数和赋值函数,不可使用默认的。,容易混淆的概念,构造函数与拷贝构造函数: 都是创建新对象,区别在于用什么作实参; 类 新对象(数值); 类 新对象(旧对象); 拷贝构造函数与赋值函数: 实参都是对象,区别在于创建新对象还是更新旧对象。 类 新对象(旧对象); 对象1 = 对象2 ;,告诫:,不要让构造函数做与创建对象、初始化对象无关的事; 不要让拷贝构造函数做与创建出一个与已有对象一样的对象无关的事; 不要让析构造函数做与释放一个已有对象无关的事; 请不要将构造函数、拷贝构造函数、析构造函数设计为内联。因为它们负有特殊使命,尤其是在动态多态、多继承、类组合时,它们要将一些隐秘的东西加给对象。将其内联会影响它们的工作。 这“告诫”给出了一条重要的规则:只做该做的事,做就把它做好。否则只会节外生枝、凭添烦恼。 这“告诫”是让我们养成良好的编程习惯。,C+能自动产生的成员函数,构造函数 类名() 拷贝构造函数 类名(类名& ) 赋值函数 类名& operator =(类名& ); 取址函数 类名 * operator &(); 取常址函数 const 类名*operator &()const; 析构函数 类名(),是指当类没有显式给出以上函数的代码时,编译器会自动补偿。如果用户已定义了,则不再补。 “缺哪个则补哪个 ,需要才补”,例:,class Empty; 和下面这么写效果是一样的: class Empty public: Empty(); / 缺省构造函数 Empty(const Empty,如果需要,这些函数就会被生成,你会很容易就需要它们。下面的代码将使得每个函数被生成: const Empty e1; / 缺省构造函数 Empty e2(e1); / 拷贝构造函数 e2 = e1; / 赋值运算符 Empty *pe2 = / 取址运算符(const) 最后调析构函数。 顺便说一句,生成的析构函数一般是非虚的,除非它所在的类是从一个声明了虚析构函数的基类继承而来。,思考:,现有类:class C ; 以下三句的区别: C x(100); C y = C(100); C z = 100 ;,直接初始化,先创建匿名对象,再调拷贝构造函数。,同1。即=与()作用相同。,类的“大三件”(Big-Three),C+的大师称类的构造函数、析构函数、赋值函数为类的“大三件”(Big-Three)。 这确实说明了它们的重要性。这也就是为何系统会自动生成这些函数的原因,当然不止这三个。 程序员在设计类时一定不可掉以轻心。随着编程经验的积累,相信人们会对这经验之谈有更加深入的认识。,C+不能自动产生比较成员函数,不会自动弥补如 operator ()或operator =()等比较函 数,以及其它函数。为何编译器不将所需的函数都自动补齐呢? 原因是对象的内存布局遵守着对齐原则:按字长的整数倍分配空间。和 按位操作原则。 这对于拷贝、赋值都没问题,可用在比较上就不行了:成员间的“填补字节”不属于成员,初始化也没有它们的份,只会保存着“脏值”,而比较又是“ 按位比”,让脏值参与比较只能搅局。 系统只好将比较运算符重载的责任交给了程序员各自完成。这也正是程序员自己写重载函数的实施要点。,这也启发我们:自己定义比较函数时,要注意绕过那些“脏值”。,打造类的准则,以往我们的思路是:需要什么就造什么,不需要的功能则不写,不写即是没有,就是封锁了该功能。 现在看来是不行了。因为C+会给类补某些函数。不写出不是封锁了该功能,而是会得到你不想要的功能。这是你的本意吗? 于是,不想要某个功能,则要写一个空成员函数让它占位;想有某个功能但不想让外界使用,则将其放在私有域中。,由于C+允许类的成员函数重载,而重载函数的行为又往往类似,于是往往出现几个函数都包含相同的代码片断。这种现象又违反了“一事一地”的原则。 解决的办法是:设法把几个函数的相同代码提取出来,做成一个非公有成员函数,让别的函数来调用它即可。 当然,这样会增加调用成本,但代码更清晰、易维护。,还有一条重要原则是:应将数据成员放在私有权限下,以防被不小心改动。 这是说,类的数据成员是对对象的描述,是对象状态的记录,是使用对象的依据。它的变化将导致误用,会引起一系列的错误,因此它们应当被小心的呵护着。 呵护的办法就是:将其放在不能被随意访问的地方,对它的访问一律通过专职的成员函数来完成。 当然,这样会增加使用成本,但数据更安全、代码更清晰、易维护。,再一条原则是:应使所造的类能被方便的使用,而不易被误用,还能方便的被已有代码所共享。这靠的就是“封装”。 可见“封装”绝非仅仅是“隐藏”那么简单,要与“接口”相配合。 类是程序员在写代码时造的,对象是程序在运行时造的。程序员无法在程序运行时再去改造类,那为时已晚;也绝不能在编译时造出对象来,那又太早了。 请你理解好这句话的含义。,引用作类的成员要占存储空间,#include using namespace std; class A public: A(int i); void print(); const int A:A(int i):a(i),r(a) /引用作类的数据成员只能通过初始化列表来获得初值 . 这也要求我们:必须显式地给出构造函数,引用作类的成员时要占对象的存储空间。 这难道坏了“引用不占存储空间”的规矩了吗? 这究竟怎么回事?,void A:print() cout“ a = “a“ r = “rendl; void main() int i = 10; int ,运行结果: i=10 r =10 *p=10 &i =0012FF7C &r =0012FF7C p = 0012FF7C a = 100 r = 100 sizeof a1 = 8,这个例子说明: 引用作数据成员时,编译器会将引用替换成指针,将其指向目标。于是对引用的访问会被映射到所指的目标上,于是值就与其主人保持一致了。,构造函数的副作用,构造函数的原本作用就是将依照实参构建起类的对象来。这个过程实际上完成了数据的类型转换外界的数据变成了对象。这是构造函数的副作用。也就是说,构造函数每逢被调用,皆有两种解释。于是给使用它埋下了隐患。 因为C+的旧的类型转换方式恰恰使用 类型名(被转换数) 这样的形式。这也恰是构造函数显式调用的形式。那么,这样写究竟是构建对象还是类型转换?为了分离这种一式二义性,C+提供了关键字explicit,用于关闭构造函数的转换功能。 explicit仅用于关闭构造函数的转换功能。,class two; class one public: one( ) one( two c) ; class two public: two() two( const one ,44,此句应发生错误,但由于有two( const one & a) 函数的存在,会调用它,从而使错误被合法化了。“洗钱”,构造函数产生二义的例子,改造为: class two; class one public: one() one( two c) ; class two public: two() explicit two( const one /必须显式使用转换。阳光化操作 ,44,类型转换的内幕,不少人误以为“转换”既是“改变”。其实类型转换仅仅是用源对象的值初始化一个新的对象,即制作了一个满足要求的源对象的副本,这个副本就是“表达式的值”,是个临时对象,而源对象毫发未损。可见转换是个运算过程。 “转换”应该是“新造”,千万不可以为源对象被改变了。 这“新造”恰恰是构造函数的“本职工作”。,类的封装的再认识,C+通过类来实现封装:将数据与对数据的操作封装在类中。 访问权限体现了封装。C+对于外界访问本类保护的和私有的成员一概拒绝,对象与外界的唯一联系就只有公有成员。 公有成员被称为类的公用接口,可以通过接口来实现对对象的操纵,但无法知道内部的具体实现细节。这对用户来说是隐蔽的,我们称之为隐蔽实现。这种“类的公有接口与隐蔽实现的分离”形成了类封装的另一层含义。,是否有点像人穿起衣服,遮住了不想让别人看到的部分,刻意露出了想让别人看的脸庞,还有不得不露出来的手。露出来的就叫接口。 其实皮肤也是封装,把需要保护的内脏器官藏起来,只露出了眼睛、鼻子、耳朵、嘴巴 封装的层次化。 封装提供了将对象的逻辑状态和物理状态完全分离的手段。 封装还对应着捆绑(内在联系机制),捆绑服务于隐藏; 封装的精髓在于抽象,绝非杂烩。,类的接口,接口是由封装引起的概念:把一些东西包裹起来,就要留一些在包裹外面。被包裹起来的东西称为封装,留在包裹外面的叫接口。它们应体现某种内在联系:服务与被服务关系。接口体现了承诺,是靠内部机制支撑的。 一台电视机,线路板、内部连线等被封装了,屏幕、开关、操作按钮则是接口。 类的公有成员是接口,其他成员是被封装的细节。 于是面向对象的概念:类、类型、对象、封装、接口、行为、功能、消息、状态、成员、识别标志等就浑然一体了。,类接口的配置,一个类应具有: 一个或多个构造函数; 有或没有拷贝构造函数; 有或没有析构函数; 对数据成员的设置、修改等; 对数据成员的取出、显示等; 对数据成员的计算加工处理; 必要的运算符重载函数; 向外部作类型转换的转换函数。 以上函数应恰当的使用:引用、指针、常 等等。,“类”对语言的影响,程序组织结构化的整合:文件分工的专业化使它们各司其职 。 开发过程的细化:专业意义上的再分工及协作。 类定义与类实现的分离,使代码的可重用性、透明性大为提高。,类 和 对 象,某软件公司的CEO称其公司的软件是面向对象的,“因为我们的产品是用C+写成的”。这件事成了该CEO的笑柄。 是否是面向对象的,不但要用C+写成,更重要的是是否使用了面向对象的编程思想。,类的封装与信息隐藏的好处,通过公用接口来修改类内部保护的和私有的数据成员,可以在修改前判断数据的有效性,有效的数据才可进入,增加了数据的安全性。 在使用某个类时,只要了解公用接口就可以了,而不必知道内部的具体实现细节,这大大降低了工作强度。 类的内部实现修改时,只要公用接口不变,就不影响应用程序对类的调用。,数据成员为何不能公有,将数据成员放在类定义的public下,好像是不加任何限制,敞开给任何用户使用,其实意味着它是无用的。 因为对于一个被广泛使用的类的对象而言,若谁都可以任意修改,则意味着它是不确定的、不可靠的、不可信的,你不敢将程序逻辑建立在飘忽不定的基础上,那将导致代码的重写、重新测试、重新编译及文档修改等等。于是导致了谁也不敢使用它。所以将数据成员放在private下是明智的选择。,类 和 对 象,类声明与成员函数的分离,一般来说类的定义与类中成员函数的定义并不放在同一个文件中。类的定义放在头文件中,类中函数的定义放在.cpp文件中。 /在point.h 文件中 class point private: int x;int y; int z; public: void print(); ;,类声明与成员函数的分离,/在point.cpp文件中 #include “point.h” void point : print() /对该类的使用 #include “point.h” void main(void) . ,“对象相同”的涵义,你在说“对象相同”时,想表达什么意思?是想说它们的内容相同,还是想说它们就是同一个对象? 前一个是这样表达: string A = “Hello”; string B = “Hello”; 用这样的判断表达式: strcmp( data1, data2 ); 或:( * this = rhs ); 后一个是这样表达: string ,用来判是否“自赋值”,封装不是万能的,别以为有了类的封装性,私有成员就可以高枕无忧。 class A private: int x; public: int /照样可以修改到私有成员! 甚至成员是常函数仍可以通过返回一个非常引用深入到对象内部。 结论:不要让成员函数返回一个非常引用(指针)。,别名问题,由于有了别名,不同名的对象可能是同一个(同址): class Base void fun1( Base ,别名问题,void func2(Derived /rb1与rb2是同一个 以上问题皆由别名所生,(指针也有类似的问题)。操作时只有弄清对象到底是谁,才会对诸如赋值、拷贝、删除、修改等操作有正确的目标。否则会发生张冠李戴、滥杀无辜等错误。,函数的参数是“承诺” 是“保证”,是答应了必须做的事; 函数的返回是“需求”,是要求外界做的事。 对于类,公有成员函数原型是“承诺”; 对于类继承,基类的架构是“需求” ; 对于类多态,基类的虚函数是“需求” ; 请时时事事按“承诺” “需求” 的方向来思考。,关于编程的箴言,另外两种类,在C+中还有两种类,它们是struct 和 union。与class不同的是:其成员默认的访问权限是公有的,其余都一样。 C+为了与C兼容,其实保留了两套struct 和 union。除了做成类的一套外,还有一套叫POD风格的struct 和 union。其特点是编译器不会给它们增加任何隐含成分,只在C编译器中运行。 由于内存布局的差异,切记,不要交叉继承。,“组合”的概念 (composition),类中的成员是另一个类的对象。 可以在已有的抽象的基础上实现更复杂的抽象。 复杂问题是由若干简单问题组成。则恰提供了层次化开发的重要思想方法。,类 的 组 合,举例,class Point private: float x,y; /点的坐标 public: Point(float h,float v); /构造函数 float GetX(void); /取X坐标 float GetY(void); /取Y坐标 void Draw(void); /在(x,y)处画点 ; /.函数的实现略,类 的 组 合,class Line private: Point p1,p2; /线段的两个端点 public: Line(Point a,Point b); /构造函数 void Draw(void); /画出线段 ; /.函数的实现略,49,类组合的语义,类的组合技术实际上是在给类作对象布局的安排:让类的对象中含有一个别的类对象。于是就要在本对象的适当位置处,调用那个类的构造函数,来产生内嵌对象作为本类的数据成员。因此,其语义为“has a”。,类 的 组 合,类组合时构造函数的设计,原则:不仅要负责对本类中的基本类型成员数据初始化,也要对对象成员初始化。 可是类之间严格遵守“互不干涉内政”的公约,那么,对象成员的初始化只能由它的类完成。 声明形式:,类 的 组 合,类名:类名(形参总表) :对象1(参数),对象2(参数) 构造函数的其他功能 ,初始化列表。 承担初始化任务,没用初始化列表的反例,#include using namespace std; class StudentID int value; public: StudentID(int id=0) value = id; cout“Assigning student id “value “n“; ;,class Student string name; StudentID id; public: Student(string n = “noName“, int ssID=0) cout “Constructing student “ + n + “n“; name = n; StudentID id(ssID); / 错误的初始化尝试晚了 /此时 id已被释放 ; void main() Student s(“Randy“, 58); ,初始化列表,创建类的对象的一种初始化形式 ; 一般格式: 该列表用于书写欲抢在函数体执行之前就要执行的功能(抢在对象创建完成前); 是在对象创建完成之前即已完成的动作; 其实是构造函数的一种调用形式; 仅能用于构造函数和拷贝构造函数; 常用于类组合和类继承。,类 的 组 合,:对象成员(值 | 对象),:数据成员(值),关于初始化列表,类 的 组 合,请注意“初始化列表”的执行时机”抢在函数体执行之前就执行“,这说明它的特殊功效适用于对象初始化。 因此应在创建对象时完成的工作,皆应交由“初始化列表”去做,决不要在函数体内用赋值的方式完成那时对象已经创建完毕,太晚了。 用初始化列表和用赋值是有重大区别的,尤其是对常成员和引用成员,它们是不能赋值的。/ 在书写初始化列表时,不要打乱各成员在类声明时的次序,否则会出错。,初始化列表的不当使用反例,#include using namespace std; class Fatal (致命的) int x; int y; public: Fatal(int i) :y(i),x(y*2) /这是错的,应该先x(i*2),y(i) void show() cout“x = “x endl“y = “y “n“; ;,void main() Fatal F(10); F.show(); ,其结果是: x = -1717986920 y = 10,类的组合举例(二),类 的 组 合,class Part /部件类 public: Part(); Part(int i); Part(Part ,class Whole /整体类 public: Whole(); Whole(int i,int j,int k); Whole(Whole ,Whole:Whole() date=0; Whole:Whole(int i,int j,int k) : two(i),one(j),date(k) Whole:Whole(Whole &wr) : two(wr.two),one(wr.one), data(wr.data) /.其它的函数实现略,类组合的构造函数调用(1),构造函数调用顺序:先调用内嵌对象的构造函数(按内嵌时的声明顺序,先声明者先构造)。然后执行本类构造函数体。(析构函数的调用顺序相反),类 的 组 合,类组合的构造函数调用(2),若内嵌对象的构造函数无形参时,则外包类的构造函数的初始化中可以不显式给出内嵌对象构造函数的调用。但不写不等于不调用,是隐含调用。调用的是相应的无形参的构造函数。,类 的 组 合,Whole:Whole() date=0; Whole:Whole(int i,int j,int k):two(i),one(j),date(k) ,此处没写初始化列表,是否调用!Why?,该函数的初始化列表是隐含的。下面的函数是显式的。,类组合时拷贝构造函数的设计,若类中没有显式的给出拷贝构造函数,则编译器会自动补充一个; 若要为类编写一个拷贝构造函数,则应用初始化列表为内嵌对象的拷贝构造函数传递内嵌对象成员。 类名:类名(引用名) :成员对象 (引用.成员对象 ) 构造函数的其他功能 ,类 的 组 合,程序阅读练习。 并回答: 为何拷贝函数调用了四遍?,类名也可以重定义,typedef也可以对类名重定义: 如有 class a ; typedef a CN; a obj ; /原类名继续好用 CN obj1 ; /也可以用新类名定义对象,C中的struct、union也是类,C+ 将struct、union都视为类。与class不同的是,数据成员默认为公有的。 若将数据成员放在公有权限下,原来的初始化形式继续好用。如: struct stu int age; ; stu ss = 0; 但若将数据成员放在私有或保护的权限下,则此句错误。 class的类亦同。,类中可以镶嵌enum,类中声明(不是定义)的enum通常是为了给类提供一个或几个仅供类内使用的常量的,因此也不必命名,也往往不必定义变量。所声明的常量不得取引用,也不得用指针指向。 类内的不定义变量的enum不占用类的存储空间(其它内嵌的情况也一样)。,本章回顾:,“封装”的涵义:,之一,聚合; 之二,隐藏; 之三,受控; 之四,隔离;,OOP编程的涵义: 定义数据类型( 值的布局和类型的行为); 使用这些类型;,C的努力用程序结构支配数据:,作用域与可见性,存储类型决定了变量在内存中的存放区域,也决定了在多文件系统中被使用的属性; 非静态全局变量具有外部存储类型,这使它能被程序的各文件共享。但前提是程序的各文件只有一处定义,其他皆为声明,且不再有同名变量; 静态就是让名在其声明区域(块、文件)内成为私有,这样可提供一定程度的保护; 以上是C已有的特性。C+在完全照搬的基础上新加了类、名空间。,作用域的种类: 5种,(按从小到大的次序) 函数原型作用域 块作用域(函数作用域) 类作用域 文件作用域 名空间作用域(另文专述),作用域与可见性,例5-3具有静态、动态生存期对象的时钟程序,#include using namespace std; class Clock /时钟类声明 public: /外部接口 Clock(); void SetTime(int NewH, int NewM, int NewS); /三个形参均具有函数原型作用域 void ShowTime(); Clock() private: /私有数据成员 int Hour,Minute,Second; ;,/时钟类成员函数的实现 Clock:Clock() /构造函数 Hour=0; Minute=0; Second=0; void Clock:SetTime(int NewH, int NewM, int NewS) Hour=NewH; Minute=NewM; Second=NewS; void Clock:ShowTime() coutHour“:“Minute“:“Secondendl; ,20,Clock globClock; /声明对象globClock, /具有静态生存期,文件作用域 void main() /主函数 cout“First time output:“endl; /引用具有文件作用域的对象: globClock.ShowTime(); /对象的成员函数具有类作用域 globClock.SetTime(8,30,30); Clock myClock(globClock); /声明具有块作用域的对象myClock cout“Second time output:“endl; myClock.ShowTime(); /引用具有块作用域的对象 ,21,程序的运行结果为: First time output: 0:0:0 Second time output: 8:30:30 此题的globClock是全局对象,它是在编译时就已经规划好了的,等到程序加载后,在主函数运行之前抢先运行了调用构造函数。,22,对于名称的搜索次序,(名称查找规则) 本模块内 本类(因有多种类间关系,变得很复杂) 本名空间 全局,作用域与可见性,C 对数据的共享机制,由于数据存储在局部变量中,函数模块是通过参数传递实现共享函数间的参数传递。 数据存储在全局变量中,借助全局变量的长命实现共享。,数据与函数,C+ 对数据的共享机制,将数据和使用数据

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论