Связь указателей и массивов
Между указателями и массивами существует прямая связь. Объявление массива, например,
int a[5];
в действительности, помимо резервирования памяти для пяти элементов типа int, определяет также указатель с именем a, значение которого – адрес начального элемента этого массива (&a[0]):
Для доступа к элементам массива используются индексные или адресные выражения, содержащие указатель на данный массив, например, непосредственно указатель a:
*(a+1) = *(a+4)+1; // это то же, что и a[1] = a[4]+1;
Понятно, что, скажем, a+1 указывает на второй по счету элемент этого массива, а a+4 – на последний его элемент.
Так как имя массива означает адрес начала массива, то в следующем фрагменте программы
int a[5];
int *ptr = a;
указатель ptr устанавливается на адрес начального элемента массива a. После этого ptr становится указателем на массив a и его можно использовать для ссылки на элементы этого массива. Поскольку элементы массива расположены в памяти последовательно, увеличение указателя ptr на единицу означает смещение к следующему элементу массива a. Например, следующие два присваивания эквивалентны:
a[1] = 1;
*(ptr+1) = 1;
Константные указатели и указатели на константы
Указатели могут быть объявлены как неизменяемые (константные) или указывающие на неизменяемые (константные) значения. Для этого при объявлении указателя в качестве модификатора используется ключевое слово const, например:
int a, b;
...
const int *xPtr; // указатель на константное значение
int* const iPtr = &a; // константный указатель на значение
const int * const dcPtr = &b; //константный указатель на константное значение
(константные указатели обязательно должны инициализироваться при объявлении, отсутствие инициализирующего адресного значения приведет к ошибке компиляции).
Идентификатор массива является константным указателем на начальный элемент массива, поэтому его значение не может быть изменено.
Для такого рода действий следует использовать переменную-указатель на данные того же типа, что и массив:
int a[25], *p;
p = a;
p++; // это правильно
a++; // а это – нет
Связь указателей и строк
Поскольку строка – разновидность массива, между строками и указателями существует та же взаимосвязь: имя строки – это константный указатель на значение типа char. При инициализации указателей на тип char часто используются строковые константы. Все строковые константы, используемые в программе, размещаются компиляторами C/C++ в так называемой таблице строк, являющейся частью исполнимого файла программы.
Строковая константа – это массив неизменяемых символов (байтов) необходимого размера. Для нее, как и для любого массива, неявно определен указатель (для строковой константы – на тип char), содержащий адрес начального элемента этого массива (строки). Обращение к строковой константе – это обращение к значению этого указателя.
Строковая константа может быть "присвоена" указателю на тип char:
char* s1 = "The string";
В приведенном объявлении указатель на константные символьные данные s1 инициализируется адресом строковой константы "The string".
Следует отметить отличие данного объявления от объявления
char s2[] = "The string";
Первое из приведенных объявлений формирует указатель s1 размером в четыре байта, содержащий адрес строковой константы "The string", второе – массив символов s2 размером в 11 байтов (длина строки "The string" плюс нулевой символ) с соответствующими начальными символьными значениями. Соответственно, различаются и способы работы с объявленными данными: элементы массива s2 можно изменять, а данные, на которые указывает s1, вообще говоря, нет.
Второе объявление можно рассматривать как краткую форму записи объявления
char s2[] = {'T','h','e',' ','s','t','r','i','n','g','\0'};
Стандартная библиотека включает большое количество специальных функций, облегчающих работу со строками и символами.
Символьные функции работают с отдельными символами и требуют включения в программу директивы
#include <ctype.h>
Строковые функции оперируют массивами символов, завершающихся нуль-символом, и в большинстве своём в качестве параметров принимают указатели на тип char. Для использования этих функций в программе нужно подключить их описания с помощью директивы препроцессора
#include <string.h>
Преобразование указателей
Указатель на данные одного типа может быть преобразован в указатель на данные другого типа. Однако при этом следует учитывать, что данные, адресуемые преобразованным указателем, будут интерпретироваться по-новому (может измениться их размер, а, следовательно, и значение). Преобразование указателей выполняется операцией приведения типа:
( тип *) указатель
Например, операторы:
short int i = 0x1010; // Это десятичное 1*4096+ 1*16 = 4112
short *ptr = &i;
short int j = *ptr;
short int k = *(char*)ptr;
приведут к созданию переменной j с начальным значением 0x1010 и переменной k с начальным значением 0x10.
Бестиповые указатели
Бестиповый указатель (указатель на неопределенный тип) позволяет отсрочить определение типа, на который ссылается указатель, или использовать один указатель для обращения к данным разных типов. При объявлении такого указателя вместо типа адресуемых им данных указывается ключевое слово void:
void * имя_указателя ;
Переменная, объявленная как указатель на тип void, может быть использована для работы с данными любого типа. Но для того, чтобы оперировать бестиповым указателем или данными, которые он адресует, необходимо явно указать требуемый тип в каждой операции с таким указателем. Это можно сделать с помощью операции приведения типа:
int a = 125;
double d = 10.3975;
short b[4] = {0, 1, 2, 3};
void *vp;
. . .
vp = &a;
cout << *(int*)vp << endl; // Вывод значения переменной a
vp = &d;
cout << *(double*)vp << endl; // Вывод значения переменной d
vp = b;
cout << *((short*)vp+1) << endl; // Вывод значения b[1]
vp = (short*)vp+2; // Изменение значения vp
cout << *(short*)vp << endl; // Вывод значения b[2]
Переменная vp объявлена как бестиповый указатель, поэтому ей можно присвоить адрес переменной любого типа. Однако ни одна операция не может быть выполнена над бестиповым указателем до тех пор, пока не будет явно задан тип данных, на который он указывает. В примере указатель vp с помощью операции приведения типа преобразуется к типам int*, double* и short*.
Массив указателей
Указатели, как и любые другие данные одного и того же типа, могут объединяться в массивы. Массивы указателей можно использовать для работы со всеми типами данных, но целесообразнее применять их для хранения символьных строк.
Объявление
char *ch[10];
означает, что ch – одномерный массив из 10 элементов, являющихся указателями на данные типа char.
Определить и инициализировать массив строк (массив указателей на данные типа char) можно, например, с помощью объявления
char* s[5] = { "one", "two", "three", "four", "five" };
или равнозначного ему
char* s[] = { "one", "two", "three", "four", "five" };
Оба приведенных в примере объявления создают массив указателей s и инициализируют его элементы адресами константных строк. Так как s – массив, указатель s будет содержать адрес начального элемента этого массива, в данном случае – указателя s[0], а каждый из указателей s[0], … , s[4] – адрес начала соответствующей строки. Соответственно, *s, как и s[0], будет означать адрес начала первой по счету строки массива s, а s[0][0], *s[0] (приоритет операции [] выше, чем приоритет операции *) и **s – значение ее начального символа.
Указатели на указатели
Поскольку массив в С++ реализован как указатель на его элементы, то массив указателей неявно определяет новый производный тип данных – указатель на указатель.
Объявление
int **p;
говорит о том, что p – указатель на указатель, то есть содержит адрес другого указателя, а операция *, дважды примененная к указателю p, даст значение типа int. Понятно, что можно определить указатель на указатель на указатель и т.д., то есть переменные с адресами могут образовывать некоторую иерархию (быть многоуровневыми). Например,
int ***v;
определяет указатель на указатель на указатель.