




已阅读5页,还剩116页未读, 继续免费阅读
版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
第四章 编译时多态性,封装性是面向对象程序设计的基础,可以说没有封 装性就没有面向对象的程序设计。封装性从根本上解 决了数据的安全性,也为实现不同数据的操作同一性 奠定了基础,这种操作的同一性反映了客观世界中规 范不同对象行为的一致性需要,它构成了面向对象程 序设计多态性的一部分。不过这种多态性必须预先确 定操作所施加的数据类型,因此不是完备意义上的多 态性,而只是基于对象的多态性。这种不同数据的操 作同一性必须在程序编译时静态确定,故称为编译时 多态性,或称为静态多态性。实现这种静态多态性的 途径是:函数重载和运算符重载。,本章要点 1 函数重载 2 运算符重载 3 使用成员函数重载运算符 4 使用友元函数重载运算符 5 自增 + 和自减 - 运算符的重载 6 调用运算符 () 和下标运算符 的重载 7 动态内存管理运算符的重载 8 赋值运算符重载 9 输入输出运算符重载 10 类型转换(函数),4.1 函数重载 函数重载为实现不同数据的操作同一性提供了根本编程机制。函数重载的基本规则: 函数名必须相同:体现了操作同一性。 函数的参数必须不同:体现了同一操作的实现差异 和所施加的数据差异。 在面向对象的程序中函数重载表现在两个方面: 类外全局函数重载 类成员函数重载,4.1.1 类外全局函数重载 在面向对象程序设计中,全局函数常常用来作为类 的友元函数,因此全局函数重载可以用来实现不同类 对象的同一类外操作。关于全局函数重载的方法已经 在第二章中讲述,本章不再赘述。 为了进一步理解函数重载是如何在编译时确定同一 操作施加于不同类型的数据,我们模拟分析 C+ 编译 器如何采用 “名子压延” 的方法实现重载函数的调用。 所谓 “名子压延” 是指编译器编译重载函数调用时, 先将原重载函数名和参数类型结合起来,以创建新重 载函数名。然后用新重载函数名替代原重载函数名。 例如,一个程序中有两个重载函数,原型声明为:,int myAns(float x, int j); int myAns(int I, char c); 编译时,编译器首先压延修改这两个重载函数名。修改后的新函数名或许会变成如下形式: int myAnsFLTINT(float x, int j); int myAnsINTCHAR(int i, char c); 这样,就把原来无法区分开来的相同的函数名变成 可以区分的不同的函数名。例如: 调用 myAns(float x, int j) 时, 则调用 myAnsFLTINT(float x, int j); 而调用 myAns(int I, char c) 时, 则调用 myAnsINTCHAR(int i, char c)。,假如调用这两个函数的语句为: exam1 = myAns(15.3, 15); exam2 = myAns(45, a); 用新函数名替代原函数名后,使得调用这两个函数的 语句会发生如下相应的改变: exam1 = myAnsFLTINT(15.3, 15); exam2 = myAnsINTCHAR(45, a);,注意,上述对函数名的具体压延过程和结果只是 为了便于叙述假定的,并非 C+ 编译器对这两个 函数名的真实压延结果。,4.1.2 类成员函数重载 类成员函数重载分为两类: 1 构造函数重载 为类对象的创建和类对象的数据成员赋初值提供不 同的方法。重载构造函数的参数必须不同。 2 公有成员函数重载 提供响应相同消息的不同接口方法。这类重载有两 种情况: 同一类中的重载成员函数的参数必须有差别。 派生类中分属于基类和派生类的重载成员函数的 参数可以相同,但基类的重载成员函数被覆盖。 例如:,class point int x, y; public: point(int a, int b) x = a; y = b; float area() return 0.0; ; class circle:public point int radius; public: circle(int x, int y, int rad):point(x, y) radius = rad; float area() / 重载成员函数 return float(3.1416)*float(radius)*float(radius); ;,main() point p(20, 20); circle c(8, 8, 30); cout p.area() endl; cout c.area() endl; cout c.point:area() endl; return 0; 其中 area 是参数相同的重载成员函数。编译过程 中区别这两个不同版本的 area 的情况有两种:, 使用对象名只能分别调用基类和派生类的参数 相同的重载成员函数。例如: p.area() 将调用 point:area() 方法; c.area() 将调用 circle:area 方法。 在对象名后添加 “基类名:” 可以使用派生类对 象名可以调用基类的参数相同的重载成员函 数。例如: c.point:area() 将调用 point:area() 方法。 返回,4.2 运算符重载 运算符可以视为是一种特殊的函数,它们可以用一 种简洁的,接近自然语言的表达式方式被调用。 C+ 拥有一系列的预定义运算符,这些运算符能对 所有的预定义类型的数据进行形式一致的操作。也就 是说,可以使用预定义类型的数据作参数调用这些运 算符函数。例如,加运算符 “+“: int x, y; float e, f; y = x + y; f = e + f;,这种对于不同类型的数据使用相同的表达式调用相同 的运算符正是通过重载机制实现的。但这些运算符却 不能自动施加在自定义类型的对象。例如:自定义的 复数类 complex 的定义如下: class complex double real, imag; public: complex(double r = 0, double i = 0) real = r; imag = i; ;,main() complex com1(1.1, 2.2), com2(3.3, 4.4), total; total = com1 + com2; / “+”运算符无法对复数进行操作 return 0; 产生上述错误的原因很显然,因为运算符 “+” 允许的操 作数据类型中不包括 complex 类型,所以编译器就不知 道调用该运算符的哪个重载版本来完成相应的操作。,解决的办法就是扩展 “+” 运算符允许操作的数据类 型,即为复数类 complex 定义自己的 “+” 运算符操作。 C+ 的运算符重载方法为设计不同类对象的同一行为 提供了非常有效和方便的手段。 运算符重载是通过对运算符函数的重载实现的,运 算符函数重载的原型和定义的一般形式如下: 函数类型 operator (参数表列); / 是运算符名的通配符号 函数类型 operator (参数表列) 重载操作代码 ,例如,对复数类 complex 的 “+” 运算符重载: class complex double real, imag; public: complex(double r = 0, double i = 0) real = r; imag = i; complex operator + (complex); ; complex complex:operator +(complex ,4.2.1 重载运算符的规则 C+ 不允许定义新的运算符,只能对系统预定义运 算符进行重载。 C+ 的预定义运算符中允许重载的包括:,不允许重载预定义运算符的包括: 重载不能改变预定义运算符函数的参数(操作数) 个数。双目运算符重载后仍为双目运算符,单目运 算符重载后仍为单目运算符。 重载不能改变预定义运算符的原有优先级。 重载不能改变预定义运算符的原有结合律。 重载运算符函数的参数不允许有缺省值。否则编译 器会认为是改变了运算符函数的参数个数。, 重载的运算符必须与用户自定义类型的对象一起使 用,因此重载运算符函数的参数中至少应该有一个 是自定义类对象或类对象的引用。换句话说,重载 运算符函数的参数不能全部是预定义类型对象,防 止用户修改预定义运算符的性质。例如: int operator +(int a, int b) return a b; 显然,这是绝对不允许的。 对于双目运算符,允许两个参数都是自定义类对 象,例如两个复数的加运算;也允许一个参数是自 定义类对象,另一个参数是预定义类型对象,例如 一个复数与一个实数的加运算。,complex operator +(double d, complex 系统会为每个自定义类缺省重载了赋值运算符 “=” 和取地址运算符 “&”,其他运算符都需要根据需要进 行重载定义。其中: 赋值运算符 “=” 用于同类对象之间的数据成员赋 值操作。一般情况下,缺省重载的赋值运算符 “=” 可以满足要求,但遇到类的数据成员中包含 了动态数据指针,则使用缺省重载赋值运算符就 可能发生危险,因此必须重载新的赋值运算符。 取地址运算符 “&” 用于计算返回对象在内存中的 起始地址。, 虽然可以任意定义重载运算符的操作功能,但应该 使重载运算符的功能类似于被重载运算符作用于预 定义类型数据时的操作功能。 运算符重载函数可以是运算所施加类对象的成员函 数,也可以是该类的友元函数,如果不需要访问类 的私有数据成员,还可以是既非类成员函数也非友 元函数的普通函数。,在 Java 中虽然也不乏运算符重载的例子,例如: String str = “hello“ + “there“; 该表达式相当于在 C+ 中的表达式: string str = “hello“ + “there“; 上述表达式都是因为对 Java 的 String 类型和 C+ 的 string 类型的进行了 “+” 运算符重载的结果。但要注意 的是在 Java 中不允许用户进行运算符重载。 返回,4.3 使用成员函数重载运算符 1 使用成员函数重载运算符的语法形式 在类定义体中声明要重载的运算符成员函数 type 运算符函数类型; operator 运算符函数名关键字; 要重载的运算符名; 参数表 被重载运算符所需的右操作数。 参数个数 = 运算符所需操作数个数 - 1,缺省 的左操作数必须是运算符所属类对象。例如:,type operator (参数表);,class complex double real, imag; public: complex operator + (complex ,type 类名:operator (参数列表) , 重载运算符的使用 运算符的左操作数必须是该运算符成员函数所属 类的对象。 双目运算符 例如: complex com1, com2, com3; com3 = com1 + com2; 或 com3 = com1.operator +(com2);,表达式形式:对象名 参数对象名; 函数形式: 对象名.operator (参数对象名);, 单目运算符 例如: complex com; +com; com+; 或 com.operator +(); com.operator +(0);,表达式形式:对象名; 对象名; 函数形式: 对象名.operator (); 对象名.operator (0);,2 用成员函数重载运算符的使用实例 例4-1 定义了一个表达三维空间位置的简单类 three_d, 在此类中含有三维空间位置的坐标。通过运算符 重载来实现对此类对象的 +、 和 = 运算。 注意:定义中 + 和 运算符函数的返回值不应 是被加对象 和被减对象,而 = 运算符函数的返 回值必须是被赋值的对象。这都是运算符的操作 含义所决定的。 返回,4.4 使用友元函数重载运算符 1 使用友元函数重载运算符的语法形式 在类定义体中声明重载运算符友元函数 friend 友元函数关键字 type 运算符函数类型; operator 运算符函数名关键字; 要重载的运算符名; 参数表 被重载运算符所需的操作数。 参数个数 = 运算符所需操作数个数。 例如:,friend type operator (参数表);,class point int x, y; public: friend point operator +(point ,type operator (参数表) , 重载运算符的使用 双目运算符 例如: point pt1, pt2, pt3; pt3 = pt1 + pt2; 或 pt3 = operator +(pt1, pt2);,表达式调用:参数对象名1 参数对象名2; 函数调用: operator (参数对象名1,参数对象名2);, 单目运算符 例如: point pt; +pt; pt+ 或 operator +(pt); operator +(pt, 0);,表达式调用:参数对象名; 参数对象名; 函数调用: operator (参数对象名); operator (参数对象名, 0);,2 使用友元函数重载运算符的应用实例 例4-2 使用友元函数重载运算符的方法实现复数的四则 运算,复数运算规则如下: (a + bi) + (c + di) = (a + c) + (b + d)i; (a + bi) - (c + di) = (a - c) + (b - d)i; (a + bi) * (c + di) = (ac - bd) + (bc + ad)i; (a + bi) / (c + di) = (ac + bd) + (bc - ad)i)/(c2 + d2); 定义一个复数类 complex,并声明和定义相应的 友元函数,用于重载运算符 +、-、*、/。,例4-3 使用友元函数重载运算符的方法实现集合运算。 集合可以用数组或链表表示,在该数组或链表中 不允许包含重复元素。定义整型数集合 set,元 素个数用整型变量 card 表示。集合的操作包括 向集合中追加元素、显示集合的全部元素以及使 用友元函数重载运算符的方法实现的集合的主要 运算,这些运算包含: 判定某一个元素属于集合的运算符 &; 判定两个集合相等的运算符 =;, 判定两个集合的不等于运算符 !=; 两个集合的交运算符 *; 两个集合的并运算符 +; 判定某集合是另一集合的子集运算符 =; 判定某集合是另一集合的纯子集运算符 。,4.4.1 两种运算符重载方法的比较 1 参数数目: 双目运算符 重载运算符的成员函数只有一个参数,用于 表示表达式的右操作数。 重载运算符的友元函数有两个参数,分别用 于表达式的左、右操作数 单目运算符 重载运算符的成员函数无参数。 重载运算符的友元函数带有一个参数,用于 表示表达式的唯一操作数。,2 无论是使用成员函数还是友元函数重载的运算符, 调用它们的表达式形式是一致的,而调用它们的函 数形式是有差别的。,3 大部分运算符重载函数既可以是成员函数,又可以 是友元函数。究竟选择哪一种好呢?要视实际情况 和程序员的习惯。但是应注意以下情况: 对于双目运算符,如果使用成员函数重载,则在 有些情况下会产生操作数类型错误。例如: class complex double real, imag; public: complex operator +(double x); ; complex complex:operator +(double x) return complex(real + x, imag); ,对 complex 的对象 com 进行如下的 + 运算: com = com + 100.0; 由于对象 com 是 + 运算符的左操作数,所以它调 用了 complex 类重载的 + 运算符,把实数 100.0 加 到 com 的实部数据成员 real 上。如果按照 + 运算 符操作数的交换律,将上面的表达式写成: com = 100.0 + com; 则会引起操作数类型错误,无法完成表达式所要 求的操作。这是因为 complex 类 + 运算符的左操 作数是 complex 类对象,而表达式的左操作数是 实数,无法调用 complex 类 + 运算符函数。,如果使用两个友元函数来替换上述 complex 类的 + 运算符重载: friend operator +(complex com, double x); friend operator +(double x, complex com); complex operator +(complex com, double x) return complex(com.real + x, imag); complex operator +(double x, complex com) return complex(x + com.real, imag); 则可避免上述问题,因为友元运算符函数的两个 操作数都是显式地传递给运算符函数的,能满足 加运算操作数的交换律。,所以一般建议使用友元函数重载双目运算符,特 别是期望双目运算符的左操作数的类型能够隐式 转换的情况,则重载双目运算符必须使用友元函 数,而不能使用成员函数。 若一个运算符需要修改其操作所施加对象(特别 是该对象为该运算符的第一操作数)的状态,则 选择使用成员函数重载运算符不破坏类对象的封 装性,更符合面向对象程序设计的原则。 返回,4.5 自增 + 和自减 - 运算符的重载 从运算操作的正确性考虑,使用成员函数还是使用 友元函数重载自增运算符 + 和自减运算符 - 的效果是 一致的。但从面向对象设计原则出发,类对象的自增 和自减运算实际上是对被封装的类数据成员依次进行 的。因此,建议使用成员函数实现自增运算符 + 和自 减运算符 - 的重载。 使用 + 和 - 的表达式形式分为前缀和后缀两种形 式,这两种形式对操作数的最终修改虽然相同,但运 算符函数的返回值不同,前缀形式返回修改后的操作 数,而后缀形式返回修改前的操作数。例如:,int i = 0, j = 0; i+; +j; cout i “,“ j endl; / 显示1,1 cout i+ “,“ +j endl; / 显示1,2 在 C+ 2.1 及以后的版本中,编译器可以通过判别在 自增或自减运算符函数的参数列表中是否增加了一个 附加的整型 int 形式参数来区分所重载的自增或自减运 算符可以用于前缀表达式还是用于后缀表达式。,1 自增运算符 “+” 使用成员函数重载 原型 前缀形式:类名 operator +(); 后缀形式:类名 operator +(int); 例如: class point int x, y; public: point operator +(); point operator +(int); ;, 定义 前缀形式:类名 类名:operator +() 后缀形式:类名 类名:operator +(int) 例如: point point:operatot +() +x, +y; return *this; point point:operatot +(int) point temp(x, y); +x, +y; return temp; , 调用 前缀表达式形式:+类对象名; 后缀表达式形式:类对象名+; 例如: point pt; +pt; / 前缀表达式 pt+; / 后缀表达式 前缀函数形式:对象名.operator +(); 后缀函数形式:对象名.operator +(0); 例如: point pt; pt.operator +(); / 前缀函数形式 pt.operator +(0); / 后缀函数形式,2 自减运算符 “-” 使用成员函数重载 原型 前缀形式:类名 operator -(); 后缀形式:类名 operator -(int); 例如: class point int x, y; public: point operator -(); point operator -(int); ;, 定义 前缀形式:类名 类名:operator -() 后缀形式:类名 类名:operator -(int) 例如: point point:operatot -() -x, -y; return *this; point point:operatot -(int) point temp(x, y); x-, y-; return temp; , 调用 前缀表达式形式:-类对象名; 后缀表达式形式:类对象名-; 例如: point pt; -pt; / 前缀表达式 pt-; / 后缀表达式 前缀函数形式:对象名.operator -(); 后缀函数形式:对象名.operator -(0); 例如: point pt; pt.operator -(); / 前缀函数形式 pt.operator -(0); / 后缀函数形式,例4-4 在 point 类中对运算符 + 和 - 进行重载,其操作 是对 point 类的数据成员 x 和 y 分别进行的。 注意,如果在程序中没有重载能用于后缀表达式形式 的 + 和 -,则在后缀表达式形式调用 + 和 - 时,会 出现编译警告,而运行时会以前缀表达式的结果替代 后缀表达式的操作结果。 返回,4.6 调用运算符 () 和下标运算符 的重载 1 调用运算符 “()” 顾名思义,调用运算符可以用函数调用表达式的形 式使运算符操作所施加的对象完成任何需要的功能 操作,例如,为操作对象的各个数据成员赋值。调 用该运算符的表达式的一般形式为: 类对象名(调用参数列表); 可以认为调用运算符是一个双目运算符。 左操作数: 必须是该运算符操作所施加的类对象。 右操作数: 为操作类对象所需要传递的一组参数, 这组参数可以由任意个数、任何类型的 对象组成,当然也可以无参数。,显然,应该使用成员函数重载调用运算符。 原型 返回类型 operator ()(调用参数列表); 其中返回类型可以是任何合法类型。例如: class point int x, y; public: void operator ()(int, int); ;, 定义 返回类型 类名:operator ()(调用参数列表) 例如: void point:operator ()(int x, int y) this-x = x; this-y = y; 调用 类对象名(调用参数列表); 例如: point pt; pt(50, 80);,例4-5 是一个使用成员函数重载调用运算符 “()” 的简单 实例,有助 于对调用运算符定义形式、使用方法 和用途的理解。 注意,调用运算符函数的右操作数,调用参数列表中 的参数也允许有缺省参数值。,2 下标运算符 “” 与调用运算符相似,下标运算符可以用下标访问表 达式的形式对运算符操作所施加对象的相关数据成 员进任何需要的访问和操作,例如,读写矩阵类对 象封装的内部矩阵(数组)的某个指定元素。调用 该运算符的表达式的一般形式为: 类对象名 下标列表; 可以认为下标运算符也是一个双目运算符。 左操作数: 必须是该运算符操作所施加的类对象。 右操作数: 为访问类对象所需要传递的一组下标, 这组下标可以由任意个数的整型对象组 成,不允许无下标。,显然,应该使用成员函数重载下标运算符。 原型 返回类型 operator (); 其中返回类型可以是除 void 外的任何合法类型。 例如: class Matrix double mt10, 10; public: double, 定义 返回类型 类名:operator (下标列表) 例如: double,例4-6 是一个使用成员函数重载下标运算符的简单实 例,有助于对下标运算符定义形式、使用方法和 用途的理解。 通过重载的下标运算符函数可以方便地访问私有数 据成员 divisionTotals 中的每一个元素。 重载下标运算符 “” 时,返回一个 int 的引用,使得 通过重载的 “” 访问的元素既能够读,也能够写。 因此,下标表达式可以出现在赋值语句的左边。,例4-7 是一个用来处理矩阵运算操作的实例。 假定有一个实数矩阵,需要对它进行加法、减法和 乘法运算(通过重载运算符 +、-、*来实现),为此需 要通过重载函数调用运算符 (),来返回矩阵元素,以 便在实现矩阵的加、减、乘运算中使用。注意:矩阵 的析构函数 matrix:matrix() if(elems) / 判断矩阵是否不为空 delete elems; elems = 0; 返回,4.7 动态内存管理运算符的重载 用于动态内存管理的运算符包括 new、delete (用于 单个对象的内存分配和释放)和 new、delete (用于 对象数组的内存分配和释放)。系统预定义的这两对 运算符可以适用于所有类型对象的动态内存管理,但 不能确保对所有类型对象的动态内存管理都高效,尤 其是对那些占用内存空间小、结构简单的类型对象, 使用那些为了适应复杂情况管理的时、空开销是没有 必要的,因此大大降低运行效率。解决这一问题的办 法就是重载动态内存管理运算符,定义适应类型对象 动态创建和撤消的内存管理操作。,重载动态内存管理运算符的方式有两种: 全局方式:重载的动态内存管理运算符完全替代了 原有的动态内存管理运算符 new、delete、new、 delete 。这种方式一般很少使用,除非确实需要修 改原来的通用管理算法或添加所有类型对象创建和 撤消都需要的附加操作。 局部方式:重载的运算符函数只对某个特定类对象 的创建和撤消有效,这是重载动态内存管理运算符 的常用方式。显然,将动态内存管理运算符重载函 数定义为成员函数更符合面向对象程序设计原则。,重载 new 和 delete 运算符的一般方法: 定义可以存放 n 个被管理类型对象的静态数组,作 为动态分配的内存储备。 定义空闲链,将被回收的对象内存链结起来。 定义指示变量,用于指示作为内存储备的静态数组 空间是否已经被初次顺序分配完。 重载动态内存分配运算符 new,用于顺序从静态数 组中或从空闲链中为动态创建的对象分配空间。 重载动态内存回收运算符 delete,用于释放动态创 建的对象,并将该被释放对象的内存空间链结到空 闲链中。,例4-8 定义适用于 point 类对象动态创建和撤消的内存 管理运算符 new 和 delete 重载成员函数,并测试 使用重载的 new 和 delete 动态创建和撤消 point 类对象的情况。 注意,此例中重载的 new 和 delete 运算符每次只能为 单个 point 类对象分配和释放内存 ,而如果分配和释放 point 类对象数组则需要重载 new 和 delete 运算符。,1 问题分析 分配一个具有足够空间静态数组(数组元素的结构 应满足存放 point 类对象属性和动态分配和释放的需 要。对该数组的分配和释放操作示意如下:,其中: 静态数组的元素由存放位置属性 x 和 y 的单元, 以及用于存放被释放元素地址的指针单元组成。 静态数据成员 used 用于指示从静态数组中已经被 初次顺序分配的元素个数。 静态数据成员 freelist 用于指向被释放回收的元素 链表。 元素的动态分配和释放操作分为两种情况: 静态数组中的元素未被初次顺序分配完,即 used 的值小于静态数组的元素总数时(元素第一次被 分配):, 分配:以 used 的值为下标,为用户动态分配 一个元素,并修改 used 的值。 释放:将新释放的元素与 freelist 指向的存放 已释放元素的空闲链连接,并使 freelist 指向 修改后的空闲链。 数组中的元素已被初次顺序分配完,即 used 的 值大于静态数组的元素总数时(数组中元素的再 分配): 分配:如果被释放元素的空闲链不为空,则从 空闲链中动态分配一个元素,并修改空闲链。, 释放:将新释放的元素与 freelist 指向的存放 已释放元 素的空闲链连接,并使 freelist 指向 修改后的空闲链。 将运算符 new 和 delete 重载函数声明和定义为 point 类的成员函数,point 类的类图被描述如下:,2 详细设计 类设计 point 类 类定义 class point int x, y; static int used; static block *freelist; public: point(int vx, int vy); void* operator new (size_t size); void operator delete (); void print(); ;,其中,block 为用于动态定义 point 对象时,分 配其属性空间的数据结构: struct block int x, y; block *next; ; 算法描述 重载运算符 new 的算法 N-S 图:,res = freelist 修改 freelist,res = 0,res = &blockused used = used + 1,返回结果指针 res,重载运算符 delete 的算法 N-S 图: 类的应用 在主函数 main 中动态创建 point 对象、显示对象 信息;然后动态释放动态创建 point 对象,用以 测试重载的动态内存管理运算符 new 和 delete 的 功能。 返回,被释放元素的指针 next = freelist,freelist = &(被释放元素的指针),4.8 赋值运算符重载 类对象之间的赋值操作是在一一对应的数据成员之 间进行的。所以赋值运算符的重载函数应作为类的成 员函数。赋值运算符成员函数的原型、定义和调用表 达式如下: 原型 类名 operator =(const 类名, 定义 类名 类名:operator = (const 类名 , 调用 对象名1 = 对象名2; 例如: point pt1, pt2; pt2 = pt1; 系统会为每一个类缺省定义一个隐含的赋值运算符 成员函数。 通常情况下,缺省赋值运算符是可以胜任 类对象之间的赋值操作,但在某些特殊情况下,如类 中有指针类型的数据成员时,使用缺省赋值运算符就 可能产生错误。例如:,#include #include class string char* ptr; public: string(char* s) ptr = new charstrlen(s) + 1; strcpy(ptr, s); string() delete ptr; void print() cout ptr endl; ;,void main() string p1(“Chen“); string p2(“Zhang“); p2 = p1; cout “p2:“; p2.print(); cout “p1:“; p1.print(); ,在执行 p1.print(); 时将发生错误。原因是执行赋值语 句 p2 = p1,使 p2.ptr 和 p1.ptr 都指向了 “Chen” 占用的 内存空间,而 p2.ptr 原来所指向的 “Zhang” 占用的内存 空间被泄漏。当 p2 的生命周期结束时,系统自动调用 析构函数将这一内存空间撤消。这时 p1 的指针成员 ptr 所指向的内存空间已经成为不允许访问的非法空间 了。下图描述了这一错误产生的过程:,执行 p2 = p1 之前,解决这一问题的方法是显式地定义一个重载赋值运 算符的成员函数,使 p1 和 p2 有各自的存储空间。 string / 返回被赋值的类对象引用 ,两点说明: 重载类的赋值运算符应该使用成员函数,而不应使 用友元函数,如果上述赋值运算符重载使用了友元 函数: friend string 这时,表达式 p1 = “chen“ 将被解释为 operator = (p1, “chen“) 这显然是没有什麽问题的,但对于表达式 “chen“ = p1 / 错误的赋值语句 将被解释为: operator = (“chen“, p1),即 C+ 编译器首先将 “chen” 转换成一个隐含的 string 对象,然后引用该隐含对象。因此并不认为这个表 达式是错误的,从而将导致赋值语法的混乱。 类的赋值运算符可以被重载,但重载的赋值运算符 成员函数在其派生类中是不能被继承的。 建议:在属性的复制操作上,重载赋值运算符和拷贝 构造函数具有一致性。因此,在需要用户自定义重载 赋值运算符和拷贝构造函数的情况下,一般先定义一 个实现属性复制操作的私有成员函数,然后在重载赋 值运算符和拷贝构造函数定义中调用此私有成员函 数。这样既保证了重载赋值运算符和拷贝构造函数的 一致性,又提高了代码的重用。 返回,4.9 输入输出运算符重载 4.9.1 重载输入运算符 “ 输入运算符 “” 是输入流类(将在第八章中详细讲 述)的成员函数。该运算符也是一个双目运算符,它 的左操作数必须是输入流类对象的引用,表示被输入 的信息必须来自标准的输入流设备;而右操作数是接 收输入信息的指定类对象的引用。因此,为自定义类 重载的输入运算符函数只能是类的友元函数。,1 原型 friend 输入流类,2 定义 输入流 ,3 调用 cin 类对象名; 例如: point pt; cin pt;,注意: 输入运算符重载函数的第一个参数的类型必须是输 入流类 istream 对象的引用,形参名(流对象名)可 以使用任何合法的标识符。 输入运算符重载函数的第二个参数的类型必须是接 收输入信息的指定类对象的引用,例如 point&, 而 不能使用指定类名,例如 point。 输入运算符重载函数的返回类型必须是输入流类 istream 对象的引用,并且在函数体中由 return 返回 的输入流类对象的引用名必须与第一个参数的形参 名(流对象名)一致。,4.9.2 重载输出运算符 “ 输出运算符 “” 是输出流类(将在第八章中详细讲 述)的成员函数。该运算符也是一个双目运算符,它 的左操作数必须是输出流类对象的引用,表示信息必 须被输出到标准的输出流设备;而右操作数是输出信 息的指定类对象。因此,为自定义类重载的输入运算 符函数也只能是类的友元函数。,1 原型 friend 输出流类,2 定义 输出流 ,3 调用 cout pt; cout pt;,注意: 输出运算符重载函数的第一个参数的类型必须是输 出流类 ostream 对象的引用,形参名(流对象名)可 以使用任何合法的标识符。 输出运算符重载函数的第二个参数的类型必须是输 出信息的指定类对象或对象的引用,例如 point&, 或 point。 输出运算符重载函数的返回类型必须是输出流类 ostream 对象的引用,并且在函数体中由 return 返回 的输出流类对象的引用名必须与第一个参数的形参 名(流对象名)一致。,例8-3 中对输入运算符 和输出运算符 和输出运算符 进行了重 载,使它们能对按照指定形式(例如由分子分母 组成的分数形式,3/8) 表示的有理数进行标准 输入输出操作。 返回,4.10 类型转换(函数) 所谓类型转换是指编译器将一种数据类型值转换为 另一种数据类型值的功能。 1 在 C+ 中,系统预定义类型和用户自定义类型的对 象均可以进行类型转换。 2 类型转换有两种形式: 隐式类型转换 如果发生了对象类型与表达 式的语法要求不符的情况,编译器能按照语言标 准确定的规则自动地将对象的类型进行转换。 显式类型转换 用户使用特定的语法表示形 式(类型转换法形式或函数法形式)指示编译器 将对象的当前类型转换为指定类型。,4.12.1 预定义数据类型间的类型转换 1 隐式类型转换 如果赋值表达式 A = B 中操作数的类型不一致, 则赋值运算符右端 B 会自动转换为 A 的类型后 再进行赋值运算。 当 char 或 short 类型变量与 int 类型变量进行运算 时,char 或 short 类型变量会先自动转换成 int 类 型,然后再进行运算。 如果参与算术运算的两个操作对象类型不一致, 则两个操作对象中精度低的类型会自动转换为精 度高的类型。,2 显式类型转换 强制转换法 格式:(类型名)表达式 例如: int i, j; cout (float)(i + j); / 将 i + j 的运算结果强制转换成 float 类型后输出 函数法 格式:类型名(表达式) 例如: int i, j; cout float(i + j); / 将 i+j 的运算结果作为类型函数 float 的参数。 两种方法实现的效果是等效的。使用显式类型转换 在有些情况下是必要的,例如:,int a = 5, b = 8; printf(“a = %f b = %f“, a, b); 会产生不可期望的结果,例如: a = 8192.000001 b = 0.000000 使用显式转换将上面的输出语句改写为: printf(“a = %f b = %f“, float(a), float(b); 就可以得到可以预期的结果: a = 5.000000 b = 8.000000,结构类型与预定义类型之间在数据的组成结构上没 有任何共同之处,因此不能进行这两种类型变量的 显式类型转换,但可以进行这两种类型指针变量的 显式类型转换。例如: #include struct example int y; float z; ;,main() char *str = “Windows!“; int *p = new int; *p = 67; example *ex1, *ex2; ex1 = (example*)str; / str 强制转换为 example 指针类型 ex2 = (example*)p; / p 强制转换为 example 指针类型 cout y z y z “n“;,delete p; str = (char*)ex1; / ex1 强制转换为 char 指针类型 cout str “n“; return 1; 输出结果可能如下: 26999 4。855453e+33 67 0 Windows!,4.12.2 通过构造函数进行类型转换 在面向对象程序设计中自定义类型之间以及自定义 与预定义数据类型之间的转换是经常发生的。通过类 的构造函数进行类型转换就是方法之一,进行这种转 换的前提是: 如果 A 类对象能够转换为 B 类对象,则类型 B 必须 有一个仅以 A 类对象为参数的构造函数。换句话说, 就是 B 类对象创建时,只需要传递一个 A 类对象的实 参。例如:,class example public: example(int) example(const char*, int i=0) ; void f(example arg) void main() example a = example(1); / example b = 3; / example c = “Windows!“; / f(3); / ,分析原因: 构造函数 example(int) 将整数 1 显式转换为 example 类对象后赋给 example 类对象 a。 构造函数 example(int) 将整数 3 隐式转换为 example 类对象后赋给 example 类对象 b。 构造函数 example(const char*, int i=0) 将字符串变量 “Windows!” 隐式转换为 example 类对象后赋给 example 类对象 c。 构造函数 example(int) 将整数 3 隐式转换为 example 类对象后作为实参传给函数 f。,通过构造函数实现的类型转换可以隐式的,即可以 隐含自动完成类型转换,但有时这种隐式转换又不是 用户所期望的,甚至是要禁止的。如果要禁止这种隐 式转换,可以将会引起隐式转换的构造函数声明为私 有成员函数。例如: class vector vector(int s); / 构造函数作为私有成员 public: vector(); static vector make(int s) return s; / 构造函数将参数 s 隐式转换成 vector 对象后返回。 ;,main() vector v = vector:make(10); / 将参数10 显式转换成 vector 对象后赋给用无参数构造 / 函数创建的 vector 对象 v。 v = 10; / 无法将整数 10 隐式转换 vector 对象。 v = vector:make(99); / 将参数 99 显式转换成 vector 对象后赋给用无参数构 / 造函数创建的 vector 对象 v。 ,禁止通过构造函数发生隐式类型转换,还可以使用 explicit 关键字将具有隐式类型转换功能的构造函数声 明为只能显式类型转换,即通过这个构造函数进行类 型转换时,必须使用强制类型转换表达式。例如: class MyClass int m; public: explicit MyClass(int i = 0); ;,MyClass:MyClass(int i) m = i; void fun(MyClass); 在这种情况下,函数表达式 f
温馨提示
- 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
- 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
- 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
- 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
- 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
- 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
- 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
最新文档
- 坐月子饮食调理常识试题及答案
- 2025年文化产业引导资金申请项目可持续发展战略报告
- 推拿治疗学考试题库附参考答案详解(夺分金卷)
- 2025年肿瘤精准医疗临床实践中的临床试验信息化技术应用支持服务研究报告
- 2025年职业技能培训在乡村振兴中的需求与供给研究报告
- 推拿治疗学考试题库及参考答案详解【夺分金卷】
- 2025年老龄化趋势下老年教育课程体系构建与创新实践报告
- 2025至2030年中国国际旅游度假市场行情动态分析及发展前景趋势预测报告
- 解析卷-华东师大版8年级下册期末试题及参考答案详解【培优】
- 2025至2030年中国大黄提取物行业市场发展现状及未来发展趋势预测报告
- 汤小丹《计算机操作系统》官方课件 第四版
- 走近昆曲《牡丹亭》
- 3D打印混凝土材料性能试验方法
- 装饰色彩课件
- XX学校学校集体备课实施方案细则、方案、计划、制度、总结(全套资料)
- 医疗设备、器械项目实施方案、服务实施方案
- 非居民金融账户涉税信息尽职调查和信息报送制度
- (医学课件)急诊科进修汇报
- 世界排名前100的大学校徽
- 医疗纠纷典型案例分析课件
- 分布式光伏发电项目投标技术方案(纯方案)
评论
0/150
提交评论