В этой статье будет рассказано:
- Указатели, массивы и арифметика указателей
- Замечания по программе
- Адрес массива
- Объявление указателей
- Присваивание значений указателям
- Разыменование указателей
- Различие между указателем и указываемым значением
- Арифметика указателей
- Имена массивов
Указатели, массивы и арифметика указателей
Родство указателей и имен массивов происходит из арифметики указателей, а также того, как язык C++ внутренне работает с массивами. Сначала рассмотрим арифметику указателей. Добавление единицы к целочисленной переменной увеличивает ее значение на единицу, но добавление единицы к переменной типа указателя увеличивает ее
значение на количество байт, составляющих размер типа, на который она указывает. Добавление единицы к указателю на double добавляет 8 байт к числовой величине указателя на системах с 8-байтным double, в то время как добавление единицы к указателю на short добавляет к его значению 2 байта. Код ниже доказывает истинность этого утверждения. Он также демонстрирует еще один важный момент: C++ интерпретирует имена массивов как адреса.
*знак решентки*include <iostream>
int main()
{
using namespace std;
double wages[3] = {10000.0, 20000.0, 30000.0};
short stacks[3] = {3, 2, 1};
// Два способа получить адрес массива
double * pw = wages; // имя массива равно адресу
short * ps = &stacks[0]; // либо использование операции взятия адреса
//с элементом массива
cout « "pw = " « pw « ", *pw = " « *pw << endl;
pw = pw + 1;
cout << "add 1 to the pw pointer: \n"; // добавление 1 к указателю pw
cout « "pw = " « pw « ", *pw = " « *pw « "\n\n" ;
cout « "ps = " « ps « ", *ps = " « *ps « endl;
ps = ps + 1;
cout « "add 1 to the ps pointer: \n"; // добавление 1 к указателю ps
cout « "ps = " « ps « ", *ps = " « *ps « "\n\n";
// Доступ к двум элементам с помощью нотации массивов
cout « "access two elements with array notation\n";
cout « "stacks[0] = " « stacks[0]
«", stacks[l] = " « stacks[l] << endl;
// Доступ к двум элементам с помощью нотации указателей
cout << "access two elements with pointer notation\n";
cout << "*stacks = " « *stacks
« ", * (stacks + 1) = " « * (stacks + 1) « endl;
cout « sizeof (wages) « " = size of wages array\n"; // размер массива wages
cout « sizeof (pw) « " = size of pw pointer\n"; // размер указателя pw
return 0;
}
Результат
pw = 0x28ccf0, *pw = 10000
add 1 to the pw pointer:
pw = 0x28ccf8, *pw = 20000
ps = 0x28ccea, *ps = 3
add 1 to the ps pointer:
ps = 0x28ccec, *ps = 2
access two elements with array notation
stacks[0] = 3, stacks[l] = 2
access two elements with pointer notation
*stacks = 3, *(stacks + 1) = 2
24 = size of wages array
4 = size of pw pointer
Замечания по программе
В большинстве контекстов C++ интерпретирует имя массива как адрес его
первого элемента. Таким образом, следующий оператор создает pw как указатель на тип double, затем инициализирует его wages, который также является адресом первого элемента массива wages:
double * pw = wages;
Для wages, как и любого другого массива, справедливо следующее утверждение:
wages = &wages[0] = адрес первого элемента массива
Чтобы доказать, что это так, программа явно использует операцию взятия адреса в выражении &stacks [0] для инициализации указателя ps адресом первого элемента массива stacks.
Далее программа инспектирует значения pw и *pw. Первое из них представляет адрес, а второе — значение, расположенное по этому адресу. Поскольку pw указывает на первый элемент, значение, отображаемое *pw, и будет значением первого элемента — 10000. Затем программа прибавляет единицу к pw. Как и ожидалось, это добавляет 8 (fd24 + 8 = f 62с в шестнадцатеричном виде) к числовому значению адреса,
потому что double в этой системе занимает 8 байт. Это присваивает pw адрес второго элемента. Таким образом, теперь *pw равно 20000, т.е. значение второго элемента (Значения адресов на рисунке подкорректированы для ясности.) После этого программа выполняет аналогичные шаги для ps. На этот раз, поскольку ps указывает на тип short, а размер значений short составляет 2 байтам, добавление 1 к этому указателю увеличивает его значение на 2 (0х28ссеа + 2 = 0х28ссес
в шестнадцатеричной форме). Опять-таки, в результате указатель устанавливается на следующий элемент массива.
На заметку!
Добавление единицы к переменной указателя увеличивает его значение на количество байт, представляющее размер типа, на который он указывает.
Теперь рассмотрим выражение stacks [1]. Компилятор C++ трактует это
выражение точно так же, как если бы вы написали * (stacks + 1). Второе выражение означает вычисление адреса второго элемента массива, и затем извлечение значения, сохраненного в нем. Конечный результат — значение stacks [1]. (Приоритет операций требует применения скобок. Без них значение 1 было бы добавлено к * stacks вместо stacks.)
double wages[3] = {10000.0, 20000.0, 30000.0};
short stacks[3] = {3, 2, 1}
double * pw = wages;
short * ps = &stacks[0];
Вывод программы демонстрирует, что * (stacks + 1) и stacks [1] — это одно и то же. Аналогично * (stacks + 1) эквивалентно stacks [2]. В общем случае, всякий раз, когда вы используете нотацию массивов, C++ выполняет следующее преобразование:
имя_массива[і] превращается в * {имя_массива + і)
И если вы используете указатель вместо имени массива, C++ осуществляет то же самое преобразование:
имя_указателя[і] превращается в * {имя_указателя + і)
Таким образом, во многих отношениях имена указателей и имена массивов можно использовать одинаковым образом. Нотация квадратных скобок применима и там, и там. К обоим можно применять операцию разыменования (*). В большинстве выражений каждое имя представляет адрес. Единственное отличие состоит в том, что значение указателя изменить можно, а имя массива является константой:
имя_указателя = имя_указателя + 1; // правильно
имя массива = имя массива + 1; // не допускается
Второе отличие заключается в том, что применение операции sizeof к имени массива возвращает размер массива в байтах, но применение sizeof к указателю возвращает размер указателя, даже если он указывает на массив. Например, в программе выше как pw, так и wages ссылаются на один и тот же массив. Однако применение операции sizeof к ним порождает разные результаты:
24 = размер массива wages <- отображение sizeof для wages
4 = размер указателя pw <- отображение sizeof для pw
Это один из случаев, когда C++ не интерпретирует имя массива как адрес.
Адрес массива
Получение адреса массива является другим случаем, при котором имя массива не интерпретируется как его адрес. Но подождите, разве имя массива не интерпретируется как адрес массива? Не совсем — имя массива интерпретируется как адрес первого элемента массива, в то время как применение операции взятия адреса приводит к выдаче адреса
целого массива:
short tell [10]; // создание массива из 20 байт
cout « tell « endl; // отображение &tell[0]
cout « &tell « endl; // отображение адреса целого массива
С точки зрения числового представления эти два адреса одинаковы, но
концептуально &tell[0] и, следовательно, tell — это адрес 2-байтного блока памяти, тогда как &tell — адрес 20-байтного блока памяти. Таким образом, выражение tell + 1 приводит к добавлению 2 к значению адреса, а & tell + 1 — к добавлению 20 к значению адреса. Это можно выразить и по-другому: tell имеет тип "указатель на short", или short *, а &tell — тип "указатель на массив из 20 элементов short", или short (*) [20].
Теперь вас может заинтересовать происхождение последнего описания типа. Сначала посмотрим, как можно объявить и инициализировать указатель этого типа:
short (*pas) [20] = &tell; // pas указывает на массив из 20 элементов short
Если опустить круглые скобки, то правила приоритетов будут ассоциировать [20] в первую очередь с pas, делая pas массивом из 20 указателей на short, поэтому круглые скобки необходимы. Далее, если вы хотите описать тип переменной, вы можете воспользоваться
объявлением этой переменной в качестве руководства и удалить имя переменной. Таким образом, типом pas является short (*) [20]. Кроме того, обратите внимание, что поскольку значение pas установлено в &tell, *pas эквивалентно tell, и (*pas) [0] будет первым элементом массива tell.
Говоря кратко, использовать new для создания массива и применять указатели для доступа к его различным элементам очень просто. Вы просто трактуете указатель как имя массива. Однако понять, почему это работает — интересная задача. Если вы действительно хотите понимать массивы и указатели, то должны тщательно исследовать их поведение, связанное с изменчивостью.
Объявление указателей
Чтобы объявить указатель на определенный тип, нужно использовать следующую форму:
имяТипа * имяУказателя;
Вот некоторые примеры:
double * pn; // pn может указывать на значение double
char * pc; // pc может указывать на значение char
Здесь pn и pc — указатели, a double * и char* — нотация C++ для представления указателя на double и указателя на char.
Присваивание значений указателям
Указателям должны быть присвоены адреса памяти. Можно применить операцию & к имени переменной, чтобы получить адрес именованной области памяти, либо операцию new, которая возвращает адрес неименованной памяти. Вот некоторые примеры:
double * pn; // pn может указывать на значение double
double * pa; // так же и pa
char * pc; // pc может указывать на значение char
double bubble = 3.2;
pn = &bubble; // присваивание адреса bubble переменной pn
pc = new char; // присваивание адреса выделенной памяти char
// переменной pc
pa = new double [30] ; // присваивание адреса массива из 30 double переменной pa
Разыменование указателей
Разыменование указателя — это получение значения, на которое он указывает. Для этого к указателю применяется операция разыменования (*). То есть, если pn — указатель на bubble, как в предыдущем примере, то *pn — значение, на которое он указывает, в данном случае — 3.2. Вот некоторые примеры:
cout « *pn; // вывод значения bubble
*pc = 'S'; // помещение 'S' в область памяти, на которую указывает pc
Нотация массивов — второй способ разыменования указателя; например, pn [ 0 ] — это то же самое, что и *pn. Никогда не следует разыменовывать указатель, который не был инициализирован правильным адресом.
Различие между указателем и указываемым значением
Помните, если pt — указатель на int, то *pt — не указатель на int, а полный
эквивалент переменной типа int. Указателем является просто pt.
Вот некоторые примеры:
int * pt = new int; // присваивание адреса переменной pt
*pt = 5; // сохранение 5 по этому адресу
Имена массивов
В большинстве контекстов C++ трактует имя массива как эквивалент адреса его первого элемента.
Вот пример:
int tacos[10]; // теперь tacos — то же самое, что и &tacos[0]
Одно исключение из этого правила связано с применением операции sizeof к имени массива. В этом случае sizeof возвращает размер всего массива в байтах.
Арифметика указателей
C++ позволяет добавлять целые числа к указателю. Результат добавления к
указателю единицы равен исходному адресу плюс значение, эквивалентное количеству байт в указываемом объекте. Можно также вычесть один указатель из другого, чтобы получить разницу между двумя указателями. Последняя операция, которая возвращает целочисленное значение, имеет смысл только в случае, когда два указателя указывают на элементы одного и того же массива (указание одной из позиций за границей массива также допускается); при этом результат означает расстояние между элементами массива. Ниже приведен ряд примеров:
int tacos[10] = {5,2,8,4,1,2,2,4,6,8};
int * pt = tacos; // предположим, что pf и tacos указывают на адрес 3000
pt = pt + 1; // теперь pt равно 3004, если int имеет размер 4 байта
int *ре = &tacos[9]; // ре равно 3036, если int имеет размер 4 байта
ре = ре - 1; // теперь ре равно 30 32 - адресу элемента tacos [8]
int diff = ре - pt; // diff равно 7, т.е. расстоянию между tacos [8] и tacos [1]
Конец 1 части
Спасибо, если вы прочитали эту статью. Надеюсь вы что-то новое узнали для себя, и конечно же поняли. Подпишитесь, поставьте лайк, напишите комментарий, поддержите меня. Хотелось бы увидеть как улучшать статьи и чего не хватает. Буду анализировать и улучшать контент. Еще раз спасибо, до свидания! :)
#c++ #компиляторы #программирование #разработка #указатели #ооп