- 🌸博主主页:@釉色清风
- 🌸文章专栏:@C++
- 🌸今日语录:人生本就是一首代写的诗歌,而他们的文字浅薄,不该被潦草地印刷着。所以在我笔下,“一重山有一重山地错落,我有我的平仄”。
类和对象
- 🌼面向过程和面向对象的初步认识
- 🌼类的引入
- 🌼用class定义类
- 🌻类的作用域
- 🌼类声明与定义的分离
- 🌼类中成员数据的规范化定义
- 🌼类的实例化
- 🌼类对象模型
- 🌻计算类对象的大小
- 🌻类对象的存储方式
🌼面向过程和面向对象的初步认识
C语言是面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题。
比如说洗衣服,C语言更加注重过程。
C++是基于面向对象的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成。
对于C++引入面向对象是很有必要的。这是因为在一些大型的项目中,如果只是注重过程,那么就需要考虑很多很多的情况,不是仅仅就能够用if…else就能解决的。而如果在面向过程的设计中,中间某一过程的设计有一点问题,那么就将会导致整个项目出现问题。
而对于C++的面向对象,这里的面向对象不仅仅是只看重对象的,只是更加关注对象本身,它的属性以及它能做什么。在程序或者项目的设计过程中,往往是面向对象与面向过程相结合,各有用途,互相补充。
所以在C++的学习中,类和对象的思想是极为重要的。(一定要认真学习奥!)
🌼类的引入
首先,在学习C++的类之前,我们先以我们C语言中熟悉的结构体来进行引入。
先来看我们熟悉的栈结构:(如果忘了,可以简单看一下,只是引入,没看懂也没关系的可以直接跳过。)
typedef int datatype;
typedef struct Stack
{
datatype *a;
int top;
int capacity;
}Stack;
对于指针a,理解:
接下来就是栈的基本操作:
栈的置空初始化—StackInit()
void StackInit(Stack *ps)
{
assert(ps);//断言
ps->a=NULL;
ps->top=0;
ps->capacity=0;
}
栈的初始化2.0—给栈开辟一点空间StackInit1()
void StackInit1(Stack *ps)
{
assert(ps);
ps->a=(datatype *)malloc(sizeof(Stack)*4);//开辟4个空间
ps->capacity=4;
}
栈的销毁—StackDestory()
void StackDestory(Stack* ps)
{
assert(ps);
free(ps->a);
ps->a=NULL;
ps->top=ps->capacity=0;
}
入栈----StackPush()
void StackPush(Stack *ps,datatype x)
{
assert(ps);
//判断栈的空间是否已满
if(ps->top==ps->capacity)
{
//扩容为原容量的2倍
datatype *tmp=(datatype *)relloc(ps->a,sizeof(Stack)*capacity*2);
ps->capacity=ps->capacity*2;
}
ps->a[ps->top]=x;
ps->top++;
}
出栈----StackPop()
void StackPop(Stack *ps)
{
assert(ps);
//判断当前栈是否为空,若空则无法出栈,退出程序
assert(ps->top >0);
ps->top--;
}
…
当然这里还有一些函数…
通过上面我们简单定义的栈结构,在结构体内我们定义的是数据
,在结构体外我们定义的是函数
。
所以说,在C语言中数据和方法是分离的。
因为C++兼容C语言,并对C语言中的一些用法进行了升级。那么在C++中,我们是可以直接照搬C语言中对结构体的定义的。
C++对C语言的struct进行了升级,升级成为了类。
其特点之一,就是类型就是类名,不需要加struct。
在C语言中,如果我们没有typedef一个结构体的话,那么我们需要这样定义一个结构体变量:
这样的定义有点冗杂,在C++中我们可以直接Stack s
这样来定义。
其特点之二,类里面可以定义函数。
在上面的C语言的对于栈结构的定义,我们可以写成:
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
typedef int datatype;
struct Stack
{
datatype* a;
int top;
int capacity;
//栈的置空
void StackInit(Stack* ps)
{
assert(ps);
ps->a = NULL;
ps->top = 0;
ps->capacity = 0;
}
//栈的初始化
void StackInit1(Stack* ps)
{
assert(ps);
ps->a = (datatype*)malloc(sizeof(Stack) * 4);//开辟4个空间
ps->capacity = 4;
}
//栈的销毁
void StackDestory(Stack* ps)
{
assert(ps);
free(ps->a);
ps->a = NULL;
ps->top = ps->capacity = 0;
}
//入栈
void StackPush(Stack* ps, datatype x)
{
assert(ps);
//判断栈的空间是否已满
if (ps->top == ps->capacity)
{
//扩容为原容量的2倍
datatype* tmp = (datatype*)realloc(ps->a, sizeof(Stack) * capacity * 2);
ps->capacity = ps->capacity * 2;
}
ps->a[ps->top] = x;
ps->top++;
}
//出栈
void StackPop(Stack* ps)
{
assert(ps);
//判断当前栈是否为空,若空则无法出栈,退出程序
assert(ps->top > 0);
ps->top--;
}
};
除此之外,因为一个结构体/类就是一个域,所以,我们还可以进一步简化函数的名字。
如下图:
🌼用class定义类
上面结构体的定义,在C++中更喜欢用 class 来代替。
其实就是,将关键字struct改为关键字class,但仍然是由一些区别的。区别在于访问限定符。
在初学C++时,我们可以先暂且认为protected(保护)和private(私有)是没有区别的。
对访问限定符的简单说明:
- ①public修饰的成员在类外可以直接被访问
- ②protected和private修饰的成员在内外不能被直接访问。
- ③访问权限,作用域重构该访问限定符出现的位置开始到下一个访问限定符出现为止。
- ④如果后面没有访问限定符,作用域就到}即类结束。
- ⑤class的默认访问权限是private,struct为public。
所以说,在上面我们的栈结构的定义中,用struct定义类和用class定义类是不同的。如果我们不些访问限定符,struct默认里面的数据和方法是共有(public)的,在主函数中可以直接被访问。而class默认里面的数据和方法是私有(private)的,在主函数中不可以直接被访问。
🌻类的作用域
类定义一个新的作用域,类的所有成员都在作用域中,面我们来看一下访问限定符的使用:
🌼类声明与定义的分离
我们将类的声明写在.h头文件中,将类中的成员函数定义写在.cpp文件中。这和之前的写法是一样的。唯一需要我们注意的是,关于成员函数的定义,我们需要使用域作用限定符,告诉编译系统这个函数是属于哪个类的。
在头文件Stack.h中作声明:
在Stack.cpp文件中:
🌼类中成员数据的规范化定义
下面,我们来探讨一下类中成员数据的规范化定义。这一次,我们声明一个日期Date类,数据成员包含年(year)、月(month)、日(day)。然后 我们在定义一个成员函数 对日期进行初始化,参数仍然是年(year)月(month)日(day)。
这里我们需要着重看一下 初始化函数:在函数内部进行赋值的时候,year是参数year还是成员数据year?
答案是这里都是函数的参数year、month、day。这是因为,编译系统首先会在当前域里面检查,如果有,那就是使用当前域里面的这个变量,如果没有,才会去出当前域进行进一步查找。
很显然,函数的参数就是year、month、day就是当前域里面的。所以赋值操作中都是函数的参数,最终改变的也是函数参数的值而已。随着函数调用的结束,函数的栈帧销毁,这一番赋值操作并没有改变类中数据成员的值,所以我们进行打印,结果就是随机数了。
这时,有的同学,可能会说,那我们之间改变函数的参数不就可以了吗?
简化为这样:
void Init(int year,int month,int day);
但是在一个大项目中,这样做并不好,并不能反映参数的直接含义。
所以,更规范的写法就是 改变成员数据的定义:
private:
int _year;
int _month;
int _day;
这是一种惯例,在成员数据变量前加"_"通常表示内部的。
所以,我们对上述我们的程序进行修改:
#include <iostream>
using namespace std;
class Date
{
public:
void Init(int year, int month, int day);
void display();
private:
int _year;
int _month;
int _day;
};
void Date::Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Date::display()
{
cout << _year << ":" << _month << ":" << _day;
}
int main()
{
Date d;
d.Init(2024, 3, 25);
d.display();
return 0;
}
🌼类的实例化
对象的定义也叫做类的实例化。
怎样理解类和对象?
用类类型创建对象的过程,称为类的实例化。
🌼类对象模型
🌻计算类对象的大小
我们仍以上面的日期类为例,创建一个对象,计算对象的大小
得到对象d的大小为12个字节。
🌻类对象的存储方式
进而,我们对类对象的存储方式进行猜测。
缺陷:每个对象中成员变量是不同的,但是调用同一份函数,如果按照此种方式存储,当一
个类创建多个对象时,每个对象中都会保存一份代码,相同代码保存多次,浪费空间。那么
如何解决呢?
根据,我们上述的日期类的运行结果,我们可以得知对象存储是按照第三种方式进行存储的,即只保存成员变量,成员函数存放在公共的代码段。
接下来,我们再随意写两个类,对其进行进一步探索。
#include <iostream>
using namespace std;
class A
{
public:
int a;
int b;
private:
void fun()
{
};
};
class B
{
public:
void fun()
{
};
private:
int a;
char b;
};
int main()
{
A a;
B b;
cout << endl << sizeof(a) << endl;
cout << endl << sizeof(b) << endl;
return 0;
}
由此,我们可以得出结论,①一个类的大小,实际就是该类中”成员变量”之和,这里的成员变量无关是public还是private的。
②其次,我们还应当注意内存对齐问题。
内存对齐的规则如下:
③空类的大小为一个字节·。
无成员变量的类,其对象开一个字节,这个字节不存储有效数据,用来标识定义的对象存在过。
class C
{
};
int main()
{
A a;
B b;
C c;
cout << endl << sizeof(a) << endl;
cout << endl << sizeof(b) << endl;
cout << endl << sizeof(c) << endl;
return 0;
}