Язык Си. Указатели

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

Указатель хранит адрес объекта, указанного типа.
  char   *p_ch;
  double *p_dbl;
В примере выше объявлено 2 указателя, первый хранит адрес объекта с типом char, второй хранит адрес объекта с типом double.
Для доступа к данным по этому указателю, есть такая операция, которая называется разименованием *.
void init_integer_obj(int *apIntObj)
{
  // apIntObj - хранит адрес объекта
  // например 0xFA0102FF
  
  // *apIntObj - операция разименования
  // получаем доступ к данным которые находятся
  // по указанному выше(как пример) адресу
  *apIntObj = 100; 
}
Также есть операция взятия адреса у объектов - &.
Операция &D возвращает указатель на объект D(адрес объекта D).
#include <stdio.h>
int main()
{
  int   i = 10;
  int *pi = &i;
  printf("value of object   i = %i\n", i);
  printf("address of object i = %p\n", pi);
  return 0;
}
В примере выше, мы объявили объект типа int с именем i, проинициализировали его значением 10, объект размещен в автоматической(локальной, на стеке, ...) памяти. Значит у него есть адрес. В следующей строке мы объявляем объект типа указатель на int,  с именем pi, и инициализируем его адресом созданного выше объектом, посредством операции взятия адреса &.
Так как мы знаем, что функция либо возвращает значение определенного типа либо не возвращает ничего. Но что если нам нужно вернуть несколько значений? Значит нам нужна функция которая на вход принимает адреса объектов, меняет у себя внутри, и после завершения работы функции, объекты изменены. Смотрите пример ниже.
void get_some_values_a(int aIn, int aOut1, int aOut2) // NOT CORRECT
{
  // для входящих параметров 'aIn', 'aOut1', 'aOut2'
  // будут созданы локальные копии, если мы их изменим внутри
  // функции, эти изменения никак не затронут те объекты которые
  // мы передали как аргументы, так как это КОПИИ
  aOut1 = aIn * 10;
  aOut2 = aIn * 100;
}

void get_some_values_b(int aIn, int *aOut1, int *aOut2) // CORRECT
{
  // для входящего параметра 'aIn'
  // будет создана локальная копия
  // но для 'aOut1', 'aOut2' будут созданы только копии адресов,
  // грубо говоря эти объекты передаются по адресу, соответственно
  // копий не делается, и с помощью операций разименования
  // мы можем изменить значения переданных объектов
  *aOut1 = aIn * 10;
  *aOut2 = aIn * 100;
}

int main()
{
  int i = 1;
  int i1 = 0;
  int i2 = 0;

  get_some_values_a(i, i1, i2);
  // i, i1, i2 - имеют те же значения что
  // и до вызова функции

  get_some_values_b(i, &i1, &i2);
  // i1, i2 - имеют уже не те же значения что
  // и до вызова функции

  return 0;
}

#include <stdio.h>
int main()
{
  int array[10] = {0};

  printf("addr of array - %p\n", array);
  printf("addr of first array element - %p\n", &array[0]);
  return 0;
}
Код выше, выведет одинаковые адреса, так как объект массива с именем array по факту является указателем, который хранит адрес начала массива, и он совпадает с адресом первого элемента.
Размер указателя, то есть ячейки для хранения адреса равен разрядности системы, для 32-х битных это 32 бита(4 байта) а для 64-х это 64 бита(8 байт). От сюда и ограничение(4GB) на максимальный объем оперативной памяти для 32-х битных систем, в которых мы можем адресоваться в диапазоне 0 ... 2^32 (0 ... 4294967296).
Также работает адресная арифметика.
#include <stdio.h>
int main()
{
  char *pch = 0x00;
  pch++;    printf("addr - %p\n", pch);
  pch += 2; printf("addr - %p\n\n", pch);

  short *psh = 0x00;
  psh++;    printf("addr - %p\n", psh);
  psh += 2; printf("addr - %p\n\n", psh);

  double *pd = 0x00;
  pd++;    printf("addr - %p\n", pd);
  pd += 2; printf("addr - %p\n\n", pd);

  void *pv = 0x00;
  pv++;    printf("addr - %p\n", pv);
  pv += 2; printf("addr - %p\n\n", pv);
  return 0;
}
Арифметика над указателями почти ничем не отличается от арифметики с обычными числами, при арифметике с указателями учитывается тип указателя. Тип указателя говорит компилятору о том сколько единиц информации занимает объект хранящийся по этому указателю. Посмотрите на пример выше, и найдите там pch++, так вот, компилятор видя эту запись, смотрит на тип указателя, а он char, значит pch указывает на данные у которых минимальная единица информации(char) занимает как правило 1 байт. Значит значение адреса увеличивается на 1 байт. Смотрим на следующую строку pch += 2; Что видит компилятор? Он видит что нужно увеличить значение адреса на 2 единицы информации, то есть sizeof(char)*2 = 2.
Найдите pd++. По аналогии, вы уже должны были понять, что значение адреса увеличивается на 8, так как тип pd это double который занимает 8 байт. pd += 2; Тут, адрес нужно увеличить на 2 единицы информации, а это sizeof(double)*2.
Указатель на тип void означает что тип данных неизвестен, мы просто передаем или сохраняем адрес чего-то, это довольно часто нужно. Посмотрите на функции выделения памяти, освобождения, и подобные, где возвращаемые значения и входящие аргументы это указатели на void. Указатель на void может быть преобразован в указатель на любой объект, и наоборот.
  void  *pv  = 0x00;

  int    i = 0;
  char  *pch = 0x00;
  short *psh = 0x00;

  pv = pch;   // OK 
  pv = psh;   // OK
  pv = &i;    // OK
  pch = pv;   // OK
  psh = pv;   // OK

  pch = psh;  // Incompatible pointer type
  psh = &i;   // Incompatible pointer type
Разименование указателя на void приводит компилятор в замешательство, так как компилятор не знает размер единицы хранения по этому указателю. Смотрите, когда программист разименовывает указатель, к примеру, на int, то компилятор знает что этот адрес указывает на данные int и при разименовании(доступу к этим данным) нужно взять 4 байта, если же это, к примеру double, то соответственно это 8 байт. Но с void мы получаем проблему, эта проблема на уровне предупреждения компилятора. Но предупреждение справедливое, так как мы не указали, сколько же нужно взять данных по этому адресу.
#include <stdio.h>
int main()
{
  long l = 0x0102030405060708;
  void *p = &l;

  printf("%i\n", *((char*)p));        // результат 0x08
  printf("%i\n", *((short*)p));       // результат 0x0708
  printf("%i\n", *((unsigned int*)p));// результат 0x05060708

  return 0;
}
В примере выше, мы конкретно указали, на что же все таки указывает наш указатель.
*((char*)p) означает - взять указатель p, явно привести его к указателю на char, и разименовать результат, то есть операция разименования применяется к указателю на char, значит с этого адреса нужно взять один байт.
Давайте решим такую задачу, решать только с помощью указателей и арифметики с указателями. Смотрите на предыдущий код, мы имеем объект типа long со значением в шеснадцатиричной системе - 0x0102030405060708,  суть задачи - увеличить 3-й байт(06) на 1, далее поменять местами 1-й(08) и последний байт(01) местами, в результате должны получить следующее - 0x0802030405070701. Ставя себе подобного рода задачки, вы легко разберетесь в физической природе указателей.
#include <stdio.h>
int main()
{
  long l = 0x0102030405060708;

  // здесь приведение значения l к void
  // говорит компилятору что это значение следует
  // воспринимать как адрес, тут особо не важно на что
  printf("%p\n", (void*)l); // 0x102030405060708

  // сначала нужно увеличить третий байт на 1
  // это байт со значением 06

  // объявляем указатель на char
  // и присваиваем ему адрес этого объекта
  char *ch = (char*)&l;

  // далее, нам нужно добраться к третьему байту

  ch++; // добрались к байту номер 2 (07)
  ch++; // добрались к байту номер 3 (06)

  // теперь нужно увеличить третий байт на 1
  // выполняем операцию разименования,
  // в результате получаем значение байта,
  // увеличиваем его на 1
  (*ch)++;
  printf("%p\n", (void*)l); // 0x102030405070708

  // первая задача выполнена

  // теперь нам нужно сохранить адрес первого
  // и последнего байтов, и поменять значения местами
  // по этим адресам
  char *pfirt = (char*)&l;
  char *plast = (char*)&l + 7;

  // заводим временную переменную для обмена значений
  char tmp_ch = 0;
  tmp_ch = *pfirt;

  // меняем местами значения байт
  *pfirt = *plast;
  *plast = tmp_ch;

  printf("%p\n", (void*)l); // 0x0102030405070708

  return 0;
}

Пример номер ДВА, подобное сплошь и рядом при отправке сообщений по сети, функции отправки потока куда-то и чтения откуда то работают с указателями на void.
#include <stdio.h>
void prepare_for_send(void *apOut)
{
  // например нужно отправить
  // следующие данные по сети
  unsigned char year  = 17;
  unsigned char month = 10;
  unsigned char day   = 6;
  unsigned long public_key = 0xff010203fafbfcfd;
  double        price = 10.4587;
  // для этого это все упаковывается
  // в массив байт, и отправляется

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

  unsigned char *p_year = ((unsigned char*)apOut);
  *p_year = year;

  unsigned char *p_month = ((unsigned char*)apOut+1);
  *p_month = month;

  unsigned char *p_day = ((unsigned char*)apOut+2);
  *p_day = day;

  unsigned long *pkey = (unsigned long *)((unsigned char *)apOut + 3);
  *pkey = public_key;

  double *pprice = (double *)((unsigned char *)apOut + 11);
  *pprice = price;

  printf("sended:\n");
  printf("year:%i\n", year);
  printf("month:%i\n", month);
  printf("day:%i\n", day);
  printf("key:%lu\n", public_key);
  printf("price:%f\n", price);
}

void receive_data(const void *apIn)
{
  // читаем данные полученные по сети
  unsigned char year  = 0;
  unsigned char month = 0;
  unsigned char day   = 0;
  unsigned long public_key = 0;
  double        price = 0;
  // распаковываем входящий массив
  // размер мы его уже знаем(19 байт)
  unsigned char *p_year = ((unsigned char*)apIn);
  year = *p_year;

  unsigned char *p_month = ((unsigned char*)apIn+1);
  month = *p_month;

  unsigned char *p_day = ((unsigned char*)apIn+2);
  day = *p_day;

  unsigned long *pkey = (unsigned long *)((unsigned char *)apIn + 3);
  public_key = *pkey;

  double *pprice = (double *)((unsigned char *)apIn + 11);
  price = *pprice;

  printf("received:\n");
  printf("year:%i\n", year);
  printf("month:%i\n", month);
  printf("day:%i\n", day);
  printf("key:%lu\n", public_key);
  printf("price:%f\n", price);
}

int main()
{
  // для отправки данным нам нужно 19 байт
  unsigned char buffer[19] = {0};

  prepare_for_send(buffer);
  // send_data(to_server, buffer, 19);
  // ...
  printf("\n\n");
  // ...
  receive_data(buffer);
  return 0;
}



Иногда нужно изменить адрес который хранит указатель, внутри функции. То есть передать указатель в функцию, чтобы он внутри изменился, это делается так как показано ниже:
void change_pointer_ncorrect(char *apCh) // NOT CORRECT
{
  // параметр apCh является копией указателя на объект
  // то есть, по этому указателю хранится объект
  // изменяя копию этого указателя, мы не меняем
  // адрес объекта который был передан сюда
  apCh = NULL;
}

void change_pointer_correct(char **apCh) // CORRECT
{
  // параметр apCh является указателем на указатель
  // то есть, по этому указателю хранится
  // адрес объекта(другой указатель) но не объект
  // разименовывая мы получаем доступ
  // к непосредственному адресу объекта
  *apCh = NULL;
}

int main()
{
  char *pch = (char*)0x0102;

  change_pointer_ncorrect(pch);
  // pch имеет старое значение

  // &pch - возвращает адрес ячейки, которая
  // хранит адрес объекта
  change_pointer_correct(&pch);
  // pch имеет новое значение
  return 0;
}


!ВОЗМОЖНЫ ДОПОЛНЕНИЯ!






Комментарии