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

Posted

技术标签:

【中文标题】传递给类时 c_str() 和字符串的奇怪行为【英文标题】:Curious behaviour of c_str() and strings when passed to class 【发布时间】: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; 【参考方案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; 以摆脱它。

【讨论】:

以上是关于传递给类时 c_str() 和字符串的奇怪行为的主要内容,如果未能解决你的问题,请参考以下文章

传递包含“!!!!”的字符串时 argv 的奇怪行为

cordova.exec 参数中的奇怪行为

使用通知传递字符串,然后传递给类

传递给类构造函数的 C++ 字符串 - 链接器错误

将 QXmlStreamReader 实例传递给类

将字符串传递给自定义类时,在 UICollectionViewCell 中设置标签文本不起作用