类的6个默认成员函数
一、认识类的默认成员函数
空类:如果一个类中没有任何成员。
空类中并不是什么都没有,编译器会自动生成以下6个默认成员函数。
默认成员函数:用户没有显式实现,编译器会生成的成员函数。
二、初始化和清理
1. 构造函数
构造函数的主要任务:初始化对象
①概念
eg:写一个简单的日期类
class Date
{
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
std::cout << _year << "-" << _month << "-" << _day << std::endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.Init(2024, 4, 3);
d1.Print();
return 0;
}
对于Date类,可以通过公有方法给对象设置日期,但是每次都要调用公有方法Init
设置信息。而使用构造函数就可以在创建对象时,就把信息设置进去
构造函数:名字和类名相同,创建类的类型对象时由编译器自动调用,给每一个数据成员一个合适的初始值,在对象的整个生命周期只调用一次。
②特性
构造函数是特殊的成员函数:
- 函数名和类名相同
- 无返回值
- 对象实列化时编译器自动调用对应的构造函数
- 构造函数可以重载
- 通过无参构造函数构造对象时,对象后面不要跟括号,否则就是函数声明了
eg:使用构造替换Init
公有方法
class Date
{
public:
// 1.无参构造函数
Date()
{}
// 2.带参构造函数
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
void TestDate()
{
Date d1; //调用无参构造函数
Date d2(2015, 1, 1); //调用带参的构造函数
//通过无参构造函数构造对象时,对象后面不要跟括号,否则像下面这样就成为函数声明了
//声明了d3函数,该函数无参,返回一个日期类型的对象
Date d3(); //warning
}
- 如果类中用户没有显示定义构造函数,编译器会自动生成一个无参的默认构造函数。
eg1:
class Date
{
public:
//定义一个带参的构造函数。用户显示定义构造函数,编译器不会再生成
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
//将Date类带参构造函数屏蔽,代码可以通过编译,因为编译器会生成了一个无参的默认构造函数
//将Date类带参构造函数放开,则编译失败
//因为用户显示定义任何形式的构造函数,编译器都不会在自动生成
Date d1; // error C2512: “Date”: 没有合适的默认构造函数可用
return 0;
}
- C++把类型分成内置类型(基本类型:int/char…)和自定义类型,编译器生成的默认构造函数会对自定义类型成员调用它的默认构造函数。
eg2:
class Time
{
public:
Time()
{
cout << "Time()" << endl;
_hour = 0;
_minute = 0;
_second = 0;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
//基本类型(内置类型)
int _year;
int _month;
int _day;
//自定义类型
Time _t;
};
int main()
{
Date d;
return 0;
}
//运行结果:Time()
注:内置类型成员变量在类中声明时可以给默认值(C++11),例如下面这样
eg3:
class Time
{
public:
Time()
{
cout << "Time()" << endl;
_hour = 0;
_minute = 0;
_second = 0;
}
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;
}
- 默认构造函数只能有一个,有以下三种默认构造
- 无参构造函数
- 全缺省构造函数
- 编译器默认生成的构造函数
eg4:
class Date
{
public:
Date()
{
_year = 1900;
_month = 1;
_day = 1;
}
Date(int year = 1900, int month = 1, int day = 1) //全缺省
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
//不能通过编译。虽然构造函数重载,但是调用时无参调用,存在歧义
Date d1;
return 0;
}
③小结
- 一般情况构造函数都需要我们自己写
- 不写的情况
a、内置类型都有缺省值,且初始化符合我们的要求
b、全是自定义类型的构造,且这些类型都定义默认构造
2. 析构函数
析构函数的主要任务:对象中资源的清理
①概念
析构函数:局部对象的销毁工作是由编译器完成的,而对象在销毁时会自动调用析构函数,完成对象中资源清理的工作
②特性
析构函数是特殊的成员函数:
- 析构函数名是在类名前加上字符
~
- 无参数,无返回值类型
- 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数
- 对象生命周期结束时,C++编译系统自动调用析构函数
注:析构函数不能重载
eg:
typedef int DataType; //如果不是int类型,是别的类型,便于维护。以后有模板也不用这样
class Stack
{
public:
//构造函数
Stack(size_t capacity = 3) //缺省值
{
std::cout << "Stack(size_t capacity = 3)" << std::endl;
_array = (DataType*)malloc(sizeof(DataType) * capacity);
if (nullptr == _array)
{
perror("malloc::fail");
return;
}
_capacity = capacity;
_size = 0;
}
//析构函数
~Stack()
{
std::cout << "~Stack()" << std::endl;
if (_array)
{
free(_array);
_array = nullptr;
_capacity = 0;
_size = 0;
}
}
private:
DataType* _array;
int _capacity;
int _size;
};
int main()
{
Stack s;
return 0;
}
//运行结果:
//Stack(size_t capacity = 3)
//~Stack()
- 编译器生成的默认析构函数,对自定义类型成员会调用它的析构函数。
eg1:
class Time
{
public:
~Time()
{
cout << "~Time()" << endl;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
//基本类型(内置类型)
int _year = 1900;
int _month = 1;
int _day = 1;
//自定义类型
Time _t;
};
int main()
{
Date d;
return 0;
}
//运行结果:~Time()
//d对象中包含了自定义类型和内置类型。内置类型成员,销毁时不需要资源清理,系统直接将
//其内存回收。而自定义类型成员_t是Time类对象,所以销毁_t时要调用Time类的析构函数。
//but:main方法不能直接调用Time类的析构,所以要先调用Date类的析构函数,而Date类没
//有显示提供,所以编译器默认生成Date类析构函数,在其内部调用Time类析构
//当Date对象销毁时,要保证其内部每个自定义对象都可以正确销毁
③小结
- 要写析构函数的情况:有动态申请资源就要显示写析构函数释放资源。
- 不要写析构函数的情况:
a. 没有动态申请资源,不需要写析构。
b. 需要释放的成员都是自定义类型,不需要写析构
eg:
//这样就不需要我们写析构
class MyQueue
{
private:
Stack _pushst;
Stack _popst;
};
三、拷贝构造和赋值运算符重载
1. 拷贝构造函数
①概念
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
②特性
拷贝构造也是特殊的成员函数:
- 拷贝构造函数是构造函数一种重载形式。
- 拷贝构造函数的参数只有一个,且必须是类类型对象的引用。
注意:使用传值方式编译器直接报错,会引发无穷递归
eg1:
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//拷贝构造函数
//Date(const Date d) //error:编译器报错引发无穷递归
Date(const Date& d) //ok
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2(d1);
return 0;
}
- 若没有显式定义,编译器会生成默认的拷贝构造函数。默认的拷贝构造函数对象按内存存储,按字节序完成拷贝。—> 浅拷贝(值拷贝)
- 默认生成的拷贝构造函数,内置类型按照字节直接拷贝,而自定义类型是调用它的拷贝构造完成拷贝
eg2:值拷贝
class Time
{
public:
Time()
{
_hour = 1;
_minute = 1;
_second = 1;
}
Time(const Time& t)
{
cout << "Time(const Time& t)" << endl;
_hour = t._hour;
_minute = t._minute;
_second = t._second;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
int _year = 1900;
int _month = 1;
int _day = 1;
Time _t;
};
int main()
{
Date d1;
//使用d1拷贝构造d2,此处调用Date类的拷贝构造
//但是Date类没有显示定义拷贝构造,则编译器会给Date类生成一个默认的拷贝构造
//编译器生成的默认拷贝构造,内置类型是按照字节序方式直接拷贝,
//而自定义类型是调用其拷贝构造函数完成拷贝的
Date d2(d1);
return 0;
}
//运行结果:
Time(const Time& t)
③小结
默认的拷贝构造函数不够用,因为有深拷贝的存在
1. 拷贝构造是为了深拷贝而生:
浅拷贝图解:
为栈实现一个深拷贝构造函数
eg:
Stack(const Stack& st)
{
_array = (int*)malloc(sizeof(int ) * st._capacity);
if (nullptr == _array)
{
perror("malloc::fail");
return;
}
memcpy(_array, st._array, sizeof(int) * st._size);
_size = st._size;
_capacity = st._capacity;
}
注:
类中如果没有涉及资源申请,拷贝构造函数写不写都行。但是涉及到资源的申请,则一定要写拷贝构造,否则就是浅拷贝
2. 拷贝构造函数典型调用场景:
- 使用已存在对象创建新对象
- 函数参数类型为类类型对象
- 函数返回值类型为类类型对象
为了提高运行效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能使用引用最好使用引用
2. 赋值运算符重载
①运算符重载
C++为了增加代码的可读性,引入了运算符重载,运算符重载是具有特殊函数名的函数。和不同函数区别就在于函数名,返回值类型和参数列表和普通函数类似
函数名定义:关键字operator后面直接跟重载的运算符符号
语法:返回值类型 operator操作符(参数列表)
注:
- 不能通过其他的符号创建新的操作符。eg:operator@
- 重载操作符必须有一个类类型的参数
- 用于内置类型的运算符,其含义不能改变。eg:内置类型的+,不能把其当作减法来用
- 作为类成员函数重载时,其形参看起来比操作数数目少一。原因:成员函数第一个参数为隐藏的this
.* :: sizeof ?: . 以上五个运算符不能重载
eg:
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//bool opeartor==(Date* this, const Date& d2)
//左操作数是this,指向调用函数的对象
bool operator==(const Date& d2)
{
return _year == d2._year
&& _month == d2._month
&& _day == d2._day;
}
private:
int _year;
int _month;
int _day;
};
//运算符重载写成全局的,但是这里就需要使用公有的成员变量了。
//需要解决,友元可以解决该问题
//bool opeartor==(Date* this, const Date& d2);
int main()
{
Date d1(2023, 7, 13);
Date d2(2023, 7, 13);
//(d1 == d2) <==> d1.operator==(d2)
cout << (d1 == d2) << endl;
cout << d1.operator==(d2) << endl;
return 0;
}
②赋值运算符重载特性
- 赋值运算符重载格式:
- 参数类型:const T& (传递引用提高传参效率)
- 返回值类型:T& (返回值引用可以提高返回效率,有返回值为了支持连续赋值)
- 函数体:
- 检测是否是自己给自己赋值
- 返回*this:要符合连续赋值的含义
eg1:
class Date
{
public:
//构造函数
Date(int year = 1900, 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;
}
//赋值运算符重载
Date& operator=(const Date& d)
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
//this的生命周期结束,但是Date类创建的对象生命周期还没有结束
return *this;
}
private:
int _year;
int _month;
int _day;
};
- 赋值运算符只能重载成类的成员函数不能重载成全局函数
eg2:
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
int _year;
int _month;
int _day;
};
//赋值运算符重载成全局函数
Date& operator=(Date& left, const Date& right)
{
if (&left != &right)
{
left._year = right._year;
left._month = right._month;
left._day = right._day;
}
return left;
}
int main()
{
Date d1(2023, 7, 12);
Date d2, d3;
d2 = d3 = d1;
return 0;
}
运行结果:代码编译失败。原因:在类中没有显示实现,编译器会默认生成,此时用户在类外自己实现一个全局的赋值运算符重载就和默认生成的冲突了
- 没有显示实现,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。
注意:内置类型成员变量直接赋值,而自定义类型成员变量需要调用类的赋值运算符重载完成赋值
eg3:
class Time
{
public:
Time()
{
_hour = 1;
_minute = 1;
_second = 1;
}
Time& operator=(const Time& t)
{
cout << "Time& operator=(const Time& t)" << endl;
if (this != &t)
{
_hour = t._hour;
_minute = t._minute;
_second = t._second;
}
return *this;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
int _year;
int _month;
int _day;
Time _t;
};
int main()
{
Date d1;
Date d2;
d1 = d2;
return 0;
}
//运行结果:Time& operator=(const Time& t)
③小结
一旦涉及资源管理必须要实现运算符重载,如果未涉及到,是否实现都可以。
拷贝构造和赋值运算符重载的区别:
拷贝构造函数:用一个对象初始化创建另一个对象
赋值运算符重载:两个已经创建的对象,一个赋值给另一个。
eg:
Date d1(2023, 1, 1);
Date d2 = d1; //拷贝构造
Date d3(d1); //拷贝构造
d2 = d3; //赋值运算符重载
四、取地址及const取地址操作符重载
这两个默认成员函数一般不用重新定义,编译器会默认生成。主要解释:自定义类型使用运算符要重载。
eg:
class Date
{
public:
Date* operator&()
{
return this;
}
const Date* operator&() const //这个const实际修饰*this
{
return this;
}
private:
int _a;
};
有些情景,比如不想让别人看
return nullptr;
总结
- 类的六个默认成员函数不能定义为全局函数。它们必须定义在类的内部作为成员函数。
- 类的默认成员函数可以在两个文件中进行声明和定义分离。将类的接口和实现分开,使代码更加模块化和可维护。
构造函数:
- 一般情况构造函数都需要我们自己写
- 不写的情况
a、内置类型都有缺省值,且初始化符合我们的要求
b、全是自定义类型的构造,且这些类型都定义默认构造
析构函数:
- 要写析构:有动态申请资源就要显示写析构函数释放资源。
- 不要写析构:
a、没有动态申请资源,不需要写析构。
b、需要释放的成员都是自定义类型,不需要写析构
拷贝构造函数:
拷贝构造是为了深拷贝而生。
类中如果没有涉及资源申请,拷贝构造是否写都可以,一旦涉及资源申请就一定要写拷贝构造
赋值运算符:
没有显示实现,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。
一旦涉及资源管理必须要实现运算符重载,如果未涉及到,是否实现都可以。
拷贝构造函数和赋值运算符的区别:
拷贝构造函数:用一个对象初始化创建另一个对象
赋值运算符重载:两个已经创建的对象,一个赋值给另一个。