🏖️作者:@malloc不出对象
⛺专栏:C++的学习之路
👦个人简介:一名双非本科院校大二在读的科班编程菜鸟,努力编程只为赶上各位大佬的步伐🙈🙈
目录
- 前言
- 一、面向过程和面向对象初步认识
- 二、类的引入
- 三、类的定义
- 四、类的访问限定符及封装
- 4.1 访问限定符
- 4.2 封装
- 五、类的作用域
- 六、类的实例化
- 七、类对象模型
- 7.1 如何计算类对象的大小
- 7.2结构体内存对齐规则
- 八、this指针的引出
- 8.1 this指针的特性
前言
本篇文章我们将来初步认识一下C++中的类和对象,了解面向对象与面向过程之间有什么区别。
一、面向过程和面向对象初步认识
C语言是面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题。C++是基于面向对象的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成。
二、类的引入
C语言结构体中只能定义变量,在C++中,结构体内不仅可以定义变量,也可以定义函数。
struct Student
{
void Func()
{
cout << "hello world" << endl;
}
char* _name;
int _age;
char _sex;
};
C++ 中的结构体里能定义成员变量(成员属性),也能定义成员函数(成员方法),所以 C++ 的结构体就升级成了类。而 C 语言的结构体只能定义成员变量,其成员函数不能在结构体中定义。
三、类的定义
class Student
{
// 类体:由成员函数和成员变量组成
}; //这个分号一定要记得写
class
为定义类的关键字,Student
为类的名字,{}
中为类的主体。注意类定义结束时后面分号不能省略。
类体中内容称为类的成员:类中的变量称为类的属性或成员变量; 类中的函数称为类的方法或者成员函数。
类的两种定义方式:
- 声明和定义全部放在类体中
注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理。
- 类声明放在.h文件中,成员函数定义放在.cpp文件中。注意:成员函数名前需要加类名::表示成员函数在哪个类域中。
四、类的访问限定符及封装
4.1 访问限定符
C++ 实现封装的方式:用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用。
【访问限定符说明】
- public修饰的成员在类外可以直接被访问
- protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)
- 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
- 如果后面没有访问限定符,作用域就到 } 即类结束。
- class的默认访问权限为private,struct为public(因为struct要兼容C)
在简单了解访问限定符之后,我想问大家一个问题既然C++中的结构体升级为类了,那么它与class
之间有什么区别?
C++ 需要兼容 C 语言,所以 C++ 中struct可以当成结构体使用。另外 C++ 中 struct 还可以用来定义类。和 class 定义类是一样的,区别是 struct 定义的类默认访问权限是 public,class 定义的类默认访问权限是 private。注意:在继承和模板参数列表位置,struct 和 class 也有区别,后序给大家介绍。
注意:访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别。
4.2 封装
面向对象的三大特性:封装、继承、多态。
在类和对象阶段,主要是研究类的封装特性,那什么是封装呢?
封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。
封装本质上是一种管理,让用户更方便使用类。比如:对于电脑这样一个复杂的设备,提供给用户的就只有开关机键、通过键盘输入,显示器,USB插孔等,让用户和计算机进行交互,完成日常事务。但实际上电脑真正工作的却是CPU、显卡、内存等一些硬件元件。对于计算机使用者而言,不用关心内部核心部件,比如主板上线路是如何布局的,CPU内部是如 何设计的等,用户只需要知道,怎么开机、怎么通过键盘和鼠标与计算机进行交互即可。因此计算机厂商在出厂时,在外部套上壳子,将内部实现细节隐藏起来,仅仅对外提供开关机、鼠标以 及键盘插孔等,让用户可以与计算机进行交互即可。
在 C++ 中实现封装,可以通过类将数据以及操作数据的方法进行有机结合,通过访问权限来隐藏对象内部实现细节,控制哪些方法可以在类外部直接被使用。
五、类的作用域
类定义了一个新的作用域,类的所有成员都在类的作用域中。在类体外定义成员时,需要使用 ::作用域操作符指明成员属于哪个类域。
// Person.h
class Person
{
public:
void PrintPersonInfo();
private:
char _name[20];
char _gender[3];
int _age;
};
// Person.cpp
#include "Person.h"
// 这里需要指定PrintPersonInfo是属于Person这个类域
void Person::PrintPersonInfo()
{
cout << _name << " "<< _gender << " " << _age << endl;
}
六、类的实例化
1.类是对对象进行描述的,是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它;比如:入学时填写的学生信息表,表格就可以看成是一个类,来描述具体学生信息。
2.一个类可以实例化出多个对象,实例化出的对象占用实际的物理空间,存储类成员变量。
3.做个比方。类实例化出对象就像现实中使用建筑设计图建造出房子,类就像是设计图,只设计出需要什么东西,但是并没有实体的建筑存在,同样类也只是一个设计,实例化出的对象才能实际存储数据,占用物理空间。
用类类型创建对象的过程,称为类的实例化。简单来说,类的的实例化就是为对象实际开辟了空间。
下面来看几个常见的例子:
#include<iostream>
using namespace std;
class Person
{
public:
void showInfo();
// 以下在类中的成员变量都是声明,因为它们都没有开辟空间,只有当类实例化对象时它们才有存储空间
const char* _name;
const char* _sex;
int _age;
};
int main()
{
//Person 类是没有空间的,只有 Person 类实例化出的对象才有具体的年龄。
// Person._age = 10; 不行
//Person::_age = 10; 不行
Person man; // 实例化一个对象man,此时成员变量有实际的空间了
man._age = 10;
man._name = "张三";
man._sex = "男";
cout << man._age << " " << man._name << " " << man._sex << endl;
return 0;
}
七、类对象模型
7.1 如何计算类对象的大小
大家可以试着先计算一下这个类对象的大小
class A
{
public:
void PrintA()
{
cout<<_a<<endl;
}
private:
char _a;
};
问题:类中既可以有成员变量,又可以有成员函数,那么一个类的对象中包含了什么?如何计算一个类的大小?
计算类对象跟我们之前讲过的结构体大小一样都是要考虑内存对齐的,sizeof(A)等于 1 说明该类只存储了成员变量_a,成员函数没有计算进来。
为什么成员变量在对象中,成员函数不在对象中?
因为每个对象的成员变量是不一样的,它们都在各自对象的存储空间当中;而每个对象调用成员函数是一样的,所以我们不需要将成员函数加入到每个对象的存储空间当中,这样会极大的浪费我们的空间,因此我们的成员函数不放在对象中,而是放到共享公共区域(代码段)。
7.2结构体内存对齐规则
1.第一个成员在与结构体偏移量为0的地址处。
2.其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
注意:对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。VS中默认的对齐数为8.
3.结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。
4.如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
类对象的大小跟之前我们在C语言中讲的结构体内存对齐规则是一样的,这里我就不做过多的介绍了,下面我们一起来看看特殊的例子:
class A1 {}; //空类
class A2
{
void f2(){}
};
int main()
{
cout << sizeof(A1) << endl;
cout << sizeof(A2) << endl;
return 0;
}
我们来进行分析一下,A1类是一个空类,A2类中只有一个成员函数,在上面我们分析过成员函数是不被计算在内的,所以A1和A2类其实在大小上两者是相等的,所以它们的大小都为0?我们来看看结果:
我们发现A1和A2类的大小都为1,这是为何?我们来看看下面的例子:
从上图我们发现A1和A2类都实例化了一个对象,那么假设它们的大小为0的话,它们的对象怎么可能会有对应的地址?那么这个1又是怎么计算出来的呢?
这一个字节不存储有效数据,它是用来占位标识对象被实例化定义出来了!!
结论:没有成员变量的类占1个字节大小,这1个字节用来占位标识对象被实例化定义出来了。
八、this指针的引出
我们先来看一个例子:
#include <iostream>
using namespace std;
class Date
{
public:
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;
Date d2;
d1.Init(2022, 11, 11);
d2.Init(2023, 2, 24);
return 0;
}
在上述内容中我们已经得知成员函数它是被所有对象所共享的,那么在这个例子中我想请问我们是如何知道是哪个对象调用的成员函数,有的读者说在main函数中这不是清清楚楚的写着d1、d2调用Init函数吗?在main函数中调用Init函数传递相同的参数,那么在Init函数内部你知道year、month以及day是给哪个对象的成员变量赋值吗?
这里其实C++在这里做了一个隐式的处理,使用了this指针使得我们清楚的知道此时是对哪个对象做的操作:
编译器在编译之后隐式的帮我们进行了处理,在主函数中d1对象调用成员函数就传入d1对象的地址,d2对象调用成员函数就传入d2对象的地址,在Init函数内部用一个指针函数this接收,这样就能知道此时year、month以及day是为哪个对象赋值了。这是编译隐式的帮我们进行了处理,我们不可显式的进行这样的处理,因为这是编译干的活,但是我们可以在成员函数内部显式的使用this:
这里额外的给大家讲一个关于成员变量命名风格的问题,我们来看看下面这个例子:
#include <iostream>
using namespace std;
class Date
{
public:
void Init(int year, int month, int day)
{
year = year;
month = month;
day = day;
}
int year;
int month;
int day;
};
int main()
{
Date d1;
Date d2;
d1.Init(2022, 11, 11);
d2.Init(2023, 2, 24);
cout << d1.year << " " << d1.month << " " << d1.day << endl;
cout << d2.year << " " << d2.month << " " << d2.day << endl;;
return 0;
}
大家想一想这里的结果会是多少?
为什么这里成员变量赋值未成功呢?
这是因为我们在成员函数内部局部变量优先被使用,成员函数形参接收到主函数传递过来的实参又被赋给了形参,所以我们的成员变量根本没有被赋值,因此我们看到打印的都是随机值。
第一种解决方案:使用this
指针显式说明此时的变量为对象的成员变量。
关于这点也许有些读者还有一个疑问,既然之前讲过编译器在编译之后会隐式的使用this
指针对成员变量进行指示,那么这里为什么会赋值不成功呢?
这其实就是语法规定了我们的成员变量与形参原则上是不要同名的,所以这里我们需要显式的使用
this
指针指明对象。
第二种解决方案:重命名成员变量的名称。
相较于第一种解决方案,我更喜欢这一种解决方案,这样可以避免频繁的使用this显示的指明对象。
关于成员变量的命名有很多中,目前来说我更喜欢使用前缀_ + 单词的命名方式,当然了自己喜欢使用哪种就用哪种即可。
8.1 this指针的特性
1.this指针的类型:类类型* const,即成员函数中,不能给this指针赋值。
2.只能在“成员函数”的内部使用
3.this指针本质上是“成员函数”的形参,当对象调用成员函数时,将对象地址作为实参传递给
this形参。所以对象中不存储this指针。
4.this指针是“成员函数”第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传
递,不需要用户传递
下面我们来看两个经典的面试题
Q1:this指针存在哪里?
this指针一般是存在栈上的,因为它本身就是一个隐含形参。在VS下编译器做了一定的优化处理,它认为this指针是频繁使用的,所以一般将它放在寄存器中进行传递。
我们可以在VS下简单的查看一下汇编代码:
Q2:this指针可以为空吗?
我们来看一个例子,下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行
#include <iostream>
using namespace std;
class A
{
public:
void Func1()
{
cout << "Func1" << endl;
}
void Func2()
{
cout << _a << endl;
}
int _a;
};
int main()
{
A* ptr = nullptr;
ptr->Func1();
ptr->Func2();
return 0;
}
我们先来看看结果:
在看到这个结果之后想必很多读者会露出不可思议的表情,ptr是一个空指针,那么它用来调用这个成员函数不应该直接会发生程序崩溃吗?那为什么调用第一个成员函数竟然还成功的得出了答案呢?而调用第二个成员函数时却发生了崩溃呢?
看到这种看上去很简单的题我们一定要保持警惕,说不定就有大坑等着你呢!!在这道题中我们一定要联系类的特性来做,我们知道成员函数不是在对象中的,它是共享的放在代码段的,此时调用成员函数利用不是ptr解引用找到这个函数的,而是利用call指令找到这个函数的地址。说的再通俗一点ptr此时没有进行解引用操作,它的作用仅仅是作为实参将地址隐式的传递给this指针,所以此时成员函数中this指针为空,但在Func1成员函数中我们并没有使用this指针进行解引用操作,所以此时运行正常;而对于Func2成员函数来说,它想要打印成员变量_a,实际上编译器隐式的进行了处理用this指针指明_a所在的对象,cout << _a << endl; ==> cout << this->_a << endl; 空指针进行解引用于是引发了程序崩溃。
同理,写成(*ptr).Func1();这样也是可以正常运行的,我们不要表面上看见*ptr就是对ptr进行解引用操作了,实际上成员函数根本不在对象中。我们通过汇编代码进一步验证一下:
结论:有没有解引用的行为取决于访问右边的东西在不在对象里面,而不是用没用*和->这个符号。
本篇文章的内容就到这儿了难度不是很大,要求重点掌握this指针的细节。如果文章有任何问题或者错处,欢迎大家评论区相互交流啊orz~🙈🙈