类和对象(一)
目录
一. 类的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进行运算,我们这里只是为了介绍这两种运算符的重载,所以只是简单的实现一下,而若是真的想要全面地实现一个日期类,必然要对年份和月份进行考虑,这里就先不多做赘述。