2: using namespace std;
3:
4: int main()
5: {
6: int SomeArray[2][5] = { {0,1,2,3,4}, {0,2,4,6,8} },
7: for (int i=0; i<2; i++)
8: {
9: for (int j=0; j<5; j++)
10: {
11: cout << "SomeArray[" << i << "][" << j << "]: ";
12: cout << SomeArray[i][j]<< endl;
13: }
14: }
15: return 0;
16: }
?????????
SomeArray[0][0]: 0
SomeArray[0][1]: 1
SomeArray[0][2]: 2
SomeArray[0][3]: 3
SomeArray[0][4]: 4
SomeArray[1][0]: 0
SomeArray[1][1]: 2
SomeArray[1][2]: 4
SomeArray[1][3]: 6
SomeArray[1][4]: 8
??????
В строке 6 объявлен двумерный массив
SomeArray
. Первый ряд элементов состоит из двух целых чисел, а второй – из пяти. Это создает сетку из 2
Ч
5 элементов
(рис. 13.4).
???? 13. ??????? ? ????????? ??????
367
Рис. 13.4. Массив 2
Ч
5
Значения разделены на два набора чисел. Первый набор – это исходные числа,
а второй – их удвоенные значения. В этом коде исходные значения заданы непосред- ственно, но их также можно было вычислить. Строки 7 и 9 содержат два цикла for
Внешний цикл for
(начинающийся в строке 7) перебирает все элементы по первой размерности (т.е. оба набора целых чисел). Для каждого элемента этой размерности
(набора) внутренний цикл for
(начинающийся в строке 9) перебирает все элементы второй размерности. Это сопровождается выводом значений на экран. За элементом
SomeArray[0][0]
следует элемент
SomeArray[0][1]
и т.д. Приращение значения по первой размерности происходит только после полного перебора второй. Затем пере- бор второй размерности начинается снова.
??????? ? ??????
??? ?????????? ??????? ??????????? ????? ?????????, ??????? ???????? ???????????
? ??? ?????????. ?????????? ????????????? ?????? ??? ???? ???????? ???????, ????
???? ??? ?? ????? ????????????. ???? ???????? ???????, ??????? ????????? ??????
??????? ??????, ?? ??????? ??????? ?? ?????????. ????????, ????????? ????? ??????
????? 64 ??????, ? ? ????? ?? ????? ???? ?????? 10 ?????. ?? ???? ?????????? ??????*
??? ??????? ??????????, ???????? ????????? ???? ???????? ??????????? ??????.
? ???? ????? ??????????????? ??????? ??????????, ???????, ??????????? ? ????*
???????? ??????, ? ?????? ???? ????????. ?? ????? ????????? ?????????? ?? ????
???? ????????? ? ?????????? ????? ??????, C++ Unleashed, ?????????????? ????*
????????? Sams Publishing, ? ????? ? ?????????? ?, “????????? ??????”.
??????? ??????????
До сих пор обсуждались массивы, члены которых размещались в стеке. Но размер стека значительно меньше объема динамической памяти, поэтому можно объявить объекты массива в динамической памяти, а в самом массиве хранить лишь указатели на них. Этот существенно уменьшает объем используемой стековой памяти. Лис- тинг 13.6 является модификацией листинга 13.4, но элементы массива в нем располо- жены в динамической памяти. Поскольку появилась возможность занять больше па- мяти, размер массива увеличен с 5 до 500 элементов, а имя массива
Litter
(потомство) изменено на
Family
(семья).
??????? 13.6. ?????????? ??????? ? ???????????? ??????
0: //
???????
13.6.
??????
??????????
??
???????
1:
2: #include
3: using namespace std;
368
?????? 2. ???????? ???????
4:
5: class Cat
6: {
7: public:
8: Cat() { itsAge = 1; itsWeight=5; }
9: Cat() {} //
??????????
10: int GetAge() const { return itsAge; }
11: int GetWeight() const { return itsWeight; }
12: void SetAge(int age) { itsAge = age; }
13:
14: private:
15: int itsAge;
16: int itsWeight;
17: };
18:
19: int main()
20: {
21: Cat * Family[500];
22: int i;
23: Cat * pCat;
24: for (i=0; i<500; i++)
25: {
26: pCat = new Cat;
27: pCat->SetAge(2*i+1);
28: Family[i] = pCat;
29: }
30:
31: for (i=0; i<500; i++)
32: {
33: cout << "Cat #" << i+1 << ": ";
34: cout << Family[i]->GetAge() << endl;
35: }
36: return 0;
37: }
?????????
Cat #1: 1
Cat #2: 3
Cat #3: 5
Cat #499: 997
Cat #500: 999
??????
Класс
Cat
, объявленный в строках 5—17, идентичен классу
Cat
, объявленному в листинге 13.4. А массив
Family
, объявленный в строке 21, на сей раз будет содер- жать до 500 элементов, являющихся указателями на объекты класса
Cat
Цикл инициализации (строки 24—29) создает в динамической памяти 500 новых объектов класса
Cat
, и для каждого из них устанавливается возраст вдвое больше его номера, увеличенного на единицу. Следовательно, первый кот будет иметь возраст
1
,
второй –
3
, третий –
5
и т.д. Затем указатели добавляются в массив.
Второй цикл (строки 31—35) выводит каждое значение на экран. Код строки 33
отображает на экране число, соответствующее номеру объекта. Поскольку индексиро- вание начинается с нуля, для правильного счета в строке 33 к числу добавляется
1
???? 13. ??????? ? ????????? ??????
369
Для доступа к указателю используется индекс массива
Family[i]
, а полученный ад- рес используется для доступа к методу
GetAge()
текущего объекта.
В данном случае сам массив
Family и все его указатели хранятся в стеке, но 500 объек- тов класса
Cat созданы и размещены в области динамической памяти.
?????????????? ????????
??? ???????????
На занятии дня 8, “Указатели”, первоначальные сведения об указателях уже были представлены. Однако прежде, чем продолжать изучение массивов, имеет смысл вер- нуться к указателям и рассмотреть еще одну тему – арифметические операции над указателями.
С указателями можно осуществить несколько математических действий. Их можно вычитать друг из друга. Давайте рассмотрим одну из возможностей применения этого подхода. Если существуют два указателя, содержащих адреса двух разных элементов массива, то их разница позволит выяснить, сколько элементов расположено между ними. При анализе символьных массивов это может оказаться весьма полезным, как проиллюстрировано в листинге 13.7.
??????? 13.7. ?????? ? ????????? ?????????? ?????? ?? ?????
0: #include
1: #include
2: #include
3:
4: bool GetWord(char* theString,
5: char* word, int& wordOffset);
6:
7: //
????????
?????????
8: int main()
9: {
10: const int bufferSize = 255;
11: char buffer[bufferSize+1]; //
????????
???
??????
12: char word[bufferSize+1]; //
????????
?????
13: int wordOffset = 0; //
?????????
???????
14:
15: std::cout << "Enter a string: ";
16: std::cin.getline(buffer,bufferSize);
17:
18: while (GetWord(buffer, word, wordOffset))
19: {
20: std::cout << "Got this word: " << word << std::endl;
21: }
22: return 0;
23: }
24:
25: //
???????
??????????
??????
??
?????
26: bool GetWord(char* theString, char* word, int& wordOffset)
27: {
28: if (theString[wordOffset] == 0) //
?????
??????
?
29: return false;
30:
31: char *p1, *p2;
32: p1 = p2 = theString+wordOffset; //
?????????
??
?????????
33: //
?????
370
?????? 2. ???????? ???????
34: //
??????
????????????
???????
35: for (int i=0; i<(int)strlen(p1) && !isalnum(p1[0]); i++)
36: p1++;
37:
38: //
????????
??
?????
?
39: if (!isalnum(p1[0]))
40: return false;
41:
42: //
??????
p1
?????????
??
??????
??????????
?????
43: // p2 -
??????
????
44: p2 = p1;
45:
46: //
?????????
p2
?
?????
?????
47: while (isalnum(p2[0]))
48: p2++;
49:
50: //
??????
p2 -
?
?????
?????
,
51: // p1 -
?
??????
?????
,
52: //
?
???????
?????
????
-
???
?????
?????
53: int len = int (p2 - p1);
54:
55: //
???????????
?????
?
?????
56: strncpy (word, p1, len);
57:
58: //
????????
??????
???????????
??????
59: word[len] = '\0';
60:
61: //
??????
?????
??????
??????????
?????
62: for (int j=int(p2-theString); j<(int)strlen(theString)
63: && !isalnum(p2[0]); j++)
64: {
65: p2++;
66: }
67:
68: wordOffset = int(p2-theString);
69:
70: return true;
71: }
?????????
Enter a string: this code first appeared in C++ Report
Got this word: this
Got this word: code
Got this word: first
Got this word: appeared
Got this word: in
Got this word: C
Got this word: Report
??????
Эта программа предлагает пользователю ввести предложение, которое она разделяет на отдельные слова (наборы алфавитно-цифровых символов). Предложение ввести стро- ку расположено в строке 15. В строке 18 эта строка передается функции
GetWord()
на- ряду с предназначенным для хранения первого слова буфером и целочисленной пере- менной
WordOffset
, инициализированной в строке 13 нулевым значением.
???? 13. ??????? ? ????????? ??????
371
Функция
GetWord()
возвращает
отдельные слова из строки до тех пор, пока не будет достигнут конец строки. Пока функция
GetWord()
не вернет значение
False
,
возвращаемые ей слова отображаются на экране кодом строки 20.
После каждого вызова функции
GetWord()
управление возвращается к строке 26.
В строке 28 осуществляется проверка значения string[wordOffset]
на равенство нулю. Это произойдет в случае, если процесс находится в конце строки (функция
GetWord()
возвратила значение
False
). Функция cin.GetLine()
позволяет завер- шить введенную строку пустым символом, т.е. символом, полученным в результате вычисления управляющей последовательности '\0';
В строке 31 объявлены два указателя на тип char
(символ) p1
и p2
, а в строке 32 им присваиваются адрес начала строки плюс смещение wordOffset
. Поскольку первоначаль- но значением переменной wordOffset является нуль, они указывают на начало строки.
Строки 35 и 36 содержат цикл, перебирающий строку в поисках первого алфавит- но-цифрового символа, увеличивая соответственно значение указателя p1
. Код строк
39 и 40 проверяет, является ли найденный символ алфавитно-цифровым. Если это не так, возвращается значение
False
Теперь указатель p1
содержит адрес начала следующего слова, а в строке 44 указа- тель p2
устанавливается в ту же позицию.
Цикл в строках 47 и 48, перебирая слово, увеличивает значение указателя p2
и ос- танавливается на первом, не алфавитно-цифровом, символе. Теперь указатель p2
со- держит адрес конца того слова, на начало которого указывает p1
. Вычислив разницу значений в указателях p1
из p2
и приведя результат в строке 53 к целому числу, полу- чим длину слова. Передав методу strncpy()
из стандартной библиотеки буфер word
,
отправную точку p1
и длину, скопируем слово в буфер.
В строке 59 к расположенному в буфере слову добавляется нулевой символ, отме- чающий конец слова. Затем значение адреса в указателе увеличивается на единицу,
чтобы он указывал на начало следующего слова, а значение его смещения заносится в переменную wordOffset
. И, наконец, возвращается значение true
, свидетельст- вующее об успешном обнаружении слова.
Это классический пример кода, изучать который удобнее всего в режиме пошаго- вого выполнения в отладчике.
В этом листинге можно заметить несколько случаев арифметических операций над указателями: в строке 53 вычитание одного указателя из другого позволяет вычислить
количество элементов между двумя указателями, а в строке 55 приращение значения указателя применяется для его сдвига на следующий элемент массива. Использование арифметических операций над указателями – это общепринятый подход при работе с указателями и массивами. Однако поскольку ошибки здесь способны привести к опасным последствиям, применять его следует внимательно.
?????????? ???????? ? ???????
???????????? ??????
Весь массив можно разместить в области динамической памяти (известной также под именем heap). Для этого при объявлении массива используется оператор new
. Ре- зультатом окажется указатель на пространство в динамической памяти, где и будет со- держаться массив, например:
Cat *Family = new Cat[500];
Здесь объявлено, что
Family будет указателем на первый элемент массива из 500 объ- ектов класса
Cat
. Иными словами,
Family указывает на элемент
Family[0]
(или со- держит его адрес).
372
?????? 2. ???????? ???????
Преимуществом такого способа использования массива является возможность арифметических операций над указателями для доступа к каждому элементу массива
Family
. Например, можно написать:
Cat *Family = new Cat[500];
Cat *pCat = Family; // pCat
?????????
??
Family[0]
pCat->SetAge(10); //
?????????
Family[0]
????????
10
pCat++; //
??????
?
Family[1]
pCat->SetAge(20); //
?????????
Family[1]
????????
20
Здесь в динамической памяти объявлен массив из 500 объектов класса
Cat
, а также создан указатель, содержащий адрес начала массива. С помощью этого указателя осу- ществляется вызов метода
SetAge()
первого из объектов класса
Cat
, который и при- сваивает ему значение
10
. Затем указатель увеличивается и указывает уже на следую- щий объект класса
Cat
, поэтому при вызове метода
SetAge()
значение
20
будет присвоено второму объекту.
????????? ?? ?????? ? ?????? ??????????
Рассмотрим следующие три объявления:
1: Cat FamilyOne[500];
2: Cat * FamilyTwo[500];
3: Cat * FamilyThree = new Cat[500];
Здесь
FamilyOne
– это массив из 500 объектов класса
Cat
,
FamilyTwo
– массив из 500 указателей на объекты класса
Cat
, а
FamilyThree
– указатель на массив из
500 объектов класса
Cat
Различие между этими тремя строками весьма существенно и влияет на способ существования массивов. Но самое удивительное то, что указатель
FamilyThree явля- ется вариантом
FamilyOne
, а от указателя
FamilyTwo отличается принципиально.
В этом вся суть проблемы взаимосвязи указателей и массивов. В третьем случае
FamilyThree представляет собой указатель на массив. Т.е. адрес, находящийся в ука- зателе
FamilyThree
, является адресом первого элемента в этом массиве. Но это ана- логично тому, что имеет место и для
FamilyOne
!
????? ???????? ? ??????????
В языке C++ имя массива является постоянным указателем на первый элемент массива.
Cat Family[50];
Следовательно, здесь
Family представляет собой указатель на переменную
&Family[0]
, являющуюся первым элементом массива
Family
Вполне допустимо использовать имена массивов как постоянные указатели и на- оборот. Следовательно,
Family + 4
– вполне законный способ доступа к данным в элементе
Family[4]
При инкременте и декременте (увеличении и уменьшении) указателя компилятор делает все вычисления сам. Адрес, полученный в результате вычисления выражения
Family + 4
, не на четыре байта больше исходного, а на четыре объекта. Если все объекты обладают размером четыре байта, то
Family + 4
составит 16 байтов от нача- ла массива. Таким образом, если каждый объект класса
Cat будет содержать четыре переменные-члена типа long размером четыре байта каждая и две – типа short раз- мером два байта каждая, то размер каждого объекта класса
Cat составит 20 байтов,
а
Family + 4
будет на 80 байтов больше
Family
(адреса начала массива).
???? 13. ??????? ? ????????? ??????
373
Листинг 13.8 иллюстрирует объявление и использование массива в динамической памяти.
??????? 13.8. ???????? ??????? ? ???????????? ??????
0: //
???????
13.8.
??????
?
????????????
??????
1:
2: #include
3:
4: class Cat
5: {
6: public:
7: Cat() { itsAge=1; itsWeight=5; }
8: Cat();
9: int GetAge() const { return itsAge; }
10: int GetWeight() const { return itsWeight; }
11: void SetAge(int age) { itsAge = age; }
12:
13: private:
14: int itsAge;
15: int itsWeight;
16: };
17:
18: Cat :: Cat()
19: {
20: // std::cout << "Destructor called!\n";
21: }
22:
23: int main()
24: {
25: Cat * Family = new Cat[500];
26: int i;
27:
28: for (i=0; i<500; i++)
29: {
30: Family[i].SetAge(2*i +1);
31: }
32:
33: for (i=0; i<500; i++)
34: {
35: std::cout << "Cat #" << i+1 << ": ";
36: std::cout << Family[i].GetAge() << std::endl;
37: }
38:
39: delete [] Family;
40:
41: return 0;
42: }
?????????
Cat #1: 1
Cat #2: 3
Cat #3: 5
Cat #499: 997
Cat #500: 999
374
?????? 2. ???????? ???????
??????
В строке 25 объявлен массив
Family
, содержащий 500 объектов класса
Cat
. Весь массив создан в динамической памяти с помощью выражения new Cat[500]
Как можно заметить, в строке 30 объявленный указатель применяется с использо- ванием оператора индекса
[]
, а, следовательно, обрабатывается так же, как и обыч- ный массив. В строке 36 при вызове метода
GetAge()
это продемонстрировано еще раз. Таким образом, при решении практических задач указатель на массив
Family можно использовать вместо имени массива. Не забывайте, однако, освободить память,
выделенную для массива при его создании, как это сделано в строке 39 при помощи оператора delete
???????? ???????? ?? ???????????? ??????
Что происходит с выделенной для объектов класса
Cat памятью, когда массив уда- ляется? Не происходит ли утечка памяти?
Удаление массива
Family автоматически возвращает всю выделенную для него память, если оператор delete использован с квадратными скобками
[]
. Компилятор вычисляет размер всех элементов массива и освобождает занимаемую им область ди- намической памяти.
Чтобы продемонстрировать это, уменьшим в строках 25, 28 и 33 размер массива с 500 до 10-ти элементов, а затем раскомментируем в строке 20 оператор cout
. По достижении строки 39 массив будет удален, и для каждого объекта класса
Cat будет вызван деструктор.
Когда с помощью оператора new в динамической памяти создается элемент, то при его удалении непременно следует освободить занимаемую им область памяти. Создав в динамической памяти массив с помощью оператора new <
?????>[
??????]
, впо- следствии необходимо применить оператор delete[]
, чтобы освободить эту область памяти. Квадратные скобки сообщают компилятору о том, что удаляется массив.
Если забыть квадратные скобки, то освобожден будет лишь первый объект масси- ва. В
этом легко убедиться, удалив скобки в строке 39. Если отредактировать строку
20 так, чтобы при каждом вызове деструктора печаталось сообщение, то можно уви- деть, что был освобожден только один объект класса
Cat
. Поздравляем! Только что произошла утечка памяти.
????????? ??????? ??????? ?? ?????
??????????
Наибольшим преимуществом создания массива в распределяемой памяти является возможность выяснить необходимый для него размер во время выполнения, а затем создать его. Например, если запросить у пользователя размер семьи и занести его значение в переменную
SizeOfFamily
(размер семьи), то массив элементов класса
Cat можно объявить следующим образом:
Cat *pFamily = new Cat[SizeOfFamily];
В результате будет получен указатель на массив объектов класса
Cat
. Теперь мож- но создавать указатель на первый элемент массива, а затем, используя этот указатель и арифметические операции над указателями, перебрать в цикле все его элементы следующим образом:
Cat *pCurrentCat = Family[0];
for (int Index=0; Index
???? 13. ??????? ? ????????? ??????
375
{
pCurrentCat->SetAge(Index);
};
Поскольку язык C++ рассматривает массивы как частный случай указателя, второй указатель можно отбросить и просто использовать стандартную индексацию массива:
for (int Index=0; Index
{
pFamily[Index].SetAge(Index);
};
Использование квадратных скобок позволяет компилятору автоматически получить адрес соответствующего указателя и произвести необходимые арифметические опера- ции над ним.
Еще одно преимущество: подобный подход можно использовать для изменения размера массива во время выполнения, если отведенный для него участок памяти ис- черпан. Такую возможность демонстрирует листинг 13.9.