В языке D указатели изучать легко и интересно. Некоторые задачи программирования выполняются проще с помощью указателей, а другие задачи, такие как распределение динамической памяти, не могут выполняться без них. Ниже показан простой указатель.
Вместо прямого указания на переменную, указатель указывает на адрес переменной. Как вы знаете, каждая переменная является местом в памяти, и каждая ячейка памяти имеет свой адрес, который можно получить, используя оператор ampersand (&), который возвращает адрес в памяти. Рассмотрим следующую программу, которая печатает адрес объявленных переменных:
import std.stdio; void main () { int var1; writeln("Адрес переменной var1: ",&var1); char var2[10]; writeln("Адрес переменной var2: ",&var2); }
Когда вы скомпилируете и выполните эту программу, она возвратит результат, похожий на это:
Адрес переменной var1: 7FFE686BBB28 Адрес переменной var2: 7FFE686BBB30
Указатель (pointer) – это переменная, значением которой является адрес другой переменной. Как и любую другую переменную или константу, вы должны объявить указатель, прежде чем сможете с ним работать. Общая форма объявления переменной указателя:
тип *имя_указателя;
Здесь тип – это базовый тип, на который будет указывать указатель; Он должен быть допустимым типом программирования, а имя_указателя – это имя переменной указателя. Звездочкой, которую вы использовали для объявления указателя, является та же самая звездочка, которую вы используете для умножения. Однако, в этом выражении звездочка используется для обозначения переменной как указателя. Ниже приведены допустимые объявления указателей:
int *ip; // указатель на integer double *dp; // указатель на double float *fp; // указатель на float char *ch // указатель на символ
Фактический тип данных у всех указателей, указывают ли они на integer, float, char или на что-то другое – один и тот же, длинное шестнадцатеричное число, которое представляет собой адрес в памяти. Единственное различие между указателями разных типов данных – это тип данных переменной или константы, на которую указывает указатель.
Существует несколько важных операций, которые мы часто используем с указателями.
Определить переменные указателя
Назначить адрес переменной указателю
Наконец, получить доступ к значению по адресу, доступному из переменной указателя.
Это делается с помощью унарного оператора *, который возвращает значение переменной, расположенной по адресу, указанному его операндом. В следующем примере используются эти операции:
import std.stdio; void main () { int var = 20; // фактическое объявление переменной. int *ip; // переменная указателя ip = &var; // сохранить адрес переменной var в переменной указателя writeln("Значение переменной var: ", var); writeln("Адрес, сохранённый в переменной ip: ", ip); writeln("Значение переменной *ip: ", *ip); }
Когда вы скомпилируете и выполните эту программу, она возвратит результат, похожий на это:
Значение переменной var: 20 Адрес, сохранённый в переменной ip: 7FFEBEA77270 Значение переменной *ip: 20
Всегда полезно назначить указатель null переменной указателя, если у вас нет точного адреса, который нужно назначить. Это делается во время объявления переменной. Указатель, которому присвоен null, называется нулевым указателем.
Указатель null - это константа с нулевым значением, определённая в нескольких стандартных библиотеках, включая iostream. Рассмотрим следующую программу:
import std.stdio; void main () { int *ptr = null; writeln("Значение ptr равно " , ptr) ; }
Когда вы скомпилируете и выполните эту программу, она возвратит следующий результат:
Значение ptr равно null
В большинстве операционных систем программам не разрешается обращаться к памяти по адресу 0, поскольку эта память зарезервирована операционной системой. Однако, нулевой адрес памяти имеет особое значение; он означает, что указателю не назначена никакая доступная ячейка памяти.
По соглашению, если указатель содержит значение null (ноль), предполагается, что он ни на что не указывает. Чтобы проверить указатель, содержится ли в нём null, вы можете использовать оператор if следующим образом:
if(ptr) // успешно, если p не равен null if(!ptr) // успешно, если p равен null
Таким образом, если для всех неиспользуемых указателей задано значение null, и вы избегаете использования нулевого указателя, вы можете избежать случайного ошибочного применения неинициализированного указателя. Довольно часто неинициализированные переменные могут содержать какие-то мусорные значения, и это затрудняет отладку программы.
Существует четыре арифметических оператора, которые могут применяться для указателей: ++, --, + и -
Чтобы понять арифметику указателей, рассмотрим указатель на целое с именем ptr, который указывает на адрес 1000. Предполагая, что целые числа являются 32-битными, давайте выполним следующую арифметическую операцию над указателем:
ptr++
теперь ptr будет указывать на адрес 1004, потому что каждый раз, когда ptr увеличивается, он указывает на следующее целое число. Эта операция перемещает указатель к следующей ячейке памяти, не влияя на фактическое значение в ячейке памяти.
Если ptr указывает на символ (тип char), адрес которого равен 1000, то приведённая выше операция перенаправит его к адресу 1001, потому что следующий символ будет доступен в ячейке 1001.
Мы предпочитаем использовать указатель в нашей программе вместо массива, потому что переменную указателя можно увеличить, в отличие от имени массива, которое увеличить невозможно, поскольку оно является постоянным указателем. Следующая программа увеличивает указатель переменной для доступа к каждому последующему элементу массива:
import std.stdio; const int MAX = 3; void main () { int var[MAX] = [10, 100, 200]; int *ptr = &var[0]; for (int i = 0; i < MAX; i++, ptr++) { writeln("Адрес var[" , i , "] = ", ptr); writeln("Значение var[" , i , "] = ", *ptr); } }
Когда вы скомпилируете и выполните эту программу, она возвратит результат, похожий на это:
Адрес var[0] = 7FFD2870CCD0 Значение var[0] = 10 Адрес var[1] = 7FFD2870CCD4 Значение var[1] = 100 Адрес var[2] = 7FFD2870CCD8 Значение var[2] = 200
Указатели и массивы сильно связаны. Однако они не являются полностью взаимозаменяемыми. Например, рассмотрим следующую программу:
import std.stdio; const int MAX = 3; void main () { int var[MAX] = [10, 100, 200]; int *ptr = &var[0]; var.ptr[2] = 290; ptr[0] = 220; for (int i = 0; i < MAX; i++, ptr++) { writeln("Адрес var[" , i , "] = ", ptr); writeln("Значение var[" , i , "] = ", *ptr); } }
В приведенной выше программе вы можете увидеть выражение var.ptr[2], используемое для присвоения значения второму элементу, и ptr[0], которое используется для присвоения нулевому элементу. Оператор приращения может использоваться с ptr, но не с var.
Когда вы скомпилируете и выполните эту программу, она возвратит результат, похожий на это:
Адрес var[0] = 7FFC03A8AEA0 Значение var[0] = 220 Адрес var[1] = 7FFC03A8AEA4 Значение var[1] = 100 Адрес var[2] = 7FFC03A8AEA8 Значение var[2] = 290
Указатель на указатель представляет собой форму многократной косвенности или цепочки указателей. Обычно указатель содержит адрес переменной. Когда мы определяем указатель на указатель, первый указатель содержит адрес второго указателя, который, в свою очередь, указывает на местоположение, содержащее фактическое значение, как показано ниже.
Переменная, являющаяся указателем на указатель, должна быть объявлена соответствующим образом. Это делается путём размещения дополнительной звездочки перед её именем. Например, вот так выглядит синтаксис объявления указателя на указатель типа int:
int **var;
Когда к целевому значению применяется косвенный доступ через указатель на указатель, тогда для доступа к этому значению требуется, чтобы оператор звёздочки применялся дважды, как показано ниже в примере:
import std.stdio; const int MAX = 3; void main () { int var = 3000; writeln("Значение переменной var : ", var); int *ptr = &var; writeln("Значение, доступное как *ptr : ", *ptr); int **pptr = &ptr; writeln("Значение, доступное как **pptr : ", **pptr); }
Когда вы скомпилируете и выполните эту программу, она возвратит следующий результат:
Значение переменной var : 3000 Значение, доступное как *ptr : 3000 Значение, доступное как **pptr : 3000
D позволяет вам передать указатель в функцию. Для этого просто объявите параметр функции с типом указателя.
В следующем простом примере указатель передаётся в функцию.
import std.stdio; void main () { // массив из пяти элементов целого типа. int balance[5] = [1000, 2, 3, 17, 50]; double avg; avg = getAverage( &balance[0], 5 ) ; writeln("Среднее арифметическое равно : " , avg); } double getAverage(int *arr, int size) { int i; double avg, sum = 0; for (i = 0; i < size; ++i) { sum += arr[i]; } avg = sum/size; return avg; }
Когда вы скомпилируете и выполните эту программу, она возвратит следующий результат:
Среднее арифметическое равно : 214.4
Рассмотрим следующую функцию, которая возвращает 10 чисел с помощью указателя, через который передаётся адрес первого элемента массива.
import std.stdio; void main () { int *p = getNumber(); for ( int i = 0; i < 10; i++ ) { writeln("*(p + " , i , ") : ", *(p + i)); } } int * getNumber( ) { static int r [10]; for (int i = 0; i < 10; ++i) { r[i] = i; } return &r[0]; }
Когда вы скомпилируете и выполните эту программу, она возвратит следующий результат:
*(p + 0) : 0 *(p + 1) : 1 *(p + 2) : 2 *(p + 3) : 3 *(p + 4) : 4 *(p + 5) : 5 *(p + 6) : 6 *(p + 7) : 7 *(p + 8) : 8 *(p + 9) : 9
Я тут опять со своими нудными замечаниями. В этом примере важно "волшебное" ключевое слово static в объявлении массива внутри функции. Если его пропустить, то компилятор откажется выводить наружу ссылку через указатель на внутреннюю переменную r, потому что при выходе из функции она перестанет существовать. Существует несколько способов обойти эту проблему, и использование static – самый простой из них, но он имеет побочный эффект – массив r становится чем-то вроде глобальной переменной – прим.пер.
Имя массива является постоянным указателем на первый элемент массива. Поэтому в объявлении:
double balance[50];
balance – это указатель на &balance[0], т.е. на адрес первого элемента массива balance. Таким образом, следующий фрагмент программы присваивает указателю p адрес первого элемента массива balance:
double *p; double balance[10]; p = balance;
Легально использовать имена массивов в качестве постоянных указателей и наоборот. Поэтому *(balance + 4) является законным способом доступа к значению balance[4].
Когда вы храните адрес первого элемента в p, вы можете обращаться к элементам массива, используя *p, *(p+1), *(p+2) и так далее. В следующем примере показаны все концепции, рассмотренные выше:
import std.stdio; void main () { // массив из 5 элементов. double balance[5] = [1000.0, 2.0, 3.4, 17.0, 50.0]; double *p; p = &balance[0]; // Выводит значение каждого элемента массива writeln("Значения элементов массива с использованием указателя " ); for ( int i = 0; i < 5; i++ ) { writeln( "*(p + ", i, ") : ", *(p + i)); } }
Когда вы скомпилируете и выполните эту программу, она возвратит следующий результат:
Значения элементов массива с использованием указателя *(p + 0) : 1000 *(p + 1) : 2 *(p + 2) : 3.4 *(p + 3) : 17 *(p + 4) : 50
При таком использовании указателей необходимо вручную следить за индексами и не выйти за границы массива. Для эксперимента я увеличил верхнюю границу цикла for в последнем примере, никаких ошибок (ни от компилятора, ни времени выполнения) не последовало, и программа послушно выдала мне "мусорные" данные, расположенные в памяти после массива. При использовании стандартной нотации массивов, например, balance[6], система будет сама отслеживать недопустимые индексы – прим.пер.