C++ 继承与多态(一)

面向对象编程基于三个基本概念:数据抽象、继承和动态绑定。C++中用类进行数据抽象;用类派生一个类使得派生类继承基类的成员。动态绑定是编译器能够在运行时确定是使用基类中定义的函数还是派生类中定义的函数。

面向对象编程的关键思想是 :多态性。之所以称通过继承而相关联的类型为多态类型,是因为在很多情况下可以互换地使用派生类或者基类型的“许多形同”。在C++中,多态性仅用于通过继承而相关联的类型的引用或指针。

一、继承

1、通过继承我们可以定义派生类,这个派生类可以继承基类的成员方法和成员变量。

     派生类的大小不仅要计算自己本身成员变量的大小,也要算入其基类的成员变量的大小

     派生类用class定义,默认是私有继承;用struct定义,默认是共有继承

2、继承的本质:代码复用

3、继承的三种方式:public、private、protected

      public、private在C++之前的学习中一直在用到。protected是第一次见,那么protected访问标号是什么样的呢?

      protected是在继承中常用的一种访问限定符,可以认为protected访问标号是private和public的混合:

     ①像private成员一样,protected成员不能被类的用户访问

     ②像public成员一样,protected成员可以被该类的派生类访问

     protected重要性质:派生类只能通过派生类对象访问其基类的protected成员,派生类对其基类类型对象的protected成员                                                没有特殊访问权限。

4、派生类中,从基类继承来的成员的访问限定是什么?

                                    

通过表格我们可以总结:

①只有在公有继承下,基类的public成员在派生类中和主函数中都可以访问。

②所有继承方式下,基类的private成员在派生类中和主函数中都不可访问。

5、派生类怎么初始化从基类继承来的成员呢?

一开始想到的肯定就是与派生类本身的成员一样,在构造函数初始化列表直接初始化。但是这样正确吗?自己可以试着ctrl+F5跑一下,你就会发现报的错误是:没有合适的默认构造函数可用。

当然正确初始化从基类继承来的成员的方式是:通过调用基类相应的构造函数进行初始化,派生类只会初始化自己的成员

class Base
{
public:
	Base(int a) :ma(a) { cout << "Base()" << endl; }
	~Base() { cout << "~Base()" << endl; }
protected:
	int ma;
};
class Derive : public Base
{
public:
	//调用基类构造函数初始化基类成员
	Derive(int data) :Base(data), mb(data) { cout << "Derive()" << endl; }
	~Derive() { cout << "~Derive()" << endl; }
private:
	int mb;
};
int main()
{
	Derive d(10);
	return 0;
}

通过代码运行结果我们可以发现基类和派生类的构造顺序:先构造基类再构造派生类,析构先析构派生类再析构基类

6、派生类和基类方法或者变量可以重名吗?

class Base
{
public:
	Base(int a) :ma(a) { cout << "Base()" << endl; }
	~Base() { cout << "~Base()" << endl; }
	void show() { cout << "Base::show()" << endl; }
	void show(int i) { cout << "Base::show(int)" << endl; }
protected:
	int ma;
};
class Derive : public Base
{
public:
	Derive(int data) :Base(data), mb(data) { cout << "Derive()" << endl; }
	~Derive() { cout << "~Derive()" << endl; }
	void show(){cout << "Derive::show()" << endl;}
private:
	int ma;
	int mb;
};
int main()
{
	Derive d(10);
	//d.show(20); //error,默认调用的是派生类的show()
	d.show();//默认调用派生类的show()
	d.Base::show();//若想访问基类的show(),可以在函数名前加::
	return 0;
}

可以看一下代码运行结果: 

 

 先调用构造,然后d.show()调用的是派生类的show(); d.Base::show()则调用的是基类的show(); 

所以基类和派生类的方法或者变量可以重名,因为各自有各自的作用域。

7、重载、隐藏、覆盖分别是怎样的关系?

8、 继承结构是默认的从上到下的结构,以下四种方式是否正确? 

    ① 基类对象 -----赋值给-----> 派生类对象
    ② 派生类对象 -----赋值给-----> 基类对象
    ③ 基类指针/引用 -----指向-----> 派生类对象
    ④ 派生类指针/引用 -----指向-----> 基类对象      

  • 重载:函数名,参数列表,作用域都相同为重载
  • 隐藏:基类和派生类当中,函数名相同,那么称为隐藏
  • 覆盖:又称重写,是指在基类和派生类当中,函数的返回值,函数名,参数列表都相同,而且基类的函数是virtual虚函数,             那么称之为覆盖关系
class Base
{
public:
	Base(int a) :ma(a) { cout << "Base()" << endl; }
	~Base() { cout << "~Base()" << endl; }
	void show() { cout << "Base::show()" << endl; }
	void show(int i) { cout << "Base::show(int)" << endl; }
protected:
	int ma;
};
class Derive : public Base
{
public:
	Derive(int data) :Base(data), mb(data) { cout << "Derive()" << endl; }
	~Derive() { cout << "~Derive()" << endl; }
	void show() {cout << "Derive::show()" << endl;}
private:
	int mb;
};
int main()
{  
	Base b(10);//基类对象
	Derive d(20);//派生类对象
	//d = b;  // 派生类对象 = 基类对象    error
	b = d;  // 基类对象 = 派生类对象     ok

    // 通过基类指针只能访问从基类继承来的成员变量或成员方法
	Base *pb = &d; // 基类指针/引用 = 派生类对象   ok
	pb->show(10);

	//Derive *pd = &b; // 派生类指针/引用 = 基类对象   error
	//pd->show();  // mb

	return 0;
}

 

根据代码可以得到:

   基类对象 --赋值给--> 派生类对象          no      上 --> 下
   派生类对象 --赋值给--> 基类对象          yes     下 --> 上
   基类指针/引用 --指向--> 派生类对象      yes    下 --> 上        
   派生类指针/引用 --指向--> 基类对象      no      上 --> 下

所以我们可以得到,在继承结构中,默认支持下到上的类型转换

9、请解释静态绑定和动态绑定

  •    静态绑定:编译时确定函数调用,类中此函数不是虚函数
  •    动态绑定:运行时确定函数调用,类中此函数是虚函数,对象调用虚函数时执行动态绑定
class Base
{
public:
	Base(int a) :ma(a) { cout << "Base()" << endl; }
	~Base() { cout << "~Base()" << endl; }
	void show() { cout << "Base::show()" << endl; }
	void show(int i) { cout << "Base::show(int)" << endl; }
protected:
	int ma;
};
class Derive : public Base
{
public:
	Derive(int data) :Base(data), mb(data) { cout << "Derive()" << endl; }
	~Derive() { cout << "~Derive()" << endl; }
	void show() { cout << "Derive::show()" << endl; }
private:
	int mb;
};
int main()
{
	Derive d(20);
	
	Base *pb = &d;
	pb->show();//调用基类Base的show(),因为指针类型为Base
	
	cout << sizeof(Base) << endl;  // 4 
	cout << sizeof(Derive) << endl; // 8

	cout << typeid(pb).name() << endl;  // Base* 
	cout << typeid(*pb).name() << endl; // Base  
	return 0;
}

 我们可以根据代码运行结果得到:首先调用构造函数;然后因为Base()类型的指针指向,所以调用Base的show();然后基类和派生类的大小;然后pb是Base*类型,*pb是Base类型的。这就相当于:

int main()
{
    int a = 10;
    int *p = &a;
    /*
    如果问p是什么类型的,int*
    如果问*p是什么类型的,int
    */
}

如果我们将基类和派生类里的show()都改为虚函数,结果会怎么样?

class Base
{
public:
	Base(int a) :ma(a) { cout << "Base()" << endl; }
	~Base() { cout << "~Base()" << endl; }
	virtual void show() { cout << "Base::show()" << endl; }
	virtual void show(int i) { cout << "Base::show(int)" << endl; }
protected:
	int ma;
};
class Derive : public Base
{
public:
	Derive(int data) :Base(data), mb(data) { cout << "Derive()" << endl; }
	~Derive() { cout << "~Derive()" << endl; }
	virtual void show() { cout << "Derive::show()" << endl; }
private:
	int mb;
};
int main()
{
	Derive d(20);	
	Base *pb = &d;
	pb->show();//pb指向的是派生类的show()

	cout << sizeof(Base) << endl;  //8
	cout << sizeof(Derive) << endl; //12

	cout << typeid(pb).name() << endl;  //Base*
	cout << typeid(*pb).name() << endl; //Derive
	return 0;
}

根据代码运行结果,别的就不用多作解释,为什么基类和派生类求sizeof的结果都多了四个字节呢?为什么pb调用了派生类的show()?为什么*pb的类型变成了Derive了?

要解释这几个问题,我们首先应该意识到是因为增加了虚函数使得代码运行结果发生了改变,可是这是怎么改变的?

       如果一个类包含虚函数,那么在编译阶段,该类型会产生一个对应的虚函数表,存虚函数的地址。虚函数表的类型与类的类型有关,一个类只有一个虚函数表。虚函数表为全局静态类型,存于.rodata段。一个类里虚函数的多少不会影响类的大小,只会影响到虚函数表的大小。派生类从基类继承到虚函数之后,同样也会产生一个虚函数表,派生类与基类同名的成员方法也会被置为虚函数,从而在派生类的虚函数表中覆盖掉基类的该同名的虚函数。

       因为要产生虚函数表来存储虚函数的地址,所以需要一个指针指向虚函数表的起址。这时编译器会自动往类内添加这个指针,也就是图中的*vfptr。同样也是因为这个指针,所以基类和派生类求sizeof的结果都多了四个字节。

       因为派生类和基类的show()方法重名,所以在派生类的虚函数表中派生类的show()方法覆盖掉了基类的show()方法。而pb指向的是派生类对象的地址,所以调用就变成了调用派生类的show()方法。

        pb的类型取决于当前运行的类型,也就是取决于RTTI(Run-Time Type Information)。那么怎么判定是RTTI类型,还是静态(编译时期)类型?   如果类中有虚函数,就会被识别为RTTI(运行时的类型信息)类型。如果没有虚函数就被识别为静态类型。RTTI指针存储在虚函数表vftable当中,指向一段类型字符串,也就是当前的类名。

10、什么是多态?
   静态(编译阶段)的多态:函数重载和模板
   动态(运行阶段)的多态:虚函数
   多态的好处:可以用统一的函数接口指针接收,然后调用同名覆盖方法可以区分参数是来自哪个派生类然后调用不同的派生                           类的方法。

11、基类一般不代表任何实体(只保留派生类的一些公有属性,提供一个接口)所以一般不实例化对象,给派生类保留公共的覆盖接         口(重写)和公共的属性(复用)

12、

class Window
{
public:
	Window(int t = 0) 
	{ a = t; cout << "window" << endl; }
	virtual void onResize() 
	{ a = 10;  cout << "call Window::onResize" << endl; }
	int a;
};

class SpecialWindow :public Window
{
public:
	virtual void onResize()
	{
		((Window)(*this)).onResize();
		cout << "call SpecialWindow::onResize" << endl;
		cout << Window::a << endl;
	}
};
int main()
{
	SpecialWindow sw;  //  8  vfptr  a
	sw.onResize();

	return 0;
}

这段代码中,如果使用派生类的对象调用onResize()函数,为什么a的值没有被改变?

//((Window)(*this)).onResize(); 
/*
  将当前指针类型强制转换为Window类型,生成一个临时对象,
  而a的值是在临时对象调用的方法里修改的,所以真正a的值
  并没有被修改
*/

13、有纯虚函数的类称为 “抽象类”,多重继承称为虚基类。抽象类:不能实例化对象,但是可以定义指针或者引用。

class Animal
{
public:
	Animal(string name) :_name(name) {}
	virtual void bark() = 0;// 纯虚函数
protected:
	string _name;
};

class Cat : public Animal
{
public:
	Cat(string name):Animal(name){}
	void bark() { cout << _name << " bark:喵喵!" << endl; }
};
class Dog : public Animal
{
public:
	Dog(string name) :Animal(name) {}
	void bark() { cout << _name << " bark:旺旺!" << endl; }
};
int main()
{
	Animal *p1 = new Cat("猫");
	Animal *p2 = new Dog("二哈");
	int *p11 = (int*)p1;
	int *p22 = (int*)p2;
	int tmp = p11[0];
	p11[0] = p22[0];
	p22[0] = tmp;
	p1->bark();
	p2->bark();
	cout << typeid(*p1).name() << endl;
	return 0;
}

 首先用抽象类定义了两个指针p1,p2指向构造了两个派生类各自对应的对象的地址。*p11和*p22也分别是指向两个对象的地址。因为基类中有纯虚函数,所以在两个派生类中都有各自的虚函数和虚函数表。而两个派生类内存中的前4个字节都存放的是指向各自虚函数表的指针,所以定义tmp和之后的交换,是将两个指针指向的虚函数表的地址做了交换。从而导致打印出这样的结果,而当前的类型是class Dog是因为RTTI判断出当前运行时的状态是派生类Dog在运行。

猜你喜欢

转载自blog.csdn.net/Disremembrance/article/details/89416475
今日推荐