LINUX.ORG.RU

Ошибки при сборке тренировочного задания по C

 , , , ,


1

2

Есть код на C, для выполнения тренировочного задания: напишите программу для вывода всех строк входного потока, имеющих длину более 80 символов. Вот код: http://ix.io/12jI

Собираю с помощью GCC, вот выхлоп "cc -v"

Using built-in specs.
COLLECT_GCC=cc
COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-linux-gnu/4.9/lto-wrapper
Target: x86_64-linux-gnu
Configured with: ../src/configure -v --with-pkgversion='Debian 4.9.2-10' --with-bugurl=file:///usr/share/doc/gcc-4.9/README.Bugs --enable-languages=c,c++,java,go,d,fortran,objc,obj-c++ --prefix=/usr --program-suffix=-4.9 --enable-shared --enable-linker-build-id --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --with-gxx-include-dir=/usr/include/c++/4.9 --libdir=/usr/lib --enable-nls --with-sysroot=/ --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --enable-gnu-unique-object --disable-vtable-verify --enable-plugin --with-system-zlib --disable-browser-plugin --enable-java-awt=gtk --enable-gtk-cairo --with-java-home=/usr/lib/jvm/java-1.5.0-gcj-4.9-amd64/jre --enable-java-home --with-jvm-root-dir=/usr/lib/jvm/java-1.5.0-gcj-4.9-amd64 --with-jvm-jar-dir=/usr/lib/jvm-exports/java-1.5.0-gcj-4.9-amd64 --with-arch-directory=amd64 --with-ecj-jar=/usr/share/java/eclipse-ecj.jar --enable-objc-gc --enable-multiarch --with-arch-32=i586 --with-abi=m64 --with-multilib-list=m32,m64,mx32 --enable-multilib --with-tune=generic --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu
Thread model: posix
gcc version 4.9.2 (Debian 4.9.2-10)
Получаю ошибки:
hw.c:5:5: error: conflicting types for ‘getline’
 int getline(char line[], int limit);
     ^
In file included from hw.c:1:0:
/usr/include/stdio.h:678:20: note: previous declaration of ‘getline’ was here
 extern _IO_ssize_t getline (char **__restrict __lineptr,
                    ^
hw.c:16:5: error: conflicting types for ‘getline’
 int getline(char line[], int limit) {
     ^
In file included from hw.c:1:0:
/usr/include/stdio.h:678:20: note: previous declaration of ‘getline’ was here
 extern _IO_ssize_t getline (char **__restrict __lineptr,
Не могу понять, что не так. Пожалуйста, помогите разобраться.

★★
Ответ на: комментарий от Norong

Размер типа - это кол-во бит под переменные этого типа?

Да.

Тут же вроде не он нужен, а размер в смысле кол-ва значащих бит конкретной переменной.

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

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

Тогда я, наверное, просто свою примитивную реализацию логарифма сделаю, для решения задачи хватит.

Norong ★★
() автор топика
Ответ на: комментарий от xaizek

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

Вот функция, как она приведена в учебнике:

/* binsearch(x, v, n): поиск x в v[0] <= v[1] <= ... <= v[n-1] (версия K&R) */
int binsearch(int x, int v[], int n) {
    int low, high, mid;

    low = 0;
    high = n - 1;
    while (low <= high) {
        mid = (low + high) / 2;
        if (x < v[mid])
            high = mid - 1;
        else if (x > v[mid])
            low = mid + 1;
        else
            return mid;
    }
    return -1;
}

Сам я не смог придумать другой реализации (с одной проверкой внутри цикла) и полез искать решения, нашёл вот такой вариант:

/* binsearch2(x, v, n): поиск x в v[0] <= v[1] <= ... <= v[n-1] (версия с одной проверкой внутри цикла) */
int binsearch2(int x, int v[], int n) {
    int low, high, mid;

    low = 0;
    high = n - 1;
    mid = (low + high) / 2;
    while (low <= high && x != v[mid]) {
        if (x < v[mid])
            high = mid - 1;
        else
            low = mid + 1;
        mid = (low + high) / 2;
    }
    if (x == v[mid])
        return mid;
    else
        return -1;
}

Меня сразу смутила мысль о том, что они, с точки зрения кол-ва проверок, одинаковые. Да, во втором варианте проверка x != v[mid] находится не в теле цикла, но является частью условия, которое проверяется перед каждым входом в цикл, так что, количество проверок в первой и во второй функции, вроде, одинаковое, +-1 проверка. Когда же я выполнил по очереди программы, работающие с первой и второй функцией соответственно, и замерил время их работы с помощью time, time везде показал по нулям - программы выполнились мгновенно. В связи со всем этим у меня возникло несколько вопросов:

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

Не мог бы ты помочь мне разобраться в этой ситуации? Заранее огромное спасибо.

Norong ★★
() автор топика
Ответ на: комментарий от Norong

У меня вот так получалось:

int binsearch(int x, int v[], int n)
{
    int high = n - 1;
    int low = 0;
    int mid;

    while (low + 1 < high) {
        mid = (low + high) / 2;
        if (x < v[mid])
            high = mid - 1;
        else
            low = mid;
    }

    if (x == v[high])
        return high;

    if (x == v[low])
        return low;

    return -1;
}

anonymous
()
Ответ на: комментарий от Norong

Что же имели в виду авторы, когда говорили о варианте с одной проверкой внутри цикла?

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

Как следовало написать эту функцию, чтобы она работала максимально эффективно?

С одной проверкой внутри должно быть эффекривнее чем с двумя. Ещё используют подобные микрооптимизации условий цикла, но наизусть не помню какие. Дальше оптимизировать мало вариантов, тело цикла уже маленькое.

Как можно сравнить скорость работы разных реализаций одного и того же, если время работы в любом случае очень мало?

Делать не один поиск, а, скажем, 10 млн. поисков. Для точного сравнения много чего выдумывают, есть целые статьи с рекомендациями, но многократного повторения для таких задач на интерес хватит (можно ещё сначала прокрутить пару сотен тысяч для «разогрева», но тогда замерять надо будет уже в коде).

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

В вариант анонима я вник и суть понял. Что касается проверки скорости - так впредь и буду делать, спасибо. Хочется сразу писать оптимальный код, ведь вдруг когда-нибудь написанное реально будет выполняться много раз и нюансы будут играть большую роль.

Norong ★★
() автор топика
Ответ на: комментарий от xaizek

У меня возникла пара вопросов насчёт оформления кода. В K&R функцию main определяют просто как

main() { ...

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

int main() { ...

Правильно ли я понимаю, что такой вариант как минимум ничем не хуже, чем просто main?

И ещё: я прочитал, что для указания на то, что функция не принимает никаких аргументов, можно указывать void в списке аргументов при написании прототипа или при определении функции. В то же время, в K&R используются просто пустые скобки. Какой из этих вариантов является более правильным?

Norong ★★
() автор топика
Последнее исправление: Norong (всего исправлений: 1)
Ответ на: комментарий от Norong

Правильно ли я понимаю, что такой вариант как минимум ничем не хуже, чем просто main?

Вариант без типа возвращаемого значения объявлен deprecated, так что стоит указывать.

Какой из этих вариантов является более правильным?

Писать (void), () означает, что функция принимает любые аргументы (если в C11 ничего не поменялось, но, вроде, не меняли).

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

Мне с невысоких позиций середины третьей главы кажется, что функция main должна возвращать значение типа int (exit-коды), а вот про возможные аргументы я пока не в курсе. Пожалуйста, если тебе не трудно, не мог бы ты рассказать про те сигнатуры, о которых ты говорил?

Norong ★★
() автор топика
Ответ на: комментарий от Norong

Две стандартные сигнатуры:

int main(void);
int main(int argc, char *argv[]);
Но реализации могут предоставлять дополнительные. Обычно ещё есть такая с переменными окружения:
int main(int argc, char *argv[], char *env[]);
В стандарте про это в секции «Program startup», его, кстати, можно скачать (черновики бесплатно, сами стандарты есть на торрентах; вообще и черновики годятся, там различия с финальной версией в форматировании) и подглядывать туда (требуется практика в нахождении нужного, ибо документ написан в формальном стиле, но поиск помогает в случаях как этот).

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

Ага, всё понял. Гуглинг ещё показал, что это - для тех случаев, когда сама программа вызывается с какими-нибудь параметрами. Стандарт буду иметь в виду. Спасибо!

Norong ★★
() автор топика
Ответ на: комментарий от xaizek

И снова я :)

Есть функция itoa, преобразующая число в строку, вот код, работающий с ней: https://gist.github.com/anonymous/2113a011f37be8be0a30274b01f77fb0

Задание 3.4 гласит: «В представлении чисел с помощью дополнения до двойки наша версия функции itoa не умеет обрабатывать самое большое по модулю отрицательное число, т.е. значение n, равное -(2^длина_слова - 1). Объясните, почему это так. Доработайте функцию так, чтобы она выводила это число правильно независимо от системы, в которой она работает.»

Я так понял, что представление чисел с помощью дополнения до двойки - это один знаковый разряд (0 = +, 1 = -), а все остальные - под модуль числа, и тогда самое большое по модулю отрицательное - это, вроде как, ~0 (все единицы, которые помещаются в переменную).

Вспомнив, что целочисленные знаковые переменные принимают значения в диапазоне [-2 ^ (n - 1); 2 ^ (n - 1) - 1], где n - кол-во бит под переменную (правда, у меня это плохо в сознании сочетается с представлением чисел с помощью дополнения до двойки. Допустим, есть 5 бит. Тогда диапазон значений - от -16 до 15, как раз всего 32 разных значения. Максимум - это 01111 (0 означает +, 1111 = 15), но как быть с минимумом? По идее, на первом месте там 1, чтобы обозначить минус, но 16-то я уже никак не наберу 4 разрядами. Похоже, я что-то не так понял. Есть подозрение, что как-то замешан 10000 или 00000 - два нуля разного знака кажутся странными немного), я предположил, что для данного числа проблема возникает в строке 21. Как я понимаю, если n - максимальное по модулю отрицательное число, -n в ту же переменную уже не поместится. Так что, я завёл отдельную переменную типа unsigned int, и модуль числа n записывал в эту новую переменную un, дальше работал именно с ней. Однако, нужного результата в строке я всё равно не получил.

Пожалуйста, помоги мне разобраться во всем этом хитросплетении, если у тебя будет время и желание.

Norong ★★
() автор топика
Ответ на: комментарий от Norong

Там не модуль. Отрицательное число в этой системе получается инверсией бит и добавлением единицы. Единица и все нули это минимальное отрицительное число. Числа идут так:

0 1 ... 15 -16 -15 ... -1
Отрицательного нуля тут нет (он есть в обратном и прямом кодах, модуль числа, кстати, в прямом коде).

Про 21 строку и unsigned int правильно мыслишь, только унарный минус для минимального числа даст это же самое число (можно легко в этом убедиться самому), поэтому преобразование в положительное число unsigned типа надо делать вручную.

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

Так, эту теорию я понял, спасибо!

Вот что у меня получилось: https://gist.github.com/anonymous/f78942ff74945673256494055d9ddaf9

В строках 16 - 18 я рассчитываю то самое минимальное отрицательное число, которое требует особой обработки. В строках 34 - 42 происходит следующее:

  • Если число - минимальное отрицательное, то я просто записываю его в переменную типа unsigned, потому что, как я понял, в signed (если, допустим, под переменную отведено 5 бит) значения идут в порядке: 14 15 -16, а в unsigned в порядке 14 15 16, т.е чтобы из минимального отрицательного signed сделать положительное unsigned, менять число не нужно, достаточно лишь изменить способ его «восприятия» с signed на unsigned (кстати, а не должно было в этом месте произойти изменение самих данных из-за того, что я переменной типа unsigned присваиваю значение переменной типа int?)
  • Для остальных отрицательных чисел предыдущий метод не работает, но для них унарный минус даёт положительное число, поэтому это положительное число я записываю в un.
  • Остальные числа - неотрицательные, они представляются одинаково в signed и unsigned, un = n.

Этот код нормально работает для всех чисел.

Norong ★★
() автор топика
Ответ на: комментарий от Norong

кстати, а не должно было в этом месте произойти изменение самих данных из-за того, что я переменной типа unsigned присваиваю значение переменной типа int?

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

Этот код нормально работает для всех чисел.

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

un = (unsigned)~n + 1;
Это даст правильное абсолютное значение для любого отрицательного.

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

А зачем указание (unsigned)? Там же, вроде, в любом случае будет преобразование типов. Или нет?

С самой сутью преобразования разобрался. Я правильно понимаю, что ~n + 1 работает в любую сторону, делая из положительного числа отрицательное и наоборот?

Norong ★★
() автор топика
Ответ на: комментарий от Norong

А зачем указание (unsigned)? Там же, вроде, в любом случае будет преобразование типов. Или нет?

Будет, но оно произойдёт уже после переполнения (INT_MAX + 1), а переполнение знаковых чисел это undefined behaviour.

С самой сутью преобразования разобрался. Я правильно понимаю, что ~n + 1 работает в любую сторону, делая из положительного числа отрицательное и наоборот?

Да, преобразование работает одинаково (кроме минимального числа, когда оно не работает).

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

Разобрался, спасибо!

У меня вопрос не по теме: ты ведь используешь цветовую схему lucius в vim? Мне она пришлась по нраву, но почему-то при таком содержимом .vimrc:

"GUI
if has('gui_running')
	set guioptions-=t
        colorscheme lucius
	set guifont=Liberation\ Mono\ 10
"Terminal
else
    colorscheme lucius
endif
если я запускаю vim в терминале, то у меня сначала текст просто белый, никакой подсветки. Если переключиться на какую-нибудь другую схему и опять сделать colorscheme lucius, то заработает нормально. Ты с таким не сталкивался?

Norong ★★
() автор топика
Ответ на: комментарий от Norong

Да, её, хоть и старую версию (новая какая-то слишком яркая). Может просто не хватает

set background=dark
для консольного варианта?

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

Не помогло, но я посмотрел на ту тему, которую использовал раньше (256-grayvim) и увидел там set t_Co=256. После добавления этого в .vimrc у меня и люциус заработал, как надо. Вопрос: как считаешь, set t_Co=256 надо в .vimrc писать в секцию для терминала, или же в саму цветовую схему добавить? И ещё - для гуя ведь это не нужно, да?)

Norong ★★
() автор топика
Последнее исправление: Norong (всего исправлений: 1)
Ответ на: комментарий от Norong

Только хотел спросить за t_Co. И ещё выяснилось, что 'background' сбрасывается командой `:hi Normal ...`.

Прописывать стоит в .vimrc так как это настройка терминала.

t_Co для gui не надо, но оно и не мешает. А вообще при правильном $TERM оно само выставляется в 256, так что может стоит $TERM установить в «что-то-256color».

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

Я практикую xfce4-terminal и zsh. В .zshrc ничего про TERM не сказано. Если я пропишу в TERM поддержку 256 цветов (сейчас у меня TERM=xterm, эмулятор - xfce4-terminal, прописать надо xterm-256color, как я понимаю), хуже не станет там, где 256 не поддерживаются (например, на tty)? И ещё: мне в .zshrc писать, или где-то в другом месте?

Norong ★★
() автор топика
Ответ на: комментарий от Norong

Там в настройках xfce4-terminal где-то должно быть. Он выставляет $TERM. Может просто опция «256 цветов», а может поле для ввода (xterm-256color).

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

Там можно выставить режим эмуляции, но кроме xterm там нет вариантов. Я, наверное, тогда просто в vim t_Co установлю, остальное трогать не буду. Ведь если я в vim включу 256, но терминалом 256 не поддерживаются, ничего не сломается, он просто будет показывать, как без поддержки?

И, возвращаясь к коду:

un = (unsigned) ~n + 1;

Переполнение int'а у меня ведь может произойти только для минимального отрицательного числа при выполнении ~n, а также ещё и для предыдущего числа (на 1 больше, чем минимальное) при выполнении +1, так?

(unsigned) здесь означает, что всё, что дальше, имеет тип unsigned? Кстати, может, этого в K&R не было, или я подзабыл, но вот в такой ситуации, как здесь, у меня сначала выполняется ~n, потом +1, потом результат записывается в un. Пока выполняются вычисления, где хранится результат вычислений? Как я понимаю, они происходят в чём-то, что имеет тип int (потому что n имеет тип int), из-за этого может возникнуть переполнение, а (unsigned) делает так, чтобы вычисления происходили в чём-то типа unsigned и результат был таким же.

Norong ★★
() автор топика
Ответ на: комментарий от Norong

Ведь если я в vim включу 256, но терминалом 256 не поддерживаются, ничего не сломается, он просто будет показывать, как без поддержки?

Чёрно-белое или цвета по соседству должны быть, хотя детали могут зависеть от терминала.

Переполнение int'а у меня ведь может произойти только для минимального отрицательного числа при выполнении ~n, а также ещё и для предыдущего числа (на 1 больше, чем минимальное) при выполнении +1, так?

От ~ переполнения не будет, только при сложении.

(unsigned) здесь означает, что всё, что дальше, имеет тип unsigned?

Только выражение, которое следует за ним.

Пока выполняются вычисления, где хранится результат вычислений?

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

un = n;
un = ~un;
un = un + 1;

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

А тип временной переменной определяется из тех же соображений, из каких определяется тип результата?

И насчёт переполнения: я правильно понимаю, что переполнение возникает, когда для нового значения не хватает бит в переменной, т.е, например, в случае вроде?

В n 5 бит.
n = 0
~n = 11111
~n + 1 = 100000 // переполнение
Если так, то, наверное, в нашем случае оно может возникнуть только для n = 0, потому что тогда ~n = все_единицы, ~n + 1 даёт выход в следующий разряд?

Norong ★★
() автор топика
Ответ на: комментарий от Norong

А тип временной переменной определяется из тех же соображений, из каких определяется тип результата?

Да, всё в соответствии с операциями. +, ~ и подобные тип не меняют, поэтому он остаётся исходным int.

переполнение возникает, когда для нового значения не хватает бит в переменной, т.е, например, в случае вроде?

Это для беззнаковых и такое переполнение (оборачивание, в данном случае) разрешено. Для знаковых переполнение это когда затирается знаковый разряд при операции, т.е. сложили INT_MAX и 1, оба числа положительные, а получили отрицательный результат.

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

Так, про знаковые понял. А у беззнаковых при переполнении вида INT_MAX + 1 получается ноль, да?

Norong ★★
() автор топика
Ответ на: комментарий от Norong

А у беззнаковых при переполнении вида INT_MAX + 1 получается ноль, да?

Да, только UINT_MAX. Ну и наоборот (unsigned)0 - 1 == UINT_MAX. ( (unsigned)0 также можно записать как 0U).

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

Здравствуй! И снова я кое-что не полностью понимаю. Вот страница: https://i.imgur.com/qgxG5PA.png

Моё непонимание начинается со слов «В свете всего сказанного...» посередине страницы. Говорится о том, что отсутствует прототип функции, и идёт неявное объявление, но ведь в самом начале функции main объявлены и типы значений, возвращаемых функциями atof, getline, и типы аргументов, принимаемых этими функциями. Кроме того, если в такой ситуации возникают какие-то проблемы, мне не ясно, зачем объявлять функции внутри main, если раньше писался прототип вне какой-либо функции и с ним всё было хорошо. Ну и наконец тут говорится про отсутствие проверки на соответствие типов, если функция компилируется отдельно. Когда я includ'ю стандартные заголовочные файлы, то для функций, содержащихся в них, проверка типов происходит. Благодаря чему так получается, или, вернее, при каком условии будет то самое отсутствие проверки, о котором говорится в тексте? Пожалуйста, помоги мне разобраться в этом, если тебе не трудно.

Norong ★★
() автор топика
Ответ на: комментарий от Norong

Говорится о том, что отсутствует прототип функции, и идёт неявное объявление, но ведь в самом начале функции main объявлены и типы значений, возвращаемых функциями atof, getline, и типы аргументов, принимаемых этими функциями.
Благодаря чему так получается, или, вернее, при каком условии будет то самое отсутствие проверки, о котором говорится в тексте?

Это баг перевода, в оригинале «если отсутствует прототип функции» (а не «здесь», которое они взяли не пойми откуда).

Кроме того, если в такой ситуации возникают какие-то проблемы, мне не ясно, зачем объявлять функции внутри main, если раньше писался прототип вне какой-либо функции и с ним всё было хорошо.

Сейчас так объявлять смысла особого нет. Оно сокращает область видимости объявления, но увеличивает вероятность ошибок, что плохо.

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

Спасибо! Скачал и положил рядом оригинал, буду туда смотреть, если натолкнусь на странное.

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

Меня вот эти слова смутили: The function atof must be declared and defined consistently. If atof itself and the call to it in main have inconsistent types in the same source file...

Или там речь только об объявлении и определении на самом деле?

Norong ★★
() автор топика
Ответ на: комментарий от Norong

Правильно, хотя, диапазон того, что разрешено, шире чем кажется. «Указатель преобразуется в число» и в обратную сторону являются предупреждениями, а не ошибками.

Или там речь только об объявлении и определении на самом деле?

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

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

Т.е. если я просто скомпилирую два исходника, в одном из которых используется некая функция, но она там не объявлена никаким образом, а в другом исходнике определена эта функция, то код скомпилируется, но без проверки типов аргументов и возвращаемого значения (и может получиться хаос). Если в первом исходнике будет объявление, но не согласованное с определением во втором исходнике, то тоже может получиться хаос, а если в первом исходнике видно (напрямую или через include хедера) согласованное объявление - всё будет хорошо?

И ещё - я правильно понимаю, что объявление нужно для использования, а если у меня в некотором исходнике функция просто определяется, то в нём не нужно объявление?

И какие ошибки может вызвать объявление функции внутри другой функции, кроме как то, что она не будет видна за её пределами?

Norong ★★
() автор топика
Ответ на: комментарий от Norong

... всё будет хорошо?

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

И ещё - я правильно понимаю, что объявление нужно для использования, а если у меня в некотором исходнике функция просто определяется, то в нём не нужно объявление?

Да.

И какие ошибки может вызвать объявление функции внутри другой функции, кроме как то, что она не будет видна за её пределами?

Типу легче разойтись с реальным типом функции, если её обновят, то в функции с локальным объявлением всё останется как и раньше.

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

Только не уверен какой статус у этого механизма вывода определения типа функции при её первом использовании

Это то самое автоматическое предположение, что функция будет возвращать int?

Norong ★★
() автор топика
Ответ на: комментарий от xaizek

Спасибо большое! Пошёл читать дальше :)

Norong ★★
() автор топика
Ответ на: комментарий от Norong

Всё таки удалили в C99:

Remove implicit function declaration.

Т.е. с c99 объявление функции перед использованием обязательно.

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

И снова я. Возник совсем небольшой вопрос: в первой главе про внешние переменные было сказано:

Внешняя переменная должна быть определена ровно один раз за пределами всех функций программы. ... Она также должна быть объявлена в каждой функции, которая к ней обращается, с указанием её типа. Объявление может быть явным, с помощью оператора extern, а может быть и неявным - по контексту.

Позже, в параграфе, посвящённом внешним переменным, много где используются внешние переменные без какого-либо объявления внутри отдельных функций, их использующих (например: https://i.imgur.com/eRJyE7E.png). Это и есть то самое неявное объявление по конексту? Если да, то в каких случаях оно уместно, а в каких лучше эти переменные всё-таки extern'ом объявлять?

Norong ★★
() автор топика
Последнее исправление: Norong (всего исправлений: 1)
Ответ на: комментарий от Norong

Наверное, имеется в виду, что extern это явное, а если оно просто объявлено до функции, то это «по контексту». Объявлять внутри функции обычно не надо, разве что с environ (man environ) иногда так делают, так как используют лишь в одной функции.

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

так как используют лишь в одной функции.

А объявление с помощью extern внутри функции на что-нибудь влияет?

Norong ★★
() автор топика
Ответ на: комментарий от Norong

На видимость этой переменной внутри этой самой функции, если объявления данной переменной нет в глобальном контексте.

xaizek ★★★★★
()
Ответ на: комментарий от Norong

Внешняя, если где-то объявлена в другом объектном файле или библиотеке и в принципе доступна для обращения. extern просто делает её доступной в какой-то конкретной области видимости.

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

Т.е. я правильно понимаю, что переменная будет внешней, если она объявлена где-то, и это место объявления видно на этапе компиляции (тот же исходник, другой объектный файл, подключённый заголовочный файл), но для её использования необходимо, чтобы в том исходнике, о котором идёт речь, она была объявлена - либо без extern вне функций, либо с extern внутри каждой функции, где она нужна?

Norong ★★
() автор топика
Ответ на: комментарий от Norong

Все глобальные переменные, которые не static по определению внешние. Но если они в отдельном файле, то компилятор об этом не узнает без явного объявления. include может предоставлять такое объявление, а можно сделать это самому вне/в функции. Вне функций тоже можно с extern объявлять, кстати.

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

Вне функций тоже можно с extern объявлять, кстати.

А эффект будет отличаться от объявления без extern?

Norong ★★
() автор топика
Ответ на: комментарий от Norong

Хотя бы одно объявление во множестве файлов должно быть без extern иначе переменная не будет создана нигде.

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

Спасибо, понял!

У меня ещё небольшое недопонимание возникло. Вот есть код из учебника:

wget "http://ix.io/161D" -O - | base64 -d | tar -x

(По ссылке - tar с несколькими файлами исходного кода, закодированный в base64. После выполнения команды в текущем каталоге будут лежать исходники, 4 файла)

Вот выхлоп от компиляции:

getch.c: In function ‘ungetch’:
getch.c:12:9: warning: incompatible implicit declaration of built-in function ‘printf’
         printf("ungetch: too many characters\n");
         ^
getop.c: In function ‘getop’:
getop.c:26:14: error: ‘EOF’ undeclared (first use in this function)
     if (c != EOF)
              ^
getop.c:26:14: note: each undeclared identifier is reported only once for each function it appears in
getop.c:28:12: error: ‘NUMBER’ undeclared (first use in this function)
     return NUMBER;
            ^
pushpop.c: In function ‘push’:
pushpop.c:11:9: warning: incompatible implicit declaration of built-in function ‘printf’
         printf("error> stack full, can't push %g\n", f);
         ^
pushpop.c: In function ‘pop’:
pushpop.c:19:9: warning: incompatible implicit declaration of built-in function ‘printf’
         printf("error: stack empty\n");
         ^
Две ошибки: в getop.c неизвестно, что есть EOF и что есть NUMBER (NUMBER - это символическая константа, созданная в main.c). Три предупреждения: в ungetch, push и pop неявное объявление printf.

Если я в getop.c добавлю

#include <stdio.h>

#define NUMBER '0'
то ошибки исчезают, остаются только предупреждения.

Я понимаю происходящее так:

Предупреждения вызваны тем, что printf из stdio.h виден для функций ungetch, push и pop через main.c, но в их файлах исходного кода не объявлен (не будет проверки аргументов; чтобы она была, надо объявить (через include)).

Ошибка с EOF вызвана тем, что EOF (видимо, являющийся символической константой) тоже определён в stdio.h, но для getop не виден (кстати,

grep -R "#define EOF" /usr/include

не находит определения EOF).

Ошибка с NUMBER вызвана теми же причинами, что и с EOF - определён в main.c, но для getop не виден.

Правильно ли я понимаю природу этих явлений и то, что константы и макросы (#define) должны объявляться непосредственно в каждом исходнике, в котором они используются (напрямую или через include)?

Norong ★★
() автор топика
Ответ на: комментарий от Norong

Предупреждения вызваны тем, что printf из stdio.h виден для функций ungetch, push и pop через main.c, но в их файлах исходного кода не объявлен (не будет проверки аргументов; чтобы она была, надо объявить (через include)).

Через main.c компилятору ничего не видно. При компиляции других файлов, он без понятия о том, что находится в других (если они не включены через include).

То что не объявлен без include <stdio.h> это да.

grep -R «#define EOF» /usr/include

Оно может быть где-то в /usr/lib64/gcc.

константы и макросы (#define) должны объявляться непосредственно в каждом исходнике, в котором они используются (напрямую или через include)?

Да. Они существуют только во время компиляции, в выходе компиляции есть только результат их применения.

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

Получается, что gcc в принципе позволяет использовать функции из стандартных библиотек без include, но так делать не стоит (из соображений эстетики и проверки аргументов)?

Norong ★★
() автор топика
Ответ на: комментарий от Norong

Ага. И, если аргументы будут угаданы неправильно, произойдёт ошибка во время выполнения. Так что лучше так не делать.

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

И вот насчёт этого

При компиляции других файлов, он без понятия о том, что находится в других (если они не включены через include).

У меня в некоторых файлах определены функции, которые объявляются и используются в других файлах. Получается, что компилятор, собирая исходник, в котором объявляется и используется функция из другого исходника, не может быть уверен, что такая функция вообще существует? Я попробовал закомментировать функцию push и собрать исходники - получил:

/tmp/ccOrluGk.o: In function `main':
main.c:(.text+0x38): undefined reference to `push'
main.c:(.text+0x5c): undefined reference to `push'
main.c:(.text+0x80): undefined reference to `push'
main.c:(.text+0xa2): undefined reference to `push'
main.c:(.text+0xd7): undefined reference to `push'
collect2: error: ld returned 1 exit status
Я правильно понимаю, что это уже проблемы не компиляции, а компоновки?

Norong ★★
() автор топика
Ответ на: комментарий от Norong

Я правильно понимаю, что это уже проблемы не компиляции, а компоновки?

Да. Компилятор там уже не участвует.

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

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

Norong ★★
() автор топика
Последнее исправление: Norong (всего исправлений: 3)
Ответ на: комментарий от Norong

Только по вызовам (они дают неразрешённые символы в o-файлах), объявления следов в объектных файлов не оставляют.

А компилятор работает только с обособленными единицами трансляции, которые получаются после подстановки include и разворачивания макросов (если считать препроцессор отдельным элементом, который это и делает).

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

Большое спасибо, картина сложилась!

Что бы я делал без твоей помощи..? Мне даже неловко. Тебя это не очень затрудняет?

Norong ★★
() автор топика
Ответ на: комментарий от Norong

Та не, я вон тоже заодно освежаю некоторые спорные моменты в памяти, подглядывая в стандарт в процессе.

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

У меня возникли ещё вопросы о внешних переменных:

Внешняя переменная должна быть определена ровно один раз за пределами всех функций программы. ... Она также должна быть объявлена в каждой функции, которая к ней обращается, с указанием её типа. Объявление может быть явным, с помощью оператора extern, а может быть и неявным - по контексту.

Какая в контексте этого абзаца разница между определением переменной и её объявлением?

Дальше:

ровно один раз за пределами всех функций программы.

А если у меня несколько файлов исходного кода, и я хочу, чтобы внешняя переменная из одного файла использовалась в других, то я должен как минимум в одном файле исходного кода (любом) объявить переменную без extern вне функций, а в остальных - с extern или без вне функций, и/или с extern внутри тех функций, в которых я хочу её использовать? А могу ли я такую переменную объявить внутри функции без extern, и будет ли разница?

Ты говорил:

Хотя бы одно объявление во множестве файлов должно быть без extern иначе переменная не будет создана нигде.

Правильно ли я понимаю, что без extern происходит либо отведение памяти под переменную, либо обращение к уже существующей, а с extern - только обращение к уже существующей? Если да, то что из этого делает компилятор, а что - компоновщик?

Norong ★★
() автор топика
Последнее исправление: Norong (всего исправлений: 1)
Ответ на: комментарий от Norong

Какая в контексте этого абзаца разница между определением переменной и её объявлением?

Определение чаще всего то, которое с инициализацией. Т.е. так делать нельзя:

int a = 1;
int a = 2;
Но так как определение может быть и без инициализации, то каждое объявление трактуется двояко: может быть определение с инициализацией нулём, а может и только лишь объявление. Я, пользуясь этой неоднозначность, сделал авторегистрацию тестов на C (тут что-то расписано, правда там текста много, а в комментариях люди пишут, что всё равно не поняли как оно работает). Так вот, всё неоднозначное объявляется в объектном файле как common symbol (C) (в man nm есть описания). А при инициализации переменная определяется как нормальная инициализированная переменная (D). Линкер потом это совмещает и если есть только C, то оно и остаётся, но если где-то есть D, то берётся этот символ. Если есть два D, то будет ошибка из-за двойного определения.

А если у меня несколько файлов исходного кода, и я хочу, чтобы внешняя переменная из одного файла использовалась в других, то я должен как минимум в одном файле исходного кода (любом) объявить переменную без extern вне функций, а в остальных - с extern или без вне функций, и/или с extern внутри тех функций, в которых я хочу её использовать? А могу ли я такую переменную объявить внутри функции без extern, и будет ли разница?

Да, только без «extern» в функции это будет обычная локальная переменная.

Правильно ли я понимаю, что без extern происходит либо отведение памяти под переменную, либо обращение к уже существующей, а с extern - только обращение к уже существующей? Если да, то что из этого делает компилятор, а что - компоновщик?

Правильно. Как выше написано компилятор пытается угадать, а линкер имея все объектные файлы принимает решение, что попадёт в исполняемый файл.

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

Но так как определение может быть и без инициализации

Под определением без инициализации подразумевается объявление (тип название; как я понимаю)?

каждое объявление трактуется двояко: может быть определение с инициализацией нулём, а может и только лишь объявление.
Так вот, всё неоднозначное объявляется в объектном файле как common symbol (C)

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

Как выше написано компилятор пытается угадать

Дико извиняюсь, а где выше про компилятор? Про компоновщик видел :) Или это в статье на хабре? Я её завтра на более свежий мозг прочитаю.

Norong ★★
() автор топика
Ответ на: комментарий от Norong

Под определением без инициализации подразумевается объявление (тип название; как я понимаю)?

Да, объявление без extern.

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

Нет, глобальные заполняются нулями, если инициализатор отсутствует. Они не инициализированны пользователем, поэтому об этом заботится язык/система.

а где выше про компилятор?

Там где про символы C и D типов, их же компилятор создаёт на основе кода.

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

Что-то у меня в голове плохо совмещаются вот эти данные:

каждое объявление трактуется двояко: может быть определение с инициализацией нулём, а может и только лишь объявление
Нет, глобальные заполняются нулями, если инициализатор отсутствует.
Так вот, всё неоднозначное объявляется в объектном файле как common symbol (C)

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

Или там логика такая: не указано, что именно будет в переменной при её объявлении -> неоднозначность -> в объектном файле обозначаем как «C», и фишка именно в том, что в исходнике ничто про значение не указано?

Norong ★★
() автор топика
Ответ на: комментарий от Norong

может быть определение с инициализацией нулём
глобальные заполняются нулями, если инициализатор отсутствует
всё неоднозначное объявляется в объектном файле как common symbol (C)

Вот это всё одно и то же и происходит в этом случае:

int i;
double d;
void *bla;
<type> name;

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

В том то и дело, что нет, поэтому делает допущение, что это определение, полагаясь на линкер. Линкер потом разберётся на основании того, будет ли где-то объявление с явной инициализацией или нет. У определения с инициализацией приоритет выше, поэтому компилятор может безопасно предполагать, что видит определение.

Или там логика такая: не указано, что именно будет в переменной при её объявлении -> неоднозначность -> в объектном файле обозначаем как «C», и фишка именно в том, что в исходнике ничто про значение не указано?

Да, вроде такого.

xaizek ★★★★★
()
Последнее исправление: xaizek (всего исправлений: 2)
Ответ на: комментарий от xaizek

Спасибо! Я почти понял. Завтра еще раз всё перечитаю на свежую голову и, наверное, окончательно пойму :)

Norong ★★
() автор топика
Ответ на: комментарий от xaizek

Я перечитал все твои сообщения, но всё-таки осталось несколько вопросов.

В контексте C и D в объектном файле: я правильно понимаю, что переменные, объявленные с extern, как-то иначе там отражаются, ведь, как я понял, и C (неинициализированная) и D (инициализированная) после «объединения» приводят к выделению памяти, а extern, вроде, не приводит?

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

Т.е. он предполагает определение без инициализации?

А что вообще следует из этих предположений компилятора? Как раз-таки C или D в объектных файлах?

И почему всё-таки он вынужден предполагать и не может определить точно? Есть инициализация - D, иначе - C, компилятор же видит, есть ли равно после имени переменной, или нет.

И наконец:

каждое объявление трактуется двояко
всё неоднозначное объявляется в объектном файле как common symbol (C)

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

Norong ★★
() автор топика
Ответ на: комментарий от Norong

В контексте C и D в объектном файле: я правильно понимаю, что переменные, объявленные с extern, как-то иначе там отражаются, ведь, как я понял, и C (неинициализированная) и D (инициализированная) после «объединения» приводят к выделению памяти, а extern, вроде, не приводит?

Да, оно там undefined (U).

Т.е. он предполагает определение без инициализации?

Да.

А что вообще следует из этих предположений компилятора? Как раз-таки C или D в объектных файлах?

C. Если бы он записал D, то линкер выдал бы ошибку из-за переопределения символа.

И почему всё-таки он вынужден предполагать и не может определить точно? Есть инициализация - D, иначе - C, компилятор же видит, есть ли равно после имени переменной, или нет.

Потому что с точки зрения языка, объявление без инициализации может быть как объявлением, так и определением. На основе одного файла компилятор не может быть уверен в том, что это. Если будет везде писать D, линкер выдаст ошибку. Если везде U, выдаст другую (нету определения). Поэтому есть C, которое предоставляет инициализацию нулём, если нигде нет другой инициализации. А несколько C объединяются в одну область памяти, поэтому не возникает конфликтов.

Но какое значение здесь имеет тот факт, что объявление без инициализации может привести к двум разным результатам - нули или мусор?

Глобальные объявления не могут содержать мусор, там будут нули, если ничего другого не задано. Символ типа C является частью сегмента памяти, который всегда забивается нулями. D содержит явную инициализацию. Если нет ни того ни другого — ошибка. Если есть D — используется он. В противном случае используется C, а там нули.

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

Дошло! Ура :) Спасибо!

Я правильно понимаю, что всё вышесказанное (включая вчерашнее) - только для внешних переменных, а для локальных (внутренних для функций) всё обстоит иначе? Могу предположить, что в их случае компилятор сразу знает, есть ли инициализация или нет. В случае, если есть - D, иначе - b, наверное? (из прочтения man nm)

Norong ★★
() автор топика
Ответ на: комментарий от Norong

Я правильно понимаю, что всё вышесказанное (включая вчерашнее) - только для внешних переменных

Да.

а для локальных (внутренних для функций) всё обстоит иначе? Могу предположить, что в их случае компилятор сразу знает, есть ли инициализация или нет. В случае, если есть - D, иначе - b, наверное? (из прочтения man nm)

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

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

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

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

Norong ★★
() автор топика
Ответ на: комментарий от Norong

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

Получается, что стек - это что-то типа временной области?

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

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

Мне тут в учебнике говорят, что

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

Это - устаревшие сведения? У меня, вроде, внешние между несколькими файлами переменные отлично себя без extern чувствовали.

Кроме того, в учебнике говорят:

размеры массивов обязательно указываются в определении, но необязательно - в объявлении со словом extern.

Проверил. Если массив, определённый в одном файле, объявить в другом без extern, то требуется указание размера. С extern - не требуется. У меня складывается ощущение, что лучше использовать extern везде, где имеется в виду объявление внешней переменной (кроме как, может быть, при использовании внешней переменной внутри функции, если вне функции она была объявлена в том же файле). Это будет правильно?

Norong ★★
() автор топика
Ответ на: комментарий от Norong

Это - устаревшие сведения? У меня, вроде, внешние между несколькими файлами переменные отлично себя без extern чувствовали.

Не помню таких требований, скорее просто осторожничают, чтобы случайно не определить что-то не там где нужно (в случае опечатки, например).

У меня складывается ощущение, что лучше использовать extern везде, где имеется в виду объявление внешней переменной (кроме как, может быть, при использовании внешней переменной внутри функции, если вне функции она была объявлена в том же файле). Это будет правильно?

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

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

А при использовании внешних переменных внутри функции необходимо как минимум одно: либо объявлена вне функции выше по тексту, либо объявлена с extern внутри функции, так?

Norong ★★
() автор топика
Ответ на: комментарий от xaizek

Понял, спасибо!

Мне почему-то приятнее писать код в соответствии с нюансами такого рода. И чтобы без warning'ов. :)

Norong ★★
() автор топика
Ответ на: комментарий от xaizek

У меня возник вопрос про статические внутренние (локальные) переменные. Я понял так: Когда я объявляю такую переменную внутри функции, под неё выделяется память, и вместо того, чтобы содержать мусор, эта переменная инициализируется нулями. А если я её хочу проинициализировать чем-то другим, то я её внутри функции определяю вот так:

static int buf = 4;

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

Я правильно всё это понимаю, или есть какие-то хитрости?

Norong ★★
() автор топика
Последнее исправление: Norong (всего исправлений: 3)
Ответ на: комментарий от Norong

На самом деле это как глобальная переменная, только видна исключительно в одной функции. Создаётся и инициализируется во время компиляции.

А локальные это те, которые на стеке. Статические локальными обычно не называют.

Инициализация при первом входе это в C++, но память там тоже выделяется при компиляции. Разница в том, что в C инициализотором может быть только константа, поэтому всё можно сделать во время компиляции.

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

Т.е. отличия от глобальной (внешней) переменной два:

1.) Видна только в одной функции.

2.) Создаётся и инициализируется во время компиляции. Как следствие: инициализация единожды и только константами.

Ага?

Norong ★★
() автор топика
Ответ на: комментарий от Norong

1.) Видна только в одной функции.

Да.

2.) Создаётся и инициализируется во время компиляции. Как следствие: инициализация единожды и только константами.

Это не отличие, глобальные работают аналогично. Просто у них сложности с объявлениями/определениями в разных местах.

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

Это не отличие, глобальные работают аналогично. Просто у них сложности с объявлениями/определениями в разных местах.

А можно поподробнее про это? Глобальные тоже только константами инициализируются? И в чём там сложности? (или это о том, что ты мне объяснял вчера/позавчера?)

Norong ★★
() автор топика
Последнее исправление: Norong (всего исправлений: 1)
Ответ на: комментарий от Norong

А можно поподробнее про это? Глобальные тоже только константами инициализируются?

Да, причём именно константными выражениями. Вот так нельзя:

const int a = 1243;
const int b = a;
Это сделано как раз чтобы компилятору было проще, иначе надо откладывать инициализацию до момента запуска программы.

И в чём там сложности? (или это о том, что ты мне объяснял вчера/позавчера?)

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

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

Мне тут в задании предложили написать рекурсивную версию функции, которая преобразует целое число в строку. Я написал её, вот она: https://gist.github.com/anonymous/3fa0420c719eed30f8bd3c083da63b64

В первой версии у меня получилась функция, настолько тесно завязанная на остальную программу (i и string были внешними переменными, itoa вызывалась с одним аргументом - числом, а string был захардкожен), что самодостаточной её нельзя было назвать. Потом я решил, что i должен быть статической переменной внутри функции itoa, а string - передаваться в itoa аргументом, причём таким, чтобы при вызове itoa рекурсивно из той же itoa этот аргумент оставался тем же (т.е. все вызовы itoa работали с одним и тем же массивом). И я сделал вот такое:

void itoa(int n, static char s[]) {

Компилятор мне этого не простил.

Только чуть позже я вспомнил, что если массив передаётся в функцию, как аргумент, то функция в любом случае работает с тем самым массивом, с которым её вызвали, не создавая его копию; и получилась конечная версия программы (которая по ссылке).

У меня возникло несколько вопросов:

  • Правильно ли я вспомнил эту информацию о массиве в роли аргумента?
  • Если да, то как так получается? У меня же в этом случае массив может не быть внешней переменной. Что там происходит при таком вызове?
  • Если я захочу, чтобы функция, вызванная с неким аргументом, не являющимся массивом, работала непосредственно с ним, не создавая копию, я должен прежде сделать ту переменную, которую передаю в функцию, внешней? Потребуются ли ещё какие-нибудь манипуляции, кроме этой? В функцию я её передаю как обычно, наверное. А в заголовке функции не должно быть чего-то типа extern i?
Norong ★★
() автор топика
Ответ на: комментарий от Norong

Правильно ли я вспомнил эту информацию о массиве в роли аргумента?

Правильно.

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

Независимо от типа переменной (хотя, если только она не временная, которая порождается вычислениями), у неё есть адрес места, где переменная лежит в памяти. Массив «разлагается» до адреса и копируется уже он, а при операциях компилятор работает с тем, что лежит по этому адресу (косвенное обращение).

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

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

А в заголовке функции не должно быть чего-то типа extern i?

Нет, там такого не будет, но будет другая нотация.

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

Спасибо, понял! Предвкушаю пятую главу...

Norong ★★
() автор топика
Ответ на: комментарий от xaizek

И вот я на пятой главе. Там есть пример функции, которая делает следующее: Русский вариант:

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

Английский вариант:

Performs free-format input conversion by breaking a stream of characters into integer values, one integer per call. getint has to return the value it found and also signal end of file when there is no more input. Our version of getint returns EOF for end of file, zero if the next input is not a number, and positive value, if the input contains a valid number.

Кроме того, упражнение к этому коду гласит:

Функция getint в её нынешнем виде воспринимает плюс или минус без последующей цифры как разрешённое представление нуля. Измените функцию так, чтобы она возвращала символ назад в поток.

Код я перепечатал из учебника, проверил пару раз:

wget http://ix.io/17dj -O - | base64 -d | tar -x

Что этот код в моём представлении должен делать (это из моего понимания задания): до получения EOF считывать символы, и каждый раз, когда встречается последовательность, являющаяся числом, записывать её, как число, в следующий элемент массива.

Однако, вот что происходит не так, как мне кажется правильным:

  • Во-первых, работа цикла for, в котором вызывается функция, по сути (чуть ниже подробности) завершается при получении любого символа, не являющегося знаком +/-, концом файла, цифрой.
  • Во-вторых, в том случае, который описан в упражнении (знак без цифр) получается неправильное возвращаемое getint значение и запись нуля в текущий элемент массива.
  • В третьих, переход к следующему элементу массива происходит даже если getint вернула 0, т.е. не было записано число.

Меня смутила строка 14 в getint.c - то есть, если символ - не цифра, не EOF и не знак, просто вернуть его обратно в буфер и вызвать функцию ещё раз? Буфер - внешний, тогда я при следующем getch() опять получу тот же символ и результат будет немного предсказуем. В массив при таком сценарии ничего не запишется, но n увеличивается при каждом вызове функции (если не вернёт EOF), т.е. получается, что вся программа завершится при получении первого символа, не являющегося цифрой, ± или EOF.

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

wget http://ix.io/17em -O - | base64 -d | tar -x

Собственно, единственный вопрос, который меня терзает: что это было? Либо я неправильно понял задание, либо в учебнике просто неправильный код. Если тебе не трудно, не мог бы ты высказать своё мнение по этому поводу?

Norong ★★
() автор топика
Ответ на: комментарий от xaizek

А, и ещё: в их версии и в моей версии в функции main объявляется функция getint. С одной стороны, мне это кажется абсолютно нормальным, с другой стороны, раньше всегда объявление функций происходило вне каких-либо функций. Тут всё в порядке, нет нюансов?

Norong ★★
() автор топика
Ответ на: комментарий от Norong

Сама функция там выглядит выполняющей то, что от ней хотели. Даже для ввода +/- она вернёт есть или нет дальше что-то, и число ноль тоже ввела. Там просто в main() не хватает обработки ошибок, которые эта функция возвращает. При нуле предполагается либо завершение чтения строки, либо вычитывание не-чисел и повторного вызова функции.

Убрав ungetch() (их версия на много символов, но больше одного не используется, поэтому стандартное ungetc() тоже сойдёт), ты также убрал возможность работы с числами в строке, в которой есть не только числа. А в задании же «Измените функцию так, чтобы она возвращала символ назад в поток.».

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

Я убрал эти две функции, заменив их двумя переменными и работой с ними прямо в getint. Буфер по-прежнему есть, все работает хорошо.

Все понял, спасибо!

Norong ★★
() автор топика
Ответ на: комментарий от xaizek

Хотя, нет, не уверен, что всё понял.

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

Я правильно понимаю, что имеется в виду, что main должен, исходя из возвращённого getint значения, при том, что getint вернул в буфер неправильный символ, сам решить, что делать с возвращённым в буфер символом?

Norong ★★
() автор топика
Ответ на: комментарий от Norong

Ну да. Может надо вычитать строку:

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

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

И ещё небольшой вопрос. Вот есть немного теории: https://i.imgur.com/SRw7oqu.png

Я правильно понимаю, что если я в функцию передаю имя массива, то я могу в функции написать, что она принимает как аргумент массив (char s[]), и тогда внутри функции я работаю с тем же самым массивом, а могу написать, что она принимает как аргумент указатель (char *s), и тогда работаю в терминах указателей?

Norong ★★
() автор топика
Ответ на: комментарий от Norong

Записи char s[] и char *s в списке параметров ничем друг от друга не отличаются кроме синтаксиса. В обоих случаях это указатель.

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

И снова я.

У меня тут есть небольшой код:

#include <stdio.h>

void strcat(char *s, char *t);

int main(void) {
    char string[20] = "pelmeni";
    char append[] = "append";

    printf("%s\n%s\n", string, append);
    strcat(string, append);
    printf("%s\n", string);

    return 0;
}

void strcat(char *s, char *t) {
    while (*s)
        ++s;
    
    while (*s++ = *t++)
        ;
}
При компиляции я столкнулся с
5.3.c:3:6: warning: conflicting types for built-in function ‘strcat’
 void strcat(char *s, char *t);
      ^
Видимо, это из-за того, что функция strcat существует в стандартных библиотеках (грепнул в /usr/include, нашёл эту функцию в string.h). Как я понимаю, ошибки не возникает, так как библиотечная функция у меня не объявлена, т.е. мне просто говорят, что такое имя лучше не использовать. Я попробовал добавить #include <string.h> в начало кода и действительно получил ошибку. После этого я попробовал добавить #undef strcat сразу после #include <string.h>, рассчитывая на то, что библиотечная функция разобъявится обратно и моя сможет полноценно работать, но ошибка никуда не делась. Пожалуйста, подскажи - я неправильно понял суть #undef, или что-то другое не так?

Norong ★★
() автор топика
Последнее исправление: Norong (всего исправлений: 1)
Ответ на: комментарий от Norong

#undef влияет только на макросы препроцессора, т.е. на то, что объявлено с помощью #define. Объявление сущностей вроде функций и любых типов им нельзя убирать. #undef уже не существует к тому моменту, когда компилятор обрабатывает типы, препроцессор отрабатывает один раз при старте, а дальше всё происходит без него.

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

Здравствуй! У меня возник теоретический вопрос по многомерным массивам. Говорилось, что при передаче многомерного массива в функцию в качестве аргумента, в объявлении параметров функции нет необходимости указывать длину первого его измерения, потому что имя такого массива является указателем на массив, каждый элемент которого является другим массивом, но при этом обязательно надо указывать длины всех остальных измерений. Мне такое объяснение оказалось совсем непонятным. Не мог бы ты объяснить мне эти моменты?

  • Почему нет необходимости указывать длину первого измерения, но необходимо указывать длины остальных?
  • Это относится только к объявлению параметров при определении функции, или к чему-то ещё (например, к прототипам)?
  • Как многомерный массив представляется в памяти?

Заранее большое спасибо.

Norong ★★
() автор топика
Ответ на: комментарий от Norong

Почему нет необходимости указывать длину первого измерения, но необходимо указывать длины остальных?

Для индексации массива необходимо знать размер элемента массива. В одномерном массиве это просто размер типа (int, например), а в многомерном это другие массивы. Отсюда и требование. Компилятору нужен sizeof(*array), а это какой-нибудь int [4].

Это относится только к объявлению параметров при определении функции, или к чему-то ещё (например, к прототипам)?

К прототипам и везде в других местах, где хочется иметь указатель на многомерный массив.

Как многомерный массив представляется в памяти?

Последовательно элементы каждого массива на каждом уровне. Т.е. для int arr[3][4]:

Начало здесь (адреса растут вправо и потом вниз)
arr[0] = arr + 0*sizeof(*arr): arr[0][0] arr[0][1] arr[0][2] arr[0][3]
arr[1] = arr + 1*sizeof(*arr): arr[1][0] arr[1][1] arr[1][2] arr[1][3]
arr[2] = arr + 2*sizeof(*arr): arr[2][0] arr[2][1] arr[2][2] arr[2][3]
arr[3] = arr + 3*sizeof(*arr): arr[3][0] arr[3][1] arr[3][2] arr[3][3]

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

Для индексации массива необходимо знать размер элемента массива.

Я правильно понимаю, что общий размер этого массива (как длина области памяти, которую он занимает со всеми своими измерениями) будет и так известен при передаче? По аналогии с тем, что если я объявил переменную short, то известно, что она 16 бит занимает. Объявил массив short array[a][j][k] - занимает 16*a*j*k бит.

Norong ★★
() автор топика
Последнее исправление: Norong (всего исправлений: 3)
Ответ на: комментарий от Norong

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

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

Вот как я теперь понял ситуацию: есть int arr[a][j][k]. В определении (или прототипе) функции в объявлении параметров указано: int arr[][j][k] и передаётся в функцию указатель на этот массив. Известен размер int, известно k => известен размер arr[M][N]. Известен размер arr[M][N], известно j => известен размер arr[M], но мне неизвестно, чему может быть равен M <=> неизвестен размер массива. Т.е. я могу любое отведённое мне пространство проиндексировать, но разве мне не нужно знать, где оно кончается?

Norong ★★
() автор топика
Последнее исправление: Norong (всего исправлений: 1)
Ответ на: комментарий от Norong

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

В C99 на самом деле были добавления по этому поводу, но в C11 их сделали опциональными. Оно довольно запутанно и не очень удобно, решили, что программисты разберутся сами.

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

И ещё вопрос. В учебнике есть такое место: https://i.imgur.com/u4kn5Jh.png

Первое, что меня смущает - массив объявлен статическим внутри функции. Его имя же вроде в любом случае не должно быть известно снаружи, если он внутри объявлен? Раньше по тексту говорилось, что если внутреннюю переменную объявить статической, то она просто будет сохраняться между вызовами функции (но видна только из неё). Тут, видимо, другой случай? Кроме того, для меня неочевидно, что происходит со строковыми константами (вроде тех, что там). Как я понимаю, где-то выделяется под них память, а наш массив name содержит указатели на начала этих участков памяти. Эта память, получается, выделяется «глобально» и нужные данные там существуют всё время работы программы, а не только пока выполняется функция? Так получается всегда, или есть варианты в зависимости от того, внутри функции происходит объявление (определение?) этих данных или снаружи, или может static играет какую-то роль?

Norong ★★
() автор топика
Ответ на: комментарий от Norong

Тут, видимо, другой случай?

Да нет, тот же. Просто незачем создавать массив на стеке при каждом вызове, поэтому делают во время компиляции.

Эта память, получается, выделяется «глобально» и нужные данные там существуют всё время работы программы, а не только пока выполняется функция?

Да. Это так же как с глобальными/статическими данными. Место резервируется и инициализируется во время компиляции.

Так получается всегда, или есть варианты в зависимости от того, внутри функции происходит объявление (определение?) этих данных или снаружи, или может static играет какую-то роль?

Всегда. Любой строковой литерал лежит где-то в глобальной памяти.

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

И ещё один вопрос :)

В учебнике есть вот такой эпизод: https://i.imgur.com/7fQUXur.png

Я правильно понимаю, что int *b[10] это массив из 10 указателей на int? Тогда b[3] - это четвёртый в массиве указатель на int, т.е. указатель на нулевой элемент какого-то массива int'ов, а b[3][4] - это пятый элемент этого массива. Но сказано, что те 10 указателей b[N] не проинициализированы. Без этого, наверное, в переменных b[N] хранится мусор? Куда они указывают? Могут, наверное, указывать и друг на друга, и на одни и те же области, и вообще неведомо куда. То же самое и с теми массивами, которые начинаются по указателям b[N]. Длина не обозначена, объявлений не было; есть ли у нас гарантии, что для них найдётся место, что они не пересекутся...?

Norong ★★
() автор топика
Ответ на: комментарий от Norong

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

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

Просто не надо использовать те указатели.

В смысле? Зачем-то же про них рассказывают...

там мусор со стека
Если этот указатель покажет куда-то

Он же, наверное, может показать куда угодно? И в уже выделенную область памяти, например. Или я не так понимаю?

Norong ★★
() автор топика
Ответ на: комментарий от Norong

Он же, наверное, может показать куда угодно? И в уже выделенную область памяти, например. Или я не так понимаю?

Может. Его надо направить в нужное место, перед использованием.

В смысле? Зачем-то же про них рассказывают...

Не надо использовать неинициализированные указатели, т.е. те значения с мусором.

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

А, догнал! Т.е. я объявляю массив указателей как в тексте, и каждый из них пригоден для следующего использования: указывать на массив int'ов. Дальше, если у меня есть на примете готовый массив int'ов или область памяти, которую я хочу использовать, как массив int'ов, то я указываю указателем туда и использую. Ага?

Norong ★★
() автор топика
Ответ на: комментарий от Norong

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

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

Т.е. указатель позволяет выйти за пределы того, на что указывает, но если я так сделаю, то попаду неизвестно куда, да?

Norong ★★
() автор топика
Последнее исправление: Norong (всего исправлений: 2)
Ответ на: комментарий от xaizek

Мне тут в задании предлагают такой вот массив:

static char daytab[2][13] = {
    {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31},
    {0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}
};
изобразить с помощью указателей. Тут у меня массив daytab из двух элементов, каждый из которых является массивом из 13 элементов типа char.

Я попробовал сделать так:

static char *daytab[2] = {
    {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31},
    {0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}
};
Я вижу себе это так: массив из двух указателей на char, и я его инициализирую двумя объектами. Каждый - это массив char'ов (под эти массивы глобально выделяется память), он же - указатель на нулевой элемент этого массива, и именно эти указатели записываются в daytab.

Однако при использовании этого у меня segfault. Не подскажешь, что я делаю не так?

Norong ★★
() автор топика
Ответ на: комментарий от Norong

Списки инициализации не превращаются в указатели. Эти массивы надо объявлять отдельно. Там, видимо, 0 и 31 были интерпретированы как два указателя.

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

Выше по тексту был такой пример:

static char *name[] = {
    "Illegal month",
    "January", "February"... и так далее.
};
Чем он принципиально отличается от того случая, о котором мы говорим? Вроде, например, «January» аналогично { 'J', 'a', 'n'... }

Norong ★★
() автор топика
Ответ на: комментарий от Norong

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

char str[] = "bla";
const char *str = "bla";
Но это не работает со списками инициализации в общем случае.

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

Понял. Т.е. я отдельно объявляю свои массивы чисел, а потом в массив указателей эти массивы кладу?

И ещё вопрос: вчера ты мне объяснил, что под строковые литералы в глобальной памяти место отводится при компиляции. Так только со строковыми литералами, или любой массив также себя поведёт?

Norong ★★
() автор топика
Ответ на: комментарий от Norong

Т.е. я отдельно объявляю свои массивы чисел, а потом в массив указателей эти массивы кладу?

Да.

Так только со строковыми литералами, или любой массив также себя поведёт?

Со строковыми, которые разлагаются до указателя (иначе в этом нет необходимости). Статические/глобальные массивы тоже конечно статически выделяются. Но локальные инициализируются во время выполнения.

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

которые разлагаются до указателя

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

Norong ★★
() автор топика
Ответ на: комментарий от xaizek

Т.е. если я его использую, как указатель, то тогда выделяем глобальную память, иначе - по ситуации (в зависимости от того, внешний/статический или нет)?

И ещё один вопрос (как-то у меня указатели нелегко идут). Я дошёл до параграфа про аргументы командной строки. Сказано, что argv - указатель на массив строк-аргументов. В качестве примера в параграфе приведена программа echo, которая воспринимает argv как массив символьных указателей. В ней argv объявлен так:

char *argv[]

и обращение к аргументам происходит по argv[a], т.е. всё действительно так, как если argv - массив символьных указателей (массив строк). Но раньше было сказано, что он - указатель на такой массив. Я понимаю, если бы мне сказали, что он - указатель на нулевой элемент массива. Тогда указатель на нулевой элемент массива == это и есть само имя массива, но тут ситуация другая, видимо. Указатель на массив - это указатель на указатель на нулевой элемент? Не мог бы ты помочь мне понять, что здесь происходит?

Norong ★★
() автор топика
Последнее исправление: Norong (всего исправлений: 2)
Ответ на: комментарий от Norong

Т.е. если я его использую, как указатель, то тогда выделяем глобальную память, иначе - по ситуации (в зависимости от того, внешний/статический или нет)?

Память где-то выделится в любом случае. Просто это будет либо часть объекта, который инициализируется, либо глобально, если объект берёт только указатель.

Я понимаю, если бы мне сказали, что он - указатель на нулевой элемент массива.

Так и есть. Это же эквивалент записи char **argv. Указатель на нулевой элемент массива и считается указателем на весь массив, так как в обоих случаях это одно и то же место в памяти.

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

Просто это будет либо часть объекта, который инициализируется, либо глобально, если объект берёт только указатель.

Понял, спасибо!

Указатель на нулевой элемент массива и считается указателем на весь массив

Вот он - ключевой момент, которого мне не хватало :) Спасибо!

Norong ★★
() автор топика
Ответ на: комментарий от xaizek

Просто это будет либо часть объекта, который инициализируется, либо глобально, если объект берёт только указатель.

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

char string[] = «string»

а второй - это

char *pointer = «string»

? Если да, то с точки зрения использования, вроде, разницы особо нет - по string или pointer я получаю указатель на нулевой элемент, по string[N] или pointer[N] - обращение к N-ому элементу. А с точки размещения в памяти во втором случае, как я понял, указатель в одном месте, а сам массив - в другом; а в первом случае они как-то «вместе»? И, прости что к этому возвращаюсь:

Со строковыми, которые разлагаются до указателя (иначе в этом нет необходимости)

- нет необходимости в том, чтобы хранить отдельно указатель и сам массив?

Norong ★★
() автор топика
Ответ на: комментарий от Norong

Правильно ли я понимаю, что первый случай - это
char string[] = «string»
а второй - это
char *pointer = «string»
?

Да.

Если да, то с точки зрения использования, вроде, разницы особо нет

В массиве будет своя копия литерала. А если сделать так:

char *a = "abc";
char *b = "abc";
То a и b могут указывать на одну и ту же строку.

А с точки размещения в памяти во втором случае, как я понял, указатель в одном месте, а сам массив - в другом; а в первом случае они как-то «вместе»?

В первом случае есть только массив и можно использовать указатель на него, если надо.

нет необходимости в том, чтобы хранить отдельно указатель и сам массив?

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

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

У меня тут возникла проблема с кодом. Вот он: https://gist.github.com/anonymous/0bc9e7a7f3850dcf68a0c6d1e07679c9

Вот что он должен делать: если программа запущена без аргументов, то во входном потоке заменять каждый таб необходимым числом пробелов до следующей границы табуляции. Если есть аргументы, и они - числовые, то заменять лишь те табы, которые на позициях, указанных аргументами. Почему-то при вводе таких данных, при которых таб стоит не на позиции, указанной в аргументе (если аргументы есть), программа sefault'ится:

(gdb) run
Starting program: /home/user/C/a.out 2 5 8
aТАБ
a\\\
aesdТАБ
aesd
aesdasdТАБ
aesdasd\
aeТАБ

Program received signal SIGSEGV, Segmentation fault.
0x00000000004006d0 in atoi (s=0x0) at 5.11.1.c:52
52	    sign = (*s == '-') ? -1 : 1;
При вызове без аргументов работает без проблем. Меня очень смущает место сегфолта. Эту функцию (atoi) я не раз копировал из старого кода в новый, она, вроде, работает корректно. Аргумент для неё (ожидается строка) - это аргумент самой программы (строка). Кроме того, конкретно та строка, на которой сегфолт, выглядит вполне невинной.

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

Norong ★★
() автор топика
Ответ на: комментарий от xaizek

Ага, догнал! Там i++ всё портит, надо сделать проверку

i < arrlen

и инкрементировать i внутри while. Ну и можно i = 1 в начале сделать, проверка нулевого аргумента мне не нужна.

Спасибо!

Norong ★★
() автор топика
Ответ на: комментарий от xaizek

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

Добавьте в программу ключ -d (от directory order - упорядочение каталожного типа), при наличии которого сравнение должно выполняться только по буквам, цифрам и пробелам.

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

Пожалуйста, если тебя не затруднит, подскажи, как мне следует понять задание. Заранее спасибо.

Norong ★★
() автор топика
Ответ на: комментарий от Norong

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

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

Вот что у меня получилось: https://gist.github.com/anonymous/955adbdfa006e1f6d10bc963be1137e6

Функция начинается с 70 строки, обработка аргументов командной строки - с 25.

dirtype = 1 если нужно выполнить то, что в задании.

ignorecase = 1, если нужно игнорировать регистр.

При запуске проги с -d, вводе данных и завершении ввода функция сегфолтится на

Program received signal SIGSEGV, Segmentation fault.
0x00000000004009da in mystrcmp (s=0x60154a <strings+10> "dir/ctory", t=0x601540 <strings> "directory")
    at 5.14-17.c:84
84	                while (!LETNUMSPACE(s[i]))
Относительно предыдущего варианта (до этого задания) в функцию добавилась директива

#define LETNUMSPACE(a) (((a >= 'a' && a <= 'Z') || (a >= '0' && a <= '9') || a == ' ') ? 1 : 0)

для определения, принадлежит ли символ множеству, проверка

|| dirtype

в for,

if (!equal && dirtype) {

со всем содержимым и переменная j (раньше везде было только i, т.к. по строкам шёл одновременно).

Я рассчитывал на то, что если вдруг символы разошлись и dirtype задан, то если какой-то из символов - не из множества, то идём вперёд по той строке, где этот символ, пока не найдём правильный символ. Потом --j и --i - чтобы выполнить ещё одну проверку после ++i, ++j цикла for. Если вдруг в обеих строках символы не из множества попались, и они разные - то переход вперёд должен за два выполнения произойти.

Кажется, я что-то делаю сильно не так. Если вдруг у тебя в какой-то момент появится желание почитать жутковатый код... :)

Norong ★★
() автор топика
Ответ на: комментарий от Norong

надо идти вперёд пока не будет найден признак конца строки или один из символов того множества

признак конца строки

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

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

Ага, уловил. \n там принципиально быть не может (обрубается функцией readlines). Добавил проверку на != '\0' и добавил '\0' в допустимые символы:

    #define LETNUMSPACE(a) (((a >= 'a' && a <= 'Z') || (a >= '0' && a <= '9') || a == ' ' || a == '\0') ? 1 : 0)
    #define CSDIFF ('A' - 'a')

    extern int ignorecase;
    extern int dirtype;
    int equal, i, j;

    for (i = 0, j = 0; (equal = (((CAPITAL(s[i]) && ignorecase) ? (s[i] - CSDIFF) : s[i]) == ((CAPITAL(t[j]) && ignorecase) ? (t[j] - CSDIFF) : t[j]))) || dirtype; i++, j++) {
        if (!equal && dirtype) {
            if (!LETNUMSPACE(s[i])) {
                while (!LETNUMSPACE(s[i]) && s[i] != '\0')
                    ++i;
                --i;
                --j;
            }
            else if (!LETNUMSPACE(t[j])) {
                while (!LETNUMSPACE(t[j]) && t[j] != '\0')
                    ++j;
                --i;
                --j;
            }
        }
Сегфолта больше нет, логики в сортировке вывода не вижу. Входные данные:
directory
dirfctory
dirgctory
dir/ctory
dir/story
direstory
dirfstory
dir/file
dir/gile
Выхлоп:
dir/story
directory
direstory
dirgctory
dirfstory
dirfctory
dir/file
dir/ctory
dir/gile

Norong ★★
() автор топика
Последнее исправление: Norong (всего исправлений: 2)
Ответ на: комментарий от Norong

Мне тоже не понятно, как оно должно сортировать. Может есть какие-то другие трактовки. Фиг с ним, если не смогли написать нормально задание.

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

Судя по всему, ты правильно истрактовал условие: нужно только по буквам-цифрам и пробелам сравнивать строки. Однако, выхлоп в моём предыдущем комменте как-то всё равно не соответствует. Тебе случайно никакой косяк не бросился в глаза по ходу кода? Если нет, то просто перейду к следующему заданию, фиг с ним, действительно :)

Norong ★★
() автор топика
Ответ на: комментарий от Norong

Norong это мой студент пусть мучается леньтяй. Заходи осенью ко мне с конъячком и шоколадом на пересдачу.

anonymous
()
Ответ на: комментарий от xaizek

Здравствуй! У меня тут снова возникли затруднения с кодом.

Суть его в том, чтобы сгруппировать поступающие слова в группы так, чтобы у слов в каждой группе первые 6 символов были одинаковыми. Затем надо вывести эти слова по группам. Первоначально код выглядел вот так: https://gist.github.com/anonymous/0f6c1848e41584a7308680abaae900a1

Проблема возникала в районе строки 59, и заключалась она в том, что под *pointer - указатель на очередную область в памяти для строки - не было выделено место. Если бы я заранее выделил массив для указателей на char, я был бы ограничен в числе слов. Если бы я выделял место под эти указатели с помощью malloc, потребовалось бы где-то хранить указатели на свежевыделенное место (участки памяти были бы разбросаны случайным образом), и опять ограничение. Если я правильно понял, без выделения места под *pointer (а он там каждый раз новый, по одному на каждое слово) - нельзя, потому что могу попасть в уже выделенную область памяти. В этом варианте код работает логически корректно, но время от времени вместо одного из слов я в выводе получаю «мусор» - думаю, как раз из-за невыделения памяти под *pointer.

Я переписал код, создав фиксированной длины массив под указатели на char, но массив объявил как массив указателей на void. Когда я дохожу до конца массива (заполняя его указателями на char), я malloc-ом выделяю место под ещё один такой массив, указатель размещаю в последний элемент предыдущего массива (именно поэтому у меня указатели на void, чтобы я, как я понял, мог записывать туда указатель на что угодно). Таким образом, у меня происходит динамическое выделение массивов под указатели на char. Вот код: https://gist.github.com/anonymous/9b792c447ec7a6ed021b6c3a169474e2

То, о чём я говорю, происходит в строках [59; 75].

Он не компилируется:

6.2.c: In function ‘addtree’:
6.2.c:62:28: warning: dereferencing ‘void *’ pointer
             t = (void **) **(t + ARRSIZE - 1);
                            ^
6.2.c:62:13: error: void value not ignored as it ought to be
             t = (void **) **(t + ARRSIZE - 1);
             ^
6.2.c:65:14: warning: dereferencing ‘void *’ pointer
             t[p->count] = malloc(sizeof(word));
              ^
6.2.c:65:13: error: invalid use of void expression
             t[p->count] = malloc(sizeof(word));
             ^
6.2.c:66:21: warning: dereferencing ‘void *’ pointer
             strcpy(t[p->count], word);
                     ^
6.2.c:66:13: error: invalid use of void expression
             strcpy(t[p->count], word);
             ^
6.2.c:69:14: warning: dereferencing ‘void *’ pointer
             t[p->count] = malloc(sizeof(p->names));
              ^
6.2.c:69:13: error: invalid use of void expression
             t[p->count] = malloc(sizeof(p->names));
             ^
6.2.c:71:20: warning: dereferencing ‘void *’ pointer
             t = *(t[p->count]);
                    ^
6.2.c:71:13: error: void value not ignored as it ought to be
             t = *(t[p->count]);
             ^
6.2.c:72:13: warning: dereferencing ‘void *’ pointer
             *t = malloc(sizeof(word));
             ^
6.2.c:72:13: error: invalid use of void expression
6.2.c:73:20: warning: dereferencing ‘void *’ pointer
             strcpy(*t, word);
                    ^
6.2.c:73:13: error: invalid use of void expression
             strcpy(*t, word);
             ^
По-видимому, я что-то фундаментально недопонял про указатели на void. Сам я надеялся, что в t, являющийся указателем на void, я смогу записывать любой указатель и работать с ним, как с любым указателем. Пожалуйста, если тебе не трудно, помоги мне разобраться в этой ситуации. Заранее большое спасибо.

Norong ★★
() автор топика
Ответ на: комментарий от Norong

В код полностью не вдавался, но в принципе понятно, что не так.

 *p->names = (char *) malloc(sizeof(word));
sizeof() не считает размер строки, а вернёт размер указателя, в таких случах надо делать
 *p->names = (char *) malloc(strlen(word) + 1);
А можно воспользоваться strdup(), которая делает malloc()+strcpy().

Если нужен динамический массив, то надо делать realloc(), который перевыделяет память с изменением размера (данные копируются). Тогда место под новые значения будут.

А с void* проблема в том, что записать в него можно что угодно, но прежде чем вытащить это обратно надо преобразовать к правильному типу. Т.е. преобразование к void* стирает информацию о типе и её надо вернуть, чтобы компилятор смог что-то с этим указателем делать.

Вторая реализация это уже что-то ближе к спискам, можно и так сделать, только проще завести отдельную структуру с полями char *data (можно массивом указателей сделать, если хочется) и struct list_node *next_node и обойтись без указателей на void.

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

Теорию, вроде, понял. В первом варианте кода поменял sizeof(word) на strlen(word) + 1

В выводе всё равно иногда появляется мусор. Мне кажется, это из-за того, что в 58 строке делаю pointer указателем на то, что после p->names, не выделив прежде это место.

А вот про void*: я попробовал, например, 62-ую строку второго кода сделать такой:

t = (void **) **((void **) t + ARRSIZE - 1);

В моём представлении, это должно было помочь, но почему-то ошибка никуда не делась:

6.2.c:62:13: error: invalid use of void expression
             t = (void **) **((void **) t + ARRSIZE - 1);
             ^

Norong ★★
() автор топика
Ответ на: комментарий от Norong

В выводе всё равно иногда появляется мусор. Мне кажется, это из-за того, что в 58 строке делаю pointer указателем на то, что после p->names, не выделив прежде это место.

realloc() ещё нужен.

В моём представлении, это должно было помочь, но почему-то ошибка никуда не делась:

Так нельзя с значением void ничего делать, только с указателями на него. Без одного разименования должно работать:

t = (void **) *((void **) t + ARRSIZE - 1);

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

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

Что касается t, то я разобрался в сути; мне действительно надо было остановиться на одно разыменование раньше. Я понял так, что *((void **) t + ARRSIZE - 1) — это массив (=указатель на массив=указатель на его нулевой элемент), а с ещё одним разыменованием - уже сам нулевой элемент этого массива, т.е. не то, что мне нужно. Однако мне не до конца ясно, почему такое не прокатило. Если я правильно понял происходящее, и два разыменования дают мне нулевой элемент массива (это void *), то он должен бы в t хорошо поместиться. Или тут мешает (void **)?

Итак, t у меня теперь - массив. И дальше облом:

6.2.c:65:14: warning: dereferencing ‘void *’ pointer
             t[p->count] = malloc(sizeof(word));
              ^
6.2.c:65:13: error: invalid use of void expression
             t[p->count] = malloc(sizeof(word));
             ^
Если в t у меня указатель на нулевой элемент массива, то t[p->count] должен бы быть элементом с номером p->count, т.е. void *. Но не получается :(

Norong ★★
() автор топика
Ответ на: комментарий от Norong

Там массив указателей на void. Т.е. тип элемента это void*. А для работы с самим массивом надо ещё одну * добавить и получается void**. Сейчас t не работает потому что он должен быть типа void**, а не void*, массивов void'ов не может существовать (это, кстати, нельзя сделать даже в параметрах функции, т.е. void func(void arr[]) не будет работать).

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

t = (void **) p->names;

p->names — имя массива с элементами void*, т.е. t становится void**

t = (void **) *((void **) t + ARRSIZE - 1);

t + ARRSIZE - 1 — по-прежнему void**, указатель на последний элемент массива, void*. После одного разыменования получается void* - сам элемент массива. Но хотя массив объявлен, как состоящий из void*, этот конкретный элемент содержит указатель на ещё один такой же массив из void*, т.е. этот элемент - void**. С помощью (void **) я говорю, что это выражение - суть void**, и присваиваю в t. t должен void** получится же, или я чего-то не понимаю?

Norong ★★
() автор топика
Ответ на: комментарий от xaizek

Я подумал, что в переменную типа void* можно запихнуть указатель на что угодно - в том числе, и на такой же void* (и получить void**). Это не так?

Norong ★★
() автор топика
Ответ на: комментарий от Norong

Так, но только для хранения. Для работы с данными нужны другие типы. В данном случае void** в качестве типа переменной. Как иначе компилятору знать что в

void *p = ...;
p[30]
p[30] должно вернуть void*, а не, скажем, double.

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

Большое спасибо, понял! Сейчас попробую)

Norong ★★
() автор топика
Ответ на: комментарий от xaizek

Сделал void **t.

Теперь у меня осталась единственная ошибка, вот в этом месте:

t[p->count] = malloc(sizeof(p->names));
t = *(t[p->count]);
t[p->count] - это последний элемент массива из void*, и я выделяю место под ещё один такой массив, записываю указатель в этот элемент. Теперь t[p->count] - указатель на void**. Вроде, так можно.

Разименовываю его до просто void** (массива) и присваиваю t.

6.2.c:71:17: warning: dereferencing ‘void *’ pointer
             t = *(t[p->count]);
                 ^
6.2.c:71:15: error: void value not ignored as it ought to be
             t = *(t[p->count]);
               ^

И ещё:

Для работы с данными нужны другие типы.

А в таких случаях разве не должно помогать принудительное приведение типов?

Norong ★★
() автор топика
Ответ на: комментарий от Norong

Разименовываю его до просто void** (массива) и присваиваю t.

А в таких случаях разве не должно помогать принудительное приведение типов?

Должно, но его ведь нет.

Тип t[p->count] это void*, тип *t[p->count] это void (и так делать нельзя), а нужно (void **)t[p->count].

void* это только для передачи/хранения. Для работы нужен хотя бы void**. Компилятор эти преобразования автоматически делает только в сторону void* при этом реальный тип он больше не знает, обратно надо конвертировать самому.

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

Т.е. ситуация такова: в *void можно положить любой указатель и считать оттуда тоже можно любой указатель, но если я попробую разыменовать переменную, имеющую тип *void, то даже если фактически я туда записал **void, компилятор воспримет её так, как она была объявлена, и получится обращение к void, чего делать нельзя. Таким образом, везде, где фактический тип отличается от того, с которым переменная объявлена, следует выполнять принудительное приведение типов (и тогда я могу переменную объявлять хоть как void*, хоть как void**). Я правильно уловил суть?

Norong ★★
() автор топика
Последнее исправление: Norong (всего исправлений: 1)
Ответ на: комментарий от xaizek

У меня тут в одном месте segfault получается.

            t[p->count] = malloc(sizeof(p->names));
            ++(p->arrays);
            t = *((void ***) t[p->count]);
            *t = malloc(strlen(word) + 1);
            strcpy(*t, word);
Это выделение нового массива и помещение в его первый элемент указатель на строку.

  • t - массив, t[p->count] - его элемент типа void *. Туда помещается указатель на void**.
  • Простое приращение переменной
  • t[p->count] теперь - указатель на void**, т.е, void***. Я указываю это явно и разименую. Получаю тот самый массив, который выделен в первой строке. Присваиваю это значение t. t теперь - void**, указатель на нулевой элемент массива из void*
  • Тут segfault. *t - нулевой элемент массива из *void, я выделяю место под слово, получаю указатель на это место (типа char*) и записываю в *t.
  • Копирую туда слово.

Не подскажешь, что не так? Совсем не догоняю :(

Norong ★★
() автор топика
Ответ на: комментарий от Norong

Это

t = *((void ***) t[p->count]);
должно быть либо
t = *((void ***) &t[p->count]);
либо (хотя касты эти можно и не писать, главное чтобы тип t был правильный).
t = (void **) t[p->count];

В t[p->count] лежит указатель и это выражение вернёт его значение, если это значение трактовать как void*** и разименовать, то это уже будет трактовка первого элемента того участка памяти как указателя, а он указывает непонятно куда.

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

Я увидел ситуацию так: t[p->count] - это void***, указатель на void**, указатель на указатель на нулевой элемент массива из void*.

В этом коде

t = *((void ***) t[p->count]);

происходит следующее: я обозначаю, что t[p->count] - это void*** (потому что иначе компилятор бы подумал, что это лишь void*), при разыменовании компилятор начинает с самого «глубокого» уровня, и как указатель трактуется тот самый нулевой элемент массива из void*. В итоге, то, на что он указывает, записывается в t, а указывает он непонятно куда.

В этом коде:

t = (void **) t[p->count];

по-видимому, приведение к типу void** оставляет два «последних» уровня указателей, отбрасывая один «верхний»: от t[p->count] остаётся void**: указатель на нулевой элемент массива из void*. Его я присваиваю t, всё хорошо.

В этом коде:

t = *((void ***) &t[p->count]);

операция & даёт мне адрес t[p->count] (это void****), но я плохо понимаю, что получается из приведения получившегося к типу void***. Если верно моё предположение о том, что приведение к void с меньшим числом звёздочек оставляет последние уровни указателей, то от приведения к void*** должно получится то же t[p->count] что и было.

Похоже, моё понимание происходящего несовершенно. Не мог бы ты его подкорректировать, если тебе не сложно?

Norong ★★
() автор топика
Последнее исправление: Norong (всего исправлений: 2)
Ответ на: комментарий от Norong

t[p->count] - это void***, указатель на void**, указатель на указатель на нулевой элемент массива из void*.

Но указатель на массив это указатель на его первый элемент, поэтому там просто указатель на нулевой элемент массива, т.е. void**. Там именно этот адрес и хранится, а не адрес того, где этот адрес хранится. Самого верхнего указателя нет.

то от приведения к void*** должно получится то же t[p->count] что и было.

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

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

Снова здравствуй!

Я тут столкнулся с сегфолтом, с которым у меня не вышло разобраться. Не поможешь с ним справиться?

Вот код: https://gist.github.com/anonymous/e368e439c0dec05d53a02f3b01a79dfb

Сегфолт возникает на строке 82 (сейчас она закомментирована, потому что без неё код работает). Тут связанные списки, и чтобы было удобнее написать функцию undef (со строки 91, служит для того, чтобы отменять определение), я решил в каждом блоке списка (содержащем изначально имя, определение и указатель на следующий блок) сохранять также указатель на предыдущий блок. Участок кода (строки [80-83])

        np->next = hashtab[hashval];
        np->prev = NULL;
        /* (np->next)->prev = np; */
        hashtab[hashval] = np;

имеет следующий смысл: в hashtab[hashval] указатель на первый блок списка. Я записываю указатель на этот блок в поле next текущего блока. Поле prev текущего блока делаю равным null. В поле prev следующего блока (на него указывает np->next) записываю np - указатель на текущий блок. В hashtab[hashval] записываю указатель на текущий блок. Таким образом, должно произойти встраивание нового блока в начало списка.

Буду очень признателен, если поможешь мне понять, что тут не так :)

Norong ★★
() автор топика
Ответ на: комментарий от Norong

В поле prev следующего блока (на него указывает np->next) записываю np - указатель на текущий блок.

Так таблица же изначально пустая. Отсюда и проблема.

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

А, т.е. фишка банально в том, что следующего блока, куда я планирую писать, нет?) Тогда, наверное, достаточно будет сделать проверку, а есть ли блок. Кстати, а указатели инициализируются NULL'ом же всегда, да?

Norong ★★
() автор топика
Ответ на: комментарий от Norong

А, т.е. фишка банально в том, что следующего блока, куда я планирую писать, нет?)

Откуда же ему взяться.

Кстати, а указатели инициализируются NULL'ом же всегда, да?

Если объявлены глобально или статически, то да. Иначе только если инициализировать явно.

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

Откуда же ему взяться.

Ну, да, что-то я забыл этот случай рассмотреть.

Если объявлены глобально или статически, то да. Иначе только если инициализировать явно.

Понял, спасибо большое!

Norong ★★
() автор топика
Ответ на: комментарий от xaizek

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

Как я понимаю, машинное слово - это область в памяти, равная нескольким байтам (количество байт зависит от архитектуры процессора или от чего-то другого?). Я прочитал, что в памяти переменные выравниваются по границе, кратной своему размеру. Какая здесь связь с размером машинного слова и самим машинным словом?

Параграф начинается словами:

Если место в памяти - на вес золота <...> приходится упаковывать несколько объектов в одно машинное слово.

Как один из таких случаев приводится ситуация с однобитовыми флажками, и первый способ для работы с ними - это создать переменную типа char или int, и работать с битами с помощью битовых операций. Получается, что char (1 байт, как я понимаю) можно использовать как 8 однобитовых переменных. Однако, мне не ясно, как это будет выглядеть в контексте машинных слов. Вот у меня есть машинное слово размером, допустим, в 4 байта. Первый байт - char, и я работаю с составляющими его битами. Как это поможет мне уместить несколько объектов в одно машинное слово? Если char занимает 1 байт, то он может принимать 2^8 разных значений; вроде, описанная работа с битами - это только способ трактовки значения в нём.

Кроме того, я прочитал, что

байт является наименьшей единицей, в которой отсчитываются адреса

Как это соотносится с тем, что мы спокойно выполняем операции с отдельными битами?

Как альтернативный вариант предлагается использование битовых полей. Пример:

struct {
    unsigned int is_keyword : 1;
    unsigned int is_extern : 1;
    unsigned int is_static : 1;
} flags;
Сказано, что здесь определяется переменная flags, содержащая три однобитовых поля. По синтаксису, flags - это структура из трёх сущностей, каждая из которых, как я понимаю, обозначает одно битовое поле и занимает один бит. Как это представляется с точки зрения машинных слов и выравнивания переменных? И, если байт - минимальная область, которую может занимать переменная, то даже если тут используются три бита, занят этой структурой будет целый байт, и экономии опять не выходит?

Заранее большое спасибо!

Norong ★★
() автор топика
Ответ на: комментарий от Norong

количество байт зависит от архитектуры процессора или от чего-то другого?

От этого и от того, как сложится исторически. Значение у «слова» несколько и это либо лучший размер для архитектуры, либо то, что исторически было (на x86 это два байта, так как на 16-битных оно так было, а потом начали четверное слово, восьмерное и подобное писать).

char (1 байт, как я понимаю)

В основном да, хотя, вроде (в стандарте размер определён неявно, отсюда трудности с толкованием), могут быть архитектуры, где это не так (даже не везде в одном байте 8 бит).

мне не ясно, как это будет выглядеть в контексте машинных слов

Если char, то это 8 бит (CHAR_BIT точнее) и будет, для типов больших размеров будет больше бит. Просто не надо с большими типами работать как с char.

Как это соотносится с тем, что мы спокойно выполняем операции с отдельными битами?

Так что адрес битового поля взять нельзя. Т.е. оператор & не может выдать адрес 2.5 байта, только целые. А то, что дальше, это уже работа со значением внутри адресуемого участка памяти (теже >>, <<, |, &, но делается компилятором).

Как это представляется с точки зрения машинных слов и выравнивания переменных?

Обычно поля заполняют байты последовательно, хотя там ещё little/big endian порядок влияет на это. Выравнивание всей структуры будет до int скорее всего по умолчанию.

и экономии опять не выходит?

Будет, так как иначе понадобилось бы минимум 3 байта.

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

Будет, так как иначе понадобилось бы минимум 3 байта.

Это если выравнивание по int? А от чего зависит, по чему будет получаться выравнивание? Прочитал https://habrahabr.ru/post/142662/, у меня сложилось ощущение, что можно 4 char подряд поместить, а сдвиг ещё на три байта - это если int следом идёт, который 4 байта занимает.

Norong ★★
() автор топика
Ответ на: комментарий от xaizek

Т.е. ситуация такова:

Если мне достаточно нескольких битов (и я хочу сэкономить), то я могу либо: взять 8-битовый char (в принципе, любой тип можно так использовать, наверное?) и писать нужные мне данные в его разряды, либо завести такую структуру с битовыми полями и пользоваться ими. В первом случае будет занят 1 байт, во втором случае, вероятно, будет занято целое число байт, достаточное для вмещения того количества бит, которое я напишу в сумме после двоеточий. Размещение остальных переменных в машинном слове зависит от их длины.

Я правильно понимаю суть?

И правильно ли я понимаю, что любая переменная занимает место, равное 1/2^k от длины слова, и выравнивается строго по своей длине, т.е. при длине слова W переменная длиной W/2 будет размещена либо в первой половине W (если там пусто), либо во второй (если в первой есть хоть что-то), и таким образом я не всегда получу выигрыш от битовых полей/использования отдельных битов переменной? Например, если мне нужно, допустим, 7 «флажков», я их умещаю в байт, но следом у меня идёт 4-байтовая переменная и слово целиком занято:

  • флажки
  • пусто
  • пусто
  • пусто
  • 4-байтовая
  • переменная
  • занимает
  • всё это

И ещё: правильно ли я понимаю, что обычная структура (состоящая из переменных) представляется в памяти именно как несколько отдельных переменных (с соответствующим выравниванием), а структура из битовых полей является одной переменной, размер которой зависит от содержимого?

Norong ★★
() автор топика
Ответ на: комментарий от Norong

в принципе, любой тип можно так использовать, наверное?

Любой из целочисленных.

Я правильно понимаю суть?

Ага.

и таким образом я не всегда получу выигрыш от битовых полей/использования отдельных битов переменной?

Да. Можно размещать переменные отсортировав по размеру от больших к меньшим, при этом зазоров не будет (в них просто не будет необходимости).

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

Да.

а структура из битовых полей является одной переменной, размер которой зависит от содержимого?

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

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

Привет! Я тут столкнулся с кодом, который ведёт себя неожиданно для меня. Вот он: https://gist.github.com/anonymous/65f43af9a01e4a955eaaebde0592b6ba

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

Что меня смущает: в строке 8 я сознательно поставил в конце формата просто процент. Я ожидал, что произойдёт следующее: В цикле for дохожу до этого процента, попадаю в switch. Следующий после него символ - символ \0, которым кончается строка fmt. Этот символ ни одним из case'ов не обрабатывается и просто выводится на экран. После этого выполняется p++ и p теперь указывает на следующий элемент после конца строки (в учебнике говорилось: стандарт языка гарантирует, что адресная арифметика с участием первого элемента после конца массива будет работать корректно. А что в этом первом элементе находится? Ещё один \0? Всегда ли так?). Если там \0, то на этом работа программы должна закончиться, иначе - непредсказуемый результат.

Но программа почему-то спокойно выводит в итоге «2 4 8». Запустил в отладчике, поставив брейкпоинты на строках 26 и 27, смотрю на значение *p. Для первых двух пар остановок значения ожидаемые: %, d, %, d. Потом остановка на 26 даёт %, после continue остановка опять на 26 и опять %, после ещё одного continue остановка на 27 и *p = d, потом continue завершает выполнение программы. Я не понимаю, почему так происходит. Не мог бы ты помочь мне разобраться в происходящем? Заранее спасибо.

Norong ★★
() автор топика
Ответ на: комментарий от Norong

стандарт языка гарантирует, что адресная арифметика с участием первого элемента после конца массива будет работать корректно. А что в этом первом элементе находится? Ещё один \0? Всегда ли так?

Никто не знает. Работать будет только арифметика, разименовывать там нельзя.

Если там \0, то на этом работа программы должна закончиться, иначе - непредсказуемый результат.

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

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

Т.е. предсказуемое поведение заканчивается чётко на \0?

А что с брейкпоинтами?

(gdb) break 26
Breakpoint 1 at 0x40063f: file 7.3.c, line 26.
(gdb) break 27
Breakpoint 2 at 0x40066b: file 7.3.c, line 27.
(gdb) run
Starting program: /home/user/Программы/C/a.out 

Breakpoint 1, minprintf (fmt=0x400874 "%d %d %") at 7.3.c:26
26	        switch(*++p) {
(gdb) continue
Continuing.

Breakpoint 2, minprintf (fmt=0x400874 "%d %d %") at 7.3.c:28
28	                ival = va_arg(ap, int);
(gdb) continue
Continuing.

Breakpoint 1, minprintf (fmt=0x400874 "%d %d %") at 7.3.c:26
26	        switch(*++p) {
(gdb) continue
Continuing.

Breakpoint 2, minprintf (fmt=0x400874 "%d %d %") at 7.3.c:28
28	                ival = va_arg(ap, int);
(gdb) continue
Continuing.

Breakpoint 1, minprintf (fmt=0x400874 "%d %d %") at 7.3.c:26
26	        switch(*++p) {
(gdb) print *p
$1 = 37 '%'
(gdb) continue
Continuing.

Breakpoint 1, minprintf (fmt=0x400874 "%d %d %") at 7.3.c:26
26	        switch(*++p) {
(gdb) print *p
$2 = 37 '%'
(gdb) continue
Continuing.

Breakpoint 2, minprintf (fmt=0x400874 "%d %d %") at 7.3.c:28
28	                ival = va_arg(ap, int);
(gdb) print *p
$3 = 100 'd'
После %, d, %, d, % я ожидал увидеть \0 на Breakpoint 2, а вместо этого я снова на Breakpoint 1 почему-то.

Norong ★★
() автор топика
Ответ на: комментарий от Norong

Т.е. предсказуемое поведение заканчивается чётко на \0?

Да, дальше же чужая память.

После %, d, %, d, % я ожидал увидеть \0 на Breakpoint 2, а вместо этого я снова на Breakpoint 1 почему-то.

Так если там \0, то он пойдёт на default: и потом продолжит за \0.

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

Так если там \0, то он пойдёт на default: и потом продолжит за \0.

Тьфу. Я, видимо, неправильно распорядился breakpoint'ами. Как я понимаю, breakpoint останавливает программу прямо перед выполнением строки, номер которой указан. Установив breakpoint на строку 27, я думал, что остановлюсь сразу после строки 26 и print *p покажет мне *p после выполнения ++p. Не подскажешь, как следовало для этого поставить breakpoint?

Norong ★★
() автор топика
Последнее исправление: Norong (всего исправлений: 1)
Ответ на: комментарий от Norong

Как я понимаю, breakpoint останавливает программу прямо перед выполнением строки, номер которой указан?

Да.

как следовало для этого поставить breakpoint?

Можно было обойтись одним breakpoint и сделать (p это сокращённая форма print):

p *(p + 1)
# или
p p[1]
print и некоторые другие команды gdb понимают чуть ли не весь синтаксис C (можно и функции вызывать, правда некоторые могут завершить сессию отладки, если что-то сложное сделают).

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

Привет! У меня тут возникло недопонимание со scanf. Есть такой вызов этой функции:

scanf("%lf %lf %*c %c %c", &num1, &num2, &oper, &ending)
num1 и num2 - double, oper и ending - char.

Этот вызов используется в калькуляторе с обратной польской записью, я считываю два числа типа double, потом пропускаю пробел после них с помощью %*c, считываю знак операции, потом считываю следующий символ. В зависимости от того, будет ли он пробелом, концом строки или EOF, будет принято решение о том, что делать дальше. После вызова функции я ввожу с клавиатуры следующее:

5 5 +<ENTER>

и рассчитываю, что в num1 будет 5, в num2 тоже 5, пробел после пятёрок будет пропущен, в oper будет + и в ending будет '\n'. Тем не менее, ввод не прекращается. После ввода ещё двух символов ввод прекращается, в num1 и в num2 стоят пятёрки, а в oper и ending - два последних символа. Я также попробовал вызов

scanf("%lf %lf %lc %c", &num1, &num2, &oper, &ending)
Тогда после ввода

5 5 +<ENTER>

я должен ввести ещё один символ, и в oper находится +, а в ending последний символ.

Во-первых, мне непонятно, почему эти два вызова дают разный результат. Насколько я понял, %*c считает и пропустит пробел, а %lc считает следующий непробельный символ, и вызовы должны бы сработать одинаково - пропустить пробел после второй пятёрки и взять знак плюс.

Во-вторых, не ясно, почему в первом вызове %*c как будто игнорирует пробел после пятёрки и берёт вместо этого знак плюс, а %c в обоих вызовах будто бы игнорирует конец строки после плюса и считывает следующий символ. В учебнике сказано, что при спецификации %c отменяется обычно включённый пропуск пустого пространства, но тут создаётся ощущение, что он работает.

Не мог бы ты прояснить мне происходящее? Заранее спасибо.

Norong ★★
() автор топика
Ответ на: комментарий от Norong

В учебнике сказано, что при спецификации %c отменяется обычно включённый пропуск пустого пространства, но тут создаётся ощущение, что он работает.

Для этого перед ним не должно быть пробелов. Пробел в форматной строке означает «любое количество пробельных символов» (включая \n). Т.е. строка должна быть такой:

scanf("%lf %lf%*c%c%c", &num1, &num2, &oper, &ending)
Это отменит специальную обработку пробелов после второго double и всё будет работать как ожидалось. Сопоставлять тот пробел явно нет необходимости на самом деле:
scanf("%lf %lf %c%c", &num1, &num2, &oper, &ending)
И пробел между двумя %lf можно убрать, там он подразумевается (чтобы можно было определить конец первого числа).

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

У меня в итоге такой код получился: https://gist.github.com/anonymous/cd6dd831002344960e10d88ae37c25ff

Он не совсем правильно обрабатывает EOF при вводе из командной строки, потому что если ввести

5 8 +<ENTER>

то проходится полный цикл for, и хотя логично отправить теперь EOF, если вычисления завершены, отправка EOF вступает в конфликт с %lf и программа завершается. Мне не пришло в голову никакого способа для решения этой проблемы, но я нашёл ungetc. Думаю, вникать в её использование раньше, чем будет прочитана теория по файловому вводу/выводу, не стоит, так что, эта проблема меня не беспокоит. Тем не менее, почему-то даже если строка просто заканчивается EOF'ом, программа завершается со словами «Некорректный формат входных данных». Я делаю так:

 printf "5 8 +" | ./a.out 
И ожидаю, что в ending попадёт EOF. Тем не менее, отладочная информация, выводимая в строках 43-44, говорит, что произошёл конфликт при считывании EOF по спецификации %c в char, и в ending находится 0 (а EOF, если я правильно помню, -1). Попробовал сделать ending типа short, вспомнив о том, что все приличные символы имеют неотрицательный код. Может, EOF нельзя поместить в char? Оказалось, что можно, и проблема не в этом. Кстати, я прочитал, что char может быть как знаковым, так и беззнаковым. Я правильно понимаю, что в C все целочисленные переменные по-умолчанию знаковые, если не сказано обратное? На этом идеи о том, почему программа не работает, иссякли. Вероятно, это какая-то хитрость спецификации %c, о которой я не в курсе. Пожалуйста, подскажи, что я делаю не так, если тебе не сложно.

Norong ★★
() автор топика
Ответ на: комментарий от Norong

И ожидаю, что в ending попадёт EOF.

Не попадёт, его нету в потоке. Это значение только сигнализирует о завершении потока, но никак не читается форматированным вводом (scanf его встречает и просто завершается). Из getc() оно возвращается потому, что иначе никак не сказать об ошибке, а scanf() может вернуть количество реально считанных значений.

Я правильно понимаю, что в C все целочисленные переменные по-умолчанию знаковые, если не сказано обратное?

Все кроме char. Он знаковый/беззнаковый в зависимости от архитектуры. Например, на x86 знаковый по умолчанию, на ARM беззнаковый. Есть ещё флаги компилятора -fsigned-char, -funsigned-char.

Если первое число не считалось, можно проверить на feof(stdin) и просто выйти. Если не использовать дополнительных функций, то либо ввод всегда завершать \n, либо не полагаться на наличие \n (но тогда всё равно нужны будут fgets и sscanf). Так что проще первое.

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

Не попадёт, его нету в потоке.

А getchar(), получается, работает по аналогии с getc()?

Если первое число не считалось, можно проверить на feof(stdin) и просто выйти.

Прочитал про feof. Если EOF как таковой в потоке отсутствует, то, получается, дойдя до конца файла, scanf оставляет поток пустым, а feof проверяет, пусто ли там, и тогда проверку feof'ом надо проводить после вызова scanf, про который я подозреваю, что мог встретиться EOF?

Если не использовать дополнительных функций, то либо ввод всегда завершать \n

Т.е. чтобы каждая строка кончалась \n, а последняя - ещё и EOF? А как мне в моём случае поможет \n? Всё равно же надо будет определять, встретился EOF, или нет, чтобы завершить выполнение программы.

Norong ★★
() автор топика
Ответ на: комментарий от Norong

А getchar(), получается, работает по аналогии с getc()?

getchar() == getc(stdin)

Если EOF как таковой в потоке отсутствует, то, получается, дойдя до конца файла, scanf оставляет поток пустым, а feof проверяет, пусто ли там, и тогда проверку feof'ом надо проводить после вызова scanf, про который я подозреваю, что мог встретиться EOF?

Только проверка в feof() не на пустоту, а на флаг. Если поток дошёл до конца, то флаг взводится и остаётся в таком состоянии (сбрасывается вручную, но это редко надо). А так да.

Т.е. чтобы каждая строка кончалась \n, а последняя - ещё и EOF?

Так практически всегда и есть.

А как мне в моём случае поможет \n? Всё равно же надо будет определять, встретился EOF, или нет, чтобы завершить выполнение программы.

Я думал, последний %c можно заменить на \n, но, видимо, не получится так.

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

Флаг - это бит, который есть у каждого потока, и говорит о том, достигнут ли был конец файла при чтении из потока? А как происходит определение конца потока, если EOF в самом потоке нет? И если из потока действительно изымаются символы при их чтении, то в чём принципиальное отличие проверки флага от проверки «пустоты»?

Norong ★★
() автор топика
Ответ на: комментарий от Norong

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

Да.

А как происходит определение конца потока, если EOF в самом потоке нет?

Когда системный вызов возвращает признак конца данных («считано 0 байт»).

И если из потока действительно изымаются символы при их чтении, то в чём принципиальное отличие проверки флага от проверки «пустоты»?

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

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

Понял, спасибо!

И у меня возник опять вопрос по размещению переменных в машинных словах. Ты раньше сказал:

Можно размещать переменные отсортировав по размеру от больших к меньшим, при этом зазоров не будет (в них просто не будет необходимости).

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

Norong ★★
() автор топика
Ответ на: комментарий от Norong

Правильно ли я понимаю, что порядок размещения переменных в памяти зависит от порядка их объявления?

Правильно для элементов структур, для остального не обязательно.

Кроме того, ты раньше говорил, что локальные переменные размещаются «на стеке». Это какая-то отдельная область памяти, отличная от той, где выделяется место под глобальные переменные?

Ага, та же, которая отвечает за вызовы подпрограмм (передача аргументов через стек может происходить). Глобальные переменные не меняются в размере, поэтому с ними проще. А стек растёт/уменьшается постоянно.

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

Обычно имеет смысл упорядочивать только структуры, которые часто используются (или создаются большие массивы таких структур). В противном случае экономия будет «на спичках». На глобальные переменные и стек никаких гарантий размещения, вроде, нет и компилятор может их перетасовать для экономии памяти самостоятельно (на стеке точно нет, там такое делают давно, а для глобальных не уверен, но вряд ли, оно просто ничего не даёт).

xaizek ★★★★★
()
Последнее исправление: xaizek (всего исправлений: 4)
Ответ на: комментарий от xaizek

Понял, спасибо!

И ещё один вопрос про scanf и EOF. Я понял так, что если я запрашиваю символ, а там - конец файла, система говорит, что достигнут конец файла, и в поток ставится флаг EOF. Возвращается ли при этом какой-то символ в ответ на запрос? У себя я несколько раз получил числовое значение 0 в той переменной, куда хотел считать символ, но не могу быть уверен, всегда ли будет так, или это неопределённое поведение.

Norong ★★
() автор топика
Последнее исправление: Norong (всего исправлений: 1)
Ответ на: комментарий от Norong

Возвращается ли при этом какой-то символ в ответ на запрос?

Нет, только признак конца.

У себя я несколько раз получил числовое значение 0 в той переменной, куда хотел считать символ, но не могу быть уверен, всегда ли будет так, или это неопределённое поведение.

То случайность, либо ноль было был на стеке, либо scanf() инициализирует переменную сначала. Если эта переменная не в числе тех, которые scanf() смог полностью обработать (скажем, четвёртое, а scanf() вернул 3), то его значение использовать не стоит.

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

Я прошёл дальше к разделу о файловом вводе/выводе, и у меня возникло несколько вопросов. Не мог бы ты мне их прояснить?

В учбенике написано такое:

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

Дальше в учебнике сказано, что символы для вывода putc накапливаются в буфере, а потом сбрасываются в нужный файл при закрытии файла, которое происходит по fclose или при нормальном завершении программы. Это - тот же буфер, о котором идёт речь в цитате? Если да, то он, получается, либо существует только для файлов, открытых на запись, либо для файлов, открытых на чтение, он имеет другой смысл. Это так? Место под такой буфер, наверное, динамически выделяется malloc'ом по ситуации?

И ещё вопрос: правильно ли я понимаю, что при чтении из stdin я «забираю» из потока символы (и там их больше не остаётся), а при чтении из файла я «продвигаюсь» вперёд по символам?

И ещё: под «нормальным» завершением программы, при котором происходит сброс буфера, понимается завершение по return/exit из main и завершение по exit из любого другого места, или что-то другое?

Заранее спасибо.

Norong ★★
() автор топика
Ответ на: комментарий от Norong

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

Есть разные виды буферизации. При закрытии оно сбрасывается всегда (при нормальном завершении дескрипторы просто закрываются тем же fclose). Но также сбрасывается либо по \n, либо немедленно (т.е. без буферизации). stdout по \n обычно, если не был от терминала.

Это - тот же буфер, о котором идёт речь в цитате? Если да, то он, получается, либо существует только для файлов, открытых на запись, либо для файлов, открытых на чтение, он имеет другой смысл. Это так? Место под такой буфер, наверное, динамически выделяется malloc'ом по ситуации?

Их два. Размер обычно фиксированный (на усмотрение libc), но буфер можно и самому менять. Может динамически, может быть частью FILE на самом деле.

И ещё вопрос: правильно ли я понимаю, что при чтении из stdin я «забираю» из потока символы (и там их больше не остаётся), а при чтении из файла я «продвигаюсь» вперёд по символам?

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

И ещё: под «нормальным» завершением программы, при котором происходит сброс буфера, понимается завершение по return/exit из main и завершение по exit из любого другого места, или что-то другое?

Да, exit по сути. main на самом деле вызывается как exit(main(argc, argv)), так что выход из него эквивалентен вызову exit.

xaizek ★★★★★
()
Ответ на: комментарий от Norong

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

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

stdout по \n обычно, если не был от терминала.

Прошу прощения, я не совсем понял условие «если не был от терминала». Т.е. сброс по \n, если stdout в файл перенаправлен?

Norong ★★
() автор топика
Ответ на: комментарий от Norong

Там слово потерялось при редактировании, «если не был перенаправлен от терминала». Т.е. при выводе в терминал буфер сбрасывается по \n (или явному fflush() или при заполнении буфера, что раньше произойдёт), но если перенаправить в файл или через конвеер, то уже по заполнению буфера (либо fflush). Поэтому иногда перенаправление в файл и вывод потом этого файла на экран может быть быстрее, чем просто вывод на экран.

xaizek ★★★★★
()
Последнее исправление: xaizek (всего исправлений: 1)
Ответ на: комментарий от xaizek

Мне тут в учебнике привели код функции fputs в том виде, в котором он фигурировал в стандартной библиотеке системы авторов. Вот он: https://i.imgur.com/Bs2ivOc.png

Я не очень понимаю, зачем нужен «int c». Мне кажется, можно было легко обойтись без него, просто используя *s++ и *s. Не подскажешь, есть ли в этом «c» какой-то глубинный смысл?

Norong ★★
() автор топика
Последнее исправление: Norong (всего исправлений: 3)
Ответ на: комментарий от xaizek

Кстати, я нередко по ходу учебника видел, что хотя подразумевается помещение в переменную символа, используют тип int. Это, наверное, из-за того, что char может быть беззнаковым, и в таком случае в него не удастся поместить EOF?

Norong ★★
() автор топика
Ответ на: комментарий от Norong

EOF в char равно символу '\xff' и их невозможно отличить. Так как int может хранить все значения что и char, но больше, то для работы с символами в stdio используется int, как раз чтобы можно было кодировать EOF без опасности пересечься с настоящим символом '\xff'.

xaizek ★★★★★
()
Ответ на: комментарий от Norong

есть ли в этом «c» какой-то глубинный смысл?

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

Да и код концептуально проще — всегда можно считать, что s всегда указывает на текущий символ, а не на следующий, если перед этим было *s++.

i-rinat ★★★★★
()
Ответ на: комментарий от Norong

В int это 0xffffffff (для sizeof(int) == 4), а в char 0xff и при правильном расширении до int оно будет 0x000000ff (правильное это с преобразованием к unsigned char, иначе будет 0xffffffff из-за расширения знакового разряда).

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

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

#include <stdio.h>
#include <string.h>

#define LIMIT 200

int main(int argc, char *argv[])
{
    char string1[LIMIT], string2[LIMIT];
    FILE *fp1, *fp2;

    fp1 = fopen(argv[1], "r");
    fp2 = fopen(argv[2], "r");

    do {
        fgets(string1, LIMIT, fp1);
        printf("%s", string1);
        fgets(string2, LIMIT, fp2);
        printf("%s", string2);

        if (strcmp(string1, string2) != 0) {
            printf("%s: %s\n%s: %s\n", argv[1], string1, argv[2], string2);
            return 0;
        }
    } while(!feof(fp1) && !feof(fp2));
    return 0;
}
Код работает не совсем корректно. Если файлы совпадают за исключением того, что один файл длиннее, например:
file1:
первая строка
вторая строка
третья строка
последняя строка
file2:
первая строка
вторая строка
третья строка
то неверно выводится та строка из второго файла, которая отличается от строки с тем же номером первого файла. При таких входных данных я ожидал вывод:
file1: последняя строка
file2: 
, но получаю
file1: последняя строка
file2: третья строка

В своих предположениях я исходил из приведённого в учебнике кода fgets: https://i.imgur.com/UbiubwU.png После того, как считаны третьи строки, я захожу в цикл ещё раз. Для первого файла fgets запишет в string1 «последняя строка», а для второго, как я понимаю, getc вернёт EOF, и '\0' должен быть записан в нулевую позицию string2. Тем не менее, этого не происходит. Не подскажешь, что я делаю не так?

И ещё: флаг EOF ставится в поток после того, как я считаю оттуда последний символ, или же при попытке считать что-то сверх последнего символа?

Norong ★★
() автор топика
Ответ на: комментарий от Norong

В своих предположениях я исходил из приведённого в учебнике кода fgets

В man fgets явно не сказано о неизменности массива (просто, мол '\0' ставится после последнего считанного символа, а EOF это не считанный символ). В стандарте более явно:

7.19.7.2 The fgets function
... If end-of-file is encountered and no characters have been read into the array,
the contents of the array remain unchanged and a null pointer is returned. ...
Так что эту ситуацию надо самому обрабатывать.

И ещё: флаг EOF ставится в поток после того, как я считаю оттуда последний символ, или же при попытке считать что-то сверх последнего символа?

Второй вариант.

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

Я в учебнике прочитал описания двух функций, и не совсем понял пару моментов.

Функция ungetc. Помещает символ «c» назад в файл «fp» и возвращает либо «c», либо EOF в случае ошибки. Для каждого файла гарантирован возврат только одного символа.

Как происходит «возврат» символа в файл в случае работы с обычным файлом и в случае с stdin? Раньше в учебнике была реализована функция, выполняющая схожую роль, она записывала символы в отдельный буфер, а для чтения символов использовалась функция-обёртка над этим буфером и над обычным getchar(). И не совсем ясно про «гарантирован возврат только одного символа». Как я понимаю, это значит, что один символ я в любом случае смогу вернуть в файл. А что может мне помешать вернуть больше, и чем обусловлена гарантия?

Дальше:

Функция free(p) освобождает участок памяти, на который указывает указатель p. ... Порядок освобождения выделенных участков памяти не регламентируется. Однако если указатель не был получен с помощью malloc или calloc, то его освобождение является грубой ошибкой. Обращение по указателю после его освобождения - также ошибка. <дальше пример правильного освобождения связанного списка> В разделе 8.7 показана реализация функции распределения памяти наподобие malloc, в которой выделенные блоки можно освобождать в любом порядке.

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

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

Там ещё много текста, но, вроде, основная мысль в этом.

Ну и тут есть одно задание:

Такие функции, как isupper, можно реализовать с позиций экономии времени, а можно с позиций экономии места. Исследуйте обе возможности.

isupper же - функция проверки, является ли аргумент функции символом в верхнем регистре. Мне приходит в голову один единственный способ такой проверки: if (c >= A && c <= Z). Вроде, он настолько примитивный, что тут ни время не тратится, ни память. Чего ждёт от меня автор?

Пожалуйста, помоги мне разобраться с этими вопросами. Заранее большое спасибо.

Norong ★★
() автор топика
Последнее исправление: Norong (всего исправлений: 1)
Ответ на: комментарий от Norong

Как происходит «возврат» символа в файл в случае работы с обычным файлом и в случае с stdin? Раньше в учебнике была реализована функция, выполняющая схожую роль, она записывала символы в отдельный буфер, а для чтения символов использовалась функция-обёртка над этим буфером и над обычным getchar().

Так и происходит, но уже внутри реализации стандартной библиотеки.

И не совсем ясно про «гарантирован возврат только одного символа». Как я понимаю, это значит, что один символ я в любом случае смогу вернуть в файл. А что может мне помешать вернуть больше, и чем обусловлена гарантия?

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

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

О том, что порядок вызовов free не должен повторять или быть обратным порядку вызовов malloc. Не знаю, зачем именно они это тут уточняют (это для стека важен порядок, но с ним напрямую в C не работают).

Мне приходит в голову один единственный способ такой проверки: if (c >= A && c <= Z). Вроде, он настолько примитивный, что тут ни время не тратится, ни память. Чего ждёт от меня автор?

Детали, конечно, зависят от архитектуры, но условные переходы вещь не бесплатная и от них бывают избавляются (ну и это для английского в ASCII, для других кодировок проверки могут быть изощрённее; EBCDIC, например, неудобна в этом смысле была). Так что это с позиции экономии памяти. Как сделать с противоположной позиции и так, чтобы было без переходов, должно быть несложно догадаться.

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

О том, что порядок вызовов free не должен повторять или быть обратным порядку вызовов malloc.

Т.е. он должен быть каким угодно, но не прямым и не обратным?

Не знаю, зачем именно они это тут уточняют (это для стека важен порядок, но с ним напрямую в C не работают).

А для меня, как для пишущего код, это, получается, не имеет значения? А в каком случае имеет?

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

Могу так: return (c >= A && c <= Z) или return (isupper(c) == c), но есть сомнения. Первый вариант из этого сообщения от варианта с if-ом отличается разве что тем, что я не проверяю значение этого логического выражения, а сразу его возвращаю, да и всё ещё зависимо от кодировки. Второй вариант, вероятно, дороже, ибо isupper тоже в себе что-то содержит.

Norong ★★
() автор топика
Ответ на: комментарий от Norong

Т.е. он должен быть каким угодно, но не прямым и не обратным?

Нет. Это было отрицание необходимости соблюдения дополнительных условий. Главное это освобождать память один раз и не обращаться к ней потом.

А для меня, как для пишущего код, это, получается, не имеет значения? А в каком случае имеет?

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

Могу так: return (c >= A && c <= Z) или return (isupper(c) == c), но есть сомнения. Первый вариант из этого сообщения от варианта с if-ом отличается разве что тем, что я не проверяю значение этого логического выражения, а сразу его возвращаю, да и всё ещё зависимо от кодировки. Второй вариант, вероятно, дороже, ибо isupper тоже в себе что-то содержит.

И без сравнений тогда ещё, так как их может понадобиться много. А можно не делать эти проверки во время исполнения, а сделать иначе. У char всего 256 возможных значений.

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

про память

Т.е. ситуация такова, что в большинстве случаев нет разницы, в каком порядке выделять/освобождать память, если только я сначала её выделяю, потом использую, освобождаю и больше не использую. Именно это и имелось в виду под:

Порядок освобождения выделенных участков памяти не регламентируется.

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

Ага?

И без сравнений тогда ещё, так как их может понадобиться много. А можно не делать эти проверки во время исполнения, а сделать иначе. У char всего 256 возможных значений.

Может быть, заранее составить список кодов подходящих букв, и искать в этом списке мой код?

Norong ★★
() автор топика
Последнее исправление: Norong (всего исправлений: 1)
Ответ на: комментарий от Norong

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

Ага?

Да. Обычно такого нету, но видимо авторы работали с такой реализацией.

Может быть, заранее составить список кодов подходящих букв, и искать в этом списке мой код?

Вроде того. Но тут смотря как этот список и поиск в нём будут организованы.

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

Спасибо, про память всё понял!

Вроде того. Но тут смотря как этот список и поиск в нём будут организованы.

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

Norong ★★
() автор топика
Ответ на: комментарий от Norong

Создать массив, пронумерованный символьными кодами больших букв, и при получении символа «c» попытаться обратиться к элементу с номером «c»?

Да. Только главное про возможную знаковость char не забыть.

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

Можно просто создать массив на 256 элементов. Есть же и другие is*() функции, каждый элемент каким-то свойством да обладает. В C89 иных вариантов особо и нет (кроме вычисления таблицы единожды при первом вызове или её генерации другой программой с последующей вставкой в исходники). В C99 добавили designated initializers и можно делать так:

char array[] = {
    ['A'] = 1,
    ['B'] = 1,
    ...
};
Можно реализовать, просто вычислив таблицу один раз и вызвав проверку несколько раз.

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

Да. Только главное про возможную знаковость char не забыть.

Это как раз в случае, если я создаю вручную массив на 256 персон? Если беззнаковый, то нумерую от 0 до 255, иначе - от -128 до 127, и делать выбор надо будет самому в зависимости от системы, так? И в те элементы, которым соответствуют большие буквы, пишу 1, остальные инициализируются нулями. Тогда можно сходу return array['c'], наверное. Если хочу хранить разные свойства - можно каждый элемент массива сделать структурой с битовыми полями. Или структурой с переменными, если места не жалко.

В C99 добавили designated initializers и можно делать так

Я правильно понимаю, что отличие от того, что есть в C89, заключается в том, что я могу не просто указывать инициализирующие значения, но и помещать их в нужные мне элементы?

Можно реализовать, просто вычислив таблицу один раз и вызвав проверку несколько раз.
В C89 иных вариантов особо и нет (кроме вычисления таблицы единожды при первом вызове или ...

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

Norong ★★
() автор топика
Ответ на: комментарий от Norong

Это как раз в случае, если я создаю вручную массив на 256 персон? Если беззнаковый, то нумерую от 0 до 255, иначе - от -128 до 127, и делать выбор надо будет самому в зависимости от системы, так?

Просто преобразовывать к unsigned char и помнить, что 128 элемент это -128 и т.д.

Я правильно понимаю, что отличие от того, что есть в C89, заключается в том, что я могу не просто указывать инициализирующие значения, но и помещать их в нужные мне элементы?

Да.

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

Ага. Но тогда будет проверка на то, что вызов первый. Так что это как вариант, чаще всю таблицу просто готовят заранее.

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

Просто преобразовывать к unsigned char и помнить, что 128 элемент это -128 и т.д.

Т.е. достаточно делать массив от -128 до 127, и преобразовывать к unsigned char счётчик (при заполнении) и номер символа (при поиске)? Вроде, он тогда сам преобразуется из 128 в -128, мне даже помнить об этом не надо будет.

Norong ★★
() автор топика
Ответ на: комментарий от Norong

Т.е. достаточно делать массив от -128 до 127, и преобразовывать к unsigned char счётчик (при заполнении) и номер символа (при поиске)?

Да. В общем, главное не обратиться к элементам до начала массива и значения для отрицательных char'ов правильно разместить.

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

А в чём тут есть подводные камни? У меня же при приведении к unsigned char всё равно любому коду символа будет поставлен в соответствие номер в массиве, и даже порядок, в сущности, значения не имеет. Или я что-то не так понимаю?

И, кажется, я постом выше ошибся: если приводим к unsigned, то массив как раз 0..255.

Norong ★★
() автор топика
Последнее исправление: Norong (всего исправлений: 1)
Ответ на: комментарий от Norong

А в чём тут есть подводные камни? У меня же при приведении к unsigned char всё равно любому коду символа будет поставлен в соответствие номер в массиве, и даже порядок, в сущности, значения не имеет. Или я что-то не так понимаю?

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

xaizek ★★★★★
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.