本文最后更新于:2024年1月14日 晚上

C++ 知识点记录。

左值(Lvalues)和右值(Rvalues)

C++ 中有两种类型的表达式:

  • 左值(lvalue): 指向内存位置的表达式被称为左值(lvalue)表达式。左值可以出现在赋值号的左边或右边。
  • 右值(rvalue): 术语右值(rvalue)指的是存储在内存中某些地址的数值。右值是不能对其进行赋值的表达式,也就是说,右值可以出现在赋值号的右边,但不能出现在赋值号的左边。变量是左值,因此可以出现在赋值号的左边。数值型的字面值是右值,因此不能被赋值,不能出现在赋值号的左边。

Extern

变量声明向编译器保证变量以给定的类型和名称存在,这样编译器在不需要知道变量完整细节的情况下也能继续进一步的编译。变量声明只在编译时有它的意义,在程序连接时编译器需要实际的变量声明。

当您使用多个文件且只在其中一个文件中定义变量时(定义变量的文件在程序连接时是可用的),变量声明就显得非常有用。您可以使用 extern 关键字在任何地方声明一个变量。虽然您可以在 C++ 程序中多次声明一个变量,但变量只能在某个文件、函数或代码块中被定义一次。

我们知道,程序的编译单位是源程序文件,一个源文件可以包含一个或若干个函数。在函数内定义的变量是局部变量,而在函数之外定义的变量则称为外部变量,外部变量也就是我们所讲的全局变量。它的存储方式为静态存储,其生存周期为整个程序的生存周期。全局变量可以为本文件中的其他函数所共用,它的有效范围为从定义变量的位置开始到本源文件结束。

然而,如果全局变量不在文件的开头定义,有效的作用范围将只限于其定义处到文件结束。如果在定义点之前的函数想引用该全局变量,则应该在引用之前用关键字 extern 对该变量作“外部变量声明”,表示该变量是一个已经定义的外部变量。有了此声明,就可以从“声明”处起,合法地使用该外部变量。

来看一个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
int max(int x,int y);
int main(void)
{
int result;
/*外部变量声明*/
extern int g_X;
extern int g_Y;
result = max(g_X,g_Y);
printf("the max value is %d\n",result);
return 0;
}
/*定义两个全局变量*/
int g_X = 10;
int g_Y = 20;
int max(int x, int y)
{
return (x>y ? x : y);
}

代码中,全局变量 g_X 与 g_Y 是在 main 函数之后声明的,因此它的作用范围不在 main 函数中。如果我们需要在 main 函数中调用它们,就必须使用 extern 来对变量 g_X 与 g_Y 作“外部变量声明”,以扩展全局变量的作用域。也就是说,如果在变量定义之前要使用该变量,则应在使用之前加 extern 声明变量,使作用域扩展到从声明开始到本文件结束。

如果整个工程由多个源文件组成,在一个源文件中想引用另外一个源文件中已经定义的外部变量,同样只需在引用变量的文件中用 extern 关键字加以声明即可。下面就来看一个多文件的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/****max.c****/
#include <stdio.h>
/*外部变量声明*/
extern int g_X ;
extern int g_Y ;
int max()
{
return (g_X > g_Y ? g_X : g_Y);
}

/***main.c****/
#include <stdio.h>
/*定义两个全局变量*/
int g_X=10;
int g_Y=20;
int max();
int main(void)
{
int result;
result = max();
printf("the max value is %d\n",result);
return 0;
}

对于多个文件的工程,都可以采用上面这种方法来操作。对于模块化的程序文件,可在其文件中预先留好外部变量的接口,也就是只采用 extern 声明变量,而不定义变量,max.c 文件中的 g_X 与 g_Y 就是如此操作的。

通常,这些外部变量的接口都是在模块程序的头文件中声明的,当需要使用该模块时,只需要在使用时具体定义一下这些外部变量即可。main.c 里的 g_X 与 g_Y 则是相关示例。

不过,需要特别注意的是,由于用 extern 引用外部变量,可以在引用的模块内修改其变量的值,因此,如果有多个文件同时要对应用的变量进行操作,而且可能会修改该变量,那就会影响其他模块的使用。因此,我们要慎重使用。

于指示 C 或者 C++函数的调⽤规范。⽐如在 C+ + 中调⽤ C 库函数,就需要在 C++ 程序中⽤ extern “C” 声明要引⽤的函数。这是给链接器 ⽤的,告诉链接器在链接的时候⽤C 函数规范来链接。主要原因是 C++ 和 C 程序编译完成 后在⽬标代码中命名规则不同,⽤此来解决名字匹配的问题。

注意 extern 声明的位置对其作⽤域也有关系,如果是在 main 函数中进⾏声明的,则只能在 main 函数中调⽤,在其它函数中不能调⽤。其实要调⽤其它⽂件中的函数和变量,只需把该 ⽂件⽤ #include 包含进来即可,为啥要⽤ extern?因为⽤ extern 会加速程序的编译过程,这 样能节省时间。

static

作⽤⼀:修饰局部变:⼀般情况下,对于局部变在程序中是存放在栈区的,并且局部的⽣ 命周期在包含语句块执⾏结束时便结束了。但是如果⽤ static 关键字修饰的话,该变便会存 放在静态数据区,其⽣命周期会⼀直延续到整个程序执⾏结束。但是要注意的是,虽然⽤ static 对局部变进⾏修饰之后,其⽣命周期以及存储空间发⽣了变化,但其作⽤域并没有改 变,作⽤域还是限制在其语句块。

  • (1)该变量在全局数据区分配内存(局部变量在栈区分配内存);
  • (2)静态局部变量在程序执行到该对象的声明处时被首次初始化,即以后的函数调用不再进行初始化(局部变量每次函数调用都会被初始化);
  • (3)静态局部变量一般在声明处初始化,如果没有显式初始化,会被程序自动初始化为0(局部变量不会被初始化);
  • (4)它始终驻留在全局数据区,直到程序运行结束。但其作用域为局部作用域,也就是不能在函数体外面使用它(局部变量在栈区,在函数结束后立即释放内存);

作⽤⼆:修饰全部变:对于⼀个全局变,它既可以在本⽂件中被访问到,也可以在同⼀个 ⼯程中其它源⽂件被访问(添加 extern进⾏声明即可)。⽤ static 对全局变进⾏修饰改变了其 作⽤域范围,由原来的整个⼯程可⻅变成了本⽂件可⻅。

  • 静态全局变量不能被其它文件所用(全局变量可以);
  • 其它文件中可以定义相同名字的变量,不会发生冲突(自然了,因为static隔离了文件,其它文件使用相同的名字的变量,也跟它没关系了);

作⽤三:修饰函数:⽤ static 修饰函数,情况和修饰全局变类似,也是改变了函数的作⽤域。

  1. 静态函数不能被其它文件所用;
  2. 其它文件中可以定义相同名字的函数,不会发生冲突;

作⽤四:修饰类:如果 C++ 中对类中的某个函数⽤ static 修饰,则表示该函数属于⼀个类⽽不是属于此类的任何特定对象;如果对类中的某个变进⾏ static 修饰,则表示该变以及所 有的对象所有,存储空间中只存在⼀个副本,可以通过;类和对象去调⽤。(补充:静态⾮常数据成员,其只能在类外定义和初始化,在类内仅是声明⽽已。)

作⽤五

  • 类成员/类函数声明 static 函数体内 static 变的作⽤范围为该函数体,不同于 auto 变量,该变量的内存只被分配 ⼀次,因此其值在下次调⽤时仍维持上次的值;
  • 在模块内的 static 全局变量可以被模块内所⽤函数访问,但不能被模块外其它函数访问;
  • 在模块内的 static 函数只可被这⼀模块内的其它函数调⽤,这个函数的使⽤范围被限制在 声明它的模块内;
  • 在类中的 static 成员变属于整个类所拥有,对类的所有对象只有⼀份拷⻉;
  • 在类中的 static 成员函数属于整个类所拥有,这个函数不接收 this 指针,因⽽只能访问类 的 static 成员变。
  • static 类对象必须要在类外进⾏初始化,static 修饰的变先于对象存在,所以 static 修 饰的变要在类外初始化; 由于 static 修饰的类成员属于类,不属于对象,因此 static 类成员函数是没有 this 指针, this 指针是指向本对象的指针,正因为没有 this 指针,所以 static 类成员函数不能访问⾮ static 的类成员,只能访问 static修饰的类成员;
  • static 成员函数不能被 virtual 修饰,static 成员不属于任何对象或实例,所以加上 virtual 没有任何实际意义;
  • 静态成员函数没有 this 指针,虚函数的实现是为每⼀个对象分配⼀个 vptr 指针,⽽ vptr 是通过 this 指针调⽤的,所以不能为 virtual;虚函数的调⽤关系, this->vptr->ctable->virtual function。

对于非静态数据成员,每个类对象(实例)都有自己的拷贝。而静态数据成员被当作是类的成员,由该类型的所有对象共享访问,对该类的多个对象来说,静态数据成员只分配一次内存。 静态数据成员存储在全局数据区。静态数据成员定义时要分配空间,所以不能在类声明中定义。

静态成员函数不能访问非静态(包括成员函数和数据成员),但是非静态可以访问静态。

const

const 修饰基本类型数据类型

基本数据类型,修饰符 const 可以⽤在类型说明符前,也可以 ⽤在类型说明符后,其结果是⼀样的。

在使⽤这些常量的时候,只要不改变这些常量的值即 可。

const 修饰指针变量和引⽤变量

如果 const 位于⼩星星的左侧,则 const 就是⽤来修饰指针 所指向的变量,即指针指向为常量;

如果 const 位于⼩星星的右侧,则 const 就是修饰指针 本身,即指针本身是常量。

const 应⽤到函数中

作为参数的 const 修饰符:调⽤函数的时候,⽤相应的变量初始化 const 常量,则在函数体中,按照 const 所修饰的部分进⾏常量化,保护了原对象的属性。

[注意]:参数 const 通常⽤于参数为指针或引⽤的情况; 作为函数返回值的 const 修饰符:声 明了返回值后,const 按照"修饰原则"进⾏修饰,起到相应的保护作⽤。

const 在类中的⽤法

const 成员变量,只在某个对象⽣命周期内是常量,⽽对于整个类⽽⾔ 是可以改变的。因为类可以创建多个对象,不同的对象其 const 数据成员值可以不同。所以不 能在类的声明中初始化 const 数据成员,因为类的对象在没有创建时候,编译器不知道 const 数据成员的值是什么。const 数据成员的初始化只能在类的构造函数的初始化列表中进⾏。

const 成员函数:const 成员函数的主要⽬的是防⽌成员函数修改对象的内容。要注意,const 关键字和 static 关键字对于成员函数来说是不能同时使⽤的,因为 static 关键字修饰静态成员 函数不含有 this 指针,即不能实例化,const 成员函数⼜必须具体到某⼀个函数。

const 修饰类对象,定义常量对象:常量对象只能调⽤常量函数,别的成员函数都不能调⽤。

补充:const 成员函数中如果实在想修改某个变量,可以使⽤ mutable 进⾏修饰。成员变量中 如果想建⽴在整个类中都恒定的常量,应该⽤类中的枚举常量来实现或者 static const。

常量对象可以调⽤类中的 const 成员函数,但不能调⽤⾮ const 成员函数; (原因:对象调 ⽤成员函数时,在形参列表的最前⾯加⼀个形参 this,但这是隐式的。this 指针是默认指向调 ⽤函数的当前对象的,所以,很⾃然,this 是⼀个常量指针 test * const,因为不可以修改 this 指针代表的地址。但当成员函数的参数列表(即⼩括号)后加了 const 关键字(void print() const;),此成员函数为常量成员函数,此时它的隐式this形参为 const test * const, 即不可以通过 this 指针来改变指向对象的值.

⾮常量对象可以调⽤类中的 const 成员函数,也可以调⽤⾮ const 成员函数。

C 和 C++ 区别 (函数/类/struct/class)

C 和 C++ 在基本语句上没有过⼤的区别。

C++ 有新增的语法和关键字,语法的区别有头⽂件的不同和命名空间的不同,C++ 允许我们 ⾃⼰定义⾃⼰的空间,C 中不可以。关键字⽅⾯⽐如 C++ 与 C 动态管理内存的⽅式不同, C++ 中在 malloc 和 free 的基础上增加了 new 和 delete,⽽且 C++ 中在指针的基础上增加 了引⽤的概念,关键字例如 C++中还增加了 auto,explicit 体现显示和隐式转换上的概念要 求,还有 dynamic_cast 增加类型安全⽅⾯的内容

函数⽅⾯ C++ 中有重载和虚函数的概念:C++ ⽀持函数重载⽽ C 不⽀持,是因为 C++ 函数 的名字修饰与 C 不同,C++ 函数名字的修饰会将参数加在后⾯,例如,int func(int,double)经 过名字修饰之后会变成_func_int_double,⽽ C 中则会变成 _func,所以 C++ 中会⽀持不同 参数调⽤不同函数

C++ 还有虚函数概念,⽤以实现多态

类⽅⾯,C 的 struct 和 C++ 的类也有很⼤不同:C++ 中的 struct 不仅可以有成员变量还可 以成员函数,⽽且对于 struct 增加了权限访问的概念,struct 的默认成员访问权限和默认继 承权限都是 public,C++ 中除了 struct 还有 class 表示类,struct 和 class 还有⼀点不同在 于 class 的默认成员访问权限和默认继承权限都是 private。

C++ 中增加了模板还重⽤代码,提供了更加强⼤的 STL 标准库。

最后补充⼀点就是 C 是⼀种结构化的语⾔,重点在于算法和数据结构。C 程序的设计⾸先考 虑的是如何通过⼀个代码,⼀个过程对输⼊进⾏运算处理输出。⽽ C++ ⾸先考虑的是如何构 造⼀个对象模型,让这个模型能够契合与之对应的问题领域,这样就能通过获取对象的状态信 息得到输出

template(模板)

模板就是建立通用模具大大提高复用性。C++另一种编程思想称为泛型编程,主要利用的技术就是模板。

C++中的模板可以分为两种类型:函数模板和类模板。

  1. 函数模板

函数模板是一种通用的函数定义,它可以用于多种类型的数据。函数模板使用一个或多个类型参数作为函数参数,从而定义了一组可重用的函数代码。函数模板的语法如下:

1
2
3
4
5
template <typename T>
T add(T a,T b)
{
return a+b;
}
1
2
template --- 声明创建模板
typename --- 表明其后面的符号是一种数据类型,可以用class代替。

上面的代码定义了一个函数模板**add**,它使用类型参数T来表示函数参数的类型。函数体中的代码可以使用T来执行算术操作。调用该函数模板时,编译器会根据实际参数的类型自动实例化该函数模板。

使用函数模板时,需要在函数名后面使用尖括号(<>)指定类型参数,如下所示:

1
2
int result1 add<int>(10,20); //result1 30
float result2 add<float>(3.14f,2.71f); //result2 5.85f

注意事项:

● 自动类型推导,必须推导出一致的数据类型 T,才可以使用。

● 模板必须要确定出T的数据类型,才可以使用 T 。

  1. 类模板

类模板是一种通用的类定义,它可以用于多种类型的数据。类模板使用一个或多个类型参数作为类成员的类型,从而定义了一组可重用的类代码。类模板的语法如下:

1
2
3
4
5
6
7
template <typenane T>
class myclass
{
public:
T myVariable:
void myMethod(T myParam);
}

上面的代码定义了一个类模板**myClass**,它使用类型参数T来表示类成员的类型。类体中的代码可以使用T来定义成员变量和成员函数。创建类模板的对象时,需要指定T的类型参数。

模板是C++中非常强大和灵活的工具,可以帮助我们编写通用和高效的代码。在实践中,需要注意模板的正确使用方法和避免一些常见的错误,例如模板参数的正确使用、模板特化、模板元编程等。

面是一个简单的类模板例子,实现一个栈(先进后出)数据结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
template<typename T>
class Stack{
public:
void push(T value){
m_data.push_back(value);
}
T pop(){
T value = m_data.back();
m_data.pop_back();
return value;
}
private:
std:vector<T> m_data;
}

使用类模板时,需要在类名后面使用尖括号(<>)指定类型参数,如下所示:

1
2
3
4
5
6
7
8
Stack<int> stack1;
stack1.push (10);
stack1.push (20);
int value1 stack1.pop(); //value1 = 20
Stack<std::string>stack2;
stack2.push("hello");
stack2.push("world");
std::string value2 stack2.pop(); //value2 = "world"

在这里,我们分别使用int和string作为类型参数来实例化Stack.

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

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

  1. 普通函数调用时可以发生自动类型转换(隐式类型转换)。

  2. 函数模板调用时,如果利用自动类型推导,不会发生隐式类型转换。

  3. 如果利用显示指定类型的方式,可以发生隐式类型转换。

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

  1. 如果函数模板和普通函数都可以实现,优先调用普通函数。

  2. 可通过空模板参数列表来强制调用函数模板。

  3. 函数模板也可以发生重载。

  4. 如果函数模板可以产生更好的匹配,优先调用函数模板

重载和重写,重定义

重载

同⼀可访问区内被声明的⼏个具有不同参数列表的同名函数,依赖于 C++函数名字的修饰会将参数加在后⾯,可以是参数类型,个数,顺序的不同。根据参数列表 决定调⽤哪个函数,重载不关⼼函数的返回类型。

重写

派⽣类中重新定义⽗类中除了函数体外完全相同的虚函数,注意被重写的函 数不能是 static 的,⼀定要是虚函数,且其他⼀定要完全相同。要注意,重写和被重写的函数 是在不同的类当中的,重写函数的访问修饰符是可以不同的,尽管 virtual 中是 private 的,派 ⽣类中重写可以改为 public。

重定义

派⽣类重新定义⽗类中相同名字的⾮ virtual 函数,参数列表和返回类型都可以不同,即⽗类中除了定义成 virtual 且完全相同的同名函数才不会被派⽣类中的同名函数所隐藏(重定义)。

C++ 的构造函数

类的对象被创建时,编译系统为对象分配内存空间,并自动调用构造函数,由构造函数完成成员的初始化工作。

即构造函数的作用:初始化对象的数据成员。

无参数构造函数:即默认构造函数,如果没有明确写出无参数构造函数,编译器会自动生成默认的无参数构造函数,函数为空,什么也不做,如果不想使用自动生成的无参构造函数,必需要自己显示写出一个无参构造函数。

一般构造函数:也称重载构造函数,一般构造函数可以有各种参数形式,一个类可以有多个一般构造函数,前提是参数的个数或者类型不同,创建对象时根据传入参数不同调用不同的构造函数。

拷贝构造函数:拷贝构造函数的函数参数为对象本身的引用,用于根据一个已存在的对象复制出一个新的该类的对象,一般在函数中会将已存在的对象的数据成员的值一一复制到新创建的对象中。如果没有显示的写拷贝构造函数,则系统会默认创建一个拷贝构造函数,但当类中有指针成员时,最好不要使用编译器提供的默认的拷贝构造函数,最好自己定义并且在函数中执行深拷贝。

类型转换构造函数:根据一个指定类型的对象创建一个本类的对象,也可以算是一般构造函数的一种,这里提出来,是想说有的时候不允许默认转换的话,要记得将其声明为explict的,来阻止一些隐式转换的发生。

赋值运算符的重载:注意,这个类似拷贝构造函数,将=右边的本类对象的值复制给=左边的对象,它不属于构造函数,=左右两边的对象必需已经被创建。如果没有显示的写赋值运算符的重载,系统也会生成默认的赋值运算符,做一些基本的拷贝工作。

explicit

explicit是一个关键字,用于修饰类的构造函数,其主要作用是防止隐式类型转换。当一个构造函数被声明为explicit时,它将禁止编译器执行隐式类型转换来调用该构造函数,只允许显式调用。

四种强制转换

四种强制转换包括:static_cast,dynamic._cast,const_cast,reinterpret_cast

  • static cast:明确指出类型转换,一般建议将隐式转换都替换成显示转换,因为没有动态
    类型检查,上行转换(派生类->基类)安全,下行转换(基类->派生类)不安全,所以
    主要执行非多态的转换操作;

  • dynamic_cast:专门用于派生类之间的转换,type-id必须是类指针,类引用或void*,对
    于下行转换是安全的,当类型不一致时,转换过来的是空指针,而static cast,.当类型不
    一致时,转换过来的事错误意义的指针,可能造成非法访问等问题。

  • const_cast:专门用于const属性的转换,去除const性质,或增加const性质,是四
    个转换符中唯一一个可以操作常量的转换符。

  • reinterpret_cast:不到万不得已,不要使用这个转换符,高危操作。使用特点:从底层
    对数据进行重新解释,依赖具体的平台,可移植性差;可以将整形转换为指针,也可以
    把指针转换为数组;可以在指针和引用之间进行肆无忌惮的转换。

指针和引⽤的区别

指针和引⽤都是⼀种内存地址的概念,区别呢,指针是⼀个实体,引⽤只是⼀个别名。

在程序编译的时候,将指针和引⽤添加到符号表中。

指针它指向一块内存,指针的内容是所指向的内存的地址,在编译的时候,则是将“指针变量名-指针变量的地址”添加到符号表中,所以说,指针包含的内容是可以改变的,允许拷贝和赋值,有const和非const区别,甚至可以为空,sizeof指针得到的是指针类型的大小。

而对于引用来说,它只是一块内存的别名,在添加到符号表的时候,是将"引用变量名-引用对象的地址"添加到符号表中,符号表一经完成不能改变,所以引用必须而且只能在定义时被绑定到一块内存上,后续不能更改,也不能为空,也没有const和非const区别。

sizeof引用得到代表对象的大小。而sizeof指针得到的是指针本身的大小。另外在参数传递中,指针需要被解引用后才可以对对象进行操作,而直接对引用进行的修改会直接作用到引用对象上。
作为参数时也不同,传指针的实质是传值,传递的值是指针的地址;传引用的实质是传地址,传递的是变量的地址。

函数指针

从定义和用途两方面来说一下自己的理解:

首先是定义:函数指针是指向函数的指针变量。函数指针本身首先是一个指针变量,该指针变量指向一个具体的函数。这正如用指针变量可指向整型变量、字符型、数组一样,这里是指向函数。

在编译时,每一个函数都有一个入口地址,该入口地址就是函数指针所指向的地址。有了指向函数的指针变量后,可用该指针变量调用函数,就如同用指针变量可引用其他类型变量一样,在这些概念上是大体一致的。

其次是用途:调用函数和做函数的参数,比如回调函数。

1
2
3
4
char* fun(char *p){...};  //函数fun
char* (*pf)(char *p); //函数指针pf
pf=fun; //函数指针pf指向函数fun
pf(p); //通过函数指针pf调用函数fun

volatile

volatile是一个关键字,用于告诉编译器不要对被声明为volatile的变量进行某些优化,以确保程序在访问这些变量时不会出现意外的行为。通常,volatile关键字用于表示一个变量可能会在程序的控制之外发生改变,因此编译器不应该对它进行优化,例如不应该将对该变量的读取或写入操作进行重排或删除。

define 和 const 区别(编译阶段、安全性、内存占⽤等)

对于 define 来说,宏定义实际上是在预编译阶段进⾏处理,没有类型,也就没有类型检查, 仅仅做的是遇到宏定义进⾏字符串的展开,遇到多少次就展开多少次,⽽且这个简单的展开过 程中,很容易出现边界效应,达不到预期的效果。因为 define 宏定义仅仅是展开,因此运⾏ 时系统并不为宏定义分配内存,但是从汇编 的⻆度来讲,define 却以⽴即数的⽅式保留了多 份数据的拷⻉。

对于 const 来说,const 是在编译期间进⾏处理的,const 有类型,也有类型检查,程序运⾏ 时系统会为 const 常量分配内存,⽽且从汇编的⻆度讲,const 常量在出现的地⽅保留的是真 正数据的内存地址,只保留了⼀份数据的拷⻉,省去了不必要的内存空间。⽽且,有时编译器不会为普通的 const 常量分配内存,⽽是直接将 const 常量添加到符号表中,省去了读取和 写⼊内存的操作,效率更⾼。

参考资料



文章链接:
https://www.zywvvd.com/notes/coding/cpp/knowledge/cpp-info/


“觉得不错的话,给点打赏吧 ୧(๑•̀⌄•́๑)૭”

微信二维码

微信支付

支付宝二维码

支付宝支付

C++ 知识点
https://www.zywvvd.com/notes/coding/cpp/knowledge/cpp-info/
作者
Yiwei Zhang
发布于
2023年6月2日
许可协议