Основы C Моим детям. Никогда не смейтесь, помогая мне осваивать
Скачать 1.68 Mb.
|
unique_ptr<double> dp{new double}; *dp= 7; } Основным отличием от обычных указателей является то, что память автома- тически освобождается при уничтожении указателя. Таким образом, ему нельзя присваивать адреса, которые не выделены динамически: double d; unique_ptr Деструктор указателя dd попытается удалить d. Уникальные указатели нельзя присваивать другим типам указателей или неяв- но преобразовывать в другие типы. Для получения хранящегося в интеллектуаль- ном указателе обычного указателя можно использовать функцию-член get: double * raw_dp = dp.get(); C++11 C++11 02_ch01.indd 84 14.07.2016 10:46:43 1.8. Массивы, указатели и ссылки 85 Его нельзя даже присваивать другому уникальному указателю: unique_ptr Его можно только перемещать: unique_ptr Семантику перемещения мы рассмотрим в разделе 2.3.5. Пока же просто ска- жем, что в то время как копирование дублирует данные, перемещение передает данные от источника к целевому объекту. В нашем примере владение памятью, на которую указывает интеллектуальный указатель, сначала передается от dp к dp2, а затем к dp3. После этих передач dp и dp2 имеют значения nullptr, а выде- ленная память будет освобождена деструктором dp3. Таким же образом владение памятью передается и когда unique_ptr возвращается из функции. В следующем примере dp3 получает во владение память, выделенную в f(): std::unique_ptr { return std::unique_ptr } int main () { unique_ptr } В этом случае move() не требуется, поскольку результат функции является временным значением, которое будет перемещено (детальнее, опять же, — в раз- деле 2.3.5). Уникальный указатель имеет специальную реализацию 16 для массивов. Это не- обходимо для правильного освобождения памяти (с помощью delete[]). Кроме того, специализация обеспечивает доступ к элементам массива: unique_ptr<double[]> da{new double[3]}; for(unsigned i = 0; i < 3; ++i) da[i] = i+2; В то же время operator* для массивов недоступен. Важным преимуществом unique_ptr является то, что он не имеет абсолют- но никаких накладных расходов по сравнению с обычными указателями — ни в смысле производительности, ни в смысле расходуемой памяти. Дополнительные материалы. Важной возможностью уникальных указателей является возможность предоставления собственной функции освобождения памя- ти (удалителя); подробности приведены в [26, §5.2.5f], [43, §34.3.1] и в онлайн-ру- ководствах (например, cppreference.com). 16 Специализации будут рассмотрены в разделах 3.6.1 и 3.6.3. 02_ch01.indd 85 14.07.2016 10:46:44 Основы C++ 86 1.8.3.2. shared_ptr Как становится понятно из названия, интеллектуальный указатель shared_ptr управляет памятью, которая совместно используется несколькими объектами (и каждый хранит указатель на нее). Такая память автоматически освобождается, как только не остается ни одного shared_ptr, указывающего на нее. Это может значительно упростить программу, особенно при использовании сложных струк- тур данных. Чрезвычайно важной областью применения этих интеллектуальных указателей является параллелизм: память освобождается автоматически, когда все потоки завершают свой доступ к ней. В отличие от unique_ptr, shared_ptr может быть скопирован сколько угод- но раз, например shared_ptr { shared_ptr<double> p1{new double}; shared_ptr<double> p2{new double}, p3 = p2; cout << "p3.use_count () = " << p3.use_count() << endl; return p3; } int main () { shared_ptr<double> p = f(); cout << "p.use_count() = " << p.use_count() << endl; } В этом примере мы выделяем память для двух значений типа double — для p1 и p2. Указатель p2 копируется в p3, так что оба они указывают на одну и ту же память, как показано на рис. 1.1. counter ... Диспетчер Данные p1 p3 Рис. 1.1. shared_ptr в памяти Мы можем увидеть это, выводя значение функции use_count, которая дает значение счетчика counter (рис. 1.1): p3.use_count () = 2 p.use_count () = 1 C++11 02_ch01.indd 86 14.07.2016 10:46:45 1.8. Массивы, указатели и ссылки 87 При завершении функции f() указатели уничтожаются, и память, на которую указывает p1, освобождается (она так и не была использована). Второй выделен- ный блок памяти продолжает существовать, поскольку p из функции main про- должает на нее указывать. counter ... Диспетчер Данные p1 p3 Рис. 1.2. shared_ptr в памяти после вызова make_shared По возможности интеллектуальный указатель shared_ptr следует создавать с помощью вызова make_shared: shared_ptr Тогда данные диспетчера и данные, на которые указывает интеллектуальный указатель, сохраняются в памяти вместе, как показано на рис. 1.2, так что кеши- рование памяти работает эффективнее. Поскольку make_shared возвращает shared_ptr, для простоты можно использовать автоматический вывод типа (раз- дел 3.4.1): auto p1 = make_shared<double>(); Мы должны признать, что интеллектуальный указатель shared_ptr имеет некоторые накладные расходы памяти и производительности. С другой стороны, упрощение наших программ благодаря применению shared_ptr в большинстве случаев стоит некоторых небольших накладных расходов. Дополнительные материалы. Об удалителях и прочих деталях реализации shared_ptr читайте в [26, §5.2], [43, §34.3.2] и в онлайн-руководствах (например, cppreference.com). 1.8.3.3. weak_ptr Применение shared_ptr не лишено проблем — так, наличие циклических ссы- лок препятствует освобождению памяти. “Разбить” такие циклы и решить тем са- мым проблему может интеллектуальный указатель weak_ptr. Он не претендует ни на право владения памятью, ни даже на совместное использование. Пока что мы просто упоминаем о них — для полноты изложения, но если вам потребуются эти интеллектуальные указатели, информацию и них вы найдете в [26, §5.2.2], [43, §34.3.3] и на сайте cppreference.com. C++11 02_ch01.indd 87 14.07.2016 10:46:46 Основы C++ 88 Для динамического управления памятью указателям нет альтернативы. Если требуется только ссылаться на другие объекты, можно воспользоваться другой возможностью языка, именуемой ссылками (сюрприз, сюрприз!), о которой мы и поговорим в следующем разделе. 1.8.4. Ссылки Следующий код объявляет ссылку: int i = 5; int& j = i; j = 4; std::cout << "i = " << i << '\n'; Переменная j ссылается на i. Изменение j приведет к изменению i (и наобо- рот), как показано в примере. i и j всегда будут иметь одно и то же значение. Ссылку можно рассматривать как псевдоним, который вводит новое имя для су- ществующего объекта или подобъекта. Всякий раз, когда мы определяем ссылку, мы тут же должны явно указать, на что она ссылается (в отличие от указателей, которые могут получить свое значение позже). После объявления ссылка не мо- жет измениться и начать указывать на другую переменную. Пока что не прозвучало ничего особо полезного. Но ссылки являются чрезвы- чайно полезными в качестве аргументов функций (раздел 1.5), для обращения к частям других объектов (например, к седьмому элементу вектора) и для построе- ния представлений (см., например, раздел 5.2.3). В качестве компромисса между указателями и ссылками новый стандарт предлагает класс reference_wrapper, который ведет себя аналогично ссылкам, но позволяет избежать некоторых из их ограничений. Напри- мер, он может использоваться внутри контейнеров (см. раздел 4.4.2). 1.8.5. Сравнение указателей и ссылок Основным преимуществом указателей перед ссылками является возможность динамического управления памятью и вычисления адресов. С другой стороны, ссылки могут ссылаться только на корректные местоположения в памяти 17 . Таким образом, они не грозят утечками памяти (если только вы не будете ими злоупот- реблять), а кроме того, используют такую же запись, как и объект, на который они ссылаются. К сожалению, практически невозможно создать контейнеры ссылок. Словом, ссылки не являются безопасной панацеей, но они, тем не менее, намно- го менее подвержены ошибкам, чем указатели. Указатели следует использовать только при работе с динамической памятью, например когда мы динамически со- здаем такие структуры данных, как списки или деревья. Даже тогда мы должны делать это с помощью тщательно протестированных типов или инкапсулировать 17 Ссылки могут также ссылаться на произвольные адреса, но добиться этого несколько сложнее. Для вашей же безопасности мы не покажем, как этого добиться (к тому времени, как вы сможете сделать это самостоятельно, вы будете понимать, что стоит делать, а что — нет). C++11 02_ch01.indd 88 14.07.2016 10:46:46 1.8. Массивы, указатели и ссылки 89 указатели в классах, насколько это возможно. Поскольку интеллектуальные ука- затели заботятся о выделении и освобождении памяти, им следует отдавать пред- почтение над обычными указателями даже внутри классов. Сравнение указателей и ссылок приводится в табл. 1.9. Таблица 1.9. Сравнение указателей и ссылок Свойство Указатели Ссылки Ссылка на определенное местоположение √ Обязательная инициализация √ Отсутствие утечек памяти √ Обозначения, применяемые для объектов √ Управление памятью √ Адресная арифметика √ Использование в контейнерах √ 1.8.6. Не ссылайтесь на устаревшие данные! Локальные переменные функций доступны только в области видимости функ- ции, например double& square_ref(double d) // НЕ ДЕЛАЙТЕ ТАК!!! { double s = d*d; return s; } Здесь результат нашей функции ссылается на локальную переменную s, кото- рой больше не существует. Память, где она хранилась до сих пор, никуда не ис- чезла, так что нам может повезти (ложное везение!) и она не будет перезаписана к тому моменту, когда мы ею воспользуемся. Но рассчитывать на это категорически нельзя. На самом деле такие скрытые ошибки даже хуже, чем очевидные, приво- дящие к аварийному завершению, — потому что они могут разрушить програм- му только при определенных условиях, и потому их очень трудно найти. Такие ссылки называются устаревшими ссылками. Хороший компилятор пре- дупредит вас, когда вы захотите получить ссылку на локальную переменную. К ве- личайшему сожалению, такие примеры приходилось видеть даже в учебниках по программированию в Интернете! То же самое справедливо и в отношении указателей: double* square_ptr(double d) // НЕ ДЕЛАЙТЕ ЭТОГО!!! { double s = d*d; return &s; } Этот указатель хранит локальный адрес, который вышел из области види- мости. Такой указатель называется висячим указателем. 02_ch01.indd 89 14.07.2016 10:46:47 Основы C++ 90 Возврат ссылок или указателей может быть корректен в случае функций-чле- нов, когда они указывают на члены-данные (см. раздел 2.6). Совет Возвращайте указатели и ссылки только на данные в динамически выделенной па- мяти, на данные, существовавшие до вызова функции, или на статические данные. 1.8.7. Контейнеры в качестве массивов В качестве альтернативы традиционным массивам C++ мы хотим представить вам два типа контейнеров, которые могут быть использованы аналогичным образом. 1.8.7.1. Стандартный вектор Массивы и указатели являются частью языка C++. В отличие от них std::vector принадлежит стандартной библиотеке и реализован в виде шаблона класса. Тем не менее он может использоваться практически так же, как и массивы. Например, в примере из раздела 1.8.1 создание двух массивов, v и w, выглядит для векторов следующим образом: #include { std::vector <float> v(3), w(3); v[0] = 1; v[1] = 2; v[2] = 3; w[0] = 7; w[1] = 8; w[2] = 9; } Размер вектора не обязан быть известен во время компиляции. Размеры векторов могут изменяться во время их жизни, как будет показано в разделе 4.1.3.1. Поэлементная инициализация вектора не очень-то компактна. Поэтому C++11 допускает инициализацию с помощью списков в фигурных скобках: std::vector В этом случае размер вектора следует из длины списка инициализации. Сложение векторов, рассматривавшееся ранее, также может быть реализовано более надежно: void vector_add(const vector<float>& v1, const vector<float>& v2, vector<float>& s) { assert(v1.size() == v2.size()); assert(v1.size() == s.size()); for(unsigned i = 0; i < v1.size(); ++i) s[i] = v1[i] + v2[i]; } C++11 02_ch01.indd 90 14.07.2016 10:46:47 1.8. Массивы, указатели и ссылки 91 В отличие от массивов и указателей C аргументы типа vector знают свои разме- ры, и теперь мы можем проверить, совпадают ли они. Примечание: размер мас- сива можно вывести с помощью шаблонов; этот вопрос мы оставим в качестве упражнения (см. раздел 3.11.9). Векторы могут копироваться и возвращаться из функций. Это позволяет нам использовать более естественную запись: vector<float> add(const vector { assert(v1.size() == v2.size()); vector } int main () { std::vector } Эта реализация потенциально дороже предыдущей, в которой целевой вектор передавался с помощью ссылки. Позже мы обсудим возможности оптимизации — осуществляемой как компилятором, так и пользователем. По нашему опыту более важно начинать работу с создания производительного интерфейса, а вопросами производительности озадачиваться несколько позже. Легче сделать правильную программу быстрой, чем сделать быструю программу правильной. Таким обра- зом, первоначальная цель заключается в хорошем дизайне программы. Почти во всех случаях хороший интерфейс может быть реализован с достаточной произво- дительностью. Контейнер std::vector не является вектором в математическом смысле. В нем нет арифметических операций. Тем не менее этот контейнер оказывается весьма полезным в научных приложениях для обработки нескалярных промежу- точных результатов. 1.8.7.2. valarray valarray представляет собой одномерный массив с поэлементными операци- ями; даже умножение выполняется поэлементно. Операции со скалярным значе- нием выполняются с каждым из элементов valarray. Таким образом, valarray чисел с плавающей точкой можно рассматривать как векторное пространство. В следующем примере демонстрируются некоторые операции: #include #include { 02_ch01.indd 91 14.07.2016 10:46:47 Основы C++ 92 std::valarray<float> v = {1, 2, 3}, w = {7, 8, 9}, s = v + 2.0f*w; v = sin(s); for(float x : v) std::cout << x << ' '; std::cout << '\n'; } Обратите внимание, что valarray Основная привлекательность valarray заключается в способности доступа к его срезам. Это позволяет эмулировать матрицы и тензоры высоких поряд- ков, включая соответствующие их операции. Тем не менее из-за отсутствия не- посредственной поддержки большинства операций линейной алгебры valarray используется в числовых приложениях не столь широко. Мы со своей стороны рекомендуем использовать для решения задач линейной алгебры авторитетные и проверенные библиотеки C++. Хочется верить, что в будущих стандартах язык программирования C++ будет включать такую библиотеку. 1.9. Структурирование программных проектов Большой проблемой крупных проектов являются конфликты имен. По этой причине мы рассмотрим, как данная проблема усугубляется макросами. С другой стороны, позже, в разделе 3.2.1, мы покажем, как пространства имен помогают нам бороться с конфликтами имен. Чтобы понять, как в программном проекте C++ взаимодействуют файлы, не- обходимо разобраться в процессе построения, т.е. в том, как выполнимый файл генерируется из исходных файлов. Это и будет предметом нашего первого под- раздела. В этом свете мы представим механизм макросов и другие возможности языка. Прежде всего мы хотим кратко обсудить возможность языка, которая способс- твует структурированию программы, — комментарии. 1.9.1. Комментарии Очевидно, что основная цель комментария — описание на понятном языке того, что в исходном тексте программы не является очевидным для всех, например // Трансмутация антибрахия за время O(n log n) while(trans(mutation) < end_of(anti_brachius)) { 02_ch01.indd 92 14.07.2016 10:46:48 1.9. Структурирование программных проектов 93 Часто комментарий представляет собой псевдокод, поясняющий запутанную ре- ализацию: // A = B * C for(...) { int x78zy97 = yo6954fq, y89haf = q6843, ... for(...) { y89haf += ab6899(fa69f) + omygosh(fdab); ... for(...) { A(dyoa929,oa9978 ) += ... В таком случае мы должны спросить себя, нельзя ли реструктуризировать наше программное обеспечение таким образом, чтобы такие непонятные реализа- ции осуществлялись разово, в каком-нибудь темном углу библиотеки, а везде мы писали бы ясные и простые инструкции, например A= B * C; как программный, а не псевдокод. Это и есть одна из главных целей нашей кни- ги — показать вам, как написать именно то выражение, которое вы хотите, в то время как его реализация “под капотом” выжимает из него максимальную произ- водительность. Еще одно частое использование комментариев — временно скрыть от компи- лятора код, который должен исчезнуть на время эксперимента с альтернативны- ми реализациями, например for(...) { // int x78zy97 = yo6954fq, y89haf = q6843, ... int x78zy98 = yo6953fq, y89haf = q6842, ... for(...) { Как и C, C++ предоставляет возможность блочных комментариев, окруженных /* и */. Они могут использоваться для превращения произвольной части кода или нескольких строк в комментарий. К сожалению, они не могут быть вложен- ными: независимо от того, сколько уровней комментариев открыты с помощью /*, первые встреченные символы */ завершают все комментарии блока. Почти все программисты сталкиваются с этой ловушкой. Они пытаются закомментиро- вать большую часть кода, которая уже содержит блок комментариев, а в результа- те комментарий заканчивается раньше, чем планировалось, например for(...) { /* int x78zy97 = yo6954fq; // Начало нового комментария int x78zy98 = yo6953fq; /* int x78zy99 = yo6952fq; // Начало старого комментария int x78zy9a = yo6951fq; */ // Конец старого комментария int x78zy9b = yo6950fq; */ // Конец нового комментария (увы, нет!) int x78zy9c = yo6949fq; for(...) { 02_ch01.indd 93 14.07.2016 10:46:48 |