Занятие 7 и 8. Базовые понятия об указателях в C++ и их связи с массивами. Символьные массивы (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 << endl;
int* p;
p = &a; // скопировали адрес переменной a в указатель p
cout << p << endl; // вывели адрес, хранимый в указателе (совпадёт с ранее виденным адресом)
cout << &p << endl; // вывели адрес самого указателя (он же тоже хранится где-то в памяти, потому имеет адрес)

Вывод мог бы быть примерно таким:

0x22ff08
0x22fef8 0x22fefc 0x22ff00 0x22ff04
0x22ff08
0x22fef4

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

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

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

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

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

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

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

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

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

  • p+n, где p — указатель, n — целое положительное число. Результат — некоторый указатель, полученный смещением p на n позиций вправо.
  • p-n, где p — указатель, n — целое положительное число. Результат — некоторый указатель, полученный смещением p на n позиций влево.
  • p-q, где p и q — указатели на один и тот же тип. Результат — целое число, равное количеству шагов, на которое нужно сместить q вправо, чтобы он достиг указателя p, также этот результат можно называть “расстоянием” между указателями, оно может быть и отрицательным, если элемент, на который направлен указатель q расположен правее (то есть, далее), чем элемент, на который направлен указатель p.
  • p++ (инкремент), p-- (декремент), где p — указатель. Операции эквивалентны действиям p=p+1 и p=p-1, соответственно.

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

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

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 допустимый индекс этого массива. Пользуясь этим тождеством легко переходить от индексов к указателям и обратно при работе с массивом.

Константные указатели и указатели на константы

В C++ существуют специальные виды указателей:

Константный указатель -- не может быть перенаправлен на другой адрес. Хранимый в нём адрес задаётся при инициализации и далее не может быть изменён. Подобный указатель автоматически создаётся при объявлении массива и всегда остаётся направленным на его первый элемент. Но может быть объявлен и явно:

int a = 5, b = -5;
const int* p = &a;
*p = 15; // разименовывать можно
p = &b; //   ошибка, перемещать нельзя

Указатель на константу -- может быть перенаправлен на другой адрес, но не допускает разименование. Используется для того, чтобы хранить адреса констант (C++ не позволяет направить на константу обычный указатель). Но кроме этого может быть направлен и на обычную переменную (хотя практического смысла в такой возможности нет).

int a = 5;
const int b = -5;
const int* pc; // создали константный указатель
pc = &b; // направили на константу
*pc = 15; // ошибка, разименовывать нельзя
pc = &a; // можно направить на переменную
*pc = 15; // но разименовывать всё равно нельзя

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

const int a = 5;
const int* const cpc; // создали константный указатель на константу
const int b = 117;
cpc = &b; // ошибка, нельзя перенаправить
*cpc = 15; // ошибка, разименовывать тоже нельзя
Название Объявление Можно перенаправлять Можно разименовывать
Указатель
int* p;
да да
Константный указатель
const int* p = &a;
нет да
Указатель на константу
int* const p;
да нет
Константный указатель на константу
const int* const p = &a;
нет нет

Пример, в котором встречаются все виды указателей:

#include <iostream>
using namespace std;
int main () {
	int s = 5;
	int v = 3;
	const int z = 7;
	int* ps = &s;
	const int* pcz = &z;
	int* const cpv = &v;
	const int* const cpcz = &z; 
	//cpv = &s; // ошибка, нельзя перенаправить константный указатель
	//*pcz = 9; // ошибка, нельзя разименовать указатель на константу
	cout << *ps << endl; // 5
	cout << *pcz << endl; // 7
	cout << *cpv << endl; // 3
	cout << *cpcz << endl; // 7
}

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

При объявлении символьного массива (типа char), даже в том случае, если мы не производим его инициализации и он является локальным, все элементы массива по-умолчанию получают значение нулей-терминаторов (символов с ASCII-кодом 0). При этом, метод 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;
}

Вывод будет таким:

P 80
r 114
i 105
v 118
e 101
t 116

Пример программы, которая подсчитает и выведет на экран количество символов в строке, введённой пользователем с клавиатуры:

char str[100];
cout << "Vvedite stroku: ";
cin.getline(str,100);
int count = 0;
char* p = str;
while(*p != '\0') {
    p++;
    count++;
}
cout << "Kol-vo simvolov: " << count << endl;

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

Задачи

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

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

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

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

  4. Написать программу, изменяющую в строке не длиннее 80 символов, введённой пользователем с клавиатуры, все заглавные латинские буквы на строчные.

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

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

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

    Если с одинаковой частотой в строке встретилось несколько цифр, то в качестве лидера вывести любую из подходящих цифр. Для обхода строк использовать указатели.

  7. Для введённой пользователем с клавиатуры строки (максимальная длина строки — 80 символов) программа должна определить, корректно ли расставлены круглые скобки или нет. То есть в строке не должна встречаться закрывающая круглая скобка раньше, чем соответствующая ей закрывающей. Примеры корректных строк: (), (a+b+(a-c)). Пример некорректной строки: ((a+b), )a+b(, (a+b+(c-a)).
  8. Для введённой пользователем с клавиатуры строки (максимальная длина строки — 80 символов) программа должна определить, корректно ли расставлены круглые, фигурные и квадратные скобки или нет. Перемешивание скобок (пример: «{[}]») считается некорректным вариантом.