Правильно ли я понимаю передачу по стоимости и передачу по ссылке в C? - PullRequest
2 голосов
/ 13 января 2020

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

void set_array(int array[4]);
void set_int(int x);

int main(void){
    int a = 10; 
    int b[4] = {0, 1, 2, 3);
    set_int(a);
    set_array(b);
    printf("%d %d\n", a, b[0]);
}

void set_array(int array[4]){
    array[0] = 22;
}

void set_int(int x){
    x = 22;
}

Я хочу убедиться, что после первоначального выбора неправильного вывода, я понимаю, почему выполняется правильный вывод "10, 22" распечатаны.

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

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

Не понял ли я, что происходит, и упускаю ли я какие-то важные моменты?

Ответы [ 2 ]

10 голосов
/ 13 января 2020

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

  • В первом случае переменная a передается по значению следовательно, его нельзя изменить из вызова функции.

  • Во втором случае переменная b передается по значению, следовательно, ее также нельзя изменить из вызова функции.

    Однако во втором случае происходит особый феномен. Переданный аргумент b является массивом. Массив, передаваемый в качестве аргумента функции (среди многих других случаев использования), распадается на указатель на первый элемент массива. По сути, вы передаете указатель, и это значение по адресу памяти, на который указывает указатель, можно изменить (но не сам указатель). Поэтому изменение функции set_array() сохраняется в main().

    Другими словами, второй случай эквивалентен

    void set_pointer(int *ptr);
    void set_int(int x);
    
    int main(void){
        int a = 10; 
        int b[4] = {0, 1, 2, 3);
        int * p = &(b[0]);         // address of the first element
        set_int(a);
        set_pointer(p);           // pass the pointer
        printf("%d %d\n", a, b[0]);
    }
    
    void set_array(int *ptr){
        *ptr = 22;                 // operate on pointer
    }
    
    void set_int(int x){
        x = 22;
    }
    
3 голосов
/ 13 января 2020

Выражения массива являются специальными в C.

6.3.2.1 L-значения, массивы и обозначения функций
...
3 За исключением случаев, когда это операнд оператора sizeof, оператор _Alignof или унарный оператор &, или строковый литерал, используемый для инициализации массива, выражение с типом '' массив типа '' преобразуется в выражение с типом '' указатель на тип '', которое указывает на начальный элемент объекта массива и не является lvalue. Если объект массива имеет класс хранения регистров, поведение не определено.

C 2011 онлайн-черновик

При звонке

set_array(b);

выражение b преобразуется из типа "массив из 4 элементов int" в "указатель на int" (int *), а значением выражения является адрес b[0] , Следовательно, set_array получает только значение указателя (&b[0]), а не массив.

6.7.6.3 Деклараторы функций (включая прототипы)
...
7 Объявление параметра как «массива типа» должно быть скорректировано на «квалифицированный указатель на тип», где квалификаторы типа (если таковые имеются) - те, которые указаны в [ и ] вывода типа массива. Если ключевое слово static также присутствует в [ и ] деривации типа массива, то при каждом вызове функции значение соответствующего фактического аргумента должно обеспечивать доступ к первому элементу массива с at наименьшее количество элементов, указанное в выражении размера.

там же.

По сути, в определении функции любой параметр, объявленный как T a[N] или T a[] должно интерпретироваться как T *a - IOW, параметр обрабатывается как указатель, а не как массив. Ваше определение функции

void set_array(int array[4]){
    array[0] = 22;
}

обрабатывается так, как если бы оно было написано

void set_array(int *array){
    array[0] = 22;
}

Таким образом, массивы вроде как-то-но-не-действительно передаются по ссылке в C. Что действительно происходит, так это то, что указатель на первый элемент массива передается по значению . Параметр array в set_array обозначает отдельный объект в памяти от b, поэтому любые изменения, которые вы вносите в array , сам не влияют на b (IOW, вы можете назначить новое значение в array и оно указывает на другой объект, но это назначение вообще не влияет на b). Вместо этого вы обращаетесь к элементам с b по array.

В виде изображения:

       +---+
    b: |   | b[0] <---+
       +---+          |
       |   | b[1]     |
       +---+          |
       |   | b[2]     |
       +---+          |
       |   | b[3]     |
       +---+          |
        ...           |
       +---+          |
array: |   | ---------+
       +---+

Одним из практических следствий этого является то, что sizeof array возвращает размер указателя типа int *, а не размер массива, в отличие от sizeof b. Это означает, что вы не можете подсчитать количество элементов в массиве, используя трюк sizeof array / sizeof array[0]. Когда вы передаете массив в качестве аргумента функции, вы также должны передать размер массива (то есть количество элементов) в качестве отдельного параметра , если массив не содержит четко определенного значения часового (например, терминатор 0 в строках).

Помните, что выражение a[i] определено как *(a + i) - с учетом начального адреса a, смещение i элементов (не байтов!) с этого адреса и почтите результат. Если a является выражением массива, оно сначала преобразуется в выражение указателя перед выполнением этого вычисления. Вот почему вы можете получить доступ к элементам с b по array - array просто хранит адрес первого элемента b.

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

Мы подделываем семантику передачи по ссылке, передавая указатель фактическому параметру и записывая в разыменованный указатель:

void foo( T *ptr )
{
  *ptr = new_value();  // write a new value to the thing ptr points to
}

void bar( void )
{
  T var;
  foo( &var ); // have foo write a new value to var
}

IOW, записывая *ptr - это то же самое, что писать var. Запись в ptr, OTOH, никак не влияет на foo.

В истинной системе передачи по ссылке (например, Fortran) и формальный аргумент в определении функции, и фактический аргумент в вызове функции обозначают один и тот же объект (в «вещи, которая занимает памяти и может хранить значения "смысл, а не объектно-ориентированный" экземпляр класса "смысл), поэтому в этих системах любые изменения формального аргумента отражаются в фактическом аргументе (приводящем к классу c вопрос по хакерскому тесту : «Вы когда-нибудь меняли значение 4? Неумышленно? На языке, отличном от Fortran?»)

...