C++类中有6个默认函数,分别是:构造函数、析构函数、拷贝构造函数、赋值运算符重载、取地址及const取地址运算符重载。这六个函数是很特殊的函数,如果我们不自己实现,编译器就会自己实现。这篇博客中将一一介绍这六个默认函数。
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。
其特征如下:
举例
class Date
{
private:
int _year;
int _month;
int _day;
public:
Date(int year = 2022, int month = 9, int day = 27) // 构造函数推荐使用缺省
{
cout << "构造函数调用" << endl;
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
};
int main()
{
// 构造函数调用方式场合
Date d1;
d1.Print();
Date d2(2022, 1, 15);
d2.Print();
return 0;
}
输出结果
可以看出在实例化对象d1、d2的时候编译器自动调用了构造函数来对他们进行初始化。
默认构造函数
C++里面把类型分为两类:内置类型(基本类型),自定义类型
内置类型:int/char/double/指针/内置类型数组等等
自定义类型:struct/class定义的类型我们不写构造函数,编译器默认生成的构造函数,对于内置类型不做初始化处理
对于自定义类型成员变量会去调用它的默认造函数初始化,如果没有默认构造函数就会报错;对于内置类型成员变量不做处理
默认构造函数:不用参数就可以调用的。拿Date类来举例,默认构造函数(有三个)就是Date()、Date(int year = 1, int month = 1, int day = 1)全缺省、编译器自己生成。推荐写全缺省的构造函数。
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;
};
int main()
{
// 将Date类中构造函数屏蔽后,代码可以通过编译,因为编译器生成了一个无参的默认构造函数
// 将Date类中构造函数放开,代码编译失败,因为一旦显式定义任何构造函数,编译器将不再生成
// 无参构造函数,放开后报错:error C2512: “Date”: 没有合适的默认构造函数可用
Date d;
// d应该要调用默认构造函数,但是我们显示定义了构造函数,所以编译器不再生成默认构造函数,所以会报错
// 如果我们定义的显式构造函数可以全缺省,那么d也可以调用
d.Print();
Date d1(1,1,1);
d1.Print();
return 0;
}
一个固定语法
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;
};
int main()
{
/* 编译器就是不让这样用构造函数,按理来说全缺省的构造函数是可以的,应该是为了规避一些重名
Date d(); // 不能这样用
d.Print();
*/
Date d1;
d1.Print();
return 0;
}
总结:C++关于默认构造函数的生成,设计的不好,没有对内置类型和自定义类型做到统一处理。C++不处理内置成员变量,只处理自定义成员变量,只对自定义类型会主动去寻找它的构造函数。
概念:通过前面构造函数的学习,我们知道一个对象是怎么来的,那一个对象又是怎么没呢的?
析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
析构函数是特殊的成员函数,其特征如下:
举例
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类没有资源需要清理,所以不实现析构函数是可以的
}
private:
int _year;
int _month;
int _day;
};
class Stack
{
public:
Stack(int capacity = 4)
{
_a = (int*)malloc(sizeof(int) * capacity);
if (_a == nullptr)
{
cout << "malloc fail\n" << endl;
exit(-1);
}
_top = 0;
_capacity = capacity;
}
void push()
{}
~Stack()
{
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
private:
int* _a;
int _top;
int _capacity;
};
int main()
{
Date d1;
d1.Print();
// 析构函数先s2后s1
Stack s1(20);
Stack s2(30);
return 0;
}
在创建对象时,可否创建一个与已存在对象一某一样的新对象呢?
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
拷贝构造函数也是特殊的成员函数,其特征如下:
举例
class Date
{
public:
// 构造函数
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// 拷贝构造函数
// 如果这块不用引用,而使用传值拷贝的话,会一层一层调用这个拷贝构造函数,套娃了,我穷无尽递归
// 使用引用的话,就不需要再调用拷贝构造函数,就不会套娃
// 最好加上const
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
~Date()
{
// Date类没有资源需要清理,所以不实现析构函数是可以的
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2022, 1, 15);
// 拷贝复制
Date d2(d1);
d2.Print();
return 0;
}
class Stack
{
public:
Stack(int capacity = 4)
{
_a = (int*)malloc(sizeof(int) * capacity);
if (_a == nullptr)
{
cout << "malloc fail\n" << endl;
exit(-1);
}
_top = 0;
_capacity = capacity;
}
void push()
{}
~Stack()
{
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
private:
int* _a;
int _top;
int _capacity;
};
int main()
{
Date d1(2022, 1, 15);
// 拷贝复制
Date d2(d1); // 不写拷贝构造,编译器会默认逐字节拷贝
d2.Print();
/* 栈不写拷贝构造函数程序会崩溃,因为默认的拷贝构造函数是逐字节拷贝,会导致将指针拷贝过来,两个对象中的指针指向同一个内存
Stack s1(10);
Stack s2(s1);
*/
return 0;
}
总结
拷贝构造:使用同类型的对象去初始化实例对象。 如果我们不实现,编译器会默认生成拷贝构造函数。
默认生成的拷贝构造函数:
有些类,不需要自己写拷贝构造函数,例如日期类,默认的靠别构造函数就足够了;然而对于栈类,需要深拷贝,所以要自己写拷贝构造函数。
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)
注意:
.* :: sizeof ?: .
注意以上5个运算符不能重载。举例
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;
}
// 运算符重载函数写在类内,d1和d2比较大小应该是d1.operator(d2),
// 写在类内成员函数会生成d1对象的this指针,所以要修改重载函数,原先的写法相当于d1重复写了
// bool operator>(const Date& d1, const Date& d2) 原先的写法
/*bool operator>(const Date& d2)
{
if (this->_year > d2._year)
{
return true;
}
else if (this->_year == d2._year && this->_month > d2._month)
{
return true;
}
else if (this->_year == d2._year && this->_month == d2._month && this->_day > d2._day)
{
return true;
}
else
{
return false;
}
}*/
// 实际上就是按照上面的代码在执行的,但是this指针默认生成的,我们一般不写出来
// 如下的话就是_year <==> this->_year , d是d2的别名
bool 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;
}
}
private:
int _year;
int _month;
int _day;
};
/* 运算符重载写在类外面,会出现不能访问私有成员变量的问题,所以写在类内
// 函数名:operator+操作符,
// 返回类型:看操作符运算后返回的值
// 参数: 操作符有几个操作数,他就有几个参数
bool operator>(const Date& d1, const Date& 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 && d2._day > d2._day)
{
return true;
}
else
{
return false;
}
}
*/
int main()
{
Date d1(2022, 2, 3);
Date d2(2022, 8, 23);
Date d3(2022, 9, 3);
// 写在类内的调用方法
d1 > d2; // 先在类中寻找运算符重载函数,找不到再到全局寻找
d1.operator>(d2); // 在类中找到运算符重载函数这样调用
cout << (d1 > d2) << endl;
/* 重载函数 写在类外 的调用方法
// 加括号是因为运算符的优先级问题
cout << (d1 > d2) << endl; // 会调用函数operator>()
// 上下是等价的
cout << operator>(d1, d2) << endl;
*/
return 0;
}
class Date
{
public:
// 构造函数
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// 拷贝构造函数
Date(const Date& d)
{
cout << "const Date&" << endl; // 测试用,不能正常拷贝的
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
// 赋值运算符重载
//
// 因为this是指向一个对象的指针,出了作用域这个对象还在,所以这里可以使用引用做返回值
// 如果以Date作为返回值,那么会调用拷贝构造函数创建一个形参然后作为返回值,所以使用引用做返回值更好
// 传引用返回的就是调用重载的对象
Date& operator=(const Date& d)
{
// this是当前对象的指针,&d是要赋值给当前对象的对象的地址,
// 判断相等的话说明是当前对象赋值给自己,所以可以不用执行赋值过程,提高效率
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
// 所以this是存在的
return *this; // 如果不传引用作为返回值,那么会先调用拷贝构造函数拷贝一份*this对象,然后作为返回值返回
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2022, 1, 1);
Date d2(2022, 1, 2);
Date d3(2022, 1, 3);
// 一个已经存在的对象拷贝初始化一个马上创建实例化的对象
Date d4(d1); // 拷贝构造
Date d5 = d1; // 这个是拷贝构造,两个已经存在的对象赋值才是赋值重载
// 赋值运算符重载
d1 = d3;
d1.Print();
// i = j = k = 1 这个式子的语法是正确的,也就是说赋值是具有返回值的
d1 = d3 = d2;
d1.Print();
return 0;
}
将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
void Print() const;
// 本来参数应该是 Date* const this,this这个指针变量不能改变
// 后面加上const之后,就变成了 const Date* const this, this和*this都会被保护
// 所以定义的const Date d1就不存在权限放大,相当于权限缩小了
成员函数加const是好的,能加最好加上,这样普通对象和const对象都可以调用成员函数。要加的话声明和定义都得加const
class Date
{
public:
Date* operator&()
{
return this;
}
const Date* operator&()const
{
return this;
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容!
C++类中的6个默认成员函数可以分为以下三类
这六个函数是很特殊的函数,如过我们不实现,编译器会自己实现。