Тип объекта, на который указывает void*, неизвестен, и мы не можем манипулировать этим объектом. Все, что мы можем сделать с таким указателем, – присвоить его значение другому указателю или сравнить с какой-либо адресной величиной. (Более подробно мы расскажем об указателе типа void в разделе 4.14.)
Для того чтобы обратиться к объекту, имея его адрес, нужно применить операцию разыменования, или косвенную адресацию, обозначаемую звездочкой (*). Имея следующие определения переменных:
int ival = 1024;, ival2 = 2048;
int *pi = ival;
мы можем читать и сохранять значение ival, применяя операцию разыменования к указателю pi:
// косвенное присваивание переменной ival значения ival2
*pi = ival2;
// косвенное использование переменной ival как rvalue и lvalue
*pi = abs(*pi); // ival = abs(ival);
*pi = *pi + 1; // ival = ival + 1;
Когда мы применяем операцию взятия адреса () к объекту типа int, то получаем результат типа int*
int *pi = ival;
Если ту же операцию применить к объекту типа int* (указатель на int), мы получим указатель на указатель на int, т.е. int**. int** – это адрес объекта, который содержит адрес объекта типа int. Разыменовывая ppi, мы получаем объект типа int*, содержащий адрес ival. Чтобы получить сам объект ival, операцию разыменования к ppi необходимо применить дважды.
int **ppi = pi;
int *pi2 = *ppi;
cout "Значение ival\n"
"явное значение: " ival "\n"
"косвенная адресация: " *pi "\n"
"дважды косвенная адресация: " **ppi "\n"
endl;
Указатели могут быть использованы в арифметических выражениях. Обратите внимание на следующий пример, где два выражения производят совершенно различные действия:
int i, j, k;
int *pi = i;
// i = i + 2
*pi = *pi + 2;
// увеличение адреса, содержащегося в pi, на 2
pi = pi + 2;
К указателю можно прибавлять целое значение, можно также вычитать из него. Прибавление к указателю 1 увеличивает содержащееся в нем значение на размер области памяти, отводимой объекту соответствующего типа. Если тип char занимает 1 байт, int – 4 и double – 8, то прибавление 2 к указателям на char, int и double увеличит их значение соответственно на 2, 8 и 16. Как это можно интерпретировать? Если объекты одного типа расположены в памяти друг за другом, то увеличение указателя на 1 приведет к тому, что он будет указывать на следующий объект. Поэтому арифметические действия с указателями чаще всего применяются при обработке массивов; в любых других случаях они вряд ли оправданы.
Вот как выглядит типичный пример использования адресной арифметики при переборе элементов массива с помощью итератора:
int ia[10];
int *iter = ia[0];
int *iter_end = ia[10];
while (iter != iter_end) {
do_something_with_value (*iter);
++iter;
}
Даны определения переменных:
int ival = 1024, ival2 = 2048;
int *pi1 = ival, *pi2 = ival2, **pi3 = 0;
Что происходит при выполнении нижеследующих операций присваивания? Допущены ли в данных примерах ошибки?
(a) ival = *pi3; (e) pi1 = *pi3;
(b) *pi2 = *pi3; (f) ival = *pi1;
(c) ival = pi2; (g) pi1 = ival;
(d) pi2 = *pi1; (h) pi3 = pi2;
Работа с указателями – один из важнейших аспектов С и С++, однако в ней легко допустить ошибку. Например, код
pi = ival;
pi = pi + 1024;
почти наверняка приведет к тому, что pi будет указывать на случайную область памяти. Что делает этот оператор присваивания и в каком случае он не приведет к ошибке?
Данная программа содержит ошибку, связанную с неправильным использованием указателей:
int foobar(int *pi) {
*pi = 1024;
return *pi;
}
int main() {
int *pi2 = 0;
int ival = foobar(pi2);
return 0;
}
В чем состоит ошибка? Как можно ее исправить?
Ошибки из предыдущих двух упражнений проявляются и приводят к фатальным последствиям из-за отсутствия в С++ проверки правильности значений указателей во время работы программы. Как вы думаете, почему такая проверка не была реализована? Можете ли вы предложить некоторые общие рекомендации для того, чтобы работа с указателями была более безопасной?
3.4. Строковые типы