note

C++核心编程

一、内存分区模型

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

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

1.1 程序运行前

在程序编译后,生成了exe可执行程序,==未执行该程序前==分为两个区域
#include <iostream>
#include <string>

using namespace std;

int a = 10;
int b = 20;    // 全局变量

const int c_g_a = 10;
const int c_g_b = 20;

int main() {
	cout << (int)&a << endl;
	cout << (int)&b << endl;   // 在全局区

	static int s_a = 10;
	static int s_b = 20;  // 用static关键词修饰的就是静态变量
	cout << (int)&s_a << endl;
	cout << (int)&s_b << endl;  // 在全局区,跟上面地址相似

	cout << (int)&c_g_a << endl;
	cout << (int)&c_g_b << endl;   // 在全局区,const修饰的全局变量,跟上面地址相似

	cout << (int)&"hello" << endl;  // 这是字符串常量,这在全局区,跟上面地址相似
	
	// 下面的就不在全局区了
	string ss = "world";
	cout << (int)&ss << endl;   // 注意这应该是字符串变量,就不在全局区,地址也不相似

	const int c_l_a = 10;
	const int c_l_b = 20;
	cout << (int)&c_l_a << endl;
	cout << (int)&c_l_b << endl;   // 这也不在全局区,这是const修饰的局部变量,局部的应该都不在全局区

	system("pause");
	return 0;
}

1.2 程序运行前

1.3 new操作符

​ C++中利用==new==操作符在堆区间开辟数据,手动释放利用操作符==delete==,

Ps:针对数组是new int[个数]; 针对单个数字是new int(5) (一个中括号,一个小括号)


针对图像内存地址申请,看到海康相机的SDK,可以参考一下:

{
    unsigned char *pData = nullptr;
	unsigned char *pDataForBGR = nullptr;
    unsigned int nDstBufferSize = nWidth * nHeight * 4 + 2048;  // 随便给的值,大小要根据具体情况而定
	// 原来的申请方式。一:(数据包大小的数据类型一般为 unsigned int)
    pData = (unsigned char *)malloc(sizeof(unsigned char) * 数据包大小);
    pDataForBGR = (unsigned char *)malloc(nDstBufferSize);
    free(pData);
    free(pDataForBGR);
    // 用c++的方式。二:
    pData = new unsigned char[数据包大小];
    pDataForBGR = new unsigned char[nDstBufferSize];
    delete[] pData;
    delete[] pDataForBGR;
}

说明:

1.4 内存泄漏

申请的堆空间,记得手动去释放,不然容易造成内存泄漏

#include<Window.h>   // 下面Sleep(1)需要用到的包
// 泄露(要看泄露效果的话就把delete屏蔽掉)
void leak() {
	for (int i = 1; i < 1000000; i++) {
		int*a = new int[100000];  // 这中括号是是数组;申请100000个整形空间
		delete[] a;  // 这是中括号,释放内存就要有[]
		
		//int a[100000]  // c++是先申请空间(可以先不给值);这一行是看栈空间不手动释放也不会泄露
		Sleep(1);  // 休眠一秒
	}
}

二、引用

2.1 引用的基本使用

作用:给变量起别名

语法:数据类型 &别名 = 原名; // 注意这个数据类型要和原名的类型一样

	int a = 10;
	int &b = a;   // 引用必须初始化
	cout << "a:" << a << endl;   // 10
	cout << "b:" << b << endl;   // 10

	int c = 20;
	// 起别名后,就可以把b看做a了,所有对b的操作,也会改变a的
	b = c;   // 这就是赋值
	cout << "a:" << a << endl;   // 20
	cout << "b:" << b << endl;   // 20

2.2 引用做函数参数

​ 这也是可以直接修改传进来的参数的

// 这就是定义的引用传递;;相当于给传进来的参数起别名
void swap(int &a, int &b) {
	int temp = a;    // 有了别名后,直接对别名的操作就是对原数据的操作
	a = b;
	b = temp;
}
int main() {
	int x = 10;
	int y = 20;   
	swap(x, y);  // 引用传递的时候直接把x、y传进去就行了
	cout << "a:" << x << endl;   // 20
	cout << "b:" << y << endl;   // 10

	system("pause");
	return 0;
}

2.3引用做函数的返回值

// 必须要加这个引用,不然返回10,下面就是 int &ref = 10; 这是错的,引用不能是对一个数字常量,就会报错
int& test01() {
	int a = 10;  // 栈区
	return a;  // 本来返回的a这个值,但是函数返回那里加了引用,返回的就是a的引用
}  
int& test02() {
	static int a = 20;  // 加了static关键字后,成了静态变量,存放在全局区,全局区的数据在程序结束后系统释放
	return a;
}
int main() {
	int &ref = test01();   // 函数返回的是引用,所以要用一个引用去接收
	// 千万不要引用一个局部变量,它在栈区,用了一次后就会被系统释放
	cout << "ref:" << ref << endl;  // 10  ,系统做了一次保留
	// cout << "ref:" << ref << endl;  // 随机乱码了,错的
	
	int &a_ref = test02();
	cout << "a_ref:" << a_ref << endl;  // 20
	cout << "a_ref:" << a_ref << endl;  // 20   这就没问题


	// 函数返回的引用可以做左值
	test02() = 1000;  
	// 这里就是返回的a,然后重赋值了1000;;而上面int &a_ref = test02(); 又给这个起了别名叫a_ref
	// 前面重新赋值了,那对应的别名a_ref也等于1000,因为这俩指向的是同一个地址
	cout << "a_ref:" << a_ref << endl;  // 1000

	system("pause");
	return 0;
}

Ps:函数不要返回局部变量的引用;函数返回的引用可以做左值,进行赋值操作

2.4引用的本质

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

//发现是引用,转换为 int* const ref = &a;
void func(int& ref){
	ref = 100; // ref是引用,转换为*ref = 100
}
int main(){
	int a = 10;
    
    //自动转换为 int* const ref = &a; 指针常量是指针指向不可改,也说明为什么引用不可更改
	int& ref = a; 
	ref = 20; //内部发现ref是引用,自动帮我们转换为: *ref = 20;
    
	cout << "a:" << a << endl;
	cout << "ref:" << ref << endl;
    
	func(a);
	return 0;
}

2.5 常量引用

​ 作用:常量引用主要用来修饰形参(函数传递),防止误操作

void showValue(const int &value) {
	//value = 30;  // 加了const就不准再赋值了
	cout << value << endl;
}
int main() {
	//int &ref = 10;  // 引用本身需要引用一个合法的内存空间,这是错误的。
	const int &ref = 10;  // 这是可以的,但一般更多还是用在定义函数时
	// 这里面也是编译器优化代码,过程是 int temp=10; const int &ref=temp;  temp就是系统任意起的一个名字

	//ref = 20;    // 加入const后不可修改变量
	cout << ref << endl;

	// 函数中利用常量引用防止误操作修改实参(就相当于是只读)
	int a = 20;
	showValue(a);

	system("pause");
	return 0;
}

三、函数提高

3.1 默认参数

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

// 函数声明和函数实现,如果需要默认值,只要其中任意一个写就好了
int add(int a=10, int b=20);

int add(int a, int b) {
	return a + b;
}
int main() {
	int out = add();
	cout << out << endl;
	system("pause");
	return 0;
}

注意:当在头文件写了默认参数 int add(int a, int b=20); 那么在.cpp文件实现时,默认参数就不写了,只能写成int add(int a, int b) {//} 绝对不能写成int add(int a, int b=20){},这样会引发错误“错误 C2572 , 重定义默认参数 : 参数 1”

3.2 函数占位参数

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

//函数占位参数 ,占位参数也可以有默认参数
void func(int a, int=20) {
	cout << "this is func" << endl;
}
int main() {
	func(10); //占位参数必须填补(在没有默认参数时)
	system("pause");

	return 0;
}

PS:在运算符重载,区分前置++与后置++,用作后置++的占位参数

3.3 函数重载

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

函数重载需满足以下条件:

Ps:若函数名、传参都一模一样,仅仅是定义函数时返回类型不同是不行的(在调用时也无法区分到底是选择的哪个。)

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

上面很明显可以在调用函数时通过传参的不同来区别。

注意点

四、类和对象

一种类定义的方式:

typedef struct {
	int age;
	const char* name;
}a_type;          // 可以像这样定义一个数据类型
// 同样这里的struct还可以换成一个enum枚举,形式是一样
// 这里的struct后面没跟一个类型名,也可以跟一个,意义不大,enum也是一样的

int main(int argc, char** argv) {
	a_type person;
	person.age = 42;
	person.name = "zhangsan";
	return 0;
}

4.0 =default

struct My_print {
	My_print() = default;   // 使用合成的默认构造函数,直接用 My_print(); 好像区别不大
    My_print(const My_print&) = default;  // 拷贝构造函数
    My_print& operator=(const My_print&);  // 拷贝赋值运算符
	~My_print() = default;
};

​ 在C++11新标准中,如果我们需要默认行为,那么可以通过在参数列表后面写上= default来要求编译器生成构造函数(我的理解是自己写了拷贝构造函数,就不会生成默认构造函数,加上就是代表即使有拷贝构造函数,也要编译器生成合成的默认构造函数),其中 = default 既可以和声明一起出现在类内部,也可以作为定义出现在类的外部。和其它函数一样,如果 = default 在类的内部,则默认构造函数是内联的,如果它在类外部,则该成员默认下不是内联。

​ class 和 struct定义类的唯一区别就是默认的访问权限,struct默认是public,而class默认是private。

4.1 封装、权限

封装的意义:

// 这3个权限里的东西,类内都可以访问
class Student {
// 公共权限
public:    // public类外也可访问
	string name;      
	
// 保护权限
protected:   // 类外不能访问,继承这个类的子类可以访问
	string gender = "male";  

// 私有权限
private:   // 类外不可以访问,继承这个类的子类也不可以访问
	int id=15;
	void showGender() {
		cout << "这个人的性别是:" << gender << endl;
	}

public:
	void showStudent() {  // 类内的函数都可以访问
		cout << "姓名:" << name << endl;
		cout << "性别:" << gender << endl;
		cout << "学号:" << id << endl;
	}
    // 可以传指针、引用、值传递
    void demo(Student *s1) {    // 可以放其他类作为参数,甚至自己这个类
        
    }
};

int main() {
    // 实例化
	Student s1;
	s1.name = "张三";
	//s1.id = 123;  // 这就是错的,访问不了
	//s1.showGender();  // 函数一样没有权限
	s1.showStudent();

	system("pause");
	return 0;

Ps:类中的函数,定义的参数,可以是一个类的实例化那种,看上面的24行代码

4.2 struct和class区别

​ 在C++中,struct和class==唯一区别==就在于在没有权限声明时 ==默认的访问权限不同==

4.2.1 枚举:enum

​ 枚举数据类型跟struct和class有点相似,一般两种定义方式,跟struct类似。

还可以修改枚举数据类型的默认值:

enum { east=10, west=20, north=30, south=40 } dir;
	dir = south;
	switch (dir) {
	case north:
		cout << "这是北半球" << endl;
		break;
	case south:
		cout << "这是南半球:" << south << endl;
		break;
	}

甚至可以只是 enum {east=10, west=20}; 然后直接使用里面的值

​ 使用枚举值的一个demo:

// 1、枚举值放全局变量
enum Department { scheme = 10, arts = 20, develop = 30 };

class Person {
public:
	// 2、枚举值放函数类
	//enum Department { scheme = 10, arts = 20, develop = 30 };  

	string m_Name;  
	int m_Salary;   // 薪酬
	Department m_Depart;  // 部门
	Person(string name, int salary, Department depart) {
		this->m_Name = name;
		this->m_Salary = salary;
		this->m_Depart = depart;
	}
};
void test01() {
	// 1、枚举值放全局变量
	Person p1("张三", 200, Department::arts);
	Person p2("李四", 400, arts);

	// 2、枚举值放函数类
	//cout << Person::arts << endl;  // 20
	//cout << Person::Department::develop << endl;  // 30
	// 这俩都是可以的,,类名+枚举值也行
	//Person p1("张三", 200, Person::arts);
	//Person p2("李四", 400, Person::Department::develop);  
}

​ Ps:1是一种情况,一起放开或注释;2是另一种情况,也是一起放开或注释;

​ 枚举值可以直接使用,或前面加上其所在作用域,枚举值的”类名”可加可不加。


以下是书上的内容补充:effective中建议使用enum class而非enmu

​ C++11新标准引入了==限定作用域范围的枚举类型==,定义的一般形式是:首先是关键字 enum class(或者等价地使用enum struct),随后枚举类型名字以及枚举成员: ​ enum class open_model {input, output, append}; ​ 这就是定义了一个名为open_models的枚举类型,有三个枚举成员。

还有一种==不限定作用域的枚举类型==,定义时省略掉关键字class(或struct),枚举类型的名字是可选的。

限定作用域范围的枚举类型与不限定作用域的枚举类型比较直接的==区别==:

C++11新标准中,可以在enum的名字后加上冒号以及想在enum中使用的类型:(注意后缀 UL、ULL 这些后缀) enum intValues : unsigned long long {a=100, b=42946UL, c=15484561651578ULL};

​ 在不指定enum的潜在类型,默认下:限定作用域的enum成员类型是int;对于不限定作用域的枚举类型来说,其不存在默认类型。

还有一个注意点,若是要提前声明enum,像上面intValues这种不限定作用域的就一定要指明类型: enum intValues : unsigned long long; // 提前声明,一定要指明类型(因为没有默认类型) enum class open_model; // 提前声明,可以不指定类型(因为限定作用域的有默认类型int)

4.3 对象的初始化和清理

4.3.1 构造函数、析构函数

​ c++利用了==构造函数和析构函数==解决对象的==初始化和清理==,这两个函数将会被编译器自动调用,完成对象初始化和清理工作。

​ 对象的初始化和清理工作是编译器强制要我们做的事情,因此如果我们不提供构造和解析,编译器会提供,但提供的都是空实现(即有这两个函数,但是里面没有实体代码)。

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

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

==析构函数语法==:~类名() {} // 就是多了一个 ~

  1. 析构函数,没有返回值也不写void;
  2. 函数名称与类名相同,在名称前加上符号 ~
  3. 析构函数==不可以有参数==,因此==不可以发生重载==;
  4. 程序在对象销毁前会自动调用析构,且只会调用一次。
class Person {
public:
	Person() {
		cout << "这是会自动调用的构造函数!" << endl;
	}
	~Person() {
		cout << "这是析构函数" << endl;
	}
};
void test01() {
	Person p1;   // 这创建的空间就是在栈上
}
int main() {
	test01();   // 这个函数执行完,就会调用 析构函数

	Person p2; // 这种先调用 构造函数, 再执行下一句就卡住了,任意键后,就会看到这个调用的 析构函数,就是在return之前进行释放
    cout << "看这个在哪里" << endl;  // 在这对象调用析构函数之前
	system("pause");
	return 0;
}

4.3.2 构造函数的分类及调用

两种分类方式:

​ 按参数分为:有参构造和无参构造

​ 按类型分为:普通构造和拷贝构造

三种调用方式:

​ 括号法、显式法、隐式转换法

实例:

// 分类
class Person {
public:
	// 构造函数是可以重载的
	// 这就是无参构造函数,又称为默认构造函数
	Person() {
		cout << "这是无参构造函数" << endl;
	}
	Person(int a) {
		age = a;      // 构造函数就是来初始化
		cout << "这就是有参构造函数" << endl;
	}
    
	// 拷贝构造函数(一般是就是把属性进行拷贝);可以把Person看做定义时的数据类型
	// const是为了防止数据被修改,然后对象传进来一定要是`引用`以及const;
	Person(const Person &p1) {
		cout << "这就是拷贝构造函数" << endl;
		age = p1.age;      // 把对象p1的age赋值过来
	}
    // 与拷贝构造函数相似用的还有`拷贝赋值运算符`
    Person& operator=(const Person &p1) {
		cout << "这就是拷贝赋值运算符" << endl;
		age = p1.age;      // 把对象p1的age赋值过来
	}

	// 析构函数是不能重载的
	~Person() {
		cout << "这是析构函数" << endl;
	}
public:
	int age;
};

// 调用
void test01() {
	// 1、括号法
	Person p1;  // 默认构造函数调用
	// 这里一定不要给括号,即不要写成了Person p1();  因为编译器认为是函数的声明,不是在创建对象,就不会执行对象的初始化(数据类型Person,函数名p1())

	Person p2(10);  // 有参构造函数(给值初始化)
	Person p3(p2);  // 拷贝构造函数(把上面的一个对象p2直接传进来就好了)
	cout << p2.age << endl;  // 10
	cout << p3.age << endl;  // 10
}

void test02() {
	// 2、显式法
	Person p1;
	Person p2 = Person(10);  // 其实差不多,知道这么个存在就好
	Person p3 = Person(p2);

	Person(20);  // 这是`匿名对象`,即初始化对象时没有命名,
    // 匿名对象 就`函数名()`,构造函数无参数时,有参数就是上面那行
    // 这在后面函数调用运算符`()`重载时用得到
	// 特点:当前行执行结束后,系统会立即回收掉匿名对象(没名后续本也就无法调用)
	cout << "***再看看这行的顺序***" << endl;  // 在这个对象的调用析构函数之后

	// 错误:不用利用拷贝函数来 初始化匿名对象,
	// Person(p3);  // 这种是等于 Person (p3) == Person p3;前面又定义了p3,就会报错重定义(知道一下就行)
}

void test03() {
	// 3、隐式转换法
	Person p1 = 10;  // 编译器自动转成了 Person p1 = Person(10);  自动转成显式法了
	Person p2 = p1;  // 这是拷贝构造,上面一行就是有参构造
}

4.3.3 拷贝构造函数调用时机

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

Person类使用4.3.2里面的。

// 1.使用一个已经创建完毕的对象来初始化一个新对象
void test01() {
	Person p1(30);
	Person p2(p1);  // 调用拷贝构造(显示法)
	Person p3 = p1;  // 调用拷贝构造(隐式转换法)
}

// 2.值传递的方式给函数参数传值
void doWork2(Person p) {
}
void test02() {
	Person p1;  // 无参构造函数
	doWork2(p1);  // 这就是前面讲的值传递,是会复制一个副本出来,所以会调用拷贝构造函数
}

// 3.以值方式返回局部变量
Person doWork3() {    // 定义的函数,注意返回值类型
	Person p1;  // 也是拷贝构造函数
	cout << (int)&p1 << endl;  
	return p1;
}
void test03() {
	Person p2 = doWork3();
	cout << (int)&p2 << endl;  // 因为拷贝,所以两个的地址是不一样的
}

4.3.4 构造函数调用规则

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

构造函数调用规则如下:

class Person {
public:
	// 自定义了有参构造,那就不会再有默认构造了,
	// 虽然没自定义拷贝构造,但是编译器会自动提供,并会把属性都进行复制
	Person(int a) {
		age = a;      // 构造函数就是来初始化
		cout << "这就是有参构造函数" << endl;
	}

	// 析构函数是不能重载的
	~Person() {
		cout << "这是析构函数" << endl;
	}
public:
	int age;
};

void test01() {
	// 这就是错的,系统不再提供默认构造函数
	//Person p;
	Person p1(18);
	// 虽然没再自定义拷贝构造函数,编译器会自动提供,并把属性复制了
	Person p2(p1);
	cout << "p1的年纪:" << p1.age << endl;  // 18
	cout << "p2的年纪:" << p1.age << endl;  // 18
}

4.3.5 深拷贝和浅拷贝

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

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

class Person {
public:
	Person(int a, int b) {
		age = a;      
		height = new int(b);
		cout << "这就是有参构造函数" << endl;
	}
	// 可以先试试不要自定义拷贝构造函数,这代码回再最后一行报错的
	Person(const Person &p) {
		//height = p.height;  // 这会是系统提供的默认拷贝构造函数的写法,简单的浅拷贝
		height = new int(*p.height);  // 自己把数据解引用出来,再开辟一个新的堆空间:深拷贝
	}
	
	~Person() {
		// 析构函数就是在创建的对象回收前调用;那就刚好用来回收手动创建的new开辟的堆空间
		if (height != NULL) {
			delete height;
			height = NULL;  // 防止野指针出现再做一个置空的操作
		}
		cout << "这是析构函数" << endl;
	}
public:
	int age;
	int *height;
};

Ps:如果有属性在堆区间开辟的,一定要自己提供拷贝构造函数,防止浅拷贝带来的问题。

4.3.6 ==构造函数初始化列表==

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

语法:构造函数(): 属性1(值1), 属性2(值2) {} 或者 构造函数(int a, int b): 属性1(a), 属性2(b) {}

class Person {
public:
	// 初始化列表的方式初始化
	//Person() :m_A(10), m_B(20), m_C(30) {  // 这样初始化时就写死了

	//}
	Person(int a, int b, int c) :m_A(a), m_B(b), m_C(c) {
	
	}  // 这样里面也不用写赋值操作了
	void show() {
		std::cout << "m_A = " << m_A << std::endl;
		std::cout << "m_B = " << m_B << std::endl;
		std::cout << "m_C = " << m_C << std::endl;
	}
public:
	int m_A, m_B, m_C;
};
int main() {
	Person p(10, 20, 30);
	p.show();
	return 0;
}

子类的构造函数在初始化列表时可以同时初始化父类的构造函数,如:

class GameObject {
public:
	bool        IsSolid;
	bool        Destroyed;
	// constructor(s)
	GameObject();
	GameObject(glm::vec2 pos, glm::vec2 size);
};
// 子类
class BallObject : public GameObject {
public:
	// 球的状态
	GLfloat    Radius;
	GLboolean  Stuck;

	BallObject();
	BallObject(glm::vec2 pos, GLfloat radius);
};

// 子类实现  // 注意这里还初始化了其父类的构造函数
BallObject::BallObject() : GameObject(), Radius(12.5f), Stuck(true) { }
BallObject::BallObject(glm::vec2 pos, GLfloat radius) : GameObject(pos, glm::vec2(radius * 2.0f, radius * 2.0f), Radius(radius), Stuck(true) { }

4.3.7 类对象作为类成员

c++类中的成员可以是另一个类的对象,称该成员为==对象成员==

class Phone {
public:
	Phone(string phone_name) :p_name(phone_name) {
		cout << "Phone的构造函数" << endl;
	}
	~Phone() {
		cout << "Phone的析构函数" << endl;
	}

public:
	string p_name;
};

class Person {
public:
	// a_phone(pName)可以看作是 Phone a_phone = pName;  隐式转换法 
	Person(string name, string pName) :a_name(name), a_phone(pName) {
		cout << "Person的构造函数" << endl;
	}
	~Person() {
		cout << "Person的析构函数" << endl;
	}
public:
	string a_name;
	// 一个类的实例对象作为一个类的成员
	Phone a_phone;  // 这是默认构造的写法,但是有了有参构造,讲道理这写法是不对的,但是(看上面)  // 再看时的理解:这只是声明一个数据类型,下面实例化的时候传参数`小米手机`进来会将这自动构造
};

void test01() {
	Person p("张三", "小米手机");
	// p.a_phone还只是一个类的实例对象,还要再去点一下p_name属性
	cout << p.a_name << " 拿着 " << p.a_phone.p_name << endl;
}

Ps:Phone类的实例,作为Person的属性,那么构造函数的先后顺序:先有的Phone的构造函数,再有的Person的构造函数(就像先有小的部件,再有完整的玩具);

​ 但是析构函数时先Person,再Phone,和构造函数刚好相反。

4.3.8 静态成员

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

class Person {
public:
	static void func() {
		// 修改值
		a = 100;  // 静态成员函数只能访问静态成员变量
		 
		// 直接报错,因为所有对象共用一个函数,对象都会有自己的属性b,就不知道改哪个,而静态成员变量就一份
		//b = 200;  
		cout << "静态成员函数的调用,a = " << a << endl;
	}
public:
	static int a;  // 静态成员变量
	int b;  // 非静态成员变量
};
int Person::a = 50;  // 类外初始化

void test01() {
	// 1、通过对象访问
	Person p;
	p.func();
	// 2、通过类名访问
	Person::func();
}

4.4 C++对象模型和this指针

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

​ 在c++中,类内的成员变量和成员函数分开存储,==只有非静态成员变量才属于类的对象上,才占类实例化对象的空间==

class Person {
public:
	// 非静态成员变量,实例化一个对象,就有这么一份
	int m_A;  // 非静态成员变量,属于类的对象上,所以下面打印对象的占的空间比空类会变大
	static int m_B;  // 静态成员变量,不占对象空间,下面打印对象的大小也不会变
	
	void func() {}
	static void func1() {}  // 这俩都不占对象空间,所有函数都是共享一个函数实例
	// 这种就相当于代码就一份,大家共用,下面会讲到用this区分是谁在调用
};

void test01() {
	Person p1;
	// 空类都会占一个字节,用于标记区分空对象占内存的位置,即空类占一个字节
	cout << "空类占的字节数:" << sizeof(p1) << endl;  // 1(空类时)
	// 在加了int类型的非静态成员变量后,大小就成了4
}

4.4.2 this指针概念

​ 在上一小节知道,在c++中成员变量和成员函数是分开存储的,每一个非静态成员函数(我学习时的理解觉得静态成员函数也是啊)都只会诞生一份函数实例,也就说是一个类的多个实例化对象会共用一份代码,那么问题:这一块代码时如何区分哪个对象调用自己的呢?

​ 答:c++通过提供特殊提供的对象指针—-this指针,解决上述问题;==this指针指向被调用的成员函数所属的对象==

this指针的用途:

class Person {
public:
	// 1、thi来区分同名是的情况
	Person(int age) {  // 构造函数
		// 这样的话就根本没再给成员变量赋值,因为名字相同了,是错的
		//age = age;  // 下面打印出来根本不是20岁,
		this->age = age;  // +一个this,表明这是成员变量的age(但尽量命名要规范)
	}

	void personOneAddAge(Person p) {  // 本来的age加上传进来的人的age
		//age += p.age;  // 这样也是可以的,但是下面写法更清楚
		this->age += p.age; // 只可加一次
	}
	// 2、返回对象本身
	//Person personManyAddAge(Person p) {   // 值返回
	Person& personManyAddAge(Person p) {   // 引用返回
		this->age += p.age;
		return *this;  // this指向实例化对象本身,解引用后返回
	}
public:
	int age;
};

void test01() {
	Person p1(10);
	cout << "p1的年纪:" << p1.age << endl;

	Person p2(15);
	p2.personOneAddAge(p1);
	cout << "p2的年纪:" << p2.age << endl;

	Person p3(10);
	/*要是上面函数返回的类型是 Person,就是值传递,第一次操作时就拷贝构造是复制一份去做加的操作;
	第二次执行.personManyAddAge(p1),还是原来的p3.age=10复制一份去操作,所以无论多少次最后结果都是20;*/
	/*要是是返回的引用,即 Person& ,就返回的是引用,就一直是在p3.age=10上进行加法,
	所以最后的结果会是40 */
	p3.personManyAddAge(p1).personManyAddAge(p1).personManyAddAge(p1);
	//  p3.personManyAddAge(p1)只有返回了对象本身才能再继续调用.personManyAddAge()函数
	cout << "p3的年纪:" << p3.age << endl;
}

4.4.3 空指针访问成员函数

​ C++中空指针也是可以调用成员函数的,但是也要注意有没有用到this指针(有用到成员属性的,默认前面就会有this指针,就不行);

​ 如果有空指针和this指针(我理解为用到成员属性),需加以判断保证代码健壮性。

class Person {
public:
	Person(int age) {
		m_Age = age;
		//this->m_Age = age;  // 这也是可以的
	}
	void showSentence() {
		cout << "只为打印一句话,没用到成员属性(就没this指针)" << endl;
	}
	void showAge() {
		// 必须加这个空指针判断,更健壮,不然下面就会报错,错的原因:
		// 传进来一个空指针,去访问成员属性肯定是没有的,是错的
		if (this == NULL) {
			return;
		}
		cout << "年龄:" << m_Age << endl;  // 这俩是一样的,前面不写也会默认有
		cout << "年龄:" << this->m_Age << endl;  
	}
public:
	int m_Age;
};

void test01() {
	Person *p = NULL;   // 创建一个空指针
	p->showSentence();  // 指针要这样去访问,空指针这也没问题
	p->showAge();
}

4.4.4 const修饰成员函数

==常函数==:成员函数后加const后我们称这个函数为常函数。

==常对象==:

class Person {
public:
	// 构造函数初始化
	Person() {
		m_A = 10;
		m_B = 20;
	}
	/*this指针的本质是一个指针常量,指向不可以改了,但是指向的值是可以改的,这就是普通函数通过赋值操作来改指向的值
	要想使指向的值也不可改,需要声明常函数,指针前面也要再加个const,这里加到了函数名称后,就是常函数,就是 const Type * const pointer
	*/
	void showPerson() const {
		// 普通成员函数时可以修改的,加了const后这就是错的,就不能修改
		// this->m_A = 100;  // this可省略
		cout << "a:" << m_A << endl;
		this->m_B = 50;  // this可省略
		cout << "b:" << m_B << endl;  // 常函数中只能对加了mutable的成员属性可以修改
	}
	void func() {
		cout << "这就是一个普通的函数" << endl;
	}
public:
	int m_A;
	mutable int m_B;
};

void test01() {
	Person p1;  // 普通对象
	p1.showPerson();  // OK
	p1.func();  // OK
	cout << "*****" << endl;
	// 常对象
	const Person p2; 
	p2.showPerson();  // OK
	// p2.func();  // 错的,常对象只能调用常函数
	// 常对象要是能调用别的函数,别的函数是可以修改成员属性的,就违背了常对象这一理念,
}

4.5 友员

​ 对于一个类,在类外,public的属性是都能访问的,但对于私有属性(private)是不能访问的,但偶尔想让一些例外的函数进行访问,就需要用到==友员==的技术;

​ 友员的目的就是让==一个函数或者类能够访问另一个类中的私有成员;

友员的三种实现: 关键字为 ==friend==

4.5.1 全局函数做友员

​ 那这全局函数就可以访问这个类中的private属性。

class Building {
	// 友员函数核心是这
	friend void goodGay(Building *buil);
public:
	Building() {
		SittingRoom = "客厅";
		BeddingRoom = "卧室";
	}
public:
	string SittingRoom;
private:
	string BeddingRoom;
};
// 全局函数(练习下指针)
void goodGay(Building *buil) {
	cout << "好友正在访问:" << buil->SittingRoom << endl;
	// 直接这样是访问不了私有属性的, 类内最上面像写函数声明一样,然后再加上`friend`
	cout << "好友正在访问:" << buil->BeddingRoom << endl;  
}
int main() {
	Building building;
	goodGay(&building);
	system("pause");
	return 0;
}

4.5.2 类做友员

​ 那这个类中的成员函数都可以访问另外一个类的私有属性。

// 下面类在用 Building这个类了,先在这里声明下,后面在写,不然会报错,类似于函数声明
class Building;
class GoodGay {
public:
	GoodGay();   // 构造函数,也可以在类外实现
public:
	// 在类内可以只写函数声明,然后再类外实现
	void visit();
	// 把另外一个类作为函数成员,且创建的是指针
	Building *building;  // 看作一个属性,然后要去赋值的
};

class Building {
	// 告诉解释器,GoodGay类时Building类的好朋友,它的成员函数都可以访问Building中的私有属性
	friend class GoodGay;
public:
	Building() {
		m_SittingRoom = "客厅";
		m_BeddingRoom = "卧室";
	}
public:
	string m_SittingRoom;
private:
	string m_BeddingRoom;
};

// 类外构造函数(也要加作用域)
GoodGay::GoodGay() {
	// 上面类属性building就是定义的指针,new出来的值就必须指针去接收
	building = new Building;  // new的语法:new 类型;
}
// 类中函数在类外实现时一定要加作用域,不然就成了全局函数
void GoodGay::visit() {
	// 也可以不要构造函数,直接就在这里初始化building
	//building = new Building;  // new的语法:new 类型;
	cout << "好友正在访问:" << building->m_SittingRoom << endl;
	// GoodGay这个类时不能去访问Building类中的private属性的,除非它是它的友员
	cout << "好友正在访问:" << building->m_BeddingRoom << endl;
}

void test01() {
	GoodGay person;
	person.visit();
}

4.5.3 类成员函数做友员

​ 这个类指定的函数,可以访问另一个类的private属性,其他不行。

class Building;
class GoodGay {
public:
	GoodGay();

public:
	void visit01();
	void visit02();
	Building *build;
};

class Building {
	// 只把GoodGay中的visit01函数设成了友员函数
	friend void GoodGay::visit01();
public:
	Building() {
		this->m_SittingRoom = "客厅";
		m_BedingRoom = "卧室";  // 这俩是一样的,一定要看得懂
	}
	string m_SittingRoom;
private:
	string m_BedingRoom;
};

GoodGay::GoodGay() {
	// Building类实现一定要写在这前面,不然这会报错
	build = new Building;
}
void GoodGay::visit01() {
	cout << "visit01正在访问:" << build->m_SittingRoom << endl;
	cout << "visit01正在访问:" << build->m_BedingRoom << endl;
}
void GoodGay::visit02() {
	cout << "visit02正在访问:" << build->m_SittingRoom << endl;
	// 这不是友员,就不能访问
	//cout << "visit02正在访问:" << build->m_BedingRoom << endl;
}

void test01() {
	GoodGay guy;
	guy.visit01();
	guy.visit02();
}

4.6 运算符重载

​ 概念:对已有的运算符(+、-等等)重新进行定义,赋予其另一种功能,以适应不同的数据类型;简单来说已有运算符一般是用来运算内置数据类型,重载后就可以更好的计算更多的是自定义数据类型。

​ 关键字:operator 加上要重载的符号作为函数名,如:operator+operatpr<<

[]这种也可以重载,用于自定义数据数组的[2]这样去取数据

4.6.0 下标重载的注意点

一般会定义下标运算符 operator[]

​ ==下标运算符返回的是元素的引用==,所以当StrVec是非常量时,可以给元素赋值,而当我们对常量对象取下标时,不能为其赋值:

const StrVec cvec = svec;      // 假设 svec 是一个StrVec对象,这是把svec的元素拷贝到cvec中
if (svec.size() && sevc[0].empty()) {
	svec[0] = "zero";   // 正确:svec是非常量,下标运算符返回string的引用,可以赋值
	cvec[0] = "zip";    // 错误:对cvec取下标返回的是常量引用
}

4.6.1 +运算符重载

​ ==加号运算符==重载:实现有自定义数据类型相加的运算

// 类内成员函数实现重载

class Person {
public:
	Person(int a, int b) :m_A(a), m_B(b) {}
	// 两种方式实现成员函数 + 号运算符重载
	// 1.可以取任意的名字
	//Person personAddPerson(Person *p1) {  // 这也可以是引用
	// 2.使用operator关键字可以可以简化写法
	Person operator+(Person &p1) {
		// 因为上面构造函数的写法,我这必须要初始化
		Person temp(0, 0);
		temp.m_A = this->m_A + p1.m_A;
		temp.m_B = m_B + p1.m_B;  // 类内,自身的+上传进来的 
		return temp;  // 值返回,相当与是创建一个新的副本
	}
public:
	int m_A;
	int m_B;
};
void test01() {
	Person p1(10, 20);
	Person p2(5, 10);
	//Person p3 = p1.personAddPerson(&p2);
	//Person p3 = p1.operator+(&p2);  // 本质调用,下面是简写
	Person p3 = p1 + p2;  // 注意使用operator关键字时,尽量就用引用,用指针前面还要加个&,不简洁,不好看
	// 这应该用的就是隐式转换法,使用的拷贝构造
	cout << "p3.m_A = " << p3.m_A << endl;
	cout << "p3.m_B = " << p3.m_B << endl;
}

​ 全局函数实现运算符重载,也可函数重载。

// 全局函数运算符的重载
// 若是用p + 10 就是Perons + int 就不对,就可以使用下面函数重载
Person operator+(Person &p1, Person &p2) {
	Person temp(0, 0);
	temp.m_A = p1.m_A + p2.m_A;
	temp.m_B = p1.m_B + p2.m_B;
	return temp;
}
// 运算符重载,也可以发生函数重载
Person operator+(Person &p, int a) {
	Person temp(0, 0);
	temp.m_A = p.m_A + a;
	temp.m_B = p.m_B + a;
	return temp;
}

void test01() {
	Person p1(10, 20);
	Person p2(5, 10);

	//Person p3 = operator+(p1, p2);  // 本质调用,下面是简写
	Person p3 = p1 + p2;
	cout << "p3.m_A = " << p3.m_A << endl;
	cout << "p3.m_B = " << p3.m_B << endl;

	Person p4 = p1 + 100;
	cout << "p4.m_A = " << p4.m_A << endl;
	cout << "p4.m_B = " << p4.m_B << endl;
}

4.6.2 <<运算符重载

​ ==左移运算符==重载:可以输出自定义数据类型

class Person {
	// 私有属性,外部访问,需加成友员函数
	friend std::ostream& operator<<(std::ostream &cout, Person p);
public:
	Person(int a, int b): m_A(a), m_B(b) {}
private:
	int m_A;
	int m_B;
};
/*
ostream &cout 是标准输出流,必须唯一,所以传进去必须用引用;
若无返回值,是void,那就只能执行一次,就没办法做到链式编程了,
那下面就无法跟"hello world"了,,所以要返回标准的cout的引用
*/
// 注意这是函数,()里是定义的参数,那么就是`参数类型 参数名`,不要把`ostream cout` (这只是举例)看成了类的实例化,更不会有::这种作用域的符号
std::ostream& operator<<(std::ostream &cout, Person p) {
	cout << "m_A = " << p.m_A << "m_B = " << p.m_B;
	return cout;
}
/* 这由于是引用,就是起别名,所以可以就写成任意名,比如c,都是指向标准的cout */
//ostream& operator<<(ostream &c, Person p) {
//	c << "m_A = " << p.m_A << "m_B = " << p.m_B;
//	return c;
//}

void test01() {
	Person p(10, 20);
	// 本质:operator<<(cout, p)   简写就是  cout << p
	cout << p << "  hello woeld" << endl;
}
/*
这个无法使用成员函数重载,成员函数的本质形式一定是这样 p.operator<<(cout) ,
简写就是 p << cout ,就不对
成员函数,必然是自身的this去.operator
*/

Ps:若有需要,可以看看自己做题的这个demo

4.6.3 ++运算符重载

​ ==递增运算符==重载:通过这,实现自己的整型数据的++

class MyInteger {
public:
	MyInteger(int a) {
		m_A = a;
	}
	// 这是写的前置++,因为自身+1后再返回的
	// 必须返回自身引用才能一直对一个数据对象+,不然每次都是在复制一份再+,++(++myint) 的结果是12了,但是myint始终是11
	MyInteger& operator++() {
		this->m_A += 1;
		return *this;  // 这this是指针,*解引用后就是对象,返回引用
	}

	// 这是后置++,先用一个临时对象来记录当前值,m_A再+1,再返回临时对象
	// 这里千万不能返回引用,不能返回局部变量(栈区对象)引用,一次后就释放了,后续的操作就是非法的
	// int 就是前面写到的函数占位参数,这里必须int,区分前置++和后置++的重载
	MyInteger operator++(int) {  
		// 这应该是 隐式转换法,this是这个对象的指针,再解引用就是这个对象
		// 类似  Person p2 = p1;会调用拷贝构造,把所有的属性都拷过去
		MyInteger temp = *this;  // 
		this->m_A += 1;
		return temp;
	}
	int m_A;
};

// 全局重载 左移运算符 <<  ;如果m_A是private,就是这写进MyInteger类做友员
ostream& operator<<(ostream &cout, MyInteger m_int) {
	cout << m_int.m_A;
	return cout;
}
void test01() {
	MyInteger myint(10);
	// 为了输出自定义数据类型,先要重载一下 <<
	cout << ++(++myint) << endl;  //12
	cout << myint << endl;  // 12,,如果上面返回的不是引用,这就是11
}
void test02() {
	MyInteger myint(10);
	cout << (myint++)++ << endl;
	cout << myint << endl;
}

Ps:前置递增要==返回引用==;后置递增要==返回值==。

如果是显示的调用后置运算符(用的上面的对象myint): myint.operator++(0); // 调用后置版本的operator++;我理解0就是那个占位的int myint.operator++(); // 调用前置版本的operator++

4.6.4 =运算符重载

c++编译器至少给一个类添加4个函数

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

​ ==赋值运算符==重载意义:如果类中有属性指向堆区,做赋值操作时就会出现深浅拷贝的问题,编译器自带的赋值是浅拷贝,假如有属性是指针,指向堆区,默认的就是把一个对象的指针赋值给另外一个对象,那么这两个对象的这个指针属性都是指向同一个堆区,当class有析构函数对堆区数据进行回收时,就会因为反复释放引发错误,就需要重载赋值运算法进行深拷贝。

class Person {
public:
	Person(int age) {
		m_Age = new int(age);
	}
	~Person() {
		if (m_Age != NULL) {
			delete m_Age;
			m_Age == NULL;
		}
	}
	// 重载赋值运算符
	// 参数一定要用引用或是指针传递,绝对不能用值传递
	Person& operator=(Person &p) {
		// 编译器的操作就是(浅拷贝):
		// m_Age = p.m_Age;  把自己指向的地址复制过去,所以第二次释放就会错

		// 深拷贝之前还要判定一下这个区域为不为空,不空就要删除堆区,再置为NULL,然后重新开辟
		if (m_Age != NULL) {
			delete m_Age;
			m_Age = NULL;
		}
		this->m_Age = new int(*p.m_Age);
		return *this;  // void只能用一次(p1 = p2),为了连等(p1=p2=p3)
	}
	int *m_Age;
};
void test01() {
	Person p1(10);
	Person p2(20);
	p1 = p2;  // 把p2都赋值给p1;
	// 自带的赋值是浅拷贝,然后析构函数对堆区数据的释放,第二次就会报错,故要把自带的赋值运算符重载成深拷贝

	cout << *p1.m_Age << endl;  // p1.m_Age是指针,要*解引用
	cout << *p2.m_Age << endl;
	Person p3(30);
	p1 = p2 = p3;
	// p2 = p3 先执行(p3赋值给p2),又返回p2本身得到 p1=p2;所以最后都是30
}

4.6.5 == !=运算符重载

一般重载了 == 操作,那尽量也要重载 != ;重载了 < ,也要重载其它关系运算符,以保持较好的兼容性。且一种比较便捷的写法就是:

bool operator==(const A_class &lhs, const A_class &rhs) {
    return lhs.isbn() == rhs.isbn() &&
           lhs.units_sold == rhs.units_sold;
}
// 接下来重载不等就很简单了
bool operator!=(const A_class &lhs, const A_class &rhs) {
    return !(lhs == rhs);         // 直接用上面==的结果取反就好了
}

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

class Person {
public:
	Person(string name, int age) {
		m_Name = name;
		this->m_Age = age;
	}
	// 记得用引用
	bool operator==(Person &p) {
		bool out = false;
		if (m_Age == p.m_Age && m_Name == p.m_Name) {
			out = true;
		}
		return out;
	}
	bool operator!=(Person &p) {
		bool out = false;
		if (m_Age != p.m_Age || m_Name != p.m_Name) {
			out = true;
		}
		return out;
	}
	string m_Name;
	int m_Age;
};
void test01() {
	Person p1("张三", 10);
	Person p2("张三", 10);
	cout << (p1 != p2) << endl;
}

4.6.6 ()运算符重载

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

class MyAdd {
public:
	// 重载了函数调用的()
	int operator()(int a, int b) {
		return a + b;
	}
};
// 全局函数实现一个加法
int addFunc(int a, int b) {
	return a + b;
}
void test01() {
	MyAdd ya;
	// 这又叫仿函数,重载了(),跟函数调用一模一样
	int ret1 = ya(10, 20);
	// 这是函数调用,长得很像
	int ret2 = addFunc(10, 20);
	cout << "ret1 = " << ret1 << endl;
	cout << "ret2 = " << ret2 << endl;
	// 看到 函数名()第一反应就应该是匿名对象,再跟着的 () 就是重载的 () 的调用
	cout << MyAdd()(10, 20) << endl;  // 30  
	// 针对单纯调用一下,并不想创建对象,用完就回收
}

4.6.7 标准库定义的函数对象

标准库定义了函数对象(可以理解为函数名来代替各种运算符):这写都是定义在#include <functional>头文件中。

算术 关系 逻辑
std::plus<T> + std::equal_to<T> == std::logical_and<T> &&
std::minus<T> - std::not_equal_to<T> != std::logical_or<T> ||
std::multiplies<T> * std::greater<T> > std::logical_not<T> !
std::divides<T> / std::greater_equal<T> >=  
std::modulus<T> % std::less<T> <  
std::negate<T> 取相反数 std::less_equal<T> <=  

使用:把这看作一个类,先实例化一个对象,然后用这个对象去做对应的操作:

#include <functional>
std::minus<int> a;
std::negate<int> b;
std::cout << a(10, 5) << std::endl;  // 5
std::cout << b(10) << std::endl;     // -10

这个就是常用于一些算法的第三个参数,类似于这std::sort(v.begin(), v.end(), std::greater<int>());它这里就理解为一个匿名对象,等着被()调用,看这行就很好理解了(第一个括号时是创建匿名对象,第二个才是调用):

std::cout << std::minus<int>()(10, 5) << std::endl;    // 5

一个练习:

(a) 统计大于1024的值有多少个。 (b) 找到第一个不等于pooh的字符串。 (c) 将所有的值乘以42。

using std::placeholders;
std::count_if(vec.begin(), vec.end(), std::bind(std::greater<int>(), _1, 1024));
std::find_if(v.begin(), v.end(), std::bind(std::not_equal_to<std::string>)(), _1, "pooh");
std::transform(v.begin(), v.end(), des_begin(), std::bild(std::multiplies<int>(), _1, 42));

再一个练习:题目是:使用标准库函数对象判断一个给定的int值是否能被 int 容器中的所有元素整除。

#include <functional>
#include <algorithm>
std::vector<int> vec{2, 4, 5, 6, 8, 10};
int num = 2;   // 先把这个任意num写死
// 我的写法是:
auto res = std::find_if(vec.begin(), vec.end(), std::bind(std::modulus<int>(), std::placeholders::_1, num));   // 注意modulus<int>(),有这个()才是匿名对象
if (res != vec.end()) { /* 不等就代表有除不尽的 */}

// 书上的写法是
std::modulus<int> mod;
auto pre = [&](int i) {return 0 == mod(num , i); };
auto is_divid = std::any_of(vec.begin(), vec.end(), pre);  // 注意这个用法
std::cout << (is_divid ? "Yes!" : "no!" ) << std::endl;

Tips:

4.6.8 类型转换运算符

==类型转换运算符==是类的一种特殊成员函数,它负责将一个类型的值转换成其他类型。类型转换函数的一般形式如下:operator type() const; (这在书上也是重载的那一章)

比如:

class SmallInt {
public:
	SmallInt(int i = 0):val(i) {
		if (i < 0 || i > 255) {  // 定义一个类,表示0~255之间的整数
			throw std::out_of_range("Bad SmallInt value");
		}
	}
	// 这里就定义了从类类型向其他类型的转换。
	operator int() const {
		return val;
	}
private:
	std::size_t val;
};

int main(int argc, char*argv[]) {
	
	SmallInt si;
     // 首先将4隐式地转换成SmallInt,然后调用SmallInt::operator=  (这里没定义,应该就是调用合成的)
	si = 4; 
	std::cout << si << std::endl;  // 如果没定义`类型转换运算符`,这里是会直接报错的
	si + 3;  // 首先将si隐式地转换成int,然后执行整数的加法

	SmallInt si1 = 3.14;  // 调用SmallInt(int)构造函数
	si + 3.14;    // 内置类型转换将所得的int继续转换成double
	system("pause");
	return 0;
}

注意容易错误的:地方

class SmallInt;
operator int(SmallInt&);   // 错误:不是成员函数
class SmallInt {
	int operator int() const;   // 错误:指定了返回类型
	operator int(int i = 0) const;  // 错误:参数类表不为空
	operator int*() const {return 42;}   // 错误:42不是一个指针
};
显式的类型转换运算符

​ 类型转换运算符可能产生意外结果:简单说如果类型转换自动发生(就像上面那样隐式转换),可能会引发意想不到的结果,所以在c++11新标准中引入了显式的类型转换运算符

class SmallInt {
public:
    // 这样编译器就不会自动执行这一类型转换
	explict operator int() const {return val;}
    // explict operator const int() {return val;}  // 这种是没有意义的
	// 其它成员与上面的版本一致
};

这样和显式的构造函数一样,编译器(通常)也不会将一个显式的类型转换运算符用于隐式类型转换:

SmallInt si = 3;   
si + 2;   // 错误:此处需要隐式的类型转换,但类的运算符指定必须是显式的
static_cast<int>(si) + 2;  // 正确:显式地请求类型转换

​ 即:当类型转换运算符是显式的试,必须通过显式的强制类型转换才可以,但是该规定错在一个例外:如果表达式被用作条件,则编译器会将显式的类型转换自动应用于它;换句话说,当表达式出现在下列外置时,显式的类型转换将被隐式地执行:

转换为bool

​ 早期标准中,IO类定义了向void*的转换规则,以求避免上面提到的问题,在c++11新标准中,IO标准库通过定义一个向bool的显式类型转换实现同样的目的。

​ 无论什么时候在条件中使用流对象,都会使用为IO类型定义的operator bool。例如:while(std::cin » value),while语句的条件执行输入运算符,它负责将数据读入到value并返回cin,为了对条件求值,cin被istream operator bool类型转换函数隐式地执行了转换,如果cin的条件状态是good(文件流那里应该记录过这),则该函数返回为真,否则返回为假。Tips:向bool的类型转换通常在条件部分,因此operator bool一般定义为explicit的。

转换的优先级
  1. 精确匹配
  2. const 转换。
  3. 类型提升
  4. 算术转换
  5. 类类型转换

4.7 继承

基本语法:class 子类 : 继承方式 父类 {};

class A : public B {}; // 还可以有private、protected

A 类称为子类 或 派生类

B 类称为父类 或 基类 //基类通常都应该定义一个虚析构函数,即使该函数不执行任何实际操作也是如此。

4.7.0 override | final

​ 派生类必须在内部对所有重新定义的虚函数进行声明,派生类可以在这样的函数之前加上virtual关键字,但是并不是非得这么做,C++11新标准允许派生类显式地注明它将使用哪个成员函数改写基类的虚函数,具体措施是在该函数的形参列表之后增加一个override关键字(override关键字来说明派生类中的虚函数,相当于一种标记)。

struct A {
	virtual void f1(int) const;
	virtual void f2();
	void f3();
};
struct B : A {
    virtual void f1(int) const override;  // 正确:和下面一行一个意思,virtual可要可不要
	void f1(int) const override;     // 正确:f1重载,且与基类中的f1匹配
	void f2(int) override;           // 错误:类A中没有形如f2(int)的函数
    void f3() override;              // 错误:f3并不是虚函数(只有虚函数才能被覆盖)
    void f4() override;              // 错误:A中并没有名为f4的函数
};  // 

// 关键字final还能阻止函数被覆盖
struct B2 : A {
    // 已经从A中继承f2()和f3(),然后下面覆盖f1(int)
    void f1(int) const final;      // 加了final就不允许后续的其它类覆盖f1(int)了
}
struct C3 : B2 {
    void f2();            // 正确:覆盖从间接基类B2继承而来的f2
    void f1(int) const;   // 错误:B3已经将f2声明成了 final    
};

final和override说明符出现在形参列表(包括任何const或引用修饰符)以及位置返回类型之后。

有防止继承的发生:c++11新标准提供了一种防止继承发生的办法,即在类名后跟一个关键字final:

// 1、
class NoDerived final { /**/ };    // 类NoDerived就不能作为基类了

// 2、
class Base { /**/ };
class Last final : Base { /**/ };    // 这时类Last就不能作为基类了
class Bad : Last { /**/ };           // 错误:Last是final的

​ 在某些情况下,希望对虚函数不要进行动态绑定,而是强迫其执行虚函数的某个特定版本,则可以使用作用域运算符实现这一目的,如下面的代码: ​ double undis_123 = base->Quote::net_price(42);
​ 该代码强行调用Quote的net_price函数,而不管baseP实际指向的对象类型到底是什么,该调用将在编译时完成解析。这就是==回避虚函数的机制==。就是如果一个派生类虚函数需要调用它的基类版本,但是没有使用作用域运算符,则在运行时该调用将被解析为对派生类版本的自身的调用,从而导致无限递归。


可以将一个成员函数同时声明成 overridefinaloverride 的含义是重写基类中相同名称的虚函数,final 是阻止它的派生类重写当前虚函数。

class Base { /**/ };  
struct D1 : Base { /**/ };    // 默认public继承
class D2 : Base { /**/ };     // 默认private继承

4.7.1 继承方式

class Base {
public:
	int m_A;
protected:
	int m_B;
private:
	int m_C;
};
class Son1 : public Base {
public:
	// 需要用函数类内去访问,不能直接在外面做m_A = 11;这样的操作
	void func() {
		m_A = 11; // OK,父类public,到子类也是public
		m_B = 12; // OK,父类protected,到子类也是protected
		//m_C = 13;  // 错的,父类的privated不能访问
	}
};
class Son2 : protected Base {
public:
	void func() {
		m_A = 11; // OK,父类public,到子类是protected
		m_B = 12; // OK,父类protected,到子类还是protected
		//m_C = 13;  // 错的,父类的private不能访问
	}
};

// 这种就全部变成了私有属性,Son3再作为父类继承,那它的子类是一个属性都访问不到的
class Son3 : private Base {
public:
	void func() {
		m_A = 11; // OK,父类public,到子类是private
		m_B = 12; // OK,父类protected,到子类还是private
		//m_C = 13;  // 错的,父类的privated不能访问
	}
};
// 类外都只能访问public,类内可以访问public和protected
void test01() {
	Son1 s1;
	Son2 s2;
	Son3 s3;
	s1.m_A = 10;
	//s2.m_A = 10;  // 这俩都不能访问
	//s3.m_A = 10;
}

​ 父类中所有非静态成员竖向都会被子类继承下去,私有成员只是被编译器隐藏了访问不到,还是会继承下去。

class Base {
public:
	int m_A;
protected:
	int m_B;
private:
	int m_C;
};
class Son : public Base {
public:
	int m_D;
};
void test01() {
	Son s;
	cout << sizeof(s) << endl;  // 16个字节
}

==继承中构造和析构顺序==

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

​ 且

先调用父类构造函数,再调用子类构造函数,析构顺序与构造相反(先子类,再父类)

4.7.2 继承同名函数调用

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

class Base {
public:
	Base() {
		m_A = 100;
	}
	void func() {
		cout << "Base - func()调用" << endl;;
	}
	// 同名重载
	void func(int) {
		cout << "Base - func(**int**)调用" << endl;
	}
	int m_A;
};
class Son : public Base {
public:
	Son() {
		m_A = 200;
	}
	void func() {
		cout << "Son - func()调用" << endl;
	}
	int m_A;
};
void test01() {
	Son s;
	cout << s.m_A << endl;  // 200, 直接访问的是子类的
	cout << s.Base::m_A << endl;  // 10, 加作用域 

	s.func();
	// s.func(10); 不行,讲道理,这个传参数重载了,应该不用加作用域了,但是子类中也有同名的存在
	s.Base::func();
	s.Base::func(10);
}

当子类与父类拥有同名的成员函数(静态成员函数也一样),子类会隐藏父类中所有版本的同名成员函数

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

4.7.3 继承同名静态成员

class Base {
public:
	static void func() {
		cout << "---父类func()调用---" << endl;
	}
	static int m_A;
};
// 静态变量一定要类内定义,类外实现,且一定要加作用域
int Base::m_A = 100;  

class Son : public Base {
public:
	static void func() {
		cout << "---子类func()调用---" << endl;
	}
	static int m_A;
};
int Son::m_A = 200;

// 静态成员属性
void test01() {
	// 1、通过创建对象来访问
	Son s;
	cout << s.m_A << endl;  // 直接访问肯定是子类的
	cout << s.Base::m_A << endl;  // 访问父类是这样加作用域
	s.func();
	s.Base::func();  // 注意调用方式,先点再俩冒号
	cout << "************************************" << endl;

	// 2、通过类名来访问
	cout << Son::m_A << endl;  // 类名::属性名称访问
	cout << Son::Base::m_A << endl;  // 通过资子类中的父类作用域去访问
	Son::func();
	Son::Base::func();  // 这是俩冒号
}

Ps:同时静态成员处理方式和非静态处理方式一样,只不过有两种访问的方式(==通过对象== 和 ==通过类名==)

4.7.4 多重继承

C++允许一个类继承多个类,但在实际开发中,不建议使用多继承。

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

class Son : public Base2, public Base1 {};

​ 多继承可能会引发父类中有同名成员出现(即比如父类1、父类2中都有一个m_A属性),那就产生了==二义性==,那子类访问的时候就要像上面同名函数处理一样,加作用域进行区分。

​ 用于查看类的结构,用打开vs的命令提示符,cd到这个类所在cpp的路径,然后输入下面这行命令就行了:cl /d1 reportSingleClassLayout类名 类所在cpp文件名称

4.7.5 菱形继承|虚继承

​ 概念:两个派生类都继承了同一个基类;而又有某个类同时继承了这两个派生类,这样的继承被称为==菱形继承==,或者==钻石继承==。

​ 那么这某个类在使用时就会自最开始的基类的数据继承了两份,然而我们其实就要一份就好,这就加大了开销,导致了资源浪费及毫无意义。

​ ==虚继承==的目的是令某个类做出声明,承诺愿意共享它的基类,共享的基类子对象称为成为==虚基类==,这样无论虚基类在继承体系中出现了多少次,在派生类中都只包含唯一一个共享的虚基类子对象。

// 菱形继承
// Son1 Son2两个类都继承了Base,都有m_A属性,此时GrandChdir又同时继承了Son1 Son2两个类,那就有两份m_A(就会有资源浪费)
class Base {
public:
	int m_A;
};
class Son1 : public Base {};
class Son2 : public Base {};
class GrandChdir : public Son1, public Son2 {};

void test01() {
	GrandChdir gs;
	gs.Son1::m_A = 18;
	gs.Son2::m_A = 20;
	cout << gs.Son1::m_A << endl;  // 18
	cout << gs.Son2::m_A << endl;  // 20,必须加作用域去访问
    // couy << gs.m_A << endl;  //因为有二义性,不能这样去访问
}

​ 解决办法:继承前加==virtual==关键字后,变成==虚继承==。

// 此时公共的父类(最开始的基类)称为虚基类
class Base {
public:
	int m_A;
};
class Son1 : virtual public Base {};
class Son2 : virtual public Base {};  // 就这俩加了
class GrandChdir : public Son1, public Son2 {};

void test01() {
	GrandChdir gs;
	gs.Son1::m_A = 18;
	gs.Son2::m_A = 20;
	cout << gs.Son1::m_A << endl;  // 20
	cout << gs.Son2::m_A << endl;  // 20
	cout << gs.m_A << endl;  // 20  // 这是就唯一了,就可以这样访问
}

Ps:虚继承实现的原理,靠的是一个叫vbptr(叫做虚基类指针),具体怎么实现的看视频吧。

4.8 多态

4.8.1 多态的基本概念

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

多态分为两类:

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

class Animal {
public:
	virtual void speak() {  
		cout << "动物在说话" << endl;
	}
};
class Cat : public Animal {
public:
	void speak() {
		cout << "猫在说话" << endl;
	}
};
class Dog : public Animal {
public:
	void speak() {  // 子类在重写时也是可以加个virtual关键字的(一般不要吧)
		cout << "狗在说话" << endl;
	}
};
// 如果函数地址在编译阶段就能确定,那么就是静态联编
// 如果函数地址在运行阶段才能确定,那么就是动态联编

// 这里相当于是nimal &animal = cat;
/* 要是没有虚函数,那就是`地址早绑定`,在编译阶段已经确定函数地址,
所以无论后面传什么派生类动物,都是`动物在说话`*/
void test01(Animal &animal) {  // 注意这是父类指针或是引用
	animal.speak();
}
/*可是我们想传猫,猫说话;传狗,狗说话(即希望传入什么对象,就调用什么对象的函数),那么这个函数地址就不能早绑定,
需要在运行阶段进行绑定,即地址晚绑定,那就需要在基类这函数加virtual关键字,
	这应该就是`虚函数`  */

int main() {
	Cat cat;
	test01(cat);
	Dog dog;
	dog.speak();
    
     // 若是只有一个简单的speak函数,就占一个字节
	// 若是加了virtual,就占4个字节:存的是一个叫`vfptr`的`虚拟函数(表)指针`
	// 多态的实现原理看下方视频链接
	cout << sizeof(Animal) << endl;
	system("pause");
	return 0;
}

总结:

多态满足的条件:

多态使用的条件:

多态实现原理

4.8.2 多态案例·计算器类

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

多态的优点:

// 1、普通写法,要是加一个加法就不好扩展,就要在原来的代码上进行改动
class Cal {
public:
	int m_A;
	int m_B;

	int getResult(char oper) {
		switch (oper) {
		case '+':
			return m_A + m_B;
		case '-':
			return m_A - m_B;
		case '*':
			return m_A * m_B;
		}
	}
};
void test01() {
	Cal cal = { 5, 3 };  // 这里居然可以这样初始化赋值
	int out = cal.getResult('*');
	cout << out << endl;
}
/*===============================================================*/

// 2、写一个计算器的抽象类
class Calculator {
public:
	virtual int getResult() = 0;  // 纯虚函数

	int m_A;
	int m_B;
};
// 每一种计算写一个类
class AddCal : public Calculator {
public:
	int getResult() {
		return m_A + m_B;
	}
};
class SubCal : public Calculator {
public:
	int getResult() {
		return m_A - m_B;
	}
};
class MulCal : public Calculator {
public:
	int getResult() {
		return m_A * m_B;
	}
};
class DivCal : public Calculator {
public:
	int getResult() {
		return m_A / m_B;
	}
};

void test02(Calculator *cal) {
	int out = cal->getResult();
	cout << out << endl;
}

int main() {
	// 1、以函数传参的方式来实现多态
	DivCal divcal;
	divcal.m_A = 15;
	divcal.m_B = 3;
	test02(&divcal);

	MulCal mulcal;
	mulcal.m_A = 5;
	mulcal.m_B = 6;
	test02(&mulcal);

	// 2、直接左边是抽象类(一定是父类),右边是派生类的数据类型
	Calculator *abc = new AddCal;
	abc->m_A = 3; 
	abc->m_B = 2;
	cout << abc->getResult() << endl;
	delete abc;  // 用完了把数据销毁释放

	abc = new SubCal;  // 数据释放了,指针还在
	abc->m_A = 20;
	abc->m_B = 10;
	cout << abc->getResult() << endl;

	system("pause");
	return 0;
}

4.8.3 纯虚函数和抽象类(= 0;)

​ 在多态中,通常父类中虚函数的实现是毫无意义的主要都是调用子类重写的内容,因此可以将==虚函数==改为==纯虚函数==,纯虚函数语法:virtual 返回值类型 函数名(参数列表) = 0; // 这里好像不要virtual也行,核心标志是=0

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

抽象类特点:

class Base {
public:
	virtual void func() = 0;  // 纯虚函数可以不要实现代码
};
class Son : public Base {
public:
	void func() {
		cout << "hello world" << endl;
	}
};
void test01() {
	Son s;
	s.func();
}

4.8.4 虚析构和纯虚析构

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

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

虚析构和纯虚析构共性:

虚析构和纯虚析构的区别:

虚析构语法:析构函数前加virtual virtual ~类名() {}

纯虚析构语法:virtual ~类名() = 0;类名::~类名() {} // 相当于是类内定义,类外实现(类外一定要有实现,且要加作用域)

Ps:一定注意,这 virtual 都是加在基类里面的

class Animal {
public:
	Animal() {
		cout << "Animal构造函数调用" << endl;
	}
	// 纯虚函数
	virtual void speak() = 0;

	//// 方式1、
	//// 虚析构函数(没这的话,多态调用时父类指针是不会释放子类中的堆数据的)
	//virtual ~Animal() {  // 
	//	cout << "Animal析构函数调用" << endl;
	//}

	// 方式2、纯虚析构:必须类内定义,类外实现
	virtual ~Animal() = 0;
};
Animal::~Animal() {
	cout << "Animal析构函数调用" << endl;
}  // 好像要不要这内部实现都可以的啊(这里跟上面点写定义时有点出入)


class Cat : public Animal {
public:
	Cat(string name) {
		// 指针的赋值尽量就用new的方式创建,而不是直接定义传进来来为string *name
		m_A = new string(name);
		cout << "Cat构造函数调用" << endl;
	}
	// 重写从父类继承的虚函数
	void speak() {
		cout << "这是小猫在说话,多态的调用" << endl;
	}
	~Cat() {
		cout << "Cat析构函数调用" << endl;
		if (this->m_A != NULL) {
			delete m_A;
			m_A == NULL;
		}
	}
	string *m_A;  // 定义一个指针类型,后续好在堆空间开辟内存
};

void test01() {
	Animal *animal = new Cat("Tom");
	animal->speak();
	delete animal;  
	// 要是没有虚析构函数或是纯虚析构函数结束了都不会调用子类Cat的析构函数;
	// 那么子类属性在堆上的数据就可能泄露,
	// 
}

总结:

  1. 虚析构或纯虚析构就是用来解决通过父类指针释放子类对象(我的理解就是多态调用时,也能够调用子类的析构函数,没有虚析构或纯虚析构是不能的)
  2. 如果子类在都定义时没有定义指针类型的成员属性,也就是没有堆区数据,可以不写虚析构或纯虚析构
  3. 拥有纯虚析构函数的类也属于抽象类
  4. 虚析构、纯虚析构一定是在基类中,关键字virtual也是在基类里

4.8.5 多态案例·制作饮品

基本描述:制作饮品的大致流程为:煮水 - 冲泡 - 倒入杯中 - 加入辅料

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

class AbstractDranking {
public:  // 可以有几个纯虚函数
	virtual void boil() = 0;  // 煮水
	virtual void brew() = 0;  // 冲泡
	virtual void pourCup() = 0;  // 倒入杯中
	virtual void addsomething() = 0;  // 加东西
	// 制定流程顺序
	void dowork() {
		boil();
		brew();
		pourCup();
		addsomething();
	}
};

class Tea : public AbstractDranking {
	void boil() {
		cout << "用农夫山泉煮水" << endl;
	}
	void brew() {
		cout << "用紫砂壶冲泡" << endl;
	}
	void pourCup() {
		cout << "倒入茶杯中" << endl;
	}
	void addsomething() {
		cout << "加入一些枸杞" << endl;
	}
};
class Coffee : public AbstractDranking {
	void boil() {
		cout << "用自来水煮水" << endl;
	}
	void brew() {
		cout << "用简单器皿冲泡" << endl;
	}
	void pourCup() {
		cout << "倒入咖啡杯中" << endl;
	}
	void addsomething() {
		cout << "加入一些suger" << endl;
	}
};

void test01(AbstractDranking *ad) {
	ad->dowork();
	delete ad;  // 记得释放,防止内存泄露
}
int main() {
	test01(new Tea);  // 直接就把new写进去了
	cout << "===================" << endl;
	test01(new Coffee);
	system("pause");
	return 0;
}

4.8.6 多态案例·电脑组装

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

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

创建电脑类提供让电脑工作的函数,并且调用每个零件工作的接口;测试时组装三台不同的电脑进行工作

// 几个基本硬件抽象类
class CPU {
public:
	virtual void calculate() = 0;
};
class VideoCard {
public:
	virtual void display() = 0;
};
class Memory {
public:
	virtual void storage() = 0;
};

// 具体化这几个硬件(重写)
class InterCPU : public CPU {
public:
	void calculate() {
		cout << "inter的CPU开始计算" << endl;
	}
};
class AmdCPU : public CPU {
public:
	void calculate() {
		cout << "amd的CPU开始计算" << endl;
	}
};

class NvidiaVideoCard : public VideoCard {
public:
	void display() {
		cout << "英伟达的显卡正在使用" << endl;
	}
};
class AmdVideoCard : public VideoCard {
public:
	void display() {
		cout << "AMD的显卡正在使用" << endl;
	}
};

class PirateMemory :public Memory {
public:
	void storage() {
		cout << "海盗船的内存条正在使用" << endl;
	}
};
class JinMemory : public Memory {
public:
	void storage() {
		cout << "金士顿的内存条正在使用" << endl;
	}
};

// 电脑类把这几个组装起来
class Computer {
public:
	Computer(CPU *cpu, VideoCard *vc, Memory *m) {
		this->cpu = cpu;
		this->vc = vc;
		this->m = m;
	}
	void work() {
		cpu->calculate();
		vc->display();
		m->storage();
	}
	~Computer() {
		if (cpu != NULL) {
			delete cpu;
			cpu = NULL;
		}
		if (vc != NULL) {
			delete vc;
			vc = NULL;
		}
		if (m != NULL) {
			delete m;
			m = NULL;
		}
	}
private:
	CPU *cpu;
	VideoCard *vc;
	Memory *m;
};

int main() {
	// 每台电脑都是要新的一个零件
	Computer c1(new InterCPU, new AmdVideoCard, new PirateMemory);
	c1.work();
	cout << "=================" << endl;
	Computer c2(new AmdCPU, new NvidiaVideoCard, new JinMemory);
	c2.work();
	cout << "=================" << endl;
	Computer *c3 = new Computer(new InterCPU, new NvidiaVideoCard, new PirateMemory);
	c3->work();       // 千万记得new要用 *c3去接收啊
	delete c3;
	cout << "=================" << endl;
	system("pause");
	return 0;
}

五、模板

5.0 书上补充(非常重要)

5.0.1 typename跟class的区别

​ 假定T是一个模板类类型参数,当编译器遇到类似T::men这样代码时,它不会知道men是一个类型成员还是一个static数据成员,直到实例化时才会知道。但是,为了处理模板,编译器必须知道名字是否表示一个类型。 ​ 例如:假定T是一个类型参数的名字,当编译器遇到这语句时:T::size_type * p; 它需要知道这是正在定义一个名为p的变量还是将一个名为size_type的static数据成员与名为p的变量相乘;默认情况下,C++语言假定通过作用域运算符(::)访问的名字不是类型,因此,如果我们希望使用一个模板类型参数的类型成员,就必须显式告诉编译器该名字是一个类型,我们通过==关键字typename==来实现这一点(不能使用class),所以除了通知编译器一个名字表示类型时,必须使用typename,其他时候typename跟class没任何差别。

简单总结:==使用using这种来命名别称时,或者其它,如果类型中出现了“::”的情况,前面就一定要加typename==。

template <typename T>
typename T::value_type func(const T &c) {
    if (!c.empty()) 
        return c.back();
    else
        return typename T::value_type();
}

// 同理,在pcl库还看到很多这样的(后面两个就必须加typename,表示是类型)
using value_type = PointT;
using reference = PointT&;
using const_reference = const PointT&;
using difference_type = typename VectorType::difference_type;
using size_type = typename VectorType::size_type;

​ 解读:func函数期待一个容器类型的实参,它使用typename指明其返回类型并在c中没有元素时生成一个值初始化的元素返回给调用者。

如下面的一个练习:编写函数,接收一个容器的引用,打印容器中的元素,(1)使用容器的size_type和size成员来控制打印元素的循环;(2)改用begin和end返回的迭代器来控制循环

#include <algorithm>   // std::for_each算法需要这个头文件
template <typename T>
void func(const T &t) {
	// 第一小题:
	// 下面一定要typename(没有一定报错),表示类型,,而不是作用域符号::默认的代表的数据成员
	for (typename T::size_type i = 0; i < t.size(); ++i) {
		std::cout << t[i] << std::endl;
	}

	// 第二小题,第1种:这也一定要typename
	std::for_each(t.begin(), t.end(), [](typename T::value_type val) {std::cout << val << std::endl; });
	
	// 第二小题,第2种:自己写类型也一定要typename,当然也可以用auto自己推导
	//for (auto iter = t.begin(); iter != t.end(); ++iter) {
	for (typename T::const_iterator iter = t.begin(); iter != t.end(); ++iter) {
		std::cout << *iter << std::endl;
	}
}

int main(int argc, char*argv[]) {
	std::vector<std::string> vec = { "hello", "wotld", "this", "is" };
	std::deque<int> d(3, 5);
	func(vec);
	func(d);
	system("pause");
	return 0;
}

​ 特别注意:==当使用类型参数::类型时,一定要在前面加typename,即好比typename T::size_type index= 0;==,因为这样才能告诉编译器,使用的是size_type这种类型,而不是作用域符号(::)默认代表后面的一个成员名称。如果不加,就可能会得到这个错误提示==语法错误: 意外的令牌“标识符”,预期的令牌为“;”==,或是更加直白的==“value_type”: 类型 从属名称的使用必须以“typename”为前缀==(第11行没加typename就是和这个错误)


5.0.2 默认模板实参、尾置返回类型与类型转换

==默认模板实参==:

在新标准中可以为函数和类模板提供模板实参,而更早的C++标准只允许为类模板提供默认实参。

template <typename T, typename F=std::less<T>>
int compare(const T &v1, const T &v2, F f = F()) {
	if (f(v1, v2)) return -1;
	if (f(v2, v1)) return 1;
	return 0;
}
int main(int argc, char*argv[]) {
	// 这一行就是默认用的less小于比较
	int res1 = compare(42, 0);     // 1
	// 下面这样就是用的greater大于比较(注意传进去可调用对象的形式)
	int res2 = compare(0, 42, std::greater<int>());  // 1
	return 0;
}

Tips:


==显式实例化==:

​ 当模板被使用时才会进行实例化,那么相同的实例可能出现在多个对象文件中,当两个或多个独立编译的源文件使用了相同的模板,并提供了相同的模板参数时,每个文件中就都会有该模板的一个实例。 ​ 在大系统中,在多个文件中实例化相同模板的额外开销可能非常严重,在新标准中,可以通过==显式实例化==来避免这种开销,一个显式实例化有如下形式(declaration是一个类或函数声明): ​ extern template declaration; // 声明 ​ template declaration; // 定义(这就是实例化定义了)

举个例子就是: extern template class My_class<int>; // 声明 template int compare(const int&, const int&); // 定义

当编译器遇到 extern 模板声明时,它不会再本文件中生成实例化代码,那么这也意味着承诺再程序其它位置有该实例化的一个非extern声明(定义)。


==参数类型进行自己推断的时候,要统一==,好比下面:

template <class T>
int compare(const T&, const T&);
(a) compare("hi", "world");    // 这是错的:const char[3] 和 const char[6],推断时两个实参类型不一致
(b) compare("bye", "dad");  // 这就是ok的,两个参数类型都是const char[4]

(c) compare<std::string>("hi", "world");  // 这就是ok的,显式指定模板类型

==显式指定类型和位置返回类型(挺重要的)==:

显式似乎就好像==c++都是模板编程==,不显示的指定参数类型,它就会推断,能推断出来就可以,若是不能推断出来就会编译不通过(这一点挺重要的,对后续无论是函数还是类的使用):

int a = 1, c = 2;
double b = 2;
std::max(a, b);  // 错误;无法推断类型应该是int还是doubule
std::max<int>(a, b); // 正确:指定显式模板参数,所以也是可以std::max<double>(a, b);
std::max(a, c);  // 正确,隐式推断出来类型都是int

==尾置返回类型与类型转换==:例如希望编写一个函数,接受表示序列的一对迭代器和返回序列中一个元素的引用:

template <typename It>
auto func(It beg, It end) -> decltype(*beg) {
	return *beg;  // 返回序列中一个元素的引用
}
int main(int argc, char*argv[]) {
	std::vector<std::string> vec = { "hello", "wotld", "this", "is" };
	std::deque<int> d(3, 5);

	auto &i = func(vec.begin(), vec.end());   // 返回的是string&
	auto &s = func(d.begin(), d.end());       // 返回的是int&   
	return 0;
}

解读:

以上这都只能获取到引用,无法直接获得所需要的类型,好比上面func函数,如果我们想要返回一个元素的值而非引用,那就要用到==标准库的类型转换模板==,这些模板定义在头文件#include <type_traits>,上面的func的写法就成了:

#include <type_traits>
template <typename It>
auto func(It beg, It end) -> 
	typename std::remove_reference<decltype(*beg)>::type   // 核心是这行
{
	return *beg;
}
auto i = func(vec.begin(), vec.end());  // 是auto i,不是auto &i了

解读:

下表为==标准类型转换模板==:

对Mod<T>,中Mod为 若T 为 则Mod<T>::type为
remove_reference X&或X&&
否则
X
T
add_const X&、const X或函数
否则
T
const T
add_lvalue_reference X&
X&&
否则
T
X&
T&
add_rvalue_reference X&或X&&
否则
T
T&&
remove_pointer x*
否则
X
T
add_pointer X&或X&&
否则
X*
T*
make_signed unsigned X
否则
X
T
make_unsigned 带符号类型
否则
unsigned X
T
remove_extent X[n]
否则
X
T
remove_all_extents X[n1][n2]…
否则
X
T

根据上表:每个类型模板的工作方式都与std::remove_reference类似,每个模板都一个名为type的public成员,表示一个类型。简单总结来说,如果T是一个之类类型,则std::remove_pointer<T>::type是T指向的类型,如果T不是一个指针,则不会进行任何转换,从而type具有与T相同的类型。


==特别特别重要==,对于函数传递时,类似func(123)是错的,num=123;func(num);又是可以的一个说明:

// 1:绑定非const右值
void f1(int &&index) {
	std::cout << index << std::endl;
}
// 2:左值和const右值
void f2(const int &index) {
	std::cout << index << std::endl;
}
// 以下的说明非常重要
void test1() {
	f1(123);  // 可以的,函数参数是非const右值
	f2(456);  // 可以的,函数参数是左值和const右值

	int i = 789;
	//f1(i);   // 错误,无法将右值引用绑定到左值
	f1(std::move(i));  // 这样就可以了

	// 如果f2函数中没有const,直接f2(456)也是错误的。
}

5.0.3 std::move的理解

==std::move的理解==:

​ 来理解一下std::move是如何定义的(就是下面my_move函数):move的函数参数T&&是一个指向模板类型参数的右值引用,通过==引用折叠==(我的理解是参看上面的表的内容),此参数可以与任何类型的实参匹配,特别是,既可以传递给move一个左值,也可以传递给它一个右值。

#include <type_traits>     // 书上说remove_reference需要这个头文件,vs中没加好像也行
template <typename T>
typename std::remove_reference<T>::type&& my_move(T&& t) {
	return static_cast<typename std::remove_reference<T>::type&&>(t);
}   // 上面的 std::remove_reference<T>::type 可以用 std::remove_reference_t<T> 代替,注意这种 _t的方式是c++14后才有的特性
void test1() {
	std::string s1("hi"), s2;
	s2 = my_move(std::string("ok!"));   // 正确,从一个右值移动数据
	s2 = my_move(s1);    // 正确:但在赋值后,s1的值是不确定的
}

再次说明,这里的my_move,就是std::move的实现,我为了作区分,文字里写的move就是上面的my_move,也代表std::move

那么开始std::move是如何工作的:

说明:从一个左值 static_cast 到一个右值是允许的。通常情况下,static_cast只能用于其它合法的类型转换,但有一条针对右值引用的特许规则:==虽然不能隐式地将一个左值转换为右值引用,但可以用static_cast显示地讲一个左值转换为一个右值引用==。


==std::forward==:

这一小节再写一句:很多时候参数传递,特别是函数代码中有其他函数的引用,参数传递进去后一些细节会被改变,这时候就要用std::forward,它会保持实参类型的所有细节:

template <typename F, typename T1, typename T2>
void flip(F, f, T1 &&t1, T2 &&t2) {
    f(std::forward<T2>(t2), std::forward<T1>(t1));
}

如果我们用flip(g, i, 42),i将以int&类型传递给g,42将以int&&类型传递给g(如果不用std::forward,引用这些会有改变,就不细写了)


5.0.4 可变参数模板(==…==)以及==sizeof…== 运算符

==可变参数模板(使用…)==:

补充:( sizeof… 运算符

当需要知道包中有多少元素时,可以使用 sizeof…运算符,类似sizeof,然后sizeof…也返回一个常量表达式,且不会对其实参求值:)

​ 一个==可变参数模板==就是一个接受不可变数目参数的模板函数或模板类。其中可变数目的参数被称为==参数包==,存在两种参数包:==模板参数包==,表示零个或多个模板参数;==函数参数包==表示零个或多个函数参数。

​ 用一个省略号来指出一个模板参数或函数参数表示一个包;在一个模板参数列表中==class…==或==typename…==指出接下来的参数表示零个或多个类型的列表;一个类型名后面跟一个省略号表示零个或多个给定类型的非类型参数的列表。在函数参数列表中,如果一个参数的类型是一个模板参数包,则此参数也是一个函数参数包。

template <typename T, typename... Args>
void foo(const T &t, const Args& ... rest) {
	std::cout << sizeof...(Args) << std::endl;   // 类型参数的数目
	std::cout << sizeof...(rest) << std::endl;   // 函数参数的数目
}
int i = 0; double d = 3.14; std::string s = "hello!";
foo(i, s, 42, d);    // 包中有3个参数
foo(s, 42, "hi");    // 包中有2个参数
foo(d, s);           // 包中有1个参数
foo("hi");           // 空包

编译器会为foo实例化出四个不同的版本。

​ 编写可变参数函数模板:

​ 可变参数函数通常是递归的,第一步调用处理包中的第一个实参,然后剩余实参调用自身。可以定义print函数为这样的模式,每次递归调用将第二个实参打印到第一个实参表示的流中,为了终止递归,一定还要定义一个非可变参数的print函数,它接收一个流和一个对象:

// print版本一:
template <typename T>
std::ostream& print(std::ostream &os, const T &t) {
	return os << t;   // 包中最后一个元素之后不打印分隔符
}
// print版本二:
template <typename T, typename... Args>
std::ostream& print(std::ostream &os, const T &t, const Args&... rest) {
	os << t << ", ";
	return print(os, rest...);
}
void test01() {
    int i = 123; double d = 3.14; std::string s = "hello!";
    print(std::cout, i, s, 42);
}

调用顺序: t rest

注意:一定要有版本一,当定义可变版本的print时,非可变参数版本的声明必须在作用域中,否则可变参数版本会无线递归,会报错的。

5.0.5 实现shared_ptr和unique_ptr

下面是练习的解答(我没自己写):编写你自己版本的shared_ptr和unique_ptr

shared_ptr

#pragma once
#include <functional>
#include "delete.h"

namespace cp5
{
	template<typename T>
	class SharedPointer;

	template<typename T>
	auto swap(SharedPointer<T>& lhs, SharedPointer<T>& rhs)
	{
		using std::swap;
		swap(lhs.ptr, rhs.ptr);
		swap(lhs.ref_count, rhs.ref_count);
		swap(lhs.deleter, rhs.deleter);
	}

	template<typename T>
	class SharedPointer
	{
	public:
		//
		//  Default Ctor
		//
		SharedPointer()
			: ptr{ nullptr }, ref_count{ new std::size_t(1) }, deleter{ cp5::Delete{} }
		{}
		//
		//  Ctor that takes raw pointer
		//
		explicit SharedPointer(T* raw_ptr)
			: ptr{ raw_ptr }, ref_count{ new std::size_t(1) }, deleter{ cp5::Delete{} }
		{}
		//
		//  Copy Ctor
		//
		SharedPointer(SharedPointer const& other)
			: ptr{ other.ptr }, ref_count{ other.ref_count }, deleter{ other.deleter }
		{
			++*ref_count;
		}
		//
		//  Move Ctor
		//
		SharedPointer(SharedPointer && other) noexcept
			: ptr{ other.ptr }, ref_count{ other.ref_count }, deleter{ std::move(other.deleter) }
		{
			other.ptr = nullptr;
			other.ref_count = nullptr;
		}
		//
		//  Copy assignment
		//
		SharedPointer& operator=(SharedPointer const& rhs)
		{
			//increment first to ensure safty for self-assignment
			++*rhs.ref_count;
			decrement_and_destroy();
			ptr = rhs.ptr, ref_count = rhs.ref_count, deleter = rhs.deleter;
			return *this;
		}
		//
		//  Move assignment
		//
		SharedPointer& operator=(SharedPointer && rhs) noexcept
		{
			cp5::swap(*this, rhs);
			rhs.decrement_and_destroy();
			return *this;
		}
		//
		//  Conversion operator
		//
		operator bool() const
		{
			return ptr ? true : false;
		}
		//
		//  Dereference
		//
		T& operator* () const
		{
			return *ptr;
		}
		//
		//  Arrow
		//
		T* operator->() const
		{
			return &*ptr;
		}
		//
		//  Use count
		//
		auto use_count() const
		{
			return *ref_count;
		}
		//
		//  Get underlying pointer
		//
		auto get() const
		{
			return ptr;
		}
		//
		//  Check if the unique user
		//
		auto unique() const
		{
			return 1 == *refCount;
		}
		//
		//  Swap
		//
		auto swap(SharedPointer& rhs)
		{
			::swap(*this, rhs);
		}
		//
		// Free the object pointed to, if unique
		//
		auto reset()
		{
			decrement_and_destroy();
		}
		//
		// Reset with the new raw pointer
		//
		auto reset(T* pointer)
		{
			if (ptr != pointer)
			{
				decrement_n_destroy();
				ptr = pointer;
				ref_count = new std::size_t(1);
			}
		}
		//
		//  Reset with raw pointer and deleter
		//
		auto reset(T *pointer, const std::function<void(T*)>& d)
		{
			reset(pointer);
			deleter = d;
		}
		//
		//  Dtor
		//
		~SharedPointer()
		{
			decrement_and_destroy();
		}
	private:
		T* ptr;
		std::size_t* ref_count;
		std::function<void(T*)> deleter;

		auto decrement_and_destroy()
		{
			if (ptr && 0 == --*ref_count)
				delete ref_count,
				deleter(ptr);
			else if (!ptr)
				delete ref_count;
			ref_count = nullptr;
			ptr = nullptr;
		}
	};
}//namespace

unique_ptr:

#include "debugDelete.h"

// forward declarations for friendship

template<typename, typename> class unique_pointer;
template<typename T, typename D> void
swap(unique_pointer<T, D>& lhs, unique_pointer<T, D>& rhs);

/**
*  @brief  std::unique_ptr like class template.
*/
template <typename T, typename D = DebugDelete>
class unique_pointer
{
	friend void swap<T, D>(unique_pointer<T, D>& lhs, unique_pointer<T, D>& rhs);

public:
	// preventing copy and assignment
	unique_pointer(const unique_pointer&) = delete;
	unique_pointer& operator = (const unique_pointer&) = delete;

	// default constructor and one taking T*
	unique_pointer() = default;
	explicit unique_pointer(T* up) : ptr(up) {}

	// move constructor
	unique_pointer(unique_pointer&& up) noexcept
		: ptr(up.ptr) { up.ptr = nullptr; }

	// move assignment
	unique_pointer& operator =(unique_pointer&& rhs) noexcept;

	// std::nullptr_t assignment
	unique_pointer& operator =(std::nullptr_t n) noexcept;



	// operator overloaded :  *  ->  bool
	T& operator  *() const { return *ptr; }
	T* operator ->() const { return &this->operator *(); }
	operator bool() const { return ptr ? true : false; }

	// return the underlying pointer
	T* get() const noexcept{ return ptr; }

	// swap member using swap friend
	void swap(unique_pointer<T, D> &rhs) { ::swap(*this, rhs); }

	// free and make it point to nullptr or to p's pointee.
	void reset()     noexcept{ deleter(ptr); ptr = nullptr; }
	void reset(T* p) noexcept{ deleter(ptr); ptr = p; }

	// return ptr and make ptr point to nullptr.
	T* release();


	~unique_pointer()
	{
		deleter(ptr);
	}
private:
	T* ptr = nullptr;
	D  deleter = D();
};


// swap
template<typename T, typename D>
inline void
swap(unique_pointer<T, D>& lhs, unique_pointer<T, D>& rhs)
{
	using std::swap;
	swap(lhs.ptr, rhs.ptr);
	swap(lhs.deleter, rhs.deleter);
}

// move assignment
template<typename T, typename D>
inline unique_pointer<T, D>&
unique_pointer<T, D>::operator =(unique_pointer&& rhs) noexcept
{
	// prevent self-assignment
	if (this->ptr != rhs.ptr)
	{
		deleter(ptr);
		ptr = nullptr;
		::swap(*this, rhs);
	}
	return *this;
}


// std::nullptr_t assignment
template<typename T, typename D>
inline unique_pointer<T, D>&
unique_pointer<T, D>::operator =(std::nullptr_t n) noexcept
{
	if (n == nullptr)
	{
		deleter(ptr);   ptr = nullptr;
	}
	return *this;
}

// relinquish contrul by returnning ptr and making ptr point to nullptr.
template<typename T, typename D>
inline T*
unique_pointer<T, D>::release()
{
	T* ret = ptr;
	ptr = nullptr;
	return ret;
}

5.1 函数模板

5.1.1 函数模板语法

​ 函数模板作用:

​ 建立一个通用函数,其函数返回值类型和形参类型可以不具体指定,用一个==虚拟的类型==来代表。

语法:

template<typename T>
函数声明或定义       // 注意两个是紧挨着写的

demo:

// 模板就理解为把数据类型的地方换成了`T`,
// 函数要是有返回值,void也可以换成`T`
template<typename T>
void myswap(T &a, T &b) {
	T temp = a;
	a = b;
	b = temp;
}
// 两种模板使用方式
void test01() {
	int x = 10, y = 20;
	// 1、自动推到类型
	//myswap(x, y);  // 我的理解是都是int,可以直接推导出来

	// 2、显示指定类型(就中间加个类型)
	myswap<int>(x, y);  // 要是x、y是别的类型,就类似加<float>
	std::cout << "x:" << x << std::endl;
	std::cout << "y:" << y << std::endl;

	float e = 3.14f, f = 5.23f;
	myswap<float>(e, f);
	std::cout << "e:" << e << std::endl;
	std::cout << "f:" << f << std::endl;
}

总结:

5.1.2 函数模板注意事项

demo:

template<typename T>
void myswap(T &a, T &b) {
	T temp = a;
	a = b;
	b = temp;
}
void test01() {
	int x = 10, y = 20;
	char z = 'c';
	// 针对自动类型推导
	// 错的,x,z的数据类型推断出来不一致,不行
	//myswap(x, z);  
    myswap(x, y);  // 可以,能推导出一致的T
}
// 虽然是模板函数,也可以是没有参数的
template<typename T>
void func() {
	cout << "这是模板函数" << endl;
}
void test02() {
	// 若不是模板,只是普通函数就没问题
	//func();  // 调用失败,没有参数无法自动推导,又没有手动指定
	func<int>();  // 随便指定都行,float也可以,显示指定类型的方式,一定要给T一个类型
}

5.1.3 函数模板demo

​ 利用函数模板封装一个可以对不同数据类型数组从小到大排序的函数,再用char数组和int数组进行测试。

// 排序模板
template<typename T>
void mysort(T array[], int len) {   
	// 特别注意:传数组名作为参数,不能有引用符号,直接传进来的就是数组的地址了;;然后数组作为参数传进来要带`[]`,千万别忘了
	for (int i = 0; i < len; i++) {
		for (int j = i + 1; j < len - 1; j++) {
			if (array[i] > array[j]) {
				T temp = array[i];
				array[i] = array[j];
				array[j] = temp;
			}
		}
	}
}
// 打印模板
template<typename T>
void myprint(T array[], int len) {
	for (int i = 0; i < len; i++) {
		cout << array[i] << ' ';
	}
	cout << endl;
}
void test01() {
	int arr1[] = { 5, 6, 1, 3, 7, 9, 4, 2, 8 };
	int len1 = sizeof(arr1) / sizeof(arr1[0]);

	char arr2[] = "kjhgfds";
	int len2 = sizeof(arr2) / sizeof(char);  // 注意这要减去一个或是不减去,下面打印输出都一样

	mysort(arr1, len1);
	mysort(arr2, len2);  // 排序模板
	
	myprint(arr1, len1);
	myprint(arr2, len2);  // 打印模板
}

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

主要区别:

demo:

template<typename T>
//T myadd(T a, T &b) {  // 这在第3个方法处是要报错的,不能引用
T myadd(T a, T b) {  // 测试时,这不能传引用,特别是要传字符的参数
	return a + b;
}
int fun_add(int a, int b) {
	return a + b;
}
void test01() {
	int x = 10, y = 20;
	char z = 'a';
	// 1、普通函数(这里`a`转成了97)
	cout << fun_add(x, z) << endl;  // 107

	// 2、自动类型推导(这是错的,不知道该怎么转)
	//cout << myadd(x, z) << endl;

	// 3、显示指定类型(指定了类型,就可以隐式转换)
	cout << myadd<int>(x, z) << endl;
}

​ 总结:建议就直接使用显示指定类型的方式,调用函数模板,直接就自己确定了通用类型T,不去推断。

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

调用规则如下:

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

demo:

void func(int a, int b) {
	cout << "这是普通函数" << endl;
}
template<typename T>
void func(T a, T b) {
	cout << "这是函数模板的调用" << endl;
}
template<typename T>
void func(T a, T b, T c) {  // 函数模板重载
	cout << "这是重载的函数模板的调用" << endl;
}
void test01() {
	int x = 10, y = 20;
	// 1、都可以时,就会调用普通函数
	func(x, y);
	// 2、通过空参数列表强制调用函数模板
	func<>(x, y);
	// 3、函数模板重载
	func(x, y, 100);

	// 4、普通函数、函数模板都是可以的,但是普通函数还要做一次隐式转换,所以直接使用函数模板更优
	char e = 'a', f = 'b';
	func(e, f);
}

总结:既然提供了函数模板,最好就不要提供普通函数,否则容易出现二义性。

5.1.6 具体化的模板

​ 模板更多的是对内置数据类型的通用,可是要是是自定义的数据类型,一般就很难处理了,因此c++提供模板的重载,为这些==特定类型==提供==具体化的模板==。

​ 具体化的模板大概是这样,以 template <> 开头: template<> bool compare(Person &p1, Person &p2) {}

demo:在inicpp这个项目里就用到了很多具体化的模板。

class Person {
public:
	Person(std::string name, int age):m_Name(name), m_Age(age) {}
	std::string m_Name;
	int m_Age;
};
template<typename T>
bool compare(T &a, T &b) {
	if (a == b) {
		return true;
	}
	else {
		return false;
	}
}
// 具体化,显示具体化的原型,定义以`template<>`开头,并通过名称来指出类型
// 具体化优先于常规模板
template<> 
bool compare(Person &p1, Person &p2) {
// 这行写作 bool compare<Person>(Person &p1, Person &p2) {  也是可以的,上面的inicpp炫目就是大量用了这种具体化模板的写法
	if (p1.m_Age == p2.m_Age && p1.m_Name == p2.m_Name) {
		return true;
	}
	else {
		return false;
	}
}
void test01() {
	int x = 10, y = 10;
	int ret1 = compare(x, y);  // true
	std::cout << ret1 << std::endl;

	// 这个类型不行,就要用具体化的原型
	Person p1("zhaoliu", 30), p2("zhaoliu", 30);
	int ret2 = compare(p1, p2);
	std::cout << ret2 << std::endl;
}

总结:

5.2 类模板

使用类模板,必须显示指定传入的数据类型

5.2.1 类模板语法

​ 类模板作用:建立一个通用类,类中的成员 数据类型不具体制定,用一个==虚拟的类型==来代表。

语法:

template<class T>   // class也可以是typename

demo:

​ 这个例子不知道为什么,要么是第3行跟第7行这个组合;要么是第2行跟第6行这个组合,==顺序要一致==,不然第16行总是报错。

// 需要两个类型,就整两个,后面的名字自己起就好
//template<class NameType, class AgeType>   //这行
template<class AgeType, class NameType>
class Person {
public:
	//Person(NameType name, AgeType age) {  // 这行
	Person(AgeType age, NameType name) {
		this->m_Name = name;
		this->m_Age = age;
	}
	AgeType m_Age;
	NameType m_Name;
};
void test01() {
	// 使用类模板,必须显示指定传入的数据类型
	Person<string, int>p1("孙悟空", 9999);
}

5.2.2 类模板与函数模板的区别

主要区别:

template<class T1, class T2 = int>  // 可以制定一个默认类型
class Person {
public:
	Person(T1 name, T2 age) {
		this->m_Name = name;
		this->m_Age = age;
	}
	T1 m_Name;
	T2 m_Age;
};
void test01() {
	//Person p("张三", 18); // 这是错的,是没有自动推导的

	Person<string>p1("张三", 18);  // 有了默认类型,就可以少给一个
	Person<string, float>p2("王五", 18.5);  // 当然也可以给其它的类型
}

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

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

理解:写类模板时,你可以写很多成员函数,编译是不会出错的,因为传进来的数据类型不确定(可以是自己写的类),是都可能的,就不会在编译时创建(我理解的),所以是在调用时(即 .函数名())才创建。

5.2.4 类模板对象做函数参数

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

  1. 指定传入的类型 — 直接显示对象的数据类型
  2. 参数模板化 — 将对象中的参数变为模板进行传递
  3. 整个类模板化 — 将这个对象类型 模板化进行传递
// 1、第一种,指定传入的类型
void myprint1(Person<string, int> &person) {
	cout << person.m_Name << "  " << person.m_Age << endl;
}
void test01() {
	Person<string, int>p1("孙悟空", 1000);
	myprint1(p1);
}

// 2、第二种,参数模板化
template<class T1, typename T2>  // 比起第一种就是加了这行,把参数模板化了
void myprint2(Person<T1, T2> &person) {
	cout << person.m_Name << "  " << person.m_Age << endl;
}
void test02() {
	Person<string, int>p2("猪八戒", 800);
	myprint2(p2);
}

// 3、第三种,整个类模板化
template<class T>
void myprint3(T &person) {   // 直接整个都是全部自己推导
	cout << person.m_Name << "  " << person.m_Age << endl;
	cout << typeid(person).name() << endl;
}
void test03() {
	Person<string, int>p3("猪八戒", 800);
	myprint3(p3);
}

总结:比较广泛使用的是==第一种==,即指定传入的类型,比较直接,后面两种都是类模板和函数模板的联合使用了,不够简洁。

5.2.5 类模板与继承

当类模板碰到继承时,需要注意以下几点:

template<class T>
class Base {
	T name;
};
//class Son : Base {  // 这就是错的,没指定类型
class Son : Base<int> {  // 必须要指定一个类型
	int age;
};

或者:

template<class T>
class Base {
	T name;
};
// 子类也写成模板,这样就能灵活指定父类的数据类型了
template<class T1, class T2>
class Son : Base<T2> {
	T1 age;
};

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

template<class T1, class T2>
class Person {
public:
	Person(T1 name, T2 age);
	void showPerson();
	T1 m_Name;
	T2 m_Age;
};
// 开始类外实现
// 必须要这行template,不然编译器就不认识这T1、T2
template<class T1, class T2>
Person<T1, T2>::Person(T1 name, T2 age) {
	this->m_Name = name;
	this->m_Age = age;
}
// 类外实现不但要有作用域,还一定要在作用域后跟上模板参数列表,不然就会报错(不加的话就跟普通的类外实现一样了)
template<class T1, class T2>
void Person<T1, T2>::showPerson() {
	cout << "这就是类外实现" << endl;
}

5.2.7 类模板分文件编写

问题:

解决:

==person.h==

#pragma once
#include <iostream>
#include <string>
template<class T1, class T2>
class Person {
public:
	Person(T1 name, T2 age);
	void showPerson();
	T1 m_Name;
	T2 m_Age;
};

==person.cpp==

#include "person.h"
using namespace std;
// 开始类外实现
// 必须要这行template,不然编译器就不认识这T1、T2
template<class T1, class T2>
Person<T1, T2>::Person(T1 name, T2 age) {
	this->m_Name = name;
	this->m_Age = age;
}
template<class T1, class T2>
void Person<T1, T2>::showPerson() {
	cout << "这就是类外实现" << this->m_Name << endl;
}

主程序入口:

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

#include "person.h"
// #include "person.cpp"  // 第一种解决办法:直接导入源码文件
void test01() {
	Person<string, int>p("孙悟空", 1000);
	p.showPerson();
}// 这里就会报连接错误,这有两行就有两个外部链接无法解析;

​ 错误解析:因为编译器在看Peron.h时,Person是一个类模板就不会创建,声明都没看,就更不会看到person.cpp源码中的实现了,等到这主程序中第7、8行开始调用时,根本就没有,所以无法解析。

​ 解决:最后还是把类模板的声明实现写到一个文件里,并将后缀名改为.hpp

5.2.8 类模板与友员

目标:掌握类模板配合友元函数的类内和类外的实现

全局函数类内实现:

template<class T1, class T2>
class Person {
	// 注意这是全局函数,并不是成员函数(去回顾友员);这是声明实现一起写了,这里面不能用this指针这种直接去访问成员属性的
	friend void showPerson(Person<T1, T2> &person) {   // 传进来的Person类型也是推导的
		cout << "这是全局函数类内实现:" << person.m_Name << endl;
	}
public:
	Person(T1 name, T2 age) {
		this->m_Name = name;
		this->m_Age = age;
	}
private:
	T1 m_Name;
	T2 m_Age;
};
void test01() {
	Person<string, int>p1("孙悟空", 1000);
	showPerson(p1);  // 这是全局函数,不是成员函数,不能 p1.showPerson()的
}

全局函数类外实现:

template<class T1, class T2>
class Person;  // 要先声明,上面这行也一定不能少

template<class T1, class T2>  // 提前让知道这个函数的存在(也可以把实现直接写到这里)
void showPerson(Person<T1, T2> &person);  // 这里用到了Person类模板,所以上面第2行也要先声明

template<class T1, class T2>
class Person {
	// 下面这行是错的,因为这是普通函数的声明,而下面的实现又是模板的
	//friend void showPerson(Person<T1, T2> &person);
	friend void showPerson<>(Person<T1, T2> &person);  // 所以一定要加空的模板参数列表
	// 这里还要注意一点,全局函数类外实现,需要让编译器提前知道这个函数的存在
public:
	Person(T1 name, T2 age) {
		this->m_Name = name;
		this->m_Age = age;
	}
private:
	T1 m_Name;
	T2 m_Age;
};
// 下面是类外实现
template<class T1, class T2>
void showPerson(Person<T1, T2> &person) {
	cout << "这是全局函数--内外实现:" << person.m_Age << endl;
}

void test01() {
	Person<string, int>p1("孙悟空", 1000);
	showPerson(p1); 
}

5.2.9 类模板案例

描述:实现一个通用的数组类,要求如下:

array.hpp文件:一开始直接写模板不出来,于是先写的int类型,再把对应位置的int改成模板T

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

template<class T>
class Array {
public:
	int m_len;  // 数组容量
	int m_record;  // 记录现在数组中有的元素个数
	T ** m_Array;  // 定义数组,数组中存放的是指针比较好,这样也能放自定义数据
	// 普通有参构造函数,来确定整个数组长度
	Array(int len) {
		this->m_record = 0;  // 初始化为0
		this->m_len = len;
		m_Array = new T*[len];
	}
	// 自定义拷贝构造函数(数组中的数据都是new的,必须得深拷贝)
	Array(const Array &arr) {
		this->m_len = arr.m_len;
		this->m_record = arr.m_record;
		// 深拷贝主要是这,自己开辟新的空间,然后把传进来的值赋值
		this->m_Array = new T*[m_len];
		for (int i = 0; i < arr.m_record; i++) {
			m_Array[i] = new T(*arr.m_Array[i]);
		}
	}
	// 析构函数
	~Array() {
		// 先释放数组中的数据
		for (int i = 0; i < this->m_record; i++) {
			if (this->m_Array[i] != NULL) {
				delete m_Array[i];
				m_Array[i] = NULL;
			}
		}
		// 再释放整个数组
		if (this->m_Array != NULL) {
			delete[] m_Array;
			m_Array = NULL;
		}
	}
	// 重载`=`这一赋值符号,使其为深拷贝
	Array<T>& operator=(const Array<T> &arr) {  // 加const防止被修改
		// 因为有初始化,所有要判一下(这里的逻辑,应该都是不会为空)
		if (this->m_Array != NULL) {
			delete[] this->m_Array;
			m_Array = NULL;   // 先所有属性清掉再全部赋值
			this->m_len = 0;
			this->m_record = 0;
		}
		// 开辟一个数组放指针,类型就是T*
		this->m_Array = new T*[arr.m_len];  
		for (int i = 0; i < arr.m_record; i++) {
			this->m_Array[i] = new T(*arr.m_Array[i]);
		}
		this->m_record = arr.m_record;
		this->m_len = arr.m_len;
		return *this;  // 返回本身,好后面可以使用链式编程,a=b=c这种
	}
	// 用[]去取值,重载了,尽量都是返回引用
	T& operator[](int index) {
		return *this->m_Array[index];  // 数据是指针,解引用先
	}

	// 在末尾添加数据
	void add_Num(T a_num) {
		if (this->m_record == this->m_len) {
			cout << "数组已满,无法添加" << endl;
			return;
		}
		m_Array[this->m_record] = new T(a_num);
		this->m_record++;
	}
	// 删除末尾的数据
	void sub_Num() {
		if (this->m_record == 0) {
			cout << "数组已经为空了,不能再删除" << endl;
			return;
		}
		delete this->m_Array[this->m_record - 1];
		this->m_Array[this->m_record - 1] = NULL;
		this->m_record--;
	}
	void showInfo() {
		cout << "当前数组元素个数:" << this->m_record << endl;
		cout << "当前数组的容量:" << this->m_len << endl;
	}
};

随便一个.cpp

#include <iostream>
#include <string>
#include "array.hpp"  //导入模板类 
using namespace std;

struct Person {  // 自定义数据类型
	string m_Name;
	int m_Age;
};
// 【1】
void test01() {
	// (1)使用类模板时都要给定模板参数列表
	Array<float> arr1(5);
	arr1.add_Num(3.14);
	arr1.add_Num(4.15);
	arr1.add_Num(5.16);
	// (2)测试重写的构造函数
	Array<float> arr2(arr1);  //Array<float> arr2 = arr1;  // 一样的

	// (3)测试重载的operator=(重载成深拷贝,系统是浅拷贝)
	Array<float> arr5(100);  // 初始化了一个100的(后面赋值会把这清掉重来,随便给的值)
	arr5 = arr2;  // 不重载就是浅拷贝,就会出错
	for (int i = 0; i < arr5.m_record; i++) {
		cout << *arr5.m_Array[i] << endl;
	}
	arr2.showInfo();
	// (4)测试重载 `[]`
	cout << arr5[1] << endl;  // 4.15
}

// 注意这里打印时要指定这个类模板的数据类型+参数列表(要打印那个,就把那个实例化前面的数据类型弄过来)
void MyPrint(Array<Person> & arr) {
	for (int i = 0; i < arr.m_record; i++) {  // 注意下面两种不同的打印,后面这解引用要括起来
		cout << "姓名:" << arr.m_Array[i]->m_Name << " 年龄:" << (*arr.m_Array[i]).m_Age << endl;
	}
}
// 【2】这里就是测试类模板添加自定义数据类型
void test02() {
	Array<Person> arr3(6);
	// 可以这样创建结构体指针的初始化
	//Person *p1 = new Person({ "孙悟空", 1000 });
	Person p1 = { "孙悟空1", 1000 };
	Person p2 = { "猪八戒2", 800 };
	struct Person p3 = { "沙和尚3", 500 };
	arr3.add_Num(p1);
	arr3.add_Num(p2);
	arr3.add_Num(p3);
	MyPrint(arr3);  // 函数的定义一定要在这个前面,没有实现都要有声明的

	// [重要]测试重载的`[]`
	cout << arr3[1].m_Name << endl;
	// 要是没重载,这里必须得是 arr3.m_m_Array[1].m_Name
}

// 【3】下面仅仅是为了复习数组的两种不同的打印
void test03() {
	struct Person persons[3] = { {"孙悟空", 1000}, {"猪八戒", 800}, {"沙和尚", 500} };
	// 方法1: 指针在结构体中的使用来遍历
	Person *p = persons;
	for (int i = 0; i < 3; i++) {
		cout << p++->m_Name << endl;
	}
	// 方法2: 这种方式来遍历数组
	for (Person &k : persons) {
		cout << k.m_Name << endl;
	}
}