Магия векторов в C++


#1

Здравствуйте. Пишу под linux на C++, выполняю ДЗ. В процессе выполнения столкнулся с преинтереснейшей проблемой.

#include <iostream>
#include <fstream>
#include <array>
#include <vector>
#include <unistd.h>
#include <string.h>

using namespace std;

int main(int argc, char *argv[], char** envp) {

    if(argc < 3)
        return 1; // Выходим и возвращаем код ошибки, если аргументов меньше, чем нужно

    std::vector<char *> vEnv; // Вектор, содержащий переменные из файла
    std::vector<char *> cEnv; // Вектор, содержащий текущие переменные окружения
    char * arguments[50]; // Массив аргументов

    /*
     * argv[0]: Имя текущего файла
     * argv[1]: Имя запускаемого файла
     * argv[2]: Режим работы
     * argv[3..(argc - 1)]: Аргументы для файла
     * argv[argc]: 0-байт
     * */
    int i = 0;
    for(; i < (argc - 2); ++i)
    {
        arguments[i] = argv[i + 2];
    }
    arguments[i] = (char *) 0;

    // Чтение переменных из файла env.ini и их запись в вектор
    ifstream fEnv("env.ini");
    if(fEnv.is_open())
    {
        for (string line; getline(fEnv, line);)
        {
            char row[1024];
            strcpy(row, line.c_str());
            vEnv.push_back(row);
            // cout << vEnv[vEnv.size() - 1] << endl;
        }
        fEnv.close();
    }

    char** env;
    for (env = envp; *env != 0; env++)
    {
        char * row2 = * env;
        cEnv.push_back(row2);
    }

    // Перенос значений из векторов в результирующий массив
    unsigned long size = vEnv.size() + cEnv.size() + 1;
    char * resultArray[size];
    unsigned long j = 0;
    if(!strcmp("--replace", argv[2])) {
        for(std::vector<char *>::iterator vt = vEnv.begin(); vt != vEnv.end(); ++vt){
            resultArray[j++] = * vt;
        }
    }
    else if(!strcmp("--append", argv[2])) {
        for (unsigned i = 0; i < vEnv.size(); i++) {
            resultArray[j++] = vEnv[i];
            // cout << i << ". " << vEnv[i] << endl;
        }
        for(std::vector<char *>::iterator it = cEnv.begin(); it != cEnv.end(); ++it) {
            resultArray[j++] = *it;
        }
    }
    resultArray[j] = (char *) 0;

    // Вызов программы
    execve(argv[1], arguments, resultArray);
    return 0;
}

Почему программа при проходе по вектору vEnv пишет в массив resultArray абсолютно одинаковые значения? Нужно отметить, что когда я забираю эти значения из файла env.ini, всё в порядке. Настолько в порядке, что даже закомментированный cout выдает разные значения, как и должно быть. В чем секрет, в чем хитрость, где я ошибся?

Компилятор gcc 4.8.2, ОС Ubuntu 14.04.1 LTS.

P.S.: Цикл с итератором был заменен на цикл с числом намеренно, я пытался найти проблему, с итератором проблема не решается.


#2

Ничего особо интересного, вы неверно работаете с указателями и статическими массивами вот здесь:

char row[1024];
strcpy(row, line.c_str());
vEnv.push_back(row);

Формально это UB, потому что после завершения каждой итерации массив row перестаёт существовать: в векторе остаётся указатель неизвестно на что. По факту выходит, что это одно и то же место в памяти и у вас все указатели вектора смотрят на это место.

Поменяйте тип row на string или ну худой конец:

char * row = new char[line.size() + 1];

Тогда в конце должен быть цикл delete, конечно.


#3

Ошибку понял, большое спасибо!


#4

Предложу другой вариант

vEnv.push_back(line.c_str());

#5

@RS, так не выходит, в консоли вылезают кракозябры… С чем это связано, я ума не приложу.


#6

Видимо с кодировкой.


#7

Нет, так тоже неверно: функция-член c_str возвращает так называемый указатель на представление объекта, который можно использовать, пока жив объект, но тут line живёт тоже только внутри цикла. Есть только два корректных варианта получения неограниченного числа кусков памяти: создание нужного числа объектов string (например, через vector<string>::push_back) или выделение этой памяти вручную, как я писал выше (это хуже).

Позволю себе абзац оффтопа (click to unhide).

Данное обсуждение показывает, что язык C++ лучше не использовать, если у вас не было приличного курса по нему: это сложный язык, который нелегко освоить в одиночку. Иначе он превращается, как в названии старой книги, в верёвку достаточной длины чтобы выстрелить себе в ногу. Этим он отличается от PHP и 1С, например. И, кстати, про План счетов, о котором писал @RS в другое теме: у меня он был в лицее (экономическом) на курсе бухгалтерии, и я бы не сказал, что это что-то сложное (сравнимое, например, с C++).


#8

Копирование указателей на строки в массив arguments можно заменить простым присваиванием:

char **arguments = argv + 2;

Этот вариант проще, к тому же он будет правильно работать, если количество параметров командной строки больше 52.


#9

Да, я выстрелил себе в ногу. Был уверен что копию. Кстати, оно еще и const char * возвращает. Мне решение со vector<string> больше нравится, чем остальные.


#10

А всё потому, что в C++ нет управляемой памяти со сборщиком мусора. Собственно, известно, что в C++ размерная модель памяти. То есть, штатное средство - копировать объекты. Если же это непроизводительно, приходится самому писать управление динамической памятью, организуя подсчет ссылок и т.п.


#11

Уже давно ничего самому писать не надо, потому что есть такая вещь, как умные указатели(пример). И вообще если соблюдать стиль с++ (использовать string вместо указателей на char, отказаться от владеющих сырых указателей, не использовать cstdlib с её strcpy и прочим огнестрельным оружием(времена когда стандартная библиотека c++ не была самодостаточной уже давно прошли)), то писать становится гораздо проще.


#12

Обратите внимание, что в этой задаче по другому нельзя. Т.к. в итоге требуется запустить программу с аргументами командной строки, которые могут быть только char*.


#13

Я бы с удовольствием воспользовался best practies of c++, но системный вызов execve(), вокруг которого и городятся все эти костыли, и вокруг которого крутится суть решаемой задачи ‒ запустить другое приложение с такими-то аргументами и переменными окружения запускающей программы, которые либо заменяются переменными из файла, либо дополняются ими ‒ не дружит с типом string.


#14

Без владеющих сырых указателей? Можно, конечно.


#15

Написать обертку в несколько строк конечно не в силах.


#16

Для человека, у которого практически не было опыта работы с C++ и нет представления о том, что вообще есть в этом языке и какие практики написания кода существует, пока да, сил не хватает :]


#17

Я просто оставлю это здесь: Modern C++: What You Need to Know


#18

@AndrewRudenets как раз для низкоуровневых системных интерфейсов и сделали функцию-член string::c_str. Про обёртки Нолан пишет какую-то ерунду: никогда не доверяйте тому, что пишут на русскоязычных форумах по программированию, кроме случаев, когда это лично знакомые вам квалифицированные люди. Нолан явно понахватался каких-то умных слов и ставит их в произвольных местах.


#19

Да, безусловно, лучше стрелять в колено, чем использовать более-менее высокоуровневые конструкции и написать на две строчки больше.

UPD: Про обёртку я, конечно, погоречился: тут всё гораздо проще, но всё-равно считаю, что из-за подобных проблем лучше избегать использования си строк.


#20

Всем спасибо, решение найдено! Заменил тип вектора vEnv с char * на string и запись из векторов в результирующий массив организовал так:

for (unsigned i = 0; i < vEnv.size(); i++) {
        resultArray[j++] = (char *) vEnv[i].c_str();
}