关于 this
多数人都知道的是, C++ 中的每一个对象都可以使用 this 指针来访问自己的地址, 但是可能没有考虑过以下问题:
- 空类 class 的大小为什么是 1 byte?
this既然叫做指针, 保存在哪里 ? 占用类对象的空间大小吗 ?- 返回对象指针的时候,
this和*this的区别 ? - 为什么
this指针可以指向 类对象的地址 ?
空类
1. 空类的大小为 1 byte
注意: 平时讲的 类的大小 其实就是 类的实例化的对象所占的空间大小 ! 类本身只是一种 类型定义, 属于特殊的结构体, 本身并没有大小可言, 所以
sizeof()运算符对一个类型名操作, 得到的是具有该类型实体的大小
#include <iostream>
using namespace std;
class Empty {
};
struct Empty2 {
};
int main(int argc, char **argv) {
Empty obj_empty1;
Empty obj_empty2;
Empty2 struct_empty;
std::cout << sizeof(obj_empty1) << std::endl; // 1
std::cout << sizeof(Empty) << std::endl; // 1
// &obj_empty1 = 0x7fff9402efae
std::cout << "&obj_empty1 = " << &obj_empty1 << std::endl;
// &obj_empty2 = 0x7fff9402efaf
std::cout << "&obj_empty2 = " << &obj_empty2 << std::endl;
std::cout << sizeof(struct_empty) << std::endl; // 1
std::cout << sizeof(Empty2) << std::endl; // 1
}
在 C++ 中空类会占用 1 个 byte, 是为了让对象的实例可以互相区分, 也就是说, 空类同样可以被实例化, 并且每一个实例在内存中都有独一无二的地址, 因此, 编译器会给空类隐含加上 1 个byte, 这样空类实例化之后就会拥有独一无二的内存地址,
如果没有这一个字节的占位, 空类就无所谓实例化了, 因为实例化的过程就是在内存中分配一块地址.
但是值得注意的是:
如果一个空类作为基类, 该类的大小就会优化为 0. 即 "空白基类最优化"
2. 空白基类最优化
/***********************************************************************************************************************
* @swcomponent
* @file main.cpp
* @date 2023/2/14
* @brief 探究 C++ 中空类的大小
* @author ZHANG Hao (snowzhang183@126.com)
**********************************************************************************************************************/
#include <iostream>
using namespace std;
class EmptyBase1 {
};
// 1. 单继承中的空白基类最优化问题
class EmptySingleDerived : public EmptyBase1 {
public:
int a;
protected:
private:
};
class EmptyBase2 {
};
// 2. 多继承中的空白基类最优化问题
class EmptyMultipleDerived : public EmptyBase1, EmptyBase2 {
public:
int a;
protected:
private:
};
int main(int argc, char **argv) {
// 单继承
EmptyBase1 empty_base1;
EmptySingleDerived empty_derived1 {};
std::cout << "sizeof(empty_base) = " << sizeof(empty_base1) << std::endl;
// sizeof(empty_derived1) = 4 而不是 5 因为存在空白基类优化
std::cout << "sizeof(empty_derived1) = " << sizeof(empty_derived1) << std::endl;
// address of empty_base1 = 0x7fff7e007d5e
std::cout << "address of empty_base1 = " << &empty_base1 << std::endl;
// address of empty_derived1 = 0x7fff7e007d60
std::cout << "address of empty_derived1 = " << &empty_derived1 << std::endl;
// 多继承
EmptyBase2 empty_base2;
EmptyMultipleDerived empty_derived2 {};
std::cout << "sizeof(empty_base2) = " << sizeof(empty_base2) << std::endl;
std::cout << "sizeof(empty_derived2) = " << sizeof(empty_derived2) << std::endl;
// address of empty_base2 = 0x7fff7e007d5f
std::cout << "address of empty_base2 = " << &empty_base2 << std::endl;
// address of empty_derived2 = 0x7fff7e007d64
std::cout << "address of empty_derived2 = " << &empty_derived2 << std::endl;
return 0;
}
一个对象的大小 >= 所有非静态成员(non static) 成员大小的总和
this 指针
一个对象的 this 指针并不是对象本身的一部分, 不会影响 sizeof(obj) 的大小 !
this 的作用域是在类的内部, 当在类的 非静态成员函数中访问类的非静态成员时, 编译器会自动将对象本身的地址 作为一个隐含参数传递给函数. (这句话非常非常重要)
1. this 指针的作用
- 在类的 非静态成员函数中 return 类对象本身的时候, 直接使用
return *this - 参数和成员变量名相同时,
this->num = num;
为什么 return *this; 表示的是类对象本身 ?
所以 *this 其实就是类中的第一个数据成员 !
/***********************************************************************************************************************
* @swcomponent
* @file main.cpp
* @date 2023/2/7
* @brief 探究 this, *this, &this 的区别和作用
* @author ZHANG Hao (snowzhang183@126.com)
**********************************************************************************************************************/
#include <iostream>
using namespace std;
/**
* a (a 是这一块内存的名称), a.c 就可以取到变量 c
* *this 是浅拷贝, 不改变原来
* &this 是对象自身
*
*/
class Test {
public:
char c;
int d;
Test *GetThis() {
printf("GetThis : Address of this = %p\r\n", this);
return this;
} // 返回当前对象的地址
Test GetCopy() {
printf("GetCopy : Address of *this (Test) = %p\r\n", &*this);
return *this;
} // 返回当前对象的 拷贝, 浅拷贝
// *this 和 obj 作用一致, &*this 就相当于 &obj
Test &GetReference() {
printf("GetReference : Address of *this (Test &) = %p\r\n", &*this);
return *this;
} // 返回当前对象自身
protected:
private:
};
int main(int argc, char **argv) {
// sizeof(ClassName) 实际上就是当前类实例化对象的内存大小
printf("size of class Test = %u\r\n", sizeof(Test));
Test obj_test;
obj_test.c = 'c';
obj_test.d = 2;
// obj_test 和 obj_test.c 等价, 类似 数组首地址表示 第一个元素
printf("obj_test.c = %c \r\n", obj_test); // obj_test
printf("Address of obj_test = %p\r\n", &obj_test);
obj_test.GetThis();
obj_test.GetCopy();
obj_test.GetReference();
printf("\r\n");
// 验证 Test GetCopy() { return *this; } 返回的是浅拷贝
Test obj_test2 = obj_test.GetCopy();
obj_test2.c += 2;
// 看下修改 obj_test2.c 是否会影响到 obj_test.c
printf("obj_test.c = %c\r\n", obj_test.c);
printf("Address of obj_test2 = %p\r\n", &obj_test2);
// 验证 Test &GetReference() { return *this; } 返回的是 对象自身
// 注意类型需要是 Test &
Test & obj_test3 = obj_test.GetReference(); // 传递引用并没有开辟新的内存, 只是起了新的别名, !!!
obj_test3.c += 2;
printf("obj_test.c = %c\r\n", obj_test.c);
printf("Address of obj_test3 = %p\r\n", &obj_test3);
return 0;
}
/**
size of class Test = 8
obj_test.c = c
Address of obj_test = 0x7ffc5a9a8d08
GetThis : Address of this = 0x7ffc5a9a8d08
GetCopy : Address of *this (Test) = 0x7ffc5a9a8d08
GetReference : Address of *this (Test &) = 0x7ffc5a9a8d08
GetCopy : Address of *this (Test) = 0x7ffc5a9a8d08
obj_test.c = c
Address of obj_test2 = 0x7ffc5a9a8d10
GetReference : Address of *this (Test &) = 0x7ffc5a9a8d08
obj_test.c = e
Address of obj_test3 = 0x7ffc5a9a8d08
*/
多态
关于多态(polymorphism) ?
在 C++ 中, 允许通过 基类(BaseClass) 类型的指针 or 引用 来访问 派生类中的 成员函数. 并且 允许需要执行的函数在运行时 进行延迟绑定 (late binding), 称之为 多态.
所以:
多态的前提条件是 存在类的继承关系.
多态实现的方法是C++ 的续函数机制 (虚函数指针 vptr 和 虚函数表 vptl)
函数重载 overload
函数重载 overload 也称为 多态, 发生在 静态编译阶段, 根据函数的 参数类型 的区别就确定了应该调用的函数
函数重写 overwrite
函数重写 overwrite 也叫 覆盖 override 是通过 虚函数机制实现的, 动态绑定的多态.
为什么需要多态
- 多态意味着 可以用同一个函数名去执行不同的动作, 对函数命名复用的同时, 还可以简化代码.
- 多态约定了实现的接口, 便于 子类的具体功能的实现, 而 接口不变
- 可以实现各个子类之间互相不影响, 从而提高了代码的拓展性
- 便于后期代码维护, 如果不是多态, 就需要手动进行判断具体需要执行的方法 (会有一堆的 if ... else ...)
如何使用多态
- 在 C++ 中, 多态的实现是通过 覆盖 override
而决定是否覆盖函数的关键点在于 该基类中的成员函数 是否有关键字
virtual修饰, 被修饰的成员函数被称之为 虚函数, 然后在 派生类(Derived Class)中定义的 同名函数 覆盖 (override)
多态的原理
C++ 基类中包含 virtual 修饰的成员函数, 编译器 将会在类的内存模型中添加 虚函数表的指针, 即 vptr.
该指针占用的大小 (sizeof(void *)) 和平台相关, 该 vptr 指向存储在别处的虚函数表 (vtbl)
vtbl 中又存放着类中的虚拟成员函数的地址
关于内存对齐
memory alignment:
成员变量在类中的内存存储 不一定是连续的, 是按照编译器的设置, 按照 内存块来存储, 这个对齐的内存块大小的取值, 就是 内存对齐.
内存对齐规则:
-
类的第一个成员变量放在类内存中 offset(偏移量) = 0 的地方, 之后的 成员变量的对齐按照
#pragma pack(n)指定的数值 和 这个成员变量类型所占字节数中, 比较小 的那个进行 成员变量间补齐 -
在成员变量完成 各自的内存对齐之后, 该类(结构 or 联合) 本身也要进行内存对齐. 对齐按照
#pragma pack(n)指定的数值 和 类中最大成员变量类型所占字节数中比较小的那个进行 (类中最后一个成员变量结尾后 补齐) 类大小需要是 对齐值的整数倍. -
#pragma pack(n)作为一个预编译指令 用来 设置结构内存对齐的字节数
要注意: n 的缺省数值是编译器设置的, 一般为 8, 合法的数值分别是
1, 2, 4, 8, 16
@Q: 为什么要进行内存对齐 ?
@A: 内存对齐是为了 提高 cpu 读取数据的效率, 并不是所有的硬件平台都能够随意访问任意位置的内存, 因为一些平台的cpu, 如果读取的数据是未对齐的话, 将拒绝访问 or 抛出硬件异常.
内存对齐的规则:
-
假设结构体的起始地址为
0x0, 结构体 第一个成员相当结构体首地址的偏移量(offset) = 0, 此后每个成员的 对齐值为 其自身对齐大小与#progama pack指定的数值中较小的那个 -
结构体中所有成员完成字节对齐之后, 结构体自身的 内存对齐值 为 所有成员中对齐值最大的那个
有效对齐值: 是给定值 #pragma pack(n) 和 结构体(类) 中最长数据类型长度中 较小 的那个, 有效对齐值也叫 对齐单位.
-
结构体第一个成员的偏移量 (offset) 为 0, 以后每个成员相对于结构体 首地址的 offset 都是该成员大小 与 有效对齐值 中较小那个的 整数倍, 如有需要编译器会在成员之间加上填充字节
-
结构体的总大小为 有效对齐值 的整数倍, 如有需要编译器会在最末一个成员之后加上填充字节
-
对于含有其他结构体变量的情况, 该结构体变量存储位置 从自己的最大对齐数的整数倍处开始, 而结构体的整体大小就是 所有最大对齐数 (含嵌套的结构体的对齐数) 的整数倍 (最小公倍数)
e.g.
struct A {
char a; // 1
int b; // 4
short c; // 2
long long int d; // 8
char e; // 1
};
按照字节对齐(align)规则:
align 对齐(v)
on linux: 默认对齐系数是 4, #pragma pack(4)
根据 规则-1:
该结构体中最大的成员变量是 long long int, 8 byte > 对齐系数 4
所以 有效对齐值(对齐单位) = 4
struct A {
char a: // 1 < 4, offset: 0x00 ~ 0x01 1 byte
根据 以后每个成员相对于结构体 **首地址的** offset 都是该成员大小 与 **有效对齐值** 中较小那个的 **整数倍**
int b; // 4 <= 4, offset: 0x04(开始) ~ 0x08 4 bytes
short c; // 2 < 4, offset: 0x08(0x08 刚好是 2 的整数倍) ~ 0x10 2 bytes
long long int d; // 8 > 4; 0x10 不是 4 的整数倍, 所以从 0x12(开始) ~ 0x20 8 bytes
char e; // 1 < 4; offset: 0x20 ~ 0x21
};
根据 规则-2: 由于 21 不是 4 的整数倍, 所以补全到 24
图解:
如果使用默认的 8 bytes 对齐,
多态指针偏移demo
64 bit 操作系统上 64 bit 编译器默认是 8 bytes 对齐