Приховані особливості С


141

Я знаю, що у всіх реалізаціях компілятора С є стандарт, тому не повинно бути прихованих функцій. Незважаючи на це, я впевнений, що всі розробники C мають приховані / секретні хитрощі, якими вони користуються весь час.


Було б чудово, якби ви / хтось редагував "запитання", щоб вказати на вибір найкращих прихованих функцій, наприклад, у версіях C # та Perl цього питання.
стипендіати

Відповіді:


62

Функціональні покажчики. Ви можете використовувати таблицю покажчиків функцій для реалізації, наприклад, швидких інтерпретаторів коду з непрямими потоками (FORTH) або диспетчерів байт-коду, або для імітації OO-подібних віртуальних методів.

Потім у стандартній бібліотеці є приховані дорогоцінні камені, такі як qsort (), bsearch (), strpbrk (), strcspn () [останні два корисні для впровадження заміни strtok ()].

Неправильною характеристикою C є те, що підписане арифметичне переповнення є невизначеним поведінкою (UB). Отже, коли ви бачите такий вираз, як x + y, обидва підписані входами, він може потенційно переповнювати і викликати UB.


29
Але якби вони вказали поведінку на переповнення, це зробило б це дуже повільно в архітектурах, де це було не нормальною поведінкою. Дуже низькі накладні витрати завжди були ціллю дизайну C, і це означало, що багато подібних речей не визначено.
Марк Бейкер

9
Я дуже добре знаю, чому переповнення - це UB. Це все-таки невдача, тому що стандарт повинен мати принаймні передбачені підпрограми бібліотеки, які можуть перевірити наявність арифметичного переповнення (усіх основних операцій) без утворення UB.
zvrba

2
@zvrba, "підпрограми бібліотеки, які можуть перевірити наявність арифметичного переповнення (всіх основних операцій)", якби ви додали це, то ви зробили б значущий показник ефективності для будь-яких цілих арифметичних операцій. ===== Аналіз конкретних випадків Matlab спеціально додає особливості управління поведінкою переповнення цілих чисел для загортання або насичення. І він також викидає виняток, коли виникає переповнення ==> Виконання цілочисельних операцій Matlab: ДУЖЕ СЛІД. Мій власний висновок: Я думаю, що Matlab - це переконливе тематичне дослідження, яке показує, чому ви не хочете перевіряти переповнення цілих чисел.
Тревор Бойд Сміт

15
Я сказав, що стандарт повинен був забезпечити підтримку бібліотеки для перевірки арифметичного переповнення. Тепер, як може звичайна бібліотека спричинити показник ефективності, якщо ви ніколи не використовуєте його?
zvrba

5
Великий негатив полягає в тому, що GCC не має прапора, щоб ловити підписані цілі переливи та викидати виняток із виконання. Хоча існують прапорці x86 для виявлення таких випадків, GCC їх не використовує. Наявність такого прапора дозволить застосувати некритичні для роботи (особливо застарілі) програми переваги безпеки з мінімальним переглядом коду та рефакторингом.
Ендрю Кітон

116

Більше хитрості компілятора 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();
     ...
}

2
Ця хитрість класна ... :) Особливо з макросами, які ви визначаєте. :)
sundar

77
int8_t
int16_t
int32_t
uint8_t
uint16_t
uint32_t

Це необов’язковий елемент у стандарті, але це має бути прихованою особливістю, оскільки люди постійно переробляють їх. В одній базі коду, над якою я працював (і дотепер це роблю, є багато разів), все з різними ідентифікаторами. Більшість часу це з макросами препроцесора:

#define INT16 short
#define INT32  long

І так далі. Це змушує мене витягнути волосся. Просто використовуйте чудернацькі стандартні цілі типиdedefs!


3
Я думаю, що вони є С99 або близько того. Я не знайшов портативного способу, щоб переконатися, що вони існують.
akauppi

3
Вони є додатковою частиною C99, але я не знаю жодного постачальника компіляторів, який би не реалізував це.
Бен Коллінз

10
stdint.h не є обов'язковим у C99, але дотримання стандарту C99, мабуть, є для деяких постачальників ( кашель Microsoft).
Бен Комбі

5
@Pete, якщо ви хочете бути анальними: (1) Ця тема не має нічого спільного з будь-яким продуктом Microsoft. (2) Цей потік взагалі ніколи не мав нічого спільного з C ++. (3) Немає такого поняття, як C ++ 97.
Бен Коллінз

5
Подивіться на azillionmonkeys.com/qed/pstdint.h - наближений до портативного
stdint.h

73

Оператор з комами широко не використовується. Це, звичайно, можна зловживати, але це також може бути дуже корисно. Це використання є найбільш поширеним:

for (int i=0; i<10; i++, doSomethingElse())
{
  /* whatever */
}

Але ви можете користуватися цим оператором де завгодно. Дотримуйтесь:

int j = (printf("Assigning variable j\n"), getValueFromSomewhere());

Кожне висловлення оцінюється, але значення виразу буде значення останнього оціненого твердження.


7
За 20 років КІ НІКОЛИ цього не бачили!
Мартін Бекетт

11
У C ++ ви навіть можете перевантажити його.
Wouter Lievens

6
може! = слід, звичайно. Небезпека при перевантаженні полягає в тому, що вбудоване застосовується до всього, що вже є, включаючи порожнечу, тому ніколи не пройде компілювати через відсутність наявного перевантаження. Тобто, дає програмісту багато мотузки.
Аарон

Інт всередині циклу не працюватиме з C: це поліпшення C ++. Чи "," та ж операція, що і для (i = 0, j = 10; i <j; j--, i ++)?
Ейф

63

ініціалізація структури до нуля

struct mystruct a = {0};

це зведе до нуля всі елементи структури.


2
Однак це не занулює прокладки, якщо такі є.
Пробіг

2
@simonn, ні, це не робить невизначеної поведінки, якщо структура містить нецілісні типи. memset з 0 на пам’яті float / double все одно буде нульовим, коли ви інтерпретуєте float / double (float / double розроблені так, як спеціально).
Тревор Бойд Сміт

6
@Andrew: memset/ callocdo "всі байти нульові" (тобто фізичні нулі), що дійсно не визначено для всіх типів. { 0 } гарантовано вселяє все правильними логічними нульовими значеннями. Наприклад, покажчики гарантуються для отримання належних нульових значень, навіть якщо нульове значення на даній платформі 0xBAADFOOD.
ANT

1
@nvl: Ви отримуєте фізичний нуль, коли просто насильно встановлюєте всю пам'ять, зайняту об'єктом, на стан біт-нуль. Це те, що memsetробить ( 0як другий аргумент). Ви отримуєте логічний нуль, коли ініціалізуєте / призначите 0(або { 0 }) об'єкт у вихідному коді. Ці два нулі не обов'язково дають однаковий результат. Як у прикладі з вказівником. Виконуючи memsetвказівник, ви отримуєте 0x0000вказівник. Але при призначенні 0вказівника ви отримуєте нульове значення вказівника , яке на фізичному рівні може бути 0xBAADF00Dчи будь-що інше.
ANT

3
@nvl: Ну, на практиці різниця часто лише концептуальна. Але теоретично його може мати практично будь-який тип. Наприклад, double. Зазвичай він реалізується відповідно до стандарту IEEE-754, в якому логічний нуль і фізичний нуль однакові. Але IEEE-754 мовою не потрібен. Тож може статися так, що коли ви зробите double d = 0;(логічний нуль), фізично деякі біти в пам'яті, зайняті, dне будуть нульовими.
ANT

52

Багатозначні константи:

int x = 'ABCD';

Це встановлюється xна 0x41424344(або 0x44434241, залежно від архітектури).

EDIT: Ця методика не є портативною, особливо якщо ви серіалізуєте int. Однак це може бути надзвичайно корисно для створення переписок самодокументації. напр

enum state {
    stopped = 'STOP',
    running = 'RUN!',
    waiting = 'WAIT',
};

Це набагато простіше, якщо ви дивитесь на неочищений дамп пам'яті і вам потрібно визначити значення перерахунку, не потребуючи його шукати.


Я впевнений, що це не портативна конструкція. Результатом створення константи багато символів є визначена реалізація.
Марк Бессі

8
"Не портативний" коментар повністю пропускає суть. Це як би критикувати програму за використання INT_MAX лише тому, що INT_MAX "не портативний" :) Ця функція така ж портативна, як і повинна бути. Константа мультиварки - надзвичайно корисна функція, яка забезпечує читабельний шлях для створення унікальних цілих ідентифікаторів.
ANT

1
@Chris Lutz - Я майже впевнений, що кома в кінці йде повністю до K&R. Це описано у другому виданні (1988).
Ferruccio

1
@Ferruccio: Ви мусите замислюватися над комою в списках сукупних ініціалізаторів. Що стосується кінцевої коми через перерахування перерахувань - це нещодавнє доповнення, C99.
ANT

3
Ви забули "HANG" або "BSOD" :-)
JBRWilkinson

44

Я ніколи не використовував бітові поля, але вони звучать круто для наднизьких рівнів.

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


Включені коментарі Аарона та леппі , спасибі хлопці.


Поєднання структур та об'єднань ще цікавіше - на вбудованих системах або низькому рівні драйвера. Наприклад, коли вам подобається розбирати регістри SD-карти, ви можете прочитати її за допомогою об'єднання (1) та прочитати за допомогою об'єднання (2), що є структурою бітових полів.
ComSubVie

5
Бітфілди не є портативними - компілятор може вільно вибрати, чи буде у вашому прикладі ногам виділятися найбільш значущі 3 біти або найменш значущі 3 біти.
zvrba

3
Бітфілди - це приклад того, коли стандарт дає реалізаціям стільки свободи в тому, як вони втілюються, що на практиці вони майже марні. Якщо вам все одно, скільки бітів займає значення і як воно зберігається, вам краще скористатися біт-масками.
Марк Бессі

26
Бітфілди справді портативні, якщо ви ставитесь до них як до структурних елементів, які вони є, а не до "цілих чисел". Розмір, а не розташування, має значення у вбудованій системі з обмеженою пам'яттю, оскільки кожен біт дорогоцінний ... але більшість сучасних кодерів занадто молоді, щоб про це пам’ятати. :-)
Адам Лісс

5
@Adam: розташування може мати значення у вбудованій системі (або в іншому місці), якщо ви залежите від положення бітового поля в його байті. Використання масок знімає будь-яку неоднозначність. Аналогічно і для спілок.
Стів Мельникофф

37

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

Взагалі, однак, мені не подобаються хитромудрі хитрощі, але роблять код надмірно складним для читання (як приклад swap), і я люблю ті, які роблять код яснішим і безпосередньо передають наміри (як, наприклад, FSM) .


18
C підтримує ланцюг, тому ви можете зробити ^ = b ^ = a ^ = b;
ОВ.

4
Строго кажучи, державний приклад - це галочка препроцесора, а не мова С - можна використовувати перший без останнього.
Грег Вітфілд,

15
ОВ: насправді те, що ви пропонуєте, є невизначеною поведінкою через правила точок послідовності. Він може працювати на більшості компіляторів, але не є правильним або переносним.
Еван Теран

5
Xor своп міг би бути менш ефективним у випадку вільного реєстру. Будь-який гідний оптимізатор зробить змінною temp реєстр. Залежно від реалізації (і потреби в підтримці паралелізму) своп може фактично використовувати реальну пам'ять замість регістра (що було б те саме).
Пол де Вріезе

27
будь ласка, ніколи насправді цього не робіть: en.wikipedia.org/wiki/…
Крістіан Оудар

37

Переплетення таких структур, як пристрій Даффа :

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

29
@ComSubVie, кожен, хто використовує пристрій Даффа, є скриптом, який бачив пристрій Даффа і думав, що їх код буде виглядати 1337, якби вони використовували пристрій Даффа. (1.) Пристрій Даффа не пропонує ніяких підвищеннях продуктивності на сучасному процесорі, оскільки сучасні процесори мають нульове накладення. Іншими словами, це застарілий фрагмент коду. (2.) Навіть якщо ваш процесор не пропонує нульового циклічного циклу, він, ймовірно, матиме щось на кшталт SSE / altivec / векторної обробки, що призведе до того, що ваш пристрій Даффа зірветься при використанні memcpy (). (3.) Я згадав, що інше, що робити memcpy () duff's, не є корисним?
Тревор Бойд Сміт

2
@ComSubVie, будь ласка, знайомся з моїм кулаком смерті ( en.wikipedia.org/wiki/… )
Тревор Бойд Сміт

12
@Trevor: значить, лише скрипти для дітей програми 8051 та мікроконтролери PIC, правда?
СФ.

6
@Trevor Бойд Сміт: Незважаючи на те, що пристрій Даффа застаріло, все ще існує історична цікавість, яка підтверджує відповідь ComSubVie. У будь-якому разі, цитуючи Вікіпедію: "Коли численні екземпляри пристрою Даффа були видалені з сервера XFree86 у версії 4.0, відбулося помітне поліпшення продуктивності". ...
paercebal

2
На Symbian ми колись оцінювали різні цикли для швидкого кодування пікселів; пристрій Даффа в асемблері було найшвидшим. Тож воно все ще мало актуальність на основних ядрах ARM на ваших смартфонах.
Буде

33

Мені дуже подобаються призначені ініціалізатори, додані в C99 (і підтримуються в gcc протягом тривалого часу):

#define FOO 16
#define BAR 3

myStructType_t myStuff[] = {
    [FOO] = { foo1, foo2, foo3 },
    [BAR] = { bar1, bar2, bar3 },
    ...

Ініціалізація масиву вже не залежить від позиції. Якщо змінити значення FOO або BAR, ініціалізація масиву автоматично відповідатиме їх новому значенню.


Підтримуваний синтаксис gcc тривалий час не такий, як стандартний синтаксис C99.
Марк Бейкер

28

C99 має дивовижну ініціалізацію структури будь-якого порядку.

struct foo{
  int x;
  int y;
  char* name;
};

void main(){
  struct foo f = { .y = 23, .name = "awesome", .x = -38 };
}


27

анонімні структури та масиви - це моя улюблена. (пор .: 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});

він навіть може бути використаний для інстанціювання пов'язаних списків ...


3
Цю особливість зазвичай називають "складеними літералами". Анонімні (або безіменні) структури позначають вкладені структури, які не мають імен членів.
calandoa

згідно з моїм GCC, "ISO C90 забороняє складні літерали".
jmtd

"ISO C99 підтримує складні літерали." "Як розширення, GCC підтримує складені літерали в режимі C89 і в C ++" (dixit info gcc). Крім того, "Як розширення GNU, GCC дозволяє ініціалізувати об'єкти зі статичною тривалістю зберігання складними літералами (що неможливо в ISO C99, оскільки ініціалізатор не є постійною)".
PypeBros

24

gcc має низку розширень до мови С, що мені подобається, які можна знайти тут . Деякі з моїх улюблених є атрибутами функції . Один надзвичайно корисний приклад - атрибут формату. Це можна використати, якщо ви визначите власну функцію, яка приймає рядок формату printf. Якщо ви ввімкнете цей атрибут функції, gcc здійснить перевірку ваших аргументів, щоб гарантувати, що ваш рядок і аргументи формату збігаються, а також генеруватиме попередження чи помилки у відповідних випадках.

int my_printf (void *my_object, const char *my_format, ...)
            __attribute__ ((format (printf, 2, 3)));

24

(прихована) функція, яка "шокувала" мене, коли я вперше побачила, стосується printf. ця функція дозволяє використовувати змінні для форматування самих специфікаторів формату. шукайте код, ви побачите краще:

#include <stdio.h>

int main() {
    int a = 3;
    float b = 6.412355;
    printf("%.*f\n",a,b);
    return 0;
}

персонаж * досягає цього ефекту.


24

Ну ... Я думаю, що одним із сильних моментів мови C є її портативність та стандартність, тому щоразу, коли я знаходжу якийсь "прихований трюк" у виконанні, яким я зараз користуюся, я намагаюся не використовувати його, тому що намагаюся зберегти свою Код С максимально стандартний і портативний.


Але насправді, як часто вам доводиться складати свій код з іншим компілятором?
Джо Д

3
@Joe D, якщо такий проект на різних платформах, як Windows / OSX / Linux, напевно, небагато, а також є різні арки, такі як x86 vs x86_64 і т.
Д.

@JoeD Якщо тільки ви не дуже вузький проект, який дуже радий одружитися на одному постачальнику компілятора. Можливо, ви хочете уникати переключення компіляторів, але ви хочете, щоб ця опція була відкритою. Однак із вбудованими системами ви не завжди можете вибрати вибір. AHS, ASS.
XTL

19

Твердження, складені часом, про які вже говорилося тут .

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

16

Постійне з'єднання рядків

Я був дуже здивований, не побачивши це все у відповідях, оскільки всі компілятори, які я знаю, підтримують його, але, здається, багато програмістів це ігнорують. Іноді це справді зручно і не тільки при написанні макросів.

У моєму поточному коді є випадок використання: у мене є #define PATH "/some/path/"файл конфігурації (він дійсно задається makefile). Тепер я хочу побудувати повний шлях, включаючи назви файлів, щоб відкрити ресурси. Це просто переходить до:

fd = open(PATH "/file", flags);

Замість жахливих, але дуже поширених:

char buffer[256];
snprintf(buffer, 256, "%s/file", PATH);
fd = open(buffer, flags);

Зауважте, що загальне жахливе рішення:

  • втричі довше
  • набагато менше легко читати
  • набагато повільніше
  • менша потужність у ньому встановлюється довільним обмеженням розміру буфера (але вам доведеться використовувати ще довший код, щоб уникнути цього без постійного контактування рядків).
  • використовувати більше місця для стеку

1
Також корисно розділити контентну константу на кілька ліній джерела, не використовуючи брудне "\".
долмен


12

Під час ініціалізації масивів або переліків ви можете поставити кому після останнього елемента у списку ініціалізатора. наприклад:

int x[] = { 1, 2, 3, };

enum foo { bar, baz, boom, };

Це було зроблено так, що якщо ви автоматично генеруєте код, вам не потрібно буде турбуватися про усунення останньої коми.


Це також важливо в середовищі для багатьох розробників, де, наприклад, Ерік додає "baz", а потім Джордж додає "boom". Якщо Ерік вирішить витягнути свій код для наступної збірки проекту, він все одно компілюється зі зміною Джорджа. Дуже важливо для управління кількома галузями вихідного коду та перекриваючих графіків розробки.
Гарольд Бамфорд

Енуми можуть бути C99. Ініціалізатори масиву та кінцева кома - це K&R.
Ferruccio

Прості перерахунки були в c89, AFAIK. Принаймні, вони існували протягом багатьох віків.
XTL

12

Призначення структури - це класно. Багато людей, здається, не усвідомлюють, що структури теж є значеннями, і їх можна призначати навколо, не потрібно використовувати 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);

Таким чином ви можете написати досить чистий і об'єктно-орієнтований код-ish, все в звичайному стандартному С.


4
Звичайно, є наслідки для продуктивності проходження таким чином круглих великих конструкцій; це часто корисно (і це дійсно щось, що багато людей не розуміють, що ви можете зробити), але вам потрібно врахувати, чи краще проходження покажчиків.
Марк Бейкер

1
Звичайно, може бути. Так само цілком можливо для компілятора виявити використання та оптимізувати його.
розкручується

Будьте уважні, якщо будь-який з елементів є покажчиками, оскільки ви будете копіювати самі вказівники, а не їх вміст. Звичайно, те саме відбувається, якщо ви використовуєте memcpy ().
Адам Лісс

Компілятор не може оптимізувати цю перетворювальну побічну величину, передаючи by -rererenece, якщо тільки вона не може зробити глобальну оптимізацію.
Blaisorblade

Напевно, варто відзначити, що в C ++ стандарт спеціально дозволяє оптимізувати віддалену копію (стандарт повинен дозволяти компіляторам реалізовувати її, оскільки це означає, що конструктор копій, який може мати побічні ефекти, не може бути названий), а оскільки більшість компіляторів C ++ також є компіляторами C, є хороший шанс, що ваш компілятор зробить цю оптимізацію.
Джозеф Гарвін

10

Дивна індексація:

int v[100]; int index = 10; 
/* v[index] it's the same thing as index[v] */

4
Ще краще ... char c = 2 ["Привіт"]; (c == 'l' після цього).
yrp

5
Не так дивно, якщо врахувати, що v [індекс] == * (v + індекс) та індекс [v] == * (індекс + v)
Ferruccio

17
Скажіть, будь ласка, ви фактично не використовуєте це "постійно", як запитання!
Tryke

9

Компілятори C реалізують один із декількох стандартів. Однак наявність стандарту не означає, що всі аспекти мови визначені. Наприклад, пристрій Даффа - це улюблена «прихована» функція, яка стала настільки популярною, що сучасні компілятори мають код розпізнавання спеціального призначення для того, щоб методи оптимізації не обмежували бажаний ефект цього часто використовуваного шаблону.

Загалом, приховані функції або мовні хитрощі відлякують, коли ви працюєте по краю бритви залежно від стандартних стандартів C, якими користується ваш компілятор. Багато таких хитрощів не працюють від одного компілятора до іншого, і часто такі види функцій перестають переходити від однієї версії набору компілятора даного виробника до іншої версії.

Різні хитрощі, які порушили код С, включають:

  1. Спираючись на те, як компілятор викладає структури в пам'яті.
  2. Припущення про витривалість цілих чисел / плавців.
  3. Припущення щодо функцій ABI.
  4. Припущення про напрямок, в якому ростуть рамки стеку.
  5. Припущення про порядок виконання всередині виписок.
  6. Припущення щодо порядку виконання висловлювань у аргументах функції.
  7. Припущення щодо розміру бітів або точності коротких, int, long, float та double типів.

Інші проблеми та проблеми, які виникають, коли програмісти роблять припущення щодо моделей виконання, які в більшості стандартів C визначені як поведінка "залежно від компілятора".


Щоб вирішити більшість із них, зробіть ці припущення залежними від характеристик вашої платформи та опишіть кожну платформу у своєму власному заголовку. Виконання замовлення є винятком - ніколи на це не покладайтеся; з іншого боку, кожна платформа потребує надійного рішення.
Blaisorblade

2
@Blaisorblade, Ще краще, використовуйте твердження часу компіляції, щоб документувати свої припущення таким чином, щоб компіляція вийшла з ладу на платформі, де вони порушені.
RBerteig

Я думаю, що слід поєднувати і те, і інше, щоб ваш код працював на декількох платформах (це був початковий намір), і якщо макроси функції встановлені неправильно, твердження під час компіляції підхоплять це. Я не впевнений, чи, скажімо, припущення щодо функцій ABI можна перевірити як твердження часу компіляції, але це повинно бути можливим для більшості інших (дійсних) (крім порядку виконання ;-)).
Blaisorblade

Функція перевірок ABI повинна оброблятися тестовим набором.
долмен

9

Під час використання 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

8

використання INT (3) для встановлення точки перерви у коді є моїм улюбленим часом


3
Я не думаю, що це портативно. Він буде працювати на x86, а як щодо інших платформ?
Крістіан Цюпіту,

1
Я поняття не маю - ви повинні опублікувати питання про це
Dror Helper

2
Це хороша техніка, і вона специфічна для X86 (хоча на інших платформах, мабуть, схожі методи). Однак це не є властивістю C. Це залежить від нестандартних розширень C або викликів бібліотеки.
Ферруччо

1
У GCC є __builtin_trap, а для MSVC __debugbreak, який буде працювати на будь-якій підтримуваній архітектурі.
Аксель Гнейтінг

8

Моя улюблена "прихована" функція C - це використання% n у printf для повернення до стека. Зазвичай printf виводить значення параметрів із стека на основі рядка формату, але% n може записати їх назад.

Ознайомтесь з розділом 3.4.2 тут . Може призвести до безлічі неприємних вразливих місць.


посилання більше не працює, адже сам сайт, здається, не працює. Чи можете ви надати ще одне посилання?
thequark

@thequark: будь-яка стаття про "вразливості рядкових форматів" матиме в ній інформацію .. (наприклад, crypto.stanford.edu/cs155/papers/formatstring-1.2.pdf ) .. Однак через характер поля, безпека Самі веб-сайти трохи невмілі, і справжні наукові статті важко придумати (з реалізацією).
Шрідхар Ієр

8

Перевірка припущення під час компіляції за допомогою enums: Дурний приклад, але може бути дуже корисним для бібліотек з константами конфігуруваного часу компіляції.

#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)
};

2
+1 Охайний. Раніше я використовував макрос CompilerAssert від Microsoft, але і ваш не поганий. ( #define CompilerAssert(exp) extern char _CompilerAssert[(exp)?1:-1])
Патрік Шлютер

1
Мені подобається метод перерахування. Підхід, який я використовував раніше, скористався усуненням мертвого коду: "if (something_bad) {void BLORG_IS_WOOZLED (void); BLORG_IS_WOOZLED ();}", який не помилявся до моменту посилання, хоча він пропонував перевагу відпускання Програміст знає через повідомлення про помилку, що Blorg був перероблений.
supercat

8

Gcc (c) має деякі цікаві функції, які ви можете ввімкнути, такі як вкладені декларації функції та форма a:: b оператора?:, Яка повертає a, якщо a - неправдиво.


8

Я нещодавно відкрив 0 бітфілів.

struct {
  int    a:3;
  int    b:2;
  int     :0;
  int    c:4;
  int    d:3;
};

який дасть макет

000aaabb 0ccccddd

замість без: 0;

0000aaab bccccddd

Поле ширини 0 говорить про те, що наступні бітові поля слід встановити на наступному атомному об'єкті ( char)


7

Макроси змінних аргументів у стилі C99, ака

#define ERR(name, fmt, ...)   fprintf(stderr, "ERROR " #name ": " fmt "\n", \
                                  __VAR_ARGS__)

які б використовувались як

ERR(errCantOpen, "File %s cannot be opened", filename);

Тут я також використовую оператор строгізації та константа константних рядків, інші функції, які мені дуже подобаються.


У вас є додаткове значення 'R' у VA_ARGS .
Blaisorblade

6

Автоматичні змінні розміру розміру також корисні в деяких випадках. Вони були додані i nC99 і підтримуються в gcc протягом тривалого часу.

void foo(uint32_t extraPadding) {
    uint8_t commBuffer[sizeof(myProtocol_t) + extraPadding];

Ви опиняєтесь буфером на стеці, в якому є місце для заголовка протоколу фіксованого розміру плюс даних змінного розміру. Такий же ефект можна отримати і аллока (), але цей синтаксис є більш компактним.

Ви повинні переконатися, що extraPadding є розумним значенням перед тим, як викликати цю рутину, або в кінцевому підсумку видуває стек. Вам доведеться з розумом перевірити аргументи перед тим, як викликати malloc або будь-яку іншу техніку розподілу пам’яті, тож це насправді незвично.


Чи буде це також правильно, якщо байт / char не буде точно 8 біт шириною на цільовій платформі? Я знаю, такі випадки рідкісні, але все ж ... :)
Stephan202
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.