Итак, давайте разберем ваш код:
Объявление указателя на указатель на struct Test
.
struct Test **ar;
Выделение места для указатели, если ваша система 64-битная, вы выделяете пространство ровно для двух смежных указателей. ), это никогда не используется, но без оптимизации компилятора вам все равно нужно его инициализировать, выделение 0 байтов может быть не лучшим вариантом, поскольку оно вызывает неопределенное поведение, но поскольку вы никогда ничего не храните там, это не вызывает проблем.
*ar = malloc(0);
Вы храните foo
адрес в указателе номер 2, который, поскольку он работает, заставляет меня думать, что ваша система действительно 64-битная.
*(ar+1) = &foo;
Работает, присваивая 'c'
char c
одному за структурой foo. То же, что и ar[0][1].c ='c';
(*(*ar+1)).c = 'c';
printf("%c\n", (*(*ar+1)).c); //prints 'c'
Доступ за пределы работает, потому что в C один за пределами конца массива или выделенный блок памяти доступен, и похоже, что ваша реализация позволяет вам получить к нему доступ для записи и разыменования, хотя он находится за пределами, кстати, мой тоже, это не всегда так, поэтому вы не можете ожидать, что он всегда будет работать.
Все это работает случайно (а может и нет), потому что вы выделяете необходимое пространство для двух указателей.
Теперь давайте внесем некоторые изменения в ваше выделение, чтобы сравнить с тем, что у вас есть, и давайте разберемся с заполнениями структуры для хранения и доступа к значениям за пределами выделенной памяти.
#include <stdio.h>
#include <stdlib.h>
struct Test
{
char c;
} foo;
int main(void)
{
struct Test **ar; //declaring a pointer to pointer to struct Test
ar = malloc(sizeof(*ar) * 2); //allocationg space for 2 pointers to struct Test.
//without optimization you still need to allocate space
//or otherwise initialize the 1st pointer to avoid UB
*(ar + 0) = malloc(sizeof(**ar)); //or ar[0] = ... or *ar = ...
*(ar + 1) = &foo; //or ar[1] = ... storing foo's address in the second pointer
(*(*ar + 1)).c = 'c'; //works fine, one past the allocated memory
printf("%c\n", ar[0][1].c);
(**(ar + 1)).c = 'b'; //works, actually foo
printf("%c\n", ar[1][0].c);
(*(*(ar + 1) + 1)).c = 'a'; //also works, accessing ou of bounds
printf("%c\n", ar[1][1].c);
printf("%c\n", foo.c); //test print foo
return 0;
}
Живая демонстрация
Это намного лучше не только с точки зрения удобочитаемости, но также и с точки зрения переносимости, поскольку именно система решает, какой размер указателей.
Теперь посмотрите на этот упрощенный код:
#include <stdio.h>
#include <stdlib.h>
struct Test
{
char c;
} foo;
int main(void)
{
struct Test** ar;
ar = malloc(sizeof(*ar) * 2);
ar[0] = malloc(sizeof(**ar)); //or *ar = ...
ar[1] = &foo; //or *(ar + 1) = ...
ar[0]->c = 'a';
printf("%c\n", ar[0]->c);
ar[1]->c = 'c';
printf("%c\n", ar[1]->c);
printf("%c\n", foo.c); //ok foo has 'c'
return 0;
}
Посмотрите, как легко используйте двойные указатели, если не усложняете вещи.