C++类和对象(二)

news/2024/11/27 23:44:53/

​​​​​​类和对象(一)


目录

一.   类的6个默认成员函数

二.   构造函数 

1.概念

 2.特性

三.   析构函数

1.概念

2.特性

四.   拷贝构造函数

1.概念

2.特征

五.   赋值运算符重载

1.运算符重载

2.赋值运算符重载

3.前置++和后置++重载


一.   类的6个默认成员函数

任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。

默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。

class Date {};

 

 



二.   构造函数 

1.概念

class Date
{
public:void Init(int year, int month, int day){_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.Init(2022, 1, 1);d1.Print();return 0;
}

例如上面的日期类,可以通过 Init 公有方法给对象设置日期,但如果每次创建对象时都调用该方法设置信息,未免有点麻烦,那能否在对象创建时,就将信息设置进去呢?

构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证
每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次。

例如我们就可以将上面的日期类替换为构造函数

class Date
{
public:Date(int year, int month, int day){_year = year;_month = month;_day = day;}void Print(){cout << _year << "-" << _month << "-" << _day << endl;}
private:int _year;int _month;int _day;
};
int main()
{Date d1(2022,1,1);d1.Print();return 0;
}

 


 2.特性

构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。

其特征如下:

1. 函数名与类名相同。

2. 无返回值。

3. 对象实例化时编译器自动调用对应的构造函数。

4. 构造函数可以重载。

//无参构造函数
Date
{ }//带参构造函数
Date(int year, int month, int day)
{_year = year;_month = month;_day = day;
}

当然,我们也可以将这两个构造函数合并为一个缺省构造函数

Date(int year=0, int month=1, int day=1)
{_year = year;_month = month;_day = day;
}

在调用有参构造函数时,就如下边所展示的那样

Date d1(2022,1,1);

而在调用无参构造函数时要注意一个问题,就是不要带括号

Date d2;//right
Date d3();//error

5.无参的构造函数和全缺省的构造函数都称为默认构造函数,默认构造函数只能有一个。

6. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。

class Date
{
public:void Print(){cout << _year << "-" << _month << "-" << _day << endl;}
private:int _year;int _month;int _day;
};
int main()
{Date d1;d1.Print();return 0;
}

我们可以看到,上述的类中我们调用的是自动生成的默认构造函数,但是类成员变量依旧是随机值,这是为什么呢?

这是因为,C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型,如:int/char...,自定义类型就是我们使用class/struct/union等自己定义的类型。

编译器生成默认构造函数,对于内置类型不做初始化处理,而对于自定类型成员变量则会调用它的默认构造函数。如果没有默认构造函数,会报错。

class Time
{
public:Time(int hour = 12, int minute = 0, int second = 0)//默认构造函数{_hour = hour;_minute = minute;_second = second;}void Print(){cout << _hour << ":" << _minute << ":" << _second << endl;}private:int _hour;int _minute;int _second;
};class Date
{
public:void Print(){_t.Print();}
private:int _year;int _month;int _day;Time _t;//自定义类型
};
int main()
{Date d1;d1.Print();return 0;
}



7.C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在类中声明时可以给默认值



三.   析构函数

1.概念

通过前面构造函数的学习,我们知道一个对象是怎么来的,那一个对象又是怎么没呢的?

析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。

class Date
{
public:Date(int year, int month, int day){_year = year;_month = month;_day = day;}
private:int _year;int _month;int _day;
};
int main()
{Date d1(2022,1,1);d1.Print();return 0;
}

依旧是这样一个代码,我们需要自己定义一个析构函数吗?

答案是不需要的,我们并没有动态开辟空间,类成员变量都有自己的生命周期,在成员被销毁时类成员变量的生命周期也会结束,不需要使用析构函数。

而若是我们将顺序表实现的栈写作一个类

class Stack
{int* _a;int top;int capacity;
};

先不管其中的类成员函数,我们知道,栈顺序表中的数组是动态开辟的,不会随着对象的销毁而销毁,这时我们便需要一个析构函数来完成这个顺序表的释放。



2.特性

析构函数是特殊的成员函数,其特征如下:

1. 析构函数名是在类名前加上字符 ~。

2. 无参数无返回值类型。

3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载。

4. 对象生命周期结束时,C++编译系统系统自动调用析构函数。

5. 与构造函数相同的是,对于内置类型不做处理,而对于自定类型成员变量则会调用它的默认析构函数。

class Time
{
public:~Time(){cout << "~time()" << endl;}private:int _hour;int _minute;int _second;
};class Date
{
private:int _year;int _month;int _day;Time _t;
};
int main()
{Date d1;return 0;
}



四.   拷贝构造函数

1.概念

表面意思,就是用作拷贝其他成员的构造函数。

只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。

依旧是我们熟悉的日期类

class Date
{
public:Date(int year=0, 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(2022,1,1);return 0;
}

若我们想要定义一个对象d2与d1相同时,我们就可以用到拷贝构造函数 

class Date
{
public:Date(int year=0, int month=1, int day=1){_year = year;_month = month;_day = day;}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(2022,1,1);Date d2(d1);d2.Print();return 0;
}

这样,我们就可以完成拷贝

我们在上面说到,函数使用的是传引用调用,这是由于,若是使用传值调用,首先需要产生一个拷贝的临时变量,产生时又需要调用拷贝构造函数,这样会发生死循环。

2.特征

拷贝构造函数也是特殊的成员函数,其特征如下:

1. 拷贝构造函数是构造函数的一个重载形式。

2. 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,这一点我们已经在上面提到了。

3.若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。

因此,在我们的日期类中,没有必要自己定义拷贝构造函数,直接进行浅拷贝即可。
 

class Date
{
public:Date(int year = 0, 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(2022,1,1);Date d2(d1);d2.Print();return 0;
}



那么我们再来看一种使用动态开辟空间情况

class Id
{
public:Id(){strcpy(_id, "111111111111111111");}void Print(){cout << _id << endl;}void change(char* id){strcpy(_id, id);}
private:char* _id=(char*)malloc(sizeof(char)*19);
};int main()
{Id person1;person1.Print();Id person2(person1);person2.Print();char id2[19] = "222222222222222222";person2.change(id2);person1.Print();person2.Print();return 0;
}

在这种情况下,我们先定义了一个对象person1,之后用person2拷贝person1,之后将person2._id改变。


可以看到,在person2改变后,person1也跟着改变了。

我们可以进行调试看一下


首先对象person1中为 _id 动态开辟了一块空间


之后在拷贝时,我们可以发现,由于浅拷贝是按字节序完成的拷贝,所以person2._id只是将person1._id的地址拷贝了过去。因此才会发生以上情况,当然,若是这样,在析构时也会发生错误。因此,面对动态开辟的空间,我们应该选择使用深拷贝。

class Id
{
public:Id(){strcpy(_id, "111111111111111111");}Id(Id& person){strcpy(_id, person._id);}void Print(){cout << _id << endl;}void change(char* id){strcpy(_id, id);}
private:char* _id=(char*)malloc(sizeof(char)*19);
};int main()
{Id person1;person1.Print();Id person2(person1);person2.Print();char id2[19] = "222222222222222222";person2.change(id2);person1.Print();person2.Print();return 0;
}



可以看到,在使用深拷贝后,就不会出现以上的问题了,因此,在使用类和对象时,我们要根据情况,严谨地选择深浅拷贝。 



五.   赋值运算符重载

1.运算符重载

C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。

函数名字为:关键字operator后面接需要重载的运算符符号。

函数原型:返回值类型 operator操作符(参数列表)。

注意:

不能通过连接其他符号来创建新的操作符:比如operator@

重载操作符必须有一个类类型参数

用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义

作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
.*   ::   sizeof   ?:   . 注意以上5个运算符不能重载。
 

依旧以我们的日期类为例

class Date
{
public:Date(int year = 0, int month = 1, int day = 1){_year = year;_month = month;_day = day;}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(2022, 1, 1);Date d2(2022, 1, 2);return 0;
}

首先先定义两个对象

首先我们可以先从比较大小的操作符进行重载

我们先将其定义在全局中试试

bool operator==(const Date& d1, const Date& d2)
{return d1._year == d2._year&& d1._month == d2._month&& d1._day == d2._day;
}

在写好函数后,我们会发现一个问题,由于类中的类成员变量被封装为私有,我们无法在全局函数中进行访问,因此我们可以将其写作类成员函数

class Date
{
public:Date(int year = 0, int month = 1, int day = 1){_year = year;_month = month;_day = day;}Date(const Date& d){_year = d._year;_month = d._month;_day = d._day;}void Print(){cout << _year << "-" << _month << "-" << _day << endl;}
​bool operator==(const Date& d1, const Date& d2){return d1._year == d2._year&& d1._month == d2._month&& d1._day == d2._day;}private:int _year;int _month;int _day;
};
int main()
{Date d1(2022, 1, 1);Date d2(2022, 1, 2);return 0;
}

 但我们若是直接将这个函数放进类中,又会出现一个问题

 可以看到,这是由于参数过多引起的,这是因为,类成员函数中有一个隐藏的参数this。这也就印证了注意中所说参数问题

那么,我们就需要去除一个参数

bool operator==(const Date& d)
{return _year == d._year;&& _month == d._month&& _day == d._day;
}

这样,便可以编译成功,进行操作符的使用了

int main()
{Date d1(2022, 1, 1);Date d2(2022, 1, 2);Date d3(2022, 1, 1);Date d4(2022, 1, 1);printf("%d\n", d1.operator==(d2));printf("%d\n", d3 == d4);return 0;
}

 而在使用时,有以上两种方法,第一种方法类似于函数,而第二种更符合操作符的写法,因此我们常常采用第二种写法。

而在其他的作比较的操作符中,整体思路都是一致的,只需要我们对函数主体进行修改即可,这里就不多做赘述。



2.赋值运算符重载

赋值运算符重载格式

参数类型:const T&,传递引用可以提高传参效率

返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值

检测是否自己给自己赋值

返回*this :要复合连续赋值的含义

Date& operator=(const Date& d)
{if(this != &d){_year = d._year;_month = d._month;_day = d._day;}return *this;
}

形式与上面的运算符重载类似,但还有一些其他的注意事项

1.我们可以看到,我们在这里使用的传引用返回,因为this指针的作用域是整个类,不会在函数结束后被销毁,所以可以使用传引用返回

2.用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数。

而既然这样,我们的日期类其实就不需要自己实现赋值运算符了,只需要进行逐字节的拷贝。而与拷贝构造函数类似的是,如果类中未涉及到资源管理,赋值运算符是否实现都可以;一旦涉及到资源管理则必须要实现。


3.前置++和后置++重载

前置++和后置++都是一元运算符,为了让前置++与后置++形成能正确重载,C++规定:后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器自动传递。

Date& operator++()//前置
{_day += 1;return *this;
}Date operator++(int)//后置
{Date temp(*this);_day += 1;return temp;
}

而由于后置++需要先赋值后运算,所以我们需要一个临时变量拷贝运算前的结果,并在运算结束后将其返回,而又因为它与this指针不同,是一个临时变量,因此不能使用传引用返回。

当然,由于这是一个日期类,我们在前置++或后置++时不仅仅是单纯的对_day进行运算,我们这里只是为了介绍这两种运算符的重载,所以只是简单的实现一下,而若是真的想要全面地实现一个日期类,必然要对年份和月份进行考虑,这里就先不多做赘述。
 


http://www.ppmy.cn/news/10897.html

相关文章

Python识别屏幕题目并模拟做题

前言 马上就要过年了&#xff0c;有许多小伙伴们本本还没拿到&#xff0c;还在苦苦刷题&#xff0c;一直及格不了&#xff0c;现在&#xff0c;我们用Python模拟做题&#xff0c;看看效果。 环境使用 python 3.9pycharm 模块使用 requestsreselenium谷歌驱动 import reimpor…

Leetcode - 19 - 删除链表的倒数第 N 个结点

19. 删除链表的倒数第 N 个结点 题目描述 给你一个链表&#xff0c;删除链表的倒数第 n 个结点&#xff0c;并且返回链表的头结点。 示例1&#xff1a; 输入&#xff1a;head [1,2,3,4,5], n 2 输出&#xff1a;[1,2,3,5]示例 2&#xff1a; 输入&#xff1a;head [1],…

Django(14):分页查询

使用Django框架内置模块django.core.paginator中封装的Paginator类Page类进行分页功能实现。其中Paginator是分页器&#xff0c;从分页器中可以得到Page&#xff0c;即分页对象。源码如下&#xff1a; import collections.abc import inspect import warnings from math impor…

接口测试——postman和Jemter

接口测试——postman和Jemterpostmanpostman工作原理postman入门postman的基础用法postman的高级用法使用postman管理测试用例批量执行测试用例postman断言环境变量和全局变量postman关联postman请求前置脚本postman参数化及生成测试报告参数化与数据驱动postman生成测试报告je…

JDK并发编程Actomic和AQS详解

JDK并发编程Actomic和AQS详解1 Atomic系列优化加锁并发性能1.0 i和ActomicInteger之间的差别分析1.1 AtomicInteger中的CAS无锁化原理1.2 AtomicInteger源码剖析&#xff1a;仅限JDK内部使用的Unsafe类1.3 AtomicInteger源码剖析&#xff1a;无线重复循环以及CAS操作1.4 Atomic…

Kubernetes(k8s) 笔记总结(二)

提示&#xff1a;针对kubernetes的工作均衡学习。 文章目录1. Kubernetes 创建资源方式2. Kubernetes 操作NameSpace3. Kubernetes的 Pod应用3.1 Pod的 解释3.2 通过命令行来创建一个pod3.3 配置文件方式创建一个Pod3.4 dashboard 可视化操作Pod3.5 针对Pod的一些细节操作3.6 P…

基于 js 制作一个贪吃蛇小游戏

目录前言&#xff1a;项目效果展示&#xff1a;代码实现思路&#xff1a;使用方法&#xff1a;实现代码&#xff1a;总结&#xff1a;前言&#xff1a; 在工作学习之余玩一会游戏既能带来快乐&#xff0c;还能缓解生活压力&#xff0c;跟随此文一起制作一个小游戏吧。 描述&…

Java日期时间类

Java日期时间类Datenew Date()**获取当前系统时间**通过**指定毫秒数得到时间**format**指定日期格式**SimpleDateFormat的模式字母&#xff1a;parse()可以把**格式化的String转成对应Date**Calendar&#xff08;日历&#xff09;创建日期类对象获取日历对象的某个日历字段第三…