Скрытые возможности C
Я знаю, что за всеми реализациями компилятора C стоит стандарт, поэтому не должно быть никаких скрытых возможностей. Несмотря на это, я уверен, что все разработчики C имеют скрытые / секретные приемы, которые они используют постоянно.
56 ответов
Функциональные указатели. Вы можете использовать таблицу указателей функций для реализации, например, быстрых интерпретаторов косвенного кода (FORTH) или диспетчера байт-кода, или для имитации ОО-подобных виртуальных методов.
Затем в стандартной библиотеке есть скрытые гемы, такие как qsort(),bsearch(), strpbrk(), strcspn() [последние два полезны для реализации замены strtok()].
Недостатком C является то, что знаковое арифметическое переполнение является неопределенным поведением (UB). Поэтому всякий раз, когда вы видите выражение, такое как x+y, оба являются целыми числами со знаком, оно может потенциально переполниться и вызвать UB.
Еще одна хитрость компилятора GCC, но вы можете дать подсказки указания ветки компилятору (распространено в ядре Linux)
#define likely(x) __builtin_expect((x),1)
#define unlikely(x) __builtin_expect((x),0)
см.: http://kerneltrap.org/node/4705
Что мне нравится в этом, так это то, что это также добавляет выразительности некоторым функциям.
void foo(int arg)
{
if (unlikely(arg == 0)) {
do_this();
return;
}
do_that();
...
}
int8_t
int16_t
int32_t
uint8_t
uint16_t
uint32_t
Это необязательный элемент в стандарте, но он должен быть скрытым, потому что люди постоянно его переопределяют. Одна база кода, над которой я работал (и до сих пор работаю), имеет несколько переопределений, все с разными идентификаторами. В большинстве случаев это макросы препроцессора:
#define INT16 short
#define INT32 long
И так далее. Это заставляет меня хотеть вырвать мои волосы. Просто используйте чертовы стандартные целочисленные определения типов!
Оператор запятой широко не используется. Конечно, им можно злоупотреблять, но это также может быть очень полезно. Это использование является наиболее распространенным:
for (int i=0; i<10; i++, doSomethingElse())
{
/* whatever */
}
Но вы можете использовать этот оператор где угодно. Заметим:
int j = (printf("Assigning variable j\n"), getValueFromSomewhere());
Каждый оператор оценивается, но значением выражения будет значение последнего оцененного оператора.
инициализация структуры к нулю
struct mystruct a = {0};
это обнулит все элементы структуры.
Многосимвольные константы:
int x = 'ABCD';
Это устанавливает x
в 0x41424344
(или же 0x44434241
в зависимости от архитектуры).
РЕДАКТИРОВАТЬ: эта техника не переносима, особенно если вы сериализуете Int. Тем не менее, это может быть чрезвычайно полезно для создания самодокументируемых перечислений. например
enum state {
stopped = 'STOP',
running = 'RUN!',
waiting = 'WAIT',
};
Это значительно упрощает задачу, если вы просматриваете необработанный дамп памяти и хотите определить значение перечисления без необходимости его просмотра.
Я никогда не использовал битовые поля, но они звучат круто для вещей со сверхнизким уровнем.
struct cat {
unsigned int legs:3; // 3 bits for legs (0-4 fit in 3 bits)
unsigned int lives:4; // 4 bits for lives (0-9 fit in 4 bits)
// ...
};
cat make_cat()
{
cat kitty;
kitty.legs = 4;
kitty.lives = 9;
return kitty;
}
Это означает, что sizeof(cat)
может быть как маленький sizeof(char)
,
Чересстрочные структуры, такие как устройство Даффа:
strncpy(to, from, count)
char *to, *from;
int count;
{
int n = (count + 7) / 8;
switch (count % 8) {
case 0: do { *to = *from++;
case 7: *to = *from++;
case 6: *to = *from++;
case 5: *to = *from++;
case 4: *to = *from++;
case 3: *to = *from++;
case 2: *to = *from++;
case 1: *to = *from++;
} while (--n > 0);
}
}
C имеет стандарт, но не все компиляторы C полностью совместимы (я еще не видел полностью совместимого компилятора C99!).
Тем не менее, я предпочитаю приемы, которые неочевидны и переносимы между платформами, так как основаны на семантике C. Они обычно о макросах или битовой арифметике.
Например: замена двух целых чисел без знака без использования временной переменной:
...
a ^= b ; b ^= a; a ^=b;
...
или "расширение C" для представления конечных автоматов, таких как:
FSM {
STATE(x) {
...
NEXTSTATE(y);
}
STATE(y) {
...
if (x == 0)
NEXTSTATE(y);
else
NEXTSTATE(x);
}
}
это может быть достигнуто с помощью следующих макросов:
#define FSM
#define STATE(x) s_##x :
#define NEXTSTATE(x) goto s_##x
В целом, тем не менее, мне не нравятся хитрые уловки, которые усложняют чтение кода (как пример подкачки), и мне нравятся те, которые делают код более понятным и напрямую передают намерение (как пример FSM),
Я очень люблю назначенные инициализаторы, добавленные в C99 (и поддерживаемые в gcc в течение длительного времени):
#define FOO 16
#define BAR 3
myStructType_t myStuff[] = {
[FOO] = { foo1, foo2, foo3 },
[BAR] = { bar1, bar2, bar3 },
...
Инициализация массива больше не зависит от позиции. Если вы измените значения FOO или BAR, инициализация массива будет автоматически соответствовать их новому значению.
C99 имеет отличную инициализацию структуры любого порядка.
struct foo{
int x;
int y;
char* name;
};
void main(){
struct foo f = { .y = 23, .name = "awesome", .x = -38 };
}
Анонимные структуры и массивы - мой любимый. (см. http://www.run.montefiore.ulg.ac.be/~martin/resources/kung-f00.html).
setsockopt(yourSocket, SOL_SOCKET, SO_REUSEADDR, (int[]){1}, sizeof(int));
или же
void myFunction(type* values) {
while(*values) x=*values++;
}
myFunction((type[]){val1,val2,val3,val4,0});
это может даже использоваться, чтобы создать связанные списки...
(скрытая) особенность, которая "шокировала" меня, когда я впервые увидела, касается printf. эта функция позволяет использовать переменные для форматирования самих спецификаторов формата. ищите код, вы увидите лучше:
#include <stdio.h>
int main() {
int a = 3;
float b = 6.412355;
printf("%.*f\n",a,b);
return 0;
}
персонаж * достигает этого эффекта.
У gcc есть ряд расширений языка Си, которые мне нравятся, которые можно найти здесь. Некоторые из моих любимых - это атрибуты функций. Одним из чрезвычайно полезных примеров является атрибут формата. Это можно использовать, если вы определяете пользовательскую функцию, которая принимает строку формата printf. Если вы включите этот атрибут функции, gcc выполнит проверку ваших аргументов, чтобы убедиться, что строка формата и аргументы совпадают, и при необходимости выдаст предупреждения или ошибки.
int my_printf (void *my_object, const char *my_format, ...)
__attribute__ ((format (printf, 2, 3)));
Ну... Я думаю, что одной из сильных сторон языка C является его переносимость и стандартность, поэтому всякий раз, когда я нахожу какой-то "скрытый трюк" в реализации, которую я сейчас использую, я стараюсь не использовать ее, потому что я стараюсь сохранить свою C-код как стандарт и портативный, насколько это возможно.
Утверждения времени компиляции, как уже обсуждалось здесь.
//--- size of static_assertion array is negative if condition is not met
#define STATIC_ASSERT(condition) \
typedef struct { \
char static_assertion[condition ? 1 : -1]; \
} static_assertion_t
//--- ensure structure fits in
STATIC_ASSERT(sizeof(mystruct_t) <= 4096);
Постоянная конкатенация строк
Я был очень удивлен, не увидев это уже в ответах, поскольку все известные мне компиляторы поддерживают его, но многие программисты, кажется, игнорируют это. Иногда это действительно удобно, и не только при написании макросов.
Случай использования, который у меня есть в моем текущем коде: у меня есть #define PATH "/some/path/"
в конфигурационном файле (на самом деле это устанавливается make-файлом). Теперь я хочу построить полный путь, включая имена файлов, чтобы открыть ресурсы. Это просто идет к:
fd = open(PATH "/file", flags);
Вместо ужасного, но очень распространенного
char buffer[256];
snprintf(buffer, 256, "%s/file", PATH);
fd = open(buffer, flags);
Обратите внимание, что общее ужасное решение:
- в три раза длиннее
- гораздо менее легко читать
- намного медленнее
- для него установлено меньше мощности при произвольном ограничении размера буфера (но вам придется использовать еще более длинный код, чтобы избежать этого без постоянной константы строк).
- использовать больше места в стеке
Ну, я никогда не использовал его, и я не уверен, рекомендую ли я его кому-либо, но я чувствую, что этот вопрос был бы неполным без упоминания о совместной уловке Саймона Тэтэма .
При инициализации массивов или перечислений вы можете поставить запятую после последнего элемента в списке инициализатора. например:
int x[] = { 1, 2, 3, };
enum foo { bar, baz, boom, };
Это было сделано для того, чтобы при автоматической генерации кода вам не пришлось беспокоиться об удалении последней запятой.
Структура назначения это круто. Многие люди, кажется, не понимают, что структуры тоже являются ценностями, и их можно назначать вокруг, нет необходимости использовать memcpy()
, когда простое назначение делает свое дело.
Например, рассмотрим некоторую воображаемую библиотеку 2D-графики, она может определить тип для представления (целочисленной) экранной координаты:
typedef struct {
int x;
int y;
} Point;
Теперь вы делаете вещи, которые могут выглядеть "неправильно", например, пишете функцию, которая создает точку, инициализированную из аргументов функции, и возвращает ее, например, так:
Point point_new(int x, int y)
{
Point p;
p.x = x;
p.y = y;
return p;
}
Это безопасно, пока (конечно), поскольку возвращаемое значение копируется по значению, используя присвоение структуры:
Point origin;
origin = point_new(0, 0);
Таким образом, вы можете написать довольно чистый и объектно-ориентированный код, все в простом стандартном C.
Странное векторное индексирование:
int v[100]; int index = 10;
/* v[index] it's the same thing as index[v] */
При использовании sscanf вы можете использовать%n, чтобы узнать, где вы должны продолжать читать:
sscanf ( string, "%d%n", &number, &length );
string += length;
По-видимому, вы не можете добавить другой ответ, поэтому я добавлю второй ответ, вы можете использовать "&&" и "||" в качестве условия:
#include <stdio.h>
#include <stdlib.h>
int main()
{
1 || puts("Hello\n");
0 || puts("Hi\n");
1 && puts("ROFL\n");
0 && puts("LOL\n");
exit( 0 );
}
Этот код выведет:
Привет ROFL
Компиляторы C реализуют один из нескольких стандартов. Однако наличие стандарта не означает, что все аспекты языка определены. Например, устройство Даффа- это любимая "скрытая" функция, которая стала настолько популярной, что современные компиляторы имеют специальный код распознавания, чтобы гарантировать, что методы оптимизации не сгладят желаемый эффект от этого часто используемого шаблона.
В общем, скрытые возможности или языковые уловки не приветствуются, поскольку вы работаете на острие бритвы любого стандарта C, используемого вашим компилятором. Многие такие приемы не работают от одного компилятора к другому, и часто эти виды функций не будут работать в одной версии пакета компилятора данного производителя в другой версии.
Различные трюки, которые сломали C-код, включают:
- Опираясь на то, как компилятор размещает структуры в памяти.
- Предположения о порядке байтов целых чисел / чисел.
- Предположения о функции ABI.
- Предположения о направлении роста кадров стека.
- Предположения о порядке исполнения в выписках.
- Предположения о порядке выполнения операторов в аргументах функции.
- Допущения относительно размера битов или точности типов short, int, long, float и double.
Другие проблемы и проблемы, возникающие всякий раз, когда программисты делают предположения о моделях выполнения, которые все определены в большинстве стандартов C как поведение, зависящее от компилятора.
Использование INT(3) для установки точки останова в коде - мой самый любимый
Проверка предположений во время компиляции с использованием перечислений: глупый пример, но может быть действительно полезным для библиотек с настраиваемыми константами во время компиляции.
#define D 1
#define DD 2
enum CompileTimeCheck
{
MAKE_SURE_DD_IS_TWICE_D = 1/(2*(D) == (DD)),
MAKE_SURE_DD_IS_POW2 = 1/((((DD) - 1) & (DD)) == 0)
};
Я недавно обнаружил 0 битовых полей.
struct {
int a:3;
int b:2;
int :0;
int c:4;
int d:3;
};
который даст макет
000aaabb 0ccccddd
вместо без:0;
0000aaab bccccddd
Поле 0 width говорит о том, что следующие битовые поля должны быть установлены на следующем элементарном объекте (char
)
Gcc (c) имеет некоторые забавные функции, которые вы можете включить, такие как объявления вложенных функций, и форму a?: B оператора?:, Которая возвращает a, если a не ложно.
Моя любимая "скрытая" функция C, это использование%n в printf для обратной записи в стек. Обычно printf извлекает значения параметров из стека на основе строки формата, но%n может записать их обратно.
Проверьте раздел 3.4.2 здесь. Может привести к множеству неприятных уязвимостей.
Переменные аргументы в стиле C99, иначе
#define ERR(name, fmt, ...) fprintf(stderr, "ERROR " #name ": " fmt "\n", \
__VAR_ARGS__)
который будет использоваться как
ERR(errCantOpen, "File %s cannot be opened", filename);
Здесь я также использую оператор stringize и конкатенацию строковых констант, другие функции, которые мне действительно нравятся.
Автоматические переменные размера переменных также полезны в некоторых случаях. Они были добавлены в nC99 и уже давно поддерживаются в gcc.
void foo(uint32_t extraPadding) {
uint8_t commBuffer[sizeof(myProtocol_t) + extraPadding];
В итоге получается стек в буфере с местом для заголовка протокола фиксированного размера плюс данные переменного размера. Вы можете получить тот же эффект с помощью alloca(), но этот синтаксис более компактен.
Вы должны убедиться, что extraPadding является разумным значением, прежде чем вызывать эту подпрограмму, иначе вы в конечном итоге перевернете стек. Вы должны тщательно проверить аргументы перед вызовом malloc или любого другого метода выделения памяти, так что это не является необычным.