目录
1.类是什么?
2.类的访问限定符
5.类的作用域
6.类的实例化
7.类的对象
8.this指针
9.类的默认成员函数
1.构造函数
①定义
②特征
③结论
④初始化列表
⑤explicit关键字
2.析构函数
①定义
②特性
③适用场景
3.拷贝复制函数
①定义
②特征
4.赋值运算符重载
①运算符重载
②赋值运算符重载
③前置++与后置++的重载
④取地址及const取地址操作符重载
10.const成员
11.static成员
①概念
②特性
12.友元
①友元函数
②友元类
13.内部类
①概念
②特性
14.匿名对象
①概念
②特性
类(Class)是一种用户自定义的数据类型,它是一组数据和方法的集合,用来描述某个对象的属性和行为。
class className
{
// 类体:由成员函数和成员变量组成
};
类的两种定义方式:
1. 声明和定义全部放在类体中,需注意:成员函数如果 在类中定义 ,编译器可能会将其当成 内 联函数 处理。2. 类声明放在 .h 文件中,成员函数定义放在 .cpp 文件中。
class Date
{
public:
void Init(int year)
{
// 这里的year到底是成员变量,还是函数形参?
year = year;
}
private:
int year;
};
class Date
{
public:
void Init(int year)
{
_year = year;
}
private:
int _year;
};
1. public 修饰的成员在类外可以直接被访问2. protected 和 private 修饰的成员在类外不能直接被访问 ( 此处 protected 和 private 是类似的 )3. 访问权限 作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止4. 如果后面没有访问限定符,作用域就到 } 即类结束。5. class 的默认访问权限为 private , struct 为 public( 因为 struct 要兼容 C)
class Person
{
public:
void PrintPersonInfo();//函数声明
private:
char _name[20];
char _gender[3];
int _age;
};
//这里需要指定PrintPersonInfo是属于Person这个类域
void Person::PrintPersonInfo()
{
cout << _name << " "<< _gender << " " << _age << endl;
}
用类类型创建对象的过程,称为类的实例化
对于如下代码
//类中既有成员变量,又有成员函数
class A1
{
public:
void f1() {}
private:
int _a;
};
//类中仅有成员函数
class A2 {
public:
void f2() {}
};
//类中什么都没有---空类
class A3
{};
分别计算大小,有
解释this指针之前,我们借用如下的日期类
class Date
{
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1, d2;
d1.Init(2022, 1, 11);
d2.Init(2022, 1, 12);
d1.Print();
d2.Print();
return 0;
}
1. this 指针的类型:类类型 * const ,即成员函数中,不能给 this 指针赋值。2. 只能在 “ 成员函数 ” 的内部使用3. this 指针本质上是 “ 成员函数 ” 的形参 ,当对象调用成员函数时,将对象地址作为实参传递给this形参。所以 对象中不存储 this 指针 。4. this 指针是 “ 成员函数 ” 第一个隐含的指针形参,一般情况由编译器通过 ecx 寄存器自动传 递,不需要用户传递
因此,有
对于this指针还有如下一些问题:
1.this指针存在哪里?
答:因为this指针实际上是成员函数的一个隐藏参数,它在函数调用时被自动传递。在对象创建时,系统会为其分配一块内存空间,这个空间中包含了对象的成员变量和成员函数。当成员函数被调用时,this 指针会指向对象的内存空间,从而实现访问对象的成员变量和成员函数。因此this指针存放在对象里面。
2.this指针可以为空吗?
对于这个问题,这里给出两段代码
代码一:
class A
{
public:
void PrintA()
{
cout << _a << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->PrintA();
return 0;
}
结果
代码二:
class A
{
public:
void Print()
{
cout << "Print()" << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->Print();
return 0;
}
结果
对于结果我们能发现
对于代码一:p调用Print,不会发生解引用,因为Print的地址不在对象中。p会作为实参传递给this指针。this指针是空的,但是函数内访问_a,本质是this->_a,因此不能通过编译。
对于代码二:p调用Print,不会发生解引用,因为Print的地址不在对象中。p会作为实参传递给this指针。this指针是空的,但是函数内没有对this指针解引用,因此可以通过编译。
如果一个类中什么成员都没有,简称为空类。
空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。
默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。
在C语言中,我们可能常常会忘了初始化数据或者销毁数据。而在C++中,对于一些特定场景来说,销毁数据时会略显繁琐,因此C++在类中引入了构造函数。
构造函数 是一个 特殊的成员函数,名字与类名相同 , 创建类类型对象时由编译器自动调用 ,以保证每个数据成员都有一个合适的初始值,并且在对象整个生命周期内只调用一次 。
注:构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。
它有如下的一些特征:
1. 函数名与类名相同。
2. 无返回值。
3. 对象实例化时编译器自动调用对应的构造函数。
4. 构造函数可以重载。
这里用日期类举例
class Date
{
public:
// 1.无参构造函数
Date()
{}
// 2.带参构造函数
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
在使用时, 也要注意使用方法
void TestDate()
{
Date d1; // 调用无参构造函数
Date d2(2015, 1, 1); //调用带参的构造函数
//注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明
//以下代码的函数:声明了d3函数,该函数无参,返回一个日期类型的对象
Date d3();
}
对于d3编译器会给出警示
5. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦
用户显式定义编译器将不再生成。
这里使用如下代码测试
class Date
{
public:
/*
// 如果用户显式定义了构造函数,编译器将不再生成
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
Date()
{}
*/
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
return 0;
}
1.将Date类中构造函数屏蔽
代码可以通过编译,因为编译器生成了一个无参的默认构造函数
2.将Date类中构造函数放开
代码编译失败,因为一旦显式定义任何构造函数,编译器将不再生成无参构造函数,放开后报错:error C2512: “Date”: 没有合适的默认构造函数可用
6.对于这个默认构造函数,他对于内置类型成员不做处理(C++11中规定可以使用缺省值进行初始化)如下
class Stack
{
private:
int* _a = nullptr;
int _top = 0;
int _capacity = 0;
};
而对于自定义类型成员,它会去调用他的默认构造函数。
7.无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。
注:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数。
因此,可以有以下结论
1. 正常情况下,构造函数都需要我们自己去完成
2. 在以下情况下,可以不用书写构造函数
a. 内置的成员函数都已经有缺省值,且缺省值符合要求
b. 成员都为自定义类型的构造,且这些自定义类型都有自己定义的默认构造
定义:
以一个 冒号开始 ,接着是一个以 逗号分隔的数据成员列表 ,每个 " 成员变量 " 后面跟一个放在括号中的初始值或表达式。
如
class Date
{
public:
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
注:
1.每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
2.类中包含以下成员,必须放在初始化列表位置进行初始化:
a. 引用成员变量b. const 成员变量c. 自定义类型成员 ( 且该类没有默认构造函数时 )
如
class A
{
public:
A(int a)
:_a(a)
{}
private:
int _a;
};
class B
{
public:
B(int a, int ref)
:_aobj(a)
,_ref(ref)
,_n(10)
{}
private:
A _aobj; // 没有默认构造函数
int& _ref; // 引用
const int _n; // const
};
注:如果不在初始化列表初始,后面无法在对它们进行修改。
3.尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。
我们用如下代码来检测
class Time
{
public:
Time(int hour = 0)
:_hour(hour)
{
cout << "Time()" << endl;
}
private:
int _hour;
};
class Date
{
public:
Date(int day)
{}
private:
int _day;
Time _t;
};
int main()
{
Date d(1);
return 0;
}
有
4. 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
对于下列代码
class A
{
public:
A(int a)
:_a1(a)
, _a2(_a1)
{}
void Print()
{
cout << _a1 << " " << _a2 << endl;
}
private:
int _a2;
int _a1;
};
int main()
{
A aa(1);
aa.Print();
return 0;
}
有
在这里,虽然在初始化列表中,分别初始化的是a1,a2,但是在声明中却是a2,a1,因此初始化列表中,先用a1初始化a2,而此时a1是随机值,因此便得到上图结果。所以,在初始化列表中的初始化顺序最好和类中的声明顺序相同,以防出现bug。
用explicit修饰构造函数,将会禁止构造函数的隐式转换。
在介绍explicit之前,我们先了解下与类相关的隐式类型转换
对于下面这个类
class A
{
public:
A(int a)
:_a(a)
{
cout << "A(int a)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
和int与double之间的隐式类型转换相似,用int初始化aa2时也发生了隐式类型转换
而在构造函数前加上explicit后,编译器将不再允许类型转换
explicit A(int a)
:_a(a)
{
cout << "A(int a)" << endl;
}
在知道了构造函数初始化一个对象后,那么怎样结束一个对象呢?
析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
它也是一个特殊的成员函数,具有如下一些特性:
a.析构函数不能重载b.析构函数不对内置数据类型作出处理c.如果自定义类型中本身就有的话,会调用他的析构函数
4. 对象生命周期结束时,C++编译系统系统自动调用析构函数。
析构函数的适用场景:
1.有动态内存申请资源时,就需要显示写析构函数释放资源
如对于下面这个栈类来说,我们就需要手动写出析构函数
class Stack
{
public:
Stack(int capacity = 4)
{
cout << "Stack()" << endl;
_a = (int*)malloc(sizeof(int) * capacity);
if (nullptr == _a)
{
perror("malloc申请空间失败");
return;
}
_capacity = capacity;
_top = 0;
}
~Stack()
{
cout << "~Stack()" << endl;
free(_a);
_a = nullptr;
_capacity = _top = 0;
}
private:
int* _a = nullptr;
int _top = 0;
int _capacity;
};
2.没有动态内存申请的资源,不需要写析构
如下面这个日期类
class Date
{
private:
int _year;
int _month;
int _day;
};
3.需要释放资源的成员都是自定义类型,不需要写析构
如用栈实现一个队列,那么这个队列也不需要写析构
class MyQueue
{
private:
Stack _pushst;
Stack _popst;
};
在创建对象时,可否创建一个与已存在对象一模一样的新对象呢?
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
拷贝构造函数也是一个特殊的成员函数,其特征如下:
1.拷贝构造函数是构造函数的一个重载形式。
(在这里还是采用日期类举例)
class Date
{
public:
//第一种
Date(Date d)
{
cout << "Date(Date d)" << endl;
_year = d._year;
_month = d._month;
_day = d._day;
}
//第二种
Date(Date& d)
{
cout << "Date( Date& d)" << endl;
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
如果使用第一种拷贝构造函数的话,会有如下的过程
传值得话,需要先把这个对象构造出来,那就要调用拷贝构造,那又要构造新的对象,这最终导致陷入无限的递归中,
而如果采用第二种的话就没有这样的忧虑,因为传入的是自定义类型对象的引用而非对象,这样就可以直接操作传入的对象,而不用再次构建新的对象。
3.若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按
因为逐字节拷贝,所以st2._a与st1._a所指向的空间为同一份,而且在st1与st2的生命周期结束时,它们指向的这个空间会被释放两次,这样的结果显然不是我们想要的。
而下面的深拷贝才是我们想要的
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator+需要重载的运算符符号。
1. 不能通过连接其他符号来创建新的操作符:比如 operator@2. 重载操作符必须有一个类类型参数3. 用于内置类型的运算符,其含义不能改变,例如:内置的整型 + ,不 能改变其含义4. 作为类成员函数重载时,其形参看起来比操作数数目少 1 ,因为成员函数的第一个参数为隐藏的this5. .* :: sizeof ?: . 注意以上 5 个运算符不能重载。这个经常在笔试选择题中出现。
这里还是使用日期类来进行举例
operator可以有全局和封装在类内部两种
// 全局的operator==
class Date
{
public:
Date(int year = 2000, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//private:
int _year;
int _month;
int _day;
};
bool operator==(const Date& d1, const Date& d2)
{
return d1._year == d2._year
&& d1._month == d2._month
&& d1._day == d2._day;
}
这里我们发现运算符重载成全局的,而成员变量是私有的,这种情况不好处理,因此我们一般选择将其封装到内部。
// 封装的operator==
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// bool operator==(Date* this, const Date& d2)
// 这里需要注意的是,左操作数是this,指向调用函数的对象
bool operator==(const Date & d2)
{
return _year == d2._year
&& _month == d2._month
&& _day == d2._day;
}
private:
int _year;
int _month;
int _day;
};
class Date
{
public:
Date(int year = 2000, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
Date& operator=(const Date& d)
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
private:
int _year;
int _month;
int _day;
};
注:
a. 返回*this返回的是this指向的内容,为这块空间取别名并返回没有问题
b. 赋值运算符只能重载成类的成员函数不能重载成全局函数,因为赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数。c. 用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。在这里 内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。这里与前面的拷贝复制函数差不多,当涉及到动态内存分配时,需要手动实现深拷贝,而不能使用浅拷贝(值拷贝)。
Date& operator++()
{
_day += 1;
return *this;
}
Date operator++(int)
{
Date temp(*this);
_day += 1;
return temp;
}
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容!
Date* operator&()
{
return this;
}
const Date* operator&()const
{
return this;
}
将 const 修饰的 “ 成员函数 ” 称之为 const 成员函数 , const 修饰类成员函数,实际修饰该成员函数 隐含的 this 指针 ,表明在该成员函数中 不能对类的任何成员进行修改。
举个例子
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << "Print()" << endl;
cout << "year:" << _year << endl;
cout << "month:" << _month << endl;
cout << "day:" << _day << endl << endl;
}
void Print() const
{
cout << "Print()const" << endl;
cout << "year:" << _year << endl;
cout << "month:" << _month << endl;
cout << "day:" << _day << endl << endl;
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
void Test()
{
Date d1(2022, 1, 13);
d1.Print();
const Date d2(2022, 1, 13);
d2.Print();
}
运行如下
在这里const写在Print函数之后,实际是是将Date* const this转换为const Date* const this,即先前this指针不能更换指向对象,现在this指针指向的内容也不能更改(即无法通过this指针来修改对象的状态)。即
声明为 static 的类成员 称为 类的静态成员 ,用 static 修饰的 成员变量 ,称之为 静态成员变量 ;用 static 修饰 的 成员函数 ,称之为 静态成员函数 。 静态成员变量一定要在类外进行初始化
下面用两个面试题来举例
实现一个类,计算程序中创建出了多少个类对象。
class A
{
public:
A() { ++_scount; }
A(const A& t) { ++_scount; }
~A() { --_scount; }
static int GetACount() { return _scount; }
private:
static int _scount;
};
int A::_scount = 0;
void TestA()
{
cout << A::GetACount() << endl;
A a1, a2;
cout << A::GetACount() << endl;
A a3(a1);
cout << A::GetACount() << endl;
}
运行有
设计一个类,在类外面只能在栈上创建对象
设计一个类,在类外面只能在堆上创建对象
class A
{
public:
static A GetStackObj()
{
A aa;
return aa;
}
static A* GetHeapObj()
{
return new A;
}
private:
A()
{}
private:
int _a1 = 1;
int _a2 = 2;
};
int main()
{
//static A aa1; // 静态区
//A aa2; // 栈
//A* ptr = new A; // 堆
A::GetStackObj();
A::GetHeapObj();
return 0;
}
1. 静态成员 为 所有类对象所共享 ,不属于某个具体的对象,存放在静态区2. 静态成员变量 必须在 类外定义 ,定义时不添加 static 关键字,类中只是声明3. 类静态成员即可用 类名 :: 静态成员 或者 对象 . 静态成员 来访问4. 静态成员函数 没有 隐藏的 this 指针 ,不能访问任何非静态成员5. 静态成员也是类的成员,受 public 、 protected 、 private 访问限定符的限制
友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏封装性,因此友元不宜多用。
友元分为:友元函数和友元类
友元函数 可以 直接访问 类的 私有 成员,它是 定义在类外部 的 普通函数 ,不属于任何类,但需要在类的内部声明,声明时需要加friend 关键字。
这里就用日期类(C++应用实例—日期类的实现)里的流操作符举例
问题:
尝试重载 operator<<时 ,我们发现没办法将 operator<< 重载成成员函数。 因为 cout 的 输出流对象和隐含的 this 指针在抢占第一个参数的位置 。 this 指针默认第一个参数是左操作数。但是实际使用中cout 需要第一个是形参对象,才能正常使用。所以要将 operator<< 重载成全局函数。但又会导致类外没办法访问成员,此时就需要友元来解决。operator>> 同理。
class Date
{
friend ostream& operator<<(ostream& _cout, const Date& d);
friend istream& operator>>(istream& _cin, Date& d);
public:
Date(int year = 1900, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& _cout, const Date& d)
{
_cout << d._year << "-" << d._month << "-" << d._day;
return _cout;
}
istream& operator>>(istream& _cin, Date& d)
{
_cin >> d._year;
_cin >> d._month;
_cin >> d._day;
return _cin;
}
int main()
{
Date d;
cin >> d;
cout << d << endl;
return 0;
}
运行有
注:
1. 友元函数 可访问类的私有和保护成员,但 不是类的成员函数2. 友元函数 不能用 const 修饰3. 友元函数 可以在类定义的任何地方声明, 不受类访问限定符限制4. 一个函数可以是多个类的友元函数5. 友元函数的调用与普通函数的调用原理相同
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
举例
class Time
{
friend class Date; // 声明日期类为时间类的友元类,
// 则在日期类中就直接访问Time类中的私有成员变量
public:
Time(int hour = 0, int minute = 0, int second = 0)
: _hour(hour)
, _minute(minute)
, _second(second)
{}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
void SetTimeOfDate(int hour, int minute, int second)
{
// 直接访问时间类私有的成员变量
_t._hour = hour;
_t._minute = minute;
_t._second = second;
}
private:
int _year;
int _month;
int _day;
Time _t;
};
注:
1. 友元关系是单向的,不具有交换性。比如上述 Time 类和 Date 类,在 Time 类中声明 Date 类为其友元类,那么可以在 Date 类中直接访问Time 类的私有成员变量,但想在 Time 类中访问 Date 类中私有的成员变量则不行。2. 友元关系不能传递如果 C 是 B 的友元, B 是 A 的友元,则不能说明 C 时 A 的友元。3. 友元关系不能继承,在继承位置再给大家详细介绍。
如果一个类定义在另一个类的内部,这个内部类就叫做内部类 。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。
class A
{
private:
static int k;
int h;
public:
class B // B天生就是A的友元
{
public:
void foo(const A& a)
{
cout << k << endl;
cout << a.h << endl;
}
};
};
int A::k = 1;
int main()
{
A::B b;
b.foo(A());
return 0;
}
有
1. 内部类可以定义在外部类的 public 、 protected 、 private 都是可以的。2. 注意内部类可以直接访问外部类中的 static 成员,不需要外部类的对象 / 类名。3. sizeof( 外部类 )= 外部类,和内部类没有任何关系。
在 C++ 中,匿名对象是指没有被命名的对象,即没有通过变量名或引用名来指向它的对象。通常情况下,匿名对象是通过在对象类型后面直接添加一对括号来创建的
举例
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
class Solution
{
public:
int Sum_Solution(int n)
{
//...
return n;
}
};
int main()
{
A aa1;
// 不能这么定义对象,因为编译器无法识别下面是一个函数声明,还是对象定义
//A aa1();
// 但是我们可以定义匿名对象,匿名对象的特点不用取名字,
// 但是他的生命周期只有这一行,我们可以看到下一行他就会自动调用析构函数
A(1);
A aa2(2);
// 匿名对象在这样场景下就很好用
Solution().Sum_Solution(10);
return 0;
}
对于A(1)逐语句调试发现有
注:若对匿名对象取引用,则会使匿名对象的生命周期延长至该函数范围内。
匿名对象没有名字,只能通过创建它的表达式来访问它。
匿名对象通常只能用于一次计算或者函数调用中,因为它们没有名字,无法被其他代码所引用,也无法进行修改。
匿名对象的生命周期通常比较短暂,一般在创建它的表达式计算完毕后就会被立即销毁。
匿名对象可以作为函数的参数传递,但是传递后就无法再次使用了。
匿名对象也可以作为函数的返回值,但是需要注意的是,如果返回的是对象的指针或者引用,那么需要确保匿名对象的生命周期足够长,以免出现未定义行为。