我正在做一项大学作业。一般的想法是制作一个用c++编写的库,并在c和c++头中公开其内容。我们应该有一个全局变量,包含存储电话号码更改的字典的字典(unordered_map),并使用标头中公开的函数对其进行修改。换句话说,我们有unordered_map<id, unordered_map<old_phone_nr, new_phone_number>>
。这是我的C++头
namespace jnp1 {
extern "C" {
extern size_t TEL_NUM_MAX_LEN;
unsigned long maptel_create();
// create a dictionary of telephone number changes (old->new) and return its ID
void maptel_delete(unsigned long id);
// delete the dictionary ID
void maptel_insert(unsigned long id, char const *tel_src, char const *tel_dst);
// insert change into dictionary ID
void maptel_erase(unsigned long id, char const *tel_src);
// erase change from dictionary ID
void maptel_transform(unsigned long id, char const *tel_src, char *tel_dst, size_t len);
// follow chain of changes (nr1 -> nr2 -> ... -> final_number) and return final number
}
}
我们有一个类似的C头。我们的库必须符合外部测试-C版本:
#include <assert.h>
#include <string.h>
#include "maptel.h" // our library C header
static const char t112[] = "112";
static const char t997[] = "997";
int main() {
unsigned long id;
char tel[TEL_NUM_MAX_LEN + 1]; /* +1 for terminal zero */
id = maptel_create();
maptel_insert(id, t112, t997);
maptel_transform(id, t112, tel, TEL_NUM_MAX_LEN + 1);
assert(strcmp(tel, t997) == 0);
return 0;
}
这项工作没有任何问题。然而,C++测试用例使用了类似的未命名名称空间
#include <cassert>
#include <cstddef>
#include <cstring>
#include "cmaptel" // our library C++ header
namespace {
unsigned long testuj() {
unsigned long id;
id = ::jnp1::maptel_create();
::jnp1::maptel_insert(id, "997", "112");
return id;
}
unsigned long id = testuj();
} // anonymous namespace
int main() {
char tel[::jnp1::TEL_NUM_MAX_LEN + 1];
::jnp1::maptel_transform(id, "997", tel, ::jnp1::TEL_NUM_MAX_LEN + 1); // here it breaks
assert(::std::strcmp(tel, "112") == 0);
::jnp1::maptel_delete(id);
}
在第一次从命名空间外部访问时,我们在函数内部做出的断言会中断,因为id
不在字典中。因此,换句话说,匿名命名的maptel_create()
在变量的其他实例中创建了id
字典,而不是void main(){}
中的maptel_transform()
尝试访问它。我们无法解决此问题,然而,将我们的全局unordered_map<unsigned long, unordered_map<string, string>>
移动到堆(我们使变量成为指针,而不是对象,并在maptel_create()
的第一次调用中分配它)是可行的——只是任务的规范不能保证我们可以解除分配它,导致内存泄漏。
我正在寻求对这一现象的帮助/解释。请不要告诉我使用类而不是全局变量-虽然这显然是正确的设计选择,但我们有一个必须遵守的规范。下面我展示了创建/擦除函数的堆和堆栈版本(在这个问题上,擦除的工作原理与转换相同,只是更短,所以作为一个例子更好)
堆栈:
#include "cmaptel"
typedef std::unordered_map<std::string, std::string> tel_book;
std::unordered_map<unsigned long, tel_book> books;
unsigned long id_counter;
extern "C" {
size_t TEL_NUM_MAX_LEN = 22;
unsigned long jnp1::maptel_create()
{
books.reserve(1);
// without this for some reason we would get float arithmetic exception right here
books[id_counter] = tel_book();
return id_counter++;
}
void jnp1::maptel_erase(unsigned long id, char const *tel_src)
{
assert(books.count(id)); // this is the assertion that breaks
std::string src(tel_src);
int result = books[id].erase(src);
}
}
堆:
#include "cmaptel"
typedef std::unordered_map<std::string, std::string> tel_book;
std::unordered_map<unsigned long, tel_book> *books;
unsigned long id_counter;
extern "C" {
size_t TEL_NUM_MAX_LEN = 22;
unsigned long jnp1::maptel_create()
{
if (books == NULL) books = new std::unordered_map<unsigned long, tel_book>();
(*books)[id_counter] = tel_book();
return id_counter++;
}
void jnp1::maptel_erase(unsigned long id, char const *tel_src)
{
assert(books->count(id));
std::string src(tel_src);
int result = (*books)[id].erase(src);
}
}
全局变量的初始化顺序不是跨多个翻译单元指定的。问题是id
(以及对maptel_insert
的调用)在books
之前被初始化。由于maptel_insert
在未初始化的对象上操作,因此这是未定义的行为。
您已经找到了一种解决方法:按需动态分配地图,从而确保在使用之前对其进行分配。另一个细微的变化是使用函数范围的静态,因为这些规则保证它们在使用前初始化:
std::unordered_map<unsigned long, tel_book>& books()
{
static std::unordered_map<unsigned long, tel_book> instance;
return instance;
}
C++11还保证以线程安全的方式进行初始化。