Я знаю, что за всеми реализациями компилятора C стоит стандарт, поэтому скрытых функций быть не должно. Несмотря на это, я уверен, что у всех C-разработчиков есть скрытые / секретные приемы, которые они используют постоянно.
Я знаю, что за всеми реализациями компилятора C стоит стандарт, поэтому скрытых функций быть не должно. Несмотря на это, я уверен, что у всех C-разработчиков есть скрытые / секретные приемы, которые они используют постоянно.
Ответы:
Указатели на функции. Вы можете использовать таблицу указателей на функции для реализации, например, быстрых интерпретаторов непрямого кода (FORTH) или диспетчеров байтового кода, или для имитации виртуальных методов, подобных объектно-ориентированным.
Кроме того, в стандартной библиотеке есть скрытые жемчужины, такие как qsort (), bsearch (), strpbrk (), strcspn () [последние два полезны для реализации замены strtok ()].
Недостатком C является то, что подписанное арифметическое переполнение является неопределенным поведением (UB). Поэтому всякий раз, когда вы видите выражение, такое как x + y, оба подписанные int, оно может потенциально переполниться и вызвать 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};
это обнулит все элементы конструкции.
memset
/ calloc
do "все нулевые байты" (т.е. физические нули), что действительно не определено для всех типов. { 0 }
Гарантируется, что все будет инициализировано с правильными значениями логического нуля. Например, для указателей гарантируется получение правильных нулевых значений, даже если нулевое значение на данной платформе равно 0xBAADFOOD
.
memset
происходит (со 0
вторым аргументом). Вы получаете логический ноль, когда вы инициализируете / назначаете 0
(или { 0 }
) объекту в исходном коде. Эти два вида нулей не обязательно дают одинаковый результат. Как в примере с указателем. Когда вы делаете memset
указатель, вы получаете 0x0000
указатель. Но когда вы назначаете 0
указателю, вы получаете значение нулевого указателя , которое на физическом уровне может быть 0xBAADF00D
или чем-то еще.
double
. Обычно это реализовано в соответствии со стандартом IEEE-754, в котором логический ноль и физический ноль совпадают. Но IEEE-754 не требуется для языка. Таким образом, может случиться так, что когда вы это сделаете double d = 0;
(логический ноль), физически некоторые биты в памяти, занятой d
не будут нулевыми.
Многосимвольные константы:
int x = 'ABCD';
Устанавливается x
в 0x41424344
(или 0x44434241
, в зависимости от архитектуры).
EDIT: этот метод не переносится, особенно если вы сериализуете 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)
.
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) .
Чередование структур, таких как Устройство Даффа :
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);
}
}
Мне очень нравятся назначенные инициализаторы, добавленные в C99 (и уже давно поддерживаемые в gcc):
#define FOO 16
#define BAR 3
myStructType_t myStuff[] = {
[FOO] = { foo1, foo2, foo3 },
[BAR] = { bar1, bar2, bar3 },
...
Инициализация массива больше не зависит от позиции. Если вы измените значения FOO или BAR, инициализация массива автоматически будет соответствовать их новому значению.
анонимные структуры и массивы - мои любимые. (см. 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});
его даже можно использовать для создания связанных списков ...
gcc имеет ряд расширений для языка C, которые мне нравятся, которые можно найти здесь . Некоторые из моих любимых - это атрибуты функций . Один чрезвычайно полезный пример - атрибут формата. Это можно использовать, если вы определяете пользовательскую функцию, которая принимает строку формата printf. Если вы включите этот атрибут функции, gcc проверит ваши аргументы, чтобы убедиться, что ваша строка формата и аргументы совпадают, и при необходимости будет генерировать предупреждения или ошибки.
int my_printf (void *my_object, const char *my_format, ...)
__attribute__ ((format (printf, 2, 3)));
(скрытая) особенность, которая "шокировала" меня, когда я впервые увидел, связана с printf. эта функция позволяет вам использовать переменные для форматирования самих спецификаторов формата. ищи код, увидишь лучше:
#include <stdio.h>
int main() {
int a = 3;
float b = 6.412355;
printf("%.*f\n",a,b);
return 0;
}
символ * достигает этого эффекта.
Что ж ... Я думаю, что одной из сильных сторон языка 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/"
в файле конфигурации (на самом деле он задается файлом makefile). Теперь я хочу построить полный путь, включая имена файлов, для открытия ресурсов. Это просто идет на:
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] */
Компиляторы C реализуют один из нескольких стандартов. Однако наличие стандарта не означает, что определены все аспекты языка. Устройство Даффа , например, является излюбленной «скрытой» функцией, которая стала настолько популярной, что современные компиляторы имеют специальный код распознавания, гарантирующий, что методы оптимизации не подавят желаемый эффект этого часто используемого шаблона.
В общем, скрытые функции или языковые уловки не приветствуются, поскольку вы работаете на грани бритвы, независимо от того, какие стандарты C использует ваш компилятор. Многие такие приемы не работают от одного компилятора к другому, и часто такие функции не работают при переходе от одной версии компилятора от одного производителя к другой версии.
Различные уловки, которые нарушают код C, включают:
Другие проблемы и проблемы, возникающие всякий раз, когда программисты делают предположения о моделях исполнения, которые все указаны в большинстве стандартов C как «зависимое от компилятора» поведение.
При использовании 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 );
}
Этот код выведет:
Здравствуй РОФЛ
использование INT (3) для установки точки останова в коде - мой фаворит на все времена
Моя любимая «скрытая» функция C - это использование% n в printf для обратной записи в стек. Обычно printf извлекает значения параметров из стека на основе строки формата, но% n может записать их обратно.
Ознакомьтесь с разделом 3.4.2 здесь . Может привести к множеству неприятных уязвимостей.
Проверка предположений во время компиляции с использованием перечислений: глупый пример, но может быть действительно полезен для библиотек с конфигурируемыми константами во время компиляции.
#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)
};
#define CompilerAssert(exp) extern char _CompilerAssert[(exp)?1:-1]
)
Gcc (c) имеет несколько забавных функций, которые вы можете включить, например, объявления вложенных функций и форму a?: B оператора?:, Который возвращает a, если a не ложно.
Я обнаружил недавно 0 битовых полей.
struct {
int a:3;
int b:2;
int :0;
int c:4;
int d:3;
};
что даст макет
000aaabb 0ccccddd
вместо без: 0;
0000aaab bccccddd
Поле ширины 0 указывает, что следующие битовые поля должны быть установлены на следующем атомарном объекте ( char
)
Макросы переменных аргументов в стиле C99, также известные как
#define ERR(name, fmt, ...) fprintf(stderr, "ERROR " #name ": " fmt "\n", \
__VAR_ARGS__)
который будет использоваться как
ERR(errCantOpen, "File %s cannot be opened", filename);
Здесь я также использую оператор преобразования строки и конкатенацию строковых констант, другие функции, которые мне очень нравятся.
В некоторых случаях также полезны автоматические переменные переменного размера. Они были добавлены в nC99 и долгое время поддерживаются в gcc.
void foo(uint32_t extraPadding) {
uint8_t commBuffer[sizeof(myProtocol_t) + extraPadding];
В итоге вы получаете буфер в стеке с местом для заголовка протокола фиксированного размера и данных переменного размера. Вы можете получить тот же эффект с помощью alloca (), но этот синтаксис более компактный.
Вы должны убедиться, что extraPadding имеет разумное значение, прежде чем вызывать эту процедуру, иначе вы в конечном итоге взорвете стек. Вам нужно будет проверить аргументы перед вызовом malloc или любого другого метода выделения памяти, так что в этом нет ничего необычного.