C++学习之类和对象_2
1. 类的六个默认成员函数
类中有六个默认成员函数,分别为:
构造函数
析构函数
拷贝构造函数
赋值重载
普通对象取地址重载
const对象取地址重载
其中,构造与析构函数主要是用于完成初始化和清理工作;拷贝构造是通过同类对象来创建一个新的对象;赋值重载主要是用于将一个对象赋值给另一个对象;取地址重载主要是普通与const对象取地址,一般很少自己实现。
默认成员函数中,默认的意思是,我们不进行显式定义,那么系统会自动生成及调用这六个函数。
2. 构造函数
构造函数用于初始化对象,而非创建对象。
下面是一个日期类:
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 = 0;int _month = 0;int _day = 0;};
public下面第一个没有返回值,与类同名的函数就是构造函数。
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有一个合适的初始值,并且在对象整个生命周期内只调用一次。
构造函数有如下特征:
- 函数名与类名相同。
- 无返回值。
- 对象实例化时编译器自动调用对应的构造函数。
- 构造函数可以重载。
构造函数重载如下:
date(int year = 1, int month = 1, int day = 1) //构造函数{_year = year;_month = month;_day = day;}date(){}
调用时
date d1(2000,1,1); //调用第一个构造函数
date d2; //调用第二个构造函数,注意:这里不能加括号,不然就成了函数声明!
date d3(); //这样会构成歧义,到底是函数声明还是调用第一个构造函数?
如果我们在类中不显式定义构造函数,编译器会自动生成一个无参的默认构造函数,一旦显式定义,编译器将不再生成。
对于内置类型,若没有缺省,自动调用编译器生成的构造函数会给对象赋予随机值,所以我们可以在成员变量定义时进行初始化:
private:int _year = 2000;int _month = 1;int _day = 1;
无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。
注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数。
class Date
{
public:Date(){_year = 1900;_month = 1;_day = 1;cout << "第一个" << endl;}Date(int year = 1900, int month = 1, int day = 1){_year = year;_month = month;_day = day;cout << "第二个" << endl;}
private:int _year;int _month;int _day;
};
// 以下测试函数能通过编译吗?
void Test()
{Date d1; //调用不明确
}int main()
{Test();return 0;
}
上面的代码无法通过编译,因为对重载构造函数的调用不明确。
3. 析构函数
析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
析构函数是特殊的成员函数,其特征如下:
- 析构函数名是在类名前加上字符 ~。
- 无参数无返回值类型。
- 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构 函数不能重载
- 对象生命周期结束时,C++编译系统系统自动调用析构函数。
因为date类的成员变量都是内置类型,且都存储在栈中,故不需要自己写析构函数也能完成自动清理,即使写了也不需要进行什么操作。
class date
{
public:date(int year = 1, int month = 1, int day = 1) //构造函数{_year = year;_month = month;_day = day;}~date() //析构函数,无需操作{}private:int _year = 0;int _month = 0;int _day = 0;};
而对于栈类,在创建栈对象时,会在堆区申请空间,当销毁栈时,需要手动清理:
typedef int DataType;
class Stack
{
public:Stack(size_t capacity = 3){_array = (DataType*)malloc(sizeof(DataType) * capacity);if (NULL == _array){perror("malloc申请空间失败!!!");return;}_capacity = capacity;_size = 0;}void Push(DataType data){// CheckCapacity();_array[_size] = data;_size++;}// 其他方法...~Stack(){if (_array){free(_array);_array = NULL;_capacity = 0;_size = 0;}}
private:DataType* _array;int _capacity;int _size;
};void TestStack()
{Stack s;s.Push(1);s.Push(2);
}
有这么一种情况,一个类内其成员变量包括其它类的对象,如果创建这个类的对象,之后通过析构函数进行销毁,那么在销毁时,其中其他类的对象该怎么销毁呢?
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的对象,但date类的对象d会被编译器生成的析构函数销毁,其中,三个内置类型被销毁时不需要资源清理,最后系统直接将其内存回收即可,而time类的_t则会被编译器调用其本身类的析构函数所销毁。
按照这个逻辑,我们之前在使用两个栈来实现队列的OJ题中,就可以在队列类内声明两个栈类成员变量,当创建并销毁队列类的对象时,就会调用到栈类内的析构函数来销毁队列类内的两个栈类的成员变量。
总结一下:
- 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如Date类;有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类;
- 析构函数的调用会追根溯源;
- 注意:创建某类的三个对象d1,d2,d3,在销毁时顺序为d3,d2,d1,类似于栈的push和pop。
4. 拷贝构造函数
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
拷贝构造函数也是特殊的成员函数,其特征如下:
- 拷贝构造函数是构造函数的一个重载形式。
- 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。
class date
{
public:date(int year = 1, int month = 1, int day = 1) //构造函数{_year = year;_month = month;_day = day;}date(const date& d)// 错误写法:date(date d) //拷贝构造函数,用引用或指针,最好用const修饰,防止对象d被错误赋值// 如果不引用就会造成无限递归,date d本身就是调用构造函数// 因为创建对象副本是要调用自身(拷贝构造函数)才能创建的,所以就会造成无限递归// 如果没有动态内存申请空间,可以不用自己写拷贝构造函数{this->_year = d._year; //year都是对象的year,只是左边的和右边的对象不同,但肯定不是类的year,类的是声明this->_month = d._month;this->_day = d._day;}void print() //简单的函数放在声明里,作为内联函数{cout << _year << "-" << _month << "-" << _day << endl;}private:int _year = 0;int _month = 0;int _day = 0;};
值拷贝(浅拷贝):
若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
注意:在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的。
试想,如果用编译器自动生成的拷贝构造函数去构造一个包含需要申请动态内存空间的成员变量的类对象时,那么这个新的对象的自定义成员变量的内存空间的指向是否就会与被复制的对象的自定义成员变量的内存空间的指向一致?
所以,当遇到成员变量需要申请动态内存空间的类时,必须自己写拷贝构造函数,完成深拷贝(以后再说,如果有兴趣则可以主动去查查)。
拷贝构造函数典型调用场景:
- 使用已存在对象创建新对象
- 函数参数类型为类类型对象
- 函数返回值类型为类类型对象
5. 赋值运算符重载
运算符重载:
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其
返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)
注意:
不能通过连接其他符号来创建新的操作符:比如operator@
重载操作符必须有一个类类型参数
用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义
作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐
藏的this
( .* ) ( :: ) ( sizeof ) ( ?: ) ( . ) 注意这5个运算符不能重载。这个经常在笔试选择题中出
现。
bool operator>(const date& d) const//这个const修饰this指针指向的对象
{ if (this->_year > d._year) //这里的this是指对象调用该函数时,对象的地址;而形参则是运算符需要的第二个参数{return true;}else if (this->_year == d._year && this->_month > d._month){return true;}else if (this->_year == d._year && this->_month == d._month && this->_day > d._day){return true;}return false;
}bool operator<(const date& d) const
{if (this->_year < d._year) //这里的this是指对象调用该函数时,对象的地址;而形参则是运算符需要的第二个参数{return true;}else if (this->_year == d._year && this->_month < d._month){return true;}else if (this->_year == d._year && this->_month == d._month && this->_day < d._day){return true;}return false;
}bool operator==(const date& d) const
{return this->_year == d._year && this->_month == d._month && this->_day == d._day;
}
赋值运算符重载
赋值运算符重载格式 :
参数类型:const T&,传递引用可以提高传参效率
返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
检测是否自己给自己赋值
返回*this :要复合连续赋值的含义
//date operator=(const date& d) //可以运行,但代价太大,每次都要拷贝,所以要用引用返回
date& operator=(const date& d) //赋值运算符重载函数
{//cout << "operator=" << endl;if (this != &d) // 如果自己给自己赋值,那么就不执行{this->_year = d._year; //一般不加this->this->_month = d._month;this->_day = d._day;}return *this;
}
赋值运算符只能重载成类的成员函数不能重载成全局函数
如果重载成全局函数,那么就没有this指针,需要两个参数才行,这样就没办法实现类似于内置类型的赋值效果。
// 怎么用? d1 = oeprator=(d1, d2),很别扭
Date& operator=(Date& left, const Date& right)
{if (&left != &right){left._year = right._year;left._month = right._month;left._day = right._day;}return left;
}
// 编译失败:
// error C2801: “operator =”必须是非静态成员
为何会编译失败?
如果赋值运算符如果不在类内显式定义,那么编译器就会在类内默认生成,此时用户若在外面进行赋值运算符重载,那么就会发生冲突,所以赋值运算符重载只能是成员函数。
用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。
当然,编译器默认生成的赋值运算符重载只是完成了值拷贝,同拷贝构造一样,若需要完成深拷贝,那么就得自己定义。
前置++与后置++
date& operator+=(int day)
{if (day < 0){return *this -= -day;}_day += day;while (_day > get_month_day(_year, _month)){_day -= get_month_day(_year, _month);_month++;if (_month == 13){_year++;_month = 1;}}return *this;//*this = *this + day;//return *this;
}date& operator++() //前置++
{*this += 1;return *this;
}date operator++(int) //后置++,形参接收不接收都没有意义,所以不写具体形参上去
{date temp = *this;*this += 1;return temp;
}
6. const成员函数
将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数
隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
bool operator!=(const date& d) const //const date* this
{return !(*this == d);
}
以下有四个问题:
- const对象可以调用非const成员函数吗?
不可以,因为涉及到权限放大,const对象不能赋值给this指针。 - 非const对象可以调用const成员函数吗?
可以,权限缩小。 - const成员函数内可以调用其它的非const成员函数吗?
不可以,const成员函数的this指针与非const成员函数的this指针的类型不同!
const date* const this != date* const this; - 非const成员函数内可以调用其它的const成员函数吗?
可以,权限缩小。
7. 取地址及const取地址操作符重载
这两个取地址操作符重载一般不用自己定义,让编译器默认生成即可。
class Date
{
public :Date* operator&(){return this ;}const Date* operator&()const{return this ;}
private :int _year ; // 年int _month ; // 月int _day ; // 日
};
总结:本文讲述了6大默认成员函数,重点在构造、析构、拷贝构造及赋值运算符重载这四个函数上面,内容较多,如有错误,请批评指正,谢谢!