虚函数
virtual修饰的成员函数就是虚函数
-
虚函数对类的内存影响:需要增加一个指针类型的内存大小
-
无论多少虚函数,只会增加一个指针类型的内存大小(如果是64位操作系统就是八字节,32位系统就是4字节)
-
虚函数表的概念: 指向虚函数的指针
-
我们自己也可以通过虚函数表指针去访问函数(一般做这样的操作不写数据类型)
class MM
{
public:
virtual void print() //1.会写
{
cout << "第一个虚函数" << endl;
}
virtual void printData()
{
cout << "第二个虚函数" << endl;
}
protected:
};
int main()
{
cout << sizeof(MM) << endl; //2.对类内存影响
MM mm;
mm.print();
mm.printData();
//了解一下.32位没问题,64位 vs2022 有问题
int** pObject = (int **)(&mm);
typedef void(*PF)();
PF pf = (PF)pObject[0][0];
pf(); //调用第一个虚函数
pf = (PF)pObject[0][1];
pf(); //调用第二个虚函数
return 0;
}
这里我们只需要知道有虚函数指针即可,如果类中有数据一般不用指针去调用函数,因为算地址偏移会比较麻烦,而且32位系统和64位系统的地址偏移还不相同
纯虚函数
具有一个或者多个纯虚函数的类型称之为抽象类,抽象类特性:
-
抽象类不能创建对象
-
抽象类可以创建对象指针
纯虚函数也是一个虚函数,所以也需要virtual修饰,纯虚函数是没有函数体,函数=0
//抽象类
class MM
{
public:
//纯虚函数
virtual void print() = 0;
protected:
string name;
};
int main()
{
//MM object; 抽象类不能构建对象 报错
MM* pMM = nullptr;
return 0;
}
虚析构函数
virtual修饰的析构函数 就是虚析构函数
-
使用的条件是:当父类指针被子类对象初始化的时候需要用虚析构函数
-
所有析构函数底层解析其实函数名相同(所以会出现一个覆盖问题,覆盖是指,子类中有与父类同名的函数,根据就近原则,它就不会调用父类的函数)
下面就看代码,如果不加virtual修饰析构函数,它的运行结果如下:
class MM
{
public:
void print()
{
cout << "MM::print" << endl;
}
~MM() //普通析构函数
{
cout << "~MM" << endl;
}
};
class Son :public MM
{
public:
void print()
{
cout << "Son::print" << endl;
}
~Son()
{
cout << "~Son" << endl;
}
};
int main()
{
MM* pMM = new Son; //构造子类对象,必须构造父类对象在构造自身
delete pMM;
pMM = nullptr;
/*
打印结果:~MM
*/
return 0;
}
我们会发现它的打印结果少了对子类对象的析构,这就是问题所在
如果我们加上virtual修饰,他就会把所构造的对象都给析构,运行结果如下:
class MM
{
public:
void print()
{
cout << "MM::print" << endl;
}
virtual ~MM() //虚析构函数
{
cout << "~MM" << endl;
}
};
class Son :public MM
{
public:
void print()
{
cout << "Son::print" << endl;
}
~Son()
{
cout << "~Son" << endl;
}
};
int main()
{
MM* pMM = new Son; //构造子类对象,必须构造父类对象在构造自身
delete pMM;
pMM = nullptr;
/*
打印结果:~Son
~MM
*/
return 0;
}
虚函数和多态
多态概念: 指在继承中指针的同一行为的不同结果,举个栗子(男生和女生上厕所,都是上厕所的行为,男生站着,女生蹲着)
多态的概念并不重要,重要的是需要知道那个对象指针在特定情况调用那个成员才是重要
实现多态的三个前提条件:
-
必须是public继承
-
必须父类存在virtual类型的成员函数,并且子类中存在该函数的同名函数
-
一定存在指针的引用
1. 正常的对象访问,不存在多态,都是就近原则(除非有特殊说明)
class MM
{
public:
void print()
{
cout << "MM::print" << endl;
}
virtual void printData()
{
cout << "MM virtual printData" << endl;
}
virtual ~MM(){}
};
class Son :public MM
{
public:
void print()
{
cout << "Son::print" << endl;
}
void printData()
{
cout << "Son printData" << endl;
}
~Son(){}
};
int main()
{
//正常对象的访问,不存在多态
//都是就近原则
cout << "正常对象访问" << endl;
MM mmobject;
mmobject.print();
mmobject.printData();
Son sonobject;
sonobject.print();
sonobject.printData();
/*打印结果:
正常对象访问
MM::print
MM virtual printData
Son::print
Son printData
*/
return 0;
}
2. 正常指针访问,也不存在多态,也是就近原则
//正常的指针访问
cout << "正常指针访问" << endl;
MM* pMM = new MM;
pMM->print();
pMM->printData();
Son* pSon = new Son;
pSon->print();
pSon->printData();
/*打印结果:
正常对象访问
MM::print
MM virtual printData
Son::print
Son printData
*/
3.非正常指针访问(父类指针用子类初始化)
//非正常的初始化
//父类指针被子类初始化
cout << "不正常的指针赋值" << endl;
MM* pObject = new Son;
pObject->print(); //没有virutal 看指针类型 调用MM::print
pObject->printData(); //有virtual 看对象 调用Son::printData
pObject = new MM;
pObject->printData(); //调用MM
/* 打印结果:
不正常的指针赋值
MM::print
Son printData
MM virtual printData
*/
这里我们需要记住一个东西,如果这个函数是普通函数,那么就看指针的类型是什么,就调用该类型的函数;如果这个函数是虚函数,那么就看对象是什么,就调用该对象的函数
4.非正常的引用(父类引用子类)
//非正常的引用
cout << "非正常的引用" << endl;
Son sonobject;
MM& mmobject = sonobject;
mmobject.print();
mmobject.printData();
/* 打印结果:
非正常的引用
MM::print
Son printData
*/
虚函数在继承特殊现象
虚函数在继承之后,会一直保持其虚函数的地位
class A
{
public:
virtual void print()
{
cout << "A" << endl;
}
};
class B :public A
{
public:
void print()
{
cout << "B" << endl;
}
};
class C :public B
{
public:
void print()
{
cout << "C" << endl;
}
};
int main()
{
B* pb = new C;
pb->print(); //调用C::print
pb = new B;
pb->print(); //调用B::print
/* 打印结果:
C
B
*/
return 0;
}
可以看到,B继承A之后,B中的print并没有加上virtual关键字修饰,但是C继承B类的时候,B中的print也是虚函数
重写 :子类实现父类虚函数的同名函数就被称为重写操作
下面有两种新的在重写中的关键字:final 和 override
- final:父类中用来禁止子类重写同名方法(如果父类中的虚函数被该关键字修饰,那么子类就没有办法重写该函数)
- override: 强制重写,起说明作用,表示当前子类当前方法是重写父类,仅表示子类中的该函数是重写父类中的同名函数,可写也可不写
纯虚函数和ADT
ADT: 抽象数据类型
抽象类本身不能创建对象,但是子类如果重写类父类中纯虚函数,子类是可以被允许创建对象
抽象类一般用于架构项目,构建好整个项目模块,具体细致工作可以交给子类去实现
采用ADT方式设计项目,可以把这个模块构建出来,并且测试代码也可以提前完成。
下面用代码来解释:
//抽象学生类
class Student
{
public:
virtual void printStudent() = 0;
};
//抽象系统类---ADT
//析构函数一定写虚析构函数
class ManagerialSystem //学生管理系统
{
public:
virtual ~ManagerialSystem() {}
//以下就是该管理系统所具有的方法
//这些都可以在子类中进行实现
virtual void insertData(Student* student) = 0;
virtual void printData()const = 0;
virtual int size() const = 0;
virtual bool empty() const = 0;
};
class ArraySystem :public ManagerialSystem //用数组方式管理
{
public:
//以下就是实现父类中的纯虚函数
void insertData(Student* product)
{
}
void printData()const
{
}
int size() const
{
return 0;
}
bool empty() const
{
return 0;
}
};
class ListSystem :public ManagerialSystem //用链表方式管理
{
public:
void insertData(Student* product)
{
}
void printData()const
{
}
int size() const
{
return 0;
}
bool empty() const
{
return 0;
}
};
int main()
{
ManagerialSystem* p = new ArraySystem;
p->printData(); //ArraySystem中的printData
//UI中用的比较多
//MFC --->不需要自己创建,只需要重写一个,构建对象即可
p = new ListSystem; //ListSystem中的printData
p->printData();
return 0;
}
我们要注意一个点,在ADT中,子类所实现的父类中的纯虚函数必须所有形式一样,仅只能去除virtual关键字,比如代码中的返回值类型,函数名,函数成员类型const也不能省略
上述代码的解释就是,管理系统的方法已经构架完成,并且主函数中的测试代码也可以提前写好,后续工作只需要给子类实现父类中的所有纯虚函数即可让代码完全运行