本期我们继续学习类与对象,没有看过上和中的小伙伴建议先看前两期内容
(2条消息) C++类与对象—上_KLZUQ的博客-CSDN博客
(2条消息) C++类与对象—中_KLZUQ的博客-CSDN博客
目录
1.再谈构造函数
1.1构造函数体赋值
1.2初始化列表
1.3 explicit关键字
2. static成员
2.1 概念
2.2 特性
3.友元
3.1友元函数
3.2友元类
4.内部类
5.匿名对象
7.再次理解类和对象
1.再谈构造函数
1.1构造函数体赋值
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
1.2初始化列表
class Date
{
public:
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
为什么需要初始化列表呢?因为在有些情况下,只能使用初始化列表来解决问题
【注意】1. 每个成员变量在初始化列表中最多 只能出现一次 ( 初始化只能初始化一次 )2. 类中包含以下成员,必须放在初始化列表位置进行初始化:引用成员变量const 成员变量自定义类型成员 ( 且该类没有默认构造函数时 )
我们用下面的代码来举例
class A
{
public:
A(int a)
:_a(a)
{}
private:
int _a;
};
class B
{
public:
B(int a, int ref)
:_aobj(a)
, _ref(ref)
, _n(10)
{}
private:
A _aobj; // 没有默认构造函数
int& _ref; // 引用
const int _n; // const
};
我们一个一个来看
先看const成员,const成员不能在函数体内赋值
不仅是const,引用也是一样的
原因是const和引用的特征是必须在定义时初始化
引用是别名,而const也只有一次初始化的机会
我们创建对象时是对象的整体的定义,而对象的成员也需要一个地方定义,所以就有了初始化列表,选择初始化列表作为对象成员定义的地方,而我们之前在函数体里写的是初始化不叫初始化,而叫做赋值
而且我们上面也说了,初始化最多只能初始化一次
这里就初始化了两次,所以报错了
除了引用和const,自定义成员也必须在初始化列表初始化
我们先在A类里加一条输出语句,同时给a一个值
如果B的成员有A类,即使我们不在初始化列表去初始化A,他也会自动去调用进行初始化
而如果是内置类型,比如int等等,就不做处理
如果我们这里写了缺省值的话,就会用缺省值进行初始化
如果在初始化列表显示的写了,就不会用缺省值(之前说的那个C++11打的缺省值补丁就是给初始化列表打的)
我们把A类的缺省值去掉,然后这时编译会报错
此时我们可以在B里这样调用默认构造
我们运行也没有问题,当然A有默认构造时我们也可以这样调用
我们之前用两个栈实现了队列
我们这里有两个构造函数,上面的第一个构造函数,我们什么也没写,但是我们创建MyQueue时也会进行初始化,原因是这两个栈会在初始化列表里进行初始化,就像我们上边的A类一样
我们再回过头来看我们的类B和类A
所以当A这里没有默认构造时,我们这里不写就会报错
我们的代码还有问题,引用也是错误的
要这样写才行,刚才的写法出了作用域就销毁了,这样才是正确的
知道了上面的内容,可能就有同学迷惑了,既然有初始化列表,那为什么还需要函数体赋值呢?我们来看几个例子就知道了,他们各有各的用途
比如我们的栈,我们甚至可以直接用初始化列表来完成,但是,我们在malloc空间后,我们还需要进行检查的,检查就只能写在函数体里,所以没有函数体是不行的
再比如,我们要求还要把数组也初始化
这些操作,没有函数体都是不行的
所以初始化列表和函数体都有存在的价值
尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化
我们再看下一个内容
成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
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();
}
大家想想这段代码运行后是什么?
答案是D,原因就是上面说的,成员变量在类中声明次序就是其在初始化列表中的初始化顺序
即我们声明时的顺序先是_a2,然后是_a1,所以初始时会先初始化_a2,然后才是_a1
这点是非常重要的,因为如果不注意的话,有人就可能会写出下面这种代码
我们声明时先声明了_a,然后才是_capacity,但是不知道上面知识的同学就可能会认为在初始化列表里先初始化的_capacity,然后在_a中使用没有问题,但这是错误的,因为代码会先初始化_a,此时_capacity是随机值,就会出现问题
所以我们建议声明顺序和定义顺序保持一致,这样就不会出现问题
这里的size也会走初始化列表,但是会是随机值, 我们可以给缺省值,这样在初始化列表时就会用缺省值了
当然,我们还可以用函数体内赋值,大家根据场景选择即可
1.3 explicit关键字
我们知道,这段代码的aa1会调用a的构造函数,那aa2呢?
aa2这里是隐式类型转换,是整形转换为自定义类型
我们知道,在我们把x赋值给y时,会先产生临时变量,然后把临时变量赋值给y
上面的aa2也是一样的,数字2会先去调用构造函数,生成一个A类型的临时对象,然后再去调用拷贝构造,才有了aa2
不过这样做太繁琐了,老式的编译器可能还是这样,新的编译器都会优化为直接构造
在同一个表达式里,连续的构造编译器基本都会优化,为了提高效率
这样是否我们就没办法证明我们上面说的呢?我们来看这个例子
aa3可以引用aa2,但是不能引用2,这些大家都可以理解
但是我们给aa3加上const就可以引用2了
如果我们运行的话还会调用一次构造
因为临时对象具有常性,所以不能引用,但const引用可以
这个知识是在为我们的后续做铺垫,我们来看下面的例子(这里大家了解即可,我们后续会讲)
这两行代码的结果是一样的,但name1是构造,而name2是构造加拷贝构造加优化
上面的push就是老老实实的构造,然后传值,下面的就是隐式类型转换
如果没有隐式类型转换,我们只能老老实实的用第一种push,写起来就不如下面的方便
知道了什么是隐式类型转换,我们来看下面的内容
如果我们不想让转换发生,我们就可以加上一个关键字,explicit
不能转换时下面就报错了,我们后续会学习智能指针,就不希望发生转换,需要使用到这个关键字
2. static成员
2.1 概念
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。静态成员变量一定要在类外进行初始化
我们创建一个全局变量,构造和拷贝构造++,析构--
但是这段代码有一个问题,全局变量我们是可以随意修改的,有点危险
所以我们就需要把这个变量封装一下,放到类里面
成员变量和静态成员变量的区别是什么?
成员变量是属于每一个类对象的,而静态成员变量是属于类,属于类的每个对象共享,存储在静态区,生命周期是全局的
我们上面的静态成员 _scount只是声明
他不能在初始化列表初始化,因为初始化列表是对象的成员定义的地方,而静态成员不是属于某个对象的,所以他的定义要在类的外部
因为是私有的,所以我们不能直接使用
如果写成公有的就可以,不过写成公有的就和全局的没什么区别了
我们可以通过公有的成员函数进行访问
静态成员,只要突破类域和访问限定符就能访问
不过更好的方式是使用静态成员函数,静态成员函数的特点是没有this指针,指定类域和访问限定符就可以访问,所以一般静态成员变量和静态成员函数是配套出现
这样都是可以的
静态成员函数不能访问非静态成员变量,因为他没有this指针
我们这样做相比全局变量的好处就是无法随意修改
这里是声明,不能给缺省值,因为静态成员变量不能走初始化列表,我们只能在类外部定义
下面我们来看一道神奇的例题
看题目非常简单,从1加到n即可,但是我们看条件,不能用乘除法,循环条件语句等等都不能使用,那该怎么办呢?我们看下面的解法
class Sum{
public:
Sum(){
_ret += _i;
++_i;
}
static int GetRet(){
return _ret;
}
private:
static int _i;
static int _ret;
};
int Sum::_i=1;
int Sum::_ret=0;
class Solution {
public:
int Sum_Solution(int n) {
Sum a[n];
return Sum::GetRet();
}
};
该解法非常巧妙,调用n次构造函数,用静态成员变量解决问题
我们继续看我们上面的代码
Func是可以调用GetACount的,即非静态可以调用静态
静态不能调用非静态,因为非静态成员函数要传递this指针
非静态函数内部是可能会进行修改成员变量的
我们再看一个奇怪的例子
设计一个类,只能在栈上创建对象(或者堆上)
我们创建的变量,对象,一般都是在栈上面的,加static就在静态区
new我们之后会讲,现在大家只要知道这里的对象在堆上即可,我们该如何设计呢?
我们分析一下,这三个对象,都需要调用构造函数
所以我们可以将构造函数私有化,这样都不能调用了
然后我们提供一个公有的方法(这里new返回的是一个指针 )
此时就会出现一个问题,我们调用函数需要对象,而我们创建对象又需要调用函数
所以这里我们就需要静态
这里用静态的很好,完美解决了问题
我们来总结一下静态成员的特性
2.2 特性
1. 静态成员 为 所有类对象所共享 ,不属于某个具体的对象,存放在静态区2. 静态成员变量 必须在 类外定义 ,定义时不添加 static 关键字,类中只是声明3. 类静态成员即可用 类名 :: 静态成员 或者 对象 . 静态成员 来访问4. 静态成员函数 没有 隐藏的 this 指针 ,不能访问任何非静态成员5. 静态成员也是类的成员,受 public 、 protected 、 private 访问限定符的限制
3.友元
友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。友元分为: 友元函数 和 友元类
3.1友元函数
我们之前重载<<时,使用了友元,但是友元一般是不推荐使用的,因为友元就像特例一样,让你和别人不一样,这是不好的
class Date
{
friend ostream& operator<<(ostream& _cout, const Date& d);
friend istream& operator>>(istream& _cin, Date& d);
public:
Date(int year = 1900, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& _cout, const Date& d)
{
_cout << d._year << "-" << d._month << "-" << d._day;
return _cout;
}
istream& operator>>(istream& _cin, Date& d)
{
_cin >> d._year;
_cin >> d._month;
_cin >> d._day;
return _cin;
}
int main()
{
Date d;
cin >> d;
cout << d << endl;
return 0;
}
我们可以使用get和set方法来解决友元的问题
而且使用友元函数还有别的问题,比如我们想把类里的成员变量_year改名为year,那么友元函数里也得进行改名
我们来总结一下友元函数的特点
友元函数 可访问类的私有和保护成员,但 不是类的成员函数友元函数 不能用 const 修饰友元函数 可以在类定义的任何地方声明, 不受类访问限定符限制一个函数可以是多个类的友元函数友元函数的调用与普通函数的调用原理相同
除了友元函数,还有友元类,我们来看看什么是友元类
3.2友元类
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。友元关系是单向的,不具有交换性。比如上述 Time 类和 Date 类,在 Time 类中声明 Date 类为其友元类,那么可以在 Date 类中直接访问 Time类的私有成员变量,但想在 Time 类中访问 Date 类中私有的成员变量则不行。友元关系不能传递如果 B 是 A 的友元, C 是 B 的友元,则不能说明 C 时 A 的友元。友元关系不能继承,在继承位置再给大家详细介绍。
友元函数是函数成为你的朋友,友元类就是这个类成为你的朋友,这个类可以随意访问你的私有和保护
class Time
{
friend class Date; // 声明日期类为时间类的友元类,则在日期类中就直接访问Time类中的私有成员变量
public:
Time(int hour = 0, int minute = 0, int second = 0)
: _hour(hour)
, _minute(minute)
, _second(second)
{}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
void SetTimeOfDate(int hour, int minute, int second)
{
// 直接访问时间类私有的成员变量
_t._hour = hour;
_t._minute = minute;
_t._second = second;
}
private:
int _year;
int _month;
int _day;
Time _t;
};
现在我们把友元的声明屏蔽掉
这里就会报错
有人可能会纠结声明应该放在哪里,放在哪里都行,访问限定符只限制成员,成员是成员变量和成员函数,而友元声明只是声明,不受限制
4.内部类
概念: 如果一个类定义在另一个类的内部,这个内部类就叫做内部类 。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。注意: 内部类就是外部类的友元类 ,参见友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。特性:1. 内部类可以定义在外部类的 public 、 protected 、 private 都是可以的。2. 注意内部类可以直接访问外部类中的 static 成员,不需要外部类的对象 / 类名。3. sizeof( 外部类 )= 外部类,和内部类没有任何关系。
我们一般定义类是在全局定义,而内部类就是定义在类的里面
class A
{
private:
static int k;
int h;
public:
class B // B天生就是A的友元
{
public:
void foo(const A& a)
{
}
};
};
int A::k = 1;
int main()
{
cout << sizeof(A) << endl;
return 0;
}
大家先想想这段代码的计算结果是多少?
答案是4
我们先看类A,他的成员变量有一个k,是static的,我们计算时是不需要计算k的,因为k没有存到对象里面
B对象的大小也不需要计算,因为A里面也没有B对象
除非我们定义一个B对象,才需要计算
此时的A和B其实是没有多大关系的,我们可以认为A就是一个大房子的图纸,B是买房子时房子里的赠品
我们不买房子是没有赠品的
当我们买了房子后,就可以得到赠品
如果我们把B设为私有,也是会受访问限定符的限制的
另外,内部类还是外部类的友元
内部类可以随意访问外部类的成员
C++其实是不太喜欢使用内部类的,不过某些场景使用还是不错的
我们再看这道题
class Solution {
class Sum {
public:
Sum() {
_ret += _i;
++_i;
}
};
public:
int Sum_Solution(int n) {
Sum a[n];
return _ret;
}
private:
static int _i;
static int _ret;
};
int Solution::_i = 1;
int Solution::_ret = 0;
我们使用内部类修改代码如上,我们把sum类放到Solution类里,这样sum类就是Solution的友元,所以sum类可以随意访问Solution的成员,同时我们把_i和_ret改为Solution的成员,这样我们也不需要get函数,直接就可以返回_ret
内部类的优势在于我们在外部类定义的东西,我们可以在外部类使用,也可以在内部类使用
5.匿名对象
我们正常情况下是这样定义对象的
我们还可以使用这样的方式来定义对象
上面的叫做有名对象,下面的叫做匿名对象
匿名对象连名字都没有,我们该如何使用它呢?
我们调用有名对象是先创建,再调用,写了两行代码
我们使用匿名对象的话就只需要一行
我们只调用一次的话就使用匿名对象,想调用多次就用有名的
匿名对象还有一个特点,就是即用即销毁,我们看A的析构函数打印的位置,在Sum_Solution前也有打印
匿名对象的生命周期在当前行,而有名对象的生命周期在当前函数局部域
匿名对象和临时对象类似,具有常性, 不能引用
所以加上const可以引用
加上const引用后,匿名对象的生命周期就会被延长,变为和引用的生命周期相同
我们之后可能会写一个push_back,如果用有名对象调用就是这样的,这样写了两行代码
使用匿名对象就可以一行解决
更简单一点就是用隐式类型转换(隐式类型转换会产生临时对象,const引用引用的是临时对象)
我们一般会用第三种写法来写,这些知识要结合起来
另外,删除const下面的代码就编不过了,所以使用引用最好加上const,这里就体现了const引用的作用
我们继续往下看
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a = 0)" << endl;
}
A(const A& aa)
:_a(aa._a)
{
cout<< "A(const A& aa)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
A& operator=(const A& aa)
{
cout << "A& operator=(const A& aa)" << endl;
if (this != &aa) {
_a = aa._a;
}
return *this;
}
private:
int _a;
};
这里有一个类A
我们有一个传值传参的Func1,传值传参要调用拷贝构造
不想调用拷贝构造我们就使用引用,引用最好加上const
另外,这两个函数构成重载,加上const,类型就不同了,但是这里存在调用歧义
我们写了一个Func3,传值返回,它会调用构造,拷贝构造,返回的是aa的拷贝构造
而我们使用Func4的写法就不会有拷贝
Func5返回的是拷贝的对象,但是我们不能用引用接收
但是加上const就可以了
如果我们这样写,aa返回时会先拷贝构造临时变量,然后临时变量会拷贝构造ra
不过我们运行的话这里是一次,原因是这是编译器的优化,如果是老的编译器就是两次拷贝
在同一行的表达式里连续的构造和拷贝构造,编译器会进行优化,会合二为一甚至合三为一
就像这样的优化,构造+拷贝构造就优化成了构造
如果我们是分开写的,先创建对象,再调用,是不会优化的
而一行内就会优化,这里也是构造+拷贝构造就优化成了构造
这里也是同理
所以我们传参的时候最好按照这些优化的方法写
这样的差别就非常大了
上面的写法是一个构造加一个拷贝构造,下面是两个构造加一个拷贝构造加一个赋值
所以我们以后能写在一个步骤就写在一个步骤里,这样可以提高效率
7.再次理解类和对象
现实生活中的实体计算机并不认识,计算机只认识二进制格式的数据。如果想要让计算机认识现实生活中的实体,用户必须通过某种面向对象的语言,对实体进行描述,然后通过编写程序,创建对象后计算机才可以认识。比如想要让计算机认识洗衣机,就需要:1. 用户先要对现实中洗衣机实体进行抽象 --- 即在人为思想层面对洗衣机进行认识,洗衣机有什么属性,有那些功能,即对洗衣机进行抽象认知的一个过程2. 经过 1 之后,在人的头脑中已经对洗衣机有了一个清醒的认识,只不过此时计算机还不清楚,想要让计算机识别人想象中的洗衣机,就需要人通过某种面相对象的语言( 比如: C++ 、 Java 、 Python 等 ) 将洗衣机用类来进行描述,并输入到计算机中3. 经过 2 之后,在计算机中就有了一个洗衣机类,但是洗衣机类只是站在计算机的角度对洗衣机对象进行描述的,通过洗衣机类,可以实例化出一个个具体的洗衣机对象,此时计算机才能洗衣机是什么东西。4. 用户就可以借助计算机中洗衣机对象,来模拟现实中的洗衣机实体了。在类和对象阶段,大家一定要体会到, 类是对某一类实体 ( 对象 ) 来进行描述的,描述该对象具有那些属性,那些方法,描述完成后就形成了一种新的自定义类型,才用该自定义类型就可以实例化具体的对象 。
以上即为本期全部内容,希望大家可以有所收获
如有错误,还请指正