类的6个默认成员函数
空类并不是什么都没有,任何一个类都会自动生成以下6个默认成员函数
构造函数
平时写代码容易忘记初始化和销毁,并且有些地方写起来很繁琐,有了构造函数和析构函数之后就不用担心这些问题了
构造函数是一个特殊的成员函数,构造函数的主要任务并不是开空间创建对象,而是初始化对象
构造函数的特征:
函数名与类名相同
没有返回值(也不需要写void)
对象实例化时编译器自动调用对应的构造函数
构造函数可以重载(应对多种初始化的方式)
class A
{
public:
//构造函数
A(int y = 2023, int m = 11, int d = 1)
{
_y = y;
_m = m;
_d = d;
}
private:
int _y;
int _m;
int _d;
};
int main()
{
A day1;//对象实例化自动调用构造函数
return 0;
}
如果构造函数设为私有则无法调用
如果我们不写构造函数会怎么样?
class A
{
public:
void Print()
{
cout << _y << " " << _m << " " << _d << endl;
}
private:
int _y;
int _m;
int _d;
};
int main()
{
A day1;
A day2;
day1.Print();
return 0;
}
运行效果:
代码运行出来是一堆随机值,那么编译器到底有没有调用构造函数呢?
其实是调用了的,C++把类型分成了两类,一类是内置类型/基本类型 - 语言本身定义的基础类型,比如 int char double 任何类型的指针等
还有一类是自定义类型,用struct/class等等定义的类型
如果我们不写,编译器默认生成的构造函数对于内置类型不做初始化处理(有些编译器也会处理,但是我们要默认当成编译器不会处理),对于自定义类型会去调用他的默认构造
编译器默认生成的构造函数 、无参的构造函数 、全缺省的构造函数都称为默认构造函数,这三种只能存在一个
注:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数(不传参就可以调用的)
C++11标准发布的时候,在成员声明的时候可以给缺省值
class A
{
public:
void Print()
{
cout << _y << " " << _m << " " << _d << endl;
}
private:
//内置类型
//C++11支持的
//注:这里并不是初始化,因为没有开空间
//这里给的是默认的缺省值,给编译器生成的默认构造函数用的
int _y = 1;
int _m = 1;
int _d;
};
int main()
{
A day1;
//注:不能写成这样 A day1() 这样编译器无法区分是day1对象还是函数名
day1.Print();
return 0;
}
运行效果:
结论:
- 一般情况下,有内置类型成员,就需要自己写构造函数,不能用编译器自己生成的
- 如果内置类型的成员都有缺省值,且初始化符合我们的要求
或者全是自定义类型的构造,且这些类型都定义了默认构造,这两种情况可以考虑不写构造函数
析构函数
析构函数不是完成对象的销毁,局部对象销毁工作是由编译器完成的,而对象在销毁时会自动调用析构函数,完成类的一些资源清理工作
析构函数的特征:
析构函数名是在类名前加上字符 ~
无参数无返回值
一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数
对象生命周期结束时,C++编译系统系统自动调用析构函数
析构函数不能重载
class A
{
public:
//构造函数
A(int y = 2023, int m = 11, int d = 1)
{
cout << "已调用构造函数" << endl;
_y = y;
_m = m;
_d = d;
}
~A()
{
cout << "已调用析构函数" << endl;
_y = 0;
_m = 0;
_d = 0;
}
private:
int _y;
int _m;
int _d;
};
int main()
{
A day1;
return 0;
}
运行效果:
系统自动生成的析构函数和构造函数类似:
- 内置类型成员不做处理
- 自定义类型会去调用他的析构函数
- 一般情况下,如果有动态申请的资源那就要显式写析构函数释放资源
- 如果没有动态申请的资源或者需要释放的成员都是自定义类型,就不用写析构
拷贝
浅拷贝
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用
拷贝构造函数也是特殊的成员函数
拷贝构造函数是构造函数的一个重载形式
class Date
{
public:
Date(int year = 2023, int month = 11, int day = 4)
{
_year = year;
_month = month;
_day = day;
}
//错误写法Date(Date d)
Date(Date& d)//正确写法
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date day1;
Date day2(day1);
return 0;
}
C++规定了,内置类型直接拷贝,自定义类型必须调用拷贝构造完成拷贝
拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用
所以拷贝构造必须传引用或指针,否则就会无限递归
运行效果:
若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝
- 内置类型成员完成值拷贝/浅拷贝
- 自定义类型成员会调用他的拷贝构造
没有写拷贝构造函数
class Date
{
public:
Date(int year = 2023, int month = 11, int day = 4)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date day1(2024,1,1);
Date day2(day1);
return 0;
}
运行效果:
那我们能不能直接用编译器默认生成的拷贝构造函数呢?
并不行,因为默认生成的只是完成了浅拷贝
比如以下这段代码,无法正常运行
class Stack
{
public:
Stack(int capacity = 10)
{
_a = (int*)malloc(sizeof(int) * capacity);
if (_a == nullptr)
{
perror("malloc失败");
return;
}
_capacity = capacity;
_top = 0;
}
~Stack()
{
free(_a);
_a = nullptr;
_capacity = 0;
_top = 0;
}
private:
int* _a = nullptr;
int _top = 0;
int _capacity;
};
int main()
{
Stack st1;
Stack st2(st1);
return 0;
}
可以看到两个对象的_a都是指向的同一块空间,这样会发生什么问题呢?
st2出了作用域会调用一次析构函数 st1出了作用域也会调用一次析构函数,同一块空间就被析构了两次,这就是导致程序崩溃的原因
另一个问题是其中一个对象的修改会影响另一个对象
这时候就必须自己实现深拷贝
深拷贝
出现崩溃的原因是因为都指向了同一块空间,那么我们就必须让两个对象指向不同的空间
//自己实现
Stack(const Stack& st)
{
_a = (int*)malloc(sizeof(int) * st._capacity);
if (_a == nullptr)
{
perror("malloc失败");
return;
}
memcpy(_a, st._a, sizeof(int) * st._top);
_top = st._top;
_capacity = st._capacity;
}
运行效果:
运算符重载
如果我们有2个日期,我们怎么比较他们两个的大小?
class Date
{
public:
Date(int year = 2023, int month = 11, int day = 4)
{
_year = year;
_month = month;
_day = day;
}
int _year;
int _month;
int _day;
};
正常情况我们会写一个函数来比较他们的大小
//函数里不修改参数最好加上const
bool A(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 && d1._day < d2._day)
{
return true;
}
return false;
}
bool B(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 && d1._day > d2._day)
{
return true;
}
return false;
}
int main()
{
Date day1(2023, 11, 4);
Date day2(2024, 1, 1);
cout << A(day1, day2) << endl;
cout << B(day1, day2) << endl;
//无法直接比较
cout << day1 < day2 << endl;
return 0;
}
那么上面的函数有没有什么问题呢?
有,我们无法通过函数名来判断该函数是进行日期比较的,只能观察函数内部来判断
为什么内置类型可以直接比较,自定义类型不可以
因为编译器知道内置类型该如何比较,但是自定义类型是我们自己定义的,所以编译器不知道该如何比较,只有我们自己知道
因此C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似
关键字: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 && d1._day < d2._day)
{
return true;
}
return false;
}
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 && d1._day > d2._day)
{
return true;
}
return false;
}
int main()
{
Date day1(2023, 11, 4);
Date day2(2024, 1, 1);
//流插入/流提取的优先级较高,所以需要加上括号
cout << (day1 < day2) << endl;
cout << (day1 > day2) << endl;
//实际上编译器会转换成,我们也可以这样写,但是没有意义,因为这样写没有可读性
cout << operator<(day1, day2) << endl;
cout << operator>(day1, day2) << endl;
return 0;
}
操作符是几个操作数,重载函数就有几个参数,是否要重载运算符,取决于运算符对这个类是否有意义
那如果数据是私有/保护的我们该如何进行比较呢?
最好的方式是把函数写成成员函数:
class Date
{
public:
Date(int year = 2023, int month = 11, int day = 4)
{
_year = year;
_month = month;
_day = day;
}
//需要注意的是,左操作数是this,指向调用函数的对象,
//因此不能写成bool operator<(const Date& d1, const Date& d2)
bool operator<(const Date& d2)
{
if (_year < d2._year)
{
return true;
}
else if (_year == d2._year && _month < d2._month)
{
return true;
}
else if (_year == d2._year && _month == d2._month && _day < d2._day)
{
return true;
}
return false;
}
bool operator>(const Date& d2)
{
if (_year > d2._year)
{
return true;
}
else if (_year == d2._year && _month > d2._month)
{
return true;
}
else if (_year == d2._year && _month == d2._month && _day > d2._day)
{
return true;
}
return false;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date day1(2023, 11, 4);
Date day2(2024, 1, 1);
cout << (day1 < day2) << endl;
cout << (day1 > day2) << endl;
//实际转换为
//d1.operator<(d2);
//d1.operator>(d2);
return 0;
}
注:
不能通过连接其他符号来创建新的操作符:比如operator@
重载操作符必须有一个类类型参数
用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义
作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
以上5个运算符不能重载
赋值运算符重载
赋值运算符重载是对已经存在的两个对象之间的复制拷贝
构造函数是用一个已经存在的对象初始化另一个对象,不要把这两个搞混了
class Date
{
public:
Date(int year = 2023, int month = 11, int day = 4)
{
_year = year;
_month = month;
_day = day;
}
void operator=(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date day1(2023, 11, 4);
Date day2(2024, 1, 1);
//已经存在的两个对象之间的复制拷贝 -- 运算符重载函数
day1 = day2;
return 0;
}
以上函数并不能处理连续赋值
day3 = day4 = day1;
出错的原因是因为我们的函数返回的是void,而想要解决此类问题我们应该对函数进行改造,让他有一个正确的返回值
//this是day4的地址
Date& operator=(const Date& d)
{
//如果不想出现day1 = day1(自己给自己赋值)就加上判断
if(this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
参数类型:const T&,传递引用可以提高传参效率
返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
检测是否自己给自己赋值
返回*this :要复合连续赋值的含义
赋值运算符只能重载成类的成员函数不能重载成全局函数
用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝
默认生成的赋值重载跟拷贝构造行为一样:
- 内置类型成员 – 值拷贝/浅拷贝
- 自定义类型成员会去调用他的赋值重载
所以和拷贝构造一样,有些情况需要自己写,有些情况则不需要
知识总结实现一个日期类
Date.h
#pragma once
#include<iostream>
#include<assert.h>
using namespace std;
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1);
//成员函数加上const之后,普通对象和const对象都可以调用
//只要成员函数内部不修改成员变量,都应该加上const
//这样const对象和普通对象都可以调用
void Print() const
{
cout << _year << "." << _month << "." << _day << endl;
}
bool operator<(const Date& d2) const;
bool operator==(const Date& d2) const;
bool operator<=(const Date& d2) const;
bool operator>(const Date& d2) const;
bool operator>=(const Date& d2) const;
bool operator!=(const Date& d2) const;
int GetMonthDay(int year, int month);
//日期+=天数/日期+天数
Date& operator+=(int day);
Date operator+(int day) const;
//日期-=天数/日期-天数
Date& operator-=(int day);
Date operator-(int day) const;
//注意前置++和后置++
Date& operator++();
Date operator++(int);
//注意前置--和后置--
Date& operator--();
Date operator--(int);
//日期-日期
int operator-(const Date& d) const;
private:
int _year;
int _month;
int _day;
};
Date.cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include"Date.h"
Date::Date(int year, int month, int day)
{
if(month > 0 && month < 13
&&day > 0 && day <= GetMonthDay(year,month))
{
_year = year;
_month = month;
_day = day;
}
else
{
cout << "非法日期" << endl;
assert(false);
}
}
bool Date::operator<(const Date& d2) const
{
if (_year < d2._year)
{
return true;
}
else if (_year == d2._year && _month < d2._month)
{
return true;
}
else if (_year == d2._year && _month == d2._month && _day < d2._day)
{
return true;
}
return false;
}
bool Date::operator==(const Date& d2) const
{
return _year == d2._year
&& _month == d2._month
&& _day == d2._day;
}
bool Date::operator<=(const Date& d2) const
{
//复用小于或等于
return *this < d2 || *this == d2;
}
bool Date::operator>(const Date& d2) const
{
//<=(结果)取反
return !(*this <= d2);
}
bool Date::operator>=(const Date& d2) const
{
//<(结果)取反
return !(*this < d2);
}
bool Date::operator!=(const Date& d2) const
{
//==(结果)取反
return !(*this == d2);
}
int Date::GetMonthDay(int year, int month)
{
//函数需要频繁调用,直接创建静态数组就不需要每次都创建数组了
static int daysArr[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 daysArr[month];
}
}
Date& Date::operator+=(int day)
{
if (day < 0)
{
return *this -= abs(day);
}
_day += day;
//判断是否大于当前月的天数
while (_day > GetMonthDay(_year, _month))
{
//如果大于,减去当前月的天数
_day -= GetMonthDay(_year, _month);
++_month;
//判断月是否大于12
if (_month == 13)
{
++_year;
_month = 1;
}
}
return *this;
}
Date Date::operator+(int day) const
{
Date tmp(*this);
tmp += day;
return tmp;
}
Date& Date::operator-=(int day)
{
if (day < 0)
{
return *this += abs(day);
}
_day -= day;
//判断天数是否小于0
while (_day <= 0)
{
//注意借位借的是上一个月的
_month--;
//判断月是否为0月
if (_month == 0)
{
_month = 12;
_year--;
}
_day += GetMonthDay(_year, _month);
}
return *this;
}
Date Date::operator-(int day) const
{
Date tmp = *this;
tmp -= day;
return tmp;
}
//前置++
Date& Date::operator++()
{
//前置++返回++以后的对象
//后置++返回++之前的对象
*this += 1;
return *this;
}
//后置++
//增加int参数是为了占位,跟前置++构成重载
//编译器遇到++会转换:
//++day1 -> d1.operator++()
//day1++ -> d1.operator++(0)
Date Date::operator++(int)
{
Date tmp = *this;
*this += 1;
return tmp;
}
Date& Date::operator--()
{
*this -= 1;
return *this;
}
//这里的int和++同理
Date Date::operator--(int)
{
Date tmp = *this;
*this -= 1;
return tmp;
}
int Date::operator-(const Date& d) const
{
//默认左边大
Date max = *this;
Date min = d;
int flag = 1;
//如果是右边大,那么减出来的数为负数
//需要乘-1
if (*this < d)
{
max = d;
min = *this;
flag = -1;
}
int n = 0;
while (min != max)
{
++min;
++n;
}
return n * flag;
}
test.cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include"Date.h"
int main()
{
Date day1(2023, 11, 7);
Date ret1 = day1++;
ret1.Print();
day1.Print();
Date day2(2023, 11, 7);
day2 += 100;
day2.Print();
day2 -= 100;
day2.Print();
return 0;
}
运行效果:
剩下的功能由大家自己去测试了
番外(流插入和流提取)
为什么流插入/流提取可以自动识别类型呢?
其实是库里面实现了,可以自动识别类型是因为函数重载
//该函数在成员函数里面
void Date::operator<<(ostream& out)
{
out << _year << "年" << _month << "月" << _day << "日" << endl;
}
int main()
{
Date day1(2023, 11, 7);
day1 << cout; // day1.operator<<(cout);
return 0;
}
这样就可以对自定义对象进行打印了,但是打印的时候会不会有些别扭?怎么是反过来的,原因是流插入不能写成成员函数(写成成员函数也能正常运行,但是用起来会特别别扭),只能写全局
流插入是双操作数的
流插入不能写成成员函数
因为Date对象默认占用了第一个参数做了左操作数
如果在成员函数,那么写出来就是下面这样,能正常运行,但是不符合使用习惯
day1 << cout; // 转换day1.operator<<(cout);
但是写在全局我们如何访问私有的内容呢?
有2种方法,一种是写几个共有的成员函数来获取私有的数据如下,另一种方法是使用友元函数(下篇会讲解,这里先做演示)
Date.cpp
//注:ostream不能加const,因为流插入是在往out里插入东西,const就不能插入了
ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
return out;
}
//流提取两边都不能加const
istream& operator>>(istream& in, Date& d)
{
int year, month, day;
in >> year >> month >> day;
if (month > 0 && month < 13
&& day > 0 && day <= d.GetMonthDay(year, month))
{
d._year = year;
d._month = month;
d._day = day;
}
else
{
cout << "非法日期" << endl;
assert(false);
}
return in;
}
Date.h
//Date.h
class Date
{
//友元函数声明
friend ostream& operator<<(ostream& out, const Date& d);
friend istream& operator>>(istream& in, Date& d);
public:
Date(int year = 1, int month = 1, int day = 1);
void Print() const
{
cout << _year << "." << _month << "." << _day << endl;
}
bool operator<(const Date& d2) const;
bool operator==(const Date& d2) const;
bool operator<=(const Date& d2) const;
bool operator>(const Date& d2) const;
bool operator>=(const Date& d2) const;
bool operator!=(const Date& d2) const;
int GetMonthDay(int year, int month);
//日期+=天数/日期+天数
Date& operator+=(int day);
Date operator+(int day) const;
//日期-=天数/日期-天数
Date& operator-=(int day);
Date operator-(int day) const;
//注意前置++和后置++
Date& operator++();
Date operator++(int);
//注意前置--和后置--
Date& operator--();
Date operator--(int);
//日期-日期
int operator-(const Date& d) const;
private:
int _year;
int _month;
int _day;
};
//需要返回cout才能连续流插入
ostream& operator<<(ostream& out,const Date& d);
//流提取两边都不能加const
istream& operator>>(istream& in, Date& d);
test.cpp
int main()
{
Date day1(2023, 11, 7);
cout << day1;//operator<<(cout,day1);
Date day2(2023, 11, 8);
Date day3(2023, 11, 9);
Date day4(2023, 11, 10);
cout << day2 << day3 << day4;
cin >> day1 >> day2;
cout << day2 << day1;
return 0;
}
运行效果:
以上就是本篇文章的全部内容了,希望大家看完能有所收获