pair的内存结构

问题

在提交代码的时候发现了代码中的一个问题:

大概意思是, 有一个pair类型的数据, 使用如下方式打印了pairfirst的数据(实际上是代码写错了, 但是依然正常工作):

1
2
3
using ps = pair<uint64, float>;
ps p1(1, 1.1111);
printf("%p\n", p1);

编译是正常的, 这时候怀疑打印的结果是不是正常的呢?

pair的实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
  template<typename _U1, typename _U2> class __pair_base
  {
#if __cplusplus >= 201103L
    template<typename _T1, typename _T2> friend struct pair;
    __pair_base() = default;
    ~__pair_base() = default;
    __pair_base(const __pair_base&) = default;
    __pair_base& operator=(const __pair_base&) = delete;
#endif // C++11
  };

  template<typename _T1, typename _T2>
    struct pair
    : private __pair_base<_T1, _T2>
    {
      typedef _T1 first_type;    /// @c first_type is the first bound type
      typedef _T2 second_type;   /// @c second_type is the second bound type

      _T1 first;                 /// @c first is a copy of the first object
      _T2 second;                /// @c second is a copy of the second object
      //................................................................
    }

如上, pairfirstsecond两个数据是pair的成员变量, pair没有虚函数, pair继承自__pair_base, 且__pair_base中没有成员变量, 到这里就可以回答上面的问题, 问题中的输出是没问题的.

以上结论可以参考C++类的内存分布(二)C++类的内存分布.

验证

我们使用一小段代码验证上述结论, 源码在这里:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
#include <vector>
using namespace std;

using uint64 = unsigned long long int;
using ps = pair<uint64, float>;

int main()
{
    ps p1(1, 1.1111);
    ps p2(2, 2.2222);
    vector<ps> vp{p1, p2};

    printf("%p\n", p1);
    printf("%p\n", p2);
    for (auto &p : vp) {
        printf("%p\n", p);
    }
}

可以得到期望的输出:

1
2
3
4
0x1
0x2
0x1
0x2

不过并不推荐这样写, 这种写法依赖对函数/结构的了解程度. 本文仅是复习之前学习的一些知识来解释一些看似不太自然的问题.

扩展验证

还是有些不放心, 因为pair是一个struct, 虽然我们学过struct基本可以等价为class, 但是总归没有真正看过是怎么等价的. 所以我们用下面的代码大概验证一下classstruct的内存结构是不是一样的, 下面的验证不全面, 仅初步了解, 源码在这里:

 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
#include <iostream>

using namespace std;

class A{
public:
    char a;
    int b;
};
struct B{
    char a;
    int b;
};

int main() {
    A a;
    a.a = 1;
    a.b = 2;
    B b;
    b.a = 3;
    b.b = 4;

    printf("A.a = %d\n", a);
    printf("B.a = %d\n", b);

    printf("A.addr = %p\n", &a);
    printf("B.addr = %p\n", &b);

    printf("A.a.addr = %p\n", &(a.a));
    printf("B.a.addr = %p\n", &(b.a));
}

以上输出很奇怪:

1
2
3
4
5
6
A.a = 1
B.a = 174317315
A.addr = 0x7ffc0a63de28
B.addr = 0x7ffc0a63de20
A.a.addr = 0x7ffc0a63de28
B.a.addr = 0x7ffc0a63de20

我们本期望B.a输出是3, 但是我们得到了一个随机数, 所以可以观察后面的addr的输出, 这是符合预期的, class的基地址和第一个成员变量的地址一致, 那为什么B.a的输出不和期望呢?

考虑到是内存对齐的原因.

以上定义的成员a是一个char型, bint型, 所以会向b对齐, 这时候按照%d解析基地址就可能有问题了, 我们改成这样的, 就能正常解析:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
A a;
a.a = 65;
a.b = 2;
B b;
b.a = 66;
b.b = 4;

printf("A.a = %c\n", a);  //A.a = A
printf("B.a = %c\n", b);  //B.a = B

printf("A.addr = %p\n", &a);
printf("B.addr = %p\n", &b);

printf("A.a.addr = %p\n", &(a.a));
printf("B.a.addr = %p\n", &(b.a));

这小结偏题了, 但是也是在提醒我们需要注意内存对齐.

小结

  1. pairfirst成员的地址和基地址一致;
  2. 要注意class/struct的内存对齐;
  3. 仅量不要使用类的基地值访问类成员, 以免内存对齐/封装性等问题.