作者主页
📚lovewold少个r博客主页
⚠️本文重点:C++类和对象下篇知识点讲解
👉【C-C++入门系列专栏】:博客文章专栏传送门
😄每日一言:宁静是一片强大而治愈的神奇海洋!
目录
前言
再谈构造函数
初始化列表
explicit关键字
static成员
静态成员函数
静态成员特性总结
友元
友元函数
友元类
内部类
再度理解类和对象
总结
前言
前段时间主要在博客中讲述了C++类和对象中篇的一些知识点,我们发现C++类特性功能有多强大,同时也相应很复杂。在前面的文章中主要涉及到C++默认的成员函数,包括不限于构造函数,析构函数,拷贝构造函数,赋值运算符重载以及const成员函数等内容,踏上了类和对象的编程之旅。详细请看类和对象中篇文章。
本章会主要从类和对象的更多特性入手,再次剖析构造函数,通过这篇文章的知识点和前面形成连贯理解,对类和对象有更多的理解。同时,本篇文章还会谈及Static成员,友元,内部类,等关键知识点点讲解,深刻理解C++类和对象中的封装意义。学习C++的过程中,难点一是概念情况较为复杂,需要结合代码去深入理解,同时有很多知识点需要去记忆。C++特性的数量之多就意味着很难去全部使用,大多数人需要掌握的是常用特性,同时对与很多特性其实目前是无法在自己的程序中去理解,而是需要在特定情况下去使用。因此目前我会尽量结合一些特定的代码去理解,当然这种可能看似没有意义的代码不一定真没有意义,而是并非特定的使用场景。
再谈构造函数
在前面的文章中我们提到,构造函数其实就是一种特殊的成员函数,用于在类进行定义的时候直接初始化。我们不去单独构造它,编译器会默认给类自定义类型的成员进行初始化。
而今天我们提到构造函数,其本质要实现的目的还是一样的,即在创建对象的时候,编译器调用其构造函数,给对象的每一个成员变量一个合适的初始值。
class Date { public: Date(int year, int month, int day) { _year = year; _month = month; _day = day; } private: int _year; int _month; int _day; };
上述的构造函数可以完成对创建对象赋值,但当这种属于赋予初始值,不能完成称之为初始化。为何这样子说,初始化的意义在于对变量进行赋予有价值的值,避免在后期调用出现随机值等情况影响程序的正常。构造函数体内部可以多次赋值,这本质还不算是真正意义上的初始化。
初始化只初始化一次,即在对象定义的时候就得初始化。那么我们来看下面的一段代码。
const int j; ❌ const int j = 0; ✅
对于一个常量需要初始化,有且只有一次机会,且必须初始化。那么针对于这真情况,我们怎么做才能对常量初始化呢。
class Date { public: Date(int year, int month, int day,const int N) { _year = year; _month = month; _day = day; } private: int _year; int _month; int _day; const int _N = 1;//❌ };
这好比在图纸上搭建房子,要知道类里面都是声明呀,声明可以是初始化么?答案肯定是不可以的。初始化一个变量或者一个常变量,是要先拿到它的内存空间的,而_N显然只是声明出来而未定义!
那么在构造函数内部给值么?
编译器不仅diss你没有完成初始化设定项,还提醒你表达式必须是可以修改的左值。这意味什么! _N在构造函数内部就已经定义了,而现在又改不了它的值了。
祖师爷通过类和对象的思想进行封装是好事儿,构造函数也很棒,但牵一发而动全身,作为成员变量又不能在外部去定义,在内部构造函数内部构造函数又拿它没办法。因此,就需要来一个新特性来解决这个问题,就和说慌一样,说一个慌就需要用很多慌去圆。
初始化列表
初始化列表:以一个冒号开始,接着睡一个一逗号分隔的数据成员列表,每个成员变量的后面跟着一个放在括号中的初始值或者表达值。
class Date
{
public:
Date(int year, int month, int day)
:_year(year) //冒号开始
,_month(month) //逗号分隔
,_day(day) //()内部为初始化值
,_N(1)
{};
private:
int _year;
int _month;
int _day;
const int _N;//
};
初始化列表是这些成员变量定义的地方,这里就相当于在定义的时候就初始化了。终于解决了常变量这个麻烦事儿,当然对于麻烦的事情不仅可以一起处理,也可以分开去处理。
⚠️注意:
- 每一个成员变量在初始化列表中只能出现一次(初始化只初始化一次)
- 类中包含以下成员的时候必须放在初始化列表进行初始化
- 引用成员变量 int& _ret;
- const成员变量 const int _N;
- 自定义成员变量(且该类没有构造函数的时候) A _aa;
- 尽量使用初始化列表进行初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化
观察下述代码,Date创建一个对象其本身并没有使用Time _t的成员变量,但是对于自定义类型的成员变了,一定会先使用初始化列表进行初始化。
- 成员变量在类中的声明次序就是其在初始化列表的初始化顺序,与初始化列表中的前后次序无关
在类中声明的顺序是先_a2,后_a1,因此在进行初始化时候,先对_a进行初始化,此时_a1未初始化,为随机值,_a2就初始化了为这个随机值。_a1后初始化,接受了参数1,因此初始化了为1。
当然对于初始化列表的顺序最好要和声明顺序一样,不然出现情况了不能老是弯弯绕绕的分析吧!
explicit关键字
观察下面的代码:
class Date { public: Date(int year) : _year(year) { ; } void Printf() { cout << _year << endl; } private: int _year; }; int main(void) { Date d1(2022); cout << "d1-> "; d1.Printf(); Date d2 = 2022; // 隐式类型转换 cout << "d2-> "; d2.Printf(); return 0; }
d1没什么好说的,可是d2就有点奇怪了,用一个整形值给d2赋值竟然可以赋值成功。其实这里就发生了隐式类型转换。实际上编译器会用2022这个值构造一个无名对象,最后将无名赋值给d1。这种隐式类型转换是存在的,且在单参数和多参数其第一个参数无缺省值(后两位可以不传参),而其他位均有缺省值得情况下就会发生这些默认的隐式类型转换。这样就会导致代码的可读性变差。因此 explicit 能修饰函数,限制其类型转换作用的发生。
static成员
static的类成员称之为类的静态成员,用static修饰的成员变量称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。静态成员函数变量一定要在类外进行初始化。
概念不好理解这里我们先来试着写一段代码,来体会一下何为静态成员和静态成员函数。
现在,我们需要计算我们到底创建了多少个对象。这个很简单,即在所有构造函数内部实现对一个变量的加减即可,如果要计算有多少个对象正被使用也只需要在析构函数对一个变量减值即可。
思路简单,问题来了,变量设置在哪里呢?
我们设置为全局变量
int N = 0; // 全局变量计数器 class A { public: A(int a = 0) : _a(a) { N++; } A(const A& aa) : _a(aa._a) { N++; } private: int _a; }; void f(A a) { ; } int main(void) { A a1; A a2 = 1; f(a1); cout << N << endl; return 0; }
这一段代码一定可以实现记数功能的,没错!笃定切肯定!但是类和对象核心就是封装,就设置一个变量N,随便外部都可以通过修改全局变量就能改变变量N,怎么能保证功能安全呢。
我们设置为成员变量,然后问题又来了。
class A { public: A(int a = 0) : _a(a) { _N++; } A(const A& aa) : _a(aa._a) { _N++; } private: int _a; int _N = 0; };
对于上面的成员变量,每一次创建对象是使用的同一个_N吗?很显然不是,且每一个对象都是使用的this->_N,这就说明没一个对象都有一个_N,这种方案直接否定。还不如上一种全局变量呢,至少还能实现。 因此方案三出来了,static成员函数。
静态成员为所有类共享,不属于某个具体的类,存放在静态区。
class A {
public:
A(int a = 0)
: _a(a) {
_N++;
}
A(const A& aa)
: _a(aa._a) {
_N++;
}
//private:
int _a;
static int _N;
};
int A::_N = 0;
当然解决一个问题又来一个,静态成员首先也是一个成员,受public、protected、private访问限定符的限制。
因此,对于一个静态成员的访问我们需要怎么样去访问呢?如果它是类的公有成员还好说,直接突破类域即可(即非private)。
int main(void)
{
A a1;
A a2 = 1;
f(a1);
cout << A::_sCount << endl; //突破类域进行访问
//这里a1.并非在a1里面,静态成员为所有对象共享,这里只是帮助他突破类域的一种手段
cout << a1._sCount << endl;
cout << a2._sCount << endl;
return 0;
如果不是公有成员呢,我们是否可以达到这种的目的呢。肯定也是可以的,通过在类内部设置一个Get_N()公有函数拿到这个值就好了。这个时候外部调用对象函数就能得到它了。
静态成员函数
类外调用该函数可以访问到,而静态成员函数就可以不使用对象就可以访问到它。
static int GetCount()
{
return _sCount;
}
静态成员特性总结
- 静态成员为所有类共享,不属于某一个具体的对象,只存放与静态区。
- 静态成员变量必须在类外定义,定义时候不添加static关键字,类中只是声明
- 类静态成员即可用类名::静态成员或者对象.静态成员来访问。
- 静态成员函数没有隐藏的this指针,不能访问任何非限定静态成员。
- 静态成员也是类的成员,受public、protected、private访问限定符的限制。
友元
何为友元,即作为一个类外部的函数或者类,能访问其friend声明所在类的一种方式。
我们之前学了操作符重载,对与一个日期类,我们是否也可以通过重载operator<<和operator>>实现对日期类的流提取和流输入呢?
我们先看流输入和流输出的库函数。
根据函数的原理和返回类型,我们直接开始我们的输入输出流重载。不懂原理目前也没事,可以知道的是cout和cin返回值类型是ostream和sitream。
class Date
{
public:
Date(int year = 1970, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day)
{};
ostream& operator<<(ostream& _cout)
{
_cout << _year << "-" << _month << "-" << _day << endl;
return _cout;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2023, 4, 22);
cout << d1;//无法识别
return 0;
}
这里依旧识别不了,双操作数的运算符重载时,规定第一个参数是左操作数,第二个参数是右操作数。而这里悄咪咪藏着一个this指针。
因此调用的时候就不能就不能按这样子了,那怎么调用呢。
int main() { Date d1(2023, 4, 22); /*cout << d1;*/ d1.operator<<(cout); d1 << cout; return 0; }
这可不对劲,正常流输入是cout<<d1,这可怎么办才好。很显然我们要消除this指针的影响,同时还能访问其类的内部成员,而友元就可以做到这样子的事情。一步到位,既不像全局函数的访问权限受限制,还可以直接直接消除this指针
友元函数
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明的时候加上 friend 关键字。
class Date { friend ostream& operator<<(ostream& _cout, const Date& d); friend istream& operator>>(istream& _cin,Date& d); public: Date(int year= 1970, 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 << endl; return _cout; } istream& operator>>(istream& _cin, Date& d)//要修改,别加const { _cin >> d._year >> d._month >> d._day; return _cin; } int main() { Date d; cin >> d; cout << d; return 0; }
⚠️注意:
- 友元函数可访问类的私有和保护成员,但不是类的成员函数。(只是类的朋友)
- 友元函数不能用const修饰
- 友元函数可以类定义的任何地方声明,不受类访问限定符限制
- 一个函数可以是多个类的友元函数
- 友元函数的调用与普通函数的调用原理相同
友元类
- 友元类的所有成员函数都是其friend友元所在声明类的友元函数,都可以访问另一个类的公有成员。
- 友元的关系不能传递,若如果B是A的友元,C是B的友元,则不能说明C是A的友元。
- 友元不能继承,关于继承的详细讲解在后面博客文章阐述,这里不展开讲解。
定义一个友元类
class Date; // 前置声明
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 = 1970, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
void SetTimeOfDate(int hour, int minute, int second)
{
// 直接访问Time类私有的成员变量
_t._hour = hour;
_t._minute = minute;
_t._second = second;
}
private:
int _year;
int _month;
int _day;
Time _t;
};
也就是说这种访问是单向的,时间类里不能访问日期类,因为这是 "单向好友",要想互相访问,只需要互相为各自的友元就好啦。
⚠️注意:
友元提供了一种突破封装的方式,有时候提供了便利。但是友元会增加耦合度,破坏了特定的封装,所以友元不宜多用。
内部类
内部类顾名思义就是一个类内部的类。即一个类如果定义在一个类的内部,这个类就叫做内部类。内部类是一个独立的类。它不属于外部内,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。
内部类就是外部类的友元类,参见友元的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员,但这种是绝对单向的友元,外部类不是内部类的友元。
特性:
- 内部类可以定义在外部的public,protected,private都是可以的
- 内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名
- sizeof()外部类=外部类,和内部类没有任何关系
其实这里的概念我们结合友元类就能清晰理解,内部类是外部类的天生友元。
class A {
private:
static int _s_a1;
int _a2;
public:
class B
{
// B天生就是A的友元
public:
void foo(const A& a)
{
cout << _s_a1 << endl;
cout << a._a2 << endl;
}
private:
int _b1;
};
};
但是需要着重理解的一点是,在写代码的时候看似是外部包含内部的关系,但是实际上以权限的角度来讲,内部权限大于外部,即内包外。要从权限这点抽象的去理解,而非看见的写代码时候的包含关系。
class A { private: static int _s_a1; int _a2; public: class B { // B天生就是A的友元 friend class A; // 声明A是B的友元 private: int _b1; }; };
我们也可以通过以上的方式去实现打破这种封装,不过情况很少,既然这样子为何不使用一个类呢(除非特定情况)。
再度理解类和对象
类和对象是面向对象编程(Object-Oriented Programming,简称OOP)中的两个重要概念,用于组织和管理代码,使其更易于理解、维护和扩展。
类(Class) 可以看作是一张图纸或模板。假设你要建造一栋房子,你需要一张建筑图纸来指导建筑师和工程师。这张图纸就像一个类,它包含了房子的设计、规格和特点。图纸上可能包括了房子的形状、大小、门窗的位置等信息,就像一个类包含属性和方法的定义。
对象(Object) 则就是按照那张图纸建造出来的实际房子。当你按照图纸建造房子时,你实际上是在基础上创建了一个具体的、独立的实体。这个实际的房子就像类的对象,它有自己的属性(如颜色、材料、装饰)和可以执行的操作(如打开门、关上窗户),但它们都基于同一张图纸创建。
举个更具体的例子,想象一个类是"汽车",它的属性包括颜色、品牌和速度,方法包括启动、加速和刹车。当你购买一辆具体的车,比如一辆红色的福特(Ford),你就得到了一个对象,这个对象就是这个类的一个实例,拥有红色的颜色、福特的品牌,以及能够启动、加速和刹车等方法。
所以,类就像是一个设计图纸,对象就像是根据这个图纸建造出来的实际物体。图纸定义了如何制造物体,而物体是根据图纸的规格制造的。类定义了对象的结构和行为,而对象则是类的具体实例,具有自己的属性值,可以执行类中定义的方法。这是面向对象编程的核心概念之一,它有助于将代码组织成更加模块化、可维护和可重用的部分。
总结
在C++类和对象的三个篇章中,主要涉及了C++中类和对象的一系列知识点,包括:
构造函数和析构函数
拷贝构造函数
赋值运算符重载
const成员函数
初始化列表
static成员
友元函数和友元类
内部类
在本篇文章中,完结了关于C++类和对象的重要知识点。类和对象是面向对象编程的核心,通过构造函数、析构函数、拷贝构造函数等,我们可以创建对象并初始化其属性。静态成员允许类级别的属性和方法,友元函数和友元类提供了访问私有成员的机制,而内部类允许在类内部定义嵌套的类。
这些知识点为理解和应用C++中的面向对象编程提供了基础。虽然这些概念可能看起来复杂,但它们有助于将代码组织成更模块化、可维护和可重用的部分。相信大家通过不断学习和实践,可以更好地掌握C++编程的技能,并编写更高效、可维护的代码。
作者水平有限,如有错误欢迎指正!