Разыменование указателя си. Что значит в си: что такое указатель

Пожалуйста, приостановите работу AdBlock на этом сайте.

Указатель – переменная, в которой хранится адрес какого-либо объекта в памяти компьютера, например, другой переменной. Мы уже сталкивались раньше с адресами переменных, когда изучали функцию scanf.

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

Объявление указателя отличается от объявления переменной только добавлением символа * после названия типа. Примеры:

Листинг 1.

int * p_g; // указатель на переменную типа int double * p_f; // указатель на переменную типа double

Присвоить указателю какой-то адрес можно, используя оператор присваивания. Примеры:

Листинг 2.

int n = 100; double PI = 3.1415926; int * p_k; // указатель на переменную типа int double * p_pi; // указатель на переменную типа double p_k = &n; // получаем адрес переменной n и присваиваем его указателю p_k p_pi = Π // получаем адрес переменной PI и присваиваем его указателю p_pi

Для вывода значения указателя на экран нужно в функции printf использовать модификатор %p. Пример:

Листинг 3.

printf ("adres peremennoi PI %p\n", p_pi);

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

Листинг 4.

#include int main(void) { int a = 100; int * p_a = &a; // сохраняем в указатель адрес переменной a printf("a = %d\n", a); // стандартный способ получить значение переменной a printf("a = %d\n", *p_a); // получаем значение переменной a через указатель на неё // используя указатель p_a, записываем в переменную a другое значение *p_a = 50; printf("a = %d\n", *p_a); return 0; }

Рис.1 Доступ к переменной через указатель

Итого, * применительно к указателям используется в двух случаях:

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

Есть еще, так называемый, нулевой указательNULL. Нулевой указатель не ссылается никуда. Он используется, чтобы обнулять указатели. Посмотрите на пример.

Листинг 5.

#include int main(void) { int a = 100; int * p_a = &a; // сохраняем в указатель адрес переменной a printf("a = %d\n", a); // стандартный способ получить значение переменной a printf("a = %d\n", *p_a); // получаем значение переменной a через указатель на неё // используя указатель p_a, записываем в переменную a другое значение *p_a = 50; printf("a = %d\n", *p_a); printf("%p\n", p_a); p_a = NULL; printf("%p\n", p_a); return 0; }

Рис.2 Обнуление указателя

На главную

Язык Си на примерах

Функции в Си

Для чего нужны функции в C?

Функции в Си применяются для выполнения определённых действий в рамках общей программы. Программист сам решает какие именно действия вывести в функции. Особенно удобно применять функции для многократно повторяющихся действий.

Простой пример функции в Cи

Пример функции в Cи:

Это очень простая программа на Си. Она просто выводит строку «Functions in C». В программе имеется единственная функция под названием main. Рассмотрим эту функцию подробно. В заголовке функции, т.е. в строке

int – это тип возвращаемого функцией значения;

main - это имя функции;

(void) - это перечень аргументов функции. Слово void указывает, что у данной функции нет аргументов;

return – это оператор, который завершает выполнение функции и возвращает результат работы функции в точку вызова этой функции;

EXIT_SUCCESS - это значение, равное нулю. Оно определено в файле stdlib.h;

часть функции после заголовка, заключенная в фигурные скобки

{
puts(«Functions in C»);
return EXIT_SUCCESS;
}

называют телом функции.

Итак, когда мы работаем с функцией надо указать имя функции, у нас это main, тип возвращаемого функцией значения, у нас это int, дать перечень аргументов в круглых скобках после имени функции, у нас нет аргументов, поэтому пишем void, в теле функции выполнить какие-то действия (ради них и создавалась функция) и вернуть результат работы функции оператором return. Вот основное, что нужно знать про функции в C.

Как из одной функции в Cи вызвать другую функцию?

Рассмотрим пример вызова функций в Си:

Запускаем на выполнение и получаем:

В этом примере создана функция sum, которая складывает два целых числа и возвращает результат. Разберём подробно устройство этой функции.

Заголовок функции sum:

int sum(int a, int b)

здесь int - это тип возвращаемого функцией значения;

sum - это имя функции;

(int a, int b) - в круглых скобках после имени функции дан перечень её аргументов: первый аргумент int a, второй аргумент int b. Имена аргументов являются формальными, т.е. при вызове функции мы не обязаны отправлять в эту функцию в качестве аргументов значения перемнных с именами a и b. В функции main мы вызываем функцию sum так: sum(d, e);. Но важно, чтоб переданные в функцию аргументы совпадали по типу с объявленными в функции.

В теле функции sum, т.е. внутри фигурных скобок после заголовка функции, мы создаем локальную переменную int c, присваиваем ей значение суммы a плюс b и возвращаем её в качестве результата работы функции опрератором return.

Теперь посмотрим как функция sum вызывается из функции main.

Вот функция main:

Сначала мы создаём две переменных типа int

int d = 1; int e = 2;

их мы передадим в функцию sum в качестве значений аргументов.

int f = sum(d, e);

её значением будет результат работы функции sum, т.е. мы вызываем функцию sum, которая возвратит значение типа int, его-то мы и присваиваем переменной f. В качестве аргументов передаём d и f. Но в заголовке функции sum

int sum(int a, int b)

аргументы называются a и b, почему тогда мы передаем d и f? Потому что в заголовке функций пишут формальные аргументы, т.е. НЕ важны названия аргументов, а важны их типы. У функции sum оба аргумента имеют тип int, значит при вызове этой функции надо передать два аргумента типа int с любыми названиями.

Ещё одна тонкость. Функция должна быть объявлена до места её первого вызова. В нашем примере так и было: сначала объявлена функция sum, а уж после мы вызываем её из функции main. Если функция объявляется после места её вызова, то следует использовать прототип функции.

Прототип функции в Си

Рассмотрим пример функциив Си:

В этом примере функция sum определена ниже места её вызова в функции main.

В таком случае надо использовать прототип функции sum. Прототип у нас объявлен выше функции main:

int sum(int a, int b);

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

int f = sum(d, e);

а ниже функции main определяем функцию sum, которая предварительно была объявлена в прототипе:

Чем объявление функции в Си отличается от определения функции в Си?

Когда мы пишем прототип функции, например так:

int sum(int a, int b);

то мы объявляем функцию.

А когда мы реализуем функцию, т.е. записываем не только заголовок, но и тело функции, например:

то мы определяем функцию.

Оператор return

Оператор return завершает работу функции в C и возвращает результат её работы в точку вызова. Пример:

Эту функцию можно упростить:

здесь оператор return вернёт значение суммы a + b.

Операторов return в одной функции может быть несколько. Пример:

Если в примере значение аргумента a окажется больше двух, то функция вернет ноль (первый случай) и всё, что ниже комментария «// Первый случай;» выполнятся не будет.

Указатели в языке Си

Если a будет меньше двух, но b будет меньше нуля, то функция завершит свою работу и всё, что ниже комментария «// Второй случай;» выполнятся не будет.

И только если оба предыдущих условия не выполняются, то выполнение программы дойдёт до последнего оператора return и будет возвращена сумма a + b.

Передача аргументов функции по значению

Аргументы можно передавать в функцию C по значению. Пример:

В примере, в функции main, создаём переменную int d = 10. Передаём по значению эту переменную в функцию sum(d). Внутри функции sum значение переменной увеличивается на 5. Но в функции main значение d не изменится, ведь она была передана по значению. Это означает, что было передано значение переменной, а не сама переменная. Об этом говорит и результат работы программы:

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

Передача указателей функции Си

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

В этом варианте программы я перешел от передачи аргумента по значению к передаче указателя на переменную. Рассмотрим подробнее этот момент.

printf(«sum = %d\n», sum(&d));

в функцию sum передается не значение переменной d, равное 10-ти, а адрес этой переменной, вот так:

Теперь посмотрим на функцию sum:

Аргументом её является указатель на int. Мы знаем, что указатель - это переменная, значением которой является адрес какого-то объекта. Адрес переменной d отправляем в функцию sum:

Внутри sum указатель int *a разыменовывается. Это позволяет от указателя перейти к самой переменной, на которую и указывает наш указатель. А в нашем случае это переменная d, т.е. выражение

равносильно выражению

Результат: функция sum изменяет значение переменной d:

На этот раз изменяется значение d после возврата из sum, чего не наблюдалось в предыдущм пункте, когда мы передавали аргумент по значению.

C/C++ в Eclipse

Все примеры для этой статьи я сделал в Eclipse. Как работать с C/C++ в Eclipse можно посмотреть здесь. Если вы работаете в другой среде, то примеры и там будут работать.

Теги: Си указатели. Указатель на указатель. Тип указателя. Арифметика указателей. Сравнение указателей.

Указатели

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

Определение

Указатель – это переменная, которая хранит адрес области памяти.

Тема 7. Указатели в Си.

Указатель, как и переменная, имеет тип. Синтаксис объявления указателей

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

Например
Два основных оператора для работы с указателями – это оператор & взятия адреса, и оператор * разыменования. Рассмотрим простой пример.

#include #include void main() { int A = 100; int *p; //Получаем адрес переменной A p = &A; //Выводим адрес переменной A printf(«%p\n», p); //Выводим содержимое переменной A printf(«%d\n», *p); //Меняем содержимое переменной A *p = 200; printf(«%d\n», A); printf(«%d», *p); getch(); }

Рассмотрим код внимательно, ещё раз

Была объявлена переменная с именем A . Она располагается по какому-то адресу в памяти. По этому адресу хранится значение 100.

Создали указатель типа int .

Теперь переменная p хранит адрес переменной A . Используя оператор * мы получаем доступ до содержимого переменной A .
Чтобы изменить содержимое, пишем

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

#include #include void main() { int A = 100; int *a = &A; double B = 2.3; double *b = &B; printf(«%d\n», sizeof(A)); printf(«%d\n», sizeof(a)); printf(«%d\n», sizeof(B)); printf(«%d\n», sizeof(b)); getch(); }

Будет выведено
Несмотря на то, что переменные имеют разный тип и размер, указатели на них имеют один размер. Действительно, если указатели хранят адреса, то они должны быть целочисленного типа. Так и есть, указатель сам по себе хранится в переменной типа size_t (а также ptrdiff_t ), это тип, который ведёт себя как целочисленный, однако его размер зависит от разрядности системы. В большинстве случаев разницы между ними нет. Зачем тогда указателю нужен тип?

Арифметика указателей

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

Для их выполнения необходимо знать размер.
операция сдвигает указатель вперёд на байт.
Например, если указатель int *p; хранит адрес CC02, то после p += 10; он будет хранить адрес СС02 + sizeof(int)*10 = CC02 + 28 = CC2A (Все операции выполняются в шестнадцатиричном формате). Пусть мы создали указатель на начало массива. После этого мы можем «двигаться» по этому массиву, получая доступ до отдельных элементов.

#include #include void main() { int A = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; int *p; p = A; printf(«%d\n», *p); p++; printf(«%d\n», *p); p = p + 4; printf(«%d\n», *p); getch(); }

Заметьте, каким образом мы получили адрес первого элемента массива

Массив, по сути, сам является указателем, поэтому не нужно использовать оператор &. Мы можем переписать пример по-другому

Получить адрес первого элемента и относительно него двигаться по массиву.
Кроме операторов + и — указатели поддерживают операции сравнения. Если у нас есть два указателя a и b, то a > b, если адрес, который хранит a, больше адреса, который хранит b.

#include #include void main() { int A = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; int *a, *b; a = &A; b = &A; printf(«&A == %p\n», a); printf(«&A == %p\n», b); if (a < b) { printf(«a < b»); } else { printf(«b < a»); } getch(); }

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

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

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

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

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

#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(); }

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

Так как указатель хранит адрес, можно кастовать его до другого типа.

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

#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(); }

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

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

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

int *ptr = NULL;

По стандарту гарантировано, что в этом случае указатель равен NULL , и равен нулю, и может быть использован как булево значение false . Хотя в зависимости от реализации NULL может и не быть равным 0 (в смысле, не равен нулю в побитовом представлении, как например, int или float ).
Это значит, что в данном случае

int *ptr = NULL; if (ptr == 0) { … }

вполне корректная операция, а в случае

int a = 0; if (a == NULL) { … }

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

#include #include #include void main() { int *a = NULL; unsigned length, i; printf(«Enter length of array: «); scanf(«%d», &length); if (length > 0) { //При выделении памяти возвращается указатель. //Если память не была выделена, то возвращается NULL if ((a = (int*) malloc(length * sizeof(int))) != NULL) { for (i = 0; i < length; i++) { a[i] = i * i; } } else { printf(«Error: can’t allocate memory»); } } //Если переменая была инициализирована, то очищаем её if (a != NULL) { free(a); } getch(); }

Примеры

Теперь несколько примеров работы с указателями
1. Пройдём по массиву и найдём все чётные элементы.

#include #include void main() { int A = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; int even; int evenCounter = 0; int *iter, *end; //iter хранит адрес первого элемента массива //end хранит адрес следующего за последним «элемента» массива for (iter = A, end = &A; iter < end; iter++) { if (*iter % 2 == 0) { even = *iter; } } //Выводим задом наперёд чётные числа for (—evenCounter; evenCounter >= 0; evenCounter—) { printf(«%d «, even); } getch(); }

2. Когда мы сортируем элементы часто приходится их перемещать. Если объект занимает много места, то операция обмена местами двух элементов будет дорогостоящей. Вместо этого можно создать массив указателей на исходные элементы и отсортировать его. Так как размер указателей меньше, чем размер элементов целевого массива, то и сортировка будет происходить быстрее. Кроме того, массив не будет изменён, часто это важно.

#include #include #define SIZE 10 void main() { double unsorted = {1.0, 3.0, 2.0, 4.0, 5.0, 6.0, 8.0, 7.0, 9.0, 0.0}; double *p; double *tmp; char flag = 1; unsigned i; printf(«unsorted array\n»); for (i = 0; i < SIZE; i++) { printf(«%.2f «, unsorted[i]); } printf(«\n»); //Сохраняем в массив p адреса элементов for (i = 0; i < SIZE; i++) { p[i] = &unsorted[i]; } do { flag = 0; for (i = 1; i

3. Более интересный пример. Так как размер типа char всегда равен 1 байт, то с его помощью можно реализовать операцию swap – обмена местами содержимого двух переменных.

#include #include #include void main() { int length; char *p1, *p2; char tmp; float a = 5.0f; float b = 3.0f; printf(«a = %.3f\n», a); printf(«b = %.3f\n», b); p1 = (char*) &a; p2 = (char*) &b; //Узнаём сколько байт перемещать length = sizeof(float); while (length—) { //Обмениваем местами содержимое переменных побайтно tmp = *p1; *p1 = *p2; *p2 = tmp; //не забываем перемещаться вперёд p1++; p2++; } printf(«a = %.3f\n», a); printf(«b = %.3f\n», b); getch(); }

В этом примере можно поменять тип переменных a и b на double или любой другой (с соответствующим изменением вывода и вызова sizeof ), всё равно мы будет обменивать местами байты двух переменных.

4. Найдём длину строки, введённой пользователем, используя указатель #include #include void main() { char buffer; char *p; unsigned length = 0; scanf(«%127s», buffer); p = buffer; while (*p != ‘\0’) { p++; length++; } printf(«length = %d», length); getch(); }

Обратите внимание на участок кода

while (*p != ‘\0’) { p++; length++; }

его можно переписать

while (*p != 0) { p++; length++; } или while (*p) { p++; length++; }

или, убрав инкремент в условие

while (*p++) { length++; }

ru-Cyrl18-tutorialSypachev [email protected]

Указатель - это специальная переменная, которая хранит адрес другой переменной. Указатель объявляется следующим образом: тип* переменная; где тип - любой допустимый как простой, так и составной базовый тип указателя.

Например, пусть объявлена обычная переменная int t; Объявление и инициализация int* p= &t; означают следующее. В переменной p будетхраниться не обрабатываемое программой целое число (оценка студента, количество выпущенной продукции и т. п.), а адрес ячейки, в которой будет находиться информация указанного типа (целое число). Под адресом будем понимать номер первого байта выделенного для переменной участка оперативной памяти. Для переменных, не являющихся указателями, без дополнительного объявления адрес также запоминается системой, и его можно получить с помощью операции & (разадресации), например, &t. Эта унарная операция, которую иногда называют “взятие адреса”, ничего не делает со значением переменной t .

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

Один из способов показан выше и означает, что в переменную p помещается адрес ячейки t . Важно понять, что int* p= &t; равносильно int* p; p=&t ; а не *p=&t ; В этом заключается одна из трудностей начального этапа изучения указателей. Эта тема усложняется ещё и тем, что такой же символ “& ” используется при объявлении переменной ссылочного типа.

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

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

Заметим, что расстановка пробелов при объявлении указателей свободная. Допустимы также следующие записи: int * p= & t; int *p= &t; Предпочтение следовало бы отдать записи в начале параграфа, из которой легче понять смысл указателя. Объявляется переменная p , а не *p , и, кроме этого, типом является int* , а не int .

Если одновременно объявляется несколько указателей, то символ “*” надо писать перед каждой переменной: float* q1, *q2;

Содержимое ячейки, адрес которой находится в p , в тексте программы обозначается с помощью операции разыменование . Для неё используется тот же символ “*”, что и при объявлении переменной-указателя. Эта унарная операция возвращает значение переменной, находящейся по указанному адресу. Поэтому *p - это обрабатываемое программой целое число, находящееся в ячейке, адрес которой - в переменной-указателе p . С учётом инициализации (p = &t ) *p и t - это одно и то же значение. Значит, если с помощью cin>>t; введём, например, число 2 и выполним *p*=5 ; или *p=*p*5; то изменится и величина t , хотя, казалось бы, не было явного её изменения. Поэтому оператор cout << t; выведет число 10 (2*5). И наоборот, изменив t (например, t++; ), этим самым мы изменим и значение *p. С помощью cout<<(*p); выведем 11.

Сказанное выше будем обозначать так:

p (или &t ) *p (или t )

В “левом прямоугольнике” (ячейке памяти) находится адрес, а в ячейке “справа” - обрабатываемое целое число.

Рассматриваемые здесь операции “&” и ”*” являются унарными и имеют более высокий приоритет по сравнению с аналогичными бинарными операциями “битовое и” и арифметическое умножение.

Для *p определены те же операции, что и для переменной указанного типа, у нас - для целых чисел. Поэтому допустимы, например, следующие операторы: а) cin>>(*p); b) int r; r=*p*2; c) if (*p%2)…; d) cout<<(*p);.

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

⇐ Предыдущая567891011121314Следующая ⇒

Дата публикования: 2015-02-18; Прочитано: 526 | Нарушение авторского права страницы

Studopedia.org — Студопедия.Орг — 2014-2018 год.(0.001 с)…

— указатель на Работника. Вы можете назначить один выделенный объект этому указателю или, в вашем случае, несколько (с синтаксисом массива). Таким образом, он указывает на массив сотрудников.

Вы разыменовали этот указатель.

Обозначения и предположения

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

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

в словах низкого уровня означает: возьмите указатель, добавьте раз размер указанного типа (чтобы он указывал на -ную запись) и разыменовал этот адрес, Это означает, что фактически является Employee в -ом индексе (но не в указателе).

Затем вы хотите получить доступ к члену этого сотрудника.

Если он все еще был указателем, вам нужно будет использовать оператор стрелки, но поскольку вы использовали оператор индекса массива (), вы уже разыменовали его, то оператор точки правильный:

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

Указатели похожи на метки, которые ссылаются на места в памяти. Они тоже имеют адрес, а их значение является адресом некоторой другой переменной. Переменная, объявленная как указатель, занимает 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;

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

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


/* Ребят, в статье было найдено много ошибок. Спасибо тем людям, которые внесли свои замечания. В связи с этим - после прочтения статьи обязательно перечитайте комментарии */

1. Общие сведения

Итак, что же такое указатель? Указатель - это та же переменная, только инициализируется она не значением одного из множества типов данных в C++, а адресом, адресом некоторой переменной, которая была объявлена в коде ранее. Разберем на примере:

Void main(){ int i_val = 7; }
# Здесь ниже, конечно, я ребятки вам соврал. Переменная i_val - статическая, она явно будет размещена в стеке. В куче место выделяется под динамические объекты. Это важные вещи! Но в данном контексте, я, сделав сам себе замечание, позволю оставить себе все как есть, так что сильно не ругайтесь.

Мы объявили переменную типа int и здесь же ее проинициализировали. Что же произойдет при компиляции программы? В оперативной памяти, в куче, будет выделено свободное место такого размера, что там можно будет беспрепятственно разместить значение нашей переменной i_val . Переменная займет некоторый участок памяти, разместившись в нескольких ячейках в зависимости от своего типа; учитывая, что каждая такая ячейка имеет адрес, мы можем узнать диапазон адресов, в пределах которого разместилось значение переменной. В данном случае, при работе с указателями нам нужен лишь один адрес - адрес первой ячейки, именно он и послужит значением, которым мы проинициализируем указатель. Итак:

Void main(){ // 1 int i_val = 7; int* i_ptr = &i_val; // 2 void* v_ptr = (int *)&i_val }
Используя унарную операцию взятия адреса & , мы извлекаем адрес переменной i_val и присваиваем ее указателю. Здесь стоит обратить внимание на следующие вещи:

  1. Тип, используемый при объявлении указателя в точности должен соответствовать типу переменной, адрес которой мы присваиваем указателю.
  2. В качестве типа, который используется при объявлении указателя, можно выбрать тип void . Но в этом случае при инициализации указателя придется приводить его к типу переменной, на которую он указывает.
  3. Не следует путать оператор взятия адреса со ссылкой на некоторое значение, которое так же визуально отображается символом & .
Теперь, когда мы имеем указатель на переменную i_va l мы можем оперировать ее значением не только непосредственно с помощью самой переменной, но и с помощью указателя на нее. Посмотрим, как это работает на простом примере:

#include using namespace std; void main(){ int i_val = 7; int* i_ptr = &i_val; // выведем на экран значение переменной i_val cout << i_val << endl; // C1 cout << *i_ptr << endl; // C2 }

  1. Здесь все ясно - используем саму переменную.
  2. Во втором случае - мы обращаемся к значению переменной i_val через указатель. Но, как вы заметили, мы не просто используем имя указателя - здесь используется операция разыменования: она позволяет перейти от адреса к значению.
В предыдущем примере был организован только вывод значения переменной на экран. Можем ли мы непосредственно через указатель оперировать с значением переменной, на которую он указывает? Да, конечно, для этого они и реализованы (однако, не только для этого - но об этом чуть позже). Все, что нужно - сделать разыменование указателя:

(*i_ptr)++; // результат эквивалентен операции инкремента самой переменной: i_val++ // т.е. в данном случае в i_val сейчас хранится значение не 7, а 8.

2. Массивы

Сразу перейдем к примеру - рассмотрим статичный одномерный массив определенной длинны и инициализируем его элементы:

Void main(){ const int size = 7; // объявление int i_array; // инициализация элементов массива for (int i = 0; i != size; i++){ i_array[i] = i; } }
А теперь будем обращаться к элементам массива, используя указатели:

Int* arr_ptr = i_array; for (int i = 0; i != size; i++){ cout << *(arr_ptr + i) << endl; }
Что здесь происходит: мы инициализируем указатель arr_ptr адресом начала массива i_array . Затем, в цикле мы выводим элементы, обращаясь к каждому с помощью начального адреса и смещения. То есть:

*(arr_ptr + 0) это тот же самый нулевой элемент, смещение нулевое (i = 0),
*(arr_ptr + 1) - первый (i = 1), и так далее.

Однако, здесь возникает естественный вопрос - почему присваивая указателю адрес начала массива, мы не используем операцию взятия адреса? Ответ прост - использование идентификатора массива без указания квадратных скобок эквивалентно указанию адреса его первого элемента. Тот же самый пример, только в указатель «явно» занесем адрес первого элемента массива:

Int* arr_ptr_null = &i_array; for (int i = 0; i != size; i++){ cout << *(arr_ptr_null + i) << endl; } Пройдем по элементам с конца массива:
int* arr_ptr_end = &i_array; for (int i = 0; i != size; i++){ cout << *(arr_ptr_end - i) << endl; } Замечания:

  1. Запись array[i] эквивалентна записи *(array + i ). Никто не запрещает использовать их комбинированно: (array + i ) - в этом случае смещение идет на i , и еще на единичку. Однако, в данном случае перед выражением (array + i ) ставить * не нужно. Наличие скобок это «компенсирует.
  2. Следите за вашими „перемещениями“ по элементам массива - особенно если вам захочется использовать порнографический такой метод записи, как (array + i)[j].

3. Динамическое выделение памяти

Вот та замечательная плюшка, из-за которой я использую указатели. Начнем с динамических массивов. Зачастую при решении какой-либо задачи возникает потребность в использовании массива неопределенного размера, то есть размер этот заранее неизвестен. Здесь нам на помощь приходят динамические массивы - память под них выделяется в процессе выполнения программы. Пример:

Int size = -1; // здесь происходят какие - то // действия, которые изменяют // значение переменной size int* dyn_arr = new int;
Что здесь происходит: мы объявляем указатель и инициализируем его началом массива, под который выделяется память оператором new на size элементов. Следует заметить, что в этом случае мы можем использовать те же приемы в работе с указателями, что и с статическим массивом. Что следует из этого извлечь - если вам нужна какая - то структура (как массив, например), но ее размер вам заранее неизвестен, то просто сделайте объявление этой структуры, а проинициализируете ее уж позже. Более полный пример приведу чуть позже, а пока что - рассмотрим двойные указатели.

Что такое указатель на указатель? Это та же переменная, которая хранит адрес другого указателя „более низкого порядка“. Зачем он нужен? Для инициализации двумерного динамического массива, например:

Const int size = 7; // двумерный массив размером 7x7 int** i_arr = new int*; for(int i = 0; i != size; i++){ i_arr[i] = new int; }
А тройной указатель? Трехмерный динамический массив. Неинтересно, скажите вы, так можно продолжать до бесконечности. Ну хорошо. Тогда давайте представим себе ситуацию, когда нам нужно разместить динамические объекты какого-нибудь класса MyClass в двумерном динамическом массиве. Как это выглядит (пример иллюстрирует исключительно использование указателей, приведенный в примере класс никакой смысловой нагрузки не несет):

Class MyClass{ public: int a; public: MyClass(int v){ this->a = v; }; ~MyClass(){}; }; void main(){ MyClass*** v = new MyClass**; for (int i = 0; i != 7; i++){ v[i] = new MyClass*; for (int j = 0; j != 3; j++){ v[i][j] = new MyClass(i*j); } } } Здесь два указателя нужны для формирования матрицы, в которой будут располагаться объекты, третий - собственно для размещения там динамических объектов (не MyClass a , а MyClass* a ). Это не единственный пример использования указателей такого рода, чуть ниже будут рассмотрены еще примеры.

4. Указатель как аргумент функции

Для начала создадим два динамических массива размером 4x4 и проинициализируем их элементы некоторыми значениями:

Void f1(int**, int); void main(){ const int size = 4; // объявление и выделение памяти // под другие указатели int** a = new int*; int** b = new int*; // выделение памяти под числовые значения for (int i = 0; i != size; i++){ a[i] = new int; b[i] = new int; // собственно инициализация for (int j = 0; j != size; j++){ a[i][j] = i * j + 1; b[i][j] = i * j - 1; } } } void f1(int** a, int c){ for (int i = 0; i != c; i++){ for (int j = 0; j != c; j++){ cout.width(3); cout << a[i][j]; } cout << endl; } cout << endl; }
Функция f1 выводит значения массивов на экран: первый ее аргумент указатель на двумерный массив, второй - его размерность (указывается одно значение, потому как мы условились для простоты работать с массивами, где количество строк совпадает с количеством столбцов).

Задача : заменить значения элементов массива a соответствующими элементами из массива b , учитывая, что это должно произойти в некоторой функции, которая так или иначе занимается обработкой массивов. Цель: разобраться в способе передачи указателей для их дальнейшей модификации.

  1. Вариант первый. Передаем собственно указатели a и b в качестве параметров функции:

    Void f2(int** a, int** b, int c){ for (int i = 0; i != c; i++){ for (int j = 0; j != c; j++){ a[i][j] = b[i][j]; } } } После вызова данной функции в теле main - f2(a, b, 4) содержимое массивов a и b станет одинаковым.

  2. Вариант второй. Заменить значение указателя: просто присвоить значение указателя b указателю a.

    Void main(){ const int size = 4; // объявление и выделение памяти // под другие указатели int** a = new int*; int** b = new int*; // выделение памяти под числовые значения for (int i = 0; i != size; i++){ a[i] = new int; b[i] = new int; // собственно инициализация for (int j = 0; j != size; j++){ a[i][j] = i * j + 1; b[i][j] = i * j - 1; } } // Здесь это сработает a = b; }
    Однако, нам интересен случай, когда массивы обрабатываются в некоторой функции. Что первое приходит на ум? Передать указатели в качестве параметров нашей функции и там сделать то же самое: присвоить указателю a значение указателя b . То есть реализовать следующую функцию:

    Void f3(int** a, int** b){ a = b; } Сработает ли она? Если мы внутри функции f3 вызовем функцию f1(a, 4) , то увидим, что значения массива действительно поменялись. НО: если мы посмотрим содержимое массива a в main - то обнаружим обратное - ничего не изменилось. Так в чем же причина? Все предельно просто: в функции f3 мы работали не с самим указателем a , а с его локальной копией! Все изменения, которые произошли в функции f3 - затронули только локальную копию указателя, но никак не сам указатель a . Давайте посмотрим на следующий пример:

    Void false_eqv(int, int); void main(){ int a = 3, b = 5; false_eqv(a, b); // Поменялось значение a? // Конечно же, нет } false_eqv(int a, int b){ a = b; } Итак, я думаю, вы поняли, к чему я веду. Переменной a нельзя присвоить таким образом значение переменной b - ведь мы передавали их значения напрямую, а не по ссылке. То же самое и с указателями - используя их в качестве аргументов таким образом, мы заведомо лишаем их возможности изменения значения.
    Вариант третий, или работа над ошибками по второму варианту:

    Void f4(int***, int**); void main(){ const int size = 4; int** a = new int*; int** b = new int*; for (int i = 0; i != 4; i++){ a[i] = new int; b[i] = new int; for (int j = 0; j != 4; j++){ a[i][j] = i * j + 1; b[i][j] = i * j - 1; } } int*** d = &a; f4(d, b); } void f4(int*** a, int** b){ *a = b; }
    Таким образом, в main"е мы создаем указатель d на указатель a , и именно его передаем в качестве аргумента в функцию замены. Теперь, разыменовав d внутри f4 и приравняв ему значение указателя b , мы заменили значение настоящего указателя a , а не его локальной копии, на значение указателя b .

    Кстати, а чего это мы создаем динамические объекты? Ну ладно размер массива не знали, а экземпляры классов мы зачем динамическими делали? Да потому что зачастую, созданный нами объекты свое - они генерились, порождали новые данные/объекты для дальнейшей работы, а теперь пришло им время... умереть [фу, как грубо] уйти со сцены. И как мы это сделаем? Просто:

    Delete(a); delete(b); // Вот и кончились наши двумерные массивы delete(v); // Вот и нет больше двумерного массива с динамическими объектами delete(dyn_array); // Вот и удалился одномерный массив

  3. На данной ноте я хотел бы закончить свое повествование. Если найдется хотя бы пара ребят, которым понравится стиль изложения материала, то я постараюсь продолжить… ой, да кого я обманываю, мне нужен инвайт и все на этом, дайте инвайт и вашим глазам больше не придется видеть это околесицу. Шучу, конечно. Ругайте, комментируйте.

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

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

Объяснение для военных на примере взвода. Есть взвод солдат. Численность - 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);

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

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

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