Что означает получение адреса переменной массива?

Что такое NullPointerException?

Хорошим местом для начала является JavaDocs . Они охватывают это:

Брошено, когда приложение пытается использовать null в случае, когда требуется объект. К ним относятся:

  • Вызов метода экземпляра нулевого объекта.
  • Доступ или изменение поля нулевого объекта.
  • Выполнение длины null, как если бы это был массив.
  • Доступ или изменение слотов с нулевым значением, как если бы это был массив.
  • Бросать нуль, как если бы это было значение Throwable.

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

Также, если вы попытаетесь использовать нулевую ссылку с synchronized, который также выдаст это исключение, за JLS :

SynchronizedStatement:
    synchronized ( Expression ) Block
  • В противном случае, если значение выражения равно null, NullPointerException.

Как это исправить?

Итак, у вас есть NullPointerException. Как вы это исправите? Возьмем простой пример, который выдает NullPointerException:

public class Printer {
    private String name;

    public void setName(String name) {
        this.name = name;
    }

    public void print() {
        printString(name);
    }

    private void printString(String s) {
        System.out.println(s + " (" + s.length() + ")");
    }

    public static void main(String[] args) {
        Printer printer = new Printer();
        printer.print();
    }
}

Идентифицирует нулевые значения

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

Exception in thread "main" java.lang.NullPointerException
    at Printer.printString(Printer.java:13)
    at Printer.print(Printer.java:9)
    at Printer.main(Printer.java:19)

Здесь мы видим, что исключение выбрано в строке 13 (в методе printString). Посмотрите на строку и проверьте, какие значения равны нулю, добавив протоколирующие операторы или используя отладчик . Мы обнаруживаем, что s имеет значение null, а вызов метода length на него вызывает исключение. Мы видим, что программа прекращает бросать исключение, когда s.length() удаляется из метода.

Трассировка, где эти значения взяты из

Затем проверьте, откуда это значение. Следуя вызовам метода, мы видим, что s передается с printString(name) в методе print(), а this.name - null.

Трассировка, где эти значения должны быть установлены

Где установлен this.name? В методе setName(String). С некоторой дополнительной отладкой мы видим, что этот метод вообще не вызывается. Если этот метод был вызван, обязательно проверьте порядок , что эти методы вызывают, а метод set не будет называться после методом печати. ​​

Этого достаточно, чтобы дать нам решение: добавить вызов printer.setName() перед вызовом printer.print().

Другие исправления

Переменная может иметь значение по умолчанию setName может помешать ему установить значение null):

private String name = "";

Либо метод print, либо printString может проверить значение null например:

printString((name == null) ? "" : name);

Или вы можете создать класс, чтобы name всегда имел ненулевое значение :

public class Printer {
    private final String name;

    public Printer(String name) {
        this.name = Objects.requireNonNull(name);
    }

    public void print() {
        printString(name);
    }

    private void printString(String s) {
        System.out.println(s + " (" + s.length() + ")");
    }

    public static void main(String[] args) {
        Printer printer = new Printer("123");
        printer.print();
    }
}

См. также:

Я все еще не могу найти проблему

Если вы попытались отладить проблему и до сих пор не имеете решения, вы можете отправить вопрос для получения дополнительной справки, но не забудьте включить то, что вы пробовали до сих пор. Как минимум, включите stacktrace в вопрос и отметьте важные номера строк в коде. Также попробуйте сначала упростить код (см. SSCCE ).

25
задан Community 23 May 2017 в 12:24
поделиться

5 ответов

Давайте рассмотрим его.

a имеет тип int [4] (массив из 4 int). Его размер 4 * sizeof(int).

&a имеет тип int (*)[4] (указатель на массив из 4 int).

(&a + 1) также имеет тип int (*)[4]. Он указывает на массив из 4 int, который начинает 1 * sizeof(a) байт (или 4 * sizeof(int) байт) после начала a.

*(&a + 1) имеет тип int [4] (массив 4 int). Его память начинает 1 * sizeof(a) байт (или 4 * sizeof(int) байт после начала a.

*(&a + 1) - 1 имеет тип int * (указатель на int), потому что массив *(&a + 1) уменьшается до указатель на его первый элемент в этом выражении. Он будет указывать на int, который начинает 1 * sizeof(int) байт до начала *(&a + 1). Это то же значение указателя, что и &a[3].

*(*(&a + 1) - 1) типа int. Поскольку *(&a + 1) - 1 - это то же значение указателя, что и &a[3], *(*(&a + 1) - 1) эквивалентно a[3], который был инициализирован как 3, так что это число, напечатанное printf .

12
ответ дан Ian Abbott 23 May 2017 в 12:24
поделиться

&a + 1 будет указывать на память сразу после последнего элемента a или, лучше сказать, после массива a, поскольку &a имеет тип int (*)[4] (указатель на массив из четырех int) , Построение такого указателя допускается стандартным, но не разыменованием. В результате вы можете использовать его для последующей арифметики.

Итак, результат *(&a + 1) не определен. Но тем не менее *(*(&a + 1) - 1) это нечто более интересное. Эффективно он оценивается до последнего элемента в a, подробное объяснение см. В https://stackoverflow.com/a/38202469/2878070 . И просто замечание - этот хак может быть заменен более читаемой и очевидной конструкцией: a[sizeof a / sizeof a[0] - 1] (конечно, его следует применять только к массивам, а не к указателям).

7
ответ дан Community 23 May 2017 в 12:24
поделиться

Если у вас есть объект типа T, например

T obj;

, то объявление

T *p = &obj;

инициализирует указатель p адресом памяти, занятым объект obj

выражение p + 1 указывает на память после объекта obj. Значение выражения p + 1 равно значению &obj plus sizeof( obj ), что эквивалентно

( T * )( ( char * )&obj + sizeof( obj ) )

Так что, если у вас есть массив, показанный в вашем посте int a[] = {0, 1, 2, 3};, вы можете переписать его объявление, используя typedef следующим образом:

typedef int T[4];

T a = { 0, 1, 2, 3 };

sizeof( T ) в этом случае равно sizeof( int[4] ) и, в свою очередь, равно 4 * sizeof( int )

Выражение &a дает адрес объем памяти, занимаемый массивом. Выражение &a + 1 дает адрес памяти после массива, а значение выражения равно &a + sizeof( int[4] )

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

Таким образом, выражение &a + 1 указывает на воображаемый элемент типа int[4] после реального первого элемента a. Выражение *(&a + 1) дает этот воображаемый элемент. Но поскольку элемент является массивом типа int[4], то это выражение преобразуется в указатель на свой первый элемент типа int *

. Этот первый элемент следует за последним элементом массива a. И в этом случае выражение *(&a + 1) - 1 дает адрес этого последнего элемента массива a

Разыменовывая в *(*(&a + 1) - 1), вы получаете значение последнего элемента массива a, поэтому будет выведено число 3.

1
ответ дан Vlad from Moscow 23 May 2017 в 12:24
поделиться

Сначала небольшое напоминание (или что-то новое, если вы не знали этого раньше): для любого массива или указателя p и индекса i выражение p[i] точно такое же, как *(p + i).

Теперь, надеюсь, помочь вам понять, что происходит ...

Массив a в вашей программе хранится где-то в памяти, именно там, где это не имеет значения. Чтобы получить место, где хранится a, то есть получить указатель на a, вы используете оператор address-of &, например, &a. Здесь важно учиться, что указатель сам по себе ничего особенного не означает, важен базовый тип указателя . Тип a является int[4], то есть a является массивом из четырех int элементов. Тип выражения &a является указателем на массив из четырех int или int (*)[4]. Скобки важны, потому что тип int *[4] представляет собой массив из четырех указателей на int, что совсем другое.

Теперь вернемся к начальной точке, которая p[i] совпадает с *(p + i). Вместо p у нас есть &a, поэтому наше выражение *(&a + 1) совпадает с (&a)[1].

Теперь это объясняет, что означает *(&a + 1) и что он делает. Теперь давайте немного подумаем о расположении памяти в массиве a. В памяти это выглядит примерно как

+---+---+---+---+
| 0 | 1 | 2 | 3 |
+---+---+---+---+
^
|
&a

Выражение (&a)[1] обрабатывает &a как массив массивов, которым он определенно не является, и получает доступ ко второму элементу в этом массиве, который будет быть вне границ. Технически это, конечно, неопределенное поведение . Давайте немного поработаем с ним и рассмотрим, как будет выглядеть в памяти:

+---+---+---+---+---+---+---+---+
| 0 | 1 | 2 | 3 | . | . | . | . |
+---+---+---+---+---+---+---+---+
^               ^
|               |
(&a)[0]         (&a)[1]

Теперь вспомните, что тип a (который совпадает с (&a)[0] и, следовательно, означает, что (&a)[1] также должен быть этого типа) массив из четырех int . Поскольку массивы естественным образом распадаются на указатели на свой первый элемент, выражение (&a)[1] совпадает с &(&a)[1][0], а его тип указывает на int . Поэтому, когда мы используем (&a)[1] в выражении, компилятор дает нам указатель на первый элемент во втором (несуществующем) массиве из &a. И снова мы приходим к уравнению p[i] равно *(p + i): (&a)[1] - это указатель на int , это p в выражении *(p + i), поэтому полное выражение равно *((&a)[1] - 1), и, глядя на схему памяти выше, вычитая единицу int из указателя, данного в (&a)[1], мы получим элемент перед (&a)[1], который является последним элементом в (&a)[0], т.е. такой же, как a[3].

Таким образом, выражение *(*(&a + 1) - 1) совпадает с a[3].

Он многословен и проходит через опасную территорию (с индексированием вне границ), но благодаря арифметике с указателями все это в конце концов сработает. Я не рекомендую вам когда-либо писать код, подобный этому, но людям нужно по-настоящему знать, как эти преобразования работают, чтобы иметь возможность его расшифровать.

35
ответ дан Some programmer dude 23 May 2017 в 12:24
поделиться

Обратите внимание, что следующее эквивалентно, но одинаково неприятно:

printf("%d\n", (&a)[1][-1]);

В этом случае, на мой взгляд, более явно, что происходит:

указатель на массив a берется

  • указатель используется, как если бы он был массивом: массив элементов наподобие a, то есть массивы из 4 целых чисел, используется 1-й элемент этого массива.

  • Поскольку a на самом деле не массив, а только один элемент (состоящий из четырех подэлементов!), Это индексирует часть памяти сразу после

  • [ -1] читает целое число непосредственно перед памятью непосредственно после a, которое является последним подэлементом в

0
ответ дан slingeraap 23 May 2017 в 12:24
поделиться
Другие вопросы по тегам:

Похожие вопросы: