第二阶段C++核心编程(黑马程序员)——Day7

奋斗吧
奋斗吧
擅长邻域:未填写

标签: 第二阶段C++核心编程(黑马程序员)——Day7 博客 51CTO博客

2023-07-14 18:24:15 172浏览

第二阶段C++核心编程(黑马程序员)——Day7,4.5运算符重载运算符重载概念:对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型4.5.1加号运算符重载作用:实现两个自定义数据类型相加的运算Personp1(10,10);Personp2(10,10);Personp3=p1+p2;classPerson{public:intm_A;intm_B;//1、通过成员函数重载+号P

4.5 运算符重载

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


4.5.1加号运算符重载

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

Person p1(10,10);
Person p2(10,10);

Person p3 = p1 + p2;
class Person
{
public:
	int m_A;
	int m_B;

	//1、通过成员函数重载+号
	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;
	}

	//本质调用: Person p3 = p1.operator+(p2);
};

//2、全局函数重载+号
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 p3 = operator+(p1,p2);

//运算符重载 也可以发生函数重载
Person operator+(Person& p, int num)
{
	Person temp;
	temp.m_A = p.m_A + num;
	temp.m_B = p.m_B + num;
	return temp;
}

void test01()
{
	Person p1;
	p1.m_A = 10;
	p1.m_B = 10;

	Person p2(10,10);

	Person p3 = p1 + p2;  //Person p3 = p1.operator+(p2);
	
	Person p4 = p1 + 100; //Person p4 = operator+(p1,100);
 
}

int main()
{
	test01();
	
	system("pause");
	return 0;
}

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

int a=1;
int b=2;
int c=a+b;  //c=3,这是内置数据类型的+运算符的操作

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


4.5.2左移运算符重载

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

Person p;
p.m_A=10;
p.m_B=10;
cout<<p;
class Person
{
	friend ostream& operator<<(ostream& out, Person p);
private:

	//成员函数重载 左移运算符 void operator<<(cout) {}
	//通常不会利用成员函数重载<<运算符, 无法实现cout在左侧
	//p.operator(cout) 简化版本:p<<cout

	int m_A;
	int m_B;

public:
	Person(int a, int b)
	{
		m_A = a;
		m_B = b;
	}
};

//只能利用全局函数重载左移运算符
//ostream& 因为iostream 的拷贝构造函数不允许使用
ostream& operator<<(ostream &out, Person p) //本质 operator<<(cout,p) 简化 cout<< p
{
	out << "m_A=" << p.m_A << endl;
	out << "m_B=" << p.m_B << endl;
	return out;
}

void test01()
{
	Person p(10, 10);

	//此处是一个函数重载,执行完第一个<<之后语句为cout<<endl
	//然后第二个<<后不为Person类型,所以使用iostream中定义的<<

	cout << p << endl;
}

int main()
{
	test01();
	
	system("pause");
	return 0;
}

总结:重载左移运算符配合友元可以实现输出自定义数据类型


4.5.3 递增运算符重载

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

class MyTntager
{
private:
	int m_Num;
public:
	MyTntager()
	{
		m_Num = 0;
	}
};

MyTntager myint;
cout << myint << endl;   //0
cout << ++myint << endl; //1  前置递增
cout << myint++ << endl; //1  后置递增
cout << myint << endl;   //2
class MyInteger
{
	friend ostream& operator<<(ostream& cout, MyInteger& myint);
private:
	int m_Num;

public:
	MyInteger();

	//重载++运算符(前置)
	//MyInteger& 返回引用而不是返回值,方便链式编程 ++(++myint)
  
	MyInteger& operator++()
	{
		m_Num++;
		return *this;   //返回自身
	}

	//重载++运算符(后置)
	//int代表占位参数,可以用于区分前置和后置递增
	//MyInteger 返回值而不是引用,假如返回局部变量的引用,
	//局部变量temp在当前函数执行完毕后释放,再返回引用属于非法操作
  
	MyInteger operator++(int)  //局部变量temp不能引用传递
	{
		MyInteger temp = *this; //先记录当时结果
		m_Num++; //再做自增
		return temp;   //将记录结果返回
	}
  //或者
  //MyInteger& operator++(int)
	//{
	//	MyInteger *temp = this;
	//	temp = new MyInteger(*this);

	//	m_Num++;
	//	return *temp;
	//}
  
};

MyInteger::MyInteger()
{
	m_Num = 0;
}

ostream& operator<<(ostream& cout, MyInteger& myint)
{
	cout << "m_Num=" << myint.m_Num << endl;
	return cout;
}

void test01()
{
	MyInteger myint;
	cout << myint << endl;   //0
	cout << ++myint << endl; //1
	cout << myint++ << endl; //1
	cout << myint << endl;   //2
}

int main()
{
	test01();
	
	system("pause");
	return 0;
}

4.5.4赋值运算符重载

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

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

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

class Person
{
public:
	int* m_A;
	Person(int age)
	{
    //将年龄开辟到堆区
		m_A = new int(age);
	}

	~Person()
	{
		if (m_A != NULL)
		{
			delete m_A;
			m_A = NULL;
		}
	}

};

void test01()
{
	Person p1(18);
	Person p2(20);

	p2 = p1;  //赋值操作

	cout << "p1=" << *p1.m_A << endl;  //18
	cout << "p2=" << *p2.m_A << endl;  //18
}

int main()
{
	test01();
	
	system("pause");
	return 0;
}

上述代码运行崩溃。

原因:浅拷贝导致堆区内存重复释放,程序崩溃

解决:深拷贝解决浅拷贝带来的问题

//重载 赋值运算符

//1.注意:这里的重载函数的参数一定要以引用或者指针的形式传入!!
//2.不然在传入的时候进行了一次拷贝将赋值右边p2的值传入的时候临时变量记录的p2的属性m_Age的地址
//3.而出了赋值运算符重载函数会进行一次析构 这时p2的属性new出来的空间已经被释放了
//4.最后结束调用虽然深拷贝了但是程序还是会崩

	void operator=(Person& p)
	{
		//先判断是否有属性在堆区,如果有先释放,再深拷贝
		if (m_A != NULL)
		{
			delete m_A;
			m_A = NULL;
		}

		//编译器提供浅拷贝
		//m_A = p.m_A;

    //深拷贝 直接赋值的话指针m_Age之前指向内容的地址就丢了,内存依然在那里,没法释放只会空占内存
		m_A = new int(*p.m_A);
    
	}
//重载 赋值运算符
Person& operator=(Person& p)
{
	if (m_A != NULL)
	{
		delete m_A;
		m_A = NULL;
	}
	
    //提供深拷贝 解决浅拷贝问题
	m_A = new int(*p.m_A);
    
    //返回自身
	return *this;
}

void test01()
{
	Person p1(10);
	Person p2(20);
	Person p3(30);

	p3 = p2 = p1; //赋值操作

	cout << "p1=" << *p1.m_A << endl;  //10
	cout << "p2=" << *p2.m_A << endl;  //10
	cout << "p3=" << *p3.m_A << endl;  //10
}

4.5.5 关系运算符重载

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

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_Name == p.m_Name && this->m_Age == p.m_Age)
		{
			return true;
		}
		return false;
	}

	//重载 !=
	bool operator!=(Person& p)
	{
		if (this->m_Name == p.m_Name && this->m_Age == p.m_Age)
		{
			return false;
		}
		return true;
	}
};

void test01()
{
	Person p1("Tom", 20);
	Person p2("Tom", 20);
	if (p1 == p2)
	{
		cout << "p1=p2" << endl;
	}
	else {
		cout << "p1!=p2" << endl;
	}
	if (p1 != p2)
	{
		cout << "p1!=p2" << endl;
	}
	else
	{
		cout << "p1=p2" << endl;
	}
}

int main()
{
	test01();
	
	system("pause");
	return 0;
}

4.5.6函数调用运算符重载

  • 函数调用运算符 ()也可以重载
  • 由于重载后使用的方式非常像函数的调用,因此称为仿函数
  • 仿函数没有固定写法,非常灵活
class MyPrint
{
public:

	//重载函数调用()运算符
	void operator()(string test)
	{
		cout << test << endl;
	}
};

class MyAdd
{
public:

	//重载函数调用()运算符
	int operator()(int num1, int num2)
	{
		return num1 + num2;
	}
};

void MyPrint02(string test)
{
	cout << test << endl;
}

void test01()
{
	MyPrint myPrint;
	myPrint("Hello World!");  //重载(),又称仿函数

	MyPrint02("hello world!"); //函数调用

	//仿函数非常灵活
	MyAdd myAdd;
	int ret = myAdd(1, 2);  //1+2=3
	cout << ret << endl;

	//匿名函数对象   MyAdd()是匿名对象
	cout << MyAdd()(100, 100) << endl; //200
}

int main()
{
	test01();
	
	system("pause");
	return 0;
}

4.6继承

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

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

第二阶段C++核心编程(黑马程序员)——Day7_C++

我们发现,定义这些类时,下级别的成员除了拥有上一级的共性,还有自己的特性。

这个时候考虑使用继承,减少重复代码。


4.6.1继承的基本语法

例如我们看到很多网站中,都有公共的头部,公共的底部,甚至公共的左侧列表,只有中心内容不同。接下来我们分别利用普通写法和继承的写法来实现网页中的内容,看一下继承存在的意义以及好处。

继承实现:

class BasePage
{
public:
	void header()
	{
		cout << "首页、公开课、登录、注册...公共头部" << endl;
	}

	void footer()
	{
		cout << "帮助中心、交流合作、站内地图...公共底部" << endl;
	}

	void left()
	{
		cout << "java、python、C++...公共列表" << endl;
	}
};

//JAVA页面
class JAVA : public BasePage
{
public:
	void content()
	{
		cout << "JAVA学科视频" << endl;
	}
};

//CPP页面
class CPP : public BasePage
{
public:
	void content()
	{
		cout << "C++学科视频" << endl;
	}
};

void test01()
{
	cout << "JAVA视频页面:" << endl;
	JAVA ja;
	ja.header();
	ja.footer();
	ja.left();
	ja.content();
	cout << "------------------------" << endl;

	cout << "C++视频页面:" << endl;
	CPP cpp;
	cpp.header();
	cpp.footer();
	cpp.left();
	cpp.content();
}


int main()
{
	test01();

	system("pause");
	return 0;
}

总结:

继承的好处:可以减少重复的代码

class A:public B;

  • A类称为子类或 派生类
  • B类称为父类或 基类

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

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

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


4.6.2继承方式

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

继承方式一共有三种:

  • 公共继承 public
  • 保护继承 protected
  • 私有继承 private

第二阶段C++核心编程(黑马程序员)——Day7_C++_02

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 = 10;  //父类中的保护权限成员,子类中依然是保护权限
		//m_C = 10;  //出错,父类中私有权限成员,子类访问不到
	}
};

//保护继承
class Son2 : protected Base1
{
public:
	void func()
	{
		m_A = 100;  //父类中的公共成员,子类中是保护权限
		m_B = 100;  //父类中的保护成员,子类中是保护权限
		//m_C = 100; //出错,父类中私有成员,子类访问不到
	}
};

//私有继承
class Son3 : private Base1
{
public:
	void func()
	{
		m_A = 100;  //父类中的公共成员,子类中是私有权限
		m_B = 100;  //父类中的保护成员,子类中是私有权限
		//m_C = 100; //出错,父类中私有成员,子类访问不到
	}
};

void test01()
{
	Son1 s1;
	s1.m_A = 100;
	//s1.m_B = 100;  //出错,Son1中,m_B是保护权限,类外无法访问

	Son2 s2;
	//s2.m_A = 1000;   //出错,Son2中,m_A是保护权限,类外无法访问
	//s2.m_B = 1000;   //出错,Son2中,m_B是保护权限,类外无法访问

	Son3 s3;
	//s2.m_A = 1000;   //出错,Son3中,m_A是私有权限,类外无法访问
	//s2.m_B = 1000;   //出错,Son3中,m_B是私有权限,类外无法访问
}


int main()
{
	test01();

	system("pause");
	return 0;
}

4.6.3继承中的对象模型

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

父类中所有非静态成员属性 被编译器隐藏,因此访问不到,但被继承。

class Base
{
public:
	int m_A;
protected:
	int m_B;
private:
	int m_C; //私有成员只是被隐藏了,但还是会继承下去
};

//公共继承
class Son : public Base
{
public:
	int m_D;
};

//利用开发人员命令提示工具查看对象模型
//跳转盘符 E:
//跳转文件路径  cd 具体路径
//查看命名
// cl  /d1 reportSingleClassLayout类名 文件名

void test01()
{
	cout << "size of Son=" << sizeof(Son) << endl;   //16B
}


int main()
{
	test01();

	system("pause");
	return 0;
}

第二阶段C++核心编程(黑马程序员)——Day7_C++_03

父类中私有成员也被子类继承,只是被编译器隐藏后访问不到。


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 s;  
}


int main()
{
	test01(); //Base构造函数!
	          //Son构造函数!
		      //Son析构函数!
		      //Base析构函数!

	system("pause");
	return 0;
}

4.6.5继承同名成员处理方式

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

  • 访问子类同名成员——直接访问即可
  • 访问父类同名成员——需要加作用域
Son s;
s.m_A;           //访问子类同名属性
s.Base::m_A;     //访问父类同名属性

s.func();        //访问子类同名函数   
s.Base::func();  //访问父类同名函数
class Base
{
public:
	int m_A;
	Base()
	{
		m_A = 100;
	}

	void func()
	{
		cout << "Base - func()" << endl;
	}
	void func(int a)
	{
		cout << "Base - func(int)" << endl;
	}
};

class Son : public Base
{
public:
	int m_A;
	Son()
	{
		m_A = 200;
	}

	void func()
	{
		cout << "Son - func()" << endl;
	}
	
};

//父类和子类中存在同名属性
void test01()
{
	Son s;  
	cout << "Son m_A=" << s.m_A << endl;         //200
	cout << "Base m_A=" << s.Base::m_A << endl;  //100 需要加作用域
}

//父类和子类中存在同名函数
void test02()
{
	Son s;
	s.func();        //Son - func()
	s.Base::func();  //Base - func()

	//如果子类出现和父类同名的成员函数,子类的同名成员会隐藏父类中所有成员函数
	//s.func(100);  出错

	s.Base::func(100); //如果想访问到父类中被隐藏的同名成员函数,需要加作用域
}

int main()
{
	test01(); 
	test02();

	system("pause");
	return 0;
}

总结

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


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

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

静态成员和非静态成员出现同名,处理方式一致,只不过有两种访问方式(通过对象 和 通过类名)。

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

子类和父类出现同名静态成员函数,也会隐藏父类中所有同名成员函数;如果想访问父类中被隐藏同名成员,需要加作用域。

class Base
{
public:
	static int m_A;

	static void func()
	{
		cout << "Base- static void func()" << endl;
	}
  
  static void func(int a)
	{
		cout << "Base- static void func(int)" << endl;
	}
};

int Base::m_A = 100;

class Son : public Base
{
public:
	static int m_A;

	static void func()
	{
		cout << "Son- static void func()" << endl;
	}
	
};
int Son::m_A = 200;

//同名静态成员属性
void test01()
{
	//1、通过对象访问
	Son s;
	cout << "Son—— m_A=" << s.m_A << endl;  //200
	cout << "Base——m_A=" << s.Base::m_A << endl;  //100

	//2、通过类名访问
	cout << "Son—— m_A=" << Son::m_A << endl;
	
	//第一个::代表通过类名的方式访问  第二个::代表访问父类作用域下
	cout << "Base——m_A=" << Son::Base::m_A << endl;
}

//同名静态成员函数
void test02()
{
	Son s;
	//1、通过对象访问
	s.func();
	s.Base::func();

	//2、通过类名访问
	Son::func();
	Son::Base::func();
  
  //出现同名,子类会隐藏掉父类中所有同名成员函数,需要加作用域访问
  Son::Base::func(100);
}

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;
	}
};

//子类继承Base1、Base2
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 Son=" << sizeof(s) << endl;  //16B

	cout << "Base1 m_A=" << s.Base1::m_A << endl; //100
	cout << "Base2 m_A=" << s.Base2::m_A << endl; //200
}

总结:多继承中如果父类出现了同名情况,子类使用时要加作用域。


4.6.8菱形继承

菱形维承概念

  • 两个派生类继承同一个基类
  • 又有某个类同时继承者两个派生类

这种继承被称为菱形继承,或者钻石继承

典型的菱形继承案例

第二阶段C++核心编程(黑马程序员)——Day7_C++_04

菱形继承问题

第二阶段C++核心编程(黑马程序员)——Day7_C++_05

  1. 羊继承了动物的数据,驼同样继承了动物的数据,当羊驼使用数据时,就会产生二义性。
  2. 羊驼继承自动物的数据继承了两份,其实这份数据我们只需一份。
//动物类
class Animal
{
public:
	int m_Age;
};

//利用虚继承 解决菱形继承问题
//在继承前加上关键字 virtual 变为虚继承,Animal称为虚基类

class Sheep :virtual public Animal{};

class Tuo :virtual public Animal{};

class SheepTuo :public Sheep, public Tuo{};

//同名静态成员属性
void test01()
{
	SheepTuo st;
	st.Sheep::m_Age = 10;
	st.Tuo::m_Age = 11;

	//虚继承前:当菱形继承,两个父类拥有相同数据,需要加作用域区分
	cout << "Sheep::m_Age=" << st.Sheep::m_Age << endl;  //10
	cout << "Tuo::m_Age=" << st.Tuo::m_Age << endl;   //11

	//这份数据只有一份就行,菱形继承导致两份数据,资源浪费
	//虚继承后:输出都是11
	cout << st.m_Age << endl;  //11
}

总结:

  1. 菱形继承带来的主要问题是子类继承两份相同的数据,导致资源浪费以及毫无意义
  2. 利用虚继承可以解决菱形继承问题


4.7多态

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

多态分为两类

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

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

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

下面通过案例进行讲解多态。

多态基本语法:
//动物类
class Animal
{
public:

	//Speak函数是虚函数
  //函数前加上virtual关键字,变成虚函数,那么编译器在编译时就不能确定函数调用了
	virtual void speak()
	{
		cout << "动物说话" << endl;
	}
};

//猫类
class Cat :public Animal
{
public:

	//重写:函数返回指类型 函数名 参数列表 完全相同
	//Cat类重写Animal类的speak虚函数
	void speak()
	{
		cout << "猫说话" << endl;
	}
};

//狗类
class Dog :public Animal
{
public:
	void speak()
	{
		cout << "狗说话" << endl;
	}
};

//地址早绑定,在编译阶段确定函数地址
//如果想猫说话,那么这个地址不能提前绑定,需要在运行阶段绑定,地址晚绑定

//执行说话的函数
void doSpeak(Animal &animal)  
{
	animal.speak();
}

//动态多态满足条件
//1、有继承关系
//2、子类重写父类的虚函数

//多态使用:父类指针或引用 指向子类对象

//同名静态成员函数
void test01()
{
	Cat cat;
	doSpeak(cat);  //Animal & animal = cat; 猫说话

	Dog dog;
	doSpeak(dog);  //狗说话
}

总结:

多态满足条件

  • 有继承关系
  • 子类重写父类中的虚函数

多态使用条件

  • 父类指针或引用指向子类对象

重写:函数返回值类型 函数名 参数列表 完全一致称为重写


多态的原理剖析:

第二阶段C++核心编程(黑马程序员)——Day7_C++_06

任何指针都占 4B


4.7.2多态案例1计算器类

案例描述:

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

多态的优点:

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

普通实现:

class Calculator
{
public:
	int m_Num1;
	int m_Num2;

	int getResult(string oper)
	{
		if (oper == "+")
		{
			return m_Num1 + m_Num2;
		}
		else if (oper == "-")
		{
			return m_Num1 - m_Num2;
		}
		else if (oper == "*")
		{
			return m_Num1 * m_Num2;
		}

		//如果想扩展新的功能,需要修改源码
		//真实开发中,提倡开闭原则:对扩展进行开放,对修改进行关闭
	}
};


void test01()
{
	Calculator c;
	c.m_Num1 = 1;
	c.m_Num2 = 2;

	cout << c.m_Num1 << "+" << c.m_Num2 << "=" << c.getResult("+") << endl;
}

多态实现:

class AbstractCalculator
{
public:
	int m_Num1;
	int m_Num2;

	virtual int getResult()
	{
		return 0;
	}
};

//加法计算器类
class AddCalculator :public AbstractCalculator
{
public:
	int getResult()
	{
		return m_Num1 + m_Num2;
	}
};

//减法计算器类
class SubCalculator :public AbstractCalculator
{
public:
	int getResult()
	{
		return m_Num1 - m_Num2;
	}
};

//乘法计算器类
class MulCalculator :public AbstractCalculator
{
public:
	int getResult()
	{
		return m_Num1 * m_Num2;
	}
};

void test01()
{
	//多态使用条件
	//父类指针或者引用指向子类对象

	AbstractCalculator* abc = new AddCalculator;
	abc->m_Num1 = 10;
	abc->m_Num2 = 10;

	cout << abc->m_Num1 << "+" << abc->m_Num2 << "=" << abc->getResult() << endl;

	//用完记得销毁
	delete abc;

	//减法运算
	abc = new SubCalculator;
	abc->m_Num1 = 10;
	abc->m_Num2 = 10;

	cout << abc->m_Num1 << "-" << abc->m_Num2 << "=" << abc->getResult() << endl;
	delete abc;
}

总结:C++开发提倡利用多态设计程序架构


4.7.3纯虚函数和抽象类

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

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

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

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

抽象类特点:

  • 无法实例化对象
  • 子类必须重写抽象类中的纯虚函数,否则也属于抽象类
class Base   //只要有一个纯虚函数,就称为抽象类
{
public:
	virtual void func() = 0;  //纯虚函数
};

class Son :public Base
{
	virtual void func() 
	{
		cout << "func" << endl;
	}
};

void test01()
{
	//Base b;   错误,抽象类无法实例化对象
	//new Base; 错误,抽象类无法实例化对象
	//Son s;    抽象类的子类必须重写父类中的纯虚函数,否则也属于抽象类,无法实例化对象

	Base* base = new Son;
	base->func(); //func
}

4.7.4多态案例2制作饮品

案例描述:

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

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

冲咖啡:1、煮水 2、冲泡咖啡 3、倒入杯中 4、加糖和牛奶

冲茶叶:1、煮水 2、冲泡茶叶 3、倒入杯中 4、加柠檬

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 MakeCoffee :public AbstractDrinking
{
public:
	virtual void Boil()
	{
		cout << "煮水" << endl;
	}
	virtual void Brew()
	{
		cout << "冲泡咖啡" << endl;
	}
	virtual void PourInCup()
	{
		cout << "倒入杯中" << endl;
	}
	virtual void PutSomething()
	{
		cout << "加糖和牛奶" << endl;
	}
};

class MakeTea :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 MakeCoffee);

	cout << "-------------"<< endl;

	doWork(new MakeTea);
}

4.7.5虚析构和纯虚析构

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

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

当子类中new数据到堆区,且我们使用多态时将父类的指针指向子类对象,那么父类指针在释放时则无法调用子类的析构代码,此时父类析构必须用虚析构或者纯虚析构。

虚析构和纯虚析构共性:

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

虚析构和纯虚析构区别:

  • 如果是纯虚析构,该类属于抽象类,无法实例化对象

虚析构语法:

virtual ~类名( ) {}

纯虚析构语法:

virtual ~类名( ) = 0 ;

类名 : : ~类名( ) { } 

class Animal
{
public:
	virtual void speak() = 0;

	Animal()
	{
		cout << "Animal构造函数" << endl;
	}

	~Animal()
	{
		cout << "Animal析构函数" << endl;
	}
};

class Cat :public Animal
{
public:
	string* m_Name;

	Cat(string name)
	{
		cout << "Cat构造函数" << endl;
		m_Name = new string(name);
	}

	virtual void speak()
	{
		cout << *m_Name <<"猫叫" << endl;
	}

	~Cat()
	{
		if (m_Name != NULL)
		{
			delete m_Name;
			m_Name = NULL;
		}
		cout << "Cat析构函数" << endl;
	}
};


void test01()
{
	Animal* animal = new Cat("Tom");    
	animal->speak();				    
	
	//父类指针在析构时不会调用子类中的析构函数,导致子类中堆属性出现内存泄露

	//解决:父类析构函数变为虚析构  virtual ~Animal() {}
	delete animal;
}
Animal构造函数
Cat构造函数
Tom猫叫
Animal析构函数

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

virtual ~Animal()
{
	cout << "Animal虚析构函数" << endl;
}

输出变为:

Animal构造函数
Cat构造函数
Tom猫叫
Cat析构函数
Animal析构函数

或者改为纯虚析构函数:

class Animal
{
  virtual ~Animal() = 0;
};

Animal::~Animal()
{
  cout<<"Animal纯虚析构函数"<<endl;
}

语法强制纯虚析构函数必须有函数实现,因为有时父类也有一些数据开辟在堆区

  • 纯虚析构:需要声明,也需要实现;有纯虚析构,这个类也属于抽象类,无法实例化对象;
  • 纯虚函数:只有声明,没有实现。

总结

  1. 虚析构或纯虚析构就是用来解决通过父类指针释放子类对象
  2. 如果子类中没有堆区数据,可以不写为虚析构或纯虚析构
  3. 拥有纯虚析构函数的类也属于抽象类


4.7.6多态案例3电脑组装

案例描述

电脑主要组成部件为CPU(用于计算),显卡(用于显示),内存条(用于存储)

将每个零件封装出抽象基类,并且提供不同的厂商生产不同的零件,例如lntel厂商和Lenovo厂商

创建电脑类提供让电脑工作的函数,并且调用每个零件工作的接口

测试时组装三台不同的电脑进行工作

第二阶段C++核心编程(黑马程序员)——Day7_C++_07

第二阶段C++核心编程(黑马程序员)——Day7_C++_08

//抽象CPU类
class CPU
{
public:
	virtual void calculate() = 0;
};

//抽象显卡类
class VideoCard
{
public:
	virtual void display() = 0;
};

//抽象内存类
class Memory
{
public:
	virtual void storage() = 0;
};

//电脑类
class Computer
{
private:
	CPU* m_cpu;
	VideoCard* m_vc;
	Memory* m_mem;

public:
	Computer(CPU* cpu, VideoCard* vc, Memory* mem)
	{
		m_cpu = cpu;
		m_vc = vc;
		m_mem = mem;
	}

	void work()
	{
		//零件工作,调用接口
		m_cpu->calculate();
		m_vc->display();
		m_mem->storage();
	}

	//提供析构函数,释放3个电脑零件
	~Computer()
	{
		if (m_cpu != NULL)
		{
			delete m_cpu;
			m_cpu = NULL;
		}
		if (m_vc != NULL)
		{
			delete m_vc;
			m_vc = NULL;
		}
		if (m_mem != NULL)
		{
			delete m_mem;
			m_mem = NULL;
		}
	}
};

//Intel厂商
class IntelCPU :public CPU
{
public:
	void calculate()
	{
		cout << "Intal的CPU开始计算;" << endl;
	}
};

class IntelVideoCard :public VideoCard
{
public:
	void display()
	{
		cout << "Intal的显卡开始显示;" << endl;
	}
};

class IntelMemory :public Memory
{
public:
	void storage()
	{
		cout << "Intal的内存条开始存储;" << endl;
	}
};

//Lenovo厂商
class LenovoCPU :public CPU
{
public:
	void calculate()
	{
		cout << "Lenovo的CPU开始计算;" << endl;
	}
};

class LenovoVideoCard :public VideoCard
{
public:
	void display()
	{
		cout << "Lenovo的显卡开始显示;" << endl;
	}
};

class LenovoMemory :public Memory
{
public:
	void storage()
	{
		cout << "Lenovo的内存条开始存储;" << endl;
	}
};

void test01()
{
	//第一台电脑零件
	CPU* intelCPU = new IntelCPU;
	VideoCard* intelCard = new IntelVideoCard;
	Memory* intelMem = new IntelMemory;

	//组装第一台电脑
	Computer* pc1 = new Computer(intelCPU, intelCard, intelMem);
	pc1->work();
	delete pc1;

	cout << "------------------------" << endl;

	//组装第二台电脑
	Computer* pc2 = new Computer(new LenovoCPU,new LenovoVideoCard,new LenovoMemory);
	pc2->work();
	delete pc2;
}

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::app 与 ios::ate 的区别主要在多线程写文件时有区别:

ios::app:第一个线程(进程)往里面写了个d,第二个线程(进程)写了个e的话,结果是abcde;

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

例如:用二进制方式写文件  ios::binary I ios::out

#include<iostream>
using namespace std;
#include<fstream>

void test01()
{
	ofstream ofs;
	
	ofs.open("test.txt", ios::out);

	ofs << "姓名:张三" << endl;
	ofs << "性别:男" << endl;

	ofs.close();
}


int main()
{
	test01();  

	system("pause");
	return 0;
}

总结:

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


5.1.2读文件

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

读文件步骤如下:

  1. 包含头文件 #include<fstream>
  2. 创建流对象 ifstream ifs;
  3. 打开文件并判断文件是否打开成功 ifs.open("文件路径", 打开方式);
  4. 读数据 四种方式读取
  5. 关闭文件 ifs.close();
#include<iostream>
using namespace std;
#include<fstream>
#include<string>

void test01()
{
	ifstream ifs;
	
	//打开文件并判断是否打开成功
	ifs.open("test.txt", ios::in);
	if (!ifs.is_open())
	{
		cout << "文件打开失败!" << endl;
		return;
	}
	
	//读数据
	//方法一

	/*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)  //EOF: end of file
	{
		cout << c;
	}*/

	ifs.close();
}

总结:

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


5.2二进制文件

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

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

5.2.1写文件

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

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

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

#include<iostream>
using namespace std;
#include<fstream>

class Person
{
public:
	char m_Name[64];
	int m_Age;
};

void test01()
{
  //1、包含头文件
  
	//2、创建输出流对象
	ofstream ofs("person.txt", ios::out | ios::binary);

	//3、打开文件
	//ofs.open("person.txt", ios::out | ios::binary);
	
	Person p = { "张三",20 };
  
  //4、写文件
	//_Str是传入文件的首指针,就是这里为什么要取地址,因为返回的是Person的首指针
	ofs.write((const char*)&p, sizeof(Person));
  
  //5、关闭文件
	ofs.close();
}

总结:

文件输出流对象 可以通过 write 函数,以二进制方式写数据。


5.2.2读文件

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

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

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

#include<iostream>
using namespace std;
#include<fstream>

class Person
{
public:
	char m_Name[64];
	int m_Age;
};

void test01()
{
	//1、包含头文件

	//2、创建流对象
	ifstream ifs;

	//3、打开文件 判断文件是否打开成功
	ifs.open("person.txt", ios::in | ios::binary);
	if (!ifs.is_open())
	{
		cout << "文件打开失败" << endl;
		return;
	}

	//4、读文件
	Person p;
	ifs.read((char*)&p, sizeof(Person));
	cout << "姓名:" << p.m_Name << "年龄:" << p.m_Age << endl;

	//5、关闭文件
	ifs.close();
}

文件输入流对象 可以通过read函数,以二进制方式读数据


好博客就要一起分享哦!分享海报

此处可发布评论

评论(0展开评论

暂无评论,快来写一下吧

展开评论

您可能感兴趣的博客

客服QQ 1913284695