Указатели


Предыдущая страница
Следующая страница  

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

Pointer in 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 или на что-то другое – один и тот же, длинное шестнадцатеричное число, которое представляет собой адрес в памяти. Единственное различие между указателями разных типов данных – это тип данных переменной или константы, на которую указывает указатель.

Использование указателей в программировании на D

Существует несколько важных операций, которые мы часто используем с указателями.

Это делается с помощью унарного оператора *, который возвращает значение переменной, расположенной по адресу, указанному его операндом. В следующем примере используются эти операции:

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

Указатель на указатель

Указатель на указатель представляет собой форму многократной косвенности или цепочки указателей. Обычно указатель содержит адрес переменной. Когда мы определяем указатель на указатель, первый указатель содержит адрес второго указателя, который, в свою очередь, указывает на местоположение, содержащее фактическое значение, как показано ниже.

C++ Pointer to Pointer

Переменная, являющаяся указателем на указатель, должна быть объявлена соответствующим образом. Это делается путём размещения дополнительной звездочки перед её именем. Например, вот так выглядит синтаксис объявления указателя на указатель типа 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], система будет сама отслеживать недопустимые индексы – прим.пер.


Предыдущая страница
Следующая страница