类的六个默认成员函数
默认成员函数的概念
我们看这样一个类:
class ClassName {
};
这个类里面没有成员函数,也没有成员变量,那么他是不是什么都没有呢?是一个空类吗?答案是并不是,当你创建一个类的时候,就算你什么都不写,编译器会自动帮你创建六个默认的成员函数,称为默认成员函数,当需要用到时可以对其进行覆盖重写。
是哪六个成员函数呢?它们的作用是什么?
- 构造函数。初始化对象。
- 析构函数。清理工作,主要完成资源释放等。
- 拷贝构造。使用同类对象初始化创建对象。
- 运算符重载。主要是把一个对象赋值给另一个对象
- 取地址重载。取当前对象的地址。
- const对象取地址重载。取const对象的地址。
构造函数
构造函数名字和类名相同,创建对象的时候自动调用构造函数,完成初始化工作,并且只调用一次。
class Stack {
public:
//普通成员函数
void Init(int n=4) {
a = (int*)malloc(sizeof(int) * n);
if (a == NULL) {
perror("malloc fail");
exit(-1);
}
capacity = n;
size = 0;
}
private:
int* a;
int size;
int capacity;
};
int main() {
Stack s1;
s1.Init();
return 0;
}
上面这个栈类,初始化的时候需要调用Init()
这个函数进行初始化,这个是完全没有必要的,这样的工作可以交给构造函数,例如。
class Stack {
public:
//构造函数
Stack(int n = 4) {
a = (int*)malloc(sizeof(int) * n);
if (a == NULL) {
perror("malloc fail");
exit(-1);
}
capacity = n;
size = 0;
}
private:
int* a;
int size;
int capacity;
};
int main() {
Stack s1;//无参构造调用
Stack s2(10);//有参构造调用
return 0;
}
创建类时构造函数会自动完成初始化,但是我们不创建构造函数时,系统会自动帮我们创建一个,但是成员变量也没有初始化,那么系统自动创建的构造函数是不是没用呢?其实也不是,系统默认的构造函数会自动初始化自定义类型(class,union,struct),对于系统内置类型不做处理,这其实是C++一个历史遗留的问题,也是设计的不好的地方之一。
class Stack {
public:
Stack(int n = 4) {
a = (int*)malloc(sizeof(int) * n);
if (a == NULL) {
perror("malloc fail");
exit(-1);
}
capacity = n;
size = 0;
}
private:
int* a;
int size;
int capacity;
};
class Queue {
public:
private:
Stack s1;
int a;
};
Queue这个类有两个成员函数,当这个类被实例化一个对象时,系统默认的构造函数会被调用,s1这个自定义成员变量会被调用Stack构造函数初始化,但是a这个成员变量系统不会处理,因为它是内置类型,有的编译器会初始化为0,但C++标准里面没有规定必须初始化,所以不能依赖编译器,这个问题C++11之后有一个对应的处理。
class Queue {
public:
private:
Stack s1;
int a=0;//在声明时给成员变量一个缺省值,如果没有初始化就会被赋值缺省值。
};
析构函数
当创建的一个对象出作用域时,会自动调用析构函数,以便于完成资源的集中清理。析构函数名是类名前面加~
。
class Stack {
public:
Stack(int n = 4) {
a = (int*)malloc(sizeof(int) * n);
if (a == NULL) {
perror("malloc fail");
exit(-1);
}
capacity = n;
size = 0;
}
void Destroy() {
free(a);
capacity = size = 0;
}
private:
int* a;
int size;
int capacity;
};
int main() {
Stack s1;
//....................
s1.Destroy();
return 0;
}
创建栈类我们需要用malloc开辟空间,最后对象销毁之前我们还需要调用成员函数进行销毁,析构函数帮我们解决了这个问题。
class Stack {
public:
Stack(int n = 4) {
a = (int*)malloc(sizeof(int) * n);
if (a == NULL) {
perror("malloc fail");
exit(-1);
}
capacity = n;
size = 0;
}
~Stack(){
free(a);
capacity = size = 0;
}
private:
int* a;
int size;
int capacity;
};
int main() {
Stack s1;//无参构造调用
Stack s2(10);//有参构造调用
return 0;
}
析构函数一般是malloc之后需要单独释放空间需要重载析构,其他情况基本不需要,析构函数只能重写一次。
拷贝构造
拷贝构造说的通俗一点就是用另一个相同类型的对象给自己赋值。完成一个浅拷贝。
class Date {
public:
Date(int year, int month, int day) {
_year = year;
_month = month;
_day = day;
}
void show() {
std::cout << _year << "年" << _month << "月" << _day << "日" << std::endl;
}
private:
int _year;
int _month;
int _day;
};
int main() {
Date d1(2012, 1, 1);
Date d2(d1);
d1.show();
d2.show();
return 0;
}
d2调用了系统默认生成的拷贝构造完成了对象的初始化,那么系统默认的拷贝构造是怎么实现的呢?
class Date {
public:
Date(int year, int month, int day) {
_year = year;
_month = month;
_day = day;
}
Date(const Date& d) {
_year = d._year;
_month = d._month;
_day = d._day;
}
void show() {
std::cout << _year << "年" << _month << "月" << _day << "日" << std::endl;
}
private:
int _year;
int _month;
int _day;
};
拷贝构造名字和类名相同,参数是引用类型,不能进行普通传值,否则会无限递归,因为对象只要有拷贝行为,就会自动调用拷贝构造。
使用值传递,形参是实参的拷贝,只要拷贝就会自动调用拷贝构造,只有引用传参才可以。
什么是深拷贝,什么是浅拷贝
上面我提到了系统默认生辰的拷贝构造会实现浅拷贝,就是按照成员变量的字节数去拷贝到另一个对象,如果成员变量有指针指向数组空间就会出错。
class Stack {
public:
Stack(int n = 4) {
a = (int*)malloc(sizeof(int) * n);
if (a == NULL) {
perror("malloc fail");
exit(-1);
}
capacity = n;
size = 0;
}
~Stack(){
free(a);
capacity = size = 0;
}
private:
int* a;
int size;
int capacity;
};
栈这个类就必须实现深拷贝,浅拷贝会出错。假如给s2插入数据就会把s1的数据覆盖,s1调用析构函数,释放空间,s2不能再释放。
至于深拷贝,我会给大家一个后续的分享,这里不做过多介绍,拷贝构造一般情况之下只有需要深拷贝才会去重写实现,浅拷贝系统默认的拷贝构造可以完成。
运算符重载
赋值运算符重载
我们用普通内置类型时,可以用赋值运算符对其他变量进行赋值
int main() {
int a = 10;
int b = a;//使用赋值运算符赋值。
return 0;
}
那么内置类型可以这样做,自定义类型是否可以呢?答案是可以的
这个其实是类里面有一个自动生成的成员函数(赋值运算符),帮我们做了这个事情,到底怎么写的我给大家演示。
class Date {
public:
Date(int year, int month, int day) {
_year = year;
_month = month;
_day = day;
}
Date(const Date& d) {
_year = d._year;
_month = d._month;
_day = d._day;
}
//返回值是一个Date& 要符合连续赋值的原理,
Date& operator=(const Date& d) {
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
void show() {
std::cout << _year << "年" << _month << "月" << _day << "日" << std::endl;
}
private:
int _year;
int _month;
int _day;
};
这就是类里面六个默认的成员函数之一,operator是运算符重载的关键字。默认生成的赋值重载函数和拷贝构造是一样的,按成员变量的字节数拷贝,只能完成浅拷贝,深拷贝还需要自己进行覆盖重写。
其他运算符重载
既然赋值运算符可以重写,那么其他的运算符是否也可以呢,大部分是可以的。例如:
//左操作数是*this,右操作数是d (*this==d)
bool Date::operator==(const Date& d) {
return _year == d._year && _month == d._month && _day == d._day;
}
bool Date::operator!=(const Date& d) {
return _year != d._year || _month != d._month || _day != d._day;
}
//d1<d2
bool Date::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;
}
else {
return false;
}
}
bool Date::operator<=(const Date& d) {
return *this < d || *this == d;
}
bool Date::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;
}
else {
return false;
}
}
bool Date::operator>=(const Date& d) {
return *this > d || *this == d;
}
Date& Date::operator+=(int day) {
_day += day;
while (_day > GetMonthDay(_year, _month)) {
_day -= GetMonthDay(_year, _month);
_month++;
if (_month > 12) {
_year++;
_month = 1;
}
}
return *this;
}
Date Date::operator+(int day) {
Date tem(*this);
return tem += day;
}
int Date::operator-(const Date& d) {
Date max = *this;
Date min = d;
int flag = 1;
if (*this < d) {
max = d;
min = *this;
flag = -1;
}
int n = 0;
while (min != max) {
++min;
++n;
}
return n * flag;
}
Date& Date::operator-=(int day) {
if (day < 0) {
*this += -day;
return *this;
}
_day -= day;
while (_day <= 0) {
--_month;
if (_month == 0) {
_year--;
_month = 12;
}
_day += GetMonthDay(_year, _month);
}
return *this;
}
Date Date::operator-(int day) {
Date tem(*this);
tem -= day;
return tem;
}
//前置++
Date& Date::operator++() {
*this += 1;
return *this;
}
//后置++,形参需要用一个int占位,这是C++规定
Date Date::operator++(int) {
Date tem(*this);
*this += 1;
return tem;
}
这就是Date类实现的运算符重载,但是也不所有运算符都可以重载
- 不能通过连接其他符号来创建新的操作符:比如operator@
- 重载操作符必须有一个类类型参数
- 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义
- 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐
藏的this * :: sizeof ?: .
注意以上5个运算符不能重载。这个经常在笔试选择题中出
现。
取地址操作符重载
class Date
{
public :
Date* operator&()
{
return this ;
}
private :
int _year ; // 年
int _month ; // 月
int _day ; // 日
};
int main(){
Date d1;
cout<<&d1<<endl;
return 0;
}
这个操作符也是类里面自动生成的,了解即可,工作时很少对这个运算符重载进行重写。
const取地址操作符重载
class Date
{
public :
const Date* operator&()const
{
return this ;
}
private :
int _year ; // 年
int _month ; // 月
int _day ; // 日
};
int main(){
const Date d1;
cout<<&d1<<endl;
return 0;
}
const修饰的变量取地址操作符重载,很少需要我们进行重写,了解即可。
const成员
我们发现d1变量被const修饰了,而调用show()这个成员函数时会报错,这是因为show里面有一个隐含的this指针,这个函数里面的指针没有被const修饰,也就导致了*this是可以修改的,但是d1不可以修改,这是一个权限放大的问题。如果const修饰的变量也想调用成员函数,就必须把成员函数里面的指针也修饰成const类型。
把const写在函数名后,一般const修饰不需要改变值得成员函数内,但是需要改变*this的成员函数不可以被const修饰,比如:
Date& Date::operator+=(int day) {
_day += day;
while (_day > GetMonthDay(_year, _month)) {
_day -= GetMonthDay(_year, _month);
_month++;
if (_month > 12) {
_year++;
_month = 1;
}
}
return *this;
}