Konvence pro programování v jazyce C

Konvence pro programování v jazyce C

Konvence jsou při programování (v jazyce C) chápány jako soubor pravidel či doporučení, který firma předepisuje svým programátorům jakožto povinné. Důvodem k tomu je usnadnit či dokonce umožnit, aby někdo mohl pokračovat v práci udělané někým jiným – ať už to znamená dopracovat, odstranit chyby či optimalizovat.

Většinou se má na mysli

  • dokumentace (včetně komentářů),

  • způsob typografického odlišovaní různých entit či úrovní zanoření programových struktur,

  • pojmenovávací a jazyková pravidla, či omezení znakových sad.

Následuje shrnutí konvencí, které se budou používat na cvičeních PB071. Kontrola jejich dodržování nebude striktní, nicméně se snažte, aby byl váš kód přehledný a formátování konzistentní. Můžete třeba psát závorky v domácím úkolu jinak než je uvedeno zde, pak ale dodržujte stejný způsob v celém svém řešení.

Názvy identifikátorů

Identifikátory jsou názvy funkcí, proměnných, struktur atd. Používejte anglický jazyk a snažte se, aby název identifikátoru odpovídal jeho účelu.

identifikátor název

funkce, proměnné

začínají vždy malým písmenem; pokud je navíc název složen z více slov, používejte underscore_case, nikoliv camelCase

struktury a výčtové typy (enum)

položky struktury

položky výčtového typu

UPPER_UNDERSCORE_CASE s jedinečným prefixem

pojmenované konstanty

UPPER_UNDERSCORE_CASE

Poznámka: dle standardu C99 jsou názvy začínající _ a velkým písmenem nebo dalším _ rezervované, tj. __linux__ nebo _M_node, nicméně vyhýbejte se také názvům začínajícím _ a malým písmenem, např. _ptr. Pokud programujete na UNIXovém systému (např. Linux, MacOS), nedávejte vlastním typům názvy, které končí _t (např. stack_t), protože jsou rezervované pro POSIX.

/** TAKHLE ANO **/
struct node {
    struct node *next;
    int value;
};

enum mode {
    MODE_EXECUTE = 1,
    MODE_WRITE = 2,
    MODE_READ = 4
};

int         position;
size_t      array_size;
const char  DELIMITER = ':';

/** TAKHLE NE */
enum flags {
    autoCloseFiles = 1,            // --> FLAG_AUTO_CLOSE_FILES
    verbose = 2,                   // --> FLAG_VERBOSE
    // ...
};

int  ARGUMENT, Argument, ArGuMeNt; // --> argument
char lastChar;                     // --> last_char
void __do_something() ...          // --> do_something()
int  pom;                          // --> ne nutně špatně, ale raději temp

Formátování zdrojového kódu

Délka každého řádku by měla být nanejvýš 80 znaků. Tento limit není striktní, nicméně pokud by měl řádek „přetéct za okraj obrazovky,“ je vhodné ho raději rozdělit (viz níže).

Na každém řádku pište nejvýše jeden výraz nebo řídící strukturu.

Blokové závorky doporučujeme používat na vyznačených místech i v případě, že blok obsahuje pouze jediný příkaz.

Psaní blokových závorek

Závorky pište podle K&R style. V dalším textu se odsazením myslí odsazení ≥4 mezery doprava vzhledem k odsazení v aktuálním bloku kódu, bez odsazení znamená, že se použije stejné odsazení jako v původním bloku. Následující tabulka obsahuje shrnutí formátování, kde ␣␣␣␣ znázorňuje odsazení.

Funkce a struktury

int factorial(int x)
{
␣␣␣␣// ...
}
  • Otevírací a uzavírací bloková závorka na novém řádku bez odsazení.

  • Tělo je odsazené.

Řídící struktury obecně

while (condition) {
␣␣␣␣// ...
}
  • Otevírací bloková závorka na stejném řádku.

  • Tělo začíná samostatným řádkem a je odsazené.

  • Uzavírací bloková závorka na samostatném řádku bez odsazení.

Podmínky (if, else if)

if (condition) {
␣␣␣␣// ...
} else {
␣␣␣␣// ...
}
  • else ifelse mohou začít na stejném řádku jako ukončovací závorka předchozího bloku.

Cyklus do …​ while

do {
␣␣␣␣// ...
} while (condition);
  • Stejně jako výše, while může být za ukončovací blokovou závorkou.

switch

switch (cardinal) {
case 0:
␣␣␣␣// ...
␣␣␣␣break;
default:
␣␣␣␣// ...
}
  • casedefault mají stejné odsazení jako switch.

  • Těla větví jsou na samostatných řádcích s odsazením.

  • Těla větví nejsou v blokových závorkách, pokud to není potřeba.

Psaní mezer

Při psaní mezer dodržujte běžné typografické konvence, tedy mezery (značené ) pište

  • kolem závorek, ale ne pokud jde o parametry funkce,

    if(a < b) {   // mezery kolem závorek
    factorial(42);  // výjimka: bez mezer
  • kolem binárních operátorů kromě . a `→`,

    x+=2;         // tady ano
    y<1;
    queue->head;    // tady ne
    stack.size;
  • za čárkou,

    printf("%d %d %d",a,b,c);
  • za středníkem ve `for`,

    for (int i = 0;i < 10;++i)

Naopak, mezery () se nepíší

  • před otevírací závorkou seznamu argumentů a odpovídající uzavírací závorkou,

    /* ANO */ queue_enq(queue,item);
    /* NE  */ queue_enq(queue,item);
    /* NE  */ queue_enq(queue,item);

Speciálně při deklaraci ukazatele je mezera mezi typem a `*, ale ne mezi `* a identifikátorem (proč?), tedy:

/* ANO */ int*ptr;
/* NE  */ int*ptr;
/* NE  */ int*ptr;
/* NE  */ int*ptr;

Rozdělení dlouhého příkazu

Místo, na kterém rozdělit dlouhý příkaz, se těžko určuje obecně. Nejlépe je příkaz rozdělit tak, aby každý řádek obsahoval sémanticky významný celek. Vhodná místa k rozdělení jsou často za čárkou v argumentech volání funkce nebo před operátorem složeného výrazu. Další řádky se odsadí o 8 znaků nebo tak, aby bylo zřejmé, že jde o pokračování prvního řádku.

if (COND && COND && COND
        // toto je odsazeno o 8 znaků, protože je to pokračování podmínky
        && COND && COND
        && COND && COND) {
    // toto je odsazeno pouze o 4 znaky, aby bylo zřejmé,
    // že začíná tělo podmínky
    STATEMENT;
}

doStuff(arg, arg, arg,
        arg, arg, arg);

Ukázka formátování — jak psát závorky a mezery

typedef struct node
{
    struct node *left;
    struct node *right;
    void        *value;
} node;

int main(int argc, char **argv)         // mezera za , a před *
{
    while (player->health > 0) {        // mezery kolem operátoru
        search(player, map);            // pište { } i kolem jediného příkazu
    }

    if (temperature < 80) {
        printf("temperature is OK\n");
    } else if (temperature < 1000) {
        printf("a bit too hot\n");
    } else {
        printf("MELTDOWN\n");
        system("rm -rf /");             // nezkoušet doma ;)
    }

    switch (it->type) {
    case BIRD:
        printf("It's a bird!\n");
        break;
    case PLANE:
        printf("It's a plane!\n");
        break;
    default:
        printf("It's a birdplane!\n");
    }

    do {
        printf("Enter zero: ");
        scanf("%d", &x);
    } while (x != 0);                       // while může pokračovat za blokovou závorkou

    for (int i = 0; i < array_size; ++i) {  // mezery za ;
        printf("%d -> %d\n", i, array[i]);
    }
}

Příklady, které konvence porušují

enum answer {
    yes,                    // ANSWER_YES
    No,                     // ANSWER_NO
    MayBe,                  // ANSWER_MAYBE
    iDontKnow               // ANSWER_I_DONT_KNOW
};

if (isStrange(something) && InNeighbourhood(something))
    Call(ghostbusters);     // is_strange(), in_neighbourhood(), call()

while(problems>0)           // chybí mezery

if(condition){              // chybí mezera " (" a ") "
if ( condition ) {          // mezery navíc "( " a " )"

array [index];              // mezera navíc " ["

printf ("%d", 25);          // mezera " (" tady být nemá
printf("%d",25);            // chybí mezera za čárkou ", "
printf("%d" , 25);          // tohle se taky občas vyskytuje v úkolech

Pravidla pro konverze

Dodatečná pravidla pro zlepšení čitelnosti kódu.

  • Pokud konstanta v programu má reprezentovat znak, zapište ji jako znakovou konstantu, nikoliv číslo.

    int c = getchar();
    if (c == '\n') {           // ANO
        // ...
    } else if (c == 97) {      // NE
        // ...
    }
  • Nepište do kódu magická čísla, ale používejte pro ně pojmenované konstanty;
    výjimkou jsou čísla se zřejmým významem, např. circ = 2 * PI * r.

  • Pravdivostní test proměnné pište jako test hodnoty proměnné, nikoliv porovnání hodnoty s nulou.

    char *ptr;      // Ukazatel, který může a nemusí být nastaven.
    int   flag;     // Proměnná s logickým významem.
    int   num;      // Proměnná s aritmetickým významem (číslo).
    
    /* NE  */ if (ptr)          // ptr není logická hodnota
    /* ANO */ if (ptr != NULL)  // porovnání ukazatelů
    
    /* NE  */ if (!*ptr)        // *ptr neobsahuje logickou hodnotu
    /* ANO */ if (*ptr == '\0') // porovnání hodnot znaků
    
    /* NE  */ if (num)          // num nereprezentuje logickou hodnotu
    /* ANO */ if (num == 0)     // porovnání v kontextu čísla
    
    /* ANO */ if (!flag)        // flag používáme jako logickou hodnotu
    /* NE  */ if (flag != 0)    // a tudíž přímé porovnání s 0 nebo false je zbytečné

Stále platné zásady efektivního programování

Dobří programátoři a vývojáři aplikací se drží několika prověřených zásad tvorby zdrojového kódu. Tyto principy provázejí programování už několik desítek let, přesto jsou dodnes platné a uznávané.

  • neduplikujte kód

    • uložení a sdílení částí kódu ve společných metodách je mnohem čistší a efektivnější řešení než jejich kopírování a vkládání na různá místa ve zdrojovém kódu

  • vytvářejte kód, který bude snadno čitelný a pochopitelný

    • jeho struktura by měla být konzistentní s ostatními částmi systému nebo aplikace

  • dokumentujte hodnoty konstant

    • konstanty objevující se zázračně v kódu bez vysvětlení kontextu zatemňují strukturu a myšlenku softwaru

  • volte dostatečně vysvětlující názvy proměnných

    • názorné názvy usnadní orientaci v kódu, a pokud jsou doplněné o dostatečně informativní komentáře, umožní i dalším vývojářům snadno se zapojit do práce na kódu

  • při pojmenování konstant, proměnných, funkcí a dalších prvků kódu buďte konzistentní

    • pokud zavedete do názvů určitou strukturu (nejlépe odpovídající obecným zvyklostem), držte se jí v celém kódu

  • udržujte jednotlivé funkce malé a jednoznačné

    • příliš velký záběr znepřehledňuje kód a znejasňuje účel, ke kterému má daná funkce opravdu sloužit v případě, že nefunguje tak, jak bylo zamýšleno

  • vytvářejte kód s ohledem na výkonnost

    • při kódování využívejte profilační a diagnostické aplikace, které upozorní na možná zpomalení běhu výsledné aplikace

  • strukturujte kód tak, aby jej bylo možné snadno změnit v případě doplnění nových funkcí nebo přepracování existujících

  • často a důkladně testujte

S dobře strukturovaným kódem se následně lépe pracuje. Příprava před kódováním je sice náročnější, ale o to snazší je pak samotná tvorba. Pokud úsilí o dobrou čitelnost, udržovatelnost a výkonnost kódu zanedbáme se slovy „skoro nikdo ve skutečnosti nebude tuto část aplikace používat“ nebo „nechceme být perfektní, potřebujeme něco vypustit co nejdříve“, skončíme u hromady nekvalitního a neudržovatelného kódu, se kterým se nikdo další nebude chtít zabývat.

Povídání starého zbrojnoše

Už v šedesátých letech se začala projevovat „krize programování“ — to byla situace, kdy poptávka po programování (psaní nových či údržba dosavadních) vysoce překračovala nabídku — tedy počet a produktivitu programátorů. Situace byla oproti dnešku ještě komplikována drahotou, nevýkonností a nespolehlivostí tehdejších počítačů, tím, že se programy psaly v mnoha často specifických jazycích: strojových kódech nebo assemblerech jednotlivých procesorů a také nekomfortností tehdejších OS a vstupů — děrné štítky a pásky.

Jeden z důsledků onoho stavu bylo, že dobří programátoři měli velmi vysoké platy a špatní nebyli vyhazováni. Často se u jednotlivých organizací stávalo, že poměr produktivity mezi nejlepšími a nejhoršími programátory byl 10 : 1. K nápravě krize programování částečně vedlo strukturované programování, tvorba nových programovacích jazyků, metodik, paradigmatu objektově orientovaného programování. Programy měly být univerzálnější, opětovně použitelné a zejména snadno udržovatelné.

Ze začátku, kdy programovali jen opravdoví programátoři (originál), kteří psali neuvěřitelně efektivní a krátké programy (kvůli úspoře paměti kódu samomodifikující se), se ukázal jako velký problém nedostatek těch, kdo ty geniální programy měl udržovat — opravovat v nich chyby, rozšiřovat jejich funkčnost a upravovat pro nové OS či stroje. Finanční nákladnost údržby rostla. A čím dražší, protože komplikovanější, byl prvotní vývoj souboru programů, tím větší byly požadavky na prodloužení životnosti — tedy udržování. Začalo se mluvit o ceně za celou životnost — tedy pořízení a udržování. A udržování mnohdy ve finančních nárocích převyšovalo pořízení. Když si uvědomíme, že mnohý opravdový programátor údržbu prováděl záplatami ve strojovém kódu přímo na disk a nikde to nedokumentoval, pak se nemůžeme divit, že odchod takového opravdového programátora znamenal odepsání programu, ať už byl drahý, jak chtěl.

Způsob jak problém řešit je nabíledni: opravy a úpravy se musí dělat výhradně do dostatečně přehledného a komentovaného zdrojového textu ve vyšším jazyce — typicky C a velmi podrobná a pečlivá dokumentace. Záhy se ale projevilo, že když sice všichni píší texty programů dostatečně přehledně a podrobně a pečlivě je dokumentují, ale každý úplně jinak, tak to situaci nezlepší příliš a začaly se vytvářet konvence pro přehlednost textu programu a pro jejich dokumentaci a také nástroje, které tomu mohou být nápomocny.