[前言]
类中有六大默认成员函数,默认成员函数就是用户不显示实现,编译器也会自动生成的成员函数。
目录
一、构造函数
构造函数虽然叫构造,但是它并不是开空间创建对象,而是在对象实例化的时候初始化
构造函数的本质就是为了替代Init函数
构造函数的特点:
- 函数名和类名相同
- 对象实例化的时候系统会自动调用对应的构造函数
- 构造函数无返回值( 不需要写void)
构造函数我们不写,系统也会自动生成一个构造函数,但是一旦我们在类中写了,系统就将不在生成,其目的都是为了初始化
系统生成的这个构造函数在初始化的时候就分两种情况:
- 内置类型成员(int/char...):系统对其初始化没有要求,是否初始化,全看编译器
- 自定义类型成员(class/struct...):系统会调用默认构造函数来初始化,如果没有默认构造函数,就将报错
这里又有一个新的专业名词,默认构造函数。那啥是默认构造函数嘞?
默认构造函数分为三种:
- 无参的构造函数
- 全缺省的构造函数
- 我们不写使,编译器默认生成的构造函数
以上三种都被称为默认构造函数,但是需要注意的是,默认构造函数有且仅有一个能存在,它们不能同时存在
总结一下就是,不传参就能调用的函数就是默认构造函数
class Date
{
public:
//Date() //无参构造函数
//{
// _year = 1;
// _month = 1;
// _day = 1;
//}
Date(int year = 1, int month = 1, int day = 1)//全缺省构造函数
{
_year = year;
_month = month;
_day = day;
}
//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 d1; //使用无参构造函数的时候,后面的小括号就不需要加了
Date d2(2024, 9, 8);
d1.Print();
d2.Print();
return 0;
}
唠叨一句,因为编译器的自动生成的默认构造函数不靠谱 ,所有构造函数还是得我们自己写
二、析构函数
析构函数不是把对象本身给销毁,而是把对象中的资源清理释放,以免造成资源泄露。
析构函数的功能有点像之前我们学的Destroy函数。
析构函数的特点:
- 析构函数的函数是类名前面加~
- 无参数无返回值
- 一个类只能有一个析构函数
- 对象生命周期结束的时候,系统会自动调用析构函数
若没有定义析构函数,系统会自动生成默认的析构函数。
析构函数对成员的处理也分为两种情况:
- 内置类型成员:析构函数对其不做处理,因为没有资源需要释放
- 自定义类型成员:会调用这个自定义类型里面的析构函数
注意哈,无论我们写不写析构函数,这个自定义类型成员,都只会调用自己类型的析构函数
typedef int STDataType;
class Stack
{
public:
Stack(int n = 4) //构造函数
{
_a = (STDataType*)malloc(sizeof(STDataType) * n);
if (_a == nullptr)
{
perror("malloc fail");
return;
}
_capacity = n;
_top = 0;
}
~Stack()
{
free(_a); //由于上面申请了资源,所以最后要清理释放
_a = nullptr;
_top = _capacity = 0;
}
private:
STDataType* _a;
int _capacity;
int _top;
};
//用两个栈实现队列
class MyQueue
{
public:
//由于成员变量都是自定义类型成员,所以写不写析构函数都无所谓
private:
Stack pushst; //自定义类型成员只会调用自己的析构函数,也就是Stack里面的析构函数
Stack popst;
};
int main()
{
Stack st;
MyQueue mq;
return 0;
}
如上代码,C++中有规定后定义的要先析构,也就是这里MyQueue mq先析构,Stack st后析构
如果类中没有申请资源的话,我们就可以不写析构函数,直接使用编译器默认生成的析构函数即可,但是一旦有资源申请时,必须要自己写析构函数,否则就会造成资源的泄露
三、拷贝构造函数
拷贝构造函数其实就是构造函数的重载,是一个特殊的构造函数。
特殊就特殊在其第一个参数必须是自身类类型的引用。并且其他参数都要有缺省值。
class Date
{
public:
//构造函数
Date(int year = 1, int month = 1, int day =1)
{
_year = year;
_month = month;
_day = day;
}
//由于成员变量都是内置类型,故析构函数可以不写
//拷贝构造函数
Date(const Date& d)
{
//const是为了避免权限被放大,毕竟有些时候传的可能是临时对象,而临时对象具有常性
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2024, 9, 11);
Date d2(d1); //拷贝构造一个d2
Date d3 = d1; //拷贝构造的另外一种写法,构造一个d3
return 0;
}
若没有显示定义拷贝构造函数,编译器也会自动生成。
自动生成的拷贝构造函数对成员的处理分为两种:
- 内置类型:浅拷贝/值拷贝,也就是一个字节一个字节的拷贝
- 自定义类型:调用该类型的拷贝构造
注!若类里面的成员变量都是内置类型,我们就可以不用定义拷贝构造函数了,但是有特殊情况,就比如Date和Stack都是只有内置类型,但Date不用定义拷贝构造,Stack却需要。这是因为Stack这样的类里面指向了一定的空间,如果我们还是使用浅构造的话,再拷贝一个对象出来,两个对象指向同一个空间,显然不合理,故得使用深拷贝(就像之前随机链表的复制一样)
有个小技巧就是,我们看有没有显示定义析构函数,只要有析构函数我们就定义拷贝构造函数,如果没有,我们也不需要写拷贝构造函数。
现在我们来说一下为啥拷贝构造函数的第一个参数必须要是类类型的引用?
在上图的代码中,如果我们想要调用func,就先得传参
而C++规定类类型的传值传参必须调用拷贝构造函数
所以在传参前我们就得调用拷贝构造了,如果这个时候拷贝构造函数的一个参数是类类型而不是类类型的引用,那么我们在调用拷贝构造函数的时候还得再传一个参数,但是根据规定,要先拷贝构造,所以以此往复就是传参、调用拷贝构造函数、传拷贝构造函数里面的参数、调用拷贝构造函数、传参......
当然了,为了避免后续引发的无穷递归调用的现象,当我们把拷贝构造函数的第一个参数错误的写成类类型的时候,编译器就会直接报错了
如果第一个参数是类类型的引用就完全不会有这种顾虑,因为在调用一次拷贝构造函数的时候,咱就给这个实参取了一个别名
在 自定义类型进行拷贝行为时,是必须得调用拷贝构造函数的。
自定义类型的拷贝行为有两种:
- 传值传参
- 传引用返回
传值传参:
void Func1(Date d)
{
//...
}
int main()
{
Date d1(2024, 9, 11);
Func1(d1);
return 0;
}
传引用返回:
Date& Func2()
{
//...
}
int main()
{
Date d1(2024, 9, 11);
Date ret = Func2();
return 0;
}
注!局部对象不能返回它的引用
局部对象在函数结束的时候就销毁了,如果这个时候再次引用它返回,这里的引用就类似于一个野指针。
故当传引用返回的时候一定要确保该返回对象在函数结束后还继续存在
总而言之就是传值传参怎么传都可以,但是传引用返回 一定要小心和细心。
四、赋值运算符重载
在聊赋值运算符重载之前,我们先来了解一下什么是运算符重载
我们知道内置类型可以直接通过运算符进行运算,但是如果我们想把类类型也通过这样的方式运算,简单的使用运算符就不可取了。
C++规定,对类类型对象使用运算符时,必须转换成调用对应的运算法重载
运算符重载也可以理解为运算符重载函数,特点如下:
- 具有特殊的名字:名字由operator和一个运算符共同构成
- 参数个数,同那个运算符的作用对象一样多
- 运算符重载作为成员函数时,第一个运算对象默认传给了隐藏的this指针(也就是参数看上去少了一个)
- 运算符重载函数至少有一个参数是类类型的
有些时候我们必不可少的会用类类型进行运算,比如日期的加减,但是由于编译器不知道这个类的比较方式应该是什么样的,所以这个运算比较的方法就由我们自己去定义,由此啊运算符重载的出现是很有必要的
如下,简单写一个运算符重载函数:
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
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;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2024, 9, 11);
Date d2(2024, 9, 18);
bool ret1 = d1.operator<(d2); //显示调用运算符重载对应的运算符重载函数
bool ret2 = d1 < d2; //即使不显示调用,在编译的时候也会转换成
return 0;
}
关于运算符重载呢,还有点想说的:
- 不能使用新的运算符,比如说@
- 有五个运算符不能实现重载,分别是 .* ,:: ,sizeof ,?: ,.
- 重载++运算符时,为了区分前置++和后置++,重载后置++时,要增加一个int的形参
好的,了解完运算符重载函数,现在我们就进入赋值运算符重载的世界~~~
赋值运算符重载是一个运算符重载,规定赋值运算符重载必须是成员函数
因为赋值运算符重载是一个默认成员函数
注!要将赋值运算符重载函数和拷贝构造函数进行区分,前者用于完成两个已经存在的对象的直接拷贝赋值,而后者用于一个已经存在的对象给另一个将要创造的对象拷贝
赋值运算符重载函数的一些特点:
- 参数要用const来修饰,
- 有返回值,其目的是为了能够支持连续赋值的操作
- 建议参数和返回值都写成引用的形式,减少拷贝(指的是另外又创一个空间),提高效率
- 没有显示实现的时候,编译器会自动生成
赋值运算符重载的自动生成的内部规则,和拷贝构造函数类似,也是分两种情况,这里不再重复赘述,可以返回到【三、拷贝构造函数】那块再看一遍
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& operator=(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this; //this是地址,所以要有个解引用
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2024, 9, 11);
Date d2(2024, 9, 18);
//赋值运算符重载
d2 = d1;
//拷贝构造
Date d3 = d1;
return 0;
}
到此,我们先总结一下以上的四种默认成员函数:
- 构造一般需要自己写,自己传参定义初始化
- 对于析构来说,如果构造时涉及到了资源的申请,那就需要自己写析构了
- 如果自己显示写了析构,那么拷贝构造和赋值重载也需要自己写
五、取地址运算符重载
取地址运算符重载作为默认成员函数中的一个函数来说,还是非常简单的。不过在讲取地址运算重载之前呢,要先讲一个const成员函数,而这个const成员函数是一个比较重要的小知识点。
const修饰的成员函数就是const成员函数,其形式就是在成员函数的后面加上一个const。
注意!如果是const成员函数,那么其声明和定义的后面都要加const
那啥是const成员函数嘞?
先来回顾一个知识,那就是
- const在*前面修饰的就是指向的内容,const在*的右边修饰的就是指针本身
- 修饰指针本身的const不存在权限的放大和缩小,只有修饰指向内容的const才存在
之前我们聊到的const说的一直都是const引用相关的内容,但是如果传一个const修饰的对象给成员函数嘞?
既然是const修饰,那么如果函数只是简单的接收就相当于把权限放大了,很显然这就是错误的了,因此成员函数为了能够接收一个const修饰的对象,必须发生相应的调整,即把成员函数变成const成员函数
const成员函数当中的const,实际修饰的是隐含this指针指向的内容,也就是对该成员函数中出现的成员变量不能进行修改,那么我们刚刚接收的const修饰的对象传进来的时候就不怕被修改了
之前在类和对象上我们有学到一个知识点是this指针,在Date类中,成员函数默认隐藏的this指针的形式是Date* const this,这里的const修饰的就是指针本身了,也就是说传过来的指针是不被允许发生改变的,不能够在指向其他地址。
由const修饰过的成员函数,在Date类中,其this指针就会由Date* const this变为const Date* const this
最后在多一嘴,在C++中,加const修饰的原则就是,能加的尽量都给加上
注!成员函数里面需要修改成员变量的时候不能加const,其他情况皆可以加const
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// void Print(const Date* const this) const
void Print() const //const成员函数就是在函数后面加const
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
// 这⾥⾮const对象也可以调⽤const成员函数,也就是⼀种权限的缩⼩
Date d1(2024, 7, 5);
d1.Print();
const Date d2(2024, 9, 21);
d2.Print();
return 0;
}
好的,说完了const成员函数,咱就简单说一下取地址运算符重载
取地址运算符重载分为两种
- 普通取地址符重载
- const取地址符重载
const取地址符重载也就是取一个地址,但是这个对象被const修饰了
其实正常情况来说,编译器自动生成的取地址符重载就已经完全够我们使用了,我们自己是不需要取定义的
但是,如果你不想让别人取到地址,那么你就可以自己去显示实现
如下,带大家看一下,编译器自动生成的取地址符重载是啥样的
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date* operator&()
{
return this;
// return nullptr;
}
const Date* operator&()const //const修饰返回值,表面返回值作为常量返回,不允许被修改
{
return this;
// return nullptr;
}
private:
int _year; // 年
int _month; // ⽉
int _day; // ⽇
};
int main()
{
Date d1(2024, 7, 5);
const Date d2(2024, 9, 21);
cout << &d1 << endl;
cout << &d2 << endl; //会自动匹配最适合自己的取地址符重载函数
return 0;
}
标签:const,函数,对象,C++,运算符,int,Date,重点,构造函数
From: https://blog.csdn.net/2401_83883881/article/details/142026901