文章目录

  • ​​默认成员函数​​
  • ​​1.构造函数​​
  • ​​1.1特性​​
  • ​​1.2基本使用​​
  • ​​1.3编译器默认生成的构造函数​​
  • ​​1.4规范命名类的成员变量​​
  • ​​2.析构函数​​
  • ​​2.1特性​​
  • ​​2.2基本使用​​
  • ​​3.拷贝构造​​
  • ​​3.1特性和使用​​
  • ​​3.2外置类型拷贝问题​​
  • ​​3.3深拷贝​​
  • ​​3.3.1new和delete​​
  • ​​3.3.2深拷贝实现​​
  • ​​3.3.3深拷贝效果​​
  • ​​4.运算符重载​​
  • ​​4.1定义​​
  • ​​4.2基本使用​​
  • ​​4.3赋值运算符重载​​
  • ​​4.4拷贝构造和赋值重载的调用问题​​
  • ​​5.const成员​​
  • ​​5.1用const修饰类的成员函数​​
  • ​​①实例-权限问题​​
  • ​​②什么时候需要使用?​​
  • ​​5.2取地址及对const取地址重载​​
  • ​​日期类的实现​​
  • ​​结语​​

默认成员函数

当我们创建一个类的时候,即便类里面啥都不放,都会自动生成下面6个默认成员函数

【C++】类和对象(第二站)默认成员函数+操作符重载_c++

它们都有啥功能呢?且听我一一道来

1.构造函数

众所周周知,当我们写C语言的顺序表、链表等代码的时候,一般都会写一个​​Init​​函数来初始化内容。

void Init()
{
a=(int*)malloc(sizeof(int)*4);
size=0;
capa=4;
}

但是这样有一个缺点,就是不够智能,需要我们自己来调用它进行初始化。

于是C++就整出来了一个构造函数来解决这个问题

1.1特性

构造函数:名字和类名相同,创建类对象的时候编译器会自动调用,初始化类中成员变量,使其有一个合适的初始值。构造函数在对象的生命周期中只调用一次

构造函数有下面几个特性:

  1. 函数名和类名相同
  2. 无返回值
  3. 构造函数可以重载
  4. 对象实例化的时候,编译器会自动调用对应的构造函数
  5. 如果你自己不写构造函数,编译器会自己创建一个默认的构造函数

1.2基本使用

下面用一个队列来演示一下构造函数

class Queue{
public:
Queue()
{
cout<<"Queue Init"<<endl;//测试是否调用
_a=(int*)malloc(sizeof(int)*4);
_size=0;
_capa=4;
}
void Print()
{
cout<<this<<": ";
cout<<"size: "<<_size<<" ";
cout<<"capa: "<<_capa<<endl;
}
private:
int* _a;
int _size;
int _capa;
};

可以看到,在创建对象q1的时候,编译器就自动调用了类中的构造函数,帮我们初始化了这个队列

【C++】类和对象(第二站)默认成员函数+操作符重载_c++_02


除了上面这种最基本的无参构造函数以外,一般写构造函数的时候,我们都会带一个有缺省值的参数,这样可以更好地灵活使用这个队列

Queue(int Capacity=4)
{
_a=(int*)malloc(sizeof(int)*Capacity);
_size=0;
_capa=Capacity;
}

调用这种构造函数也更加灵活,我们可以根据数据类型的长度,来创建不同容量的队列,避免多次​​realloc​​造成的内存碎片

Queue q1;//调用无参的构造函数
Queue q2(100);//调用带参的构造函数

多种构造函数是可以同时存在的,不过!它们需要满足函数重载的基本要求

当你调用一个无参的函数,和一个全缺省的函数的时候,编译器会懵逼!

Queue();
Queue(int Capacity=4);
//这两个函数不构成重载,会报错

正确的重载应该是下面的情况

Queue();
Queue(int Capacity);

编译器在创建对象的时候,就会智能选择这两个构造函数其中之一进行调用。但是同一个对象只会调用一个构造函数

1.3编译器默认生成的构造函数

上面提到过,如果我们不写构造函数,编译器会自己生成一个。

但测试过以后,你会发现,这个默认生成的构造函数,好像啥事都没有干——或者说,它把​​_a _b _c​​ 都初始化成了随机值!

【C++】类和对象(第二站)默认成员函数+操作符重载_拷贝构造_03

实际上,编译器默认生成的构造函数是不会处理内置类型的

  • 内置类型:int、char、float、double……
  • 外置类型:自定义类型(其他的类)

在处理的时候,编译器忽略内置类型;外置类型会调用它的构造函数

class Date{
public:
//默认构造函数:不传参就能调用的
//1.全缺省 2.无参 3.编译器自动生成
//可以是半缺省的,但是不实用
Date(int year=2022,int month=2,int day=30)
{
_year=year;
_month=month;
_day=day;
}
void Print()
{
cout<<_year<<"-"<<_month<<"-"<<_day<<endl;
_A.Print();
}
private:
//编译器会自动生成构造函数(如果你没有自己写的话)
//自动生成的构造函数是不会初始化内置类型的
//内置类型:int,char,double等等
int _year;
int _month;
int _day;
//外置类型:自定义类型
//外置类型会调用它自己的默认构造函数
Queue _A;
};

可以看到,编译器调用了自己的构造函数的同时,还调用了外置类型​​Queue​​的构造函数,搞定了它的初始化

【C++】类和对象(第二站)默认成员函数+操作符重载_编译器_04

如果我们去掉Date的构造函数,就能看到下面的情况。​​Queue​​成功初始化,但是内置类型的年月日都是随机值

【C++】类和对象(第二站)默认成员函数+操作符重载_拷贝构造_05

一般情况下一个C++类都需要自己写构造函数,下面这两个情况除外

  1. 类里面的成员都是自定义类型成员(且有自己的构造函数)
  2. 如果还有内置类型成员,声明时给了缺省值

注:只有类在声明变量的时候才可以给缺省值

//下面的情况就不需要写
class MyS{

private:
Queue q1;//自定义类型
Queue q2;
int a=1;//内置类型声明的时候给了缺省值
};

【C++】类和对象(第二站)默认成员函数+操作符重载_构造函数_06


1.4规范命名类的成员变量

为了更好的使用构造函数,以及区分类内外的函数类型

一般我们定义类中的成员变量的时候,都会使用一个下划线进行标明​​_YEAR​

在一些地方,你会看到函数名前面也带了一个​​_​​,这一般表明该函数是另外一个函数的子函数,同样是用于区分的。


不同人的代码风格不同,你可以选择你自己喜欢的风格,但不能影响我们程序的正常使用


比如下面这种情况,就会影响类的构造了

class Date{
public:
Date(int year=2022)
{
year=year;
}
private:
int year;
};

请问​​year=year​​里面的这个year,到底是成员变量,还是构造函数的传参呢?编译器又双懵逼了

实际上,编译器在找year的时候,会先在当前​​{ }​​中找,找到了传参的year,就不会去找其他地方的year了。所以这个语句实际上是传参过来的year自己给自己赋值,编译器会报错。


2.析构函数

和构造函数相对应,析构函数是对象在出了生命周期后自动调用的函数,用来爆破对象里的成员(如进行free操作)

生命周期是离这个对象最近的​​{ }​​括号

2.1特性

  • 析构函数名是在类名前加​​~​
  • 无参数,无返回值
  • 一个类只能有一个析构函数
  • 如果你没有自己写,编译器会自动生成一个析构函数

和构造函数一样,编译器自己生成的析构函数不会处理内置类型;会调用外置类型的析构函数

2.2基本使用

析构函数的定义和我们在外部写的​​Destroy​​函数一样,主要执行free操作

#include<iostream>
#include<stdlib.h>
using namespace std;

class Queue{
public:
Queue()
{
cout<<"Queue"<<endl;//测试是否调用
_a=(int*)malloc(sizeof(int)*4);
_size=0;
_capa=4;
}
void Print()
{
cout<<this<<": ";
cout<<"size: "<<_size<<" ";
cout<<"capa: "<<_capa<<endl;
}
~Queue()
{
//析构函数
free(_a);
_a=nullptr;
_size=_capa=0;
cout<<"distory:"<<this<<endl;//测试调用
}
private:
int* _a;
int _size;
int _capa;
};

假设我们在main函数里面定义了两个对象,你能说出q1和q2谁先进行析构函数的调用吗?

【C++】类和对象(第二站)默认成员函数+操作符重载_拷贝构造_07

可以看到,先调用的是q2的析构函数

【C++】类和对象(第二站)默认成员函数+操作符重载_开发语言_08

因为在底层操作中,编译器会给main函数开辟栈帧

栈遵从后进先出的原则,q2是后创建的,所以在析构的时候会先析构


3.拷贝构造

3.1特性和使用

拷贝构造是一个特殊的构造函数,它的参数是另外一个Date类型。在用已有的类类型对象来创建新对象的时候,由编译器自动调用

因为拷贝的时候我们不会修改d的内容,所以传的是​​const​

另外,我们必须进行传引用调用!


这里补充说明一下,下面的这个函数,在传参的时候,编译器会去调用Date的拷贝构造

void func(Date d);


如果你没有写拷贝构造,或者拷贝构造里面不是传引用,编译器会就递归不断创建新的对象进行值拷贝构造,程序就死循环辣

//拷贝构造,如果不写的时候,编译器会默认生成一个
//对内置类型进行值拷贝(浅拷贝)
Date(const Date& d)
{
_year=d._year;
_month=d._month;
_day=d._day;
//外置类型会调用外置类型的拷贝构造
Queue b(_A);
}

和构造、析构不同的是,编译器自己生成的拷贝构造终于有点用了

  • 它会对内置类型进行按内存存储的字节序完成拷贝,这种称为值拷贝(又称浅拷贝
  • 对外置类型会调用它的构造函数

3.2外置类型拷贝问题

但是!如果你使用了外置类型,该类型中包含malloc的时候,编译器默认生成的构造函数就不能用辣!

因为这时候,编译器默认生成的拷贝构造会进行值拷贝,拷贝完了之后,就会出现q1和q2指向同一个空间的情况。修改q2会影响q1,free的时候多次释放同一个空间会报错,不符合我们的拷贝构造的要求

【C++】类和对象(第二站)默认成员函数+操作符重载_c++_09

注意注意,malloc不行的原因是,数据是存在堆区里面,拷贝的时候,q2的​​_a​​得到的是一个地址,而不是拷贝了新的数据内容。


如果你在类里面定义了一个​​int arr[10]​​数组,这时候拷贝构造就相当于memcpy,是可以完成拷贝的工作的。


如何解决这个问题呢?我们需要使用深拷贝

这里我还没有学到那个地方,后续写深浅拷贝的博客的时候,再来填上这个坑


黑马16分钟视频速成完毕,前来填坑


3.3深拷贝

3.3.1new和delete

这里先给大家从C语言转到C++,讲解一下new和delete关键字,它们分别对应malloc和free

非常简单!比malloc的使用简单多了!

int main()
{
int*p1=new int;//开辟一个int类型的空间
int*p2=new int(10);//开辟一个int类型的空间,并初始化为10
int*p3=new int[10];//开辟10个int类型的空间
//注意后两个的括号区别!

delete p1;//销毁p1指向的单个空间
delete p2;//同上

//delete p3;//销毁p3指向的第一个空间,不能用于数组
delete[] p3;//销毁p3指向的数组

return 0;
}

怎么样?是不是超级简单!

【C++】类和对象(第二站)默认成员函数+操作符重载_编译器_10


3.3.2深拷贝实现

在上面写道过,编译器会自动生成拷贝构造函数,完成值拷贝工作。但是队列的代码里面包含堆区的空间,需要我们正确释放。这时候就需要自己写一个拷贝构造完成深拷贝????

//拷贝构造
Queue(const Queue& q)
{
_a=new int[*q._a];//注意解引用
memcpy(_a, q._a, q._capa*sizeof(int));//拷贝内容
_size=q._size;
_capa=q._capa;
}

用下面这个队列和日期类的混合代码用来测试深拷贝

#include<iostream>
#include<stdlib.h>
#include<string.h>
using namespace std;

class Queue{
public:
Queue()
{
cout<<"Queue Init"<<endl;//测试是否调用
//_a=(int*)malloc(sizeof(int)*4);
_a=new int[_capa];
_size=0;
_capa=4;
for(int i=0;i < _capa;i++)
{
_a[i]=i+1;
}
}
//拷贝构造
Queue(const Queue& q)
{
cout<<"Queue Copy"<<endl;
_a=new int[*q._a];
memcpy(_a, q._a, q._capa*sizeof(int));
_size=q._size;
_capa=q._capa;

}
void Print()
{
cout<<"this:"<<this<<" ";
cout<<"_a:"<<_a<<" ";
cout<<"size: "<<_size<<" ";
cout<<"capa: "<<_capa<<endl;
for(int i=0;i < _capa;i++)
{
cout<<_a[i]<<" ";
}
cout<<endl;
}
~Queue()
{
//析构函数
//free(_a);
delete[] _a;
_a=nullptr;
_size=_capa=0;
cout<<"distory:"<<this<<endl;
}
private:
int* _a;
int _size;
int _capa;
};

class Date{
public:
Date(int year=2022,int month=2,int day=30)
{
_year=year;
_month=month;
_day=day;
}
void Print()
{
cout<<_year<<"-"<<_month<<"-"<<_day<<endl;
_A.Print();
}

//拷贝构造,如果不写的时候,编译器会默认生成一个
//对内置类型进行值拷贝(浅拷贝)
Date(const Date& d)
{
_year=d._year;
_month=d._month;
_day=d._day;
//外置类型会调用外置类型的拷贝构造
//这时候我们没有写队列的拷贝构造,编译器自动生成
//结果就是,两个队列的int*a会指向同一个空间
//析构函数free的时候报错
Queue b(_A);
}
private:
//内置类型:int,char,double等等
int _year;
int _month;
int _day;
//外置类型:自定义类型
Queue _A;
};

int main()
{
Queue q1;
q1.Print();
cout<<endl;
Queue q2=q1;
q2.Print();

cout<<endl;
return 0;
}

3.3.3深拷贝效果

先注释掉​​Queue​​的拷贝构造函数析构函数(不然会报错)

看一看,发现在不写拷贝构造函数的时候,q2和q1的​​_a​​指向了同一个地址

【C++】类和对象(第二站)默认成员函数+操作符重载_构造函数_11

取消析构函数的注释,可以看到两次释放同一片空间,发生了报错

【C++】类和对象(第二站)默认成员函数+操作符重载_编译器_12

如果我们把写好的深拷贝构造加上,就不会出现这个问题

【C++】类和对象(第二站)默认成员函数+操作符重载_c++_13

当你加上给​​_a​​​里面初始化一些数据,以及打印​​_a​​​数据的函数后,就可以看到,不仅q2的​​_a​​有了自己全新的地址,其内部的值也和q1一样了

【C++】类和对象(第二站)默认成员函数+操作符重载_c++_14

这样写出来的拷贝构造,即便把队列中的​​int* _a​​​修改为​​char*​​或者其他类型,都能正确完成拷贝工作

【C++】类和对象(第二站)默认成员函数+操作符重载_开发语言_15

这里有一个小点哈,就是打印​​char* _a​​​的地址的时候,咱需要用​​printf​​​而不是​​cout​​​,因为cout会把​​_a​​直接当作字符串打印了,效果就变成了下面这样

【C++】类和对象(第二站)默认成员函数+操作符重载_编译器_16

用printf来控制输出格式为​​%x​​即可

printf("_a:%x ",_a);

4.运算符重载

4.1定义

在讲解赋值运算符重载之前,我们可以来认识一下完整的运算符重载


C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。


函数名为:关键字 ​​operator​​​运算符,如​​operator=​

函数原型:返回值类型 operator操作符(参数列表),如​​Date operator=();​

下面有几点注意:

  • 重载操作符必须有一个自定义类型的操作数(即操作符重载对内置类型无效)
  • 不能通过其他符号来创建新的操作符
  • 对于类类型的操作符重载,形参比操作数少一个传参(因为有一个默认的形参this指针)
  • 这5个操作符是不能重载的:​​.*​​​、​​::​​​、​​sizeof​​​、​​? :​​​、​​.​

4.2基本使用

以下是在全局定义的操作符重载,用于判断日期是否相等

bool operator==(const Date& d1, const Date& d2)
{
return d1._year == d2._year;
&& d1._month == d2._month
&& d1._day == d2._day;
}

当我们在main函数中使用​​d1==d2​​的时候,编译器就会自动调用该操作符重载

当然,你也可以自己来传参使用,如​​if(operator==(d1,d2))​​但是这样非常不方便,和调用一个而普通函数没啥区别,压根算不上操作符重载


当我们把它放入类​​Date​​中间,就需要修改成下面这样

bool operator==(const Date& d2)
{
return d1._year == d2._year;
&& d1._month == d2._month
&& d1._day == d2._day;
}

编译器在调用的时候,会优化成下面这样

bool operator==(Date* this, const Date& d2)
//显示调用为 d1.operator==(d2);

后续会以日期类为样板,实现更多的操作符重载

4.3赋值运算符重载

因为每一个类都有不同的成员,编译器不可能智能的进行赋值操作。这时候就需要我们自己写一个赋值运算符重载来进行赋值操作了

以日期类为例,赋值操作其实就是把内置类型成员一一赋值即可

Date& operator=(const Date& d){
if(this != &d)//避免自己给自己赋值
{
_year=d._year;
_month=d._month;
_day=d._day;
}
return *this;
}

编写赋值重载代码的时候,需要注意下面己点:

  • 返回值和参数类型(注意要引用传参,不然会调用拷贝构造)
  • 检测是否自己给自己赋值(避免浪费时间)
  • 因为返回的是​​*this​​,出了函数后没有销毁,所以可以用传引用返回
  • 一个类如果没有显式定义赋值运算符重载,编译器也会自己生成一个,完成对象按字节序的值拷贝。

如果类中有自定义类型,编译器会默认调用它的赋值运算符重载(这里也会涉及到深浅拷贝的问题,后面会在深浅拷贝的博客里详解)

4.4拷贝构造和赋值重载的调用问题

当赋值操作符和拷贝构造同时存在的时候,什么时候会调用赋值,什么时候会调用拷贝构造呢?

【C++】类和对象(第二站)默认成员函数+操作符重载_c++_17

在这两个函数中添加​​cout​​进行打印提示,可以看到:

  • 如果对象在之前已经存在,就会调用赋值重载
  • 如果是一个全新的变量在定义的时候初始化,就调用的是拷贝构造

【C++】类和对象(第二站)默认成员函数+操作符重载_开发语言_18

5.const成员

5.1用const修饰类的成员函数


将const修饰的类成员函数称之为​​const成员函数​​​,const修饰类成员函数,实际修饰的是该成员函数隐含的​​this指针​​,表明在该成员函数中不能对类的任何成员进行修改。


基本的修饰方法如下,在函数的括号后加const即可

void Print()const
{
cout<<_year<<endl;
}

实际修饰的是该函数隐含的this指针

this指针本身是​​Date*const​​​类型的,修饰后变为​​const Date* const​​类型

void Print(const Date* const this)
{
cout<<_year<<"-"<<_month<<"-"<<_day<<endl;
}

①实例-权限问题

这么说好像有点迷糊,我们用实例来演示一下为什么需要const修饰成员函数

class Date{
public:
Date(int year=2022,int month=2,int day=30)
{
_year=year;
_month=month;
_day=day;
}
void Print()
{
cout<<_year<<"-"<<_month<<"-"<<_day<<endl;
}
private:
int _year;
int _month;
int _day;
};

假设我们需要在函数中调用​​Print​​函数,在main中是可以正常调用的

int main()
{
Date d1(2022,5,10);
d1.Print();
return 0;
}

但当你用一个函数来进行这个操作的时候,事情就不一样了

void TEST(const Date& d)
{
d.Print();//d.Print(&d) -->const Date*
}
int main()
{
Date d1(2022,5,10);
d1.Print();//d1.Print(&d1) -->Date*
TEST(d1);

return 0;
}

这时候我们进行了引用调用,因为在TEST中我们不会修改d1的内容,所以用​​const​​进行了修饰

  • 这时候TEST中的​​d.Print()​​​函数调用,传入的是​​const Date*​​指针,指针指向的内容不能被修改
  • main中的​​d1.Print();​​​函数调用,传入的是​​Date*​​指针

于是就会发生权限冲突问题????

【C++】类和对象(第二站)默认成员函数+操作符重载_编译器_19

这时候如果我们在函数后面加了const,就可以避免此种权限放大问题。这样不管是main函数还是TEST函数中对​​Print()函数​​的调用,就都可以正常打印了!


②什么时候需要使用?

众所周周知,const修饰指针有下面两种形式

  • 在​​*​​之前修饰,代表该指针指向对象的内容不能被修改(地址里的内容不能改)
  • 在​​*​​之后修饰,代表该指针指向的对象不能被修改(指向的地址不能改)

this指针本身就是​​类型名* const​​类型的,它本身不能被修改。加上const之后,this指向的内容,既类里面的成员变量也不能被修改了。

知道了这一点后,我们可以合理的判断出:只要是需要修改类中成员变量的函数,就不需要在​​()​​后面加const修饰


5.2取地址及对const取地址重载

最后两个默认成员函数,编译器会自动生成。这两个函数一般都不需要重载,毕竟返回的本身就是一个this指针,没有什么奇怪的地方

class Date{ 
public :
Date* operator&()
{
return this ;
}

const Date* operator&()const
{
return this ;
}
private :
int _year ;
int _month ;
int _day ;
};

只有特殊情况,我们需要让&只获取特定内容的时候,才需要手动重载这两个函数


日期类的实现

类和对象第一站????中提到过,在项目协作的时候,我们一半要用定义和声明分离的形式来些一个项目。


下面就让我们用日期类来演示这样的操作


在类中定义的函数会被默认设置为内联,我们的目标就是:短小函数在​​.h​​​中定义,长函数在​​.h​​​中声明,在​​.cpp​​中定义

至于源码和解析嘛……大家直接来我的gitee仓库看吧!

注释写的很详细了⏲有啥问题可以在下面留言哦

结语

最后的最后,今天是5月20日,用下图给大家送上祝福????

【C++】类和对象(第二站)默认成员函数+操作符重载_编译器_20