【C++】类和对象——(上)
前言:结束了C++入门的学习紧接着就步入到了C++类和对象的学习,类和对象比C++入门更具有挑战性,类和对象相比C语言就像是进入了一个完全不同的世界,让我们一起探索一下c++类和对象的奥妙。
C++系列订阅请点击:C++专栏
文章目录
- 一,类
- 1.1类的概念
- 1.2类的定义
- 1.3访问限定符
- 1.4类域
- 二,类的实例化
- 2.1实例化
- 2.2对象的大小
- 三,this指针
- 3.1this指针的认识
- 四,封装
- 4.1面向对象封装的基本认识
一,类
1.1类的概念
C++ 中的类(Class)是一种用户自定义的数据类型,用于封装数据(成员变量)和操作数据的方法(成员函数)。类是面向对象编程(OOP)的核心,支持封装、继承和多态等特性。
1.2类的定义
- class为定义类的关键字,Stack为类的名字,{}中为类的主体,注意类定义结束时后面分号不能省略。类体中内容称为类的成员:类中的变量称为类的属性或员变量; 类中的函数称为类的方法或者成员函数。
类的定义与C语言的结构体的定义类似,我们先来看看C语言结构体的定义:
struct A
{//定义各种变量或指针int* arr;int b;//在结构体中嵌套定义结构体struct A B;
};
下面我们来看看类的定义,这里举一个之前数据结构的例子:
class stack
{
//成员函数
public:void Init(int n=4){arr=(int*)malloc(sizeof(int)*n);if(arr==nullptr){perror("malloc申请空间失败");return;} capacity = n;top = 0;}void push_back(int x){st.arr[_size++]=x;}
private://成员变量 习惯以下划线_ 开头用于区分类里的变量与成员函数的形参int* arr;int _size;int _capacity;
};
这就是C++定义的一个数据结构栈的类,观察他和上面结构体的区别:
类以class定义,结构体以struct定义;类的内部有成员变量,成员函数,还可以嵌套定义类。而C语言的结构体内部只能定义变量。类还多出了
public
h和private
这样的访问限定符这个后面会介绍。
注意:类里的成员变量一般以下划线_开头,在赋值等操作的时候用于区成员变量与成员函数的形参同名的情况!
所以从本质上来说类就是C++对C语言的结构体进行了一个升级,所以如果在C++中定义了一个结构体那么它将被默认的升级成为类!比如:
// C++兼容C中struct的⽤法
//在C语言中我们的链表是这样定义的
typedef struct ListNode
{struct ListNode* next;//这里指针的类型可以不用sturct 因为struct被升级成为了类 类名就是类型//ListNode*next;int val;
}LTNode;
注意C++是兼容C语言的用法的,但是在C++中定义了结构体后它就会被升级成为类。那么在使用结构体的时候就不用在加
struct
这个关键字了,因为struct
升级成了类,类名就是类型!
注意定义在类里面的函数默认为内联函数,也就是类里面的函数默认是展开的!
不了解内联函数的请点击:内联函数
1.3访问限定符
class A
{
public:
//成员函数int add(int x,in y){return x+y;}
private:
//成员变量int _a;int _b;
};
上面定义了一个简单的类,这个类中
public
,private
就是访问限定符。public
是公共的意思,所以被public
修饰的成员(不管是变量还是函数)在类外面都是可以访问到的;而private
是私人的意思就是不能被类外面访问只能在类里面被访问!
- 另外访问限定符其实有三个,这里少了一个
protect
被它修饰的成员(无论是函数还是变量)跟被private修饰的一样只能在类里边被访问;要弄清楚它们三者的关系要到后面学习了继承章节才能理解,这里先简单认识一下访问限定符的作用功能。
- 注意:类中的成员变量或成员函数如果没有被访问限定符修饰那么默认就是私有
private
;如果是结构体那么默认就是公有public
。
到这里可能有人会问那么访问限定符的意义是什么呢?
访问限定符用于控制类、方法或变量的可见性和访问权限,确保代码的封装性和安全性。通过合理使用访问限定符,可以限制其他类或模块对特定成员的访问,避免数据被意外修改或滥用。
1.4类域
在之前C++入门的时候我们就说过C++有4个域,局部域,全局域,命名空间域和类域。前三个域我们都已经有了基本的认识,那么今天就来介绍一下类域。
下面我们来定义一个类:
在定义成员函数的时候有两种一种是声明和定义分离,一种是直接将函数定义在类里面。
#include<iostream>
using namespace std;
class Stack
{
public:
// 成员函数//第一种直接定义在类里面void Init(int n=4){arr=(int*)malloc(sizeof(int)*n);if(arr==nullptr){perror("malloc申请空间失败");return;} }
private:// 成员变量int* array;size_t capacity;size_t top;
}
第二种:声明和定义分离
#include<iostream>
using namespace std;
class Stack
{
public:
// 成员函数//第二种在类里面声明 定义在其他文件 或者类外面void Init(int n=4);
private:// 成员变量int* array;size_t capacity;size_t top;
};
//在类外面定义就要加上作用域解析运算符:: 指定类域
void stack::Init(int n=4)
{arr=(int*)malloc(sizeof(int)*n);if(arr==nullptr){perror("malloc申请空间失败");return;}
}
在上面的代码中我们看到了两种类的成员函数定义的方式,一种是定义在类里面;一种是定义在类外面,定义在类外面的要加上作用域操作符
::
。
假如我们不加::
操作符那么编译器就会把Init函数当成全局函数,那么在编译时就会找不到arr等成员的声明在哪里就会报错。
- 由此我们认识到:类定义了⼀个新的作用域。类的所有成员都在类的作用域中,在类体外定义成员时,需要使⽤
::
作⽤域操作符指明成员属于哪个类域。 - 类域既然是域那就有解决命名冲突的功能,命名空间域是解决全局、局部的命名冲突,那么类域肯定就是解决类与类之间的命名冲突了。
class stack
{
public:void push(int x);
private:int _top;int _capacity;
};class queue
{
public:void push(int x);
};
上面的代码中两个同名函数
push
之所以能够存在且没有冲突的原因是它们被两个不同的类分隔开了,所以类域可以解决类和类之间的命名冲突。
二,类的实例化
2.1实例化
概念:类实例化是指通过类创建对象的过程。类定义了对象的属性和方法,实例化则是根据类的定义生成具体的对象实例。每个实例拥有独立的属性和方法,但共享类的行为定义。
举一个生活中的例子:
- 类(Class):相当于饼干模具。模具定义了饼干的形状(如圆形、星形)、厚度等属性,但模具本身不是饼干。
- 实例化:用模具压出具体的饼干。每块饼干(实例)拥有模具定义的形状,但可能带有不同的装饰(属性值),比如饼干的颜色,巧克力豆的数量不同。
- 实例:每块独立的饼干就是类实例化的体现,它们共享模具的特征,但各自是独立存在的实体。
这里用一个简单的日期类来举例:
#include<iostream>
using namespace std;
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;Date d2;d1.Init(2025, 6, 18);d2.Init(2015, 6, 18);d1.Print();d2.Print();
}
在上面的代码中,我们创建了一个日期类,而我们在main函数里创建的d1和d2就是类实例化出来的对象,创建d1和d2的过程就是实例化的过程。
2.2对象的大小
上面我们已经实例化好了两个对象,那么我们如何去计算我们创建出来的对象的大小呢?
计算对象的大小就像C语言计算结构体的大小一样要使用内存对齐的规则去计算,如果不了解内存对齐,或者不知道如何计算的读者可以看我之前介绍结构体内存对齐的文章这里就不再赘述。传送门:内存对齐
下面就来计算一下
class Date
{
public:void Init(int year, int month, int day){_year = year;_month = month;_day = day;}
private:int _year;int _month;int _day;
};class A
{
public:void Init(int year, int month, int day){}
};class B
{
};
int main()
{//分别计算Date,A,B类创建的对象的大小Date d;A a;B b;cout << sizeof(d) << endl;//打印得到的结果为12cout << sizeof(a) << endl;//打印得到的结果为1cout << sizeof(b) << endl;//打印得到的结果为1
}
看到运行结果有些人就可能开始有疑问了,第一:Date类创建的对象大小根据内存对齐的规则计算出来的为什么不是16而是12?第二对象b什么都没有定义为什么大小不是0而是1?
对于第一个问题,如果我们认为Darte创建的对象d的大小为16那么就是将类里面的函数地址(指针)也计算到对象里面去了,但是打印出来的结果是12那么就说明函数的地址没有被存在对象中,为什么呢?d1和d2除了内部的值不同函数的指针其实都一样,如果有很多个实例化的对象,对象里面都存有相同函数的地址那么就不可避免的造成了空间浪费。所以计算对象大小时成员函数的指针不计算在内,如果该函数有定义那么编译的时候就去call函数的地址,如果只有声明那就链接的时候再去call地址! 成员函数就相当于存在代码的公共区域,每一个对象都可以共享这个函数。
对于第二个问题,为什么只有一个成员函数和什么都不定义的类创建的对象大小为1呢?这其实就是一个占位标识表示对象存在过,如果一个字节的空间都不给那么怎么表示对象存在呢?相反只有一个成员函数的类创建的对象大小为一也验证了成员函数的指针不存在对象中!!!
三,this指针
3.1this指针的认识
先来看一段代码:
//还是上面的代码
int main()
{Date d1;Date d2;d1.Print();d2.Print();//为什么不把对象的地址传过去呢?//d1.Print(&d1);//d2.Print(&d2);
}
上面我们看到C++在调用类里面的函数为什么不用传对象地址,也能调用对应的函数呢?为什么不像C语言那样传对象的地址呢?这就是接下来要介绍的
this指针了
。
- 在调用类里面的函数是肯定要传对应对象的地址才能打印出不同对象所存储的值,只是C++中隐含存在了一个
this指针
接收对象的值,且编译器会自动的传参不需要我们人为的传参,下面画图来让大家更直观的认识:
- 注意我们不能人为去传参和修改成员函数的形参,这是编译器干的活。
- 可能会有人对this指针的存在性有疑问,下面我们就来使用一下
this指针
,注意this指针
只能在成员函数里使用
#include<iostream>
using namespace std;
class Date
{
public:void Init(int year, int month, int day){_year = year;_month = month;_day = day;} void Print(){//在调用print函数前先打印一些this指针cout<<"this="<<this<<endl;//this=nullptr this指针不允许修改cout << _year << "/" << _month << "/" << _day << endl;}
private:// 这⾥只是声明,没有开空间int _year;int _month;int _day;
};
int main()
{Date d1;Date d2;//打印d1和d2的地址cout<<"&d1="<<&d1<<endl;cout<<"&d2="<<&d2<<endl<<endl;d1.Init(2025, 6, 17);d2.Init(2015, 6, 18);d1.Print();d2.Print();
}
运行结果很好的向我们说明了
this指针
就如我们前面所说的那样:this指针
是隐藏存在的它接收不同对象的地址,在函数内本质都是this指针在调用成员变量,且this指针能在成员函数内使用,但不能修改!
最后再来几个小题巩固一下上面所学的知识:
到这里可能也会有人好奇this指针到底是存在哪的?我们打开反汇编代码:
这里我们看到每次函数调用都会使用
ecx
,所以this指针
一般使用ecx寄存器
传递所以this指针
存在寄存器中,同时this指针
是在函数内调用成员变量的因此它也存在函数栈帧中!
四,封装
4.1面向对象封装的基本认识
- 封装是面向对象编程(OOP)的三大核心特性之一,指将数据(属性)和操作数据的方法(行为)捆绑为一个独立单元(即类或对象),并对外隐藏内部实现细节。
+-----------------------+
| Person |
|-----------------------|
| - name: string |
| - age: int |
|-----------------------|
| + setName(string) |
| + getName(): string |
| + setAge(int) |
| + getAge(): int |
+-----------------------+
外部:只能通过公共方法(如 setName)与类交互。
内部:私有成员(如 name)被“黑箱”保护。
- 在前面我们说类是结构体的一个升级,类相比结构体能在内部定义成员函数,使用访问限定符限定哪些可以访问,哪些不能被访问,以及能提高代码的可维护性,内部逻辑的修改影响外部的调用,这些就体现了C++的封装,当然封装不仅仅是这样的通过后面的学习我们才能逐步的体会。
以上就是本章的全部内容啦!
最后感谢能够看到这里的读者,如果我的文章能够帮到你那我甚是荣幸,文章有任何问题都欢迎指出!制作不易还望给一个免费的三连,你们的支持就是我最大的动力!