一、封装
1.什么是封装
封装是将数据与方法进行结合,隐藏对象的部分属性和实现细节,对外开放一些接口,通过这些接口约束,类外可以合理的访问类内的属性
2.封装的作用
封装可以让数据隐藏 让类外合理访问类内的数据
3.为什么需要封装
将一个对象的属性和行为结合在一起更符合人们对事务的认知,通过访问限定符将部分功能开放出来域其他对象进行交互,外部用户是不需要知道具体的实现细节的,即使知道了,也只会增加使用和维护的难度,让事情变得复杂
4.举例
乘火车
- 售票系统:负责售票,用户凭票进入,对号入座
- 工作人员:售票、咨询、安检、保全、卫生等
- 火车:带用户到目的地
例如我们坐火车买票,我们只需要知道票在哪买的,去哪里可以乘车,不需要去了解火车的构造,购票系统的内部操作,但是火车站不能不管理,它需要“封”起来,只提供部分通道供乘客进入,不能就直接暴露出来,如果从任何地方都能上车,那整个流程就乱套了
二、继承
1.什么是继承
继承机制是面向对象程序设计代码可以复用的重要手段,它允许程序在保持原有类特性的基础上进行扩展,增加功能,产生的新类称为派生类
class Person
{
public:
void Print()
{
cout << "name:" << _name << endl;
cout << "age:" << _age << endl;
}
protected:
string _name = "limengru";
int _age = 21;
};
class Student : public Person
{
protected:
int _stuid;
};
1.1 继承定义格式
1.2继承关系和访问限定符
1.3.继承基类成员访问方式的变化
类成员/继承方式 | public继承 | protected继承 | private继承 |
---|
基类的public成员 | 派生类的public成员 | 派生类的protected成员 | 派生类的private成员 | 基类的protected成员 | 派生类protected成员 | 派生类的protected成员 | 派生类的private成员 | 基类的private成员 | 在派生类中不可见 | 在派生类中不可见 | 在派生类中不可见 |
2.基类和派生类对象的赋值转换
- 1.派生类对象可以赋值给基类对象/基类指针/基类的引用(切片或切割,指把派生类中父类的那部分切割下来,赋值过去)
- 2.基类对象不能赋值给派生类的对象
- 3.基类的指针可以通过强制类型转换赋值给派生类的指针
3.继承中的作用域
- 1.在继承体系中基类和子类都有独立的作用域
- 2.子类和父类拥有同名成员 ,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义
- 3.注意:如果是成员函数的隐藏,只需要函数名相同就构成隐藏
- 4.所以实际中在继承体系里最好不要定义同名的成员
举例1
class Person
{
public:
void Print()
{
cout << "name:" << _name << endl;
cout << "age:" << _age << endl;
}
protected:
string _name = "limengru";
int _age = 21;
};
class Student : public Person
{
protected:
int _stuid;
string _name = "dameinv";
};
可以看到上面的代码虽然没有错误,但是基类和子类的_name 构成了隐藏关系,非常容易混淆
举例2
class A
{
public:
void fun()
{
cout << "fun()" << endl;
}
};
class B : public A
{
public:
void fun(int i)
{
A::fun();
cout << "fun(int i)" << endl;
}
};
上面这个例子,A和B中的fun 函数不构成重载,因为不在同一个作用域 但是这两个fun 构成隐藏,因为成员函数满足函数名相同就构成隐藏
4.派生类的默认成员函数
在之前的学习中,我们知道类会有六个默认的成员函数,即使我们不写,编译器也会为我们自动生成一个,那么派生类中这几个“特殊”的函数是怎么生成的呢?
- 1.派生类的构造函数必须调用基类的构造函数初始化基类那一部分的成员,如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用
- 2.派生类的拷贝构造函数必须调用基类的拷贝构造函数完成基类的拷贝和初始化
- 3.派生类的
operator= 必须调用基类的operatr= 完成基类的赋值 - 4.派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员,因为这样才能保证派生类对象先清理派生类成员,再清理基类成员函数的顺序
- 5.派生类对象初始化先调用基类的构造函数,再调用派生类的构造函数
- 6.派生类的对象的清理先调用派生类的析构函数,再调用基类的析构函数
5.继承和友元
友元关系是不能继承的,基类的友元函数不能访问子类的私有和保护成员
#include <iostream>
#include <string>
using namespace std;
class Student;
class Person
{
public:
friend void Display(const Person& p, const Student& s);
protected:
string _name;
};
class Student : public Person
{
protected:
int _stuNo;
};
void Display(const Person& p, const Student& s)
{
cout << p._name << endl;
cout << s._stuNo << endl;
}
正如上面的代码,在编译后会给我们报出如下图的错误,代表即使是基类的友元,也不能访问子类的私有和保护成员
6.继承和静态成员
基类定义了static 静态成员,则整个继承体系里面只有一个这样的成员,无论派生出多少个子类,都只有一个static 成员实例
1.举例
#include <iostream>
#include <string>
using namespace std;
class Student;
class Person
{
public:
Person() { ++_count; }
protected:
string _name;
public:
static int _count;
};
int Person::_count = 0;
class Student : public Person
{
protected:
int _stuNo;
};
class Graduate : public Student
{
protected:
string _sem;
};
void test()
{
Student s1;
Student s2;
Student s3;
Graduate s4;
cout << "人数" << Person::_count << endl;
Student::_count = 0;
cout << "人数" << Person::_count << endl;
}
int main()
{
test();
system("pause");
return 0;
}
入上面的代码展示,_count 这个静态变量三个类值维护一个static 实例,结果如下图
7.复杂的菱形继承和菱形虚拟继承
1.单继承
一个子类只有一个直接父类时称这个继承关系为单继承
class Person
{
};
class student : public Person
{
};
class postgraduate : public student
{
};
2.多继承
一个子类有两个或两个以上直接父类时称这个继承关系为多继承
class Person
{
};
class student
{
};
class postgraduate : public student, public Person
{
};
3.菱形继承
菱形继承是多继承的一种特殊情况
class Person
{
};
class student : public Person
{
};
class teacher : public Person
{
};
class postgraduate : public student, public teacher
{
};
菱形继承存在问题
菱形继承可能会导致数据冗余和二义性的问题,看下面菱形继承的代码
class Person
{
public:
string _name;
};
class Student : public Person
{
protected:
int _stuNo;
};
class Teacher : public Person
{
protected:
int _id;
};
class Graduate : public Student, public Teacher
{
protected:
string _sem;
};
如果我们在使用父类的成员时,显示的指定(::)访问的时哪个,可以解决二义性的问题,但是一个子类对象里有几份重复的父类成员人就不能解决(数据冗余)
解决办法
虚拟继承可以解决菱形继承的二义性和数据冗余的问题, 虚拟继承不要在其他的地方使用
class Person
{
public:
string _name;
};
class Student : virtual public Person
{
protected:
int _stuNo;
};
class Teacher : virtual public Person
{
protected:
int _id;
};
class Graduate : public Student, public Teacher
{
protected:
string _sem;
};
原理 1.先看一下不用虚拟继承的内存监控 2.再看一下使用虚拟继承后的内存 可以看到 Graduate 对象将Person 放到了对象组成的最下面。这个Person 同时属于Student 和Teacher ,Student 和Teacher 通过两个指针指向同一个表,这两个指针叫虚基表指针,表叫虚基表,通过虚基表中的偏移量,可以找到Person
8.继承和组合
1.继承
class Person
{
protected:
string _name;
string _face;
};
class Student : public Person
{
public:
void eat() { cout << "吃美食" << endl; }
};
2.组合
class Person
{
protected:
string _name;
string _face;
};
class Student
{
protected:
double height;
double weight;
Person _p;
};
- 1.
public 继承是一种is-a 的关系,每一个派生类都是一个基类的对象 - 2.组合是一种
has-a 的关系,假设B 组合了A , 则每个B 对象中都有一个A 对象 - 3.优先使用对象组合,而不是类继承
- 4.原因
- 1.继承允许你根据基类的实现来定义派生类的实现,这种通过派生类的复用通常被称为白箱复用,在继承方式中,基类的内部实现对子类可见,继承一定程度破坏了基类的封装,基类的该表,对派生类有很大的影响。派生类和基类的依赖关系很强,耦合度高
- 2.对象组合是类继承之外的另一种复用选择,新的更复杂的功能可以通过组装或组合对象来获得,对象组合要求被组合的对象拥有良好定义的接口,这种复用风格被称为黑箱复用,因为对象内部的实现细节是不可见的。组合类之间没有很强的依赖关系,耦合度低, 优先使用对象组合有助于保持每个类都被封装
- 3.实际尽量用组合,类之间关系既可以用继承又可以用组合的,选择组合;适合用继承的就用继承;想要实现多态,必须用继承
👩?🏫小结
来做一做面试题吧 1.什么是菱形继承?菱形继承存在什么问题?如何解决? 2.继承和组合的区别?什么时候用继承?什么时候用组合?
三、多态
1.多态的概念
多态多态,多种形态,具体来说就是去完成某个行为,当不同的对象去完成相同的动作会产生不同的形态 举例 例如购买火车票,普通人买票是全价票,学生买票是半价票,军人买票是优先买票,不同的人买票会产生不同的形态
2.多态的定义和实现
2.1多态的继承条件
- 1.多态是在不同继承关系的类对象,去调用同一个函数,产生不同的行为
- 2.必须通过基类的指针或者引用调用虚函数
- 3.被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
2.2虚函数
被virtual 修饰的函数就是虚函数
class Person
{
public:
virtual void BuyTicket() {cout << "买票-全价" << endl;}
};
2.3 虚函数重写
虚函数重写就是在派生类中有一个跟基类完全相同的虚函数(函数名,返回值类型,,参数列表完全相同),就称子类的虚函数重写了基类的虚函数
class Person
{
public:
virtual void BuyTicket() {cout << "买票-全价" << endl;}
};
class Student : public Person
{
public:
virtual void BuyTicket() {cout << "买票-半价" << endl;}
};
虚函数重写的两个例外
1.协变(基类与派生类虚函数的返回值类型不同)
派生类重写基类的虚函数时,与基类虚函数的返回值类型不同, 基类返回的是基类对象的指针或引用,派生类返回的是派生类对象的指针或引用,称为协变
class A{};
class B : public A{};
class Person
{
public:
virtual void A* fun() {return new A;}
};
class Student : public Person
{
public:
virtual void B* fun() {return new B;}
};
2.析构函数的重写(基类与派生类析构函数的名字不同)
如果基类的析构函数为虚函数,此时派生类的析构函数只要定义,无论是否加virtual 关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数的名字不同,看起来违背了重写的规则,但是可以理解为编译器对析构函数的名字进行特殊处理,编译后析构函数的名字统一处理成destructor
class Person
{
public:
virtual ~Person() {cout << "买票-全价" << endl;}
};
class Student : public Person
{
public:
virtual ~Student() {cout << "买票-半价" << endl;}
};
注意 :在继承体系中,最好将基类的析构函数设计为虚函数,当子类中涉及到资源管理的时候,一定要将析构函数设计为虚函数,不然可能会导致内存泄露
2.4override和final
C++对重写的要求比较严格,所以可能我们不小心写错了一点就导致重写不成功,因此C++11提供了两个关键字:override和final,帮助我们检查是否重写
1.final
- 既可以修饰类,表示该类不被继承
- 也可以修饰虚函数,表示虚函数不想被子类重写
class Person
{
virtual void eat() final{}
};
class Student : public Person
{
public:
virtual void eat() { cout << "吃美食" << endl; }
};
2.override
- 检查派生类是否重写了基类的某个虚函数,如果没有重写,编译报错
class Person
{
virtual void eat() final{}
};
class Student : public Person
{
public:
virtual void eat() override { cout << "吃美食" << endl; }
};
2.5 重载,重写,隐藏的对比
3.抽象类
3.1 概念
在虚函数后面写上=0,则这个函数就被称为纯虚函数,包含纯虚函数的类就叫抽象类,抽象类不能实例化对象,派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才可以实例化对象
3.1 接口继承和实现继承
- 普通函数的继承是一种实现继承,派生类继承了基类基类函数。可以使用函数,继承是函数的实现
- 虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承是接口
- 所以如果不是为了实现多态,不要把函数定义成虚函数
4.多态的原理
4.1 虚函数表
class Person
{
public:
virtual void fun()
{
cout << "fun()" << endl;
}
private:
int _a = 1;
};
int main()
{
Person P;
int n = sizeof(P);
cout << n << endl;
return 0;
}
通过观察结果发现结果是8字节,除了一个_a的成员变量外,还有一个_vfptr放在对象前面,对象中的这个指针我们叫做虚函数表指针 一个虚函数的类中至少有一个虚函数表指针,因为虚函数表的地址要被放到虚函数表中 我们再来做一个测试
class Person
{
public:
virtual void fun1()
{
cout << "Person::fun1()" << endl;
}
virtual void fun2()
{
cout << "Person::fun2()" << endl;
}
void fun3()
{
cout << "Person::fun3()" << endl;
}
private:
int _a = 1;
};
class Student : public Person
{
public:
virtual void fun1()
{
cout << "Student::fun()" << endl;
}
private:
int _b = 2;
};
int main()
{
Person p;
Student s;
system("pause");
return 0;
}
通过监视窗口发现,p对象内部是这样的 s对象的内部是这样的
- 1.派生类当中也有一个虚函数表指针
- 2.基类
p 对象和派生类s 对象的虚函数表是不一样的,我们发现fun1() 完成了重写,所以s的虚函数表中存的是重写的虚函数fun1() ,所以虚函数表的重写也叫覆盖 - 3.
fun2() 继承下来后虚函数,所以放进虚函数表, fun3() 也继承下来了,但是不是虚函数,所以不会放进虚函数表, - 4.虚函数表的本质是一个存虚函数指针的指针数组,这个数组最后面放了一个
nullptr - 5.派生类的虚函数表构成:
5.1 先将基类的虚函数表内容拷贝一份到派生类的虚函数表中 5.2 如果派生类重写了基类中的某些虚函数,则用派生类自己的虚函数覆盖基类中的虚函数 5.3派生类自己新增的虚函数,按其在派生类中的声明次序,增加到派生类虚函数表的最后
注意:虚函数表存的是虚函数指针,不是虚函数,虚函数是存在代码段的,对象中存的不是虚函数表,是虚函数表指针,虚函数表在vs下也是存在代码段的
4.2多态的原理
先看一段代码
class Person
{
public:
virtual void BuyTicket() {cout << "买票-全价" << endl;}
};
class Student : public Person
{
public:
virtual void BuyTicket() {cout << "买票-半价" << endl;}
};
void fun(Person)
{
p.BuyTicket();
}
int main()
{
Person Li;
fun(Li);
Student Ru;
fun(Ru);
return 0;
}
- 1.观察红色的线,
p 是指向Li 对象时,p 调用的是在Li 的虚表中的虚函数Person::BuyTicket - 2.观察绿色的线,
p 是指向Ru 对象时,p 调用的使在Ru 的虚表中的虚函数Student::BuyTicket - 3.这样就实现了不同的对象完成同一个行为时,展现出不同的形态
满足多态的函数调用,不是在编译时期确定的,而是在运行之后去对象那个中获取的 不满足多态的函数调用,是在编译时期确定好的
4.3 动态绑定与静态绑定
- 1.静态绑定又称前期绑定,在程序编译期间就确定了程序的行为,也称静态多态,例如:函数重载
- 2.动态绑定又称后期绑定,在程序的运行期间,根据具体拿到的类型确定程序的行为,调用具体的函数,也称为动态多态
5.单继承和多继承关系的虚函数表
5.1单继承的虚函数表
#include <iostream>
using namespace std;
class Person
{
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
virtual void BuyTicket1() { cout << "买票-全价" << endl; }
};
class Student : public Person
{
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
virtual void BuyTicket1() { cout << "买票-半价" << endl; }
virtual void BuyTicket2() { cout << "买票-半价" << endl; }
};
int main()
{
Person Li;
Student Ru;
return 0;
}
我们看不到BuyTicket2()这个虚函数,这是编译器故意隐藏了, 我们可以通过打印查看
#include <iostream>
using namespace std;
class Person
{
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
virtual void BuyTicket1() { cout << "买票-全价" << endl; }
};
class Student : public Person
{
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
virtual void BuyTicket1() { cout << "买票-半价" << endl; }
virtual void BuyTicket2() { cout << "买票-半价" << endl; }
};
typedef void(*VFPTR) ();
void Print(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()
{
Person Li;
Student Ru;
VFPTR* vtabled = (VFPTR*)(*(int*)&Li);
Print(vtabled);
vtabled = (VFPTR*)(*(int*)&Ru);
Print(vtabled);
return 0;
}
5.2多继承的虚函数表
多继承派生类的为重写的虚函数放在第一个继承基类部分的虚函数表当中
#include <iostream>
using namespace std;
class Person
{
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
virtual void BuyTicket1() { cout << "买票-全价" << endl; }
};
class Teacher
{
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
virtual void BuyTicket1() { cout << "买票-全价" << endl; }
};
class Student : public Person, public Teacher
{
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
virtual void BuyTicket1() { cout << "买票-半价" << endl; }
virtual void BuyTicket2() { cout << "买票-半价" << endl; }
};
typedef void(*VFPTR) ();
void Print(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()
{
Person Li;
Student Ru;
VFPTR* vtabled = (VFPTR*)(*(int*)&Li);
Print(vtabled);
vtabled = (VFPTR*)(*(int*)&Ru);
Print(vtabled);
return 0;
}
|