C++
2022 年 6 月 18 日
[C++] 类和对象(1): 认识类、封装特性、隐含的this指针...
C++ 中的类其实与 C语言 中的结构体类似。不过, C++将C语言 中的 struct 进行了升级, 在C++中 struct 可以用来定义类
一、类
C++ 中的类其实与 C语言 中的结构体类似。不过, C++ 将 C语言 中的
struct
进行了升级, 在C++中 struct
可以用来定义类C++ 升级了
struct
, 使其定义的结构体:- 结构体内部可以定义函数
- 结构体名可以直接作为类型使用
struct User
{
void Init(const char* name, const char* sex, const char* tele, int age)
{
strcpy(_name, name);
strcpy(_sex, sex);
strcpy(_tele, tele);
_age = age;
}
void Print()
{
cout << _name << " " << _sex << " " << _tele << " " << _age << endl;
}
char _name[20];
char _sex[10];
char _tele[20];
int _age;
};
类似这样的结构体, 在 C++ 中是合理合法的, 函数是可以被定义在结构体内部的。
并且在定义变量时, 可以直接使用
User
不用再添加 struct
结构体内的函数也可以用使用结构体变量的方式调用。不过, 在 C++ 中定义结构体通常用
class
而不是 struct
;定义出的类型也通常被称为 类
, 而不是 结构体;使用类型定义的变量也不再称为变量了, 而是 对象
.上图简单的将 由
struct定义的结构体
改为了 由class定义的类
, 并进行了一定的修改类的定义结构体的定义究竟有何不同?
二、类的定义
类的定义其实与结构体的定义相似, 将
struct
改为了 class
其实经过C++ 升级过后的
struct
与 class
, 基本上是一样的
不同的地方在于 默认的访问权限2.1 public、private
C++中的 结构体和类的内部 其实都设置有访问权限。但是结构体与类 默认的访问权限 略有不同。
结构体内部成员默认是
公有的(public)
;而类内部成员默认是 私有的(private)
;而 public、private 被称为访问限定符
public修饰的成员 和 private修饰的成员 区别是:
公有(public) : 公有成员可以在外部直接进行访问
私有(private) : 私有成员不能在外部直接进行访问, 但是可以通过公有成员间接进行访问
除公有、私有之外, 还存在一个访问限定符 : 保护(protected)。现阶段不过多介绍, 可将其看为 与private 相同
在上边介绍
class
时, 其实就已经使用过 public
和 private
但是访问限定符具体有什么作用呢?
以上图中的
User 类
class User
{
public:
void Init(const char* name, const char* sex, const char* tele, int age)
{
strcpy(_name, name);
strcpy(_sex, sex);
strcpy(_tele, tele);
_age = age;
}
void Print()
{
cout << _name << " " << _sex << " " << _tele << " " << _age << endl;
}
private:
char _name[20];
char _sex[10];
char _tele[20];
int _age;
};
在此类中, public 修饰两个成员函数, private 修饰四个成员变量。就表示, 在类外不能直接访问成员变量
无法用
u1._age
来直接访问 对象u1 中的 _age 变量
但是因为 private修饰的成员 只是
无法在类外进行访问, 在类内依旧是可以进行访问的
, 所以可以通过 public修饰的成员对private成员进行间接的访问:有一个细节需要注意的是, 访问限定符只在编译时起作用, 当编译完成, 数据映射至内存时就已经不存在什么公有成员、私有成员的概念了
2.2 类的封装特性
众所周知, C++ 是一种面向对象的编程语言。面向对象, 有三大基本特性:
封装
、继承、多态使用C++类, 可以更好的将数据与操作封装结合起来。而 C语言的结构体不可以。
比如在 之前文章中用C语言分析实现过的数据结构:
栈
C语言 结构体实现栈:
使用 C语言 实现的栈, 其结构与操作接口是分离的。而使用 C++ 的类进行对栈的实现, 就完全可以将操作接口也写入类中
C++ 类实现栈:
#include <iostream>
#include <cstdlib>
#include <cassert>
using namespace std;
class Stack
{
public:
// 栈的初始化
void StackInit()
{
_data = nullptr;
_top = 0; // 栈顶初始位置定义 (top = -1, 先++后赋值; top = 0, 先赋值后++)
_capacity = 0;
}
// 栈的销毁
void StackDestroy()
{
free(_data);
_data = nullptr;
_top = _capacity = 0;
}
// 压栈
void StackPush(int x)
{
if (_top == _capacity)
{
int newCapacity = _capacity == 0 ? 4 : _capacity * 2;
int* tmp = (int*)realloc(_data, newCapacity * sizeof(int));
if (tmp == nullptr)
{
printf("realloc failed\n");
exit(-1);
}
else
_data = tmp;
_capacity = newCapacity;
}
_data[_top++] = x;
}
// 出栈
void StackPop()
{
assert(_top > 0);
--_top;
}
// 判空
bool StackEmpty()
{
return _top == 0; // top 等于 0 栈为空, 返回 true; 不等于 0 , 返回 false
}
// 栈顶数据
int StackTop()
{
return _data[_top - 1];
}
private:
int *_data;
int _top; // 栈顶位置
int _capacity; // 栈的容量
};
这样将 数据结构与操作接口 封装到类中有什么用呢?
这样的封装操作, 可以避免在C语言中可能出现的错误的操作——
不调用接口, 直接操作数据
对数据结构中的数据进行操作时, 是
很忌讳不调用接口直接对数据进行操作
的:比如: 栈初始化时
这两种方式, 直接操作数据肯定是不规范的。有时候会造成不必要的麻烦
而类的出现, 将成员变量修饰为 private 成员, 直接在编译时阻止了直接操作数据这种不规范的操作
类, 将 成员与操作接口 封装至在一起, 但是设置不同的访问权限。类的封装特性, 其实可以看作是对数据的管理。
2.3 类的作用域
既然, 一个类内部可以包含 成员函数。那么函数的声明及定义就有可能分离:
当类中, 成员函数的声明与定义分离, 定义时需要在函数名前加
类名::
连接(:: 是作用域运算符)
需要注意:
在类内定义的成员函数, 默认
inline
, 所以 一般成员函数较小时在类内定义
2.4 类的大小
之前C语言关于结构体的文章中, 分析了一个结构体类型大小的计算方式。
引入了一个概念叫结构体对齐,
C++中类的大小计算, 也是根据对齐计算的
。且 类的对齐方式 与 结构体对齐的方式 是一模一样的。这里不再赘述, 如有需要可以参考结构体博文:
所以,
一般的类的大小其实就是对齐后成员变量的大小
但是, 由于在类内可以存在成员函数, 那么就存在一个特殊的类——只有成员函数, 没有成员变量的类:
像这样没有成员变量的类, 它的大小会是多少呢?
可以看到, 只有成员函数的类的大小为
1
如果是空类呢?
可以看到, 没有成员变量的类, 其大小都为
1
所以, 可以说 C++中, 没有成员变量的类, 其大小为
1
(表示这个类存在)为什么?为什么没有成员变量, 类的大小就为
1
呢?成员函数大小不算入类的大小吗?那成员函数存放在哪呢?2.4.1 类对象的存储
上边提到, 类的大小计算其实是
类内所有成员变量经对齐后的大小的和
那么一个类 定义的一个对象, 这个对象的内容在内存中大致上是如何存储的呢?
对一个有成员变量和成员函数的类, 定义出的对象的内容有成员变量是一定的。但
这个对象的内容会存在成员函数吗?
以这个类为例:
根据这个疑问, 就可以假设两种情况:
-
每个对象均存储一份成员函数
对于相同的类, 每个对象大致是这样的
此方式有一个缺点, 就是每个对象中都存储有功能相同的函数, 会造成浪费
-
多个相同类的对象共用同一个成员函数
对于相同的类, 多个对象大致是这样的:
类对象在内存中的存储大致也就这两种方式了, 那么计算机中究竟使用的是哪一种呢?
调用函数调试, 并查看反汇编代码:
可以发现, 相同类的不同的对象, 调用同一个成员函数, 调用地址相同。
所以计算机中, 类对象的存储, 其实是第二种:
多个相同类的对象共用同一个成员函数
看到这里有一个疑问——对象调用成员函数可以访问对应对象的成员变量, 但是在定义成员函数时, 并没有进行传参操作。
那么怎么能够做到的 对象调用成员函数可以访问对应对象的成员变量呢?
2.5 隐含的 this指针
我们知道, 对象中是不存储类的成员函数的。那么, 是如何保证类似这样的操作:
U1.Print
、U2.Print
, 可以分别操作 U1
、U2
的成员变量的呢?其实, 类的成员函数中
除自己写的参数之外, 还有一个隐含的 this指针 参数
, 就是用来指定函数所控制的对象的:同样以此类为例:
定义不同的对象, 并且调用函数时, 编译器会进行这样的处理:
所以, 对于一个成员函数, 编译器处理后其实是这样的:
但是这些操作都是由
编译器
完成的, 不能手动添加, 手动添加是错误的:所以,
this指针在定义(声明)成员函数时不能写出来, 也不能手动传参, this指针传参的整个过程是编译器完成的。所以被称为 隐含的this指针
虽然传参是编译器执行的, 但是
成员函数内部其实是可以直接使用 this指针
的。可以使用成员函数将 this指针 的地址打印出来:
2.5.1 面试: this指针可否为空指针?this指针存储在哪个区?*
思考一个问题, 既然 this指针传参的整个过程都是由编译器实现的, 那么this指针能否为空指针呢?
以下面这段代码为例:
class A
{
public:
void PrintA()
{
cout << _a << endl;
}
void Show()
{
cout << "Show()" << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->Show();
p->PrintA();
return 0;
}
这段代码 定义了 类的一个空指针, 再用空指针调用成员函数
试分析一下,
这段代码是否有可能会崩溃?如果崩溃了, 原因在哪里?
答案是, 当代码进入
PrintA()
执行 cout << _a << endl;
时, 程序崩溃。原因是对空指针解引用
注意: 在执行
Show()
时并不会崩溃, 因为 Show() 函数中并没有对 this指针进行解引用
由此, 可以判断出 其实
this指针 是可以为空指针的, this指针为空指针时不会有任何的编译错误
。但是, 如果 this指针为空指针, 那么成员函数很有可能会对空指针解引用, 发成运行错误导致程序崩溃。所以, this指针 最好不要为空指针
传参时, this指针 不能被写出来, 那么 this指针 存储在内存中哪个区域呢?
答案是,
栈区
。因为, 归根结底 this指针也只是函数的一个形参
而已, 形参也就存储在栈帧中, 也就是栈区。不过, 由于this指针 在成员函数中可能被频繁的使用到, 所以为了提高使用效率, this指针也有可能被存储至寄存器中。(因编译器而异)
版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)
作者: 哈米d1ch 发表日期:2022 年 6 月 18 日