嗨喽大家好,阿鑫又和大家见面了,经过了昨天清明假期的休整,今天也是给大家带来类和对象的第二弹
类和对象第二弹
1.类的作用域
2.面向对象的三大特性及浅谈封装的优越性
3.对象实例化
4.计算类的大小
5.类成员函数的this指针
6.初识类的默认成员函数
好的接下来开始我们有趣的编程学习吧
1.类的作用域
我们先来创建三个文件
第一个文件用来放成员函数的声明,第二个文件用来放函数的定义
在这里插入代码片#pragma once
#include<iostream>
#include<cassert>
using namespace std;
class stack
{
public:
void Init();
void Push(int x);
private:
int* _a;
int _top;
int _capacity;
};
//stack.cpp中
void Init()
{//报错
_a = nullptr;
_top = 0;
_capacity = 0;
}
当我们采用如上代码的方式定义函数是会报错的,因为当我们调用函数或者变量时,编译器都会去找调用的地方,而在当前域中是找不到这三个变量
并且不指定,编译器不知道是谁的函数,只知道是一个全局函数,但是访问不了类中的成员变量,加了就知道是类的成员函数
所以正确的写法是
void stack::Init()
{
_a = nullptr;
_top = 0;
_capacity = 0;
}
这就是我们要提到的类域的概念,为了防止两个类中相同名字的函数会重复及其他原因,一个类会形成一个类域,我们在调用域中私有变量时,需要在函数名称前面加上类名::。
2.面向对象的三大特性及浅谈封装的优越性
面向对象的三大特性:封装、继承、多态。
封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。
封装本质上是一种管理,让用户更方便使用类。比如:对于电脑这样一个复杂的设备,提供给用户的就只有开关机键、通过键盘输入,显示器,USB插孔等,让用户和计算机进行交互,完成日常事务。但实际上电脑真正工作的却是CPU、显卡、内存等一些硬件元件。基c
把数据和方法放到一起更方便管理,想给你看到的变成公有,不想给你访问的变成私有
在面向对象编程时,我们必须使用类提供的公有成员函数(如 setMyPrivateVar 和 getMyPrivateVar)来间接地访问和修改私有成员变量的值。
这种封装机制是面向对象编程的一个重要特性,它有助于隐藏类的内部实现细节,并只允许通过类的公有接口来访问和操作对象的状态。这有助于提高代码的安全性和可维护性。
然而,如果变量是公有的(public),那么情况就不同了。公有变量是类的外部接口的一部分,它们可以在类的外部直接通过类的对象来访问和修改。这意味着任何拥有类对象的人都可以直接读取或写入这些公有变量的值。
虽然公有变量提供了直接的访问方式,但这种做法通常并不被推荐,因为它破坏了封装性,可能导致数据完整性和安全性的问题
所以我们将变量和方法封装在一起,有利于编程的规范性和安全性,同时将变量作为私有变量
3.对象实例化
那么,如何确定变量是申明还是定义呢?
private:
int* _a;
int _top;
int _capacity;
};
如上代码是变量的声明,声明就是类型和名字,定义是开空间,在利用类型去定义一个对象时才会定义出来,也叫做对象实例化
#include"stack.h"
int main()
{//对象实例化
stack st1;
stack st2;
return 0;
}
4.计算类的大小
接下来我们介绍如何计算一个类的大小,我们给出两个问题
1.我们是利用类名还是对象进行大小的计算?
2.计算类的大小时,类的成员函数需要计算大小吗?
首先我们回答,类名,和类的对象都可以进行大小的计算
如上图所示,我们既可以用类名来计算类的大小,也可以用对象来计算
其次,我们在计算类的大小时是不需要计算成员函数的大小的,他们不存在于对象的内存当中,存在与一个独立的空间
我们将代码转成汇编语言很明显能看出,调用的函数的地址都一样
至于大小如何计算,本博客只做简单介绍,同c语言中的结构体一致,需要遵循内存对齐
1.第一个成员在与结构体偏移量为0的地址处。
2.其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
注意:对齐数 =编译器默认的一个对齐数 与 该成员大小的较小值,
VS中默认的对齐数为8
3.结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。4.如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
注意:空类的大小是1,这一个字节不存储有效数据,用来表示对象被定义出来
5.类成员函数的this指针
#include"stack.h"
class Date
{
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
//stack st1;
//stack st2;
//cout << "sizeof(st1) = " << sizeof(st1) << endl;
//cout << "sizeof(stack) = " << sizeof(stack) << endl;
Date d1;
Date d2;
d1.Init(2024, 4, 2);
d2.Init(2024, 4, 3);
//d1.Print(&d1);
d1.Print();
//d2.Print(&d2);
d2.Print();
return 0;
}
同学们在看到上面的运行结果有没有疑问,为什么调用的是相同的Print函数,打印出来的结果却不同呢?
这是因为编译器的功劳啊,请看:
注释掉的是实际上运行的代码,编译器用了一个this指针来接受我们传过去的对象的地址,从而通过this来进行对象的调用
下面我们先给出this指针的规则
1.实参和形参的位置不能显示写,编译器自己加
2.但是在类里面可以调用this指针
接下来通过两个例子让我们对this指针的理解更加深刻
上面的两道题目,第一个是正常运行,第二个则是运行崩溃
原因如下:
1.第一题不用去对象里面找print的地址,只需要传对象的地址
2.第二题的代码会运行崩溃,this->_a,此处的this指针为空,对空指针进行访问,程序会运行崩溃
注意:this指针是形参,形参存在栈中
6.初识类的默认成员函数
在c++中,如果一个类中什么成员都没有,简称为空类,
空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。
默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。
我们要介绍的第一个默认成员函数是构造函数
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。
其特征如下:
1.函数名与类名相同。
2.无返回值。
3.对象实例化时编译器自动调用对应的构造函数。
4.构造函数可以重载。
根据C语言标准,读取未初始化的局部变量的值是未定义行为(Undefined Behavior, UB)。未定义行为意味着程序可能表现出不可预测的行为,这包括产生随机值、崩溃或任何其他意想不到的结果。
在过去,程序员们可能会经常忘记初始化,从而导致程序报错或者出现随机值的情况,而我们的构造函数就能很好解决初始化的事情
下面给出构造函数实现的代码
stack::stack(int n)
{
_a = (int*)malloc(sizeof(int) * 4);
_top = 0;
_capacity = 4;
}
//text.cpp中
int main()
{
stack st1;
//stack st2;
//cout << "sizeof(st1) = " << sizeof(st1) << endl;
//cout << "sizeof(stack) = " << sizeof(stack) << endl;
Date d1;
Date d2;
d1.Init(2024, 4, 2);
d2.Init(2024, 4, 3);
//d1.Print(&d1);
d1.Print();
//d2.Print(&d2);
d2.Print();
return 0;
}//stack.h中
#pragma once
#include<iostream>
#include<cassert>
#include<cstdlib>
using namespace std;
class stack
{
public:
void Init();
void Push(int x);
stack(int n = 4);
private:
int* _a;
int _top;
int _capacity;
};
只需要我们对象实例化时编译器自动调用对应的构造函数。
因为构造函数可以重载,所以我们有多种初始化的方式
#include"stack.h"
class Date
{
public:
Date()
{
_year = 1;
_month = 1;
_day = 1;
}
Date(int year = 2005, int month = 07, int day = 16)
{
_year = year;
_month = month;
_day = day;
}
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
stack st1;
//stack st2;
//cout << "sizeof(st1) = " << sizeof(st1) << endl;
//cout << "sizeof(stack) = " << sizeof(stack) << endl;
Date d1;
d1.Print();
Date d2(2005,07,16);
/*d1.Init(2024, 4, 2);
d2.Init(2024, 4, 3);*/
//d1.Print(&d1);
d2.Print();
//d2.Print(&d2);
//d2.Print();
return 0;
}
虽然代码中两种构造方式能构成函数重载,但会出现冲突导致调用不明确,所以我们一般保留含有全省参数的方法
更深入的内容我们将在下篇博客给出,这里只做介绍
我们要介绍的第二个默认成员函数是析构函数
通过前面构造函数的学习,我们知道一个对象是如何初始化的,那一个对象又是怎么销毁的呢?析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
析构函数是特殊的成员函数,其特征如下:
1.析构函数名是在类名前加上字符~。
2.无参数无返回值类型,
3.一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构不能重载
stack::~stack()
{
cout << "~Stack()" << endl;
free(_a);
_a = nullptr;
_top = 0;
_capacity = 0;
}
我们借用打印来看有没有在对象销毁时自动调用析构函数
我们可以看出在对象销毁时,编译器自动调用了析构函数
好啦!学习的时光总是短暂的,阿鑫要跟大家说再见了!期待我们的下一次相遇,觉得博主写的还可以的留下你们的三连支持哦,拜拜!