1.类的6个默认成员函数
-
如果一个类中什么成员都没有,简称为空类。
-
空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。
-
默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。
默认成员函数是一种特殊成员函数:
1**.我们不写,编译器会自己生成一个;我们写了,编译器就不会生成。(例如构造函数,析构函数)**
2.隐含意思:对于有些类,需要我们自己写;对于另一些类,编译器默认生成就可以用。
class Date {};
2. 构造函数
2.1 概念
对于下面的Date类:
class Date
{
public:
/*void Init(int year, int month, int day) // 普通的初始化函数
{
_year = year;
_month = month;
_day = day;
}*/
// 初始化(构造函数)
// 1.带参构造函数
// 可以使用缺省;使用缺省参数就可以不用传参
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
/*
// 构造函数可以重载,但是对于这两个构造函数,如果不传参数,会造成二义性(因为两个构造函数,都可以不用进行传参,但是语法上这么写是正确的,但是函数调用时,会有二义性)
// 2.无参构造函数
Date()
{
_year = 1;
_month = 1;
_day = 1;
}
*/
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
int main()
{
Date d1(2022, 9, 21); // 给构造函数传参,在创建的对象后写需要传递的参数
Date d2(2022, 9, 21);
Date d3; // 不传参的形式
// 无参的不要像下面这样写
// Date d4();
// Date func(); // 编译器无法判断func()是函数还是对象
d1.Print();
d2.Print();
d3.Print();
return 0;
}
-
对于
Date
类,可以通过Init
公有方法给对象设置日期,但如果每次创建对象时都调用该方法设置信息,未免有点麻烦,那能否在对象创建时,就将信息设置进去呢? -
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次。
2.2 特性
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。
其特征如下:
-
函数名与类名相同。
-
无返回值。
-
对象实例化时编译器自动调用对应的构造函数。
-
构造函数可以重载。
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函数(并不是d3对象),该函数无参,返回一个日期类型的对象
// warning C4930: “Date d3(void)”: 未调用原型函数(是否是有意用变量定义的?)
Date d3(); // 错误演示
}
int main()
{
TestDate();
return 0;
}
// case1:
class Stack
{
public:
// 构造函数
Stack(int capacity = 4)
{
_a = (int*)malloc(sizeof(int)*capacity);
if (_a == nullptr)
{
perror("malloc fail");
exit(-1);
}
_top = 0;
_capacity = capacity;
}
~Stack() // 析构函数
{
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
void Push(int x)
{
// ....
// 扩容
_a[_top++] = x;
}
private:
int* _a;
int _top;
int _capacity;
};
int main()
{
// 有了构造函数和析构函数,我们不在需要主动对创建的对象进行初始化; 而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作(和Destory的功能类似)。
Stack st;
st.Push(1);
st.Push(2);
st.Push(3);
return 0;
}
- 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
class Date
{
public:
/*
// 如果用户显式定义了构造函数,编译器将不再生成
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
*/
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
// 将Date类中我们自己定义的构造函数屏蔽后,代码可以通过编译,因为编译器生成了一个无参的默认构造函数
// 将Date类中我们自己定义构造函数放开(则不会生成默认构造函数),代码编译失败(由于自己写的构造函数需要传参,但Date d1并没有传参,因此匹配不到合适的构造函数),因为一旦显式定义任何构造函数,编译器将不再生成
// 无参构造函数,放开后报错:error C2512: “Date”: 没有合适的默认构造函数可用
Date d1;
return 0;
}
-
关于编译器生成的默认成员函数,很多人会有疑惑:不实现构造函数的情况下,编译器会生成默认的构造函数。但是看起来默认构造函数又没什么用?d对象调用了编译器生成的默认构造函数,但是d对象
year/_month/_day
,依旧是随机值。也就说在这里编译器生成的默认构造函数并没有什么用??解答:C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型,如:
int/char...
,自定义类型就是我们使用class/struct/union
等自己定义的类型,看看下面的程序,就会发现编译器生成默认的构造函数会对自定类型成员_t
调用的它的默认构造函数。
// case1:编译器生成默认的构造函数会对自定类型成员 _t 调用的它的默认构造函数
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的自定义类型,不需要我们来写构造函数,编译器会直接调用默认构造函数)
// 但是内置类型又无法处理
// 因此,需要用到初始化列表 ==> 类和对象下篇再解释初始化列表
Time _t;
};
int main()
{
Date d;
return 0;
}
// case2:编译器生成默认的构造函数会对自定类型成员_pushST,_popST调用的它的默认构造函数
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; 如果myQueue还存在一个内置类型,又该怎么办呢?
};
// 面向需求:编译器默认生成就可以满足,就不用自己写,不满足就需要自己写
// Stack的构造函数需要自己写
// MyQueue构造函数就不需要自己写,默认生成就可以用
// Stack的析构函数,需要我们自己写
// MyQueue 就不需要自己写析构函数,默认生成就可以用
int main()
{
MyQueue mq;
mq.push(1);
mq.push(2);
return 0;
}
注意:C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在类中声明时可以给默认值。
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;
}
再例如:
class Stack
{
public:
/*
// 如果构造函数的参数不是全缺省值,则不是默认构造函数,编译器会报错
// 在class MyQueue中,会调用Stack初始化Stack _pushST;Stack _popST, 但是Stack的构造函数不是全缺省值,所以会报错
Stack(int capacity)
{
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;
}
// 再声明内置类型时,给到缺省值,则相当于默认构造
private:
int* _a = nullptr;
int _top = 0;
int _capacity = 0;
/*
// 只有创建对象之后,才会调用默认构造函数,才会malloc空间
// 下面这种方法无法检测malloc是否成功开辟空间
int* _a = (int*)malloc(sizeof(int)*4);
int _top = 0;
int _capacity = 4;
*/
};
// 用两个栈来实现一个队列
class MyQueue {
public:
/*
// 初始化列表 -- 类和对象下篇再讲
MyQueue(size_t capacity = 8)
:_popST(8)
, _pushST(8)
, _size(0)
{
_size = 0;
}
*/
void push(int x)
{
//_pushST.Push(x);
}
private:
// 调用Stack的默认构造函数来初始化
Stack _pushST;
Stack _popST;
// 下面这种在定义时给缺省值的方式,不建议(建议使用初始化列表,后续讲解)
// 这里不是初始化,给的缺省值,但并不会开辟空间(则默认构造函数不用考虑这个内置类型,默认构造函数就会使用其进行初始化)
size_t _size = 0;
};
int main()
{
MyQueue q;
return 0;
}
7.无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。
注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数。(也就是不需要传参数,就可以调用的构造函数,就叫默认构造函数)
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;
};
// 以下测试函数能通过编译吗?
void Test()
{
Date d1;
}
// 答:不能够通过测试,因为无参构造函数和全缺省构造函数都可以接收参数,因此,由于d1对象,没有传参,导致二义性(也就是没有默认构造函数)
3.析构函数
3.1 概念
通过前面构造函数的学习,我们知道一个对象是怎么来的,那一个对象又是怎么没呢的?
- 析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
3.2 特性
-
析构函数名是在类名前加上字符 ~。
-
无参数无返回值类型。
-
一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载
-
对象生命周期结束时,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);
}
int main()
{
TestStack();
return 0;
}
5.关于编译器自动生成的析构函数,是否会完成一些事情呢?下面的程序我们会看到,编译器生成的默认析构函数,对自定类型成员调用它的析构函数。
#include<iostream>
#include <assert.h>
using namespace std;
// 栈的类
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;
}
// 内置类型
private:
int* _a;
int _top;
int _capacity;
};
// 队列的类
class MyQueue
{
public:
void push(int x)
{
//_pushST.Push(x);
}
// 自定义类型
private:
Stack _pushST;
Stack _popST;
};
// 面向需求:编译器默认生成就可以满足,就不用自己写,不满足就需要自己写
// 用两个栈来实现一个队列
// Stack的构造函数需要自己写 (都是内置类型)
// MyQueue构造函数就不需要自己写,默认生成就可以用(都是自定义类型)
// Stack的析构函数,需要我们自己写 (都是内置类型)
// MyQueue 就不需要自己写析构函数,默认生成就可以用 都是自定义类型)
int main()
{
Date d1;
MyQueue q;
return 0;
}
实现上述oj题,深刻体会编译器生成析构函数的作用。
- 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如Date类;有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类。
4. 拷贝构造函数
4.1 概念
在现实生活中,可能存在一个与你一样的自己,我们称其为双胞胎
那在创建对象时,可否创建一个与已存在对象一某一样的新对象呢?
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
4.2 特性
拷贝构造函数也是特殊的成员函数,其特征如下:
-
拷贝构造函数是构造函数的一个重载形式。
-
拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为这会引发无穷递归调用。
#include<iostream>
using namespace std;
class Date
{
public:
// 自己写的默认构造函数
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// 拷贝构造函数
Date(const Date& d)
{
cout << "Date 拷贝构造" << endl;
_year = d._year;
_month = d._month;
_day = d._day;
// 形参加const,防止写反了,下面问题就可以检查出来(因为d被const修饰,所以d是无法被修改的)
//d._day = _day;
}
private:
int _year;
int _month;
int _day;
};
void Func1(Date d)
{
cout << "Func1()" << endl;
}
void Func2(Date& d)
{
cout << "Func2()" << endl;
}
int main()
{
// 构造 - 初始化
Date d1(2022, 9, 22);
// 为传值传参,因此传参,需要先调用拷贝函数拷贝d1,再将拷贝的值传参给void Func1(Date d)
Func1(d1);
// 为引用,因此不需要调用拷贝函数
Func2(d1);
// 拷贝一份d1
// 拷贝构造 -- 拷贝初始化
// Date d2(d1);
return 0;
}
// 打印结果
// Date 拷贝构造
// Func1()
// Func2()
思考:为什么调用func1
时,会先调用拷贝函数?
答:由下图可知,将d1
传参,需要先调用拷贝函数 拷贝d1
,再将其传参给Func1(Date d),而Date& d,不需要拷贝传参,d
为d1
的别名,不需要拷贝d1
,也就不需要调用拷贝函数
- 若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// 我们不写拷贝函数,则编译器会默认生成拷贝函数
private:
int _year;
int _month;
int _day;
};
int main()
{
// 构造 - 初始化
Date d1(2022, 9, 22);
// 拷贝一份d1
// 拷贝构造 -- 拷贝初始化
Date d2(d1);
return 0;
}
注意:在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的。
4.编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,还需要自己显式实现吗?当然像日期类这样的类是没必要的。那么下面的类呢?验证一下试试?
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(const Stack& st)
{
_a = st._a;
_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;
};
int main()
{
Stack st1;
st1.Push(1);
st1.Push(2);
//浅拷贝,st1和st2在堆上会共用一块空间;两个对象,因此会调用两次析构函数,所以会报错
Stack st2(st1);
return 0;
}
运行之后我们会发现程序崩溃了
思考:
由下图可以得出:st1和st2共用一块空间,被调用两次析构函数,同一块空间被释放两次,因此会发生报错。
注意:类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请时,则拷贝构造函数是一定要写的,否则就是浅拷贝。
-
我们应该如何解决浅拷贝的问题呢?
-
为了解决浅拷贝,一旦涉及到资源申请时,则拷贝构造函数我们自己需要写,这也就是深拷贝
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(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;
};
// 需要写析构函数的类,都需要写深拷贝的拷贝构造 Stack
// 不需要写析构函数的类,默认生成的浅拷贝的拷贝构造就可以用 Date/MyQueue
class MyQueue {
public:
void push(int x)
{
_pushST.Push(x);
}
private:
Stack _pushST;
Stack _popST;
size_t _size = 0;
};
int main()
{
Stack st1; // 调用默认构造函数
st1.Push(1);
st1.Push(2);
Stack st2(st1); // 调用拷贝函数
st2.Push(10);
// MyQueue为自定义类型,调用默认的拷贝构造函数(如果默认拷贝构造函数,满足需求,则我们不用自己写)
// 调用两次默认构造函数(因为队列,是由两个栈来实现的,所以会调用两次栈的默认构造函数)
MyQueue q1;
// 调用两次拷贝函数(因为队列,是由两个栈来实现的,所以会调用两次栈的默认拷贝函数)
MyQueue q2(q1);
return 0; // 调用6次析构函数
}
// 打印结果如下:
/*
Stack(int capacity = 4)
Stack(const Stack& st)
Stack(int capacity = 4)
Stack(int capacity = 4)
Stack(const Stack& st)
Stack(const Stack& st)
~Stack()
~Stack()
~Stack()
~Stack()
~Stack()
~Stack()
*/
**5.**赋值运算符重载
5.1 运算符重载
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表);如:bool operator==(const Date& d1, const Date& d2)
注意:
-
不能通过连接其他符号来创建新的操作符:比如operator@
-
重载操作符必须有一个类类型参数
-
用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义
-
作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this指针
-
.* :: sizeof ?: . 注意这5个运算符不能重载。
// 全局的operator== ;判断两个日期类是否相等
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//private:
int _year;
int _month;
int _day;
};
// 这里会发现运算符重载成全局的,就需要成员变量是公有的,那么问题来了,封装性如何保证?
bool operator==(const Date& d1, const Date& d2)
{
return d1._year == d2._year
&& d1._month == d2._month
&& d1._day == d2._day;
}
int main()
{
Date d1(2022, 9, 22);
Date d2(2023, 1, 1);
//d1 > d2;
cout << (d1 == d2) << endl; // 程序运行时会转换成operator == (d1, d2);
// 也可以显示调用,一般不这样
operator==(d1, d2);
return 0;
}
// 将operator== 定义在类里面
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// bool operator==(Date* this, const Date& d2)
// 这里需要注意的是,左操作数是this,指向调用函数的对象(==操作符应该刚好是两个操作数)
// 成员函数都是放在公共代码区
bool operator==(const Date& d)
{
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2022, 9, 22);
Date d2(2023, 1, 1);
//d1 > d2;
// (d1 == d2) 一定要加括号,因为<< 的优先级比+高
// 运行时会转换成 d1.operator==(d2)
cout << (d1 == d2) << endl;
// 也可以显示调用,一般不这样
cout << (d1.operator == (d2)) << endl;
return 0;
}
5.2 赋值运算符重载
1.赋值运算符重载格式
参数类型:
const T&
,传递引用可以提高传参效率返回值类型:
T&
,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值检测是否自己给自己赋值
返回*this :要复合连续赋值得含义
我们自己写的运算符重载(日期类)
#include<iostream>
using namespace std;
class Date
{
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;
}
}
// cout << (d1 == d2) << endl;
// 转换成d1.operator==(d2)
// 判断两个日期类是否相等
bool operator==(const Date& d)
{
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
// d1 > 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;
}
return false;
}
// d1 >= d2
bool operator >= (const Date& d)
{
return *this > d || *this == d; // *this 也就是d1
}
// .... < <= != 都可以按照如上的方式来实现
// d1 += 100
// 使用传引用返回,则不会拷贝*this来返回,栈帧销毁后d1还存在
Date& operator += (int day)
{
_day += day;
while (_day > GetMonthDay(_year, _month))
{
_day -= GetMonthDay(_year, _month);
_month++;
if (_month == 13)
{
++_year;
_month = 1;
}
}
return *this;
}
// d1 + 100 不会改变d1的值
// 由于栈帧销毁后,对ret没有访问权限,所以不能够使用传引用返回
// ret是一个临时创建的日期类对象,只在当前运算符重载函数中存活
Date operator+(int day)
{
// 调用拷贝构造函数来创建ret这个日期类对象
Date ret(*this);
ret += day;
return ret;
}
void print()
{
cout << _year << endl;
cout << _month << endl;
cout << _day << endl;
}
// d1 = d2;(赋值重载)
Date& operator=(const Date& d)
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
private:
int _year;
int _month;
int _day;
};
void TestDate1()
{
Date d1;
Date d2(2022, 10, 8);
// 初始化时,可以检测出来为非法日期
Date d3(2022, 2, 40);
// 初始化时,可以检测出来为非法日期
Date d4(2022, 2, 29);
d3.Print();
d4.Print();
}
void TestDate2()
{
Date d0;
Date d1;
Date d2(2022, 10, 8);
// 拷贝构造(初始化) 将d2拷贝来初始化d3
Date d3(d2);
// 拷贝构造(将d2拷贝给d4)
Date d4 = d2;
d1.Print();
// 赋值重载(复制拷贝) 已经存在两个对象之间拷贝
d0 = d1 = d2;
d1.Print();
int i, j;
(i = j = 10)++;
cout << i << " " << j << endl; // 打印结果为 11 10
}
int main()
{
TestDate1();
Date d1(2022, 9, 22);
Date d2(2022, 9, 22);
d1 += 8;
d2 += 100;
Date d3 = d1 + 9;
d1.print();
d3.print();
/*d1 - d2;*/
return 0;
}
系统默认生成的运算符重载(栈)
class Stack
{
public:
// 默认构造函数
Stack(int capacity = 4)
{
cout << "Stack(int capacity = )" <<capacity<<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;
}
// st1 = st2
// 此处我们不自己写运算符重载的函数,使用系统默认生成的运算符重载函数
~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;
};
void TestSatck()
{
Stack st1;
st1.Push(1);
st1.Push(2);
Stack st2(10);
st2.Push(10);
st2.Push(20);
st2.Push(30);
st2.Push(40);
st1 = st2;
}
int main()
{
TestSatck();
return 0;
}
运行之后我们会发现使用默认生成的运算符重载,系统崩溃了
系统为什么会崩溃呢,出现了那些问题?
1.通过下图可知,赋值重载之后,st1和st2中_a的地址是相同的
2.通过详细的分析得出下面的结论
我们自己写的运算符重载(栈)
/*
1.怎样才可以保证不发生如上所说的内存泄漏,且对同一块空间多次析构?
第一步:首先释放st1的空间
第二步:为st1申请一块和st2同样大小的空间
第三步:将st2的数据拷贝到st1中
*/
// st1 = st2;
Stack& operator=(const Stack& st)
{
cout << " Stack& operator=(const Stack& st)" << endl;
// 考虑到会有本身给本身赋值的情况,所以在赋值前,要确保this != &st为真
if (this != &st)
{
// 1.首先释放st1的空间
free(_a);
// 2.为st1申请一块和st2同样大小的空间
_a = (int*)malloc(sizeof(int)*st._capacity);
if (_a == nullptr)
{
perror("malloc fail");
exit(-1);
}
// 3.将st2的数据拷贝到st1中
memcpy(_a, st._a, sizeof(int)*st._top);
_top = st._top;
_capacity = st._capacity;
}
// 将st1对象返回
return *this;
}
- 使用我们自己写的运算符重载,再来运行一下
class Stack
{
public:
Stack(int capacity = 4)
{
cout << "Stack(int capacity = )" <<capacity<<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;
}
// st1 = st2;
// st1 = st1;
Stack& operator=(const Stack& st) // 我们自己写的运算符赋值重载
{
cout << " Stack& operator=(const Stack& st)" << endl;
if (this != &st)
{
free(_a);
_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;
}
return *this;
}
~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);
}
private:
Stack _pushST;
Stack _popST;
size_t _size = 0;
};
void TestSatck()
{
//Stack st1(10000);
// 运行结果证明,自己写的运算符重载,不会再导致程序崩溃
// 默认生成operator= 不行 需要自己实现
Stack st1;
st1.Push(1);
st1.Push(2);
Stack st2(10);
st2.Push(10);
st2.Push(20);
st2.Push(30);
st2.Push(40);
st1 = st2;
st1 = st1;
// MyQueue 默认生成operator= 是可以的,我们不需要自己去写
MyQueue q1;
q1.push(1);
q1.push(2);
q1.push(3);
MyQueue q2;
q2.push(10);
q2.push(20);
q2.push(30);
q1 = q2;
}
int main()
{
TestSatck();
return 0;
}
6.日期类的实现(完善日期类,并将它的声明与定义分离)
Date.h
// Date.h
class Date
{
// 友元声明(这个声明可以写在类的任意位置)
// 有了这个友元声明,那么这两个运算符重载函数就可以访问类内的成员变量和成员函数
friend ostream& operator<<(ostream& out, const Date& d); // 流输出
friend istream& operator>>(istream& in, Date& d); // 流输入
public:
// 这个函数可以得到,年份月份对应的天数,如2024-4,就有30天
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;
}
}
// d1 == d2
bool operator==(const Date& d) const;
// d1 > d2
bool operator>(const Date& d) const;
// d1 >= d2
bool operator>=(const Date& d) const;
// d1 <= d2
bool operator<=(const Date& d) const;
// d1 < d2
bool operator<(const Date& d) const;
// d1 != d2
bool operator!=(const Date& d) const;
// d1 += 100
Date& operator+=(int day);
// d1 + 100
Date operator+(int day) const; // d1是不改变的
// 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;
/*
// 错误演示
// d1.operator<<(cout);
// d1 << cout
// << 和 >> 重载一般不写为成员函数,因为this默认抢了第一个参数的位置,Date对象就是左操作数,不符合使用习惯和可读性; 因此我们可以考虑将其定义为全局变量
// 如果operator<<不是成员函数的话,那么第一个参数就不是this指针,就不需要写为d1 << cout
// 错误演示
// ostream 为流插入的类类型
// 不可以作为成员函数
void operator<<(ostream& out)
{
out << _year << "年" << _month << "月" << _day << "日" << endl;
}
*/
// 取类对象地址的重载
// 要求这个类的对象不让取地址(需要自己写取地址及const取地址操作符重载的一种情况)
Date* operator&()
{
// 自己写的重载,使用&,取类对象的地址,拿到的就是空指针
return nullptr;
//return this; // 只是取类的对象的地址,则不需要自己写,系统默认生成的就可以
}
// 取类对象地址的重载(这个是被const修饰的)
const Date* operator&() const
{
return nullptr;
//return this;
}
private:
int _year;
int _month;
int _day;
};
// 流插入的声明
ostream& operator<<(ostream& out, const Date& d);
// 此处operator运算符重载必须声明与定义分离;因为其是全局的,不是类的成员函数
// 错误演示;如果都定义到Date.h中,由于Date.cpp和test.cpp都包含了Date.h那么他们生成的.o文件中都会包含这个函数,则在编译链接时,就会报错
/*
// 不可以直接定义到Date.h
ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
return out;
}
*/
// 解决方法
// 1.声明与定义分离
// 2.用static来修饰 operator
// 3.定义为内联函数 这样可以放到Date.h
// 注意:将函数定义为全局变量,那么想要访问类里面的私有成员变量,则需要在类里面添加友元声明
/*
// static修饰(则链接时,函数不进入符号表)
static ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
return out;
}
*/
// 内联函数(则链接时,函数不进入符号表)
// 流插入
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
// Date.cpp
#include "Date.h"
// 运算符重载
bool Date::operator==(const Date& d)
{
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
// d1 > d2(被const修饰的运算符重载)
// 这种被const修饰的函数,是专门给被const修饰的对象调用的,普通对象也可以调用,只是不可以进行修改
// 注:最右侧的const是修饰隐藏的this指针的
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;
}
// d1 >= d2
bool Date::operator>=(const Date& d) const
{
// 大于 或上 等于,也就是大于等于,直接复用,不用自己重新写
// 编译器会自行调用 bool Date::operator>(const Date& d) const
// bool Date::operator==(const Date& d)进行重载
return *this > d || *this == d;
}
// d1 <= d2
bool Date::operator<=(const Date& d) const
{
return !(*this > d); // 大于取反,也就是小于等于
}
// d1 < d2
bool Date::operator<(const Date& d) const
{
return !(*this >= d); // 大于等于取反也就是小于
}
// d1 != d2
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 (d > *this)
// 如果 bool operator>(const Date& d) const; 没有加const修饰*this,则程序会报错
// 因为d被const修饰,但是传参过去之后的*this并没有并const修饰,是一种权限放大,因此会报错
// 加上const修饰*this之后;if (d > *this) 和 if (*this < d) 都是可行的
if (*this < d)
//if (d > *this)
{
max = d;
min = *this;
flag = -1;
}
int n = 0;
while (min != max)
{
++n;
++min;
}
return n*flag;
}
// 重载流插入
ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
return out;
}
test.cpp
// test.cpp
#include "Date.h"
void TestDate1()
{
Date d1(2022, 10, 8);
Date d3(d1);
Date d4(d1);
d1 -= 10000;
d1.Print();
Date d2(d1);
/*
// 打印 d2 - 10000 的两种方法
Date d3 = d2 - 10000;
d3.Print();*/
(d2 - 10000).Print();
d2.Print();
d3 -= -10000;
d3.Print();
d4 += -10000;
d4.Print();
}
void TestDate2()
{
Date d1(2022, 10, 8);
Date d2(d1);
Date d3(d1);
Date d4(d1);
(++d1).Print(); // d1.operator++()
d1.Print();
// 后置++,会存在一个占位符
(d2++).Print(); // d2.operator++(1)
d2.Print();
(--d1).Print(); // d1.operator--()
d1.Print();
(d2--).Print(); // d2.operator--(1)
d2.Print();
}
void TestDate3()
{
Date d1(2022, 10, 10);
Date d2(2023, 7, 1);
cout << d2 - d1 << endl;
cout << d1 - d2 << endl;
}
void TestDate4()
{
Date d1, d2;
//cin >> d1; // 流提取
//cout << d1; // 流插入
//d1 << cout; // d1.operator<<(cout);
cin >> d1 >> d2;
cout << d1 << d2 << endl; // operator<<(cout, d1);
// 根据优先级,会先运行cout << d1; 因此operator<<的返回值类型应为ostream&;这样才可以支持d2进行插入
cout << d1 - d2 << endl;
}
void TestDate5()
{
Date d1(2022, 10, 10);
d1.Print();
// 此处详解看下图(被const修饰的类类型对象)
const Date d2(2022, 10, 10);
d2.Print();
//d2 += 10;
cout << d2 + 1000 << endl;
cout << &d1 << endl; // 取地址及const取地址操作符重载
cout << &d2 << endl;
}
int main()
{
TestDate2();
return 0;
}
- 下图是对d2.Print() 的详解
7.const
-
将const修饰的“成员函数"称之为const成员函数,
-
const修饰类成员函数,实际修饰该成员函数隐含的
this
指针(也就是const Date* this),表明在该成员函数中不能对类的任何成员变量进行修改。
// const 在*的左侧是修饰 *this; (*this就是this指针指向的对象)
// const Date* this; Date const* this
// const 在*的右侧修饰 this;
// Date* const this
我们来看看下面的代码:
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << "Print()" << endl;
cout << "year:" << _year << endl;
cout << "month:" << _month << endl;
cout << "day:" << _day << endl << endl;
}
void Print() const
{
cout << "Print()const" << endl;
cout << "year:" << _year << endl;
cout << "month:" << _month << endl;
cout << "day:" << _day << endl << endl;
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
void Test()
{
//调用 void Print()
Date d1(2022, 1, 13);
d1.Print();
//调用 void Print() const
const Date d2(2022, 1, 13);
d2.Print();
}
请思考下面的几个问题:
-
const对象可以调用非const成员函数吗?
不可以。const 对象被视为只读对象,它们不允许修改对象的状态。因此,对 const 对象调用非 const 成员函数将违反 const-correctness 规则,因此编译器会报错。 -
非const对象可以调用const成员函数吗?
是的。非 const 对象可以调用 const 成员函数。const 成员函数被设计用于不修改对象状态的情况,因此它们可以安全地被非 const 对象调用。 -
const成员函数内可以调用其它的非const成员函数吗?
不可以。const 成员函数被视为只读函数,它们保证不会修改对象的状态。因此,在 const 成员函数内部调用非 const 成员函数将破坏这一保证,编译器会报错。 -
非const成员函数内可以调用其它的const成员函数吗?
是的。非 const 成员函数可以调用 const 成员函数。const 成员函数不会修改对象的状态,因此它们可以安全地被非 const 成员函数调用。
总结:
- const 成员函数可以访问对象的成员变量,但不能修改它们。
- 非 const 成员函数可以访问并修改对象的成员变量。
- const 成员函数之间可以互相调用,但不能调用非 const 成员函数。
- 非 const 成员函数之间可以互相调用,也可以调用 const 成员函数。
8.取地址及const取地址操作符重载
class Date
{
public:
Date* operator&()
{
return this;
}
const Date* operator&() const
{
return this;
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
//这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容!
标签:中篇,const,int,默认,month,Date,day,构造函数
From: https://blog.csdn.net/Q2691463021/article/details/137515033