Comments 169
А как же 1[arr]
?
auto int x; в C и старом C++ и auto idx = 1 в современном C++ - совсем не одно и то же.
int *dyn_arr = (int*)malloc(... Привидение типа в C здесь вообще лишнее, void* приводится к любому указателю.
Стоило упомянуть, что в C функция, объявленная без параметров принимает произвольное число аргументов.
1[arr]
- я рассказывал про это явление:
Индекс с массивом, но вверх ногами
Простой пример:
int arr[ARR_SIZE], i = ARR_SIZE-1;while (i>=0) {i--[arr] = i+1;}
Возьму на заметку.
Здесь я сделал это для наглядности. На самом деле как по мне каст увеличивает читабельность подобных записей:
char** mat = (char**)malloc(...
Если в C++ это необходимость, то в С это на вкус и цвет, и всё равно этот каст никому не навредил =) (Разве что исходный код будет весить на пару байт больше)
Добавил в раздел Функции с переменным количеством параметров, спасибо
Каст malloc
в C (и вообще, явные касты из void *
) -- вредная практика. Пример:
#include <stdio.h>
int *foo(void) {
return (int *)malloc(10);
}
Здесь функция malloc
не объявлена (она объявляется в <stdlib.h>
, а не в <stdio.h>
). По умолчанию в отсутствие объявления будет считаться что функция возвращает int
, и результат будет непредсказуем. Конечно, компилятор, скорее всего, выдаст предупреждение, но, если бы каста не было -- была бы более наглядная ошибка.
Спасибо, буду знать
А как без кастов делать?
А как без кастов делать?
А тут каст и не нужен. По стандарту, указатель void *
автоматически приводится к любому указателю (за исключением указателя на функцию) -- так же как можно, скажем, константу 42
(без суффиксов тип int
) присвоить переменной типа long
без всяких кастов.
char foo(void) {
return (char )malloc(3);
}
Или так:
int foo(void) {
return (int )malloc(3);
}
И оно скомпилируется (с варнингами), и даже как-то запустится. А если привести к указателю — возможно, даже будет работать более-менее правильно с некоторыми данными, за счет того, что malloc выделит не ровно 3 байта, а выровненный по каким-то границам кусок. ;)
int brr[100] = {0};
int *arr = brr+50;
arr[-1] = 3;
Есть ли здесь "undefined behaviour" ?
По стандарту, arr указывает на 51й элемент, то есть валиден всего для четырех байтов.
Код содержит UB, потому что эксплуатируется особенность компилятора не проверять границы адресов для указателей. arr[-1] указывает на последний байт 50го элемента, чего быть не может.
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
вроде, соответствует написанному в статье
Код на си иногда выводит правильный результат как доказательство что он корректен? Вы серьезно?
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
Указатель в Си — это не адрес памяти. Это просто абстракция.
Как вышло?
Вы, используя указатель на a2
, поменяли значение в массиве a1
. Естественно, это будет UB.
оптимизация? я про адреса
Указатель в Си — это не адрес памяти. Это просто абстракция.
Брайан Керниган писал что это именно адрес в памяти.
Брайан Керниган писал что это именно адрес в памяти.
Спасибо деду за победу, конечно, но мог бы еще тогда догадаться что:
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]);
Много бы крови сэкономил
Assertion failed!
Program: C:\Users\Random_User\ \a.exe
File: tmp.c, Line 10Expression: i1 == 3
Дальше программа естественно не выполняется.
А что не так? Вы выполнили переход к области памяти, в которой никто не знает что лежит: arr[-2]. Конечно там не может быть 3.
Ну так да, диды не догадались сделать отрицательные индексы — индексацией с конца. Как это сейчас во многих языках, даже в том же пхп.
Тогда бы они еще тогда поняли, что вместе с массивами и указателями на них, надо таскать их размер (хотя бы во время компиляции).
Я имел в виду, что arr[-2] и arr[3] в случае массива из 5 элементов указывали бы на одинаковый элемент.
Скажете перф? Только вот из-за этого в си невозможны аггрессивные оптимизации, когда сразу инвестно что указатели не пересекаются. Всякие расты оказываются быстрее си, как раз благодаря тому что гарантируют что уникальные ссылки уникальны (не пересекаются).
А современные компиляторы все равно надеятся на strict aliasing даже в дефолтных настройках (-О3), что я вам и показал.
Я имел в виду, что arr[-2] и arr[3] в случае массива из 5 элементов указывали бы на одинаковый элемент.
До сих пор не понимаю как так-то
Вот массив:
{0 1 2 3 4}
Вот i1 = arr[-2]:
x x {0 1 2 3 4}
^
i1
То есть мы взяли какую-то другую неизвестную нам память. Так каким образом тогда мы получим arr[3]?
Я не про историческую реализацию си, а про то как надо было делать (легко говорить из будушего).
https://www.php.net/manual/ru/function.substr.php
Положительный индекс — считать с начала, отрицательный — считать с конца.
Положительный индекс — считать с начала, отрицательный — считать с конца.
Полный бред =) Тогда смысл этих "отрицательных" индексов если можно написать просто:
extern int arr[];
extern size_t i;
... = arr[ARR_SIZE-i];
Ох уж эти ленивые пхпшники... Мы Сишники, с нами стандарт!
extern size_t i;
ptrdiff_t
... = arr[ARR_SIZE-i];
error: ARR_SIZE undefined
Мы Сишники, с нами стандарт!
Крииинж %)
https://gcc.gnu.org/bugzilla/show_bug.cgi?id=30475
Почему сишникам меньше всего платят тогда?
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;
}
Крииинж %)
Ох уж эти ваши тиктоки
Почему сишникам меньше всего платят тогда?
Откуда такая информация? C занимает 2 место по популярности по рейтингу TIOBE
>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
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. Это так?Ну так да, диды не догадались сделать отрицательные индексы
И не зря. В каком-нибудь высокоуровневом питоне это можно сделать почти бескровно, там и так все еле ворочается. Ну подумаешь, еще и длину массива хранить. А в довольно низкоуровневом Си так делать нельзя. Если у вас массив статический, то еще худо бедно компилятор может помнить какой же размер этого массива. А вот если это указатель, переданный в функцию. Или, прости господи, нуль-терменированная строка? Таскать везде еще и длину массива — лишние накладные расходы. Если кому надо с конца считать, то пусть сами где-то длину массива протащат и в квадратных скобочках перед минусом вставят.
А в довольно низкоуровневом Си так делать нельзя.
Но приходится, так как сразу же оказалось, что можно сделать оптимизации, когда регионы памяти не пересекаются, но си этого гарантировать не может, пришлось подпирать костылями вроде restrict, генерировать два варианта кода и прочий бред
Ну да. Если программист хочет использовать эту фичу, он пишет restrict. Также как с выходом за границы массива. Программист сам проверяет, а не компилятор не спрашивая пихает везде проверки.
Если программист хочет использовать эту фичу, он пишет restrict.
Чтобы ее юзать, о ней надо знать, а даже если знаешь, то надо доказывать что регионы и правда не пересекаются.
Как показывает практика — это интересно целым 0 разработчикам. (поискал сейчас в кодовой базе в одной очень крупной российской софтовой компании, нашел одно упоминание restrict в библиотечном коде).
Ни разу не программист на C, но про UB интересно стало.
Вот моя реализация:
#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>
#include <stdlib.h>
bool doesOverlap(int *arr1, int size1, int *arr2, int size2) {
uintptr_t a1 = (uintptr_t) arr1;
uintptr_t a2 = (uintptr_t) arr2;
if (a1 > a2) {
a1 ^= a2;
a2 ^= a1;
a1 ^= a2;
size1 ^= size2;
size2 ^= size1;
size1 ^= size2;
}
return a2 < a1 + size1 * sizeof(int);
}
int main() {
int* arr = (int *) malloc(10 * sizeof(int));
int* arr2 = (int *) malloc(10 * sizeof(int));
printf("%d\n", doesOverlap(arr, 10, arr2, 10));
return 0;
}
Пока знакомился с темой узнал, что если указатели указывают на разные объекты, то их нельзя сравнивать с помощью <
, >
, >=
, <=
. (Ведь мы как раз и хотим узнать, указатели на один и тот же объект или нет). Поэтому нам нельзя использовать эти операторы. Можно лишь использовать ==
и !=
. Но тогда придётся пройтись по всем возможным значениям, чтобы выяснить, есть ли совпадение.
bool doesOverlap(int *arr1, int size1, int *arr2, int size2) {
bool isOverlap = false;
for (int i = 0; i < size1; i++) {
for (int j = 0; j < size2; j++) {
isOverlap |= (arr1 + i) == (arr2 + j);
if (isOverlap) break;
}
if (isOverlap) break;
}
return isOverlap;
}
Можно лишь использовать
==
и!=
И эти тоже не сработают. Я выше привел пример. В сети еще куча вариантов (надо только погуглить).
А разгадка одна: указатель в си - это не адрес в памяти.
Забавная ситуация, кстати
#include <stdio.h>
#include <stdint.h>
int main(void) {
int a, b;
int *p = &a + 1;
int *q = &b;
printf("%p %p %d %d\n", (void *)p, (void *)q, p == q, (uintptr_t) p == (uintptr_t) q);
return 0;
}
Компилировал с флагом -O2
, вывод: 0x... 0x... 0 1
Может быть, стоит использовать uintptr_t
, чтобы написать функцию doesOverlap
?
костылями вроде restrict
Не костыль, а принятый стандартом С99 инструмент
Про статический массив и VLA — да, но только в текущей функции. Уже передать в другую функцию — надо не только указатель передавать, но и размер. Теоретически, компилятор мог бы это как-то сам сделать, но это не всегда надо. Вот и оверхед.
Что касается маллока, то размер есть у системы. Вы уверены, что рантайм Си хранит этот размер? Зачем ему копию у себя поддерживать? Она не всегда нужна. А есть этот размер у системы, потому что нельзя доверять пользователю системного апи тащить его от malloc'а до free из соображений безопасности. Потому что можно передать в free больше, чем вы запросили и потом утащить память другого процесса.
Как раз необходимость протаскивать самому размер массива — оверхед
Нет. Вы или протаскиваете его сами, если оно вам надо, или он не нужен и он нигде не протаскивается (как, например, с нуль-терминированными строками). Альтернатива, когда рантайм его помнит и как-то везде протаскивает, даже если оно не используется — это и есть ненужная работа, или как я ее назвал — оверхед.
Ну вообще, теоретически, компилятор мог бы сам определять, а используется размер или нет и как оптимизацию не передавать туда, где не надо. Но во времена дизайна Си это было наверно не реализуемо. Да и философия не в том, что оптимизатор потом все лишнее уберет, а не вводить ничего лишнего, если не надо.
Что касается маллока, то размер есть у системы. Вы уверены, что рантайм Си хранит этот размер?
Да, размер есть именно у аллокатора, который является частью рантайма. На каждую аллокацию маппить страницу памяти в здравом уме никто не будет. И ровно поэтому же система ничего не будет знать.
Ну вообще, теоретически, компилятор мог бы сам определять, а используется размер или нет и как оптимизацию не передавать туда, где не надо. Но во времена дизайна Си это было наверно не реализуемо. Да и философия не в том, что оптимизатор потом все лишнее уберет, а не вводить ничего лишнего, если не надо.
И именно это в современных реалиях осложняет как написание на Си безопасного и эффективного кода, так и его хорошую оптимизацию.
А что до "философии", то уже много лет оптимизирующие компиляторы с ней упорно борются, удаляя или эквивалентно заменяя то самое "лишнее". И судя по бенчмаркам и то и дело всплывающим историям с "оптимизациями UB", делают это они довольно успешно
И именно это в современных реалиях осложняет как написание на Си безопасного и эффективного кода, так и его хорошую оптимизацию.
this.
Раст (и его потомки) показали на практике, что безопасность еще и может приносить перф, причем такой, что можно си обогнать. И все это без боли и страданий, автоматически.
Ну, то есть, да, в С как он написан это действительно невозможно, потому что типы не позволяют.
Таки позволяет, начиная с C99 (описано в блоке, который начинается со слов "In function parameter lists, additional syntax elements are allowed within the array declarators"):
void accept(int num[static 5]) {
return;
}
void foo() {
int arr[] = {1, 2, 3, 4, 5};
accept(arr); // ок
}
void bar() {
int arr[] = {1, 2, 3};
accept(arr); // предупреждение от компилятора
}
Правда, это не мешает передать массив размера большего, чем требует функция :/
Эх, я к этому и подводил, но что-то надоело болтать со студентами. Последнее время много от них статей стало, действительно что ли зачеты ставят за это...
Только вот из-за этого в си невозможны аггрессивные оптимизации, когда сразу инвестно что указатели не пересекаются.
Почему же? Есть такое ключевое слово, которым гарантируется что данный указатель является единственным способом получения доступа к объекту:
static void foo (int * restrict p1, const void * restrict p2);
Здесь указатели p1 и p2 могут быть «агрессивно» оптимизированы так как программист гарантирует что они ни с чем не пересекаются
https://godbolt.org/z/ah46avxb6
поломалось, в выводе указатели отличаются, хотя код не изменен. Т.е. у вас настоящее UB
3, 0
0x7ffe1dae5124, 0x7ffe1dae5184
Мне дальше говорить не интересно, сорян.
Т.е. у вас настоящее UB
И оно наглядно показывает что "Указатель в Си — это не адрес памяти. Это просто абстракция."
Вот гляжу я на ваш ассемблированный пример (строки 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
arr[-1] указывает на последний байт 50го элемента
Абсолютно неверно. У вас тип arr
— это int *
, он будет указывать на 50-й элемент.
Никакого UB в данном случае не будет.
Указатели на функции
Серьезно? Это средство для реализации калбека. С чего это она не полезна?
Я не говорил что она не полезна. Каждому инструменту найдётся своё применение, статья о редких а не бесполезных инструментах. Конечно я не отрицаю того факта, что указатели на функции редко встречаются только на моей практике, может вы работайте в той сфере где без них никуда.
может вы работайте в той сфере где без них никуда.
Они нужны, полезны и частоиспользуемы в любой сфере, если только вы не работаете в сфере написания Hello world программ.
Еще, как пример, часто их использую при реализации конечных автоматов. Чем по свичам прыгать, проще массив с указателями на функции сделать( на мой вкус).
бОльшая часть – реально используемая.
А в примере с преобразованием указателя – вообще ошибка: в числе 0x2E6D6172676F7270 все 8 байт ненулевые, значит, после 8 символов начнёт печатать то, что дальше в памяти.
Двоичные числа в разделе с каламбуром типов относятся к примеру со строкой - это то самое длинное число только в двоичном виде, чтобы как бы показать что в памяти всё хранится одинаково.
бОльшая часть – реально используемая.
Не спорю - если вы видели много проектов с такими конструкциями, то значит вы решайте те специфичные проблемы, которые трудно было бы решить без этих инструментов. Просто здесь приведены именно они потому, что они редко используются в средне-статистических программах на С. И учитывайте:
Сначала расскажу о базовых и часто встречаемых конструкциях, которые могут и вовсе не удивить опытных программистов на С, но вот уже ниже идут всё более редкие и интересные конструкции, которые уже далеко не часто можно где-либо встретить.
Если вы опытный программист на С то могу предположить что бОльшая часть находится именно вверху
А в примере с преобразованием указателя – вообще ошибка: в числе 0x2E6D6172676F7270 все 8 байт ненулевые, значит, после 8 символов начнёт печатать то, что дальше в памяти.
Поправил формат
Временный файл, двойные указатели, указатели на функции, константные указатели - это для вас странные технологии? Разбиение строки на токены - странная технология? Вы точно пишете на C? Вы точно программируете?
strtok – странная, да. Как минимум, strtok_s, а то strtok, хранящая свой контекст неизвестно где (а мало ли между вашими вызовами будет вызов strtok из какой-то библиотеки?) – лютый зашквар.
Константные указатели находятся вверху, поэтому есть шанс что они не так уж и редко используются. Применение временному файлу я практически не нашёл, да и не особо видел. Смело присылайте свои идеи и применения, может добавлю их. Как упоминал @aamonster strtok действительно странная и нестабильная вещь. Лично я в большинстве случаев пишу свои функции для разбиения на токены.
Вы точно пишете на C? Вы точно программируете?
Тише, если бы я не писал на С и не программировал то вы бы эту статью не увидели )
Ощущение, что вы только hello world писали
strtok, действительно,имеет ограниченный спектр применения, так как требует крайне аккуратного применения, что для однопоточных программ при грамотном проектировании (большинство из coreutils), впрочем - не проблема (с strtok_s по производительности не сравнивал). tmpfile, действительно, с ходу не вспомнить, где встречалось, но идея-то понятна: можно подсунуть библиотекам, которые умеют работать только с файлами такой вот "файл". С натяжкой можно сравнить это с подходом интерфейсов Reader и Writer в Go.
Указатели на функции
И всё, что вообще с ними связано. Я думаю они не сильно полезны, максимум как аргумент для другой функции
В крупных проектах на С используются для реализации полиморфизма.
Например, в ядре Linux и 100500 мультимедиа библиотеках.
что-то мне подсказывает, что это не такие уж и редко используемые возможности языка. про них я узнал всего лишь из одной книги https://www.ozon.ru/product/yazyk-programmirovaniya-c-lektsii-i-uprazhneniya-147927650/?sh=Lrd_YwAAAA, которая рассчитана на начинающих.
Все эти фичи абсолютно необходимы для программирования "железяк". И константные указатели, и volatile, и все все все. А указатели на функции позволяют избежать switch-ей и других ненужных переборов.
Так что удивляться тут нечему, надо просто использовать С для написания быстрых и лаконичных программ.
Так что удивляться тут нечему, надо просто использовать С для написания быстрых и лаконичных программ.
Жаль, только что компиляторы си не умеют сами разбираться, какие диапазоны памяти пересекаются, а какие нет
Это как раз не жаль. Просто надо владеть, а не пользоваться. Я, например, терпеть не могу когда компиляторы начинают делать предположения о психологическом состоянти автора программы.
#undef широко используется при злоупотреблении макросами, в частности, при использовании x-macro, когда макрос с одним и тем же именем (традиционно Х, отсюда и название) разворачивается по-разному. Из безобидных примеров вспомню генерацию массива строк для enum, но вообще можно и полиморфизм реализовывать, и RAII, и много чего ещё. Дебажить, конечно, весело, но это всё равно лучше преобразования к void*.
uint64_t x = 0x2E6D6172676F7270;
printf ("%s\n", (char*)&x);
Аффтар жжот. Если в программе есть ещё переменные — printf может вывести что угодно кроме ожидаемой строки.
uint64_t x = 0x2E6D6172676F7270;
printf ("%.8s\n", (char*)&x);
Поправил - поставил ограничение на вывод максимум 8 символов строки.
uint64_t x = 0x2E006172676F7270;
Напечатает не то, что ожидалось
Правильно - у вас 7 байт другой. В оригинале он 6D
а у вас 00
.
Дело в том что uint64_t* и char* - это сильно разные обьекты с сильно разными свойствами. У одного длина фиксированная, у другого может быть любая. Не нужно приводить их к друг другу. Просто не нужно. Даже в примере из 2 строк сделать более-менее правильно ожидаемо у вас получилось со 2 раза.
Гениальное наблюдение, конечно, но вы попробуйте не просто думать (тут вам опыта не хватает), а запустить программу и посмотреть что именно она выведет.
uint64_t x = 0x2E006172676F7270;
printf ("%.8s\n", (char*)&x);
Вот моя версия:
#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 и точку. Всё верно
Вывод:
program.
(правильный вывод)
С чего это он правильный? Такой же UB'шный, как и любые другие варианты.
Осталось только запустить на машине с другим порядком байтов, другим положением старшего бита или запретом невыравненного доступа к памяти...
И главное — повторять мантру:
Я пишу на С потому что удобно - а не потому что много платят :) Это тот самый язык «что написано то и сделано»
Это тот самый язык «что написано то и сделано»
А у вас есть что возразить насчёт этого? Написал допустим + или -> и сразу можешь предсказать что будет на выходе
А у вас есть что возразить насчёт этого?
Так вы не можете написать сниппет, который работал бы на чем-то кроме х86, но утверждаете что понимаете Си.
Написал допустим + или -> и сразу можешь предсказать что будет на выходе
Это не имеет никакого значения для бизнеса.
Почему же не могу? Пишите что должен делать сниппет (только не гигантский проект) а я реализую его как кросс-платформенный
Это не имеет никакого значения для бизнеса
Я здесь не для заработка денег :)
Почему же не могу? Пишите что должен делать сниппет (только не гигантский проект) а я реализую его как кросс-платформенный
Вы себе сами выше придумали задачу: распечатать текст, который хранится в uint64. Ее хотя бы доделайте до конца.
Я здесь не для заработка денег :)
Бизнес-задача — это полезная работа, которую должен делать код. Мелочи одного единственного (причем одного из самых простых) языка для этого не играют никакого значения.
Даже если деньги не зарабатываются и это пет-проект.
Вы себе сами выше придумали задачу: распечатать текст, который хранится в uint64. Ее хотя бы доделайте до конца.
А кросс-платформенное решение я уже опубликовал в статье, вот оно краткое и лаконичное:
extern uint64_t x; //сама строка
if (!x)
return; //если строка нулевая то завершаем работу функции
do {
putchar((char)(x & 0xFF)); //печатаем байт
x >>= 8; //удаляем байт
} while (x); //пока число не станет нулем
Это немного переделанная версия чтобы не возникло претензий к стилю написания. Я буду удивлён если такой код не заработает на какой-либо платформе имеющей хорошую реализацию С. И так этот код может НЕ заработать только по следующим причинам:
Платформа не поддерживает С
Платформа не имеет файла
inttypes.h
/stdint.h
uint64_t
не хранится в памяти на этой платформе по порядку в одной ячейке и его байты раскиданы по памяти как связный список.Платформа не имеет файла
stdio.h
Не знаю как вы но платформы с выше перечисленным списком я не знаю и поэтому не могу полностью отрицать факта её существования. Можно ли такой код назвать кросс-платформенным?
Платформа не имеет 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
.
Полностью переносимый код -- это сложно. Порой, непереносимо сложно.
А почему вы, собственно, решили, что порядок байт будет именно LE, а не BE? Стандарт это никак не регламентирует и ваш >> окажет совсем не тот эффект, что вы ожидаете.
А кросс-платформенное решение я уже опубликовал в статье, вот оно краткое и лаконичное:
ваш вариант с
printf ("%.8s\n", (char*)&x);
тоже такой же кросс платформенный, но имхо более понятный, так как не потребует рефакторинга.
Единственная проблема в обоих решениях это BE/LE, а это можно обойти директивой препроцессора, каким-нибудь #ifdef__BYTE_ORDER__
, или compile-time проверкой через определение положения не нулевого байта в переменной int32 == 1.
Там, где запрещен невыравненный доступ к памяти этот код может упасть.
Также, этот код не соберется каким-нибудь IAR c включенными MISRA. Ну и главное - этот код не пройдет сертификацию у более-менее серьезных людей.
Какую из переменных, в упоминаемом мной коде вы считаете не выровненым?
Код не упадет, так как переменная uint64_t x выровнена естественным образом, и в printf передается адрес на эту переменную. А как внутри printf уже будут обрабатываться эти байты не очень важно.
уверен что и иар с MISRA норм соберет, но наверно для самой printf возникнет нюанс, какое-то из правил касалось переменного кол-ва аргументов
Написал допустим + или -> и сразу можешь предсказать что будет на выходе
undefined behavior in c. 100500 примеров.
Спасибо за комментарий!
Если это программа на Си — то она достаточно близка к железу
Я тут рассказывал преимущественно про ту сферу которой я занимаюсь - разработка на С под 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) — термин, который используется в информатике для обозначения различных техник нарушения или обмана системы типов некоторого языка программирования, имеющих эффект, который было бы затруднительно или невозможно обеспечить в рамках формального языка. Вот я здесь и обманул систему типов.
Это плохо, не делайте так
Я то не делаю - просто рассказываю
Если у вас исходная переменная уже является указателем, то вам нужен еще один чтобы её поменять.
Я это знаю. Почитайте мои комментарии, я об этом писал:
int **p; /* это всё ещё переменная, а может быть двумерный массив. А может надо изменить указатель через его адрес */
А что, в ардуино по-другому бывает?
Ардуино создавалась как лайтовая платформа для практики электроники (как конструктор). Библиотеки на любой вкус и цвет. Там конечно можно и на ассемблере писать, но зачем если есть прекрасный средне-уровневый С который в каждой микроволновке работает. Если интересно как работают железяки и процессоры на низком уровне и хочется разбираться то как по мне лучше сразу брать какой-нибудь MOS 6502 и писать на нём на чистом ассемблере. Правда у меня его пока нет =)
А уж про саму оболочку вообще ничего нематерного сказать не смогу, это вообще какой-то конец 90х в плане комфорта и эффективности разработки
Ну да среда и правда сомнительная но наоборот заточена под комфорт как бы это парадоксально не звучало. В Arduino IDE (если вы о нём говорите) как по мне удобное решение - простой редактор, две кнопки для прошивки и компиляции. А что именно вам пришлось не по вкусу?
И учить с ассемблера я бы никому не рекомендовал начинать, есть предел того, куда стоит лезть с отсутствием понимания, на данный момент и Си работает отлично
Вот это да но почему никому? Если человек работает и прошивает или пишет прошивки для железа он хочет чтобы программа работала настолько быстро насколько это возможно в теории. Я не спорю - Си это один из немногих языков где можно писать дичь - while($>>=x^28+~10) f("89!"+op<<1);
, и на том же языке можно писать вполне себе читабельные высокоуровневые программы. А если захочется - с помощью инструментов языка можно вставить ассемблерный код напрямую в С - тогда компилятор его компилировать не будет. Вот это я понимаю комбо =)
Си работает отлично
Несомненно. Правда иногда задумываешься - какую следующую коварную оптимизацию применит компилятор чтобы испортить твою программу?)
Если человек работает и прошивает или пишет прошивки для железа он хочет чтобы программа работала настолько быстро насколько это возможно в теории
Нет конечно, скорость даже самых дохлых современных микроконтроллеров достаточна. Людям важна корректность.
В >50% случаев в финальные устройства заливаются прошивки скомпилированные в дебаге, о чем речь вообще?
Да я писал под Attiny13, Attiny45, Arduino Nano/Uno но не разу не использовал volatile. Не отрицаю факта что он используется в сложных комплексных проектах
Как-то писал совсем небольшой проект на ATTiny13. Так вот, volatile просто необходим при работе с железом. Смысл его в том, что данный модификатор запрещает компилятору оптимизировать строки с доступом к переменной, объявленной как volatile. Например:
char working = 1;
void main(void)
{
while (working)
{
// do something
}
}
Имея такой код компилятор может запросто подставить 1 вместо обращения к переменной и дать на выходе бесконечный цикл. Но что, если мы хотим изменять значение в прерывании? Компилятор по-прежнему не знает, что эта переменная могла измениться, ведь она не изменяется нигде до чтения. Таким образом, без volatile изменение переменной в прерывании может быть проигнорировано, а с ним - не будет. Насчёт конкретно глобальных переменных уже точно не помню (давно дело было), но кто нам мешает глобально хранить лишь указатель на вполне локальную переменную? Надеюсь, смысл использования volatile теперь стал более прозрачен.
Зато вопрос про volatile на собеседовании позволяет сделать хороший отсев)
Конечно претензий к статье много, но спасибо автору за то, что вызвал бурную дискуссию в комментариях))) многим будет полезно
volatile
Иногда компилятор может оптимизируя удалять целые куски кода. Чтобы такого не было желательно ставить ключевое слово volatile перед объявлением переменной, если вы хотите чтобы компилятор 100% не применил свои коварные оптимизации к этой переменной. (Хорошо что слово volatile не может игнорироваться, как иногда бывает с register, а то в этой жизни пришлось бы больше ни в чём не быть уверенным)
Ну сколько можно транслировать эту ошибку? volatile говорит о том, что переменная может измениться в другом месте (потоке / прерывании / аппаратном регистре).
Про указатели на указатели на указатели… Не могу не поделиться своей болью, хоть это и C++. Сигнатура MFEnumDeviceSources:
HRESULT MFEnumDeviceSources(
[in] IMFAttributes *pAttributes,
[out] IMFActivate ***pppSourceActivate,
[out] UINT32 *pcSourceActivate
)
Туда передается указатель на место, куда будет записан массив указателей на интерфейсы...
Вообще winapi полно подобных перлов.
не похоже на С++ (особенно in out)
Это типа комментарии на docs.microsoft.com
Интересная идея определить такие имена:
#define IN
#define OUT
А потом при объявлении функции с указателями в качестве аргументов указывать что именно она с ними делает:
void foo(IN const uint8_t * const some_input, OUT uint8_t *some_output){...}
Вот ещё одна полезная штука при инициализации массива. В некоторых ситуациях очень удобно, но почему-то не все об этом знают.
int x[ 10 ] = { [ 0 ] = 42, [ 1 ] = 22, [ 2 ] = 73 /*и так далее, можно даже в любом порядке*/ }
Как по мне просто удлиняет программу (или по крайней мере пример такой).
Пост выглядит как «хипстеры познают язык C».
А это неплохое название кстати. Я сам новичок в С - прямиком из Паскаля. Может поэтому для опытных программистов С с опытом >10 лет этот пост может показаться странным.
Вот ещё одна полезная штука при инициализации массива. В некоторых ситуациях очень удобно, но почему-то не все об этом знают.
Глядя на ваш пример сразу вспомнилась такая конструкция:
return (vec2){
.x = 10.0f,
.y = 62.0f
};
Не знаю правда, можно и вернуть структуру традиционным путём:
return (vec2){10.0f, 62.0f};
Но кажется с именами компонентов более наглядно. Но то, что такое и к массивам применимо - не знал. Спасибо за приём осталось придумать как его использовать =) Ну разве что когда хотите не по порядку элементы указывать:
int x[ 10 ] = { [ 1 ] = 21, [ 0 ] = 30 ...
Интересно а так вообще можно делать? Надо будет проверить. В случае со структурами знаю что 100% можно менять поля местами.
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: Когда перечисление большое, можно облажаться с количеством элементов. Особенно при добавлении новой строки. А при такой инициализации можно случайно пропустить один элемент, и это плохо, но хотя бы не поедут все остальные. Короче, мне удобно. Как и инициализация с указанием имени члена структуры.
Ага, понятно теперь. То есть таким образом можно не запутаться в коде.
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
называть с одного и того-же слова? Если да то возьму на заметку - нередко такое видел в больших программах.
А ещё какая-то у вас не сильно понятная форматировка (я привык что запятые ставятся на текущей строке, а не на следующей) - а у вас тут такие записи:
, .k = 15.3846153
, .b = -2076.923 }
Немного запутывает, также как и static
перед массивом строк. У вас программа много-файловая?
о том, что часть конструкций языка непонятно для чего нужны и их никто не использует
У вас абсолютное непонимание смысла статьи. Вот:
Сначала расскажу о базовых и часто встречаемых конструкциях, которые могут и вовсе не удивить опытных программистов на С, но вот уже ниже идут всё более редкие и интересные конструкции, которые уже далеко не часто можно где-либо встретить.
Где здесь упоминание что они не для чего не нужны и их никто не использует?
зачем вы написали этот пост
Я захотел попробовать себя в чём-то новом. Я уже давно читаю Хабр и решил тоже присоединиться - делиться и получать опыт. И несмотря на критику я буду продолжать это дело, и это нормально: как я понял хабрасообщество не всегда положительно относится к новичкам, но здесь как мы видим статья ушла в плюс, а это означает что кто-то нашёл или подчеркнул для себя что-то новенькое. Ошибки делают сильнее
Отрицательные индексы у указателей вполне можно встретить в библиотечном коде. По этим адресам обычно хранятся метаданные, например, количество элементов для массива или тип объекта для сишной реализации ООП.
brightness это массив из индексов (яркостей). К примеру такой:
const uint8_t brightness[10] = {2, 3, 4, 5, 6, 7, 6, 5, 4, 3};
Если запустить упомянутый вами код вместе с приведённым массивом на выводе будет простой ASCII градиент: c;:,. .,:;
Я этот пример привёл потому что таким приёмом часто пользуются при создании ASCII графики, а массив записывают сразу, дабы сэкономить память.
В самом деле, немногие начинающие программисты знают что можно объявить массив/строку и взять элемент из неё.
стоп, что? А почему? А Можно ли их назвать хотя-бы начинающими? Звучит как те, кто язык вообще в жизни не видел
Это статья-стёб, или почему кто-то ее плюсует?
Плюсует - значит понравилось. И нет статья не стеб - какие-то сомнения? Я жду конструктивной критики, а не вот это вот всё =)
В Си статическими удобно делать мелкие функции определяемые в хедерах.
Sizeof это оператор, такой же как +, - или ?:. Формально в скобках (как вызов функции) пишется напрямую тип, а без скобок - выражение, размер типа которого нам нужен. Практически gcc и clang считают оба использования синонимичными, но есть люди которые настаивают на различии для большего контекста.
В Си статическими удобно делать мелкие функции определяемые в хедерах.
А зачем писать функции в хедерах если можно сразу написать их в том файле который подключает этот хедер так ещё и статические. Может тут какой-то трюк?
Ну, например, если в хедере определена общая для нескольких файлов структура, и обращение к каким-то полям удобно выделить в функцию:
struct Object {
enum State state;
...
};
static bool objectInTransition(struct Object *obj) {
return obj->state == STATE_TRANSITION_1 ||
obj->state == STATE_TRANSITION_2;
}
Соответственно
scanf()
возвращает кол-во считанных байт.
Нет. scanf возвращает количество успешно считанных полей.
Про union как способ реинтерпретации тут уже написали (использовать с осторожностью), но главный, как по мне смысл: экономия памяти. Например, у нас некоторое значение может быть как int так и double, а то и что поэкзотичней (разные структуры), но мы знаем, что значение может быть только одно в один момент времени. Таким образом, нам не нужно выделять память под два поля в структуре, например, а лишь под объединение, которое будет иметь размер максимального типа: sizeof(double) < sizeof(int) + sizeof(double).
И небольшое напутствие на будущее: не стоит считать себя сколь угодно серьезным разработчиком и пытаться давать экспертные заключения, если все ваши приложения состоят из одного файла (а если это файлы по 4000+ строк - стоит обратиться к специалисту...)).
Статья довольно странная, но основной профит от комментариев. Сам пишу кусок компилятора на С11, но о некоторых вещах и не догадывался. Пора уже создавать себе склад таких заковыристых вещей, чтобы потом знать что с ними делать.
arr[-1] или самые редкие конструкции в С