C++中面向对象编程

转载自:

http://www.prglab.com/cms/pages/c-tutorial/oo-programming.php

 

4.1 类(Classes)

类(class)是一种将数据和函数组织在同一个结构里的逻辑方法。定义类的关键字为class ,其功能与C语言中的struct类似,不同之处是class可以包含函数,而不像struct只能包含数据元素。

类定义的形式是:

 

    class class_name {
        permission_label_1:
            member1;
        permission_label_2:
            member2;
        ...
    } object_name;
    

其 中 class_name 是类的名称 (用户自定义的类型) ,而可选项object_name 是一个或几个对象(object)标识。Class的声明体中包含成员members,成员可以是数据或函数定义,同时也可以包括允许范围标志 permission labels,范围标志可以是以下三个关键字中任意一个:private:, public: 或 protected:。它们分别代表以下含义:

  • private :class的private成员,只有同一个class的其他成员或该class的“friend” class可以访问这些成员。
  • protected :class的protected成员,只有同一个class的其他成员,或该class的“friend” class,或该class的子类(derived classes) 可以访问这些成员。
  • public :class的public成员,任何可以看到这个class的地方都可以访问这些成员。

如果我们在定义一个class成员的时候没有声明其允许范围,这些成员将被默认为 private范围。

例如:

 

    class CRectangle {

            int x, y;
        public:
            void set_values (int,int);
            int area (void);
    } rect;
    

上 面例子定义了一个class CRectangle 和该class类型的对象变量rect 。这个class 有4个成员:两个整型变量 (x 和 y) ,在private 部分 (因为private 是默认的允许范围);以及两个函数, 在 public 部分:set_values() 和 area(),这里只包含了函数的原型(prototype)。

注意 class名称与对象(object)名称的不同:在上面的例子中,CRectangle 是class 名称 (即用户定义的类型名称),而rect 是一个CRectangle类型的对象名称。它们的区别就像下面例子中类型名 int和 变量名a 的区别一样:

int a;

int 是class名称 (类型名) ,而a 是对象名 object name (变量)。

在程序中,我们可以通过使用对象名后面加一点再加成员名称(同使用C structs一样),来引用对象rect 的任何public成员,就像它们只是一般的函数或变量。例如:

rect.set_value (3,4);
myarea = rect.area();

但我们不能够引用 x 或 y ,因为它们是该class的 private 成员,它们只能够在该class的其它成员中被引用。晕了吗?下面是关于class CRectangle的一个复杂的例子:

    // classes example


    #include <iostream.h>
    class CRectangle {
            int x, y;
        public:
            void set_values (int,int);
            int area (void) {return (x*y);}
    };
    
    void CRectangle::set_values (int a, int b) {
        x = a;
        y = b;
    }
    
    int main () {
        CRectangle rect;
        rect.set_values (3,4);
        cout << "area: " << rect.area();
    }			
			
area: 12

上 面代码中新的东西是在定义函数set_values().使用的范围操作符(双冒号:: )。它是用来在一个class之外定义该class的成员。注意,我们在CRectangle class内部已经定义了函数area() 的具体操作,因为这个函数非常简单。而对函数set_values() ,在class内部只是定义了它的原型prototype,而其实现是在class之外定义的。这种在class之外定义其成员的情况必须使用范围操作 符::。

范围操作符 (::) 声明了被定义的成员所属的class名称,并赋予被定义成员适当的范围属性,这些范围属性与在class内部定义成员的属性是一样的。例如,在上面的例子 中,我们在函数set_values() 中引用了private变量x 和 y,这些变量只有在class内部和它的成员中才是可见的。

在class内部直接定义完整的函数,和只定义函数的原型而把具体实现放在class外部的唯一区别在于,在第一种情况中,编译器(compiler) 会自动将函数作为inline 考虑,而在第二种情况下,函数只是一般的class成员函数。

我 们把 x 和 y 定义为private 成员 (记住,如果没有特殊声明,所有class的成员均默认为private ),原因是我们已经定义了一个设置这些变量值的函数 (set_values()) ,这样一来,在程序的其它地方就没有办法直接访问它们。也许在一个这样简单的例子中,你无法看到这样保护两个变量有什么意义,但在比较复杂的程序中,这是 非常重要的,因为它使得变量不会被意外修改 (这里意外指的是从object的角度来讲的意外)。

使用class的一个更大的好处是我们可以用它来定义多个不同对象(object)。例如,接着上面class CRectangle的例子,除了对象rect之外,我们还可以定义对象rectb :

    // class example


    #include <iostream.h>
    
    class CRectangle {
            int x, y;
        public:
            void set_values (int,int);
            int area (void) {return (x*y);}
    };
    
    void CRectangle::set_values (int a, int b) {
        x = a;
        y = b;
    }
    
    int main () {
        CRectangle rect, rectb;
        rect.set_values (3,4);
        rectb.set_values (5,6);
        cout << "rect area: " << rect.area() << endl;
        cout << "rectb area: " << rectb.area() << endl;
    }
			
rect area: 12
rectb area: 30

注意 : 调用函数rect.area() 与调用rectb.area()所得到的结果是不一样的。这是因为每一个class CRectangle 的对象都拥有它自己的变量 x 和 y,以及它自己的函数set_value() 和 area()。

这 是基于对象( object) 和 面向对象编程 (object-oriented programming)的概念的。这个概念中,数据和函数是对象(object)的属性(properties),而不是像以前在结构化编程 (structured programming) 中所认为的对象(object)是函数参数。在本节及后面的小节中,我们将讨论面向对象编程的好处。

在这个具体的例子中,我们讨论的class (object的类型)是CRectangle,有两个实例(instance),或称对象(object):rect 和 rectb,每一个有它自己的成员变量和成员函数。

 

构造函数和析构函数 (Constructors and destructors)

对 象(object)在生成过程中通常需要初始化变量或分配动态内存,以便我们能够操作,或防止在执行过程中返回意外结果。例如,在前面的例子中,如果我们 在调用函数set_values( ) 之前就调用了函数area(),将会产生什么样的结果呢?可能会是一个不确定的值,因为成员x 和 y 还没有被赋于任何值。

为了避免这种情况发生,一个class 可以包含一个特殊的函数:构造函数 constructor,它可以通过声明一个与class同名的函数来定义。当且仅当要生成一个class的新的实例 (instance)的时候,也就是当且仅当声明一个新的对象,或给该class的一个对象分配内存的时候,这个构造函数将自动被调用。下面,我们将实现 包含一个构造函数的CRectangle :

    // class example


    #include <iostream.h>
    
    class CRectangle {
        int width, height;
      public:
        CRectangle (int,int);
        int area (void) {return (width*height);}
    };
    
    CRectangle::CRectangle (int a, int b) {
        width = a;
        height = b;
    }
    
    int main () {
        CRectangle rect (3,4);
        CRectangle rectb (5,6);
        cout << "rect area: " << rect.area() << endl;
        cout << "rectb area: " << rectb.area() << endl;
    }    
			
rect area: 12
rectb area: 30

正如你所看到的,这个例子的输出结果与前面一个没有区别。在这个例子中,我们只是把函数set_values换成了class的构造函数constructor。注意这里参数是如何在class实例 (instance)生成的时候传递给构造函数的:

CRectangle rect (3,4);
CRectangle rectb (5,6);

同时你可以看到,构造函数的原型和实现中都没有返回值(return value),也没有void 类型声明。构造函数必须这样写。一个构造函数永远没有返回值,也不用声明void,就像我们在前面的例子中看到的。

析 构函数Destructor 完成相反的功能。它在objects被从内存中释放的时候被自动调用。释放可能是因为它存在的范围已经结束了(例如,如果object被定义为一个函数内 的本地(local)对象变量,而该函数结束了);或者是因为它是一个动态分配的对象,而被使用操作符delete释放了。

析构函数必须与class同名,加水波号tilde (~) 前缀,必须无返回值。

析构函数特别适用于当一个对象被动态分别内存空间,而在对象被销毁的时我们希望释放它所占用的空间的时候。例如:

    // example on constructors and destructors


    #include <iostream.h>
    
    class CRectangle {
        int *width, *height;
      public:
        CRectangle (int,int);
        ~CRectangle ();
        int area (void) {return (*width * *height);}
    };
    
    CRectangle::CRectangle (int a, int b) {
        width = new int;
        height = new int;
        *width = a;
        *height = b;
    }
    
    CRectangle::~CRectangle () {
        delete width;
        delete height;
    }
    
    int main () {
        CRectangle rect (3,4), rectb (5,6);
        cout << "rect area: " << rect.area() << endl;
        cout << "rectb area: " << rectb.area() << endl;
        return 0;
    }
			
rect area: 12
rectb area: 30

构造函数重载(Overloading Constructors)

像 其它函数一样,一个构造函数也可以被多次重载(overload)为同样名字的函数,但有不同的参数类型和个数。记住,编译器会调用与在调用时刻要求的参 数类型和个数一样的那个函数(Section 2.3, Functions-II)。在这里则是调用与类对象被声明时一样的那个构造函数。

实际上,当我们定义一个class而没有明确定义构造函数的时候,编译器会自动假设两个重载的构造函数 (默认构造函数"default constructor" 和复制构造函数"copy constructor")。例如,对以下class:

 

   class CExample {
     public:
       int a,b,c;
       void multiply (int n, int m) { a=n; b=m; c=a*b; };
   };
   

没有定义构造函数,编译器自动假设它有以下constructor 成员函数:

  • Empty constructor

    它是一个没有任何参数的构造函数,被定义为nop (没有语句)。它什么都不做。

    CExample::CExample () { };
  • Copy constructor

    它是一个只有一个参数的构造函数,该参数是这个class的一个对象,这个函数的功能是将被传入的对象(object)的所有非静态(non-static)成员变量的值都复制给自身这个object。

       CExample::CExample (const CExample& rv) {
           a=rv.a;  b=rv.b;  c=rv.c;
       }
       

必须注意: 这 两个默认构造函数(empty construction 和 copy constructor )只有在没有其它构造函数被明确定义的情况下才存在。如果任何其它有任意参数的构造函数被定义了,这两个构造函数就都不存在了。在这种情况下,如果你想要 有empty construction 和 copy constructor ,就必需要自己定义它们。

当然,如果你也可以重载class的构造函数,定义有不同的参数或完全没有参数的构造函数,见如下例子:

    // overloading class constructors


    #include <iostream.h>
    
    Class CRectangle {
        int width, height;
      public:
        CRectangle ();
        CRectangle (int,int);
        int area (void) {return (width*height);}
    };
    
    CRectangle::CRectangle () {
        width = 5;
        height = 5;
    }
    
    CRectangle::CRectangle (int a, int b) {
        width = a;
        height = b;
    }
    
    int main () {
        CRectangle rect (3,4);
        CRectangle rectb;
        cout << "rect area: " << rect.area() << endl;
        cout << "rectb area: " << rectb.area() << endl;
    }
			
rect area: 12
rectb area: 25

在上面的例子中,rectb 被声明的时候没有参数,所以它被使用没有参数的构造函数进行初始化,也就是width 和height 都被赋值为5。

注意 在我们声明一个新的object的时候,如果不想传入参数,则不需要写括号():

CRectangle rectb; // right
CRectangle rectb(); // wrong!

类的指针(Pointers to classes)

类也是可以有指针的,要定义类的指针,我们只需要认识到,类一旦被定义就成为一种有效的数据类型,因此只需要用类的名字作为指针的名字就可以了。例如:

CRectangle * prect;

是一个指向class CRectangle类型的对象的指针。

就像数据机构中的情况一样,要想直接引用一个由指针指向的对象(object)中的成员,需要使用操作符 ->。这里是一个例子,显示了几种可能出现的情况:

    // pointer to classes example


    #include <iostream.h>
    
    class CRectangle {
        int width, height;
      public:
        void set_values (int, int);
        int area (void) {return (width * height);}
    };
    
    void CRectangle::set_values (int a, int b) {
        width = a;
        height = b;
    }
    
    int main () {
        CRectangle a, *b, *c;
        CRectangle * d = new CRectangle[2];
        b= new CRectangle;
        c= &a;
        a.set_values (1,2);
        b->set_values (3,4);
        d->set_values (5,6);
        d[1].set_values (7,8);
        cout << "a area: " << a.area() << endl;
        cout << "*b area: " << b->area() << endl;
        cout << "*c area: " << c->area() << endl;
        cout << "d[0] area: " << d[0].area() << endl;
        cout << "d[1] area: " << d[1].area() << endl;
        return 0;
    }
			
a area: 2
*b area: 12
*c area: 2
d[0] area: 30
d[1] area: 56

以下是怎样读前面例子中出现的一些指针和类操作符 (*, &, ., ->, [ ]):

  • *x 读作: pointed by x (由x指向的)
  • &x 读作: address of x(x的地址)
  • x.y 读作: member y of object x (对象x的成员y)
  • (*x).y 读作: member y of object pointed by x(由x指向的对象的成员y)
  • x->y 读作: member y of object pointed by x (同上一个等价)
  • x[0] 读作: first object pointed by x(由x指向的第一个对象)
  • x[1] 读作: second object pointed by x(由x指向的第二个对象)
  • x[n] 读作: (n+1)th object pointed by x(由x指向的第n+1个对象)

在继续向下阅读之前,一定要确定你明白所有这些的逻辑含义。如果你还有疑问,再读一遍这一笑节,或者同时参考 小节 "3.3, 指针(Pointers)" 和 "3.5, 数据结构(Structures)".

 

由关键字struct和union定义的类

类不仅可以用关键字class来定义,也可以用struct或union来定义。

因 为在C++中类和数据结构的概念太相似了,所以这两个关键字struct和class的作用几乎是一样的(也就是说在C++中struct定义的类也可以 有成员函数,而不仅仅有数据成员)。两者定义的类的唯一区别在于由class定义的类所有成员的默认访问权限为private,而struct定义的类所 有成员默认访问权限为public。除此之外,两个关键字的作用是相同的。

union的概念与struct和class定义的类不同, 因为union在同一时间只能存储一个数据成员。但是由union定义的类也是可以有成员函数的。union定义的类访问权限默认为public。

 

 

4.2 操作符重载(Overloading operators)

C++ 实现了在类(class)之间使用语言标准操作符,而不只是在基本数据类型之间使用。例如:

int a, b, c;
a = b + c;

是有效操作,因为加号两边的变量都是基本数据类型。然而,我们是否可以进行下面的操作就不是那么显而易见了(它实际上是正确的):

struct { char product [50]; float price; } a, b, c;
a = b + c;

将一个类class (或结构struct)的对象赋给另一个同种类型的对象是允许的(通过使用默认的复制构造函数 copy constructor)。但相加操作就有可能产生错误,理论上讲它在非基本数据类型之间是无效的。

但归功于C++ 的操作符重载(overload)能力,我们可以完成这个操作。像以上例子中这样的组合类型的对象在C++中可以接受如果没有操作符重载则不能被接受的操作,我们甚至可以修改这些操作符的效果。以下是所有可以被重载的操作符的列表:

 

	+    -    *    /    =    <    >    +=   -=   *=   /=   <<   >>
	<<=  >>=  ==   !=   <=   >=   ++   --   %    &    ^    !    |
	~    &=   ^=   |=   &&   ||   %=   []   ()   new  delete
	

要想重载一个操作符,我们只需要编写一个成员函数,名为operator ,后面跟我们要重载的操作符,遵循以下原型定义:

type operator sign (parameters);

这 里是一个操作符 +的例子。我们要计算二维向量(bidimensional vector) a(3,1) 与b(1,2)的和。两个二维向量相加的操作很简单,就是将两个x 轴的值相加获得结果的x 轴值,将两个 y 轴值相加获得结果的 y值。在这个例子里,结果是 (3+1,1+2) = (4,3)。

    // vectors: overloading operators example

    #include <iostream.h>
    
    class CVector {
      public:
        int x,y;
        CVector () {};
        CVector (int,int);
        CVector operator + (CVector);
    };
    
    CVector::CVector (int a, int b) {
        x = a;
        y = b;
    }
    
    CVector CVector::operator+ (CVector param) {
        CVector temp;
        temp.x = x + param.x;
        temp.y = y + param.y;
        return (temp);
    }
    
    int main () {
        CVector a (3,1);
        CVector b (1,2);
        CVector c;
        c = a + b;
        cout << c.x << "," << c.y;
        return 0;
    }    
			
4,3

如果你迷惑为什么看到这么多遍的 CVector,那是因为其中有些是指class名称CVector ,而另一些是以它命名的函数名称,不要把它们搞混了:

 

   CVector (int, int);            // 函数名称 CVector (constructor)

   CVector operator+ (CVector);   // 函数 operator+ 返回CVector 类型的值

   

Class CVector的函数 operator+ 是对数学操作符+进行重载的函数。这个函数可以用以下两种方法进行调用:

c = a + b;
c = a.operator+ (b);

注意: 我们在这个例子中包括了一个空构造函数 (无参数),而且我们将它定义为无任何操作:

CVector ( ) { };

这是很必要的,因为例子中已经有另一个构造函数,

CVector (int, int);

因此,如果我们不像上面这样明确定义一个的话,CVector的两个默认构造函数都不存在。

这样的话,main( )中包含的语句

CVector c;

将为不合法的。

尽 管如此,我已经警告过一个空语句块 (no-op block)并不是一种值得推荐的构造函数的实现方式,因为它不能实现一个构造函数至少应该完成的基本功能,也就是初始化class中的所有变量。在我们 的例子中,这个构造函数没有完成对变量x 和 y 的定义。因此一个更值得推荐的构造函数定义应该像下面这样:

CVector ( ) { x=0; y=0; };

就 像一个class默认包含一个空构造函数和一个复制构造函数一样,它同时包含一个对赋值操作符assignation operator (=)的默认定义,该操作符用于两个同类对象之间。这个操作符将其参数对象(符号右边的对象) 的所有非静态 (non-static) 数据成员复制给其左边的对象。当然,你也可以将它重新定义为你想要的任何功能,例如,只拷贝某些特定class成员。

重载一个操作符并不要求保持其常规的数学含义,虽然这是推荐的。例如,虽然我们可以将操作符+定义为取两个对象的差值,或用==操作符将一个对象赋为0,但这样做是没有什么逻辑意义的。

虽然函数operator+ 的原型定义看起来很明显,因为它取操作符右边的对象为其左边对象的函数operator+的参数,其它的操作符就不一定这么明显了。以下列表总结了不同的操作符函数是怎样定义声明的 (用操作符替换每个@):

ExpressionOperator (@)Function memberGlobal function
@a+ - * & ! ~ ++ --A::operator@( )operator@(A)
a@++ --A::operator@(int)operator@(A, int)
a@b+ - * / % ^ & | < > == != <= >= << >> && || ,A::operator@(B)operator@(A, B)
a@b= += -= *= /= %= ^= &= |= <<= >>= [ ]A::operator@(B)-
a(b, c...)( )A::operator()(B, C...)-
a->b->A::operator->()-

* 这里a 是class A的一个对象,b 是 B 的一个对象,c 是class C 的一个对象。

从 上表可以看出有两种方法重载一些class操作符:作为成员函数(member function)或作为全域函数(global function)。它们的用法没有区别,但是我要提醒你,如果不是class的成员函数,则不能访问该class的private 或 protected 成员,除非这个全域函数是该class的 friend (friend 的含义将在后面的章节解释)。

 

关键字 this

关键字this 通常被用在一个class内部,指正在被执行的该class的对象(object)在内存中的地址。它是一个指针,其值永远是自身object的地址。

它可以被用来检查传入一个对象的成员函数的参数是否是该对象本身。例如:

    // this

    #include <iostream.h>
    
    class CDummy {
      public:
        int isitme (CDummy& param);
    };
    
    int CDummy::isitme (CDummy& param) {
        if (&param == this) return 1;
        else return 0;
    }
    
    int main () {
        CDummy a;
        CDummy* b = &a;
        if ( b->isitme(a) )
            cout << "yes, &a is b";
        return 0;
    }
			
yes, &a is b

它还经常被用在成员函数operator= 中,用来返回对象的指针(避免使用临时对象)。以下用前面看到的向量(vector)的例子来看一下函数operator= 是怎样实现的:

   CVector& CVector::operator= (const CVector& param) {
       x=param.x;
       y=param.y;
       return *this;
   }

实际上,如果我们没有定义成员函数operator=,编译器自动为该class生成的默认代码有可能就是这个样子的。

 

静态成员(Static members)

一个class 可以包含静态成员(static members),可以是数据,也可以是函数。

一个class的静态数据成员也被称作类变量"class variables",因为它们的内容不依赖于某个对象,对同一个class的所有object具有相同的值。

例如,它可以被用作计算一个class声明的objects的个数,见以下代码程序:

    // static members in classes

    #include <iostream.h>
    
    class CDummy {
      public:
        static int n;
        CDummy () { n++; };
        ~CDummy () { n--; };
    };
    
    int CDummy::n=0;
    
    int main () {
        CDummy a;
        CDummy b[5];
        CDummy * c = new CDummy;
        cout << a.n << endl;
        delete c;
        cout << CDummy::n << endl;
        return 0;
    } 
			
7
6

实 际上,静态成员与全域变量(global variable)具有相同的属性,但它享有类(class)的范围。因此,根据ANSI-C++ 标准,为了避免它们被多次重复声明,在class的声明中只能够包括static member的原型(声明),而不能够包括其定义(初始化操作)。为了初始化一个静态数据成员,我们必须在class之外(在全域范围内),包括一个正式 的定义,就像上面例子中做法一样。

因为它对同一个class的所有object是同一个值,所以它可以被作为该class的任何object的成员所引用,或者直接被作为class的成员引用(当然这只适用于static 成员):

cout << a.n;
cout << CDummy::n;

以上两个调用都指同一个变量:class CDummy里的static 变量 n 。

在提醒一次,它其实是一个全域变量。唯一的不同是它的名字跟在class的后面。

就 像我们会在class中包含static数据一样,我们也可以使它包含static 函数。它们表示相同的含义:static函数是全域函数(global functions),但是像一个指定class的对象成员一样被调用。它们只能够引用static 数据,永远不能引用class的非静态(nonstatic)成员。它们也不能够使用关键字this,因为this实际引用了一个对象指针,但这些 static函数却不是任何object的成员,而是class的直接成员。

 

 

4.3 类之间的关系(Relationships between classes)

友元函数(Friend functions)

在 前面的章节中我们已经看到了对class的不同成员存在3个层次的内部保护:public, protected 和 private。在成员为 protected 和 private的情况下,它们不能够被从所在的class以外的部分引用。然而,这个规则可以通过在一个class中使用关键字friend来绕过,这样 我们可以允许一个外部函数获得访问class的protected 和 private 成员的能力。

为了实现允许一个外部函数访问 class的private 和 protected 成员,我们必须在class内部用关键字friend来声明该外部函数的原型,以指定允许该函数共享class的成员。在下面的例子中我们声明了一个 friend 函数 duplicate:

    // friend functions

    #include <iostream.h>
    
    class CRectangle {
        int width, height;
      public:
        void set_values (int, int);
        int area (void) {return (width * height);}
        friend CRectangle duplicate (CRectangle);
    };
    
    void CRectangle::set_values (int a, int b) {
        width = a;
        height = b;
    }
    
    CRectangle duplicate (CRectangle rectparam) {
        CRectangle rectres;
        rectres.width = rectparam.width*2;
        rectres.height = rectparam.height*2;
        return (rectres);
    }
    
    int main () {
        CRectangle rect, rectb;
        rect.set_values (2,3);
        rectb = duplicate (rect);
        cout << rectb.area();
    }
			
24

函 数duplicate是CRectangle的friend,因此在该函数之内,我们可以访问CRectangle 类型的各个object的成员 width 和 height。注意,在 duplicate()的声明中,及其在后面main()里被调用的时候,我们并没有把duplicate 当作class CRectangle的成员,它不是。

friend 函数可以被用来实现两个不同class之间的操作。广义来说,使用friend 函数是面向对象编程之外的方法,因此,如果可能,应尽量使用class的成员函数来完成这些操作。比如在以上的例子中,将函数duplicate() 集成在class CRectangle 可以使程序更短。

 

友元类 (Friend classes)

就像我们可以定义一个friend 函数,我们也可以定义一个class是另一个的friend,以便允许第二个class访问第一个class的 protected 和 private 成员。

    // friend class

    #include <iostream.h>
    
    class CSquare;

    class CRectangle {
        int width, height;
      public:
        int area (void) {return (width * height);}
        void convert (CSquare a);
    };
    
    Class CSquare {
      private:
        int side;
      public:
        void set_side (int a){side=a;}
        friend class CRectangle;
    };
    
    void CRectangle::convert (CSquare a) {
        width = a.side;
        height = a.side;
    }
    
    int main () {
        CSquare sqr;
        CRectangle rect;
        sqr.set_side(4);
        rect.convert(sqr);
        cout << rect.area();
        return 0;
    }
			
16

在这个例子中,我们声明了CRectangle 是CSquare 的friend,因此CRectangle可以访问CSquare 的protected 和 private 成员,更具体地说,可以访问CSquare::side,它定义了正方形的边长。

在 上面程序的第一个语句里你可能也看到了一些新的东西,就是class CSquare空原型。这是必需的,因为在CRectangle 的声明中我们引用了CSquare (作为convert()的参数)。CSquare 的定义在CRectangle的后面,因此如果我们没有在这个class之前包含一个CSquare 的声明,它在CRectangle中就是不可见的。

这 里要考虑到,如果没有特别指明,友元关系(friendships)并不是相互的。在我们的CSquare 例子中,CRectangle 是一个friend类,但因为CRectangle 并没有对CSquare作相应的声明,因此CRectangle 可以访问CSquare 的 protected 和private 成员,但反过来并不行,除非我们将 CSquare 也定义为CRectangle的 friend。

 

类之间的继承(Inheritance between classes)

类 的一个重要特征是继承,这使得我们可以基于一个类生成另一个类的对象,以便使后者拥有前者的某些成员,再加上它自己的一些成员。例如,假设我们要声明一系 列类型的多边形,比如长方形CRectangle或三角形CTriangle。它们有一些共同的特征,比如都可以只用两条边来描述:高(height)和 底(base)。

这个特点可以用一个类CPolygon 来表示,基于这个类我们可以引申出上面提到的两个类CRectangle 和 CTriangle 。

类CPolygon 包含所有多边形共有的成员。在我们的例子里就是: width 和 height。而CRectangle 和 CTriangle 将为它的子类(derived classes)。

由其它类引申而来的子类继承基类的所有可视成员,意思是说,如果一个基类包含成员A ,而我们将它引申为另一个包含成员B的类,则这个子类将同时包含 A 和 B。

要定义一个类的子类,我们必须在子类的声明中使用冒号(colon)操作符: ,如下所示:

class derived_class_name: public base_class_name;

这 里derived_class_name 为子类(derived class)名称,base_class_name 为基类(base class)名称。public 也可以根据需要换为protected 或 private,描述了被继承的成员的访问权限,我们在以下例子后会很快看到:

    // derived classes

    #include <iostream.h>
    
    Class CPolygon {
      protected:
        int width, height;
      public:
        void set_values (int a, int b) { width=a; height=b;}
    };
    
    class CRectangle: public CPolygon {
      public:
        int area (void){ return (width * height); }
    };
    
    class CTriangle: public CPolygon {
      public:
        int area (void){ return (width * height / 2); }
    };
    
    int main () {
        CRectangle rect;
        CTriangle trgl;
        rect.set_values (4,5);
        trgl.set_values (4,5);
        cout << rect.area() << endl;
        cout << trgl.area() << endl;
        return 0;
    }
			
20
10

如上所示,类 CRectangle 和 CTriangle 的每一个对象都包含CPolygon的成员,即: width, height 和 set_values()。

标 识符protected 与 private类似,它们的唯一区别在继承时才表现出来。当定义一个子类的时候,基类的protected 成员可以被子类的其它成员所使用,然而private 成员就不可以。因为我们希望CPolygon的成员width 和 height能够被子类CRectangle 和 CTriangle 的成员所访问,而不只是被CPolygon自身的成员操作,我们使用了protected 访问权限,而不是 private。

下表按照谁能访问总结了不同访问权限类型:

可以访问publicprotectedprivate
本class的成员yesyesyes
子类的成员yesyes no
非成员yes no no

这里"非成员"指从class以外的任何地方引用,例如从main()中,从其它的class中或从全域(global)或本地(local)的任何函数中。

在我们的例子中,CRectangle 和CTriangle 继承的成员与基类CPolygon拥有同样的访问限制:

   CPolygon::width           // protected access

   CRectangle::width         // protected access

   CPolygon::set_values()    // public access

   CRectangle::set_values()  // public access

   

这是因为我们在继承的时候使用的是public,记得我们用的是:

class CRectangle: public CPolygon;

这 里关键字 public 表示新的类(CRectangle)从基类(CPolygon)所继承的成员必须获得最低程度保护。这种被继承成员的访问限制的最低程度可以通过使用 protected 或 private而不是public来改变。例如,daughter 是mother 的一个子类,我们可以这样定义:

class daughter: protected mother;

这 将使得protected 成为daughter 从mother处继承的成员的最低访问限制。也就是说,原来mother 中的所有public 成员到daughter 中将会成为protected 成员,这是它们能够被继承的最低访问限制。当然这并不是限制daughter 不能有它自己的public 成员。最低访问权限限制只是建立在从mother中 继承的成员上的。

最常用的继承限制除了public 外就是private ,它被用来将基类完全封装起来,因为在这种情况下,除了子类自身外,其它任何程序都不能访问那些从基类继承而来的成员。不过大多数情况下继承都是使用public的。

如果没有明确写出访问限制,所有由关键字class 生成的类被默认为private ,而所有由关键字struct 生成的类被默认为public。

 

什么是从基类中继承的? (What is inherited from the base class?)

理论上说,子类(drived class)继承了基类(base class)的所有成员,除了:

  • 构造函数Constructor 和析构函数destructor
  • operator=() 成员
  • friends

虽然基类的构造函数和析构函数没有被继承,但是当一个子类的object被生成或销毁的时候,其基类的默认构造函数 (即,没有任何参数的构造函数)和析构函数总是被自动调用的。

如果基类没有默认构造函数,或你希望当子类生成新的object时,基类的某个重载的构造函数被调用,你需要在子类的每一个构造函数的定义中指定它:

derived_class_name (parameters) : base_class_name (parameters) {}

例如 (注意程序中黑体的部分):

    // constructors and derivated classes

    #include <iostream.h>
    
    class mother {
      public:
        mother ()
          { cout << "mother: no parameters\n"; }
        mother (int a)
          { cout << "mother: int parameter\n"; }
    };
    
    class daughter : public mother {
      public:
        daughter (int a)
          { cout << "daughter: int parameter\n\n"; }
    };
    
    class son : public mother {
      public:
        son (int a) : mother (a)
          { cout << "son: int parameter\n\n"; }
    };
    
    int main () {
        daughter cynthia (1);
        son daniel(1);
        return 0;
    }
			
mother: no parameters
daughter: int parameter

mother: int parameter
son: int parameter

观察当一个新的daughter object生成的时候mother的哪一个构造函数被调用了,而当新的son object生成的时候,又是哪一个被调用了。不同的构造函数被调用是因为daughter 和 son的构造函数的定义不同:

   daughter (int a)          // 没有特别制定:调用默认constructor

   son (int a) : mother (a)  // 指定了constructor: 调用被指定的构造函数

   

 

多重继承(Multiple inheritance)

在C++ 中,一个class可以从多个class中继承属性或函数,只需要在子类的声明中用逗号将不同基类分开就可以了。例如,如果我们有一个特殊的class COutput 可以实现向屏幕打印的功能,我们同时希望我们的类CRectangle 和 CTriangle 在CPolygon 之外还继承一些其它的成员,我们可以这样写:

class CRectangle: public CPolygon, public COutput {
class CTriangle: public CPolygon, public COutput {

以下是一个完整的例子:

    // multiple inheritance

    #include <iostream.h>
    
    class CPolygon {
      protected:
        int width, height;
      public:
        void set_values (int a, int b)
          { width=a; height=b;}
    };
    
    class COutput {
      public:
        void output (int i);
    };
    
    void COutput::output (int i) {
        cout << i << endl;
    }
    
    class CRectangle: public CPolygon, public COutput {
      public:
        int area (void)
          { return (width * height); }
    };
    
    class CTriangle: public CPolygon, public COutput {
      public:
        int area (void)
          { return (width * height / 2); }
    };
    
    int main () {
        CRectangle rect;
        CTriangle trgl;
        rect.set_values (4,5);
        trgl.set_values (4,5);
        rect.output (rect.area());
        trgl.output (trgl.area());
        return 0;
    }
			
20
10

 

 

 

4.4 多态 (Polymorphism)

为了能更好的理解本节内容,你需要清楚的知道怎样使用指针pointers 和类之间的继承 inheritance between classes。建议如果你觉得以下这些表达式比较生疏的的话, 请复习指定的章节:

int a::b(c) {};    // 类Classes (Section 4.1)

a->b               // 指针和对象pointers and objects (Section 4.2)

class a: public b; // 类之间的关系Relationships between classes (Section 4.3)

 

基类的指针(Pointers to base class)

继承的好处之一是一个指向子类(derived class)的指针与一个指向基类(base class)的指针是type-compatible的。 本节就是重点介绍如何利用C++的这一重要特性。例如,我们将结合C++的这个功能,重写前面小节中关于长方形rectangle 和三角形 triangle 的程序:

// pointers to base class
#include <iostream.h>
class CPolygon {
protected:
    int width, height;
public:
    void set_values (int a, int b) {
        width=a; height=b;
    }
};
class CRectangle: public CPolygon {
public:
    int area (void) {
	return (width * height);
    }
};
class CTriangle: public CPolygon {
public:
    int area (void) {
	return (width * height / 2);
    }
};
int main () {
    CRectangle rect;
    CTriangle trgl;
    CPolygon * ppoly1 = &rect;
    CPolygon * ppoly2 = &trgl;
    ppoly1->set_values (4,5);
    ppoly2->set_values (4,5);
    cout << rect.area() << endl;
    cout << trgl.area() << endl;
    return 0;
}
20
10

在主函数 main 中定义了两个指向class CPolygon的对象的指针,即 *ppoly1 和 *ppoly2。 它们被赋值为rect 和 trgl的地址,因为rect 和 trgl是CPolygon 的子类的对象,因此这种赋值是有效的。

使用*ppoly1 和 *ppoly2 取代rect 和trgl 的唯一限制是*ppoly1 和 *ppoly2 是CPolygon* 类型的,因此我们只能够引用CRectangle 和 CTriangle 从基类CPolygon中继承的成员。正是由于这个原因,我们不能够使用*ppoly1 和 *ppoly2 来调用成员函数 area(),而只能使用rect 和 trgl来调用这个函数。

要想使CPolygon 的指针承认area()为合法成员函数,必须在基类中声明它,而不能只在子类进行声明(见下一小节)。

 

虚拟成员(Virtual members)

如果想在基类中定义一个成员留待子类中进行细化,我们必须在它前面加关键字virtual ,以便可以使用指针对指向相应的对象进行操作。

请看一下例子:

// virtual members
#include <iostream.h>
class CPolygon {
    protected:
        int width, height;
    public:
        void set_values (int a, int b)  {
            width=a;
            height=b;
        }
        virtual int area (void) { return (0); }
};
class CRectangle: public CPolygon {
    public:
        int area (void) { return (width * height); }
};
class CTriangle: public CPolygon {
    public:
        int area (void) {
            return (width * height / 2);
        }
};
int main () {
    CRectangle rect;
    CTriangle trgl;
    CPolygon poly;
    CPolygon * ppoly1 = &rect;
    CPolygon * ppoly2 = &trgl;
    CPolygon * ppoly3 = &poly;
    ppoly1->set_values (4,5);
    ppoly2->set_values (4,5);
    ppoly3->set_values (4,5);
    cout << ppoly1->area() << endl;
    cout << ppoly2->area() << endl;
    cout << ppoly3->area() << endl;
    return 0;
}
20
10
0

现在这三个类(CPolygon, CRectangle 和 CTriangle) 都有同样的成员:width, height, set_values() 和 area()。

area() 被定义为virtual 是因为它后来在子类中被细化了。你可以做一个试验,如果在代码种去掉这个关键字(virtual),然后再执行这个程序,三个多边形的面积计算结果都将是 0 而不是20,10,0。这是因为没有了关键字virtual ,程序执行不再根据实际对象的不用而调用相应area() 函数(即分别为CRectangle::area(), CTriangle::area() 和 CPolygon::area()),取而代之,程序将全部调用CPolygon::area(),因为这些调用是通过CPolygon类型的指针进行 的。

因此,关键字virtual 的作用就是在当使用基类的指针的时候,使子类中与基类同名的成员在适当的时候被调用,如前面例子中所示。

注意 ,虽然本身被定义为虚拟类型,我们还是可以声明一个CPolygon 类型的对象并调用它的area() 函数,它将返回0 ,如前面例子结果所示。

 

抽象基类(Abstract base classes)

基本的抽象类与我们前面例子中的类CPolygon 非常相似,唯一的区别是在我们前面的例子中,我们已经为类CPolygon的对象(例如对象poly)定义了一个有效地area()函数,而在一个抽象类 (abstract base class)中,我们可以对它不定义,而简单得在函数声明后面写 =0 (等于0)。

类CPolygon 可以写成这样:

// abstract class CPolygon
class CPolygon {
    protected:
        int width, height;
    public:
        void set_values (int a, int b) {
            width=a;
            height=b;
        }
        virtual int area (void) =0;
};

注意我们是如何在virtual int area (void)加 =0 来代替函数的具体实现的。这种函数被称为纯虚拟函数(pure virtual function),而所有包含纯虚拟函数的类被称为抽象基类(abstract base classes)。

抽象基类的最大不同是它不能够有实例(对象),但我们可以定义指向它的指针。因此,像这样的声明:

CPolygon poly;

对于前面定义的抽象基类是不合法的。

然而,指针:

CPolygon * ppoly1;
CPolygon * ppoly2

是完全合法的。这是因为该类包含的纯虚拟函数(pure virtual function) 是没有被实现的,而又不可能生成一个不包含它的所有成员定义的对象。然而,因为这个函数在其子类中被完整的定义了,所以生成一个指向其子类的对象的指针是完全合法的。

下面是完整的例子:

// virtual members
#include <iostream.h>
class CPolygon {
    protected:
        int width, height;
    public:
        void set_values (int a, int b) {
            width=a;
            height=b;
        }
        virtual int area (void) =0;
};
class CRectangle: public CPolygon {
    public:
        int area (void) { return (width * height); }
};
class CTriangle: public CPolygon {
    public:
        int area (void) {
            return (width * height / 2);
        }
};
int main () {
    CRectangle rect;
    CTriangle trgl;
    CPolygon * ppoly1 = &rect;
    CPolygon * ppoly2 = &trgl;
    ppoly1->set_values (4,5);
    ppoly2->set_values (4,5);
    cout << ppoly1->area() << endl;
    cout << ppoly2->area() << endl;
    return 0;
}
20
10

再看一遍这段程序,你会发现我们可以用同一种类型的指针(CPolygon*)指向不同类的对象,至一点非常有用。 想象一下,现在我们可以写一个CPolygon 的成员函数,使得它可以将函数area()的结果打印到屏幕上,而不必考虑具体是为哪一个子类。

// virtual members
#include <iostream.h>
class CPolygon {
    protected:
        int width, height;
    public:
        void set_values (int a, int b) {
            width=a;
            height=b;
        }
        virtual int area (void) =0;
        void printarea (void) {
            cout << this->area() << endl;
        }
};
class CRectangle: public CPolygon {
    public:
        int area (void) { return (width * height); }
};
class CTriangle: public CPolygon {
    public:
        int area (void) {
            return (width * height / 2);
        }
};
int main () {
    CRectangle rect;
    CTriangle trgl;
    CPolygon * ppoly1 = &rect;
    CPolygon * ppoly2 = &trgl;
    ppoly1->set_values (4,5);
    ppoly2->set_values (4,5);
    ppoly1->printarea();
    ppoly2->printarea();
    return 0;
}
20
10

记住,this 代表代码正在被执行的这一个对象的指针。

抽象类和虚拟成员赋予了C++ 多态(polymorphic)的特征,使得面向对象的编程object-oriented programming成为一个有用的工具。这里只是展示了这些功能最简单的用途。想象一下如果在对象数组或动态分配的对象上使用这些功能,将会节省多少 麻烦。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值