C++面向对象的三大特征为:封装、继承、多态
C++认为万物皆为对象,对象上有其属性和行为
# 一、封装
# 1.1意义
封装是C++面向对象的三大特征之一
封装的意义:
- 将属性和行为作为一个整体,表现生活中的事物
- 将属性和行为加以权限控制
封装的意义:
- 在设计类的时候,属性和行为写在一起表现事物
class 类名 {访问权限:属性/行为}
例如:设计学生类
//
// Created by Jacob Lee on 2021/12/17.
//
#include "iostream"
#include "string"
using namespace std;
// 设计一个学生类
class Student {
public:
string m_Name;
int m_Id;
void showStudent() {
cout << m_Name << ": " << m_Id << endl;
}
// 通过行为进行实例化
void setStudent(string name, int id) {
m_Name = name;
m_Id = id;
}
};
int main() {
// 创建学生
Student stu;
stu.setStudent("John", 11);
stu.showStudent();
return 0;
}
类中的属性和成员统一称为成员,所以属性也称为成员属性或成员变量,类中的行为也被称为成员函数或成员方法
# 1.2访问权限设定
类在设计时,可以把属性和行为放在不同的权限之下加以控制
访问权限共有三种:
- public:公共权限
- protected:保护权限
- private:私有权限
# 1.2.1公共权限public
类内可以访问,类外可以访问
# 1.2.2保护权限protected
类内可以访问,类外不可以访问,和私有权限的区别在于子类可以使用保护权限,但是不可以访问私有权限的内容
# 1.2.3私有权限private
类内可以访问,类外不可以访问
#include "iostream"
#include "string"
using namespace std;
class Person {
public:
// 公共权限
string m_Name;
protected:
// 保护权限
string m_Car;
private:
// 私有权限
int m_Password;
public:
void func() {
m_Name = "John";
m_Car = "Lexus";
m_Password = 123456;
}
};
int main() {
// 实例化对象
Person p1;
p1.m_Name = "Jack";
// p1.m_Car = "Benz";
// p1.m_Password = 654321;
return 0;
}
# 1.3struct和class的区别
struct和class的唯一区别在于默认的访问权限不同
- struct默认权限为公有
- class默认权限为私有
# 1.4成员属性私有化
成员属性私有化之后有两大优点:
- 优点一:将所有成员属性设置为私有,可以自己控制读写权限
- 优点二:对于写权限,可以检测数据的有效性
//
// Created by Jacob Lee on 2021/12/17.
//
#include "iostream"
#include "string"
using namespace std;
// 成员属性设置为私有的好处
// 1、可以自己控制读写权限
// 2、对于写可以检测数据的有效性
class Person {
private:
string m_Name; // 可读可写
int m_Age; // 只读
string m_Lover; // 只写
public:
// 写姓名
void setName(string name) {
m_Name = name;
}
// 读姓名
string getName() {
return m_Name;
}
// 获取年龄
int getAge() {
m_Age = 0; // 初始化为0岁
return m_Age;
}
// 设置爱人
void setLover(string lover) {
m_Lover = lover;
}
};
int main() {
Person p1;
p1.setName("John");
cout << p1.getName() << endl;
cout << p1.getAge() << endl;
p1.setLover("11");
return 0;
}
# 二、对象特性
# 2.1构造函数与析构函数
对象的初始化:在现实生活中所购买的商品都会有出厂设置,在某一天不使用的时候也会删除一些信息保证安全
C++中的面向对象,每个对象也都会有初始化设置以及对象的销毁设置
- C++中利用了构造函数和析构函数解决了初始化和清理的问题,这两个函数将会被编译器自动调用,完成对象初始化和清理工作。对象的初始化和清理工作是编译器强制需要我们做的事情,因此,如果我们不提供构造和析构,编译器会提供编译器提供的构造函数和析构函数是空实现
- 构造函数:主要作用在于创建对象时为对象的成员属性赋值,构造函数有编译器自动调用,无须手动调用
- 析构函数:主要作用在于对象销毁前系统自动调用,执行一些清理工作
# 2.1.1构造函数
语法:类名(){}
- 构造函数没有返回值也不用写void
- 函数名称和类名相同
- 构造函数可以有参数,因此也可以发生重载
- 程序在调用对象的时候会自动调用构造,无须手动调用,而且只会调用一次
# 2.1.2析构函数
语法:~类名(){}
- 析构函数没有返回值也不用写void
- 函数名称与类名相同,在名称前加上符号~
- 析构函数不可以有参数,所以不可以发生重载
- 程序在对象销毁前会自动调用析构函数,无须手动调用,而且只会调用一次
#include "iostream"
#include "string"
using namespace std;
// 对象的初始化和清理
class Person {
public:
// 构造函数(没有返回值,不用void,函数名与类名相同)
Person() {
cout << "构造函数已调用" << endl;
}
// 析构函数(没有返回值,不写void,名称前加上~)
~Person() {
cout << "析构函数已调用" << endl;
}
};
int main() {
static Person p1;
return 0;
}
# 2.2构造函数的分类及调用
按照参数分类,构造函数可以分为有参构造和无参构造
按照类型分类,构造函数可以分为普通构造和拷贝构造
另外构造函数共有三种调用方法:括号法、显示法、隐式转换法
#include "iostream"
#include "string"
using namespace std;
// 构造函数的分类及调用
class Person {
private:
int m_Age;
public:
// 构造函数
Person() {
cout << "无参构造函数已调用" << endl;
}
// 有参构造函数
Person(int a) {
cout << "有参构造函数已调用" << endl;
}
// 拷贝构造函数
Person(const Person &p) {
// 将传入的类身上所有的属性,拷贝到当前类上
m_Age = p.m_Age;
cout << "拷贝构造函数已调用" << endl;
}
// 析构函数
~Person() {
cout << "析构函数已调用" << endl;
}
};
// 调用
int main() {
// 无参构造(注意:无参构造属于默认构造,一定不能加括号)
Person p1;
// 有参构造
Person p2(10);
// 拷贝构造
Person p3(p1);
// 显示法(显示调用有参构造)
Person p4 = Person(10);
// 匿名对象,特点是:当前行执行结束后,系统会立即回收该匿名对象
Person(10);
// 不要利用拷贝构造函数来初始化一个匿名对象
return 0;
}
# 2.3拷贝构造函数调用
C++中拷贝构造函数调用时机通常有三种情况:
- 使用一个已经创建完毕的对象来初始化一个新对象
- 值传递的方式给函数参数传值
- 以值的方式返回局部对象
#include "iostream"
#include "string"
using namespace std;
class Person {
public:
int m_Age;
Person() {
cout << "无参构造函数已调用" << endl;
}
Person(int age) {
m_Age = age;
cout << "有参构造函数已调用" << endl;
}
// 拷贝构造函数
Person(const Person &p) {
// 将传入的类身上所有的属性,拷贝到当前类上
m_Age = p.m_Age;
cout << "拷贝构造函数已调用" << endl;
}
// 析构函数
~Person() {
cout << "析构函数已调用" << endl;
}
};
// 通过值传递的方式给函数传值
void doWork01(Person p) {}
void test01() {
Person p;
doWork01(p);
}
// 值方式返回局部对象
Person doWork02() {
Person p1;
return p1;
}
void test02() {
Person p = doWork02();
}
// 调用
int main() {
Person p1(20);
Person p2(p1);
cout << "p2数值大小" << p2.m_Age << endl;
test01();
test02();
return 0;
}
# 2.4构造函数的调用规则
默认情况下,C++编译器至少会给一个类添加三个函数
- 默认构造函数(无参,函数体为空)
- 默认析构函数(无参,函数体为空)
- 默认拷贝构造函数,对属性进行拷贝
构造函数的调用规则如下:
- 如果用户定义有参构造函数,C++不再提供默认无参构造函数,但是会提供默认拷贝构造
- 如果用户定义拷贝构造函数,C++不再提供其他构造函数
# 2.5深拷贝与浅拷贝
- 浅拷贝就是一个简单的复制拷贝操作
- 深拷贝是在堆区重新申请空间进行拷贝的操作
浅拷贝带来的问题就是堆区的内存会重复释放,解决的方法就是通过深拷贝
出错代码:
#include "iostream"
using namespace std;
class Person {
public:
int m_Age;
int * m_Height;
Person() {
cout << "Person默认构造函数调用" << endl;
}
Person(int age, int height) {
m_Age = age;
m_Height = new int(height);
cout << "Person有参构造函数调用" << endl;
}
~Person() {
// 将堆区开辟出来的数据做释放操作
if(m_Height != NULL) {
delete m_Height;
m_Height = NULL;
}
cout << "Person析构函数调用" << endl;
}
};
// 调用
int main() {
Person p1(18, 170);
cout << "P1的年龄:" << p1.m_Age << "身高:" << *p1.m_Height <<endl;
Person p2(p1);
cout << "P2的年龄:" << p2.m_Age << "身高:" << *p2.m_Height << endl;
return 0;
}
通俗来说,如果单纯地进行浅拷贝,在执行析构函数时会把所指向的内存释放掉,从而导致拷贝出的对象指向出现问题
如果要解决这个问题需要自己实现拷贝构造函数来解决浅拷贝带来的问题
// 深拷贝构造函数
Person(const Person &p) {
m_Age = p.m_Age;
// 深拷贝
// 编译器默认执行:m_Height = p.m_Height
m_Height = new int(*p.m_Height);
}
# 2.6初始化列表
C++提供了初始化列表的语法,用来初始化属性
- 语法:
构造函数(): 属性1(值1),属性2(值2),属性3(值3)... {}
//
// Created by Jacob Lee on 2021/12/17.
//
#include "iostream"
using namespace std;
class Person {
public:
int m_A;
int m_B;
int m_C;
// 传统的初始化操作
Person(int a, int b, int c) {
m_A = a;
m_B = b;
m_C = c;
}
// 初始化列表操作
Person(): m_A(10), m_B(20), m_C(30) {}
// 传参初始化列表
Person(int a, int b, int c): m_A(a), m_B(b), m_C(c) {}
};
// 调用
int main() {
Person p1(10, 20, 30);
cout << p1.m_A << p1.m_B << p1.m_C << endl;
Person p2;
cout << p2.m_A << p2.m_B << p2.m_C << endl;
Person p3(10, 20, 30);
cout << p3.m_A << p3.m_B << p3.m_C << endl;
return 0;
}
# 2.7类对象作为类成员
C++类中的成员可以是另一个类的对象,我们称该成员位成员对象
#include "iostream"
#include "string"
using namespace std;
class Phone {
public:
string m_PName;
Phone(string pName): m_PName(pName) {};
};
class Person {
public:
string m_Name;
Phone m_Phone;
// 此处的m_Phone(pName)相当于做了一个Phone m_Phone = pName的操作(隐式转换法)
Person(string name, string pName): m_Name(name), m_Phone(pName) {};
};
// 调用
int main() {
Person p("Jacob", "iPhone");
cout << p.m_Name << endl;
cout << p.m_Phone.m_PName << endl;
return 0;
}
# 2.8静态成员
静态成员就是在成员变量和成员函数前加上关键字static,称为静态成员
静态成员分为:
- 静态成员变量
- 所有对象共享同一份数据
- 在编译阶段分配内存
- 类内声明,类外初始化
- 静态成员函数
- 所有对象共享同一个函数
- 静态成员函数只能访问静态成员变量(原因是无法区分到底是哪个对象的成员变量)
注意:静态成员变量需要在类内声明,在类外进行初始化!
#include "iostream"
using namespace std;
class Person {
public:
static int m_A;
int m_B;
static void func() {
// 静态成员函数可以访问静态成员变量
m_A = 100;
// 静态成员函数不可以访问非静态成员变量
// m_B = 100;
cout << "static void func调用" << endl;
}
};
// 注意:静态成员变量需要在类内声明,在类外进行初始化
int Person::m_A;
// 调用
int main() {
// 通过对象访问
Person p1;
p1.func();
// 通过类名访问
Person::func();
return 0;
}
静态成员函数也是有访问权限的
# 2.9C++对象模型与this指针
# 2.9.1成员变量与成员函数的分开存储
在C++中,类内的成员变量和成员函数分开存储
只有非静态成员变量才属于类的对象上
class Person {
int m_A; // 非静态成员变量,属于类的对象上
static int m_B; // 静态成员变量,不属于类的对象上
void funcA() {} // 非静态成员函数,不属于类的对象上
static void funcB() {} // 静态成员函数,不属于类的对象上
}
# 2.9.2this指针
由于C++中成员变量和成员函数是分开存储的
每一个非静态成员函数只会诞生一份函数实例,也就是说多个同类型的对象会用到一块代码
- 解决问题:这一块代码是如何区分是哪个对象在调用它?
- C++通过this指针解决该问题
this指针指向被效用的成员函数所属的对象
this指针式隐含每一个非静态成员函数内的一种指针
this指针不需要定义直接使用即可
用途:
- 当形参和成员变量同名时,可以使用this指针进行区分
- 再类的非静态成员函数中返回对象本身,可以使用
return *this
//
// Created by Jacob Lee on 2021/12/20.
//
#include "iostream"
using namespace std;
class Person {
public:
int age;
Person(int age) {
// this指针指向的是被调用的成员函数所属的对象
this->age = age;
}
Person & PersonAddAge(Person &p) {
this->age += p.age;
return *this;
}
};
// 调用
int main() {
Person p1(18);
cout << p1.age << endl;
Person p2(20);
p2.PersonAddAge(p1).PersonAddAge(p1);
cout << p2.age << endl;
return 0;
}
# 2.9.3const修饰成员函数
成员函数后加上了const后称该函数为常函数
- 常函数内不可以修改成员属性
- 成员属性声明时加关键字mutable后,在常函数中可以对成员属性进行修改
// 常函数
class Person {
public:
// 在成员函数后面添加const,修饰的是this指向,让指针指向的值不允许修改
void showPerson() const {
// this->m_A = 100;
this->m_B = 100;
}
int m_A;
// 特殊变量,即使在常函数中,也可以修改这个值
mutable int m_B;
}
声明对象前加上了const称该对象为常对象
- 常对象只能调用常函数
// 常对象
void test() {
// 在对象前面加const,变为常对象
const Person p;
// p.m_A = 100;
p.m_B = 100;
// 常对象只能调用常函数
p.showPerson();
}
# 三、友元
# 3.1友元全局函数
友元的目的就是让一个函数或者类访问另一个类中的私有成员
友元的关键字为friend
友元的三种实现:
- 全局函数做友元
- 类做友元
- 成员函数做友元
#include "iostream"
using namespace std;
class Building {
// 声明该函数为友元函数,可以访问该类中的私有成员
friend void friendFunc(Building &building);
public:
string m_SittingRoom;
Building(): m_SittingRoom("客厅"), m_BedRoom("卧室") {}
private:
string m_BedRoom;
};
// 全局函数
void friendFunc(Building &building) {
cout << "正在访问:" << building.m_SittingRoom << endl;
cout << "正在访问:" << building.m_BedRoom << endl;
}
// 调用
int main() {
Building b1;
friendFunc(b1);
return 0;
}
# 3.2友元类
- 语法
friend class 友元类名
#include "iostream"
#include "string"
using namespace std;
class friendClass;
class Building {
// 声明友元类
friend class friendClass;
public:
string m_SittingRoom;
Building(): m_SittingRoom("客厅"), m_BedRoom("卧室") {}
private:
string m_BedRoom;
};
// 友元类
class friendClass{
public:
Building * building;
friendClass() {
// 创建需要访问的对象
building = new Building;
}
// 参观函数,访问Building中的属性
void visit() {
cout << "当前访问的是" << building->m_SittingRoom << endl;
cout << "当前访问的是" << building->m_BedRoom << endl;
}
};
// 调用
int main() {
friendClass f1;
f1.visit();
return 0;
}
# 3.3友元成员函数
- 语法
friend void 类名::成员函数;
//
// Created by Jacob Lee on 2021/12/20.
//
#include "iostream"
#include "string"
using namespace std;
class Building;
class friendClass{
public:
Building * building;
friendClass();
void visit01();
void visit02();
};
class Building {
friend void friendClass::visit01();
public:
string m_SittingRoom;
Building(): m_SittingRoom("客厅"), m_BedRoom("卧室") {}
private:
string m_BedRoom;
};
friendClass::friendClass() {
building = new Building;
}
// 可以访问Building中的私有成员
void friendClass::visit01() {
cout << "当前访问的是" << building->m_SittingRoom << endl;
cout << "当前访问的是" << building->m_BedRoom << endl;
}
// 不可以访问Building中的私有成员
void friendClass::visit02() {
cout << "当前访问的是" << building->m_SittingRoom << endl;
// cout << "当前访问的是" << building->m_BedRoom << endl;
}
// 调用
int main() {
friendClass f1;
f1.visit01();
f1.visit02();
return 0;
}
# 四、运算符重载
重载运算符概念:对已有的运算符进行重新定义,赋予其另外一种功能,以适应不同的数据类型
# 4.1加号运算符重载
- 思路:
- 关键字:
operator
- 举例:
// 成员函数重载+号
Person operator+(Person & p) {
Person temp;
temp.m_A = this->m_A + p.m_A;
temp.m_B = this->m_B + p.m_B;
return temp;
}
- 方法一:通过成员函数重载
#include "iostream"
#include "string"
using namespace std;
class Person {
public:
int m_A;
int m_B;
// 成员函数重载+号
Person operator+(Person & p) {
Person temp;
temp.m_A = this->m_A + p.m_A;
temp.m_B = this->m_B + p.m_B;
return temp;
}
};
// 调用
int main() {
Person p1;
p1.m_A = 10;
p1.m_B = 10;
Person p2;
p2.m_A = 20;
p2.m_B = 20;
Person p3 = p1 + p2;
cout << p3.m_A << endl;
cout << p3.m_B << endl;
return 0;
}
- 方法二:通过全局函数重载
// 全局函数重载
Person operator+(Person &p1, Person&p2) {
Person temp;
temp.m_A = p1.m_A + p2.m_A;
temp.m_B = p1.m_B + p2.m_B;
return temp;
}
# 4.2左移运算符重载
注意:通常情况下不会通过成员函数来重载左移运算符,因为无法实现cout在左侧
首先要知道什么是cout:
打开源文件可以发现,cout属于标准的输出流对象(ostream)
#include "iostream"
using namespace std;
class Person {
public:
int m_A;
int m_B;
Person(): m_A(10), m_B(10) {}
};
// 全局函数重载左移运算符(本质是operator<< (cout, p))
ostream &operator<<(ostream &cout, Person &p) {
cout << "m_A:" << p.m_A << ";m_B:" << p.m_B;
return cout;
}
// 调用
int main() {
Person p;
cout << p << endl;
return 0;
}
# 4.3递增运算符重载
- 作用:通过重载递增运算符,实现自己的整形数据
#include "iostream"
using namespace std;
// 自定义整型
class Integer {
friend ostream &operator<<(ostream &cout, Integer i);
public:
Integer() {
m_Num = 0;
}
// 重载前置递增
Integer& operator++() {
m_Num++;
return *this;
}
// 重载后置递增(int表示占位参数,用于区分前置和后置)
Integer& operator++(int) {
Integer temp = *this;
m_Num++;
return temp;
}
private:
int m_Num;
};
// 重载左移运算符
ostream &operator<<(ostream &cout, Integer i) {
cout << i.m_Num;
return cout;
}
// 调用
int main() {
Integer int1;
cout << ++int1 << endl;
cout << int1++ << endl;
cout << int1 << endl;
return 0;
}
# 4.4赋值运算符重载
C++编译器同样会给赋值运算符operator添加一个默认函数,对属性进行拷贝
如果类中有属性指向堆区,做赋值操作时也会出现深浅拷贝的问题
#include "iostream"
using namespace std;
class Person {
public:
int *m_Age;
Person(int age) {
m_Age = new int(age);
}
~Person() {
if(m_Age != NULL) {
delete m_Age;
m_Age = NULL;
}
}
// 重载赋值运算符
Person& operator=(Person &p) {
// 先判断是有否有属性在堆区
if(m_Age != NULL) {
delete m_Age;
m_Age = NULL;
}
// 深拷贝
m_Age = new int(*p.m_Age);
// 返回自身
return *this;
}
};
// 调用
int main() {
Person p1(18);
Person p2(20);
p2 = p1;
cout << *p2.m_Age << endl;
return 0;
}
# 4.5关系运算符重载
#include "iostream"
#include "string"
using namespace std;
class Person {
public:
string m_Name;
int m_Age;
Person(string name, int age): m_Name(name), m_Age(age) {}
// 重载关系运算符==
bool operator==(Person &p) {
if (this->m_Age == p.m_Age && this->m_Name == p.m_Name) {
return true;
}
return false;
}
// 重载关系运算符!=
bool operator!=(Person &p) {
if (this->m_Age != p.m_Age || this->m_Name != p.m_Name) {
return true;
}
return false;
}
};
// 调用
int main() {
Person p1("Tom", 18);
Person p2("Tom", 19);
if (p1 == p2) {
cout << "p1与p2相等" << endl;
} else if (p1 != p2) {
cout << "p1与p2不相等" << endl;
}
return 0;
}
# 4.6函数调用运算符重载
函数调用运算符()也可以进行重载
由于重载后使用方式非常像函数的调用,因此也被称为仿函数
仿函数没有固定的写法,非常灵活
#include "iostream"
#include "string"
using namespace std;
class simulatePrint {
public:
// 重载函数调用运算符
void operator()(string txt) {
cout << txt;
}
};
// 调用
int main() {
simulatePrint sp;
sp("Hello World!");
return 0;
}
# 五、继承
继承是面向对象的三大特性之一
类与类之间可能会存在特殊的关系,例如动物之下有猫、狗,猫狗下面又有更细致的分类:
这时候就可以使用继承的技术来减少重复的代码
# 5.1基本语法
- 语法:
class 子类类名: 继承方式(public等等) 父类类名
class base {};
class inherit: public base {};
继承的好处在于减少了代码量
子类也被称为派生类,父类也被称为基类
# 5.2继承方式
有三种继承方式:
- 公共继承
- 保护继承
- 私有继承
对于公共继承而言:父类中的private无法访问,其他的权限在子类中不会发生任何改变
对于保护继承而言:父类中的private无法访问,其他的权限在子类中更改为保护权限
对于私有继承而言:父类中的private无法访问,其他的权限在子类中更改为私有权限
# 5.3继承中的对象模型
在子类中从父类继承下来的属性都是属于子类的,只是被编译器隐藏了
# 5.4继承的构造与析构
子类继承父类后,当创建子类对象,也会调用弗雷德构造函数
解决问题:父类和子类的构造和析构函数顺序是怎样的?
- 继承中的构造与析构顺序如下:
- 先构造父类,在构造子类,析构顺序与构造的顺序相反
也就是说:
父类的构造函数已调用...
子类的构造函数已调用...
子类的析构函数已调用...
父类的析构函数已调用...
# 5.5同名成员处理
# 5.5.1同名成员属性处理方式
再不加以处理的情况下,如果子类的某一个属性与父类的某一个属性重名,默认使用的是子类的属性
如果需要访问父类继承下来的该属性,只需要在创建的子类对象后加父类作用域,例如:son.Base::m_Test
// 父类
class Base {
public:
int m_A;
Base() {
m_A = 100;
}
};
// 子类
class Son: public Base{
public:
int m_A;
Son() {
m_A = 200;
}
};
// 调用
int main() {
Son son;
cout << son.m_A << endl;
cout << son.Base::m_A << endl;
return 0;
}
# 5.5.2同名成员函数处理方式
如果子类中出现和父类重名额成员函数,子类的同名成员会隐藏掉父类中所有同名成员函数
如果想访问到父类中被隐藏的同名成员函数,需要加作用域
// 调用
int main() {
Son son;
cout << son.m_A << endl;
cout << son.Base::m_A << endl;
son.func();
son.Base::func();
return 0;
}
# 5.5.3同名静态成员处理方法
静态成员和非静态成员出现同名,处理方法一致
- 访问子类同名成员,直接访问即可
- 访问父类同名成员需要加作用域
# 5.6多继承
C++中允许一个类继承多个类
语法:class 子类: 继承方法 父类1, 继承方法 父类2, ...
多继承可能会引发父类中有同名成员的问题,需要加作用域进行区分
注意:C++中不建议使用多继承
# 5.7菱形继承
- 概念:两个派生类同时继承同一个基类,又有一个类同时继承两个派生类,这种继承称为菱形继承
例如:
示例如上,菱形继承产生的问题:
- 羊继承了动物的数据,驼同样继承了动物的数据,当羊驼使用数据时就会产生二义性
- 羊驼继承自动物的数据分为了两份,然而作为动物的这一份数据只需要一份即可
如果不做处理,在菱形的底层类会出现两个最高级父类的某一属性,而且这个属性会出现冲突,如下:
// 调用
int main() {
Alcapa alcapa;
alcapa.Sheep::m_Age = 100;
alcapa.Camel::m_Age = 80;
cout << alcapa.Sheep::m_Age << endl;
cout << alcapa.Camel::m_Age << endl;
return 0;
}
利用虚继承可以解决菱形继承问题
- 关键字
virtual
使用虚继承技术之后,顶层的父类被称为虚基类
// 动物类
class Animal {
public:
int m_Age;
Animal(): m_Age(18) {}
};
// 羊类
class Sheep:virtual public Animal{};
// 驼类
class Camel:virtual public Animal{};
// 羊驼类
class Alcapa: public Sheep, public Camel{};
# 六、多态
# 6.1多态的基本概念
多态-多种形态
多态是C++面向对象的三大特性之一
多态分为两类:
- 静态多态:函数重载和运算符重载属于静态多态,复用函数名
- 动态多态:派生类和虚函数实现运行时的多态
静态多态和动态多态的区别:
- 静态多态的函数地址早绑定:编译阶段确定函数地址
- 动态多态的函数地址晚绑定:运行阶段确定函数地址
如果需要实现地址晚绑定就需要使用到虚函数技术,关键字:virtual
#include "iostream"
using namespace std;
class Animal {
public:
virtual void speak() {
cout << "Hello Human" << endl;
}
};
class Cat: public Animal{
public:
void speak() {
cout << "Miao Human" << endl;
}
};
// 地址早绑定,在编译阶段确定好了函数的地址
// 如果想要执行猫说话,那么函数地址不能提前绑定,需要在运行阶段进行绑定,也就是地址晚绑定
// 如果要实现地址晚绑定就需要使用到关键字virtual
void doSpeak(Animal &animal) {
animal.speak();
}
// 调用
int main() {
Cat cat;
doSpeak(cat);
return 0;
}
动态多态的满足条件:
- 有继承关系
- 子类需要重写父类的虚函数(重写与重载是不同的,重写是返回值、函数名、形参列表均相同)
动态多态的使用:父类的指针或者引用执行子类的对象
void doSpeak(Animal &animal) {
animal.speak();
}
# 6.2多态的底层原理
使用多态的好处:
- 组织结构清晰
- 可读性强
- 对于前后期的拓展和维护性更高
案例:计算器
#include "iostream"
using namespace std;
class Calculator {
// 在实现一个诸如计算器的类时,提倡开闭原则
// 开闭原则:对拓展进行开放,对修改进行关闭
public:
int m_Num1;
int m_Num2;
virtual int getResult() {
return 0;
}
};
// 加法计算器类(只做加法运算)
class AddCalculator: public Calculator {
public:
int getResult() {
return m_Num1 + m_Num2;
}
};
// 减法计算器类(只做减法运算)
class SubCalculator: public Calculator {
public:
int getResult() {
return m_Num1 - m_Num2;
}
};
// 乘法计算器类(只做乘法运算)
class MultiCalculator: public Calculator {
public:
int getResult() {
return m_Num1 * m_Num2;
}
};
// 调用
int main() {
// 指针指向加法运算器
Calculator * calculator = new AddCalculator;
calculator->m_Num1 = 10;
calculator->m_Num2 = 10;
cout << calculator->m_Num1 << "+" << calculator->m_Num2 << "=" << calculator->getResult() << endl;
// 销毁calculator
delete calculator;
// 指针指向减法运算器
calculator = new SubCalculator;
calculator->m_Num1 = 10;
calculator->m_Num2 = 10;
cout << calculator->m_Num1 << "-" << calculator->m_Num2 << "=" << calculator->getResult() << endl;
delete calculator;
// 指针指向乘法运算器
calculator = new MultiCalculator;
calculator->m_Num1 = 10;
calculator->m_Num2 = 10;
cout << calculator->m_Num1 << "*" << calculator->m_Num2 << "=" << calculator->getResult() << endl;
delete calculator;
return 0;
}
# 6.3纯虚函数和抽象类
在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容
因此,可以将虚函数改为纯虚函数
- 语法:
virtual 返回值类型 函数名(参数列表) = 0;
当类中有了纯虚函数,这个类也被称为抽象类
抽象类特点:
- 无法实例化对象
- 子类必须重写抽象类中的纯虚函数,否则也属于抽象类
#include "iostream"
using namespace std;
class Base {
public:
// 只要有一个纯虚函数,这个类就被称为抽象类
virtual void func() = 0;
};
class Son: public Base {
public:
void func() {
cout << "Hello, Virtual Class!" << endl;
}
};
// 调用
int main() {
Base * base = new Son;
base->func();
delete base;
return 0;
}
# 6.4虚析构和纯虚析构
多他使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构函数
- 解决办法:将父类中的析构函数改为虚析构或者纯虚析构
虚析构和纯虚析构共性:
- 解决父类指针释放子类对象
- 都需要有句析的实现函数
虚析构和纯虚析构区别:
- 如果是纯虚析构,该类属于抽象类,无法实例化对象
语法:virtual ~类名() {}
纯虚析构语法:virtual ~类名() = 0;
#include "iostream"
#include "string"
using namespace std;
class Base {
public:
// 只要有一个纯虚函数,这个类就被称为抽象类
virtual void func() = 0;
// // 利用虚析构可以解决父类指针释放子类对象时不干净的问题
// virtual ~Base() {
// cout << "Base析构函数调用" << endl;
// }
// 纯虚析构 需要声明也需要实现
// 有了纯虚析构之后,这个类也属于抽象类,无法实例化对象
virtual ~Base() = 0;
};
// 外部实现
Base::~Base() {
cout << "Base析构函数调用" << endl;
};
class Son: public Base {
public:
string *m_Name;
void func() {
cout << *m_Name << endl;
}
Son(string name) {
cout << "Son构造函数调用" << endl;
m_Name = new string(name);
}
~Son() {
if (m_Name != NULL) {
cout << "Son析构函数调用" << endl;
delete m_Name;
m_Name = NULL;
}
}
};
// 调用
int main() {
Base * base = new Son("Tom");
base->func();
// 父类的指针在析构的时候,不会调用子类中的析构函数,导致子类如果有堆区属性,会出现内存的泄漏
delete base;
return 0;
}