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

【类与对象(中)】C++类默认成员函数全解析

目录

类的默认成员函数

构造函数(对象的初始化):

析构函数(清理数据):

拷贝构造函数:

赋值运算符重载:

日期类Date:


类的默认成员函数

默认函数:

我们不写,编译器默认生成的函数。

无参构造函数

全缺省构造函数

初始与清理:构造函数与析构函数。

构造函数(对象的初始化):

  1. 函数名与类名相同

  2. 无返回值

  3. 对象实例化时系统会自动调用对应的构造函数

  4. 构造函数可以重载

  5. 如果没有显示的写构造函数,编译器会默认生成一个无参的构造

  6. 默认构造:无参构造,全缺省构造和编译器默认生成的构造

析构函数(清理数据):

  1. 析构函数在类名前加“~”

  2. 无返回值,无参数

  3. 生命周期结束时自动调用析构

  4. 一个类只有一个析构函数,若未显示析构,系统自动生成默认的析构函数

  5. 编译器自动生成的默认析构函数对内置类型不做处理,自定义类型成员会调用他的析构函数。

  6. 显示写的析构函数,自定义类型成员也会调用,就是说自定义类型成员无论什么情况都会调用析构函数

  7. 如果类中没有申请资源时,析构函数可以不写;但是有资源申请时,⼀定要自己写析构,否则会造成资源泄漏,如Stack。

  8. ⼀个局部域的多个对象,C++规定后定义的先析构。

一般情况下,显示申请了资源才需要自己写析构,其他情况基本都不需要写。

关于5,6点的解释:

class Date
{
public:Date(int year=1,int date=1,int day=1){_year = year;_date = date;_day = day;}~Date(){cout << "~Date()" << endl;}
private:int _year;int _date;int _day;
};typedef int STDataType;
class Stack
{
public:Stack(int capacity = 4){_Data = (STDataType*)malloc(sizeof(STDataType)*capacity);if (_Data==nullptr){perror("申请空间失败");return;}_capacity = capacity;_top = 0;}~Stack(){cout << "~Stack()" << endl;if (_Data){free(_Data);}_capacity = 0;_top = 0;}private:STDataType* _Data;int _capacity;int _top;
};class Myqueue
{
public:/*~Myqueue(){cout << "~Myqueue()" << endl;}*/
private:Stack _push;Stack _pop;
};
int main()
{Date d1;Stack s1;Myqueue q1;return 0;
}
//Myqueue类的析构函数被注释掉,没有显示的析构函数,所以编译器自动生成一个默认的析构函数,因为Myqueue成员变量是自定义类型Stack,所以会调用~Stack(),因为有两个Stack类型的成员变量,所以调用两次;
//取消Myqueue的注释,现在有显示析构函数,那么显示析构函数会被调用,自定义类型变量的析构函数也会被调用
//在main函数中,有三个对象,那么后定义的先析构,先析构Myqueue q1,在析构Stack s1,最后Date d1

拷贝构造函数:

  1. 是构造函数的重载

  2. 拷贝构造函数的第一个参数必须是类类型对象的引用(Date& d ),传值方式编译器直接报错,因为语法逻辑上会引发无穷递归调用,拷贝构造函数也可以多个参数,但是第一个参数必须是类类型对象的引用,后面的参数必须有缺省值

  3. C++规定自定义类型对象进行拷贝行为必须调用拷贝构造,所以这里自定义类型传值传参和传值返 回都会调用拷贝构造完成。

  4. 若未显式定义拷贝构造,编译器会生成自动生成拷贝构造函数。

  5. 拷贝构造中引用换成指针也可以,功能上看起来像拷贝构造,但是这样就不是拷贝构造了,就是一个普通构造函数。

  6. 自动生成的拷贝构造对内置类型成员变量会完成值拷贝/浅拷贝(一个一个字节的拷贝,对自定义类型成员变量会调用他的拷贝构造。浅拷贝是系统的吗,默认行为。对于栈类类型的对象,不能进行浅拷贝,这样会造成对同一块空间析构两次的问题,所以对于栈类类型的对象,要进行深拷贝(开一样大的空间,里面的值是一样的)

第6点的解释:

typedef int STDataType;
class Stack
{
public:Stack(int capacity = 4)//构造{_Data = (STDataType*)malloc(sizeof(STDataType) * capacity);if (_Data == nullptr){perror("申请空间失败");return;}_capacity = capacity;_top = 0;}Stack(Stack& st)//拷贝构造,深拷贝构造,因为这里存在资源的申请{//如果没有这个深拷贝,用系统自动生成的默认拷贝构造,会报错,因为代码中存在资源的申请//在代码结束时,Stack对象会调用析构函数,但是st1与st2的成员变量_a指向的是同一块空间,所以在析构时会造成对同一块空间进行两次释放的问题。_Data = (STDataType*)malloc(sizeof(STDataType)*st._capacity);if (_Data==nullptr){perror("申请空间失败");return;}memcpy(_Data,st._Data,sizeof(STDataType)*st._top);_capacity = st._capacity;}~Stack(){cout << "~Stack()" << endl;if (_Data){free(_Data);}_capacity = 0;_top = 0;}private:STDataType* _Data;int _capacity;int _top;
};
int main()
{Stack st1;Stack st2(st1);return 0;
}
  • 类成员变量全是内置类型且没有指向什么资源,编译器自动生成的拷贝构造就可以完成需要的拷贝,所以不需要我们显示实现拷贝构造。像Stack这样的类,虽然也都是内置类型,但是_a指向了资源,编译器自动生成的拷贝构造完成的值拷贝/浅拷贝不符合我们的需求,所以需要 我们自己实现深拷贝(对指向的资源也进行拷贝)。这里还有一个小技巧,如果一个类显示实现了析构并释放资源,那么他就需要显示写拷贝构造,否则就不需要。

  • 传值返回会产生一个临时对象调用拷贝构造,传引用返回,返回的是返回对象的别名(引用),没有产生拷贝。但是如果返回对象是⼀个当前函数局部域的局部对象,函数结束就销毁了,那么使用引用返回是有问题的,这时的引用相当于⼀个野引用,类似一个野指针⼀样。传引用返回可以减少拷贝,但是⼀定要确保返回对象,在当前函数结束后还在,才能用引用返回。

typedef int STDataType;
class Stack
{
public:Stack(int capacity = 4)//构造{_Data = (STDataType*)malloc(sizeof(STDataType) * capacity);if (_Data == nullptr){perror("申请空间失败");return;}_capacity = capacity;_top = 0;}Stack(Stack& st)//拷贝构造{_Data = (STDataType*)malloc(sizeof(STDataType)*st._capacity);if (_Data==nullptr){perror("申请空间失败");return;}memcpy(_Data,st._Data,sizeof(STDataType)*st._top);_capacity = st._capacity;}~Stack(){cout << "~Stack()" << endl;if (_Data){free(_Data);}_capacity = 0;_top = 0;}void push(int x){_Data[_top++] = x;}
private:STDataType* _Data;int _capacity;int _top;
};Stack& func()
{Stack st;st.push(1);st.push(2);st.push(3);//.....return st;//这里返回了局部域的对象,函数结束时,st就销毁了,
}
//返回st,把st的值拷贝到一块临时空间中去,此时这块临时空间就是临时对象,这一过程要调用拷贝构造,再把临时对象的值拷贝给ret,这一过程也要调用拷贝构造,但是当func函数结束时,st对象中指向的资源就已经被释放,所以ret对象存在野引用,当main函数结束时调用析构函数,对ret对象进行析构就存在对已经释放的空间进行析构的问题。 
int main()
{Stack ret=Func();return 0;
}

赋值运算符重载:

运算符重载:(运算符重载是由operator加要重载的符号构成 )

运算符重载是由operator加要重载的运算符构成

  • operator+()、operator-()....

运算符的参数有个数与重载的运算符操作对象保持一致

  • +运算符,两个操作对象,operator+(Date& const x)(因为运算符重载是声明在类中的,所以有一个隐式的参数this。所以运算符重载参数个数显示来看会比实际少一个)

  • ++运算符,一个操作对象,operator++(),因为存在this指针,所以不用写参数。关于自增自减操作符在前在后重载的问题,在前,就如前面的例子直接写就行,在后则需要再写一个参数,这个参数只是为了告诉编译器++--是在操作对象后面的,比如:operator++(int)→这个参数可以只写一个参数类型。

  1. 运算符重载后,不会改变它的优先级与结合性,它与内置类型运算符保持一致。

  2. 语法中没有运算符不能通过运算符重载来创建,比如:operator@()。

  3. 有五个运算符不能重载:“.*、::、sizeof、.、?:

  • 关于".*"符号,它是主要用于调用成员函数的指针  

class A
{
public:void func(){cout << "yes" << endl;}
};typedef void(A::*PF)();//A::*表示这是一个指向类A的成员的函数指针,是一个指向类A的成员函数的指针的类型,所以下文定义的pf是一个成员函数指针。int main()
{PF pf = &A::func;//c++规定,调用函数的指针时需要取地址符号,A::可以理解为取的是A类中的成员函数的地址,但是取的不是该成员函数的实际内存地址,因为成员函数不是像普通函数那样独立存储的,它取的是成员函数的“函数表示符”的地址,这个地址在类的上下文中是唯一的。A obj;(obj.*pf)();//对pf解引用得到成员函数的地址,但是仅仅解引用pf是不足以调用成员函数的,因为成员函数需要知道它是在哪个对象上被调用的,此时.就起到了作用,这个操作符将*pf与obj绑定在了一起,告诉编译器是被obj这个对象调用的。return 0;
}
  1. 当运算符被用于类类型的对象时,c++语言允许我们通过运算符重载的形式指定新的含义。c++规定类类型对象使用运算符时,要转换成他对应的运算符重载,若没有对应的运算符重载,编译器将会报错

  2. 运算符重载是具有特殊名字的函数,他的名字是由operator和后⾯要定义的运算符共同构成。和其他函数⼀样,它也具有其返回类型和参数列表以及函数体。

  3. 重载运算符函数的参数个数和该运算符作用的运算对象数量一样多。一元运算符有一个参数,二元运算符有两个参数,二元运算符的左侧运算对象传给第一个参数,右侧运算对象传给第二个参数。

  4. 如果一个重载运算符函数成员函数,则它的第一个运算对象默认传给隐式的this指针,因此运算 符重载作为成员函数时,参数比运算对象少一个。

  5. 运算符重载以后,其优先级和结合性与对应的内置类型运算符保持⼀致

  6. 不能通过连接语法中没有的符号来创建新的操作符:比如operator@。

  7. .* 、:: 、sizeof 、?:、 . 注意以上5个运算符不能重载。

关于".*"运算符:

class A
{
public:void func(){cout << "A::func()" << endl;}
};typedef void(A::*Pf)();//定义一个函数指针,指向的函数返回类型为void,是在A类中的并且参数个数为0,所以函数指针类型为void(A::*)();int main()
{Pf pf;pf = &A::func;A obj; (obj.*pf)();return 0;
}

重载操作符至少有一个类类型参数,不能通过运算符重载改变内置类型对象的含义,如:int operator+(int x,int y)

关于调用运算符重载函数的形式:

Date operator+(int day);Date Date::operator+(int 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._year;tmp._month = 1;}}return tmp;
}
int main()
{Date d1;d1+1;//可以这么写d1.operator(1);//这样写也对//写成第一种形式编译器会自动转换成第二种形式。//从底层的角度来讲,编译器调用函数会转换成一串指令call,这两种形式的底层汇编代码是一样的return 0;
}

日期类Date:

取地址运算符重载

const成员函数:

  1. 将const修饰的成员函数称之为const成员函数,const修饰成员函数放到成员函数参数列表的后面。

  2. const实际修饰成员函数隐含的this指针,表面在该成员函数中不能对类的任何成员进行修改。

#include<iostream>
using namespace std;class Date
{
public:Date(int year=2000,int month=1,int day=1){_year = year;_month = month;_day = day;}void Print()const//const修饰Date类的Print成员函数,Print的this指针变成://(const Date*const this){//(*this)._day = 30;cout << _year << "-" << _month << "-" << _day << endl;}private:int _year;int _month;int _day;
};int main()
{Date d1(2024,1,1);d1.Print();const Date d2(2024,10,10);d2.Print();return 0;
}

取地址运算符重载:

取地址运算符重载分为普通取地址运算符重载和const取地址运算符重载,一般这两个函数编译器自动生成的就可以够我们用了,不需要去显示实现。

除非一些很特殊的场景,比如我们不想让别人取到当我们定义的类前类对象的地址,就可以自己实现一份,胡乱返回一个地址。

class Date
{
public :Date* operator&(){return this;// return nullptr;}const Date* operator&()const{return this;// return nullptr;}
private :int _year ; // 年int _month ; // ⽉int _day ; // ⽇};

http://www.lryc.cn/news/614195.html

相关文章:

  • 使用 Grunt 替换 XML 文件中的属性值
  • 50系显卡ubuntu20.04安装显卡驱动,解决gazebo不调用显卡的问题
  • Java文件读写(IO、NIO)
  • HttpURLConnection (JDK原生)和Hutool HTTP工具的区别
  • 浅析线程池工具类Executors
  • ASTM D4169-23版本有哪些实施指南
  • 2025年最新Java后端场景题+八股文合集(100w字面试题总结)
  • [激光原理与应用-176]:测量仪器 - 频谱型 - AI分类与检测相对于传统算法的优缺点分析
  • 零知开源——基于STM32F103RBT6的TDS水质监测仪数据校准和ST7789显示实战教程
  • 【优选算法】BFS解决拓扑排序
  • Rust语言序列化和反序列化vec<u8>,serde库Serialize, Deserialize,bincode库(2025年最新解决方案详细使用)
  • 全面了解svm
  • 海量数据处理问题详解
  • MySQL 正则表达式详细说明
  • [ MySQL 数据库 ] 环境安装配置和使用
  • 零基础深度学习规划路线:从数学公式到AI大模型的系统进阶指南
  • IPC总结
  • 【接口自动化测试】
  • FastAPI的BackgroundTasks如何玩转生产者-消费者模式?
  • 关于 Rust 异步底层实现中 waker 的猜测
  • #C语言——刷题攻略:牛客编程入门训练(六):运算(三)-- 涉及 辗转相除法求最大公约数
  • GPT OSS 双模型上线,百度百舸全面支持快速部署
  • 创建MyBatis-Plus版的后端查询项目
  • SQL Server 2019搭建AlwaysOn高可用集群
  • 模块 PCB 技术在未来通信领域的创新突破方向
  • Cisco 2018-2023年度互联网报告深度解析:数字化转型时代的网络发展趋势与战略洞察
  • kafka 为什么需要分区?分区的引入带来了哪些好处
  • SpringMVC(四)
  • 前后端日期交互方案|前端要传时间戳还是字符串?后端接收时是用Long还是Date还是String?
  • 机器学习 SVM支持向量机