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
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
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$
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$
4) Utiliser le mot clef const
Le mot clef const permet de définiir une vairaible prenant une valeur constante.
const int LENGTH = 10;
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);
}
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);
}
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
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
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$
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; }
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
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;
}
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));
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;
}
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;
}
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$
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;
}
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$
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;
}
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>
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);