2 c++编程-核心

 重新系统学习c++语言,并将学习过程中的知识在这里抄录、总结、沉淀。同时希望对刷到的朋友有所帮助,一起加油哦!

 

本章是继上篇 c++编程-基础 之后的 c++ 编程-核心。

生命就像一朵花,要拼尽全力绽放!死磕自个儿,身心愉悦!

1 程序内存分区模型

c++程序内存分区:

区域 存放内容 管理方式
代码区 存放代码的二进制代码 由操作系统进行管理
全局区 存放全局变量、静态变量、常量 由操作系统进行管理,该区域的数据在程序结束后由操作系统释放。

栈区

函数的参数、局部变量等 由编译器自动对内存分配和释放
堆区 利用new操作符开辟内存 由程序员分配和释放。若程序员不主动释放,程序结束由操作系统释放

四区存在意义:

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

 

1.1 程序运行前

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

代码区:

        存放CPU执行的机器指令

        代码区有两个特点:

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

                是只读的。原因是防止程序被意外修改。

全局区:

        存放全局变量、静态变量

        还包含一些常量:字符串常量、const修饰的全局常量。(const 修饰的局部常量不在该区)

       

1.2 程序运行后

栈区

函数的参数、局部变量等 由编译器自动对内存分配和释放
堆区 利用new操作符开辟内存 由程序员分配和释放。若程序员不主动释放,程序结束由操作系统释放

注意事项:

        不要返回局部变量的地址,因为栈区的数据由系统自动释放,使用了可能会出错。

栈区示例:

#include <iostream>
#include <string>

using namespace std;

int* test() {
	// 局部变量 存放栈区,在函数执行完后系统自动释放
	int a = 10;
	// 返回局部变量地址
	return &a;
}

int main()
{
	int* p = test();
	cout << *p << endl;  // 第一次打印正确,因为编译器做了保留
	cout << *p << endl;  //第二次使用数据就不再保留了 
	cout << *p << endl;

	system("pause");
	return 0;
}

堆区示例:

#include <iostream>
#include <string>

using namespace std;

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

int main()
{
	int* p = test();
	// 三次打印都是正确的
	cout << *p << endl;  
	cout << *p << endl; 
	cout << *p << endl;

	system("pause");
	return 0;
}

 

1.3 new操作符

作用:在堆区开辟数据

特点:

  • 只能在堆区开辟数据;
  • 由程序员手动开辟,手动释放;
  • 释放利用操作符delete;

语法:

        new 数据类型

利用new创建的数据,返回的是数据类型的指针。

示例1:基本语法

#include <iostream>
#include <string>

using namespace std;

int* test() {
	int* p = new int(10);
	return p;
}

int main()
{
	int* p = test();
	// 三次打印都是正确的
	cout << *p << endl;  
	cout << *p << endl; 
	cout << *p << endl;

	delete p;
	// cout << *p << endl; // 内存已被delete释放,再次访问就是非法操作,报错

	system("pause");
	return 0;
}

示例2:new 开辟数组

#include <iostream>
#include <string>

using namespace std;

//在堆区利用new开辟数组
void test() {
	// 创建10个元素的整型数组
	int* arr = new int[10];
	for (int i = 0; i < 10; i ++) {
		arr[i] = i;
	}

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

	// 释放数组
	// 释放数组要delete[]才可以
	delete[] arr;

	// 释放后打印会报错
	// for (int i = 0; i < 10; i++) {
		// cout << arr[i] << endl;
	// }
}

int main()
{
	test();

	system("pause");
	return 0;
}

 

2 c++引用

2.1 引用的基本使用

作用:给变量起别名。可以跟变量一样操作数据。

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

示例:

#include <iostream>
#include <string>

using namespace std;

int main()
{
	int a = 10;
	int& b = a;  // 引用
	cout << "a = " << a << endl;
	cout << "b = " << b << endl;

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

	// 从地址打印可以看出a和b指向的都是同一块内存
	cout << "a的地址为:" << &a << endl;
	cout << "b的地址为: " << &b << endl;

	system("pause");
	return 0;
}

 

 

2.2 引用注意事项

1、引用在定义时必须初始化。

        例如 int& b;//是错误

2、引用一旦初始化后,就不可以更改,即不能修改为其他变量的别名。

示例:

#include <iostream>
#include <string>

using namespace std;

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

	//int& c; // 错误,引用必须在定义时初始化
	int& c = a;// 一旦初始化后,就不可以更改
	c = b;// 这是赋值操作,不是更改引用

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

	// 从地址打印可以看出a和c指向的都是同一块内存,没有更改c的引用
	cout << "a的地址为:" << &a << endl;
	cout << "b的地址为: " << &b << endl;
	cout << "c的地址为:" << &c << endl;
	

	system("pause");
	return 0;
}

 

2.3 引用做函数参数

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

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

示例:

#include <iostream>
#include <string>

using namespace std;
// 1 值传递 
void swap01(int a,int b) {
	int tmp = a;
	a = b;
	b = tmp;
}

// 2 地址传递
void swap02(int* a, int* b) {
	int tmp = *a;
	*a = *b;
	*b = tmp;
}

// 3  引用传递
void swap03(int& a, int& b) {
	int tmp = a;
	a = b;
	b = tmp;
}

void testSwap01() {
	cout << "testSwap01:" << endl;
	int a = 10;
	int b = 20;

	swap01(a, b);
	cout << "a = " << a << endl;
	cout << "b = " << b << endl;
}

void testSwap02() {
	cout << "testSwap02:" << endl;
	int a = 10;
	int b = 20;

	swap02(&a, &b);
	cout << "a = " << a << endl;
	cout << "b = " << b << endl;
}

void testSwap03() {
	cout << "testSwap03:" << endl;
	int a = 10;
	int b = 20;

	swap03(a, b);
	cout << "a = " << a << endl;
	cout << "b = " << b << endl;
}

int main()
{
	testSwap01();
	testSwap02();
	testSwap03();

	system("pause");
	return 0;
}

总结:通过引用传参和按地址传参可以得到一样的效果,但是引用传参更简单方便。

 

2.4 引用做函数返回值

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

注意:

  • 不要返回局部变量的引用
  • 若函数返回的是引用,则函数调用可以作为左值

示例:

#include <iostream>
#include <string>

using namespace std;

// 函数返回引用
int& test() {
	static int a = 10;// 全局变量 在程序结束后才由系统释放
	return a;
}

int main()
{
	int& ref = test();
	cout << "ref = " << ref << endl;
	cout << "ref = " << ref << endl;
    
    // 函数返回的是引用,函数调用可以作为左值
	test() = 2000;
	cout << "ref = " << ref << endl;
	cout << "ref = " << ref << endl;

	system("pause");
	return 0;
}

结果:

ref = 10

ref = 10

ref = 2000

ref = 2000

 

2.5 引用的本质

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

#include <iostream>
#include <string>

using namespace std;

// 内部发现参数是引用,自动转换为 int* const x = &a;
void test(int& x) {
	x = 100; // ref是引用,自动转换为*x = 100;
}

int main()
{
	int a = 10;

	// 自动转换为指针常量 int* const ref = &a; 
    // 指针常量是指针指向不可以更改,也说明为什么引用不可以更改
	int& ref = a;
	// 内部发现ref是引用,编译器自动转换为:*ref = 20
	ref = 20;

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

	test(a);

	system("pause");
	return 0;
}

总结:c++推荐用引用,因为引用的本质是指针常量,但比使用指针方便,而且所有的指针操作编译器都自动在内部转换做了。

 

2.6 常量引用

作用:若不想在函数内部改变传入实参的值,用常量引用来做函数参数,防止函数内部误操作。

语法:const 数据类型 形参名

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

示例:

#include <iostream>
#include <string>

using namespace std;

// 形参用const修饰,防止内部误操作改了实参
void showValue(const int& x) {
	// x = 100; // 错误,在函数内部不可以修改实参的值
	cout << "showValue :" << x << endl;
}

int main()
{
	int a = 10;
	showValue(a);
	cout << "a = " << a << endl;

	// int& ref = 10; // 引用本身需要一个合法的内存空间,不能直接给引用赋值数值
	
	// 用const修饰就可以,原因:编译器优化代码为:int tmp =10; const int& ref = tmp; 
	// ref是tmp的别名,只是tmp这个原名我们不知道不可以使用。
	const int& ref = 10;

	system("pause");
	return 0;
}

 

3 函数高级

3.1 函数默认参数

作用:函数的形参列表中形参可以有默认值,在函数调用时可传入实参或不传入。

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

注意事项:

  1. 如果参数列表某个位置有默认值,则从这个位置后的参数都必须有默认值
  2. 函数声明和函数实现只能有一个地方有默认值,不能同时出现默认值。

示例:

#include <iostream>
#include <string>

using namespace std;

int func(int a, int b, int c) {
	return a + b + c;
}

// 正确案例
int func2(int a, int b=20, int c=30) {
	return a + b + c;
}

// 错误案例
// 参数有默认值后,从这个位置后的参数都必须有默认值
//int func3(int a, int b = 20, int c) {
//	return a + b + c;
//}

// 错误案例 声明和实现都有默认值
//int func4(int a, int b = 20, int c = 30);
//int func4(int a, int b = 20, int c = 30) {
//	return a + b + c;
//}

int main()
{
	cout << func(10, 20, 30) << endl;
	cout << func2(10) << endl;

	system("pause");
	return 0;
}

 

3.2 函数占位参数

函数列表中可以有占位参数,用来做占位,调用函数时比如传入该位置的参数。

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

示例:

#include <iostream>
#include <string>

using namespace std;

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

// 占位参数也可以给默认值
int func2(int a, int = 20) {
	cout << "print func2";
}

int main()
{
	// func(10); //  错误,必须传入占位参数位置的参数值
	func(10, 20);

	func2(10);

	system("pause");
	return 0;
}

 

3.3 函数重载

3.3.1 函数重载概述

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

函数重载的条件:

  1. 在同一个作用域下;
  2. 函数名相同;
  3. 函数参数类型不同,或参数个数不同,或参数顺序不同。

注意:函数返回值不同不能函数重载。

示例:

#include <iostream>
#include <string>

using namespace std;

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

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

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

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

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

// 函数返回值不同不能函数重载 会报错
//int func(double a, int b) {
//	cout << "print func (double a, int b)" << endl;
//	return 0;
//}

int main()
{
	func();
	func(1);
	func(1.23);
	func(1,1.2);
	func(1.2,1);

	system("pause");
	return 0;
}

 

3.3.2 函数重载注意事项

注意两点:

  • 引用参数作为重载条件。有const和无const引用参数仍然可以重载,调用方式不同。
  • 函数重载碰到默认参数,容易出错,尽量避免重载时使用默认参数。

示例:

#include <iostream>
#include <string>

using namespace std;
// 1 引用作为重载条件
void func(int& a) {
	cout << "print func(int& a)" << endl;
}

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

// 2 默认参数构成重载条件
void func2(int a, int b = 10) {
	cout << "print func2(int a,int b)" << endl;
}

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


int main()
{
	int a = 10;
	func(a);// 调用无const
	func(1);// 调用有const
	
	//func2(1);// 碰到默认参数产生歧义不知道调哪个函数,需要避免
	
	func2(1, 10);

	system("pause");
	return 0;
}

 

4 类和对象

面向对象三大特性:

  • 封装
  • 继承
  • 多态

c++认为万事万物皆为对象,每个对象都有属性和行为。

 

例如:

人可以作为对象,属性有姓名、年龄、身高、体重……,行为有走、跑、吃饭、睡觉……

车可以作为对象,属性有方向盘、轮胎、座椅……,行为有载人、放音乐、开空调……

 

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

 

4.1 封装

4.1.1 封装的意义

 封装的意义:

  1. 将属性和行为作为一个整体,表现事物
  2. 将属性和行为加以权限控制

 

意义1:将属性和行为作为一个整体,表现事物

语法:

        class 类名{

                访问权限:

                        属性

                        行为

        }

扩展:

  • 类中的属性和行为,统一称为 成员
  • 属性,又可称为成员属性,或成员变量
  • 行为,又可称为成员函数,或成员方法

示例:

#include <iostream>
#include <string>

using namespace std;

const double PI = 3.14;

class Circle {
	// 访问权限
public:
	// 属性:
	// 半径
	int m_r;

	// 行为:
	// 圆的周长
	double getZC() {
		return 2 * PI * m_r;
	}
};

int main()
{
	// 通过圆类 创建具体的圆对象
	// 实例化(通过一个类创建一个对象的过程)
	Circle c1;
	// 给圆对象的属性赋值
	c1.m_r = 10;

	// 获取对象的行为
	cout << c1.getZC() << endl;

	system("pause");
	return 0;
}

 

意义2:将属性和行为加以权限控制

访问权限有三种:

权限名 作用域
public 公共权限 类内可以访问,类外可以访问
protected 保护权限 类内可以访问,类外不可以访问。子类可以访问父类成员
private 私有权限 类内可以访问 类外不可以访问。子类不可以访问父类成员

示例:

#include <iostream>
#include <string>

using namespace std;

const double PI = 3.14;

class Person {
	// 访问权限
public:
	// 属性:
	// 姓名
	string m_name;

protected:
	// 汽车
	string m_car;

private:
	// 银行卡密码
	int m_password;

	// 行为:
private:	
	double func() {
		m_name = "张三";
		m_car = "奔驰";
		m_password = 1234567;
	}
};

int main()
{
	Person p;
	p.m_name = "echo";
	//p.m_car = "宝马";//不能访问
	//p.m_password = 123;//不能访问

	system("pause");
	return 0;
}

 

4.1.2 struct和class区别

struct和class唯一的区别在于默认访问权限不同。

区别:

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

示例:

#include <iostream>
#include <string>

using namespace std;

const double PI = 3.14;

class Test1 {
	int a;// 默认权限私有
};

struct Test2 {
	int a;// 默认权限公共
};

int main()
{
	Test1 t1;
	// t1.a = 10;// 报错

	Test2 t2;
	t2.a = 10;

	system("pause");
	return 0;
}

 

4.1.3 成员属性设置为私有

优点:

  1. 将所有成员属性私有化,自己控制读写权限;
  2. 对于写权限,可以检测数据写入的有效性。

示例:

#include <iostream>
#include <string>

using namespace std;

const double PI = 3.14;

class Men {
public:
	void setName(string name) {
		m_name = name;
	}

	string getName() {
		return m_name;
	}

	void setAge(int age) {
		if (age < 0 || age>150) {
			cout << "你输入的年龄错误" << endl;
			m_age = 0;
		}
		else {
			m_age = age;

		}
	}

	int getAge() {
		return m_age;
	}

	void setLover(string lover) {
		m_lover = lover;
	}

private:
	string m_name;
	int m_age;
	string m_lover;
};


int main()
{

	Men m;
	m.setName("张三");
	cout << m.getName() << endl;

	m.setAge(1000);
	cout << m.getAge() << endl;

	m.setLover("rose");
	//cout << m.getLover() << endl;  // 没有获取lover的函数
	//cout << m.m_lover << endl;  // 没有获取lover的权限


	system("pause");
	return 0;
}

 

4.2 对象的初始化和清理

4.2.1 构造函数和析构函数

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

  • 一个对象或变量没有初始化,对其使用的后果是未知的;
  • 使用完一个对象或变量,没有及时清理,也可能会造成安全问题。

c++利用构造函数和析构函数解决上述问题。

这两个函数在类使用过程中会被编译器自动调用,完成对象初始化和清理工作。

如果代码不提供构造和析构,编译器会提供默认的空的构造函数和析构函数并调用。

构造函数:主要用于创建对象时为成员属性赋值,构造函数由编译器自动调用,无须手动调用。

析构函数:主要用于对象销毁前系统自动调用,执行一些清理工作。

 

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

特点:

  1. 没有返回值,也不用写void;
  2. 函数名与类名相同;
  3. 构造函数也可以有参数,可以有重载;
  4. 程序在调用对象时,一定会自动调用调用一次构造;

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

特点:

  1. 没有返回值,也不用写void;
  2. 函数名与类名相同,在函数名前有一个~
  3. 析构函数不可以有参数,不能重载;
  4. 程序在对象销毁前会,一定会自动调用一次析构函数

示例:

#include <iostream>
#include <string>

using namespace std;

class Person {
public:
	Person() {
		cout << "这是构造函数" << endl;
	}

	~Person() {
		cout << "这是析构函数" << endl;

	}
};

void test() {
	Person p;
}

int main()
{
	test();

	system("pause");
	return 0;
}

结果:

这是构造函数

这是析构函数

 

4.2.2 构造函数的分类和调用

编辑

 

示例:
 

#include <iostream>
#include <string>

using namespace std;

class Person {
public:
	// 无参构造 也是普通构造
	Person() {
		cout << "这是构造函数 无参" << endl;
	}

	// 有参构造  也是普通构造
	Person(int a) {
		cout << "这是构造函数 有参" << endl;
	}

	// 拷贝构造
	Person(const Person& p) {
		cout << "这是构造函数 拷贝" << endl;
		name = p.name;
	}

	~Person() {
		cout << "这是析构函数" << endl;

	}

	string name;
};

//调用法
// 1 括号法
void test() {
	
	Person p1;
	Person p2(10);
	Person p3(p2);
	// 注意事项 调用默认构造函数时,不要加括号
	// 因为编译器会认为是一个函数声明,不会认为在创建对象
	// Person p4();//无法达到创建对象效果
}

// 2 显示法
void test2() {
	Person p21;
	Person p22 = Person(10);
	Person p23 = Person(p22);

	// Person(10);//是匿名对象 特点:当前结束后,系统会立即回收匿名对象 在当前c++14及后续版本不支持该种写法了
	// cout << "aaaa" << endl;

	// 注意事项:不要利用拷贝构造函数初始化匿名对象,编译器会认为Person(p23) ===Person p23 对象声明
	// Person(p23);//编译报错
}

// 3 隐式转换法
void test3() {
	Person p31 = 10;// 相当于写了  Person p31=Person(10); 有参构造
	Person p32 = p31;// 拷贝构造
}


int main()
{
	//test();
	//test2();
	test3();

	system("pause");
	return 0;
}

 

4.2.3 拷贝构造函数调用时机

  1. 使用一个已创建的对象来初始化另一个新对象;
  2. 值传递的方式给函数参数传值;
  3. 以值方式返回局部对象;

示例:

#include <iostream>
#include <string>

using namespace std;

class Person {
public:
	// 无参构造 也是普通构造
	Person() {
		cout << "这是构造函数 无参" << endl;
	}

	// 有参构造  也是普通构造
	Person(int age) {
		m_age = age;
		cout << "这是构造函数 有参" << endl;
	}

	// 拷贝构造
	Person(const Person& p) {
		cout << "这是构造函数 拷贝" << endl;
		m_age = p.m_age;
	}

	~Person() {
		cout << "这是析构函数" << endl;

	}

	int m_age;
};


// 1 使用一个已创建的对象来初始化另一个新对象;
void test1() {
	Person p1(10);
	Person p2(p1);
	cout << "p2的age = " << p2.m_age << endl;
}

// 2 值传递的方式给函数参数传值

// 使用值传递参数
void doTest2(Person p) {//会调拷贝构造函数
	p.m_age = 1000;
}

void test2() {
	Person p;
	p.m_age = 1;
	doTest2(p);

	// 值传递参数未改变p的值
	cout << "p的age = " << p.m_age << endl;

}

//使用引用传递,不会调用拷贝构造函数,会更改实参对象的值
void doTest22(Person& p) {
	p.m_age = 1000;
}

void test22() {
	Person p;
	p.m_age = 1;
	doTest22(p);

	cout << "p的age = " << p.m_age << endl;

}

// 3 以值方式返回局部对象
Person doTest3() {
	Person p1;// 无参构造函数
	cout << &p1 << endl;// 打印地址,看出两次不是同一个对象
	return p1;
}

void test3() {
	Person p = doTest3();// 拷贝构造函数
	cout << &p << endl;// 打印地址,看出两次不是同一个对象
}

int main()
{
	//test1();
	//test2();
	//test22();
	test3();

	system("pause");
	return 0;
}

 

4.2.4 构造函数调用规则

默认情况下,编译器至少给一个类默认添加三个函数:

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

构造函数调用规则如下:

  • 如果用户定义了有参构造函数,编译器就不再提供默认无参构造函数,但会提供默认拷贝构造函数。
  • 如果用户定义了拷贝构造函数,编译器就不再提供默认无参构造函数。

示例1:
 

#include <iostream>
#include <string>

using namespace std;

class Person {
public:
	// 无参构造 
	/*Person() {
		cout << "这是构造函数 无参" << endl;
	}*/

	// 有参构造
	Person(int age) {
		m_age = age;
		cout << "这是构造函数 有参" << endl;
	}

	// 拷贝构造
	/*Person(const Person& p) {
		cout << "这是构造函数 拷贝" << endl;
		m_age = p.m_age;
	}*/

	~Person() {
		cout << "这是析构函数" << endl;

	}

	int m_age;
};


// 1 如果用户定义了有参构造函数,编译器就不再提供默认无参构造函数,但会提供默认拷贝构造函数
void test() {
	// Person p; // 会报错,没有默认无参构造函数
	Person p1(10);
	Person p2(p1); // 会提供默认拷贝构造函数
	cout << "p2的age = " << p2.m_age << endl;
}


int main()
{
	test();

	system("pause");
	return 0;
}

示例2:

#include <iostream>
#include <string>

using namespace std;

class Person {
public:
	// 无参构造 
	/*Person() {
		cout << "这是构造函数 无参" << endl;
	}*/

	// 有参构造
	/*Person(int age) {
		m_age = age;
		cout << "这是构造函数 有参" << endl;
	}*/

	// 拷贝构造
	Person(const Person& p) {
		cout << "这是构造函数 拷贝" << endl;
		m_age = p.m_age;
	}

	~Person() {
		cout << "这是析构函数" << endl;

	}

	int m_age;
};


// 2 如果用户定义了拷贝构造函数,编译器就不再提供默认无参构造函数
void test() {
	//Person p; // 会报错,没有默认无参构造函数
}


int main()
{
	test();

	system("pause");
	return 0;
}

 

4.2.5 深拷贝和浅拷贝

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

深拷贝:在堆区重新申请空间,再拷贝操作

示例:

#include <iostream>
#include <string>

using namespace std;

class Person {
public:
	// 无参构造 
	Person() {
		cout << "这是构造函数 无参" << endl;
	}

	// 有参构造
	Person(int age, int height) {
		m_age = age;
		m_height = new int(height);
		cout << "这是构造函数 有参" << endl;
	}

	// 拷贝构造
	Person(const Person& p) {
		cout << "这是构造函数 拷贝" << endl;
		m_age = p.m_age;

		// 深拷贝
		// 如果不利用深拷贝在堆区创建新内存,而使用浅拷贝,会导致在析构函数重复释放堆区问题
		m_height = new int(*p.m_height);
	}

	~Person() {
		cout << "这是析构函数" << endl;
		if (m_height != NULL) {
			// 如果拷贝构造函数中不使用深拷贝,这里会重复释放。
			// p2的析构函数会先释放,p1在释放时m_height指针能取到地址,但是找不到可释放的内存。
			delete m_height;
			m_height = NULL;
		}
	}

public:
	int m_age;
	int* m_height;
};


void test() {
	Person p1(10, 180);
	Person p2(p1);

	cout << "p1的age = " << p1.m_age << ",height = " << *p1.m_height << endl;
	cout << "p2的age = " << p2.m_age << ",height = " << *p2.m_height << endl;
}


int main()
{
	test();

	system("pause");
	return 0;
}

 

4.2.6 初始化列表

作用:用来初始化类的成员属性

语法:

构造函数名(): 属性1(值1), 属性2(值2)……{   

}

示例:
 

#include <iostream>
#include <string>

using namespace std;

class Person {
public:
	Person(int a, int b, int c) :
		m_a(a),
		m_b(b),
		m_c(c) {

	}

public:
	int m_a;
	int m_b;
	int m_c;
};

void test() {
	Person p1(1, 2,3);
	
	cout << "p1的m_a = " << p1.m_a << endl;
	cout << "p1的m_b = " << p1.m_b << endl;
	cout << "p1的m_c = " << p1.m_c << endl;
}


int main()
{
	test();

	system("pause");
	return 0;
}

 

4.2.7 类对象作为类成员

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

例如:

class A{ }

class B{

        A a;

}

注意:

当其他类作为本类成员时,构造时先构造对象成员,再构造本类。析构函数顺序相反。
 

示例:

#include <iostream>
#include <string>

using namespace std;

class Phone {
public:
	Phone(string p_name) {
		m_p_name = p_name;
		cout << "Phone 构造函数" << endl;

	}

public:
	string m_p_name;
};

class Person {
public:
	//编译器在给m_phone实例化时内部自己做了: Phone m_phone = p_name 隐式转换法
	Person(string name,string p_name) :
		m_name(name),
		m_phone(p_name){
		cout << "Person 构造函数" << endl;
	}

public:
	string m_name;
	Phone m_phone;
};

// 当其他类作为本类成员时,构造时先构造 对象成员,再构造本类。析构函数顺序相反。

void test() {
	Person p1("张三","苹果");
	
	cout <<  p1.m_name << " 的手机: " << p1.m_phone.m_p_name << endl;
}


int main()
{
	test();

	system("pause");
	return 0;
}

结果:

        Phone 构造函数        

        Person 构造函数

        张三  的手机: 苹果

 

4.2.8 静态成员

静态成员:在成员变量或成员属性前加上static。

静态成员变量特点:

  1. 所有对象共享同一份内存
  2. 在编译阶段就已分配好内存
  3. 类内声明,类外需初始化

静态成员函数特点:

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

静态成员变量和静态成员函数都有两种访问方式:

  1. 通过对象访问
  2. 通过类名访问

另外,静态成员变量和静态成员函数都有访问权限,private权限的在类外都无法访问。

示例1:

#include <iostream>
#include <string>

using namespace std;

//静态成员变量特点:

class Demo {
public:
	//所有对象共享同一份内存
	//在编译阶段就已分配好内存
	//类内声明,类外需初始化
	static int  m_a;

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

//类内声明,类外需初始化
int Demo::m_a = 10;
int Demo::m_b = 20;

void test() {
	Demo d1;
	cout << d1.m_a << endl;

	Demo d2;
	d2.m_a = 123;

	// 修改d2影响到了d1,说明是共享内存
	cout << d1.m_a << endl;
}


void test2() {
	// 静态成员变量,所有对象共享同一份内存,所以它不属于某一个对象
	// 因此静态成员有两种调用方法

	// 1 通过对象访问
	Demo d1;
	cout << d1.m_a << endl;

	// 2 通过类名访问
	cout << Demo::m_a << endl;

	// cout << Demo::m_b << endl; // 报错,无访问权限
}

int main()
{
	//test();
	test2();

	system("pause");
	return 0;
}

示例2:

#include <iostream>
#include <string>

using namespace std;

//静态成员函数特点

class Demo {
public:
	//所有对象共享同一个函数
	//静态成员函数只能访问静态成员变量
	static void func() {
		m_a = 200;

		// 报错,不可以访问非静态成员变量。
		// 因为非静态成员变量一定是属于某一个对象的,而静态成员函数不属于某个对象,
		// 若在静态成员函数中修改了静态成员变量,就不知道修改的是哪个对象的变量。
		// m_b = 300; 

		cout << "Demo func" << endl;
	}

	static int  m_a;
	int m_b;

// 静态成员变量也有访问权限
private:
	static void func2() {
		cout << "Demo func2" << endl;
	}
};

//类内声明,类外需初始化
int Demo::m_a = 10;


void test() {
	// 静态成员函数,所有对象共享同一份内存,所以它不属于某一个对象
	// 因此静态成员函数有两种调用方法

	// 1 通过对象访问
	Demo d1;
	d1.func();

	// 2 通过类名访问
	Demo::func();

	// 报错,无访问权限
	// Demo::func2();

}

int main()
{
	test();

	system("pause");
	return 0;
}

 

4.3 对象模型和this指针

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

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

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

示例:

#include <iostream>
#include <string>

using namespace std;

//类内的成员变量和成员函数是分开存储。

class Demo {

};

// 只有非静态成员变量才属于类的对象上
class Demo2 {
	int m_a; // 非静态成员变量 占用对象空间
	
	static int m_b; // 静态成员变量 不占对象空间
	
	void func() { //非静态成员函数 不占对象空间,所有函数共享一个函数示例
		m_a = 2;
	}

	static void func2() { } //非静态成员函数 不占对象空间
};

//类内声明,类外需初始化
int Demo2::m_b = 10;

// 空类占用内存 1个字节
// c++编译器给每个空对象也分配一个自己空间,是为了区分各个空对象占内存的位置
// 每个空对象也应该有一个独一无二的内存地址
void test() {
	Demo d;
	cout << "空类占用内存:" << sizeof(d) << endl;
}

void test2() {
	Demo2 d;
	cout << "Demo2占用内存:" << sizeof(d) << endl;
}

int main()
{
	//test();
	test2();

	system("pause");
	return 0;
}

 

4.3.2 this指针概念

问题:

        4.3.1中我们知道成员函数不占对象内存,也就是说多个同类型的对象会公用一块代码。

        那么问题来了,这一块代码怎么区分是哪个对象来调用自己的呢?

解决办法:

        每个对象都有this指针,this指向被调用的成员函数所属对象。

this指针特点:

  1. 是隐含在每一个非静态成员函数内的一种指针。   
  2. this指针不需要定义,是编译器提供。

this指针用途:

  1. 当形参和成员变量同名时,用this区分。如this->n=n; 不能写成n=n; 
  2. 在类的非静态成员函数中返回对象本身,可使用 return *this;

思考问题:this指针为什么不能用在静态成员函数中呢?

原因:

        静态成员函数,在没有实例化对象时就可以调用,不属于某个具体的对象,也就是说用this无法指向到具体的对象,所以不能用。非静态成员函数属于具体的对象,执行到函数时对象已经创建,所以可以用this,表示对象自己。

示例:

#include <iostream>
#include <string>

using namespace std;

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

	// 需要返回Person&地址,不能返回Person值
	// 如果返回的是Person值则使用时会在使用时是创建一个新的对象,不是原来的对象。
	Person& addAge(const Person& p) {
		this->age += p.age;
		return *this;
	}

public:
	int age;
};

// 1 解决命名冲突
void test() {
	Person p(10);
	cout << "p的年龄:" << p.age << endl;
}

// 2 返回对象本身
void test2() {
	Person p1(10);
	Person p2(5);

	// 链式编程思想 可以无限追加,类似cout追加输出
	p2.addAge(p1).addAge(p1).addAge(p1);
	cout << "p2的年龄:" << p2.age << endl;
}

int main()
{
	// test();
	test2();

	system("pause");
	return 0;
}

 

4.3.3 空指针访问成员函数

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

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

示例:

#include <iostream>
#include <string>

using namespace std;

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

	void showAge() {
		// 用到this指针,如果是空指针,会系统崩溃,通常需要判断一下this指针是否为空
		if (this == NULL) {
			return;
		}
		// m_age 编译器会自动转换成 this->m_age
		cout << "showAge:" <<m_age << endl;
		cout << "showAge:" << this->m_age << endl;
	}

public:
	int m_age;
};

void test() {
	Person *p=NULL;
	p->showClassName(); // 空指针没问题,没有用到this指针
	p->showAge(); // 空指针有问题,用到this指针,会系统崩溃
}

int main()
{
	test();

	system("pause");
	return 0;
}

 

4.3.4 const修饰成员函数

常函数:

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

常对象:

  • 声明对象时前加const,称为常对象;
  • 常对象只能调用常函数;
  • 常对象不可以修改成员属性;
  • 成员属性声明时加关键字mutable,常对象可以修改。

示例:
 

#include <iostream>
#include <string>

using namespace std;

class Person {
public:
	void func() {
		m_age = 100;
	}

	// 常函数
	// this指针的本质:是指针常量,指向的对象地址是不可以修改的,不可以修改指向。
	//                但是this指向的值(值=对象的成员属性)可以修改
	//                实际是 Person * const this
	// 如果要让this指向的值也不可以修改,c++规定的语法是将const加在函数后面,称为常函数
	// 当前常函数实际是:const Person * const this
	// 
	void func() const 
	{
		// m_age = 100; // m_age 不可修改 常函数内不可以修改成员属性
		// this = NULL; //this指针不可以修改指针的指向

		m_a = 100;
	}

	void func2()
	{
		m_a = 100;
	}

	int m_age;
	mutable int m_a;// 特殊变量,加了mutable的成员属性,即使在常函数内,也可以修改
};

void test() {
	Person p;
}

void test2() {
	const Person p;// 在对象前加const,变为常对象
	// p.m_age = 100; // 常对象不可以修改普通的成员变量
	p.m_a = 100; // 加了mutable的成员属性,常对象可以修改

	// 常对象只能调用常函数
	p.func();
	// p.func2();// 常对象不可以调用普通成员函数,因为普通成员函数可以修改属性

}

int main()
{
	test();

	system("pause");
	return 0;
}

 

4.4 友元

案例:

生活中你家里有客厅和卧室,客厅是所有客人都可以进去,但是你的卧室是私密的,只有你可以进去,然后一些特殊的人,比如你的好朋友好闺蜜好基友也可以进去。

在c++中,有些私有属性,也想让类外特殊的一些函数或者类可以访问,就需要用到友元

友元的作用:让一个函数或类,访问另一个类中的私有成员。

友元关键字:friend

友元的三种实现:

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

 

4.4.1 全局函数做友元

#include <iostream>
#include <string>

using namespace std;

class Building {
	// goodFriend全局函数是Building的好朋友,可以访问Building的私有成员属性
	friend void goodFriend(Building& h);

public:
	Building() {
		m_sitting_room = "客厅";
		m_bed_room = "卧室";
	}
	
public:
	string m_sitting_room;
private:
	string m_bed_room;
};

// 全局函数
void goodFriend(Building& h){
	cout << "好朋友全局函数 访问:" << h.m_sitting_room << endl;
	cout << "好朋友全局函数 访问:" << h.m_bed_room << endl;
}

void test() {
	Building h;
	goodFriend(h);
}

int main()
{
	test();

	system("pause");
	return 0;
}

 

4.4.2 类做友元

#include <iostream>
#include <string>

using namespace std;

class Building {
	// GoodFriend 类是Building的好朋友,可以访问Building的私有成员属性
	friend class GoodFriend;

public:
	Building() {
		m_sitting_room = "客厅";
		m_bed_room = "卧室";
	}

public:
	string m_sitting_room;
private:
	string m_bed_room;
};

class GoodFriend {
public:
	GoodFriend();
	void visit();
	~GoodFriend();

	Building* building;
};

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

void GoodFriend::visit() {
	cout << "好朋友类 访问:" << building->m_sitting_room << endl;
	cout << "好朋友类 访问:" << building->m_bed_room << endl;
}

GoodFriend:: ~GoodFriend() {
	if (this->building != NULL) {
		delete this->building;
	}
}

void test() {
	GoodFriend gf;
	gf.visit();
}

int main()
{
	test();

	system("pause");
	return 0;
}

4.4.3 成员函数做友元

#include <iostream>
#include <string>

using namespace std;

class Building;

// 注意:友元函数所在的类需要写在前面
class GoodFriend {
public:
	GoodFriend();
	void visit();
	void visit2();
	~GoodFriend();

public:
	Building* building;
};

class Building {
	// GoodFriend 类是Building的好朋友,可以访问Building的私有成员属性
	friend void GoodFriend::visit();

public:
	Building();

public:
	string m_sitting_room;
private:
	string m_bed_room;
};

Building::Building() {
	m_sitting_room = "客厅";
	m_bed_room = "卧室";
}

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

void GoodFriend::visit() {
	cout << "visit 好朋友成员函数 访问:" << building->m_sitting_room << endl;
	cout << "visit 好朋友成员函数 访问:" << building->m_bed_room << endl;
}

void GoodFriend::visit2() {
	cout << "visit2 好朋友成员函数 访问:" << building->m_sitting_room << endl;
}

GoodFriend:: ~GoodFriend() {
	if (building != NULL) {
		delete building;
	}
}

void test() {
	GoodFriend gf;
	gf.visit();
	gf.visit2();

}

int main()
{
	test();

	system("pause");
	return 0;
}

 

4.5 运算符重载

运算符重载:对已有的运算符重新定义,赋予其另外一种能力,以适应不同数据类型。

 

4.5.1 加号运算符重载

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

实现方式:

  1. 通过类的成员函数来实现;
  2. 通过全局函数来实现

编译器给加号运算符重载起了一个名称:operator+

 

示例:

#include <iostream>
#include <string>

using namespace std;

class Demo {

public:
	Demo(int a,int b) {
		m_a = a;
		m_b = b;
	}

	// 通过成员函数重载+号
	//Demo operator+(Demo& d) {
	//	Demo tmp = d;
	//	tmp.m_a = this->m_a + d.m_a;
	//	tmp.m_b = this->m_b + d.m_b;
	//	return tmp;
	//}

public:
	int m_a;
	int m_b;
};

// 通过全局函数重载+号
Demo operator+ (Demo& d1, Demo& d2) {
	Demo tmp = d1;
	tmp.m_a = d1.m_a + d2.m_a;
	tmp.m_b = d1.m_b + d2.m_b;
	return tmp;
}

// 调成员函数测试
void test() {
	Demo d1(10, 20);
	Demo d2(10, 20);
	//Demo d3 = d1.operator+(d2);// 简化为: Demo d3 = d1 + d2;
	Demo d3 = d1 + d2;

	cout << d3.m_a << endl;
	cout << d3.m_b << endl;
}

// 调全局函数测试
void test2() {
	Demo d1(1, 2);
	Demo d2(1, 2);
	//Demo d3 = operator+(d1,d2);// 简化为: Demo d3 = d1 + d2;
	Demo d3 = d1 + d2;

	cout << d3.m_a << endl;
	cout << d3.m_b << endl;
}

int main()
{
	//test();
	test2();

	system("pause");
	return 0;
}

 

4.5.2 左移运算符重载

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

注意:

  1. 通常不会利用成员函数来重载<<元算符,因为无法实现cout在左侧;
  2. 只能通过全局函数重载<<

示例:

#include <iostream>
#include <string>

using namespace std;

class Demo {

public:
	Demo(int a,int b) {
		m_a = a;
		m_b = b;
	}

	// 通过成员函数重载左移运算符
	// 调用方式:p.operator<<(cout),简化为 p << cout
	// 该简化版本cout在右侧,不是我们想要的效果。
	// 所以通常不会利用成员函数来重载<<元算符,因为无法实现cout在左侧。
	ostream& operator<< (ostream& cout) {
		cout << this->m_a << endl;
		cout << this->m_b << endl;
		return cout;
	}

public:
	int m_a;
	int m_b;
};

// 只能通过全局函数重载<<
ostream& operator<<(ostream& cout, Demo& d) {
	cout << d.m_a << endl;
	cout << d.m_a << endl;
	return cout;
}

// 调成员函数测试
void test() {
	Demo d1(10, 20);
	// d1.operator<<(cout);// 简化为 d1 << cout
	d1 << cout;
	cout << "hell";
}

// 调全局函数测试
void test2() {
	Demo d(1, 2);
	cout << d << "test" << endl;
}

int main()
{
	//test();
	test2();

	system("pause");
	return 0;
}

 

4.5.3 递增运算符重载

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

需要实现前置递增(++i)、后置递增(i++)。

重载后置递增,通过int占位符来区分。

示例1:类的成员函数实现

#include <iostream>
#include <string>

using namespace std;

class MyInteger {
	friend ostream& operator<<(ostream& cout, const MyInteger& myint);
public:
	MyInteger() {
		m_num = 0;
	}

	// 重载前置递增 ++
	// 返回引用是为了一直对同一个数据进行递增操作
	MyInteger& operator++() {
		// 先自增
		++m_num;
		// 再返回
		return *this;
	}

	// 重载后置递增 通过int占位符来区分前置和后置递增
	// 不能返回引用,因为tmp是局部变量
	MyInteger operator++(int) {
		// 先记录当时结果
		MyInteger tmp = *this;

		// 再递增
		m_num++;

		// 最后将记录结果返回
		return tmp;
	}

private:
	int m_num;
};

// 重载<<
ostream& operator<<(ostream& cout, const MyInteger& myint) {
	cout << myint.m_num;
	return cout;
}
void test() {
	MyInteger myint;

	cout << "前置递增输出:" << ++(++myint) << endl;
	cout << "前置递增输出:" << myint << endl;

	cout << "后置递增输出:" << myint++ << endl;
	cout << "后置递增输出:" << myint << endl;
}

int main() {
	test();

	system("pause");
	return 0;
}

示例2:全局函数实现前置递增(无法实现后置递增)

#include <iostream>
#include <string>

using namespace std;

class MyInteger {
	friend ostream& operator<<(ostream& cout, const MyInteger& myint);
public:
	MyInteger() {
		m_num = 0;
	}

public:
	int m_num;
};

// 全局函数 重载前置递增 ++
// 无法用全局函数 重载后置递增
MyInteger& operator++(MyInteger& myint) {
	++myint.m_num;
	return myint;
}

// 重载<<
ostream& operator<<(ostream& cout, const MyInteger& myint) {
	cout << myint.m_num;
	return cout;
}
void test() {
	MyInteger myint;

	cout << "前置递增输出:" << ++(++myint) << endl;
	cout << "前置递增输出:" << myint << endl;

	/*cout << "后置递增输出:" << myint++ << endl;
	cout << "后置递增输出:" << myint << endl;*/
}

int main() {
	test();

	system("pause");
	return 0;
}

 

4.5.4 赋值运算符重载

c++编译器给一个类至少提供4个函数:

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

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

示例:


#include <iostream>
#include <string>

using namespace std;
class Demo {
public:
	Demo(int a) {
		// 开辟到堆区
		m_a = new int(a);
	}
	
	~Demo() {
		if (m_a != NULL) {
			delete m_a;
			m_a = NULL;
		}
	}

	// 重载赋值运算符
	Demo& operator=(Demo& d) {
		if (m_a != NULL) {
			delete m_a;
			m_a = NULL;
		}

		// 编译器提供的浅拷贝
		//m_a = d.m_a;

		// 提供深拷贝,解决浅拷贝问题
		m_a = new int(*d.m_a);

		return *this;
	}

public:
	int* m_a;
};

void test() {
	Demo demo1(1);
	Demo demo2(2);
	Demo demo3(3);

	demo3 = demo2 = demo1;

	cout << "demo1.m_a = " << *demo1.m_a << endl;
	cout << "demo2.m_a = " << *demo2.m_a << endl;
	cout << "demo3.m_a = " << *demo3.m_a << endl;
}

int main() {
	test();

	system("pause");
	return 0;
}

 

4.5.5 关系运算符重载

作用:比较两个自定义类型对象

示例:
 


#include <iostream>
#include <string>

using namespace std;
class Person {
public:
	Person(string name, int age) {
		m_name = name;
		m_age = age;
	}

	// 重载 == 号
	bool operator==(Person& p)
	{
		if (this->m_name == p.m_name && this->m_age == p.m_age)
		{
			return true;
		}
		else
		{
			return false;
		}
	}

	// 重载 != 号
	bool operator!=(Person& p) {
		if (this->m_name != p.m_name || this->m_age != p.m_age)
		{
			return true;
		}
		else
		{
			return false;
		}
	}

public:
	string m_name;
	int m_age;
};

void test() {
	Person p1("p1",10);
	Person p2("p1", 10);
	Person p3("p3", 10);

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

int main() {
	test();

	system("pause");
	return 0;
}

 

4.5.6 函数调用运算符重载

  • 函数调用运算符 () 也可以重载
  • 由于重载后使用的方式非常像函数的调用,因此称为仿函数。
  • 仿函数没有固定的写法,非常灵活。

示例:


#include <iostream>
#include <string>

using namespace std;
class MyPrint {
public:
	// 重载 () 也称为仿函数
	void operator()(string str) {
		cout << str << endl;
	}
};

void test() {
	MyPrint p;
	p("hello");
}

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

void test2() {
	MyAdd add;
	cout<<"1 + 3 = " << add(1, 3) << endl;

	// MyAdd()为匿名对象,匿名对象调用仿函数
	// 当不想创建对象是可以创建匿名对象
	cout << "MyAdd()(3,4) = " << MyAdd()(3,4) << endl;
}

int main() {
	//test();
	test2();

	system("pause");
	return 0;
}

 

4.6 继承

有些类与类之间存在特殊的关系,例如下图:

  编辑

 从上图可以看出,下级别的成员除了拥有上级的共性,还有自己的特性。这种情况下,可以使用继承,减少代码的重复。

 

4.6.1 继承的基本语法

作用:减少代码重复

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

        例:class A : public B

        子类 也称为 派生类

        父类 也称为 基类

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

  1. 从基类继承过来的
  2. 自己增加的成员

从基类继承过来的表现为共性,自己增加的成员是自己的个性。

 

案例:

在很多网站中,都有公共的头部、底部、左侧的列表栏,只有中间内容部分不同。

接下来分别利用普通写法和继承写法来实现网页中的内容,看下继承的意义和好处。

 

普通实现:


#include <iostream>
#include <string>

using namespace std;

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

	void footer() {
		cout << "帮助中心、联系方式……(公共底部)" << endl;
	}

	void left() {
		cout << "JAVA、C++、PYTHON……(公共左侧分类列表)" << endl;
	}

	void content() {
		cout << "JAVA 各类视频" << endl;
	}
};

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

	void footer() {
		cout << "帮助中心、联系方式……(公共底部)" << endl;
	}

	void left() {
		cout << "JAVA、C++、PYTHON……(公共左侧分类列表)" << endl;
	}

	void content() {
		cout << "CPP 各类视频" << endl;
	}
};

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

	void footer() {
		cout << "帮助中心、联系方式……(公共底部)" << endl;
	}

	void left() {
		cout << "JAVA、C++、PYTHON……(公共左侧分类列表)" << endl;
	}

	void content() {
		cout << "PYTHON 各类视频" << endl;
	}
};

void test() {
	cout << "java--------------------------------------" << endl;
	JAVA j;
	j.head();
	j.footer();
	j.left();
	j.content();

	cout << "CPP--------------------------------------" << endl;
	CPP cpp;
	cpp.head();
	cpp.footer();
	cpp.left();
	cpp.content();

	cout << "PYTHON--------------------------------------" << endl;
	PYTHON PYTHON;
	PYTHON.head();
	PYTHON.footer();
	PYTHON.left();
	PYTHON.content();
}

int main() {
	test();

	system("pause");
	return 0;
}

继承实现:


#include <iostream>
#include <string>

using namespace std;
// 公共页面
class BasePage {
public:
	void head() {
		cout << "首页、公开课、登录、注册……(公共头部)" << endl;
	}

	void footer() {
		cout << "帮助中心、联系方式……(公共底部)" << endl;
	}

	void left() {
		cout << "JAVA、C++、PYTHON……(公共左侧分类列表)" << endl;
	}
};

class JAVA:public BasePage {
public:
	void content() {
		cout << "JAVA 各类视频" << endl;
	}
};

class CPP:public BasePage {
public:
	void content() {
		cout << "CPP 各类视频" << endl;
	}
};

class PYTHON :public BasePage {
public:
	void content() {
		cout << "PYTHON 各类视频" << endl;
	}
};

void test() {
	cout << "java--------------------------------------" << endl;
	JAVA j;
	j.head();
	j.footer();
	j.left();
	j.content();

	cout << "CPP--------------------------------------" << endl;
	CPP cpp;
	cpp.head();
	cpp.footer();
	cpp.left();
	cpp.content();

	cout << "PYTHON--------------------------------------" << endl;
	PYTHON PYTHON;
	PYTHON.head();
	PYTHON.footer();
	PYTHON.left();
	PYTHON.content();
}

int main() {
	test();

	system("pause");
	return 0;
}

两种实现方式达到的效果一样,测试代码完全相同。继承方式实现的代码没有重复。

 

4.6.2 继承方式

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

 

继承方式有三种:

  1. 公共继承
  2. 保护继承
  3. 私有继承

编辑

 示例:


#include <iostream>
#include <string>

using namespace std;
class Base {
public:
	int m_a;
protected:
	int m_b;
private:
	int m_c;
};

class Son1 : public Base {
public:
	void func() {
		m_a = 1;// 父类公共成员,子类中还是 公共成员
		m_b = 2;// 父类保护成员,子类中还是 保护成员
		//m_c = 3;// 父类公共成员,子类访问不到
	}
	
};

class Son2 : protected Base {
public:
	void func() {
		m_a = 1;// 父类公共成员,子类中变为 保护成员
		m_b = 2;// 父类保护成员,子类中还是 保护成员
		//m_c = 3;// 父类公共成员,子类访问不到
	}

};

class Son3 : private Base {
public:
	void func() {
		m_a = 1;// 父类公共成员,子类中变为 私有成员
		m_b = 2;// 父类保护成员,子类中还是 私有成员
		//m_c = 3;// 父类公共成员,子类访问不到
	}

};

class GrandSon3 : public Son3 {
public:
	void func() {
		//m_a = 1;// 父类Son3私有成员,孙子类中访问不到
		//m_b = 2;// 父类Son3私有成员,孙子类中访问不到
	}

};


void test1() {
	Son1 son;
	son.m_a = 2;
	//son.m_b = 2;// 类外访问不到 保护成员
	//son.m_c = 2;// 类外访问不到 私有成员
}

void test2() {
	Son2 son;
	//son.m_a = 2;// 类外访问不到 保护成员
	//son.m_b = 2;// 类外访问不到 保护成员
	//son.m_c = 2;// 类外访问不到 私有成员
}

void test3() {
	Son3 son;
	//son.m_a = 2;// 类外访问不到 私有成员
	//son.m_b = 2;// 类外访问不到 私有成员
	//son.m_c = 2;// 类外访问不到 私有成员
}

int main() {
	test1();

	system("pause");
	return 0;
}

 

4.6.3 继承中的对象模型

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

结论:

  1. 父类中的所有非静态成员属性都会被子类继承;
  2. 父类中的私有成员属性,只是被编译器隐藏了,因此访问不到,但是还会被继承下去。

示例:


#include <iostream>
#include <string>

using namespace std;
class Base {
public:
	int m_a;
protected:
	int m_b;
private:
	int m_c;
};

class Son :public Base {
public:
	int m_d;
};

void test() {
	cout << "size of son = " << sizeof(Son) << endl;
}

int main() {
	test();

	system("pause");
	return 0;
}

 

还可以通过开发人员命令提示工具来验证结论

查看命令:cl /d1 reportSingleClassLayout类名  文件名

 

该工具是vs自带的一个命令行工具,我用的是2022版本vs,工具位置如下:

编辑

打开后切换到需要查看类所在文件路劲:

编辑

 然后输入命令:cl /d1 reportSingleClassLayoutSon  "4.6.3 继承中的对象模型.cpp"

reportSingleClassLayoutSon 意思是:报告单个类的布局Son

(注意:cl中l是字母l,d1中的1是数字1。后面引号中的文件名在窗口中打几个字用tab补齐)

就可以看到父类、子类的属性,及子类的大小:

 编辑

 

4.6.4 继承中构造和析构顺序

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

继承中构造和析构的顺序:

  1. 调用时,先构造父类,再构造子类;
  2. 析构顺序与构造顺序相反。

示例:
 


#include <iostream>
#include <string>

using namespace std;
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;

}

int main() {
	test();

	system("pause");
	return 0;
}

 

4.6.5 继承同名成员处理方式

1、子类和父类同名成员属性调用方式:

  • 调子类成员属性:直接调用即可。—— 子类名.属性
  • 调父类成员属性:需要在成员前加父类名作为作用域。——子类名.父类名::属性

2、同名成员函数调用方式与成员属性一样。

        注意:如果子类出现父类的同名函数,则子类的同名成员函数会将父类中同名成员函数全部隐藏掉(包括有参无参的,全部同名重载函数)。如果要访问父类中被隐藏调的同名成员函数只能加父类名称的作用域。

示例:


#include <iostream>
#include <string>

using namespace std;
class Base {
public:
	Base() {
		m_a = 100;
	}

	void fun()
	{
		cout << "Son fun 调用" << endl;
	}

	void fun(int a)
	{
		cout << "Son fun(int a)调用" << endl;
	}
	int m_a;
};

class Son :public Base {
public:
	Son() {
		m_a = 200;
	}

	void fun()
	{
		cout << "Son fun 调用" << endl;
	}

	int m_a;
};

void test() {
	Son son;
	cout << son.m_a << endl;
	cout << son.Base::m_a << endl;// 同名属性,父类要加作用域调用

	Son son2;
	son2.fun();// 直接调用子类的成员函数

	// 同名成员函数,父类要加作用域调用,不管是重载多少个,都要加作用域。
	// 原因:子类同名函数会将父类同名函数隐藏掉
	son2.Base::fun();
	son2.Base::fun(1);
	
}

int main() {
	test();

	system("pause");
	return 0;
}

 

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

继承中,同名静态成员属性和静态成员函数在子类对象上访问,与4.6.5章节非静态的一样。

  • 访问子类同名成员,直接访问;
  • 访问父类同名成员,需要带父类名称作为作用域访问。

#include <iostream>
#include <string>

using namespace std;
class Base {
public:
	static void fun()
	{
		cout << "Son fun 调用" << endl;
	}

	static void fun(int a)
	{
		cout << "Son fun(int a)调用" << endl;
	}
	static int m_a;
};

int Base::m_a = 100;

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

	static int m_a;
};

int Son::m_a = 200;

void test() {
	// 一、同名静态属性子类访问
	 
	// 1、通过对象来访问同名属性
	Son son;
	cout << son.m_a << endl;
	cout << son.Base::m_a << endl;// 同名属性,父类要加作用域调用

	// 2、通过子类名来访问同名属性
	cout << Son::m_a << endl;
	// 同名属性,父类要加作用域调用
	// 第一个 ::代表通过类名方式访问。第二个::代表访问父类作用域下
	cout << Son::Base::m_a << endl;


	// 二、同名静态成员子类访问
	
	// 1、通过对象来访问同名函数
	Son son2;
	son2.fun();// 直接调用子类的成员函数
	// 同名成员函数,父类要加作用域调用,不管是重载多少个,都要加作用域。
	// 原因:子类同名函数会将父类同名函数隐藏掉
	son2.Base::fun();
	son2.Base::fun(1);

	// 2、通过子类名来访问同名属性
	Son::fun();// 直接调用子类的成员函数
	// 同名成员函数,父类要加作用域调用,不管是重载多少个,都要加作用域。
	// 原因:子类同名函数会将父类同名函数隐藏掉
	Son::Base::fun();
	Son::Base::fun(1);

}

int main() {
	test();

	system("pause");
	return 0;
}

 

4.6.7 多继承语法

允许一个类继承多个父类。

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

由于多个父类中可能出现同名成员,在使用时需要加作用域区分。

正因为这个问题,不建议使用多继承开发模式

示例:


#include <iostream>
#include <string>

using namespace std;
class Base1 {
public:
	Base1() {
		m_a =100;
	}

public:
	int m_a;
};

class Base2 {
public:
	Base2() {
		m_a = 200;
	}

public:
	int m_a;
};

class Son:public Base1,public Base2 {
public:
	Son() {
		m_c = 300;
	}

public:
	int m_c;
};

void test() {
	Son son;
	cout << "size of Son = " << sizeof(son) << endl;
	// 当父类中出现同名成员,要加父类的作用域做区分
	cout << "son.Base1::m_a= " << son.Base1::m_a << endl;
	cout << "son.Base2::m_a= " << son.Base2::m_a << endl;

}

int main() {
	test();

	system("pause");
	return 0;
}

 

4.6.8 菱形继承

定义:

        两个派生类继承一个基类

        又有一个类继承了两个派生类

        这种继承称为菱形继承

示意图:

                       编辑

 典型的菱形继承案例:

                  编辑

 菱形继承的问题:

1、羊继承了动物,骆驼继承了动物,当羊驼使用数据时会产生二义性。

         解决方法:使用作用域来解决。

        例如:动物有最大年龄属性,羊有自己的最大年龄,骆驼也有自己的最大年龄,那羊驼

                使用最大年龄时,需要加上羊的作用域能访问到羊的最大年龄,加上骆驼的作用域

                能访问到骆驼的最大年龄

2、羊驼继承自动物的数据继承了两份,但是羊驼只需要一份。

        例如:羊驼的最大年龄只可能有一种,不可能有两种

        解决方法:

                利用虚继承来解决。

                在继承的父类之前加上关键字  virtual ,变为虚继承。

                 继承的基类,称为 虚基类。

示例一:继承基类


#include <iostream>
#include <string>

using namespace std;

class Animal {
public:
	int m_max_age;
};

class Sheep:public Animal {

};

class Tuo :public Animal {

};

class SheepTuo :public Sheep,public Tuo {

};

void test() {
	SheepTuo sheep_tuo;
	// sheep_tuo.m_max_age = 50; //  无法直接访问,会报错
	sheep_tuo.Sheep::m_max_age = 50;
	sheep_tuo.Tuo::m_max_age = 60;
	// 菱形继承,当两个父类拥有同名成员,需要加作用域来区分才能访问
	cout << "sheep_tuo.Sheep::m_max_age = "<< sheep_tuo.Sheep::m_max_age << endl;
	cout << "sheep_tuo.Tuo::m_max_age = " << sheep_tuo.Tuo::m_max_age << endl;
	// 占用内存 8
	cout << "sizeof(sheep_tuo) = " << sizeof(sheep_tuo) << endl;

	// 但是上述代码产生了问题,羊驼有两份最大年龄数据,造成资源浪费。
	// 这个时候要使用虚基类来解决
}

int main() {
	test();

	system("pause");
	return 0;
}

示例二:继承虚基类


#include <iostream>
#include <string>

using namespace std;

class Animal {
public:
	int m_max_age;
};

// 利用虚继承 来解决 菱形继承 两份数据的问题
// 在继承的基类之前加上 关键字 virtual 变为虚基类
// Aniaml 称为虚基类
class Sheep:virtual public Animal {

};

class Tuo :virtual public Animal {

};

class SheepTuo :public Sheep,public Tuo {

};

void test() {
	// 羊驼有两份最大年龄数据,造成资源浪费。
	// 这个时候要使用虚基类来解决
	SheepTuo sheep_tuo2;
	sheep_tuo2.m_max_age = 60;
	// 可以直接访问羊驼的最大年龄,也可以通过作用域访问到羊和骆驼的最大年龄,三个数据打印都是一样的,只有一份数据。
	cout << "sheep_tuo2.m_max_age = " << sheep_tuo2.m_max_age << endl;
	cout << "sheep_tuo2.Sheep::m_max_age = " << sheep_tuo2.Sheep::m_max_age << endl;
	cout << "sheep_tuo2.Tuo::m_max_age = " << sheep_tuo2.Tuo::m_max_age << endl;

	// 占用内存 12
	// 可以看出利用虚基类占用内存会增大
	cout << "sizeof(sheep_tuo2) = " << sizeof(sheep_tuo2) << endl;
}

int main() {
	test();

	system("pause");
	return 0;
}

总结:

另外,通过开发者命令工具查看到以下结果:

在不使用虚基类时,所占内存小,但是继承了基类的两份数据。

使用虚基类时,所在内存大,通过继承虚基类指针的形式,只继承一份基类数据。

查看继承基类的结果:

编辑

 查看继承虚基类的结果:

 编辑

 

4.7 多态

4.7.1 多态基本概念

多态分为两类:

  1. 静态多态:函数重载、运算符重载
  2. 动态多态:派生类和虚函数实现运行时多态。

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

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

 

在理解动态多态之前,需要先了解如下几个概念:

虚函数概念:

        在函数前面加上关键字 virtual,叫虚函数。

        语法:virtual void func(){}

函数重写:

         函数返回值类型、函数名、参数列表,都要完全相同,不是函数重载。

        函数重写一般发生在子类继承父类,子类重写父类的函数。

 

在子类继承父类时,如果想让子类执行子类内部重写父类的函数,那么这个函数就不能早绑定地址,需要在运行阶段再绑定地址,地址晚绑定——即实现动态多态。

动态多态满足条件:

  1. 有继承关系
  2. 父类定义的是虚函数
  3. 子类要重写父类虚函数(重写的这个函数展现的是动态多态特性)

动态多态使用方法:
        父类的指针或引用执行子类对象

注意:

  • 父类定义的虚函数,子类可以不重写,只有子类重写的虚函数才展现动态多态特性。
  • 对于未重写的虚函数,子类通过父类指针或引用仍然可以调用。

 

示例一:静态多态


#include <iostream>
#include <string>

using namespace std;
class Animal {
public:
	void speak() {
		cout << "动物在叫" << endl;
	}
};

class Cat:public Animal {
public:
	void speak() {
		cout << "cat在叫" << endl;
	}
};

class Dog:public Animal {
public:
	void speak() {
		cout << "Dog在叫" << endl;
	}
};

// 执行speak函数
// 地址早绑定,在编译阶段就确定函数地址
void doSpeak(Animal &animal) {
	animal.speak();
}

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

	Dog dog;
	doSpeak(dog);
}

int main() {
	test();

	system("pause");
	return 0;
}

输出:

动物在叫

动物在叫

 

分析:

实际上从test()函数体内代码可以看出,想要的结果是:传入cat就想要cat在叫,传入dog就想要dog在叫。因此该代码并不能实现我们的目的。这个时候就需要函数地址晚绑定。

 

示例二:动态多态


#include <iostream>
#include <string>

using namespace std;
class Animal {
public:
	// 虚函数 
	// 在函数前面加上关键字 virtual,叫虚函数
	virtual void speak() {
		cout << "动物在叫" << endl;
	}
};

class Cat:public Animal {
public:
	void speak() {
		cout << "cat在叫" << endl;
	}
};

class Dog:public Animal {
public:
	void speak() {
		cout << "Dog在叫" << endl;
	}
};

// 执行speak函数
// 如果想执行让不同的动物说不同的话,那么这个函数就不能早绑定地址,需要在运行阶段再绑定地址,地址晚绑定

// 函数重写:函数返回值类型、函数名、参数列表,都要完全相同,不是函数重载。

// 动态多态满足条件:
// 1、 有继承关系
// 2、 子类要重写父类虚函数

// 动态多态使用方法:
// 父类的指针或引用执行子类对象
void doSpeak(Animal &animal) {
	animal.speak();
}

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

	Dog dog;
	doSpeak(dog);
}

int main() {
	test();

	system("pause");
	return 0;
}

输出:

        cat在叫

        dog在叫

 

示例三:子类通过父类指针或引用调用未重写父类虚函数


#include <iostream>
#include <string>

using namespace std;
class Animal {
public:
	// 虚函数 
	// 在函数前面加上关键字 virtual,叫虚函数
	virtual void speak() {
		cout << "动物在叫" << endl;
	}

	virtual void eat() {
		cout << "动物在吃" << endl;
	}
};

class Cat:public Animal {
public:
	void speak() {
		cout << "cat在叫" << endl;
	}
};

class Dog:public Animal {
public:
	void speak() {
		cout << "Dog在叫" << endl;
	}
};

void doEat(Animal& animal) {
	animal.eat();
}

void test() {
	Cat cat;
	doEat(cat);

	Dog dog;
	doEat(dog);
}

int main() {
	test();

	system("pause");
	return 0;
}

输出:

        动物在吃

        动物在吃

 

4.7.2 多态原理

主要使用开发者命令工具来剖析。

1、静态多态分析:

class Animal {
public:
void speak() {
cout << "动物在叫" << endl;
}
};

class Cat:public Animal {
public:
void speak() {
cout << "cat在叫" << endl;
}
};

用命令查看:

类中成员函数是不占内存的,所以以上两类相当于是空类,都只占1字节空间。

编辑

编辑

 2、动态多态

class Animal {
public:
	virtual void speak() {
		cout << "动物在叫" << endl;
	}
};

class Cat:public Animal {
public:
	void speak() {
		cout << "cat在叫" << endl;
	}
};

用命令查看:

编辑

 Animal类占4字节内存空间,原因是保存了一个vfptr来指向vftable。

vfptr-虚函数(表)指针

v-virtual

f-function

ptr-pointer

vftable-虚函数表

v-virtual

f-function

table-table

vftable表内记录基类虚函数的地址:&Animal::speak

编辑

 派生类Cai类占4字节内存空间,原因是保存了一个vfptr来指向vftable。

但是,vftable表内记录的是自身类虚函数的地址:&Cat::speak

正是因为基类与派生类vftable表中记录的地址不同,才实现了动态多态。

 

再扩展下:

若基类有多个虚函数,仍然只占4字节内存,只保一个vfptr来指向vftable,vftable里记录了多个虚函数地址。

class Animal {
public:
	virtual void speak() {
		cout << "动物在叫" << endl;
	}

	virtual void eat() {
		cout << "动物在吃" << endl;
	}
};

编辑

 

4.7.3 多态优点

  1. 代码组织结果清晰
  2. 代码可读性强
  3. 利于前期和后期代码扩展和维护

案例:分别利用普通写法和多态写法,实现两个数运算的计算器。

示例一:普通写法


#include <iostream>
#include <string>

using namespace std;

class Caculator {
public:
	int getResult(string oper) {
		if (oper == "+") {
			return m_num1 + m_num2;
		}
		else if (oper == "-") {
			return m_num1 - m_num2;
		}
		else if (oper == "*") {
			return m_num1 * m_num2;
		}

		// 如果想扩展功能,就要在这里修改源码
		// 在实际开发中,提倡 开闭原则
		// 开闭原则:可扩展开发,关闭修改。
	}

public:
	int m_num1;
	int m_num2;
};

void test() {
	Caculator c;
	c.m_num1 = 20;
	c.m_num2 = 10;

	cout << c.m_num1 << " + " << c.m_num2 << " = " << c.getResult("+")<<endl;
	cout << c.m_num1 << " - " << c.m_num2 << " = " << c.getResult("-") << endl;
	cout << c.m_num1 << " * " << c.m_num2 << " = " << c.getResult("*") << endl;

}

int main() {
	test();

	system("pause");
	return 0;
}

示例二:多态写法


#include <iostream>
#include <string>

using namespace std;
class BaseCaculator {
public:
	virtual int getResult() {
		return 0;
	}

public:
	int m_num1;
	int m_num2;
};

class Add :public BaseCaculator {
	int getResult() {
		return m_num1 + m_num2;
	}
};

class Sub :public BaseCaculator {
	int getResult() {
		return m_num1 - m_num2;
	}
};

class Mul :public BaseCaculator {
	int getResult() {
		return m_num1 * m_num2;
	}
};

void test() {
	// 父类指针或引用指向子类对象
	Add c;
	c.m_num1 = 20;
	c.m_num2 = 10;
	BaseCaculator* base = &c;
	cout << base->m_num1 << " + " 
        << base->m_num2 << " = " 
        << base->getResult() << endl;

	Sub s;
	s.m_num1 = 20;
	s.m_num2 = 10;
	base = &s;
	cout << base->m_num1 << " - " 
        << base->m_num2 << " = " 
        << base->getResult() << endl;

	Mul m;
	m.m_num1 = 20;
	m.m_num2 = 10;
	base = &m;
	cout << base->m_num1 << " * " 
        << base->m_num2 << " = " 
        << base->getResult() << endl;
}

int main() {
	test();

	system("pause");
	return 0;
}

 

4.7.4 纯虚函数和抽象类

在多态中,通常父类中定义的虚函数是用不到的,主要调用的是派生类中重写的函数。

所以,可以将父类中的虚函数定义为纯虚函数

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

当类中有一个纯虚函数,该类也称为抽象类

抽象类特点:

  1. 无法实例化
  2. 子类必须重写抽象类中的纯虚函数,否则也为抽象类

示例:


#include <iostream>
#include <string>

using namespace std;
class Base {
public:
	// 纯虚函数
	virtual void func() = 0;
};

class Son :public Base {
public:
	void func() {
		cout << "Son子类func" << endl;
	}
};

class Son2 :public Base {
public:
	void func2() {
		cout << "Son2子类func2" << endl;
	}
};

void test() {
	//Base b;// 抽象类无法实例化对象
	//new Base;// 抽象类无法实例化对象

	Son s;
	Base* b = &s;
	b->func();

	//Son2 s;//子类必须重写父类中的纯虚函数,否则也属于抽象类,无法实例化对象
}

int main() {
	test();

	system("pause");
	return 0;
}

 

4.7.5 多态案例-制作饮品

制作饮品的流程分四步:煮水、冲泡、倒入杯中、加入辅料。

利用多态的技术,提供抽象的饮品制作基类,提供子类制作茶和咖啡。


#include <iostream>
#include <string>

using namespace std;
// 利用抽象类定义制作饮品的四个步骤
class AbstracktDrinking {
public:
	virtual void Boil() = 0;

	virtual void Brew() = 0;

	virtual void PourInCup() = 0;

	virtual void PutSomething() = 0;

	void makeDrink() {
		Boil();
		Brew();
		PourInCup();
		PutSomething();
	}
};

class MakeTea :public AbstracktDrinking {
public:
	virtual void Boil() {
		cout << "1 煮矿泉水" << endl;
	}

	virtual void Brew() {
		cout << "2 放入茶叶泡茶" << endl;
	}

	virtual void PourInCup() {
		cout << "3 倒入杯中" << endl;
	}

	virtual void PutSomething() {
		cout << "4 放入枸杞" << endl;
	}
};

class MakeCoffe :public AbstracktDrinking {
public:
	virtual void Boil() {
		cout << "1 煮农夫山泉" << endl;
	}

	virtual void Brew() {
		cout << "2 放入咖啡" << endl;
	}

	virtual void PourInCup() {
		cout << "3 倒入杯中" << endl;
	}

	virtual void PutSomething() {
		cout << "4 放入牛奶、白糖" << endl;
	}
};

void doWork(AbstracktDrinking* drink) {
	drink->makeDrink();
}

void test() {
	doWork(new MakeTea);

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

	doWork(new MakeCoffe);
}

int main() {
	test();

	system("pause");
	return 0;
}

 

4.7.6 虚析构和纯虚析构

问题:

        在多态使用时,如果子类有开辟到堆区的数据,需要在子类析构函数中释放。当使用父类指针指向子类对象时,无法调用到子类的析构函数。

解决方法:将父类的析构函数定义为虚析构或纯虚析构。

 

父类虚析构和纯虚析构,共性:

  • 都可以解决父类指针指向子类对象释放子类对象的问题
  • 都需要写具体实现代码

父类虚析构和纯虚析构,区别:

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

 

虚析构语法:

        virtual ~类名(){  }

纯虚析构语法:

  1. 在父类定义纯虚析构:        virtual ~类名() = 0;
  2. 在父类外定义纯虚构函数对应函数实现:             类名::~类名(){     }

示例:


#include <iostream>
#include <string>

using namespace std;
class Animal {
public:
	// 纯虚函数
	virtual void speak()=0;

	// 构造函数不可以是虚函数
	// virtual Animal() = 0;

	// 先调 父类构造函数,再调 子类构造函数
	Animal() {
		cout << "Animal 构造函数" << endl;
	}

	// 使用父类指针指向子类对象,在释放时,析构函数调用情况:
	// 1 如果父类是 析构函数,则只走 父类析构,不走 子类析构
	// 2 如果父类是 虚析构函数,则先走 子类析构,再走 父类析构
	// 3 如果父类是 纯虚析构函数,同样先走 子类析构,再走 父类析构
	
	// 析构
	/* ~Animal() {
		cout << "Animal 析构函数" << endl;
	}*/

	// 虚析构
	/* virtual ~Animal() {
		cout << "Animal 虚析构函数" << endl;
	}*/

	// 纯虚析构
	// 纯虚析构定义方法:
	// 1 在父类定义纯虚析构: virtual ~类名() = 0;
	// 2 在父类外定义纯虚构函数对应函数实现:类名::~类名(){}
	virtual ~Animal() = 0;

};

Animal:: ~Animal() {
	cout << "Animal 纯虚析构函数" << endl;
}

class Cat :public Animal {
public:
	void speak() {
		cout << "cat 在说话" << endl;
	}

	Cat(string name) {
		cout << "Cat 构造函数" << endl;
		m_name = new string(name);
	}

	~Cat() {
		cout << "Cat 析构函数" << endl;
		if (m_name != NULL) {
			delete m_name;
			m_name = NULL;
		}
	}

	string* m_name;
};

void test() {
	Animal* animal = new Cat("Tom");
	animal->speak();

	delete animal;
}

int main() {
	test();

	system("pause");
	return 0;
}

4.7.6 多态案例-组装电脑

#include <iostream>
#include <string>

using namespace std;
class CPU {
public:
	virtual void caculate() = 0;
};

class Memory {
public:
	virtual void storage() = 0;
};

class VidoCard {
public:
	virtual void display() = 0;
};

class Computer {
public:
	Computer(CPU* cpu, Memory* mem,VidoCard* vc) {
		m_cpu = cpu ;
		m_mem = mem;
		m_vc = vc;
	}

	void dowork() {
		m_cpu->caculate();
		m_mem->storage();
		m_vc->display();
	}

	~Computer() {
		if (m_cpu != NULL) {
			delete m_cpu;
			m_cpu = NULL;
		}
		if (m_mem != NULL) {
			delete m_mem;
			m_mem = NULL;
		}
		if (m_vc != NULL) {
			delete m_vc;
			m_vc = NULL;
		}
	}

private:
	CPU* m_cpu;
	Memory* m_mem;
	VidoCard* m_vc;
};

// Intel厂商零件
class IntelCPU :public CPU {
	void caculate() {
		cout << "Intel CPU 在 计算" << endl;
	}
};

class IntelMemory :public Memory {
	void storage() {
		cout << "Intel 内存条 在 计算" << endl;
	}
};

class IntelVidoCard :public VidoCard {
	void display() {
		cout << "Intel 显卡 在 显示" << endl;
	}
};

// Huawei厂商零件
class HuaweiCPU :public CPU {
	void caculate() {
		cout << "Huawei CPU 在 计算" << endl;
	}
};

class HuaweiMemory :public Memory {
	void storage() {
		cout << "Huawei 内存条 在 计算" << endl;
	}
};

class HuaweiVidoCard :public VidoCard {
	void display() {
		cout << "Huawei 显卡 在 显示" << endl;
	}
};

void test() {
	cout << "------------------------------" << endl;
	cout << "组装第一台电脑:" << endl;
	IntelCPU* intelCPU = new IntelCPU;
	IntelMemory* intelMemory = new IntelMemory;
	IntelVidoCard* intelVidoCard = new IntelVidoCard;
	Computer* computer1 = new Computer(intelCPU, intelMemory, intelVidoCard);
	computer1->dowork();
	delete computer1;
	/*if (intelCPU != NULL) {
		delete intelCPU;
		intelCPU = NULL;
	}*/

	cout << "------------------------------" << endl;
	cout << "组装第二台电脑:" << endl;
	Computer* computer2 = new Computer(new HuaweiCPU, new HuaweiMemory, new HuaweiVidoCard);
	computer2->dowork();
	delete computer2;

	cout << "------------------------------" << endl;
	cout << "组装第三台电脑:" << endl;
	Computer* computer3 = new Computer(new HuaweiCPU, new IntelMemory, new HuaweiVidoCard);
	computer3->dowork();
	delete computer3;
}

int main() {
	test();

	system("pause");
	return 0;
}

5 c++文件操作

通过文件将数据持久化。

头文件需要包括   fstream

文件类型包括两种:

  1. 文本文件:文件以文本的ASCII码形式存储
  2. 二进制文件:文件以二进制形式存储,用户一般无法读懂。01的形式。

操作文件的三大类:

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

 

5.1 文件文本

5.1.1 写文件

写文件步骤:

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

文件打开方式:

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

注意: 文件打开方式可以配合使用,利用 | 操作符

例如:用二进制方式写文件   ios::binary  |  ios::out

示例:


#include <iostream>
#include <string>
#include <fstream>

using namespace std;

void test() {
	ofstream ofs;

	ofs.open("testOutFile.txt",ios::out | ios::trunc);

	ofs << "姓名:张三" << endl;
	ofs << "性别:男" << endl;
	ofs << "年龄:18" << endl;

	ofs.close();
}

int main() {
	test();

	system("pause");
	return 0;
}

 

5.1.2 读文件

读文件和写文件步骤类似,但是读取方式较多。

读文件步骤:

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

示例:


#include <iostream>
#include <string>
#include <fstream>

using namespace std;

void test() {
	// 1 包含头文件: #include <fstream>
	// 2 创建流对象: ifstream ifs;
	ifstream ifs;

	// 3 打开文件并判断文件是否打开成功: ifs.open("文件路径", 打开方式);
	ifs.open("testOutFile.txt", ios::in);
	// 判断是否打开
	if (!ifs.is_open()) {
		cout << "打开文件失败" << endl;
		return;
	}

	// 4 读数据: 四种方式读取
	// 第一种方式
	char buf[1024] = {0};
	while (ifs >> buf) {
		cout << buf << endl;
	}

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

	// 第三种
	//string buf;
	//while (getline(ifs, buf)) {
	//	cout << buf << endl;
	//}

	// 第四种  不推荐,按字符读取效率低
	//char c;// 按字符一个个读取
	//while ((c = ifs.get()) != EOF) //EOF  指的是文件尾部
	//{
	//	cout << c;
	//}

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

int main() {
	test();

	system("pause");
	return 0;
}

 

5.2 二进制文件

以二进制方式对文件进行读写操作,与文本方式步骤一致,打开方式要指定为 ios::binary

 

5.2.1 写文件

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

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

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

示例:


#include <iostream>
#include <string>
#include <fstream>

using namespace std;

class Person {
public:
	char m_name[64];
	int m_age;
};

void test() {
	// 1 包含头文件
	// 2 创建流对象
	ofstream ofs;

	// 3 打开文件
	ofs.open("Person.txt", ios::out | ios::binary);
	
	// 4 写文件
	Person p = { "张三",20 };
	ofs.write((const char *)&p, sizeof(p));// 强转 (const char *)
	
	// 5 关闭文件
	ofs.close();
}

int main() {
	test();

	system("pause");
	return 0;
}

 

5.2.2 读文件

利用流对象调用成员函数read读取二进制数据。

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

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

示例:


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

class Person {
public:
	char m_name[64];
	int m_age;
};

void test() {
	// 1 包含头文件
	
	//2 创建流对象
	ifstream ifs;
	
	//3 打开文件 并判断文件是否成功打开
	ifs.open("Person.txt", ios::in);
	if (!ifs.is_open()) {
		cout << "打开文件失败" << endl;
		return;
	}
	
	//4 读文件
	Person p;
	ifs.read((char*)&p, sizeof(Person));
	cout << "读取二进制文件内容:" << endl;
	cout << p.m_name << endl;
	cout << p.m_age << endl;

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

int main() {
	test();

	system("pause");
	return 0;
}

 

 


posted on 2022-11-24 11:31  爱学习的小灵子  阅读(30)  评论(0编辑  收藏  举报