1 友元(Friend)

采用类的机制后实现了数据的隐藏与封装,类的数据成员一般定义为私有成员,成员函数一般定义为公有的,依此提供类与外界间的通信接口。但是,有时需要定义一些函数,这些函数不是类的一部分,但又需要频繁地访问类的数据成员,这时可以将这些函数定义为该类的友元函数。除了友元函数外,还有友元类,两者统称为友元。友元的作用是提高了程序的运行效率(即减少了类型和安全性检查及调用的时间开销),但它破坏了类的封装性和隐藏性,使得非成员函数可以访问类的私有成员。

友元可以是一个函数,该函数被称为友元函数;友元也可以是一个类,该类被称为友元类

1.1 同类对象间无私处

  1. MyString::MyString(const MyString& other)
  2. {
  3. int len = strlen(other._str);
  4. this->_str = new char[len + 1];
  5. strcpy(this->_str, other._str);
  6. }

1.2 异类对象间有友员

友元目的本质,是让其它不属于本类的成员(全局函数,其它类的成员函数),成为类的成 员而具备了本类成员的属性。

1.2.1 友元函数

友元函数是可以直接访问类的私有成员的非成员函数。它是定义在类外的普通函数,它 不属于任何类,但需要在类的定义中加以声明,声明时只需在友元的名称前加上关键字 friend,其格式如下:

  1. friend 类型函数名(形式参数);

一个函数可以是多个类的友元函数,只需要在各个类中分别声明。

1.2.1.1 全局函数作友元函数

  1. #include <cmath>
  2. #include <iostream>
  3. using namespace std;
  4. class Point {
  5. public:
  6. Point(double xx, double yy)
  7. {
  8. x = xx;
  9. y = yy;
  10. }
  11. void Getxy();
  12. friend double Distance(Point& a, Point& b);
  13. private:
  14. double x, y;
  15. };
  16. void Point::Getxy()
  17. {
  18. cout << "(" << x << "," << y << ")" << endl;
  19. }
  20. double Distance(Point& a, Point& b)
  21. {
  22. double dx = a.x - b.x;
  23. double dy = a.y - b.y;
  24. return sqrt(dx * dx + dy * dy);
  25. }
  26. int main(void)
  27. {
  28. Point p1(3.0, 4.0), p2(6.0, 8.0);
  29. p1.Getxy();
  30. p2.Getxy();
  31. double d = Distance(p1, p2);
  32. cout << "Distance is " << d << endl;
  33. return 0;
  34. }

1.2.1.2 类成员函数作友元函数

  1. #include <cmath>
  2. #include <iostream>
  3. using namespace std;
  4. class Point;
  5. class ManagerPoint {
  6. public:
  7. double Distance(Point& a, Point& b);
  8. };
  9. class Point {
  10. public:
  11. Point(double xx, double yy)
  12. {
  13. x = xx;
  14. y = yy;
  15. }
  16. void Getxy();
  17. friend double ManagerPoint::Distance(Point& a, Point& b);
  18. private:
  19. double x, y;
  20. };
  21. void Point::Getxy()
  22. {
  23. cout << "(" << x << "," << y << ")" << endl;
  24. }
  25. double ManagerPoint::Distance(Point& a, Point& b)
  26. {
  27. double dx = a.x - b.x;
  28. double dy = a.y - b.y;
  29. return sqrt(dx * dx + dy * dy);
  30. }
  31. int main(void)
  32. {
  33. Point p1(3.0, 4.0), p2(6.0, 8.0);
  34. p1.Getxy();
  35. p2.Getxy();
  36. ManagerPoint mp;
  37. float d = mp.Distance(p1, p2);
  38. cout << "Distance is " << d << endl;
  39. return 0;
  40. }

补充:
前向声明,是一种不完全型(forward declaration)声明,即只需提供类名(无需提供 类实现)即可。但存在局限性:

  • 不能定义类的对象。
  • 可以用于定义指向这个类型的指针或引用。
  • 用于声明(不是定义),使用该类型作为形参类型或者函数的返回值类型。

    1.2.2 友元类

    友元类的所有成员函数都是另一个类的友元函数,都可以访问另一个类中的隐藏信息(包 括私有成员和保护成员)。

当希望一个类可以存取另一个类的私有成员时,可以将该类声明为另一类的友元类。定义 友元类的语句格式如下:

  1. friend class 类名;
  2. 其中:friend class 是关键字, 类名必须是程序中的一个已定义过的类
  3. 例如: 以下语句说明类B是类A的友元类
  4. class A
  5. {
  6. public:
  7. // ....
  8. friend class B;
  9. // ....
  10. };

经过以上说明后,类B的所有成员函数都是类A 的友元函数,能存取类A的私有成员和 保护成员。

  1. #include <stdio.h>
  2. class A {
  3. public:
  4. inline void Test()
  5. {
  6. }
  7. private:
  8. int x, y;
  9. friend class B;
  10. };
  11. class B {
  12. public:
  13. inline void Test()
  14. {
  15. A a;
  16. printf("x=%d,y=%d", a.x, a.y);
  17. }
  18. };

1.3 论友元

1.3.1.声明位置

友元声明以关键字 friend 开始,它只能出现在类定义中。因为友元不是授权类的成员,所以它不受其所在类的声明区域 public private 和 protected 的影响。通常我们选择把所有友元声明组织在一起并放在类头之后。

1.3.2.友元的利弊

友元不是类成员,但是它可以访问类中的私有成员。友元的作用在于提高程序的运行效率,但是,它破坏了类的封装性和隐藏性,使得非成员函数可以访问类的私有成员。不过,类的访问权限确实在某些应用场合显得有些呆板,从而容忍了友元这一特别语法现象。

1.3.3.注意事项

  • 友元关系不能被继承。
  • 友元关系是单向的,不具有交换性。若类 B 是类 A 的友元,类 A 不一定是类 B的友元,要看在类中是否有相应的声明。
  • 友元关系不具有传递性。若类 B 是类 A 的友元,类 C 是 B 的友元,类 C 不一定是类 A 的友元,同样要看类中是否有相应的声明。

    2 封装(Encapsulation)

    3.1 封装

    3.1.1 从struct说起

    当单一变量无法完成描述需求的时候,结构体类型解决了这一问题。可以将多个类型打包成一体,形成新的类型。

    但是,新类型并不包含对数据类的操作。所的有操作都是通过函数的方式去进行封装。

3.1.2 封装

封装,可以达到:对外提供接口屏蔽数据,对内开放数据。

比如我们用 struct 封装的类,即知其接口,又可以直接访问其内部数据,这样却没有达到信息隐蔽的功效。而 class 则提供了这样的功能,屏蔽内部数据,对外开放接口。

struct 中所有行为和属性都是 public 的(默认)。C++中的 class 可以指定行为和属性的访问方式,默认为 pirvate。

访问属性 属性 对象内部 对象外部
public 公有 可访问 可访问
protected 保护 可访问 不可访问
private 私有 可访问 不可访问

3.1.3 用class去封装带行为的类

class 封装的本质,在于将数据和行为,绑定在一起然后通过对象来完成操作。
class Date

  1. #include <iostream>
  2. using namespace std;
  3. class Date {
  4. public:
  5. void init(Date& d);
  6. void print(Date& d);
  7. bool isLeapYear(Date& d);
  8. private:
  9. int year;
  10. int month;
  11. int day;
  12. };
  13. void Date::init(Date& d)
  14. {
  15. cout << "year,month,day:" << endl;
  16. cin >> d.year >> d.month >> d.day;
  17. }
  18. void Date::print(Date& d)
  19. {
  20. cout << "year month day" << endl;
  21. cout << d.year << ":" << d.month << ":" << d.day << endl;
  22. }
  23. bool Date::isLeapYear(Date& d)
  24. {
  25. if ((d.year % 4 == 0 && d.year % 100 != 0) || d.year % 400 == 0)
  26. return true;
  27. else
  28. return false;
  29. }
  30. int main()
  31. {
  32. Date d;
  33. d.init(d);
  34. d.print(d);
  35. if (d.isLeapYear(d))
  36. cout << "leap year" << endl;
  37. else
  38. cout << "not leap year" << endl;
  39. return 0;
  40. }

3 继承与派生( inherit&&derive )

在 C++中可重用性(software reusability)是通过继承(inheritance)这一机制来实现的。 如果没有掌握继承性,就没有掌握类与对象的精华。

3.1 继承

3.1.1 关系定性 is-a / has-a

is-a 是一种属于关系,如:狗属于一种动物,车属于一种交通工具(Dog is an Animal. Car is a Vehicle.)在面向对象中表现为一种继承关系。可以设计一个 Animal 类,Dog 类作为Animal 类(基类)的派生类;设计一个 Vehicle 类,Car 类作为 Vehicle 类(基类)的派生类。

has-a 是一种包含、组合关系。如:车包含方向盘、轮胎、发动机(Car has steering-wheel, wheels, engine),但不能说方向盘/轮胎/发动机是一种车;狗包含腿、尾巴,但不能说腿/尾巴是一种狗。正确的应该说车聚合(包含)了方向盘、轮胎、发动机。

is-a 这种关系可以完成代码复用,是继承。把比较抽象的类(如例子中的动物、交通工具)定义为基类,把比较具体的定义为子类(派生类),比如狗、兔子、马都以动物做为基类而做出派生类。基类都可以有跑、吃、睡觉等共同方法,高度、体重等共同属性;具体到狗、兔子、马则有自己特别的属性如食物,特别的方法如叫声等。

因此,如果 A 是 B,则 B 是 A 的基类,A 是 B 的派生类。为继承关系。

has-a 这种关系可以把一个复杂的类处理为一个个相对简单的类,是聚合。比如创建方向盘类、轮胎类、发动机类,最后创建车类。车类调用 4 个轮胎类实例(假如该车有 4 个轮胎)和 1 个方向盘类实例(对象)和一个发动机类实例(对象)。每个类本身相对简单,通过聚合(组合)成为一个复杂的类。

因此,如果 A 包含 B,则 B 是 A 的组成部分。为聚合关系,可以由组成部分聚合成为一个类。

3.3.2 语法

派生类的声明:

  1. class 派生类名:[继承方式] 基类名
  2. {
  3. 派生类成员声明;
  4. };

一个派生类可以同时有多个基类,这种情况称为多重继承,派生类只有一个基类,称为 单继承。下面从单继承讲起

3.3.3 继承方式

继承方式规定了如何访问基类继承的成员。继承方式有 public、private、protected。继承方式不影响派生类的访问权限,影响了从基类继承来的成员的访问权限,包括派生类内的访问权限和派生类对象
简单讲:

  • 公有继承:基类的公有成员和保护成员在派生类中保持原有访问属性,其私有成员仍为基类的私有成员。
  • 私有继承:基类的公有成员和保护成员在派生类中成了私有成员,其私有成员仍为基类的私有成员。
  • 保护继承:基类的公有成员和保护成员在派生类中成了保护成员,其私有成员仍为基类的私有成员。
  • pretected 对于外界访问属性来说,等同于私有,但可以派生类中可见。 ```cpp

    include

using namespace std;

class Base { public: int pub;

protected: int pro;

private: int pri; };

class Drive : public Base { public: void func() { pub = 10; pro = 100; // pri = 1000; }

public: int a;

protected: int b;

private: int c; };

int main() { Base b; b.pub = 10; // b.pro = 100; // b.pri = 1000; return 0; }

  1. <a name="cNTpR"></a>
  2. ### 3.3.4 派生类的组成
  3. 派生类中的成员,包含两大部分,一类是从基类继承过来的,一类是自己增加的成员。 从基类继承过过来的表现其共性,而新增的成员体现了其个性。<br />几点说明:
  4. - 全盘接收,除了构造器与析构器。基类有可能会造成派生类的成员冗余,所以说基
  5. 类是需设计的。
  6. - 派生类有了自己的个性,使派生类有了意义。
  7. ```cpp
  8. #include <iostream>
  9. #include <typeinfo>
  10. using namespace std;
  11. class A {
  12. public:
  13. A()
  14. {
  15. cout << this << endl;
  16. cout << typeid(this).name() << endl;
  17. }
  18. int a;
  19. };
  20. class B : public A {
  21. public:
  22. B()
  23. {
  24. cout << this << endl;
  25. cout << typeid(this).name() << endl;
  26. }
  27. int b;
  28. };
  29. class C : public B {
  30. public:
  31. C()
  32. {
  33. cout << this << endl;
  34. cout << typeid(this).name() << endl;
  35. }
  36. void func()
  37. {
  38. cout << &a << endl;
  39. cout << &b << endl;
  40. cout << &c << endl;
  41. }
  42. int c;
  43. };
  44. int main()
  45. {
  46. C c;
  47. cout << "&c " << &c << endl;
  48. cout << "*************" << endl;
  49. c.func();
  50. return 0;
  51. }

3.2 派生类的构造

派生类中由基类继承而来的成员的初始化工作还是由基类的构造函数完成,然后派生类 中新增的成员在派生类的构造函数中初始化。

3.2.1 派生类构造函数的语法:

派生类名::派生类名(参数总表)
        :基类名(参数表),内嵌子对象(参数表)
{
    派生类新增成员的初始化语句; // 也可出现地参数列表中
}

注:

  • 构造函数的初始化顺序并不以上面的顺序进行,而是根据声明的顺序初始化。
  • 如果基类中没有默认构造函数(无参),那么在派生类的构造函数中必须显示调用基类构 造函数,以初始化基类成员。
  • 派生类构造函数执行的次序:基类—>成员—>子类

    • 调用基类构造函数,调用顺序按照它们被继承时声明的顺序(从左到右)
    • 调用内嵌成员对象的构造函数,调用顺序按照它们在类中声明的顺序
    • 派生类的构造函数体中的内容

      3.2.2 结论

      子类构造器中,要么显示的调用父类的构造器(传参),要么隐式的调用。发生隐式调用时, 父类要有无参构造器或是可以包含无参构造器的默认参数函数。子类对象亦然。

      3.3 派生类的拷贝构造

      3.3.1 格式

      派生类::派生类(const 派生类& another)
      :基类(another), 派生类新成员(another.新成员) 
      {
      }
      

      3.3.2 结论

      派生类中的默认拷贝构造器会调用父类中默认或自实现拷贝构造器,若派生类中自实现拷 贝构造器,则必须显示的调用父类的拷贝构造器。

      3.4 派生类的赋值运算符重载

      赋值运算符函数不是构造器,所以可以继承,语法上就没有构造器的严格一些。

      3.4.1 格式

      子类& 子类::operator=(const 子类& another)
      {
      if(this == &another)
         return *this;                //防止自赋值
      
      父类::operator =(another);       // 调用父类的赋值运算符重载
      
      this->salary = another.salary;   //子类成员初始化
      return * this;
      }
      

      3.4.2 结论

      派生类的默认赋值运算符重载函数,会调用父类的默认或自实现函数。派生类若自实现, 则不会发生调用行为也不报错(区别拷贝),赋值错误;若要正确,需要显示的调用父类的构 造器。

      3.5 派生类友元函数

      由于友元函数并非类成员,因引不能被继承,在某种需求下,可能希望派生类的友元函数能 够使用基类中的友元函数。为此可以通过强制类型转换,将派生类的指针或是引用强转为其基类的 引用或是指针,然后使用转换后的引用或是指针来调用基类中的友元函数。

      3.6 派生类析构函数的语法

      派生类的析构函数的功能是在该对象消亡之前进行一些必要的清理工作,析构函数没有类 型,也没有参数。析构函数的执行顺序与构造函数相反

析构顺序:子类->成员->基类

无需指明析构关系。why? 析构函数只有一种,无重载,无默参。

3.7 派生类成员的标识和访问

3.7.1 作用域分辨符

格式:
基类名::成员名;基类名::成员名(参数表)
如果某派生类的多个基类拥有同名的成员,同时,派生类又新增这样的同名成员,在这种情况下,派生类成员将 shadow(隐藏)所有基类的同名成员。这就需要使用上述的调用方式才能调用基类的同名成员

#include <iostream>

using namespace std;

class Base {
public:
    void func(int)
    {
        cout << "haha" << endl;
    }
};

class Drive : public Base {
public:
    void func()
    {
        // func(); // func 死循环
        // Base::func(); // 被 shadow 的成员,可以这样访问
        cout << "hehe" << endl;
    }
};

//
int main()
{
    Drive d;
    d.func(); // 访问派生类成员
    // d.Base::func(3); //访问基类成员
    return 0;
}

小结
重载:同一作用域 ,函数同名不同参(个数,类型,顺序);
隐藏:父子类中,标识符(函数,变量)相同,无关乎返值和参数(函数),或声明类型(变量)。

3.7.2 继承方式

3.7.2.1 图示

成员 \ 继承方式 public protected private
public public protected private
protected protected protected inaccessable
private inaccessable inaccessable inaccessable

3.7.2.2 详解

public 公有继承
当类的继承方式为公有继承时,基类的公有和保护成员的访问属性在派生类中不变,而基类的私有成员不可访问。即基类的公有成员和保护成员被继承到派生类中仍作为派生类的公有成员和保护成员。派生类的其他成员可以直接访问它们。无论派生类的成员还是派生类的对象都无法访问基类的私有成员。

private 私有继承
当类的继承方式为私有继承时,基类中的公有成员和保护成员都以私有成员身份出现在派生类中,而基类的私有成员在派生类中不可访问。基类的公有成员和保护成员被继承后作为派生类的私有成员,派生类的其他成员可以直接访问它们,但是在类外部通过派生类的对象无法访问。无论是派生类的成员还是通过派生类的对象,都无法访问从基类继承的私有成员。通过多次私有继承后,对于基类的成员都会成为不可访问。因此私有继承比较少用。

protected 保护继承
保护继承中,基类的公有成员和私有成员都以保护成员的身份出现在派生类中,而基类的私有成员不可访问。派生类的其他成员可以直接访问从基类继承来的公有和保护成员,但是类外部通过派生类的对象无法访问它们,无论派生类的成员还是派生类的对象,都无法访问基类的私有成员。

图示之:
image.png

3.7.3 派生类成员属性划分为四种:

公有成员;保护成员;私有成员;不可访问的成员;

#include <iostream>

using namespace std;

class Base {
public:
    int pub;

protected:
    int pro;

private:
    int pri;
};

class Drive : public Base {
public:
    void func()
    {
        pub = 10;
        pro = 100;
        // pri = 1000;
    }
};

int main()
{
    Base b;
    b.pub = 10;
    // b.pro = 100;
    // b.pri = 1000;
    return 0;
}

4 多继承

从继承类别上分,继承可分为单继承和多继承,前面讲的都是单继承。

4.1 多继承的意义

俗话讲的,鱼与熊掌不可兼得,而在计算机就可以实现,生成一种新的对象,叫熊掌鱼, 多继承自鱼和熊掌即可。还比如生活中,“兼”。

4.2 继承语法

派生类名::派生类名(参数总表)
    :基类名 1(参数表 1),基类名(参数名 2)....基类名 n(参数名 n),
      内嵌子对象 1(参数表 1),内嵌子对象 2(参数表 2)....内嵌子对象 n(参数表 n) .
{
    派生类新增成员的初始化语句;
}

4.3 三角问题(二义性问题)

多个父类中重名的成员,继承到子类中后,为了避免冲突,携带了各父类的作用域信息, 子类中要访问继承下来的重名成员,则会产生二义性,为了避免冲突,访问时还需要父类的 作用域信息。

#include <iostream>

using namespace std;

class X {
public:
    X(int d)
        : _data(d)
    {
    }
    void setData(int i)
    {
        _data = i;
    }
    int _data;
};

class Y {
public:
    Y(int d)
        : _data(d)
    {
    }
    int getData()
    {
        return _data;
    }
    int _data;
};

class Z : public X, public Y {
public:
    Z()
        : X(2)
        , Y(3)
    {
    }
    void dis()
    {
        // cout << _data << endl; // 这样就不知道是哪个基类的成员变量
        cout << X::_data << endl;
        cout << Y::_data << endl;
    }
};

int main()
{
    Z z;
    z.dis();
    z.setData(2000);
    cout << z.getData() << endl;
    return 0;
}

4.4 钻石问题

4.4.1 三角转四角

#include <iostream>
using namespace std;
class M {
public:
    M(int i)
        : _data(i)
    {
    }
    int _data;
};

class X : public M {
public:
    X(int d)
        : M(d)
    {
    }
    void setData(int i)
    {
        _data = i;
    }
};

class Y : public M {
public:
    Y(int d)
        : M(d)
    {
    }
    int getData()
    {
        return _data;
    }
};

class Z : public X, public Y {
public:
    Z()
        : X(2)
        , Y(3)
    {
    }
    void dis()
    {
        cout << X::_data << endl;
        cout << Y::_data << endl;
    }
};

int main()
{
    Z z;
    z.dis();
    z.setData(2000);
    cout << z.getData() << endl;
    return 0;
}

4.4.2 虚继承

#include <iostream>
using namespace std;
class M {
public:
    M(int d)
        : _data(d)
    {
    }
    int _data;
};

class X : virtual public M {
public:
    X(int d)
        : M(d)
    {
    }
    void setD(float d)
    {
        _data = d;
    }
};

class Y : virtual public M {
public:
    Y(int d)
        : M(d)
    {
    }
    int getD()
    {
        return _data;
    }
};

class Z : public X, public Y {
public:
    Z(int _x, int _y)
        : X(_x)
        , Y(_y)
        , M(100)
    {
    }
    void dis()
    {
        // cout<<X::_data<<endl;
        // cout<<Y::_data<<endl;
        cout << _data << endl;
    }
};

int main()
{
    Z z;
    z.dis();
    z.setData(2000);
    cout << z.getData() << endl;
    return 0;
}

4.5 小结

虚继承的意义
在多继承中,保存共同基类的多份同名成员,虽然有时是必要的,可以在不同的数 据成员中分别存放不同的数据,但在大多数情况下,是我们不希望出现的。因为保留多 份数据成员的拷贝,不仅占有较多的存储空间,还增加了访问的困难。

为此,c++提供了,虚基类和虚继承机制,实现了在多继承中只保留一份共同成员。

虚基类,需要设计和抽象,虚继承,是一种继承的扩展。

语法总结:

  • M 类称为虚基类(virtual base class ),是抽象和设计的结果
  • 虚继承语法:class 派生类名:virtual 继承方式 基类
  • 虚基类及间接类的实始化
    class A {
    public:
      A(int i)
      {
      }
    };
    class B : virtual public A {
    public:
      B(int n)
          : A(n)
      {
      }
    };
    class C : virtual public A {
    public:
      C(int n)
          : A(n)
      {
      }
    };
    class D : public B, public C {
      D(int n)
          : A(n)
          , B(n)
          , C(n)
      {
      }
    };
    

    5 多态

    5.1 浅析多态的意义

如果有几个上似而不完全相同的对象,有时人们要求在向它们发出同一个消息时,它们的反应各不相同,分别执行不同的操作。这种情况就是多态现象。

例如,甲乙丙 3 个班都是高二年级,他们有基本相同的属性和行为,在同时听到上课铃声的时候,他们会分别走向 3 个不同的教室,而不会走向同一个教室。

同样,如果有两支军队,当在战场上听到同种号声,由于事先约定不同,A 军队可能实施进攻,而 B 军队可能准备 kalalok。

又如在 winows 环境下,用鼠标双击一个对象(这就是向对象传递一个消息),如果对象是一个可执行文件,则会执行此程序,如果对象是一个文本文件,由会启动文本编辑器并打开该文件。

C++中所谓的多态(polymorphism)是指,由继承而产生的相关的不同的类,其对象对同一消息会作出不同的响应。

多态性是面向对象程序设计的一个重要特征,能增加程序的灵活性。可以减轻系统升级、维护、调试的工作量和复杂度。

5.2 赋值兼容(多态实现的前提)

5.2.1 规则

赋值兼容规则是指在需要基类对象的任何地方都可以使用公有派生类的对象来替代。赋值兼容是一种默认行为,不需要任何的显示的转化步骤。

赋值兼容规则中所指的替代包括以下的情况:

  • 派生类的对象可以赋值给基类对象
  • 派生类的对象可以初始化基类的引用
  • 派生类对象的地址可以赋给指向基类的指针
  • 在替代之后,派生类对象就可以作为基类的对象使用,但只能使用从基类继承的成员

    5.2.2 代码

    ```cpp

    include

    using namespace std; class Shape { public: Shape(int x, int y)
      : _x(x)
      , _y(y)
    
    { } void draw() {
      cout << "draw Shap ";
      cout << "start (" << _x << "," << _y << ") " << endl;
    
    }

protected: int _x; int _y; };

class Circle : public Shape { public: Circle(int x, int y, int r) : Shape(x, y) , _r(r) { } void draw() { cout << “draw Circle “; cout << “start (“ << _x << “,” << _y << “) “; cout << “radio r = “ << _r << endl; }

private: int _r; }; int main() { Shape s(3, 5); s.draw();

Circle c(1, 2, 4);
c.draw();

s = c;
s.draw();

Shape& rs = c;
rs.draw();

Shape* ps = &c;
ps->draw();
return 0;

}

<a name="kZSbr"></a>
### 5.2.3 补充
父类也可以通过强转的方式转化为子类。 父类对象强转为子类对象后,访问从父类继承
下来的部分是可以的,但访问子类的部分,则会发生**越界**的风险,越界的结果是未知的。
```cpp
//c = static_cast<Circle>(s); //缺少转化函数
//c.draw();
Circle* pc = static_cast<Circle*>(&s);
pc->draw();

5.3 多态形成的条件

5.3.1 多态

5.3.1.1 静多态

前面学习的函数重载,也是一种多态现象,通过命名倾轧在编译阶段决定,故称为静多 态。

5.3.3.2 动多态

动多态,不是在编译器阶段决定,而是在运行阶段决定,故称为动多态。动多态行成的 条件如下:

  • 父类中有虚函数。
  • 子类 override(覆写)父类中的虚函数。
  • 通过己被子类对象赋值的父类指针或引用,调用共用接口。

    5.3.2 虚函数

    格式

    class 类名
    {
      virtual 函数声明; 
    }
    

    例举

    virtual void draw()
    {
      cout << "draw Shap ";
      cout << "start (" << _x << "," << _y << ") " << endl;
    }
    

    Circle 类中

    void draw()
    {
      cout << "draw Circle ";
      cout << "start (" << _x << "," << _y << ") ";
      cout << "radio r = " << _r << endl;
    }
    

    Rect类中

    void draw()
    {
      cout << "draw Rect";
      cout << "start (" << _x << "," << _y << ") ";
      cout << "len = " << _len << " wid = " << _wid << endl;
    }
    

    测试

    int main()
    {
      Circle c(1, 2, 4);
      c.draw();
      Rect r(2, 3, 4, 5);
      r.draw();
      Shape* ps;
      int choice;
      while (1) //真正的实现了动多态,在运行阶段决定。
      {
          scanf("%d", &choice);
          switch (choice) {
          case 1:
              ps = &c;
              ps->draw();
              break;
          case 2:
              ps = &r;
              ps->draw();
              break;
          }
      }
      return 0;
    }
    

    5.3.3 虚函数小结

  • 在基类中用 virual 声明成员函数为虚函数。类外实现虚函数时,不必再加 virtual

  • 在派生类中重新定义此函数称为覆写,要求函数名,返值类型,函数参数个数及类型全部匹配。并根据派生类的需要重新定义函数体
  • 当一个成员函数被声明为虚函数后,其派生类中完全相同的函数(显示的写出)也为虚函数。 可以在其前加 virtual 以示清晰
  • 定义一个指基类对象的指针,并使其指向其子类的对象,通过该指针调用虚函数,此时调用的就是指针变量指向对象的同名函数
  • 子类中的覆写的函数,可以为任意访问类型,依子类需求决定。 ```cpp

    include

using namespace std;

class Base { public: virtual void func() { cout << “Base” << endl; } };

class Derive : public Base { private: void func() { cout << “Derive” << endl; } };

int main() { Derive d; // d.func(); Base* p = &d; p->func(); return 0; }

<a name="DIvwL"></a>
### 5.3.4 纯虚函数
格式
```cpp
class 类名
{
    virtual 函数声明 = 0;
}

例举
Shape 类中

virtual void draw() = 0;

Circle 类中

void draw()
{
    cout << "draw Circle ";
    cout << "start (" << _x << "," << _y << ") ";
    cout << "radio r = " << _r << endl;
}

测试

int main()
{
    // Shape s(1,2); //函数纯虚函数的类称为抽象基类
    Circle c(1, 2, 3);
    Rect r(1, 2, 3, 5);
    Shape* pc = &c;
    pc->draw();
    pc = &r;
    pc->draw();
    return 0;
}

5.3.5 纯虚函数小结

  • 含有纯虚函数的类,称为抽象基类,不可实列化。即不能创建对象,存在的意义就是被继承,提供族类的公共接口,java 中称为 interface
  • 纯虚函数只有声明,没有实现,被“初始化”为 0
  • 如果一个类中声明了纯虚函数,而在派生类中没有对该函数定义,则该虚函数在派生类中仍然为纯虚函数,派生类仍然为纯虚基类

    5.3.5 含有虚函数的析构

    含有虚函数的类,析构函数也应该声明为虚函数。在 delete 父类指针的时候,会调用子类的析构函数,实现完整析构。

    5.3.6 若干限制

  • 只有类的成员函数才能声明为虚函数虚函数仅适用于有继承关系的类对象,所以普通函数不能声明为虚函数。

  • 静态成员函数不能是虚函数。静态成员函数不受对象的捆绑,只有类的信息。
  • 内联函数不能是虚函数
  • 构造函数不能是虚函数。构造时,对象的创建尚未完成。构造完成后,才能算一个名符其实的对象。
  • 析构函数可以是虚函数且通常声明为虚函数。

    5.3.7 虚析构

    多态由栈对象到堆对象的过度,堆对象销毁的问题。

    5.4 运行时类型信息(RTTI)

    typeid dynamic_cast 是 C++ 运行时类型信息 RTTI(run time type identificaiton)重 要组成部分。运行时信息,来自于多态,所以以下运算符只用于基于多态的继承体系中。

    5.4.1 typeid

    运算符 typeid 返回包含操作数数据类型信息的 type_info 对象的一个引用,信息中包括数据类型的名称,要使用 typeid,程序中需要包含头文件

其中 type_info 重载了操作符==, !=,分别用来比较是否相等、不等,函数 name()返回类 型名称。type_info 的拷贝和赋值均是私有的,故不可拷贝和赋值。

常用于返回检查、调试之用。

#include <iostream>
#include <typeinfo>

using namespace std;

typedef void (*Func)();
class Base {
public:
    virtual ~Base() { }
};

class Derive : public Base {
};

int main()
{
    cout << typeid(int).name() << endl;
    cout << typeid(double).name() << endl;
    cout << typeid(char*).name() << endl;
    cout << typeid(char**).name() << endl;
    cout << typeid(const char*).name() << endl;
    cout << typeid(const char* const).name() << endl;

    cout << typeid(Func).name() << endl;
    cout << typeid(Base).name() << endl;
    cout << typeid(Derive).name() << endl;

    Derive d;
    Base& b = d; //Base 中没有虚函数时,有时?
    cout << typeid(b).name() << endl;
    cout << typeid(d).name() << endl;

    Base* p = &d;
    cout << typeid(p).name() << endl; //判断指针是,其实是看不出其类型信息的
    cout << typeid(*p).name() << endl;
    cout << typeid(d).name() << endl;
    cout << boolalpha << (typeid(*p) == typeid(d)) << endl;
    return 0;
}

多态下使用 typeid 时要注意的问题:

  • 确保基类定义了至少一个虚函数(虚析构也算)
  • 不要将 typeid 作用于指针,应该作用于引用,或解引用的指针
  • typeid 是一个运算符,而不是函数
  • typeid 运算符返回的 type_info 类型,其拷贝构造函数和赋值运算函数都声明为private 了,这意味着其不能用于 stl 容器,所以我们一般不能不直接保存 type_info 信息,而保存 type_info 的 name 信息

注解:

Notice how the type that typeid considers for pointers is the pointer type itself (both a and b are of type class Base *). However, when typeid is applied to objects (like *a and *b) typeid yields their dynamic type (i.e. the type of their most derived complete object).

    If the type typeid evaluates is a pointer preceded by the dereference operator (*), and this pointer has a null value, typeidthrows a bad_typeid exception.

5.4.2 typecast

5.4.1.1 static_cast

在一个方向上可以作隐式转换的,在另外一个方向上可以作静态转换。发生在编译阶段,不保证后序使用的正确性。

5.4.2 reinterpreter_cast

既不在编译器期也不在运行期进行检查,安全性完全由程序员决定。

5.4.1.3 dynamic_cast

ynamic_cast 一种运行时的类型转化方式,所以要在运行时作转换判断。检查指针所指类型,然后判断这一类型是否与正在转换成的类型有一种 “is a”的关系。如果是, dynamic_cast 返回对象地址;如果不是,dynamic_cast 返回 nullptr。

dynamic_cast 常用多态继承中,判断父类指针的真实指向。

#include <iostream>
#include <typeinfo>

using namespace std;

class A {
public:
    virtual ~A() { }
};

class B : public A {
};

class C : public A {
};

class D {
};

int main()
{
    B b;
    A* pa = &b;
    B* pb = dynamic_cast<B*>(pa); //成功
    cout << pb << endl;
    C* pc = dynamic_cast<C*>(pa); //成功 安全
    cout << pc << endl;
    D* pd = dynamic_cast<D*>(pa); //成功 安全
    cout << pd << endl;
    pb = static_cast<B*>(pa); //成功
    cout << pb << endl;
    pc = static_cast<C*>(pa); //成功 不安全
    cout << pc << endl;
    // pd = static_cast<D*>(pa); //编译 不成功
    // cout<<pd<<endl;
    pb = reinterpret_cast<B*>(pa); //成功 不安全
    cout << pb << endl;
    pc = reinterpret_cast<C*>(pa); //成功 不安全
    cout << pc << endl;
    pd = reinterpret_cast<D*>(pa); //成功 不安全
    cout << pd << endl;
    return 0;
}

5.5 多态实现浅析

5.5.1 虚函数表

C++的多态是通过一张虚函数表(Virtual Table)来实现的,简称为 V-Table。在这个表中,主是要一个类的虚函数的地址表,这张表解决了继承、覆写的问题,保证其真实反应实际的函数。这样,在有虚函数的类的实例中这个表被分配在了这个实例的内存中,所以,当我们用父类的指针来操作一个子类的时候,这张虚函数表就显得由为重要了,它就像一个地图一样,指明了实际所应该调用的函数。

这里我们着重看一下这张虚函数表。C++的编译器应该是保证虚函数表的指针存在于对象实例中最前面的位置(这是为了保证取到虚函数表的有最高的性能——如果有多层继承或是多重继承的情况下)。 这意味着我们通过对象实例的地址得到这张虚函数表,然后就可以遍历其中函数指针,并调用相应的函数。

5.5.6 常见问题

为什么类的静态成员函数不能为虚函数:
如果定义为虚函数,那么它就是动态绑定的,也就是在派生类中可以被覆盖的,这与静态成员函数的定义(:在内存中只有一份拷贝;通过类名或对象引用访问静态成员)本身就是相矛盾的。

为什么构造函数不能为虚函数:
因为如果构造函数为虚函数的话,它将在执行期间被构造,而执行期则需要对象已经建立,构造函数所完成的工作就是为了建立合适的对象,因此在没有构建好的对象上不可能执行多态(虚函数的目的就在于实现多态性)的工作。在继承体系中,构造的顺序就是从基类到派生类,其目的就在于确保对象能够成功地构建。构造函数同时承担着虚函数表的建立,如果它本身都是虚函数的话,如何确保 vtbl 的构建成功呢? 注意:当基类的构造函数内部有虚函数时,会出现什么情况呢?结果是在构造函数中,虚函数机制不起作用了,调用虚函数如同调用一般的成员函数一样。当基类的析构函数内部有虚函数时,又如何工作呢?与构造函数相同,只有“局部”的版本被调用。但是,行为相同,原因是不一样的。构造函数只能调用“局部”版本,是因为调用时还没有派生类版本的信息。析构函数则是因为派生类版本的信息已经不可靠了。我们知道,析构函数的调用顺序与构造函数相反,是从派生类的析构函数到基类的析构函数。当某个类的析构函数被调用时,其派生类的析构函数已经被调用了,相应的数据也已被丢失,如果再调用虚函数的派生类的版本,就相当于对一些不可靠的数据进行操作,这是非常危险的。因此,在析构函数中,虚函数机制也是不起作用的。