execve
的Posix char**
。在我的其余代码中,我更喜欢使用相当现代的C ++样式,因此我的向量为std::string
。这是处理它们之间胶水的最佳方法吗?char ** strlist(std::vector<std::string> &input) {
char** result = new char*[input.size()];
result[input.size()] = nullptr;
for (int i=0; i<input.size(); i++) {
char *temp = new char[input[i].size()];
strcpy(temp, input[i].c_str());
result[i] = temp;
}
return result;
}
bool del_strlist(char ** strings, int limit=1024) {
bool finished = false;
for (int i=0; i<limit; i++) {
char *temp = strings[i];
if (temp == nullptr) {
finished = true;
break;
}
delete temp;
}
delete strings;
return !finished;
}
#1 楼
由于您已经拥有一个std::vector<std::string>
,让它拥有内存并构建一个并行的std::vector<char*>
更加简单,它仅将指针保留在原始字符串中。唯一的困难是确保在拥有向量超出范围后不使用它。最简单的实现是:
std::vector<char*> strlist(std::vector<std::string> &input) {
std::vector<char*> result;
// remember the nullptr terminator
result.reserve(input.size()+1);
std::transform(begin(input), end(input),
std::back_inserter(result),
[](std::string &s) { return s.data(); }
);
result.push_back(nullptr);
return result;
}
调用方负责管理两个引导程序的生命周期。您可以通过调用
char **
来获得所需的result.data()
。如果要避免潜在的生命周期问题,可以编写一个类来同时拥有两个向量,以便使它们的生命周期同步。
请注意,我故意不使用const ref提取矢量,因为
execve
会使用指向非const char的指针数组,但它不会对内容进行突变,因此无需进行复制。缺乏const安全性在旧的C接口中很常见。PS。当一个简单的循环确实希望进行一些魔术优化时,我并没有强迫自己使用
transform
。我使用transform
而不是简单的循环,因为它可以更清楚地表达我的代码的语义。“简单”循环是功能强大的多用途语言构造,可以执行任何操作。相比之下,
transform
是一种简单的专用算法,因此很容易推理。尤其重要,因为尽管无意,但我还是不得不对参数进行非常量引用进行突变,任何使一眼就看不到可能发生突变的事情都是有用的。
评论
\ $ \ begingroup \ $
我更喜欢这种方法。有一点要注意-我认为您需要在末尾push_back一个nullptr。另外-如果沿着课程路径前进,则需要禁用副本或进行复制或分配操作。
\ $ \ endgroup \ $
–GuyRT
18-10-10在16:06
\ $ \ begingroup \ $
好点都!
\ $ \ endgroup \ $
–没用
18-10-10在16:36
\ $ \ begingroup \ $
哦-另外一件事。我认为您应该使用c_str()而不是data()(不能保证终止为null)。
\ $ \ endgroup \ $
–GuyRT
18-10-10在16:42
\ $ \ begingroup \ $
…和所有已知的实现都以C ++ 11之前的null终止data()的结果。
\ $ \ endgroup \ $
–马丁·邦纳(Martin Bonner)支持莫妮卡(Monica)
18-10-10在16:56
\ $ \ begingroup \ $
@nurettin,存在空指针,因为用户需要它来分隔数组(这是exec()函数家族的接口)。
\ $ \ endgroup \ $
– Toby Speight
18-10-10在17:16
#2 楼
我在这里看到了一些潜在的问题:由于您分配了一个
char*
数组的input.size()
元素,所以result[input.size()]
越界。类似地,
std::string
的size()
是数字字符-不包括C样式字符串所需的结尾strcpy
。因此,这里的每个std::strings
都有可能发生缓冲区溢出(有风险,因为C ++ strcpy
可能在中间包含一个空值,从而在中间终止strings
)。您已经设置了
delete
元素的数量限制您delete strings
,但无论是否违反该限制,都请new <type>[<size>];
。如果超出限制,则存在内存泄漏的风险。 您使用新的
delete [] <object>
的阵列版本。这意味着您需要使用删除q4312079q的阵列版本(请注意方括号)。#3 楼
不同的方法问题中的方法有些程序化。我的目的是使用RAII来简化易用性,因为当前代码可能会泄漏内存并依靠程序员来释放它。
使用案例
首先让我们看一下使用时:
从
std::string
s创建cstring样式数组。将创建的数组沉入
exec
函数族中等待直到子进程退出
回收内存
现在,它显然看起来像构造函数和析构函数调用,因为它们对同一数据进行操作,甚至可能未经修改。
代码
这里是我想到的班级的粗略草图:
class owned_cstrings {
std::vector<char*> cstring_array;
public:
owned_cstrings(const std::vector<std::string>& source) :
cstring_array(source.size())
{
std::transform(source.begin(), source.end(), cstring_array.begin(), [](const auto& elem_str) {
char* buffer = new char[elem_str.size() + 1];
std::copy(elem_str.begin(), elem_str.end(), buffer);
buffer[elem_str.size()] = 0;
return buffer;
});
cstring_array.push_back(nullptr);
}
owned_cstrings(const owned_cstrings& other) = delete;
owned_cstrings& operator=(const owned_cstrings& other) = delete;
owned_cstrings(owned_cstrings&& other) = default;
owned_cstrings& operator=(owned_cstrings&& other) = default;
char** data() {
return cstring_array.data();
}
~owned_cstrings() {
for (char* elem : cstring_array) {
delete[] elem;
}
}
};
设计决策
上面的代码要危险得多。脚枪没有经过深思熟虑。首先,它不是可复制的,尽管它可以进行深层复制,但我认为这不是故意的。不进行深层复制将导致多个删除,这是灾难性的。其次,数据访问受到某种程度的限制,因为涉及的唯一使用情况是陷入
exec
系列功能。演示
Wandbox上的小演示:
#include <vector>
#include <string>
#include <algorithm>
class owned_cstrings {
std::vector<char*> cstring_array;
public:
owned_cstrings(const std::vector<std::string>& source) :
cstring_array(source.size())
{
std::transform(source.begin(), source.end(), cstring_array.begin(), [](const auto& elem_str) {
char* buffer = new char[elem_str.size() + 1];
std::copy(elem_str.begin(), elem_str.end(), buffer);
buffer[elem_str.size()] = 0;
return buffer;
});
cstring_array.push_back(nullptr);
}
owned_cstrings(const owned_cstrings& other) = delete;
owned_cstrings& operator=(const owned_cstrings& other) = delete;
owned_cstrings(owned_cstrings&& other) = default;
owned_cstrings& operator=(owned_cstrings&& other) = default;
char** data() {
return cstring_array.data();
}
~owned_cstrings() {
for (char* elem : cstring_array) {
delete[] elem;
}
}
};
#include <iostream>
template <typename T>
std::ostream& operator<<(std::ostream& os, const std::vector<T>& v) {
if (v.empty()) {
return os;
}
os << v.front();
for (std::size_t i = 1; i < v.size(); ++i) {
os << ' ' << v[i];
}
return os;
}
std::ostream& operator<<(std::ostream& os, char** words) {
while (*words) {
os << *words++ << ' ';
}
return os;
}
int main() {
std::vector<std::string> words = { "What", "a", "beautiful", "world" };
std::cout << words << '\n';
owned_cstrings cstring_words(words);
std::cout << cstring_words.data() << '\n';
}
评论
\ $ \ begingroup \ $
一旦有了这样的类,可惜不要通过提供插入/删除功能使std :: vector
\ $ \ endgroup \ $
– papagaga
18-10-10在7:35
\ $ \ begingroup \ $
我不确定你的意思。我相信您是指该类具有某种修改功能吗?在那种情况下,我将从cstring类开始,因为这将使他们以自己的方式将其组合。无论如何,请记下这个主意,我将在今晚晚些时候尝试实施/讨论。
\ $ \ endgroup \ $
–难以置信
18-10-10在7:38
\ $ \ begingroup \ $
我的意思是非常简单地为owned_cstrings提供一个push_back方法,因此在将其转换为owned_cstrings之前,无需创建std :: vector
\ $ \ endgroup \ $
– papagaga
18-10-10在10:12
\ $ \ begingroup \ $
@papagaga,也许将范围构造函数与初始化列表一起使用会更好?
\ $ \ endgroup \ $
–难以置信
18-10-10在12:44
\ $ \ begingroup \ $
新的char []与删除p?
\ $ \ endgroup \ $
–StoryTeller-Unslander Monica
18-10-10在17:14
#4 楼
首先,可能存在内存泄漏,因为strlist
使用new[]
执行两次分配。如果第一个成功,但是第二个抛出std::bad_alloc
,那么我们就没有引用*result
来引用它。Incomputable的答案显示了如何大大改善接口。
我有一点要说在阅读答案时可能会被忽略,并且可以将效率提高并入原始或提议的RAII对象中。
关键点在于,
delete[]
出于充分的理由而需要引用可变向量-签名应该是char ** strlist(const std::vector<std::string> &input);
// ^^^^^
效率的提高是,我们知道函数/构造函数开始时所有字符串的总存储需求,因此我们可以进行一次分配并将所有字符串放在该字符串中块,而不是为每个要访问的字符串进行单独分配。请参见下面的示例代码。
从C ++ 11开始,我们可以更进一步,使我们的对象成为视图对象,只需存储指向输入字符串
strlist()
的指针(现在需要将其可变即可-考虑一下按值传递,并在有用的地方使用data()
进行调用。)最后,是否有充分的理由认为仅适用于
std::move()
而不适用于其他容器?单-分配方法
如何在
std::vector
上进行两次通过,以节省少量分配。我保留(几乎)原始接口以使更改更明显,但我真的建议您创建一个类型以确保内存管理是自动的。#include <cstring>
#include <string>
#include <vector>
char *const *strlist(const std::vector<std::string>& input)
{
char **result = new char*[input.size() + 1];
std::size_t storage_size = 0;
for (auto const& s: input) {
storage_size += s.size() + 1;
}
try {
char *storage = new char[storage_size];
char *p = storage;
char **q = result;
for (auto const& s: input) {
*q++ = std::strcpy(p, s.c_str());
p += s.size() + 1;
}
*q = nullptr; // terminate the list
return result;
}
catch (...) {
delete[] result;
throw;
}
}
void del_strlist(char *const *strings)
{
// First string is the allocated storage
delete[] strings[0];
delete[] strings;
}
#include <iostream>
int main()
{
std::vector<std::string> args{ "/bin/ls", "ls", "-l" };
auto v = strlist(args);
for (auto p = v; *p; ++p) {
std::cout << '\'' << *p << "'\n";
}
del_strlist(v);
}
使用智能指针的单分配方法
如果我们不介意使用自定义删除器,则可以使用
input
来保存数据:#include <cstring>
#include <memory>
#include <string>
#include <vector>
auto strlist(const std::vector<std::string>& input)
{
static auto const deleter = [](char**p) {
// First string is the allocated storage
delete[] p[0];
delete[]p;
};
std::unique_ptr<char*[], decltype(deleter)>
result{new char*[input.size() + 1], deleter};
// Ensure that destructor is safe (in case next 'new[]' fails)
result[0] = nullptr;
std::size_t storage_size = 0;
for (auto const& s: input) {
storage_size += s.size() + 1;
}
char *p = result[0] = new char[storage_size];
char **q = result.get();
for (auto const& s: input) {
*q++ = std::strcpy(p, s.c_str());
p += s.size() + 1;
}
*q = nullptr; // terminate the list
return result;
}
#include <iostream>
int main()
{
std::vector<std::string> args{ "/bin/ls", "ls", "-l" };
auto v = strlist(args);
for (auto p = v.get(); *p; ++p) {
std::cout << '\'' << *p << "'\n";
}
}
您应该看到它比我的第一个版本更易于编写和使用。
附言这两个演示都使用
std::unique_ptr
进行编译,并且可以在Valgrind下运行,而不会发出警告或错误,以防万一尚未实现。评论
\ $ \ begingroup \ $
同意。我想将更多注意力放在std :: string_view上。如果您不需要管理字符串的内存(只读并且可能是大多数保留大小的操作),则应在string_view而不是string&上运行。
\ $ \ endgroup \ $
–WorldSEnder
18-10-10在14:47
\ $ \ begingroup \ $
在这种情况下可能合适,也可能不合适-如果我们接受std :: string_view作为输入,则需要复制内容以获取终止的NUL。因此,这里是一个摇摆和回旋处。
\ $ \ endgroup \ $
– Toby Speight
18-10-10在14:51
\ $ \ begingroup \ $
如果这很生锈,您可以安全地摆脱掉,只需在c调用期间临时替换该零字节即可。可以在这里做同样的事情,但是这是有问题的做法:D
\ $ \ endgroup \ $
–WorldSEnder
18-10-10在14:55
\ $ \ begingroup \ $
但是应该注意,如果要将其传递给exec()系列,则必须使用NULL指针终止它。
\ $ \ endgroup \ $
–Pryftan
18-10-14在18:00
\ $ \ begingroup \ $
@Pryftan-这就是* q = nullptr的用途,即在返回之前。我什至评论它来解释!
\ $ \ endgroup \ $
– Toby Speight
18-10-15在7:37
#5 楼
存在一些隐含的有问题的假设...您一定不要忘记调用
del_strlist
,并只调用一次。您显然必须通过未知方式从
strlist
调用到发生del_strlist
的位置,即向量的大小。用户必须知道每个字符串的分配方式。由于您的
char**
数据结构是可读写的,因此外面的某个人甚至可能将一个元素更改为另一个元素,并尝试使用malloc
/ free
。所有这些数据聚合/“使用正确的方法”-强烈要求建议编写一个类。
您可以通过提供一个由
class
或std::vector<std::string>
构造的const char**
来避免这些问题,该char**
会将数据存储在本地size_t
中(并在析构函数中自动对其进行分配) )。这已经解决了问题#1。然后您可以在包含数组大小的类中添加
operator[]
成员-这解决了问题#2-现在析构函数知道数组大小。要解决#3,您甚至可以添加一些
char*
,它们可以访问甚至替换索引处的operator char**()
(并以正确的方式重新分配内存)。如果您提供
char**
返回存储分配的字符串的内部(私有)成员,您可以在需要std::vector<std::string> get() const
的任何地方使用此类:) 如果再次需要将存储的数据作为C ++向量,则可以可能想向此类添加q4312079q成员。
#6 楼
execve
之类的函数实际上需要一个char* const *
,因此是指向可变字符串的const
指针数组。因此,strlist
可能会返回char* const *
。strlist
应该通过const引用接受input
参数,因为它不会对其进行修改。char** result = new char*[input.size() + 1];
是必需的,因为附加了附加元素(尾随空指针)。 (array[3]
是一个由3个项组成的数组,索引为0、1、2。)简单地说,需要将字符串分配为
char *temp = new char[input[i].size() + 1];
,因为std::string::size
(或std::string::length
)不计算字符串的尾随0字节。 br />需要std::strcpy
,除非以前使用过using
声明。在像<cstring>
这样的C ++头文件中,C函数位于std
命名空间中。在
del_strlist
中,delete[]
需要使用两次,而不是delete
,因为使用operator new[]
分配了内存。因为应该使用
del_strlist
为了始终完成(返回false),如果没有抛出异常,则最好抛出异常,以便用户无需检查返回值。为了避免需要具有删除功能
del_strlist
,最好有一个包含已分配数组的类,然后在析构函数中对其进行分配。例如:class strlist {
private:
std::vector<char*> cstrings_;
public:
template<typename It>
strlist(It begin, It end) { // does not need std::vector as input
for(It it = begin; it != end; ++it) {
const std::string& string = *it;
char* cstring = new char[string.length() + 1];
std::strcpy(cstring, string.c_str());
cstrings_.push_back(cstring);
}
cstrings_.push_back(nullptr);
}
~strlist() {
for(char* cstring : cstrings_)
if(cstring != nullptr) delete[] cstring;
}
char* const * data() const {
return cstring_.data();
}
};
并使用如下:
void f() {
std::vector<std::string> original_strings = ....
strlist cstrings(original_strings.begin(), original_strings.end());
execve("program", cstrings.data(), nullptr);
// no need to call function to delete cstrings
}
一些较旧的C API可能以
char*
数组作为输入,但实际上不修改数组内容。在这种情况下,不复制新字符串而是使用const_cast
获取指向原始数组中字符串的指针会更简单。例如:std::vector<char*> strlist(const std::vector<std::string>& strings) {
std::vector<char*> cstrings;
for(const std::string& string : strings)
cstrings.push_back(const_cast<char*>(string.c_str()));
cstrings.push_back(nullptr);
return cstrings;
}
但这是一个hack,主要仅用于获得性能/节省内存。
评论
您能解释为什么del_strlist返回布尔值吗?结果用于什么?您能告诉我们您要坚持的C ++标准版本吗?它可以改变界面和首选的代码样式。在过去的十年中,对于C语言而言,在标准方面没有那么大的机会。 C ++ 11与以前有很大的不同,C ++ 14又是一次重大更改(C ++ 17并不是那么显眼的恕我直言)。
也许更有意义的是创建一个execve包装器(采用vector
您通过的char **的寿命是多少?指针是否需要始终保持有效,还是可以在调用后将其丢弃?
这很像c。如果字符串以0终止,我认为Useless的解决方案最简单。只需将指向每个字符串开头的指针存储为char *。通常,c_str()将返回什么。