一、类的默认成员函数
类内的默认成员函数:用户不显示实现,编译器就会自动生成的成员函数,被称为类的默认成员函数。分别是构造函数,析构函数,拷贝构造函数,赋值运算符重载,&操作符重载,const成员函数&操作符重载。
1.构造函数
构造函数会由编译器在类实例化的时候自动调用,而不需要由程序员自己手动调用初始化函数。而默认构造函数,还可以自动调用自定义类型成员变量的默认构造,注意,一定需要是默认构造。
无参或者全缺省的构造函数被称为默认构造函数,但是实际上,类对象在实例化时,只要存在对应的构造函数即可完成初始化。即使是含参数的非默认构造,也会自动调用自定义类型成员变量的构造函数。
一句话,构造函数的作用就是初始化类对象的内置类型和自定义类型的成员变量。如何初始化可以自己设置。
1.1构造函数的特点
无返回值
函数名为类名
构造函数可以重载
对象实例化时编译器自动调用对应的构造函数
1.2构造函数的作用
对象实例化时自动调用构造函数,完成对类对象的初始化。也就是对对象的成员变量初始化。
1.3初始化列表
赋值语句不符合构造函数初始化的定义,因此有了初始化列表的诞生。
初始化列表的写法:在构造函数函数名和参数列表之后,函数内部{}之前。以冒号开始,紧接着是成员变量(),括号内是对成员变量的初始化值或者表达式,其余成员变量之间以逗号隔开。
初始化列表的要求:
每个成员变量在初始化列表最多只能出现一次。
初始化列表必须使用的情况:
存在自定义类型的成员变量,且自定义类型没有默认构造函数
存在引用类型的成员变量
存在const成员变量
2.析构函数
构造函数是为了便于初始化,而析构函数是为了便于释放类对象所占用的空间资源。前者在类对象实例化的时候自动调用,后者在类对象生命周期结束的时候自动调用。
对于类对象在手动开辟的空间,比如通过malloc或者new的方式在堆上开辟空间,那么一定要显示的实现析构函数,并且通过delete的方式释放开辟的空间,否则就会造成内存泄漏。
对于局部变量,其生命周期结束后,资源就会自动被释放,但是手动开辟的空间,也需要手动进行释放,我们将释放的实现,通过析构函数来实现,虽然仍然需要自己实现,但是当生命周期结束时可以自动调用,一切都是为了方便。
2.1析构函数的作用
作用:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
翻译一下就是:析构函数的存在是为了释放动态申请资源所占据的空间(堆区),其余的临时变量和静态变量和全局变量,在程序结束后,会由编译器自动释放。
2.2析构函数的特点
析构函数是特殊的成员函数,其特征如下:
1. 析构函数名是在类名前加上字符 ~。
2. 无参数无返回值类型。
3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构 函数不能重载
4.对象生命周期结束时,C++编译系统系统自动调用析构函数
3.拷贝构造函数
类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请 时,则拷贝构造函数是一定要写的,否则就是浅拷贝。
拷贝构造是用一个已经初始化的对象来初始化一个未初始化的对象。
3.1编译器默认生成的拷贝构造函数
规定:,编译器生成的默认拷贝构造函数对于内置类型只会进行浅拷贝,而对于自定义类型,则会调用该自定义类型的拷贝构造。
浅拷贝也叫值拷贝,拷贝的是变量本身。例如拷贝int类型变量,就是拷贝变量本身,int*类型的指针变量,也会拷贝指针变量本身,也就是地址,而这,就可能会引起问题。
3.2拷贝构造函数的特点
1.是构造函数的重载函数。
2.拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用
这是因为,在调用拷贝构造函数的时候,也就是调用函数,如果是传值方式,会将实参拷贝一份,在传给形参,而这个过程也是在拷贝对象,也需要调用拷贝构造。也就是说,调用拷贝构造前的传参也是拷贝的过程,也需要调用拷贝构造,会引发无穷递归,或者报错。(注:对象是自定义类型)
3.拷贝构造函数的作用
作用:通过拷贝构造,用已经存在的对象初始化另一个对象(拷贝已存在的对象)。
4.赋值运算符重载
4.1 运算符重载
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其 返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)
注意:
不能通过连接其他符号来创建新的操作符:比如operator@ 重载操作符必须有一个类类型参数 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义 。作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this指针。
.* :: sizeof ?: . 以上5个运算符不能重载。
运算符重载要求重载操作符必须有一个类类型参数,但是我们会发现,如果是在类中进行重载,那么天然自带一个this指针。至于更多的参数需求,可以根据需要自行设置。
赋值运算符重载的参数如果只有一个,功能就和拷贝构造相同,都是给自己进行初始化。我们也可以设置为两个,让其中一个初始化另外一个,也可以设置多个,依次进行赋值。
如果类中未涉及到资源管理,赋值运算符是否实现都可以;一旦涉及到资源管理则必须要实现。
4.2 赋值运算符重载格式
参数类型:const T&,传递引用可以提高传参效率
返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
检测是否自己给自己赋值
返回*this :要复合连续赋值的含义
4.3注意事项
1.赋值运算符只能重载成类的成员函数不能重载成全局函数
原因:赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现 一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值 运算符重载只能是类的成员函数。
2.用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。
如果类中未涉及到资源管理,赋值运算符是否实现都可以;一旦涉及到资源管理则必须要实现。
5.&运算符重载和const成员函数的&运算符重载
这两个重载函数一般都是用于返回this指针的地址。一般情况下不需要自主实现。
二、const成员
1.概念
将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数 隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
2.疑难点
1. const对象可以调用非const成员函数吗?
2. 非const对象可以调用const成员函数吗?
3. const成员函数内可以调用其它的非const成员函数吗?
4.非const成员函数内可以调用其它的const成员函数吗?
普通对象的this指针的类型是 类类型const,也就是不能修改this指针的指向那么const对象的this指针就该是const 类类型*const,const对象新增了一条限制,不允许修改this指针指向的成员变量。而const成员函数,也是修饰的this指针,其this指针类型也为const 类类型*const。
3.答疑
普通对象:类类型* const
const对象:const 类类型*const
const成员函数:const 类类型*const
非const成员函数:类类型* const
const对象调用非const成员函数,权限放大,所以不能调用
非const对象调用const成员函数,权限缩小,可以调用
const成员函数调用非const成员函数,权限放大,不能调用
非const成员函数调用const成员函数,权限缩小,可以调用
总结:权限只能缩小或者平移,不能放大
三、static成员
1. 概念
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用 static修饰的成员函数,称之为静态成员函数。静态成员变量一定要在类外进行初始化。
static成员包括静态成员变量和静态成员函数,类的大小只和成员变量相关,但静态成员变量被所有类对象共享,存放在数据区(静态区),与类大小无关。如下图
同样的,静态成员函数也为所有类对象共享,存放在数据区(静态区),且没有this指针。
没有this指针,就意味着无法直接访问类的普通成员变量。如下图
static成员函数就相当于是一个被所有类对象知晓且共享的全局函数,虽然无法直接访问类的普通成员变量,但是如果函数参数有类类型的参数,还是可以访问的。
同样,static也无法直接访问类的普通成员函数,如下图
但是可以通过类对象参数,借用类对象进行访问。如下图
访问方式:static可以通过域作用限定符::static成员,进行访问。或者可以通过类对象.static成员进行访问。
总结:
1.静态成员函数可以调用非静态成员函数吗?
静态成员函数就是一个被所有类对象共享的全局函数,没有this指针,无法直接调用非静态成员函数,但是可以通过类类型参数的对象进行调用。
2.非静态成员函数可以调用类的静态成员函数吗?
一个全局函数,当然可以被调用。如下图
2.特性
1. 静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区
2. 静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明
3. 类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问
4. 静态成员函数没有隐藏的this指针,不能访问任何非静态成员
5. 静态成员也是类的成员,受public、protected、private 访问限定符的限制
四、友元
前言
输出运算符只能对已有类型进行输出,而不能对自定义对象直接进行输出
能否在自定义类型中对输出运算符重载,如下
ostream& operator<<(ostream& cout_);实现用输出运算符直接输出自定义对象。
可以看到,虽然可以实现,但是有问题。因为成员函数的第一个参数一定是隐含的this指针,而不是流对象,但是我们想要的就应该是cout<<自定义类型对象,需要流对象成为第一个参数,而不是自定义类型对象<<cout。
既然重载为类的成员函数不行,我们将重载放入全局不就OK了?尝试如下
重载到全局虽然可以自己设置参数的顺序,让流对象为第一个参数,但是在类外无法访问类内的private成员变量,虽然可以将其属性设置为public,但安全性可能得不到保证。
那么有没有一种函数,既能放在全局自己设置参数顺序,又能访问到类的私有成员变量呢?C++为此设计了新的语法,友元。
友元分为:友元函数和友元类
1.关于友元函数的特点
可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在 类的内部声明,声明时需要加friend关键字。
友元函数可访问类的私有和保护成员,但不是类的成员函数,所以友元函数不能用const修饰 。
友元函数可以在类定义的任何地方声明,不受类访问限定符限制 。
一个函数可以是多个类的友元函数。
友元函数的调用与普通函数的调用原理相同。
2.友元类
友元函数是将一个在全局的函数,通过friend关键字,在某一个类中声明,声明的友元函数可以在类外访问类内的私有或者受保护的成员变量。
如下图,全局的重载的<<函数不能访问类A的私有成员变量。
将该函数在类A通过frienf关键字声明为类A的友元函数,友元函数即可以访问,如下图。
而友元类和友元函数的作用相同,不过是将一个类,通过friend关键字,在另一个类中进行声明,那么声明的友元类的所以函数,都可以访问该类类内的私有或者受保护的成员变量。
如下图,类B中虽然有类A类型的成员变量,但因为类A成员变量的属性为私有,所以无法在类B的成员函数中访问类A类型成员变量的具体属性。
在类A中通过friend将类B声明为类A的友元类,类B的成员函数即可访问类A的私有成员变量,如下图。
实际上,友元类的所有成员函数都是友元函数。但是在友元函数中,无法直接访问类的静态成员变量。如下图
必须通过类访问限定符,或者类对象. ,如下图
3.内部类
在一个类中定义另一个类,一个类被称为外部类,另一个类被称为内部类,内部类天生是外部类的友元类。内部类除了具有友元类的所有功能外,内部类还可以直接访问外部类的静态成员变量,而无需域作用限定符或者对象.
其实内部类就是友元类的基础上加了可以直接访问静态成员变量。所以外部类的大小,只和外部类有关,与内部类无关。
如下图
五、匿名对象
C++通过语法 类型 对象名来定义一个类对象,。
C++同样允许定义一个没有名字的对象,被称为匿名对象,语法为类类型();
有名字的对象的生命周期是随着作用域的结束而结束,而匿名对象的生命周期只有一行,也就是定义匿名对象之后的下一行,匿名对象就会被销毁,由此可见,匿名对象其实是一个临时对象。
作用:可以通过匿名对象直接调用成员函数,节省空间。
或者在不确定函数参数的类型时,将匿名对象当作函数参数的缺省值。
可以是自定义类型,也可以是内置类型。如下图
六、拷贝对象时的一些编译器优化
运行结果:
参数如果是值传递或者返回值是传值返回,那么传值过去的和返回的都只是一份拷贝,所以才会出现四次析构的情况,f1中拷贝构造的对像除了函数作用域后生命周期结束,会立即被析构释放,f2也是相同。
本来类A的构造函数 将f1(1);中的1通过隐式类型转换,先构造了一个匿名对象,再通过拷贝构造拷贝一份作为参数传递个f1,来纳许构造+拷贝构造被编译器直接优化为了构造。
f1(A(2));分析
A(2)是用构造函数构造了一个匿名对象,在通过拷贝构造拷贝一份作为参数传递给f1函数,同样被优化为一次构造。
A aa2 = f2();分析
首先是f2();会经过构造+拷贝构造,但是被优化为了一次构造,而后赋值给aa2的过程中,先发生一次拷贝构造,再调用赋值运算符重载函数赋值,该过程的连续拷贝构造被优化为一次拷贝构造。