7
#include <stdio.h>
#include <stdlib.h>
#include <memory.h>
#include <vector>
#include <string>
#include <iostream>
#include <map>
#include <utility>
#include <algorithm>

void * GetMemory(size_t n) {
  void *ptr = malloc(n);
  printf("getMem n %d   ptr 0x%x\n", n, reinterpret_cast<unsigned int> (ptr));
  return ptr;
}

void FreeMemory(void *p) {
  free(p);
}

void* operator new (size_t n) {
  void *p = GetMemory(n);
  return p;
}

void* operator new [] (size_t n) {
  void *p = GetMemory(n);
  return p;
}

void operator delete (void *p) {
  FreeMemory(p);
}

void operator delete [] (void *p) {
  FreeMemory(p);
}

typedef std::vector<int> vec;

int main(int argc, char *argv[]) {
  std::map<int, vec> z;
  vec x;
  z.insert(std::pair<int,vec>(1,x));
}

使用 g++ -Wall -ansi test.cpp -o test 编译

运行测试。

为什么在 n = 0 时调用 GetMemory 有 3 次?

4

2 回答 2

8

在 FreeMemory 中粘贴一些跟踪并将 main 更改为:

int main(int argc, char *argv[]) {
  printf("map\n");
  std::map<int, vec> z;
  printf("vec\n");
  vec x;
  printf("pair\n");
  std::pair<int,vec> y(1,x);
  printf("insert\n");
  z.insert(y);
  printf("inserted 1\n");
  y.first = 2;
  printf("insert\n");
  z.insert(y);
  printf("inserted 2\n");

}

输出:

$ make mapinsert CXXFLAGS=-O3 -B && ./mapinsert
g++ -O3    mapinsert.cpp   -o mapinsert
map
vec
pair
getMem n 0   ptr 0x6b0258
insert
getMem n 0   ptr 0x6b0268
getMem n 32   ptr 0x6b0278
getMem n 0   ptr 0x6b02a0
FreeMemory ptr 0x6b0268
inserted 1
insert
getMem n 0   ptr 0x6b0268
getMem n 32   ptr 0x6b02b0
getMem n 0   ptr 0x6b02d8
FreeMemory ptr 0x6b0268
inserted 2
FreeMemory ptr 0x6b0258
FreeMemory ptr 0x6b02d8
FreeMemory ptr 0x6b02b0
FreeMemory ptr 0x6b02a0
FreeMemory ptr 0x6b0278

因此,在您的 3 个 0 大小的分配中:

  • 一种是将空向量复制到对中。
  • 一种是将空向量的副本存储在地图中。

这两个显然是必要的。我不确定的是:

  • 一种是在对 的调用中将向量复制到某处insert,这也在对插入的调用中被释放。

就好像insert(或它在内部调用的东西)通过值而不是通过引用来获取其参数,或者insert在分配新地图节点之前的某个时间显式地将副本复制到自动变量中。目前,启动调试器对我来说很费力,我会把它留给其他人。

编辑:谜团解开了。insert需要 a std::pair<const int, vec>,而不是std::pair<int, vec>. 空向量的额外副本是因为您构造的对必须转换为(另一个)临时对象,然后将该临时对象的引用传递给insert. std::pair 有一个构造函数模板,可以让你摆脱几乎任何事情。20.2.2/4:

template<class U, class V> pair(const pair<U,V> &p);

效果:从参数的相应成员初始化成员,根据需要执行隐式转换。

我还观察到,在我的实现中,vec x;不调用getMem,而是调用vec x(0);。所以实际上:

z[1] = vec();

代码更少,并剥夺了您制作额外副本的机会(尽管它改为调用operator=)。至少对我来说,它仍然会进行 2 个 0 大小的分配。

C++ 标准定义operator[]返回涉及调用的指定表达式的结果insert。我不确定这是否意味着 的效果operator[]“好像”make_pair并被insert调用(也就是说,标准与指定源必须是什么一样好operator[]),或者只是返回的值与指定的表达式会产生。如果是后者,那么也许一个实现可以使用单个 0 大小的分配来完成。但是,如果不首先创建包含映射类型的对,肯定map无法保证创建条目的方法,因此应该预期 2 次分配。或者更准确地说,所需映射值的 2 个副本:复制 0 大小的向量会产生 0 大小的分配这一事实取决于实现。

因此,如果您遇到的情况是复制该值非常昂贵,但对于默认构造却非常便宜(例如具有大量元素的容器),那么以下内容可能有用:

std::map<int, vec> z;
vec x(1000);
z[1] = x;
// i.e. (*(z.insert(std::pair<const int, vec>(1,vec())).first)).second = x;

进行 2 次大小为 4000 的分配和 2 次大小为 0 的分配,而:

std::map<int, vec> z;
vec x(1000);
z.insert(std::pair<const int, vec>(2, x));

使 3 的大小为 4000 而没有大小为 0。最终大小足够大,以至于第一个代码中的额外分配比第二个代码中的额外复制便宜。

C++0x 中的移动构造函数可能会对此有所帮助,我不确定。

于 2010-03-09T12:48:37.847 回答
6

所有 3 种情况都与空向量的初始化有关:

  1. 初始化包含空向量的树的根元素(std::map 的内部实现)。
  2. 您自己初始化“vec x”。
  3. 用于调用复制变量“x”的空集的元素“second”的 std::pair 的复制构造函数
于 2010-03-09T10:25:03.560 回答