前言:在前面我们带大家初步步入了C++,让大家大概知道了他的样子,那今天就可以说我们要正式步入C++的大门了,这一章内容的细节比较多各位学习的时候一定要仔细。
类和对象
C++中,类是一种自定义的数据类型,用于封装数据和相关的操作。对象是类的实例,通过实例化类来创建对象。
类包含了属性(成员变量)和方法(成员函数),用于描述对象的状态和行为。成员变量是类的数据成员,用于存储对象的数据;成员函数是类的操作成员,用于实现对象的行为。
什么是对象
C语言是面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题,而在C++中C++是基于面向对象的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成,通俗的理解C语言好比送外卖我们需要关注如何下单如何拿,而C++只需要关注如何点外卖。
什么是类
C语言结构体中只能定义变量,在C++中,结构体内不仅可以定义变量,也可以定义函数。比如:之前在数据结构初阶中,用C语言方式实现的栈,结构体中只能定义变量;
定义类的格式:
class 类的名称 { //类体:成员变量--属性 成员函数---功能 }
现在以C++方式实现,会发现struct中也可以定义函数,但在C++中类的定义通过关键字class来实现。以下是一个简单的类的定义示例:
- 声明和定义全部放在类体中,需注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理。
class Date//C++中的定义方式 { public://限定符 void Init(int year,int month,int day) { _year = year; _month = month; _day = day; } void Print() { cout <<"year = " << _year <<" month = " << _month <<" day = " << _day << endl; } //成员变量在这里声明(并未开辟空间)并没有定义,开了空间才是定义 private://限定符 int _year; int _month; int _day; }; struct Date1//C语言的定义方式 { int _year; int _month; int _day; }; int main() { Date dl;//定义类 dl.Init(2024, 1, 30);//在c++中引用类的函数可以直接和C语言一样加.即可 dl.Print(); return 0; }
- 类声明放在.h文件中,成员函数定义放在.cpp文件中,注意:成员函数名前需要加类名
- 这里作者在强调一个点,我们上面这种定义函数的形式是为了平常方便练习,如果我们引用了头文件在头文件定义的话用下面这种形式进行定义
- 类是可以直接引用内部的函数,就像C语言中引用成员变量一样,直接在后面加.即可
//定义放在类的实现文件Person.cpp中 #include "Person.h" void Person::Init(int year,int month,int day){ _year = year; _month = month; _day = day; }
在上面这个例子中我们可以看到,C++中定义的类中我们可以放入函数,而C语言不可以,这个我们在后面马上会讲到,在然后各位肯定看到了这里怎么有个public和private这又是什么东西,那么我们接下来就会给大家讲解这两个是用来干嘛的。
类的访问限定符
- public修饰的成员在类外可以直接被访问
- protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)
- 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
- 如果后面没有访问限定符,作用域就到 } 即类结束。
- class的默认访问权限为private,struct为public(因为struct要兼容C)
- :C++需要兼容C语言,所以C++中struct可以当成结构体使用。另外C++中struct还可以用来定义类。和class定义类是一样的,区别是struct定义的类默认访问权限是public,class定义的类默认访问权限是private。注意:在继承和模板参数列表位置,struct和class也有区别,后序给大家介绍。
封装
封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。(初学阶段我个人理解对这个有个大概的概念即可)
类的作用域
类定义了一个新的作用域,类的所有成员都在类的作用域中。在类体外定义成员时,需要使用 :: 作用域操作符指明成员属于哪个类域,下面我们举了一个例子:
class Date { public: void Init(int year,int month,int day); void Print(); private: int _year; int _month; int _day; }; void Date::Init(int year, int month, int day)//这里需要指定Init是属于Date这个类域 { _year = year; _month = month; _day = day; } void Date::Print()//同理这里需要指定Print属于Date这个域 { cout << "year = " << _year << " month = " << _month << " day = " << _day << endl; } int main() { Date dl; dl.Init(2024, 1, 30); dl.Print(); return 0; }
因此在次强调在类体外定义成员时,需要使用 :: 作用域操作符指明成员属于哪个类域。
类的实例化
用类类型创建对象的过程,称为类的实例化
类是对对象进行描述的,是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没
有分配实际的内存空间来存储它;比如:入学时填写的学生信息表,表格就可以看成是一个类,来描述具体学生信息。类就像谜语一样,对谜底来进行描述,谜底就是谜语的一个实例。谜语:“年纪不大,胡子一把,主人来了,就喊妈妈” 谜底:山羊
- 一个类可以实例化出多个对象,实例化出的对象 占用实际的物理空间,存储类成员变量。
- 做个比方。类实例化出对象就像现实中使用建筑设计图建造出房子,类就像是设计图,只设计出需要什么东西,但是并没有实体的建筑存在,同样类也只是一个设计,实例化出的对象才能实际存储数据,占用物理空间
以下是一些示例代码来演示如何实例化类:
#include <iostream> class MyClass { public: void myMethod() { std::cout << "Hello, World!" << std::endl; } }; int main() { // 实例化类的对象 MyClass obj; // 调用类的方法 obj.myMethod(); return 0; }
类的对象的大小计算
我们来看看下面这段代码,类中既可以有成员变量,又可以有成员函数,那么一个类的对象中包含了什么?如何计算一个类的大小?
class A { public: void PrintA() { cout<<_a<<endl; } private: char _a; };
代码分析:
int main() { cout << sizeof(A) << endl; return 0; }
为什么会是1呢?我们这里就可以猜测一下难到说那个函数是不算类的空间中的大小了嘛?答案是肯定的。那么我们将讲解一下对象的存储方式
对象的存储方式
每个对象只保存成员变量,成员函数指针(指针)存放在公共的代码段保存
- 不同的对象,成员变量是不同的,但是成员函数是完全相同的,都是执行的一模一样的函数代码,此时成员函数就不需要存放在对象中,而应该存放在公共代码区。
- 当对象调用成员函数的时候,是去公共代码区找的,而不是去存放对象的空间里面找。
4.一个类的大小,实际就是该类中”成员变量”之和,当然要注意内存对齐注意空类的大小,空类比较特殊,编译器给了空类一个字节来唯一标识这个类的对象,至于其余的成员变量就和结构体的对齐规则一致如果有不懂的可以看我之前的博客结构体对齐规则。
this指针
this指针的引出
我们来看下面这个代码:
class Date { public: void Init(int year,int month,int day) { _year = year; _month = month; _day = day; } void Print() { cout << "year = " << _year << " month = " << _month << " day = " << _day << endl; } private: int _year; int _month; int _day; }; int main() { Date d1; Date d2; d1.Init(2024, 1, 30); d2.Init(2025, 2, 32); d1.Print(); d2.Print(); return 0; }
上面我们定义了两个对象,那么我们在都调用Init和Print函数的时候,编译器又是如何区分是d1和d2两个对象分别的初始化和打印的呢?
这里我们就要提到一个叫做this指针的概念:
因为C++中通过引入this指针解决了该问题,在C++中,this指针是一个隐含的指针,它指向当前对象的地址。当一个类的成员函数被调用时,编译器会自动地将当前对象的地址作为this指针传递给该成员函数。通过this指针,我们可以在成员函数中访问当前对象的成员变量和成员函数。所以刚刚初始化的代码和打印的代码本质上是这样的:
class Date { public: void Init(int year,int month,int day) { this->_year = year;//本质上是有一个this指针指向了成员变量 this->_month = month; this->_day = day; } void Print() { cout << "year = " << this->_year << " month = " << this->_month << " day = " << this->_day << endl; } private: int _year; int _month; int _day; }; int main() { Date d1; Date d2; d1.Init(&d1,2024, 1, 30);//这里只是为了方便大家理解,才写的取地址,实际写的过程中不需要前面这&d1 d2.Init(&d2,2025, 2, 32); d1.Print(); d2.Print(); return 0; }
接下来我们再来看几道面试的经典题目:
例1:下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行
class A { public: void Print() { cout << "Print()" << endl; } private: int _a; }; int main() { A* p = nullptr; p->Print(); return 0; }
代码分析:
这个主函数我们乍一看怎么p是指向的空指针,空指针又指向了Print这个函数,这里我们第一反应肯定是 这空指针传过去能不报错嘛?我们仔细思考一下,这里本质上虽然是传递了空指针的地址,但是是this指针接收了空指针,为什么会报错?所以这里无非是对象的地址是空而已并不会报错。
运行结果:
我们在来验证一下我们的想法
class A { public: void Print() { assert(NULL); cout << "Print()" << endl; } private: int _a; }; int main() { A* p = nullptr; p->Print(); return 0; }
我们在这里在函数部分判断一下传过来的是否是空指针,运行结果如下,因此原本的程序并不会报错只是传递的是空指针。
例题2:下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行
class A { public: void PrintA() { cout << _a << endl; } private: int _a; }; int main() { A* p = nullptr; p->PrintA(); return 0; }
代码分析:这里我们可以看到和刚刚一样传递了空指针过去,但是这次不同的是我们又要访问成员变量,可是空指针怎么可以访问成员变量呢?空指针是不能解引用的这里我们在C语言的指针和结构体阶段都有过学习,这里就不在过多解释了,因此这个代码是错误的。
这里我们再次对this指针做一个总结:
- this指针的类型:类类型* const,即成员函数中,不能给this指针赋值。
- 只能在“成员函数”的内部使用
- this指针本质上是“成员函数”的形参,当对象调用成员函数时,将对象地址作为实参传递给
this形参。所以对象中不存储this指针。 - this指针是“成员函数”第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递.
好啦,今天的内容就到这里啦,下期内容预告类和对象(二)构造函数、析构函数等等
结语:今天的内容就到这里吧,谢谢各位的观看,如果有讲的不好的地方也请各位多多指出,作者每一条评论都会读的,谢谢各位。