Указатель в языке Си : обозначение и использование

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

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

Указатели широко используются в программировании на языке Си. Указатели часто используются при работе с массивами.

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

Каждая переменная в памяти имеет

  • свой адрес — номер первой ячейки, где она расположена;
  • свое значение.

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

Указатель, как и любая переменная, должен быть объявлен. Общая форма объявления указателя

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

Тип указателя— это тип переменной, адрес которой он содержит.

Для работы с указателями в Си определены две операции:

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

Например,

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

Для указанного примера обращение к одним и тем же значениям переменной и адреса представлено в таблице

Переменная Указатель
Адрес &c p
Значение c *p

Пример на Си

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include <stdlib.h>
int main()
{
  int a, *b;
  system("chcp 1251");
  system("cls");
  a = 134;
  b = &a;
  // %x = вывод числа в шестнадцатеричной форме
  printf("\n Значение переменной a равно %d = %x шестн.", a,a);
  printf("\n Адрес переменной a равен %x шестн.", &a);
  printf("\n Данные по адресу указателя b равны %d = %x шестн.", *b,*b);
  printf("\n Значение указателя b равно %x шестн.", b);
  printf("\n Адрес расположения указателя b равен %x шестн.", &b);
  getchar();
  return 0;
}

Результат выполнения программы:

Использование указателя
Расположение в памяти переменной a и указателя b:
Расположение в памяти переменной и указателя

Компиляторы высокого уровня поддерживают прямой способ адресации: младший байт хранится в ячейке, имеющей младший адрес.

35 комментариев к “Указатель в языке Си”

  1. Алексей

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

    1. Елена Вставская

      Представить адрес как указатель и записать значение по этому адресу:

      1
      *((int*)0x8000)=1;

      В 4 байта, начиная с адреса 0x8000, будет записано значение 1.

  2. Владимир

    Добрый вечер.

    Задача такая. Есть две функции, одна кодирует строку из восьми символов, другая раскодирует то что было закодировано (используется алгоритм шифрования/дешифрования TEA). Шифрация и дешифрация проходят успешно. Проблема такая: функция дешифрации uint32_t* decrypt(uint32_t* v, uint32_t* k)возвращает указатель на массив uint32_t* decoded = decrypt(plain, key);
    В возвращаемом массиве 2 тридцатидвухбитных числа, которые и есть результат раскодирования. Как имея эти два числа получить символьную строку чтобы было видно, что строки на входе и на выходе совпадают. Строку надо поместить в массив из восьми элементов типа char.

  3. Владимир

    Добрый день.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    // Функция кодирования текста

    uint32_t* encrypt(uint32_t* v, uint32_t* k)
    {                                          
        uint32_t v0 = v[0];
        uint32_t v1 = v[1];
        uint32_t sum = 0;

        /* a key schedule constant */
        uint32_t delta = 0x9e3779b9;

        /* cache key */
        uint32_t k0 = k[0];
        uint32_t k1 = k[1];
        uint32_t k2 = k[2];
        uint32_t k3 = k[3];
        uint32_t i;

        /* basic cycle start */
        for (i = 0; i < 32; i++)
        {
            sum += delta;
            v0 += ((v1 << 4) + k0) ^ (v1 + sum) ^ ((v1 >> 5) + k1);
            v1 += ((v0 << 4) + k2) ^ (v0 + sum) ^ ((v0 >> 5) + k3);
        }
        /* end cycle */

        v[0] = v0;
        v[1] = v1;

        j = 0;

        return v; // Возвращаем указатель на нулевой элемент массива зашифрованного числа

    }

    // Функция декодирования текста

    uint32_t* decrypt(uint32_t* v, uint32_t* k)
    {

        /* set up */
        uint32_t v0 = v[0];
        uint32_t v1 = v[1];
        uint32_t sum = 0xC6EF3720;
        uint32_t i;

        /* a key schedule constant */
        uint32_t delta = 0x9e3779b9;

        /* cache key */
        uint32_t k0 = k[0];
        uint32_t k1 = k[1];
        uint32_t k2 = k[2];
        uint32_t k3 = k[3];        

        /* basic cycle start */
        for (i = 0; i < 32; i++)
        {                              
            v1 -= ((v0 << 4) + k2) ^ (v0 + sum) ^ ((v0 >> 5) + k3);
            v0 -= ((v1 << 4) + k0) ^ (v1 + sum) ^ ((v1 >> 5) + k1);
            sum -= delta;
        }
        /* end cycle */

        v[0] = v0;
        v[1] = v1;

        return v;

    }

    uint32_t* plain;
    char shelf1[8]; // В массив записан текст из 8-символов
    char shelf2[8];

    plain = (uint32_t*)shelf1; // Загружаем текст в plain
    uint32_t* encoded = encrypt(plain, key); // Шифруем текст

    uint32_t* decoded = decrypt(plain, key); // Расшифровываем текст

    Правильно я понимаю, что функция decrypt возвращает указатель на первый элемент массива v ?

    Как расшифрованный текст поместить в символьный массив shelf2 ?

    1. Елена Вставская

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

      1. Владимир

        Массив v имеет тип uint32_t. В двух элементах этого массива содержаться два числа. Как эти два числа преобразовать в текстовый массив ?

        1. Елена Вставская

          1
          2
          3
          4
          5
          6
          7
          8
          9
          10
          11
          #include <stdio.h>
          int main()
          {
            int a[2] = { 0x31323334, 0x35363738 };
            printf("%d %d\n", a[0], a[1]);
            char* s = (char*)a;
            for (int i = 0; i < 8; i++)
              printf("%d ", s[i]);
            getchar();
            return 0;
          }
          1. Владимир

            Елена, вы в какой среде программы на C пишите ?

          2. Владимир

            1
            2
            3
            char key_buffer[KEY_SIZE];
            uint32_t* key;
            key = (uint32_t*)key_buffer;

            В последней строке текста, как я понимаю, в указатель key загружается адрес первого элемента массива key_buffer.
            Елена, а зачем перед key_buffer прописано (uint32_t*) ? Что это означает ?

          3. Елена Вставская

            Это явное переопределение типа указателя

  4. Владимир

    Добрый день.

    В программе есть такой фрагмент кода

    1
    2
    uint32_t p[] = {0xDEADBEEF, 0xCAFEBABE};
    uint32_t* plain = p;

    plain — это указатель. Прежде чем использовать указатель надо определить его значение, т.е. занести туда адрес переменной. В программе значение p не задано. В связи с этим возникает вопрос:

    В операторе uint32_t* plain = p; p куда пишется ?

    1. Елена Вставская

      p — это адрес начала массива. Можно напечатать

      1
      printf("%p",p);

      чтобы узнать его значение

  5. Сергей

    Есть такая часть кода SysMemBootJump = (void (*)(void)) (*((uint32_t *)(addr + 4))); Я понимаю что этот метод позволяет выполнить прыжок в другую область памяти, начиная с addr + 4. Всё прекрасно работает. Но мне не понятны эти действия с указателями, помогите, пожалуйста, разобраться с этим механизмом

    1. Елена Вставская

      Значение, которое хранится по адресу addr+4, используется как указатель на uint32_t (адрес), и идёт обращение к значению по этому адресу, которое, в свою очередь, является адресом функции, не принимающей аргументов и возвращающей указатель, который присваивается SysMemBootJump

  6. Владимир

    Запутался совсем. Надо организовать многоуровневое меню. У каждого пункта в корневом (0) уровне имеется свой индивидуальный набор подпунктов. Надо как-то универсально по индексам работать с 1м и последующими уровнями и чтобы не расходовать память впустую.
    у меня есть такие описатели:
    char * menu1_1[4] = { "пункт1", "пункт2", "абвгд", "иклмн" };
    char * menu1_2[2] = { "пункт10", "пункт14" };
    char * menu1_3[6] = { "п/п2", "пунктХ", "абвгд", "п/п3", "п/п4", "п/п5"};
    char * menu1_4[26] = { "п/п4", … , "п/п30"}; // я тут сократил многоточием
    char *** mnu1; // вот здесь вот проблема
    //например если в корневом меню выбран 3й пункт, тогда
    mnu1 = &menu1_3; // и здесь проблема
    mnu1_length = 6;
    чтобы потом получить индексированный доступ к нужным строкам наподобие
    i=2;
    t14.txt = **mnu1; // должен дать указатель на строку "пунктХ". Всем моим функциям надо кормить только указатели на строки для работы.
    можно конечно использовать многомерные массивы (как у меня сейчас), но получается ощутимый расход памяти впустую (большая система меню).
    Нигде не нашел никакого подобного решения на си.

    1. Елена Вставская

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

      Возможно, ещё тип или какие-то вспомогательные поля.
      У верхнего уровня id родителя 0, остальные id нумеруются с 1. Сначала строим главное меню с id 0. Если выбран какой-то пункт, строим вложенное меню из тех пунктов, которые имеют родителем выбранный пункт

      1. Владимир

        Ну да. И каждый раз эту базу полностью перелопачивать чтобы обеспечить скроллинг, либо делать вспомогательные массивы (текущее меню и порядок входа). Плюс расход на каждую строку текста по 2 id, ну и еще 2 id, показывающие предыдущий и последующий пункт меню.
        В общем как я понял указателями в си такую задачу не решить (указатель на массив указателей на строки). Только структура+массивы+индексы…

  7. Тимур

    Я тоже мучался с указателями 3 месяца. Понял только сегодня!

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

    1
    2
    int a=100; 
    int *p=&a; // я думал, что адрес переменной а присваивается "*p" (звездочке p).

    То есть я думал, что *p — "звездочкаП" это некое неделимое имя, что НЕВЕРНО!

    Все сразу понимается четко и верно, как только поставить пробел между звездочкой и p!!!

    1
    2
    int a=100; 
    int * p= &a; 

    Здесь сразу понимаешь, что int * это определитель типа данных (который, кстати можно записать и как int*), а вот после того как тип данных определен, уже идет присваивание к имени p адреса переменной a, что естественно, так как имени p присвоен тип указатель (который может хранить только адрес), прохождение к этому адресу ведет к значению int, лежащему по этому адресу.

    Я это долго не мог понять, пока не наткнулся на двойные типы даных, такие как long long и мне сразу стала понятна конструкция int *p=&a; просто еще раз говорю, для лучшего понимания ставьте мысленно пробел между *_и_p, это сразу будет наглядно и понятно! int *p=&a; превращается в int * p=&a;

    Надеюсь это поможет многим в понимании как понять запись указателей!

    1. Сергей

      Не надо никаких "мысленных" пробелов! Я всегда пишу так: int* p = &a; и сразу видно что тип p — это именно "указатель на int".

  8. Я как понял int a[]; и int *arr; это одно и тоже? Почему тогда такая запись возможна: int *arr[]; а такая нет int **arr; это же должно соответствовать записи int arr[][];
    Хотелось бы, чтобы вы прокомментировали все эти 5 типов записей, и как они взаимосвязаны

    1. Елена Вставская

      Следует различать статические массивы, которые описываются с помощью квадратных скобок, и динамические массивы, для описания которых используются указатели.
      Для статического массива память выделяется в момент объявления. Для динамического массива — с помощью специальных функций динамического выделения памяти (malloc(), new).

      1
      2
      3
      4
      5
      int arr[10]; // одномерный статический массив из 10 элементов типа int
      int *arr; // указатель, которому может быть присвоен адрес начала динамического массива
      int *arr[10]; // массив из 10 указателей, каждому из которых должен быть присвоен адрес своей выделенной области памяти
      int **arr; // указатель на указатель, то есть массив указателей (см.строчку выше), размер которого тоже будет определен позднее
      int arr[10][10]; // двумерный статический массив
  9. Владик

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

    vladislav@My-Comp:~/Рабочий стол/CodeC$ gcc pointers.c -o pointers.exe
    pointers.c: In function ‘main’:
    pointers.c:12:62: warning: format ‘%x’ expects argument of type ‘unsigned int’, but argument 2 has type ‘int *’ [-Wformat=]
    printf("\n Адрес переменной a равен %x шестн.", &a);
    ~^ ~~
    %ls
    pointers.c:14:66: warning: format ‘%x’ expects argument of type ‘unsigned int’, but argument 2 has type ‘int *’ [-Wformat=]
    printf("\n Значение указателя b равно %x шестн.", b);
    ~^
    %ls
    pointers.c:15:85: warning: format ‘%x’ expects argument of type ‘unsigned int’, but argument 2 has type ‘int **’ [-Wformat=]
    ntf("\n Адрес расположения указателя b равен %x шестн.", &b);
    ~^ ~~

    1. Елена Вставская

      Судя по всему, Ваш компилятор в шестнадцатеричном виде желает выводить только числа типа unsigned int. Чтобы не было ошибки, нужно явно привести все указатели к этому типу.

      1
      printf("\n Адрес переменной a равен %x шестн.", (unsigned int)(&a));
  10. Такой вопрос : Зачем нужны указатели ?
    это как ссылка на объект в других высокоуровневых языках?

    1. Елена Вставская

      Указатель — это переменная, значением которой является адрес.
      Часто указатели применяются, например, для обращения к динамически выделенной памяти.
      Ссылка — это не то же самое. Ссылка — это ещё одно имя объекта, а сама по себе ссылка объектом не является.

  11. Сергей

    Здравствуйте Елена, из прочитанного понял, что символ "звездочка" (*)применяется в Си в различных операциях: "объявление указателя", "присваивание значения через указатель", "умножение". На ваш взгляд в этом какой то смысл, может необходимость, или просто создатели Си так захотели и все тут.
    Спасибо за понятные для "начинающих" уроки.

      1. Сергей

        Несомненно вы правы, что при изучении необходимо строго придерживаться синтаксиса языка, но в том то и дело, что изучая указатели я вижу, что строго определенного синтаксиса то и нет, эту звездочку (*)толкают куда кому вздумается при объявлении указателя, что сбивает с толку человека начавшего изучать Си:

        1
        2
        3
        int *p;
        int* p; 
        int * p;

        Какой же это синтаксис,это полная анархия:)
        Было бы неплохо услышать ваш комментарий по этому поводу, или ссылку.
        Спасибо.

        1. Елена Вставская

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

    1. Елена Вставская

      Потому что шестнадцатеричное число 0x86 равно десятичному 134.

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Прокрутить вверх