03C++复习.继承/多态
面向对象的三个基本特征是:封装、继承、多态。其中,封装可以隐藏实现细节,使得代码模块化;继承可以扩展已存在的代码模块(类);它们的目的都是为了——代码重用。而多态则是为了实现另一个目的——接口重用!
封装:将一类事物所共有的属性和行为方法进行总结,封装在一个类中。该类的形成过程称为封装,该封装的类实例化的对象具有类的所有属性和行为方法。
封装的类的内部按照访问限定符可以分为:(public:)公有部分、(protected:)保护部分、(private:)私有部分。
一、继承
1、简介
面向对象程序设计中最重要的一个概念是继承。继承允许我们依据另一个类来定义一个类,这使得创建和维护一个应用程序变得更容易。这样做,也达到了重用代码功能和提高执行时间的效果。
当创建一个类时,您不需要重新编写新的数据成员和成员函数,只需指定新建的类继承了一个已有的类的成员即可。这个已有的类称为基类,新建的类称为派生类。
2、分类
继承分为:公有继承 、 保护继承 、 私有继承。
(1)公有继承: (格式: class Derived:public Base)
父类的共有部分继承到子类的公有部分,父类的保护部分继承到子类的保护部分,父类的私有部分继承到子类的私有部分(父类的私有部分在子类不可访问)。
(2)保护继承: (格式: class Derived:protected Base)
父类的共有部分继承到子类的保护部分,父类的保护部分继承到子类的保护部分,父类的私有部分继承到子类的私有部分(父类的私有部分在子类不可访问)。
(3)私有继承: (格式: class Derived:private Base)
父类的所有都继承到子类的私有部分,父类的私有部分在子类不可访问。
注意:不管何种类型的继承关系,父类私有成员到子类中都不能被访问。
3、实例
(1)公开继承
using namespace std;
//基类A
class A
{
public:
int a;
int geta()
{
a = 300;
return a;
}
/*保护类型成员在本类和子类中可以访问*/
protected:
int b;
private:
int c;
};
//派生类B
class B:public A//公开继承
{
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;
}
(2)私有继承
#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();
}
4、友元类
突破成员访问权限,可以设置合理的访问接口,也可以使用友元类。下面我们看一下,友元类的使用:
#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; c
out <<a.x<< "---" << a.y << endl;
}
};
int main()
{
B b;
b.show();
C c;
c.show();
}
5、继承中的构造函数、析构函数、重载赋值运算符和拷贝构造函数
构造函数和析构函数是不能被继承的,但是可以被调用。并且子类一定会调用父类的构造函数;
子类创建对象时,首先调用父类的构造函数,再调用子类自己的构造函数;子类创建的对象被释放时,先调用子类自己的析构函数,再调用父类的析构函数
被继承,但是可以被调用,而且子类肯定会调用父类的构造函数
和析构函数。这种机制可以很自然的用于访问父类的私有成员*/
#include <iostream>
using namespace std;
class A
{
private:
int x;
public:
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的析构*/
}
拷贝构造函数和赋值运算符函数也不能被继承:在子类不提供拷贝构造和赋值运算符时,子类默认调用父类的赋值运算符和拷贝构造函数。但子类一旦提供拷贝构造和赋值运算符函数则不再调用父类拷贝构造和赋值运算符函数。
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;
}
6、名字隐藏机制
名字隐藏机制:子类中如果定义了和父类中同名的数据,这些数据包括成员变量和成员函数。则会把父类中的数据隐藏掉。
注意:只要名字相同,计算返回值或者形参列表不同,也会被隐藏。隐藏不代表就没有了,可以通过类名作用域::访问到被隐藏的成员。
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();
}
7、多继承和函数重写
多继承是c++特有的语法机制,表现为一个子类有多个直接的父类。
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这个成员只需要复制一份就可以了,因为多余的复制是无意义的。首先采用顶层抽象的方式,将三个父类抽象到更高的层面上。
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;
}
};
//C++ 虚继承
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的构造函数只被调用了一次。
8、虚函数
在函数前面加上virtual关键字修饰过的就是虚函数:
using namespace std;
class A{
int x;
public:
virtual void show(){}
virtual void showa(){}
};
int main(){
cout << sizeof(A) << endl;
}
虚函数的主要表现为会占用四个字节的空间,只要成员中出现虚函数,不管有多少个虚函数,都只用四个字节来维护这个虚关系。虚函数会影响对象的大小。维护虚关系使用一个指针来维护的,所以是四个字节。
9、函数重写
在父类中出现一个虚函数,如果在子类中提供和父类同名的函数(注意区分名字隐藏),这就加函数重写。
函数重写要求必须有相同函数名,相同的参数列表,相同的返回值。
二、多态
1、简介
多态:一个父类型的对象的指针或者引用指向或者是引用一个子类对象时,调用父类型中的虚函数,如果子类覆盖了虚函数,则调用的表现是子类覆盖之后的。
多态产生的必要条件:
(1)继承是构成多态的基础;
(2)虚函数是构成多态的关键;
(3)函数覆盖是构成多态的必备条件;
(4)多态的应用:函数参数,函数返回值。
#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);
}
2、多态的实现原理
多态的实现主要依赖于下面的三个东西:
虚函数:成员函数加了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(); /*上面的更改后,狗变成了猫的特性*/
}
虚表中存放的就是虚函数的函数指针,可以理解为函数指针的数组,通过typedef将指针降级。
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;
}
3、虚析构函数
virtual关键字只能修饰成员函数或者析构函数,其他的函数都不行。
当我们用new创建一个指向子类对象的父类指针时,例如Animal * animal = new Dog()时,其中Animal时父类,Dog是子类,并且delete animal时,其子类对象的析构函数不会被调用,只会调用父类的析构函数。所以就会遇到一个问题,如果子类对象有自己独立的堆内存时,这部分内存就无法释放。这时,我们只需要在父类的析构函数上用virtual修饰即可,子类析构函数就会被调用。
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;
/*子类析构函数的调用必然引发父类析构*/
}
虚析构函数的应用场景:
(1)当父类中有虚函数时,应该把父类的析构函数定义成虚析构函数。
(2)子类和父类中都有自己的堆内存分配时。
转自https://blog.csdn.net/sinat_33924041/article/details/83617501
------------恢复内容结束------------
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通