您的位置:首页 > 编程语言 > C语言/C++

C++技术点积累(4)——继承、多态、抽象类

2015-09-16 19:47 393 查看

1、继承:

1)对于单个类来说,访问修饰符:

       public 修饰的成员变量 方法 在类的内部 类的外部都能使用;

       protected: 修饰的成员变量方法,在类的内部使用 ,在继承的子类中可用 ;其他 类的外部不能被使用;

       private: 修饰的成员变量方法 只能在类的内部使用 不能在类的外部;

2)C++中的继承方式会影响子类的对外访问属性:

       public继承:父类成员在子类中保持原有访问级别

       private继承:父类成员在子类中变为private成员

       protected继承:父类中public成员会变成protected
                                  父类中protected成员仍然为protected
                                  父类中private成员仍然为private

       private成员在子类中依然存在,但是却无法访问到。不论种方式继承基类,派生类都不能直接使用基类的私有成员 。

3)C++中的继承方式(public、private、protected)会影响子类的对外访问属性,判断某一句话,能否被访问:
             a)看调用语句,这句话写在子类的内部、外部
             b)看子类如何从父类继承(public、private、protected)
             c)看父类中的访问级别(public、private、protected)



4)派生类访问控制的结论:

        a. protected 关键字 修饰的成员变量 和成员函数 ,是为了在家族中使用 ,是为了继承;

        b. 项目开发中 一般情况下 是 public;

5)类型兼容性原则:

        a)子类对象可以当作父类对象使用——父类指针 (引用) 指向 子类对象

        b)子类对象可以直接赋值给父类对象——指针(引用)做函数参数,实参是子类指针(实例),形参却要求的是父类指针(实例)

       c)子类对象可以直接初始化父类对象——可以用子类对象直接   初始化   父类对象,子类就是一种特殊的父类

       d)父类指针可以直接指向子类对象

       e)父类引用可以直接引用子类对象

       在替代之后,派生类对象就可以作为基类的对象使用,但是只能使用从基类继承的成员。类型兼容规则是多态性的重要基础之一。

6)继承中的构造析构调用原则:
       在子类对象构造时,需要  调用父类构造函数  对其  继承得来的成员  进行初始化;
       在子类对象析构时,需要  调用父类析构函数  对其  继承得来的成员  进行清理;
       子类对象在创建时会首先调用父类的构造函数,父类构造函数执行结束后,执行子类的构造函数,当父类的构造函数有参数时,需要在子类的初始化列表中显示调用;
       析构函数调用的先后顺序与构造函数相反;

7)继承与组合混搭情况下,构造和析构调用原则: 

    (类的组合其实就是“包含”,A类里面有一个数据成员的B类)
         原则:先构造父类(父之父),再构造成员变量(组合类的对象),最后构造自己
                     先析构自己,                   再析构成员变量(组合类的对象),最后析构父类(父之父)

继承与组合混搭情况例子:

#include <iostream>
using namespace std;

class Object  //祖宗类
{
public:
Object(int a, int b)
{
this->a = a;
this->b = b;
cout<<"object构造函数 执行 "<<"a "<<a<<"   b "<<b<<endl;
}
~Object()
{
cout<<"object析构函数 \n";
}
protected:
int a;
int b;
};

class Parent : public Object  //父类
{
public:
Parent(char *p) : Object(1, 2)
{
this->p = p;
cout<<"父类构造函数..."<<p<<endl;
}
~Parent()
{
cout<<"析构函数..."<<p<<endl;
}

void printP(int a, int b)
{
cout<<"我是爹..."<<endl;
}

protected:
char *p;

};

class child : public Parent    //子类
{
public:
child(char *p) : Parent(p) , obj1(3, 4), obj2(5, 6)        //对象初始化列表
{
this->myp = p;
cout<<"子类的构造函数"<<myp<<endl;
}
~child()
{
cout<<"子类的析构"<<myp<<endl;
}
void printC()
{
cout<<"我是儿子"<<endl;
}
protected:
char *myp;
Object obj1;  //组合了Object类
Object obj2;
};

void objplay()
{
child c1("继承测试");
}
void main()
{
objplay();   //构造祖宗类,构造父类,构造子类中的祖宗对象、父对象,构造子类
cout<<"hello..."<<endl;
system("pause");
return ;
}




8)虚继承:如果一个派生类从多个基类派生,而这些基类又有一个共同的基类,则在对该基类中声明的名字进行访问时,可能产生二义性;如果在多条继承路径上有一个公共的基类,那么在继承路径的某处汇合点,这个公共基类就会在派生类的对象中产生多个基类子对象;要使这个公共基类在派生类中只产生一个子对象,必须对这个基类 声明为虚继承,使这个基类成为虚基类。虚继承声明使用关键字——virtual。——解决下图第一种



       注意:实际开发经验抛弃多继承,工程开发中真正意义上的多继承是几乎不被使用的,多重继承带来的代码复杂性远多于其带来的便利,多重继承对代码维护性上的影响是灾难性的。

       多重继承接口不会带来二义性和复杂性等问题  ,多重继承可以通过精心设计用单继承和接口(纯虚函数抽象类定义一套接口)来代替,接口类只是一个功能说明,而不是功能实现,子类需要根据功能说明定义功能实现。

2、多态:



1)多态成立的三个条件

    1) 要有继承;

    2) 要有虚函数重写;

    3) 要有父类指针(父类引用)指向子类对象(或传递参数);——想一下函数传参(兼容性原则)、设计模式、框架等的好处

多态是设计模式的基础,多态是框架的基础。  

2)迟绑定——http://blog.csdn.net/songshimvp1/article/details/46893677——C++的动态绑定和静态绑定

3)虚析构函数目的是为了当用一个基类的指针删除一个派生类的对象时,派生类的析构函数会被调用。

      当然,并不是要把所有类的析构函数都写成虚函数。因为当类里面有虚函数的时候,编译器会给类添加一个虚函数表,里面来存放虚函数指针。所以,只有当一个类被用来作为最高的基类的时候,才把析构函数写成虚函数。

#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;

class A    //最高基类
{
public:
A()
{
p = new char[20];
strcpy(p, "obja");
printf("A()\n");
}
virtual ~A()     //虚析构函数
{
delete[] p;
printf("~A()\n");
}
private:
char *p;
};

class B : public A
{
public:
B()
{
p = new char[20];
strcpy(p, "objb");
printf("B()\n");
}
~B()
{
delete[] p;
printf("~B()\n");
}
private:
char *p;
};

class C : public B
{
public:
C()
{
p = new char[20];
strcpy(p, "objc");
printf("C()\n");
}
~C()
{
delete[] p;
printf("~C()\n");
}
private:
char *p;
};

//不使用虚析构函数
//下面的程序不会表现成 多态 这种属性,只执行了 父类的 析构函数

//想通过 父类指针 把 所有的子类对象的析构函数 都执行一遍
//想通过 父类指针 释放所有的子类资源
//使用 虚析构函数 来实现
void howtodelete(A *base)
{
delete base;  //单独这句话不会表现成 多态 这种属性,需要借助虚析构
}

void main()
{
C *myC = new C;

//delete myC; //直接通过子类对象释放所有资源,能正确调用所有的析构函数(~C,~B,~A),不需要写virtual也可以实现

howtodelete(myC);//通过父类指针释放所有资源(~C,~B,~A)

cout << "hello..." << endl;
system("pause");
return;
}
4)区别重载、重写(虚函数)和重定义
#include <iostream>
using namespace std;

//重写(虚函数) 重载 重定义
//重写发生在2个类之间
//重载必须在一个类之间

//重写分为2类
//1 虚函数重写  将发生多态
//2 非虚函数重写 (重定义)

class Parent
{
//这个三个函数都是重载关系
public:
void abc()
{
printf("abc");
}

virtual void func()
{
cout << "func() do..." << endl;
}
virtual void func(int i)
{
cout << "func() do..." << i << endl;
}
virtual void func(int i, int j)
{
cout << "func() do..." << i << " " << j << endl;
}

virtual void func(int i, int j, int m, int n)
{
cout << "func() do..." << i << " " << j << endl;
}
};

class Child : public Parent
{

public:
void abc()
{
printf("child abc");
}
/*
void abc(int a)
{
printf("child abc");
}
*/
virtual void func(int i, int j)
{
cout << "func(int i, int j) do..." << i << " " << j << endl;
}
virtual void func(int i, int j, int k)
{
cout << "func(int i, int j) do.." << endl;
}
};

//重载、重写(虚函数)和重定义
void main()
{
//: error C2661: “Child::func”: 没有重载函数接受 0 个参数
Child c1;

//c1.func();
//子类无法重载父类的函数,父类同名函数将被名称覆盖
c1.Parent::func();

//想调用父类的四个函数的func
//1 C++编译器看到func名字,因子类中func名字已经存在了(名称覆盖),编译器认为这是个自己的函数.所以c++编译器不会去找父类的4个参数的func函数(被子类中相同的名字覆盖了)
//2 c++编译器只会在子类中,查找func函数,找到了两个func,一个是2个参数的,一个是3个参数的.
//4 若想调用父类的func,只能加上父类的域名::..这样去调用..
c1.func(1, 3, 4, 5);   //3 C++编译器开始报错.....   error C2661: “Child::func”: 没有重载函数接受 4 个参数

c1.func();   //错误 2 error C2661: “Child::func”: 没有重载函数接受 0 个参数
//func函数的名字,在子类中发生了名称覆盖——覆盖了;子类的函数的名字,占用了父类的函数的名字的位置
//因为子类中已经有了func名字的重载形式。。。。
//编译器开始在子类中找func函数。。。。但是没有0个参数的func函数

cout << "hello..." << endl;
system("pause");
return;
}
5)多态的实现原理

      当类中声明虚函数时,编译器会在类中生成一个虚函数表,虚函数表是一个存储类成员函数指针的数据结构,虚函数表是由编译器自动生成与维护的,virtual成员函数会被编译器放入虚函数表中,当存在虚函数时,每个对象中都有一个指向虚函数表的指针(C++编译器给父类对象、子类对象提前布局vptr指针;当进行howToPrint(Parent *base)函数是,C++编译器不需要区分子类对象或者父类对象,只需要在base指针中,找vptr指针即可。),VPTR一般作为类对象的第一个成员
,存在虚函数时,每个对象中都有一个指向虚函数表的指针(vptr指针)。







    说明1:通过虚函数表指针VPTR调用重写函数是在程序运行时进行的,因此需要通过寻址操作才能确定真正应该调用的函数。而普通成员函数是在编译时就确定了调用的函数。在效率上,虚函数的效率要低很多。

    说明2:出于效率考虑,没有必要将所有成员函数都声明为虚函数。




     加上virtual关键字 c++编译器会增加一个指向虚函数表的指针 。对象在创建的时侯,由编译器对VPTR指针进行初始化 ;只有当对象的构造完全结束后VPTR的指向才最终确定;父类对象的VPTR指向父类虚函数表;子类对象的VPTR指向子类虚函数表。



6)父类指针和子类指针的步长

结论:父类p++与子类p++步长不同;不要混搭,不要用父类指针++方式操作数组。
class Parent
{
public:
Parent(int a = 0)
{
this->a = a;
}

virtual void print()
{
cout << "我是爹" << endl;
}
private:
int a;
};

class Child : public Parent
{
public:
Child(int b = 0) :Parent(0)
{
this->b = b;
}
virtual void print()
{
cout << "我是儿子" << endl;
}
private:
int b;
};

void main()
{
Parent *pP = NULL;
Child  *pC = NULL;

Child  array[] = { Child(1), Child(2), Child(3) };

pP = array;
pC = array;
pP->print();
pC->print(); //多态发生

pP++;
pC++;
pP->print();
pC->print(); //多态发生

pP++;
pC++;
pP->print();
pC->print(); //多态发生

cout << "hello..." << endl;
system("pause");
return;
}


             


     程序自动宕机,从上图可以看出在基类指针++以后,指向内存的出错!

3、纯虚函数和抽象类——http://blog.csdn.net/songshimvp1/article/details/46894439

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  C++ 继承 多态