例如之前在学习栈用顺序表是现实会经常出现两个错误,一个是忘记初始化,另一个是我忘记释放内存造成内存泄漏;在C++中弥补了这一点,学完类的默认成员函数就可以解决大多数情况下这个问题
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,保证每个数据成员都有 一个合适的初始值,并且在对象的生命周期内只调用一次。
这里以日期类举例
class Data
{
public:
void SetDate(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Display()
{
cout <<_year<< "-" <<_month << "-"<< _day <<endl;
}
private:
int _day;
int _month;
int _year;
};
int main()
{
Date d1;
d1.SetDate(2018,5,1);
d1.Display();
return 0;
}
上面代码中,可以用 SetDate 初始化,但是如果每次创建对象都需要调用该函数就会有些麻烦,所以在C++中就有了构造函数来替换这个工作
构造函数的特性
构造函数是特殊的成员函数,需要注意的是,构造函数的虽然名称叫构造,但是需要注意的是构造函数的主要任务并不是开空间创建对象,而是初始化对象。
代码示例如下
class Data
{
public:
构造函数的形式1
Data()
{
}
构造函数的形式2
Data()
{
_year = 1;
_month = 1;
_day = 1;
}
构造函数的形式3
Data(int year,int month,int day)
{
_day = day;
_month = month;
_year = year;
}
构造函数的形式4
Data(int year = 1,int month = 1,int day = 1)
{
_day = day;
_month = month;
_year = year;
}
void Display()
{
cout <<_year<< "-" <<_month << "-"<< _day <<endl;
}
private:
int _day;
int _month;
int _year;
};
注意:上面三种之许创建一种,这里只是示范不同的情况该如何调用
调用如下:
int main()
{
Date d1; 型式1, 实例化时自动调用,但是没有对成员进行改变
Date d2; 型式2 对成员赋默认值
Date d3(1,1,1); 型式3 对成员传参赋值
Date d4(1,1,1); 型式4 全缺省函数构造 传参时,参数对成员赋值
Date d4; 型式4 全缺省函数构造 不传参时,形参数默认值对成员赋值
return 0;
}
上面代码提供了4个形式,但最建议用全缺省的形式
代码验证如下:
class A
{
public :
A()
{
cout << "A()" << endl;
_a = 0;
}
private:
int _a;
};
class Data
{
public:
Data(int year = 1,int month = 1,int day = 1)
{
//this->_day = day
_day = day;
_month = month;
_year = year;
}
void Display()
{
cout <<_year<< "-" <<_month << "-"<< _day <<endl;
}
private:
int _day;
int _month;
int _year;
A temp;
};
int main()
{
Data d1;
return 0;
}
A(int a = 10)
{
cout << "A()" << endl;
_a = a;
}
任何一个类都有三种默认构造函数:1.全缺省,2.无参,3.编译器默认生成, 对于自定义类型,如果没有无参的构造函数就会报错
析构函数:与构造函数功能相反,析构函数不是完成对象的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成类的一些资源清理工作。
析构函数的特性:
代码示例如下
class Data
{
public:
Data(int year = 1,int month = 1,int day = 1)
{
_day = day;
_month = month;
_year = year;
}
~Data()
{
cout << "~Data()" << endl;
}
private:
int _day;
int _month;
int _year;
A temp;
};
int main()
{
Data d1;
return 0;
}
由输出结果可知,析构函数也是自动调用的
析构函数一般用于资源的清理,例如动态申请的内存,结束之前就需要释放,就可以写到析构函数里
例如对栈的初始化和释放函数接口进行改写,代码如下:
class Stack
{
public:
构造函数
Stack(int capacity = 4)
{
_arr = (int*)malloc(sizeof(int)*capacity);
if(_arr == NULL)
{
perror("realloc");
exit(-1);
}
_capacity = capacity;
_top = 0;
}
析构函数
~Stack()
{
free(_arr);
_arr = NULL;
}
private:
int* _arr;
int _top;
int _capacity;
};
5.和构造函数一样,编译器生成的默认析构函数,对会自定类型成员调用它的析构函数。
如果我们不写析构函数,他会自动生成并调用自己的析构函数
(1)对于内置类型,但是是空处理也可以说不做任何处理
(2)对于自定义类型会去调用他的析构函数(可能是默认的也可能是自己传创建的)
对于自定义类型,如果他的成员变量都是在栈帧里的,那么就可以使用默认生成的析构函数,若成员变量对动态申请内存,那就必须要自己创建析构函数
构造函数的顺序是按实例化顺序来的,而析构函数的顺序就是逆序的,最后构造的最先析构,且构造函数是全局在前局部在后,析构时是全局在后局部在前(可以看成栈)
构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用
拷贝构造的特征:
验证如下
传值时,形参是实参的临时拷贝,形参可以看成是个临时变量,是会占用同样大小的内存的
传引用就是对实参取别名,形参就是实参的别名,并且之前对引用的介绍是:在语法层,引用不会去新申请空间(不占用空间);所以引用只是实参临时多了个别名,不会占用空间,可直接操作到实参本身,就不存在传值传参的拷贝死循环了
所以正确的写法应该是:
class Data
{
public:
Data(int year = 1,int month = 1,int day = 1)
{
_day = day;
_month = month;
_year = year;
cout << "Data(int year = 1,int month = 1,int day = 1)" << endl;
}
拷贝构造
建议加上const
Data(const Data& d) 一定是传引用,传值会造成无限递归
{
//this->_year = d._year
_year = d._year;
_month = d._month;
_day = d._day;
}
~Data()
{
cout << "~Data()" << endl;
}
private:
int _day;
int _month;
int _year;
};
int main()
{
Data d1;
Data d2(d1);
return 0;
}
代码验证如下
当成员都是普通内置类型时(浅拷贝)
class Data
{
public:
Data(int year = 1,int month = 1,int day = 1)
{
_day = day;
_month = month;
_year = year;
cout << "Data(int year = 1,int month = 1,int day = 1)" << endl;
}
~Data()
{
cout << "~Data()" << endl;
}
private:
int _day;
int _month;
int _year;
};
int main()
{
Data d1;
Data d2(d1);
return 0;
}
但是当成员申请内存资源时就会出错(例如动态内存,深拷贝)
这里用原先学习的栈做示例(这里只是作为演示,有些接口函数没有写如push、pop等)
class Stack
{
public:
Stack(int capacity = 4)
{
_arr = (int*)malloc(sizeof(int)*capacity);
if(_arr == NULL)
{
perror("realloc");
exit(-1);
}
_capacity = capacity;
_top = 0;
}
~Stack()
{
free(_arr);
_arr = NULL;
}
private:
int* _arr;
int _top;
int _capacity;
};
int main()
{
Stack s1;
Stack s2(s1);
return 0;
}
请注意看,s2拷贝连同动态内存一起拷贝了,s1_arr和 s2_arr的指针都指向这同一块地址,那么可能就会造成以下几个问题
1.s1改变动态内存的数值,s2里就会被改变,相反也是如此
2.例如扩容时是top等于capacity,假如s1或s2中有一个扩容说明他的capacity和top相等了 ,但另一个的capacity和top并没有同步但被扩容了,可能会发生错误
3.在程序结束时,他们的析构函数会对这个动态内存释放两次,就会出错
这里还用上面定义的日期类来演示
class Data
{
public:
Data(int year = 1,int month = 1,int day = 1)
{
_day = day;
_month = month;
_year = year;
cout << "Data(int year = 1,int month = 1,int day = 1)" << endl;
}
private:
int _day;
int _month;
int _year;
};
int main()
{
Data d1;
Data d2(d1);
return 0;
}
在默认情况下,C++是不支持自定义类型对象使用运算符的,但C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)
运算符重载代码示例如下
d1 < d2
bool operator>(const Data& d1,const Data& d2)
{
if(d1._year > d2._year )
return true;
else if(d1._year == d2._year && d1._month > d2._month )
return true;
else if(d1._year == d2._year && d1._month == d2._month && d1._day > d2._day)
return true;
else
false;
}
上面这种情况虽然是对的,但是比较麻烦,上面介绍类中存在一个this指针,那么就可以写成下面这种型式
//bool operator>(Data* const this,const Data& d)
bool operator>(const Data& 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;
else
return false;
}
那么该如何使用呢?
开头介绍中,运算符重载是具有特殊函数名的函数,那么就可以把他放到类体中,当作成员函数一样
class Data
{
public:
Data(int year = 1,int month = 1,int day = 1)
{
_day = day;
_month = month;
_year = year;
cout << "Data(int year = 1,int month = 1,int day = 1)" << endl;
}
bool operator>(const Data& 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;
else
return false;
}
private:
int _day;
int _month;
int _year;
};
int main()
{
Data d1;
Data d2(d1);
cout << (d1 < d2) << endl;
return 0;
}
int main()
{
Data d1(2,1,1);
Data d2(1,1,1);
if(d1 > d2) //也可以写成这样d1.operator>(d2)
{
cout << "hello" << endl;
cout << (d1 > d2) << endl;
}
return 0;
}
赋值运算符的定义型式和调用与运算符重载类似
赋值运算符主要有四点需要注意:
Data d3;
d3 = d1;
void operator=(const Data& d)
{
d._year = _year;
d._month = _month;
d._day = _day;
}
以上就是基本的赋值运算符重载,但是在逻辑上还有些问题
int i,j,k;
i = j = k = 10;
10先给k赋值,k当右操作数给j赋值,j当右操作数给i赋值,所以当d3 = d2 = d1,d2应该变为右操作数 所以:
Data d3;
d3 = d2 = d1;
//d2 = d1
//d3 = d2
Data& operator=(const Data& d)
{
if(this == &d) 防止自己给自己赋值
return *this;
d._year = _year;
d._month = _month;
d._day = _day;
return *this;
}
先在operator=中给d2赋值,在返回d2的别名,d2的别名当做右值传参给d3赋值
形参传引用:
因为传值会生成临时拷贝,并且形参还会占用内存,而传引用就是对实参取别名,不会占用空间;对于基础类型差别不大,但是对于类(结构体)他们的大小不确定,传值既占用空间还浪费时间
返回值传引用:
假如是传返回值,那么会有两次拷贝构造,第一次是d1赋值给d2时的栈帧销毁, *this给临时变量时的拷贝构造,第二次是d2赋值给d3时的栈帧销毁,*this给临时变量拷贝构造。而引用返回,栈帧销毁 this还存在,返回的是this的别名,不会占用空间,不存在拷贝构造
所以相比传值,无论是传参还是传返回值,传引用都比传参效率高,传引用不会额外占用空间,也节省了拷贝时间,所以C++提倡是传引用的(返回值传引用前提是出了域返回值还存在)
赋值重载与拷贝构造的区别:
Data d1;
Data d2(d1); 拷贝构造是在即将实例化时使用的
Data d3 = d2;
Data d3;
d3 = d2 = d1; 赋值重载是对已经存在的类使用的
赋值运算符:
所以和拷贝构造类似,只有出现动态内存等才需要我们自己实现(深拷贝)
将const修饰的类成员函数称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
请看代码验证:
class Data
{
public:
Data(int year = 1,int month = 1,int day = 1)
{
_day = day;
_month = month;
_year = year;
cout << "Data(int year = 1,int month = 1,int day = 1)" << endl;
}
void print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _day;
int _month;
int _year;
};
int main()
{
Data d1;
d1.print();
const Data d2;
d2.print;
return 0;
}
在类的实例化对象前面加上const,代表 const Data* const this
而调用成员函数的参数是:Data* const this,成员函数的权限是可读可写的(权限放大)
针对这种情况,C++的解决办法是
void print()const
{
cout << _year << "-" << _month << "-" << _day << endl;
}
在成员函数后面 加上const 等于 const Data const this*
加了const后,普通对象也可以调用(权限缩小),const修饰的对象也可以调用,所以对于不需要修改值的成员函数建议都加上const
一般的运算符号,操作符都需要我们自己写,但是取地址一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容
int main()
{
Data d1;
cout << &d1 << endl;
return 0;
}
不想让别人获取地址时
const Data* operator&() const
{
return this;
}
写一个日期类,并实现判断日期大小、赋值、比较等
#ifndef DATA_H
#define DATA_H
#ifndef DATA_H
#define DATA_H
#include
using namespace std;
class Data
{
public:
Data& operator=(const Data& d);
bool operator==(const Data& d);
bool operator!=(const Data& d);
bool operator>(const Data& d);
bool operator>=(const Data& d);
bool operator<(const Data& d);
bool operator<=(const Data& d);
Data& operator+=(int day);
Data operator+(int day);
Data& operator++();
Data operator++(int);
Data& operator-=(int day);
Data operator-(int day);
Data operator--(int); //后置
Data& operator--(); //前置
对象 减 对象获取相差天数
int operator-(const Data& d);
Data(int year = 1,int month = 1,int day = 1);
Data(const Data& d);
打印
void print();
获取月份天数
int GetMonthDay(int year,int month);
获取星期
void PrintWeekDay();
private:
int _year;
int _month;
int _day;
};
#endif DATA_H
int Data::GetMonthDay(int year,int month)
{
static int monthDayarr[13] = {0,31,28,31,30,31,30,31,31,30,31,30,31};
int day = monthDayarr [month];
if( month == 2 && ((year % 4 == 0 && year % 100 != 0 ) || year % 400 == 0)) 或的括号不能省
day++;
return day;
}
Data::Data(int year,int month,int day) 缺省值定义和声明只能出现一次
{
_year = year;
_month = month;
_day = day;
if( !(year > 0 && (month > 0 && month <13) && (day > 0 && day < GetMonthDay(year,month) ) ))
{
cout << "日期非法" << endl;
}
}
Data::Data(const Data& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
1. = 、== 、!=
Data& Data::operator=(const Data& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
bool Data::operator==(const Data& d)
{
if((_year == d._year) && (_month == d._month) && (_day == d._day))
return true;
else
return false;
}
bool Data::operator!=(const Data& d)
{
if(!(*this == d))
return true;
else
return false;
}
2. >、 >=
bool Data::operator>(const Data& 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;
else
return false;
}
bool Data::operator>=(const Data& d)
{
if( (*this > d) || (*this == d))
return true;
else
return false;
}
3. < 、<=
bool Data::operator<(const Data& d)
{
if(!(*this >= d))
return true;
else
return false;
}
bool Data::operator<=(const Data& d)
{
if(!( *this > d))
return true;
else
return false;
}
5.+、+=、++前置、后置++
Data& Data::operator+=(int day)
{
_day += day;
while(_day > GetMonthDay(_year,_month))
{
_day -= GetMonthDay(_year,_month);
_month++;
if(_month == 13)
{
_month = 1;
_year++;
}
}
return *this;
}
Data Data::operator+(int day)
{
Data temp = *this;
temp += day;
return temp;
}
Data& Data::operator++()
{
*this += 1;
return *this;
}
Data Data::operator++(int)
{
Data temp = *this;
*this += 1;
return temp;
}
6.-、-=、前置–、–后置
Data& Data::operator-=(int day)
{
if(day < 0)
{
*this += -day;
}
_day -= day;
while(_day <= 0)
{
_month--;
if(_month == 0)
{
_month = 12;
_year--;
}
_day += GetMonthDay(_year,_month);
}
return *this;
}
Data Data::operator-(int day)
{
Data temp = *this;
temp -= day;
return temp;
}
Data& Data::operator--()
{
*this -= 1;
return *this;
}
Data Data::operator--(int)
{
Data temp = *this;
*this -= 1;
return temp;
}
7.获取当前日期的星期
int Data::operator-(const Data& d)
{
Data max = *this;
Data min = d;
int day = 0;
if(*this < d)
{
max = d;
min = *this;
}
while(max != min)
{
day++;
++min; 尽量用前置
}
return day;
}
void Data::PrintWeekDay()
{
static char* week[] = {"星期一","星期二","星期三","星期四","星期五","星期六","星期日"};
Data temp(1900,1,1);
int Getweek = *this - temp;
cout << week[Getweek%7] << endl;
}
8.流插入和流提取
这里先介绍格式,但涉及到友元,请看下一篇
class Data
{
public:
friend ostream& operator<<(ostream& out,const Data& d);
friend istream& operator>>(istream& in,Data& d);
..........
..........
..........
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& out,const Data& d)
{
cout << d._year << "-" << d._month << "-" << d._day << endl;
return out;
}
istream& operator>>(istream& in,Data& d)
{
in >> d._year >> d._month >> d._day ;
return in;
}
#ifndef DATA_H
#define DATA_H
#include
using namespace std;
class Data
{
public:
friend ostream& operator<<(ostream& out,const Data& d);
friend istream& operator>>(istream& in,Data& d);
Data(int year = 1,int month = 1,int day = 1);
Data(const Data& d);
//void print();
int GetMonthDay(int year,int month);
Data& operator=(const Data& d);
bool operator==(const Data& d);
bool operator!=(const Data& d);
bool operator>(const Data& d);
bool operator>=(const Data& d);
bool operator<(const Data& d);
bool operator<=(const Data& d);
Data& operator+=(int day);
Data operator+(int day);
Data& operator++();
Data operator++(int i);
Data& operator-=(int day);
Data operator-(int day);
Data operator--(int i); //后置
Data& operator--(); //前置
int operator-(const Data& d);
void PrintWeekDay();
private:
int _year;
int _month;
int _day;
};
#endif DATA_H
容易遇到的错误:
缺省值在声明和定义中都出现;
1.++ 、-- 的前置后置;
2.+=、-= 和 + 、 - 的混淆;
3.传参传值 和 反回值在调用结束后不销毁的情况下传值;
4.逻辑操作省括号;
刚开始学C++时,面对传参和返回值需要多使用什么(如指针、引用、值)
对于类的运算符重载中,后置++和 - - 要尽量少用,因为他会临时实例化一个对象,并在返回时返回的是这个临时对象的值,相对于前置操作,会先调用构造函数,返回时又会调用拷贝构造放到临时变量中
class A
{
public:
A()
{
//cout << "A()" << endl;
//count++;
}
A(A& a)
{
cout << "A(A& a)" << endl;
count++;
}
static int get_count()
{
return count;
}
private:
int a;
static int count;
};
int A::count = 0;
A f(A u)
{
A b1(u);
A b2 = b1;
return b2;
}
int main()
{
A a1;
A a2 = f(f(a1));
cout << A::get_count() <<endl;
return 0;
}