Домой / Skype / Операции с указателями. Что значит в си: что такое указатель Указатели и приведение типов

Операции с указателями. Что значит в си: что такое указатель Указатели и приведение типов

При изучении Си у начинающих часто возникают вопросы связанные с указателями, думаю вопросы у всех возникают примерно одинаковые поэтому опишу те, которые возникли у меня.

Для чего нужен указатель?

Почему всегда пишут “указатель типа” и чем указатель типа uint16_t отличается от указателя типа uint8_t ?

И кто вообще выдумал указатель?

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

Для объявления переменной как указателя необходимо перед её именем поставить * , а для получения адреса переменной используется & (унарный оператор взятия адреса).
char a = "a"; char *p = &a;
В данном случае в р содержится адрес переменной а. Но что интересно, для дальнейшей работы с указателем не надо писать звёздочку, она нужна только при объявлении .
char a = "a"; char b = "b"; char *p = &a; p = &b;
В данном случае в р содержится адрес переменной b, но если мы хотим получить значение лежащее по этому адресу, то нужно использовать оператор разыменования , та же звёздочка *.
char new_simbol = 0; char a = "a"; char *p = &a; new_simbol = *p;
Таким образом, переменная new_simbol будет содержать ascii код символа "a".

Теперь перейдём непосредственно к вопросам, для чего нужен указатель. Представьте что у нас есть массив, с которым мы хотим работать в функции. Для того чтобы передать массив в функцию его надо скопировать, то есть потратить память, которой у МК и так мало, поэтому более правильным решение будет не копировать массив, а передать адрес его первого элемента и размер.
m ={1,2,3...};
Можно это сделать так
void foo(char *m, uint8_t size) { }
или так
void foo(char m, uint8_t size) { }
Поскольку имя массива, содержит адрес его первого элемента, это есть не что иное, как указатель. Перемещаться по массиву можно с помощью простейших арифметических операций, например, для того чтобы получить значение пятого элемента массива, необходимо к адресу массива(адрес первого элемента) прибавить 4 и применить оператор разыменования.
m = *(m + 4);

И тут же возникает вопрос, для чего везде пишут тип перед указателем? Все просто, передавая адрес первого элемента массива и размер массива, мы говорим: Вот отсюда(указатель) выкопать 10 ямок(размер массива), приходим через два часа, а те, кто должны были выкопать ямки, вызвали трактор и роют котлован. Чтобы не попасть в такую ситуацию надо было указать размер ямки, в нашей аналогии тип указателя так, как тип определяет сколько байт будет занимать переменная в памяти.

Таким образом, указывая тип указателя, мы говорим компилятору, вот тебе адрес начала массива, один элемент массива занимает 2 байта, таких элементов в массиве 10 итого сколько памяти выделить под этот массив? 20 байт - отвечает компилятор. Для наглядности давайте возьмем указатель типа void, для него не определено сколько места он занимает - это просто адрес, приведём его к указателям разного типа и выполним операцию разадресации.


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

Ну и последний вопрос, кто выдумал эту бяку указатель. Для того чтобы разобраться в этом вопросе, надо обратиться к ассемблеру, например AVR, и там мы найдём инструкции
st X, r1 ;сохранить содержимое r1 в SRAM по адресу Х, где X – пара регистров r26, r27 ld r1,X ; загрузить в r1 содержимое SRAM по адресу Х, где X – пара регистров r26, r27
Становится понятно, что Х содержит указатель (адрес) и, оказывается, нет никакого злого дядьки, который придумал указатель, чтобы запудрить всем мозги, работа с указателями(адресами) поддерживается на уровне ядра МК.

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

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

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

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

Тип *имя;

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

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. Написать свои версии функций преобразования строки в число и числа в строку.

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

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

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

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

// 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 равен четырём.

Немного о памяти

Память можно представить по-разному.

Объяснение для военных на примере взвода. Есть взвод солдат. Численность - 30 человек. Построены в одну шеренгу. Если отдать им команду рассчитаться, у кажого в этой шеренге будет свой уникальный номер. Обязательно у каждого будет и обязательно уникальный. Этот взвод - доступная нам память. Всего нам здесь выделено для работы 30 ячеек. Можно использовать меньше. Больше - нельзя. К каждой ячейке можно обратиться и быть уверенным, что обратился именно к ней. Любому солдату можно дать что-то в руки. Например, цветы. То есть поместить по адресу данные.

Объяснение для Маленького Принца. Здравствуй, Маленький Принц. Представим, что твоему барашку стало одиноко. И ты попросил нарисовать ему друзей. Ты выделил для барашков целую планету (точнее, астероид) по соседству. Эта планета - доступная память. Вся она уставлена коробочками, в которых будут жить барашки. Чтобы не запутаться, все коробочки пронумерованы. Коробочки - это ячейки памяти. Барашек в коробочке - это данные. Допустим, что попался какой-то особо упитанный барашек. Ему понадобится две коробочки. Или даже больше. Барашек - неделимая структура (для нас с тобой, Маленький Принц, это точно так), а коробочки идут подряд. Нет ничего проще. Мы вынимает стенки между двумя рядом стоящими коробочками и кладем туда барашка. Места в коробочке не очень много. И барашек не может свободно развернуться. Поэтому мы всегда знаем, где его голова, а где хвост. И если нам что-то нужно будет сказать барашку, мы обратимся к той коробочке, где у него голова.

Объяснение для хулиганов. Есть забор. Забор из досок. Забор - доступная память. Доска - ячейка памяти. Забор длинный. И чтобы потом похвастаться друзьям, где ты сделал надпись, надо как-то обозначить место. Я знаю, о уважаемый хулиган, что ты нашел бы что-то поинтереснее, чем нумеровать каждую доску. Но в программировании не такие выдумщики. Поэтому доски просто пронумерованы. Возможно, твоя надпись поместится на одну доску. Например, %знак футбольного клуба%. Тогда ты просто скажешь номер и друзья увидят серьезность твоего отношения к футболу. А возможно, что одной доски не хватит. Ничего, главное, чтобы хватило забора. Пиши подряд. Просто потом скажи, с какой доски читать. А что если не подряд? Бывает и не подряд. Например, ты хочешь признаться Маше в любви. Ты назначаешь ей встречу под доской номер 40. Если все пройдет хорошо, ты возьмешь Машу и поведешь ее к доске 10, где заранее написал «Хулиган + Маша = любовь». Если что-то пошло не так, ты поведешь Машу к доске 60, на которой написано все нехорошее, что ты думаешь о Маше. Примерно так выглядит условный переход. То есть оба его исхода помещаются в память заранее. На каком-то этапе вычисляется условие. Если условие выполнилось - переходим к одному месту памяти и начинаем идти дальше подряд. Если условие не выполнилось - переходим к другому месту, с другими инструкциями. И тоже продолжаем выполнять их подряд. Инструкции всегда выполняются одна за другой, если только не встретился переход (с условием или без условия). Ну, или что-то поломалось.

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

В памяти хранятся числа. Ни с чем кроме чисел компьютер работать не умеет. Если вы поместили в память какую-то комплексную структуру, она все равно будет представлена числами. Даже если вы работаете с ней как со структурой. Примером комплексной структуры в терминах языков C и C++ может быть, например, экземпляр структуры или объект класса.

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

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

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

Система в компьютере двоичная (хотя есть и тернарные машины). В 1 байте 8 бит. Английское bit означает binary digit, то есть двоичный разряд. Получается, что байт может принимать числовые значения от 0 до 2 в 8 степени без единицы. То есть от 0 до 255. Если представлять числа в шестнадцатеричной системе, то от 0x00 до 0xFF.

Представим область памяти.

0x01 0x02 0x03 0x04
0x05 0x06 0x07 0x08
0x09 0x0A 0x0B 0x0C
0x0D 0x0E 0x0F 0x10

В ней лежат числа от 1 до 16. Направление обхода обычно задается слева направо и сверху вниз. Помните, что никакой таблицы на самом деле нет (почти как ложки в Матрице). Она нужна человеку для удобства восприятия. Каждая такая ячейка описывается двумя величинами: значением и адресом. В приведенной таблице значение и адрес совпадают.

Понятие указателя

Указатель - это переменная. Такая же, как и любая другая. Со своими «можно» и со своими «нельзя». У нее есть свое значение и свой адрес в памяти.

Значение переменной-указателя - адрес другой переменной. Адрес переменной-указателя свой и независимый.

Int *pointerToInteger;

Здесь объявляется переменная pointerToInteger. Ее тип - указатель на переменную типа int.

Немного лирики.

Как следует писать звездочку относительно типа и имени переменной? Встречаются, например, такие формы записи, и все они имеют право на существование:

Int* p1; int * p2; int *p3;

Аргументы за первую форму. Чтобы объявить переменную следует указать ее тип, а затем имя. Звездочка является частью типа, а не частью имени. Это также подтверждается тем, что при привидении типов пишется тип со звездочкой, а не тип отдельно. Следовательно, должна писаться слитно с типом. Минус в том, что при объявлении нескольких переменных после объявления int*, только первая из них будет указателем, а вторая будет просто переменной типа int. Не объявляйте несколько указателей в одной строчке. Это не очень хороший стиль.

Аргументы за вторую форму. Есть люди, которым нравится «когда код дышит» Они ставят пробел до скобок и после скобок. И здесь тоже ставят. Возможно, это просто такой компромисс.

Аргументы за третью форму. Если писать так, то с объявлением нескольких указателей в одной строчке проблем быть не должно (хотя это все равно плохой тон). Некоторая идеология нарушается. Но этот стиль - самый распространенный, так как точно видно, что переменная - указатель.

И помните, что компилятору все это безразлично.

Адрес переменной и значение переменной по адресу

Рассмотрим две переменные: целочисленную переменную x и указатель на целочисленную переменную.

Int x; int *p;

Чтобы получить адрес переменной, нужно перед ее именем написать амперсанд.

Данная конструкция будет выполняться справа налево. Сначала с помощью оператора &, примененного к переменной x, будет получен адрес x. Затем адрес x будет сохранен в указателе p.

Есть и обратная операция. Чтобы получить значение переменной по ее адресу, следует написать звездочку перед именем указателя.

Int y = *p;

Такая операция в русском языке называется не слишком благозвучным словом «разыменование». В английском - dereference.

В данном примере с помощью оператора * мы получим то значение, которое находится в памяти по адресу p. Затем мы сохраним его в переменную y. В итоге получится, что значения x и y совпадают.

Все это несложно увидеть на экране.

#include int main(void) { int x; int y; int *p; x = 13; y = 0; p = &x; y = *p; printf("Value of xt%d", x); printf("Address of xt%p", &x); printf("n"); printf("Value of pt%p", p); printf("Address of pt%p", &p); printf("n"); printf("Value of yt%d", y); printf("Address of yt%p", &y); printf("n"); return 0; }

В указанном примере значение x и y будут одинаковы. А также адрес x и значение p.

Адресная арифметика

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

Int array = {1, 2, 3, 4, 5}; int *p = &array; p++;

Первая строка простая и понятая. Объявлен массив и заполнен числами от 1 до 5.

Во второй строке объявляется указатель на int и ему присваивается адрес нулевого элемента массива. Некоторые компиляторы разрешают писать такое присвоение так, считая, что имя массива означает адрес его нулевого элемента.

Int *p = array;

Но если вы хотите избежать неоднозначности, пишите явно. Таким образом в p лежит адрес начала массива. А конструкция *p даст 1.

Третья строчка увеличивает значение p. Но не просто на 1, а на 1 * sizeof(int). Пусть в данной системе int занимает 4 байта. После увеличения p на 1, p указывает не на следующий байт, а на первый байт из следующей четверки байтов. Программисту не нужно думать в данном случае о размере типа.

С вычитанием ситуация такая же.

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

Конструкция array[i] будет преобразована компилятором к *(array + i). К начальному адресу массива будет прибавлено число с учетом размерности типа данных. А затем будет взято значение по вычисленному адресу. Обратите внимание, что никто не запрещает написать и так i. Ведь конструкция будет преобразована к виду...

С указателем можно складывать число, представленное переменной или целочисленной константой. Вычесть можно не только число, но и указатель из указателя. Это бывает полезно. А вот сложить два указателя, умножить или разделить указатель на число или на другой указатель - нельзя.

С указателями разных типов нельзя обходиться легкомысленно.

Char c; char *pc = &c; int *pi = pc;

С точки зрения языка C все корректно. А вот в C++ будет ошибка, потому что типы указателей не совпадают.

Int *pi = (int*)pc;

Вот такая конструкция будет принята C++.

Небольшое резюме.

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

Применение указателей

Обычно функция возвращает одно значение. А как вернуть больше одного? Рассмотрим код функции, которая меняет местами две переменные.

Int swap(double a, double b) { double temp = a; a = b; b = temp; }

Пусть есть переменные x и y с некоторыми значениями. Если выполнить функцию, передав в нее x и y, окажется, что никакого обмена не произошло. И это правильно.

При вызове этой функции в стеке будут сохранены значения x и y. Далее a и b получат значения x и y. Будет выполнена перестановка. Затем функция завершится и значения x и y будут восстановлены из стека. Все по-честному.

Чтобы заставить функцию работать так, как нужно, следует передавать в нее не значения переменных x и y, а их адреса. Но и саму функцию тогда нужно адаптировать для работы с адресами.

Void swap(double* a, double* b) { double temp = *a; *a = *b; *b = temp; }

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

Swap(&x, &y);

Теперь в функцию передаются адреса. И работа ведется относительно переданных адресов.

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

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

У тех, кто только начинает программировать на C, есть одна распространенная ошибка. При вводе с клавиатуры с помощью функции scanf() они передают значение переменной, а не ее адрес. А ведь scanf() должна менять значение переменной.

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

Немного посчитаем.

Пусть нам нужно передать в функцию целое число типа int. Таким образом мы передаем в функцию sizeof(int) байт. Обычно это 4 байта (размер будет зависеть от архитектуры компьютера и компилятора). 4 байта - не так много. 4 байта уйдут в стек. Потому что имеет место передача по значению.

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

Вообразим себя проектировщиками Большого Адронного Коллайдера. Вы отвечаете за безопасность системы. Именно вас окружают люди с недобрыми взглядами и факелами. Нужно показать им на модели, что конца света не будет. Для этого нужно передать в функцию collaiderModel(), скажем, 1 Гб данных. Представляете, сколько информации будет сохранено в стек? А скорее всего программа не даст вам стек такого объема без специальных манипуляций.

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

Указатели - это мощный инструмент. Указатели эффективны и быстры, но не слишком безопасны. Потому как вся ответственность за их использования ложится на разработчика. Разработчик - человек. А человеку свойственно ошибаться.

Представим ситуацию.

Int x; int *p;

В большинстве компиляторов C и С++ неинициализированные локальные переменные имеют случайное значение. Глобальные обнуляются.

Если мы захотим разыменовать указатель и присвоить ему значение, скорее всего, будет ошибка.

*p = 10;

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

Указатели можно и нужно обнулять. Для этого есть специальное значение NULL.

Int *p = NULL;

Это запись больше соответствует стилю C. В C++ обычно можно инициализировать указатель нулем.

Int *p = 0;

Ловкость рук и никакого мошенничества. На самом деле, если изучить библиотечные файлы языка, можно найти определение для NULL.

#define NULL (void*)0

Для C NULL - это нуль, приведенный к указателю на void. Для C++ все немного не так. Стандарт говорит: «The macro NULL is an implementation-defined C++ null pointer constant in this International Standard. Possible definitions include 0 and 0L, but not (void*)0». То есть это просто 0 или 0, приведенный к long.

Предлагаю вам такую задачку. Папа Карло дал Буратино 5 яблок. Злой Карабас Барабас отобрал 3 яблока. Сколько яблок осталось у Буратино?

Ответ: неизвестно. Так как нигде не сказано, сколько яблок у Буратино было изначально.

Мораль: обнуляйте переменные.

Ссылки

В языке C++ появился новый механизм работы с переменными - ссылки. Функция swap() была хороша, только не слишком удобно применять разыменование. С помощью ссылок функция swap() может выглядеть аккуратнее.

#include void swap(double& a, double& b) { double temp = a; a = b; b = temp; }

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

Swap(x, y);

Для взятия адреса переменной и для объявления ссылки используется одинаковый символ - амперсанд. Но в случае взятия адреса & стоит в выражении, перед именем переменной. А в случае объявления ссылки - в объявлении, после объявления типа.

Использование ссылок и указателей - это очень широкая тема. Описание основ на этом закончим.

За мысли и замечания спасибо Юрию Борисову,

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

Указатели, как и любые другие переменные, должны быть объявлены, прежде чем они будут использоваться. В операторе

int *countPtr, count;

объявляется переменная countPtrтипаint* (указатель на целочисленное значение). Символ * в объявлении распространяется только наcountPtr. Этот символ означает, что объявляемая переменная является указателем. Можно объявлять указатели, ссылающиеся на объекты любого типа.

Указатели должны быть инициализированы либо при объявлении, либо при помощи оператора присваивания. Указатель может быть инициализирован нулем, макросом NULLили значением адреса. Указатель со значениемNULLне указывает ни на что. Инициализация указателя значением 0 эквивалента инициализации указателя константойNULL, однако использованиеNULLпредпочтительнее. Когда присваивается значение 0, то происходит его преобразование к указателю соответствующего типа. Значение 0 является единственным целым числом, которой может быть присвоено переменной-указателю непосредственно.

Операции с указателями. Язык Си предлагает 5 основных операций, которые можно применить к указателям.

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

    Определения значения. Операция (*) выдает значение, хранящееся в указанной ячейке.

    Получение адреса указателя. Подобно любым переменным переменная типа указатель имеет адрес и значение. Операция & сообщает, где находится сам указатель.

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

5. Разность. Можно найти разность двух указателей. Обычно это делается для указателей, ссылающихся на элементы одного и того же массива; чтобы определить, на каком расстоянии друг от друга находятся элементы. Результат имеет тот же тип, что и переменная, содержащая размер массива.

К указателям можно применить арифметические операции, такие как: ++, --, +, +=, -, -= и можно вычислить разность двух указателей.

В качестве примера определим массив int v, первый элемент которого будет иметь адрес в памяти, равный 3000. Инициализируем указатель vPtr значением адреса v, т.е. значение vPtr равно 3000, любым из следующих операторов

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

Размер объекта в байтах зависит от типа объекта. Например, оператор

даст результат 3008 (3000+2*4), если для целого числа отводится в памяти 4 байта. Теперь vPtr будет ссылаться на элемент v.

Если бы vPtr был увеличен до значения 3016, которое соответствует адресу элемента массива v, то оператор

вернул бы vPtr к значению 3000, соответствующему началу массива. При увеличении или уменьшении указателя на единицу можно использовать операции инкремента (++) и декремента (--). Каждый из следующих операторов

увеличивает значение указателя, который будет ссылаться на следующий элемент массива. Любой из следующих операторов

уменьшает значение указателя, который получает при этом доступ к предыдущему элементу массива.

x = v2Ptr - vPtr;

переменной х будет присвоено число элементов массива, расположенных начиная с адреса vPtrи доv2Ptr; в данном случае это будет значение 2.

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

Передача параметра по ссылке. В Си для организации вызова по ссылке используются указатели и операция косвенной адресации. Если вызывается функция, аргументы которой должны изменяться, то в этом случае ей передаются адреса аргументов. Обычно для этой цели применяется операция взятия адреса (&) к переменной, значение которой будет изменяться. Когда адрес переменной передан функции, то для изменения ее значения может быть использована операция косвенной адресации (*). В следующей программе приведено использование передачи параметра по ссылке.

#include

{ int x=5, y=10;

printf(“x=%d y=%d\n”, x, y);

change(&x,&y); /* передача адресов функции */

printf(“x=%d y=%d\n”, x, y); }

change (int *u, int *v)

temp=*u; /*tempприсваивается значение, на которое указываетu*/

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

Данная функция изменяет значения переменных xиy. Путем передачи функции адресов переменных х и у мы предоставили ей возможность доступа к ним. Используя указатели и операцию (*), функция смогла извлечь величины, помещенные в соответствующие ячейки памяти, и менять их местами.

Основная ли тература: 1осн,2осн

Дополнительная литератур а: 9доп

Контрольные вопросы:

1. Назовите операции для работы с указателями?

2. На какое число увеличивается значение указателя при прибавлении из указателя целого числа?

3. Приведите пример инициализации переменной-указателя?

4. Приведите пример объявления переменной-указателя?

5. Какая операция используется для организации вызова по ссылке?