Программирование
On-line приложения
Почитать
Web-сервер Apache
Печать и форматирование
MySQL
Разные рецепты
Сборка/установка
Редактор vi
Справки
Философия
Мой опыт
Скачать
Программы на Tcl/Tk (GUI)
Программы на Python/Tk (GUI)
Программы (CLI)
Help
Хобби
Фракталы
on-line
Язык для рисования фракталов
Гиперкуб
Теория относительности
Ампуллярии
Преподавание
Студенту/абитуриенту
Мой опыт
Автора!

C++: Памятка

Очень долго собирался написать памятку по разным приёмам программирования на C++. Но никак не мог выбрать достаточно компактный способ изложения. Получалось либо непонятно, либо очень длинно. В конце концов, я решил написать небольшой пример, снабжённый комментариями. Здесь, конечно, представлено далеко не всё, что стоило бы включить в памятку, но за-то то, что есть, изложено очень сжато, компактно и, мне кажется, что понятно. Я не планирую останавливаться на этом примере, для других аспектов программирования на C++ я постараюсь придумать другие примеры. Пока же, жду замечаний, предложений и критики этой первой заметки.

Обзор классов примера

Пример организован в виде одного файла. Это не правильно, но здесь я иду на такое решение для упрощения жизни тем, кто захочет разобраться в коде.

Пример содержит три вспомогательных класса и один класс, который несёт основную смысловую нагрузку.

Вспомогательные классы:

  • Place — пара координат. Объект этого класса описывает либо точку с координатами (xo, yo), либо прямоугольник (0 <= x < xo, 0 <= y < yo). Для краткости, я не стал вводить два разных класса для описания этих двух сущностей.
  • PlaceIterator — примитивный итератор, пригодный только для пробегания по всем точкам прямоугольной области. В рамках этого примера никаких других возможностей от итераторов не требуется.
  • Value — объекты этого класса просто содержат скалярное значение.

Основной класс, служащий памяткой, — это шаблон Arena<T>.

  • Arena<T> — это контейнер, который ведёт себя как одномерный массив, но индекс у него — векторный объект Place. То есть Arena<T>, фактически, хранит двумерный массив, обращение к которому выглядит как к одномерному. А благодаря итератору PlaceIterator мы можем пробегать по всем элементам массива, не используя вложенных циклов, — тоже, как в одномерном варианте. В коде это широко используется при инициализации и копировании.

Пример-памятка

// Версия 0.3.1 от 30-11-2009
// (c) 2009 Alexey V Michurin

#include <iostream>

/////////////////////////////////////////////////////////////
//
//  Присказка.
//
//  Здесь описаны вспомогательные классы, необходимые для основного
//  повествования. Они снабжены некоторыми комментариями,
//  но основная часть комментариев находится в следующей части.
//  При первом прочтении присказку можно прочитать бегло,
//  разобравшись в ней только на столько, на сколько это необходимо,
//  чтобы понять сказочку.
//
/////////////////////////////////////////////////////////////

// Декларируем класс.
// Это необходимо, так как мы отказались от использования
// заголовочных файлов и полноценных деклараций. Такой
// подход неприемлем в большинстве случаев. Здесь он используется
// только для того, чтобы сделать пример более компактным.
class PlaceIterator;

// Реализация этого класса далека от полноты и совершенства.
// Он сделан таким, чтобы быть максимально компактным и
// понятным, чтобы проиллюстрировать работу объектов класса
// Arena<T>.
class Place {
private:
    int xx;
    int yy;
public:
    Place(); // для подстраховки реализацию не делаем (см. ниже)
    Place(int x, int y): xx(x), yy(y) {}
    int x() const { return xx; }
    int y() const { return yy; }
    // Работа с итераторами.
    // Здесь только декларации, описания мы сможем сделать
    // только после описания класса PlaceIterator
    PlaceIterator begin() const;
    PlaceIterator end() const;
};

// Теперь объекты Place можно выводить с помощью iostream
std::ostream & operator<< (std::ostream & os, const Place & p) {
    os << "Place(" << p.x() << ", " << p.y() << ")";
    return os;
}

// Строго говоря, этот итератор можно было сделать более
// изящным, воспользовавшись тем, что он сам знает,
// по какой области идёт перебор, а значит, он сам знает,
// когда надо остановиться. Тем не менее, я решил не использовать
// эту возможность и придать итератору более классический
// STL-вид.
// Строго говоря, здесь бы больше подошла не концепция
// итераторов, а концепция интервалов (range)
// (http://www.boostcon.com/site-media/var/sphene/sphwiki/
//         attachment/2009/05/08/iterators-must-go.pdf)
// Кроме того, здесь мы стараемся чётко разделять операцию
// инкремента и операцию получения значения. Это хорошая практика.
// Не следует выполнять в одном месте и доступ к внутренним данным
// и изменение состояния объекта (как это обычно происходит в
// методах типа push; такие методы чреваты и неповоротливы).
class PlaceIterator {
private:
    Place curent;
    int width;
public:
    // Уничтожаем возможность создавать итераторы
    // без указания области, по которой будет идти итерация
    PlaceIterator();
    PlaceIterator(const Place & l): curent(0, 0), width(l.x()) {}
    PlaceIterator(const Place & l, const Place & c):
                     curent(c), width(l.x()) {}
    // Инкремент должен возвращать ссылку на итератор
    // (-Weffc++)
    // Спорно, но лично я разделяю мнение, высказанное тут
    // http://google-styleguide.googlecode.com
    //      /svn/trunk/cppguide.xml#Preincrement_and_Predecrement
    // Пре-инкремент лучше пост-инкремента
    PlaceIterator & operator++() {
        int x = curent.x() + 1;
        int y = curent.y();
        if (x >= width) {
            x = 0;
            ++y;
        }
        curent = Place(x, y);
        return *this;
    }
    // Метод ++ только изменяет внутреннее состояние объекта,
    // метод *, напротив, только извлекает данные, не изменяя
    // состояния. Это упрощает отладку и диагностику; делает
    // работу с объектом более упорядоченной.
    const Place & operator*() const {
        return curent;
    }
    // Для краткости, сделана такая кривоватая реализация
    // сравнения.
    // Хорошей практикой была бы честная реализация operator== и
    // за тем operator!= как !(*this == other)
    bool operator!=(const PlaceIterator & o) const {
        return (curent.x() != o.curent.x() && curent.y() != o.curent.y());
    }
};

// Только после того, как описан класс PlaceIterator мы можем
// описать методы класса Place, связанные с итераторами.
// Пришлось сделать такую корявость, чтобы не разбивать
// пример на несколько файлов.

PlaceIterator Place::begin() const {
    return PlaceIterator(*this);
}

PlaceIterator Place::end() const {
    return PlaceIterator(*this, *this);
}

// Класс Value сделан просто для подстановки в шаблон
// Arena<T>.
class Value {
private:
    int vv;
public:
    Value(): vv(-1) {}
    Value(int v): vv(v) {}
    int v() const { return vv; }
};

// Теперь объекты Value можно выводить с помощью iostream
std::ostream & operator<< (std::ostream & os, const Value & v) {
    os << "Value(" << v.v() << ")";
    return os;
}

/////////////////////////////////////////////////////////////
//
//  Сказочка.
//
//  Класс-памятка, иллюстрирующий различные аспекты,
//  о которых не надо забывать, при программировании на C++
//
/////////////////////////////////////////////////////////////

template<class T>
class Arena {

    // Шаблон оператора вывода сделан дружественным --
    // распространённый приём.
    template<class U>
    friend
    std::ostream & operator<< (std::ostream & os, const Arena<U> & v);

private:
    // Порядок инициализации переменных в конструкторах
    // будет в точности таким, как настоящий порядок объявлений.
    // (-Wall)
    // Для полной безопасности конструкторов, лучше использовать
    // обёртки для указателей типа auto_ptr. Имеется в виду
    // ситуация, когда исключение обрывает работу конструктора на
    // середине, а деструктор при этом не вызывается. В этом
    // случае уже созданные с помощью new и new[] объекты,
    // не будут уничтожены и получится утечка ресурсов.
    Place size;
    T * values;

public:
    // Так как для объектов этого класса нет смысла в конструкторе
    // без параметров, то мы декларируем этот конструктор, но не
    // создаём для него реализацию. Это приведёт к возникновению
    // ошибок на стадии компиляции, если кто-то попробует создавать
    // элементы этого класса без параметров. Если мы не создадим
    // конструктор без параметров, то компилятор создаст его за нас,
    // а это совсем не то, что нам нужно.
    Arena();
    // Штатный конструктор. Создаёт арену заданных размеров.
    Arena(const Place & p):
        size(p),
        values(new T[p.x()*p.y()]) // для каждого new в деструкторе
    {                              // должен быть delete
        std::cout << "create Arena at " << this << std::endl;
    }
    // Конструктор копирования нужен почти всегда, когда
    // среди членов класса есть ссылки.
    // (-Weffc++)
    // Если мы создадим конструктор копирования, то компилятор
    // создаст его автоматически.
    Arena<T>(const Arena<T> & a):
        size(a.size),
        values(new T[size.x()*size.y()])
    {
        std::cout << "create copy Arena at " << this <<
                     " from " << &a << std::endl;
        for (PlaceIterator i = size.begin(); i != size.end(); ++i) {
            (*this)[*i] = a[*i];
        }
    }
    // Если вы определяете оператор [], то недурственно
    // определить оператор * так, чтобы эти два оператора
    // не конфликтовали. Одним словом, лучше не переопределять
    // оператор [], хотя часто это удобно.
    T & operator[](const Place & p) {
        return values[size.x()*p.y() + p.x()];
    }
    // const-версия нужна обязательно, она используется
    // для const-объектов (см. комментарий в operator=).
    // Но полноценный путь -- создание ещё и метода at(i) --
    // аналога const-версии operator[]; это позволит пользователю
    // класса точно указывать метод доступа -- const/не-const для
    // любого (const/не-const) объекта.
    const T & operator[](const Place & p) const {
        return values[size.x()*p.y() + p.x()];
    }
    // Оператор присвоения нужен почти всегда, когда среди
    // членов класса есть ссылки.
    // (-Weffc++)
    // Кроме того, оператор присвоения должен всегда возвращать
    // ссылку на *this.
    // (-Weffc++)
    // Компилятор создаст оператор присвоения автоматически, если
    // мы этого не сделаем сами. Это не всегда хорошо.
    Arena<T> & operator=(const Arena<T> & a) {
        if (this == &a) { // обязательно проверяем на
            return *this; // присвоение самому себе
        }
        size = a.size;
        delete [] values;
        values = new T[size.x()*size.y()];
        for (PlaceIterator i = size.begin(); i != size.end(); ++i) {
            // Для *this используется
            // T & operator[](const Place p)
            // так как объект, на который мы получаем ссылку
            // должен быть изменяемым.
            // Для a используется
            // const T & operator[](const Place p) const
            // так как a является const.
            // Скобки вокруг *this необходимы.
            (*this)[*i] = a[*i];
        }
        return *this;
    }
    // Компилятор автоматически создаёт методы взятия адреса
    // объекта и константного объекта (это разные методы).
    // Здесь мы их переопределять не будем, но об этом полезно помнить.
//  Arena<T> * operator&();
//  const Arena<T> * operator&() const;
    // В базовых классах деструкторы должны быть виртуальными
    // (-Weffc++, -Wnon-virtual-dtor)
    ~Arena() {
        std::cout << "delete Arena at " << this << std::endl;
        delete [] values; // при удалении массивов, не забываем "[]"
    }
};

// Довольно топорненькая функция вывода для Arena<T>.
// Благодаря дружественности (см. выше), имеет доступ
// к приватным данным класса Arena<T>.
template<class T>
std::ostream & operator<< (std::ostream & os, const Arena<T> & v) {
    os << "Arena at " << &v << " (size=" << v.size << "):";
    for (PlaceIterator i = v.size.begin(); i != v.size.end(); ++i) {
        if ((*i).x() == 0) {
            os << std::endl;
        }                 // видимыми в коде
        os << "\040" << v[*i]; // пробельные символы полезно делать видимыми
    }
    return os;
}

/////////////////////////////////////////////////////////////
//
//  D E M O
//
/////////////////////////////////////////////////////////////

// Пример позволяет убедиться, что объект Arena
// ведёт себя адекватно. Корректно создаётся, удаляется,
// копируется, присваивается, изменяется.
void test() {
    Arena<Value> a(Place(2, 2)); // две строки по два элемента
    Arena<Value> b(Place(3, 2)); // две строки по три элемента
    std::cout << "Arena a = " << a << std::endl;
    std::cout << "Arena b = " << b << std::endl;
    b[Place(0, 0)] = 10;
    std::cout << "Arena b = " << b << std::endl;
    a = b;
    std::cout << "Arena a = " << a << std::endl;
    a[Place(1, 0)] = 20;
    Arena<Value> c(b);
    c[Place(2, 0)] = 30;
    std::cout << "Arena a = " << a << std::endl;
    std::cout << "Arena b = " << b << std::endl;
    std::cout << "Arena c = " << c << std::endl;
}

int main() {
    // раскомментируйте цикл, чтобы убедиться
    // в отсутствии утечек памяти
    //while (true) { 
    std::cout << "Begin test." << std::endl;
    test();
    std::cout << "End test." << std::endl;
    //}
    return 0;
}

Компиллировать

c++ -Wall -Wextra -pedantic -Weffc++ file.cpp

Эта страница набрала немалую популярность, её посещает множество людей, поэтому я решил провести небольшое исследование. Если у вас есть комментарий, если вы сочли полученную информацию полезной, не полной, или вообще бесполезной, вы можете высказать своё мнение, пожелания, дополнения.

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

© 1999 − 2010 Мичурин Алексей — http://www.michurin.com.ru/