类与对象(中)
类的6个默认成员函数
如果一个类中什么成员都没有,简称为空类。空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下 6 个默认成员函数。默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。这六个默认成员函数用户不写编译器会自动生成,下边我来一一讲解
构造函数
对于以下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(2022, 7, 5); d1.Print(); Date d2; d2.Init(2022, 7, 6); d2.Print(); return 0; }
对于 Date 类,可以通过 Init 公有方法给对象设置日期,但如果每次创建对象时都调用该方法设置信息,未免有点麻烦,那能否在对象创建时,就将信息设置进去呢?构造函数 是一个 特殊的成员函数,名字与类名相同 , 创建类类型对象时由编译器自动调用 ,以保证每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次 。特性:
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;
};
void TestDate()
{
Date d1; // 调用无参构造函数
Date d2(2015, 1, 1); // 调用带参的构造函数
// 注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明
// 以下代码的函数:声明了d3函数,该函数无参,返回一个日期类型的对象
// warning C4930: “Date d3(void)”: 未调用原型函数(是否是有意用变量定义的?)
Date d3();
}
注意,无参的对象调用无参构造函数,传参的对象调用带参构造函数
如果类中没有显式定义构造函数,则 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 d1; return 0; }
如果没有显示写构造函数,编译器默认生成一个无参构造函数,不会报错,原因等会我会总结
关于编译器生成的默认成员函数,很多童鞋会有疑惑:不实现构造函数的情况下,编译器会生成默认的构造函数。但是看起来默认构造函数又没什么用? d 对象调用了编译器生成的默认构造函数,但是 d 对象 _year/_month/_day ,依旧是随机值。也就说在这里 编译器生成的默认构造函数并没有什么用??解答: C++ 把类型分成内置类型 ( 基本类型 ) 和自定义类型。内置类型就是语言提供的数据类型,如: int/char... ,自定义类型就是我们使用 class/struct/union 等自己定义的类型,看看下面的程序,就会发现编译器生成默认的构造函数会对自定类型成员 _t 调用的它的默认成员函数。class Stack { public: private: int* _a; int _capacity; int _top; }; // 两个栈实现一个队列 class MyQueue { private: Stack _pushst; Stack _popst; int _size = 1; }; int main() { Stack st1; MyQueue mq; return 0; }
看上面这段代码,会是什么样的结果呢
可以看到内置类型没有被处理(size用的是c++11的缺省值,没写缺省值也是随机值),mq被处理了是vs2022的版本太高了,用vs2013测试内置类型还是随机值,所以我们认为内置类型不做处理
、
如果栈的构造函数我们写了,看一下会发生什么
可以看到成功调用我们写的构造函数,栈的内置类型和mq的自定义类型都被初始化了
得出一个结论:我们不写编译器会生成一个默认成员函数,内置成员不做处理,自定义类型会去调用它的默认构造
c++11打了一个补丁,支持声明时给缺省值(当我们没写内置类型会默认用缺省值)
一般情况下,我们都要自己写构造函数,当成员都是自定义类型,或者声明时给了缺省值,可以考虑让编译器自己生成构造函数
1、我们不写编译默认生成那个构造函数,叫默认构造 2、无参构造函数也可以叫默认构造 3、全缺省也可以叫默认构造 可以不传参数就调用构造,都可以叫默认构造 这三个函数不能同时存在,只能存在一个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; }; // 以下测试函数能通过编译吗? void Test() { Date d1; }
会报错,调用不明确,验证了只能有一个默认构造函数
析构函数
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; } ~Date() { // Date严格来说,不需要写析构函数 cout << "~Date()" << endl; } private: // C++11支持,声明时给缺省值 int _year = 1; int _month = 1; int _day = 1; }; class Stack { public: Stack(size_t capacity = 3) { cout << "Stack(size_t capacity = 3)" << endl; _a = (int*)malloc(sizeof(int) * capacity); if (nullptr == _a) { perror("malloc申请空间失败!!!"); } _capacity = capacity; _top = 0; } ~Stack() { cout << "~Stack()" << endl; free(_a); _capacity = _top = 0; _a = nullptr; } private: int* _a; int _capacity; int _top; }; class MyQueue { private: Stack _pushst; Stack _popst; int _size = 1; }; // 21:17继续 int main() { Date d1; Stack st1; MyQueue mq; return 0; }
可以看到,三次构造,4次析构
说明析构函数原理和构造函数相似,内置类型成员不做处理,自定义类型成员会调用他的析构,并且Date类的析构可写可不写,不写和写的效果一样,但是stack这种必须写,编译器默认生成的并不会帮我们释放堆区空间,会造成内存泄漏
拷贝构造函数
特征拷贝构造函数也是特殊的成员函数,其 特征 如下:1. 拷贝构造函数 是构造函数的一个重载形式2. 拷贝构造函数的 参数只有一个 且 必须是类类型对象的引用 ,使用 传值方式编译器直接报错 ,因为会引发无穷递归调用。class Date { public: Date(int year = 1900, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } // Date(const Date& d) // 正确写法 Date(const Date& d) // 错误写法:编译报错,会引发无穷递归 { _year = d._year; _month = d._month; _day = d._day; } private: int _year; int _month; int _day; }; int main() { Date d1; Date d2(d1); return 0; }
如果拷贝构造形参没有&,会造成无穷拷贝,一定要加上引用
像上边这种代码是纯粹内置类型,我们不写编译器默认生成的也会完成值拷贝,所以这种类型全是内置类型的不用写构造函数也能完成任务
下边我用代码来解释原理
#include<iostream> using namespace std; class Stack { public: Stack(size_t capacity = 3) { cout << "Stack(size_t capacity = 3)" << endl; _a = (int*)malloc(sizeof(int) * capacity); if (nullptr == _a) { perror("malloc申请空间失败!!!"); } _capacity = capacity; _top = 0; } // Stack st2(st1); Stack(const Stack& stt) { cout << " Stack(Stack& stt)" << endl; // 深拷贝 _a = (int*)malloc(sizeof(int) * stt._capacity); if (_a == nullptr) { perror("malloc fail"); exit(-1); } memcpy(_a, stt._a, sizeof(int) * stt._top); _top = stt._top; _capacity = stt._capacity; } ~Stack() { cout << "~Stack()" << endl; free(_a); _capacity = _top = 0; _a = nullptr; } private: int* _a; int _capacity; int _top; }; class MyQueue { Stack _pushst; Stack _popst; int _size = 0; }; int main() { Stack st1; Stack st2(st1); MyQueue q1; MyQueue q2(q1); return 0; }
可以看到我们写了stack的构造和拷贝构造,结果st1和st2完成了构造和拷贝构造的任务,并且st1拷贝st2是深拷贝,_a新开辟了一块堆区空间
问题来了,为什么要写拷贝构造呢,因为不写默认生成的拷贝构造只能完成值拷贝,就像刚才Date类一样,只能完成内置类型值拷贝,自定义类型如果也是值拷贝,会造成同一块空间析构两次,因为是两个对象,作用域结束都会销毁,两个对象析构2次,所以是错误的,我们要针对栈类写一个拷贝构造,目的是完成深拷贝
再来看两个栈实现一个队列的q1,q2可以看到内置类型不做处理(我们认为内置类型不做处理,有些编译器会处理为0,但还是建议认为内置不做处理更好理解规则),自定义类型又去调用它的拷贝构造
总结:
Date 和 MyQueue 默认生成拷贝就可以用
1、内置类型成员完成值拷贝
2、自定义类型成员调用这个成员的拷贝构造
Stack需要自己写拷贝构造,完成深拷贝。
顺序表、链表、二叉树等等的类,都需要深拷贝(使用堆区空间的都需要深拷贝)
赋值运算符重载
运算符重载不是赋值重载,也不是默认成员函数,不写编译器不会默认生成,需要我们自己实现(默认实现成operator)
// 运算符重载 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; } //private: int _year; int _month; int _day; }; bool operator>(const Date& x, const Date& y) { if (x._year > y._year) { return true; } else if (x._year == y._year && x._month > y._month) { return true; } else if (x._year == y._year && x._month == y._month && x._day > y._day) { return true; } return false; } bool operator==(const Date& x, const Date& y) { return x._year == y._year && x._month == y._month && x._day == y._day; } int main() { Date d1; Date d2(2025, 10, 22); d1 == d2; d1 > d2; // 11:40 继续 bool ret1 = d1 > d2; // operator>(d1, d2);//编译器会转换成这个 bool ret2 = d1 == d2; // operator==(d1, d2); /*int x = 1, y = 2; bool ret1 = x > y; bool ret2 = x == y;*/ return 0; }
上边将运算符重载函数实现为全局的,如果没有友元需要将私有去掉,才能访问私有内置类型
为什么会有运算符重载呢,因为我们要比较的时候,内置类型是简单类型,可以直接用各种运算符,语言自己定义,编译直接转换成指令,但自定义类型是复杂类型不支持,需要我们自己实现
赋值运算符重载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& 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; }; int main() { Date d1; Date d2(2025, 1, 1); d2=d1; return 0; }
可以看到赋值运算符重载对于内置类型可以实现,也可以不实现,结果是一样的
赋值运算符只能重载成类的成员函数不能重载成全局函数(记住就行,本来就是在类里边实现的)
用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝 。 注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。这个赋值重载和拷贝构造的意义可以类比理解,都是内置类型不用处理,自定义类型会调用它的复制拷贝(也就是说有深拷贝的地方就要自己实现新的赋值拷贝,从而防止析构两次,和拷贝构造一个原理)
日期类的实现
我把日期类原理给讲一下,然后把代码整这上边想看了可以看看
这是简单的求日期天数,当输入非法日期会打印日期非法
打印日期很简单
运算符重载,年月日都相等会返回True
this是d1的地址,*this是d1,返回比较的真假
比较大小,先比较年,年大即大,年相等比较月,月大即大,年月都相等比较日,日大即大,其他情况都是不符合条件返回错误
这三个都比较简单,就不赘述了
得到天数,最好写成一个函数方便得到天数,也就是已知年和月要得到日
+直接构造一个tmp,直接复用+=,后返回tmp,因为+不会改变自身,所以不返回*this
+=可以直接写+的逻辑,然后返回*this,因为自身会修改(day<0复用-=逻辑)
-=和-和+=,+是一个逻辑,就不赘述了
注意,如果返回自身*this并且返回值加引用,是因为出了作用域自定义对象不会销毁,我们加一个&,可以少拷贝一次提高效率;没有&返回的时候,因为出作用域就销毁了,所以不能加引用,会拷贝构造一次
最后再说一下前置++和后置++
这里参数是空的默认是前置++,所以返回*this,有一个int是后置++,返回构造的对象
前置--和后置--是一个道理
流插入流提取的实现
全局会重载<<流插入运算符,内置类型会自动识别类型,自定义类型的打印需要自己实现
cout的类型是ostream,是一个流,将内存的值打印到屏幕上(从内存流到屏幕上)
cin就不写了,和cout反着来,从屏幕上流入内存
这里不能设成成员函数,成员函数默认第一个参数是this,不符合cout打印的格式
这里设成友元函数进行操作,可以实现正确打印格式
注意
const成员
将 const 修饰的 “ 成员函数 ” 称之为 const 成员函数 , const 修饰类成员函数,实际修饰该成员函数 隐含的 this 指针 ,表明在该成员函数中 不能对类的任何成员进行修改记住,const在成员函数后边表示修饰*this,const在*前边表示修饰指针指向的对象,在*后边修饰指针拿Print成员函数举例子,当是const对象时,const Date* 不能传给Date* const this 权限放大;但是可以传给const成员函数的Print()权限缩小,并且非const对象也能调用const成员函数的Print,是权限缩小所以我么看到总结:成员函数定义原则1.能定义成const的成员啊函数都应该定义成const,这样 const对象(权限平移)和非const对象都可以调用2.要修改成员变量的成员函数,不能定义成const,const对象不能调用(很合理),非const才能调用
请思考下面的几个问题:1. const 对象可以调用非 const 成员函数吗?不能,权限扩大2. 非 const 对象可以调用 const 成员函数吗?可以,权限缩小记住,权限平移或者缩小可以调用,权限扩大不行
取地址及const取地址操作符重载
就这两个东西,非const对象调用非const版本,返回非const,const对象调用const版本,返回const,一般我们不用实现,编译器默认会实现这两个成员函数
如果显示实现会调用显示实现的
没显示实现编译器会自己生成
到此类的6个默认成员函数都讲完了,如果有什么问题可以私信或者在评论区交流
感谢支持!!!
下边我把日期类的实现代码结合我们刚才讲的const和operator<<在复制一份在下边,有需要的可以看看(代码都已经测试过,没有任何问题,细节我能讲的已经全部讲到,欢迎友好交流)
日期类完整代码
Date.h
#include<iostream> #include<assert.h> using namespace std; class Date { public: Date(int year = 1, int month = 1, int day = 1); void Print() const; int GetMonthDay(int year, int month); bool operator==(const Date& y) const; bool operator!=(const Date& y) const; bool operator>(const Date& y) const; bool operator<(const Date& y) const; bool operator>=(const Date& y) const; bool operator<=(const Date& y) const; int operator-(const Date& d) const; Date& operator+=(int day); Date operator+(int day) const; Date& operator-=(int day); Date operator-(int day) const; Date& operator++(); Date operator++(int); Date& operator--(); Date operator--(int); friend ostream& operator<<(ostream& out, const Date& d); friend istream& operator>>(istream& in, Date& d); private: int _year; int _month; int _day; }; ostream& operator<<(ostream& out, const Date& d); istream& operator>>(istream& in, Date& d);
Date.cpp
#include "Date.h" Date::Date(int year, int month, int day) { _year = year; _month = month; _day = day; if (_year < 1 || _month < 1 || _month > 12 || _day < 1 || _day > GetMonthDay(_year, _month)) { //assert(false); Print(); cout << "日期非法" << endl; } } void Date::Print() const { cout << _year << "/" << _month << "/" << _day << endl; } bool Date::operator==(const Date& y) const { return _year == y._year && _month == y._month && _day == y._day; } // d1 != d2 bool Date::operator!=(const Date& y) const { return !(*this == y); } bool Date::operator>(const Date& y) const { if (_year > y._year) { return true; } else if (_year == y._year && _month > y._month) { return true; } else if (_year == y._year && _month == y._month && _day > y._day) { return true; } return false; } bool Date::operator>=(const Date& y) const { return *this > y || *this == y; } bool Date::operator<(const Date& y) const { return !(*this >= y); } bool Date::operator<=(const Date& y) const { return !(*this > y); } int Date::GetMonthDay(int year, int month) { assert(year >= 1 && month >= 1 && month <= 12); int monthArray[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 monthArray[month]; } // d1 += 100 Date& Date::operator+=(int day) { if (day < 0) { return *this -= (-day); } _day += day; while (_day > GetMonthDay(_year, _month)) { _day -= GetMonthDay(_year, _month); ++_month; if (_month == 13) { _year++; _month = 1; } } return *this; } Date Date::operator+(int day) const { Date tmp(*this); tmp += day; return tmp; } Date& Date::operator-=(int day) { if (day < 0) { return *this += (-day); } _day -= day; while (_day <= 0) { --_month; if (_month == 0) { --_year; _month = 12; } _day += GetMonthDay(_year, _month); } return *this; } Date Date::operator-(int day) const { Date tmp(*this); tmp -= day; return tmp; } // 21:13继续 // ++d1 Date& Date::operator++() { *this += 1; return *this; } // d1++ Date Date::operator++(int) { Date tmp(*this); *this += 1; return tmp; } Date& Date::operator--() { *this -= 1; return *this; } Date Date::operator--(int) { Date tmp(*this); *this -= 1; return tmp; } // d1 - d2 int Date::operator-(const Date& d) const { // 假设左大右小 int flag = 1; Date max = *this; Date min = d; // 假设错了,左小右大 if (*this < d) { max = d; min = *this; flag = -1; } int n = 0; while (min != max) { ++min; ++n; } return n * flag; } ostream& operator<<(ostream& out, const Date& d) { out << d._year << "年" << d._month << "月" << d._day << "日" << endl; return out; } istream& operator>>(istream& in, Date& d) { in >> d._year >> d._month >> d._day; return in; }
test.cpp
#include "Date.h" //void TestDate1() //{ // Date d1(2023, 10, 1); // Date d2(2023, 11, 3); // d1.Print(); // // cout << d1 << d2 << endl; // // // cin >> d1 >> d2; // cout << d1 << d2 << endl; // // int i = 0; // i << 10; // // /*double d = 1.1; // int i = 2; // // cout << i; // cout << d;*/ //} //int main() //{ // TestDate1(); // return 0; //} int main() { // const对象和非const对象都可以调用const成员函数 const Date d1(2025, 10, 31); //d1.Print(); Date d2(2025, 1, 1); //d2.Print(); cout << &d1 << endl; cout << &d2 << endl; return 0; }