Адреса указателей и арифметика

программисты!

Я полностью утонул в двойном указателе (указатель на указатели)... Здесь много вопросов!

Начнем с этой задачи: я пишу свою версию функции calloc, которая должна возвращать указатель на n элементов памяти размера size. Вот что я изобрел:

void **calloc1(int n, int size)
{
    int i, j;
    char *tmp, **p = NULL;
    tmp = (char *) malloc1(n * size);
    p = tmp;
    for (i = 0; i < n; i++) {
        p[i] = tmp;
        for (j = 0; j < size; j++)
            tmp++;
    }
    p = &p[0];
    return (void **)p;
}
/* I entered n==4, size==3;  real pointers are: p[0] == 0x804cfe0; p[1] == 0x804cfe3; p[2] == 0x804cfe6; ... */

Итак, по сути, я выделяю байты n * size, а затем «назначаю» массив указателей равного «размера» соответствующим начальным позициям. Введем n=4 и size=3; это означает, что p[0] указывает на tmp[0], p[1] на tmp[3], p[2] на tmp[6] и так далее. В GDB я отслеживаю значения указателей почти после каждого шага.

Затем в «main» я объявляю двойной указатель и «прикрепляю» его к буферу, полученному от моего «calloc»:

int main (short argc, char **argv)
    {
        char **space;
        space = (char **) calloc1(n, size);   /* removing '(char**)' here does not have effect */ 

    /*  at this stage pointers seems to be correctly "located": 'space' == 'space[0]' == 0x804cfe0; 'space[1]' == 0x804cfe3; 'space[2]' == 0x804cfe6; ... */

1) Вот уже первый вопрос: как 'main()' (или любая другая функция, которой я передам копию **p) узнает размер арифметики указателя? Например, откуда 'main()' знает, что если я добавлю '1' к 'space' (или просто увеличим его один раз), он должен указывать на свой второй указатель (в 'calloc' это p[1]), который ( в этом конкретном случае) на 3 символа дальше первого указателя (p[0])? Более того, если я создам в 'alloc' массив указателей на строки с "переменной длиной" (например, p[0] указывает на tmp[0], p[1] на tmp[7], p[2] на tmp [11] и т. д.), как любая другая функция узнает, где она должна увеличить «верхний» указатель на 4, а где на 7 «символов»?

Хорошо, идем дальше, я пытаюсь поместить некоторые символы в полученный буфер:

int i = 0, j = 0, n, size;
char nn, ssize, c, temp[3];

printf ("Enter number/size \n");
sc = scanf ("%c/%c", &nn, &ssize);
n = nn - '0';  /* n==4 */
size = ssize - '0';   /* size==3 */ 

printf ("Enter a 'number' of words\n");
while (j < n) {
    for (i = 0; (c = getchar()) != EOF && i < size; i++)
        *(*space)++ = c; 
    (*space)--;   /* this line is unneccesary; if I remove it - nothing changes */
    ++j;
    ++space;
}

2) И вот доказательство для первого вопроса: на самом деле, когда я увеличиваю пробел здесь, он перемещается не на 3, а на 4 символа (после первого «++» это 0x804cfe4, после второго 0x804cfe8). Почему? Есть ли какая-то связь с размером типа «плавающий»? После первого такого увеличения '*space' указывает на 0x804cfe6... Я не думаю, что это правильно.

Я попробовал другой способ - ссылаясь на «пространство», как не на указатель, а на массив:

....
while (j < n) {
    for (i = 0; (c = getchar()) != EOF && i < size; i++)
        *space[j]++ = c; 
    space[j]--;
    ++j;
}

3) В этом случае указатели кажутся в порядке - например. пробел[1] == 0x804cfe3, пробел[2] == 0x804cfe6. Проблема в том, что пока этот цикл работает с j == 2, значение 'space[0]' каким-то образом меняется с 0x804cfe2 (перемещено дважды - ок) на что-то вроде 0x6a04cfe2 (что выходит за пределы). Что за х..???

4) И вообще, какое-то странное поведение адресов. Я также пытался не записывать символы напрямую в **space, а использовать функцию копирования строки:

    char i, temp[3];  
    ...
    while (j < n) {
        for (i = 0; (c = getchar()) != EOF && i < size; i++)
            temp[i] = c; 
        strncpy1 (space[j],temp,3);
        ++j;
    }
.....
void strncpy1 (char *s, char *t, int k)
{
    while (--k > 0) {
        *s = *t;
        s++;  t++;
    }
}

Внутри функции копирования копирование и приращение отображаются в GDB правильно. Но после возврата из 'strncpy1' пробел[j] меняется с 0x804cfe0 на что-то вроде 0x804000a. Как возможно, что вызываемая функция может воздействовать на родительский (внешний) указатель?

Итак, наконец, какой тип указателей на символы? Какой у него размер?


person digitest    schedule 08.05.2015    source источник
comment
Я не понимаю, что вы пытаетесь сделать с помощью функции calloc. Почему он возвращает void ** вместо того, чтобы возвращать void *, как это делает обычный calloc?   -  person hugomg    schedule 08.05.2015
comment
Да, я ожидал такого вопроса )) во всяком случае, изменив calloc1 с 'void **' на 'void *' (и также вернув тип указателя с '(void **)p' на '(void *)p') ни на что не влияет...   -  person digitest    schedule 08.05.2015
comment
Прежде чем делать что-либо еще, нарисуйте на бумаге карту памяти, где находятся ваши выделения памяти и куда указывает каждый указатель.   -  person M.M    schedule 08.05.2015
comment
В 3) вы пишете «j», верно? Это 0x6A, что при установке на третье место в вашей перекрывающейся памяти изменит четвертый байт указателя. Подробности смотрите в моем ответе.   -  person Sami Kuhmonen    schedule 08.05.2015
comment
Я не думаю, что вы понимаете, что делает calloc. Он не выделяет массив указателей. Все, что он делает, это вычисляет n*size байтов и очищает эти байты до нуля. В calloc нет указателей на указатели. Если вы пытаетесь написать что-то другое, пожалуйста, объясните, что вы пытаетесь сделать, потому что ваша программа в данный момент не имеет смысла.   -  person JS1    schedule 08.05.2015
comment
Я выполняю задания из книги K&R, в частности - пишу собственную версию calloc(n,s)'. Во встроенную версию пока не заглядывал (да и не хочу - надо самому делать!). И лучшая идея, которая пришла мне в голову, заключалась в том, чтобы выделить «n * s» байт с помощью «malloc», а затем назначить этим байтам указатель на n указателей (размером «s»). В любом случае всем спасибо, ребята, чувствую скоро мой мозг будет этому радоваться )   -  person digitest    schedule 08.05.2015
comment
void *calloc(int n, int size) { void *ret = malloc(n*size); memset(ret, 0, n*size); return ret; } Это все, чем является calloc(). Я не имел в виду, что вам нужно смотреть исходный код. Я имел в виду, что вы не поняли, что функция возвращает один единственный указатель.   -  person JS1    schedule 09.05.2015


Ответы (2)


Я полагаю, что в вашей системе размер указателя равен 4, и, поскольку вы используете указатель на указатель, а затем увеличиваете его, он добавляет 4 байта в текущее местоположение, чтобы достичь следующего указателя своего типа (char **).

ПРИМЕЧАНИЕ: вы не увеличиваете указатель на char, вы увеличиваете указатель на указатель.

Теперь снова, если я расскажу о вашем втором вопросе о том, как функция будет знать, где увеличить 4 и где 7, так что это не связано с функцией. Поскольку массив указателя представляет собой адрес указателя, который сохраняется в последовательном месте (они не являются значением указателя, я говорю об указателях, которые сохраняются в массиве указателей), поэтому он просто увеличивает этот указатель на единицу и достигает следующего указателя. от своего типа, независимо от того, находится ли он на P[0], P[4] или p[7]..

person Dayal rai    schedule 08.05.2015
comment
Привет, Дайал! Да, спасибо, я вижу, что увеличиваю указатель не на char, а на другой указатель. Кажется, я начинаю понимать... Итак, главный (верхний) указатель здесь на самом деле является последовательной ячейкой памяти, верно? И какова длина этой локации? Каков размер одной позиции указателя -1/2/4 байта? - person digitest; 08.05.2015
comment
так как это массив указателей, поэтому каждое местоположение будет иметь 4 байта. - person Dayal rai; 08.05.2015

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

Вы выделяете только один буфер, а затем используете этот буфер для хранения указателей в тот же буфер. Конечно, когда вы затем измените содержимое, ваши указатели изменятся. У вас должны быть отдельные области памяти: одна для данных, другая для указателей на эти данные.

Если вы действительно хотите это сделать, вы можете сделать это следующим образом:

void **calloc1(int n, int size)
{
    int i, j;
    char *tmp, **p = NULL;
    tmp = (char *) malloc1(n * size);
    p = (char**) malloc1(n * sizeof(char*));
...

ПРИМЕЧАНИЕ: в этом случае вам, естественно, понадобятся два вызова для освобождения памяти, и у вас может не быть возможности узнать, что такое второй вызов, поскольку пользователь может изменить указатели. Так что еще лучше было бы объединить распределения в одно:

void **calloc1(int n, int size)
{
    int i, j;
    char *tmp, **p = NULL;
    p = (char**) malloc1(n * size + n * sizeof(char*));
    tmp = (char *) (p + n);
...

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

Что касается других вопросов:

1) как «main()» может узнать размер арифметики указателя?

Указатели всегда имеют фиксированный размер. Они указатели, им все равно, на что они указывают. sizeof(char*) == sizeof(int*) == sizeof(yourcoolstruct*)

2) когда я увеличиваю пробел здесь, он перемещается не на 3, а на 4 символа (после первого «++» это 0x804cfe4, после второго 0x804cfe8). Почему? Есть ли какая-то связь с размером типа «плавающий»?

Поскольку в вашей системе sizeof(pointer) == 4, каждый указатель занимает 4 байта. Нормально в 32-битных средах и не имеет ничего общего с числами с плавающей запятой, целыми числами или чем-то еще.

3) пока этот цикл работает с j == 2, значение 'space[0]' каким-то образом меняется с 0x804cfe2 (дважды перемещено - ок) на что-то вроде 0x6a04cfe2 (что выходит за пределы). Что за х..???

Потому что вы используете одну и ту же память для указателей и данных. Вы записываете 'j' (0x6A в шестнадцатеричном формате) в пробел [1][0], что указывает на четвертый байт распределения. Который также является старшим байтом указателя пробела [0], поэтому он становится им.

person Sami Kuhmonen    schedule 08.05.2015
comment
Спасибо, Сами! На самом деле, я как раз заканчиваю исследование Кернигана-Ритчи и застрял на последних задачах, связанных с памятью. Так вот, хм... Не надо строк вроде 'char *tmp, **p = NULL;' или 'символ **пробел;' сделать необходимое выделение для этих указателей? Я спрашиваю, потому что во всех примерах в книге и во всех ее задачах (которые я успешно выполнил) не было никакого специального выделения памяти для переменных, указателей и т. д. Они (мы) просто объявляют какой-то указатель, а затем присваивают какое-то значение к нему (например, char *ptr, b[5]; ptr = b;) - person digitest; 08.05.2015
comment
Да, char **p выделит память для указателя на указатели, но, конечно, память, которая содержит указатели, должна быть выделена отдельно. И если эти указатели указывают на какие-то данные, они также должны быть каким-то образом распределены. Если вы используете char b[5], то вы явно выделяете память на пять символов во время компиляции и можете, естественно, использовать ее без каких-либо выделений. - person Sami Kuhmonen; 08.05.2015