Указатели в С++

Указатель – переменная, значением которой является адрес ячейки памяти. То есть указатель ссылается на блок данных  из области памяти, причём на самое его начало. Указатель может ссылаться на переменную или функцию. Для этого нужно знать адрес переменной или функции. Так вот, чтобы узнать адрес конкретной переменной в С++ существует унарная операция взятия адреса &. Такая операция извлекает адрес объявленных переменных, для того, чтобы его присвоить указателю.

Указатели используются для передачи по ссылке данных, что намного ускоряет процесс обработки этих данных (в том случае, если объём данных большой), так как их не надо копировать, как при передаче по значению, то есть, используя имя переменной. В основном указатели используются для организации динамического распределения памяти, например при объявлении массива, не надо будет его ограничивать в размере. Ведь программист заранее не может знать, какого размера нужен массив тому или иному пользователю, в таком случае используется динамическое выделение памяти под массив. Любой указатель необходимо объявить перед использованием, как и любую переменную.

//объявление указателя
/*тип данных*/  * /*имя указателя*/;

Принцип объявления указателей такой же, как и принцип объявления переменных. Отличие заключается только в том, что перед именем ставится символ звёздочки *. Визуально указатели отличаются от переменных только одним символом. При объявлении указателей компилятор выделяет несколько байт памяти, в зависимости от типа данных отводимых для хранения некоторой информации в памяти. Чтобы получить значение, записанное в некоторой области, на которое ссылается указатель нужно воспользоваться операцией разыменования указателя *. Необходимо поставить звёздочку перед именем и получим доступ к значению указателя. Разработаем программу, которая будет использовать указатели.

// pointer1.cpp: определяет точку входа для консольного приложения.

#include "stdafx.h"
#include <iostream>
using namespace std;

int main(int argc, char* argv[])
{
    int var = 123; // инициализация переменной var числом 123
    int *ptrvar = &var; // указатель на переменную var (присвоили адрес переменной указателю)
    cout << "&var    = " << &var << endl;// адрес переменной var содержащийся в памяти, извлечённый операцией взятия адреса 
    cout << "ptrvar  = " << ptrvar << endl;// адрес переменной var, является значением указателя ptrvar 
    cout << "var     = " << var << endl; // значение в переменной var
    cout << "*ptrvar = " << *ptrvar << endl; // вывод значения содержащегося в переменной var через указатель, операцией разименования указателя
    system("pause");
    return 0;
}

// код Code::Blocks

// код Dev-C++

// pointer1.cpp: определяет точку входа для консольного приложения.

#include <iostream>
using namespace std;

int main(int argc, char* argv[])
{
    int var = 123; // инициализация переменной var числом 123
    int *ptrvar = &var; // указатель на переменную var (присвоили адрес переменной указателю)
    cout << "&var    = " << &var << endl;// адрес переменной var содержащийся в памяти, извлечённый операцией взятия адреса
    cout << "ptrvar  = " << ptrvar << endl;// адрес переменной var, является значением указателя ptrvar
    cout << "var     = " << var << endl; // значение в переменной var
    cout << "*ptrvar = " << *ptrvar << endl; // вывод значения содержащегося в переменной var через указатель, операцией разименования указателя
    return 0;
}

В строке 10 объявлен и инициализирован адресом переменной var указатель ptrvar. Можно было сначала просто объявить указатель, а потом его инициализировать, тогда были бы две строки:

int *ptrvar;        // объявление указателя
     ptrvar = &var; // инициализация указателя

В программировании принято добавлять к имени указателя приставку ptr, таким образом, получится осмысленное имя указателя, и уже с обычной переменной такой указатель не спутаешь. Результат работы программы (см. Рисунок 1).

CppStudio.com
&var    = 0x22ff08
ptrvar  = 0x22ff08
var     = 123
*ptrvar = 123
Для продолжения нажмите любую клавишу . . .

Рисунок 1 — Указатели в С++

Итак, программа показала, что строки 11 и 12 выводят идентичный адрес, то есть адрес переменной var, который содержится в указателе ptrvar. Тогда как операция разыменования указателя *ptrvar обеспечивает доступ к значению, на которое ссылается указатель.

Указатели можно сравнивать не только на равенство или неравенство, ведь адреса могут быть меньше или больше относительно друг друга. Разработаем программу, которая будет сравнивать адреса указателей.

// pointer.cpp: определяет точку входа для консольного приложения.

#include "stdafx.h"
#include <iostream>
using namespace std;

int main(int argc, char* argv[])
{
    int var1 = 123; // инициализация переменной var1 числом 123
    int var2 = 99; // инициализация переменной var2 числом 99
    int *ptrvar1 = &var1; // указатель на переменную var1
    int *ptrvar2 = &var2; // указатель на переменную var2
    cout << "var1    = " << var1 << endl;
    cout << "var2    = " << var2 << endl;
    cout << "ptrvar1 = " << ptrvar1 << endl;
    cout << "ptrvar2 = " << ptrvar2 << endl;
    if (ptrvar1 > ptrvar2) // сравниваем значения указателей, то есть адреса переменных
        cout << "ptrvar1 > ptrvar2" << endl;
    if (*ptrvar1 > *ptrvar2) // сравниваем значения переменных, на которые ссылаются указатели
        cout << "*ptrvar1 > *ptrvar2" << endl;
    system("pause");
    return 0;
}

// код Code::Blocks

// код Dev-C++

// pointer.cpp: определяет точку входа для консольного приложения.

#include <iostream>
using namespace std;

int main(int argc, char* argv[])
{
    int var1 = 123; // инициализация переменной var1 числом 123
    int var2 = 99; // инициализация переменной var2 числом 99
    int *ptrvar1 = &var1; // указатель на переменную var1
    int *ptrvar2 = &var2; // указатель на переменную var2
    cout << "var1    = " << var1 << endl;
    cout << "var2    = " << var2 << endl;
    cout << "ptrvar1 = " << ptrvar1 << endl;
    cout << "ptrvar2 = " << ptrvar2 << endl;
    if (ptrvar1 > ptrvar2) // сравниваем значения указателей, то есть адреса переменных
        cout << "ptrvar1 > ptrvar2" << endl;
    if (*ptrvar1 > *ptrvar2) // сравниваем значения переменных, на которые ссылаются указатели
        cout << "*ptrvar1 > *ptrvar2" << endl;
    return 0;
}

Результат работы программы показан на рисунке 2.

CppStudio.com
var1    = 123
var2    = 99
ptrvar1 = 0x22ff04
ptrvar2 = 0x22ff00
ptrvar1 > ptrvar2
*ptrvar1 > *ptrvar2
Для продолжения нажмите любую клавишу . . .

Рисунок 2 — Указатели в С++

В первом случае, мы сравнивали адреса  переменных, и, причём адрес второй переменной, всегда меньше адреса первой переменной. При каждом запуске программы адреса выделяются разные. Во втором случае мы сравнивали значения этих переменных используя операцию разыменования указателя.

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

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

Указатели могут ссылаться на другие указатели. При этом в ячейках памяти, на которые будут ссылаться первые указатели, будут содержаться не значения, а адреса вторых указателей. Число символов * при объявлении указателя показывает порядок указателя. Чтобы получить доступ к значению, на которое ссылается указатель его необходимо разыменовывать соответствующее количество раз. Разработаем программу, которая будет выполнять некоторые операции с указателями порядка выше первого.

// pointer.cpp: определяет точку входа для консольного приложения.
#include "stdafx.h"
#include <iostream>
using namespace std;

int _tmain(int argc, _TCHAR* argv[])
{
    int var = 123; // инициализация переменной var числом 123
    int *ptrvar = &var; // указатель на переменную var
    int **ptr_ptrvar = &ptrvar; // указатель на указатель на переменную var
    int ***ptr_ptr_ptrvar = &ptr_ptrvar;
    cout << " var\t\t= " << var << endl;
    cout << " *ptrvar\t= " << *ptrvar << endl;
    cout << " **ptr_ptrvar   = " << **ptr_ptrvar << endl; // два раза разименовываем указатель, так как он второго порядка 
    cout << " ***ptr_ptrvar  = " << ***ptr_ptr_ptrvar << endl; // указатель третьего порядка
    cout << "\n ***ptr_ptr_ptrvar -> **ptr_ptrvar -> *ptrvar ->      var -> "<< var << endl;
    cout << "\t  " << &ptr_ptr_ptrvar<< " -> " << "    " << &ptr_ptrvar << " ->" << &ptrvar << " -> " << &var << " -> " << var << endl;
    system("pause");
    return 0;
}

// код Code::Blocks

// код Dev-C++

// pointer.cpp: определяет точку входа для консольного приложения.

#include <iostream>
using namespace std;

int main()
{
    int var = 123; // инициализация переменной var числом 123
    int *ptrvar = &var; // указатель на переменную var
    int **ptr_ptrvar = &ptrvar; // указатель на указатель на переменную var
    int ***ptr_ptr_ptrvar = &ptr_ptrvar;
    cout << " var\t\t= " << var << endl;
    cout << " *ptrvar\t= " << *ptrvar << endl;
    cout << " **ptr_ptrvar   = " << **ptr_ptrvar << endl; // два раза разименовываем указатель, так как он второго порядка
    cout << " ***ptr_ptrvar  = " << ***ptr_ptr_ptrvar << endl; // указатель третьего порядка
    cout << "\n ***ptr_ptr_ptrvar -> **ptr_ptrvar -> *ptrvar ->      var -> "<< var << endl;
    cout << "\t  " << &ptr_ptr_ptrvar<< " -> " << "    " << &ptr_ptrvar << " ->" << &ptrvar << " -> " << &var << " -> " << var << endl;
    return 0;
}

На рисунке 3 показан результат работы программы.

CppStudio.com
 var            = 123
 *ptrvar        = 123
 **ptr_ptrvar   = 123
 ***ptr_ptrvar  = 123

 ***ptr_ptr_ptrvar -> **ptr_ptrvar -> *ptrvar ->      var -> 123
          0x22ff00 ->     0x22ff04 ->0x22ff08 -> 0x22ff0c -> 123
Для продолжения нажмите любую клавишу . . .

Рисунок 3 — Указатели в С++

Данная программа доказывает тот факт, что для получения значения количество разыменований указателя должно совпадать с его порядком. Логика n-кратного разыменования заключается в том, что программа последовательно перебирает адреса всех указателей вплоть до переменной, в которой содержится значение. В программе показана реализация указателя третьего порядка. И если, используя такой  указатель (третьего порядка) необходимо получить значение, на которое он ссылается, делается 4 шага:

  1. по значению указателя третьего порядка получить адрес указателя второго порядка;
  2. по значению указателя второго порядка получить адрес указателя первого порядка;
  3. по значению указателя первого порядка получить адрес переменной;
  4. по адресу переменной получить доступ к её значению.

Данные четыре действия показаны на рисунке 3 (две предпоследние строки). Верхняя строка показывает имена указателей, а нижняя строка их адреса.

На рисунке 4 показана схема разыменовывания указателя третьего порядка из верхней программы. Суть в том, что указатели связаны друг с другом через свои адреса. Причём, например, для указателя ptr_ptrvar данное число  0015FDB4 является  адресом, а для указателя ptr_ptr_ptrvar это же число является значением.

Указатели в С++

Рисунок 4 — Указатели в С++

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

Указатели на функции

Указатели могут ссылаться на функции. Имя функции, как и имя массива само по себе является указателем, то есть содержит адрес входа.

// объявление указателя на функцию
/*тип данных*/ (* /*имя указателя*/)(/*список аргументов функции*/);

Тип данных определяем такой, который будет возвращать функция, на которую будет ссылаться указатель. Символ указателя и его имя берутся в круглые скобочки, чтобы показать, что это указатель, а не функция, возвращающая указатель на определённый тип данных.  После имени указателя идут круглые скобки, в этих скобках перечисляются все аргументы через запятую как в объявлении прототипа функции. Аргументы наследуются от той функции, на которую будет ссылаться указатель. Разработаем программу, которая использует указатель на функцию. Программа должна находить НОД – наибольший общий делитель. НОД – это наибольшее целое число, на которое без остатка делятся два числа, введенных пользователем. Входные числа также должны быть целыми.

// pointer_onfunc.cpp: определяет точку входа для консольного приложения.

#include "stdafx.h"
#include <iostream>
using namespace std;
int nod(int, int ); // прототип указываемой функции
int main(int argc, char* argv[])
{
    int (*ptrnod)(int, int); // объявление указателя на функцию
    ptrnod=nod; // присваиваем адрес функции указателю ptrnod 
    int a, b;
    cout << "Enter first number: ";
    cin >> a;
    cout << "Enter second number: ";
    cin >> b;
    cout << "NOD = " << ptrnod(a, b) << endl; // обращаемся к функции через указатель
    system("pause");
    return 0;
}
int nod(int number1, int number2) // рекурсивная функция нахождения наибольшего общего делителя НОД
{
    if ( number2 == 0 ) //базовое решение
        return number1;
    return nod(number2, number1 % number2); // рекурсивное решение НОД
}

// код Code::Blocks

// код Dev-C++

// pointer_onfunc.cpp: определяет точку входа для консольного приложения.

#include <iostream>
using namespace std;
int nod(int, int ); // прототип указываемой функции
int main(int argc, char* argv[])
{
    int (*ptrnod)(int, int); // объявление указателя на функцию
    ptrnod=nod; // присваиваем адрес функции указателю ptrnod
    int a, b;
    cout << "Enter first number: ";
    cin >> a;
    cout << "Enter second number: ";
    cin >> b;
    cout << "NOD = " << ptrnod(a, b) << endl; // обращаемся к функции через указатель
    return 0;
}
int nod(int number1, int number2) // рекурсивная функция нахождения наибольшего общего делителя НОД
{
    if ( number2 == 0 ) //базовое решение
        return number1;
    return nod(number2, number1 % number2); // рекурсивное решение НОД
}

Данная задача решена рекурсивно, чтоб уменьшить объём кода программы, по сравнению с итеративным решением этой же задачи. В строке 9 объявляется указатель,  которому в строке 10 присвоили адрес функции. Как мы уже говорили до этого, адресом функции является просто её имя. То есть данный указатель теперь указывает на функцию nod(). При объявлении указателя на функцию ни в коем случае не забываем о скобочках, в которые заключаются символ указателя и его имя. При объявлении указателя в аргументах указываем то же самое, что и в прототипе указываемой функции. Результат работы программы (см. Рисунок 5).

CppStudio.com
Enter first number: 16
Enter second number: 20
NOD = 4
Для продолжения нажмите любую клавишу . . .

Рисунок 5 — Указатели в С++

Вводим первое число, затем второе и программа выдает НОД. На рисунке 5 видно, что НОД для чисел 16 и 20 равен четырём.

Практика

К сожалению, для данной темы пока нет подходящих задач. Если у вас есть таковые на примете, отправте их по адресу: admin@cppstudio.com. Мы их опубликуем!

Автор: admin
Дата: 25.08.2012
Поделиться:

Комментарии

  1. Константин Пучин

    Не понял что делает эта строка:  return nod(number2, number1 % number2)

  2. aibalit

    Хотелось бы узнать в каких случаях стоит использовать указатель на функцию?
    И что получится в результате выполнения следующего оператора ++ptrnod()?

    • Georgy Petrov

      #include "stdafx.h"
      #include <iostream>
      #include <cmath>
      
      using namespace std;
      
      int& nod(int, int);
      int& (*ptrnod)(int, int);
      
      int _tmain(int argc, _TCHAR* argv[])
      {
      	ptrnod = nod;
      	int n1, n2;
      	cin >> n1 >> n2;
      	cout << "NOD= " << ++ptrnod(n1, n2);
      
      	_gettch();
      	return 0;
      }
      int& nod(int _n1, int _n2){
      	if (_n2 == 0) return _n1;
      	return nod(_n2,abs(_n1%_n2));
      }

       

Оставить комментарий

Вы должны войти, чтобы оставить комментарий.