资讯详情

类和对象(2)

📅 2026/10/24 2:05:03 | 华诺云谱 👁 阅读
类和对象(2)
默认成员函数⽤⼾没有显式实现编译器⾃动⽣成的成员函数。类中如果不写任何实现编译器会默认生成6 个成员函数构造函数、析构函数、拷贝构造函数、拷贝赋值重载。这些涉及对象的创建、销毁和拷贝是资源管理的核心需要重点学习。取地址重载普通取地址和const取地址。简单了解即可。C11 又增加了2 个默认成员函数移动构造和移动赋值。需要掌握的是我们不写时编译器默认⽣成的函数是什么是否满⾜我们的需求。编译器默认⽣成的函数不满⾜我们的需求时如何⾃⼰实现一、构造函数应写尽写构造函数的主要任务不是开空间创建对象对象空间在栈帧创建时已分配而是实例化时初始化对象。其本质是替代 C 语言中手动调用的Init初始化函数利用“对象实例化时系统自动调用”的特性让初始化更自动化、更安全。一特点函数名与类名相同。无返回值不需写void这是 C 的强制规定。对象实例化时系统自动调用。支持函数重载可提供多种初始化方式。什么是“默认构造函数”不传实参就可以调用的构造函数。包含三类我们写的无参构造函数。我们写的全缺省构造函数所有参数都有默认值。编译器自动生成的默认构造函数。注意这三者只能存在一个。因为无参和全缺省虽构成重载但调用时如Date d1;编译器无法区分会产生歧义报错。如果没写任何构造函数编译器会自动生成一个无参默认构造如果写了任意构造函数无论是有参、无参还是缺省编译器不再生成。编译器生成的默认构造做了什么C 区分类型来处理成员变量内置类型int、char、指针等不要求初始化其初始值通常是随机值取决于编译器。自定义类型类/结构体对象必须调用该成员变量自身的默认构造函数来初始化。若该自定义类型成员没有默认构造函数即没有无参构造/全缺省构造且编译器也未生成则会编译报错。解决方案需使用初始化列表。二代码示例对象定义陷阱调用无参构造创建对象时对象后面不要跟括号即写Date d1;。若写成Date d3();编译器会将其识别为函数声明而不是创建对象。自动调用传递如示例中的MyQueue类它包含两个Stack自定义成员。即使MyQueue没写构造函数编译器生成的默认构造也会自动调用Stack的构造函数完美完成嵌套对象的初始化。class Date { public: // 1.⽆参构造函数 //Date() //{ // _year 1; // _month 1; // _day 1; //} // 2.带参构造函数 //Date(int year, int month, int day) //{ // _year year; // _month month; // _day day; //} // 3.全缺省构造函数 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; }; int main() { // 如果留下三个构造中的第⼆个带参构造第⼀个和第三个注释掉 // 编译报错error C2512 : “Date”:没有合适的默认构造函数可⽤ Date d1;// 调⽤默认构造函数 Date d2(2025, 1, 1); // 调⽤带参的构造函数 // 注意如果通过⽆参构造函数创建对象时对象后⾯不⽤跟括号否则编译器⽆法区分这⾥是函数声明还是实例化对象 // warning C4930: “Date d3(void)”: 未调⽤原型函数(是否是有意⽤变量定义的? ) Date d3(); d1.Print(); d2.Print(); return 0; }typedef int STDataType; class Stack { public: Stack(int n 4) { _a (STDataType*)malloc(sizeof(STDataType) * n); if (nullptr _a) { perror(malloc申请空间失败); return; } _capacity n; _top 0; } // ... private: STDataType * _a; size_t _capacity; size_t _top; }; // 两个Stack实现队列 class MyQueue { public: //编译器默认⽣成MyQueue的构造函数调⽤了Stack的构造完成了两个成员的初始化 private: Stack pushst; Stack popst; }; int main() { MyQueue mq; return 0; }二、析构函数析构函数不是销毁对象本身局部对象占用的栈帧随函数结束自动释放不需我们管。在对象销毁时自动调用用于完成对象中资源的清理释放如动态内存、文件句柄等。等价于 C 语言中 Stack 的Destroy函数而像Date类这种没有资源申请的严格来说不需要析构函数。一特点函数名类名前加~如~Stack()。参数与返回值无参数、无返回值不需要写void。唯一性一个类只能有一个析构函数不可重载。触发时机对象生命周期结束时系统自动调用。编译器自动生成的析构函数做了什么内置类型int、指针等不做处理不释放资源自定义类型成员其他类对象自动调用该成员自身的析构函数。即使你显式写了析构函数类中的自定义类型成员依然会自动调用其自身的析构函数何时该写何时不用写不用写使用编译器默认生成类中没有申请资源如Date。默认生成的析构够用如MyQueue它只包含Stack对象默认析构会自动调用Stack的析构释放干净。必须显式写类中动态申请了资源如Stack中用了malloc开空间。如果不写默认析构不会释放_a指向的内存会造成内存泄漏。8.局部对象的析构顺序同一个作用域内有多个对象时遵循后定义、先析构的原则构造顺序的逆序。二代码示例Stack动态申请了空间显式写了析构内部free(_a)防止泄漏。MyQueue仅包含两个Stack对象无需显式写析构编译器生成的默认析构会自动调用Stack的析构正确完成嵌套资源的释放。typedef int STDataType; class Stack { public: Stack(int n 4) { _a (STDataType*)malloc(sizeof(STDataType) * n); if (nullptr _a) { perror(malloc申请空间失败); return; } _capacity n; _top 0; } ~Stack() { cout ~Stack() endl; free(_a); _a nullptr; _top _capacity 0; } private: STDataType* _a; size_t _capacity; size_t _top; }; // 两个Stack实现队列 class MyQueue { public: //编译器默认⽣成MyQueue的析构函数调⽤了Stack的析构释放的Stack内部的资源 // 显⽰写析构也会⾃动调⽤Stack的析构 /*~MyQueue() {}*/ private: Stack pushst; Stack popst; }; int main() { Stack st; MyQueue mq; return 0; }三、拷贝构造函数定义第一个参数是自身类类型的引用通常为const修饰且其余参数均有默认值的构造函数。本质构造函数的一种重载形式用于用已存在的对象初始化同类型的新对象。一特点1.参数必须为引用若使用传值方式如Date(Date d)编译器会报错C规定传值传参会调用拷贝构造导致无限递归。传值调用拷贝构造调用拷贝构造需要传值传值又要调用拷贝构造为什么会发生无限递归假设我们写了错误的拷贝构造Date(Date d)并执行代码 Date d2(d1);用 d1 拷贝构造 d2。编译器和运行时的逻辑推演是这样的调用起点我们要用 d1 拷贝构造 d2所以需要调用拷贝构造 Date(Date d)。传值困境第一次递归这个函数的参数是 Date d它是一个值传递的参数。C 规定值传递自定义类型时形参 d 需要用实参 d1 来拷贝初始化。触发新拷贝为了完成“把 d1 拷贝给形参 d”这个动作编译器必须再次调用拷贝构造函数。回到起点再次调用的拷贝构造依然是 Date(Date d)它的参数依然是值传递。死循环为了进入第二次调用又得完成它的传值传参于是又触发第三次调用……如此层层嵌套永远不会到达函数体内部直到编译器栈溢出或直接报错C标准规定编译器必须拒绝这种代码。为什么改成引用 (const Date d) 就解决了因为引用是别名不是值。当你写 Date(const Date d) 时形参 d 只是实参 d1 的一个别名类似于给 d1 起了个小名。传递引用不需要拷贝对象它只是传递了一个地址。因此编译器不需要调用任何拷贝构造就能完成传参直接进入了函数体内部递归链条就此打断。可以有额外参数但必须提供默认值如Date(const Date d, int n 0)。调用时机自定义类型对象的拷贝行为传值传参、传值返回、显式拷贝初始化都会触发拷贝构造。2.编译器自动生成的拷贝构造若用户未显式定义编译器会自动生成一个默认拷贝构造。默认行为对内置类型成员执行浅拷贝逐字节复制。比如日期类就不需要我们显式的写拷贝构造函数对自定义类型成员调用该成员对应的拷贝构造。何时需要显式定义类成员指向动态分配的资源如 Stack 中的 _a浅拷贝会导致多个对象共享同一资源比如析构时重复释放同一块空间 → 程序崩溃入栈时互相覆盖对方资源。此时必须显式实现深拷贝为指针成员重新分配空间并复制内容。经验技巧若类显式定义了析构函数来释放资源则通常也需要显式定义拷贝构造以及拷贝赋值。3. 深拷贝 vs 浅拷贝浅拷贝仅复制指针值不复制指向的资源。深拷贝复制指针所指向的资源内容确保每个对象拥有独立的资源副本。典型场景Date成员均为内置类型无资源→ 无需显式拷贝构造默认即可。Stack含动态数组→ 必须显式深拷贝。MyQueue成员为 Stack 对象→ 编译器自动生成拷贝构造时会调用 Stack 的深拷贝因此无需再显式实现。4. 传值返回与引用返回传值返回会产生一个临时对象调用拷贝构造。性能开销较大。传引用返回返回对象的别名引用避免拷贝效率高。⚠️ 关键风险绝不能返回局部对象的引用函数结束局部对象销毁引用成为“野引用”。只有确保返回对象在函数结束后仍存在如静态、全局、或通过参数传入的引用时才可使用引用返回。二代码示例// 正确的拷贝构造const 引用 Date(const Date d) { _year d._year; _month d._month; _day d._day; } // 错误的写法值传递报错 // Date(Date d) { ... } // 深拷贝示例Stack Stack(const Stack st) { _a (STDataType*)malloc(sizeof(STDataType) * st._capacity); memcpy(_a, st._a, sizeof(STDataType) * st._top); _top st._top; _capacity st._capacity; }class Date { public: Date(int year 1, int month 1, int day 1) { _year year; _month month; _day day; } // 编译报错error C2652 : “Date”:⾮法的复制构造函数:第⼀个参数不应是“Date” //Date(Date d) Date(const Date d) { _year d._year; _month d._month; _day d._day; } Date(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; }; void Func1(Date d) { cout d endl; d.Print(); } // Date Func2() Date Func2() { Date tmp(2024, 7, 5); tmp.Print(); return tmp; } int main() { Date d1(2024, 7, 5); // C规定⾃定义类型对象进⾏拷⻉⾏为必须调⽤拷⻉构造所以这⾥传值传参要调⽤拷⻉构造 // 所以这⾥的d1传值传参给d要调⽤拷⻉构造完成拷⻉传引⽤传参可以较少这⾥的拷⻉ Func1(d1); cout d1 endl; // 这⾥可以完成拷⻉但是不是拷⻉构造只是⼀个普通的构造 Date d2(d1); d1.Print(); d2.Print(); //这样写才是拷⻉构造通过同类型的对象初始化构造⽽不是指针 Date d3(d1); d2.Print(); // 也可以这样写这⾥也是拷⻉构造 Date d4 d1; d2.Print(); // Func2返回了⼀个局部对象tmp的引⽤作为返回值 // Func2函数结束tmp对象就销毁了相当于了⼀个野引⽤ Date ret Func2(); ret.Print(); return 0; }四、运算符重载1. 什么是运算符重载给类类型的对象使用运算符时如d1 d2编译器会将其转换为调用对应的operator函数。如果没有重载编译会报错。2. 重载的语法规则函数名operator后加上运算符类似于函数名例如operator。参数个数与运算符操作数数量一致。一元运算符如1个参数。二元运算符如、2个参数左侧操作数传给第1个参数右侧传给第2个。成员函数特例如果重载为类的成员函数第一个操作数会默认传给隐藏的this指针所以参数列表少一个。3. 重要限制高频考点不能创建新运算符如operator是不允许的。以下5个运算符不能重载死记硬背.*::sizeof?: .必须有一个类类型参数不能改变内置类型的运算含义比如你不能重载int int。优先级和结合性不变重载后运算符的优先级依然和内置类型一样。6. 访问私有成员的问题如果重载为全局函数直接访问对象的私有成员会报错。解决办法把成员变量设为公有不推荐。提供公有的GetXXX接口。使用友元函数常用。直接重载为成员函数省去友元但要注意不能用此法。7、赋值运算符重载核心重点赋值运算符重载是一个特殊的默认成员函数用于两个已经存在的对象之间的拷贝赋值。7.1 必须遵守的三大特点7.必须重载为成员函数。参数建议写成const 类名传引用避免拷贝加 const 防止修改右侧对象。void operator(Date d)//这样也可以不过传值传参会有拷贝会出现额外开销但不会出现嵌套调用拷贝构造 {}返回值建议写成类名返回当前对象的引用目的是支持连续赋值如d1 d2 d3。传值返回会创建临时对象调用拷贝构造产生拷贝开销。传引用返回不产生拷贝返回的是对象的别名但必须确保返回的对象在函数结束后依然有效如全局变量、静态变量、成员变量或调用方传入的引用若返回的是函数内的局部对象则会导致野引用危险。简言之引用返回高效但需保证对象生命周期局部对象不可用引用返回。7.2 编译器自动生成的默认行为如果没写编译器会自动生成一个。默认行为与拷贝构造类似对内置类型成员完成浅拷贝逐字节复制。对自定义类型成员调用该成员的赋值运算符重载。7.3 什么时候需要自己实现关键判断技巧像Date类只有内置类型不指向资源默认生成的完全够用不需要自己写。像Stack类有动态申请的_a指针默认浅拷贝会导致两个对象指向同一块内存析构时崩溃。必须自己实现深拷贝。像MyQueue类成员全是Stack类型因为编译器会调用Stack的赋值重载所以MyQueue不需要自己写。技巧如果一个类显式写了析构函数来释放资源那么这个类大概率也需要显式写赋值运算符重载和拷贝构造。7.4赋值 vs 拷贝构造比较维度拷贝构造赋值重载发生时机对象还未创建用已有对象初始化新对象。对象已经存在用另一个对象覆盖它。代码示例Date d2 d1;d2 还没创建d1 d3;d1 已经创建好了是否生成新对象是创建新对象否修改已有对象Date d1(2026, 1, 1); Date d2(2026, 1, 2); //赋值运算符重载 d2 d1; //拷贝构造 Date d3(d2); Date d4 d2;代码示例日期类Date.h#pragma once #includeiostream using namespace std; class Date { public: //友元函数声明加在public或者publicprivate之外的位置(即private)也可以加在哪儿都可以 friend ostream operator(ostream out, const Date d);//支持连续的输出需要加引用返回ostream friend istream operator(istream in, Date d); int getMonthDay(int year, int month)//获取当前月份天数 { static int MonthDayArray[13] { -1, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; if (month 2 ((year % 4 0 year % 100 ! 0) || year % 400 0))//闰年的2月有29天其他年份2月为28天 //后两个条件是一个整体必须加括号4年一润百年不润 或 四百年一润 { return 29; } return MonthDayArray[month]; } Date(int year 1900, int month 1, int day 1); void Print() const; //检查日期是否合法 bool CheckDate(); // d1 天数 Date operator(int day) const; Date operator(int day); //Date operator-(int day); //d1-天数 Date operator-(int day); Date operator-(int day) const; // Date operator();//d1 Date operator(int);//d1,加一个参数区分前置还是后置加加 //-- Date operator--();//--d1 Date operator--(int);//d1-- //比较大小 bool operator(const Date d) const; bool operator(const Date d) const; bool operator(const Date d) const; bool operator(const Date d) const; bool operator(const Date d) const; bool operator!(const Date d) const; int operator-(const Date d) const; //ostream operator(ostream out)//支持连续的输出需要加引用返回ostream //{ // out _year 年 _month 月 _day 日 endl; // //二元运算符左操作数传递给第一个对象右操作数传递给第二个对象 // //d1cout这样才对d1传给this指针cout传给out // //但这样可读性比较差只能被迫重载为全局函数 // return out; //} ////属于默认成员函数不显示式写会默认生成库里面有可以不写 //Date* operator()//与底下函数构成重载返回值与参数(隐含的this指针)都不同一个是普通对象Date*一个是const对象const Date* //{ // return this; // // return nullptr; //} //const Date* operator()const //{ // return this; // // return nullptr; //} private: int _year; int _month; int _day; }; //重复声明没事 ostream operator(ostream out, const Date d);//支持连续的输出需要加引用返回ostream istream operator(istream in, Date d);//d是形参Date d加引用形参d改变外面也改变提取的日期类值要放在对象d中d得可写不能加constDate.cpp#includeDate.h bool Date::CheckDate() { if(_month 1 || _month 12 || _day 1 || _day getMonthDay(_year, _month)) { return false; } else { return true; } } Date::Date(int year, int month, int day) { _year year; _month month; _day day; if (!CheckDate())//if(!(this-CheckDate())) { cout 非法日期 *this endl; } } void Date::Print() const { cout _year - _month - _day endl; } Date Date::operator(int day) const { Date tmp *this; tmp day;//直接用写好的重载 //Date tmp *this; //tmp._day day; //while (tmp._day getMonthDay(tmp._year, tmp._month)) //{ // tmp._day - getMonthDay(tmp._year, tmp._month); // tmp._month; // if (tmp._month 13) // { // tmp._month 1; // tmp._year; // } //} return tmp; } Date Date::operator(int day) { if (day 0)//d1 -100处理这种情况 { return *this - -day; } _day day; while (_day getMonthDay(_year, _month)) { _day - getMonthDay(_year, _month); _month; if (_month 13) { _month 1; _year; } } return *this; } //日期-天数 //2025.4.26 26-100-74 //2025.4.-74 -7431-43 //2025.3.-43 -4328-15 //20252.-15 -153116 //2025.1.16 Date Date::operator-(int day) { if (day 0) { return *this -day; } _day - day;//26-100-74 while (_day 0)//4.0那就不对了 { --_month; if (_month 0) { _month 12; --_year; } _day getMonthDay(_year, _month);//加上减去的那个月天数 } return *this; } Date Date::operator-(int day) const { Date tmp(*this); tmp - day; return tmp; } Date Date::operator()//d1出了作用域还在可以用引用返回 { *this 1; return *this; } Date Date::operator(int)//d1出了作用域tmp不在不能用引用返回 { Date tmp(*this); *this 1; return tmp; } Date Date::operator--()//--d1 { *this - 1; return *this; } Date Date::operator--(int)//d1-- { Date tmp(*this); *this - 1; return tmp; } bool Date::operator(const Date d) const { return _year d._year _month d._month _day d._day; } bool Date::operator(const Date d) const { if (_year d._year) { return true; } else if(_year d._year _month d._month) { return true; } else if (_year d._year _month d._month _day d._day) { return true; } return false; } //实现,实现其它,,,!都可以复用任何涉及到比较的类都可以用这套复用的逻辑 bool Date::operator(const Date d) const { return *this d || *this d; } bool Date::operator(const Date d) const { return !(*this d);//取反 } bool Date::operator(const Date d) const { return !(*this d); } bool Date::operator!(const Date d) const { return !(*this d); } int Date::operator-(const Date d) const//把小的那个日期一天一天往大的加加了多少次就差多少天 { 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;//d1d2,flag1,天数为正;d1d2,flag-1,天数为负 } //类外想访问类内私有成员变量用友元函数加在哪里都可以这里放在声明位置 ostream operator(ostream out, const Date d)//支持连续的输出需要加引用返回ostream { out d._year 年 d._month 月 d._day 日 endl; return out; } istream operator(istream in, Date d) { while(1) { cout 请依次输入年月日; in d._year d._month d._day; if (d.CheckDate()) { break; } else { cout 非法日期请重新输入 endl; } } return in; }Test.cpp#includeDate.h void TestDate1()//测试 { Date d1(2026, 8, 1); Date d2(2026, 8, 2); d1.Print(); Date d3(d1 10000); d1 10000; d3.Print(); d1.Print(); d1 -100; d1.Print(); } void TestDate2()//测试-- { Date d1(2026, 10, 22); Date d2 d1 - 10000; d1.Print(); d2.Print(); Date d3(2026, 10, 22); Date d4 d3 - 10000; d4.Print(); d1 - -100; d1.Print(); } void TestDate3()//测试-- { Date d1(2026, 10, 22); Date d2 d1; //Date d2 d1.operator(1);//显式调用一般不这样用 d1.Print(); d2.Print(); Date d3(2026, 10, 22); Date d4 d2; d3.Print(); d4.Print(); } void TestDate4() { Date d1(2026, 10, 22); Date d2(2026, 11, 22); cout (d1 d2) endl;//必须要加括号 cout (d1 d2) endl; cout (d1 d2) endl; } void TestDate5() { Date d1(2025, 4, 26); Date d2(2025, 7, 1); cout d1 - d2 endl;//d1 - d2不需要加括号-的优先级比较高 } void TestDate6() { Date d1(2025, 4, 26); Date d2(2025, 7, 1); //int i 1; //double d 1.1; //cout i d endl; //之所以可以自动识别输出变量类型是因为iostream库中把重载好了 //cout.operator(i);//等价于调用这个函数 //流插入运算符 cout d1;//operator operator (cout, d1); cin d1 d2; cout d1 d2; cout d1 - d2 endl; } void TestDate7() { Date d1(2025, 4, 26); d1.Print(); const Date d2(2025, 7, 1); //d2.Print();错误 } void TestDate8() { Date d1(2025, 4, 26); Date d2(2025, 7, 1); cout d1 endl; cout d2 endl; } int main() { //TestDate1(); //TestDate2(); //TestDate3(); //TestDate4(); //TestDate5(); //TestDate7(); TestDate8(); return 0; }8.流插入运算符int i 1; double d 1.1; cout i d endl; //之所以可以自动识别输出变量类型是因为iostream库中把重载好了 //cout.operator(i);//等价于调用这个函数9.const成员函数将const修饰的成员函数称之为const成员函数const修饰成员函数放到成员函数参数列表的后 ⾯。const实际修饰该成员函数隐含的this指针表明在该成员函数中不能对类的任何成员进⾏修改。 const 修饰Date类的Print成员函数Print隐含的this指针由 Date* const thisconst Date* const this 那么这两个this有什么区别呢写法名字能不能改变指针指向能不能改变指向的内容比喻Date* const this常量指针不能能默认this指针都是这样因为如果this的指向可以更改this nullptr; // 灾难this-_year 2026; // 程序直接崩溃 (空指针解引用)const Date* p指针常量 (指向常量的指针)能不能p指向的内容只能读不能写const Date* const p两者皆是不能不能10.取地址运算符重载取地址运算符重载分为普通取地址运算符重载和const取地址运算符重载⼀般这两个函数编译器⾃动 ⽣成的就可以够我们⽤了不需要去显⽰实现。除⾮⼀些很特殊的场景。//属于默认成员函数不显示式写会默认生成库里面有可以不写 Date* operator()//与底下函数构成重载返回值与参数(隐含的this指针)都不同一个是普通对象Date*一个是const对象const Date* { return this; // return nullptr; } const Date* operator()const { return this; // return nullptr; }void TestDate8() { Date d1(2025, 4, 26); Date d2(2025, 7, 1); cout d1 endl; cout d2 endl; }
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑