目录
一、类的默认成员函数
二、构造函数
三、析构函数
四、拷贝构造函数
五、运算符重载
1.基本知识
2.赋值运算符重载
3.取地址运算符重载
a.const成员函数
b.取地址运算符重载
一、类的默认成员函数
二、构造函数
7. 我们不写,编译器默认⽣成的构造,对内置类型成员变量的初始化没有要求,也就是说是是否初始化是不确定的,看编译器。对于自定义类型成员变量,要求调用这个成员变量的默认构造函数初始化。如果这个成员变量,没有默认构造函数,那么就会报错,我们要初始化这个成员变量,需要用初始化列表才能解决,初始化列表,我们在类和对象(下)再来探讨。
大家可以结合代码来理解,基本上以上的内容在代码中都有体现。
#include <iostream>
using namespace std;
// 日期类
class Date
{
public:
// 构造函数
// 无返回值,函数名与类名相同
// 1.无参构造函数
Date()
{
_year = 1900;
_month = 1;
_day = 1;
}
// 2.带参构造函数(构造函数可以重载)
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
// 3.半缺省构造函数
//Date(int year, int month = 1, int day = 1)
//{
// _year = year;
// _month = month;
// _day = day;
//} // 这里一定要注意函数重载的规则,参数不能都一样,这里是举例是想说明构造函数也可以使用缺省参数
// 4.全缺省构造函数
//Date(int year = 1900, int month = 1, int day = 1)
//{
// _year = year;
// _month = month;
// _day = day;
//}
// 其中1、4以及不自己定义而编译器自动生成的构造函数,不需要传递参数,称为默认构造
// 程序员自己定义构造函数后,编译器就不会自己生成
// 默认构造只能存在一个,所以这里把4注释掉了
// Init函数可以被构造函数替代,并且Init函数不会自动执行,显然构造函数更好一点
//void Init(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 d1;
// 对象实例化时系统会自动调用对应的构造函数
// 为什么不是Date d1(); ?
// 因为这样写就无法区分这是函数调用还是声明,大家记住是这样用就好
// 手动写的两个默认构造没有全部注释掉,会调用手动写的默认构造
// 都注释掉,会调用编译器生成的默认构造,结果未知,一般是随机值,取决于编译器
d1.Print();
// 运行结果是1900/1/1
Date d2(2024, 7, 24);
//这里直接调用了传参的构造函数,没有默认构造的事情
d2.Print();
// 运行结果是2024/7/24
return 0;
}
另外提一下,如果把默认构造的代码屏蔽掉,只写了一个需要传参的构造函数,编译器就不会生成默认构造函数,那么这样的写法就是有问题的,又因为编译器的默认构造不太好用,所以我们还是最好自己写出需要的默认构造和传参构造。
那么是否每个类我们都需要自己写构造函数?
不是的,我们上面提到,对于自定义类型成员变量,要求调用这个成员变量的默认构造函数初始化。也就是说一个类里面放着的都是自定义类型,那么这个类的默认构造就会调用类里面自定义类型实例化时的默认构造函数,如果类里面的自定义类型没有默认构造就会报错。
比如说用两个栈来实现队列:
#include <iostream>
using namespace std;
typedef int STDataType;
class Stack
{
public:
// 这里我们只需要写出Stack的默认构造,下面MyQueue在实例化时就会自动调用Stack的默认构造
Stack(int n = 4)
{
_a = (STDataType*)malloc(sizeof(STDataType) * n);
if (nullptr == _a)
{
perror("malloc申请空间失败");
return;
}
_capacity = n;
_top = 0;
}
// ......各种操作省略
private:
STDataType * _a;
size_t _capacity;
size_t _top;
};
// 两个Stack实现队列
class MyQueue
{
public:
//编译器默认⽣成MyQueue的构造函数调⽤了Stack的构造,完成了两个成员的初始化
private:
Stack pushst;
Stack popst;
};
int main()
{
MyQueue mq;
return 0;
}
三、析构函数
typedef int STDataType;
class Stack
{
public:
Stack(int n = 4)
{
_a = (STDataType*)malloc(sizeof(STDataType) * n);
if (nullptr == _a)
{
perror("malloc申请空间失败");
return;
}
_capacity = n;
_top = 0;
}
// ...
// 析构函数
// 无参数无返回值,函数名为~加上类名
~Stack()
{
cout << "~Stack()" << endl;// 这里打印是为了在控制台显示出析构函数被调用过,正常情况下不用写
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
private:
STDataType * _a;
size_t _capacity;
size_t _top;
};
// 两个Stack实现队列
class MyQueue
{
public:
// 编译器默认生成MyQueue的析构函数调用了Stack的析构,完成了空间释放
// 就算这里写了MyQueue的析构函数,编译器也会自动调用Stack的析构
private:
Stack pushst;
Stack popst;
};
int main()
{
MyQueue mq;
return 0;
}
运行结果是调用了两次Stack的析构,不难理解,这两次是对pushst和popst的分别析构
对比一下之前的C语言代码,我们可以发现我们再也不用害怕忘记写Init和Destroy函数了,因为根本不用写,有构造函数和析构函数确实方便了很多。
四、拷贝构造函数
普通的构造函数是给对象默认或者自己指定的值,那么能不能在不知道一个类对象的各种值把它的值直接拷贝给另一个对象呢?
可以。
#include <iostream>
using namespace std;
//日期类
class Date
{
public:
Date()
{
_year = 1900;
_month = 1;
_day = 1;
}
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
// 拷贝构造函数
Date(const Date& d)// 参数只有一个且必须为类类型对象的引用,加const是为了防止d被修改,造成错误拷贝
{
_year = d._year;
_month = d._month;
_day = d._day;
}
// 这个不是拷贝构造函数,这是一个普通的构造重载,传指针,一般不用
Date(Date* d)
{
_year = d->_year;
_month = d->_month;
_day = d->_day;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
// 取地址并打印日期
void Func1(Date d)
{
cout << &d << endl;
d.Print();
}
// 返回野引用的情况举例
Date& Func2()
{
Date tmp(2024, 7, 24);
tmp.Print();
return tmp;
}
int main()
{
Date d1(2024, 7, 24);
// C++规定自定义类型对象进行拷贝行为必须调用拷贝构造,所以这里传值传参要调用拷贝构造
// 所以这里的d1传值传参给d要调用拷贝构造完成拷贝,传引用传参可以减少这里的拷贝
Func1(d1);
cout << &d1 << endl;
// 这里可以完成拷贝,但是不是拷贝构造,只是一个普通的构造
Date d2(&d1);
d1.Print();
d2.Print();
//这样写才是拷贝构造,通过同类型的对象初始化构造,而不是指针
Date d3(d1);
d3.Print();
// 也可以这样写,这里也是拷贝构造
Date d4 = d1;
d2.Print();
// Func2返回了⼀个局部对象tmp的引用作为返回值
// Func2函数结束,tmp对象就销毁了,相当于了⼀个野引用
Date ret = Func2();
ret.Print();
return 0;
}
为什么拷贝构造只能传引用?
因为C++规定对于自定义类型的拷贝行为必须调用拷贝构造,如果拷贝构造传值,那么传值的过程中还会调用拷贝构造再次传值,结果就是永远都在调用拷贝构造和传值,无限递归下去,不能完成拷贝构造,程序就会崩溃。
#include<iostream>
using namespace std;
typedef int STDataType;
class Stack
{
public:
Stack(int n = 4)
{
_a = (STDataType*)malloc(sizeof(STDataType) * n);
if (nullptr == _a)
{
perror("malloc申请空间失败");
return;
}
_capacity = n;
_top = 0;
}
Stack(const Stack& st)
{
// 需要对_a指向资源创建同样大的资源再拷贝值
_a = (STDataType*)malloc(sizeof(STDataType) * st._capacity);
if (nullptr == _a)
{
perror("malloc申请空间失败!!!");
return;
}
memcpy(_a, st._a, sizeof(STDataType) * st._top);//空间拷贝
_top = st._top;
_capacity = st._capacity;
}
void Push(STDataType x)
{
if (_top == _capacity)
{
int newcapacity = _capacity * 2;
STDataType* tmp = (STDataType*)realloc(_a, newcapacity *
sizeof(STDataType));
if (tmp == NULL)
{
perror("realloc fail");
return;
}
_a = tmp;
_capacity = newcapacity;
}
_a[_top++] = x;
}
~Stack()
{
cout << "~Stack()" << endl;
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
private:
STDataType* _a;
size_t _capacity;
size_t _top;
};
// 两个Stack实现队列
class MyQueue
{
public:
private:
Stack pushst;
Stack popst;
};
int main()
{
Stack st1;
st1.Push(1);
st1.Push(2);
// Stack不显示实现拷贝构造,用编译器自动生成的拷贝构造完成值拷贝/浅拷贝
// 会导致st1和st2里面的_a指针指向同一块资源,析构时会析构两次,程序崩溃
// 其实不止有析构两次的问题,由于是两者共用同一片空间,还有数据覆盖...
// 所以有资源申请的自定义类型,都要自己实现深拷贝
Stack st2 = st1;
//Stack st2(st1); 都可以
MyQueue mq1;
// MyQueue自动生成的拷贝构造,会自动调用Stack拷贝构造完成pushst/popst的拷贝
// 只要Stack拷贝构造自己实现了深拷贝,那就没问题
MyQueue mq2 = mq1;
return 0;
}
五、运算符重载
1.基本知识
(别慌,这些在后面的代码基本上都有体现)
#include<iostream>
using namespace std;
// 编译报错:“operator +”必须至少有一个类类型的形参,一定要注意运算符重载必须有类类型形参
//int operator+(int x, int y)
//{
// return x - y;
//}
class A
{
public:
void func()
{
cout << "A::func()" << endl;
}
};
typedef void(A::* PF)(); //成员函数指针类型 返回类型(*函数指针变量名)(参数类型和个数)
int main()
{
// C++规定成员函数要加&才能取到函数指针
PF pf = &A::func;
// 如果我们想调用func函数,怎么调用?
//(*pf)();
// 不对,因为类成员函数有一个特点就是有this指针,而this指针又不能显示传
// 所以可以这样,对象调用成员函数指针时,使用.*运算符
A a;
(a.*pf)();
// 运行结果打印出了A::func(),证明调用成功
return 0;
}
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year = 1, 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;
}
Date& operator++()
{
cout << "前置++" << endl;
//...
return *this;//返回加之后
}
// C++规定,后置++重载时,增加一个int形参,跟前置++构成函数重载,方便区分
Date operator++(int)
{
Date tmp;
cout << "后置++" << endl;
//...
return tmp;//返回加之前
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2024, 7, 25);
d1.operator++();
// 编译器会转换成 d1.operator++();
++d1;
d1.operator++(0);
d1.operator++(1);
d1.operator++(-1);// 括号里面int类型的值无所谓,但是我们一般给0
// 编译器会转换成 d1.operator++(0);
d1++;
return 0;
}
//重载流运算符输入和打印日期类
#include<iostream>
using namespace std;
class Date
{
friend ostream& operator<<(ostream& out, const Date& d);
// 这个是友元函数声明,现在只需要知道有了这个声明,operator<<函数在全局就可以使用Date的私有成员了
// 这里是为了方便博主才使用的,在类和对象(下)博主会细说
friend istream& operator>>(istream& in, Date& d);
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//void operator<<(ostream& out);// 如果在类里面实现,那么隐式传的this指针会占第一个形参位置
// 我们打印时就只能 d << cout 不符合使用习惯
// 所以我们最好在全局重载流运算符
private:
int _year;
int _month;
int _day;
};
// 打印
ostream& operator<<(ostream& out, const Date& d)// 参数传引用和传引用返回都是为了减少拷贝
{
out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
return out;// 传过来是cout其实返回的也就是cout,这里设置返回值主要是为了可以连续打印,不然就只能一个一个写
}
// 与打印类似
istream& operator>>(istream& in, Date& d)//这里的d千万不要加const!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
{
cout << "请在下方依次输入年月日:" << endl;
in >> d._year >> d._month >> d._day;
return in;
}
int main()
{
Date d1, d2;
cin >> d1 >> d2;
cout << d1 << d2 << endl;
//程序运行会成功
return 0;
}
#include<iostream>
using namespace std;
// 重载为全局的面临对象访问私有成员变量的问题
// 有几种方法可以解决:
// 1、成员放公有
// 2、Date提供getxxx函数
// 3、友元函数(这里先不讲,放到类和对象下博主会写清楚)
// 4、重载为成员函数
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
// 2.在类里面提供Getxxx函数,把需要得到的成员变量值作为返回值返回,在全局重载函数内部接收
// 其实写起来有点不方便
int Getyear() const// 这里后面为什么加const? 是为了防止在该函数里面误修改成员变量,会造成权限放大
// 因为后面的全局重载函数参数有const,这里与那里相对应,const修饰成员函数后面一点我会细说
{
return _year;
}
int Getmonth() const
{
return _month;
}
int Getday() const
{
return _day;
}
private: // 1.直接把需要的成员放成公有,不建议
int _year;
int _month;
int _day;
};
// 直接将操作符重载为全局,会出现类类型内私有成员无法访问的问题
bool operator==(const Date& d1, const Date& d2)
{
return d1.Getyear() == d2.Getyear()
&& d1.Getmonth() == d2.Getmonth()
&& d1.Getday() == d2.Getday();
}
int main()
{
Date d1(2024, 7, 5);
Date d2(2024, 7, 6);
// 运算符重载函数可以显示调用
operator==(d1, d2);
// 编译器会转换成 operator == (d1, d2);
d1 == d2;
return 0;
}
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
// 4.不重载为全局函数,重载为成员函数
bool operator==(const Date& d2)// 为什么只需要传一个d2呢,因为this指针已经替我们传了d1,其实这里把d2写成d会更好看一点
{
return this->_year == d2._year
&& this->_month == d2._month
&& this->_day == d2._day;
}
private: // 1.直接把需要的成员放成公有,不建议
int _year;
int _month;
int _day;
};
// 直接将操作符重载为全局,会出现类类型内私有成员无法访问的问题
//bool operator==(const Date& d1, const Date& d2)
//{
// return d1.Getyear() == d2.Getyear()
// && d1.Getmonth() == d2.Getmonth()
// && d1.Getday() == d2.Getday();
//}
int main()
{
Date d1(2024, 7, 5);
Date d2(2024, 7, 6);
// 运算符重载函数可以显示调用
// 调用方式会略微变化一丢丢
d1.operator==(d2);
// 下面这个不变
// 编译器会转换成 operator == (d1, d2);
d1 == d2;
return 0;
}
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 << " 拷贝构造 " << endl;// 打印是为了在运行结果可以看到函数调用
_year = d._year;
_month = d._month;
_day = d._day;
}
// 传引用返回减少拷贝
// d1 = d2;
Date& operator=(const Date& d)
{
cout << " 赋值重载 " << endl;// 打印是为了在运行结果可以看到函数调用
// 检查自己给自己赋值的情况
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
// d1 = d2表达式的返回对象应该为d1,也就是*this
// 这样设计是为了可以支持连续赋值
return *this;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2024, 7, 25);
Date d2(d1);//拷贝构造
Date d3(2024, 7, 26);
d1 = d3;//赋值重载,因为d1之前已经存在了
Date d4 = d1;//拷贝构造
// 赋值重载完成两个已经存在的对象直接的拷贝赋值
// 而拷贝构造用于一个对象拷贝初始化给另一个要创建的对象
return 0;
}
3.取地址运算符重载
a.const成员函数
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
// 为什么此函数后面要加const?
// 加上const后,this指针由 Date* const this 变为 const Date* const this
// 这样this指针指向的内容便不能被修改
// 我们以后在写不涉及到内容修改的成员函数时,都可以加上const修饰,可以避免不必要的错误
// 这里的this指针变成const Date* const this后,就会与下面全局重载函数的参数const Date& d1呼应
// 这样就避免了权限的放大
// 我们在日常写代码过程中用const修饰不需要修改的函数参数和不需要修改this指针指向内容的成员函数是一种非常好的习惯
int Getyear() const
{
return _year;
}
int Getmonth() const
{
return _month;
}
int Getday() const
{
return _day;
}
private:
int _year;
int _month;
int _day;
};
bool operator==(const Date& d1, const Date& d2)
{
return d1.Getyear() == d2.Getyear()
&& d1.Getmonth() == d2.Getmonth()
&& d1.Getday() == d2.Getday();
}
int main()
{
Date d1(2024, 7, 5);
Date d2(2024, 7, 6);
operator==(d1, d2);
d1 == d2;
return 0;
}
b.取地址运算符重载
// 取地址运算符重载
#include<iostream>
using namespace std;
class Date
{
public:
Date* operator&()
{
//return this;
return nullptr;// 如果不想让别人取到地址,返回一个nullptr或随便其它一个地址就行了
}
const Date* operator&()const //这个是主要适用于const类型的变量取地址,因为上面那个会导致权限放大,编译器会自动选择
{
//return this;
return (const Date*)0x0060fa71;// 道理和上面那个一样,为了后面打印的时候和上面那个区分,博主就随便写了一个地址
}
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;
const Date d2;//这里默认构造会初始化
cout << &d1 << endl;
cout << &d2 << endl;
return 0;
}
终终终终终于写完了..............................
大家可以试试自己实现一个日期类,里面包含输入输出,差值,比较等等功能,博主会在类和对象(下)最后把代码附上。
点个赞吧〒_〒......