目录
类的6个默认成员函数
如果一个类中什么成员都没有,简称为空类。
空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员
函数。
默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。
class Date
{
//空类,编译器仍会自动生成6个默认成员函数
};
注意:这6个默认成员函数是由编译器自动生成,但是如果自己已经定义了,那么编译器就不会再自动生成。
构造函数
构造函数的概念
构造函数:名字与类名相同,创建类类型对象时由编译器自动调用,保证每个数据成员都有 一个合适的初始值,并且在对象的生命周期内只调用一次。
比如以下的日期类:
class Date
{
public:
Date(int year = 0, 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;
}
对于Date类,可以通过 Init 公有方法给对象设置日期,但如果每次创建对象时都调用该方法设置
信息,未免有点麻烦。因此,构造函数的出现就可以解决这个问题,创建类类型对象后,编译器会自动调用构造函数,帮助初始化。构造函数就是为了完成初始化工作。
构造函数的特性
一、构造函数的函数名与类名相同
二、构造函数无返回值
这里所说的构造函数无返回值是真的无返回值,而不是说返回值为void。
三、对象实例化时编译器自动调用对应的构造函数
当你用类创建一个对象时,编译器会自动调用该类的构造函数对新创建的变量进行初始化。
四、构造函数支持重载
这意味着你可以有多种初始化对象的方式,编译器会根据你所传递的参数去调用对应的构造函数。
五、无参的构造函数、全缺省的构造函数以及我们不写编译器自动生成的构造函数都称为默认构造函数,并且默认构造函数只能有一个
初学C++时,你可能认为只有当我们不写,编译器自动生成的构造函数才被称为默认构造函数。其实并不是这样的,以下3种都叫做默认构造函数:
1、我们不写,编译器自动生成的构造函数。
2、我们自己写的无参的构造函数。
3、我们自己写的全缺省的构造函数。
关于系统默认生成的构造函数,但是编译器生成的默认构造函数对内置类型不处理,对自定义类型调用它的默认构造函数。
总而言之,无需传参就可以调用的构造函数就是默认构造函数。
六、如果类中没有显示定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,若用户显示定义了,则编译器就不再生成。
说到这里,你可能会想:既然在我们不写的情况下,编译器会自动生成一个构造函数,那我们就没有必要自己写构造函数了。这种想法是不对的。
对于构造函数我们应该:面向需求——编译器默认生成就可以满足,就不用自己写,不满足就需要自己写。
析构函数
析构函数的概念
析构函数:与构造函数功能相反,析构函数负责完成对象的销毁,对象在销毁时会自动调用析构函数,完成类的一些资源清理工作。
我们知道当一个类对象销毁时,其中的局部变量也会随着该对象的销毁而销毁,例如,我们用日期类创建了一个对象d1,当d1被销毁时,对象d1当中的局部变量_year/_month/_day也会被编译器销毁。
但是这并不意味着析构函数没有什么意义。像栈(Stack)这样的类对象,当该对象被销毁时,其中动态开辟的栈并不会随之被销毁,需要我们对其进行空间释放,这时析构函数的意义就体现了。
析构函数的特性
析构函数是特殊的成员函数,其特征如下:
一、析构函数名是在类名前加上字符 ~。
class Date
{
public:
Date()// 构造函数
{}
~Date()// 析构函数
{}
private:
int _year;
int _month;
int _day;
};
二、析构函数无参数无返回值类型
析构函数所谓的无返回值和构造函数一样,也是真的无返回值,而不是返回值为void。
三、一个类只能有一个析构函数。若未显示定义,系统会自动生成默认的析构函数。
编译器自动生成的析构函数机制:
- 编译器自动生成的析构函数对内置类型不做处理。
- 对于自定义类型,编译器会再去调用它们自己的默认析构函数。
四、对象生命周期结束时,C++编译器会自动调用析构函数。
注意:对于什么时候需要我们自己写析构函数,或者什么时候可以使用编译器自动生成的析构函数,这个问题与构造函数相同。面向需求——编译器默认生成就可以满足,就不用自己写,不满足就需要自己写。最后补充一句析构函数不能重载
构造与析构函数补充知识
首先看下面这段代码,我定义了两个类
class Stack
{
public:
//构造函数 全缺省
Stack(int capacity = 4)
{
cout << "Stack(int capacity = 4)" << endl;
_a = (int*)malloc(sizeof(int) * capacity);
if (_a == nullptr)
{
perror("malloc fail");
exit(-1);
}
_top = 0;
_capacity = capacity;
}
//析构函数
~Stack()
{
cout << "~Stack()" << endl;
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
void Push(int x)
{
// ....
// 扩容
_a[_top++] = x;
}
private:
int* _a;
int _top;
int _capacity;
};
//此类 未定义构造函数和析构函数,我们使用系统自动生成的
class MyQueue {
public:
void push(int x)
{
_pushST.Push(x);
}
Stack _pushST;
Stack _popST;
size_t _size = 0;
};
int main()
{
MyQueue q1;
return 0;
}
在上文中提到,系统默认生成的构造函数,对内置类型不处理,对自定义类型调用它的默认构造函数。如果我们定义的一个类中既有内置类型,又有自定义类型,如果我们不想要自己写构造函数的话,那么系统默认生成的构造函数就不能满足我们的需求了,对此我们有以下解决方案:
C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在
类中声明时可以给默认值。
比如:在上面的两个类中,第一个Stack我们自己定义了构造函数以及析构函数,第二个MyQueue我们没有自己定义而是使用了系统自动生成的。运行结果如下:
可以看出:我们定义了一个对象q1后,调用了两次Stack的构造函数和析构函数。因此MyQueue中含有两个自定义类型,一个内置类型。
系统自动生成的构造函数,对内置类型不处理,但是我们在声明中给出了默认值,所以size初始化为了1;对于自定义类型pushST和popST,调用了我们在Stack中自己写的构造函数,也成功地初始化了。
总之,再次强调:面向需求——编译器默认生成就可以满足,就不用自己写,不满足就需要自己写。
拷贝构造函数
拷贝构造函数概念
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用从const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
#include <iostream>
using namespace std;
class Date
{
public:
Date(int year = 0, int month = 1, int day = 1)// 构造函数
{
_year = year;
_month = month;
_day = day;
}
Date(const Date& d)// 拷贝构造函数
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2021, 5, 31);
Date d2(d1); // 用已存在的对象d1创建对象d2
return 0;
}
拷贝构造函数的特性
一、拷贝构造函数是构造函数的一个重载形式
拷贝构造函数的函数名也必须和类名相同
二、拷贝构造函数的参数只有一个且必须使用引用传参
注意:拷贝构造使用传值方式编译器直接报错,会引发无穷递归调用。因为传值过程会拷贝一个临时变量,拷贝则又会调用拷贝构造函数,从而引发无穷递归。
三、若未显示定义拷贝构造函数,系统将生成默认的拷贝构造函数
编译器自动生成的拷贝构造函数机制:
1、编译器自动生成的拷贝构造函数对内置类型会完成浅拷贝(值拷贝)。
2、对于自定义类型,编译器会再去调用它们自己的默认拷贝构造函数。
四、编译器自动生成的拷贝构造函数不能实现深拷贝
上面说到,编译器自动生成的拷贝构造函数会对内置类型完成浅拷贝。对于以下这句代码,浅拷贝实际上就是将d1的内容逐字节地复制了一份拷贝给d2,所以说浅拷贝也叫做值拷贝。
但某些场景下浅拷贝并不能达到我们想要的效果。例如,栈(Stack)这样的类,编译器自动生成的拷贝构造函数就不能满足我们的需求。此时,便需要我们自己定义拷贝构造函数,进行深拷贝,请看下面的例子。
class Stack
{
public:
Stack(int capacity = 4)
{
cout << "Stack(int capacity = 4)" << endl;
_a = (int*)malloc(sizeof(int)*capacity);
if (_a == nullptr)
{
perror("malloc fail");
exit(-1);
}
_top = 0;
_capacity = capacity;
}
// st2(st1)
Stack(const Stack& st)
{
cout << "Stack(const Stack& st)" << endl;
_a = (int*)malloc(sizeof(int)*st._capacity);
if (_a == nullptr)
{
perror("malloc fail");
exit(-1);
}
memcpy(_a, st._a, sizeof(int)*st._top);
_top = st._top;
_capacity = st._capacity;
}
~Stack()
{
cout << "~Stack()" << endl;
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
void Push(int x)
{
// ....
// 扩容
_a[_top++] = x;
}
private:
int* _a;
int _top;
int _capacity;
};
注意,当我们自己写拷贝构造函数时,可以使用const保护被拷贝的数据,防止拷贝构造函数写错时,损害原有数据。
我们可以总结出一个规律:
- 需要写析构函数的类,都需要写深拷贝的拷贝构造 Stack
- 不需要写析构函数的类,默认生成的浅拷贝的拷贝构造就可以用 Date/MyQueue
同时还应注意,对于动态开辟的内存空间等浅拷贝存在风险,直接看图
运算符重载
运算符重载
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,其目的就是让自定义类型可以像内置类型一样可以直接使用运算符进行操作。
运算符重载函数也具有自己的返回值类型,函数名字以及参数列表。其返回值类型和参数列表与普通函数类似。
运算符重载函数名为:关键字operator后面接需要重载的操作符符号。
函数原型:返回值 operator运算符(参数列表)
注意:
- 不能通过连接其他符号来创建新的操作符:比如operator@
- 重载操作符必须有一个类类型参数
- 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义
- 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐
藏的this - .* :: sizeof ? : . 注意以上5个运算符不能重载。
这里以重载 == 运算符作为例子:
class Date
{
public:
Date(int year = 0, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
bool operator==(const Date& d)// 运算符重载函数
{
return _year == d._year
&&_month == d._month
&&_day == d._day;
}
private:
int _year;
int _month;
int _day;
};
赋值运算符重载
下面给出 = 重载的具体代码
class Date
{
public:
Date(int year = 0, int month = 1, int day = 1)// 构造函数
{
_year = year;
_month = month;
_day = day;
}
Date& operator=(const Date& d)// 赋值运算符重载函数
{
//解决d1 = d1的特殊情况
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
private:
int _year;
int _month;
int _day;
};
重载赋值运算符需要注意以下几点:
一、参数类型设置为引用,并用const进行修饰
赋值运算符重载函数的第一个形参默认是this指针,第二个形参是我们赋值运算符的右操作数。
由于是自定义类型传参,我们若是使用传值传参,会额外调用一次拷贝构造函数,所以函数的第二个参数最好使用引用传参(第一个参数是默认的this指针,我们管不了)。
其次,第二个参数,即赋值运算符的右操作数,我们在函数体内不会对其进行修改,所以最好加上const进行修饰。
二、函数的返回值使用引用返回
实际上,我们若是只以d2 = d1这种方式使用赋值运算符,赋值运算符重载函数就没必要有返回值,因为在函数体内已经通过this指针对d2进行了修改。但是为了支持连续赋值,即d3 = d2 = d1,我们就需要为函数设置一个返回值了,而且很明显,返回值应该是赋值运算符的左操作数,即this指针指向的对象。
和使用引用传参的道理一样,为了避免不必要的拷贝,我们最好还是使用引用返回,因为此时出了函数作用域this指针指向的对象并没有被销毁,所以可以使用引用返回。
三、赋值前检查是否是给自己赋值
若是出现d1 = d1,我们不必进行赋值操作,因为自己赋值给自己是没有必要进行的。所以在进行赋值操作前可以先判断是否是给自己赋值,避免不必要的赋值操作。
四、引用返回的是*this
应该注意=可能出现链式赋值的情况,所以返回值不应该单纯的使用void,否则d1=d2=d3的情况无法正常使用
赋值操作进行完毕时,我们应该返回赋值运算符的左操作数,而在函数体内我们只能通过this指针访问到左操作数,所以要返回左操作数就只能返回*this。
五、一个类如果没有显示定义赋值运算符重载,编译器也会自动生成一个,完成对象按字节序的值拷贝
没错,赋值运算符重载编译器也可以自动生成,并且也是支持连续赋值的。但是编译器自动生成的赋值运算符重载完成的是对象按字节序的值拷贝,例如d2 = d1,编译器会将d1所占内存空间的值完完全全地拷贝到d2的内存空间中去,类似于memcpy。
对于日期类,编译器自动生成的赋值运算符重载函数就可以满足我们的需求,我们可以不用自己写。但是这也不意味着所有的类都不用我们自己写赋值运算符重载函数,当遇到一些特殊的类,我们还是得自己动手写赋值运算符函数的。
拷贝构造和赋值运算符重载区分
Date d1(2021, 6, 1);
Date d2(d1);
Date d3 = d1; //这里是在初始化d3,所以调用的是拷贝构造函数,与赋值运算符重载无关
这里一个三句代码,我们现在都知道第二句代码调用的是拷贝构造函数,那么第三句代码呢?调用的是哪一个函数?是赋值运算符重载函数吗?其实第三句代码调用的也是拷贝构造函数,注意区分拷贝构造函数和赋值运算符重载函数的使用场景:
拷贝构造函数:用一个已经存在的对象去构造初始化另一个即将创建的对象。
赋值运算符重载函数:在两个对象都已经存在的情况下,将一个对象赋值给另一个对象。
const 成员
将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数
隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
如下图:
我们定义了两个对象d1,d2,其中d2用const修饰后便不能调用print函数了,这是为什么那,我们应该怎么解决这个问题。
void Print() const
{
cout << _year << "-" << _month << "-" << _day << endl;
}
我们只需要在print函数后面加上const就好了,因为
现在,思考下面几个问题:
- const对象可以调用非const成员函数吗?
- 非const对象可以调用const成员函数吗?
- const成员函数内可以调用其他的非const成员函数吗?
- 非cosnt成员函数内可以调用其他的cosnt成员函数吗?
答案是:不可以、可以、不可以、可以
解释如下:
- 非const成员函数,即成员函数的this指针没有被const所修饰,我们传入一个被const修饰的对象,用没有被const修饰的this指针进行接收,属于权限的放大,函数调用失败。
- const成员函数,即成员函数的this指针被const所修饰,我们传入一个没有被const修饰的对象,用被const修饰的this指针进行接收,属于权限的缩小,函数调用成功。
- 在一个被const所修饰的成员函数中调用其他没有被const所修饰的成员函数,也就是将一个被const修饰的this指针的值赋值给一个没有被const修饰的this指针,属于权限的放大,函数调用失败。
- 在一个没有被const所修饰的成员函数中调用其他被const所修饰的成员函数,也就是将一个没有被const修饰的this指针的值赋值给一个被const修饰的this指针,属于权限的缩小,函数调用成功。
取地址及const取地址操作符重载
取地址操作符重载和const取地址操作符重载,这两个默认成员函数一般不用自己重新定义,使用编译器自动生成的就行了。因为自己定义的情况极少所以我们在这里直接跳过了。
Date类
最后,附上一个Date类
Date.h
#pragma once
#include <iostream>
using namespace std;
class Date
{
// 友元声明(类的任意位置)
friend ostream& operator<<(ostream& out, const Date& d);
friend istream& operator>>(istream& in, Date& d);
public:
int GetMonthDay(int year, int month)
{
static int monthDayArray[13] = {0,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;
}
else
{
return monthDayArray[month];
}
}
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
// 检查日期是否合法
if (!(year >= 1
&& (month >= 1 && month <= 12)
&& (day >= 1 && day <= GetMonthDay(year, month))))
{
cout << "非法日期" << endl;
}
}
void Print() const
{
cout << _year << "/" << _month << "/" << _day << endl;
}
bool operator==(const Date& d) const;
// d1 > d2
bool operator>(const Date& d) const;
// d1 >= d2
bool operator>=(const Date& d) const;
bool operator<=(const Date& d) const;
bool operator<(const Date& d) const;
bool operator!=(const Date& d) const;
// d1 += 100
Date& operator+=(int day);
// d1 + 100
Date operator+(int day) const;
// d1 -= 100
Date& operator-=(int day);
// d1 - 100
Date operator-(int day) const;
// 前置
Date& operator++();
// 后置
Date operator++(int);
// 前置
Date& operator--();
// 后置
Date operator--(int);
// d1 - d2
int operator-(const Date& d) const;
private:
int _year;
int _month;
int _day;
};
// cout<<d1 <<重载必须定义到类外面,因为定义到类里面,this指针会占据第一个参数
inline ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
return out;
}
// cin >> d1 operator(cin, d1)
inline istream& operator>>(istream& in, Date& d)
{
in >> d._year >> d._month >> d._day;
return in;
}
Date.cpp
#include "Date.h"
bool Date::operator==(const Date& d) const
{
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
// d1 > d2
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);
}
bool Date::operator!=(const Date& d) const
{
return !(*this == d);
}
Date& Date::operator+=(int day)
{
if (day < 0)
{
//return *this -= -day;
return *this -= abs(day);
}
_day += day;
while (_day > GetMonthDay(_year, _month))
{
_day -= GetMonthDay(_year, _month);
_month++;
if (_month == 13)
{
++_year;
_month = 1;
}
}
return *this;
}
// d1 + 100
Date Date::operator+(int day) const
{
Date ret(*this);
ret += day;
return ret;
}
// d1 -= 100
Date& Date::operator-=(int day)
{
if (day < 0)
{
//return *this -= -day;
return *this += abs(day);
}
_day -= day;
while (_day <= 0)
{
--_month;
if (_month == 0)
{
--_year;
_month = 12;
}
_day += GetMonthDay(_year, _month);
}
return *this;
}
// d1 - 100
Date Date::operator-(int day) const
{
Date ret(*this);
ret -= day;
return ret;
}
// 前置
Date& Date::operator++()
{
*this += 1;
return *this;
}
// 后置 -- 多一个int参数主要是为了根前置区分
// 构成函数重载
Date Date::operator++(int)
{
Date tmp(*this);
*this += 1;
return tmp;
}
// 运算符重载
// 函数重载
// 前置
Date& Date::operator--()
{
*this -= 1;
return *this;
}
// 后置
Date Date::operator--(int)
{
Date tmp = *this;
*this -= 1;
return tmp;
}
// d1 - d2
int Date::operator-(const Date& d) const
{
Date max = *this;
Date min = d;
int flag = 1;
if (*this < d)
//if (d > *this)
{
max = d;
min = *this;
flag = -1;
}
int n = 0;
while (min != max)
{
++n;
++min;
}
return n*flag;
}