我正在处理一些较旧的C风格API,例如采用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;
}


评论

您能解释为什么del_strlist返回布尔值吗?结果用于什么?

您能告诉我们您要坚持的C ++标准版本吗?它可以改变界面和首选的代码样式。在过去的十年中,对于C语言而言,在标准方面没有那么大的机会。 C ++ 11与以前有很大的不同,C ++ 14又是一次重大更改(C ++ 17并不是那么显眼的恕我直言)。

也许更有意义的是创建一个execve包装器(采用vector ),而不是尝试维护并行的vector

您通过的char **的寿命是多少?指针是否需要始终保持有效,还是可以在调用后将其丢弃?

这很像c。如果字符串以0终止,我认为Useless的解决方案最简单。只需将指向每个字符串开头的指针存储为char *。通常,c_str()将返回什么。

#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::stringsize()是数字字符-不包括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

所有这些数据聚合/“使用正确的方法”-强烈要求建议编写一个类。

您可以通过提供一个由classstd::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,主要仅用于获得性能/节省内存。