c++中的类(class)-----笔记(类多态)

1,多态是一种运行期绑定机制,通过这种机制,实现将函数名绑定到函数具体实现代码的目的。一个函数的名称与其入口地址是紧密相连的,入口地址是该函数在内存中的起始地址。如果对一个函数的绑定发生在运行时刻而非编译时刻,我们就称该函数是多态的。

 

2,C++多态的三个前提条件:(a)必须存在一个继承体系结构;(b)继承体系结构中的一些类必须具有同名的 virtual 成员函数(virtual 是关键字);(c)至少有一个基类类型的指针或者基类类型的引用可用来对 virtual 成员函数进行调用。

 1 #include<iostream>
 2 #include<string>
 3 using namespace std;
 4 
 5 class TradesPerson {
 6     public:
 7         virtual void sayHi() {
 8             cout<<"Just hi."<<endl;
 9         }
10 }; 
11 
12 class Tinker : public TradesPerson {
13     public:
14         virtual void sayHi() {
15             cout<<"Hi,I tinker."<<endl;
16         }
17 };
18 
19 class Tailor : public TradesPerson {
20     public:
21         virtual void sayHi() {
22             cout<<"Hi,I Tailor."<<endl;
23         }
24 };
25 
26 int main() {
27     TradesPerson *p;
28     int which;
29     
30     do {
31         cout<<"1 == TradesPerson, 2 == Tinker, 3 == Tailor"<<endl;
32         cin>>which;
33     }while(which<1 || which > 3);
34     
35     switch(which) {
36         case 1 : p = new TradesPerson; break;
37         case 2 : p = new Tinker; break;
38         case 3 : p = new Tailor; break;
39     }
40     p->sayHi();
41     delete p;
42     
43     return 0;
44 }
多态使用示例

 

3,因为基类类型的指针可以指向任何基类对象或派生类对象,所以上面程序我们并不需要强制类型转换。在上面的代码中,每个函数都使用了关键字 virtual ,但实际上并没有必要,因为当声明了基类的一个成员函数为虚函数后,那么即使该成员函数没有在派生类中被显式地声明为虚函数,但它在所有派生类中也将自动成为虚函数。在上面代码中,只是 TradesPerson 中声明为虚函数也是可以的,因为 Tinker::sayHi 仍然是虚函数,因为它与 TradesPerson::sayHi 具有相同的函数签名,而 Tradesperson::sayHi 是在基类中声明的虚函数。

 

4,如果虚函数在类声明之外定义,关键字 virtual 仅在函数声明时需要,不需在函数定义中使用 virtual 关键字。C++ 仅允许将成员函数定义为虚函数,顶层函数不能为虚函数。

 

5.1,C++ 使用 vtable (虚成员函数表)来实现虚成员函数的运行期绑定。虚成员函数表存在的用途是支持运行时查询,使得系统可以将某一函数名绑定到虚成员函数表中的特定入口地址。虚成员函数表的实现是与系统无关的。

 1 class B {
 2     public:
 3         virtual void m1() { /*    */}
 4         virtual void m2() { /*    */}
 5 };
 6 
 7 class D : public B {
 8     public:
 9         virtual void m1() { /*    */}   // override m1
10 };
vtable 示例
虚成员函数 入口地址示例 虚成员函数 入口地址示例
   B::m1     0x7723     D::m1     0x99a7
   B::m2     0x23b4     D::m2     0x23b4

 

 

 

 

 在虚成员函数表中,对应于程序中的每一个虚成员函数,都有一个单独的入口地址,我们发现 B::m2 和 D::m2 有相同的入口地址(0x23b4),这是因为派生类 D 没有覆盖成员函数 m2,而是直接继承了其基类的 B。如果执行以下代码:

1 int main() {
2     B b;
3     D d;
4     B* p;
5     //...        // p is set to b's or d's address
6     p->m1();     // vtable lookup for run-time binding
7     //...
8 }
vtable 使用示例

上述代码中,p->m1(); 绑定到虚成员函数表中的某一项,系统首先决定指针 p 指向哪个对象,如果 p 指向 B 的对象 b,系统将在虚成员函数表中查询 B::m1 的表项;如果指向 d,系统将在虚成员函数表中查询 D::m1 的入口地址,一旦查询完成,就可以执行相应的函数体。

note: 使用动态绑定的程序会影响效率,因为虚成员函数需要额外的存储空间,而且对虚成员函数表进行查询也需要额外的时间。

5.2,对象中的vptr指针什么时候初始化?

 1 #include <iostream>
 2 
 3 using namespace std;
 4 
 5 class Parent{
 6 public:
 7     Parent(int a=0){
 8         this->a = a;
 9         print();
10     }
11     virtual void print(){ cout << "Parent" <<endl;}
12 private:
13     int a;
14 };
15 
16 class Son:public Parent{
17 public:
18     Son(int a=0,int b=0):Parent(a){
19         this->b = b;
20         print();
21     }
22     virtual void print(){ cout << "Son" <<endl;}
23 private:
24     int b;
25 };
26 
27 int main(){
28     Son s;
29     return 0;
30 }
31 
32 // 输出结果
33 // Parent
34 // Son
虚表指针初始化过程

结论:Son s 的过程:

1)初始化 s.vptr 指针,初始化时分步;

2)  当执行父类的构造函数时,s.vptr 指向了父类的虚函数表,当父类构造函数运行完毕后,会把 s.vptr 指针指向子类的虚函数表

3)结论:子类的 s.vptr 指针初始化分步完成

 

6,构造函数和析构函数:构造函数不能是虚成员函数,但析构函数可以是虚成员函数。

7,虚析构函数:举一个例子来说明虚析构函数的重要性:

 1 #include<iostream>
 2 #include<string>
 3 using namespace std;
 4 
 5 class A {
 6     public:
 7         A() {
 8             cout<<endl<<"A() firing"<<endl;
 9             p = new char[5];             // allocate 5 bytes
10         }
11         ~A() {
12             cout<<"~A() firing"<<endl;
13             delete[] p;
14         }
15     private:
16         char* p;
17 };
18 
19 class Z : public A {
20     public:
21         Z() {
22             cout<<"Z() firing"<<endl;
23             q = new char[500];        // allocate 500 bytes
24         }
25         ~Z() {
26             cout<<"Z() firing"<<endl;
27             delete[] q;
28         }
29     private:
30         char* q; 
31 };
32 
33 void f() {
34     A* ptr;
35     ptr = new Z();
36     delete ptr;
37 }
38 
39 int main() {
40     for(int i=0;i<3;++i)
41         f();
42     return 0;
43 }
44 
45 /* output
46 
47 A() firing
48 Z() firing
49 ~A() firing
50 
51 A() firing
52 Z() firing
53 ~A() firing
54 
55 A() firing
56 Z() firing
57 ~A() firing
58 */
内存泄漏示例

在上述代码中,当我们通过 ptr 进行 delete 操作时,尽管 ptr 实际指向一个 Z 对象,但只有 ~A() 被调用,这是因为它们的析构函数不是虚成员函数,所以编译器实施的是静态绑定。编译器根据ptr 的数据类型 A* 来决定调用哪一个析构函数,因此,仅调用了~A(),而没有调用 ~Z(),这样就会造成内存泄漏。通过定义基类的析构函数 ~A() 为虚成员函数可以确保其派生类的析构函数也为虚成员函数。当通过 ptr 来删除其所指的对象时,编译器进行的是运行期绑定。在这里,因为 ptr 指向一个 Z 类型的对象,所以 ~Z() 被调用,接着 ~A() 也被调用,这是因为析构函数的调用是沿着继承树自下向上延伸的。

note: 通常来说,如果基类有一个指向动态分配内存的数据成员,并定义了负责释放这块内存的析构函数,就应该将这个析构函数声明为虚成员函数,这样做可以保证在以后添加该类的派生类时发挥多态性的作用。

 

8,对象成员函数和类成员函数:只有非静态成员函数才可以是虚成员函数,换句话说,只有对象成员函数才可以是虚成员函数。

1 class C {
2     public:
3         static virtual void f();  // ERROR: static and virtual
4         static void g();   // OK
5         virtual h();      // OK
6 };
错误示例

试图使一个成员函数既定义为虚成员函数又为静态函数,这是不允许的。

 

9,重载:在一个类中,成员函数可以有相同的函数名,只要它们的函数签名不同即可,我们将这种情况成为重载。重载与编译期绑定相对应,不管是成员函数还是顶层函数。编译器依据函数签名来进行绑定。在进行重载时,总是使用编译器绑定,在这个方面重载函数(不管是成员函数还是顶层函数)和虚函数是截然不同的,虚函数总是在运行期绑定。

 1 class C {
 2     C() { /*  */ }
 3     C( int x ) { /*   */}
 4 };
 5 
 6 void f(double d) { /*  */}
 7 void f(char c) { /*   */}
 8 
 9 int main() {
10     C c1;
11     C c2(26);
12     f( 3.14 );
13     f('z');
14     //...
15 }
重载函数使用示例

 

10,覆盖:假定基类 B 有一个成员函数 m,其派生类 D 也有一个具有相同函数签名的成员函数 m,如果这个成员函数是虚函数,则任何通过指针或引用对 m 的调用都会激活运行期绑定。对这种情况,我们称派生类的成员函数 D::m 覆盖了其基类的成员函数 B::m。如果成员函数不是虚函数,都 m 的任何调用均为编译器绑定。

 1 class B {
 2     public:
 3         void m() { cout<<"B::m"<<endl;}
 4 };
 5 
 6 class D : class B {
 7     public:
 8         void m() { cout<<"D::m"<<endl;}
 9 };
10 
11 int main() {
12     B* p;
13     p = new D;
14     p->m();   // invoke m
15     return 0;
16 }
覆盖示例

上述代码中,因为 m 不是虚函数,而在 C++ 中只有虚函数才会进行运行期绑定。编译器会使用 p 的数据类型 B* 进行绑定,结果是绑定到 B::m 。上述调用相当于 p->B::m。

 

11,遮蔽:假定基类拥有一个非虚函数 m ,其派生类 D 也有一个成员函数m,我们就说函数 D:m 遮蔽了继承而来的函数 B::m。如果派生类的同名成员函数与其基类的这个成员函数有不同的函数签名,那么这种遮蔽情况会相当复杂。

 1 class A {
 2     public:
 3         void m( int x ) { cout<<x<<endl; }
 4 };
 5 
 6 class C : public A {
 7     public:
 8         void m() { cout<< "Hi"<<endl; }
 9 };
10 
11 int main() {
12     C c1;
13     c1.m();
14 //    c1.m(26);  // [Error] no matching function for call to 'C::m(int)'
15     
16     return 0;
17 }
遮蔽示例

上面的程序将产生一条严重编译错误,因为 D 又定义了一个同名函数,因此 D 的本地函数 D::m 遮蔽了继承而来的函数 B::m 。要调用这个继承而来的带单个参数的基类函数 B::m,必须修改成以下格式:c.A::m(26);  

note:虚函数和非虚函数都有可能产生名字遮蔽,实际上一旦派生类的虚函数不能覆盖基类的虚函数,就会产生虚函数遮蔽。将函数定义为虚函数并不能消除遮蔽现象,改正的办法就是显式调用 B::m;这样做虽然消除了编译错误,但不是好的编译风格,为了发挥多态的作用,B::m 和 D::m 应该具有相同的函数签名,而不仅是具有相同的函数名。

 1 class B {
 2     public:
 3         virtual void m(int x) { cout<<x<<endl;}
 4 };
 5 
 6 class D : public B {
 7     public:
 8         virtual void m() { cout<<"Hi"<<endl;}
 9 };
10 
11 int main() {
12     D d1;
13     d1.m();
14 //    d1.m(26);    //  Error: D's m takes no arguments
15     
16     return 0;
17 }
虚函数遮蔽示例

 

12,名字共享:(a)顶层函数重载;(b)构造函数重载;(c)多态中的相同函数签名的函数。但是在类层次中共享函数名但函数签名不同时,将产生遮蔽,而遮蔽是非常危险的,建议要谨慎地运用这种遮蔽类型的名字共享机制。

 

13,抽象基类:抽象基类确保其派生类必须定义某些指定的函数,否则这个派生类就不能被实例化。要求 该类必须拥有一个纯虚成员函数,在纯虚成员函数声明的结尾加上 =0 就可以将这个函数定义为纯虚成员函数。

1 class ABC {
2     public:
3         virtual void open() = 0;
4 };
纯虚成员函数

 

14,虽然不能创建一个抽象基类的对象,但抽象基类可以拥有派生类,从抽象基类派生来的类必须覆盖基类的所有纯虚成员函数,否则派生类也成为抽象基类,因而也不能用来创建对象。一个抽象基类可以有其他不是纯虚成员函数或甚至不是虚函数的成员函数,还可以有数据成员。抽象基类的成员可以是 private、protected 或 public。

 

15,定义纯虚成员函数的限制:只有虚函数才可以成为纯虚成员函数,非虚函数或顶层函数都不能声明为纯虚成员函数。

1 void f() = 0; // ERROR : not a virtual method
2 
3 class {
4     public:
5         void open = 0; // ERROR: not a virtual method
6 }; 
纯虚函数错误定义示例

 

16,抽象基类作用:通过这种机制,可以用来指明某些虚函数必须被派生类覆盖,否则这些派生类就不能拥有对象。从这种意义上看,抽象基类实际上定义了一个公共接口,这个接口被所有从抽象基类派生的类共享。因为抽象基类通常只有 public 成员函数,所以经常使用关键字 struct(默认为 public ) 来声明抽象基类。

 

17,运行期类型识别(RTTI):一个基类指针不经过明确的转型操作,就能指向基类对象或派生类对象,反过来就不大一样了,将一个派生类指针指向基类对象是一种不明智的做法。当然,通过明确的转型操作可以做到这一点:

 1 class B {
 2     //...
 3 };
 4 
 5 class D : public B {
 6     //...
 7 };
 8 
 9 int main() {
10     D* p;
11     p = new B;  // ERROR: explicit cast needed
12     p = static_cast<D*>(new B);   // caution
13     //...
14     return 0;
15 }
static_cast 用法示例

上述这种用法是合法的,但这种转型操作相当危险,可能会造成难以跟踪的运行期错误。static_cast 不能保证类型安全(type safely)。如果 p 不小心指向了一个没有定义 m 的 B 对象,将会导致错误。

 1 class B {
 2     f() { }  // Note: no method m
 3 };
 4 
 5 class D : public B {
 6     void m() { }  // not in base class
 7 };
 8 
 9 int main() {
10     D* p;
11     p = static_cast<D*>(new B);
12     p->m();     // ERROR: there is no B::m 
13     return 0;
14 }
类型不安全使用示例

 

18,C++ 提供的 dynamic_cast 操作符可以在运行期检测某个转型动作是否类型安全。dynamic_cast 和 static_cast 有同样的语法,不过 dynamtic_cast 仅对多态类型(至少有一个虚函数的类)有效。

 1 class B {
 2     virtual f() { }  // Note: no method m
 3 };
 4 
 5 class D : public B {
 6     void m() { }  // not in base class
 7 };
 8 
 9 int main() {
10     D* p;
11     p = dynamic_cast< D* >(new B);
12     if(p)  // 如果转型动作安全,返回指向的对象指针 ptr 
13         p->m();
14     else   // 如果转型动作不安全,返回 false 
15         cout<<"Not safe for p to point to a B"<<endl;
16 
17     return 0;
18 }
转换类型动作检查是否安全

 

19,dynamic_cast 的规则:假定基类 B 具有多态性,而类 D 是直接或间接从类 B 派生而来的。通过继承,类 D 也因此具有多态性,在这种情况下:

  (a)从派生类 D* 到基类 B* 的dynamic_cast 可以进行,这称为 向上转型(upcast)。

  (b)从基类 B* 到派生类 D* 的dynamic_cast 不能进行,这称为 向下转型(downcast)

  假定类 A 和类 Z 都具有多态性,但它们之间不存在继承关系,在这种情况下,相互的转型均不能进行。

 

20,typeid 用法:操作符 typeid 返回一个 type_info 类对象的引用,type_info 是一个系统类,用来描述类型,这个操作符可以施加于类型名(包括类名)或 C++ 表达式。

1 #include<typeinfo>
2 
3 int main() {
4     float x;
5     long y;
6     bool result = typeid(y) == typeid(x);
7     cout<< boolalpha << result <<endl;
8     return 0; 
9 }
typeid 使用示例

 

posted on 2019-03-04 20:28  爱笑的张飞  阅读(2560)  评论(0编辑  收藏  举报

导航