前言
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<iostream> 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<iostream> 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; }
在类外定义的时候需要注意,应该在函数名前加上 类名 + ::
通常情况下练习可采用第一种,但是更建议采用第二种方式,将类的声明放在头文件中,函数单独放在一个源文件中。
三、访问限定符
3.1 public
公有的意思即是所有人都可以使用,都可以访问,通常是类的函数的定义。
例如:
#include<iostream> 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 就能理解什么叫做 “ 公有 ”。
3.2 private / protected
在初学阶段,private 和 protected 用法基本相同,因此在此不展开。
若 class 类内没有访问限定符,默认为 private 类型,而 struct 类内默认为 public 类型
私有是指在类外无法访问到 private 修饰的类的成员,通常是变量的定义,直接修改变量的函数的定义等。
例如:
#include<iostream> 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 限定的,只能通过公有的函数来对变量进行访问,这就是我们对这个类进行封装的实例。
五、类的大小计算
5.1 类的存储方式
对于一个类而言,这个类的函数都是相同的,那么存储时还有必要给每一个类都创建一个新的函数吗?答案当然是否定的。这些类共用同样的一部分函数即可,有人也许就会想到指针,每个类不用创建新的函数,只需要保存到每一个函数的指针即可。
但是实际上 C++ 使用的类的存储方式是,仅保存成员变量的地址,将类成员函数放在另一片公共的区域,需要使用时直接在对应的区域内找即可。
5.2 类的大小的计算方式
在计算成员变量的内存空间时,仍旧遵循 C 语言的结构体对齐规则
例如:
#include<iostream> 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 字节并不存储有效数据。
5.3 结构体对齐规则
1. 第一个成员在与结构体偏移量为0的地址处。
2. 其他成员变量要对齐到对齐数的整数倍的地址处。 注意:对齐数 = 编译器默认对齐数与该成员变量大小的较小值。VS2022中默认的对齐数为8。
3. 结构体总大小为:对齐过程中的最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。
4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
六、this 指针
6.1 this 指针的引入
在上面我们已经知道,对于同一个类型的类而言,成员函数都是在同一块空间,那么编译器是如何识别是哪一个类在调用函数呢?
我们先来看看下面的代码:
#include<iostream> 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 进行使用。
6.2 this 指针特性
1. this 指针的类型:类类型* c onst,即成员函数中,不能修改 this 指针本身的指向的地址,即不能给 this 指针赋值,但可修改其所指向的地址存储的内容。
2. this 指针只能在 “ 成员函数 ” 的内部使用。
3. this 指针本质上是 “ 成员函数 ” 的形参,当对象调用成员函数时,将对象地址作为实参传递给 this 形参,所以对象中不存储 this 指针,this 指针存储在栈帧之中,部分编译器会进行优化,存储在寄存器之中。
6.3 this 指针为空的情况
我们先来看看下面两个程序:
#include<iostream> 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<iostream> 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 指针的解应用来进行访问的,因此发生了程序崩溃。