2 - C++ 核心编程

​ 主要是 C++ 面向对象编程技术。

一、内存分区模型

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

  • 代码区 :存放函数体的二进制代码,由操作系统进行管理。

  • 全局区 :存放全局变量和静态变量以及常量。

  • 栈区 :由编译器自动分配释放,存放函数的参数值,局部变量等。

  • 堆区 :由程序员分配和释放,若程序员不释放,程序结束时由系统操作回收。

内存四区意义

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

1、变量作用域

一般来说有三个地方可以定义变量:

  • 在函数或一个代码块内部声明的变量,称为局部变量

  • 在函数参数的定义中声明的变量,称为形式参数

  • 在所有函数外部声明的变量,称为全局变量

作用域是程序的一个区域,变量的作用域可以分为以下几种:

  • 局部作用域 :在函数内部声明的变量具有局部作用域,它们只能在函数内部访问。局部变量在函数每次被调用时被创建,在函数执行完后被销毁。
  • 全局作用域 :在所有函数和代码块之外声明的变量具有全局作用域,它们可以被程序中的任何函数访问。全局变量在程序开始时被创建,在程序结束时被销毁。
  • 块作用域 :在代码块内部声明的变量具有块作用域,它们只能在代码块内部访问。块作用域变量在代码块每次被执行时被创建,在代码块执行完后被销毁。
  • 类作用域 :在类内部声明的变量具有类作用域,它们可以被类的所有成员函数访问。类作用域变量的生命周期与类的生命周期相同。

2、程序运行前

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

代码区

​ 存放CPU执行的机器指令。

​ 代码区是 共享 的,共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可。

​ 代码区是 只读 的,使其只读的原因是防止程序意外的修改了指令。

全局区

​ 全局变量和静态变量存放在此。

​ 全局区还包含了常量区,字符串常量和其它常量也存放于此。

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

实例

#include<iostream>
using namespace std;

//全局变量
int g_a = 10;
int g_b = 20;

//const修饰的全局变量(全局常量)
const int c_g_a = 10;
const int c_g_b = 20;

int main() {

	//局部变量
	int a = 10;
	int b = 20;
	cout << "局部变量a的地址为:" << (int)&a << endl;		//-803472732
	cout << "局部变量b的地址为:" << (int)&b << endl;		//-803472700

	//全局变量
	cout << "全局变量g_a的地址为:" << (int)&g_a << endl;		//778162176
	cout << "全局变量g_b的地址为:" << (int)&g_b << endl;		//778162180

	//静态变量
	//在变量之前加一个static即为静态变量
	static int s_a = 10;
	static int s_b = 20;
	cout << "静态变量s_a的地址为:" << (int)&s_a << endl;		//778162184
	cout << "静态变量s_b的地址为:" << (int)&s_b << endl;		//778162188

	//常量
	//字符串常量
	cout << "字符串常量的地址为:" << (int)&"hello world" << endl;		//778152904

	//const修饰的常量
	//const修饰的全局变量(全局常量)
	cout << "const修饰的全局变量(全局常量)c_g_a的地址为:" << (int)&c_g_a << endl;		//778153064
	cout << "const修饰的全局变量(全局常量)c_g_b的地址为:" << (int)&c_g_b << endl;		//778153068

	//const修饰的局部变量(局部常量)
	const int c_l_a = 10;
	const int c_l_b = 20;
	cout << "const修饰的局部变量(局部常量)c_l_a的地址为:" << (int)&c_l_a << endl;		//-803472668
	cout << "const修饰的局部变量(局部常量)c_l_b的地址为:" << (int)&c_l_b << endl;		//-803472636

	system("pause");
	return 0;
}

总结

  • C++ 在程序运行前分为全局区和代码区。

  • 代码区的特点是共享和只读。

  • 全局区存放全局变量、静态变量、常量。

  • 常量区存放 const 修饰的全局常量和字符串常量。

3、程序运行后

栈区

​ 由编译器自动分配释放,存放函数的参数值,局部变量等。

注意 :不要返回局部变量的地址,栈区开辟的数据由编译器自动释放。

实例

#include<iostream>
using namespace std;

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

int* func(int b) {		//形参数据也会存放在栈区

	b = 100;
	int a = 10;		//局部变量:存放在栈区,栈区的数据在函数执行完后自动释放
	return &a;		//返回局部变量的地址
}

int main() {

	int* p = func(100);

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


	system("pause");
	return 0;
}

堆区

​ 由 程序员 分配释放,若程序员不释放,程序结束时由系统操作回收。

​ 在 C++ 中主要利用 new 在堆区开辟内存。

实例

#include<iostream>
using namespace std;

int* func() {

	//利用new关键字,可以将数据开辟到堆区
	//指针本质也是局部变量,放在栈上,并指向堆区
	int* p = new int(10);
	return p;
}
int main() {

	int* p = func();

	cout << *p << endl;		//10
	cout << *p << endl;		//10

	system("pause");
	return 0;
}

二、static 的用法全局变量与局部变量

1、什么是 static?

staticC/C++ 中很常用的修饰符,它被用来控制变量的存储方式和可见性。

2、static 的引入

​我们知道在函数内部定义的变量,当程序执行到它的定义处时,编译器为它在栈上分配空间,函数在栈上分配的空间在此函数执行结束时会释放掉,这样就产生了一个问题: 如果想将函数中此变量的值保存至下一次调用时,如何实现? 最容易想到的方法是定义为全局的变量,但定义一个全局变量有许多缺点,最明显的缺点是破坏了此变量的访问范围(使得在此函数中定义的变量,不仅仅只受此函数控制)。static 关键字则可以很好的解决这个问题。

​另外,在 C++ 中,需要一个数据对象为整个类而非某个对象服务,同时又力求不破坏类的封装性,即要求此成员隐藏在类的内部,对外不可见时,可将其定义为静态数据。

3、在 C/C++static 的作用

  • 在修饰变量的时候,static 修饰的静态局部变量只执行初始化一次,而且延长了局部变量的生命周期,直到程序运行结束以后才释放。
  • static 修饰全局变量的时候,这个全局变量只能在本文件中访问,不能在其它文件中访问,即便是 extern 外部声明也不可以。
  • static 修饰一个函数,则这个函数的只能在本文件中调用,不能被其他文件调用。static 修饰的变量存放在全局数据区的静态变量区,包括全局静态变量和局部静态变量,都在全局数据区分配内存。初始化的时候自动初始化为 0。
  • 不想被释放的时候,可以使用 static 修饰。比如修饰函数中存放在栈空间的数组。如果不想让这个数组在函数调用结束释放可以使用 static 修饰。
  • 考虑到数据安全性(当程序想要使用全局变量的时候应该先考虑使用 static)。

4、静态变量与普通变量

静态全局变量有以下特点

  1. 静态变量都在全局数据区分配内存,包括后面将要提到的静态局部变量。

  2. 未经初始化的静态全局变量会被程序自动初始化为 0(在函数体内声明的自动变量的值是随机的,除非它被显式初始化,而在函数体外被声明的自动变量也会被初始化为 0)。

  3. 静态全局变量在声明它的整个文件都是可见的,而在文件之外是不可见的。

    优点 :静态全局变量不能被其它文件所用;其它文件中可以定义相同名字的变量,不会发生冲突。

全局变量和全局静态变量的区别

  1. 全局变量是不显式用 static 修饰的全局变量,全局变量默认是有外部链接性的,作用域是整个工程,在一个文件内定义的全局变量,在另一个文件中,通过 extern 全局变量名的声明,就可以使用全局变量。

  2. 全局静态变量是用 static 修饰的全局变量,作用域是声明此变量所在的文件,其他的文件即使用 extern 声明也不能使用。

5、静态局部变量特点

  1. 该变量在全局数据区分配内存。

  2. 静态局部变量在程序执行到该对象的声明处时被首次初始化,即以后的函数调用不再进行初始化。

  3. 静态局部变量一般在声明处初始化,如果没有显式初始化,会被程序自动初始化为 0。

  4. 它始终驻留在全局数据区,直到程序运行结束。但其作用域为局部作用域,当定义它的函数或语句块结束时,其作用域随之结束。

​一般程序把新产生的动态数据存放在堆区,函数内部的自动变量存放在栈区。自动变量一般会随着函数的退出而释放空间,静态数据(即使是函数内部的静态局部变量)也存放在全局数据区。全局数据区的数据并不会因为函数的退出而释放空间。

三、new操作符

C++ 中利用 new 操作符在堆区开辟数据。

​ 堆区开辟的数据,由程序员手动开辟,手动释放,释放利用操作符delete

语法new 数据类型

​ 利用new创建的数据,会返回该数据对应数据类型的指针。

实例1 :释放变量指针

#include<iostream>
using namespace std;

//创建一个new函数
int* func() {
	int* p = new int(10);
	return p;
}

void test01() {

	int* p = func();
	cout << *p << endl;
	cout << *p << endl;
	cout << *p << endl;

	delete p;

	//cout << *p << endl;

	/*
	
		前三次可以正常打印,但是当程序员手动释放p之后,就不可以打印了,此时p没有指向,如果打印,属于非法操作
		
	*/

}
int main() {

	test01();


	system("pause");
	return 0;
}

实例2 :释放数组指针

#include<iostream>
using namespace std;

void test02() {

	int* arr = new int[10];

	for (int i = 0; i < 10; i++) {
		arr[i] = i + 100;
	}

	for (int i = 0; i < 10; i++) {
		cout << arr[i] << endl;
	}

	delete[] arr;		//在释放数组的时候,需要在delete之后加上[],以此来说明释放的是一个数组。

}
int main() {

	test02();
    
	system("pause");
	return 0;
}

四、引用

1、引用的基本使用

作用 :给变量起别名,即两个不同的变量名操作(指向)同一块内存空间,当使用其中一个变量操作这块内存空间时,相应的另外的变量也会随之改变。

语法数据类型 &别名 = 原名

实例

#include<iostream>
using namespace std;

int main() {

	int a = 10;
	int& b = a;

	cout << "未使用b修改前a = " << a<<endl;		//10
	cout << "未使用b修改前b = " << b << endl;		//10

	b = 100;
	cout << "使用b修改后a = " << a<<endl;		//100
	cout << "使用b修改后b = " << b<<endl;		//100

	system("pause");
	return 0;
}

2、引用注意事项

  • 引用必须 初始化

  • 引用在初始化后,不可以改变

实例

#include<iostream>
using namespace std;

int main() {

	int a = 10;

	//1、引用必须初始化
		//int& b;		//此处引用b未进行初始化

	int& b = a;
	int c = 20;

	//int& b = c;		//b重定义:多次初始化,错误

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

	system("pause");
	return 0;
}

3、引用做函数参数

作用 :函数传参时,可以利用引用的技术让形参修饰实参。

优点 :可以简化指针修改实参。

实例

#include<iostream>
using namespace std;

//值传递
void swap01(int a, int b) {
	int temp = 0;
	temp = a;
	a = b;
	b = temp;

	cout << "在值传递内部执行交换之后的结果为:" << endl;
	cout << "a = " << a <<  "  b=" << b << endl;
}

//地址传递
void swap02(int* pa,int* pb) {
	int temp = 0;
	temp = *pa;
	*pa = *pb;
	*pb = temp;

	cout << "在地址传递内部执行交换之后的结果为:" << endl;
	cout << "a = " << *pa << "  b=" << *pb << endl;
}

//引用传递
void swap03(int& a,int& b) {

	int temp = 0;
	temp = a;
	a = b;
	b = temp;
}

int main() {

	int a = 10;
	int b = 20;
	
	cout << "值传递之前,a = 10,b = 20" << endl;
	swap01(a, b);
	cout << "值传递完成之后,在函数外部的结果为:" << endl;
	cout << "a = " << a << "  b=" << b << endl;
	//a = 10,b = 20;可见,值传递只会堆函数内部造成影响,不会对函数外部造成影响,即值传递相当于拷贝了一份,并对拷贝的这份进行操作

	swap02(&a,&b);
	cout << "地址传递完成之后,在函数外部的结果为:" << endl;
	cout << "a = " << a << "  b=" << b << endl;
	//a = 20,b = 10;可见,地址传递的时候,函数内部的运行结果对函数外部是有影响的,即使用指针操作同一片空间

	swap03(a, b);
	cout << "引用传递完成之后,在函数外部的结果为:" << endl;
	cout << "a = " << a << "  b=" << b << endl;
	//a = 20,b = 10;可见,引用传递的时候,函数内部的运行结果对函数外部是有影响的,效果与指针是相同的

	system("pause");
	return 0;
}

4、引用做函数的返回值

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

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

用法 :函数调用作为左值。

#include<iostream>
using namespace std;

//引用做函数的返回值
//1、不要返回局部变量的引用
int& test01(){
	int a = 10;		//局部变量存放在四区中的栈区,在函数使用完之后自动释放
	return a;
}

//函数的调用可以用为左值
int& test02() {
	static int a = 10;		//static 静态变量,将局部变量a作为一个静态变量,放在全局区,就不会在执行完之后自动释放,这是由系统释放的。
	return a;
}
int main() {

	int& ref1 = test01();
	cout << "ref1 = " << ref1 << endl;		//ref1 = -858993460,垃圾值

	int& ref2 = test02();
	cout << "ref2 = " << ref2 << endl;		//ref2 = 10

	test02() = 100;
	cout << "ref2 = " << ref2 << endl;
	//ref2 = 100,函数调用作为左值,可以进行赋值操作,可以将这行代码理解为int& a =100;而21行代码里面是int& ref2 = a;此处a的数据类型为引用

	system("pause");
	return 0;
}

5、引用的本质

本质 :引用的本质是一个指针常量。

实例

#include<iostream>
using namespace std;

//发现是引用,转换为int *const ref =&a;
void func(int& ref) {
	ref = 100;		//ref是引用,转换为*ref = 100
}
int main() {

	int a = 10;

	int& ref = a;	//编译器会自动将这行代码转换为int *const ref = &a;
	ref = 20;

	cout << "a = " << a << endl;		//a=20
	cout << "ref = " << ref << endl;		//ref = 20

	system("pause");
	return 0;
}

6、常量引用

作用 :常量引用主要用来修饰形参,防止误操作。

在函数形参列表中,可以加 const 修饰形参,防止形参改变实参。

实例

#include<iostream>
using namespace std;

void func(const int& val) {
	//val++;		//val被const修饰,所以无法进行修改
	//需要注意的是,由于引用本身是一个指针常量,所以它的指向是不可以修改的,但是此处同时将整体变为常量,所以这时指向和指向的值都不可修改
	//即const int& val = const int * const val
	//而当函数体形参部分没有加const的时候,这个形参所代表的实参值是可以改变的
	cout << val << endl;
}
int main() {

	int a = 10;
	func(a);

	system("pause");
	return 0;
}

五、函数提高

1、函数默认参数

C++ 中函数列表中的形参是可以有默认值的。

语法返回值类型 函数名(参数=默认值){}

注意

  1. 第一个默认参数值后面的所有形参都必须有默认值,即默认参数只能放在形参列表结尾。
  2. 函数的声明和定义两个只能有一个有默认参数。

实例

#include<iostream>
using namespace std;

int func(int a = 10, int b = 20) {
	return a + b;
}

//错误:第一个拥有默认参数的后面的形参都必须拥有默认参数
//int func(int a =10,int b,int c){
//	return a+b+c;
//}

int main() {

	cout << func() << endl;

	system("pause");
	return 0;
}

2、函数占位参数

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

语法返回值类型 函数名(数据类型){}

使用一个数据类型名称进行占位,在调用函数的时候需要传入一个对应数据类型的实参,否则函数无法被进行调用。

实例

#include<iostream>
using namespace std;

void func1(int a, int) {
	cout << "this is func!" << endl;
}
int main() {

	func1(10, 10);


	system("pause");
	return 0;
}

3、函数重载

函数重载概述

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

函数重载满足条件

  • 同一个作用域下。

  • 函数名称相同。

  • 函数参数 类型不同 或者 个数不同 或者 顺序不同

注意 :函数的返回值类型不可以作为函数重载的条件。

实例

#include<iostream>
using namespace std;

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(double a,int b) {
	cout << "func(double a,int b)的调用!" << endl;
}

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

int main() {

	func();			//func()的调用!
	func(10);		//func(int a)的调用!:个数不同可以作为重载条件
	func(3.14);		//func(double a)的调用!:传入的参数类型不同可以作为重载条件
	func(3.14, 10);	//func(double a,int b)的调用!
	func(10, 3.14);	//func(int a,double b)的调用!:传入参数的顺序不同可以作为重载条件

	system("pause");
	return 0;
}

函数重载注意事项

  • 引用作为重载条件

  • 函数重载碰到函数默认参数

实例

#include<iostream>
using namespace std;

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

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

void func4(int a,int b =10) {
	cout << "func(int a,int b =10)被调用!" << endl;
}

void func4(int a) {
	cout << "func(int a)被调用!" << endl;
}

int main() {

	int a = 10;
	//func4(a);			//func4(int& a)被调用!
	//func4(10);		//func4(const int& a)被调用!

	//func4(10);		//不可以调用,因为这行代码在运行的时候,编译器无法确定是选择“func4(int 							a,int b =10)”还是“func4(int a)”
	func4(10, 10);		//func(int a,int b =10)被调用!

	system("pause");
	return 0;
}

六、类和对象

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

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

1、封装

封装的意义

  • 将属性和行为作为一个 整体,表现生活中的事物。

  • 将属性和行为加以权限控制。

封装意义一

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

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

实例1 :设计一个圆类,求圆的周长

#include<iostream>
using namespace std;

/*

	在设计类的时候,属性和行为写在一起,表现事物
	语法:class	类名{	访问权限:	属性/行为	};


*/
const double pi = 3.14;

class Circle {

	//1、权限
public:

	//2、属性
	int m_r;

	//3、行为
	double calculateZC() {
		return 2 * pi * m_r;
	}
};
int main() {

	//设计一个圆类,求圆的周长

	Circle c1;		//创建一个实例对象
	c1.m_r = 10;

	cout << "圆的周长为:" << c1.calculateZC() << endl;

	system("pause");
	return 0;
}

封装意义二

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

  • public :公共权限:成员类内可以访问,类外也可以访问。

  • protected :保护权限:成员类内可以访问,类外不可以访问,(继承中子类可以访问父类的保护权限)。

  • private :私有权限:成员类内可以访问,类外不可以访问。(继承中子类不可以访问父类的保护权限)。

实例

#include<iostream>
using namespace std;

class Person {

public:
	string name;
	
protected:
	string car;

private:
	int password;

public:		//在类内所有权限都可以访问到
	void func() {
		name = "张三";
		car = "奔驰";
		password = 123456;
	}
};

int main() {

	Person p1;
	p1.name = "张三";		//类外可以访问public公有权限
	//p1.car = "大众";			//类外不可以访问protected保护权限
	//p1.password = 1234567;	//类外不可以访问private私有权限

	system("pause");
	return 0;
}

structclass 的区别

  • struct :默认权限为公有。

  • class :默认权限为私有。

实例

#include<iostream>
using namespace std;

class C1 {
	int m_c1;		//class:默认权限为私有
};

struct C2{
	int m_c2;		//struct:默认权限为公有
};

int main() {

	C1 c1;
	//c1.m_c1 = 100;		//无法访问

	C2 c2;
	c2.m_c2 = 100;			//可以访问

	system("pause");
	return 0;
}

成员属性设置为私有

优点1 :将所有成员属性设置为私有,可以自己控制读写权限。

优点2 :对于写权限,我们可以检测数据的有效性。

实例

#include<iostream>
using namespace std;

class Person {
	//设置私有属性
private:
	int p_age;			//年龄
	string p_name;		//姓名
	string p_lover;		//情人

/*
	因为上述属性是私有的,在外部是无法进行访问的,但是如果全部设为私有,外部无法访问,这些私有属性就没有意义,因此需要一些公有权限接口来可以对这些属性进行一些操作,有可读可写,可读但不可写,可写但不可读.
*/
public:
	//对姓名可以进行可读可写操作
	//写入姓名
	void setname() {
		cout << "请输入用户名字:" << endl;
		string user_name;
		cin >> user_name;
		p_name = user_name;
	}

	//读取姓名
	string getname() {
		return p_name;
	}

	//对年龄只能进行读取操作
	int getage() {
		int age = 10;
		p_age = age;
		return p_age;
	}

	//对情人只能进行写入操作
	void setLover() {
		cout << "请输入情人名字:" << endl;
		string lover_name;
		cin >> lover_name;
		p_lover = lover_name;
	}

};
int main() {

	Person p1;
	p1.setname();
	cout << "用户姓名为:" << p1.getname() << endl;

	cout << "用户年龄为:" << p1.getage() << endl;

	p1.setLover();
	//cout << "用户情人为:" << p1.gerLover() << endl;		//并未对获取情人设置接口

	system("pause");
	return 0;
}

2、对象的初始化和清理

  • 生活中买的电子产品基本都会有 出厂设置,在某一天不用的时候也会删除一些自己的信息保证数据安全。

  • C++ 中的面向对象来自于生活,每个对象也会有初始化设置以及对象销毁前的清理数据的设置。

构造函数和析构函数

对象的 初始化和清理 也是两个非常重要的安全问题。一个对象或者变量没有初始状态,对其使用后果是未知,同样的使用完一个对象或者是变量,没有及时清理,也会造成一定的安全问题。

C++ 利用了 构造函数析构函数 解决上述问题,这两个函数会被编译器自动调用,完成对象初始化和清理工作。对象的初始化和清理工作是编译器强制要求做的事情,因此如果我们不提供构造和析构,编译器会提供。

编译器提供的构造函数和析构函数是空实现。

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

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

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

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

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

实例 :

#include<iostream>
using namespace std;

class Person {
public:
	//1、构造函数:类名(){}
	Person() {
		cout << "构造函数被调用!" << endl;
	}

	//2、析构函数:~类名(){}
	~Person() {
		cout << "析构函数被调用!" << endl;
	}
};

void test01() {
	Person p;		//作用在栈区的一个局部变量,因此在函数运行完之后会被自动释放
}

int main() {

	test01();		//构造函数被调用!
					//析构函数被调用!

	system("pause");
	return 0;
}

构造函数的分类以及调用

两种分类方式:

  • 参数 分为:有参构造和无参构造。
  • 类型 分为:普通构造和拷贝构造。

三种调用方式:

  • 括号法
  • 显示法
  • 隐式转换法

实例

#include<iostream>
using namespace std;

//分类
class Person {
public:

//1、无参构造(默认构造)
	Person() {
		cout << "无参构造(默认构造)被调用!" << endl;
	}

//2、有参构造:可以在构造的时候传递参数,以进行对对象的赋初值
	Person(int a) {
		age = a;
		cout << "有参构造被调用!" << endl;
	}

//3、拷贝构造:
/*
	拷贝构造同时也是一个有参构造,参数为另一个类对象,作用是将传入的参数作为新建对象的模板进行一份拷贝,由于不可以改变原有对象,因此需要添加const,且传递使用引用传递
*/

	Person(const Person& p) {
		cout << "拷贝构造被调用!" << endl;
	}

	~Person() {
		cout << "无参解析被调用!" << endl;
	}

private:
	int age=0;
};

//调用
void test02() {

//1、括号法
	Person p1;			//默认构造的调用,需要注意此处不加括号,否则会被认为是一个函数声明
	Person p2(10);		//有参构造的调用
	Person p3(p2);		//拷贝构造的调用:将p2作为p3的模板进行一份拷贝,此时p3的年龄与p2一样是10

//2、显示法
/*
	如果只有Person(10)这一行代码,此时其实已经进行过一次实例化对象了,但并未给这个对象一个名称,所以这个对象是匿名对象,编译器会在该行代码执行完毕之后,直接释放这个匿名对象
	不要利用拷贝构造函数进行初始化匿名对象,编译器会认为Person (p6) ===Person p6,是一个重定义操作,是一个对象声明。
*/
	Person p4;
	Person p5 = Person(10);
	Person p6 = Person(p5);

	//隐式转换法
	Person p7 = 10;		//相当于Person p7 = Person(10);
	Person P8 = p7;		//相当于Person p8 = Person(p7);

}

int main() {

	test02();

	system("pause");
	return 0;
}

构造函数调用规则

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

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

构造函数调用规则如下:

  • 如果用户定义有参构造函数,C++ 不再提供默认构造函数,但会提供默认拷贝构造。
  • 如果用户定义默认拷贝构造,C++ 不再提供其它构造函数。
  • 即拷贝构造函数>有参构造函数>默认构造函数,用户从中挑选定义,定义之后,该构造函数之后的 C++ 不会再默认提供,而之前的 C++ 依然会默认提供。

实例

#include<iostream>
using namespace std;

class Person {

public:
	//1、无参(默认)构造
	Person() {
		cout << "默认构造函数被调用!" << endl;
	}

	//2、有参构造
	Person(int a) {
		s_age = a;
		cout << "有参构造函数被调用!" << endl;
	}

	//3、拷贝函数构造
	Person(const Person& p) {
		cout << "拷贝函数被调用!" << endl;
	}

	//析构函数
	~Person() {
		cout << "析构函数被调用!" << endl;
	}

	int s_age;
};

void test03() {
	Person p;
	Person p1(10);	//在定义好有参构造的时候,类内就不再提供默认构造了,假如将上述类里面自己设置的默认构造注释掉,那么32行代码就会报错。

	Person p2(p1);	//假如将上面类里面定义好的拷贝函数注释掉,该代码也可正常运行,这是因为即使类中自己没有定义拷贝函数,编译器也会自动定义一个

}
int main() {

	test03();

	system("pause");
	return 0;
}

浅拷贝与深拷贝

假如在函数内部有由 new 辟出来的空间,那么由于 new 出来的空间需要程序员手动释放,而我们在进行拷贝构造函数的时候,假如使用的是编译器自带的默认拷贝构造函数,此时是浅拷贝,那么就会造成两个不同的对象有一个指向同一 new 空间的指针,而析构函数也就是手动释放内存空间的时候,由于局部变量是在栈中,因此拷贝出来的新的对象先被析构,而在析构的时候会将指向的 new 空间进行释放,在原对象进行析构的时候,也会对这个 new 出来的空间进行释放,就会造成内存的重复释放,为了解决这个问题,就需要自己进行拷贝构造函数,进行深拷贝,注意第19行代码,此时也就是重新 new 一个空间进行深拷贝。

实例 :

#include <iostream>
using namespace std;

class Person {
public:
	Person() {	/*默认构造函数*/
		cout << "Person默认构造函数的调用" << endl;
	}
	Person(int age, int height) {	/*有参构造函数*/
		m_age = age;
		m_height = new int(height);	//在堆区开辟内存
		cout << "Person有参构造函数的调用" << endl;
	}
	//拷贝构造函数  
	Person(const Person& p) {
		cout << "Person拷贝构造函数!" << endl;
		//如果不利用深拷贝在堆区创建新内存,会导致浅拷贝带来的重复释放堆区问题
		m_age = p.m_age;
		m_height = new int(*p.m_height);
	}
	~Person() {
		if (m_height != NULL) {
			delete m_height;
			m_height = NULL;
		}
		cout << "Person析构函数的调用" << endl;
	}
	int m_age;	//年龄
	int* m_height;	//体重
};

void test04(void) {
	Person p1(18, 160);
	cout << "p1的年龄是" << p1.m_age << "体重是" << *p1.m_height << endl;
	Person p2(p1);
	cout << "p2的年龄是" << p2.m_age << "体重是" << *p2.m_height << endl;

}
int main(){
	test04();

	system("pause");
	return 0;
}

初始化列表

作用C++ 提供了初始化列表语法,用来初始化属性。

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

实例

#include<iostream>
using namespace std;

class Person5 {
public:

	//传统初始化
	//Person5(int a,int b, int c) {
	//	s_A = a;
	//	s_B = b;
	//	s_C = c;
	//}

	//初始化列表
	Person5(int a, int b, int c) :s_A(a), s_B(b), s_C(c) {
		
	}

	int s_A;
	int s_B;
	int s_C;

};

void test05() {

	//Person5 p(10,20,30);
	Person5 p(30,20,10);
	cout << "s_A = " << p.s_A << endl;
	cout << "s_B = " << p.s_B << endl;
	cout << "s_C = " << p.s_C << endl;

}

int main() {

	test05();

	system("pause");
	return 0;
}

类对象作为类成员

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

例如:

class A{};
class B{
    A a;
}

B 类中有对象 A 作为成员,A 为对象成员。

当其他类对象作为本类成员,构造时候先构造类对象,再构造自身。

实例

#include<iostream>
using namespace std;

class Phone {
public:
	//手机名字
	Phone(string pname) {
		cout << "Phone的构造函数被调用" << endl;
		p_name = pname;
	}

	string p_name;
};

class Person6 {
public:

	Person6(string mname, string pname) :m_name(mname), m_phone(pname) {
		cout << "Person构造函数被调用" << endl;
		cout << m_name << "拿着: " << m_phone.p_name << endl;
	}

	//本人姓名
	string m_name;

	//手机
	Phone m_phone;
};

void test06() {
	Person6 p("张三", "苹果MAX");
}

int main() {

	test06();

	system("pause");
	return 0;
}

静态成员

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

静态成员分为:

  • 静态成员变量所有对象共享同一份数据 ,意思就是,在用这个类创建多个对象的时候,这个静态变量是所有对象共享的,并不是独属于某个对象,假如某个对象对这个变量进行更改,那么其他对象在访问这个变量的时候,就会变成更改之后的数值。即在创建对象的时候,这个变量只会分配一块内存来进行存储变量,而不会每创建一个对象,就会给这个变量分配一块内存。

    • 在编译阶段分配内存。
    • 类内声明,类外初始化。
  • 静态成员函数

    • 所有对象共享同一个函数。
    • 静态成员函数只能访问静态成员变量。

实例1 :静态成员变量

#include<iostream>
using namespace std;

class Person7 {

public:
	
	//静态成员变量,所有对象共享的一个变量。
	//即在创建对象的时候,这个变量只会分配一块内存来进行存储变量,而不会每创建一个对象,就会给这个变量分配一块内存。
	//需要在类内进行声明。
	static int s_A;

private:
	static int s_B;		//静态成员变量有访问权限
};

int Person7::s_A = 100;		//Person7内的静态变量 s_A ,在类外进行初始化
int Person7::s_B = 200;
void test07() {
	Person7 p;
	cout << p.s_A << endl;

	Person7 p1;
	p1.s_A = 200;		//利用p1对静态变量进行修改
	cout << p.s_A << endl;		//利用p对静态变量进行访问
}

void test007() {

	//访问静态变量
	//1、利用对象对变量进行访问
	Person7 p;
	cout << p.s_A << endl;

	//2、利用作用域进行访问
	cout << Person7::s_A << endl;
}

int main() {

	//test07();
	test007();

	system("pause");
	return 0;
}

实例2 :静态成员函数

#include<iostream>
using namespace std;

class Person8 {
public:

	//静态成员函数
	static void func1() {
		m_A = 200;		//静态成员函数可以访问静态成员变量
		
		//静态成员函数无法访问非静态成员变量,这是因为无法确定访问的时候是哪个对象的m_B
		//m_B = 100;		
		cout << "static void func1()被调用" << endl;
	}

	static int m_A;		//静态成员变量
	int m_B;

private:
	//静态成员函数也有访问权限
	static void func2() {
		cout << "static void func2()被调用" << endl;
	}
};

int Person8::m_A = 100;

void test08() {

	//访问方式
	//1、创建对象进行访问
	Person8 p;
	p.func1();

	//2、直接使用作用域进行访问
	Person8::func1();
}
int main() {

	test08();

	system("pause");
	return 0;
}

3、C++ 对象模型和 this 指针

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

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

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

#include<iostream>
using namespace std;

class Person1 {
public:
	int s_A;	//非静态成员变量	属于类的对象上
	static int s_B;	//静态成员变量	不属于类的对象上
	void func() {}	//非静态成员函数	不属于类的对象上
	static void func1(){}	//静态成员函数	不属于类的对象上
};

int Person1::s_B = 200;

void test1() {

	//空对象p的占用内存空间为 1,
	//C++会给每个空对象也分配一个字节空间,这样是为了区分不同的空对象,每个空对象都有对应的不同的一个地址
	Person1 p;
	cout << "空对象p的占用内存空间:" << sizeof(p)<<endl;

	//添加s_A之后的内存空间:4
	Person1 p1;
	cout << "添加s_A之后的内存空间:" << sizeof(p1) << endl;

	//添加s_B之后的内存空间:4
	Person1 p2;
	cout << "添加s_B之后的内存空间:" << sizeof(p2) << endl;

	//添加 void func(){} 之后的内存空间:4
	Person1 p3;
	cout << "添加 void func(){}之后的内存空间:" << sizeof(p3) << endl;

	//添加 static void func1(){} 之后的内存空间:4
	Person1 p4;
	cout << "添加 static void func1(){} 之后的内存空间:" << sizeof(p4) << endl;

}

int main() {

	test1();

	system("pause");
	return 0;
}

this 指针概念

每一个非静态成员函数只会诞生一份函数实例,也就是说多个同类型的对象会公用一块代码,如何区分这个代码被哪个对象调用,就需要用到 this 指针。

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

this 指针是隐含在每一个非静态成员函数内的一种指针。

this 指针不需要定义,可以直接使用。

this 指针的用途

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

实例

#include<iostream>
using namespace std;

class Person2 {
public:
	//1、解决同名冲突
	Person2(int age) {
		this->age = age;		//这行代码的意思就是,这个对象本身(this)的属性age=传入的参数age
	}

	//2、返回对象本身使用*this
	Person2& Person2Addage(Person2& p) {
		this->age += p.age;
		return *this;
	}

	int age;
};

void test02(){

	Person2 p1(18);
	cout << p1.age << endl;

	Person2 p2(10);
	p2.Person2Addage(p1);
	cout << "p2.age=" << p2.age << endl;

	//为了使下述代码可以运行,则每次函数运行完返回的对象必须是这个对象本身,才可以继续调用该函数
	//链式编程思想
	p2.Person2Addage(p1).Person2Addage(p1).Person2Addage(p1);
	cout << "p2.age=" << p2.age << endl;
}

int main() {

	test02();

	system("pause");
	return 0;
}

空指针访问成员函数

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

如果用到 this 指针,则需要加以判断保证代码的健壮性。

实例

#include<iostream>
using namespace std;

class Person3 {
public:
	void showPerson3() {
		cout << "这是一个Person3类" << endl;
	}

	void showPerson3Age() {
		
		//首先进行判断,传入的指针是否为一个空指针,如果是则返回
		if (this == NULL) {
			return;
		}
		cout << "age = " << s_age << endl;
	}
	
	int s_age;
};

void test3() {
	Person3* p = NULL;

	//21行代码不会出错,这是因为访问的函数并无成员属性
	p->showPerson3();
	//22行代码会出错,这是因为在函数内部有一个s_age属性,而每个属性前面默认添加的this->。所以会出错
	p->showPerson3Age();
	
}

int main() {

	test3();

	system("pause");
	return 0;
}

const 修饰成员函数

常函数

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

  • 常函数内不可修改成员属性。

  • 成员属性声明时加关键字 mutable 后,在常函数中依然可以修改。

常对象

  • 声明对象前加 const 称该对象为常对象。
  • 常对象只能调用常函数。

实例

#include<iostream>
using namespace std;

class Person4 {
public:

	//在成员函数后面添加const,那么这个函数被称为常函数,常函数是不可以修改成员属性的
	//这是因为每个成员属性之前时默认添加的this->
	//this的本质是一个指针常量,它的指向是不可以更改的,而在添加const之后,const修饰this
	//会使this这个指针常量变为常指针常量,即 const Person4 * const this,即指针指向不可以更改,指针指向的数据也不可以进行更改
	void showAge()const {
		//s_age = 100;
		s_Age = 100;
	}

	void func() {};

	int s_age;
	mutable int s_Age;		//特殊变量,在前面加上mutable,就可以随时进行修改,即使是在常函数内部
};

void test4() {
	
	//常对象
	const Person4 p;
	//p.s_age = 100;		//常对象无法访问成员属性
	p.showAge();			//常对象可以访问常函数
	//p.func();			//常对象无法访问普通函数
}

int main() {

	test4();

	system("pause");
	return 0;
}

连续创建类对象

  • 由于对象是一片内存空间,所以此处需要使用 new 来创建数组。

  • 将每一次创建的类对象的指针存放到一个数组中 parr[i]=p

  • 再使用一个指针指向这个指针数组 class ** pparr=new class*[]

实例

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

class Person {
public:
	Person(string name, int age) {
		this->s_Name = name;
		this->s_Age = age;
	}

	string s_Name;
	int s_Age;
};

void test02() {
	vector<Person*> v;

	cout << "请输入指针数组长度:" << endl;
	int len=0;
	cin >> len;
	Person** Parr = new Person*[len];
	for (int i = 0; i < len; i++) {
		string name;
		int age;

		cout << "输入名字:" << endl;
		cin >> name;
		cout << "输入年龄:" << endl;
		cin >> age;

		Person* p = new Person(name,age);
		Parr[i] = p;

		v.push_back(p);
	}

	for (int i = 0; i < len; i++) {
		cout << Parr[i]->s_Name << endl;
	}

int main2() {

	test02();

	system("pause");
	return 0;
}

4、友元

在程序里,有些私有属性想让类外特殊的一些函数或类进行访问就需要用到友元的技术。

友元的目的就是让一个函数或者类访问另一个类种的私有成员。

关键字为 friend

友元的 三种实现

  • 全局函数做友元。

  • 类做友元。

  • 成员函数做友元。

全局函数做友元

实例

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

//建筑物类
class Building {

	friend void goodGay(Building& p);	//friend关键字,将类外需要访问私有属性的函数在类内进行friend声明,就可以访问私有属性

public:
	Building() {
		s_sittingRoom = "客厅";
		s_Bedroom = "卧室";
	}

public:
	string s_sittingRoom;

private:
	string s_Bedroom;
};

void goodGay(Building &p) {
	cout << "好基友正在访问:" << p.s_sittingRoom << endl;
	cout << "好基友正在访问:" << p.s_Bedroom << endl;
}

void test1() {
	Building b;
	goodGay(b);
}

int main() {

	test1();

	system("pause");
	return 0;
}

类做友元

实例

#include<iostream>
using namespace std;

class Building2;
class GoodGay {
public:
	void visit();
	Building2* building;
};

class Building2 {

	friend GoodGay;		//声明GoodGay是Building2的友元,可以访问本类的私有属性

public:
	Building2();

public:
	string s_sittingRoom;

private:
	string s_Bedroom;
};

//类外对类进行构造
Building2::Building2() {
	s_Bedroom = "卧室";
	s_sittingRoom = "客厅";
}

void GoodGay::visit() {
	building = new Building2;
	cout << "好基友正在访问:" << building->s_sittingRoom << endl;
	cout << "好基友正在访问:" << building->s_Bedroom << endl;
}

void test2() {
	GoodGay gg;
	gg.visit();
}

int main() {

	test2();

	system("pause");
	return 0;
}

成员函数做友元

实例

#include<iostream>
using namespace std;

class Building;
class GoodGay3 {
public:
	GoodGay3();
	void visit1();
	void visit2();
	Building* building;
};

class Building {

	friend void GoodGay3::visit1();		//声明GoodGay3作用域下的visit1()是友元函数

public:
	Building();
	
public:
	string s_sittingRoom;

private:
	string s_Bedroom;
};

//类外成员
Building::Building() {
	s_sittingRoom = "客厅";
	s_Bedroom = "卧室";
}

GoodGay3::GoodGay3() {
	building = new Building;
}

void GoodGay3::visit1() {
	cout << "visit1 正在访问:" << building->s_sittingRoom << endl;
	cout << "visit1 正在访问:" << building->s_Bedroom << endl;
}

void GoodGay3::visit2() {
	cout << "visit2 正在访问:" << building->s_sittingRoom << endl;
	//cout << "visit2 正在访问:" << building->s_Bedroom << endl;
}

void test3() {
	GoodGay3 gg;
	gg.visit1();
	gg.visit2();
}

int main() {

	test3();

	system("pause");
	return 0;
}

5、运算符重载

运算符重载概念 :对已有的运算符进行重新定义,赋予其另一种功能,以适应不同的数据类型。通算点理解就是,由于类内有多个属性,直接使用运算符,会使编译器无法确认需要运算的是哪个属性,当指向不明确的时候,就需要对运算符进行重载。

语法返回值类型 operator重载的运算符(参数1,参数2){}

加号运算符 (+) 重载

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

例如 :正常情况下,类对象与类对象不可以进行相加,但是重载“+”之后,就可以实现类对象之间的相加,并且此时返回值类型应该与参数数据类型,如果参数是类,咱返回值类型也为类。

理解 :“+”内置没有类与类的相加,因此当需要不同对象的属性可以进行相加的时候,就需要用到重载运算符。

  • 参数 :加号进行的是两个数据类型之间的运算,所以,重载加号的时候就需要有两个参数,以此来实现可以对自定义的类型进行相加。
  • 函数体 :内部代码就是想要对除内置数据类型之外的数据类型进行相加,比如一个类,可以创建多个对象,但却可以实现对象1的A属性可以与对象2的B属性进行相加。
  • 返回值 :可以发现,两个数据类型相同的进行相加,那么为了使链式编程思想的实现,在相加之后,所返回的结果,也应该与这两个数据类型相同,所以返回值类型为类。
  • 对38行代码的理解就是 :在运行 p1+p2 的时候,会跳到重载 “+” 函数,将 “+” 左边的 p1 与右边的 p2 作为参数传进去,再进入函数体内部进行运算,之后将结果进行返回,再用一个相同数据类型的变量进行接收。

实例

#include<iostream>
using namespace std;

class Person1 {
public:
	//成员内部进行 “+” 重载
	//Person1 operator+(Person1& p) {
	//	Person1 temp;
	//	temp.s_A = this->s_A + p.s_A;
	//	temp.s_B = this->s_B + p.s_B;
	//	return temp;
	//}
public:
	int s_A;
	int s_B;
};

//全局函数进行对 “+” 重载
Person1 operator+(Person1& p1,Person1&p2) {
	Person1 temp;
	temp.s_A = p1.s_A + p2.s_A;
	temp.s_B = p1.s_B + p2.s_B;
	return temp;
}

void test1() {
	Person1 p1;
	p1.s_A = 10;
	p1.s_B = 20;

	Person1 p2;
	p2.s_A = 20;
	p2.s_B = 20;

	//加号重载就是为了可以使38行代码得以实现
	//若使类内重载,其本质是:Person1 p3 = p1.operator+(p2)
	//若是全局函数重载,其本质是:Person1 p3 = operator+(p1,p2)
	Person1 p3 = p1 + p2;		
	cout << "p3.s_A = " << p3.s_A << "  p3.s_B = " << p3.s_B << endl;
}

int main() {

	test1();

	system("pause");
	return 0;
}

左移运算符 (<<) 重载

作用 :可以输出自定义数据类型。

例如 :正常情况下,无法直接输出一个类对象,这是因为类内有很多成员属性,直接输出会导致编译器无法确认输出内容是那个属性,而重载之后就可以实现直接输出类对象,返回值类型一般为 ostream。可以实现输出内容自定义。

理解 :同 “+” 一样,”<<“ 也是一个运算符,也是左右各有一个值,同样也是无法直接输出一个类对象,因为编译器无法确定需要输出类对象里面的哪个属性,因此需要对 "<<" 进行重载。需要注意的是,cout 并不是 c++ 里面的关键字,它是一个类对象,作用是将输出流之后的内容显示在屏幕上。

  • 参数 :需要有两个参数,一个是 cout,一个是类对象,cout 的数据类型为 ostream 是一个输出流类对象。

  • 函数体 :代码主要内容为想要输出类对象中的某个属性,那么在运行完之后,就可以进行类对象的输出,即此时进行的输出,就会给输出一个具体的值,让编译器知道是想输出什么。

  • 返回值 :同样的,为了实现链式编程,在运算完之后的结果应该返回一个 ostream 类的对象,返回值类型为类引用,这是因为 cout 作为一个全局对象,只需要一个就可以,不需要每次都创建一个 cout 对象。

  • 对32行的理解就是,当进行 “<<” 运算的时候,会将 "<<" 左右两侧的 cout 与输出对象作为参数传给 “<<" 函数,来进行运算,同样的 endl 也是 ostream 类对象。因此就可以实现链式编程思想。

实例

#include<iostream>
using namespace std;

class Person2 {

	friend ostream& operator<<(ostream& cout, Person2 p);

public:
	Person2(int a, int b) {
		s_A = a;
		s_B = b;
	}

private:
	int s_A,s_B;
};

//左移运算符使用全局函数进行重载
/*

	25~28行代码就是用来实现第32行代码,为了使cout << p可以使用
	返回值为ostream& 是为了可以在使用完重载之后,可以继续使用ostream里面的函数,以此来实现链式编程。

*/
ostream& operator<<(ostream& cout, Person2 p) {
	cout << "s_A = " << p.s_A << " s_B = " << p.s_B;
	return cout;
}

void test2() {
	Person2 p(10, 10);
	cout << p << endl;	//cout << p << endl == cout << "s_A = " << p.s_A << " s_B = " << p.s_B
}

int main() {

	test2();

	system("pause");
	return 0;
}

递增运算符 (++) 重载

作用 :通过递增运算符,实现自己的整型数据。

理解 :递增的语法为 变量名++ 或者 ++变量名

  • 参数 :由于是在类内重载,因此不需要传入参数,且自增的目的很明确,只能对某一个属性进行自增。

  • 函数体 :内部只需要进行对类属性的自增就可以,要注意前自增和后自增,后自增需要一个占位参数 (int),且只能是 int

  • 返回值 :同样的为了实现链式编程思想,需要返回的是一个该类对象本身,因此返回值类型为类,需要注意的是,前自增的返回值是用类引用进行接收,这是因为需要对同一块空间进行修改;而后自增的时候,应该首先返回对象本身,然后再进行自增,因此只需要类接收就可以。

实例

#include<iostream>
using namespace std;

class MyInteger {

	friend ostream& operator<<(ostream& cout, MyInteger my);

public:

	MyInteger() {
		s_A = 0;
	}

	//重载前置++运算符	返回值是一个引用,是为了可以在递增的时候一直对这个数据进行操作
	MyInteger& operator++() {
		s_A++;
		return *this;
	}

	//重载后置运算符		这里的int是一个占位参数,用来区分前置++与后置++		这里的返回值类型是值返回是因为再函数内部返回结果是一个局部变量,在运行完之后会被自动释放。
	MyInteger operator++(int) {

		//后置++,应该是先返回当前结果,再进行自增
		//由于直接返回会使自增无法运行,所以可以首先记录当前结果
		MyInteger temp = *this;

		//再进行自增
		s_A++;

		//返回的结果是当前记录结果
		return temp;
	}

private:
	int s_A;
};

//首先需要对 “<<” 进行重载
ostream& operator<<(ostream& cout, MyInteger my) {
	cout << my.s_A;
	return cout;
}

void test3() {
	MyInteger my;
	cout << ++my << endl;		//1
	cout << my << endl;			//2
}

void test03() {
	MyInteger my1;
	cout << my1++ << endl;		//0		这里是因为返回值是一个当前结果(将my1++看作一个整体),但是函数内部已经对my1进行了递增
	cout << my1 << endl;		//1		这里是因为上一步操作对my1进行了递增,因此输出为递增之后的结果
}

int main() {

	test3();
	test03();

	system("pause");
	return 0;
}

赋值运算符 (=) 重载

C++ 编译器至少给一个类添加四个函数:

  1. 默认构造函数(无参,函数体为空)。

  2. 默认析构函数(无参,函数体为空)。

  3. 默认拷贝构造函数,对属性进行值拷贝。

  4. 赋值运算符 operator=,对属性进行值拷贝。

如果类中有属性指向堆区,做赋值操作时也会出现深浅拷贝问题,编译器默认提供的拷贝,会使不同的对象中的指针属性指向同一块堆区。那么在析构的时候,就会出现重复释放的问题。

理解

  • 参数 :由于在类内重载,且可以看出,需要的是本对象的某个属性=另一个对象的某个属性,因此需要传入一个参数,类型为类对象。

  • 函数体:首先判断指向的堆区是否有数据,如果有,则需要先进行释放,之后再将传入对象的属性赋值给该属性。

  • 返回值:同样的,为了实现链式编程思想,每次赋值之后,应该返回的值为该对象本身,返回值类型为类引用,以实现对本身的重复操作。

实例

#include<iostream>
using namespace std;

class Person4 {
public:

	Person4(int age) {
		s_age = new int(age);
	}

	Person4& operator=(Person4& p) {
		
		//首先判断是否有属性在堆区,如果有需要先释放,然后进行深拷贝
		if (s_age != NULL) {
			delete s_age;
			s_age = NULL;
		}

		this->s_age = new int(*p.s_age);
		return *this;
	}

	~Person4() {
		if (s_age != NULL) {
			delete s_age;
			s_age = NULL;
		}
	}

	int* s_age;

};

void test4() {
	Person4 p1(18);
	Person4 p2(20);
	Person4 p3(30);

	p3 = p2 = p1;

	cout << "p1年龄为:" << *p1.s_age << endl;
	cout << "p2年龄为:" << *p2.s_age << endl;
	cout << "p3年龄为:" << *p3.s_age << endl;
}

int main() {

	test4();

	system("pause");
	return 0;
}

关系运算符重载

作用 :重载关系运算符,可以让两个自定义类型对象进行对比操作。

关系运算符== > < !=

理解 :同样的,由于类对象拥有多个属性,无法进行比较,因此需要进行重载。

  • 参数 :由于是在类内,且是类对象的某个属性与另一个类对象的某个属性进行比较,因此只需要传入一个类对象就可以了。

  • 函数体 :代码为本对象某个属性与传入对象的某个属性进行判断比较。

  • 返回值 :由于比较的结果用在判断语句中,因此返回结果应为 true 或者 false,返回值类型为 bool

实例

#include<iostream>
using namespace std;

class Person5 {
public:
	Person5(int age) {
		s_age = age;
	}

	bool operator==(Person5& p) {
		if (s_age == p.s_age) {
			return true;
		}
		return false;
	}

	bool operator>(Person5& p) {
		if (s_age > p.s_age) {
			return true;
		}
		return false;
	}

	bool operator<(Person5& p) {
		if (s_age < p.s_age) {
			return true;
		}
		return false;
	}

	int s_age;
};

void test5() {
	Person5 p1(18);
	Person5 p2(17);

	if (p1 == p2) {
		cout << "p1 == p2" << endl;
	}
	else if (p1 > p2) {
		cout << "p1 > p2" << endl;
	}
	else {
		cout << "p1 < p2" << endl;
	}
}

int main() {

	test5();

	system("pause");
	return 0;
}

函数调用运算符重载

  • 函数调用运算符 () 也可以重载。

  • 由于重载之后使用的方式非常像函数的调用,因此称为仿函数。

  • 仿函数没有固定写法,非常灵活。

实例

#include<iostream>
using namespace std;

class MyPrint {
public:

	void operator()(string test) {
		cout << test << endl;
	}
};

class MyAdd {
public:
	
	int operator()(int num1, int num2) {
		return num1 + num2;
	}
};

void test6() {
	MyPrint myprint;
	myprint("Hello World!");

	MyAdd myadd;
	int rel = myadd(100, 200);
	cout << "rel = " << rel << endl;

	cout << MyAdd()(100, 100) << endl;	//匿名函数对象,直接类名加括号,创建一个匿名对象,在使用完之后就会被释放
}

int main() {

	test6();

	system("pause");
	return 0;
}

6、继承

​继承是面向对象三大特性之一,在定义类的时候,下级别的成员除了拥有上一级的共性,还有自己的特性,这时可以考虑使用 继承,减少重复代码。

继承的基本语法

语法class 子类:继承方式 父类

普通实现

#include<iostream>
using namespace std;

class BasePage {
public:

	void header() {
		cout << "首页、公开课、登录、注册……(公共头部)" << endl;
	}

	void footer() {
		cout << "帮助中心、交流合作、站内地图……(公共底部)" << endl;
	}

	void left() {
		cout << "Java、Python、C++、……(公共分类列表)" << endl;
	}
};

//继承
class Java :public BasePage {
public:
	void content() {
		cout << "Java学科视频" << endl;
		cout << "-------------" << endl;
	}
};

class Python :public BasePage {
public:
	void content() {
		cout << "Python学科视频" << endl;
		cout << "-------------" << endl;
	}
};

class CPP :public BasePage {
public:
	void content() {
		cout << "C++学科视频" << endl;
		cout << "-------------" << endl;
	}
};

void test1() {
	Java ja;
	ja.footer();
	ja.header();
	ja.left();
	ja.content();

	Python py;
	py.footer();
	py.header();
	py.left();
	py.content();

	CPP c;
	c.footer();
	c.header();
	c.left();
	c.content();
}

int main() {

	test1();

	system("pause");
	return 0;
}

继承方式

继承方式有三种

  • 公共继承 :子类继承自父类的成员,到了子类中的权限不会发生改变。

  • 保护继承 :子类继承自父类的成员,到了子类中权限变为保护权限。

  • 私有继承 :子类继承自父类的成员,到了子类中权限变为私有权限。

注意!!!:父类中的私有属性,子类中是无法访问的。

实例

#include<iostream>
using namespace std;

//父类
class Base {
public:
	int s_A;		//父类中的公共权限
protected:
	int s_B;		//父类中的保护权限
private:
	int s_C;		//父类中的私有权限
};

//公共继承
class Son1 :public Base {		//(继承自Base)
public:
	void func() {
		s_A = 100;			//父类中公共成员属性	到子类中	依然为公共权限
		s_B = 200;			//父类中保护成员属性	到子类中	依然为保护权限
		//s_C = 300;			//父类中的私有成员属性	到子类中  访问不到。
	}
};

void test2() {			//用来测试公共继承的子类成员属性权限
	Son1 s1;
	s1.s_A = 10;		//父类中公共成员属性	到子类中	依然为公共权限	类外可以进行访问和修改
	//s1.s_B = 100;		//父类中保护成员属性	到子类中	依然为保护权限	类外无法进行访问和修改
}

//保护继承
class Son2 :protected Base {		//(继承自Base)
	void func() {
		s_A = 100;		//父类中公共成员属性	到子类中	变为保护权限
		s_B = 200;		//父类中保护成员属性	到子类中	变为保护权限
		//s_C = 300;		//父类中的私有成员属性	到子类中  访问不到。
	}
};

void test02() {			//用来测试保护继承的子类成员属性权限
	Son2 s2;
	//s2.s_A = 10;		//父类中公共成员属性	到子类中	变为保护权限	类外无法进行访问和修改
	//s2.s_B = 20;		//父类中保护成员属性	到子类中	变为保护权限	类外无法进行访问和修改
}

//私有继承
class Son3 :private Base {		//(继承自Base)
	void func() {
		s_A = 100;		//父类中公共成员属性	到子类中	变为私有权限
		s_B = 200;		//父类中保护成员属性	到子类中	变为私有权限
		//s_C = 300;		//父类中的私有成员属性	到子类中  访问不到。
	}
};

class GrandSon3 :public Son3 {		//用来测试私有继承的子类成员属性权限(继承自Son3)
	void func() {
		//s_A = 10;		//父类中的私有成员属性	在子类中		访问不到
		//s_B = 20;		//父类中的私有成员属性	在子类中		访问不到
		//s_C = 30;		//父类中的私有成员属性	在子类中		访问不到
	}
};

int main() {

	test2();
	test02();

	system("pause");
	return 0;
}

继承中的对象模型

问题 :从父类继承过来的成员,哪些属于子类对象中?

  • 父类中所有非静态成员属性都会被子类继承下去。

  • 父类中的私有成员属性,是被编译器隐藏了,因此是访问不到,但是确实被继承了。

实例

#include<iostream>
using namespace std;

class Base1 {
public:
	int s_A;		//父类中的公共权限
protected:
	int s_B;		//父类中的保护权限
private:
	int s_C;		//父类中的私有权限
};

class son1 :public Base1 {
public:
	int s_D;
};

void test3() {

	//结果为16
	cout << "son1类所占的空间为:" << sizeof(son1) << endl;
}

int main() {

	test3();

	system("pause");
	return 0;
}

继承中构造和析构顺序

子类继承父类后,当创建子类对象,也会调用父类的构造函数。

实例

#include<iostream>
using namespace std;

class Base2 {
public:
	Base2() {
		cout << "Base2构造函数被调用!" << endl;
	}

	~Base2() {
		cout << "Base2析构函数被调用!" << endl;
	}
};

class son4 :public Base2 {
public:
	son4() {
		cout << "son4构造函数被调用!" << endl;
	}

	~son4() {
		cout << "son4析构函数被调用!" << endl;
	}
};

void test4() {
	
	/*
		Base2构造函数被调用!
		son4构造函数被调用!
		son4析构函数被调用!
		Base2析构函数被调用!	
	*/
	son4 s;
}

int main() {

	test4();

	system("pause");
	return 0;
}

继承同名成员处理方式

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

  • 访问子类同名成员 直接访问即可。

  • 访问父类同名成员 需要加作用域。

实例

#include<iostream>
using namespace std;

class Base5 {
public:

	Base5() {
		s_A = 100;
	}

	void func() {
		cout << "Base5 - func被调用!" << endl;
	}
	int s_A;
};

class son5 :public Base5 {
public:
	son5() {
		s_A = 200;
	}

	void func() {
		cout << "son5 - func被调用!" << endl;
	}

	void func(int a) {
		cout << "Base5 - void func(int a)被调用!" << endl;
	}

	int s_A;
};

//同名属性的处理
void test5() {
	son5 s;
	cout << "son 下 s_A = " << s.s_A << endl;	//直接访问,访问的是子类中的属性
	cout << "Base5 下 s_A = " << s.Base5::s_A << endl;		//加作用域,访问的是继承过来的成员
}

//同名函数的处理
void test05() {
	son5 s1;
	s1.func();		//同理,直接访问,访问的是子类中的func()
	s1.Base5::func();		//加作用域,访问的是继承过来的函数

	s1.func(10);		//旧版本中,子类如果出现和父类同名的函数,子类的同名成员会隐藏掉父类中所有的同名函数,包括重载之后的函数。
}
int main() {

	test5();
	test05();

	system("pause");
	return 0;
}

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

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

  • 访问子类同名成员,直接访问。

  • 访问父类同名成员,加作用域。

实例

#include<iostream>
using namespace std;

class Base6 {
public:
	static int s_A;		//静态成员变量,需要类内声明,类外初始化
	static void func() {
		cout << "Base6-func被调用" << endl;
	}
};
int Base6::s_A = 200;

class Son6 :public Base6{
public:
	static int s_A;
	static void func() {
		cout << "Son6-func被调用" << endl;
	}
};
int Son6::s_A = 100;

//测试静态同名成员变量
void test6() {

	//1、通过对象进行访问
	Son6 s;
	cout << "通过对象进行访问" << endl;
	cout << "Son	下 s_A = " << s.s_A << endl;
	cout << "Base	下 s_A = " << s.Base6::s_A << endl;

	//2、通过类名进行访问
	cout << "---------------------" << endl;
	cout << "通过类名进行访问" << endl;
	cout << "Son	下 s_A = " << Son6::s_A<< endl;
	cout << "Base	下 s_A = " << Son6::Base6::s_A << endl;
}

//测试静态同名成员函数
void test06() {

	//1、通过对象进行访问
	Son6 s;
	cout << "通过对象进行访问" << endl;
	s.func();
	s.Base6::func();

	//2、通过类名进行访问
	cout << "---------------------" << endl;
	cout << "通过类名进行访问" << endl;
	Son6::func();
	Son6::Base6::func();
}

int main() {

	test6();
	cout << endl;
	test06();

	system("pause");
	return 0;
}

多继承语法

C++ 允许 一个类继承多个类

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

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

C++ 实际开发不建议使用多继承

实例

#include<iostream>
using namespace std;

class Base07 {
public:
	Base07() {
		s_A = 100;
	}

	int s_A;
};

class Base007 {
public:
	Base007() {
		s_A = 200;
	}

	int s_A;
};

class Son7 :public Base07, public Base007 {
public:
	int s_C = 200;
	int s_D = 300;
};

void test7() {
	Son7 s;
	cout << "sizeof Son7 = " << sizeof(Son7) << endl;

	//多个父类出现同名成员,需要加作用域区分
	cout << "Base 07  -  s_A = " << s.Base07::s_A<< endl;
	cout << "Base 007  -  s_A = " << s.Base007::s_A << endl;
}

int main() {

	test7();

	system("pause");
	return 0;
}

菱形继承

概念

​ 两个派生类继承同一个类。

​ 又有某个类继承两个派生类。

​ 这种继承被称为菱形继承,或者钻石继承。

问题

​在同时继承两个派生类的时候,会有同一份数据,例如假如两个派生类继承了基类中的某个属性,那么新类会同时拥有两个派生类的该属性,就会出现指向不明确的问题。

解决方法

​使用 virtual 关键字。

思考

​此处不将父类中该属性设为静态变量是因为,静态变量是使用该类创建的对象共享同一份数据,而不是派生类共享同一份数据,所以,使用 virtual 的本质是一个指针,即派生类中的该属性是指向父类中的该属性的,当派生类中该属性发生改变,会使父类中的该属性也同时发生改变,可以等价理解为使用 virtual 之后,两个派生类中使用同一份父类属性。与 static 类似,但是本质是不同的。

实例

#include<iostream>
using namespace std;

class Animal {
public:
	int s_age;
};


//继承前加virtual关键字,变为虚继承
//此时父类Animal被称为虚基类
class sheep :virtual public Animal {};

class Tuo :virtual public Animal {};

class SheepTuo :public sheep, public Tuo {};

void test8() {
	SheepTuo st;
    
   //使用虚继承之后,两个派生类共享同一份父类数据
	st.sheep::s_age = 18;
	st.Tuo::s_age = 28;

	cout << "st.s_age = " << st.s_age << endl;

}

int main() {

	test8();

	system("pause");
	return 0;
}

7、多态

多态的基本概念

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

多态分为两类

  • 静态多态 :函数重载和运算符重载属于静态多态,复用函数名。

  • 动态多态 :派生类和虚函数实现运行时多态。

静态多态和动态多态的区别

  • 静态多态的函数地址早绑定 - 编译阶段确定函数地址。

  • 动态多态的函数地址晚绑定 - 运行阶段确定函数地址。

理解 :多态的意思为,要让基类的不同派生类拥有共同的行为,但行为的结果不同,即让 Animal 类拥有共同的说话行为,但要使 Cat 类和 Dog 类的说话行为产生的结果是不同的。

具体实现方法

在基类中构建需要使用多态的虚函数,在子类中重写该函数,以达到不同的派生类拥有基类共同的行为,但不同的派生类对该行为产生的结果是不同的。

实例

#include<iostream>
using namespace std;

class Animal1 {
public:

	//虚函数	:为实现之后的多态,这里需要一个virtual关键字
	virtual void speak() {
		cout << "动物在说话" << endl;
	}

};

class Cat :public Animal1 {
public:
	//重写:不论是返回值,函数名,还是形参列表,完全一样才叫重写,而只有形参不同则称为重载
	void speak() {
		cout << "小猫在说话" << endl;
	}
};

class Dog :public Animal1 {
public:
	void speak() {
		cout << "小狗在说话" << endl;
	}
};


/*
	1、为什么传入的参数是Animal类?
		因为在此处,我们需要实现,使用一个函数可以使不同的对象之间存在的同名函数实现调用,因为对于继承了同一个类的子类,比如此处,Cat类继承了Animal类,他们同时都有speak函数,但是,我们想要使不同的动物类,拥有不同的speak,但是一个一个使用‘子类.speak’就会很麻烦,因此只需要一个函数就可以实现说话的功能,且比较直观的,传入什么样的类对象,就会输出对应的对象行为,而因为都是Animal类,所以这里的形参应该为Animal类
	
	2、virtual关键字的理解?
		可以发现,假如不适用virtual关键字使得Animal类内的函数变为虚函数,那么在调用dospeak的时候,无论传入的形参是什么样的类对象,就会只输出Animal类中的speak函数,而不会随着传入对象的不同而执行不同的函数,这就是早绑定,在编译阶段就使得形参列表中的函数地址被绑定了,所以只能输出Animal类的speak,而在使用virtual关键字使得Animal类中的speak变为虚函数之后,就可以实现在运行阶段,随着传入参数的不同,实现晚绑定,就可以随着传入参数的不同而使得执行对应的不同的函数。

	3、多态的理解?
		由dospeak函数可以看出,传入的参数不同,执行的函数也不同,这就是函数的多种形态,即多态。

	4、多态的条件
		有继承关系;
		子类重写父类的虚函数;

	5、动态多态使用
		父类的指针或引用	执行子类对象

*/
void doSpeak(Animal1 & animal) {		//通过对传入参数不同实现不同函数的执行
	animal.speak();
}

void test1() {
	Cat cat;
	doSpeak(cat);

	Dog dog;
	doSpeak(dog);

	Animal1 ani;
	doSpeak(ani);
}

int main() {

	test1();

	system("pause");
	return 0;
}

多态案例——计算器类

案例描述

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

多态的优点

  • 代码组织结构清晰。

  • 可读性强。

  • 利于前期和后期的扩展以及维护。

实例

#include<iostream>
using namespace std;

//普通实现
class Calculator {
public:

	int getResult(string str) {
		if (str == "+") {
			return s_Num1 + s_Num2;
		}
		else if (str == "-") {
			return s_Num1 - s_Num2;
		}
		else if (str == "*") {
			return s_Num1 * s_Num2;
		}
	}

	int s_Num1;
	int s_Num2;
};

void test2() {
	Calculator c;
	c.s_Num1 = 10;
	c.s_Num2 = 20;
	cout << c.s_Num1 << "+" << c.s_Num2 << "=" << c.getResult("+") << endl;
}


//多态实现
//这样的实现方法,使得在需要添加新的行为的时候,不会对源代码进行修改,而只需要新建一个派生类,使其不会影响到其它类内行为。
class AbstractCalculator {
public:
	virtual int getResult() {
		return 0;
	}

	int s_Num1;
	int s_Num2;
};

//加法计算器类
class AddCalculator :public AbstractCalculator{
public:
	int getResult() {
		return s_Num1 + s_Num2;
	}
};

//减法计算器
class SubCalculator :public AbstractCalculator {
public:
	int getResult() {
		return s_Num1 - s_Num2;
	}
};

//乘法计算器
class MulCalculator :public AbstractCalculator {
public:
	int getResult() {
		return s_Num1 * s_Num2;
	}
};

void test02() {
	AddCalculator ad;
	ad.s_Num1 = 100;
	ad.s_Num2 = 200;
	cout << ad.s_Num1 << "+" << ad.s_Num2 << "=" << ad.getResult() << endl;
}

int main() {

	//test2();
	test02();

	system("pause");
	return 0;
}

纯虚函数和抽象类

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

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

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

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

抽象类特点

  • 无法实例化对象。

  • 子类必须重写抽象类中的纯虚函数,否则也属于抽象类。

实例

#include<iostream>
using namespace std;

class Base {
public:

	//纯虚函数
	virtual void func() = 0;
};

class Son :public Base {
public:
	void func() {
		cout << "func 被调用" << endl;
	}
};

void test3() {
	//Base b;
	//new Base;		两个编译都不会通过,因为抽象类无法实例化对象

	Son s;
	s.func();

	//多态调用
	Base* base = new Son;
	base->func();
}

int main() {

	test3();

	system("pause");
	return 0;
}

多态案例——制作饮品

饮品的制作步骤为:煮水——冲泡——倒入杯中——加入辅料。

算法 :将上述步骤封装在一个类中,而对不同的子类再进行相对应的行为修改。

实例

#include<iostream>
using namespace std;

class Step {
public:
	virtual void boil() = 0;		//煮水
	virtual void chong() = 0;		//冲泡
	virtual void dao() = 0;			//倒入杯中
	virtual void add() = 0;			//添加辅料
	//制作饮品
	void makeDrink() {
		boil();
		chong();
		dao();
		add();
	}

};

//咖啡类
class Coffee :public Step {
public:
	void boil() {
		cout << "煮农夫山泉" << endl;
	}
	void chong() {
		cout << "冲泡咖啡" << endl;
	}
	void dao() {
		cout << "倒入杯中" << endl;
	}
	void add() {
		cout << "添加牛奶和糖" << endl;
	}
};

//茶类
class Tea :public Step {
public:
	void boil() {
		cout << "煮农夫山泉" << endl;
	}
	void chong() {
		cout << "冲泡茶叶" << endl;
	}
	void dao() {
		cout << "倒入杯中" << endl;
	}
	void add() {
		cout << "添加柠檬" << endl;
	}
};

//手磨咖啡:制作咖啡的子类,可以制作不同的咖啡类
class SelfCoffee :public Coffee {
	void chong() {
		cout << "冲泡手磨咖啡" << endl;		//只需要修改冲泡的内容,所以只需要改写这一个函数就可以,如果有需要的话,例如加入佐料不同,也可将add改写。
	}
};

void dowork(Step * abs) {		//提供一个可以调用制作饮品的接口,并且由传入对象不同调用不同的制作接口。
	abs->makeDrink();
	cout << "-------------------" << endl;
	delete abs;
}

void test4() {
	dowork(new Coffee);
	dowork(new SelfCoffee);
}

int main() {

	test4();

	system("pause");
	return 0;
}

虚析构和纯虚析构

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

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

虚析构和纯虚析构共性

  • 可以解决父类指针释放子类对象。

  • 都需要由具体的函数实现。

虚析构和纯虚析构区别

  • 如果是纯虚析构,该类属于抽象类,无法实例化对象。

虚析构语法virtual ~类名(){}

纯虚析构语法virtual ~类名() = 0;

实例

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

class Animal5 {
public:
	Animal5() {
		cout << "Animal5 构造函数被调用!" << endl;
	}

	virtual ~Animal5() {		//为了解决39行代码出现的问题,此处使用虚析构,就可以解决无法释放子类对象指向的堆区
		cout << "Animal5 析构函数被调用!" << endl;
	}

	//virtual ~Animal5() = 0;	//虚析构和纯虚析构有一个就可以,不能两个同时出现
	virtual void speak() = 0;
};

Animal5::~Animal5() {	//由于析构函数必须有一个实现过程,所以纯虚析构必须在类外进行一次函数实现。
	cout << "Animal5 纯虚析构函数被调用" << endl;
}

class Cat :public Animal5 {
public:
	Cat(string Name) {
		cout << "Cat 构造函数被调用!" << endl;
		s_Name = new string(Name);
	}
	void speak() {
		cout <<*s_Name<< "小猫在说话" << endl;
	}
	~Cat() {
		if (s_Name != NULL) {
			cout << "Cat析构函数被调用!" << endl;
			delete s_Name;
			s_Name = NULL;
		}

	}
	string* s_Name;
};


void test5() {
	Animal5* abs = new Cat("Tom");		//此处父类指针指向子类对象,但是加入子类对象中有指针指向堆区,此时父类指针无法调用子类析构函数
	abs->speak();
	delete abs;
}

int main() {

	test5();

	system("pause");
	return 0;
}

多态案例——电脑组装

案例描述

电脑主要组成部件为 CPU(用于计算),显卡(用于显示),内存条(用于存储)。

将每个零件抽象出封装基类,并且提供不同的厂商生产不同的零件,例如 IntelLenovo 厂商。

创建电脑类提供让电脑工作的函数,并且调用每个零件工作的接口。

测试时组装三台不同的电脑进行工作。

实例

#include<iostream>
using namespace std;

//CPU抽象类
class CPU {
public:
	virtual void calculate() = 0;
};

//显卡抽象类
class VideoCard {
public:
	virtual void display() = 0;
};

//内存条抽象类
class Memory {
public:
	virtual void storage() = 0;
};

//电脑抽象类
class Computer {
public:
	Computer(CPU* cpu, VideoCard* vc, Memory* mem) {	//因为不同的电脑有不同类型的CPU,所以传入的对象应该是对应的CPU类型,
														//所以此处的形参是类指针,用来接受传入的cpu类对象。
		s_cpu = cpu;
		s_vc = vc;
		s_mem = mem;
	}

	void work() {		//电脑工作函数
		s_cpu->calculate();
		s_vc->display();
		s_mem->storage();
	}

	~Computer() {
		if (s_cpu != NULL) {
			delete s_cpu;
			s_cpu = NULL;
		}
		else if (s_vc != NULL) {
			delete s_vc;
			s_vc = NULL;
		}
		else if (s_mem != NULL) {
			delete s_mem;
			s_mem = NULL;
		}
	}

private:	//类指针指向的对象,是一个类对象,而使用类指针是因为,类内有不同的行为,只能通过指针进行访问,相当于在电脑内部使用一块内存,这个内存存放指针地址,指向零件,即电脑的该指针指向一个零件,这个零件可以指向Intel的也可以指向Lenovo的,不同的电脑可以有不同的指向,这样做的原因是为了方便对每一个对象进行维护
	CPU* s_cpu;			//CPU的零件指针
	VideoCard* s_vc;	//显卡的零件指针
	Memory* s_mem;		//内存条的零件指针
};

//厂商
//Intel厂商
class IntelCPU :public CPU {
	void calculate() {
		cout << "Intel的CPU开始计算了" << endl;
	}
};

class IntelVideoCard :public VideoCard {
	void display() {
		cout << "Intel的显卡开始显示了" << endl;
	}
};


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

//联想厂商
class LenovoCPU :public CPU {
	void calculate() {
		cout << "Lenovo的CPU开始计算了" << endl;
	}
};

class LenovoVideoCard :public VideoCard {
	void display() {
		cout << "Lenovo的显卡开始显示了" << endl;
	}
};


class LenovoMemory :public Memory {
	void storage() {
		cout << "Lenovo的内存条开始存储了" << endl;
	}
};

void test6() {
	//第一台电脑零件:使用一个类指针指向堆区,这个堆区是创建该类对象时候的地址
	CPU* intelCPU = new IntelCPU;
	VideoCard* intelvc = new IntelVideoCard;
	Memory* intelmem = new IntelMemory;

	//创建第一台电脑
	Computer* computer1 = new Computer(intelCPU, intelvc, intelmem);
	computer1->work();
	delete computer1;

	//创建第二台电脑
	Computer* computer2 = new Computer(new LenovoCPU, new LenovoVideoCard, new LenovoMemory);
	computer2->work();
	delete computer2;
}

int main() {

	test6();

	system("pause");
	return 0;
}

七、文件操作

程序运行时产生的数据都属于临时数据,程序一旦运行结束就会被释放。

通过 文件可以将数据持久化

C++ 中对文件的操作需要包含头文件 <fstream>

文件类型分为两种

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

操作文件的三大类:

  • ofstream :写操作。

  • ifstream :读操作。

  • fstream :读写操作。

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

实例

#include<iostream>
using namespace std;

//1、包含头文件
#include<fstream>

void test1() {

	//2、创建文件流对象
	ofstream ofs;

	//3、选择打开方式
	ofs.open("test1.txt", ios::out);

	//4、数据输入内容
	ofs << "姓名:张三" << endl;		//文件中endl也同样适用
	ofs << "性别:男" << endl;
	ofs << "年龄:18" << endl;

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

int main() {

	test1();

	system("pause");
	return 0;
}

读文件

读取步骤如下:

  1. 包含头文件:include<fstream>

  2. 创建流对象:ifstream ifs;

  3. 打开文件并判断文件是否打开成功:ifs.open("文件路径",打开方式);

  4. 读数据:四种方式读取。

  5. 关闭文件:ifs.close();

实例

#include<iostream>
using namespace std;
#include<string>
//1、添加头文件
#include<fstream>

void test2() {

	//2、创建文件输入流对象
	ifstream ifs;

	//3、打开并判断是否打开成功
	ifs.open("test.txt", ios::in);

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

	//4、读数据
	//第一种:创建一个字符数组用以存储,需进行初始化,再使用while循环,判断条件为输入流对象进行读取到数组,循环体为输出内容
	char buf[1024] = {0};
	while (ifs >> buf) {
		cout << buf << endl;
	}

	//第二种:创建一个数组,需进行初始化,使用while循环,判断条件为输入流对象进行分行读取到数组,参数为:1、数组名;2、数组长度
	char buf1[1024] = { 0 };
	while (ifs.getline(buf, sizeof(buf1))) {
		cout << buf1 << endl;
	}

	//第三种:使用字符串进行接收,使用while循环,判断条件为输入流对象进行分行读取到数组,参数为:1、输入流对象;2、数组名
	string buf3;
	while (getline(ifs, buf3)) {
		cout << buf3 << endl;
	}

	//第四种:使用单个字符,一个一个进行接收并输出,使用while循环,判断条件为是否读到文件尾EOF-end of file
	//具体操作为,将输入流对象进行单个读取字符并赋值给c再进行输出
	char c;
	while ((c = ifs.get())!= EOF){
		cout << c;
	}

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

int main() {

	test2();

	system("pause");
	return 0;
}

2、二进制文件

以二进制的方式对文件进行读写操作。

打开方式要指定为:ios::binary

写文件

二进制方式写文件主要利用流对象调用成员函数 write

函数原型:ostream& write(const char * buffer,int len);

参数解释:字符指针 buffer 指定内存中一段存储空间,len 是读写的字节数。

实例

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

class Person {
public:

	char m_Name[64];
	int age;
};

void test3() {
	ofstream ofs("person.txt", ios::out | ios::binary);

	Person p = { "张三",19};
	ofs.write((const char*)&p, sizeof(Person));

	ofs.close();
}

int main() {

	test3();

	system("pause");
	return 0;
}

读文件

二进制方式读文件主要利用流对象调用成员函数 read

函数原型:istream& read(char *buffer,int len);

参数解释:字符指针 buffer 指向内存中一段存储空间,len 是读写的字节数。

实例

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

class Person4 {
public:

	char m_Name[64];
	int age;
};
void test4() {
	ifstream ifs;
	ifs.open("person.txt", ios::in | ios::binary);
	if (!ifs.is_open()) {
		cout << "文件打开失败" << endl;
		return;
	}

	Person4 p;
	ifs.read((char*)&p, sizeof(Person4));
	cout << "姓名:" << p.m_Name << " 年龄:" << p.age << endl;

	ifs.close();
}

int main() {

	test4();

	system("pause");
	return 0;
}
posted @ 2025-12-31 14:06  Amireux77  阅读(1)  评论(0)    收藏  举报