malloc和free和new和delete区别
重写重载重定义
封装继承多态
默认构造函数,拷贝构造函数,析构函数,赋值函数
深拷贝浅拷贝
C和C++的区别
C是面向过程的语言,是一个结构化的语言,考虑如何通过一个过程对输入进行处理得到输出;C++是面向对象的语言,主要特征是“封装、继承和多态”。封装隐藏了实现细节,使得代码模块化;派生类可以继承父类的数据和方法,扩展了已经存在的模块,实现了代码重用;多态则是“一个接口,多种实现”,通过派生类重写父类的虚函数,实现了接口的重用。
C和C++动态管理内存的方法不一样,C是使用malloc/free,而C++除此之外还有new/delete关键字。
C++中有引用,C中不存在引用的概念
在C++中,函数原型必不可少,但是在C中是可选的。这一区别在声明一个函数时让函数名后面的圆括号为空,就可以看出来。在C中,空圆括号说明这是前置原型,但是在C++中则说明该函数没有参数。也就是说,在C++中,intslice();和int slice(void);相同。例如,下面旧风格的代码在C中可以接受,但是在C++中会产生错误:
变量的声明和定义有什么区别
为了支持分离式编译,C++语言将声明和定义区分开来。声明(declaration)使得名字为程序所知,一个文件如果想使用别处定义的名字则必须包含对那个名字的声明。而定义(definition)负责创建与名字关联的实体
什么是分离式编译? 分离编译模式源于C语言,在C++语言中继续沿用。简单地说,分离编译模式是指:一个程序(项目)由若干个源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有目标文件连接起来形成单一的可执行文件的过程为变量分配地址和存储空间的称为定义,不分配地址的称为声明。
为变量分配地址和存储空间的称为定义,不分配地址的称为声明。一个变量可以在多个地方声明,
但是只在一个地方定义。加入 extern 修饰的是变量的声明,说明此变量将在文件以外或在文件后面部分定义。
说明:很多时候一个变量,只是声明不分配内存空间,直到具体使用时才初始化,分配内存空间,
如外部变量。
可以通过在构造函数的定义里的作用域前面加:后面加成员属性进行初始化
编译型语言执行流程(编译是把高级语言变成计算机可识别的汇编语言)
源代码 预处理→编译器→目标代码→链接器(链接库代码和启动代码)→可执行代码(汇编语言)
预处理主要处理文本信息#include "stdafx.h"/预编译头
1 Switch
#include <iostream>
using namespace std;
int main()
{
int Num=0;
cin>>Num;
switch (Num)
{
case 1: //标签,输入内容为switch里唯一存在的常量或枚举,注意是:不是;
cout<<"a"<<endl;
break;
case 2:
cout<<"b"<<endl;
break;
default: //默认执行这个
cout<<"c"<<endl;
break;
}
}
2 enum枚举
#include <iostream>
using namespace std;
//构建一个枚举类
enum EColor:char //不加:char则默认是int类型,枚举必须是整形 可选类型:char,short,int,long,long long
{
Red, //注意是逗号不是分号,Red值为0
Blue=10, //Blue值为10
Green, //Green值为11
}; //注意枚举后面有个分号
int main()
{
EColor TColor = EColor::Blue;
cout << sizeof(EColor) << endl; //1,默认int则为4
//枚举的一种判断方式
if (TColor == EColor::Blue)
{
cout << "OK" << endl;
}
//switch加枚举
switch (TColor)
{
case Red:
break;
case Blue:
break;
case Green:
break;
default:
break;
}
}
函数形参与实参
形参:在声明函数名和函数体时使用的参数,接受调用该函数时传递的参数(形参是抽象的一个概念)
实参:在调用时传递给函数的参数,传递给被调用参数的值(对我来说 实参是个对象,需要实际的值)
形参只有在被调用时才分配内存单元,在调用结束时即刻释放所分配的内存单元。形参只在函数内部有效。函数调用结束后则不能再使用该形参变量
在调用函数过程中,系统会把实参的值传递给被调用函数的形参。或者说,形参从实参得到一个值。该值在函数调用期间有效,可以参加该函数中的运算
在一般传值调用的机制中只能把实参传递给形参,而不能把形参传送给实参(但是可以拷贝)。因此在函数调用中,形参值发生变化,而实参中的值不会变化。而在引用调用的机制中是将实参应用的地址传递给了形参,因此发生在形参上的改变也会改变实参
3 struct结构体
由一系列具有相同类型或不同类型的数据构成的数据集合,是一种数据形式(为了整合数据)
结构体里面也可以存行为(函数),但结构体内的行为不受保护,类内的函数自带private
#include <iostream>
using namespace std;
struct Box
{
int Id = 10; //注意是分号
float Height = 5.5f; //默认初始化,实例时若不给实例赋值则为该值
void Say();
}; //注意有分号,且分号前面可以直接实例一个结构体对象
void Box::Say()
{
cout<<"hello"<<endl;
}
int main()
{
//Say(); 无效
//Box::Say(); 无效
Box box; //若没有默认初始化则可以加{}里面填入相应值,注意赋值要按顺序从头开始哪怕是默认初始化过了,而且不是=而是直接Box box{10,5.9f};
box.Say(); //hello
cout << box.Height << endl; //5.5
}
结构体的大小
数据对齐:结构体中计算数据大小时,将向最大数据类型看齐,其他数据类型则采用对齐方案。即小数据进行拼接,如果无法完整填充一个最大数据大小,则拼接,如果超过则开启一个新的最大数据类型大小,再次填充
struct Box
{
char A; //1
short B; //2+2
int C; //2+2+4
char D; //4(空1个)+4+4(最后一个4空3个)
}; //最终实例的大小为12
struct Box
{
char A; //1
short B; //2+2
char D; //2+2+2
int C; //4+4+4
}; //最终实例的大小为12
4 union共用体
是一种数据类型,能够存储不同的数据类型,将他们放在同一块内存中,但是只能同时使用其中的一种类型。共用体只能存储基本数据类型,如整型,浮点型,布尔型
在同一时间共用体只有一个数据有效,同一时刻只能有效存储一种数据
应用场景:解决硬件平台内存吃紧问题,共用体可以灵活的应对不同的数据类型需求,根据需求进行调整
共用体的大小:最大数据类型的大小
#include <iostream>
using namespace std;
union UBox
{
int A;
short B;
char C;
};
int main()
{
UBox box;
box.A = 10;
cout<<box.A<<endl;
return 0;
}
5 class类
用来抽象描述对象属性行为的模板
类本质是一种数据类型,需要构建实例
在C++中默认产生6个类成员函数,即缺省函数,编译器只会在需要的时候生成它们:(空类会有1个字节大小的占位符——空类也是可以被实例化的,而每个实例在内存中都是独一无二的地址,为了达到这个目的,编译器往往会给一个空类隐含的加一个字节,这样空类在实例化后就可以得到了独一无二的地址,这就是一个占位符的意义)
缺省构造函数
缺省拷贝构造函数
缺省析构函数
缺省赋值运算符
缺省取址运算符
缺省取地址运算符const
#include <iostream>
using namespace std;
class Animal
{
public:
Animal(); //构造函数
//写在public域则别人可以任意构建实例对象→需要对象
//写在protected域的作用?子类可以构建实例对象→
//写在private域则实例对象都无法访问其构造函数→单例模式
~Animal();
protected:
private:
};
Animal::Animal()
{
cout << "OK" << endl;
}
Animal::~Animal()
{
cout << "guale" << endl;
}
int main()
{
Animal xiaohong; //OK
return 0;
}
封装(private,protected,public)
private私有,修饰的成员属性或函数,只允许本类内进行访问,子类无法访问,类实例无法访问
protected受保护,修饰的成员属性或函数,只允许子类及本类访问,类的实例无法访问
public公有访问,修饰的成员属性或函数,子类和本类所有均可以访问,类的实例也可以访问
#include <iostream>
#include <string>
using namespace std;
class Animal
{
public:
int Age;
string Name;
protected:
private:
};
int main()
{
Animal xiaohong;
xiaohong.Age = 10;
xiaohong.Name = "笑哄";
cout << xiaohong.Name << endl;
return 0;
}
构造函数(帮助类的成员属性初始化)
一种特殊的类的成员函数,没有返回类型,构造函数的名称必须与类名相同,对象被创建时会调用构造函数,我们可以显式定义构造函数,并重载更多的构造函数。如果不显示编写构造函数,系统将提供默认的无参构造函数
如果显示重写构造函数,并标注了构造函数的访问关键字非public,则此类无法被创建出对象,如果重写构造函数带参数,则创建时只能使用已有的构造函数进行创建
#include <iostream>
using namespace std;
class Animal
{
public:
Animal();
~Animal();
protected:
private:
};
Animal::Animal()
{
cout << "OK" << endl;
}
Animal::~Animal()
{
cout << "guale" << endl;
}
int main()
{
Animal xiaohong; //OK
return 0;
}
构造函数:主要用来在创建对象时完成成员变量初始化操作,创建对象时会默认调用构造函数,所以无需主动调用构造函数。允许在一个类中编写多个构造函数,但必须是重载
构造函数的三个作用:
创建的对象建立一个标识符
为对象数据成员开辟存储空间
完成对象数据成员的初始化
析构函数(析构函数没有参数)
在对象被释放时调用,在函数名前加~符号
(指针对象进行释放时用)
应用:指针释放时(内存泄漏 自身指针释放成功,但里面还有没被释放)
存储空间(不同存储空间用来区分不同数据内容)
栈(栈大小一般小于2GB)
由编译器自动分配并释放,存放的是函数的参数值,局部变量等,方法调用的实参也是保存在栈区的。栈是系统数据结构,对应线程/进程是唯一的。优点快速高效,缺点是有限制,数据不灵活
堆
堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储空闲内存空间的,自然是不连续的,而链表的遍历方向是由低地址向高地址的。
特点:堆的大小受限于计算机系统中有效的虚拟内存,所以堆获得的空间比较灵活,也比较大,但速度相对慢一些,也容易产生内存泄漏问题
在C++中,构建自定义对象(类对象)应优先考虑使用堆空间。堆空间需要主动申请,借助关键字new进行申请,当不使用空间时需要及时释放
全局/静态存储(常驻内存的值)
全局变量和静态变量时放在一起的,初始化的全局变量和静态变量存放在一块区域,未初始化的全局变量和静态变量在相邻的另一块区域,程序结束后由系统释放
申请了就一直存在没法释放它,程序不结束一直存在,程序结束才释放
常量存储(常驻内存的值)
存放常量,不允许修改(通过非正当手段也可以修改)。随着程序的结束而消亡
找的速度快
代码区
存放代码(如函数),不允许修改(类似常量存储区),但可以执行(不同于常量存储区)
栈和堆的区别
对于栈来讲,是由编译器自动管理,速度快,空间有限,无需手动控制
对于堆来讲,申请和释放工作由程序员控制,速度相对慢一些,空间大,容易产生memory leak(内存泄漏)
内存泄漏
在程序运行期间,程序员主动进行堆中的内存申请。这些内存程序不进行管理的,由申请的人进行管理,而我们在使用期间,对于不使用的内存忽略了释放,那么这些内存将会无法被回收,重复利用,导致了内存越来越少,出现了泄露
指针
new(在堆中使用内存空间)
构造函数本质是无法被直接调用的,但可以通过和new关键字结合使用,用来在堆中申请空间存储对象,并将堆中的地址进行返回
new运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例。返回地址,我们将地址存放在指针中。通过操作指针即可操作堆空间中的数据
int *p=new int(5665);(在堆中开辟了一个int大小的空间,向空间中存入5665这个数,并将空间的首地址进行返回,存到了p指针上)
delete(释放堆中空间)(只是说这块堆里的地址可以被再利用了,并没有改变里面的值)
不要对已经释放的对象再进行操作,否则会崩溃
使用delete关键字进行指针的空间释放,需要将指针指向NULL(0)(nullptr),防止误操作。主动调用delete关键字释放自定义数据指针时,析构函数将被调用
class People
{
}
int main()
{
People* aha=new People();
delete aha; //调用析构函数 切记先delete释放堆中内存,再将指针指向为空,否则你就是在放屁!!!
aha=0; //切记要指向为空 否则你就是没擦屁!!!
}
*跟在指针前就是解地址符——获得当前地址的数据(以当前指针类型来解析大小)
int *p=new int(5665);
cout<<*p<<endl; //5665
& —— 按位与 取地址符 引用
&只要在变量名前,在赋值运算符后就是取地址符
int Num=1;
int* a=&Num; //a是一个指向Num地址的指针
this指针
this是一个特殊的关键字,旨在帮助我们在对象内部获得自身的引用。this不是对象的属性,不会影响对象的大小,this的作用域在类内,外部无法访问操作,只能在成员函数内使用,代表当前操作对象的指针,由编译器构造的一个隐形的操作变量
野指针
指针本身存储了一个内存地址值,但是地址指向的空间已经被标记为删除回收或是访问受限
产生原因
1.构建指针时没有给予指针进行初始化(系统随机塞一个值)
2.指针释放时没有将指针指向空
引用(理解成小名)
地址相同(在该数据旁边又给他写一个名字)
引用的作用:
帮助我们在函数中传递时减少拷贝消耗,从而可以直接将形参与实参绑定,达到修改形参就可以修改实参的目的
虚函数纯虚函数
纯虚函数是在基类中声明的虚函数,在基类中没有定义,但要求任何派生类都要定义自己的实现方法。
在基类的虚函数后面加=0,定义纯虚函数的类为抽象类
抽象类无法被实例化,若其继承类没有实现纯虚函数,则其也成为抽象类
纯虚函数强化了结构的概念,如果我们编写父类的函数时,子类需要对其进行扩展,则使用虚函数。即父类只完成了功能的一部分,子类需要继续完成
6 Const(保护作用,别人不能动)
const修饰全局或局部基本数据类型——const int a=10;(必须初始化)
const修饰全局或基本数据类型指针
const int* p=new int(10); //指针指向的内容不可以被修改
int* const p=new int(10); //指针值不可被修改
const修饰引用(作用:减少拷贝,同时起到保护的目的)——const int& b=a;
const修饰成员属性——需要在构造函数或在类声明时进行初始化
const修饰函数参数——不可在函数内进行更改参数值
const修饰返回指针值——外面接的时候也要用const指针去接,否则可以*指针 可以抹去常量特性(目的是保护了返回内容的安全性)
const int* Say(int* const num)
{
return new int(100); //自动转换成const指针
}
int main()
{
int num=50;
const int* a=Say(&num); //必须拿const指针去接
return 0;
}
const修饰成员函数(const挂载到成员函数的末尾——只能在末尾)——成员函数为常函数,表明当前函数禁止修改对象的成员变量,并且在当前函数中只能调用常函数
#pragma once
class Hero
{
public:
int Num;
int ChangeNum(int num);
int GetNum() const;
};
#include "Hero.h"
#include <iostream>
using namespace std;
int Hero::ChangeNum(int num)
{
Num = num + 1;
return num;
}
int Hero::GetNum() const
{
//Num=20; //常函数内不允许修改成员属性值
//ChangeNum(num); //常函数内不允许调用其他非常函数
return Num;
}
int main()
{
Hero* p = new Hero();
p->Num = 10; //常函数外可以更改
p->ChangeNum(p->Num); //合法
cout << p->GetNum() << endl; //11
return 0;
}
const修饰自定义数据类型对象(const挂在到自定义数据类型的最前面)——指针只能调用常函数,或是获取成员属性,不能修改成员属性
#include "Hero.h"
#include <iostream>
using namespace std;
int Hero::ChangeNum(int num)
{
Num = num + 1;
return num;
}
int Hero::GetNum() const
{
//Num=20; //常函数内不允许修改成员属性值
//ChangeNum(num); //常函数内不允许调用其他非常函数
return Num;
}
int main()
{
const Hero* p = new Hero(); //const修饰自定义数据类型
//p->Num = 10; //不能修改成员属性
//p->ChangeNum(p->Num); //不能调用非常函数
cout << p->GetNum()+1 << endl; //1
return 0;
}
命名空间(作用:减少命名冲突的可能性)
由于编码过程中,C++类非常的庞大,我们不可能熟悉类库的过程中操作或是覆盖了已有的函数或是对象,导致不可预知的bug,在一定程度上增加了名称冲突的可能性
写法一:using namspace std; //都有
写法二:using std::cin; //只有cin前面可以省略std
全局命名空间(在全局域),普通命名空间(namespace UE4{int a;}),嵌套命名空间(一个命名空间里面有另一个 调用时需要命名空间1::命名空间2::里面的东西),内联命名空间(inline可以省略嵌套中的命名空间2),未命名命名空间(没有命名空间名字,容易和全局起歧义)
Static(只会初始化一次)
构建静态操作函数或是静态变量的关键字,主要目的是将内容静态化。静态内容无法被主动释放,只有随着程序的结束而结束
(全局变量无法被释放,常量也无法被释放)
static可以修饰变量(常量)也可以修饰函数
面向过程的static:
静态全局变量(未被初始化则自动初始化,只在声明的cpp文件内可见,不同cpp中允许有相同名称因为不是一个),
static int* num; //初始化为nullptr
静态全局常量(所有常量都需要声明后初始化,只在声明的cpp文件内可见)
static const int num=10;
静态局部变量(程序执行到该处时被初始化后以后的函数调用不再进行初始化,未被初始化则自动初始化,静态局部变量的作用域为局部作用域)
#include <iostream>
using namespace std;
void Say()
{
static int num=1; //未加static则结果为2 2 2
num++;
cout << num << endl;
}
int main()
{
for (int i = 0; i < 3; i++)
{
Say(); //2 3 4
}
return 0;
}
静态全局函数(如声明在头文件中,定义必须写在头文件中,使用需要引入头文件;声明在cpp中,允许在不同的cpp中存在命名相同的静态全局函数;静态全局函数只能操作全局类型数据)
面向对象的static:
静态成员变量:
在类内部定义静态变量称为类的静态成员变量,受访问修饰符约束。禁止在类声明的地方初始化,禁止在初始化参数列表中初始化,需要在CPP文件中初始化(只一次即可 ,如果声明在头文件中需要引入头文件),不初始化无法使用
只会初始化一次
静态成员常量:
#include <iostream>
using namespace std;
class People
{
public:
People();
~People();
int Num=6;
static int a; //静态成员变量理解为类的成员而不是对象的成员,数据变化则整个类的所有实例中都发生变化
static const int b; //静态成员常量
};
int People::a = 6; //静态成员变量初始化
const int People::b=8; //静态成员常量初始化
People::People()
{
cout << "gggggzzzzz" << endl; //构造函数
}
People::~People()
{
cout << "ggggglllll" << endl; //析构函数
}
int main()
{
People* p = new People(); //ggggggzzzzz
People* p2 = new People(); //gggggzzzzz
p->Num++;
p->a++; //People::a=7
cout << p->Num << endl; //7
cout << p2->Num << endl; //6
cout << p->a << endl; //7
cout << p2->a << endl; //7
//cout<<People::a<<endl; 静态成员变量这么写,不要从实例里去访问
cout << People::b << endl; //8
delete p; //ggggglllll
p = 0;
delete p2; //ggggglllll
p2 = 0;
return 0;
}
静态成员函数:
也是属于类而不是实例对象,不能调用类里的非静态成员变量
应用:单例模式
将一个类变为对外,只有一个实例入口(如关卡蓝图、场景管理器等),负责建立沟通桥梁
特点:
某个类只能有一个实例
某个类实例必须由自身创建(方法:构造函数私有化)
这个实例由某个类向整个系统提供
因此
将构造函数写在private域里——使得子例和实例对象都无法访问其构造函数,别人只能通过类内的另一个成员函数返回它的new实例对象(懒汉模式2.1)或通过一个成员实例属性(饿汉模式2.2)来拿到这个类的实例对象
2.1 (懒汉模式——在第一次被使用时才构建类实例)类的成员函数里是怎么拿到它的实例的?我们因为只想要有一个实例,只初始化一次它的实例对象,所以选择静态成员函数,其只会被初始化一次
2.2 (饿汉模式——在类中有一个静态成员实例指针(不是静态则会构建多次))在全局域进行初始化
懒汉模式
懒汉模式:唯一的类实例只有在第一次被使用时才构建
饿汉模式:唯一的类实例在类装载时构建
虚函数表
https://blog.youkuaiyun.com/Primeprime/article/details/80776625?spm=1001.2014.3001.5506
每个包含了虚函数的类都包含一个虚表。如果一个基类包含了虚函数,那么其继承类也可调用这些虚函数,换句话说,一个类继承了包含虚函数的基类,那么这个类也拥有自己的虚表
虚表:虚表是一个指针数组,其元素是虚函数的指针,每个元素对应一个虚函数的函数指针。需要指出的是,普通的函数即非虚函数,其调用并不需要经过虚表,所以虚表的元素并不包括普通函数的函数指针。
虚表内的条目,即虚函数指针的赋值发生在编译器的编译阶段,也就是说在代码的编译阶段,虚表就可以构造出来了
虚表指针:对象的虚表指针用来指向自己所属类的虚表,虚表中的指针会指向其继承的最近的一个类的虚函数
为了指定对象的虚表,对象内部包含一个虚表的指针,来指向自己所使用的虚表。为了让每个包含虚表的类的对象都拥有一个虚表指针,编译器在类中添加了一个指针,*__vptr,用来指向虚表。这样,当类的对象在创建时便拥有了这个指针,且这个指针的值会自动被设置为指向类的虚表。
动态绑定:
通过指针来调用函数
指针upcast向上转型(继承类向基类的转换称为upcast,关于什么是upcast,可以参考本文的参考资料)
调用的是虚函数
如果一个函数调用符合以上三个条件,编译器就会把该函数调用编译成动态绑定,其函数的调用过程走的是上述通过虚表的机制。
数组和指针
数组的地址是连续的,访问速度最快,传统数组(定长,无法动态扩容,无法主动获得大小)
动态数组
在运行阶段初始化数组
数据结构
MAP
动态数组
链表
红黑树
哈希表
TCP
UTP
HTTP
共享指针 共享引用 弱指针
//lua 的tail 和itail
//drawcall
深度优先
排序
A星算法
虚函数表
Vector List Map
STL库
把各个数据结构给写出来
写一些算法的实现
同时读那些知识
各个缺省函数为什么要写在不同的访问修饰符里
1. C++可以使用一个int类型来存储多个bool型变量,试写出从该int中对某个bool赋值、取值的代码。
一个int可以有32种状态即存储32个bool型变量
代码:
#include <iostream>
using namespace std;
int main()
{
int A = 0; //将所有bool置为false
int N = 1;
bool B = A >> (N - 1) & 0x1; //取A的二进制第N位上的值 取值
cout << A << " " << B << endl;
A = 1 << (N - 1) | A; //将A的二进制第N位上的数改为1
B = A >> (N - 1) & 0x1; //取A的二进制第N位上的值 赋值给boolB
cout << A << " " << B << endl;
A = ~(1 << (N - 1)) &A; //将A的二进制第N位上的数改为0
B = A >> (N - 1) & 0x1; //取A的二进制第N位上的值 赋值给boolB
cout << A << " " << B << endl;
}
封装继承多态
封装:是指将数据和行为进行封装,数据被保护在数据成员中,只能通过方法来进行访问和修改。封装的目的是将数据和行为组合成一个整体,隐蔽对象的内部细节,使得用户无需了解具体实现细节,只需通过接口来进行操作。
继承:是指一个新的类从已有的类继承了一些数据和方法,并可以添加新的数据和方法。继承的目的是实现代码的复用和扩展。
多态:是指通过不同的方式使用同一个方法,而产生不同的结果。多态分为静态多态和动态多态。静态多态是通过函数重载和运算符重载实现的;动态多态是通过虚函数和继承实现的。
C++中使用封装、继承和多态可以提高代码的复用性、灵活性和可维护性,同时也让代码更易于理解和扩展。
重写重载重定义
重写(override):是指在子类中重新定义基类中已有的虚函数,并使用override关键字进行标记。重写的目的是为了实现多态,从而让基类指针或基类引用可以指向子类对象并正确执行子类中的方法。
重载(overload):是指在同一个类中定义多个具有相同名称但参数列表不同的函数,也被称之为函数重载。重载的目的是为了通过同一个函数名实现多种不同的功能,从而提高代码复用性和可读性。
重定义(redefine):是指在同一个类或者在派生类中重新定义与基类同名、同参数列表的非虚函数。重定义的目的是为了在派生类中覆盖基类中的同名函数,从而实现新的功能或者改变原有的行为。
总体而言,重写和重载是多态的实现手段,而重定义则是OOP中的继承特性之一。重写和重载都必须基于继承和多态的基础之上来实现,而重定义则仅仅是继承的一种实现方式。