C 语言和 C++ 最大的区别就是一个面向过程,一个面向对象。而提到面向对象就不得部提到类,这一篇文章,我们主要探讨一下 C++ 中类的定义以及一些基本的权限。
一、类的引入
二、类的定义
三、访问限定符
3.1 public
3.2 private / protected
四、封装
五、类的大小计算
5.1 类的存储方式
5.2 类的大小的计算方式
5.3 结构体对齐规则
六、this 指针
6.1 this 指针的引入
6.2 this 指针特性
6.3 this 指针为空的情况
C 语言结构体中只能定义变量,而在 C++ 中,结构体兼容了之前的 C ,不仅可以定义变量,也可以定义函数。而为了和 C 语言区分开来,C++ 中更喜欢使用 class 来代替 struct
例如:
#include < iostream> using namespace std; struct Person { int age; void print() { cout << age << endl; } Person* next; }; int main() { Person x; x.age = 2; x.print(); }
同时,由于 C++ 中结构体直接作为类名,可以在结构体内直接定义 Person* next,而在 C 语言中则需要用 struct Person* next。
例如:
class Person { int age; };
class为定义类的关键字,其后跟着的 Person 就是类的名字,{} 中的为类的主体,注意类定义结束时后面分号不能省略。
类的主体中内容称为类的成员:
主体中的变量称为 “ 类的属性 ” 或 “ 成员变量 ” ;
主体中的函数称为 “ 类的方法 ” 或 “ 成员函数 ” 。
类通常有两种定义方式:
1. 声明和定义全部放在类体中,需注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理
例如:
#include
using namespace std; class person { public: int age; void add() { age *= 2; } }; int main() { person x; x.age = 10; x.add(); cout << x.age; return 0; } 需要注意的是,这种情况下,编译器有概率将函数编译为内联函数,如下:
2. 类声明放在头文件中,成员函数定义放在源文件中,注意:成员函数名前需要加类名 --- “ :: ”
例如:
#include
using namespace std; class person { public: int age; void add(); }; void person::add() { age *= 2; } int main() { person x; x.age = 10; x.add(); cout << x.age; return 0; } 在类外定义的时候需要注意,应该在函数名前加上 类名 + ::
通常情况下练习可采用第一种,但是更建议采用第二种方式,将类的声明放在头文件中,函数单独放在一个源文件中。
公有的意思即是所有人都可以使用,都可以访问,通常是类的函数的定义。
例如:
#include
using namespace std; class person { public: int age; void add() { age *= 2; } }; int main() { person x; x.age = 10; x.add(); cout << x.age; return 0; } 对于变量 age 以及函数 add,我们都可以在类外访问到,而对比下面的 privavte 就能理解什么叫做 “ 公有 ”。
在初学阶段,private 和 protected 用法基本相同,因此在此不展开。
若 class 类内没有访问限定符,默认为 private 类型,而 struct 类内默认为 public 类型
私有是指在类外无法访问到 private 修饰的类的成员,通常是变量的定义,直接修改变量的函数的定义等。
例如:
#include
using namespace std; class person { private: int age; public: void get() { age = 2; } void print() { cout << age; } }; int main() { person x; // x.age = 1; x.get(); // cout << x.age; x.print(); return 0; } 上图中注释行试图修改或者读取 age 的时候,就是对 private 修饰变量的一种访问,这种访问在类外无法进行,只能由类内的函数访问,如 x.print() 就可以访问到 age。
我们都知道面向对象有三大特性:封装、继承、多态。今天我们主要谈谈封装
封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来 和对象进行交互。 封装本质上是一种管理,让用户更方便使用类。
通俗来说,封装就是让用户更加规范地使用类,使类内数据更加安全。
例如,我们定义了一个类,其中所有的变量都是 private 限定的,只能通过公有的函数来对变量进行访问,这就是我们对这个类进行封装的实例。
对于一个类而言,这个类的函数都是相同的,那么存储时还有必要给每一个类都创建一个新的函数吗?答案当然是否定的。这些类共用同样的一部分函数即可,有人也许就会想到指针,每个类不用创建新的函数,只需要保存到每一个函数的指针即可。
但是实际上 C++ 使用的类的存储方式是,仅保存成员变量的地址,将类成员函数放在另一片公共的区域,需要使用时直接在对应的区域内找即可。
在计算成员变量的内存空间时,仍旧遵循 C 语言的结构体对齐规则
例如:
#include
using namespace std; class A1 { private: char c; int x; void show(); public: void print(); void add(); }; class A2 { private: void show(); public: void print(); void add(); }; class A3 { }; int main() { cout << sizeof A1 << ' ' << sizeof A2 << ' ' << sizeof A3 << endl; return 0; } 对于 A1 的内存就是遵循 C 语言的结构体对齐规则,对于 A2 和 A3 这类成员变量占内存大小为空的类而言,类的大小为 1,表示该类存在,所占 1 字节并不存储有效数据。
1. 第一个成员在与结构体偏移量为0的地址处。
2. 其他成员变量要对齐到对齐数的整数倍的地址处。 注意:对齐数 = 编译器默认对齐数与该成员变量大小的较小值。VS2022中默认的对齐数为8。
3. 结构体总大小为:对齐过程中的最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。
4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
在上面我们已经知道,对于同一个类型的类而言,成员函数都是在同一块空间,那么编译器是如何识别是哪一个类在调用函数呢?
我们先来看看下面的代码:
#include
using namespace std; class student { private: string _id; string _name; int _age; public: void Inset(string id, string name, int age) { _id = id, _name = name, _age = age; } void print() { cout << _id << endl << _name << endl << _age << endl; } }; int main() { student a; student b; a.Inset("001", "大黄", 19); b.Inset("002", "小黄", 18); a.print(); b.print(); return 0; } 我们可以看到当 studen 类的 a, b 分别调用 print 函数的时候,打印出来的结果并不相同,可是我们并没有任何参数传入,print 函数又是怎么知道是哪一个类在进行调用的呢?答案就是 this 指针,print 函数实际上隐含了一个 this 指针传参,如下:
void print(student* const _this) { cout << _this->_id << endl << _this->_name << endl << _this->_age << endl; }
上图只是一个类似的说法,实际并非完全一样!上图中的 this 指针就是指向当前调用函数的类的一个指针,通过这个指针,函数才能知道应该访问哪一个类的成员变量。需要注意的是,this 指针的定义和传递都是编译器自主实现,用户无法代替编译器定义、传参,但是我们可以对 this 进行使用。
1. this 指针的类型:类类型* c onst,即成员函数中,不能修改 this 指针本身的指向的地址,即不能给 this 指针赋值,但可修改其所指向的地址存储的内容。
2. this 指针只能在 “ 成员函数 ” 的内部使用。
3. this 指针本质上是 “ 成员函数 ” 的形参,当对象调用成员函数时,将对象地址作为实参传递给 this 形参,所以对象中不存储 this 指针,this 指针存储在栈帧之中,部分编译器会进行优化,存储在寄存器之中。
我们先来看看下面两个程序:
#include
using namespace std; class student { private: string _id; string _name; int _age; public: void print() { cout << _id << endl << _name << endl << _age << endl; } }; int main() { student* a = nullptr; a->print(); return 0; } #include
using namespace std; class student { private: string _id; string _name; int _age; public: void print() { cout << "hello world ! " << endl; } }; int main() { student* a = nullptr; a->print(); return 0; } 大家可以先自己判断一下每一个程序的运行结果。
答案是:第一个程序会运行崩溃,第二个程序则正常运行。
也许大家会觉得奇怪,a 不是空指针吗?为什么还可以写 a->print(),这样不就是对空指针进行访问了吗?
实际上不是,我们已经知道类的成员函数实际上不在类内,而是在一片公共区域,因此 a->print() 本质上没有发生解引用,只是将 a 的地址传参到了 this 指针之中。对于第二个程序,函数调用时,没有对 this 指针进行解引用,因此正常运行,但是第一个程序打印成员变量时,是通过对 this 指针的解应用来进行访问的,因此发生了程序崩溃。