ITEEDU

9.3 protected成员

基类的public成员能够被程序中所有函数访问,private成员只能被基类的成员函数和友元访问。protected访问是public访问和private访问之间的中间层次。基类的protected成员只能被基类的成员和友元以及派生类的成员和友元访问。派生类成员简单地使用成员名就可以引用基类的public成员和protected成员。注意protected数据破坏了封装,基类protected成员改变时,所有派生类都要修改。

软件工程视点9. 1

一般来说,声明private类的数据成员和使用Protected方式只能是系统要满足特定性能要求时的“最后一招”。

9.4 把基类指针强制转换为派生类指针

公有派生类的对象可作为其相应基类的对象处理,这使得一些有意义的操作成为可能。例如,从某个特定基类派生出来的各种类,尽管这些类的对象彼此之间互不相同,但是仍然能够建立这些对象的链表,只要把这些对象作为基类对象处理就可以了。然而反过来是不行的,基类的对象不能自动成为派生类的对象。

常见编程错误9.1

将基类对象作为派生类对象处理。

程序员可以用显式类型转换把基类指针强制转换为派生类指针。但是,如果要复引用该指针,那么在转换前首先应该把它指向某个派生类的对象,这一点要小心。本节采用大多数编译器中常用的方法。第21章将介绍符合ANSI/ISO C++草案标准的最新编译器的新特性,包括运行时类型信息(RTFI)、dynamic_cast和typeid。

常见编程错误9.2

把指向基类对象的指针显式地强制转换为派生类指针,然后引用该对象中并不存在的派生类的成更会导

致运行时的逻辑错误。

第一个例子见图9.4。第1行到第39行是类Point的定义和其成员函数的定义,第40行到第94行是类Circle的定义和其成员函数的定义,第95行到第132行是类的驱动程序,该程序演示了如何把派生类指针赋给基类指针和如何把基类指针强制转换为派生类指针。余下的部分是程序输出。

首先看一下类Point的定义。Point的public接口包含成员函数setPoint、getX和getY。Point的数据成员x和y指定为protected,从而在防止了Point对象的用户直接访问这些数据的同时,又能够让派生类直接访问继承来的数据成员。如果将数据成员指定为private,那么就要用Point的public成员函数甚至派生类来访问这些数据。注意,由于重载的流插入运算符函数是类Point的友元,所以Point重载流插入函数能够直接引用变量x和y。因为重载的流插入运算符函数不是类Point的成员函数,所以需要通过对象来引用变量x和y(即p.x和p.y)。注意这个类提供内联的publiic成员函数getX

和getY,因此operator<<不必成为友元就可以达到良好性能。但所需public成员函数不一定在每个类的public接口中提供,因此最好还是建立友元。

 // Fig. 9.4: point.h
 // Definition of class Point
 #ifndef POINT H
 #define POINT H
 class Point {
   friend ostream &operator<<( ostream &, const Point & );
 public:
    Point( int = 0, int = 0 );     // default constructor
   void setPoint( int, int );     // set coordinates
   int getX() const { return x; }  // get x coordinate
   int getY() const/{/ return y; }  // get y coordinate
 protected:          accessible by derived classes
   int x, y;      // x and y coordinates of the Point
 };
 #endif
 // Fig. 9.4: point.cpp
 // Member functions for class Point
 #include < iostream.h>
 #include "point.h"
 // Constructor for class Point
 Point::Point( int a, int b ) { setPoint( a, b ); }
 // Set x and y coordinates of Point
 void Point::setPoint{ int a, int b )
 {
   y = b;
 }
 // output Point (with overloaded stream insertion operator)
 ostream &operator<<{ ostream &output, const Point &p )
 {
   output << '[' << p.x << ", "<< p.y << ']';
   return output;  // enables cascaded calls
 }
 // Fig. 9.4: circle.h
 // Definition of class Circle
 #ifndef CIRCLE_H
 #define CIRCLE_H
 #include < iostream.h>
 #include < iomanip.h>
 #include "point.h"
 class Circle : public Point {  // Circle inherits from Point
   friend ostream &operator<<( ostream &, const Circle & );
 public:
   // default constructor
   Circle( double r = 0.0, int x = O, int y = 0 );
   void setRadius( double );  // set radius
   double getRadius() const;  // return radius
   double area() const;      // calculate area
 protected:
   double radius;
 };
 #endif
 // Fig. 9.4:circle.cpp
 // Member function definitions for class Circle
 #include "circle.h"
 // Constructor for Circle calls constructor for Point
 // with a member initializer then initializes radius.
 Circle::Circle( double r, int a, int b )
   : Point( a, b )      // call base-class constructor
 { setRadius( r ); }
 // Set radius of Circle
 void Circle::setRadius( double r )
    { radius = ( r >= O ? r : 0 ); }
 // Get radius of Circle
 double Circle::getRadius() const { return radius; }
 // Calculate area of Circle
 double circle::area() const
  { return 3.14159 * radius * radius; }
 // Output a Circle in the form:
 ostream &operator<<( ostream &output, const Circle &c )
 {
   output << "Center =" << static cast< Point >( c )
          << "; Radius ="
          << setiosflags( ios::fixed | ios::showpoint )
          << setprecision( 2 ) << c.radius;
   return output;  // enables cascaded calls
 }
 // Fig. 9.4:fig09 04.cpp
 // Casting base-class pointers to derived-class pointers
 #include < iostream.h>
 #include < iomanip.h>
 #include "point.h"
 #include "circle.h"
 int main()
 {
    Point *pointPtr = 0, p( 30, 50 );
    Circle *circlePtr = 0, c( 2.7, 120, 89 );
   cout << "Point p: "<< p << ,\nCircle C: "<< c << '\n';
    // Treat a Circle as a Point (see only the base class part)
    pointPtr = &C;  // assign address of Circle to pointPtr
    cout << "\nCircle C (via *pointPtr):"
         << *pointPtr << '\n';
   // Treat a Circle as a Circle (with some Casting)
  pointPtr = &C;  // assign address of Circle to pointPtr
   // cast base-class pointer to derived-class pointer
   circlePtr = static cast< Circle * >( pointPtr );
   cout << "\nCircle C (via *circlePtr):\n" << *circlePtr
        << "\nArea of C (via circlePtr):"
       << circlePtr->area() << '\n';
   // DANGEROUS: Treat a Point as a Circle
   pointPtr = &p;  // assign address of Point to pointPtr
  // cast base-class pointer to derived-class pointer
   circlePtr = static_cast< Circle * >( pointPtr );
   cout << "\nPoint p (via *circlePtr):\n" << *circlePtr
       << "\nArea of object circlePtr points to:"
       << circlePtr->area() << endl;
   return 0;
 }

输出结果:

Point p: [ 30, 50]

Circle c: Center = [ 120, 89]; Radius = 2.70

Circle c(via *circlePtr):[ 120,89 ]

Circle c( via *circlePtr ):

Center = [ 120,89] ;Radius = 2.70

Area of c (via circlePtr): 22.90

oint p( via *circlePtr ):

Center = [ 30, 50]; Radius = 0.00

Area of object circlePtr points to: 0.00

图 9.4 把基类指针强制转换为派生类指针

类Circle继承了类Point,类定义的第一行指定了这种继承是public继承:

class Circle : public Point { // Circle inherits from Point

Point和Circle重载的流插入运算符输出了这两个对象的信息。然后,驱动程序将派生类指针(对象c的地址)赋绐基类指针pointPtr并用Point的operator<<输出Circle的对象c,并复引用指针*pointPtr。

注意只显示Circle对象c的Point部分。对public继承,总是可以将派生类指针赋给基类,因为派生类对象也是基类对象。基类指针只“看到”派生类对象的基类部分。编译器进行派生类指针向基类指针的隐式转换。

随后,程序将派生类指针(对象c的地址)赋给基类指针pointPtr,并将pointPtr强制转换回Circle*类型,强制转换后的结果赋给指针circlePtr。使用Circle重载流插入运算符输出Circle的对象c并复引用指针*circlePtr。然后通过指针circlePtr输出Circle对象c的面积。因为该指针一直指向Circle对象,所以输出了该对象的合法面积值。

因为把基类指针直接赋给派生类指针蕴含着危险性,所以编译器不允许这么做,也不执行隐式转换。使用显式类型转换是告诉编译器程序员已经知道了这种危险性。正确地使用指针是程序员的责任,因此编译器允许有危险的转换。

接着,程序演示了将基类指引(对象p的地址)赋给基类指针pointPtr,并将pointPtr强制转换为Circle*类型,强制转换操作的结果赋给了circlePtr。Point对象p用Circle的operator<<输出,并复引用指针*circlePtr。注意半径元素输出为0(实际上不存在,因为circlePtr实际上针对Point对象)。将Point作为Circle输出就会导致radius为未定义的值(这里刚好为0),因为指针总是指向Point对象。Point对象没有radius成员,因此输出circlePtr所指radius数据成员内存地址中的值。circlePtr所指对象的面积(Point对象P)也是通过circlePtr输出。注意面积值为0.00,这是根据radius“未定义”的值算出的。显然,访问不存在的数据成员是很危险的。调用不存在的成员函数可能使程序崩溃。

本节介绍指针转换的机制。为下一章介绍多态与面向对象编程打下了基础。