Есть ли недостатки для передачи структур по значению в C, а не для передачи указателя?

Есть ли недостатки для передачи структур по значению в C, а не для передачи указателя?

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

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

Есть ли какие-либо причины для этого или против этого?

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

Если вы программируете на C, вы рано или поздно начнете писать функции, которые выглядят следующим образом:

void examine_data(const char *ptr, size_t len)
{
    ...
}

char *p = ...;
size_t l = ...;
examine_data(p, l);

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

Но что происходит, когда вы хотите вернуть такую ​​же информацию? Обычно вы получаете что-то вроде этого:

char *get_data(size_t *len);
{
    ...
    *len = ...datalen...;
    return ...data...;
}
size_t len;
char *p = get_data(&len);

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

Итак, решение, которое я предлагаю, это простая структура

struct blob { char *ptr; size_t len; }

Примеры можно переписать следующим образом:

void examine_data(const struct blob data)
{
    ... use data.tr and data.len ...
}

struct blob = { .ptr = ..., .len = ... };
examine_data(blob);

struct blob get_data(void);
{
    ...
    return (struct blob){ .ptr = ...data..., .len = ...len... };
}
struct blob data = get_data();

По какой-то причине, я думаю, что большинство людей инстинктивно сделают make_data, чтобы сделать указатель на struct blob, но я не понимаю, почему. Он по-прежнему получает указатель и целое число, но гораздо более ясно, что они идут вместе. И в случае get_data невозможно испортить способ, который я описал ранее, поскольку для длины нет входного значения, и должна быть возвращенная длина.

Ответ 1

Для небольших структур (например, point, rect), проходящих по значению, вполне приемлемо. Но, кроме скорости, есть еще одна причина, по которой вам следует осторожно передавать/возвращать большие структуры по значению: пространство стека.

Много программирование на языке C предназначено для встроенных систем, где память стоит на высоте, а размеры стека могут быть измерены в КБ или даже байтах. Если вы передаете или возвращаете структурированные значения по стоимости, копии этих структур будут попадаете в стек, потенциально вызывая ситуацию, когда этот сайт назван в честь...

Если я вижу приложение, которое, как представляется, имеет чрезмерное использование стека, структуры, переданные по значению, являются одной из вещей, которые я ищу в первую очередь.

Ответ 2

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

В зависимости от используемого компилятора структуры могут передаваться через стек или регистры в зависимости от параметров/реализации компилятора

Смотрите: http://gcc.gnu.org/onlinedocs/gcc/Code-Gen-Options.html

-fpcc-структура-возвращение

-freg-структура-возвращение

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

Ответ 3

Чтобы действительно ответить на этот вопрос, нужно углубиться в землю для сбора:

(В следующем примере используется gcc на x86_64. Любой желающий может добавлять другие архитектуры, такие как MSVC, ARM и т.д.)

Пусть у нас есть пример программы:

// foo.c

typedef struct
{
    double x, y;
} point;

void give_two_doubles(double * x, double * y)
{
    *x = 1.0;
    *y = 2.0;
}

point give_point()
{
    point a = {1.0, 2.0};
    return a;
}

int main()
{
    return 0;
}

Скомпилируйте его с полной оптимизацией

gcc -Wall -O3 foo.c -o foo

Посмотрите на сборку:

objdump -d foo | vim -

Это то, что мы получаем:

0000000000400480 <give_two_doubles>:
    400480: 48 ba 00 00 00 00 00    mov    $0x3ff0000000000000,%rdx
    400487: 00 f0 3f 
    40048a: 48 b8 00 00 00 00 00    mov    $0x4000000000000000,%rax
    400491: 00 00 40 
    400494: 48 89 17                mov    %rdx,(%rdi)
    400497: 48 89 06                mov    %rax,(%rsi)
    40049a: c3                      retq   
    40049b: 0f 1f 44 00 00          nopl   0x0(%rax,%rax,1)

00000000004004a0 <give_point>:
    4004a0: 66 0f 28 05 28 01 00    movapd 0x128(%rip),%xmm0
    4004a7: 00 
    4004a8: 66 0f 29 44 24 e8       movapd %xmm0,-0x18(%rsp)
    4004ae: f2 0f 10 05 12 01 00    movsd  0x112(%rip),%xmm0
    4004b5: 00 
    4004b6: f2 0f 10 4c 24 f0       movsd  -0x10(%rsp),%xmm1
    4004bc: c3                      retq   
    4004bd: 0f 1f 00                nopl   (%rax)

За исключением пэдов nopl, give_two_doubles() имеет 27 байтов, а give_point() - 29 байт. С другой стороны, give_point() дает меньшее количество команд, чем give_two_doubles()

Интересно, что мы замечаем, что компилятор смог оптимизировать mov в более быстрых вариантах SSE2 movapd и movsd, Кроме того, give_two_doubles() действительно перемещает данные из памяти и выходит из нее, что замедляет работу.

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

Ответ 4

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

Просто для завершения ответа (полный кредит Родди) использование стека - еще одна причина, не пропускающая структуру по значению, верьте, что отладка - это реальная PITA.

Повторить для комментариев:

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

Ответ 5

Я бы сказал, что простые (не слишком большие) структуры по значению, как параметры, так и возвращаемые значения, являются совершенно законной техникой. Разумеется, нужно позаботиться о том, чтобы структура была либо типом POD, либо семантикой копирования хорошо указаны.

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

Ответ 6

Я думаю, что ваш вопрос очень хорошо подвел итоги.

Другим преимуществом передачи структур по значению является то, что владение памятью является явным. Не стоит задаваться вопросом, есть ли структура из кучи и кто несет ответственность за ее освобождение.

Ответ 7

Одна вещь, которую люди забыли упомянуть до сих пор (или я упустил ее), состоит в том, что у структур обычно есть прокладка!

struct {
  short a;
  char b;
  short c;
  char d;
}

Каждый char - 1 байт, каждый короткий - 2 байта. Насколько велика структура? Нет, это не 6 байтов. По крайней мере, не в более часто используемых системах. В большинстве систем это будет 8. Проблема в том, что выравнивание не является постоянным, оно зависит от системы, поэтому одна и та же структура будет иметь разное выравнивание и разные размеры в разных системах.

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

Ответ 8

Здесь никто не упоминал:

void examine_data(const char *c, size_t l)
{
    c[0] = 'l'; // compiler error
}

void examine_data(const struct blob blob)
{
    blob.ptr[0] = 'l'; // perfectly legal, quite likely to blow up at runtime
}

Члены const struct являются const, но если этот элемент является указателем (например, char *), он становится char *const, а не const char *, который нам действительно нужен. Конечно, мы можем предположить, что const является документом о намерениях, и что любой, кто нарушает это, пишет плохой код (который они есть), но это недостаточно для некоторых (особенно тех, кто просто провел четыре часа, отслеживая причиной сбоя).

Альтернативой может быть создание struct const_blob { const char *c; size_t l } и использование этого, но это довольно беспорядочно - оно попадает в ту же проблему с именованной схемой, с которой я располагаю указателями typedef. Таким образом, большинство людей придерживаются только двух параметров (или, более вероятно, для этого случая, используя библиотеку строк).

Ответ 9

Стр. 150 из учебника по сборке ПК http://www.drpaulcarter.com/pcasm/ содержит ясное объяснение того, как C позволяет функции возвращать структуру:

C также позволяет тип структуры быть используется как возвращаемое значение функции func- Тион. Очевидно, что структура не может быть возвращается в регистр EAX. Различные компиляторы справляются с этим положение дел по-разному. Общий решение, используемое компиляторами для внутренне переписать функцию как одну который принимает указатель структуры как параметр. Указатель используется для возвращаемое значение в структуру определяется вне подпрограммы.

Я использую следующий код C для проверки вышеуказанного утверждения:

struct person {
    int no;
    int age;
};

struct person create() {
    struct person jingguo = { .no = 1, .age = 2};
    return jingguo;
}

int main(int argc, const char *argv[]) {
    struct person result;
    result = create();
    return 0;
}

Используйте "gcc -S" для создания сборки для этого фрагмента кода C:

    .file   "foo.c"
    .text
.globl create
    .type   create, @function
create:
    pushl   %ebp
    movl    %esp, %ebp
    subl    $16, %esp
    movl    8(%ebp), %ecx
    movl    $1, -8(%ebp)
    movl    $2, -4(%ebp)
    movl    -8(%ebp), %eax
    movl    -4(%ebp), %edx
    movl    %eax, (%ecx)
    movl    %edx, 4(%ecx)
    movl    %ecx, %eax
    leave
    ret $4
    .size   create, .-create
.globl main
    .type   main, @function
main:
    pushl   %ebp
    movl    %esp, %ebp
    subl    $20, %esp
    leal    -8(%ebp), %eax
    movl    %eax, (%esp)
    call    create
    subl    $4, %esp
    movl    $0, %eax
    leave
    ret
    .size   main, .-main
    .ident  "GCC: (Ubuntu 4.4.3-4ubuntu5) 4.4.3"
    .section    .note.GNU-stack,"",@progbits

Стек перед вызовом create:

        +---------------------------+
ebp     | saved ebp                 |
        +---------------------------+
ebp-4   | age part of struct person | 
        +---------------------------+
ebp-8   | no part of struct person  |
        +---------------------------+        
ebp-12  |                           |
        +---------------------------+
ebp-16  |                           |
        +---------------------------+
ebp-20  | ebp-8 (address)           |
        +---------------------------+

Стек сразу после вызова create:

        +---------------------------+
        | ebp-8 (address)           |
        +---------------------------+
        | return address            |
        +---------------------------+
ebp,esp | saved ebp                 |
        +---------------------------+

Ответ 10

Я просто хочу указать одно преимущество передачи ваших структур по значению в том, что оптимизирующий компилятор может лучше оптимизировать ваш код.