老九学堂 学习C++ 第十天

10继承

10.1 概念:

所谓 “继承(inheritance)” 就是在一个已经存在的类基础上,再建立一个新类

从已有的类派生出新的类,派生类就继承了原有类(基类)的特征,包括成员和方法

通过继承可以完成以下的一些功能:

  1. 可以在已有类的基础上添加新功能,如对于数组类,可以添加数学计算
  2. 可以给类添加数据成员,如对于字符串类,可以派生出一个类,并添加指定成员表示颜色
  3. 可以修改类方法的行为,如对于普通英雄类,可以派生出拥有更丰富技能的近战英雄类

注:继承机制只需提供新特性,甚至不需要访问源码就可以派生出类(即如果购买的类库只提供了类的方法的头文件和编译后的代码,仍可以使用库中的类派生出的新类,而且可以在不公开实现的情况下将自己的类分发给其他人,同时允许他们在类中添加新特性)

使用继承的优点

  1. 基类定义公共内容,方便统一修改
  2. 重定义基类的成员函数
  3. 添加新类、新成员(职业)方便

注:

  1. 派生类对象存储了积累的数据成员,即:派生类继承了基类的实现
  2. 派生类对象可以使用基类的非私有函数(私有的不能访问),即:派生类继承了基类的接口
  3. 派生类需要自己的构造函数
  4. 派生类可以根据需要添加额外的数据成员和函数

10.2 继承方式:

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

老九学堂 学习C++ 第十天_第1张图片

老九学堂 学习C++ 第十天_第2张图片
公有继承(public inheritance)

基类的公有成员和受保护成员,在派生类中保持原来的访问属性,其私有成员仍为基类所独有

私有继承(private inheritance)

基类的公有成员和受保护成员,在派生类中成为了私有成员,私有成员仍为基类独有

受保护继承(private inheritance)

基类公有成员和受保护成员,在派生类中成了受保护成员,私有成员仍为基类独有

派生类的成员访问有三种方式:

  1. 公有访问:公有权限下,基类、派生类及外部都可以访问
  2. 私有访问: 私有权限下,只能基类访问,派生类及外部都无法访问
  3. 保护访问:受保护权限下,基类和派生类可以访问,外部无法访问

继承方式供给程序员对类进行封装的机制:

1.全部继承,不封装基类,那么用公有继承
2.全部继承,完全封装基类,那么使用私有继承
3.全部继承,有选择封装基类,那么使用受保护继承

不管是哪种继承,派生类都不能访问基类的私有成员(除非改成protected)

继承的C++实现

class Base1 {
public:
	int m_a;
protected:
	int m_b;
private:
	int m_c;
};
//公有继承
class Son1 : public Base1 {
public:
	void func() {
		m_a = 10;//基类中的公共权限成员,在子类中依然是公共权限
		m_b = 20;//基类中的保护权限成员,在子类中依然是保护权限
		//m_c = 30;//基类中的私有成员,在子类中不可访问
	}
};
void test01() {
	Son1 s1;
	s1.m_a = 10;
	//s1.m_b = 10;//到son1中,m_b是保护权限,类外不可访问
}
//保护继承
class Son2 : protected Base1 {
public:
	void func() {
		m_a = 10;//基类中的公共权限成员,在子类中变为保护权限
		m_b = 10;//基类中的保护权限成员,在子类中依然是保护权限
		//m_c = 10;//基类中的私有成员,在子类中不可访问
	}
};
void test02() {
	Son2 s2;
	//s2.m_a = 10;//到son2中,m_a变为保护权限,类外不可访问
	//s2.m_b = 10;//到son2中,m_b变为保护权限,类外不可访问
}
//私有继承
class Son3 : private Base1 {
public:
	void func() {
		m_a = 10;//基类中的公共权限成员,在子类中变为私有成员
		m_b = 10;//基类中的保护权限成员,在子类中变为私有成员
		//m_c = 10;//基类中的私有成员,在子类中不可访问
	}
};
void test03() {
	Son3 s3;
	//s3.m_a = 10;//到son3中,m_a变为私有成员,类外不可访问
	//s3.m_b = 10;//到son3中,m_b变为私有成员,类外不可访问
}

10.3 继承下的C++对象模型

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

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

在继承中,构造的顺序:先构造基类,在构造子类,析构则相反

10.4 继承同名成员处理方式

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

class Base {
public:
	Base() {
		m_a = 10;
	}
	void func() {
		cout << "Base 的func()调用" << endl;
	}
	void func(int a) {
		cout << "Base 的func(int a)调用" << endl;
	}
	int m_a;
};
class Son : public Base {
public:
	Son() {
		m_a = 20;
	}
	void func() {
		cout << "Son 的func()调用" << endl;
	}
	int m_a;
};
//同名成员函数的处理
void test() {
	Son s1;
	s1.func();//直接调用是调用子类中的同名成员函数
	s1.Base::func();
	//如果子类中出现和父类同名的成员函数,子类的同名成员会隐藏掉父类中所有同名成员函数
	//s1.func(100);
	//解决方案:加作用域
	s1.Base::func(10);
}
int main() {
	Son s;
	cout << "Son中的m_a = " << s.m_a << endl;
	//通过子类对象访问父类的同名成员,加作用域
	cout << "Base中的m_a = " << s.Base::m_a << endl;
	test();
}

总结:

  1. 子类对象可以直接访问到子类中同名成员
  2. 子类对象加作用域可以访问到父类同名成员
  3. 当子类与父类拥有同名的成员函数,子类会隐藏父类中同名成员函数,加作用域可以访问到父类中同名函数

继承中同名静态成员处理方式与非静态成员同名处理方式一致

静态成员属性特点:编译阶段分配内存;所有对象共享同一份数据;类内声明,类外初始化

class Base {
public:
	static void func() {
		cout << "Base 的func()调用" << endl;
	}
	static void func(int a) {
		cout << "Base 的func(int a)调用" << endl;
	}
	static int m_a;//静态成员属性特点:编译阶段分配内存;所有对象共享同一份数据;类内声明,类外初始化
};
int Base::m_a = 10;
class Son : public Base {
public:
	static void func() {
		cout << "Son 的func()调用" << endl;
	}
	static int m_a;
};
int Son::m_a = 5;
//同名静态成员属性
void test1() {
	//1.通过对象访问
	Son s;
	cout << "通过对象访问" << endl;
	cout << "Son中的m_a = " << s.m_a << endl;
	cout << "Base中的m_a = " << s.Base::m_a << endl;
	//2.通过类名访问
	cout << "通过类名访问" << endl;
	cout << "Son中的m_a = " << Son::m_a << endl;
	//第一个::代表通过类名方访问,第二个::代表访问父类作用域下
	cout << "Base中的m_a = " << Son::Base::m_a << endl;
}
//同名静态成员函数
void test2() {
	Son s1;
	//通过对象访问
	cout << "通过对象访问" << endl;
	s1.func();
	s1.Base::func();
	//通过类名访问
	cout << "通过类名访问" << endl;
	//子类出现和父类同名静态成员函数,也会隐藏父类中所有同名成员函数
	//如果想访问父类中被隐藏同名成员,需要加作用域
	Son::func();
	Son::Base::func();
	Son::Base::func(10);
}
int main() {
	test1();
	test2();
	return 0;
}

10.5 多继承

c++可一个类继承多个类
语法:class 子类 : 继承方式 父类1 , 继承方式 父类2…

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

10.6 菱形继承

概念:两个派生类继承同一个类,又有某个类同时继承两个派生类,这种继承称为菱形继承,或者钻石继承

class Animal {//动物类
public:
	int m_age;
};
//利用虚继承(关键字virtual),解决菱形继承导致数据有两份,资源浪费的问题
//在继承前加上virtual
//Animal类称为虚基类
class Sheep :virtual public Animal {};//羊类
class Tuo : virtual public Animal {};//驼类
class SheepTuo : public Sheep, public Tuo {};//羊驼类
void test() {
	SheepTuo st;
	st.Sheep:: m_age = 1;
	st.Tuo::m_age = 5;
	cout << "st.Sheep::m_age = " << st.Sheep::m_age << endl;
	cout << "st.Tuo::m_age = " << st.Tuo::m_age << endl;
	//加上虚继承之后:
	cout << "st.m_age = " << st.m_age << endl;
}
int main() {
	test();
}

10.7.1单继承1

单继承是一般的单一继承,一个子类只 有一个直接父类时称这个继承关系为单继承。(一对一的关系)

没有继承时,成员变量和成员函数会分开存储:

  1. 对象的内存中只包含成员变量,存储在栈区或堆区(使用new创建对象时)
  2. 成员函数与对象内存分离,存储在代码区

老九学堂 学习C++ 第十天_第3张图片
注:

  1. 编译器会将成员变量和成员函数分开存储:分别为每个对象的成员变量分配内存,但是所有对象都共享同一段代码
  2. 类可以看做是一种复杂的数据类型,如果使用sizeof求类所占空间的大小,会发现,只是计算了成员变量的大小,并没有把成员函数也包含在内

10.7.1单继承2

有继承关系时的内存模型:

  1. 有继承关系时,派生类的内存模型可以看成是基类成员变量和新增成员变量的总和;
  2. 所有的成员函数仍然存储在另外一个区域——代码区,由所有对象共享

老九学堂 学习C++ 第十天_第4张图片(上图为实现继承关系的战士对象所占的内存)

在初始化时,先初始化基类成员,在初始化派生类成员。释放时,先释放派生类成员,在释放基类成员

注:

  1. 实例化派生类对象时,首先会创建基类对象(调用基类构造)
  2. 派生类构造应通过成员初始化列表将基类信息传递给基类构造
  3. 应该在派生类构造中初始化派生类新增的数据成员

派生类及基类中存在同名函数时调用问题:

情况1:派生类中如果不实现Move方法,默认会调用基类实现
情况2:派生类实现了Move方法(相当于覆盖了基类实现),那么就会调用子类实现

小结:

  1. 派生类对象可以使用基类的非私有成员函数
    在这里插入图片描述
  2. 基类指针可以在不进行显式类型转换的情况下指向派生类对象(基类指针可以直接指向派生类对象)
    老九学堂 学习C++ 第十天_第5张图片
refHero.Move()	//基类引用调用函数
ptrHero->Move();	//基类指针调用函数

3.派生类对象可以赋值给基类对象,程序将使用隐式重载赋值运算符
在这里插入图片描述

注:

  • 基类指针或引用只能调用基类函数,并不能调用派生类函数
  • 如果使用基类引用指向派生类对象,那么基类引用就不能调用派生类中定义的方法了
Hero& refHero = warrior1;
Hero * ptrHero = &warrior;	//基类指针也可以指向派生类对象
Warrior& warrior2 = (Warrior&)refHero;//父类引用/指针需要强转成子类引用/指针
  • 不可以将基类对象和地址赋值给派生类引用和对象,即不能够做逆操作

11 多态

面向对象编程的多态性包括:

向不同的对象发送同一条消息(函数调用);不同的对象在接收时会产生不同的行为(不用的实现,即执行不同的函数。函数名相同,单执行的具体细节不同)

多态分为两类:

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

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

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

11.1 静态多态——函数重载

特点:

函数名相同;
函数参数个数不同;
如果参数个数相同,那么参数类型不能相同;
如果参数个数相同,并且类型也相同,那么必须是顺序不同
函数重载与函数返回值无关

11.2 动态多态——函数重写

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

要实现C++函数重写,必须要先把父类的成员函数设定为虚函数。

C++中的虚函数的作用主要是实现了多态的机制。基类定义虚函数,子类可以重写该函数;在派生类中对基类定义的虚函数进行重写时,需要在派生类中声明该方法为虚方法。

动态多态满足条件:

  1. 有继承关系
  2. 子类重写父类的虚函数

多态使用条件:父类指针或引用指向子类对象

virtual 返回值 函数名();

注:派生类重写基类方法Move时,可以加上override关键字表示重写;override关键字为C++11标准后新加入的,用来明确派生类重写基类继承来的虚函数

class Animal {//动物类
public:
	//虚函数,基类加上virtual之后,子类可加可不加
	virtual void speak() {
		cout << "动物在说话" << endl;
	}
};
class Cat : public Animal {
public:
	virtual void speak() {
		cout << "小猫在说话" << endl;
	}
};
class Dog : public Animal {
	virtual void speak() {
		cout << "小狗在说话" << endl;
	}
};
//执行说话的函数
//如果想执行让猫说话,那么这个函数地址就不能提前绑定,需要在运行阶段进行绑定—地址晚绑定
void doSpeak(Animal& animal) {
	animal.speak();
}
void test1() {
	Cat cat;
	doSpeak(cat);
	Dog dog;
	doSpeak(dog);
}
int main() {
	test1();
}

多态案例:利用多态实现两个操作数进行运算的计算器类

class AbstractCalculator {
public:
	virtual int getResult() {
		return 0;
	}
	int m_Num1;//操作数1
	int m_Num2;//操作数2
};
class AddCalculator : public AbstractCalculator {//加法计算器类
public:
	virtual int getResult() {
		return m_Num1 + m_Num2;
	}
};
class SubCalculator : public AbstractCalculator {//减法计算器类
public:
	virtual int getResult() {
		return m_Num1 - m_Num2;
	}
};
class MulCalculator : public AbstractCalculator {//乘法计算器类
public:
	virtual int getResult() {
		return m_Num1 * m_Num2;
	}
};
void test02() {
	AbstractCalculator* abc = new AddCalculator;
	abc->m_Num1 = 10;
	abc->m_Num2 = 10;
	cout << abc->m_Num1 << "+" << abc->m_Num2 << "=" << abc->getResult() << endl;
	delete abc;
	//减法运算
	abc = new SubCalculator;
	abc->m_Num1 = 10;
	abc->m_Num2 = 10;
	cout << abc->m_Num1 << "-" << abc->m_Num2 << "=" << abc->getResult() << endl;
	delete abc;
	//乘法
	abc = new MulCalculator;
	abc->m_Num1 = 10;
	abc->m_Num2 = 10;
	cout << abc->m_Num1 << "*" << abc->m_Num2 << "=" << abc->getResult() << endl;
	delete abc;
}
int main() {
	test02();
}

11.3 纯虚函数和抽象类

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

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

抽象类特点:

  • 无法实例化对象
  • 子类必须重写抽象类中的纯虚函数,否则也属于抽象类
using namespace std;
class Base {
public:
	virtual void func() = 0;
};
class Son : public Base {
public:
	void func() {
		cout << "func()的调用" << endl;
	}
};
int main() {
	Base * base = new Son;
	base->func();
	delete base;
}

11.4 虚析构和纯虚析构

多态使用时,如果子类有属性开辟在堆区,父类指针在释放时无法调用子类的析构函数;解决方式:将父类中的析构函数改为虚析构或纯虚析构

虚析构和纯虚析构共性:

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

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

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

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

using namespace std;
class Animal {
public:
	Animal() {
		cout << "Animal构造调用" << endl;
	}
	/*virtual ~Animal() {
		cout << "Animal析构调用" << endl;
	}*/
	virtual ~Animal() = 0;//纯虚析构,需要实现
	virtual void speak() = 0;
};
//Animal:: ~Animal() {//纯虚析构
//	cout << "Animal纯虚析构调用" << endl;
//}
class Cat : public Animal {
public:
	Cat(string name) {
		cout << "Cat构造调用" << endl;
		m_Name = new string(name);
	}
	void speak() {
		cout << *m_Name << "小猫在说话" << endl;
	}
	~Cat() {
		if (m_Name != NULL) {
			cout << "Cat析构调用" << endl;
			delete m_Name;
		}
	}
	string* m_Name;
};
void testCat() {
	//父类指针在析构时,不会调用子类中析构,导致子类如果有堆区属性,出现内存泄漏
	//解决方案:在父类析构前加virtual
	Animal* animal = new Cat("Tom");
	animal->speak();
	delete animal;
}
int main() {
	testCat();
}

总结:

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

11.5 向上转型和向下转型

当B是A的子类型时,就意味着所有对A的操作都可以对B对象,即B重用A的操作来实现自己的操作

向上转型:把子类型对象转换为父类型对象,有三个注意点:

  1. 向上转型是安全的
  2. 向上转型可以自动类型转换
  3. 向上转型的过程中会丢失子类型信息

·

例如:Warrior w;
w.XiaoQuanQuan();
Warrior warrior;    //子类型对象
Hero& hero = warrior;//父类型引用指向了子类型对象 - 向上转型
hero.XiaoQuanQuan();    //丢失了子类型信息,编译器会报错
如果还想调用子类型方法,那么就需要在进行强制转换 - 向下转型
Warrior& newWarrior = (Warrior&)hero;//向下转型不安全,因为hero对象有可能是Hero父类型的另一个子类型
假设程序是这样的:
Archmage warrior;
Hero& hero = warrior;
Warrior& newWarrior = (Warrior&)hero;

注:

  1. 构造函数不能是虚函数
  2. 析构函数应该是虚函数(除非不用做基类,通常应该为基类提供一个虚析构函数,即使它不需要析构函数)
  3. 友元不能是虚函数,因为友元不是类成员
  4. 如果基类析构不加virtual关键字,那么派生类对象在释放时,只会调用基类构造;加上virtual关键字后,会先调用派生类构造,在调用基类构造;意味着即使基类不需要显示析构提供服务,也应该提供虚析构函数,即使它不执行任何任务

12 C++文件操作

程序运行时产生的数据都属于临时数据,程序一旦运行结束都会被释放;通过文件可以将数据持久化

c++中对文件操作需要包含头文件:<fstream>

文件类型分为两种:

1.文本文件——文件以ASCLL码形式存储在计算机中
2.二进制文件——文件以文本的二进制形式存储在计算机中

操作文件的三大类:

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

12.1 文本文件操作 - 写文件

写文件的步骤:

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

文件打开方式:
老九学堂 学习C++ 第十天_第6张图片
注:文件打开方式可以配合使用,用 | 操作符

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

#include 
#include 
using namespace std;
int main() {
	//创建流对象
	ofstream ofs;
	//指定打开方式
	ofs.open("test.txt", ios::out);
	ofs << "姓名:张三" << endl;
	ofs << "姓名:李四" << endl;
	ofs << "性别:男" << endl;
	ofs.close();
}

12.2 文本文件操作 - 读文件

读文件的步骤:

1.包含头文件:#include <fstream>
2.创建流对象:ifstream ifs;
3.打开文件并判断文件是否打开成功:ifs.open("文件路径", 打开方式);
4.读数据:有四种
5.关闭文件:ifs.close();
#include 
#include 
#include 
using namespace std;
void test() {
	//1.包含头文件
	//2.创建流对象
	ifstream ifs;
	//3.指定打开方式并判断是否打开成功
	ifs.open("test.txt", ios::in);
	if ( !ifs.is_open()) {
		cout << "文件打开失败!" << endl;
		return;
	}
	//4.读数据
	//第一种:
	/*char buf[2014] = { 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();
}
int main() {
	test();
}

12.3 二进制文件 - 写文件

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

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

函数原型
ostream& writer(const char * buffer, int len);
字符指针buffer指向内存中一段空间,len是读写的字节数
#include 
#include 
using namespace std;
class Person {
public:
	char m_Name[64];
	int m_age;
};
void test() {
	//创建流对象
	ofstream ofs("person.text", ios::out | ios::binary);
	//打开文件,可以和创建流对象和成一步操作
	//ofs.open("person.text", ios::out | ios::binary);
	//写文件
	Person p = { "张三", 18 };
	ofs.write((const char*)&p, sizeof(Person));
	ofs.close();
}
int main() {
	test();
}

12.4 二进制文件 - 读文件

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

函数原型
istream& read(插入 * buffer, int len):
字符指针buffer指向内存中一段存储空间,len是读写的字节数
#include 
#include 
#include 
using namespace std;
class Person {
public:
	char m_Name[640];
	int m_age;
};
void test() {
	//创建流对象
	ifstream ifs;
	//打开文件,判断文件是否打开成功
	ifs.open("person.text", ios::in | ios::binary);
	if (!ifs.is_open()) {
		cout << "文件打开失败!" << endl;
	}
	//读文件
	Person p;
	ifs.read((char*)&p, sizeof(Person));
	cout << "姓名:" << p.m_Name << "年龄" << p.m_age << endl;
	//关闭文件
	ifs.close();
}
int main() {
	test();
}

13 函数模板

模板的概念:模板就是建立通用的模具,大大提高复用性

模板的特点:

1.模板不可以直接使用,他只是一个框架
2.模板的通用并不是万能的

13.1 函数模板

c++中的泛型编程思想,主要利用的技术就是模板,c++提供的两种模板机制就是函数模板类模板

函数模板的作用:建立一个通用函数,其函数返回值类型和形参类型可以不具体制定,用一个虚拟的类型来代表

函数模板语法

template<typename T>
函数声明或定义
//typename - 表明其后的符号是一种数据类型,可以使用class代替
//T - 通用数据类型,名称可以替换

使用模板的两种方法:

1.自动类型推导
2.显示指定类型

//声明模板,T是通用的数据类型
template<typename T>
void mySwap(T& a, T& b)
{
	T temp = a;
	a = b;
	b = temp;
}
void test() {
	int a = 2, b = 3;
	//利用函数模板交换
	//两种方式使用函数模板
	//1.自动类型推导
	mySwap(a, b);
	//2.显示指定类型
	//mySwap(a, b);
	cout << a << '\t' << b << endl;
	double c = 2.1, d = 3.1;
	mySwap(c, d);
	cout << c << '\t' << d << endl;
}
int main() {
	test();
}

注:

  • 自动类型推导,必须推导出一致的数据类型才可以使用
  • 模板必须要确定出T的数据类型才可以使用

13.2 函数模板案例

案例描述:

  • 利用函数模板封装一个排序的函数,可以对不同数据类型数组进行排序
  • 排序规则从大到小,排序算法为选择排序
  • 分别用char数组和int数组进行测试
template<class T>//函数交换模板
void Swap(T& a, T& b) {
	T temp = a;
	a = b;
	b = temp;
}
template<typename T>
void mySort(T array[], int len) {
	for (int i = 0; i < len; i++) {
		int max = i;//认定最大值的下标
		for (int j = i + 1; j < len; j++) {
			//认定的最大值比遍历出的数值要小,说明j下标的元素才是真正的最大值
			if (array[max] < array[j]) {
				max = j;
			}
		}
		if (max != i) {//交换max和i下标的元素
			Swap(array[max], array[i]);
		}
	}
}
template<class T>//打印结果模板
void Print(T array[], int len) {
	for (int i = 0; i < len; i++) {
		cout << array[i] << ' ';
	}
	cout << endl;
}
void test1() {
	char charArray[] = "asdfghj";
	int lenght = sizeof(charArray) / sizeof(char);
	mySort(charArray, lenght);
	Print(charArray, lenght);
}
void test2() {
	int intArray[] = { 1,3,4,55,77,11,33,10,35 };
	int lenght = sizeof(intArray) / sizeof(int);
	mySort(intArray, lenght);
	Print(intArray, lenght);
}
int main() {
	test1();
	cout << "------------" << endl;
	test2();
}

13.3 普通函数与函数模板的区别

隐式类型转换参考:https://blog.csdn.net/liunan199481/article/details/85251197

区别:

1.普通函数调用时可以发生自动类型转换(隐式类型转换)
2.函数模板调用时,如果利用自动类型推导,不会发生隐式类型转换
3.如果利用显示指定类型的方式,可以发生隐式类型转换

//普通函数调用
int myAdd(int a, int b) {
	return a + b;
}
//函数模板
template<typename T>
T myAdd1(T a, T b) {
	return a + b;
}
void test() {
	int a = 10;
	int b = 2;
	char c = 'c';
	cout << myAdd(a, c) << endl;
	//函数模板用自动类型推导不可发生自动类型转换
	//cout << myAdd1(a, c) << endl;//错误
	//显示指定类型
	cout << myAdd1<int>(a, c) << endl;
}
int main() {
	test();
}

13.4 普通函数与函数模板的调用规则

调用规则:

1.如果函数模板和普通函数都可以实现,优先调用普通函数
2.可以通过空模板参数列表来强制调用函数模板
3.函数模板也可以发生重载
4.如果函数模板可以产生更好的匹配,优先调用函数模板

void myPrint(int a, int b) {
	cout << "调用普通函数" << endl;
} 
template<typename T>
void myPrint(T a, T b) {
	cout << "函数模板调用" << endl;
}
template<typename T>
void myPrint(T a, T b, T c) {
	cout << "重载的函数模板调用" << endl;
}
void test() {
	int a = 10; 
	int b = 1;
	int c = 3;
	myPrint(a, b);
	myPrint<>(a, b);//通过空模板参数列表,强制调用函数模板
	myPrint(a, b, c);//重载
	//函数模板产生更好的匹配,优先调用函数模板
	char c1 = 'a';
	char c2 = 'b';
	myPrint(c1, c2);
}
int main() {
	test();
}

老九学堂 学习C++ 第十天_第7张图片

13.5 模板的局限性

注:模板并不是万能的

template<typename T>
void fun(T a, T b){
a = b;
}

如果传入的是两个数组就无法实现

利用具体化的模板,可以解决自定义类型的通用化

template<typename T>//比较模板
bool Comper(T& a, T& b) {
	if (a == b) return true;
	else false;
}
class Person {
public:
	Person(string name, int age) {
		this->m_name = name;
		this->m_age = age;
	}
	string m_name;
	int m_age;
};
//利用具体化Person的版本实现代码,具体化优先调用
template<> bool Comper(Person& a, Person& b) {
	if (a.m_name == b.m_name && a.m_age == b.m_age)return true;
	else return false;
}
int main() {
	Person person("张三", 10);
	Person person1("张三", 10);
	bool ret = Comper(person, person1);
	if (ret)cout << "p1 == p2" << endl;
	else cout << "p1 != p2 " << endl;
}

13.6 类模板

作用:建立一个通用类,类中的成员,数据类型可以不具体制定,用一个虚拟的类型来代表

语法:

template<typename T>
//类模板
template<class NameType, class AgeType>
class Person {
public:
	Person(NameType name, AgeType age) {
		this->m_name = name;
		this->m_age = age;
	}
	NameType m_name;
	AgeType m_age;
};
int main() {
	Person<string, int> p1("张三", 10);
	cout << p1.m_name << '\t' << p1.m_age << endl;
}

注:类模板与函数模板的区别:

1.类模板没有自动类型推导的使用方式
2.类模板在模板参数列表中可以有默认参数

13.7 类模板中成员函数创建时机

类模板中成员函数和普通类中成员函数创建时机的区别

1.普通类中的成员函数一开始就可以创建
2.类模板中的成员函数在使用时才创建

13.8 类模板对象做函数参数

类模板实例化处的对象向函数传参的方式有三种:

1.指定传入的类型 — — 直接显示对象的数据类型
2.参数模板化 — — 将对象中的参数变为模板进行传递
3.整个类模板化 — — 将这个对象类型模板进行传递

template<class T1, class T2>
class Person {
public:
	Person(T1 name, T2 age) {
		this->m_Name = name;
		this->m_Age = age;
	}
	void ShowPerson() {
		cout << "姓名:" << this->m_Name << endl;
		cout << "年龄:" << this->m_Age << endl;
	}
	T1 m_Name;
	T2 m_Age;
};
//1.指定传入类型
void PrintPerson1(Person<string, int>&p) {
	p.ShowPerson();
}
void test1() {
	Person<string, int>p("张三", 10);
	PrintPerson1(p);
}
//2.参数模板化
template<class T1, class T2>
void PrintPerson2(Person<T1, T2>& p1) {
	p1.ShowPerson();
}
void test2() {
	Person<string, int>p("李四", 10);
	PrintPerson2(p);
}
//3.整个类模板化
template<class T>
void PrintPerson3(T &p) {
	p.ShowPerson();
}
void test3() {
	Person<string, int>p("王五", 20);
	PrintPerson3(p);
}
int main() {
	test1();
	test2();
	test3();
}

13.9 类模板与继承

当类模板碰到继承时,注:

1.当子类继承的父类是一个类模板时,子类在声明时,要指定出父类中T的类型
2.如果不指定,编译器无法给子类分配内存
3.如果想灵活指出父类中T的类型,子类也需变为类模板

template<class T>
class Base {
public:
	T m_a = 'a';
};
//calss Son:public Base//必须要知道父类中的T类型,才能继承给子类
class Son:public Base<int> {}; 
template<class T1, class T2>
class Son1 :public Base<T2> {
public:
	//T2为继承的父类成员,T1为子类成员
	T1 m_b = 'w';
};
int main() {
	Son1<int, char>s2;
	cout << s2.m_a << endl;
	cout << s2.m_b << endl;
}

在这里插入图片描述

13.10 类模板成员函数类外实现

template<class T1, class T2>
class Base {
public:
	Base(T1 a, T2 b);
	void show();
	T1 m_a;
	T2 m_b;
};
template<class T1, class T2>
Base<T1, T2>::Base(T1 a, T2 b) {//构造函数类外实现
	this->m_a = a;
	this->m_b = b;
}
template<class T1, class T2>
void Base<T1, T2>::show() {//成员函数类外实现
	cout << this->m_a << ' ' << this->m_b << endl;
}

int main() {
	Base<int, int> base(1, 2);
	base.show();
}

13.11 类模板分文件编写

注:类模板中成员函数创建时机是在调用阶段,导致分文件编写时链接不到

解决方式1.直接包含.cpp文件;
解决方式2.将声明和实现写在同一个文件中,并更改后缀名为.hpp,hpp是约定的名称并不是强制的

//Person.h文件, 第二种方法将后缀名改为.hpp
#pragma once
#include 
#include 
using namespace std;
template<class T1, class T2>
class Person {
public:
	Person(T1 name, T2 age);
	T1 m_name;
	T2 m_age;
};
template<class T1, class T2>
Person<T1, T2>::Person(T1 name, T2 age) {
	this->m_name = name;
	this->m_age = age;
}
//Person.cpp文件
//#include "Person.h"
//template
//Person::Person(T1 name, T2 age) {
//	this->m_name = name;
//	this->m_age = age;
//}
//main.cpp文件
#include 
//#include "Person.cpp"//第一种解决方式,直接包含源文件
#include "Person.hpp"
//第二种方式,把.h和.cpp中的内容写到一起,将后缀名改为.hpp文件
int main() {
	Person<string, int>p("张三", 10);
	cout << p.m_name << " " << p.m_age << endl;
}

13.12 类模板与友元

全局函数类内实现,直接在类内声明友元即可
全局函数类外实现,需要让提前让编译器知道全局函数的存在

template<class T1, class T2>//提前让编译器知道Person类
class Person;

//全局函数,类外实现
template<class T1, class T2>
void PrintPerson2(Person<T1, T2>p) {
	cout << "类外实现的:" << p.m_name << " " << p.m_age << endl;
}
template<class T1, class T2>
class Person {
	//全局函数,类内实现
	friend void PrintPerson(Person<T1, T2>p) {
		cout << "类内实现:" << p.m_name << " " << p.m_age << endl;
	}
	//全局函数,类外实现
	//加上空模板的参数列表,如果全局函数是类外实现,需要让编译器提前知道这个函数的存在
	friend void PrintPerson2<>(Person<T1, T2>p);
public:
	Person(T1 name, T2 age) {
			this->m_name = name;
			this->m_age = age;
	}
	T1 m_name;
	T2 m_age;
};
int main() {
	Person<string, int>p("张三", 3);
	PrintPerson(p);
	PrintPerson2(p);
}

你可能感兴趣的:(c++)