Como pensar como programador C depois de influenciar a linguagem OOP? [fechadas]

38

Anteriormente, eu usava apenas linguagens de programação orientada a objetos (C ++, Ruby, Python, PHP) e agora estou aprendendo C. Estou tendo dificuldade em descobrir a maneira correta de fazer as coisas em uma linguagem sem o conceito de 'Objeto'. Percebo que é possível usar paradigmas de POO em C, mas eu gostaria de aprender a maneira C-idiomática.

Ao resolver um problema de programação, a primeira coisa que faço é imaginar um objeto que resolva o problema. Com quais etapas eu substituo isso ao usar um paradigma de Programação Imperativa não OOP?

Mas Bagol
fonte
15
Ainda não encontrei uma linguagem que corresponda ao meu modo de pensar, por isso tenho que "compilar" meus pensamentos para qualquer idioma que esteja usando. Um conceito que achei útil é o de uma "unidade de código", seja um rótulo, sub-rotina, função, objeto, módulo ou estrutura: cada um deles deve ser encapsulado e expor uma interface bem definida. Se você estiver acostumado a uma abordagem de cima para baixo no nível do objeto, em C, poderá começar elaborando um conjunto de funções que se comportam como se o problema tivesse sido resolvido. Freqüentemente, APIs C bem projetadas parecem OOP, mas qux = foo.bar(baz)se tornam qux = Foo_bar(foo, baz).
amon
Para eco amon , o foco no seguinte: gráfico semelhante a estrutura de dados, os ponteiros, algoritmos, a execução (fluxo de controle) de código (funções), ponteiros de função.
Rwong 31/05
1
O LibTiff (código fonte no github) é um exemplo de como organizar grandes programas em C.
rwong 31/05
1
Como programador de C #, sentiria falta dos delegados (ponteiros de função com um parâmetro vinculado) muito mais do que sentiria falta de objetos.
CodesInChaos
Pessoalmente, achei a maior parte do C fácil e direta, com a notável exceção do pré-processador. Se eu tivesse que reaprender C, essa seria uma área na qual eu concentraria muito do meu esforço.
biziclop

Respostas:

53
  • O programa AC é uma coleção de funções.
  • Uma função é uma coleção de instruções.
  • Você pode encapsular dados com um struct.

É isso aí.

Como você escreveu uma aula? É assim que você escreve um arquivo .C. Concedido, você não obtém coisas como polimorfismo e herança de método, mas pode simular aqueles com diferentes nomes de funções e composição .

Para pavimentar o caminho, estude Programação Funcional. É realmente incrível o que você pode fazer sem aulas, e algumas coisas realmente funcionam melhor sem a sobrecarga das aulas.

Leitura adicional
Orientação a objetos em ANSI C

Robert Harvey
fonte
9
você também pode fazer typedefisso structe criar algo parecido com classe . typedeftipos e -ed podem ser incluídos em outros structs que podem ser typedef-ed. o que você não obtém com C é a sobrecarga do operador e a herança superficialmente simples das classes e dos membros contidos no C ++. e você não recebe muitas sintaxes estranhas e não naturais que obtém com o C ++. Eu realmente amo o conceito de POO, mas acho que C ++ é uma realização feia de POO. Eu gosto de C porque é um idioma menor e deixa de fora a sintaxe do idioma que é melhor deixar para funções.
Robert bristow-johnson
22
Como alguém cuja primeira língua era / é C, atrevo-me a dizer isso . a lot of things actually work better without the overhead of classes
precisa saber é o seguinte
1
Para expandir, muitas coisas foram desenvolvidas sem o OOP: sistemas operacionais, servidores de protocolo, gerenciadores de inicialização, navegadores e assim por diante. Os computadores não pensam em termos de objetos e nem precisam. De fato, geralmente é muito lento para eles forçarem isso.
Edmz 01/06/2015
Contraponto: a lot of things actually work better with addition of class-based OOP. Fonte: TypeScript, Dart, CoffeeScript e todas as outras maneiras pelas quais a indústria está tentando se afastar de uma linguagem OOP funcional / protótipo.
Den
Para expandir, muitas coisas foram desenvolvidas com o OOP: tudo o resto. Os humanos pensam naturalmente em termos de objetos e os programas são escritos para que outros humanos possam ler e entender.
Den
18

Leia o SICP e aprenda Scheme, além da idéia prática de tipos de dados abstratos . Então codificar em C é fácil (já que com SICP, um pouco de C e um pouco de PHP, Ruby, etc ... seu pensamento seria bastante amplo e você entenderia que a programação orientada a objetos pode não ser o melhor estilo em todos os casos, mas apenas para algum tipo de programa). Tenha cuidado com a alocação de memória dinâmica C , que provavelmente é a parte mais difícil. O padrão da linguagem de programação C99 ou C11 e sua biblioteca padrão C são na verdade bastante pobres (ele não conhece TCP ou diretórios!), E muitas vezes você precisará de algumas bibliotecas ou interfaces externas (por exemplo,POSIX , libcurl para a biblioteca do cliente HTTP, libonion para a biblioteca do servidor HTTP, GMPlib para bignums, alguma biblioteca como libunistring para UTF-8, etc ...).

Seus "objetos" geralmente estão em C - alguns relacionados struct- e você define o conjunto de funções operando neles. Para funções curtas ou muito simples, considere defini-las, com o relevante struct, como static inlineem algum arquivo de cabeçalho, foo.hpara estar #include-d em outro lugar.

Observe que a programação orientada a objetos não é o único paradigma de programação . Em algumas ocasiões, outros paradigmas valem a pena ( programação funcional à Ocaml ou Haskell ou mesmo Scheme ou Commmon Lisp, programação lógica à Prolog, etc etc ... Leia também o blog de J.Pitrat sobre inteligência artificial declarativa). Veja o livro de Scott: Pragmática da Linguagem de Programação

Na verdade, um programador em C ou Ocaml geralmente não deseja codificar em um estilo de programação orientado a objetos. Não há razão para se forçar a pensar em objetos quando isso não é útil.

Você definirá algumas structe as funções que operam nelas (geralmente através de ponteiros). Você pode precisar de algumas uniões marcadas (geralmente uma structcom um membro de tag, muitas vezes algumas enume algumas unioninternas), e pode ser útil ter um membro de matriz flexível no final de alguns dos seus struct-s.

Olhe dentro do código fonte de alguns softwares livres existentes em C (consulte o github & sourceforge para encontrar alguns). Provavelmente, a instalação e o uso de uma distribuição Linux seria útil: ela é feita quase apenas de software livre, possui ótimos compiladores C de software livre ( GCC , Clang / LLVM ) e ferramentas de desenvolvimento. Consulte também Programação avançada do Linux se você deseja desenvolver para Linux.

Não se esqueça de compilar com todos os avisos e informações de depuração, por exemplo - gcc -Wall -Wextra -gnotavelmente durante as fases de desenvolvimento e depuração - e aprender a usar algumas ferramentas, por exemplo, valgrind para detectar vazamentos de memória , o gdbdepurador, etc. Lembre-se de entender bem o que é indefinido comportamento e evite-o fortemente (lembre-se de que um programa pode ter algum UB e às vezes parece "funcionar").

Quando você realmente precisa de construções orientadas a objetos (em particular herança ), pode usar ponteiros para estruturas e funções relacionadas. Você poderia ter seu próprio maquinário de tabela , ter cada "objeto" começando com um ponteiro para um structponteiro de função contendo. Você aproveita a capacidade de converter um tipo de ponteiro para outro tipo de ponteiro (e do fato de poder converter a partir de um que struct super_stcontenha os mesmos tipos de campo que aqueles que iniciam um struct sub_stpara emular herança). Observe que C é suficiente para implementar sistemas de objetos bastante sofisticados - em particular seguindo algumas convenções -, como o GObject (do GTK / Gnome) demonstra.

Quando você realmente precisa de fechamentos , geralmente os emula com retornos de chamada , com a convenção de que todas as funções que usam um retorno de chamada passam por um ponteiro de função e por alguns dados do cliente (consumidos pelo ponteiro de função quando chama isso). Você também pode ter (convencionalmente) seu próprio fechamento-como- structs (contendo algum ponteiro de função e os valores fechados).

Como C é uma linguagem de nível muito baixo, é importante definir e documentar suas próprias convenções (inspiradas pela prática em outros programas C), em particular sobre gerenciamento de memória e, provavelmente, algumas convenções de nomenclatura. É útil ter uma idéia sobre a arquitetura do conjunto de instruções . Não se esqueça que um compilador C pode fazer muitas otimizações no seu código (se você pedir), então não se importe muito em fazer micro-otimizações manualmente, deixe isso para o seu compilador ( gcc -Wall -O2para compilação otimizada de Programas). Se você se preocupa com benchmarking e desempenho bruto, deve habilitar otimizações (depois que o programa for depurado).

Não se esqueça que às vezes a metaprogramação é útil . Frequentemente, grandes softwares escritos em C contêm alguns scripts ou programas ad-hoc para gerar algum código C usado em outro lugar (e você também pode executar alguns truques sujos do pré-processador C , por exemplo , macros X ). Existem alguns geradores de programas C úteis (por exemplo, yacc ou gnu bison para gerar analisadores, gperf para gerar funções de hash perfeitas, etc ...). Em alguns sistemas (principalmente Linux e POSIX), você pode até gerar algum código C em tempo de execução no generated-001.carquivo, compilá-lo em um objeto compartilhado executando algum comando (como gcc -O -Wall -shared -fPIC generated-001.c -o generated-001.so) em tempo de execução, carregar dinamicamente esse objeto compartilhado usando dlopen& obtenha um ponteiro de função de um nome usando dlsym . Estou fazendo esses truques no MELT (uma linguagem específica de domínio semelhante ao Lisp que pode ser útil para você, pois permite a personalização do compilador GCC ).

Esteja ciente dos conceitos e técnicas de coleta de lixo (a contagem de referência geralmente é uma técnica para gerenciar a memória em C, e é uma forma ruim de coleta de lixo que não lida bem com referências circulares ; você pode ter indicadores fracos para ajudar nisso, mas pode ser complicado). Em algumas ocasiões, você pode considerar usar o coletor de lixo conservador de Boehm .

Basile Starynkevitch
fonte
7
Honestamente, independentemente dessa pergunta, ler o SICP é sem dúvida um bom conselho, mas para o OP isso provavelmente levará à próxima pergunta "Como pensar como programador C depois de influenciar o SICP".
Doc Brown
1
Não, porque o esquema do SICP e PHP (ou Ruby ou Python) é tão diferente que o OP teria uma visão muito mais ampla; e SICP explica muito bem o que é tipo de dados abstrato na prática, e isso é muito útil para compreender, em particular para a codificação em C.
Basile Starynkevitch
1
O SICP é uma sugestão estranha. Esquema é muito diferente de C.
Brian Gordon
Mas SICP é ensinar um monte de bons hábitos, e sabendo regime não ajuda quando a codificação em C (para os conceitos de fechos, tipos de dados abstratos, etc ...)
Basile Starynkevitch
5

A maneira como o programa é construído é basicamente definindo quais ações (funções) devem ser executadas para resolver o problema (é por isso que é chamado de linguagem processual). Cada ação corresponderá a uma função. Em seguida, você precisa definir que tipo de informação cada função receberá e quais informações precisam retornar.

O programa é normalmente separado em arquivos (módulos). Cada arquivo normalmente possui um grupo de funções relacionadas. No início de cada arquivo, você declara (fora de qualquer função) variáveis ​​que serão usadas por todas as funções nesse arquivo. Se você usar o qualificador "estático", essas variáveis ​​serão visíveis apenas dentro desse arquivo (mas não de outros arquivos). Se você não usar o qualificador "estático" em variáveis ​​definidas fora das funções, elas também estarão acessíveis a partir de outros arquivos e esses outros arquivos deverão declarar a variável como "externa" (mas não defini-la) para que o compilador as procure. em outros arquivos.

Então, resumindo, você primeiro pensa nos procedimentos (funções) e depois garante que todas as funções tenham acesso às informações necessárias.

Mandrill
fonte
3

As APIs C frequentemente - talvez até mesmo, geralmente - têm uma interface essencialmente orientada a objetos se você as observar da maneira certa.

Em C ++:

class foo {
    public:
        foo (int x);
        void bar (int param);
    private:
        int x;
};

// Example use:
foo f(42);
f.bar(23);

Em C:

typedef struct {
    int x;
} foo;

void bar (foo*, int param);

// Example use:
foo f = { .x = 42 };
bar(&f, 23);

Como você deve saber, no C ++ e em várias outras linguagens OO formais, um método de objeto usa um primeiro argumento que é um ponteiro para o objeto, como a versão C bar()acima. Para um exemplo de onde isso vem à tona no C ++, considere como std::bindpode ser usado para ajustar os métodos de objeto às assinaturas de funções:

new function<void(int)> (
    bind(&foo::bar, this, placeholders::_1)
//                  ^^^^ object pointer as first arg
);

Como outros já apontaram, a diferença real é que linguagens formais de OO podem implementar polimorfismo, controle de acesso e vários outros recursos interessantes. Mas a essência da programação orientada a objetos, a criação e manipulação de estruturas de dados complexas e discretas, já é uma prática fundamental em C.

Cachinhos Dourados
fonte
2

Uma das grandes razões pelas quais as pessoas são incentivadas a aprender C é que é uma das mais baixas das linguagens de programação de alto nível. As linguagens OOP facilitam o pensamento sobre modelos de dados e modelagem de código e passagem de mensagens, mas no final do dia, um microprocessador executa o código passo a passo, pulando dentro e fora dos blocos de código (funções em C) e movendo-se referências a variáveis ​​(ponteiros em C) para que diferentes partes de um programa possam compartilhar dados. Pense em C como linguagem assembly em inglês - fornecendo instruções passo a passo para o microprocessador do seu computador - e você não errará muito. Como bônus, a maioria das interfaces de sistema operacional funciona como chamadas de função C em vez de paradigmas OOP,

Gaurav
fonte
2
O IMHO C é uma linguagem de baixo nível, mas muito mais alta que o código do montador ou da máquina, pois o compilador C pode fazer muitas otimizações de baixo nível.
Basile Starynkevitch
Os compiladores C também estão, em nome da "otimização", movendo-se para um modelo abstrato de máquina que pode negar as leis de tempo e causalidade quando recebe uma entrada que causaria comportamento indefinido, mesmo que o comportamento natural do código na máquina em que se executado atenderia aos requisitos. Por exemplo, a função uint16_t blah(uint16_t x) {return x*x;}funcionará de forma idêntica em máquinas com unsigned int16 bits ou 33 bits ou mais. Alguns compiladores para máquinas com unsigned int17 a 32 bits, no entanto, podem considerar uma chamada para esse método ... #
308
... como conceder permissão para o compilador inferir que nenhuma cadeia de eventos que faria com que o método recebesse um valor superior a 46340 poderia ocorrer. Embora a multiplicação de 65533u * 65533u em qualquer plataforma produza um valor que, quando convertido em uint16_t, renderia 9, o Padrão não determina tais comportamentos ao multiplicar valores do tipo uint16_tem plataformas de 17 a 32 bits.
Supercat
-1

Também sou nativo de OO (C ++ em geral) que às vezes precisa sobreviver em um mundo de C. Para mim, o maior obstáculo fundamental é lidar com o tratamento de erros e o gerenciamento de recursos.

Em C ++, lançamos para repassar um erro de onde ocorre todo o caminho de volta ao nível superior, onde podemos lidar com isso e temos destruidores para liberar automaticamente nossa memória e outros recursos.

Você pode notar que muitas APIs C incluem uma função init que fornece um void * digitado, que é realmente um ponteiro para uma estrutura. Em seguida, você passa isso como o primeiro argumento para cada chamada de API. Essencialmente, isso se torna o ponteiro "this" do C ++. Ele é usado para todas as estruturas de dados internas que estão ocultas (um conceito muito OO). Você também pode usá-lo para gerenciar memória, por exemplo, ter uma função chamada myapiMalloc, que aloca sua memória e grava o malloc na versão C de um ponteiro deste, para garantir que ele seja liberado quando a API retornar. Além disso, como descobri recentemente, você pode usá-lo para armazenar códigos de erro e usar setjmp e longjmp para fornecer um comportamento muito semelhante ao throw catch. A combinação dos dois conceitos oferece muitas funcionalidades de um programa C ++.

Agora você disse que não queria aprender a forçar C em C ++. Não é exatamente isso que estou descrevendo (pelo menos não deliberadamente). Este é simplesmente um método (esperançosamente) bem projetado para explorar a funcionalidade C. Ele acaba tendo alguns sabores de OO - talvez seja por isso que as linguagens de OO se desenvolveram, elas foram uma maneira de formalizar / reforçar / facilitar conceitos que algumas pessoas consideraram melhores práticas.

Se você acha que esse é o ponto de partida para você, a alternativa é que praticamente todas as funções retornem um código de erro que você deve religiosamente garantir que verifique após cada chamada de função e propague a pilha de chamadas. Você deve garantir que todos os recursos sejam liberados não apenas no final de cada função, mas em todos os pontos de retorno (que podem ocorrer após qualquer chamada de função que possa retornar um erro que indica que você não pode continuar). Pode ser muito tedioso e fazer com que você pense que provavelmente não preciso lidar com essa falha potencial de alocação de memória (ou leitura de arquivo ou conexão de porta ...), apenas assumirei que funcionará ou eu ' Vou escrever o código "interessante" agora e voltar e lidar com o tratamento de erros - o que nunca acontece.

Phil Rosenberg
fonte