C - неожиданное смещение указателя

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

#include <stdio.h>
#include <stdint.h>

int main()
{
    uint8_t *source[5];

    // Initial
    printf("%p\n", (void *)source); // 786796896

    // Offset by 2
    printf("%p\n", (void *)(source + 2)); // 786796912 (unexpected, more than 2)
}

Мой компилятор (с настройками по умолчанию) выдает 7 предупреждений. Неплохо для 20-строчной программы.

pmacfarlane 17.05.2023 01:02

@pmacfarlane Ну, если 11 не плохо, то насколько хорошо 0?

user16217248 17.05.2023 01:03
%d — неправильный способ печати указателя. Попробуйте %p.
pmacfarlane 17.05.2023 01:04
uint8_t *ptr = source; — это ошибка, как вам скажет ваш компилятор. @user16217248 имеет объяснение значений, которые вы видите, но это, вероятно, неопределенное поведение, которое просто делает то, что вы хотите.
pmacfarlane 17.05.2023 01:17

Спасибо, что указали на предупреждения, я использовал онлайн-компилятор, когда делал пример, и не видел их. Я исправил предупреждения, упростил пример и обновил printf, чтобы использовать %p для указателей на @pmacfarlane.

koga73 17.05.2023 02:08
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
3
5
147
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Ответ принят как подходящий

Проблема здесь в том, что когда вы добавляете 2 к source, массив распадается на тип указателя uint8_t **. Когда вы выполняете арифметические действия с указателем, добавленное смещение представляет собой количество добавленных элементов, а не количество байтов, если размер элемента указателя больше байта. Смещение в байтах от source + 2 на самом деле равно 2*sizeof(*source) байтам, то есть 16.

Чтобы обойти это поведение, приведите source к char *, выполните сложение, а затем выполните обратное приведение. Однако имейте в виду, что неправильное выполнение может привести к невыровненному доступу, что может быть плохой новостью.

Это также UB для создания невыровненного указателя

M.M 17.05.2023 02:26

@M.M Значит, невыровненные указатели являются представлениями-ловушками?

user16217248 17.05.2023 04:40

Не совсем. Преобразование указателя в другой тип указателя с неправильным выравниванием — это UB ; и преобразование целого числа в указатель с неправильным выравниванием определяется реализацией и может привести к представлению ловушки. (Понятия не имею, почему несоответствие между этими правилами, вероятно, исторические причины)

M.M 17.05.2023 05:47

@M.M Ну, а что, если я сначала преобразую указатель в целое число, а затем преобразую в другой указатель? Простой обход UB?

user16217248 17.05.2023 07:21

Как это сделает указатель выровненным после этого?

Gerhardh 17.05.2023 08:47

@ Герхард Это не так. Но по крайней мере это не будет мгновенный UB.

user16217248 17.05.2023 08:55

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

Gerhardh 17.05.2023 08:58

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

M.M 19.05.2023 00:54

Хммм... тут нет распада... объект совсем другой, это массив из пяти указателей, а не указатель на массив из пяти int8_t. Поэтому, пожалуйста, не путайте ЗП с неправильным ответом.

Luis Colorado 30.05.2023 09:46

Затухание до указателя происходит только для переменных параметров, объявленных как массивы, и только на первом уровне, поэтому определение параметра, например double matrix[3][5], распадается на double (*matrix)[5], А НЕ НА double ***matrix или любую другую вашу идею о распаде.

Luis Colorado 30.05.2023 10:00

в этом случае объявленный объект вопроса является не указателем, а массивом указателей, и затухание не выполняется, поскольку это локальная переменная, а не параметр функции. В переменной есть пять неинициализированных указателей на int8_t, но ни одного указателя на массив. Мы сумасшедшие? есть целая глава (или даже больше), посвященная несколько неясному синтаксису выражений определения типа в K&R. Пожалуйста, прочитайте это.

Luis Colorado 30.05.2023 10:02

@LuisColorado Я имел в виду, что когда вы добавляете x к массиву a, это эквивалентно получению адреса элемента в a[x] так же, как если бы массив был просто указателем на его первый элемент.

user16217248 30.05.2023 20:51

@user16217248 user16217248, когда вы используете имя массива (необработанное, оператор субиндекса не используется) как часть выражения, это означает ссылку указателя на первый элемент массива. Вы не можете ссылаться на массив в C, но только для использования sizeof оператор для расчета его размера. Итак, вы имели в виду указатель на первый элемент, добавленный двумя, который указывает на две позиции uint8_t впереди array. Это просто арифметика указателя, а не затухание. Хуже, когда вы говорите что-то вроде 2 + array, что означает то же самое, и делает выражение array[2] эквивалентным 2[array]. Затемнять!

Luis Colorado 31.05.2023 07:44
array сам по себе в выражении является rvalue типа uint8_t *, который отличается от типа массива (это uint8_t*[5], как указано в вопросе, или как uint8_t(*)[5], поскольку предлагаемое объявление должно было иметь указатель на массив ) Это изменяет фактический тип выражения array и делает значение, переданное printf(), другим.
Luis Colorado 31.05.2023 07:48

Указатель Arthimetics нужно избегать, насколько это возможно. Для выше

#include <stdio.h>
#include <stdint.h>

int main() {
    uint8_t* source[5]; // array of 5 pointers of uint8_t* type

    printf("%p\n", &source[2]); // address of 3rd element place in array source
}

Очень важно отметить, что добавление 2 к исходному адресу не приводит к увеличению адреса на +2, а на +10, потому что 2 интерпретируется как char* [5], а не char *.

// using without casting
char * arr[5];
char * parr = malloc(sizeof(int));
    
printf("%p\t%p\n", arr, parr);
printf("%p\t%p\n", arr+2, parr+2);

0x7ffde2925fb0  0x55b519f252a0
 +10             +2
0x7ffde2925fc0  0x55b519f252a2
//using with casting

char * arr[5];
char * parr = malloc(sizeof(int));
    
printf("%p\t%p\n", arr, parr);
printf("%p\t%p\n", (void*)arr+2, parr+2);

0x7ffde2925fb0  0x55b519f252a0
 +2               +2
0x7ffde2925fb2  0x55b519f252a2

Тогда как правильно использовать source[2] в контексте memcpy?

koga73 17.05.2023 02:43

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

hsuecu 17.05.2023 02:48

Я не вижу преимущества в том, чтобы изо всех сил избегать арифметики указателей. И этого на самом деле трудно избежать, особенно если вы когда-либо использовали массивы. Потому что индексация массива — это арифметика указателей + разыменование.

John Bollinger 17.05.2023 02:57

Прямой отказ от арифметики с указателями — это не норма, а, я думаю, хорошо известный факт в сообществе.

hsuecu 17.05.2023 02:58

Учитывая, что синтаксис массива — это просто еще один способ записи арифметических операций с указателями, я не вижу причин строго его избегать. &source[2] точно так же, как source+2. И в этом случае я нахожу последний даже немного более читабельным, чем использование синтаксиса массива. Кстати: вы вызываете неопределенное поведение, читая неопределенные значения ваших элементов указателя. И вы забыли привести к void*, который, как ожидается, будет передан для %p.

Gerhardh 17.05.2023 08:53

Хорошо, это работает, но мне не нравится приведение: memcpy((uint8_t *)buf_header + offset1 + offset2, val, val_size);

koga73 17.05.2023 20:55

Арифметики с указателями нужно избегать настолько, насколько это возможно, это плохой совет для изучающих язык C. Если вы не понимаете арифметики указателей, пожалуйста, не путайте людей своими утверждениями. Вы не поняли, что int8_t *source[5] ЯВЛЯЕТСЯ НЕ указателем на массив, а массивом указателей (совсем другой объект).

Luis Colorado 30.05.2023 09:44

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

Luis Colorado 30.05.2023 09:55

У меня есть указатель на массив

    uint8_t *source[5];

Извините, но это не указатель на массив. Это массив из 5 указателей на uint8_t, неинициализированный, поэтому вы должны его инициализировать.

Это указатель на массив:

    uint8_t (*source)[5];

(приоритет оператора [] выше, чем у * в выражениях типа)

...Что здесь происходит?

ну, исходя из вашего неверного предположения, все ожидаемо будет неверным.

    // Initial
    printf("%p\n", (void *)source); // 786796896

    // Offset by 2
    printf("%p\n", (void *)(source + 2)); // 786796912 (unexpected, more than 2)

source + 2 — это адрес третьего элемента массива source, который представляет собой массив указателей, поэтому два указателя смещаются от самого source (это 16 байт в 64-битной архитектуре — значение, которое вы получаете — и 8 байт в 32-битной архитектуре). архитектуры)

Просто попробуйте с предложенной декларацией, и вы получите то, что ожидаете.

$ a.out
0x560bee9df060
0x560bee9df06a
$ _

(10 позиций, как и ожидалось для двух массивов из 5 uint8_t элементов)

Но вы можете проверить это сами.

Если вы используете оператор sizeof для самого source, вы получите размер массива (5 указателей или 40 байтов в 64-битной архитектуре), в то время как, как вы указываете в своем вопросе, он должен быть размером с один указатель (8 байтов , в 64-битной арке) Если вы объявите его с помощью предложенного объявления, то вы получите 8, как и должно быть expetec, а если вы сделаете sizeof *source (размер объекта, на который указывает source), в этом случае вы получите 5 (размер массива из пяти значений uint8_t) Попробуйте и убедитесь.


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

  1. Объявленный объект был массивом указателей, а не указателем на массив из пяти uint8_t, поэтому его адрес (значение, переданное printf, действительно выровнено и действительно (это действительно адрес в стеке, где была создана переменная source). ), и при использовании этого адреса не возникает UB, потому что содержимое массива само по себе не используется, поэтому он остается неинициализированным и не вызывает UB.)
  2. Если он правильно объявлен как указатель на массив, печатается его неинициализированное значение --- единственной непредсказуемой вещью является само значение ---, но это не требует разыменования указателя или выравнивания (что не является проблемой, см. следующий). В этом случае следует избегать арифметики указателя для неинициализированной переменной указателя, поскольку не гарантируется, что значением назначения указателя может быть допустимый адрес, и поэтому есть некоторые, обычно не вредные, U.B. (это связано с тем, что исходное неинициализированное значение также может быть само по себе недопустимым адресом и не должно вызывать UB, будучи просто неинициализированным. Приведение (void *) само по себе обычно не вызывает UB.)
  3. Если он объявлен как указатель на массив, опять же, проблем с выравниванием нет, так как базовый тип указателя — это массив uint8_t с выравниванием 1 (единица), поэтому любой адрес должен быть приемлемым. Поскольку указатель не разыменовывается, он ведет себя как любая неинициализированная переменная и, таким образом, не вызывает U.B.

Другие вопросы по теме