【C++初阶】:(3)C++基础类和对象(中)

📅 发布时间:2026/7/6 0:25:43 👁️ 浏览次数:
【C++初阶】:(3)C++基础类和对象(中)
1. 类的默认成员函数默认成员函数就是用户没有显示实现编译器会自动生成的成员函数称为默认成员函数。一个类我们不写的情况下编译器会默认生成以下6个默认成员函数需要注意的是这6个中最重要的是前4个最后两个取地址重载不重要稍微了解即可。其次就是C11以后还会增加两个默认成员函数移动构造和移动赋值这个以后再去学习。默认成员函数很重要但是比较复杂所以从下面两个方面去进行学习我们不写时编译器默认生成的函数行为是什么是否满足我们的需求编译器默认生成的函数不满足我们的需求我们需要自己实现那么应该如何去实现2. 构造函数构造函数是特殊的成员函数需要注意的是构造函数虽然名称叫构造但是构造函数的主要任务并不是开辟空间创建对象常使用的局部对象是栈帧创建时空间就开好了而是对象实例化时初始化对象。构造函数的本质是代替以前Stack和Date类中写的Init函数的功能构造函数自动调用的功能就完美代替了Init。构造函数的特点构造函数的函数名与创建的类名相同。构造函数没有返回值。返回值类型不用写void不用纠结这一点这是规定对象实例化时系统会自动调用对应的构造函数。构造函数可以进行函数重载。如果类中没有显式定义构造函数则C编译器会自动生成一个无参的默认构造函数一旦用户显示定义编译器将不再生成构造函数。无参构造函数全缺省构造函数我们不写构造时编译器默认生成的构造函数这三者都叫做默认构造函数。但是这三个函数有且只有一个存在不能同时存在。无参构造函数和全缺省构造函数虽然构成函数重载但是调用会存在歧义。要注意很多人会认为默认构造函数只是编译器默认生成的那个叫做默认构造函数实际上无参构造函数以及全缺省构造函数也属于默认构造函数总结一下就是不需要传参数就可以调用的构造叫做默认构造函数。我们不写构造编译器自动生成的构造对于内置类型成员变量的初始化没有要求也就是说是否对内置类型成员变量进行初始化是不确定的这主要取决于编译器的行为。对于自定义类型的成员变量要求调用这个成员变量的默认构造函数来进行初始化。如果这个成员变量没有默认构造函数那么系统就会产生报错我们要初始化这个成员变量需要使用初始化列表才能解决初始化列表在下个章节进行学习。说明C把类型分为内置类型(基本类型)和自定义类型。内置类型就是计算机语言提供的原生数据类型如int/char/double/指针等自定义类型就是我们使用class/struct等关键字自己定义的类型。下面通过代码的形式来感受以下构造函数的特点#includeiostream using namespace std; 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() { Date d1; d1.Print(); Date d2(2024, 7, 10); d2.Print(); //Date d3(2024); //d3.Print(); //Date func(); //func.Print(); return 0; } //因为无参构造函数和全缺省构造函数不能同时存在为了确保代码的可运行性采用分开展示 #includeiostream using namespace std; 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() { //Date d1; //d1.Print(); //Date d2(2024, 7, 10); //d2.Print(); Date d3(2024); d3.Print(); //Date func(); //func.Print(); return 0; }在上面所展示的代码中需要注意的是当调用无参构造函数时对象后面并不需要加“()”不然会产生歧义这是正确的书写法也就是在对象实例化的同时调用构造函数来对成员变量进行初始化。这是错误的写法因为这样写就会产生歧义会被误以为是函数的声明。学习了构造函数的特点之后现在可以来回答一下上一节中提到的问题我们不写时编译器默认生成的函数行为是什么是否满足我们的需求答案是在大多数情况下编译器默认生成的构造函数并不能满足我们的需求但是在一些情况下编译器默认生成的构造函数行为是可以满足需求的例子如下#includeiostream using namespace std; 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() { free(_a); _a nullptr; _top _capacity 0; } private: STDataType* _a; size_t _capacity; size_t _top; }; //两个Stack实现队列 class MyQueue { public: /* 编译器默认生成MyQueue的构造函数调用了Stack的构造完成了两个成员的初始化 编译器默认生成MyQueue的析构函数调用了Stack的析构释放的Stack内部的资源 显示写析构也会自动调用Stack的析构*/ ~MyQueue() { cout ~MyQueue() endl; } private: Stack pushst; Stack popst; int size; }; int main() { MyQueue mq; //Stack st1; //Stack st2; return 0; }在上面的演示代码中定义了对象mq而在C中只要定义了对象就会调用默认构造函数调用不到就会产生报错而定义的对象mq会调用class MyQueue中自动生成的默认构造由于class MyQueue中的成员变量属于自定义类型Stack所以class MyQueue中自动生成的默认构造又会去调用自定义类型Stack的默认构造函数即class Stack中的全缺省默认构造函数经过调试得到的结果如下通过调试结果可以看到通过class Stack中的全缺省默认构造函数对象mq中的成员变量pushst和popst得到了初始化红色方框中显示同时可以注意到绿色方框中的成员变量size属于内置类型它现在所展示的初始化数值为0但是这并不意味着编译器会对内置类型的成员变量进行初始化因为编译器对内置类型的函数行为是随机的这取决于编译器所以这个0只能说是巧合并不是编译器自动检测到我们想赋予size为0这是一个坑所以对于内置类型的成员变量最好是手动进行初始化。与此同时上面提到的如果定于的对象调用不到默认构造函数就会产生报错下面给个例子进行演示我将上面的演示代码中n的缺省值进行了删除这就导致此函数不再是全缺省构造函数因此在定义对象时就无法调用默认构造函数便会产生以下报错通过上面的一些例子我们便可以对第一节提到的第一个问题进行总结大多数情况下构造函数都需要我们自己去实现少数情况类似MyQueue且Stack有默认构造时MyQueue自动生成就可以用。3. 析构函数析构函数与构造函数的功能相反析构函数不是完成对对象本身的销毁比如局部对象是存在栈帧的函数结束栈帧销毁空间就释放了不需要我们管C规定对象在销毁时会自动调用析构函数完成对象中资源的清理释放工作。析构函数的功能类比之前Stack实现的Destroy功能而像Date没有Destroy其实就是没有资源需要释放所以严格说Date是不需要析构函数的。析构函数的特点1. 构函数名是在类名前加上字符~按位取反操作符代表析构函数与构造函数功能相反。#includeiostream using namespace std; 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() { free(_a); _a nullptr; _top _capacity 0; } private: STDataType* _a; size_t _capacity; size_t _top; };2. 无参数无返回值。这里和构造函数一样也不需要加void3. 一个类只能有一个析构函数。如果没有显式定义系统会自动生成默认的析构函数。4. 对象的生命周期结束时系统会自动调用析构函数。一般执行到 return 0 后自动调用析构函数5. 析构函数和构造函数类似编译器自动生成的析构函数对内置类型成员变量不做处理自定义类型成员变量会调用他的析构函数。下面的演示代码中对象mq中的自定义类型成员变量调用Stack中的析构函数#includeiostream using namespace std; 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() { free(_a); _a nullptr; _top _capacity 0; } private: STDataType* _a; size_t _capacity; size_t _top; }; // 两个Stack实现队列 class MyQueue { public: // 编译器默认生成MyQueue的构造函数调用了Stack的构造完成了两个成员的初始化 // 编译器默认生成MyQueue的析构函数调用了Stack的析构释放的Stack内部的资源 private: Stack pushst; Stack popst; }; int main() { MyQueue mq; return 0; }6. 当显式写析构函数时自定义类型成员也会调用他的析构也就是说自定义类型成员无论什么情况都会自动调用析构函数。下面演示代码中就算在MyQueue中显式写了析构函数也还是会对自定义类型成员调用Stack中的析构函数也就是说自定义类型的析构不用手动管理但是内置类型成员需要手动管理#includeiostream using namespace std; 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() { free(_a); _a nullptr; _top _capacity 0; } private: STDataType* _a; size_t _capacity; size_t _top; }; // 两个Stack实现队列 class MyQueue { public: // 编译器默认生成MyQueue的构造函数调用了Stack的构造完成了两个成员的初始化 // 编译器默认生成MyQueue的析构函数调用了Stack的析构释放的Stack内部的资源 // 显式写析构也会自动调用Stack的析构 ~MyQueue() { cout ~MyQueue() endl; } private: Stack pushst; Stack popst; //int size; }; int main() { MyQueue mq; return 0; }7. 如果类中没有申请资源时析构函数可以不写直接使用编译器默认生成的析构函数如Date如果默认生成的析构函数就可以使用就不需要显式写析构函数了如MyQueue但是有资源申请时一定要自己手写析构函数否则会造成资源泄漏如Stack。8. 一个局部域的多个对象C中规定后定义的对象先析构。下面对比一下⽤C和C实现的Stack解决之前括号匹配问题is Valid我们发现有了构造函数和析构函数确实方便了很多不会再忘记调⽤Init和Destory函数了也方便了不少。#includeiostream using namespace std; // 用最新加了构造和析构的C版本Stack实现 bool isValid(const char* s) { Stack st; while (*s) { if (*s [ || *s ( || *s {) { st.Push(*s); } else { // 右括号比左括号多数量匹配问题 if (st.Empty()) { return false; } // 栈里面取左括号 char top st.Top(); st.Pop(); // 顺序不匹配 if ((*s ] top ! [) || (*s } top ! {) || (*s ) top ! ()) { return false; } } s; } // 栈为空返回真说明数量都匹配 左括号多右括号少匹配问题 return st.Empty(); } // 用之前C版本Stack实现 bool isValid(const char* s) { ST st; STInit(st); while (*s) { // 左括号入栈 if (*s ( || *s [ || *s {) { STPush(st, *s); } else // 右括号取栈顶左括号尝试匹配 { if (STEmpty(st)) { STDestroy(st); return false; } char top STTop(st); // 不匹配 if ((top ( *s ! )) || (top { *s ! }) || (top [ *s ! ])) { STDestroy(st); return false; } } s; } // [[[[]] // 栈不为空说明左括号比右括号多数量不匹配 bool ret STEmpty(st); STDestroy(st); return ret; } int main() { cout isValid([()][]) endl; cout isValid([(])[]) endl; return 0; }4. 赋值运算符重载4.1 运算符重载当运算符被用于类类型的对象时C语言允许我们通过运算符重载的形式指定新的含义。C规定类类型对象使用运算符时必须转换成调⽤对应运算符重载若没有对应的运算符重载则会编译报错。运算符重载是具有特殊名字的函数他的名字是由operator和后面要定义的运算符共同构成。和其他函数⼀样它也具有其返回类型和参数列表以及函数体。bool operator(Date d1, Date d2) { return d1._year d2._year d1._month d2._month d1._day d2._day; }重载运算符函数的参数个数和该运算符作用的运算对象数量一样多。⼀元运算符有⼀个参数⼆元运算符有两个参数二元运算符的左侧运算对象传给第一个参数右侧运算对象传给第二个参数。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; } /*int GetYear() { return _year; }*/ private: int _year; int _month; int _day; //int _hour; }; bool operator(Date d1, Date d2) { return d1._year d2._year d1._month d2._month d1._day d2._day; } int main() { Date x1(2024, 7, 10); Date x2(2024, 7, 11); //operator(x1, x2); x1 x2; //这种写法和上面一样更简单一点左侧运算对象传给第一个参数右侧运算对象传给第二个参数。 return 0; }但是上面的演示代码中存在一些问题成员变量_year,_month,_day被访问限定符限定成了private因此运算符重载函数bool operator 在类外访问不到这三个变量目前的解决方法有两个1. 将三个成员变量设置为public(暴力解法不推荐)2. 在类中设置取值成员函数中等解法但不会破坏成员变量的安全性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; } int GetYear() { return _year; } int GetMonth() { return _month; } int GetDay() { return _day; } private: int _year; int _month; int _day; //int _hour; }; bool operator(Date d1, Date d2) { return d1.GetYear() d2.GetYear() d1.GetMonth() d2.GetMonth() d1.GetDay() d2.GetDay(); }如果⼀个重载运算符函数是成员函数则它的第⼀个运算对象默认传给隐式的this指针因此运算符重载作为成员函数时参数比运算对象少⼀个。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; } /*int GetYear() { return _year; }*/ bool operator(Date d2) { return _year d2._year _month d2._month _day d2._day; } private: int _year; int _month; int _day; //int _hour; }; int main() { Date x1(2024, 7, 10); Date x2(2024, 7, 11); x1.operator(x2);//这里只需要传一个参数因为bool operator 为成员函数另外一个参数由this指针传递 //x1 x2;代表的意思和上面代码相同 return 0; }运算符重载以后其优先级和结合性与对应的内置类型运算符保持⼀致。不能通过连接语法中没有的符号来创建新的操作符比如operator。.* :: sizeof ?: .注意以上5个运算符不能重载。重载操作符⾄少有⼀个类类型参数不能通过运算符重载改变内置类型对象的含义如 int operator(int x, int y)⼀个类需要重载哪些运算符是看哪些运算符重载后有意义比如Date类重载operator-就有意 义但是重载operator就没有意义。重载运算符时有前置和后置运算符重载函数名都是operator无法很好的区分。 C规定后置重载时增加⼀个int形参跟前置构成函数重载方便区分。重载时需要重载为全局函数因为重载为成员函数this指针默认抢占了第⼀个形参位置第⼀个形参位置是左侧运算对象调用时就变成了对象