类和对象(下)
前言
构造函数、析构函数、赋值重载、初始化列表、匿名对象…慢慢扩展
零、类的6个默认成员函数
如果一个类中什么成员都没有,简称为空类。
空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员
函数。
默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数
class Date {};
一、构造函数
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。
其特性如下:
1.函数名与类名相同。
2.无返回值。
3.对象实例化时编译器自动调用对应的构造函数。
4.构造函数可以重载。
(本质就是可以写多个构造函数,提供多种初始化方式)
#include <iostream>
using namespace std;
class Date
{
public:
Date() //无参构造函数
{}
Date(int year, int mon, int day) //带参构造函数
{
this->_year = year;
_mon = mon;
_day = day;
//cout << this << endl; //构造函数也有this指针
}
Date(int year=2024, int mon=2, int day=3) //全缺省构造函数
{
this->_year = year;
_mon = mon;
_day = day;
}
void Print()
{
cout << _year << '/' << _mon << '/' << _day << endl;
}
private:
int _year;
int _mon;
int _day;
};
int main()
{
//Date d0(); // x 没有参数的话不要加"()",因为编译器分不清d0是一个对象还
//是一个函数的声明。
//d0.Print(); // x
Date d1; //调用无参构造函数
d1.Print();
Date d2(2024,3,30); //调用带参构造函数
d2.Print();
Date d3(2024); //可以和全缺省函数相结合
d3.Print();
return 0;
}
构造函数,也是默认成员函数,我们不写,编译器会自动生成
编译生成的默认构造的特点:
1、我们不写才会生成,我们写了任意一个构造函数就不会生成了
2、内置类型的成员不会处理
3、自定义类型的成员才会处理,回去调用这个成员的构造函数
class Date
{
public:
//Date(int year=2024, int mon=2, int day=3)
//{
// this->_year = year;
// _mon = mon;
// _day = day;
//} //不显式写构造,使用编译器生成的默认构造函数
void Print()
{
cout << _year << '/' << _mon << '/' << _day << endl;
}
private:
int _year;
int _mon;
int _day;
};
int main()
{
Date d1; //这里必须调用构造函数,不显式写,则调用默认的构造函数
d1.Print();
}
输出为:随机值,如下图
因此可以知道对于内置类型的成员并不会处理 ,调用了编译器自动生成的默认构造之后依旧是随机值,但是编译器生成默认的构造函数会对自定义类型成员调用它的默认成员函数。
代码如下
class Time
{
public:
Time()
{
cout << "Time()" << endl;
_hour = 0;
_minute = 0;
_second = 0;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
public:
void Print()
{
cout << _year << '/' << _month << '/' << _day << endl;
}
private:
// 基本类型(内置类型)
int _year;
int _month;
int _day;
// 自定义类型
Time _t;
};
int main()
{
Date d;
d.Print();
return 0;
}
C++把类型分成内置类型(基本类型)和自定义类型。
内置类型:就是语言提供的数据类型,如:int、char、double…
自定义类型:就是我们通过class/struct/union等等定义出来的类型
针对
2、内置类型的成员不会处理
这一点在C++11中,声明支持给缺省值
class Date
{
public:
void Print()
{
cout << _year << '/' << _mon << '/' << _day << endl;
}
private:
int _year = 2;
int _mon = 3;
int _day = 4;
};
int main()
{
Date d5;
d5.Print();
return 0;
}
无参的构造函数和全缺省的构造函数都称为默认构造函教,并且默认构造函数只能有一个。
注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数.
Date d1;调用无参的构造函数:包括3个
1、无参构造函数
2、全缺省构造函数
3、编译器默认生成的构造函数
1、以上构造函数多个并存时,会存在调用二义性
2、默认构造函数:不参数就可以调用的
同时,构造函数可以声明和定义分离,但是:
缺省参数:缺省参数声明和定义不能同时存在!
不加参数的时候,d5后面不要加括号,因为编译器分不清它是定义还是声明。
如果 int后加了引用&,可以在int前加上const。因为别人有可能修改返回值的
二、析构函数
析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
- 析构函数名是在类名前加上字符 ~。
- 无参数无返回值类型。
- 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构
函数不能重载 - 对象生命周期结束时,C++编译系统系统自动调用析构函数。
自己写的析构函数,代码如下:
typedef int DataType;
class Stack
{
public:
Stack(size_t capacity = 3)
{
_array = (DataType*)malloc(sizeof(DataType) * capacity);
if (NULL == _array)
{
perror("malloc申请空间失败!!!");
return;
}
_capacity = capacity;
_size = 0;
}
void Push(DataType data)
{
// CheckCapacity();
_array[_size] = data;
_size++;
}
// 其他方法...
~Stack()
{
if (_array)
{
free(_array);
_array = NULL;
_capacity = 0;
_size = 0;
}
}
private:
DataType* _array;
int _capacity;
int _size;
};
void TestStack()
{
Stack s;
s.Push(1);
s.Push(2);
}
编译器自动生成的析构函数,会做什么事情呢?代码如下:
class Time
{
public:
~Time()
{
cout << "~Time()" << endl;
}
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;
}
程序运行结束后输出:~Time()
在main方法中根本没有直接创建Time类的对象,为什么最后会调用Time类的析构函数?
因为:main方法中创建了Date对象d,而d中包含4个成员变量,其中_year, _month,_day三个是内置类型成员,销毁时不需要资源清理,最后系统直接将其内存回收即可;而_t是Time类对象,所以在d销毁时,要将其内部包含的Time类的_t对象销毁,所以要调用Time类的析构函数。但是:main函数中不能直接调用Time类的析构函数,实际要释放的是Date类对象,所以编译器会调用Date类的析构函数,而Date没有显式提供,则编译器会给Date类生成一个默认的析构函数,目的是在其内部调用Time类的析构函数,即当Date对象销毁时,要保证其内部每个自定义对象都可以正确销毁,main函数中并没有直接调用Time类析构函数,而是显式调用编译器为Date类生成的默认析构函数
注意:
创建哪个类的对象则调用该类的构造函数,销毁哪个类的对象则调用该类的析构函数
总结
1、内置类型不做处理,自定义类型调用这个成员的析构函数
2、一般情况都需要我们自己写构造函数,决定初始化方式 成员变量全是自定义类型,可考虑不写构造函数
三、拷贝构造函数
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存
在的类类型对象创建新对象时由编译器自动调用。
拷贝构造函数也是特殊的成员函数,其特征如下:
- 拷贝构造函数是构造函数的一个重载形式。
- 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。
代码如下:
class Date
{
public:
Date(int year = 1970,int month = 2,int day = 8)
{
_year = year;
_month = month;
_day = day;
}
Date(Date d) //错误写法
{
_year = d._year;
_month = d._month;
_day = d._day;
cout << "Date(const Date& d)" << endl;
}
void Print()
{
cout << _year <<'/' << _month << '/' << _day << endl;
}
private:
// 基本类型(内置类型)
int _year;
int _month;
int _day;
};
void func(Date d)
{
d.Print();
}
void func1(Stack s)
{
}
int main()
{
Date d1;
Date d2(d1);
return 0;
}
虽然上图中称不加&也是可以的, 但是会又要开空间调用拷贝构造,没有必要,因此尽量还是要加上&。出了作用域返回对象还在不在,在就可以用引用返回
因此这里要注意
- 拷贝构造:是一个已经存在的对象去初始化另一个要创建的对象
Date d1(2029,7,21);
Date d2(d1);
- 赋值:两个已经存在的对象进行拷贝
Date d3(2229,5,21);
d3 = d1;
1、
2、
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
class Date
{
public:
Date(int year = 1970,int month = 2,int day = 8)
{
_year = year;
_month = month;
_day = day;
}
Date(const Date& d)//正确写法
{
_year = d._year;
_month = d._month;
_day = d._day;
cout << "Date(const Date& d)" << endl;
}
~Date()
{
_year = 0;
_month = 0;
_day = 0;
}
void Print()
{
cout << _year <<'/' << _month << '/' << _day << endl;
}
private:
// 基本类型(内置类型)
int _year;
int _month;
int _day;
};
void func1(Date d)
{
d.Print();
}
int main()
{
Date d(2023,7,21);
func1(d);
return 0;
}
func1调用,调用之前要先传参,传参调用拷贝构造,拷贝构造调用完了回来继续调用func1;
- C++规定,自定义类型,传值传参必须调用拷贝构造!
- 为防止无限循环,必须要用 引用&
- 加const作用在于→
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
typedef int DataType;
class Stack
{
public:
Stack(size_t capacity = 3)
{
_array = (DataType*)malloc(sizeof(DataType) * capacity);
if (NULL == _array)
{
perror("malloc申请空间失败!!!");
return;
}
_capacity = capacity;
_size = 0;
}
// s1(s) //将提前写好的Stack的拷贝构造函数注释掉
//Stack(Stack& s)
//{
// cout << "Stack(Stack& s)" << endl;
// // 深拷贝
// _array = (DataType*)malloc(sizeof(DataType) * s._capacity);
// if (NULL == _array)
// {
// perror("malloc申请空间失败!!!");
// return;
// }
// memcpy(_array, s._array, sizeof(DataType) * s._size);
// _size = s._size;
// _capacity = s._capacity;
//}
void Push(DataType data)
{
// CheckCapacity();
_array[_size] = data;
_size++;
}
~Stack()
{
cout << "~Stack()" << endl;
free(_array);
_array = nullptr;
_size = _capacity = 0;
}
private:
// 内置类型
DataType* _array;
int _capacity;
int _size;
};
void func2(Stack s)
{
;
}
int main()
{
Stack s1;
func2(s1);
return 0;
}
运行结果如图
程序报错:
原因:_array被free了2次,func2先结束,因此func2先调用析构函数,main中的s1又调用析构函数,空间被释放2次 (s把指向的空间释放掉了,虽然置空了,但同时s1指向的空间被释放了,成为野指针了)
此时引用可以解决这个问题:
void func2(Stack& s)
{
s.Push(1);
s.Push(2);
}
但是期望的结果是s的改变并不要影响到s1,因此要写出对象Stack的拷贝构造函数(深拷贝),也就是上述代码中被注释掉的部分
//s1(s)
Stack(Stack& s)
{
cout << "Stack(Stack& s)" << endl;
// 深拷贝
_array = (DataType*)malloc(sizeof(DataType) * s._capacity);
if (NULL == _array)
{
perror("malloc申请空间失败!!!");
return;
}
memcpy(_array, s._array, sizeof(DataType) * s._size);
_size = s._size;
_capacity = s._capacity;
}
总结
我们不写,编译默认生成的拷贝构造,跟之前的构造函数特性不一样
1、内置类型, 值拷贝
2、自定义的类型,调用他的拷贝
Date不需要我们实现拷贝构造,默认生成就可以用
stack需要我们自己实现
深拷贝的拷贝构造,默认生成会出问题(在释放资源的时候)
四、赋值运算符重载
1.运算符重载
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其
返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
- 不能通过连接其他符号来创建新的操作符:比如operator@
- 重载操作符必须有一个类类型参数
- 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义
- 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
- .* :: sizeof ?: . 注意以上5个运算符不能重载。
class Date
{
public:
Date(int year = 1970, int month = 2, int day = 8)
{
_year = year;
_month = month;
_day = day;
}
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
cout << "Date(const Date& d)" << endl;
}
//重载 "="
Date& operator=(const Date& d)//"="双操作数,有个this参数,所有有一个参数就要省略掉
{
//d1 == d2 d1就是this,d2就是d
_year = d._year;
_month = d._month;
_day = d._day;
cout << "Date& operator==(const Date& d)" << endl;
return *this;
}
//重载 "=="
bool operator==(const Date& d)
{
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
bool operator != (const Date& d)
{
return !(*this == d);
}
//重载 "<"
//为了解决private变量不让访问的问题,把operator写成成员函数,如下 d1就是this,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;
}
}
bool operator > (const Date& d)
{
return !(*this < d);
}
bool operator <= (const Date& d) //直接复用"<" "=="的重载
{
return *this < d || *this == d;
}
~Date()
{
_year = 0;
_month = 0;
_day = 0;
}
void Print()
{
cout << _year << '/' << _month << '/' << _day << endl;
}
int GetMonthDay(int year, int month)
{
int monthArray[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;
}
return monthArray[month];
}
Date& operator+=(int day)
{
_day += day;
while (_day > GetMonthDay(_year, _month))
{
// 月进位
_day -= GetMonthDay(_year, _month);
++_month;
// 月满了
if (_month == 13)
{
++_year;
_month = 1;
}
}
return *this;
}
Date operator+(int day)
{
Date tmp(*this);
tmp += day;
return tmp;
}
private:
// 基本类型(内置类型)
int _year;
int _month;
int _day;
// 自定义类型
//Time _t;
};
void func2(Stack& s)
{
s.Push(1);
s.Push(2);
}
int main()
{
Date d(2023,7,21);
Date d1(2024, 6, 4);
d1 = d;
d1.Print();
Date d2(2024, 6, 5);
cout << (d == d2) << endl;
cout <<(d1 < d2)<< endl;//(d1 < d2)要加括号,因为<<优先级要比 < 高
cout << (d1.operator < (d2)) << endl;//也√
cout << (d <= d1) << endl;
Date d3(d2);
d2 += 1;
d2.Print();
d3 = d2 + 1;
d3.Print();
return 0;
}
2.赋值运算符重载
-
赋值运算符重载格式
参数类型:const T&,传递引用可以提高传参效率
返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值检测是否自己给自己赋值
返回*this :要符合连续赋值的含义 -
赋值运算符只能重载成类的成员函数不能重载成全局函数
-
用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。
注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符 重载完成赋值
上述日期类的赋值运算符重载
Date& operator=(const Date& d)
{
//d1 == d2 d1就是this,d2就是d
_year = d._year;
_month = d._month;
_day = d._day;
cout << "Date& operator==(const Date& d)" << endl;
return *this;
}
我们可以重载赋值运算符。不论形参的类型是什么,赋值运算符都必须定义为成员函数。
还有如果类中未涉及到资源管理,赋值运算符是否实现都可以(例如日期类可不实现赋值运算符的重载);一旦涉及到资源管理则必须要实现。
3.前置++和后置++重载
1.前置++重载
前置++:返回+1之后的结果
!!!:this指向的对象函数结束后不会销毁,故以引用方式返回提高效率
代码:
//前置++重载
Date& operator++()
{
_day += 1;
return *this;
}
2.后置++重载
后置++是先使用后+1,因此需要返回+1之前的旧值,故需在实现时需要先将this保存一份,然后给this+1,而temp是临时对象,因此只能以值的方式返回,不能返回引用(this指向的对象函数结束后销毁)
后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器自动传递
代码:
//后置++重载
Date operator++(int) //多增加一个int类型的参数
{
Date tmp(*this);
_day += 1;
return tmp;
}
总结
结束!~;