C++学习记录 三、C++核心编程(面向对象) 继承
发布日期:2021-05-08 14:29:36 浏览次数:23 分类:精选文章

本文共 23755 字,大约阅读时间需要 79 分钟。

再系统地过一次,夯实基础

学习目标:

过一遍黑马程序员C/C++学习视频

文章目录

学习内容:

一、


二、


三、

4.4 友元

生活中你的家有客厅public,有你的卧室private

客厅所有来的客人都可以进去,但是你的卧室是私有的,也就是说只有你能进去

但是呢,你也可以允许你的好朋友进去。

在程序里,有些私有属性也想让类外特殊的一些函数或者类进行访问,就需要用到友元的技术

友元的目的就是让一个函数或者类 访问另一个类中私有成员

友元的关键字为friend

友元的三种实现

  • 全局函数做友元
  • 类做友元
  • 成员函数做友元
4.4.1 全局函数做友元
//房屋类class Building {   	//goodfriend全局函数是Building的好朋友,可以访问私有成员	//告诉编译器 goodfriend全局函数 是Building类的好朋友,可以访问类中的私有内容	friend void goodfriend(Building* building);public:	Building() {   		m_SittingRoom = "客厅";		m_BedRoom = "卧室";	}	string m_SittingRoom;//客厅private:	string m_BedRoom;//卧室 };//全局函数void goodfriend(Building* building) {   	cout << "好朋友的全局函数 正在访问: " << building->m_SittingRoom << endl;	cout << "好朋友的全局函数 正在访问: " << building->m_BedRoom << endl; //不再报错}void test01() {   	Building building;	goodfriend(&building);}
4.4.2 类做友元
class Building;//类做友元class Goodfriend {   public:	Goodfriend();	void visit();//参观函数 访问Building中的属性	Building* building;};class Building {   	//goodfriend是本类的好朋友,可以访问本类中的私有成员	friend class Goodfriend;public:	Building();	string m_SittingRoom;private:	string m_BedRoom;};//类外写成员函数Building::Building() {   	m_SittingRoom = "客厅";	m_BedRoom = "卧室";}Goodfriend::Goodfriend() {   	//创建建筑物对象	building = new Building;}void Goodfriend::visit() {   	cout << "好朋友类正在访问: " << building->m_SittingRoom << endl;	cout << "好朋友类正在访问: " << building->m_BedRoom << endl;}void test01() {   	Goodfriend gg;	gg.visit();}
4.4.3 成员函数做友元
class Building;class GoodGay {   public:	GoodGay();	void visit();//让visit函数可以访问Building中私有成员	void visit2();//让visit2函数不可以访问Building中的私有成员	Building* building;};class Building {   	//告诉编译器 GoodGay类下的visit成员函数作为苯类的好朋友,可以访问私有成员	friend void GoodGay::visit();public:	Building();//声明	string m_SittingRoom;private:	string m_BedRoom;};Building::Building(){   	m_SittingRoom = "客厅";	m_BedRoom = "卧室";}GoodGay::GoodGay() {   	building = new Building;}void GoodGay::visit() {   	cout << "visit正在访问:" << building->m_SittingRoom << endl;	cout << "visit正在访问:" << building->m_BedRoom << endl;}void GoodGay::visit2() {   	cout << "visit2正在访问:" << building->m_SittingRoom << endl;}void test01() {   	GoodGay goodgay;	goodgay.visit2();	goodgay.visit();}

4.5 运算符重载

运算符重载概念:对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型

4.5.1 加号运算符重载

作用:实现两个自定义数据类型相加的运算

对于内置数据类型,编译器知道如何运算

int a = 10;int b = 20;int c = a + b;

而对于:

class Person{   public:	int m_a;	int m_b;}Person p1;p1.m_a = 10;p2.m_b = 10;Person p2;p2.m_a = 20;p2.m_b = 20;Person p3 = p1 + p2;//编译器不知道怎么进行相加运算

可以通过自己写成员函数,实现两个对象相加属性后返回新的对象

Person add(Person& p) {   		Person temp;		temp.m_a = this->m_a + p.m_a;		temp.m_b = this->m_b + p.m_b;		return temp;	}

Person p3 = p1.add(p2);

而编译器起了一个通用名称 operator+
Person p3 = p1.operator+(p2);
可以简化为:
Person p3 = p1 + p2;

通过成员函数重载+号

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;	//Person p3 = p1.operator+(p2)	system("pause");	return 0;}

通过全局函数重载+

class Person {   public:	int m_a;	int m_b;};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;}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;	//Person p3 = operator+(p1, p2);	system("pause");	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;}Person operator+(Person& p1, int a) {   	Person temp;	temp.m_a = p1.m_a + 10;	temp.m_b = p1.m_b + 10;	return temp;}Person p1;p1.m_a = 10;p1.m_b = 10;Person p2;p2.m_a = 20;p2.m_b = 20;Person p3 = p1 + p2;Person p4 = p1 + 10;

总结1:对于内置的数据类型的表达式的运算符是不可能改变的

总结2:不要滥用运算符重载

4.5.2 左移运算符重载

作用:可以输出自定义数据类型

class Person {   	friend ostream& operator<<(ostream& cout, Person& p);public:	Person(int a, int b) {   		m_a = a;		m_b = b;	}private:	int m_a;	int m_b;};ostream& operator<<(ostream& cout, Person& p) {   //本质 operator<<(cout, p) 简化 cout << p;	cout << "m_a = " << p.m_a << "m_b = " << p.m_b;	return cout;}void test01() {   	Person p1(10, 10);	//运用了链式和重载和友元	cout << p1 << endl;}
4.5.3 递增运算符重载

作用:通过重载递增运算符,实现自己的整形数据

//重载递增运算符//自定义整型class MyInteger {   	friend ostream& operator<<(ostream& cout, MyInteger myint);public:	MyInteger() {   		m_Number = 0;	}	//重载前置++运算符	//返回引用是为了一直对一个数据进行操作	MyInteger& operator++() {   		m_Number++;		//this指针指向被调用的成员函数所属的对象		return *this;	}	//重载后置++运算符	//这个int代表的是占位参数,可以用于区分前置和后置递增	//如果是后置递增,返回的是值,不能返回引用	MyInteger operator++(int) {   		//先记录当时结果		MyInteger temp = *this;		//后递增		m_Number++;		//最后将记录结果做返回		return temp;	}private:	int m_Number;};//重载一下左移运算符ostream& operator<<(ostream& cout, MyInteger myint) {   	cout << myint.m_Number;	return cout;}void test01() {   	MyInteger myint;	cout << ++myint << endl;}void test02() {   	MyInteger myint2;	cout << myint2++ << endl;	cout << myint2 << endl;}

注意:

ostream& operator<<(ostream& cout, MyInteger myint) {   	cout << myint.m_Number;	return cout;}

重载一下左移运算符中myint传入的是值引用,而不是地址引用,是因为上面的后置递增中的temp是局部变量,在那个函数结束后就会被释放,所以是不可以使用地址引用的。

4.5.4 赋值运算符重载

C++编译器至少给一个类添加4个函数

  1. 默认构造函数(无参,函数体为空)
  2. 默认析构函数(无参,函数体为空)
  3. 默认拷贝构造函数,对属性进行值拷贝
  4. 赋值运算符operator=,对属性进行值拷贝

如果类种有属性指向堆区,做赋值操作时也会出现深浅拷贝问题。

//赋值运算符重载class Person {   public:	Person(int age) {   		m_age = new int(age);	}	~Person() {   		delete m_age;		m_age = nullptr;	}	//重载赋值运算符	Person& operator=(Person& p) {   		//编译器提供的是浅拷贝		//做深拷贝		//先判断是否有属性在堆区,如果有先释放干净,然后再深拷贝		if (m_age != nullptr) {   			delete m_age;			m_age = nullptr;		}		m_age = new int(*p.m_age);		return *this;	}	int *m_age;};void test01() {   	Person p1(18);	Person p2(20);	//赋值运算	p2 = p1;	cout << *(p1.m_age) << endl;	cout << *p2.m_age << endl;}
4.5.5 关系运算符重载

作用: 重载关系运算符,可以让两个自定义类型进行对比操作

//重载关系运算符 == !=class Person {   public:	Person(string name, int age) {   		m_Age = age;		m_Name = name;	}	// 重载==	bool operator==(Person& p) {   		if (m_Age == p.m_Age && m_Name == p.m_Name)			return true;		else return false;	}	// 重载!=	bool operator!=(Person& p) {   		if (m_Age != p.m_Age || m_Name != p.m_Name)			return true;		else return false;	}	string m_Name;	int m_Age;};void test01() {   	Person p1("Time", 18);	Person p2("Tome", 18);	if (p1 == p2) {   		cout << "p1和p2是相等的" << endl;	}	if (p1 != p2) {   		cout << "p1和p2是不相等的" << endl;	}}
4.5.6 函数调用运算符重载
  • 函数调用运算符 () 也可以重载
  • 由于重载后使用的方式非常像函数的调用,因此称为仿函数
  • 仿函数没有固定写法,非常灵活
//函数调用运算符重载//打印输出类class MyPrint {   public:	//重载的函数调用运算符	void operator() (string test) const {   		cout << test << endl;	}};void test01() {   	MyPrint myprint;	myprint("Hello world");//由于使用起来非常类似于函数调用,因此称为仿函数}//仿函数非常灵活 没有固定的写法//加法类class MyAdd {   public:	int operator()(int a, int b) const {   		return a + b;	}};void test02() {   	MyAdd myadd;	cout << myadd(1, 2) << endl;}

匿名函数对象

void test02() {   	MyAdd myadd;	cout << myadd(1, 2) << endl;	//匿名函数对象	cout << MyAdd()(100, 100) << endl;}

4.6 继承

继承是面向对象的三大特性之一

有些类与类之间存在特殊的关系,例如下图

在这里插入图片描述
我们发现,定义这些类时,下级别的成员除了拥有上一级的共性,还有自己的特性。

这个时候我们就可以考虑利用继承的技术,减少重复代码。

4.6.1 继承的基本语法

例如哦我们看到很多网站中,都有公共的头部,底部甚至公共的左侧列表,只有中心内容不同

语法:

class 子类 :继承方式 父类{};

子类也被称为派生类

父类也被称为基类

普通实现:

class Java {   public:	void header() {   		cout << "首页、公开课、登录。。。(公共头部)" << endl;	}	void footer() {   		cout << "帮助中心、交流合作(公共底部)" << endl;	}	void left() {   		cout << "Java、Python...(公共分类列表)" << endl;	}	void content() {   		cout << "Java学科视频" << endl;	}};class Python {   public:	void header() {   		cout << "首页、公开课、登录。。。(公共头部)" << endl;	}	void footer() {   		cout << "帮助中心、交流合作(公共底部)" << endl;	}	void left() {   		cout << "Java、Python...(公共分类列表)" << endl;	}	void content() {   		cout << "Pthon学科视频" << endl;//就这句不同	}};

继承实现

语法: class 子类 :继承方式 父类{};

派生类中的成员包含两大部分

一类时从基类继承过来的,一类是自己增加的成员。

从基类继承过来的表现其共性,而新增的成员体现了其个性。

继承的好处,减少重复代码

//继承实现//公共类实现class BasePage {   public:	void header() {   		cout << "首页、公开课、登录。。。(公共头部)" << endl;	}	void footer() {   		cout << "帮助中心、交流合作(公共底部)" << endl;	}	void left() {   		cout << "Java、Python...(公共分类列表)" << endl;	}};//Java页面class Java :public BasePage {   public:	void content() {   		cout << "Java学科视频" << endl;	}};//Python页面class Python :public BasePage {   public:	void content() {   		cout << "Python学科视频" << endl;	}};void test01() {   	Java ja;	Python py;}
4.6.2 继承方式

继承的语法:class 子类 :继承方式 父类

继承的方式一共有三种:

  • 公共继承
  • 保护继承
  • 私有继承

在这里插入图片描述

公共继承
//继承方式class Base1 {   public:    int m_A;protected:    int m_B;private:    int m_C;};//公共继承class son1 :public Base1 {   public:    void func() {           m_A = 10;//父类中的公共权限成员到子类中依然是公共权限        m_B = 20;//父类中的保护权限成员到子类中依然是保护权限        m_C = 30;//报错,父类中的私有权限成员,子类无法访问    }};void test01() {       son1 s1;    s1.m_A = 100;    s1.m_B = 100;//报错,保护权限成员内容在类外无法访问}
保护继承
//保护继承class son1 :protected Base1 {   public:    void func() {           m_A = 10;//父类中的公共权限成员到子类中变成保护权限        m_B = 20;//父类中的保护权限成员到子类中依然是保护权限        m_C = 30;//报错,父类中的私有权限成员,子类无法访问    }};void test01() {       son1 s1;    s1.m_A = 100;//报错,保护权限成员内容无法在类外访问    s1.m_B = 100;//报错,保护权限成员内容在类外无法访问}
私有继承
//私有继承class son1 :private Base1 {   public:    void func() {           m_A = 10;//父类中的公共权限成员到子类中变成私有权限        m_B = 20;//父类中的保护权限成员到子类中变成私有权限        m_C = 30;//报错,父类中的私有权限成员,子类无法访问    }};class GrandSon1 :public son1 {       void func() {           m_A = 10;//报错,父类son1的私有权限成员,子类无法访问        m_B = 20;//报错,父类son1的私有权限成员,子类无法访问    }};
4.6.3 继承中的对象模型

问题: 从父类继承过来的成员,哪些属于子类对象中?

在父类中所有非静态成员属性都会被子类继承下去,父类中的私有成员属性,是被编译器隐藏了,因此访问不到,但确实继承下来了。

4.6.4 继承中构造和析构顺序

子类继承父类后,当创建子类对象,也会调用父类地构造函数

问题:父类和子类的构造和析构顺序是谁先谁后?

//继承中的构造和析构顺序class Base {   public:    Base() {           cout << "Base的构造函数" << endl;    }    ~Base() {           cout << "Base的析构函数" << endl;    }};class Son :public Base {   public:    Son() {           cout << "Son的构造函数" << endl;    }    ~Son() {           cout << "Son的析构函数" << endl;    }};void test01() {       Son s1;}

父类的构造函数先于子类的构造函数

子类的析构函数先于父类的析构函数

在这里插入图片描述

4.6.5 继承同名成员处理方式

问题:当子类与父类出现同名的成员,如何通过子类对象,访问到子类或父类同名的数据呢?

  • 访问子类同名成员 直接访问即可
  • 访问父类同名成员 需要加作用域

同名成员属性:

//继承同名成员属性 需要加作用域class Base {   public:    int a;    Base() {           a = 10;    }};class Son :public Base {   public:    int a;    Son(){           a = 20;    }};void test01() {       Son s1;     cout << "Son 下的a: " << s1.a << endl;//20    cout << "Base 下的a: " << s1.Base::a << endl;//10}

同名成员函数:

等同成员属性

void test01() {       Son s1;     s1.func();//son    s1.Base::func();//Base}

注意:

若在以上Base类代码的基础上再加一句

void func(int a){};重载函数,子类不添加;

再在test01中添加 s1.func(100);是会报错的。

原因是因为:如果子类中出现和父类同名的成员函数,子类的同名成员函数会隐藏掉父类中所有的同名成员函数;包括重载版本,不能够直接访问。

依然要加作用域s1.Base::func(100);

总结:

  1. 子类对象可以直接访问到子类中同名成员
  2. 子类对象加作用域可以访问到父类同名成员
  3. 当子类与父类拥有同名的成员函数,子类会隐藏父类中同名成员函数,加作用域可以访问到父类中同名函数
4.6.6 继承同名静态成员处理方式

问题:继承中,同名的静态成员在子类对象上如何进行访问?

静态成员和非静态成员出现同名,处理方式一致

  • 访问子类同名成员 直接访问即可
  • 访问父类同名成员 需要加作用域

继承同名静态成员 属性处理方式

//继承同名静态成员 处理方式class Base {   public:    static int m_A;};//类外初始化int Base::m_A = 100;class Son :public Base {   public:    static int m_A;};int Son::m_A = 200;void test01() {       //1、通过对象访问    cout << "通过对象访问" << endl;    Son s;    cout << "Son 里的 m_A " << s.m_A << endl;    cout << "Base 里的 m_A " << s.Base::m_A << endl;    //2、通过类名访问    cout << "通过类名访问" << endl;    cout << "Son 下的 m_A " << Son::m_A << endl;    cout << "Base 下的 m_A " << Son::Base::m_A << endl;}

在这里插入图片描述

注意: 对于Son::Base::m_A;中,第一个::代表通过类名方式访问,第二个::代表访问父类作用域下的m_A

继承同名静态成员函数 处理方式

//继承同名静态成员 处理方式class Base {   public:    static void func() {           cout << "Base的静态成员函数" << endl;    }};class Son :public Base {   public:    static void func() {           cout << "Son的静态成员函数" << endl;    }};void test01() {       Son s;    //通过对象    s.func();    s.Base::func();    //通过类名    Son::func();    Son::Base::func();}
4.6.7 多继承语法

C++允许一个类继承多个类

语法:class 子类 :继承方式 父类1, 继承方式 父类2...

多继承可能会引发父类中有同名成员出现,需要加作用域区分

C++实际开发中不建议用多继承

//多继承语法class Base1 {   public:    int m_A;    Base1() {           m_A = 100;    }};class Base2 {   public:    int m_A;    Base2() {           m_A = 200;    }};class Son :public Base1, public Base2 {   public:    int m_C;    int m_D;    Son() {           m_C = 300;        m_D = 400;    }};void test01() {       Son s;    cout << "sizeof:" << sizeof(Son) << endl;//16    cout << "Base1下的m_A:" << s.Base1::m_A << endl;//100    cout << "Base2下的m_A:" << s.Base2::m_A << endl;//200}
4.6.8 菱形继承

菱形继承概念:

  • 两个派生类继承同一个基类
  • 又有某个类同时继承这两个派生类
  • 这种继承被称为菱形继承,或者砖石继承

典型的继承案例:

在这里插入图片描述
菱形继承问题:

  1. 羊继承了动物的数据,驼同样继承了动物的数据,当杨桐使用数据时,就会产生二义性
  2. 羊驼继承自动物的数据继承了两份,其实我们应该清楚,这份数据我们只需要一份就可以
    在这里插入图片描述
    当菱形继承,两个父类拥有相同成员属性,需要加以作用域区分
//菱形继承//动物类class Animal {   public:    int m_Age;};//羊类class Sheep :public Animal{   public:};//驼类class Tuo :public Animal {   public:};//羊驼类class SheepTuo :public Sheep, public Tuo {   public:};void test01() {       SheepTuo st;    st.m_Age = 18;//报错 原因:不明确    st.Sheep::m_Age = 18;    st.Tuo::m_Age = 28;     cout << "st.Sheep::m_Age  " << st.Sheep::m_Age << endl;    cout << "st.Tuo::m_Age  " << st.Tuo::m_Age << endl;}

问题:这份数据我们知道,只需要有一份就可以,菱形继承导致数据有两份,导致资源浪费。

利用虚继承可以解决菱形继承的问题

在继承之前加上关键字virtual变为虚继承

//羊类//虚继承class Sheep :virtual public Animal{   public:};//驼类//虚继承class Tuo :virtual public Animal {   public:};

Animal类称为 虚基类

在这里插入图片描述

void test01() {       SheepTuo st;    st.Sheep::m_Age = 18;    st.Tuo::m_Age = 28;        cout << "st.Sheep::m_Age  " << st.Sheep::m_Age << endl;    cout << "st.Tuo::m_Age  " << st.Tuo::m_Age << endl;    cout << "st.m_Age = " << st.m_Age << endl;//不再报错}

原因解释:

在这里插入图片描述

vbptr: 虚基类指针

v - virtual

b - base
ptr - pointer

虚基类指针会指向一个叫vbtable的虚基类表。虚基类指针会通过偏移量找到唯一的数据如:m_Age

在这里插入图片描述

在这里插入图片描述

4.7 多态

4.7.1 多态的基本概念

多态是C++面向对象三大特性之一

多态分为两类

  • 静态多态:函数重载 和 运算符重载属于静态多态, 复用函数名
  • 动态多态:派生类和虚函数实现运行时多态

静态多态和动态多态的区别:

  • 静态多态的函数地址早绑定 - 编译阶段确定函数地址
  • 动态多态的函数地址晚绑定 - 运行阶段确定函数地址

对于函数 doSpeak(cat);执行的是animal在说话

下面的代码地址早绑定 在编译阶段确定函数地址

//多态//动物类class Animal {   public:	void speak() {   		cout << "动物在说话" << endl;	}};//猫类class  Cat:public Animal{   public:	void speak() {   		cout << "小猫在说话" << endl;	}};//执行说话的函数void doSpeak(Animal& animal) {   //Animao& animal = cat;	animal.speak();}void test() {   	Cat cat;	doSpeak(cat);}int main(){   		test();	system("pause");	return 0;}

如果想执行让猫类说话

那么这个函数的地址就不能提前绑定,需要在运行阶段进行绑定,也就是晚绑定
需要在 动物类的speak()函数前加virtual

class Animal {   public:	//虚函数	virtual void speak() {   		cout << "动物在说话" << endl;	}};

动态多态的满足条件:

  1. 有继承关系
  2. 子类要重写父类的虚函数

动态多态的使用:

  1. 父类的指针或者引用指向子类对象,如:

重写: 返回值类型,函数名,参数列表 完全相同

void doSpeak(Animal& animal) // Animal& animal = cat

会多一个虚函数指针 vfptr

在这里插入图片描述

vfptr: 虚函数指针
v - virtual
f - function
ptr - pointer

4.7.2 多态案例一、计算器类

案例描述:

分别利用普通写法和多态技术,设计实现两个操作数进行运算的计算器类

多态的优点:

  • 代码组织结构清晰
  • 可读性强
  • 利于前期和后期的扩展以及维护

普通写法:

//普通写法class Caculator {   public:	int m_Num1;//操作数1	int m_Num2;//操作数2	int getResult(string oper) {   		if (oper == "+") {   			return m_Num1 + m_Num2;		}		if (oper == "-") {   			return m_Num1 - m_Num2;		}		if (oper == "*") {   			return m_Num1 * m_Num2;		}	}};void test01() {   	Caculator c;	c.m_Num1 = 10;	c.m_Num2 = 10;	cout << c.m_Num1 << " + " << c.m_Num2 << " = " << c.getResult("+") << endl;}

如果想要扩展新的功能,需要修改源码

在真实开发中 提倡 开闭原则
开闭原则: 对扩展进行开发,对修改进行关闭

多态写法:

//多态实现//实现计算器抽象类class AbstractCaculator {   public:	virtual int getResult() {    return 0; }	int m_Num1;	int m_Num2;};//设计加法计算器类class AddCaculator :public AbstractCaculator {   public:	int getResult() {    return m_Num1 + m_Num2; }};//设计减法计算器类class SubCaculator :public AbstractCaculator {   public:	int getResult() {    return m_Num1 - m_Num2; }};//设计乘法计算器类class MulCaculator :public AbstractCaculator {   public:	int getResult() {    return m_Num1 * m_Num2; }};void test01() {   	//多态的使用条件	//父类的指针或者引用指向子类对象	//加法运算	AbstractCaculator* abc = new AddCaculator;	abc->m_Num1 = 10;	abc->m_Num2 = 10;	cout << abc->m_Num1 << " + " << abc->m_Num2 << " = " << abc->getResult() << endl;	delete abc;	//剑法运算	abc = new SubCaculator;	abc->m_Num1 = 10;	abc->m_Num2 = 10;	cout << abc->m_Num1 << " - " << abc->m_Num2 << " = " << abc->getResult() << endl;	delete abc;}
4.7.3 纯虚函数和抽象类

在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容

因此可以将虚函数改为纯虚函数

纯虚函数语法:virtual 返回值类型 函数名 (参数列表) = 0;

当类中有了纯虚函数,这个类也称为抽象类

抽象类特点:

  • 无法实例化对象
//纯虚函数和抽象类class Base {   public:	virtual void func() = 0;};void test01() {   	//抽象类无法实例化对象	Base b;//报错	new Base;//报错}
  • 子类必须重写抽象类中的纯虚函数,否则也属于抽象类
class Son :public Base {   public:};void test01() {   	Son s;//报错	new Son;//报错}

正确的写法:

class Base {   public:	virtual void func() = 0;};class Son :public Base {   public:	virtual void func() {   		cout << "func函数调用" << endl;	}};void test01() {   	Base* base = new Son;	base->func();}
4.7.4 多态案例二、制作饮品

案例描述:

制作饮品的大概流程为:煮水、冲泡、倒入杯中,加入辅料

利用多态技术实现本案例,提供抽象制作饮品基类,提供子类制作咖啡和茶叶

//多态案例2 制作饮品class AbstractDrinking {   public:	//煮水	virtual void Boil() = 0;	//冲泡	virtual void Brew() = 0;	//倒入杯中	virtual void PourInCup() = 0;	//加入辅料	virtual void PutSomething() = 0;	//制作饮品	void makeDrink() {   		Boil();		Brew();		PourInCup();		PutSomething();	}};//制作咖啡class Coffee :public AbstractDrinking {   public:	virtual void Boil() {   		cout << "煮水" << endl;	}	virtual void Brew() {   		cout << "冲泡咖啡" << endl;	}	virtual void PourInCup() {   		cout << "倒入杯中" << endl;	}	virtual void PutSomething() {   		cout << "加入糖和牛奶" << endl;	}};//制作茶class Tea :public AbstractDrinking {   public:	virtual void Boil() {   		cout << "煮水" << endl;	}	virtual void Brew() {   		cout << "冲泡茶叶" << endl;	}	virtual void PourInCup() {   		cout << "倒入杯中" << endl;	}	virtual void PutSomething() {   		cout << "加入枸杞" << endl;	}};//制作函数void doWork(AbstractDrinking* abs) {   	abs->makeDrink();	delete abs;//释放}void test01() {   	//制作咖啡	doWork(new Coffee);	cout << "----------------" << endl;	//制作茶叶	doWork(new Tea);}

在这里插入图片描述

4.7.5 虚析构和纯虚析构

多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的西沟代码

解决方式:将父类中的析构函数改为虚析构或者纯虚析构

虚析构和纯虚析构共性:

  • 可以解决父类指针释放子类对象
  • 都需要有具体的函数实现

虚析构和纯虚析构区别:

  • 如果时纯虚析构,该类属于抽象类,无法实例化对象
//虚析构和纯虚析构class Animal {   public:	Animal() {   		cout << "Animal构造函数的调用" << endl;	}	~Animal() {   		cout << "Animal虚析构函数的调用" << endl;	}	virtual void speak() = 0;};class Cat :public Animal {   public:	Cat(string name) {   		cout << "Cat的构造函数调用" << endl;		m_Name = new string(name);	}	void speak() {   		cout << *m_Name << "小猫在说话" << endl;	}	~Cat() {   		if (m_Name != nullptr) {   			cout << "Cat析构函数调用" << endl;			delete m_Name;			m_Name = nullptr;		}	}	string* m_Name;};void test01() {   	Animal* animal = new Cat("Tom");	animal->speak();	delete animal;}

结果:

在这里插入图片描述
原因:
用父类的指针指向了子类的对象
delete的时候 不会调用子类的析构函数,导致子类如果有堆区属性,会出现内存泄漏的情况

void test01() {   	//用父类的指针指向了子类的对象	Animal* animal = new Cat("Tom");	animal->speak();	//在delete的时候 不会调用子类的析构函数,导致子类如果有堆区属性,会出现内存泄漏的情况	delete animal;}

解决方式:

利用虚析构可以解决 父类指针释放子类对象时不干净的问题

在父类虚构函数前加 virtual

class Animal {   public:	Animal() {   		cout << "Animal构造函数的调用" << endl;	}	virtual ~Animal() {   		cout << "Animal析构函数的调用" << endl;	}	virtual void speak() = 0;};

纯虚析构

纯虚析构 需要声明也需要实现

有了纯虚析构之后,这个类也属于抽象类,无法实例化对象

class Animal {   public:	Animal() {   		cout << "Animal构造函数的调用" << endl;	}	//纯虚析构函数	virtual ~Animal() = 0;	virtual void speak() = 0;};//有声明与实现,因为基类本身也需要调用Animal::~Animal() {   	cout << "~Animal纯虚析构函数调用" << endl;}

虚析构语法:

virtual ~类名(){};

纯虚析构语法:

virtual ~类名() = 0
virtual ~类名() {}//外部实现

总结:

  1. 虚析构或纯虚析构是用来解决通过父类指针释放子类对象
  2. 如果子类中没有堆区数据,可以不写为虚析构或纯虚析构
  3. 拥有纯虚析构函数的类也属于抽象类
4.7.6 多态案例三、电脑组装

案例描述:

  • 电脑主要组成部件为CPU,显卡,内存条
  • 将每个零件封装出抽象基类,并且提供不同的厂商生产不同的零件,例如Inter厂商和Lenovo厂商
  • 创建电脑类提供让电脑工作的函数,并且调用每个零件工作的接口
  • 测试时组装三台不同的电脑进行工作
//抽象不同零件类class CPU {   public:	//抽象计算函数	virtual void calculate() = 0;	//虚析构,避免delete地不干净	virtual ~CPU(){   };};class GPU {   public:	//抽象图像函数	virtual void display() = 0;	//虚析构,避免delete地不干净	virtual ~GPU(){   };	};class Memory {   public:	//抽象存储函数	virtual void storage() = 0;	//虚析构,避免delete地不干净	virtual ~Memory(){   };};//电脑类class Computer {   public:	Computer(CPU* cpu, GPU* gpu, Memory* memory) {   		c = cpu;		g = gpu;		m = memory;	}	//提供析构函数 释放3个电脑零件	~Computer() {   		if (c != nullptr) {   			delete c;			c = nullptr;		}		if (g != nullptr) {   			delete g;			g = nullptr;		}		if (m != nullptr) {   			delete m;			m = nullptr;		}	}	//提供工作函数	void work() {   		//让零件工作起来,调用接口		c->calculate();		g->display();		m->storage();	}private:	//零件指针	CPU* c;	GPU* g;	Memory* m;};//具体零件厂商//Interclass IntelCPU :public CPU{   public:	void calculate() {   		cout << "Intel的CPU在计算" << endl;	}};class IntelGPU :public GPU{   public:	void display() {   		cout << "Intel的GPU在显示" << endl;	}};class IntelMemory :public Memory {   public:	void storage() {   		cout << "Intel的内存在存储" << endl;	}};//Lenovoclass LenovoCPU :public CPU {   public:	void calculate() {   		cout << "Lenovo的CPU在计算" << endl;	}};class LenovoGPU :public GPU{   public:	void display() {   		cout << "Lenovo的GPU在显示" << endl;	}};class LenovoMemory :public Memory {   public:	void storage() {   		cout << "Lenovo的内存在存储" << endl;	}};void test01() {   	//第一台电脑零件	CPU* intelcpu = new IntelCPU;	GPU* intelgpu = new IntelGPU;	Memory* intelmemory = new IntelMemory;	//创建第一台电脑	Computer* computer1 = new Computer(intelcpu, intelgpu, intelmemory);	computer1->work();	delete computer1;	cout << "--------" << endl;	//创建第二台电脑	Computer* computer2 = new Computer(new LenovoCPU, new LenovoGPU, new LenovoMemory);	computer2->work();	delete computer2;	cout << "--------" << endl;	//创建第三台电脑	Computer* computer3 = new Computer(new IntelCPU, new LenovoGPU, new LenovoMemory);	computer3->work();	delete computer3;}

在这里插入图片描述

5. 文件操作

程序运行时产生地数据都属于临时数据,程序一旦运行结束都会被释放

通过文件可以将数据持久化

C++中对文件操作需要包含头文件==< fstream >==

文件类型分为两种

  1. 文本文件 - 文件以文本地ASCII码形式存储在计算中
  2. 二进制文件 - 文件以文本地二进制形式存储在计算机中,用户一般不能直接读懂它们

操作文件地三大类:

  1. ofstream: 写操作
  2. ifstream: 读操作
  3. fstream: 读写操作

5.1 文本文件

5.1.1 写文件

写文件步骤如下

  1. 包含头文件 #include <fstream>
  2. 创建流对象 ofstream ofs;
  3. 打开文件 ofs.open("文件路径", 打开方式);
  4. 写数据 ofs << "写入数据;"
  5. 关闭文件 ofs.close();

文件打开方式:

打开方式 解释
ios::in 为读文件而打开文件
ios::out 为写文件而打开文件夹
ios::ate 初始位置:文件尾
ios::app 追加方式写文件
ios::trunc 如果文件存在,先删除,再创建
ios::binary 二进制方式

注意: 文件打开方式可以配合使用, 利用 | 操作符

例如: ios::binary | ios::out

#include
//头文件包含using namespace std;//文本文件 写文件void test01() { //1、包含头文件 fstream //2、创建流对象 ofstream ofs; //3、指定打开方式 ofs.open("test.txt", ios::out); //4、写内容 ofs << "姓名:张三" << endl; ofs << "性别:男" << endl; ofs << "年龄:18" << endl; //5、关闭文件 ofs.close();}

总结:

  • 文件操作必须包含头文件 fstream
  • 读文件可以利用 ofstream,或者fstream
  • 打开文件时候需要指定操作文件的路径,以及打开方式
  • 利用 << 可以向文件中写数据
  • 操作完毕,要关闭文件
5.1.2 读文件

读文件与写文件步骤相似,但是读取方式相对比较多

读文件步骤如下:

  1. 包含头文件 #include<fstream>

  2. 创建流对象 ifstream ifs;

  3. 打开文件并判断是否打开成功

    ifs.open("文件路径", 打开方式);if(!ifs.is_open()){         cout << "文件打开失败" << endl;    return;}
  4. 读数据 四种方式读取

  5. 关闭文件 ifs.close()

#include
//头文件包含//文本文件读文件void test01() { //1、包含头文件 //2、创建流对象 ifstream ifs; //3、打开文件并且判断是否打开成功 ifs.open("test.txt", ios::in); if (!ifs.is_open()) { cout << "文件打开失败" << endl; return; } //4、读数据 //第一种 char buf[1024] = { 0 }; while (ifs >> buf) { cout << buf << endl; } //第二种 char buf[1024] = { 0 }; while (ifs.getline(buf, sizeof(buf))) { cout << buf << endl; } //第三种 string buf; while (getline(ifs, buf)) { cout << buf << endl; } //第四种 char c; while ( (c = ifs.get()) != EOF) { cout << c; } //5、关闭文件 ifs.close();}

总结:

  • 读文件可以利用ifstream,或者 fstream
  • 利用 is_open() 函数可以判断文件是否打开成功
  • close() 关闭文件

二进制文件

以二进制的方式对文件进行读写操作

打开方式要指定为 ios::binary

5.2.1 写文件

二进制文件写文件主要利用流对象调用成员函数write

函数原型: ostream& write(const char* buffer, int len);

参数解释:字符指针 buffer 指向内存中一段存储空间。 len是读写的字节数

//二进制文件 写文件class Person {   public:	char m_Name[64];//姓名	int m_Age;//年龄};void test01() {   	//1、包含头文件	//2、创建流对象	ofstream ofs;	//3、打开文件	ofs.open("person.txt", ios::out | ios::binary);	//或者:ofstream ofs("person.txt", ios::out | ios::binary);	//4、写文件	Person p = {   "张三", 18};	ofs.write((const char*) &p, sizeof(Person));	//5、关闭文件	ofs.close();}
5.2.2 读文件

二进制方式读文件主要利用流对象调用成员函数read

函数原型:istream& read(char *buffer, ien);

参数解释: 字符指针buffer指向内存中一段存储空间, len时读写的字节数

//二进制文件 读文件class Person {   public:	char m_Name[64];//姓名	int m_Age;//年龄};void test01() {   	ifstream ifs;	//打开文件 判断文件是否打开成功	ifs.open("person.txt", ios::in | ios::binary);	if (!ifs.is_open()) {   		cout << "文件打开失败" << endl;		return;	}	Person p;	ifs.read((char*)&p, sizeof(p));	cout << "姓名:" << p.m_Name << ' ' << "年龄: " << p.m_Age << endl;	ifs.close();}

四、


五、


六、


七、


学习产出:

1、github 啃STL简化项目,能够自己实现STL相关项目

2、做一个微信小程序,具体功能暂定

上一篇:C++学习记录 四、基于多态的企业职工系统
下一篇:C++学习记录 三、C++核心编程(面向对象)

发表评论

最新留言

不错!
[***.144.177.141]2025年04月05日 12时03分22秒