Depois de todos esses anos, o mundo ainda é alimentado por programação C
Publicados: 2022-03-11Muitos dos projetos C que existem hoje foram iniciados décadas atrás.
O desenvolvimento do sistema operacional UNIX começou em 1969, e seu código foi reescrito em C em 1972. A linguagem C foi criada para mover o código do kernel UNIX do assembly para uma linguagem de nível superior, que faria as mesmas tarefas com menos linhas de código .
O desenvolvimento do banco de dados Oracle começou em 1977, e seu código foi reescrito de assembly para C em 1983. Tornou-se um dos bancos de dados mais populares do mundo.
Em 1985, o Windows 1.0 foi lançado. Embora o código-fonte do Windows não esteja disponível publicamente, afirma-se que seu kernel é escrito principalmente em C, com algumas partes em assembly. O desenvolvimento do kernel Linux começou em 1991, e também é escrito em C. No ano seguinte, foi lançado sob a licença GNU e foi usado como parte do Sistema Operacional GNU. O próprio sistema operacional GNU foi iniciado usando as linguagens de programação C e Lisp, então muitos de seus componentes são escritos em C.
Mas a programação C não se limita a projetos que começaram décadas atrás, quando não havia tantas linguagens de programação como hoje. Muitos projetos C ainda são iniciados hoje; existem algumas boas razões para isso.
Como é o mundo Powered by C?
Apesar da prevalência de linguagens de alto nível, C continua a capacitar o mundo. A seguir estão alguns dos sistemas que são usados por milhões e são programados na linguagem C.
Microsoft Windows
O kernel do Windows da Microsoft é desenvolvido principalmente em C, com algumas partes em linguagem assembly. Por décadas, o sistema operacional mais usado do mundo, com cerca de 90% de participação de mercado, foi alimentado por um kernel escrito em C.
Linux
O Linux também é escrito principalmente em C, com algumas partes em assembly. Cerca de 97% dos 500 supercomputadores mais poderosos do mundo executam o kernel Linux. Também é usado em muitos computadores pessoais.
Mac
Os computadores Mac também são alimentados por C, já que o kernel do OS X é escrito principalmente em C. Todos os programas e drivers em um Mac, como em computadores Windows e Linux, estão sendo executados em um kernel baseado em C.
Móvel
Os kernels iOS, Android e Windows Phone também são escritos em C. Eles são apenas adaptações móveis dos kernels Mac OS, Linux e Windows existentes. Portanto, os smartphones que você usa todos os dias estão sendo executados em um kernel C.
Bancos de dados
Os bancos de dados mais populares do mundo, incluindo Oracle Database, MySQL, MS SQL Server e PostgreSQL, são codificados em C (os três primeiros, na verdade, em C e C++).
Os bancos de dados são usados em todos os tipos de sistemas: financeiro, governamental, mídia, entretenimento, telecomunicações, saúde, educação, varejo, redes sociais, web e afins.
Filmes 3D
Os filmes 3D são criados com aplicativos que geralmente são escritos em C e C++. Esses aplicativos precisam ser muito eficientes e rápidos, pois lidam com uma enorme quantidade de dados e fazem muitos cálculos por segundo. Quanto mais eficientes eles forem, menos tempo levará para os artistas e animadores gerarem as tomadas do filme, e mais dinheiro a empresa economizará.
Sistemas Embarcados
Imagine que você acorda um dia e vai fazer compras. O despertador que o acorda provavelmente está programado em C. Então você usa seu micro-ondas ou cafeteira para fazer seu café da manhã. Eles também são sistemas embarcados e, portanto, provavelmente são programados em C. Você liga sua TV ou rádio enquanto toma seu café da manhã. Esses também são sistemas embutidos, alimentados por C. Quando você abre a porta da garagem com o controle remoto, também está usando um sistema embutido que provavelmente está programado em C.
Então você entra no seu carro. Se possuir as seguintes funcionalidades, também programadas em C:
- transmissão automática
- sistemas de detecção de pressão dos pneus
- sensores (oxigênio, temperatura, nível de óleo, etc.)
- memória para assentos e configurações de espelho.
- exibição do painel
- freios anti-trava
- controle automático de estabilidade
- controle de cruzeiro
- controle climático
- fechaduras à prova de crianças
- entrada sem chave
- bancos aquecidos
- controle de airbag
Você chega à loja, estaciona o carro e vai até uma máquina de venda automática para comprar um refrigerante. Que linguagem eles usaram para programar esta máquina de venda automática? Provavelmente C. Então você compra algo na loja. A caixa registradora também está programada em C. E quando você paga com cartão de crédito? Você adivinhou: o leitor de cartão de crédito é, novamente, provavelmente programado em C.
Todos esses dispositivos são sistemas embarcados. Eles são como pequenos computadores que possuem um microcontrolador/microprocessador dentro que está executando um programa, também chamado de firmware, em dispositivos embarcados. Esse programa deve detectar pressionamentos de tecla e agir de acordo, além de exibir informações ao usuário. Por exemplo, o despertador deve interagir com o usuário, detectando qual botão o usuário está pressionando e, às vezes, por quanto tempo está sendo pressionado, e programar o dispositivo de acordo, enquanto exibe ao usuário as informações relevantes. O sistema de freios antibloqueio do carro, por exemplo, deve ser capaz de detectar o travamento súbito dos pneus e atuar para liberar a pressão sobre os freios por um pequeno período de tempo, destravando-os e evitando assim derrapagens descontroladas. Todos esses cálculos são feitos por um sistema embarcado programado.
Embora a linguagem de programação usada em sistemas embarcados possa variar de marca para marca, eles são mais comumente programados na linguagem C, devido às características da linguagem de flexibilidade, eficiência, desempenho e proximidade com o hardware.
Por que a linguagem de programação C ainda é usada?
Existem muitas linguagens de programação, hoje, que permitem que os desenvolvedores sejam mais produtivos do que com C para diferentes tipos de projetos. Existem linguagens de nível superior que fornecem bibliotecas internas muito maiores que simplificam o trabalho com JSON, XML, UI, páginas da Web, solicitações de clientes, conexões de banco de dados, manipulação de mídia e assim por diante.
Mas, apesar disso, há muitas razões para acreditar que a programação em C permanecerá ativa por muito tempo.
Em linguagens de programação, um tamanho não serve para todos. Aqui estão algumas razões pelas quais C é imbatível, e quase obrigatório, para certas aplicações.
Portabilidade e Eficiência
C é quase uma linguagem assembly portátil . É o mais próximo possível da máquina, embora esteja quase universalmente disponível para arquiteturas de processador existentes. Existe pelo menos um compilador C para quase todas as arquiteturas existentes. E hoje em dia, por causa de binários altamente otimizados gerados por compiladores modernos, não é uma tarefa fácil melhorar sua saída com assembly escrito à mão.
Tal é a sua portabilidade e eficiência que “compiladores, bibliotecas e interpretadores de outras linguagens de programação são frequentemente implementados em C”. Linguagens interpretadas como Python, Ruby e PHP têm suas implementações primárias escritas em C. Ele é usado até mesmo por compiladores de outras linguagens para se comunicar com a máquina. Por exemplo, C é a linguagem intermediária subjacente a Eiffel e Forth. Isso significa que, em vez de gerar código de máquina para cada arquitetura a ser suportada, os compiladores para essas linguagens apenas geram código C intermediário, e o compilador C lida com a geração de código de máquina.
C também se tornou uma língua franca para comunicação entre desenvolvedores. Como diz Alex Allain, gerente de engenharia do Dropbox e criador do Cprogramming.com:
C é uma ótima linguagem para expressar ideias comuns em programação de uma maneira que a maioria das pessoas se sinta confortável. Além disso, muitos dos princípios usados em C – por exemplo,
argc
eargv
para parâmetros de linha de comando, bem como construções de loop e tipos de variáveis – aparecerão em muitas outras linguagens que você aprender, então você poderá falar para as pessoas, mesmo que elas não conheçam C de uma maneira comum a vocês dois.
Manipulação de Memória
Acesso de endereço de memória arbitrário e aritmética de ponteiro é um recurso importante que torna C um ajuste perfeito para programação de sistemas (sistemas operacionais e sistemas embarcados).
No limite de hardware/software, sistemas de computador e microcontroladores mapeiam seus periféricos e pinos de E/S em endereços de memória. Os aplicativos do sistema devem ler e gravar nesses locais de memória personalizados para se comunicar com o mundo. Portanto, a capacidade do C de manipular endereços de memória arbitrários é imperativa para a programação do sistema.
Um microcontrolador pode ser arquitetado, por exemplo, de forma que o byte no endereço de memória 0x40008000 seja enviado pelo receptor/transmissor assíncrono universal (ou UART, um componente de hardware comum para comunicação com periféricos) toda vez que o bit número 4 do endereço 0x40008001 for definido para 1, e que depois de definir esse bit, ele será automaticamente desabilitado pelo periférico.
Este seria o código para uma função C que envia um byte através desse UART:
#define UART_BYTE *(char *)0x40008000 #define UART_SEND *(volatile char *)0x40008001 |= 0x08 void send_uart(char byte) { UART_BYTE = byte; // write byte to 0x40008000 address UART_SEND; // set bit number 4 of address 0x40008001 }
A primeira linha da função será expandida para:
*(char *)0x40008000 = byte;
Esta linha diz ao compilador para interpretar o valor 0x40008000
como um ponteiro para um char
, então para desreferenciar (fornecer o valor apontado por) aquele ponteiro (com o operador *
mais à esquerda) e finalmente para atribuir o valor de byte
a esse ponteiro desreferenciado. Em outras palavras: escreva o valor da variável byte
no endereço de memória 0x40008000
.

A próxima linha será expandida para:
*(volatile char *)0x40008001 |= 0x08;
Nesta linha, realizamos uma operação OR bit a bit no valor no endereço 0x40008001
e no valor 0x08
( 00001000
em binário, ou seja, um 1 no bit número 4), e salvamos o resultado no endereço 0x40008001
. Em outras palavras: configuramos o bit 4 do byte que está no endereço 0x40008001. Também declaramos que o valor no endereço 0x40008001
é volátil . Isso informa ao compilador que esse valor pode ser modificado por processos externos ao nosso código, portanto, o compilador não fará nenhuma suposição sobre o valor nesse endereço depois de gravá-lo. (Neste caso, este bit é desabilitado pelo hardware UART logo após ser configurado pelo software.) Esta informação é importante para o otimizador do compilador. Se fizermos isso dentro de um loop for
, por exemplo, sem especificar que o valor é volátil, o compilador pode assumir que esse valor nunca muda após ser definido e pular a execução do comando após o primeiro loop.
Uso determinístico de recursos
Um recurso de linguagem comum no qual a programação de sistemas não pode contar é a coleta de lixo, ou mesmo apenas a alocação dinâmica para alguns sistemas embarcados. Os aplicativos incorporados são muito limitados em tempo e recursos de memória. Eles são frequentemente usados para sistemas de tempo real, onde uma chamada não determinística para o coletor de lixo não pode ser oferecida. E se a alocação dinâmica não puder ser usada por falta de memória, é muito importante ter outros mecanismos de gerenciamento de memória, como colocar dados em endereços personalizados, como os ponteiros C permitem. Linguagens que dependem muito de alocação dinâmica e coleta de lixo não seriam adequadas para sistemas com recursos limitados.
Tamanho do código
C tem um tempo de execução muito pequeno. E a pegada de memória para seu código é menor do que para a maioria das outras linguagens.
Quando comparado ao C++, por exemplo, um binário gerado em C que vai para um dispositivo incorporado tem cerca de metade do tamanho de um binário gerado por um código C++ semelhante. Uma das principais causas para isso é o suporte a exceções.
As exceções são uma ótima ferramenta adicionada por C++ sobre C e, se não forem acionadas e implementadas de maneira inteligente, praticamente não terão sobrecarga de tempo de execução (mas ao custo de aumentar o tamanho do código).
Vejamos um exemplo em C++:
// Class A declaration. Methods defined somewhere else; class A { public: A(); // Constructor ~A(); // Destructor (called when the object goes out of scope or is deleted) void myMethod(); // Just a method }; // Class B declaration. Methods defined somewhere else; class B { public: B(); // Constructor ~B(); // Destructor void myMethod(); // Just a method }; // Class C declaration. Methods defined somewhere else; class C { public: C(); // Constructor ~C(); // Destructor void myMethod(); // Just a method }; void myFunction() { A a; // Constructor aA() called. (Checkpoint 1) { B b; // Constructor bB() called. (Checkpoint 2) b.myMethod(); // (Checkpoint 3) } // b.~B() destructor called. (Checkpoint 4) { C c; // Constructor cC() called. (Checkpoint 5) c.myMethod(); // (Checkpoint 6) } // c.~C() destructor called. (Checkpoint 7) a.myMethod(); // (Checkpoint 8) } // a.~A() destructor called. (Checkpoint 9)
Os métodos das classes A
, B
e C
são definidos em outro lugar (por exemplo, em outros arquivos). Portanto, o compilador não pode analisá-los e não pode saber se eles lançarão exceções. Portanto, ele deve se preparar para lidar com exceções lançadas de qualquer um de seus construtores, destruidores ou outras chamadas de método. Destrutores não devem lançar (prática muito ruim), mas o usuário pode lançar de qualquer maneira, ou pode lançar indiretamente chamando alguma função ou método (explícita ou implicitamente) que lança uma exceção.
Se qualquer uma das chamadas em myFunction
lançar uma exceção, o mecanismo de desenrolamento da pilha deve ser capaz de chamar todos os destruidores dos objetos que já foram construídos. Uma implementação para o mecanismo de desenrolamento de pilha usará o endereço de retorno da última chamada dessa função para verificar o “número do ponto de verificação” da chamada que acionou a exceção (esta é a explicação simples). Ele faz isso usando uma função autogerada auxiliar (uma espécie de tabela de consulta) que será usada para desenrolar a pilha caso uma exceção seja lançada do corpo dessa função, que será semelhante a esta:
// Possible autogenerated function void autogeneratedStackUnwindingFor_myFunction(int checkpoint) { switch (checkpoint) { // case 1 and 9: do nothing; case 3: b.~B(); goto destroyA; // jumps to location of destroyA label case 6: c.~C(); // also goes to destroyA as that is the next line destroyA: // label case 2: case 4: case 5: case 7: case 8: a.~A(); } }
Se a exceção for lançada dos pontos de verificação 1 e 9, nenhum objeto precisa ser destruído. Para o checkpoint 3, b
e a
devem ser destruídos. Para o checkpoint 6, c
e a
devem ser destruídos. Em todos os casos a ordem de destruição deve ser respeitada. Para os checkpoints 2, 4, 5, 7 e 8, apenas o objeto a
precisa ser destruído.
Esta função auxiliar adiciona tamanho ao código. Isso faz parte da sobrecarga de espaço que o C++ adiciona ao C. Muitos aplicativos incorporados não podem arcar com esse espaço extra. Portanto, os compiladores C++ para sistemas incorporados geralmente têm um sinalizador para desabilitar exceções. Desabilitar exceções em C++ não é gratuito, pois a Biblioteca de Modelos Padrão depende muito de exceções para informar erros. O uso desse esquema modificado, sem exceções, requer mais treinamento para os desenvolvedores de C++ para detectar possíveis problemas ou encontrar bugs.
E, estamos falando de C++, uma linguagem cujo princípio é: “Você não paga pelo que não usa”. Esse aumento no tamanho binário fica pior para outras linguagens que adicionam sobrecarga adicional com outros recursos que são muito úteis, mas não podem ser oferecidos por sistemas embarcados. Embora o C não ofereça a você o uso desses recursos extras, ele permite uma pegada de código muito mais compacta do que as outras linguagens.
Razões para aprender C
C não é uma linguagem difícil de aprender, então todos os benefícios de aprendê-la sairão bem baratos. Vamos ver alguns desses benefícios.
Língua franca
Como já mencionado, C é uma língua franca para desenvolvedores. Muitas implementações de novos algoritmos em livros ou na internet são primeiramente (ou apenas) disponibilizadas em C por seus autores. Isso dá a máxima portabilidade possível para a implementação. Eu vi programadores lutando na internet para reescrever um algoritmo C para outras linguagens de programação porque eles não conheciam conceitos muito básicos de C.
Esteja ciente de que C é uma linguagem antiga e difundida, então você pode encontrar todos os tipos de algoritmos escritos em C na web. Portanto, você provavelmente se beneficiará ao conhecer esse idioma.
Entenda a máquina (pense em C)
Quando discutimos com os colegas o comportamento de certas partes do código, ou certas características de outras linguagens, acabamos “conversando em C:” essa parte está passando um “ponteiro” para o objeto ou copiando o objeto inteiro? Poderia algum “cast” estar acontecendo aqui? E assim por diante.
Raramente discutimos (ou pensamos) sobre as instruções de montagem que uma parte do código está executando ao analisar o comportamento de uma parte do código de uma linguagem de alto nível. Em vez disso, ao discutir o que a máquina está fazendo, falamos (ou pensamos) com bastante clareza em C.
Além disso, se você não consegue parar e pensar assim sobre o que está fazendo, pode acabar programando com algum tipo de superstição sobre como (magicamente) as coisas são feitas.
Trabalhe em muitos projetos C interessantes
Muitos projetos interessantes, desde grandes servidores de banco de dados ou kernels de sistemas operacionais, até pequenos aplicativos embutidos que você pode fazer em casa para sua satisfação pessoal e diversão, são feitos em C. Não há razão para parar de fazer coisas que você pode amar pelo único motivo que você não conhece uma linguagem de programação antiga e pequena, mas forte e comprovada pelo tempo, como C.
Conclusão
A linguagem de programação C parece não ter uma data de validade. Sua proximidade com o hardware, grande portabilidade e uso determinístico de recursos o torna ideal para desenvolvimento de baixo nível para coisas como kernels de sistemas operacionais e software embarcado. Sua versatilidade, eficiência e bom desempenho o tornam uma excelente escolha para softwares de manipulação de dados de alta complexidade, como bancos de dados ou animação 3D. O fato de muitas linguagens de programação hoje serem melhores que C para o uso pretendido não significa que elas superem C em todas as áreas. C ainda é insuperável quando o desempenho é a prioridade.
O mundo está rodando em dispositivos alimentados por C. Usamos esses dispositivos todos os dias, quer percebamos ou não. C é o passado, o presente e, até onde podemos ver, ainda é o futuro para muitas áreas de software.