на домашнюю страницу автора

к списку статей

Опубликовано в ныне покойном журнале
«Программист», N10, 2001г.

Поучительный CString

1. Что может быть интересного в этом CString?

Наверняка каждый из вас знаком с классом CString из библиотеки MFC, либо с его аналогами, которые входят в любую современную библиотеку. Действительно, работа со строковыми данными в стиле "чистого C" – занятие утомительное, а строковые классы делают её такой же простой, как с элементарными типами вроде int или float. Научиться использовать CString очень просто; ради этого, конечно же, не стоило писать специальную статью.

Почему же я, тем не менее, выбрал CString темой своего рассказа? Как любой хорошо спроектированный класс, его можно использовать, не задумываясь о внутреннем устройстве. Но оказывается, если всё-таки заглянуть внутрь, можно найти немало интересного и поучительного. Несмотря на кажущуюся простоту, CString использует несколько хотя и простых, но весьма интересных приёмов, зная которые, можно не только заставить его самого работать максимально эффективно, но и использовать эти принципы при построении своих собственных классов. Именно об этих приемах я и хочу рассказать.

Я ни в коем случае не призываю вас использовать в своей работе именно CString, а не какой-либо другой аналогичный класс. Просто CString оказался очень поучительным примером, который в своё время удивил меня самого!

Кстати, поскольку я собираюсь сосредоточиться именно на идеологии построения классов, я решил по возможности избегать примеров и фрагментов кода. Если вас заинтересуют детали реализации, вы легко найдете их в исходниках MFC. Там же можно найти немало примеров использования CString.

2. Принцип скрытого указателя

Первое, что поразило меня в классе CString – его размер, который составляет ровно 4 байта! Иными словами, CString – обычное 32-битное слово. Благодаря этому (и другим хитростям) его легко передавать в качестве параметра функции и возвращать в качестве её результата. Когда в функцию или из неё передаётся экземпляр некого класса, рука программиста сама собой пытается написать ссылку или указатель, дабы избежать копирования большой структуры данных. В случае с CString это излишне, вы скорее проиграете из-за увеличения косвенности ссылок.

Хитрость здесь заключается в том, что CString – уже указатель, только скрытый. Вот как определён класс CString:

class CString

{

public:

// описание методов класса

protected:

LPTSTR m_pchData; // указатель на буфер данных

// описание внутренних методов ...

};

Кроме m_pchData в классе CString нет других членов данных. Все необходимые для работы данные хранятся в блоке памяти, на который указывает m_pchData. В этом и заключается секрет маленького размера самого CString.

Почему я назвал CString скрытым указателем? Дело в том, что почти вся работа с указателем полностью спрятана внутри класса. CString содержит множество методов для обработки строк: слияние, выделение подстроки, поиск… При этом CString выступает в качестве самостоятельного и самодостаточного объекта. При виде только списка этих методов у вас даже подозрения не возникнет, что CString – указатель.

Чаще всего, когда говорят об "умных указателях" (smart pointers), имеют в виду противоположный тип указателей, назовём его "открытым". Основная логическая функция такого указателя – именно предоставление доступа к некоему другому объекту. "Ум" же его заключается в том, что помимо основных своих указующих обязанностей, он наделён какими-либо дополнительными функциями. Например: подсчетом ссылок, сборкой мусора, копированием при записи, о которых речь пойдёт ниже. Скрытый указатель реально делает то же самое, но на логическом уровне он переставляется как самостоятельный объект, его задача – наоборот спрятать от пользователя указываемый объект. Именно в представлении на уровне интерфейса класса и состоит их различие.

Строго говоря, CString не является скрытым указателем в чистом виде; несколько методов для работы с буфером напрямую все же имеется, о них мы ещё поговорим в п. 9. Однако, эти методы служат лишь дополнением к основной функциональности класса. Они обеспечивают совместимость, упрощают работу, когда необходимо "классическое сишное" представление строки.

Рассмотрим, какие же преимущества даёт скрытый указатель.

3. Передача CString как параметра и результата функции

"Умные" указатели, в частности, полезны, когда класс, содержащий большой объём данных, надо часто передавать через параметры функций и возвращать в качестве результатов. Именно так обычно и используют CString.

Рассмотрим абстрактный пример: пусть у нас есть некий большой по объёму данных класс, и в нём должна быть функция, создающая новый экземпляр класса и возвращающая его в качестве результата, например оператор сложения:

// вариант 0

class CBigData {

public:

// ...

CBigData operator + ( const CBigData& _summand );

private:

// и здесь куча данных

};

CBigData CBigData::operator + ( const CBigData& _summand )

{

CBigData result;

// здесь проводим необходимые вычисления

return result;

}

Параметр _summand мы, не задумываясь, передаём по ссылке. А вот результат передаётся по значению, что плохо, поскольку требует копирования всего класса. Что делать? Попытаться передать результат по ссылке?

// ВНИМАНИЕ!!! ТАК НЕПРАВИЛЬНО!!!

CBigData& CBigData::operator + ( const CBigData& _summand )

//      ^ вставили ссылку

{

CBigData result;

// необходимые вычисления

return result;

}

В эту ловушку попадают все новички; такой код просто не будет работать! Переменная result создана в стеке, значит при выходе из функции она будет уничтожена. Получается – мы передаём ссылку не на результат, а на то место где он был, но где его, увы, уже нет. Как же выйти из положения? Очевидно, что раз мы не можем создать результат в стеке – надо поместить его в кучу, в специально выделенный из неё блок памяти:

// вариант 1

CBigData* CBigData::operator + ( const CBigData& _summand )

{

CBigData* pResult = new CBigData;

// необходимые вычисления

return pResult;

}

Однако теперь вызывающий код должен позаботиться о том, чтобы самому освободить результат (именно поэтому мы заменили ссылку на указатель):

CBigData A;

CBigData B;

CBigData* pS = A + B;

// делаем всё что нужно с pS ...

delete pS;

Такой код уже будет работать. Но, согласитесь, он производит неестественное впечатление. Во-первых, наш метод потерял логическую целостность, поскольку в нём выделяется память, об освобождении которой должен заботиться программист, использующий этот метод. Во-вторых, если для другой функции возвращаемый указатель и может выглядеть логично, то для оператора сложения это явно не согласуется со здравым смыслом: складывали объекты, а получили указатель, который вдобавок ещё надо не забыть освободить.

Освобождение результата можно "автоматизировать", если возвращать не простой, а "умный" указатель, в который встроена функция управления памятью и "сборки мусора". Как раз такой механизм реализован в CString. (о нём мы ещё поговорим подробно). Как мы уже выяснили, этот класс является скрытым указателем. Благодаря этому, как правило, нет никакого смысла размещать CString в куче. Обычно он выступает в роли локальной переменной, члена другого класса, аргумента или результата функции. Во всех этих случаях компилятор сам позаботится о вызове деструктора, когда данный экземпляр класса больше не нужен. А в деструкторе сработает сборщик мусора, и выделенная память будет своевременно освобождена. И для этого не придётся написать ни одной лишней строчки! Да и сам код выглядит красиво и естественно:

CString A = "Строчка А";

CString B = "Строчка В";

CString S = A + B;

// и никаких delete !

В принципе, стоит упомянуть ещё один вариант: возвращать данные, не как результат функции, а как выходной параметр:

// вариант 2

class CBigData {

public:

static void Sum( CBigData* _pResult, const CBigData* _pSummand1, const CBigData* _pSummand1 );

};

То есть в качестве одного из параметров мы передаём методу адрес, куда он должен записать результат. Правда, нам пришлось пожертвовать красивой записью "A + B":

CBigData A;

CBigData B;

CBigData S;

CBigData::Sum( &S, &A, &B );

Достоинство этого варианта заключается в том, что память под результат выделяет вызывающая программа, а значит, она вольна размещать его где угодно: на стеке, в другом классе, и т.п., а не только в куче, в отдельном блоке памяти, как в случае с указателем, что обычным, что хитрым. Поэтому в том случае, когда возвращаемые данные имеют фиксированный, заранее известный размер, этот вариант будет самым оптимальным. Однако если этот размер заранее не известен (как в случае со строкой) – всё сразу усложняется. Очевидно, что размер результата удобнее всего вычислять там же, где и сам результат; там же и выделять буфер под него. Получается, мы вернулись к варианту с указателем.

Таким образом, использование "умных" указателей, в которые встроен механизм управления памятью, является наиболее универсальным решением в случаях, когда функция или метод возвращает в качестве результата большой объём данных.

4. Единый блок памяти

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

class CStringUnsophisticated {

public:

// описания методов

private: // данные

unsigned int m_nStringLength; // длина строки

unsigned int m_nBufferSize; // размер блока памяти

// здесь могут быть и другие дополнительные данные

char* m_pString; // указатель на собственно строку символов

};

Это – самый простой способ реализовать класс, содержащий данные переменного размера. В самом классе мы размещаем данные, размер которых определён заранее, а под переменные данные выделяем отдельный буфер. То есть такой класс будет занимать два блока памяти.

CString ограничивается всего лишь одним блоком. В его начало помещается заголовок – структура фиксированного размера, где хранится длина строки и другие дополнительные параметры (эта структура называется CStringData); сразу же после неё следует собственно буфер строки. Таким образом, мы выигрываем как минимум в двух местах. Во-первых, размещая все данные в одном блоке, мы экономим на вызовах функций работы с кучей, и уменьшая фрагментацию памяти. Во-вторых, мы избавляемся от многоуровневых указателей на указатели, поскольку CString уже указатель и передавать указатель на него, как правило, смысла нет (в отличие от CStringUnsophisticated).

Таким образом, хотя CString и вынужден всегда хранить свои данные в куче, он делает это максимально экономно.

5. Совместное использование буфера данных

Ещё большей экономии памяти позволяет достичь следующая хитрость: несколько объектов CString могут совместно использовать один и тот же буфер данных. Таким образом, если вы, скажем, передаёте строку в качестве параметра функции, реально передаётся только указатель, сама строка при этом не копируется – экземпляры CString и в функции, и в вызывающем коде ссылаются на один буфер. Это делает CString ещё более похожим на "обычный" указатель char*.

Такое поведение указателя может показаться единственно возможным, но это конечно не так. В некоторых случаях полезными оказываются умные указатели, устроенные по полностью противоположному принципу: каждому указателю однозначно соответствует собственный буфер данных. Если мы копируем указатель, должны быть скопированы и данные, на которые он указывает. Такие указатели называют ведущими (master pointer). Вообще "умных указателей" можно наизобретать неисчислимое множество и их обсуждение увело бы нас далеко за рамки выбранной темы. Потому вернёмся к нашим CString.

6. Принцип "copy on write"

Итак, несколько экземпляров CString, могут совместно использовать один общий буфер данных на всех. Что произойдёт, если мы попытаемся изменить один из этих CString? Если буфер останется тем же самым, изменятся и все остальные объекты CString. Если бы это было так – неизбежно возникла бы путаница! Поэтому CString использует принцип "copy on write" – копирование при записи. Если строку надо изменить, но её буфер использует ещё хотя бы один объект CString – создаётся новый буфер, в который и помещается результат. Проще всего скопировать старый буфер в новый, и работать с копией – отсюда и "copy on write". Экземпляр CString, с которым производилась операция, теперь будет ссылаться на новый буфер, а все остальные – по-прежнему на старый, не измененный, потому операция останется для них незамеченной.

7. Автоматический счётчик ссылок

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

Счётчик ссылок в CString является частью структуры CStringData. Принцип его действия прост. Всякий раз, когда возникает новый экземпляр CString, ссылающийся на данный буфер, счётчик увеличивается на единицу. Когда он удаляется или просто перестаёт использовать данный буфер, счётчик, наоборот уменьшается на единицу. Таким образом, мы всегда будем знать, сколько экземпляров CString ссылаются на данный буфер.

Особо отмечу, что вся работа со счётчиком ссылок инкапсулирована внутри класса CString. Она происходит в его конструкторах, деструкторах, операторах присваивания, и во всех методах, где происходит замена буфера. Пользователю не нужно самому заботиться о счётчике ссылок.

Я подчёркиваю это потому, что счётчик ссылок – всегда потенциальный источник трудноуловимых ошибок. При разработке нужно очень тщательно следить, чтобы не забыть в нужный момент увеличить или уменьшить его, или наоборот не сделать это лишний раз. Чаще всего, то, что значение счётчика ссылок неверно, выясняется в ходе работы программы слишком поздно, и найти место, где было пропущено (или сделано лишнее) уменьшение либо увеличение счётчика очень сложно. Именно поэтому, если вы решили где-то использовать счётчик ссылок – надо позаботиться об автоматизации работы с ним, чтобы вся работа была локализована где-то в одном месте, иначе не избежать поиска багов по всей программе! Пожалуй, самый разумный способ сделать это – инкапсулировать работу со счётчиком внутри умного указателя.

8. Определение времени жизни объекта.

Раз уж в классе завёлся счётчик ссылок – грех не использовать его заодно и для "сборки мусора". Собственно это и есть наиболее распространённое применение счётчика ссылок. Идея тривиальна: если счётчик ссылок обнулился – значит, буфер данных больше не используется ни одним экземпляром CString и его можно смело удалить.

Задача определения времени жизни объекта может стать очень нетривиальной в сложной системе, содержащей много объектов, различным образом связанных друг с другом. Счётчик ссылок даёт очень простое и элегантное решение этой задачи. Вот почему он приобрёл такую популярность. Например, в COM счётчиком ссылок обязан быть снабжён каждый интерфейс. Надо помнить только две вещи. Во-первых, избегайте зацикленных ссылок, когда несколько объектов, которые уже никому не нужны, тем не менее, удерживают друг друга в памяти. Во-вторых, повторю: работу со счётчиком лучше инкапсулировать внутри умного указателя, иначе он превратится в постоянных источник трудноуловимых ошибок!

Принцип автоматической сборки мусора стал стандартом в современном программировании, он жёстко встроен во все современные RAD системы: Visual Basic, C#, Delphi… Это, несомненно, оправдано для подобного рода систем. В С++ нет подобного встроенного механизма, однако, как вы видите, в нём нет ничего сверхъестественного. Используя счётчик ссылок, его легко реализовать в своих классах. А отсутствие в C++ жёстко встроенного механизма подсчёта ссылок и сборки мусора является несомненным достоинством языка, поскольку не ограничивает свободу разработчика. Сборка мусора требует, чтобы объекты размещались исключительно в куче, более того, каждый такой объект обязан находиться в собственном блоке памяти. Если мы не завязываем класс на сборщик мусора – его экземпляры можно разместить где угодно: в куче, стеке, внутри другого класса и т.п. Это позволяет реализовывать программы наиболее эффективно.

Сборщик мусора, в принципе, можно реализовать и без счётчика ссылок. Но лично мне неизвестен алгоритм, который был бы так же прост, нагляден и эффективен, как основанный на счётчике ссылок. Что же касается самого счётчика – для него найдутся и другие применения, например описанный принцип копирования при записи. Вообще эти три принципа: счётчик ссылок, сборка мусора и копирование при записи очень хорошо сочетаются, хотя их можно использовать и по отдельности.

9. Использование автоматического приведения типов в C++

Как я уже упоминал в п. 2, несмотря на то, что CString в своей основе – скрытый указатель, в нём есть несколько методов, позволяющих работать с указателем "открыто". Они используются в ситуациях, когда требуется обычный "сишный" указатель на строку оканчивающуюся нулём. Для этого CString активно использует возможности C++ по автоматическому приведению типов. Три метода класса CString: конструктор, операторы присваивания и преобразования типа, позволяют добиться того, что типы CString и LPCTSTR становятся как бы взаимозаменяемыми. Вот эти методы:

class CString {

public:

// ...

CString( LPCTSTR lpsz );

const CString& operator = ( LPCTSTR lpsz );

operator LPCTSTR() const;

// ...

};

Благодаря последнему, например, мы можем смело подставить CString в качестве параметра функции, который на самом деле должен иметь тип const char*, C++ автоматически выполнит преобразование:

CString sTitle( "Заголовок" );

// Вызываем функцию Win32 API, которая естественно ничего не знает о CString

::SetWindowText( hWindow, sTitle ); // здесь будет вызван operator LPCTSTR

Или наоборот, конструктор позволяет использовать обычную строчку там, где должен быть CString:

void SomeFunction( CString _someString );

// ...

SomeFunction( "Некая строчка" ); // здесь будет вызван конструктор CString(LPCTSTR lpsz)

В обоих примерах используется тот факт, что если тип параметра, который вы пытаетесь передать функции, не соответствует типу формального аргумента, C++ пытается сделать преобразование за вас. Для этого создаётся временный объект, который передаётся в качестве параметра, и удаляется сразу же после возврата из функции.

Такое неявное преобразование типов в C++ – очень полезный инструмент. Он удобен хотя бы тем, что как в случае с CString, часто позволяет значительно упростить исходный текст программы, скрывая второстепенные рутинные детали. А значит другому программисту (или даже вам самим через некоторое время) будет гораздо легче разобраться в коде вашей программы. Но здесь же заключена и обратная сторона: часто за простой конструкцией в C++ оказывается скрыта большая работа. За примером далеко ходить не надо: конструктор CString(LPCTSTR lpsz) всегда выполняет копирование. И если забыть об этом, вы можете вдруг обнаружить, что ваша программа тратит слишком много времени в, казалось бы, простом месте. Но что действительно опасно, излишнее увлечение такими фокусами приведёт к тому, что ваша программа превратится в головоломку, разобраться в которой станет гораздо сложнее. Поэтому использовать такие "скрытые" преобразования стоит лишь там, где они выглядят естественно и логично.

Кстати, с преобразованием типов связана ещё одна маленькая хитрость CString! Как мы помним, блок данных, на который ссылается скрытый указатель CString, состоит их структуры CStringData и собственно строки символов. Подробно эту конструкцию мы обсудили в п. 4. Но одну маленькую деталь я специально приберёг до этого места. Может показаться естественным, что указатель, который хранит CString, должен указывать на начало этого блока памяти. Но в CString сделано хитрее, он указывает на первый символ строки! Добраться до CStringData нетрудно – ведь она находится прямо перед этим местом и имеет фиксированный размер, зато доступ к самой строке максимально упрощается. Оператор LPCTSTR при этом фактически ничего не делает, CString хранит уже готовый указатель. Таким образом, мы получаем пусть и небольшой, но выигрыш в производительности, который может стать очень даже заметным при интенсивной работе со строками. Подобные простые приёмы оптимизации почти не требуют усилий от программиста, и при этом нередко дают эффект не меньший, чем трудоёмкое кодирование на ассемблере. Пренебрегать ими было бы недальновидно.

10. Метод монопольного захвата буфера

А теперь продолжим разговор о приведении типов. Вы обратили внимание, что CString определяет оператор LPCTSTR, но не LPTSTR? Напомню, что LPCTSTR означает const char*, а LPTSTR – просто char*, без const . То есть методом скрытого преобразования мы можем получить только указатель на константную строку, но не указатель на строку, которую можно изменить. Всё дело в том, что один и тот же буфер может использоваться одновременно несколькими объектами CString. Поэтому, operator char* не может быть таким же тривиальным, как и const char*. Прежде чем менять строку, мы должны сначала позаботиться о выполнении принципа копирования при записи.

Именно для этого в CString предусмотрен метод GetBuffer (и его вариант GetBufferSetLength), специально предназначенный для того, чтобы обрабатывать хранящуюся в буфере CString строку напрямую. Если при вызове этого метода оказывается, что данный буфер используется также кем-то ещё, создаётся его копия. В этом случае GetBuffer возвращает указатель именно на копию. После того, как вы модифицировали строку, следует вызвать ReleaseBuffer, который также позволяет задать новую длину строки. Наверно, было бы логично, чтобы GetBuffer устанавливал также специальный флаг, указывающий, что данный буфер предназначен для прямой работы и не может быть совместно использован другими экземплярами CString, тогда ReleaseBuffer должен был бы сбрасывать это флаг. Но в текущей реализации подобный флаг отсутствует, поэтому вызов ReleaseBuffer не является обязательным. Впрочем, я рекомендую вам всегда использовать его. Как минимум, это позволяет чётко выделить участок кода, где производится модификация строки напрямую.

Применение методов GetBuffer/ReleaseBuffer на практике оказалось очень удобным – далеко не все действия со строками, которые вам могут понадобиться, можно легко свести к набору стандартных операций. Кроме того, большинство функций Win32 API устроено так, что если они должны возвращать строку, они копируют её в указанный вами буфер. Выделите его с помощью GetBuffer, и вы получите результат в виде удобного для дальнейшей работы CString!

Но вернёмся к приведению типов. Почему бы метод GetBuffer не оформить как operator LPTSTR, и таким образом спрятать его так же, как это сделано с LPCTSTR? Начнём с того, что метод GetBuffer удобен тем, что позволяет дополнительно указать минимально необходимый размер буфера. Но есть и более глубокая причина. Пожалуй, это как раз тот случай, когда надо сказать: всё хорошо, что хорошо в меру. Как уже отмечалось, негативная сторона "продвинутых" возможностей C++, заключается в том, что за вроде бы простым выражением может скрываться очень большая работа. В данном случае ситуация ещё хуже: появляются два внешне очень похожих оператора: но один из них фактически ничего не делает, а другой наоборот делает большую работу, связанную с соблюдением принципа копирования при записи. Использование обычного метода вместо переопределённого оператора позволяет оставить эту деятельность на виду, и не дать программисту перепутать эти операторы. Кроме того, пара GetBuffer/ReleaseBuffer позволит чётко ограничить тот кусок кода, где происходит работа со строкой напрямую.

Кстати, хочу обратить внимание на то, как важно использовать атрибут const. Приведённый пример показывает, насколько могут различаться реализации двух методов, отличающихся только этим атрибутом. К сожалению, как показывает практика, встречаются программисты, даже не новички, которые полностью его игнорируют. Особенно много неприятностей это доставляет, когда написанный таким программистом код или библиотеку приходится совмещать с полноценным кодом, где аккуратно используется const. Поэтому призываю: используйте const!!! Этот атрибут позволяет сделать контроль типов более точным и тонким, а логику вашей программы более строгой и изящной.

11. Использование CString, плюсы и минусы строковых классов

Класс CString оказался удивительно удобным в практической работе. Я активно использую его во всех своих проектах и теперь мне трудно даже представить, что бы я делал без него. CString в большинстве случаев заметно упрощает исходный текст (хотя, к сожалению, это не касается конечного бинарного кода). С одной стороны, это облегчает его написание, а с другой его понимание, если кому-то придётся разбираться в вашей программе. В итоге всё это ускоряет создание программы; и не просто благодаря упрощению исходников, но также и сокращению числа ошибок, что, на мой взгляд, и есть самое ценное!

Пожалуй, единственный недостаток CString, как впрочем, и любого другого подобного класса – некоторый проигрыш в производительности по сравнению с традиционным "сишным" стилем работы. Связано это в первую очередь с тем, что CString всегда хранит данные в куче и почти каждая операция со строками связана с выделением и освобождением памяти. Стиль C чаще всего подразумевает хранение строк в стеке, работа с которым, конечно, быстрее. Некоторая потеря производительности всегда была платой за универсальность.

Впрочем, на практике выигрыш в производительности за счёт отказа от использования CString, как правило, настолько мал, что во много раз перекрывается возникающими недостатками: усложнением кода, а следовательно – увеличением вероятности появления ошибок. Еще один минус стиля "чистого C" – использование для хранения строк буферов фиксированного размера, который в лучшем случае выливается в неудобства пользователей, а в худшем – в ошибки, связанные с переполнением буфера (которое так обожают хакеры). Так что писать код "в стиле C" имеет смысл только в специфических случаях, когда производительность имеет критическое значение.

Всё сказанное относится не только к CString, но и к любому классу строки. Я снова повторю, что ни в коем случае не призываю вас бросить свой любимый строковый класс и использовать именно CString. Очевидно, что устройство различных строковых классов не может сильно отличаться. Например, мне известно, что класс std::string из библиотеки STL устроен практически также, тот же принцип совместного использования буфера, счётчик ссылок… Хотя, лично мне показалось, по сравнению с ним CString чуточку более удобен. В общем, выбор строкового класса завит от того, в какой среде вы программируете и какие библиотеки используете.

12. Заключение

Итак, CString может служить образцом проектирования класса. Он сделан так, что с одной стороны, использовать его в самых различных ситуациях очень просто и удобно. С другой стороны – в нём реализовано несколько простых и вместе с тем нетривиальных приёмов, делающих работу класса максимально эффективной. Все описанные приёмы очень просты, их легко реализовать в своих проектах. В моей практике использование описанных идей в собственных классах оказалось очень плодотворным.

Особенно я бы выделил принцип "умного" указателя. Для меня это была одной из первых встреч с нетривиальным использованием указателя, когда он является не просто адресом ячейки памяти, а достаточно сложным объектом, наделённым расширенной функциональностью. Это может быть не только подсчёт ссылок; например, в последнем проекте я использовал указатели, которые участвовали в синхронизации потоков. Язык C++ предоставляет блестящие возможности для конструирования самых различных "хитрых" указателей. Недавно я встретил замечательную книгу на эту тему: Джеф Элджер, "C++ библиотека программиста". Рекомендую эту книгу всем, кому интересны как "продвинутые" указатели, так и максимально полное использование возможностей C++ вообще.

© 2001 Алексей Курзенков

Внимание!!! Если вы хотите перепечатать эту статью или её часть на на своём сайте, в печтном издании, где либо ещё, большая просьба, согласуйте пожалуйста это с автором! Связаться со мной можно по адресу: