VS | ||
Data da primeira versão | 1972 | |
---|---|---|
Paradigma | Imperativo , procedimental , estruturado | |
Autor | Dennis Ritchie , Brian Kernighan | |
Desenvolvedor | Dennis Ritchie e Kenneth Thompson, Bell Labs | |
Digitando | Estático , fraco | |
Padrões | ANSI X3.159-1989 (ANSI C, C89) ISO / IEC 9899: 1990 (C90) ISO / IEC 9899: 1990 / AMD1: 1995 (C95) ISO / IEC 9899: 1999 (C99) ISO / IEC 9899: 2011 ( C11) ISO / IEC 9899: 2018 (C18) |
|
Influenciado por | BCPL , B , Algol 68 , Fortran | |
Influenciado | awk , csh , C ++ , C # , Objective-C , D , C simultâneo , Java , JavaScript , PHP , Perl | |
Implementações | GCC , MSVC , Borland C , Clang , TCC | |
Extensões de arquivo | .CH | |
É uma linguagem de programação imperativa de baixo nível e propósito geral . Inventado no início dos anos 1970 para reescrever o UNIX , C se tornou uma das linguagens mais amplamente usadas, até hoje. Muitas outras linguagens modernas, como C ++ , C # , Java e PHP ou Javascript , adotaram uma sintaxe semelhante a C e retomaram parcialmente sua lógica. C oferece ao desenvolvedor uma margem significativa de controle sobre a máquina (em particular sobre o gerenciamento de memória) e, portanto, é usado para construir as “bases” (compiladores, interpretadores, etc.) dessas linguagens mais modernas.
A linguagem C foi inventada em 1972 nos Laboratórios Bell . Ele foi desenvolvido ao mesmo tempo que o UNIX por Dennis Ritchie e Kenneth Thompson. Kenneth Thompson desenvolveu um predecessor de C, a linguagem B , que por sua vez é inspirada na BCPL . Dennis Ritchie desenvolveu a linguagem B para uma nova versão que era diferente o suficiente, incluindo a adição de tipos , de modo que foi chamada de C.
Embora C seja oficialmente inspirado por B e BCPL, notamos uma forte influência de PL / I (ou PL360); poderíamos dizer que C foi para Unix e PDP-11 o que PL / I foi para reescrever o Multics .
Posteriormente, Brian Kernighan ajudou a popularizar a linguagem C. Também fez algumas alterações de última hora.
Em 1978 , Kernighan foi o autor principal do livro The C Programming Language que descreve a linguagem finalmente estabilizada; Ritchie cuidou dos apêndices e exemplos com o Unix. Este livro também é chamado de "o K&R", e falamos de C ou C K&R tradicional quando nos referimos à linguagem como ela existia naquela época.
Em 1983 , o American National Standards Institute (ANSI) formou um comitê de padrões de linguagem (X3J11) que em 1989 culminou no chamado padrão ANSI C ou C89 (formalmente ANSI X3.159-1989). Em 1990 , esse padrão também foi adotado pela Organização Internacional de Padronização ( C90, C ISO , formalmente ISO / IEC 9899: 1990). ANSI C é uma evolução do K&R C que permanece extremamente compatível. Ele pega algumas idéias de C ++ , em particular a noção de protótipo e qualificadores de tipo.
Entre 1994 e 1996 , o grupo de trabalho ISO (ISO / IEC JTC1 / SC22 / WG14) publicou duas correções e uma emenda para C90: ISO / IEC 9899 / COR1: 1994 Technical Corrigendum 1 , ISO / IEC 9899 / AMD1: 1995 Integridade de C e ISO / IEC 9899 / COR1: 1996 Technical Corrigendum 2 . Essas mudanças bastante modestas são às vezes chamadas de C89 com alteração 1 ou C94 / C95. Três arquivos de cabeçalho foram adicionados, dois dos quais relacionados a caracteres largos e outro definindo uma série de macros relacionadas ao padrão de caracteres ISO 646 .
Em 1999 , um novo desenvolvimento da linguagem foi padronizado pela ISO : C99 (formalmente ISO / IEC 9899: 1999). Os novos recursos incluem arrays de tamanho variável, ponteiros restritos, números complexos, literais compostos, instruções mistas com instruções, funções embutidas , suporte avançado de ponto flutuante e sintaxe de comentário C ++. A biblioteca padrão C 99 foi aprimorada com seis arquivos de cabeçalho desde o padrão anterior.
Em 2011 , a ISO ratifica uma nova versão da norma: C11 , formalmente ISO / IEC 9899: 2011. Esta mudança introduziu incluindo suporte para programação multi-threaded , as expressões em tal genérico e melhor suporte de Unicode .
É uma linguagem de programação imperativa e de propósito geral. É qualificada como uma linguagem de baixo nível no sentido de que cada instrução da linguagem é projetada para ser compilada em um número de instruções de máquina que é bastante previsível em termos de ocupação de memória e carga de cálculo. Além disso, oferece uma gama de número inteiro e ponto flutuante tipos concebido para ser capaz de corresponder directamente para os tipos de dados suportados pelo processador . Finalmente, faz uso intensivo de cálculos de endereço de memória com o conceito de ponteiro .
Além dos tipos básicos, C suporta tipos enumerados , compostos e opacos . No entanto, não oferece nenhuma operação que processe diretamente objetos de nível superior ( arquivo de computador , cadeia de caracteres , lista , tabela de hash , etc.). Esses tipos mais evoluídos devem ser tratados por meio da manipulação de ponteiros e tipos compostos. Da mesma forma, a linguagem não oferece, como padrão, o gerenciamento de programação orientada a objetos , ou um sistema de gerenciamento de exceções . Existem funções padrão para gerenciar entrada-saída e cadeias de caracteres , mas, ao contrário de outras linguagens, nenhum operador específico para melhorar a ergonomia. Isso facilita a substituição de funções padrão por funções projetadas especificamente para um determinado programa.
Essas características o tornam uma linguagem preferida ao tentar controlar os recursos de hardware usados, a linguagem de máquina e os dados binários gerados pelos compiladores sendo relativamente previsíveis. Esta linguagem é, portanto, amplamente utilizada em campos como programação embarcada em microcontroladores , cálculos intensivos, escrita de sistemas operacionais e módulos onde a velocidade de processamento é importante. É uma boa alternativa à linguagem assembly nessas áreas, com as vantagens de uma sintaxe mais expressiva e portabilidade do código-fonte . A linguagem C foi inventada para escrever o sistema operacional UNIX e ainda é usada para programação de sistema. Assim, o kernel de grandes sistemas operacionais como Windows e Linux são desenvolvidos amplamente em C.
Por outro lado, o desenvolvimento de programas em C, especialmente se eles usam estruturas de dados complexas, é mais difícil do que com linguagens de nível superior. Na verdade, por uma questão de desempenho, a linguagem C exige que o usuário programe certos processos (liberar a memória, verificar a validade dos índices nas tabelas, etc.) que são automaticamente atendidos em linguagens de alto nível.
Sem as conveniências fornecidas por sua biblioteca padrão, C é uma linguagem simples, assim como seu compilador . Isso é sentido no tempo de desenvolvimento de um compilador C para uma nova arquitetura de processador : Kernighan e Ritchie estimaram que poderia ser desenvolvido em dois meses porque "notaremos que 80% do código de um novo compilador são idênticos aos do códigos de outros compiladores existentes. "
É uma das linguagens mais utilizadas porque:
Suas principais desvantagens são:
O programa Hello world é oferecido como exemplo em 1978 em The C Programming Language por Brian Kernighan e Dennis Ritchie . A criação de um programa exibindo " hello world " tornou-se o exemplo ideal para mostrar o básico de um novo idioma. Aqui está o exemplo original do 1 st Edição 1978
main() { printf("hello, world\n"); }O mesmo programa, de acordo com o padrão ISO e seguindo as boas práticas contemporâneas:
#include <stdio.h> int main(void) { printf("hello, world\n"); return 0; }A sintaxe do C foi projetada para ser breve. Historicamente, muitas vezes foi comparado ao de Pascal , uma linguagem imperativa também criada na década de 1970 . Aqui está um exemplo com uma função fatorial :
/* En C (norme ISO) */ int factorielle(int n) { return n > 1 ? n * factorielle(n - 1) : 1; } { En Pascal } function factorielle(n: integer) : integer begin if n > 1 then factorielle := n * factorielle(n - 1) else factorielle := 1 end.Onde Pascal 7 usa palavras-chave ( function, integer, begin, if, then, elsee end), C utiliza apenas 2 ( inte return).
Linguagem das expressõesA brevidade de C não é apenas sobre sintaxe. O grande número de operadores disponíveis, o fato de que a maioria das instruções contém uma expressão, que as expressões quase sempre produzem um valor e as instruções de teste simplesmente comparam o valor da expressão testada com zero, tudo contribui para a brevidade do código-fonte.
Aqui está o exemplo de uma função de cópia de string - cujo princípio é copiar os caracteres até que você tenha copiado o caractere nulo, que por convenção marca o fim de uma string em C - fornecido em The C Programming Language, 2ª edição , p. 106 :
void strcpy(char *s, char *t) { while (*s++ = *t++) ; }O Loop whileusa um estilo de escrita C clássico, o que ajudou a dar-lhe a reputação de ser uma linguagem pouco legível. A expressão *s++ = *t++contém: duas desreferências de ponteiro ; incrementos de dois ponteiros; uma atribuição ; e o valor atribuído é comparado com zero pelo while. Este loop não tem corpo, pois todas as operações são realizadas na expressão de teste de while. Consideramos que é necessário dominar este tipo de notação para dominar C.
Para comparação, uma versão que não usa operadores abreviados e a comparação implícita de zero daria:
void strcpy(char *s, char *t) { while (*t != '\0') { *s = *t; s = s + 1; t = t + 1; } *s = *t; }Um programa escrito em C geralmente é dividido em vários arquivos-fonte compilados separadamente.
Os arquivos de origem C são arquivos de texto , geralmente na codificação de caracteres do sistema host. Eles podem ser escritos com um editor de texto simples . Existem muitos editores, até mesmo ambientes de desenvolvimento integrado (IDEs), que têm funções específicas para suportar a escrita de fontes C.
O objetivo é dar às extensões o nome do arquivo .c e .hos arquivos de origem C. Os arquivos .hsão chamados de arquivos de cabeçalho , cabeçalho em inglês . Eles são projetados para serem incluídos no início dos arquivos de origem e contêm apenas instruções .
Quando um arquivo .cou .husa um identificador declarado em outro arquivo .h, inclui o último. O princípio geralmente aplicado consiste em escrever um arquivo .hpara cada arquivo .c, e declarar no arquivo .htudo o que é exportado pelo arquivo .c.
A geração de um executável a partir dos arquivos de origem é feita em várias etapas, que geralmente são automatizadas com o uso de ferramentas como make , SCons ou ferramentas específicas para um ambiente de desenvolvimento integrado. Há quatro etapas que conduzem a partir da fonte para o arquivo executável: a pré-compilação , compilação , montagem , edição de ligação . Quando um projeto é compilado, apenas os arquivos .cfazem parte da lista de arquivos a compilar; os arquivos .hsão incluídos pelas diretivas do pré-processador contidas nos arquivos de origem.
O pré-processador C executa as diretivas contidas nos arquivos de origem. Ele os reconhece pelo fato de que estão no início da linha e todos começam com o caractere cruzado # . Algumas das diretrizes mais comuns incluem:
Além de executar as diretivas, o pré-processador substitui comentários por espaço em branco e substitui macros. Para o resto, o código-fonte é transmitido como está para o compilador para a próxima fase. No entanto, cada um #includeno código-fonte deve ser substituído recursivamente pelo código-fonte incluído. Assim, o compilador recebe uma única fonte do pré-processador, que constitui a unidade de compilação.
Aqui está um exemplo de arquivo de origem que copyarray.hfaz uso clássico de diretivas de pré-processador:
#ifndef COPYARRAY_H #define COPYARRAY_H #include <stddef.h> void copyArray(int *, size_t); #endifAs diretivas #ifndef, #definee #endifgarantem que o código interno seja compilado apenas uma vez, mesmo que seja incluído várias vezes. A diretiva #include <stddef.h>inclui o cabeçalho que declara o tipo size_tusado abaixo.
A fase de compilação geralmente consiste na geração do código de montagem . Esta é a fase mais intensiva dos tratamentos. É executado pelo próprio compilador. Para cada unidade de compilação, obtemos um arquivo em linguagem assembly.
Esta etapa pode ser dividida em subetapas:
Por abuso de linguagem, chama-se compilação toda a fase de geração de um arquivo executável a partir dos arquivos fonte. Mas esta é apenas uma das etapas que levam à criação de um executável.
Alguns compiladores C funcionam neste nível em duas fases, a primeira gerando um arquivo compilado em uma linguagem intermediária destinada a uma máquina virtual ideal (ver Bytecode ou P-Code ) portátil de uma plataforma para outra, a segunda convertendo a linguagem intermediária em assembly o idioma depende da plataforma de destino. Outros compiladores C tornam possível não gerar uma linguagem assembly, mas apenas o arquivo compilado em linguagem intermediária , que será interpretado ou compilado automaticamente em código nativo em tempo de execução na máquina de destino (por uma máquina virtual que será vinculada no final programa).
Esta etapa consiste em gerar um arquivo objeto em linguagem de máquina para cada arquivo de código assembly. Os arquivos objeto são geralmente extensão .ono Unix , e .objcom as ferramentas de desenvolvimento para MS-DOS , Microsoft Windows , VMS , CP / M ... Esta fase às vezes é agrupada com a anterior estabelecendo um fluxo de dados interno sem passar por arquivos em linguagem intermediária ou linguagem assembly. Nesse caso, o compilador gera diretamente um arquivo-objeto.
Para compiladores que geram código intermediário, esta fase de montagem também pode ser completamente eliminada: é uma máquina virtual que interpretará ou compilará essa linguagem em código de máquina nativo. A máquina virtual pode ser um componente do sistema operacional ou uma biblioteca compartilhada.
A vinculação é a última etapa e visa reunir todos os elementos de um programa. Os vários arquivos-objeto são então reunidos, assim como as bibliotecas estáticas, para produzir apenas um arquivo executável.
O objetivo da vinculação é selecionar elementos de código úteis presentes em um conjunto de códigos e bibliotecas compilados e resolver referências mútuas entre esses diferentes elementos para permitir que eles façam referência direta à execução do programa. A vinculação falha se os elementos de código referenciados estiverem ausentes.
O conjunto de caracteres ASCII é suficiente para escrever em C. É até possível, mas incomum, restringir-se ao conjunto de caracteres invariáveis do padrão ISO 646 , usando sequências de escape chamadas trigraph. Normalmente, as fontes C são escritas com o conjunto de caracteres do sistema host. No entanto, é possível que o conjunto de caracteres de execução não seja o da fonte.
C é sensível a maiúsculas e minúsculas . Os caracteres brancos ( espaço , tabulação , fim de linha ) podem ser usados livremente para o layout, pois são equivalentes a um único espaço na maioria dos casos.
O C89 possui 32 palavras-chave, cinco das quais não existiam no K&R C, e que estão em ordem alfabética:
auto, break, case, char, const(C89), continue, default, do, double, else, enum(C89), extern, float, for, goto, if, int, long, register, return, short, signed(C89), sizeof, static, struct, switch, typedef, union, unsigned, void(C89), volatile(C89) while.Estes são termos reservados e não devem ser usados de outra forma.
A revisão C99 adiciona cinco:
_Bool, _Complex, _Imaginary, inline, restrict.Essas novas palavras-chave começam com uma letra maiúscula prefixada com um sublinhado para maximizar a compatibilidade com os códigos existentes. Os cabeçalhos da biblioteca padrão fornecem os aliases bool( <stdbool.h>) complexe imaginary( <complex.h>).
A última revisão, C11, apresenta outras sete novas palavras-chave com as mesmas convenções:
_Alignas, _Alignof, _Atomic, _Generic, _Noreturn, _Static_assert, _Thread_local.Os cabeçalhos padrão <stdalign.h>, <stdnoreturn.h>, <assert.h>e <threads.h>fornecer pseudónimos respectivamente alignase alignof, noreturn, static_assert, e thread_local.
O pré-processador da linguagem C oferece as seguintes diretivas:
#include, #define, #pragma(C89) #if, #ifdef, #ifndef, #elif(C89), #else, #endif, #undef, #line, #error.A linguagem C compreende muitos tipos de inteiros , ocupando mais ou menos bits . O tamanho dos tipos é apenas parcialmente padronizado: o padrão define apenas um tamanho mínimo e uma magnitude mínima. Magnitudes mínimas são compatíveis com representações binárias diferentes do complemento de dois , embora essa representação seja quase sempre usada na prática. Essa flexibilidade permite que a linguagem seja eficientemente adaptada a uma ampla variedade de processadores , mas complica a portabilidade de programas escritos em C.
Cada tipo de inteiro tem uma forma “com sinal” que pode representar números negativos e positivos, e uma forma “sem sinal” que só pode representar números naturais . Formas assinadas e não assinadas devem ter o mesmo tamanho.
O tipo mais comum é int, ele representa a palavra máquina.
Ao contrário de muitas outras linguagens, o tipo charé um tipo inteiro como qualquer outro, embora seja geralmente usado para representar caracteres. Seu tamanho é, por definição, um byte .
Modelo | Capacidade mínima de representação | Magnitude mínima exigida pelo padrão |
---|---|---|
char | como signed charou unsigned char, dependendo da implementação | 8 bits |
signed char | -127 a 127 | |
unsigned char (C89) | 0 a 255 | |
short signed short |
-32 767 a 32 767 | 16 bits |
unsigned short | 0 a 65.535 | |
int signed int |
-32 767 a 32 767 | 16 bits |
unsigned int | 0 a 65.535 | |
long signed long |
-2 147 483 647 a 2147 483 647 | 32 bits |
unsigned long | 0 a 4.294.967.295 | |
long long(C99) signed long long(C99) |
−9 223 372 036 854 775 807 a 9 223 372 036 854 775 807 | 64 bits |
unsigned long long (C99) | 0 a 18.446.744.073 709.552.000 |
Os tipos listados são definidos com a palavra-chave enum.
Existem tipos de número de ponto flutuante , precisão, portanto comprimento em bits, variável; em ordem ascendente:
Modelo | Precisão | Magnitude |
---|---|---|
float | ≥ 6 dígitos decimais | cerca de 10 -37 a 10 +37 |
double | ≥ 10 dígitos decimais | cerca de 10 -37 a 10 +37 |
long double | ≥ 10 dígitos decimais | cerca de 10 -37 a 10 +37 |
long double (C89) | ≥ 10 dígitos decimais |
C99 adicionado float complex, double complexe long double complex, representando os números complexos associados.
Tipos elaborados:
O tipo _Boolé padronizado pelo C99. Em versões anteriores da linguagem, era comum definir um sinônimo:
typedef enum boolean {false, true} bool;O tipo voidrepresenta o vazio, como uma lista de parâmetros de função vazia ou uma função que não retorna nada.
O tipo void*é o ponteiro genérico: qualquer ponteiro de dados pode ser convertido implicitamente de e para void*. É, por exemplo, o tipo retornado pela função padrão malloc, que aloca memória. Este tipo não é adequado para operações que requeiram saber o tamanho do tipo apontado (aritmética de ponteiros, desreferenciação).
C suporta tipos compostos com a noção de estrutura . Para definir uma estrutura, você deve usar a palavra-chave structseguida do nome da estrutura. Os membros devem então ser declarados entre colchetes. Como qualquer instrução, um ponto-e-vírgula termina tudo.
/* Déclaration de la structure personne */ struct Personne { int age; char *nom; };Para acessar os membros de uma estrutura, você deve usar o operador ..
int main() { struct Personne p; p.nom = "Albert"; p.age = 46; }As funções podem receber ponteiros para estruturas. Eles funcionam com a mesma sintaxe dos ponteiros clássicos. No entanto, o operador ->deve ser usado no ponteiro para acessar os campos da estrutura. Também é possível desreferenciar o ponteiro para não usar este operador e ainda usar o operador ..
void anniversaire(struct Personne * p) { p->age++; printf("Joyeux anniversaire %s !", (*p).nom); } int main() { struct Personne p; p.nom = "Albert"; p.age = 46; anniversaire(&p); }Nas versões de C anteriores a C99, os comentários tinham que começar com uma barra e um asterisco ("/ *") e terminar com um asterisco e uma barra. Quase todas as linguagens modernas usaram essa sintaxe para escrever comentários no código. Tudo entre esses símbolos é o comentário, incluindo uma quebra de linha:
/* Ceci est un commentaire sur deux lignes ou plus */O padrão C99 substituiu os comentários de fim de linha do C ++, introduzidos por duas barras e terminando com uma linha:
// Commentaire jusqu'à la fin de la ligneA sintaxe das várias estruturas de controle existentes em C é amplamente utilizada em várias outras linguagens, como C ++ é claro, mas também Java , C # , PHP ou mesmo JavaScript .
Os três principais tipos de estruturas estão presentes:
Funções em C são blocos de instruções, recebendo um ou mais argumentos e podendo retornar um valor. Se uma função não retornar nenhum valor, a palavra void- chave será usada. Uma função também não pode receber argumentos. A palavra void- chave é recomendada neste caso.
// Fonction ne retournant aucune valeur (appelée procédure) void afficher(int a) { printf("%d", a); } // Fonction retournant un entier int somme(int a, int b) { return a + b; } // Fonction sans aucun argument int saisir(void) { int a; scanf("%d", &a); return a; } ProtótipoUm protótipo consiste em declarar uma função e seus parâmetros sem as instruções que a compõem. Um protótipo termina com um ponto e vírgula.
// Prototype de saisir int saisir(void); // Fonction utilisant saisir int somme(void) { int a = saisir(), b = saisir(); return a + b; } // Définition de saisir int saisir(void) { int a; scanf("%d", &a); return a; }Normalmente, todos os protótipos são gravados em arquivos .h e as funções são definidas em um arquivo .c .
O padrão da linguagem C deixa deliberadamente certas operações não especificadas. Esta propriedade de C permite que os compiladores usem diretamente instruções específicas do processador , para realizar otimizações ou ignorar certas operações, para compilar programas executáveis curtos e eficientes. Em contrapartida, às vezes é a causa de bugs de portabilidade do código-fonte escrito em C.
Existem três categorias de tal comportamento:
Em C, os comportamentos definidos pela implementação são aqueles em que a implementação deve escolher um comportamento e segui-lo. Esta escolha pode ser gratuita ou de uma lista de possibilidades fornecida pela norma. A escolha deve ser documentada pela implementação, para que o programador possa conhecê-la e utilizá-la.
Um dos exemplos mais importantes desse comportamento é o tamanho dos tipos de dados inteiros. O padrão C especifica o tamanho mínimo para tipos de base, mas não seu tamanho exato. Assim, o tipo intpor exemplo, correspondente à palavra de máquina , deve ter um tamanho mínimo de 16 bits . Ele pode ter 16 bits em um processador de 16 bits e 64 bits em um processador de 64 bits.
Outro exemplo é a representação de inteiros com sinal. Pode ser o complemento de dois , o complemento de um ou um sistema com um bit de sinal e bits de valor (en) . A grande maioria dos sistemas modernos usa o complemento de dois, que é, por exemplo, o único ainda suportado pelo GCC. Os sistemas antigos usam os outros formatos, como o IBM 7090 que usa o formato sinal / valor, o PDP-1 ou o UNIVAC e seus descendentes, alguns dos quais ainda são usados hoje como o UNIVAC 1100/2200 series # UNISYS 2200 series (en) , que usa o complemento de um.
Outro exemplo é o deslocamento para a direita de um número inteiro negativo com sinal. Normalmente, a implementação pode escolher se deslocar como para um inteiro sem sinal ou propagar o bit mais significativo que representa o sinal.
Comportamentos não especificadosOs comportamentos não especificados são semelhantes aos comportamentos definidos pela implementação, mas o comportamento adotado pela implementação não precisa ser documentado. Nem precisa ser igual em todas as circunstâncias. No entanto, o programa permanece correto, o programador simplesmente não pode confiar em uma regra particular.
Por exemplo, a ordem de avaliação dos parâmetros durante uma chamada de função não é especificada. O compilador pode até optar por avaliar os parâmetros de duas chamadas à mesma função em uma ordem diferente, se isso ajudar na sua otimização.
Comportamentos indefinidosO padrão C define certos casos em que construções sintaticamente válidas têm comportamento indefinido. De acordo com o padrão, então tudo pode acontecer: a compilação pode falhar, ou então produzir um executável cuja execução será interrompida, ou que produzirá resultados falsos, ou mesmo que dará a aparência de funcionar sem erros. Quando um programa contém um comportamento indefinido, é o comportamento de todo o programa que se torna indefinido, não apenas o comportamento da instrução que contém o erro. Assim, uma instrução errada pode corromper dados que serão processados muito mais tarde, atrasando assim a manifestação do erro. E mesmo sem ser executada, uma instrução errada pode fazer com que o compilador execute otimizações com base em suposições erradas, resultando em um executável que não faz nada do que se espera.
ExemplosPodemos apontar a divisão clássica por zero , ou a atribuição múltipla de uma variável na mesma expressão com o exemplo:
int i = 4; i = i++; /* Comportement indéfini. */Assim, pode-se pensar que neste exemplo ipode valer 4 ou 5 dependendo da escolha do compilador, mas também pode valer 42 ou a atribuição pode parar a execução ou o compilador pode recusar a compilação. Nenhuma garantia existe assim que existir um comportamento indefinido.
Para citar apenas alguns exemplos, desreferenciar um ponteiro nulo, acessar uma matriz fora de seus limites, usar uma variável não inicializada ou transbordar de inteiros assinados têm comportamentos indefinidos. O compilador pode usar o fato de que um build é indefinido em alguns casos para assumir que esse caso nunca ocorre e otimizar o código de forma mais agressiva. Embora o exemplo acima possa parecer óbvio, alguns exemplos complexos podem ser muito mais sutis e, às vezes, levar a erros graves.
Por exemplo, muitos códigos contêm verificações para evitar a execução em casos fora dos limites, que podem ter a seguinte aparência:
char buffer[BUFLEN]; char *buffer_end = buffer + BUFLEN; unsigned int len; /* ... */ if (buffer + len >= buffer_end || /* vérification de dépassement du buffer */ buffer + len < buffer) /* vérification de débordement si len très large */ return; /* Si pas de débordement, effectue les opérations prévues */ /* ... */Aparentemente, esse código é cuidadoso e executa as verificações de segurança necessárias para não estourar o buffer alocado. Na prática, versões recentes de compiladores como GCC , Clang ou Microsoft Visual C ++ podem suprimir o segundo teste e possibilitar estouros. Na verdade, o padrão especifica que a aritmética do ponteiro em um objeto não pode fornecer um ponteiro fora desse objeto. O compilador pode, portanto, decidir que o teste ainda é falso e excluí-lo. A verificação correta é a seguinte:
char buffer[BUFLEN]; unsigned int len; /* ... */ if (len >= BUFLEN) /* vérification de dépassement du buffer */ return; /* Si pas de débordement, effectue les opérations prévues */ /* ... */Em 2008, quando os desenvolvedores do GCC modificaram o compilador para otimizar certas verificações de estouro que eram baseadas em comportamentos indefinidos, o CERT emitiu um aviso sobre o uso de versões recentes do GCC . Essas otimizações estão de fato presentes na maioria dos compiladores modernos, o CERT revisou sua isenção de responsabilidade para esse efeito.
Existem algumas ferramentas para detectar essas compilações problemáticas e os melhores compiladores detectam algumas delas (às vezes você precisa habilitar opções específicas) e pode sinalizá-las, mas nenhuma afirma ser exaustiva.
A biblioteca padrão padronizada, disponível com todas as implementações, apresenta a simplicidade associada a uma linguagem de baixo nível. Aqui está uma lista de alguns cabeçalhos que declaram tipos e funções da biblioteca padrão:
A biblioteca padrão padronizada não oferece suporte para interface gráfica , rede, E / S em porta serial ou paralela, sistemas em tempo real , processos ou até mesmo tratamento avançado de erros (como com exceções estruturadas). Isso poderia restringir ainda mais a portabilidade prática de programas que precisam usar alguns desses recursos, sem a existência de muitas bibliotecas portáteis e compensando essa falta; no mundo UNIX , essa necessidade também levou ao surgimento de outro padrão, POSIX .1.
Sendo a linguagem C uma das linguagens de programação mais utilizadas, muitas bibliotecas foram criadas para serem utilizadas com C: glib , etc. Freqüentemente, ao inventar um formato de dados , existe uma biblioteca de referência C ou software para manipular o formato. Esse é o caso de zlib , libjpeg , libpng , Expat , decodificadores de referência MPEG , libsocket, etc.
Aqui estão alguns exemplos apresentando brevemente algumas propriedades de C. Para obter mais informações, consulte o WikiBook "Programação C" .
A estrutura int_listrepresenta um elemento de uma lista vinculada de inteiros. As duas funções a seguir ( insert_nexte remove_next) são usadas para adicionar e remover um item da lista.
/* La gestion de la mémoire n'est pas intégrée au langage mais assurée par des fonctions de la bibliothèque standard. */ #include <stdlib.h> struct int_list { struct int_list *next; /* pointeur sur l'élément suivant */ int value; /* valeur de l'élément */ }; /* * Ajouter un élément à la suite d'un autre. * node : élément après lequel ajouter le nouveau * value : valeur de l'élément à ajouter * Retourne : adresse de l'élément ajouté, ou NULL en cas d'erreur. */ struct int_list *insert_next(struct int_list *node, int value) { /* Allocation de la mémoire pour un nouvel élément. */ struct int_list *const new_next = malloc(sizeof *new_next); /* Si l'allocation a réussi, alors insérer new_next entre node et node->next. */ if (new_next) { new_next->next = node->next; node->next = new_next; new_next->value = value; } return new_next; } /* * Supprimer l'élément suivant un autre. * node : élément dont le suivant est supprimé * Attention : comportement indéterminé s'il n'y pas d'élément suivant ! */ void remove_next(struct int_list *node) { struct int_list *const node_to_remove = node->next; /* Retire l'élément suivant de la liste. */ node->next = node->next->next; /* Libère la mémoire occupée par l'élément suivant. */ free(node_to_remove); }Neste exemplo, as duas funções essenciais são malloce free. O primeiro serve para alocar memória, o parâmetro que recebe é a quantidade de bytes que queremos alocar e retorna o endereço do primeiro byte que foi alocado, caso contrário retorna NULL. freeé usado para liberar a memória que foi alocada por malloc.