上海松江网站建设公司,开创者wordpress素材,我图网类网站建设,电子技术培训机构文章目录1. 类的6个默认成员函数2. 构造函数2.1 构造函数的引出2.2 构造函数的特性3. 析构函数3.1 析构函数的引出3.2 析构函数的特性4. 拷贝构造函数4.1 概念4.2 特性5.赋值运算符重载5.1 运算符重载概念注意练习5.2 赋值重载实现赋值重载的特性6. const成员函数7. 取地址及co…
文章目录1. 类的6个默认成员函数2. 构造函数2.1 构造函数的引出2.2 构造函数的特性3. 析构函数3.1 析构函数的引出3.2 析构函数的特性4. 拷贝构造函数4.1 概念4.2 特性5.赋值运算符重载5.1 运算符重载概念注意练习5.2 赋值重载实现赋值重载的特性6. const成员函数7. 取地址及const取地址操作符重载这篇文章呢我们接着上一篇的内容继续C类和对象的学习。 1. 类的6个默认成员函数 通过上一篇文章的学习我们知道如果一个类中没有成员变量也没有成员函数啥也没有那我们把它叫做空类。 即如果一个类中什么成员都没有简称为空类。 比如
class Date
{};那现在问大家一个问题空类中真的什么都没有吗 其实并不是的。 如果一个类什么成员都没有那么编译器会为该类自动生成以下6个默认成员函数。 默认成员函数即用户没有显式实现编译器自动生成的成员函数称。 那这6个默认成员函数都是什么呢 大家先简单了解一下接下来我们会一一学习。 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;
};那对于一个类来说我们实例化出来对象之后一般会对其进行一个初始化 如果有时候不初始化直接用可能就会出现问题但是有时候我们可能会忘记初始化直接就对对象进行一些操作了。 再举个大家可能经历过的例子 比如我们写了一个栈的类然后用该类创建一个对象对象创建好之后我们就迫不及待地往栈里放数据了上去直接调用压栈的成员函数哐哐哐数据就搞进去了。 但是一运行发现程序崩溃了最后吭哧吭哧去调试发现没有对创建出来的栈进行初始化空间都没开呢就放数据了。 有可能忘了不说每次创建一个对象都要初始一次好像也有点麻烦。 那针对上面提到的这种情况呢C呢就提供了一种方法帮助我们解决这个问题 那就是我们接下来要学的——构造函数。 有了构造函数我们每创建完一个对象就不用手动去调用Init函数进行初始化了因为在创建对象时编译器会自动去调用构造函数对对象进行初始化。 那构造函数到底是个啥呢 构造函数是一个特殊的成员函数名字与类名相同,创建类类型对象时由编译器自动调用以保证每个数据成员都有 一个合适的初始值并且在对象整个生命周期内只调用一次。 那接下来我们就来详细地认识一下构造函数。
2.2 构造函数的特性 构造函数是特殊的成员函数需要注意的是构造函数虽然名称叫构造但是构造函数的主要任务并不是开空间创建对象而是初始化对象。 其特征如下
构造函数的函数名与类名相同 也就是说定义好一个类它的构造函数的函数名就确定好了跟当前类的类名是相同的。 构造函数无返回值 要注意这里说的无返回值不是说返回类型是void而是根本就不写返回类型。 比如上面我们定义的那个Date类如果要写它的构造函数就应该是这样的 对象实例化时编译器自动调用对应的构造函数 有了构造函数我们初始化对象就不用再手动初始化了实例化一个对象时编译器会自动调用其对应的构造函数。 构造函数可以重载 构造函数可以重载那是不是就意味着一个类可以有多个构造函数那也就是说该类创建的对象可以有多种初始化方式。 那不能光说不练啊现在已经认识了构造函数了那我们练习一下呗就给上面的Date类写一下构造函数
Date()
{}首先看这是不是就是一个构造函数啊当然是没有返回值并且函数名和类名相同嘛。 但是我们说构造函数是用来初始化对象的那啥也不写是不是没意思啊写点东西吧 Date(){_year 1;_month 1;_day 1;}这样我们把年月日都初始化成1。 那我们来试一下刚才不初始化都打印出来随机值了那现在有构造函数不是说会自动初始化吗行不行啊验证一下 哦豁可以啊这次我们并没有调用初始化函数但是打印出来不是随机值而是我们在构造函数中给定的初值说明我们实例化对象的时候确实自动调用构造函数进行初始化了。 那这样的话我们每次创建Date类的对象初值都是1 1 1如果我们想每次都按照自己的想法给对象进行初始化呢能做到吗 是不是可以啊。 上面提到的构造函数的第4条特性是啥 是不是构造函数可以重载啊那我们重载一下给参数不就行了。 这样的话我们不知道初始化给什么初值的时候就可以调用无参的构造函数自己想指定初值的话调用有参数的传参不就行了。 Date(int year, int month, int day){_year year;_month month;_day day;}这样是不是就搞定了。 这是不是就达到我们想要的效果了。 但是要注意调用无参构造函数的时候我们不要写成这样Date d1();即后面不要加括号。 这样的话编译器会报一个警告大家看这样写的话是不是可能会被认为是一个函数声明啊是吧。 一个返回类型为Date函数名为d1无参的函数声明是不是也长这样啊。 那大家再来思考一下 这两个构造函数有没有必要分开写或者说能不能一个函数就搞定了。 当然是可以的怎么做呢 上一篇文章刚学的——缺省参数 是不是可以这样写
Date(int year 1, int month 1, int day 1){_year year;_month month;_day day;}用这一个是不是就行了啊 我们不传参就用默认的传了就用我们传的。 另外还有一个需要注意的点 我们现在呢实现了一个带缺省值的构造函数那大家思考一下这两个构造函数可以同时存在吗 那要告诉大家的是首先在语法上它们两个是可以存在的因为它们构成重载嘛但是我们现在再去运行程序 报错了为什么 原因在于我们这里是不是调用了无参的构造函数啊d1我们创建时没传参嘛但是上面这两个构造函数是不是都适用于无参的情况啊所以编译器就不知道该调那个了就报错了。 那我们把d1的创建注释掉呢 就不报错了好吧这是需要大家注意的一个地方。 那除了上面这些其实构造函数还有一些其它的特性
如果类中没有显式定义构造函数则C编译器会自动生成一个无参的默认构造函数一旦用户显式定义编译器将不再生成。 哦豁那也就是说构造函数不一定非要我自己写如果我们自己没有定义构造函数编译器会自动生成一个。只不过是无参的嘛。 那现在把我们自己定义的构造函数全部注释掉 我们发现确实没问题编译通过了。 将Date类中构造函数屏蔽后代码可以通过编译因为编译器生成了一个无参的默认构造函数 那特性还说了如果我们自己定义的有编译器就不再生成了 这个大家好理解我们上面自己写的无参构造函数把_year、 _month、 _day全部初始化为1打印出来确实是全1。 那编译器会自动生成的话我们以后是不是就不用自己写构造函数了 我们把自己写的构造函数屏蔽掉然后直接运行 欸这~怎么回事嘛 不是说有自动生成的构造函数嘛怎么还是随机值啊。 这编译器自动生成的默认构造函数怎么没用啊 什么原因呢 这个地方呢大家可以认为是我们的祖师爷设计的不好的一个地方或者说是一个失误。 具体是这样的 C把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型如int/char...包括各种指针类型自定义类型就是我们使用class/struct/union等自己定义的类型 而编译器自动生成的构造函数不会对内置类型进行处理对于自定义类型会处理怎么处理会去调用该自定义类型对应的默认构造函数 所以刚才为什么打印出来是随机值 因为我们Date类中的成员变量都是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 d;return 0;
}大家看这里的Date类与上面那个有什么区别是不是它的成员变量里既有内置类型又有自定义类型啊。 但是我们现在并没有给Date类写构造函数那我们在main函数里直接拿Date去创建一个对象它自然就会去调用编译器自动生成的构造函数那内置类型不做处理我们不是还有一个自定义类型Time _t;呢我们说对于自定义类型编译器会自动去调用它对应的默认构造函数。 那我们在Time 类的默认构造函数里面故意加了一个打印 如果运行会打印就说名编译器自动调用了 是不是调了啊。 那说到底内置类型呢这样的话内置类型不写构造函数就没法初始化了吗 我们的祖师爷呢在后来也发现了这个问题并在C11中针对内置类型不初始化的缺陷打了一个补丁。 即内置类型成员变量在类中声明的时候可以给缺省值。 这样如果我们不写构造函数内置类型的初始化就会按给定的缺省值进行初始化。 无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数都可以认为是默认构造函数默认构造函数只能有一个 所以这里想告诉大家的是不要认为默认构造函数就是我们不写编译器自动生成的那个除了这个之外我们自己定义的无参的构造函数或者全缺省的构造函数都可以认为是默认构造函数。 为什么说只能有一个呢因为我们调用这些构造函数是不是都不用传参啊那这样如果同时存在多个的话编译器就不知道到底该调哪个了。 这个问题我们上面也有提到过的。 3. 析构函数
3.1 析构函数的引出
首先我们来回顾一个问题 我们在之前数据结构的学习中在学到栈的时候有一个与栈相关的非常经典的题目——括号匹配问题。 链接: link 不知道大家做过这个题没有只不过当时我们用的栈是用C语言写的那现在我们也可以用C的类实现了。 但是这道题里有一个比较恶心的点是什么呢 来看一下我们C语言写出来的代码我们进行判断之后需要return的地方可能有好几处但是呢每次return之前其实最好都要去调用一下StackDestroy把我们动态开辟的空间给销毁一下但是我们可能很容易会忘掉导致内存泄漏。 那现在我们学了C有没有什么好的办法可以帮助我们解决这个问题呢 可不可以像上面的构造函数自动初始化一样自动对对象中的资源进行清理呢 那当然是有的就是我们接下来要学习的析构函数 析构函数 其与构造函数功能相反析构函数不是完成对对象本身的销毁局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数完成对象中资源的清理工作。 解释一下我们用这样一个类来举例
typedef int DataType;
class Stack
{
public://构造函数Stack(size_t capacity 4){_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;}// 其他方法...
private:DataType* _array;int _capacity;int _size;
};是一个栈类并且我们已经写好了构造函数。 那我们上面说 析构函数不是完成对对象本身的销毁而是完成对象中资源的清理工作 是什么意思呢 我们那这个类去实例化栈对象 int main()
{Stack s;s.Push(1);s.Push(2);return 0;
}大家思考一下这里的对象s需要我们自己去销毁吗 是不是不需要啊因为s是定义在栈区上的局部变量程序结束它是不是就随着main函数的栈帧自动销毁了啊。 那析构函数的作用是啥呢 完成对象中资源的清理工作什么意思 像栈这样的对象它里面是不是有在堆上动态开辟的空间啊那经过C语言的学习我们都知道这些空间是不是需要我们手动去释放的啊否则可能会导致内存泄漏。 所以说析构函数就是来帮我们干这件事情的。 那析构函数到底是个啥又怎么用呢
3.2 析构函数的特性 和构造函数一样析构函数也是一个特殊的成员函数其特征如下 析构函数名是在类名前加上字符 ~ 也就是说一个类定义好之后它的析构函数的函数名也是确定的即在类名前面加上“~”。 ~是啥在C语言中是不是按位取反啊表示它的功能和构造函数是相反的。 无返回值且无参数 和构造函数一样析构函数也是没有返回值的并且析构函数还没有参数。 对象生命周期结束时C编译系统系统自动调用析构函数 析构函数起作用的关键就在这里对象声明周期结束时编译器会自动调用析构函数对对象的资源进行清理。 析构函数不能重载 注意析构函数不能重载因为它连参数都没有何谈重载。 那了解到这里我们就可以尝试写一个析构函数来练练手了 就给我们刚才那个栈类写一个析构函数吧。 ~Stack(){free(_array);_array NULL;_capacity 0;_size 0;}那是不是很简单啊即使释放我们在堆上开辟的空间嘛。 然后呢它没有返回值没有参数 那就写好了那测试一下呗 为了方便看出来是否自动调用了析构函数我们可以在加一个打印 此时我们的main函数里并没有显式的调用~Stack函数 然后我们运行 是不是自动调用了啊。 一个类只能有一个析构函数。若未显式定义编译器会自动生成默认的析构函数 这一点呢和构造函数一样如果我们自己不写析构函数则编译器会自动生成默认的析构函数。 然后说一个类只能有一个析构函数我们上面说了析构函数不能重载所以肯定只能有一个了。 那编译器默认生成的析构函数有什么特点呢 和编译器默认生成的构造函数一样内置类型成员不处理当然如果全是内置类型的成员变量也不需要处理比如上面写的Date类。 那同样对于自定义类型会自动调用其对应的析构函数。 举个栗子
typedef int DataType;
class Stack
{
public:Stack(int capacity 4){_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;free(_array);_array NULL;_capacity 0;_size 0;}
private:DataType* _array;int _capacity;int _size;
};
class Date
{
private:// 基本类型(内置类型)int _year;int _month;int _day;// 自定义类型Stack _s;
};
int main()
{Date d;return 0;
}这里我们没有给Date显式定义析构函数那d声明周期结束时就会调用编译器自己生成的默认析构函数那里面的内置类型不做处理当然也不用处理关键在于自定义类型Stack _s;申请的资源需要清理那我们看编译器自己生成的默认析构函数会不会调用Stack 类的析构函数 是不是调了啊。 如果类中没有申请资源时析构函数可以不写直接使用编译器生成的默认析构函数比如Date类有资源申请时一定要写否则会造成资源泄漏比如Stack类
4. 拷贝构造函数
4.1 概念
我们再来看上面写的这个Date类
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;
};那现在我们用这个类创建一个对象d1
int main()
{Date d1;return 0;
}然后大家思考一下如果我们现在想在创建一个对象让这个对象和d1一样或者说是d1的一份拷贝应该怎么搞 那经过了上面的学习我们现在创建一个对象一般都直接用构造函数对其进行初始化想初始化什么值传参就行了。 那现在我们想创建一个和d1一样的新对象是不是可以用d1去初始化创建出来的新对象啊。 怎么做是不是把构造函数的参数类型设置成类对象的类型就行了。 那这其实就是我们接下来要学的拷贝复制构造函数。 拷贝构造函数 只有单个形参的构造函数该形参是对本类 类型对象的引用(一般常用const修饰)在我们用已存在类的类型对象创建新对象对象的拷贝时由编译器自动调用。 接下来我们来更加详细的认识一下它
4.2 特性
拷贝构造函数也是一种特殊的成员函数其特征如下
拷贝构造函数是构造函数的一个重载形式 因为我们刚才上面说了嘛它的作用其实也是用来初始化对象的只不过参数类型指定了是我们当前类的类型嘛。 所以它算是构造函数的一种重载形式。 那我们先自己来尝试实现一下它好吧
Date(Date d)
{_year d._year;_month d._month;_day d._day;
}这样是不是就行了啊。 然后要创建一个和d1一样的对象是不是这样 int main()
{Date d1;Date d2(d1);return 0;
}直接把d1作为参数初始化d2然后我们构造函数的参数类型正好是Date 嘛可以接收然后把d1的成员变量一个一个赋给d2不就搞定了嘛。 但是呢我们发现 这样写编译器直接就报错了还没运行就报错了。 那相信大家刚才也注意到上面的概念了在拷贝构造函数的概念中其实就指明了说它的参数类型应该是类对象的引用。 确实我们这样修改之后就可以了。 那这里为啥非得是引用呢我们来看拷贝构造函数的第2条特性
拷贝构造函数的参数只有一个且必须是类类型对象的引用使用传值方式编译器直接报错因为会引发无穷递归调用 那为啥直接传值就会引发无穷递归呢 结合这张图给大家解释一下 ps图中还在形参前加了const大家可以先不管后面会解释。 大家想一下首先我们这里是用已有的类对象去创建一个相同的新对象类对象的拷贝所以会调用拷贝构造函数那要调用函数是不是要先传参啊而传值调用传的是啥形参是实参的一份临时拷贝是不是传的实参的拷贝那要拷贝实参是不是又是一个类对象的拷贝啊那既然是类对象的拷贝就又要调用拷贝构造函数那就又需要传参一传参就会再次调用拷贝构造函数那这样是不是就陷入一个死递归了。 所以这里不能直接传对象而是要传对象的引用别名 我们传对象的引用还需要拷贝实参吗是不是就不用了所以也就不会出现上面的问题了。 这时我们再运行程序 不就达到我们想要的效果了吗。 另外呢对一个对象进行拷贝构造也可以这样写 直接用“”也可以这样也是拷贝构造。 除此之外大家是不是还注意到 上面一开始拷贝构造函数的概念中说它的形参一般用const修饰 为什么要加个const呢 其实很容易理解大家想形参d是用来干嘛的是用来初始化我们新创建的对象的那我们肯定不希望形参d被修改所以加个const修饰 这样我们如果不小心写反了啥的是不是就直接报错了。 所以正确的实现应该是这样的
Date(const Date d){_year d._year;_month d._month;_day d._day;}另外加const还有什么好处呢 大家想如果我们不加const但传过来的参数是const修饰的这样的话是不是根本就接收不了啊这个问题我们之前也讲了是不是属于权限放大了是不行的。 但是如果我们加了const传过来的不管是否加了const是不是都可以接收啊。 所以呢 这里一般加上const会比较好。 若未显式定义编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数 拷贝对象 按内存存储字节序完成拷贝这种拷贝叫做浅拷贝或者值拷贝。 那我们上面说了拷贝构造函数是构造函数的一种重载形式那其实就也属于是构造函数了那构造函数我们不写的话编译器不是会自动生成嘛那拷贝构造函数是不是也具有这样的特性呢 是的对于拷贝构造函数来说若未显式定义编译器也会生成默认的拷贝构造函数。 那默认生成的拷贝构造函数是什么样的我们来研究一下 我们刚才不是对Date类实现了一个拷贝构造函数嘛先我们现在把它屏蔽调 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;}*/void Print(){cout _year - _month - _day endl;}
private:int _year;int _month;int _day;
};那这次我们自己没写拷贝构造我们看看编译器自动生成的能不能帮助我们完成拷贝构造
int main()
{Date d1;d1.Print();Date d2(d1);d2.Print();Date d3 d1;d3.Print();return 0;
}我们运行程序 欸是不是可以啊。 那既然编译器自动生成的拷贝构造函数就可以帮助我们完成类对象的拷贝了那我们还需要自己写吗 那为了解决这个问题我们再来看这样一个类
typedef int DataType;
class Stack
{
public:Stack(int capacity 4){_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;free(_array);_array NULL;_capacity 0;_size 0;}
private:DataType* _array;int _capacity;int _size;
};还是我们之前用过的这个栈Stack类大家看它的成员变量是不是也都是内置类型啊前面提到过指针也属于内置类型嘛。 那对于Stack这个类我们也是没写拷贝构造函数的那编译器自动生成的能不能完成下面这样的拷贝呢
int main()
{Stack s1;s1.Push(1);s1.Push(2);s1.Push(3);Stack s2(s1);return 0;
}这里是把s1拷贝给s2我们运行一下 但是呢嗯 一运行发现我们的程序挂掉了。 为什么会这样呢刚才Date类不也都是内置类型为啥就没事呢 大家有没有注意到我们上面的特性3后面的一句话是 默认的拷贝构造函数 拷贝对象 按内存存储字节序完成拷贝这种拷贝叫做浅拷贝或者值拷贝。 在这里其实就是对逐个成员变量依次进行拷贝里面存的是啥就把啥拷过去。 那原因其实就出现在这里我们来对比一下Date和Stack这两个类进行的拷贝 首先对于Date类来说进行这样的浅拷贝有没有问题啊。 是不是没问题啊一共12个字节的内容直接拷贝过去就行了嘛。 但是对于Stack类来说呢 我们还是这样进行浅拷贝的话 大家看出来有什么问题了吗 这样拷贝过后两个栈对象是不是指向同一块堆上的空间啊。 这样会有什么问题呢 我们在st1中入栈几个数据st2里面是不是就也有数据了因为它俩用的是同一块空间然后如果我们再用st2去入栈数据此时st1的_size前面是不是已经过了但是st1的_size前面是不是还是0这样st2入的数据是不是就把之前st1入的数据给覆盖掉了。 除此之外还会有什么问题。 st1声明周期结束析构一次st2声明周期结束析构一次是不是会对一块空间析构两次啊。 那大家先思考一下这里st1和st2谁先进行析构啊 简单解释一下 这里是st2先析构我们知道st1和st2都是在栈上的栈区 那栈区之所以叫栈区也是有些讲究的它在这个地方也是遵循先进后出的这个顺序的即后定义的会先进行析构。 所以这里会有什么问题呢 st2先析构那堆上的这块空间就被释放了但是接下来st1也会进行它的析构而此时虽然st1还保留了这块空间的地址但是这块空间已经被释放所以st1就是个野指针了。 所以为什么程序崩溃了就是我们这里对野指针进行free了。 所以 在编译器生成的默认拷贝构造函数中内置类型是按照浅拷贝进行拷贝的浅拷贝在某些场景下是适用的比如上面的Date类但是在有些场景下是会出问题的比如这里的Stack类。 那总结一下就是 类中如果没有涉及资源申请时拷贝构造函数我们自己写不写都可以因为默认生成的就可以搞定一旦涉及到资源申请时则拷贝构造函数是一定要写的否则就是浅拷贝就会出现问题。 所以说 对于Stack这样存在资源申请的类我们是需要自己去写拷贝构造函数的那浅拷贝不行这里我们应该怎么实现呢 那要完成这种类的拷贝就需要我们实现一个深拷贝。 那深拷贝呢我们后面会专门去讲这里我们先来简单的试一下 那刚才Stack进行浅拷贝为什么不行是不是导致两个栈对象指向了同一块空间了。 所以我们深拷贝要做的就是让这两个对象各自拥有自己独立的空间就行了。 这样对两个对象进行操作就不会互相影响了。 那我们来实现一下代码吧
Stack(const Stack st){_array (DataType*)malloc(sizeof(DataType) * st._capacity);if (NULL _array){perror(malloc申请空间失败!!!);exit(-1);}memcpy(_array, st._array, sizeof(DataType) * st._size);_capacity st._capacity;_size st._size;}我们来运行一下 这次就正常运行了。 再来调试观察一下 是不是没问题啊。 当然 如果类的成员变量有自定义类型默认生成的拷贝构造还是会去调用该类对应的拷贝构造。 我们再来看这个类
class MyQueue
{
public:// 默认生成构造// 默认生成析构// 默认生成拷贝构造private:Stack _pushST;Stack _popST;int _size 0;
};大家看对于这个类来说我们还需要自己写构造函数、析构函数包括拷贝构造函数嘛 是不是不需要啊默认的是不是都能搞定啊。 对于构造函数来说内置类型虽然不做处理但是我们给了缺省值对于自定义类型默认生成的会自动调用它对应的构造函数啊而Stack 的构造函数我们也实现的有了 对于析构函数内置类型不用处理自定义类型这里也会自动调用Stack 对应的析构 那如果用到拷贝构造的话这里的_size 直接默认的浅拷贝就能搞定自定义类型还是会自动调Stack 对应的拷贝构造。 那总结一下这一部分就是 在编译器生成的默认拷贝构造函数中内置类型是按照浅拷贝值拷贝进行拷贝的而自定义类型是调用其对应的拷贝构造函数完成拷贝的。 拷贝构造函数典型调用场景 使用已存在对象创建新对象 函数参数类型为类对象 函数返回值类型为类对象 当然 为了提高程序效率一般对象传参时尽量使用引用类型减少拷贝返回时根据实际场景能用引用尽量使用引用。 5.赋值运算符重载
接下来我们要来学习赋值运算符重载那赋值运算符重载呢是属于运算符重载的所以在学习之前我们要先来了解一下C的运算符重载。
5.1 运算符重载
我们还来看上面实现过的那个日期Date类
class Date
{
public://构造函数Date(int year 1, 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;
};那我们现在用Date类实例化出两个对象
int main()
{Date d1(2023, 4, 13);Date d2(2023, 4, 12);return 0;
}现在有两个对象d1d2大家思考一个问题现在我们想比较这两个对象是否相等要怎么搞 那我们是不是可以考虑实现一个函数来判断两个对象是否相等 bool Equal(Date x1, Date x2)
{//...
}大家看该函数的参数这样写好不好是不是不太好啊。 这里是传值传参形参是实参的拷贝那对象的拷贝还要调用拷贝构造。 所以这里我们是不是可以考虑传引用啊这样就不用拷贝了另外呢这里只是去比较两个对象我们并不想改变它们所以是不是再加一个const比较好 bool Equal(const Date x1, const Date x2)
{//...
}写一个函数这是一种方法。 那C引入了运算符重载之后呢就使得我们能够这样去玩 比较两个日期类对象d1d2是否相等直接这样 d1d2 但是我们首先要知道自定义类型是不能直接作为这些操作符的操作数的。 不像我们的内置类型可以直接进行加减乘除比较相等这些运算为什么自定义类型不可以啊 因为自定义自定义是不是我们自己写的啊就比如我们实现的这个日期类是我们按照自己的想法实现出来的编译器肯定不知道比较这样两个对象应该怎么做。 而且有些自定义类型不是进行所有的运算都有意义的就比如日期类两个日期对象如果相加有意义吗是不是没啥意义啊如果两个日期相减还有点意义可以理解为两个日期之间差了多少天。 所以这个是由我们自己决定的我们觉得它可以进行什么样的运算有意义然后去实现。 那我们要怎么做才能让我们的自定义类型像这样d1d2直接进行一些运算和比较呢 这就需要我们对这些运算符进行重载。 概念 C为了增强代码的可读性引入了运算符重载运算符重载是具有特殊函数名的函数也具有其返回值类型函数名字以及参数列表其返回值类型与参数列表与普通的函数类似。 函数名字为关键字operator后面接需要重载的运算符符号 函数原型返回值类型 operator操作符(参数列表) 那我们接下来就来练习一下 上面我们不是相比较两个日期类对象是否相等嘛那我们就来重载一下运算符。 根据上面的概念我们可以写出 bool operator(const Date x1, const Date x2)
{}那函数体的实现即比较的逻辑其实也很简单 只要两个对象的三个属性成员变量_year_month_day全部相同就说明两个对象相等。 bool operator(const Date d1, const Date d2)
{return d1._year d2._year d1._month d2._month d1.day d2.day;
}这样是不是就行了但是现在有一个问题 什么原因呢 因为我们Date类的这3个成员变量是私有的private所以在类外面是不能访问的。 那怎么解决 我们可以在类里写一个Get方法函数通过Get方法来访问或者呢直接把private访问限定符去掉。 我们这里先把private注释一下 然后就不报错了。 那重载好我们就可以直接用了 当然我们也可以像普通函数那样去调用 当然正常情况下我们不会像普通函数那样去调用因为我们重载就是为了可以直接d1 d2这样用。 所以我们直接写成这样就行 d1 d2 剩下的工作就由编译器去做编译器看到这样的代码就会去看你有没有重载如果进行了重载就会转化成去调用这个函数operator(d1, d2)。 那我们可以打印一下这个结果
cout d1 d2 endl;
cout operator(d1, d2) endl;但是我们会发现又报错球了 cout d1 d2 endl;这一句报错了。 什么原因呢 是因为这里的优先级比高所以加个括号就行了 int main()
{Date d1(2023, 4, 13);Date d2(2023, 4, 12);cout (d1 d2) endl;cout operator(d1, d2) endl;return 0;
}0为假而这两个对象也确实是不相等的。 那我们就把重载好了但是 刚才我们是直接重载到了全局我们把成员变量变成了共有的才能这样的。 那么问题又来了我们把成员变量全部公有了封装性又如何体现呢 那当然是有办法解决的我们刚才上面已经提了一种就是提供一些共有的get方法那除此之外呢我们还可以用友元函数解决但是我们还没学而且不推荐用这个。 所以这里比较好的一种方法是 我们直接重载到类里面即重载成成员函数。 但是呢我们直接把它放到类里面的话 嗯又报错了说此运算符函数的参数太多。 怎么回事啊 这里我们重载的是运算符正常情况下只有两个操作数所以只需要两个参数就够了。 那大家可能会疑惑了这里不就是两个参数嘛 那大家不要忘了这里是不是还有一个隐藏参数啊。 什么隐藏参数是不是就是this指针啊。 这是不是我们上一篇文章学习的知识啊。 C编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数让该指针指向当前对象(函数运行时调用该函数的对象) 所以我们这里只需给一个参数就够了。 bool operator(const Date d){return _year d._year _month d._month _day d._day;}那调用的时候this指针接收d1的地址形参d就是d2引用传参。 注意
下面我们一起来看一下在运算符重载这一块需要注意的一些内容 不能连接其他符号来创建新的操作符比如operator重载操作符至少有一个类类型的参数用于内置类型的运算符其含义不能重载改变例如内置的整型不能改变其含义作为类成员函数重载时其形参看起来比操作数数目少1个因为成员函数的第一个参数为隐藏的this.* :: sizeof ?: .注意这5个运算符不能重载这个经常在笔试选择题中出现。 .*其中这个运算符大家可能都没见过也没用过没关系大家可以记一下就行了。 练习
那上面我们对运算符进行了重载接下来我们再来练习几个。
那就还是上面那个日期类现在我们来尝试重载一下好吧 那其实逻辑也不难就是判断两个日期的大小嘛。 我们可以只判断小于的情况返回true其它情况一律false 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;}就是这样嘛。 测试一下 没毛病。 那我们再重载一下日期类的吧 那是不是很简单啊那里面的换成就行了嘛 这样当然是可以的。 但是呢我们可能还会实现大于等于小于等于… 所以呢接下来给大家说一个简单的方法对所有的类都适用 怎么做呢 我们现在是不是已经重载了和了那现在我在想去重载什么大于大于小于之类的其实根本没必要再自己写了可以直接复用我们写好的那两个。 那我们现在想重载的话其实可以考虑先重载 那怎么复用 bool operator(const Date d){return *this d || *this d;}是不是就搞定了我们的小于等于。 那呢
bool operator(const Date d)
{return !(*this d);
}再来呢
bool operator(const Date d)
{return !(*this d);
}不等于呢
bool operator!(const Date d)
{return !(*this d);
}这样搞是不是很爽啊。 5.2 赋值重载 赋值运算符重载呢 是属于运算符重载的一种但是它还是我们类的6个默认成员函数的其中一个。 实现
那我们就先来重载一下赋值运算符吧 那经过了刚才的学习重载一个是不是简简单单啊。 //d1d2(this就是d1d就是d2)void operator(const Date d){_year d._year;_month d._month;_day d._day;}这是不是就好了啊测试一下 可以完成赋值。 但是呢我们当前的这个实现还有一些缺陷 什么缺陷呢 大家回忆一下我们之前用内置类型进行赋值操作时是不是支持像这样的连续赋值啊 i j k 这句代码怎么执行的是不是从右向左啊先把k赋给j然后再把表达式 j k的结果就是k赋给i。 当然还可以连续的更多。 而对于我们刚才对日期类重载的可以支持连续赋值吗 额是不行的这里直接报错了。 那这里为啥报错了啊 因为正常情况下d2赋给d1是不是应该有一个结果啊然后把这个结果再赋给d3。 但是我们这里d1 d2是不是调了我们重载的函数而我们上面实现的函数并没有返回值。 所以我们要加一个返回值来支持连续赋值 那我们返回的话是不是还是返回对象的引用比较好啊 //d1d2(this就是d1d就是d2)Date operator(const Date d){_year d._year;_month d._month;_day d._day;return *this;}那这下我们的连续赋值就可以了。 但是有时候呢不排除有人可能会写出这样的代码 把自己赋给自己。 这样可以吗 可以当然是可以的但是它调用函数是不是白白进行了一次拷贝啊所以呢我们一般还会加一点东西 Date operator(const Date d){if (this ! d){_year d._year;_month d._month;_day d._day;}return *this;}加一个判断如果它们是同一个对象就不用进行拷贝了。 那我们来简单总结一下赋值运算符重载 参数类型const 类对象的引用传递引用可以提高传参效率 返回值类型类类型返回引用可以提高返回的效率有返回值目的是为了支持连续赋值 最好检测一下是否是自己给自己赋值并进行一下处理 返回*this返回的结果用于支持连续赋值 那我们说了赋值运算符重载是属于6个类默认成员函数的其中一个所以它还有一些属于自己的特性。
赋值重载的特性
用户没有显式实现时编译器会生成一个默认赋值运算符重载以值的方式逐字节拷贝浅拷贝。 注意默认生成的赋值重载对于内置类型成员变量是直接赋值的而自定义类型成员变量需要调用其对应类的赋值运算符重载完成赋值。 那有了这个特性的话对于我们上面的日期类我们还需要自己写赋值重载吗 是不是不用啊用编译器自动生成的是不是就可以完成啊。 因为日期类的成员变量是不是都是内置类型啊而且赋值不涉及深拷贝的问题浅拷贝就可以完成。 那我们试一下把我们自己写的赋值重载注释掉 然后运行 是不是可以啊。 那这里的问题是不是就和拷贝构造一样了 编译器生成的默认赋值运算符重载函数已经可以完成浅拷贝赋值了所以像日期类这样的我们就没必要自己实现赋值重载了因为默认生成的就可以帮我们搞定了。 那同样如果涉及深拷贝的问题像栈Stack这样的类是不是就得我们自己实现去完成深拷贝了。 和拷贝构造一样如果类中未涉及到资源管理赋值运算符是否实现都可以一旦涉及到资源管理则必须要自己实现。 然后我们再来看一个代码 大家看这里会调用拷贝构造还是赋值重载 这里是不是拷贝构造啊这个我们上见过的嘛 那为啥这里用了赋值但是是拷贝构造呢 我们来简单总结一下 什么时候是调赋值重载呢 是我们用已经实例化出来的对象进行相互赋值的时候调用赋值重载。 而当我们用一个已经实例化出来的对象去初始化一个新对象的时候调的是拷贝构造。 赋值运算符只能重载成类的成员函数不能重载成全局函数 我们上面重载的一些什么等于、大于、小于、大于等于之类的运算符是不是可以重载到类外也可以重载到类里面啊。 那赋值重载也是运算符重载我们刚才是定义在类里面的那它可以重载到外面吗 我们试一下 先把成员变量的private注释掉确保在类外能访问。 然后我们在类外实现一下赋值重载 Date operator(Date left, const Date right)
{if (left ! right){left._year right._year;left._month right._month;left._day right._day;}return left;
}重载成全局函数注意重载成全局函数时没有this指针了需要给两个参数 那这就实现好了。 行不行呢 还没运行直接就看到报错了说必须是成员函数。 为什么这样不行呢解释一下 赋值重载如果在类里不显式实现编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载就和编译器在类中生成的默认赋值运算符重载冲突了故赋值运算符重载只能是类的成员函数。 6. const成员函数
我们来看这样一个类
class A
{
public:void Print(){cout _a endl;}
private:int _a 10;
};然后
int main()
{A a;a.Print();return 0;
}定义一个对象a并调用成员函数Print。 没有什么问题。 那这样呢 加一个const修饰对象a。 然后我们发现调用Print就出错了。 那为什么呢 其实呢是因为这里存在了一个权限放大的问题。 这也是我们之前学习过的对于引用还有指针来说对它们进行赋值和初始化时权限可以缩小但不能放大。 我们来分析一下 对于我们的成员函数Print虽然看起来没有参数但是是不是有一个隐藏参数就是我们熟悉的this指针嘛。 那this指针的类型是啥 this指针的类型类类型* const 那对于当前这个类来说就是A* const thisconst 修饰的是指针this即指针this不能被修改但this指向的内容可以被修改。 那我们传过来的参数是啥是调用函数的对象的地址即a的地址但我们的对象a是const修饰的所以传过来的地址的是const A* aconst修饰的是该地址指向的内容即对象a不能被修改。 那这样的话传给thisthis可以修改其指向的内容即对象a所以就是权限放大了。 所以这里报错了。 那怎么解决呢 如果我们可以把this指针的类型也变成const A*是不是就可以了啊。 但是this指针的类型是我们想改变就能改变的吗 this指针是类成员函数中的一个隐藏参数我们是没法直接改变它的。 那就没有办法了吗 办法肯定是有的 我们只需在对应成员函数的括号后面加一个const 就行了。 这就是我们要学的const成员函数 const修饰的“成员函数”称之为const成员函数。 const修饰类成员函数实际修饰的是*this这样this指向的对象将不能被修改。 那这样this指针的类型就也变成了const A*了这样就可以传了。
但是我们平时定义一个对象好像一般也不会在前面加一个const那这个用处是不是不大啊 虽然定义对象时我们一般不加const但是我们是不是可能经常会这样搞 void Func(const A x)
{x.Print();
}首先这里传引用与传值相比减少拷贝然后如果我们不想对象被改变的话不是一般会加一个const嘛。 那当前这种情况
class A
{
public:void Print(){cout _a endl;}
private:int _a 10;
};
void Func(const A x)
{x.Print();
}
int main()
{A a;Func(a);return 0;
}x是a的引用别名a没有被const修饰然后在Func里x是被cosnt修饰的x去调用Print这里是不是也是权限放大了。 那这是不是跟我们开始讲的那个例子一样啊怎么解决 把Print变成const成员函数就行了 像这种情况其实还是比较常见的。 所以说 对于类的成员函数如果在成员函数内部不需要改变调用它的对象最好呢都可以把它写成const成员函数。 另外如果const成员函数的声明和定义是分开的声明和定义都要加const。 7. 取地址及const取地址操作符重载 类的6个成员函数呢比较重要的前4个我已经学完了最后还剩两个。 我们一起来看一下 那剩下的两个默认成员函数呢都是取地址重载包括对普通对象的取地址和对const对象取地址。 这两个默认成员函数呢一般不需要我们自己去实现编译器会自动生成绝大多数情况下我们用编译器自动生成的就行了。 我们可以试一下 对普通对象取地址 对const对象取地址 所以这两个默认成员函数一般不需要我们自己写用编译器默认生成的取地址的重载即可 但是如果你想自己去重载一下的话当然也是可以的 你可以自己指定一个地址返回。 这两个运算符一般不需要重载使用编译器生成的默认取地址的重载即可只有特殊情况才需要重载比如想让别人获取到指定的内容 那我们这篇文章的内容就先到这里欢迎大家指正