Ubuntu - Programmer en C avec GCC


L'écriture d'un code source en langage C n'est pas simple car les concepts et la syntaxe utilisés dans ce langage sont subtils (types de données, pointeurs, adresses, passages d'arguments...). De plus la compilation et l'édition des liens nécessitent un minimum de maitrise du compilateur gcc.
Le principal intérêt d'utiliser le langage C, plutôt qu'un langage plus simple comme le langage interprété Python par exemple, c'est que le code écrit en langage C s'exécute beaucoup plus vite; presque aussi vite que le code écrit en assembleur. Dans ce qui suit, on utilise un ordinateur sous Ubuntu 18.04 avec le compilateur gcc version 7.5.0.

Pour pouvoir programmer avec aisance en C, il faut savoir Utiliser le compilateur GCC et connaître les principales notions de base.

On va regarder ci-après comment :
1) Utiliser le préprocesseur
2) Utiliser le mot clef typedef
3) Utiliser les arguments int argc et char *argv[]
4) Utiliser le mot clef const
5) Utiliser le mot clef extern
6) Utiliser le mot clef static
7) Utiliser des variables globales et locales
8) Utiliser les types de données
9) Spécifier des formats pour l'instruction printf()
10) Utiliser les opérateurs logiques
11) Réaliser des opérations sur les bits
12) Utiliser des fonctions
13) Utiliser des tableaux
14) Passer un tableau à une fonction
15) Retourner un tableau depuis une fonction
16) Utiliser des pointeurs
17) Utiliser des chaines de caractère
18) Utiliser des structures
19) Utiliser les entrées et sorties
20) Manipuler des fichiers

[haut de page]

1) Utiliser le préprocesseur

Les commandes de préprocesseur sont des indications données au compilateur sur la façon dont il doit effectuer la compilation. Ces commandes commencent par le caractère #.

#define pour définir une macro
#include pour insérer un fichier d'entête
#if
#endif
...

Exemple
/* test.c */
#include <stdio.h>
#define MIN 100
#if!defined (MIN)
    #define MIN 30
#endif
int main()
{
printf("%d \n", MIN);
return 0;
}

ubuntu@ubuntu-MS-7C08:/media/ubuntu/data/essai$ ./essai
100

/* test.c */
#include <stdio.h>
#if!defined (MIN)
    #define MIN 30
#endif
int main()
{
printf("%d \n", MIN);
return 0;
}

ubuntu@ubuntu-MS-7C08:/media/ubuntu/data/essai$ ./essai
30

[haut de page]

2) Utiliser le mot clef typedef

Le mot-clé typedef permet de donner un nom supplémentaire à un type de donnée particulier

Exemple

/* test.c */
#include <stdio.h>
typedef int ENTIER;
int main( )
{
ENTIER x=10;
printf( "x= %d\n",x);
return 0;
}

ubuntu@ubuntu-MS-7C08:/media/ubuntu/data/essai$ gcc -Wall test.c -o essai
ubuntu@ubuntu-MS-7C08:/media/ubuntu/data/essai$ ./essai
x= 10
ubuntu@ubuntu-MS-7C08:/media/ubuntu/data/essai$

[haut de page]

3) Utiliser les arguments int argc et char *argv[]

argv[0] contient le nom du programme
argv[1] est un pointeur vers le premier argument indiqué après le nom du programme
argv[2] est un pointeur vers le premier argument indiqué après le nom du programme

Exemple

/* test.c */
#include <stdio.h>
int main( int argc, char *argv[] )
{
printf("argc: %d\n",argc);
printf("argv[0]: %s\n", argv[0]);
printf("argv[1]: %s\n", argv[1]);
printf("argv[2]: %s\n", argv[2]);
return(0);
}

ubuntu@ubuntu-MS-7C08:/media/ubuntu/data/essai$ gcc -Wall test.c -o essai
ubuntu@ubuntu-MS-7C08:/media/ubuntu/data/essai$ ./essai bonjour
argc: 2
argv[0]: ./essai
argv[1]: bonjour
argv[2]: (null)
ubuntu@ubuntu-MS-7C08:/media/ubuntu/data/essai$

[haut de page]

4) Utiliser le mot clef const

Le mot clef const permet de définiir une vairaible prenant une valeur constante.
const int LENGTH = 10;

[haut de page]

5) Utiliser le mot clef extern

Le mot clef extern, placé en dehorsd'une portée, permet d'indiquer qu'une fonction ou qu'une variable a été déclarée dans un autre fichier et que par conséquent on peut l'utiliser dans les portées qui suivent.

Exemple

Soient les deux fichiers sources test.c et mesfonctions.c ci-après, compilés avec la commande suivante :
ubuntu@ubuntu-MS-7C08:/media/ubuntu/data/essai$ gcc -Wall test.c mesfonctions.c -o essai
ubuntu@ubuntu-MS-7C08:/media/ubuntu/data/essai$ ./essai
Cube de globvar = 27

/* test.c */
#include <stdio.h>
int globvar; //variable globale
extern void cube(); //fonction déclarée ailleurs int main()
{
globvar = 3;
cube();
}

//------ mesfonctions.c -----
#include <stdio.h>
extern int globvar;//variable déclarée ailleurs void cube(void)
{
int x=globvarglobvarglobvar;
printf("Cube de globvar = %d\n", x);
}

[haut de page]

6) Utiliser le mot clef static

Le mot clef static permet de conserver la valeur d'une variable dans une portée, entre plusieurs exécution de cette portée.

Exemple

Soit le fichier source test.c suivant compilé avec la commande suivante:
ubuntu@ubuntu-MS-7C08:/media/ubuntu/data/essai$ gcc -Wall test.c -o essai
ubuntu@ubuntu-MS-7C08:/media/ubuntu/data/essai$ ./essai
x = 10
x = 20

/* test.c */
#include <stdio.h>
void affiche(void);
int main()
{
affiche();
affiche();
return 0;
}

void affiche()
{
static int x= 0;
x=x+10;
printf("x = %d\n", x);
}

[haut de page]

7) Utiliser des variables globales et locales

Exemple

#include <stdio.h>
int x=0; //variable globale
int main ()
{
int y=0;//variable locale
printf ("Hello !");
return 0;

Les variables doivent être initialisées avec les valeurs suivantes
int 0
char '\0'
float 0
double 0
pointeur NULL

[haut de page]

8) Utiliser les types de données

char: 1 octet: 0 à 255
short: 2 octets: -32,768 à 32,767
int: 4 octets: -2,147,483,648 à 2,147,483,647
long: 4 octets: -2,147,483,648 à 2,147,483,647
float: 4 octets
double: 8 octets
unsigned char: 1 octet: 0 to 255 (%c)
signed char: 1 octet: -128 to 127 (%c)
unsigned int: 4 octets: 0 à 4,294,967,295
unsigned short: 2 octets: 0 à 65,535
unsigned long: 4 octets: 0 à 4,294,967,295 (avec un ordinateur 32bits)
unsigned long: 8 octets: 0 à 18,446,744,073,709,551,615 (avec un ordinateur 64bits)
unsigned long long: 8 octets: 0 à 18,446,744,073,709,551,615

Remarque
Les ordinateurs anciens possédait un processeur 32bits donc une taille de mot (wordsize) de 32 bits (4 octets). Sur ces ordinateurs le type unsigned long correspondait à 4 octets avec une valeur variant de 0 à 4,294,967,295. Les ordinateurs récents possèdent un processeur 64bits donc une taille de mot (wordsize) de 64 bits (8 octets). Sur ces ordinateurs le type unsigned long correspond à 8 octets avec une valeur variant de 0 à 18,446,744,073,709,551,615.

[haut de page]

9) Spécifier des formats pour l'instruction printf()

%d : int
%u : unsigned int
%o : int (exprimé en octal)
%x : int (exprimé en hexadécimal)
%c : int (exprimé en caractère)
%f : double (en notation décimale)
%e : double (en notation scientifique)
%s : char* (chaîne de caractères)
%lu : unsigned long (exprimé en décimal)
%llu : unsigned long long (exprimé en décimal)

Exemple

/* essai.c */
#include <limits.h>
#include <stdio.h>
//gcc -Wall essai.c -lm -o essai
int main(void)
{
printf("unsigned char: %%u: 0 à %u: %lu octet\n", UCHAR_MAX, sizeof(unsigned char));
printf("unsigned short: %%u: 0 à %u: %lu octets\n", USHRT_MAX, sizeof(unsigned short));
printf("unsigned int: %%u: 0 à %u: %lu octets\n", UINT_MAX, sizeof(unsigned int));
printf("unsigned long: %%lu: 0 à %lu: %lu octets\n", ULONG_MAX, sizeof(unsigned long));
printf("unsigned long long: %%llu: 0 à %llu: %lu octets\n", ULLONG_MAX, sizeof(unsigned long long));
return 0;
}

ubuntu@ubuntu-MS-7C08:/media/ubuntu/data/essai$ ./essai
unsigned char: %u: 0 à 255: 1 octet
unsigned short: %u: 0 à 65535: 2 octets
unsigned int: %u: 0 à 4294967295: 4 octets
unsigned long: %lu: 0 à 18446744073709551615: 8 octets
unsigned long long: %llu: 0 à 18446744073709551615: 8 octets
ubuntu@ubuntu-MS-7C08:/media/ubuntu/data/essai$

[haut de page]

10) Utiliser les opérateurs logiques

&& (et) - Exemple : if (a && b) { instructions; }
| (ou) - Exemple : if (a || b) { instructions; }
! (non) - Exemple : if (!(a||b)) { instructions; }
^ (ou exclusif) - Exemple : if (a^b) { instructions; }

[haut de page]

11) Réaliser des opérations sur les bits

Les opérations sur les bits s'effectuent en suivant la table de vérité suivante et avec les opérateurs ~ complement, << décalage à gauche et >> décalage à droite

   x       y      x & y     x | y      x ^ y  
   0       0       0       0       0   
   0       1       0       1       1   
   1       1       1       1       0   
   1       0       0       1       1   

Exemple

#include <stdio.h>
int main()
{
unsigned char a = 3; // 0000 0011
unsigned char b = 14; // 0000 1110
unsigned char c = 0; // 0000 0000
printf("a = %d\n", a );
printf("b = %d\n", b );
c = a|b; //0000 1111 = 15
printf("a|b = %d\n", c );
}

ubuntu@ubuntu-MS-7C08:/media/ubuntu/data/essai$ gcc -Wall test.c -o essai
ubuntu@ubuntu-MS-7C08:/media/ubuntu/data/essai$ ./essai
a = 3
b = 14
a|b = 15

[haut de page]

12) Utiliser des fonctions

Le passage d'argument par valeur ne conserve pas, au retour de la fonction, les modifications de l'argument effectuées dans la fonction.
En revanche, le passage d'argument par adresse conserve, au retour de la fonction, les modifications de l'argument effectuées dans la fonction.

Exemple

ubuntu@ubuntu-MS-7C08:/media/ubuntu/data/essai$ gcc -Wall test.c -o essai
ubuntu@ubuntu-MS-7C08:/media/ubuntu/data/essai$ ./essai
Passage d'argument par adresse
valeur initiale de x : 3
valeur finale de x : 6
Passage d'argument par adresse
valeur initiale de x : 3
valeur finale de x : 3
ubuntu@ubuntu-MS-7C08:/media/ubuntu/data/essai$

/* test.c */
#include <stdio.h>
void add_adr(int *x);
void add_val(int x);
int main()
{
int x = 3;
printf("Passage d'argument par adresse\n");
printf("valeur initiale de x : %d\n", x );
add_adr(&x);
printf("valeur finale de x : %d\n", x );
printf("Passage d'argument par adresse\n");
x=3;
printf("valeur initiale de x : %d\n", x );
add_val(x);
printf("valeur finale de x : %d\n", x );
return 0;
}

void add_adr(int x)
{
int y=
x;
*x=y+y;
return;
}

void add_val(int x)
{
int y=x;
x=y+y;
return;
}

[haut de page]

13) Utiliser des tableaux

int a[3]; tableau à une dimension
int a[3][4]; tableau à deux dimensions
...

Exemple

ubuntu@ubuntu-MS-7C08:/media/ubuntu/data/essai$ gcc -Wall test.c -o essai
ubuntu@ubuntu-MS-7C08:/media/ubuntu/data/essai$ ./essai
t[7999999] = 255
ubuntu@ubuntu-MS-7C08:/media/ubuntu/data/essai$

/* test.c */
#include <stdio.h>
#define N 8000000
int main()
{
unsigned char t[N];
int i;
for (i=0; i< N; i++){t[i] = 255;}
printf("t[%d] = %d\n",N-1,t[N-1]);
return 0;
}

Dès que le tableau est trop grand (taille supérieure à 8.000.000 pour un tableau de unsigned char par exemple), le programme retourne l'erreur suivante lors de l'exécution.
ubuntu@ubuntu-MS-7C08:/media/ubuntu/data/essai$ ./essai
Erreur de segmentation (core dumped)
Dans ce cas, il faut définir les tableaux immenses de la façon suivante :
unsigned char *t;
long N;
t = (unsigned char *)malloc((N+1)*sizeof(unsigned char));

[haut de page]

14) Passer un tableau à une fonction

Pour passer un tableau en paramètre à une fonction, on utilise l'une des trois syntaxes suivantes
void somme(int *t)
void somme(int t[10])
void somme(int t[])

Exemple

/* essai.c */
#include <stdio.h>
int somme(int t[]);
int main ()
{
int t[5] = {1, 2, 3, 4, 5};
int total;
total = somme(t); // t pointeur vers le tableau
printf( "Total = %d\n", total );
return 0;
}

int somme(int t[])
{
int total;
total=t[0]+t[1]+t[2]+t[3]+t[4];
return total;
}

[haut de page]

15) Retourner un tableau depuis une fonction

Pour retourner un tableau t[] depuis une fonction, il faut déclarer le tableau en variable static dans la fonction puis retourner non pas le tableau mais un pointeur vers ce tableau

Exemple

/* essai.c */
#include <stdio.h>
int * getTableau( );
int main ()
{
int *p;// pointeur vers un int
p = getTableau();
printf( "*p = %d\n", *p);
printf( "(p+1) = %d\n",*(p+1));
printf( "(p+2) = %d\n", *(p+2));
return 0;
}
#include <stdio.h>
int * getTableau( )
{
static int t[3];
t[0]=0; t[1]=1; t[2]=2;
printf( "t[0] = %d\n", t[0]);
printf( "t[1] = %d\n", t[1]);
printf( "t[2] = %d\n", t[2]);
return t;
}

[haut de page]

16) Utiliser des pointeurs

Un pointeur vers une variable contient l'adresse de cette variable en mémoire. Les pointeurs sont nécessaires pour pouvoir effectuer de l'allocation dynamique de mémoire et notamment pour pouvoir utiliser de très grands tableaux.
Il est recommandé d'attribuer une valeur NULL à une variable de pointeur
int*ptr = NULL;

Pour afficher une adresse avec la fonction printf(), sans warning à la compilation, il faut utiliser la syntaxe suivante :
printf("Adresse de x en hexadécimal: %p\n", (void*)&x );

Exemple

/* essai.c */
#include <stdio.h>
int main ()
{
int x=0;
printf("Adresse de x en hexadécimal: %p\n", (void*)&x ); return 0;
}

ubuntu@ubuntu-MS-7C08:/media/ubuntu/data/essai$ gcc -Wall test.c -o essai
ubuntu@ubuntu-MS-7C08:/media/ubuntu/data/essai$ ./essai
Adresse de x en hexadécimal: 0x7ffc59cd31a4

Syntaxe:
soit p un pointeur vers une variable x.
&x et p représentent l'adresse de x tandis que *p représente le contenu de la variable x

Exemple

/* essai.c */
#include <stdio.h>
int main ()
{
int x = 3;
int *p;
p = &x;
printf("&x : %p\n", (void*)&x );//adresse de x
printf("p : %p\n", (void*)p );//adresse de x
printf("*p : %d\n", *p ); //contenu de x
return (0);
}

ubuntu@ubuntu-MS-7C08:/media/ubuntu/data/essai$ ./essai
&x : 0x7ffca20a6b5c
p : 0x7ffca20a6b5c
*p : 3
ubuntu@ubuntu-MS-7C08:/media/ubuntu/data/essai$

[haut de page]

17) Utiliser des chaines de caractère

Les chaines de caractères (string) sont des tableaux à une dimension terminées par le caractère '\0'
char machaine[] = "bonjour";

Exemple

/* test.c */
#include <stdio.h>
int main()
{
char machaine[20+1] = "bonjour";
printf(" machaine= %s\n", machaine);
return 0;
}

[haut de page]

18) Utiliser des structures

La structure est un type de données qui permet de regrouper divers type de données. On la définie à l'aide du mot clef struct.

Exemple

ubuntu@ubuntu-MS-7C08:/media/ubuntu/data/essai$ gcc -Wall test.c -o essai
ubuntu@ubuntu-MS-7C08:/media/ubuntu/data/essai$ ./essai
h1 nom : Dupond
h1.prenom : Jean
h1.age : 32
ubuntu@ubuntu-MS-7C08:/media/ubuntu/data/essai$

/* test.c */
#include <stdio.h>
#include <string.h>
struct Humain //structure globale
{
char nom[50];
char prenom[50];
int age;
};
int main( )
{
struct Humain h1;
strcpy(h1.nom, "Dupond");
strcpy(h1.prenom, "Jean");
h1.age= 32;
printf( "h1 nom : %s\n", h1.nom);
printf( "h1.prenom : %s\n", h1.prenom);
printf( "h1.age : %d\n", h1.age);
return 0;
}

Pour définir et utiliser un pointeur p vers une structure Humain on écrit:
struct Humain *p
p=&h1
printf("%s",p->nom);

Exemple

/* test.c */
#include <stdio.h>
#include <string.h>
struct Humain //structure globale
{
char nom[50];
char prenom[50];
int age;
};
int main( )
{
struct Humain *p;
struct Humain h1;
strcpy(h1.nom, "Dupond");
strcpy(h1.prenom, "Jean");
h1.age= 32;
printf( "h1.nom : %s\n", h1.nom);
printf( "h1.prenom : %s\n", h1.prenom);
printf( "h1.age : %d\n", h1.age);
p=&h1;
printf( "p->nom : %s\n", p->nom);
printf( "p->prenom : %s\n", p->prenom);
printf( "p->age : %d\n", p->age);
return 0;
}

ubuntu@ubuntu-MS-7C08:/media/ubuntu/data/essai$ ./essai
h1.nom : Dupond
h1.prenom : Jean
h1.age : 32
Dupond
p->nom : Dupond
p->prenom : Jean
p->age : 32
ubuntu@ubuntu-MS-7C08:/media/ubuntu/data/essai$

[haut de page]

19) Utiliser les entrées et sorties

Une donnée d'entrée (input) peut provenir d'une saisie au clavier ou d'un fichier
Une donnée de sortie (output) peut être envoyée à l'écran, dans un fichier ou vers une imprimante.

Il existe trois fichiers principaux relatifs aux d'entrée-sortie
Le fichier d'entrée standard associé au clavier(pointeur de fichier stdin)
Le fichier de sortie standard associé à l'écran (pointeur de fichier stdout)
Le fichier de sortie des erreurs associé à l'écran (pointeur de fichier stderr)

Les pricnipales fonctions d'entrée et sortie sont
scanf ("format", variable)
printf ("format", variables...)

Exemple

ubuntu@ubuntu-MS-7C08:/media/ubuntu/data/essai$ ./essai
Entrez un nombre :
7
x=7
Entrez une chaine de caractères :
bonjour
s=bonjour
ubuntu@ubuntu-MS-7C08:/media/ubuntu/data/essai$

/* test.c */
#include <stdio.h>
int main( )
{
char s[80+1];
int x;
printf("Entrez un nombre :\n");
scanf("%d", &x);
printf( "x=%d\n", x);
printf("Entrez une chaine de caractères :\n");
scanf("%s", s);
printf( "s=%s\n", s);
return 0;
}

[haut de page]

20) Manipuler des fichiers

On distingue les fichiers texte et les fichiers binaires. Ces deux types de fichiers sont composés d'une suite d'octets.
La différence entre ces deux types de fichiers c'est que les octets contenus dans le fichiers texte correspondent à des caractères affichables à l'écran, ce qui n'est pas le cas pour les fichiers binaires.

Pour manipuler les fichiers, on commence par déclarer les fichier d'entête stdio.h et stdlib.h
#include <stdio.hh>
#include <stdlib.hh>

Pour définir un fichier, on déclare un pointeur vers un objet FILE
FILE *pf;

Pour ouvrir un fichier on utilise la fonction fopen()
pf= fopen(const char * nom_fichier, const char * mode);

Pour fermer un fichier on utilise la fonction fclose()
int fclose(FILE *pf);

Remarque :
Pour ouvrir un fichier texte, on utilise les modes suivants:
r : ouvre un fichier texte existant en lecture seule.
w : ouvre un fichier texte existant en écriture seule et crée le fichier s'il n'existe pas
a : ouvre un fichier texte existant en écriture, en mode ajout, et crée le fichier s'il n'existe pas
r+: ouvre un fichier texte existant en lecture et écriture (le fichier doit exister)
w+: ouvre un fichier texte existant en lecture et écriture en vidant d'abord le fichier (crée le fichier s'il n'existe pas)
a+: ouvre un fichier texte existant en lecture et écriture et crée le fichier s'il n'existe pas. la lecture commence au début du fichier et l'écriture à la fin.

• Pour ouvrir un fichier binaire, on utilise les modes suivants (par rapport aux modes précédents, on ajoute la lettre b qui signifie binaire):
rb, wb, ab, rb+, r+b, wb+, w+b, ab+, a+b

• Pour lire un caractère d'un fichier texte, on utilise la fonction fgetc
int fgetc( FILE * fp );

• Pour lire n-1 caractères d'un fichier texte et les placer dans un tableau t, on utilise la fonction fgets (cette fonction ajoute le caractère de fin de chaine '\0' après les n-1 caractères lus)
char *fgets(char *t, int n, FILE *pf);

• Pour écrire un caractère dans un fichier texte, on utilise la fonction fputc
int fputc( int c, FILE *pf );

• Pour écrire une chaine de caractères s (terminée par le caractère de fin de chaine '\0') dans un fichier texte, on utilise la fonction fputs
int fputs( const char *s, FILE *pf );

• Pour lire n octets d'un fichier binaire et les placer dans un tableau t, on utilise la fonction fread (taille est la taille du bloc lu, en octets, et nombre est le nombre de blocs lus)
size_t fread(void *t, size_t taille, size_t nombre, FILE *a_file);

• Pour écrire dans fichier binaire, n octets d'un un tableau t, on utilise la fonction fwrite (taille est la taille du bloc écrit, en octets, et nombre est le nombre de blocs écrit)
size_t fwrite(const void *t, size_t taille, size_t nombre, FILE *a_file);

Remarque : le type size_t dépend du système d'exploitation utilisé. Sous Linux 64bits, il correspond en principe au type long.

Exemple
Création d'un tableau t de nbytes octets et enregistrement des octets dans le fichier sauv.dat

#include <stdio.h>
#include <stdlib.h>
int main ()
{
FILE *pf;
unsigned char *t=NULL;//pointeur vers un tableau d'octets
long nbytes=1000;
int i;
t = (unsigned char )malloc((nbytes+1)sizeof(unsigned char));
for( i = 0; i < nbytes; i++){t[i]= 255;}
pf = fopen("sauv.dat","w+b");
for (i=0;i<nbytes;i++){ fprintf (pf,"%c", t[i]);}
fclose(pf);
free(t);
printf("Fin du traitement\n");
return (0);
}

On peut remplacer
for (i=0;i<nbytes;i++){ fprintf (pf,"%c", t[i]);}
par
fwrite(t, nbytes, 1, pf);