C/SO Tips

Good practice

Verificare cod eroare

  • Verificați codul de eroare întors de apelurile de sistem.
    • Exemplu:
      fd = open(FILENAME, O_RDONLY);
      if (fd < 0) {
          perror("open");
          exit(EXIT_FAILURE);   /* sau alta actiune */
      }
       
      if (ReadFile(fHandle,
              &buffer,
              bytesToRead,
              &bytesRead,
              NULL) == FALSE) {
                     fprintf(stderr, "Error reading file %d\n", GetLastError());
                     exit(EXIT_FAILURE);
      }

Structura unui fișier header

Un fișier header reprezintă interfața publică a unui modul. Un fișier header:

  • începe cu un include guard (pentru a evita includerea sa într-un modul de mai multe ori)
  • include un set minim de alte fișiere header necesare pentru a putea compila fișierele sursă (.c) în care header-ul curent va fi inclus
  • poate conține directive pentru compilare condiționată
  • conține macrodefiniții și definiții de tipuri ce trebuiesc partajate
  • conține declarații incomplete (forward declarations) pentru structurile și uniunile ce apar doar ca pointeri în cadrul header-ului și pentru care nu se încearcă accesarea vreunui membru din interiorul header-ului; această tehnică poate reduce numărul de fișiere header ce trebuiesc incluse în header-ul curent:
struct x_t;
 
struct y_t {
    struct x_t *x;
    ...
};
/*
 * nu este necesar să includem header-ul în care este declarat tipul struct x_t
 * acel header va fi inclus în fișierele sursă care folosesc obiecte de tipul struct x_t
 */
  • conține declarații de structuri, uniuni și enumerații ce trebuiesc partajate
  • NU conține structurile, uniunile și enumerațiile interne modulului; dacă un alt modul trebuie să lucreze cu un obiect de un tip declarat în modulul curent (o structură sau o uniune), dar nu vrem să facem publică implementarea acelui tip, putem să ne folosim de o declarație incompletă și de definirea unui nou nume, ce va fi utilizat de acel modul:
struct vector;
typedef struct vector vector_t;
/* 
 * alte module vor folosi tipul vector_t
 * variabilele de acest tip vor fi manipulate prin funcții exportate de modulul curent
 */
  • declară variabilele globale exportate de modul (declarate cu keyword-ul extern)
  • conține prototipurile funcțiilor exportate; NU se exportă funcțiile ce sunt folosite doar intern de către modul (funcțiile folosite intern trebuie definite în fișierele sursă (.c) folosind keyword-ul static)
  • poate conține definiții de funcții inline ce trebuiesc partajate
  • NU conține cod executabil cu excepția funcțiilor inline și a macro-urilor

Alocare și eliberare de resurse

O practică bună este ca atunci când scriem o instrucțiune ce alocă o resursă (alocare dinamică de memorie, deschidere a unui fișier) să scriem și instrucțiunea asociată care eliberează acea resursă atunci când nu mai este necesară. În acest fel ne asigurăm că nu ținem ocupate resursele sistemului mai mult decât este cazul.

Folosirea de tool-uri pentru verificarea coding style-ului

Un coding style bun, folosit consecvent, face mai ușoară și mai rapidă înțelegerea unui cod. Marile proiecte software îsi stabilesc un coding style și realizează utilitare pentru verificarea automată a acestuia. Checkpatch.pl este un script Perl folosit în kernelul Linux pentru a verifica coding style-ului patch-urilor ce urmează a fi submise.

Limbaj

Omiterea parantezelor în cazul folosirii macrodefinițiilor

În cazul unei macrodefiniții sub forma unei expresii, aceasta trebuie încadrată între paranteze pentru a evita probleme legate de precendența operatorilor la expandarea macro-ului.

Greșit (neadecvat)

main.c
#define add(x, y) x + y
 
int main(void)
{
    float x = 10.0;
    float y = 20.0;
    float avg = add(x, y) / 2; // se expandează la: x + y / 2
    ...
    return 0;
}

Corect

main.c
#define add(x, y) (x + y)
 
int main(void)
{
    float x = 10.0;
    float y = 20.0;
    float avg = add(x, y) / 2; // se expandează la: (x + y) / 2
    ...
    return 0;
}

Din același motiv, parametrii unei macrodefiniții trebuie încadrați între paranteze.

Greșit (neadecvat)

main.c
#define trapezoid_area(h, total_bases_len) (h * total_bases_len / 2)
 
int main(void)
{
    float h = 10.0;
    float b = 20.0;
    float B = 30.0;
    float area = trapezoid_area(h, b + B); // se expandează la: (h * b + B / 2)
    ...
    return 0;
}

Corect

main.c
#define trapezoid_area(h, total_bases_len) ((h) * (total_bases_len) / 2)
 
int main(void)
{
    float h = 10.0;
    float b = 20.0;
    float B = 30.0;
    float area = trapezoid_area(h, b + B); // se expandează la: ((h) * (b + B) / 2)
    ...
    return 0;
}

Duplicarea efectelor secundare în cazul folosirii macrodefinițiilor

Greșit (neadecvat)

main.c
#define min(x, y) ((x) <= (y) ? (x) : (y))
 
int read_int(void)
{
    // citește și întoarce un întreg de la tastatură
}
 
int main(void)
{
    int x = 10;
    int z = min(x, read_int()); // se expandează la: ((x) <= (read_int()) ? (x) : (read_int()))
    ...
    return 0;
}

În exemplul anterior, dacă primul întreg citit de la tastatură este mai mic decât x atunci funcția read_int() se va apela de două ori, iar variabila z va avea valoarea celui de-al doilea întreg citit.

Corect

main.c
#define min(x, y) ((x) <= (y) ? (x) : (y))
 
int read_int(void)
{
    // citește și întoarce un întreg de la tastatură
}
 
int main(void)
{
    int x = 10;
    int y = read_int();
    int z = min(x, y); // se expandează la: ((x) <= (y) ? (x) : (y))
    ...
    return 0;
}

Nume de variabile/funcții/macrouri care încep cu underscore

  • Numele de variabile/funcții/macrouri care încep cu underscore _ sunt rezervate de biblioteca standard C. Se recomandă să nu folosiți astfel de nume în codul vostru.
    • Mai multe detalii găsiți aici.

Greșit (neadecvat)

test.h
#ifndef _TEST_H
#define _TEST_H      1
...
#ifndef __TEST_H
#define __TEST_H     1
...
#ifndef _TEST_H_
#define _TEST_H_     1
...
#ifndef __TEST_H__
#define __TEST_H__   1
...

Corect

test.h
#ifndef TEST_H
#define TEST_H      1      /* unele șanse de conflict */
...
#ifndef TEST_H_
#define TEST_H_    1
...
#ifndef TEST_H__
#define TEST_H__    1

Includere fișiere C

NU includeți fișiere C în alte fișiere C sau headere. Directiva #include se folosește doar pentru headere.

Greșit (neadecvat)

test.c
int test_fun1(int a, int b)
{
     ...
}
 
int test_fun2(char *c, size_t d)
{
     ...
}
main.c
#include "test.c"
 
int main(void)
{
    ...
    test_fun1(t, u);
    ...
    test_fun2(v, w);
    ...
 
}

Corect

test.c
/* definiții de funcții */
 
int test_fun1(int a, int b)
{
     ...
}
 
int test_fun2(char *c, size_t d)
{
     ...
}
test.h
#ifndef TEST_H_
#define TEST_H_     1
 
/* declarații (antete) de funcții */
int test_fun1(int a, int b);
int test_fun2(char *c, size_t d);
 
#endif
main.c
#include "test.h"
 
int main(void)
{
    ...
    test_fun1(t, u);
    ...
    test_fun2(v, w);
    ...
}

Alocare dinamică a unui caracter sau întreg

Greşit(neadecvat)

#define    BUFFER_SIZE     128
 
char *x = malloc(1);
char *buffer = malloc(BUFFER_SIZE);
 
if (read(STDIN_FILENO, x, 1) < 0) {
...
if (read(STDIN_FILENO, buffer, BUFFER_SIZE) < 0) {
...

Corect

#define    BUFFER_SIZE     128
 
char x;
char buffer[BUFFER_SIZE];
 
if (read(STDIN_FILENO, &x, 1) < 0) {
...
if (read(STDIN_FILENO, buffer, BUFFER_SIZE) < 0) {
...

Explicație

  • În cazul de faţă nu este nevoie alocarea dinamică a variabilei x, se poate utiliza operatorul de referenţiere & care află adresa unei variabile.
  • În cazul variabilei buffer, întrucât se cunoaște din momentul compilării valoarea acesteia, se poate folosi alocarea statică. Alocarea statică este realizată de compilator. Alocarea dinamică presupune gestiunea acesteia de utilizator (gestiunea pointerului, eliberarea zonei, evitarea leak-urilor) însemnând un risc mai ridicat.

Testare condiţie în if sau while

Greşit

if (fd = open("file", O_RDONLY) < 0) {
	perror("open");
} else {
	/* use fd */
}

Corect

if ((fd = open("file", O_RDONLY)) < 0) {
	perror("open");
} else {
	/* use fd */
}

Explicaţia

Operatorul < are prioritate mai mare decât operatorul de atribuire =. Aceaşi greşeală este şi dacă testăm condiţia fără paranteze într-un while.

Și mai corect

fd = open("file", O_RDONLY);
if (fd < 0)
        perror("open");
else {
        /* Use fd. */
}

Formularea cu apelul funcției în condiția if este error-prone și face codul mai greu de citit.

Typecast la / de la void *

În C (nu și în C++), nu sunt necesare typecast-uri într-o atribuire atunci când unul dintre termeni este de tip void *.

Greșit

int *a = (int *) malloc(N * sizeof *a);
void *p = (void *) a;

Corect

int *a = malloc(N * sizeof *a);
void *p = a;

Operatorul sizeof

Dacă dorim să alocăm memorie pentru un array de N numere întregi am putea folosi următoarea instrucțiune:

int *a = malloc(N * sizeof(int));

Dacă decidem să avem numere dintr-un interval mult mai mare, trebuie să modificăm în două locuri:

long long int *a = malloc(N * sizeof(long long int));

Abordarea anterioară este susceptibilă la erori. Este recomandat să folosim operatorul sizeof pentru tipul expresiei *a (expresia nu este evaluată):

long long int *a = malloc(N * sizeof *a);

Operatorul sizeof poate fi folosit pentru a determina numărul de elemente ale unui array alocat static:

int a[100];
printf("%lu\n", sizeof a / sizeof a[0]); // afișează 100

Comparații între signed și unsigned

NU comparați tipurile cu semn cu cele fără semn.

Greșit

int a = -1;
unsigned int b = 1;
if (a < b)
    printf("a < b\n");
else
    printf("a >= b\n");

În exemplul anterior, în cadrul expresiei condiționale se face un cast implicit la unsigned int pentru variabila a, rezultând într-o valoare foarte mare (2^32 - 1). Se va afișa incorect că a este mai mare sau egal cu b.

Corect

int a = -1;
unsigned int b = 1;
if ((INT_MAX < b) || (a < (int)b))
    printf("a < b\n");
else
    printf("a >= b\n");

Explicaţia

Putem face un cast explicit la int pentru variabila b doar dacă avem certitudinea că valoarea sa poate fi reprezentată ca un întreg cu semn. În cazul în care valoarea lui b este mai mare decât constanta INT_MAX (definită în limits.h) atunci cu certitudine a este mai mic decât b.

Când un array este un pointer

Înainte de a vedea când un array poate fi considerat un pointer, trebuie să ne reamintim ce este o declarație a unui array. În cazul unui array putem avea trei tipuri de declarații:

  • declarația unui array extern:
extern int a[]; // definit în alt modul ca: int a[N];
  • definiția unui array; definiția este un caz special de declarație care alocă spațiu pentru array-ul definit și eventual îl inițializează cu diverse valori:
int a[10] = {1, 2, 3};
  • declarația unui array ca fiind un parametru al unei funcții:
void f(int a[]);

În primele două cazuri, un array NU poate fi rescris ca un pointer. În cazul al treilea, array-ul poate fi rescris ca un pointer la primul element:

void f(int *a);

Compilatorul îl va trata în mod automat ca fiind un pointer, indiferent dacă îl declarăm sau nu ca un array. În mod similar, orice folosire a unui array într-o expresie poate fi rescrisă sub forma unui pointer (iar compilatorul va face asta automat și va trata array-ul ca fiind un pointer la primul element):

int n = a[i];
int m = *(a + i);

Folosirea specificatorului "static" pentru variabile globale și funcții

O variabilă globală sau o funcție declarată cu specificatorul “static” are internal linkage, adică poate fi referită doar din translation unit-ul respectiv (un translation unit reprezintă un fișier sursă .c împreună cu toate fișierele header pe care acesta le include).

Avantajele acestei practici sunt:

  • simbolurile sunt vizibile doar în translation unit-ul respectiv; astfel, se evită poluarea namespace-ului global și se reduc șansele conflictelor de nume
  • încapsulare: exte expusă doar interfața publică a modulului (nu și obiectele și funcțiile interne ce contribuie la implementarea interfeței)

Utilizare API

Utilizare open

Greşit

if ((fd = open("file_name", O_WRONLY | O_CREAT)) < 0) {

Corect

if ((fd = open("file_name", O_WRONLY | O_CREAT, 0644)) < 0) {

Explicaţia

În cazul în care folosim flagul O_CREAT, trebuie să specificăm drepturile cu care se creează fişierul. Compilează pentru că funcţia open este declarată

int open (char *file, int oflag, ...);

Sfaturi

  • Fiți consecvenţi în indentare!
  • Folosiți denumiri relevante pentru funcții și variabile.
so/laboratoare/resurse/c_tips.txt · Last modified: 2016/06/05 17:51 by adrian.stanciu
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0