目录
[🚈1. 类的6个默认成员函数](#🚈1. 类的6个默认成员函数)
[🚈2. 构造函数](#🚈2. 构造函数)
[🚝2.1 概念](#🚝2.1 概念)
[🚈3. 析构函数](#🚈3. 析构函数)
[🚝3.2 特性](#🚝3.2 特性)
[🚈4. 拷贝构造函数](#🚈4. 拷贝构造函数)
[🚝4.2 特征](#🚝4.2 特征)
[🚝4.4 拷贝构造函数典型调用场景](#🚝4.4 拷贝构造函数典型调用场景)
[🚈5. 赋值运算符重载](#🚈5. 赋值运算符重载)
[🚝5.1 运算符重载](#🚝5.1 运算符重载)
[🚝5.2 赋值运算符重载](#🚝5.2 赋值运算符重载)
[✈️5.2.1. 赋值运算符重载格式](#✈️5.2.1. 赋值运算符重载格式)
[✈️5.2.2. 赋值运算符只能重载成类的成员函数不能重载成全局函数](#✈️5.2.2. 赋值运算符只能重载成类的成员函数不能重载成全局函数)
[🚝 5.3编译器默认生成的赋值函数](#🚝 5.3编译器默认生成的赋值函数)
[🚝5. 4前置++和后置++重载](#🚝5. 4前置++和后置++重载)
[🚈6. const成员函数](#🚈6. const成员函数)
[🚈7. 取地址及const取地址操作符重载](#🚈7. 取地址及const取地址操作符重载)
🌞0.前言
言C++之言,聊C++之识,以C++会友,共向远方。各位博友的各位你们好啊,这里是持续分享C++知识的小赵同学,今天要分享的C++知识是C++类与对象,在这一章,小赵将会向大家继续聊聊C++类与对象。✊
🚈1. 类的6个默认成员函数
引言:如果一个类中什么成员都没有,简称为空类。 空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。
默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。
🚈2. 构造函数
🚝2.1 概念
啥叫构造函数呢?我们首先来看看这样一个类
cpp
class Date
{
public:
void Init(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;
};
我们前面说了我们的类里面可以装函数,也就是说我们可以不用像以前一样在外面定义变量了,然后再对变量初始化,我们可以将这些都写进类里面,并且由于我们之前说的this指针的出现,我们这里也不需要再传地址了,只需要将需要初始化的变量写进去,然后进行初始化就行。
cpp
int main()
{
Date d1;
d1.Init(2024, 9, 1);
d1.Print();
Date d2;
d2.Init(2024, 9, 2);
d2.Print();
return 0;
}
但是如果我们没写一个类都要去手动调用初始化,那是不是有点太麻烦了,为什么不能试着去让编译器自己调用呢?我们想到了,我们的C++祖师爷也想到了,他就在类里面加入了构造函数。
🚝2.2特性
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证 每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次。
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任 务并不是开空间创建对象,而是初始化对象。
其特征如下:
1. 函数名与类名相同。
2. 无返回值。
3. 对象实例化时编译器自动调用对应的构造函数。
4. 构造函数可以重载。
那么对于上面的代码我们就可以改成这样:
cpp
class Date
{
public:
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 a(2024, 9, 1);
a.Print();
}
同时在这个时候我们还可以加入我们之前学的缺省函数的知识,让我们的构造函数更方便
cpp
class Date
{
public:
Date(int year=2024, int month=9, int day=1)//对私有成员进行初始化
{
_year = year;
_month = month;
_day = day;
}
void Print()//打印函数
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date a(2024, 9, 2);
Date b(2024, 10);
Date c(2025);
//Date d();//不能这样写,不需要参数时候不用()
Date f;
//b();
a.Print();
b.Print();
c.Print();
//d.Print();
f.Print();
}
注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明 ;(这里的设计我觉得主要还是为了避免冲突,因为我们之前函数声明时就是 void print();这两种是非常相似的,所以这里不允许这种方式也是有理可原的)。
然后我们再看看我们的函数重载与这里的结合。
cpp
class Date
{
public:
Date()//不需要参数
{
_year = 2024;
_month = 9;
_day = 2;
}
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 a(2024, 9, 2);
Date b;
a.Print();
b.Print();
}
🚝2.3编译器默认生成的构造函数。
我们之前说编译器会在我们的类中生成6个默认构造函数,那其实构造函数也是可以默认生成的,为什么不用我们的默认构造函数呢?好那么我们下面就来看看我们的默认构造函数究竟存在什么样的问题。
注意:如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦 用户显式定义编译器将不再生成。
首先我们发现我们默认构造函数是不需要传参的,没有参数。
接着我们发现他给我们处理的数据是一个随机数,也就是说这种初始化是不能满足我们的需求的,既然不能够去满足我们的需求,那这样的默认构造函数的意义在哪里呢?这让我们不得不向下学习。
小赵在网络上搜到了这样一个有意思的自问自答
关于编译器生成的默认成员函数,很多童鞋会有疑惑:不实现构造函数的情况下,编译器会 生成默认的构造函数。但是看起来默认构造函数又没什么用?d对象调用了编译器生成的默 认构造函数,但是d对象_year/_month/_day,依旧是随机值。也就说在这里编译器生成的 默认构造函数并没有什么用??
解答:C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类 型,如:int/char...,自定义类型就是我们使用class/struct/union等自己定义的类型。
这句话是什么意思呢,就是内置类型它不去处理,但是如果你是自定义类型它就可以处理了,那这个咋用呢?看看下面小赵编写的这个程序或许我们的疑惑可以解决一点
cpp
class Time
{
public:
Time()
{
cout << "Time()" << endl;
_hour = 0;
_minute = 0;
_second = 0;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
// 基本类型(内置类型)
int _year;
int _month;
int _day;
// 自定义类型
Time _t;
};
int main()
{
Date d;
return 0;
}
这就是我们的嵌套类,就是我类的里面还有一个类的时候这个的作用很大。
但就算是这样,我们还是感觉这个东西不太好因为这样设计出来的化,我们不写默认构造函数,哪些内置类型怎么办还是很麻烦,于是在我们的C++11的时候就对这个问题进行了一定的解决(当然这个问题还是不可能从根源上解决不然会导致前人写的程序代码无法运行,那对公司和个人都是巨大的损失,只能是去添加新的东西。)
解决方案:C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在 类中声明时可以给默认值。
cpp
class Time
{
public:
Time()
{
cout << "Time()" << endl;
_hour = 0;
_minute = 0;
_second = 0;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
// 基本类型(内置类型)
int _year=2024;
int _month=9;
int _day=1;
// 自定义类型
Time _t;
};
int main()
{
Date d;
return 0;
}
那么这样一种更改就可以解决我们的很多之前的问题了。
✈️补充1:
无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。 注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为 是默认构造函数。
✈️补充2:开空间问题
前面我们聊了一系列的类型,这里我们在自定义类型里面单拿出一个来聊聊,这个东西就是指针,这里如果我们用编译器自动生成的化也是不会对其处理的(好像有的编译器的地下现在会把它赋值成空指针nullptr),也就是说这个指针如果我们不对其处理它就是随机地址,乱用的话会有问题,所有指针的话大家一定要对其初始化,不然会有很大的问题的。
🚈3. 析构函数
引入:通过前面构造函数的学习,我们知道一个对象是怎么来的,那一个对象又是怎么没呢的?
🚝3.1概念
析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由 编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
🚝3.2 特性
析构函数是特殊的成员函数,其特征如下:
1. 析构函数名是在类名前加上字符 ~。
2. 无参数无返回值类型。
**3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。**注意:析构 函数不能重载(毕竟没有参数也没有返回值)
4. 对象生命周期结束时,C++编译系统系统自动调用析构函数。(和构造函数一样都是自动生成的)
cpp
typedef int DataType;
class Stack
{
public:
Stack(size_t capacity = 3)
{
cout << "Stack()" << endl;//方便观察其是否调用
_array = (DataType*)malloc(sizeof(DataType) * capacity);
if (NULL == _array)
{
perror("malloc申请空间失败!!!");
return;
}
_capacity = capacity;
_size = 0;
}
void Push(DataType data)
{
// CheckCapacity();
_array[_size] = data;
_size++;
}
~Stack()
{
cout << "~Stack()" << endl;//方便观察其是否调用
if (_array)
{
free(_array);
_array = NULL;
_capacity = 0;
_size = 0;
}
}
private:
DataType* _array;
int _capacity;
int _size;
};
void TestStack()
{
Stack s;
s.Push(1);
s.Push(2);
}
int main()
{
TestStack();
}
这里我们补一下前面的一个点,没有去捕捉看一下是否调用构造函数和析构函数,这里我们调用发现它确实调用了两个函数。同时大家如果去一点点调试的话,会发现我们的析构函数是在所有东西结束之后调用的,也就是在它定义的那个函数栈帧里,这里用我们之前的就是说这个函数栈帧在结束的时候会自动销毁变量。(但是这里要注意我们的指针,我们自己去开辟空间的时候,函数栈帧结束的时候是不会对其进行销毁的。)
🚝3.3编译器默认生成的析构函数。
接下来我们看编译器的析构函数
其实我们的编译器默认生成的析构函数和我们编译器默认生成的构造函数一样,只对自定义类型处理,不对内置类型处理。
下面我们看下面的代码去验证我们的上述说的;
cpp
class Time
{
public:
~Time()//析构函数
{
cout << "~Time()" << endl;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
// 基本类型(内置类型)
int _year = 1970;
int _month = 1;
int _day = 1;
// 自定义类型
Time _t;
};
int main()
{
Date d;
return 0;
}
在main方法中根本没有直接创建Time类的对象,为什么最后会调用Time类的析构函数?
因为:main方法中创建了Date对象d,而d中包含4个成员变量,其中_year, _month,
_day三个是内置类型成员,销毁时不需要资源清理,最后系统直接将其内存回收即可;而_t是Time类对象,所以在d销毁时,要将其内部包含的Time类的_t对象销毁,所以要调用Time类的析构函数。
但是:
main函数中不能直接调用Time类的析构函数,实际要释放的是Date类对象,所以编译器会调用Date类的析构函数,而Date没有显式提供,则编译器会给Date类生成一个默认的析构函数,目的是在其内部调用Time 类的析构函数,即当Date对象销毁时,要保证其内部每个自定义对象都可以正确销毁。main函数中并没有直接调用Time类析构函数,而是显式调用编译器为Date类生成的默认析构函数。
注意:创建哪个类的对象则调用该类的析构函数,销毁那个类的对象则调用该类的析构函数
问题:我们上面说了我们自己开辟的空间,是对指针(也就是内置类型),那么这部分空间既不会被函数栈帧销毁,也不会被默认析构函数处理,那怎么办呢?这里就是要重点说的,如果我们自己去开辟空间一定一定要自己去写析构函数,不然就会导致空间被不断的占用可能会造成空间越来越小的情况。
所以总结就是,如果我们没有申请空间资源可以使用由编译器自动生成的析构的函数,但是如果有申请资源,那么就必须去手动写析构函数。
🚝3.4补充
这里要注意的先构造的对象往往后析构,后构造的对象先异构。(当然这个也要和我们的函数栈帧的那个结合,也就是我们之前说的生存周期)。
🚈4. 拷贝构造函数
引入:在现实生活中,可能存在一个与你一样的自己,我们称其为双胞胎。
(CV双胞胎,哈哈)
那在创建对象时,可否创建一个与已存在对象一模一样的新对象呢?
那么我们该如何去创建一个和前面的一模一样的对象呢?如果直接去创建显然是太烦了,有木有什么好的办法呢?于是我们就引入了我们的拷贝构造函数。
🚝4.1概念
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
其实光看这个还是蛮烦的,还是看下面的实现回头再看可能会恍然大悟。
🚝4.2 特征
拷贝构造函数也是特殊的成员函数,其特征如下:
1. 拷贝构造函数是构造函数的一个重载形式。
2. 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错, 因为会引发无穷递归调用。
先给大家看看拷贝构造函数长啥样
cpp
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// Date(const Date d) // 错误写法:编译报错,会引发无穷递归
Date(const Date& d) // 正确写法//拷贝构造函数
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2(d1);
return 0;
}
看完拷贝构造函数,我们发现它和我们构造函数是很像的,只是里面的参数是一个const对象同时还是引用,那么为什么是这样一个对象呢?为什么使用我们之前的传值方式会引发我们的无穷递归呢?不急,我们慢慢来解决。
首先从效率上讲传引用效率更高,不用重新创建对象,而且加上了const也防止了你对其进行修改。
第二就是我们的无穷递归问题:
解析:这里引发无穷递归的主要原因在于:我们要传值给拷贝构造,而如果我们是传值给拷贝构造,那么拷贝构造的参数date就要去拷贝构造一个d1,这个刚好又是我们的拷贝构造(传一个参数去构造),那么就要再次调用拷贝构造,成了俄罗斯套娃了。这里有点难理解,各位可以多看多想一定能绕过来。
🚝4.3编译器的拷贝构造函数及其的问题
若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按
字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
这句话是啥意思呢?就是如果你没有涉及到到自己内存资源的调用拷贝(malloc,new),我都可以直接去赋值,但如果有内存的拷贝就要自己去写,这个我们下面还会展开说其中的原因。
自定义拷贝构造函数
cpp
class Time
{
public:
Time()
{
_hour = 1;
_minute = 1;
_second = 1;
}
Time(const Time& t)
{
_hour = t._hour;
_minute = t._minute;
_second = t._second;
cout << "Time::Time(const Time&)" << endl;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
// 基本类型(内置类型)
int _year = 1970;
int _month = 1;
int _day = 1;
// 自定义类型
Time _t;
};
int main()
{
Date d1;
// 用已经存在的d1拷贝构造d2,此处会调用Date类的拷贝构造函数
// 但Date类并没有显式定义拷贝构造函数,则编译器会给Date类生成一个默认的拷贝构
造函数
Date d2(d1);
return 0;
}
系统默认生成的拷贝构造函数
cpp
class Time
{
public:
Time()
{
_hour = 1;
_minute = 1;
_second = 1;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
// 基本类型(内置类型)
int _year = 1970;
int _month = 1;
int _day = 1;
// 自定义类型
Time _t;
};
int main()
{
Date d1;
// 用已经存在的d1拷贝构造d2,此处会调用Date类的拷贝构造函数
// 但Date类并没有显式定义拷贝构造函数,则编译器会给Date类生成一个默认的拷贝构造函数
Date d2(d1);
return 0;
}
监视窗口发现都是一样的
注意:在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定
义类型是调用其拷贝构造函数完成拷贝的。 //这一点和我们之前的默认构造函数,析构函数是一样的。
好了上面的程序已经验证了我们上面说的,对于可以直接进行传值的,我们的编译器生成的拷贝构造可以轻松完成任务,那么对于我们说的要申请空间的呢?
cpp
typedef int DataType;
class Stack
{
public:
Stack(size_t capacity = 10)
{
_array = (DataType*)malloc(capacity * sizeof(DataType));
if (nullptr == _array)
{
perror("malloc申请空间失败");
return;
}
_size = 0;
_capacity = capacity;
}
void Push(const DataType& data)
{
_array[_size] = data;
_size++;
}
~Stack()
{
if (_array)
{
free(_array);
_array = nullptr;
_capacity = 0;
_size = 0;
}
}
private:
DataType *_array;
size_t _size;
size_t _capacity;
};
int main()
{
Stack s1;
s1.Push(1);
s1.Push(2);
s1.Push(3);
s1.Push(4);
Stack s2(s1);
return 0;
}
这个时候我们发现我们的程序崩溃掉了,这里当然与我们说的编译器生成的拷贝构造函数对于我们申请空间的这一类拷贝会有问题,那么问题到底在哪里呢?
小赵在这里画了个图,写了点注释
其实这里的主要原因就是在于它直接传值,让你的指针和我的指向同一块空间,那么我们的对象进行析构的时候,就相当于对这块空间释放了两次,同时两个对象公用一块空间还会导致两个对象在修改对象数据的时候影响彼此,所以这里的拷贝构造也是要我们自己去写的。
注意:类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请 时,则拷贝构造函数是一定要写的,否则就是浅拷贝。 (浅拷贝就是直接传值)
🚝4.4 拷贝构造函数典型调用场景
使用已存在对象创建新对象
函数参数类型为类类型对象(传一个对象的时候)
函数返回值类型为类类型对象(函数返回值直接返回时候调用拷贝构造)
cpp
class Date
{
public:
Date(int year, int minute, int day)
{
cout << "Date(int,int,int):" << this << endl;
}
Date(const Date& d)
{
cout << "Date(const Date& d):" << this << endl;
}
~Date()
{
cout << "~Date():" << this << endl;
}
private:
int _year;
int _month;
int _day;
};
Date Test(Date d)//函数传对象和函数返回对象都要拷贝构造
{
Date temp(d);
return temp;
}
int main()
{
Date d1(2022,1,13);
Test(d1);
return 0;
}
解析:
为了提高程序效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能用引用尽量使用引用。
🚈5. 赋值运算符重载
🚝5.1 运算符重载
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其 返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)
注意:
- 不能通过连接其他符号来创建新的操作符:比如operator@
- 重载操作符必须有一个类类型参数
- 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义
- 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐 藏的this
- .* 、:: 、sizeof、?: 、. 、注意以上5个运算符不能重载。这个经常在笔试选择题中出 现。
虽然有这么多注意点和一些运算符无法重载,但是运算符的重载无疑是特别重要的发明,它大大加强了代码的可读性,让我们的自定义类型在使用时可以被更好的阅读。
好了我们先给大家重载一个玩玩
cpp
class Date
{
public:
void operator+=(int x)//重载加等
{
while (x--)
{
int months[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
_day += 1;
if (_year % 4 == 0 && _year % 100 != 0 || _year % 400 == 0)
{
months[2] += 1;
}
if (_day > months[_month])
{
_month++;
if (_month > 12)
{
_year++;
_month = 1;
}
_day = 1;
}
}
}
void print()
{
cout << "year:" << _year << "->month:" << _month << "->_day:" << _day;
}
private:
int _year=2024;
int _month=9;
int _day=1;
};
重载了+=我们下面就可以这样玩了
其实很多人也想到了为什么不能这样定义在外面,比如这个判断相等的例子
cs
// 全局的operator==
class Date
{
public:
Date(int year = 1900, 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)
{
return d1._year == d2._year
&& d1._month == d2._month
&& d1._day == d2._day;
}
void Test ()
{
Date d1(2018, 9, 26);
Date d2(2018, 9, 27);
cout<<(d1 == d2)<<endl;
}
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// bool operator==(Date* this, const Date& d2)
// 这里需要注意的是,左操作数是this,指向调用函数的对象
bool operator==(const Date& d2)
{
return _year == d2._year;
&& _month == d2._month
&& _day == d2._day;
}
private:
int _year;
int _month;
int _day;
};
我在注释里面也说了,其的封装性无法去保证,所以我们还是去用我们的运算符重载好用。
🚝5.2 赋值运算符重载
✈️5.2.1. 赋值运算符重载格式
参数类型:const T&,传递引用可以提高传参效率
返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
检测是否自己给自己赋值
返回*this :要复合连续赋值的含义
好了看这么多定义先重载一个给大家看看
cpp
class Date
{
public :
Date(int year = 1900, 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)
{
if(this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
private:
int _year ;
int _month ;
int _day ;
};
✈️5.2.2. 赋值运算符只能重载成类的成员函数不能重载成全局函数
下面我们来看下面的一个问题
cpp
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
int _year;
int _month;
int _day;
};
// 赋值运算符重载成全局函数,注意重载成全局函数时没有this指针了,需要给两个参数
Date& operator=(Date& left, const Date& right)
{
if (&left != &right)
{
left._year = right._year;
left._month = right._month;
left._day = right._day;
}
return left;
}
// 编译失败:
// error C2801: "operator ="必须是非静态成员
原因:赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现 一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值 运算符重载只能是类的成员函数。
这句话是啥意思呢?就是说我们如果在类外实现的话,会与编译器自动生成的冲突 。(这里主要原因还是我们的赋值运算符是我们说的默认成员函数之一),那么就会产生冲突。
这里附上我们的C++prime的一句话:
🚝 5.3编译器默认生成的赋值函数
用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。注
意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符
重载完成赋值。
其实我们看了这么多编译前默认生成的函数,发现只要不涉及到申请空间的,指针等就可以直接用,但是涉及到这些就要我们去手动写。(其实我们后面大多数情况还是会遇到要申请空间,指针等,所以大多数时候我们还是要自己去写)
这里我们就不举例了,和我们上面的拷贝构造是极其相似的。大家可以自己去调试。
注意:如果类中未涉及到资源管理,赋值运算符是否实现都可以;一旦涉及到资源管理则必须要实现。
🚝5. 4前置++和后置++重载
这里我们单独谈一下前置++和我们的后置++,这里的主要原因还是我们的函数重载对这两个一模一样的如何去重载运算符。
cpp
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// 前置++:返回+1之后的结果
// 注意:this指向的对象函数结束后不会销毁,故以引用方式返回提高效率
Date& operator++()//
{
_day += 1;
return *this;
}
// 后置++:
Date operator++(int)
{
Date temp(*this);
_day += 1;
return temp;
}
//注意:后置++是先使用后+1,因此需要返回+1之前的旧值,故需在实现时需要先将this保存
一份,然后给this+1而temp是临时对象,因此只能以值的方式返回,不能返回引用
private:
int _year;
int _month;
int _day;
};
前置++和后置++都是一元运算符,为了让前置++与后置++形成能正确重载
C++规定:后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器
自动传递
有了这些我们就可以试着去写实现一个完整的日期类了 。
🚈6. const成员函数
将const修饰的"成员函数"称之为const成员函数,const修饰类成员函数,实际修饰该成员函数 隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
其实看完这个定义,小赵想到了一个问题就是如果我们是const对象在调用内部函数的时候,我们传入的this其实是个const类型,那么我们写函数的时候就应该要去写一个const类型不然就是一个权限的放大(这里主要说的是函数本身的参数不是const类型,无法去接收一个const 的对象),那么我们该如何把类里面的这个默认的这个this变量定义成const类型呢?难道要显示着写this吗?这里我们的C++祖师爷想到了一个极其巧妙的方法就是下面这种:
完成上面的函数
cpp
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << "Print()" << endl;
cout << "year:" << _year << endl;
cout << "month:" << _month << endl;
cout << "day:" << _day << endl << endl;
}
void Print() const
{
cout << "Print()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(2022,1,13);
d1.Print();
const Date d2(2022,1,13);
d2.Print();
}
然后我们看看下面几个问题
请思考下面的几个问题:
1. const对象可以调用非const成员函数吗?
这个一定是不可以的const类型的权限要比非const类型小,这里就是权限的放大。
2. 非const对象可以调用const成员函数吗?
这里是可以的,非const类型的权限要比const类型大,因此可以调用。
3. const成员函数内可以调用其它的非const成员函数吗?
不能这里和1问题一样是权限的放大
4. 非const成员函数内可以调用其它的const成员函数吗?
可以和2一样是权限的缩小
注释:其实const的函数和非const函数的最大的区别就是非const函数会涉及到成员的值的改变,而const函数不会改变值。(用数学里面说就是非const是长方形,const是正方形)。
🚈7. 取地址及const取地址操作符重载
取地址及const取地址操作符:这两个默认成员函数一般不用重新定义 ,编译器默认会生成。
cpp
class Date
{
public :
Date* operator&()
{
return this ;
}
const Date* operator&()const
{
return this ;
}
private :
int _year ; // 年
int _month ; // 月
int _day ; // 日
};
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需
要重载,比如想让别人获取到指定的内容!
💎8.结束语
好了小赵今天的分享就到这里了,如果大家有什么不明白的地方可以在小赵的下方留言哦,同时如果小赵的博客中有什么地方不对也希望得到大家的指点,谢谢各位家人们的支持。你们的支持是小赵创作的动力,加油。
如果觉得文章对你有帮助的话,还请点赞,关注,收藏支持小赵,如有不足还请指点,方便小赵及时改正,感谢大家支持!!!