类与对象(下)

1.再谈构造函数

    类与对象下的第一个问题就是再谈构造函数,下面看这样一个例子:

一个类要初始化成员变量有两种方式在构造函数里面,第一种方式叫构造函数体内赋值,第二种方式叫初始化列表。构造体赋值:在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值。虽然构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对对象中成员变量的初始化, 构造函数体中的语句只能将其称为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值。初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。为什么C++祖师爷要设计初始化列表?因为有些情况只有初始化列表才能解决,先看初始化列表有这样一些规定:1. 每个成员变量在初始化列表中最多只能出现一次(初始化只能初始化一次,不出现也是可以的)。2.类中包含以下成员,必须放在初始化列表位置进行初始化:1.引用成员变量 2.const成员变量 3.自定义类型成员(且该类没有默认构造函数时)。下面以这样一个例子一个一个进行相关举例:

第一个是const成员,const成员按照以前的方式在函数体内赋值是不可以的,会报错,除了它引用也不可以在函数体内赋值。这两个不能在函数体内初始化是因为它们两都有个初始化的特征,针对这两个成员的特征是必须在定义的时候初始化:引用必须在定义的时候初始化是必须变成谁的别名;const不能改了,也必须是定义的时候只有一次初始化的机会。如果成员变量里有这两个成员变量总是要初始化吧,那什么地方是它们定义的地方呢?我们知道这些成员变量都是类对象的一部分,对象在实例化的时候它就整体定义了,对象定义要调用构造函数完成初始化,但这里说的是对象整体定义,对象中有多个成员,每个成员到底在哪里初始化呢?

上图是对象整体定义,那对象成员在什么地方定义呢?祖师爷选了个地方,这个地方叫初始化列表,他认为初始化列表是对象的成员定义的位置。对象整体定义要调用构造函数,可认为构造函数对每个成员进行初始化。构造函数的花括号中不是成员变量定义的地方,这里都是赋值了,所以之前用的方法的名字叫函数体内赋值。

所以按照上图的方式初始化比较合适,而且这个格式是规定死的,此时就可以编译通过了。

那初始化列表中写两次一样的可以吗?不可以,因为前面说了每个成员变量最多只能在初始化列表出现一次,况且只能定义一次,这是定义的地方。下面再来看看自定义类型成员,之前说过内置类型不做处理,自定义类型会去调用它的默认构造:

上图看到会调用,是在初始化列表中调用的,实例化bb1时调用构造函数,在初始化列表中实例化_aobj时又去调用构造函数。

初始化列表中成员函数有且仅能出现一次,但不是说必须出现一次,也可以不出现,如上图。不出现还是对于自定义类型会去调用它的默认构造函数,对于内置类型不会处理,就是原自初始化列表,它是成员变量定义的地方,类定义还是去调用构造函数:

那如果A中啥事都没有做什么都没写自定义类型为啥处理?因为初始化列表阶段你不调用构造它也会去调用,因为这是定义的地方,如果写了就用写的,没写就用它自己的,只是说对内置类型它不处理。但内置类型也不是完全不处理:

比如上图int x = 2,这里给的2是缺省值,缺省值是给初始化列表的,本来不处理,写了就会用缺省值初始化。

但是如果在初始化列表中显示写了,那缺省值就没有用了。此时还有一类情况:

如果A_aobj没有默认构造函数怎么办?(全缺省、无参、自动生成的都叫默认构造函数)此时不显示调编译器就编译不通过了,此时只有在一个阶段可调用A中的构造,就是在B的列表里,需要_aobj(10)这样调,意味着没有默认构造函数就在这里显示调用构造函数。

如果A中有默认构造函数,在B中显示或不显示调用都可以,如果显示就用显示的参数,不显示就用默认构造,所以说初始化列表本质就是成员变量定义的地方。了解到这里有个问题就可以解决了:

当时说MyQueue可以不写构造函数,不写编译器会默认生成,_pushst和_popst会去调用默认构造,就把capacity初始化为10了,但有没有一种可能性:

期望自己可以控制capacity,可以手动的传参。

现在不传参也可以初始化,因为不写所有成员还是会走初始化列表,初始化列表是成员定义的地方,写了就用写的,没写定义时去调用默认构造函数,就是在初始化列表中调用的。

如果想显示调用也可以,就会走自己写的,这样自己也可以控制了,通过调式也可以看到。此时对于自定义类型想自己显示初始化也可以,不想显示初始化用默认的也可以。

上图还有一个小问题:引用的初始化有一些问题,引用的是一个局部变量,出了作用域ref销毁,引用会变成野引用,所以应该改为这样:

改为int& ref后就不能传常量了,所以写个变量,把变量传过去,ref是n的别名,_ref是ref别名也是n的别名。上面的小问题语法上没问题但野引用了,所以这里发现引用也并不是绝对安全。初始化列表是构造函数的一部分,替代的是函数体内赋值,以后基本建议用初始化列表初始化就可以了。那是不是当初祖师爷最开始用初始化列表不要用函数体内赋值不就挺好的?其实不是这样的,看下面一个例子:

在Stack这里,初始化列表也可以完成所有成员变量的初始化,只是写起来有点复杂,那现在有个问题,要不要做_a的检查?要,此时初始化列表就不能完成了,所以此时要让函数体上场。可理解为初始化列表把95%的事情完成了,但有5%处理不了。再比如要求把数组都初始化为0呢?所以按照下图方式写:

再看初始化列表中的一个坑:

可以看看这个题选什么,假如用排除法猜一下,上面说了这里是坑,所以排除A;这里也没有什么语法问题,所以排除B;一般涉及内存越界,指针问题程序会奔溃,所以排除C;这里选D,那为什么选D呢?因为成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关。所以这里先初始化_a2,再初始化_a1,所以输出1和随机值,因此建议声明顺序和定义顺序保持一致。

    再总结一下上述类容:

初始化列表不论是无参、全缺省、其他我们不写初始化列表它也会走初始化列表,像MyQueue一样,为什么会走呢?因为初始化列表是这些成员定义的地方,定义的地方自定义类型就会去调用它的默认构造。如果还有个_size,它也会走初始化列表,只不过没有给值的化被初始化为随机值。这也就说明了为什么自定义类型会去调用它的默认构造,内置类型不做处理。如果想让_size有值有这样的方法:1.用缺省值。2.初始化列表中给值。3.花括号中赋值。像size这样的建议直接使用缺省值比较爽。

    下面还是C++中一个很坑的地方,看下图例子:

这里实例化aa1没什么问题,是调用构造函数就可以了。那A aa2 = 2是什么意思呢?这里是隐式类型转换,将整型转化成自定义类型。

就像double d = i 一样,这里类型转换会产生临时变量,这个临时变量是double类型的,i给double的临时变量,这个临时变量再给d。所以之前说过double那里不能加引用,因为引用的是临时变量,临时变量具有常性,加const引用才可以。A aa2 = 2这里也是产生了临时变量,这个临时变量是A类型的,它用2做参数去调用它的构造函数生成一个A类型的临时对象,然后再去调用拷贝构造生成aa2。但是特别老的编译器可能不会优化,现在的编译器基本都会优化,编译器不会容忍构造一个对象再去拷贝构造,会优化为用2做参直接构造aa2。为了验证这个结论可以看一下下图:

构造和拷贝构造都是构造,编译器不能容忍在同一个步骤里的连续构造,同一步骤里连续的构造编译器基本都会优化,这样做为了提高效率,所以这里只看到调用了两次构造函数。那此时就有质疑了,我觉得A aa2 = 2这里就是用2直接构造,没有说先用2构造一个临时对象再去拷贝构造,那这里看这样一个例子:

上图左边报错了,aa3引用2出现了问题,但是加const就可以了,并且还调用了一次构造。这里只有用2构造临时变量,没有拷贝构造是因为这里是引用,临时变量具有常性用const引用才可以,所以说明有先构造一个临时对象。那如果不想让这样的转换发生呢?

就在构造函数前加一个关键字 explicit,此时就不允许类型转换发生了。

2.static成员

    下一部分说一下静态成员,下面先看这样一个例子:

比如说我想统计一下这个系统里面当前还有多少个对象正在使用,其中想到的思路之一就是定义一个全局变量_scount,给这个全局变量初始化给0。每次调用一次构造和拷贝就++一次,调用一次析构就--一次,这样就知道现在有多少个对象了,这里随机写一些对象来测试一下(__LINE__这个宏表示提取文件的哪一行),此时就可以统计出有多少对象了(356行有一个说明全局在main函数之前就调用了构造,静态不会在main之前初始化)。再看一下下图感受一下:

这里想说的是static A aa2是一个局部的静态对象,这个局部的静态对象在静态区只会被定义一次,它不在Func这个栈帧里面。这里用构造和拷贝非常灵活的可以统计个数,但这个代码有个不好的地方:

可以随意的对这个变量++,用全局变量的劣势是任何地方都可以随意的改变。祖师爷不喜欢这种方式,所以提出了一个方法说能不能封装一下?不想用全局的,可以把这个数据封装在类里面,也就是除了定义成员变量外还可以定义静态成员变量。

成员变量和静态成员变量的区别是:成员变量属于每一个类对象,存储在对象里面;静态成员变量属于类,属于类的每个对象共享,存储在静态区,它的生命周期是全局的。它们虽然都是声明,但是static int _scount要在类外面定义,不能在初始化列表_scount(0)这样初始化,因为这是对象成员定义的地方,它不属于对象自己的成员,它是属于每个对象共享的,所以它要在类外面(全局位置)定义,必须以这样的形式写:int A::_scount = 0,可认为这里可以突破一次私有。

也不能像上图直接A::_scount这样访问,因为它是私有的,假设放到公有可以这样访问。那为什么这里共有的可以私有的不可以?因为_scount属于整个类,也属于每个对象共享,如果放共有就和全局差不多了,放成私有就不一样了,此时就不能随意像在Func中那样++了。那Func这些地方想访问_scount只能通过共有的成员函数,比如提供一个GetAcount函数。

提供函数别的都可以用对象访问,但是main函数下面那里都没有对象怎么访问的到呢?这里意味着静态成员变量突破类域和访问限定符就能访问,所以还有个更好的方式是有个配对的,除了静态成员变量还有个东西叫静态成员函数:

静态成员函数的特点是没有this指针,指定类域和访问限定符就可以访问。所以一般静态成员函数和静态成员变量是配套出现的,不能A::_scount但可以A::GetAcount()。静态成员函数也可以像aa2.GetAcount这样用对象调用,因为对象.可以突破类域,突破类域的意思就是告诉编译器去那里找。

所以静态里是不能访问非静态的,因为它没有this指针。静态函数里是可以访问静态成员变量的,因为属于每个类。这个比起全局的好处是至少不能在外面用函数随便改,类这里的静态成员相当于用类去封装全局变量,这样对数据管理更规范。也不能给静态成员变量缺省值,因为它没有初始化列表,反正静态成员变量就是在类里面声明,在类外面定义,受访问限定符的限制。下面来看一个题目,通过该题体会一下静态变量的用法:

求1+2+3+...+n_牛客题霸_牛客网求1+2+3+...+n,要求不能使用乘除法、for、while、if、else、swit。题目来自【牛客题霸】https://www.nowcoder.com/practice/7a0da8fc483247ff8800059e12d7caf1?tpId=13&tqId=11200&tPage=3&rp=3&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking

更具题目的描述,这里相当于把平时用的常规方法都禁止了,让我们用新的方法解决,这里有个巧妙的方法:不是有n个相加,那可以创建长度为n的数组sum a[n] (支持这样写的前提是编译器支持边长数组),这里创建了n个对象,n个对象就要调用n次构造函数,像循环那些其实也是出现n个值。n次调用里面不断加等就可以了,加等所要的变量定义在局部肯定不行,全局的没有被封装,所以推荐定义静态变量,定义i和ret,从类里面声明外面定义。每次让ret+=i,i++就可以了,题目要求返回,静态成员变量在私有里面,所以写静态函数返回。

    静态中还涉及这样一个问题:

Func1中可以调用GetAcount,也就是非静态可以调用静态,因为静态的指定类域和访问限定符就可以访问,类中没有类域和访问限定符的限制,类外有。在GetAcount中不能调用Func2,也就是静态不能调用非静态,因为Func2中可能做_a++这样的动作,静态函数没有this指针。

    有些地方还有这样的限制:设计一个类,只能在栈上创建对象或只能在堆上创建对象。如果没这个条件可以随便创建对象:

现在只能在栈或堆上创建对象怎么办?

观察看出这三个对象都有的特征就是调构造函数,这里把构造函数置为私有就调不动了,然后提供静态函数,相当于垄断了,这样就可以达到目的了。

3.友元

    友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度(关系更紧密了),破坏了封装,所以友元不宜多用。友元分为:友元函数和友元类。

    友元函数:现在尝试去重载operator<<,然后发现没办法将operator<<重载成成员函数。因为cout的输出流对象和隐含的this指针在抢占第一个参数的位置。this指针默认是第一个参数也就是左操作数了。但是实际使用中cout需要是第一个形参对象,才能正常使用。所以要将operator<<重载成全局函数。但又会导致类外没办法访问成员,此时就需要友元来解决,operator>>同理。友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字。

再看几点说明:1.友元函数可访问类的私有和保护成员,但不是类的成员函数。2.友元函数不能用const修饰,因为没有this指针。3.友元函数可以在类定义的任何地方声明,不受类访问限定符限制。4. 一个函数可以是多个类的友元函数。5.友元函数的调用与普通函数的调用原理相同。

    友元类:友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。

友元关系是单向的,不具有交换性: 比如上述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time 类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。

4.内部类

    普通的类都是定义在全局的,内部类就是可以定义在类里面的。如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。下面看第一个问题:

计算A的大小是4,这里不算k的大小,因为k没有存到对象里面,这里是算了h的大小,A这个类里面没有B对象,所以不算B对象的大小,除非定义一个B类对象:

这时大小就是8,所以内部类A类和B类暂且看就是没有关系。

上图左右两边的区别是:右边B类是定义在全局的,左边B类是定义在A的类域里的,并且不占空间。因为这都是声明,定义出的对象才会占空间,并且main中如果定义A aa中是没有B对象的。

main函数中不能直接B bb,这个好比说是B这个房子的图纸是放在A图纸里面的,比如现在有两个房子A和B,内部类的原理就是现在设计A房图纸,但A房工程大所以附赠一个B房设计图纸,突然说不要A房设计单独要B房设计是不可以的,所以必须要A房设计才能拿B房设计,所以必须用A指定一下才可以。现在B是公有的,如果弄为私有的也会改变访问限定符限制,它相当于类域中定义的类型,也受访问限定符限制。所以内部类一种是公有的内部类,一种是私有的内部类,所以比如不想让别人用这个类就可以定义为内部类且是私有的,这样别人用不了,只有类中可以用。还有内部类是外部类的天生友元:

所以上图可以通过A对象访问A的私有成员,静态成员也可直接访问。C++一般不喜欢用内部类,所以这里了解一下就行,但有了内部类下题可以进行改造:

求1+2+3+...+n_牛客题霸_牛客网求1+2+3+...+n,要求不能使用乘除法、for、while、if、else、swit。题目来自【牛客题霸】https://www.nowcoder.com/practice/7a0da8fc483247ff8800059e12d7caf1?tpId=13&tqId=11200&tPage=3&rp=3&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking这题需要Sum类和Solution类,Solution这个类要定义n次构造函数然后去完成1~n的加等,所以定义Sum类去完成,那现在可以弄成内部类试一下:什么情况下可以用内部类呢?就是我期望Sum这个藏在Solution类里面,别人都访问不到;还有个特点是内部类是外部类的友元,Solution不能直接访问Sum的成员,但在Soultion里是可以用Sum的,因为是内部定义的。但不能用_ret,因为不是互相友元,所以把_i和_ret放外部定义,Get函数也不要了,因为有元所以直接返回_ret。

所以这有个契合是如果数据定义在外部类,内部类和外部类都可以访问,因为内部类天生是外部类的友元,类中定义出来可以给别人用的东西才受访问限定符的限制。

5.匿名对象

上图看到可以A aa(1)这样定义一个对象,还可以A(2)这样定义对象。那这两个对象的区别是什么?一个是有名对象,一个是匿名对象,语法上能通过,也可以运行,那没名字怎么用呢?

看上图场景,假设想调用Sum_Solution怎么调用?按照以前有名对象的方式是定义一个对象,通过对象.去调用;现在就可以直接用匿名对象去调用了。如果想调用一次就用匿名,想调用多次就用有名。

这里运行观察一下,也看到匿名对象的特点是即用即销毁。有名对象的生命周期在当前函数局部域,匿名对象的生命周期在当前行。那没有参数可以Solution.Sum_Solution(20)这样调用吗?不可以,因为类型不能调用函数,也不能Solution::Sum_Solution(20)这样调用,只有静态成员函数才能这样调,因为静态成员函数没有this指针,如果这样调没法传this指针。匿名这加括号就不是类了,必须有对像传给this,有名那无参不加括号是怕和函数声明分不开,一般想定义个临时对象用一用如定义一个对象为了调一次函数就可以用匿名对象。

    下面再看这样的例子:

发现ra不可以引用匿名对象,因为匿名对象具有常性,所以前面加一个const就可以了。那ra会变为野引用吗?

从上图可以发现,const引用延长了匿名对象的生命周期,生命周期在当前函数作用域,就是引用的生命周期在哪里它的就在哪里了。可以这样理解:A(2)是一个匿名对象,后面也没有对象用它干脆销毁了;const A& ra = A(2)中也有匿名对象,但ra要用它,所以不能销毁,留下让别人用。

    下面再来看这样一个东西:

上图这里有一个A类

现在调用Func1,左边是传值传参,a1传给aa要去调用拷贝构造,不想调用拷贝构造就加引用和const,这两个可以同时存在构成重载,但存在的问题是Func1(a1)这里调用不明确,所以将右边的函数名换为Func2就可以了。下面再看:

这里有一次构造,还有传值返回的拷贝构造,有两次构造。下面再看:

这里传引用返回,就没有拷贝构造了。Func3和Func4也证明了用引用返回可以减少拷贝构造提高效率。下面再看:

这个表达式的返回值是aa拷贝的临时对象,所以不能直接用引用接受返回,前面需要加const,临时对象具有常性。

那直接用对象接收返回值应该有2次拷贝,但新的编译器是一次,因为编译在连续的一个步骤中的构造会进行优化,就直接拿aa构造ra了。

上图左边不会优化,因为连续构造不在一行,右边会进行优化。直接Func(1)也会优化,这个和    A aa2 = 1是一个道理。再如有A ra2;ra2 = Func5();这里已经定义了ra2,所以是赋值。所以通过下图观察一下:

明显发现上面优化的更好,所以以后遇到连续的构造如果可以写到一个步骤中就写到一个步骤里面。

6.再次理解类和对象

    现实生活中的实体计算机并不认识,计算机只认识二进制格式的数据。如果想要让计算机认识现实生活中的实体,用户必须通过某种面向对象的语言,对实体进行描述,然后通过编写程序,创建对象后计算机才可以认识。比如想要让计算机认识洗衣机,就需要:1. 用户先要对现实中洗衣机实体进行抽象---即在人为思想层面对洗衣机进行认识,洗衣机有什么属性,有哪些功能,即对洗衣机进行抽象认知的一个过程。2. 经过1之后,在人的头脑中已经对洗衣机有了一个清醒的认识,只不过此时计算机还不清楚,想要让计算机识别人想象中的洗衣机,就需要人通过某种面相对象的语言(比如:C++、Java、Python等)将洗衣机用类来进行描述,并输入到计算机中。3. 经过2之后,在计算机中就有了一个洗衣机类,但是洗衣机类只是站在计算机的角度对洗衣机对象进行描述的,通过洗衣机类,可以实例化出一个个具体的洗衣机对象,此时计算机才能知道洗衣机是什么东西。 4. 用户就可以借助计算机中洗衣机对象,来模拟现实中的洗衣机实体了。在类和对象阶段,大家一定要体会到,类是对某一类实体(对象)来进行描述的,描述该对象具有哪些属性,哪些方法,描述完成后就形成了一种新的自定义类型,再用该自定义类型就可以实例化具体的对象。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值