尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

C++:类和对象(中)

C++:类和对象(中) 1.析构函数2.拷贝构造函数3.赋值运算符重载4.取地址运算符重载1.析构函数析构函数与构造函数功能相反析构函数不是完成对对象本⾝的销毁比如局部对象是存在栈帧的函数结束栈帧销毁他就释放了不需要我们管C规定对象在销毁时会自动调用析构函数完成对象中资源的清理释放⼯作。析构函数的功能类比我们之前Stack实现的Destroy功能而像Date没有Destroy其实就是没有资源需要释放所以严格说Date是不需要析构函数的。析构函数特点1、析构函数名在类名前加上2、不需要参数和返回值连 void 也不需要写3、对象生命周期结束时系统会自动调用析构函数4、如果类中没有显式定义析构函数那么 C 编译器会自动生成一个析构函数。一旦用户显式定义编译器将不再自动生成。析构函数对内置类型不做处理对自定义类型则会调用自定义类型的析构函数。5、我们显式写析构函数对于自定义类型编译器也会自动调用它的析构函数。6、当类中没有需要释放的资源时编译器自动生成的析构函数就够用不需要我们显式定义。7、一个局部域的多个对象C 规定后定义的先析构。class Date { public: Date(int year 1, int month 1, int day 1) { _year year; _month month; _day day; } void Print() { cout _year / _month / _day endl; } ~Date() { cout this~Date() endl; } private: int _year; int _month; int _day; }; int main() { // 后定义的先析构 Date d1; Date d2(2026, 8,6); d1.Print(); d2.Print(); return 0; }此时我们定义了析构函数Date通过调试我们发现后定义的d2先调用了析构函数先定义的d1后调用了析构函数这体现了析构函数的后定义的先析构的特点。同时也体现析构函数自动调用的特点。class Stack { public: Stack(int n 4) { _a (int*)malloc(sizeof(int) * n); if (nullptr _a) { perror(malloc申请空间失败); return; } _capacity n; _top 0; } ~Stack() { cout this ~Stack() endl; if (_a) { free(_a); _a nullptr; _top _capacity 0; } } private: int* _a; size_t _capacity; size_t _top; }; class MyQueue { public: ~MyQueue() { cout this ~MyQueue() endl; } private: Stack _st1; Stack _st2; // int _size; }; int main() { Stack st1; MyQueue q; return 0; }这里体现了内置类型q虽然调用了它的析构函数但是其中的自定义类型st1和st2还是会调用它自己的析构函数同时也体现了后定义先析构的特点。总结对于大多数的类都要写构造少数的类需要写析构有资源申请2.拷贝构造函数如果一个构造函数的第⼀个参数是自身类类型的引用且任何额外的参数都有默认值则此构造函数也叫做拷⻉构造函数也就是说拷⻉构造是⼀个特殊的构造函数。拷贝构造函数特点1.拷贝构造函数是构造函数的一个重载。2.拷贝构造函数的第一个参数必须是类类型对象的引用使用传值方式编译器直接报错因为语法逻辑上会引发无穷递归调用。 拷贝构造函数也可以多个参数但是第一个参数必须是类类型对象的引用后面的参数必须有缺省值。3.C规定自定义类型对象进行拷贝行为必须调用拷贝构造所以这里自定义类型传值传参和传值返回都会调用拷贝构造完成。4.若未显式定义拷贝构造编译器会生成自动生成拷贝构造函数。自动生成的拷贝构造对内置类型成员变量会完成值拷贝/浅拷贝(一个字节一个字节的拷贝)对自定义类型成员变量会调用他的拷贝构造。5.如果一个类显示实现了析构并释放资源那么他就需要显示写拷贝构造否则就不需要。6.传值返回会产生⼀个临时对象调用拷贝构造传值引用返回返回的是返回对象的别名(引用)没有产生拷贝。但是如果返回对象是⼀个当前函数局部域的局部对象函数结束就销毁了那么使用引用返回是有问题的这时的引用相当于一个野引用类似一个野指针⼀样。传引用返回可以减少拷贝但是⼀定要确保返回对象在当前函数结束后还在才能用引用返回。class Date { public: Date(int year 1, int month 1, int day 1) { _year year; _month month; _day day; } // Date d3(d2); Date(const Date d) { _year d._year; _month d._month; _day d._day; } void Print() { cout _year / _month / _day endl; } private: int _year; int _month; int _day; }; int main() { Date d1; //const Date d2(2026, 8, 6); Date d2(2026, 8, 6); d1.Print(); d2.Print(); // 拷贝构造 Date d3(d2); Date d4 d2; }这里的d3d2和d4d2都是拷贝构造而拷贝构造函数中第一个参数是类类型对象的引用而又利用了this指针去帮助值拷贝。这样用引用就是防止值传参导致无限拷贝构造而这里用const是为了防止拷贝时写反导致权限扩大的难题。//错误写法 Date Func() { Date d(2026, 8, 6); return d; } //正确写法 Date Func() { Date d(2026, 8, 6); return d; }这种形式是错误的形式因为它返回的是d的别名而d已经销毁了从而导致出现随机值因此下面这个是正确的写法符合拷贝构造特点。因此返回局部对象只能传值返回不能引用返回。而这里如果我们不写Date的拷贝构造它会自动调用生成的拷贝构造这是一个一个字节的拷贝。接下来让我们看下不写栈的拷贝构造可以用吗class Stack { public: Stack(int n 4) { _a (int*)malloc(sizeof(int) * n); if (nullptr _a) { perror(malloc申请空间失败); return; } _capacity n; _top 0; ~Stack() { cout this ~Stack() endl; if (_a) { free(_a); _a nullptr; _top _capacity 0; } } private: int* _a; size_t _capacity; size_t _top; }; int main() { Stack st1; Stack st2 st1; return 0; }通过监视我们发现虽然我们没写拷贝构造但是系统自动生成的拷贝构造也会自动调用并且一个一个字节的拷贝这就导致str1和str2的地址相同从而析构时会出现一个地址析构两次系统就会报错。// st2(st1) Stack(const Stack st) { _a (int*)malloc(sizeof(int) * st._capacity); if (nullptr _a) { perror(malloc申请空间失败); return; } memcpy(_a, st._a, sizeof(int) * st._top); _top st._top; _capacity st._capacity; }所以我们就要自己写拷贝构造。通过this指针和st和memcpy来实现拷贝构造。这次我们在通过监视就发现str1和str2的地址虽然不同但是拷贝完成了这样析构时各自互不影响。接下来我们看下用两个栈去实现队列class MyQueue { public: private: Stack _st1; Stack _st2; }; int main() { MeQuene q1; MeQuene q2(q1); return 0; }结合上面栈的拷贝构造和构造和析构。因为自定义类型会调用它的拷贝构造虽然我们没写MeQuene的拷贝构造和构造和析构但是系统会自动生成这样我们在调用拷贝构造时就会自动调用自定义类型的拷贝构造。q1和q2的拷贝构造的str1和str2也会成功构造。//Stack Func() // 错误的 Stack Func() { Stack st(100); return st; }上面注释的是错误写法因为如果用引用返回返回的就是临时变量函数结束时就会销毁引用就会出现问题。所以我们要用传参返回这样就会利用临时对象的拷贝构造就不会出现问题。3.赋值运算符重载3.1运算符重载1.当运算符被用于类类型的对象时C语言允许我们通过运算符重载的形式指定新的含义。C规定类类型对象使用运算符时必须转换成调用对应运算符重载若没有对应的运算符重载则会编译报错。2.运算符重载是具有特殊名字的函数他的名字是由operator和后⾯要定义的运算符共同构成。和其他函数⼀样它也具有其返回类型和参数列表以及函数体。3.重载运算符函数的参数个数和该运算符作用的运算对象数量一样多。一元运算符有一个参数二元运算符有两个参数二元运算符的左侧运算对象传给第一个参数右侧运算对象传给第二个参数。4.如果一个重载运算符函数是成员函数则它的第一个运算对象默认传给隐式的this指针因此运算符重载作为成员函数时参数比运算对象少⼀个。5.运算符重载以后其优先级和结合性与对应的内置类型运算符保持⼀致。6.不能通过连接语法中没有的符号来创建新的操作符⽐如operator。7..* :: sizeof ?: .注意以上5个运算符不能重载。8. 重载操作符至少有一个类类型参数不能通过运算符重载改变内置类型对象的含义如 int operator(int x, int y)9.一个类需要重载哪些运算符是看哪些运算符重载后有意义比如Date类重载operator-就有意义但是重载operator*就没有意义。10.重载运算符时有前置和后置运算符重载函数名都是operator无法很好的区分。C规定后置重载时增加⼀个int形参跟前置构成函数重载方便区分。11.重载和时需要重载为全局函数因为重载为成员函数this指针默认抢占了第⼀个形参位置第⼀个形参位置是左侧运算对象调⽤时就变成了 对象cout不符合使⽤习惯和可读性。重载为全局函数把ostream/istream放到第一个形参位置就可以了第二个形参位置当类类型对象。先看一个简单的案例class Date { public: Date(int year 1, int month 1, int day 1) { _year year; _month month; _day day; } void Print() { cout _year / _month / _day endl; } private: int _year; int _month; int _day; }; bool operator(const Date x1, const Date x2) { return x1._year x2._year x1._month x2._month x1._day x2._day; } int main() { Date d1(2026, 9, 3); Date d2(2026, 8, 6); d1.Print(); d2.Print(); cout operator(d1, d2)endl; return 0; }这里重载运算符但出现问题因为这个运算符重载函数在类外面它不能去访问类里面的数值这样就会导致报错。所以我们要修改可以把类中的数据全部修改为公有的这样就可以访问了同时我们可以用下面的形式去打印这样体现出重载的意义。cout (d1 d2) endl;重载为全局的⾯临对象访问私有成员变量的问题有几种方法可以解决1、成员放公有2、Date提供getxxx函数3、友元函数4、重载为成员函数必须加括号因为的优先级比优先级大所以要加括号class Date { public: Date(int year 1, int month 1, int day 1) { _year year; _month month; _day day; } void Print() { cout _year / _month / _day endl; } // d1 d2; // d1.operator(d1, d2) //bool operator(Date* const this, const Date d) { bool operator(const Date d) { return _year d._year _month d._month _day d._day; } // d1 d2 bool operator(const Date d) { if (_year d._year) { return true; } else if (_year d._year) { if (_month d._month) { return true; } else if (_month d._month) { return _day d._day; } } return false; } private: int _year; int _month; int _day; }; int main() { Date d1(2026, 9, 3); Date d2(2026, 8, 6); d1.Print(); d2.Print(); //cout operator(d1, d2)endl; cout d1.operator(d2)endl; cout (d1 d2) endl; cout (d1 d2) endl; return 0; }这串代码把运算符重载写在类里面了这样就不用把成员变量放在公有中调用时利用隐氏的this指针完成附值的任务而对于这里的重载运算符用于比较两个变量的大小。3.1.1日期类声明和定义分离Date.h#includeiostream #includeassert.h using namespace std; #pragma once class Date { public: Date(int year 1, int month 1, int day 1); // 类里面直接定义默认是inline void Print() { cout _year / _month / _day endl; } bool operator(const Date d); bool operator(const Date d); bool operator(const Date d); bool operator(const Date d); bool operator(const Date d); bool operator!(const Date d); Date operator(int day); Date operator(int day); Date operator-(int day); Date operator-(int day); private: int _year; int _month; int _day; };Date.cpp#includeDate.h Date::Date(int year, int month, int day) { _year year; _month month; _day day; } bool Date::operator(const Date d) { return _year d._year _month d._month _day d._day; } bool Date::operator(const Date d) { if (_year d._year) { return true; } else if (_year d._year) { if (_month d._month) { return true; } else if (_month d._month) { return _day d._day; } } return false; } int GetMonthDay(int year, int month) { assert(month 0 month 13); int monthDayArr[13] { 0, 31,28,31,30,31,30,31,31,30,31,30,31 }; // 闰年2月 if (month 2 ((year % 4 0 year % 100 ! 0) || year % 400 0)) return 29; return monthDayArr[month]; } // d1 100 Date Date::operator(int day) { _day day; while (_day GetMonthDay(_year, _month)) { _day - GetMonthDay(_year, _month); _month; if (_month 13) { _year; _month 1; } } return *this; } // d1 100 Date Date::operator(int day) { //Date tmp(*this); Date tmp *this; tmp day; return tmp; }Test.cpp#includeDate.h int main() { Date d1(2026, 9, 3); Date d2(2026, 8, 6); d1.Print(); d2.Print(); cout (d1 d2) endl; cout (d1 d2) endl; Date d3 d2 100; d3.Print(); d2.Print(); return 0; }这里的赋值运算符重载返回要返回this指针只可以在类中显示写形参和实参都不能写因为只有this指针可以得到这里的d在自身加减后的d。而d又不是局部变量所以可以用引用返回。这里我们可以通过写的函数调用来运用拷贝构造写的函数也可以通过this指针实现的函数。这里如果运用tmp接收this指针返回类型必须是赋值返回因为tmp是局部对象出作用域自动销毁。class A { public: void func() { cout A::func() endl; } }; // 成员函数的函数指针 typedef void (A::*PF)(); void sortFunc(PF pf) { A a; // 对象回调成员函数的函数指针 (a.*pf)(); } int main() { sortFunc(A::func); }这里是关于.*的用法如果是成员函数的函数指针就要加上指定的类域如果我们要回调sortfunc我们就要加上才能得到函数指针。而这里用类定义的对象就要利用.*来调用成员函数指针。3.1.3重载和首先我们要明白这重载运算符和 是为了访问自定义类型如果是内置类型就调用库里面的输入和输出自定义类型就调用自定义类型的输入和输出。//d1 cout //成员的方式重载无法满足我们正常使用流习惯和可读性 void operator(ostream _cout) { _cout _year / _month / _day endl; } int main() { cout d1; return 0; }这种成员函数的方式无法满足我们的需求因为这里的cout对应了隐氏的this指针d对应了_cout。这样对不上也传不过来也就是成员函数错位了。但可以反过来利用这个函数但与我们想要实现的函数天差地别。因此我们只能把它写成全局函数。// 重载为全局函数可以控制流对象和Date位置 // coutd ostream operator(ostream _cout, const Date d); // cind istream operator(istream _cin, Date d); //这里d不加const是因为要在流中提取到数值因此不要加const如果我们把这写到头文件中记得在前面加上static这样可以避免重定义。但如果我们只在头文件中声明在源文件中定义就会出现访问不到私有成员这样我们可以利用成员函数Getyear、GetMonth、Getday得到私有也可以通过下面的友元函数直接得到私有成员。友元函数只需写在类里面就可以帮助我们得到私有成员ostream operator(ostream _cout, const Date d) { _cout d._year / d._month / d._day endl; return _cout; } istream operator(istream _cin, Date d) { while (1) { cout 请依次输入年月日:; _cin d._year d._month d._day; if (d.IsValid()) break; else cout 日期非法请重新输入 endl; } return _cin; }这是我们在源文件中写的运算符重载函数这里的运算符重载支持连续使用这就需要返回值因此我们要利用引用返回来得到返回值。3.1.2前置和后置Date.h// d; Date operator(); // d; Date operator(int); // --d Date operator--(); // d--; Date operator--(int);Date.cpp// d; Date Date::operator() { *this 1; return *this; } // d; // 形参int只是占位跟前置进行函数重载区分 Date Date::operator(int) { Date tmp(*this); *this 1; return tmp; } // --d Date Date::operator--() { *this - 1; return *this; } // d--; Date Date::operator--(int) { Date tmp(*this); *this - 1; return tmp; }前置先在d的基础上直接1所以函数返回时直接可以引用返回this指针1之后的值。后置则是返回1之前的值所以需要一个tmp来接收之前的值返回时利用传值返回tmp是临时变量并且后置在定义中需要增加一个int形参跟前置构成重载。3.2赋值运算符重载默认成员函数⽤于完成两个已经存在的对象直接的拷贝赋值。赋值运算符重载的特点1.必须重载为成员函数。参数建议写成const 当前类类型引用(值传参有拷贝)2.有返回值(建议写成当前类类型引用提高效率)可以支持连续赋值场景3.没有显式自动生成默认赋值运算符重载对内置类型成员变量是值拷贝/浅拷贝对自定义类型成员变量是调用赋值重载函数。4.类成员变量全是内置类型没有指向什么资源自动生成的足够。内置类型指向了资源要实现深拷⻉。内部要是自定义类型A成员该重载会调用A的赋值运算符重载。显示实现了析构并释放资源就要显示写赋值运算符重载反之不用。Date.h//d1 d2 //d1 d1 Date operator(const Date d) { //不用检查自己给自己赋值的情况 if (this ! d) { _year d._year; _month d._month; _day d._day; } return *this; }这里利用传参用引用的方式减少拷贝同时避免自己给自己赋值的情况(深拷贝)。Test.cppvoid TestDate1() { Date d1(2026, 9, 3); Date d2(2026, 8, 6); d1.Print(); d2.Print(); // 拷贝构造一个已经存在的对象去初始化一个要创建的对象 Date d3(d1); Date d4 d1; // 赋值重载一个已经存在的对象拷贝给另一个已经存在的对象 d3 d1 d2; d1 d1; } int main() { TestDate1(); return 0; }赋值重载支持连续赋值且是从右往左赋值。看上面代码先把d2赋值给d1在把d1赋值给d3绝大多数类都要写构造一个类只要写了析构就要写拷贝构造和赋值重载3.3日期类完善bool Date::operator(const Date d) { return _year d._year _month d._month _day d._day; } bool Date::operator(const Date d) { if (_year d._year) { return true; } else if (_year d._year) { if (_month d._month) { return true; } else if (_month d._month) { return _day d._day; } } return false; } // d1 d2 bool Date::operator(const Date d) { return *this d || *this d; } // d1 d2; // d2 d1 bool Date::operator(const Date d) { return !(*this d); // return d *this; } bool Date::operator(const Date d) { return !(*this d); } bool Date::operator!(const Date d) { return !(*this d); }这里我们只需写和或和这两类运算符重载就可以通过复用他们实现其他几个运算符重载。int GetMonthDay(int year, int month) { assert(month 0 month 13); int monthDayArr[13] { 0, 31,28,31,30,31,30,31,31,30,31,30,31 }; // 闰年2月 if (month 2 ((year % 4 0 year % 100 ! 0) || year % 400 0)) return 29; return monthDayArr[month]; } // d1 100 // d1 -100 Date Date::operator(int day) { if (day 0) { return *this - -day; } _day day; while (_day GetMonthDay(_year, _month)) { _day - GetMonthDay(_year, _month); _month; if (_month 13) { _year; _month 1; } } return *this; } // d1 100 Date Date::operator(int day) { //Date tmp(*this); Date tmp *this; tmp day; return tmp; } Date Date::operator-(int day) { if (day 0) { return *this -day; } _day - day; while (_day 1) { --_month; if (_month 0) { --_year; _month 12; } _day GetMonthDay(_year, _month); } return *this; } // d1 - 100; Date Date::operator-(int day) { Date tmp(*this); tmp - day; return tmp; }这段代码我们实现复用了而实现-复用了-这样我们不仅可以实现代码复用而且修改时也大大减少难度。但我们在复用时还是建议先写和-因为这样利于观察。并且这里如果出现加减负数也可以复用它们各自的代码使其正确实现。// d1 - d2 int Date::operator-(const Date d) { Date max *this; Date min d; int flag 1; if (*this d) { max d; min *this; flag -1; } int n 0; while (min ! max) { min; n; } return n*flag; }我们可以利用上面这张图的方法这张图表达的是先算出各自与当年1月1日的相隔天数然后通过计算两个年数之间的闰年个数。也可以利用上面代码的方法上面的代码是假设d1大d2小如果是错的就d1小d2大。然后通过n来计算小的年一直加到大的年之间的天数同时利用flag了得到正确的正负因为n只拿到间隔的绝对值正负信息丢掉了上面我们也写了重载运算符和但是如果我们调用输入一个非法值还是会正常运行因此我们要写一个函数去判断数值是否非法。bool Date::IsValid() { if (_year 1) return false; if (_month 1 || _month 12) return false; if (_day 1 || _day GetMonthDay(_year, _month)) return false; return true; }这段代码就可以帮助我们判断一个数值是否合法我们还可以把这段代码写到构造函数里面这样可以防止构造出错误的日期Date::Date(int year, int month, int day) { _year year; _month month; _day day; if (!IsValid()) { cout 构造日期非法 *this endl; assert(false); } }这里的cin的输入效率没有scanf高大量数据输入时cin需要多次调用scanf只需一次调用但我们可以通过利用inline函数来提高效率直接把重载运算符和加上inline这样就可以大大提高效率。inline ostream operator(ostream _cout, const Date d) { _cout d._year / d._month / d._day endl; return _cout; } inline istream operator(istream _cin, Date d) { while (1) { cout 请依次输入年月日:; _cin d._year d._month d._day; if (d.IsValid()) break; else cout 日期非法请重新输入 endl; } return _cin; }4.取地址运算符重载4.1const成员函数将const修饰的成员函数称之为const成员函数const修饰成员函数放到成员函数参数列表的后面const实际修饰该成员函数隐含的this指针表明在该成员函数中不能对类的任何成员进⾏修改。例如const 修饰Date类的Print成员函数Print隐含的this指针由 Date* const this变为constDate* const this这里我们发现如果在d1的最前面加上const我们就无法调用Printf函数因为这里的权限放大不允许权限放大而d2可以正常使用是因为d2的权限平移。这里隐含的this指针是Date*类型如果我们把this指针改为const Date*的形式d1和d2都可以正常使用d1的权限平移d2的权限缩小。const在成员函数中的位置非常刁钻(如上图)。并且加上const以后就不能修改成员变量。因此如果一个成员函数不需要修改成员变量就可以加上const这样就不会有权限放大的问题。4.2取地址运算符重载分为普通取地址符重载和const取地址运算符重载一般编译器自动生成的够用。Date* operator() { return this; } const Date* operator() const { return this; } int main() { const Date d1(2026, 9, 3); Date d2(2026, 8, 7); d1.Print(); d2.Print(); cout d1 endl; cout d2 endl; }这里d1调用const取地址运算符d2调用普通取地址运算符。
返回列表