С - это неоднозначный язык. Сам по себе он небольшой, об этом писал Брайан Керниган в своей знаменитой книге "Язык программирования С". Но это не мешает комбинируя базовые возможности языка составлять более комплексные и запутанные конструкции. Иногда они встречаются в кодах реальных программ довольно редко, из-за своей узкой области применения - о них и пойдёт речь в этой статье.
Структура статьи
Сначала расскажу о базовых и часто встречаемых конструкциях, которые могут и вовсе не удивить опытных программистов на С, но вот уже ниже идут всё более редкие и интересные конструкции, которые уже далеко не часто можно где-либо встретить.
Содержание
union
register
Статические функции
Константный указатель
Препроцессорная склейка строк
Конкатенация строк во время компиляции
#undef
sizeof без ()
Создание функции в стиле K&R
Указатели на функции
Указатели на указатели
Функции с переменным количеством параметров
Массив + индекс (два в одном)
Индекс с массивом, но вверх ногами
strtok(), tmpfile()
Возврат указателя на функцию из функции
volatile
Макросы с переменным количеством параметров
auto
Использование возвращаемых значений scanf() и printf()
Каламбур типизации
Отрицательные индексы
union
Объединения в С встречаются в зависимости от задачи:
union cnt {
int ival;
float fval;
};
Это далеко не редкое явление, и эта конструкция вошла в список только по той причине, что по частоте применения она не может конкурировать с подобным ей struct, который использует такой же синтаксис но находит себе место даже в небольших программах гораздо чаще чем объединения.
register
Это ещё одно ключевое слово в С. А вот вы можете вспомнить когда вы создавали регистровую переменную в последний раз? И я нет. Стандарт языка говорит что компилятор может и вовсе проигнорировать это ключевое слово и вместо регистра процессора поместить переменную в оперативную память. К тому же нельзя брать адрес регистровой переменной в независимости от того помещена она в регистр на самом деле, или нет. Из плюсов только то, что к ней вроде как обращение идёт быстрее, так как она находится в регистре, но что-то не особо верится. Думаю понятно, почему это ключевое слово вошло в список - минусов здесь к сожалению больше чем плюсов.
Статические функции
Если объявить автоматическую переменную с ключевым словом static
, то переменная в отличии от обычной сохранит своё значение при выходе из функции. Но можно и саму функцию сделать статической:
static char* get_next_password (const password_t stack[], ssize_t *sp){
return (sp > 0) ? stack[--*sp].data : NULL;
}
При беглом просмотре можно подумать что функция возвращает static char*
, но на деле же она возвращает обычный указатель на строку, а вот статическое объявление говорит о том, что функция видна только в том файле исходного кода, в котором она описана. То есть если вы подключите заголовочный файл с этой функцией к другому, и попытайтесь вызвать её из другого файла, то будет ошибка о том что имя не найдено. Это некий механизм сокрытия имён, только в С. Такой приём часто используется в ядре ОС Linux. Но вот в небольших, а уж тем более одно-файловых программах вы такое вряд-ли найдёте. То же применимо и к статическим глобальным переменным - эффект такой-же как и с функциями. Применение я здесь увидел только одно - хранение 2-х функций с одинаковыми именами в 2-х разных файлах исходного кода.
Константный указатель
Указателем на константу уже никого не удивишь - const char*
. Совсем другое дело константный указатель: char * const
Хоть я сам часто пишу на С, но о такой вещи не знал, и встречается она далеко не часто. Для ясности картины:
char* ptr;
- указатель на charconst char* ptr;
- указатель на const charchar * const ptr;
- константный указатель на charconst char * const ptr;
- константный указатель на const char
Конечно никто не запрещает делать так: char const *ptr;
, то есть поменять char и const местами. Это также будет указателем на const char.
Препроцессорная склейка строк
Выглядит странно, но следующий фрагмент программы прекрасно компилируется, и запускается:
int ma\
in (void){
retu\
rn 0; }
Оператор \
- это специальный инструмент препроцессора, позволяющий склеить текущую и следующую строку исходного кода перед компиляцией. Зачем это нужно? Например когда слишком длинное условие:
} while ((x >= 0) && (y >= 0) && (x <= WIN_SX-1) && (y <= WIN_SY-1) && flag && f(2));
Можно его разделить с помощью знаков \
:
} while\
((x >= 0) &&\
(y >= 0) &&\
(x <= WIN_SX-1) &&\
(y <= WIN_SY-1) &&\
flag && f(2));
Пример надуманный так как описанный выше код прекрасно работает и без этих символов. Но нельзя просто так переносить слова - они будут восприниматься как два имени, будь-то имена переменных, констант, ключевые слова и т.д. Да и вообще не пишите длинных условий, лучше пишите функции/макросы для таких длинных участков:
} while (access()); /* мм красота */
Также они применяются чтобы красиво оформить макросы
#define macro\
/* code */
Конкатенация строк во время компиляции
Продолжая тематику строк можно ещё вспомнить приём конкатенации строк во время компиляции. То есть если написать две строки рядом, то они склеятся в одну строку.
puts( "Hello, " "world!" );
- склеится в "hello, world!"
А вот это уже будет полезно для работы с длинными строками, чтобы можно было их переносить. Так как просто перенести, не закрыв и/или не открыв двойные кавычки не получится:
const char* ultra_string = "Lorem
ipsum..."; //ошибка
const char* ultra_string = "Lorem"
" ipsum..."; //склеится в "Lorem ipsum..."
Это свойство строк хорошо сочетается с препроцессорным оператором #
,который переводит операнд в строку. Итак можно придумать такой отладочный вывод:
#define dprint(exp)\
printf(#exp " = %i\n", (int)(exp));
При вызове int n = 10; dprint(n/5);
будет следующая последовательность операций препроцессора:
printf(#exp " = %i\n", (int)(exp)); //исходная строка
printf(#(n/5) " = %i\n", (int)(n/5)); //вставка аргумента
printf("n/5" " = %i\n", (int)(n/5)); //конвертирование в строку
printf("n/5 = %i\n", (int)(n/5)); //конкатенация строк
Вот простой пример применения, который был описан в книге Брайана Кернигана.
#undef
На самом деле применений у этой препроцессорной директивы не так уж и много. Разве что она может ограничить действие имени определённого через #define
. Или же ставить его перед функциями так:
#undef to_str /* на всякий "пожарный" */
char* to_str (long val);
Но всё же я не думаю, что это сильно полезно, особенно когда программист уверен что такого макроса не существует.
sizeof без ()
И действительно, так как sizeof
не является функцией, то по идеи как и в случае с return
можно писать выражение, размер которого мы вычисляем без скобок. Но не всегда - стандарт говорит что скобки необходимы в случае с вычислением размера типа. А когда вычисляется размер выражения - скобки вообще не обязательны:
int *dyn_arr = malloc( sizeof *dyn_arr * A_SIZE);
Создание функции в стиле K&R
Вообще в стандарте C2X больше не позволительно объявлять функции так:
size_t move(a,b)
size_t a, b;
{
a = a ^ b;
b = a ^ b;
a = a ^ b;
}
К сожалению или к счастью практически все современные программы на С которые написаны на С11 и выше уже используют привычную всем конструкцию: тип аргумент, тип аргумент...
Указатели на функции
И всё, что вообще с ними связано. Я думаю они не сильно полезны, максимум как аргумент для другой функции, к примеру qsort()
из заголовочного файла <stdlib.h>
использует int (*compare)(const void*, const void*)
в качестве параметра. Конечно говорить что они совсем не нужны категорически нельзя. Здесь всё зависит от задачи. Ещё они позволяют чуть лучше понять С и как он работает. Кстати в указатель на функцию можно положить как адрес функции, так и имя функции, так какf = &f
, напоминает ситуацию со статическими массивами: m = &m
. Бывают полезны указатели на функции и в случае с массивом из функций, обращение к которому выглядит весьма необычно: (void)funcs[idx](param1,param2);
void parse_line(const char* input, rtable_line* line){
sscanf("%zu %zu %c\n", line->adress, line->hist_cnt, line->state);
}
...
void (*parse_func)(const char*, rtable_line*) = &parse_line; // 1 способ
parse_func = parse_line; //2 способ
parse_func(_input, _line); //вызов по указателю
В приведённом примере выше указать на функцию parse_line
можно двумя способами: явно указав амперсанд, или не указывая его вовсе. 2 способа эквивалентны.
Указатели на указатели
на указатели на указатели на указатели...
То есть:
int *p; /* всё нормально - может быть переменная, а может быть массив */
Как по мне такая конструкция может встретится на практике в таком виде:
int **p; /* это всё ещё переменная, а может быть двумерный массив. А может надо изменить указатель через его адрес */
int ***p; /* что-то много звёздочек. Трёх-мерный массив? Адрес адреса указателя? Переменная? */
int ****p; /* если вы пишите что-то такое не комментируя - обратитесь к врачу */
void free_and_setto_NULL (void** ptr){
free(*ptr);
*ptr = NULL;
}
Но что-то я сомневаюсь что в таком случае нельзя обойтись макросом, или вовсе сделать все эти операции на месте вызова.
Функции с переменным количеством параметров
Как часто вы видите подобное?
#include <stdarg.h>
void print_strings (size_t count, ...){
va_list cstring;
va_start(cstring, count);
while (count > 0){
puts(va_arg(cstring, char*));
--count;
}
va_end(cstring);
}
Можно было например передать массив строк, тогда код получился бы короче:
void print_strings (size_t count, const char* strings[]){
while (count > 0)
puts(*strings++), --count;
}
Ну да, вызывать будет посложнее: print_strings( 2, (const char*[]){"I love C", "What am I doing?"});
, но такой способ как минимум уменьшает объём кода в 3 раза. Но зато объявление функций с переменным количеством параметров выглядят красиво.
void print_strings( size_t, ... ); /* красота */
Стоит упомянуть, что в C функция, объявленная без параметров принимает произвольное число аргументов. Если вы не собирайтесь передавать в функцию никаких параметров, то при объявлении функции хорошей практикой будет написать void
на месте параметров. В таком случае при передаче чего-либо в функцию компилятор не проигнорирует такой вызов в отличии от ()
на месте аргументов.
Массив + индекс (два в одном)
Редко (очень редко) где, я встречаю подобные выражения:
"Programming"[i++]
В самом деле, немногие начинающие программисты знают что можно объявить массив/строку и взять элемент из неё. Наверно с первого взгляда покажется, что данная комбинация вообще не имеет смысла - зачем брать целый массив только ради получения одного элемента из него? Однако данная конструкция находит себе место под солнцем, например в рисовании символов по яркости:
for (int i = 0; i < 10; i++){ putchar("@#c;:,. "[brightness[i]]); }
Здесь очень удобно проиллюстрирована возможность не создавать отдельный массив для символов яркости. Или:
printf ((char*[]){ "NO", "YES" } [state]);
Вот так можно обойти создание массива строк, хотя в таких ситуациях более понятным и читабельным всё же будет тернарный оператор ?:
Индекс с массивом, но вверх ногами
Простой пример:
int arr[ARR_SIZE], i = ARR_SIZE-1;
while (i>=0) {i--[arr] = i+1;}
Здесь может показаться что будет ошибка в выражении i--[arr]
, так как индекс и имя массива перепутаны местами. Но всё прекрасно работает - язык С, это язык возможностей. Вот незадача - оно работает, но как? При первом беглом взгляде я подумал об адресах. Быть может если arr представить как адрес, то тогда всё сходится, так как это целое число а индекс должен быть целым числом. Но меня ждал сюрприз во время разбора индекса, я не понял как из i--
можно взять к примеру элемент под 3 индексом? 3 бит числа? Но потом прочитав объяснение всё сразу стало понятно.
Когда компилятор встречает выражение на подобие arr[i--]
, то он представляет его как *(arr + i--).
Иными словами он прибавляет к адресу первой (нулевой) ячейки массива индекс в квадратных скобках, а потом по полученному адресу достаёт значение. А что будет если поменять местами индекс и массив? i--[arr]
оттранслируется как *(i-- + arr)
. От перемены мест слагаемых сумма не меняется, следовательно и итоговый адрес тоже. Если ранее упомянутые конструкции находили себе хоть какое-то применение, то это просто забавный способ запутать программу для человека, который пока что не прочёл эту статью.
strtok(), tmpfile()
Те самые, одни из самых редких функций библиотеки С. char* strtok(s, d)
разбивает строку s на токены. При первом вызове надо передавать функции параметр s, а вот уже далее надо передавать NULL
, тогда функция вернёт следующий токен из той же строки. d - это строка делителей. То есть те символы, которые разбивают строку на токены. Возвращает указатель на следующий токен.
char str[50] = "Enjoy C programming!";
char* token = strtok(str, " .,!?"); //токены разделяются одним из символов d
do {
printf("token - \'%s\'\n", token);
token = strtok(NULL, " .,!?");
} while (token);
Такая функция может быть полезна для различного рода парсеров. Хотя для парсинга простых шаблонных строк больше подходит sscanf()
. + ко всему функция является небезопасной, так как редактирует исходную строку s, которую она принимает на вход.
А tmpfile()
просто возвращает указатель на промежуточный файл (или поток). В большинстве случаев достаточно просто массива. Если посмотреть использование функции в интернете на разных обучающих сайтах, то вы не найдёте ничего кроме заголовка FILE* tmpfile(void)
,и демонстрации открытия и закрытия этого файла.
Возврат указателя на функцию из функции
int (*foo(void))(size_t, size_t);
Страшное объявление, но это всего лишь функция, возвращающая указатель на функцию, которая имеет сигнатуру int f(size_t,size_t)
. Хорошей практикой будет избегать таких объявлений, используя typedef:typedef int (*funcptr)(size_t, size_t);
funcptr foo(void); //и всё понятно
Встречается такой стиль гораздо реже, но к нему надо стремиться. Ведь цель написать и быстрый и понятный код, а не только быстрый. (К программистам на Assemler не относится понятный, можете не волноваться по поводу читабельности). Да и вообще причиной ненависти к С у многих людей являются именно сложные объявления, а особенно те, в которых используются функции.
volatile
Иногда компилятор может оптимизируя удалять целые куски кода. Чтобы такого не было желательно ставить ключевое слово volatile
перед объявлением переменной, если вы хотите чтобы компилятор 100% не применил свои коварные оптимизации к этой переменной. (Хорошо что слово volatile
не может игнорироваться, как иногда бывает с register
, а то в этой жизни пришлось бы больше ни в чём не быть уверенным)
Макросы с переменным количеством параметров
Вы думали всё заканчивается на функциях? К удивлению для меня в некоторых стандартах (а в частности и С11) можно делать макросы с переменным количеством аргументов, точно так же как и функции
#define vmacro(...)
А как к параметрам-то обратиться? Всё просто - тут вам никаких <stdarg.h>
. Всего лишь в тексте для замены макросом нужно прописать __VA_ARGS__
. Может показаться странным, но если вы подумайте как работает препроцессор - это просто обработчик текста, и он не может думать о том, как работать с этими аргументами и их типами. Точно также как и в макросы с фиксированным числом аргументов препроцессор С вставляет их на их места, так и здесь. Отличие лишь в том что в случае с переменным количеством параметров на место вставится не один, а столько сколько было указано в круглых скобках при макро-вызове аргументов.
#define myprint(str, ...)\
printf(str, __VA_ARGS__);
Также такой способ даёт почву для размышлений о том какие розыгрыши можно выдумать с таким инструментом:
#define for(...) //обезвредили все циклы for
auto
Не знаю зачем, может в ранних версиях языка без этого слова было никак не обойтись. В современных реалиях int x;
объявленная внутри функции и так подразумевается автоматической, несмотря на то что здесь отсутствует явное auto int x;
На моей теме VScode оно даже не подсвечивается как ключевое слово. Хотя в C++ оно активно используется в случае, когда программист абсолютно не переживает о том какой тип имеет переменная:
for (auto idx = 0; idx < 10; ++idx)
Использование возвращаемых значений scanf() и printf()
Что может быть абсурднее? Кто-то может первый раз слышать о том, что printf()
из стандартной библиотеки возвращает кол-во напечатанных байтов на экран. Его братья sprintf()
, vprintf()
, fprintf()
делают тоже самое. Соответственно scanf()
возвращает кол-во считанных байт. Один раз видел даже такой код:
assert (printf("Some str") == strlen("Some str"));
Предохраниться лишний раз никогда не бывает лишним.
Каламбур типизации
Одно из моих любимых. Если знаешь как устроена память - то власть в твоих руках. Каламбур типизации это в большинстве случаев небезопасные / неопределённые стандартом / платформо-зависимые и опасные решения, основывающиеся на знании устройства памяти, и использующих инструменты языка. Объяснить мощь каламбура можно на следующем простом примере
Задача: дано шестнадцатеричное число uint64_t x = 0x2E6D6172676F7270;
В каждом байте числа записан символ. Вывести число как строку символов.
Первое что приходит в голову - извлекать текущий байт, отображать его на экране как символ, затем сдвигать число направо на один байт пока число не станет нулём.
uint8_t cb; while (cb = x & 0xFF){ putchar(cb); x >>= 8; }
Такая запись вполне работает, и выводит каждый байт числа как символ. Но всегда есть альтернативные пути решения такой-же задачи, но заметно короче. "Короче" не всегда значит лучше!
printf ("%.8s", (char*)&x);
В один вызов printf уложилась задача, ранее решаемая с помощью цикла. Но как?
Дело в том что память в компьютере хранится в ячейках, и каждая ячейка имеет свой размер в байтах. Если представить x не как одно большое 8-байтное число как массив байтов (символов), то получим вот это:
10111001 10110101 10000101 11001001 10011101 10111101 11001000 00000000} x
Далее мы берём адрес первого байта этого числа:
10111001 10110101 10000101 11001001 10011101 10111101 11001000 00000000
^
&x
И далее мы преобразуем адрес из указателя на число в указатель на строку приведением типов:
10111001 10110101 10000101 11001001 10011101 10111101 11001000 00000000 }
^
(char*)&x
И вот мы и получили строку, так как знаем что uint64_t
это просто массив из 8 байт, и с помощью изменения адреса мы изменили представление памяти этого конкретного куска для компилятора, которому принципиально не важно что именно находится по адресу - надо лишь чтобы размер соответствовал. Формат %.8s
неспроста имеет цифру 8. В ином случае мы рискуем вывести всё что лежит в памяти, пока не встретим 0. Поэтому поставим ограничение на 8 символов.
Почему это плохо?
Многие люди задаются этим вопросом. Это ведь сокращает код! Может показаться что это плохо из-за того что это не предусмотрено стандартом языка или из-за такого что такой код тяжёлый для чтения. Однако проблема совсем в другом. Вот пример базовой функции, возвращающей компонент структуры:
struct point {
int x, y;
};
int get_y(const struct point* p){ return p->y; }
Выражение p->y
здесь уж слишком банальное. Давайте усложним его
int get_y(const struct point* p){ return *(int*)((char*)&(p->x)+4); }
Здесь сначала берётся адрес переменной p->x.
Затем зная что переменные в структуре идут в памяти по порядку мы можем сдвинутся на 4 байта вправо и получить адрес переменной p->y
. Это производится с помощью приведения типа указателя на символ (байт), сдвижке его на 4 ячейки вправо. Теперь полученный адрес обязательно нужно привести к указателю на целую ячейку, и перейти по нему. Такой пример в отличии от предыдущего демонстрирует не сокращение объёма кода, а наоборот его увеличение, а что ещё хуже - увеличению количества операций которое нужно выполнить. Из этих примеров вывод такой - каламбур типизации в реальных программах это плохо потому, что
Размер ячеек на каждой платформе может отличаться
Разные компиляторы могут по разному хранить переменные в памяти
Оба этих фактора могут обеспечить неправильное или неопределённое поведение программы.
Отрицательные индексы
Изюминка этого чарта. arr[-1]
- вполне себе привычная запись для программиста на Pascal. Всё потому что в этом языке при создании массива указывается не только правая, но ещё и левая граница массива:
var arr : array[-100..100] of integer;
Но С - язык другой. Каждый программист наизусть знает что индексы массивов здесь начинаются с нуля как и в любых других С-подобных языках. Тем не менее не забывайте, что оператор []
применим не только к массивам но и у указателям.
int real_array[100], *arr = real_array+3;
arr[-1] = 3;
arr[-2] = 65;
arr[-3] = 3278;
printf("arr[-1] = %d, arr[-2] = %d, arr[-3] = %d\n", arr[-1], arr[-2], arr[-3]);
Если вы всё равно не поняли как это работает, то вот то как компилятор представил запись arr[-1]
:
*(arr + (-1)) = 3;
Или же эквивалент ей:
*(real_arr + 2) = 3;
Такого вы точно скорее всего не видели в уже готовых работающих проектах на языке С.
Выводы
Язык С намного больше, обширнее и глубже чем кажется - необходимо как минимум несколько лет практики и опыта чтобы знать большинство возможностей этого инструмента. Хоть язык и имеет множество конструкций которые редко применяются, всё же они как никакие другие нужны в некоторых задачах. Я постарался дать примеры использования практически всех конструкций из этого списка. Надеюсь вы узнали что-то новое для себя из этой статьи, или хотя бы вспомнили хорошо забытое старое. Всем желаю чтобы ваши индексы находились в границах массивов, а malloc()
никогда не возвращал NULL
.
Комментарии (150)
0xd34df00d
21.02.2022 10:11+1Прочитал не так, del
lz961
21.02.2022 10:23int brr[100] = {0};
int *arr = brr+50;
arr[-1] = 3;
Есть ли здесь "undefined behaviour" ?
Gordon01
21.02.2022 10:33-1По стандарту, arr указывает на 51й элемент, то есть валиден всего для четырех байтов.
Код содержит UB, потому что эксплуатируется особенность компилятора не проверять границы адресов для указателей. arr[-1] указывает на последний байт 50го элемента, чего быть не может.
lz961
21.02.2022 10:42int brr[100] = {0};
int *arr = brr+50;
arr[-1] = 3;
printf("%d, %d, %d\n", arr[-1], brr[49], arr[0]);
printf("%p, %p, %p\n", &(arr[-1]), &(brr[49]), &(arr[0]));
return 0;вывод
3, 3, 0
0x7ffd3b2f0eb4, 0x7ffd3b2f0eb4, 0x7ffd3b2f0eb8вроде, соответствует написанному в статье
Gordon01
21.02.2022 13:01Код на си иногда выводит правильный результат как доказательство что он корректен? Вы серьезно?
https://godbolt.org/z/6s3a15ee1
#include <stdio.h> int main(void) { int a1[10] = {0}; int a2[10] = {1}; int *arr = a2; arr[-3] = 3; printf("%d, %d\n", arr[-3], a1[9]); printf("%p, %p\n", &(arr[-3]), &(a1[9])); return 0; }
Как вышло?
Program returned: 3, 0 0x7ffe46167f24, 0x7ffe46167f24
Указатель в Си — это не адрес памяти. Это просто абстракция.
DistortNeo
21.02.2022 16:28+1Как вышло?
Вы, используя указатель на
a2
, поменяли значение в массивеa1
. Естественно, это будет UB.
Bunikido Автор
21.02.2022 18:11+1Указатель в Си — это не адрес памяти. Это просто абстракция.
Брайан Керниган писал что это именно адрес в памяти.
Gordon01
21.02.2022 20:40+2Брайан Керниган писал что это именно адрес в памяти.
Спасибо деду за победу, конечно, но мог бы еще тогда догадаться что:
int arr[5] = { 0, 1, 2, 3, 4 }; i0 = arr[0]; assert(i0 == 0); i1 = arr[-2]; assert(i1 == 3); assert(&arr[-2] == &arr[3]);
Много бы крови сэкономил
Bunikido Автор
21.02.2022 20:57Assertion failed!
Program: C:\Users\Random_User\ \a.exe
File: tmp.c, Line 10Expression: i1 == 3
Дальше программа естественно не выполняется.
А что не так? Вы выполнили переход к области памяти, в которой никто не знает что лежит: arr[-2]. Конечно там не может быть 3.
Gordon01
21.02.2022 21:17-3Ну так да, диды не догадались сделать отрицательные индексы — индексацией с конца. Как это сейчас во многих языках, даже в том же пхп.
Тогда бы они еще тогда поняли, что вместе с массивами и указателями на них, надо таскать их размер (хотя бы во время компиляции).
Я имел в виду, что arr[-2] и arr[3] в случае массива из 5 элементов указывали бы на одинаковый элемент.
Скажете перф? Только вот из-за этого в си невозможны аггрессивные оптимизации, когда сразу инвестно что указатели не пересекаются. Всякие расты оказываются быстрее си, как раз благодаря тому что гарантируют что уникальные ссылки уникальны (не пересекаются).
А современные компиляторы все равно надеятся на strict aliasing даже в дефолтных настройках (-О3), что я вам и показал.
Bunikido Автор
21.02.2022 21:33+2Я имел в виду, что arr[-2] и arr[3] в случае массива из 5 элементов указывали бы на одинаковый элемент.
До сих пор не понимаю как так-то
Вот массив:
{0 1 2 3 4}
Вот i1 = arr[-2]:
x x {0 1 2 3 4} ^ i1
То есть мы взяли какую-то другую неизвестную нам память. Так каким образом тогда мы получим arr[3]?
Gordon01
21.02.2022 21:54-2Я не про историческую реализацию си, а про то как надо было делать (легко говорить из будушего).
https://www.php.net/manual/ru/function.substr.php
Положительный индекс — считать с начала, отрицательный — считать с конца.
Bunikido Автор
21.02.2022 22:03Положительный индекс — считать с начала, отрицательный — считать с конца.
Полный бред =) Тогда смысл этих "отрицательных" индексов если можно написать просто:
extern int arr[]; extern size_t i; ... = arr[ARR_SIZE-i];
Ох уж эти ленивые пхпшники... Мы Сишники, с нами стандарт!
Gordon01
21.02.2022 22:07extern size_t i;
ptrdiff_t
... = arr[ARR_SIZE-i];
error: ARR_SIZE undefined
Мы Сишники, с нами стандарт!
Крииинж %)
https://gcc.gnu.org/bugzilla/show_bug.cgi?id=30475
Почему сишникам меньше всего платят тогда?
Bunikido Автор
21.02.2022 22:29+1ptrdiff_t
Заглядываем в стандартную библиотеку и видим:
typedef long long ptrdiff_t; typedef long long ssize_t; typedef unsigned long long size_t;
Чем вас
ssize_t
тогда не устраивает? Да и вообще какая разница тем более в таком простом примере который я привёл =)error: ARR_SIZE undefined
Ладно, теперь сойдёт?
#include <stdlib.h> #include <stdio.h> #define ARR_SIZE 5 int main(int argc, char *argv[]){ int arr[ARR_SIZE] = {0, 1, 2, 3, 4}; long long i = -2; printf("%i\n", arr[(i < 0) ? ARR_SIZE-(-i) : i]); return EXIT_SUCCESS; }
Крииинж %)
Ох уж эти ваши
тиктоки
Gordon01
21.02.2022 22:39+1Вы почти выполнили задание "выведите n-ый аргумент командной строки".
Осталось сделать обработку ошибок и можно будет:
> leetcode_easy.exe 2 a b c d c > leetcode_easy.exe -1 a b c d d > leetcode_easy.exe 22 a b c d index out of bounds
Bunikido Автор
21.02.2022 22:31Почему сишникам меньше всего платят тогда?
Откуда такая информация? C занимает 2 место по популярности по рейтингу TIOBE
Gordon01
21.02.2022 22:36+1Откуда такая информация? C занимает 2 место по популярности по рейтингу TIOBE
С рынка труда)
Bunikido Автор
21.02.2022 22:37Я пишу на С потому что удобно - а не потому что много платят :) Это тот самый язык «что написано то и сделано»
Balling
22.02.2022 01:17Именно поэтому unsigned арифметика быстрее signed. Это классная фича, а не баг. stackoverflow.com/a/4712784/11173412
>Undefined signed integer overflow allows the compiler to assume that overflows don't happen, which may introduce optimization opportunities.
Одна из важнейших фич в Си, как по мне. В ffmpeg постоянно int меняют на unsigned (unsigned это синоним unsigned int).
Например, github.com/FFmpeg/FFmpeg/commit/bf33a384995ac21aa41422c6246ebdc5d9632452
ИЛИ вот этот ужас github.com/FFmpeg/FFmpeg/commit/203b0e3561dea1ec459be226d805abe73e7535e5
tabtre
22.02.2022 11:53Undefined signed integer overflow allows the compiler to assume that overflows don't happen, which may introduce optimization opportunities.
Немного странный коментарий на SO. Исходя из него можно подумать что как раз signed integer быстрее раз для него оптимизации возможны)
В том ответе есть ссылка на интересную статью о Signed Overflow www.airs.com/blog/archives/120
Там есть пример с оптимизацией ф-цииint f(int x) { return 0x7ffffff0 < x && x + 32 < 0x7fffffff; }
до return 0
Однако из описания непонятно почему так происходит. У меня есть предположение, что компилятор считаетx + 32
дляx > 0x7ffffff0
— всегда будет выходить за 0x7fffffff. Это так?
Balling
22.02.2022 16:00Имеются в виду оптимизации от противного. Signed overflow надо как-то обязательно проверять (так как есть консенсус, как обрабатывать это неопределенное поведение), даже если это выделенная инструкция ассемблера с флагами overflow.
wataru
22.02.2022 00:41+2Ну так да, диды не догадались сделать отрицательные индексы
И не зря. В каком-нибудь высокоуровневом питоне это можно сделать почти бескровно, там и так все еле ворочается. Ну подумаешь, еще и длину массива хранить. А в довольно низкоуровневом Си так делать нельзя. Если у вас массив статический, то еще худо бедно компилятор может помнить какой же размер этого массива. А вот если это указатель, переданный в функцию. Или, прости господи, нуль-терменированная строка? Таскать везде еще и длину массива — лишние накладные расходы. Если кому надо с конца считать, то пусть сами где-то длину массива протащат и в квадратных скобочках перед минусом вставят.
Gordon01
22.02.2022 09:58А в довольно низкоуровневом Си так делать нельзя.
Но приходится, так как сразу же оказалось, что можно сделать оптимизации, когда регионы памяти не пересекаются, но си этого гарантировать не может, пришлось подпирать костылями вроде restrict, генерировать два варианта кода и прочий бред
wataru
22.02.2022 13:06+1Ну да. Если программист хочет использовать эту фичу, он пишет restrict. Также как с выходом за границы массива. Программист сам проверяет, а не компилятор не спрашивая пихает везде проверки.
Gordon01
22.02.2022 17:54Если программист хочет использовать эту фичу, он пишет restrict.
Чтобы ее юзать, о ней надо знать, а даже если знаешь, то надо доказывать что регионы и правда не пересекаются.
Как показывает практика — это интересно целым 0 разработчикам. (поискал сейчас в кодовой базе в одной очень крупной российской софтовой компании, нашел одно упоминание restrict в библиотечном коде).
0xd34df00d
23.02.2022 05:40+1Программист сам проверяет
Очень интересно! То есть, каждый программист должен сам реализовать функцию вроде
bool doesOverlap(int *arr1, int size1, int *arr2, int size2) { ... }
?
Как бы вы её реализовали, кстати?
0xd34df00d
23.02.2022 23:29+1Раз никто не ответил, то напишу, что просто вычесть указатели и сравнить их с размерами нельзя, а тем временем я отчего-то уверен, что 95% программистов сделали бы именно это.
Эх, такая дискуссия об UB пропала…
0xd34df00d
23.02.2022 05:36+1Если у вас массив статический, то еще худо бедно компилятор может помнить какой же размер этого массива. А вот если это указатель, переданный в функцию. Или, прости господи, нуль-терменированная строка? Таскать везде еще и длину массива — лишние накладные расходы.
Есть три основных случая:
- Массив статический и с константным размером. Тогда компилятор всё знает.
- VLA. Я считаю, что это плохой тон, но
компиляторрантайм про размер тоже знает, таскать длину ему всё равно надо (чтобы правильно подёргать указатель на вершину стека, например), и мы ничего не теряем. - Массив выделен на хипе условным маллоком. Тогда рантайму снова должен быть известен размер, чтобы потом корректно освободить память.
Где здесь оверхед?
Если кому надо с конца считать, то пусть сами где-то длину массива протащат и в квадратных скобочках перед минусом вставят.
Как раз необходимость протаскивать самому размер массива — оверхед, потому что эта информация у компилятора или рантайма уже есть, а мне приходится её ещё таскать, засорять регистры, и так далее. Это же C, а не язык с более продвинутой системой типов, я же не могу сказать, что вот этот параметр функции должен быть всегда размером вон того массива, чтобы компилятор осилил это соптимизировать.
wataru
23.02.2022 13:24Про статический массив и VLA — да, но только в текущей функции. Уже передать в другую функцию — надо не только указатель передавать, но и размер. Теоретически, компилятор мог бы это как-то сам сделать, но это не всегда надо. Вот и оверхед.
Что касается маллока, то размер есть у системы. Вы уверены, что рантайм Си хранит этот размер? Зачем ему копию у себя поддерживать? Она не всегда нужна. А есть этот размер у системы, потому что нельзя доверять пользователю системного апи тащить его от malloc'а до free из соображений безопасности. Потому что можно передать в free больше, чем вы запросили и потом утащить память другого процесса.
Как раз необходимость протаскивать самому размер массива — оверхед
Нет. Вы или протаскиваете его сами, если оно вам надо, или он не нужен и он нигде не протаскивается (как, например, с нуль-терминированными строками). Альтернатива, когда рантайм его помнит и как-то везде протаскивает, даже если оно не используется — это и есть ненужная работа, или как я ее назвал — оверхед.
Ну вообще, теоретически, компилятор мог бы сам определять, а используется размер или нет и как оптимизацию не передавать туда, где не надо. Но во времена дизайна Си это было наверно не реализуемо. Да и философия не в том, что оптимизатор потом все лишнее уберет, а не вводить ничего лишнего, если не надо.
DerRotBaron
23.02.2022 23:04Что касается маллока, то размер есть у системы. Вы уверены, что рантайм Си хранит этот размер?
Да, размер есть именно у аллокатора, который является частью рантайма. На каждую аллокацию маппить страницу памяти в здравом уме никто не будет. И ровно поэтому же система ничего не будет знать.
Ну вообще, теоретически, компилятор мог бы сам определять, а используется размер или нет и как оптимизацию не передавать туда, где не надо. Но во времена дизайна Си это было наверно не реализуемо. Да и философия не в том, что оптимизатор потом все лишнее уберет, а не вводить ничего лишнего, если не надо.
И именно это в современных реалиях осложняет как написание на Си безопасного и эффективного кода, так и его хорошую оптимизацию.
А что до "философии", то уже много лет оптимизирующие компиляторы с ней упорно борются, удаляя или эквивалентно заменяя то самое "лишнее". И судя по бенчмаркам и то и дело всплывающим историям с "оптимизациями UB", делают это они довольно успешно
0xd34df00d
23.02.2022 23:28+1Про статический массив и VLA — да, но только в текущей функции. Уже передать в другую функцию — надо не только указатель передавать, но и размер. Теоретически, компилятор мог бы это как-то сам сделать, но это не всегда надо.
Какой рантайм-оверхед для статического массива?
Ну, то есть, да, в С как он написан это действительно невозможно, потому что типы не позволяют. Но передача статически известной информации в вызываемую функцию не приводит ни к какому рантайм-оверхеду. Условно, представьте себе, что вы добавили темплейты, и вместо функции
void foo(int *arr);
пишете
template<size_t N> void foo(int [&arr][N]);
Что касается маллока, то размер есть у системы. Вы уверены, что рантайм Си хранит этот размер?
Тут нельзя рассуждать об одном конкретном рантайме, и есть варианты. Например, с одной стороны спектра ваш десктоп с glibc (или чем-то аналогичным) — glibc запрашивает у ядра память одним большим куском, и оттуда откусывает потихоньку по мере каждого
malloc
куски поменьше. С другой стороны что-нибудь freestanding, но тогда рантайм C и есть система.Альтернатива, когда рантайм его помнит и как-то везде протаскивает, даже если оно не используется — это и есть ненужная работа, или как я ее назвал — оверхед.
Далеко не везде рантайм должен об этом помнить.
Gordon01
23.02.2022 22:18Эх, я к этому и подводил, но что-то надоело болтать со студентами. Последнее время много от них статей стало, действительно что ли зачеты ставят за это...
lz961
21.02.2022 19:07https://godbolt.org/z/ah46avxb6
поломалось, в выводе указатели отличаются, хотя код не изменен. Т.е. у вас настоящее UB
3, 0 0x7ffe1dae5124, 0x7ffe1dae5184
Gordon01
21.02.2022 20:33Мне дальше говорить не интересно, сорян.
Т.е. у вас настоящее UB
И оно наглядно показывает что "Указатель в Си — это не адрес памяти. Это просто абстракция."
lz961
21.02.2022 21:52+2Вот гляжу я на ваш ассемблированный пример (строки 22-24, соответствуют строке 6 исходной программы на языке С, оптимизация отключена), и вижу вполне конкретное действие (с точностью до некоторых тонкостей): в регистр rax кладётся значение указателя arr. Из этого значения вычитается 12 (=sizeof(int)*3). По адресу, хранящемуся в регистре rax кладётся тройка. Как видим, все адреса вполне конкретные.
mov rax, QWORD PTR [rbp-8] ; rax = arr
sub rax, 12 ; rax = arr - 3
mov DWORD PTR [rax], 3; *rax = 3
DistortNeo
21.02.2022 16:29+2arr[-1] указывает на последний байт 50го элемента
Абсолютно неверно. У вас тип
arr
— этоint *
, он будет указывать на 50-й элемент.
Никакого UB в данном случае не будет.
Myxach
21.02.2022 10:12+14Указатели на функции
Серьезно? Это средство для реализации калбека. С чего это она не полезна?
Bunikido Автор
21.02.2022 14:03-2Я не говорил что она не полезна. Каждому инструменту найдётся своё применение, статья о редких а не бесполезных инструментах. Конечно я не отрицаю того факта, что указатели на функции редко встречаются только на моей практике, может вы работайте в той сфере где без них никуда.
DrGluck07
21.02.2022 15:06+1Например всё API X-Plane сделано на коллбеках. Да и на железе это очень часто используемая фича.
aamonster
21.02.2022 10:15+8бОльшая часть – реально используемая.
А в примере с преобразованием указателя – вообще ошибка: в числе 0x2E6D6172676F7270 все 8 байт ненулевые, значит, после 8 символов начнёт печатать то, что дальше в памяти.eurol
21.02.2022 13:01Не говоря уже о том, что приведенные двоичные числа вовсе непонятно, к чему относятся…
Bunikido Автор
21.02.2022 19:30Двоичные числа в разделе с каламбуром типов относятся к примеру со строкой - это то самое длинное число только в двоичном виде, чтобы как бы показать что в памяти всё хранится одинаково.
eurol
21.02.2022 20:01Даже близко не вижу ничего похожего. Мало того, что в двоичном виде нулевой байт есть, а в исходном числе его нет, так там только одно четное число, которое в 16-ричном виде будет выглядеть как E8, а остальные числа все нечетные, в то время как в исходном числе четных несколько есть...
Bunikido Автор
21.02.2022 19:35бОльшая часть – реально используемая.
Не спорю - если вы видели много проектов с такими конструкциями, то значит вы решайте те специфичные проблемы, которые трудно было бы решить без этих инструментов. Просто здесь приведены именно они потому, что они редко используются в средне-статистических программах на С. И учитывайте:
Сначала расскажу о базовых и часто встречаемых конструкциях, которые могут и вовсе не удивить опытных программистов на С, но вот уже ниже идут всё более редкие и интересные конструкции, которые уже далеко не часто можно где-либо встретить.
Если вы опытный программист на С то могу предположить что бОльшая часть находится именно вверху
А в примере с преобразованием указателя – вообще ошибка: в числе 0x2E6D6172676F7270 все 8 байт ненулевые, значит, после 8 символов начнёт печатать то, что дальше в памяти.
Поправил формат
unsignedchar
21.02.2022 19:40+2редко используются в средне-статистических программах на С
Знать бы, что это такое, среднестатистическая программа на С ;)
Xeldos
21.02.2022 10:52+16Временный файл, двойные указатели, указатели на функции, константные указатели - это для вас странные технологии? Разбиение строки на токены - странная технология? Вы точно пишете на C? Вы точно программируете?
Bunikido Автор
21.02.2022 19:28-2Константные указатели находятся вверху, поэтому есть шанс что они не так уж и редко используются. Применение временному файлу я практически не нашёл, да и не особо видел. Смело присылайте свои идеи и применения, может добавлю их. Как упоминал @aamonster strtok действительно странная и нестабильная вещь. Лично я в большинстве случаев пишу свои функции для разбиения на токены.
Вы точно пишете на C? Вы точно программируете?
Тише, если бы я не писал на С и не программировал то вы бы эту статью не увидели )
Devoter
23.02.2022 01:28strtok, действительно,имеет ограниченный спектр применения, так как требует крайне аккуратного применения, что для однопоточных программ при грамотном проектировании (большинство из coreutils), впрочем - не проблема (с strtok_s по производительности не сравнивал). tmpfile, действительно, с ходу не вспомнить, где встречалось, но идея-то понятна: можно подсунуть библиотекам, которые умеют работать только с файлами такой вот "файл". С натяжкой можно сравнить это с подходом интерфейсов Reader и Writer в Go.
RomanArzumanyan
21.02.2022 10:54+5Указатели на функции
И всё, что вообще с ними связано. Я думаю они не сильно полезны, максимум как аргумент для другой функции
В крупных проектах на С используются для реализации полиморфизма.
Например, в ядре Linux и 100500 мультимедиа библиотеках.
shaykemelov
21.02.2022 11:53+1что-то мне подсказывает, что это не такие уж и редко используемые возможности языка. про них я узнал всего лишь из одной книги https://www.ozon.ru/product/yazyk-programmirovaniya-c-lektsii-i-uprazhneniya-147927650/?sh=Lrd_YwAAAA, которая рассчитана на начинающих.
rcl
21.02.2022 12:40+3Все эти фичи абсолютно необходимы для программирования "железяк". И константные указатели, и volatile, и все все все. А указатели на функции позволяют избежать switch-ей и других ненужных переборов.
Так что удивляться тут нечему, надо просто использовать С для написания быстрых и лаконичных программ.
Gordon01
21.02.2022 13:02+1Так что удивляться тут нечему, надо просто использовать С для написания быстрых и лаконичных программ.
Жаль, только что компиляторы си не умеют сами разбираться, какие диапазоны памяти пересекаются, а какие нет
rcl
21.02.2022 13:25Это как раз не жаль. Просто надо владеть, а не пользоваться. Я, например, терпеть не могу когда компиляторы начинают делать предположения о психологическом состоянти автора программы.
Bunikido Автор
21.02.2022 19:24делать предположения о психологическом состоянти автора программы.
Это уже какая-то новая фича. Ещё вспомнил прикол про то, что компилятор отказывается компилить неотформатированный код. Восстание машин не за горами =)
0xd34df00d
22.02.2022 00:17+1То есть, вы без оптимизаций собираете код? Тогда есть вопросы к быстроте.
А к лаконичности есть вопросы в любом случае.
Gryphon88
21.02.2022 12:53#undef широко используется при злоупотреблении макросами, в частности, при использовании x-macro, когда макрос с одним и тем же именем (традиционно Х, отсюда и название) разворачивается по-разному. Из безобидных примеров вспомню генерацию массива строк для enum, но вообще можно и полиморфизм реализовывать, и RAII, и много чего ещё. Дебажить, конечно, весело, но это всё равно лучше преобразования к void*.
unsignedchar
21.02.2022 13:48uint64_t x = 0x2E6D6172676F7270; printf ("%s\n", (char*)&x);
Аффтар жжот. Если в программе есть ещё переменные — printf может вывести что угодно кроме ожидаемой строки.Bunikido Автор
21.02.2022 14:12uint64_t x = 0x2E6D6172676F7270; printf ("%.8s\n", (char*)&x);
Поправил - поставил ограничение на вывод максимум 8 символов строки.
Gordon01
21.02.2022 14:19uint64_t x = 0x2E006172676F7270;
Напечатает не то, что ожидалось
Bunikido Автор
21.02.2022 19:20Правильно - у вас 7 байт другой. В оригинале он
6D
а у вас00
.unsignedchar
21.02.2022 20:17Дело в том что uint64_t* и char* - это сильно разные обьекты с сильно разными свойствами. У одного длина фиксированная, у другого может быть любая. Не нужно приводить их к друг другу. Просто не нужно. Даже в примере из 2 строк сделать более-менее
правильноожидаемо у вас получилось со 2 раза.viordash
21.02.2022 22:00Дело в том что uint64_t* и char*
вы пишете именно про uint64_t* (указатель на uint64_t)? как может быть фиксированная длина у одного указателя, а у другого нет?
Gordon01
22.02.2022 10:21Гениальное наблюдение, конечно, но вы попробуйте не просто думать (тут вам опыта не хватает), а запустить программу и посмотреть что именно она выведет.
uint64_t x = 0x2E006172676F7270; printf ("%.8s\n", (char*)&x);
Bunikido Автор
22.02.2022 10:37Вот моя версия:
#include <stdio.h> #include <inttypes.h> int main(void){ uint64_t x = 0x2E6D6172676F7270; printf ("%.8s\n", (char*)&x); }
Вывод:
program.
(правильный вывод)А вот ваша версия:
#include <stdio.h> #include <inttypes.h> int main(void){ uint64_t x = 0x2E006172676F7270; printf ("%.8s\n", (char*)&x); }
Вывод:
progra
(неправильный вывод)Потому что как только printf доходит до предпоследнего байта (который у вас превратился в 00) вывод других символов завершается. Из-за этого мы потеряли букву m и точку. Всё верно
Gordon01
22.02.2022 17:51+1Вывод:
program.
(правильный вывод)С чего это он правильный? Такой же UB'шный, как и любые другие варианты.
Осталось только запустить на машине с другим порядком байтов, другим положением старшего бита или запретом невыравненного доступа к памяти...
И главное — повторять мантру:
Я пишу на С потому что удобно - а не потому что много платят :) Это тот самый язык «что написано то и сделано»
Bunikido Автор
22.02.2022 21:05Это тот самый язык «что написано то и сделано»
А у вас есть что возразить насчёт этого? Написал допустим + или -> и сразу можешь предсказать что будет на выходе
Gordon01
22.02.2022 21:34А у вас есть что возразить насчёт этого?
Так вы не можете написать сниппет, который работал бы на чем-то кроме х86, но утверждаете что понимаете Си.
Написал допустим + или -> и сразу можешь предсказать что будет на выходе
Это не имеет никакого значения для бизнеса.
Bunikido Автор
22.02.2022 22:50Почему же не могу? Пишите что должен делать сниппет (только не гигантский проект) а я реализую его как кросс-платформенный
Это не имеет никакого значения для бизнеса
Я здесь не для заработка денег :)
Gordon01
22.02.2022 22:56Почему же не могу? Пишите что должен делать сниппет (только не гигантский проект) а я реализую его как кросс-платформенный
Вы себе сами выше придумали задачу: распечатать текст, который хранится в uint64. Ее хотя бы доделайте до конца.
Я здесь не для заработка денег :)
Бизнес-задача — это полезная работа, которую должен делать код. Мелочи одного единственного (причем одного из самых простых) языка для этого не играют никакого значения.
Даже если деньги не зарабатываются и это пет-проект.
Bunikido Автор
22.02.2022 23:39Вы себе сами выше придумали задачу: распечатать текст, который хранится в uint64. Ее хотя бы доделайте до конца.
А кросс-платформенное решение я уже опубликовал в статье, вот оно краткое и лаконичное:
extern uint64_t x; //сама строка if (!x) return; //если строка нулевая то завершаем работу функции do { putchar((char)(x & 0xFF)); //печатаем байт x >>= 8; //удаляем байт } while (x); //пока число не станет нулем
Это немного переделанная версия чтобы не возникло претензий к стилю написания. Я буду удивлён если такой код не заработает на какой-либо платформе имеющей хорошую реализацию С. И так этот код может НЕ заработать только по следующим причинам:
Платформа не поддерживает С
Платформа не имеет файла
inttypes.h
/stdint.h
uint64_t
не хранится в памяти на этой платформе по порядку в одной ячейке и его байты раскиданы по памяти как связный список.Платформа не имеет файла
stdio.h
Не знаю как вы но платформы с выше перечисленным списком я не знаю и поэтому не могу полностью отрицать факта её существования. Можно ли такой код назвать кросс-платформенным?
oldnomad
23.02.2022 00:41Платформа не имеет 64-битного беззнакового типа без набивочных бит (и, соответственно, не определяет
uint64_t
).
Пункт 2 невозможен, наличия заголовка
<stdint.h>
требует стандарт (по крайней мере, для современного C). Пункт 3 также невозможен (C17 draft, 6.2.6.2). Пункт 4 вполне реален (freestanding implementation, C17 draft, 4 (6)), я писал для таких платформ.Вдогонку,
char
может быть больше 8 бит (см. определение константыCHAR_BIT
). Для такого я не писал, но знаю о существовании платформ сCHAR_BIT==32
.Полностью переносимый код -- это сложно. Порой, непереносимо сложно.
Devoter
23.02.2022 01:38А почему вы, собственно, решили, что порядок байт будет именно LE, а не BE? Стандарт это никак не регламентирует и ваш >> окажет совсем не тот эффект, что вы ожидаете.
Devoter
23.02.2022 02:37+1Вынужден покаяться, для unsigned типов сдвиг определен арифметически, то есть, как я понимаю, проблем в данном случае быть не должно.
viordash
23.02.2022 13:30А кросс-платформенное решение я уже опубликовал в статье, вот оно краткое и лаконичное:
ваш вариант с
printf ("%.8s\n", (char*)&x);
тоже такой же кросс платформенный, но имхо более понятный, так как не потребует рефакторинга.
Единственная проблема в обоих решениях это BE/LE, а это можно обойти директивой препроцессора, каким-нибудь #ifdef
__BYTE_ORDER__
, или compile-time проверкой через определение положения не нулевого байта в переменной int32 == 1.
Gordon01
23.02.2022 22:14Там, где запрещен невыравненный доступ к памяти этот код может упасть.
Также, этот код не соберется каким-нибудь IAR c включенными MISRA. Ну и главное - этот код не пройдет сертификацию у более-менее серьезных людей.
viordash
23.02.2022 22:48Какую из переменных, в упоминаемом мной коде вы считаете не выровненым?
Код не упадет, так как переменная uint64_t x выровнена естественным образом, и в printf передается адрес на эту переменную. А как внутри printf уже будут обрабатываться эти байты не очень важно.
уверен что и иар с MISRA норм соберет, но наверно для самой printf возникнет нюанс, какое-то из правил касалось переменного кол-ва аргументов
unsignedchar
22.02.2022 22:22Написал допустим + или -> и сразу можешь предсказать что будет на выходе
undefined behavior in c. 100500 примеров.Bunikido Автор
22.02.2022 22:52Опытные программисты на С умеют избегать UB жертвуя объёмом кода, и я стремлюсь следовать этому совету. Мы же тут не за краткостью кода собрались, а если да то тогда Python будет хорошим вариантом
staticmain
21.02.2022 14:13+11>но вот уже ниже идут всё более редкие и интересные конструкции, которые уже далеко не часто можно где-либо встретить
>union
Одна из базовых конструкций языка. Если это программа на Си — то она достаточно близка к железу, а там union зачастую как спасительный круг и для конвертации и для уменьшения объема хранения данных. Используется постоянно.>register
Немного устаревший, потому что сейчас чаще полагаются на оптимизации компилятора.>Статические функции
? А где тут редкость или ненужность? Статические функции делаются для того, чтобы не светить ими из модуля при компиляции. У нас, например, PR апрув не пройдет, если функции, которые не нужны за пределами модуля не будут static (мы еще используем префикс _ чтобы детектить такие «утечки»).>Константный указатель
Серьезно?>Препроцессорная склейка строк
Используется в больших макросах, которые изредка, но нужны например для кодогенерации или большого набора действий для сверхкритичных мест.>Конкатенация строк во время компиляции
Да это же вообще базовая фича языка, ну>#undef
Да что за бред? Откройте любой std-файл, там таких undef'ов выше головы. В пользовательском коде тоже иногда приходится делать, когда дело доходит до компиляции под Win.>sizeof без ()
Это зависит от того как кто привык, тут криминала нет, sizeof не функция.>Создание функции в стиле K&R
Так никто не пишет уже лет 15 как.>Указатели на функции
>Я думаю они не сильно полезны
Чего? Статью писал человек который в первый раз учебник открыл? Да любые таблицы коллбеков, обработчиков роутов, подвязки на сигналы, события — это всё указатели на функции и массивы.>Указатели на указатели
>Но что-то я сомневаюсь что в таком случае нельзя обойтись макросом, или вовсе сделать все эти операции на месте вызова.
Автор несет дичь. Массивы в школе видимо не проходил.>Функции с переменным количеством параметров
>Как часто вы видите подобное?
Всегда, когда речь идет о форматировании аргументов или передаче неизвестного количества параметров.>Массив + индекс (два в одном)
Это не запрещено, но так никто не делает, просто «баг» подкапотного обращения к массиву как a[i] = *(a + i) = i[a].>Индекс с массивом, но вверх ногами
Так никто не делает.>strtok(), tmpfile()
Автор никогда не писал парсеров, поэтому видимо считает что можно парcер написать на sscanf. Для безопасности есть strtok_r. Strtok небезопасна не из-за того что редактирует строку а из-за отсутствия внешнего стейта.>Возврат указателя на функцию из функции
См. выше. Любая таблица коллбеков.>volatile
Да откройте вы уже хоть один проект, связанный с железом! Без volatile любое обращение к железным регистрам будет выкинуто:typedef struct __attribute__((packed)) { volatile u64 ctrl; volatile u16 in; volatile u16 pad1; volatile u16 out; volatile u16 pad2; volatile u16 bset; volatile u16 breset; volatile u16 breset2; volatile u16 pad3; volatile u16 lock; volatile u16 pad4;
>Макросы с переменным количеством параметров
Редко, но встречаются.>auto
Автор путает C++ auto и C auto. В С auto используется для указания расположения переменной (register, static, extern, auto)>Использование возвращаемых значений scanf() и printf()
Автор на С не писал, да? printf редко проверяется, scanf нужно проверять ВСЕГДА. Иначе есть шанс что вы будете читать мусор.>Каламбур типизации
А каламбур где? Автор не парсил входной поток байт от устройств?>Отрицательные индексы
Это плохо, не делайте так. Очень редко такое нужно когда обрабатываются элементы массивов в цикле.Bunikido Автор
21.02.2022 14:59-2Спасибо за комментарий!
Если это программа на Си — то она достаточно близка к железу
Я тут рассказывал преимущественно про ту сферу которой я занимаюсь - разработка на С под Windows/Linux. Правда иногда чисто как любитель могу и микроконтроллеры программировать, но не так часто.
? А где тут редкость или ненужность?
В таких простых тестовых проектах на С (учебных или уже готовых но состоящих из одного файла) такой инструмент не нужен. Если вы работайте в компании которая пишет большие программы с большим кол-вом файлов и системами сборки - моё уважение.
>Константный указатель
Серьёзно?
Вполне. Лично я в большинстве случаев знаю и так что это read-only указатель и не пишу лишнего. Но когда хочу быть максимально уверен, то всё же делаю его const. Ну а что - функция большая и всё может быть.
Да что за бред? Откройте любой std-файл, там таких undef'ов выше головы. В пользовательском коде тоже иногда приходится делать, когда дело доходит до компиляции под Win.
Конечно их будет много, это же библиотека как никак. Там надо всё учесть для каждой платформы. Я же имел ввиду проекты на С пользовательские а не заголовочные файлы и библиотеки. (С компиляцией вы правы - иногда без них никуда), но опять таки они используются не так часто. Поэтому и попали в список.
Чего? Статью писал человек который в первый раз учебник открыл? Да любые таблицы коллбеков, обработчиков роутов, подвязки на сигналы, события — это всё указатели на функции и массивы.
Возьму на заметку, не имел дела с такими проектами.
Автор несет дичь. Массивы в школе видимо не проходил.
Я вот тут не совсем понял. Я написал:
void free_and_setto_NULL (void** ptr){free(*ptr);*ptr = NULL;}
Но что-то я сомневаюсь что в таком случае нельзя обойтись макросом, или вовсе сделать все эти операции на месте вызова.
А при чём тут free(), указатель на указатель и массивы? Я же выше писал в комментариях к коду что это может быть двумерный массив:
int **p; /* это всё ещё переменная, а может быть двумерный массив. А может надо изменить указатель через его адрес */
Всё я прекрасно проходил )
Это не запрещено, но так никто не делает, ...
Так никто не делает.
Конечно - этот список является сборником редких конструкций. А эти - настолько редкие что так никто и не пишет, потому что незачем. Но вот знать о таких моментах надо, может на собеседовании спросят...
Автор никогда не писал парсеров, поэтому видимо считает что можно парcер написать на sscanf. Для безопасности есть strtok_r. Strtok небезопасна не из-за того что редактирует строку а из-за отсутствия внешнего стейта.
Я не считаю что парсер можно написать на sscanf. Вот:
Хотя для парсинга простых шаблонных строк больше подходит
sscanf()
Я написал для парсинга простых шаблонных строк. Я писал что-то про обычные парсеры? Как-раз интересная тема и хочу в ней разобраться. Скоро планирую создать парсер PDF-файлов, обязательно напишу пост про это - поделюсь опытом. То что
strtok()
редактирует строку это тоже небезопасно - об этом необходимо знать хотя-бы для того чтобы не обнаружить "сюрпризов" в строке после того как скормили еёstrtok()
.Да откройте вы уже хоть один проект, связанный с железом!
Да я писал под Attiny13, Attiny45, Arduino Nano/Uno но не разу не использовал volatile. Не отрицаю факта что он используется в сложных комплексных проектах.
Редко, но встречаются.
В этом и есть фишка
Автор путает C++ auto и C auto. В С auto используется для указания расположения переменной (register, static, extern, auto)
Да, мне уже писали об этом. Спасибо что упомянули
Автор на С не писал, да? printf редко проверяется, scanf нужно проверять ВСЕГДА. Иначе есть шанс что вы будете читать мусор.
Да что-ж так жёстко ) Конечно писал, если бы не писал то и статью бы вы не увидели. Про проверку scanf() я не слышал, но спасибо за хорошую практику
А каламбур где? Автор не парсил входной поток байт от устройств?
Мы же используем научные понятия. Каламбур типизации (англ. type punning) — термин, который используется в информатике для обозначения различных техник нарушения или обмана системы типов некоторого языка программирования, имеющих эффект, который было бы затруднительно или невозможно обеспечить в рамках формального языка. Вот я здесь и обманул систему типов.
Это плохо, не делайте так
Я то не делаю - просто рассказываю
staticmain
21.02.2022 15:10+1Да я писал под Attiny13, Attiny45, Arduino Nano/Uno но не разу не использовал volatile.
Да любой регистр там всегда volatile. Вы не «не использовали», вы «не смотрели что используете».
www.arduino.cc/reference/en/language/variables/variable-scope-qualifiers/volatileЯ вот тут не совсем понял.
Одинарный указатель при передаче используется либо чтобы по стеку не гонять структуры либо чтобы изменить значение переменной внутри функции. Если у вас исходная переменная уже является указателем, то вам нужен еще один чтобы её поменять.mmr_error_t mmr_json_create(mmr_query_t query, struct frs_json **json) Вызов: struct frs_json * output; mmr_json_create(query, &output);
Bunikido Автор
21.02.2022 15:25Если у вас исходная переменная уже является указателем, то вам нужен еще один чтобы её поменять.
Я это знаю. Почитайте мои комментарии, я об этом писал:
int **p; /* это всё ещё переменная, а может быть двумерный массив. А может надо изменить указатель через его адрес */
Polaris99
21.02.2022 17:04+4Вы не «не использовали», вы «не смотрели что используете».
А что, в ардуино по-другому бывает? Есть, конечно, люди, которые все эти библиотеки пишут, но 99% "разработки" под ардуино сводится к поиску подходящей библиотеки и попытке запустить ее из коробки. Не заработала сразу - плохая, негодная библиотека, нужна другая. Заработала - да я крутой эмбеддед-разработчик, пёс:
Bunikido Автор
22.02.2022 10:28-1А что, в ардуино по-другому бывает?
Ардуино создавалась как лайтовая платформа для практики электроники (как конструктор). Библиотеки на любой вкус и цвет. Там конечно можно и на ассемблере писать, но зачем если есть прекрасный средне-уровневый С который в каждой микроволновке работает. Если интересно как работают железяки и процессоры на низком уровне и хочется разбираться то как по мне лучше сразу брать какой-нибудь MOS 6502 и писать на нём на чистом ассемблере. Правда у меня его пока нет =)
Polaris99
22.02.2022 11:54+3Там вообще-то не голый C, а плюсы, причем спрятанные под капотом так, что любая ошибка компиляции обеспечит долгий и насыщенный событиями поиск причины. А уж про саму оболочку вообще ничего нематерного сказать не смогу, это вообще какой-то конец 90х в плане комфорта и эффективности разработки. И учить с ассемблера я бы никому не рекомендовал начинать, есть предел того, куда стоит лезть с отсутствием понимания, на данный момент и Си работает отлично, многие производители даже стартапы на нем пишут.
Bunikido Автор
22.02.2022 12:06-1А уж про саму оболочку вообще ничего нематерного сказать не смогу, это вообще какой-то конец 90х в плане комфорта и эффективности разработки
Ну да среда и правда сомнительная но наоборот заточена под комфорт как бы это парадоксально не звучало. В Arduino IDE (если вы о нём говорите) как по мне удобное решение - простой редактор, две кнопки для прошивки и компиляции. А что именно вам пришлось не по вкусу?
И учить с ассемблера я бы никому не рекомендовал начинать, есть предел того, куда стоит лезть с отсутствием понимания, на данный момент и Си работает отлично
Вот это да но почему никому? Если человек работает и прошивает или пишет прошивки для железа он хочет чтобы программа работала настолько быстро насколько это возможно в теории. Я не спорю - Си это один из немногих языков где можно писать дичь -
while($>>=x^28+~10) f("89!"+op<<1);
, и на том же языке можно писать вполне себе читабельные высокоуровневые программы. А если захочется - с помощью инструментов языка можно вставить ассемблерный код напрямую в С - тогда компилятор его компилировать не будет. Вот это я понимаю комбо =)Си работает отлично
Несомненно. Правда иногда задумываешься - какую следующую коварную оптимизацию применит компилятор чтобы испортить твою программу?)
Gordon01
22.02.2022 17:58Если человек работает и прошивает или пишет прошивки для железа он хочет чтобы программа работала настолько быстро насколько это возможно в теории
Нет конечно, скорость даже самых дохлых современных микроконтроллеров достаточна. Людям важна корректность.
В >50% случаев в финальные устройства заливаются прошивки скомпилированные в дебаге, о чем речь вообще?
Devoter
23.02.2022 02:02+2Да я писал под Attiny13, Attiny45, Arduino Nano/Uno но не разу не использовал volatile. Не отрицаю факта что он используется в сложных комплексных проектах
Как-то писал совсем небольшой проект на ATTiny13. Так вот, volatile просто необходим при работе с железом. Смысл его в том, что данный модификатор запрещает компилятору оптимизировать строки с доступом к переменной, объявленной как volatile. Например:
char working = 1; void main(void) { while (working) { // do something } }
Имея такой код компилятор может запросто подставить 1 вместо обращения к переменной и дать на выходе бесконечный цикл. Но что, если мы хотим изменять значение в прерывании? Компилятор по-прежнему не знает, что эта переменная могла измениться, ведь она не изменяется нигде до чтения. Таким образом, без volatile изменение переменной в прерывании может быть проигнорировано, а с ним - не будет. Насчёт конкретно глобальных переменных уже точно не помню (давно дело было), но кто нам мешает глобально хранить лишь указатель на вполне локальную переменную? Надеюсь, смысл использования volatile теперь стал более прозрачен.
jakushev
21.02.2022 14:57+5volatile
Иногда компилятор может оптимизируя удалять целые куски кода. Чтобы такого не было желательно ставить ключевое слово volatile перед объявлением переменной, если вы хотите чтобы компилятор 100% не применил свои коварные оптимизации к этой переменной. (Хорошо что слово volatile не может игнорироваться, как иногда бывает с register, а то в этой жизни пришлось бы больше ни в чём не быть уверенным)
Ну сколько можно транслировать эту ошибку? volatile говорит о том, что переменная может измениться в другом месте (потоке / прерывании / аппаратном регистре).
wataru
21.02.2022 15:16+2Про указатели на указатели на указатели… Не могу не поделиться своей болью, хоть это и C++. Сигнатура MFEnumDeviceSources:
HRESULT MFEnumDeviceSources( [in] IMFAttributes *pAttributes, [out] IMFActivate ***pppSourceActivate, [out] UINT32 *pcSourceActivate )
Туда передается указатель на место, куда будет записан массив указателей на интерфейсы...
Вообще winapi полно подобных перлов.
Kelbon
21.02.2022 15:21+2не похоже на С++ (особенно in out)
Bunikido Автор
21.02.2022 18:33-1Интересная идея определить такие имена:
#define IN #define OUT
А потом при объявлении функции с указателями в качестве аргументов указывать что именно она с ними делает:
void foo(IN const uint8_t * const some_input, OUT uint8_t *some_output){...}
Kelbon
21.02.2022 18:37+2интересная идея это использовать возвращаемое значение в качестве возвращаемого как ни странно значения, а то что тут обозначено как in делать const или принимать по значению
DrGluck07
21.02.2022 15:27А какие глубины глубин скрыты внутри виндового API для драйверов. Структура из юнионов, которые являются структурами, ммм…
JerleShannara
21.02.2022 15:32+1Спасибо, у меня опять винддкшные флешбэки начались.
DrGluck07
21.02.2022 15:52Двадцать лет назад мне на работе досталась поддержка VXD-драйвера к железке под ISA-слот. Естественно на ассемблере. И это, кстати, довольно простая штука, по сравнению с WDM. Потом пришлось писать под WDF, но к сожалению не пригодилось. Зато было весело и я узнал что такое Syser Kernel Debugger (или как оно там называется, уже забыл).
DrGluck07
21.02.2022 15:19+4Пост выглядит как «хипстеры познают язык C». Это я не со зла, просто пост вызывает некоторое недоумение, это основы языка C.
Вот ещё одна полезная штука при инициализации массива. В некоторых ситуациях очень удобно, но почему-то не все об этом знают.int x[ 10 ] = { [ 0 ] = 42, [ 1 ] = 22, [ 2 ] = 73 /*и так далее, можно даже в любом порядке*/ }
Bunikido Автор
21.02.2022 15:36Как по мне просто удлиняет программу (или по крайней мере пример такой).
Пост выглядит как «хипстеры познают язык C».
А это неплохое название кстати. Я сам новичок в С - прямиком из Паскаля. Может поэтому для опытных программистов С с опытом >10 лет этот пост может показаться странным.
Вот ещё одна полезная штука при инициализации массива. В некоторых ситуациях очень удобно, но почему-то не все об этом знают.
Глядя на ваш пример сразу вспомнилась такая конструкция:
return (vec2){ .x = 10.0f, .y = 62.0f };
Не знаю правда, можно и вернуть структуру традиционным путём:
return (vec2){10.0f, 62.0f};
Но кажется с именами компонентов более наглядно. Но то, что такое и к массивам применимо - не знал. Спасибо за приём осталось придумать как его использовать =) Ну разве что когда хотите не по порядку элементы указывать:
int x[ 10 ] = { [ 1 ] = 21, [ 0 ] = 30 ...
Интересно а так вообще можно делать? Надо будет проверить. В случае со структурами знаю что 100% можно менять поля местами.
DrGluck07
21.02.2022 16:05Например у меня есть здоровенный enum, в котором перечислены типы самолётов. Мне нужно напечатать название в зависимости от типа. Получается вот такая конструкция:
static const char *aircraftNames[ AircraftTypesCount ] = { [ AircraftTypeUnknown ] = "Undefined" , [ AircraftTypeWindow ] = "Unknown aircraft, Window mode" , [ AircraftTypeB737Zibo ] = "Boeing 737NG family by Zibo" , [ AircraftTypeB737700Eadt ] = "Boeing 737NG family by EADT" , [ AircraftTypeB737Default ] = "Boeing 737-800 default" , [ AircraftTypeB747Default ] = "Boeing 747-400 default" // Ну и т.д.
Ну или большая таблица с параметрами для GPWS mode 4:static const Mode4Params mode4Params[ GpwstMode4TypeCount ] = { [ GpwsMode4Type1 ] = { .mode4A = { .gearDownSpeed = 190.0 , .maxAltitude = 1000.0 , .k = 8.333333 , .b = -1083.0 } , .mode4B = { .flapsDownAltitude = 245.0 , .flapsDownSpeed = 159.0 , .maxAltitude = 1000.0 , .k = 8.2967 , .b = -1074.1758 } } , [ GpwsMode4Type5 ] = { .mode4A = { .gearDownSpeed = 178.0 , .maxAltitude = 1000.0 , .k = 22.72727272 , .b = -3545.0 } , .mode4B = { .flapsDownAltitude = 200.0 , .flapsDownSpeed = 148.0 , .maxAltitude = 1000.0 , .k = 15.3846153 , .b = -2076.923 } }
Что касается «удлиняет программу», то у меня нет задачи сделать короче, у меня есть задача написать так, чтоб и я об этом не забыл через год, и другие люди могли понять что происходит.
upd: Когда перечисление большое, можно облажаться с количеством элементов. Особенно при добавлении новой строки. А при такой инициализации можно случайно пропустить один элемент, и это плохо, но хотя бы не поедут все остальные. Короче, мне удобно. Как и инициализация с указанием имени члена структуры.Bunikido Автор
21.02.2022 18:29Ага, понятно теперь. То есть таким образом можно не запутаться в коде.
static const char *aircraftNames[ AircraftTypesCount ] = { [ AircraftTypeUnknown ] = "Undefined" , [ AircraftTypeWindow ] = "Unknown aircraft, Window mode" , [ AircraftTypeB737Zibo ] = "Boeing 737NG family by Zibo" , [ AircraftTypeB737700Eadt ] = "Boeing 737NG family by EADT" , [ AircraftTypeB737Default ] = "Boeing 737-800 default" , [ AircraftTypeB747Default ] = "Boeing 747-400 default" // Ну и т.д.
А это вообще хорошая практика каждый элемент
enum
называть с одного и того-же слова? Если да то возьму на заметку - нередко такое видел в больших программах.
Bunikido Автор
21.02.2022 18:35А ещё какая-то у вас не сильно понятная форматировка (я привык что запятые ставятся на текущей строке, а не на следующей) - а у вас тут такие записи:
, .k = 15.3846153 , .b = -2076.923 }
Немного запутывает, также как и
static
перед массивом строк. У вас программа много-файловая?
staticmain
22.02.2022 01:05+2Я сам новичок в С — прямиком из Паскаля.
Простите, если вы новичок, то зачем вы написали этот пост в котором выдвигаете предположения о том, что часть конструкций языка непонятно для чего нужны и их никто не использует?Bunikido Автор
22.02.2022 10:04о том, что часть конструкций языка непонятно для чего нужны и их никто не использует
У вас абсолютное непонимание смысла статьи. Вот:
Сначала расскажу о базовых и часто встречаемых конструкциях, которые могут и вовсе не удивить опытных программистов на С, но вот уже ниже идут всё более редкие и интересные конструкции, которые уже далеко не часто можно где-либо встретить.
Где здесь упоминание что они не для чего не нужны и их никто не использует?
зачем вы написали этот пост
Я захотел попробовать себя в чём-то новом. Я уже давно читаю Хабр и решил тоже присоединиться - делиться и получать опыт. И несмотря на критику я буду продолжать это дело, и это нормально: как я понял хабрасообщество не всегда положительно относится к новичкам, но здесь как мы видим статья ушла в плюс, а это означает что кто-то нашёл или подчеркнул для себя что-то новенькое. Ошибки делают сильнее
unsignedchar
22.02.2022 12:18+1
darkdaskin
21.02.2022 16:14Отрицательные индексы у указателей вполне можно встретить в библиотечном коде. По этим адресам обычно хранятся метаданные, например, количество элементов для массива или тип объекта для сишной реализации ООП.
Bunikido Автор
21.02.2022 18:22Про библиотечный код вы верно упомянули. В пользовательских программах на С редко такое встретить можно.
сишной реализации ООП.
Сам одно время рассматривал этот момент, и базовые вещи по типу методов через указатели на функции. Но зачем, когда есть плюсы =)
Polaris99
21.02.2022 16:40+1for (int i = 0; i < 10; i++){ putchar("@#c;:,. "[brightness[i]]); }
А где ж тут brightness и зачем она вообще??
Bunikido Автор
21.02.2022 18:19brightness это массив из индексов (яркостей). К примеру такой:
const uint8_t brightness[10] = {2, 3, 4, 5, 6, 7, 6, 5, 4, 3};
Если запустить упомянутый вами код вместе с приведённым массивом на выводе будет простой ASCII градиент:
c;:,. .,:;
Я этот пример привёл потому что таким приёмом часто пользуются при создании ASCII графики, а массив записывают сразу, дабы сэкономить память.
Myxach
21.02.2022 18:36В самом деле, немногие начинающие программисты знают что можно объявить массив/строку и взять элемент из неё.
стоп, что? А почему? А Можно ли их назвать хотя-бы начинающими? Звучит как те, кто язык вообще в жизни не видел
pronvit
21.02.2022 23:09+2Это статья-стёб, или почему кто-то ее плюсует?
Bunikido Автор
21.02.2022 23:27Плюсует - значит понравилось. И нет статья не стеб - какие-то сомнения? Я жду конструктивной критики, а не вот это вот всё =)
unsignedchar
22.02.2022 09:38+1Авторское видение ненужности некоторых фич С вызывает интерес ;) Если бы в каждом утверждении были бы слова ИМХО - было бы более правильно. А критиковать то, что автор не знает, зачем указатель на функцию, или указатель на указатель, это неконструктивно.
win32asm
22.02.2022 12:29В Си статическими удобно делать мелкие функции определяемые в хедерах.
Sizeof это оператор, такой же как +, - или ?:. Формально в скобках (как вызов функции) пишется напрямую тип, а без скобок - выражение, размер типа которого нам нужен. Практически gcc и clang считают оба использования синонимичными, но есть люди которые настаивают на различии для большего контекста.
Bunikido Автор
22.02.2022 12:37В Си статическими удобно делать мелкие функции определяемые в хедерах.
А зачем писать функции в хедерах если можно сразу написать их в том файле который подключает этот хедер так ещё и статические. Может тут какой-то трюк?
win32asm
22.02.2022 13:36Ну, например, если в хедере определена общая для нескольких файлов структура, и обращение к каким-то полям удобно выделить в функцию:
struct Object { enum State state; ... }; static bool objectInTransition(struct Object *obj) { return obj->state == STATE_TRANSITION_1 || obj->state == STATE_TRANSITION_2; }
andy_p
22.02.2022 20:21+2Соответственно
scanf()
возвращает кол-во считанных байт.Нет. scanf возвращает количество успешно считанных полей.
Devoter
23.02.2022 02:27+1Про union как способ реинтерпретации тут уже написали (использовать с осторожностью), но главный, как по мне смысл: экономия памяти. Например, у нас некоторое значение может быть как int так и double, а то и что поэкзотичней (разные структуры), но мы знаем, что значение может быть только одно в один момент времени. Таким образом, нам не нужно выделять память под два поля в структуре, например, а лишь под объединение, которое будет иметь размер максимального типа: sizeof(double) < sizeof(int) + sizeof(double).
И небольшое напутствие на будущее: не стоит считать себя сколь угодно серьезным разработчиком и пытаться давать экспертные заключения, если все ваши приложения состоят из одного файла (а если это файлы по 4000+ строк - стоит обратиться к специалисту...)).
sergio_nsk
А как же
1[arr]
?auto int x; в C и старом C++ и auto idx = 1 в современном C++ - совсем не одно и то же.
int *dyn_arr = (int*)malloc(... Привидение типа в C здесь вообще лишнее, void* приводится к любому указателю.
Стоило упомянуть, что в C функция, объявленная без параметров принимает произвольное число аргументов.
Bunikido Автор
1[arr]
- я рассказывал про это явление:Возьму на заметку.
Здесь я сделал это для наглядности. На самом деле как по мне каст увеличивает читабельность подобных записей:
char** mat = (char**)malloc(...
Если в C++ это необходимость, то в С это на вкус и цвет, и всё равно этот каст никому не навредил =) (Разве что исходный код будет весить на пару байт больше)
Добавил в раздел Функции с переменным количеством параметров, спасибо
oldnomad
Каст
malloc
в C (и вообще, явные касты изvoid *
) -- вредная практика. Пример:Здесь функция
malloc
не объявлена (она объявляется в<stdlib.h>
, а не в<stdio.h>
). По умолчанию в отсутствие объявления будет считаться что функция возвращаетint
, и результат будет непредсказуем. Конечно, компилятор, скорее всего, выдаст предупреждение, но, если бы каста не было -- была бы более наглядная ошибка.Bunikido Автор
Спасибо, буду знать
quaer
А как без кастов делать?
oldnomad
А тут каст и не нужен. По стандарту, указатель
void *
автоматически приводится к любому указателю (за исключением указателя на функцию) -- так же как можно, скажем, константу42
(без суффиксов типint
) присвоить переменной типаlong
без всяких кастов.quaer
Подобные автоматические приведения типов могут быть источником ошибок, поэтому мне больше нравятся явные. Поэтому и интересно какие ещё есть варианты получить явную ошибку в данном случае.
unsignedchar
Нет проверок на размер кастуемых данных — это проблема. Можно даже так вот сделать:
Или так:
И оно скомпилируется (с варнингами), и даже как-то запустится. А если привести к указателю — возможно, даже будет работать более-менее правильно с некоторыми данными, за счет того, что malloc выделит не ровно 3 байта, а выровненный по каким-то границам кусок. ;)