Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Программирование на C / C++ / Ален И. Голуб. Правила программирования на Си и Си++ [pdf]

.pdf
Скачиваний:
237
Добавлен:
02.05.2014
Размер:
5.67 Mб
Скачать

С++ для начинающих

392

*p_auto_int = 1024;

Если auto_ptr ни на что не указывает, то как заставить его адресовать что-либо? Другими словами, как мы можем присвоить значение внутреннему указателю объекта

else

// хорошо, присвоим ему значение

auto_ptr? Это делается с помощью операции reset(). Например: p_auto_int.reset( new int( 1024 ) );

Объекту auto_ptr нельзя присвоить адрес объекта, созданного с помощью оператора

void example() {

// инициализируется нулем по умолчанию auto_ptr< int > pi;

{

// не поддерживается pi = new int( 5 ) ;

}

new:

}

В этом случае надо использовать функцию reset(), которой можно передать указатель или 0, если мы хотим обнулить объект auto_ptr. Если auto_ptr указывает на объект и является его владельцем, то этот объект уничтожается перед присваиванием нового

auto_ptr< string >

pstr_auto( new string( "Brontosaurus" ) );

// "Brontosaurus" уничтожается перед присваиванием

значения внутреннему указателю auto_ptr. Например: pstr_auto.reset( new string( "Long-neck" ) );

Впоследнем случае лучше, используя операцию assign(), присвоить новое значение

//более эффективный способ присвоить новое значение

//используем операцию assign()

существующей строке, чем уничтожать одну строку и создавать другую: pstr_auto->assign( "Long-neck" );

Одна из трудностей программирования состоит в том, что получить правильный результат не всегда достаточно. Иногда накладываются и временные ограничения. Такая мелочь, как удаление и создание заново строкового объекта, вместо использования

функции assign() при определенных обстоятельствах может вызвать значительное замедление работы. Подобные детали не должны вас беспокоить при проектировании, но при доводке программы на них следует обращать внимание.

С++ для начинающих

393

Шаблон класса auto_ptr обеспечивает значительные удобства и безопасность использования динамически выделяемой памяти. Однако все равно надо не терять бдительности, чтобы не навлечь на себя неприятности:

нельзя инициализировать объект auto_ptr указателем, полученным не с помощью оператора new, или присвоить ему такое значение. В противном случае

после применения к этому объекту оператора delete поведение программы непредсказуемо;

два объекта auto_ptr не должны получать во владение один и тот же объект. Очевидный способ допустить такую ошибку присвоить одно значение двум

auto_ptr< string >

pstr_auto( new string( "Brontosaurus" ) );

//ошибка: теперь оба указывают на один объект

//и оба являются его владельцами

объектам. Менее очевидный с помощью операции get(). Вот пример: auto_ptr< string > pstr_auto2( pstr_auto.get() );

Операция release() гарантирует, что несколько указателей не являются владельцами одного и того же объекта. release() не только возвращает адрес объекта, на который ссылается auto_ptr, но и передает владение им.

//правильно: оба указывают на один объект,

//но pstr_auto больше не является его владельцем auto_ptr< string >

Предыдущий фрагмент кода нужно переписать так: pstr_auto2( pstr_auto.release() );

8.4.3. Динамическое создание и уничтожение массивов

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

//создание единственного объекта типа int

//с начальным значением 1024

int *pi = new int( 1024 );

//создание массива из 1024 элементов

//элементы не инициализируются

int *pia = new int[ 1024 ];

// создание двумерного массива из 4x1024 элементов

элемент массива. Например:

int (*pia2)[ 1024 ] = new int[ 4 ][ 1024 ];

pi содержит адрес единственного элемента типа int, инициализированного значением 1024; pia адрес первого элемента массива из 1024 элементов; pia2 адрес начала

С++ для начинающих

394

массива, содержащего четыре массива по 1024 элемента, т.е. pia2 адресует 4096 элементов.

В общем случае массив, размещаемый в хипе, не может быть инициализирован. (В разделе 15.8 мы покажем, как с помощью конструктора по умолчанию присвоить начальное значение динамическому массиву объектов типа класса.) Задавать инициализатор при выделении оператором new памяти под массив не разрешается. Массиву элементов встроенного типа, размещенному в хипе, начальные значения

for (int index = 0; index < 1024; ++index )

присваиваются с помощью цикла for: pia[ index ] = 0;

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

Например, если указатель в ходе выполнения программы ссылается на разные C-строки,

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

Оператор new допустимо использовать для задания первого измерения массива с помощью значения, вычисляемого во время выполнения. Предположим, у нас есть

const char *noerr = "success"; // ...

const char *err189 = "Error: a function declaration must "

следующие C-строки:

"specify a function return type!";

Размер создаваемого с помощью оператора new массива может быть задан значением,

#include <cstring>

const char *errorTxt;

if (errorFound) errorTxt = errl89;

else

errorTxt = noerr;

int dimension = strlen( errorTxt ) + 1; char *strl = new char[ dimension ];

// копируем текст ошибки в strl

вычисляемым во время выполнения: strcpy( strl, errorTxt );

С++ для начинающих

395

//обычная для С++ идиома,

//иногда удивляющая начинающих программистов

dimension разрешается заменить выражением:

char *strl = new char[ str1en( errorTxt ) + 1 ];

Единица, прибавляемая к значению, которое возвращает strlen(), необходима для учета завершающего нулевого символа в C-строке. Отсутствие этой единицы весьма распространенная ошибка, которую достаточно трудно обнаружить, поскольку она проявляет себя косвенно: происходит затирание какой-либо другой области программы. Почему? Большинство функций, которые обрабатывают массивы, представляющие собой С-строки символов, пробегают по элементам, пока не встретят завершающий нуль.

Если в конце строки нуля нет, то возможно чтение или запись в случайную область памяти. Избежать подобных проблем позволяет класс string из стандартной библиотеки С++.

Отметим, что только первое измерение массива, создаваемого с помощью оператора new, может быть задано значением, вычисляемым во время выполнения. Остальные измерения

int getDim();

// создание двумерного массива

int (*pia3)[ 1024 ] = new int[ getDim() ][ 1024 ]; // правильно

// ошибка: второе измерение задано не константой

должны задаваться константами, известными во время компиляции. Например: int **pia4 = new int[ 4 ][ getDim() ];

Оператор delete для уничтожения массива имеет следующую форму:

delete[] str1;

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

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

Чтобы избежать проблем, связанных с управлением динамически выделяемой памятью для массивов, рекомендуется пользоваться контейнерными типами из стандартной библиотеки, такими, как vector, list или string. Они управляют памятью автоматически. (Тип string был представлен в разделе 3.4, тип vector в разделе 3.10. Подробное описание контейнерных типов см. в главе 6.)

С++ для начинающих

396

8.4.4. Динамическое создание и уничтожение константных объектов

Программист способен создать объект в хипе и запретить изменение его значения после инициализации. Этого можно достичь, объявляя объект константным. Для этого применяется следующая форма оператора new:

const int *pci = new const int(1024);

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

Во-вторых, указатель, возвращаемый выражением new, должен адресовать константу. В предыдущем примере pci служит указателем на const int.

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

delete pci;

Хотя операнд оператора delete имеет тип указателя на const int, эта инструкция является корректной и освобождает область памяти, на которую ссылается pci.

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

const int *pci = new const int[100]; // ошибка

8.4.5. Оператор размещения new А

Существует третья форма оператора new, которая создает объект без отведения для него памяти, то есть в памяти, которая уже была выделена. Эту форму называют оператором размещения new. Программист указывает адрес области памяти, в которой размещается объект:

new (place_address) type-specifier

place_address должен быть указателем. Такая форма (она включается заголовочным файлом <new>) позволяет программисту предварительно выделить большую область памяти, которая впоследствии будет содержать различные объекты. Например:

С++ для начинающих

397

#include <iostream> #include <new>

const int chunk = 16; class Foo {

public:

int val() { return _val; } FooQ(){ _val = 0; }

private:

int _val;

};

// выделяем память, но не создаем объектов Foo char *buf = new char[ sizeof(Foo) * chunk ];

int main() {

//создаем объект Foo в buf Foo *pb = new (buf) Foo;

//проверим, что объект помещен в buf if ( pb.val() == 0 )

cout << "Оператор new сработал!" << endl;

// здесь нельзя использовать pb delete[] buf;

return 0;

}

Результат работы программы:

Оператор new сработал!

Для оператора размещения new нет парного оператора delete: он не нужен, поскольку эта форма не выделяет память. В предыдущем примере необходимо освободить память, адресуемую указателем buf, а не pb. Это происходит в конце программы, когда буфер больше не нужен. Поскольку buf ссылается на символьный массив, оператор delete

имеет форму

delete[] buf;

При уничтожении buf прекращают существование все объекты, созданные в нем. В нашем примере pb больше не ссылается на существующий объект класса Foo.

Упражнение 8.5

(a)const float *pf = new const float[100];

(b)double *pd = new doub1e[10] [getDim()];

(c)int (*pia2)[ 1024 ] = new int[ ][ 1024 ];

Объясните, почему приведенные операторы new ошибочны:

(d) const int *pci = new const int;

Упражнение 8.6

Как бы вы уничтожили pa?

С++ для начинающих

398

typedef int arr[10];

int *pa = new arr;

Упражнение 8.7

Какие из следующих операторов delete содержат потенциальные ошибки времени

int globalObj; char buf[1000];

void f() {

int *pi = &global0bj; double *pd = 0;

float *pf = new float(O); int *pa = new(buf)int[20];

delete pi;

// (a)

delete pd;

// (b)

delete pf;

// (c)

de1ete[] pa; // (d)

выполнения и почему:

}

Упражнение 8.8

Какие из данных объявлений auto_ptr неверны или грозят ошибками времени

int ix = 1024; int *pi = & ix;

int *pi2 = new int ( 2048 );

(a)auto_ptr<int> p0(ix);

(b)auto_ptr<int> pl(pi);

(c)auto_ptr<int> p2(pi2);

(d)auto_ptr<int> p3(&ix);

(e)auto_ptr<int> p4(new int(2048));

(f)auto_ptr<int> p5(p2.get());

(9) auto_ptr<int> p6(p2.release());

выполнения? Объясните каждый случай.

(h) auto_ptr<int> p7(p2);

Упражнение 8.9

int *pi0 = p2.get();

Объясните разницу между следующими инструкциями: int *pi1 = p2.release() ;

Для каких случаев более приемлем тот или иной вызов? Упражнение 8.10

С++ для начинающих

399

Пусть мы имеем:

 

 

auto_ptr< string > ps( new string( "Daniel" )

);

 

 

 

 

В чем разница между этими двумя вызовами assign()?Какой их них предпочтительнее

ps.get()->assign( "Danny" );

и почему?

ps->assign( "Danny" );

8.5. Определения пространства имен А

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

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

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

class cplusplus_primer_matrix { ... };

префикса употребляется определенная последовательность символов. Например: void inverse( cplusplus_primer_matrix & );

Однако у этого решения есть недостаток. Программа, написанная на С++, может содержать множество глобальных классов, функций и шаблонов, видимых в любой точке кода. Работать со слишком длинными идентификаторами для программистов утомительно.

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

namespace cplusplus_primer { class matrix { /*...*/ }; void inverse ( matrix & );

вынести используемые в библиотеке имена из глобальной области видимости:

}

С++ для начинающих

400

cplusplus_primer является пользовательским пространством имен (в отличие от глобального пространства, которое неявно подразумевается и существует в любой программе).

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

Однако в разных пользовательских пространствах могут встречаться члены с одинаковыми именами.

Имя члена пространства имен автоматически дополняется, или квалифицируется, именем этого пространства. Например, имя класса matrix, объявленное в пространстве cplusplus_primer, становится cplusplus_primer::matrix, а имя функции inverse() превращается в cplusplus_primer::inverse().

Члены cplusplus_primer могут использоваться в программе с помощью спецификации

void func( cplusplus_primer::matrix &m )

{

// ...

cplusplus_primer::inverse(m); return m;

имени:

}

Если в другом пользовательском пространстве имен (скажем, DisneyFeatureAnimation)

также существует класс matrix и функция inverse() и мы хотим использовать этот класс вместо объявленного в пространстве cplusplus_primer, то функцию func()

void func( DisneyFeatureAnimation::matrix &m )

{

// ...

DisneyFeatureAnimation::inverse(m); return m;

нужно модифицировать следующим образом:

}

Конечно, каждый раз указывать специфицированные имена типа

namespace_name::member_name

неудобно. Поэтому существуют механизмы, позволяющие облегчить использование пространств имен в программах. Это псевдонимы пространств имен, using-объявления и using-директивы. (Мы рассмотрим их в разделе 8.6.)

С++ для начинающих

401

8.5.1. Определения пространства имен

Определение пользовательского пространства имен начинается с ключевого слова namespace, за которым следует идентификатор. Он должен быть уникальным в той области видимости, в которой определяется данное пространство; наличие другой сущности с тем же именем является ошибкой. Конечно, это не означает, что проблема засорения глобального пространства решена полностью, но существенно помогает в ее решении.

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

Помещая объявление в пользовательское пространство, мы не меняем его семантики. Единственное отличие состоит в том, что имена, вводимые такими объявлениями,

namespace cplusplus_primer { class matrix { /* ... */ }; void inverse ( matrix & );

matrix operator+ ( const matrix &ml, const matrix &m2 ) {/* ... */ }

const double pi = 3.1416;

включают в себя имя пространства, внутри которого они объявлены. Например:

}

Именем класса, объявленного в пространстве cplusplus_primer, будет

cplusplus_primer::matrix

Именем функции

cplusplus_primer::inverse()

Именем константы

cplusplus_primer::pi

Имя класса, функции или константы расширяется именем пространства, в котором они объявлены. Такие имена называют квалифицированными.

Определение пространства имен не обязательно должно быть непрерывным. Например, предыдущее пространство могло быть определено таким образом: