传递给类时 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 -> std::string
return copy... 你应该这样做auto getName() const -> const std::string&
。
【参考方案1】:
如前所述,发布代码中的问题源于对临时对象的悬空引用,这些临时对象要么存储为类成员,要么由.c_str()
返回和访问。
第一个解决方法是将实际的 std::string
s 存储为成员,而不是(悬空)引用,然后编写访问器函数返回对这些的 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&
引用存储为类成员,您存储对临时对象的悬空引用。
当您将字符串文字传递给构造函数时,它们本身不是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&
以摆脱它。
【讨论】:
以上是关于传递给类时 c_str() 和字符串的奇怪行为的主要内容,如果未能解决你的问题,请参考以下文章