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

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

1. 类的默认成员函数

默认成员函数就是用户没有显示实现,编译器会自动生成的成员函数称为默认成员函数。一个类,我们不写的情况下编译器会默认生成以下6个默认成员函数,需要注意的是这6个中最重要的是前4个,最后两个取地址重载不重要,稍微了解即可。其次就是C++11以后还会增加两个默认成员函数,移动构造和移动赋值,这个以后再去学习。默认成员函数很重要,但是比较复杂,所以从下面两个方面去进行学习:

  1. 我们不写时,编译器默认生成的函数行为是什么,是否满足我们的需求?
  2. 编译器默认生成的函数不满足我们的需求,我们需要自己实现,那么应该如何去实现?

2. 构造函数

构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开辟空间创建对象(常使用的局部对象是栈帧创建时,空间就开好了),而是对象实例化时初始化对象。构造函数的本质是代替以前Stack和Date类中写的Init函数的功能,构造函数自动调用的功能就完美代替了Init。

构造函数的特点:

  1. 构造函数的函数名与创建的类名相同。
  2. 构造函数没有返回值。(返回值类型不用写void,不用纠结这一点,这是规定)
  3. 对象实例化时系统会自动调用对应的构造函数。
  4. 构造函数可以进行函数重载。
  5. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显示定义编译器将不再生成构造函数。
  6. 无参构造函数,全缺省构造函数,我们不写构造时编译器默认生成的构造函数,这三者都叫做默认构造函数但是这三个函数有且只有一个存在,不能同时存在。无参构造函数和全缺省构造函数虽然构成函数重载,但是调用会存在歧义。要注意很多人会认为默认构造函数只是编译器默认生成的那个叫做默认构造函数,实际上无参构造函数以及全缺省构造函数也属于默认构造函数,总结一下就是不需要传参数就可以调用的构造叫做默认构造函数。
  7. 我们不写构造编译器自动生成的构造对于内置类型成员变量的初始化没有要求,也就是说是否对内置类型成员变量进行初始化是不确定的,这主要取决于编译器的行为。对于自定义类型的成员变量,要求调用这个成员变量的默认构造函数来进行初始化。如果这个成员变量没有默认构造函数,那么系统就会产生报错,我们要初始化这个成员变量,需要使用初始化列表才能解决,初始化列表在下个章节进行学习。

说明:C++把类型分为内置类型(基本类型)和自定义类型。内置类型就是计算机语言提供的原生数据类型,如:int/char/double/指针等,自定义类型就是我们使用class/struct等关键字自己定义的类型。

下面通过代码的形式来感受以下构造函数的特点:

#include<iostream> 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; } //因为无参构造函数和全缺省构造函数不能同时存在,为了确保代码的可运行性,采用分开展示 #include<iostream> 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; }

在上面所展示的代码中,需要注意的是:当调用无参构造函数时,对象后面并不需要加“()”,不然会产生歧义:

这是正确的书写法,也就是在对象实例化的同时调用构造函数来对成员变量进行初始化。

这是错误的写法,因为这样写就会产生歧义,会被误以为是函数的声明。

学习了构造函数的特点之后,现在可以来回答一下上一节中提到的问题:我们不写时,编译器默认生成的函数行为是什么,是否满足我们的需求?

答案是:在大多数情况下,编译器默认生成的构造函数并不能满足我们的需求,但是在一些情况下,编译器默认生成的构造函数行为是可以满足需求的,例子如下:

#include<iostream> 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. 构函数名是在类名前加上字符~(按位取反操作符,代表析构函数与构造函数功能相反)。

#include<iostream> 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. 无参数无返回值。(这里和构造函数一样,也不需要加void)

3. 一个类只能有一个析构函数。如果没有显式定义,系统会自动生成默认的析构函数。

4. 对象的生命周期结束时,系统会自动调用析构函数。(一般执行到 return 0 后自动调用析构函数)

5. 析构函数和构造函数类似,编译器自动生成的析构函数对内置类型成员变量不做处理,自定义类型成员变量会调用他的析构函数。(下面的演示代码中,对象mq中的自定义类型成员变量调用Stack中的析构函数)

#include<iostream> 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中的析构函数,也就是说自定义类型的析构不用手动管理,但是内置类型成员需要手动管理

#include<iostream> 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函数了,也方便了不少。

#include<iostream> 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 运算符重载

  1. 当运算符被用于类类型的对象时,C++语言允许我们通过运算符重载的形式指定新的含义。C++规定类类型对象使用运算符时,必须转换成调⽤对应运算符重载,若没有对应的运算符重载,则会编译报错。
  2. 运算符重载是具有特殊名字的函数,他的名字是由operator和后面要定义的运算符共同构成。和其他函数⼀样,它也具有其返回类型和参数列表以及函数体。
    bool operator==(Date d1, Date d2) { return d1._year == d2._year && d1._month == d2._month && d1._day == d2._day; }
  3. 重载运算符函数的参数个数和该运算符作用的运算对象数量一样多。⼀元运算符有⼀个参数,⼆元运算符有两个参数,二元运算符的左侧运算对象传给第一个参数,右侧运算对象传给第二个参数。
    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(); }
  4. 如果⼀个重载运算符函数是成员函数,则它的第⼀个运算对象默认传给隐式的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; }
  5. 运算符重载以后,其优先级和结合性与对应的内置类型运算符保持⼀致。
  6. 不能通过连接语法中没有的符号来创建新的操作符:比如operator@。
  7. .* :: sizeof ?: .注意以上5个运算符不能重载。
  8. 重载操作符⾄少有⼀个类类型参数,不能通过运算符重载改变内置类型对象的含义,如: int operator+(int x, int y)
  9. ⼀个类需要重载哪些运算符,是看哪些运算符重载后有意义,比如Date类重载operator-就有意 义,但是重载operator+就没有意义。
  10. 重载++运算符时,有前置++和后置++,运算符重载函数名都是operator++,无法很好的区分。 C++规定,后置++重载时,增加⼀个int形参,跟前置++构成函数重载,方便区分。
  11. 重载>时,需要重载为全局函数,因为重载为成员函数,this指针默认抢占了第⼀个形参位置,第⼀个形参位置是左侧运算对象,调用时就变成了对象
http://www.jsqmd.com/news/453965/

相关文章:

  • 《从零开始的java从入门到入土的学习生活——JavaWeb前端篇》Chapter16——JavaWeb前端篇学习记录——HTML、CSS、盒子模型、flex弹性布局、表单标签
  • 毕设程序javaweb的计算机课程在线学习平台 基于Java Web的计算机技术在线教学与实训平台 计算机专业网络教育及技能测评系统
  • TechWiz LCD 1D应用:高延迟膜(彩虹mura仿真)
  • 企业策略路由(PBR)实战:原理、场景与故障排查(多出口必看)
  • 跨境卖家如何建立供应商考核指标提升稳定性
  • 2026年 喷雾干燥机厂家推荐排行榜:高速离心、气流喷雾、锂电池专用等十大机型核心优势与选购指南 - 品牌企业推荐师(官方)
  • Dify 实战系列(4):实现新闻内容概要生成
  • GLM-4.5 vs GLM-4.7 vs GLM-5 全方位技术演进对比
  • 如何选择优质品牌设计公司
  • 选购费氏粒度仪的关键指标:不仅仅是看测量范围 - 品牌推荐大师1
  • 数据同步备份软件:数字化时代的“双保险”策略
  • 西门子S7-1200PLC双轴定位算法在电池焊接控制中的应用:博图程序案例与威纶触摸屏操作界面
  • 觉察 改变
  • 全栈开发核心技术解析
  • 互联网大厂Java求职面试实战:三轮技术问答与热点技术深度解析
  • 并网逆变器VSG虚拟同步控制Matlab/Simulink仿真模型及其完全正确结果
  • 2026年阿里云企业邮箱代理商哪家好?真实案例解析靠谱伙伴 - 品牌2026
  • 2026年 拉力带厂家推荐排行榜:弹性拉力带/11件套拉力带/练背拉伸带,专业健身辅具助力科学塑形 - 品牌企业推荐师(官方)
  • 京东e卡怎么换成现金,亲测快捷的三种方式 - 猎卡回收公众号
  • 咱们直接动手搭个T型逆变器模型试试。先整明白核心结构:三相桥臂中间各接两个双向开关,形成T字拓扑。这种结构优势在于能输出五电平电压,谐波特性比传统三电平好不少
  • 国产化、安可、信创、自主可控说的是什么?一文读懂
  • 2026年知名的娃娃机_文审机_弹珠机源头厂家推荐-陕西英杰儿童主题乐园有限公司 - 朴素的承诺
  • 2026年 毛呢面料厂家推荐排行榜:羊毛/羊绒/驼绒/阿尔巴卡/功能性面料,精选实力源头工厂与创新工艺解析 - 品牌企业推荐师(官方)
  • 深度解析NX PowerLite智能压缩技术原理
  • 做 Agent,不一定要先改 workflow,也可以先把模型成本降下来
  • 织梦程序访问首页或其他页面出现空白问题是什么原因?织梦dedecms
  • 2026天然石口碑厂家推荐:选材更放心,文化石/地铺石/蘑菇石/贴墙石/石材/碎拼石/冰裂纹/脚踏石,天然石厂家推荐榜单 - 品牌推荐师
  • Epson M-G366PDG惯性测量单元:精准导航与稳定控制的理想选择
  • 知识点总结2
  • 2026广东 EUDR 认证 + 亚马逊气候友好认证双优:靠谱环评公司 TOP5 榜单 - 深度智识库