Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
kernigan_paik.doc
Скачиваний:
0
Добавлен:
01.07.2025
Размер:
2.91 Mб
Скачать

Упражнение 4-4

Спроектируйте и реализуйте библиотеку для записи данных в формате CSV. Простейшая версия может просто брать массив строк и печатать их с кавычками и запятыми. Более интересный вариант — использовать формат­ные строки как printf. В главе 9 вы найдете некоторые полезные советы.

4.4. Реализация на C++

В этом разделе мы напишем версию библиотеки CSV на C++, в которой постараемся преодолеть некоторые ограничения, имеющиеся в С-версии. Нам придется внести некоторые изменения в спецификацию, главным из которых станет то, что функции будут теперь обрабатывать строки C++ вместо массивов символов С. Использование строк C++ автоматически решит некоторые проблемы, связанные с хранением данных, поскольку управлением памятью вместо нас займутся библиотечные функции. Так, в частности, функции работы с полями будут возвращать строки, которые затем могут изменяться вызывающей стороной, — проект получится бо­лее гибким, чем в предыдущей версии.

Класс Csv определяет внешние спецификации, изящно скрывая при этом переменные и функции реализации. Поскольку объект класса ис­черпывающе описывает все состояния экземпляра, мы можем обраба­тывать сразу несколько переменных Csv, при этом каждая из них будет абсолютно независима, так что одновременно могут обрабатываться сразу несколько входных потоков CSV.

class Csv { // читает и разбирает CSV

// пример ввода: "LU",86.25,"11/4/1998", "2:19РМ",+4.0625

public:

Csv(istream& fin = cin, string sep = ",") :

fin(fin), fieldsep(sep) {}

int getline(string&);

string getfield(int n);

int getnfield() const { return nfield; }

private:

istream& fin; // указатель на файл ввода

string line; // вводимая строка

vector<string> field; // строки полей

int nfield; // количество полей

string fieldsep; // символы разделителей

int split();

int endofline(char);

int advplain(const string& line, strings fid, int);

int advquoted(const string& line, string& fid, int);

};

Для конструктора определены параметры, принимаемые по умолча­нию, — такой объект Csv будет читать из стандартного входного потока и использовать обычный символ-разделитель; эти параметры можно из­менить, задав другие значения в явном виде.

Для работы со строками класс использует не строки С, а стандартные С++-классы st ring и vector. Для типа st ring не существует невозможно­го состояния — "пустая" строка означает всего лишь строку нулевой длины, и нет никакого значения, эквивалентного по своему смыслу NULL, который бы мы использовали как сигнал достижения конца файла. Та­ким образом,

Csv: :getline возвращает введенную строку через аргу­мент, передаваемый по ссылке, используя возвращаемое значение для сообщений о конце файла или ошибках.

// getline: получает одну строку,

// по мере необходимости наращивает размер

int Csv::getline(string& str)

{

char с;

for (line = ""; fin.get(c) && !endofline(c); )

line += c;

split();

str = line;

return !fin.eof();

}

Операция += здесь переопределяется, чтобы добавлять символ в строку.

Несколько меньше изменений потребуется вносить в endof line. Нам точно так же придется считывать ввод посимвольно, поскольку ни одна из стандартных функций ввода не может обработать все многообразие вариантов.

// endofline: ищет и удаляет \r, \n, \r\n или EOF

int Csv::endofline(char с)

{

int eol;

eol = (c=='\r’ | | c==’\n’);

if (c == ‘\r’) {

fin.get(c);

if (!fin.eof() && с != '\n’)

fin.putback(c); // слишком много прочитали

}

return eol;

}

А вот как выглядит новая версия функции split:

// split: разделяет строку на поля

int Csv::split()

{

string fid;

int i, j;

nfield = 0;

if (line.length() == 0)

return 0;

i = 0;

do {

if (i < line.length() && line[i] == ‘ “ ‘ )

j = advquoted(line, fid, ++i); // пропуск кавычки

else

j = advplain(line, fid, i);

if (nfield >= field.size())

field.push_back(fld);

else

field[nfield] = fid;

nfield++;

i = j + 1;

} while (j < line.length());

return nfield;

}

Поскольку strcspn не работает со строками C++, нам надо изменить и split, и advquoted. Новая версия advquoted для поиска следующего вхождения символа-разделителя использует стандартную С++-функ-ию find_first_of. Вызов s. find_first_of(fieldsep, j) ищет в строке s первое вхождение любого символа из fieldsep, начиная с позиции j. Если вхождение найдено не было, возвращается индекс, лежащий за кон­цом строки, так что нам надо будет вернуть его обратно в должный диа­пазон. Внутренний цикл for в advquoted добавляет в поле fid все симво­лы, расположенные до ближайшего разделителя.

// advquoted: для полей, заключенных в кавычки;

// возвращает индекс следующего разделителя

int Csv::advquoted(const strings s, string& fid, int i)

{

int j;

fid = "";

for (j = i; j < s.length(); j++) {

if (s[j] == ' '' ‘ && s[++j] != ‘ " ‘) {

int k = s.find_first_of(fieldsep, j);

if (k > s.length()) // разделитель не найден

k = s.length();

for (k -= j; k-- > 0; )

fid += s[j++];

break;

}

fid += s[j];

}

return j;

}

Функция find_first_of используется также и в новой функции Bdvplain, которая обрабатывает обычные, не заключенные в кавычки поля. Еще раз подчеркнем, что необходимость в этом обусловлена тем, что функции языка С вроде strcspn не могут быть применены к строкам C++, которые представляют собой совершенно особый тип данных.

// advplain: для полей, не заключенных в кавычки,

// возвращает индекс следующего разделителя

int Csv::advplain(const string& s, strings fid, int i)

{

int j;

j = s.find_first_of(fieldsep, i); // поиск разделителя

if (j > s.length()) // разделитель не найден

j = s.length();

fid = string(s, i, j-i);

return j;

}

И снова, как и в предыдущей версии, Csv; :getfield абсолютно тривиальна, a Csv: :getnfield настолько коротка, что воплощена прямо в описании класса.

// getfield: возвращает n-е поле string Csv::getfield(int n)

{

if (n < 0 || n >= nfield)

return "";

else

return field[n];

}

Тестовая программа представляет собой несколько упрощенный ва­риант предыдущей версии:

// Csvtest main: тестирует класс Csv

int main(void)

{

string line;

Csv csv;

while (csv.getline(line) != 0) {

cout << "Строка = '" << line << “’\n’;

for (int i = 0; i < csv.getnfield(); i++)

cout << "Поле[" << i << "] = ' “

<< csv.getfield(i) << '"\n";

}

return 0;

}

Использование библиотеки в С++ незначительно отличается от вер­сии на С. В зависимости от компилятора новая версия в сравнении с С-версией дает замедление от 40 % до четырех раз на большом файле из 30 000 строк примерно по 25 полей на строку. Как мы уже выясняли при оценке быстродействия программы markov, подобный разброс зави­сит от степени проработанности используемых библиотек. Последнее, что остается добавить: исходный код версии C++ получился примерно на 20 % короче.

Упражнение 4-5

Введите в версию C++ оператор [ ], чтобы к полям можно было обра­щаться как к csv[i].

Упражнение 4-6

Напишите библиотеку CSV на Java, а затем сравните все три версии с точки зрения простоты и ясности, надежности и скорости.

Упражнение 4-7

Перепишите C++ версию кода CSV с использованием класса STL iterator.

Упражнение 4-8

Версия на C++ предоставляет возможность нескольким независимым экземплярам Csv работать одновременно, никак не мешая друг другу, — в этом выразилось важное достоинство инкапсуляции всего состояния объекта, экземпляры которого можно порождать многократно. Измени­те версию на С так, чтобы добиться подобного эффекта; для этого заме­ните глобальные структуры данных структурами, выделение памяти для которых и инициализация осуществляются явным образом с помощью отдельной функции csvnew.

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]