栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > C/C++/C#

C++:3类和对象、文件操作

C/C++/C# 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

C++:3类和对象、文件操作

目录
  • 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++认为万事万物都皆为对象,对象上有其属性和行为

1.封装
  • 将属性和行为作为一个整体,表现生活中的事物
  • 将属性和行为加以权限控制

语法: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
  • 函数名称与类名相同
  • 构造函数可以有参数,因此可以发生重载
  • 程序在调用对象时候会自动调用构造,无须手动调用,而且只会调用一次

析构函数语法: ~类名(){}

  • 析构函数,没有返回值也不写void
  • 函数名称与类名相同,在名称前加上符号 ~
  • 析构函数不可以有参数,因此不可以发生重载
  • 程序在对象销毁前会自动调用析构,无须手动调用,而且只会调用一次
2.1构造函数的分类
  • 按参数分为: 有参构造和无参构造
  • 按类型分为: 普通构造和拷贝构造

拷贝构造函数语法

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三个变量,构造函数:

初始化列表:注意冒号位置

2.7类作为成员

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,说明非静态成员变量属于类的对象上,同理可验证静态成员变量不属于类的对象上

3.2 this指针

P115
this指针指向被调用的成员函数所属的对象

this指针的用途:

  • 当形参和成员变量同名时,可用this指针来区分
  • 在类的非静态成员函数中返回对象本身,可使用return *this

  • 引用方式返回的是本身,值的方式返回的是复制的另一个对象
  • 引用指向本身内存,不用引用就是拷贝了,而拷贝指向另一个内存,不加引用的话返回的就不是p2了,而是别的Person 对象(这个对象值会一直加,最后值是40)
  • 如果不用引用的方式返回,相当于返回与p2不同的另一个Person(只是age都是20),那么后续的加年龄操作与p2就没有关系了
  • this指向p2的指针,而*this指向的就是p2这个对象本体,返回本体要用引用+*this
3.3空指针访问成员函数

类:

调用:

空指针调用成员函数:第一个可以运行,第二个函数程序出错

报错原因:传入的指针里面为空,第二个函数调用了成员属性,可以修改为:为空就直接返回,

3.4 const修饰成员函数

常函数:就是 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和分号即可

4.3 成员函数做友元

在被访问的类的首行: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.继承

定义这些类时,下级别的成员除了拥有上一级的共性,还有自己的特性。
这个时候我们就可以考虑利用继承的技术,减少重复代码

6.1 继承的基本语法

语法:class 子类 : 继承方式 父类
子类:派生类
父类:基类

//父类
class BasePage
{
public:
	void header()
	{
	}
};

//子类
class SpecialPage: public BasePage
{
public:
	void content()
	{
	}
};

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

  • 一类是从基类继承过来的,一类是自己增加的成员
  • 从基类继承过过来的表现其共性,而新增的成员体现了其个性
6.2 继承方式

继承方式一共有三种:

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


如图:
父类:private——>子类什么情况都无法访问,此外:

  • 公共继承public:继承后访问权限不变
  • 保护继承protected:都变为保护权限protected
  • 私有继承private:都变为私有权限private
6.3 继承中的对象模型
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属性

6.4 继承中构造和析构顺序

P48跳过
继承中 先调用父类构造函数,再调用子类构造函数,析构顺序与构造相反

6.5 继承同名成员处理方式

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

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

当子类与父类拥有同名的成员函数,子类会隐藏父类中所有版本的同名成员函数,如果想访问父类中被隐藏的同名成员函数,需要加父类的作用域

	s.func();//子类func函数
	s.Base::m_A;//子类访问父类同名属性
	s.Base::func();//父类func函数
	s.Base::func(10);//父类func重载函数

总结:

  1. 子类对象可以直接访问到子类中同名成员
  2. 子类对象加作用域可以访问到父类同名成员
  3. 当子类与父类拥有同名的成员函数,子类会隐藏父类中同名成员函数,加作用域可以访问到父类中同名函数
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,甚至可以直接通过对象访问

7.多态 7.1多态的基本语法

多态分为两类:

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

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

  • 静态多态的函数地址早绑定 - 编译阶段确定函数地址
  • 动态多态的函数地址晚绑定 - 运行阶段确定函数地址
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唯一的区别就在于 默认的访问权限不同

  • struct 默认权限为公共
  • class 默认权限为私有

9.文件操作

程序运行时产生的数据都属于临时数据,程序一旦运行结束都会被释放,通过文件可以将数据持久化
C++中对文件操作需要包含头文件 < fstream >

文件类型分为两种:

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

操作文件的三大类:

  1. ofstream:写操作
  2. ifstream: 读操作
  3. fstream : 读写操作
9.1文本文件 9.1.1写文件

写文件步骤如下:

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

文件打开方式:

文件打开方式可以配合使用,用|操作符
例如:用二进制方式写文件 ios::binary | ios:: out

#include 
void main()
{
	ofstream ofs;
	ofs.open("51.txt",ios::out);
	ofs << "20220718"< 

文件存储位置:

总结:

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

读文件步骤如下:

  1. 包含头文件:#include
  2. 创建流对象:ifstream ifs;
  3. 打开文件并判断文件是否打开成功:ifs.open("文件路径",打开方式);
  4. 读数据: 四种方式读取
  5. 关闭文件: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二进制文件

以二进制的方式对文件进行读写操作
打开方式要指定为 ios::binary
除了内置数据类型,还能操作自定义数据类型

9.2.1写文件

通过流对象操作文件
写文件:通过输出流对象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函数,以二进制方式读数据

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/1019308.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号