C++核心编程


面向对象编程思想

一、 内存分区模型

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

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

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

1、 程序执行前

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

代码区

  • 存放CPU执行的机械命令
  • 代码区是共享的,共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可
  • 代码区是只读的,使其只读的原因是防止程序意外地修改了它的指令

全局区

  • 全局变量和静态变量存放在此
  • 全局区还包括了常量区、字符串常量和其他常量也存放在此
  • 该区域的数量在程序结束后由操作系统释放
#include <iostream>
using namespace std;

// 全局变量
int global_a = 10;
int global_b = 10;

// 全局常量
const int c_g_a = 10;
const int c_g_b = 10;

int main() {
	// 全局区
	// 全局变量、静态变量、常量

	// 静态变量
	static int static_a = 10;
	static int static_b = 10;

	// 常量
	// 字符串常量
	cout << "字符串常量的地址为 " << (int)&"hello" << endl;
	// const修饰的变量
	// const修饰的全局变量
	// const修饰的局部变量
	const int c_l_a = 10;
	const int c_l_b = 10;


	// 创建普通局部变量
	int a = 10;
	int b = 10;
	cout << "局部变量a的地址为 " << (int)&a << endl;
	cout << "局部变量b的地址为 " << (int)&b << endl;
	cout << "局部常量c_l_a的地址为 " << (int)&c_l_a << endl;
	cout << "局部常量c_l_b的地址为 " << (int)&c_l_b << endl;
	cout << "全局变量global_a的地址为 " << (int)&global_a << endl;
	cout << "全局变量global_b的地址为 " << (int)&global_b << endl;
	cout << "静态变量static_a的地址为 " << (int)&static_a << endl;
	cout << "静态变量static_b的地址为 " << (int)&static_b << endl;
	cout << "全局常量c_g_a的地址为 " << (int)&c_g_a << endl;
	cout << "全局常量c_g_b的地址为 " << (int)&c_g_b << endl;
}

2、 程序运行后

栈区

  • 由编译器自动分配释放,存放函数的参数值,全局变量等
  • 注意事项:不要返回局部变量的地址,栈区开辟的数据由编译器自动释放
#include <iostream>
using namespace std;

int* func(int b) {  // 形参数据也会放在栈区
   	 b = 100;
	int a = 10;  // 局部变量在栈区,在函数运行完成之后就被清除
	return &a;  // 返回局部变量的地址,非法函数
}

int main() {
	cout << *func() << endl;  // 第一次可以打印正确的数字,是因为编译器做了留
	cout << *func() << endl;  // 第二次的数据可能不会保留
}

堆区

  • 由程序员分配释放,若程序员不释放,程序结束时由操作系统回收
  • 在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;
}

3、 new操作符

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

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

语法:new 数据类型

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

#include <iostream>
using namespace std;


int* func() {
	// new方法开辟一个数组
	int* p = new int(10);  // 返回的是该数据类型的指针
	return p;
}

void test() {
 //创建10整型数据的数组,在堆区 
	int* arr = new int[10];  // 10代表数组有10个元素
	for (int i = 0; i < 10; i++) {
		arr[i] = i;  // 给数组赋值
	}
	// 释放堆区数组,释放数组的时候,要加[]才可以
	delete[] arr;
}

int main() {
	int* p = func();
	cout << *p << endl;  // 堆区的数据由程序员管理数据
	delete p;  // 数据已经释放,再次访问就是非法操作
	 cout << *p << endl;
	test();
}

二、 引用

1、 基本使用

作用:给变量起别名

语法:数据类型 &别名 = 变量名;

#include <iostream>
using namespace std;


int main() {
	// 引用基本语法
	// 数据类型 &别名 = 变量名
	int a = 10;
	int& b = a;
	cout << b << endl;
	b = 100;
	cout << a << endl;
}

2、 注意事项

  • 引用必须初始化
  • 引用在初始化后,不可以改变
#include <iostream>
using namespace std;


int main() {
	int a = 10;
	// 引用必须要初始化
	int& b = a;
	// int &b;  // 非法操作
	// 引用一旦初始化后就不可以更改其地址,但是可以赋值
}

3、 引用做函数参数

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

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

#include <iostream>
using namespace std;


// 交换函数
// 1、值传递
void swap(int a,int b) {
	int temp = a;
	a = b;
	b = temp;
}

// 2、 地址传递
void swap1(int* a, int* b) {
	int temp = *a;
	*a = *b;
	*b = temp;
}

// 3、 引用传递
void swap3(int &a, int &b) {  // &a即为别名,别名可以和变量名字一样
	int temp = a;
	a = b;
	b = temp;
}


int main() {
	int a = 10;
	int b = 20;

	cout << a << b << endl;
	cout << "运行函数" << endl;
	swap(a, b);
	cout << a << b << endl;
	// 实参并没有改变

	cout << a << b << endl;
	cout << "运行函数" << endl;
	swap1(&a, &b);
	cout << a << b << endl;
	// 实参发生改变


	cout << a << b << endl;
	cout << "运行函数" << endl;
	swap3(a, b);
	cout << a << b << endl;
	// 实参发生改变
}

通过引用参数产生的效果同按地址传递是一样的,引用的语法更加清楚简单

4、 引用做函数的返回值

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

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

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

#include <iostream>
using namespace std;

// 引用做函数的返回值
// 1、 不要返回局部变量的引用
int& test1() {  // 加&相当于以引用的方式返回
	int a = 10;  // 局部变量存放在四区中的 栈区
	return a;
}

// 2、 函数的调用可以作为左值
int& test2() {
	static int a = 10;  // 静态变量,存放在全局区,全局区上的数据在程序结束后系统释放
	return a;			
}

int main() {
	int& b = test1();
	cout << b << endl;
	cout << b << endl;
	int& c = test2();
	cout << c << endl;
	test2() = 1000;  // 内存地址修改,同时函数的调用可以作为左值
	cout << c << endl;
}

5、 本质

本质:引用的本质在C++内部的实现是一个指针常量

6、 常量引用

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

在函数形参列表中使用

#include <iostream>
using namespace std;


// 打印数据
void showValue(int &a) {  // 可以加入const防止误操作
	a = 1100;
	cout << a << endl;
}

int main() {
	// 常量引用:用来修饰形参,防止误操作
	int a = 10;
	const int& ref = 10;  // 加const之后,编译器将代码修改 int temp = 10; const int & ref = temp;
	// ref = 20;  // 加入const之后变为只读
	showValue(a);
	cout << a << endl;
}

三、 函数提高

1、 默认参数

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

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

#include <iostream>
using namespace std;


// 函数的默认参数
void func(int a , int b = 10) {  // 自己也可以传入参数,如果传入参数,就使用自定义参数,否则使用默认参数
	cout << a << "\t" << b << endl;
}

// 注意事项
// 如果莫个位置已经有了默认参数,那么从这个位置以后都必须要有默认值
// 如果函数声明有默认参数,那么函数的实现就不能有默认参数,不能重定义参数,即声明和实现只能有一个有默认参数
void func(int a, int b);

int main() {
	int a = 20;
	// int b = 30;
	func(a);
}

2、 占位参数

C++中函数的形参列表里可以有占位参数,用来做占位,调用函数是必须填补这个位置

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

#include <iostream>
using namespace std;


// 占位参数:可以有默认参数
// 返回值类型 函数名(数据类型){}
void func(int a, int) {
	cout << "this is func" << endl;
}

void func(int a, int);

int main() {
	func(10, 20);
}

3、 函数重载

3.1 概述

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

函数重载的条件

  • 同一作用域下
  • 函数名相同
  • 函数参数类型不同 或者 个数不同 或者 顺序不同

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

#include <iostream>
using namespace std;


// 函数重载
// 在同一作用域下
// 函数名相同
// 函数参数类型不同,或者个数不同,或者顺序不同
void func(double a, int b) {
	cout << "10" << endl;
}
void func(string name) {
	cout << name << endl;
}
void func(int a, double b) {
	cout << "1010" << endl;
}


int main() {
	func("hello");
	func(1.0, 20);
	func(20, 1.0);
}

3.2 注意事项

  • 引用作为重载条件
  • 函数重载碰到函数默认参数
#include <iostream>
using namespace std;


// 函数重载的注意事项
// 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 << "func(int a默认参数)调用" << endl;
}
void func2(int a) {
	cout << "func(int a)调用" << endl;
}


int main() {
	const int a = 10;
	int b = 20;
	func(a);
	func(b);
	func2(a, b);
}

四、 类与对象

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

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

1、 封装

1.1 封装的意义

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

封装的意义

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

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

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

    // 设置一个圆类,求其周长
    #include <iostream>
    using namespace std;
    
    // 圆周率
    const double PI = 3.14;
    
    class Circle {
    	// 访问权限:公共权限
    public:  
    
    	// 属性:半径
    	int m_r;  
    
    	// 行为:获取圆的周长
    	double calcculate() {
    		return 2 * PI * m_r;
    	}
    };
    
    
    int main() {
    
    	// 通过圆类来创建对象(实例化对象)
    	Circle c1;
        
    	// 给圆对象的属性赋值
    	c1.m_r = 10;
    
    	cout << "圆的周长为" << c1.calcculate() << endl;
    }
    

    案例:设计一个学生类,属性有姓名和学号,可以给姓名和学号赋值,可以显示学生的姓名和学号

    #include <iostream>
    using namespace std;
    #include <string>;
    
    
    class Student
    {
    	// 访问权限
    public:
    
    	// 属性
    	string stu_name;
    	int stu_id;
    
    	// 行为
    	void showInfo() {
    		cout << "姓名:" << stu_name << " 学号:" << stu_id << endl;
    	}
    
    	// 给属性赋值
    	void setInfo(string name, int id) {
    		stu_name = name;
    		stu_id = id;
    	}
    };
    
    
    int main() {
    	Student stu;  // 实例化
    	stu.setInfo("张三", 1);  
    	stu.showInfo();
    }
    

    类中的属性和行为,我们同一称为成员

    成员属性,成员变量,成员方法/成员函数

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

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

    访问权限有

    • public 公共权限
    • protected 保护权限
    • private 私有权限
    #include <iostream>
    using namespace std;
    #include <string>;
    
    
    // public     成员类内可以访问,类外也可以访问 
    // protected  成员类内可以访问,类外不可以访问,儿子也可以访问父亲保护的内容
    // private    成员类内可以访问,类内不可以访问,儿子不可以访问父亲的私有内容
    
    class Person {
    	// 公共权限
    public:
    	string name;
    	// 保护权限
    protected:
    	string car;
    	// 私有权限
    private:
    	int money;
    
    public:
    	void func() {
    		name = "zhansan";
    		car = "tuolaji";
    		money = 20;
    	}
    
    };
    int main() {
    	// 实例化具体对象
    	Person p1;
    	p1.name = "lishi";
    	// p1.car = "benc";  // 保护权限在类外不能访问
    	// p1.money = 30;  // 私有权限内容,类外也不能访问
    }
    

1.2 struct 和 class 的区别

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

  • struct 默认权限为公共
  • class 默认权限为私有
#include <iostream>
using namespace std;
#include <string>;


class C1 {
	int A;  // 默认权限是私有
};
struct C2
{
	int A2;  // 默认公有
};
int main() {
	// struct 默认公有
	C2 c2;
	c2.A2 = 100;  // 可以访问
	// class 默认私有
	C1 c;
	// c.A = 100;  // 不能修改
}

1.3 成员属性私有化

优点

  • 将所有成员属性设为私有,可以自己控制读写权限
  • 对于写权限,我们可以检测数据的有效性
#include <iostream>
using namespace std;
#include <string>;


// 成员属性私有化
class Person {
public:
	// 设置姓名、获取姓名 提供权限
	void setName(string pname) {
		name = pname;
	}
	string getName() {
		return name;
	}

	/* // 获取年龄
	int getAge() {
		age = 18;  // 初始化为18岁
		return age;
	} */
	// 可读可写年龄,如果想修改(年龄范围必须是0到18岁)
	int getAge() {
		return age;
	}
	// 设置年龄
	void setAge(int num) {
		if (num < 0 || num > 18) {
			age = 15;
			cout << "您输入的年龄有问题" << endl;
			return;
		}
		cout << num << endl;
		age = num;
	}


	// 只写财产
	void setMoney(int num) {
		money = num;
	}



private:  // 全部设置为私有
	// 姓名  可读可写
	string name;
	// 年龄  只读
	int age; 
	// 财产 只写
	int money;
};


int main() {
	Person p;
	p.setName("张三");
	cout << "名字:" << p.getName() << endl;

	p.setAge(150);  // 如果输入的值不符合要求,直接强制赋值
	cout << "年龄:" << p.getAge() << endl;
}

1.4 案例

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

#include <iostream>
using namespace std;


// 创建一个立法体的类
class Cube {
	// 设计属性
private:
	int c_l;  // 长
	int c_h;  // 高
	int c_w;  // 宽

	// 行为 获取立方体的面积和体积
public:
	// 设置属性
	void setInfo(int l, int w, int h) {
		c_l = l;
		c_w = w;
		c_h = h;
	}
	// 获取属性
	int getL() {
		return c_l;
	}
	int getH() {
		return c_h;
	}
	int getW() {
		return c_w;
	}
	// 获取面积
	int getS() {
		return c_l * c_h * 2 + c_l * c_w * 2 + c_h * c_w * 2;
	}
	// 获取体积
	int getV() {
		return c_l * c_h * c_w;
	}
	// 成员函数判断是否相等
	bool isSameByClass(Cube& c) {
		if (getH() == c.getH() && getW() == c.getW() && getL() == c.getL()) {
			return true;
		}
		return false;
	}

};

// 利用全局函数和成员函数判断两个立方体是否相等

// 全局函数
bool isSame(Cube &c1, Cube &c2) {  // 引用传递
	if (c1.getH() == c2.getH() && c1.getW() == c2.getW() && c1.getL() == c2.getL()) {
		return true;
	}
	return false;
}

int main() {
	// 第一个正方体
	Cube c1;
	c1.setInfo(1, 1, 1);
	int s1 = c1.getS();  // 体积
	int v1 = c1.getV();  // 面积

	// 第二个正方体
	Cube c2;
	c2.setInfo(1, 1, 2);
	int s2 = c2.getS();
	int v2 = c2.getV();
	
	if (isSame(c1, c2)) {
		cout << "相同" << endl;
	}
	else
	{
		cout << "不相同" << endl;
	}

	if (c1.isSameByClass(c2)) {
		cout << "成员判断,相同" << endl;
	}
	else
	{
		cout << "成员判断,不相同" << endl;
	}
}

判断点在圆上的位置

#include <iostream>
using namespace std;

// 判断点和圆的关系
// 点类
class Point {
private:
	int x;
	int y;
public:
	void setPoint(int x, int y) {
		x = x;
		y = y;
	}
	int getx() {
		return x;
	}
	int gety() {
		return y;
	}
};


// 圆类
class Circle {
private:
	int r; // 半径
	Point circlePoint;  // 代表圆心
public:
	void setR(int r) {
		r = r;
	}
	int getR() {
		return r;
	}
	void setCenter(Point center) {
		circlePoint = center;
	}
	Point getCenter() {
		return circlePoint;
	}
};

// 判断点和圆的关系
void isInCircle(Circle &c, Point &p) {
	// 计算距离的平方
	if (pow(c.getCenter().getx() - p.getx(), 2) - pow(c.getCenter().gety() - p.gety(), 2) == pow(c.getR(), 2)) {
		cout << "点在圆上" << endl;
		return;
	}
	cout << "点不在圆上" << endl;
}


int main() {
	// 创建圆
	Circle c;
	Point ct;
	ct.setPoint(10, 0);
	c.setCenter(ct);
	c.setR(10);
	Point p;
	p.setPoint(10, 10);
	isInCircle(c, p);
}

一个类可以实例化另一个类,作为其属性

2、 对象的初始化和清理

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

2.1 构造函数和解析函数

对象的初始化和清理也是两个非常重要的安全问题

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

C++利用了构造函数和析构函数解决上述问题,这两个函数将会被编译器自动调用,完成对象初始化操作和清理工作

对象的初始化和清理工作是编译器强制要我们做的事情,因此如果我们不提供构造函数和析构,编译器会提供

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

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

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

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

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

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

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

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

};
// 构造和析构都是必须有的实现,如果我们自己不提供,编译器会提供一个空实现的构造和析构
void test() {
	Person p;  // 栈上的数据,test执行完毕之后,会自动释放这个对象
}
int main() {
	// 对象的初始化和清理
	//test();
	Person p;

	system("pause");
	return 0;
}

2.2 构造函数的分类及调用

两种分类方式

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

三种调用方式

  • 括号法
  • 显示法
  • 隐式转换法
#include <iostream>
using namespace std;

// 构造函数的分类和调用
class Person {
public:
	int age;
	// 构造函数
	// 无参构造
	Person() {
		cout << "Person 构造函数的调用" << endl;
		age = 1;
	}
	// 有参构造
	Person(int a) {
		age = a;
		cout << "Person 构造函数的调用有参" << a << endl;
	}
	// 拷贝构造函数
	Person(const Person &p) {
		// 将传入的人身上的所有属性,拷贝到我身上
		age = p.age;
		cout << "Person 构造函数的调用拷贝" << age << endl;
	}
};
void test() {
	// 调用
	// 括号法
	Person p;  // 默认构造函数的调用
	Person p02(10);  // 调用有参构造函数
	Person p03(p02);  //  拷贝构造函数的调用 
	//cout << "p2的年龄为:" << p2.age << endl;
	//cout << "p3的年龄为:" << p3.age << endl;
	
	// 显示法
	Person p1;
	Person p2 = Person(10);  // 有参构造
	Person p3 = Person(p2);  // 拷贝构造
	//Person(10);  // 匿名对象,特点:当前行执行结束后,系统会立即回收掉匿名对象

	// 隐式转换法
	Person p4 = 10;  // 相当于 Person p4 = Person(10);
	Person p5 = p4;
}
int main() {
	test();
}

注意事项

  • 调用默认构造函数的时候,不要加()

    • 因为下面这行代码,编译器会认为是一个函数的声明

      Person p1();

  • 不要利用拷贝构造函数,初始化匿名对象

    • 因为编译器会认为Person p4(p3) === Person p3;,认为其为对象的声明

      Person p4(p3);

2.3 拷贝函数的调用时机

C++中拷贝函数调用时机通常由三种情况

  • 使用一个已经创建完毕的对象来初始化一个新对象
  • 值传递的方式给函数参数传值
  • 以值方式返回局部对象
#include <iostream>
using namespace std;


// 拷贝构造函数的调用时机

class Person {
public:
	Person() {
		cout << "默认函数" << endl;
	}
	Person(int age) {
		age = age;
		cout << "有参函数" << endl;
	}
	Person(const Person& p) {
		age = p.age;
		cout << "拷贝函数" << endl;
	}
	~Person() {
		cout << "类被释放" << endl;
	}
	int age;
};
void test01() {
// 使用一个已经创建完毕的对象来初始化一个新对象
	Person p1(20);
	Person p2(p1);
	cout << p2.age << endl;
}
void doWork( Person p ) {
	cout << "doWork" << endl;
}
void test02() {
// 值传递的方式给函数参数传值
	Person p;
	doWork(p);
}
Person doWork02() {
	Person p1;
	return p1;
}
void test03() {
// 以值方式返回局部对象
	Person p = doWork02();
}
int main() {
	//test01();
	//test02();
	test03();
	system("pause");
	return 0;
}

2.4 构造函数调用规则

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

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

构造函数调用规则

  • 如果用户定义有参构造函数,C++下不会提供默认无参构造,但是会提供默认拷贝构造
  • 如果用户定义拷贝构造函数,C++不会再提供其他构造函数
#include <iostream>
using namespace std;


// 构造函数的调用规则

// 1. 创建一个类,C++编译器会给每个类添加至少三个函数,析构函数,默认函数,拷贝函数

// 如果用户定义有参构造函数,C++下不会提供默认无参构造,但是会提供默认拷贝构造
class Person {
public:
	//Person() {
	//	cout << "默认函数调用" << endl;
	//}
	~Person() {
		cout << "析构函数调用" << endl;
	}
	int age;
	Person(int age) {
		age = age;
		cout << age << endl;
	}
	//Person(const Person &p) {
	//	age = p.age;
	//	cout << "拷贝构造函数" << endl;
	// }
};
//void test1() {
//	Person p;
//	p.age = 18;
//	Person p2(p);
//	cout << p2.age << "拷贝" << endl;  // 自动调用拷贝函数
//}
void test2() {
	Person p(10);
	Person p2(p);
	cout << p2.age << "拷贝" << endl;
}
int main() {
	//test1();
	test2();
}

2.5 深拷贝与浅拷贝

深拷贝:简单的赋值拷贝操作

浅拷贝:在堆区重新申请空间,进行拷贝操作

#include <iostream>
using namespace std;


// 深拷贝、浅拷贝
class Person {
public:
	int age;
	int* height;
	Person() {
		cout << "Person的默认构造函数调用" << endl;
	}
	Person(int age_, int height_) {
		cout << "Person的有参构造函数调用" << endl;
		age = age_;
		height = new int(height_);  // 将身高的数据创建到堆区
	}
	Person(const Person& p) {
		cout << "Person的拷贝构造函数调用" << endl;
		age = p.age;
		// height = p.height;  // 编译器默认实现的就是这行代码——浅拷贝
		// 深拷贝操作
		height = new int(*p.height);
	}
	~Person() {
		// 析构代码,将堆区开辟的数据做释放操作
		if (height != NULL) {
			delete height;
			height = NULL;  // 避免野指针的出现
		}
		cout << "Person的析构函数调用" << endl;
	}
};
void test() {
	Person p1(18, 160);
	cout << "p1的年龄为" << p1.age << "  p1的身高为" << *p1.height <<endl;
	Person p2(p1);  // 如果利用编译器提供的拷贝构造函数,会做浅拷贝操作;浅拷贝带来的问题是堆区的内存会重复释放
	// 这个问题要用深拷贝来解决
	cout << "p2的年龄为" << p2.age << "  p2的身高为" << *p2.height <<endl;
}
int main() {
	test();
	system("pause");
	return 0;
}

2.6 初始化列表

作用

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

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

#include <iostream>
using namespace std;


// 初始化列表
class Person {
public:
	// 传统初始化操作:利用传参构造函数进行操作

	// 初始化列表初始化属性
	// Person() :a(10), b(20), c(30) {
	// }
	// 更灵活的初始化
	Person(int a, int b, int c) :a(a), b(b), c(c) {
	}
	int a;
	int b;
	int c;
};
void test() {
	Person p(20, 30, 50);
	cout << "创建P" << endl;
	cout << p.a << p.b << p.c << endl;
}
int main() {
	test();
	system("pause");
	return 0;
}

2.7 类对象作为类成员

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

class A {}
class B {
    A a;  // B类中有对象A作为成员,A为对象成员
}

那么,当创建B对象时,A与B的构造和析构顺序是谁先谁后呢?

#include <iostream>
using namespace std;


// 类对象作为类成员
class Phone {
public:
	string c_Pname;
	Phone(string pName) {
		c_Pname = pName;
		cout << "phone构造函数" << endl;
	}
	~Phone() {
		cout << "Phone析构函数" << endl;
	}
};
class Person {
public:
	string c_Name;
	Phone c_Phone;
	Person(string name, string pName): c_Name(name), c_Phone(pName) {
		cout << "person构造函数" << endl;
	}
	~Person() {
		cout << "person析构函数" << endl;
	}
};
void test() {
	Person p("张三", "水果13ProMax");
	cout << "c_Name:" << p.c_Name << "  c_Phone:" << p.c_Phone.c_Pname << endl;
}
int main() {
	test();
	system("pause");
	return 0;
}

构造的顺序是:先调用对象成员的构造,在调用本类的构造

析构的顺序相反

2.8 静态成员

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

静态成员分为

  • 静态成员变量
    • 所有对象共享同一份数据
    • 在编译阶段分配内存
    • 类内声明,类外初始化
  • 静态成员函数
    • 所有对象共享同一个函数
    • 静态成员函数只能访问静态成员变量
#include <iostream>
using namespace std;

// 静态成员函数
class Person {
public:
	// 静态成员函数
	static void func() {
		c_A = 100;
		// c_B = 100;  // 静态成员函数不可以访问非静态成员变量
		cout << "static void func" << c_A << endl;
	}
	static int c_A;  // 静态成员变量,类内申明
	int c_B;  // 非静态成员变量
};
int Person::c_A = 10;  // 类外的初始化
void test() {
	// 访问
	Person p;
	p.func();  // 通过对象访问
	
	Person::func();  // 通过类名访问  :: 为域操符
}
int main() {
	test();
	system("pause");
	return 0;
}

静态函数也有访问权限

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

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

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

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

#include <iostream>
using namespace std;

// 成员变量和成员函数分开存储
class Person {
	int c_A;  // 非静态成员变量,属于类的对象
	static int c_B;  // 静态成员变量,不属于类对象上面
	void fn() {};  // 非静态成员函数,不属于类对象上
	static void  fn() {};  // 静态成员函数 ,不属于类的对象
};

void test01() {
	Person  p;
	/*
	空对象占用内存空间为1
	 C++ 编译器会给每个空对象也分配一个字节空间,是为了区分空对象占内存的位置
	每个空对象也应该有一个独一无二的内存地址
	 */
	cout << sizeof(p) << endl;
}

int main() {
	test01();

	system("pause");
	return 0;
}

3.2 this 指针

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

C++ 通过提供特殊的对象指针,this 指针来判断这一块代码是否被对象调用。

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

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

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

用途

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

class Person {
public:
	Person(int age) {
		this->c_age = age;  // this 指针指向被调用的成员函数所属的对象
	}
	int c_age;

	Person& addAge(Person &p) {  // Person& 返回值的引用,如果返回一个值,会创建新的对象
		this->c_age += p.c_age;
		return *this;  // this 指向调用函数的对象的指针,故 *this 指向的就是对象的本体 
	}
};

void test01() {
	Person p1(18);
	cout << "p1的年龄为" << p1.c_age << endl;
}
void test02() {
	Person p1(18);
	Person p2(19);

	// 链式编程思想
	p2.addAge(p1).addAge(p1);
	cout << "p2的年龄为" << p2.c_age << endl;
}

int main() {
	test01();
	test02();

	system("pause");
	return 0;
}

3.3 空指针访问成员函数

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

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

#include <iostream>
using namespace std;


class Person {
public:
	void showClassName() {
		cout << "this is Person class" << endl;
	}

	void showPersonAge() {
		// 报错的原因是因为传入的指针为 NULL
		if (this == NULL) {
			return;
		}
		cout << "age=" << c_Age << endl;
	}
	int c_Age;
};

void test01() {
	Person* p = NULL;

	p->showClassName();
	//p->showPersonAge();
}
int main() {
	test01();
	system("pause");
	return 0;
} 

3.4 const 修饰成员函数

常函数

  • 成员函数后加 const 后,我们称这个函数为常函数
  • 常函数内不可以修改成员属性
  • 成员属性声明时加关键字 mutable 后,在常函数中依然可以修改

常对象

  • 声明函数前加 const 称该对象为常对象
  • 常对象只能调用常函数
// 常函数
class Person {
public:

	// this 指针的本质是指针常量,指针的指向是不可以修改的
	void showPerson() const {  // -> 相当于 const Person * const this;  原本是 Person * const this; 
		// m_A = 100;  // 会报错
		this->m_B = 100;
	}

	int m_A;
	mutable int m_B;  // 特殊变量,即使在常函数中,也可以修改这个值,加关键字 mutable
};

// 常对象
void test() {
	const Person p;  // 其为常对象
	// p.m_A = 100;  // 不允许修改
	p.m_B = 100;  // m_B 是特殊值,在常对象下也可以修改
	// 常对象只能调用常函数,其不能调用普通成员函数,因为普通成员函数可以修改属性
}

4、 友元

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

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

友元的关键字为:friend

友元的三种实现

  1. 全局函数做友元
  2. 类做友元
  3. 成员函数做友元

4.1 全局函数

// 在类中,添加对象声明。同时在开头添加 friend
// 全局函数做友元
class Building {
	// goodGay 全局函数是 Building 的好朋友,可以访问 Building 中私有成员
	friend void goodGay(Building* building);

public:
	Building() {
		m_Bedroom = "卧室";
		m_Sittingroom = "客厅";
	}

public:
	string m_Sittingroom;  

private:
	string m_Bedroom;
};

// 全局函数
void goodGay(Building* building) {
	cout << "好盆友访问" << building->m_Sittingroom << endl;
	cout << "好盆友访问" << building->m_Bedroom << endl;
}
void test01() {
	Building building;
	goodGay(&building);

4.2 类

// 类做友元
class Building {
	// 友元
	friend class GoodGay;

public:
	Building();

public:
	string m_Sittingroom;  

private:
	string m_Bedroom;
};
// 类外写成员函数
Building::Building() {
	m_Bedroom = "卧室";
	m_Sittingroom = "客厅";
}
// 类
class GoodGay {
public:
	GoodGay();

	void visitHouse();

	Building* building;
};
// 类外写成员函数
GoodGay::GoodGay() {
	// 创建建筑物对象
	building = new Building;
}
void GoodGay::visitHouse() {
	cout << "好朋友正在访问" << building->m_Sittingroom << endl;
	cout << "好朋友正在访问" << building->m_Bedroom << endl;
}

void test() {
	GoodGay gg;
	gg.visitHouse();
}

4.3 成员函数

class Building;  // 类声明
class GoodGay {
public:
	GoodGay();

	void visit();  // 让函数可以访问 Building 中私有成员
	void visit1();  // 让函数不可以访问私有成员
	Building* building;
};
class Building {
	friend void GoodGay::visit();  // 使用域操符
public:
	Building();
	string m_Sitttingroom;
private:
	string m_Bedroom;
};
// 类外实现成员函数
GoodGay::GoodGay() {
	building = new Building;
}
void GoodGay::visit() {
	cout << "正在访问" << building->m_Sitttingroom << endl;
	cout << "正在访问" << building->m_Bedroom << endl;
}
void GoodGay::visit1() {
	cout << "正在访问" << building->m_Sitttingroom << endl;
	// cout << "正在访问" << building->m_Bedroom << endl;  // 报错
}
Building::Building() {
	m_Sitttingroom = "客厅";
	m_Bedroom = "卧室";
}

void test() {
	GoodGay gg;
	gg.visit();
	cout << "---------"<< endl;
	gg.visit1();
}

5、 运算符重载

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

5.1 加号运算符

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

// 加号运算符重载
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;
	}
	int m_A;
	int m_B;
};
// 全局函数重载
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;
}
void test() {
	Person p1;
	p1.m_A = 10;
	p1.m_B = 10;
	Person p2;
	p2.m_A = 10;
	p2.m_B = 10;

    // 成员函数的本质调用
    // Person p3 = p1.operator+(p2);
    // 全局函数的本质调用
    // Person p3 = operator+(p1, p2);
	Person p3 = p1 + p2;
	cout << "p3.m_A:" << p3.m_A << "\tp3.m_B:" << p3.m_B << endl;
}

运算符重载也可以发生函数重载

Person operator+(Person& p1, int num) {
	Person temp;
	temp.m_A = p1.m_A + num;
	temp.m_B = p1.m_B + num;
	return temp;
}  // 数字相加

对于内置的数据类型的表达式的运算符是不可以改变的

不要滥用运算符重载

5.2 左移运算符

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

// 左移运算符重载
class Person
{
public:
	int m_A;
	int m_B;
	/* 
	利用成员函数重载:p.operator<<(cout); -> 简化函数 p << cout;
	不会利用成员函数重载<<运算符,因为无法实现 cout 在左侧
	void operator<< (ostream& cout)
	{
		cout << "a:" << m_A << " b:" << m_B << endl;
	} 
	*/
};
void test()
{
	Person p;
	p.m_A = 10;
	p.m_B = 10;
	cout << p << endl;
}
// 只能利用全局函数重载,ostream其属于标准输出流
ostream& operator<< (ostream& cout, Person p)  // 本质:operator<< (cout, p); -> 简化 cout << p;
{
	cout << "a:" << p.m_A << " b:" << p.m_B;
	return cout;  // 链式编程,要有返回值
}

总结:重载左移运算符配合友元可以实现输出自定义数据类型

5.3 递增运算符

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

// 重载自增运算符

// 自定义整型
class MyInt
{
	friend ostream& operator<<(ostream& cout, MyInt i);  // 友元访问私有元素
public:
	MyInt()
	{
		m_num = 0;
	}
	// 重载前置自增运算符
	MyInt& operator++()
	{
		++m_num;  // 自增操作
		return *this;
	}
	// 重载后置自增运算符
	MyInt operator++(int)  // int 代表占位参数,用于区分前置递增和后置递增,注意不要使用引用
	{
		// 先记录当时结果
		MyInt temp = *this;
		// 后自增
		m_num++;
		// 最后返回结果
		return temp;
	}
private:
	int m_num;
};
// 重载左移运算符
ostream& operator<<(ostream& cout, MyInt i)
{
	cout << i.m_num;
	return cout;
}
void test()
{
	MyInt i;
	cout << ++i << "--" << i << endl;
	cout << "=====" << endl;
	cout << i++ << "--" << i << endl;
}

5.4 赋值运算符

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

  1. 默认构造函数(无参,函数体为空)
  2. 默认析构函数(无参,函数体为空)
  3. 默认拷贝函数,对属性进行值拷贝
  4. 赋值运算符 operator=,对属性进行值拷贝

如果中有属性指向堆区,做赋值操作时也会出现深浅拷贝问题

class Person
{
public:
	Person(int age)
	{
		m_Age = new int(age);  // 将数据开辟到堆区
	}
	// 重载赋值运算符
	Person& operator=(Person& p)
	{
		// 编译器提供浅拷贝 this->m_Age = p.m_Age;
		// 应该判断是否有属性在堆区,如果有先释放干净,然后再深拷贝
		if (this->m_Age)  // 相当于 if (this->m_Age != NULL)
		{
			delete this->m_Age;  // 删除数据
			m_Age = NULL;
		}
		m_Age = new int(*p.m_Age);
		// 返回对象本身
		return *this;
	}
	int* m_Age;
	~Person()
	{
		if (m_Age)  // 当m_Age不为空时
		{
			delete m_Age;
			m_Age = NULL;
		}
	}
};
void test()
{
	Person p1(10);
	Person p2(20);
	p2 = p1;  // 赋值操作,默认为浅拷贝
	cout << "p1的年龄为:" << *p1.m_Age << endl;
	cout << "p2的年龄为:" << *p2.m_Age << endl;
}

5.5 关系运算符

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

// 重载关系运算符
class Person
{
public:
	Person(string name, int age);
	// 重载 == 号,其余类似
	bool operator==(Person& p); 
	string m_Name;
	int m_Age;
};
Person::Person(string name, int age)
{
	this->m_Name = name;
	this->m_Age = age;
}
bool Person::operator==(Person& p)
{
	if (this->m_Name == p.m_Name && this->m_Age == p.m_Age)
	{
		return true;
	}
	else
	{
		return false;
	}
}
void test()
{
	Person p1("Tom", 18);
	Person p2("Tom", 18);
	Person p3("李华", 18);
	if (p1 == p2)
	{
		cout << "p1 == p2" << endl;
	}
	else
	{
		cout << "p1 != p2" << endl;
	}
}

5.6 函数调用运算符

  • 函数调用运算符()也可重载
  • 由于重载后使用的方式非常像函数的调用,因此称为仿函数
  • 仿函数没有固定的写法,非常灵活
class MyPrint
{
public:
	void operator()(string test)
	{
		cout << test << endl;
	}
};
class Add
{
public:
	int operator()(int num1, int num2)
	{
		return num1 + num2;
	}
};
void test()
{
	MyPrint myPrint;
	myPrint("hello world");  // 仿函数

	Add add;
	cout << "和为:" << add(1, 2) << endl;  // 仿函数非常灵活没有固定的写法
	cout << "和为:" << Add()(2, 3) << endl;  // 匿名的函数对象 `Add()`
}

6、 继承

继承是面向对象三大特性之一

有些类与类之间存在特殊关系

如:动物 -> 猫 -> 加菲猫

我们发现,定义这些类时,下级别的成员除了拥有上一级的共性,还有自己的属性

这时候我们可以考虑利用继承的技术,减少重复代码

6.1 基础语法

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

例如,很多网站中,都有公共的头部,公共的底部,甚至公共的左侧列表栏

/*
class Java
{
public:
	Java()  // 使用构造函数,输出内容
	{
		cout << "Java 页面:" << endl;
		header();
		buttom();
		left();
		content();
		cout << "=====" << endl;
	}
	void header()
	{
		cout << "首页、公开课、登录、注册..." << endl;
	}
	void buttom()
	{
		cout << "帮助中心、交流合作、站内地图..." << endl;
	}
	void left()
	{
		cout << "Java / Python / C++ ..." << endl;
	}
	void content()
	{
		cout << "关于 Java 的课程" << endl;
	}
};
class Python  // 许多内容和 Java 的内容一样
{
public:
	Python()  
	{
		cout << "Python 页面:" << endl;
		header();
		buttom();
		left();
		content();
		cout << "=====" << endl;
	}
	void header()
	{
		cout << "首页、公开课、登录、注册..." << endl;
	}
	void buttom()
	{
		cout << "帮助中心、交流合作、站内地图..." << endl;
	}
	void left()
	{
		cout << "Java / Python / C++ ..." << endl;
	}
	void content()
	{
		cout << "关于 Python 的课程" << endl;
	}
};
// 里面有很多函数重复
*/
// 使用继承思想
class PublicPage  // 定义公共页面
{
public:
	void header()
	{
		cout << "首页、公开课、登录、注册..." << endl;
	}
	void buttom()
	{
		cout << "帮助中心、交流合作、站内地图..." << endl;
	}
	void left()
	{
		cout << "Java / Python / C++ ..." << endl;
	}
};
class Java : public PublicPage
{
public:
	Java()
	{
		cout << "Java 页面:" << endl;
		header();
		buttom();
		left();
		content();
		cout << "=====" << endl;
	}
	void content()
	{
		cout << "关于 Java 的课程" << endl;
	}

};
class Python : public PublicPage
{
public:
	Python()
	{
		cout << "Python 页面:" << endl;
		header();
		buttom();
		left();
		content();
		cout << "=====" << endl;
	}
	void content()
	{
		cout << "关于 Python 的课程" << endl;
	}

};
void test()
{
	Java java;
	Python python;
}

子类也称为派生类

父类也称为基类

6.2 继承方式

继承不能访问私有权限的内容

继承方式有三种

  • 公共继承

    • 基类中所有 public 成员在派生类中为 public 属性
    • 基类中所有 protected 成员在派生类中为 protected 属性
    • 基类中所有 private 成员在派生类中不能使用
  • 保护继承

    • 基类中的所有 public 成员在派生类中为 protected 属性
    • 基类中的所有 protected 成员在派生类中为 protected 属性
    • 基类中的所有 private 成员在派生类中不能使用。
  • 私有继承

    • 基类中的所有 public 成员在派生类中均为 private 属性;
    • 基类中的所有 protected 成员在派生类中均为 private 属性
    • 基类中的所有 private 成员在派生类中不能使用
继承方式/基类成员 public成员 protected成员 private成员
public继承 public protected 不可见
protected继承 protected protected 不可见
private继承 private private 不可见

6.3 对象模型

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

// 继承中的对象模型
class Base
{
public:
	int m_A;
private:
	int m_B;
protected:
	int m_C;
};
class Son : public Base
{
public:
	int m_D;
};
void test()
{
	cout << "size of son:" << sizeof(Son) << endl;
	// 在父类中所有非静态成员属性都会被子类继承下去
	// 父类中私有成员属性,是被编译器给隐藏了,因此是访问不到的,但是继承下去了
}

利用开发人员命令提示工具查看对象模型

  1. 跳转盘符
  2. 跳转文件路径:cd 具体路径
  3. 查看命令:cl /d1 reportSingleClassLayout类名 文件名

6.4 构造和析构顺序

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

class Base
{
public:
	Base()
	{
		cout << "Base的构造函数" << endl;
	}
	~Base()
	{
		cout << "Base的析构函数" << endl;
	}
};
class Son : public Base
{
public:
	Son()
	{
		cout << "Son的构造函数" << endl;
	}
	~Son()
	{
		cout << "Son的析构函数" << endl;
	}

};
void test()
{
	Son son;  // 套娃,白发人送黑发人
}

6.5 同名成员处理

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

  • 访问子类同名成员:直接访问
  • 访问父类同名成员:需要加作用域
class Base
{
public:
	Base()
	{
		m_A = 100;
	}
	int m_A;
	void fn() 
	{
		cout << "Base" << endl;
	}
    void fn(int a)
    {
        cout << a << endl;
    }

};
class Son : public Base
{
public:
	Son()
	{
		m_A = 101;
	}
	int m_A;
	void fn()
	{
		cout << "Son" << endl;
	}
};
void test()
{
	Son s;
	cout << "s.m_A=" << s.m_A << endl;  // 直接访问
	cout << "Base.m_A=" << s.Base::m_A << endl;  // 添加作用域
	s.fn();
	s.Base::fn();
    s.Base::fn(100);
}

同名函数调用方式类似:添加作用域

如果子类中出现和父类同名的成员函数,子类的同名成员函数会隐藏掉父类中所有同名成员函数

如果想访问到父类中被隐藏的同名成员函数,需要加作用域

6.6 同名静态成员处理

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

  • 访问子类同名成员,直接访问即可
  • 访问父类同名成员,需要加作用域
class Base
{
public:
	static int m_A;  // 类内声明
	static void fn()
	{
		cout << "Base" << endl;
	}
};
int Base::m_A = 100;  // 类外要初始化
class Son : public Base
{
public:
	static int m_A;
	static void fn()
	{
		cout << "Son" << endl;
	}
};
int Son::m_A = 101;

void test() 
{
	cout << "通过类名访问数据" << endl;
	Son s;
	cout << "s.m_A=" << s.m_A << endl;
	cout << "Base.m_A=" << s.Base::m_A << endl;

	cout << "通过类名访问数据" << endl;
	cout << "Son.m_A=" << Son::m_A << endl;
	cout << "Base.m_A=" << Son::Base::m_A << endl; 
	// 第一对冒号代表通过类名的方式访问作用域,第二对冒号代表访问父类作用域下
}

函数调用方式类似

6.7 多继承语法

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

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

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

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

6.8 菱形继承

问题

  1. 当 B 继承了 A 的数据,C 同样继承了 A 的数据,当 D 使用数据时,就会产生二义性
  2. D 继承了两份数据,其实,我们清楚只要继承一份数据就可以了
class A
{
public:
	int m_Age;
};
/* 
利用虚继承解决菱形继承的问题
继承之前,加上关键字 virtual 变为虚继承
A 类称为虚基类
*/
class B : virtual public A{};
class C : virtual public A{};
class D : public B, public C{};
void test()
{
	D d;
	d.B::m_Age = 18;
	d.C::m_Age = 28;
	// 当出现菱形继承时,两个父类拥有相同的数据,需要加以区分
	cout << "B.m_Age = " << d.B::m_Age << endl;
	cout << "C.m_Age = " << d.C::m_Age << endl;
	// 这份数据只要有一份数据就可以了,菱形继承导致有两份数据,资源浪费
}

利用虚继承解决菱形继承的问题
继承之前,加上关键字 virtual 变为虚继承
A 类称为虚基类

7、 多态

7.1 基本概念

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

多态分为两类

  • 静态多态:[函数重载](#3、 函数重载) 和 [运算符重载](#5、 运算符重载)属于静态多态,复用函数名
  • 动态多态:派生类 和 虚函数实现运行时多态

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

  • 静态多态的函数地址早绑定 - 编译阶段函数地址
  • 动态多态的函数地址晚绑定 - 运行阶段确定函数地址
class A
{
public:
	virtual void speak()  // 虚函数,可以实现地址晚绑定
	{
		cout << "A is speaking" << endl;
	}
	
};
class B : public A 
{
public:
	void speak()
	{
		cout << "B is speaking" << endl;
	}
};
// 执行说话函数
void doSpeak(A& a)  // A& a = b;
{
	a.speak();  // 如果没加virtual,则地址早绑定,在编译阶段确定函数地址
	// 如果要执行 b.speak(); 需要在运行阶段进行绑定,地址晚绑定
}
void test()
{
	B b;
	doSpeak(b);
}

重写函数:函数返回值类型,函数名,参数列表完全相同

动态多态满足条件

  • 有继承关系
  • 子类重写父类的虚函数,子类重写函数可以不为虚函数

动态多态使用

  • 父类指针或者引用,执行子类对象

多态优点

  • 代码组织结构清晰
  • 可读性强
  • 利于前期和后期的扩展以及维护

7.2 案例-计算器类

// 如果有扩展功能,要实现开闭原则:对扩展进行开放,对修改进行关闭
class AbstarctCalc  // 抽象类
{
public:
	virtual int getResult()  // 虚函数
	{
		return 0;
	}
	int m_A;
	int m_B;
};
class Add : public AbstarctCalc  // 加法
{
public:
	int getResult()
	{
		return m_A + m_B;
	}
};
class Sub : public AbstarctCalc
{
public:
	int getResult()
	{
		return m_A - m_B;
	}
};
void test()
{
	// 多态使用条件
	// 父类指针或引用指向子类对象
	AbstarctCalc* abc = new Add;
	abc->m_A = 10;
	abc->m_B = 20;
	cout << abc->m_A << " + " << abc->m_B << " = " << abc->getResult() << endl;  // 执行子类对象
	delete abc;  // 销毁数据
	
	abc = new Sub;  
	abc->m_A = 10;
	abc->m_B = 20;
	cout << abc->m_A << " - " << abc->m_B << " = " << abc->getResult() << endl; 
}

7.3 纯虚函数和抽象类

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

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

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

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

抽象类的特点

  • 无法实现实例化对象
  • 子类必须重写抽象类中的纯虚函数,否则也属于抽象类
class Base
{
public:
	virtual void func() = 0;
};
class Son : public Base
{
public:
	void func()
	{
		cout << "Son" << endl;
	}
};
void test(Base& b)  // 引用父类指针
{
	b.func();
}
void test()
{
	Son s;
	test(s);
}

7.4 虚析构和纯虚析构

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

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

虚析构和纯虚析构的共性

  • 可以解决父类指针释放子类对象
  • 都需要有具体的函数实现

虚析构和纯虚析构的区别

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

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

纯虚析构语法:类内部:virtual ~类名() = 0; | 类外:类名::~类名() {}

// 虚析构和纯虚析构
class A
{
public:
	A()
	{
		cout << "A()" << endl;
	}
	virtual void speak() = 0;
	virtual ~A()
	{
		cout << "~A()" << endl;
	}
	// 纯虚析构 virtual ~A() = 0;
};
// A::~A(){}  // 纯虚析构需要有函数实现
class C : public A
{
public:
	C(string name)
	{
		cout << "C()" << endl;
		m_name = new string(name);  // 将数据开辟到堆区
	}
	void speak()
	{
		cout << *m_name << " is speaking" << endl;
	}
	string* m_name;  // 没有加 virtual 时,没有调用此析构函数
	~C()
	{
		if (m_name)
		{
			cout << "~C()" << endl;
			delete m_name;
			m_name = NULL;
		}
	}
};
void test()
{
	A* a = new C("Tom");
	a->speak();
	// 父类指针在析构时候,不会调用子类中析构函数,导致子类如果有堆区的属性,会出现资源浪费
	delete a;
}

虚析构函数或纯虚析构函数使用来解决通过父类指针释放子类对象问题

如果子类中没有数据开辟到堆区,可以不写为虚析构或纯虚析构

拥有纯虚析构函数的类也属于抽象类

五、 文件操作

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

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

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

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

操作文件分为三大类

  1. ofstream:写操作
  2. ifstream:读操作
  3. fstream:读写操作

1、 文本文件

1.1 写文件

1.1.1 步骤

  1. 包含头文件

    • #include <fstream>
      
  2. 创建流对象

    • ofstream ofs;
      
  3. 打开文件

    • ofs.open("文件路径", 打开方式);
      
  4. 写数据

    • ofs << "写入的数据";
      
  5. 关闭文件

    • ofs.close();
      

1.1.2 文件打开方式

文件打开模式标记
模式标记 适用对象 作用
ios::in ifstream
fstream
打开文件用于读取数据。如果文件不存在,则打开出错。
ios::out ofstream
fstream
打开文件用于写入数据。如果文件不存在,则新建该文件;如果文件原来就存在,则打开时清除原来的内容。
ios::app ofstream
fstream
打开文件,用于在其尾部添加数据。如果文件不存在,则新建该文件。
ios::ate ifstream 打开一个已有的文件,并将文件读指针指向文件末尾(读写指 的概念后面解释)。如果文件不存在,则打开出错。
ios:: trunc ofstream 打开文件时会清空内部存储的所有数据,单独使用时与 ios::out 相同。
ios::binary ifstream
ofstream
fstream
以二进制方式打开文件。若不指定此模式,则以文本模式打开。
ios::in | ios::out fstream 打开已存在的文件,既可读取其内容,也可向其写入数据。文件刚打开时,原有内容保持不变。如果文件不存在,则打开出错。
ios::in | ios::out ofstream 打开已存在的文件,可以向其写入数据。文件刚打开时,原有内容保持不变。如果文件不存在,则打开出错。
ios::in | ios::out | ios::trunc fstream 打开文件,既可读取其内容,也可向其写入数据。如果文件本来就存在,则打开时清除原来的内容;如果文件不存在,则新建该文件。

文件打开方式可以配合|使用

1.2 读文件

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

  1. 包含头文件

    • #include <fstream>
      
  2. 创建流对象

    • ifstream ifs;
      
  3. 打开文件

    • ifs.open("文件路径", 打开方式);
      if (!ifs.is_open())
      {
          cout << 文件打开失败 << endl;
          return;
      }  // ifs.is_open() 判断文件是否打开失败
      
  4. 读数据

    • // 四种方式读取
      // 第一种
      char buf[1024] = { 0 };  // 初始化字符数组
      while (ifs >> buf)
      {
          cout << buf << endl;
      }
      // 第二种
      char buf[1024] = { 0 };  // 初始化字符数组
      while (ifs.getline(buf, sizeof(buf)))
      {
          coutt << buf << endl;
      }
      // 第三种
      string buf;
      while (getline(ifs, buf))
      {
          cout << buf << endl;
      }
      // 第四种(不推荐)
      char c;
      while (c = ifs.get() != EOF)  // EOF 文件结尾
      {
          cout << c;
      }
      
  5. 关闭文件

    • ifs.close();
      

2、 二进制文件

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

打开方式为 ios::binary

2.1 写文件

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

语法:ostream& write(const char * buffer, int len);

参数

  • buffer:字符指针,指向内存中一段存储空间
  • len:读写的字节数
class Person
{
public:
	char m_Name[64];
	int m_Age;
};
// 包含头文件
// #include <fstream>

// 创建流对象
ofstream ofs;

// 打开文件
ofs.open("Person.txt", ios::binary | ios::out);
// 也可以:ofstream ofs("Person.txt", ios::binary | ios::out);

// 写文件
Person p = { "张三", 18 };
ofs.write((const char*)&p, sizeof(Person));

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

2.2 读文件

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

语法:istream& read(char *buffer, int len)

参数

  • buffer:字符指针,指向内存中一段存储空间
  • len:读写的字节数
class Person
{
    public:
    char m_Name[64];
    int m_Age;
};
// 包含头文件 #include <fstream>

// 创建流对象
ifstream ifs;
// 打开文件,并判断文件是否打开成功
ifs.open("Person.txt", ios::in | ios::binary);  
if (!ifs.is_open())
{
    cout << "文件打开失败" << endl;
    return;
}
// 读文件
Person p;
ifs.read((char*)&p, sizeof(Person));
cout << "姓名:" << p.m_Name << " 年龄:" << p.m_Age << endl;

// 关闭文件
ifs.close();
posted @ 2022-02-14 12:40  Kenny_LZK  阅读(620)  评论(0编辑  收藏  举报