第二阶段C++核心编程(黑马程序员)——Day7
标签: 第二阶段C++核心编程(黑马程序员)——Day7 博客 51CTO博客
2023-07-14 18:24:15 172浏览
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个函数
- 默认构造函数(无参,函数体为空)
- 默认析构函数(无参,函数体为空)
- 默认拷贝函数,对属性进行值拷贝
- 赋值运算符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继承
继承是面向对象三大特性之一
有些类与类之间存在特殊的关系,例如下图中:

我们发现,定义这些类时,下级别的成员除了拥有上一级的共性,还有自己的特性。
这个时候考虑使用继承,减少重复代码。
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

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

父类中私有成员也被子类继承,只是被编译器隐藏后访问不到。
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;
}
总结:
- 子类对象可以直接访问到子类中同名成员
- 子类对象加作用域可以访问到父类同名成员
- 当子类与父类拥有同名的成员函数,子类会隐藏父类中同名成员函数,加作用域可以访问到父类中同名函数
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菱形继承
菱形维承概念:
- 两个派生类继承同一个基类
- 又有某个类同时继承者两个派生类
这种继承被称为菱形继承,或者钻石继承
典型的菱形继承案例:

菱形继承问题:

- 羊继承了动物的数据,驼同样继承了动物的数据,当羊驼使用数据时,就会产生二义性。
- 羊驼继承自动物的数据继承了两份,其实这份数据我们只需一份。
//动物类
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
}
总结:
- 菱形继承带来的主要问题是子类继承两份相同的数据,导致资源浪费以及毫无意义
- 利用虚继承可以解决菱形继承问题
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); //狗说话
}
总结:
多态满足条件
- 有继承关系
- 子类重写父类中的虚函数
多态使用条件
- 父类指针或引用指向子类对象
重写:函数返回值类型 函数名 参数列表 完全一致称为重写
多态的原理剖析:

任何指针都占 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;
}
语法强制纯虚析构函数必须有函数实现,因为有时父类也有一些数据开辟在堆区
- 纯虚析构:需要声明,也需要实现;有纯虚析构,这个类也属于抽象类,无法实例化对象;
- 纯虚函数:只有声明,没有实现。
总结:
- 虚析构或纯虚析构就是用来解决通过父类指针释放子类对象
- 如果子类中没有堆区数据,可以不写为虚析构或纯虚析构
- 拥有纯虚析构函数的类也属于抽象类
4.7.6多态案例3电脑组装
案例描述:
电脑主要组成部件为CPU(用于计算),显卡(用于显示),内存条(用于存储)
将每个零件封装出抽象基类,并且提供不同的厂商生产不同的零件,例如lntel厂商和Lenovo厂商
创建电脑类提供让电脑工作的函数,并且调用每个零件工作的接口
测试时组装三台不同的电脑进行工作


//抽象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>
文件类型分为两种:
- 文本文件——文件以文本的ASCII码形式存储在计算机中
- 二进制文件——文件以文本的二进制形式存储在计算机中,用户一般不能直接读懂它们
操作文件的三大类:
- ofstream——写操作
- ifstream——读操作
- fstream——读写操作
5.1文本文件
5.1.1写文件
写文件步骤如下:
- 包含头文件 #include <fstream>
- 创建流对象 ofstream ofs;
- 打开文件 ofs.open("文件路径", 打开方式);
- 写数据 ofs<< "写入的数据";
- 关闭文件 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读文件
读文件与写文件步骤相似,但是读取方式相对比较多
读文件步骤如下:
- 包含头文件 #include<fstream>
- 创建流对象 ifstream ifs;
- 打开文件并判断文件是否打开成功 ifs.open("文件路径", 打开方式);
- 读数据 四种方式读取
- 关闭文件 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)展开评论
展开评论



