¿Cuáles son las nociones importantes en C que no aprendiste de tus maestros?

En septiembre, daré mis primeras conferencias sobre C a estudiantes en la escuela de ingeniería (normalmente enseño matemáticas y procesamiento de señales, pero también he hecho mucho trabajo práctico en C, sin dar las conferencias). La informática no es su tema principal (son más estudios de electrónica y procesamiento de señales), pero necesitan tener una buena formación en progtwigción (algunos de ellos pueden convertirse en desarrolladores de software)

Este año será su segundo año de aprendizaje de C (se supone que deben saber qué es un puntero y cómo usarlo, pero, por supuesto, esta noción aún no se ha asimilado)

Además de las cosas clásicas (estructuras de datos, algoritmos clásicos, …), probablemente centraré algunas de mis conferencias en:

  • Diseñe el algoritmo (y escríbalo en pseudocódigo) antes de codificarlo en C (piense antes de codificar)
  • Haga que su código sea legible (comentarios, nombres de variables, …) y
  • Punteros, punteros, punteros! (qué es, cómo y cuándo usarlo, asignación de memoria, etc.)

Según su experiencia, ¿cuáles son las nociones más importantes en C que sus maestros nunca le enseñaron? ¿En qué punto particular debo enfocar?

Por ejemplo, ¿debería presentarles algunas herramientas ( lint , …)?

Uso de la palabra clave const en el contexto de los punteros:

La diferencia entre las siguientes declaraciones:

  A) const char* pChar // pointer to a CONSTANT char B) char* const pChar // CONSTANT pointer to a char C) const char* const pChar // Both 

Así que con A:

 const char* pChar = 'M'; *pChar = 'S'; // error: you can't modify value pointed by pChar 

Y con B:

 char OneChar = 'M'; char AnotherChar = 'S'; char* const pChar = &OneChar; pChar = &AnotherChar; // error: you can't modify address of pChar 

Mis maestros pasaron tanto tiempo enseñándonos que los punteros son pequeños goobers que pueden causar muchos problemas si no se usan correctamente, que nunca se molestaron en mostrarnos lo poderosos que pueden ser.

Por ejemplo, el concepto de aritmética de punteros era extraño para mí hasta que ya había estado utilizando C ++ durante varios años:

Ejemplos:

  • c [0] es equivalente a * c
  • c [1] es equivalente a * (c + 1)
  • Iteración de bucle: para (char * c = str; * c! = ‘\ 0’; c ++)
  • y así…

En lugar de hacer que los alumnos tengan miedo de usar punteros, enséñeles cómo usarlos adecuadamente.

EDITAR: Como me llamó la atención un comentario que acabo de leer sobre una respuesta diferente, creo que también hay algo de valor en discutir las diferencias sutiles entre los punteros y las matrices (y cómo unirlas para facilitar algunas estructuras bastante complejas), así como la forma correcta de usar la palabra clave const con respecto a las declaraciones de puntero.

Realmente deberían aprender a usar herramientas de ayuda (es decir, cualquier otra cosa que no sea el comstackdor).

1) Valgrind es una excelente herramienta. Es increíblemente fácil de usar y rastrea las memory leaks y la corrupción de la memoria a la perfección.

Les ayudará a entender el modelo de memoria de C: qué es, qué puede hacer y qué no debe hacer.

2) GDB + Emacs con gdb-many-windows. O cualquier otro depurador integrado, de verdad.

Ayudará a aquellos que son perezosos a pasar por el código con lápiz y papel.


Realmente no restringido a C; Esto es lo que creo que deberían aprender:

1) Cómo escribir correctamente el código: Cómo escribir un código que no se puede mantener . Al leer eso, encontré al menos tres delitos de los que fui culpable.

En serio, escribimos código para otros progtwigdores . Por lo tanto, es más importante para nosotros escribir con claridad que escribir con inteligencia .

Usted dice que sus estudiantes no son en realidad progtwigdores (son ingenieros). Entonces, no deberían estar haciendo cosas difíciles, deberían enfocarse en una encoding clara .

2) STFW. Cuando comencé a progtwigr (comencé en Pascal, luego me mudé a C), lo hice leyendo libros. Pasé innumerables horas tratando de averiguar cómo hacer cosas.

Más tarde, descubrí que todo lo que había tenido que averiguar ya había sido hecho por muchos otros, y al menos uno de ellos lo había publicado en línea.

Tus estudiantes son ingenieros; No tienen tanto tiempo para dedicarse a la progtwigción . Entonces, el poco tiempo que tienen, deben pasar leyendo el código de otras personas y, tal vez, repasando los modismos .


Con todo, C es un lenguaje bastante fácil de aprender. Tendrán muchos más problemas para escribir algo más que unas pocas líneas que para aprender nociones independientes.

Cuando tuve que usar C como parte de un proyecto más grande en la escuela, fue la capacidad de usar gdb correctamente (es decir, en absoluto) lo que terminó por predecir quién terminaría su proyecto y quién no. Sí, si las cosas se vuelven locas y tienes un montón de errores relacionados con la memoria y el puntero, gdb mostrará información extraña, pero incluso sabiendo que puede apuntar a las personas en la dirección correcta.

También recordarles que C no es C ++, Java, C #, etc. es una buena idea. Esto ocurre con mayor frecuencia cuando ves a alguien tratando a un char * como una cadena en C ++.

Sin firmar vs firmado.

Operadores de cambio de bit

Enmascaramiento de bits

Ajuste de bits

tamaños enteros (8 bits, 16 bits, 32 bits)

Orientación a objetos:

 struct Class { size_t size; void * (* ctor) (void * self, va_list * app); // constructor method void * (* dtor) (void * self); // destructor method void (* draw) (const void * self); // draw method }; 

( Código fuente )

Portabilidad: rara vez se enseña o se menciona en la escuela, pero aparece mucho en el mundo real.

Los efectos secundarios (peligrosos) de las macros.

Usar valgrind

Las herramientas son importantes, así que recomiendo al menos mencionar algo sobre

  • Makefiles y cómo funciona el proceso de construcción
  • gdb
  • hilas
  • La utilidad de las advertencias del comstackdor.

Con respecto a C, creo que es importante enfatizar que el progtwigdor debe saber lo que realmente significa “comportamiento indefinido”, es decir, saber que podría haber un problema futuro incluso si parece funcionar con la combinación actual de comstackdor / plataforma.

Edit: Olvidé: enseñarles cómo buscar y hacer preguntas adecuadas sobre SO!

Utilice un estilo de encoding coherente y legible.

(Esto también debería ayudarlo a revisar su código).

Relacionados: No optimizar prematuramente. Perfil primero para ver dónde está el cuello de botella.

Sepa que cuando incrementa un puntero, la nueva dirección depende del tamaño de los datos apuntados por ese puntero … (IE, ¿cuál es la diferencia entre un carácter * incrementado y un largo sin signo *) …

Saber exactamente qué es realmente una falla de segmentación en primer lugar, y también cómo tratar con ellos.

Saber cómo usar GDB es genial. Saber cómo usar valgrind es genial.

Desarrollar un estilo de progtwigción en C … Por ejemplo, tiendo a escribir código bastante orientado a objetos cuando escribo grandes progtwigs en C (por lo general, todas las funciones en un archivo .C en particular aceptan algunas (1) estructuras en particular * y operan en él. .. Tiendo a tener foo * foo_create () y foo_destroy (foo *) ctor’s y dtors …) …

Entendiendo el enlazador. Cualquiera que use C debería entender por qué “static int x;” en el scope del archivo no crea una variable global. El ejercicio de escribir un progtwig simple en el que cada función está en su propia unidad de traducción y comstackr cada una por separado no se realiza con suficiente frecuencia en las primeras etapas del aprendizaje C.

Advertencias siempre activas. Con GCC, use al menos -Wall -Wextra -Wstrict-prototypes -Wwrite-strings .

I / O es difícil. scanf() es el mal. gets() nunca debe utilizarse.

Cuando imprime algo que no está '\n' terminado, debe vaciar la stdout si desea imprimirlo inmediatamente, por ejemplo

 printf("Type something: "); fflush(stdout); getchar(); 

Utilice los punteros const siempre que sea posible. Por ejemplo, void foo(const char* p); .

Utilice size_t para almacenar tamaños.

Las cuerdas litterales generalmente no pueden modificarse, así que haz que sean const . Por ejemplo, const char* p = "whatever"; .

  • La memoria en papel puede desencadenar todo tipo de errores extraños.
  • Los depuradores pueden mentirte.

Creo que la idea general parece realmente buena. Estas son algunas cosas extra.

  1. Un depurador es un buen amigo.
  2. Revisa los límites.
  3. Asegúrese de que el puntero esté apuntando a algo antes de usarlo.
  4. Gestión de la memoria.

Espero que esto no se haya publicado antes (solo lea muy rápido), pero creo que lo que es muy importante cuando tiene que trabajar con C, es conocer la representación de datos en la máquina. Por ejemplo: números de punto flotante IEEE 754, big vs little endian, alineación de estructuras (aquí: Windows vs Linux) … Para practicar esto, es muy útil hacer algunos rompecabezas de bits (resolver algunos problemas sin usar ninguna funcionalidad) luego, printf para imprimir el resultado, un número limitado de variables y algunos operadores lógicos). También a menudo es útil tener un conocimiento básico sobre cómo funciona un enlazador, cómo funciona todo el proceso de comstackción, etc. Pero sobre todo entender el enlazador (sin eso, es muy difícil encontrar algún tipo de error …)

El libro que más me ayudó a mejorar mis habilidades en C y C ++ fue: http://www.amazon.com/Computer-Systems-Programmers-Randal-Bryant/dp/013034074X

Creo que un profundo conocimiento de la architecture de la computadora hace la diferencia entre un progtwigdor de C bueno y uno malo (o al menos es un factor importante).

Enséñales la prueba de unidad.

¿Qué hay de las mejores prácticas generales?

  • Siempre asum que alguien más ya ha escrito su código y que está disponible gratuitamente en Internet y mejor escrito y probado que cualquier cosa que produzca antes de su fecha límite.
  • Regresar temprano / Evitar las cláusulas de lo contrario
  • Inicializar todas las variables
  • Una página por función como guía (es decir, utilice piezas de código más pequeñas juntas)
  • Cuándo usar switch, if-else if, o una tabla hash
  • Evitar variables globales.
  • Siempre revise sus entradas y sus salidas (no confío en mi propio código).
  • La mayoría de las funciones deberían devolver un estado

    [Para otros: siéntase libre de editar esto y agregarlo a la lista]

Con respecto a las entradas de control:

Una vez escribí un progtwig grande a toda prisa y escribí todo tipo de cláusulas de Guardia, verificaciones de entrada, en mis funciones. Cuando ejecuté el progtwig por primera vez, los errores de esas cláusulas se transmitieron tan rápido que ni siquiera pude leerlos, pero el progtwig no se bloqueó y se pudo cerrar limpiamente. Entonces era una simple cuestión de repasar la lista y corregir errores que fueron sorprendentemente rápidos.

Piense en las cláusulas de la Guardia como advertencias y compiler errors en tiempo de ejecución.

No creo que debas estar enseñando herramientas. Eso debería dejarse a los profesores de Java. Son útiles y ampliamente utilizados, pero no tienen nada que ver con C. Un depurador es todo lo que deberían tener acceso. Muchas veces todo lo que obtiene es printf y / o un LED parpadeante.

Enséñeles punteros pero enséñeles bien, diciéndoles que son una variable entera que representa una posición en la memoria (en la mayoría de los cursos también tienen algo de capacitación en ensamblaje, incluso si es para alguna máquina imaginaria, por lo que deberían poder entender eso) y no es una variable con un prefijo de asterisco que de alguna manera apunta a algo y que a veces se convierte en una matriz (C no es Java). Enséñeles que las matrices C son solo puntero + índice.

Pídales que escriban progtwigs que se desborden y segreguen con seguridad y después de eso, asegúrese de que entiendan por qué sucedió.

La biblioteca estándar también es C, pídales que lo usen y que sus progtwigs mueran dolorosamente en sus pruebas privadas debido a que han usado get () y strcpy () o algo de doble liberación.

Oblígalos a lidiar con variables de diferente tipo, endianness (tus pruebas podrían ejecutarse en un arco diferente), de flotación a conversión int. Haz que usen máscaras y operadores bit a bit.

es decir, enseñarles C.

Lo que obtuve en cambio fue un procesamiento por lotes en C que también podría haberse hecho en GW-BASIC.

Esta palabra clave en C: volatile

  1. Verifica los limites
  2. Compruebe los límites,

    y por supuesto,

  3. Revisa los límites.

Y si olvidó una de estas reglas, use Valgrind. Esto se aplica a matrices, cadenas y punteros, pero es realmente muy fácil olvidarse de lo que realmente está haciendo cuando hace asignaciones y aritmética de memoria.

  • Donde termina el lenguaje y comienza la implementación: por ejemplo, stdio.h es parte de la biblioteca estándar, conio.h no, cosas así;
  • La diferencia entre un comportamiento indefinido y definido por la implementación, y por qué cosas como x = x ++ no están definidas;
  • Solo porque comstack no significa que sea correcto;
  • La diferencia entre la prioridad y el orden de evaluación, y por qué a * b + c no garantiza que a se evaluará antes de b o c;
  • “Funciona en mi máquina” no supera el comportamiento especificado por el estándar de idioma: por ejemplo, solo porque void main () o x = x ++ le está dando los resultados que espera para una plataforma específica y el comstackdor no significa que esté bien de usar ;
  • Imagina que nunca has oído hablar de get ();

Dados sus antecedentes, quizás un buen enfoque en C para sistemas integrados, incluyendo:

  • Herramientas de análisis estático (por ejemplo, PC-Lint )
  • MISRA-C .
  • Exposición a múltiples procesadores (por ejemplo, PIC, STM32) y comstackdores
  • Cómo depurar.
  • Problemas en tiempo real, incluyendo interrupciones, señales de rebote, progtwigción simple / RTOS.
  • Diseño de software.

Y muy significativamente: software de control de versiones . Trabajo en la industria y lo uso religiosamente, pero estoy asombrado de que nunca se mencionó en el curso de mi carrera.

El depurador es tu amigo. C es un lenguaje fácil de desordenar y la mejor manera de entender sus errores es a menudo verlos en un depurador.

Sería beneficioso si los estudiantes estuvieran expuestos en algún momento a herramientas que les ayuden a escribir código mejor y más limpio. Es posible que no todas las herramientas sean relevantes para ellos en esta etapa, pero saber qué hay disponible ayuda.

  • Depuradores – gdb, totalview, …
  • Analizadores estáticos / dynamics – férula , valgrind , …
  • Marcos de pruebas unitarias – CUnit , cmockery , Check , …
  • Gestión de la documentación – Doxygen , …
  • Gestión de la construcción – Hacer, memorizar , fabricar , …
  • Métricas de código – CCCC , Understand , SLOCCount , …
  • Cobertura del código – gcov, LCOV, …

También se debe hacer hincapié en el uso de comstackdores diferentes (!) Con indicadores de advertencia de comstackdor estrictos y atender a todos y cada uno de los mensajes de advertencia.

Hay demasiados para nombrarlos a todos. Algunos de ellos son específicos de C; Algunos de ellos son el mejor tipo de cosas en general.

  • Aprende a utilizar las herramientas disponibles.
    • Sistema de control de revisiones. Cada vez que funcione, registralo.
    • Diferentes herramientas: diff, rdiff, meld, kdiff3, etc. Especialmente en conjunto con el RCS.
    • Opciones del comstackdor. -Wextra -Wall __attribute __ ((alineado (8))), cómo empaquetar estructuras.
    • Marca: Produce versiones de depuración y producción.
    • depurador: cómo obtener e interpretar un seguimiento de stack. Cómo establecer puntos de interrupción. Cómo pasar a través / sobre el código.
    • Editor: Comstackr dentro del editor. Abra varias ventanas, Mx tags-query-replace (¿se muestran mis raíces de emacs?) Etc.
    • cscope, kscope, [ce] tags u otras herramientas de navegación de origen
  • Progtwig defensivo. assert (foo! = NULL) en -DDEBUG; scrub entradas de usuario.
  • Detener y Catch Fire cuando se detecta un error. La depuración es más fácil cuando realiza un volcado de 2 líneas después de detectar el problema.
  • Mantenga una comstackción de 0 advertencias con -Wextra y -Wall habilitados.
  • No ponga todo en 1 enorme archivo .c de bocina.
  • Prueba. Prueba. Y prueba un poco más. Y revisa esas pruebas junto a tu fuente. Porque el instructor puede volver y cambiar los requisitos después de que se haya entregado una vez.

Repase todo el ciclo de vida de la progtwigción, incluido lo que sucede con su código una vez que haya terminado con él .

  • Etapas previas a la planificación, y un poco sobre cómo buscar un proyecto / código existente que pueda usar para reducir la cantidad de código original
  • Una pequeña descripción general (básica) de las licencias y cómo ese código externo afecta a las licencias que puede y no puede usar (y otras consideraciones relacionadas con la licencia)
  • Control de versiones concurrentes, y control de versiones. Haría SVN / Git, pero para cada uno lo suyo. Los ahorrará MUCHO tiempo si se los presenta ahora en lugar de aprender en el trabajo.
  • Muéstrales qué vías hay para el código de código abierto (Google Code, Github, etc.) y cuándo / cómo saber si es apropiado o no.

Nada de esto es específico de C, pero lo agrego porque personalmente, simplemente pasé por la “C para Ingenieros Eléctricos” en mi universidad, y esto es todo lo que tenía que descubrir por mi cuenta.

Una noción importante en C que no aprendí de mis maestros es:

Operador * no significa “puntero a” (en el lado izquierdo). En cambio, es el operador de referencia, exactamente como está en el lado derecho (sí, sé que es molesto para algunos).

Así:

 int *pInt 

significa que cuando no se hace referencia a pInt, se obtiene un int. Por lo tanto, pInt es un puntero a int. O dicho de otra manera: * pInt es un int – no referenciado pInt es un int; pInt debe ser un puntero a int (de lo contrario, no obtendríamos un int cuando se elimine la referencia).

Esto significa que no es necesario aprender de memoria declaraciones más complicadas:

 const char *pChar 

* pChar es de tipo const char. Por lo tanto pChar es un puntero a const char.


 char *const pChar 

* const pChar es de tipo char. Así, const pChar es un puntero a char (pChar en sí es constante).


const char * const pChar

* const pChar es de tipo const char. Así, const pChar es un puntero a const char (pChar es constante).

Estilo de sangría. Todos los maestros estaban diciendo que el código debe estar sangrado, pero nadie realmente dio instrucciones sobre cómo hacerlo. Recuerdo que el código de todos los estudiantes era realmente un desastre.