¿Cómo pensar como un programador en C después de sesgado con el lenguaje OOP? [cerrado]

38

Anteriormente, solo he usado lenguajes de programación orientados a objetos (C ++, Ruby, Python, PHP), y ahora estoy aprendiendo C. Me resulta difícil encontrar la manera correcta de hacer las cosas en un lenguaje sin el concepto de un 'Objeto'. Me doy cuenta de que es posible usar paradigmas OOP en C, pero me gustaría aprender la forma idiomática C.

Al resolver un problema de programación, lo primero que hago es imaginar un objeto que resuelva el problema. ¿Con qué pasos reemplazo esto cuando uso un paradigma de programación imperativa que no es OOP?

Mas Bagol
fuente
15
Todavía tengo que encontrar un idioma que coincida con mi forma de pensar, por lo que tengo que "compilar" mis pensamientos para cualquier idioma que estoy usando. Un concepto que he encontrado útil es el de una "unidad de código", ya sea una etiqueta, subrutina, función, objeto, módulo o marco: cada uno de ellos debe estar encapsulado y exponer una interfaz bien definida. Si está acostumbrado a un enfoque de nivel de objeto de arriba hacia abajo, en C puede comenzar elaborando un conjunto de funciones que se comporten como si el problema se hubiera resuelto. A menudo, las API de C bien diseñadas parecen OOP, pero se qux = foo.bar(baz)vuelven qux = Foo_bar(foo, baz).
amon
Para hacerse eco de amon , concéntrese en lo siguiente: estructura de datos tipo gráfico, punteros, algoritmos, la ejecución (flujo de control) de código (funciones), punteros de función.
rwong
1
LibTiff (código fuente en github) es un ejemplo de cómo organizar grandes programas en C.
rwong
1
Como programador de C #, extrañaría delegados (punteros de función con un parámetro enlazado) mucho más de lo que extrañaría objetos.
CodesInChaos
Personalmente, encontré que la mayoría de C es fácil y directo, con la notable excepción del preprocesador. Si tuviera que volver a aprender C, esa sería un área en la que concentraría mucho mi esfuerzo.
biziclop

Respuestas:

53
  • El programa AC es una colección de funciones.
  • Una función es una colección de declaraciones.
  • Puede encapsular datos con a struct.

Eso es.

¿Cómo escribiste una clase? Así es como escribes un archivo .C. De acuerdo, no obtienes cosas como el método de polimorfismo y herencia, pero de todos modos puedes simular aquellos con diferentes nombres de funciones y composiciones .

Para allanar el camino, estudie la Programación Funcional. Es realmente sorprendente lo que puedes hacer sin clases, y algunas cosas funcionan mejor sin la sobrecarga de clases.

Lectura adicional
Orientación a objetos en ANSI C

Robert Harvey
fuente
9
también puedes hacer typedefeso structy hacer algo parecido a una clase . Los typedeftipos yed se pueden incluir en otros structs que pueden ser ellos mismos typedef. lo que no se obtiene con C es la sobrecarga del operador y la herencia superficialmente simple de las clases y los miembros que se encuentran en C ++. y no obtienes mucha sintaxis extraña y antinatural que obtienes con C ++. Realmente me encanta el concepto de OOP, pero creo que C ++ es una realización fea de OOP. Me gusta C porque es un lenguaje más pequeño y deja de lado la sintaxis del lenguaje que mejor se deja a las funciones.
Robert Bristow-Johnson
22
Como alguien cuyo primer idioma era / es C, me aventuraría a decir eso . a lot of things actually work better without the overhead of classes
haneefmubarak 01 de
1
Para expandirse, se han desarrollado muchas cosas sin OOP: sistemas operativos, servidores de protocolo, cargadores de arranque, navegadores, etc. Las computadoras no piensan en términos de objetos y tampoco necesitan hacerlo. De hecho, a menudo es bastante lento para ellos forzar eso.
edmz 01 de
Contrapunto: a lot of things actually work better with addition of class-based OOP. Fuente: TypeScript, Dart, CoffeeScript y todas las otras formas en que la industria está tratando de alejarse de un lenguaje OOP funcional / prototipo.
Den
Para expandirse, se han desarrollado muchas cosas con OOP: todo lo demás. Los humanos piensan naturalmente en términos de objetos y los programas están escritos para que otros humanos los lean y comprendan.
Den
18

Lea el SICP y aprenda el Esquema y la idea práctica de los tipos de datos abstractos . Luego, codificar en C es fácil (ya que con SICP, un poco de C y un poco de PHP, Ruby, etc., su pensamiento sería lo suficientemente amplio, y comprendería que la programación orientada a objetos podría no ser el mejor estilo en todos los casos, pero solo para algún tipo de programa). Tenga cuidado con la asignación de memoria dinámica C , que probablemente sea la parte más difícil. El estándar de lenguaje de programación C99 o C11 y su biblioteca estándar C es bastante pobre (¡no sabe sobre TCP o directorios!), Y a menudo necesitará algunas bibliotecas o interfaces externas (p. Ej.POSIX , libcurl para la biblioteca del cliente HTTP, libonion para la biblioteca del servidor HTTP, GMPlib para bignums, alguna biblioteca como libunistring para UTF-8, etc.).

Sus "objetos" a menudo están en C algunos struct-s relacionados , y usted define el conjunto de funciones que operan en ellos. Para funciones cortas o muy simples, considere definirlas, con la correspondiente struct, como static inlineen algún archivo de encabezado foo.hpara ser #include-d en otro lugar.

Tenga en cuenta que la programación orientada a objetos no es el único paradigma de programación . En algunas ocasiones, otros paradigmas valen la pena ( programación funcional a la Ocaml o Haskell o incluso Scheme o Commmon Lisp, programación lógica a la Prolog, etc. etc ... Lea también el blog de J.Pitrat sobre inteligencia artificial declarativa). Ver el libro de Scott: Pragmática del lenguaje de programación

En realidad, un programador en C, o en Ocaml, generalmente no quiere codificar en un estilo de programación orientado a objetos. No hay razón para obligarse a pensar en objetos cuando eso no es útil.

Definirá algunos structy las funciones que operan en ellos (a menudo a través de punteros). Es posible que necesite algunas uniones etiquetadas (a menudo, structcon un miembro de etiqueta, a menudo algunas enumy algunas uniondentro), y puede resultarle útil tener un miembro de matriz flexible al final de algunos de sus struct-s.

Mire dentro del código fuente de algún software libre existente en C (vea github & sourceforge para encontrar algunos). Probablemente, instalar y usar una distribución de Linux sería útil: está hecho casi exclusivamente de software libre, tiene excelentes compiladores de software libre C ( GCC , Clang / LLVM ) y herramientas de desarrollo. Consulte también Programación avanzada de Linux si desea desarrollar para Linux.

No olvide compilar todas las advertencias e información de depuración, por ejemplo, gcc -Wall -Wextra -gnotablemente durante las fases de desarrollo y depuración, y aprenda a usar algunas herramientas, por ejemplo, valgrind para buscar fugas de memoria , el gdbdepurador, etc. Tenga cuidado de comprender bien lo que no está definido. comportamiento y evítelo (recuerde que un programa podría tener algo de UB y que a veces parece "funcionar").

Cuando realmente necesita construcciones orientadas a objetos (en particular herencia ) puede usar punteros a estructuras relacionadas y funciones. Puede tener su propia maquinaria vtable , hacer que cada "objeto" comience con un puntero a una structfunción que contiene punteros. Aprovecha la capacidad de emitir un tipo de puntero a otro tipo de puntero (y el hecho de que puede emitir desde un que struct super_stcontenga los mismos tipos de campo que aquellos que comienzan struct sub_sta emular la herencia). Tenga en cuenta que C es suficiente para implementar sistemas de objetos bastante sofisticados, en particular siguiendo algunas convenciones , como lo demuestra GObject (de GTK / Gnome).

Cuando realmente necesite cierres , a menudo los emulará con devoluciones de llamada , con la convención de que cada función que utiliza una devolución de llamada pasa tanto un puntero de función como algunos datos del cliente (consumidos por el puntero de función cuando llama a eso). También podría tener (convencionalmente) sus propios cierres como struct(que contienen algún puntero de función y los valores cerrados).

Dado que C es un lenguaje de muy bajo nivel, es importante definir y documentar sus propias convenciones (inspiradas en la práctica en otros programas de C), en particular sobre la administración de memoria, y probablemente también algunas convenciones de nomenclatura. Es útil tener alguna idea sobre la arquitectura del conjunto de instrucciones . No olvide que un compilador de C puede hacer muchas optimizaciones en su código (si lo solicita), así que no se preocupe demasiado por hacer micro-optimizaciones a mano, déjelo a su compilador ( gcc -Wall -O2para una compilación optimizada de software). Si le interesan las evaluaciones comparativas y el rendimiento sin procesar, debe habilitar las optimizaciones (una vez que se haya depurado el programa).

No olvides que a veces la metaprogramación es útil . Muy a menudo, el software grande escrito en C contiene algunos scripts o programas ad-hoc para generar algún código C utilizado en otro lugar (y también puede jugar algunos trucos sucios del preprocesador C , por ejemplo , macros X ). Existen algunos generadores de programas C útiles (por ejemplo, yacc o gnu bison para generar analizadores, gperf para generar funciones hash perfectas, etc.). En algunos sistemas (especialmente Linux y POSIX), incluso podría generar algún código C en tiempo de ejecución en el generated-001.carchivo, compilarlo en un objeto compartido ejecutando algún comando (como gcc -O -Wall -shared -fPIC generated-001.c -o generated-001.so) en tiempo de ejecución, cargar dinámicamente ese objeto compartido usando dlopen& obtener un puntero de función de un nombre usando dlsym . Estoy haciendo tales trucos en MELT (un lenguaje específico de dominio similar a Lisp que podría serle útil, ya que permite la personalización del compilador GCC ).

Tenga en cuenta los conceptos y técnicas de recolección de basura (el conteo de referencias es a menudo una técnica para administrar la memoria en C, y en mi humilde opinión, es una forma pobre de recolección de basura que no trata bien con referencias circulares ; podría tener punteros débiles para ayudar con eso, pero puede ser complicado) En algunas ocasiones, puede considerar usar el recolector de basura conservador de Boehm .

Basile Starynkevitch
fuente
77
Honestamente, independientemente de esta pregunta, leer SICP es, sin duda, un buen consejo, pero para el OP esto probablemente conducirá a la siguiente pregunta "Cómo pensar como un programador C después de sesgarse con SICP".
Doc Brown
1
No, porque el esquema de SICP y PHP (o Ruby o Python) son tan diferentes que el OP tendría un pensamiento mucho más amplio; y SICP explica bastante bien qué es el tipo de datos abstractos en la práctica, y eso es muy útil de entender, en particular para la codificación en C.
Basile Starynkevitch
1
SICP es una sugerencia extraña. El esquema es muy diferente de C.
Brian Gordon
Pero SICP está enseñando muchos buenos hábitos, y saber que Scheme ayuda cuando se codifica en C (para los conceptos de cierres, tipos de datos abstractos, etc.)
Basile Starynkevitch
5

La forma en que se construye el programa es básicamente definir qué acciones (funciones) se deben realizar para resolver el problema (es por eso que se llama lenguaje de procedimiento). Cada acción corresponderá a una función. Luego, debe definir qué tipo de información recibirá cada función y qué información necesita devolver.

El programa normalmente está separado en archivos (módulos), cada archivo normalmente tendrá un grupo de funciones relacionadas. Al comienzo de cada archivo, declara (fuera de cualquier función) las variables que serán utilizadas por todas las funciones en ese archivo. Si utiliza el calificador "estático", esas variables solo serán visibles dentro de ese archivo (pero no desde otros archivos). Si no utiliza el calificador "estático" en las variables definidas fuera de las funciones, también será accesible desde otros archivos y estos otros archivos deberían declarar la variable como "externa" (pero no definirla) para que el compilador las busque en otros archivos

En resumen, primero piensa en los procedimientos (funciones) y luego se asegura de que todas las funciones tengan acceso a la información que necesitan.

Mandril
fuente
3

Las API de C a menudo, tal vez incluso, por lo general , tienen una interfaz esencialmente orientada a objetos si las mira de la manera correcta.

En C ++:

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

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

Cía:

typedef struct {
    int x;
} foo;

void bar (foo*, int param);

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

Como ya sabrá, en C ++ y en varios otros lenguajes formales de OO, bajo el capó, un método de objeto toma un primer argumento que es un puntero al objeto, al igual que la versión C bar()anterior. Para ver un ejemplo de dónde sale esto a la superficie en C ++, considere cómo std::bindse puede usar para ajustar los métodos de objeto a las firmas funcionales:

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

Como otros han señalado, la verdadera diferencia es que los lenguajes formales de OO pueden implementar polimorfismo, control de acceso y varias otras características ingeniosas. Pero la esencia de la programación orientada a objetos, la creación y manipulación de estructuras de datos discretas y complejas, ya es una práctica fundamental en C.

encerrada dorada
fuente
2

Una de las principales razones por las que se alienta a las personas a aprender C es que es uno de los lenguajes de programación de nivel más bajo. Los lenguajes OOP hacen que sea más fácil pensar en los modelos de datos y el código de plantillas y el paso de mensajes, pero al final del día, un microprocesador ejecuta el código paso a paso, entrando y saliendo de bloques de código (funciones en C) y moviéndose referencias a variables (punteros en C) alrededor para que diferentes partes de un programa puedan compartir datos. Piense en C como lenguaje ensamblador en inglés, que proporciona instrucciones paso a paso para el microprocesador de su computadora, y no se equivocará. Como beneficio adicional, la mayoría de las interfaces del sistema operativo funcionan como llamadas de función C en lugar de paradigmas OOP,

Gaurav
fuente
2
En mi humilde opinión, C es un lenguaje de bajo nivel, pero mucho más alto que el ensamblador o el código de máquina, ya que el compilador de C podría hacer muchas optimizaciones de bajo nivel.
Basile Starynkevitch 01 de
Los compiladores de C también, en nombre de la "optimización", se están moviendo hacia un modelo de máquina abstracto que puede negar las leyes del tiempo y la causalidad cuando se les da una entrada que causaría un comportamiento indefinido, incluso si el comportamiento natural del código en la máquina donde se ejecuta cumpliría de lo contrario cumpliría los requisitos. Por ejemplo, la función uint16_t blah(uint16_t x) {return x*x;}funcionará de manera idéntica en máquinas con unsigned int16 bits o con 33 bits o más. unsigned intSin embargo, algunos compiladores para máquinas de 17 a 32 bits pueden considerar una llamada a ese método ...
supercat
... como otorgando permiso para que el compilador deduzca que no puede ocurrir una cadena de eventos que pueda causar que el método tenga un valor superior a 46340. Aunque multiplicar 65533u * 65533u en cualquier plataforma arrojaría un valor que, cuando se convierte en uint16_t, arrojaría 9, el Estándar no exige tales comportamientos al multiplicar valores de tipo uint16_ten plataformas de 17 a 32 bits.
supercat
-1

También soy un nativo de OO (C ++ en general) que a veces tiene que sobrevivir en un mundo de C. Para mí, el obstáculo más importante es tratar con el manejo de errores y la gestión de recursos.

En C ++ tenemos que pasar un error desde donde ocurre hasta el nivel superior donde podemos lidiar con él y tenemos destructores para liberar automáticamente nuestra memoria y otros recursos.

Puede notar que muchas API de C incluyen una función init que le proporciona un typedef'd void * que es realmente un puntero a una estructura. Luego, pasa esto como el primer argumento para cada llamada a la API. Esencialmente, eso se convierte en su "este" puntero de C ++. Se utiliza para todas las estructuras de datos internas que están ocultas (un concepto muy OO). También puede usarlo para administrar la memoria, por ejemplo, tener una función llamada myapiMalloc que malloquea su memoria y registra el malloc en su versión C de este puntero para que pueda asegurarse de que esté libre cuando regrese su API. Además, como descubrí recientemente, puede usarlo para almacenar códigos de error y usar setjmp y longjmp para brindarle un comportamiento muy similar al lanzamiento de capturas. La combinación de ambos conceptos le brinda mucha funcionalidad de un programa C ++.

Ahora dijiste que no querías aprender a forzar C a C ++. Eso no es realmente lo que estoy describiendo (al menos no deliberadamente). Este es simplemente un método (con suerte) bien diseñado para explotar la funcionalidad de C. Resulta que tiene algunos sabores OO, tal vez por eso se desarrollaron los lenguajes OO, fueron una forma de formalizar / hacer cumplir / facilitar conceptos que algunas personas consideraron la mejor práctica.

Si cree que esto es algo que OO siente por usted, entonces la alternativa es que casi todas las funciones devuelvan un código de error que debe asegurarse religiosamente de verificar después de cada llamada de función y propagar la pila de llamadas. Debe asegurarse de que todos los recursos se liberen no solo al final de cada función, sino en cada punto de retorno (lo que podría suceder después de cualquier llamada de función que pueda devolver un error que indique que no puede continuar). Puede volverse muy tedioso y tiende a hacerte pensar que probablemente no necesito lidiar con esa falla potencial de asignación de memoria (o lectura de archivo o conexión de puerto ...), solo asumiré que funcionará o ' Escribiré el código "interesante" ahora y volveré y me ocuparé del manejo de errores, lo que nunca sucede.

Phil Rosenberg
fuente