O Desenvolvimento da Linguagem C*
Dennis M. Ritchie
Bell Labs/Lucent Technologies
Murray Hill, NJ 07974 USA
RESUMO
A linguagem de programação C foi inventada no começo dos anos 70 como uma linguagem de implementação de sistema para o nascente sistema operacional Unix. Derivada da linguagem sem tipos BCPL, ela evoluiu para um modelo estruturado; criada numa minúscula máquina como uma ferramenta para melhorar um ambiente de programação escasso, ela tornou-se um das linguagens dominantes de hoje. Este documento estuda sua evolução.
Introdução
NOTA: *Copyright 1993 Association for Computing Machinery, Inc. Esta reimpressão eletrônica tornou-se disponível pelo autor como uma cortesia. Para direitos de publicação adicionais contate a ACM ou o autor. Este artigo foi apresentado na Segunda Conferência sobre História das Linguagens de Programação, Cambridge, Mass., Abril de 1993.
Este documento fala sobre o desenvolvimento da linguagem de programação C, as influências sobre ela e as condições sob as quais ela foi criada. Por causa da brevidade, eu omiti a completa descrição da linguagem C e suas parentas ancestrais, as linguagens B [Johnson 73] e BCPL [Richards 79], concentrando-me nos elementos característicos de cada linguagem e como eles evoluíram.
A linguagem C tomou existência nos anos de 1969 - 1973, em paralelo com o primitivo desenvolvimento do sistema operacional Unix; o período mais criativo ocorreu durante 1972. Outra inundação de mudanças apareceu entre 1977 e 1979, quando a portabilidade do sistema Unix estava sendo demonstrada. No meio deste segundo período, a primeira grande descrição disponível da linguagem apareceu: A Linguagem de Programação C, freqüentemente chamada de 'livro branco' ou 'K&R' [Kernighan 78]. Finalmente, na metade dos anos 80, a linguagem foi oficialmente padronizada pelo comitê ANSI X3J11, o qual fez novas mudanças. Até o começo dos anos 80, apesar de existirem compiladores para uma variedade de arquiteturas de máquinas e sistemas operacionais, a linguagem foi quase exclusivamente associada com o Unix; mais recentemente, ela tem difundido-se mais extensamente, e hoje está entre a linguagens mais comumente usada por toda a indústria de computação.
História: o cenário
O final dos anos 60 foi uma era turbulenta para a pesquisa de sistemas de computadores no Bell Telephone Laboratories [Ritchie 78] [Ritchie 84]. A companhia estava saindo do projeto Multics [Organick 75], o qual tinha começado como um empreendimento conjunto do MIT, General Eletric e Bell Labs; por 1969, a administração do Bell Labs e igualmente os pesquisadores, chegaram à conclusão de que as promessas do Multics poderiam ser satisfeitas somente muito depois e também muito custosamente. Antes que a máquina Multics GE-645 fosse removida das premissas, um grupo informal, liderado primariamente por Ken Thompson, tinha começado a investigar alternativas.
Thompson queria criar um ambiente de computação confortável de acordo com seu próprio projeto, usando quaisquer meios disponíveis. Seus planos, estando isto evidente em retrospecto, incorporariam muitos dos aspectos inovadores do Multics, incluindo uma notação explícita de um processo como um foco de controle, um sistema de arquivo estruturado em árvore, um interpretador de comandos como um programa ao nível do usuário, simples representação de arquivos texto e acesso generalizado a dispositivos. Ele excluiu outros, tais como acesso unificado a memória e arquivos. No começo, além disso, ele e o resto de nós protelamos outro elemento pioneiro (ainda que não original) do Multics, chamado de escrita quase que exclusiva em linguagem de alto-nível. A PL/I, a linguagem de implementação do Multics, não era muito do nosso gosto, mas nós estávamos usando outras linguagens, incluindo BCPL, e nós lamentávamos perder as vantagens de escrever programas em uma linguagem acima do nível do montador, tais como facilidade de escrita e claridade de entendimento. Naquele tempo nós não colocamos muito valor em portabilidade, o interesse nisso surgiu depois.
Thompson foi defrontado com um ambiente de hardware restrito e espartano ao mesmo tempo: o DEC PDP-7 na qual ele começou em 1968 era uma máquina com memória de 8K, o tamanho de palavra era de 18-bits e não havia nenhum software útil para ela. Enquanto desejava usar uma linguagem de alto-nível, ele escreveu o sistema original Unix no montador PDP-7. No começo, ele nem mesmo fez o programa no PDP-7 propriamente dito, mas ao invés disto usou um conjunto de macros para o montador GEMAP em uma máquina GE-635. Um pós-processador gerou uma fita de papel legível pelo PDP-7.
Estas fitas foram levadas da máquina GE para o PDP-7 para testar um primitivo kernel do Unix, um editor, um montador, um simples shell (interpretador de comandos) e uns poucos utilitários (como os comandos Unix rm, cat e cp) foram completados. Após este ponto o sistema operacional era auto-sustentável: programas podiam ser escritos e testados sem recorrer a fita de papel, e o desenvolvimento continuou no PDP-7 propriamente dito.
O montador PDP-7 de Thompson excedeu o DEC em simplicidade; ele avaliava expressões e emitia os bits correspondentes. Não havia bibliotecas, nenhum carregador ou link editor: o fonte inteiro do programa era apresentado ao montador e o arquivo de saída - com um nome fixo - que emergia era diretamente executável. (Este nome, a.out, explica um pouquinho a etimologia Unix; ele é à saída do montador. Igualmente após o sistema ganhar um linker e um conjunto especificando um outro nome explicitamente, ele foi mantido como o resultado executável default de uma compilação.).
Não muito depois do Unix rodar no PDP-7, em 1969, Doug Mcllroy criou a primeira linguagem de alto-nível para o novo sistema: uma implementação da TMG de McClure [McClure 65]. A TMG era uma linguagem para a escrita de compiladores (mais geralmente, TransMoGrifiers) em um estilo top-down e recursivo descendente, combinando sintaxe de notação de livre contexto com elementos procedurais. Mcllroy e Bob Morris tinham usado a TMG para escrever o primitivo compilador PL/I para o Multics.
Desafiado pelo feito de Mcllroy na reprodução da TMG, Thompson decidiu que o Unix - possivelmente ele não tinha sido chamado assim - necessitava de uma linguagem de programação de sistema. Após uma rapidamente abandonada tentativa em Fortran, ele criou uma linguagem própria dele mesmo, o qual ele chamou de B. A linguagem B pode ser pensada como a C sem tipos; mais precisamente, ela é a BCPL comprimida em 8K de memória e filtrada pelo cérebro de Thompson. Seu nome provavelmente representa uma contração de BCPL, entretanto uma teoria alternativa pensa que ele deriva de Bon [Thompson 69], uma linguagem não relatada criada por Thompson nos dias do Multics.
Bon por sua vez foi nomeada de conformidade com o nome de sua esposa Bonnie, ou (de acordo com uma citação em seu manual) de conformidade com o nome de uma religião cujos rituais envolvem a murmuração de fórmulas mágicas.
Origens: as linguagens
A linguagem BCPL foi projetada por Martin Richards no meio dos anos 60 enquanto ele estava visitando o MIT, e foi usada no começo dos anos 70 para vários projetos interessantes, dentre eles o sistema operacional OS6 em Oxford [Stoy 72] e partes do trabalho no Xerox PARC [Thacker 79]. Nós tornamo-nos familiarizados com ela porque o sistema MIT CTSS [Corbato 62] na qual Richards trabalhou era usado para desenvolvimento do Multics. O compilador original BCPL foi transportado para o Multics e para o sistema GE-635 GECOS por Rudd Canaday e outros do Bell Labs [Canaday 69]; durante a agonia final da vida do Multics no Bell Labs e imediatamente após, ela foi a linguagem escolhida pelo grupo de pessoas que estariam envolvidas depois com o Unix.
A linguagens BCPL, B e C, ajustam-se firmemente na tradicional família procedural simbolizada pela Fortran e Algol 60. Elas são particularmente orientadas para a programação de sistemas, são pequenas e descritas compactamente, sendo amenas para a tradução por compiladores simples. Elas estão 'próximas à máquina' na qual as abstrações por elas introduzidas são prontamente fundamentadas nos tipos de dados concretos e operações providas por computadores convencionais, confiando em bibliotecas de rotinas para entrada-saída e outras interações com um sistema operacional. Com menos sucesso, elas também usam bibliotecas de rotinas para especificar construções de controles interessantes como co-rotinas e rotinas de fechamento. Ao mesmo tempo, suas abstrações ficam num nível suficientemente alto que, com cuidado, pode alcançar portabilidade entre máquinas.
BCPL, B e C, diferem sintaticamente em muitos detalhes, mas globalmente elas são similares. Um programa consiste de declarações globais e declarações de funções (procedimento). Procedimentos podem ser aninhados em BCPL, mas não podem referir-se a objetos não estáticos definidos contendo procedimentos. B e C evitam esta restrição impondo uma mais severa ainda: nenhum procedimento aninhado. Cada uma das linguagens (exceto as primeiras versões da B) reconhece compilação separada, fornecendo meios para inclusão de texto a partir de nomes de arquivos.
Vários mecanismos sintáticos e léxicos da BCPL são mais elegantes e regulares do que aqueles de B e C. Por exemplo, procedimentos em BCPL e declarações de dados têm uma estrutura mais uniforme, e ela fornece um conjunto de construções de loop mais completo. Embora programas em BCPL sejam notacionalmente fornecidos a partir de um fluxo indelimitado de caracteres, regras inteligentes permitem colocar ponto-e-vírgula após sentenças que terminam no limite de uma linha. B e C omitem esta conveniência e terminam a maioria das sentenças com ponto-e-vírgula. Apesar das diferenças, a maioria das sentenças e operadores em BCPL projeta-se diretamente nas declarações em B e C.
Algumas das diferenças estruturais entre BCPL e B originam-se de limitações na memória intermediária. Por exemplo, declarações BCPL podem tomar esta forma:
let P1 be command
and P2 be command
and P3 be command
...
onde o texto do programa representado por command contém procedimentos inteiros. As subdeclarações conectadas por um and ocorrem simultaneamente, assim o nome P3 é conhecido dentro do procedimento P1. Similarmente, BCPL pode empacotar um grupo de declarações e sentenças em uma expressão que produzem um valor, por exemplo:
E1 := valof $( declarations ; commands ; resultis E2 $) + 1
O compilador BCPL manipulou prontamente tais construções armazenando e avaliando uma representação gramaticalmente analisada do programa inteiro antes de produzir a saída. Limitações de armazenamento no compilador B exigiram uma técnica de passo único na qual a saída era gerada o mais rápido possível, e o reprojeto sintático elaborado foi levado adiante na linguagem C.
Certos aspectos menos agradáveis da BCPL são devidos a seus próprios problemas tecnológicos e foram conscientemente evitados no projeto da B. Por exemplo, a BCPL usa um mecanismo de 'vetor global' para comunicar-se entre programas compilados separadamente. Neste esquema, o programador associa explicitamente o nome de cada procedimento externo visível e objeto de dados com um offset numérico em um vetor global; a linkagem era realizada no código compilado usando estes offsets numéricos. A linguagem B livrou-se desta inconveniência insistindo inicialmente que o programa inteiro seria apresentado todo de uma vez ao compilador. Implementações posteriores da B, e todas aquelas da C, usam um linker convencional para resolver nomes externos que aparecem em arquivos compilados separadamente, ao invés de colocar o fardo de assinalar offsets ao programador.
Outras mudanças foram introduzidas na transição da BCPL para a B como um tópico experimental, sendo que algumas permanecem controversas, por exemplo, a decisão de usar um simples caracter = para assinalamento ao invés de :=. Similarmente, a B usa /* */ para encerrar comentários, ao passo que a BCPL usa // para ignorar texto até o final da linha. O legado da PL/I é evidente aqui (C++ ressuscitou a convenção de comentário da BCPL). A linguagem Fortran influenciou a sintaxe das declarações: declarações em B começam com um especificador como auto ou static, seguidas por uma lista de nomes. A linguagem C não somente seguiu este estilo, mas o ornamentou colocando suas palavras-chave especificadoras de tipo no início das declarações.
Nem toda a diferença entre a linguagem BCPL documentada no livro de Richards [Richards 79] e a linguagem B era deliberada; nós começamos de uma versão inicial da BCPL [Richards 67]. Por exemplo, o endcase que termina uma declaração switchon em BCPL não estava presente na linguagem quando nós a aprendemos nos anos 60. Assim a sobrecarga da palavra-chave break, usada para sair de uma declaração switch da B e C, deve-se a uma evolução divergente ao invés de uma mudança consciente.
Em contraste com a variação de sintaxe que ocorreu durante a criação da linguagem B, o núcleo do conteúdo semântico da BCLP - seu modelo estruturado e regras de avaliação de expressões - permaneceu intacto. Ambas as linguagens não possuíam tipos, ou melhor, tinham um simples tipo de dado, o 'word' ou 'cell', um padrão de bit de comprimento fixo. A memória nestas linguagens consiste de um array linear de tais células, sendo que o significado do conteúdo de uma célula depende da operação aplicada. O operador +, por exemplo, simplesmente adiciona seus operandos usando a instrução de adição de inteiros da máquina. As outras operações são igualmente inconscientes do significado atual de seus operandos. Pelo fato da memória ser um array linear, era possível interpretar o valor em uma célula como um índice neste array, sendo que a BCPL supre um operador para este propósito. Na linguagem original ele era rv, passando depois a ser !, enquanto que a B usa o unário *. Assim, se p é uma célula contendo o índice (ou endereço de, ou ponteiro para) de outra célula, *p refere-se ao conteúdo do ponteiro para a célula, ou como um valor em uma expressão ou como um alvo de uma atribuição.
Pelo fato dos ponteiros em BCPL e B serem meramente índices inteiros na memória, a aritmética neles é significativa: se p é o endereço de uma célula, então p + 1 é o endereço da próxima célula. Esta convenção é o básico para a semântica de arrays em ambas as linguagens. Quando em BCPL uma pessoa escreve
let V = vec 10
ou em B,
auto V[10];
o efeito é o mesmo: a célula nomeada V é alocada, então outro grupo de 10 células contínuas são colocadas ao lado e o índice da memória da primeira delas é colocada em V. Por uma regra geral, em B a expressão
*(V+i)
adiciona V e i, referindo-se a i-ésima localização após V. Ambas, BCPL e B, adicionaram notações especiais para suavizar tais acessos a array; em B uma expressão equivalente é:
V[i]
e em BCPL
V!i
Esta aproximação com arrays era incomum naquela ocasião; a linguagem C assimilaria isto depois de modo até menos convencional.
As linguagens BCPL, B e C, não suportam fortemente dados caracter; cada uma trata strings como vetores de inteiros e suplementam regras gerais com algumas poucas convenções. Nas linguagens BCPL e B, uma string literal denota o endereço de uma área estática inicializada com caracteres da string, armazenados em células. Em BCPL, o primeiro byte armazenado contém o número de caracteres na string; em B, não há contador e strings são terminados por um caracter especial, o caracter '*e'. Esta mudança foi feita parcialmente para evitar a limitação do comprimento de uma string causada pela retenção do contador em um espaço de 8 ou 9 bits e em parte porque, em nossa experiência, manter o contador parecia ser menos conveniente do que usar um terminador.
Caracteres individuais numa string em BCPL eram usualmente manipulados distribuindo a string em outro array, um caracter por célula, juntando-os depois; a B fornecia rotinas correspondentes, mas as pessoas usaram mais freqüentemente outras funções de biblioteca que acessavam ou substituíam caracteres individuais em uma string.
Mais História
Após a versão TGM da B estar trabalhando, Thompson reescreveu a B em si mesma (um passo de bootstrapping). Durante o desenvolvimento, ele lutou continuamente contra limitações de memória: cada adição à linguagem inchava o compilador de modo que dificilmente poderia ajustar-se, mas cada reescrita tomava vantagem da característica reduzida de seu tamanho. Por exemplo, a B introduziu operadores de atribuição generalizados, usando x=+y para adicionar y a x. A notação veio da Algol 68 [Wijngaarden 75] via Mcllroy, que a tinha incorporado em sua versão da TGM. (Na B e na primitiva C, o operador era =+ ao invés de +=; este erro, reparado em 1976, foi induzido pelo modo fácil e sedutor de manipulação da primeira forma no analisador léxico da B.)
Thompson foi um passo adiante inventando os operadores ++ e --, os quais incrementam ou decrementam; sua posição pré-fixada ou pós-fixada determina se a alteração acontece antes ou depois de se observar o valor do operando. Eles não estavam nas primeiras versões da B, mas apareceram ao longo do caminho. As pessoas freqüentemente supõem que eles foram criados para usar os modos de auto-incremento e autodecremento de endereço do DEC PDP-11 na qual a C e o Unix tornaram-se populares. Esta é uma impossibilidade histórica, visto que não havia PDP-11 quando a B foi desenvolvida. O PDP-7, contudo, tinha umas poucas células de memória de ‘auto-incremento’, com a propriedade que uma referência indireta à memória por feita por meio delas incrementava a célula. Esta característica provavelmente sugeriu tais operações a Thompson; a generalização para as tornar pós ou pré-fixadas era próprio dele. Na verdade as células de ‘auto-incremento’ não eram usadas diretamente na implementação dos operadores, e uma motivação forte para a inovação era provavelmente a observação por parte dele que a tradução de ++x era menor do que x=x+1.
O compilador B no PDP-7 não gerou instruções de máquina, mas ao invés disso ‘threaded code' [Bell 72], um esquema interpretativo na qual a saída do compilador consistia de uma seqüência de endereços de fragmentos de código que executavam as operações elementares. As operações típicas, especialmente em B, agiam em uma simples pilha de máquina.
No sistema Unix PDP-7, somente poucas coisas foram escrita em B, exceto a própria B, isto porque a máquina era muito pequena e lenta para algo mais do que experimentos; reescrever completamente o sistema operacional e os utilitários em B era um passo muito caro para parecer possível. Em algum ponto Thompson aliviou o espaço de endereçamento oferecendo um compilador B ‘virtual’ que permitia ao programa interpretado ocupar mais do que 8K bytes através da paginação do código e dados dentro do interpretador, mas era muito lento para ser prático para as utilidades comuns. Apesar disso, alguns utilitários escritos em B apareceram, incluindo uma versão primitiva da calculadora de precisão variável dc, familiar aos usuários Unix [Mcllroy 79]. A mais ambiciosa iniciativa que eu empreendi foi um genuíno compilador-cruzado que traduzia as instruções B para instruções da máquina GE-635, mas não em threaded code. Ele era uma pequena excursão de força: um completo compilador B, escrito em sua própria linguagem e gerando código para um Mainframe de 36-bit que rodava em uma máquina de 18-bit com 4K palavras de espaço de endereçamento para usuário. Este projeto foi possível somente por causa da simplicidade da linguagem B e seu sistema de run-time.
Embora nós ocasionalmente nos entretivéssemos pensando sobre a implementação de uma das linguagens daquele tempo como Fortran, PL/I ou Algol 68, tal projeto parecia desesperadamente grande para nossos recursos: muitas ferramentas simples e pequenas foram requeridas. Todas aquelas linguagens influenciaram nosso trabalho, mas era mais divertido fazer as coisas de nosso próprio jeito.
Por volta de 1970, o projeto Unix tinha mostrado-se suficientemente promissor para que nós adquiríssemos o novo DEC PDP-11. O processador estava entre os primeiros de sua linha a ser entregues pela DEC, e passaram-se três meses antes que o seu disco chegasse. Fazer programas em B rodar nele usando a técnica threaded requeria somente escrever os fragmentos de código para os operadores e um simples assembler que eu escrevi em B; logo, o dc tornou-se o primeiro programa interessante a ser testado, antes de qualquer sistema operacional, em nosso PDP-11. Quase como que rapidamente, ainda esperando pelo disco, Thompson recodificou o kernel do Unix e alguns comandos básicos na linguagem de montagem do PDP-11. Dos 24K bytes de memória na máquina, o primitivo sistema Unix PDP-11 usava 12K bytes para o sistema operacional, um minúsculo espaço para programas do usuário e restante como RAM disk. Esta versão foi somente para testes, não para trabalho real. Uma vez que seu disco apareceu, nós rapidamente migramos para ele após transliterar comandos da linguagem de montagem para o dialeto do PDP-11 e portando aqueles já escritos em B.
Por volta de 1971, nosso pequeno centro de computação estava começando a ter usuários. Todos nós queríamos criar softwares interessantes mais facilmente. Usar o montador era mais enfadonho do que usar B, e apesar de seus problemas de desempenho, tinha sido completada uma pequena biblioteca de rotinas de serviços úteis que estava sendo usada para programas mais novos. Entre os resultados mais notáveis deste período estava a primeira versão de Steve Johnson do yacc parser-generator [Johnson 79a].
Os problemas da B
As máquinas nas quais nós primeiramente usamos a BCPL, e depois a B, eram endereçadas por palavra, e nestas linguagens o simples tipo de dado, o ‘cell’, equiparava-se com a palavra de hardware da máquina. O advento do PDP-11 expôs várias insuficiências do modelo semântico da B. Primeiro, seus mecanismos de manipulação de caracter, herdados com poucas mudanças da BCPL, eram desajeitados: usar bibliotecas de procedimentos para distribuir strings em células individuais e depois uni-las, ou ter acesso e substituir caracteres individuais, começou a parecer desajeitado, até mesmo tolo, em uma máquina orientada para byte.
Segundo, embora o PDP-11 original não fornecesse aritmética de ponto flutuante, o fabricante prometeu que ela logo estaria disponível. Operações de ponto flutuante tinham sido adicionadas a BCPL em nossos compiladores Multics e GCOS através da definição de operadores especiais, mas o mecanismo só era possível por que nas referidas máquinas uma simples palavra era grande o bastante para conter um número em ponto flutuante; isto não era verdadeiro nos 16-bits do PDP-11.
Finalmente, o modelo da B e da BCPL implicaram em overhead no tratamento com ponteiros: as regras da linguagem, definindo um ponteiro como um índice em um array de palavras, forçava os ponteiros a ser representados como palavras índices. Cada referência a ponteiro gerava uma conversão de escala em tempo de execução do ponteiro para o endereço do byte esperado pelo hardware.
Por todas estas razões, parecia que um esquema de tipagem era necessário para lidar com caracteres, endereçamento de byte e preparar-se para o hardware de ponto-flutuante. Outros assuntos, particularmente segurança de tipo e checagem de interface, não pareciam ser importantes como se tornaram depois.
Aparte os problemas com a própria linguagem, a técnica threaded-code do compilador B rendeu programas mais lentos que os seus contrapartes em linguagem de montagem, de modo que nós descartamos a possibilidade de recodificar o sistema operacional ou seus utilitários centrais em B.
Em 1971 eu comecei a estender a linguagem B adicionando um tipo caracter e também reescrevendo seu compilador para gerar instruções de máquina do PDP-11 ao invés de threaded code. Assim a transição da B para a C foi contemporânea com a criação de um compilador capaz de produzir programas rápidos e pequenos o bastante para competir com a linguagem assembler. Eu chamei o idioma ligeiramente estendido de NB (New B).
C embrionário
A NB teve uma existência tão breve que nenhuma descrição completa dela foi escrita. Ela fornecia os tipos int, char, arrays (para os tipos int e char) e ponteiros para eles, declarados num estilo simbolizado por
int i, j;
char c, d;
int iarray[10];
int ipointer[ ];
char carray[10];
char cpointer[ ];
A semântica de arrays permaneceu exatamente como em B e BCPL: as declarações de iarray e carray criam células inicializadas dinamicamente com um valor apontando para o primeiro de uma seqüência de 10 inteiros e caracteres respectivamente. As declarações para ipointer e cpointer omitem o tamanho, asseverando que nenhum armazenamento deve ser alocado dinamicamente. Dentro de procedimentos, a interpretação de ponteiros por parte das linguagens era idêntica a de variáveis arrays: uma declaração de ponteiro criava uma célula, diferindo de uma declaração de um array somente pelo fato do programador estar esperando assinalar uma referência ao invés de deixar o compilador alocar espaço e inicializar a célula.
Valores armazenados em células demarcadas para arrays e ponteiros eram endereços de máquina, medidos em bytes, da correspondente área de armazenamento. Portanto, a indireção através de ponteiro não implicava em overhead em tempo de execução para escalar o ponteiro da palavra para o byte offset. Por outro lado, o código de máquina para subscrição de arrays e aritmética de ponteiros depende agora do tipo do array ou do ponteiro: computar iarray[i] ou ipointer+i implicou escalar i pelo tamanho do objeto referenciado.
Estas semânticas representaram uma fácil transição da B e eu as experimentei por alguns meses. Problemas ficaram evidentes quando eu tentei estender a notação de tipo, especialmente para adicionar tipos estruturados (record). Estruturas, parecia, deveriam mapear de modo intuitivo na memória da máquina, mas em uma estrutura contendo um array, não havia um bom lugar para esconder o ponteiro contendo a base do array, nem qualquer modo conveniente para arranjar que ele seja inicializado. Por exemplo, o diretório de entradas dos primeiros sistemas Unix poderia ser escrito em C como
struct {
int inumber;
char name[14];
};
Eu não somente queria a estrutura para caracterizar um objeto abstrato, mas também para descrever uma coleção de bits que poderiam ser lidos de um diretório. Aonde o compilador poderia esconder o ponteiro para nomear o que a semântica exigiu? Até mesmo se estruturas fossem pensadas de modo mais abstrato e o espaço para ponteiros pudesse ser escondido de alguma maneira, como eu poderia manipular o problema técnico de inicializar corretamente estes ponteiros quando alocando um objeto complicado, talvez um que especificasse estruturas contendo arrays armazenando estruturas?
A solução constituiu o salto crucial na cadeia evolucionária entre a linguagem sem tipos BCPL e a linguagem com tipos C. Ela eliminou a materialização do ponteiro em armazenamento e, ao invés disso, causou a criação do ponteiro quando o array é mencionado em uma expressão. A regra, que sobrevive na C de hoje, é que valores de um tipo array são convertidos, quando eles aparecem em expressões, em ponteiros para o primeiro dos objetos que compõem o array.
Esta invenção permitiu que a maioria do código existente em B continuasse a trabalhar, apesar da troca subjacente na semântica da linguagem. Os poucos programas que assinalavam novos valores para um array para ajusta-los à sua origem – possivelmente em B e BCPL, sem sentido em C – foram facilmente reparados. Mais importante, a nova linguagem reteve uma coerente e viável (se não incomum) explicação da semântica de arrays, enquanto abriu o caminho para um tipo de estrutura mais abrangente.
A segunda inovação que distingue mais claramente a C de suas predecessoras é este tipo de estrutura e especialmente sua expressão na sintaxe de declarações. A NB oferecia os tipos básicos int e char, juntos com arrays deles e ponteiros para eles, mas nenhum modo de composição adicional. A generalização foi requerida: dado um objeto para qualquer tipo, deveria ser possível descrever um novo objeto que junta vários em um array, obtidos de uma função, ou sendo um ponteiro para ele.
Para cada objeto de tal tipo composto já havia um modo para mencionar o objeto subjacente: indexar o array, chamar a função, usar o operador de indireção no ponteiro. O raciocínio analógico levou a uma sintaxe de declaração para nomes que reflete isso na sintaxe de expressão na qual os nomes aparecem tipicamente. Assim,
int i, *pi, **ppi;
declara um inteiro, um ponteiro para um inteiro e um ponteiro para um ponteiro que aponta para um inteiro. A sintaxe dessas declarações reflete a observação de que i, *pi e **ppi todos provêm um tipo int quando usados em uma expressão. Similarmente,
int f(), *f(), (*f)();
declara uma função retornando um inteiro, uma função retornando um ponteiro para um inteiro, um ponteiro para uma função retornando um inteiro;
int *api[10], (*pai)[10];
declaram um array de ponteiros para inteiros e um ponteiro para um array de inteiros. Em todos estes casos a declaração de uma variável é semelhante a seu uso em uma expressão cujo tipo é um dos especificados no cabeçalho da declaração.
O esquema do tipo de composição adotado pela C deve considerável débito ao Algol 68, embora não o faça, talvez, emergir na forma que os partidários da Algol aprovariam. A noção central que eu capturei da Algol foi um tipo estruturado baseado em tipos atômicos (incluindo estruturas), compostas em arrays, ponteiros (referências) e funções (procedimentos). O conceito do Algol 68 de uniões e moldagens também teve influência que apareceu depois.
Após criar o sistema de tipos, a sintaxe associada e o compilador para a nova linguagem, eu senti que ela merecia um novo nome; NB parecia insuficientemente distintivo. Eu decidi seguir o estilo de letra única e a chamei de C, deixando em aberto a questão se o nome representava uma progressão pelo alfabeto ou pelas letras em BCPL.
Neonatal C
Rápidas mudanças continuaram após a linguagem ter sido nomeada, por exemplo, a introdução dos operadores && e ||. Em BCPL e B, a avaliação de expressões depende do contexto: dentro de um if e outras declarações condicionais que comparam o valor de uma expressão com zero, estas linguagens colocam uma interpretação especial nos operadores and (&) e or (|). Em contextos ordinários, eles operam bitwise, mas na declaração em B
if (e1 & e2) ...
o compilador deve avaliar e1, e se ele é diferente de zero, avaliar e2, e se ele também é diferente de zero, elaborar a declaração dependente no if. A exigência desce recursivamente nos operadores & e | dentro de e1 e e2. O curto-circuito semântico de operadores booleanos em tal contexto de "valores verdadeiros" parecia desejável, mas a sobrecarga dos operadores era difícil de explicar e usar. Por sugestão de Alan Snyder, eu introduzi os operadores && e || para tornar o mecanismo mais explícito.
A introdução tardia explica uma infelicidade das regras de precedência da C. Em B uma pessoa escreve
if (a==b & c) ...
para checar se a é igual a b e se c é diferente de zero; em tal expressão condicional é melhor que o & tenha precedência menor do que ==. Na conversão da B para a C, a pessoa necessita trocar o & por &&; para tornar a conversão menos dolorosa, nós decidimos manter a precedência do operador & a mesma do operador ==, e meramente separar a precedência do && ligeiramente do &. Hoje, parece que teria sido preferível mover a relativa precedência do & e do ==, simplificando assim um acontecimento comum na linguagem C: para testar um valor mascarado contra outro valor, alguém escreveria
if ((a&mask) == b) ...
aonde os parêntesis internos são requeridos, mas facilmente esquecidos.
Muitas outras mudanças ocorreram por volta de 1972-1973, mas o mais importante foi à introdução do pré-processador, em parte no urgir de Alan Snyder [Snyder 74], mas também em reconhecimento da utilidade do mecanismo de inclusão de arquivos disponíveis em BCPL e PL/I. Sua versão original era extremamente simples e fornecia somente inclusão de arquivos e simples substituição de strings: #include e #define (de macros sem parâmetros). Logo depois, ele foi estendido, principalmente por Mike Lesk e então por John Reiser, incorporando macros com argumentos e compilação condicional. O pré-processador foi considerado originalmente um suplemento opcional da linguagem. De fato, por alguns anos, ele nem mesmo invocou que o programa contivesse ao menos um sinal especial no seu início. Esta atitude persistiu e explica a incompleta integração da linguagem do pré-processador com o resto da linguagem e a imprecisão de sua descrição nos primeiros manuais de referência.
Portabilidade
No início de 1973, o essencial da moderna linguagem foi completado. A linguagem e o compilador eram fortes o bastante para nos permitir reescrever o kernel do Unix para o PDP-11 durante o verão daquele ano. (Thompson tinha feito uma breve tentativa de produzir um sistema codificado em uma versão primitiva da C antes das estruturas em 1972, mas deixou o esforço). Também durante este período, o compilador foi transportado para outras máquinas próximas, particularmente o Honeywell 635 e IBM 360/370; pelo fato da linguagem não poder viver em isolamento, os protótipos para as modernas bibliotecas foram desenvolvidos. Em particular, Lesk escreveu um ‘portável pacote de I/O’ [Lesk 72] que foi refeito para tornar-se as rotinas de ‘I/O padrão’ da C. Em 1978 Brian Kernighan e eu publicamos A Linguagem de Programação C [Kernighan 78]. Embora não descrevendo algumas adições que logo ficaram comuns, este livro serviu como a referência da linguagem até um padrão formal ser adotado mais de dez anos depois. Embora nós trabalhássemos próximos neste livro, havia uma clara divisão de trabalho: Kernighan escreveu quase todo o material expositório, enquanto eu fui responsável pelo apêndice contendo o manual de referência e o capítulo sobre interfaceamento com o sistema Unix.
Durante 1973-1980, a linguagem cresceu um pouco: o tipo estruturado ganhou unsigned (sem sinal), long (longo), union (união) e enumeration types (tipos enumerados), e as estruturas tornaram-se quase objetos de primeira classe (faltando somente uma notação para literais). Desenvolvimentos igualmente importantes apareceram em seu ambiente e acompanharam a tecnologia. Escrever o kernel do Unix em C tinha dado a nós bastante confiança na eficiência e utilidade da linguagem, de modo que nós começamos a recodificar os utilitários do sistema e ferramentas, movendo então os mais interessantes deles para outras plataformas. Como descrito em [Johnson 78a], nós descobrimos que o mais duros problemas em propagar ferramentas Unix não eram a interação da linguagem C com novo hardware, mas na adaptação do software existente de outros sistemas operacionais. Assim Steve Johnson começou a trabalhar no pcc, um compilador C que intentava ser fácil de ser transportado para novas máquinas [Johnson 78b], enquanto ele, Thompson e eu começamos a mover os sistema Unix propriamente dito para o computador Interdata 8/32.
A linguagem mudou durante este período, especialmente por volta de 1977, quando era largamente enfocada a consideração de portabilidade e de segurança de tipo, em um esforço para enfrentar os problemas que nós previmos e observamos ao mover um considerável corpo de código para a nova plataforma Interdata. A C naquele momento ainda manifestou sinais fortes de sua origem sem tipos. Ponteiros, por exemplo, eram pouco distintos dos índices integrais de memória nos primeiros manuais da linguagem ou no código existente; a similaridade das propriedades aritméticas de ponteiros de caracteres e inteiros não assinalados tornou difícil a tentação de os identificar. Os tipos unsigned foram adotados para tornar a aritmética de não assinalados disponível sem confusão com a manipulação de ponteiros. Semelhantemente, a primitiva linguagem tolerou a assinalação entre inteiros e ponteiros, mas esta prática começou a ser desencorajada; uma notação para conversões de tipo (chamada de ‘casts’ (moldagens) do exemplo da Algol 68) foi inventada para especificar conversões de tipo mais explicitamente. Iludida pelo exemplo da PL/I, a primitiva C não amarrou firmemente a estrutura de ponteiros para as estruturas que eles apontavam, permitindo que programadores escrevessem ponteiro ® membro quase sem levar em conta o tipo de ponteiro; tal expressão foi levada sem críticas como uma referência para uma região de memória designada pelo ponteiro, enquanto o membro especificava somente um offset e um tipo.
Embora a primeira edição de K&R descrevesse a maioria das regras que anteciparam o tipo de estrutura da C para a sua presente forma, muitos programas escritos no estilo mais antigo, mais relaxado, persistiram, fazendo assim com que muitos compiladores tolerassem isto. Para encorajar as pessoas a prestar mais atenção nas regras oficiais da linguagem, detectar construções legais, mas suspeitas, e ajudar a achar erros de comparação de interface indetectáveis com simples mecanismos para separar compilação, Steve Johnson adaptou seu compilador pcc para produzir lint [Johnson 79b], o qual esquadrinhava um conjunto de arquivos e comentava construções duvidosas.
Crescimento no Uso
O sucesso de nosso experimento de portabilidade no Interdata 8/32 conduziu a outro experimento por Tom London e John Reiser no DEC VAX 11/780. Esta máquina tornou-se muito mais popular do que o Interdata, e o Unix e a linguagem C espalharam-se rapidamente, dentro e fora da AT&T. Embora na metade dos anos 70 o Unix estivesse em uso por uma variedade de projetos dentro do sistema Bell, bem como por um pequeno grupo de organizações industriais, acadêmicas e governamentais fora de nossa companhia, seu crescimento real veio somente após a portabilidade ter sido alcançada. Particularmente de nota foram as versões System III e System V do sistema da emergente divisão de sistemas de computadores da AT&T, baseado no trabalho de desenvolvimento da companhia e grupos de pesquisa, e a série de lançamentos BSD pela Universidade da Califórnia em Berkeley que derivaram de organizações de pesquisa no Bell Laboratories.
Durante os anos 80 o uso da linguagem C espalhou-se globalmente e compiladores tornaram-se disponíveis em quase todas as arquiteturas de máquinas e sistemas operacionais; em particular ela tornou-se popular como uma ferramenta de programação para computadores pessoais, para fabricantes de software comercial destas máquinas e usuários finais interessados em programação. No começo da década, todo compilador era baseado no pcc de Johnson; por volta de 1985 havia muitos produtores independentes de compiladores.
Padronização
Por volta de 1982 estava claro que a C necessitava de uma padronização formal. A melhor aproximação para um padrão, a primeira edição de K&R, já não descrevia a linguagem em uso atual; em particular, não mencionava os tipos void e enum. Enquanto pressagiou a mais nova aproximação para estruturas, somente depois dela ter sido publicada é que a linguagem suportou designá-las, passando-as para funções e recebendo-as de funções, associando os nomes dos membros firmemente com a estrutura ou união que os continha. Embora a maioria dos compiladores distribuídos pela AT&T incorporassem essas mudanças e a maioria dos fornecedores de compiladores não baseados no pcc rapidamente as incorporassem, ela permaneceu incompleta.
A primeira edição K&R também era insuficientemente precisa em muitos detalhes da linguagem e tornou-se crescentemente impraticável considerar o pcc como um ‘compilador de referência’; ele não encarnou igualmente a linguagem descrita por K&R, deixando extensões subseqüentes sozinhas. Finalmente, o uso incipiente da C em projetos sujeitos a contrato comercial e governamental significou que um padrão oficial era importante. Assim (no ímpeto de M. D. Macllroy), a ANSI estabeleceu no verão de 1983 o comitê X3J11 sob a direção do CBEMA, objetivando produzir um padrão para a linguagem C. O comitê X3J11 produziu seu relatório [ANSI 89] no final de 1989, e subseqüentemente este padrão foi aceito pela ISO como ISO/IEC 9899-1990.
Desde o princípio, o comitê X3J11 tomou uma visão cautelosa, uma visão conservadora das extensões da linguagem. Para minha satisfação, eles levaram o objetivo deles a sério: desenvolver um limpo, consistente e não ambíguo padrão para a linguagem de programação C que codifica a definição comum existente da C e que promove a portabilidade de programas do usuário através de ambientes da linguagem C [ANSI 89]. O comitê percebeu que a mera promulgação de um padrão não faz uma mudança mundial.
O comitê X3J11 introduziu somente uma mudança genuinamente importante à linguagem propriamente dita: ele incorporou os tipos de argumentos formais na assinatura de tipo de uma função, usando a sintaxe emprestada da C++ [Stroustrup 86]. No estilo antigo, funções externas eram declaradas assim:
double sin();
a qual diz somente que sin é uma função retornando um valor double (isto é, um ponto flutuante de dupla precisão). No novo estilo, isto se tornaria
double sin(double);
para tornar o tipo de argumento explícito e assim encorajar uma melhor conferência de tipo e conversão apropriada. Igualmente esta adição, apesar de produzir uma linguagem notadamente melhor, causou dificuldades. O comitê justificadamente sentiu que simplesmente proscrever definições de função no estilo antigo e declarações não era possível, contudo também concordou que as novas formas eram melhores. O compromisso inevitável era tão bom quanto poderia ter sido, entretanto a definição da linguagem é complicada, permitindo ambas as formas, não devendo os escritores de software portátil combater com compiladores criados fora do padrão.
O comitê X3J11 também introduziu um grande número de pequenas adições e ajustes, por exemplo, os qualificadores de tipo const e volatile. Não obstante, o processo de padronização não mudou o caráter da linguagem. Em particular, o padrão C não tentou especificar formalmente a semântica da linguagem, e assim pode haver disputa em cima de pontos delicados; não obstante, ela respondeu prosperamente por mudanças no uso desde a sua descrição original, sendo suficientemente precisa para embasar implementações nela.
Assim o núcleo da linguagem C escapou quase incólume do processo de padronização e o padrão emergiu mais como uma melhor e cuidadosa codificação do que como uma nova invenção. As mais importantes mudanças aconteceram no ambiente da linguagem: o pré-processador e a biblioteca. O pré-processador executa substituição de macros usando convenções distintas do resto da linguagem. Sua interação com o compilador nunca tinha sido bem descrita, e o comitê X3J11 tentou remediar a situação. O resultado é notoriamente melhor do que a explanação na edição de K&R; além de ser mais compreensiva, ela fornece operações, como concatenação de símbolos, previamente disponíveis somente por acidentes de implementação.
O comitê X3J11 acreditou que uma completa e cuidadosa descrição de um padrão para a biblioteca C era tão importante quanto seu trabalho na linguagem propriamente dita. A própria linguagem C não fornece entrada-saída ou qualquer outra interação com o mundo externo, e assim depende de um conjunto de procedures padrão. Por momento da publicação da K&R, a linguagem C era pensada principalmente como a linguagem de programação do Unix; embora nós fornecêssemos exemplos da biblioteca de rotinas intentando as transpor para outros sistemas operacionais, o apoio subjacente do Unix era entendido implicitamente. Assim, o comitê X3J11 gastou muito de seu tempo projetando e documentando um conjunto de rotinas de biblioteca requeridas para serem disponíveis em todas implementações conforme o padrão.
Pelas regras do processo de padronização, a atividade corrente do comitê X3J11 é limitada a emitir interpretações sobre o padrão existente. Contudo, um grupo informal originalmente citado por Rex Jaeschke como NCEG (Numerical C Extensions Group) tem sido oficialmente aceito como subgrupo X3J11.1, e eles continuaram a considerar extensões para a C. Como o nome indica, muitas daquelas possíveis extensões são intentadas para tornar a linguagem mais satisfatória para uso numérico: por exemplo, arrays multidimensionais que são determinados dinamicamente, incorporação de facilidades de procedimentos com a aritmética IEEE e tornando a linguagem mais efetiva em máquinas com vetor ou outras características de arquitetura avançadas. Nem todas as extensões são especificadamente numéricas; elas incluem a notação para literais de estruturas.
Sucessores
As linguagens B e C tem vários descendentes diretos, entretanto elas não rivalizam com Pascal na geração de descendência. Uma filial lateral desenvolveu-se cedo. Quando Steve Johnson visitou a Universidade de Waterloo em 1972, ele trouxe a B com ele. Lá ela tornou-se popular em máquinas Honeywell e depois gerou Eh e Zed (a resposta canadense para "o que segue B?"). Quando Johnson retornou ao Bell Labs em 1973, ele estava desconcertado por achar que a linguagem que ele mesmo tinha semeado no Canadá tinha evoluído ao voltar para casa; até mesmo seu programa yacc tinha sido reescrito em C por Aland Snyder.
Os mais recentes descendentes da C formal incluem Concurrent C [Gehani 89], Objective C [Cox 86], C* [Thinking 90] e especialmente C++ [Stroustrup 86]. A linguagem é também usada globalmente como uma representação intermediária (essencialmente, como uma linguagem assembler portátil) para uma larga variedade de compiladores, tanto para descendentes diretos como C++ ou linguagens independentes como Modula 3 [Nelson 91] e Eiffel [Meyer 88].
Crítica
Duas idéias são mais características da C entre linguagens de sua classe: o relacionamento entre arrays e ponteiros e o modo pela qual a sintaxe de declaração imita sintaxe de expressão. Eles também estão entre as mais freqüentemente características criticadas e freqüentemente servem como blocos de tropeço para iniciantes. Em ambos os casos, acidentes históricos ou erros tem exarcebado sua dificuldade. A mais importante destas tem sido a tolerância de compiladores C para erros em tipo. Como deveria estar claro pela a história acima, a C evoluiu de linguagens sem tipos. Ela não apareceu de repente para seus usuários e desenvolvedores como uma nova linguagem com suas próprias regras; ao invés disso nós tivemos que adaptar programas existentes conforme o desenvolvimento da linguagem e dar desconto para um corpo de código existente. (Depois, o comitê ANSI X3J11 que a padronizou enfrentaria o mesmo problema.)
Compiladores em 1977, e igualmente bem após, não reclamaram sobre usos tais como assinalamento entre inteiros e ponteiros ou uso de objetos de um tipo errado para referir-se a membros de estruturas. Embora a definição da linguagem apresentada na primeira edição de K&R fosse razoavelmente (entretanto não completamente) coerente em seu tratamento de regras de tipo, este livro admitiu que compiladores existentes não deveriam forçá-los. Além disso, algumas regras projetadas para facilitar primitivas transições contribuíram para posterior confusão. Por exemplo, os colchetes vazios na declaração de função
int f(a) int a[ ]; { ... }
é um fóssil vivo, um remanescente do modo da NB de declarar um ponteiro; a é, neste caso especial, interpretado em C como um ponteiro. A notação sobreviveu em parte por causa da compatibilidade, em parte sob a racionalização que permitiria a programadores comunicar aos leitores o intento de passar f, um ponteiro gerado de um array, em lugar de uma referência a um simples inteiro. Infelizmente, ela serve para muito atrapalhar o estudante em como alertar o leitor.
Em C K&R, o fornecimento de argumentos de um tipo formal para uma chamada de função era de responsabilidade do programador, e os compiladores existentes não checavam o tipo. A falha da linguagem original em incluir tipos de argumentos na assinatura de uma função foi uma fraqueza significante, e foi realmente a única que requereu do comitê X3J11 a mais corajosa e dolorosa inovação. O projeto inicial é explicado (se não justificado) por minha vacância de problemas tecnológicos, especialmente checagem cruzada entre arquivos fonte compilados separadamente e minha incompleta assinalação das implicações de mudança entre uma linguagem sem tipos para uma com tipos. O programa lint, mencionado acima, tentou aliviar o problema: entre suas outras funções, lint checava a consistência e coerência de um programa inteiro esquadrinhando um conjunto de arquivos fonte, comparando os tipos de argumentos de função usados em chamadas com aqueles de suas definições.
Um acidente de sintaxe contribuiu para a percebida complexidade da linguagem. O operador de indireção, representado em C por *, é sintaticamente um operador prefixado unário, exatamente como em BCPL e B. Isto trabalha bem em expressões simples, mas nos casos mais complexos, parênteses são requeridos para dirigir a análise gramatical. Por exemplo, para distinguir a indireção pelo valor retornado por uma função da chamada por uma função designada por um ponteiro, alguém escreveria *fp() e (*pf)() respectivamente. O estilo usado em expressões transporta-se para declarações, assim os nomes poderiam ser declarados
int *fp();
int (*pf)();
Em formato mais ornamentado, mas ainda em casos realísticos, as coisas ficam piores:
int *(*pfp)();
é um ponteiro para um inteiro retornando um ponteiro para um inteiro. Há dois efeitos ocorrendo. Mais importante, C tem um conjunto relativamente rico de modos de descrever tipos (comparados, digamos, com Pascal). Declarações em linguagens tão expressivas quanto C, Algol 68, por exemplo, descrevem objetos igualmente duros de entender, simplesmente porque os objetos são eles mesmos complexos. Um segundo efeito deve-se a detalhes de sintaxe. Declarações em C devem ser lidas em um estilo de "dentro para fora" que muitos tem achado difícil de compreender [Anderson 80]. Sethi [Sethi 80] observou que muitas das declarações aninhadas e expressões se tornariam simples se o operador de indireção tivesse sido tomado como pós-fixado ao invés de pré-fixado, mas então era muito tarde para mudar.
Apesar de suas dificuldades, eu acredito que a aproximação de declarações da C permanece plausível, e eu estou à vontade com isto; ela é um princípio de unificação útil.
A outra característica da C, seu tratamento de arrays, é mais suspeita em fundamentos práticos, entretanto ela tem suas reais virtudes. Embora a relação entre ponteiros e arrays seja incomum, ela pode ser aprendida. Além disso, a linguagem mostra considerável força para descrever importantes conceitos, por exemplo, vetores cujo comprimento é variável em tempo de execução, com somente umas poucas regras básicas e convenções. Em particular, strings de caracteres são manipuladas pelos mesmos mecanismos como qualquer outro array, mais a convenção que um caracter nulo termina a string. É interessante comparar a aproximação da C com estes vetores com duas linguagens quase contemporâneas, Algol 68 e Pascal [Jensen 74]. Arrays em Algol 68 ou tem limites fixos ou são flexíveis: considerável mecanismo é requerido em ambos na definição da linguagem, e em compiladores, para acomodar arrays flexíveis (e nem todos os compiladores os implementam completamente.) O Pascal original tinha somente arrays de tamanho fixo e strings, e isto se mostrou ser limitado [Kernighan 81]. Depois, isto foi parcialmente fixado, entretanto a linguagem resultante não é universalmente disponível.
A linguagem C trata strings como arrays de caracteres convencionalmente terminados por um marcador. Aparte uma regra especial sobre inicialização através de literais string, a semântica de strings é completamente subsomada por regras mais gerais governando todos os arrays, e como resultado a linguagem é simples para descrever e para traduzir do que uma que incorpora a string como um único tipo de dado. Alguns custos resultam desta sua aproximação: certas operações com string são mais custosas do que outras porque o código de aplicação, ou a rotina de biblioteca, deve ocasionalmente procurar pelo final de uma string, porque poucas operações embutidas estão disponíveis, e porque o fardo da administração de armazenamento para strings cai mais pesadamente no usuário. Não obstante, a aproximação para strings da C trabalha bem.
Por outro lado, o tratamento de arrays da C em geral (não apenas strings) tem implicações infortunadas para optimização e futuras extensões. A prevalência de ponteiros em programas C, se esses são declarados explicitamente ou surgem de arrays, significa que otimizações devem ser cautelosas e devem usar técnicas cuidadosas de fluxo de dados para alcançar bons resultados. Compiladores sofisticados podem entender que a maioria dos ponteiros pode possivelmente mudar, mas alguns importantes usos continuam difíceis de analisar. Por exemplo, funções com argumentos ponteiro derivados de arrays são difíceis de compilar em código eficiente em máquinas vetor, porque raramente é possível determinar se aquele argumento ponteiro não sobrepõe dados também referidos por outro argumento, ou são acessados externamente. Mais fundamentalmente, a definição da C especificamente descreve a semântica de arrays que mudam ou extensões que tratam arrays como objetos mais primitivos, e permitem operações neles com um todo, tornando difícil de encaixar na linguagem existente. Até mesmo extensões para permitem a declaração e uso de arrays multidimensionais cujo tamanho é determinado dinamicamente não são inteiramente diretas [MacDonald 89] [Ritchie 90], embora elas fariam a escrita de bibliotecas numéricas em C mais fácil. Assim, a linguagem C cobre os mais importantes usos de strings e arrays que surgem na prática por um uniforme e simples mecanismo, mas deixa problemas para implementações mais altamente eficientes e para extensões.
Muitas pequenas infelicidades existem na linguagem e sua descrição está além daquelas discutidas acima, é claro. Há também críticas gerais a serem alojadas que transcendem pontos detalhados. A principal destas é que a linguagem e seu ambiente fornecem pouca ajuda para a escrita de sistemas muito grandes. A estrutura de nomes fornece somente dois níveis principais, ‘externo’ (visível em qualquer lugar) e ‘interno’ (dentro de um simples procedimento). Um nível intermediário de visibilidade (em um simples arquivo de dados e procedimentos) é debilmente amarrado à definição da linguagem. Assim, há pequeno suporte direto para modularização e designers de projeto são forçados a criar suas próprias convenções.
Semelhantemente, a própria C fornece duas classes de armazenamento: objetos ‘automáticos’ - que existem enquanto o controle reside neles ou sob um procedimento - e objetos ‘estáticos’ – que existem ao longo da execução de um programa. Armazenamento fora da pilha (alocação dinâmica) é fornecido somente por uma biblioteca de rotina e o fardo da administração disto é colocado sobre o programador: a linguagem C é hostil à garbage collection automática.
De onde o sucesso?
A linguagem C é um sucesso que ultrapassa de longe qualquer primitiva expectativa. Que qualidades contribuíram para a difusão do seu uso?
Indubitavelmente o sucesso do próprio Unix foi o mais importante fator; ele tornou a linguagem disponível para centenas de milhares de pessoas. Reciprocamente, é claro, o uso da C pelo Unix e sua conseqüente portabilidade para uma grande variedade de máquina foi importante no sucesso do sistema. Mas a invasão da linguagem em outros ambientes sugere méritos mais fundamentais.
Apesar de alguns aspectos misteriosos para o iniciante e ocasionalmente até mesmo para o adepto, a linguagem C permanece uma simples e pequena linguagem, traduzível com simples e pequenos compiladores. Seus tipos e operações são bem fundamentados naquelas fornecidas por máquinas reais, e para pessoas que usam o computador para trabalhar, aprender a linguagem para gerar programas em tempo – e espaço – eficientes não é difícil. Ao mesmo tempo a linguagem é suficientemente abstrata dos detalhes da máquina de modo que a portabilidade de programa pode ser alcançada.
Igualmente importante, a linguagem C e sua biblioteca central sempre permaneceram em contato com o ambiente real. Ela não foi projetada em isolamento para provar um ponto ou para servir como um exemplo, mas como uma ferramenta para escrever programas que fizeram coisas úteis; ela sempre teve a intenção de interagir com um grande sistema operacional e foi considerada como uma ferramenta para construir grandes ferramentas. Uma aproximação parcimoniosa, pragmática, influenciou as coisas que entraram na C: ela cobre as necessidades essenciais de muitos programadores, mas não tenta suprir muitas.
Finalmente, apesar das mudanças que sofreu desde primeira publicação, a qual foi admitidamente informal e incompleta, a linguagem C atual como visto por milhões de usuários, usando muitos diferentes compiladores, permaneceu notavelmente estável e unificada quando comparada àquelas de similar aceitação geral, por exemplo, Pascal e Fortran. Há diferentes dialetos da C mais notórios, aqueles descritos pelo velho K&R e o novo padrão C, mas na integra, a linguagem C permanece mais livre de extensões proprietárias do que outras linguagens. Talvez a mais significante extensão seja os qualificadores de ponteiros ‘far’ e ‘near’ intentados para lidar com as peculiaridades de alguns processadores Intel. Embora a C não fosse originalmente projetada tendo a portabilidade como uma meta principal, ela teve sucesso expressando programas, incluindo igualmente sistemas operacionais, em máquinas que variam de pequenos computadores pessoais até poderosos supercomputadores.
C é ardilosa, imperfeita e um enorme sucesso. Enquanto que acidentes de história seguramente ajudaram, ela evidentemente satisfez uma necessidade por uma linguagem de implementação de sistema eficiente o bastante para descartar a linguagem assembler, contudo suficientemente abstrata e fluente para descrever algoritmos e interações em uma larga variedade de ambientes.
Reconhecimentos
Isto sumariza compactamente os papéis dos contribuintes diretos para a linguagem C de hoje. Ken Thompson criou a linguagem B em 1969-1970; ela foi derivada diretamente da BCPL de Martin Richards. Dennis Ritchie transformou a B em C durante 1971-1973, mantendo a maioria da sintaxe da B enquanto adicionava tipos e muitas outras mudanças, e escreveu o primeiro compilador para a linguagem. Ritchie, Alan Snyder, Steven C. Johnson, Michael Lesk e Thompson contribuíram com idéias para a linguagem durante 1972-1977, sendo que o compilador portável de Johnson permanece amplamente usado. Durante este período, a coleção de rotinas cresceu consideravelmente, graças a aquelas pessoas e muitas outras no Bell Laboratories. Em 1978, Brian Kernighan e Ritchie escreveram o livro que se tornou a definição da linguagem por vários anos. Começando em 1983, o comitê ANSI X3J11 padronizou a linguagem. Especialmente notável em manter seus esforços na trilha foram seus funcionários Jim Brodie, Tom Plum, P. J. Plauger e os sucessivos redatores do projeto, Lary Rosler e Dave Prosser.
Eu agradeço a Brian Kernighan, Doug Mcllroy, Dave Prosser, Peter Nelson, Rob Pike, Ken Thompson e árbitros do HOPL por conselhos na preparação deste documento.
Referências
[ANSI 89]
American National Standards Institute, American National Standard for Information Systems­Programming Language C, X3.159-1989.
[Anderson 80]
B. Anderson, `Type syntax in the language C: an object lesson in syntactic innovation,' SIGPLAN Notices 15 (3), March, 1980, pp. 21-27.
[Bell 72]
J. R. Bell, `Threaded Code,' C. ACM 16 (6), pp. 370-372.
[Canaday 69]
R. H. Canaday and D. M. Ritchie, `Bell Laboratories BCPL,' AT&T Bell Laboratories internal memorandum, May, 1969.
[Corbato 62]
F. J. Corbato, M. Merwin-Dagget, R. C. Daley, `An Experimental Time-sharing System,' AFIPS Conf. Proc. SJCC, 1962, pp. 335-344.
[Cox 86]
B. J. Cox and A. J. Novobilski, Object-Oriented Programming: An Evolutionary Approach, Addison-Wesley: Reading, Mass., 1986. Second edition, 1991.
[Gehani 89]
N. H. Gehani and W. D. Roome, Concurrent C, Silicon Press: Summit, NJ, 1989.
[Jensen 74]
K. Jensen and N. Wirth, Pascal User Manual and Report, Springer-Verlag: New York, Heidelberg, Berlin. Second Edition, 1974.
[Johnson 73]
S. C. Johnson and B. W. Kernighan, `The Programming Language B,' Comp. Sci. Tech. Report #8, AT&T Bell Laboratories (January 1973).
[Johnson 78a]
S. C. Johnson and D. M. Ritchie, `Portability of C Programs and the UNIX System,' Bell Sys. Tech. J. 57 (6) (part 2), July-Aug, 1978.
[Johnson 78b]
S. C. Johnson, `A Portable Compiler: Theory and Practice,' Proc. 5th ACM POPL Symposium (January 1978).
[Johnson 79a]
S. C. Johnson, `Yet another compiler-compiler,' in Unix Programmer's Manual, Seventh Edition, Vol. 2A, M. D. McIlroy and B. W. Kernighan, eds. AT&T Bell Laboratories: Murray Hill, NJ, 1979.
[Johnson 79b]
S. C. Johnson, `Lint, a Program Checker,' in Unix Programmer's Manual, Seventh Edition, Vol. 2B, M. D. McIlroy and B. W. Kernighan, eds. AT&T Bell Laboratories: Murray Hill, NJ, 1979.
[Kernighan 78]
B. W. Kernighan and D. M. Ritchie, The C Programming Language, Prentice-Hall: Englewood Cliffs, NJ, 1978. Second edition, 1988.
[Kernighan 81]
B. W. Kernighan, `Why Pascal is not my favorite programming language,' Comp. Sci. Tech. Rep. #100, AT&T Bell Laboratories, 1981.
[Lesk 73]
M. E. Lesk, `A Portable I/O Package,' AT&T Bell Laboratories internal memorandum ca. 1973.
[MacDonald 89]
T. MacDonald, `Arrays of variable length,' J. C Lang. Trans 1 (3), Dec. 1989, pp. 215-233.
[McClure 65]
R. M. McClure, `TMGA Syntax Directed Compiler,' Proc. 20th ACM National Conf. (1965), pp. 262-274.
[McIlroy 60]
M. D. McIlroy, `Macro Instruction Extensions of Compiler Languages,' C. ACM 3 (4), pp. 214-220.
[McIlroy 79]
M. D. McIlroy and B. W. Kernighan, eds, Unix Programmer's Manual, Seventh Edition, Vol. I, AT&T Bell Laboratories: Murray Hill, NJ, 1979.
[Meyer 88]
B. Meyer, Object-oriented Software Construction, Prentice-Hall: Englewood Cliffs, NJ, 1988.
[Nelson 91]
G. Nelson, Systems Programming with Modula-3, Prentice-Hall: Englewood Cliffs, NJ, 1991.
[Organick 75]
E. I. Organick, The Multics System: An Examination of its Structure, MIT Press: Cambridge, Mass., 1975.
[Richards 67]
M. Richards, `The BCPL Reference Manual,' MIT Project MAC Memorandum M-352, July 1967.
[Richards 79]
M. Richards and C. Whitbey-Strevens, BCPL: The Language and its Compiler, Cambridge Univ. Press: Cambridge, 1979.
[Ritchie 78]
D. M. Ritchie, `UNIX: A Retrospective,' Bell Sys. Tech. J. 57 (6) (part 2), July-Aug, 1978.
[Ritchie 84]
D. M. Ritchie, `The Evolution of the UNIX Time-sharing System,' AT&T Bell Labs. Tech. J. 63 (8) (part 2), Oct. 1984.
[Ritchie 90]
D. M. Ritchie, `Variable-size arrays in C,' J. C Lang. Trans. 2 (2), Sept. 1990, pp. 81-86.
[Sethi 81]
R. Sethi, `Uniform syntax for type expressions and declarators,' Softw. Prac. and Exp. 11 (6), June 1981, pp. 623-628.
[Snyder 74]
A. Snyder, A Portable Compiler for the Language C, MIT: Cambridge, Mass., 1974.
[Stoy 72]
J. E. Stoy and C. Strachey, `OS6 An experimental operating system for a small computer. Part I: General principles and structure,' Comp J. 15, (Aug. 1972), pp. 117-124.
[Stroustrup 86]
B. Stroustrup, The C++ Programming Language, Addison-Wesley: Reading, Mass., 1986. Second edition, 1991.
[Thacker 79]
C. P. Thacker, E. M. McCreight, B. W. Lampson, R. F. Sproull, D. R. Boggs, `Alto: A Personal Computer,' in Computer Structures: Principles and Examples, D. Sieworek, C. G. Bell, A. Newell, McGraw-Hill: New York, 1982.
[Thinking 90]
C* Programming Guide, Thinking Machines Corp.: Cambridge Mass., 1990.
[Thompson 69]
K. Thompson, `Bon an Interactive Language,' undated AT&T Bell Laboratories internal memorandum (ca. 1969).
[Wijngaarden 75]
A. van Wijngaarden, B. J. Mailloux, J. E. Peck, C. H. Koster, M. Sintzoff, C. Lindsey, L. G. Meertens, R. G. Fisker, `Revised report on the algorithmic language Algol 68,' Acta Informatica 5, pp. 1-236.
Copyright © 1996 Lucent Technologies Inc. All rights reserved.