c++的继承

101 阅读5分钟

c++的继承

继承是面向对象三大特性之一

作用:减少重复代码

语法

class 子类:继承方式 父类

子类也称为派生类

父类也称为基类

派生类中的成员,包含两部分:一类从基类继承过来,一类是自己增加的成员。

从基类继承过来的表现其共性,而新增的成员体现了其个性

#include<iostream>
using namespace std;
class Father 
{
public:
	void header()
	{
		cout << "我是头部" << endl;
	}
	void left()
	{
		cout << "我是left" << endl;
	}
	void footer()
	{
		cout << "我是footer" << endl;
	}
};

class Son :public Father
{
public:
	void content()
	{
		cout << "我是个性的content" << endl;
	}
};
class Son1 :public Father
{
public:
	void content()
	{
		cout << "我是个性的content1" << endl;
	}
};
void test()
{
	Son s;
	s.header();
	s.content();
	s.left();
	s.footer();
	Son1 s1;
	s1.header();
	s1.content();
	s1.left();
	s1.footer();
}
int main()
{
	test();
	system("pause");
	return 0;
}

继承方式

继承方式一共有三种

公共继承

保护继承

私有继承

#include<iostream>
using namespace std;
class Father
{
public:
	int m_A;
protected:
	int m_B;
private:
	int m_C;
};
//第一种保护继承方式
class Son :public Father
{
	void fn()
	{
		m_A = 100;//访问权限依然是公共权限
		m_B = 100;//访问权限依然是保护权限
		//m_C = 100;//不能继承Father类中私有成员
	}
};
class Son1 :protected Father//保护继承
{
	void fn()
	{
		m_A = 100;//访问权限变成了保护权限
		m_B = 100;//访问权限依然是保护权限
		//m_C = 100;//不能继承Father类中私有成员
	}
};
class Son2 :private Father
{
	void fn()
	{
		m_A = 100;//访问权限变成了私有权限
		m_B = 100;//访问权限依然是私有权限
		//m_C = 100;//不能继承Father类中私有成员
	}
};
class Son3 :private Son2
{
	void fn()
	{
		//m_A = 100;//继承了Son2类,因为他的类都是私有,不能继承过来他的私有成员
		//m_B = 100;//继承了Son2类,因为他的类都是私有,不能继承过来他的私有成员
		//m_C = 100;//Son2不能继承Father类中私有成员,目前都没有这个成员
	}
};
void test01()
{
	Son s;
	s.m_A = 100;//访问权限是公共权限能访问到
	//s.m_B = 110;//访问权限是保护,类外访问不到
	Son1 s1;
	//s1.m_A = 100;//访问权限都为保护权限,类外访问不到
	//s1.m_B = 120;//访问权限都为保护权限,类外访问不到
	Son2 s3;
	//s3.m_A = 100;//访问权限都为私有权限,类外访问不到
	//s3.m_B = 100;//访问权限都为私有权限,类外访问不到
	
}
int main()
{
	test01();
	system("pause");
	return 0;
}

继承中的对象模型

父类中所有非静态成员属性都会被子类继承下去

父类中私有成员属性是被编译器给隐藏了,因此是访问不到,但确实被继承下去了

#include<iostream>
using namespace std;
class Father
{
public:
	static int m_S;
	int m_A;
protected:
	int m_B;
private:
	int m_C;
};
class Son :public Father
{
public:
	int m_D;
};
int Son::m_S = 100;
int main()
{
	cout << sizeof(Son) << endl;//16,在继承的时候,父类的非静态成员属性包括私有,都继承给子类,只是继承过来的私有属性被编译器隐藏了。
	Son s;
	cout << s.m_S << endl;
	system("pause");
	return 0;
}

继承中构造和析构顺序

子类继承父类后,当创建子类对象,也会调用父类的构造函数

#include<iostream>
using namespace std;
class Father
{
public:
	static int m_S;
	int m_A;
	Father()
	{
		cout << "father构造函数调用" << endl;
	}
	~Father()
	{
		cout << "father析构函数调用" << endl;
	}
protected:
	int m_B;
private:
	int m_C;
};
class Son :public Father
{
public:
	int m_D;
	Son()
	{
		cout << "Son构造函数调用" << endl;
	}
	~Son()
	{
		cout << "Son析构函数调用" << endl;
	}
};
int Son::m_S = 100;
int main()
{
	Son s;
	system("pause");
	return 0;
}

继承同名成员处理方式

访问子类同名成员 直接访问即可

访问父类同名成员 需要加作用域

#include<iostream>
using namespace std;
class Father
{
public:
	int m_A;
	Father()
	{
		m_A = 100;
	}
	void print1()
	{
		cout << "我是Father类中的成员函数" << endl;
	}
	void print1(int a)
	{
		cout << "我是Father类中的成员函数int a" << endl;
	}
};
class Son:public Father
{
public:
	int m_A;
	Son()
	{
		m_A = 200;
	}
	void print1()
	{
		cout << "我是Son类中的成员函数" << endl;
	}
};
void test07()
{
	Son s;
	cout << "m_A= " << s.m_A << endl;//输出是两百,出现父子之间同名情况,在子类,会替换掉父类或者隐藏掉父类的成员
	//想访问到父类的同名成员,必须要加上作用域
	cout << "m_A= " << s.Father::m_A << endl;//100
}
void test08()
{
	Son s1;
	s1.print1();//我是Son类中的成员函数,输出为子类的成员函数
	//s1.print1(100);
	//要访问到父类的同名函数,要加作用域
	s1.Father::print1();//我是Father类中的成员函数
	s1.Father::print1(100);
}
int main()
{
	//test07();
	test08();
	system("pause");
	return 0;
}

子类对象可以直接访问到子类中同名成员

子类对象想访问父类同名成员,要加作用域

3.当子类与父类拥有同名的成员函数,子类会隐藏或者替换父类中的同名成员函数,加作用域可以访问到父类中同名函数

继承同名静态成员处理方式

静态成员和非静态成员出现同名,处理方式一致

访问子类同名成员,直接点处理就行。

访问父类静态成员,要加作用域

#include<iostream>
using namespace std;
class Father
{
public:
	static int m_A;
};
int Father::m_A = 100;
class Son:public Father
{
public:
	static int m_A;
};
int Son::m_A = 200;

void test14()
{
	Son s;
	//子类和父类静态成员同名访问和非静态成员访问基本一致
	//但是静态成员可以通过类名和对象方式访问
	cout << "m_A =" << s.m_A << endl;//子类中m_A
	cout << "m_A =" << s.Father::m_A << endl;//父类中m_A
	//通过类名访问
	cout << "m_A =" << Son::m_A << endl;//通过类名访问到了Son类中的m_A
	cout << "m_A =" << Son::Father::m_A << endl;//第一个::表示在Son类下,第二个::表示在Father作用域下

}
int main()
{
	test14();
	system("pause");
	return 0;
}

多继承语法

c++运行一个类继承多个类

语法:

class 子类 : 继承方式 父类1,继承方式 父类2...

多继承可能会引发父类中有同名成员出现,需要加作用域区分

c++实际开发中不建议用多继承

#include<iostream>
using namespace std;
class Base1
{
public:
	int m_B;
};
class Base
{
public:
	int m_B;
};
class Son:public Base,public Base1 {
public:
	int m_C;
	int m_D;
};
int main()
{
	Son s;
	s.Base::m_B = 100;
	s.Base1::m_B = 200;
	cout << "m_A =" << s.Base1::m_B << endl;//多继承容易造成同名成员的问题,解决这个问题需要加作用域
	cout << "m_A =" << s.Base::m_B << endl;
	system("pause");
	return 0;
 }

菱形继承

菱形继承概念

两个派生类继承同一个基类

又有某个类同时继承者两个派生类

这种继承被称为菱形继承,或者钻石继承

利用虚继承可以解决,菱形继承的问题

在继承最大基类的子类中加上virtual关键字

这时最大基类我们就称它为虚基类

多态

多态的基本概念

多态是c++面向对象三大特性之一

多态分为两类

静态多态:函数重载和运算符重载属于静态多态,复用函数名

动态多态:派生类和虚函数实现运行时多态

静态多态和动态多态区别

静态多态的函数地址早绑定编译阶段确定函数地址

动态多态的函数地址晚绑定.运行阶段确定函数地址

动态多态满足条件

1.有继承关系

2.子类重写父类的虚函数

动态多态使用

父类的指针或者引用 指向子类对象 重写:函数名 函数返回值 参数列表 完全一致称为重写

#include<iostream>
using namespace std;
class Father
	//多态分为静态多态和动态多态
	//静态多态:列如函数重载和运算符重载等
	//动态多态:虚函数和派生类实现运行时多态
	//区别:静态,函数地址早绑定,在编译时函数地址就绑定完毕,动态多态,函数地址晚绑定,运行阶段确定函数地址 
{
public:
	virtual void speak()
	{
		cout << "动物叫" << endl;
	}
};
class Son:public Father
{
	//多态条件
	//有继承
	//重写父类的虚函数
	//多态的使用
	//父类引用或者指针指向子类对象

public:
	virtual void speak()
	{
		cout << "小猫叫" << endl;
	}
};
void speak(Father &f)
{
	f.speak();
}
void test01()
{
	Son s;
	speak(s);
}
int main()
{
	test01();
	system("pause");
	return 0;
}

多态的原理剖析

  • 当类中存在虚函数时,编译器会在类中自动生成一个虚函数表
  • 虚函数表是一个存储类成员函数指针的数据结构
  • 虚函数表由编译器自动生成和维护
  • virtual 修饰的成员函数会被编译器放入虚函数表中
  • 存在虚函数时,编译器会为对象自动生成一个指向虚函数表的指针(通常称之为 vptr 指针)
    #include <iostream>
     
    using namespace std;
     
    class Parent
    {
    public:
        // 父类虚函数必须要有 virtual 关键字
        virtual void fun()
        {
            cout << "父类" << endl;
        }
    };
     
    class Child : public Parent
    {
    public:
        // 子类有没有 virtual 关键字都可以
        void fun()
        {
            cout << "子类" << endl;
        }
    };
     
    int main()
    {
        Parent *p = NULL; // 创建一个父类的指针
        Parent parent;
        Child child;
        p = &parent; // 指向父类的对象
        p->fun(); // 执行的是父类的 fun() 函数
        p = &child; // 指向子类的对象
        p->fun(); // 执行的是子类的 fun() 函数
        return 0;
    }

如上例代码所示,当我们传入父类对象时,将调用和执行父类的函数,当我们传入子类对象时,将调用和执行子类的函数。而 C++ 编译器的执行过程其实是这样的:

  1. 父类的 fun() 是个虚函数,所以编译器给父类对象自动添加了一个 vptr 指针,指向父类的虚函数表,这个虚函数表里存放了父类的 fun() 函数的函数指针
  2. 子类的 fun() 函数是重写了父类的,即写不写 virtual 编译器都会为其自动添加一个 virtual,然后编译器给子类对象自动添加了一个 vptr 指针,指向子类的虚函数表,这个虚函数表里存放了子类的 fun() 函数的函数指针
  3. 执行 p->fun() 时,编译器检测到 fun() 是一个虚函数,所以不会静态的将 Parent 类的 fun() 方法直接编译过来,而是是运行的时候,动态的根据 base 指向的对象,找到这个对象的 vptr 指针,然后找到这个对象的虚函数表,最后调用虚函数表里对应的函数,实现多态

多态案例-计算器类

多态优点:

代码组织结构清晰

可读性强

利于前期和后期的扩展以及维护

开闭原则:对扩展进行开放,对修改进行关闭

#include<iostream>
#include<string>
using namespace std;
//普通写法
//不利于前期和后期的维护,
//代码结构不清晰
//可读性差
class Category
{
public:
	int m_A;
	int m_B;
	int getRuslt(string a) 
	{
		if (a == "+")
		{
			return this->m_A + this->m_B;
		}
		else if (a == "-")
		{
			return this->m_A - this->m_B;
		}
		else if (a == "*")
		{
			return this->m_A * this->m_B;
		}
	}
};
//利用多态写法
class Acategory
{
public:
	int m_A;
	int m_B;
	virtual int getRuslt()
	{
		return 0;
	}
};
class AddAcategory:public Acategory
{
	virtual int getRuslt()
	{
		return m_A + m_B;
	}
};
class SubAcategory :public Acategory
{
	virtual int getRuslt()
	{
		return m_A - m_B;
	}
};
class CAcategory :public Acategory
{
	virtual int getRuslt()
	{
		return m_A * m_B;
	}
};
void test03()
{
	Category c;
	c.m_A = 100;
	c.m_B = 100;
	cout << "m_A" << "+" << "m_B" << "=" << c.getRuslt("+") << endl;
	cout << "m_A" << "-" << "m_B" << "=" << c.getRuslt("-") << endl;
	cout << "m_A" << "*" << "m_B" << "=" << c.getRuslt("*") << endl;
}
void test04()
{
	Acategory* a = new AddAcategory();
	a->m_A = 100;
	a->m_B = 100;
	cout << "m_A" << "+" << "m_B" << "=" << a->getRuslt() << endl;
	delete a;
	a = new SubAcategory();
	a->m_A = 100;
	a->m_B = 100;
	cout << "m_A" << "-" << "m_B" << "=" << a->getRuslt() << endl;
	delete a;
	a = new CAcategory();
	a->m_A = 100;
	a->m_B = 100;
	cout << "m_A" << "*" << "m_B" << "=" << a->getRuslt() << endl;
}
void test05(Acategory &a)
{
	//引用
	a.m_A = 100;
	a.m_B = 100;
	cout << "m_A" << "+" << "m_B" << "=" << a.getRuslt() << endl;
}
void test06()
{
	AddAcategory a;
	test05(a);
	SubAcategory c;
	test05(c);
	CAcategory ca;
	test05(ca);
}
int main()
{
	//test03();
	//test04();
	test06();
	system("pause");
	return 0;
}

纯虚函数和抽象类

在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容

因此可以将函数改为纯虚函数

纯虚函数语法:virtual 返回值类型 函数名(参数列表)=0;

类中有了纯虚函数,这个类也称为抽象类

抽象类的特点:

无法实例化对象

子类必须重写抽象类中的纯虚函数,否则也属于抽象类

#include<iostream>
using namespace std;
class Base
{
public:
	virtual void fn() = 0;//只要类中拥有纯虚函数,那么这个类就是抽象类
};
class Son:public Base
{
public:
	virtual void fn() {
		cout << "111" << endl;
	}
};
void test08()
{
	//Base b;//抽象类的特点:无法实例化对象,子类继承不重写纯虚函数,那么也变成了抽象类,不能实例化对象
	//Son s;//子类继承不重写纯虚函数,那么也变成了抽象类,不能实例化对象
	//new Son;//也不能在堆区创造
	Son s;
	s.fn();

}
int main()
{
	test08();
	system("pause");
	return 0;
}

虚析构和纯虚析构

多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码

解决方式:将父类中的析构函数改为虚析构或者纯虚析构

虚析构和纯虚析构共性:

​ 可以解决父类指针释放子类对象

​ 都需要有具体的函数实现

虚析构和纯虚析构区别:

如果是纯虚析构,该类属于抽象类,无法实例化对象

纯虚析构 需要声明也需要实现

虚析构语法: virtual ~类名(){}

纯虚析构语法: virtual ~类名() = 0

类名::~类名(){}

#include<iostream>
using namespace std;
class Base12
{
public:
	Base12() {
		cout << "我是Base构造函数" << endl;
	}
	virtual void speak1() = 0;
	//virtual ~Base12()//解决子类析构函数的调用
	//{
	//	cout << "Base虚析构函数" << endl;
	//}
	virtual ~Base12() = 0;
};
Base12::~Base12() {
	cout << "Base虚析构函数" << endl;//纯虚析构函数
}
class Son1:public Base12
{
public:
	Son1(int a)
	{
		m_A = new int(a);
		cout << "Son1构造函数调用" << endl;
	}
	virtual void speak1()
	{
		cout << "m_A=" << *m_A << endl;
	}
	 ~Son1()
	{
		cout << "Son1虚析构函数" << endl;
		if (m_A != NULL) {
			delete m_A;
			m_A = NULL;
		}
	}
	int *m_A;
};
void test10()
{
	Base12* base = new Son1(10);
	base->speak1();
	delete base;
	//通过父类指针去释放,会导致子类对象可能清理不干净,造成内存泄露
	//解决方法
	//给父类添加一个虚析构函数,虚析构函数就是用于解决父类指针释放子类指针
}
int main()
{
	test10();
	system("pause");
	return 0;
}

总结:

1.虚析构或者纯虚析构就是用于解决父类指针释放子类对象

2.如果子类中没有堆区数据,可以不写虚析构或者纯虚析构

3.拥有纯虚析构函数的类也属于抽象类