Указатели в C/C++ сложная вещь для новичков. Без указателей программировать серьезные программы на С/С++ просто нельзя. Кроме того, указатели помогут понять как устроена память в компьютере, и как с ней работает процессор.Поэтому я сегодня попытаюсь объяснить вам доходчиво как они работают и зачем нужны. Прошу под кат.В первой части я расскажу об общем устройстве указателей, как они работают и зачем нужны.И так, начнем с базовой теории. Указатель это переменная указывающая на область памяти. Немножко кода:
int var1; //переменная типа int int* pointer1; // указатель на переменную типа int float* pointer2; //указатель на переменную типа float
С синтаксисом все понятно, достаточно просто поставить звездочку после типа данных. Возникает вопрос, а на что указывает указатель? Ответ прост — не на что. Почему? Потому, что мы его только объявили, но не указали на что ему указывать. Как все запутанно, правда? Когда мы объявляем переменную компьютер выделяет память для этой переменной в стеке. То есть код:
int myVar1;
Указывает компьютеру, что нужно выделить память для переменной myVar1 типа int. Размер памяти выделяемой для нее зависит от самого компьютера. Например на 32-х разрядном компьютере он равен 4 байтам(32 бит). Когда мы пишем:
myVar1 = 25;
Компьютер значение 25 записывает по адресу выделенном для переменной myVar1; Когда мы пишем например:
printf("%i", myVar1);
Компьютер обращается по адресу переменной myVar1, берет оттуда значение и выводит его на экран.
Указатель же — это тоже переменная. И компьютер так же выделяет для нее память. Но в ней хранится адрес памяти. Что бы это проверить, сделаем следующее:
int* pointer; printf("%p", pointer);
Программа объявляет указатель, а потом выводит его содержимое.Формат «%p» выводит на экран адрес памяти. Пока что, указатель указывает на случайный адрес, так как мы только создали, но не объявили ему на что указывать.
Примечание: при создании любых переменных(и указателей в том числе) в них находится мусор, так как компьютер не занимается очисткой области памяти, которая отводиться для переменных.
При создании указателей советуется делать так:
int* pointer = NULL; printf("%p", pointer);
NULL — это так называемый «нулевой» указатель. Это поможет избежать ошибок. Так как в случайном адресе может храниться все что угодно. Кстати, для условных операторов NULL означает ложь. Напишем две программы. Первая будет выглядеть так:
#include <stdio.h> int main() { int* pointer; if (pointer) printf("%pn", pointer); else printf("Указатель не определен!n"); return 0; }
А вторая так же, только заменим строку объявления указателя:
#include <stdio.h> int main() { int* pointer = NULL //инициализируем указатель if (pointer) printf("%pn", pointer); else printf("Указатель не определен!n"); return 0; }
Откомпилируйте и запустите. В первом случае на экран выведеться адрес памяти, а во втором «Указатель не определен». Такое простое правило, как инициализация указателей NULL’ом и проверка через if(или любой другой условный оператор) помогает избежать ошибок.
Примечание: не ициализированный указатель содержит в себе мусор, так же как и не инициализированная переменная. Но он намного опаснее. К примеру, по адресу который случайно попал в не инициализированный указатель может лежать какая нибудь системная информация, и попытка с помощью указателя ее изменить может привести к непредвиденным последствиям, в плоть до краха ядра ОС.
Раз указатель указывает на переменную, то есть хранит в себе ее адрес памяти, значит можно как то получить ее адрес? Да, все верно. Адрес переменной получается с помощью символа «&», например:
int var1; printf("%p", &var1);
При выполнении этого кода, выведется в консоль адрес переменной var1. Как вы уже наверно догадались, в указатель можно записать адрес переменной. Например так:
#include <stdio.h> int main() { int* pointer = NULL; int var1; pointer = &var1; printf("Адрес переменной var1 %pn", &var1); printf("Указатель pointer %pn", pointer); return 0; }
Программа выведет на экран:
Примечание: у вас будут другие значения адресов, так как они выделяются во время работы ОС и программы, вероятность того, что мой компьютер сделает это так же, как и ваш стремится к нулю!
Что нам это дает? Ну есть у нас указатель на переменную, там ее адрес памяти, что с ним делать? С помощью указателя, мы можем изменить саму переменную, например так:
#include <stdio.h> int main() { int* pointer = NULL; int var1; pointer = &var1; printf("Адрес переменной var1 %pn", &var1); printf("Указатель pointer %pn", pointer); var1 = 10; //установим значение переменной printf("Значение переменной var1 %in", var1); *pointer = 25; //установим значение переменной с помощью указателя printf("Значение переменной var1 %in", var1); printf("Значение по указателю %in", *pointer); return 0; }
Программа выведет на экран:
Как видите, переменной можно управлять с помощью указателя. И кстати, количество указателей не ограничено. Можно хоть сотню создать и все они могут указывать на одну и туже переменную. Указатели начинают приносить пользу, когда функция должна изменить значение какой-то переменной, переданной ей в качестве аргумента. Например наша функция func будет принимать 3 аргумента, первые два складывает, а значение записывает в третий, функция main создаст 3 переменные и передаст их функции:
#include <stdio.h> void func(int a, int b, int sum) { sum = a + b; } int main() { int var1 = 10, var2 = 20; //переменные со значением int mySum = 0; //в эту переменную мы запишем значение var1 и var2 func(var1, var2, mySum); printf("%in", mySum); return 0; }
Компилируем, запускаем. Наш ждет разочарование:
Попробуем найти ошибку. Добавим пару printf функций в нашу программу:
#include <stdio.h> void func(int a, int b, int sum) { sum = a + b; printf("Значение sum внутри функции func %in", sum); } int main() { int var1 = 10, var2 = 20; //переменные со значением int mySum = 0; //в эту переменную мы запишем значение var1 и var2 func(var1, var2, mySum); printf("Зачение mySum в функции main %in", mySum); return 0; }
Компилируем, запускам, получается:
А все потому, что функция во время передачи ей переменных, просто создала копии. То есть a копия var1, b копия var2, ну и sum копия mySum. После выхода из функции переменные a, b, sum просто уничтожаются. Что же делать? Выход есть! Изменим нашу функцию:
#include <stdio.h> void func(int a, int b, int* sum) { *sum = a + b; printf("Значение sum внутри функции func %in", *sum); }
Теперь наша функция принимает два аргумента типа int, а третий указатель на тип int. Функцию main изменим вот так:
int main() { int var1 = 10, var2 = 20; int mySum = 0; func(var1, var2, &mySum); //передаем адрес mySum printf("Зачение mySum в функции main %in", mySum); return 0; }
Откомпилируем и запустим:
Все работает!
И так, подведем итоги:
- Указатель — это переменная, хранящая адрес памяти;
- Указатель может хранить адрес другой переменной, с его помощью можно менять значение переменной. Если быть более точным, меняется значение лежащее по указанному адресу;
- Указатели при создании желательно инициализировать либо NULL либо необходимым адресом;
- Указатели могут быть опасны для программы/системы, если с ними работать невнимательно.
В следующей части я расскажу как можно выделять память под указатель, зачем это нужно, как можно обойтись без переменных и почему массивы тесно связаны с указателями.
Здраствуйте у меня к вам три вопроса:
void func(int a, int b, int* sum)
{
*sum = a + b;
printf("Значение sum внутри функции func %in", *sum);
}
1)третий аргумент функции int* sum содержит в себе адрес переменной?
2)знак * перед sum сообщает программе,что нужно обратиться к переменной?
3)функция имеет тип void,который не возвращает значения,как и почему задали такой тип?
P.S Да вопросы немного детские, но мне надо с чего то начинать…
1. Да. Объявление вида TYPE * Variable говорит программе о том, что переменная будет содержать в себе адрес другой переменной с типом TYPE ( то есть указатель на нее).
2. Да. Если вы создали указатель, а потом при операциях с ним используете знак * (звездочка), то программа воспользуется значением переменной по указателю.
3. Тип void используется для функции тогда, когда не нужно что бы она значения возвращала. Как видите, В примере в этом нет необходимости, поэтому я объявил ее как void.
Спасибо,как оказалось все просто и ясно)))
Да, так в большинстве случаев. Пишите еще, спрашивайте.
А почему указатель в этой функции не присвоен адрес, это опасно тоже получаеться)
А вторая так же, только заменим строку объявления указателя:
?
1
2
3
4
5
6
7
8
9
10
11
#include
int main()
{
int* pointer = NULL //инициализируем указатель
if (pointer)
printf(«%pn», pointer);
else
printf(«Указатель не определен!n»);
return 0;
}
В этом примере пропущено » ; » после int* pointer = NULL
Если пользоваться gcc for Ubuntu, то для перевода строки лучше использовать «\n»
Спасибо.
Пожалуйста.
В Unix, Linux системах по стандарту POSIX можно использовать перенос строки «\n». Стандарт ANSI — «\r\n»;
Если я конечно же не ошибаюсь.
А за ошибку — спасибо :).
Жутко непонятный пример. А не проще ли переменные сделать глобальными? Просто вынести их за рамки main и функции? Ведь так будет в сто раз легче и не будет никаких копий создаваться. Или сделать функцию int и присваивать ей результат. Тоже проще, чем через указатели. Так что полезного применения я тут не увидел. А пример ваще не понял.
Инструмент реализации выбирается исходя из задачи. Жаль, что мои примеры показались вам не удачными, но они как раз существуют для того, что бы понять как работают указатели. Понятное дело, что задачи в примерах настолько просты, что можно было бы их решить и без указателей. Но дело в том, что когда вы в жизни столкнетесь с определенной задачей, возможно ее можно эффективно решить, используя указатели. Но если вы не знаете как с ними работать — вы не сможете ее решить. Возможно даже не сможете додуматься до того, что ее можно решить используя указатели.
Все, когда учились программировать, читали книги по программированию. И я уверен, что большая часть информации воспринималась с непониманием и вставал вопрос «зачем это нужно?» Так вот. «Зачем» приходит с опытом. Если вы знаете, хотя бы образно, какую-то технологию, решая задачу — вы можете понять, что эта технология вам необходима. А вот если вы о ней ничего не знаете, то возможно будете изобретать велосипед для решения задачи.
Надеюсь донес до вас свою мысль.
Спасибо за статью! Вы мне очень помогли!
Спасибо. Я понял теперь их! Преследовали меня как демоны ) я, чтобы не мучаться, работал через массив. Я понимаю, что это те же указатели, но все же было как-то проще.тяжело использовать то, что не понимаешь. Теперь проблема решена 🙂 ура!
Я рад за вас °_°
Рад, что вам помогло. Теперь я веду блог на английском: https://jakeroid.com/blog/
Ничего не понятно. Бред из учебников. Так и не понятно как их использовать
Кому-то понятно, а кому-то нет.
Думаю сильно тут зависит от вас.
Теперь я веду английский блог, может быть вам будет интересно и понятнее: https://jakeroid.com/blog/
Спасибо автору за добрый посыл!
Но к сожалению у меня в visual studio 2017 все эти линуксовые прибамбасы не работают.
Ошибки выдает на этапе компиляции. А жаль, хоть и дельная статья, да ставить из-за нее линукс что-то не тянет.
В принципе, там разница то не большая между линуксом и виндовсом. Изучите вопрос и попробуйте еще раз.
Кстати, зацените мой блог на английском.
Сколько грамматических ошибок в статье! Это внушает сомнение в солидности сайта.
Извините, но я не умею писать внимательно. Для меня это сложнее, чем программирование.