目录
构造函数的概念:
构造函数的特性
析构函数的概念:
析构函数的特性
拷贝构造函数的概念:
拷贝构造函数的特性
比较运算符 (==) 重载
赋值运算符(=)重载
const修饰类的成员函数
取地址及const取地址操作符重载
内容大纲:
如果一个类中什么成员都没有,我们称之为空类。但是空类并非什么都没有,任何一个类,即使我们什么都不写,类中也会自动生成6个默认成员函数。
注:任何一个类的默认成员函数如果你写了,编译器就不会生成,如果没写,编译器才会自动生成。
在对象生成时,为对象变量初始一个合适的值,构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,并且在对象的生命周期内只调用一次。
以下日期类中的成员函数Date就是一个构造函数。当你用该日期类创建一个对象时,编译器会自动调用该构造函数对新创建的变量进行初始化。
class Data
{
public:
Data(int year = 0, int month = 0, int day = 1)//构造函数
{
_year = year;
_month = month;
_day = day;
}private:
int _year;
int _month;
int _day;
};注:构造函数不是一个为对象开辟空间,而是对其变量初始化。
这里d1打印的都是随机值,这就是编译器自动调用的默认构造函数处理的结果,既然编译器初始化对象的值是随机值,那默认构造函数有什么意义?
其实不然,编译器默认生成的构造函数机制如下:
1.对自定义类型才做处理,会回到这个自定义类调用它的默认构造函数
2.对内置类型不做处理
class Time
{
private:
int _t;
};class Date{private:// 基本类型(内置类型)int _year;int _month;int _day;// 自定义类型Time _t ;};int main(){Date d ;return 0 ;}自定义类型:通过class或者struct写的自定义类 内置类型:自己类中的成员变量
所以在编译器默认生成的默认构造函数不能满足要求,还是需要我们自己写一个。
与构造函数功能相反,析构函数负责完成对象的销毁,对象在销毁时会自动调用析构函数,完成类的一些资源清理工作。(一般类不需要清理空间,类中都是局部变量,随对象的销毁而销毁,但是如果类中有动态申请的空间(比如战),析构函数就有用了)
1.函数名与类名相同,得在前加~。
class Data
{
public:
//构造函数
Data(int year = 0, int month = 0, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//析构函数
~Data()
{}
}
2.析构函数无返回值,无参数
3.对象生命周期结束时,编译器会自动调用其析构函数
4.一个类有且只有一个析构函数。若未显示定义系统会自动生成默认的析构函数
对于自定义类型需要处理,回该类调用其默认析构函数。
对于内置类型不处理。
5.先构造的后析构,后构造的先析构
因为对象的定义在函数中,函数调用会建立栈帧,栈帧中的对象构造和析构也要符合先进后出的原则。
只有单个形参,该形参是对本类类型对象的引用(一般常用从const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
class Data
{
public:Data(const Data& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}}
int main()
{
Data d1;//调用无参的构造函数
Data d2(2022,10,22);//调用有参的构造函数Data d3(d2)//拷贝构造函数
}
1.拷贝构造函数是构造函数的一个重载形式(拷贝构造函数的函数名也与类名相同)
2.拷贝构造参数只有一个而且必须是引用传参,如果传值传参的会引发无限递归
要调用拷贝构造得先传参,使用传值传参的话,传参的过程是一次拷贝构造,拷贝构造就得先传参,就这样无限递归,所以拷贝构造必须使用引用传参。
自定义类型的对象进行函数传参时,一般推荐使用引用传参。使用传值传参也可以,但每次传参时都会调用拷贝构造函数。
3.若未显示定义拷贝构造函数,编译器会默认生成字节序的拷贝构造函数
字节序:按字节大小进行值拷贝
编译器自动生成的拷贝构造函数机制:
1.编译器自动生成的拷贝构造函数对内置类型会完成浅拷贝(值拷贝)
2.对于自定义类型,编译器会再去调用它们自己的默认拷贝构造函数
4.编译器自动生成的拷贝构造函数不能完成深拷贝
栈(Stack)这样的类,编译器自动生成的拷贝构造函数就不能满足我们的需求了
int main()
{
Stack s1;
s1.Print();// 打印s1栈空间的地址
Stack s2(s1);// 用已存在的对象s1创建对象s2
s2.Print();// 打印s2栈空间的地址 return 0;
}
创建栈对象s1,s2,如果是编译器默认生成的浅拷贝,通过打印地址发现,s1,s2是同一片空间,那么对s1的改变直接影响s2,并且析构函数的时候会报错,同一片空间析构了两遍。
所以编译器默认的拷贝构造函数就不能满足要求,这就需要我们自己实现拷贝构造函数。
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,其目的就是让自定义类型可以像内置类型一样可以直接使用运算符进行操作。
日期类创建d1和d2
d1 == d2;// 通过对==运算符重载,可读性高(书写简单)
IsSame(d1, d2);// 自己写一个函数进行比较,可读性差(书写麻烦)
运算符重载函数也具有自己的返回值类型,函数名字以及参数列表。其返回值类型和参数列表与普通函数类似。
运算符重载函数名为:关键字operator后面接需要重载的操作符符号。
函数原型:返回值 operator运算符(参数列表)
注意:
1.不能通过连接其他符号来创建新的操作符:比如operator@。
2.重载操作符必须有一个类类型或枚举类型的操作数。
3.用于内置类型的操作符,重载后其含义不能改变。
4.作为类成员的重载函数时,函数有一个默认的形参this,限定为第一个形参。
5.sizeof :: .* ?: . 这5个运算符不能重载。
class Data
{
public:
Data(int year = 0, int month = 0, int day = 1)
{
_year = year;
_month = month;
_day = day;
}//函数有一个默认的形参this,限定为第一个形参
bool operator==(const Data& d) //==的重载
{
return _year == d._year && _month == d._month && _day == d._day;
}
private:
int _year;
int _month;
int _day;
};
我们也可以将该运算符重载函数放在类外面,但此时外部无法访问类中的成员变量,这时我们可以将类中的成员变量设置为共有(public),这样外部就可以访问该类的成员变量了(也可以用友元函数解决该问题)。并且在类外没有this指针,所以此时函数的形参我们必须显示的设置两个。
class Data
{
public:
Data(int year = 0, int month = 0, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
int _year;
int _month;
int _day;
};bool operator==(const Data& d1,const Data& d2)
{
return d1._year == d2._year && d1._month == d2._month && d1._day == d2._day;
}不推荐这种写法,破坏封装性
class Data
{
public:
Data(int year = 0, int month = 0, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Data& operator=(const Data& d)
{
if (this != &d)//同一片空间的赋值不处理
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
private:
int _year;
int _month;
int _day;
};
重载赋值运算符需要注意以下几点:
1.参数类型设置为引用,并用const进行修饰
赋值运算符重载函数的第一个形参默认是this指针,第二个形参是我们赋值运算符的右操作数。使用引用传参避免了一次拷贝构造函数,且第二个形参是右操作数,其值不能改变,加const修饰。
2.函数的返回值用引用返回
如果只是d1=d2,不返回也没事,但是如果是d1=d2=d3这种连续赋值,那就需要有返回值,而且很明显,返回值应该是赋值运算符的左操作数,即this指针指向的对象。并且这个对象不会因为函数调用结束销毁,所以可以使用引用返回,避免拷贝构造(传值返回就是一次拷贝构造)。
3.赋值前检查是否是给自己赋值
如果是d1=d1,那么可以不进行赋值操作,直接返回
4.引用返回的是*this
赋值操作完成时,需要返回赋值运算符的左操作数,函数体内我们this指针访问到的就是左操作数
5.一个类如果没有显示定义赋值运算符重载,编译器也会自动生成一个,完成对象按字节序的值拷贝
赋值运算符重载编译器也可以自动生成,并且也是支持连续赋值的,只需要判断深浅拷贝,决定是否需要自己写
区分:
Date d1(2021, 6, 1);
Date d2(d1);//这是一次拷贝构造
Date d3 = d1;//这也是一次拷贝构造
拷贝构造函数:用一个已经存在的对象去构造初始化另一个即将创建的对象。
赋值运算符重载函数:在两个对象都已经存在的情况下,将一个对象赋值给另一个对象。
我们将const修饰的类成员函数称之为const成员函数,const修饰类成员函数,实际修饰的是类成员函数隐含的this指针,表明在该成员函数中不能对this指针指向的对象进行修改。
void Print()const // cosnt修饰的打印函数
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
思考下面几个问题(经典面试题):
1.const对象可以调用非const成员函数吗?不可以
2.非const对象可以调用const成员函数吗? 可以
3.const成员函数内可以调用其他的非const成员函数吗?不可以
4.非cosnt成员函数内可以调用其他的cosnt成员函数吗? 可以
原因:
1.非const成员函数指this指针没有被const修饰,传入一个const修饰的对象,接收的确是没有const修饰的this指针,属于权限的放大!函数调用失败。
2.const成员函数指的是this指针被const修饰,传入一个非const修饰的对象,接收的是const修饰的this指针,属于权限缩小! 函数调用成功。
3.const修饰的this指针调用没有被const修饰的this指针,相当于将没有被const修饰的指针赋值给被const修饰的this指针,属于权限放大!函数调用失败。
4.非const修饰的this指针调用被const修饰的this指针,相当于将被const修饰的指针赋值给没有被const修饰的this指针,属于权限缩小!函数调用成功
取地址操作符重载和const取地址操作符重载,这两个默认成员函数一般不用自己重新定义,使用编译器自动生成的就行了。
class Date
{
public:
Date* operator&()// 取地址操作符重载
{
return this;
}
const Date* operator&()const// const取地址操作符重载
{
return this;
}
private:
int _year;
int _month;
int _day;
};