23 сент. 2009 г.

Блог переехал

И теперь он не совсем про C++. Но все равно должно быть многим интересно.
http://typedef.ru

22 мая 2007 г.

Простой пример файлового ввода-вывода

Ну, поглядим, как это все работает. Допустим нам надо вывести числа, хранящиеся в векторе, в файл через пробелы – достаточно распространенная задача. Для этого можно написать примерно такую функцию:
#include <fstream>
#include <string>
#include <vector>
#include <iostream>
using namespace std;
int output(vector<int> &values, string filename)
{
ofstream ofile(filename.c_str());
if (!ofile)
{
cerr << "Error opening file!" << endl;
return -1;
}
for (int i = 0; i < values.size(); ++i)
ofile << values[i] << ' ';
ofile.close();
return 0;
}

На мой взгляд, код достаточно очевидный, чтобы его объяснять построчно. Обращу внимание только на то, что вывод в файл (ofile << values[i] << ' ';) и вывод в стандартный поток ошибок (cerr << "Error opening file!" << endl;) выполняются одинаково, с помощью переопределенного оператора << . А таинственный cerr (наряду с cout и clog) – это всего лишь объект класса ostream, определенный в файле iostream; работает он точно так же, как и созданный нами собственноручно ofile (ну, вообще-то, работает не совсем так же, но тонкости рассмотрим позже).
Справедливости ради создадим функцию, которая выполняет обратное действие – считывает последовательность чисел из файла:
int input(vector<int> &values, string filename)
{
ifstream ifile(filename.c_str());
if (!ifile)
{
cerr << "Error opening file!" << endl;
return -1;
}
int v;
while (!ifile.eof())
{
ifile >> v;
values.push_back(v);
}
ifile.close();
return 0;
}

Объект потока не обязательно должен лежать на стеке, возможны всевозможные варианты:
// можно так (на стеке)
ifstream ifile;
ifile.open(filename.c_str());
// а можно и так (в куче)
ifstream *ifile = new ifstream;
ifile->open(filename.c_str());
// можно даже так
ifstream *ifile = new ifstream(filename.c_str());

Домашнее задание: замените ofstream на ostrstream, ifstream на istrstream, а имя файла - на адрес char-строки. Убедитесь, что ввод-вывод на символьных строках выполняется точно так же, как и файловый.

Библиотека iostream

Как и в старичке C, в C++ нет встроенных средств для ввода и вывода данных куда бы то ни было. Редкие в наше время программисты на C++ под MS-DOS, да и вообще старшее поколение программистов, могут возразить – мол, ввод и вывод суть чтение и запись по определенным адресам в оперативной памяти (угу, а программирование суть написание машинных команд). Однако это мало того что неудобно, так еще и обладает такими нехорошими свойствами, как абсолютная платформенная непереносимость, а также несовместимость с объектно-ориентированной парадигмой. Короче говоря, по абсолютным адресам в памяти мы писать не хотим, и волшебные слова «контроллер», «DMA» и «драйвер» оставим тем, кому это интересно.
К счастью, добрые люди присобачили к C++ стандартную библиотеку, выдержанную в традициях ООП и позволяющую все, что только душа пожелает (ну, почти).
В этой и нескольких следующих статьях я собираюсь дать небольшое обзорное описание библиотеки iostream, но несколько нетрадиционное. Обычно авторы книг по C++ начинают с элементарных
cout << “Hello, World!” << endl;

и только потом описывают манипуляторы, работу с файлами и т.д. Прилежные ученики послушно переписывают примеры из книги, убеждаются, что они работают, и запоминают на всю жизнь, что cout<< – это такая хитрая языковая конструкция, позволяющая выводить на экран строки, и совершенно не связанная с файловым вводом-выводом. В головах возникает каша, которую потом долго приходится разгребать (зато сколько будет приятных сюрпризов!). Я пойду по несколько другому пути – начну с самых основ и, продвигаясь наверх, в сторону конкретизации абстрактных понятий, покажу, как там все на самом деле работает. Начнем с того, из чего состоит библиотека iostream.
Каждый уважающий себя компилятор C++ поставляется со стандартной библиотечкой ввода-вывода по имени iostream. Ключевым понятием в этой библиотеке является поток (stream) – не путать с потоком управления (thread)! Не вдаваясь в детали реализации, можно сказать, что поток – это объект, который может служить источником или приемником байтов (или и тем, и другим сразу). Не все сразу могут понять отличие потоковых средств ввода-вывода от традиционных read/write. Суть этих отличий в том, что поток – это более высокая ступень абстракции; он может использоваться как, например, для чтения из файла (и при этом он ведет себя почти как обычный read), так и для чтения данных из последовательного порта или с клавиатуры. В последнем случае данные уже имеют ярко выраженную потоковую природу.
Кстати, надо заметить, что консерваторы по-прежнему могут использовать привычную, аки домашние тапочки, библиотеку stdio, но в таком получается меньше контроля над потоками, меньше удобства и меньше соответствия парадигме ООП.
Для начала посмотрим, какие в библиотеке iostream есть заголовочные файлы:
  • fstream – классы, инкапсулирующие ввод-вывод на внешних файлах.
  • ios – базовые классы iostream; обычно этот заголовочный файл не нужно включать напрямую, он включается в других заголовочниках.
  • iostream – определяет объекты ввода-вывода на стандартных потоках.
  • ostream – определяет шаблонный базовый класс basic_ostream, управляющий выводом в потоки; этот файл также обычно не включается в программу напрямую.
  • streambuf – определяет шаблонный базовый класс basic_streambuf, управляющий буферизацией; не нужно включать напрямую.
  • iomanip – определяет ряд манипуляторов (про них ниже).
  • iosfwd – объявляет (но не определяет!) кучу шаблонных классов и typedef’ов.
  • istream – брат-близнец ostream, только в отношении потокового ввода.
  • sstream – определяет ряд классов для потоковой работы с хранящимися в памяти последовательностями.
  • strstream – определяет ряд классов для потоковой работы с char-строками.
Как нетрудно догадаться, большую часть этих заголовочников нормальный программист никогда в своей жизни не увидит (и хорошо). Обычно используются только fstream, iostream, iomanip, sstream, strstream и соответствующие им классы.
Теперь рассмотрим структуру и использование одной из частей библиотеки iostream, а именно классы для потокового ввода-вывода на файлах. Работа с ними ничуть не отличается от потокового ввода-вывода на памяти, на строках или на стандартных потоках (то есть консоли и клавиатуре).
Заголовочный файл fstream определяет ряд классов: filebuf, wfilebuf, fstream, wfstream, ifstream, wifstream, ofstream, wofstream, basic_filebuf, basic_fstream, basic_ifstream, basic_ofstream. На самом деле из приведенного списка только последние четыре пункта – классы. Они определяются следующим образом:
template <class Elem, class Tr = char_traits<Elem> >
class basic_filebuf : public basic_streambuf<Elem, Tr>
template <class Elem, class Tr = char_traits<Elem> >
class basic_fstream : public basic_iostream<Elem, Tr>
template <class Elem, class Tr = char_traits<Elem> >
class basic_ifstream : public basic_istream<Elem, Tr>
template <class Elem, class Tr = char_traits<Elem> >
class basic_ofstream : public basic_ostream<Elem, Tr>

Они очень похожи – шаблонные классы с двумя параметрами: Elem – собственно, тип вводимо-выводимых элементов, и Tr – класс символьных характеристик этих элементов. Tr обязательно должен быть типа char_traits (лучше оставлять указанное по умолчанию char_traits<Elem>).
Остальные имена из приведенного выше списка – всего лишь специализации этих классов; например, классы ofstream и wofstream, отвечающие, соответственно, за вывод в файл последовательностей char- и wchar-символов, определяются так:
typedef basic_ofstream<char, char_traits<char> > ofstream;
typedef basic_ofstream
<wchar_t, char_traits<wchar_t> > wofstream;
В следующий раз посмотрим, как вся эта радость работает.

19 апр. 2007 г.

CppUnit для ленивых

Приспичило мне однажды овладеть юнит-тестированием кода C++. Так приспичило, что сил никаких нет - ни есть не могу, ни спать. Прошвырнувшись по ближайшим форумам, я понял, что не один такой, и что страждущих немало, но все они в каких-то непонятках. Казалось бы - чего тут сложного? Ан нет, каждый свое защищает: линуксоиды все как один говорят, что "у них все есть, но виндузятникам этого не понять", виндузятники раскидывают по сторонам слюни, описывая некую приблуду к Visual Studio, а все остальные тихо сидят и посмеиваются. В общем, черт их разберет, кто прав. Единственное название, упоминающееся более-менее часто - это CppUnit.
Вспомнив, как классно было работать с NUnit, я глядел на прогресс-бар скачивания дистрибутива, счастливо улыбаясь и предвкушая легкую жизнь. Но суровая реальность оказалась жестокой - ничего напоминающего мои прошлые опыты с инструментами тестирования я там не нашел.
Началось все с того, что меня заставили компилировать библиотеки самостоятельно. Но то ли авторы не удосужились проверить свое творение на последних версиях Студии, то ли Студия мне попалась кривая, но компиляция у меня шла, спотыкаясь на каждом шагу и ругаясь на чем свет стоит. В результате я все же смог получить нужные либы, правда, только для режима отладки.
Следующим шагом было прочтение readme.txt, который по совместительству является этаким сборником рецептов. Следуя инструкциям, я пытался заставить свои тесты запускаться с помощью GUI-утилиты TestPlugInRunnerd.exe, но потратив на это несколько часов, убедил себя, что GUI - это для маленьких, а настоящие дядьки работают в консоли, на чем и успокоился.
Запустить тесты в консоли оказалось не в пример проще. Ниже приведу последовательность действий, актуальную для Visual Studio 8 (2005).
1) Добавляем каталог include установленного CppUnit в стандартные пути (Tools - Options - Projects and Solutions - VC++ Directories).
2) Добавляем в проект ссылку на cppunitd.lib (в свойствах проекта: Configuration Properties - Linker - Input).
3) Пишем тест примерно такого вида:
#include <cppunit/TestFixture.h>
#include <cppunit/extensions/HelperMacros.h>

class MyTest : public CppUnit::TestFixture
{
CPPUNIT_TEST_SUITE(MyTest);
CPPUNIT_TEST(myTest1);
CPPUNIT_TEST(myTest2);
CPPUNIT_TEST(myTest3);
CPPUNIT_TEST_SUITE_END();
public:
void setUp() {
// инициализация
}
void tearDown() {
// деинициализация
}
void myTest1() {
CPPUNIT_ASSERT(true);
}
void myTest2() {
CPPUNIT_ASSERT(true);
}
void myTest3() {
CPPUNIT_ASSERT(false &&amp; true);
}
};

4) Пишем функцию main() примерно так:
#include <cppunit/ui/text/TestRunner.h>
#include "MyTest.h"

CPPUNIT_TEST_SUITE_REGISTRATION(MyTest);

int main()
{
CppUnit::TextUi::TestRunner runner;
CppUnit::TestFactoryRegistry &registry = CppUnit::TestFactoryRegistry::getRegistry();
runner.addTest( registry.makeTest() );
runner.run();
return 0;
}

5) Запускаем и радуемся. Если появляются новые тесты, то для каждого из них (я имею в виду для каждого Test Suite) нужно добавить строку вида CPPUNIT_TEST_SUITE_REGISTRATION(...) в файл с функцией main(). После запуска программа выдаст на консоль информацию о выполненных тестах, и если какие-то из них не прошли, укажет причину ошибок и соответствующие номера строк в исходных файлах.

Вот так, дешево и сердито. Если кто знает, как делать то же самое, но с помощью GUI-утилиты, буду рад у вас сонно поучиться.