C++继承和多态特性

前言


c++文章连载:
1.C++基础
  1.C++基础
  2.C++新增和有变化的关键字
  3.C++的内存管理
2.面向对象
  1.C++的封装和访问权限
  2.C++继承和多态特性
  3.C++的运算符重载
  4.C++静态类和静态成员
  5.C++的友元函数和友元类
3.模板编程和STL
  1.C++模板编程入门
  2.STL的容器类和迭代器
  3.STL的泛型算法
  4.模板特化与类型萃取
  5.STL的其他容器讲解
  6.智能指针与STL查漏补缺
4.杂项
  1.c++各种流操作
  2.依赖,关联,聚合,组合,继承
  3.一些技巧性的代码设计
  
  


1.什么是面向对象的继承特性

1.1、C++类的继承语法如下:
class 派生类名:访问控制 基类名1,访问控制 基类名2,访问控制 基类名n
{
// 成员变量和成员方法列表(除了构造和析构之外的成员方法)
};
1.2、继承特性是天然的
(1)天然的意思就是:符合现实世界的本质规律,而不是纯粹人为施加的。
(2)继承inheritance和组合composition是软件重用的2种有效方式
(3)派生类,范围小,具体。 基类,范围大,抽象
(4)派生类又叫子类,基类又叫父类。

2.C++继承中的权限管控

2.1、C++类中的访问权限管控回顾
(1)public 类内部可以访问,类的外部可以访问
(2)private 类内部可以访问,类的外部不能访问
(3)protected 类内部可以访问,类的外部不能访问
2.2、继承时的三种权限设置对派生类的影响
(1)public继承(公有继承):父类成员在子类中保持原有访问级别
(2)private继承(私有继承):父类成员在子类中变为private成员
(3)protected继承(保护继承):父类中public成员会变成protected,父类中protected成员仍然为protected,父类中private成员仍然为private
(4)如果继承时不写则默认情况下派生类为class时是private继承,而派生类为struct时是public继承
2.3、设计类时如何规定成员的访问权限
(1)需要被外界访问的成员直接设置为public
(2)只能在当前类中访问的成员设置为private
(3)只能在当前类和子类中访问的成员设置为protected。

总结:
1、父类的public成员,经过public继承后,在子类中是public的。
2、父类的private成员,经过public继承后,在子类中是比private还可怜的。意思是这个成员在子类中是存在的,意思是它不能被直接访问,怎么访问?只能通过子类从父类继承而来的父类里实现了的成员函数来间接访问。
3、父类的protected成员,经过public继承后,在子类中是protected的。意思就是该成员在子类中是子类内部成员可以访问,子类对象外部不可以访问,子类再次去继承产生孙类中他还是protected的。
4、public继承,对于父类中public和protected成员的权限在子类中,其实是没有增强也没有削弱的;但是对于父类中private成员,其实在子类中是有削弱的(比private还可怜)
5、如果是private继承,那么父类中的public成员和protected成员就变成了子类中的private成员,父类中的private成员成了子类中比private还可怜的那种成员。
6、如果是protected继承,那么父类中的public成员和protected成员就变成了子类中的protected成员,父类中的private成员成了子类中比private还可怜的那种成员。
进一步总结:
1、父类的private成员,在三种继承下都会被变成在子类中是比private还可怜的这种成员
2、父类的public成员,在public继承下到子类中还是public,在protected继承下到子类中是protected的,在private继承下到子类中是private的。
3、父类的protected成员,在public继承下到子类中是protected的,在protected继承下到子类中是protected的,在private继承下到子类中是private的。
4、private:如果是父类中的private权限成员,那么在继承时会被降到比private还惨的这种;而如果是private继承,则不会导致父类中public和protected的成员降权到比private还惨的这种,只会降级到private权限。
5、在继承中父类中所有的成员(不管哪种权限)都会被继承给子类,而不会丢失任何一个。

3.继承体系下子类和父类的关系

3.1、本质上是2个独立的类
3.2、站在子类角度看继承
(1)非继承体系下的类,完全是自己“手工”构建的,所有成员根据访问权限不同而分为1个层级3大块
(2)继承体系下的子类,部分直接从父类继承,部分自己手工打造,所有成员分成2个层级(继承来的,自己写的),每个层级3大块(访问权限)
3.3、为什么父类private成员在子类中还有
(1)思考:父类的所有成员在子类中都必须有吗?有没有可能父类有一个成员其实子类没必要具备的?
(2)思考:如何做到在继承时让程序员指定父类的哪些成员参与继承,哪些不参与?
(3)选择:继承就是父类所有成员全部按分类规则传给子类
3.4、为什么父类private成员在子类中还有但不能直接访问
(2)为什么这样设计
1:如果能直接访问,那就打破了父类private的含义,破坏了class的封装性
2:父类的private成员在子类中很大可能本来就不需要去访问,所以能直接访问反而有风险,没必要
3:间接访问合乎一个原则:在哪里定义的成员变量在哪里操作。子类和父类不是一个人写的时尤其要这样,避免问题

4.派生类和基类的构造析构关系

4.1、派生类并不继承基类的构造和析构函数,只继承成员变量和普通成员方法
(1)不继承,意思是派生类中确实没有,不包含基类的构造和析构函数
(2)派生类自己有自己的构造和析构,规则和之前讲过的完全一样
4.2、派生类的构造函数一定会调用基类的构造函数,析构也一样
(1)代码验证
person.hpp:

#ifndef __PERSON_HPP__
#define __PERSON_HPP__
#include <string>
#include <iostream>
using namespace std;

class person
{
public:
	string name;
	int age;

	person()
	{
		cout << "person()" << endl;
	}
	
	person(string myname)
	{
		name = myname;
		cout << "person(string myname)" << endl;
	}
	
	person(string myname, int myage)
	{
		name = myname;
		age = myage;
		cout << "person(string myname, int myage)" << endl;
	}
	
	~person()
	{
		cout << "~person()" << endl;
	}

	void speak(void);
	void print(void);
private:
	bool male;
};
#endif

man.hpp:

#ifndef __MAN_HPP__
#define __MAN_HPP__
#include "person.hpp"

class man:public person
{
public:
	int length;				// 基类自己特有的一个成员
	
//	man()					// 这是简写,下面这行才是完整的写法	
	man():person("aston")
	{
		cout << "man()" << endl;
	}
	man(string myname, int myage, int mylen);
	
//	~man():~person()				// 显式写了后面的基类析构就会编译报错
	~man()
	{
		cout << "~man()" << endl;
	}
	void print(void);
};
#endif

man.cpp:

#include "man.hpp"
#include <iostream>
using namespace std;

void man::print(void)
{
	cout << "name = " << name << endl;
	cout << "age = " << age << endl;
	cout << "length = " << length << endl;
}

man::man(string myname, int myage, int mylen):person(myname, myage),length(mylen)
{		
	cout << "man(string myname, int myage, int mylen)" << endl;
}

main.cpp:

#include "man.hpp"
#include "person.hpp"
int main(void)
{
	man mn1("richard", 23, 170);
	mn1.print();
	return 0;
}

在基类和派生类中都显示提供“默认构造”并添加打印信息,通过执行结果来验证
通过代码执行结果看到的现象总结:派生类的构造函数执行之前,会先调用基类的构造函数,然后再调用自己的构造函数。而在派生类的析构函数之后,会先执行自己的析构函数,再执行基类的析构函数。(构造函数可以显示调用基类的,但是析构函数不可以显示调用,也就是如下代码出错:~man():~person()
(2)代码验证:派生类的任意构造函数,可以显式指定调用基类的任意一个构造函数,通过参数匹配的方式(类似于函数重载)

4.3、其他几个细节
(1)派生类构造函数可以直接全部写在派生类声明的class中,也可以只在class中声明时只写派生类构造函数名和自己的参数列表,不写继承基类的构造函数名和参数列表,而在派生类的cpp文件中再写满整个继承列表,这就是语法要求
(2)派生类析构函数则不用显式调用,直接写即可直接调用基类析构函数。猜测是因为参数列表问题。
(3)构造函数的调用顺序是先基类再派生类,而析构函数是先派生类再基类,遵循栈规则。
(4)派生类的构造函数可以在调用基类构造函数同时,用逗号间隔同时调用初始化式来初始化派生类自己的成员
4.4、派生类做的三件事
(1)吸收基类成员:除过构造和析构函数以外的所有成员全部吸收进入派生类中
(2)更改继承的成员。1是更改访问控制权限 2是同名覆盖
(3)添加派生类独有的成员。

5.派生类和基类的同名成员问题

5.1、派生类中再实现一个基类中的方法会怎样
(1)代码实验:派生类和基类中各自实现一个内容不同但函数原型完全相同的方法,会怎么样
(2)结论:基类对象调用的是基类的方法,派生类对象调用执行的是派生类中重新提供的方法
(3)这种派生类中同名同参方法替代掉基类方法的现象,叫做:重定义(redefining),也有人叫做隐藏
(4)隐藏特性生效时派生类中实际同时存在2份同名同参(但在不同类域名中)的方法,同时都存在,只是一个隐藏了另一个
5.2、派生类中如何访问被隐藏的基类方法
(1)派生类对象直接调用时,隐藏规则生效,直接调用的肯定是派生类中重新实现的那一个
(2)将派生类强制类型转换成基类的类型,再去调用则这时编译器认为是基类在调用,则调用的是基类那一个,隐藏规则被绕过了(在引用或者指针中可能出现)
方法一:强制类型转换
person *p = (person*)&mn1;
p->speak();
方法二:不用指针而用普通变量,隐式类型转换:
person p=mn1;
p.speak();
方法三:pa::方法

using namespace std;
class pa{ 
        public:
                void speak(){cout << "papa" << endl;};
};

class son:public pa{ 
        public:
                void speak(){cout << "son" << endl;};
};

int main(void)
{
        son s;
        s.speak();
        s.pa::speak();
    
        son *s1;
        s1->speak();
        s1->pa::speak();
        s1->son::speak();
    
        //强制类型转换
        pa *p = (pa *)&s;
        p->speak();

        //隐🥌类型转换
        pa p2 = s;
        p2.speak();
}

5.3、注意和总结
(1)其实不止成员方法,成员变量也遵循隐藏规则。
(2)隐藏规则本质上是大小作用域内同名变量的认领规则问题,实际上2个同名成员都存在当前派生类的对象内存中的。
(3)隐藏(重定义,redefining),与重载(overload)、重写(override,又叫覆盖)(和虚函数、多态有关),这三个概念一定要区分清楚,后面还会讲。

6.子类和父类的类型兼容规则

6.1、何为类型兼容规则
(1)C和C++都是强类型语言,任何变量和对象,指针,引用等都有类型,编译器根据类型来确定很多事。
(2)派生类是基类的超集,基类有的派生类都有,派生类有的基类不一定有,所以这2个类型间有关联。
(3)派生类对象可以cast后当作基类对象,而基类对象不能放大成派生类对象,否则就可能会出错。
(4)考虑到指针和引用与对象指向后,派生类和基类对象的访问规则就是所谓类型兼容规则。
6.2、类型兼容规则的常见情况及演示。
(1)子类对象可以当作父类对象使用,也就是说子类对象可以无条件隐式类型转换为一个父类对象。
(2)子类对象可以直接初始化或直接赋值给父类对象。
(3)父类指针可以直接指向子类对象。
(4)父类引用可以直接引用子类对象。
6.3、总结
(1)派生类对象可以作为基类的对象使用,但是只能使用从基类继承的成员。
(2)类型兼容规则是多态性的重要基础之一。
(3)总结:子类就是特殊的父类 (base *p = &child;)。

7.继承的优势与不良继承

7.1、何为不良继承
(1)鸵鸟不是鸟问题。因为鸵鸟从鸟继承了fly方法但是鸵鸟不会飞
(2)圆不是椭圆问题。因为圆从椭圆继承了长短轴属性然而圆没有长短轴属性
(3)不良继承是天然的,是现实世界和编程的继承特性之间的不完美契合
7.2、如何解决不良继承
(1)修改继承关系设计。既然圆继承椭圆是一种不良类设计就应该杜绝。去掉继承关系,两个类可以继承自同一个共同的父类,不过该类不能执行不对称的setSize计算,然后在圆和椭圆这2个子类中分别再设计以区分
(2)所有不良继承都可以归结为“圆不是椭圆”这一著名具有代表性的问题上。在不良继承中,基类总会有一些额外能力,而派生类却无法满足它。这些额外的能力通常表现为一个或多个成员函数提供的功能。要解决这一问题,要么使基类弱化,要么消除继承关系,需要根据具体情形来选择。

8.组合介绍以及与继承对比

8.1、什么是组合
(1)composition,组合,就是在一个class内使用其他多个class的对象作为成员
(2)用class tree做案例讲解
(3)组合也是一种代码复用方法,本质也是结构体包含
8.2、继承与组合的特点对比
(1)继承是a kind of(is a)关系,具有传递性,不具有对称性。
(2)组合是a part of(has a)的关系,
(3)继承是白盒复用。因为类继承允许我们根据自己的实现来覆盖重写父类的实现细节,父类的实现对于子类是可见的。
(4)继承的白盒复用特点,一定程度上破坏了类的封装特性,因为这会将父类的实现细节暴露给子类
(5)组合属于黑盒复用。被包含对象的内部细节对外是不可见的,所以它的封装性相对较好,实现上相互依赖比较小
(6)组合中被包含类会随着包含类创建而创建,消亡而消亡。组合属于黑盒复用,并且可以通过获取其它具有相同类型的对象引用或指针,在运行期间动态的定义组合。而缺点就是致使系统中的对象过多。
(7)OO设计原则是优先组合,而后继承
更多类之间的关系看文章:(依赖,关联,聚合,组合,继承)https://blog.csdn.net/weixin_44705391/article/details/115664960

9.多继承及其二义性问题

9.1、多继承
(1)多继承就是一个子类有多个父类
(2)多继承演示
(3)多继承和单继承的原理,效果并无明显区别
(4)多继承会导致二义性问题
9.2、多继承的二义性问题1
(1)场景:C多继承自A和B,则C中调用A和B的同名成员时会有二义性
(2)原因:C从A和B各自继承了一个同名(不同namespace域)成员,所以用C的对象来调用时编译器无法确定我们想调用的是哪一个
(3)解决办法1:避免出现,让A和B的public成员命名不要重复冲突。但这个有时不可控。
(4)解决办法2:编码时明确指定要调用哪一个,用c.A::func()明确指定调用的是class A的func而不是class B的
(5)解决办法3:在C中重定义func,则调用时会调用C中的func,A和B中的都被隐藏了
(6)总结:能解决,但是都没有很好的解决。
9.3、多继承的二义性问题2
(1)场景:菱形继承问题。即A为祖类,B1:A, B2:A, C:B1,B2,此时用C的对象调用A中的某个方法时会有二义性
(2)分析:c.func()有二义性,c.A::func()也有二义性,但是c.B1::func()和c.B2::func()却没有二义性
(3)解决办法:和问题1中的一样,但是问题2更隐蔽,也更难以避免
9.4、总结
(1)二义性就是歧义,好的情况表现为编译错误,不好的情况表现为运行时错误,最惨的情况表现为运行时莫名其妙
(2)随着系统的变大和变复杂,难免出现二义性,这不是程序员用不用心的问题,是系统自身带来的
(3)解决二义性问题不能靠程序员个人的细心和调试能力,而要靠机制,也就是编程语言的更高级语法特性
(4)虚函数、虚继承、纯虚函数、抽象类、override(重写,覆盖)、多态等概念就是干这些事

10.虚继承解决菱形继承的二义性问题

10.1、虚继承怎么用
(1)场景:菱形继承导致二义性问题,本质上是在孙子类C中有B1和B2中包含的2份A对象,所以有了二义性。
(2)虚继承解决方案:让B1和B2虚继承A,C再正常多继承B1和B2即可
(3)虚继承就这么简单,就是为了解决菱形继承的二义性问题而生,和虚函数(为了实现多态特性)并没有直接关系

class A
{};
class B1: virtual public A
{
	// A::set
};
class B2:virtual public A
{
	// A::set
};
class D:public B1, public B2
{
	// B1::A::set
	// B2::A::set
};

10.2、虚继承的实现原理
(1)虚继承的原理是:虚基类表指针vbptr和虚基类表virtual table
(2)参考:https://blog.csdn.net/xiejingfa/article/details/48028491
在博客中可以看到,在没有使用虚继承时,程序中是含有两份data A的,但是在用了虚继承之后,就会省略一份。

11.多态和虚函数

#include <iostream>
using namespace std;
class A{
        public:
                virtual void speak();			//虚函数
                //virtual void speak() = 0;		//纯虚函数
};
class B:public A{
        public:
                void speak();
};
class C:public A{
        public:
                void speak();
};

void A::speak()
{
        cout << "a" << endl;
}

void B::speak()
{
        cout << "b" << endl;
}
void C::speak()
{
        cout << "c" << endl;
}
int main()
{
        //A a;
        //a.speak();
        B b;
        A *a;
        a=&b;
        a->speak();
        return 0;
}

11.1、什么是多态
(1)polymorphism,多态,面向对象的三大特征之一。
(2)从宏观讲,多态就是要实现一套逻辑多种具体适配的执行结果。猫就应该是猫的叫声,狗就应该是狗的叫声。
(3)从微观讲,多态就是要一套代码在运行时根据实际对象的不同来动态绑定/跳转执行相匹配的具体函数
(4)函数声明前加virtual的即是虚函数
(5)虚函数是C++实现多态特性的基础,从语法上讲多态特性的基类方法必须是虚函数
11.2、多态中的override
(1)基类中方法声明为virtual,派生类中重新实现同名方法以实现多态,这就叫override(中文为覆盖,或重写)
(2)注意区分override和redefining,微观上最大区别就是是否有virtual,宏观上最大区别就是是否表现为多态
11.3、多态一定要通过面向对象和override来实现吗
(1)宏观上的多态是一种编程效果,微观上的多态是一种C++支持的编程技术,微观是为了去实现宏观
(2)不用C++的virtual和override,也可以实现宏观上的多态,C中我们就经常这么干。
(3)C中实现多态的案例:
(4)C++源生支持多态,实现起来更容易,后续修改和维护更容易,架构复杂后优势更大。
11.4、对比下三个概念
(1)overload,重载 同一个类里面的多个方法,函数名相同但参数列表不同。
下面两种同名同参:

(2)redifining,重定义,隐藏 继承中子类再次实现父类中同名方法然后把父类方法隐藏
(3)override,覆盖重写 继承中子类去实现父类中同名virtual方法然后实现多态特性

12.纯虚函数与抽象类

121、纯虚函数
(1)纯虚函数就是基类中只有原型没有实体的一种虚函数
(2)纯虚函数形式:virtual 函数原型=0;
(3)纯虚函数是否占用内存?不会,因为纯虚函数所在的类根本无法实例化对象
12.2、抽象类(abstract type)
(1) 带有纯虚函数的类称为抽象类。(抽象类中可以存在一般函数)抽象类只能作为基类来派生新类,不可实例化对象。
(2)派生类必须实现基类的纯虚函数后才能用于实例化对象。
(3)抽象类的作用:将有关的数据和行为组织在一个继承层次结构中,保证派生类具有要求的行为。对应暂时无法实现的函数,可以声明为纯虚函数,留给派生类去实现。这种机制可以让语法和语义保持一致。
(4)抽象类的子类必须实现基类中的纯虚函数,这样子类才能创建对象,否则子类就还是个抽象类
12.3、接口(interface)
(1)接口是一种特殊的类,用来定义一套访问接口,也就是定义一套规约
(2)接口类中不应该定义任何成员变量
(3)接口类中所有成员函数都是公有且都是纯虚函数
(4)有些高级语言中直接提供关键字interface定义接口,接口其实就是个纯粹的抽象基类

13.虚析构函数

13.1、析构函数前加virtual,则析构函数变为虚析构函数
规则:基类有1个或多个虚函数时(注意不要求是纯虚函数),则其析构函数应该声明为virtual
13.2、为什么需要虚析构函数
(1)代码演示:

	Animal *p = new Dog();		// 对象是Dog类对象,但是分配在堆上面
	p->speak();
	delete p;		//实际析构时只执行了父类的析构函数,并没执行子类的析构函数
	
	Dog d;					// 对象是Dog类对象,分配在栈上
	Animal *p = &d;
	p->speak();	//实际析构时只执行了子类的析构函数,并没有执行父类的析构函数

在析构函数不是虚函数时,使用new分配内存会导致子类对象最后执行的是父类的析构函数,这样就不对了。那么将父类的析构函数变成虚函数,就对了。
(2)结论:虚析构函数在各种情况下总能调用正确的(和对象真正匹配的)析构函数。
13.3、分析和总结
(1)其实虚函数的virtual的价值,就是让成员函数在运行时动态解析和绑定具体执行的函数,这是RTTI机制的一部分。(动态运行时类型识别机制)
(2)析构函数也是成员函数,加virtual的效果和普通成员函数加virtual没什么本质差异
(3)加virtual是有开销的,运行时动态绑定不如编译时静态绑定效率高资源消耗优,但是可以多态。

14.using重新定义继承时访问权限

14.1、using关键字在非public继承时的权限重开作用
(1)父类的public方法在private/protected继承时,到了子类就成了private/protected而不是public了,无法用子类对象来调用了
(2)解决方法1是改为public继承,有用但是有时候不得不protected或者private继承时就没办法了
(3)解决方法2是在子类中再实现一个public的方法,内部调用父类继承而来的那个方法,能用但是有点麻烦而且有额外开销
(3)解决方法3是在子类中使用using关键字将该方法声明为public访问权限,本质上类似于权限打洞
(4)用法:在子类public声明中使用 using Base::func;不带括号 即可,不带返回值类型不带参数列表。
(5)注意:using只用于private/protected继承中的权限损失找回,如果方法在父类中本来就是private的子类中没法using后访问
14.2、本部分总结
本部分主要讲了OO的2个特性:继承和多态,其中继承和权限管控、继承中的构造和析构、隐藏和覆盖、虚函数和多态、纯虚函数和抽象类、接口、静态和动态绑定等是重点。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
封装、继承多态是面向对象编程中的三大特性C++也支持这三种特性。 1. 封装 封装是指将数据和对数据的操作封装在一起,形成一个类。类中的数据和方法可以设置为私有的,只有类内部可以访问,外部无法访问。这样可以保证数据的安全性和完整性。 下面是一个封装的例子: ```c++ class Person { private: string name; int age; public: void setName(string n) { name = n; } void setAge(int a) { age = a; } string getName() { return name; } int getAge() { return age; } }; ``` 2. 继承 继承是指一个类可以继承另一个类的属性和方法。被继承的类称为父类或基类,继承的类称为子类或派生类。子类可以使用父类的属性和方法,也可以重写父类的方法。 下面是一个继承的例子: ```c++ class Student : public Person { private: int id; public: void setId(int i) { id = i; } int getId() { return id; } }; ``` 3. 多态 多态是指同一种类型的对象,在不同的情况下可以有不同的表现形式。C++中实现多态有两种方式:虚函数和模板。 下面是一个虚函数的例子: ```c++ class Shape { public: virtual double getArea() { return 0; } }; class Circle : public Shape { private: double radius; public: Circle(double r) { radius = r; } double getArea() { return 3.14 * radius * radius; } }; class Rectangle : public Shape { private: double width; double height; public: Rectangle(double w, double h) { width = w; height = h; } double getArea() { return width * height; } }; ```

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值