Путает аргументы одиночного указателя и двойного указателя в вызовах функций



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



У меня есть программа, которая имеет две функции. Первая функция modifyMe1 принимает единственный указатель в качестве аргумента и изменяет свойство a на 7. Вторая функция modifyMe2 принимает двойной указатель в качестве аргумента и изменяет свойство a на 7.

I ожидалось, что первая функция modifyMe1, будет "pass-by-value", то есть если я передам в моей структуре указатель, C создаст копию данных, указанных им. В то время как с последним, я делаю "проход по ссылке", который должен изменить структуру на месте.



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

Спасибо!



Вот что у меня есть:



#include <stdio.h>
#include <stdlib.h>

struct myStructure {
int a;
int b;
};

void modifyMe1(struct myStructure *param1) {
param1->a = 7;
}

void modifyMe2(struct myStructure **param1) {
(*param1)->a = 7;
}

int main(int argc, char *argv[]) {
struct myStructure *test1;

test1 = malloc(sizeof(test1));
test1->a = 5;
test1->b = 6;

modifyMe1(test1);

printf("a: %d, b: %dn", test1->a, test1->b);

// set it back to 5
test1->a = 5;
printf("reset. a: %d, b: %dn", test1->a, test1->b);

modifyMe2(&test1);

printf("a: %d, b: %dn", test1->a, test1->b);


free(test1);
return 0;
}


, в котором мой вывод:



$ ./a
a: 7, b: 6
reset. a: 5, b: 6
a: 7, b: 6
612   3  

3 ответов:

Вы можете передавать аргумент различными способами В C (капитан очевидный, да).

  1. По значению. Затем он копируется в стек. Таким образом, функция имеет локальную копию переменной в фрейме функции. Любые изменения аргумента не изменяют переданное значение. Это похоже на режим "Только чтение"

    void fooByValue(myStructure_t arg) {
        printf("passed by value %d %d\n", arg.a, arg.b);
        arg.a = 0;
    }
    
  2. Пройти по указателю. Затем передается копия адреса этой переменной (так что да, она по-прежнему передается по значению, но вы передаете значение адреса, а не всего аргумента). Так что это как " читать и писать" режим. Поскольку вы можете получить доступ к передаваемой переменной через ее адрес, вы можете изменить значение этой переменной вне функции.

    void fooByPtr(myStructure_t *arg) {
        printf("passed by pointer %d %d\n", arg->a, arg->b);
        arg->a = 0;
    }
    

    Но! Вы все еще не можете изменить указатель.

  3. Так что если вы хотите изменить указатель, то вы должны передать указатель на указатель. Это похоже на режим "read-write modify":

    void fooByDblPtr(myStructure_t **arg) {
        *arg = (myStructure_t*) malloc(sizeof(myStructure_t));
        (*arg)->a = 10;
        (*arg)->b = 20;
    }
    

    Если бы это был просто указатель, то произошла бы утечка памяти:

    void fooByDblPtr(myStructure_t *arg) {
        arg = (myStructure_t*) malloc(sizeof(myStructure_t));
        (arg)->a = 10;
        (arg)->b = 20;
    }
    

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

    UPD. Например, у нас есть

    void fooByPtr(myStructure_t *arg) {
        printf("addr inside foo before %p\n", arg);
        arg = (myStructure_t*) malloc(sizeof(myStructure_t));
        (arg)->a = 10;
        (arg)->b = 20;
        printf("addr inside foo after %p\n", arg);
    }
    
    void main() {
        myStructure_t *x = NULL;
        x = malloc(sizeof(myStructure_t));
        x->a = 10;
        x->b = 20;
        printf("x addr before = %p\n", x);
        fooByPtr(x);
        printf("x addr after = %p\n", x);
        free(x);
    }
    

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

    Краткий вывод: есть простое правило-если нужно изменить аргумент, передайте на него указатель. Итак, если вы хотите изменить указатель, передайте указатель на указатель. Если вы хотите изменить двойной указатель, передайте указатель на указатель на указатель.

  4. Передача аргумента по указателю также намного быстрее, потому что вам не нужно копировать все значения в стеке (конечно, если значение больше, чем указатель на это значение, в противном случае передача указателя только для чтения бессмысленна). Но это опасно, потому что это может быть изменено внутри функции. Таким образом, вы можете защитить аргумент, определяя его с помощью ключевого слова const

    void constFoo(const myStructure_t *arg) {
        arg->a = 10;    //compilation error
        arg->b = 20;    //compilation error
    }
    

    Это действительно полезно при работе с библиотеками сторонних разработчиков: сигнатура функции сообщает вам, является ли функция может изменить ваш аргумент или нет. Хотя const является необязательным, рекомендуется писать ключевое слово const каждый раз, когда это возможно

  5. Передающий массив. Обычно в качестве аргумента также передается размер массива (следовательно, size_t). Вы передаете массив как указатель.

    void foo (int *buf, size_t nbuf) {
        ....
    }
    

    Иногда можно найти код, в котором разработчик отправляет указатель на объект вместо массива, например

    void foo (int *buf, size_t size) {
        size_t i;
        for (i = 0; i < size; i++) {
            printf("%d ", buf[i]);
        }
    }
    
    int main(int argc, char **argv) {
        int a = 10;
        int buf[1] = { 10 };
        foo(buf, 1);
        foo(&a, 1);
    }
    

    В этом случае массив одного элемента и указатель на элемент ведут себя одинаково (хотя они и не являются одинаковый).

С регулярным параметром, скажем int вы получаете локальную копию
с помощью параметра указателя, скажем int*, Вы можете изменить то, на что он указывает
с помощью параметра двойного указателя, скажем int**, Вы можете изменить сам указатель, т. е. "переставить" его.

Добавьте еще одну функцию:

void modifyMe0(struct myStructure param1)
{
    param1.a = 7;
}
Это передает структуру по значению. Изменение, внесенное в функцию, не отражается в аргументе, переданном в modifyMe0().

Добавьте вызывающий код следующим образом:

printf("Before 0: a = %d, b = %d\n", test1->a, test1->b);

modifyMe0(*test1);

printf("After  0: a = %d, b = %d\n", test1->a, test1->b);
Обратите внимание, что значения before и after в вызывающем коде одинаковы. Вы также можете добавить печать в свои функции modifyMeN(), чтобы продемонстрировать, что в них изменяется значение.

Когда вы передаете указатель на структуру вызываемой функции, значение структура вызывающей функции может быть изменена. При передаче структуры вызываемой функции по значению значение структуры в вызывающей функции не изменяется.

Можно создать другую функцию:

void modifyMe3(struct myStructure **p1)
{
    free(*p1);
    *p1 = malloc(sizeof(*p1));
    (*p1)->a = -3;
    (*p1)->b = -6;
}

Добавьте вызывающий код следующим образом:

printf("Before 3: address = %p, a = %d, b = %d\n", (void *)test1, test1->a, test1->b);

modifyMe0(*test1);

printf("After  3: address = %p, a = %d, b = %d\n", (void *)test1, test1->a, test1->b);
Обратите внимание, что адрес структуры изменился после вызова modifyMe3().

Comments

    Ничего не найдено.