企业网站建设作品分析,定制网站设计,哪些网站才能具备完整的八项网络营销功能,前端开发是什么专业前言#xff1a;在之前#xff0c;我们对类和对象的上篇进行了讲解#xff0c;今天我们我将给大家带来的是类和对象中篇的学习#xff0c;继续深入探讨【C】中类和对象的相关知识#xff01;#xff01;#xff01; 目录 1. 类的6个默认成员函数
2. 构造函数
2.1概念介…前言在之前我们对类和对象的上篇进行了讲解今天我们我将给大家带来的是类和对象中篇的学习继续深入探讨【C】中类和对象的相关知识 目录 1. 类的6个默认成员函数
2. 构造函数
2.1概念介绍
2.2 特性介绍
3. 析构函数
3.1 概念介绍
3.2 特性介绍
4. 拷贝构造函数
4.1 概念介绍
4.2 特征介绍
5. 赋值运算符重载
5.1概念引出
5.2 运算符重载
5.3 赋值运算符重载
6. const成员函数
7. 取地址及const取地址操作符重载 1. 类的6个默认成员函数
首先我们直接给出类中有哪六类默认1. 类的6个默认成员函数
此时我们可以会想到为什么要有这些默认成员函数这些默认成员函数会带来什么作用呢 要弄清楚这个问题我们先来引入一个“空类”的概念。
空类的定义如果一个类中什么成员都没有即一个类中没有成员变量也没有成员函数简称为空类。定义形式如下 class Date
{};通过如上代码发现空类中什么都没有放此时请大家认真思考一下空类中难道真的什么都没有吗
答案其实是否定的对于任何一个类来说它们都有六个默认成员函数即使是空类。经过编译器处理之后类【Date】便不在为空它会自动的生成六个默认的成员函数即使这六个成员函数什么也不做。
因此这就给我们解答了为什么要引入这六个默认成员函数具体大家可以这样理解
当我们定义一个类时在初始化之前就调用了打印函数这样会导致输出的是一个随机值为了避免这种情况所以c给了六种默认成员函数而且当我们定义的类为空类时都会自动生成六个默认成员函数。
具体还可以像如下这样分大家可以直观的感受各个函数的区别与功能
至此这个六个默认成员函数的作用与由来便给大家将清楚了接下来我们逐个去认识 2. 构造函数
2.1概念介绍
首先在正式的给出【构造函数】具体的概念前我们通过代码的方式来为大家做个前情铺垫这样大家可以直观的感受通过之前类和对象上的学习我相信大家都能写出一个如下日期类如果对其还有疑惑的话可以参考【类和对象上】 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;
};int main()
{Date d1;d1.Init(2023, 3, 15);d1.Print();Date d2;d2.Init(2023, 2, 14);d2.Print();return 0;
}对于上述这样一个日期类当我们构造出来之后一般都会先对其进行“初始化”的操作 但是有时候我们可能会忘记进行初始化操作直接对对象进行操作这时当我们不初始化就直接用可能就会出现问题 当我们进行调试时也可以直观的看到 因此为了解决当我们构造出函数之后未进行初始化就直接对对象进行操作的情况【C】就给出了今天我们将要学习的知识——构造函数。有了构造函数当我们每创建完一个对象就不用再去手动的调用【Init】函数因为在创建对象时编译器会自动去调用构造函数对对象进行初始化。 有了上述的认知之后在这里我给出构造函数的具体概念
构造函数 是一种特殊的方法。主要用来在创建对象时初始化对象 即为对象成员变量赋初始值总与【new】运算符一起使用在创建对象的语句中。特别的一个类可以有多个构造函数 可根据其参数个数的不同或参数类型的不同来区分它们 即构造函数的重载。2.2 特性介绍 构造函数是特殊的成员函数需要注意的是构造函数虽然名称叫构造但是构造函数的主要任 务并不是开空间创建对象而是初始化对象。 其特征如下
1. 函数名与类名相同。
这个意思很简单当我们定义好一个类后此时它的构造函数的函数名就确定好了跟当前类的类名是相同的。
2. 无返回值不能指定返回类型即使是void也不行。
3. 对象实例化时编译器自动调用对应的构造函数。虽然在一般情况下构造函数不被显式调用而是在创建对象时自动被调用。但是并不是不能被显式调用。
4. 构造函数可以重载。
我们通过代码进行举例说明此时我们已经创建出了一个【Date】类 class Date
{
public:// 1.无参构造函数Date(){}// 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 d1; // 调用无参构造函数//d1.Init(2023, 3, 15);d1.Print();Date d2(2023, 3, 15);// 调用带参的构造函数//d2.Init(2023, 2, 14);d2.Print();Date d3();d3.Print(); return 0;
} 解析
此时当我们运行【Date.d1】和【Date.d2】时集合上面说到的我们可以发现运行结果如下传参就调用有参数的构造函数不传参就调用不传参的构造函数
而当我们此时运行【Date.d3】时我们会发现程序出现了报错的情况
此时就需要注意一点如果通过无参构造函数创建对象时对象后面不用跟括号否则就成了函数声明 声明了【d3】函数该函数无参返回一个日期类型的对象
到这大家思考一下当我们写出这两个代码时是否可以进行合并为一个代码呢 class Date
{
public:Date(){_year 1;_month 2;_day 3;}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;
};答案当然是可以的那么我们要怎么做呢这就需要用到之前学习的缺省参数的知识 Date(int year1 , int month2 , int day3 ){_year year;_month month;_day day;} 此时当我们运行它时如果开始我们不传参就用默认的如果传了就用我们传的。
这跟之前那个比较就显得很“高级”不仅如此这个功能相对比上面那种写法还更多因此这里支持缺省参数例如
因此这里就给大家说明一个点一个类从大部分场景来说当能提供构造函数的情况下尽可能提供全缺省或者至少是半缺省就会显得十分好用。
紧接着就是一点小细节的问题大家注意以下这两个函数可以同时存在吗
我们浅浅的分析一波 首先这里的两个函数构成我们之前讲过的重载吗不知道大家是否还知道重载的基本知识【函数名相同参数不同】大家从语法上看着可能觉得“确实像那么回事”但是真的可以吗我们直接运行代码
那么到底为什么呢大家可以试着想想当我们传参数的时候既可以调无参又可以调有参数的函数那么这样到底应该调用谁呢编译器就不知道该调那个了调用时存在歧义因此就报错了。5. 如果类中没有显式定义构造函数则C编译器会自动生成一个无参的默认构造函数一旦用户显式定义编译器将不再生成。 class Date
{
public:void Print(){cout _year 年 _month 月 _day 日 endl;}private:int _year;int _month;int _day;
};int main()
{Date d1;d1.Print();return 0;
} 在这时我们将构造函数删除掉了当我们去进编译时我们会发现可以编译通过。为什么呢?因为之前说过默认会生成一个构造函数。
那么此时大家是否会有这样的想法既然编译器自己就有默认的那么是不是我们就不需要在去构造了呢事实真的是这样的吗当我们的代码运行起来时大家可以看到下图 为什么这是随机值呢
这个问题大家可以认为是我们的祖师爷设计的不好的一个地方可能当时没有“想明白”。C把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类 型如int/char...自定义类型就是我们使用class/struct/union等自己定义的类型。对编译器自动生成的构造函数不会对内置类型进行处理然而对于自定义类型则是去调用该自定义类型对应的默认构造函数。因此上面代码类中的成员变量都是整形【int】是内置类型所以是随值。
接下来我们还是通过下图代码来进行相关的理解 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 d1;return 0;
} 解析
上述代码我们不难发现既有内置类型也有自定义类型的其中【Date】类中我们并没有对其写相应的构造函数此时当我们在创建一个对象的时候根据前面讲的内置类型不做处理这时自然而然就会去调用编译器自动生成的构造函数。而对于这里的自定义类型【Time _t】因为为自定义类型因此编译器会自动去调用它对应的默认构造函数。此时当我们故意在【Time 】类的默认构造函数里面增加打印看是否进行了调用。
我们运行程序结果如下 此时我们就会注意到自定义类型不写构造函数就没法初始化这不是一个妥妥的【bug】吗
因此祖师爷呢在后来也发现了这个问题并在C11中针对内置类型不初始化的缺陷打了一个补丁即内置类型成员变量在 类中声明时可以给默认值。
什么意思呢意思就是如果你不写构造函数那么就默认用这个缺省值如果你写构造函数了就不会用这个缺省值 。注意这里不是初始化千万要分辨清楚这里没开空间哟
6. 无参的构造函数和全缺省的构造函数都称为默认构造函数并且默认构造函数只能有一个。 注意无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数都可以认为 是默认构造函数。 3. 析构函数 通过前面构造函数的学习我们知道一个对象是怎么来的那一个对象又是怎么没呢的 3.1 概念介绍
这里我们就需要引入有关于虚构函数的知识
析构函数 与构造函数相反当对象结束其生命周期 如对象所在的函数已调用完毕时系统自动执行析构函数。 析构函数往往用来做“清理善后” 的工作例如在建立对象时用new开辟了一片内存空间delete会自动调用析构函数后释放 内存 。3.2 特性介绍
析构函数是特殊的成员函数其特征如下
1. 析构函数名是在类名前加上字符 ~。
2. 无参数无返回值类型。
3. 一个类只能有一个析构函数。若未显式定义系统会自动生成默认的析构函数。注意析构 函数不能重载
4. 对象生命周期结束时C编译系统系统自动调用析构函数。 typedef int DataType;
class Stack
{
public:Stack(size_t capacity 3){_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(){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);
} 我们通过以上代码来做分析
上述我们定义了一个栈类并且已经写好了构造函数。我的问题是这里的【s】需要我们亲自动手去进行清理工作吗很显然是不需要的因为【s】是定义在栈区上的局部变量一旦整个程序运行结束就会随着【main】函数的栈帧自动销毁。
5. 关于编译器自动生成的析构函数是否会完成一些事情呢
下面的程序我们会看到编译器生成的默认析构函数对自定类型成员调用它的析构函数。 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;
} 当我们运行程序之后我们会发现结果是输出了【~Time()】具体如下
此时问题就来了。在【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】类生成的默认析构函数注意创建哪个类的对象则调用该类的析构函数销毁那个类的对象则调用该类的析构函数
6. 如果类中没有申请资源时析构函数可以不写直接使用编译器生成的默认析构函数比如 Date类有资源申请时一定要写否则会造成资源泄漏比如Stack类。 4. 拷贝构造函数
4.1 概念介绍
在现实生活中可能存在一个与你一样的自己我们称其为双胞胎。
那在创建对象时可否创建一个与已存在对象一某一样的新对象呢因此这就引出了拷贝构造函数的概念
复制构造函数是构造函数的一种也称拷贝构造函数它只有一个参数参数类型是本类的引用。复制构造函数的参数可以是 const 引用也可以是非 const 引用。 一般使用前者这样既能以常量对象初始化后值不能改变的对象作为参数也能以非常量对象作为参数去初始化其他对象。一个类中写两个复制构造函数一个的参数是 const 引用另一个的参数是非 const 引用也是可以的。
此时我们在通过代码来进行直观的理解 class Date
{
public:Date(int year 2023, int month 3, int day 15){_year year;_month month;_day day;}void Print(){cout _year 年 _month 月 _day 日 endl;}private:int _year;int _month;int _day;
};int main()
{Date d1(2008,2,5);Date d2(d1);d2.Print();return 0;
} 解析
这里给出了初始化 【d2】的参数即 【d1】。只有编译器自动生成的那个默认复制构造函数的参数才能和【d1】匹配因此【d2】就是以 【d1】 为参数调用默认复制构造函数进行初始化的。初始化的结果是 【d2】 成为【d1】 的复制品即 【d2】 和 【d1】 每个成员变量的值都相等。4.2 特征介绍
拷贝构造函数也是特殊的成员函数其特征如下
1. 拷贝构造函数是构造函数的一个重载形式。
2. 拷贝构造函数的参数只有一个且必须是类类型对象的引用使用传值方式编译器直接报错 因为会引发无穷递归调用。
根据上述的基本知识在这里我们知道可以通过如下去写 Date(Date d){_year d._year;_month d._month;_day d._day;}但是这样呢就会发生报错的情况具体情况如下如果语法强制这里编译不会通过通过了这里它也会发生无穷递归的情况就是不能使用传值传参这里需要使用引用
要理解上述问题我们需要了解这里为什么不能使用传值传参 我们先来理解下面这几行代码的意思 // 传值传参
void Func1(Date d)
{}// 传引用传参
void Func2(Date d)
{}int main()
{Func1(d1);Func2(d1);return 0;
} 解析
这里的【func1】是传值传参是一个拷贝即理解为新开一片空间把【d1】拷贝给【d】传引用传参即【d】是【d1】的别名形参是实参的拷贝。内置类型编译器可以直接拷贝自定义类型的拷贝需要调用拷贝构造。 解析
对于【Date】这样的类编译器可以自己去进行拷贝而对于栈这样的类来说编译器则是不能自己擅自去进行拷贝的如果编译器这样的工作都能干的话程序员可能真的就要失业了。对于上述【Date】类里面只有简单的年月日等可以按照类似于【memcpy】的方式一字节一字节的进行拷贝俗称为浅拷贝。而对于栈这样的类来说假设【stl1】和【stl2】同时以字节去进行拷贝把【_a】拷贝到一块新空间这时就会出现问题。不难看出此时两个对象指向同一块空间当两个对象此时指向同一块时假设过一会儿就会调用析构函数当【stl1】先析构过了一会儿【stl2】又会继续析构同一块空间析构了两次这是不允许的同时也还会引起其他的问题。因此基于这样的原因自定义类型需要调用拷贝构造栈上的内容需要进行相应的深拷贝构造具体的我们后面会讲这里先给出这样的概念到这里我们在解释上述提到的为什么要是无穷递归
大家想想那是一个对象实例化对象实例化就需要用到构造函数对应的构造函数又是拷贝构造调拷贝构造之前需要先传参传值传参又是一个拷贝构造拷贝构造又需要传参这样的不断循环最终就是无穷递归因此编译器构造的不能是传值传参 因此这里可以怎么做呢答案是在这里我们可以使用引用的基本方法去解决这个问题具体如下 class Date
{
public:Date(int year 2023, int month 3, int day 16){_year year;_month month;_day day;}Date(const Date d) {_year d._year;_month d._month;_day d._day;}void Print(){cout _year 年 _month 月 _day 日 endl;}
private:int _year;int _month;int _day;
};int main()
{Date d1;Date d2(d1);d1.Print();d2.Print();return 0;
} 加引用之后要怎么理解呢
我们可以这样进行理解。这里的【d】加了引用因此我们可以看做【d】是【d1】的别名【d】就是【d1】指针【this】就是【d2】我们这里并没有有把【*this】写出来此时【d1】就传给了【d2】
同时还有一种写法也是拷贝构造编译器也可以允许像如下这样去写 int main()
{Date d1;Date d2(d1);Date d3 d1;d1.Print();d2.Print();d3.Print();return 0;
} 程序运行结果如下
注意 这里有个小细节的地方问问大家上面代码中可以发现我们加入了【const】大家知不知道为什么要加上这个【const】的 对于为什么要加入【const】我们还是以代码为例进行直观的了解当我们不小心写反的时候如果不加其中的【const】会出现什么情况呢具体如下 Date( Date d){d._year _year;d._month _month;d._day _day;} 当我们去编译这个程序时却不会出现报错的情况。但是当我们一运行这个程序结果就会出现报错的情况。
此时我们浅浅的分析一波
我们可以发现代码【d2】本来是拷贝【d1】结果【d2】非但没能拷贝【d1】还把【d1】改为了随机值。原因就是因为写反【d】是【d1】的别名【*this】的【d2】本来是【d1】赋值给【d2】但是现在你变成了【d2】赋值给【d1】的情况因此就出现上述运行出错。因此如果要确保实参的值不会改变又希望避免复制构造函数带来的开销解决办法就是将形参声明为对象的 const 引用加上【const】传递过来的不管是不是加了【const】都可以进行接收但是如果不加【const】就会引起权限放大的问题编译器是不允许这种情况出现的。出现任何有可能导致 【d】的值被修改的语句都会引发编译错误。 3. 若未显式定义编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按 字节序完成拷贝这种拷贝叫做浅拷贝或者值拷贝这上面已经说过。
注意在编译器生成的默认拷贝构造函数中内置类型是按照字节方式直接拷贝的而自定 义类型是调用其拷贝构造函数完成拷贝的。
4. 编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了还需要自己显式实现吗 当然像日期类这样的类是没必要的。那么下面的类呢验证一下试试
我们通过下列栈类的进行举例 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){// CheckCapacity();_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;
} 上述代码当我们去运行时 这里会发现下面的程序会崩溃掉。什么原因呢这里就需要我们理解深拷贝去解决。 在上面我们已经浅浅的谈到过这个问题会发生析构两次的问题那么到底谁先析构呢在这里我们仔细分析一下。
分析如下
首先我们给出答案这里是【st2】先析构我们知道【st1】和【st2】都是在栈上的建立的而之前我们学习数据结构的时候知道栈的特点是“先进后出”因此遵循这样的原则【st1】比【st2】先进入栈区中这就会导致【st2】先析构申请的这块空间就被释放了。但是紧接着当【st2】析构完了【st1】也会进行它的析构而此时虽然【st1】还保留了这块空间的地址但是这块空间刚才已经被释放所以这就会导致【st1】变成野指针而编译器对野指针进行释放就会导致我们看到的崩溃现象。
注意类中如果没有涉及资源申请时拷贝构造函数是否写都可以一旦涉及到资源申请 时则拷贝构造函数是一定要写的否则就是浅拷贝。 而对于深拷贝我们后面会具体去讲这里我们先浅浅的谈一下
对于上述的【Stack】进行浅拷贝就会导致两个栈对象指向了同一块空间了所以才会出现崩溃的情况那么深拷贝是怎么做的呢?其实深拷贝解决这个问题的原理就是让这两个对象各自拥有独立的空间。这样做对两个对象之间就不会互相影响了。 用代码浅浅的实现一下 Stack(const Stack st){cout Stack(const Stack st) endl;_array (DataType*)malloc(sizeof(DataType)*st._capacity);if (nullptr _array){perror(malloc fail);exit(-1);}memcpy(_array, st._array, sizeof(DataType)*st._size);_size st._size;_capacity st._capacity;} 此时我们会看到两个地址空间不同此时问题就解决了
5. 拷贝构造函数典型调用场景
使用已存在对象创建新对象函数参数类型为类类型对象函数返回值类型为类类型对象接下来我们一一进行分析复制构造函数在以下三种情况下会被调用 1) 当用一个对象去初始化同类的另一个对象时会引发复制构造函数被调用。例如下面的两条语句都会引发复制构造函数的调用用以初始化 【d2】。 Date d2(d1);Date d2 d1; 注意上面说过这两条语句是等价的。第二条语句是初始化语句不是赋值语句。赋值语句的等号左边是一个早已有定义的变量赋值语句不会引发复制构造函数的调用。例如 Date d1d2; d1 d2
d1 d2; 这条语句不会引发复制构造函数的调用因为 【d1】早已生成已经初始化过了。
2) 如果函数 【d1】的参数是类 【Date】的对象那么当 【d1】被调用时类 【Date】的复制构造函数将被调用。换句话说作为形参的对象是用复制构造函数初始化的而且调用复制构造函数时的参数就是调用函数时所给的实参。 class Date
{
public:Date(){};Date(const Date d1){cout Time() endl;}
};void Func(Date d1)
{ }int main()
{Date d1;Func(d1);return 0;
} 输出结果为
这是因为 Func 函数的形参 【d1】在初始化时调用了拷贝构造函数。
前面说过函数的形参的值等于函数调用时对应的实参现在可以知道这不一定是正确的。如果形参是一个对象那么形参的值是否等于实参取决于该对象所属的类的复制构造函数是如何实现的。例如上面的例子Func 函数的形参 【d1】 的值在进入函数时是随机的未必等于实参因为复制构造函数没有做复制的工作。 3) 如果函数的返冋值是类 【Date】的对象则函数返冋时类 【Date】的复制构造函数被调用。换言之作为函数返回值的对象是用复制构造函数初始化 的而调用复制构造函数时的实参就是 return 语句所返回的对象。例如下面的程序 class Date
{
public:Date(int year 2023, int month 3, int day 16){_year year;_month month;_day day;}Date(const Date d){_year d._year;_month d._month;_day d._day;cout Date(const Date d): this endl;}void Print(){cout _year 年 _month 月 _day 日 endl;}private:int _year;int _month;int _day;
};Date Test(Date d)
{Date temp(d);return temp;
}int main()
{Date d1(2008, 2, 15);Test(d1);d1.Print();return 0;
}程序的输出结果是
调用了 Test函数其返回值是一个对象该对象就是用复制构造函数初始化的 而且调用复制构造函数时实参就是return 语句所返回的 【temp】。复制构造函数在之前确实完成了复制的工作所以函数的返回值为赋值的。 所以为了提高程序效率一般对象传参时尽量使用引用类型返回时根据实际场景能用引用 尽量使用引用。 5. 赋值运算符重载
5.1概念引出 在讲解这个知识点之前我们先了解一个以及回顾一下之前的 初始化和赋值的区别
在定义的同时进行赋值叫做初始化Initialization定义完成以后再赋值不管在定义的时候有没有赋值就叫做赋值Assignment。初始化只能有一次赋值可以有多次。
当以拷贝的方式初始化一个对象时会调用拷贝构造函数当给一个对象赋值时会调用重载过的赋值运算符。 即使我们没有显式的重载赋值运算符编译器也会以默认地方式重载它。默认重载的赋值运算符功能很简单就是将原有对象的所有成员变量一一赋值给新对象这和默认拷贝构造函数的功能类似。 对于简单的类默认的赋值运算符一般就够用了我们也没有必要再显式地重载它。但是当类持有其它资源时例如动态分配的内存、打开的文件、指向其他数据的指针等默认的赋值运算符就不能处理了我们必须显式地重载它这样才能将原有对象的所有数据都赋值给新对象。
基于上述情况我们这里就引出了关于赋值运算符重载的概念。
5.2 运算符重载 在学习正式学习赋值运算符重载我们先来学习运算符重载的基本知识有了对这个理解当我们讲解赋值运算符重载大家才会轻松上手因为赋值运算符重载是属于运算符重载的。 为什么引入运算符重载
C为了增强代码的可读性引入了运算符重载运算符重载是具有特殊函数名的函数也具有其 返回值类型函数名字以及参数列表其返回值类型与参数列表与普通的函数类似
还是以我们写过的日期类来举例子在平常生活中我们是不是经常比较两个日期啊想着多少天是几号多少多少号跟今天相差几天这种情况。那么是否支持进行比较判别呢答案当然是支持:,此时我们用日期类来实例化出两个对象具体如下 int main()
{Date d1(2008, 2, 15);Date d2(2023, 3, 16);return 0;
} 当我们实例化出两个对象d1d2后此时大家就会思考一个问题现在我们想比较这两个对象是否相等该怎么办呢根据我们之前学过的知识当然是用函数来封装它呀写了功能函数就可以了。 bool Equal(const Date x1, const Date x2)
{//......
} 这时大家会有这样的想法这样的方法可行肯定是可行的但是有没有更加直观的呢就像我们下面这样去写 d1 d2; 为此当C引入了运算符重载之后再去判断就直接像上面的代码这样去操作。但是我们要知道一点那就是自定义类型是不能直接作为这些操作符的操作数的它不想内置类型一样可以直接进行操作。具体原因如下
所谓的自定义类型即为我们按照我们自己的想法或者为实现某个功能自己编写的程序对于这样的程序编译器是不知道该怎么做的。其次就是我们自己编写的自定义类型其实并不是所有的运算都是有意义的这个是由我们自己决定的因此基于以上两点不难得出结论。为了解决这个问题就引入了运算符重载的概念使得可以像【d1d2】这样去进行操作。 函数名字为关键字operator后面接需要重载的运算符符号。函数原型返回值类型 operator操作符(参数列表) 对比的逻辑思路也很简单只需比较各个成员变量是否相等。即如下代码 bool operator(const Date d1, const Date d2)
{return d1._year d2._year d1._month d2._month d1._day d2._day;
} 但是此时当我们编译时会出现报错的情况
咦....什么原因呢这是我们浅浅的分析一波
我们不难发现这是定义在类外面的然而类中的成员变量却是私有的基于这个原因当我们去编译时就出出现报错的情况。
那么如何解决呢在这里我给出几种解决方法
1.第一种就是我们刚才已经把问题 分析出来了我们就会想到既然你是私有的我无法访问那我把你直接变为公有的不就可以了吗因此第一种方法就是先全部都变为公有的即屏蔽我们的【private】 class Date
{
public:Date(int year 1900, int month 1, int day 1){_year year;_month month;_day day;}void Print(){cout _year 年 _month 月 _day 日 endl;} //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;
}int main()
{Date d1(2008, 2, 15);Date d2(2023, 3, 16);operator (d1, d2);d1 d2; // 转换成去调用这个operator(d1, d2);return 0;
} 此时在当我们去运行代码的时候就可以正常的运行了
当程序能够正常编译后我们就会想着去运行既然这个是函数那我们可以打印一下这个结果会有一个返回值但是当我们运行时又会出现报错 cout operator(d1, d2) endl;cout d1 d2 endl; 这又是什么原因呢答案很简单是因为【】的优先级比【】高因此为了限制这种情况我们需要加个括号 cout operator(d1, d2) endl;cout (d1 d2) endl; 此时当我们再次运行时结果显示就为正确
从上可以看出这种办法可以解决这个问题但是大家是否能够发现这样做存在的问题呢上述方式我们把全部都变为了公有那么问题来了封装性如何保证
2.因此基于以上方法存在的问题我们给出了第二种方法。我们可以把这个函数重载到类里面干脆重载成成员函数。
但是当我们放到类里面去后我们再次运行代码咦....怎么出错了呢你不是说放到类里面去可以吗别急我们先看报错报的是什么
它说我们的参数太多了什么意思呢
我们这里重载的是【】运算符正常情况下只有两个操作数所以只需要两个参数就够了。但是大家是否还记得我们默认的还有一个【*this】这个隐藏的参数呀因此这里只需给一个参数就可以了。bool operator(const Date d2){return _year d2._year _month d2._month _day d2._day;} 此时我们就需要这样去打印 cout d1.operator(d2) endl;cout (d1 d2) endl; 这样我们再去运行程序此时程序就正常运行的了。 注意
不能通过连接其他符号来创建新的操作符比如operator重载操作符必须有一个类类型参数用于内置类型的运算符其含义不能改变例如内置的整型不 能改变其含义作为类成员函数重载时其形参看起来比操作数数目少1因为成员函数的第一个参数为隐 藏的this【.*】 【 ::】 【 sizeof 】【 ?: 】【 .】 注意以上5个运算符不能重载。这个经常在笔试选择题中出 现。【.*】只需记住即可接下来我们在多写几个来进行练习。 1.第一个先写一个日期类的【】 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;}else{return false;}
} 或者直接这样写 return _year d._year|| (_year d._year _month d._month)|| (_year d._year _month d._month _day d._day); 以上这两种都是可以的那么到底是不是呢我们直接运行程序可以看到结果是正确的
如果我们还想要实现其他的话是不是看着麻烦呀这门一大堆的东西。其实根本没必要在像以上这样去写了上面我们已经写好了【】和【】,在写其他的直接复用这个就可以了。
2.例如写个【!】,我们可以这样去写 bool operator!(const Date d)
{return !(*this d);
} 3.对于【】 // d1 d2
bool operator(const Date d)
{return *this d || *this d;
} 4.对于【】 // d1 d2
bool operator(const Date d)
{return !(*this d);
} 5.对于【】 // d1 d2
bool operator(const Date d)
{return !(*this d);
} 当我们像这样做是不是就会很大程度上的减少我们的工作量呀 5.3 赋值运算符重载
接下来我们正式的介绍关于赋值运算符重载的知识。
1. 赋值运算符重载格式
参数类型const T传递引用可以提高传参效率返回值类型T返回引用可以提高返回的效率有返回值目的是为了支持连续赋值检测是否自己给自己赋值返回*this 要复合连续赋值的含义首先我们先来看看【】即赋值怎么操作的把。通过上述的知识学习我们不难写出这样的一个代码 //d1 d2;
void operator(const Date d)
{_year d._year;_month d._month;_day d._day;
} 当我们写出这样的代码时接下来我们就需要去验证这个代码的正确性紧接着我们直接运行程序运行结果如下
从上可以得出代码的运行结果是正确的我们在调试去看看是否真的这样。 上述我们也可以发现程序有去调对应的函数同时记住一点在转化的时候并不是让编译器把它给改了而是编译的时候编译器识别它看你有没有实现赋值有实现赋值就转化为去【call】这个函数 然后根绝我们之前的学习经历赋值往往会有连续赋值这一说法就像【ijk】这样不断的去连续赋值然而上述代码当我们去进行这样的操作的时候我们会发现是编译不通过的。
遇到困难不要害怕我们浅浅的分析一波
因此最终这里却是【d2 d3】调用了重载函数而我们上面实现的函数并没有返回值。因此这里就会出现报错的情况那么怎么解决呢很简单我们只需要在这里添加个返回值即可具体如下 // 返回值为了支持连续赋值保持运算符的特性
Date operator(const Date d)
{if (this ! d){_year d._year;_month d._month;_day d._day;}return *this;
} 最终的运行结果如下 2. 赋值运算符只能重载成类的成员函数不能重载成全局函数这个我们之前已经说过了不再具体讲解。
3. 用户没有显式实现时编译器会生成一个默认赋值运算符重载以值的方式逐字节拷贝。
注 意内置类型成员变量是直接赋值的而自定义类型成员变量需要调用对应类的赋值运算符 重载完成赋值。
既然如此我们是不是可以理解为上述的那个日期类的赋值就不需要我们自己去写相应的程序编译器自动生成的是不是就可以帮我们完成任务。那当我们屏蔽时会不会正常运行呢结果如下: 可以是可以但是这里的问题是不是就跟上面讲到的拷贝构造一样了一样的这里就不讲了。 接下来我们在理解以下代码是什么意思 Date d5 d1;// 拷贝构造Date d6(d1);// 拷贝构造 如果我没有给出答案大家会怎样理解上述代码呢你认为它是拷贝构造还是赋值重载呀那就有人会问这里不是有个【】赋值符号然而它却是拷贝构造呢道理其实很简答因为赋值重载是已经定义出来的对象。已经实例化好了然而这里的【d5】没有实例化出来只是用一个已经存在的实例化对象去初始化另外一个对象而已。6. const成员函数 在类中如果你不希望某些数据被修改可以使用const关键字加以限定。const 可以用来修饰成员变量和成员函数。 const成员变量
【const 】成员变量的用法和普通 【const 】变量的用法相似只需要在声明时加上 【const 】关键字。初始化 【const 】成员变量只有一种方法就是通过构造函数的初始化列表这点在前面已经讲到了。
const成员函数常成员函数
【const 】成员函数可以使用类中的所有成员变量但是不能修改它们的值这种措施主要还是为了保护数据而设置的。【const 】成员函数也称为常成员函数。 还是通过代码来进行直观的举例说明例如当我们运行下列代码时程序时可以正常运行的这个大家学到这了应该不陌生了 class Date
{
public:Date(int year, int month, int day){_year year;_month month;_day day;}void Print(){cout Print() endl;cout _year 年 _month 月 _day 日 endl;}private:int _year; // 年int _month; // 月int _day; // 日
};int main()
{Date d1(2023, 3, 17);d1.Print();return 0;
} 然而当我们这样写的时候呢即加入【const】时 const Date d2(2008, 1, 13);d2.Print(); 此时当我们再去编译的时候我们发现程序就会出现报错的情况
还是浅浅的分析一下
其实是因为这里存在了一个权限放大的问题即从可读 到 可读可写【d2】是被【const】修饰的说明对象本身不可被修改相当于【const Date*】的指针形式但类成员函数中的【this】指针又是【Date* const this】【const】修饰的是该地址指向的内容即对象【d2】不能被修改。因此当传给【this】【this】可以修改其指向的内容即对象【d2】因此权限放大了所以发生了报错。 权限放大跟之前讲到的一个情况类似的
问题分析如何解决呢可以看到如果不想让权限放大我们必须在【*】的前面加上【const 】由于【this】是隐形的所以编译器规定在函数括号后面加【const】来表示此对象不可被修改。即如下表示方法 void Print()const{cout Print() endl;cout _year 年 _month 月 _day 日 endl;} 在当我们去运行代码时就不会出现报错的情况了。 这里回答几个小问题 1. const对象可以调用非const成员函数吗
不允许【const】成员函数调用非【const】成员函数调用该【const】成员函数的对象已经被设置为【const】类型只可以访问但是不能进行修改在用该const成员函数访问其他非【const】成员函数可能会修改因此【const】成员函数不能调用非const成员函数。
2. 非const对象可以调用const成员函数吗
可以当一个类只有const成员函数的时候,非const对象也可以调用const成员函数
3. const成员函数内可以调用其它的非const成员函数吗
不可以若你把一个函数声明为const类型函数那么就说明这个函数是只读的不可修改而非const成员函数是可读可写的。
4. 非const成员函数内可以调用其它的const成员函数吗
可以外层函数类型【Date* const】:是可读可写的而内层函数类型是【const Date* const】:只读外层可以修改也可以不修改到底是否要修改视情况而定。最后再来区分一下 const 的位置
函数开头的 const 用来修饰函数的返回值表示返回值是 const 类型也就是不能被修改函数头部的结尾加上 const 表示常成员函数这种函数只能读取成员变量的值而不能修改成员变量的值。总结 到底要不要使用【const】去修饰成员函数就看你函数中的变量需不需被修改如果不希望被修改则加上即可。 7. 取地址及const取地址操作符重载 取地址成员函数也是类的六大默认成员函数之一。其分为两种普通取地址操作符、【const】取地址操作符。 这两个默认成员函数一般不用重新定义 编译器默认会生成用编译器默认生成的取地址的重载即可 class Date
{
public:Date* operator(){return this;}const Date* operator()const{return this;}private:int _year; int _month; int _day;
};int main()
{Date d1;cout d1 endl;return 0;
} 小结
这两个运算符一般不需要重载使用编译器生成的默认取地址的重载即可只有特殊情况才需 要重载比如想让别人获取到指定的内容总结
本期主要介绍的是【C】中默认六大成员函数大家对前四个一定要认真的学习后两个只需知道懂即可。最后如果本文对你有帮助的话记得点赞三连哟