回顾运算符重载的使用
弄懂了上述的知识,我们可以发现取地址符号的重载对我们来说是没有什么意义的,所以不需要过于在意,只要知道有这个东西,并且知道这个东西怎么使用就行了,此时我们就在取地址符号的基础上,我们再来回顾一下运算符重载是如何具体使用的,如下代码所示:
#include<iostream> #include<assert.h> using namespace std; class Array { public: int& operator[](int i)//直接重载这个括号,然后使用这个括号就是在执行我们这个函数 { assert(i < 10); return _a[i]; } const int& operator[](int i) const//所以此时可以提供一个重载版本给下面const修饰的使用 { //所以一个函数有可能是普通的函数,也有可能是const函数,所以一个运算符可以写多种版本的函数重载 assert(i < 10); //并且此时只要后面加const就是构成了我们的const成员函数了,前面加的那个const只是为了确定返回值的类型而已 return _a[i]; } private: int _a[10]; int _size; int _capacity; }; void Function(const Array&a) { for (int i = 0; i < 10; ++i) { cout << a[i] << " ";//此时这个就是经典的权利放大,调不动,所以要有const成员函数 } } int main() { Array a; for (int i = 0; i < 10; ++i) { a[i] = i;//此时我的i是访问不到我的私有成员变量的,但是此时上面的括号函数已经把_a[],作为返回值返回给我了,所以这整句代码就是一个函数调用的意思 } for (int i = 0; i < 10; ++i) { cout << a[i] << " "; } cout << endl; Function(a); return 0; }
从代码中,我们可以发现,我们的运算符重载int& operator[](int i)其实本质上就是一个函数,在使用该重载运算符的过程就是在调用一个函数的过程而已,并且我们的运算符重载是可以写成多样的形式的,如:const int& operator[](int i) const,可以写成具有常属性的const成员函数,也可以写成不具有const的成员函数,怎么写具体就是看你想要怎么使用,如果你传参是时候使用的是带有常属性的参数,那么你写的成员函数也就应该带有常属性,这叫做防止权利的放大,做到权利的保持,当然如果你写成const成员函数,那么缺点就是你不能进行修改,别的地方基本是没有缺点的,优点居多,再次强调,使用重载运算符就是在使用函数调用,如上述代码中的a[i] = i;和cout << a[i] << " ";其中使用的[],就是在调用我们的int& operator[](int i)运算符重载函数,并且像上述Function函数,我们可以看到,它的参数是一个具有常属性的参数,所以此时普通的运算符重载函数,它是调用不了的,原因就是会导致权利放大,所以此时必须要在int& operator[](int i)该重载运算符函数的基础之上,再通过函数重载的方式再重载一个const成员函数const int& operator[](int i) const,这样才可以同时满足我的各种需求,所以代码具体怎么写,细节怎么处理,大部分都是由我们自己决定的。
再谈构造函数(初始化列表的使用)
当初我们学习了构造函数的使用,但是我们只学了70%的样子,所以现在我们继续把有关构造函数的知识给学习一下,从而彻底搞定小小的构造函数,此时我们就来学一下什么初始化列表;初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个成员变量后面跟一个放在括号中的初始值或表达式。如下图所示:
所以如图所示,我们可以发现,当我们想要使用const修饰的变量时,就必须要给其初始化,然而,我们又发现一个问题,就是我们的类中只有声明的地方,并没有给变量定义的地方,所以此时就引出了我们的初始化成员列表的概念,并且哪个对象调用构造函数,初始化列表就是它所有成员变量定义的位置,不管是否对初始化列表就行定义,编译器此时都会默认每个变量是在初始化列表初始化的,如下图就是初始化成员列表的具体使用方法和细节处理:
所以此时我们可以发现初始化列表的一些的好处,它可以帮助我们进行成员变量的定义,和处理const成员变量,引用成员变量和没有默认构造函数的自定义成员变量的初始化定义,并且每一个成员变量都只能在初始化列表初始化一次,所以此时我们就大致学会了在构造函数中的初始化列表的使用。此时我们通过一个有关初始化列表的题目来看一下初始化的一些小细节的处理。如下代码:
#include<iostream> using namespace std; class A { public: A(int a) :_a1(a),_a2(_a1) { cout << "证明调用过该函数" << endl; } void Print() { cout << _a1 << endl << _a2 << endl; } private: int _a2; int _a1; }; int main() { A a(1); a.Print(); return 0; }
运行结果如下:
此时就可以说明一个问题,**成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关。**所以按照这个原理,此时得到的答案就是1和随机值,不是1 和1 ,这点要注意一下。
什么是explicit
此时搞懂了上述的知识,我们现在就来看一下什么是explicit,和explicit的基本使用方法是什么 ;此时针对于单参数的构造函数,A a1(1);这种,此时我们也可以写成这样A a1 = 1;但当我们写成这样时,此时就涉及了隐式类型转换,此时我们整形(int)先隐式转换成类(A)类型,再讲我们的数据赋值给a1这个对象,例如:我们在C语言中的double i = d;,所以当我们了解了什么是隐式转换,此时我们就来聊一聊,构造和拷贝构造的那点事,如:此时的A a1(1);因为是直接进行单个参数的传递,所以此时就可以直接调用构造函数进行a1对象的初始化,但是如果是:A a1 = 1;这样的,没有进行直接传值的,此时就会涉及到临时变量的问题,因为要隐式转换,所以它此时按照正常来说,会调用一次构造函数,去构造出一个A类型的临时变量,然后再通过A类型的临时变量将我们的数据拷贝构造给aa2,从而实现a1的初始化。但正常来说是这样的,此时由于编译器会对这种类型进行一定的优化,所以此时一次构造和一次拷贝构造会被优化成就一次构造,就会直接就用1去构造a1,不需要用1 去构造一个A类型的变量,再同过A类型的变量拷贝构造给aa2了。如下图:就可以很好的证明这个特点
#include<iostream> using namespace std; class A { public: A(int a) :_a1(a) { cout << "A(int a)" << endl; } A(const A& aa) :_a1(aa._a1)//这个的意思就是用一个已经初始话的成员变量去初始化另一个变量(名字相同,但是却是两个不同的变量) { cout << "const A& aa" << endl; } private: int _a1; }; int main() { A aa1(1);//此时按照我们以前学的有关,构造和拷贝构造的知识,此时这里应该是要调用一次构造就行了 A aa2 = 1;//一次构造和一次拷贝构造=>编译器会进行优化,直接就用1去拷贝构造aa2,不需要用1 去构造一个A类型的变量,再同过A类型的变量拷贝构造给aa2 //所以答案是两次构造就行来了 //const A& ref = 10;//注意此时用的是引用,所以在进行拷贝构造的时候,经过类型转换生成了那个临时变量是具有常属性的,所以只有加了常属性的ref此时临时变量才可以拷贝构造给ref,不然就是一个权利放大 return 0; }
并没有调用拷贝构造,而是优化成了一次构造,并且此时还涉及一个问题,就是关于引用的问题, const A& ref = 10;注意此时用的是引用,所以在进行拷贝构造的时候,经过类型转换生成了那个临时变量是具有常属性的,所以只有加了常属性的ref此时临时变量才可以拷贝构造给ref,不然就是一个权利放大问题,所以要注意好是否产生临时变量和const的合理使用
所以针对隐式转换讲了这么多,此时我们就可以来看看关键字(explicit) 的使用了,explicit的作用就是可以防止你进行类型的转换,起着不允许自定义类型直接变成内置类型的作用,使用场景:explict A(int a);此时对我们的构造函数加了这个新的关键字之后,我们的A类型就不允许被转换成内置类型了,也就是不能用int等内置类型就行对A类型对象的初始化。当然这个知识针对于单参数的构造函数而已
针对多参数的构造函数
多参数的想要进行隐式类型转换,此时就用我们的大括号就可以支持了,如: A a1 ={1,1};这样就支持类型转换,当然前提没有加explicit修饰构造函数。并且此时const A& ret = {2,2};这种代码和上述所说一样是涉及到了临时变量的常属性问题,这里不多加描述了。
什么是友元
友元我们其实在上述的知识中,我们已经学习过了,就是把一个全局变量的函数(或者称为该类外部的函数),在某个类中进行声明的,用关键字(friend) 进行声明,此时这样我的该函数就可以去使用类的成员变量或者成员函数了,就相当于该函数就是类的朋友,可以使用朋友内部的东西,如下图:就是一个友元的使用方式
友元函数的说明:
友元函数可访问类的私有和保护成员,但不是类的成员函数 |
友元函数不能使用const修饰 |
友元函数可以在类定义的任何地方声明,不受类访问限定符限制 |
一个函数可以是多个类的友元函数 (注意一下) |
友元函数的调用与普通函数的调用原理是相同的 |
友元类
就是表示不仅函数可以是类的友元函数,类也可以是类的友元,这个就叫友元类,无相互关系,只看谁的类声明了谁的函数或者声明了谁的类,谁就可以访问,还是以声明为主。你的是我的,我的还是我的,这句话很形象说明。
总:友元函数可以让我们直接访问类的私有成员变量或者函数,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时加上friend关键字就行了,但由于友元容易破坏类的封装,所以我们应该尽量少使用友元函数。
什么是static
概念:声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数,静态成员变量一定要在类外进行初始化。
实现一个类,计算程序中创建出了多少个类对象的代码如下:
原理:使用构造函数,因为不管怎样,只要有使用我们的类创建一个对象出来,哪么此时编译器就会优先去调用我们构造函数来对这个对象就行初始化,所以只要在构造函数中使用一个计数器(前提这个计数器是创建在全局的),此时就可以很好的使用这个计数器来统计我们创建了几个对象,但当我们一写完,却发现报错了,这是什么原因呢?原因就是:count在我们的C语言库中是一个已经被实现了的函数,此时如果我们使用的话,就会和库冲突,所以此时就有了两个解决的方法,一个是把库的展开using namespace std;给取消掉,还有一个就是用我们接下来要学习的static静态变量来处理,此时我们就直接把定义在全局的count计数器给放到类中,这样就可以避免命名冲突的问题,但是此时又有一个问题,就是把计算器放入了类中,此时这个计数器就不再是整个程序的了,而是变成某一个类独有的了,所以此时不符合我们的预期,所以此时我们就可以使用我们的static静态成员变量,让它去修饰该类中的那个计数器,这样就可以使这个计数器还是属于整个程序,而不是单独的某个类,这样,我们就可以真正的利用这个计数器去统计整个程序当中创建了几个类对象了。
注意点:1.此时的该静态成员变量不可以在类中进行初始化,只能是在全局进行初始化,因为此时的这个计数器是属于整个程序的,并不属于该类,类中只能初始化该类自己的成员变量 。2.访问静态成员变量一般会用到静态成员函数,这样在写法上更好。3.初始化一个数组时,数组的大小决定初始化的次数,也就是决定调用构造函数的次数。4.区分好类域,指明好函数调用的是那一个类,并且我们是可以直接使用类对象进行对类域的使用的。
#include<iostream> using namespace std; class A { public: A(int a = 0)//构造函数 { ++_count; } A(const A& aa)//拷贝构造函数 { ++_count; } static int GetCount()//下面那个叫静态成员变量,这个叫静态成员函数,特性:在类中没有this指针,因为其根本不属于该类,是属于整个程序的。 { //_a++;//此时没有this指针,所以就不可以去访问外部的非静态成员变量 return _count; } //一个自定义对象不是调用构造函数就一定是调用拷贝构造函数 //private: static int _count;// 把这个_count搞成静态的,此时它就是属于所有的对象,属于整个程序,这样就可以计算每个类创建了的对象了 //此处是声明,不可以给缺省值,想初始化只能下面这样,因为类中只能初始化自己的成员变量,有静态成员变量就一定要有静态成员函数,这样才可以正常使用 int _a = 0; }; int A::_count = 0;//静态成员的初始化 void Function(A a) { A aa1; A aa2; A aa3; A aa4[10];//直接会调用10次构造函数(初始化10次)这个联想到调式过程就很好理解 cout << A::_count << endl; } int main() { A a1; A a2; A a3; A a4; A aa; A* ptr = nullptr; //Function(a1); cout << A::_count << endl;//想要访问,就一定要声明类的作用域在哪里 cout << a1._count << endl; cout << ptr->_count << endl;//公有通过直接调用成员变量使用,ptr只是为了声明类域,并不是解引用,没有进行访问 cout << a3.GetCount() << endl;//私有通过函数调用 cout << aa.GetCount() - 1 << endl;//这种写法就是为了可以去访问类中的函数,但是导致我们多了一个对象,所以要-1,但是此时这种写法非常的傻 cout << A::GetCount << endl;//所以此时我们就可以实现一个静态成员函数,这样我们就可以直接访问类中的那个静态成员函数了,因为此时这个函数是属于整个程序的,所以不需要对象,只需要声明一下类域就行了 return 0; }
总:静态成员函数由于没有this指针无法访问非静态成员函数,而非静态成员函数由于具有this指针,所以可以访问静态成员函数。
什么是匿名对象
此时搞定了上述的一系列的问题,此时我们学习一个新的语法,叫匿名对象,例:此时我有一个类叫Sum,此时用这个类定义对象,我们就可以直接使用Sum();这个就是一个匿名对象的定义,此时的特性有:1.没有名字,2.生命周期就只有一行。所以此时凭借着生命周期只有一行的特性,我们对匿名对象的使用,就是在当我们要返回一个值和调用类中的某个函数或者成员变量的时候,我们经常就会直接使用匿名对象的调用,例:经常直接使用 cout << Sum().Solution(10) < endl;而不是先用类去创建一个对象,然后使用对象去访问类,例:先Sum a; 然后再cout << a.Solution(10) << endl;所以这就是匿名对象的好处,在有的情况下非常的方便。举例:一个一次性的杯子和一个经常用的水杯的区别。
编译器内部优化
这个知识点,我们在上述的构造函数和拷贝构造的时候大致讲过了,这里就不多加描述了,因为我要去睡觉了,北京时间:2023/2/9/15:22,感谢下午每课,不让这篇文章要到晚上才能发。午觉是人生中最快乐的事情。See you everyone.
总结:C++中类和对象的基本知识我们差不多是学完了,所以此时我们要开始学习C++后面的内容了,但我们并不可以和类和对象说拜拜,无论是以后我们会经常的使用类和对象,还是我们需要去把之前有关类和对象的知识给复习一下,都使我们离不开类和对象,类和对象可以说是C++中非常重要的一块知识了。So,lest’go.