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、重载、隐藏、覆盖分别是怎样的关系?

  • 重载:函数名,参数列表,作用域都相同为重载
  • 隐藏:基类和派生类当中,函数名相同,那么称为隐藏
  • 覆盖:又称重写,是指在基类和派生类当中,函数的返回值,函数名,参数列表都相同,而且基类的函数是virtual虚函数,             那么称之为覆盖关系

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

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

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在运行。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
设计一个图书管理系统,可以采用面向对象的思想,使用C++语言来实现,以下是一个简单的设计思路: 1. 首先,创建一个基类 Book,包含一些共同的属性和方法,例如书名、作者、出版社、价格等属性,以及借阅、归还等方法。 2. 然后,创建派生类 FictionBook 和 NonFictionBook,分别表示小说类和非小说类图书。这两个派生类继承了基类的属性和方法,并可以添加自己特有的属性和方法。 3. 接着,创建一个管理图书的类 BookManager,用于添加、删除和查询图书信息等操作。这个类中可以包含一个 Book 类型的数组或链表,用于存储所有的图书信息。 4. 最后,实现多性,通过虚函数实现不同类型的图书的借阅和归还操作,将这些函数定义为虚函数,让不同类型的图书派生类去实现。 下面是一个简单的类图: ``` +------------------------+ | Book | +------------------------+ | -title: string | | -author: string | | -publisher: string | | -price: double | +------------------------+ | +borrow() | | +return() | +------------------------+ /\ || || +------------------------+ | FictionBook | +------------------------+ | -genre: string | | -rating: int | +------------------------+ | +borrow() | | +return() | +------------------------+ /\ || || +------------------------+ | NonFictionBook | +------------------------+ | -subject: string | | -level: string | +------------------------+ | +borrow() | | +return() | +------------------------+ /\ || || +------------------------+ | BookManager | +------------------------+ | -books: Book[] | +------------------------+ | +addBook() | | +removeBook() | | +searchBook() | +------------------------+ ``` 这样设计的好处是,可以扩展新的图书类型,同时也方便管理和查询图书信息。同时,使用多性可以让代码更加灵活,同时也更加易于维护。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值