Autor Tema: Consejos sobre buena programacion en C  (Leído 7610 veces)

0 Usuarios y 1 Visitante están viendo este tema.

Desconectado KILLERJC

  • Colaborador
  • DsPIC33
  • *****
  • Mensajes: 8242
Consejos sobre buena programacion en C
« en: 06 de Febrero de 2016, 19:42:20 »
Siempre vi cursos de programacion, pero nunca un post relacionado a las buenas practicas de la programacion, como casi muchas personas que se inician en el mismo suelen tener problemas y al momento de pedir ayuda te encontras con un codigo que es ilegible y uno debe estar identando, poniendo llaves, etc para poder llegar a encontrar aunque sea el error. Ademas estuve leyendo algunas que me parecieron interesantes para compartir.

Por eso mismo decidi crear este post, y si alguna alma caritativa desea aportar, sera bienvenido.

La lista va a estar un poco desordenada. Y tal ves muchos entremos en discucion sobre si es mejor o no. Y lo que se intenta bajo todo motivo es aumentar la legibilidad/portabilidad del codigo.

Vamos a comenzar por lo basico y que deberia enseñar cualquier tutorial de programacion C, y luego ir apuntando a casos mas particulares. Lo primero que vamos a ver es la estructura del programa, todos realizamos casi siempre los mismo.


Código: C
  1. // Comentarios sobre el archivo, dato, fecha, autor, funcion
  2.  
  3. #include  "xxx.h" // Aca todos los include necesarios
  4.  
  5. // FUSES opcionales!
  6.  
  7. // Declaracion de variables globales usadas en el main() o archivos externos
  8.  
  9. void main(void){
  10.  
  11.    //Configuracion de los modulos
  12.    // Setup inicial
  13.  
  14.    while(1)
  15.    {
  16.         // Programa principal
  17.    }
  18. }

Sobre esta estructura hay que hacer notar un par de cosas.

- FUSES opcionales: Es muy comun poner los fuses sobre el archivo de main y que simplifica las cosas por estar mas a "mano". Esto es simple cuando uno posee un PIC16F84A el cual casi no poseen muchos FUSES a pesar que un codigo bien comentado ocupa espacio. Ej:

Código: C
  1. #pragma config FOSC = INTOSCIO  // Oscillator Selection bits (INTOSC oscillator: I/O function on RA6/OSC2/CLKOUT pin, I/O function on RA7/OSC1/CLKIN)
  2. #pragma config WDTE = OFF       // Watchdog Timer Enable bit (WDT disabled)
  3. #pragma config PWRTE = OFF      // Power-up Timer Enable bit (PWRT disabled)
  4. #pragma config MCLRE = OFF      // RA5/MCLR/VPP Pin Function Select bit (RA5/MCLR/VPP pin function is digital input, MCLR internally tied to VDD)
  5. #pragma config BOREN = ON       // Brown-out Detect Enable bit (BOD enabled)
  6. #pragma config LVP = OFF        // Low-Voltage Programming Enable bit (RB4/PGM pin has digital I/O function, HV on MCLR must be used for programming)
  7. #pragma config CPD = OFF        // Data EE Memory Code Protection bit (Data memory code protection off)
  8. #pragma config CP = OFF         // Flash Program Memory Code Protection bit (Code protection off)

Cuando vamos a un PIC18 o un PIC16 mas actual y que poseen mas cosas, nos encontramos con casi 4 o 5 veces esa cantidad de FUSES, y que si lo colocamos en el main.c se vuelve poco entendible. La solucion es ponerlo en un archivo aparte, digamos "fuses.c", el cual contenga unicamente los fuses ( y el include a xc.h obviamente por las definiciones en caso de estar en XC8 ). Si este archivo esta incluido en el proyecto se va a compilar correctamente sin necesidad de agregar un include al main.c

De esta forma liberamos el espacio del archivo main.c y es mas simple de leer lo que nos interesa.

- Configuracion de los modulos y setup inicial:

En un programa simple es bastante comun poner las instrucciones directamente alli, pero a veces al tener muchos modulos este "setup" se vuelve demasiado extenso, y es mejor crear algunas funciones y ponerlas en otro archivo que cumpla con ese objetivo, si suponemos que debemos iniciar: SPI, PWM, Timer, UART. Algo que podria llevar varias lineas si se hace registro por registro. Nuestro main quedaria mejor asi:

Código: [Seleccionar]
InitPuertos();
 InitUART();
 InitSPI();
 InitPWM();
 InitTimer();

Entonces el lector de este archivo main.c no necesita conocer a ciencia cierta que esta ocurriendo sino que simplemente observa que es lo que se esta realizado con el nombre de las funciones.

- Y finalmente el Programa Principal (Loop infinito).

En este lugar me atreveria a decir que nunca deberiamos realizar una modificacion a la configuracion a un modulo. Lo unico que podriamos hacer si es activar un timer/ desactivar, etc pero rara ves deberiamos cambiar el baudrate de la UART por ejemplo. Asi que abstenerse a poner esas cosas aqui.

Siguiendo con las recomendaciones es la de intentar separar en funciones, las cuales sean "simples" pero que no crezcan en demasia. Como por ejemplo no tiene sentido para encender y apagar un led crear funciones para cada uno de esas.

Pero tampoco es buena idea tener un main de 300 lineas, y ademas funciones en el main.c, lo cual lleva a tener un archivo a 600 lineas y que al momento de leerlo se vuelve complejo.

Terminado lo de la estructura de programas como plato frio comenzamos con algunos consejos.

Consejos:

- Identa tu codigo.

Codigo tomado de un ejemplo real ( Perdon RSC, estas aprendiendo asi que no te hecho la culpa)

Código: C
  1. for(i2=0;i2!='\n';i2++)if(r1[i2]==',')
  2.       i3=i2+1; //printf("\r\n%d\r\n",i3);
  3. for (p1=i3;p1<10;p1++){pas2[p1-i3]=r1[p1];}

Gracias al formato del foro podemos notar que hay un printf comentado, y sin darnos cuenta podemos incluso llegar a omitir ese if(), y no solo eso sino que uno puede pensar equivocamente que el printf es parte del for o el if. Lo mejor es identarlo de esta forma quitamos de toda duda que pensamos hacer.

Código: C
  1. for(i2=0;i2!='\n';i2++)
  2. {
  3.         if(r1[i2]==',')
  4.         {
  5.                 i3=i2+1;
  6.         }
  7.         //printf("\r\n%d\r\n",i3);
  8.         for (p1=i3;p1<10;p1++)
  9.         {
  10.                 pas2[p1-i3]=r1[p1];
  11.         }
  12. }


- No escribas todo en una sola linea (a pesar que es parte de lo anterior, quiero hacer ver otra cosa):

Este es otro ejemplo de un codigo tomado del foro (No voy a decir el nombre por que el supone que se entiende asi, y yo no soy quien para decirle lo contrario)


Código: C
  1. if (peti==2) {//peti=2 detecto tarjeta
  2. //salir=acogetargeta();// return 21 = fallo F.C Acoge targeta 1 =cliente registrado 2=sinregistrar 11 = cliente saco la targeta si realizar operacion alguna
  3. if (salir<3 && MASTER){ // recoge(); } else {//while (Input(PIN_C0)==0) { } lcd_putc("\finicia espera"); tiempo(1); }  else {
  4. //registro();
  5. nu=salir;//(1,0,0,0)   1 o 2 normal
  6. borratotal();//acumula=0; total=0; lee=0; decimal=1;
  7. mensa(1);
  8. if (target[cliente]) {//TRUE cliente reconocido

Es interesante ver la cantidad de comentarios dentro de una misma linea, pero lo peor es terminar comentando llaves sin su par (observar el ultimo else comentado), es decir. si vamos a comentar un if hacemos esto:

Código: C
  1. MALLLLLL
  2.  
  3. // if (xxx) {
  4.    // Codigo del IF
  5. }
  6.  
  7. BIENNNN
  8.  
  9. /* Comentado todo el IF
  10. if (xxx) {
  11.    // Codigo del else
  12. }
  13. */


Es decir si queremos comentar una linea, simplemente le ponemos un //
Pero si queremos comentar ya un bloque no comentamos solo la primer llave { , sino que encerramos en un bloque que incluya ambas llaves {, }. Esto porque podriamos tener problemas sobre que puede cerrar esa otra llave y al descomentar tendriamos una llave de mas en nuestro programa, cambiando totalmente el funcionamiento del mismo. Esto es en parte por la identacion, Si uno identa el codigo apropiadamente no deberia tener estos problemas.

Ahora volvamos un poco a la legibilidad del programa. quisiera saber si alguno es capaz de entender exactamente lo que dice el codigo, donde se abren las llaves o se cierran, y veran que luego de un arduo trabajo encontraran como es. Pero por que volverlo un arduo trabajo cuando no lo es!. Asi que pensalo.

- Evitar usar demasiados while() y Delays

Muchas veces las personas abusan de esto. y uno se encuentra que cada funcion a la que llaman termina teniendo un while() dentro. O hay delays dentro del loop principal de 2 a 5 segundos, haciendo que el micro no responda por ese tiempo.
Ejemplos hay muchos no tengo ningun en particular, Pero es para que quede asentado aqui.

- Nombres de funciones y variables.

Suele suceder siempre en los programas que estamos muy tentados a crear variables con nombres cortos.

Código: C
  1. int tarje,rul,num,co,c,cot;

Para la gente comun eso. NO SIGNIFICA NADA. Lo hacemos por que es mas corto para escribir en nuestros programas, pero si vamos y volvemos dentro de un mes seguro que no vamos a entender absolutamente NADA del programa. Y si no me creen intenten como lector descifrar para que se usan esas varaibles. Al igual que ocurre con las funciones.

Código: [Seleccionar]
mensa(),ponme(),skri(),sajesal(),ponsea(),recogmateria(),protMASTER(),protSLAVE()
Nuevamente dejo al lector tratar de imaginar que significan esas funciones.

En la programacion no es una regla sobre COMO se debe nombrar una funcion/variable. Pero normalmente los primeros 8 caracteres definen que es esa funcion. Cada uno posee su propio formato. Ejemplo si tenemos un modulo de LCD y la funcion es un gotoxy de ese modulo. Muchos optan por un nombre asi:

Código: C
  1. LCDgotoxy(int,int);

El cual es correcto y da al lector que no tiene no sabe como es por dentro esa funcion la clara idea de que hace. Otros por ejemplo proceden a agregar una letra mas para indicar que devuelve la funcion. Supongamos estas funciones:

Código: C
  1. void vLCDgotoxy(int,int);
  2. int  iLCDRead(void);
  3. bool bLCDBusy(void);

Entonces el programador o el mismo lector ajeno puede conocer ademas mirando esa simple letra al comienzo que valor devuelve la funcion. Algunos usan Mayusculas para separar las palabras en la funcion otros guion bajo.

Código: C
  1. void LCD_gotoxy();
  2. void LCDGotoXY();

Eso ya termina siendo cuestion de cada uno. Pero lo importante es que cuando leamos un codigo sea realmente simple de entender. Para las variables aplica lo mismo sobre que sean relevante con el nombre

Código: C
  1. int PasoMotor1;
  2. int UidTarjeta;
  3. int NumeroPassword;
« Última modificación: 06 de Febrero de 2016, 20:30:01 por KILLERJC »

Desconectado KILLERJC

  • Colaborador
  • DsPIC33
  • *****
  • Mensajes: 8242
Re:Consejos sobre buena programacion en C
« Respuesta #1 en: 06 de Febrero de 2016, 19:47:54 »
- Nunca asumas que el tamaño de una estructura en C es igual a la suma de sus componentes.

Puede ser un error o tal ves no. Una de las ventajas de C es no tener que preocuparse por como estan guardados los datos en memoria, ni como es que son accedidos a la misma. A pesar que C garantiza que los datos van a estar seguidos, no significa que en algun momento ingrese un "padding" en la misma. O tampoco podemos saber si es que justo cambio de banco ( en el caso de los PIC16 mas viejitos ). Entonces ocurren cosas que parecen ilogicas.

Hace poco (desde escrito esto) ocurrieron 2 casos que involucraban estructuras. El primero fue la falta de optimizacion, y la segunda por que la estructura ocupaba mas espacio.

En este caso presento una version resumida de la estructura, por supuesto hay mas componentes

Código: C
  1. typedef struct {
  2.         uint8 start;
  3.         uint8 end;
  4. } buffer_circular

Ocurria que al hacer la siguiente linea:

Código: C
  1. dato = buffer->data[buffer->start++];

C calculaba el valor como la Base + offset, lo cual generaba mas instrucciones que la que el usuario queria. Entonces llego a la conclusion de crear un MACRO ( ya vamos a ver esto ) que haga lo siguiente:


Código: C
  1. (*((uint8*)dato)) = *(struct_pointer + DATA_OFFSET + (2* *(struct_pointer+START_OFFSET)));
  2. (*((uint8*)dato + 1)) = *(struct_pointer + DATA_OFFSET + (2* *(struct_pointer+START_OFFSET) + 1));

Lo interesante del macro era que creaba un codigo exactamente para esa situacion. Lo cual funciona, pero ahora pensemos un poco, esto es un codigo de una libreria para un buffer circular que por ser en C puedo aplicarlo en cualquier microcontrolador, GENIAL!

Pero este codigo puede fallar por multiples razones. Pero antes presento el segundo caso y tendremos la "moraleja" servida.
El segundo caso ocurrio algo similar. Un usuario que poseia una estructura y que le funcionaba en los micros de 8 bits, pero se fue a un micro de 16 bits y comenzaron los problemas. Su estructura estaba ocupando 2 bytes mas.

Código: C
  1. struct Struct1{
  2.    int8   var1;
  3.    int8   var2;
  4.    int16  Var3;
  5.    struct Struct2 mStr;
  6.    struct Struct2 mStr2;
  7.    int16  var4;
  8. };
  9.  
  10. struct Struct2{
  11.    int8 var10;
  12.    int8 var11;
  13.    int8 var12;
  14. };

Entonces que es lo que cambio ? Lo unico que cambio es los bits de sistema. Y C que decidio hacer la estructura de esta forma en memoria:

Código: [Seleccionar]
+-------+-------+
| var1  |  var2 |
+-------+-------+
|      var3     |
+-------+-------+
| var10 | var11 |  mStr
+-------+-------+
| var12 |       |
+-------+-------+
| var10 | var11 |  mStr2
+-------+-------+
| var12 |       |
+-------+-------+
|      nrec     |
+-------+-------+

Si ahora volvemos al primer caso, esto hubiera sido un desastre, por que la persona asumio que todos los bytes iban a estar seguidos. Que como dije para los 8 bits puede (no se a ciencia cierta) no llegar a ser problema, pero cuando se tiene un codigo que es una libreria se desea que sirva para todos.

- Usar funciones "inline" en ves de macros, o en caso de usar macros usar parentesis!.

Hay varias razones para hacer esto. Normalmente cuando uno posee una funcion muy pequeña y que se usa varias veces es preferible hacerlo asi para incrementar la velocidad del sistema. El por que no Macros y si la funcion es por que muchas veces los Macros si no se escriben correctamente pueden terminar ocasionando problemas.

Muchas veces creamos funciones en las Macros. Veamos una muy simple:

Código: C
  1. #define Cubo(X) (X * X * X)

Bien.. es nuestra funcion de un numero al cubo. Pero esta se comportara como realmente una funcion?

Si tenemos la siguiente linea:

Código: C
  1. a = 81 / Cubo(2+1);

Y uno pensaria que este codigo daria 81/27 = 3 , donde 27 es el cubo de 3. Pero esto es un error. Esto se traduce a :

Código: C
  1. a= 81 / (2+1 * 2+1 * 2+1);

Por la precedencia de la multiplicacion sobre la suma, obtenemos 81 / 7 = 11

No es lo que pretendiamos. La solucion a esto es:

Código: C
  1. #define Cubo(X) ((X) * (X) * (X))

De misma forma podriamos pensar que al tener los parentesis dentro podriamos quitar los de afuera. Y es otro error

Código: C
  1. #define Cubo(X) (X) * (X) * (X)
  2.  
  3. a = 81 / Cubo(2+1);

Uno esperaria nuevamente que esto funcion, ahora ya sabemos que 2+1 se van a sumar antes, pero NO. No funciona por que termina evaluandose asi:

Código: [Seleccionar]
((81 / 3) * 3 * 3)
Al tener el mismo orden de precedencia se divide y luego se multiplica, quedando en 243, Por eso mismo uno debe poner muchos parentesis ahi. Pero esto es la solucion ? NO.

Supongamos ahora este caso:

Código: C
  1. #define Cubo(X) ((X) * (X) * (X))
  2. i = 2;
  3. a= 81 / Cubo(++i);

Lo que uno normalmente en las funciones conoce es que primero se va a aumentar i y luego se pasa ese valor. Bueno en los Macros eso no ocurre. y terminarian con algo asi:

Código: C
  1. (++i) * (++i) * (++i)

Lo cual es totalmente indefinido el resultado que va a salir de ahi. tal ves no es lo mas simple de ver. Pero ocurre algo como esto:

Código: C
  1. b[2]={5,8};
  2. i=0;
  3. a = i + b[++i];

Uno podria penasr que si.. primero aumentaria i, y luego haria la suma, PERO tal ves el compilador comenzo a sumar y todavia no evaluo el array, entonces i no se incremento. Y de cosas asi no se pueden depender. Ya que no se sabe a ciencia cierta el resultado, ¿5 o 9?.

Otra de macros... Es posile pasarle cualquier cosa. Sin que el compilador nos diga que existe un error, por que son resueltas por el pre-procesador (antes que el compilador).

Código: C
  1. #define Cubo(X) ((X) * (X) * (X))
  2. char i = 'a';
  3. a= 81 / Cubo(i);

Para todo lo demas existen las funciones inline

Código: C
  1. inline int Cubo(int x){
  2.     return (x * x * x)
  3. }

Y con esto resolvimos nuestros problemas. Otro caso es de las macros con vaaaaarias instrucciones, es mejor crear directamente una funcion de estas. Esto no quiere decir que los macros sean malas, sino que uno debe saber elegir cual usar.

Ademas las funciones inline van a hacer que el compilador las introduzca en el codigo sin la sobrecarga de pasar las variables al stack como una funcion normal haria. Y lo que se busca desde una macro.

- Mas sobre MACROS

Aunque esta recomendacion, puede parecer absurda. En el sentido de que se de un error de esta naturaleza, pero lo eh visto por ahi.

Supongamos este codigo:

Código: C
  1. #define FIN_DE_DATOS -1
  2.  
  3. ...
  4. if ( getchUSART() FIN_DE_DATOS)
  5. {
  6.    //xxxx..
  7. }

Si por alguna razon alguien se olvidara de poner el condicional ( == , != , etc ) tal ves un error que no suele ocurrir normalmente, el compilador no levantaria ningun error ya que la expresion se evaluaria como:

Código: [Seleccionar]
getchUSART() - 1
Es un error y queremos que el compilador nos avise de esto, entonces tendriamos 2 soluciones:

Código: C
  1. #define FIN_DE_DATOS (-1)
  2. enum { FIN_DE_DATOS = -1 };

Aunque nuevametne el caso del #define puede no ser el correcto. Hay funciones realizadas con macro de la forma:

*Codigo sacado de internet:

Código: C
  1. #define GET_MACRO(_1,_2,_3,NAME,...) NAME
  2. #define FOO(...) GET_MACRO(__VA_ARGS__, FOO3, FOO2)(__VA_ARGS__)

En el cual se podria confundir el getchUSART() como una de estas al estar juntas y no poseer una separacion:

Código: C
  1. getchUSART()(-1)

Un poco cuestion de gustos y tener en cuenta esto.

- Poner siempre las llaves por mas que sea una linea , y poner las condiciones en los if.

Algo que yo hago muy a menudo ( soy sincero ) es lo siguiente ( el codigo para ambos casos):

Código: C
  1. char a[10];
  2. for ( i=0 ; i<10 ; i++ )
  3.         a[i] = i;
  4. //....
  5. if (!BusyUSART())
  6. {
  7.    // .....
  8. }

Es por ahi algo comun de ver. Como se ve esta identado, pero aun asi si en el afan o apresuro uno quisiera agregar una linea en el for podria llegar a cometer un error, Es una sola linea!. Pero aun asi seria una buena costumbre tener las llaves.
Por otra parte tenemos el if, en el cual para alguien que conoce el lenguaje sabra cuando es que va a entrar al if, pero si apuntamos a la legibilidad del codigo para que incluso alguien ajeno sepa rapidamente que estamos haciendo,entonces deberia ser asi:

Código: C
  1. char a[10];
  2. for ( i=0 ; i<10 ; i++ )
  3. {
  4.         a[i] = i;
  5. }
  6. //....
  7. if (BusyUSART() == 0)
  8. {
  9.    // .....
  10. }

Nuevamente por ahi con el afan de ganar un poco de tiempo hacemos las cosas mas rapido para ahorranos esos 3 caracteres de mas.

- Documentacion / Comentarios.

Esta creo que es la mas importante. Cuando es un codigo pequeño y siguiendo las consejos para que sea legible el codigo no necesita demasiados comentarios. Pero a medida que el codigo aumenta. Se vuelve imposible recordar cada una de las cosas que hicimos. Y tampoco es demasiado problema si es que el codigo lo toca una sola persona. Pero cuando ya varias estan involucradas y no conocen por dentro lo que hay en esas funciones se vuelve totalmente necesario.

De esta forma podemos ubicar comentarios en distintos lugares:

Al comienzo del archivo, indicando el archivo, al modulo que pertenece, fecha de creacion, explicacion, copyrigth y muchos etc mas.

Antes de cada funcion, explicando la funcion en si, cada parametro que es (entrada y retorno). Un ejemplo de uso de la funcion si es que se quiere. Que es lo que va a servir de apoyo a la persona que necesite usar la funcion sin entrar en detalles sobre esta (Como referencia a otras funciones). Tomando de codigo de ejemplo:

Código: C
  1. //*****************************************************************************
  2. //
  3. //! Registra un Handler para la interrupcion del ADC.
  4. //!
  5. //! \param ui32Base Es la base del modulo ADC.
  6. //! \param ui32SequenceNum Es el numero de secuencia de muestreo.
  7. //! \param pfnHandler Es un puntero a la funcion a ser llamada cuando ocurra la interrupcion
  8. //!
  9. //! This function sets the handler to be called when a sample sequence
  10. //! interrupt occurs.  This function enables the global interrupt in the
  11. //! interrupt controller; the sequence interrupt must be enabled with
  12. //! ADCIntEnable().  It is the interrupt handler's responsibility to clear the
  13. //! interrupt source via ADCIntClear().
  14. //!
  15. //! \sa IntRegister() for important information about registering interrupt
  16. //! handlers.
  17. //!
  18. //! \return None.
  19. //
  20. //*****************************************************************************
  21. void
  22. ADCIntRegister(uint32_t ui32Base, uint32_t ui32SequenceNum,void (*pfnHandler)(void))


Esto es ayuda tanto para la persona que lo va a usar, como la persona que esta trabajando con este codigo. Aun asi tambien deberiamos comentar dentro del codigo. Creo que la mas sana de las constumbres es comentar no es la misma linea, sino en bloques.

Código: C
  1. //
  2.     // Checkeo los argumentos que cumplan los requisitos
  3.     //
  4.     xxxxxxxxx;
  5.  
  6.     //
  7.     // Determino xxxx cosa.
  8.     //
  9.     xxxxxxx;
  10.  
  11.     //
  12.     // Registro la interrupcion
  13.     //
  14.     IntRegister(x, Y);
  15.  
  16.     //
  17.     // Habilito la interrupcion.
  18.     //
  19.     IntEnable(x);

Aunque hay casos por ejemplo en la inicializacion de variables. Es complejo ver esto:

Código: C
  1. int i=0,j=0,NumeroTarde=30,PleotaDesinflada=50;

Para los casos de variables inicializadas como estas, es mejor que cada una tenga su propia linea y su comentario

Código: C
  1. int i=0;                        /**< Variable auxiliar para el uso en loops */
  2.         int j=0;                        /**< Variable auxiliar para el uso en loops */
  3.         int NumeroTarde=30;             /**< Variable que mantiene el numero de tardes que pasaron */
  4.         int PelotaDesinflada=50;        /**< Variable para saber cuantas pelotas desinfladas hubo hasta el momento */

Esto es bueno para el caso de variables inicializadas , cuando no lo son asi. En el momento que lo usemos en el codigo ahi lo comentamos. Ej:

Código: C
  1. int i,j,NumeroTarde,PleotaDesinflada;
  2.  
  3. void xxxx(void){
  4.         i=0;                    /**< Variable auxiliar para el uso en loops */
  5.         j=0;                    /**< Variable auxiliar para el uso en loops */
  6.         NumeroTarde=30;         /**< Variable que mantiene el numero de tardes que pasaron */
  7.         PelotaDesinflada=50;    /**< Variable para saber cuantas pelotas desinfladas hubo hasta el momento */
  8.         ..... Mas codigo ....
  9. }

- Headers Guard, Para los archivos .h

Cuando creamos un .h por ahi solemos incorporarlo en varios lados. Esto puede traer problemas ya que estariamos definiendo en varios lados lo mismo. Es por eso que se suele usar una guarda para evitar que esto suceda.

Código: C
  1. #ifndef HEADER_H
  2. #define HEADER_H
  3.  
  4. /* ... Contenido de <header.h> ... */
  5.  
  6. #endif /* HEADER_H */

Una cosa a observar es el nombre que posee esta guarda. casi siempre usamos el mismo nombre que el archivo. Pero algunos hace uso de este nombre _HEADER_H ( con un guion bajo adelante ) eso SI que es una mala implementacion, por que C se reserva el uso de todos los macros/definiciones/etc con un guion bajo adelante ( en realidad es mas detallado referirse a C Standard, 7.1.3 )
El principal problema seria que nosotros incluimos una libreria de C y esta posee tal ves una cabecera con el mismo nombre que nuestro archivo por culpa de ese guien bajo. resultando que uno de los dos no se incluya y tengamos problemas. Por eso mismo no deberiamos crear estas definiciones/variables como reservadas ( con el guion bajo ).

- No acceder a una variable por un puntero que no es de su tipo

Y aca me incluyo nuevamente. Y recuerdo bien el tema que se dio aca en el foro al enviar un valor de un float ( 32 bits ) por UART. Entonces se podia hacer de 2 formas.

Creamos un puntero a float pero con el tipo char de esa forma enviabamos los 4 valores. por UART y luego lo juntabamos de la misma manera.. Hace poco encontre lo que en GCC le llaman strict-aliasing rule (por lo que tengo entendido es por la optimizacion) y muestro un ejemplo

Código: C
  1. void funcion(void) {
  2.   short a[2];
  3.   a[0]=0x11;
  4.   a[1]=0x11;
  5.  
  6.   *(int *)a = 0x2222;   // Creo un puntero de tamaño int ( 16 bits ) con la direccion de a. Y accedo a su contenido
  7.  
  8.   printf("%x %x\n", a[0], a[1]);
  9. }

Dependiendo de la optimizacion de GCC o de la arquitectura es posible que ese codigo no funcione como nosotros quisieramos. Segun si estaba activo ese strict-alising o no es posible obtener del printf 11 11 o 22 22 en la salida. Como esto depende del compilador/arquitectura/etc no se recomienda, tal ves era una verson vieja pero encontre que puede ocurrir, y lo correcto es usar una union:

Código: C
  1. void func(void) {
  2.   union {
  3.     short a[2];
  4.     int i;
  5.   } u;
  6.  
  7.   u.a[0]=0x11;
  8.   u.a[1]=0x11;
  9.   u.i = 0x2222;
  10.  
  11.   printf("%x %x\n", u.a[0], u.a[1]);
  12. }

El cual ahora si imprimiria el valor correcto. Sin posibilidad de problemas.

EDIT para expandir mas sobre esto:

Supuestamente existe una regla llamada "strict-aliasing rule" en el cual se "asume" que 2 punteros de distinto tipo no deben apuntar al mismo espacio de memoria. De esta forma el compilador (con optimizacion) puede llegar a pensar en esto. Y acomodar las instrucciones que le dimos de otra forma. Como en el ejemplo anterior tal ves ejecutar el printf primero antes de asignar el valor al otro puntero, ya que tenia esos valores (los del char) en los registros de trabajo, y no tenia sentido sacarlos y volverlos a pasar por ahi. Y esto es lo que causa este comportamiento. Esa opcion creo que actualmente viene activada y no deberia ocurrir, pero es bueno tenerlo en cuenta.
« Última modificación: 06 de Febrero de 2016, 20:38:24 por KILLERJC »

Desconectado elgarbe

  • Moderadores
  • PIC24H
  • *****
  • Mensajes: 2178
Re:Consejos sobre buena programacion en C
« Respuesta #2 en: 06 de Febrero de 2016, 20:55:44 »
Muy buena iniciatiba!!!!!

A mí lo que más me interesa de las buenas prácticas y nunca me senté a leer es como dividir los programas en varios .c/.h y dentro de eso usar variables locales y globales. El uso de extern para una variable, etc. Muchas veces me surje dudas de qué cosas van en el .h y que cosas no. Para proyectos grandes (me encuentro programando uno "mediano") es fundamental la división y no siempre lo tengo claro.

Saludos!
-
Leonardo Garberoglio

Desconectado Miquel_S

  • Colaborador
  • PIC24H
  • *****
  • Mensajes: 1251
Re:Consejos sobre buena programacion en C
« Respuesta #3 en: 06 de Febrero de 2016, 21:47:26 »
Gracias KILLERJC se agradece el trabajo.

Miquel Servera
Todos somos muy ignorantes. Lo que ocurre es que no todos ignoramos las mismas cosas.

Desconectado PalitroqueZ

  • Moderadores
  • DsPIC33
  • *****
  • Mensajes: 5490
    • Electrónica Didacta
Re:Consejos sobre buena programacion en C
« Respuesta #4 en: 06 de Febrero de 2016, 22:53:06 »
Excelente iniciativa KILLERJC

en cuanto a los PICs,  yo por lo general no soy tan rígido a la hora de echar código y dependiendo de la cantidad de líneas es que voy "ordenando" la programación, y de los comentarios, comento muy poco (a menos que sea para presentarlo a otros).

en cuanto a la organización, por ejemplo los nombres de las funciones y variables todas en minúsculas y que mejor describan su utilidad, para describir mejor las funciones y variables, le coloco una letra minúscula que haga referencia al tipo de datos que devuelva la función.

ej: int8 iMiFuncion();  void vMiFuncion(); const cCaracter, int8 iEntero, etc etc.

las llaves las comento con un

// inicio de..
// fin de..


porque es un dolor de cabeza cuando tienes más de 5 if con for y después te falta una llave o añadir una línea entre llaves y no sabes quién es de quien

uso bastantes banderas (flag), muchas banderas, arrays de definiciones de banderas, declaro un int8 y luego declaro cada bit como una bandera, yo lo veo como una forma de organizar el montón de banderas en la medida que van creciendo.

si la ocasión lo requiere, utilizo arreglos para hacer lecturas/escrituras de variables/puertos mediante barridos y/o punteros (es bastante práctico)


si el código se hace muy extenso, procuro exprimir los códigos que sean repetitivos y envolverlos en funciones, procuro usar variables GLOBALES y muy pocas locales.

las funciones van para un archivo, la declaraciones de las funciones para otro archivo, las interrupciones para otro archivo, las declaraciones de variables globales y constantes para otro archivo, al final tengo un montón de includes al inicio del archivo principal (al que suelo nombrar con el nombre del proyecto)

ah! y nunca hago eso de dibujar diagramas de flujo, no me agradan  :?

la verdad no sé si mi forma de escribir códigos para micros sea un insulto para los estándares que usan los profesionales, pero para lo que yo hago me basta.  :mrgreen:
La propiedad privada es la mayor garantía de libertad.
Friedrich August von Hayek

Desconectado KILLERJC

  • Colaborador
  • DsPIC33
  • *****
  • Mensajes: 8242
Re:Consejos sobre buena programacion en C
« Respuesta #5 en: 07 de Febrero de 2016, 01:41:03 »
las llaves las comento con un

// inicio de..
// fin de..


Eso esta muy bueno cuando tenes codigos en donde los if/for/etc se vuelven un poco grande y se pierde de vista la llave que abrio ese bloque, eso me parece una buena practica.
Lo de las funciones tambien y lo nombre al final del primer post.

uso bastantes banderas (flag), muchas banderas, arrays de definiciones de banderas, declaro un int8 y luego declaro cada bit como una bandera, yo lo veo como una forma de organizar el montón de banderas en la medida que van creciendo.

Es importante comentar bien las banderas como decis, y por supuesto que en algun momento llegamos a utilizar varias, aunque ya cuando el codigo se torna un poco mas grande creo que se mantiene un numero reducido de las mismas, ya que se empiezan a llamar a funciones que devuelven valores y dependiendo de estas se trabaja.

si el código se hace muy extenso, procuro exprimir los códigos que sean repetitivos y envolverlos en funciones, procuro usar variables GLOBALES y muy pocas locales.

A mi parecer, Esto puede y no puede ser bueno, me refiero a lo de las variables globales. Por que es memoria RAM perdida. Si vos limitas tus variables a ser locales, cuando se llama a la funcion y se mueve el puntero del stack, se reserva el espacio en el stack para estas variables, Algo que se hace de todos modos. Y luego cuando se sale de una funcion estas se destruyen dejando de ocupar RAM.

Entonces si tenes un codigo pequeño/mediano y no estas "limitado" por RAM entonces si.. dale para adelante, vas a perder tal ves un poco mas de tiempo en la compilacion por la cantidad de simbolos que hay. Pero que no va a ser algo que te mortifique la vida. Pero cuando tenes un sistema en el que la RAM es valiosa no podes permitirte tener ocupado 10 espacios de memoria para variables que solo sirven para contar en un for.. O punteros que no uses mas, etc etc etc.

Por eso digo que esto no es una "obligacion" y que deba hacerse ASI, sino que va a depender de cada uno.

las funciones van para un archivo, la declaraciones de las funciones para otro archivo, las interrupciones para otro archivo, las declaraciones de variables globales y constantes para otro archivo, al final tengo un montón de includes al inicio del archivo principal (al que suelo nombrar con el nombre del proyecto)

ah! y nunca hago eso de dibujar diagramas de flujo, no me agradan  :?

la verdad no sé si mi forma de escribir códigos para micros sea un insulto para los estándares que usan los profesionales, pero para lo que yo hago me basta.  :mrgreen:

Esta bien. Yo tambien separo las interrupciones para un lado, las funciones especificas para otro lado, tratando de crear un main() simple y entendible.

Respecto a lo de insulto de programar, yo con que cumplan lo que esta en el primer post me parece excelente. Y tampoco soy un profesional como para hablar de que se usa o que no.
Sino que busco organizarlo bien.

iniciatiVa!!!!!

A mí lo que más me interesa de las buenas prácticas y nunca me senté a leer es como dividir los programas en varios .c/.h y dentro de eso usar variables locales y globales. El uso de extern para una variable, etc. Muchas veces me surje dudas de qué cosas van en el .h y que cosas no. Para proyectos grandes (me encuentro programando uno "mediano") es fundamental la división y no siempre lo tengo claro.

Yo tampoco soy un experto en esto. Es mas hasta hace poco yo era de tener para cada archivo .c un .h, Pero mirando un poco sobre la encapsulacion de datos, me encontre con cosas interesantes en el que se suelen crear 2 headers uno "privado" es decir que va incluido en el .c de ese "modulo" y uno "publico" el cual tiene lo suficiente y necesario para que el usuario lo use, sin que se vea a simple vista detalles del mismo.
Voy a intentar crear un par de ejemplos, primero probarlos, y traerlos aca. Mostrando el uso que normalmente vi de ellos.
Solo pido paciencia por que a veces no da el tiempo :P

Desconectado Berto

  • PIC16
  • ***
  • Mensajes: 191
Re:Consejos sobre buena programacion en C
« Respuesta #6 en: 10 de Febrero de 2016, 19:08:49 »
Yo soy culpable al 100% en utilizar variables cortas pero no me castigo. My Gran excusa es esta :mrgreen:
ejemplo
///dentro de interrupciones
co,con,cont///contadores
v8,v16///int8 e int16  (de usar y tirar)
///dentro de main() y sus funciones
c0,c1,c2///contadores
va8///int8  (de usar y tirar) es decir para operaciones donde el flujo de datos variable es constante
               ///inporta solo el instante actual y no es necesario guardar.

Todo esto es un Standar no puedo olvidarme de lo que significan. Fallo muchas veces si, pero estoy muy seguro que no es por esto

Si creo que el while es malo abusar y no entiendo esto ya lo probe varias veces
do{   }while(!Input(PIN_C0)) Es todo un fracaso si lo que quiero es que sea instantaneo Siempre se retarda Y no lo entiendo ¿porque si dentro de do{ } no hay nada?

Respecto a esto:
Citar
si el código se hace muy extenso, procuro exprimir los códigos que sean repetitivos y envolverlos en funciones, procuro usar variables GLOBALES y muy pocas locales.

Segun un obsoleto compilador DIV 2 Las variables locales las recoge antes que las globales, ¿pasa lo mismo en CCS? Y aunque fuese a si merece hacer esto

void MiFuncion(unsigned int8 d, unsigned int8 a){

//Suponiendo que el bucle se repite de media 4 veces a=4. ¿Merece declarar estas locales una y otra vec, cuando esta //MiFuncion(); es continua y se ejecuta cada 5 por 6.? 
unsigned int8 contador
unsigned int8 dato8
unsigned int16 dato16
for(contador=d;a>contador;contador++){
.......
.......
}
}








Desconectado KILLERJC

  • Colaborador
  • DsPIC33
  • *****
  • Mensajes: 8242
Re:Consejos sobre buena programacion en C
« Respuesta #7 en: 10 de Febrero de 2016, 20:30:34 »
Citar
Si creo que el while es malo abusar y no entiendo esto ya lo probe varias veces
do{   }while(!Input(PIN_C0)) Es todo un fracaso si lo que quiero es que sea instantaneo Siempre se retarda Y no lo entiendo ¿porque si dentro de do{ } no hay nada?

Y es correcto.
Yo realmente reconozco que abusar de un while no es malo. Pero aca puse que SI es malo. mas que nada por que la mayoria de las personas, como aquellas que comienzan, no tienen en cuenta las consecuencias.

Con un while el programa puede quedar encerrado ahi para siempre. Si es que no se cumple la condiciones que lo saquen.
La diferencia entre un while, y un do..while, es que el while primero revisa la condicion y si se cumple ejecuta lo de adentro. Mientras que un do..while, primero ejecuta lo de adentro y luego revisa la condicion.

Código: C
  1. i = 0;
  2.  
  3. while (i!=0)
  4. {
  5.    i++;
  6. }
  7.  
  8. do
  9. {
  10.    i++;
  11. } while(i!=0);

En ese ejemplo. como i es igual a 0, el while primer reviza la condicion y ve que no se cumple entonces no ejecuta  el i++.
Pero cuando llega al do..while , primero se ejecuta i++ y luego revisa la condicion, quedando atrapado ahi hasta que i tenga un overflow y llegue a 0.

Citar
do{   }while(!Input(PIN_C0)) Es todo un fracaso si lo que quiero es que sea instantaneo Siempre se retarda Y no lo entiendo ¿porque si dentro de do{ } no hay nada?

Se retarda por que queda encerrado dentro del do..while al no activarse la tecla, y por ende te bloquea todo el programa.
El que el  {  } este vacio no implica que no va a hacer nada. el do while pensalo como 2 puntos en tu programa.


Código: [Seleccionar]
Hacer esto:

xxx
xxx

Reviso condicion, se cumple ir a "Hacer esto", sino continuo

Si pensas en tu programa de esa forma veras que por mas que entre ese "hacer esto" y revisar la condicion no exista nada, estarias volviendo a "hacer esto". Y es por eso que queda atrapado incluso sin tener nada.

Citar
Segun un obsoleto compilador DIV 2 Las variables locales las recoge antes que las globales, ¿pasa lo mismo en CCS? Y aunque fuese a si merece hacer esto

Esto es sobre C mas que nada.. C se maneja con un "stack", es decir es una porcion de memoria RAM en el que "crea" variables y las destruye, y tambien por ahi se pasan los valores.

Entonces cuando vos llamas a una funcion. el compilador reserva un pequeño espacio de RAM para los parametros y tambien un valor que indica el comienzo del anterior "frame" de la funcion que estaba. Cuando entra a la funcion y uno declara variables, el compilador reserva espacio en esa RAM para esas variables, pero cuando se sale de la funcion toma el puntero al "frame" guardado antes y usa eso como tope de pila ( Top of Stack ), es decir no borra la RAM sino que apunta a donde estaba antes de que se llamara a la otra funcion, dejando de importar las variables creadas en ese momento.
Ahora si son globales, son permanentes, y ahi si tenes algo como estuvieron muchos acostumbrados en el que por ejemplo una varaible ocupaba la memoria 0x20, la otra de 16 bits ocupa 0x21 y 0x22, y SIEMPRE van a estar alli.

En realidad es mas complejo ( lifetime, linkage, scope, visibility ) pero es para que te des una idea de cual es la diferencia, aunque esto es una imagen de un x86 es muy parecido a lo que verias en cualquier microcontrolador:

http://people.eecs.ku.edu/~kpoduval/UMOD/_images/StackFrameX86.png

Y aca si queres aprender mas de C ( en el cual vas a aprender mas sobre static,extern,volatile,etc):
https://msdn.microsoft.com/en-us/library/592xa3sk.aspx
« Última modificación: 10 de Febrero de 2016, 20:44:04 por KILLERJC »

Desconectado KILLERJC

  • Colaborador
  • DsPIC33
  • *****
  • Mensajes: 8242
Re:Consejos sobre buena programacion en C
« Respuesta #8 en: 10 de Febrero de 2016, 20:51:21 »
- Al declarar un puntero, poner el * sobre la variable y no el tipo.

Aunque creo que la mayoria lo hace bien. Algunos tienen la idea de que es mejor como a uno le paresca, pero si vamos a hablar de legibilidad y para que todos lo entiendan es mejor hacerlo de una manera clara. El claro ejemplo de esto es este:

int* p,q;

En el que no queda claro si q es un int* o un int a simple vista, a pesar que algunos conoscan lo suficiente para decir cual es cual.

Esto se soluciona haciendolo de esta manera:

int *p,q;

Como se puede ver claramente uno entiende que declara un puntero p del tipo int, y una variable q del tipo int.

Las 2 expresiones son equivalentes, y ambas hablan por si mismas sobre cual es mejor a la vista

- ¿Declarar el nombre de las variables en los prototipos o no ?.

Aqui tengo 2 posiciones encontradas y es respecto a mi persona, voy a presentar ambas y voy a dar mi opnion sobre cada una de ellas y voy a decir cual es la recomendada algunos institutos de ingenieria en soft.

Declararlas:

En C es posible crear prototipos de funciones de este estilo:

Código: C
  1. void LeerCaracteres(int ,int ,char* ,char* );

Lo cual es totalmente valido. Pero si una persona comienza a usar el header de alguna libreria y se encuentra con eso. Es imposible que la persona sepa de que se trata, tal ves imagine que el char* (uno de ellos) es el array de caracteres pero hasta ahi nomas. Esto se solucionaria si realmente ponemos los nombres de cada una

Código: C
  1. void LeerCaracteres(int start ,int end ,char* source ,char* dest );

Ahora podemos entender facilmente que pasamos un string y le damos valores iniciales y finales y nos devuelve el valor en otro array.

No declararlas:

Pero buceando el basto oceano de internet, me encuentro con que la recomendacion es... NO DECLARARLAS, es decir usar esto:

Código: C
  1. void LeerCaracteres(int ,int ,char* ,char* );

Lo cual se enfrenta a la posicion demostrada anteriormente, Y creo yo que es anti-intuitiva. Las unicas razones que creo que uno deberia hacerlo de esta forma es por que uno posee documentacion, me refiero a que en el header este explicado bien como usarlo. Y para disminuir la cantidad de nombres en el espacio que podrian provocar problemas, supongamos algo asi:

Código: C
  1. #define estado 30
  2. void EnviarStatus(int estado);

Esto trae aparejado que el prototipo se reemplaze por 

Código: C
  1. void EnviarStatus(int 30);
Lo cual vamos a tener un error pero por ejemplo aqui no:

Código: C
  1. #define estado []
  2. void EnviarStatus(int estado );     // quedaria void EnviarStatus(int []);

Como constumbre tengo que los define van en mayuscula al ser constantes y por lo tanto es muy raro que esto ocurra al menos desde mi punto de vista. Pero solo quiero traer lo que "deberia ser", aunque en esta, estoy en una posicion media entre uno y otro, pero valoro ambos, y pienso que si uno posee una documentacion deberia usar esta ultima.

- No creer que todas las variables en distintas plataformas son iguales. Tal ves entre 64/32 bits

Es raro que tengamos que crear esto.. pero supongamos que quisieramos poner todos los bits en uno de una variable int u otro tipo (Nombro estas por que la mayoria usa estos y para mi esta MAL ). Lo normal que uno haria es:

Código: C
  1. unsigned char i = 0xFF;
  2. unsigned int j = 0xFFFF;
  3. unsigned long k = 0xFFFFFFFF;

Esto va todo bien hasta que cambiamos de sistema en donde tal ves int / long no tengan esa representacion. Aunque esto es mas de procesadores, es bueno tenerlo en cuenta. en la cual tal ves el unsigned long no sea de 32 bits como queremos sino que sea de 64 y terminemos con: 0x00000000FFFFFFFF

Esto se puede remediar facilmente con algo simple:

Código: C
  1. unsigned char i = -1;
  2. unsigned int j = -1;
  3. unsigned long k = -1;         // En XC8  long y long long, son identicos
  4. unsigned long long k = -1;

Y esto nos va a permitir en cualquier tipo de representacion que sean todos 1 los bits.

O usar lo que viene definido en limits.h, va a haber un archivo de estos para cada micro y que si lo llevamos otro va a funcionar por igual ya que va a tenerlo definido para el suyo. ( En XC8 esta dentro de la carpeta include )

Código: C
  1. unsigned long = ULONG_MAX;

- Flotantes y sus limites de representacion.

Casi todos sabemos que NO se debe comparar los mismos en un "es igual a". O por ponerlo en un ejemplo:

Código: C
  1. float a = 3.0f;
  2. float b = 7.0f;
  3. float c = a / b;
  4.  
  5. if ( c == a/b )

En el que Diferencia lo unico que hace es tomar el mayor de esa forma siempre el resultado es positivo.
Lo que encontre dando vueltas por internet es que esto puede variar. Como por ejemplo con GCC y sin optimizacion puede que esto falle y con optimizacion el compilador sea tan inteligente de que al estar tan cerca, realmente deba entrar al if. Pero es depender si uno tiene ganas o no, entonces ¿Como podemos comprar si 2 flotantes son iguales? Lo que hariamos tal ves hariamos algo asi:

Código: C
  1. if ( abs(c - a/b) <= EPSILON )

Pero ¿que valor deberiamos poner a epsilon ? Para esto C trae en sus librerias las minima granularidad obtenida por la operacion de cada tipo, es decirn tenemos una para floats, una para double, y una para long double ( que nadie va a usar para microcontroladores xD )

Esto esta en float.h y son puras definiciones. Siemrpe uno pensara, y bueno copio y pego esa definicion a mi archivo. Eso esta MAL. Por que tambien depende de la plataforma que se use, Ejemplos:

dsPIC:

Código: C
  1. #define FLT_EPSILON     1.19209290e-07

PIC32:

Código: C
  1. #define FLT_EPSILON             3.05176e-05

PIC16/18:

Código: C
  1. #define FLT_EPSILON     3.05176e-05

Y mas que nada por supuesto, esto si estamos trantado de ver hasta el ultimo lugar que realmente sean iguales. Pero casi siempre nuestros float depende de variables externas y nunca vamos a tratar con igualdades, sino que con limites usando < ,> ,<= ,>= .

El tema de flotantes es LARGO, bastante LARGO.. especialmente en operaciones el cual uno pierde bits al hacerlo de una u otra forma

- Bajo todo motivo Desconfia de lo que viene del exterior.

Y puede que hable de pines tambien, pero mas que nada de las comunicaciones, la mayoria de los codigos que han estado por el foro carecen de algun tipo de proteccion. Tal es el caso que sucedio hace poco de este post con la funcion gets() de CCS, en la cual solo toma por argumento la referencia al array de salida, pero no limita en cantidad y la unica forma que pare es: recibiendo un \r. Entonces tenemos esto:

Código: C
  1. char buffer[6};
  2. char buffer2[3];
  3.  
  4. void func(void){
  5.     int valor;
  6.     gets(buffer);
  7.     valor = atoi(buffer);
  8.     //uso valor
  9. }

Y uno pensara que el programa de PC, creado por uno mismo, siempre va a enviar 5 caracteres ("1234\r"), Uno puede estar en lo correcto hasta que un momento la PC fallo, y se olvido de mandar la "\r" por un error o se colgo la PC y el gets() no paro de recibir. Haciendo que buffer le sea pequeño y seguir escribiendo la memoria, si suponemos que al lado de buffer, estaba buffer2. Este fallo sobreescribiria buffer2 sin querer. O si se cree que este error es muy improbable, tal ves le pase que su terminal en ves de enviar "\r" solo envie "\n\r"


- Usar los errores devuelto por las funciones.

Sigamos con el ejemplo anterior, Bueno entonces supongamos que esta ves me aseguro, modifico gets() o creo mi propia funcion que solamente reciba los caracteres que le digo y/o hasta que encuentre una "\r". pero esta ves un caracter fallo y en ves de recibir un 2, recibi una letra, algo asi "1S34\r".

Mi programa va a creer que recibio un 0, cuando no fue asi. atoi() me va a devolver 0 al no poder convertirlo en un int. Esto deberia tener un if que revise y actue acorde.
Tambien podria haber realizado a mi gets() modificado que me devuelva un error en caso de no cumplirse el limite


Código: C
  1. char buffer[6];
  2. char buffer2[3];
  3.  
  4. void func(void){
  5.  
  6.     int valor, temp;
  7.  
  8.     temp = getsModificado(buffer,5);
  9.     if(temp = 0)
  10.     {
  11.        // Error, se llego a 5 valores y no hubo una \r
  12.     }
  13.  
  14.     valor = atoi(buffer);
  15.     if ( valor == 0)
  16.     {
  17.         // Error, el valor devuelto no posee unicamente numeros.
  18.     }    
  19.     //uso valor
  20. }

- No especificar limites cuando se inicializa un string.

Cuando creamos un string y esta no va a ser cambiada lo mejor es no ponerle limites.

Código: C
  1. char texto[5] = "hola";

Sino que lo mejor es quitarle el limite y que el mismo compilador sea el encargado de elegir el tamaño justo.

Código: C
  1. char texto[] = "C se encargara de esto";

Obviamente esto es para estos casos.. Cuando se desea crear justamente un array de valores y no un string esto no se aplicaria:

Código: C
  1. char arrayvalores[3] = {'a','b','c'};

Desconectado elgarbe

  • Moderadores
  • PIC24H
  • *****
  • Mensajes: 2178
Re:Consejos sobre buena programacion en C
« Respuesta #9 en: 10 de Febrero de 2016, 21:43:57 »
- ¿Declarar el nombre de las variables en los prototipos o no ?.

Para mí es un SI declararlas rotundo. Creo que jamás dejaría de poner nombre en las variables.

- No creer que todas las variables en distintas plataformas son iguales. Tal ves entre 64/32 bits

Esto no se soluciona con los tipos genéricos uint8_t, int8_t, uint16_t, etc, etc?

- Flotantes y sus limites de representacion.

Casi todos sabemos que NO se debe comparar los mismos en un "es igual a". O por ponerlo en un ejemplo:

Código: C
  1. float a = 3.0f;
  2. float b = 7.0f;
  3. float c = a / b;
  4.  
  5. if ( c == a/b )

En el que Diferencia lo unico que hace es tomar el mayor de esa forma siempre el resultado es positivo.
Lo que encontre dando vueltas por internet es que esto puede variar. Como por ejemplo con GCC y sin optimizacion puede que esto falle y con optimizacion el compilador sea tan inteligente de que al estar tan cerca, realmente deba entrar al if. Pero es depender si uno tiene ganas o no, entonces ¿Como podemos comprar si 2 flotantes son iguales? Lo que hariamos tal ves hariamos algo asi:

Código: C
  1. if ( abs(c - a/b) <= EPSILON )

Pero ¿que valor deberiamos poner a epsilon ? Para esto C trae en sus librerias las minima granularidad obtenida por la operacion de cada tipo, es decirn tenemos una para floats, una para double, y una para long double ( que nadie va a usar para microcontroladores xD )

Esto esta en float.h y son puras definiciones. Siemrpe uno pensara, y bueno copio y pego esa definicion a mi archivo. Eso esta MAL. Por que tambien depende de la plataforma que se use, Ejemplos:

dsPIC:

Código: C
  1. #define FLT_EPSILON     1.19209290e-07

PIC32:

Código: C
  1. #define FLT_EPSILON             3.05176e-05

PIC16/18:

Código: C
  1. #define FLT_EPSILON     3.05176e-05

Y mas que nada por supuesto, esto si estamos trantado de ver hasta el ultimo lugar que realmente sean iguales. Pero casi siempre nuestros float depende de variables externas y nunca vamos a tratar con igualdades, sino que con limites usando < ,> ,<= ,>= .

El tema de flotantes es LARGO, bastante LARGO.. especialmente en operaciones el cual uno pierde bits al hacerlo de una u otra forma

Esto lo vi en un apunte de teoría de filtros, creo. No sé si era un moving average o alguno de esos, donde el error de representacion del float se hacía muy evidente despues de dejar al filtor funcionando unas horas... mostraban que a veces se obtiene mejor resultado con int's teniendo cuidad que con floats siendo descuidados... Si lo encuentro lo pongo como ejemplo.

- Bajo todo motivo Desconfia de lo que viene del exterior.
... tal ves le pase que su terminal en ves de enviar "\r" solo envie "\n\r"

grrrrr  :oops:

- Usar los errores devuelto por las funciones.

Sigamos con el ejemplo anterior, Bueno entonces supongamos que esta ves me aseguro, modifico gets() o creo mi propia funcion que solamente reciba los caracteres que le digo y/o hasta que encuentre una "\r". pero esta ves un caracter fallo y en ves de recibir un 2, recibi una letra, algo asi "1S34\r".

Mi programa va a creer que recibio un 0, cuando no fue asi. atoi() me va a devolver 0 al no poder convertirlo en un int. Esto deberia tener un if que revise y actue acorde.
Tambien podria haber realizado a mi gets() modificado que me devuelva un error en caso de no cumplirse el limite


Código: C
  1. char buffer[6];
  2. char buffer2[3];
  3.  
  4. void func(void){
  5.  
  6.     int valor, temp;
  7.  
  8.     temp = getsModificado(buffer,5);
  9.     if(temp = 0)
  10.     {
  11.        // Error, se llego a 5 valores y no hubo una \r
  12.     }
  13.  
  14.     valor = atoi(buffer);
  15.     if ( valor == 0)
  16.     {
  17.         // Error, el valor devuelto no posee unicamente numeros.
  18.     }    
  19.     //uso valor
  20. }

El tratamiento de errores es algo fundamental en sistemas que van a estar en uso en el mercado. Me encuentro tratando de aprender algo al respecto e ir implementando cuanto puedo en un sistema que estamos desarrollando.... Es un tema más que importante!

- No especificar limites cuando se inicializa un string.

Código: C
  1. char texto[] = "C se encargara de esto";


Esta es muy buena y lo desconocía!!!! Siempre tenía dudas sobre si el compilador iba a guardar el tamaño necesario o cuánto guardaría...

Saludos!
-
Leonardo Garberoglio

Desconectado elgarbe

  • Moderadores
  • PIC24H
  • *****
  • Mensajes: 2178
Re:Consejos sobre buena programacion en C
« Respuesta #10 en: 10 de Febrero de 2016, 21:50:14 »
- Flotantes y sus limites de representacion.

No sé como poner un link a este mail que me llegó de un grupo de discución que participo sobre UAVs:

Tom,

Thank you about the comments regarding decimal place and fixed-point math in MatrixPilot.

You have to be careful when you use floating point math and recursive algorithms. About a dozen years ago, the first of a new model of a variable speed drive that GE manufactured was causing trouble in the steel rolling mill in which the first installation was running. It would run fine for several weeks, and then it would go berserk and ruin a batch of steel. No one else could figure it out. I was eventually called in, and quickly figured out that the problem was in a recursive algorithm that was implemented in 64 bit floating point that should have been implemented with integers. As a result, there was an accumulation of round off error in the computation that built up exponentially. The author of the code thought that using 64 bits would prevent any problems, but it only delayed the inevitable. Also, the problem was undetectable until the last few seconds before the drive shit the bed.

It turned out that with integers and a modicum of thought, 16 bits were enough.

Most computations are fine with floating point, but some recursive algorithms (such as a recursive implementation of a moving average filter!!) will run into problems with floating point, because of accumulation of round off error.

When we started out with MatrixPilot, I was worried that we would run out of CPU power (we did not) so I pushed for integer arithmetic. Since then, we have used floating point in a few places, especially when we were too lazy to do it with integers.

Best regards,
Bill


saludos!
-
Leonardo Garberoglio

Desconectado KILLERJC

  • Colaborador
  • DsPIC33
  • *****
  • Mensajes: 8242
Re:Consejos sobre buena programacion en C
« Respuesta #11 en: 11 de Febrero de 2016, 00:15:42 »
Esto no se soluciona con los tipos genéricos uint8_t, int8_t, uint16_t, etc, etc?

Si, esto es en parte el standard de C ( y que la mayoria de los compiladores acepta hoy en dia ), y es por eso que ponia que muchos siguen usando int/long/short etc.

Ademas te dejo unos tipos mas para que leas.

uint_fast8_t
uint_least8_t

http://embeddedgurus.com/stack-overflow/2008/06/efficient-c-tips-1-choosing-the-correct-integer-size/


Esto lo vi en un apunte de teoría de filtros, creo. No sé si era un moving average o alguno de esos, donde el error de representacion del float se hacía muy evidente despues de dejar al filtor funcionando unas horas... mostraban que a veces se obtiene mejor resultado con int's teniendo cuidad que con floats siendo descuidados... Si lo encuentro lo pongo como ejemplo.

Exacto cuando uno usa flotantes pueden ocurrir bastantes cosas raras xD. En especial en casos redundantes en el que el error casi siempre es acumulativo.
Obviamente uno puede realizar un moving average de forma redundante y una de forma NO redundante.

Ademas con los flotantes , segun la IEEE754 hay valores que son "excepcionales" y que hay que comprobarlos, como por ejemplo NaN, o +inf, -inf.
Si se desea tener algo que tenga precision decimal lo mejor es trabajar con enteros y tal ves almacenarlos en BCD.

Y un ejemplo es el que mostraste con ese mail.





Creo que aqui finalizo el tema de los consejos..
Ya que no tengo mas que aportar, lo que si voy a hacer y tengo pendiente es lo siguiente:

- Contestar a elgarbe sobre los .h ( lo mas seguro que en este post )
- Un post sobre como leer punteros ( Aunque sea una copia de otra pagina, quedara aca en el foro y en español ). Ejemplo:

Código: C
  1. int * (* (*fp1) (int) ) [10];

fp1 es un puntero a una funcion que tiene un argumento int, que devuelve un puntero a un array de 10 punteros a int.
No se quien en su sano juicio haria eso pero... ahi lo tienen.
« Última modificación: 11 de Febrero de 2016, 00:18:16 por KILLERJC »

Desconectado Carl47D

  • PIC16
  • ***
  • Mensajes: 160
Re:Consejos sobre buena programacion en C
« Respuesta #12 en: 11 de Febrero de 2016, 02:22:53 »
Con respecto a un array de punteros a funciones, tenia este link que habla sobre el tema:

Replacing nested switches with multi-dimensional arrays of pointers to functions

E intente un programa, que aunque no esta en un microcontrolador la idea es la misma:
Código: C
  1. #include <stdio.h>
  2. #include <stdint.h>
  3.  
  4. void funcion1(void);
  5. void funcion2(void);
  6. void funcion3(void);
  7. void funcion4(void);
  8. void funcion5(void);
  9. void funcion6(void);
  10.  
  11. /* Funcion que usa el puntero a las funciones */
  12. void test(const uint8_t jump_index);
  13.  
  14. void main(){
  15.        
  16.         test(0);
  17.        
  18. }
  19.  
  20. void test(const uint8_t jump_index){
  21.         // Array de punteros a funciones que retornan void y no esperan parametros
  22.         static void (* const puntero[])(void) = {funcion1, funcion2, funcion3, funcion4, funcion5, funcion6};
  23.         //       ^                              ^                 ^                                             ^
  24.         //       |                              |                 |                                             |
  25.         //     Retorno                  Array   parametros                      Funciones
  26.         // static nos sirve para solo usar el array dentro de esta funcion = SEGURIDAD
  27.         // const sirve para indicarle al compilador que no va a cambiar
  28.        
  29.         printf("%d\n", sizeof(puntero)); // Obtengo 48
  30.         printf("%d\n", sizeof(*puntero)); // Obtengo 8
  31.  
  32.         // Esa division nos da 6, por lo tanto nos aseguramos que el index del array sea igual o menor a 5 */
  33.         if(jump_index < sizeof(puntero) / sizeof(*puntero)){
  34.                 puntero[jump_index](); 
  35.         }
  36.        
  37. }
  38.  
  39. void funcion1(void){
  40.         printf("Funcion 1.\n");
  41. }
  42.  
  43. void funcion2(void){
  44.         printf("Funcion 2.\n");
  45. }
  46.  
  47. void funcion3(void){
  48.         printf("Funcion 3.\n");
  49. }
  50.  
  51. void funcion4(void){
  52.         printf("Funcion 4.\n");
  53. }
  54.  
  55. void funcion5(void){
  56.         printf("Funcion 5.\n");
  57. }
  58.  
  59. void funcion6(void){
  60.         printf("Funcion 6.\n");
  61. }

La duda que tengo esta en :
Código: C
  1. printf("%d\n", sizeof(puntero)); // Obtengo 48
  2. printf("%d\n", sizeof(*puntero)); // Obtengo 8

Por que retorna esos valores?, busque por internet y no encontre respuesta, o no supe buscar xD

Saludos y re buenos consejos Killer

Desconectado KILLERJC

  • Colaborador
  • DsPIC33
  • *****
  • Mensajes: 8242
Re:Consejos sobre buena programacion en C
« Respuesta #13 en: 11 de Febrero de 2016, 02:43:45 »
Citar
Por que retorna esos valores?, busque por internet y no encontre respuesta, o no supe buscar xD

Esos valores depende y mucho de donde estes trabajando. Incluso vi que si esta en una estructura en C++ puede cambiar aun mas y ser mas "loco"

Creo que esta claro.. puntero es un array ... si haces *puntero, estarias haciendo lo mismo que puntero[0]

Por lo cual:
Código: C
  1. printf("%d\n", sizeof(*puntero)); // Obtengo 8

8 bytes.. una plataforma de 64 bits ? o es que tal ves sean 32 y tenga algo mas ?

Código: C
  1. printf("%d\n", sizeof(puntero)); // Obtengo 48

Son 6 funciones * 8 bytes cada una , son 48. Lo cual te indica que no hay padding, o que el padding esta incorporado en cada una de los punteros.

Por eso digo que depende mucho de donde lo estes probando. :P

Código: [Seleccionar]
static void (* const puntero[])(void)
                     puntero[] puntero es un array
               const constante
             * de punteros
                               (void) a funciones con parametros void
       void y que devuelven void

« Última modificación: 11 de Febrero de 2016, 02:51:36 por KILLERJC »

Desconectado Carl47D

  • PIC16
  • ***
  • Mensajes: 160
Re:Consejos sobre buena programacion en C
« Respuesta #14 en: 11 de Febrero de 2016, 02:55:53 »
Citar
8 bytes.. una plataforma de 64 bits ? o es que tal ves sean 32 y tenga algo mas ?

Cierto, estoy en una pc de 64 bits, solo estaba pensando en 32 bits y por eso no me cuadraba  :oops: , ahora ya tambien me cuadra la cuenta de cuando hago sizeof(puntero).

Gracias por la rápida respuesta  ((:-))


 

anything