Come pensare come programmatore C dopo aver influenzato il linguaggio OOP? [chiuso]


38

In precedenza, ho usato solo linguaggi di programmazione orientata agli oggetti (C ++, Ruby, Python, PHP) e ora sto imparando C. Sto trovando difficoltà a capire il modo corretto di fare le cose in un linguaggio senza il concetto di un 'Oggetto'. Mi rendo conto che è possibile utilizzare i paradigmi OOP in C, ma mi piacerebbe imparare il modo C-idiomatico.

Quando risolvo un problema di programmazione, la prima cosa che faccio è immaginare un oggetto che risolva il problema. Con quali passaggi lo sostituisco quando utilizzo un paradigma di programmazione imperativa non OOP?


15
Devo ancora trovare un linguaggio che corrisponda strettamente al mio modo di pensare, quindi devo "compilare" i miei pensieri per qualsiasi linguaggio che sto usando. Un concetto che ho trovato utile è quello di una "unità di codice", che si tratti di un'etichetta, di una subroutine, di una funzione, di un oggetto, di un modulo o di un framework: ognuno di essi dovrebbe essere incapsulato ed esporre un'interfaccia ben definita. Se sei abituato ad un approccio top-down a livello di oggetto, in C potresti iniziare elaborando una serie di funzioni che si comportano come se il problema fosse stato risolto. Spesso, le API C ben progettate sembrano OOP, ma qux = foo.bar(baz)diventano qux = Foo_bar(foo, baz).
amon,

Per echo amon , concentrarsi sui seguenti: grafico simile struttura di dati, puntatori, algoritmi, l'esecuzione (flusso di controllo) di codice (funzioni), puntatori a funzione.
rwong

1
LibTiff (codice sorgente su github) è un esempio di come organizzare grandi programmi C.
rwong

1
Come programmatore C # mi mancheranno i delegati (puntatori a funzioni con un parametro associato) molto più di quanto mi manchino gli oggetti.
Codici InCos

Personalmente ho trovato la maggior parte di C semplice e diretta, con la notevole eccezione del pre-processore. Se dovessi ri-imparare il C, quella sarebbe un'area su cui concentrerei molto i miei sforzi.
biziclop,

Risposte:


53
  • Il programma AC è una raccolta di funzioni.
  • Una funzione è una raccolta di istruzioni.
  • È possibile incapsulare i dati con a struct.

Questo è tutto.

Come hai scritto una lezione? È praticamente come scrivi un file .C. Certo, non ottieni cose come il polimorfismo del metodo e l'ereditarietà, ma puoi comunque simulare quelli con nomi di funzioni e composizione diversi .

Per aprire la strada, studia Programmazione funzionale. È davvero sorprendente ciò che puoi fare senza le lezioni, e alcune cose funzionano davvero meglio senza il sovraccarico delle lezioni.

Ulteriore lettura
dell'orientamento agli oggetti in ANSI C


9
è anche possibile typedefche structe fare qualcosa di classe simile . i typedeftipi e -ed possono essere inclusi in altri structs che possono essere typedef-ed essi stessi . ciò che non si ottiene con C è il sovraccarico dell'operatore e l'eredità superficialmente semplice delle classi e dei membri all'interno di ciò che si ottiene in C ++. e non ottieni molta sintassi strana e innaturale che ottieni con C ++. amo davvero il concetto di OOP, ma penso che C ++ sia una brutta realizzazione di OOP. mi piace C perché è una lingua più piccola ed esclude la sintassi dalla lingua che è meglio lasciare alle funzioni.
robert bristow-johnson,

22
Come persona la cui prima lingua era / è la C, mi azzarderei a dirlo . a lot of things actually work better without the overhead of classes
Haneefmubarak,

1
Per espandere, molte cose sono state sviluppate senza OOP: sistemi operativi, server di protocollo, caricatori di avvio, browser e così via. I computer non pensano in termini di oggetti e nemmeno ne hanno bisogno. In effetti, è spesso piuttosto lento per loro forzarlo.
edmz,

Contrappunto: a lot of things actually work better with addition of class-based OOP. Fonte: TypeScript, Dart, CoffeeScript e tutti gli altri modi in cui l'industria sta cercando di allontanarsi da un linguaggio OOP funzionale / prototipo.
Den

Per espandere, molte cose sono state sviluppate con OOP: tutto il resto. Gli umani pensano naturalmente in termini di oggetti e programmi scritti per altri umani da leggere e comprendere.
Den

18

Leggi SICP e apprendi Schema e l' idea pratica di tipi di dati astratti . Quindi la codifica in C è facile (poiché con SICP, un po 'di C e un po' di PHP, Ruby, ecc ... il tuo pensiero sarebbe abbastanza ampio e capiresti che la programmazione orientata agli oggetti potrebbe non essere lo stile migliore in tutti i casi, ma solo per alcuni tipi di programmi). Fai attenzione all'allocazione dinamica della memoria C , che è probabilmente la parte più difficile. Lo standard del linguaggio di programmazione C99 o C11 e la sua libreria standard C sono in realtà piuttosto scadenti (non conoscono TCP o directory!) E spesso avrai bisogno di alcune librerie o interfacce esterne (ad es.POSIX , libcurl per libreria client HTTP, libonion per libreria server HTTP, GMPlib per bignum, alcune librerie come libunistring per UTF-8, ecc ...).

I tuoi "oggetti" sono spesso in C alcuni-correlati structe tu definisci l'insieme di funzioni che operano su di essi. Per funzioni brevi o molto semplici, considera di definirle, con i relativi struct, come static inlinein alcuni file di intestazione che foo.hdevono essere #include-d altrove.

Si noti che la programmazione orientata agli oggetti non è l'unico paradigma di programmazione . In alcune occasioni, sono utili altri paradigmi ( programmazione funzionale alla Ocaml o Haskell o persino Scheme o Commmon Lisp, programmazione logica alla Prolog, ecc. Ecc . Leggi anche il blog di J.Pitrat sull'intelligenza artificiale dichiarativa). Vedi il libro di Scott: Programming Language Pragmatics

In realtà, un programmatore in C, o in Ocaml, di solito non vuole programmare in uno stile di programmazione orientato agli oggetti. Non c'è motivo di forzarti a pensare agli oggetti quando ciò non è utile.

structNe definirai alcune e le funzioni che operano su di esse (spesso tramite puntatori). Potresti aver bisogno di alcuni sindacati con tag (spesso, uno structcon un membro tag, spesso alcuni enume alcuni unionall'interno), e potresti trovare utile avere un membro dell'array flessibile alla fine di alcuni dei tuoi struct-s.

Guarda all'interno del codice sorgente di alcuni software gratuiti esistenti in C (vedi github e sourceforge per trovarne alcuni). Probabilmente, installare e utilizzare una distribuzione Linux sarebbe utile: è fatto quasi solo di software libero, ha grandi compilatori C di software libero ( GCC , Clang / LLVM ) e strumenti di sviluppo. Vedi anche Advanced Linux Programming se vuoi sviluppare per Linux.

Non dimenticare di compilare tutti gli avvisi e le informazioni di debug, ad esempio, in gcc -Wall -Wextra -gparticolare durante le fasi di sviluppo e debug, e imparare ad usare alcuni strumenti, ad esempio valgrind per cacciare perdite di memoria , gdbdebugger, ecc. Fai attenzione a capire bene cosa non è definito comportamento ed evitarlo fortemente (ricorda che un programma potrebbe avere un certo UB e talvolta sembra "funzionare").

Quando hai davvero bisogno di costrutti orientati agli oggetti (in particolare ereditarietà ) puoi usare i puntatori alle strutture correlate e alle funzioni. Potresti avere il tuo macchinario vtable , ogni "oggetto" che inizia con un puntatore a puntatori di una structfunzione contenente. Sfruttate la possibilità di trasmettere un tipo di puntatore a un altro tipo di puntatore (e del fatto che è possibile eseguire il cast da un struct super_stcontenente gli stessi tipi di campo di quelli che iniziano struct sub_sta emulare l'ereditarietà). Si noti che C è sufficiente per implementare sistemi di oggetti piuttosto sofisticati, in particolare seguendo alcune convenzioni , come dimostra GObject (da GTK / Gnome).

Quando hai davvero bisogno di chiusure , le emulerai spesso con i callback , con la convenzione che a ogni funzione che utilizza un callback vengono passati sia un puntatore a funzione che alcuni dati client (consumati dal puntatore a funzione quando lo chiama). Potresti anche avere (convenzionalmente) il tuo tipo di chiusura struct(contenente un puntatore a funzione e i valori chiusi).

Poiché C è un linguaggio di livello molto basso, è importante definire e documentare le proprie convenzioni (ispirate alla pratica in altri programmi C), in particolare sulla gestione della memoria, e probabilmente anche su alcune convenzioni di denominazione. È utile avere qualche idea sull'architettura del set di istruzioni . Non dimenticare che un compilatore C può fare molte ottimizzazioni sul tuo codice (se lo chiedi), quindi non preoccuparti troppo di fare le micro-ottimizzazioni a mano, lascialo al tuo compilatore ( gcc -Wall -O2per una compilazione ottimizzata di rilasciati Software). Se ti interessa il benchmarking e le prestazioni non elaborate, dovresti abilitare le ottimizzazioni (una volta eseguito il debug del programma).

Non dimenticare che a volte la metaprogrammazione è utile . Molto spesso, i software di grandi dimensioni scritti in C contengono alcuni script o programmi ad-hoc per generare del codice C usato altrove (e potresti anche giocare a trucchi del preprocessore C sporchi , ad esempio X-macro ). Esistono alcuni generatori di programmi C utili (ad es. Yacc o gnu bison per generare parser, gperf per generare funzioni hash perfette, ecc ...). Su alcuni sistemi (in particolare Linux e POSIX) potresti persino generare del codice C in fase di runtime nel generated-001.cfile, compilarlo in un oggetto condiviso eseguendo alcuni comandi (come gcc -O -Wall -shared -fPIC generated-001.c -o generated-001.so) in fase di runtime, caricare dinamicamente l'oggetto condiviso utilizzando dlopene ottieni un puntatore a funzione da un nome usando dlsym . Sto facendo questi trucchi in MELT (un linguaggio specifico del dominio simile a Lisp che potrebbe esserti utile, poiché consente la personalizzazione del compilatore GCC ).

Fai attenzione ai concetti e alle tecniche di garbage collection (il conteggio dei riferimenti è spesso una tecnica per gestire la memoria in C, ed è IMHO una cattiva forma di garbage collection che non si occupa bene dei riferimenti circolari ; potresti avere dei punti deboli per aiutarti, ma potrebbe essere complicato). In alcune occasioni, potresti prendere in considerazione l'uso del garbage collector conservatore di Boehm .


7
Onestamente, indipendentemente da questa domanda, leggere SICP è senza dubbio un buon consiglio, ma per l'OP questo porterà probabilmente alla domanda successiva "Come pensare come programmatore C dopo aver influenzato SICP".
Doc Brown,

1
No, perché gli Schemi di SICP e PHP (o Ruby o Python) sono così diversi che l'OP otterrebbe un pensiero molto più ampio; e SICP spiega abbastanza bene quale sia il tipo di dati astratto nella pratica, e che è molto utile da capire, in particolare per la codifica in C.
Basile Starynkevitch

1
SICP è uno strano suggerimento. Lo schema è molto diverso da C.
Brian Gordon,

Ma la SICP sta insegnando molte buone abitudini, e conoscere Scheme aiuta quando si codifica in C (per i concetti di chiusure, tipi di dati astratti, ecc ...)
Basile Starynkevitch

5

Il modo in cui è costruito il programma sta sostanzialmente definendo quali azioni (funzioni) devono essere eseguite per risolvere il problema (ecco perché si chiama linguaggio procedurale). Ogni azione corrisponderà a una funzione. Quindi è necessario definire il tipo di informazioni che ciascuna funzione riceverà e quali informazioni devono restituire.

Il programma è normalmente separato in file (moduli) ogni file avrà normalmente un gruppo di funzioni correlate. All'inizio di ogni file si dichiarano (al di fuori di qualsiasi funzione) le variabili che verranno utilizzate da tutte le funzioni in quel file. Se si utilizza il qualificatore "statico", tali variabili saranno visibili solo all'interno di quel file (ma non da altri file). Se non usi il qualificatore "statico" su variabili definite al di fuori delle funzioni, saranno accessibili anche da altri file e questi altri file dovrebbero dichiarare la variabile come "esterna" (ma non definirla) in modo che il compilatore li cercherà in altri file.

Quindi, in breve, prima pensi alle procedure (funzioni), quindi assicurati che tutte le funzioni abbiano accesso alle informazioni di cui hanno bisogno.


3

Le API C spesso - forse persino, di solito - hanno un'interfaccia essenzialmente orientata agli oggetti se le guardi nel modo giusto.

In C ++:

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

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

In C:

typedef struct {
    int x;
} foo;

void bar (foo*, int param);

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

Come forse saprai, in C ++ e in vari altri linguaggi OO formali, sotto il cofano un metodo a oggetti accetta un primo argomento che è un puntatore all'oggetto, proprio come la versione C bar()sopra. Per un esempio di come questo viene a galla in C ++, considera come std::bindpuò essere usato per adattare i metodi degli oggetti alle funzioni delle firme:

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

Come altri hanno sottolineato, la vera differenza è che i linguaggi OO formali possono implementare il polimorfismo, il controllo degli accessi e varie altre funzioni intelligenti. Ma l'essenza della programmazione orientata agli oggetti, la creazione e la manipolazione di strutture di dati discrete e complesse, è già una pratica fondamentale in C.


2

Uno dei motivi principali per cui le persone sono incoraggiate ad imparare il C è che è uno dei linguaggi di programmazione più bassi di alto livello. I linguaggi OOP rendono più semplice pensare ai modelli di dati e al modello di codice e al passaggio di messaggi, ma alla fine un microprocessore esegue il codice passo dopo passo, saltando dentro e fuori da blocchi di codice (funzioni in C) e spostandosi riferimenti a variabili (puntatori in C) intorno in modo che parti diverse di un programma possano condividere dati. Pensa a C come linguaggio di assemblaggio in inglese - fornendo istruzioni passo-passo al microprocessore del tuo computer - e non ti sbaglierai molto. Come bonus, la maggior parte delle interfacce del sistema operativo funzionano come chiamate di funzione C piuttosto che paradigmi OOP,


2
IMHO C è un linguaggio di basso livello, ma molto più elevato dell'assemblatore o del codice macchina, poiché il compilatore C potrebbe eseguire molte ottimizzazioni di basso livello.
Basile Starynkevitch,

I compilatori C si stanno anche muovendo, nel nome di "ottimizzazione", verso un modello di macchina astratta che può negare le leggi del tempo e della causalità quando viene fornito un input che causerebbe un comportamento indefinito, anche se il comportamento naturale del codice sulla macchina in cui si trova è eseguito soddisferebbe altrimenti soddisfare i requisiti. Ad esempio, la funzione funzionerà in uint16_t blah(uint16_t x) {return x*x;}modo identico su macchine con unsigned int16 bit o con 33 bit o più. Alcuni compilatori per macchine da unsigned int17 a 32 bit, tuttavia, potrebbero considerare una chiamata a quel metodo ...
supercat

... come concessione dell'autorizzazione per il compilatore a dedurre che nessuna catena di eventi che provocherebbe un valore superiore a 46340 potrebbe verificarsi. Anche se moltiplicare 65533u * 65533u su qualsiasi piattaforma produrrebbe un valore che, se lanciato a uint16_t, produrrebbe 9, lo Standard non impone tali comportamenti quando moltiplica valori di tipo uint16_tsu piattaforme da 17 a 32 bit.
supercat

-1

Sono anche io nativo di OO (C ++ in generale) che a volte deve sopravvivere in un mondo di C. Per me l'ostacolo fondamentalmente più grande riguarda la gestione degli errori e la gestione delle risorse.

In C ++ abbiamo intenzione di passare un errore da dove accade fino al livello più alto in cui possiamo gestirlo e abbiamo distruttori per liberare automaticamente la nostra memoria e altre risorse.

Potresti notare che molte API C includono una funzione init che ti fornisce un vuoto tipizzato * che è in realtà un puntatore a una struttura. Quindi lo passi come primo argomento per ogni chiamata API. In sostanza, questo diventa il tuo "questo" puntatore dal C ++. Viene utilizzato per tutte le strutture di dati interne che sono nascoste (un concetto molto OO). Puoi anche usarlo per gestire la memoria, ad esempio avere una funzione chiamata myapiMalloc che mallocizza la tua memoria e registra il malloc nella tua versione C di questo puntatore in modo da poter essere sicuro che venga liberato quando l'API ritorna. Inoltre, come ho scoperto di recente, puoi usarlo per memorizzare i codici di errore e utilizzare setjmp e longjmp per darti un comportamento molto simile a quello del lancio. La combinazione di entrambi i concetti offre molte funzionalità di un programma C ++.

Ora hai detto che non volevi imparare a forzare C in C ++. Questo non è proprio quello che sto descrivendo (almeno non deliberatamente). Questo è semplicemente un metodo (si spera) ben progettato per sfruttare la funzionalità C. Si scopre che ha alcuni sapori OO - forse è per questo che i linguaggi OO sviluppati, sono stati un modo per formalizzare / applicare / facilitare concetti che alcune persone hanno scoperto essere le migliori pratiche.

Se ritieni che ciò provi OO per te, l'alternativa è avere praticamente ogni funzione che restituisca un codice di errore che devi religiosamente assicurarti di controllare dopo ogni chiamata di funzione e propagare lo stack di chiamate. È necessario assicurarsi che tutte le risorse vengano liberate non solo alla fine di ciascuna funzione, ma in ogni punto di ritorno (che potrebbe essere potenzialmente dopo una chiamata di funzione che può restituire un errore che indica che non è possibile continuare). Può diventare molto noioso e tende a farti pensare che probabilmente non ho bisogno di occuparmi di quel potenziale errore di allocazione della memoria (o della lettura del file o della connessione della porta ...), suppongo che funzionerà o Scriverò ora il codice "interessante" e tornerò e gestiremo la gestione degli errori, cosa che non succede mai.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.