rrnum7
Usuario (México)

"Arreglos y punteros son lo mismo". "Internamente, los arreglos son punteros". Casi todos los que programamos en C y C++ nos hemos encontrado en más de una ocasión con alguna de estas afirmaciones. Lo cierto es que los arreglos y los punteros tienen varios puntos en común, pero no son lo mismo. En este post veremos algunas de sus diferencias, desde las más obvias hasta algunas un poco más sutiles, que suelen ser motivo de muchas confusiones. Cuando haga referencia al estándar, me estaré refieriendo tanto al de C como el de C++, pues coinciden en los puntos tratados en este post. He colocado algunas secciones marcadas como opcionales. Se trata de explicaciones complementarias que pueden ser de interés pero no son indispensables para entender el post. Aunque esto va dirigido a quienes ya tienen una buena base en estos temas, definiremos, de forma muy breve y a manera de recordatorio, estos tipos de datos. Los punteros son variables cuyo valor es una dirección de memoria. Así, si tenemos uno cuyo valor es 1234, decimos que apunta a la dirección 1234. Como cualquier variable, un puntero está almacenado en una dirección de la memoria, que además es distinta de aquella a la que apunta. En esta imagen tenemos un puntero localizado en la dirección 1000, que hace referencia, o apunta, a la dirección 1234: Cuando declaramos un puntero, únicamente se reserva memoria para él. Antes de poder usarlo, debemos asegurarnos de que apunte a alguna dirección correcta, ya sea reservándola de forma dinámica: // C int *ptr = malloc(numBytes); // C++ int *ptr = new int; o directamente haciendo que apunte a la dirección de alguna variable: int n; int *ptr = &n; Un arreglo es una variable que contiene uno o más elementos. La dirección del arreglo es la misma que la de su primer elemento. Veamos un arreglo de 4 int almacenado en la dirección 1000: Como vemos en la imagen, arreg es todo el arreglo completo, y su tamaño es el de todos sus elementos. A pesar de lo que a veces se lee, un arreglo no es un puntero disfrazado, ni nada por el estilo. Es simplemente eso, un arreglo, cuyo tipo viene determinado por la cantidad y tipo de elementos que contiene. Así, en la siguiente declaración: int arreg[4]; arreg es una variable cuyo tipo es arreglo de 4 int. Es importante comprender bien esto, pues aunque en el lenguaje cotidiano podamos describirlo simplemente como un arreglo de int, su tipo completo (de manera que el compilador lo entienda) incluye el tamaño. A diferencia de los punteros, cuando declaramos un arreglo, se reserva suficiente memoria para todos sus elementos y está listo para usarse. Tanto los arreglos como los punteros son lo que se conoce como tipos derivados, porque se forman a partir de otros tipos. De esta manera tenemos, por ejemplo, punteros a int, punteros a float, punteros a char, arreglos de 4 int, arreglos de 4 float, arreglos de 6 int, etc. Algunos manuales dicen que el nombre de un arreglo es un puntero a su primer elemento, lo cual no es del todo cierto. Lo que en realidad sucede es que, en casi todas las circunstancias, cuando escribimos una expresión de tipo arreglo, el compilador la convierte a una expresión de tipo puntero, que hace referencia a su primer elemento. Un ejemplo de expresión de tipo arreglo es el nombre del arreglo sin índices: arreg Lo anterior se convierte a algo así: &arreg[0] En algunos casos podemos usar notación de punteros con los arreglos y viceversa. Por ejemplo, en el siguiente código: int *ptr = malloc(10 * sizeof(int)); ptr apunta a un bloque de memoria que contiene 10 objetos int. Es válido hacer esto: ptr[4] = 15; La línea anterior modifica el int que se encuentra 4 posiciones más allá de la dirección a la que apunta ptr. También podemos usar notación de punteros con arreglos: int arreg[10]; *(arreg+4) = 15; es decir, arreg se convierte a puntero a la dirección a su primer elemento; luego, se le suman 4 posiciones; después, el operador * indica que deseamos acceder al valor de arreg+4 y no a su dirección; y finalmente lo modificamos. De hecho, siempre que accedemos a algún elemento específico de un arreglo, el compilador calcula su dirección de esa misma forma: dirección_del_arreglo + índice. En el caso de un arreglo bidimensional, la fórmula para encontrar la dirección de un elemento es: dirección_del_arreglo + (num_de_fila * cantidad_de_columnas + num_de_columna). Antes de seguir, vamos a dejar algo en claro: cuando se dice que el nombre del arreglo sin índices se convierte a puntero, nos referimos a un puntero "conceptual". Aclaro esto porque se podría pensar que el compilador añade punteros para cada uno de los arreglos que declaramos, lo cual no ocurre. Sería un desperdicio de memoria, ya que los punteros, como cualquier otra variable, ocupan espacio. Veremos a continuación lo que en realidad ocurre. Los arreglos no se pueden modificar Todo programador sabe que, aunque es posible modificar los elementos de un arreglo (siempre que no lo hayamos declarado como const), no podemos hacer lo mismo con el arreglo en sí. El estándar dice que, cuando una expresión tipo arreglo se convierte a expresión tipo puntero, la expresión resultante es un rvalue (rvalue puro en la terminología de C++), es decir, un valor no modificable y que no es ningún objeto (variable, función, etc.) del programa. Un ejemplo de rvalue es el número 5. De lo anterior se desprende lo que ya habíamos anticipado: tal vez sería más preciso decir que el nombre de un arreglo (o cualquier expresión que haga referencia a un arreglo) se convierte, no a un puntero, sino a la dirección de su primer elemento. Es por eso que no podemos modificarlos: int a1[5], a2[5]; a1 = a2; A la izquierda del signo igual, tenemos a1, que es una expresión de tipo "arreglo de 5 int". El compilador la convierte a la dirección a su primer elemento: &a1[0] = a2; Suponiendo que la dirección inicial del arreglo a1 sea 1000, quedaría algo parecido a esto: 1000 = a2; que obviamente no es válido, pues estaríamos intentando modificar un valor constante. De cualquier forma, considero adecuado seguir usando el término "puntero" al hablar de esta conversión. Simplemente, hay que tener presente que no es una variable puntero. ================================================================================ Opcional: A veces se dice que el nombre de un arreglo se convierte a un puntero constante. Esto también es correcto, siempre y cuando no olvidemos que no se trata de un puntero "real", pues los punteros const también ocupan espacio, además de que son lvalues (objetos del programa) no modificables, pero el estándar dice que debe ser un rvalue. La dirección de un arreglo (así como cualquier constante literal), en cambio, sí es un rvalue. Podríamos comprobar esto observando el código generado por un compilador. Veríamos que, en efecto, no se reserva más espacio que el ocupado por el arreglo, y las partes donde escribimos su nombre son simplemente reemplazadas por su dirección. La forma de ver y analizar el código objeto resultado de una compilación rebasa por mucho el objetivo de este post (por no hablar de que se necesitan conocimientos en lenguaje ensamblador), pero no quería dejar de mencionar que es posible verificar todo lo expuesto anteriormente. ================================================================================ ¿Qué hay de los arreglos multidimensionales? Todo lo visto anteriormente aplica también a los arreglos de más de una dimensión, pero tenemos que aclarar un par de detalles más. La conversión arreglo-puntero sólo se efectúa en la primera dimensión. Veamos un ejemplo: int matriz[5][4]; matriz es un arreglo de 5 arreglos de 4 enteros. Cuando escribamos su nombre sin índices, el compilador lo convertirá a un puntero a arreglo(s) de 4 enteros. La parte en negrita es la única que sufre la conversión. Es por eso que esto no compilaría: void func(int **mat); ... int matriz[5][4]; funcion(matriz); func espera un puntero a puntero a int. Lo que le estamos intentando pasar en la última línea es un arreglo de arreglos. Si la regla de conversión aplicara a todas las dimensiones, el compilador convertiría matriz en un puntero a puntero a int. Sabemos que no es así: el compilador sólo convierte la primera dimensión, así que matriz es convertido a puntero a arreglo de 4 int, que es un tipo diferente e incompatible. Nota: es posible que algunos compiladores acepten el código anterior, pero el programa fallará durante la ejecución. Para asegurarnos de haberlo comprendido, veremos un ejemplo más: int planos[5][4][9]; planos es un arreglo de 5 arreglos de 4 arreglos de 9 int. Cuando escribamos su nombre solo, se convertirá a un puntero a arreglos de 4 arreglos de 9 int. Paso de arreglos a funciones La regla de conversión también se cumple con los parámetros de una función. Tenemos tres formas de indicar que una función recibe un arreglo (supongamos que es de 5 elementos): void func(int a[]); void func(int a[5]); void func(int *a); En la primera declaración, el parámetro a es de tipo arreglo de int. No especificamos el tamaño (lo que se conoce como un tipo incompleto) pero en este caso es válido, ya que de cualquier forma se perderá, dado que el compilador lo convertirá a puntero a int. En la segunda forma, el tipo de a es arreglo de 5 int y también se convierte a puntero a int. La última línea ya es directamente un puntero, por lo que no hay conversión. Podemos ver que las tres formas son equivalentes: sin importar cómo declaremos la función, el compilador la convertirá a la tercera. Puesto que siempre se perderá el tamaño, es común pasarlo como un segundo parámetro void func(int *a, size_t tam); Nota: normalmente declararía el parámetro tam como int, pero en apego a las buenas prácticas, en este post usé size_t (definido en stddef.h, o cstddef en C++), que es el tipo recomendado para las variables que harán referencia a tamaños. En los siguientes ejemplos omitiré el parámetro del tamaño para simplificar el código. Si lo que queremos es una función que reciba un arreglo bidimensional (digamos, de 5x4), podemos declararla de cualquiera de estas formas: void func(int a[][4]); void func(int a[5][4]); void func(int (*a)[4]); Igual que en el caso anterior, las dos primeras formas serán convertidas a la tercera (puntero a arreglo de 4 enteros). Es importante el paréntesis, porque sin él estaríamos indicando un arreglo de punteros, que es algo totalmente diferente. La segunda dimensión no es opcional. Esto no es válido: void func(int a[][]); porque el compilador lo convertiría a esto (recordemos, la conversión se efectúa sólo en la primera dimensión): void func(int (*a)[]); y aquí sí nos encontraríamos con que a tiene un tipo incompleto, que el compilador no tiene forma de resolver. Con un arreglo de 3 dimensiones haríamos esto: void func(int a[][4][9]); void func(int a[5][4][9]); void func(int (*a)[4][9]); y obviamente, sucede lo mismo que en los casos anteriores. Resumiendo, la regla general para pasar arreglos a funciones es que únicamente la primera dimensión es opcional (y se pierde). Todas las demás son obligatorias. No está de más decir que las dimensiones que escribamos tienen que coincidir con las reales. No podemos hacer esto: void func(int a[][4]); ... int matriz[5][6]; func(matriz); Nuevamente, es posible que algún compilador no marque error, pero al ejecutar el programa vamos a tener problemas porque, cuando la función quiera acceder a los elementos de la matriz, sus cálculos serán erróneos. ================================================================================ Opcional: ¿"Expresiones" tipo arreglo/puntero? En la regla de conversión que puse al inicio de este post, se habla de que una expresión de tipo arreglo se convierte a expresión de tipo puntero. Esto es porque la conversión no sólo tiene efecto cuando escribimos el nombre de un arreglo sin índices, sino con cualquier expresión que tenga este tipo. Para entender esto, supongamos que tenemos esta variable: int matriz[5][4]; es decir, un arreglo de 5 arreglos de 4 int. Si en alguna parte de nuestro código escribimos esto: matriz[1] nos estamos refiriendo al elemento 1 de la variable matriz. Los elementos de matriz son de tipo arreglo de 4 int, por lo tanto, matriz[1] es una expresión de tipo arreglo y se convertirá en una de tipo puntero. Esto significa que el siguiente código es válido: void func(int *a); ... int matriz[5][4]; func(matriz[1]); porque la función espera un puntero a int, y le estamos pasando un arreglo de 4 int (específicamente, el segundo de los arreglos que contiene matriz), que es convertido justamente a puntero a int. ================================================================================ Cadenas de arreglos char y punteros a char Vamos a ver un ejemplo de dos declaraciones de variables que pudieran parecer equivalentes pero no lo son en absoluto: char arreg[] = "hola mundo"; char *ptr = "hola mundo"; En la primera se reserva espacio para 11 caracteres (los 10 de la cadena de inicialización más el caracter ''), que son inicializados con los valores 'h', 'o', 'l', etc. Como sucede con cualquier arreglo no const, podemos modificar sus elementos sin ningún problema. Por ejemplo, cambiar la 'h' a mayúscula: arreg[0] = 'H'; En el caso de ptr char *ptr = "hola mundo"; ocurre algo diferente. Se reserva espacio para un puntero. Luego, el compilador construye la cadena literal "hola mundo" y la coloca en algún lugar de la memoria, y finalmente hace que ptr apunte a ella. El estándar dice que si intentamos modificar el contenido de una cadena literal, el resultado queda indefinido (traducción: jamás intentemos hacerlo), así que es común que, por cuestiones de eficiencia, los compiladores coloquen las cadenas literales en una parte de la memoria marcada como de sólo lectura. Entonces, si intentamos hacer esto: ptr[0] = 'H'; el compilador lo permitirá, porque ptr no fue declarado como const; sin embargo, al ejecutar la instrucción anterior, muy probablemente se generará un error de violación de segmento (porque estamos tratando de modificar memoria de sólo lectura) y el programa se cerrará. Algunos compiladores permiten, ya sea de forma predeterminada o cambiando alguna configuración, modificar estas cadenas, pero eso queda fuera de las reglas del lenguaje y no se recomienda. De hecho, GCC era uno de esos compiladores, pero en las versiones recientes ya se eliminó la opción que hacía posible usar cadenas literales modificables. Por todo lo anterior, no debemos declarar una cadena de la forma en que lo hicimos en este último ejemplo. En su lugar, declararemos el puntero char como constante: const char *ptr = "hola mundo"; Si intentamos modificar los datos apuntados por ptr, el compilador detectará y marcará el error. Si lo que queremos es una cadena modificable, necesitamos usar un arreglo char, o bien, un puntero a char inicializado correctamente mediante malloc o new, o, mejor aún, usemos la clase string si estamos programando en C++. No siempre ocurre la conversión de arreglo a puntero Al principio se dijo que, aunque casi siempre una expresión tipo arreglo se convierte a puntero, hay excepciones. Las dos más importantes son, cuando al arreglo le aplicamos el operador de dirección &, y sizeof. Podemos hacer la siguiente prueba, que imprime la cantidad de bytes que ocupan un arreglo y un puntero: int arreg[20]; int *ptr; cout << sizeof arreg << endl; cout << sizeof ptr << endl; Si el nombre del arreglo fuera un puntero, deberíamos obtener el mismo número, pero no es así, ya que, como se dijo, cuando le aplicamos el operador sizeof no hay conversión. Veamos el otro caso: cout << arreg << endl; cout << &arreg << endl; Se imprime la misma dirección. En la primera línea, el nombre arreg se convierte a puntero a su primer elemento y se imprime su valor, es decir, la dirección del primer elemento. En la segunda línea no hay conversión, así que le estamos aplicando el operador & al arreglo en sí, por lo que se imprime la dirección del arreglo, que es la misma de su primer elemento. Si lo intentamos con el puntero cout << ptr << endl; cout << &ptr << endl; obtendremos direcciones distintas. En el primero caso se imprime el valor del puntero (la dirección a la que apunta). En el segundo, mostramos la dirección en la que está almacenado el propio puntero. ================================================================================ Opcional: Hay que tener cuidado al usar el operador &, como en el caso siguiente. Al usar la función scanf, hay que pasarle la dirección de las variables en las que queremos que almacene valores. Por eso normalmente usamos el operador &: int n; scanf("%d", &n); Cuando queremos leer una cadena, no es necesario usar este operador: char cadena[20]; scanf("%s", &cadena); porque, como ya sabemos, con tan sólo escribir el nombre del arreglo, éste es convertido a puntero. Aquí el & es redundante, pero no pasa nada, ya que le estamos enviando a scanf la dirección de cadena, que es la misma que la de su primer elemento. Si tratamos de hacer lo mismo con un char * char *cadena = malloc(20); scanf("%s", &cadena); el programa va a fallar. Lo que queremos es que scanf lea una cadena y la almacene en el bloque de memoria al que apunta cadena, pero le estamos pasando la dirección del puntero cadena, así que scanf almacenará los datos en el lugar equivocado, probablemente sobrescribiendo otras variables del programa. Para evitar confusiones (y de paso no ser redundantes) es mejor no usar el operador de dirección al leer cadenas o, dicho de forma más general, al pasar cadenas a funciones, pues este problema no es exclusivo de scanf. A los lectores observadores: sé que el scanf anterior es inseguro porque no especifica máximo de caracteres a leer, pero no compliquemos más este post. ================================================================================ Y con esto llegamos al final del post. Espero que les haya sido de utilidad. No duden en dejar comentarios con cualquier duda o sugerencia que tengan. Muy pronto: Números flotantes: todo lo que siempre quisiste saber y no te atrevías a preguntar . Mis otros post: Mitos sobre los 64 bits Lectura de cadenas en C
La forma correcta de leer cadenas es un tema que casi nunca se trata a profundidad en los libros de C, y menos aún en los cursos, así que no es raro que sea una de las cosas que más problemas ocasionan a quienes están aprendiendo este lenguaje. En este post vamos a ver cómo trabajan algunas de las principales funciones de entrada de C, así como sus pros y contras en relación a la lectura de cadenas. Al final se muestra una función de ejemplo que se puede utilizar para facilitarnos la programación. El post está enfocado a la lectura de cadenas desde el teclado. Algunos de los conceptos o ideas podrían variar para la lectura de datos desde archivos, pero en general, lo aquí descrito debería aplicar también para leer desde archivos de texto. Alternativas La primera función de lectura de datos que se enseña es scanf. Se puede usar para leer cadenas, salvo por un detalle conocido por todos los programadores en C: no acepta cadenas con espacios. La recomendación que la mayoría hace, es que se utilice gets, pero se trata de una función insegura. Muchos la consideran el defecto más grande del lenguaje C, al grado de que ya ha sido considerada obsoleta (deprecated), por lo que está próxima a desaparecer. El problema de esta función es que lee todo lo que el usuario introduzca y lo almacena en la cadena, sin verificar si hay espacio suficiente. Es decir, si tenemos esto: char nombre[30]; printf("Escribe tu nombre: "); gets(nombre); y el usuario introduce un nombre de 30 o más caracteres, gets almacena todo en la variable nombre. Como sólo le caben 30 caracteres (29 más el caracter nulo o de fin de cadena '\0'), los restantes sobrescribirán lo que sea que esté en las posiciones de memoria más allá del byte 30 de la variable nombre. Es lo que se conoce como un desbordamiento de buffer. A partir de aquí, pueden ocurrir varias cosas, desde la menos grave, esto es, que el programa termine su ejecución de forma inesperada, hasta tener bugs muy difíciles de encontrar (por ejemplo, si se sobrescribe una variable) además de que algún malware podría aprovechar esta vulnerabilidad para poner en riesgo la seguridad del sistema operativo. Es por lo tanto una función que se debe evitar. Aquí cabe decir que scanf en su forma típica tiene el mismo problema al usarlo con cadenas; si el usuario introduce un nombre que no tiene espacios, y es mayor a la capacidad de nuestra variable, habrá desbordamiento. ¿Cuál es la solución entonces? En realidad hay varias. Una forma de hacerlo es usando scanf con una serie de modificadores que sí permiten leer cadenas con espacios, incluso de forma segura, pero es algo complicada y propensa a errores por todo lo que hay que teclear. La función que casi siempre se recomienda es fgets, que es una buena opción si se sabe utilizar. Tiene el siguiente prototipo: char *fgets(char *s, int n, FILE *stream); Sus parámetros son: s - cadena donde se almacenarán los caracteres n - el tamaño de s stream - el archivo de donde se leerán los caracteres. Si se especifica stdin, se leerán de la entrada estándar (normalmente el teclado) Hay que recalcar que el parámetro n especifica la cantidad de caracteres a leer, más el caracter de fin de cadena o caracter nulo '\0'. Es decir, esta función lee un máximo de n-1 caracteres, o hasta encontrar un caracter de nueva línea (el Enter con el que se finaliza la entrada de datos), lo que ocurra primero. Y finalmente cierra la cadena, agregando un '\0' justo después del último caracter leído. En otras palabras, siempre tendremos una cadena perfectamente formada y sin desbordamientos de buffer. Vamos a ver un ejemplo de su uso. En este código, fgets lee un máximo de 29 caracteres de la entrada estándar: char nombre[30]; printf("Escribe tu nombre: "); fgets(nombre, 30, stdin); Como se ve, es muy fácil de usar, pero debemos tener en cuenta algunos detalles que casi nunca se mencionan cuando se recomienda esta función. Cuando leemos una cadena con fgets, puede darse cualquiera de estos casos: 1. El número de caracteres introducidos por el usuario es menor a n-1. Cuando esto suceda, la cadena incluirá al final el caracter de nueva línea (cosa que no pasaría con scanf o gets). Si al ejecutar el código de ejemplo anterior, introdujéramos el nombre "Jorge", la cadena quedaría así (omitiendo el caracter nulo): "Jorge\n" Cuando se imprima la variable, siempre se meterá una línea nueva al final. Esto signfica, por ejemplo, que no será posible, al menos de forma sencilla, imprimir en una misma línea el nombre y la edad de una persona. Que esto sea aceptable o no, dependerá del uso que se le quiera dar al programa. En cualquier caso, siempre se puede verificar si la cadena contiene ese caracter, y eliminarlo, poniendo en su lugar el caracter nulo. 2. El número de caracteres introducidos es exactamente n-1. La cadena no contendrá el caracter de nueva línea, el cual se quedará en el buffer de entrada (más sobre esto en la siguiente sección). 3. El número de caracteres introducidos es mayor a n-1. fgets leerá únicamente los primeros n-1 caracteres y los asignará a la cadena, dejando en el buffer todos los caracteres no leídos. Que fgets funcione de esta manera no es casualidad ni capricho. Es útil para saber si se leyó completo el valor introducido. Si después de llamar a esta función, la cadena contiene un caracter de nueva línea al final (caso 1) significa que se leyeron todos los caracteres introducidos por el usuario, así que el buffer de entrada está limpio. Si no hay caracter de nueva línea en la cadena, significa que quedó "basura" en el buffer. Como mínimo, el '\n' (caso 2), pero podrían ser más caracteres (caso 3). Limpieza de buffer Lo común en programas de consola o modo texto, es que para la entrada de datos por el teclado se use un buffer manejado por líneas. Esto significa que el programa no lee los caracteres directamente tal cual se van tecleando, sino que se guardan en un buffer o memoria intermedia, y hasta que se presiona Enter quedan disponibles para que el programa pueda leerlos mediante scanf, getchar, etc. Las funciones de lectura de cadenas no se llevan muy bien con funciones de entrada más generales como scanf, debido a que manejan de diferente forma la lectura del buffer. La función scanf funciona así: después de revisar el especificador de formato que le enviamos (%d, %f, etc.) lee y descarta todos los espacios en blanco que haya en el buffer de entrada* (esto incluye tabulaciones y caracteres de nueva línea) y a continuación lee los valores introducidos (o espera a que se introduzcan, si aún no se ha hecho) y los almacena. En cambio, gets y fgets no descartan nada, sino que leen lo que haya hasta que encuentran un caracter de nueva línea (o, en el caso de fgets, hasta leer el máximo de caracteres indicados). Y es ahí donde aparece el problema, porque scanf deja en el buffer de entrada el caracter de nueva línea, a diferencia de gets (y fgets, si se da el caso 1 de la sección anterior). *Si el especificador de formato tiene %c, %[, o %n, scanf no descartará los espacios iniciales. Vamos a ver un ejemplo de lo que ocurre cuando se utilizan estas funciones. Todo esto se explicará desde el punto de vista del programador y del programa, ya que internamente el sistema operativo puede realizar algunas tareas más, (por ejemplo, convertir el Enter a '\n') pero no son relevantes aquí. Si ejecutamos un programa con este código: int num1, num2; char nombre[30]; printf("Escribe un numero: "); scanf("%d", &num1); printf("Escribe otro numero: "); scanf("%d", &num2); printf("Escribe tu nombre: "); fgets(nombre, 30, stdin); el primer scanf analizará el especificador de formato que le mandamos, que en este ejemplo es %d, así que primero descartará cualquier espacio que haya en el buffer de entrada (en este caso, ninguno) y después intentará leer un valor entero desde el buffer de entrada. Como en este momento el buffer está vacío, la ejecución se detendrá, a la espera de que se introduzca un valor. Si escribimos 50 y presionamos Enter, en el buffer se tendrá algo así: 50\n en este momento, scanf leerá el '5' y el '0', que son caracteres válidos para un entero, y a continuación encontrará el '\n', que no es un caracter válido para un entero, así que terminará de leer, y asignará el valor 50 a num1, dejando el buffer así: \n A continuación se ejecutará el segundo scanf, que, de nuevo, verificará el especificador de formato (%d), por lo que descartará el '\n' que hay actualmente en el buffer, y a continuación esperará a que introduzcamos un valor. Si ahora escribimos 25 y presionamos Enter, el buffer quedará así: 25\n y scanf leerá el '2' y el '5', y se detendrá al encontrar el otro '\n'. Entonces asignará el valor 25 a num2, y el buffer quedará así: \n Hasta aquí todo funciona bien, ya que se ha utilizado únicamente scanf, pero la siguiente instrucción de entrada es fgets y es entonces donde aparece el problema. Puesto que fgets lee lo que haya en el buffer sin descartar nada, se encontrará directamente con un '\n' (justo el caracter que le indica que deje de leer), así que lo asignará a la cadena, y saltará a la siguiente instrucción, sin habernos dejado escribir nada. La solución pasa por limpiar el buffer de entrada después de un scanf, si la siguiente instrucción es una lectura de cadena con gets o fgets. Hay quienes sugieren utilizar para esto fflush(stdin), pero es incorrecto. Como el propio estándar del lenguaje C dice, fflush es una función para vaciar flujos de salida (stdin es de entrada, obviamente). Si lo usamos con flujos de entrada, el resultado queda indefinido. En Linux y sistemas tipo Unix no funciona. En Windows suele funcionar, pero no podemos contar con eso, porque realmente estamos usando la función de forma incorrecta. Imaginemos que mediante alguna extraña técnica pudiéramos usar printf para leer datos en vez de imprimirlos, ¿tendría justificación hacer semejante disparate sólo porque "a mí me funciona"? La manera recomendada de limpiar la entrada estándar es la siguiente: int c; while ((c = getchar()) != '\n' && c != EOF); este código es como un fflush(stdin) pero correcto y estándar. Lo que hace es leer un caracter hasta que encuentra un caracter de nueva línea o de fin de archivo (EOF). En realidad, cuando estamos leyendo desde la entrada estándar, no deberíamos encontrarnos nunca con un fin de archivo, así que se podría omitir esta parte, pero es preferible dejarlo tal cual, ya que es posible redirigir a stdin el contenido de un archivo, que obviamente sí tiene fin. El código anterior funciona en ambos casos. Función Nuestra función de ejemplo tiene el siguiente prototipo: int leecad(char *cad, int n); Acepta dos parámetros: la variable donde almacenaremos la cadena, y su tamaño (incluyendo el caracter nulo). Es decir, que la invocaríamos de esta forma: char nombre[30]; printf("Escribe un nombre: "); leecad(nombre, 30); Además, devuelve un valor de tipo entero, que servirá para verificar si hubo un error. Podemos separar su funcionamiento en tres partes: 1. Comprobación del buffer 2. La lectura en sí de la cadena 3. Limpieza de buffer Comprobación del buffer Puesto que queremos una función que se pueda usar de forma más o menos general, primero hay que verificar si el buffer está limpio, o si hay un '\n' dejado por scanf y, en ese caso, limpiarlo: int i, c; /* Comprobación */ c=getchar(); if (c == EOF) { cad[0] = '\0'; return 0; } if (c == '\n') i = 0; else { cad[0]=c; i = 1; } Tenemos dos variables: i, que es el típico contador usado en los ciclos for, y c, que es donde guardaremos los caracteres individuales que vayamos leyendo y después se irán agregando a la cadena cad. Empezamos leyendo sólo el primer caracter que haya en la entrada. Si es EOF, significa que no hay nada por leer, así que cerramos la cadena, dejándola "vacía" y salimos de la función retornando un valor de 0 ó falso, para indicar que hubo un error. Si el valor leído es '\n', significa que había un caracter de nueva línea dejado por un scanf o función similar. Simplemente inicializamos i a 0, para indicar que los siguientes caracteres que leamos los iremos asignando a partir del primer caracter de la cadena. Si no había un '\n', significa que el caracter que leímos es el primer caracter de la cadena introducida. En este caso, lo guardamos en la posición 0 de cad, e inicializamos i a 1, porque en este caso, como ya tenemos el primer caracter de la cadena, continuaremos agregando caracteres a partir del segundo. Lectura de la cadena Pasamos ahora a la lectura de la cadena: for (; i < n-1 && (c=getchar())!=EOF && c!='\n'; i++) cad = c; cad = '\0'; el for empieza con un ; porque estamos omitiendo la inicialización del contador, ya que fue inicializado en el punto anterior. Este código lee un caracter a la vez, lo agrega a cad, y se repite hasta que encuentre un fin de línea, fin de archivo, o haya leído la cantidad máxima de caracteres que se le indicó. Luego, cierra la cadena agregando un '\0' al final. Todo esto es muy similar a la forma en que los compiladores suelen implementar la función fgets, sólo que en lugar de getchar usan getc o fgetc. Limpieza del buffer Finalmente, limpiamos el buffer si es necesario: if (c != '\n' && c != EOF) while ((c = getchar()) != '\n' && c != EOF); la variable c contiene el último caracter leído. Recordemos que había 3 formas de salir del for: que hayamos encontrado un '\n', un EOF, o que hayamos llegado al máximo de caracteres que debemos leer. Si se da cualquiera de los dos primeros casos, significa que leímos todo lo que había en el buffer, por lo que no hay nada que limpiar. En el tercer caso, el usuario escribió más caracteres de los debidos, que aún están en el buffer, por lo que hay que quitarlos, para lo cual usamos el método que vimos poco más arriba. Juntándolo todo, tenemos la función: int leecad(char *cad, int n) { int i, c; c=getchar(); if (c == EOF) { cad = '\0'; return 0; } if (c == '\n') i = 0; else { cad[0] = c; i = 1; } for (; i < n-1 && (c=getchar())!=EOF && c!='\n'; i++) cad = c; cad = '\0'; if (c != '\n' && c != EOF) while ((c = getchar()) != '\n' && c != EOF); return 1; } Notas finales y sugerencias Aunque puesta aquí sólo a manera de ejemplo, la función está lista para ser usada tal cual en sus programas. Como usa sólo código estándar, debe funcionar en cualquier plataforma que tenga una implementación de C. Por simplicidad, funciona únicamente para leer de la entrada estándar, pero fácilmente se podría modificar para que trabaje también con archivos. Para eso habría que agregar un tercer argumento: FILE *stream, y reemplazar los getchar() por fgetc(stream). Finalmente, muchos programadores consideran que, si se quiere tener un programa lo más correcto y tolerante a fallas posible (en cuanto a lectura de datos), se deberían leer todas las variables (incluso de tipo int, float, etc.) en una cadena temporal, usando, por ejemplo, fgets (o incluso nuestra función de ejemplo) y después extraer de esta cadena los datos a leer, mediante sscanf, que funciona igual a scanf, pero en vez de leer los datos del teclado, los lee desde la cadena que le especifiquemos. Esto se deja como un ejercicio para quien quiera implementarlo.