目录
前言
什么是面向对象?什么是面向过程?
面向过程
面向对象
比较
类
引入
定义
实例化
类的大小
this指针
前言
今天我们来进入C++类和对象的学习。相信大家一定听说过C语言是面向过程的语言,而C++是面向对象的语言?那么他们有什么却别呢?又怎么体现呢?这就与我们今天要说的类和对象脱不了关系了。
什么是面向对象?什么是面向过程?
面向过程
面向对象是相对于面向过程来讲的,面向对象方法,把相关的数据和方法组织为一个整体来看待,从更高的层次来进行系统建模,更贴近事物的自然运行模式。
面向过程(Procedure Oriented)是一种以过程为中心的编程思想。这些都是以什么正在发生为主要目标进行编程。
这是两种方法的官方解释,我们首先要明白的是不论是面向过程还是面向对象他们的最终目的都是解决具体的问题,只是角度不同罢了。下面举个具体的例子来看。
比如现在我们要洗衣服。如果以面向过程的思想,那么它应该按照如下的思路进行
紧接着我们用代码标识上述的一些列操作,先定义个盆,设置盛水量等自定义变量,然后写个放水的函数,再放洗衣粉,手搓……最后晾衣服。可以看出面向过程就是按照事物的发展逐步的解决问题,符合我们的直觉,当我们知道具体的过程时,便可以一步步的实现代码,将洗衣服的大事不断化小,分解,最终再汇总就解决了问题。
当然面向过程的实现代码的难度不会太高,只要我们知道了一件事的过程便可以逐步的解决。化大为小,通过不断地解决子问题最总解决问题。
面向对象
不难发现,在C语言中数据和函数是分离的。我们不能在自定义的类型如struct中实现函数,要想使用函数,必须给函数传递数据的参数,然后进行处理。但在C++中数据和函数是紧密结合的,像这样的自定义类型不只包含数据,还有函数的就可以称为类。
还是上面洗衣服的例子。假如我们以面向对象的思想看待这个问题,我们该如何做呢?
首先我们要抽象出来对象,就可以理解为我们要操作的数据,对象就有人,盆,洗衣机,衣服,而接水,手搓,拧干就可以看为数据的处理,即类的函数。
我们接着就调用人类中的拿衣服函数,然后盆类装衣服函数,假如洗衣粉函数,然后再调用人类中的洗衣函数,最后调用人类中的晒衣服函数。
可以看出,所谓的面向对象就是从一件事情中抽象出对象,然后在对象间处理事情。
面向对象看起来十分的简单,只需要不断地调用函数,但这些函数却是要我们自己一一实现的,与面向过程最大的不同是将函数与数据封装在一起。可以说是从另外一个思想角度上解决了问题。但类还有许多巨大的优势,例如简化代码,在类中实现函数不需要传太多的参数,安全性提高,命名冲突大大减少,代码复用性好等,我们在后面会一一介绍。
比较
有个恰当的比喻是,面向过程是编年体,以时间为线索记录历史,面向对象是纪传体,以人物为线索记录历史,二者殊途而同归。同样面向过程就是按照我们理解的事情发展而不断地写函数处理,而面向对象是先在类中实现类的各种函数,最后再使用。二者最终都可以解决问题。
从上面看来说,面向对象和面向过程复杂度没有太大区别,但面向对象又发展出了三大特性,封装,继承,多态。这个我们会在后面说,这几种重要的性质大大简化了面向对象开发的复杂度。
接下来我们正式的了解类的概念和用法。
类
引入
在正式的说类之前,我们先看一个我们熟悉的C语言结构体知识。
struct Date
{
int year;
int month;
int day;
};
void DateInit(struct Date* p,int year,int month,int day)
{
p->day = day;
p->month = month;
p->year = year;
}
void DatePrint(struct Date* p)
{
printf("%d-%d-%d\n",p->year,p->month,p->day);
}
int main()
{
struct Date t;
DateInit(&t, 2024, 4, 11);
DatePrint(&t);
return 0;
}
我们定义了一个日期结构体,然后打印出日期。函数名为了不与其他的函数混淆,同一加上了Date, DatePrint,DateInit,当然就目前的小段程序而言不需要区分,但加了区分是一个好习惯。
可能会有读者十分疑惑?不是要讲类么,怎么讲了结构体。我们要明白没有人可以什么都从无到有的创造,我们要学会站在巨人的肩膀上。类可以看为C语言结构体的PLUS版。
定义
类的定义十分简单如下
class/struct className
{
// 类体:由成员函数和成员变量组成
};
其内部的成员又可以被操作符public: private: protected:修饰从而产生不同的用法。是不是与结构体十分相似,看起来不过加个三个修饰符并且支持内置函数。但这几个用法较为复杂,我们后序再做介绍。
由此我们便可以将上面C语言的代码用C++写出来。
struct Date
{
int _year;
int _month;
int _day;
void Init( int year, int month, int day)
{
_day = day;
_month = month;
_year = year;
}
void Print()
{
printf("%d-%d-%d\n", _year, _month, _day);
}
};
int main()
{
Date t;
t.Init(2024, 4, 11);
t.Print();
return 0;
}
首先我们要明白,类里面的变量,函数存在类的命名空间内,俗称类域。这样我们的函数名就不需要加上Date标识符了,即使其他的类也有Init函数,但他们处于不同的命名空间中,不会发生命名冲突。其次我们也不需要传递结构体的指针了,或者说不需要我们人为的传递指针,在类的里面函数可以直接调用内置类型。是不是相对于C语言来说简化了许多代码了!!
上述的struct Date也可以称之为类,但大家看见更多的可能是class Date,他们都可以称之为类。但C++为了衔接C语言,将struct Date默认数据类型用public:修饰,class Date默认类型用private:修饰。他们的区别如下。
当然访问限定符的区别不知有上面的,更多的在类的继承模块,在这里就不多讲了。其次访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止。 如果后面没有访问限定符,作用域就到}即类结束。
除了默认访问权限的不同,struct Date与class Date基本没有区别,但我们定义类一般用class,与以前的结构体做区分。
我们保存的日期数据肯定是不希望其他人随便更改的,于是便可以使用访问限定符,只流出一定的接口函数给别人使用,对于最基础的数据禁止直接修改访问。如下代码。
class Date
{
public:
void Init( int year, int month, int day)
{
_day = day;
_month = month;
_year = year;
}
void Print()
{
printf("%d-%d-%d\n", _year, _month, _day);
}
private:
int _year;
int _month;
int _day;
};
假如我们在类的外面修改 _year,便会报出如下错误。
实例化
此时我们再次回到刚才类的代码上。我们思考一个问题?这段代码是声明还是定义?
class Date
{
public:
void Init( int year, int month, int day)
{
_day = day;
_month = month;
_year = year;
}
void Print()
{
printf("%d-%d-%d\n", _year, _month, _day);
}
private:
int _year;
int _month;
int _day;
};
我们要区别声明和定义首先要了解他们的区别,简单来说定义就是开辟一段空间然后可以存储对应的数据,而声明不开辟空间,仅仅是告诉编译器有这个变量或者函数。显然我们上述的代码是一段声明,并没有开辟一段空间。而下面一段代码则不同。
Date t;
它就是定义了一个Date变量,开辟了空间。我们可以把类称之为一种自定义类型的变量类型,所谓实例化就是用这个变量类型开辟空间,定义变量。
我们可以把类看为房子的设计图纸,而对象就是根据类实现的房子。如下图
类的大小
说完实例化,我们紧接着来看类的大小,我们前面提到过类与C语言的结构体是血脉相连的,结构体计算内存采用的内存对齐在类中也有。
下面是结构体内存对齐规则,在类中变量也遵循如下的规则。
1. 第一个成员在与结构体偏移量为0的地址处。
2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。 注意:对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。 VS中默认的对齐数为8
3. 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。
4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整 体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
但类比C语言多了个函数,那么函数如何计算内存呢?
我们先看如下使用场景。
int main()
{
Date t;
t.Init(2024, 4, 11);
t.Print();
Date t2;
t2.Init(2024, 4, 11);
t2.Print();
Date t3;
t3.Init(2024, 4, 11);
t3.Print();
Date t4;
t4.Init(2024, 4, 11);
t4.Print();
return 0;
}
这个代码十分简单,创建四个对象,并且初始化,打印。对于成员函数而言,他们在接受参数后,会根据参数进行处理。那么我们四次打印,初始化的代码逻辑是不是相同的,只不过实参不同罢了。如果我们给每个对象都保存一份成员函数,是不是大大的浪费了使用空间,于是C++就规定,类的成员函数不计算内存,类的成员函数代码保存在系统的代码区。
于是我们可以用关键字sizeof求出类的大小,除非不给用sizeof,否则不要自己算!这里为了让大家理解,会带着大家算一遍。
为什么这样呢?看如下示意图。
有的读者可能觉得这不就是三个int相加么?但这只是一种巧合,下面我们稍微修改代码在计算一次。
class Date
{
public:
void Init( int year, int month, int day)
{
_day = day;
_month = month;
_year = year;
}
void Print()
{
printf("%d-%d-%d\n", _year, _month, _day);
}
private:
int _year;
char t;
int _month;
int _day;
};
大家可以先自行计算,然后看下面的分析。
我们也可以用程序检测下。可以看出此时类的大小也为16.
关于大小我们最后再看一个特殊的情况空类。如下代码
class pr
{
};
那么他的大小是多少呢?为0还是什么?
可以看出他的大小为1,这也是C++的规定之一,空类的大小为1.也就是说类的大小至少为1.
C++做了这个规定,可能也是为了如下代码考虑,用空类定义了一个对象,属于定义还是声明。如果空类的大小为0,那么就不符合定义的基本条件开辟内存,划为声明有有些不和常量,所以最后规定空类的大小为1.
int main()
{
pr t;
printf("%d", sizeof(pr));
return 0;
}
this指针
接下来我们来认识个C++关键字this,它有什么意义呢?
我们回头看上面的一段程序
int main()
{
Date t;
t.Init(2024, 4, 11);
t.Print();
Date t2;
t2.Init(2024, 4, 11);
t2.Print();
Date t3;
t3.Init(2024, 4, 11);
t3.Print();
Date t4;
t4.Init(2024, 4, 11);
t4.Print();
return 0;
}
对于每个t.Init(2024, 4, 11);操作我们只提供了对应要初始化的值,编译器如何才能找到对于的变量对其进行初始化,而不会找错对象。我们回顾C语言对结构体初始化的程序,如下代码。我们为什么可以精准的找到对应变量并对齐进行初始化。我们传递了个结构体指针保证了我们不会出错。
void DateInit(struct Date* p,int year,int month,int day)
{
p->day = day;
p->month = month;
p->year = year;
}
回到C++,我们写出t2.Init(2024, 4, 11);的函数,也能初始化,其实是编译器帮我们默认传递了一个类的指针!!在类中用this表示。并不是C++语言多神秘,只是编译器帮我们做了许多底层的工作,帮助我们更好的使用语言。我们也可以简单的看下汇编代码。
编译器在背后做出了巨大的奉献!!方便我们使用。
今天的文章就到此结束了,大家喜欢的点点关注!