C++基础学习

目录

 

字符串

switch

随机数

数组

一维数组

二维数组

函数

函数

函数默认参数

函数占位参数

函数重载

指针

指针

空指针

野指针

const修饰指针

指针和数组

指针和函数

结构体

结构体

结构体数组

结构体指针

结构体嵌套结构体

结构体做函数参数

结构体中const使用

内存分区模型

内存分区模型

程序运行前

代码区

全局区

程序运行后

栈区

堆区

new操作符

delete操作符

引用

引用

引用做函数参数

引用做函数返回值

常量引用

类和对象

封装

struct和class的区别

对象的初始化和清理

构造函数和析构函数

构造函数的分类及调用

深拷贝和浅拷贝

初始化列表

类对象作为类成员

静态成员

对象模型

this指针

空指针访问成员函数

const修饰成员函数

友元

全局函数做友元

类做友元

成员函数做友元

运算符重载

加号运算符重载

左移运算符重载

递增运算符重载

赋值运算符重载

关系运算符重载

函数调用运算符重载

继承

继承中构造函数和析构函数顺序

继承同名成员处理方式

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

多继承

菱形继承

多态

纯虚函数和抽象类

虚析构和纯虚析构

文件操作

文本文件

写文件

读文件

二进制文件

写文件

读文件

模板

函数模板

类模板

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

类模板与函数模板区别

类模板与继承

类模板成员函数类外实现

类模板分文件编写

类模板与友元


字符串

C格式:char 变量名[] = "字符串值";

C++格式:string 变量名 = "字符串值"; 需加入头文件#include <string>

前置递增/递减,先运算后赋值。

后置递增/递减,先赋值后运算。

switch

switch中表达式类型只能是整型或字符型。

case中没有break,语句会一直向下执行。

switch语句虽然结构清晰,但是不能判断区间。

随机数

伪随机数:rand() % 100表示生成0~99的随机数42,但是每次生成的随机数都一样。

添加随机数种子,利用当前系统时间生成随机数,防止每次随机数都一样,添加头文件<ctime>

随机数种子不放在循环中

#include <ctime>
srand((unsigned int)time(NULL));
int val = rand % 100;

数组

数组:存放相同类型数据元素的一个集合。

数组是由连续的内存位置组成的。

一维数组

三种定义方式:

  1. 数据类型 数组名[数组长度];
  2. 数据类型 数组名[数组长度] = {值1, 值2 ...};
  3. 数据类型 数组名[] = {值1, 值2 ...};

如果在初始化数据时,值的长度小于数组长度,会用0填充剩余数据部分。

两种数组名的用途:

1.通过数组名获取数组占用内存空间大小。

整个数组占内存空间大小:sizeof(arr_name)

单个元素占内存空间大小:sizeof(arr_name[0])

数组元素个数:sizeof(arr_name)/sizeof(arr_name[0])

2.通过数组名获取数组在内存中首地址。

数组首地址:(int)arr_name

数组中第i个元素地址:(int)&arr_name[i]

数组名是常量,不可以赋值。

二维数组

四种定义方式:

  1. 数据类型 数组名[行数][列数];
  2. 数据类型 数组名[行数][列数] = {{数据1, 数据2}, {数据3, 数据4}};
  3. 数据类型 数组名[行数][列数] = {数据1, 数据2, 数据3, 数据4};
  4. 数据类型 数组名[][列数] = {数据1, 数据2, 数据3, 数据4};

两种数组名的用途:

1.获取二维数组占内存空间大小:sizeof(arr_name)

第i行占内存空间大小:sizeof(arr_name[i])

第j列占内存空间大小:sizeof(arr_name[j])

某个元素占内存空间大小:sizeof(arr_name[i][j])

二维数组行数:sizeof(arr_name) / sizeof(arr_name[0])

二维数组列数:sizeof(arr_name[0]) / sizeof(arr_name[0][0])

2.获取二维数组首地址:(int)arr_name

第一行首地址:(int)arr_name[0]

第二行首地址:(int)arr_name[1]

第一个元素首地址:(int)&arr_name[0][0]

第二个元素首地址:(int)&arr_name[0][1]

函数

函数

组成:1.返回值类型2.函数名3.参数列表4.函数体语句5.return表达式

值传递的时候,形参发生任何改变,不会影响实参。

声明的作用:提前告诉编译器函数的存在,可以多次声明函数,但只能定义一次函数。

函数默认参数

如果某个位置有了默认参数,那么从这个位置往后,从左到右都必须有默认值。

如果函数声明有默认参数,那么函数定义不能有默认参数。如果函数定义有默认参数,那么函数声明不能有默认参数。函数声明和定义只能有一个默认参数。

函数占位参数

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

占位参数可以有默认参数。如 void func(int  = 10){}

函数重载

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

函数重载满足条件:

  1. 同一个作用域下
  2. 函数名称相同
  3. 函数参数的类型/个数/顺序不同

注意:

  1. 引用作为重载条件时,使用const区分引用和常量引用。
  2. 重载有默认参数时,出现二义性,报错,应避免这种情况。
//引用作为重载条件时
void func1(int &a)
{
	cout << "func1 (int &a) 调用" << endl;
}
 
void func1(const int &a)
{
	cout << "func1 (const int &a) 调用" << endl;
}
 
//重载有默认参数时
void func2(int a, int b = 10)
{
	cout << "func2 (int a, int b = 10) 调用" << endl;
}
 
void func2(int)
{
	cout << "func2 (int) 调用" << endl;
}
 
int main()
{
	int a = 10;
	func1(a); //输出 func1 (int &a) 调用
	func1(10); //输出 func1 (const int &a) 调用
	func2(a); //重载有默认参数,出现二义性,报错,应避免这种情况
 
 
	return 0;
}

指针

指针

指针是一个变量,值为另一个变量的地址,即指针就是一个地址。

指针的定义:数据类型 * 指针变量名;

指针的赋值:指针变量名 = &指针指向的变量;

指针的使用:指针前加 * 表示解引用,找到指针指向的内存中的数据。

指针所占用的内存空间大小:任何数据类型的指针,在32位系统占用4字节,在64位系统占用8字节。

int main()
{ 
	int a = 10;
	//int *p = &a;
        int *p;
        p = &a;
 
	cout << "a的地址" << &a << endl;      //输出a的地址
        cout << "指针p为:" << p << endl;     //输出a的地址,和&a结果相同
        cout << "a的值为:" << a << endl;     //输出10
        cout << "解引用*p为:" << *p << endl; //输出10
	return 0;
}

空指针

指针变量指向内存中编号为0的空间。空指针用来初始化指针变量,如果指针一开始不知道指向哪里合适,就使用空指针。空指针指向的内存是不可访问的,因为0~255之间的内存编号是系统占用的,所以不可以访问。

int * p = NULL;

野指针

指针变量指向非法的内存空间。

const修饰指针

const修饰指针的三种情况:

1. const修饰指针 常量指针:指针的指向可以修改(可以指向另一个地址),指针指向的值不可以修改(*p不可以修改)。const int * p = &a;   常量(const)指针(*)

int main()
{ 
	int a = 10;
	int b = 20;
 
	const int *p = &a;
	cout << *p << endl;//输出10
	//*p = 20;   错误,指针指向的值不可以修改
	p = &b;    //正确,指针指向可以修改
	cout << *p << endl;//输出20
 
	return 0;
}

2. const修饰常量 指针常量:指针的指向不可以修改(不可以指向另一个地址),指针指向的值可以修改(*p可以修改)。int * const p = &a;  指针(*) 常量(const)

int main()
{ 
	int a = 10;
	int b = 20;
 
	int * const p = &a;
	cout << *p << endl;//输出10
 
	*p = 20;//正确,指针指向的值可以修改
	//p = &b;错误,指针指向不可以修改
 
	cout << *p << endl;//输出20
 
	return 0;
}

3. const既修饰指针,又修饰常量:指针的指向和指针指向的值都不可以修改。const int * const p = &a;

指针和数组

变量的地址用&获取,数组的首地址是数组名本身

利用指针偏移遍历访问数组中元素:

int main()
{ 
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
 
	int * p = arr; //arr为数组首地址
	cout << "用指针访问第一个元素:" << *p << endl; //输出1
 
	p++; //指针向后偏移4个字节
	cout << "用指针访问第二个元素:" << *p << endl; //输出2
 
	int * p2;
	for (int i = 0; i < 10; i++) { //用指针遍历数组输出12345678910
		cout << *p2 << endl;
		p2++;
	}
 
	return 0;
}

指针和函数

利用指针做为函数的参数,可以修改实参的值。

使用值传递,不能修改实参。使用地址传递,可以修改实参。

将函数中的形参改为指针,可以减少内存空间,不会复制新的副本出来。

将函数中的形参改为指针,可以减少内存空间,不会复制新的副本出来。

将函数中的形参改为指针,可以减少内存空间,不会复制新的副本出来。

void swap1(int a, int b)
{
	int temp = a;
	a = b;
	b = temp;
 
}
 
void swap2(int *p1, int *p2)
{
	int temp = *p1;
	*p1 = *p2;
	*p2 = temp;
}
 
int main()
{ 
	int a = 10;
	int b = 20;
 
	//1.值传递,不能修改实参
	swap1(a, b);
	cout << a << " " << b << endl; //输出10 20
 
	//2.地址传递,可以修改实参
	swap2(&a, &b);
	cout << a << " " << b << endl; //输出20 10
 
	return 0;
}

结构体

结构体

结构体可以存储不同的数据类型。

语法:struct 结构体名 {成员列表};

三种方法创建结构体变量:

  1. struct 结构体名 变量名;
  2. struct 结构体名 变量名 = {成员列表中属性对应的值};
  3. struct 结构体名 {成员列表}变量名;

定义结构体时,关键字struct不能省略。

创建结构体变量时,关键字struct可以省略。

通过"."点号访问结构体变量中的属性

结构体数组

将自定义的结构体放入到数组中方便维护。结构体数组放在main函数中。

结构体数组做参数传递给函数时,形参表示为:struct 结构体名 结构体数组名[]

语法:struct 结构体名 数组名[元素个数] = { { 成员列表中属性对应的值 }, { }, ... { } };

//定义结构体
struct Student {
	string name;
	int age;
	int score;
};
 
//遍历打印结构体数组
void printStudent(struct Student stuArray[], int len)
{
	for (int i = 0; i < 3; i++) {
		cout << "姓名:" << stuArray[i].name
			<< " 年龄:" << stuArray[i].age
			<< " 分数:" << stuArray[i].score << endl;
	}
}
 
int main()
{
	//创建结构体体数组
	struct Student stuArray[3] = {
		{"张三",18,100},
		{"李四", 16,80},
		{"王五", 14,60},
	};
 
	//给结构体数组中的元素赋值
	stuArray[2].name = "赵六";
	stuArray[2].age = 80;
	stuArray[2].score = 50;
 
	int len = sizeof(stuArray) / sizeof(stuArray[0]);
	printStudent(stuArray, len);
 
	return 0;
}

结构体指针

通过指针访问结构体中的成员,使用"->"访问结构体属性。

语法:

指针指向结构体变量:结构体名 * 指针变量名 = &结构体变量名;

指针访问结构体变量中数据:指针变量名->结构体属性名;

//定义结构体
struct Student {
	string name;
	int age;
	int score;
};
 
int main()
{
	//创建结构体变量
	struct Student s = { "张三",15, 100 };
 
	//通过指针指向结构体变量
	Student * p = &s;
 
	//通过指针访问结构体变量中的数据
	cout << "姓名:" << p->name << "年龄:" << 
		p->age << "分数:" << p->score << endl;
 
	return 0;
}

结构体嵌套结构体

在一个结构体中定义另一个结构体成员。

//定义学生结构体
struct Student {
	string name;
	int age;
	int score;
};
 
//定义老师结构体
struct teacher {
	int id;           //编号
	string name;      //姓名
	int age;          //年龄
	struct Student stu; //学生
};
 
int main()
{
	struct teacher t;
	t.id = 150;
	t.name = "老张";
	t.age = 40;
	t.stu.name = "张三";
	t.stu.age = 20;
	t.stu.score = 100;
 
	cout << "老师编号" << t.id << "老师姓名:" << t.name << 
		"老师年龄:" <<	t.age << "学生姓名:" << t.stu.name << 
		"学生年龄:" << t.stu.age << "学生分数:" << t.stu.score << endl;
 
	return 0;
}

结构体做函数参数

将结构体作为参数向函数中传递。传递方式:值传递/地址传递。

//打印学生信息函数
//值传递
void printstudent1(struct student s)
{
	cout << "值传递 姓名:" << s.name << " 年龄:" << s.age << " 分数" << s.score << endl;
}
 
//地址传递
void printstudent2(struct student *p)
{
	p->age = 50;
	cout << "地址传递 姓名:" << p->name << " 年龄:" << p->age << " 分数" << p->score << endl;
}
 
int main()
{
	//将学生传入到一个参数中,打印学生所有信息
	struct student s;
	s.name = "张三";
	s.age = 20;
	s.score = 100;
 
	printstudent1(s);  //值传递不修改实参(结构体属性值)
	printstudent2(&s); //地址传递修改实参(结构体属性值)
	cout << "姓名:" << s.name << " 年龄:" << s.age << " 分数" << s.score << endl;
 
	return 0;
}

结构体中const使用

const防止误操作,设置成常量指针,如:const struct student *stu;

内存分区模型

内存分区模型

程序执行时,内存分为4个区域:代码区、全局区、栈区、堆区

程序运行前

在程序编译后生成exe可执行程序,未执行该程序前分为两个区域,代码区和全局区。

代码区

存放CPU执行的机器指令,由操作系统进行管理。代码区是共享的(对于被频繁执行的程序,只需在内存中有一份代码即可)、只读的(防止程序修改指令)。

全局区

存放全局变量(global)、静态变量(static)、常量(字符串常量、const修饰的全局变量)。全局区的数据在整个程序结束后由操作系统释放。

程序运行后

栈区

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

注意:不要返回局部变量的地址,栈区开辟的数据由编译器自动释放。函数值执行完编译器自动释放,局部变量地址就不存在了,返回的地址不对应原来的局部变量。

堆区

由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收。主要使用new在堆区开辟内存。

int * func()
{
	int * p = new int(10);
	return p;
}
 
int main()
{
	int * p = func();
 
	cout << *p << endl;
	return 0;
}

使用new关键字将数据开辟到堆区,指针本质也是局部变量,存放在栈区,但是指针保存的数据存放在堆区。

new操作符

使用new操作符在堆区开辟数据,使用delete操作符释放数据。

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

如果由于内存不足等原因而无法正常分配空间,则new会返回一个空指针NULL,用户可以根据该指针的值判断分配空间是否成功。

用new分配数组空间时不能指定初值。

语法:

new 数据类型

new 数据类型 (初值) 

new 数据类型 [数组大小] 

delete操作符

语法:

delete 指针变量

delete [] 指针变量

int *p = new int(5); /* 开辟整型空间 */
 
int *p = new int;
*p = 5;
 
int p = *new int;
p = 5;
 
delete p; /* 释放空间 */
 
 
int *arr = new int[5]; /* 开辟数组空间 */
delete[] arr; /* 释放空间 */

引用

引用

作用:给变量起别名。

引用不创建新内存空间,指向同一块内存。

若修改变量别名的值,变量原名的值也会修改。

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

本质:指针常量,指针常量的指向不可以修改。int &b = a;  编译器自动转换为 int * const b = &a;

注意:

引用必须初始化。

引用初始化后不可以改变,可以赋值。

int main()
{
	int a = 10;
 
	int &b = a; //正确,引用初始化
	int &b; //错误,没有初始化
        //int &b = 10; //错误,引用必须引一块合法的内存空间
        //正确,加上const后编译器将代码修改为 int temp = 10; const int &b = temp;
        const int &b = 10; 
 
	int c = 20;
	b = c;		//正确,赋值操作,不是更改引用
	int &b = c; //错误,初始化后不可以改变
 
	return 0;
}

引用做函数参数

作用:函数传参时,引用让形参修饰实参。可以简化指针修改实参。

//值传递
void swap1(int a, int b)
{
	int temp = a;
	a = b;
	b = temp;	
}
 
//地址传递
void swap2(int *a, int *b)
{
	int temp = *a;
	*a = *b;
	*b = temp;
}
 
//引用传递
void swap3(int &a, int &b)
{
	int temp = a;
	a = b;
	b = temp;
}
 
int main()
{
	int a = 10;
	int b = 20;
 
	//swap1(a, b);//值传递,形参不会修饰实参 => a=10 b=20
	//swap2(&a, &b);//地址传递,形参会修饰实参 => a=20 b=10
	swap3(a, b);//引用传递,形参会修饰实参 => a=20 b=10
 
	cout << "a = " << a << endl;
	cout << "b = " << b << endl;
 
	return 0;
}

引用做函数返回值

注意:不要返回局部变量引用。如果函数的返回值是引用,则函数调用可以作为左值。

//不要返回局部变量的引用
int & test1()
{
	int a = 10; //局部变量存放在栈区
	return a;
}
 
//函数的调用可以作为左值
int & test2()
{
	//静态变量存放在全局区,全局区上的数据在整个程序结束后系统释放
	static int a = 10; 
	return a;
}
 
int main()
{
	//int &ref = test1();
	//
	//cout << "ref = " << ref << endl;//输出10,因为编译器做了保留
	//cout << "ref = " << ref << endl;//输出乱码,因为a内存被释放
 
	int &ref2 = test2();
	cout << "ref2 = " << ref2 << endl;//输出10
	cout << "ref2 = " << ref2 << endl;//输出10
 
	test2() = 20; //如果函数的返回值是引用,则函数调用可以作为左值
	cout << "ref2 = " << ref2 << endl;//输出20
	cout << "ref2 = " << ref2 << endl;//输出20
 
	return 0;
}

常量引用

作用:常量引用用来修饰形参,防止误操作。在形参列表中,加const修饰形参,防止形参改变实参。

//打印数据函数,修饰形参防止误操作
void print_val(const int &val)
{
	//val = 200;
	cout << "val = " << val << endl;
}
 
int main()
{
	//int &b = 10; //错误,引用必须引一块合法的内存空间
	const int &b = 10; //正确,加上const后编译器将代码修改为 int temp = 10; const int &b = temp;
	
	int a = 10;
	print_val(a); //输出10
 
	return 0;
}

类和对象

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

C++认为所有事物都为对象,对象上有属性和行为。

封装

意义:

  • 将属性和行为作为一个整体。
  • 将属性和行为加以权限控制。

1.将属性和行为作为一个整体。

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

实例化:通过一个类创建一个对象的过程。

成员:类中的属性和行为的统称。

属性:成员属性、成员变量

行为:成员函数、成员方法

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

访问权限三种:

  • public        公共权限    类内可以访问  类外可以访问
  • protected  保护权限    类内可以访问  类外不可以访问  子类可以访问父类
  • private       私有权限    类内可以访问  类外不可以访问  子类不可以访问父类

struct和class的区别

默认的访问权限不同。

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

对象的初始化和清理

构造函数和析构函数

如果不提供构造函数和析构函数,编译器会提供,编译器提供的构造函数和析构函数是空实现。

构造函数:创建对象时为对象的成员属性赋值,构造函数由编译器自动调用。

语法:类名(){}

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

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

语法:~类名(){}

  • 没有返回值,不写void
  • 函数名称与类名相同,在名称前加上波浪号~
  • 没有参数,不可以发生重载
  • 程序在对象销毁前会自动调用析构函数,无需手动调用,且只会调用一次

构造函数的分类及调用

分类方式:

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

拷贝构造函数调用时机:

  • 使用一个已经创建完毕的对象初始化一个新对象
  • 值传递的方式给函数参数传值
  • 以值方式返回局部对象

调用方式:

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

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

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

构造函数调用规则:

  • 如果用户定义有参构造函数,编译器不提供默认无参构造函数,但会提供默认拷贝构造函数。
  • 如果用户定义拷贝构造函数,编译器不提供其他构造函数。
class Person
{
public:
	//无参构造函数(默认)
	Person()
	{
		cout << "Person无参构造函数" << endl;
	}
	//有参构造函数
	Person(int a)
	{
		age = a;
		cout << "Person有参构造函数" << endl;
	}
	//拷贝构造函数
	Person(const Person &p)
	{
		//将传入的人类上所有属性拷贝进来
		age = p.age;
		cout << "Person拷贝构造函数" << endl;
	}
	//析构函数
	~Person()
	{
		cout << "Person析构函数" << endl;
	}
 
	int age;
};
 
//构造函数调用
void test()
{
	//括号法
	Person p1;     //默认构造函数调用
	Person p2(10); //有参构造函数调用
	Person p3(p2); //拷贝构造函数调用
	//注意:调用无参(默认)构造函数时,不加(),否则编译器会语句认为是函数的声明。
 
	//显示法
	Person p1;				//默认构造函数调用
	Person p2 = Person(10); //有参构造函数调用
	Person p3 = Person(p2);	//拷贝构造函数调用
	Person(10); //匿名对象,当前行执行结束后,系统会立即回收匿名对象
	//注意:不要用拷贝构造函数初始化匿名对象,否则编译器会认为Person(p2) ===Person p2;
 
	//隐式转换法
	Person p4 = 10; //相当于 Person p4 = Person(10); 有参构造函数调用
	Person p5 = p4; //拷贝构造函数调用
}
 
int main()
{
	test();
 
	return 0;
}

深拷贝和浅拷贝

浅拷贝:简单的赋值拷贝操作。会造成堆区内存重复释放。

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

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

初始化列表

作用:用来初始化属性。

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

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){}
	
	int m_a;
	int m_b;
	int m_c;
};
 
void test()
{
	Person p;
 
	Person p(10, 20, 30);
}

类对象作为类成员

对象成员:类中的成员是另一个类的对象。

当其他类对象作为本类成员时,构造的时候先构造其他类的对象,再构造自身;析构顺序与构造相反(析构的时候先析构自身类的对象,再析构其他类的对象)。

静态成员

静态成员:在成员变量和成员函数前加上关键字static

静态成员变量:

  • 所有对象共享同一份数据
  • 在编译阶段分配内存
  • 类内声明,类外初始化

静态成员函数:

  • 所有对象共享同一个函数
  • 静态成员函数只能访问静态成员变量
  • 静态成员函数可以设置访问权限,类外访问不到私有静态成员函数
class Person
{
public:
	static void func() //静态成员函数
	{
		m_a = 100; //静态成员函数可以访问静态成员变量
		//m_b = 200; //错误静态成员函数不可以访问非静态成员变量
		cout << "static void func 调用" << endl;
	}
 
	static int m_a; //静态成员变量
	int m_b;
private:
	static void func2()
	{
		cout << "func2调用" << endl;
	}
};
 
int Person::m_a = 100;
 
void test()
{
	//通过对象访问
	Person p;
	p.func();
 
	//通过类名访问
	Person::func();
 
	//Person::func2();//私有静态成员函数访问不了
}
 
int main()
{
	test();
 
	return 0;
}

对象模型

类内的成员变量和成员函数分开存储。只有非静态成员变量才属于类的对象。静态成员变量、静态成员函数、非静态成员函数不属于类对象。

空对象占用内存空间大小:1字节。C++编译器会给每个空对象分配一个字节空间,是为了区分空对象占内存的位置。每个空对象有独一无二的内存地址。

this指针

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

this指针本质:指针常量,指针指向不可以修改。

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

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

用途:

  • 当形参和成员变量同名是,用this指针来区分。
  • 在类的非静态成员函数中返回对象本身,可使用 return *this;
class Person
{
public:
	Person(int age)
	{
		//this指针指向被调用的成员函数所属的对象p1
		this->age = age;
	}
	Person & person_add_age(Person &p)
	{
		this->age += p.age;
		//this指向p2的指针,*this指向p2对象本身
		return *this;
	}
	int age;
};
 
void test1()
{
	Person p1(18);
	cout << "p1年龄为:" << p1.age << endl;
}
 
void test2()
{
	Person p1(10);
	Person p2(10);
	//链式编程
	p2.person_add_age(p1).person_add_age(p1).person_add_age(p1);
	cout << "p2年龄为:" << p2.age << endl;
}
 
int main()
{
	//test1();
	test2();
 
	return 0;
}

空指针访问成员函数

空指针可以调用成员函数,但是要注意有没有用到this指针。如果用到this指针,需要加以判断,保证代码健壮性。

if (this == NULL) {
	return;
}

const修饰成员函数

常函数:

  • 成员函数后加const的函数,修饰的是this指向,让指针指向的值不可以修改
  • 常函数内不可以修改成员属性
  • 成员属性声明时加关键字mutable,在常函数中可以修改变量值

常对象:

  • 声明对象前加const的对象
  • 常对象只能调用常函数
class Person
{
public:
	//this指针本质是指针常量,指针指向不可修改
	//Person * const this; 成员函数后 不加const时this指针表示形式
	//const Person * const this; 成员函数后 加const时this指针表示形式
	void show_person() const
	{
		//this->m_a = 100;
		//this = NULL;//不可以修改指针指向
		this->m_b = 100;
	}
 
	void func(){ }
 
	int m_a;
	mutable int m_b;//特殊变量,在常函数中可以修改这个值
};
 
void test1()
{
	Person p;
	p.show_person();
 }
 
void test2()
{
	const Person p;	 //常对象
	//p.m_a = 100;	 //不可以修改
	p.m_b = 100;	 //可以修改,有mutable修饰
	//p.func();		 //常对象不能调用普通成员函数
	p.show_person(); //常对象只能调用常函数
}
 
int main()
{
	test1();
	test2();
 
	return 0;
}

友元

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

关键字:friend

友元的三种实现:

  • 全局函数做友元
  • 类做友元
  • 成员函数做友元

全局函数做友元

在有私有成员的类中声明全局函数并在前加friend,就可以访问类中的私有成员

类做友元

在有私有成员的类中声明来访问的类并在前加friend,如 friend class Person;

成员函数做友元

在有私有成员的类中声明来成员函数类并在前加friend,如 friend void Person::visit();

运算符重载

对已有的运算符重新定义,赋予另一种功能,适应不同数据类型。

加号运算符重载

实现两个自定义数据类型相加。对于内置的数据类型的表达式的运算符不可修改。不要滥用运算符重载。

  • 成员函数重载+号
  • 全局函数重载+号
class Person
{
public:
	//1.成员函数重载+号
	Person operator+(Person &p)
	{
		Person temp;
		temp.m_a = this->m_a + p.m_a;
		temp.m_b = this->m_b + p.m_b;
		return temp;
	}
 
	int m_a;
	int m_b;
};
 
//2.全局函数重载+号
Person operator+(Person &p1, Person &p2)
{
	Person temp;
	temp.m_a = p1.m_a + p2.m_a;
	temp.m_b = p1.m_b + p2.m_b;
	return temp;
}
 
//函数重载
Person operator+(Person &p1, int num)
{
	Person temp;
	temp.m_a = p1.m_a + num;
	temp.m_b = p1.m_b + num;
	return temp;
}
 
void test()
{
	Person p1;
	p1.m_a = 10;
	p1.m_b = 10;
 
	Person p2;
	p2.m_a = 10;
	p2.m_b = 10;
 
	//成员函数重载本质调用
	//Person p3 = p1.operator+(p2);
 
	//全局函数重载本质调用
	//Person p3 = operator+(p1, p2);
	
	Person p3 = p1 + p2;
	cout << "p3.m_a = " << p3.m_a << endl;
	cout << "p3.m_b = " << p3.m_b << endl;
 
	Person p4 = p1 + 20; //Person + int
	cout << "p4.m_a = " << p4.m_a << endl;
	cout << "p4.m_b = " << p4.m_b << endl;
}
 
int main()
{
	test();	
	
	return 0;
}

左移运算符重载

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

  • 全局函数重载左移运算符
class Person
{
	friend ostream & operator<<(ostream &cout, Person &p);
public:
	Person(int a, int b)
	{
		m_a = a;
		m_b = b;
	}
private:
	//利用成员函数重载左移运算符 p.operator<<(cout) 简化为 p << cout
	//不会利用成员函数重载左移运算符,因为无法实现const在左侧
	//void operator<<(cout)
 
	int m_a;
	int m_b;
};
 
//全局函数重载左移运算符
ostream & operator<<(ostream &cout, Person &p) //operator<<(cout, p) 简化为 cout << p
{
	cout << "p.m_a = " << p.m_a << "p.m_b = " << p.m_b;
	return cout;
}
 
void test()
{
	Person p(10,10);
 
	cout << p << endl;
}
 
int main()
{
	test();	
	
	return 0;
}

递增运算符重载

class MyInteger
{
	friend ostream& operator<<(ostream& cout, MyInteger myint);
public:
	MyInteger()
	{
		m_num = 0;
	}
 
	//重载前置++运算符
	MyInteger& operator++()
	{
		m_num++;
		return *this; //将自身返回
	}
 
	//重载后置++运算符
	MyInteger& operator++(int) //int表示占位参数,区分前置和后置
	{
		//先记录当前结果,后递增,最后将记录结果返回
		MyInteger temp =  *this;
		m_num++;
		return temp;
	}
 
private:
	int m_num;
};
 
//重载<<运算符
ostream& operator<<(ostream& cout, MyInteger myint)
{
	cout << myint.m_num;
	return cout;
}
 
void test1()
{
	MyInteger myint;
 
	cout << ++(++myint) << endl;
	cout << myint << endl;
}
 
void test2()
{
	MyInteger myint;
 
	cout << myint++ << endl;
	cout << myint << endl;
}
 
int main()
{
	//test1();
	test2();
	return 0;
}

赋值运算符重载

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

  1. 默认构造函数(无参,函数体为空)
  2. 默认析构函数(无参,函数体为空)
  3. 默认拷贝构造函数,对属性进行值拷贝
  4. 赋值运算符operator=,对属性进行值拷贝
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)
	{
		//先判断是否有属性在堆区,如果有先释放干净,再深拷贝
		if (m_age != NULL) {
			delete m_age;
			m_age = NULL;
		}
		m_age = new int(*p.m_age); //深拷贝
 
		return *this;
	}
	int *m_age;
};
 
void test1()
{
	Person p1(18);
	Person p2(20);
	Person p3(30);
	p3 = p2 = p1; //赋值操作
 
	cout << "p1年龄:" << *p1.m_age << endl;
	cout << "p2年龄:" << *p2.m_age << endl;
	cout << "p3年龄:" << *p3.m_age << endl;
}
 
int main()
{
	test1();
	return 0;
}

关系运算符重载

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;
		}
		return false;
	}
 
	bool operator!=(Person &p)
	{
		if (this->m_name == p.m_name && this->m_age == p.m_age) {
			return false;
		}
		return true;
	}
	string m_name;
	int m_age;
};
 
void test()
{
	Person p1("tom", 18);
	Person p2("jerry", 18);
	if (p1 == p2) {
		cout << "p1 p2 相等" << endl;
	}
	else {
		cout << "p1 p2 不相等" << endl;
	}
 
	if (p1 != p2) {
		cout << "p1 p2 不相等" << endl;
	}
	else {
		cout << "p1 p2 相等" << endl;
	}
}
 
int main()
{
	test();
	return 0;
}

函数调用运算符重载

调用运算符为()

重载后使用的方式像函数的调用,称为仿函数。仿函数无固定写法。

class MyPrint
{
public:
	//重载函数调用运算符
	void operator()(string test)
	{
		cout << test << endl;
	}
};
 
void myprint2(string test)
{
	cout << test << endl;
}
 
void test1()
{
	MyPrint myprint;
	myprint("hello world"); //仿函数
 
	myprint2("hellor world");
}
 
//仿函数无固定写法
class MyAdd
{
public:
	int operator()(int num1, int num2)
	{
		return num1 + num2;
	}
};
 
void test2()
{
	MyAdd myadd;
	int ret = myadd(100, 100);
	cout << "ret = " << ret << endl;
 
	//匿名函数对象
	cout << MyAdd()(100, 100) << endl;
 
}
 
int main()
{
	test1();
	test2();
	return 0;
}

继承

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

子类(派生类)/ 父类(基类)

派生类中包括两部分:1.从基类继承过来 2.自己增加的成员

继承方式:公共继承、保护继承、私有继承

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

父类中的私有成员只是被编译器隐藏了,虽然访问不到,但是还会继承下去。

公共继承:

  • 父类中的公共权限成员,到子类中依然是公共权限,类外可以访问
  • 父类中的保护权限成员,到子类中依然是保护权限,类外访问不到
  • 父类中的私有权限成员,子类访问不到

保护继承:

  • 父类中的公共权限成员,到子类中变成保护权限,类外访问不到
  • 父类中的保护权限成员,到子类中变成保护权限,类外访问不到
  • 父类中的私有权限成员,子类访问不到

私有继承:

  • 父类中的公共权限成员,到子类中变成私有权限,类外访问不到
  • 父类中的保护权限成员,到子类中变成私有权限,类外访问不到
  • 父类中的私有权限成员,子类访问不到
class A
{
public:
	int a;
protected:
	int b;
private:
	int c;
};
 
//共有继承
class B : public A
{
public:
	int a;
protected:
	int b;
	//int c; 无法访问
};
 
//保护继承
class B :protected A
{
protected:
	int a;
	int b;
	//int c; 无法访问
};
 
//私有继承
class B :private A
{
private:
	int a;
	int b;
	//int c; 无法访问
};

继承中构造函数和析构函数顺序

先构造父类,再构造子类。析构顺序与构造顺序相反。

父类构造函数-->子类构造函数-->子类析构函数-->父类析构函数

继承同名成员处理方式

  • 访问子类同名成员,直接访问。
  • 访问父类同名成员,需要加作用域。
  • 当子类与父类拥有同名的成员函数,子类会隐藏父类中同名成员函数,加作用域可以访问父类中同名函数。
class Base
{
public:
	Base()
	{
		m_a = 100;
	}
	void func()
	{
		cout << "Base - fun()" << endl;
	}
	void func(int a)
	{
		cout << "Base - fun(int a)" << endl;
	}
	int m_a;
};
 
class Son :public Base
{
public:
	Son()
	{
		m_a = 200;
	}
	void func()
	{
		cout << "Son - fun()" << endl;
	}
	void func(int a)
	{
		cout << "Son - fun(int a)" << endl;
	}
 
	int m_a;
};
 
//同名成员属性
void test1()
{
	Son s1;
	cout << "Son下 m_a = " << s1.m_a << endl;
	//子类访问父类中同名成员,需要添加作用域
	cout << "Base下 m_a = " << s1.Base::m_a << endl;  
}
 
//同名成员函数
void test2()
{
	Son s2;
	s2.func();
	//子类访问父类中同名成员函数,需要添加作用域
	s2.Base::func();
	s2.Base::func(100);
}
 
int main()
{
	test1();
	test2();
	return 0;
}

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

  • 访问子类同名成员,直接访问。
  • 访问父类同名成员,需要加作用域。
  • 同盟静态成员处理方式和非静态处理方式一样,只不过有两种访问方式(通过对象和通过类名)。
class Base
{
public:
	static int m_a;
	static void func()
	{
		cout << "Base - static void func()" << endl;
	}
};
int Base::m_a = 100;
 
class Son : public Base
{
public:
	static int m_a;
	static void func()
	{
		cout << "Son - static void func()" << endl;
	}
};
int Son::m_a = 200;
 
//同名静态成员属性
void test1()
{
	//1.通过对象访问
	cout << "通过对象访问:" << endl;
	Son s1;
	cout << "Son 下 m_a = " << s1.m_a << endl;
	cout << "Base 下 m_a = " << s1.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()
{
	//1.通过对象访问
	cout << "通过对象访问:" << endl;
	Son s2;
	s2.func();
	s2.Base::func();
 
	//2.通过类名访问
	cout << "通过类名访问:" << endl;
	Son::func();
	Son::Base::func();
}
 
int main()
{
	test1();
	test2();
	return 0;
}

多继承

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

菱形继承

两个派生类继承同一个基类,又有某个类同时继承两个派生类。

当两个父类拥有相同数据,需要加作用域区分。菱形继承导致数据有两份,造成资源浪费。

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

虚基类指针- vbptr(v - virtual b - base ptr - pointer)指向vbtable虚基类表

多态

多态通常用指针或引用实现

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

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

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

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

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

只要父类在定义成员函数时声明了virtual关键字,在子类中实现的时候覆盖该函数时,virtual关键字可加可不加,不影响多态的实现。

多态满足条件:

  • 有继承关系
  • 子类重写父类的虚函数,子类的虚函数表内部会替换成子类的虚函数地址。

多态使用条件:

  • 父类的指针或引用指向子类对象

虚函数指针 - vfptr(v - virtual f - function ptr - pointer)指向vftable虚函数表,表内记录虚函数的地址。

多态优点:

  • 代码组织结构清晰
  • 可读性强
  • 利于前期和后期的扩展和维护
//普通写法实现计算器
class Calculator
{
public:
	int get_result(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;
		}
		//需要扩展新的功能,需要修改源码
		//在实际开发中,遵循 开闭原则 :对扩展进行开发,对修改进行关闭
	}
 
	int m_num1;
	int m_num2;
};
 
void test1()
{
	Calculator c;
	c.m_num1 = 10;
	c.m_num2 = 10;
 
	cout << c.m_num1 << " + " << c.m_num2 << " = " << c.get_result("+") << endl;
	cout << c.m_num1 << " - " << c.m_num2 << " = " << c.get_result("-") << endl;
	cout << c.m_num1 << " * " << c.m_num2 << " = " << c.get_result("*") << endl;
}
 
//多态写法实现计算器
//计算器抽象类
class AbstractCalculator
{
public:
	virtual int get_result()
	{
		return 0;
	}
 
	int m_num1;
	int m_num2;
};
 
//加法计算器类
class AddCalcultor :public AbstractCalculator
{
public:
	int get_result()
	{
		return m_num1 + m_num2;
	}
};
 
//减法计算器类
class SubCalcultor :public AbstractCalculator
{
public:
	int get_result()
	{
		return m_num1 - m_num2;
	}
};
 
//乘法计算器类
class MulCalcultor :public AbstractCalculator
{
public:
	int get_result()
	{
		return m_num1 * m_num2;
	}
};
 
void test2()
{
	//多态使用条件:父类指针或引用指向子类对象
	//加法运算
	AbstractCalculator * abc = new AddCalcultor;
	abc->m_num1 = 100;
	abc->m_num2 = 100;
 
	cout << abc->m_num1 << " + " << abc->m_num2 << " = " << abc->get_result() << endl;
	delete abc; //用完销毁
 
	//减法运算
	abc = new SubCalcultor; //虽然销毁,但是指针类型没有变,还是父类指针
	abc->m_num1 = 100;
	abc->m_num2 = 100;
 
	cout << abc->m_num1 << " - " << abc->m_num2 << " = " << abc->get_result() << endl;
	delete abc; 
 
	//乘法运算
	abc = new MulCalcultor; 
	abc->m_num1 = 100;
	abc->m_num2 = 100;
 
	cout << abc->m_num1 << " * " << abc->m_num2 << " = " << abc->get_result() << endl;
	delete abc; 
 
}
 
int main()
{
	test1();
	test2();
 
	return 0;
}

纯虚函数和抽象类

在多态中,通常父类中的虚函数实现是毫无意义的,主要是调用子类重写的内容,因此可以将虚函数改为纯虚函数。

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

抽象类:有纯虚函数的类。

抽象类特点:

  • 无法实例化对象。
  • 子类必须重写抽象类中的纯虚函数,否则也属于抽象类。

虚析构和纯虚析构

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

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

虚析构和纯虚析构共性:

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

虚析构和纯虚析构区别:

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

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

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

虚析构和纯虚析构用来解决通过父类指针释放子类对象

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

文件操作

文件操作头文件<fstream>

文件操作:

  • 文本文件
  • 二进制文件

操作文件三大类:

  • ofstream:写操作
  • ifstream:读操作
  • fstream:读写操作

文本文件

文件以文本的ASCII码形式存储

写文件

写文件步骤:

  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   二进制方式

读文件

读文件步骤:

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

ifs.is_open() 判断打开成功返回1,打开失败返回0

void test()
{
	ifstream ifs;
 
	ifs.open("test.txt", ios::in);
	if (!ifs.is_open()) {
		cout << "failed to open" << endl;
		return;
	}
 
	//读数据
	//第一种
	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) {
		cout << c;
	}
 
	ifs.close();
}

二进制文件

文件以文本的二进制形式存储,打开方式指定ios::binary

写文件

调用成员函数write

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

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

class Person
{
public:
	char m_name[64];
	int m_age;
};
 
void test()
{
	ofstream ofs("person.txt", ios::out | ios::binary);
	Person p = { "haha", 20 };
	ofs.write((const char *)&p, sizeof(p));
	ofs.close();
}

读文件

调用成员函数read

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

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

模板

模板目的:提高复用性,将类型参数化。

函数模板

建立一个通用函数,函数返回值类型和形参类型不具体制定,用一个虚拟的类型代表

语法:

  1. template<typename T>
  2. 函数声明或定义
  • template 声明创建模板
  • typename 表明后面的符号是一种数据类型,可以用class代替,typename和class意义一样
  • T 通用的数据类型,名称可以替换,通常为大写字母

两种方式使用函数模板:

  1. 自动类型推导
  2. 显示指定类型(建议使用)

使用模板时必须确定出通用数据类型T,并且能够推导出一致的类型。

/*
 * 函数模板
 * 声明一个模板,告诉编译器后面代码中T不要报错,T是一个通用数据类型
 */
template<typename T>
void my_swap(T &a, T &b)
{
	T temp = a;
	a = b;
	b = temp;
}
 
void test()
{
	int a = 10;
	int b = 20;
 
	//两种方式使用函数模板
	//1.自动类型推导
	//my_swap(a, b);
 
	//2.显示指定类型
	my_swap<int>(a, b);
 
	cout << "a = " << a << endl;
	cout << "b = " << b << endl;
}
 
 
int main()
{
	test();
 
	return 0;
}

普通函数与函数模板区别:

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

普通函数和函数模板调用规则:

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

类模板

建立一个通用类,类中得成员数据类型可以不具体指定,用一个虚拟的类型代表。

语法:

  1. template<typename T1, typename T2, typename T3 ....>
  • template 声明创建模板
  • typename 表明后面的符号是一种数据类型,可以用class代替,typename和class意义一样
  • T 通用的数据类型,名称可以替换,通常为大写字母

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

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

  • 普通类中的成员函数一开始就可以创建
  • 类模板中的成员函数在调用时才创建

类模板与函数模板区别

类模板与函数模板区别主要有两点:

  1. 类模板没有自动类型推导的使用方式,如Person<string, int>p("haha",20);
  2. 类模板在模板参数列表中可以有默认参数,如template<typename T=int>

类模板与继承

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

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

类模板成员函数类外实现

类模板中成员函数类外实现时,需要加上模板参数列表

template<class T1, class T2>
class Person
{
public:
	//成员函数类内声明
	Person(T1 name, T2 age);
	void show_person();
 
	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;
}
 
//成员函数  类外实现
template<class T1, class T2>
void Person<T1, T2>::show_person()
{
	cout << "姓名:" << this->m_name << "年龄:" << this->m_age << endl;
}

类模板分文件编写

将声明和实现写到同一个文件中,并更改后缀名为.hpp

类模板与友元

  • 全局函数类内实现-直接在类内声明友元即可
  • 全局函数类外实现-需要提前让编译器知道全局函数的存在
//提前让编译器知道Person类的存在
template<class T1, class T2>
class Person;
 
//类外实现-提前让编译器知道全局函数的存在
template<class T1, class T2>
void print_person_2(Person<T1, T2> p)
{
	cout << "类外实现 姓名:" << p.m_name << " 年龄:" << p.m_age << endl;
}
 
template<class T1, class T2>
class Person
{
	//全局函数 类内实现
	friend void print_person_1(Person<T1,T2> p)
	{
		cout << "姓名:" << p.m_name << " 年龄:" << p.m_age << endl;
	}
 
	//全局函数 类外实现
	//加空模板参数列表
	//全局函数类外实现 - 需要提前让编译器知道全局函数的存在
	friend void print_person_2<>(Person<T1, T2> p);
	
public:
	Person(T1 name, T2 age)
	{
		this->m_name = name;
		this->m_age = age;
	}
 
private:
	T1 m_name;
	T2 m_age;
};
 
void test()
{
	Person<string, int>p("haha", 20);
	print_person_1(p);
	print_person_2(p);
}
 
int main()
{
	test();
 
	return 0;
}

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

奶油芝士汉堡包

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值