第四部分思维导图PDF下载C++程序设计 第四章 类和对象

4.1 类及其实例化

4.1.1 定义类

  • 类是一种用户自己构造的数据类型
    • 1、类要先声明后使用。
    • 2、不管声明的内容是否相同,声明同一个名字的两个类是错误的。
    • 3、类是具有唯一标识符的实体。
    • 4、在类中声明的任何成员不能使用如下几种关键字进行修饰
      • extern
      • auto
      • register
    • 5、类中声明的变量属于该类,某些情况下可以被其他实例共享。

  • 类不仅可以有数据,而且可以有对数据进行操作的函数,分别为类的:

    • 数据成员
    • 成员函数
  • 1、声明类
    • class 类名 { private: 私有数据和函数 public: 公有数据和函数 protected: 保护数据和函数 }
    • 如果没有使用关键字,则所有成员默认声明为private权限!
  • 2、定义成员函数
    • 返回类型 类名 ::成员函数名(参数列表){ 成员函数的函数体 }
    • 其中“::”是作用域运算符,由于表明其后的成员函数是属于这个特定的类,就是对属于“类名”的成员函数进行定义。
    • 如果在声明类的同时,在类体内给出成员函数的定义,则默认为内联函数。
  • 3、数据成员的赋值
    • 不能再类体内给数据成员赋值,类体外更不允许,数据成员的具体值是用来描述对象的属性的。只有产生了一个具体的对象,这些数值才有意义。
    • 如果在产生对象时就使对象的数据成员具有指定值,则称为对象的初始化
    • 初始化和赋值是两个不同的概念
      • 赋初值是在有了对象之后,对象调用自己的成员函数实现赋值操作。
      • 初始化是使用与类同名的构造函数实现的!
  • 4、类定义完成后以“ ; ”结尾!

4.1.2 使用类的对象

  • 对数据成员的访问通过类的成员函数来进行,只有产生类的对象,才能使用这些数据和成员函数
  • 类不仅可以声明对象,还可以声明对象的引用和指针,语法与基本数据类型一样

    • 对象和引用都使用运算符“ . ”
      • C++推荐使用引用,如:
        • void print(Point&a){ a.Display(); }
    • 指针使用“ -> ”运算符

  • 类的规律

    • 1、类的成员函数可以直接使用自己类的私有成员(数据成员和成员函数)。
    • 2、类外面的函数不能直接访问类的私有成员(数据成员和成员函数)。
    • 3、类外面的函数只能通过类的对象使用该类的共有成员函数。
    • 4、若两个对象的成员函数代码一样,则两个对象的区别是属性的取值。
  • 程序运行时,通过为对象分配内存来创建对象。创建时使用类作为样板,故称对象为类的实例。

  • 定义类对象指针

    • 类名* 对象指针名
      • Point* p;
    • 对象指针名 = 对象的地址
      • p = a;
    • 或直接进行初始化 类名* 对象指针名 = 对象的地址
      • Point* p = a;
    • 类对象的指针可以通过i“ -> ”运算符访问对象的成员
      • 对象指针名 -> 对象成员名
        • P -> display();

4.1.3 数据封装

  • 面向对象的程序设计是通过为数据和代码建立分块的内存区域,以便提供对程序进行模块化的一种程序设计方法。
    • 1、这些内存块中不但存储数据,也存储代码。只有对象中的代码才可以访问存储于这个对象中的数据。
    • 2、这些内存块的结构可以被用作样板产生对象的更多副本。
  • 面向对象的程序中,只有向对象发送消息才能引用对象的行为,所以说面向对象是消息处理机制,对象之间只能通过成员函数调用实现相互通信。

  • C++通过类实现数据封装,即通过制定各成员的访问权限来实现。

    • 一般情况下将数据成员说明为私有的,以便隐藏数据
    • 部分成员函数说明为共有的,由于提供外界和这个类的对象相互作用的接口(界面),从而使得其他函数也可以访问和处理该类的对象。
    • 对于那些仅是为支持共有函数的实现而不作为对象界面的成员函数,也将他们说明为私有的。

4.2 构造函数

4.2.1 默认构造函数

  • 建立一个对象时,对象的状态(数据成员的取值)是不确定的。为了使对象的状态确定,必须对其进行正确的初始化。C++又称构造函数为特殊的成员函数,它可以自动进行对象的初始化。
  • C++编译器会在对象生成时提供一个默认的构造函数,但函数体为空。即当生成一个对象时,对象的状态是不确定的,即没有被初始化。
    • Point::Point( ){ }
  • 一旦程序定义了自己的构造函数,系统就不再提供默认构造函数。如果此时没有再定义一个无参数的构造函数,但声明了一个没有初始化的对象,则因系统已经不再提供默认构造函数而造成编译错误。

4.2.2 定义构造函数

  • 1、构造函数的定义和使用方法

    • 1、构造函数名与类名一致
    • 2、构造函数没有返回值,即便是void类型也不可以!
    • 3、构造函数可以有参数也可以没有参数。
    • 4、和成员函数一样,须声明后再定义,可在类体内声明时定义(内联函数),也可以在类外。可以使用初始化列表或者在构造函数的函数体内定义。
      • 声明
        • 类名 (形参1, 形参2,形参3,……形参n) //可以没有形参
      • 定义
        • 类名::类名(形参1,形参2,……形参n):x1(形参),x2(形参2),……,xn(形参n){ }
        • 类名::类名(形参1,形参2,……形参n){ x1 = 形参1;x2 = 形参2;…… xn = 形参n}
        • 类体外定义记得加上 ”类名::“
    • 5、构造函数的参数在排序时无顺序要求,只要保证相互对应即可,可以使用默认参数或重载。

  • 2、自动调用构造函数

    • 不能在程序中显示地调用构造函数,构造函数时自动调用的。
    • 可以设计多个构造函数,编译系统根据对象产生的方法调用相应的构造函数,构造函数是在产生对象的同时初始化对象的。

4.2.3 构造函数和运算符new

  • 运算符new用于建立生存期可控的对象,new返回这个对象的指针。
  • 由于类名被视为一个类型名,使用new建立动态对象的语法和建立动态变量的情况类似,不同点时new和构造函数一起使用。
    • Point *prt1 = new Point;
    • Point *prt2 = new Point(5,7);
  • 当使用new建立一个动态对象时,new首先分配足以保存Point类的一个对象所需要的内存,然后调用构造函数来初始化这块内存,再返回这个动态对象的地址。

  • 使用new建立的动态对象只能用delete删除,以便释放所占空间,应养成即使释放不再使用的内存空间的习惯。

4.2.4 构造函数的默认参数

  • 如果程序定义自己的有参数构造函数,又想使用无参数形式的构造函数,解决方法时将相应的构造函数全部使用默认参数设计。

    • 在声明时 Point( int =0, int = 0);
  • 若没有为一个类定义无参数的构造函数时,则在说明对象数组时必须提供初始值!

4.2.5 复制构造函数

  • 引用在类中一个很重要的用途是用在复制构造函数中。
  • 编译器建立一个默认复制构造函数,默认复制构造函数采用拷贝方式使用已有的对象来建立新对象,所以又直译为拷贝构造函数。

    • 例如类A,则原型为
      • A::A(A &)
  • 使用引用是为了从程序执行效率的角度考虑的,为了不改变原有对象,更普通的形式是使用const限定,如
    • A::A(const A &)
  • 和构造函数一样,如果自定义了复制构造函数,编译器只调用自定义的复制构造函数。
    • 声明 Point(Point &);
    • 定义 Point::Point( Point& t)
      • { x = t.x; y = t.y;}
  • 这个成员函数具有特殊的作用,在使用该类的一个对象初始化该类的另一个对象时,调用这个函数,又时又称为复制初始化的构造函数,例如
    • Point obj1(25, 52);
    • Point obj2(obj1);
  • 复制构造函数必须使用对象的引用作为形式参数

  • 推荐使用的原型为

    • Point::Point(const Point &)

4.3 析构函数

4.3.1 定义析构函数

  • 在对象消失时,应使用析构函数释放由构造函数分配的内存。构造函数、复制构造函数和析构函数是构造型成员函数的基本成员。
  • 析构函数的定义和方法
    • 1、为了与构造函数区分,在析构函数前加” ~ “符号。
    • 2、定义析构函数时,不能指定任何返回值!即使指定void返回类型也不行!
    • 3、析构函数也不能指定参数!但是可以显示的说明参数为void。即
      • A::~A(void)
    • 4、一个类只能定义一个析构函数且不能指明参数。

    • 原型

      • 声明 ~Point();
      • 定义 Point::~Point() { cout << "Destructor is active" << endl;}
  • 析构函数在对象的生存期结束时被自动调用。全局对象和静态对象的析构函数在程序运行结束之前调用。

  • 类的对象数组的每个元素调用一次析构函数。
  • 全局对象数组的析构函数在程序结束之前被调用。

4.3.2 析构函数和运算符delete

  • 运算符delete与析构函数一起工作。当使用delete删除一个动态对象时,它首先为这个动态对象调用析构函数,然后再释放这个动态对象占用的内存,这和使用new建立动态对象的过程正好相反!

    • 建立动态对象 Point *ptr = new Point[2]
    • 删除动态对象 delete [ ]ptr
  • 当系统先后创建几个对象时,系统按后建先析构的原则析构对象,当使用delete调用析构函数时,则按delete的顺序析构。

4.3.3 默认析构函数

  • 如果在定义类时没有定义析构函数,编译器也要为它产生一个函数体为空的默认析构函数,如

    • Point::~Point( ){ }

4.4 调用复制构造函数的综合实例

4.5 成员函数重载及默认参数

略,详见函数重载

4.6 this指针

当一个成员函数被调用时,系统自动像它传递一个隐含的参数,该参数是一个指向调用该函数的对象的指针,从而使成员函数知道该对哪个对象进行操作。如

  • void Point::Setxy(int a, int b, (Point *)this )
  • {
  • this -> x =a ;
  • this -> x = b;
  • }

this 指针是C++实现封装的一种机制,外部看来,每一个对象都拥有自己的成员函数。如果在定义函数时使用this指针,也不要给出隐含参数,写如

  • void Point::Setxy(int a, int b)
  • {
  • this -> x = a;
  • this -> x = b;
  • }

但除非特殊需要,一般情况省略符号”this ->“,而让系统进行默认设置。

4.7 一个类对象作为另一个类的成员

此小节借用矩形类Rectangle包含一个顶点类Point的对象作为矩形类的数据成员这样的一个例子,来说明类的结构关系,即聚合关系,有时又称包含。

构成的新类不能直接操作另一个类的数据,必须通过原构成类的对象使用他它们的成员函数来实现。

4.8 类和对象的性质

4.8.1 对象的性质

  • 1、同一类的对象之间可以相互赋值
    • Point A,B;
    • A.Setxy(25,55);
    • B = A;
  • 2、可使用对象数组
    • 例如Point A[3],定义了数组A可以存储3个Point类对象
  • 3、可使用指向对象的指针,使用取地址符&将一个对象的地址置于该指针中。
    • Point *p = &A;
    • p -> Display();
  • 4、对象可以用作函数参数
    • 传对象的值,修改形参不影响作为实参的对象
    • 传对象的引用(传地址),参数对象被修改时,实参对象也将被修改。
    • 传对象的地址,可以使用指针作为函数参数,可以达到传引用的效果
    • 为了避免被调用函数修改原来对象的数据成员,可以使用const修饰符。
  • 5、对象作为函数参数时,可以使用对象、对象引用和对象指针,如
    • void print(Point a) { a.Display; }
      • 原型 print( Point )
    • void print(Point& a) { a.Display; }
      • 原型 print( Point& )
    • void print(Point* a) { a->Display; }
      • 原型 print( Point* )
  • 6、一个对象可以用作另一个类的成员

4.8.2 类的性质

  • 1、使用类的权限

    • 1、类本身的成员函数可以使用类的所有成员(私有和共有成员)
    • 2、类的对象只能访问公有成员函数。
    • 3、其他函数不能使用类的私有成员,也不能使用公有成员函数,它们只能通过类的对象使用类的共有成员函数。
    • 4、虽然一个类可以包含另一个类的对象,但这个类也只能通过被包含类的对象使用那个类的成员函数,通过成员函数使用数据成员,如Loc.Set(x,y).
  • 2、不完全的类声明
    • 类不是内存中的物理实体,只有当使用类产生对象时,才进行内存分配,这种对象建立的过程称为实例化。
      • class MebersOnly; //不完全的类声明
      • MembersOnly *club; //定义一个全局变量类指针
      • void main() {……} //主函数
      • class MembersOnly { ……} 完全定义该类
    • 类必须在其成员使用之前先进行声明。

    • 不完全声明的类不能实例化,否则会编译错误,不完全声明仅用语类和结构。
    • 把数据成员的声明放到最后,有利于先理解类的界面。
  • 3、空类

  • 4、类作用域

    • 声明类时所使用的一对花括号形成所谓的类作用域,在类作用域中声明的标识符只在类中可见。
    • 类中的一个成员名可以使用类名和作用域运算符来显示地指定,称为成员名限定!如
      • void MyClass::set( int i)
      • {
      • MyClass::number = i;
      • }
    • 对象的生存期由对象说明来决定,类中各数据成员地生存期由对象的生存期决定,对象存在,它们存在,对象消失,它们也消失

    • 另struct默认控制权限是public

4.9 面向对象的标记图

4.9.1 类和对象的UML标记图

  • UML中表示类有

    • 长式
    • 短式
  • UML表示对象有
    • 头部
      • 只填对象名
      • 对象名:类名
        • 和源代码编写相反,对象名可省略,但冒号必不可少,需下划线标注
    • 中部
      • 数据成员, 如 x = 3.5
    • 下部
      • 成员函数,如 display,可省略”( )”

4.9.2 对象的结构与连接

  • 对象的结构是指对象之间地分类(继承)关系和组成(聚合)关系,统称关联关系。
    • 对象之间的静态关系,通过属性之间的连接反映,称为实例连接。
    • 对象行为之间的动态关系通过行为(消息)之间的依赖关系表现的,称为消息连接。
  • 1、分类关系及其表示
    • C++的分类结构是继承(基类/派生类)结构。
      • UML使用一个空三角形表示继承,三角形指向基类。
  • 2、对象组成关系及其表示
    • 1、独立地定义,可以属于多个整体,并具有不同的生存期,这种动态的所属关系为聚集
      • UML使用一个空心菱形表示
    • 2、用一个类的对象作为一种广义的数据类型来定义整体对象的一个属性,构成一个嵌套对象。这个类的对象只能隶属于唯一的整体对象并与它们同生同灭,此为组合。
      • UML使用一个实心菱形表示
  • 3、实例连接及其表示
    • 实例连接反应对象之间的静态关系,UML使用一条直线表示连接关系
      • 一对一
      • 一对多
      • 多对多
  • 4、消息连接及其表示
    • 消息连接描述对象之的动态关系,消息连接是有方向的,UML使用一条带箭头的实线表示。

4.9.3 使用实例

  • 不管两个实例有多少消息,只画一条。
  • 继承关系可以不画消息连接。

4.9.4 对象、类和消息

  • 对象的属性是指描述对象的数据成员,对象属性的集合又称为对象的状态。
  • 对象的行为定义在对象属性上的一组操作的集合,对象的操作集合体现了对象的行为能力。
  • 对象的属性和行为是对象定义的组成要素,特征:
    • 1、有一个状态,由与其相关联的属性值集合所表征。
    • 2、有唯一的标识名,可以区别于其他对象。
    • 3、有一组操作方法,每个操作决定对象的一种行为。
    • 4、对象的状态只能被自己的行为改变。
    • 5、对象的操作包括自身操作和施加于其他对象的操作。
    • 6、对象之间以消息传递的方式进行通信。
    • 7、一个对象的成员仍可以时一个对象。
  • 一个对象所能接受的消息及其所带的参数,构成该对象的外部接口。

  • 对象传送的消息一般由3各部分组成

    • 接收对象名
    • 调用操作名
    • 必要的参数
  • 使对象操作的唯一途径是通过协议中提供的消息进行。具体实现是将消息分为公有和私有消息,私有消息只供类内部使用的消息,公有消息是对外的接口,协议则是一个对象所能接受的所有公有消息的集合。

4.10 面向对象编程的文件规范

多文件编程规范

  • 在头文件中,非常简单的成员函数可以在声明中定义,即内联函数形式,其他函数尽量只声明。
  • 实现放在.cpp文件中,在.cpp文件中将头文件包含进去。
  • 主程序可单独使用一个文件。

4.10.1 编译命令

  • 1、嵌入指令

    • 尖括号和双引号中的文件可含有路径信息
    • C++路径使用双反斜杠
      • char fname [ ] = “\user\prog.h”;
  • 2、宏定义
    • #define 宏名 替换正文
    • 宏名和替换正文之间至少有一个空格,宏名习惯大写。
    • 宏定义以新行结束,而不以分号结尾。如果给出了分号,则它也被视作替换正文的一部分,当替换正文要书写在多行时,除最后一行之外,每行的行尾要加上一个反斜杠!表示宏定义继续到下一行!
    • #undef 用于删除宏定义
    • 无参数宏时尽量使用const代替宏定义。
  • 3、条件编译指令
    • if

      • 开始处,如果条件为真,则编译,否则不编译。
    • else

      • 可增加编译指令#error xxxx 输出错误信息,当遇到此指令时,编译器显示后面的出错信息xxx 然后中止编译
    • elif

    • endif

      • 结束指令
  • 4、defined操作符
    • 不是指令,而是一个预处理操作符,用语判定一个标识符是否已经被#define定义,若被定义,则defined(identifier)为真,反之为假。
  • 条件编译指令#ifdef和ifndef同样用语测试标识符是否被#define定义,可能会被淘汰,推荐使用defined,因为可以组成复杂的测试表达式。
    • defined(LARGE) && !defined(SMALL) || !defined(TINY)

4.10.2 在头文件中使用条件编译

  • 为避免重复编译类的同一个头文件,可使用条件编译,如
    • //Point.h
    • #if!defined ( POINT_H )
    • #define POINT_H
    • class Point {
    • …………//类体
    • };
    • #endif
最后修改日期:十一月 5, 2019

作者

留言

撰写回覆或留言

发布留言必须填写的电子邮件地址不会公开。