前言:面向对象编程的三个基本概念:数据抽象、继承和动态绑定(核心概念)
数据抽象:将类的接口与实现分离
继承:我们可以定义与其他类相似但完全不相同的新类
动态绑定:在使用这些彼此相似的类时,在一定程度上忽略他们的区别,统一使用它们的对象
而后面两个就是这篇文章着重要说的点了,文章只是初步总结,限于篇幅和个人水平,只能包含部分知识点,也只是入个门。
继承与虚函数
基本概念:
继承:
概念:
定义类的继承构成一种层次关系,在层次根部的为基类,其他类则直接或者间接的从基类中继承而来,称为派生类。
当实体化一个派生类的时候,类派生类部分和基类部分是各自调用自己的析构函数来初始化各自的成员的,也就是说,基类负责定义在层次关系中所有类共有的数据成员,而派生类定义各自特有的成员。
虚函数:
使用原因:
对于某些函数,基类希望它的派生类各自定义适合其自身的版本,基类会通过加virtual关键字,将该函数声明为虚函数!而派生类必须在其内部对所有重新定义的虚函数进行声明。
使用注意:
当使用的虚函数,我们使用引用或者指针调用一个虚成员函数时才会执行动态绑定,因为我们知道在程序运行时才知道到底调用了哪个版本的虚函数,所以所有虚函数都必须有定义。
关键词virtual只能出现在类内的声明函数语句之前,而不能用于类外部的函数定义,在派生类中相应的函数将隐式的是虚函数(不加virtual的情况)
ps:派生类的子类的virtual可加可不加
关于继承和虚函数语法的一些特点我会在注释里说明
下面是代码测试
#include <iostream>
using namespace std;
class x{
public:
friend x;//友元关系不能继承,每个类负责自己控制自己成员的访问权限
};
class A{
public:
//同一个作用域重载
A();//构造函数
void func1(){}
void func1(int a){}
void func2(){}
virtual void func3(){}
static int a; //若基类中定义了静态成员static,则在整个继承体系中只存在该成员的唯一定义,并且只有一份实例
};
class B: public A{//继承的写法,这里写的的public是指对于继承的基类,继承过来之后对于基类中成员的权限是什么,protected受保护的成员,基类希望它的派生类有权访问该成员,同时禁止其他用户访问,而private即使是其派生类也不能访问
public:
//using只是令某个名字在当前作用域可见,但是当作用于构造函数时,using声明语句将会使编译器产生代码,派生类继承基类的构造函数,其派生的部分成员将会默认初始化
using A::A;//继承了基类的构造函数
//重定义基类的func,隐藏了基类的func2方法
void func1(){}
//重写基类的func2(),也可以覆盖基类的func3()
virtual void func3(){}
};
//改变个别成员的可访问性
//有时候需要改变派生类继承的某个名字的访问级别,通过using声明
class Base{
public:
size_t size() const{ return n; }
protected:
size_t n;
};
class Derived : private Base //private继承
{
public:
//保持对象尺寸相关成员的访问级别
using Base::size;
protected:
using Base::n;
};
int main(){
return 0;
}
代码二
#include <iostream>
using namespace std;
//基类和派生类的虚函数必须要有相同的形参列表
//如果基类与派生类的虚函数接受的实参不同,则我们就无法通过基类的引用或指针调用派生类的虚函数
class A{
public:
A():i(10){}
virtual void f(){cout<<"A::f()"<<i<<endl;}
int i;
};
class B :public A{
public:
void f(int);//形参列表与base中的f不一致
//隐藏了基类的函数f,这里的f函数
//B继承了A::f()的定义
virtual void f2();//一个新的虚函数,在Base中不存在
};
class C : public B{
public:
void f(int); //非虚函数,隐藏了B::中的f函数
void f(); //覆盖了A中的虚函数f
void f2(); //覆盖了B的虚函数f2
void f2(int x);//派生类如果定义了一个与基类虚函数同名函数,但参数列表不相同的话,仍然是合法行为,编译器会认为该函数与基类虚函数是相互独立的,但这往往是把形参列表弄错了的错误,编译器发现不了,所以c++11规定在其后加上override表示其要对基类的函数进行覆盖,若未覆盖,编译器报错,我们可以发现自己的错误
};
int main(){
A a,b;
a.f();
cout<<sizeof(a)<<endl;
int *p = (int *)&a;
int *q = (int *)&b;
cout<< *p << *q <<endl;
return 0;
}
内层作用域中的函数不会重载外层作用域中的函数,所以派生类成员若有名字相同,即使其形参列表不一致,基类成员也会被隐藏掉—名字查找优先于类型检查
代码测试:
#include <vector>
#include <iostream>
#include <algorithm>
using namespace std;
struct base
{
void Foo14();
};
struct Derived:base
{
void Foo14(int);//会隐藏掉基类中的相应成员,若要覆盖,则需要形参列表一致
};
int main(int argc, char**argv)
{
Derived d;
base b;
b.Foo14();
d.Foo14(10);
d.Foo14();//错误
d.base::Foo14();
//基类和派生类的虚函数参数列表必须相同,
//并且可以通过基类的作用域运算符调用基类的虚函
//数,若派生类的与基类虚函同名的成员参数列表与
//虚函数不同,那么派生类中的成员并没有覆盖基类的相应虚函数,因为形参列表不一致,派生类将拥有两个同名成员。
return 0;
}
这里在说几点值得注意的地方:
1,若要将一个类当作基类来使用,那么这个类就必须是已经定义过的,仅仅声明是不可以的,因为派生类需要知道其基类到底是什么,所以类不能派生其本身,最终的派生类将包含所有间接类的相关成员。
2,类的声明中不能包含它的派生列表。
3,防止类被继承可以在其类名后加final,表示其不能作为基类。
4,派生类到基类的类型转换只针对与引用或者指针的类型,其本类型是不支持的,即对象之间不存在类型转换
5,派生类到基类的类型转换是因为派生类之中含有基类的部分,但是基类中并不含有派生类中的成员,所以一个基类对象既可能是以独立的形式存在,也可能是派生类对象的一部分,所以不存在从基类到派生类之间的自动类型转换(可以将派生类转化为基类)
6,当我们用一个派生类的对象给一个基类对象初始化或者赋值时,只有其基类的部分被拷贝、移动或者赋值,它的派生类部分将会被忽略掉
多继承
和java的定义不同的是,在c++中允许使用多继承,即一个派生类同时继承多个基类,但是如果当两个基类中有两个同样的成员,在派生类中使用这个成员函数的时候,应该说明是哪一个基类的数据,不然就会可能出现两个父类数据重名,导致无法编译链接失败。
代码测试如下:
#include <bits/stdc++.h>
using namespace std;
class Base1{
public:
int m_A;
Base1():m_A(10){}
};
class Base2{
public:
int m_B;
int m_A;
Base2():m_B(10),m_A(20){}
};
class Son: public Base1,public Base2
{
public:
int m_C,m_D;
};
//多继承语法,打印父类中数据时,应该说明是哪一个父类的数据,不然就会可能出现两个父类数据重名
void test01(){
cout<<sizeof(Son)<<endl;
Son ccc;
cout<<sizeof(ccc)<<endl;
}
int main(){
test01();
return 0;
}
多继承虽然更加方便,但是还会出现一些问题,比如菱形继承。
菱形继承
概念:两个派生类继承了同一个基类,而基类中又有某个类同时继承着两个派生类,这种继承成为菱形继承。
举例:(图源网络,侵删)
带来的问题:
1.羊继承了动物的数据和函数,拖同样继承了动物的数据和函数,当草泥马调用函数或数据时,如果没有显示的声明作用域,会产生二义性。
2.草泥马继承自动无的函数和数据继承了两份,但是数据只需要一份就行了,造成了内存浪费。
下面是菱形继承的解决方案,使用虚继承
#include <bits/stdc++.h>
using namespace std;
class Animal{
public:
int age;
};
class sheep :virtual public Animal//加上virtual关键字,虚继承
{
public:
};
class Tuo :virtual public Animal//加上virtual关键字,虚继承
{
public:
};
//加上virtual是虚继承,animal类属于虚基类
//在sheep和tuo类中继承的内容Wiecbptr 虚基类指针
//v = virtual
//b = base
//ptr = pointer
//vbptr 指针指向的是虚基类表 vbtable
//vbtable中有偏移量,通过偏移量可以找到唯一的一份数据,不会出现多份
//这里就解决了菱形继承会浪费一份内存
class Sheeptuo:public sheep,public Tuo
{
public:
};
int main(){
Sheeptuo a;
//访问父类数据必须加作用域,不然会导致二义性
//如果是虚继承的话,就可以不加
/* a.Tuo::age = 10; */
/* a.sheep::age = 20; */
a.age = 30;
cout<<a.sheep::age;
cout<<a.Tuo::age;
cout<<a.age;
return 0;
}
纯虚函数
在子类继承父类时,有时候存在一种情况是,我们需要在父类中不会用到这个函数,但是需要在不同子类中,这个成员函数表现出他独特的功能,但是如果只是在父类中定义一个没有用的虚函数,却又浪费空间,而且导致代码不易懂,所以为了更加规范代码,c++定义了一种特殊的虚函数,叫做纯虚函数,他具有以下特征:
1,具有纯虚函数的代码无法实例化生成对象。
2,子类必须重写父类的纯虚函数,不然两个函数都是不可生成对象的,那么这两个类也就没有意义。
使用纯虚函数写的代码:
#include <iostream>
using namespace std;
//抽象层
//抽象的cpu
class CPU
{
public:
virtual void calculate() = 0;
};
//抽象的显卡
class Videocard{
public:
virtual void videocard() = 0;
};
//抽象的内存
class Memory
{
public:
virtual void storge() = 0;
};
//电脑类
class computer
{
public:
CPU * cpu;
Videocard *card;
Memory *mem;
computer(CPU * cpu,Videocard *card,Memory *mem){
this->cpu = cpu;
this->card = card;
this->mem = mem;
}
void dowork(){
this->cpu->calculate();
this->card->videocard();
this->mem->storge();
}
~computer(){
if(cpu!=nullptr){
delete cpu;
cpu = nullptr;
}
if(card!=nullptr){
delete card;
card = nullptr;
}
if(mem!=nullptr){
delete mem;
mem = nullptr;
}
}
};
//上面全是抽象,下面是实现
class intelCPU : public CPU
{
public:
virtual void calculate()
{
cout << "intel的cpu开始计算"<<endl;
}
};
class interMemory : public Memory
{
public:
virtual void storge(){
cout<<"intel的内存条开始存储了"<<endl;
}
};
class intelVideocard : public Videocard
{
public:
virtual void videocard(){
cout<<"intel的videocard开始使用了"<<endl;
}
};
class legionCPU : public CPU
{
public:
virtual void calculate()
{
cout << "legion的cpu开始计算"<<endl;
}
};
class legionMemory : public Memory
{
public:
virtual void storge(){
cout<<"legion的内存条开始存储了"<<endl;
}
};
class legionVideocard : public Videocard
{
public:
virtual void videocard(){
cout<<"legion的videocard开始使用了"<<endl;
}
};
void test01()
{
//第一台电脑组装
CPU *cpu = new intelCPU;
Memory * memory = new interMemory;
Videocard * videocard = new intelVideocard;
computer myComputer(cpu,videocard,memory);
myComputer.card->videocard();
myComputer.cpu->calculate();
myComputer.mem->storge();
cout<<"--------------------------------"<<endl;
//第二台电脑组装
CPU *cpu2 = new legionCPU;
Memory * memory2 = new legionMemory;
Videocard * videocard2 = new legionVideocard;
computer *myComputer2 = new computer(cpu2,videocard2,memory2);
myComputer2->dowork();
delete myComputer2;
cout<<"--------------------------------"<<endl;
//第二台电脑组装
CPU *cpu3 = new legionCPU;
Memory * memory3 = new interMemory;
Videocard * videocard3 = new legionVideocard;
computer *myComputer3 = new computer(cpu3,videocard3,memory3);
myComputer3->dowork();
delete myComputer3;
}
int main(){
test01();
return 0;
}
虚析构
主要解决的问题是:
当使用多态时,一个子类实例化对象时,
当子类空间中有堆区内容,如果定义父类中的析构函数为普通的析构函数,那么他不会在去找子类的析构函数,最后会导致释放的时候导致释放的不干净,内存泄露。
ps:下面要说的纯虚析构也可以解决这个问题。
//virtual ~Animal(){
//cout <<“Animal的析构函数调用”<<endl;
//}
纯虚析构
纯虚析构函数和普通纯虚函数不同,纯虚析构函数需要使用,要有声明也必须有实现,但是实现是在类外实现。
同样如果一个类中有了纯虚析构函数,那么这个类也属于抽象类。
作用: 在虚析构函数的基础上使得基类成为抽象类。
所以主要有如下两个作用:
1.删除对象时,所有子类都能进行动态识别
2.使得基类成为抽象类
注意:
由于最终会调用到基类的析构函数,所以即使基类的析构函数为纯虚的也要给出析构函数的实现,否则产生链接错误 。
当基类的析构函数为纯虚析构函数时,派生类既是不实现析构函数也是可以实例化的,这是因为编译器会为其提供默认的析构函数。
上面说的问题使用纯虚析构和虚析构的解决代码:
#include <bits/stdc++.h>
using namespace std;
class Animal{
public:
Animal(){
cout<<"动物构造"<<endl;
}
/* virtual ~Animal(){ */ //虚析构写法
/* cout<<"动物析构"<<endl; */
/* } */
virtual ~Animal() = 0;//纯虚析构写法,类内声明类外定义
virtual void speak(){
cout<<"动物在说话\n";
}
};
Animal::~Animal(){
cout<<"动物析构"<<endl;
}
class Cat: public Animal{
public:
Cat() = default;
Cat(const char * name){
cout<<"猫构造\n";
namedata = new char[strlen(name+1)];
strcpy(namedata,name);
}
~Cat(){
cout<<"猫析构\n";
if(namedata != nullptr){
delete namedata;
}
}
virtual void speak(){
if(namedata!=nullptr)
cout<<namedata<<"小猫在说话\n";
else
cout<<"小猫在说话\n";
}
char * namedata;
};
int main(){
shared_ptr<Animal> a(new Cat("hhhh"));
/* a = new Cat("hhhh"); */
a->speak(); //如果不声明父类析构函数是虚函数,则只进行父类的析构函数
//而不进行子类的析构,如果子类中没有堆区空间,则无所谓,如果有内存当场泄露
/* delete a; */
return 0;
}