模板网站建设青岛漯河网站建设

当前位置: 首页 > news >正文

模板网站建设青岛,漯河网站建设,优秀wordpress,免费制作自己的网站长文章目录一、构造函数1. 构造函数的定义2. 编译器生成的构造函数3. 默认构造函数4. 初始化列表5. 内置成员变量指定缺省值(C11)二、析构函数1. 析构函数的定义2. 编译器生成的析构函数3. 自己写的析构函数的执行方式三、拷贝构造函数1. C语言值传递和返回值时存在 bug2. 拷贝构… 文章目录一、构造函数1. 构造函数的定义2. 编译器生成的构造函数3. 默认构造函数4. 初始化列表5. 内置成员变量指定缺省值(C11)二、析构函数1. 析构函数的定义2. 编译器生成的析构函数3. 自己写的析构函数的执行方式三、拷贝构造函数1. C语言值传递和返回值时存在 bug2. 拷贝构造函数的定义3. 编译器自动生成的拷贝构造函数4. 自己写的拷贝构造函数的执行方式5. 拷贝构造函数的调用场景四、赋值运算符重载1. 运算符重载(1) 运算符重载的定义和使用(2) 前置/– 和 后置/– 的运算符重载规定(3) 运算符重载也可以实现在全局域中2. 赋值运算符重载的定义3. 编译器自动生成的赋值运算符重载五、取地址运算符重载1. const 成员函数2. 取地址运算符重载六、const 取地址运算符重载一、构造函数 #include iostreamusing namespace std;class Date { public:void Init(){_year 1970;_month 1;_day 1;}void Print(){cout _year 年 _month 月 _day 日 endl;}private:int _year;int _month;int _day; };int main() {Date d1;d1.Init();//输出 1970年1月1日d1.Print();Date d2;//输出 -858993460年 - 858993460月 - 858993460日d2.Print();return 0; }我们写出的 Date 类实例化对象后对象的成员的成员变量默认是随机值于是每个对象都需要调用初始化函数难免会有忘记的时候

  1. 构造函数的定义 C 提供了一个特殊的成员函数 用于初始化对象的成员变量 叫做构造函数可以很好的解决这个问题 构造函数的特点 实例化对象时自动调用在对象的声明周期中只会调用一次函数名和类名相同无返回值指的是不能写返回值构造函数可以重载类的对象可以有多种初始化方式 将类中的 Init 函数改为构造函数并提供多个构造函数 #include iostreamusing namespace std;class Date { public://无参构造函数Date(){_year 1970;_month 1;_day 1;//验证是否调用了构造函数cout Date() ;}//带参构造函数Date(int year, int month, int day){_year year;_month month;_day day;//验证是否调用了构造函数cout Date(int year, int month, int day) ;}void Print(){cout _year 年 _month 月 _day 日 endl;}private:int _year;int _month;int _day; };构造函数是在实例化对象的时候自动调用的 int main() {//调用无参构造函数Date d1;//输出 Date() 1970年1月1日d1.Print();//传参调用构造函数Date d2(2023, 2, 4);//Date(int year, int month, int day) 输出 2023年2月4日d2.Print();return 0; }为什么不允许 d1.Date() 调用构造函数呢 如果可以这样调用便和我们写的 Init 函数然后自己调用没有区别并且构造函数只能调用一次 为什么不传参调用构造函数时不加括号 如果加上扩号则为 Date d1(); 此时和声明一个函数的返回值是 Date函数名为 d1无参的函数一模一样便会产生歧义
  2. 编译器生成的构造函数 如果类中没有构造函数编译器会自动生成一个无参的构造函数如果存在构造函数便不会生成 编译器自动生成的构造函数的行为是什么呢 内置类型的成员变量不做处理 内置类型包括(char short int long float double 型各种类型的指针数组元素是内置类型等)自定义类型的成员变量调用该类型的默认构造函数(不需要传参的构造函数) 自定义类型(类结构体联合体) #include iostreamusing namespace std;//时间类 class Time { public://时间类的构造函数Time(){_hour _minute _second 0;}void Print(){cout _hour 时 _minute 分 _second 秒 endl;}private:int _hour;int _minute;int _second; };//日期类 class Date { public://无构造函数void Print(){cout _year 年 _month 月 _day 日 ;_time.Print();}private:int _year;int _month;int _day;Time _time; };int main() {Date d;//默认构造函数的行为内置类型不处理自定义类型调用他的默认构造函数//输出 -858993460年-858993460月-858993460日 0时0分0秒d.Print();return 0; }3. 默认构造函数 无参构造函数、全缺省构造函数、我们没写编译器自动生成的构造函数这些都是 不需要传参就可以调用的构造函数称之为默认构造函数默认构造函数只能有一个 #include iostreamusing namespace std;class Date { public://只有带参构造函数Date(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;return 0; }上述代码中由于存在带参数的构造函数编译器便不会自动生成构造函数于是就没有默认构造函数可用 因此如果是自己写构造函数需要提供默认构造函数一般提供全缺省 //全缺省构造函数 Date(int year 1970, int month 1, int day 1) {_year year;_month month;_day day; }4. 初始化列表 构造函数体内的赋值语句其实并不是成员变量初始化的地方因为初始化只能有一次而构造函数体内可以对成员变量进行多次赋值 #include iostreamusing namespace std;class Date { public:Date(int year 1970, int month 1, int day 1){//构造函数体内可以对成员变量进行多次赋值//构造函数体内并不是成员变量初始化的地方_year year;_month month;_day day;_year year;_month month;_day day;}private:int _year;int _month;int _day; };int main() {Date d1;return 0; }初始化列表才是对成员变量初始化的地方 初始化列表在构造函数括号后以冒号开始逗号分隔的成员列表每个成员后跟一个放在括号中的初始值或者构造函数中的参数 #include iostreamusing namespace std;//时间类 class Time { public:Time()//初始化列表: _hour(0), _minute(0), _second(0){}Time(int hour, int minute, int second)//初始化列表: _hour(hour), _minute(minute), _second(second){}void Print(){cout _hour 时 _minute 分 _second 秒 endl;}private:int _hour;int _minute;int _second; };//日期类 class Date { public:Date()//初始化列表: _year(1970), _month(1), _day(1){}Date(int year, int month, int day, int hour, int minute, int second)//初始化列表: _year(year), _month(month), _day(day), _time(hour, minute, second){}void Print(){cout _year 年 _month 月 _day 日 ;_time.Print();}private:int _year;int _month;int _day;Time _time; };int main() {Date d1;d1.Print(); //输出 1970年1月1日 0时0分0秒Date d2(2023, 2, 8, 22, 58, 24);d2.Print(); //输出 2023年2月8日 22时58分24秒return 0; }注意 每个成员变量在初始化列表中只能出现一次 初始化只能初始化一次内置类型 如果未出现在初始化列表中则不做处理自定义类型 如果未出现在初始化列表中则调用该类型的默认构造函数类中包含以下成员时必须放在初始化列表位置进行初始化 引用成员变量const 成员变量自定义类型成员(且该类没有默认构造函数时) 建议尽量使用初始化列表对成员变量初始化因为无论如何自定义类型都会根据初始化列表来进行初始化的方式 注意成员变量初始化完成之后才会进入构造函数体中构造函数体中只能用来赋值当成员变量存在数组时就可以在构造函数体内赋值 成员变量在类中声明次序就是其在初始化列表中的初始化顺序与其在初始化列表中的先后次序无关 #include iostreamusing namespace std;class A { public:A(int a)//根据成员声明顺序//先用随机值初始化_a1在用 a 初始化 _a2: _a2(a), _a1(_a2){}void Print() {cout _a1 _a2 endl;}private:int _a1;int _a2; };int main() {A aa(1);aa.Print(); //输出 -858993460 1return 0; }5. 内置成员变量指定缺省值(C11) 由于编译器自动生成的构造函数对内置类型不做处理不是很好到了 C11 给这里打了补丁内置类型的成员变量在类中声明时可以指定缺省值 我们没写构造函数时编译器生成的默认构造函数内置类型使用缺省值初始化成员变量(C11) #include iostreamusing namespace std;class Date { public:void Print(){cout _year 年 _month 月 _day 日 endl;}private:int _year 1970;int _month 1;int _day 1; };int main() {Date d1;d1.Print(); //输出 1970年1月1日return 0; }我们写了构造函数时内置类型成员变量在初始化时先使用初始化列表中指定的值如果初始化列表中没有指定才使用成员变量声明时指定的缺省值(C11)如果都没有则只定义不初始化(随机值) #include iostreamusing namespace std;class Date { public:Date(): _year(1970){}void Print(){cout _year 年 _month 月 _day 日 endl;}private:int _year 2023;int _month 1;int _day; };int main() {//_year 即在初识化列表中指定又在声明时给缺省值//_month 只在声明时给了缺省值//_day 什么都没有给Date d1;d1.Print(); //输出 1970年1月-858993460日return 0; }二、析构函数 #include iostreamusing namespace std;typedef int StackDataType;class Stack { public:Stack(int capacity 4): _capacity(capacity), _top(0){_a (StackDataType*)malloc(sizeof(StackDataType) * _capacity);if (_a nullptr){perror(malloc);exit(-1);}}void Push(StackDataType x){//扩容…//插入_a[_top] x;_top;}//…void Destroy(){if (_a){free(_a);_a nullptr;_top _capacity 0;}}private:StackDataType* _a;int _top;int _capacity; };int main() {Stack st;st.Push(1);//使用…return 0; }对于像栈这样需要在堆上开辟内存空间存储数据的类每个对象使用完时都需要调用销毁函数否则会导致内存泄漏和调用初识化函数一样难免会有忘记的时候
  3. 析构函数的定义 C 提供了一个特殊的成员函数 用于清理对象资源 叫做析构函数可以很好的解决这个问题他的工作和构造函数相反可以将析构函数的特性和构造函数对比 析构函数的特点 对象生命周期结束时编译器会自动调用析构函数函数名是在类名前加上字符 ~无参数无返回值指的是不能写返回值一个类只能有一个析构函数析构函数不能重载 将类中的 Destroy 函数改为析构函数 #include iostreamusing namespace std;typedef int StackDataType;class Stack { public:Stack(int capacity 4): _capacity(capacity), _top(0){_a (StackDataType*)malloc(sizeof(StackDataType) * _capacity);if (_a nullptr){perror(malloc);exit(-1);}}void Push(StackDataType x){//扩容…//插入_a[_top] x;_top;}//…//析构函数~Stack(){if (_a){free(_a);_a nullptr;_top _capacity 0;}//验证是否调用了析构函数cout ~Stack() ;}private:StackDataType* _a;int _top;int _capacity; };析构函数是在对象销毁的时候自动调用的 int main() {Stack st;st.Push(1);//使用…//可以手动调用析构函数st.~Stack();return 0; }输出 ~Stack() ~Stack() 析构函数允许 st.~Stack() 调用并且无论什么时候只要在对象销毁时编译器都会自动调用析构函数
  4. 编译器生成的析构函数 如果类中没有析构函数编译器会自动生成析构函数那编译器自动生成的析构函数的行为是什么呢 内置类型的成员变量不做处理也不需要处理(因为不用释放资源)自定义类型的成员变量调用该类型的析构函数 #include iostreamusing namespace std;typedef int StackDataType;//栈 class Stack { public:Stack(int capacity 4): _capacity(capacity), _top(0){_a (StackDataType*)malloc(sizeof(StackDataType) * _capacity);if (_a nullptr){perror(malloc);exit(-1);}}void Push(StackDataType x){//扩容…//插入_a[_top] x;_top;}//…//析构函数~Stack(){if (_a){free(_a);_a nullptr;_top _capacity 0;}//验证是否调用了析构函数cout ~Stack() ;}private:StackDataType* _a;int _top;int _capacity; };//两个栈的类 class Queue { public:void Push(StackDataType x){_pushS.Push(x);}//…//没有析构函数private:Stack _pushS;Stack _popS; };int main() {Queue q;return 0; }输出 ~Stack() ~Stack()
  5. 自己写的析构函数的执行方式 我们自己写的析构函数会先执行析构函数体中的内容然后再调用自定义类型的析构函数 在上个代码中在 Queue 类中加上析构函数输出 ~Queue() ~Stack() ~Stack() ~Queue() {cout ~Queue() ; }三、拷贝构造函数
  6. C语言值传递和返回值时存在 bug 在用 C语言写栈这些数据结构时我们在进行参数传递时都是采用传地址的方式一直以为传值的方式只是因为效率不高其实对于像栈这样需要在堆上开辟内存空间存储数据的结构以传值的方式传参会存在 bug #include stdio.h #include stdlib.htypedef int StackDataType;//栈结构 typedef struct Stack {StackDataType* a;int top;int capacity; }Stack;//初始化 void Init(Stack* ps) {ps-a (StackDataType*)malloc(sizeof(StackDataType) * 4);if (ps-a NULL){exit(-1);}ps-capacity 4;ps-top 0; }//插入 void Push(Stack* ps, StackDataType x) {ps-a[ps-top] x;ps-top; }//测试 void test(Stack s) {s.a[0] 0; }int main() {Stack s;Init(s);Push(s, 1);printf(%d\n, s.a[0]); //输出 1test(s);printf(%d\n, s.a[0]); //输出 0return 0; }根据输出结果发现 采用值传递的方式调用 test 函数尽然可以修改栈的数据为什么呢 C语言中值传递是通过按字节的方式将实参拷贝给形参(按字节拷贝称为浅拷贝) 于是在 C中为了避免这种情况如果函数传递的参数是自定义类型时需要调用该类型的拷贝构造函数同样的 返回值是自定义类型时也需要调用该类型的拷贝构造函数
  7. 拷贝构造函数的定义 拷贝构造函数是 C 提供的一个特殊的成员函数用于创建一个与已存在对象一某一样的新对象 拷贝构造函数是构造函数的重载具有构造函数的特性但是拷贝构造函数的参数只能有一个并且必须是类类型对象的引用 拷贝构造的参数如果是类类型采用值传递的方式可以吗 自定义类型传参时需要调用拷贝构造函数此时就会导致无穷递归所以不可以进行值传递 于是为了避免无穷递归拷贝构造函数的参数需要传内置类型因此必须采用地址传递的方式因此参数可以是类类型的指针也可以是类类型的引用由于引用更方便因此规定拷贝构造函数的参数必须是类类型的引用 #include iostream #include string.husing namespace std;typedef int StackDataType;//栈 class Stack { public:Stack(int capacity 4): _capacity(capacity), _top(0){_a (StackDataType*)malloc(sizeof(StackDataType) * _capacity);if (_a nullptr){perror(malloc);exit(-1);}}//拷贝构造函数//加上 const 防止内部修改源对象Stack(const Stack s): _capacity(s._capacity), _top(s._top){//开新空间_a (StackDataType*)malloc(sizeof(StackDataType) * _capacity);if (_a nullptr){perror(malloc);exit(-1);}//拷贝数据memcpy(_a, s._a, sizeof(StackDataType) * _capacity);//验证是否会调用拷贝构造cout Stack(Stack s) ;}//虽然可以完成拷贝构造的工作但这个不是拷贝构造函数//只是构造函数的一个重载形式而已Stack(Stack* s): _capacity(s-_capacity), _top(s-_top){_a (StackDataType*)malloc(sizeof(StackDataType) * _capacity);if (_a nullptr){perror(malloc);exit(-1);}//拷贝数据memcpy(_a, s-_a, sizeof(StackDataType) * _capacity);}void Push(StackDataType x){//扩容…//插入_a[_top] x;_top;}void Print(){for (int i 0; i _top; i){cout _a[i] ;}cout endl;}//…~Stack(){if (_a){free(_a);_a nullptr;_top _capacity 0;}}private:StackDataType* _a;int _top;int _capacity; };int main() {Stack s1;s1.Push(1);s1.Push(2);s1.Print(); //输出 1 2//两种方式都可以调用拷贝构造函数//Stack s2(s1);Stack s2 s1;s2.Print(); //输出 Stack(Stack s) 1 2//调用指针版的构造函数Stack s3(s1);s3.Print(); //输出 1 2return 0; }3. 编译器自动生成的拷贝构造函数 如果类中没有拷贝构造函数编译器会自动生成拷贝构造函数那编译器自动生成的拷贝构造函数的行为是什么呢 内置类型的成员变量进行浅拷贝自定义类型的成员变量调用该类型的拷贝构造函数 #include iostream #include string.husing namespace std;typedef int StackDataType;//栈 class Stack { public:Stack(int capacity 4): _capacity(capacity), _top(0){_a (StackDataType*)malloc(sizeof(StackDataType) * _capacity);if (_a nullptr){perror(malloc);exit(-1);}}//拷贝构造函数//加上 const 防止内部修改源对象Stack(const Stack s): _capacity(s._capacity), _top(s._top){//开新空间_a (StackDataType*)malloc(sizeof(StackDataType) * _capacity);if (_a nullptr){perror(malloc);exit(-1);}//拷贝数据memcpy(_a, s._a, sizeof(StackDataType) * _capacity);}void Push(StackDataType x){//扩容…//插入_a[_top] x;_top;}void Print(){for (int i 0; i _top; i){cout _a[i] ;}cout endl;}//…~Stack(){if (_a){free(_a);_a nullptr;_top _capacity 0;}}private:StackDataType* _a;int _top;int _capacity; };class Test { public://无拷贝构造函数void Push(StackDataType x){_size;_st.Push(x);}void Print(){cout size: _size ;_st.Print();}private:int _size 0;Stack _st; };int main() {//Test 类中可以使用编译器生成的默认构造函数Test t1;t1.Push(1);t1.Push(2);t1.Print(); //输出 size:2 1 2//内置类型进行浅拷贝自定义类型调用该类型的拷贝构造函数Test t2(t1);t2.Print(); //输出 size:2 1 2return 0; }4. 自己写的拷贝构造函数的执行方式 拷贝构造函数的执行方式和构造函数的执行方式是一样的成员变量先使用拷贝构造函数的初始化列表或成员变量声明时指定的缺省值(C11) 初始化成员变量然后执行拷贝构造函数中的内容 因此自定义类型成员变量需要自己在拷贝构造函数的初始化列表显示指定调用该类型的拷贝构造函数或者自己在拷贝构造函数体中显示指定调用该类型的拷贝构造 注意拷贝构造函数也是构造函数当我们写了拷贝构造函数后编译器便不会生成构造函数
  8. 拷贝构造函数的调用场景 有如下三种场景 使用已存在对象创建新对象函数参数类型为类类型对象函数返回值类型为类类型对象 #include iostreamusing namespace std;class Date { public:Date(int year, int minute, int day){cout Date(int, int, int) endl;}Date(const Date d){cout Date(const Date d) endl;}~Date(){cout ~Date(): endl;}private:int _year;int _month;int _day; };Date Test(Date d) {Date temp(d);return temp; }int main() {Date d1(2023, 2, 10);Test(d1);return 0; }输出结果 Date(int, int, int) Date(const Date d) Date(const Date d) Date(const Date d) ~Date(): ~Date(): ~Date(): ~Date():为了提高程序效率一般对象传参时尽量使用引用类型返回时根据实际场景能用引用尽量使用引用 四、赋值运算符重载
  9. 运算符重载 (1) 运算符重载的定义和使用 在 C语言中运算符只支持内置类型而不支持自定义类型于是 C 为了增强代码的可读性引入了运算符重载使得自定义类型也可以使用运算符 运算符重载是具有特殊函数名的函数其返回值类型与参数列表与普通的函数类似 函数名字为关键字operator后面接需要重载的运算符符号。 函数原型返回值类型 operator运算符(参数列表); 参数列表的个数和运算符的操作数相同如果有两个操作数则第一个参数表示左操作数第二个表示右操作数 运算符的重载函数 和 使用 #include iostreamusing namespace std;class Date { public:Date(int year 1970, int month 1, int day 1): _year(year), _month(month), _day(day){}// 运算符重载//左操作数是 this指向调用函数的对象bool operator(const Date d){return _year d._year _month d._month _day d._day;}private:int _year;int _month;int _day; };int main() {Date d1;Date d2(2023, 2, 6);Date d3(d1);//两种使用运算符重载函数的方式cout d1.operator(d2) endl; //输出 0// 比 优先级高需要加括号//编译器会解释为 d1.operator(d3);cout (d1 d3) endl; //输出 1return 0; }注意 operator 关键字后不能连接语言中没有的运算符 比如operator运算符重载函数必须有一个类类型参数 用于内置类型的运算符其含义不能改变例如内置的整型 不能改变其含义.(成员访问运算符)、.*(点星)、::(域作用限定符)、?:(三目运算符)、sizeof 注意这 5 个运算符不能重载运算符重载不能改变运算符操作数的个数 比如 需要两个操作数则重载的 也必须要有两个操作数运算符重载不能改变运算符的优先级和结合性 (2) 前置/– 和 后置/– 的运算符重载规定 默认表示前置/– 的运算符重载d1 解释为 d1.operator(); Date operator(); Date operator–();前置/– 和后置/– 都是一元运算符为了可以让前置 和后置 形成函数重载C 规定后置 重载时多增加一个 int 类型的参数(可以不用写形参名)调用函数时该参数不用传递编译器自动传递d1 解释为 d1.operator(整形)int 参数编译器会自动传 Date operator(int); Date operator–(int);(3) 运算符重载也可以实现在全局域中 cout 和 cin 其实分别是 ostream 和 istream 类的对象流插入 和流提取 运算符都在各自的类中重载了因此 cout 可以自动识别类型就是根据 运算符重载和函数重载达到的 在类中实现流插入 和流提取 运算符重载时通常我们都是 cout 和 cin 在左由于左操作数必须为第一参数而类中的成员函数第一参数是 this此时 和 运算符便不能实现在自己写的类中需要写到全局域中 但是声明在全局域中时便不能访问类的私有成员这里可以在类中进行友元函数声明 //返回 ostream 对象为了可以连续输出 inline ostream operator(ostream out, const Date d) {out d._year 年 d._month 月 d._day 日 endl;return out; }//返回 istream 对象为了可以连续输入 inline istream operator(istream in, Date d) {in d._year d._month d._day;return in; }2. 赋值运算符重载的定义 赋值运算符重载也是 C 提供的一个特殊的成员函数用于将一个已存在对象赋值给另一个已存在的对象 //赋值运算符重载 Date operator(const Date d) {//自己给自己赋值不需要处理if(this ! d){_year d._year;_month d._month;_day d._day;}//this 指针指向的对象在上一层栈帧中可以返回引用return *this; }参数传 const 引用是为了防止源数据被修改以及避免拷贝构造返回值 *this 是为了支持连续赋值并且满足连续赋值的含义d1 d2 d3返回值传引用是为了避免拷贝构造 #include iostreamusing namespace std;class Date { public:Date(int year 1970, int month 1, int day 1): _year(year), _month(month), _day(day){}//赋值运算符重载Date operator(const Date d){//自己给自己赋值不需要处理if (this ! d){_year d._year;_month d._month;_day d._day;}//this 指针指向的对象在上一层栈帧中可以返回引用return *this;}void Print(){cout _year 年 _month 月 _day 日 endl;}private:int _year;int _month;int _day; };int main() {Date d1;Date d2(2020, 9, 1);Date d3(2023, 2, 6);d1.Print(); //输出 1970年1月1日d2.Print(); //输出 2020年9月1日d3.Print(); //输出 2023年2月6日//赋值运算符重载支持连续赋值d1 d2 d3;d1.Print(); //输出 2023年2月6日d2.Print(); //输出 2023年2月6日d3.Print(); //输出 2023年2月6日return 0; }3. 编译器自动生成的赋值运算符重载 如果类中没有赋值运算符重载函数编译器会自动生成赋值运算符重载函数那编译器自动生成的赋值运算符重载函数的行为是什么呢 内置类型的成员变量进行浅拷贝自定义类型的成员变量调用该类型的赋值运算符重载函数 由于类中没有赋值运算符重载函数时编译器会自动生成因此类的赋值运算符重载不可以写在全局中 五、取地址运算符重载
  10. const 成员函数 在成员函数括号后用 const 修饰称为 const 成员函数实际上是用 const 修饰成员函数参数中的 this 指针使得调用成员函数的对象的成员变量在函数中不能被修改并且 const 对象和非 const 对象均可以调用 const 成员函数 class Date { public://const 成员函数使得调用成员函数的对象的成员变量不会被修改void Print() const{}private:int _year;int _month;int _day; };2. 取地址运算符重载 一般不需要自己写使用编译器生成的取地址运算符重载即可 Date operator() {return this; }六、const 取地址运算符重载 一般不需要自己写使用编译器生成的 const 取地址运算符重载即可 const Date* operator() const {return this; }空类其实并不是什么都没有编译器会自动生成这 6 个 默认成员函数 默认成员函数用户没有显式实现时编译器会自动生成