C++类和对象下[初始化列表,静态成员,explicit关键字,友元]
一.初始化列表
1.为什么会有初始化列表
我们在Date中添加了两种成员变量:
分别是引用类型和const类型
为什么编译器会报错呢?
是不是因为编译器默认生成的构造函数不行呢?
那我们自己去实现一下怎么样?
还是不行:它说引用和const类型的对象定义时必须初始化
对啊,因为引用不能改变指向,所以必须在初始化引用的时候就要指定好对象
const类型的变量的值是不能修改的,因此初始化时也必须设好值
那么我们应该怎么办呢?
针对于这个问题C++创始人规定了初始化列表这一语法:
为了解决有些成员变量在定义的时候必须初始化!!!
2.初始化列表的语法形式
Date(int year = 1, int month = 2, int day = 3)
:_year(year)
, _month(month)
, _day(day)
, _ref(_year)
, _cint(_year)
{
//函数体内可以继续进行代码的书写
};
第一个是冒号
后面是逗号
这样我们的程序就可以通过了
其实初始化列表还解决了下面这个问题
3.没有默认构造函数的自定义成员变量
C++类和对象中(构造函数,析构函数,拷贝构造函数)详解
那么问题来了:
如果这个Stack类没有默认构造函数呢?
会发生编译错误
那么怎么办呢?
其实我们仔细想一想:
这个MyQueue类中的Stack类没有了默认构造函数,不就意味着这个Stack类在我们这个MyQueue类定义的时候必须初始化吗?
所以这个时候初始化列表就派上用场了
MyQueue(int capacity1, int capacity2,int size)
:_st1(capacity1)
,_st2(capacity2)
,_size(size)
{
};
这样就可以解决这个问题了
其实就算是你这个Stack类有默认构造函数,但是如果我就是想自己去传参调用你的这个默认构造函数.也是可以这样做的
如果没有初始化列表这一语法,我们在声明的时候去调用Stack类的传参构造函数呢?
会直接报错
4.初始化列表是成员变量定义的地方
那么初始化列表到底是什么身份呢?
这么强大
初始化列表是成员变量定义的地方
我们之前提到过:
也就是说只要你这个类有成员变量,就一定会走构造函数,只要你走构造函数,就一定会走构造函数当中的初始化列表
而成员变量就是在初始化列表中定义的,也就是在初始化列表中分配的空间!!
5.初始化列表可以跟函数体内定义搭配使用
那么既然初始化列表这么强大,可不可以不要函数体了呢?
构造函数只留一个初始化列表不就行了吗?
当然不可以
比如:下面的Stack类
Stack(int capacity = 1)
:_a((int*)malloc(sizeof(int)*capacity))
,_capacity(capacity)
,_top(0)
{};
你这不是也可以吗,这是好的情况
万一我申请空间太大,申请失败了呢?
wzs::Stack st(1000000000000000000);
我们在这里直接申请这么大的空间
那么该怎么办呢?
在初始化列表里面去检查吗?
初始化列表是定义成员变量给成员变量开辟空间的地方,
你在里面检查_a是否等于空指针
你这不是大材小用吗
直接让_a在函数体内初始化它不香吗?
也就是这样
Stack(long long capacity = 1)
:_capacity(capacity)
,_top(0)
{
_a = ((int*)malloc(sizeof(int) * capacity));
if (_a == nullptr)
{
perror("malloc fail");
exit(-1);
}
};
wzs::Stack st(1000000000000000000);
开辟空间失败:
开辟空间成功:
wzs::Stack st(10);
也就是说初始列表和构造函数的函数体是相辅相成的,
类似的关系:引用和指针
所以日常中:对于成员变量来说
大多数情况下我们都是用初始化列表搞定
剩下那些只能用函数体去初始化的就用函数体去搞定
6.初始化列表执行的顺序
成员变量在类中声明次序就是其在初始化列表中的初始化顺序,
与其在初始化列表中的先后次序无关
大家可以看一下这个题,这是《剑指offer》上的一道题目
答案:
n1:随机值
n2:0
因为成员变量在类中声明次序就是其在初始化列表中的初始化顺序,
所以n1先被初始化,然后n2才被初始化
也就是说
它们在初始化列表中的执行次序如下:
n1 = n2+2;
n2 = 0;
而n1和n2在还没有被初始化时都是随机值
因此n1就是随机值,而n2被初始化为了0
7.总结+建议
这个注意当中的第一条和这个初始化列表跟函数体内定义的区别这一条:
也就是说如果有人这么写:
也就是说初始化列表中,成员变量只能初始化一次,因此叫做初始化列表
而函数体中,成员变量可以赋值很多次,所以在函数体中只能被称为赋值初值,而不能被称为初始化!!!
也就是说对于这个构造函数来说
Date(int year, int month, int day)
: _ref(_year)
, _cint(_year)
{
_year=year;
_month=month;
_day=day;
};
private:
int _year;
int _month;
int _day=26;
int& _ref;
const int _cint;
本质上编译器是这样进行的:
Date(int year, int month, int day)
:_year(随机值)
,_month(随机值)
,_day(缺省值26)
,_ref(_year)
, _cint(_year)
{
//下面是重新对_year,_month,_day进行再次赋值
_year=year;
_month=month;
_day=day;
};
private:
int _year;
int _month;
int _day=26;
int& _ref;
const int _cint;
也就是说构造函数中:
初始化列表是成员变量定义的地方,每一个成员变量都需要走初始化列表
如果在初始化列表当中我们没有显式定义该成员变量,就会使用该成员变量声明时给的缺省值,如果没有缺省值,那么就会使用编译器给的随机值
函数体内定义其实本质上是给成员变量进行第二次赋值
也就是像这样
int var = 随机值; //初始化列表
var=1;//函数体内定义
其实翻来覆去就只有一句话:初始化列表是成员变量定义的地方
函数体内是成员变量可以进行二次赋值的地方
二.静态成员变量和静态成员函数
我们出现了一个需求:计算一个类实例化出了多少个对象
1.static成员变量的引出
其实方法很简单:只需要定义一个全局变量count=0,然后在这个类的所有的构造函数当中都让这个count++
最后count的数值就是该类实例化出的对象的个数
这样是可行的,但是并不好
为什么呢?
因为这个变量count是一个全局变量,也就是说这个count可以在程序的任意位置访问并修改
所以有可能会出现这种情况:
这个func函数中它成功地把我count这个全局变量给修改了
导致最后得出的答案少了1
也就是我们无法避免这种极端情况的发生
那怎么办呢?
而且把这个count定义为成员变量也是不行的
因为每一个对象都有自己的count,都是++的自己的count
那么有没有一种成员变量是为我这个类实例化出的所有的对象所共享的呢?
static成员变量就出现了
2.static成员变量的特性
既然我们了解了static成员变量的特性
3.对于解决以上需求: static成员变量的不足
那么我们该怎么解决上面那个需求呢?
但是还是有一个问题
我在这里是把_count这个静态成员变量的访问属性设置为公有了,但是也防不住会出现下面这种情况:
是,你是把_count这个全局变量放到了你A这个类的内部
但是你把它的访问属性设置为了公有,
因此我func想改你这个_count,我只需要加上你A这个类域,我依然能改,你拦不住我
那怎么办呢?
把_count放到私有属性下:
3.匿名对象
是,我func现在是改不了了,但是你main函数也访问不了啊
怎么办呢?
1.封装get函数
不错,而且我func函数也无法修改你这个count了
但是你这样的前提是你这个main函数里面有一个A的对象啊
这样你才能用对象.去访问这个get函数啊
可是如果我原本就没有在这个main函数里面创建一个A类型的对象
那你就只能这样了
可是你说我为了能够得到这个类到底实例化出了多少个对象,还要去特意在我main函数里面特意实例化出一个对象,
太挫了吧
而且我还要考虑取名字的事情,而且我创建这个对象只需要让它完成者一个任务即可,我后续也不想用它,能不能让它在执行完这个任务之后就销毁呢?
这就可以用到匿名对象了
匿名对象就是A()
它的特点是:
1.不用取名字
2.它的生命周期只有
cout << wzs::A().GetCount()-1 << endl;
这一行
完美符合了我刚才的需求
请注意:但是这个-1有点碍眼,让我这个代码不是很帅
显得我这个人的水平很挫
怎么办呢?
可是想要访问这个get函数就是必须创建一个对象
然后用对象.才能访问啊
能不能不用对象呢?
于是static成员函数出现了
4.static成员函数
这样就能把-1这个影响我代码美观性地东西就消失了
到了这里我们这个需求就完美解决了
5.总结
到了这里,大家就能对下面这张图片有更深刻的理解了
6.OJ 计算1+2+…+n
下面我们来做一道OJ题来巩固一下上面的知识
求1+2+3+…+n
这么多限制条件,怎么办呢?
其实这个题的本意就是让我们利用类的静态成员来解决这个问题
注意:牛客网的编程题所采用的编译器是支持变长数组的,VS编译器是不支持变长数组的
但是这个代码还不是特别好,当我们介绍完内部类之后,我们还会对这个代码进行进一步修改
三.explicit关键字
1.一个奇怪的现象
wzs::B b2 = 1;
竟然能这么创建一个对象,这是怎么做到的呢?
其实:
我们之前在C++入门-引用中介绍过:
因此我们可以认为是这样进行的
那么下面的问题就是
为什么1这个内置类型能隐式类型转换为我B这个类类型呢?
其实这里有一个规则:
当某个类的构造函数可以只传入一个参数时,而且这个参数的类型跟我这个内置类型相同的时候
就可以发生这个类类型对象和这个内置类型变量之间的隐式类型转换
下面这个例子能帮大家更好地去理解
class A
{
public:
构造函数第1种情况:
A(int val) //单参数构造函数
:_var1(val)
{}
A(int val1,int val2 = 2,int val3 = 3)//半缺省,且只传一个参数即可完成对象的构造
:_var1(val1)
,_var2(val2)
,_var3(val3)
{}
A(int val1 = 1,int val2 = 2,int val3 = 3)//全缺省,这个构造函数允许只传一个参数进行构造
:_var1(val1)
,_var2(val2)
,_var3(val3)
{}
private:
int _var1;
int _var2;
int _var3;
};
它们都允许: A a = 1;
但是当这个内置类型跟我这个参数的类型不匹配时:
例如 A a = nullptr; 这样就无法发生隐式类型转换
而且还有一种书写方式:列表初始化
zs::A a={2023,11,3};
这个本质也跟上面那个内置类型1隐式转换为A类类型的对象一样:
可是有一个问题啊
你跟我讲这么一大堆,有什么用呢?
2.用途
我们这里先用一下STL中的vector容器
当我们在leetcode做OJ题的时候:
#include <vector>
int main()
{
vector<zs::A> v;
//当我们在leetcode做OJ题时:
//1.在没有学习这个知识之前我们平常的做法
zs::A a(1);
v.push_back(a);
//2.学习了匿名对象后
v.push_back(zs::A(1));// 这代码写起来爽了很多
//3.学习了内置类型和自定义类型之间的隐式转换后
v.push_back(1);// 这代码写起来太爽了,可是只有在构造函数允许只传一个参数的时候才可以啊
//4.学习了列表初始化隐式类型转换为类类型对象后
v.push_back({ 2023,11,3 });// 爽飞了 ,而且允许只传一个参数对我无效
return 0;
}
可见这个知识是很棒的
3.为什么要有explicit呢?
那么你介绍的这个explicit是什么呢?
他有什么用呢?
四.友元
1.友元函数
我们之前在介绍运算符重载的时候提到过友元函数
当时是为了解决日期类的流插入和流提取运算符不能定义在类内,但是还想要访问这个类的成员变量的矛盾
详细的请况大家可以去看这篇博客:
C++类和对象中:运算符重载+const成员函数+日期类的完善
2.友元类
1.语法:
这里以C类是B类的友元为例
class B
{
friend class C;//友元声明不受类访问限定符的限制
}
class C
{
B b;//需要有一个B类的对象
//然后想要访问B类的成员变量或者成员函数的时候就可以用这个B类的对象.去访问
}
2.实例:
namespace zs
{
class B
{
public:
friend class C;
private:
void FuncOfB()
{
cout << "private: FuncOfB()调用" << endl;
}
int _bint = 1;
static int _StaticInt;
};
int B::_StaticInt = 5;
class C
{
public:
void SetMemberOfB(int val)
{
b._bint = val;
B::_StaticInt = val;
}
void ReadMemberOfB()
{
cout << b._bint << endl;
cout << B::_StaticInt << endl;//只能这样访问
}
void ReadFuncOfB()
{
b.FuncOfB();
}
private:
int _cint;
B b;
};
}
int main()
{
zs::C c;
c.SetMemberOfB(100);
c.ReadMemberOfB();
c.ReadFuncOfB();
return 0;
}
3.总结
关于友元类的访问方法其实大家只需要记住一点:
友元类:我是你的朋友,
我也只是能够访问你的私有成员而已
访问方法跟普通类访问你的公有成员的方法一样
注意:友元关系是单向的,不具有交换性
因此上面的类B就无法访问类C的私有成员
3.内部类
1.语法
2.实例
namespace zs
{
//注意:D是外部类,E是内部类.E天生就是D的友元,但是默认情况下D是不能访问E的,除非在E中声明D是E的友元类
class D
{
public:
class E
{
public:
void GetStaticMemberOfD()
{
cout << _StaticInt << endl;
//yes 这是上述第3条特性: 内部类可以直接访问外部类的static成员,不需要外部类的对象/类名
//cout << _NonStaticInt << endl;//err 内部类不能直接访问外部类的非static成员
cout << d._NonStaticInt << endl;//yes 只能用对象.去访问
//也就是说内部类访问外部类:只不过是静态成员可以直接访问而已,非静态成员的访问跟普通类访问外部类的非静态成员的方法一样
}
private:
zs::D d;
};
private:
static int _StaticInt;
int _NonStaticInt = 10;
};
int D::_StaticInt = 1;
}
3.总结:
其实C++不常用内部类
OJ题的优化
学习了内部类之后,我们就能对那道OJ题进行优化
以上就是类和对象下的全部内容,希望能对大家有所帮助!!!