【问题标题】:Curious behaviour of c_str() and strings when passed to class传递给类时 c_str() 和字符串的奇怪行为
【发布时间】:2021-07-23 14:51:56
【问题描述】:

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

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

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

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

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

#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(),因为这就是我在实际代码库中使用它的方式(即使用谷歌测试和EXPECT_STREQ(s1, s2)

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

name: parameterName
description: tion

所以描述的原始字符串被截断,初始字符串丢失。我可以通过将类中的类型更改为:

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 类中存储字符串并不重要,即通过 const ref 或 value。在这两种情况下,我都会得到

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 存储两个字符串名称和描述。谁能解释一下这个问题?

【问题讨论】:

  • 您存储对临时对象的悬空引用...
  • auto getName() const -&gt; std::string return copy... 你应该这样做auto getName() const -&gt; const std::string&amp;

标签: c++ string c-str


【解决方案1】:

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

第一个解决方法是将实际的 std::strings 存储为成员,而不是(悬空)引用,然后编写访问器函数返回对这些的 const 引用:

#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 的输出符合预期(即使when 使用中间局部变量)。


我在这里使用*.c_str(),因为这是我在实际代码库中使用它的方式

然后考虑添加几个返回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;

【讨论】:

    【解决方案2】:

    在第一个问题中,您将const std::string&amp; 引用存储为类成员,您存储对临时对象的悬空引用

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

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


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

    这些方法按值返回std::string对象,因此编译器会在调用站点创建它们的临时副本。 c_str() 返回一个指向 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
    
    • 通过让方法将引用返回给std::string 类成员,而不是返回副本
    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 类成员,否则指针可能会失效。

    【讨论】:

    • 感谢您提供更多见解。我也确实事先测试了这些东西,但行为是相同的(因此标题好奇的行为......)。您的第一个解决方案:godbolt.org/z/r16xnjqa6,第二个解决方案:godbolt.org/z/KaPGrWWqz,第三个解决方案:godbolt.org/z/TxoMfe3Pe ...它们都产生原始输出(即名称显示但描述没有)。在 GCC 11.1 和 Clang 12.0.0 上测试
    • 是的,所有这三个godbolt示例都遇到了同样的问题——存储对临时对象的悬空引用
    【解决方案3】:

    发生了以下情况:您正在通过副本(即临时)返回 std::string。然后c_str() 将返回一个指向该临时数据的指针,该数据将在语句之后被销毁。因此发出警告。返回const std::string&amp; 以摆脱它。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2018-07-19
      • 2020-10-19
      • 2021-10-13
      • 1970-01-01
      • 2015-11-14
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多