在我们学习过的C语言中,C语言是面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题;而C++是基于面向对象的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成。
类
1. 类的引入
C语言结构体中只能定义成员变量,在C++中,结构体内不仅可以定义成员变量,也可以定义成员函数。比如:之前在数据结构中,用C语言方式实现的栈,结构体中只能定义成员变量;现在以C++方式实现,会发现 struct 中也可以定义成员函数,例如以下代码:
struct Stack { // 初始化 void STInit() {} // 入栈 void STPush(int x) {} int* a; int top; int capacity; };
注意,以上代码中函数的实现并没有实现,实际上我们需要在内部实现;
2. 类的定义
但是实际上在 C++ 中更喜欢用 class 来代替 struct,class为定义类的关键字,类体中内容称为类的成员:类中的变量称为类的属性或成员变量; 类中的函数称为类的方法或者成员函数。
类的两种定义方式:
- 声明和定义全部放在类体中,需注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理。
- 类声明放在 .h 文件中,成员函数定义放在 .cpp 文件中,注意:.cpp 实现的成员函数名前需要加类名,例如以下代码:
.h 文件中放声明:
#pragma once #include <iostream> using namespace std; class Stack { public: // 初始化 void STInit(); // 入栈 void STPush(int x); private: int* a; int top; int capacity; };
.cpp 文件放定义:
#include "Test.h" void Stack::STInit() {} void Stack::STPush(int x) {}
如上面定义的函数前,需要加 Stack::
到指定域去找。
在声明成员变量上,我们要注意命名规则,例如以下有一个日期类,我们要将它初始化:
// 日期类 class Date { public: // 初始化 void Init(int year, int month, int day) { year = year; month = month; day = day; } private: int year; int month; int day; };
在初始化函数中,能区分出来哪个 year 是哪个吗?能被初始化吗?答案都是未知的,所以我们需要注意命名规则,例如在声明前加个 _ ,如以下代码:
// 日期类 class Date { public: // 初始化 void Init(int year, int month, int day) { _year = year; _month = month; _day = day; } private: int _year; int _month; int _day; };
这样看起来就舒服很多了。
3. 类的访问限定符及封装
C++实现封装的方式:用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用。
类的访问限定符包括:public(公有),private(私有),protected(保护)。其中,访问限定符有以下特点:
- public修饰的成员在类外可以直接被访问
- protected 和 private 修饰的成员在类外不能直接被访问(此处protected和private是类似的)
- 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
- 如果后面没有访问限定符,作用域就到 } 即类结束。
- class的默认访问权限为 private,struct 为 public (因为struct要兼容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; };
4. 类的实例化
用类类型创建对象的过程,称为类的实例化。
- 类是对对象进行描述的,是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它。
- 一个类可以实例化出多个对象,实例化出的对象 占用实际的物理空间,存储类成员变量。
在类的成员变量中,只是对它们进行声明,并没有开空间,所以即使将成员变量设为共有,直接使用也会报错的,例如以以上日期类为例:
int main() { Date._year = 2023; // 编译失败:error C2059: 语法错误:“.” return 0; }
此时还没有对类进行实例化,所以正确的用法应该是:
int main() { // 定义开空间,实例化 Date d; d.Init(2023, 7, 20); return 0; }
5. 类对象模型
假设就以上面的日期类为例,我们计算一下这个类究竟有多大:
int main() { // 定义开空间,实例化 Date d; d.Init(2023, 7, 20); cout << sizeof(d) << endl; return 0; }
代码的结果如下:
这个时候我们就要想起以前学过的结构体内存对齐规则,结构体内存对齐 去计算这个类的大小,很明显,成员变量中三个 int 类型就已经是 12 个字节了,那么我们猜想成员函数没有算进类的大小中。注意,这里无论是 sizeof(Date)
还是 sizeof(d)
都是一样的结果。
结果确实是如此的,对于一个类来说,每个类都有自己对应的公共代码区,这个类的所有成员函数都存放在公共代码区,而不是存放在实例化出来的对象中,如果存放在每个对象中,会导致对象变得很大,并且会有很多重复的函数;所以,每个实例化出来的对象,都是在公共代码区中调用相应的函数,这样就相应的节省了类和对象的空间。
那么又有另外一个问题了,如果类里面只有一个成员函数或者空类是不是就没有大小了呢?这两个其实是一个问题,因为成员函数并不存在类中,所以相当于空类,例如以下代码:
// 类中仅有成员函数 class A2 { public: void f2() {} }; // 类中什么都没有---空类 class A3 {};
执行的结果如下:
所以结论是,即使是空类,编译器也会给空类一个字节来唯一标识这个类的对象,不存储数据,只是占位,表示对象存在过。
6. this 指针
6.1 this 指针的概念
我们先简单定义一个日期类 Date :
// 日期类 class Date { public: // 初始化 void Init(int year, int month, int day) { _year = year; _month = month; _day = day; } private: //声明 int _year; int _month; int _day; };
再实例化出来两个对象 d1 和 d2,并对它们进行初始化:
int main() { Date d1; d1.Init(2023, 7, 20); Date d2; d2.Init(2023, 6, 20); return 0; }
那么问题来了,函数体中没有关于不同对象的区分,当我们对 d1 和 d2 对象进行初始化时,d1 调用 Init 函数时,该函数是如何知道应该设置 d1 对象,而不是设置 d2 对象呢?
C++中通过引入 this 指针解决该问题,即:C++ 编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有“成员变量”的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。
例如上面两段代码,等价于以下代码:
这个过程不需要我们显示地去传,编译器会帮我们完成;我们再看成员函数:
我们可以看到,实际上成员函数是通过 this 指针分别对不同的对象进行相应的操作的。
6.2 this 指针的特性
- this 指针的类型:const,即成员函数中,不能给this指针赋值。
- 只能在“成员函数”的内部使用。
- this 指针本质上是“成员函数”的形参,当对象调用成员函数时,将对象地址作为实参传递给 this 形参。所以对象中不存储this指针。this 指针一般存在栈帧中。
- this 指针是“成员函数”第一个隐含的指针形参,一般情况由编译器通过 ecx 寄存器自动传递,不需要用户传递。
那么我们一起看看以下代码的执行结果是什么:
class A { public: void Print() { cout << "Print()" << endl; } private: int _a; }; int main() { A* p = nullptr; p->Print(); return 0; }
大家第一时间反应一定是 nullptr 解引用了,代码会崩溃,这里 p->Print();
等价于 (*p)->Print();
,然而实际上并不是这样的,我们看结果:
代码可以正常运行,在这里,我们首先需要知道一个点,这个成员函数是否存在对象中,根据我们上面类对象模型所学,成员函数并不是存在对象中,它是存在公共代码区中的,而编译器在这里会进行处理,它不会解引用,而是会在公共代码区中查找这个函数的地址,所以这里并不会报错。
而下面这段代码,它仅仅跟上面代码的不同之处是 Print() 成员函数,这段代码它打印的是成员变量 _a ,这个时候 cout << _a << endl;
其实就是 cout << this->_a << endl;
,而 this 指向的对象是空指针,所以这里代码会崩溃。
class A { public: void Print() { cout << _a << endl; } private: int _a; }; int main() { A* p = nullptr; p->Print(); return 0; }
到此,类和对象的上篇就结束啦,到类和对象中篇我会和大家分享类的默认成员函数相关的知识点噢~ 都看到这里啦,点个赞再走呗 ~ 感谢支持 ~