Вывести адрес переменной в Си без `printf`
Допустим у меня есть переменная, я записываю ее адрес в указатель. Как мне без printf вывести адрес указателя и адрес переменной которая хранится в указателе?
Отслеживать
13.7k 12 12 золотых знаков 43 43 серебряных знака 75 75 бронзовых знаков
задан 1 фев 2017 в 18:57
Георгий Соминский Георгий Соминский
13 1 1 серебряный знак 4 4 бронзовых знака
Получите адрес, это просто число — ну а уж число-то вывести сможете? Надеюсь, putc разрешена?
1 фев 2017 в 18:59
Используйте преобразование указателя в число: (unsigned)pointer и (unsigned)(&pointer) .
1 фев 2017 в 18:59
@Гоша Соминский Как вы собираетесь без функции вывода что-то вывести?
1 фев 2017 в 19:00
Вывести куда? В файл?На экран?
1 фев 2017 в 19:00
@bsuart, объясните в чём по-вашему разница между выводом в файл или на экран (контрольный вопрос: что такое таблица дескрипторов файлов и что там по умолчанию записано в первых трёх элементах?)
1 фев 2017 в 19:15
1 ответ 1
Сортировка: Сброс на вариант по умолчанию
В комментарий не втиснусь, так что в ответе. См. тут — http://ideone.com/jVhnOs
int main(int argc, const char * argv[]) < int a; printf("%p\n",&a); intptr_t p = (intptr_t)&a; char s[2*sizeof(p)]; for(int i = 2*sizeof(p)-1; i >= 0; --i) < s[i] = "0123456789ABCDEF"[p & 0x0F]; p >>= 4; > for(int i = 0; i < 2*sizeof(p); ++i) < putc(s[i],stdout); >>
Отслеживать
ответ дан 1 фев 2017 в 19:18
218k 15 15 золотых знаков 118 118 серебряных знаков 229 229 бронзовых знаков
Гуд, плюс. (и еще 4символа нужно для коммента)
1 фев 2017 в 19:56
А 64 бита пробовали?
1 фев 2017 в 22:41
@0andriy А поменять unsigned int на intptr_t самостоятельно слабо?
2 фев 2017 в 4:26
- c
- указатели
- Важное на Мете
Похожие
Подписаться на ленту
Лента вопроса
Для подписки на ленту скопируйте и вставьте эту ссылку в вашу программу для чтения RSS.
Дизайн сайта / логотип © 2023 Stack Exchange Inc; пользовательские материалы лицензированы в соответствии с CC BY-SA . rev 2023.11.29.1725
Нажимая «Принять все файлы cookie» вы соглашаетесь, что Stack Exchange может хранить файлы cookie на вашем устройстве и раскрывать информацию в соответствии с нашей Политикой в отношении файлов cookie.
Переменные, адреса и указатели
Переменная — это область памяти, к которой мы обращаемся за находящимися там данными, используя идентификатор (в данном случае, имя переменной). При этом у этой помеченной именем области есть еще и адрес, выраженный числом и понятный компьютерной системе. Этот адрес можно получить и записать в особую переменную. Переменную, содержащую адрес области памяти, называют указателем.
Когда мы меняем значение обычной переменной, то, можно сказать, просто удаляем из конкретной области памяти данные и записываем туда новые. Когда мы меняем значение переменной-указателя, то начинаем работать с совершенно иным участком памяти, т.к. меняем содержащийся в ней адрес.
Тема указателей тесно связана с темой динамических типов данных. Когда программа компилируется, то под объявленные переменные так или иначе (в зависимости от того, где они были объявлены) выделяются участки памяти. Потом размер этих участков не меняется, может меняться только их содержимое (значения или данные). Однако именно с помощью указателей можно захватывать и освобождать новые участки памяти уже в процессе выполнения программы. Динамические типы данных будут рассмотрены позже.
Прежде чем перейти к рассмотрению объявления и определения переменных-указателей, посмотрим, что из себя представляет адрес любой переменной и как его получить.
int i = 0; printf ("i=%d, &i=%p \n", i, &i);
В результате выполнения данного программного кода на экране появляется примерно следующее (шестнадцатеричное число у вас будет другим):
i=0, &i=0x7fffa40c5fac
Знак амперсанда (&) перед переменной позволяет получить ее адрес в памяти. Для вывода адреса переменной на экран используется специальный формат %p . Адреса обычных переменных (не указателей) в процессе выполнения программы никогда не меняются. В этом можно убедиться:
int a = 6; float b = 10.11; char c = 'k'; printf("%5d - %p\n", a, &a); printf("%5.2f - %p\n", b, &b); printf("%5c - %p\n", c, &c); a = 2; b = 50.99; c = 'z'; printf("%5d - %p\n", a, &a); printf("%5.2f - %p\n", b, &b); printf("%5c - %p\n", c, &c);
6 - 0x7fff653532e0 10.11 - 0x7fff653532e4 k - 0x7fff653532df 2 - 0x7fff653532e0 50.99 - 0x7fff653532e4 z - 0x7fff653532df
Как мы видим, несмотря на то, что значения переменных поменялись, ячейки памяти остались прежними.
Зная адрес, можно получить значение, которое находится по этому адресу, поставив знак * перед адресом:
int a = 8; printf("%d \n", *&a);
На экране будет выведено число 8.
Однако запись типа &a не всегда возможна или удобна. Поэтому существует специальный тип данных — указатели, которым и присваивается адрес на область памяти.
Указатели в языке C, как и другие переменные, являются типизированными, т.е. при объявлении указателя надо указывать его тип. Как мы узнаем позже, с указателями можно выполнять некоторые арифметические операции, и именно точное определение их типа позволяет протекать им корректно. Чтобы объявить указатель, надо перед его именем поставить знак *. Например:
int *pi; float *pf;
Обратите внимание на то, что в данном случае * говорит о том, что объявляется переменная-указатель. Когда * используется перед указателем не при его объявлении, а в выражениях, то обозначает совсем иное — «получить значение (данные) по адресу, который присвоен указателю». Посмотрите на код ниже:
int x = 1, y, z = 3; int *p, *q; p = &x; printf("%d\n", *p); // 1 y = *p; printf("%d\n", y); // 1 *p = 0; printf("%d %d\n", x, y); // 0 1 q = &z; printf("%d\n", *q); // 3 p = q; *p = 4; printf("%d\n", z); // 4 printf("%p %p\n", p, q); // p == q
С помощью комментариев указаны текущие значения ячеек памяти. Подробно опишем, что происходит:
- Выделяется память под пять переменных: три типа int и два указателя на int . В ячейки x и z записываются числа 1 и 3 соответственно.
- Указателю p присваивается адрес ячейки x. Извлекая оттуда значение ( *p ), получаем 1.
- В область памяти, которая названа именем у, помещают значение равное содержимому ячейки, на которую ссылается указатель p. В результате имеем две области памяти (x и y), в которые записаны единицы.
- В качестве значения по адресу p записываем 0. Поскольку p указывает на x, то значение xменяется. Переменная p не указывает на y, поэтому там остается прежнее значение.
- Указателю q присваивается адрес переменной z. Извлекая оттуда значение ( *q ), получаем 3.
- Переменной p присваивается значение, хранимое в q. Это значит, что p начинает ссылаться на тот же участок памяти, что и q. Поскольку q ссылается на z, то и p начинает ссылаться туда же.
- В качестве значения по адресу p записываем 4. Т.к. p является указателем на z, следовательно, меняется значение z.
- Проверяем, p и q являются указателями на одну и туже ячейку памяти.
Под сам указатель (там, где хранится адрес) также должна быть выделена память. Объем этой памяти можно узнать с помощью функции sizeof() :
int *pi; float *pf; printf("%lu\n", sizeof(pi)); printf("%lu\n", sizeof(pf));
Под указатели всех типов выделяется одинаковый объем памяти, т.к. размер адреса не зависит от типа, а зависит от вычислительной системы. В таком случае, зачем при объявлении указателя следует указывать тип данных, на который он может ссылаться? Дело в том, что по типу данных определяется, сколько ячеек памяти занимает значение, на которое ссылается указатель, и через сколько ячеек начнется следующее значение.
Если указатель объявлен, но не определен, то он ссылается на произвольный участок памяти с неизвестно каким значением:
int *pa, *pb; float *pc; printf(" %p %p %p\n", pa, pc, pb); // может возникнуть ошибка printf(" %d %f\n", *pa, *pc);
Результат (в Ubuntu):
0x400410 0x7fff5b729580 (nil) -1991643855 0.000000
Использование неопределенных указателей в программе при вычислениях чревато возникновением серьезных ошибок. Чтобы избежать этого, указателю можно присвоить значение, говорящее, что указатель никуда не ссылается (NULL). Использовать такой указатель в выражениях не получится, пока ему не будет присвоено конкретное значение:
int a = 5; float c = 6.98; int *pa; float *pc; pa = NULL; pc = NULL; printf(" %15p %15p\n", pa, pc); // Error // printf(" %15d %15f\n", *pa, *pc); pa = &a; pc = &c; printf(" %15p %15p\n", pa, pc); printf(" %15d %15f\n", *pa, *pc);
Результат (в Ubuntu):
(nil) (nil) 0x7ffd8e77e550 0x7ffd8e77e554 5 6.980000
В данном случае, если попытаться извлечь значение из памяти с помощью указателя, который никуда не ссылается, то возникает «ошибка сегментирования».
На этом уроке вы должны понять, что такое адрес переменной и как его получить ( &var ), что такое переменная-указатель ( type *p_var; p_var = &var ) и как получить значение, хранимое в памяти, зная адрес ячейки ( *p_var ). Однако у вас может остаться неприятный осадок из-за непонимания, зачем все это надо? Это нормально. Понимание практической значимости указателей придет позже по мере знакомства с новым материалом.
Практически проверьте результат работы всех примеров данного урока, придумайте свои примеры работы с указателями.
Курс с решением части задач:
pdf-версия
Указатели
Все определенные в программе данные, например, переменные, хранятся в памяти по определенному адресу. И указатели позволяют напрямую обращаться к этим адресам и благодаря этому манипулировать данными. Указатели представляют собой объекты, значением которых служат адреса других объектов (переменных, констант, указателей) или функций. Указатели — это неотъемлемый компонент для управления памятью в языке Си.
Определение указателя
Для определения указателя надо указать тип объекта, на который указывает указатель, и символ звездочки *.
тип_данных* название_указателя;
Сначала идет тип данных, на который указывает указатель, и символ звездочки *. Затем имя указателя.
Например, определим указатель на объект типа int:
int *p;
Пока указатель не ссылается ни на какой объект. Теперь присвоим ему адрес переменной:
int main(void) < int x = 10; // определяем переменную int *p; // определяем указатель p = &x; // указатель получает адрес переменной return 0; >
Получение адреса данных
Указатель хранит адрес объекта в памяти компьютера. И для получения адреса к переменной применяется операция & . Эта операция применяется только к таким объектам, которые хранятся в памяти компьютера, то есть к переменным и элементам массива.
Что важно, переменная x имеет тип int, и указатель, который указывает на ее адрес тоже имеет тип int. То есть должно быть соответствие по типу.
Какой именно адрес имеет переменная x? Для вывода значения указателя можно использовать специальный спецификатор %p :
#include int main(void) < int x = 10; int *p; p = &x; printf("%p \n", p); // 0060FEA8 return 0; >
В моем случае машинный адрес переменной x — 0x0060FEA8. (Для адресов в памяти применяется шестнадцатеричная система.) Но в каждом отдельном случае адрес может быть иным. Фактически адрес представляет целочисленное значение, выраженное в шестнадцатеричном формате.
То есть в памяти компьютера есть адрес 0x0060FEA8, по которому располагается переменная x.
Так как переменная x представляет тип int , то на большинстве архитектур она будет занимать следующие 4 байта (на конкретных архитектурах размер памяти для типа int может отличаться). Таким образом, переменная типа int последовательно займет ячейки памяти с адресами 0x0060FEA8, 0x0060FEA9, 0x0060FEAA, 0x0060FEAB.
И указатель p будет ссылаться на адрес, по которому располагается переменная x, то есть на адрес 0x0060FEA8.
Стоит отметить, что при выводе адреса указателя функция printf() ожидает, что указатель будет представлять void* , то есть указатель на значение типа void . Поэтому некоторые компиляторы при некоторых настройках могут при компиляции отображать предупреждения. И чтобы было все канонически правильно, то переданный указатель нужно преобразовать в указатель типа void * :
printf("%p \n", (void *)p);
Получение значения по адресу
Но так как указатель хранит адрес, то мы можем по этому адресу получить хранящееся там значение, то есть значение переменной x. Для этого применяется операция * или операция разыменования (dereference operator). Результатом этой операции всегда является объект, на который указывает указатель. Применим данную операцию и получим значение переменной x:
#include int main(void)
Address = 0060FEA8 x = 10
Используя полученное значение в результате операции разыменования мы можем присвоить его другой переменной:
int x = 10; int *p = &x; int y = *p; // присваиваем переменной y значение по адресу из указателя p printf("x = %d \n", y); // 10
Здесь присваиваем переменной y значение по адресу из указателя p , то есть значение переменной x .
И также используя указатель, мы можем менять значение по адресу, который хранится в указателе:
int x = 10; int *p = &x; *p = 45; printf("x = %d \n", x); // 45
Так как по адресу, на который указывает указатель, располагается переменная x, то соответственно ее значение изменится.
Создадим еще несколько указателей:
#include int main(void) < char c = 'N'; int d = 10; short s = 2; char *pc = &c; // получаем адрес переменной с типа char int *pd = &d; // получаем адрес переменной d типа int short *ps = &s; // получаем адрес переменной s типа short printf("Variable c: address=%p \t value=%c \n", (void*) pc, *pc); printf("Variable d: address=%p \t value=%d \n", (void*) pd, *pd); printf("Variable s: address=%p \t value=%hd \n", (void*) ps, *ps); return 0; >
В моем случае я получу следующий консольный вывод:
Variable c: address=0060FEA3 value=N Variable d: address=0060FE9C value=10 Variable s: address=0060FE9A value=2
По адресам можно увидеть, что переменные часто расположены в памяти рядом, но не обязательно в том порядке, в котором они определены в тексте программы:
C++: Указатели
Когда в инструменты добавлены оператор адреса и оператор косвенного обращения, можно поговорить об указателях. В этом уроке мы узнаем что такое указатели, как их объявлять и какие существуют базовые операции.
Объявление указателя
Указатель — это переменная, которая в качестве значения хранит адрес памяти.
Переменные-указатели объявляются так же, как обычные переменные. Только в этом случае ставится звездочка между типом данных и именем переменной. Эта звездочка не является косвенным обращением. Это часть синтаксиса объявления указателя:
int *i_ptr <>; // указатель на значение типа int double *d_ptr <>; // указатель на значение типа double int* i_ptr2 <>; // тоже допустимый синтаксис int * iPtr3<>; // тоже допустимый синтаксис (но не делайте так, это похоже на умножение)
Синтаксически C++ принимает звездочку рядом с типом данных, рядом с именем переменной или даже в середине.
При объявлении переменной-указателя нужно ставить звездочку рядом с типом, чтобы его было легче отличить от косвенного обращения.
Как и обычные переменные, указатели не инициализируются при объявлении. Если они не инициализированы значением, они будут содержать мусор.
Указатель X (где X – какой-либо тип) — это обычно используемое сокращение для «указателя на X». Поэтому, когда мы говорим «указатель int», мы на самом деле имеем в виду «указатель на значение типа int».
Хорошей практикой считается инициализировать указатель значением.
Присвоение значения указателю
Поскольку указатели содержат только адреса, когда мы присваиваем значение указателю, это значение должно быть адресом. Одна из самых распространенных вещей, которые делают с указателями, – это хранение в них адреса другой переменной.
Чтобы получить адрес переменной, мы используем оператор адреса:
#include int main() < int num < 5 >; int* ptr < &num >; // инициализируем ptr адресом переменной num std::cout
Эта программа создает следующий вывод:
0x7ffc5d336fc8 0x7ffc5d336fc8
ptr содержит адрес значения переменной, поэтому мы говорим, что ptr «указывает на» num .
Тип указателя должен соответствовать типу переменной, на которую он указывает:
int i_value < 5 >; double d_value < 7.0 >; int* i_ptr < &iValue >; // ok double* d_ptr < &dValue >; // ok i_ptr = &d_value; // ошибка
Тип double не может указывать на адрес переменной типа int . Следующее также некорректно:
int* ptr < 5 >;
Это связано с тем, что указатели могут содержать только адреса, а целочисленный литерал 5 не имеет адреса памяти. Если попробовать сделать это, компилятор сообщит, что он не может преобразовать int в указатель int .
Вопрос на засыпку: Можно ли инициализировать указатель, явно указав адрес ячейки памяти?
double* d_ptr< 0x0012FF7C >;
Ответ — нет, компиляция этого кода завершится с ошибкой! Хотя казалось бы, почему, ведь оператор адреса & , так же возвращает адрес? Тут есть отличие — оператор & возвращает тоже указатель.
Возвращение указателя оператором адреса
Оператор адреса (&) не возвращает адрес своего операнда в виде литерала. Вместо этого он возвращает указатель, содержащий адрес операнда, тип которого является производным от аргумента. Например, взятие адреса значения int вернет адрес в указателе int .
Мы можем увидеть это в следующем примере:
#include #include int main() < int num < 4 >; std::cout
В Visual Studio этот код напечатал:
При компиляции gcc вместо этого выводит «pi» («pointer to int», указатель на int).
Одной из основных операций является получение значения переменной через указатель — косвенное обращение.
Косвенное обращение через указатели
У нас есть переменная-указатель, которая указывает на что-то. Значит, другая распространенная вещь, которую мы делаем с ней, — это косвенное обращение через указатель. Это нужно, чтобы получить значение того, на что он указывает.
Косвенное обращение через указатель вычисляет содержимое адреса, на который он указывает:
int value < 5 >; std::cout ; // ptr указывает на value std::cout
Эта программа создает следующий вывод:
0x7ffcc0b6824c 5 0x7ffcc0b6824c 5
Без типа при косвенном обращении через указатель он не знал бы, как интерпретировать содержимое, на которое он указывает. По этой же причине тип указателя и тип переменной, адрес которой ему присваивается, должны совпадать. Если бы это было не так, косвенное обращение через указатель неверно интерпретировало бы биты как другой тип.
После присваивания значению указателя можно присвоить другое значение:
int value1< 5 >; int value2< 7 >; int* ptr<>; ptr = &value1; // ptr указывает на value1 std::cout
Когда адрес переменной value присваивается указателю ptr , верно следующее:
- ptr равен &value
- *ptr обрабатывается так же, как value
Поскольку *ptr обрабатывается так же, как value , можно присваивать ему значения, как если бы это была переменная value .
Следующая программа напечатает 7:
int value < 5 >; int* ptr < &value >; // ptr указывает на value *ptr = 7; // *ptr - это то же, что и value, которому присвоено 7 std::cout
Обратите внимание, через указатель мы можем работать с переменной value - получить значение, и даже изменить его.
Такой мощный механизм имеет свои минусы.
Предупреждение о косвенном обращении через недействительные указатели
Указатели в C++ по своей сути небезопасны. Неправильное использование указателей — один из лучших способов вывести приложение из строя.
Во время косвенного обращения через указатель приложение пытается перейти в ячейку памяти, которая хранится в указателе, и получить содержимое памяти. В целях безопасности современные операционные системы используют приложения-песочницы. Они предотвращают неправильное взаимодействие операционной системы с другими приложениями и защитить стабильность самой операционной системы.
Если приложение пытается получить доступ к области памяти, не выделенной ему операционной системой, операционная система может завершить работу приложения.
Следующая программа иллюстрирует это и вероятнее всего упадет с ошибкой:
#include // Мы рассмотрим & позже. Пока не беспокойтесь об этом. Мы используем его только для того, // чтобы заставить компилятор думать, что p имеет значение. void foo(int*&p) < // p — ссылка на указатель. Мы рассмотрим ссылки (и ссылки на указатели) позже в этой главе. // Мы используем ее, чтобы заставить компилятор думать, что p мог быть изменен, // поэтому он не будет жаловаться на то, что p неинициализирован. >int main() < int* p; // Создаем неинициализированный указатель (указывающий на мусор) foo(p); // Обманываем компилятор, заставляя его думать, что мы собираемся присвоить указателю допустимое значение std::cout
Для хранения указателей так же как и для обычных приманных выделяется память.
Размер указателей
Размер указателя зависит от архитектуры, для которой скомпилирован исполняемый файл — 32-битный исполняемый файл использует 32-битные адреса памяти. Следовательно, указатель на 32-битной машине занимает 32 бита (4 байта). С 64-битным исполняемым файлом указатель будет 64-битным (8 байтов). Это независимо от размера объекта, на который он указывает:
char* ch_ptr <>; // char равен 1 байту int* i_ptr <>; // int обычно равен 4 байтам std::cout
Размер указателя всегда один и тот же. Это связано с тем, что указатель — это просто адрес памяти, а количество битов, необходимых для доступа к адресу памяти на данной машине, всегда постоянно.
Что хорошего в указателях:
- Массивы реализованы с помощью указателей. Указатели могут использоваться для итерации по массиву
- Указатели в C++ — это единственный способ динамического выделения памяти
- Их можно использовать для передачи функции в качестве параметра другой функци
- Их можно использовать для достижения полиморфизма при работе с наследованием
- Их можно использовать, чтобы иметь указатель на одну структуру/класс в другой структуре/классе, чтобы сформировать цепочку. Это полезно в некоторых более сложных структурах данных, таких как связанные списки и деревья
В этом уроке мы познакомились с указателями, узнали как их объявлять, как присваивать им значения и как безопасно работать с ними.
Задание
Поменяйте значения переменных first_num и second_num местами. Попробуйте это сделать с помощью уже созданных указателей.
Упражнение не проходит проверку — что делать?
Если вы зашли в тупик, то самое время задать вопрос в «Обсуждениях». Как правильно задать вопрос:
- Обязательно приложите вывод тестов, без него практически невозможно понять что не так, даже если вы покажете свой код. Программисты плохо исполняют код в голове, но по полученной ошибке почти всегда понятно, куда смотреть.
В моей среде код работает, а здесь нет
Тесты устроены таким образом, что они проверяют решение разными способами и на разных данных. Часто решение работает с одними входными данными, но не работает с другими. Чтобы разобраться с этим моментом, изучите вкладку «Тесты» и внимательно посмотрите на вывод ошибок, в котором есть подсказки.
Мой код отличается от решения учителя
Это нормально , в программировании одну задачу можно выполнить множеством способов. Если ваш код прошел проверку, то он соответствует условиям задачи.
В редких случаях бывает, что решение подогнано под тесты, но это видно сразу.
Прочитал урок — ничего не понятно
Создавать обучающие материалы, понятные для всех без исключения, довольно сложно. Мы очень стараемся, но всегда есть что улучшать. Если вы встретили материал, который вам непонятен, опишите проблему в «Обсуждениях». Идеально, если вы сформулируете непонятные моменты в виде вопросов. Обычно нам нужно несколько дней для внесения правок.
Кстати, вы тоже можете участвовать в улучшении курсов: внизу есть ссылка на исходный код уроков, который можно править прямо из браузера.