类和对象(2)
类的默认成员函数
默认成员函数:用户没有显示实现,编译器自动生成的成员函数,就是默认成员函数。
一个类,在我们不写的情况下,会自动生成6个成员函数:
这六个默认成员函数最后两个取地址重载了解一下即可,着重学习前四个。
默认成员函数很重要,学起来也很复杂,我们要从两个方面去学习:
- 第⼀:我们不写时,编译器默认⽣成的函数⾏为是什么,是否满⾜我们的需求。
- 第⼆:编译器默认⽣成的函数不满⾜我们的需求,我们需要⾃⼰实现,那么如何⾃⼰实现?
构造函数
构造函数是特殊的成员函数,虽然名为构造函数,但它的作用不是用来开辟空间创建对象,而是对实例化的对象进行初始化,(平时常使用的局部对象在栈帧创建时,就已经开辟了空间)。构造函数很好的代替了我们平时使用的Ini
函数。
构造函数的特点:
- 函数名与类名相同
- 无返回值(不用纠结这是为什么,c++规定就是如此)
- 对象实例化时自动调用对应的构造函数
- 构造函数可以重载
- 如果用户没有显示的实现构造函数,编译器会自动生成一个无参的默认构造函数,一旦用户显示的实现了则编译器不在生成。
- 无参构造 、全缺省构造函数 、编译器自动生成的构造函数 都叫默认构造函数 ,但是这三个默认构造函数只能存在一个。无参构造 与全缺省构造 使用时存在歧义 ,使用时需要注意。
总结一下:不用传实参就能调用的构造函数就是默认构造函数。
- 我们不写,编译器自动生成的默认构造函数对内置类型成员变量的初始化没有要求,在有的编译器下,内置类型成员会被初始化成0,有的编译器则是不会对内置类型进行初始化(所以调试看到的是一个随机值)。对于自定义类型的成员变量,则会调用对应的默认构造函数,若没有则会报错,我们要初始化这个成员变量,则需要用到初始化列表。
当实例化对象时,自动调用构造函数:
构造函数可以重载:
当我们没显示的实现构造函数时,编译器自动生成一个默认构造函数,对于内置类型,在vs这个平台下是不会对其进行初始化的。
对于自定义类型成员变量,则会调用对应的构造函数,如果没有则会报错。
类MyQueue实例化出的对象q1的成员变量是Push
与Pop
,它俩是Stack类型,当q1调用构造函数时,对于自定义类型的成员变量,会调用该类型对应的构造函数,也就是Stack(int capacity=4)
,若没有构造函数,则会报错。
三种默认构造函数。
当是无参构造时,不能在对象后面加()括号。
回想在c语言中对函数的学习,声明一个函数是:类型 函数名(参数)
当是无参构造是,在对象后加括号,编译器就不知道你这是在声明函数还是在实例化对象了。
默认构造就三种: 除去这三种,其他都不是默认构造函数。
- 无参构造函数
- 全缺省构造函数
- 没显示实现构造函数时,编译器默认生成的构造函数。
下面展示的两种构造都不是默认构造。
有参构造 :
半缺省构造:
析构函数
析构函数的功能与构造函数的功能相反。
析构函数的作用是:对对象中的资源进行清理与释放。不要理解成对对象的销毁。比如局部对象是存在栈帧中的,函数结束,栈帧销毁,对象也随之销毁。这一过程不归析构函数管,c++规定,当栈帧销毁时,对象调用析构函数,对对象中的资源进行清理。
析构函数的特点:
- 析构函数名与类型相同,但是会在前面加一个"~"
- 无参数,无返回值
- 一个类只能有一个析构函数,当用户没显示的实现析构函数时,编译器会自动生成一个析构函数
- 对象生命周期结束时,自动调用析构函数
- 当用户没显示的实现析构函数时,编译器自动生成的析构函数对内置类型不做处理,对自定义类型会调用它的析构函数。
- 我们显⽰写析构函数,对于⾃定义类型成员也会调⽤他的析构,也就是说⾃定义类
型成员⽆论什么情况都会⾃动调⽤析构函数。 - 如果类中没有申请资源,那么我们可以使用编译器自动生成的析构函数,如果存在申请资源,则一定要自己写析构函数。
- 一个局部与多个对象,c++规定后实例化的先析构。
下面根据代码来理解一下:
析构函数名是在类名前加上字符 ~。
⽆参数⽆返回值。
⼀个类只能有⼀个析构函数。若未显式定义,系统会⾃动⽣成默认的析构函数。
对象⽣命周期结束时,系统会⾃动调⽤析构函数。
跟构造函数类似,我们不写编译器⾃动⽣成的析构函数对内置类型成员不做处理,⾃定类型成员会调⽤他的析构函数。
还需要注意的是我们显⽰写析构函数,对于⾃定义类型成员也会调⽤他的析构,也就是说⾃定义类
型成员⽆论什么情况都会⾃动调⽤析构函数。
- 如果类中没有申请资源,那么我们可以使用编译器自动生成的析构函数,如果存在申请资源,则一定要自己写析构函数。
- 一个局部与多个对象,c++规定后实例化的先析构。
这两点自行测试。
拷贝构造函数
若一个构造函数的第一个参数是自身类类型的引用,且其他任何额外的参数都有默认值,则此构造函数也叫做拷贝构造函数。拷贝构造函数是一个特殊的构造函数
拷贝构造函数的特点:
- 拷贝构造是构造函数的一个承载
- 拷贝构造的第一个参数必须是对自身类类型的引用,如果使用传值方式,编译器会直接报错,因为在语法逻辑上会引发无线递归。拷贝构造函数可以有多个参数,但第一个参数必须是对自身类类型的引用,后面的参数必须有缺省值
- c++规定,自定义类型对象进行拷贝行为必须调用拷贝构造,所以自定义类型传值传参和传值返回都会调用拷贝构造。
- 若未显式定义拷⻉构造,编译器会⽣成⾃动⽣成拷⻉构造函数。自动生成的拷贝构造对内置类型进行浅拷贝/值拷贝(一个字节一个字节的拷贝),对自定义类型会调用它的拷贝构造。
- 若一个类中的成员变量没有指向资源,则使用编译器自动生成的拷贝构造进行浅拷贝就行,但若类中的成员变量有指向资源,则需要我们显示的写拷贝构造,因为浅拷贝无法满足我们的需求。有个小技巧:有显示实现析构并释放资源的,就需要显示写拷贝构造。
- 传值返回会产⽣⼀个临时对象调⽤拷⻉构造。传值引⽤返回,返回的是返回对象的别名(引⽤),没有产⽣拷⻉,但若返回的是一个函数局部域的局部对象,当函数结束,栈帧销毁,这个局部对象也随之销毁,此时传该局部对象的引用回去,就会产生类似野指针的情况,称为野引用。传引⽤返回可以减少拷⻉,但是⼀定要确保返回对象,在当前函数结束后还在,才能⽤引⽤返回。
拷贝构造函数是构造函数的一个重载。
拷贝构造的第一个参数必须是对自身类类型的引用,如果使用传值方式,编译器会直接报错,因为在语法逻辑上会引发无线递归
c++规定,自定义类型对象进行拷贝行为必须调用拷贝构造,所以自定义类型传值传参和传值返回都会调用拷贝构造。如果没显示的写,则会调用编译器自动生成的拷贝构造函数。
若未显式定义拷⻉构造,编译器会⽣成⾃动⽣成拷⻉构造函数。自动生成的拷贝构造对内置类型进行浅拷贝/值拷贝(一个字节一个字节的拷贝),对自定义类型会调用它的拷贝构造。图1是内置类型,直接进行浅拷贝,调用编译器自动生成的拷贝构造就行,图2是自定义类型,指向了资源,所以需要我们显示的实现拷贝构造,这种拷贝又叫深拷贝。
前面提到了深拷贝,什么是深拷贝?为什么成员变量指向了资源就要用深拷贝?
下面我用Stakc类来解释说明。
关于最后一点特性,类比指针来就好理解了:
赋值运算符重载
运算符重载
- 当运算符被⽤于类类型的对象时,C++语⾔允许我们通过运算符重载的形式指定新的含义。C++规定类类型对象使⽤运算符时,必须转换成调⽤对应运算符重载,若没有对应的运算符重载,则会编译报错。
- 运算符重载是具有特殊名字的函数,他的名字是由operator和后⾯要定义的运算符共同构成。和其他函数⼀样,它也具有其返回类型和参数列表以及函数体。
- 重载运算符函数的参数个数和该运算符作⽤的运算对象数量⼀样多。⼀元运算符有⼀个参数,⼆元运算符有两个参数,⼆元运算符的左侧运算对象传给第⼀个参数,右侧运算对象传给第⼆个参数。
- 如果⼀个重载运算符函数是成员函数,则它的第⼀个运算对象默认传给隐式的this指针,因此运算符重载作为成员函数时,参数⽐运算对象少⼀个。
- 运算符重载以后,其优先级和结合性与对应的内置类型运算符保持⼀致。
- 不能通过连接语法中没有的符号来创建新的操作符:⽐如operator@。
.* :: sizeof ?: .
注意以上5个运算符不能重载。- 重载操作符⾄少有⼀个类类型参数,不能通过运算符重载改变内置类型对象的含义,如: int operator+(int x, int y)
- 重载++运算符时,有前置++和后置++,运算符重载函数名都是operator++,⽆法很好的区分。C++规定,后置++重载时,增加⼀个int形参,跟前置++构成函数重载,⽅便区分。
- 重载<<和>>时,需要重载为全局函数,因为重载为成员函数,this指针默认抢占了第⼀个形参位置,第⼀个形参位置是左侧运算对象,调⽤时就变成了 对象<<cout,不符合使⽤习惯和可读性。重载为全局函数把ostream/istream放到第⼀个形参位置就可以了,第⼆个形参位置当类类型对象。
当运算符被⽤于类类型的对象时,C++语⾔允许我们通过运算符重载的形式指定新的含义。C++规定类类型对象使⽤运算符时,必须转换成调⽤对应运算符重载,若没有对应的运算符重载,则会编译报错。
如果⼀个重载运算符函数是成员函数,则它的第⼀个运算对象默认传给隐式的this指针,因此运算符重载作为成员函数时,参数⽐运算对象少⼀个。图1的参数是两个,在类外;图2参数是1个,在类中
重载++运算符时,有前置++和后置++,运算符重载函数名都是operator++,⽆法很好的区分。C++规定,后置++重载时,增加⼀个int形参,跟前置++构成函数重载,⽅便区分后置++中的参数无意义,只是为了告诉编译器这是后置++的重载函数
重载<<和>>时,需要重载为全局函数,因为重载为成员函数,this指针默认抢占了第⼀个形参位置,第⼀个形参位置是左侧运算对象,调⽤时就变成了 对象<<cout,不符合使⽤习惯和可读性。重载为全局函数把ostream/istream放到第⼀个形参位置就可以了,第⼆个形参位置当类类型对象。
日期类的实现
直接上代码:
cpp
//.h
#pragma once
#include<iostream>
#include<assert.h>
using namespace std;
class Date
{
public:
friend ostream& operator<<(ostream& out, const Date& d);
friend istream& operator>>(istream& in, Date& d);
Date(int year = 2000, int month = 1, int day = 1);
Date(const Date& d);
~Date();
Date* operator&()
{
return (Date*)0xFF23d233;
}
const Date* operator&()const
{
return (Date*)0xFF23d2EE;
}
int GetMonthDay(int year,int month)const
{
assert(month > 0 && month < 13);
static int monthDayArray[13] = {-1,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 monthDayArray[month];
}
bool operator<(const Date& d)const;
bool operator<=(const Date& d)const;
bool operator>(const Date& d)const;
bool operator>=(const Date& d)const;
bool operator==(const Date& d)const;
bool operator!=(const Date& d)const;
Date& operator+=(const int day);
Date operator+(int day)const;
Date& operator-=(int day);
Date operator-(int day)const;
Date& operator++();//前置
Date operator++(int day);
Date& operator--();//前置
Date operator--(int day);
int operator-(const Date& d)const;
Date& operator=(const Date& d);
bool checkDate()const
{
if (_month<1||_month>12||_day<1||_day>GetMonthDay(_year,_month))
{
return false;
}
else
{
return true;
}
}
void prin()const;
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& out, const Date& d);
istream& operator>>(istream& in, Date& d);
cpp
#include"Date.h"
void Date::prin()const
{
cout << _year << " " << _month << " " << _day << endl;
}
Date::Date(int year , int month , int day )
{
_year = year;
_month = month;
_day = day;
if (!checkDate())
{
cout << "日期非法,请输入正确的日期" << endl;
cout << *this<<endl;
}
}
Date::Date(const Date& d)
{
*this = d;
}
Date::~Date()
{
_year = 0;
_month = 0;
_day = 0;
}
bool Date::operator<(const Date& d)const
{
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 Date::operator<=(const Date& d)const
{
return *this < d || *this == d;
}
bool Date::operator>(const Date& d)const
{
return !(*this < d);
}
bool Date::operator>=(const Date& d)const
{
return !(*this < d) || *this == d;
}
bool Date::operator==(const Date& d)const
{
if (_year == d._year && _month == d._month && _day == d._day)
{
return true;
}
return false;
}
bool Date::operator!=(const Date& d)const
{
return !(*this == d);
}
Date& Date::operator+=(const 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 += 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;
}
Date& Date::operator-=(int day)
{
if (day<0)
{
return *this += -day;
}
_day -= day;
/*if (_day==0)
{
--_month;
_day = GetMonthDay(_year,_month);
return *this;
}*/
while (_day <=0)
{
/*_day += GetMonthDay(_year, _month);*/
--_month;
if (_month == 0)
{
--_year;
_month = 12;
}
_day += GetMonthDay(_year, _month);
}
return *this;
}
Date Date::operator-(int day)const
{
Date tmp = *this;
tmp -= day;
/*while (tmp._day <0 )
{
tmp._day += GetMonthDay(tmp._year, tmp._month);
--tmp._month;
if (tmp._month == 13)
{
--tmp._year;
tmp._month = 12;
}
}*/
return tmp;
}
Date& Date::operator++()//前置
{
*this += 1;
return *this;
}
Date Date::operator++(int day)//后置,形参day可写可不写,只会为了告诉编译器这是后置++重载
{
Date tmp = *this;
*this += 1;
return tmp;
}
Date& Date::operator--()//前置
{
*this -= 1;
return *this;
}
Date Date::operator--(int day)//后置
{
Date tmp = *this;
*this -= 1;
return tmp;
}
Date& Date::operator=(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
int Date::operator-(const Date& d)const//计算日期之间相差的天数
{
Date max = *this;
Date min = d;
int flag = 1;
if (*this<d)
{
max = d;
min = *this;
flag = -1;
}
int n = 0;
while (max!=min)
{
++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)
{
/*cout << "请输入年月日" << endl;
in >> d._year >> d._month >> d._day;*/
while (1)
{
cout << "请输入年月日" << endl;
in >> d._year >> d._month >> d._day;
if (d.checkDate())
{
break;
}
else
{
cout << "请出入正确的日期" << endl;
}
}
return in;
}
取地址运算重载
const成员函数
- 将const修饰的成员函数称之为const成员函数,const修饰成员函数放到成员函数参数列表的后⾯。
cpp
class A
{
public:
A(int a=1)
{
_a = a;
}
void Prin()const
{
cout << _a << endl;
}
private:
int _a;
};
int main()
{
A a1;
A a2(2);
a1.Prin();
a2.Prin();
return 0;
}
- const实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进⾏修改。const 修饰Date类的Print成员函数,Print隐含的this指针由 Date* const this 变为 const Date* const this
取地址运算符重载
取地址运算符重载分为普通取地址运算符重载和const取地址运算符重载,⼀般这两个函数编译器⾃动⽣成的就可以够我们⽤了,不需要去显⽰实现。除⾮⼀些很特殊的场景,⽐如我们不想让别⼈取到当前类对象的地址,就可以⾃⼰实现⼀份,胡乱返回⼀个地址。
cpp
class Date
{
public :
Date* operator&()
{
return this;
// return nullptr;不想让人得到地址,返回nullptr
}
const Date* operator&()const
{
return this;
// return nullptr;不想让人得到地址,返回nullptr
}
private :
int _year ; // 年
int _month ; // ⽉
int _day ; // ⽇
};