对类的进一步认识
目录
(1)类的6个默认成员函数
一个类中什么成员都没有,简称空类。但空类并不是真的什么都没有。任何类在什么都不写的情况下,编译器会自动生成以下六个默认成员函数。
默认成员函数:用户没有显示实现,编译器会生成的成员函数称为默认成员函数。
class Date {};
(2)构造函数
1.概念:
对于以下Date类:
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(2024,8,5); d1.Print(); Date d2; d2.Init(2024,8,6); d2.Print(); return0; }
对于Date类,可以通过Init公有方法给对象设置日期,能否在对象创建时,就将信息设置进去呢?
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有一个合适的初始值,并且在对象整个生命周期内只调用一次。
2.特性:
构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。
其特征如下:
1.函数名与类名相同。
2.无返回值。
3.对象实例化时编译器自动调用对应的构造函数。
4.构造函数可以重载。(在同一个类中可以定义多个构造函数,这些构造函数有不同的参数列表)
class Date { public: // 1.无参构造函数 Date() {} // 2.带参构造函数 Date(int year,int month,int day) { _year = year; _month = month; _day = day; } private: int _year; int _month; int _day; }; int main() { Date d1; // 调用无参构造函数 Date d2(2024,8,6); // 调用带参的构造函数 }
注意:不能写成“Date d1()”。通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明。(相当于声明了d1函数,该函数无参,返回一个Date类型的对象。)
如果类中没有显示定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显示定义编译器将不再生成。
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类中构造函数屏蔽后,代码可以通过编译,因为编译器生成了一个无参的默认构造函数 // 将Date类中构造函数放开,代码编译失败,因为一旦显式定义任何构造函数,编译器将不再生成 // 无参构造函数,放开后报错:error C2512: “Date”: 没有合适的默认构造函数可用 Date d1; d1.Print(); return 0; }
注:编译器生成默认的构造函数会对自定义类型成员_t调用的它的默认成员函数。
#include <iostream> using namespace std; class Time { public: Time() { cout << "Time()" << endl; _hour = 0; _minute = 0; _second = 0; } public: int _hour; int _minute; int _second; }; class Date { public: // 基本类型(内置类型) int _year; int _month; int _day; // 自定义类型 Time _t; }; int main() { Date d; cout << d._t._hour << ' ' << d._t._minute << ' ' << d._t._second << endl; return 0; }
这里为了方便观察,限定符都置为“public”。
从结果可以看出编译器生成默认的构造函数Date()会对自定类型成员_t调用的它的默认成员
函数,从而完成赋值和打印“Time()”。注意:无参构造函数,全缺省构造函数,我们没写编译器默认生成的构造函数,都可以认为是默认构造函数。默认构造函数只能有一个。
class Date { public: // 无参构造函数 Date() { _year = 1900; _month = 1; _day = 1; } // 全缺省构造函数 Date(int year = 1900,int month = 1,int day = 1) { _year = year; _month = month; _day = day; } private: int _year; int _month; int _day; }; int main() { Date d1; return 0; }
以上的代码的编译显然是通过不了编译的。无参构造函数和全缺省构造函数同为默认构造函数不能同时存在。
(3)析构函数
1.概念:
析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
2.特性:
析构函数是特殊的成员函数,特征如下:
1.在类名前加上字符‘~’就是析构函数名。
2.无参数无返回值类型。
3.一个类只能有一个析构函数。若未显示定义,系统会自动生成默认的析构函数。
4.析构函数不能重载。
5.对象完成工作时,C++编译系统自动调用析构函数。
注意:如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如Date类;有资源申请时,一定要写,不然可能造成内存泄漏,比如Stack类。
#include <iostream> using namespace std; class Stack { public: Stack(size_t capacity = 3) { cout << "Stack()" << endl; _array = (int*)malloc(sizeof(int)*capacity); if(_array == NULL) { perror("malloc申请空间失败!!!"); return; } _capacity = capacity; _size = 0; } void Push(int data) { _array[_size] = data; _size++; } ~Stack() { cout << "~Stack()" << endl; if(_array) { free(_array); _array = NULL; _capacity = 0; _size = 0; } } private: int *_array; int _capacity; int _size; }; int main() { Stack S; S.Push(1); S.Push(2); return 0; }
运行结果:
由上面运行结果可以看出对象完成工作结束的时候会自动调用析构函数。
此外,编译器生成的默认析构函数,可以对自定义类型成员调用它的析构函数。
#include <iostream> using namespace std; class Time { public: ~Time() { cout << "~Time()" << endl; } private: int _hour; int _minute; int _second; }; class Date { private: // 基本类型(内置类型) int _year = 1970; int _month = 1; int _day = 1; // 自定义类型 Time _t; }; int main() { Date d; return 0; }
运行结果:
说明了Time类的析构函数被调用。
思考:在main()方法中没有直接创建Time类的对象,为什么最后会调用Time类的析构函数?
Answer:main方法中创建了Date对象d,d中的_year,_month,_day三个是内置类型成员,销毁时不需要资源清理,只需要系统直接将其内存回收就行了;_t是Time类对象,在d销毁时,需要将包含的Time类的_t对象销毁,于是就会调用Time类的析构函数。main函数中不能直接调用Time类的析构函数,所以编译器会调用Date类的析构函数,编译器给Date生成了默认的构造函数,然后调用Time类的析构函数,就可以保证每个自定义类型可以被正确的销毁。
(4)拷贝构造函数
1.概念:
拷贝构造函数是C++中的一种特殊的构造函数,用于初始化一个对象为另一个同类对象的副本。其主要用途是在以下情况创建对象时,自动调用拷贝构造函数:
1.通过值传递参数:当一个对象作为参数传递给一个函数时,会创建该对象的副本。
2.返回对象:当一个函数返回一个对象时,也会调用拷贝构造函数。
3.对象的初始化:在一个对象的定义中,用另一个对象进行初始化时,会调用拷贝构造函数。
2.特性:
拷贝构造函数是特殊的成员函数,其特性如下:
1.只有单个形参,一般用const修饰。
2.拷贝构造函数是构造函数的一个重载形式。
3.拷贝构造函数的参数只有一个且必须是类类型的引用(引用相当于起别名),不引用会引发无穷递归。(调用拷贝构造函数的时候要先传参,而传参就又是一个拷贝构造,从而引起无限套娃。)
class Date { public: Date(int year = 0,int month = 1,int day = 1) { _year = year; _month = month; _day = day; } // Date d2(d1); --> 调用的就是拷贝构造,但是调用函数时要先传参,传参的过程又是一个拷贝构造的调用 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(2024,8,6); // 这两种写法都是拷贝构造 Date d2(d1);// 拷贝构造 Date d3 = d1; d1.Print(); d2.Print(); d3.Print(); return 0; }
注:在这里补充一个问题:
void func(Date d) { //... } int main() { Date d1; func(d1); return 0; }
为什么在上面拷贝构造函数传参的时候会死循环,而这个函数不会。-->>我们调用func,先传参,传参就是一个拷贝构造,然后调用Date类的拷贝构造函数,然后Date类的拷贝构造函数是已经是引用类型的,所以不会造成死循环。
4.若用户未显示定义拷贝构造函数,那么编译器就会生成默认的拷贝构造函数。默认的拷贝构造函数对象按内存存储按字节序完成拷贝,也叫做浅拷贝。
#include <iostream> using namespace std; class Time { public: Time() { _hour = 1; _minute = 1; _second = 1; } Time(const Time& d) { _hour = d._hour; _minute = d._minute; _second = d._second; cout << "Time::Time(const Time&)" << endl; } private: int _hour; int _minute; int _second; }; class Date { private: // 基本类型(内置类型) int _year = 1970; int _month = 1; int _day = 1; // 自定义类型 Time _t; }; int main() { Date d1; // 用存在的d1拷贝d2,此处会调用Date类的拷贝构造函数 // 但Date类并没有显式定义拷贝构造函数,则编译器会给Date类生成一个默认的拷贝构造函数 Date d2(d1); return 0; }
运行结果:
运行结果说明了Time类的拷贝构造函数被调用。(在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的。)
注:深拷贝与浅拷贝:
1.浅拷贝:直接复制对象的所有成员(默认行为)。如果对象中包含指向动态内存分配的指针,这可能会导致多个对象指向同一内存地址,造成数据损坏或内存泄漏。
2.深拷贝:复制对象时,为每个指针分配新的内存并拷贝内容,确保各个对象独立拥有自己的数据。(在需要管理动态资源的类中,通常自定义拷贝构造函数,以实现深拷贝。)
(5)赋值运算符重载
1.运算符重载
运算符重载是具有特殊函数名的函数,也具有其返回类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字:关键字operator后面接需要重载的运算符号。
函数原型:返回值类型operator操作符(参数列表)。
注意:
- 不能通过连接其他符号来创建新的操作符:比如operator@
- 重载操作符必须有一个类类型参数
- 用于内置类型的运算符,其含义不能改变,比如:内置的整型+,不能改变其含义。
- 作为类成员函数重载时,其形参看起来比操作数数目少一,因为成员函数的第一个参数为隐藏的this指针
- “ .* ”,“ :: ”,“ sizeof ”,“ ?: ”,“ . ” 这五个运算符不能重载。
对于一个Date类:
class Date { public: Date(int year = 1900,int month = 1,int day = 1) { _year = year; _month = month; _day = day; } private: // 基本类型(内置类型) int _year = 1970; int _month = 1; int _day = 1; // 自定义类型 Time _t; };
我们知道我们可以比较内置类型(int,float....)的数据大小,那么如果我们想比较日期的大小时,编译器会报错。这时我们就要对操作符进行重载。
// 全局的operator== class Date { public: Date(int year = 1900,int month = 1,int day = 1) { _year = year; _month = month; _day = day; } private: // 基本类型(内置类型) int _year = 1970; int _month = 1; int _day = 1; }; // 运算符有几个操作数,operator重载的函数就有几个参数 bool operator==(const Date& d1,const Date& d2) { return d1._year == d2._year && d1._day == d2._day && d1._month == d2._month; }
我们把运算符重载写到全局里,会发现d1和d2的成员变量为private,全局里无法访问,还需要把private改成public。所以我们干脆就直接把运算符重载写成类的成员函数。
#include <iostream> using namespace std; 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; } // d1 == d2 // d1.operator==(&d1,d2); bool operator==(const Date& d) { return this->_year == d._year && _month == d._month && _day == d._day; } private: int _year; int _month; int _day; };
这里写成成员函数时,我们可以明显的看到参数只有一个,但实际上还有一个隐藏的this指针,且为第一个参数。(bool operator==(Date* this,const Date& d) // 不用我们显式写出)
接下来我们实现一个稍微完整的日期类:
#include <iostream> using namespace std; // 实现一个完整的日期类 class Date { public: int GetMonthDay(int year,int month) { static int monthDays[13] = {0,31,28,31,30,31,30,31,31,30,31,30,31}; if(month == 2 && ((year%4 == 0 && year % 100 != 0) || year %400 == 0)) { return 29; } return monthDays[month]; } Date(int year = 0,int month = 1,int day = 1) { if(year >= 0 && month >= 1 && month <= 12 && day >= 1 && day <= GetMonthDay(year,month)) { _year = year; _month = month; _day = day; } else { cout << "非法日期" << endl; } } // Date d2(d1); Date(const Date& d) { _year = d._year; _month = d._month; _day = d._day; } void Print() { cout << _year << "-" << _month << "-" << _day << endl; } // bool operator(Date* this,const Date& d) bool operator<(const Date& d) { if(_year < d._year) return true; else if(_year == d._year && _month < d._month) return true; else if(_year == d._year && _month == d._month && _day < d._day) return true; return false; } bool operator==(const Date& d) { return _year == d._year && _month == d._month && _day == d._day; } // d1 <= d1; // d1.operator(&d1,d2); bool operator<=(const Date& d) { return *this < d || *this ==d;//复用上面的来实现 } bool operator>(const Date& d) { return !(*this <= d);// !表示逻辑取反 } bool operator>=(const Date& d) { return !(*this < d); } bool operator!=(const Date& d) { return !(*this == d); } private: int _year; int _month; int _day; }; int main() { Date d1(2024,8,6); d1.Print(); Date d2(2024,8,7); d2.Print(); cout << (d1 < d2) << endl; system("pause"); return 0; }
上面包含了日期大小的基本比较判断,日期的合法判断。还有日期与天数的加减,读者可以自行实现一下。
2.赋值运算符的重载
1.赋值运算符重载格式
- 参数类型:const T&,传递引用可以提高传参效率。
- 返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
- 检测是否自己给自己赋值
- 返回*this:要复合连续赋值的含义
class Date { public: Date(int year = 1900,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; } Date& operator=(const Date&d) { if(this != &d) // 针对自己赋值的判断检查 { _year = d._year; _month = d._month; _day = d._day; } return *this; } private: int _year; int _month; int _day; };
2.赋值运算符只能重载成类的成员函数不能重载成全局函数
#include <iostream> using namespace std; class Date { public: Date(int year = 1900,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; } // private: int _year; int _month; int _day; }; Date& operator=(Date& left,const Date& right) { if(&left != &right) { left._year = right._year; left._month = right._month; left._day = right._day; } return left; } int main() { cout << "Hello World!" << endl; system("pause"); return 0; }
运行结果:
编译失败,“operator=”必须是非静态成员。赋值运算符如果不显式实现,编译器会实现一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数。
3.用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要对应类的赋值运算符重载完成赋值。
#include <iostream> using namespace std; class Time { public: Time() { _hour = 1; _minute = 1; _second = 1; } Time& operator=(const Time& t) { cout << "operator=" << endl; if(this != &t) { _hour = t._hour; _minute = t._minute; _second = t._second; } return *this; } private: int _hour; int _minute; int _second; }; class Date { private: // 基本类型 int _year; int _month; int _day; // 内置类型 Time _t; }; int main() { Date d1; Date d2; d1 = d2; system("pause"); return 0; }
运行结果:
那么我们提出一个问题:既然编译器生成的默认赋值运算符重载函数已经可以完成字节序的值拷贝了,还需要自己实现吗?当然像日期类这样的类是没必要的。那么对于Stack类,我们来看一下。
class Stack { public: Stack(size_t capacity = 10) { _array = (int*)malloc(capacity*sizeof(int)); if(_array == nullptr) { cout << "malloc申请空间失败" << endl; return; } } void Push(const int& data) { // CheckCapacity(); _array[_size] = data; _size++; } ~Stack() { if(_array) { free(_array); _array = nullptr; _capacity = 0; _size = 0; } } private: int *_array; size_t _size; size_t _capacity; }; int main() { Stack s1; s1.Push(1); s1.Push(2); s1.Push(3); s1.Push(4); Stack s2; s2 = s1; system("pause"); return 0; }
运行结果:
程序崩掉了。(s2 = s1,当s1给s2赋值时,编译器会将s1中内容原封不动地拷贝到s2中,这样会导致两个问题:1.s2原来的空间丢失了,存在内存泄漏。2.s1和s2共享一份内存空间,最后销毁时会导致同一份内存空间释放两次而引起程序崩溃。)
Note:如果类中未涉及到资源管理,赋值运算符是否实现都可以;一旦涉及到资源管理则必须要实现。
3.前置++和后置++重载
class Date { public: Date(int year = 1900,int month = 1,int day = 1) { _year = year; _month = month; _day = day; } void Print() { cout << _year << '-' << _month << '-' << _day << endl; } // 前置++:返回+1之后的结果 Date& operator++() {// 注意:this指向的对象函数结束后不会销毁,故以引用方式返回提高效率 _day += 1; return *this; } // 后置++ Date operator++(int) {// 后置++是先使用后+1,因此需要返回+1之前的旧值,故需在实现时需要先将this保存一份,然后给this+1 // 而temp是临时对象,因此只能以值的方式返回,不能返回引用 Date temp(*this); _day += 1; return temp; } private: int _year; int _month; int _day; }; int main() { Date d1; Date d3; Date d2(2024,1,1); d1 = d2++; d3 = ++d2; cout << "d2++:" << endl; d1.Print(); d2.Print(); cout << endl; cout << "++d2:" << endl; d3.Print(); d2.Print(); cout << endl; system("pause"); return 0; }
运行结果:
前置++和后置++都是一元运算符,为了让前置++与后置++能正确重载,C++规定:后置++重载是多增加一个int类型的参数,但调用函数时该参数不用传递,编译器自动传递。
(6)const成员函数
将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
class Date { public: // 显示日期信息:年-月-日 void Display() const { cout << _year << '-' << _month << '-' << _day << endl; } private: int _year; int _month; int _day; };
编译器会把const成员函数处理成以下形式:
class Date { public: // 显示日期信息:年-月-日 void Display(const Date* this) { cout << this->_year << '-' << this->_month << '-' << this->_day << endl; } private: int _year; int _month; int _day; };
(7)取地址及const取地址操作符重载
这两个默认成员函数一般不用重新定义,编译器会默认生成。
class Date { public: Date* operator&() { return this; } const Date* operator&() const { return this; } private: int _year; int _month; int _day; };
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容。