【完结】自学C鸭鸭笔记(三)——C++核心编程

课程源自B站:https://www.bilibili.com/video/BV1et411b73Z

C++核心编程

1 内存分区模型

C++程序在执行时,将内存大方向划分为4个区域

  • 代码区:存放函数的二进制代码,由操作系统进行管理
  • 全局区:存放全局变量、静态变量以及常量
  • 栈区:由编译器自动分配释放,存放函数的参数值,局部变量等
  • 堆区:由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收

内存四区的意义:
不同的区域存放的数据,赋予不同的生命周期,给我们更大的灵活编程。

1.1 程序运行前

在程序编译后,形成了exe可执行程序,未执行该程序前分为两个区域。

代码区

  • 存放cpu执行的机器指令(二进制)
  • 代码区是共享的,共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可
  • 代码区是只读的,使其只读的原因是防止程序意外的修改了他的指令

全局区

  • 全局变量静态变量存放在此
  • 全局区还包含了常量区,字符串常量其他常量也存放在此
    (其他常量:const修饰的就是其他常量)

该区域的数据在程序结束后由操作系统释放

名词解释

  • 局部变量:在函数体内部的变量,比如之前在笔记(一)、(二)中的所写的变量都是局部变量。
  • 全局变量:在函数体int main() 之外的变量。[全局区]
  • 静态变量:在普通变量之前加static,就变成了静态变量。[全局区]
  • 常量(分为两种类型)
    • 字符串常量:双引号修饰的。[全局区]
    • const修饰的常量(又分为两种)
      • const修饰的全局常量:在全局常量前加const [全局区]
      • const修饰的局部常量:在局部常量前加const

在这里插入图片描述

1.2 程序运行后

栈区

  • 由编译器自动分配释放,存放函数的参数值、局部变量等。
  • 注意事项:不要返回局部变量的地址,栈区开辟的地址由编译器自动释放。

程序示例:

#include<iostream>
using namespace std;

//栈区数据注意事项 —— 不要返回局部变量的地址
//栈区的数据由编译器管理开辟和释放

int * function()
{
	int a = 10; //a是一个局部变量  存放在栈区,栈区的数据在函数执行完毕后自动释放
	return &a;  //返回一个局部变量的地址
}

int main()
{
	int * p = function();

	cout << *p << endl;  //第一次可以打印正确的数字,是因为编译器做了保留
	cout << *p << endl;  //第二次这个数据就不保留了

	system("pause");

	return 0;
}

程序运行结果:
在这里插入图片描述

总结:因为栈区存放的数据在函数执行完毕之后就会被编译器自动释放,因此不能返回局部变量的地址。(这里第一次可以输出正确数据的原因是,编译器照顾你这个不懂的傻子,但是只给你一次机会/狗头)

堆区

  • 由程序员分配释放,若程序员不释放,程序结束时由操作系统回收
  • 在C++中主要利用new在堆区开辟内存

示例:

#include<iostream>
using namespace std;

int * function()
{
	//利用new关键字,在堆区开辟区域	 存放数据
	int * p = new int(10);  // 利用new关键字,指定类型,输入数据值,系统自己分配地址,然后定义一个指针接收
	//这个指针本质上是局部变量,放在栈区,指向的是堆区的地址,只不过在马上死亡的时候将堆区的地址传给了主函数
	return p;  //返回这个指针  
}

int main()
{
	int * p = function();
	cout << *p << endl;
	cout << *p << endl;
	cout << *p << endl;  //可以正常打印
	cout << *p << endl;

	system("pause");
	return 0;
}

类比上一个程序,这个可以打印,因为10这个数据存放在堆区。

1.3 new操作符

  • C++中利用new操作符在堆区中开辟数据
  • 堆区开辟的数据,由程序员手动开辟,手动释放,释放利用操作符delete
  • 语法:new 数据类型
  • 利用new创建的数据,会返回该数据对应的类型的指针

示例

#include<iostream>
using namespace std;

//1.new的基本语法
//用new在堆区开辟数据存放地址,用delete释放

int * func()
{
	int *p = new int(10);//new定义的数据类型返回的的是 该数据类型的指针
	return p;
}

void test01()
{
	int * p = func();
	cout << *p << endl;
	cout << *p << endl;

	//想要释放堆区里的数据,利用delete关键字
	delete p;//释放数据的地址,之后就不能再访问这块地址了
}

//2.在堆区利用new开辟数组
void test02()
{
	//在堆区开辟一个整型数组
	int * p = new int[10]; //开辟数组要用中括号,括号里表示数组的容量,返回的是数组的首地址,用指针接收
	
	//给数组赋值,方法和普通数组一样
	for (int i = 0; i < 10; i++)
	{
		p[i] = i + 1;
	}

	//读取数组的值,方法和普通数组一样
	for (int i = 0; i < 10; i++)
	{
		cout << p[i] << endl;
	}
}
int main()
{
	test01();

	cout << "test02的结果: " << endl;
	test02();


	system("pause");
	return 0;
}

程序运行结果:
在这里插入图片描述

2 引用

2.1 引用的基本使用

作用:给变量起别名。
语法数据类型 &别名 = 原名

示例

在这里插入代码片/*********************************************************
课程名称:引用的基本使用
**********************************************************/
#include<iostream>
using namespace std;


int main()
{
	//创建变量
	int a = 10;
	//创建引用
	int &b = a;

	cout << "a= " << a << endl;
	cout << "b= " << b << endl;

	b = 100;

	cout << "a= " << a << endl;
	cout << "b= " << b << endl;


	system("pause");
	return 0;
}

程序运行结果:
在这里插入图片描述

总结:可以把引用理解为给变量起小名,对原名和小名进行操作都会影响到数值。

2.2 引用的注意事项

  • 引用必须初始化(现有大名,才能再起小名)
  • 引用在初始化之后就不应该做出改变(小名给一个人用过之后,就不能再给别人用了)

2.3 引用做函数参数

作用:函数传参时,可以利用引用的技术让形参修饰实参
优点:可以简化指针修改实参

在这里插入图片描述
示例:

/*********************************************************
课程名称:引用做函数参数
**********************************************************/
#include<iostream>
using namespace std;

void Swap(int &a, int &b)
{
	int temp = a;
	a = b;
	b = temp;
}

int main()
{
	int A = 10;
	int B = 20;

	Swap(A, B);

	cout << "A= " << A << endl;
	cout << "B= " << B << endl;

	system("pause");
	return 0;
}

程序运行结果:
在这里插入图片描述

总结:虽然不是地址传递,也可以实现对实参进行修改。这样可以不用指针,更加方便。

2.4 引用做函数的返回值

作用:引用是可以作为函数返回值存在的。

注意:不要返回局部变量的引用。

用法:函数调用作为左值
注:所谓的左值,就是等号左边的值,就是可以被赋值的值。

示例:

/*********************************************************
课程名称:引用做函数的返回值
**********************************************************/
#include<iostream>
using namespace std;
//1、不要返回局部变量的引用
int& test01()    //在返回值类型后面加一个&,就代表返回的是引用
{
	int a = 10;
	return a;
}

//2.做返回值的引用是可以修改的左值
int & test02()
{
	static int b = 20;    //静态变量储存在全局区,所以子函数运行完不会被释放,可以返回其引用值
	return b;
}
int main()
{
	cout << "test01运行结果: " << endl;
	int & ref = test01();              //创建一个引用来接收子函数的值
	cout << "ref= " << ref << endl;    //第一次正常是因为编译器做了保留
	cout << "ref= " << ref << endl;    //因为局部变量在栈区,子函数运行完之后变量被释放

	cout << endl << "test02运行结果: " << endl;
	int & ref02 = test02();
	cout << "ref02= " << ref02 << endl;
	cout << "ref02= " << ref02 << endl;
	cout << "ref02= " << ref02 << endl;

	test02() = 100;    //返回类型为引用的子函数可以作为左值,可以被修改

	cout << "ref02= " << ref02 << endl;
	cout << "ref02= " << ref02 << endl;

	system("pause");
	return 0;
} 

程序运行结果:
在这里插入图片描述

2.5 引用的本质

本质:引用的本质在C++内部实现是一个指针常量。
知识回忆:指针常量:指针的指向不能变,但是指针指向的值可以改变。

程序运行的根本步骤
当你书写一段:

int a=10;
int& ref=a;

的时候,相当于编译器自动写了一段代码:

int * const ref=&a;

而当你对ref进行操作的时候:

ref=100

系统识别到是引用,自动转换成了:

*ref=100

相当于自动解引用。这一点在引用做函数参数的时候也是相同的,其实还是用指针来进行接受传入实参的地址,本质还是地址传递。

同时,联系之前2.2所学习的,引用只能作为一个人的小名,因为引用是一个指针常量,所以该指针的指向不能改变,因此只能成为一个人的小名。

下面还有一段代码,加深理解:

//发现是引用,转换为 int* const ref = &a;
void func(int& ref){
	ref = 100; // ref是引用,转换为*ref = 100
}
int main(){
	int a = 10;
    
    //自动转换为 int* const ref = &a; 指针常量是指针指向不可改,也说明为什么引用不可更改
	int& ref = a; 
	ref = 20; //内部发现ref是引用,自动帮我们转换为: *ref = 20;
    
	cout << "a:" << a << endl;
	cout << "ref:" << ref << endl;
    
	func(a);
	return 0;
}

结论:C++推荐用引用技术,因为语法方便,引用本质是指针常量,但是所有的指针操作编译器都帮我们做了

2.6 常量引用

作用:常量引用主要来修饰形参,防止误操作
语法:void Func(const int & ref);

如果在传入的变量前加一个const,在子函数内部就不能对传入的值进行修改,换句话说:本来是地址传递,现在变成了值传递。

深入理解:上一节说到引用是一个指针常量,相当于int * const p,再加一个const,相当于const int * const p,指针的指向和指针指向的内容均不能修改。

示例:

//引用使用的场景,通常用来修饰形参
void showValue(const int& v)
{
	//v += 10;
	cout << v << endl;
}

int main() {

	//int& ref = 10;  引用本身需要一个合法的内存空间,因此这行错误
	//加入const就可以了,编译器优化代码,int temp = 10; const int& ref = temp;
	const int& ref = 10;

	//ref = 100;  //加入const后不可以修改变量
	cout << ref << endl;

	//函数中利用常量引用防止误操作修改实参
	int a = 10;
	showValue(a);

	system("pause");

	return 0;
}

3 函数高级

3.1 函数默认参数

在C++中,自定义函数的时候,可以一上来给形参一个默认的值。
语法:返回值类型 函数名 (参数=默认值){ }

  • 如果自己传入数据,就用自己的数据,如果没有传入数据,就用默认值

示例:

/*********************************************************
课程名称:函数默认参数
**********************************************************/
#include<iostream>
using namespace std;

int Add(int a, int b=10, int c=20)
{
	return a + b + c;
}
int main()
{
	cout << Add(10, 20) << endl;  //有默认参数的情况下,不一定要给该参数赋值,比如说这里的c
	                              //有自己赋值的情况下,优先用自己的赋值而不是默认值,比如说这里的b
	system("pause");
	return 0;
} 

程序运行结果:
在这里插入图片描述
注意事项:

  1. 如果某个位置已经有了默认参数,那么从这个位置往后,从左到右都必须有默认值
    在这里插入图片描述
    如图,b有默认值,而右边的c和d就没有默认值,因此会爆红线警告。
  2. 如果函数在声明的时候给出了默认参数,则在函数实现的时候就不能再给出(重定义)默认参数了。
    在这里插入图片描述

3.2 函数占位参数

C++的形参列表里可以有占位参数,用来做占位,调用函数时必须补齐该位置

语法:返回值类型 函数名 (数据类型){}
注意:在形参列表里,只写数据类型而不写数据名

现阶段函数占位参数存在意义不大,后面的课程中会用到该技术

Tips:占位参数也可以有默认参数,如:
void func(int = 10){}

3.3 函数重载

3.3.1 函数重载概述

作用:函数名可以相同,提高复用性

函数重载满足条件

  • 同一个作用域下(现阶段写的代码都在全局作用域下)
  • 函数名称相同
  • 函数参数类型不同,或者个数不同,或者顺序不同

注意:函数的返回值不能作为函数重载的条件

示例

/*********************************************************
课程名称:函数重载
**********************************************************/
#include<iostream>
using namespace std;

//函数重载的三个条件
//1.在同一区域
//2.函数名称相同
//3.形参的 类型不同 或 顺序不同 或 数量不同

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

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

void func(int a,int b)
{
	cout << "void func(int a,int b)" << endl;
}

void func(int a, double b)
{
	cout << "void func(int a, double b)" << endl;
}

void func(double a, int b)
{
	cout << "void func(double a, int b)" << endl;
}

int main()
{
	func();
	func(10);
	func(10.10);
	func(10.3, 14);
	func(10, 3.14);

	system("pause");
	return 0;
} 

程序运行结果:
在这里插入图片描述

由运行结果可知,在函数名相同的情况下,只要形参不一样,编译器就能区分出你到底想要调用哪个函数,如果形参一样的话,那么编译器就没有办法分辨了。

3.3.1 函数重载注意事项

  • 引用作为函数重载的条件

示例

/*********************************************************
课程名称:函数重载的注意事项
          引用作为函数重载的条件
**********************************************************/
#include<iostream>
using namespace std;

//引用作为函数重载的条件
//引用值的不同也可以满足函数重载的条件

void func(int &a)
{
	cout << "调用func(int &a)" << endl;
}

void func(const int &a)
{
	cout << "调用func(const int &a)" << endl;
}

int main()
{
	int a = 10;
	func(a);   //调用第一个函数

	func(10);  //调用第二个函数

	system("pause");
	return 0;
} 

程序运行结果:
在这里插入图片描述

  • 函数重载碰到函数默认参数
    在这里插入图片描述

当我们写了上图的参数之后,用到了参数的默认值,语法上是满足函数重载的条件的,但是当调用这个函数时:
在这里插入图片描述
编译器就不知道具体用哪一个函数了,所以应该避免

4 类和对象

C++面向对象的三大特性为:封装、继承和多态

C++认为万事万物皆为对象,对象上有其属性和行为

例如:

​人可以作为对象,属性有姓名、年龄、身高、体重…,行为有走、跑、跳、吃饭、唱歌…
​车也可以作为对象,属性有轮胎、方向盘、车灯…,行为有载人、放音乐、放空调…

​ 具有相同性质的对象,我们可以抽象称为,人属于人类,车属于车类

4.1 封装

4.1.1 封装的意义

封装是C++面向对象的三大特性之一

封装的意义

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

在设计类的时候,属性和行为写在一起,表现事物

语法: class 类名{ 访问权限: 属性 / 行为 };

示例一:写一个“圆”类,求圆的周长

/*********************************************************
课程名称:封装的意义
代码演示:设计一个圆类,求圆的周长
制作人:Wu_XZ
**********************************************************/
#include<iostream>
using namespace std;

#define pi 3.14159

class Circle    //class+类名
{
	//访问权限
public:  //公共访问权限

	//属性
	int Cir_r;

	//行为:给出圆的周长,这里用一个函数
	double Cir_ZC()
	{
		return 2 * pi*Cir_r;
	}
};

int main()
{
	Circle c1;   //类似结构体,把Circle看作是一个变量类型

	c1.Cir_r = 10;   //给半径赋值,跟结构体调用数据的语法一样

	cout << "圆的周长为: " << c1.Cir_ZC() << endl;   //这里注意c1.Cir_ZC()加括号


	system("pause");
	return 0;
} 

程序运行结果:
在这里插入图片描述
**示例2:**设计一个学生类,属性有姓名和学号,可以给姓名和学号赋值,可以显示学生的姓名和学号

/*********************************************************
课程名称:封装的意义
代码演示:设计一个学生类,属性有姓名和学号,可以给姓名和学号赋值,可以显示学生的姓名和学号
制作人:Wu_XZ
**********************************************************/
#include<iostream>
#include<string>
using namespace std;

#define pi 3.14159

class Student    //class+类名
{
	//访问权限
public:  //公共访问权限

	//属性
	string name;
	string StuID;

	//行为:给出圆的周长,这里用一个函数
	void PrintInfo()
	{
		cout << "学生的姓名: " << name << endl;
		cout << "学生的学号: " << StuID << endl;
	}
};

int main()
{
	Student s1;   //类似结构体,

	s1.name = "Yan_EB";
	s1.StuID = "201704060704";

	s1.PrintInfo();

	system("pause");
	return 0;
} 

程序运行结果:
在这里插入图片描述

4.1.1.2 几个专业术语
  • 中的属性行为,统称为成员
  • 属性又称:成员属性成员变量
  • 行为又称:成员函数成员方法
4.1.1.3 用行为来给属性赋值

上述示例二中,也可在行为中定义赋值函数,对成员变量进行赋值
示例

/*********************************************************
课程名称:封装的意义
代码演示:设计一个学生类,属性有姓名和学号,可以给姓名和学号赋值,可以显示学生的姓名和学号
制作人:Wu_XZ
**********************************************************/
#include<iostream>
#include<string>
using namespace std;

#define pi 3.14159

class Student    //class+类名
{
	//访问权限
public:  //公共访问权限

	//属性
	string name;
	string StuID;

	//行为:给出圆的周长,这里用一个函数
	void PrintInfo()
	{
		cout << "学生的姓名: " << name << endl;
		cout << "学生的学号: " << StuID << endl;
	}

	/*************创建两个赋值函数*************/
	void SetName(string n)
	{
		name = n;
	}
	void SetID(string m)
	{
		StuID = m;
	}
};

int main()
{
	Student s1;   //类似结构体,

	/*s1.name = "Yan_EB";
	s1.StuID = "201704060704";*/
	s1.SetName("Alan");         //这里改用调用函数的形式来赋值
	s1.SetID("201704060713");

	s1.PrintInfo();

	system("pause");
	return 0;
} 

程序运行结果:
在这里插入图片描述

4.1.1.4 封装的意义二

类在设计时,可以把属性和行为放在不同的权限下,加以控制

访问权限有三种:

名称关键字说明备注
公共权限public成员在类内可以访问,类外也可以访问
保护权限protected成员在类内可以访问,类外不可以访问儿子可以访问父亲的保护内容
私有权限private成员在类内可以访问,类外不可以访问儿子不可以访问父亲的隐私内容

在这里插入图片描述

4.1.2 struct和class的区别

在C++中struct和class唯一的区别就是默认的访问权限不同

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

示例:
在这里插入图片描述

由图不难看出,在不指定权限的情况下,由class定义的类是不能访问的。

4.1.3 成员属性设为私有

优点1:将所有成员属性设为私有,可以自己控制读写权限
优点2:对于写权限,我们可以检测数据的有效性

示例

/*********************************************************
课程名称:成员属性设置为私有
代码演示:1.好处一:可以自己控制读写权限
制作人:Wu_XZ
**********************************************************/
#include<iostream>
#include<string>
using namespace std;


class Person
{
private:
	string Name;           //想要:可读可写
	int Age;               //想要:只读不能写
	string Lover;          //想要:只写不能读
	//这些属性都是私有属性,不可以被外部访问
	//为了实现上面的需求,我们可以操作子函数放在public中
public:
	//读写姓名
	void setName(string name)
	{
		Name = name;
	}
	string getName()
	{
		return Name;
	}

	//只读年龄
	int getAge()
	{
		Age = 0;  //给年龄一个默认值
		return Age;
	}

	//只写情人
	void serLover(string Lname)
	{
		Lover = Lname;
	}


};


int main()
{
	Person p;
	//写读姓名
	p.setName("张三");
	cout << "姓名: " << p.getName() << endl;
	//读年龄
	cout << "年龄: " << p.getAge() << endl;
	//写情人
	p.serLover("苍井");


	system("pause");
	return 0;
} 

程序运行结果:
在这里插入图片描述
下面演示优点2,要求是:可以设置年龄,但是年龄必须要在0~150岁才可视为有效。
示例

/*********************************************************
课程名称:成员属性设置为私有
代码演示:1.好处二:可以控制数据的有效性
制作人:Wu_XZ
**********************************************************/
#include<iostream>
#include<string>
using namespace std;


class Person
{
private:
	string Name;           //想要:可读可写
	int Age;               //想要:可以读,可以写,但是年龄在0-150岁才视为有效
	string Lover;          //想要:只写不能读
	//这些属性都是私有属性,不可以被外部访问
	//为了实现上面的需求,我们可以操作子函数放在public中
public:
	//读写姓名
	void setName(string name)
	{
		Name = name;
	}
	string getName()
	{
		return Name;
	}

	//读年龄,写年龄需要有效
	void setAge(int age)
	{
		if (age <= 0 || age > 150)
		{
			cout << "你可真是个小精灵鬼儿!重输!" << endl;
			return;
		}
		Age = age;
	}
	int getAge()
	{

		return Age;
	}

	//只写情人
	void serLover(string Lname)
	{
		Lover = Lname;
	}
};


int main()
{
	Person p;
	//写读姓名
	p.setName("张三");
	cout << "姓名: " << p.getName() << endl;
	//写
	p.setAge(1000);
	//写情人
	p.serLover("苍井");


	system("pause");
	return 0;
} 

程序运行结果:
在这里插入图片描述

4.1.4 封装案例

4.1.4.1 练习案例1:设计立方体类

要求
设计立方体类(Cube)
求出立方体的面积和体积
分别用全局函数和成员函数判断两个立方体是否相等。
示例:

/*********************************************************
课程名称:封装练习题
代码演示:设计立方体类
制作人:Wu_XZ
**********************************************************/
#include<iostream>
#include<string>
using namespace std;

class Cube
{
private:
	int Len;
	int Wid;
	int Hei;

public:
	//定义长宽高
	void setLen(int l)
	{
		Len = l;
	}

	void setWid(int w)
	{
		Wid= w;
	}

	void setHei(int h)
	{
		Hei = h;
	}

	//得到长宽高
	int getLen()
	{
		return Len;
	}

	int getWid()
	{
		return Wid;
	}

	int getHei()
	{
		return Hei;
	}
	//计算面积
	int calS()
	{
		return (Wid*Len + Wid * Hei + Len * Hei) * 2;
	}
	//计算体积
	int calV()
	{
		return Len * Wid * Hei;
	}

	/****成员函数判断两个立方体是否相等****/
	bool isSame(Cube c)
	{
		if (Len == c.Len&&Wid == c.Wid&&Hei == c.Hei)
		{
			return true;
		}
		return false;
	}
};


/****全局函数判断两个立方体是否相等****/
bool isSame_Whole(Cube &c1, Cube &c2)
{
	if (c1.getHei() == c2.getHei()&&c1.getLen() == c2.getLen()&&c1.getWid() == c2.getWid())
	{
		return true;
	}
	return false;
}


int main()
{
	Cube c1;
	c1.setLen(10);
	c1.setHei(10);
	c1.setWid(10);

	cout << "立方体的面积为:" << c1.calS() << endl;
	cout << "立方体的体积为:" << c1.calV() << endl;


	Cube c2;
	c2.setLen(10);
	c2.setHei(10);
	c2.setWid(10);

	bool ref = c1.isSame(c2);

	bool ref2 = isSame_Whole(c1, c2);

	if (ref)
	{
		cout << "成员函数:两个立方体相同" << endl;
	}
	if (ref2)
	{
		cout << "全局函数:两个立方体相同" << endl;
	}


	system("pause");
	return 0;
} 

程序运行结果:
在这里插入图片描述

4.1.4.2 练习案例2:点和圆的关系

要求
设计一个圆形类(Circle),和一个点类(Point),计算点和圆的关系。
示例

/*********************************************************
课程名称:封装练习题
代码演示:设计点类和圆类 并求出点和圆之间的关系
制作人:Wu_XZ
**********************************************************/
#include<iostream>
#include<string>
using namespace std;

class Point
{
private:
	int X;
	int Y;

public:
	//设置X
	void setX(int x)
	{
		X = x;
	}
	//获取X
	int getX()
	{
		return X;
	}
	//设置Y
	void setY(int y)
	{
		Y= y;
	}
	//获取Y
	int getY()
	{
		return Y;
	}
	
};

class Circle
{
private:
	int R;
	Point Center;
public:
	//设置半径
	void setR(int r)
	{
		R = r;
	}
	//获取半径
	int getR()
	{
		return R;
	}
	//设置圆心
	void setCenter(Point cen)
	{
		Center = cen;
	}
	//获取圆心
	Point getCenter()
	{
		return Center;
	}
};

//判断点和圆关系的全局函数
void isInCircle(Circle &c, Point &d)
{
	//点到圆心的距离的平方
	int distance =
		(c.getCenter().getX() - d.getX())*(c.getCenter().getX() - d.getX()) +
		(c.getCenter().getY() - d.getY())*(c.getCenter().getY() - d.getY());
	//圆的半径的平方
	int rDistance = c.getR()*c.getR();

	//判断
	if (distance == rDistance)
	{
		cout << "点在圆上" << endl;
	}
	else if (distance > rDistance)
	{
		cout << "点在圆外" << endl;
	}
	else if (distance < rDistance)
	{
		cout << "点在圆内" << endl;
	}

}


int main()
{
	Point p1;
	Point p2;
	Point p3;

	p1.setX(10);
	p1.setY(11);
	p2.setX(10);
	p2.setY(10);
	p3.setX(10);
	p3.setY(9);

	Point cen;
	cen.setX(10);
	cen.setY(0);

	Circle c;
	c.setR(10);
	c.setCenter(cen);

	isInCircle(c, p1);
	isInCircle(c, p2);
	isInCircle(c, p3);


	system("pause");
	return 0;
} 

程序运行结果:
在这里插入图片描述

4.1.5 类的分文件编写

以上题为例,分别对Circle和Point两个类分别变成一个头文件,具体操作方法如下:

  • 步骤一
    在“外部依赖项”中新建名为Circle.h的头文件。
    在这里插入图片描述

  • 步骤二
    在该头文件中输入老套路+类Circle的成员的声明,之后每个声明后需要加补全
    在这里插入图片描述
    注意:这里Point下面飘红是因为还没有写Point的声明,而且前面要补全头文件。

  • 步骤三
    在“源文件”中添加Circle.cpp用于写函数的实现。
    在这里插入图片描述

  • 步骤四
    在Circle.cpp中首先写#include“Circle.h”,然后只保留函数的实现,同时,在setR()等函数前加Circle::告诉系统这个函数是在Circle作用域下的成员函数。
    在这里插入图片描述

  • 步骤五
    同样的方法设置Point类的份文件。

  • 步骤六
    在这里插入图片描述

4.2 对象的初始化和清理

  • 生活中我们买的电子产品都基本会有出厂设置,在某一天我们不用时候也会删除一些自己信息数据保证安全
  • C++中的面向对象来源于生活,每个对象也都会有初始设置以及 对象销毁前的清理数据的设置。

4.2.1 构造函数和析构函数

  • 构造函数:做初始化的函数。主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无须手动调用。
  • 析构函数:做清理的函数。主要作用在于对象销毁前系统自动调用,执行一些清理工作。

注意

  1. 这两个函数将会被编译器自动调用,完成对象初始化和清理工作。
  2. 如果我们不提供构造和析构,编译器会提供析构和构造函数。
  3. 此时编译器提供的构造函数和析构函数是空实现

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

  1. 构造函数,没有返回值也不写void
  2. 函数名称与类名相同
  3. 构造函数可以有参数,因此可以发生重载
  4. 程序在调用对象时候会自动调用构造,无须手动调用,而且只会调用一次

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

  1. 析构函数,没有返回值也不写void
  2. 函数名称与类名相同,在名称前加上符号 ~
  3. 析构函数不可以有参数,因此不可以发生重载
  4. 程序在对象销毁前会自动调用析构,无须手动调用,而且只会调用一次

示例

/*********************************************************
课程名称:构造函数和析构函数
代码演示:……
制作人:Wu_XZ
**********************************************************/
#include<iostream>

using namespace std;

class Person    //先建立一个类
{
public:       //这里是public才能保证从外部可以调用到
	//一、构造函数
	//1. 构造函数,没有返回值也不写void
	//2. 函数名称与类名相同
	//3. 构造函数可以有参数,因此可以发生重载
	//4. 程序在调用对象时候会自动调用构造,无须手动调用, 而且只会调用一次
	Person()       //这里先不写参数
	{
		cout << "Person 构造函数的调用 " << endl;
	}

	//二、析构函数
	//1. 析构函数,没有返回值也不写void
	//2. 函数名称与类名相同, 在名称前加上符号  ~
	//3. 析构函数不可以有参数,因此不可以发生重载
	//4. 程序在对象销毁前会自动调用析构,无须手动调用, 而且只会调用一次
	~Person()
	{
		cout << "Person 析构函数的调用" << endl;
	}
};

void test01()
{
	Person p;
}

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

程序运行结果:
在这里插入图片描述

程序分析:
1.注意在这个程序中,只是在函数test01()中定义了一个Person类型的变量,并没有进行类似p.Person()调用函数的操作,但是系统已经自己调用了。
2.同时也可以看到析构函数也被执行了,这是因为我们写的子函数是在栈区,子函数执行完毕之后就会被释放,所以在子函数被释放之前析构函数就会被执行。
3.如果将定义语句Person p;放在主函数内,则程序运行之后不会出现析构函数中的语句,而在按任意键退出程序时,能看到析构函数中的语句被执行,一闪而过。

4.2.2 构造函数的分类和调用

两种分类方式:

  • 按照参数分类::
    • 有参构造
    • 无参构造(默认构造:编译器自动提供的就是这种类型的)
  • 按照类型分类:
    • 普通构造
    • 拷贝构造
      形式:Person(const Person &p)
      作用:将一个已有的实例的属性作为这个类的初始化条件
      注意:必须用上面的这种形式,用引用的形式传参

三种调用方式:

  • 括号法

    • 默认构造函数调用
      只需要创建一个该类的实例就可以调用(这里用Person类来演示)
      Person p1;
    • 有参构造函数调用
      在创建的实例名字后面加上相应的参数
      Person p2(10);
    • 拷贝构造函数的调用
      在创建的实例后面加上要拷贝的实例名(这里要创建一个实例p3,p3拷贝p2的属性)
      Person p3(p2);
    • 注意:用括号法调用构造函数的时候,后面不能加(),比如说像下面这样:
      [错误]Person p1();
      这样编译器会认为这是一个函数的声明
  • 显示法

    • 显示法调用默认构造函数
      Person p1;
    • 显示法调用有参构造函数
      Person p2=Person(10);
    • 显示法调用拷贝构造函数
      Person p3=Person(p2);
    • 注意
      一、如果只有Person(10)这样的,也可以单独成行,这里创建的是一个匿名对象,当当前执行结束后,系统会立即回收匿名对象
      二、不要用拷贝构造函数初始化匿名对象,即Person(p2)不能单独成行,因为系统默认Person(p2)=Person p2,相当于重定义了p2
  • 隐式转换法

    • 调用有参构造函数
      Person p2=10;相当于 Person p2=Person(10);
    • 调用拷贝构造函数
      Person p3= p2;

4.2.3 拷贝构造函数调用时机

C++中拷贝构造函数调用时机有三种情况

  • 使用一个已经创建完毕的对向来初始化新对象

  • 值传递的方式给函数参数传值

  • 以值方式返回局部对象
    Person func() {Person p1; return p1}这里这个函数func()返回的并不是函数里的p1,而实新建了一个对象,并且用拷贝构造函数把新建的对向初始化了。

4.2.4 构造函数调用规则

默认情况下,c++编译器至少给一个类添加3个函数

1.默认构造函数(无参,函数体为空)
2.默认析构函数(无参,函数体为空)
3.默认拷贝构造函数,对属性进行值拷贝

构造函数调用规则如下:

  • 如果用户定义有参构造函数,c++不在提供默认无参构造,但是会提供默认拷贝构造
  • 如果用户定义拷贝构造函数,c++不会再提供其他构造函数

4.2.5 深拷贝与浅拷贝

面试经典问题

  • 浅拷贝:简单的赋值拷贝操作(编译器默认提供的就是这种)
  • 深拷贝:在堆区重新申请空间,进行拷贝工作

浅拷贝的问题:
在这里插入图片描述
总结1:浅拷贝带来的问题就是,堆区的内存重复释放
总结2:如果属性有在堆区开辟的,一定要自己提供拷贝构造函数,防止浅拷贝带来的问题。

为了解决上面的问题,需要自己写一个深拷贝构造函数
在这里插入图片描述

额外补充:析构函数的作用

  • 如果在类中开辟了堆区数据,则管理员应该在数据销毁之前释放堆区的内存,这个操作就在析构函数中。
  • 下面的例子中,m_Height为指向堆区的指针,析构函数写法如下:
    在这里插入图片描述

4.2.6 初始化列表

作用
给类中的属性进行初始化操作

语法构造函数() : 属性1(值1),属性2(值2),属性3(值3).....{}

  1. 传统方式初始化
    传统方式初始化是写一个有参构造函数,在函数体内部进行初始化操作
class Person 
{
public:
	//传统方式初始化
	Person(int a, int b, int c) 
	{
		m_A = a;
		m_B = b;
		m_C = c;
	}

private:
	int m_A;
	int m_B;
	int m_C;
};
  1. 初始化列表方式进行初始化
class Person 
{
public:

	//初始化列表方式初始化
	Person(int a, int b, int c) :m_A(a), m_B(b), m_C(c) 
	{
		//这里面什么都不需要写了
	}

private:
	int m_A;
	int m_B;
	int m_C;
};

4.2.7 类对象作为类成员

C++类中的成员可以是另一个类的对象,我们称该成员为 对象成员

这个问题可以参见4.1.4.2中点和圆的关系,在类中,定义圆心也用到了

提出问题:创建圆时,的构造与析构函数是什么样的顺序

在这里插入图片描述在这里插入图片描述
结论:现有手机,再有人,析构时按照先入后出的原则。

4.2.8 静态成员

静态成员就是在成员变量和成员函数前加上关键字static,称为静态成员

静态成员分为:

  • 静态成员变量
    • 所有对象共享同一份数据
      在这里插入图片描述

    • 在编译阶段分配内存

    • 类内声明,类外初始化
      在这里插入图片描述

    • 静态成员变量也是有访问权限的

注意:静态成员变量不属于某个对象,所有对象都共享一份数据,因此静态成员变量有两种访问方式:

  1. 通过对象进行访问
  2. 通过类名进行访问
    cout<<Person::m_A<<endl;
  • 静态成员函数
    • 所有对象共享同一个函数

    • 静态成员函数只能访问静态成员变量
      在这里插入图片描述

    • 访问方式也是两种:通过对象与通过类名访问
      在这里插入图片描述

    • 静态成员函数也是有访问权限的

4.3 C++对象模型和this指针

4.3.1 成员变量和成员函数分开储存

在C++中,类内的成员变量和成员函数分开存储

只有非静态成员变量才属于类的对象上

  • 空对象所占的内存空间为1
    C++编译器会给每个空对象分配一个字节空间,是为了区分空对象占内存的位置,每个空对象也应该有一个独一无二的位置
  • 非静态成员变量属于对象上
  • 静态成员变量不属于对象上
  • 静态/非静态成员函数均不属于对象上

4.3.2 this指针概念

通过4.3.1我们知道在C++中成员变量和成员函数是分开存储的

每一个非静态成员函数只会诞生一份函数实例,也就是说多个同类型的对象会共用一块代码

提出问题:这一块代码是如何区分那个对象调用自己的呢?

c++通过提供特殊的对象指针,this指针,解决上述问题。this指针指向被调用的成员函数所属的对象

this指针的特征

  • this指针是隐含每一个非静态成员函数内的一种指针
  • this指针不需要定义,直接使用即可

this指针的用途:

  • 当形参和成员变量同名时,可用this指针来区分(解决名称冲突)
    在这里插入图片描述
    在这里插入图片描述

  • 在类的非静态成员函数中返回对象本身,可使用return *this(返回对象本身用*this
    在这里插入图片描述
    上面用引用的形式返回的是p2本身,如果不用引用则返回的是用拷贝构造函数构建的一个新对象,如p2’
    在这里插入图片描述

4.3.3 空指针访问成员函数

C++中空指针也是可以调用成员函数的,但是也要注意有没有用到this指针

空指针可以调用成员函数,但是成员函数内部不能涉及成员属性,因为此时是空指针,成员属性都没定义,自然也不能访问

不明白可以回去看视频 P116
https://www.bilibili.com/video/BV1et411b73Z?p=116

4.3.4 const修饰成员函数

常函数:

  • 成员函数后加const后我们称为这个函数为常函数

  • 常函数内不可以修改成员属性(在成员函数后面加const,修饰的是this指针,让指针指向的值也不能改变,解释见下面)
    在这里插入图片描述
    这里的原因跟this指针有关:

    • this指针的本质是指针常量,指针的指向不能改变
    • 相当于是Person * const this
    • 在函数上加了const之后,相当于变成了 const Person * const this 指针指向的值也不能改变了
  • 成员属性声明时加关键字mutable后,在常函数中依然可以修改
    在这里插入图片描述

常对象:

  • 声明对象前加const称该对象为常对象
    在这里插入图片描述

  • 常对象只能调用常函数
    在这里插入图片描述
    原因:常对象是不允许对成员属性进行修改的,普通成员函数可以对成员属性进行修改,如果常对象能调用普通成员函数的话,岂不是让你曲线救国,把成员属性给改了?

4.4 友元

对于类来说,public是所有函数或其他类均可访问,private只能自己访问,但是,在程序里,有些私有属性,也想让类外特殊的一些函数或者类进行访问,就需要用到友元的技术。

友元的关键字为 friend

友元的三种实现

  • 全局函数做友元
  • 类做友元
  • 成员函数做友元

4.4.1 全局函数做友元

示例:

/***************************************
 学习内容:4.3 类和对象-友元
***************************************/
#include <iostream>
#include <string>

using namespace std;

//声明一个建筑物类
class Building
{
public:
    Building()
    {
        m_LivingRoom_ = "客厅";
        m_BedRoom_ = "卧室";
    }

public:
    string m_LivingRoom_; //客厅
private:
    string m_BedRoom_;  //卧室
};

//全局函数
void GoodGay(Building &building)
{
    cout << "全局函数好基友,正在访问:" << building.m_LivingRoom_;
    cout << "全局函数好基友,正在访问:" << building.m_BedRoom_;  //报错!
}

//测试函数
void test01()
{
    Building building;
    GoodGay(building);
}


int main()
{
    test01();

    return 0;
}

分析:因为Building类里面的成员变量m_BedRood,是私有成员,所以全局函数GoodGay()不能访问该变量,编译器也会出现相应报错
在这里插入图片描述

修改+语法示例

/***************************************
 学习内容:4.3 类和对象-友元
***************************************/
#include <iostream>
#include <string>

using namespace std;

//声明一个建筑物类
class Building
{
    //将全局函数声明前加一个friend,来声明友元
    friend void GoodGay(Building &building);
public:
    Building()
    {
        m_LivingRoom_ = "客厅";
        m_BedRoom_ = "卧室";
    }

public:
    string m_LivingRoom_; //客厅
private:
    string m_BedRoom_;  //卧室
};

//全局函数
void GoodGay(Building &building)
{
    cout << "全局函数好基友,正在访问:" << building.m_LivingRoom_;
    cout << "全局函数好基友,正在访问:" << building.m_BedRoom_;  
}

//测试函数
void test01()
{
    Building building;
    GoodGay(building);
}


int main()
{
    test01();

    return 0;
}

4.4.2 类做友元

跟上面的例子大差不差,如果我们此时有一个类class GoodGay,这个类可以访问Building类中的私有属性,则在进行Building的声明时,写法如下:

class Building
{
    //再类前面加一个friend,来声明友元
    friend class GoodGay;
    ···········
}

4.4.3 成员函数做友元

跟上面的例子大差不差,如果我们此时有一个类class GoodGay,这个类里面的一个成员函数visit()可以访问Building类中的私有属性,则在进行Building的声明时,写法如下:

class Building
{
    //将其他类的成员函数声明前加一个friend,来声明友元,注意其他类的成员函数需要加作用域
    friend void GoodGay::visit();
    ···········
}

4.5 运算符重载

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

4.5.1 加号运算符重载

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

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

int a = 10;
int b = 10;
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_=10;
p2.m_B_=10;

Person p3 = p1 + p2;  //编译器不懂加法啥意思

上述问题,可以通过自己在类内写一个成员函数实现,如:

class Person
{
public:
    Person PersonAddPerson(Person &p)
    {
        Person temp;
        temp.m_A_ = this->m_A_ + p.m_A_;
        temp.m_B_ = this->m_B_ + p.m_B_;
        return temp;
    }
public:
	int m_A_;
	int m_B_;
};

上面是自己起了一个名字,不同的人可能会有不同的函数名,C++直接做了一个简化,所有的人都可以取一个统一的名字:operator+,只需要讲上面的自定义函数名改成operator+,就实现了运算符的重载。

  • 通过成员函数重载+号

    class Person
    {
    public:
        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;
        }
    public:
    	int m_A_;
    	int m_B_;
    };
    

    此时,调用该成员函数时,一般用法:

    Person p3 = p1.operator+(p2); 
    

    简化方法:

    Person p3 = p1 + p2; 
    
  • 通过全局函数重载+号

    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 p3 = p1 + p2; 
    
  • 运算符重载也可以发生函数重载
    以全局函数为例,在上面的基础上再添加:

    Person operator+(Person &p1, int num)
    {
        Person temp;
        temp.m_A_ = p1.m_A_ + num;
        temp.m_B_ = p1.m_B_ + num;
        return temp;
    }
    

    此时,调用该全局函数时,一般用法:

    Person p4 = operator+(p1,100); 
    

    简化方法:

    Person p4 = p1 + 100;  
    

4.5.2 左移运算符重载

作用:可以输出自定义数据类型,如想要直接cout出Person类变量
注意:重载左移运算符只能通过全局函数实现

基本思想
在正常输出的时候,一般的语句为:

cout << a; 

这里可以看作是把变量cout和变量a用运算符<<连接了起来,可以参照a + b,就是运算符把两个变量连接了起来,通过查cout的定义,可以发现cout也是一个变量,是ostream变量,ostream对象只能有一个,所以需要用引用。要实现直接输出Person类变量,全局函数可以写作:

void operator<<(ostream &cout, Person p1){}

同时注意到,在使用cout时,通常会一次输出很多量,如:

cout << a << b << endl;; 

这是基于一种链式编程思想,每次第一次couta经过一次<<运算后,返回的还是一个ostream变量,所以这个变量继续和b进行<<运算(即<<运算符左边必须是ostream类变量,右边是要输出的变量)
所以上面的重载函数也要返回的是ostream类变量,又因为所有代码中的ostream变量只能有一个对象,所以返回的时候也要用引用,写成:

ostream& operator<<(ostream &cout, Person p1){}

示例:

/***************************************
 学习内容:4.4 类和对象-运算符重载
***************************************/
#include <iostream>
#include <string>

using namespace std;

class Person
{
public:
    Person(int m_A, int m_B) : m_A_(m_A), m_B_(m_B)
    {}

public:
	int m_A_;
	int m_B_;
};

ostream& operator<<(ostream &cout, Person &p1)
{
    cout << "a: " << p1.m_A_ << " b: " << p1.m_B_;
}
void test01()
{
    Person p1(10,10);
    cout << p1 << "hello world" << endl;
}


int main()
{
    test01();
    return 0;
}

结果
在这里插入图片描述

4.5.3 递增运算符重载(待补充)

【关于运算符重载的内容后续再补充,待补充……】

4.6 继承

继承是面向对象三大特征之一
有些类与类之间存在特殊的关系,如下面这张图
在这里插入图片描述
如上图所示,在这些类里,下级别的类除了有上级别类的共性,也有自己的特点。
用继承的好处:减少重复代码

4.6.1 继承的基本语法

语法:class [子类] : [继承方式] [父类] {子类特有的属性};
名词拓展:

  • 子类,又叫派生类
  • 父类,又叫基类

4.6.2 继承的方式

继承方式一共有三种:

  • 公共继承 public
  • 保护继承 protected
  • 私有继承 private
    在这里插入图片描述
    如上图所示:
  1. 父类中的private,不管子类采用那种继承方式,都无法得到
  2. 采用公共继承 public:父类中的public继承到子类中还是public,父类中的protected继承到子类中还是protected
  3. 采用保护继承 protected:父类中的publicprotected继承到子类中都变成protected
  4. 采用私有继承 private:父类中的publicprotected继承到子类中都变成private

4.6.3 继承中的对象模型

提出问题:从父类继承过来的成员,哪些属于子类对象中?
解答:父类中所有非静态成员都会被子类继承下去
注意:父类中私有成员属性,是被编译器给隐藏了,因此访问不到,但是确实被继承下去了

这里还介绍了一个查看类下所有成员属性的方法,具体见原视频(Visio Studio自带工具)

4.6.4 继承中构造和析构顺序

子类继承父类后,当创建子类对象,也会调用父类的构造函数
在这里插入图片描述
调用的顺序如上图所示,顺序为:

  1. 父类构造
  2. 子类构造
  3. 子类析构
  4. 父类析构

4.6.5 继承同名成员处理方式

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

解答

  • 访问子类同名成员,直接访问即可
  • 访问父类同名成员,需要加父类的作用域
    • 同名的成员属性:[对象名].[父类名]::[属性名]
    • 同名的成员函数:[对象名].[父类名]::[函数名]

例1:现在有父类Base和子类Son,二者中都有一个属性为m_A,创建一个子类对象Son s,放问子类对象的m_A时,直接s.m_A,访问父类父类中的m_A时,需要s.Base::m_A

例2:现在有父类Base和子类Son,二者中都有一个函数为func(),创建一个子类对象Son s,放问子类对象的func()时,直接s.func(),访问父类父类中的func()时,需要s.Base::func()

注意:当子类出现了和父类中相同名字的函数时,即使父类中发生函数重载,也不能直接调用,原因是:出现了同名的情况,子类的同名成员会隐藏掉父类中所有的同名成员函数。

例:

class Base
{
public:
	void func()
	{
		.....
	}
	void func(int a)
	{
		.....
	}
};
class Son : Base
{
public:
	void func()
	{
		.....
	}
};

此时如果想调用父类中的void func(int a),则不能直接调用,还得加父类的作用域。

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

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

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

4.6.7 多继承语法

C++中允许一个类继承多个类,即允许一个儿子认多个爹

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

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

注意:C++实际开发中不建议使用多继承

4.6.8 菱形继承

  • 菱形继承概念:

    • 两个派生类继承同一个基类
    • 又有某个类同时继承这两个派生类
    • 这种继承被成为菱形继承,或者钻石继承,如下图:
      在这里插入图片描述
  • 菱形继承问题:

    • 羊继承了动物的数据(比图兽龄),驼也继承了动物的数据,当羊驼使用数据时,就会产生二义性——>可以加以作用域区分
    • 羊驼继承自动物的数据继承了两份,但是这份数据只需要一份即可

对于第二个问题:利用虚继承,可以解决菱形继承的问题:在继承之前,加上关键字virtual
语法

class Sheep : virtual public Animal
{};
class Tuo : virtual public Animal
{};

上面的Animal类称为虚基类,利用虚继承后,羊和驼中的年龄属性就只有一个了

拓展了一下虚继承的底层原理,可以去原视频中看相关代码。

示例

#include<iostream>

using namespace std;

class Animal
{
public:
    int age_;
};

class Sheep : virtual public Animal  //虚继承
{

};

class Tuo : virtual public Animal  //虚继承
{

};

class Sheeptuo : public Sheep, public Tuo
{

};

void test01()
{
    Sheeptuo st;
    st.age_ = 18;

    cout << "st.age_ = " << st.age_ << endl;
}

int main()
{
    test01();
    return 0;
}

4.7 多态

4.7.1 多态的基本概念

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

  • 多态分为两类:
    • 静态多态:函数重载 和 运算符重载属于静态多态,复用函数名
    • 动态多态:派生类和虚函数实现运行时多态
  • 静态多态和动态多态区别:
    • 静态多态的函数地址早绑定 - 编译阶段确定函数地址
    • 动态多态的函数地址晚绑定 - 运行阶段确定函数地址

示例1:地址早绑定,在编译阶段确定函数地址

#include<iostream>

using namespace std;

class Animal
{
public:
    void Speak()
    {
        cout << "动物在说话" << endl;
    }
};

class Cat : public Animal  
{
public:
    void Speak()
    {
        cout << "小猫在说话" << endl;
    }
};

void DoSpeak(Animal &animal)
{
    animal.Speak();
}


void test01()
{
    Cat cat;
    DoSpeak(cat);
}

int main()
{
    test01();
    return 0;
}

结果
在这里插入图片描述
分析

在编译时,由于DoSpeak(Animal &animal)函数传入的是Animal类的变量,所以使用类方法Speak()时用的时Animal类里面的方法,函数地址被早绑定

示例2:地址晚绑定,运行阶段确定函数地址

将上述函数中的Animal类的Speak函数定义前面加上virtual,使之变成一个虚函数,如下:

#include<iostream>

using namespace std;

class Animal
{
public:
    virtual void Speak()
    {
        cout << "动物在说话" << endl;
    }
};

class Cat : public Animal  
{
public:
    void Speak()
    {
        cout << "小猫在说话" << endl;
    }
};

void DoSpeak(Animal &animal)
{
    animal.Speak();
}


void test01()
{
    Cat cat;
    DoSpeak(cat);
}

int main()
{
    test01();
    return 0;
}

结果
在这里插入图片描述
分析

当Animal类中的Speak()函数变成虚函数,在DoSpeak(Animal &animal)函数进行编译时,便不会绑定函数的地址,回根据传入的变量的类型确定调用哪一个Speak()函数

总结1:动态多态满足条件:
1、有继承关系
2、子类重写父类的虚函数(重写:与重载不同,重写是指函数返回值类型、函数名、参数列表与被重写的函数完全相同)

总结2:动态多态使用:
父类的指针/引用指向子类的对象(如DoSpeak(Animal &animal)中是父类的引用,传值时用的是子类的对象Cat)

补充说明:子类重写父类的虚函数时,子类的函数前面可以加virtual,也可以不加

4.7.2 多态的原理剖析

运行下面一段代码:

#include<iostream>

using namespace std;

class Animal
{
public:
    void Speak()
    {
        cout << "动物在说话" << endl;
    }
};


void test01()
{
    cout << "the size of Animal = " << sizeof(Animal) << endl;
}

int main()
{
    test01();
    return 0;
}

输出结果为:
在这里插入图片描述

分析:对于上述代码中的Animal类来说,他没有属性(非静态成员函数不属于类),所以大小为1(空类的大小为1个字节)。

将类中的成员函数变成虚函数,执行以下代码:

#include<iostream>

using namespace std;

class Animal
{
public:
    virtual void Speak()
    {
        cout << "动物在说话" << endl;
    }
};


void test01()
{
    cout << "the size of Animal = " << sizeof(Animal) << endl;
}

int main()
{
    test01();
    return 0;
}

结果:
在这里插入图片描述

分析:这里变成8个字节是因为Animal类中存储了一个指针(64位中指针为8字节,32位中指针为4字节)。
具体来说,该指针为虚函数指针(virtual function pointer, vfptr)

多态分析
当使用虚函数时,类中存储该虚函数指针:
在这里插入图片描述
当创建Cat类继承Animal类时,上述的虚函数指针被Cat一并继承
在这里插入图片描述
当子类重写父类的虚函数,子类中的虚函数表内部会替换成子类的虚函数地址
在这里插入图片描述
当父类的指针或者引用指向子类对象的时候,发生多态

4.7.3 多态案例1——计算器类

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

  • 多态的优点:
    • 代码组织结构清晰
    • 可读性强
    • 利于前期和后期的拓展以及维护

普通方法实现计算器功能:

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

//实现计算器功能,普通方法
class Calculator
{
public:
    int operation(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.operation("+") << endl;
    cout << c.m_Num1 << "-" << c.m_Num2 << "=" << c.operation("-") << endl;
    cout << c.m_Num1 << "*" << c.m_Num2 << "=" << c.operation("*") << endl;
}

int main()
{
    test01();
    return 0;
}

结果:
在这里插入图片描述
分析:

上述代码,如果以后需要添加别的操作,比如除法、乘方之类,就需要在源码的基础上进行修改
但是在开发中,提倡开闭原则,即对拓展进行开放,对修改进行关闭

如果用多态实现以上功能

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

//实现计算器功能,多态方法
class CalculatorBase
{
public:
    virtual int operation()  //只写一个虚函数,没有内容
    {
        
    }
public:
    int m_Num1;
    int m_Num2;
};

//加法类
class AddCaltor : public CalculatorBase
{
    int operation()  //子类里面重写虚函数
    {
        return m_Num1 + m_Num2;
    }
};

//减法类
class SubCaltor : public CalculatorBase
{
    int operation()  //子类里面重写虚函数
    {
        return m_Num1 - m_Num2;
    }
};

//乘法类
class MulCaltor : public CalculatorBase
{
    int operation()  //子类里面重写虚函数
    {
        return m_Num1 * m_Num2;
    }
};

void test01()
{
    //使用多态的条件
    //父类的指针或者引用指向子类的对象(这里用父类的指针)
    CalculatorBase * c = new AddCaltor;  //让父类的指针指向子类的对象
    c->m_Num1 = 10;
    c->m_Num2 = 10;
    cout << c->m_Num1 << "+" << c->m_Num2 << "=" << c->operation() << endl;
    delete c;  //new的变量存放在堆区,用完要及时释放

    c = new SubCaltor;  //delete的时候只清楚了变量值,没有删除变量本身
    c->m_Num1 = 10;
    c->m_Num2 = 10;
    cout << c->m_Num1 << "-" << c->m_Num2 << "=" << c->operation() << endl;
    delete c;  //new的变量存放在堆区,用完要及时释放

    c = new MulCaltor;  //delete的时候只清楚了变量值,没有删除变量本身
    c->m_Num1 = 10;
    c->m_Num2 = 10;
    cout << c->m_Num1 << "*" << c->m_Num2 << "=" << c->operation() << endl;
    delete c;  //new的变量存放在堆区,用完要及时释放
    
}

int main()
{
    test01();
    return 0;
}

结果:
在这里插入图片描述
分析

上述代码虽然代码量增大了,但是有了开头提到的优点,结构更清晰,以后想要拓展的时候也不用管源码了,直接新建子类重写虚函数就行,多态的有点在这种简单的功能上体现不出来,但是当代码量很大时就能体现出来。

补充
对于上述代码,想要直接新建一个子类进行运算,虚函数不可访问
在这里插入图片描述
在这里插入图片描述

4.7.4 纯虚函数和抽象类

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

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

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

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

抽象类特点:
1.无法实例化对象
2.子类必须重写抽象类中的函数,否则也属于抽象类

4.7.5 多态案例2——制作饮品

案例描述:
制作饮品的大致流程为:煮水-冲泡-倒入杯中-加入辅料

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

示例:

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

//实现制作饮品功能
class DrinkBase   //制作饮品的抽象类
{
public:
    //煮水
    virtual void Boil() = 0;  //纯虚函数

    //冲泡
    virtual void Brew() = 0;

    //倒入杯中
    virtual void Pour() = 0;

    //加辅料
    virtual void Add() = 0;

    void MakingDrink()
    {
        Boil();
        Brew();
        Pour();
        Add();
    }
};

//制作茶叶
class Tea : public DrinkBase
{
public:
    //煮水
    void Boil()  //重写虚函数
    {
        cout << "煮山泉水" << endl;
    }

    //冲泡
    virtual void Brew()
    {
        cout << "冲泡茶叶" << endl;
    }

    //倒入杯中
    virtual void Pour() 
    {
        cout << "倒入杯中" << endl;
    }

    //加辅料
    virtual void Add() 
    {
        cout << "加入枸杞" << endl;
    }

};

//制作咖啡
class Coffee : public DrinkBase
{
public:
    //煮水
    void Boil()  //重写虚函数
    {
        cout << "煮矿泉水" << endl;
    }

    //冲泡
    virtual void Brew()
    {
        cout << "冲泡咖啡" << endl;
    }

    //倒入杯中
    virtual void Pour() 
    {
        cout << "倒入杯中" << endl;
    }

    //加辅料
    virtual void Add() 
    {
        cout << "加入糖和牛奶" << endl;
    }

};

void doWork(DrinkBase * d)
{
    d->MakingDrink();
    delete d;   //注意只要new了一个变量就要delete掉
}

void test01()
{
    doWork(new Tea);   //第一种方法,用new的方法

    cout << "-------------------" << endl;
    //第二种方法
    Coffee c;
    DrinkBase &cc = c;
    cc.MakingDrink();
}

int main()
{
    test01();
    return 0;
}

结果:
在这里插入图片描述

4.7.6 虚析构和纯虚析构

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

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

  • 虚析构和纯析构共性:
    • 可以解决父类指针释放子类对象
    • 都需要有具体的函数实现
  • 虚析构和纯虚析构区别:
    • 如果是纯虚析构,该类属于抽象类,无法实例化对象

示例:以之前的”小猫会说话“为例:

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

//实现制作饮品功能
class Animal  //制作饮品的抽象类
{
public:
    Animal()
    {
        cout << "父类构造函数调用" << endl;
    }
    ~Animal()
    {
        cout << "父类析构函数调用" << endl;
    }
    virtual void Speak() = 0;
};

class Cat : public Animal
{
public:
    Cat(string name)
    {
        cout << "子类构造函数调用" << endl;
        m_Name = new string(name);   //子类在堆区开辟变量
    }
    ~Cat()
    {
        cout << "子类析构函数调用" << endl;
        if (m_Name != NULL)  //如果不为空指针,就需要delete
        {
            delete m_Name;
            m_Name = NULL;
        }
    }
    void Speak()
    {
        cout << *m_Name << "小猫说:喵喵喵" << endl;
    }

public:
    string * m_Name;
};

void test01()
{
    Animal *  abc  = new Cat("Tom");
    abc->Speak();
    delete abc;
}

int main()
{
    test01();
    return 0;
}

结果
在这里插入图片描述

分析

以上代码在子类Cat中在堆区开辟了一个变量,并用指针m_Name指向这个变量,并且在Cat的析构函数中对这个开辟的数据进行了delete操作,但是,当执行程序时,由于是父类指针指向子类对象,所以并没有调用子类数据的析构函数。就会造成内存溢出

解决办法

  • 虚析构:在父类的析构函数前面加关键词virtual,当调用父类析构函数时,就会先调用子类的析构函数。
    在这里插入图片描述
    在这里插入图片描述
    利用虚析构就解决了父类指针释放子类对象是不干净的问题

  • 纯虚析构
    语法:virtual ~类名() = 0;
    同时在类外也要进行析构函数的实现(这一点跟虚函数不一样),实现方式类名::~类名(){}
    示例:(上面代码改进)

    #include<iostream>
    #include<string>
    using namespace std;
    
    
    class Animal 
    {
    public:
        Animal()
        {
            cout << "父类构造函数调用" << endl;
        }
        // virtual ~Animal()   //虚析构
        // {
        //     cout << "父类析构函数调用" << endl;
        // }
        virtual ~Animal() = 0;  //纯虚析构声明
    
        virtual void Speak() = 0;
    };
    
    Animal::~Animal()  //纯虚析构实现
    {
         cout << "父类析构函数调用" << endl;
    }
    
    class Cat : public Animal
    {
    public:
        Cat(string name)
        {
            cout << "子类构造函数调用" << endl;
            m_Name = new string(name);   //子类在堆区开辟变量
        }
        ~Cat()
        {
            cout << "子类析构函数调用" << endl;
            if (m_Name != NULL)  //如果不为空指针,就需要delete
            {
                delete m_Name;
                m_Name = NULL;
            }
        }
        void Speak()
        {
            cout << *m_Name << "小猫说:喵喵喵" << endl;
        }
    
    public:
        string * m_Name;
    };
    
    void test01()
    {
        Animal *  abc  = new Cat("Tom");
        abc->Speak();
        delete abc;
    }
    
    int main()
    {
        test01();
        return 0;
    }
    

    结果同上
    纯虚析构总结

    • 与纯虚函数不同,纯虚析构既要有声明也要有实现(类外实现加作用域)
    • 有了纯虚析构之后,这个类也属于抽象类,无法实例化对象

4.7.7 多态案例3——电脑组装

案例描述

电脑主要组成部件为CU(用于计算),显卡(用于显示),内存条(用于存储)
将每个零件封装出抽象基类,并且提供不同的厂商生产不同的零件,例如Intel厂商和Lenovo厂商
创建电脑类提供让电脑工作的函数,并且调用每个零件工作的接口
测试时组装三台不同的电脑进行工作

代码示例

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

/*-----------------创建抽象类-----------------*/
//创建CPU抽象类
class CPU
{
public:
    //虚函数
    virtual void Calculate() = 0;
};

//创建GPU抽象类
class GPU
{
public:
    //虚函数
    virtual void Display() = 0;
};

//创建内存条抽象类
class Memory
{
public:
    //虚函数
    virtual void Storage() = 0;
};

/*-----------------创建Computer类-----------------*/
class Computer
{
public:
    Computer(CPU * cpu, GPU * gpu, Memory * mem)  //调用时父类指针指向子类对象,发生多态
    {
        m_cpu = cpu;
        m_gpu = gpu;
        m_mem = mem;
    }

    void doWork()
    {
        m_cpu->Calculate();
        m_gpu->Display();
        m_mem->Storage();
    }

private:
    CPU * m_cpu;
    GPU * m_gpu;
    Memory * m_mem;
};

/*-----------------创建Intel的三个零件类-----------------*/
class IntelCPU : public CPU
{
    void Calculate()
    {
        cout << "Intel的CPU开始计算!" << endl;
    }
};

class IntelGPU : public GPU
{
    void Display()
    {
        cout << "Intel的GPU开始显示!" << endl;
    }
};

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

/*-----------------创建AMD的三个零件类-----------------*/
class AMDCPU : public CPU
{
    void Calculate()
    {
        cout << "AMD的CPU开始计算!" << endl;
    }
};

class AMDGPU : public GPU
{
    void Display()
    {
        cout << "AMD的GPU开始显示!" << endl;
    }
};

class AMDMemory : public Memory
{
    void Storage()
    {
        cout << "AMD的内存条开始存储!" << endl;
    }
};


void test01()
{
    //组装一个全是Inel零件的电脑
    IntelCPU Icpu;
    IntelGPU Igpu;
    IntelMemory Imem;

    Computer c1(&Icpu, &Igpu, &Imem);

    c1.doWork();

    cout << "-----------------------------" << endl;
    //组装一个全是AMD零件的电脑
    AMDCPU AMDcpu;
    AMDGPU AMDgpu;
    AMDMemory AMDmem;

    Computer c2(&AMDcpu, &AMDgpu, &AMDmem);

    c2.doWork();

}

int main()
{
    test01();
    return 0;
}

结果:
在这里插入图片描述

分析:

在上述过程中,新建变量可以通过new的方式在堆区开辟变量,但是要记得要及时delete,具体操作可以看原视频。

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二进制方式

注意1:文件打开方式可以配合使用,利用|操作符
例如,用二进制方式写文件ios::binary | ios::out
注意2:文件路径可以是绝对路径也可以是相对路径,只写文件名将保存在cpp文件的同级文件夹

示例

#include<iostream>
#include<fstream>  //1.包含 文件操作 头文件

using namespace std;

void test01()
{
    //2.创建流对象
    ofstream ofs;
    //3.打开文件
    ofs.open("./test.txt",ios::out);
    //4.写数据
    ofs << "姓名:张三" << endl << "年龄:18" << endl << "性别:男" << endl;
    //5.关闭文件
    ofs.close();
}

int main()
{
    test01();
    return 0;
}

结果
在cpp文件同级目录下生成了.txt文件,文件内容:
在这里插入图片描述

5.1.2 读文件

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

读文件步骤如下:

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

判断文件是否打开成功
ifstream里有一个函数.is_open()用来判断文件是否打开成功,判断时,可用语句:

if(!ifs.is_open())
{
	cout << "文件打开失败" << endl;
	return;
}

四种读数据的方式

  1. 第一种方式

    char buf[1024] = {0};   //创建一个大小为1024的字符数组,并且全部初始化为0
    while(ifs >> buf)   //通过右移运算符将ifs的数据存放到buf中,当读取完所有数据的时候,会返回一个假的结果,结束循环
    {
    	cout << buf << endl;
    }
    
  2. 第二种方式

    char buf[1024] = {0};
    while(ifs.getline(buf,sizeof(buf)))    //用ifs的成员函数,第一个参数是数组指针,第二个参数是读取的长度,这里选一个较大值即可
    {
    	cout << buf << endl;
    }
    
  3. 第三种方式

    string buf;
    while(getline(ifs,buf))   //这次使用全局函数getline(),每次读一行
    {
    	cout << buf << endl;
    }
    
  4. 第四种方式

    char c;
    while( (c = ifs.get() ) != EOF )  //一个字符一个字符的读,直到读到了文件尾
    {
    	cout << c ;
    }
    

示例
(读取的文件是5.1.1中写的那个文件)

#include<iostream>
#include<string>
#include<fstream>  //1.包含 文件操作 头文件

using namespace std;

void test01()
{
    //2.创建流对象
    ifstream ifs;
    //3.打开文件
    ifs.open("./test.txt",ios::in);
    //4.写数据

    //第一种方法
    // char buf[1024] = {0};   //创建一个大小为1024的字符数组,并且全部初始化为0
	// while(ifs >> buf)   //通过右移运算符将ifs的数据存放到buf中,当读取完所有数据的时候,会返回一个假的结果,结束循环
	// {
	// 	cout << buf << endl;
	// }

    //第二种方法
    // char buf[1024] = {0};
    // while(ifs.getline(buf,sizeof(buf)))
    // {
    //     cout << buf << endl;
    // }

    //第三种方法
    string buf;
	while(getline(ifs,buf))   //这次使用全局函数getline(),每次读一行
	{
		cout << buf << endl;
	}

    //第四种方法
    // char c;
	// while( (c = ifs.get() ) != EOF )  //一个字符一个字符的读,直到读到了文件尾,结束循环
	// {
	// 	cout << c ;
	// }

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

int main()
{
    test01();
    return 0;
}

总结

  1. 前三种读取文件的方法,while括号里面的操作都是每次读取一行赋给字符数组/字符串(不读取换行符),当读到文件尾时,返回假,结束循环
  2. 第四种方法,while括号里面的操作每次只读取一个字符,包括读取换行符,所以在打印的时候不能添加endl。

5.2 二进制文件

以二进制的方式对文件进行读写操作
打开方式要指定为ios::binary

5.2.1 二进制写文件

二进制方式写文件主要利用流对象调用成员函数write
函数原型:ostream& write(const char * buffer, int len);
函数解释:字符指针buffer指向内存中一段存储空间,len是读写的字节数。

示例

#include<iostream>
#include<fstream>  //1.包含 文件操作 头文件

using namespace std;

class Person
{
public:
    char m_name[48];   //写二进制的时候最好不要用C++的string,而是直接用字符数组
    int m_age;
};

void test01()
{
    //2.创建流对象
    ofstream ofs("BinaryTest.txt",ios::out | ios::binary);  //也可以直接写在第二部,省略第三步,这是一个构造函数
    //3.打开文件
    // ofs.open("BinaryTest.txt",ios::out | ios::binary);  //二进制新文件,将两种打开方式用位或操作符连接
    //4.写数据
    Person p = {"张三", 18};  //将这个类变量写入文件
    ofs.write((const char *)&p, sizeof(Person));  //将p的地址强制转化为const char *的类型

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

int main()
{
    test01();
    return 0;
}

结果
生成了BinaryTest.txt文件,打开有乱码,因为是二进制的形式,问题不大,只要在下一节的二进制读文件中能读取到即可,如下图:
在这里插入图片描述

5.2.2 二进制读文件

二进制方式写文件主要利用流对象调用成员函数read
函数原型:istream& read(char * buffer, int len);
函数解释:字符指针buffer指向内存中一段存储空间,len是读写的字节数。

示例

#include<iostream>
#include<fstream>  //1.包含 文件操作 头文件

using namespace std;

class Person
{
public:
    char m_name[48];   //写二进制的时候最好不要用C++的string,而是直接用字符数组
    int m_age;
};

void test01()
{
    //2.创建流对象
    ifstream ifs;  //也可以直接写在第二部,省略第三步,这是一个构造函数
    //3.打开文件
    ifs.open("BinaryTest.txt",ios::in | ios::binary);  //二进制读文件,将两种打开方式用位或操作符连接

    if(!ifs.is_open())    //判断文件是否打开
    {
        cout << "无法打开文件" << endl;
        return;
    }
    //4.读数据数据
    Person p;  //准备一个接数据的变量
    ifs.read((char *)&p, sizeof(Person));  //将p的地址强制转化为char *的类型

    //验证是否读到数据
    cout << "姓名: " << p.m_name << " 年龄: " << p.m_age << endl;

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

int main()
{
    test01();
    return 0;
}

结果
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

dreautumn

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值