C++:类的默认成员函数------构造函数析构函数(超详细解析,小白一看就懂!)

news2024/11/16 11:51:59

目录

一、前言

二、为什么会出现构造函数和析构函数 

 三、构造函数

🍎构造函数的概念 

🍐构造函数特性 

💦解释特性3:对象实例化时编译器自动调用对应的构造函数

💦解释特性4:构造函数支持重载

 💦解释特性5:如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成

 💦解释特性6:无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个

四、析构函数 

🍉析构函数概念 

🍓析构函数特性 

 五、共勉


一、前言

        在我们前面学习的中,我们会定义成员变量成员函数,这些我们自己定义的函数都是普通的成员函数,但是如若我们定义的类里什么也没有呢?是真的里面啥也没吗?如下:

class Date {};

如果一个中什么成员都没有,简称为空类。空类中什么都没有吗?并不是的任何一个类在我们不写的情况下,都会自动生成6个默认成员函数。

【默认成员函数概念】:用户没有显式实现,编译器会生成的成员函数称为默认成员函数


 


        其中两个默认成员函数是用来初始化清理的分别为:构造函数、析构函数

      本次博客将详解为什么会出现这两个函数,这两个函数将如何使用 的问题

二、为什么会出现构造函数和析构函数 

 首先看下面这段 C语言代码

typedef struct Date
{
	int year;
	int month;
	int day;
}D;

void Init(D* date)
{
	date->year = 2023;
	date->month = 10;
	date->day = 21;
}
void Printf(D* date)
{
	cout << date->year << "-" << date->month << "-" << date->day << endl << endl;
}
void Destory(D* date)
{
	date->year = 0;
	date->month = 0;
	date->day = 0;
}
int main()
{
	D date;
	Init(&date);
	Printf(&date);
	Destory(&date);
	return 0;
}


 

⚠ 注意:大家在日常写代码和刷题的时候,肯定会有过忘记初始化,或者忘记销毁,这些小细节很容易被大家忽略,但是出现在代码中,就会出现报错,导致我们写代码的时候,就很烦。

⚠ 忘记写初始化:输出随机值,结果会错误
 


⚠ 忘记写销毁:时间久了便会造成【内存泄漏】
 



💦你是否发现若是我们要去使用一个Date的话,通常不会忘了去往里面入数据或者是出数据,但是却时常会忘了【初始化】和【销毁】。这要如何是好呢😔
 

🔑 解决方案:

1️⃣:在上一文的学习中,我们学习到了一个类中的一个东西叫做this指针,只要是在成员函数内部都可以进行调用。而且还知晓了C++中原来是使用this指针接受调用对象地址的机制来减少对象地址的传入,减轻了调用者的工作。这也是C++区别于C很大的一点

2️⃣:那C++中是否还有东西能够替代【初始化】和【销毁】这两个工作呢?答案是有的,就是我们接下来要学习的构造函数】和【析构函数
 

 三、构造函数

🍎构造函数的概念 

 如下的日期类:

class Date
{
public:
	void Init(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << "今日日期输出:" << endl;
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1;
	d1.Init(2023, 10, 21);
	d1.Print();
	return 0;
}

运行效果:


        正常情况下,我们写的这个日期类,首先初始化,其次打印。但如果说你突然忘记初始化了,直接就开始访问会怎么样呢?



        从运行结果上看,没初始化直接访问输出的是随机值。 忘记初始化其实是一件很正常的事情,C++大佬在这一方面为了填补C语言的坑(必须得手动初始化)。因而就设计出了构造函数。
        构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,保证每个数据成员都有 一个合适的初始值,并且在对象的生命周期内只调用一次。其目的就是为了方便我们不需要再初始化。

🍐构造函数特性 

        构造函数是特殊的成员函数,需要注意的是,构造函数的虽然名称叫构造,但是需要注意的是构造函数的主要任务并不是开空间创建对象,而是初始化对象。
其特征如下:

  1. 函数名和类名相同
  2. 无返回值
  3. 对象实例化时编译器自动调用对应的构造函数
  4. 构造函数可以重载
  5. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成
  6. 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认成员函数。

如下即为构造函数:

Date()
{
	_year = 1;
	_month = 1;
	_day = 1;
}

 💦解释特性3:对象实例化时编译器自动调用对应的构造函数

        也就是说我们在实例化一个对象后,它会自动调用这个构造函数,自动就初始化了,我们可以通过调试看看:
 

💦解释特性4:构造函数支持重载

如下的函数:

Date(int year, int month, int day)
{
	_year = year;
	_month = month;
	_day = day;
}

        像这个重载函数是明确了我们要传参的,所以我们在实例化对象后就必须把参数写上去(虽然看着奇奇怪怪,但是没有办法,毕竟我们普通的调用,参数都是在函数名后面,而这个参数在实例化对象后面):

Date d2(2023, 10, 21);

来输出和我们先前的构造函数对比看看:

  • 注意:没有参数时我在调用的时候不能加上括号(),切忌!!构造函数尤为特殊
  • 如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明

无参的情况下必须要像我们刚开始实例化的d1那样:

Date d1;
d1.Print();
  •  构造函数的重载我们推荐写成全缺省的样子:
//普通的构造函数
	Date()
	{
		_year = 1;
		_month = 1;
		_day = 1;
	}
//全缺省的构造函数
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

        首先,普通的构造函数和全缺省的构造函数在不调用的情况下可以同时存在,编译也没有错误。但是在实际调用的过程中,会存在歧义。如下的调用:

class Date
{
public:
//普通的构造函数
	Date()
	{
		_year = 1;
		_month = 1;
		_day = 1;
	}
//全缺省的构造函数
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1;
	d1.Print();
}

        此时我实例化的d1到底是调用普通的构造函数?还是调用全缺省的构造函数?并且此段代码编译出现错误。何况我在没有调用函数的情况下编译是没错的。

       🔑 由此可见:它们俩在语法上可以同时存在,但是使用上不能同时存在,因为会存在调用的歧义,不知道调用的是谁,所以一般情况下,我们更推荐直接写个全缺省版的构造函数,因为是否传参数可由你决定。传参数数量也是由你决定。

 💦解释特性5:如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成

 看如下代码,自己不去写构造函数,使用编译器默认的构造函数:

class Date
{
public:
	
	// 我们不写,编译器会生成一个默认无参构造函数
	/*Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}*/
	
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	// 没有定义构造函数,对象也可以创建成功,因此此处调用的是编译器生成的默认构造函数
	Date d;
	d.Print();
}


        不是说好我不自己写构造函数,编译器会默认生成吗?为什么到这又是随机值了?这难道也算初始化?别急,搞清楚这个得先明白默认构造函数
⚠ 默认构造函数:

  • 1. 我们不写编译器默认生成的那个构造函数,叫默认构造
  • 2. 无参构造函数也可以叫默认函数
  • 3. 全缺省也可以叫默认构造

      总结: 可以不传参数就调用构造,都可以叫默认构造

C++把变量分成两种:

      1️⃣:内置类型/基本类型:int、char、double、指针……
      2️⃣:自定义类型:class、struct去定义的类型对象

        C++默认生成的构造函数对于内置类型成员变量不做处理,对于自定义类型的成员变量才会处理,这也就能很好的说明了为什么刚才没有对年月日进行处理(初始化),因为它们是内置类型(int类型的变量)


让我们来看看自定义类型是如何处理的。

class A
{
public:
	A()
	{
		cout << "A()" << endl;
		_a = 1;
	}
private:
	int _a;
};

        首先,这是一个名为A的类,有成员变量_a,并且还有一个无参的构造函数,对_a初始化为1。接着:

class Date
{
public:
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
		cout << endl;
	}
private:
	int _year = 2023;
	int _month = 11;
	int _day = 11;
	A _aa;
};

int main()
{
	Date d1;
	d1.Print();
	return 0;
}

       通过运行结果以及调试,也正验证了默认构造函数对自定义类型才会处理。这也就告诉我们,当出现内置类型时,就需要我们自己写构造函数了。

什么时候使用默认构造函数会凸显出其价值呢?就比如我们之前写的括号匹配这道题:

class Stack
{
public:
	Stack()
	{
		_a = nullptr;
		_top = _capacity;
	}
private:
	int* _a;
	int _top;
	int _capacity;
 
};
 
class MyQueue 
{
public:
	//默认生成的构造函数就可以用了
	void push(int x)
	{}
 
	int pop() 
	{}
private:
	Stack _S1;
	Stack _s2;
};

        此时我队列里自定义类型_s1和_s2就不需要单独写初始化了,直接用默认的。但是如果栈里没有写构造函数,那么其输出的还是随机的,因为栈里的也是内置类型。就是一层套一层,下一层生效的前提是上一层地基打稳了。

🔑总结:

  1. 如果一个类中的成员全是自定义类型,我们就可以用默认生成的函数
  2. 如果有内置类型的成员,或者需要显示传参初始化,那么都要自己实现构造函数。

 💦解释特性6:无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个

🔑 默认构造函数:

  • 1. 我们不写编译器默认生成的那个构造函数,叫默认构造
  • 2. 无参构造函数也可以叫默认函数
  • 3. 全缺省也可以叫默认构造

总结: 可以不传参数就调用构造,都可以叫默认构造


🔑既然我默认构造函数只对自定义类型才会处理,那如果我不想自己再写构造函数也要对内置类型处理呢?我们可以这样做:

class Date
{
public:

	// 我们不写,编译器会生成一个默认无参构造函数
	/*Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}*/

	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	// C++11 打的补丁,针对编译器自己默认成员函数不初始化问题
	int _year = 2023;
	int _month = 10;
	int _day = 21;
};
int main()
{
	// 没有定义构造函数,对象也可以创建成功,因此此处调用的是编译器生成的默认构造函数
	Date d;
	d.Print();
}

 🔑总结:

  1. 构造函数是类中默认就带有的,不过日常自己在写一个类的时候尽量不要用默认生成的,最好是自己写一个,无参或者是缺省的都可以,但是不可以无参和全缺省共存,会引发歧义。
  2. 若是使用默认生成的构造函数,会引发一些语言本身就带有的缺陷,【内置类型】的数据不会被初始化,还会是一个随机值;【自定义类型】的数据会调用默认构造函数(默认生成、无参、全缺省),若是不想看到随机值的话,可以参照C++11中的特性,在内置类型声明的时候就为其设置一个初始化值,便不会造成随机值的问题

四、析构函数 

🍉析构函数概念 

        前面通过构造函数的学习,我们知道一个对象时怎么来的,那一个对象又是怎么没呢的?
        析构函数:与构造函数功能相反,析构函数不是完成对象的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成类的一些资源清理工作。
 

🍓析构函数特性 

析构函数是特殊的成员函数。

其特征如下:

  1. 析构函数名是在类名前加上字符 ~。
  2. 无参数无返回值。
  3. 一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。
  4. 对象生命周期结束时,C++编译系统系统自动调用析构函数
  5. 编译器生成的默认析构函数,对会自定类型成员调用它的析构函数

我们实际写一个析构函数看看:

~Date()
{
	cout << "~Date()" << endl;
}

带入示例再看看:

class Date
{
public:
	Date(int year = 2023, int month = 10, int day = 21)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
	~Date()
	{
		cout << "~Date()" << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1;
	d1.Print();
	return 0;
}

        首先,我实例化出的d1会调用它的默认构造函数进行初始化,其次,出了作用域后又调用其析构函数,这也就是为什么输出结果会是~Date()
        析构的目的是为了完成资源清理,什么样的才能算是资源清理呢?像我这里定义的年月日变量就不需要资源清理,因为出了函数栈帧就销毁,真正需要清理的是malloc、new、fopen这些的,就比如清理栈里malloc出的

class Stack
{
public:
	//构造函数
	Stack(int capacity = 10)
	{
		_a = (int*)malloc(sizeof(int) * capacity);
		assert(_a);
		_top = 0;
		_capacity = capacity;
	}
	//析构函数
	~Stack()
	{
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}
private:
	int* _a;
	int _top;
	int _capacity;
};
int main()
{
	Stack st;
}

这里不难感慨C++的构造函数就像先前C语言常写的Init,而析构函数就像Destroy

  • 看如下的题目:现在我用类实例化出st1和st2两个对象,首先,st1肯定先构造,st2肯定后构造,这点毋庸置疑,那关键是谁先析构呢?
int main()
{
	Stack st1;
	Stack st2;
}

答案:st2先析构,st1后析构

解析:这里st1和st2是在栈上的,建立栈帧,其性质和之前一样,后进先出,st2后压栈,那么它肯定是最先析构的。所以栈里面定义对象,析构顺序和构造顺序是反的。

        若自己没有定义析构函数,虽说系统会自动生成默认析构函数,不过也是有要求的,和构造函数一样,内置类型不处理,自定义类型会去调用它的析构函数,如下:

class Stack
{
public:
	//构造函数
	Stack(int capacity = 10)
	{
		_a = (int*)malloc(sizeof(int) * capacity);
		assert(_a);
		_top = 0;
		_capacity = capacity;
	}
	//析构函数
	~Stack()
	{
		cout << "~Stack():" << this << endl;
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}
private:
	int* _a;
	int _top;
	int _capacity;
 
};
class MyQueue
{
public:
	//默认生成的构造函数可以用
	//默认生成的析构函数也可以用
	void push(int x)
	{}
 
	int pop()
	{}
private:
	Stack _S1;
	Stack _s2;
};
int main()
{
	MyQueue q;
}


        对于MyQueue而言,我们不需要写它的默认构造函数,因为编译器对于自定义类型成员(_S1和_S2)会去调用它的默认构造,Stack提供了默认构造,出了作用域,编译器会针对自定义类型的成员去默认调用它的析构函数,因为有两个自定义成员(_S1和_S2),自然析构函数也调了两次,所以会输出两次Stack()……
 

🔑总结:

  • 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如Date类;有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类。

 五、共勉

        以下就是我对C++类的默认成员函数--------构造函数&&析构函数的理解,如果有不懂和发现问题的小伙伴,请在评论区说出来哦,同时我还会继续更新对C++ 类的默认成员函数-------拷贝构造&&赋值重载的理解,请持续关注我哦!!!     

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1119167.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

进阶JAVA篇- Collcetions 工具类与集合的并发修改异常问题

目录 1.0 集合的并发修改问题 1.1 如何解决集合的并发修改问题 2.0 Collcetions 工具类的说明 1.0 集合的并发修改问题 我们可以简单的认为&#xff0c;就是使用迭代器遍历集合时&#xff0c;又同时在删除集合中的数据&#xff0c;程序就会出现并发修改异常的错误。 代码如下&…

linux性能分析(五)CPU篇(一)基础

一 CPU篇 遗留&#xff1a; 负载与cpu关系、负载与线程的关系? ① CPU 相关概念 1、physical 物理CPU个数 --> 一般一个实体 2、cpu 核数 3、逻辑CPU个数 逻辑核 4、超线程 super thread 技术 5、各种cpu的计算方式物理 physical CPU的个数&#xff1a; physical id逻…

【Javascript】创建对象的几种方式

通过字面量创建对象 通过构造函数创建对象 Object()-------------构造函数 通过构造函数来实例化对象 给person注入属性 Factory工厂 this指向的是对象的本身使⽤new 实例化⼀个对象&#xff0c;就像⼯⼚⼀样

5G学习笔记之5G频谱

参考&#xff1a;《5G NR通信标准》1. 5G频谱 1G和2G移动业务的频段主要在800MHz~900MHz&#xff0c;存在少数在更高或者更低频段&#xff1b;3G和4G的频段主要在450MHz ~ 6GHz&#xff1b;5G主要是410MHz ~ 6GHz&#xff0c;以及24GHz ~ 52GHz。 5G频谱跨度较大&#xff0c;可…

TCP为什么需要三次握手和四次挥手?

一、三次握手 三次握手&#xff08;Three-way Handshake&#xff09;其实就是指建立一个TCP连接时&#xff0c;需要客户端和服务器总共发送3个包 主要作用就是为了确认双方的接收能力和发送能力是否正常、指定自己的初始化序列号为后面的可靠性传送做准备 过程如下&#xff…

laravel 中 npm run 同时执行多个命令

在使用laravel 启动项目时 经常需要同时运行两个命令。 1.前端既是 npm run dev 2.后端php则是 php artisan serve 可以安装 使用 concurrently 进行并行启动 concurrently - npm npm install concurrently --save 之后修改 package.json 在 scripts 中增加 &#xff08;多条…

shell算术运算符

文章目录 算术运算符&#xff1a;算术运算扩展算术运算指令expr算术运算指令let自增自减运算符 算术运算符&#xff1a; 加法 - 减法 * 乘法 / 除法 % 取余 ** 幂运算算术运算扩展 算术运算扩展的运算数只能是整数 [rootlocalhost tmp]# num1$[41] [rootlocalhost tmp]# echo …

软件测试肖sir__python之ui自动化测试框架unittest

ui自动化测试框架unittest 一、自动化框架 1、自动化框架类型 &#xff08;1&#xff09;unittest框架 &#xff08;python中自带框架&#xff09; &#xff08;2&#xff09;pytest框架 &#xff08;第三方库&#xff09; &#xff08;3&#xff09;po框架 &#xff08;分层思…

10.一篇文章带你理解及使用CSS(前端邪术-化妆术)

文章目录 1. CSS 是什么2.基本语法规范3.引入方式3.1内部样式表3.2行内样式表3.3外部样式 4.代码风格4.1样式格式4.2样式大小写4.3空格规范 5.选择器5.1选择器的功能5.2选择器的种类5.3基础选择器5.3.1标签选择器5.3.2类选择器5.3.3 id 选择器5.3.4通配符选择器5.3.5基础选择器…

pytest自动化测试数据驱动yaml/excel/csv/json

这篇文章主要为大家介绍了pytest自动化测试数据驱动yaml/excel/csv/json的示例详解&#xff0c;有需要的朋友可以借鉴参考下。− 数据驱动 数据的改变从而驱动自动化测试用例的执行&#xff0c;最终引起测试结果的改变。简单说就是参数化的应用。 测试驱动在自动化测试中的应…

初始C++入门(2)

缺省参数 缺省参数是 声明或定义函数时 为函数的 参数指定一个缺省值 。在调用该函数时&#xff0c;如果没有指定实 参则采用该形参的缺省值&#xff0c;否则使用指定的实参。 有句俗话叫&#xff0c;做人不能像缺省函数。 #include<iostream> using namespace std;void…

C# winform如何实现数据的保存和读取

在c#winform中我们在写程序时&#xff0c;经常需要进行数据处理&#xff0c;那么数据如何保存和读取&#xff08;下面我们通过序列化和反序列化的方式来实现&#xff09; 第一步: 我们建立一个winform窗体 第二步: 构建一个外部实体类&#xff08;Student类&#xff09; 第…

再谈String

文章目录 一、字符串常量池1.1 创建对象的思考1.2 字符串常量池(StringTable)1.3 再谈String 对象创建 一、字符串常量池 1.1 创建对象的思考 下面创建String对象的方式相同吗&#xff1f; public static void main(String[] args) {String s1 "hello";String s2…

Java日志系统之Slf4j

目录 Slf4j Slf4j的简单使用 Slf4j的日志绑定流程 Slf4j桥接器 Slf4j Slf4j又称简单日志门面&#xff0c;Slf4j主要是为了给Java日志访问提供一套标准&#xff0c;规范的API框架&#xff0c;其主要意义在于提供接口&#xff0c;具体的实现可以交由其他的日志框架。 Slf4j…

概念解析 | 心脏电活动和机械活动之间的关系

注1:本文系“概念解析”系列之一,致力于简洁清晰地解释、辨析复杂而专业的概念。本次辨析的概念是:心脏电活动和机械活动之间的关系。 心跳的交响乐:心脏电活动与机械活动之间的关联 一、背景介绍 心脏通过不断跳动将血液输送到我们身体的每一个角落。而这个跳动过程,是…

【Godot】【BUG】4.x NavigationAgent 导航不生效

4.2.beta2 试了半天才发现原来默认只对第一个有导航的 TileMap 的第 1 层 生效&#xff0c;而我设置的导航层不是第一层&#xff0c;然后我新建了一个 TileMap 将导航的瓦片设置到这个 TileMap 上了&#xff0c;如图 这样就解决了问题&#xff0c;不用再修改默认设置的东西了&a…

超详细 | 差分进化算法原理及其实现(Matlab/Python)

差分进化(Differential Evolution&#xff0c;DE)算法是由美国学者Storn和 Price在1995年为求解Chebyshev多项式拟合问题而提出的。算法主要通过基于差分形式的变异操作和基于概率选择的交叉操作进行优化搜索&#xff0c;虽然其操作名称和遗传算法相同&#xff0c;但实现方法有…

英语——词根篇——单词——f

文章目录 31 . factfactdo,make做&#xff0c;作&#xff08;fact也作fac&#xff09; 32.ferferbring&#xff0c;carry带&#xff0c;拿 33 . florflorflower花&#xff08;flow也作flour&#xff09; 34 . flufluflow流 35 . fusfaus pour灌&#xff0c;流&#xff0c;倾泻 …

IOS屏幕旋转监听

1.设计窗口,添加三个按钮 2.添加事件连接 3.按钮点击事件实现 先添加三个IBAction 实现IBAction 使用旋转立刻生效 -(IBAction)btnFixPortrait:(id)sender{//访问应用程序委托成员_app.mask UIInterfaceOrientationMaskPortrait;//设置窗口旋转属性[self setNeedsUpdateOf…

有效管理token,充分发挥ChatGPT的能力

目录 给提供了 Token 的计算工具,来理解一下Token的计算方式,网址如下: 窗口如下: 实际消耗 Token 数量为 59个,换算之后为2.1-2.2的比例,即一个汉字消耗2.12.2个Token, 再测一下英文的Token消耗,包含空格在内,一共52个英文字母,消耗Token 13个,正好对应13个单词,…