【C++】类和对象-上
> 🍃 本系列为初阶C++的内容,如果感兴趣,欢迎订阅🚩
> 🎊个人主页:[小编的个人主页])小编的个人主页
> 🎀 🎉欢迎大家点赞👍收藏⭐文章
> ✌️ 🤞 🤟 🤘 🤙 👈 👉 👆 🖕 👇 ☝️ 👍
目录
🐼前言
🐼类定义
🐼访问限定符
🐼类域
🐼实例化对象
🐼对象的大小
🐼this指针
🐼前言
🌟C语言是一门面向过程的语言,在C语言中,我们用结构体来定义复合数据类型的结构。C++在设计中保留了结构体(struct)类型的优点,C++引入了类,类已经包括了结构体类型的所有功能,并且功能更强,更符合面向对象程序设计的要求,并且安全性,规范性,对象的可交互性也更高。在之后的学习中我们会更深有体会。
🐼类定义
🌟我们通过Date这个日期类来认识类:
#include<iostream> using namespace std; class Date { public: void Init(int year, int month, int day) { _year = year; _month = month; _day = day; } // 为了区分成员变量,一般习惯上成员变量 // 会加一个特殊标识,如_ 或者 m开头 private: int _year; int _month; int _day; };
- ✨在上述的示例中:class为类的关键字,Date为类的名字,{}为类的内容,在类定义后还有一对分号,这点和结构体很相似。
- 类中的变量我们称作属性或成员变量,类中的函数我们称为类的行为或成员函数。
- 在定义成员变量时,我们习惯在成员变量前加_,用来说明是类中的成员变量。
- 注意,类中的函数默认是Inline类型的
- C++兼容C语言中的结构体struct,只不过在C++中,将struct升级成了类,可以在struct中定义函数。struct名称就可以代表类型。不需要在typedef 。
如:
struct ListNode { ListNode* next;//不需要再加struct了 int data; }; int main() { ListNode n1;//ListNode就代表类型 return 0; }
😾解释:再创建对象n1时,我们没有先将struct ListNode typedef,也没有在next指针的类型像C语言中加struct ListNode,因为 ListNode就代表类型,这也是C++方便的一点.
🐼访问限定符
👀Data类的public和private是干嘛的勒?
- ✌️为了程序的规范性,C++采用了一种通过访问权限的选择性供接口给外部用户使用。
- 我们不希望外部用户能随意改变属性(成员变量),关键字protected和private修饰的成员在类外不能直接被访,关键字public修饰的成员只给公共的方法(成员函数)来供外部用户使用,正如我们上述Date类
- 一般成员变量都用private/protected保护,只有给别人用的才用public。
- 注意,如果没有外加需求,在类中的成员变量成员函数都是private修饰的,也就是在外部访问,都是没有权限的。struct默认为public。
如:
class Date { //不加访问限定符默认是private void Init(int year, int month, int day) { _year = year; _month = month; _day = day; } int _year; int _month; int _day; }; struct ListNode { void Init(int x) { next = nullptr; val = x; } ListNode* next; int val; }; int main() { ListNode n1; n1.Init(2);//这里就可以调用,struct默认权限是public Date d1; d1.Init(3,2,1);//不可以从调用,不加访问限定符默认是private return 0; }
编译器报错:“Date::Init”: 无法访问 private 成员(在“Date”类中声明)
- 访问权限符的范围为从当前访问限制符开始,到下一个访问限制符结束。
如第一个Date类从public到private中,Init方法类外能直接被访.下面的成员变量外部不可访问。
C++通过访问限定符,实现了⼀种实现封装的方式,用类将对象的属性与方法结合在⼀块,让对象更加完善。
我们只给外部用户使用我们实现的函数的接口,用户只可以调用我们写好的类中的函数,而不可以访问成员变量,而在C语言中,比如取栈顶元素,我们可以直接访问栈顶元素,arr[top-1],这对于程序的安全性就有很大影响,可能用户一不小心就将我们的数据修改了;
正是由于C语言太自由了,在C++中,访问限定符的限制,就让程序变得更加规范,可维护性也更高,更好的规避了这方面的问题。
🐼类域
⭐️在C++中,存在四个域:局部域,全局域,命名空间域,和类域。
我们之前分享了命名空间域。类域是一个新的域,在类中存在类域,类中的所有成员都存在类域中,在类外定义类中某个成员时,要用到::作用域操作符指明成员属于哪个类域。
当成员函数声明和定义分离时,需要加上域操作符,告诉编译器,当前成员函数在哪个域中,不然编译器会当他为全局变量,但是找不到capacity,top等来自哪里,就会到全局变量域中查找也没有,就会报错。本质上是修改了编译器的查找规则,去类域中查找。
我们如果将成员函数声明和定义分离时,加上域操作符。
如:
//Stack.cpp #define _CRT_SECURE_NO_WARNINGS 1 #include"Stack.h" // 声明和定义分离,需要指定类域 void Stack::Init(int n) { array = (int*)malloc(sizeof(int) * n); if (nullptr == array) { perror("malloc申请空间失败"); return; } capacity = n; top = 0; } //Stack.h #pragma once #include<iostream> #include<assert.h> using namespace std; class Stack { public: void Init(int n = 4); private: // int* array; size_t capacity; size_t top; };
😺解释:
我们只尽兴了类中的成员函数的声明,没有定义。成员函数的定义我们放到了Stack.cpp中来实现,指定类域Stack,就是去Stack中查找Init成员函数,当前域找不到的array等成员,就会到类域中去查找。规则:在函数名前,类名+::
🐼实例化对象
- ⭐️示例化对象是指,创建一个具有当前类特性的物理对象。在上述我们其实已经创建了类对象,比如Date类的d1,d2就是类实例化出的对象。
- 在上述我们定义了类Date,包括成员变量和成员函数,但是这些成员变量,成员函数只是声明,没有分配空间,用类实例化出对象时,才会分配物理空间。比如:我们在建房子时会用图纸,图纸就相当于类,有了图纸,但是我们的房子还没有创建,并没有实体的建筑存在,也不能住人,用设计图修建出房子,房子才能住人。也就是没有根据图纸实例化对象房子。
- 类仅仅是对对象的一种抽象描述。⼀个类可以实例化出多个对象,实例化出的对象占用实际的物理空间,存储类成员变量,同样类就像设计图⼀样,不能存储数据,实例化出的对象分配物理内存存储数据。
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() { // Date类实例化出对象d1和d2 Date d1; Date d2; d1.Init(2024, 3, 31); d1.Print(); d2.Init(2024, 7, 5); d2.Print(); return 0; }
🐼对象的大小
👀在类中,有成员变量和成员函数,我们创建一个类对象,那么这个类对象有多大呢?
值得肯定的是,类实例化一个对象,有独立的数据空间,对象肯定包括成员变量,那么成员函数自已有空间吗,我们可以测试一下。
#include<iostream> using namespace std; class Date { public: void Init(int year, int month, int day) { _year = year; _month = month; _day = day; } // 为了区分成员变量,一般习惯上成员变量 // 会加一个特殊标识,如_ 或者 m开头 int _year; int _month; int _day; }; int main() { Date d1; cout << sizeof(d1) << endl; cout << sizeof(Date) << endl; return 0; }
☀️运行结果:
12
12
- 我们之前学习了结构体的内存对齐规则,如果不懂,可以阅读什么?你还不知道结构体内存对齐规则-CSDN博客这篇文章。
- 我们通过对齐规则,类的大小是12,三个成员变量的大小是12,这说明在内存中,对象没法存储成员函数。
- 首先函数被编译后是⼀段指令,对象中没办法存储,这些指令存储在一个单独的区域(代码段),那么对象中非要存储的话,只能是成员函数的指针。再分析一下,对象中是否有存储指针的必要呢,Date实例化d1和d2两个对象,d1和d2都有各自独立的成员变量 _year/_month/_day存储各自的数据,但是d1和d2的成员函数Init/Print指针却是一样的,存储在对象中就浪费了。如果用Date实例化100个对象,那么成员函数指针就重复存储100次,太浪费了。也就是类中的成员函数是放在公共代码区的,对象没法存储。行为是大家共有的
- 我们再来看一个实例:
class Date { public: }; int main() { Date d1; cout << sizeof(d1) << endl; cout << sizeof(Date) << endl; return 0; }
☀️运行结果: 1 1
上述结果为什么是1呢?👀明明类中没有成员函数和成员变量。
我们反过来思考,如果一个字节都不给,那编译器怎么知道该对象存在过呢?所以,类中没有成员函数和成员变量默认给1个字节用于占位。
🐼this指针
⭐️在C++中,类的成员函数默认会在形参的第一个位置,加上一个当前类型的指针,叫做this指针。那this指针究竟是干嘛用的呢?🔎
来看下面的示例:
#include<iostream> using namespace std; class Date { public: // void Init(Date* const this, int year, int month, int day) void Init(int year, int month, int day) { // 编译报错:error C2106: “=”: 左操作数必须为左值 // this = nullptr; // this->_year = year; _year = year; this->_month = month; this->_day = day; } void Print() { cout << _year << "/" << _month << "/" << _day << endl; } private: // 这⾥只是声明,没有开空间 int _year; int _month; int _day; }; int main() { // Date类实例化出对象d1和d2 Date d1; Date d2; // d1.Init(&d1, 2024, 3, 31); d1.Init(2024, 3, 31); d1.Print(); d2.Init(2024, 7, 5); d2.Print(); return 0; }
🍏在类中,有两个成员函数Init和Print。我们创建了两个对象d1,d2,那当我们调用Init或者Print函数时,编译器如何知道是d1,还是d2调用的函数。其实,C++默认会在形参的第一个位置,加上一个当前类型的指针。Init的形参真正是: void Init(Date* const this, int year, int month, int day).会在第一个形参位置默认加上Date* const this,实参实际上将类对象d1的地址传过去,这不就和我们在C语言结构体传参对结构体变量操作时一样吗。只不过在C++中,编译器已经帮我们完成了这些事情,C++给了 ⼀个隐含的this指针解决这里的问题。这一切都交给了编译器来完成了,变得十分方便。
🍓类中的成员函数访问成员变量,本质上都是通过this指针来访问的。比如给_month,_day赋值。this->_month = month;this->_day = day;
🌽注意,C++规定,在成员函数形参和实参的部分,我们不能手动添加this指针。因为编译器已将帮我们完成了,不过在成员函数内部,可以显示使用this指针,也可以混用。
this指针场景如:
感谢你看到这里,如果觉得本篇文章对你有帮助,点赞👍收藏 ⭐️吧,你的支持就是我更新的最大动力。⛅️🌈 ☀️