Re:从零开始的C++世界——类和对象(上)
文章目录
- 1.初步认识
- 🍎 面向过程
- 🍎面向对象
- 🍎说明
- 2.类的引入
- 3.类的定义
- 🍎类的两种定义方式
- 3.访问限定符
- 🍎封装
- 🍎再谈封装
- 4.类域
- 5.类的实例化
- 6.类对象⼤⼩
- 🍎类对象的存储方式猜测
- 🍎内存对⻬规则
- 7. this指针
- 🍎this指针的引出
- 🍎this指针的特性
- 🍎重点一
- 🍎重点二
- 🍎总结
1.初步认识
在进入今天的文章之前,我们要对 面向过程 和 面向对象 有个初步认识。
我们知道:
·C 语言是 面向过程 的,关注 的是 过程,分析出求解问题的步骤,通过函数调用逐步解决问题。
·C++ 是基于 面向对象 的,关注 的是 对象,将一件事情拆分成不同的对象,靠对象之间的交互完成。
可能大家还是有点难以理解,我拿实际生活的案例来举一个例子,比如说 洗衣服。
🍎 面向过程
如果是面向过程的话,我们会将这个洗衣服任务拆解成一系列的步骤,每一个步骤就是一个函数。
第一步,打开洗衣机;
第二步,放衣服和洗衣液;
第三步,选择洗衣模式,开始洗衣;
第四步,等待洗完,拿出衣服。
🍎面向对象
如果是面向对象的编程方式,我们会拆分成 人 和 洗衣机 两个对象,再分析每一个对象,它需要做哪些事情:
人在其中需要做这三件事:
第一件事,打开洗衣机;
第二件事,放衣服和洗衣液,
第三件事,洗完衣服后拿出衣服。
洗衣机在其中只需要做一件事情:根据洗衣模式洗衣服。
🍎说明
在这个例子中,我们能够看出来面向过程跟面向对象,是两种不同的思维方式,处理问题的思考的角度不一样。
面相过程的思维方式,它更加注重这个事情的每一个步骤以及顺序。它比较直接高效,需要做什么可以直接开始干。
面向对象的思维方式,它更加注重事情有哪些参与者,需求里面有哪些对象,这些对象各自需要做些什么事情。将其拆解成一个个模块和对象,这样会更易于维护和拓展。
这个是面向过程跟面向对象的区别。
2.类的引入
C 语言中,结构体中只能定义变量,在 C++ 中,结构体内不仅可以定义变量,也可以定义函数。
#include <iostream>
using namespace std;struct Student
{void Init(const char* name, const char* gender, int age){strcpy(_name, name);strcpy(_gender, gender);_age = age;}void Print(){cout << _name << " " << _gender << " " << _age << endl;}// 这里并不是必须加_// 习惯加这个,用来标识成员变量char _name[20];char _gender[3];int _age;
};int main()
{struct Student s1; // C语言的用法Student s2; // C++中,可以直接用Students1.Init("张三", "男", 18);s1.Print();return 0;
}
• C++中struct也可以定义类,C++兼容C中struct的⽤法,同时struct升级成了类,明显的变化是
struct中可以定义函数,⼀般情况下我们还是推荐⽤class定义类。
• 为了区分成员变量,⼀般习惯上成员变量会加⼀个特殊标识,如成员变量前⾯或者后⾯加 _ 或者 m 开头,注意C++中这个并不是强制的,只是⼀些惯例。
3.类的定义
语法:
class className
{//类体:由成员变量和成员函数组成}; //注意后面的分号
其中:
• class 为定义类的关键字;• ClassName 为类的名字;• {}中为类的主体,注意类定义结束时后面分号;
类中的元素:称为 类的成员;
类中的数据:称为 类的属性 或者 成员变量,
类中的函数:称为 类的方法 或者 成员函数。
🍎类的两种定义方式
(1)声明和定义全部放在类体中
需要注意:成员函数如果在类中定义,编译器可能会将其当成 内联函数 处理。
(2)声明放在.h 文件中,类的定义放在.cpp 文件中
注意:一般情况下,更期望采用第二种方式。
3.访问限定符
C++⼀种实现封装的⽅式:⽤类将对象的属性与⽅法结合在⼀块,让对象更加完善,通过访问权限选择性的将其接⼝提供给外部的⽤⼾使⽤。
• public修饰的成员在类外可以直接被访问;
•protected和private修饰的成员在类外不能直接被访问,protected和private是⼀样的,以后继承章节才能体现出他们的区别。
• 访问权限作⽤域从该访问限定符出现的位置开始直到下⼀个访问限定符出现时为⽌,如果后⾯没有访问限定符,作⽤域就到 }即类结束。
• class定义成员没有被访问限定符修饰时默认为private,struct默认为public。
• ⼀般成员变量都会被限制为private/protected,需要给别⼈使⽤的成员函数会放为public。
🍎封装
什么是封装呢?
通过 **访问修饰符(如 private)**来修饰成员变量和成员方法,将不需要对外提供的内容都隐藏起来,提供公共方法对其访问。
封装的好处是:
•隐藏实现细节,提供公共的访问方式;
•提高了代码的复用性;
•提高安全性;
封装的意义在于,将内部的实现细节隐藏起来,对外部的调用者来说是透明的,调用者也不用关心它内部是怎么实现的,只需要知道这个方法是干什么的就好。
🍎再谈封装
封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。
举例说明:
兵马俑相信大家应该知道吧,作为是世界八大奇迹之一我们如何管理兵马俑呢?如果什么都不管,兵马俑就被随意破坏了。那么我们首先建了一座房子把兵马俑给封装起来。但是我们全封装起来不是不让别人看。所以我们开放了售票通道,可以买票突破封装在合理的监管机制下进去参观。类也是一样,我们使用类数据和方法都封装到一下,不想给别人看到的,我们使用 protected 或 private 把成员封装起来:开放一些共有的成员函数对成员合理的访问。所以封装本质是一种管理。
4.类域
• 类定义了⼀个新的作⽤域,类的所有成员都在类的作⽤域中,
在类体外定义成员时,需要使⽤ :: 作⽤域操作符指明成员属于哪个类域。
class Student
{
public://显示基本信息void Print();private:char _name[20];char _gender[3];int _age;
};//这里需要指定Print是属于Student这个类域
void Student::Print()
{cout << _name << " " << _gender << " " << _age << endl;
}
5.类的实例化
⽤类类型在物理内存中创建对象的过程,称为类实例化出对象。
(1)类是对象进⾏⼀种抽象描述,是⼀个模型⼀样的东西,限定了类有哪些成员变量,这些成员变量只是声明,没有分配空间,⽤类实例化出对象时,才会分配空间。
(2)⼀个类可以实例化出多个对象,实例化出的对象 占⽤实际的物理空间,存储类成员变量。
打个⽐⽅:类实例化出对象就像现实中使⽤建筑设计图建造出房⼦,类就像是设计图,设计图规划了有多少个房间,房间⼤⼩功能等,但是并没有实体的建筑存在,也不能住⼈,⽤设计图修建出房⼦,房⼦才能住⼈。
(3)同样类就像设计图⼀样,不能存储数据,实例化出的对象分配物理内存存储数据。
6.类对象⼤⼩
class A
{
public:void PrintA(){cout << _a << endl;}
private:char _a;
};
一个类当中既可以有成员变量,又可以有成员函数,那么一个类的对象中包含了什么?类的大小又是如何计算的呢?
🍎类对象的存储方式猜测
我们可以先来猜测一下类的对象是怎么存储的,我这里给了下面两种设想:
(1)对象中包含类的各个成员
但是这样会存在一个缺陷: 每个对象中成员变量是不同的,但是调用同一份函数,如果按照此种方式存储,当一个类创建多个对象时,每个对象中都会保存一份代码,相同代码保存多次,浪费空间。
那么如何解决呢?
(2)只保存成员变量,成员函数存放在公共的代码段
问题:对于上述两种存储方式,那计算机到底是按照那种方式来存储的?
我们再通过对下面的不同对象分别获取大小来分析看下。
#include <iostream>
using namespace std;// 类中既有成员变量,又有成员函数
class A1 {
public:void f1() {}
private:int _a;
};// 类中仅有成员函数
class A2 {
public:void f2() {}
};// 类中什么都没有---空类
class A3
{};int main()
{cout << sizeof(A1) << endl;cout << sizeof(A2) << endl;cout << sizeof(A3) << endl;return 0;
}
结论: 可以看到一个类的大小,实际就是该类中“成员变量”之和,所以类对象的存储方式就是采用上面的第二种来进行存储的,当然这里计算类成员的大小和之前计算结构体大小一样,要进行内存对齐。
另外,注意空类的大小,空类比较特殊,比如上面的 A2 只有成员函数,A3 什么都没有,那么编译器就会默认给空类1个字节来唯一标识这个类。
🍎内存对⻬规则
• 第⼀个成员在与结构体偏移量为0的地址处。
• 其他成员变量要对⻬到某个数字(对⻬数)的整数倍的地址处。
• 注意:对⻬数 = 编译器默认的⼀个对⻬数 与 该成员⼤⼩的较⼩值。
• VS中默认的对⻬数为8
• 结构体总⼤⼩为:最⼤对⻬数(所有变量类型最⼤者与默认对⻬参数取最⼩)的整数倍。
• 如果嵌套了结构体的情况,嵌套的结构体对⻬到⾃⼰的最⼤对⻬数的整数倍处,结构体的整体⼤⼩就是所有最⼤对⻬数(含嵌套结构体的对⻬数)的整数倍。
7. this指针
🍎this指针的引出
我们先来定义一个日期类date。
#include <iostream>
using namespace std;class Date
{
public:void Print(){cout << _year << "-" << _month << "-" << _day << endl;}void Init(int year, int month, int day){_year = year;_month = month;_day = day;}
private:int _year; // 年int _month; // 月int _day; // 日
};int main()
{Date d1;d1.Init(2024, 7, 14);Date d2;d2.Init(2024, 7, 13);d1.Print();d2.Print();return 0;
}
运行结果:
对于上述类,有这样的一个问题:
用 Date 类创建了 d1 和 d2 两个对象,并进行了初始化赋值,那么当我们调用 Print 函数去打印的时候,Print函数是如何区分 d1 和 d2 对象的呢?
换句话说,它怎么知道 d1 就是 2024-7-14,而 d2 是 2024-7-13 的呢?
这时候,需要引出 C++ 中一个新的概念:this 指针。
C++ 编译器给每个“非静态的成员函数”增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有成员变量的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。
上述代码调用成员函数传参时,看似只传入了一些基本数据,实际上还传入了指向该对象的指针:
当编译器进行编译时,看到的成员函数实际上也和我们所看到的不一样。
每个成员函数的第一个形参实际上是一个隐含的 this 指针,该指针用于接收调用函数的对象的地址,用 this指针就可以很好地访问到该对象中的成员变量:
🍎this指针的特性
(1) this 指针的类型: 类的类型*const
被 const 修饰以后,this 指针本身不能被修改,但是 this 指针指向的对象可以被修改,还可以进行初始化。
(2)this 指针只能在“成员函数”的内部使用。
(3)this 指针本质上其实是一个成员函数的形参,是对象调用成员函数时,将对象地址作为实参传递给 this形参,所以对象中不存储 this 指针。
(4)this 指针是成员函数第一个隐含的指针形参,一般情况由编译器通过 ecx 寄存器自动传递,不需要用户传递。
如下图所示:
🍎重点一
1.下⾯程序编译运⾏结果是()
A、编译报错 B、运⾏崩溃 C、正常运⾏
#include<iostream>
using namespace std;class A
{
public:void Print(){cout << "A::Print()" << endl;}
private:int _a;
};int main()
{A* p = nullptr;p->Print();return 0;
}
答案是C。
原因是:
这里指针p是一个类对象的空指针,但当执行 p->show()时,程序并不会崩溃,而是正常打印.。
因为对象里面存储的只有成员变量,像 show() 这些成员函数的地址并没有存到对象里面,而是存在公共代码段的。
🍎重点二
2.下⾯程序编译运⾏结果是()
A、编译报错 B、运⾏崩溃 C、正常运⾏
#include<iostream>
using namespace std;class A
{
public:void Print(){cout << "A::Print()" << endl;cout << _a << endl;}
private:int _a;
};int main()
{A* p = nullptr;p->Print();return 0;
}
答案是A。
当程序执行这句代码时,为什么会因为内存的非法访问而崩溃呢?
执行 p->printA()时,调用了成员函数 PrintA,这里并不会产生什么错误(理由同上)。
但是 PrintA 函数中打印了成员变量 _ a,成员变量 _ a 只有通过对 this 指针进行解引用才能访问到,而 this指针此时接收的是 nullptr,对空进行解引用必然会导致程序的崩溃。
🍎总结
this 指针是被作为参数传过去的,所以一般情况下它是存在 栈 上面的。但是有些编译器会使用寄存器进行优化,存放到寄存器的。
空指针实际上在虚拟内存空间中的地址为:0(0x00000000)