前言:本文是对类和对象知识点的最后一篇总结,前两篇的链接如下:
【逐步剖C++】-第二章-C++类和对象(上)
【逐步剖C++】-第二章-C++类和对象(中)
这三篇加起来就是笔者学习在类和对象中的所有总结了,希望能对读者有一些帮助
下面是文章导图:
那么本文也主要以导图为思路进行分享,话不多说,让我们开始吧
一、关于构造函数的补充
前言:在上一章中,我们了解到构造函数的基本功能是做一些初始化工作,对类的成员变量进行赋初值。但严格来说,构造函数内部的赋值不能称作是初始化,因为初始化只能初始化一次,而在构造函数内部可进行多次赋值操作。
且这里会涉及到一个有关类成员变量的声明和定义问题,因为有些类型的对象要求在定义时进行初始化。如const类型的对象。那么我们平常写的成员变量究竟是声明还是定义呢?如果是定义,那么在其之后给缺省值是否就相当于定义并初始化了呢?再具体一点,当一个const类型的对象作为一个类的成员变量时,其定义并初始化的位置在哪呢?这些问题会在下面的介绍中予以解答,请大家继续阅读。
1、初始化列表
(1)定义:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式,如:
class A
{
public:
A()
:_a1(1)
,_a2(2)
{}
private:
int _a1;
int _a2;
};
(2)四点特性:
- 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
- 类中包含以下成员,必须放在初始化列表位置进行初始化
- 引用类型的成员变量
- const成员变量
- 自定义类型成员变量(且该类没默认构造函数时)
说明:可以发现,前两种类型的变量都要求在定义时进行初始化,那么其实C++已经规定了,初始化列表为成员定义的位置。我们平常在private
下写的成员仅是声明。
还有一点,C++11后,支持在给类的对象声明时给缺省值,但给的缺省值本质也还只是声明,在调用构造函数创建对象时,都会走一遍初始化列表进行定义,此时若在初始化列表处对相应成员变量没有给值,才会真正用上声明时的缺省值。也就是说,声明时的缺省值是服务于初始化列表的。
那么最开始的问题也就有了答案:我们平常写的成员变量(无论是否有缺省值)都只是声明,成员变量定义并初始化的位置在构造函数的初始化列表
- 对于自定义类型的变量,编译器一定会先使用初始化列表进行初始化
- 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
可通过一道题来理解上面这个特性:
class A
{
public:
A(int a)
:_a1(a)
, _a2(_a1)
{}
void Print()
{
cout << _a1 << " " << _a2 << endl;
}
private:
int _a2;
int _a1;
};
int main()
{
A aa(1);
aa.Print();
}
如上程序运行的结果是?
A. 输出1 1
B.程序崩溃
C.编译不通过
D.输出1 随机值
答案是:D。解释如下:
初始化列表执行的顺序和成员变量声明的顺序一致,所以在初始化列表中先执行的是将_a1的值赋给_a2(此时_a1尚未初始化,是个随机值);接着才将参数1传给_a1,故最终输出的结果是1和随机值。
补充一点:初始化列表很重要,加上第三点特性,一般也建议用初始化列表进行初始化,但需要注意仍有初始化列表做不了的工作,如:给成员变量申请空间并检查甚至再对申请的空间写入一些数据等。
总结来说,在调用构造函数时,会优先走一遍初始化列表中的内容,接着仍会逐语句执行构造函数体中的内容。
2、explicit关键字
(1)构造函数的类型转换:
构造函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值的构造函数,还具有类型转换的作用
如:
class A1
{
public:
//单参数的构造函数
A1(int a)
{
_a1 = a;
}
private:
int _a1;
};
class A2
{
public:
//除第一个参数无默认值其余均有默认值的构造函数
A2(int a1,int a2 = 2)
{
_a1 = a1;
}
private:
int _a1;
int _a2;
};
int main()
{
A1 a1 = 1;
A2 a2 = 2;
return 0;
}
对于 A1 a1 = 1;
本质是隐式类型转换,编译器会先构造一个成员变量_a1 = 1
的A1类的匿名对象(关于匿名对象后文会介绍),最后匿名对象在将自己拷贝给对象a1;对于 A2 a2 = 2;
同理。
(2)explicit关键字:
用explicit
修饰构造函数,将会禁止构造函数的隐式转换。一定程度上可以增强代码的规范性。
在如上A1,A2两个类的构造函数前加上explicit关键字后,main函数中的两条语句将不能执行,编译报错为:
二、static静态成员
1、概念
声明为static
的类成员称为类的静态成员,用static
修饰的成员变量,称之为静态成员变量;用static
修饰的成员函数,称之为静态成员函数。
2、特性
(1)静态成员为所有类的对象所共享,不属于某个具体的对象,存放在静态区
(2)静态成员变量必须在类外定义,也就是说其没有初始化列表(类的普通成员定义的地方),故不能在声明时直接给缺省值;定义时不添加static
关键字,类中只是声明
(3)类静态成员即可用 类名::静态成员
或者 对象.静态成员
来访问
(4)静态成员函数没有隐藏的this指针,不能访问任何非静态成员;静态成员函数和静态成员变量一般配套出现
(5)静态成员也是类的成员,受public
、protected
、private
访问限定符的限制
(6)静态成员就相当于同一个类的所有对象的全局变量(或者可理解为将全局变量封装到类中,但只有通过该类或类的对象才能访问)
代码例:
class A
{
public:
static int GetA() { return _a; } //静态成员函数
private:
static int _a; //静态成员变量
};
int A::_a = 0; //静态成员变量需在类外定义(注意是定义,所以要加上类型)
int main()
{
A a;
//两种访问静态成员函数的方法
cout << a.GetA() << endl;
cout << A::GetA() << endl;
}
常见问题:
- 静态成员函数可以调用非静态成员函数吗?
不能,因为静态成员函数没有this指针,无法进行传参; - 非静态成员函数可以调用类的静态成员函数吗?
可以,都在类域中,类的静态成员函数就相当于对于整个类而言的全局函数
3、使用场景
(1)实现一个类来统计程序中共创建了多少这个类的对象。
实现思路:类的对象在创建时会自动调用类的构造函数或拷贝构造函数,故在调用构造函数和拷贝构造时时让相应地静态成员变量++
即可。
具体代码如下:
class A
{
public:
//对象创建,计数变量++
A() { ++_scount; }
A(const A& t) { ++_scount; }
//对象销毁,计数变量--
~A() { --_scount; }
static int GetACount() { return _scount; }
private:
static int _scount;
};
int A::_scount = 0;
(2)还有一道 “趣味” 题
原题链接:JZ64 求1+2+3+…+n
求解思路:创建一个类,类中有两个静态成员变量,一个_count
用于统计当前对象的数量,一个_sum
用于进行总和的计算。把它们放入构造函数中,每创建一个对象时
_count++; sum += _count;
这样在创建完n个对象时_sum的值即为1+2+3+…+n的值。
具体代码如下:
struct Sum
{
Sum()
{
_count++;
_sum += _count;
}
static int _sum;
static int _count;
};
int Sum::_sum = 0;
int Sum::_count = 0;
class Solution {
public:
int Sum_Solution(int n)
{
for(int i = 0; i < n; i++)
{
Sum s;
}
return Sum::_sum;
}
};
(3)有时会有这样的要求:设计一个类,在类外只能在栈上创建对象或在类外只能在堆上创建对象
为了实现这个要求,除了要利用静态成员函数外,更重要的一步是对构造函数的 “不一般” 处理——将构造函数作为private
成员。如此一来就只有在类中的成员函数才能调用构造函数了;此时将相应调用构造函数的成员函数作为public
并定义为static的即可(向外界提供接口)
具体代码如下:
class A
{
public:
//返回一个栈区上的A类对象()
static A GetStackObj()
{
A a(1);
return a;
}
//返回一个堆区上的A类对象
static A* GetHeapObj()
{
return new A(1);
}
private:
A(int a =1)
:_a(a)
{}
int _a;
};
int main()
{
//创建一个栈区上的A类对象
A sa = A::GetStackObj();
//创建一个堆区上的A类对象
A* ha = A::GetHeapObj();
return 0;
}
三、友元
前言:友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
1、友元函数
(1)定义:友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend
关键字。
在上一篇日期类的简单实现中,流插入和流提取运算符的重载就是友元函数
(2)特性:
- 友元函数可访问类的私有和保护成员,但不是类的成员函数
- 友元函数不能用
const
修饰 - 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
- 一个函数可以是多个类的友元函数
- 友元函数的调用与普通函数的调用原理相同
2、友元类
(1)定义:
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
(2)特性:
- 友元关系是单向的,不具有交换性。
若声明A为B的友元类,则在A中可以直接访问所有B类中的非公有成员;但在B类中无法访问A类中的非公有成员 - 友元关系不能传递
如果C是B的友元, B是A的友元,则不能说明C时A的友元 - 友元关系不能继承(关于继承会在后续章节进行说明)
四、内部类
1、定义
如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限
2、特性
(1)内部类天生就是外部类的友元类
(2)注意内部类可以直接访问外部类中的static
成员,不需要借助外部类的对象/类名(因为静态成员变量属于整个类,故对内部类也不例外)
(3)sizeof(有内部类的外部类对象) = sizeof(没有该内部类的相同外部类对象)
。即计算外部类对象的大小时,与内部类无关,因为内部类仅是定义在外部类中,并没有实例化出对象
(4)内部类也受访问限定符的限制,限定的效果体现在通过外部类使用内部类时
相关代码例:
class A
{
public:
A(int a1 = 1, int a2 = 2)
:_a1(a1)
,_a2(a2)
{}
static int _acount;
//内部类
class InnerA
{
public:
InnerA(int ia1 = 1, int ia2 = 2)
:i_a1(ia1)
, i_a2(ia2)
{}
void Func(const A& a)
{
cout << a._a1 << " " << a._a2 << endl; //内部类天生就是外部类的友元类
cout << ++_acount << endl; //直接访问外部类中的static成员
}
private:
int i_a1;
int i_a2;
};
private:
int _a1;
int _a2;
};
int A::_acount = 0;
int main()
{
A::InnerA ia;
A a(2, 3);
ia.Func(a);
}
运行结果:
其中A::InnerA ia;
是使用内部类创建对象的方法。InnerA类本质定义在了A类中,故若想使用则需通过域作用访问符,“ 穿过 ” A的类域去使用(和命名空间类似,告诉编译器到哪里去找);注意若InnerA类在A类中为private
成员,那么穿过A的类域也是无法使用InnerA类创建对象的。
五、匿名对象
1、定义
匿名对象就是和我们平常定义的对象相比,就是没有名字对象,定义方式为类型(),如:
//定义一个A类对象
A a;
//定义一个A类的匿名对象
A();
2、特性
(1)匿名对象不用取名,定义后能和正常对象一样使用;
(2)匿名对象的生命周期只有一行,即用即销;
(3)匿名对象具有常性;则const引用可延长匿名对象的生命周期,如:const A& ra = A();
(4)匿名对象通常用于仅需要相关类的对象发挥一次作用的场景,如:
class A
{
public:
A(int a = 1)
:_a(a)
{}
private:
_int a;
}
void Func(A& a)
{
...//
}
//若仅需将成员变量为1的A类对象传给Func函数完成一次操作即可写为:
Func(A(1));
//A(1)表示创建了一个成员变量值为1的A类匿名对象
六、编译器拷贝构造的优化
在传参和传返回值的过程中,一般编译器会做一些优化,减少对象的拷贝,提高程序运行的效率,下面结合代码进行简单的介绍,大家对这部分的知识可以作为了解即可。
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a)" << endl;
}
A(const A& aa)
:_a(aa._a)
{
cout << "A(const A& aa)" << endl;
}
A& operator=(const A& aa)
{
cout << "A& operator=(const A& aa)" << endl;
if (this != &aa)
{
_a = aa._a;
}
return *this;
}
private:
int _a;
};
void f1(A aa)
{}
A f2()
{
A aa;
return aa;
}
int main()
{
cout << "(1):" << endl;
f1(1); // (1)
cout << endl;
cout << "(2):" << endl;
f1(A(2)); //(2)
cout << endl;
cout << "(3):" << endl;
A aa1 = f2(); //(3)
cout << endl;
cout << "(4):" << endl;
A aa2;
aa2 = f2(); // (4)
cout << endl;
return 0;
}
运行结果:
说明:
- (1)
其中包含了隐式类型转换:因为函数f1
需要接收一个A类的对象,所以先用整数1构造出了一个A类的匿名对象;又因为函数f1
为传值调用,所以在调用时还会进行一次拷贝构造;由此一来,此条语句的执行就包含了连在一起的构造+拷贝构造,此时编译器会优化为直接构造 - (2)
本质和1类似,先是显示创建了一个成员变量为2的匿名对象,再将其作为参数传给f1
;所以本条语句的执行也包含了连在一起的构造+拷贝构造,编译器会优化为一个构造 - (3)
调用函数f2
返回一个A类对象会调用一次拷贝构造(PS:函数f2中还有一次构造,但不涉及到这里的优化,这里的拷贝构造是将f2
中构造出来的对象aa
拷贝给一个A类的临时对象);返回的A类对象再用于初始化另一个A类对象,会再调用一次拷贝构造(PS:将A类的临时对象拷贝给将要创建的对象aa1
);如此一来,此条语句的执行就包含了连在一起的拷贝构造+拷贝构造,此时编译器会优化成一个拷贝构造 - (4)
先调用构造创建了一个对象aa2
,之后通过调用f2
返回一个对象重新赋值给该对象;那么此条语句包含的就是连在一起的 拷贝构造+赋值重载,但可以发现编译器对此不会进行优化
强调一点:
可以发现,上面所发生的优化都有一个共同的要求:连在一起
若构造和拷贝构造不在一条语句时,就不会发生优化,如:
A aa1; //构造
f1(aa1); //拷贝构造
故构造和拷贝构造能写到一个步骤就写到一个步骤。
本章完
看完觉得有觉得帮助的话不妨点赞收藏鼓励一下,有疑问或有误地方的地方还请过路的朋友们留个评论,多多指点,谢谢朋友们!🌹🌹🌹