Этот шаг посвящен еще одному способу работы с массивами.
В языке C++ для создания и обработки массивов обычно используют указатели. Напомним, что указатель - это переменная, содержащая адрес другой переменной или, говоря другими словами, указатель - символическое представление адреса.
Значением объекта типа указатель является целое неотрицательное число, равное адресу того программного объекта (переменной, функции, массива и т.д.), на который ссылается указатель.
Заметим, что в языке C++ гарантируется, что ни один "правильный" указатель не может иметь значение 0. Так что возвращение нуля в качестве значения указателя может служить сигналом о ненормальном завершении функции.
Для того, чтобы начать работу с указателями, необходимо их предварительно описать. Например:
int feet[20], /* Целочисленный массив (20 элементов). */ *pfeet; /* Указатель на целое. */
Перед указателем pfeet на массив feet мы употребили символ операции *. Ясно, что *pfeet (содержимое области памяти, в которой расположен элемент массива) имеет тип int.
Напомним, что в данном контексте символ "*" является символом унарной операции косвенной адресации. Унарная операция * рассматривает свой операнд как адрес объекта и обращается по этому адресу, чтобы извлечь его содержимое.
Приведем еще один пример описания массива:
int (*a)[15];
Описан указатель на массив, содержащий 15 целых значений. Круглые скобки здесь необходимы, поскольку скобки [] имеют более высокий уровень старшинства, чем операция *.
Теперь вспомним операцию нахождения адреса &. Например, запись:
&pooh[3]
означает указатель на элемент с индексом 3 массива pooh. Скобки [] имеют более высокий уровень старшинства, чем операция &.
Операция & применима только к переменным!
Имя массива определяет также адрес его первого элемента, т.е. если nise[] является массивом, то nise==&nise[0] и обе части равенства определяют адрес первого элемента массива. Оба обозначения являются константами типа указатель, поскольку они не изменяются на протяжении всей программы. Однако их можно присваивать переменной типа указатель и изменять значение переменной, как показано в следующем примере.
#include<iostream.h> int nise[4] = { 1,2,3,4 }; main () { int *pr; pr = nise; /* Вот необходимое нам присваивание */ /* переменной pr значения nise. */ pr += 1; cout << "Адрес = " << pr << ", значение = " << *pr << endl; pr += 2; cout << "Адрес = " << pr << ", значение = " << *pr << endl; }
А что произойдет со значением указателя, если к нему прибавить 1? Компилятор языка C++ добавит единицу памяти, т.е. если массив имеет тип int, то значение указателя увеличится на два (переменная типа int занимает два байта), если массив - типа float, то значение указателя увеличится на четыре (переменная типа float занимает четыре байта), и если массив - типа char, то значение указателя увеличится на единицу (переменная типа char занимает один байт). Вот почему необходимо специально оговаривать тип объекта, на который ссылается указатель; одного адреса здесь недостаточно, так как компилятор должен знать, сколько байтов потребуется для запоминания объекта. (Это справедливо также для указателя на скалярные переменные).
В терминах массивов это означает, что если прибавить к указателю на элемент массива единицу, то мы перейдем к адресу его следующего элемента. Так, после выполнения оператора присваивания:
pr = nise;
Заметим, что имеется существенное различие между именем массива nise и указателем pr на этот массив: указатель является переменной, так что для него операция типа pr++ имеет смысл, а имя массива - это константа, так что конструкции типа:
nise = pr; nise++; pr = &nise;
#include<iostream.h> main () { int d[5]; char f[3]; int *g[2]; char (*w)[4]; /* ----------------------------------- */ cout << "sizeof (d[5]) =" << sizeof (d[5]) << endl; cout << "sizeof (f[3]) =" << sizeof (f[3]) << endl; cout << "sizeof(int *[2])=" << sizeof(int *[2]) << endl; cout << "sizeof(char (*)[3])=" << sizeof(char (*)[3]) << endl; }
Результат работы программы:
sizeof d[5] =2 sizeof f[3] =1 sizeof(int *[2])=8 sizeof(char (*)[3])=4
Теперь покажем, как можно использовать указатель для представления i-го элемента массива. Например, рассмотрим следующий фрагмент программы:
int *pa; pa = &a[0]; x = *pa; y = *(pa+1); z = *(pa+i); //Разумеется, i уже инициализирована. pb = pa+n; //Разумеется, n уже инициализирована.
Первые два оператора означают, что переменная pa является указателем на начало (первый элемент) целочисленного массива a. Тогда в х заносится содержимое a[0], в y - a[1], в z - a[i], в pb копируется адрес элемента массива a[n].
Еще раз напомним, что суть добавления 1 к указателю состоит в том, что приращение масштабируется размером памяти того объекта, на который ссылается указатель.
Заметим еще, что компилятор превращает имя массива в указатель, поэтому присваивания:
pa = &a[0]; pa = a;
Теперь ясно, что i-й элемент массива можно представить в виде a[i], или pa[i], или *(pa+i), или *(a+i), а адрес i-го элемента есть &a[i], либо &pa[i], либо pa+i, либо a+i.
Теперь переделаем программу ввода массива в программу с использованием указателей.
#include<iostream.h> main () { int score[10], *pscore; //Ввод элементов массива. pscore = &score[0]; for (int i=0; i<=9; i++) cin >> *(pscore+i); //Проверка правильности ввода. cout << "Введены следующие результаты:\n"; for (i=0; i<=9; i++) cout << *(pscore+i) << " "; cout << endl; }
Есть возможность инициализировать указатель в программе. Например, операторы:
char a[50]; char *pa = a;
описывают символьный массив a[] типа char и указатель pa на объект типа char, а так же инициализирует pa так, чтобы он указывал на начало массива a[].
В конце шага отметим, что если p и q - указатели на элементы некоторого массива, то:
На следующем шаге мы приведем несколько примеров использования
указателей и массивов.