Поможем написать учебную работу
Если у вас возникли сложности с курсовой, контрольной, дипломной, рефератом, отчетом по практике, научно-исследовательской и любой другой работой - мы готовы помочь.

Предоплата всего

Подписываем
Если у вас возникли сложности с курсовой, контрольной, дипломной, рефератом, отчетом по практике, научно-исследовательской и любой другой работой - мы готовы помочь.
Предоплата всего
Подписываем
PAGE 6
Лекция 6
4.6. Указатели и массивы
4.6.1. Указатели
Указатель это элемент данных (константа или переменная), предназначенный для хранения адресов областей памяти (констант, переменных, функций). Он не является самостоятельным типом. Указатель всегда связан с некоторым конкретным типом, адреса которого он может содержать.
Указатель на данные содержит адрес памяти, в которой хранятся данные определенного типа (основного или составного). Простейшее объявление такого указателя имеет вид:
тип *имя;
где тип может быть любым, кроме ссылки и битового поля. Звездочка относится к имени, поэтому для объявления нескольких указателей требуется ставить ее перед именем каждого из них. Например, в операторе
int *a, b, *с;
описываются два указателя на целое с именами а и с, а также целая переменная b.
Указатель на функцию содержит адрес, по которому передается управление при ее вызове. Указатели на функции используются для косвенного вызова (не по имени функции, а посредством переменной, содержащей ее адрес), а также для передачи одной функции в другую функцию в качестве параметра. Его объявление имеет вид
тип (*имя) (список_типов_аргументов);
Например, объявление:
int (*fun) (double, double);
определяет переменную fun как указатель на функцию, возвращающую значение типа int и имеющую два аргумента типа double.
Можно также определить указатель на указатель и т. д. Возможно определение указателя на void. Оно применяется в случае, когда конкретный тип адресуемых элементов не определен (например, если в одной и той же переменной возможно хранение адресов данных различных типов). Указателю на void можно присвоить значение указателя любого типа, а также сравнивать его с любыми указателями. Однако перед выполнением каких-либо действий с областью памяти, на которую он ссылается, требуется преобразовать его к конкретному типу явным образом.
Рассмотрим примеры:
int i; // целая переменная
const int ci = 1; // целая константа
int *pi; // указатель на целую переменную
const int *pci; // указатель на целую константу
int * const cp = &i; // указатель-константа на целую переменную
const int * const cpc = &ci; // указатель-константа на целую константу
void f (int a) { /* */ } // определение функции
void (*pf) (int), // указатель на функцию
pf = f; // присваивание адреса функции
Указатели часто используются при работе с динамической памятью кучей (от английского heap). Это свободная память, которую во время выполнения программы можно использовать в соответствии с ее потребностями. Доступ к выделенным участкам динамической памяти, называемым динамическими переменными, производится через указатели. Время жизни динамических переменных - от точки создания до конца программы или до явного освобождения памяти. В C++ используется два способа работы с динамической памятью. Первый использует функции mallос и free (достался в наследство от С), второй - операции new и delete.
Указателю можно присваивать следующие значения:
С указателями можно выполнять ряд операций: разадресация, или косвенное обращение к объекту (*), присваивание, сложение с константой, вычитание, инкремент (++), декремент (--), сравнение, приведение типов. При работе с указателями часто используется операция получения адреса (&).
Операция разадресации предназначена для доступа к величине, адрес которой содержится в указателе. Например:
char a; // переменная типа char
char *p = new char, /* объявление указателя и его инициализация адресом
динамической переменной типа char */
*p = Ю; a = *p; // присваивание значений обеим переменным
На одну и ту же область памяти может ссылаться несколько указателей различного типа. Применяемая к ним операция разадресации может давать различные результаты. Например:
#include <stdio.h>
int main()
{
unsigned long int A = 0xCC77FFAA;
unsigned short int *pInt = (unsigned short int*) &A;
unsigned char *pChar = (unsigned char *) &A;
printf (“ | %x | %x | %x |", A, *pInt, *pChar);
return 0;
}
Результаты: | CC77FFAA | FFAA | AA |
В этом примере при инициализации указателей были использованы операции приведения типов. Синтаксис операции явного приведения типа таков: перед именем переменной в скобках указывается тип, к которому ее требуется преобразовать. Поскольку при этом не гарантируется сохранение информации, то без необходимости не рекомендуется использовать явные преобразования типов.
При смешивании в выражении указателей разных типов явное преобразование типов требуется для всех указателей, кроме void*. Указатель может неявно преобразовываться в значение типа bool (например, в выражении условного оператора), при этом ненулевой указатель преобразуется в true, а нулевой в false. Значение 0 (пустой указатель) неявно преобразуется к указателю любого типа.
Арифметические операции над указателями автоматически учитывают размер адресуемых величин. Так инкремент перемещает указатель к следующему элементу в памяти, декремент - к предыдущему. Если указатель на определенный тип увеличивается или уменьшается на константу, его значение изменяется на величину этой константы, умноженную на размер данного типа, например:
short *р = new short[5];
р++; // значение р увеличивается на 2
p += 5; // значение р увеличивается на 10
long *q = new long[5];
q++; // значение q увеличивается на 4
Разность двух указателей - это разность их значений, деленная на размер типа в байтах. Суммирование двух указателей не допускается.
Унарная операция получения адреса & применима лишь к величинам, имеющим имя и размещенным в оперативной памяти. Таким образом, невозможно получить адрес скалярного выражения, неименованной константы или регистровой переменной.
Наряду с указателем, в C++ имеется элемент данных ссылка (в C его не было). Ссылку можно рассматривать как указатель, который всегда автоматически разыменовывается, и поэтому при работе с ней операция * не нужна. Формат объявления ссылки таков:
тип & имя;
где тип это тип величины, на которую указывает ссылка, & - признак ссылки. Например:
int kol;
int & pal = kol; // ссылка pal - альтернативное имя для kol
const char & CR = '\n'; // ссылка на константу
Имеют место следующие правила:
Ссылки чаще всего применяются в качестве параметров функций и типов возвращаемых функциями значений. Ссылки позволяют использовать в функциях переменные, передаваемые по адресу, без операции разадресации. Это улучшает читаемость программы.
Ссылка, в отличие от указателя, не занимает дополнительного пространства в памяти и является просто другим именем (синонимом) величины. Операция над ссылкой приводит к изменению величины, на которую она ссылается.
4.6.2. Массивы
Конечная именованная последовательность однотипных величин называется массивом. Описание массива в программе отличается от описания простой переменной наличием после имени квадратных скобок, в которых задается количество элементов массива (размер). Например,
float a[10]; // Вещественный массив из 10 элементов
Элементы массива всегда нумеруются с нуля. При описании массива используются те же модификаторы (класс памяти, const и инициализатор), что и для простых переменных. Инициализирующие значения для массивов записываются в фигурных скобках. Значения элементам присваиваются по порядку. Если элементов в массиве больше, чем инициализаторов, элементы, для которых значения не указаны, обнуляются:
int b[5] = {3, 2, 1}; // b[0]=3. b[l]=2, b[2]=l. b[3]=0, b[4]=0
Размер массива вместе с типом его элементов определяют объем памяти, необходимый для размещения массива. Оно выполняется на этапе компиляции, поэтому размер может быть задан только целой положительной константой или константным выражением. При описании массива размер можно не указывать, при это должен присутствовать инициализатор, и компилятор выделит память по количеству инициализирующих значений.
Для доступа к элементу массива после его имени указывается номер элемента (индекс) в квадратных скобках. В следующем примере подсчитывается сумма элементов массива.
#include <iostream h>
int main ()
{
const int n = 10;
int i, sum;
int marks[n] = {3, 4, 5, 4, 4};
for (i = 0, sum = 0; i < n; i++) sum += marks[i];
cout << "Сумма элементов- " << sum;
return 0;
}
При обращении к элементам массива автоматический контроль выхода индекса за границу массива не производится, что может приводить к ошибкам.
Идентификатор массива является константным указателем на его нулевой элемент. Например, для массива из предыдущего имя marks - это то же самое, что &marks[0], а к i-му элементу массива можно обратиться, используя выражение *(marks+i). Можно описать указатель, присвоить ему адрес начала массива и работать с массивом через указатель. Например:
int a[100], b[100];
………………………………………………
int *pa = а; // или int *рa = &а[0];
int *pb = b;
for (int i = 0; i < 100; i++) *pb++ = *pa++; // или pb[i] = pa[i];
Динамические массивы создаются с помощью операции new, при этом необходимо указать тип и размер, например:
int n = 100;
float *p = new float [n];
Преимущество динамических массивов в том, что их размер может быть переменным, то есть нужный объем памяти определяется на этапе выполнения программы. Доступ к элементам динамического массива осуществляется так же, как и к статическим. Например, к 5-му элементу приведенного выше массива можно обратиться как р[5] или *(р+5).
Альтернативный способ создания динамического массива использование функции malloc библиотеки С:
int n = 100;
float *q = (float *) malloc (n * sizeof (float));
Поскольку функция malloc возвращает значение указателя void*, то потребовалось явное преобразование типа.
Память, зарезервированная с помощью new [], должна освобождаться операцией delete[], а память, выделенная функцией malloc - посредством функции free, например:
delete[] p; free (q);
Многомерные массивы задаются указанием каждого измерения в квадратных скобках, например, оператор
int matr [6][8];
объявляет двумерный массив из 6 строк и 8 столбцов. В памяти такой массив располагается в последовательных ячейках построчно. Многомерные массивы размещаются так, что при переходе к следующему элементу быстрее изменяется последний индекс. Для доступа к элементу массива указываются все его индексы, например, matr[i][j], *(matr[i] + j) или *(*(matr + i ) + j).
Количество индексов массива называется его размерностью.
При инициализации многомерного массива он представляется либо как массив из массивов, при этом каждый массив заключается в свои фигурные скобки (в этом случае размеры при описании можно не указывать), либо задается общий список элементов в том порядке, в котором элементы располагаются в памяти:
int mass1 [][] = { {1, 1}, {0, 2}, {1, 0} };
int mass2 [3][2] = {1, 1, 0, 2, 1, 0};
Пример. Программа определяет в целочисленной матрице номер строки, которая содержит наибольшее количество нулевых элементов.
#include <stdio.h>
int main ()
{
const int nstr = 4, nstb = 5; // размеры массива
int b[nstr][nstb]; // описание массива
int i, j:
for (i = 0; i < nstr; i++)
for (j = 0; j < nstb; j++) scanf (“%d”, &b[i][j]); // ввод массива
int istr = -1, MaxKol = 0;
for (i = 0; i < nstr; i++) // просмотр массива по строкам
{
int Kol = 0;
for (j = 0; j < nstb; j++) if (b[i][j] == 0) Kol++; // Можно if (!b[i][j]) …
if (Kol > MaxKol) {istr = i; MaxKol = Kol;}
}
printf (“Исходный массив\n”);
for (i = 0; i < nstr; i++)
{
for (j = 0; j < nstb; j++) printf (“%d “, b[i][j]);
printf ("\n");
}
if (istr == -1) printf ("Нулевых элементов нет");
else printf (“Номер строки: %d”, istr);
return 0;
}
4.6.3. Строки
Строка представляет собой массив символов, заканчивающийся нулем (символом с кодом 0). Нуль-символ при необходимости записывается в виде \0. По расположению нуль-символа определяется фактическая длина строки. Строку можно инициализировать строковым литералом:
char str[10] = “Vasia”; // выделено 10 элементов с номерами от 0 до 9
// первые элементы V, a, s, i, 'а', '\0'
char str[] = “Vasia”; // выделено и заполнено б байтов
Оператор
char *str = “Vasia”;
создает не строковую переменную, а указатель на строковую константу, изменить которую невозможно. Знак равенства перед строковым литералом означает инициализацию, а не присваивание. Операция присваивания одной строки другой не определена (поскольку строка является массивом) и может выполняться с помощью цикла или функций стандартной библиотеки.
Пример. Программа запрашивает пароль не более трех раз.
#include <stdio.h>
#include <string.h>
int main ()
{
char s[10], passw[] = “kuku”; // passw - эталонный пароль,
// можно также написать *passw = “kuku”;
int i, k = 0;
for (i = 0; !k && i < 3; i++)
{
printf (“\nвведите пароль:\n”);
gets (s); // Стандартная функция ввода строки с клавиатуры
if (strstr (s, passw) ) k = 1; // “сравнение” строк (указатель вхождения passw в s)
}
if (k) printf (“\nпароль принят”); else printf (“\nпароль не принят”);
return 0;
}