Семинар №10

Указатели в C++ и их связь с массивами

Указатель — предназначен для хранения адреса некоторого объекта (например, переменной) определённого типа.

Общая схема объявления указателя:

type* name; // объявили указатель с именем name на объект типа type
type *another; // объявили указатель на объект того же типа, звёздочку можно ставить и перед именем указателя

Примеры:

int* p;
int *p2;
double* pd;

Указателям p и p2 можно будет присвоить адреса переменных типа int, но нельзя будет присвоить адреса переменных другого типа или объектов какого-нибудь класса. Аналогично, указателю pd можно будет присвоить только адреса переменных типа double.

Для взятия адреса — используется оператор &, размещаемый перед объектом, адрес которого хочется получить.

Примеры:

int a = 15;
cout << &a << endl; // вывели адрес переменной a в памяти (не её значение), увидим шестнадцатиричное число
int ar[] = {728, 3, 402, -1};
/*
  Далее выведем адреса элементов массива. Они будут отличаться на размер в байтах
  базового типа массива (в данном случае, int). Ещё раз убедимся, что в памяти
  все элементы массива расположены последовательно друг за другом.
*/   
for (int i=0; i<=3; i++) {
  cout << &ar[i] << ' ';
}
cout lt;< endl;
int* p;
p = &a; // скопировали адрес переменной a в указатель p
cout << p << endl; // вывели адрес, хранимый в указателе (совпадёт с ранее виденным адресом)
cout << &p << endl; // вывели адрес самого указателя (он же тоже хранится где-то в памяти, потому имеет адрес)

Для перехода по известному адресу — используется оператор *, размещаемый перед адресом или указателем хранящем адрес. Под переходом по адресу понимается, что от адреса мы переходим к действиям над значением, хранимом по данному адресу. Это операция называется иногда разыменованием.

int a = 15; // переменная a со значением
int* p = &a; // указатель с адресом переменной a
cout << *p << endl; // увидим 15, т.е. значение переменной

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

Справедливо тождество: выражение a == *(&a), где a любого типа — всегда истинно.

При создании любого массива в C++, вместе с ним естественным образом создаётся указатель. Имя этого указателя совпадает с именем массива. Тип этого указателя — «указатель на базовый тип массива». В появившемся указателе хранится адрес начального элемента массива. Чтобы начало массива не было потеряно этот указатель является контсантным, т.е. его нельзя направить на какой-то другой элемент массива или записать туда адрес другой переменной даже подходящего типа. Но зато можно скопировать этот адрес в какой-то другой указатель, не являющимся контсантным.

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

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

int ar[] = {-72, 3, 402, -1, 55, 132};
int* p = ar;
for (int i=101; i<=106; i++) {
 cout << *p << ' ';
 p++;
}

Над множеством указателей в C++ определён ряд операций:

Кроме того, указатели можно сравнивать с помощью операторов сравнения, которые рассматривались для числовых типов. При этом, большим считается тот указатель, который направлен на элемент, расположенный в памяти далее (т. е. правее). Указатель хранит адрес элемента, то есть некоторое целое положительное число. Соответственно, больше будет тот элемент, которых хранит численно больший адрес. Равными считаются указатели, направленные на один и тот же элемент.

Константный указатель нельзя перемещать (записывать в него другой адрес), но можно его разыменовывать или делать его участников вышеперечисленных операций:

int ar[] = {-72, 3, 402, -1, 55, 132};
cout << *ar; // -72
int* p = ar+3; // указатель на 4-ый по счёту элемент массива (со значением -1)
p--; // переместили указатель влево на 1 элемент
cout << *p; // выведется 402

Справедливо тождество: выражение a[i] == *(a+i), где a указатель на массив любого типа и i допустимый индекс этого массива — всегда истинно.

Символьные массивы (строки)

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

Это позволяет нам отправлять на вывод символьный массив, не передавая методу cout его размера.

Признаком окончания вывода является символ нуля-терминатора ('\0').

Метод cin в качестве своего аргумента также может принимать указатель на символьный массив, куда будет записана строка введённая с клавиатуры, при этом, за последним элементом строки метод cin автоматически разместит нуль-терминатор. Более того, при задании строковой константы (когда строка явно задаётся в двойных кавычках в коде программы), вслед за последним символом в строке также автоматически размещается нуль-терминатор. Соответственно, длина строки будет на один символ больше, чем мы явно укажем.

char str[] = "Privet"; // в массиве 7 элементов: 6 латинских букв и нуль-терминатор
cout << sizeof(str) << endl; // 7
cout << str << endl; // Privet - вывелась вся строка
*(str+3) = '\0'; // вместо 'v' записали нуль-терминатор в массив
cout << str << endl; // Pri - вывелась часть строки до нуль-терминатора

Обойти строку, обратившись по отдельности к каждому её символу, можно примерно так (в данном случае мы выведем на отдельной строке каждый символ и его код по символьной таблице):

char str[] = "Privet";
char* p = str;
while(*p != '\0') {
    cout << *p << ' ' << (short) *p << endl;
    p++;
}

Этого же результата можно было добиться более изящно (но менее понятно), совместив ряд операций (разыменование, инкремент, автопреведение к bool) в одной строке:

char str[] = "Privet";
char* p = str - 1;
while(*p++) {
    cout << *p << ' ' << (short) *p << endl;
}

Задачи

  1. Написать программу, создающую массив из 10 случайных целых чисел из отрезка [-50;50]. Вывести на экран весь массив и на отдельной строке — значение минимального элемента массива.

    Для обхода массива использовать указатели (запрещено обращаться к элементам массива по индексам).

  2. Написать программу, которая (без использования библиотечных функций для обработки строк), копировала бы строку введённую пользователем с клавиатуры в новую (максимальная длина строки — 80 символов). При этом в процессе копирования должны отбрасываться все незначащие пробелы в начале и конце строки, а также несколько подряд идущих пробелов должны заменяться на один. Вывести исходную и новую строки на экран.

    Для обхода строк использовать указатели.

  3. Написать программу, которая для введённой с клавиатуры строки (максимальная длина строки — 80 символов) сообщает, какая цифра в ней встречается чаще всего, либо сообщает, что цифры в строке совсем отсутствуют. Если с одинаковой частотой в строке встретилось несколько цифр, то в качестве лидера вывести любую из подходящих цифр. Для обхода строк использовать указатели.

  4. Для введённой пользователем с клавиатуры строки (максимальная длина строки — 80 символов) программа должна определить, корректно ли расставлены скобки (круглые, фигурные, квадратные) или нет. Перемешивание скобок (пример: «{[}]») считается некорректным вариантом.

← К списку семинаров