ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

C++类和对象:六个默认成员函数详解

C++类和对象:六个默认成员函数详解 一、引言在上一篇中我们学习了如何定义一个类、如何创建对象、理解this指针也简单了解了面向对象的三大特性。今天我们学习C类和对象中另外一个重要的知识点默认成员函数。在学习C语言以及数据结构时我们完成一个数据结构通常需要写初始化函数Initial、销毁函数Destroy等各种函数来实现我们的数据结构不仅在实现时麻烦而且还需要我们在每次使用时手动调用。C的创始人显然考虑到了这些问题于是便在发明C时创造出了默认成员函数这个概念。今天我们就来学习一下六个默认成员函数它们分别是构造函数Constructor析构函数Destructor拷贝构造函数Copy Constructor运算符重载Assignment Operator取地址运算符重载Address-of Operatorconst取地址运算符重载const Address-of Operator默认成员函数也属于成员函数它之所以叫默认成员函数是因为如果我们不显示地定义它们编译器也会在需要的时候自动生成。但是编译器生成地不一定符合我们的预期所以我们要理解它们在必要的时候自己实现。二、构造函数在创建对象时自动调用来初始化对象代替我们之前写的 Init() 函数构造函数的特点函数名与类名相同无返回值不是void而是什么都不写实例化对象时自动调用可以进行重载观察以上特点我们会发现它很好地解决了我们之前提到的问题不用再自己去实现简单的初始化函数、不用再自己调用函数来初始化对象。为了更好地理解我们本篇文章去实现一个简单的日期类在实现的过程中来感受各个默认成员函数的用法和作用。Class Date { private: int _year; int _month; int _day; public: //构造函数 Date(int year 2000, int month 1, int day 1) { _year year; _month month; _day day; } }; int main() { Date d1(2026, 8, 29); Date d2; return 0; }这样一个简单的构造函数就完成了对象d1被初始化为2026年8月29日对象d2被初始化为2000年1月1日。下面这种情况是错误的Class Date { private: int _year; int _month; int _day; public: //构造函数 Date(int year, int month, int day) { _year year; _month month; _day day; } }; int main() { Date d2; return 0; }由于我们显示地写了构造函数编译器便不会再生成但是我们在实例化对象时又没有传递参数因此我们在创建d2时它就找不到可以调用的构造函数发生错误。所以我们一定要注意不能让对象在实例化出来时找不到可以调用的构造函数如果我们没有显示地定义构造函数C编译器会自动帮我们生成一个无参的构造函数。三、析构函数在对象生命周期结束时自动调用来销毁对象释放资源相当于 Destroy() 函数析构函数特点函数名为~类名没有返回值没有参数不能重载一个类只能有一个析构函数对象销毁时由编译器自动调用Class Date { private: int _year; int _month; int _day; public: //构造函数 Date(int year 2000, int month 1, int day 1) { _year year; _month month; _day day; } //析构函数 ~Date() { cout 析构函数 endl; } }; int main() { Date d1(2026, 8, 29); Date d2; return 0; }由于我们的日期类目前没有需要动态分配的资源因此并不需要析构函数此处写一个只有输出的析构函数向大家展示一下。在程序运行结束后会答应两个“析构函数”分别为d1和d2销毁时调用的析构函数所打印的。注意如果没有显示地定义析构函数编译器会自动生成一个空的析构函数。因此如果类中有动态分配的资源必须显示定义析构函数来释放资源否则会造成内存泄露。四、拷贝构造函数拷贝构造函数是用同类型的对象来初始化新对象的构造函数函数特点参数必须是本类类型的引用在对象发生拷贝行为时自动调用本质是构造函数的重载Class Date { private: int _year; int _month; int _day; public: //构造函数 Date(int year 2000, int month 1, int day 1) { _year year; _month month; _day day; } //拷贝构造函数 Date(const Date other) { _year other._year; _month other._month; _day other._day; } }; int main() { Date d1(2026, 8, 29); Date d2(d1); Date d3 d1; return 0; }首先我们解释一下在什么情况下会自动调用。最简单的情况就是示例中这样d2和d3这两种方式都可以只是写法不一样d3这种写法可读性更高一点。除了这种明显的拷贝还有其他只要发生拷贝行为的情况都会调用拷贝构造函数。比如传参时如果是传值传参我们说形参是实参的拷贝这时就会调用拷贝构造函数来对实参进行拷贝。再比如函数返回时返回的也是值的拷贝。这也就解释了为什么参数必须是本类类型的引用。如果不是引用那么在调用拷贝构造函数时要将本类类型的对象先拷贝一份形参而拷贝形参时又要调用拷贝构造函数拷贝构造函数的形参又是本类类型的对象这样就陷入了无穷调用递归。与我们当时学习函数递归时的死递归有点像。深拷贝 vs 浅拷贝编译器自动生成的拷贝构造函数只能进行浅拷贝如果需要深拷贝则需要我们自己来实现下面我们来聊聊深浅拷贝的区别浅拷贝Shallow Copy按字节复制只复制指针本身不复制指针指向的内容。深拷贝Deep Copy不仅复制指针还复制指针指向的内容重新分配内存。举个例子大家就明白了比如我们想拷贝一份顺序表那么只需要进行浅拷贝即可因为浅拷贝只复制它的内容但如果我们想复制一份链表就需要深拷贝如果进行浅拷贝只会复制结点本身而无法复制结点指向的内容因此两份链表会指向同一块空间这时就需要深拷贝来重新开辟空间来存放链表结点指向的内容。如图所示浅拷贝就是只拷贝表面上的值也就是地址但拷贝完指向的还是同一块空间深拷贝则是重新开辟出相同的空间将原空间的内容拷贝过来再重新指向。实在不明白也没关系只需要记住涉及到动态资源的管理那么就需要显示定义深拷贝。内容补充拷贝构造函数的形参最好使用 const 修饰否则会让人误以为会修改源对象。拷贝构造函数的访问权限拷贝构造函数可以使 public 或 private如果是 private 就表明禁止对象被拷贝五、运算符重载我们常用的运算符、-、、*等只能对内置类型进行运算如果想对自定义类型也实现运算符的运算比如我们的日期类两个日期之间进行相减算出相差的天数或者日期加一个天数算出另一个日期那么就需要运算符重载。首先我们来看一下运算符重载的基本语法有返回值函数名为 operator 要重载的运算符this 指针默认作为第一个参数也就是左操作数Class Date { private: int _year; int _month; int _day; public: //构造函数 Date(int year 2000, int month 1, int day 1) { _year year; _month month; _day day; } //拷贝构造函数 Date(const Date other) { _year other._year; _month other._month; _day other._day; } //赋值运算符重载 Date operator(const Date other) { _year other._year; _month other._month; _day other._day; } //、-运算符重载 //日期天数得到新日期 Date operator(int days) { Date d(*this); d._day day; while (d._day GetMonthDay(d._year, d._month)) { d._day - GetMonthDay(d._year, d._month); if (d._month 12) { d._month; } else { d._month 1; d._year; } } return d; } //日期-日期得到天数 int operator-(const Date d) { int DaySum1 0; if (_month ! 1) { for (int i 1;i _month;i) { DaySum1 GetMonthDay(_year, i); } } DaySum1 _day; int DaySum2 0; if (d._month ! 1) { for (int i 1;i d._month;i) { DaySum2 GetMonthDay(d._year, i); } } DaySum2 d._day; int YearSum 0; if (_year ! d._year) { for (int i d._year;i _year;i) { if (i % 4 0 i % 100 ! 0 || i % 400 0) { YearSum 366; } else { YearSum 365; } } } return YearSum DaySum1 - DaySum2; } }; int main() { Date d1(2026, 8, 29); Date d2(d1); Date d3 d1; Date d4 d1 - d2; d3 d1 100; return 0; }这里简单为大家实现了和-的重载如此一来就能够对我们的自定义类型也进行示例中加或减的运算了。要注意的是this 指针已经作为了第一个参数因此对于二元操作符我们只需要传递一个参数。重载后的运算符与原运算符具有相同的优先级重载时要注意运算符对于此类型是否有意义。比如对于我们的日期类来说* 和 / 这样的运算符重载出来没有实际意义。.*、::、sizeof、?:、.这五个运算符不支持重载为了区分前置/--与后置/--我们在后置/--重载时会添加一个int参数构成重载// 前置 Date operator() { *this 1; return *this; } // 后置 Date operator(int) { Date d(*this); _day 1; if (_day GetMonthDay(_year, _month)) { _day - GetMonthDay(_year, _month); if (_month 12) { _month; } else { _month 1; _year; } } return d; }流插入/流提取运算符如果我们想输出自定义类型的对象在之前我们可以写一个 print 函数来实现学习了运算符重载后我们就可以直接使用 和 对自定义类型进行输入和输出了。我们重载 和 时需要重载为全局函数如果重载为成员函数this 指针会抢占第一个参数位置也就是操作符的左操作数调用时就变成了d cout来输出d。语法上没有错误但是不符合我们的书写习惯可读性较差因此我们最好将 和 重载为全局函数。但是重载为全局函数又存在一个问题我们在外部无法访问 _year 等内部成员变量。所以在这里我们简单介绍一下什么是友元函数这时下一期要讲的内容。简单来说友元函数就是在类中声明的一个函数使它能够访问类中的成员变量。只要在类中添加一句声明friend ostream operator(ostream out, const Date d);这个外部函数就能够访问类中的成员变量了Class Date { private: int _year; int _month; int _day; public: //友元函数 friend ostream operator(ostream out, const Date d); //构造函数 Date(int year 2000, int month 1, int day 1) { _year year; _month month; _day day; } //拷贝构造函数 Date(const Date other) { _year other._year; _month other._month; _day other._day; } //赋值运算符重载 Date operator(const Date other) { _year other._year; _month other._month; _day other._day; } //、-运算符重载 //日期天数得到新日期 Date operator(int days) { Date d(*this); d._day day; while (d._day GetMonthDay(d._year, d._month)) { d._day - GetMonthDay(d._year, d._month); if (d._month 12) { d._month; } else { d._month 1; d._year; } } return d; } //日期-日期得到天数 int operator-(const Date d) { int DaySum1 0; if (_month ! 1) { for (int i 1;i _month;i) { DaySum1 GetMonthDay(_year, i); } } DaySum1 _day; int DaySum2 0; if (d._month ! 1) { for (int i 1;i d._month;i) { DaySum2 GetMonthDay(d._year, i); } } DaySum2 d._day; int YearSum 0; if (_year ! d._year) { for (int i d._year;i _year;i) { if (i % 4 0 i % 100 ! 0 || i % 400 0) { YearSum 366; } else { YearSum 365; } } } return YearSum DaySum1 - DaySum2; } }; ostream operator(ostream out, const Date d) { out d._year / d._month / d._day endl; return out; } int main() { Date d1(2026, 8, 29); Date d2(d1); Date d3 d1; Date d4 d1 - d2; d3 d1 100; cout d1 d2 d3 d4 endl; return 0; }这样我们就能够直接使用 来输出我们的自定义类型了。输入也是同理。六、取地址运算符重载1.const 修饰成员函数我们如果定义了一个 const 修饰的成员变量const Date d1;如果调用了成员函数就会出现我们之前提到过的权限放大因为这个对象本身被 const 修饰而我们调用成员函数时的this 指针并没有被 const 所修饰所以我们在成员函数中依然可以用 *this 来修改我们的 d1。因此我们在定义成员函数时为了让 const 修饰的对象也能调用成员函数我们可以将不修改对象的函数也用 const 修饰起来。例如void Print() const;编译后变为void Print ( const Date* const this );2.取地址运算符重载Date* operator() { return this; }这里目前也没什么可讲的需要注意的就是我们取地址的对象有没有被 const 修饰或者如果我们不希望const修饰的对象被取地址可以在重载时直接返回 nullptr。绝大多数情况下无需我们显式定义默认生成的就足够除非有一些特殊的需求。十、总结以上就是我们本篇的全部内容了。这一篇我们学到了构造函数对象创建时自动调用用于初始化成员变量。推荐使用初始化列表。析构函数对象销毁时自动调用用于释放资源。拷贝构造函数用已有对象创建新对象时调用有动态资源时需要深拷贝。拷贝赋值运算符已有对象被赋值时调用同样需要注意深拷贝。取地址运算符通常不需要重载使用编译器默认生成的即可。下篇预告类和对象下—— static 成员、友元、内部类、匿名对象……
返回列表