Member Pointer

![sunset](C:\Users\20550\Pictures\Saved Pictures\sunset4.jpg)

本文讨论C++类中数据成员(static和non-static)的排列、静态函数、成员函数和虚函数在内存中的布局以及调用过程,认识指向成员的指针,以便加深对类对象布局的理解。

指向data member的指针

static data members

静态数据成员存放于类之外,被视为一个全局变量,但仅在class生命范围内可见。因此如果一个类Obj有一个静态类型的整型变量a,则

1
&Obj::a

会获得如下内存地址:

1
int *

类中的静态成员的名称一般被使用name-mangling编码为一个独一无二的名称,以便不同类中的相同名称的静态变量不会冲突。

nonstatic data members

nonstatic data members在class object中的排列顺序和其被声明的顺序一样,任何介入的static data members都不会被放入到对象布局中。我们也可以对nonstatic 类型的data member取地址,如同static类型的变量一样,假设有一个Obj类,包含一个整型的数据成员b,则我们也可以进行如下操作:

1
&Point3d::b

不同于static类型的成员,上述操作不会得到b的内存地址,这是显而易见的,因为每个不同的对象都有一份b。其实它的类型为**int Obj::*,而不是**int *,含义是b成员在对象中的偏移(offset)。

如下测试程序,Foo类包含一个成员方法,用于打印其内部数据成员的对象内部偏移:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
class Foo
{
public:
int x;
static int sx;

private:
int y;
float z;
static void *sp;

public:
double d;

protected:
char c;

public:
char a;

public:
void print() const
{
printf("static data: %p %p\n", &Foo::sx, &Foo::sp);
printf("%d %d %d %d %d %d\n",
&Foo::x, &Foo::y, &Foo::z, &Foo::d, &Foo::c, &Foo::a);
cout << sizeof(*this) << endl;
}
};

int Foo::sx = 19; // 有不为0的初始值,放置在.data
void *Foo::sp = nullptr; // 放置在.bss

int global; // 没有初始值,放置在.bss
int main()
{
Foo foo;
foo.print();
cout << &global << endl;

return 0;
}

编译运行:

1
2
3
4
5
> g++ two.cc -g -o two && ./two
static data: 0x563c95253010 0x563c95253158
0 4 8 16 24 25
32
0x563c95253160

第一行打印的是静态数据的地址,第二行是对象内部按照声明次序,打印其offset,可见成员的实际排列次序和声明次序完全吻合,与其修饰符(public、protected、private)以及类型无关,也不会将同种修饰符的成员专门安放在一起。对象还需要添加padding部分以满足对齐,因此大小为32,

反汇编查看.data段和.bss段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
> objdump -d -j .data two

two: file format elf64-x86-64


Disassembly of section .data:

0000000000004000 <__data_start>:
...

0000000000004008 <__dso_handle>:
4008: 08 40 00 00 00 00 00 00 .@......

0000000000004010 <_ZN3Foo2sxE>: # Foo::sx
4010: 13 00 00 00 ....
> objdump -d -j .bss two

two: file format elf64-x86-64


Disassembly of section .bss:

0000000000004040 <_ZSt4cout@GLIBCXX_3.4>:
...

0000000000004150 <completed.0>:
...

0000000000004158 <_ZN3Foo2spE>: # Foo::sp
...

0000000000004160 <global>: # global
4160: 00 00 00 00 ....

0000000000004164 <_ZStL8__ioinit>:
4164: 00 00 00 00

符合我们的预期。

多态——单继承

当类存在基类时,基类的数据会被作为类的一部分存在,我们也可以通过打印类成员指针的值,了解到类中数据成员是如何排列的。修改上述测试程序如下,首先我们考虑单继承:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

class Base
{
public:
int ba;
char bb;
};

class Derived : public Base
{
public:
char da;
int db;
};

int main()
{
Derived derived;
printf("%d %d %d %d\n",
&Derived::ba, &Derived::bb, &Derived::da, &Derived::db);

char * p1 = reinterpret_cast<char *>(&derived);
char * p2 = reinterpret_cast<char *>(&derived.ba);
char * p3 = reinterpret_cast<char *>(&derived.bb);
char * p4 = reinterpret_cast<char *>(&derived.da);
char * p5 = reinterpret_cast<char *>(&derived.db);
printf("%lld %lld %lld %lld\n",
p2 - p1, p3 - p1, p4 - p1, p5 - p1);
cout << sizeof(Base) << endl;
cout << sizeof(Derived) << endl;


return 0;
}

编译运行:

1
2
3
4
0 4 8 12
0 4 8 12
8
16

第一行打印成员指针获取的成员offset;

第二行打印通过指针算术实际求出的对象中成员的offset,可见和第一行确实一致;

三、四行打印基类和子类的大小。

可以发现,基类的数据被存放在对象的起始处,子类自定义的数据成员相继摆放在后面。值得注意的是,编译器并未把子类和基类的数据成员“混为一谈”,编译器并未把子类的第一个char类型的成员和基类最后一个char类型成员相邻放置以节省padding的部分。基类的部分是作为一个整体存在于子类之中。

多态——多继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
class Base
{
public:
int b;
};

class Base1
{
public:
int ba{};
char bb{};
};

class Base2
{
public:
int bc{};
char bd{};
};

class Base3
{
public:
int be;
};

class Derived : public Base1, public Base2, public Base3
{
public:
char da{};
int db{};
};

int main()
{
Derived derived;
printf("%d %d\n",
&Derived::ba, &Derived::bb);

printf("%d %d %d\n", &Derived::bc, &Derived::bd, &Derived::be);

// printf("%d %d %d %d\n", &Derived::bc, &Derived::bd, &Derived::b, &Derived::be);
char * p1 = reinterpret_cast<char *>(&derived);
char * p2 = reinterpret_cast<char *>(&derived.bc);
char * p3 = &derived.bd;
printf("%lld %lld\n", p2 - p1, p3 - p1);
printf("%lld %lld\n", &Derived::da, &Derived::db);
cout << sizeof(Base1) << endl;
cout << sizeof(Base2) << endl;
cout << sizeof(Derived) << endl;
int Derived::* p = nullptr;
printf("%d %p", sizeof p, p);

return 0;
}

在多继承,尤其是多继

指向member function的指针

static function

member function

virtual function