Documentos de Académico
Documentos de Profesional
Documentos de Cultura
Apéndice 2
Introducción al lenguaje C.
Al inicio se efectúa un breve repaso del lenguaje. A continuación se expone con mayor detalle
los tipos básicos y su manipulación; conceptualizando en el diseño de macros y funciones. Más
adelante se profundiza en la interfaz de entrada salida, y en el diseño de rutinas matemáticas.
1. Funciones.
De este modo si el lenguaje no dispone de una determinada acción que se desee, se la puede
desarrollar como un grupo de las instrucciones que el lenguaje ya posee, e invocar la realización
de esta nueva acción por su nombre.
Luego de esto puede seguir construyéndose nuevas funciones empleando las ya definidas, en el
mismo sentido que ya Euclides desenvolvió para el desarrollo de la geometría a través de sus
Elementos.
Si bien el lenguaje C tiene acciones muy primitivas, tiene una biblioteca (en inglés: library) muy
amplia. Las acciones básicas deben ser simples ya que el objetivo del lenguaje es lograr una
compilación eficiente; es decir, que la traducción a assembler sea siempre posible y eficiente
con los repertorios clásicos de los procesadores.
El nombre de la función recuerda que imprime con formato; es decir con determinado patrón
que se establece mediante el string de formato, que es el primer argumento de la función. En el
ejemplo mueve hacia la salida todos los caracteres del string, hasta encontrar un %. Luego de
acuerdo a la letra, después de cada signo % determina cómo imprimir el resto de los
argumentos. En este caso imprime el valor del entero almacenado en x, en forma decimal (por la
letra d).
Lo que se necesita para emplear una función de la biblioteca es conocer sus argumentos y el tipo
del valor de retorno, y la función que realiza. Esta especificación se conoce como el prototipo
de la función.
La acción de usar la función, se conoce como invocación, y se hace de tal modo que los
argumentos actuales deben estar en correspondencia de número, tipo y orden de ocurrencia con
los parámetros o argumentos formales que tiene la definición de la función.
Tomemos un ejemplo del cálculo. Y calculemos la función: f(x) = 3 x2 +5
Entonces la definición, muestra: un argumento real y el tipo del valor de retorno, que también es
real.
Luego de definida la función, se la puede invocar; es decir, si la invocación está más adelante,
en el texto del programa, que su definición.
float w, y;
y = 4.2;
w = f(y -3.0) + 3.3*y ; /* invocación */
Nótese que el argumento actual debe ser una expresión que tome un valor de tipo float. Y que
el valor de tipo float, retornado por la función puede emplearse, también dentro de una
expresión.
Antes de invocar, se calcula el valor del argumento actual (en este caso: 4.2-3.0) y con este
valor se inicializa la variable x (argumento actual) en el espacio de memoria asociado a la
función. Dentro del cuerpo de acciones de la función se realizan los cálculos y mediante la
Si se desea invocar a la función con un argumento entero, debe especificarse una conversión
explícita de tipo, mediante un cast.
int j=0;
j = 5; w = f( (float) j*2 )
Si la invocación se realiza antes, en el texto del programa, que su definición, es preciso emplear
un prototipo de la función antes de invocarla. Nótese el punto y coma que termina el prototipo.
Cuando se desea diseñar una función que sólo sea una agrupación de acciones, se define su
retorno vacío (void). También si no tiene argumentos, debe especificarse void, en el lugar de
los argumentos.
void asterisco(void)
{ putchar('*'); }
Como esta función usa una función de biblioteca, antes de definirla se debe incluir <stdio.h>
que contiene el prototipo de putchar.
Para invocarla:
asterisco( );
Una función con retorno vacío es una abstracción de procedimiento o acción y no de expresión.
Por otro lado cuando la magnitud del problema es mayor, y deben resolverlo mediante un
equipo de programadores, la especificación de las funciones permite establecer la división del
trabajo. Todos deben conocer los prototipos.
Este modelo de programación considera las funciones como operaciones sobre los datos.
También es preciso que los miembros del equipo conozcan las estructuras de datos que
manipularán las funciones.
La exposición del lenguaje C sigue empleándose como una descripción abstracta de las
capacidades de un procesador.
Entonces, las funciones en C, tienen un diseño muy limitado. Sólo se le pueden pasar los
valores de los argumentos, y sólo se dispone de un valor de retorno.
Esto es muy limitado, ya que por ejemplo si el problema se puede modelar con vectores o
matrices, se requiere un vector o matriz de retorno. La función sólo puede retornar un valor de
un tipo determinado.
En ANSI C, se decidió permitir el retorno de una estructura. Lo cual permite retornar todos los
miembros de ésta.
Deseamos incrementar en uno dos variables, en una sola operación, que identifican contadores.
int cnt1=0, cnt2=0, cnt=0;
La función:
El diseño:
void cnt( int * c1, int *c2)
{ (*c1)++; (*c2)++}
Al invocar se pasan los valores de las direcciones de las variables; en el cuerpo de la función
mediante indirección se escribe en las variables. Se pasa una referencia a las variables.
1.6. Frame.
Otro concepto relevante en el uso de funciones es el espacio de memoria que ésta ocupa para
almacenar sus variables. Este espacio se denomina frame y logra mantener en direcciones
contiguas de memoria a las variables que puede emplear la función mientras se ejecuta su
código.
Lo que se desea es tener localidad espacial. Es decir que las instrucciones que ejecuta la
función estén en direcciones contiguas de memoria y también las variables que ésta emplee.
Esto permite el empleo eficiente de las memorias caché, actualmente presentes en todos los
diseños actuales de procesadores.
Otra consideración de importancia es que sólo es necesario disponer del frame mientras la
función esté en ejecución, lo cual permite reutilizar las celdas ocupadas.
Las variables locales, las definidas dentro del cuerpo de acciones de la función, se guardan en el
frame. Los argumentos también se guardan en el frame. Si almacenamos en el frame la
dirección de la instrucción a la que debe retornarse, luego de ejecutada la función, podremos
invocar a funciones dentro de una función.
Entonces, antes de llamar a una función se introducen en el frame, los valores de los argumentos
actuales en celdas contiguas de memoria; luego se introduce la dirección de retorno, y
finalmente se llama a la función. El código de la función crea el espacio para las variables
locales en el frame, en celdas contiguas. Antes de salir de la función, se retorna el valor; luego
se recupera la dirección de retorno y finalmente se desarma el frame.
Con esta disciplina el compilador lo único que requiere para traducir a código de máquina el
texto de una función es su prototipo. Ya que debe empujar dentro del frame los valores de los
argumentos, a su vez conoce ahora a los argumentos por los desplazamientos de éstos relativos
al stack pointer. Empleando mecanismos de direccionamiento indirecto o relativos a registros
puede leer y escribir en el espacio asignado a los argumentos. Lo mismo puede decirse de la
forma en que puede leer o escribir en las variables locales.
Sin embargo dotar a un lenguaje con la capacidad de invocar a funciones tiene un costo. Se
requieren varias instrucciones para crear el espacio, copiar los valores de los argumentos, iniciar
No es razonable crear funciones cuyo código sea menor o similar al número de instrucciones
requeridas para administrar el frame. En estos casos se emplean macros; los cuales permiten la
abstracción de llamar por un nombre, sin el costo del frame.
Los compiladores actuales para un procesador determinado tienen una política para el uso de los
registros; intentan pasar un número fijo de argumentos y retornar un número fijo de valores en
registros, también intentan emplear registros para las variables locales. Y sólo emplean el frame
para los argumentos que no puedan almacenarse en registros.
Más aún: clasifican los registros en temporales y salvables. Usan indiscriminadamente los
temporales, para desarrollar el cuerpo de la función, y los salvables los emplean para las
variables locales. Y sólo salvan en el stack los valores de los registros salvables que sean
modificados por la función.
Como en cualquier lenguaje existe un aspecto léxico (vocabulario, las palabras que se pueden
escribir); un aspecto sintáctico (reglas gramaticales, para escribir correctamente); y finalmente
un aspecto semántico (que asigna un significado a las construcciones).
Los tipos básicos son: enteros, reales, carácter y strings (secuencia de caracteres).
Enteros con signo.
En C, los enteros se representan en una palabra de la memoria, y sus valores dependen del
ancho de palabra que tenga la memoria (en la actualidad, 16 ó 32 bits)
Para obtener un número en complemento a uno, basta cambiar sus unos por ceros y sus ceros
por unos. Si tenemos: 010 (el decimal 2) su complemento uno es 101 (con equivalente decimal
menos dos).
Para 16 bits, en C-2, el rango de representación de enteros con signo es desde -(2^15) hasta
+(2^15 )-1
Lo cual equivale al intervalo desde –32.768 hasta +32.767
Definiciones de Datos.
a) Para declarar una variable entera, por ejemplo, la variable i como entera, se anota:
int i;
Enteros Largos.
Ocupan el doble de largo que un entero común.
La definición:
char ch = ‟a‟ ; /* define e inicia ch con el valor ASCII del símbolo a.*/
Strings.
Se los define como un arreglo de caracteres. La definición del arreglo, se indica con el valor
entero de las componentes entre paréntesis de tipo corchete.
1.7.2. Acciones.
La manera básica de organizar las acciones es hacerlo en una de las tres siguientes formas:
Secuencia.
Alternativa.
Si la condición es verdadera se realiza la acción1; si es falsa: la acción2.
Se anota:
if ( condición ) accion1; else acción2;
Repetición.
Mientras la condición sea verdadera se repite la acción.
Se anota:
while (condición ) acción;
Se ha comprobado que cualquier diagrama de flujo puede ser representado usando las tres
construcciones básicas anteriores. Una programación que emplee estos principios se denomina
estructurada. Permite leer un programa sin tener que volver hacia atrás.
For.
int i=10;
for (i=10; i>0 ; i--) putchar( ‟*‟);
putchar( ‟\n‟);
La construcción del for(inicio; condición; reinicio) ejecuta la expresión de inicio; luego evalúa
la condición, y si es verdadera(valor diferente de cero) ejecuta el bloque asociado; finalmente
evalúa la expresión de reinicio y vuelve a evaluar la condición y así sucesivamente hasta que
ésta es falsa(valor cero), situación en que termina el for.
Abstracción.
Una acción adicional es poder invocar a una función.
Como se verá una función permite agrupar a un número de instrucciones bajo un nombre.
Dotándola de una interfaz con el resto del programa, pasándole argumentos como valores de
entrada y tomando la función un valor de retorno.
Dado un problema, encontrar las funciones que permitan resolverlo es fundamental en
programación.
Si i es de tipo entero:
printf(“%d”,i); /*escribe el entero en decimal*/
printf(“%o”,i); /*escribe el entero en octal*/
printf(“%x”,i); /*escribe el entero en hexadecimal*/
printf(“%6d”,i); /*escribe el entero en decimal, con largo de campo igual a 6*/
printf(“abcd%defgh”, i); /*escribe string abcd, el entero en decimal, y el string efgh*/
2. Tipo char.
2.1. Valores.
Primero describiremos los valores que pueden tomar los elementos de tipo char.
Es un tipo básico del lenguaje. Las variables y constantes de tipo char ocupan un byte.
El tipo unsigned char tiene el rango 0 a 255.
El tipo char (o signed char, esto es por defecto) tiene el rango –128 a 127.
A las variables de tipo char se las puede tratar como si fueran de tipo entero, ya que son
convertidas automáticamente a ese tipo cuando aparecen en expresiones.
Una constante de tipo carácter se define como un carácter encerrado entre comillas simples. El
valor de una constante de tipo carácter es el valor numérico de ese carácter en la tabla o código
de caracteres. En la actualidad la tabla más empleada es el código ASCII.
ASCII son las iniciales de American Standard Code for Information Interchange.
Internamente, un carácter, se representa por una secuencia binaria de 8 bits. Un valor
perteneciente al código ASCII es la representación numérica de un carácter como '1' o '@' o de
una acción de control.
Alternativamente pueden emplearse dos cifras hexadecimales para representar un carácter, del
siguiente modo: '\xhh' La x indica que uno o los dos dígitos siguientes deben ser reemplazados
por una cifra hexadecimal (dígitos 0 a 9, y las letras A, B, C, D, F). La secuencia binaria de 8
unos seguidos, equivale a FF en hexadecimal.
Todos los valores de la tabla anterior son positivos, si se representan mediante un byte, ya que el
bit más significativo es cero.
Los caracteres que representan los dígitos decimales tienen valores asociados menores que las
letras; y si se les resta 0x30, los cuatro bits menos significativos representan a los dígitos
decimales en BCD (Binary Coded Decimal). Las letras mayúsculas tienen códigos crecientes en
orden alfabético, y son menores en 0x20 que las letras minúsculas.
En español suelen emplearse los siguientes caracteres, que se anteceden por su equivalente
decimal: 130 é, 144 É, 154 Ü, 160 á, 161 í, 162 ó, 163 ú, 164 ñ, 165 Ñ, 168 ¿, 173 ¡.
Los valores de éstos tienen el octavo bit (el más significativo en uno), y forman parte de los 128
caracteres que conforman un código ASCII extendido.
Los caracteres de control han sido designados por tres letras que son las primeras del significado
de la acción que tradicionalmente e históricamente se les ha asociado.
Por ejemplo el carácter FF (Form Feed) con valor 0x0c, se lo emplea para enviar a impresoras, y
que éstas lo interpreten con la acción de avanzar el papel hasta el inicio de una nueva página
(esto en impresoras que son alimentadas por formularios continuos).
Los teclados pueden generar caracteres de control (oprimiendo la tecla control y una letra). Por
ejemplo ctrl-S y ctrl-Q generan DC3 y DC1 (también son conocidos por X-on y Xoff), y han
sido usados para detener y reanudar largas salidas de texto por la pantalla de los terminales).
Varios de los caracteres se han usado en protocolos de comunicación, otros para controlar
modems.
Algunos de los caracteres, debido a su frecuente uso, tienen una representación por secuencias
de escape. Se escriben como dos caracteres, pero representan el valor de uno de control. Los
más usados son:
Dentro de un string, suelen emplearse las siguientes secuencias para representar los caracteres ",
', \. Que no podrían ser usados ya que delimitan strings o caracteres o son parte de la secuencia
de escape.
\\ para representar la diagonal invertida
\" para representar la comilla doble, dentro del string.
\' para representar la comilla simple dentro del string.
Ejemplo:
Char esc = '\\';
"O\'Higgins" en un string.
El siguiente texto,
se representa internamente
según:
45 6C 20 73 69 67 75 69 65 6E 74 65 20 74 65 78 74 6F 2C 20 0D 0A
73 65 20 72 65 70 72 65 73 65 6E 74 61 20 69 6E 74 65 72 6E 61 6D 65 6E 74 65 20 0D 0A
73 65 67 FA 6E 3A 0D 0A
La representación hexadecimal de los caracteres que forman el texto, muestra los dos caracteres
de control que representan el fin de línea (0x0D seguido de 0x0A). Cada carácter gráfico es
representado por su valor numérico hexadecimal. La primera representación (externa) se emplea
para desplegar la información en pantallas e impresoras; la segunda es una representación
interna (se suele decir binaria, pero representada en hexadecimal) y se emplea para almacenar
2.5. Expresiones.
Puede verificarse cómo son tratados los enteros con signo negativo por el compilador que se
está empleando, observando los resultados de: printf(" %c\n",-23); Debe producir la letra
acentuada: é.
Un compilador moderno también debería imprimir las letras acentuadas, por ejemplo:
printf(" %c\n",'é');
El siguiente par de for anidados muestra 8 renglones de 16 caracteres cada uno, con los
caracteres que tienen valores negativos (el bit más significativo del byte es uno).
Observando la salida, la que dependerá del compilador empleado, pueden comprobarse los
caracteres (con valores negativos) que serán representados gráficamente.
Cuando se desea tratar el contenido de un byte (independiente del valor gráfico) debe emplearse
unsigned char.
Es preferible escribir: i = (int) (c – '0'); que destaca que se está efectuando un conversión de
tipos. Pero lo anterior no suele encontrarse en textos escritos por programadores
experimentados.
La expresión ('a' - 'A') toma valor (97 – 65) = 32. Valor que expresado en binario es:
00100000 y en hexadecimal 0x20.
Si con esta máscara se efectúa un or con una variable c de tipo carácter: c | ('a' - 'A')
la expresión resultante queda con el bit en la quinta posición en uno(esto conviniendo, como es
usual, que el bit menos significativo ocupa la posición cero, el más derechista).
La expresión: c |= ('a' - 'A') si c es una letra la convierte en letra minúscula.
La expresión: c & ~ ('a' - 'A') forma un and con la máscara binaria 11011111 y la expresión
resultante deja un cero en el bit en la quinta posición.
La palabra máscara recuerda algo que se pone delante del rostro, y en este caso es una buena
imagen de la operación que realiza. Nótese que con un or con una máscara pueden setearse
determinadas posiciones con unos; y con un and, con una máscara, pueden dejarse determinados
bits en cero.
Las expresiones anteriores pueden emplearse para convertir letras minúsculas a mayúsculas y
viceversa.
La condición:
(c != ' ' && c != '\t' && c!= '\n')
es verdadera(toma valor 1) si c no es un separador.
2.6. Entrada-Salida
Convierte c a unsigned char y lo escribe en la salida estándar (stdout); retorna el carácter escrito,
o EOF, en caso de error.
Lee desde la entrada estándar el siguiente carácter como unsigned char y lo retorna convertido a
entero; si encuentra el fin de la entrada o un error retorna EOF.
El valor de la constante EOF predefinida en <stdio.h> es un valor entero, distinto a los valores
de los caracteres que pueden desplegarse en la salida, suele ser –1. EOF recuerda a end of file
(debió ser fin del stream o flujo de caracteres). Por esta razón getchar, retorna entero, ya que
también debe detectar el EOF.
obtiene un carácter y lo asigna a c (la asignación es una expresión que toma el valor del lado
izquierdo); este valor es comparado con el EOF, si son diferentes, la condición toma valor 1,
que se interpreta como valor verdadero. Los paréntesis son obligatorios debido a que la
precedencia de != es mayor que la del operador =.
Dentro del string de control del printf, una especificación de conversión se inicia con el carácter
% y termina con un carácter. El carácter c indica que una variable de tipo int o char se imprimirá
como un carácter. Nótese que los caracteres que están antes y después de la especificación de
conversión se imprimen de acuerdo a su significado.
Los siguientes valores del argumento actual son imprimibles (isgraph retorna verdadero).
print_char(0x40); imprime en una línea: '@'
print_char(65); imprime en una línea: 'A'
Los siguientes valores del argumento actual se imprimen como tres cifras octales.
print_char(06); imprime en una línea: '\006'
print_char(0x15); imprime en una línea: '\025'
2.7. Funciones.
char tolower(int c)
{
if((char)c <= 'Z' && (char)c >= 'A') c |= ('a' - 'A');
return (char)c;
}
Primero el signo, luego la cifra más significativa. Lo logra reinvocando a la función con un
argumento que trunca, mediante división entera la última cifra. De este modo la primera función
(de las múltiples encarnaciones) que termina, es la que tiene como argumento n a la cifra más
significativa, que es menor que 10, imprimiendo dicho valor, a través de la conversión de entero
a carácter. Es necesario tomar módulo 10, para imprimir las cifras siguientes.
void printd(int n)
{ if (n<0) putchar('-') n= -n;
if(n/10) printd(n/10);
putchar(n % 10 + '0');
}
La función prtstr imprime un string. Igual resultado se logra con printf("%s", s).
void prtstr(char *s)
{ while(*s) putchar(*s++); }
2.8. Macros.
Se escriben:
#define <token> <string>
Todas las ocurrencias del identificador <token> en el texto fuente serán reemplazadas por el
texto definido por <string>. Nótese que no hay signo igual, y que no se termina con punto y
coma.
También las emplea el sistema para representar convenientemente y en forma estándar algunos
valores.
Por ejemplo en limits.h figuran entre otras definiciones, las siguientes:
#define CHAR_BIT 8
#define SCHAR_MAX 127
#define SCHAR_MIN (-128)
#define UCHAR_MAX 255
Si se desean emplear constantes predefinidas por el sistema, debe conocerse en cual de los
archivos del directorio include están definidas. Y antes de que sean usadas debe indicarse en una
línea la orden de inclusión, para que el preprocesador incorpore el texto completo de ese archivo
en el texto fuente previo al proceso de compilación.
Por ejemplo:
#include <ctype.h>
Los paréntesis de ángulo indican que el archivo está ubicado en el subdirectorio include. Si se
desea tener archivos definidos por el usuario, el nombre del archivo que debe incluirse debe
estar encerrado por comillas dobles.
Se suelen emplear para definir macroinstrucciones (de eso deriva su nombre), es decir una
expresión en base a las acciones primitivas previamente definidas por el lenguaje. La macro se
diferencia de una función en que no incurre en el costo de invocar a una función (crear un frame
con espacio para los argumentos y variables locales en el stack, salvar registros y la dirección de
retorno; y luego recuperar el valor de los registros salvados, desarmar el frame y seguir la
ejecución).
Su real efectividad está limitada a situaciones en las que el código assembler que genera es
pequeño en comparación con el costo de la administración de la función equivalente. O también
cuando se requiere mayor velocidad de ejecución, no importando el tamaño del programa
ejecutable.
Los identificadores arg1, ... son tratados como parámetros del macro. Todas las instancias de los
argumentos son reemplazadas por el texto definido para arg1,.. cuando se invoca a la macro,
mediante su nombre. Los argumentos se separan por comas.
Por ejemplo:
#define ISLOWER(c) ('a' <= (c) )&& (c) <= 'z')
#define TOUPPER(c) (ISLOWER(c) ? 'A' + ((c) - 'a') : (c))
Si en el texto fuente aparece ISLOWER( 'A') dentro de una expresión, antes de la compilación
el preprocesador cambia el texto anterior por: ('a' <= ('A') && ('A') <= 'z'). El objetivo de esta
macro es devolver un valor verdadero (valor numérico 1) si el carácter c tiene un valor numérico
entre los valores del código asociados al carácter 'a' y al carácter 'z'; en caso contrario, la
expresión lógica toma valor falso (valor numérico 0).
La definición debe estar contenida en una línea. En caso de que el string sea más largo, se
emplea el carácter \ al final de la línea.
#define ctrl(c) ((c) \
< ' ') /* string continua desde línea anterior */
Lo cual es equivalente a:
El siguiente macro sólo puede aplicarse si se está seguro que el argumento representa un
carácter que es una letra. Entre 0x41 y 0x51. También da resultado correcto si la letra es
minúscula (entre 0x60 y 0x7a).
Nótese que en el string que define el texto que reemplaza al macro, los argumentos se colocan
entre paréntesis.
Prototipos en include/ctype.h
El nombre de cada macro pregunta si el carácter es de cierta clase, por ejemplo si es símbolo
alfanumérico el nombre es isalnum. Cada macro retorna un valor diferente de cero en caso de
ser verdadero y cero en caso de ser falso.
El siguiente arreglo contiene la información de atributos de cada carácter, indexada por su valor
numérico ascii +1.
Si el valor entero del carácter es –1(EOF), al sumarle 1, resulta índice 0 para la tabla de
búsqueda. Si se buscan los atributos en la entrada 0 del arreglo; se advierte que tiene definido
valor cero, resultando con retornos falsos de los macros para EOF.
Se escoge para EOF un valor numérico diferente a los imprimibles.
isascii está definida para valores enteros. El resto de los macros están definidos sólo cuando
isaccii es verdadero o si c es EOF.
Los caracteres considerados gráficos no contemplan la categoría espacio (_S); pero sí están
considerados en la de imprimibles.
En la categoría espacios se consideran los caracteres de control: tab, lf, vt, ff, cr
Las letras de las cifras hexadecimales se consideran en mayúsculas y minúsculas.
Con el macro siguiente, que pone en uno el bit en posición dada por bit:
# define _ISbit(bit) (1 << (bit))
Los códigos de biblioteca suelen ser más complejos que los ilustrados, ya que consideran la
portabilidad. Por ejemplo si el macro que define un bit en determinada posición de una palabra,
se desea usar en diferente tipo de procesadores, se agrega texto alternativo de acuerdo a la
característica.
Las órdenes de compilación condicional seleccionan, de acuerdo a las condiciones, la parte del
texto fuente que será compilada.
Por ejemplo: Si se desea marcar uno de los bits de una palabra de 16 bits, el macro debe
considerar el orden de los bytes dentro de la palabra.
# if __BYTE_ORDER == __BIG_ENDIAN
# define _ISbit(bit) (1 << (bit))
# else /* __BYTE_ORDER == __LITTLE_ENDIAN */
# define _ISbit(bit) ((bit) < 8 ? ((1 << (bit)) << 8) : ((1 << (bit)) >> 8))
# endif
3. Strings.
Se describen las rutinas que manipulan strings, cuyos prototipos se encuentran en string.h
#include <string.h>
typedef unsigned size_t; /* define tipo requerido por sizeof */
char string[6]; /*crea string con espacio para 6 caracteres. Índice varía entre 0 y 5 */
string[5] = NULL;
string
\0
El nombre del string es un puntero constante que apunta al primer carácter del string. Por ser
constante no se le puede asignar nuevos valores o modificar.
3.1.2. Puntero a carácter.
La definición de un string como un puntero a carácter, puede ser inicializada asignándole una
constante de tipo string. La que se define como una secuencia de cero o más caracteres entre
comillas dobles; el compilador agrega el carácter „\0‟ automáticamente al final.
Si dentro del string se desea emplear la comilla doble debe precedérsela por un \.
En caso de escribir, en el texto de un programa, un string de varias líneas, la secuencia de un \ y
el retorno de carro(que es invisible en la pantalla) no se consideran parte del string.
char * str1 = "abcdefghi"; /* tiene 10 caracteres, incluido el NULL que termina el string.*/
Un argumento de tipo puntero a carácter puede ser reemplazado en una lista de parámetros, en
la definición de una función por un arreglo de caracteres sin especificar el tamaño. En el caso
del ejemplo anterior: char str1[ ]. La elección entre estas alternativas suele realizarse según sea
el tratamiento que se realice dentro de la función; es decir, si las expresiones se elaboran en base
a punteros o si se emplea manipulación de arreglos.
3.2. Strcpy.
destino
cp fuente
El diagrama ilustra los punteros fuente y cp, después de haberse realizado la copia del primer
carácter. Se muestra el movimiento de copia y el de los punteros.
Cuando el contenido de *fuente es el carácter NULL, primero lo copia y la expresión resultante
de la asignación toma valor cero, que tiene valor falso para la condición, terminando el lazo
while.
El operador de postincremento opera sobre un left value(que recuerda un valor que puede
colocarse a la izquierda de una asignación). Un lvalue es un identificador o expresión que está
relacionado con un objeto que puede ser accesado y cambiado en la memoria.
Puede evitarse la acción doble relacionada con los operadores de pre y postincremento, usando
éstos en expresiones que sólo contengan dichos operadores. En el caso de la acción de
repetición: while(*cp++ = *fuente++) continue;
Cuando en la lista de parámetros de una función aparece la palabra reservada const precediendo
a una variable de tipo puntero, el compilador advierte un error si la función modifica la variable
a la que el puntero apunta. Además cuando se dispone de diferentes tipos de memorias (RAM,
EEPROM o FLASH) localiza las constantes en ROM o FLASH. Si se desea que quede en un
segmento de RAM, se precede con volatile, en lugar de const.
Ejemplo:
#include <string.h>
#include <stdio.h>
char string[10]; /*crea string con espacio para 10 caracteres */
char * str1 = "abcdefghi"; /* tiene 10 caracteres, incluido el NULL que termina el string.*/
int main(void)
{ strcpy(string, str1); printf("%s\n", string);
return 0;
}
3.3. Strncpy.
strncpy copia n caracteres desde el string fuente hacia el string destino. Si el string fuente tiene
menos de n caracteres rellena con nulos hasta completar la copia de n caracteres.
Si el string fuente tiene n o más caracteres el string destino no queda terminado en un nulo.
char *strncpy(register char * destino, register const char * fuente, register size_t n )
{ register char * cp = destino;
while( n ) { n--; if (! (*cp++ = *fuente++) ) break; }
while( n--) *cp++ = 0;
return destino;
}
Concatena una copia del string fuente luego del último carácter del string destino.
El largo del string resultante es la suma: strlen(dest) + strlen(src).
Retorna un puntero al string destino, que ahora contiene la concatenación.
El primer while deja el puntero cp apuntando al carácter de fin del string destino. Luego el
segundo while efectúa la copia, sobreescribiendo el NULL del string destino, con el primer
carácter del string fuente.
Ejemplo:
#include <string.h>
#include <stdio.h>
char destino[25];
char *espacio = " ", *fin = "Final", *destino = "Inicio";
int main(void)
{ strcat(destino, espacio); strcat(destino, fin);
printf("%s\n", destino);
return 0; destino
}
cp fuente
3.5. Strncat.
strncat concatena al final del string destino a lo más n caracteres del string fuente.
Si fuente tiene menos de n caracteres, el segundo operador del and copia el fin de string. Si el
string fuente tiene n o más caracteres, es el primer operando del and es el que da por terminado
el segundo while; saliendo de éste con un valor cero de n. Es para este último caso que está el if
final que escribe el terminador del string destino.
3.6. Strlen.
Largo de un string.
Retorna el número de caracteres del string s. No cuenta el carácter de terminación.
El while termina cuando *cp tiene valor cero. Pero debido al operador de postincremento, al
salir del while, cp queda apuntado una posición más allá de la posición que ocupa el NULL.
Entonces cp-1, apunta al NULL. Y la resta de punteros, produce un entero como resultado; éste
da el número de caracteres del string. Si a s se le suman 5, se tiene el valor del puntero que
apunta al terminador del string, en el caso del ejemplo que se ilustra a continuación; entonces
(cp-1) – s resulta 5 en este caso.
\0
cp
Figura A2.5. Largo string.
3.7. Strcmp.
Si los caracteres *s1 y *s2 son iguales, r es cero; y el valor lógico del primer operando del and
es verdadero. Si son diferentes, termina el lazo de repetición. Si el valor de *s2 es mayor que el
valor de *s1, r será negativo; implicando que el string s2 es "mayor" que el string s1. Si *s2 es
el carácter NULL, r será positivo si *s1 no es cero, terminando el while; implicando que s1 >
s2. Si *s1 es el carácter NULL, r será negativo si *s2 no es cero, terminando el while;
implicando que s1 < s2.
3.8. Strncmp.
int strncmp(register const char * s1, register const char * s2, size_t n)
{
while( n--) { if(*s1 == 0 || *s1 != *s2) return (*s1 - *s2); s1++; s2++; }
return 0;
}
Para los primeros n caracteres, si los caracteres difieren o se llegó al fin del string s1 se retorna
la resta del valor entero asociado a los caracteres.
Si s2 es más corto que s1, los caracteres difieren ya que *s2 es cero, y el retorno será positivo;
indicando s1 > s2.
3.9. Strstr.
3.10. Strchr.
El tipo char es tratado como entero con signo de 8 bits(-128 a +127) y es promovido
automáticamente a tipo entero. Sin embargo se emplea un conversión explícita de entero a char
mediante el molde o cast: (char) c
Debido a que el parámetro formal se trata como puntero a una constante string(para evitar que la
función modifique a s), se debe efectuar una conversión del tipo de puntero para el retorno, se
pasa a puntero a carácter (char *) el puntero a string constante: const char *.
La búsqueda de c en s es hacia adelante (de izquierda a derecha, o de arriba hacia abajo).
Si s apunta al terminador del string, la expresión *s tiene valor cero, y la condición del while es
falsa, por lo tanto no busca el terminador del string.
El fin de string(terminador nulo) es parte del string. Si se desea que la búsqueda strchr(s, 0)
retorne un puntero al terminador nulo del string s, debe efectuarse la siguiente modificación:
\0 s
El terminador nulo es considerado parte del string. Si se desea buscar el terminador del string
debe modificarse la rutina anterior, o utilizar strchar.
3.12. Strpbrk.
strpbrk busca en el string s1 la primera ocurrencia de cualquier carácter presente en s2; retorna
un puntero al primer encontrado, en caso que ningún carácter de s2 esté presente en s1 retorna
un puntero nulo.
3.13. Strcspn.
strcspn encuentra el segmento inicial del string s1 que no contiene caracteres que se encuentren
presentes en s2. Retorna el largo del segmento encontrado.
A partir del primer carácter de s1 se revisa que éste no esté presente entre los caracteres que
forman s2.
3.14. Strspn.
strspn encuentra el segmento inicial del string s1 que solamente contiene caracteres que se
encuentren presentes en s2. Retorna el largo del segmento encontrado.
3.15. Strtok.
Strtok busca el primer substring de s1 que está antes del string s2 que se considera un
separador.
Se considera que s1 es un string formado por una secuencia de cero o más símbolos(tokens)
separados por el string s2.
La función strtkn permite obtener todos los tokens mediante llamados subsiguientes al primero.
Para esto el primer llamado debe retornar un puntero al primer token, y escribir un carácter
NULL en el string s1, inmediatamente después del último carácter del token que se retorna.
Los siguientes llamados deben realizarse con un puntero nulo en el primer argumento. El
separador s2 puede ser diferente para cada una de las siguientes invocaciones. Si no se
encuentran símbolos la función debe retornar un puntero nulo.
Como es una rutina que puede llamarse varias veces, su diseño debe incluir una variable estática
que permanezca entre llamados. Ya que los argumentos y variables locales sólo existen mientras
la función esté activa, debido a que se mantienen en un frame en el stack.
s1
t1 s2 t2 s2 t2 s2
sp
El primer llamado a strtrk, debe retornar s1, colocando un nulo en el primer carácter de s2, y
fijar la posición de la estática sp inmediatamente después del terminador recién insertado.
Los llamados subsiguientes llevan un NULL en el primer argumento, correspondiente al puntero
al string s1; esto le indica a la función que debe emplear ahora sp para seguir buscando tokens.
Mediante la función strspn se saltan los caracteres pertenecientes al separador s2, y s1 queda
apuntado al primer carácter siguiente al separador s2.
El tercer if, si se llegó al final del string, fija sp en puntero nulo, y retorna un puntero nulo.
No se efectúa la acción del tercer if, si quedan caracteres por escanear.
Mediante la función strcspn se fija sp una posición más allá del último carácter del token
encontrado.
El cuarto if, fija sp en puntero nulo, si se agotó la búsqueda (se efectúa el else); en caso,
contrario si quedan caracteres por escanear, coloca un carácter nulo para finalizar el token
encontrado, y avanza sp una posición más adelante.
El tipo del argumento de s1 no puede ser un puntero constante, ya que se escribe en el string.
Si por ejemplo el string s1 tiene el valor "abc, dd,efg" y si el separador s2 es el string ","; se
encuentra, después del primer llamado el token: abc. Luego del segundo (con primer argumento
NULL) el token: dd. Finalmente el símbolo: efg.
3.16. Strdup.
strdup crea un duplicado del string s, obteniendo el espacio con un llamado a malloc.
Malloc retorna un puntero de tipo void o un puntero genérico. Se emplean cuando el compilador
no puede determinar el tamaño del objeto al cual el puntero apunta. Por esta razón los punteros
de tipo void deben ser desreferenciados mediante casting explícito.
El siguiente ejemplo muestra que a un puntero tipo void se le puede asignar la dirección de un
entero o un real. Pero al indireccionar debe convertirse el tipo void al del objeto que éste
apunta.
int x;
float r;
void *p;
p = &x; * (int *) p = 2;
p = &r; *(float *)p = 1.1;
Bloques de memoria.
El conjunto de funciones cuyos prototipos se encuentran en string.h también incluye un grupo
de funciones que manipulan bloques de memoria.
Los bloques son referenciados por punteros de tipo void, en la lista de argumentos. Luego en las
funciones se definen como variables locales punteros a caracteres, que son iniciados con los
valores de los punteros genéricos amoldados a punteros a carácter.
3.17. Memcpy.
memcpy Copia un bloque de n bytes desde la dirección fuente hacia la dirección destino.
3.18. Memccpy.
Copia no más de n bytes desde el bloque apuntado por fuente hacia el bloque apuntado por
destino, deteniéndose cuando copia el carácter c.
Retorna un puntero al bloque destino, apuntando al byte siguiente donde se copió c. Retorna
NULL si no encuentra a c en los primeros n bytes del bloque fuente.
Se emplean locales de tipo registro para acelerar la copia. Nótese que los punteros de tipo void
de los argumentos son los valores iniciales de los registros con punteros a caracteres.
3.19. Memmove.
Con memmove, si los bloques apuntados por fuente y destino se traslapan, los bytes ubicados en
la zona de traslapo se copian correctamente.
Los dos casos del or se ilustran en las dos figuras de más a la izquierda. En el caso que ilustra la
figura central, no hay traslapo.
En el caso s>=d, si d+n>=s, se produce traslapo y se sobreescriben las primeras posiciones del
bloque s, las que primero fueron copiadas (cuando se ejecuta el else, avanzado los punteros
hacia direcciones cada vez mayores).
n n
s d d
Debido a que el bloque fuente puede ser sobrescrito, no puede ser un puntero vacío constante.
Los punteros a carácter s y d, son iniciados con los valores amoldados (cast) a punteros a
carácter de fuente y destino.
3.20. Memcmp.
memcmp compara los primeros n bytes de los bloques s1 y s2, como unsigned chars.
Rellena n bytes del bloque s con el byte c. Retorna puntero genérico al bloque.
void *memset(void * s, int c, register size_t n)
{ register char * p = (char *)s;
while(n--) *p++ = (char) c;
return s;
}
Efectuar movimientos de bloques orientados al carácter es ineficiente. Por esta razón las
funciones de movimiento tratan de mover palabras.
Primero se mueven los bytes parciales de una palabra, luego se pueden mover palabras
alineadas; para finalmente, copiar los bytes presentes en la última palabra.
Estas funciones, implementadas en base a macros para mejorar la velocidad, son dependientes
del procesador. Se requiere conocer el ancho de la palabra y el ordenamiento de los bytes dentro
de la palabra (little o big endian).
Los strings asociados reflejan el ordenamiento de los bytes dentro de la palabra. Suelen
denominarse:
El siguiente texto ilustra una función de movimiento por bloques, mostrando el grado de
complejidad de estas rutinas. Se invoca a varios macros, de los cuales sólo se da el nombre.
rettype
memmove (a1const void *a1, a2const void *a2, size_t len)
{
unsigned long int dstp = (long int) dest;
unsigned long int srcp = (long int) src;
/* There are just a few bytes to copy. Use byte memory operations. */
BYTE_COPY_FWD (dstp, srcp, len);
}
else
{
/* Copy from the end to the beginning. */
srcp += len;
dstp += len;
/* There are just a few bytes to copy. Use byte memory operations. */
BYTE_COPY_BWD (dstp, srcp, len);
}
RETURN (dest);
}
4. Rutinas de conversión.
La función ocupa un buffer estático de 65 bits, lo cual permite convertir enteros de 64 bits en
secuencias binarias. Se considera en el buffer espacio para el signo y el terminador del string.
Para enteros de 16 bits, el rango de representación es: [-32768.. 32767] el cual requiere
de 5 char para representar mediante dígitos decimales.
Para enteros de 32 bits: [-2147483648 .. +2147483647] se requieren 10 chars para
dígitos.
Para enteros de 64 bits: [-9223372036854775808..+9223372036854775807] se
requieren 19 char para dígitos decimales. Para imprimir en binario se requieren 63
dígitos binarios.
El procedimiento consiste en sacar el módulo base del número, esto genera el último carácter
del número; es decir el menos significativo. Luego se divide en forma entera por la base,
quedando el resto; del cual se siguen extrayendo uno a uno los dígitos.
Por ejemplo para el entero 123 en base decimal, al sacar módulo 10 del número se obtiene el
dígito de las unidades, que es 3. Al dividir, en forma entera por la base, se obtiene el número de
decenas; es decir, 12. Sacando módulo 10 se obtiene 2; y al dividir por la base se obtiene el
número de centenas.
#define INT_DIGITOS 63
static char buf[INT_DIGITOS + 2];
/* Buffer para INT_DIGITS dígitos, signo - y fin de string '\0' */
do { dig=(i%base); if (dig <=9) *--p = '0' + dig; else *--p= '7'+ dig ; i /= base;}
while (i != 0);
Para convertir enteros se emplea la misma rutina anterior, invocando con una conversión
explícita del entero a largo.
#include <stdio.h>
int main(void)
{
int i=-31;
long int l= -2147483647L;
Si bien esta rutina efectúa el trabajo inverso de la anterior, su diseño es más complejo, ya que
debe operar con datos suministrados por un ser humano. La anterior saca algo que está en
formato interno y que está bien especificado.
En la conversión de un string en un long integer, debe asumirse que el usuario provee una
secuencia de caracteres que tiene el siguiente formato:
Los corchetes indican elementos opcionales, el asterisco la repetición de cero o más veces. De
esta forma podrían digitarse caracteres espacios o tabuladores antes de la secuencia; podría
indicarse el signo +, y también declarar números octales si el primer dígito es cero, o
hexadecimales si los primeros dígitos son 0x ó 0X.
En el diseño se decide terminar de leer cuando encuentra un carácter que no cumpla el formato.
Para permitir seguir analizando la secuencia de entrada se decide pasar a la función, además de
un puntero al inicio de la secuencia a analizar, y de la base, la referencia a un puntero a carácter.
Al salir de la función se escribe, en la variable pasada por referencia, el valor del puntero que
apunta al carácter que detuvo el scan, por no cumplir el formato. Esto debido a que en C, sólo se
puede retornar un valor desde la función.
Además la función debe resolver que el valor del número ingresado no exceda el mayor
representable. Como los números se representan con signo en complemento a la base, se tendrán
valores máximos diferentes(en la unidad) para el máximo positivo y el máximo negativo.
Por ejemplo para enteros largos de 32 bits, el rango asimétrico de representación en base
decimal es [-2147483648..2147483647].
Como se lee la secuencia de izquierda a derecha, el núcleo del algoritmo consiste en multiplicar
el número acumulado en acc, por la base y luego sumarle el valor numérico del dígito.
Por ejemplo, la secuencia 123 en decimal, es procesada según:
0*10 + 1 = 1
1*10 + 2 = 12
12*10 + 3 = 123
/*#include <limits.h>*/
#define LONG_MAX 0x7FFFFFFFL
#define LONG_MIN ((long)0x80000000L)
/*#include <ctype.h>*/
#define islower(c) (('a' <= (c) ) && ((c) <= 'z'))
#define isupper(c) (('A' <= (c) ) && ((c) <= 'Z'))
#define isdigit(c) (('0' <= (c) ) && ((c) <= '9'))
#define isspace(c) ((c) == ' ' || (c) == '\t' || (c)== '\n')
#define isalpha(c) ((islower(c))||(isupper(c)))
/*#include <errno.h>*/
#define ERANGE 34 /* Resultado demasiado grande. Rebalse de representación. */
extern int errno;
return (acc);
}
/* strtol ejemplo */
#include <stdio.h>
int main(void)
{
char *string = "87654321guena", *endptr;
long lnumber;
Resulta útil una función que convierta un número en punto flotante en una base b1 en otro
número punto flotante en base b2. Si b1 es 2 y b2 es 10, se convierte un número real en
representación interna en externa.
El algoritmo consiste en alternar las multiplicaciones (o divisiones) de m por b1, con las
divisiones (o multiplicaciones) por b2, de tal forma que m se mantenga en el rango:
Debido a que la función entrega dos valores, se decide pasar por referencia el exponente e2.
Sólo acepta bases positivas menores o iguales que 36; en caso de estar fuera de rango asume
base decimal.
Multiplica la mantisa por la base, quedando de esta forma un número a la izquierda del punto
decimal. Dicho dígito puede obtenerse en v, truncando el doble; esto se logra mediante el cast
explícito a entero. Luego se puede enviar hacia la salida el carácter, considerando una
conversión a carácter, que toma en cuenta bases mayores a la decimal.
Antes de volver a seguir desplegando caracteres, le quita la parte entera al doble; de este modo
al inicio del bloque, u siempre es un número fraccionario puro.
Nótese que no tiene sentido invocar la impresión binaria con más de 52 bits, ya que ese es el
número de bits de un doble. Tampoco tiene sentido invocar la impresión hexadecimal de la
mantisa con más de 7 cifras.
Puede verificarse que tampoco tiene sentido solicitar salidas en base decimal con más de 18
dígitos, ya que ésta es la precisión de un doble.
4.5. Rutinas más eficientes para convertir un número punto flotante binario a punto flotante
decimal.
}
/* con a de tipo float. round(2048/10) = 205. Funciona si e<1029 */
long int i;
for(i=0; i<1030; i++)
/*realiza i/10 mediante multiplicaciones enteras. Para i<1029 */
if ( ((i*205)/2048)!=(i/10)) printf( " %d \n", i);
4.5.3. Redondeo de la mantisa.
double round(double t, unsigned int i)
{ /*no puede redondear a mas de 15 cifras */
if (i<16)
switch (i)
{ case 2: t+=0.5e-2;break;
case 3: t+=0.5e-3;break;
case 4: t+=0.5e-4;break;
case 5: t+=0.5e-5;break;
case 6: t+=0.5e-6;break;
case 7: t+=0.5e-7;break;
case 8: t+=0.5e-8;break;
case 9: t+=0.5e-9;break;
case 10: t+=0.5e-10;break;
case 11: t+=0.5e-11;break;
case 12: t+=0.5e-12;break;
case 13: t+=0.5e-13;break;
case 14: t+=0.5e-14;break;
case 15: t+=0.5e-15;break;
}
return(t);
}
Una función puede tener parámetros fijos y una lista de argumentos variables. El número y tipo
de argumentos no son conocidos en el momento que se diseña la función.
El siguiente prototipo de func indica que tiene un parámetro fijo, un puntero a entero, y los tres
puntos a continuación indican que se puede pasar a esta función un número variable de
argumentos.
En la definición de la función debe existir alguna forma de conocer el número y tipo de los
parámetros variables. En la función printf el argumento fijo es el string de control que permite
determinar el número y tipo de argumentos.
Debe conocerse la estructura del frame de la función en el stack. Si suponemos que previo a la
invocación de una función se empujan los valores de los argumentos empezando por el último
argumento, se tendrá el siguiente esquema del frame, para la invocación a
func(p, a, b, c), después del llamado:
p
a
b
c
Las
direcciones
aumentan
Entonces el primer argumento variable tiene una dirección mayor que el último argumento fijo.
Como además los tamaños de los argumentos pueden ser diferentes, deben guardarse los
parámetros alineados.
* (tipo de a *) (dirección de a)
va_list es un tipo de puntero genérico; es decir puede apuntar a cualquier elemento de memoria,
no importando su tamaño. Más adelante se explicarán detalladamente los macros.
La inicialización del puntero, dentro de la función se logra con va_start, y debe usarse antes de
llamar a va_arg o va_end.
va_arg retorna el valor del argumento y mueve el puntero al inicio del siguiente argumento.
va_end desconecta el puntero, y debe emplearse una vez que va_arg haya leído todos los
argumentos.
int main(void) {
sum("El total de: 1+2+3+4 es igual a %d\n", 1, 2, 3, 4, 0);
return 0;
}
Para efectuar correctamente los movimientos del puntero, y para considerar el alineamiento en
el almacenamiento de los argumentos, se emplea el macro:
Esto considera que un entero se guarda alineado, lo cual es una definición implícita en C. Lo
rebuscado, aparentemente, permite portar el código de estos macros a procesadores con
diferentes tamaños para el entero.
El operador sizeof retorna el número de bytes de la expresión o el tipo que es su argumento.
Para enteros de 16 bits, la máscara que se forma tomando el complemento a uno, resulta ser:
…1111110. Para enteros de 32 bits, la máscara es: ….1111100.
Para 64 bits: ….1111000.
Al tamaño de x en bytes se le suma el tamaño en bytes del entero menos uno, lo cual luego es
truncado por la máscara, obteniendo el tamaño de x en múltiplos del tamaño de un entero.
El siguiente segmento permite verificar en forma enumerativa, la función del macro que entrega
direcciones alineadas:
for(t=2;t<9;t*=2)
{ printf(" t=%d \n",t);
for( i=1; i<16;i++) printf(" i=%d largo=%d\n", i, (i+t-1)&~(t-1));
La dirección del último parámetro fijo se convierte a puntero a char(a un Byte) y se obtiene la
siguiente dirección (en Bytes) donde está almacenado el primer argumento variable, mediante:
( (char *)(&parmN) +__size(parmN) )
Luego esta dirección se convierte en la del tipo de ap, y se la escribe en ap. El primer void
indica que el macro no retorna valores.
Debe notarse que cuando se emplean macros, los argumentos deben colocarse entre paréntesis.
Es un tanto más compleja. Ya que efectúa dos cosas, retornar el valor del argumento y por otro
lado incrementar el puntero ap, de tal modo que apunte al próximo argumento.
Primero incrementa el puntero, luego obtiene el valor.
La expresión *(char **)&ap podría haberse anotado más sencillamente: (char *) ap;
El siguiente es un esquema para printf. Se traen los valores de los argumentos a variables
locales.
va_start(ap, format);
for(p = format; *p ; p++) {
if( *p !='%') {putchar(*p); continue;}
switch( *++p) {
case 'd':
ival= va_arg(ap, int);
/* trae el argumento entero a la local ival */
Esta rutina es dependiente de la forma en que el compilador pasa los argumentos. Es decir si
pasa algunos argumentos en registros o los pasa en el stack. Además depende de la forma en
que el compilador almacena los diferentes tipos. Por ejemplo se ilustra una modificación a los
macros para corregir el valor de ap, en el caso que guarde alineados los dobles en direcciones
que son múltiplos de 8 Bytes. Si el tercer bit es 1, le suma 4 bytes a ap, de tal forma de alinear
correctamente al doble. Este es el caso del compilador lcc.
En la rutina se han empleado variables locales para depositar internamente los valores de los
argumentos, de acuerdo al tipo; y su objetivo es ilustrar el uso de va_arg.
Existen funciones como itoa que transforman un entero en una secuencia de caracteres.
Mediante estas funciones se puede traducir el despliegue de un entero, en determinada base, en
el despliegue de una secuencia de caracteres, lo cual se logra con putchar.
Se traen los valores de los argumentos a punteros locales a la rutina, con el fin de ilustrar el uso
de va_arg. Nótese que en este caso se traen punteros, y que puede cambiarse el valor de la
variable pasada por referencia, mediante indirección.
va_start(ap, format);
for(p = format; *p ; p++) {
if( *p !='%') continue;
switch( *++p) {
case 'd':
pi=(int *) va_arg(ap, int*);
/* trae a pi un puntero a entero */
/*acá debe ingresarse un entero y depositarlo en *pi */
break;
case 'l':
pd=(double *) va_arg(ap, double*);
/* trae a pd un puntero a doble */
/*acá debe ingresarse un doble y depositarlo en *pd */
break;
case 's':
pc=(char *) va_arg(ap, char *);
/* trae a pc un puntero a char */
/*acá debe ingresarse un string y copiarlo desde pc */
break;
}
}
va_end(ap);
}
Esta rutina debe cuidar que los caracteres ingresados correspondan a lo que se desea leer; que el
espacio asociado al string externo no sea excedido por el largo del string ingresado. También
dependerá de cómo se termine de ingresar los datos, o la acción que deberá tomarse si lo
ingresado no corresponde al tipo que se establece en el string de control.
Funciones como atoi y atof, pasan de secuencias de caracteres a enteros o flotantes; y mediante
éstas, puede implementarse scanf como una secuencia de llamados a getchar.
Deseamos dotar a los programas compilados para MIPS, mediante el compilador lcc, de rutinas
de interfaz con los llamados al sistema que SPIM provee.
El siguiente código en C, muestra el diseño de la rutina printf, empleando los macros vistos
antes, ya que printf es una función con un número variable de argumentos:
va_start(ap, format);
for(p = format; *p ; p++) {
if( *p !='%') {putchar(*p); continue;}
switch( *++p) {
case 'd':
syscall(va_arg(ap, int),1);
break;
case 'f':
if ((int)ap&4) ap=(va_list)((int)ap +4); /*align double */
syscall(va_arg(ap, double),3);
break;
case 's':
syscall(va_arg(ap, char *),4);
break;
}
}
va_end(ap);
}
La traducción de syscall se traduce en pasar en $a0 y $a1 los argumentos y luego efectuar un:
jal syscall.
Debido a que el compilador lcc, introduce en el stack los dobles alineados en palabras dobles, se
requiere forzar el alineamiento en los casos que sea necesario.
la $a0,formato
la $a1,5
l.d $f18,doble #carga en registro un valor doble
mfc1.d $a2, $f18 #mueve a $a2 y $a3 el valor
la $t8, stringhola
sw $t8,16($sp) #pasa el puntero en el stack.
jal printf
.data
.align 0
formato: .asciiz " entero= %d doble= %f string=%s \n"
.align 3
doble:
.word 0x2843ebe8
.word 0x40280000
.align 0
stringhola: .asciiz "hola"
.rdata
.align 2
_sss: .word 0 # espacio para almacenar un char terminado en \0.
.globl putchar
.text
.align 2
putchar: la $t8,_sss
sw $a0,0($t8)
.globl printf
#void printf(char * format,...)
.text
.align 2
printf:
.frame $sp,32,$31
addu $sp,$sp,-32
.mask 0xc0800000,-8
sw $s7,16($sp)
sw $s8,20($sp)
sw $ra,24($sp)
sw $a0,32($sp)
sw $a1,36($sp)
sw $a2,40($sp)
sw $a3,44($sp)
#{
la $t8,4+32($sp)
sw $t8,-4+32($sp) # va_start(ap, format);
lw $s8,0+32($sp) # for(p = format; *p ; p++) {
b _tstcnd
_esf:
lw $t8,-4+32($sp) # if ((int)ap&4) ap=(va_list)((int)ap +4);
and $t8,$t8,4
beq $t8,$0,_nosuma
lw $t8,-4+32($sp)
la $t8,4($t8)
sw $t8,-4+32($sp)
_nosuma:lw $t8,-4+32($sp) # syscall(va_arg(ap, double),3);
la $t8,8($t8)
sw $t8,-4+32($sp)
l.d $f12,-8($t8) #pasa eldoble en reg. doble $f12
la $v0,3 # la $a1,3
syscall # jal syscall
b _siga2 # break;
_siga1: # break;
_siga2:
# }
sw $0,-4+32($sp) # va_end(ap);
#}
lw $s7,16($sp)
lw $s8,20($sp)
lw $ra,24($sp)
addu $sp,$sp,32
j $ra
.end printf
li $a1,largo #
li $v0, 8 # system call code for read_str
la $a0, str # buffer of string to read
syscall # read the string
*/
va_start(ap, format);
for(p = format; *p ; p++) {
if( *p !='%') continue;
switch( *++p) {
case 'd':
pi=(int *) va_arg(ap, int*);
*pi=syscall(5);
/*printf(" *pi=%d \n", *pi);*/
Los syscall generan jal syscall, que deben parcharse, y también el paso de los argumento a esos
llamados.
.rdata
.align 2
_sss: .word 0
.globl getchar
.text
.align 2
getchar:
la $a0,_sss # syscall(pc,2,8);
li $a1,2
la $v0,8
syscall
la $t8,sss
lw $v0,0($t8)
j $ra
.globl scanf
.text
#void scanf(char * format,...)
.text
.align 2
scanf:
.frame $sp,56,$31
addu $sp,$sp,-56
.mask 0xc0e00000,-24
lw $s8,0+56($sp)
b _tstscn
_blkscn:lb $t8,($30)
la $t7,37 # '%'
beq $t8,$t7,_swscn
b _cntscn
# }
sw $0,-4+56($sp) # va_end(ap);
#}
lw $s5,16($sp)
lw $s6,20($sp)
lw $s7,24($sp)
lw $s8,28($sp)
Si estas rutinas se agregan al código del trap.handler de SPIM, pueden emplearse llamados a
printf, scanf, getchar y putchar, sin tener que incluir dichos códigos junto a los programas
fuentes.
Puede implementarse printf en términos de putchar. Es decir una función que saca un carácter
hacia el medio de salida. La implementación de putchar suele efectuarse programando la puerta
serial de un microcontrolador, para esto bastan muy pocas instrucciones (no más de 10).
#include <stdarg.h>
int printf(const char *format, ...)
{
va_list ap;
int retval;
va_start(ap, format);
va_end(ap);
return retval;
}
Se ilustra una rutina bajada de la red (de la cual perdí la referencia), con pequeñas
modificaciones que implementa printf. Lo cual muestra que la función printf está formada por
numerosas instrucciones, lo cual debe ser tenido en cuenta al emplearla en microcontroladores.
En estos casos existen versiones recortadas, mediante la no implementación de algunos
formatos.
/*
* This version only supports 32 bit floating point
*/
#define value long
#define NDIG 12 /* máximo número de dígitos ha ser impresos */
#define expon int
const static unsigned value
dpowers[] = {1, 10, 100, 1000, 10000, 100000L, 1000000L,
10000000L,10000000L,100000000L};
const static unsigned value
hexpowers[] = {1, 0x10, 0x100, 0x1000,0x10000L, 0x100000L,0x1000000L, 0x10000000L};
/* this routine returns a value to round to the number of decimal places specified */
double fround(unsigned char prec)
{ /* prec is guaranteed to be less than NDIG */
if(prec > 10) return 0.5 * npowers[prec/10+9] * npowers[prec % 10];
return 0.5 * npowers[prec];
}
/* this routine returns a scaling factor equal to 1 to the decimal power supplied */
static double scale(expon scl)
{ if(scl < 0) { scl = -scl;
if(scl > 10) return npowers[scl/10+9] * npowers[scl%10];
return npowers[scl];
}
if(scl > 10) return powers[scl/10+9] * powers[scl%10];
return powers[scl];
}
struct __prbuf
{ char * ptr;
void (* func)(char);
} pb;
/* mini test */
int main(void)
{ int x=15, y=2678; float f=3.2e-5;
return(0);
}
Al disponer del código fuente, éste puede adaptarse a las necesidades del usuario. En caso de ser
empleado en un microcontrolador, con el objeto de disminuir la memoria ocupada por printf, se
pueden recortar algunos modos que no se requieran.
6.1. Trigonométricas.
plot(sin(x), x=0..2*Pi);
Si efectuamos el cambio de variable, w = x/2*Pi, tendremos:
plot(sin(2*Pi*w),w=0..1); cuya gráfica se ilustra a continuación:
Después de este cambio de variables, los valores del argumento estarán acotados. De esta forma
cuando se calcule con valores reales elevados, éstos se reducen a valores entre 0 y 4, y no se
producirán errores cuando se calculen las potencias del argumento al evaluar la serie.
Si efectuamos: m= 4*(w-floor(w)) las variaciones de m serán en el intervalo entre 0 y 4,
cuando w cambia entre cualquier inicio de un período hasta el final de ese período.
Entonces para todos los reales positivos (representables) de w, se puede calcular en el primer
período, para valores de m entre 0 y 4:
plot( sin(2*Pi*m/4 ), m=0..4);
Puede compararse la aproximación por series de potencia (de dos y tres términos) con el
polinomio de Pade, mediante:
plot([x-x^3/6,x-x^3/6+x^5/120, pade(sin(x),x=0,[9,6])], x=0.7..1.7,
y= 0.65..1,color=[red,blue,black], style=[point,line,point]);
Es preciso calcular polinomios, puede emplearse la función estándar poly, descrita en math.h
Si por ejemplo se desea calcular:
p(x) = d[4]*(x**4)+d[3]*(x**3)+d[2]*(x**2)+d[1]*(x)+d[0]
Con algoritmo: poli=d[n]; i=n; while (i >0) {i--; poli = poli* x + d[ i-1]};
Debido a que los polinomios son de potencias pares en el denominador, se efectúa el reemplazo
x por x*x. Y para obtener potencias impares en el numerador se multiplica el polinomio del
numerador por x.
#include <math.h>
/*Calcula para n=4 el polinomio: d[4]*(x**4)+d[3]*(x**3)+d[2]*(x**2)+d[1]*(x)+d[0] */
double eval_poly(register double x, const double *d, int n)
{ int i;
register double res;
res = d[i = n];
while ( i ) res = x * res + d[--i];
return res;
#define PI 3.14159265358979
#define TWO_PI 6.28318530717958
double seno(double x)
{ static const double coeff_a[] = { 207823.68416961012, -76586.415638846949,
7064.1360814006881, -237.85932457812158, 2.8078274176220686 };
static const double coeff_b[] = { 132304.66650864931, 5651.6867953169177,
108.99981103712905, 1.0 };
register double signo, x2;
signo = 1.0;
if(x < 0.0) { x = -x; signo = -signo; } /*Solo argumentos positivos */
x /= TWO_PI; x = 4.0 * (x - floor(x));
if(x > 2.0) { x -= 2.0; signo = -signo;}
if( x > 1.0) x = 2.0 - x;
x2 = x * x;
return signo * x * eval_poly(x2, coeff_a, 4) / eval_poly(x2, coeff_b, 3);
}
Empleando Mapple puede obtenerse el polinomio de Pade, que aproxima a la función seno.
with(numapprox):
pade(sin(x), x=0, [9,6]);
24391.323500544000+1034.1371819921864*x^2+19.695328959656098*x^4+
.17656643195797582*x^6
evalf(expand(numer(a)/10^11),17); Calcula el numerador, dividido por 10^11, con 17 cifras.
38313.801360320554*x-14131.500385136448*x^3+1306.7304862998132*x^5-
44.226197226558042*x^7+.52731372638787005*x^9
La función floor está basada en el truncamiento de la parte fraccionaria del número real.
Si se tiene: Double d, t ;
Entonces t = (double)(long)(d); es el número truncado, con parte fraccionaria igual a cero.
Primero el molde (long) transforma d a un entero, luego el molde o cast (double) transforma ese
entero a doble.
Si la cantidad de cifras enteras de un double, no pueden ser representadas en un entero largo, la
expresión será errónea. Por ejemplo si el entero largo tiene 32 bits, si las cifras enteras del doble
exceden a 231 -1 se tendrá error.
double fabs(double d)
{ if(d < 0.0) return -d; else return d; }
En ocasiones resulta conveniente tener acceso a las representaciones internas de los números.
Los programas de este tipo deben considerar el ordenamiento de los bytes dentro de la palabra
de memoria; es decir si son de orden big-endian o little endian.
Estudiaremos varias alternativas de tratamiento. Desde la más simple de interpretar los bytes
dentro de la palabra, pasando por interpretar los enteros largos que constituyen una palabra
mayor; a métodos más generales que emplean uniones y campos; esta última no se recomienda
ya que es dependiente de la implementación del compilador.
Dado un número real de doble precisión x, la función frexp calcula la mantisa m (como un real
de doble precisión) y un entero n (exponente) tal que:
Como las funciones, en el lenguaje C, sólo retornan un valor, y si éste es el de la mantisa, debe
pasarse un segundo argumento por referencia: la dirección de un entero; y la función devolverá
el exponente escrito en el entero.
Por esta razón el segundo argumento es un puntero a entero. El valor de la mantisa es el valor
retornado por la función.
Entonces el número real que debe retornar la función, como mantisa mayor que un medio y
menor que uno es:
mantisa = (-1)S 1.M2 2 -1
Para flotantes de simple precisión, que empleen 32 bits, se dedican 8 bits al exponente, el
mayor positivo en complemento a dos es, en decimal, 127; que equivale a 01111111 en binario.
El número más negativo, -127, se representa en complemento a dos como: 10000001,
cumpliéndose que, para este número, la representación interna es: 00000000. La polarización
para tipo float es 127, en decimal.
Para reales de precisión doble, se emplean 64 bits, y 11 para el exponente; en este caso la
polarización es 1023 en decimal, con representación binaria complemento a dos: 01111111111
(0x3FF en hexadecimal).
Los últimos 7 bits del primer byte (*pc & 0x7F) son los primeros siete del exponente (ya que el
primero se emplea para el signo del número).
Los primeros 4 bits del segundo, son los últimos 4 del exponente interno. Para esto es preciso
desplazar en forma lógica, en cuatro bits, esto se logra con: *ps>>4.
Para formar el exponente interno se requiere desplazar, en forma lógica, los primeros siete bits
en cuatro posiciones hacia la izquierda.
Entonces: ei = (*pc & 0x7F)<<4 | (*ps>>4) forma el exponente interno, como una secuencia
binaria de 11 bits. Al depositarlo en un entero sin signo, los primeros bits quedan en cero
(desde el doceavo hasta el largo del entero).
Finalmente, se logra:
exponente = ei –1022;
Para sobrescribir el número 0x3FE, en las posiciones en que va el exponente interno, se requiere
modificar los últimos siete bits del primer byte, para no alterar el signo del número. Esto se
logra haciendo un and con la máscara binaria 10000000(0x80) y luego un or con la máscara
binaria 00111111(0x3F)
Es decir:
*pc = (*pc & 0x80) | 0x3F;
Para el segundo byte, sólo se deben sobrescribir los primeros cuatro. Esto se logra haciendo un
and con la máscara binaria 00001111(0x0F) y luego un or con la máscara binaria
11100000(0xE0)
Es decir:
*ps = (*ps & 0x0F) | 0xE0;
Para apuntar a los primeros dos bytes, debe conocerse el orden de los bytes dentro de las
palabras de la memoria. Esto es dependiente del procesador. En algunos sistemas el primer byte
(el más significativo dentro del double) tiene la dirección menor, en otros es la más alta.
Como casi todos los tipos de datos que maneja un procesador suelen ser múltiplos de bytes,
para obtener la dirección de una variable de cierto tipo (en este caso de un double) en unidades
de direcciones de bytes puede escribirse:
Luego de esto, considerando que un double está formado por 8 bytes se tiene: pc += 7; ps=pc-1;
para sistemas en que el byte más significativo tiene la dirección de memoria más alta.
O bien: ps = pc +1; si el byte más significativo tiene la dirección menor; en este caso, no es
preciso modificar pc.
ee = ei -1023; ei = ee + 1023
El exponente interno se está considerando de 11 bits, sin signo. En la norma IEEE 754 debe
considerarse números con signo. Para los dos últimos casos esto implica ei = -1.
Entonces con las definiciones:
unsigned long int *pm2=(unsigned long int *)&number;
unsigned long int *pm1=pm2+1;
Podemos apuntar con pm1 al entero largo más significativo, donde se almacena el signo, los 11
bits del exponente y 4 bits de la mantisa.
Si se deseara manipular el exponente interno como número con signo, habría que definir:
int ei=(int) ( ( ( long int)((*pm1)<<1)) >>21);
Se corre a la derecha el largo con signo, y luego se convierte a entero.
Las siguientes definiciones, nos permiten extraer la parte más significativa de la mantisa en m1
(20 bits), y la menos significativa en m2:
unsigned long m1=(*pm1)&0x000FFFFFL;
unsigned long m2=*pm2;
Setear el exponente externo en -1, para tener mantisa decimal que cumpla:
0.5 =< m < 1
se logra, como se explico antes, dejando el exponente interno en: 01111111110 (0x3FE).
Lo cual se logra con: ((*pm1)&0x800FFFFFL)| 0x3FE00000L
Nótese que la misma rutina que no trata los casos subnormales y el cero, podría escribirse:
Que equivale al comportamiento de la primera rutina que manipulaba los bytes del double.
En el ejemplo siguiente, la union denominada buffer, puede verse como un double o como una
estructura denominada pbs. Las variables anteriores tienen la misma dirección de memoria, y se
accesan de manera similar a una estructura. Si se escribe en una variable, se modifica la otra.
La estructura pbs, define 64 bits, el mismo tamaño que el double. Y permite identificar los dos
bytes más significativos del double, b0 y b1, en caso de que el byte más significativo esté
ubicado en la dirección menor. Y b6 y b7 si el más significativo del double está asociado a la
dirección mayor.
union buf
{ struct bts
{unsigned char b0;
unsigned char b1;
unsigned char b[4];
unsigned char b6;
unsigned char b7; /*el más significativo con dirección mayor*/
} pbs;
double d;
} buffer;
El valor +2.0 en doble precisión, equivale al valor 0x40000000 en formato IEEE 754. El signo
es cero, la mantisa normalizada es cero. Y el exponente externo es +1.
Y leer los bytes de la unión, accesando por su nombre los bytes de la estructura pbs.
if (buffer.pbs.b7==0x40) printf("el byte más significativo del double tiene la dirección
mayor\n");
if (buffer.pbs.b0==0x40) printf("el byte más significativo del double tiene la dirección
menor\n");
Entonces en el ejemplo siguiente, dentro de la unión buffer, se tiene la estructura pbs, que a su
vez está formada por la estructura campos1, el arreglo de 4 caracteres b, y la estructura
union buf
{ struct bts
{ struct campos1
{ unsigned int signo1 :1;
unsigned int exp1 :11;
unsigned int man1 :4;
} pc1;
unsigned char b[4];
struct campos2
{ unsigned int man2 :4;
unsigned int exp2 :11;
unsigned int signo2 :1; /*el byte más significativo con dirección mayor*/
} pc2;
} pbs;
double d;
} buffer;
Y leer los grupos de bits de la unión, accesando por su nombre los campos de la estructura pbs.
if (buffer.pbs.pc2.exp2==0x400) printf("el byte más significativo del double tiene la
dirección mayor\n");
if (buffer.pbs.pc1.exp1==0x400) printf("el byte más significativo del double tiene la
dirección menor\n");
return i;
}
Referencias.
Índice general.
APÉNDICE 2 .............................................................................................................................................. 1
INTRODUCCIÓN AL LENGUAJE C. ................................................................................................... 1
1. FUNCIONES. .......................................................................................................................................... 1
1.1. Abstracción de acciones y expresiones. ....................................................................................... 1
1.2. Prototipo, definición, invocación. ................................................................................................ 2
1.3. Alcances del lenguaje C. .............................................................................................................. 3
1.4. Paso de argumentos por valor. .................................................................................................... 4
1.5. Paso por referencia. ..................................................................................................................... 4
1.6. Frame. .......................................................................................................................................... 5
1.7. Algunos conceptos básicos ........................................................................................................... 6
1.7.1. Datos. .................................................................................................................................................... 6
Enteros con signo. ....................................................................................................................................... 6
Enteros sin signo. ....................................................................................................................................... 8
Enteros Largos. ........................................................................................................................................... 8
Largos sin signo. ......................................................................................................................................... 8
Números Reales. ( float ) ............................................................................................................................ 8
Carácter. ...................................................................................................................................................... 9
Strings. ........................................................................................................................................................ 9
1.7.2. Acciones. ............................................................................................................................................... 9
Secuencia. ................................................................................................................................................... 9
Alternativa. ............................................................................................................................................... 10
Repetición. ................................................................................................................................................ 10
For............................................................................................................................................................. 10
Abstracción. .............................................................................................................................................. 10
1.7.3. Entrada. Salida. .................................................................................................................................... 10
Ejemplos. .................................................................................................................................................. 11
2. TIPO CHAR. ......................................................................................................................................... 12
2.1. Valores. ...................................................................................................................................... 12
2.1. Definición de variables y constantes de tipo char. ..................................................................... 12
2.2. Caracteres ASCII. ...................................................................................................................... 12
2.3. Secuencias de escape.................................................................................................................. 14
2.4. Archivos de texto y binarios. ...................................................................................................... 14
2.5. Expresiones. ............................................................................................................................... 15
2.6. Entrada-Salida ........................................................................................................................... 16
Entrada y salida con formato. ........................................................................................................................ 17
2.7. Funciones. .................................................................................................................................. 18
2.8. Macros........................................................................................................................................ 19
2.9. Macros con argumentos. ............................................................................................................ 21
2.10. Biblioteca. ctype.c .................................................................................................................. 22
3. STRINGS.............................................................................................................................................. 24
3.1. Definición de string. ................................................................................................................... 24
3.1.1. Arreglo de caracteres. .......................................................................................................................... 24
3.1.2. Puntero a carácter................................................................................................................................. 25
3.2. Strcpy.......................................................................................................................................... 25
3.3. Strncpy........................................................................................................................................ 27
Índice de figuras.