c++面向对象三大特征封装继承和多态知识总结

Posted uestclr

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了c++面向对象三大特征封装继承和多态知识总结相关的知识,希望对你有一定的参考价值。

面向对象三大特征:封装,继承,多态;

一、封装:该公开的就公开话,该私有的就隐藏掉,主要是由publicprivate实现;作用是便于分工和分模块,防止不必要的扩展;

二、继承:就是一种传承,可以把父类型中的数据传承到子类中,子类除了传承了父类的数据之外,还可以对父类型进行扩展;

公开继承  public

保护继承  protected

私有继承  private

保护成员:在子类和本类中可以访问,其他不行;


1、公开继承:在公开继承下,父类型中的数据是公开的到子类型中权限是公开的;父类型中保护权限的数据到子类中是保护的;父类中私有的数据到子类中会隐藏掉(就是说看不见权限,但是实际上式在子类中的);

2、私有继承:在私有继承下,父类中的公开数据到子类中变成私有的,父类中的保护数据到子类中称为私有的,父类中的私有数据到子类中隐藏;

3、保护继承:保护继承下,父类中的公开数据和保护数据到了子类中都成为保护权限,父类中私有的数据到了子类中就变成了隐藏的;


4、注意:不管何种类型的继承关系,父类私有成员到子类中都成为了隐藏掉。

公开继承下的public成员和protected成员的权限变化:

#include <iostream>
using namespace std;

class A{
public:
	int a;
	int geta(){
		a = 300;
		return a;
	}
	/*保护类型成员在本类和子类中可以访问*/
protected:
	int b;
private:
	int c;
};

class B:public A{
	//int x;
public:
	void getb(){
		b = 200;
	}
	void show(){
		cout << b << endl;
	}
	void showa(){
		cout<< a<<endl;
	}
}; 
/*关键在于如何设置接口,成功合理的访问到各种类型的数据*/
int main(){
	B pex;
	/*公开继承public成员依旧是public,所以可以类外访问*/
	pex.a = 100;
	/*b是保护类型成员,可以通过设置public接口来访问*/
	pex.getb();
	pex.show();
	/*隐藏成员的问题,怎么访问到隐藏的成员*/
	pex.geta();
	pex.showa();
	//A a = pex;//子类类型赋给了父类类型
	//a.geta();
	//cout << a.a << endl;
}


5、私有继承下的成员的权限变化关系,注意如何设置合理的 访问接口去访问 隐藏的数据成员。

/*私有继承下的权限变化,关键是设置合理的接口访问
父类中的各种类型的数据成员*/
#include <iostream>
using namespace std;

class A{
private:
	void showa(){
		cout << "this is showa()" << endl;
	}
protected:
	void showb(){
		cout << "this is showb" << endl;
	}
public:
	void showc(){
		cout << "this is showc" << endl;
	}
	void geta(){//设置合理的接口访问A中的私有数据
		showa();
	}
};

class B:private A{
public:
	void show(){
		showc();
		showb();
		geta();
	}
};

int main(){
	B b;
	//A a = b;对比公开继承,对比一下
	b.show();
}

6、突破成员访问权限,可以设置合理的访问接口,也可以使用友元类。

/*友元类*/
#include <iostream>
using namespace std;

class A{
private:
	int x;
	int y;
public:
	A():x(10),y(123){}
	/*B,C声明为A的友元类之后,可以访问到父类的所有类型成员*/
	friend class B;
	friend class C;
};

class B:public A{
public:
	void show(){
		cout << x << "---" << y << endl; 
	}
};

class C{
public:
	void show(){
		A a;
		cout <<a.x<< "---" << a.y << endl;
	}
};

int main(){
	B b;
	b.show();
	C c;
	c.show();
}

7、继承中构造函数、析构函数、赋值运算符函数和拷贝构造函数

构造函数和析构函数是不能被继承的,但是可以被调用。并且子类一定会调用父类的构造函数;

子类默认调用父类的无参构造,也可以制定调用构造函数;

析构函数的调用和构造函数的调用顺序相反;

拷贝构造函数和赋值运算符函数也不能被继承:在子类不提供拷贝构造和赋值运算符时,子类默认调用父类的赋值运算符和拷贝构造函数。但子类一旦提供拷贝构造和赋值运算符函数则不再调用父类拷贝构造和赋值运算符函数。

/*继承中构造函数和析构函数的调用:构造函数和析构函数不可以
被继承,但是可以被调用,而且子类肯定会调用父类的构造函数
和析构函数。这种机制可以很自然的用于访问父类的私有成员*/
#include <iostream>
using namespace std;

class A{
private:
	int x;
public:
	//A(){cout << "A()" << endl;}
	A(int x = 0):x(x){
		cout <<"A()构造"<<endl;
		cout << x << endl;}
	~A(){cout << "~A()" << endl;}
	int _get(){
		return x;
	}
};

class B:public A{
public:
	/*在初始化参数列表中可以指定调用父类的构造函数,指定调用构造函数并且给
	父类中的私有成员赋值*/
	/*注意:子类默认调用父类的无参构造,如果下面的代码没有:A(100),则会调用无参构造,但是父类无参构造
	被注释掉,所以会出错*/
	B():A(100){
		//x = 200;
		//A(100);
		cout << "B()" << endl;
	}
	//访问有参构造的方式,理解这种方式的作用
	/*注意,这种机制下的构造函数所赋的值是赋到了子类中的数据x中,
	而父类中的x仍然为0*/
	~B(){cout << "~B()" << endl;}
	int getbx(){
		return _get();
	}
};
int main(){
	A a;//构建A对象,此时A类构造被调用,并打印出了值

	B b;//B类为无参构造,首先调用了A的构造,在调用B的构造

	//打印a对象中的x成员
	cout <<a._get()<<endl;//a对象中的x为0

	//打印b对象中的x
	cout << b.getbx()<<endl;//是100
	/*一层一层的退,先调用b的析构,在调用a的析构*/
}
拷贝构造和赋值运算符的问题

#include <iostream>
using namespace std;
/*系统一旦提供构造函数,系统默认的构造函数将被回收
记住,拷贝构造也是构造函数*/
class A{
	int arr;
public:
	A(){}
	//A(int x = 0):arr(x){}
	A(const A& a){
		cout << "父类拷贝构造" << endl;
	}
	void operator=(const A& a){
		cout << "父类赋值运算符函数" << endl;
	}
};
/*有指针类型的成员时,采用默认机制就麻烦了*/
class B:public A{
	//int * pi;
public:
	B(){}
	B(const B& b):A(b){                      
		//子类中提供了拷贝构造函数将不再调用父类的拷贝构造
		cout << "子类拷贝构造" << endl;
	}
	void operator=(const B& b){
		A::operator=(b);         //调用父类的拷贝构造函数的机制
		cout << "子类赋值运算符函数"<< endl;
	}
};

int main(){
	B a;
	B b = a;
	B c;
	c = a;
}

8,名字隐藏

名字隐藏机制:子类中如果定义了和父类中同名的数据,这些数据包括成员变量和成员函数。则会把父类中的数据隐藏掉。

注意:只要名字相同,计算返回值或者形参列表不同,也会被隐藏。隐藏不代表就没有了,可以通过类名作用域::访问到被隐藏的成员。

#include <iostream>
using namespace std;

class A{
public:
	int x;
	int show(){
		cout << "show A" << endl;
		return 0;
	}
	A(){x=20;}
	A(int x):x(x){cout << "show A(int x)" << endl;}
	void shouu(){
		cout <<"shouu()"<<endl;
	}
};

class B:public A{
public:
	int x;
	int y;
	B(){cout << "B()" << endl;}
	void show(){
		cout << "show B" << endl;
		//A::show();
	}
};

int main(){
	B b;
	b.shouu();
	//cout << b.x << endl;
	//cout << b.A::x << endl;   //突破名字隐藏机制
	//int c = b.show();被隐藏,无法访问
	//b.A::show();
}

9、多继承和函数重写

多继承是c++特有的语法机制,表现为一个子类有多个直接的父类。

#include <iostream>
using namespace std;

class phone{
	double price;
public:
	//phone();
	phone(double price = 15):price(price){cout << "phone" << endl;}
	~phone(){cout << "~phone" << endl;}
	void call(){
		cout << "use calling" << endl;
	}
	double getprice(){
		return price;
	}
};

class MP3{
	double price;
public:
	MP3(double price = 20):price(price){cout << "MP3" << endl;}
	~MP3(){cout << "~MP3" << endl;}
	void play(){
		cout << "use to listening music" << endl;
	}
	double getprice(){
		return price;
	}
}; 

class vedio{
	double price;
public:
	vedio(double price = 0):price(price){cout << "vedio" << endl;}
	~vedio(){cout << "~vedio" << endl;}
	void vcd(){
		cout << "watch vedio" << endl;
	} 
	double getprice(){
		return price;
	}
};
/*多继承*/
class iphone:public phone,public MP3,public vedio{
public:
	double getprice(){
		return phone::getprice() + MP3::getprice() + vedio::getprice();
	}
};

int main(){
	iphone iphone6;
	//cout << sizeof(iphone) << endl;
	cout << iphone6.MP3::getprice() << endl;
	cout << iphone6.phone::getprice() << endl;
	cout << iphone6.getprice() << endl;    //用名字隐藏机制解决多分数据同名冲突的问题
}

多继承遇到的问题:上面的代码用sizeof就可以看到,子类在多继承的时候会多次复制顶层数据,而我们期望的是price这个成员只需要复制一份就可以了,因为多余的复制是无意义的。首先采用顶层抽象的方式,将三个父类抽象到更高的层面上。

#include <iostream>
using namespace std;
/*抽象到更高层的类中*/
class product{
	double price;
public:
	double getprice(){
		return price;
	}
	product(double price = 0):price(price){cout <<"product"<<endl;}
};
class phone:public product{
public:
	//phone();
	phone(double price = 15):product(price){cout << "phone" << endl;}
	~phone(){cout << "~phone" << endl;}
	void call(){
		cout << "use calling" << endl;
	}
};

class MP3:public product{
public:
	MP3(double price = 20):product(price){cout << "MP3" << endl;}
	~MP3(){cout << "~MP3" << endl;}
	void play(){
		cout << "use to listening music" << endl;
	}
}; 

class vedio:public product{
public:
	vedio(double price = 0):product(price){cout << "vedio" << endl;}
	~vedio(){cout << "~vedio" << endl;}
	void vcd(){
		cout << "watch vedio" << endl;
	} 
};
class iphone:public phone,public MP3,public vedio{
	
};

int main(){
	iphone iphone6;
	//cout << iphone6.getprice() << endl;同样会产生冲突的问题
	//cout << sizeof(iphone) << endl;
	cout << iphone6.MP3::getprice() << endl;
	cout << iphone6.phone::getprice() << endl;
	//cout << iphone6.getprice() << endl;//直接调用产生冲突问题,编译器不知道该调用哪一个
}
上面的代码中,product的构造函数 被调用了三次,因为这种继承是一级一级的来的,构造子类的时候找父类,发现父类还有父类,就去调用爷爷类的构造函数,三次继承,三次调用。


这种继承方式构成了一种菱形或者钻石型的继承,叫做菱形继承或者钻石继承,但钻石继承并没有实际解决数据多次复制的问题,为了解决菱形继承,c++提出了虚继承。虚继承就是在继承的时候加上virtual关键字修饰即可。虚继承对于共同的成员父亲类从爷爷类那里继承来的,这里为double price,子类直接越级访问,直接从爷爷类那里继承price。

/*类中也会有对齐和补齐*/
#include <iostream>
using namespace std;
/*抽象到更高层的类中*/
class product{
	int price;
public:
	int  getprice(){
		return price;
	}
	product(double price = 0):price(price){cout << "product" << endl;}
};

class phone:virtual public product{
public:
	//phone();
	phone(double price = 15):product(price){cout << "phone" << endl;}
	~phone(){cout << "~phone" << endl;}
	void call(){
		cout << "use calling" << endl;
	}
};

class MP3:virtual public product{
public:
	MP3(double price = 20):product(price){cout << "MP3" << endl;}
	~MP3(){cout << "~MP3" << endl;}
	void play(){
		cout << "use to listening music" << endl;
	}
}; 

class vedio:virtual public product{
public:
	vedio(double price = 0):product(price){cout << "vedio" << endl;}
	~vedio(){cout << "~vedio" << endl;}
	void vcd(){
		cout << "watch vedio" << endl;
	} 
};
class iphone:virtual public phone,virtual public MP3,virtual public vedio{
public:
	iphone(int m = 0,int v = 0,int p = 0):product(m + p + v){}
};
/*虚函数之后,product的构造函数只被调用了一次,孙子类直接越级访问
了product类*/
int main(){
	iphone iphone6(1000,2041,3201);
	//cout << iphone6.getprice() << endl;同样会产生冲突的问题
	//cout << sizeof(iphone) << endl;
	//cout << iphone6.MP3::getprice() << endl;
	//cout << iphone6.phone::getprice() << endl;
	//cout << iphone6.getprice() << endl;直接调用产生冲突问题,编译器不知道该调用哪一个
	cout << sizeof(iphone) << endl;
	cout << iphone6.getprice() << endl;
}

这个代码中product的构造函数只调用了一次,说明子类直接越级访问了爷爷类的数据。而对于父类特有的子类照常继承,只是没有通过父类去继承爷爷类的数据成员,所以product的构造函数只被调用了一次。


虚函数:在函数前面加上virtual关键字修饰过的就是虚函数.

#include <iostream>
using namespace std;
class A{
int x;
public:
virtual void show(){}
virtual void showa(){}
};
int main(){
cout << sizeof(A) << endl;
}

虚函数的主要表现为会占用四个字节的空间,只要成员中出现虚函数,不管有多少个虚函数,都只用四个字节来维护这个虚关系。虚函数会影响对象的大小。维护虚关系使用一个指针来维护的,所以是四个字节。


函数重写:

在父类中出现一个虚函数,如果在子类中提供和父类同名的函数(注意区分名字隐藏),这就加函数重写。

函数重写要求必须有相同函数名,相同的参数列表,相同的返回值。


三、c++面向对象之多态

1、多态:一个父类型的对象的指针或者引用指向或者是引用一个子类对象时,调用父类型中的虚函数,如果子类覆盖了虚函数,则调用的表现是子类覆盖之后的。

继承是构成多态的基础;

虚函数是构成多态的关键;

函数覆盖是构成多态的必备条件;

多态的应用:函数参数,函数返回值。

多态的产生必须依靠上面的四点,缺一不可。


2、多态的应用,多态相对做到了通用类型编程,主要用在函数参数和函数的返回值上。

/*多态的应用:
1、函数参数
2、函数返回值*/
#include <iostream>
using namespace std;
class Animal{
public:
	virtual void run(){
		cout <<"Ainimal run()"<<endl;
	}
	void show(){
		cout <<"Animal show()"<<endl;
	}
};

class Dog:public Animal{
public:
	void run(){
		cout <<"Dog run()"<<endl;
	}
	void show(){
		cout <<"dog show()"<<endl;
	}
};

class Cat:public Animal{
public:
	void run(){
		cout <<"cat run()"<<endl;
	}
};
/*多态用作函数参数*/
void showAnimal(Animal * animal){
	animal->show();
	animal->run();
}
/*多态用作返回值*/
Animal * getAnimal(int x){
	if (1 == x)
		return new Dog();
	if (2 == x)
		return new Cat();
}
int main(){
	Cat cat;
	showAnimal(&cat);
	Dog dog;
	showAnimal(&dog);
}

3、多态的实现原理

多态的实现主要依赖于下面的三个东西:

虚函数:成员函数加了virtual修饰

虚函数表指针:一个类型有虚函数,则对这个类型提供一个指针,这个指针放在生成对象的前四个字节。同类型的对象共享一张虚函数表。并且不同类型的虚函数表地址不同。

虚函数表:虚函数表中的每个元素都是虚函数的地址。

一个类型一旦出现虚函数,则会生成一张虚函数表,虚函数表中存放的就是虚函数的函数地址,通过这个函数地址可以到代码区中去执行对应的函数。虚函数表中只存放类型中的虚函数,不是虚函数的一概不管。在每个生成的类型对象的前四个字节中存放的是虚函数表指针,通过这个指针可以访问到虚函数表,从而访问其中的函数。同种类型共享虚函数表,不同类型有自己独立的虚函数表,继承关系中的子类和父类属于不同类型,所以有自己独立的函数表。


/*多态的原理*/
#include <iostream>
#include <cstring>
using namespace std;
class Animal{
	int x;
public:
	virtual void fun(){
		cout<< "Aniaml fun()"<<endl;
	}
	virtual void run(){
		cout <<"Animal run()"<<endl;
	}
	void show(){
		cout <<"Animal show()"<<endl;
	}
};

class Dog:public Animal{
public:
	virtual void fun(){
		cout << "dog run"<<endl;
	}
	void run(){
		cout <<"dog run"<<endl;
	}
};

class Cat:public Animal{
public:
	void fun(){
		cout <<"cat fun"<<endl;
	}
};

int main(){
	Animal a;
	Animal b;
	/*取出虚函数表的地址并打印*/
	int * pi = (int*)&a;
	cout <<showbase<< hex << *pi<<endl;

	pi = reinterpret_cast<int*>(&b);
	cout <<showbase<< hex << *pi<<endl;

	/*子类不会和父类共享虚函数表,地址不一样*/
	Dog dog;
	pi = reinterpret_cast<int*>(&dog);
	cout <<showbase<< hex << *pi<<endl;

	Animal * pcat = new Cat();
	pcat->run();
	pcat->fun();

	/*更改dog的虚表的值,我们把dog的虚表地址
	改成cat的虚表地址*/
	Animal * pdog = new Dog();
	pdog->run();
	pdog->fun();

	/*更换dog的虚表地址,将cat的前四个字节
	移动到dog的前四个字节*/
	memcpy(pdog,pcat,4);
	pdog->run();
	pdog->fun();
	/*上面的更改后,狗变成了猫的特性*/
}

上述程序对应的内存图:


可以看出,一旦满足了多态的条件,程序自然按照上图的流程执行。

4、上面既然说了,虚函数表中存放的是函数的地址,那么能不能直接我们自己取出虚函数的地址,直接调用所需要的函数呢?

#include <iostream>
using namespace std;

class Animal{
public:
	virtual void run(int x){
		cout <<"run x="<<x<<endl;
	}
	virtual void fun(int x){
		cout <<"fun x="<<x<<endl;
	}
	void show(){
		cout <<"this is show()"<<endl;
	}
};


int main()
{
	/*去掉函数名就是函数指针的类型,指针简化操作*/
	typedef void (*MFUN)(Animal* a,int x);/*MFUN就是虚表中的函数指针类型*/

	typedef MFUN* VTABLE;//MFUN*就是虚表类型

	Animal animal;
	VTABLE vt = *((VTABLE*)&animal);
	/*虚函数表表现为函数指针数组*/
	vt[0](&animal,100);
	vt[1](&animal,123);
	return 0;
}

虚表中存放的就是虚函数的函数指针,可以理解为函数指针的数组,通过typedef将指针降级。


5,虚析构函数

virtual关键字只能修饰成员函数或者析构函数,其他的函数都不行。

当我们用new创建一个指向子类对象的父类指针时,例如Animal * animal = new Dog()时,其中Animal时父类,Dog是子类,并且delete animal时,其子类对象的析构函数不会被调用,只会调用父类的析构函数。所以就会遇到一个问题,如果子类对象有自己独立的堆内存时,这部分内存就无法释放。这时,我们只需要在父类的析构函数上用virtual修饰即可,子类析构函数就会被调用。

#include <iostream>
using namespace std;

class Animal{
public:
	Animal(){
		cout <<"Animal()"<<endl;
	}
	virtual ~Animal(){
		cout <<"~Animal()"<<endl;
	}
};
class Dog:public Animal{
public:
	Dog(){
		cout <<"Dog()"<<endl;
	}
	~Dog(){
		cout <<"~Dog()"<<endl;
	}
};
int main(){
	Animal * pa = new Dog();
	delete pa;
	/*子类析构函数的调用必然引发父类析构*/
}

虚析构函数的应用场景:

当父类中有虚函数时,应该吧父类的析构函数定义成虚析构函数。

子类和父类中都有自己的堆内存分配时。



以上是关于c++面向对象三大特征封装继承和多态知识总结的主要内容,如果未能解决你的问题,请参考以下文章

面向对象理解,封装继承多态知识总结

JavaSE基础知识—面向对象(5.4面向对象三大特征:封装继承多态)

C++ 面向对象的三大特征

C++作为面向对象语言的三个基本特征:封装,继承和————

Java面向对象三大特征之继承和多态

JS面向对象三大特征:封装、继承、多态