C++笔记3:C++核心编程

7.22
1、内存分区模型
C++ Primer Plus(嵌入式公开课)—第4章 复合类型–>4.8.5 自动存储、静态存储和动态存储

C++程序在执行时,将内存大方向划分为4个区域
①代码区:存放函数体的二进制代码,由操作系统OS进行管理的。
②全局区:存放全局变量静态变量(static)以及常量(全局常量+字符串常量)
③栈区:由编译器自动分配释放, 存放函数的形参,局部变量(包括局部常量)等。
④堆区:由程序员分配和释放(new出来的东西),,若程序员不释放,程序结束时由操作系统回收。

ps.变量包括普通变量和用const修饰的变量(即常量),全局常量即用const修饰的全局变量,同理局部常量即用const修饰的局部变量。

//存放在栈区:
局部变量a地址为: 3865544
局部变量b地址为: 3865532
局部常量c_l_a地址为: 3865520
局部常量c_l_b地址为: 3865508
//存放在全局区:
全局变量g_a地址为: 13598728
全局变量g_b地址为: 13598732
静态变量s_a地址为: 13598736
静态变量s_b地址为: 13598740
字符串常量地址为: 13587268
字符串常量地址为: 13587284
全局常量c_g_a地址为: 13587072
全局常量c_g_b地址为: 13587076

补充1:
栈区存放函数的形参,局部变量(包括局部常量)等。
不要返回局部变量的地址,栈区开辟的数据由编译器自动释放

补充2:new操作符
示例:

int* func()
{
	int* a = new int(10);//new一个int型变量,赋上10,并将这个变量的地址返回
	return a;
}

int main() {

	int a = 20;
	int* p = func();
	cout << *p << endl;//10
	cout << *p << endl;//10

	//将指针p指向的堆区的数据int(10)释放掉
	delete p;
	//cout << *p << endl; p已经通过delete被释放掉就不能再次进行访问
	//引发了异常: 读取访问权限冲突。p 是 0x8123。

	p = &a;//指针p本身依旧可以重新指向别的变量,上面delete释放的是p刚开始指向的new出来的堆区的数据int(10)
	cout << *p << endl;//20

	system("pause");
	return 0;
}

①int* a = new int(10);//new一个int型变量,赋上10,并将这个变量的地址返回,即new出来的东西要用一个指针来接收
②指针*p本质上是一个局部变量,存放在栈区;而new出来的int(10)存放在堆区
③new一个数组:

	int* arr = new int[10];
	for (int i = 0; i < 10; i++)
		arr[i] = i + 10;
	for (int i = 0; i < 10; i++)
		cout << arr[i] << " ";
	cout << endl;
	//释放new出来的数组
	delete[] arr;

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

	int a = 10;
	int &b = a;//b是别名,原名是a

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

	b = 100;

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

注意事项:
①引用必须初始化(&b = a;),并且初始化之后不可改变

	int a = 10;
	int b = 20;
	//int &c; //错误,引用必须初始化
	int &c = a; //一旦初始化后,就不可以更改
	//&c = b; 更改引用的操作是不合法的
	c = b; //这是赋值操作,不是更改引用

	cout << "a = " << a << endl;//20
	cout << "b = " << b << endl;//20
	cout << "c = " << c << endl;//20

引用的本质是一个指针常量(int* const p; 指向固定,指向的值可变)

指针常量和常量指针的区别:
指针常量:指针是个常量,即指向固定;---引用
常量指针:指向常量的指针,即指向的值固定。

2.1 引用做函数参数
函数传参时,可以利用引用的技术让形参修饰实参

//值传递
void swap1(int a, int b)
{
	int temp = a;
	a = b;
	b = temp;
}
//地址传递
void swap2(int* a, int* b) {
	int temp = *a;
	*a = *b;
	*b = temp;
}
//引用传递
void swap3(int& a, int& b) {
	int temp = a;
	a = b;
	b = temp;
}

int main(){
	int a = 10, b = 20;
	swap1(a, b);
	cout << a << "," << b << endl;//10,20
	a = 10, b = 20;
	swap2(&a, &b);//形参是指针,所以实参要加&
	cout << a << "," << b << endl;//20,10
	a = 10, b = 20;
	swap3(a, b);//传参的时候直接给a和b,不用加&
	cout << a << "," << b << endl;//20,10
}

2.2 引用做函数的返回值

int& test01() {//返回一个引用
	static int a = 10;//静态变量,存在全局区
	return a;
}
int main(){
	int& ref = test01();//函数test01()返回的是一个引用&,所以要用一个引用&去接收返回值
	//别名是ref,原名是test01()中的a
	cout << ref << endl;//10

	system("pause");
	return 0;
}

2.3 常量引用
常量引用主要用来修饰形参,防止误操作
在函数形参列表中,可以加const修饰形参,防止形参改变实参。

void showValue(int& v) {
	v++;
	cout << v << endl;
}

void showValue1(const int& v) {
	//v++;语法错误,因为形参v用const修饰之后就不能再修改了
	//也就是通过这种方式来避免由于形参的变化导致实参也被动变化
	cout << v << endl;
}
int main(){

	int a = 10;
	showValue(a);//原名是a,别名是v  //a=10给到showValue()后,在函数内别名v自加一,所以结果是11
	 
	a = 100;//原名b是可以变化的,但别名v的变化也会导致b的值发生改变
	showValue(a);a=100给到showValue()后,在函数内别名v自加一,所以结果是101


	int b = 20;
	showValue1(b);原名是b,别名是v  //20
	//由于形参加了个const,所以别名v就成了一个常量,不能再变化,这样就避免了由于v的变化而导致实参b的改变
	b = 30;//
	showValue1(b);//30

	system("pause");
	return 0;
}

3、函数提高
3.1 函数默认参数

//1. 如果某个位置参数有默认值,那么从这个位置往后,从左向右,必须都要有默认值
int func(int a, int b = 10, int c = 10) {
	return a + b + c;
}
//2. 如果函数声明时有默认值,那么函数定义的时候就不能有默认参数
//声明:
int func2(int a = 10, int b = 10);//声明中给出默认值
//定义:
int func2(int a , int b ) {//定义中不允许再给出默认值 
	return a + b;
}

int main(){
//函数默认参数
	//如果我们自己传入数据,就用自己的数据,如果没有,就用默认值
	//cout << "ret = " << func() << endl;//参数a没有默认值
	cout << "ret = " << func(100) << endl;//100+10+10=120
	cout << "ret = " << func(20, 20) << endl;//20+20+10=50
	cout << "ret = " << func(10,10,10) << endl;//10+10+10=30

	cout << func2() << endl;//20
	cout << func2(20) << endl;//20+10=30

	system("pause");
	return 0;
}

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

3.2.1 函数重载满足条件:
① 同一个作用域下
②函数名称相同
③函数参数的 类型不同个数不同顺序不同

//函数重载需要函数都在同一个作用域下
void func()
{
	cout << "func 的调用!" << endl;
}
void func(int a)
{
	cout << "func (int a) 的调用!" << endl;
}
void func(double a)
{
	cout << "func (double a)的调用!" << endl;
}
void func(int a ,double b)
{
	cout << "func (int a ,double b) 的调用!" << endl;
}
void func(double a ,int b)
{
	cout << "func (double a ,int b)的调用!" << endl;
}

//函数返回值不可以作为函数重载条件
//int func(double a, int b)
//{
//	cout << "func (double a ,int b)的调用!" << endl;
//}

int main() {
	func();
	func(10);
	func(3.14);
	func(10,3.14);
	func(3.14 , 10);
	
	system("pause");
	return 0;
}

3.2.2 函数重载注意事项:
①引用作为重载条件
②函数重载碰到函数默认参数://碰到默认参数产生歧义,需要避免

//函数重载注意事项
//1、引用作为重载条件
void func(int &a)
{
	cout << "func (int &a) 调用 " << endl;
}

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

//2、函数重载碰到函数默认参数
void func2(int a, int b = 10)
{
	cout << "func2(int a, int b = 10) 调用" << endl;
}

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

int main() {
	
	int a = 10;
	func(a); //调用无const
	func(10);//调用有const

	//func2(10); //碰到默认参数产生歧义,需要避免

	system("pause");
	return 0;
}

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

4.1 封装
4.1.1 语法
class 类名{ 访问权限: 属性 / 行为 };
补充:

类(抽象)---对象(具体)
属性(变量)
行为(函数/方法)

4.1.2 访问权限
//三种权限:
//公共权限 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;
	}
};

int main() {

	Person p;
	p.m_Name = "李四";
	//p.m_Car = "奔驰";  //保护权限类外访问不到
	//p.m_Password = 123; //私有权限类外访问不到

	system("pause");
	return 0;
}

4.1.3 类class和结构体struct的区别
在C++中 struct和class唯一的区别就在于默认的访问权限不同

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

示例:

class C1
{
	int  m_A; //默认是私有权限
};
struct C2
{
	int m_A;  //默认是公共权限
};

int main() {

	C1 c1;
	c1.m_A = 10; //错误,访问权限是私有

	C2 c2;
	c2.m_A = 10; //正确,访问权限是公共

	system("pause");
	return 0;
}

4.1.4 案例
判断两个立方体是否相同:
1.Cube类中的成员变量(L W H)设置为private,每个变量对应的设置读取函数(setL()和getL())。这是因为成员变量是私有权限,所以在类之外例如main函数中就无法访问到L W H,只能通过setL()和getL()来设置和获取L的值。
2.成员函数calArea()可以不要形参,直接用类内的成员变量(L W H)
3.判断两个立方体是否相同:
成员函数即类内的函数 bool isSame(Cube c2);
全局函数isSame(Cube c1,Cube c2)
4.//测试return
else这行可以不要,但最后一句return 0不能少!!!

bool testReturn() {
	int a = 10, b = 11;
	if (a == b)
		return 1;
	else//不要这个else也行,因为只要满足a == b ,执行了上一行的return 1,这个函数就结束了,不会到return 0,所以可以不要这个else
		return 0;//但是这个return 0不能少,否则不论是否满足a == b,都会执行return 1,因为只有return 1这一个出口
}

//案例二:点和圆的关系
利用头文件和源文件使得程序更有条理:
首先Point.h下的程序为:

//类的声明:包括类的成员变量和成员函数的声明

//①下面这三行要有
#pragma once
#include<iostream>
using namespace std;

//判断点和圆的关系---点类
class Point {

private:
	double x;//②成员变量的声明
	double y;

public:
	void setX(double x);//③成员函数的声明
	double getX();
	void setY(double y);
	double getY();

}; 

然后Point.cpp下的程序:()

//类的实现:类的成员函数的实现

#include"Point.h"	//①Point类的头文件的名字


void Point::setX(double x) {	//②记得加作用域(类名::)Point::
	this->x = x;
}
double Point::getX() {
	return x;
}
void Point::setY(double y) {
	this->y = y;
}
double Point::getY() {
	return y;
}

接下来是Circle.h的程序:

//类的声明,包括类的成员变量和成员函数的声明

#pragma once
#include<iostream>
#include"Point.h"		//①因为Circle类中包括了Point类,所以要声明Point类的头文件
using namespace std;

//判断点和圆的关系---圆类
class Circle {

private:
	int radius;//半径
	Point center;//圆心

public:
	void setR(int r);
	int getR();
	void setCenter(int x, int y);
	Point getCenter();

	void result(Point p);

}; 

最后是Circle.cpp的程序:

//类的实现:类的成员函数的实现

//①Circle类的头文件的名字
#include"Circle.h"


void Circle::setR(int r) {//作用域(类名::)
	radius = r;
}
int Circle::getR() {
	return radius;
}
void Circle::setCenter(int x, int y) {
	center.setX(x);
	center.setY(y);
}
Point Circle::getCenter() {
	return center;
}

void Circle::result(Point p) {
	int distance = sqrt((p.getX() - center.getX()) * (p.getX() - center.getX()) + (p.getY() - center.getY()) * (p.getY() - center.getY()));
	if (distance > radius)
		cout << "点在圆外" << endl;
	else if (distance == radius)
		cout << "点在圆上" << endl;
	else
		cout << "点在圆内" << endl;
}

总结:
1.头文件Circle.h中
①要加

#pragma once
#include<iostream>
#include"Point.h" //因为Circle类中包含Point类
using namespace std;

②声明类的成员变量和成员函数

2.源文件Circle.cpp中
①要加头文件声名

#include"Circle.h"

②要加作用域(类名::)Circle::例如:

void Circle::setR(int r) {
	radius = r;
}

4.2 对象的初始化和清理
4.2.1 构造函数与析构函数

class Person
{
public:
	//构造函数
	Person()
	{
		cout << "Person的构造函数调用" << endl;
	}
	//析构函数
	~Person()
	{
		cout << "Person的析构函数调用" << endl;
	}

};

int main() {
	
	Person p;

	system("pause");
	return 0;
}

结果:

构造
请按任意键继续. . .
析构

4.2.2 构造函数的分类及调用
两种分类方式:
​ 按参数分为: 有参构造和无参构造
​ 按类型分为: 普通构造和拷贝构造

//构造函数分类
class Test1 {

public:
	int age;
	
public:
	Test1() {
		cout << "无参构造" << endl;
		age = 0;//成员变量初始化,否则会提示警告C26495
	}
	Test1(int age) {
		this->age = age;
		cout << "有参构造" << endl;
	}
	Test1(const Test1& p) {//参数是Test1类的引用,并且是一个常量引用,防止形参改变实参,也即形参p不可变
		this->age = p.age;//将形参p的所有属性拷贝到“我”身上
		cout << "拷贝构造" << endl;
	}

	~Test1() {
		cout << "析构2" << endl;
	}

};

三种调用方式:
​ 括号法
​ 显示法
​ 隐式转换法

int main() {

/*构造函数与析构函数*/
	Test0 p0;
	cout << "\n\n" << endl;
	Test1 p1;//创建一个对象的时候就相当于调用了无参构造函数
	Test1 p2(10);//调用有参构造函数
	Test1 p3(p1);//调用拷贝构造函数
	cout << p2.age << endl;//10
	cout << p3.age << endl;//0

	system("pause");
	return 0;
}

4.2.3 拷贝构造函数调用时机
使用一个已经创建完毕的对象来初始化一个新对象
值传递的方式给函数参数传值
以值方式返回局部对象

注意:
拷贝构造函数使用时机:1.用已有对象初始化新对象。 2,传参。3,函数返回值。当=的左侧为非新对象时,无法调用拷贝构造函数,此时要拷贝需重载赋值=运算符

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

4.2.5 深拷贝与浅拷贝

浅拷贝(编译器自带的拷贝构造函数):简单的赋值拷贝操作,浅拷贝带来的问题是堆区内存重复释放
深拷贝(手动写的拷贝构造函数):在堆区重新申请空间,进行拷贝操作。
示例:

//深拷贝&浅拷贝
class Person1 {
public:
	int age;
	int* height;

public:
	Person1() {
		cout << "无参构造" << endl;
		this->age = 0;
		*this->height = 0;
	}
	Person1(int age, int height) {
		this->age = age;
		//在堆区开辟空间,存放this->height
		this->height = new int(height);//this->height是个地址
		cout << "this->height地址:" << (int)this->height << endl;//17681832
		cout << "有参构造" << endl;
	}
	Person1(const Person1& p) {
		this->age = p.age;
		//this->height = p.height;
		//开始拷贝之前,在堆区为形参p开辟新的空间,用来存放p.height
		this->height = new int(*p.height);//括号里的*p.height是一个值,不是地址
		cout << "形参p地址:" << (int)p.height << endl;//17681832
		cout << "拷贝构造" << endl;
	}
	~Person1() {//析构是将堆区开辟的数据进行释放操作
		if (this->height != NULL){
			delete this->height;
			this->height = NULL;
		}
			
		cout << "析构" << endl;
	}
};
int main()
{
//深拷贝,浅拷贝
	Person1 p1(18, 160);//有参构造
	Person1 p2(p1);//拷贝构造
	cout << "地址p1:" << (int)p1.height << endl;//17681832
	cout << "地址p2:" << (int)p2.height << endl;//17681992
	***//对象p1在堆区开辟了空间存放身高数据160,返回地址17681832
	//对象p2在堆区17681992的位置存放从*(p.height)复制来的身高数据160
	//各自在析构的时候delete各自在堆区开辟的空间,而不会导致堆区空间重复释放***
	cout << "p1的年龄: " << p1.age << " 身高: " << *p1.height << endl;//18 160 
	cout << "p2的年龄: " << p2.age << " 身高: " << *p2.height << endl;//18 160

	system("pause");
	return 0;
}

深拷贝&浅拷贝
对上图的解释:
浅拷贝只是简单的将p1.height(0x0011)复制给p2.height,此时两个height指向同一块内存(0x0011);
由于p1和p2是局部变量,保存在栈区,所以遵循先进后出的原则,程序执行完先对p2进行析构,把p2.height(0x0011)指向的内容(160)进行释放,此时p1.height的指向已经为空,后续对p1进行析构的时候还会再释放一次p1.height(0x0011)所指向的内容,堆区中同一块内存被重复释放两次,导致报错。

4.2.6 静态成员
静态成员分为:静态成员变量和静态成员函数。

  • 静态成员变量
    • 所有对象共享同一份数据,数据可以改变
    • 在编译阶段分配内存
    • 类内声明,类外初始化
  • 静态成员函数
    • 所有对象共享同一个函数
    • 静态成员函数只能访问静态成员变量

4.3 C++对象模型和this指针
4.3.1 成员变量和成员函数分开存储
空对象占用的空间内存:1字节。
在C++中,类内的成员变量和成员函数分开存储,只有非静态成员变量才属于类的对象上,静态成员变量和静态成员函数和非静态成员函数都不占对象空间。
在这里插入图片描述

4.3.2 this指针—在两个地方有用到(C++第七阶段的补充和C++提高编程2的3.2.7 vector互换容器
this指针的用途:
①当形参和成员变量同名时,可用this指针来区分
②在类的非静态成员函数中返回对象本身,可使用return *this

//this指针
class Person4 {

public:
	int age;

public:
	Person4(int age) {
		this->age = age;
	}
	Person4& personAddAge(Person4 p) {
		this->age += p.age;
		//返回对象本身
		return *this;//返回的是一个地址
	}
};

int main(){
//this指针---在类的非静态成员函数中返回对象本身,可使用return *this
	Person4 p1(10);
	cout << p1.age << endl;//10
	cout << p1.personAddAge(p1).age << endl;//10+10=20
	cout << p1.age << endl;//20
	cout << p1.personAddAge(p1).personAddAge(p1).age << endl;/20+20=40 40+40=80
	cout << p1.age << endl;//80
	cout << p1.personAddAge(p1).age << endl;//80+80=160

	p1.age = 10;
	Person4 p2(10);//10
	p2.personAddAge(p1).personAddAge(p1).personAddAge(p1);//10+10*3=40
	cout << "p2.age = " << p2.age << endl;//40

	system("pause");
	return 0;
}

4.3.3 const修饰成员函数

4.4 友元
友元的目的就是让一个函数或者类 访问另一个类中私有成员,关键字是 friend

友元的三种实现:
全局函数做友元
类做友元
成员函数做友元

4.4.1 全局函数做友元
//告诉编译器 goodGay全局函数 是 Building类的好朋友,可以访问类中的私有内容
4.4.2 类做友元
//告诉编译器 goodGay类是Building类的好朋友,可以访问到Building类中私有内容
friend class goodGay;
4.4.3 成员函数做友元
//告诉编译器 goodGay类中的visit成员函数 是Building好朋友,可以访问私有内容

4.5 运算符重载 点这里

4.6 继承
有些类与类之间存在特殊的关系,下级别的成员除了拥有上一级的共性,还有自己的特性。这个时候我们就可以考虑利用继承的技术,减少重复代码。
4.6.1 继承的基本语法

class A: public B{
	
};
A类称为子类或派生类
B类称为父类或基类

示例:

//公共页面
class BasePage {

public:
	//公共头部
	void header() {
		cout << "首页、公开课、登录、注册...(公共头部)" << endl;
	}
	//公共底部
	void footer() {
		cout << "帮助中心、交流合作、站内地图...(公共底部)" << endl;
	}
	//公共左栏
	void left()
	{
		cout << "Java,Python,C++...(公共分类列表)" << endl;
	}
};
//Java界面:
class Java :public BasePage {//类名后面:public BasePage
public:
	void content() {
		cout << "JAVA学科视频" << endl;
	}
};
int main(){

	//Java界面
	Java ja;
	ja.header();
	ja.footer();
	ja.left();
	ja.content();
	cout << endl;

	system("pause");
	return 0;
}

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

};
继承方式分为public,protected,private
公共继承,保护继承,私有继承。
三种继承方式
总结:
1.父类中的私有内容(private)任何一种继承方式都访问不到,即无法被访问/被继承;
2.公共继承:父类中的各访问权限不变
3.保护继承:父类中的各访问权限都变成protected保护权限
4.私有继承:父类中的各访问权限都变成private私有权限

4.6.3 继承中的对象模型
父类中私有成员也是被子类继承下去了,只是由编译器给隐藏后访问不到。即子类所占内存包括父类的所有内容和自己的内容所占内存之和

4.6.4 继承中的构造和析构顺序
继承中 先调用父类构造函数,再调用子类构造函数,析构顺序与构造相反。
示例:

class Base0 {

public:
	Base0() {
		cout << "父类构造函数" << endl;
	}
	~Base0() {
		cout << "父类析构函数" << endl;
	}
};
class Son0 :public Base0{

public:
	Son0() {
		cout << "子类构造函数" << endl;
	}
	~Son0() {
		cout << "子类析构函数" << endl;
	}
};
int main(){
	
	Son0 son;//通过默认构造函数,创建一个对象
	
	system("pause");
	return 0;
}

结果:

父类构造函数
子类构造函数
请按任意键继续. . .
子类析构函数
父类析构函数

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

  1. 子类对象可以直接访问到子类中同名成员
  2. 子类对象加作用域可以访问到父类同名成员
  3. 当子类与父类拥有同名的成员函数,子类会隐藏父类中同名成员函数,加作用域可以访问到父类中同名函数
//子类与父类出现相同的成员(变量/函数)时,子类对象如何访问到同名的数据
	Son1 son;
	//同名成员变量
	cout << "子类中的a = " << son.a << endl;//直接访问
	cout << "父类中的a = " << son.Base1::a << endl;//加父类的作用域
	//同名成员函数
	son.func();//直接访问
	son.Base1::func();//加父类的作用域
	son.Base1::func(10);//加父类的作用域

4.6.6 多继承语法
多继承:一个类继承多个类
语法: class 子类 :继承方式 父类1 , 继承方式 父类2...
C++实际开发中不建议用多继承
4.6.7 菱形继承
​概念:
两个派生类继承同一个基类;又有某个类同时继承者两个派生类;这种继承被称为菱形继承,或者钻石继承。
典型案例:

  1. 羊继承了动物的数据,驼同样继承了动物的数据,当草泥马使用数据时,就会产生二义性。
  2. 草泥马继承自动物的数据继承了两份,其实我们应该清楚,这份数据我们只需要一份就可以。
    问题:
  • 菱形继承带来的主要问题是子类继承两份相同的数据,导致资源浪费以及毫无意义
  • 利用虚继承可以解决菱形继承问题

4.7 多态
4.7.1 多态的基本概念
①多态分为两类:

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

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

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

③多态满足条件:

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

④多态使用条件

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

⑤重写:函数返回值类型 函数名 参数列表 完全一致称为重写
重载: ① 同一个作用域下
②函数名称相同
③函数参数的 类型不同个数不同顺序不同

示例:
关键在于父类中成员函数前的virtual关键字

//动物类
class Animals {
public:
	//函数前面加上virtual关键字,speak函数就是虚函数
	virtual void speak() {
		cout << "动物在说话" << endl;
	}
};
//猫类
class Cats: public Animals{
public:
	void speak() {
		cout << "小猫在说话" << endl;
	}
};
class Dogs:public Animals {
public:
	void speak() {
		cout << "小狗在说话" << endl;
	}
};
//全局函数
void doSpeak(Animals& ani) {
	ani.speak();
}

int main(){
	cout << "sizeof Animals类 = " << sizeof(Animals) << endl;//有virtual关键字的Animals类占4字节
	//没有virtual关键字的Animals类占1字节,空类,并且非静态成员函数不属于类的内存(见4.3.1 成员变量和成员函数分开存储)
	//加了virtual关键字后Animals类占4字节,不再是空类,而是多了一个指针,叫vfptr(虚函数表指针),表内记录虚函数的地址

	Cats cat;//当子类 重写 父类的 虚函数 ,子类中的虚函数表内部会替换成子类的虚函数地址
	
	//子类重写父类的虚函数,即子类也有个vfptr(虚函数表指针),表内记录虚函数的地址
	cout << "sizeof Cats类 = " << sizeof(Cats) << endl;//4 
	
	//当父类的 指针或者引用 指向子类对象的时候,就发生了多态
	doSpeak(cat);//小猫在说话
	
	Dogs dog;
	doSpeak(dog);//小狗在说话

	system("pause");
	return 0;
}

多态的底层原理
多态的底层原理:
首先在父类中的虚函数virtual void Speak(){}),使得父类占4个字节,这4个字节是个vfptr(虚函数表指针),它指向虚函数表(vftable),表内记录虚函数的地址(&Animal::speak);
然后是子类重写父类中的虚函数,因此子类也占4个字节,这4个字节也是个vfptr(虚函数表指针),它也指向虚函数表(vftable),表内也记录虚函数的地址;在子类重写父类中的虚函数后,子类中的虚函数表内部会替换成子类的虚函数地址(&Cats::speak)。
最后是当父类的指针或者引用指向子类对象时,就发生了多态

派生类虚表:
1.先将基类的虚表中的内容拷贝一份
2.如果派生类对基类中的虚函数进行重写,使用派生类的虚函数替换相同偏移量位置的基类虚函数
3.如果派生类中新增加自己的虚函数,按照其在派生类中的声明次序,放在上述虚函数之后
原文链接:https://blog.csdn.net/qq_39412582/article/details/81628254

4.7.2 多态案例1—计算器类
//如果想扩展新的功能,需要修改源码
//在真实开发中提倡开闭原则
//开闭原则:对扩展进行开放,对修改进行关闭。
多态技术:
①继承②父类有虚函数③子类重写父类的虚函数
④父类的指针或引用指向子类对象
重写:函数返回值类型 函数名 参数列表 完全一致称为重写

程序:

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

//分别利用普通写法和多态技术,设计实现两个操作数进行运算的计算器类
//普通写法
class Calculator {
public:
	int a;
	int b;

public:
	int getResult(string oper) {
		if (oper == "+")
			return a + b;
		else if (oper == "-")
			return a - b;
		else if (oper == "*")
			return a * b;
		//如果想扩展新的功能,需要修改源码
		//在真实开发中提倡开闭原则
		//开闭原则:对扩展进行开放,对修改进行关闭。
	}
};

//多态写法:①继承②父类有虚函数③子类重写父类的虚函数④父类的指针(下面第76行)或引用(源.cpp中449行)指向子类对象
//父类:抽象计算器
class abstractCalculator {
public:
	int a, b;
	virtual int getResult() {//②虚函数
		return 0;
	}

};
//子类:加法计算器
class addCalculator:public abstractCalculator {//①继承
public:
	int getResult() {//③子类重写父类的虚函数
		return a + b;
	}
};
//子类:减法计算器
class subtractCalculator :public abstractCalculator {//①继承
public:
	int getResult() {//③子类重写父类的虚函数
		return a - b;
	}
};
//子类:乘法计算器
class multipleCalculator :public abstractCalculator {//①继承
public:
	int getResult() {//③子类重写父类的虚函数
		return a * b;
	}
};
//子类:除法计算器
class divideCalculator :public abstractCalculator {//①继承
public:
	int getResult() {//③子类重写父类的虚函数
		return a / b;
	}
};
//全局函数(为了完成④父类的引用指向子类对象)
int doCalculator(abstractCalculator& abc) {
	return abc.getResult();
}


int main() {

//普通写法:
	Calculator c;
	c.a = 10;
	c.b = 20;
	cout << "相加:" << c.getResult("+") << endl;
	cout << "相减:" << c.getResult("-") << endl;
	cout << "相乘:" << c.getResult("*") << endl;

//多态写法:
	//④父类的指针指向子类对象addCalculator
	abstractCalculator* abc = new addCalculator;
	abc->a = 10;
	abc->b = 12;
	cout << "加法:" << abc->getResult() << endl;//22
	delete abc;//new出来的内容要记得销毁

	//④父类的指针指向子类对象multipleCalculator
	abc = new multipleCalculator;//这里的父类指针abc本身依旧可以重新指向别的变量,(见test_new和delete.cpp)
								 //上面delete释放的是abc刚开始指向的new出来的子类对象addCalculator
	abc->a = 6;
	abc->b = 5;
	cout << "乘法:" << abc->getResult() << endl;//30
	delete abc;//这里销毁的是父类指针abc指向的89行new出来的子类对象multipleCalculator

	//④父类的引用指向子类对象subtractCalculator
	subtractCalculator sub;
	sub.a = 11;
	sub.b = 13;
	cout << "减法:" << doCalculator(sub) << endl;//-2

	system("pause");
	return 0;
}

4.7.3 纯虚函数和抽象类
在多态中,通常父类中虚函数的现实无意义的,主要都是调用子类重写的内容,所以可以将虚函数改为“纯虚函数”,纯虚函数语法:virtual 返回值类型 函数名 (参数列表)= 0 ;父类中有了纯虚函数,这个类就被称为抽象类

抽象类特点:

  • 无法实例化对象
  • 子类必须重写抽象类中的纯虚函数,否则也属于抽象类

程序:

 //父类:抽象计算器
class abstractCalculator {
public:
	int a = 0, b = 0;
	//virtual int getResult() {//②虚函数
	//	return 0;
	//}
	virtual int getResult() = 0;//纯虚函数
};

4.7.4 多态案例2—制作饮品
①可以把很多个步骤放到一个非静态成员函数中,这个函数就相当于一个接口,各个子类都可以通过这个接口进入。

//父类:抽象制作饮品
class abstractMakingDrinking {

public:
	//纯虚函数
	virtual void boilingWater() = 0;//煮水
	virtual void chongPao() = 0;//冲泡
	virtual void takeToBottle() = 0;//倒入杯中
	virtual void addFlavouring() = 0;//加佐料

	//非静态成员函数,相当于一个接口
	void makingDrinking() {
		boilingWater();
		chongPao();
		takeToBottle();
		addFlavouring();
	}
};

//子类:冲咖啡
class makingCoffee :public abstractMakingDrinking {
	virtual void boilingWater() {//煮水
		cout << "煮水" << endl;
	}
	virtual void chongPao() {//冲泡
		cout << "冲泡咖啡" << endl;
	}
	virtual void takeToBottle() {//倒入杯中
		cout << "倒入杯中" << endl;
	}
	virtual void addFlavouring() {//加佐料
		cout << "加糖和牛奶" << endl;
	}
};
//子类:冲茶叶
class makingTee :public abstractMakingDrinking {
	virtual void boilingWater() {//煮水
		cout << "煮水" << endl;
	}
	virtual void chongPao() {//冲泡
		cout << "冲泡茶叶" << endl;
	}
	virtual void takeToBottle() {//倒入杯中
		cout << "倒入杯中" << endl;
	}
	virtual void addFlavouring() {//加佐料
		cout << "加柠檬" << endl;
	}
};

②实现多态的时候,父类的指针/引用指向子类对象

//全局函数-->①父类的引用指向子类对象
void doMaking(abstractMakingDrinking& mD) {
	mD.makingDrinking();
}
//全局函数-->②父类的指针指向子类对象
void doMaking(abstractMakingDrinking* mD) {
	mD->makingDrinking();
	delete mD;//释放
}
int main() {

	//父类指针②指向子类对象---冲咖啡
	abstractMakingDrinking* md = new makingCoffee;
	md->makingDrinking();
	delete md;//释放
	cout << "------------------" <<endl;

	//全局函数-->①父类的引用指向子类对象---冲茶叶
	makingTee mt;
	doMaking(mt);
	cout << "------------------" <<endl;

	//全局函数-->②父类的指针指向子类对象
	doMaking(new makingCoffee);
	cout << "------------------" <<endl;
	doMaking(new makingTee);
	cout << "------------------" <<endl;

	system("pause");
	return 0;
}

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

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

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

4.7.6 多态案例三—电脑组装
1.各个零部件有父类,然后继承、重写、实现多态(父类指针指向子类对象,这里就会有各个零件的功能函数)
2.把各个零件的功能函数一起写到computer类中。
程序:

#include<iostream>
using namespace std;

//void usingCPU(abstractCPUs* cpu);
//void usingGPU(abstractGPUs* gpu);
//void usingMemory(abstractMemorys* memory);



//父类:CPU
class abstractCPUs {
public:
	//CPU的计算功能
	virtual void calculate() = 0;//virtual void cpuInfo() = 0;
};
//父类:显卡
class abstractGPUs {
public:
	//GPU的显示功能
	virtual void display() = 0;//virtual void gpuInfo() = 0;
};
//父类:内存条
class abstractMemorys {
public:
	//内存条的存储功能
	virtual void storage() = 0;//virtual void memoryInfo() = 0;
};

//CPU子类://Intel,AMD,IBM
class CPU1:public abstractCPUs{
public:
	virtual void calculate() {
		cout << "Intel牌CPU开始计算了" << endl;
	}
};
class CPU2 :public abstractCPUs {
public:
	virtual void calculate() {
		cout << "AMD牌CPU开始计算了" << endl;
	}
};
class CPU3 :public abstractCPUs {
public:
	virtual void calculate() {
		cout << "IBM牌CPU开始计算了" << endl;
	}
};
//全局函数--->父类指针/引用指向子类对象
void usingCPU(abstractCPUs* cpu) {
	cpu->calculate();//接口
	delete cpu;
}


//GPU子类:  NVIDIA、AMD
class GPU1 :public abstractGPUs {
public:
	virtual void display() {
		cout << "NVIDIA牌GPU开始显示了" << endl;
	}
};
class GPU2 :public abstractGPUs {
public:
	virtual void display() {
		cout << "AMD牌GPU开始显示了" << endl;
	}
};
//全局函数--->父类指针/引用指向子类对象
void usingGPU(abstractGPUs* gpu) {
	gpu->display();//接口
	delete gpu;
}



//内存条子类:ADATA USCORSAIR Kingston
class memory1 :public abstractMemorys{
public:
	virtual void storage() {
		cout << "ADATA牌内存条开始存储" << endl;
	}
};
class memory2 :public abstractMemorys {
public:
	virtual void storage() {
		cout << "USCORSAIR牌内存条开始存储" << endl;
	}
};
class memory3 :public abstractMemorys {
public:
	virtual void storage() {
		cout << "Kingston牌内存条开始存储" << endl;
	}
};
//全局函数--->父类指针/引用指向子类对象
void usingMemory(abstractMemorys* memory) {
	memory->storage();//接口
	delete memory;
}

class Computer {
public:
	void doWork(abstractCPUs* cpu, abstractGPUs* gpu, abstractMemorys* memory) {
		cout << "开始组装电脑:" << endl;
		usingCPU(cpu);
		usingGPU(gpu);
		usingMemory(memory);
		cout << "组装完成!\n" << endl;
	}
};

int main() {

	Computer computer;
	computer.doWork(new CPU1,new GPU1,new memory1);
	computer.doWork(new CPU2, new GPU2, new memory2);
	computer.doWork(new CPU3, new GPU1, new memory3);

	system("pause");
	return 0;
}

结果:

开始组装电脑:
Intel牌CPU开始计算了
NVIDIA牌GPU开始显示了
ADATA牌内存条开始存储
组装完成!

开始组装电脑:
AMD牌CPU开始计算了
AMD牌GPU开始显示了
USCORSAIR牌内存条开始存储
组装完成!

开始组装电脑:
IBM牌CPU开始计算了
NVIDIA牌GPU开始显示了
Kingston牌内存条开始存储
组装完成!

请按任意键继续. . .

5、文件操作

文件操作的头文件#include<fstream>

文件类型:
文本文件:以ASCII码的形式存在计算机中
二进制文件:以二进制(0和1)的形式存在计算机中

操作文件的三大类;

写操作:ofstream(output file stream)输出是写;`写出来`
读操作:ifstream(input file stream)输入是读;`进去读`
读写操作:fstream,可读可写。

5.1 文本文件
5.1.1 写文件
写文件步骤如下:

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

其中,第三步的文件打开方式包括以下几种:

| 打开方式     | 解释                      |
| ios::in     | 为读文件而打开文件         |
| ios::out    | 为写文件而打开文件         |
| ios::ate    | 初始位置:文件尾           |
| ios::app    | 追加方式写文件             |
| ios::trunc  | 如果文件存在先删除,再创建  |
| ios::binary | 二进制方式                 |

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

5.1.2 读文件
读文件步骤如下:

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

第四步中的四种读取方式:

//读数据
	//方式1:
	//char buf[1024] = {0};
	//while (ifs >> buf)//右移运算符
	//	cout << buf << endl;
	//方式2
	//char buf[1024] = { 0 };
	//while (ifs.getline(buf, sizeof(buf)))
	//	cout << buf << endl;
	//方式3
	//string buf;
	//while (getline(ifs, buf))
	//	cout << buf << endl;
	//方式4(不推荐)
	//char c;
	//while ((c = ifs.get()) != EOF)//EOF == End of file
	//	cout << c;// <<endl  这种方法就不要这个回车符了

程序:

#include<iostream>
#include<fstream>//文件流
#include<string>
using namespace std;

int main() {
	
//写文件:①头文件②文件流对象③打开文件(路径+方式)④写数据⑤关闭文件
//创建流对象
	ofstream ofs;
//打开文件:包括文件路径和打开方式
	ofs.open("E:\\c++example\\0723\\Project1\\file_operating\\test.txt",ios::out);//
//写数据
	ofs << "今天是周四,天气晴,现在在学习c++中的文件操作。" <<endl;
	ofs << "测试完毕,over。" << endl;
//关闭文件
	ofs.close();


//读文件:①头文件②文件流对象③打开文件并判断文件是否打开成功④读数据⑤关闭文件
//创建流对象
	ifstream ifs;
//打开文件:包括文件路径和打开方式
	ifs.open("E:\\c++example\\0723\\Project1\\file_operating\\test.txt", ios::in);
//判断文件是否打开成功
	if (ifs.is_open() == 0) {
		cout << "文件打开失败!!!" << endl;
		return 0;
	}
//读数据
	//方式1:
	//char buf[1024] = {0};
	//while (ifs >> buf)//右移运算符
	//	cout << buf << endl;
	//方式2
	//char buf[1024] = { 0 };
	//while (ifs.getline(buf, sizeof(buf)))
	//	cout << buf << endl;
	//方式3
	//string buf;
	//while (getline(ifs, buf))
	//	cout << buf << endl;
	//方式4(不推荐)
	//char c;
	//while ((c = ifs.get()) != EOF)//EOF == End of file
	//	cout << c;// <<endl  这种方法就不要这个回车符了
	
//关闭文件
	ifs.close();

	system("pause");
	return 0;
}

5.2 二进制文件
5.2.1 写文件
二进制方式写文件主要利用流对象调用成员函数write。函数原型 :ostream& write(const char * buffer,int len); 参数解释:字符指针buffer指向内存中一段存储空间。len是读写的字节数
例如:
//int nNum = 20; fout.write((char*)&nNum, sizeof(int));
//string str("Hello, world"); fout.write(str.c_str(), sizeof(char) *(str.size()));
5.2.2 读文件
二进制方式读文件主要利用流对象调用成员函数read。函数原型:istream& read(char *buffer,int len); 参数解释:字符指针buffer指向内存中一段存储空间。len是读写的字节数
例如:
//如果是整形需转成(char*)//int nNum1;fin.read((char*)&nNum1, sizeof(int));
//如果是字符串//char szBuf[256] = { 0 };fin.read(szBuf,sizeof(char) * 256);

程序(再写一遍再补充过来):

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

int main() {

	
/*文本文件 //"E:\\c++example\\0723\\Project1\\file_operating\\test2.txt"
	//写out
	ofstream ofs("E:\\c++example\\0723\\Project1\\file_operating\\test2.txt",ios::out);
	if (!ofs.is_open()) {
		cout << "打开文件错误!!!" << endl;
		return 0;
	}
		
	ofs << "文件操作第二遍\nover" << endl;
	ofs.close();

	//读in
	ifstream ifs("E:\\c++example\\0723\\Project1\\file_operating\\test2.txt", ios::in);
	if (!ifs.is_open()) {
		cout << "打开文件错误!!!" << endl;
		return 0;
	}
	//读取方式3
	string str;
	while (getline(ifs, str))
		cout << str << endl;
	ifs.close();
*/
//二进制文件 //"E:\\c++example\\0723\\Project1\\file_operating\\test3.txt"
	//写out
	ofstream ofs("E:\\c++example\\0723\\Project1\\file_operating\\test3.txt",ios::out | ios::binary);
	if (!ofs.is_open()) {
		cout << "文件打开错误" << endl;
		return 0;
	}
	//写数据
	int num = 10;
	string name = "abcdefg";
	ofs.write((const char*)&num, sizeof(int));
	ofs.write(name.c_str(),sizeof(char)*name.size());
	ofs.close();
	
	//读in
	ifstream ifs("E:\\c++example\\0723\\Project1\\file_operating\\test3.txt", ios::in | ios::binary);
	if (!ifs.is_open()) {
		cout << "文件打开错误" << endl;
		return 0;
	}

	//读数据
	int num1;
	//string str;
	char buf[256] = { 0 };
	ifs.read((char*)&num1, sizeof(int));
	//ifs.read((char *)&str, sizeof(str));//sizeof(char) * str.size()
	ifs.read(buf, sizeof(buf));//
	cout << num1 << " " << buf << endl;

	ifs.close();


	system("pause");
	return 0;
}

总结:
文本文件读写:
写:
ofstream ofs(“E:\\c++example\\test2.txt”,ios::out);
if (!ofs.is_open()) {
cout << “打开文件错误!!!” << endl;
return 0;
}
//写数据
ofs << “文件操作第二遍\nover” << endl;
ofs.close();
读:
ifstream ifs(“E:\\c++example\\test2.txt”, ios::in);
if (!ifs.is_open()) {
cout << “打开文件错误!!!” << endl;
return 0;
}
//读取方式3
//string str;
//while (getline(ifs, str))
// cout << str << endl;
ifs.close();

二进制文件读写:
写:
ofstream ofs(“E:\\c++example\\test3.txt”,ios::out | ios::binary);
if (!ofs.is_open()) {
cout << “文件打开错误” << endl;
return 0;
}
//写数据
//int num = 10;
//string name = "abcdefg";
//ofs.write((const char*)&num, sizeof(int));
//ofs.write(name.c_str(),sizeof(char)*name.size());
ofs.close();
读:
ifstream ifs(“E:\\c++example\\test3.txt”, ios::in | ios::binary);
if (!ifs.is_open()) {
cout << “文件打开错误” << endl;
return 0;
}
//读数据

int num1;
	//string str;错误!!!
	char buf[256] = { 0 };
	ifs.read((char*)&num1, sizeof(int));
	//ifs.read((char *)&str, sizeof(str));//sizeof(char) * str.size()错误!!!
	ifs.read(buf, sizeof(buf));//
	cout << num1 << " " << buf << endl;
ifs.close();

6 基于多态的企业职工系统资料
职工管理系统可以用来管理公司内所有员工的信息

公司中职工分为三类:普通员工、经理、老板,显示信息时,需要显示职工编号、职工姓名、职工岗位、以及职责。

普通员工职责:完成经理交给的任务
经理职责:完成老板交给的任务,并下发任务给员工
老板职责:管理公司所有事务

管理系统中需要实现的功能如下:

  • 退出管理程序:退出当前管理系统
  • 增加职工信息:实现批量添加职工功能,将信息录入到文件中,职工信息为:职工编号、姓名、部门编号
  • 显示职工信息:显示公司内部所有职工的信息
  • 删除离职职工:按照编号删除指定的职工
  • 修改职工信息:按照编号修改职工个人信息
  • 查找职工信息:按照职工的编号或者职工的姓名进行查找相关的人员信息
  • 按照编号排序:按照职工编号,进行排序,排序规则由用户指定
  • 清空所有文档:清空文件中记录的所有职工信息 (清空前需要再次确认,防止误删)

添加职工:
功能:
批量添加职工,并且保存到文件中。
分析:
批量添加职工时,可能会创建不同种类的职工;
如何将不同种类的职工放到一个数组中?可以将所有员工的指针维护到一个数组中,即new的每个子类对象返回的指针放在一个指针数组里;
如果想在程序中维护这个长度不定的指针数组,可以将数组创建到堆区,并利用abstractWorker **的指针维护。
如果想在程序中维护这个长度不定的指针数组,可以将数组创建到堆区,并利用abstractWorker **的指针维护

(补充)指针数组 & 数组指针:
首先需要明确一个优先级顺序:()>[]>*,所以:
(*p)[n]:根据优先级,先看括号内,则p是一个指针,这个指针指向一个一维数组,数组长度为n,这是“数组的指针”,即数组指针
*p[n]:根据优先级,先看[],则p是一个数组,再结合 *,这个数组的元素是指针类型,共n个元素,这是“指针的数组”,即指针数组

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值