c_str() 和字符串传递给 class 时的奇怪行为

Curious behaviour of c_str() and strings when passed to class

我在玩 c 字符串和 std::string 时遇到了一个奇怪的行为(我确信这对我来说只是好奇并且存在一个完全有效的 c++ 答案)。通常,当我将字符串传递给 class' 构造函数时,我会执行如下操作:

class Foo {
public:
  Foo(const std::string& bar) bar_(bar) { }
private:
  const std::string& bar_;
};

int main() {
  Foo("Baz");
  return 0;
}

到目前为止效果很好,我(也许天真?)从不质疑这种方法。

然后最近我想实现一个包含 class 的简单数据,当剥离其基本结构时,它看起来像这样:

#include <iostream>
#include <string>

class DataContainer {
public:
  DataContainer(const std::string& name, const std::string& description)
  : name_(name), description_(description) {}
  auto getName() const -> std::string { return name_; }
  auto getDescription() const -> std::string { return description_; }
private:
  const std::string& name_;
  const std::string& description_;
};

int main() {
    auto dataContainer = DataContainer{"parameterName", "parameterDescription"};
    auto name = dataContainer.getName();
    auto description = dataContainer.getDescription();

    std::cout << "name: " << name.c_str() << std::endl;
    std::cout << "description: " << description.c_str() << std::endl;
}

输出为:

name: parameterName
description:

我在这里使用 *.c_str() 因为这就是我在实际代码库中使用它的方式(即使用 google 测试和 EXPECT_STREQ(s1, s2).

当我在主函数中删除 *.c_str() 时,我得到以下输出:

name: parameterName
description: tion

因此描述的原始字符串被截断并且缺少初始字符串。我能够通过将 class 中的类型更改为:

来解决此问题
private:
  const std::string name_;
  const std::string description_;

现在我得到了

的预期输出
name: parameterName
description: parameterDescription

很好,我可以使用这个解决方案,但我想了解这里发生了什么。另外,如果我将主要功能稍微更改为

int main() {
    auto dataContainer = DataContainer{"parameterName", "parameterDescription"};
    auto name = dataContainer.getName().c_str();
    auto description = dataContainer.getDescription().c_str();

    std::cout << "name: " << name << std::endl;
    std::cout << "description: " << description << std::endl;
}

我如何将字符串存储在 DataContainer class 中并不重要,即通过 const ref 或值。在这两种情况下,我得到

name: parameterName
description: 

伴随着 clang 的警告:

<source>:19:17: warning: object backing the pointer will be destroyed at the end of the full-expression [-Wdangling-gsl]
    auto name = dataContainer.getName().c_str();

所以我猜问题出在 *.c_str() 本身?但是,我不太明白为什么我不能通过 const ref 存储这两个字符串 name 和 description 。谁能阐明这个问题?

发生了以下情况:您正在 return 复制 std::string(即临时文件)。然后 c_str() 将 return 一个指向该临时数据的指针,该指针将在语句后销毁。因此警告。 Return const std::string& 而不是摆脱它。

在第一期中,您将 const std::string& 引用存储为 class 成员,您存储 对临时对象的悬挂引用 .

当您将字符串文字传递给构造函数时,它们本身并不是 std::string 对象,而是 const char[] 数组。因此,编译器必须创建 temporary std::string 对象来满足构造函数的参数,然后您存储对这些参数的引用。一旦构造函数退出,这些临时对象就会被销毁,使您存储的引用绑定到无效内存。

您存储 std::string 对象的副本而不是对原件的引用的解决方案是正确的解决方案。


在第二期中,您在 getName()getDescription() 的 return 值上调用 c_str(),这是一个类似的问题。您正在使用 指向临时内存的悬挂指针

这些方法是 returning std::string 对象 按值 ,因此编译器会在调用站点创建它们的临时副本。 c_str() return 是指向 std::string 对象的内部数据的指针,您将这些指针存储到局部变量。但是临时变量在超出范围时会被销毁,使您的变量在您有机会使用它们之前指向无效内存。

您可以通过以下三种方式之一解决该问题:

  • 通过将 std::string 对象的 副本 保存到局部变量,而不是保存它们的内部数据指针。这就是您的 main() 代码最初做的事情:
auto dataContainer = DataContainer{"parameterName", "parameterDescription"};
auto name = dataContainer.getName(); // <-- auto is deduced as std::string, name is a copy...
auto description = dataContainer.getDescription(); // <-- auto is deduced as std::string, description is a copy...

std::cout << "name: " << name.c_str() << std::endl; // <-- using c_str() pointer is safe here
std::cout << "description: " << description.c_str() << std::endl; // <-- using c_str() pointer is safe here
  • 在临时 std::string 对象超出范围之前,完全删除局部变量并直接在 cout 语句中使用 c_str() 指针:
auto dataContainer = DataContainer{"parameterName", "parameterDescription"};

std::cout << "name: " << dataContainer.getName().c_str() << std::endl; // <-- getName() returns a temp copy, but c_str() is safe to use here
std::cout << "description: " << dataContainer.getDescription().c_str() << std::endl; // <-- getDescription() returns a temp copy, but c_str() is safe to use here
  • 通过 return references 方法 std::string class 成员,而不是 returning 副本 其中:
auto getName() const -> const std::string& { return name_; }
auto getDescription() const -> const std::string& { return description_; }
auto dataContainer = DataContainer{"parameterName", "parameterDescription"};
auto name = dataContainer.getName().c_str(); // <-- no temp is returned here
auto description = dataContainer.getDescription().c_str(); // <-- no temp is returned here

std::cout << "name: " << name << std::endl; // using c_str() pointer is safe here!
std::cout << "description: " << description << std::endl; // <-- using c_str() pointer is safe here!

在最后一种情况下,请确保在使用保存的指针之前不要修改 std::string class 成员,否则指针可能会失效。

如前所述,已发布代码中的问题起因于对临时对象的悬垂引用,这些对象要么存储为 class 成员,要么由 .c_str() 返回和访问。

第一个修复是将实际的 std::string 存储为成员,而不是(悬挂)引用,然后编写访问器函数,返回对这些成员的常量引用:

#include <iostream>
#include <string>

class DataContainer {
public:
  DataContainer(std::string name, std::string description)
    : name_(std::move(name)), description_(std::move(description)) {}
  auto getName() const -> std::string const& { return name_; }
  auto getDescription() const ->  std::string const& { return description_; }
private:
  const std::string name_;
  const std::string description_;
};

int main() {
    auto dataContainer = DataContainer{"parameterName", "parameterDescription"};
    
    std::cout << "name: " << dataContainer.getName().c_str() << std::endl;
    std::cout << "description: " << dataContainer.getDescription().c_str() << std::endl;
    return 0;
}

你可以看到 here that the output is as expected (even when 使用中间局部变量)。


I use *.c_str() here as this is how I use it my actual codebase

然后考虑添加几个访问器返回 exactly that:

//...
auto Name() const { return name_.c_str(); }
auto Description() const { return description_.c_str(); }
//...
std::cout << "name: " << dataContainer.Name() << std::endl;
std::cout << "description: " << dataContainer.Description() << std::endl;