Читаем Идиомы и стили С++ онлайн бесплатно

<p>Albert Makhmutov</p><p>Идиомы и стили С++</p><p>Шаг 1 - Введение. Зачем все это надо и что это такое.</p>

Чтобы сразу пояснить свой план, сообщу - здесь и далее я собираюсь вместе с Вами разобрать следующие темы:

1. Объекты-указатели на другие объекты.

2. Объекты-интерфейсы к другим объектам.

3. Использование шаблонов как средства безопасности.

4. Массивы данных, итераторы и курсоры.

5. Нестандартное управление памятью.

6. Разное.

Давайте посмотрим, что мы имеем на этот день. Если Вы добрались сюда, и начали читать, значит Вы облазили FirstSteps по всем разделам, вероятно, списали часть разделов себе на винчестер, и держите на столе пачку книг по программированию. Предполагаю так же, что Вы имеете за своей спиной 2-4 работающих проекта на FoxPro, Delphi или C/C++. То есть опыт имеете, но супермастером себя ПОКА не считаете. Но почему, собственно? Что нам мешает? Разве в Lotus или Microsoft работают одни гении (звучит прикольно, кстати)? Почему какие-то ребята вполне способны написать Windows или PhotoShop, или там DeltaForce, а нам то что мешает? Вы только подумайте - С++ изобрел один Бьярн Страуструп. Один! И перевернул мир. А STL придумал Алексей Степанов. Один. Сейчас в Хьюлет-Паккарде сидит. Тоже мир перевернул. (Если не верите, сравните второе и третье издание Страуструпа, "Язык программирования С++").

Похоже где-то есть неслабый пробел. Ну, может Вас, в институте учили разным штучкам и фенечкам в программировании, а меня, помнится, учили, что: "Компьютер состоит из монитора, системного блока и клавиатуры; при выходе из строя системного блока необходимо заменить системный блок". И если про бинарные деревья и хэш-таблицы можно почитать у Д. Кнута, то есть огромная область, которой похоже не учат до сих пор. Похоже на то, как если Вы выучили иностранный язык, и можете на нем читать-писать, а вот как разговорная речь так вроде все слова понятны, а вместе не ложатся. (Христос за это апостолов корил: "Какое слово вам непонятно!") Почему? Потому, что слова в разговорной речи, а равно и в программировании, складываясь, могут приобретать иной смысл и становиться фразеологизмами, или идиомами. И нам этот смысл надо постигать посредством чтения умных книжек. А что там? А там посмотришь на прилавки - выбор каков - "С++ для тупых", "С++ для Бивиса и Батт-хеда" да плюс справочники по функциям и алгоритмам. Но того, о чем я говорю, практически нет.

Итого получается, что не хватает нам того, чтобы секреты мастерства нам кто-то передавал. Секретов-то много, своим умом не дойдешь. Это в древности хорошо было - помирает старый мастер, перед смертью в двух словах раз-два, сказал пару слов и глазки закатил. Ну вы знаете - "Как ты делаешь такой вкусный чай" - "Ложите побольше заварки!". С программированием такого не бывает. Надо секрет загодя узнавать, мастера душить. Найти бы его только. Но делать то что-то надо!

Так вот, что я предлагаю: Я тут сижу и прорабатываю пачку книжек на эту тему. По мере продирания через них буду шкрябать статейки, и размещать их на FS. Надеюсь, авторы не обидятся. Напишу пяток-десяток, если будут отклики - продолжу. Если нет - значит не надо.

Насчет чего статейки будут - я выше перечислил. Еще будут объекты, поддерживающие транзакции, блокировки, очереди запросов на транзакции, имитация (в том числе и безопасная) массивов, инкапсуляция небезопасных типов в безопасные шаблоны, итераторы, создающие упрощенные копии сканируемого массива, управление памятью с оптимизацией и сборкой мусора, обработка исключений, виртуальные конструкторы(!), двойная и N-мерная диспетчеризация. Во всяком случае я это планирую (Самому страшно от того, что я перечислил; Но может силов мущинских моих не хватит, тогда не знаю…) Все это по силам практически любому, кто досюда вообще дочитал, и не помер со скуки. А пользы, как говорил Коровьев, он же Фагот, "от этого пения целый вагон". Вот например, самое простое - объект-указатель при отладке и на ранней стадии реализации проекта может управлять безопасным и низкопроизводительным массивом, а в релиз версии - самым обычным. При этом вы пользуетесь объектом-указателем как самим массивом.

Ну и ладно. Закончим со вступлением.

На всякий случай укажу свои основные источники, у которых я беру поносить идеи и мысли на время:

1. Jeff Alger. C++ for real progammers. (Джефф Элджер. С++).

2. James Coplien. Advanced C++ Styles and Idioms.

3. Мейерс, Скотт. Эффективное использование С++.

4. Страуструп, Бъярни. Язык программирования С++. Третье издание.

5. Буч, Гради. Объектно-ориентированное проектирование с примерами на С++. Второе издание.

6. Бабе, Бруно. Все о С++.

7. Microsoft corp. Официальное пособие по разработке приложений на VC++6.0

Вообще говоря, в основном все буду брать у Элджера. Проще, и интереснее. Мы же не умереть собираемся.

Ну и все. Начнем, пожалуй.

<p>Шаг 2 - Умные указатели.</p>

Сначала договоримся о терминах. Перегрузка функций (overloading) - многократное определение функции с разным набором аргументов (сигнатурой). В сигнатуру включается модификатор const, но не включается возвращаемое значение. Перегрузка операторов (overloading) - то же самое (операторы это собственно функции, только имя у них предопределенное), ПЛЮС само определение такого оператора. Если вы определили оператор для какого-то класса, это уже считается перегрузкой. Переопределение функций (overriding) - определение функций в субклассах с тем же именем. Правила переопределения функций достаточно сложны, и я не хотел бы грузить вас ими. Скажу только, что в Object Pascal они не в пример яснее, и компилятор укажет вам, что вы тормоз, если тормознете. А в Плюсах компилятор просто скроет функции, которые вы переопределите неправильно. Ну мы ему отомстим.

В C++ мы можем перегрузить почти все операторы, за исключением нескольких. Во всяком случае, оператор -› перегружается, и это имеет значение крайне важное. Кстати, он называется селектором (member selector). Итак, попробуем:

#include ‹mem.h›

class Cthat {

public:

 void doIt(void){return;};

};

class CPthat {

private:

 Cthat* aThat;

public:

 CPthat(Cthat* _that=NULL):aThat(_that){}

 ~CPthat { if (aThat) delete aThat; }

 operator Cthat* { return aThat;} // Оператор преобразования типа

 CThat* operator-› { return aThat; }; // Оператор селектора -›

 CPthat operator+(ptrdiff_t _offset) { return CPthat(aThat+_offset); }

// ^^^^^^^^^

};

int main {

 Cthat* aThat = new Cthat;

 aThat-›doSomething;

 CPthat pthat(new Cthat);

 pthat-›doIt; // Вариант обращения через -›

 ((Cthat*)pthat)-›doIt ; //Вариант обращения через Cthat*

 delete aThat;

 return 0;

}

Что получилось: Имеем класс Cthat, который может иметь экземпляры, хотя и не имеет наполнения, и может исполнить пустую функцию. (Обратите внимание. Пустой объект имеет размер 1, и если добавить переменную char, то размер будет тот же. Экземпляры пустых объектов существуют, и они различаются.) Имеем класс объекта-указателя CPthat, в котором храним обычный указатель, но доступ к нему ограничиваем, и перегружаем для него операторы:

1. приведения типа Cthat

2. member selector -›.

3. Операторы арифметики указателей. Я указал только один, сложение.

Идея ясная. Нужно переопределить все восемь, или не переопределять их вовсе. Вопрос в том, направлен ли Ваш указатель на массив, или нет. Во всяком случае, не спешите с этим. Да, и в Ваших плюсах скорее всего тип ptrdiff_t надо заменить на ptr_diff. Я просто дома на BC3.1 все проверяю.

Что здесь хорошего? Мы получили класс объектов-указателей, которые можно смело применять вместо настоящих. Деструктор ~CPthat уничтожает указуемый объект, поскольку сам по себе последний не имеет имени, и без своего указателя утрачивает идентичность. Проще говоря, останется в нашей памяти навечно, как герой. Ну можно конечно вызывать деструктор и явно, а что? Вот так:

pthat-›~Cthat;

Тогда удаление уберите из деструктора указателя.

Напоследок сделаем очевидный шаг - сделаем умный указатель параметризированным классом.

template ‹class T›

class SmartPointer {

private:

 T* tObj;

public:

 SmartPointer(T* _t=NULL):tObj(_t);

 ~SmartPointer{ if (tObj) delete tObj; }

 operator T*{ return tObj; }

 T* operator-›{ return tObj; }

};

Для интереса посмотрите, как сделан auto_ptr в STL.

Передохнем. Кофе. Джоггинг. Пиво. Сигарета. Нужное подчеркнуть, выпить, покурить.

<p>Шаг 3 - Как это применять.</p>

Берем код параметризированного класса.

template ‹class T›

class SmartPointer {

private:

 T* tObj;

public:

 SmartPointer(T* _t=NULL): tObj(_t);

 ~SmartPointer {if (tObj) delete tObj;}

 operator T*{return tObj;}

 T* operator-›{return tObj;}

};

<p>1. Обработка обращения к NULL.</p>

Заменяем реализацию оператора -› на:

T* operator-› {

 if (!tObj) {

  cerr ‹‹ "NULL";

  tObj = new T;

 }

 return tObj;

}

или

T* operator-› {

 if (!tObj) throw CError;

 return tObj;

};

Здесь CError класс исключения. Или втыкаем статический экземпляр-шпион.

private:

 T* tObj; // Это было;

 static T* spy; // Это добавлено

Ну и сам перегруженный оператор.

T* operator-› {

 if (!tObj) return spy;

 return tObj;

};

Здесь нужно пояснить: spy совсем не обязательно класса T. Можно воткнуть производный, и переопределить его функции. Тогда он будет Вам докладывать о попытках обращения к NULL. Не забудьте его создать, инициализировать, и прицепить к указателю. А то вся идея на помойку. Вы пытаетесь отловить обращение к NULL, а там… NULL!!! "Матрицу" видели?

<p>2. Отладка и трассировка.</p>

Ну это совсем банально. Выносим определение операторов за определение класса и ставим там точку останова. Чтобы не тормозило в релиз версии, окружаем слово inline ифдефами.

template ‹class T›

#ifndef DEBUG

inline

#endif

SmartPointer‹T›::operator T* {

 return tObj;

}

template ‹class T›

#ifndef DEBUG

inline

#endif

T* SmartPointer‹T›::operator T-› {

 return tObj;

}

<p>3. Статистика классов и объектов.</p>

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

<p>4. Кэширование.</p>

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

Так. Пока тормозим. Интересно, о чем я напишу следующий шаг?

<p>Шаг 4 - О двойной диспетчеризации.</p>

Предположим, у нас есть массив, в котором мы храним карту местности. Разумеется, что элементы массива разнообразные - дома, колодцы, казино… ничего общего. Кроме суперкласса - предка естественно.

CBuilding

¦

______¦_______

¦ ¦ ¦

CHouse CWell CCasino

А карту эту мы отражаем разными способами. И даже не то, что разными способами, а имеем для такой благой цели несколько видов карт. Ну я не знаю, не картограф. Черви и пики. Нет, ладно. Радиоактивность и карма.

CMap

|

____________

| |

CRadioMap CCarmaMap

И что получается? Кто будет себя отрисовывать? И кто кого? Для каждой комбинации наследников CBuilding и CMap свой уникальный алгоритм. Что делать то будем? Какие феерические решения приходят… нет… не вам! Вашему коллеге или начальнику или подчиненному в голову? Да они ни сном ни духом о двойной диспетчеризации! Они скорее всего предложат получить информацию о типе во время исполнения, и запузырить в Ваш прекрасный проект кривоногий switch {}. Да еще и положить в каждый класс статический член с информацией о типе… Одно звучание предыдущей фразы наводит на подозрения. Но что делаем мы? вот что:

class CBuilding: {

public:

 virtual void doDraw(CMap* map)=0;

}

class CHouse: public CBuilding {

public:

 virtual void doDraw (CMap* map) {

 // ВОТ ОНА САМАЯ КОРКА!

  map-›doDraw(*this);

 }

};

// Эти такие же.

class CWell: public CBuilding {

public:

 virtual void doDraw (CMap* map) {map-›doDraw(*this);}

};

class CCasino: public CBuilding {

public:

 virtual void doDraw (CMap* map) {map-›doDraw(*this);}

};

// Это абстрактный класс для карт.

class CMap {

public:

 virtual void doDraw (CHouse& cb)=0;

 virtual void doDraw (CWell& cb)=0;

 virtual void doDraw (CCasino& cb)=0;

};

Это конечно не все. Теперь нужно наследовать CRadioMap и CcarmaMap от общего предка CMap и в каждом классе рисовать реализацию алгоритма. За отрисовку отвечает карта, но какая масть - решает виртуальная CBuilding::doDraw, а какое строение - выбирается перегруженная CMap::doDraw.

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

Похожие книги