С/С++ Portal | Программирование
15.4K subscribers
1.3K photos
220 videos
26 files
902 links
Присоединяйтесь к нашему каналу и погрузитесь в мир для C/C++-разработчика

Сотрудничество, реклама: @devmangx

Менеджер: @Spiral_Yuri

РКН: https://clck.ru/3Foc4d
Download Telegram
Твои переменные в C++ выглядят как cache_size_in_kb или limit_in_mb?

Пользовательские литералы в C++ (UDL), начиная с C++11, делают такие объявления заметно чище:

- Читаемость: 8_mb вместо строки из случайных цифр
- Компонуемость: 8_mb + 512_kb
- Нулевые накладные расходы: вся математика выполняется на этапе компиляции через constexpr
- Отсутствие магических чисел: 8_mb явно задаёт смысл вместо числа 8388608
- Безопасность: можно комбинировать со static_assert, чтобы ловить несовпадения единиц измерения до сборки

👉 @Cpportal
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥11👀53
Реализовали microGPT от Andrej Karpathy с нуля на чистом языке Си: https://github.com/vixhal-baraiya/microgpt-c

Работает на 2.6 млн токенов/с на процессоре Ryzen 5 5600H.

Оптимизация через AVX2 интринсики, ядра скалярного произведения фиксированной ширины (dot16, dot4, dot64) и быструю аппроксимацию экспоненты Шраудольфа.

👉 @Cpportal
Please open Telegram to view this post
VIEW IN TELEGRAM
🤯108
Можно создать файл размером 4 ГБ в Linux, который почти не занимает места на диске.

В системе он отображается как файл на 4 ГБ. Можно читать любые смещения — в ответ будут нули. При этом реальные блоки на диске не выделяются, пока в файл не начнут записывать данные.

Размер файла — 4 ГБ.
Фактическое использование дискового пространства — около 0.
Оба значения корректны одновременно.

Это называется разрежённый файл (sparse file).

👉 @Cpportal
Please open Telegram to view this post
VIEW IN TELEGRAM
12👍7🤔3🥱2
В C есть синтаксис, позволяющий передавать структуру напрямую в функцию без предварительного создания временной переменной.

Во многих легаси-кодовых базах на C до сих пор пишут 4–6 строк шаблонного кода только ради однократного вызова одной функции.

👉 @Cpportal
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥12🥱73👍2
Одно ключевое слово в C сообщает компилятору: эти указатели никогда не пересекаются.

Без него компилятор обязан считать, что любой указатель может алиасить одну и ту же область памяти, и поэтому оптимизировать консервативно.
С ним gcc/clang могут безопасно векторизовать циклы, переупорядочивать загрузки/записи, разворачивать итерации и генерировать SIMD-инструкции.

Это ключевое слово — restrict.

👉 @Cpportal
Please open Telegram to view this post
VIEW IN TELEGRAM
9👀5🔥4🥱1
Ваш процессор хранит 0x12345678 в памяти как 78 56 34 12.

Само значение не изменилось — изменился порядок байтов. Это называется little-endian.

Большинство современных процессоров (x86, ARM) работают именно так.

👉 @Cpportal
Please open Telegram to view this post
VIEW IN TELEGRAM
🥱177👍3👎1👀1
Макрос min() в Linux kernel — один из самых тщательно спроектированных макросов вообще.

Стандартный вариант в C:

#define min(a,b) ((a)<(b)?(a):(b))


вычисляет a или b ДВАЖДЫ. Если передать min(x++, y), получите двойной инкремент.

В kernel для этого используется GCC-расширение — statement expressions.

👉 @Cpportal
Please open Telegram to view this post
VIEW IN TELEGRAM
🤔19🔥64👍2😁1
Однажды компилятор C будто бы опроверг Великую теорему Ферма.

Теорема Ферма утверждает, что для целых чисел невозможно:
a³ + b³ = c³

Её доказал Эндрю Уайлс в 1995 году спустя 358 лет после формулировки.

Но позже Джон Рейгер показал цикл, скомпилированный через gcc -O2, который выводил:

“Fermat's Last Theorem has been disproved.”

Проблема была не в математике, а в undefined behavior.

Вот что на самом деле увидел gcc.

В цикле нет I/O и нет volatile. Стандарт C11 разрешает компилятору предполагать, что такой цикл может завершиться.
Поэтому gcc может считать, что while(1) не обязательно бесконечный. Но проблема была глубже — в undefined behavior.

Переполнение signed int в aaa — это UB. Компилятор исходит из того, что такого переполнения никогда не произойдёт.
С этим допущением он может трансформировать программу так, что логика поиска полностью ломается.

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

Ошибка была в коде, который полагался на undefined behavior.

// что gcc увидел при -O2:
// в цикле нет volatile, нет I/O, нет атомарных операций
// стандарт C11 разрешает считать, что цикл может завершиться
// следовательно: если fermat() завершится, то вернёт только 1
// следовательно: можно сразу вернуть 1
// что примерно сгенерировал компилятор:
int fermat() { return 1; } // не гарантируется, но допустимо при UB


Потом Рейгер решил проверить компилятор. Он добавил printf в конец программы, чтобы вывести найденный контрпример: a b c.

После этого gcc снова начал выполнять реальные вычисления, потому что теперь у программы появился наблюдаемый вывод.
Часть агрессивных оптимизаций исчезла.

Блеф перестал работать.

Позже Рейгер написал:

> “I got the feeling these tools like Fermat himself had not enough room in the margin to explain their reasoning.”

И проблема решается не простым добавлением volatile.
Настоящее решение — не допускать undefined behavior, например переполнения signed int.

volatile лишь запрещает часть оптимизаций, но не исправляет саму ошибку.

// Приём 1: добавление printf заставляет компилятор выполнить вычисления
printf("%d %d %d\n", a, b, c); // теперь компилятор должен их вычислить

// Приём 2: volatile ограничивает оптимизацию
volatile int a = 1, b = 1, c = 1; // чтение теперь — наблюдаемый побочный эффект

// Этот же баг часто встречается во встраиваемой прошивке
// Цикл ожидания без volatile удаляется при -O2
// Процессор пропускает ожидание, что приводит к нерабочему поведению


👉 @Cpportal
Please open Telegram to view this post
VIEW IN TELEGRAM
👍1811🌚3
Одна C-функция положила 10% всего интернета в 1988 году.

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

аспирант по имени Robert Morris использовал это для переполнения fingerd и инъекции шеллкода. червь разошёлся по 6000 машин за несколько часов.

👉 @Cpportal
Please open Telegram to view this post
VIEW IN TELEGRAM
🤯17👍63👀2
gcc позволяет запускать код ещё до старта main().

attribute((constructor)) помечает функцию, которая выполняется на этапе загрузки
attribute((destructor)) выполняется после возврата из main()

так shared-библиотеки инициализируют себя при вызове dlopen()

__attribute__((constructor)) void before_main() {
printf("this runs before main\n");
}

int main() {
printf("this runs second\n");
return 0;
}


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

меньшее число выполняется раньше, поэтому если две библиотеки регистрируют конструкторы, функция с приоритетом 101 отработает до 102.

glibc резервирует всё ниже 100 под себя
туда лучше не лезть

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

__attribute__((constructor(101))) void first() {
printf("runs first\n");
}

__attribute__((constructor(102))) void second() {
printf("runs second\n");
}

int main() {
printf("runs last\n");
return 0;
}


суть работы LD_PRELOAD как раз в этом механизме загрузки разделяемых библиотек.

ты подгружаешь .so в процесс, её конструкторы выполняются до входа в main(), и к моменту старта пользовательского кода таблица символов уже переопределена.

из-за этого можно перехватывать malloc, open, read и любые функции из libc, не модифицируя сам бинарник.

👉 @Cpportal
Please open Telegram to view this post
VIEW IN TELEGRAM
21👍9🔥3👀1
Кто-то спросил у Биджа, как работают сокеты в C. Ему надоело объяснять это снова и снова. Поэтому в 1995 году он просто выложил всё в интернет.

С тех пор это главное руководство по сетевому программированию на сокетах уже 30 лет.

Там есть всё: TCP, UDP, IPv4, IPv6, неблокирующий ввод-вывод, select(), poll().

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

Оно бесплатное. И таким останется всегда.

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

Beej буквально показывает, что происходит внутри connect(), accept(), send(), recv().

Как только понимаешь это на C — понимаешь сокеты в любом языке навсегда.

// минимальный TCP-сервер по гайду Beej

int sockfd = socket(AF_INET, SOCK_STREAM, 0);

struct sockaddr_in addr = {
.sin_family = AF_INET,
.sin_port = htons(8080),
.sin_addr.s_addr = INADDR_ANY
};

bind(sockfd, (struct sockaddr*)&addr, sizeof(addr));
listen(sockfd, 10);

int client = accept(sockfd, NULL, NULL);

send(client, "hello\n", 6, 0);

// nginx, redis и большинство серверов начинают с этого паттерна


Зацени. Оно бесплатное и таким останется всегда.
http://beej.us/guide/bgnet

👉 @Cpportal
Please open Telegram to view this post
VIEW IN TELEGRAM
21🔥4
Базовый курс по аллокаторам памяти — реализация простого распределителя памяти

Эта статья про написание простого аллокатора памяти на C.
Ты реализуешь malloc(), calloc(), realloc() и free().

👉 @Cpportal
Please open Telegram to view this post
VIEW IN TELEGRAM
12👍2
Есть вещь, которая каждый раз цепляет, когда об этом задумываешься: целое число в Python занимает 28 байт памяти. В C int занимает 4.

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

Миллион целых чисел в Python — это около 28 МБ памяти.
Те же данные в C — около 4 МБ.

👉 @Cpportal
Please open Telegram to view this post
VIEW IN TELEGRAM
19🏆3👍2
printf сам по себе он ничего не выводит

printf — это по сути всего три строки, и на этом вся функция заканчивается

/* glibc stdio-common/printf.c — вся функция принтф */

int __printf(const char* format, ...) {
va_list arg;
int done;

va_start(arg, format);
done = vfprintf(stdout, format, arg); // вся реальная работа здесь
va_end(arg);

return done;
}

/*
printf — это обёртка, а всю работу делает vfprintf
внутренняя реализация vfprintf в glibc — больше 2400 строк
для функции, которую ты вызываешь в своей первой программе
*/


vfprintf — это место, где реально разбирается форматная строка, это конечный автомат

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

printf("%05.2f", 3.14) — это просто цикл с большим оператором switch, и он не пишет напрямую в терминал, а записывает в буфер, который принадлежит stdout

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

/* упрощено из внутреннего парсера vfprintf в C, основной цикл разбора */
while (*f != '\0') {
if (*f != '%') {
/* копирование литеральных символов в буфер */
f = strchrnul(f, '%'); // использует векторные инструкции (SIMD) для поиска следующего '%' или конца
} else {
/* разбор флагов, ширины, точности, модификаторов длины и преобразования */
switch (conv) {
case 'd': /* формат: знаковое целое */ break;
case 's': /* формат: строка */ break;
case 'f': /* формат: число с плавающей точкой */ break;
/* больше вариантов */
}
}
}
/* strchrnul использует векторные инструкции для сканирования нескольких байтов за раз */
/* даже сканирование форматной строки оптимизировано */


// поэтому printf без переноса строки иногда ничего не выводит

stdout использует построчную буферизацию при подключении к терминалу, поэтому отсутствие '\n' не вызывает сброс буфера и вывода

но если перенаправить stdout в файл, он становится полностью буферизованным, и ничего не выводится до завершения программы или вызова fflush

это десятилетиями сбивало с толку разработчиков, и причина всегда в режиме буферизации

printf("hello"); // может не появиться сразу
printf("hello\n"); // вызывает сброс и выводится сразу
fflush(stdout); // или явный сброс


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

👉 @Cpportal
Please open Telegram to view this post
VIEW IN TELEGRAM
20🔥13🤯3🌚2
В C++ деструкторы появились ещё в 1985 году, а разработчики на C десятилетиями писали goto cleanup и хотели получить что-то вроде RAII.

GCC добавил похожий механизм для C ещё в 2003 году.

__attribute__((cleanup)) вызывает указанную функцию в момент, когда переменная выходит из области видимости — в любом scope и при любом пути выхода из функции, включая ранние return.

Этим уже пользуются такие проекты, как libvirt и QEMU, а Linux kernel принял этот подход в 2023 году.
И всё это уже много лет поддерживается как в GCC, так и в Clang.

static void freep(void **p) { if (*p) { free(*p); *p = NULL; } }
static void fclosep(FILE **f) { if (*f) { fclose(*f); *f = NULL; } }

int process_file(const char *path) {
__attribute__((cleanup(fclosep))) FILE *f = fopen(path, "r");
__attribute__((cleanup(freep))) char *buf = malloc(4096);

if (!f || !buf) return -1; // both freed automatically

fgets(buf, 4096, f);
printf("%s", buf);

return 0; // fclose and free run automatically here
}


В libvirt на базе этого был построен полноценный RAII-подобный механизм на C, где с помощью макроса заменили большинство шаблонов очистки через goto cleanup.
Приоритет конструкторов имеет значение в экосистеме Linux, поскольку glibc резервирует приоритеты от 0 до 100, поэтому пользовательские конструкторы должны использовать приоритет 101 и выше.

/* cleanup.h из Linux kernel: показывает, как ядро использует это начиная с v6.2 */
#define DEFINE_FREE(_name, _type, _free) \
static inline void __free_##_name(void *p) \
{ _type _T = *(_type *)p; _free; } /* достаём значение и вызываем функцию освобождения */

#define __free(_name) __attribute__((__cleanup__(__free_##_name)))

DEFINE_FREE(kfree, void *, kfree(_T)) /* автоосвобождение памяти через kfree */
DEFINE_FREE(fput, struct file *, fput(_T)) /* автоосвобождение ссылки на file */

/* пример использования в реальном коде ядра */
struct file *f __free(fput) = fget(fd);

/* когда переменная f выходит из области видимости,
fput(f) вызывается автоматически */


👉 @Cpportal
Please open Telegram to view this post
VIEW IN TELEGRAM
9🔥2🤔1👀1
«Введение в реализацию стека протоколов TCP/IP с нуля» объясняет реализацию на языке C, но автор заново реализовал тот же материал на Rust: https://github.com/pandax381/microps-rs

Он реализовал всё в 30 шагов, как и в версии на C, стараясь максимально точно следовать структуре книги.
Обязательно используйте это как дополнительный материал 🫡🫡

👉 @Cpportal
Please open Telegram to view this post
VIEW IN TELEGRAM
👎75👍3👀2🔥1
Каждый процесс в Linux получает собственное виртуальное адресное пространство.

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

👉 @Cpportal
Please open Telegram to view this post
VIEW IN TELEGRAM
Please open Telegram to view this post
VIEW IN TELEGRAM
9👍2👀1
Атомарные операции встроены в современный C через заголовок <stdatomic.h>.
Компилятор может транслировать эти операции в нативные атомарные инструкции процессора, что позволяет выполнять потокобезопасные (thread-safe) обновления без использования мьютекса (mutex) для простых счётчиков, разделяемых между потоками.

👉 @Cpportal
Please open Telegram to view this post
VIEW IN TELEGRAM
👍105🔥3
Наследование в C можно реализовать с помощью одного простого правила — сделать родительскую struct первым полем.

C гарантирует, что указатель на struct имеет тот же адрес, что и её первый член, поэтому приведение указателей между ними будет работать корректно.

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

👉 @Cpportal
Please open Telegram to view this post
VIEW IN TELEGRAM
👍19🥱64
Большинство пользовательских указателей (user-space pointers) в Linux на x86-64 используют только 48 бит адреса.

Некоторые рантаймы временно хранят метаданные в старших битах, а затем очищают (маскируют) их перед повторным разыменованием указателя. Эта техника называется tagging указателей (pointer tagging).

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

👉 @Cpportal
Please open Telegram to view this post
VIEW IN TELEGRAM
9👍2🔥2🤔1🤯1