5.25k likes | 5.38k Views
程序设计. 翁惠玉 Email: hyweng@sjtu.edu.cn weng-hy@cs.sjtu.edu.cn. 教材参考教材. C++ 程序设计思想与方法 人民邮电出版社 翁惠玉 C++ Primer (第 4 版) 人民邮电出版社 C++ 大学教程(第 5 版) 电子工业出版社. 第 10 章 创建功能更强的类型. 从面向过程到面向对象 类的定义 对象的使用 对象的构造与析构 常量对象与 const 成员函数 常量数据成员 静态数据成员与静态成员函数 友元. 从面向过程到面向对象. 抽象的过程
E N D
程序设计 翁惠玉 Email: hyweng@sjtu.edu.cn weng-hy@cs.sjtu.edu.cn
教材参考教材 • C++程序设计思想与方法 人民邮电出版社 翁惠玉 • C++ Primer(第4版) 人民邮电出版社 • C++大学教程(第5版) 电子工业出版社
第10章 创建功能更强的类型 • 从面向过程到面向对象 • 类的定义 • 对象的使用 • 对象的构造与析构 • 常量对象与const成员函数 • 常量数据成员 • 静态数据成员与静态成员函数 • 友元
从面向过程到面向对象 • 抽象的过程 • 面向对象的程序设计的特点 • 库和类
抽象的过程 • 计算机的工作是建立在抽象的基础上。 • 机器语言和汇编语言是对机器硬件的抽象 • 高级语言是对汇编语言和机器语言的抽象 • 现有抽象的问题: • 要求程序员按计算机的结构去思考,而不是按要解决的问题的结构去思考。 • 当程序员要解决一个问题时,必须要在机器模型和实际要解决的问题模型之间建立联系。 • 而计算机的结构本质上还是为了支持计算,当要解决一些非计算问题时,这个联系的建立是很困难的
面向对象的程序设计 • 为程序员提供了创建工具的功能 • 解决一个问题时 • 程序员首先考虑的是需要哪些工具 • 创建这些工具 • 用这些工具解决问题 • 工具就是所谓的对象 • 现有的高级语言提供的工具都是数值计算的工具
过程化vs面向对象 以计算圆的面积和周长的问题为例 • 过程化的设计方法:从功能和过程着手 • 输入圆的半径或直径 • 利用S=πr2和C=2πr计算面积和周长 • 输出计算结果 • 面向对象的程序设计方法: • 需要什么工具。如果计算机能提供给我们一个称为圆的工具,它可以以某种方式保存一个圆,告诉我们有关这个圆的一些特性,如它的半径、直径、面积和周长。 • 定义一个圆类型的变量,以他提供的方式将一个圆保存在该变量中,然后让这个变量告诉我们这个圆的面积和周长是多少
从面向过程到面向对象 • 抽象的过程 • 面向对象的程序设计的特点 • 库和类
面向对象的程序设计的特点 • 代码重用:圆类型也可以被那些也需要处理圆的其他程序员使用 • 实现隐藏: • 类的创建者创造新的工具 • 类的使用者则收集已有的工具快速解决所需解决的问题 • 这些工具是如何实现的,类的使用者不需要知道
人 教师 学生 教辅 高级 中级 初级 本科 硕士 博士 实验室 行政 面向对象的程序设计的特点 • 继承:在已有工具的基础上加以扩展,形成一个功能更强的工具。如在学校管理系统中,可以形成如下的继承关系
面向对象的程序设计的特点 • 多态性: • 当处理层次结构的类型时,程序员往往想把各个层次的对象都看成是基类成员。 • 如需要对教师进行考核,不必管他是什么职称,只要向所有教师发一个考核指令。每位教师自会按照自己的类型作出相应的处理。如高级职称的教师会按高级职称的标准进行考核,初级职称的教师会按初级职称的标准进行考核。 • 好处:程序代码就可以不受新增类型的影响。如增加一个院士的类型,它也是教师类的一个子类,整个程序不用修改,但功能得到了扩展。
从面向过程到面向对象 • 抽象的过程 • 面向对象的程序设计的特点 • 库和类
库和类 • 类是更合理的库 • 例:设计一个库,提供动态实型数组服务,该数组满足两个要求: • 可以任意指定下标范围; • 下标范围可在运行时确定; • 使用下标变量时会检查下标的越界。
库的设计 • 数组的保存 • 数组需要一块保存数组元素的空间。这块空间需要在执行时动态分配。 • 数组的下标可以由用户指定范围。因此,对每个数组还需要保存下标的上下界。 • 保存这个数组的三个部分是一个有机的整体,因此可以用一个结构体把它们封装在一起。 • 数组操作 • 给数组分配空间 • 给数组元素赋值 • 取某一个数组元素的值 • 由于这个数组的存储空间是动态分配的,因此,还必须有一个函数去释放空间
Array库的头文件 #ifndef _array_h #define _array_h //可指定下标范围的数组的存储 struct DoubleArray { int low; int high; double *storage; };
//根据low和high为数组分配空间 bool initialize(DoubleArray &arr, int low, int high); //设置数组元素的值 bool insert(const DoubleArray &arr, int index, double value); //取数组元素的值 bool fatch(const DoubleArray &arr, int index, double &value); //回收数组空间 void cleanup(const DoubleArray &arr); #endif
Array库的实现文件 #include "array.h" #include <iostream> using namespace std; bool initialize(DoubleArray &arr, int low, int high) { arr.low = low; arr.high = high; arr.storage = new double [high - low + 1]; if (arr.storage == NULL) return false; else return true; }
bool insert(const DoubleArray &arr, int index, double value) { if (index < arr.low || index > arr.high) return false; arr.storage[index - arr.low] = value; return true; } bool fatch(const DoubleArray &arr, int index, double &value) { if (index < arr.low || index > arr.high) return false; value = arr.storage[index - arr.low] ; return true; } void cleanup(const DoubleArray &arr) { delete [] arr.storage; }
Array库的应用 #include <iostream> using namespace std; #include "array.h" int main() { DoubleArray array; double value; int low, high, i; cout << "请输入数组的下标范围:"; cin >> low >> high; if (!initialize(array, low, high)) { cout << "空间分配失败" ; return 1; }
for (i = low; i <= high; ++i) { cout << "请输入第" << i << "个元素:"; cin >> value; insert(array, i, value); } while (true) { cout << "请输入要查找的元素序号(0表示结束):"; cin >> i; if (i == 0) break; if (fatch(array, i, value)) cout << value << endl; else cout << "下标越界\n"; } cleanup(array); return 0; }
Array库的问题 • 这个数组的使用相当笨拙。每次调用和数组有关的函数时,都要传递一个结构体给它。 • 我们可能在一个程序中用到很多库,每个库都可能需要做初始化和清除工作。库的设计者都可能觉得initialize和cleanup是比较合适的名字,因而都写了这两个函数。 • 系统内置的数组在数组定义时就指定了元素个数,系统自动会根据元素个数为数组准备存储空间。而我们这个数组的下标范围要用initialize函数来指定,比内置数组多了一个操作。 • 当数组使用结束后,还需要库的用户显式地归还空间。 • 对数组元素的操作不能直接用下标变量的形式表示。
Array库的改进 • 将函数放入结构体 • 好处: • 函数原型中的第一个参数不再需要。编译器自然知道函数体中涉及到的low, high和storage是同一结构体变量中的成员 • 函数名冲突的问题也得到了解决。现在函数名是从属于某一结构体,从属于不同结构体的同名函数是不会冲突的。
改进后的Array库的头文件 #ifndef _array_h #define _array_h struct DoubleArray{ int low; int high; double *storage; bool initialize(int lh, int rh); bool insert(int index, double value); bool fatch(int index, double &value); void cleanup(); }; #endif 函数都瘦身了!
改进后的Array库的实现文件 • 与原来的实现有一个变化:函数名前要加限定 bool IntArray::initialize(int lh, int rh) {low = lh; high = rh; storage = new double [high - low + 1]; if (storage == NULL) return false; else return true; }
改进后的Array库的应用 • 函数的调用方法不同。就如引用结构体的成员一样,要用点运算符引用这些函数
#include <iostream> using namespace std; #include "array.h“ int main() { DoubleArray array; double value; int low, high, i; cout << "请输入数组的下标范围:"; cin >> low >> high; if (!array.initialize(low, high)) { cout << "空间分配失败" ; return 1;}
for (i = low; i <= high; ++i) { cout << "请输入第" << i << "个元素:"; cin >> value; array.insert(i, value); } while (true) { //数组元素的查找 cout << "请输入要查找的元素序号(0表示结束):"; cin >> i; if (i == 0) break; if (array.fatch(i, value)) cout << value << endl; else cout << "下标越界\n"; } array.cleanup(); return 0; }
将函数放入结构体的意义 • 将函数放入结构体是从C到C++的根本改变 • 在C中,结构体只是将一组相关的数据捆绑了起来,它除了使程序逻辑更加清晰之外,对解决问题没有任何帮助。 • 将处理这组数据的函数也加入到结构体中,结构体就有了全新的功能。它既能描述属性,也能描述对属性的操作。事实上,它就成为了和内置类型一样的一种全新的数据类型。 • 为了表示这是一种全新的概念,C++用了一个新的名称 — 类来表示。
第10章 创建功能更强的类型 • 从面向过程到面向对象 • 类的定义 • 对象的使用 • 对象的构造与析构 • 常量对象与const成员函数 • 常量数据成员 • 静态数据成员与静态成员函数 • 友元
类定义的一般格式 • class 类名 { [private:] 私有数据成员和成员函数 public: 公有数据成员和成员函数 };
公有和私有成员 • 私有成员(private):只能由类的成员函数调用 • 公有成员(public):类的用户可以调用的信息,是类对外的接口 • 私有成员被封装在一个类中,类的用户是看不见的
DoubleArray类的定义 class DoubleArray { private: int low; int high; double *storage; public: bool initialize(int lh, int rh); bool insert(int index, double value); bool fatch(int index, double &value); void cleanup(); };
类定义说明 • private 和public的出现次序可以是任意的。也可以反复出现多次。 • 成员还可以被说明为protected • 数据成员一般说明为private,需要被用户调用的函数说明为public
接口和实现分开 • 与库设计一样,类的定义写在接口文件中,成员函数的实现写在实现文件中。 • 某些简单的成员函数的定义可以直接写在类定义中。 • 在类定义中定义的成员函数被认为是内联函数。
类定义实例 • 试定义一个有理数类,该类能提供有理数的加和乘运算。要求保存的有理数是最简形式。如2/6应记录为1/3。
设计考虑 • 保存有理数:保存一个有理数就是保存两个整数。 • 有理数类的操作: • 加函数 • 乘函数 • 创建有理数的函数,用以设置有理数的分子和分母 • 输出有理数函数 • 化简函数 • 访问权限设计: • 数据成员是私有的 • 化简函数是内部调用的函数,与用户无关,因此也是私有的 • 其他函数都是公有的
有理数类的定义 #ifndef _rational_h #define _rational_h #include <iostream> using namespace std; class Rational { private: int num; int den; void ReductFraction(); //将有理数化简成最简形式 public: void create(int n, int d) { num = n; den = d;} void add(const Rational &r1, const Rational &r2); void multi(const Rational &r1, const Rational &r2); void display() { cout << num << '/' << den;} }; #endif
有理数类的实现 #include "Rational.h“ //add函数将r1和r2相加,结果存于当前对象 void Rational::add(const Rational &r1, const Rational &r2) { num = r1.num * r2.den + r2.num * r1.den; den = r1.den * r2.den; ReductFraction(); }
void Rational::multi(const Rational &r1, const Rational &r2) { num = r1.num * r2.num; den = r1.den * r2.den; ReductFraction(); } // ReductFraction实现有理数的化简 void Rational::ReductFraction() {int tmp = (num > den) ? den : num; for (; tmp > 1; --tmp) if (num % tmp == 0 && den % tmp ==0) {num /= tmp; den /= tmp; break;} }
第10章 创建功能更强的类型 • 从面向过程到面向对象 • 类的定义 • 对象的使用 • 对象的构造与析构 • 常量对象与const成员函数 • 常量数据成员 • 静态数据成员与静态成员函数 • 友元
对象的定义 • 类与对象的关系:类型与变量的关系 • 对象定义方法: • 直接在程序中定义某个类的对象 存储类别 类名 对象列表; 如定义两个IntArray类的对象arr1和arr2,可写成:IntArray arr1, arr2; • 用动态内存申请的方法申请一个动态对象。 Rational *rp; Rp = new Rational; rp = new Rational[20]; delete Rp;或 delete []rp;
对象的引用 • 对象名.数据成员名 或 对象指针->数据成员名 arr1.storage 或 rp->num • 对象名.成员函数名(实际参数表) 或对象指针->成员函数名(实际参数表) arr1.insert() 或 rp->add() • 外部函数不能引用对象的私有成员
有理数类的使用 计算两个有理数的和与积 #include <iostream> using namespace std; #include "Rational.h" //使用有理数类 int main() { int n, d; Rational r1, r2, r3; //定义三个有理数类的对象
cout << "请输入第一个有理数(分子和分母):"; cin >> n >> d; r1.create(n,d); cout << "请输入第二个有理数(分子和分母):"; cin >> n >> d; r2.create(n,d); r3.add(r1, r2); //执行r3=r1+r2 r1.display(); cout << " + "; r2.display(); cout << " = "; r3.display(); cout << endl; r3.multi(r1, r2); //执行r3=r1*r2 r1.display(); cout << " * "; r2.display(); cout << " = "; r3.display(); cout << endl; return 0; }
执行实例 请输入第一个有理数(分子和分母):1 6 请输入第二个有理数(分子和分母):1 6 1 / 6 + 1 / 6 = 1 / 3 1 / 6 *1 / 6 = 1 / 36
对象赋值语句 • 同类型的对象之间可以互相赋值,如两个有理数对象r1和r2,可以执行 r1 = r2; • 当一个对象赋值给另一个对象时,所有的数据成员都会逐位拷贝。上述赋值相当于执行 r1.num = r2.num r1.den = r2.den
this指针 • 当定义一个对象时,系统会为对象分配空间,用于存储对象的数据成员。而类中的成员函数对该类的所有对象只有一份拷贝。那么对于类的成员函数,它如何知道要对哪个对象进行操作呢? • 每个成员函数都有一个隐藏的指向本类型的指针形参this,它指向当前调用成员函数的对象 • 如对函数 void create(int n, int d) { num = n; den = d;} 经过编译后,实际函数为 void create(int n, int d) { this->num = n; this->den = d;}
this指针 • 通常,在写成员函数时可以省略this,编译时会自动加上它们。 • 如果在成员函数中要把对象作为整体来访问时,必须显式地使用this指针。这种情况常出现在函数中返回一个对调用函数的对象的引用,
第10章 创建功能更强的类型 • 从面向过程到面向对象 • 类的定义 • 对象的使用 • 对象的构造与析构 • 常量对象与const成员函数 • 常量数据成员 • 静态数据成员与静态成员函数 • 友元
对象的构造与析构 • 某些类的对象,必须在对它进行了初始化以后才能使用。 • 对于某些类的对象在消亡前,往往也需要执行一些操作,做一些善后的处理。 • 初始化和扫尾的工作给类的用户带来了额外的负担,使他们觉得类和内置类型还是不一样。 • 用户希望使用类的对象就和使用内置类型的变量一样,一旦定义了,就能直接使用。用完了,由系统自动回收。