一,封装性
封装是面向对象编程的核心思想,简单点说就是,我把某些东西封装起来,这些关键的核心的东西不能给你看,但是我可以提供给你一些简单使用的方法。
就说现在最最常用的手机,(与用户而言)用户都会用,打电话、发短信、玩游戏、刷视频等等,但你知道手机怎么实现这些功能的吗??不知道吧,我们会用就可以了,怎么实现的对我们来说不重要。那这就很类似封装的理念了。即我不关心你的实现过程,你只要写好实现功能的接口供我调用即可。
Cpp(C plus plus)是向下兼容C的语言
C++是基于面向对象的语言 C是面向过程的语言(关心函数如何实现)
JAVA是完全面向对象的语言(不关心实现,关注直接对方法(即函数)的调用)
C++中类就是对具体事物的一种封装,类中的方法等等也是一种封装。我们把数据、一系列操作数据的函数封装到方法中,然后通过权限修饰符控制哪些方法可以让外知道,哪些只能自己知道,这样就能减少核心的数据被外界获取甚至破坏。
我们最初学习C++时,往往都是把代码直接写在main方法中的,但是随着学习的深入,遇到的逻辑越来越复杂,我们发现只靠main方法是不能满足全部需要的,这时候,我们开始在类中扩展其他方法,最后通过main方法调用运行。再后来我们逐渐开始去写不同的类,甚至不同的业务模块。这时候就会发现,一个简单的封装能带来多大的好处。
通过封装,我们可以保护代码被破坏,提高数据安全性。
使用者只能通过事先定制好的方法来访问数据,可以方便地加入控制逻辑限制对属性的不合理操作。
通过封装,我们提高了代码的复用性(有些方法、类在很多地方都能多次反复使用)
通过封装,带来的高内聚和低耦合,使用不同对象、不同模块之间能更好的协同,同时便于修改,增强代码的可维护性
高内聚 :类的内部数据操作细节自己完成,不允许外部干涉;
低耦合 :仅对外暴露少量的方法用于使用
#include<iostream>
using namespace std;
class A
{
public:
void PrintA()//我们定义的方法,可以使用方法来访问,修改私有成员数据
{
cout << _a << endl;
}
private:
int _a=10;//私有成员数据,对象是不能直接访问的,但是可以调用方法来访问及修改
//我们也可以将方法写入私有数据成员(前提这个方法是辅助方法,一般不需要进行修改),我们可以通过公有成员的方法来调用私有成员的方法
};
int main()
{
A a;//创建·对象
a.PrintA();//对象 对 方法的调用
return 0;
//A* p = nullptr;
//p->PrintA();//没有对象要对this指针操作//对数据操作就会崩溃
//return 0;
}
二,继承
什么是继承?
继承(inheritance)机制是面向对象程序设计中使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能。这样产生的新类,称派生类(或子类),被继承的类称基类(或父类)
继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。之前接触的复用都是函数复用,继承是类设计层次的复用。
//Person类
class Person
{
public:
void Print()
{
cout << "name:" << _name << endl;
cout << "age:" << _age << endl;
}
protected:
string _name = "peter"; // 姓名
int _age = 18; // 年龄
};
//Student类,学生有名字和年龄,所以用继承复用Person的代码
class Student : public Person//共有继承Person类
{
protected:
int _stuid; // 学号
};
//Teacher类,老师有名字和年龄,所以用继承复用Person的代码
class Teacher : public Person//共有继承Person类
{
protected:
int _jobid; // 工号
};
int main()
{
Student s;
Teacher t;
s.Print();
t.Print();
return 0;
}
继承 继承的是成员,也就是说包括成员变量和成员函数,
继承后基类成员的访问权限
特征
- 基类private成员无论以什么方式继承到派生类中都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。
- 基类private成员在派生类中不能被访问,如果基类成员不想在派生类外直接被访问,但需要在派生类中访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
- 上面的表格看起来复杂,实际上归纳一下就会发现:基类的私有成员在子类都是不可见;基类的其他成员在子类的访问方式就是访问限定符和继承方式中权限更小的那个(权限排序:public>protected>private)。
- 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,但最好显式地写出继承方式。
- 在实际使用时一般都是public继承,几乎很少使用protetced/private继承,也不提倡使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,扩展和维护性不强。
2.基类和派生类赋值转换
//Person类
class Person
{
public://这里将成员都定义为public便于测试
string _name = "peter"; // 姓名
int _age = 18; // 年龄
};
//Student类,学生有名字和年龄,所以用继承复用Person的代码
class Student : public Person
{
public://这里将成员都定义为public便于测试
int _stuid = 1; // 学号
};
int main()
{
Student s;//子类
Person p;//父类
p = s;//子类对象可以赋值给父类对象
s = p;//父类对象不能赋值给子类对象
return 0;
}
子类赋值给父类可以正常编译,父类赋值给子类编译报错。
以切片规则,更好的去理解继承
父类的数据子类均有,用子给父赋值合情合理,但是父给子赋值,父缺少成员,子有的成员是无法完成赋值的,故父不能给子赋值
指针及引用
赋值兼容规则
3.继承中的作用域
基类和派生类都是独立的作用域,在不同作用域内可以定义同名的变量、函数而不会发生冲突,所以在子类访问这些同名的内容时就需要注意。
1.同名成员变量
如果子类和父类中有同名成员,子类成员将屏蔽对父类同名成员的直接访问,这种情况叫隐藏(正是因为父类的方法不行,子类才自己写了一个同名方法)(但在子类成员函数中,可以使用父类::父类成员来显式地进行访问)。
//Student的_name和Person的_name构成隐藏关系
//从编译角度来看,代码没有问题,但是从逻辑角度来看,两个同名变量非常容易混淆
class Person
{
protected:
string _name = "父类"; // 姓名
int _num = 111; // 身份证号
};
class Student : public Person
{
public:
void Print()
{
cout << "姓名:" << _name << endl;
cout << "学号:" << _num << endl;
}
protected:
string _name = "子类"; // 姓名
};
int main()
{
Student s;
s.Print();
return 0;
}
如下图所示,在子类中直接访问时访问的是子类中的_name,如果想要访问父类的_name就需要再前面加上域作用限定符,指定访问Person类内的_name。
在子类默认访问子,想访问父的同名变量 得加 类的作用域
2.同名成员函数
要注意的是,在父类和子类内同名的成员函数并不构成函数重载,因为函数重载的前提是两个函数在同一作用域。
成员函数的隐藏,只需要函数名相同就构成隐藏,对参数列表没有要求。
class A
{
public:
void func()
{
cout << "父类" << endl;
}
};
class B : public A
{
public:
void func()
{
cout << "子类" << endl;
}
};
int main()
{
B b;
b.func();
b.A::func();
return 0;
}
使用作用域,可以调动父的成员方法;
4.派生类的默认成员函数
派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函
数,则必须在派生类构造函数的初始化列表阶段显式调用。
下面的代码中Person有默认构造函数,则在子类的构造函数中,会先调用父类的默认构造函数完成父类成员的初始化,然后在调用子类的构造函数初始化子类的成员。
子可以继承父的一切,唯有构造及析构无法继承
class Person
{
public:
//父类构造函数
Person(string name = "父类")//父类有默认构造函数
:_name(name)
{
cout << "Person()" << endl;
}
protected:
string _name;
};
class Student : public Person
{
public:
//子类构造函数
Student(string name, int id)
:_id(id)
{
cout << "Student()" << endl;
}
protected:
int _id;
};
int main()
{
Student s("tom", 1);
return 0;
}
下面的代码中,父类没有默认构造函数(简单说就是不传参无法初始化),则需要在子类的构造函数中显式调用父类构造函数完成父类成员的初始化。
class Person
{
public:
//父类构造函数
Person(string name)//如果不传参无法初始化
:_name(name)
{
cout << "Person()" << endl;
}
protected:
string _name;
};
class Student : public Person
{
public:
//子类构造函数
Student(string name, int id)
: Person(name)//调用父类构造函数初始化
, _id(id)
{
cout << "Student()" << endl;
}
protected:
int _id;
};
int main()
{
Student s("tom", 1);
return 0;
}
拷贝构造
拷贝构造的逻辑和构造函数基本相同,需要注意的就是在子类中调用父类的拷贝构造时,直接传入子类对象即可,父类的拷贝构造会通过“切片”拿到父类的那一部分。
class Person
{
public:
//父类拷贝构造函数
Person(const Person& p)
:_name(p._name)
{
cout << "Person(const Person& p)" << endl;
}
protected:
string _name;
};
class Student : public Person
{
public:
//子类拷贝构造函数
Student(const Student& s)
: Person(s)//直接传s,通过切片拿到父类的部分
, _id(s._id)
{
cout << "Student(const Student& s)" << endl;
}
protected:
int _id;
};
int main()
{
Student s1("tom", 1);
Student s2(s1);//拷贝构造
return 0;
}
赋值运算符重载
子类的operator=必须要显式调用父类的operator=完成父类的赋值。
class Person
{
public:
//父类的赋值运算符重载
Person& operator=(const Person& p)
{
if (this != &p)
{
cout << "Person& operator=(const Person& p)" << endl;
_name = p._name;
}
return *this;
}
protected:
string _name;
};
class Student : public Person
{
public:
//子类赋值运算符重载
Student& operator=(const Student& s)
{
if (this != &s)
{
cout << "Student& operator=(const Student& s)" << endl;
//注意不能这样写,因为父类的赋值运算符重载被隐藏了,这样写会调用子类的赋值运算符重载导致无穷递归、栈溢出
//operator=(s);
Person::operator=(s);//指定作用域调用父类的赋值运算符重载,直接传s即可(会切片)
_id = s._id;
}
return *this;
}
protected:
int _id;
};
int main()
{
Student s1("tom", 1);
Student s2(s1);//拷贝构造
Student s3("jerry", 3);
s1 = s3;//赋值运算符重载
return 0;
}
析构函数
由于栈的特性,构造函数调用时先调父类再调子类,所以析构时先析构子类,再析构父类,编译器为了能保证这个特性,默认在子类析构完成后调用父类的析构函数,所以不需要在子类的析构函数中显式地调用父类的析构函数。
class Person
{
public:
//父类的析构函数
~Person()
{
//父类没什么资源需要处理,所以父类的析构需要做的事以打印代替
cout << "~Person()" << endl;
}
protected:
string _name;
};
class Student : public Person
{
public:
//子类的析构函数
~Student()
{
//不需要显式调用父类的析构函数,编译器会自动调用
//Person::~Person();
//子类没什么资源需要处理,所以子类的析构需要做的事以打印代替
cout << "~Student()" << endl;
}
protected:
int _id;
};
int main()
{
Student s1("tom", 1);
return 0;
}
5.友元和静态成员
友元关系无法继承,如果需要友元关系需要在父类和子类中都声明。
父类的静态成员在整个类体系中都只有一个,无论下面有多少层继承关系。
这两点了解一下即可。
6.菱形继承
1.继承类型
单继承:一个子类只有一个直接父类的继承关系
代代单传
多继承:一个子类有两个或以上直接父类的继承关系。
菱形继承:单继承和多继承组合后的一种特殊情况。
// 乱伦(bushi)
2.菱形继承
菱形继承会有数据二义性的问题,以下面的代码为例进行说明,该代码的继承关系同上。
class Person
{
public:
string name;
};
class Student : public Person
{
public:
int studentNum;
};
class Teacher : public Person
{
public:
int teacherNum;
};
class Assistant : public Student, public Teacher
{
public:
int age;
};
int main()
{
Assistant ast;
ast.name = "a";
ast.age = 18;
ast.studentNum = 111;
ast.teacherNum= 222;
return 0;
}
这里代码编写好后,如下图编译器会提示name不明确,因为它可能是Student类继承的name,也可能是Teacher类继承的name,而实际上只需要一个name即足够记录,所以有代码冗余和二义性的问题。
要想没有歧义,只能如下编写,但下面的代码显然很冗余,这也是菱形继承的问题所在。
int main()
{
Assistant ast;
//指定作用域
ast.Student::name = "a";
ast.Teacher::name = "a";
ast.age = 18;
ast.studentNum = 111;
ast.teacherNum = 222;
return 0;
}
3.虚继承
这一问题需要通过虚继承来解决,在继承方式前加上virtual。
class Person
{
public:
string name;
};
class Student : virtual public Person//虚继承
{
public:
int studentNum;
};
class Teacher : virtual public Person//虚继承
{
public:
int teacherNum;
};
class Assistant : public Student, public Teacher
{
public:
int age;
};
int main()
{
Assistant ast;
ast.Student::name = "a";
cout << ast.name << endl;
ast.Teacher::name = "b";
cout << ast.name << endl;
ast.name = "c";
cout << ast.name << endl;
return 0;
}
这样一来,main函数通过三种方式修改name,最后的结果都是同一个name被修改,这样就不会出现数据冗余和二义性的问题。
4.关于多继承
多继承是C++复杂的一个体现。有了多继承,就存在菱形继承,为了解决菱形继承,又出现了菱形虚拟继承,其底层实现又很复杂。所以一般不建议设计出多继承,一定不要设计出菱形继承。否则在复杂度及性能上都有问题。
三,多态
多态的概念
多态的概念:通俗来说,就是多种形态, 具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态 。
举个例子:比如 买票这个行为 ,当 普通人 买票时,是全价买票; 学生 买票时,是半价买票; 军人买票时是优先买票。
总结:多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如 Student 继承了 Person。 Person 对象买票全价, Student 对象买票半价。
多态的定义及实现
1.重写/覆盖 的要求
重写/覆盖: 子类中有一个跟父类完全相同的虚函数,子类的虚函数重写了父类的虚函数
即:子类父类都有这个虚函数 + 子类的虚函数与父类虚函数的 函数名/参数/返回值 都相同 -> 重写/覆盖(注意:参数只看类型是否相同,不看缺省值) 另外在父类中以父类的返回值的函数,在子类中要构成重写子类中的该函数就得以子类作为返回值!!!!
2.多态中的两个要求
1、被调用的函数必须是虚函数,子类对父类的虚函数进行重写 (重写:三同(函数名/参数/返回值)+虚函数)
2、父类指针或者引用去调用虚函数。
3.多态的切片
(1)示例1:给一个student的子类对象(临时对象也行),然后把这个对象赋给一个父类指针,通过这个父类指针就可以访问student子类的虚拟函数
(2)示例2:假设B是子类,A是父类,new一个B类的临时对象,然后把这个临时对象赋给一个父类指针A* p2,通过这个父类指针p2就可以访问子类B的虚拟函数func
class A
{
public:
virtual void func(int val = 1){ std::cout << "A->" << val << std::endl; }
virtual void test(){ func(); }
};
class B : public A
{
public:
void func(int val = 0){ std::cout << "B->" << val << std::endl; }
};
int main(int argc, char* argv[])
{
B*p1 = new B;
//p1->test(); 这个是多态调用,下有讲解 二->6
p1->func(); //普通调用
A*p2 = new B;
p2->func(); //多态调用
return 0;
}
4.多态演示:
买票场景下的多态 完整代码
普通人 买票时,是全价买票; 学生 买票时,是半价买票; 军人 买票时是优先买票。
class Person {
public:
Person(const char* name)
:_name(name)
{}
// 虚函数
virtual void BuyTicket() { cout << _name << "Person:买票-全价 100¥" << endl; }
protected:
string _name;
//int _id;
};
class Student : public Person {
public:
Student(const char* name)
:Person(name)
{}
// 虚函数 + 函数名/参数/返回值 -》 重写/覆盖
virtual void BuyTicket() { cout << _name << " Student:买票-半价 50 ¥" << endl; }
};
class Soldier : public Person {
public:
Soldier(const char* name)
:Person(name)
{}
// 虚函数 + 函数名/参数/返回值 -》 重写/覆盖
virtual void BuyTicket() { cout << _name << " Soldier:优先买预留票-88折 88 ¥" << endl; }
};
// 多态两个要求:
// 1、子类虚函数重写的父类虚函数 (重写:三同(函数名/参数/返回值)+虚函数)
// 2、父类指针或者引用去调用虚函数。
//void Pay(Person* ptr)
//{
// ptr->BuyTicket();
//}
void Pay(Person& ptr)
{
ptr.BuyTicket();
}
// 不能构成多态
//void Pay(Person ptr)
//{
// ptr.BuyTicket();
//}
int main()
{
int option = 0;
cout << "=======================================" << endl;
do
{
cout << "请选择身份:";
cout << "1、普通人 2、学生 3、军人" << endl;
cin >> option;
cout << "请输入名字:";
string name;
cin >> name;
switch (option)
{
case 1:
{
Person p(name.c_str());
Pay(p);
break;
}
case 2:
{
Student s(name.c_str());
Pay(s);
break;
}
case 3:
{
Soldier s(name.c_str());
Pay(s);
break;
}
default:
cout << "输入错误,请重新输入" << endl;
break;
}
cout << "=======================================" << endl;
} while (option != -1);
return 0;
}
5.虚函数重写的例外
协变(父类与子类虚函数返回值类型不同)
子类重写父类虚函数时,与父类虚函数返回值类型不同 称为协变。
虚函数重写对返回值要求有一个例外:协变,协变是子类虚函数与父类虚函数返回值类型不同,但子类和父类的返回值类型也必须是父子关系指针和引用。
子类虚函数没有写virtual,f依旧是虚函数,因为子类先继承了父类函数接口声明(接口部分是virtual A* f() ),重写是重写父类虚函数的实现部分( 重写函数实现部分是用子类虚函数的{ }里面的函数实现替代父类虚函数的{ }里面的函数实现 ) ps:我们自己写的时候子类虚函数也写上virtual
class A{};
class B : public A {};
// 虚函数重写对返回值要求有一个例外:协变,父子关系指针和引用
//
class Person {
public:
virtual A* f() {
cout << "virtual A* Person::f()" << endl;
return nullptr;
}
};
class Student : public Person {
public:
// 子类虚函数没有写virtual,f依旧时虚函数,因为先继承了父类函数接口声明
// 重写父类虚函数实现
// ps:我们自己写的时候子类虚函数也写上virtual
// B& f() {
virtual B* f() {
cout << "virtual B* Student::f()" << endl;
return nullptr;
}
};
int main()
{
Person p;
Student s;
Person* ptr = &p;
ptr->f();
ptr = &s;
ptr->f();
return 0;
}
6.接口继承和实现继承
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。所以就有了 子类虚函数没virtual,依旧是虚函数;子类虚函数使用的是父类虚函数的缺省参数,只是实现了重写
7.析构函数的重写-析构函数名统一会被处理成destructor()
只有派生类 Student 的析构函数重写了 Person 的析构函数,下面的 delete 对象调用析构函数,才能构成多态,才能保证 p1 和 p2 指向的对象正确的调用析构函数。函数名处理成destructor() 才能满足多态:如果父类的析构函数为虚函数,此时子类析构函数只要定义,无论是否加virtual关键字都与父类的析构函数构成重写,虽然父类与子类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。
class Person {
public:
virtual ~Person() {cout << "~Person()" << endl;}
};
class Student : public Person {
public:
virtual ~Student() { cout << "~Student()" << endl; }
};
int main()
{
Person* p1 = new Person;
Person* p2 = new Student;
delete p1;
delete p2;
return 0;
}
2.注意:期望delete ptr调用析构函数是一个多态调用, 如果设计一个类,可能会作为基类,其次析构函数最好定义为虚函数
class Person {
public:
virtual ~Person()
{
cout << "~Person()" << endl;
}
};
class Student : public Person {
public:
// Person析构函数加了virtual,关系就变了
// 重定义(隐藏)关系 -> 重写(覆盖)关系
virtual ~Student() //这里virtual加不加都行
{
cout << "~Student()" << endl;
delete[] _name;
cout << "delete:" << (void*)_name << endl;
}
private:
char* _name = new char[10]{ 'j','a','c','k' };
};
int main()
{
// 对于普通对象是没有影响的
//Person p;
//Student s;
// 期望delete ptr调用析构函数是一个多态调用
// 如果设计一个类,可能会作为基类,其次析构函数最好定义为虚函数
Person* ptr = new Person;
delete ptr; // ptr->destructor() + operator delete(ptr)
ptr = new Student;
delete ptr; // ptr->destructor() + operator delete(ptr)
return 0;
}
8.C++11 override 和 final
(1)final :修饰虚函数,表示该虚函数不能再被重写;修饰类,该类不能被继承
2. override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
override:override写在子类中,要求严格检查是否完成重写,如果没有完成重写就报错
9.重载、覆盖(重写)、隐藏(重定义)的对比
函数重载:在同一个作用域中,两个函数的函数名相同,参数个数,参数类型,参数顺序至少有一个不同,函数返回值的类型可以相同,也可以不相同。
重定义(也叫做隐藏)是指在继承体系中,子类重新定义父类中有相同名称的非虚函数 ( 参数列表可以不同 ) ,此时子类的函数会屏蔽掉父类的那个同名函数。
重写(也叫做覆盖)是指在继承体系中子类定义了和父类函数名,函数参数,函数返回值完全相同的虚函数。此时构成多态,根据对象去调用对应的函数。
10.抽象类
在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象,
(1)子类继承后也不能实例化出对象,只有重写纯虚函数,子类才能实例化出对象。
(2)父类的纯虚函数强制了派生类必须重写,才能实例化出对象 另外纯虚函数更体现出了接口继承。
(3)纯虚函数也可以写实现{ },但没有意义,因为是接口继承,{ }中的实现会被重写;不能初始化出对象,所以即便写了方法也没有任何意义
抽象类 -- 在现实一般没有具体对应实体
不能实例化出对象
间接功能:要求子类需要重写,才能实例化出对象
class Car
{
public:
virtual void Drive() = 0;
// // 实现没有价值,因为没有对象会调用他
// /*virtual void Drive() = 0
// {
// cout << " Drive()" << endl;
// }*/
};
class Benz :public Car
{
public:
virtual void Drive()
{
cout << "Benz-舒适" << endl;
}
};
class BMW :public Car
{
public:
virtual void Drive()
{
cout << "BMW-操控" << endl;
}
};
void Test()
{
Car* pBenz = new Benz;
pBenz->Drive();
Car* pBMW = new BMW;
pBMW->Drive();
}
三.多态的原理
1.虚函数介绍
被virtual修饰的成员函数称为虚函数,虚函数的作用是用来实现多态,只有在需要实现多态时,才需要将成员函数设置成虚函数,否则没有必要
2.虚函数表
和菱形虚拟继承的虚基表不一样,那个存的是偏移量
// 这里常考一道笔试题:sizeof(Base)是多少?
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
};
通过观察测试我们发现b对象是8bytes,除了_b成员,还多一个__vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关)对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表,。那么派生类中这个表放了些什么呢?我们接着往下分析
3.虚表存储
class Base
{
public:
virtual void Func1()
{
cout << "Base::Func1()" << endl;
}
virtual void Func2()
{
cout << "Base::Func2()" << endl;
}
void Func3()
{
cout << "Base::Func3()" << endl;
}
private:
int _b = 1;
};
class Derive : public Base
{
public:
virtual void Func1()
{
cout << "Derive::Func1()" << endl;
}
void Func3()
{
cout << "Derive::Func3()" << endl;
}
private:
int _d = 2;
};
int main()
{
cout << sizeof(Base) << endl;
Base b;
cout << sizeof(Derive) << endl;
Derive d;
Base* p = &b;
p->Func1();
p->Func3();
p = &d;
p->Func1();
p->Func3();
}
(1)虚函数重写/覆盖 语法与原理层解释
--语法层的概念: 派生类对继承基类虚函数实现进行了重写
--原理层的概念: 子类的虚表,拷贝父类虚表进行了修改,覆盖重写那个虚函数
(2)虚表存储解释
无论是子类还是父类中只要有虚函数都会多存一个指针,这个指针叫虚表指针,他指向一个指针数组,指针数组中存着各个虚函数的地址。
Func1是重写的函数,Base[0]中存的地址并非真正的Derive中Func1的地址,而是通过call这个地址,找到这个地址的内容,这个地址的内容指令又是jump到地址2,地址2存的才是真正的Derive中Func1的地址
(3)多态调用和普通调用底层解释(编译时多态/运行时多态)
①运行时多态是动态绑定,也叫晚期绑定;运行时的多态性可通过虚函数实现。
②编译时多态是静态绑定,也叫早期绑定,主要通过重载实现;编译时的多态性可通过函数重载和模板实现。】
-在运行期间,通过传递不同类的对象,编译器选择调用不同类的虚函数:编译期间,编译器主要检测代码是否违反语法规则,此时无法知道基类的指针或者引用到底引用那个类的对象,也就无法知道调用哪个类的虚函数。在程序运行时,才知道具体指向那个类的对象,然后通过虚表调用对应的虚函数,从而实现多态。
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
void Buy() { cout << "Person::Buy()" << endl; }
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
void Buy() { cout << "Student::Buy()" << endl; }
};
void Func1(Person* p)
{
跟对象有关,指向谁调用谁 -- 运行时确定函数地址
p->BuyTicket();
跟类型有关,p类型是谁,调用就是谁的虚函数 -- 编译时确定函数地址
p->Buy();
}
int main()
{
Person p;
Student s;
Func1(&p);
Func1(&s);
return 0;
}
重点总结:
多态调用:运行时决议-- 运行时确定调用函数的地址(不管对象类型,查对应的虚函数表,如果是父类的对象,就查看父类对象中存的虚表;如果是子类切片后的对象,就查看子类切片后对象中存的虚表)
普通调用:编译时决议-- 编译时确定调用函数的地址(只看对象类型去确定调用哪个对象中的函数)
(4)非多态的虚函数Func4在监视窗口被隐藏了,看不到,只能通过内存看到
class Derive : public Base
{
public:
// 重写
virtual void Func1()
{
cout << "Derive::Func1()" << endl;
}
void Func3()
{
cout << "Derive::Func3()" << endl;
}
virtual void Func4()
{
cout << "Derive::Func4()" << endl;
}
private:
int _d = 2;
};
// 取内存值,打印并调用,确认是否是func4
//typedef void(*)() V_FUNC; // 不支持这种写法
typedef void(*V_FUNC)(); // 只能这样定义函数指针
// 打印虚表
//void PrintVFTable(V_FUNC a[])
void PrintVFTable(V_FUNC* a)
{
printf("vfptr:%p\n", a);
for (size_t i = 0; a[i] != nullptr; ++i) //VS下的虚表以空指针结束
{
printf("[%d]:%p->", i, a[i]); //打印虚表中的所有函数的地址
V_FUNC f = a[i]; //调用函数中打印函数,可以知道是哪个func函数
f();
}
}
int c = 2;
int main()
{
Base b;
Derive d;
PrintVFTable((V_FUNC*)(*((int*)&d))); //下有解释
}
(5)同一类型对象,共用一个虚表
4.多继承,虚表的存储(一个子类继承两个父亲时)
大体的结论就是:func1是重写的函数,在子类的两个父类的虚表中存储的func1地址不相同,但是通过一系列的call这个地址,这个地址的内容又是jump到另一个指令,最终都会跳到子类重写的func1地址上
class Base1 {
public:
virtual void func1() { cout << "Base1::func1" << endl; }
virtual void func2() { cout << "Base1::func2" << endl; }
private:
int b1;
};
class Base2 {
public:
virtual void func1() { cout << "Base2::func1" << endl; }
virtual void func2() { cout << "Base2::func2" << endl; }
private:
int b2;
};
class Derive : public Base1, public Base2 {
public:
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void func3() { cout << "Derive::func3" << endl; }
private:
int d1;
};
typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{
// 依次取虚表中的虚函数指针打印并调用。调用就可以看出存的是哪个函数
cout << " 虚表地址>" << vTable << endl;
for (int i = 0; vTable[i] != nullptr; ++i)
{
printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);
VFPTR f = vTable[i];
f();
}
cout << endl;
}
int main()
{
printf("%p\n", &Derive::func1);
Derive d;
//PrintVTable((VFPTR*)(*(int*)&d));
PrintVTable((VFPTR*)(*(int*)&d));
PrintVTable((VFPTR*)(*(int*)((char*)&d+sizeof(Base1))));
}
PrintVTable((VFPTR*)(*(int*)&d));
因为对象中存虚表指针,虚表指针中存的是虚表(一个指针数组),则需要先解引用访问到这个对象的前四个字节内容(存的就是虚表指针),此时的虚表指针 *((int*)&d)是一个int类型,再把虚表指针类型强转成指针数组类型才能传参
PrintVTable((VFPTR*)(*(int*)((char*)&d+sizeof(Base1)))); 是找到Base2的虚表地址后再解引用找到虚表(直接加2个int字节也能找到base2,考虑Base1可能不单单是2个int大小,这里建议用sizeof(Base1) )
结论: Derive对象Base2虚表中func1时,是Base2指针ptr2去调用。但是这时ptr2发生切片指针偏移,需要修正。中途就需要修正存储this指针ecx的值
虚函数使用规则:
-
虚函数在类中声明和类外定义的时候,virtual关键字只在声明时加上,而不能加在在类外实现上
-
静态成员不可以是虚函数。因为静态成员函数没有this指针,使用类型::成员函数 的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。
-
友元函数不属于成员函数,不能成为虚函数
-
静态成员函数就不能设置为虚函数(原因:静态成员函数与具体对象无关,属于整个类,核心关键是没有隐藏的this指针,可以通过类名::成员函数名 直接调用,此时没有this无法拿到虚表,就无法实现多态,因此不能设置为虚函数)
-
析构函数建议设置成虚函数,因为有时可能利用多态方式通过基类指针调用子类析构函数(尤其是父类的析构函数强力建议设置为虚函数,这样动态释放父类指针所指的子类对象时,能够达到析构的多态)
1. inline函数可以是虚函数吗?
答:可以,不过多态调用的时候编译器就忽略inline属性,这个函数就不再是 inline,因为虚函数要放到虚表中去。
2. 静态成员可以是虚函数吗?
答:不能,因为静态成员函数没有this指针,使用类型::成员函数 的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。
3. 构造函数可以是虚函数吗?
答:不能,因为对象中的 虚函数表指针 是在 构造函数初始化列表阶段才初始化的 。虚函数的意义是多态,多态调用时到虚函数表中去找,构造函数之前还没初始化,如何去找?
4. 析构函数可以是虚函数吗?
什么场景下析构函数是虚函数?答:可以,并且最好把基类的析构函数定义成虚函数。析构函数名统一会被处理成destructor()
5. 对象访问普通函数快还是虚函数更快?
答:首先如果是普通对象,是一样快的。如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。
6. 虚函数表是在什么阶段生成的,存在哪的?
答: 虚函数表是在编译阶段就生成的 ,一般情况下存在代码段(常量区)的。( 虚函数表指针初始化是指把虚函数表的指针放到对象中去,但生成仍是在编译阶段 )
7. C++菱形继承的问题?虚继承的原理?
答:参考继承课件。注意这里不要把虚函数表和虚基
表搞混了。
8. 什么是抽象类?抽象类的作用?
答 抽象类。抽象类强制重写了虚函数,另外抽象类体现出了接口继承关系。