本篇介绍一下C++的自定义类型,类和对象。
1.类的定义
1.1 类定义格式
class 为定义类的关键字,Stack为类的名字,类名随便取,{}中为类的主体,类定义结束时后面的分号不可省略。类主体中内容称为类的成员:类中的变量称为类的属性或成员变量;类中函数称为类的方法或成员函数。
与C语言的结构体的定义相似,第一个不同就是类成员还可以是函数,C语言的结构体里面没有函数。比如我们定义一个栈的类。
class Stack { void Push(int x) //成员函数(类的方法) { //... } void Pop() { //... } int top() { //... } int* _a; //成员变量(类的属性) int _top; int _capacity; };
为了区分成员变量,一般习惯在成员变量上加一个特殊标识,如_或者m开头,有的也把_加在成员变量名后面,像int* a_; ,但这并不是强制的。
C++中也可以用struct定义类,C++兼容C中的struct用法,同时struct升级成了类,明显变化是C++的struct中可以定义函数,像下面的日期类,class可以是struct。一般情况下还是推荐用class定义类。
struct Date { public: void Init(int year, int month, int day) { _year = year; _month = month; _day = day; } private: int _year; int _month; int _day; };
在类里面直接定义的函数默认就是内联函数,但是声明和定义分离就不是内联了,比如在类里面声明,在类外面定义。
1.2访问限定符
C++一种实现封装的方式,用类将对象的属性(变量)和方法(函数)结合在一起,让给对象更完善,通过访问权限选择性的将其接口提供给外部的用户使用。
访问限定符有3个:public private protected。public修饰的成员在类外可以直接被访问;protected和private修饰的成员在类外不能直接被访问,protected和private目前是一样的,后面的学习中才能体现出他们的区别。
访问权限作用域从该访问限定符出现的位置开始,到下一个访问限定符出现为止。如果后面没有访问限定符,作用域就到}即类的结束。
class Stack { void Push(int x) //成员函数(类的方法) { //... } public: void Pop() { //... } int top() { //... } private: int* _a; //成员变量(类的属性) int _top; int _capacity; };
class定义成员没有被限定符修饰时默认为private,struct默认为public。一般来说,成员变量都会被限制为private/protected,需要给别人使用的成员函数会被设为public。
第二个不同是类名就是类型,可直接用类名定义对象。比如这里随便弄个结构体来对比一下,类还是用前面那个Stack类举例。
struct S { int a; };
struct S s; //C语言结构体定义结构体变量 Stack st; //类定义对象
类不需要写成class Stack st; 直接Stack st; C语言的结构体就不能丢掉struct。
因为C++兼容C语言,传统的struct在C++可以用,而struct在C++中又升级成了类,所以在C++中,下面两种写法都可以
struct S s; S s;
struct ListNode //C语言链表节点的表示 { int val; struct ListNode* next; }; struct ListNode //C++链表节点的表示 { int val; ListNode* next; };
C语言的表示在C++也能用,C++就有两种表示方法
当我们访问类成员时,跟结构体一样,用点(.)访问。
当我们引出成员变量时发现只有public限制的可以访问,Push没有访问限定符限制,默认为私有,成员变量也是私有。
这里访问限定符,可以重复设置,比如我们想把Push设为公有,可重复使用public,但是一般放在一起,这里只是说明一下可以重复。
class Stack { public: void Push(int x) //成员函数(类的方法) { //... } public: void Pop() { //... } int top() { //... } private: int* _a; //成员变量(类的属性) int _top; int _capacity; };
1.3 类域
类定义了一个新的作用域,类的所有成员都在类的作用域中,在类外定义成员时,需要用::作用域操作符指明成员属于哪个类域。
class Stack { public: void Push(int x); //类内:成员函数的声明 private: int* _a; //成员变量 int _top; int _capacity; }; void Stack::Push(int x) //类外:函数的定义 { //... }
2.实例化
2.1 实例化概念
• 用类类型在物理内存中创建对象的过程,称为类实例化出对象。
• 类是对象进行一种抽象描述,是一个模型一样的东西,限定了类有哪些成员变量,这些成员变量只是声明,没有分配空间,用类实例化出对象时,才会分配空间。
• 一个类可以实例化出多个对象,实例化出的对象占用实际的物理空间,存储类成员变量。
打个比方:类实例化出对象就像现实中使用建筑设计图建造出房子,类就像是设计图,设计图规划了有多少个房间,房间大小功能等,但是并没有实体的建筑存在,也不能住人,用设计图修建出房子,房子才能住人。
同样类就像设计图一样,不能存储数据,实例化出的对象分配物理内存存储数据。
2.2 对象大小
分析一下类对象中哪些成员呢?类实例化出的每个对象,都有独立的数据空间,所以对象中肯定包含成员变量,那么成员函数是否包含呢?
首先函数被编译后是一段指令,对象中没办法存储,这些指令存储在一个单独的区域(代码段),那么对象中非要存储的话,只能是成员函数的指针。再分析一下,对象中是否有存储指针的必要呢,Date实例化d1和d2两个对象,d1和d2都有各自独立的成员变量_year / _month / _day 存储各自的数据,但是d1和d2的成员函数Init /Print指针却是一样的,存储在对象中就浪费了。如果用Date实例化100个对象,那么成员函数指针就重复存储100次,太浪费了。这里需要再额外说一下,其实函数指针是不需要存储的,函数指针是一个地址,调用函数被编译成汇编指令[call地址],其实编译器在编译链接时,就要找到函数的地址,不是在运行时找,只有动态多态是在运行时找,就需要存储函数地址,这个我们以后会讲解。
上面一大段的意思就是对象的大小不计算成员函数,只看成员变量,成员函数会存在一个公共区域。
对象中的成员变量在内存中的存储和C语言时结构体在内存中的存储规则一模一样,要内存对齐,结构体内存对齐详解在【C语言】结构体详解-CSDN博客 ,内存对齐规则以及为什么要内存对齐都在这篇写过,不知道的可以先看结构体这篇。
我们还是举个例子回顾一下。下面这个A类对象有多大?
class A { public: void Ptint() { cout << "void Ptint()" << endl; } private: char _ch; int _i; };
应该是8个字节 ,具体计算如下
我们再来看两个,类B里面只有成员函数,类C里面没有成员,它们大小是多少呢?0吗?
class B { public: void Ptint() { cout << "void Ptint()" << endl; } }; class C { };
我们用类B实例化一个对象b,类C实例化一个对象c,看看它们的大小。
B b; //实例化对象 C c; cout << sizeof(b) << endl; cout << sizeof(c) << endl;
我们可以看到类B和类C对象的大小是1个字节,为什么不是0?因为如果一个字节也不给,怎么表示对象存在过呢?所以就给1个字节,纯粹是为了占位,表示对象存在。
3. this指针
我们先看下面这个时间类
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; };
我现在实例化两个对象d1和d2
Date d1; Date d2;
用这两个对象像下面这样引出里面的函数
d1.Init(2024, 8, 2); d1.Print(); d2.Init(2024, 8, 3); d2.Print();
运行,看结果
思考一下,d1和d2明明执行的是同一个函数,结果为什么不一样? 函数体中没有关于不同对象的区分,那么当d1调用Init函数和Print函数的时候该函数是如何知道访问d1对象还是d2对象?
这里就介绍一个C++给的一个隐含的this指针解决问题。
编译器编译后,类的成员函数默认都会在形参的第一个位置增加一个当前类类型的指针,叫this指针。所以Init的第一个形参并不是year,Print函数也并不是没有参数,前面的Print函数和Init函数实际上是像下面这样。
void Init(Date* const this, int year, int month, int day) { //... } void Print(Date* const this) { //... }
所以传参的时候也会传相应的地址过去。
//d1.Init(&d1, 2024, 8, 2); d1.Init(2024, 8, 2); //d1.Print(&d1) d1.Print(); //d2.Init(&d2, 2024, 8, 3); d2.Init(2024, 8, 3); //d2.Print(&d2) d2.Print();
类的成员函数中访问成员变量,本质是通过this指针访问的。
void Init(int year, int month, int day) { this->_year = year; this->_month = month; this->_day = day; } void Print() { cout << this->_year << "." << this->_month << "." << this->_day << endl; }
C++规定不能在实参和形参的位置显示的写this指针,编译时编译器会处理,但是可以在函数体内显示使用this指针。this指针是不能修改的。
一般this指针存在栈区,因为this指针其实是函数的形参,形参就存放在栈区,有的也会存放在寄存器里面,vs下就是把this指针放在了寄存器里。
这次就分享到到这里,拜拜~