С - это неоднозначный язык. Сам по себе он небольшой, об этом писал Брайан Керниган в своей знаменитой книге "Язык программирования С". Но это не мешает комбинируя базовые возможности языка составлять более комплексные и запутанные конструкции. Иногда они встречаются в кодах реальных программ довольно редко, из-за своей узкой области применения - о них и пойдёт речь в этой статье.

Структура статьи

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

Содержание

  1. union

  2. register

  3. Статические функции

  4. Константный указатель

  5. Препроцессорная склейка строк

  6. Конкатенация строк во время компиляции

  7. #undef

  8. sizeof без ()

  9. Создание функции в стиле K&R

  10. Указатели на функции

  11. Указатели на указатели

  12. Функции с переменным количеством параметров

  13. Массив + индекс (два в одном)

  14. Индекс с массивом, но вверх ногами

  15. strtok(), tmpfile()

  16. Возврат указателя на функцию из функции

  17. volatile

  18. Макросы с переменным количеством параметров

  19. auto

  20. Использование возвращаемых значений scanf() и printf()

  21. Каламбур типизации

  22. Отрицательные индексы

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; - указатель на char

  • const char* ptr; - указатель на const char

  • char * const ptr; - константный указатель на char

  • const 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); будет следующая последовательность операций препроцессора:

  1. printf(#exp " = %i\n", (int)(exp)); //исходная строка

  2. printf(#(n/5) " = %i\n", (int)(n/5)); //вставка аргумента

  3. printf("n/5" " = %i\n", (int)(n/5)); //конвертирование в строку

  4. 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 ячейки вправо. Теперь полученный адрес обязательно нужно привести к указателю на целую ячейку, и перейти по нему. Такой пример в отличии от предыдущего демонстрирует не сокращение объёма кода, а наоборот его увеличение, а что ещё хуже - увеличению количества операций которое нужно выполнить. Из этих примеров вывод такой - каламбур типизации в реальных программах это плохо потому, что

  1. Размер ячеек на каждой платформе может отличаться

  2. Разные компиляторы могут по разному хранить переменные в памяти

Оба этих фактора могут обеспечить неправильное или неопределённое поведение программы.

Отрицательные индексы

Изюминка этого чарта. 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)


  1. sergio_nsk
    21.02.2022 09:58
    +8

    А как же 1[arr]?

    auto int x; в C и старом C++ и auto idx = 1 в современном C++ - совсем не одно и то же.

    int *dyn_arr = (int*)malloc(... Привидение типа в C здесь вообще лишнее, void* приводится к любому указателю.

    Стоило упомянуть, что в C функция, объявленная без параметров принимает произвольное число аргументов.


    1. Bunikido Автор
      21.02.2022 13:56
      +1

      1. 1[arr] - я рассказывал про это явление:

      Индекс с массивом, но вверх ногами

      Простой пример:

      int arr[ARR_SIZE], i = ARR_SIZE-1;while (i>=0) {i--[arr] = i+1;}

      1. Возьму на заметку.

      2. Здесь я сделал это для наглядности. На самом деле как по мне каст увеличивает читабельность подобных записей:

        char** mat = (char**)malloc(...

        Если в C++ это необходимость, то в С это на вкус и цвет, и всё равно этот каст никому не навредил =) (Разве что исходный код будет весить на пару байт больше)

      3. Добавил в раздел Функции с переменным количеством параметров, спасибо


      1. oldnomad
        21.02.2022 17:11
        +1

        Каст malloc в C (и вообще, явные касты из void *) -- вредная практика. Пример:

        #include <stdio.h>
        
        int *foo(void) {
          return (int *)malloc(10);
        }

        Здесь функция malloc не объявлена (она объявляется в <stdlib.h>, а не в <stdio.h>). По умолчанию в отсутствие объявления будет считаться что функция возвращает int, и результат будет непредсказуем. Конечно, компилятор, скорее всего, выдаст предупреждение, но, если бы каста не было -- была бы более наглядная ошибка.


        1. Bunikido Автор
          21.02.2022 18:08

          Спасибо, буду знать


        1. quaer
          22.02.2022 03:44

          А как без кастов делать?


          1. oldnomad
            22.02.2022 04:13
            +1

            А как без кастов делать?

            А тут каст и не нужен. По стандарту, указатель void * автоматически приводится к любому указателю (за исключением указателя на функцию) -- так же как можно, скажем, константу 42 (без суффиксов тип int) присвоить переменной типа long без всяких кастов.


            1. quaer
              22.02.2022 13:15

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


        1. unsignedchar
          22.02.2022 13:55
          +2

          Нет проверок на размер кастуемых данных — это проблема. Можно даже так вот сделать:

          char foo(void) {
            return (char )malloc(3);
          }

          Или так:
          int foo(void) {
            return (int )malloc(3);
          }

          И оно скомпилируется (с варнингами), и даже как-то запустится. А если привести к указателю — возможно, даже будет работать более-менее правильно с некоторыми данными, за счет того, что malloc выделит не ровно 3 байта, а выровненный по каким-то границам кусок. ;)


  1. 0xd34df00d
    21.02.2022 10:11
    +1

    Прочитал не так, del


    1. lz961
      21.02.2022 10:23

      int brr[100] = {0};

      int *arr = brr+50;

      arr[-1] = 3;

      Есть ли здесь "undefined behaviour" ?


      1. Gordon01
        21.02.2022 10:33
        -1

        По стандарту, arr указывает на 51й элемент, то есть валиден всего для четырех байтов.

        Код содержит UB, потому что эксплуатируется особенность компилятора не проверять границы адресов для указателей. arr[-1] указывает на последний байт 50го элемента, чего быть не может.


        1. lz961
          21.02.2022 10:42

          int 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

          вроде, соответствует написанному в статье


          1. 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

            Указатель в Си — это не адрес памяти. Это просто абстракция.


            1. DistortNeo
              21.02.2022 16:28
              +1

              Как вышло?

              Вы, используя указатель на a2, поменяли значение в массиве a1. Естественно, это будет UB.


            1. viordash
              21.02.2022 16:43

              оптимизация? я про адреса


            1. Bunikido Автор
              21.02.2022 18:11
              +1

              Указатель в Си — это не адрес памяти. Это просто абстракция.

              Брайан Керниган писал что это именно адрес в памяти.


              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]);

                Много бы крови сэкономил


                1. Bunikido Автор
                  21.02.2022 20:57

                  Assertion failed!

                  Program: C:\Users\Random_User\ \a.exe
                  File: tmp.c, Line 10

                  Expression: i1 == 3

                  Дальше программа естественно не выполняется.

                  А что не так? Вы выполнили переход к области памяти, в которой никто не знает что лежит: arr[-2]. Конечно там не может быть 3.


                  1. Gordon01
                    21.02.2022 21:17
                    -3

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

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

                    Я имел в виду, что arr[-2] и arr[3] в случае массива из 5 элементов указывали бы на одинаковый элемент.

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

                    А современные компиляторы все равно надеятся на strict aliasing даже в дефолтных настройках (-О3), что я вам и показал.


                    1. 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]?


                      1. Gordon01
                        21.02.2022 21:54
                        -2

                        Я не про историческую реализацию си, а про то как надо было делать (легко говорить из будушего).

                        https://www.php.net/manual/ru/function.substr.php

                        Положительный индекс — считать с начала, отрицательный — считать с конца.


                      1. Bunikido Автор
                        21.02.2022 22:03

                        Положительный индекс — считать с начала, отрицательный — считать с конца.

                        Полный бред =) Тогда смысл этих "отрицательных" индексов если можно написать просто:

                        extern int arr[];
                        extern size_t i;
                        ... = arr[ARR_SIZE-i];

                        Ох уж эти ленивые пхпшники... Мы Сишники, с нами стандарт!


                      1. Gordon01
                        21.02.2022 22:07

                        extern size_t i;

                        ptrdiff_t

                        ... = arr[ARR_SIZE-i];

                        error: ARR_SIZE undefined

                        Мы Сишники, с нами стандарт!

                        Крииинж %)

                        https://gcc.gnu.org/bugzilla/show_bug.cgi?id=30475

                        Почему сишникам меньше всего платят тогда?


                      1. Bunikido Автор
                        21.02.2022 22:29
                        +1

                        ptrdiff_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;
                        }

                        Крииинж %)

                        Ох уж эти ваши тиктоки


                      1. 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


                      1. Bunikido Автор
                        21.02.2022 22:31

                        Почему сишникам меньше всего платят тогда?

                        Откуда такая информация? C занимает 2 место по популярности по рейтингу TIOBE


                      1. Gordon01
                        21.02.2022 22:36
                        +1

                        Откуда такая информация? C занимает 2 место по популярности по рейтингу TIOBE

                        С рынка труда)


                      1. Bunikido Автор
                        21.02.2022 22:37

                        Я пишу на С потому что удобно - а не потому что много платят :) Это тот самый язык «что написано то и сделано»


                      1. 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


                      1. tabtre
                        22.02.2022 11:53

                        Undefined 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. Это так?


                      1. Balling
                        22.02.2022 16:00

                        Имеются в виду оптимизации от противного. Signed overflow надо как-то обязательно проверять (так как есть консенсус, как обрабатывать это неопределенное поведение), даже если это выделенная инструкция ассемблера с флагами overflow.


                    1. wataru
                      22.02.2022 00:41
                      +2

                      Ну так да, диды не догадались сделать отрицательные индексы

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


                      1. Gordon01
                        22.02.2022 09:58

                        А в довольно низкоуровневом Си так делать нельзя.

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


                      1. wataru
                        22.02.2022 13:06
                        +1

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


                      1. Gordon01
                        22.02.2022 17:54

                        Если программист хочет использовать эту фичу, он пишет restrict.

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

                        Как показывает практика — это интересно целым 0 разработчикам. (поискал сейчас в кодовой базе в одной очень крупной российской софтовой компании, нашел одно упоминание restrict в библиотечном коде).


                      1. 0xd34df00d
                        23.02.2022 05:40
                        +1

                        Программист сам проверяет

                        Очень интересно! То есть, каждый программист должен сам реализовать функцию вроде


                        bool doesOverlap(int *arr1, int size1, int *arr2, int size2)
                        {
                          ...
                        }

                        ?


                        Как бы вы её реализовали, кстати?


                      1. 0xd34df00d
                        23.02.2022 23:29
                        +1

                        Раз никто не ответил, то напишу, что просто вычесть указатели и сравнить их с размерами нельзя, а тем временем я отчего-то уверен, что 95% программистов сделали бы именно это.


                        Эх, такая дискуссия об UB пропала…


                      1. 0xd34df00d
                        23.02.2022 05:36
                        +1

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

                        Есть три основных случая:


                        1. Массив статический и с константным размером. Тогда компилятор всё знает.
                        2. VLA. Я считаю, что это плохой тон, но компилятор рантайм про размер тоже знает, таскать длину ему всё равно надо (чтобы правильно подёргать указатель на вершину стека, например), и мы ничего не теряем.
                        3. Массив выделен на хипе условным маллоком. Тогда рантайму снова должен быть известен размер, чтобы потом корректно освободить память.

                        Где здесь оверхед?


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

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


                      1. wataru
                        23.02.2022 13:24

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


                        Что касается маллока, то размер есть у системы. Вы уверены, что рантайм Си хранит этот размер? Зачем ему копию у себя поддерживать? Она не всегда нужна. А есть этот размер у системы, потому что нельзя доверять пользователю системного апи тащить его от malloc'а до free из соображений безопасности. Потому что можно передать в free больше, чем вы запросили и потом утащить память другого процесса.


                        Как раз необходимость протаскивать самому размер массива — оверхед

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


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


                      1. DerRotBaron
                        23.02.2022 23:04

                        Что касается маллока, то размер есть у системы. Вы уверены, что рантайм Си хранит этот размер?

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


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

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


                      1. 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 и есть система.


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

                        Далеко не везде рантайм должен об этом помнить.


                      1. Gordon01
                        23.02.2022 22:18

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


            1. lz961
              21.02.2022 19:07

              https://godbolt.org/z/ah46avxb6

              поломалось, в выводе указатели отличаются, хотя код не изменен. Т.е. у вас настоящее UB

              3, 0
              0x7ffe1dae5124, 0x7ffe1dae5184


              1. Gordon01
                21.02.2022 20:33

                Мне дальше говорить не интересно, сорян.

                Т.е. у вас настоящее UB

                И оно наглядно показывает что "Указатель в Си — это не адрес памяти. Это просто абстракция."


                1. 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

                  https://godbolt.org/z/7ej4K376W


        1. DistortNeo
          21.02.2022 16:29
          +2

          arr[-1] указывает на последний байт 50го элемента

          Абсолютно неверно. У вас тип arr — это int *, он будет указывать на 50-й элемент.
          Никакого UB в данном случае не будет.


      1. 0xd34df00d
        21.02.2022 10:48
        +1

        Да, я поправил потом.


  1. Myxach
    21.02.2022 10:12
    +14

    Указатели на функции

    Серьезно? Это средство для реализации калбека. С чего это она не полезна?


    1. Bunikido Автор
      21.02.2022 14:03
      -2

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


      1. DrGluck07
        21.02.2022 15:06
        +1

        Например всё API X-Plane сделано на коллбеках. Да и на железе это очень часто используемая фича.


      1. JerleShannara
        21.02.2022 15:27
        +2

        STM32 HAL весь на коллбэках построен


  1. aamonster
    21.02.2022 10:15
    +8

    бОльшая часть – реально используемая.
    А в примере с преобразованием указателя – вообще ошибка: в числе 0x2E6D6172676F7270 все 8 байт ненулевые, значит, после 8 символов начнёт печатать то, что дальше в памяти.


    1. eurol
      21.02.2022 13:01

      Не говоря уже о том, что приведенные двоичные числа вовсе непонятно, к чему относятся…


      1. Bunikido Автор
        21.02.2022 19:30

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


        1. eurol
          21.02.2022 20:01

          Даже близко не вижу ничего похожего. Мало того, что в двоичном виде нулевой байт есть, а в исходном числе его нет, так там только одно четное число, которое в 16-ричном виде будет выглядеть как E8, а остальные числа все нечетные, в то время как в исходном числе четных несколько есть...


    1. Bunikido Автор
      21.02.2022 19:35

      бОльшая часть – реально используемая.

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

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

      Если вы опытный программист на С то могу предположить что бОльшая часть находится именно вверху

      А в примере с преобразованием указателя – вообще ошибка: в числе 0x2E6D6172676F7270 все 8 байт ненулевые, значит, после 8 символов начнёт печатать то, что дальше в памяти.

      Поправил формат


      1. unsignedchar
        21.02.2022 19:40
        +2

        редко используются в средне-статистических программах на С

        Знать бы, что это такое, среднестатистическая программа на С ;)


  1. Xeldos
    21.02.2022 10:52
    +16

    Временный файл, двойные указатели, указатели на функции, константные указатели - это для вас странные технологии? Разбиение строки на токены - странная технология? Вы точно пишете на C? Вы точно программируете?


    1. aamonster
      21.02.2022 12:49
      +3

      strtok – странная, да. Как минимум, strtok_s, а то strtok, хранящая свой контекст неизвестно где (а мало ли между вашими вызовами будет вызов strtok из какой-то библиотеки?) – лютый зашквар.


      1. Bunikido Автор
        21.02.2022 19:25
        +1

        абсолютно согласен по поводу strtok.


    1. Bunikido Автор
      21.02.2022 19:28
      -2

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

       Вы точно пишете на C? Вы точно программируете?

      Тише, если бы я не писал на С и не программировал то вы бы эту статью не увидели )


      1. Myxach
        21.02.2022 20:24
        +6

        Ощущение, что вы только hello world писали


      1. Devoter
        23.02.2022 01:28

        strtok, действительно,имеет ограниченный спектр применения, так как требует крайне аккуратного применения, что для однопоточных программ при грамотном проектировании (большинство из coreutils), впрочем - не проблема (с strtok_s по производительности не сравнивал). tmpfile, действительно, с ходу не вспомнить, где встречалось, но идея-то понятна: можно подсунуть библиотекам, которые умеют работать только с файлами такой вот "файл". С натяжкой можно сравнить это с подходом интерфейсов Reader и Writer в Go.


  1. RomanArzumanyan
    21.02.2022 10:54
    +5

    Указатели на функции

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

    В крупных проектах на С используются для реализации полиморфизма.

    Например, в ядре Linux и 100500 мультимедиа библиотеках.


  1. shaykemelov
    21.02.2022 11:53
    +1

    что-то мне подсказывает, что это не такие уж и редко используемые возможности языка. про них я узнал всего лишь из одной книги https://www.ozon.ru/product/yazyk-programmirovaniya-c-lektsii-i-uprazhneniya-147927650/?sh=Lrd_YwAAAA, которая рассчитана на начинающих.


  1. rcl
    21.02.2022 12:40
    +3

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

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


    1. Gordon01
      21.02.2022 13:02
      +1

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

      Жаль, только что компиляторы си не умеют сами разбираться, какие диапазоны памяти пересекаются, а какие нет


      1. rcl
        21.02.2022 13:25

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


        1. Bunikido Автор
          21.02.2022 19:24

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

          Это уже какая-то новая фича. Ещё вспомнил прикол про то, что компилятор отказывается компилить неотформатированный код. Восстание машин не за горами =)


          1. iliasam
            21.02.2022 22:32

            «компилятор отказывается компилить неотформатированный код»
            Python?


            1. Bunikido Автор
              21.02.2022 22:35

              Не, это была картинка-прикол где компилятор выдавал сообщение об отказе компиляции из-за отсутствия отступов в коде на C++


        1. 0xd34df00d
          22.02.2022 00:17
          +1

          То есть, вы без оптимизаций собираете код? Тогда есть вопросы к быстроте.


          А к лаконичности есть вопросы в любом случае.


  1. Gryphon88
    21.02.2022 12:53

    #undef широко используется при злоупотреблении макросами, в частности, при использовании x-macro, когда макрос с одним и тем же именем (традиционно Х, отсюда и название) разворачивается по-разному. Из безобидных примеров вспомню генерацию массива строк для enum, но вообще можно и полиморфизм реализовывать, и RAII, и много чего ещё. Дебажить, конечно, весело, но это всё равно лучше преобразования к void*.


  1. unsignedchar
    21.02.2022 13:48

    uint64_t x = 0x2E6D6172676F7270;
    printf ("%s\n", (char*)&x);


    Аффтар жжот. Если в программе есть ещё переменные — printf может вывести что угодно кроме ожидаемой строки.


    1. Bunikido Автор
      21.02.2022 14:12

      uint64_t x = 0x2E6D6172676F7270;
      printf ("%.8s\n", (char*)&x);

      Поправил - поставил ограничение на вывод максимум 8 символов строки.


      1. Gordon01
        21.02.2022 14:19

        uint64_t x = 0x2E006172676F7270;

        Напечатает не то, что ожидалось


        1. Bunikido Автор
          21.02.2022 19:20

          Правильно - у вас 7 байт другой. В оригинале он 6D а у вас 00.


          1. unsignedchar
            21.02.2022 20:17

            Дело в том что uint64_t* и char* - это сильно разные обьекты с сильно разными свойствами. У одного длина фиксированная, у другого может быть любая. Не нужно приводить их к друг другу. Просто не нужно. Даже в примере из 2 строк сделать более-менее правильно ожидаемо у вас получилось со 2 раза.


            1. viordash
              21.02.2022 22:00

              Дело в том что uint64_t* и char*

              вы пишете именно про uint64_t* (указатель на uint64_t)? как может быть фиксированная длина у одного указателя, а у другого нет?


              1. unsignedchar
                21.02.2022 22:42
                -1

                У объекта, на который указывает указатель. Ну, вы поняли ;)


          1. Gordon01
            22.02.2022 10:21

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

            uint64_t x = 0x2E006172676F7270;
            printf ("%.8s\n", (char*)&x);


            1. 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 и точку. Всё верно


              1. Gordon01
                22.02.2022 17:51
                +1

                Вывод: program.(правильный вывод)

                С чего это он правильный? Такой же UB'шный, как и любые другие варианты.

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

                И главное — повторять мантру:

                Bunikido 21.02.2022 в 22:37

                Я пишу на С потому что удобно - а не потому что много платят :) Это тот самый язык «что написано то и сделано»


                1. Bunikido Автор
                  22.02.2022 21:05

                  Это тот самый язык «что написано то и сделано»

                  А у вас есть что возразить насчёт этого? Написал допустим + или -> и сразу можешь предсказать что будет на выходе


                  1. Gordon01
                    22.02.2022 21:34

                    А у вас есть что возразить насчёт этого?

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

                    Написал допустим + или -> и сразу можешь предсказать что будет на выходе

                    Это не имеет никакого значения для бизнеса.


                    1. Bunikido Автор
                      22.02.2022 22:50

                      Почему же не могу? Пишите что должен делать сниппет (только не гигантский проект) а я реализую его как кросс-платформенный

                      Это не имеет никакого значения для бизнеса

                      Я здесь не для заработка денег :)


                      1. Gordon01
                        22.02.2022 22:56

                        Почему же не могу? Пишите что должен делать сниппет (только не гигантский проект) а я реализую его как кросс-платформенный

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

                        Я здесь не для заработка денег :)

                        Бизнес-задача — это полезная работа, которую должен делать код. Мелочи одного единственного (причем одного из самых простых) языка для этого не играют никакого значения.

                        Даже если деньги не зарабатываются и это пет-проект.


                      1. Bunikido Автор
                        22.02.2022 23:39

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

                        А кросс-платформенное решение я уже опубликовал в статье, вот оно краткое и лаконичное:

                        extern uint64_t x; //сама строка
                        if (!x)
                            return;                    //если строка нулевая то завершаем работу функции
                        do {
                            putchar((char)(x & 0xFF)); //печатаем байт
                            x >>= 8;                   //удаляем байт
                        } while (x);                   //пока число не станет нулем
                          

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

                        1. Платформа не поддерживает С

                        2. Платформа не имеет файла inttypes.h/stdint.h

                        3. uint64_t не хранится в памяти на этой платформе по порядку в одной ячейке и его байты раскиданы по памяти как связный список.

                        4. Платформа не имеет файла stdio.h

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


                      1. oldnomad
                        23.02.2022 00:41

                        1. Платформа не имеет 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.

                        Полностью переносимый код -- это сложно. Порой, непереносимо сложно.


                      1. Devoter
                        23.02.2022 01:38

                        А почему вы, собственно, решили, что порядок байт будет именно LE, а не BE? Стандарт это никак не регламентирует и ваш >> окажет совсем не тот эффект, что вы ожидаете.


                      1. Devoter
                        23.02.2022 02:37
                        +1

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


                      1. viordash
                        23.02.2022 13:30

                        А кросс-платформенное решение я уже опубликовал в статье, вот оно краткое и лаконичное:

                        ваш вариант с

                        printf ("%.8s\n", (char*)&x);

                        тоже такой же кросс платформенный, но имхо более понятный, так как не потребует рефакторинга.

                        Единственная проблема в обоих решениях это BE/LE, а это можно обойти директивой препроцессора, каким-нибудь #ifdef__BYTE_ORDER__ , или compile-time проверкой через определение положения не нулевого байта в переменной int32 == 1.


                      1. Gordon01
                        23.02.2022 22:14

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

                        Также, этот код не соберется каким-нибудь IAR c включенными MISRA. Ну и главное - этот код не пройдет сертификацию у более-менее серьезных людей.


                      1. viordash
                        23.02.2022 22:48

                        Какую из переменных, в упоминаемом мной коде вы считаете не выровненым?

                        Код не упадет, так как переменная uint64_t x выровнена естественным образом, и в printf передается адрес на эту переменную. А как внутри printf уже будут обрабатываться эти байты не очень важно.

                        уверен что и иар с MISRA норм соберет, но наверно для самой printf возникнет нюанс, какое-то из правил касалось переменного кол-ва аргументов


                      1. viordash
                        23.02.2022 22:52

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


                  1. unsignedchar
                    22.02.2022 22:22

                    Написал допустим + или -> и сразу можешь предсказать что будет на выходе


                    undefined behavior in c. 100500 примеров.


                    1. Bunikido Автор
                      22.02.2022 22:52

                      Опытные программисты на С умеют избегать UB жертвуя объёмом кода, и я стремлюсь следовать этому совету. Мы же тут не за краткостью кода собрались, а если да то тогда Python будет хорошим вариантом


  1. 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 нужно проверять ВСЕГДА. Иначе есть шанс что вы будете читать мусор.

    >Каламбур типизации


    А каламбур где? Автор не парсил входной поток байт от устройств?

    >Отрицательные индексы


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


    1. 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) — термин, который используется в информатике для обозначения различных техник нарушения или обмана системы типов некоторого языка программирования, имеющих эффект, который было бы затруднительно или невозможно обеспечить в рамках формального языка. Вот я здесь и обманул систему типов.

      Это плохо, не делайте так

      Я то не делаю - просто рассказываю


      1. 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);


        1. Bunikido Автор
          21.02.2022 15:25

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

          Я это знаю. Почитайте мои комментарии, я об этом писал:

          int **p; /* это всё ещё переменная, а может быть двумерный массив. А может надо изменить указатель через его адрес */


        1. Polaris99
          21.02.2022 17:04
          +4

          Вы не «не использовали», вы «не смотрели что используете».

          А что, в ардуино по-другому бывает? Есть, конечно, люди, которые все эти библиотеки пишут, но 99% "разработки" под ардуино сводится к поиску подходящей библиотеки и попытке запустить ее из коробки. Не заработала сразу - плохая, негодная библиотека, нужна другая. Заработала - да я крутой эмбеддед-разработчик, пёс:


          1. Bunikido Автор
            22.02.2022 10:28
            -1

            А что, в ардуино по-другому бывает?

            Ардуино создавалась как лайтовая платформа для практики электроники (как конструктор). Библиотеки на любой вкус и цвет. Там конечно можно и на ассемблере писать, но зачем если есть прекрасный средне-уровневый С который в каждой микроволновке работает. Если интересно как работают железяки и процессоры на низком уровне и хочется разбираться то как по мне лучше сразу брать какой-нибудь MOS 6502 и писать на нём на чистом ассемблере. Правда у меня его пока нет =)


            1. Polaris99
              22.02.2022 11:54
              +3

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


              1. Bunikido Автор
                22.02.2022 12:06
                -1

                 А уж про саму оболочку вообще ничего нематерного сказать не смогу, это вообще какой-то конец 90х в плане комфорта и эффективности разработки

                Ну да среда и правда сомнительная но наоборот заточена под комфорт как бы это парадоксально не звучало. В Arduino IDE (если вы о нём говорите) как по мне удобное решение - простой редактор, две кнопки для прошивки и компиляции. А что именно вам пришлось не по вкусу?

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

                Вот это да но почему никому? Если человек работает и прошивает или пишет прошивки для железа он хочет чтобы программа работала настолько быстро насколько это возможно в теории. Я не спорю - Си это один из немногих языков где можно писать дичь - while($>>=x^28+~10) f("89!"+op<<1);, и на том же языке можно писать вполне себе читабельные высокоуровневые программы. А если захочется - с помощью инструментов языка можно вставить ассемблерный код напрямую в С - тогда компилятор его компилировать не будет. Вот это я понимаю комбо =)

                Си работает отлично

                Несомненно. Правда иногда задумываешься - какую следующую коварную оптимизацию применит компилятор чтобы испортить твою программу?)


                1. Gordon01
                  22.02.2022 17:58

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

                  Нет конечно, скорость даже самых дохлых современных микроконтроллеров достаточна. Людям важна корректность.

                  В >50% случаев в финальные устройства заливаются прошивки скомпилированные в дебаге, о чем речь вообще?


      1. 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 теперь стал более прозрачен.


        1. Gordon01
          23.02.2022 22:29

          Зато вопрос про volatile на собеседовании позволяет сделать хороший отсев)


  1. cosmo68
    21.02.2022 14:55
    +2

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


    1. Bunikido Автор
      21.02.2022 15:03

      Спасибо)

       претензий к статье много

      Классика - это же первая статья


  1. jakushev
    21.02.2022 14:57
    +5

    volatile
    Иногда компилятор может оптимизируя удалять целые куски кода. Чтобы такого не было желательно ставить ключевое слово volatile перед объявлением переменной, если вы хотите чтобы компилятор 100% не применил свои коварные оптимизации к этой переменной. (Хорошо что слово volatile не может игнорироваться, как иногда бывает с register, а то в этой жизни пришлось бы больше ни в чём не быть уверенным)


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


  1. dlinyj
    21.02.2022 15:14

    80-90% использую на практике.


    1. Bunikido Автор
      21.02.2022 15:28

      Вы большой молодец! Наверняка работайте с железом?


      1. dlinyj
        21.02.2022 18:58

        Именно так.


  1. wataru
    21.02.2022 15:16
    +2

    Про указатели на указатели на указатели… Не могу не поделиться своей болью, хоть это и C++. Сигнатура MFEnumDeviceSources:


    HRESULT MFEnumDeviceSources(
      [in]  IMFAttributes *pAttributes,
      [out] IMFActivate   ***pppSourceActivate,
      [out] UINT32        *pcSourceActivate
    )

    Туда передается указатель на место, куда будет записан массив указателей на интерфейсы...


    Вообще winapi полно подобных перлов.


    1. Kelbon
      21.02.2022 15:21
      +2

      не похоже на С++ (особенно in out)


      1. wataru
        21.02.2022 15:24

        Это типа комментарии на docs.microsoft.com


      1. 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){...}


        1. Kelbon
          21.02.2022 18:37
          +2

          интересная идея это использовать возвращаемое значение в качестве возвращаемого как ни странно значения, а то что тут обозначено как in делать const или принимать по значению


    1. DrGluck07
      21.02.2022 15:27

      А какие глубины глубин скрыты внутри виндового API для драйверов. Структура из юнионов, которые являются структурами, ммм…


      1. JerleShannara
        21.02.2022 15:32
        +1

        Спасибо, у меня опять винддкшные флешбэки начались.


        1. DrGluck07
          21.02.2022 15:52

          Двадцать лет назад мне на работе досталась поддержка VXD-драйвера к железке под ISA-слот. Естественно на ассемблере. И это, кстати, довольно простая штука, по сравнению с WDM. Потом пришлось писать под WDF, но к сожалению не пригодилось. Зато было весело и я узнал что такое Syser Kernel Debugger (или как оно там называется, уже забыл).


      1. staticmain
        21.02.2022 15:38

        Вы сейчас описали Xlib events :D


  1. DrGluck07
    21.02.2022 15:19
    +4

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

    int x[ 10 ] = { [ 0 ] = 42, [ 1 ] = 22, [ 2 ] = 73 /*и так далее, можно даже в любом порядке*/ }
    


    1. 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% можно менять поля местами.


      1. 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: Когда перечисление большое, можно облажаться с количеством элементов. Особенно при добавлении новой строки. А при такой инициализации можно случайно пропустить один элемент, и это плохо, но хотя бы не поедут все остальные. Короче, мне удобно. Как и инициализация с указанием имени члена структуры.


        1. 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 называть с одного и того-же слова? Если да то возьму на заметку - нередко такое видел в больших программах.


        1. Bunikido Автор
          21.02.2022 18:35

          А ещё какая-то у вас не сильно понятная форматировка (я привык что запятые ставятся на текущей строке, а не на следующей) - а у вас тут такие записи:

          , .k = 15.3846153
          , .b = -2076.923 }

          Немного запутывает, также как и static перед массивом строк. У вас программа много-файловая?


          1. DrGluck07
            21.02.2022 19:30

            1. Ну так табличное выравнивание по заветам PVS-Studio. Мне так удобнее.
            2. Этот массив используется только в данном файле.


            1. Bunikido Автор
              21.02.2022 19:36

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


      1. staticmain
        22.02.2022 01:05
        +2

        Я сам новичок в С — прямиком из Паскаля.

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


        1. Bunikido Автор
          22.02.2022 10:04

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

          У вас абсолютное непонимание смысла статьи. Вот:

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

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

           зачем вы написали этот пост

          Я захотел попробовать себя в чём-то новом. Я уже давно читаю Хабр и решил тоже присоединиться - делиться и получать опыт. И несмотря на критику я буду продолжать это дело, и это нормально: как я понял хабрасообщество не всегда положительно относится к новичкам, но здесь как мы видим статья ушла в плюс, а это означает что кто-то нашёл или подчеркнул для себя что-то новенькое. Ошибки делают сильнее


      1. unsignedchar
        22.02.2022 12:18
        +1

        Не знаю правда, можно и вернуть структуру традиционным путём:


        Можно ;)
        C — язык возможностей ;)


  1. darkdaskin
    21.02.2022 16:14

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


    1. Bunikido Автор
      21.02.2022 18:22

      Про библиотечный код вы верно упомянули. В пользовательских программах на С редко такое встретить можно.

      сишной реализации ООП.

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


  1. Polaris99
    21.02.2022 16:40
    +1

    for (int i = 0; i < 10; i++){ putchar("@#c;:,. "[brightness[i]]); }

    А где ж тут brightness и зачем она вообще??


    1. Bunikido Автор
      21.02.2022 18:19

      brightness это массив из индексов (яркостей). К примеру такой:

      const uint8_t brightness[10] = {2, 3, 4, 5, 6, 7, 6, 5, 4, 3};

      Если запустить упомянутый вами код вместе с приведённым массивом на выводе будет простой ASCII градиент: c;:,. .,:;

      Я этот пример привёл потому что таким приёмом часто пользуются при создании ASCII графики, а массив записывают сразу, дабы сэкономить память.


  1. Myxach
    21.02.2022 18:36

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

    стоп, что? А почему? А Можно ли их назвать хотя-бы начинающими? Звучит как те, кто язык вообще в жизни не видел


    1. Bunikido Автор
      21.02.2022 19:57

      А по вашему мнению начинающие программисты на С - это сколько лет человек должен программировать чтобы стать хотя-бы начинающим?


      1. Myxach
        21.02.2022 20:23
        -1

        Хотя бы Книгу или редактор открывать это уже будет выше уровнем, чем то что вы описали


  1. pronvit
    21.02.2022 23:09
    +2

    Это статья-стёб, или почему кто-то ее плюсует?


    1. Bunikido Автор
      21.02.2022 23:27

      Плюсует - значит понравилось. И нет статья не стеб - какие-то сомнения? Я жду конструктивной критики, а не вот это вот всё =)


      1. unsignedchar
        22.02.2022 09:38
        +1

        Авторское видение ненужности некоторых фич С вызывает интерес ;) Если бы в каждом утверждении были бы слова ИМХО - было бы более правильно. А критиковать то, что автор не знает, зачем указатель на функцию, или указатель на указатель, это неконструктивно.


  1. win32asm
    22.02.2022 12:29

    В Си статическими удобно делать мелкие функции определяемые в хедерах.

    Sizeof это оператор, такой же как +, - или ?:. Формально в скобках (как вызов функции) пишется напрямую тип, а без скобок - выражение, размер типа которого нам нужен. Практически gcc и clang считают оба использования синонимичными, но есть люди которые настаивают на различии для большего контекста.


    1. Bunikido Автор
      22.02.2022 12:37

      В Си статическими удобно делать мелкие функции определяемые в хедерах.

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


      1. 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;
        }


  1. andy_p
    22.02.2022 20:21
    +2

    Соответственно scanf() возвращает кол-во считанных байт.

    Нет. scanf возвращает количество успешно считанных полей.


  1. Devoter
    23.02.2022 02:27
    +1

    Про union как способ реинтерпретации тут уже написали (использовать с осторожностью), но главный, как по мне смысл: экономия памяти. Например, у нас некоторое значение может быть как int так и double, а то и что поэкзотичней (разные структуры), но мы знаем, что значение может быть только одно в один момент времени. Таким образом, нам не нужно выделять память под два поля в структуре, например, а лишь под объединение, которое будет иметь размер максимального типа: sizeof(double) < sizeof(int) + sizeof(double).

    И небольшое напутствие на будущее: не стоит считать себя сколь угодно серьезным разработчиком и пытаться давать экспертные заключения, если все ваши приложения состоят из одного файла (а если это файлы по 4000+ строк - стоит обратиться к специалисту...)).


  1. BitLord
    23.02.2022 17:19
    +1

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


    1. Bunikido Автор
      23.02.2022 17:22

      Спасибо =)