一、构造函数
构造函数为默认成员函数之一,不写编译器会自动生成
他有如下特征:
① . 函数名和类名相同
② . 无返回值
③ . 对象实例化时自动调用对应构造函数
④ . 构造函数可以重载
有了构造就可以省去一直Init的步骤,我们看下面代码
class Date {
//注: 1和3语法上可以同时存在,但调用时会报错
// 1和2可合并为3
public:
//1
//Date()
//{
// _year = 1;
// _month = 1;
// _day = 1;
//}
2
//Date(int year, int month, int day)
//{
// _year = year;
// _month = month;
// _day = day;
//}
//3
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "--" << _month << "--" << _day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.Print();
cout << endl;
Date d3(2023,11,11);
d3.Print();
return 0;
}
可以看见,我们不传值时,会根据全缺省参数初始化,如果自己传值的话,那就会初始化传的值,但如果没有构造函数,编译器就会自动生成,这里我们就要提一下编译生成的默认构造特点
1.不写才会生成,写了不会生成
2.内置类型的成员不会处理
3.自定义类型的成员会去调用这个成员的构造函数
结果如下
class Date {
public:
void Print()
{
cout << _year << "--" << _month << "--" << _day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.Print();
cout << endl;
return 0;
}
但这生成的是个什么鬼,和没处理又有什么区别哇
总结一下:一般情况下都要写构造函数,除非成员变量全是自定义类型,这里就不得不提一下太子爷,两栈实现的队列了
class Myqueue {
Stack _pushst;
Stack _popst;
};
这玩意你让他不写构造函数我一点意见没有
关于构造函数,这里再提一下默认构造函数:无参,全缺省,系统自动生成的都叫默认构造函数(只要不传参,就可以调用的),且默认构造函数只有一个
二、析构函数
对象在销毁时会调用析构函数,完成对象中资源清理工作,同样的,他也是个默认成员函数(类似destroy)
特征:
1.析构函数名在类前加~
2.无参,无返回值
3.一个类只有一个析构函数,不能重载,若无定义,则会自动生成
4.对象生命周期结束时,C++编译器系统自动调用析构函数
class Date {
public:
Date(int year = 2023, int month = 11, int day = 11)//默认构造函数
{
_year = year;
_month = month;
_day = day;
}
~Date()//析构
{
cout << "~Date()" << endl;
_year = 1;
_month = 1;
_day = 1;
}
void Print()
{
cout << _year << "--" << _month << "--" << _day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.Print();
cout << endl;
return 0;
}
从这里我们可以清楚看见调用了析构函数,但日期类内部成员函数都是内置类型,当我们写像栈那样的复杂类,需要手写析构函数,这里我们编译一下,在仔细看一下是否调用了析构函数
默认析构函数的特性和默认构造相似
1.内置类型成员不会处理
2.自定义类型调用它的析构函数
三、拷贝构造函数
class Date {
};
class Stack {
};
void func1(Date d){ }
void func2(Stack s){ }
int main()
{
Date d1;
func1(d1);
Stack s1;
func2(s1);//这里会崩溃
return 0;
}
为什么会崩溃呢
因为func2函数中_a在堆上申请的空间和main函数中_a申请的空间是一个,func2函数结束时,会进行一次析构,而main函数结束同样也要进行一次析构,从而一块空间进行了两次析构,所以就会崩溃
这里func2参数可以用引用,一个对象别名不会析构,但怎么样才能让s改变不影响s1呢,这里就映出了拷贝构造函数
特征:
1.拷贝构造函数是构造函数的一个重载形式
2.拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值调用会让编译器直接报错,因为会引发无穷调用
class Date {
public:
Date(int year = 2023, int month = 11, int day = 11)
{
_year = year;
_month = month;
_day = day;
}
Date(const 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;
};
int main()
{
Date d1;
d1.print();
Date d2(d1);
d2.print();
return 0;
}
默认生成的拷贝构造函数
我们不写,默认生成的拷贝构造和之前构造函数特性不一样
1.内置类型:值拷贝
2.自定义类型:调用他自己的拷贝构造
总结:Date不需要我们实现拷贝构造,默认生成就可以用
Stack需要我们自己实现深拷贝的默认构造,默认生成的会出现问题(可能会析构两次)
注:对于两栈实现队列这一题,所有的默认成员函数都可以默认生成(全是自定义类型对象)
四、运算符重载
总所周知,两个自定义类类型是不可以直接比较,但就是想让他直接比,那怎么办呢?,换句话说,怎么样才能让这种写法合法,而你:运算符重载,你是我的英雄
C++规定:operator加运算符为函数名,我们后面进行详细讲解
*特性:
1.C++为了增强代码得可读性,引入了运算符重载,运算符重载是具有特殊函数名得函数,其余和普通函数相似
2.函数名为operator后面接需要重载的运算符
3.函数样式:返回值类型 operator运算符(参数列表)
注:
1.不能通过连接其他符号来创建新的操作符比如:operator@
2.重载操作符必须有一个类类型参数
3.用于内置类型的运算符,其含义不能改变,例如内置的‘+’,不能改变其含义
4.作为类成员函数重载时,其形参看起来比操作数少一,因为成员函数的第一个参数为隐藏的this指针
5. . :: sizeof ?: . 这五个运算符不能重载
关于运算符重载还有个需要注意的,那便是赋值运算符重载,很多人会把它和拷贝构造函数搞混,这里来详细分析一下
赋值运算符重载:两个已经存在的对象进行拷贝
拷贝构造:一个已经存在的对象去初始化另一个对象
class Date {
public:
Date(int year = 2023, int month = 11, int day = 11)
{
_year = year;
_month = month;
_day = day;
}
void print()
{
cout << _year << "--" << _month << "--" << _day << endl;
}
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
Date& operator=(const Date& d)
{
if (this != &d)
{
this->_year = d._year;
this->_month = d._month;
this->_day = d._day;
}
return *this;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(1, 1, 1);
Date d2(d1);//拷贝构造
Date d3(2023, 11, 12);
d1 = d3;//赋值运算符
d1.print();
d2.print();
d3.print();
return 0;
}
运行结果:
这里运算符重载就先讲到这里,更多关于运算符重载的实现,可以参见博主的日期类实现文章:日期类实现
五、const关键字
const这个关键字我们已经了解很多,下面再带着例子略带提一下
bool Date::operator < (const Date& d) const
{
if (_year < d._year ||
_year == d._year && _month < d._month ||
_year == d._year && _month == d._month && _day < d._day)
return true;
return false;
}
这是我们上面提到的<运算符重载,这里再函数名后加了个const,从而达到禁止修改的目的
注:
const变量不能调用非const成员函数
非const变量可以调用const成员函数
const成员函数不能调用非const成员变量
非const成员函数可以调用const成员变量
(这里涉及的是权限的放大与缩小)
总结:只读函数可以加const,内部不涉及修改生成
六、取地址运算符重载
Date* Date::operator&( )
{
return this;
}
平时不需要自己写,因为库里有,所有我们平时可以直接用自动生成的,除非你不想让别人取到地址,比如:return nullptr;
七、友元函数
细心观察的小伙伴们都会发现,前面打印都用的print函数,那么我们想用流提取操作符打印可以吗?可以,但是有缺陷
这里我们直接使用会报错,那么我们对<<进行重载试试
打印出来了没错,但是这个d1<<cout看上去是不是蜘蛛侠骑马—马拉个彼得,还得反着写,为什么呢,因为类内this指针永远是第一个参数,所有我们要写成全局函数,但成员函数私有,那这时友元就出来了
这个时候,是不是就好受多了,说到流插入/提取,我们为什么不用scanf和printf呢,因为这两个函数不好支持自定义类型,所以才引入了流插入/提取
但是这个时候问题又出现了
当我想连续输入时,他却报错了,一个一个写那得多少行,分析一下,cout<<d1返回的是空类型,所以会出问题也正常,这时我们把void换成ostream&,就可以完美解决了
class Date {
public:
Date(int year = 2023, int month = 11, int day = 11)
{
_year = year;
_month = month;
_day = day;
}
void print()
{
cout << _year << "--" << _month << "--" << _day << endl;
}
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
friend ostream& operator<<(ostream& out,const Date& d);
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << "--" << d._month << "--" << d._day << endl;
return out;
}
int main()
{
Date d1(2023, 11, 12);
Date d2(2023, 11, 13);
cout << d1 << d2;
return 0;
}
运行结果:
友元函数特性
1.友元函数可访问类的私有和保护成员,但不是类的成员函数
2.友元函数不能用const修饰
3.友元函数可以在类定义的任何地方声明,不受类访问限定符限制
4.一个函数可以是多个类的友元
5.友元函数的调用与普通函数原理相同
友元类
class Date {
private:
Time _t;
};
class Time {
friend class Date;
};
这里声明Date类是Time类的友元类,则在日期类中就能直接调用时间类中的私有成员
但是Time类不能调用Date类私有成员