- 1.封装
- 2.对象的初始化和清理
- 2.1构造函数的分类
- 2.2构造函数的使用
- 2.3拷贝构造函数调用时机
- 2.4构造函数调用规则
- 2.5深拷贝与浅拷贝(未看)
- 2.6初始化列表
- 2.7类作为成员
- 2.8静态成员
- 3. C++对象模型和this指针
- 3.1成员变量和成员函数分开存储
- 3.2 this指针
- 3.3空指针访问成员函数
- 3.4 const修饰成员函数
- 4.友元friend
- 4.1 全局函数做友元
- 4.2 类做友元
- 4.3 成员函数做友元
- 5.运算符重载
- 5.1 加号运算符重载
- 5.2 左移运算符重载
- 6.继承
- 6.1 继承的基本语法
- 6.2 继承方式
- 6.3 继承中的对象模型
- 6.4 继承中构造和析构顺序
- 6.5 继承同名成员处理方式
- 6.6 继承同名静态成员处理方式
- 6.7 多继承语法
- 6.8 菱形继承
- 7.多态
- 7.1多态的基本语法
- 7.2多态的深入剖析
- 7.3 多态案例:计算机类
- 7.4 纯虚函数和抽象类
- 7.5 虚析构和纯虚析构
- 8. struct和class区别
- 9.文件操作
- 9.1文本文件
- 9.1.1写文件
- 9.1.2读文件
- 9.2二进制文件
- 9.2.1写文件
- 9.2.2读文件
C++面向对象的三大特性为:封装、继承、多态
C++认为万事万物都皆为对象,对象上有其属性和行为
- 将属性和行为作为一个整体,表现生活中的事物
- 将属性和行为加以权限控制
语法:class 类名{ 访问权限: 属性 / 行为 };
const double pi=3.14;
class circle
{
public://访问权限 公共的权限
int r;
void c()
{
cout<<"圆周长为:" << 2 * pi * this->r<
circle c;//与Java不同
c.r = 4;
c.c();
}
注意public权限与实例化
类在设计时,可以把属性和行为放在不同的权限下,加以控制。
访问权限有三种:
- public:公共权限,类内可以访问 类外可以访问
- protected:保护权限,类内可以访问 类外不可以访问,子类可以访问
- private:私有权限,类内可以访问 类外不可以访问,子类不可以访问
class Person
{
//姓名 公共权限
public:
string m_Name;
//汽车 保护权限
protected:
string m_Car;
//银行卡密码 私有权限
private:
int m_Password;
public:
void func()
{
m_Name = "张三";
m_Car = "拖拉机";
m_Password = 123456;
}
};
void main()
{
Person p;
p.m_Name = "李四";
//p.m_Car = "奔驰"; //保护权限类外访问不到
//p.m_Password = 123; //私有权限类外访问不到
}
将所有成员属性设置为私有
- 可以自己控制读写权限
- 对于写权限,我们可以检测数据的有效性
class Person {
public:
//姓名设置可读可写
void setName(string name) {
m_Name = name;
}
string getName()
{
return m_Name;
}
//获取年龄
int getAge() {
return m_Age;
}
//设置年龄
void setAge(int age) {
if (age < 0 || age > 150) {
cout << "你个老妖精!" << endl;
return;
}
m_Age = age;
}
//情人设置为只写
void setLover(string lover) {
m_Lover = lover;
}
private:
string m_Name; //可读可写 姓名
int m_Age; //只读 年龄
string m_Lover; //只写 情人
};
void main()
{
Person p;
//姓名设置
p.setName("张三");
cout << "姓名: " << p.getName() << endl;
//年龄设置
p.setAge(50);
cout << "年龄: " << p.getAge() << endl;
//情人设置
p.setLover("苍井");
//cout << "情人: " << p.m_Lover << endl;//只写属性,不可以读取
}
2.对象的初始化和清理
使用完一个对象或变量,没有及时清理,也会造成一定的安全问题
对象的初始化和清理工作是编译器强制要我们做的事情,因此如果我们不提供构造和析构,编译器会提供,但是编译器提供的构造函数和析构函数是空实现。
- 构造函数:主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无须手动调用。
- 析构函数:主要作用在于对象销毁前系统自动调用,执行一些清理工作。
构造函数语法:类名(){}
- 构造函数,没有返回值也不写void
- 函数名称与类名相同
- 构造函数可以有参数,因此可以发生重载
- 程序在调用对象时候会自动调用构造,无须手动调用,而且只会调用一次
析构函数语法: ~类名(){}
2.1构造函数的分类
- 析构函数,没有返回值也不写void
- 函数名称与类名相同,在名称前加上符号 ~
- 析构函数不可以有参数,因此不可以发生重载
- 程序在对象销毁前会自动调用析构,无须手动调用,而且只会调用一次
- 按参数分为: 有参构造和无参构造
- 按类型分为: 普通构造和拷贝构造
拷贝构造函数语法
Person(const Person& p)
{
//拷贝一个personA出来,复制到要创建的B身上
age = p.age;
cout << "拷贝构造函数!" << endl;
}
构造函数:
class Person {
public:
//无参(默认)构造函数
Person() {
cout << "无参构造函数!" << endl;
}
//有参构造函数
Person(int a) {
age = a;
cout << "有参构造函数!" << endl;
}
//拷贝构造函数
Person(const Person& p) {
//拷贝一个personA出来,复制到要创建的B身上
age = p.age;
cout << "拷贝构造函数!" << endl;
}
2.2构造函数的使用
三种调用方式:括号法、显示法、隐式转换法
括号法,常用:
Person p1(10);调用无参构造函数不能加括号,如果加了编译器认为这是一个函数声明person p1();
显式法:
Person(10)单独写就是匿名对象 当前行结束之后,马上析构回收匿名对象
不要利用拷贝构造函数初始化一个匿名对象person(P3);编译器会认为是`person(P3)===person P3对象实例化
隐式转换法:
2.3拷贝构造函数调用时机
- 使用一个已经创建完毕的对象来初始化一个新对象
- 值传递的方式给函数参数传值
- 以值方式返回局部对象
class Person {
public:
Person() {
cout << "无参构造函数!" << endl;
mAge = 0;
}
Person(int age) {
cout << "有参构造函数!" << endl;
mAge = age;
}
Person(const Person& p) {
cout << "拷贝构造函数!" << endl;
mAge = p.mAge;
}
//析构函数在释放内存之前调用
~Person() {
cout << "析构函数!" << endl;
}
public:
int mAge;
};
//1. 使用一个已经创建完毕的对象来初始化一个新对象
void test01() {
Person man(100); //p对象已经创建完毕
Person newman(man); //调用拷贝构造函数
Person newman2 = man; //拷贝构造
//Person newman3;
//newman3 = man; //不是调用拷贝构造函数,赋值操作
}
//2. 值传递的方式给函数参数传值
//相当于Person p1 = p;
void doWork(Person p1) {}
void test02() {
Person p; //无参构造函数
doWork(p);
}
//3. 以值方式返回局部对象
Person doWork2()
{
Person p1;
cout << (int *)&p1 << endl;
return p1;
}
void test03()
{
Person p = doWork2();
cout << (int *)&p << endl;
}
void main() {
//test01();
//test02();
test03();
}
2.4构造函数调用规则
c++编译器至少给一个类添加3个函数:
1.默认构造函数(无参,函数体为空)
2.默认析构函数(无参,函数体为空)
3.默认拷贝构造函数,对属性进行值拷贝
构造函数调用规则:
- 如果用户定义有参构造函数,c++不在提供默认无参构造,但是会提供默认拷贝构造
- 如果用户定义拷贝构造函数,c++不会再提供其他构造函数
class Person {
public:
//无参(默认)构造函数
Person() {
cout << "无参构造函数!" << endl;
}
//有参构造函数
Person(int a) {
age = a;
cout << "有参构造函数!" << endl;
}
//拷贝构造函数
Person(const Person& p) {
age = p.age;
cout << "拷贝构造函数!" << endl;
}
//析构函数
~Person() {
cout << "析构函数!" << endl;
}
public:
int age;
};
void test01()
{
Person p1(18);
//如果不写拷贝构造,编译器会自动添加拷贝构造,并且做浅拷贝操作
Person p2(p1);
cout << "p2的年龄为: " << p2.age << endl;
}
void test02()
{
//如果用户提供有参构造,编译器不会提供默认构造,会提供拷贝构造
Person p1; //此时如果用户自己没有提供默认构造,会出错
Person p2(10); //用户提供的有参
Person p3(p2); //此时如果用户没有提供拷贝构造,编译器会提供
//如果用户提供拷贝构造,编译器不会提供其他构造函数
Person p4; //此时如果用户自己没有提供默认构造,会出错
Person p5(10); //此时如果用户自己没有提供有参,会出错
Person p6(p5); //用户自己提供拷贝构造
}
void main() {
test01();
}
2.5深拷贝与浅拷贝(未看)
P110
2.6初始化列表语法:构造函数():属性1(值1),属性2(值2)... {}
类里有ABC三个变量,构造函数:
初始化列表:注意冒号位置
C++类中的成员可以是另一个类的对象,我们称该成员为 对象成员
class A {}
class B
{
A a;
}
2.8静态成员
- 当类中成员是其他类对象时,我们称该成员为 对象成员
- 构造的顺序是 :先调用对象成员的构造,再调用本类构造
- 析构顺序与构造相反
静态成员就是在成员变量和成员函数前加上关键字static,称为静态成员
- 静态成员变量
所有对象共享同一份数据
在编译阶段分配内存
类内声明,类外初始化
- 静态成员函数
所有对象共享同一个函数
静态成员函数只能访问静态成员变量
静态成员变量不属于某个对象,所有对象都共享同一份数据,因此静态成员变量有两种访问方式
- 通过对象进行访问
- 通过类名进行访问
静态函数有两种访问方式
- 通过对象进行访问
- 通过类名进行访问
- 静态成员函数可以访问静态成员变量,不可以访问非静态成员变量
静态成员函数也有访问权限的
class Person
{
public:
static int m_A; //静态成员变量
private:
static int m_B; //静态成员变量也是有访问权限的
};
int Person::m_A = 10;
int Person::m_B = 10;
void test01()
{
//静态成员变量两种访问方式
//1、通过对象
Person p1;
p1.m_A = 100;
cout << "p1.m_A = " << p1.m_A << endl;
Person p2;
p2.m_A = 200;
cout << "p1.m_A = " << p1.m_A << endl; //共享同一份数据
cout << "p2.m_A = " << p2.m_A << endl;
//2、通过类名
cout << "m_A = " << Person::m_A << endl;
//cout << "m_B = " << Person::m_B << endl; //私有权限访问不到
}
int main() {
test01();
return 0;
}
3. C++对象模型和this指针
3.1成员变量和成员函数分开存储
在C++中,类内的成员变量和成员函数分开存储,只有非静态成员变量(不包括函数)才属于类的对象上
C++编译器会给每个空对象也分配一个字节空间,是为了区分空对象占内存的位置
是4不是5,说明非静态成员变量属于类的对象上,同理可验证静态成员变量不属于类的对象上
P115
this指针指向被调用的成员函数所属的对象
this指针的用途:
- 当形参和成员变量同名时,可用this指针来区分
- 在类的非静态成员函数中返回对象本身,可使用return *this
- 引用方式返回的是本身,值的方式返回的是复制的另一个对象
- 引用指向本身内存,不用引用就是拷贝了,而拷贝指向另一个内存,不加引用的话返回的就不是p2了,而是别的Person 对象(这个对象值会一直加,最后值是40)
- 如果不用引用的方式返回,相当于返回与p2不同的另一个Person(只是age都是20),那么后续的加年龄操作与p2就没有关系了
- this指向p2的指针,而*this指向的就是p2这个对象本体,返回本体要用引用+*this
类:
调用:
空指针调用成员函数:第一个可以运行,第二个函数程序出错
报错原因:传入的指针里面为空,第二个函数调用了成员属性,可以修改为:为空就直接返回,
常函数:就是 const修饰的成员函数
语法:void ShowPerson() const {}const加在括号后面
- 成员函数后加const后我们称为这个函数为常函数
- 常函数内不可以修改成员属性
- 成员属性声明时加关键字mutable后,在常函数中依然可以修改
void ShowPerson() const {
mA = 100;
//但是this指针指向的对象的数据是可以修改的
- 隐含在每一个成员函数内部都有一个this指针,mA = 100; 相当于this->mA = 100;this指针的本质是一个指针常量(Person* const this;):指针的指向不可修改(创建对象,调用指针,this指针指向对象),故不可以修改this指向(但是可以修改this指向的值this->mA = 100;):
- 如果想让this指针指向的值也不可以修改,需要声明常函数
- 在成员函数后面加const,修饰的是this指向,让指针指向的值也不可以修改
- const修饰成员函数,表示指针指向的内存空间的数据不能修改,除了mutable修饰的变量。声明:mutable int m_B;
class Person
{
public:
mutable int m_B; //声明
public:
void ShowPerson() const
{
this->m_B = 100;//修改
}
};
常对象:
- 声明对象前加const称该对象为常对象const Person person;
- 常对象只能调用常函数,不能调用普通函数
- 常对象不能修改成员变量的值,但是可以访问,但是常对象可以修改mutable修饰成员变量
void test01() {
const Person person; //常量对象
cout << person.m_A << endl;
//person.mA = 100;
//常对象不能修改成员变量的值,但是可以访问
person.m_B = 100;
//但是常对象可以修改mutable修饰成员变量
}
4.友元friend
在程序里,有些私有属性 也想让类外特殊的一些函数或者类进行访问,就需要用到友元的技术
友元的目的就是让一个函数或者类 访问另一个类中私有成员
友元的三种实现:
4.1 全局函数做友元
- 全局函数做友元
- 类做友元
- 成员函数做友元
只需要把类外的全局函数的声明粘贴到类的首行,加上friend和分号,之后全局函数就能访问私有变量了
class Building
{
//告诉编译器goodGay全局函数,是 Building类的好朋友,可以访问类中的私有内容
friend void goodGay(Building * building);
public:
Building()
{
this->m_SittingRoom = "客厅";
this->m_BedRoom = "卧室";
}
public:
string m_SittingRoom; //客厅
private:
string m_BedRoom; //卧室
};
void goodGay(Building * building)
//引用或指针都可以传
//目的访问Building 中属性
{
cout<<"好基友正在访问:"<m_SittingRoom<< endl;
cout << "好基友正在访问:"<m_BedRoom<< endl;
}
4.2 类做友元
一个类可以访问另一个类的私有成员
类外写成员函数:
class Building
{
public:
Building();
}
Building::Building()
{
this->m_SittingRoom = "客厅";
this->m_BedRoom = "卧室";
}
注意成员属性前面没有类名
class goodGay
{
public:
goodGay();
void visit();
private:
Building *building;
};
class Building
{
//goodGay类可以访问到Building类中私有内容
friend class goodGay;
public:
Building();
public:
string m_SittingRoom; //客厅
private:
string m_BedRoom;//卧室
};
Building::Building()
{
this->m_SittingRoom = "客厅";
this->m_BedRoom = "卧室";
}
goodGay::goodGay()//类外写成员函数
{
building = new Building;//创建建筑物对象
}
void goodGay::visit()
{
cout << "好基友正在访问" << building->m_SittingRoom << endl;
cout << "好基友正在访问" << building->m_BedRoom << endl;
}
void test01()
{
goodGay gg;
gg.visit();
}
先创建一个goodGay对象,构造函数里面创建一个building对象,调用了building构造函数,把内部属性赋值,调用visit函数,访问sittingroom属性,卧室属性私有,无法访问
只需要把class class goodGay 放在另一个类首行,加上friend和分号即可
在被访问的类的首行:friend void 访问的类::成员函数();
class goodGay
{
public:
goodGay()
{
building = new Building;
}
void visit()
{
cout<<"正在访问"<m_SittingRoom<< endl;
cout<<"正在访问"<m_BedRoom<< endl;
}
//只让visit函数作为Building的好朋友,可以访问Building中私有内容
void visit2()
{
cout << "好基友正在访问" << building->m_SittingRoom << endl;
//cout << "好基友正在访问" << building->m_BedRoom << endl;
}
//visit2函数不可以访问Building中私有内容
private:
Building *building;
};
class Building
{
//goodGay类中的visit成员函数 是Building好朋友,可以访问私有内容
friend void goodGay::visit();
public:
Building();
public:
string m_SittingRoom; //客厅
private:
string m_BedRoom;//卧室
};
Building::Building()
{
this->m_SittingRoom = "客厅";
this->m_BedRoom = "卧室";
}
void test01()
{
goodGay gg;
gg.visit();
}
5.运算符重载
对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型
5.1 加号运算符重载作用:实现两个自定义数据类型相加的运算
定义一个person类,有A、B两属性,实例化2个对象p1,p2。person p3=p1+p2;是让p1和p2的A、B属性分别相加后返回新的对象p3
成员函数:
//成员函数实现 + 号运算符重载
Person operator+(const 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);简化为Person p3 =p1 + p2;
全局函数:
//全局函数实现 + 号运算符重载
Person operator+(const Person& p1, const Person& p2) {
Person temp(0, 0);
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 p3 =p1 + p2;
运算符重载,也可以发生函数重载:
//运算符重载 可以发生函数重载
Person operator+(const Person& p2, int val)
{
Person temp;
temp.m_A = p2.m_A + val;
temp.m_B = p2.m_B + val;
return temp;
}
5.2 左移运算符重载
- 对于内置的数据类型的表达式的的运算符是不可能改变的
- 不要滥用运算符重载
可以输出自定义数据类型:P122-P126
6.继承定义这些类时,下级别的成员除了拥有上一级的共性,还有自己的特性。
这个时候我们就可以考虑利用继承的技术,减少重复代码
语法:class 子类 : 继承方式 父类
子类:派生类
父类:基类
//父类
class BasePage
{
public:
void header()
{
}
};
//子类
class SpecialPage: public BasePage
{
public:
void content()
{
}
};
派生类中的成员,包含两大部分:
6.2 继承方式
- 一类是从基类继承过来的,一类是自己增加的成员
- 从基类继承过过来的表现其共性,而新增的成员体现了其个性
继承方式一共有三种:
- 公共继承
- 保护继承
- 私有继承
如图:
父类:private——>子类什么情况都无法访问,此外:
6.3 继承中的对象模型
- 公共继承public:继承后访问权限不变
- 保护继承protected:都变为保护权限protected
- 私有继承private:都变为私有权限private
class Base {
public:
int m_A;
protected:
int m_B;
private:
int m_C;
};
class Son :public Base
{
public:
int D;
};
void main()
{
cout << "son大小:" << sizeof(Son) << endl;
}
结论:父类中所有非静态成员属性都会被子类继承下去, 父类中私有成员也是被子类继承下去了,只是由编译器给隐藏后访问不到
PS:利用工具查看:
1.找到开发人员命令提示符
2.右键打开文件夹,复制路径
3.在步骤1的工具里进入类所在的cpp的路径
4.报告单个类的布局
cl /d1 reportSingleClassLayoutXXX(类名) cpp文件名,善用tab键,补全代码,注意是CL 和 D1
父类为Base类,继承了父类的ABC属性,以及自身的D属性
P48跳过
继承中 先调用父类构造函数,再调用子类构造函数,析构顺序与构造相反
问题:当子类与父类出现同名的成员,如何通过子类对象,访问到子类或父类中同名的数据呢?
- 访问子类同名成员 直接访问即可
- 访问父类同名成员 需要加作用域
当子类与父类拥有同名的成员函数,子类会隐藏父类中所有版本的同名成员函数,如果想访问父类中被隐藏的同名成员函数,需要加父类的作用域
s.func();//子类func函数 s.Base::m_A;//子类访问父类同名属性 s.Base::func();//父类func函数 s.Base::func(10);//父类func重载函数
总结:
6.6 继承同名静态成员处理方式
- 子类对象可以直接访问到子类中同名成员
- 子类对象加作用域可以访问到父类同名成员
- 当子类与父类拥有同名的成员函数,子类会隐藏父类中同名成员函数,加作用域可以访问到父类中同名函数
问题:继承中同名的静态成员在子类对象上如何进行访问?
同名静态成员处理方式和非静态处理方式一样,只不过有两种访问的方式(通过对象 和 通过类名)
- 访问子类同名成员 直接访问即可
- 访问父类同名成员 需要加作用域
通过对象访问同名静态函数:
Son s; s.func(); s.Base::func();
通过类名访问同名静态函数:出现同名,子类会隐藏掉父类中所有同名成员函数,需要加作作用域访问
Son::func(); Son::Base::func(); Son::Base::func(100);
通过对象访问同名静态属性:
Son s; cout << "子类的m_A = " << s.m_A << endl; cout << "父类的m_A = " << s.Base::m_A << endl;
通过类名访问同名静态属性:
cout << "子类的m_A = " << Son::m_A << endl; cout << "父类的m_A = " << Son::Base::m_A << endl;
第一个::表示通过类名方式访问,第二个::表示访问父类作用域下,类似Son::(Base::m_A)
6.7 多继承语法C++允许一个类继承多个类
语法: class 子类 :继承方式 父类1 , 继承方式 父类2...
class Son : public Base2, public Base1
{
};
C++实际开发中不建议用多继承
多继承容易产生成员同名的情况
通过使用类名作用域可以区分调用哪一个基类的成员
Son s; cout << s.Base1::m_A << endl; cout << s.Base2::m_A << endl;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 {};
现在数据都是28了,虚继承之后数据只有1个了,先改为18,再改为28,甚至可以直接通过对象访问
多态分为两类:
- 静态多态: 函数重载 和 运算符重载属于静态多态,复用函数名
- 动态多态: 派生类和虚函数实现运行时多态
静态多态和动态多态区别:
- 静态多态的函数地址早绑定 - 编译阶段确定函数地址
- 动态多态的函数地址晚绑定 - 运行阶段确定函数地址
class animal
{
public :
void speak()
{
cout << "动物在说话" << endl;
}
};
class cat :public animal
{
public:
void speak()
{
cout << "喵~" << endl;
}
};
//执行说话的函数
void doSpeak(animal & dongwu)
{
dongwu.speak();
}
void main()
{
cat mao;
doSpeak(mao);//动物在说话
//相当于animal & dongwu=mao
//父类的引用指向子类的对象
//c++中允许父子之间的类型转换,不需要做强制类型转换
//父类的指针或引用可以直接指向子类对象
}
地址早绑定:在编译阶段确定函数地址,doSpeak()函数无论传什么都会走animal里面的成员函数
如果想要执行cat类的函数,函数地址不能提前绑定,需要在运行阶段绑定,需要地址晚绑定
class Animal
{
public:
//Speak函数就是虚函数
virtual void speak()
{
cout << "动物在说话" << endl;
}
};
函数前面加上virtual关键字,变成虚函数,那么编译器在编译的时候就不能确定函数调用了
执行结果为:
动态多态满足条件:
- 1.有继承关系
- 2.子类重写父类的虚函数(父类有virtual,子类virtual可写可不写)
- 3.重写:函数返回值类型、函数名、参数列表完全相同
动态多态的使用:父类的指针或引用,指向父子类对象animal &an=cat
7.2多态的深入剖析class Animal
{
public:
void speak()
{
cout << "动物在说话" << endl;
}
};
此时为空类,大小为1
class Animal
{
public:
virtual void speak()
{
cout << "动物在说话" << endl;
}
};
此时大小为4字节,此时是指针
vfptr:virtual function pointer虚函数指针/表
指针指向虚函数表(vftable),表内记录虚函数的地址&Animal::speak(成员函数的函数地址要加作用域)
class Cat :public Animal
{
public:
};
当没有重写只有继承时:
class cat :public animal
{
public:
virtual void speak()
{
cout << "喵~" << endl;
}
};
当子类重写父类的虚函数时:
子类中的虚函数表内部会替换成子类的虚函数地址
父类中的虚函数表指向不变
当父类的指针或引用指向子类对象时,发生多态:他会从cat的虚函数表中寻找函数,在运行阶段发生动态多态是哪个对象就走哪个虚函数表
animal &ani=cat; ani.speak();7.3 多态案例:计算机类
分别利用普通写法和多态技术,设计实现两个操作数进行运算的计算器类
多态的优点:
- 代码组织结构清晰
- 可读性强
- 利于前期和后期的扩展以及维护
常规实现:
//普通实现
class Calculator {
public:
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;
}
//如果要提供新的运算,需要修改源码
}
public:
int m_Num1;
int m_Num2;
};
void test01()
{
//普通实现测试
Calculator c;
c.m_Num1 = 10;
c.m_Num2 = 10;
cout << c.m_Num1 << " + " << c.m_Num2 << " = " << c.getResult("+") << endl;
cout << c.m_Num1 << " - " << c.m_Num2 << " = " << c.getResult("-") << endl;
cout << c.m_Num1 << " * " << c.m_Num2 << " = " << c.getResult("*") << endl;
}
多态实现:
//多态实现
//抽象计算器类
//多态优点:代码组织结构清晰,可读性强,利于前期和后期的扩展以及维护
class AbstractCalculator
{
public:
virtual int getResult()
{
return 0;
}
int m_Num1;
int m_Num2;
};
//加法计算器
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 main()
{
//创建加法计算器
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;
//创建乘法计算器
abc = new MulCalculator;
abc->m_Num1 = 10;
abc->m_Num2 = 10;
cout << abc->m_Num1 << " * " << abc->m_Num2 << " = " << abc->getResult() << endl;
delete abc;
}
销毁释放堆区数据,指针指向没有变
提倡用多态,虽然代码量大
7.4 纯虚函数和抽象类在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容,因此可以将虚函数改为纯虚函数
纯虚函数语法:virtual 返回值类型 函数名(参数列表)=0;
当类中有了纯虚函数,这个类也称为抽象类
抽象类特点:
- 只要有一个纯虚函数,这个类称为抽象类
- 无法实例化对象
- 抽象类的子列必须重写纯虚函数,否则也属于抽象类
class drink {
public:
void step1()
{
cout<<"煮水" << endl;
}
virtual void step2() = 0;
void step3()
{
cout << "倒入杯中" << endl;
}
virtual void step4() = 0;
void step()
{
step1();
step2();
step3();
step4();
}
};
class coffee :public drink
{
virtual void step2()
{
cout << "冲泡咖啡" << endl;
}
virtual void step4()
{
cout << "加糖和牛奶" << endl;
}
};
class tea :public drink
{
virtual void step2()
{
cout << "冲泡茶叶" << endl;
}
virtual void step4()
{
cout << "加柠檬" << endl;
}
};
void dodrink(drink * dr)
{
dr->step();//制作
delete dr;
}
void main()
{
cout<<"煮茶步骤" << endl;
dodrink(new tea);
cout << "煮咖啡步骤" << endl;
dodrink(new coffee);
}
7.5 虚析构和纯虚析构
class Animal
{
public:
Animal()
{
cout<<"Animal构造函数" << endl;
}
~Animal()
{
cout << "Animal析构函数" << endl;
}
virtual void speak() = 0;
};
class Cat :public Animal
{
public :
Cat()
{
cout << "Cat构造函数" << endl;
}
~Cat()
{
cout << "Cat析构函数" << endl;
}
virtual void speak()
{
cout<<"喵~" << endl;
}
};
void main()
{
Animal* animal = new Cat;
animal->speak();
delete animal;
}
应该先释放子类,后释放父类,但是没有。
多态使用时,如果子类中有属性开辟到堆区,那么父类指针在析构释放时无法调用到子类的析构函数,导致子类如果有堆区属性,出现内存泄漏
解决方式:将父类中的析构函数改为虚析构或者纯虚析构
虚析构:
virtual ~Animal()
{
cout << "Animal析构函数" << endl;
}
纯虚析构:需要声明,也需要代码实现(与纯虚函数(不需要代码实现)不同),有了纯虚析构之后,这个类也属于抽象类,无法实例化对象
class Animal
{
public:
Animal()
{
cout<<"Animal构造函数" << endl;
}
virtual ~Animal() = 0;
virtual void speak() = 0;
};
Animal ::~Animal()
{
cout << "Animal纯虚析构函数" << endl;
}
虚析构和纯虚析构共性:
- 可以解决父类指针释放子类对象
- 都需要有具体的函数实现
虚析构和纯虚析构区别:
- 如果是纯虚析构,该类属于抽象类,无法实例化对象
虚析构语法:
virtual ~类名(){}
纯虚析构语法:
virtual ~类名() = 0;
类名::~类名(){}
8. struct和class区别C++中 struct和class唯一的区别就在于 默认的访问权限不同
9.文件操作
- struct 默认权限为公共
- class 默认权限为私有
程序运行时产生的数据都属于临时数据,程序一旦运行结束都会被释放,通过文件可以将数据持久化
C++中对文件操作需要包含头文件 < fstream >
文件类型分为两种:
- 文本文件:文件以文本的ASCII码形式存储在计算机中
- 二进制文件 :文件以文本的二进制形式存储在计算机中,用户一般不能直接读懂它们
操作文件的三大类:
9.1文本文件 9.1.1写文件
- ofstream:写操作
- ifstream: 读操作
- fstream : 读写操作
写文件步骤如下:
- 包含头文件:#include
- 创建流对象:ofstream ofs;
- 打开文件:ofs.open("文件路径",打开方式);见下图
- 写数据:ofs << "写入的数据";
- 关闭文件:ofs.close();
文件打开方式:
文件打开方式可以配合使用,用|操作符
例如:用二进制方式写文件 ios::binary | ios:: out
#includevoid main() { ofstream ofs; ofs.open("51.txt",ios::out); ofs << "20220718"< 文件存储位置:
总结:
9.1.2读文件
- 文件操作必须包含头文件 fstream
- 读文件可以利用 ofstream ,或者fstream类
- 打开文件时候需要指定操作文件的路径,以及打开方式
- 利用<<可以向文件中写数据
- 操作完毕,要关闭文件
读文件步骤如下:
- 包含头文件:#include
- 创建流对象:ifstream ifs;
- 打开文件并判断文件是否打开成功:ifs.open("文件路径",打开方式);
- 读数据: 四种方式读取
- 关闭文件:ifs.close();
四种读取方式:
第一种方式:
char buf[1024] = { 0 };//字符数组,全为0 while (ifs >> buf)//把ifs中的数据全放入(>>)数组中 { cout << buf << endl; }第二种方式:
char buf[1024] = { 0 }; while (ifs.getline(buf,sizeof(buf))) //getline成员函数获取一行,getline(数据放入的地址,最多读取的字节数) { cout << buf << endl; }第三种方式:
string buf;//把文件读入字符串里面 while (getline(ifs, buf)) //getline(基础输入流, 准备好的字符串) { cout << buf << endl; }第四种方式:不推荐
char c; while ((c = ifs.get()) != EOF)//get函数,每次只读一个字节,放入c里面,没有读到文件尾EOF,输出字符 { cout << c; }9.2二进制文件以二进制的方式对文件进行读写操作
9.2.1写文件
打开方式要指定为 ios::binary
除了内置数据类型,还能操作自定义数据类型通过流对象操作文件
写文件:通过输出流对象ostream,调用write函数函数原型:ostream& write(const char * buffer,int len);
参数解释:字符指针buffer指向内存中一段存储空间,写入的数据地址。len是读写的字节数#include#include class Person { public: char m_Name[64];//string类型可能会报错 int m_Age; }; //二进制文件 写文件 void main() { //1、包含头文件 //2、创建输出流对象 ofstream ofs("person.txt", ios::out | ios::binary);//构造函数,构造时进行步骤3 //3、打开文件 //ofs.open("person.txt", ios::out | ios::binary); //4、写文件 Person p = {"张三" , 18}; ofs.write((const char *)&p, sizeof(p)); //5、关闭文件 ofs.close(); } 重点:把数据地址转换为const char *
9.2.2读文件二进制方式读文件主要利用流对象调用成员函数read
函数原型:istream& read(char *buffer,int len);
参数解释:字符指针buffer指向内存中一段存储空间。len是读写的字节数#include#include class Person { public://成员属性顺序和读的文件要一致 char m_Name[64]; int m_Age; }; void main() { //2.创建流对象 ifstream ifs; //3.打开文件,判断文件是否打开成功 ifs.open("person.txt", ios::in | ios::binary); if (!ifs.is_open()) { cout << "文件打开失败" << endl; } //4.读文件 Person p; ifs.read((char*)&p, sizeof(p)); cout << "姓名: " << p.m_Name << " 年龄: " << p.m_Age << endl; ifs.close(); } 1.包含头文件
2.创建流对象
3.打开文件,判断文件是否打开成功
4.读文件
5.关闭文件文件输入流对象 可以通过read函数,以二进制方式读数据



