This is an old revision of the document!


Coding style CA

Responsabili:

Această pagină are o importanță deosebită pentru modul vostru de formare ca programatori. Vă rugăm să o citiți integral!

Motivație

Este foarte important să putem scrie cod corect, eficient, dar, în aceeași măsură, codul nostru trebuie să poată fi înțeles și de alte persoane.

Imaginați-vă următoarele scenarii: 1. 2 colegi scriu cod în maniere cu totul diferite, iar la un moment dat trebuie să îl combine pentru a face produsul final. Deoarece nici unul nu înțelege ce a făcut celălalt pentru că au scris cod cum au vrut, acum sunt nevoiți să stea și să facă modificări astfel încât să ajungă la un numitor comun și să poată colabora. Nu era mai bine ca fiecare să fi urmat aceleași convenții de la bun început?

2. Un alt scenariu, mai aproape de voi, este chiar acum când voi scrieți primele programe în C. Se întămplă (și este normal) să uităm anumite lucruri o dată cu trecerea timpului, prin urmare voi veți simți nevoia să vă uitați pe programele scrise de voi (ex. cum aloc dinamic un tablou 3D în C, cum am rezolvat tema X care avea o idee asemănătoare cu ce îmi trebuie mie acum). Ce se va întâmpla dacă nu aveți un coding style adecvat? Răspunsul este simplu: nu veți înțelege propriu cod! Pentru lucrurile simple evident că trebuie să treaca un timp destul de lung, dar pentru cele grele și foarte grele puteți ajunge în situația în care să “uitați” cum se codează o chestie de pe o zi pe alta.

Toate scenariile de mai sus reprezintă exemple care v-ar putea afecta în sens negativ, de aceea la materia PC (și multe altele în facultate) vi se va impune să urmăriți anumite aspecte care să vă obisnuiască încă de la început, când lucrurile sunt foarte simple, astfel încât aceste deprinderi să intre în modul vostru normal de a coda.

Pe lângă respectarea convenției (a coding style-ului), este important să scriem cod reutilizabil și ușor de întreținut.

În mare, acest lucru se obține prin modularizarea bună a codului și prin evitarea unor practici nerecomandate care fac codul greu de înțeles și întrețnut.

În consecință, este nerecomandată utilizarea variabilelor globale și a salturilor nedisciplinate (goto).

Utilizarea celor 2 de mai sus în cazuri în care nu sunt absolut necesare, precum și scrierea de funcții nejustificat de lungi vor atrage după sine depunctarea în cadrul temelor de casă.

Convenție

Cum rezolvăm problemele de mai sus? Scenariul 2 se poate rezolva prin a scrie într-un mod ordonat, propriu, codul. Este de ajuns? Răspunsul este nu, întrucât e foarte puțin probabil să scrii cod de unul singur tot timpul. Colaborarea este un lucru care va veni la un moment dat în ajutorul vostru.

Pentru a rezolva toate problemele care pot apărea, vom folosi o convenție de coding style. Convențiile sunt seturi de reguli acceptate de comunități largi. Există mici diferențe între ele, fapt care face ca atunci când se trece de la o convenție la alta (ex. alta firmă) să nu reprezinte o problemă.

Convenție

Vom încerca să enumerăm lucrurile pe care trebuie să le aveți in vedere, apoi vom trece prin toate instrucțiunile limbajului C și vom da exemple.

* Codul trebuie să fie:

  • clar și modularizat
    • Codul se va împărți în componente astfel încât să existe o separație logică (ex. mai multe fișiere - sursă și header, mai mule funcții etc). Fiecare bucată elementară de logică va fi mutată într-o funcție (ceea ce permite și reutilizarea codului. Funcțiile care au ceva în comun (ex. funcții matematice) vor fi grupate.
  • expresiv
    • Numele entităților trebuie să fie sugestive.
      • Numele de variabilă trebuie să fie sugestiv și să arate la ce e folosită aceea variabilă. Este perfect ok să avem nume precum i, j, k, p, q pentru index/contor folosit atunci cand lucrăm cu tablouri, dar pentru lucruri netriviale, numele de variabilă trebuie să arate mai mult (ex. int count_duplicates; vs int c; - prima situatie exprimă clar ca acest contor este folosit pentru a număra duplicatele, în schimb în cea dea doua situație nu putem înțelege la ce e folosit *c* fără a citi tot codul care îl folosește).
      • Numele de funcții reflectă de asemenea acțiunea pe care aceasta o realizează. Exemplu: int count_duplicates(int n, int v[NMAX]); vs int f(int n, int v[NMAX]);. Primul caz spune ce face funcția, în cel de-al doilea caz numele funcției nu ne spune nimic.
        • Convenție: numele de variabile și funcții vor fi sugestive, vor fi compuse din litere mici, cifre sau underscore (în cazul in care dorim să avem mai mult cuvinte).
    • Comentariile ajută la îmbunătățirea expresivității, atunci când numele și organizarea codului nu sunt suficiente. Exemplu: o funcție care care caută duplicatele dintr-un vector, apoi șterge elementele duplicate, iar din cele ramăse le șterge pe toate pare sau divizibile cu 1007 și returnează și câte numere sunt prime. Nu putem exprima întreaga funcționalitate doar prin numele funcției, astfel că o vom numi update_array, iar înainte de semnatura funcției vom explica algoritmul de update în câteva cuvinte.
  • lizibil
    • O componentă foarte importantă o reprezintă spațierea. Codul trebuie să fie cât mai aerisit (să conțină spații, linii goale unde e cazul).
    • Funcțiile nu trebuie să fie foarte lungi (maximum 80 de linii în C), liniile nu trebuie să fie foarte lungi (maximum 80 de caractere pe linie în C).
    • Indentarea joacă un rol foarte important. De fiecare dată când se deschide o pereche de acolate (”{}”), în interiorul lor codul se indenteaza cu un tab (4 spații) mai la dreapta.

Exemple - construcții în C

if - else

Cititi exemplele cu if-else de mai jos si analizati:

  • spatierea (if spatiu (; spațiu operator spațiu; etc)
    • Pentru operatorii binari avem convenția: operand1 spatiu operator spatiu operand2. (Ex. “x + 2”, int x = y + 2;”)
    • Pentru operatorii unari nu lăsăm spațiu între operator și operand. (Ex. “i++”, ”++i”).
    • Nu lăsăm spațiu niciodată înainte de ';'.
    • Lăsăm spațiu mereu după virgulă, niciodată înainte.
  • identarea (de fiecare data când se intră în '{}' se scrie cu un tab mai la dreapta)
  • cum se închid și deschid acoladele
    • chiar dacă avem o singură instrucțiune în if și nu e nevoie să punem acolade, în convenția stabilită vom pune pentru a pastra aspectul unitar al codului; un alt motiv ar putea fi acela că putem adăuga direct intrucțiuni noi fără să omitem din greșeală să mai punem și acolade.
  • cum se scrie o cascadă de if-else if-uri
if (condition) {
    printf("Gigel!\n");
}
...
if (n % 2 == 0 && n > 10 && (n % 3 == 1 || n % 7 == 2)) {
    ...
}
...
 
if (condition) {
    ...
} else {
    ...
}
 
...
if (condition1) {
    ...
} else if (condition2) {
    ...
} else if (condition3) {
    ...
} else {
    ...
}
for/while/do while

Citiți exemplele cu for de mai jos și analizați

  • spațierea
  • identarea (de fiecare dată când se intră în {} se scrie cu un tab mai la dreapta)
  • cum se închid și deschid acoladele
  • cum se scriu for-uri imbricate
for (int i = 0; i <= n; ++i) {
    printf("%d", i);
}
 
...
for (int i = n; i >= 0; --i) {
    printf("%d", i);
}
 
...
 
int found = 0;
for (int i = n; i >= 0 && !found; i -= 2) {
    ...
    if (...) {
         printf("%d", i);
         found = 1;
    }
}
 
...
 
for (int i = 3; i * i <= n; i += 2) {  // i * i <= n simuleaza conditia i <= sqrt(n) fara a lucra cu numere reale
    if (n % i == 0) {
          printf("%d", i);             // afiseaza pe ecran toate numerele impare de la 3 la sqrt(n)
                                       // care sunt divizori ai lui n
    }
}
 
... 
for (int i = 0; i < n; ++i) {
    for (int j = 0; j < n; ++j) {
         printf("%d ", j);
    }
    printf("\n");
}

Observațiile de mai sus sunt valabile și pentru celelalte cicluri repetitive.

functions

Analizați exemplele de funcții de mai jos și observați că.

  • numele funcțiilor e sugestiv
  • cum pune '{}' față de celelalte cazuri
  • spațierea se menține și la nivelul listei de parametri
    • ATENȚIE! Nu punem spațiu între numele funcției și '('
    • Asemănător cu instrucțiunile, lăsăm un spațiu între ')' și '{'
    • Lăsăm un spațiu mereu după virgulă, niciodată înainte de virgulă.
void clean_display()
{
    ...
}
 
 
...
 
void print_array(int n, int v[NMAX])
{
    ...
}
 
int print_gigel(int gigel)
{
    ...
    return result;
}
 
int count_duplicates(int n, int v[NMAX])
{
    int x = n;             // niciodata nu lasam prima linie dintr-un block {} goala, nu are sens
    ...
                           // daca avem mai multe declaratii/alocari etc putem lasa o linie goala sa aratam ca ca s-a terminat initializarea
    int cnt = 0;
    for (int i = 0; i <= n; ++i) {
        ...
    }
                          // mai las o linie goala (optional) sa arat ca s-a terminat algoritmul 
    print_gigel(cnt);    
    return cnt;
}
programs

Fie următorul șablon de program. Analizați: * cum scriem în fișier mai multe funcții * care e structura generală a unui program

#include <stdio.h>
...                 // alte biblioteci incluse sau macro-uri
#include <math.h>
#define NMAX 100    // nu vom declara tablouri cu int v[100]; ci vom folosi un macro pentru dimensiune      
                    // linie goala
void dummy()
{
}
                    // lasam o linie goala intre functii
int print_array(int n, int v[NMAX])
{
     ...
}
                    // lasam o linie goala intre functii
int main()
{
   ...
   return 0;
}

Evident ca atunci cand fișirul 'main.c' devine prea mare, este nevoie să grupăm componentele după logică și să le mutăm în alte fișiere sursă C/headere.

Coding Style checker

Pentru a vă ajuta la teme și a evita eventualele depunctări pentru chestii standard, echipa vă pune la dispoziție scriptul *cs.sh*. Acesta va fi folosit la teme pentru a depuncta în mod automat cele mai frecvente erori de coding style.

Conveția impusă de checkerul nostru este cea din Linux Kernel, cu mici ajustări.

Checkerul de coding style se găsește la adresa https://cutt.ly/pc20-cs.

ATENȚIE! Anumite aspecte de coding style nu pot fi verificate automat prin checker. De aceea punctajul final pentru coding style se acordă după corectarea manuală.

Alocarea de memorie și lucrul cu fișiere

Alocarea de memorie este un subiect care pune probleme multora atunci când fac primii pași în utilizarea limbajului C. În această secțiune, atragem atenția asupra unor elemente importante referitoare la alocarea de memorie și lucrul cu fișiere.

Recomandăm citirea acestei secțiuni după parcurgerea laboratoarelor de Alocare dinamică de memorie, respectiv Fișiere.

Memory leaks

Memory leak-urile reprezintă o eroare de programare, în ceea ce privește alocarea dinamică de memorie, reprezentată de situația în care memoria nefolosită rămâne alocată.

Atunci când folosim memorie alocată dinamic (cu malloc(…), calloc(…) etc.) trebuie să avem în vedere să și dealocăm memoria atunci când nu o mai folosim!

Prin utilizarea utilitarului Valgrind checker-ul folosit la notarea automată a temelor poate descoperi execuțiile care lasă memorie nedealocată, iar acest lucru va fi depunctat, conform enunțului temei repective.

Neînchiderea fișierelor

O problemă foarte similară Memory Leak-urilor o reprezintă neînchiderea fișierelor.

Lucrul corect cu fișiere trebuie să includă:

  1. deschiderea fișierului în modul corect;
  2. verificarea reușitei operației de deschidere;
  3. procesarea conținutului fișierului (operațiile efective de citire/scriere/salt);
  4. închiderea fișierului.

Utilizarea tipului nepotrivit de alocare de memorie

O altă problemă frecventă referitoare la alocarea de memorie o reprezintă utilizarea tipului nepotrivit de alocare de memorie.

În continuare vom exemplifica doar câteva astfel de erori:


Folosirea alocării dinamice pentru situații în care cantitatea de memorie ce urmează să fie alocată este cunoscută anterior și se dealocă în același context.

#define VECTOR_SIZE 9
 
void asa_nu() { // Exemplu de utilizare nerecomandata
	int *v = malloc(VECTOR_SIZE * sizeof(int));
 
	// lucru cu vectorul v[]
	...
 
	free(v);
}
 
void asa_da() {
	int v[VECTOR_SIZE];
 
	// lucru cu vectorul v
        ...
 
}

O excepție de la această regulă se face atunci când dorim să alocăm cantități mari de mamorie, deoarece alocarea dinamică permite tratarea situației de OOM (Out of Memory) și în multe cazuri permite alocarea unor zone mult mai mari de memorie.


Utilizarea Variable Length Arrays pentru alocarea de vectori mari sau cu dimensiune provenită din calcule fără limită superioară sau din input-ul utilizatorului

În cele mai recente versiuni, limbajul C permite alocarea de vectori folosind ca dimensiune variabile de tip întreg. Deși acest lucru permite utilizarea eficientă a spațiului de memorie de pe stivă funcției, utilizarea necorespunzătoare a acestei opțiuni poate duce la apariția unor erori la execuție (segmentation fault sau stack smashed) ce nu pot fi tratate.

O eroare comună o reprezintă utilizarea input-ului (neverificat) al utilizatorului ca dimensiune de alocare:

void asa_nu() { // Exemplu de utilizare nerecomandata
	int n;
	scanf("%d", &n);
 
	int v[n]; // dacă utilizatorul introduce un număr foarte mare sau negativ putem avea eroare la execuție
 
	...	
}

În continuare, vom prezenta 2 variante de tratare corectă a acestei situații:

  1. Validarea faptului că avem un număr pozitiv și limitarea dimensiunii alocabile;
  2. Folosirea alocării dinamice.
#define MAX_SIZE 10000
 
void asa_da_1() {
	int n;
	scanf("%d", &n);
 
	while(n < 0 || n > MAX_SIZE) {
		printf("Numarul este prea mare, te rog introdu un nr. din [0, %d]\n", MAX_SIZE);
		scanf("%d", &n);
	}
 
	// Prelucrare vector v
	...
 
}
 
 
void asa_da_2() {
	int n;
	int *v;
 
	scanf("%d", &n);
 
	while(n < 0) {
		printf("n trebuie sa fie un numar pozitiv. Reincearca!\n");
		scanf("%d", &n);
	}
 
	v = malloc(n * sizeof(int));
	if(v == NULL) {
		fprintf(stderr, "Nu am putut aloca memorie.\n");
		exit(-1);
	}
 
	// Prelucrare vector v
	...
 
	// Dealocare memorie pentru v
	free(v);
}

Pentru soluția 1 se observă că pune în responsabilitatea programatorului determinarea lui 'MAX_SIZE', care poate interveni ca o limitare nejustificată a funcționalității programului.

Un cititor al soluției 2 poate reclama contradicțiecu recomandarea de a folosi memorie alocată static dacă dealocarea se va face în același context și dimensiunea alocată nu este mare. Totuși, codul nu garantează că n este un număr mic.

Nu este recomandată combinarea (pe cazuri) a celor 2 soluții, deoarece complică foarte mult codul, iar plusul de performanță este rareori semnificativ.

Eventual, am putea face 2 versiuni ale funcției (pentru valori mari respectiv mici ale lui n), dacă ne așteptăm ca funcția pentru valori mici să fie apelată de mii/milioane de ori pe secundă pentru ca timpul, mai mare, necesar pentru malloc(…) să conteze.

Concluzie - seria CA

Este foarte important să avem un coding style OK. Această pagină vine în ajutorul vostru.

Scriptul prezentat pe această pagină va fi folosit pentru verificarea automată a temelor de la seria CAa coding style-ului la teme. În caz ca apar erori se va penaliza conform mentiunilor din enunt.

programare/coding-style.1602080500.txt.gz · Last modified: 2020/10/07 17:21 by dorinel.filip
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