首页 > 代码库 > 创建功能更强的类型

创建功能更强的类型

抽象的过程

*计算机的工作是建立在抽象的基础上。
  -机器语言和汇编语言是对机器硬件的抽象
  -高级语言是对汇编语言和机器语言的抽象
*现有抽象的问题:
  -要求程序员按计算机的结构去思考,而不是按要解决的问题的结构去思考。
  -当程序员要解决一个问题时,必须要在机器模型和实际要解决的问题模型之间建立联系。
  -而计算机的结构本质上还是为了支持计算,当要解决一些非计算问题时,这个联系的建立是很困难的 
 
面向对象的程序设计
*为程序员提供了创建工具的功能
*解决一个问题时
  -程序员首先考虑的是需要哪些工具
  -创建这些工具
  -用这些工具解决问题
*工具就是所谓的对象
*现有的高级语言提供的工具都是数值计算的工具
 
过程化vs面向对象
以计算圆的面积和周长的问题为例
*过程化的设计方法:从功能和过程着手
  -输入圆的半径或直径
  -利用S=πr2和C=2πr计算面积和周长
  -输出计算结果
*面向对象的程序设计方法:
  -需要什么工具。如果计算机能提供给我们一个称为圆的工具,它可以以某种方式保存一个圆,告诉我们有关这个圆的一些特性,如它的半径、直径、面积和周长。
  -定义一个圆类型的变量,以他提供的方式将一个圆保存在该变量中,然后让这个变量告诉我们这个圆的面积和周长是多少 
 
面向对象的程序设计的特点
*代码重用:圆类型也可以被那些也需要处理圆的其他程序员使用
*实现隐藏:
  -类的创建者创造新的工具
  -类的使用者则收集已有的工具快速解决所需解决的问题
  -这些工具是如何实现的,类的使用者不需要知道 
*继承:在已有工具的基础上加以扩展,形成一个功能更强的工具。如在学校管理系统中,可以形成如下的继承关系
*多态性:
  -当处理层次结构的类型时,程序员往往想把各个层次的对象都看成是基类成员。
  -如需要对教师进行考核,不必管他是什么职称,只要向所有教师发一个考核指令。每位教师自会按照自己的类型作出相应的处理。如高级职称的教师会按高级职称的标准进行考核,初级职称的教师会按初级职称的标准进行考核。
*好处:程序代码就可以不受新增类型的影响。如增加一个院士的类型,它也是教师类的一个子类,整个程序不用修改,但功能得到了扩展。
 
库和类
*类是更合理的库
*例:设计一个库,提供动态整型数组服务,该数组满足两个要求:
  -可以任意指定下标范围;
  -下标范围可在运行时确定;
  -使用下标变量时会检查下标的越界。
 
库的设计
*数组的保存
  -数组需要一块保存数组元素的空间。这块空间需要在执行时动态分配。
  -数组的下标可以由用户指定范围。因此,对每个数组还需要保存下标的上下界。
  -保存这个数组的三个部分是一个有机的整体,因此可以用一个结构体把它们封装在一起。
*数组操作
  -给数组分配空间
  -给数组元素赋值
  -取某一个数组元素的值
  -由于这个数组的存储空间是动态分配的,因此,还必须有一个函数去释放空间 
 
Array库的头文件
 1 #ifndef _array_h
 2 #define _array_h
 3 
 4 //可指定下标范围的数组的存储
 5 struct IntArray
 6 { int low;  
 7   int high;
 8   int *storage;
 9 };
10 
11 //根据low和high为数组分配空间。分配成功,返回值为true,否则为false
12 bool initialize(IntArray &arr, int low, int high);
13 //设置数组元素的值
14 //返回值为true表示操作正常,返回值为false表示下标越界
15 bool insert(const IntArray &arr, int index, int value);
16 //取数组元素的值
17 //返回值为true表示操作正常,为false表示下标越界
18 bool fatch(const IntArray &arr, int index, int &value);
19 //回收数组空间
20 void cleanup(const IntArray &arr);
21 #endif

Array库的实现文件

 1 #include "array.h"
 2 #include <iostream>
 3 using namespace std;
 4 
 5 //根据low和high为数组分配空间。
 6 //分配成功,返回值为true,否则为false
 7 bool initialize(IntArray &arr, int low, int high)
 8 {arr.low = low;
 9  arr.high = high;
10  arr.storage = new int [high - low + 1];
11  if (arr.storage == NULL) return false; else return true;
12 }
13 
14 //设置数组元素的值
15 //返回值为true表示操作正常,返回值为false表示下标越界
16 bool insert(const IntArray &arr, int index, int value)
17 {if (index < arr.low || index > arr.high) return false;
18  arr.storage[index - arr.low] = value;
19  return true;
20 }
21 //取数组元素的值
22 //返回值为true表示操作正常,为false表示下标越界
23 bool fatch(const IntArray &arr, int index, int &value)
24 {if (index < arr.low || index > arr.high) return false;
25  value = http://www.mamicode.com/arr.storage[index - arr.low] ;
26  return true;
27 }
28 //回收数组空间
29 void cleanup(const IntArray &arr)  { delete [ ] arr.storage; } 

Array库的应用

 1 #include <iostream>
 2 using namespace std;
 3 #include "array.h" 
 4 
 5 int main()
 6 { IntArray array; //IntArray是array库中定义的结构体类型
 7   int value, i;
 8 
 9  //初始化数组array,下标范围为20到30
10   if (!initialize(array, 20, 30))
11       { cout << "空间分配失败" ; return 1;} 
12 for (i=20; i<=30; ++i) {
13      cout << "请输入第" << i << "个元素:";
14      cin >> value;
15      insert(array, i, value); 
16  }
17  while (true) {
18      cout << "请输入要查找的元素序号(0表示结束):";
19      cin >> i;
20      if (i == 0) break;
21      if (fatch(array, i, value)) cout << value << endl;
22          else cout << "下标越界\n";
23  }
24  cleanup(array); //回收array的空间
25  return 0;
26 }

 

Array库的问题

*这个数组的使用相当笨拙。每次调用和数组有关的函数时,都要传递一个结构体给它。
*我们可能在一个程序中用到很多库,每个库都可能需要做初始化和清除工作。库的设计者都可能觉得initialize和cleanup是比较合适的名字,因而都写了这两个函数。
*系统内置的数组在数组定义时就指定了元素个数,系统自动会根据元素个数为数组准备存储空间。而我们这个数组的下标范围要用initialize函数来指定,比内置数组多了一个操作。
*当数组使用结束后,还需要库的用户显式地归还空间。
*对数组元素的操作不能直接用下标变量的形式表示。 

Array库的改进

*将函数放入结构体
*好处:
  -函数原型中的第一个参数不再需要。编译器自然知道函数体中涉及到的low,  high和storage是同一结构体变量中的成员
  -函数名冲突的问题也得到了解决。现在函数名是从属于某一结构体,从属于不同结构体的同名函数是不会冲突的。
 
改进后的Array库的头文件
 1 #ifndef _array_h
 2 #define _array_h
 3 struct IntArray
 4 {
 5   int low;  
 6   int high;
 7   int *storage;
 8   bool initialize(int lh, int rh);
 9   bool insert(int index, int value);
10   bool fatch(int index, int &value);
11   void cleanup();
12 };
13 #endif

改进后的Array库的实现文件

与原来的实现有一个变化:函数名前要加限定

1 bool IntArray::initialize(int lh, int rh)
2 {low = lh;
3  high = rh;
4  storage = new int [high - low + 1];
5  if (storage == NULL) return false; else return true;
6 } 

改进后的Array库的应用

函数的调用方法不同。就如引用结构体的成员一样,要用点运算符引用这些函数 
 1 int main()
 2 {IntArray array;
 3  int value, i;
 4  if (!array.initialize(20, 30)) { cout << "空间分配失败" ; return 1;}
 5  for (i=20; i<=30; ++i) {
 6      cout << "请输入第" << i << "个元素:";     cin >> value;
 7      array.insert(i, value);
 8  }
 9  while (true) {
10      cout << "请输入要查找的元素序号(0表示结束):";
11      cin >> i;
12      if (i == 0) break;
13      if (array.fatch(i, value)) cout << value << endl;  
14           else cout << "下标越界\n";
15  }
16  array.cleanup();
17  return 0;
18 }

 

将函数放入结构体的意义

*将函数放入结构体是从C到C++的根本改变
*在C中,结构体只是将一组相关的数据捆绑了起来,它除了使程序逻辑更加清晰之外,对解决问题没有任何帮助。
*将处理这组数据的函数也加入到结构体中,结构体就有了全新的功能。它既能描述属性,也能描述对属性的操作。事实上,它就成为了和内置类型一样的一种全新的数据类型。
*为了表示这是一种全新的概念,C++用了一个新的名称 — 类来表示。
 
类的接口和实现分开
*与库设计一样,类的定义写在接口文件中,成员函数的实现写在实现文件中。
*某些简单的成员函数的定义可以直接写在类定义中。
*在类定义中定义的成员函数被认为是内联函数。
 
this指针
*通常,在写成员函数时可以省略this,编译时会自动加上它们。
*如果在成员函数中要把对象作为整体来访问时,必须显式地使用this指针。这种情况常出现在函数中返回一个对调用函数的对象的引用
 
动态对象的初始化
*动态变量的初始化是在类型后面用一个圆括号指出它的实际参数表
*如果要为一个动态的IntArray数组指定下标范围为20到30,可用下列语句:

      p = new IntArray(20, 30);

*括号中的实际参数要和构造函数的形式参数表相对应。
 
为什么要使用初始化列表 
我们完全可以在函数体内对数据成员赋初值!!!
*事实上,不管构造函数中有没有构造函数初始化列表,在执行构造函数体之前,都要先调用每个数据成员对应的类型的构造函数初始化每个数据成员。
*在构造函数初始化列表中没有提到的数据成员,系统会用该数据成员对应类型的默认构造函数对其初始化。
*显然利用初始化列表可以提高构造函数的效率。在初始化数据成员的同时完成了赋初始值的工作。
 
必须使用初始化的情况
*数据成员不是普通的内置类型,而是某一个类的对象,可能无法直接用赋值语句在构造函数体中为它赋初值
*类包含了一个常量的数据成员,常量只能在定义时对它初始化,而不能对它赋值。因此也必须放在初始化列表中。 
 
重载构造函数
*构造函数可以重载,导致对象可以有多种方式构造
*试设计一个时间转换器,用户可输入秒、分秒或时分秒输出相应的秒数。
时间转换器的实现
 1 #include <iostream>
 2 using namespace std;
 3 class timer{  int second;
 4   public:
 5     timer(int t)    {second=t;}
 6     timer(int min, int sec)  {second=60*min+sec;}
 7     timer(int h, int min, int sec)  {second=sec+60*min+3600*h;}
 8     int gettime()    {return second;}
 9 }
10 main()
11 {timer a(20),b(1,20),c(1,1,10);
12  cout<<a.gettime()<<endl;
13  cout<<b.gettime()<<endl;
14  cout<<c.gettime()<<endl;
15 }

 

析构函数不能重载,没有参数,没有返回值

 

缺省的拷贝构造函数

*如果用户没有定义拷贝构造函数,系统会定义一个缺省的拷贝构造函数。该函数将已存在的对象原式原样地复制给新成员
 
何时需要自定义拷贝构造函数
*一般情况下,默认的拷贝构造函数足以满足要求。
*但某些情况下可能需要设计自己的拷贝构造函数。
*例如,我们希望对IntArray类增加一个功能,能够定义一个和另一个数组完全一样的数组。但默认的拷贝构造函数却不能胜任。如果正在构造的对象为arr1,作为参数的对象是arr2,调用默认的拷贝构造函数相当于执行下列操作:

        arr1.low = arr2.low;

        arr1.high = arr2.high;

        arr1.storage = arr2.storage;

       前两个操作没有问题,第三个操作中,storage是一个指针,第三个操作意味着使arr1的storage指针和arr2的storage指针指向同一块空间。 

 

对象定义时

*拷贝构造函数用于对象构造时有两种用法:直接初始化和拷贝初始化。
*直接初始化将初始值放在圆括号中,直接调用与实参类型相匹配的拷贝构造函数。如

      IntArray array2(array1);

*拷贝初始化是用“=”符号。当使用拷贝初始化时,首先会用“=”右边的表达式构造一个临时对象,再调用拷贝构造函数将临时对象复制到正在构造的对象。如

      IntArray array =  IntArray(20, 30);

      IntArray array1=array2;

 

自定义拷贝构造函数举例

 1 class point{ int x, y;
 2  public: point(int a, int b){x=a; y=b;}
 3          point(const point &p)    {x=2*p.x; y=2*p.y;}
 4          void print()   {cout<<x<<"  "<<y<<endl;}
 5 };
 6 void main()
 7 {point p1(10, 20), p2(p1), p3 = p1, p4(1, 2); 
 8  p1.print();  p2.print();  p3.print();  p4.print();
 9  p4 = p1;  p4.print(); //这个p4=p1并没有调用拷贝构造函数,而是调用的赋值函数
10  }

 

const对象

*const对象的定义

    const MyClass obj(参数表);

*const对象不能被赋值,只能初始化,而且一定要初始化,否则无法设置它的值。
*C++规定:对const对象只能调用const成员函数
 
const函数
*任何不修改数据成员的函数都应该声明为const类型。如果在编写const成员函数时,不慎修改了数据成员,或者调用了其他非const成员函数,编译器将指出错误,这无疑会提高程序的健壮性。

    class A {   int x;

            public:

                    A(int i) {x=i;}

                   int getx() const

                           {return x;}

                  };

 

常量成员

*const数据成员只在某个对象生存期内是常量,而对于整个类而言却是可变的。同一类的不同的对象其const数据成员的值可以不同。
*常量成员的声明

      在该成员声明前加上const。如

      class  abc {

           const  int  x;

           …

           };

*不能在类声明中初始化const数据成员。

class A

  //错误,企图在类声明中初始化const数据成员

  const int SIZE = 200; 

  int array[SIZE];  //错误,未知的SIZE

};

 

const数据成员的初始化

*const数据成员的初始化只能在类构造函数的初始化表中进行,不能在构造函数中对它赋值。
*例:

  class A

  { 

  A(int size);  //构造函数

  const int SIZE;

  }

  A::A(int size) : SIZE(size)  //构造函数的初始化表

  {…}

  A a(100);  //对象a的SIZE的值为100

  A b(200);  //对象b的SIZE的值为200

 

静态数据成员

*静态数据成员不属于对象的一部分,而是类的一部分;
*静态数据成员的初始化不能放在类的构造函数中;
*类定义并不分配空间,空间是在定义对象时分配
*但静态数据成员属于类,因此定义对象时并不为静态成员分配空间
 
静态数据成员的定义
*为静态成员分配空间称为静态成员的定义
*静态成员的定义一般出现在类的实现文件中。如在SavingAccount类的实现文件中,必须要如下的定义:

     double SavingAccount::rate = 0.05;

     该定义为rate分配了空间,并给它赋了一个初值0.05。

*如果没有这个定义,连接器会报告一个错误
 
静态数据成员的使用
*可以通过作用域操作符从类直接调用。如: SavingAccount::rate
*但从每个对象的角度来看,它似乎又是对象的一部分,因此又可以从对象引用它。如有个SavingAccount类的对象obj,则可以用:obj.rate
*由于是整个类共享的,因此不管用哪种调用方式,得到的值都是相同的
 
静态成员函数
*成员函数也可以是静态的。静态的成员函数是为类的全体对象服务,而不是为某个类的特殊对象服务
*由于静态成员函数不需要借助任何对象就可以被调用,所以编译器不会为它暗加一个this指针。因此,静态成员函数无法处理类中的非静态成员变量。
*静态成员函数的声明只需要在类定义中的函数原型前加上保留词static。 
 
静态成员函数的用途
*定义静态成员函数的主要目的是访问静态的数据成员。
*如在SavingAccount类中,当利率发生变化时,必须修改这个静态数据成员的值。为此可以设置一个静态的成员函数

     static void SetRate(double newRate) {rate = newRate;} 

 

静态成员函数使用说明

*静态成员函数可定义为内嵌的,也可在类外定义。在类外定义时,不用static。
*静态成员函数的访问:可以通过类作用域限定符或通过对象访问

      类名::静态成员函数名()

      对象名.静态成员函数名()

 

静态的常量数据成员

*静态的常量数据成员:整个类的所有对象的共享常量
*静态的常量数据成员的声明:

static const  类型  数据成员名 = 常量表达式;

*注意const数据成员和static const数据成员的区别。 
 
老版本的兼容
*某些旧版本的C++不支持静态常量
*解决方法:用不带实例的枚举类型
*如:

class sample {

    enum {SIZE = 10};

    int storage[SIZE];

    …

};

创建功能更强的类型