目录
类的6个默认成员函数
如果一个类中什么成员都没有,简称为空类。空类中什么都没有吗?并不是的,任何一个类在我们不写的情况下,都会自动生成下面6 个默认成员函数
class Date {}; //空类
注意:这是6个特殊的成员函数
一、构造函数
概念:构造函数虽然名字叫做构造,但是它并不是开空间创造对象,而是初始化对象
特征如下:
1、函数名和类名相同
2、无返回值,并不是void
3、对象实例化的时候自动调用对应的构造函数
4、构造函数可以构成重载
class Date
{
public:
Date()//1、无参构造函数
{
_year = 2000;
_mouth = 2;
_day = 5;
}
Date(int year, int mouth,int day)//2、带参构造函数(可以是缺省的参数)
{
_year = year;
_mouth = mouth;
_day = day;
}
//以上两个函数名字相同,但是参数不同,构成了函数重载
private:
int _year;
int _mouth;
int _day;
};
int main()
{
Date d1;//这里就是具体的对象实例化
Date d2(2022,2,12);
return 0;
}
5、tips:C++把我们的类型分为了两类
第一类内置类型:int/double/指针类型/内置类型数组等
第二类自定义类型:struct/class定义的类型
特殊的处理规则:
①我们不写编译器默认生成的构造函数,对于内置类型不做处理
②对于自定义类型的成员变量,会去调用它的默认构造函数(就是不用参数就可以调用的成员函数)进行初始化
③如果没有默认构造函数,编译器就会报错
默认构造函数有三类(不用参数就可以调用):全缺省的,无参的,我们不写编译器默认生成的
举例说明:
//A类
class A
{
public:
A()
{
cout << "调用A()" << endl;
_a = 0;
}
//如果是下面的带参的函数,就不是默认构造函数,编译器就会报错
/*A(int a)
{
cout << "调用A()" << endl;
_a = 0;
}*/
private:
int _a;
};
class Date
{
public:
//这里不写
private:
int _year;
int _mouth;
int _day;
A _aa;//这里对于自定义类型,会去调用A类型的默认构造函数
};
int main()
{
Date d1;
return 0;
}
运行结果:
说明:对于内置类型不做处理,对于自定义类型A里面_aa成员变量,回去调用A类的默认构造函数,如果没有默认构造函数就会报错
打开监视窗口观察:
6、如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义,编译器就不再生成
例如:
class Date
{
public:
//这里我们不手动写,编译器会默认生成构造函数帮我们完成初始化操作
private:
int _year;
int _mouth;
int _day;
};
int main()
{
Date d1;
return 0;
}
我们此时打开监视窗口,可以观察到初始化给对象的是随机值,这也印证了我们上面的结论:
需要注意:
class Date
{
public:
Date()//1、无参构造函数
{
_year = 2000;
_mouth = 2;
_day = 5;
}
Date(int year=2000, int mouth=2, int day=5)//2、带参构造函数
{
_year = year;
_mouth = mouth;
_day = day;
}
private:
int _year;
int _mouth;
int _day;
};
int main()
{
Date d1;//这里就会报错
//1、语法上无参和全缺省的可以同时存在
//2、但是如果存在无参调用,就会存在歧义(编译器报错)
Date d2(2022, 2, 12);
return 0;
}
二、析构函数
概念:与构造函数功能相反,析构函数不是完成对象的销毁,局部的对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中一些资源的清理工作。
例如:我们动态内存开辟的一些指针,我们需要对其进行释放,这就叫做资源的清理工作
特征:
1、析构函数名个类名相同,且在前加个 ~
2、无参数,无返回值
3、一个类有且只有一个析构函数,若没有显式定义,系统会自动生成默认的析构函数
4、对象生命周期结束时,C++编译系统会自动调用析构函数
class Date
{
public:
~Date()
{//Date类没有资源需要清理,所以Date不实现析构函数也是可以的
cout << "调用 ~Date()" << endl;
}
private:
int _year;
int _mouth;
int _day;
};
int main()
{
Date d1;
return 0;
}
但是我们遇到某些情况下是需要资源清理的:(例如malloc的资源是需要处理的)
class stack
{
public:
stack(int capacity = 4)
{
_a = (int*)malloc(sizeof(int) * capacity);
if (_a == nullptr)
{
cout << "malloc fail" << endl;
exit(-1);
}
}
~stack()
{
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
private:
int* _a;
size_t _top;
size_t _capacity;
};
int main()
{
stack s1;
stack s2(10);
return 0;
}
5、tips:
如果我们不写,默认生成的析构函数和构造函数类似
①对于内置类型的成员变量,不做处理
②对于自定义类型的成员变量,会去调用它的析构函数
我们这里举一个题,用两个栈实现队列,其中默认生成的构造和析构函数就会去调用它的构造和析构:
class stack
{
public:
stack(int capacity = 4)
{
_a = (int*)malloc(sizeof(int) * capacity);
if (_a == nullptr)
{
cout << "malloc fail" << endl;
exit(-1);
}
}
~stack()
{
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
private:
int* _a;
size_t _top;
size_t _capacity;
};
//两个栈实现一个队列
class MyQueue
{
public:
// 默认生成构造函数和析构函数会对自定义类型成员调用他的构造和析构
void push(int x)
{}
private:
stack pushST;
stack popST;
};
int main()
{
MyQueue q;
return 0;
}
三、拷贝构造函数
概念:顾名思义,就是拷贝赋值一个和自己一模一样的对象出来。拷贝构造函数只有单个形参,改形参是对本类类型对象的引用(一般常用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;
}
Date(const Date& d)// 拷贝构造函数,
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2000, 1, 1);
Date d2(d1); // 用已存在的对象d1创建对象d2
//用一个同类型对象初始化就是拷贝构造,将d1的内容复制给d2
return 0;
}
特征:
1、拷贝构造函数是构造函数的一个重载形式
2、拷贝构造的函数参数只有一个,并且必须使用引用传参,使用传值的方式会引发无穷递归调用
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
~Date()
{
cout << "~Date()" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2(d1);
//将d1的内容复制给d2
//用同一个类型的对象去初始化,就是拷贝构造
return 0;
}
为什么这里是引用传参?
date就是d1的别名,避免传值时的拷贝引发无限递归
为什么这里建议加const?
这里是想复制一个与原对象一样的对象出来,我们是想保持原对象的内容不改变所以让原对象具有const常属性
3、如果没有显式定义,系统生成默认的拷贝构造函数
默认生成的拷贝构造函数:
1)内置类型的成员变量:会完成按字节序的拷贝(浅拷贝)
2)自定义类型的成员变量:会调用它的拷贝构造函数
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//我们这里不写拷贝构造函数,编译器会自动生成
~Date()
{
cout << "~Date()" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2(d1);
//将d1的内容复制给d2
return 0;
}
打开监视窗口观察两个对象:
可以发现发生了浅拷贝(值拷贝),拷贝出了一个内容完全相同的d2。
我们自己这里并没有定义拷贝构造函数,但是编译器自动生成的拷贝构造函数还是完成了对象的拷贝任务。
4、编译器自动生成的拷贝构造函数不能完成深拷贝
有些时候,我们不写,编译器自动生成的拷贝构造函数就足够用了,但是编译器自动生成的拷贝构造函数不能完成深拷贝,举个例子,当我们需要拷贝构造一个栈出来时,对于默认生成的拷贝构造函数就不能用
class Stack
{
public:
Stack(int capacity = 4)
{
_ps = (int*)malloc(sizeof(int)* capacity);
_size = 0;
_capacity = capacity;
}
void Print()
{
cout << _ps << endl;// 打印栈空间地址
}
private:
int* _ps;
int _size;
int _capacity;
};
int main()
{
Stack stack1;
stack1.Print();// 打印stack1栈空间的地址
Stack stack2(stack1);// 用已存在的对象stack1创建对象stack2
stack2.Print();// 打印s2栈空间的地址
return 0;
}
解析:如果我们的拷贝构造还是按字节序的浅拷贝,拷贝出来就会将开辟栈1的地址复制给栈2,这样两个指针就会指向堆区的同一块区域,这样的话就用出现两个栈同时操作会相互影响,当stack2析构完后,stack1也对同一块区域析构,这是不允许的,会导致程序崩溃。正确的是,我们自己写栈的拷贝构造,当程序运行结束,stack2栈将先被析构,此时那块栈空间被释放,然后stack1栈也要被析构,再次对那一块空间进行释放。
四.赋值运算符的重载
4.1运算符重载
默认C++是不支持自定义类型对象使用运算符的,为了让他们可以运算(比大小之类的),提出来运算符重载,运算符重载是具有特殊函数名的函数,其目的就是让自定义类型可以像内置类型一样可以直接使用运算符进行操作
特征:
1、具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似
2、函数名字为:关键字operator后面接需要重载的运算符符号
3、函数原型:返回值类型 operator操作符(参数列表)
4、参数:操作符有几个操作数,它就有几个参数
注意:
1)不能通过连接其他符号来创建新的操作符:比如operator@
2)重载操作符必须有一个类类型或者枚举类型的操作数
3)用于内置类型的操作符,其含义不能改变,例如:内置的整型+,不能改变其含义
作为类成员的重载函数时,其形参看起来比操作数数目少1成员函数的操作符有一个默认的形参this,限定为第一个形参
4).* 、:: 、sizeof 、?: 、. 注意以上5个运算符不能重载。这个经常在笔试选择题中出现。
我们要实现运算符重载,我们首先来举例看看全局的 operator>
#include<iostream>
using namespace std;
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)
{
if (d1._year > d2._year)
return true;
else if (d1._year == d2._year && d1._month > d2._month)
return true;
else if (d1._year == d2._year && d1._month == d2._month && d1._day > d2._day)
return true;
else
return false;
}
int main()
{
Date d1(2022,2,1);
Date d2(d1);
Date d3(2000, 1, 1);
cout <<(d1 > d3 )<< endl;
cout << (operator>(d1,d3)) << endl;
//将d1的内容复制给d2
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;
}
bool operator>(const Date& d)//成员函数默认有一个隐藏的this指针
{
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;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2022, 2, 1);
Date d2(d1);
Date d3(2000, 1, 1);
cout << (d1>d3) << endl;//这样写会被编译器转化为下面这行
cout<< d1.operator>(d3)<<endl;
return 0;
}
成员函数就会有一个隐藏的this指针
4.2赋值运算符的重载
我们知道:
一个已经存在的对象去初始化一个马上创建市级的对象——拷贝构造
两个已经存在的对象,之间进行赋值操作——赋值拷贝
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date& operator=(const Date& d)//成员函数默认有一个隐藏的this指针,加const保持赋值的那个对象不变,具有常属性
{
if (this != &d)//预防自己给自己拷贝,没有这个必要
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;//引用返回,减少值拷贝
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2022, 2, 1);
Date d2(d1);
Date d3(2000, 1, 1);
d1 = d3;
//将d3的内容拷贝给d1,将d1的内容给覆盖了
return 0;
}
赋值运算符主要特点:
1、参数类型为引用,并且用const修饰
右操作数是已经存在的对象了,我们一般不改变它,所以加个const修饰
2、使用引用返回,为了减少多次拷贝
我们若是d2=d1,这样赋值,完全没有必要有返回值,但是若是连续赋值d3=d2=d1,返回给d2的值要拷贝一份,再赋值给d3,这样最后再拷贝一份返回调用,这样形成了多重拷贝,为了避免不必要的拷贝我们使用引用返回
3、检测是否自己给自己赋值
若是出现d1=d1这种没有不要的赋值,我们可以避免不必要赋值操作
4、return *this
出了作用域*this还存在,*this就是d对象(对象并未销毁),用引用返回减少拷贝
5、一个类中如果没有显示定义赋值运算符重载,编译器会默认生成一个,会完成按对象的字节序拷贝
编译器默认生成赋值重载,根拷贝构造函数做的事情类似
1)对于内置类型,会完成按字节序的浅拷贝
2)对于自定义类型,会去调用它自己的运算符重载
五、const成员
5.1 const修饰类的成员函数
将const修饰的类成员函数称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改
class Date
{
public :
void Display ()
{
cout<<"Display ()" <<endl;
cout<<"year:" <<_year<< endl;
cout<<"month:" <<_month<< endl;
cout<<"day:" <<_day<< endl<<endl ;
}
void Display () const
{
cout<<"Display () const" <<endl;
cout<<"year:" <<_year<< endl;
cout<<"month:" <<_month<< endl;
cout<<"day:" <<_day<< endl<<endl;
}
private :
int _year ; // 年
int _month ; // 月
int _day ; // 日
};
void Test ()
{
Date d1 ;//普通对象
d1.Display ();
const Date d2;//const修饰对象
d2.Display ();
}
结论:成员函数加const是好的,建议能加的都加上。这样普通对象和const修饰的对象都可以调用,但是如果要修改成员变量的成员函数是不能加const的
六、取地址以及const取地址操作符重载
这两个默认成员函数一般不用重新定义,编译器默认会生成
class Date
{
public :
Date* operator&()
{
return this ;
}
const Date* operator&()const
{
return this ;
}
private :
int _year ; // 年
int _month ; // 月
int _day ; // 日
};
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容!
谢谢观看!