Указатель - переменная, содержащая адрес объекта. Указатель не несет информации о содержимом объекта, а содержит сведения о том, где размещен объект.

Указатели похожи на метки, которые ссылаются на места в памяти. Они тоже имеют адрес, а их значение является адресом некоторой другой переменной. Переменная, объявленная как указатель, занимает 4 байта в оперативной памяти (в случае 32-битной версии компилятора).

Синтаксис указателей

тип *ИмяОбъекта;

Тип указателя- это тип переменной, адрес которой он содержит. Для работы с указателями в Си определены две операции:

  • операция * (звездочка) - позволяет получить значение объекта по его адресу – определяет значение переменной, которое содержится по адресу, содержащемуся в указателе;
  • операция & (амперсанд) - позволяет определить адрес переменной.

Например:

Сhar c; // переменная char *p; // указатель p = &c; // p = адрес c

Объявление указателя, получение адреса переменной

Для того чтобы объявить указатель, который будет ссылаться на переменную, необходимо сначала получить адрес этой переменной. Чтобы получить адрес памяти переменной, нужно использовать знак «&» перед именем переменной. Это позволяет узнать адрес ячейки памяти, в которой хранится значение переменной. Эта операция называется - операция взятия адреса:

Int var = 5; // простое объявление переменной с предварительной инициализацией int *ptrVar; // объявили указатель, однако он пока ни на что не указывает ptrVar = &var; // теперь наш указатель ссылается на адрес в памяти, где хранится число 5

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

Указатель хранит адрес области памяти. Можно создать указатель на указатель, тогда он будет хранить адрес указателя и сможет обращаться к его содержимому. Указатель на указатель определяется как:

<тип> **<имя>;

Пример работы указателя на указатель:

#include #include #define SIZE 10 void main() { int A; int B; int *p; int **pp; A = 10; B = 111; p = &A; pp = &p; printf("A = %d\n", A); *p = 20; printf("A = %d\n", A); *(*pp) = 30; //здесь скобки можно не писать printf("A = %d\n", A); *pp = &B; printf("B = %d\n", *p); **pp = 333; printf("B = %d", B); getch(); }

Указатели и приведение типов

Так как указатель хранит адрес, можно кастовать его до другого типа. Это может понадобиться, если нужно взять часть переменной, или если мы знаем, что переменная хранит нужный нам тип.

В следующем примере мы пользуемся тем, что размер типа int равен 4 байта, а char 1 байт. За счёт этого, получив адрес первого байта, можно пройти по остальным байтам числа и вывести их содержимое.

#include #include #define SIZE 10 void main() { int A = 10; int *intPtr; char *charPtr; intPtr = &A; printf("%d\n", *intPtr); printf("--------------------\n"); charPtr = (char*)intPtr; printf("%d ", *charPtr); charPtr++; printf("%d ", *charPtr); charPtr++; printf("%d ", *charPtr); charPtr++; printf("%d ", *charPtr); getch(); }

NULL pointer – нулевой указатель

Указатель до инициализации хранит мусор, как и любая другая переменная. Но в, то, же время, этот “мусор” вполне может оказаться валидным адресом. Например, есть указатель. Каким образом узнать, инициализирован он или нет? В общем случае никак. Для решения этой проблемы был введён макрос NULL библиотеки stdlib.

Принято при определении указателя, если он не инициализируется конкретным значением, делать его равным NULL.

Int *ptr = NULL;

По стандарту гарантировано, что в этом случае указатель равен NULL, и равен нулю, и может быть использован как булево значение false. Хотя в зависимости от реализации NULL может и не быть равным 0. То есть указатель можно сравнивать с нулём, или с NULL, но нельзя NULL сравнивать с переменной целого типа или типа с плавающей точкой.

Указатель – это производный тип, который представляет собой адрес какого-либо значения. В языке Си++ используется понятие адреса переменных. Работа с адресами досталась Си++ в наследство от языка Си. Предположим, что в программе определена переменная типа int:

int x;

Можно определить переменную типа "указатель" на целое число:

int* xptr;

и присвоить переменной xptr адрес переменной x:

xptr = &x;

Надо запомнить:

  • Операция &, примененная к переменной, – это операция взятия адреса.
  • Операция *, примененная к адресу (другими словами, примененная к указателю) – это операция обращения по адресу.

Таким образом, для предыдущих определений x и y два оператора эквивалентны:

// присвоить переменной y значение x

int y = x;

// присвоить переменной y значение,

// находящееся по адресу xptr

int y = *xptr;

С помощью операции обращения по адресу можно записывать значения:

// записать число 10 по адресу xptr

*xptr = 10;

После выполнения этого оператора значение переменной x станет равным 10, поскольку xptr указывает на переменную x.

Указатель – это не просто адрес, а адрес величины определенного типа. Указатель xptr – адрес целой величины. Определить адреса величин других типов можно следующим образом:

// указатель на целое число без знака

unsigned long* lPtr;

// указатель на байт

char* cp;

// указатель на объект класса Complex

Complex* p;

Если указатель ссылается на объект некоторого класса, то операция обращения к атрибуту класса вместо точки обозначается "->", например p->real. Если вспомнить один из предыдущих примеров:

void Complex::Add(Complex x)

This->real = this->real + x.real;

This->imaginary = this->imaginary +

X.imaginary;

то this – это указатель на текущий объект, т.е. объект, который выполняет метод Add. Запись this-> означает обращение к атрибуту текущего объекта.

Можно определить указатель на любой тип, в том числе на функцию или метод класса. Если имеется несколько функций одного и того же типа:

int foo(long x);

int bar(long x);

можно определить переменную типа указатель на функцию и вызывать эти функции не напрямую, а косвенно, через указатель:

int (*functptr)(long x);

functptr = &foo;

(*functptr)(2);

functptr = &bar;

(*functptr)(4);

Для чего нужны указатели? Указатели появились, прежде всего, для нужд системного программирования. Поскольку язык Си предназначался для "низкоуровневого" программирования, на нем нужно было обращаться, например, к регистрам устройств. У этих регистров устройств вполне определенные адреса, т.е. необходимо было прочитать или записать значение по определенному адресу. Благодаря механизму указателей, такие операции не требуют никаких дополнительных средств языка.

int* hardwareRegiste =0x80000;

*hardwareRegiste =12;

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

Указатели это чрезвычайно мощный инструмент в программировании. С помощью указателей некоторые вещи в программировании можно сделать намного проще и при этом эффективность работы вашей программы значительно повысится. Указатели даже позволяют обрабатывать неограниченное количество данных. Например, с помощью указателей можно изменять значения переменных внутри функции, при этом переменные передаются в функцию в качестве параметров. Кроме того, указатели можно использовать для динамического выделения памяти, что означает, что вы можете писать программы, которые могут обрабатывать практически неограниченные объемы данных на лету — вам не нужно знать, когда вы пишете программу, сколько памяти нужно выделить заранее. Пожалуй, это самая мощная функция указателей. Для начала давайте просто получим общее представление об указателях, научимся их объявлять и использовать.

Что такое указатели и зачем они нужны?

Указатели похожи на метки, которые ссылаются на места в памяти. Представьте сейф с депозитными ячейками различного размера в местном банке. Каждая ячейка имеет номер, уникальный номер, который связан только с этой ячейкой, таким образом можно быстро идентифицировать нужную ячейку. Эти цифры аналогичны адресам ячеек компьютерной памяти. К примеру, у вас есть богатый дядя, который хранит все свои ценности в своем сейфе. И чтобы обезопасить все свои сбережения, он решил завести меньший сейф, в который положит карту, на которой показано местоположение большого сейфа и указан 16-й пароль от большого сейфа, в котором и хранятся реальные драгоценности. По сути, сейф с картой будет хранить расположение другого сейфа. Вся эта организация сбережения драгоценностей эквивалентна указателям в языке Си. В компьютере, указатели просто переменные, которые хранят адреса памяти, как правило, адреса других переменных.

Идея в том, что зная адрес переменной, вы можете пойти по этому адресу и получить данные, хранящиеся в нем. Если вам нужно передать огромный кусок данных в функцию, намного проще передать адрес в памяти, по которому хранятся эти данные, чем скопировать каждый элемент данных! Более того, если программе понадобится больше памяти, вы можете запросить больше памяти из системы. Как же это работает? Система просто возвращает адрес ячейки памяти, и мы должны сохранить этот адрес в переменной-указателе. Так мы сможем взаимодействовать с данными из указанного участка памяти.

Синтаксис указателей

Если у нас есть указатель, значит мы можем получить его адрес в памяти и данные на которые он ссылается, по этой причине указатели имеют несколько необычный синтаксис, отличающийся от объявления простых переменных. Более того, поскольку указатели — это не обычные переменные, то, необходимо сообщить компилятору, что переменная является указателем и сообщить компилятору тип данных, на которые ссылается указатель. Итак, указатель объявляется следующим образом:

Data_type *pointerName;

где, data_type — тип данных, pointerName — имя указателя.

Например, объявим указатель, который хранит адрес ячейки памяти, в которой лежит целое число:

Int *integerPointer;

Обратите внимание на использование символа * , при объявлении указателя. Этот символ является ключевым в объявлении указателя. Если в объявлении переменной, непосредственно перед именем переменной, добавить этот символ, то переменная будет объявлена как указатель. Кроме того, если вы объявляете несколько указателей в одной строке, каждый из них должен предваряться символом звездочки. Рассмотрим несколько примеров:

// Объявление указателя и простой переменной в одной строке int *pointer1, // это указатель variable; // это обычная переменная типа int // Объявление двух указателей в одно строке int *pointer1, // это указатель с именем pointer1 *pointer2; // это указатель с именем pointer2

Как я и говорил, если имя переменной не предваряется символом * , то это обычная переменная, в противном случае — это указатель. Именно это и показывает пример объявления указателей, выше.

Есть два способа использования указателя:

  1. Использовать имя указателя без символа * , таким образом можно получить фактический адрес ячейки памяти, куда ссылается указатель.
  2. Использовать имя указателя с символом * , это позволит получить значение, хранящееся в памяти. В рамках указателей, у символа * есть техническое название — операция разыименования. По сути, мы принимаем ссылку на какой-то адрес памяти, чтобы получить фактическое значение. Это может быть сложно для понимания, но в дальнейшем постараемся разобраться во всем этом.

Объявление указателя, получение адреса переменной

Для того чтобы объявить указатель, который будет ссылаться на переменную, необходимо сначала получить адрес этой переменной. Чтобы получить адрес памяти переменной (её расположение в памяти), нужно использовать знак & перед именем переменной. Это позволяет узнать адрес ячейки памяти, в которой хранится значение переменной. Эта операция называется — операция взятия адреса и выглядит вот так:

Int var = 5; // простое объявление переменной с предварительной инициализацией int *ptrVar; // объявили указатель, однако он пока ни на что не указывает ptrVar = &var; // теперь наш указатель ссылается на адрес в памяти, где хранится число 5

В строке 3 использовалась операция взятия адреса, мы взяли адрес переменной var и присвоили его указателю ptrVar . Давайте рассмотрим программу, которая наглядно покажет всю мощь указателей. Итак, вот исходник:

#include int main() { int var; // обычная целочисленная переменная int *ptrVar; // целочисленный указатель (ptrVar должен быть типа int, так как он будет ссылаться на переменную типа int) ptrVar = &var; // присвоили указателю адрес ячейки в памяти, где лежит значение переменной var scanf("%d", &var); // в переменную var положили значение, введенное с клавиатуры printf("%d\n", *ptrVar); // вывод значения через указатель getchar(); }

Результат работы программы:

В строке 10 , printf() выводит значение, хранящееся в переменной var . Почему так происходит? Что ж, давайте посмотрим на код. В строке 5 , мы объявили переменную var типа int . В строке 6 — указатель ptrVar на целое значение. Затем указателю ptrVar присвоили адрес переменной var , для этого мы воспользовались оператором присвоения адреса. Затем пользователь вводит номер, который сохраняется в переменную var , помните, что это то же самое место, на которое указывает ptrVar . В самом деле, так как мы используем амперсанд чтобы присвоить значение переменной var в функции scanf() , должно быть понятно, что scanf() инициализирует переменную var через адрес. На этот же адрес указывает указатель ptrVar .

Затем, в строке 10 , выполняется операция «разыменования» — *ptrVar . Программа, через указатель ptrVar , считывает адрес, который хранится в указателе, по адресу попадает в нужную ячейку памяти, и возвращает значение, которое там хранится.

Обратите внимание, что в приведенном выше примере, перед тем как использовать указатель, он сначала инициализируется, это нужно для того, чтобы указатель ссылался на определенный адрес памяти. Если бы мы начали использовать указатель так и не инициализировав его, он бы ссылался на какой угодно участок памяти. И это могло бы привести к крайне неприятным последствиям. Например, операционная система, вероятно, помешает вашей программе получить доступ к неизвестному участку памяти, так как ОС знает, что в вашей программе не выполняется инициализация указателя. В основном это просто приводит к краху программы.

Если бы такие приемчики были позволены в ОС, вы могли бы получить доступ к любому участку памяти. А это значит, что для любой запущенной программы вы могли бы внести свои изменения, например, если у вас открыт документ в Word, вы могли бы изменить любой текст программно. К счастью, Windows и другие современные операционные системы остановит вас от доступа к этой памяти и преждевременно закроют вашу программу.

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

P.S.:Если у вас нет денег на телефоне, и нет возможности его пополнить, но при этом, вам срочно нужно позвонить, вы всегда можете использовать доверительный платеж билайн . Сумма доверительного платежа может быть самой разнообразной, от 50 до 300р.

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

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

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

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

// pointer1.cpp: определяет точку входа для консольного приложения. #include "stdafx.h" #include << "&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 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).

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

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

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

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

"stdafx.h" #include << "var1 = " << var1 << endl; cout << "var2 = " << var2 << endl; cout << "ptrvar1 = " << ptrvar1 << endl; cout << "ptrvar2 = " << ptrvar2 << endl; if (ptrvar1 > << "ptrvar1 > ptrvar2" << endl; if (*ptrvar1 > << "*ptrvar1 > *ptrvar2" << endl; system("pause"); return 0; }

// код Code::Blocks

// код Dev-C++

// pointer.cpp: определяет точку входа для консольного приложения. #include 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.

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

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

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

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

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

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

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 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 показан результат работы программы.

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 << "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 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).

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

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

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

Указатель - это переменная, содержащая адрес некоторого объекта в оперативной памяти (ОП). Смысл применения указателей - косвенная адресация объектов в ОП, позволяющая динамически менять логику программы и управлять распределением ОП.

Основные применения:

  • работа с массивами и строками;
  • прямой доступ к ОП;
  • работа с динамическими объектами, под которые выделяется ОП.

Описание указателя имеет следующий общий вид:

Тип *имя;

то есть, указатель всегда адресует определённый тип объектов ! Например,

Int *px; // указатель на целочисленные данные char *s; //указатель на тип char (строку Си)

Опишем основные операции и действия, которые разрешены с указателями:

1. Сложение/вычитание с числом:

Px++; //переставить указатель px на sizeof(int) байт вперед s--; //перейти к предыдущему символу строки //(на sizeof(char) байт, необязательно один)

2. Указателю можно присваивать адрес объекта унарной операцией " & ":

Int *px; int x,y; px=&x; //теперь px показывает на ячейку памяти со // значением x px=&y; //а теперь – на ячейку со значением y

3. Значение переменной, на которую показывает указатель, берется унарной операцией " * " ("взять значение"):

X=*px; //косвенно выполнили присваивание x=y (*px)++; //косвенно увеличили значение y на 1

Важно ! Из-за приоритетов и ассоциативности операций C++ действие

имеет совсем другой смысл, чем предыдущее. Оно означает "взять значение y (*px) и затем перейти к следующей ячейке памяти (++)"

Расшифруем оператор

Если px по-прежнему показывал на y , он означает "записать значение y в x и затем перейти к ячейке памяти, следующей за px ". Именно такой подход в классическом Си используется для сканирования массивов и строк.

Вот пример, с точностью до адресов памяти показывающий это важное различие. Комментарием приведены значения и адреса памяти переменных x и y , а также значение, полученное по указателю px и адрес памяти, на который он показывает. Обратите внимание, что после выполнения второго варианта кода значение, полученное по указателю, стало "мусором", так как он показывал на переменную, а не на нулевой элемент массива.

#include int main() { int x=0,y=1; int *px=&y; printf ("\nx=%d on &%p, y=%d on &%p, *px=%d on &%p",x,&x,y,&y,*px,px); x=(*px)++; //после первого запуска замените на x=*px++; printf ("\nx=%d on &%p, y=%d on &%p, *px=%d on &%p",x,&x,y,&y,*px,px); /* Действие (*px)++ x=0 on &002CFC14, y=1 on &002CFC08, *px=1 on &002CFC08 x=1 on &002CFC14, y=2 on &002CFC08, *px=2 on &002CFC08 Действие *px++ x=0 on &0021F774, y=1 on &0021F768, *px=1 on &0021F768 x=1 on &0021F774, y=1 on &0021F768, *px=-858993460 on &0021F76C */ getchar(); return 0; }

Приведём пример связывания указателя со статическим массивом:

Int a={1,2,3,4,5}; int *pa=&a; for (int i=0; i<5; i++) cout

For (int i=0; i<5; i++) cout

Эти записи абсолютно эквиваленты, потому что в Си конструкция a[b] означает не что иное, как *(a+b) , где a - объект, b – смещение от начала памяти, адресующей объект. Таким образом, обращение к элементу массива a[i] может быть записано и как *(a+i) , а присваивание указателю адреса нулевого элемента массива можно бы было записать в любом из 4 видов

Int *pa=&a; int *pa=&(*(a+0)); int *pa=&(*a); int *pa=a;

Важно ! При любом способе записи это одна и та же операция, и это - не "присваивание массива указателю", это его установка на нулевой элемент массива.

4. Сравнение указателей (вместо сравнения значений, на которые они указывают) в общем случае может быть некорректно !

Int x; int *px=&x, *py=&x; if (*px==*py) ... //корректно if (px==py) ... //некорректно!

Причина – адресация ОП не обязана быть однозначной, например, в DOS одному адресу памяти могли соответствовать разные пары частей адреса "сегмент" и "смещение".

Способ 1, со ссылочной переменной C++

Void swap (int &a, int &b) { int c=a; a=b; b=c; } //... int a=3,b=5; swap (a,b);

Этот способ можно назвать "передача параметров по значению, приём по ссылке".

Способ 2, с указателями Cи

Void swap (int *a, int *b) { int c=*a; *a=*b; *b=c; } //... int a=3,b=5; swap (&a,&b); int *pa=&a; swap (pa,&b);

Передача параметров по адресу, прием по значению.

Указатели и строки языка Си

Как правило, для сканирования Си-строк используются указатели.

Char *s="Hello, world";

Это установка указателя на первый байт строковой константы, а не копирование и не присваивание!

Важно !

1. Даже если размер символа равен одному байту, эта строка займёт не 12 (11 символов и пробел), а 13 байт памяти. Дополнительный байт нужен для хранения нуль-терминатора, символа с кодом 0 , записываемого как "\0" (но не "0" – это цифра 0 с кодом 48). Многие функции работы с Си-строками автоматически добавляют нуль-терминатор в конец обрабатываемой строки:

Char s; strcpy(s,"Hello, world"); //Вызвали стандартную функцию копирования строки //Ошибка! Нет места для нуль-терминатора сhar s; //А так было бы верно!

2. Длина Си-строки нигде не хранится, её можно только узнать стандартной функцией strlen(s) , где s – указатель типа char * . Для строки, записанной выше, будет возвращено значение 12, нуль-терминатор не считается. Фактически, Си-строка есть массив символов, элементов типа char .

Как выполнять другие операции со строками, заданными c помощью указателей char * ? Для этого может понадобиться сразу несколько стандартных библиотек. Как правило, в новых компиляторах C++ можно подключать и "классические" си-совместимые заголовочные файлы, и заголовки из более новых версий стандарта, которые указаны в скобках.

Файл ctype.h (cctype) содержит:

1) функции с именами is* - проверка класса символов (isalpha , isdigit , ...), все они возвращают целое число, например:

Char d; if (isdigit(d)) { //код для ситуации, когда d - цифра }

Аналогичная проверка "вручную" могла бы быть выполнена кодом вида

If (d>="0" && d<="9") {

2) функции с именами to* - преобразование регистра символов (toupper , tolower), они возвращают преобразованный символ. Могут быть бесполезны при работе с символами национальных алфавитов, а не только латиницей.

Модуль string.h (cstring) предназначен для работы со строками, заданными указателем и заканчивающимися байтом "\0" ("строками Си"). Имена большинства его функций начинаются на "str". Часть функций (memcpy , memmove , memcmp) подходит для работы с буферами (областями памяти с известным размером).

Примеры на работу со строками и указателями

1. Копирование строки

Char *s="Test string"; char s2; strcpy (s2,s); //копирование строки, s2 - буфер, а не указатель!

2. Копирование строки с указанием количества символов

Char *s="Test string"; char s2; char *t=strncpy (s2,s,strlen(s)); cout << t;

Функция strncpy копирует не более n символов (n - третий параметр), но не запишет нуль-терминатор, в результате чего в конце строки t выведется "мусор". Правильно было бы добавить после вызова strncpy следующее:

T="\0";

то есть, "ручную" установку нуль-терминатора.

3. Копирование строки в новую память

Char *s="12345"; char *s2=new char ; strcpy (s2,s);

Здесь мы безопасно скопировали строку s в новую память s2 , не забыв выделить "лишний" байт для нуль-терминатора.

4. Приведём собственную реализацию стандартной функции strcpy:

Char *strcpy_ (char *dst, char *src) { char *r=dst; while (*src!="\0") { *dst=*src; dst++; src++; } *dst="\0"; return r; }

Вызвать нашу функцию можно, например, так:

Char *src="Строка текста"; char dst; strcpy_ (&dst,&src);

Сократим текст функции strcpy_:

Char *strcpy_ (char *dst, char *src) { char *r=dst; while (*src) *dst++=*src++; *dst="\0"; return r; }

5. Сцепление строк – функция strcat

Char *s="Test string"; char *s2; char *t2=strcat (s2,strcat(s," new words"));

Так как strcat не выделяет память, поведение такого кода непредсказуемо!

А вот такое сцепление строк сработает:

Char s; strcpy (s,"Test string"); char s2; strcat (s," new words"); strcpy (s2,s); char *t2=strcat (s2,s);

То есть, всегда должна быть память, куда писать - статическая из буфера или выделенная динамически.

6. Поиск символа или подстроки в строке.

Char *sym = strchr (s,"t"); if (sym==NULL) puts ("Не найдено"); else puts (sym); //выведет "t string" //для strrchr вывод был бы "tring" char *sub = strstr (s,"ring"); puts (sub); //выведет "ring"

7. Сравнение строк – функции с шаблоном имени str*cmp - "string comparing"

Char *a="abcd",*b="abce"; int r=strcmp(a,b); //r=-1, т.к. символ "d" предшествует символу "e" //Соответственно strcmp(b,a) вернет в данном случае 1 //Если строки совпадают, результат=0

8. Есть готовые функции для разбора строк - strtok , strspn , strсspn - см. пособие, пп. 8.1-8.3

9. Преобразование типов между числом и строкой - библиотека stdlib.h (cstdlib)

Char *s="qwerty"; int i=atoi(s); //i=0, исключений не генерируется!

Из числа в строку:

1) itoa , ultoa - из целых типов

Char buf; int i=-31189; char *t=itoa(i,buf,36); //В buf получили запись i в 36-ричной с.с.

2) fcvt , gcvt , ecvt - из вещественных типов

Работа с динамической памятью

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

1. Описать указатель на будущий динамический объект:

Int *a; //Надёжнее int *a=NULL;

2. Оператором new или функциями malloc , calloc выделить оперативную память:

A = new int ;

#include //stdlib.h, alloc.h в разных компиляторах //... a = (int *) malloc (sizeof(int)*10);

A = (int *) calloc (10,sizeof(int));

В последнем случае мы выделили 10 элементов по sizeof(int) байт и заполнили нулями "\0" .

Важно ! Не смешивайте эти 2 способа в одном программном модуле или проекте! Предпочтительней new , кроме тех случаев, когда нужно обеспечить заполнение памяти нулевыми байтами.

3. Проверить, удалось ли выделить память - если нет, указатель равен константе константе NULL из стандартной библиотеки (в ряде компиляторов null , nullptr , 0):

If (a==NULL) { //Обработка ошибка "Не удалось выделить память" }

4. Работа с динамическим массивом или строкой ничем не отличается от случая, когда они статические.

5. Когда выделенная ОП больше не нужна, её нужно освободить:

Delete a; //Если использовали new free (a); //Пытается освободить ОП, //если использовали malloc/calloc

Важно ! Всегда старайтесь придерживаться принципа стека при распределении ОП. То есть, объект, занявший ОП последним, первым её освобождает.

Пример. Динамическая матрица размером n*m.

Const int n=5,m=4; int **a = new (int *) [n]; for (int i=0; i

После этого можно работать с элементами матрицы a[i][j] , например, присваивать им значения. Освободить память можно было бы так:

For (int i=n-1; i>-1; i--) delete a[i]; delete a;

1. Написать собственную функцию работы со строкой, заданной указателем, сравнить со стандартной.

2. Написать собственную функцию для работы с одномерным динамическим массивом, заданным указателем.

3. Написать свои версии функций преобразования строки в число и числа в строку.


Close