目录
🌞0.前言
言C++之言,聊C++之识,以C++会友,共向远方。各位博友的各位你们好啊,这里是持续分享C++知识的小赵同学,今天要分享的C++知识是C++类与对象,在这一章,小赵将会向大家继续聊聊C++类与对象。✊
🚈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;
};
虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对对象中成员变量的初始化, 构造函数体中的语句只能将其称为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值。
这里其实还是蛮好理的,比如我这样改改就可以二次赋值
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
_year=2024;//二次赋值
}
那么什么才是真正的初始化呢?其实我们学过C++11的一种方式。
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year=2024;//初始化
int _month=9;
int _day=3;
};
而这个操作其实是在我们的C++98的初始化列表版本中改出来的。那什么是初始化列表呢?
🚝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成员变量//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
};
尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。
对上面的内容做个简单的总结,我们的列表初始化主要是对于每个成员变量都只构造一次,同时列表初始化的构造其实是在我们的构造函数{}的内容之前的。
这里小赵也是写了一段代码对这个进行验证,当然大家如果想看得更仔细的化可以用我们的调试功能去看内部情况。
class A
{
public:
A()
:a(2)
{
cout << "a=="<<a << endl;//验证a是否已经被初始化
a = 100;
}
private:
int a;
};
int main()
{
A c;
}
成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
在这里小赵录了一个视频去一下这里面的情况
相信大家在视频可以清楚地看见,我们定义在前面的变量在初始化列表里面是先走,而后面的则是后走的,大家也可以自己去试试。
理解了上面的意思,大家就可以发现下面这个代码的问题出在哪里了。
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();
}
这里出现的主要问题就是我们_a2要比_a1先初始化,所以这里不能用_a1去初始化_a2(因为_a)还没有被初始化。
那么这里的输出结果就是这样,_a1会给一个随机值。
那么这里还有一个地方是极其危险的就是指针:
class A
{
public:
A(int a)
:_a1((int*)malloc(sizeof(int)))
, _a2(_a1)
{}
void Print()
{
if (_a2 == nullptr)
{
cout << 1 << endl;
}
cout << _a1 << " " << _a2 << endl;
}
private:
int* _a2;
int* _a1;
};
int main()
{
A aa(1);
aa.Print();
}
这里小赵用了几个编译器去测试这里的代码首先是VS
VS的底层是直接把指针初始化成了空指针。
然后是DEVC++ 这里也是进行了初始化
这里好像初始化的不是特别明显
但是这个地址的设计感觉还是有特别的处理的。
然后是g++下面
g++下面感觉是对的,就是一个野指针去初始化指针,而野指针去初始化指针我感觉还是相当危险的,所以这里我还是不太建议大家取用未初始的变量去初始化另一个变量的,所以这里大家还是要小心。
🚝1.3 explicit关键字
在讲解这个关键字之前我们先要看到一个东西的产生。就是我们的祖师爷在设计C++的时候加入了我们的隐式类型转换。这个具体咋用呢?
class A
{
public:
A(int a)
:_a1(a)
{}
void Print()
{
cout << _a1 << endl;
}
private:
int _a1;
};
int main()
{
A aa = 1;//隐式类型转换
aa.Print();
}
我们这种隐式类型的转换的操作就是把int型的1转化成我们的A类型
那么其实 这里也是一个底层的转化就是编译把我们隐式类型转化的那个操作换成了这个:
A aa(1);
那么其实这里我们讲的隐式类型转换一般针对就是这种一个参数的转换(后面会说如何多个参数进行隐式类型转换)。而我们的 explicit正是为了这种情况出现的。
explicit主要针对的是我们的构造函数,只能用来修饰构造函数,去静止我们这种隐式类型转换的发生。
那么我们接下来再想一想哪些情况会有这种单个参数可以进行隐式类型转换的
构造函数不仅可以构造与初始化对象,对于接收单个参数的构造函数,还具有类型转换的作用。接收单个参 数的构造函数具体表现:
1. 构造函数只有一个参数
2. 构造函数有多个参数,除第一个参数没有默认值外,其余参数都有默认值
3. 全缺省构造函数
🚈2. Static成员
🚝2.1 概念
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的 成员函数,称之为静态成员函数。静态成员变量一定要在类外进行初始化
class A
{
public:
A(int a)
:_a1(a)
{}
void Print()
{
cout <<"b="<< b <<" " <<"_a1=" << _a1 << endl;
b++;//b进行++操作
cout << "b=" << b << endl;
}
private:
int _a1;
static int b;//定义静态成员b
};
int A::b = 2024;//对静态成员变量进行初始化
int main()
{
A c(2024);
c.Print();
}
这里有好多的东西需要一步步来,我们先来第一个问题
1.静态成员变量什么时候完成的初始化?
这个问题小赵也思考了很久,用调试最开始也没有得到自己想要的结果,但我们的调试结果发现它似乎在我们的对象创建前就完成了初始化。
为了帮助大家更好的看见这个,小赵设置了下面这个代码
class A
{
public:
A(int a)
:_a1(a)
{}
void PrintB()
{
cout << "b=" << b<<endl;
}
void Print()
{
cout <<"b="<< b <<" " <<"_a1=" << _a1 << endl;
b++;//b进行++操作
cout << "b=" << b << endl;
}
static int b;//定义静态成员b
private:
int _a1;
};
int A::b = 2024;//对静态成员变量进行初始化
int main()
{
cout << A::b<< endl;
A c(2024);
/* c.Print(); */
}
在这里小赵有好几个猜测 ,小赵本来以为这里应该是在编译时候完成的,但其实不是(这里小赵是通过查资料出的结果,直接看编译结果,小赵还不太会)。然后小赵将资料做了下面的总结
静态成员变量的初始化通常是在程序启动时进行的,而不是在编译时。具体来说,静态成员变量的初始化过程如下:
编译阶段:编译器会为静态成员变量分配内存,并在符号表中记录其信息,但此时并不会进行实际的初始化。
链接阶段:在链接阶段,编译器会将所有的静态成员变量的定义和引用进行连接。
运行时初始化:当程序启动时,静态成员变量会被初始化。这个过程是在程序的主函数(
main
)执行之前完成的。静态成员变量的初始化顺序是根据它们在代码中出现的顺序来决定的。因此,静态成员变量的初始化是在运行时进行的,而不是在编译时。
好了通过这个总结相信我们大家应该都能明白了,其实是这里的静态成员变量是在main函数之前就已经完成初始化的,在编译和链接之后。
下面是第二个问题,静态成员变量存在哪里?
这里小赵可以说下,存在的是静态区(也是在资料中获得)。它和我们的类里面的函数有点像但又不一样,我们之前说我们类里面的函数是不存在对象里面的,是存在公共代码区,每个对象都能调用它但不能改它,而我们的这里则是每个对象都能调用它也都能改它。
这个有点像那种好多个指针指向一块地址的感觉,当然我觉得这个有点不准确。这里更准确的是对标我们的全局变量,可以在函数中使用,改变值。
好了说了这么多下面我们来验证一下它。
class A
{
public:
void test()
{
a++;
}
void Printa()
{
cout << a << endl;
}
private:
static int a;
};
int A::a = 100;
int main()
{
A b;
A d;
b.test();//对b中的a++
cout << "d中的a:";
d.Printa();
d.test();//对d中的a++
cout << "b中的a:";
b.Printa();
}
通过这个代码我们就能发现所有的对象(属于一个类的)其实是公用静态变量,对任何一个对象的静态变量进行改变都会影响到其他对象。
接下来为了帮大家更好的巩固这块的知识,我们来看一道颇具难度的面试题,非常的有意思
面试题:我们如何确定一个类创建了多少个类对象?
这里的方法当然用的就是我们的静态变量了。
class A
{
public:
A()
{
c++;
}
void Created()
{
cout <<"创建了" << c <<"个类对象" << endl;
}
private:
static int c;
};
int A::c = 0;
int main()
{
A c;
A d;
A m;
A S;
S.Created();
}
这里我们可以把我们的函数Created也弄成静态成员函数
这样我们就可以这样玩了。(这里要注意我们类里面的非静态成员函数是不能直接A::这样调用,因为它是创建在类对象里面,需要指定对象传入this指针才能调用。同时我们的静态成员函数里面只能调用静态成员变量,非静态成员变量也是不允许的(这点原因和上面有点像大家可以自己想一下,就不多说了。))
理解了上面的东西下面我们来看一道题目,这个程序的运行结果是什么?
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;
void TestA()
{
cout << A::GetACount() << endl;
A a1, a2;
A a3(a1);
cout << A::GetACount() << endl;
}
这里我们可以自己好好的分析一下,里面设计到我们的拷贝构造等等上一章节的内容,大家可以想一想。
结果如下
🚝2.2 特性
有了上面的一系列的知识,我们就可以对我们的静态变量有一个总结了
1. 静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区
2. 静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明
3. 类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问(公有才行)
4. 静态成员函数没有隐藏的this指针,不能访问任何非静态成员
5. 静态成员也是类的成员,受public、protected、private 访问限定符的限制
6.静态成员函数不可以调用非静态成员函数
7.非静态成员函数可以调用类的静态成员函数
这里要简单说一下6,7,其实6还是蛮好理解的我们说非静态成员必须要this指针,而静态成员函数根本就没有this指针(大家共有,更本不可能写谁家的名字),那就是没法调用。而反过来,我有this指针的函数可以调用没有this指针的。
(这个感觉好像公家和私家,公有私有啊)
🚈3. 友元
🚝3.1.输入输出流重载
再开始我们友员函数前面,我们先要补充一个东西这个东西也是前面漏说的就是输入流和输出流的重载为什么要重载这两个函数呢?
因为我们用内置类型成员的时候,可以直接用cout<<输出,那么能不能让我们的自定义类型也能做到这一点呢?而且这样的调用既可以加强代码的可读性,又加强了封装。(这里主要说加强封装其实就是如果使用者不知道里面有什么变量也可以方便打印出来。)
如date类:
要努力达到这样的效果
而要想达到这样的效果,我们就不得不看cout的底层,而这部分知识对我们当下来说还是有点难度,所以先给大家看一下该如何去实现这样的效果;(输入流和输出流我们会在最会聊,因为作者目前对这部分的知识也不够自信,而且涉及的内容太多了。)
而我们这里的重载方式和这个是很像的:
ostream& operator<<(ostream& _cout,Date &d)//ostream可以理解为流对象,这里我们的第一个参数必须是ostream类型的对象
{
cout << "year:" << d._year << ", month:" << d._month << ",day:" << d._day;
return _cout;
}
就是在类的外面去重载它,那为什么不能在类的里面去重载它呢?
也就是这样
class Date
{
public:
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{}
ostream& operator<<(ostream& _cout)
{
cout << "year:" << _year << ", month:" << _month << ",day:" << _day;
return _cout;
}
private:
int _year;
int _month;
int _day;
};
其实这里的问题就是,如果这样写它的第一个对象就不是我们的ostream类型的对象了而是,我们的date类型的this指针。而如果这样调用的时候就要这样
就达不到我们想要的目的了,那也就是说我们这里必须类外定义,但是如果定义在外面我们又用不了类的私有成员那这个究竟该如何解决呢?
🚝3.2友员函数
概念和提醒:友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。 友元分为:友元函数和友元类
友员是什么意思呢?其实就是字面意思,好朋友。而且这个好朋友是极其的好,因为我们的里面一但给了一个函数认证它是友员函数那么它将可以访问我们的私有成员变量。(就好比你和你的朋友之间没有密码,我感觉这大概就是我们说的知己。)
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字。
class Date
{
public:
friend ostream& operator<<(ostream& cout, Date& d);
private:
int _year = 2024;
int _month = 9;
int _day = 4;
};
ostream& operator<<(ostream& cout, Date& d)
{
cout << "year:" << d._year << ", month:" << d._month << ",day:" << d._day;
return cout;
}
int main()
{
Date d;
cout<<d;
}
那这里我们的问题就可以轻松解决了,相信这里我们对输入流(ostream)和输出流(istream)会更加的好奇,但是这份好奇还请留着我们后面会说,这里大家也可以试着去实现这里的输入流,和输出流差不多。(其实istream和ostream都是两个封装好的类,至于为什么要返回ostream主要原因还是去实现多次输入cin>>date>>date)。
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;
}
说明:
友元函数可访问类的私有和保护成员,但不是类的成员函数
友元函数不能用const修饰
友元函数可以在类定义的任何地方声明,不受类访问限定符限制
一个函数可以是多个类的友元函数 友元函数的调用与普通函数的调用原理相同
🚝3.2 友元类
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
- 友元关系是单向的,不具有交换性。(意思就是我认定你是我的朋友你可以随便动我的东西,但不代表我可以随便动你的东西)。
- 友元关系不能传递(知己不能随便传递)
- 如果B是A的友元,C是B的友元,则不能说明C时A的友元。
- 友元关系不能继承(这个后面会说)
例子
class Time
{
friend class Date; // 声明日期类为时间类的友元类,则在日期类中就直接访问Time类中的私有成
员变量
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
{
public:
class B // B天生就是A的友元
{
public:
void foo(const A& a)
{
cout << k << endl;//OK
cout << a.h << endl;//OK
}
private:
int d=6;
};
private:
static int k;
int h=5;
};
int A::k = 1;
int main()
{
A a;
A::B b;
b.foo(a);
cout<<sizeof(A) << endl;
cout<<sizeof(A::B)<<endl;
return 0;
}
外部类对象内部没有内部类,两者是相互独立的,除了有友员关系。这里还需要补充的是我们计算中这个B类和使用这个B类的时候,都要在前面加上A::,因为它是定义在A的域里面的。
🚈5. 再次理解封装
现实生活中的实体计算机并不认识,计算机只认识二进制格式的数据。如果想要让计算机认识现实生活中的 实体,用户必须通过某种面向对象的语言,对实体进行描述,然后通过编写程序,创建对象后计算机才可以 认识。比如想要让计算机认识洗衣机,就需要:
1. 用户先要对现实中洗衣机实体进行抽象---即在人为思想层面对洗衣机进行认识,洗衣机有什么属性,有那些功能,即对洗衣机进行抽象认知的一个过程。
2. 经过1之后,在人的头脑中已经对洗衣机有了一个清醒的认识,只不过此时计算机还不清楚,想要让计算机识别人想象中的洗衣机,就需要人通过某种面相对象的语言(比如:C++、Java、Python等)将洗衣机用类来进行描述,并输入到计算机中。
3. 经过2之后,在计算机中就有了一个洗衣机类,但是洗衣机类只是站在计算机的角度对洗衣机对象进行描述的,通过洗衣机类,可以实例化出一个个具体的洗衣机对象,此时计算机才能洗衣机是什么东西。
4. 用户就可以借助计算机中洗衣机对象,来模拟现实中的洗衣机实体了。
在类和对象阶段,大家一定要体会到,类是对某一类实体(对象)来进行描述的,描述该对象具有那些属性,那些方法,描述完成后就形成了一种新的自定义类型,才用该自定义类型就可以实例化具体的对象。
其实就是我们实现的类时候,要考虑他要实现那些函数和功能,需要哪些变量。如我们的日期类,加减比较都是里面需要的。通过这一阶段的学习,我们感觉好像我们平时的东西都可以看做是一个类,而他的功能就好像是一个个函数实现的。
用图来看就是这样的(我们通过计算机的程序在来创造实体):
💎6.结束语
好了小赵今天的分享就到这里了,如果大家有什么不明白的地方可以在小赵的下方留言哦,同时如果小赵的博客中有什么地方不对也希望得到大家的指点,谢谢各位家人们的支持。你们的支持是小赵创作的动力,加油。
如果觉得文章对你有帮助的话,还请点赞,关注,收藏支持小赵,如有不足还请指点,方便小赵及时改正,感谢大家支持!!!