[C++ 提高] --- 类的存储 和 包含虚函数的类

简介: [C++ 提高] --- 类的存储 和 包含虚函数的类

1 从内存四区的角度分析类的存储

如果一个类包括了数据和函数,用这个类去实例化对象时,系统会为每一个对象分配存储空间。每个对象所占用的存储空间只是该对象的数据部分(虚函数指针和虚基类指针也属于数据部分)所占用的存储空间,而不包括函数代码所占用的存储空间。

C++编译系统会用一段空间来存放这个共同的函数代码段,在调用各对象的函数时,都去调用这个公用的函数代码。

  C++程序的内存格局通常分为四个区:全局数据区(data area),代码区(code area),栈区(stack area),堆区(heap area)(即自由存储区)。

  全局数据区存放全局变量,静态数据和常量;
  所有类成员函数和非成员函数代码存放在代码区;
  为运行函数而分配的局部变量、函数参数、返回数据、返回地址等存放在栈区;
  余下的空间都被称为堆区。

  根据这个解释,我们可以得知在类的定义时,类成员函数是被放在代码区,而类的静态成员变量在类定义时就已经在全局数据区分配了内存,因而它是属于类的。对于非静态成员变量,我们是在类的实例化过程中(构造对象)才在栈区或者堆区为其分配内存,是为每个对象生成一个拷贝,所以它是属于对象的。

  应当说明,常说的“某某对象的成员函数”,是从逻辑的角度而言的,而成员函数的存储方式,是从物理的角度而言的,二者是不矛盾的。

2 C++类分析

2.1 C++类的构成

类由两部分构成:

  • 数据成员:简单类型[char/short/long/int/double/float等]、复合类型[结构体/枚举/类类型等]
  • 函数成员:虚函数、非虚函数

2.2 数据成员的存储方式 - 内存对其原则

复合类型由简单类型组成,简单类型对其原则同C语言结构体内存对其原则,很早之前已经写过,可以参看here,这里进行简单回顾。

简单类型在类的对象中对齐方式,以字节为单位进行存储。

char 1
short 2
long 4
int 4
float 4
fouble 8

取类中最长的数据成员作为对齐原则。例如,类中最长为 double,那么就是8 个字节。

2.3 函数成员的存储方式

非虚函数是存放在代码区的,不占用类的存储空间。

在一个类的某个函数前加上virtual关键字,这个函数就变成了虚函数。编译器处理虚函数的方法是:为每个类对象添加一个隐藏成员,隐藏成员中保存了一个指向函数地址数组的指针,称为虚表指针(vptr),这种数组成为虚函数表(virtual function table, vtbl),即,每个类使用一个虚函数表,每个类对象用一个虚表指针。

2.4 实例验证类大小

读完上面的分析,现在来看示例代码,进行验证

代码示例

#include <iostream>
/*******************Test1***********************/
class Test1 {
public:
    Test1();
    ~Test1();
public:
    int n1;
    char c1;
    short s1;
private:
    int n2;
    char c2;
    short s2;
};
Test1::Test1()
{
}
Test1::~Test1()
{
}
/*******************Test1 end***********************/
/*******************Test2***********************/
class Test2 {
public:
    Test2();
    ~Test2();
    void func0();
    friend void func1();
    void func2() const ;
    inline void func3() ;
    static void func4() ;
    // virtual void func5();
public:
    int n1;
    char c1;
    short s1;
private:
    int n2;
    char c2;
    short s2;
};
Test2::Test2()
{
}
Test2::~Test2()
{
}
void Test2::func0()
{
}
void func1()
{
}
void Test2::func2() const
{
}
inline void Test2::func3()
{
}
void Test2::func4()
{
}
// void Test2::func5()
// {
// }
/*******************Test2 end***********************/
int main(void)
{
  Test1 test1_;
  Test2 test2_;
  printf("sizeof test1 = %ld\n", sizeof(test1_));
  printf("sizeof test2 = %ld\n", sizeof(test2_));
  return 0;
}

编译输出:

打开注释之后,编译输出【测试机器为x64】:

总结:

  • C++编译系统中,数据和函数是分开存放的(函数放在代码区;数据主要放在栈区和堆区,静态/全局区以及文字常量区也有),实例化不同对象时,只给数据分配空间,各个对象调用函数时都都跳转到(内联函数例外)找到函数在代码区的入口执行,可以节省拷贝多份代码的空间
  • 类的静态成员变量编译时被分配到静态/全局区,因此静态成员变量是属于类的,所有对象共用一份,不计入类的内存空间
  • 静态成员函数和非静态成员函数都是存放在代码区的,是属于类的,类可以直接调用静态成员函数,不可以直接调用非静态成员函数,两者主要的区别是有无this指针
  • 存在虚函数的类,会对应一个虚函数表,实例化对象时,每个对象会占用一个虚表指针

3 包含虚函数的类

3.1 概述

简单地说,每一个含有虚函数(无论是其本身的,还是继承而来的)的类都至少有一个与之对应的虚函数表,其中存放着该类所有的虚函数对应的函数指针。例:

其中:

B的虚函数表中存放着B::foo和B::bar两个函数指针。

D的虚函数表中存放的既有继承自B的虚函数B::foo,又有重写(override)了基类虚函数B::bar的D::bar,还有新增的虚函数D::quz。

3.2 虚函数表构造的过程

从编译器的角度来说,B的虚函数表很好构造,D的虚函数表构造过程相对复杂。下面给出了构造D的虚函数表的一种方式(仅供参考):

3.3 虚函数调用过程

以下面的程序为例:

编译器只知道pb是B*类型的指针,并不知道它指向的具体对象类型 :pb可能指向的是B的对象,也可能指向的是D的对象。

但对于“pb->bar()”,编译时能够确定的是:此处operator->的另一个参数是B::bar(因为pb是B*类型的,编译器认为bar是B::bar),而B::bar和D::bar在各自虚函数表中的偏移位置是相等的。

无论pb指向哪种类型的对象,只要能够确定被调函数在虚函数中的偏移值,待运行时,能够确定具体类型,并能找到相应vptr了,就能找出真正应该调用的函数。


目录
相关文章
|
13小时前
|
C++ 编译器 存储
|
13小时前
|
C++ Linux
|
13小时前
|
编译器 C++
【C++】继续学习 string类 吧
首先不得不说的是由于历史原因,string的接口多达130多个,简直冗杂… 所以学习过程中,我们只需要选取常用的,好用的来进行使用即可(有种垃圾堆里翻美食的感觉)
7 1
|
13小时前
|
算法 安全 程序员
【C++】STL学习之旅——初识STL,认识string类
现在我正式开始学习STL,这让我期待好久了,一想到不用手撕链表,手搓堆栈,心里非常爽
15 0
|
13小时前
|
存储 安全 测试技术
【C++】string学习 — 手搓string类项目
C++ 的 string 类是 C++ 标准库中提供的一个用于处理字符串的类。它在 C++ 的历史中扮演了重要的角色,为字符串处理提供了更加方便、高效的方法。
16 0
【C++】string学习 — 手搓string类项目
|
13小时前
|
Java C++ Python
【C++从练气到飞升】06---重识类和对象(二)
【C++从练气到飞升】06---重识类和对象(二)
|
13小时前
|
编译器 C++
【C++从练气到飞升】06---重识类和对象(一)
【C++从练气到飞升】06---重识类和对象(一)
|
13小时前
|
设计模式 安全 算法
【C++入门到精通】特殊类的设计 | 单例模式 [ C++入门 ]
【C++入门到精通】特殊类的设计 | 单例模式 [ C++入门 ]
17 0
|
13小时前
|
C语言 C++
【C++】string类(常用接口)
【C++】string类(常用接口)
21 1