pub:c_data


Los datos son el objeto de procesamiento en los programas de ordenador, en lenguajes avanzados se habla de objetos, como denominación más genérica.

En lenguaje C las variables y las constantes deben declararse antes de ser utilizadas.

La declaración de un dato requiere expresar:

  • El tipo de dato
  • El modificador (opcional)
  • El identificador
  modificador tipodato identificador;

Tipos básicos y modificadores

Los tipos de datos establecen la diferencia entre los objetos que se van a procesar, en cuanto a:

  • Memoria que ocupan
  • Rango o valores que se pueden almacenar
  • Modo en el que van a ser tratados

La cantidad de memoria necesaria para el almacenamiento de datos, así como el margen de variación (rango) de dichos datos depende:

  • Del compilador
  • Del sistema operativo
  • De la máquina

Las palabras reservadas en lenguaje C para los tipos de datos básicos son:

  • char Carácter
  • int Número entero
  • float Número real
  • double Número real de doble precisión
  • void Tipo que no existe
  • enum Tipo enumeración, lista de valores enteros

Los modificadores que se pueden aplicar a los tipos de datos básicos son:

  • signed Con signo
  • unsigned Sin signo
  • long Largo, de mayor tamaño de almacenamiento
  • short Corto, de menor tamaño de almacenamiento

Los datos fundamentales utilizados en lenguaje C se obtienen de las combinaciones permitidas de tipos básicos y modificadores.

Enteros

Los tipos de datos enteros permiten representar cantidades numéricas enteras

  • char (signed char) Tipo carácter
    • Normalmente ocupa 1 byte (permite almacenar un símbolo ASCII)
  • int (signed int) Tipo entero con signo
    • Normalmente ocupa 4 bytes
  • short (signed short int) Tipo entero corto
    • Normalmente ocupa 2 bytes
  • long (signed long int) Tipo entero en formato largo
    • En máquinas de 32 bits: 4 bytes; en 64 bits: 8 bytes
  • enum Tipo enumerado
    • Declara una variable que puede tomar como valores una lista de símbolos arbitrarios

La relación entre tamaños que se cumple siempre es:

  shortintlong

Representación interna de números enteros:

  • Números sin signo: aritmética binaria de módulo 2n siendo n el número de bits empleados
  • Números con signo: Complemento a 2 con el bit de mayor peso como bit de signo.

Ejemplos:

  • Variable letra de tipo carácter:
  char letra;
  • Variable cantidad de tipo entero:
  int cantidad;
  • Variable edad de tipo entero corto:
  short edad;
  • Variable memoria de tipo largo:
  long memoria;
  • Definición y utilización de un tipo de enumeración:
  enum semana = {lunes, martes, miercoles, jueves, viernes, sabado, domingo};
  enum semana hoy;
  hoy = martes;
    • semana es un tipo de enumeración
    • hoy es una variable de tipo enumerado que se ha cargado con el valor martes, que si se imprime, mostraría un «1».
    • Si hoy se inicializase con el valor domingo, al imprimirse, mostraría un «6» (lunes equivale a «0»)
Reales

Los tipos de datos reales permiten representar cantidades numéricas en notación científica y de mayor rango.

Se almacenan en memoria en un formato normalizado, ya explicado, en el que se distinguen tres campos:

  • El signo del número
  • La mantisa
  • El exponente (en representación sesgada)

Tipos:

  • float Tipo real de simple precisión
    • Hasta 7 dígitos significativos
  • double Tipo real de doble precisión
    • Hasta 16 dígitos significativos
  • long double Tipo real de doble precisión con formato largo
    • Puede llegar a tener hasta 19 dígitos significativos
Otros tipos de datos
  • Indefinido
    • El tipo void indica un dato de tipo indefinido
    • Se usa como genérico para instanciarlo más adelante
  • Derivados
    • Son datos complejos que se obtienen a partir de los datos fundamentales
    • Arrays, funciones, punteros, estructuras y uniones
  • Definidos
    • Son tipos creados por el usuario, con un nombre y definición propios
  typedef tipodato nuevonombre;
  • * * Facilitan la lectura y escritura de programas
    • Ejemplo:
  typedef unsigned long int mitipo;
  /* Se ha creado un nuevo tipo de dato: mitipo */

Constantes

Las constantes son valores fijos que no pueden ser alterados por el programa. Pueden ser de cualquiera de los tipos de datos posibles en lenguaje C:

  • Constantes enteras
  • Constantes reales
  • Constantes de caracteres
  • Constantes simbólicas
Enteras

Para su almacenamiento el compilador escoge el tipo de dato más pequeño compatible con esa constante

Pueden expresarse:

  • En decimal: La opción por omisión
    • El dígito de mayor peso no puede ser un «0»
    • Sólo son válidos los caracteres numéricos entre el 0 y el 9
  • En octal
    • El dígito de mayor peso es siempre un «0»
    • Sólo son válidos los caracteres numéricos entre el 0 y el 7
  • En hexadecimal
    • Van precedidas por los símbolos «0x»
    • Son válidos los caracteres numéricos del 0 al 9 y las letras A, B, C, D, E y F tanto mayúsculas como minúsculas

* Al escribirlas, se distinguirán los siguientes campos:

  • El prefijo para las hexadecimales o el carácter «0» para las octales.
  • El signo (opcional en el caso de números positivos)
  • El valor numérico
  • Un sufijo opcional que permite modificar el tamaño que el compilador debe asignarle:
    • U para indicar unsigned
    • L para indicar long
    • UL para indicar unsigned long

* Ejemplos:

  -23L  /* el número -23 almacenado como long */
  010   /* el octal 10 que equivale al 8 en decimal*/
  0xF   /* el 0F hexadecimal que es el 15 decimal */
Reales
  • En la asignación o definición, el compilador las crea siempre de tipo double
  • Al escribirlas, se distinguirán los siguientes campos:
    • El signo (opcional en el caso de números positivos)
    • Una parte entera precediendo al punto decimal «.»
    • La parte fraccionaria a la derecha del punto decimal
    • Se permite también la notación científica con «e» o «E»
    • Un sufijo opcional que permite modificar el tamaño que el compilador debe asignarle:
      • F para indicar float
      • L para indicar long double
    • Ejemplos:
  35.78       /* constante real de tipo double */
  1.25E-12    /* constante real de tipo double */
  45F         /* constante real de tipo float */
  33L         /* constante real de tipo long double */
Caracteres
  • Las constantes de un solo carácter son de tipo char y se expresan poniendo el carácter entre comillas simples: 'A'
  • Las constantes de barra invertida o caracteres de escape
    • Permiten representar códigos ASCII sin símbolo
    • Se expresan mediante el valor numérico de su código ASCII precedidos de la barra invertida y entre comillas: '\código'
    • El código puede representarse
      • En decimal, con hasta tres dígitos: '\ddd'
      • En octal, con dos dígitos: '\0oo'
      • En hexadecimal, con dos dígitos: '\0xhh'
  • Ejemplos:
  '6'       /* Carácter 6, código ASCII 0x36 */
  '\12'     /* Código ASCII 12 (Salto de línea) */
  '\0x20'   /* Códgio ASCII 32 (Espacio) */
  • Las constantes de cadena
    • No son un tipo de dato
    • Definen un conjunto de caracteres almacenados de forma consecutiva cada uno en un byte
    • Se representan entre comillas dobles
      • "Esto es una cadena de caracteres"
    • El compilador almacena la lista de caracteres y un carácter terminador para representar el final de la cadena: el carácter nulo «'\0'»
Simbólicas
  • Se definen mediante la directiva:
  #define NOMBRECONSTANTE Equivalencia
  • La directiva NO es una sentencia de lenguaje C
  • NOMBRECONSTANTE es el identificador de la constante simbólica (recomendado en mayúsculas)
  • Equivalencia representa los símbolos que va a representar NOMBRECONSTANTE
  • Siempre que en el programa aparezca NOMBRECONSTANTE será sustituido antes de compilar por Equivalencia
  • Ejemplo:
  #define MAXIMO 100               /* MAXIMO toma el valor 100 */
  #define FRASE "Pulsa una tecla"

Variables

Declaración de variables

Todas las variables deben declararse antes de ser utilizadas para que el compilador les asigne la memoria necesaria

  • La declaración de una variable es una sentencia
    • Consiste en escribir el nombre de la variable precedida por el tipo de dato:
  tipodedato nombrevariable;
    • tipodedato representa la palabra o palabras que definen el tipo de dato
    • nombrevariable es el identificador de la variable
  • Ejemplos:
   char letra;                /* variable tipo carácter */
   int actual, mayor, menor;  /* variables enteras */
   float resultado;           /* variable real */

Según el punto del programa donde se declaran, las variables pueden ser locales, globales o parámetros formales.

  • Variables locales, variables dinámicas o variables automáticas (auto)
    • Se declaran dentro de una bloque de código (función)
    • La declaración debe situarse al comienzo de la función o bloque de código, antes de realizar cualquier otra operación
    • Sólo son válidas dentro de ese bloque de código
    • Desaparecen cuando se finaliza la ejecución de ese bloque de código
    • Si el bloque de código se ejecuta varias veces, en cada ocasión la variable es creada al inicio y destruida al finalizar
    • Hasta que se inicializan, contienen valores “basura”
    • Se almacenan en una zona de memoria que funciona como memoria pila (LIFO-Last Input First Output; último en entrar, primero en salir)
  • Variables globales
    • Se declaran fuera de cualquier función
    • Permanecen activas durante todo el programa
    • Se almacenan en una zona fija de memoria establecida por el compilador
    • Pueden ser utilizadas en cualquier función a partir del punto de definición. Cualquier sentencia de tales funciones puede operar con ellas sin restricciones
    • Pueden estar definidas en otro fichero, en cuyo caso deben definirse con el modificador extern en el fichero en que se utilicen
    • Al definirse, el compilador las inicia a cero
    • No se aconseja su uso, salvo cuando sea imprescindible ya que:
      • Hacen las funciones menos portátiles
      • Ocupan la memoria permanentemente
      • Provocan fácilmente errores
  • Parámetros formales
    • Son variables que reciben los valores que se pasan a la función
    • Son siempre locales a la propia función
    • Se declaran en la línea de nombre de la función
    • Ejemplo:
  long int Mifuncion(int base, int exponente)
  {
     /* Cuerpo de la función */
  }
Inicialización de variables

La inicialización de variables sirve para asignar el primer valor

  • Por omisión:
    • Las variables globales se inicializan a 0
    • Las variables locales adquieren el valor de lo que haya en la memoria donde se almacenan (puede ser basura)
  • Puede realizarse en la misma declaración y se realiza mediante un operador de asignación:
  tipodato nombrevariable = valorinicial;
  • Ejemplo:
  unsigned int edad = 25;

Otros modificadores de datos

  • Modificadores de tipo de acceso: Complementan la declaración de una variable para cambiar la forma en la que se acceden o modifican las variables
    • const
      • Define una variable como constante, que no podrá ser modificada durante la ejecución del programa.
    • volatile
      • Crea una variable cuyo contenido puede cambiar, incluso por medios ajenos al programa
    • Ejemplo:
  unsigned int const year = 2018;
  • Modificadores de tipo de almacenamiento: Permiten indicar al compilador el modo de almacenamiento de la variable
    • extern
      • Declara una variable que ha sido definida en un archivo diferente al de la función (ya tienen memoria asignada)
    • static (dentro de una función)
      • Declara una variable local que mantiene su valor entre llamadas.
    • static (fuera de una función)
      • Declara una variable global privada del fichero en que se define
    • register
      • Indica al compilador que la variable debe ser almacenada en un lugar en el que se optimice el tiempo de acceso a ella (preferiblemente en un registro de la CPU)
    • auto
      • Declara una variable local a una función o a un bloque de código (es la opción por omisión)

Conversión de tipos

En las expresiones, los operandos cambian de tipo automáticamente

  • Si intervienen operandos reales, se unifican los tipos al de mayor precisión
  • Las constantes reales son tipo double por omisión
  • Los char y short se convierten a int si el int puede representar todos los valores del tipo original, o a unsigned int en caso contrario
  • Si intervienen operando enteros, se unifican los tipos al de mayor longitud
  • Ejemplo:
  long a
  char b;
  int c, f;
  float d;
  f = a + b * c / d;
    • b se convierte al tipo de c (int) y se realiza b*c.Se obtiene un int
    • El int b*c se convierte a float y se divide entre d. Se obtiene un float
    • a se convierte a float y se suma a b*c/d. Se obtiene un float
    • El float resultante de a+b*c/d se convierte a int (eliminando la parte fraccionaria) y se guarda en la variable entera f

Conversión explícita: operador cast

  • Consiste en convertir el tipo de dato de una variable o de una expresión
  • Sólo sirve para la evaluación de la expresión donde se realiza la conversión
  • Sintaxis:
  (tiponuevo)expresion;
    • tiponuevo es el tipo de dato al que se convertirá expresion
  • Ejemplo:
    • La expresión 7/2 da como resultado 3, sin embargo la expresión (float)7/2 convierte el 7 en real y el resultado será un número real: 3.5

Vectores

Un vector es un tipo de variable especial que permite almacenar un conjunto de datos del mismo tipo

  • Los elementos de un vector pueden ser referenciados de forma independiente mediante el nombre del vector seguido por los índices necesarios entre corchetes «[]»
    • El elemento que ocupa la primera posición tiene como índice cero.
    • El índice puede representarse con cualquier expresión cuyo resultado sea un número entero positivo
  • Pueden ser:
    • Unidimensionales
    • Multidimensionales
  • Los elementos de un vector se almacenan en posiciones consecutivas de memoria (el primer elemento en la dirección más baja)
  • En lenguaje C, los límites en la comprobación del tamaño y dimensiones de los vectores es responsabilidad del programador
Declaración de vectores

La declaración de un vector supone reservar memoria para sus elementos

  • Declaración de vectores unidimensionales
  tipodato nombrearray[tamaño];
    • tipodato representa el tipo de datos de los elementos del vector (cualquiera excepto void)
    • nombrearray es el identificador del vector y de sus elementos
    • tamaño representa un valor entero y constante que indica el número de elementos del vector
      • Si el número de elementos es n, el primer elemento es el nombrearray[0] y el último es nombrearray[n-1]
      • Puede no indicarse el tamaño si se inicializan los valores en la declaración o si está declarado ya en otro punto del programa (parámetros de una función)
    • La cantidad de memoria que se asigna a un vector es:
  num_bytes = tamaño * sizeof(tipodato)
  • Declaración de un vector multidimensional
  tipodato nombrearray[tamaño1][tamaño2]...[tamañoN];
    • tamaño1, tamaño2, tamañoN son expresiones constantes enteras
    • El número de ellas determina el número de dimensiones del vector
    • Cada una determinan el tamaño de cada dimensión
    • Cada dimensión necesita un índice para hacer referencia a una posición
  • Los elementos del vector se almacenan de forma consecutiva, siendo el índice más a la derecha el que más rápido cambia
    • La cantidad de memoria que se reserva para el vector es:
  num_bytes = tamaño1 * tamaño2 * ... * tamañoN * sizeof(tipodato)
  • Los vectores bidimensionales se llaman también tablas y sus dos dimenisones se llaman filas y columnas:
  tipodato nombrearray[numfilas][numcolumnas];
    • numfilas indica el número de filas
    • numcolumnas indica el número de columnas
    • Los elementos de una tabla se almacenan en memoria de forma consecutiva por filas.
  • Ejemplos:
  int lista[10];       /* Vector de 10 enteros */
  char vocales[5];     /* Vector de 5 letras */
  float matriz[6][4];  /* Tabla de 6 filas y  5 columnas de */
                       /* números reales */
Inicialización de vectores

Cuando se declara un vector sólo se inicializa con cero si se trata de una variable global. En caso contrario su contenido inicial será basura

  • Forma general de inicialización de un vector en su declaración:
  tipodato nombrearray[tam1]...[tamN]={listavalores};
    • listavalores es una relación de constantes del tipo declarado para el vector entre llaves «{}» y separadas por comas
  • Si un vector se inicializa en su declaración, se permite omitir el tamaño de su primera dimensión (la más a la izquierda: tam1):
    • En los unidimensionales:
  tipodato nombrearray[] = {listavalores};
  • En los multidimensionales:
  tipodato nombrearray[][tam2]..[tamN]={listavalores};
  • Al inicializar un vector en su declaración debe recordarse que el índice que cambia más rápido es el de la derecha * Cuando se inicializa un vector en su declaración, se ha de inicializar completo:
  int digit [10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
  int impares[] = {1, 3, 5, 7, 9};
  int matriz[3][4] = { s00, 01, 02, 03,
                       10, 11, 12, 13,
                       20, 21, 22, 23};
  char letras[][5] = { ‘a’, ’b’, ’c’, ’d’, ’e’,
                       ’f’, ’g’, ’h’, ’i’, ’j’,
                       ’k’, ’l’, ’m’, ’n’, ’o’,
                       ’p’, ’q’, ’r’, ’s’, ’t’,
                       ’u’, ’v’, ’x’, ’y’, ’z’};
  • La inicialización de un vector después de su declaración (en tiempo de ejecución) requiere la programación de un bucle por cada una de las dimensiones del vector.
  • Para inicializar una matriz de FILxCOL enteros el código necesario será similar la siguiente:
  int matriz[FIL][COL], f, c; /* Declaraciones */
  for(f=0 ; f<FIL ; f++)
     for(c=0 ; c<COL ; c++) {
         printf("Introduce el dato [%d][%d]", f, c);
         scanf("%d", &matriz[f][c]);
     }

Cadenas

Una cadena de caracteres, string o cadena es un vector unidimensional en el que todos sus elementos son de tipo char y el último elemento es el carácter nulo «('0')»

  char nombrecadena[longcad];
  • nombrecadena es un identificador válido para la cadena completa
  • longcad es una constante entera que indica el número de elementos de la cadena incluido el nulo final.

Inicializaciones en la declaración

  char nombrecadena[longcad]= "cadena";
  char nombrecadena[longcad]= {listacaracteres};
  • nombrecadena es el identificador de la cadena
  • longcad es una constante entera que determina el número de caracteres de la cadena incluido el carácter nulo (si es menor producirá error y si es mayor se rellenarán con “nulos”)
  • listacaracteres representa un conjunto de constantes de carácter (entre comillas simples «' '»y separados por comas) que deberá incluir el nulo '0' al final
  • El carácter nulo final es un limitador acordado
  • Los caracteres de una cadena pueden ser accedidos como elementos de un vector ordinario
  • Ejemplos de inicialización en la declaración:
  char nombre[6]= "Pedro";
  char apellido[]={'R', 'u', 'a', 'n', 'o', ''};
  • Las cadenas no son un tipo de dato
    • Su inicialización fuera de la declaración requeriría un bucle
    • Existen muchas funciones que facilitan el trabajo con las cadenas o “strings”
  • La mayoría de las funciones que facilitan las operaciones con cadenas se encuentran declaradas en:
    • stdio.h
    • stdlib.h
    • string.h
Funciones que utilizan las cadenas de caracteres
  • scanf() permite leer una cadena de caracteres desde el teclado, con las siguientes condiciones:
    • scanf(“%[^n]s”,cadena); nos permite leer una cadena completa hasta pulsar return ('n')
    • cadena, sin corchetes y sin que vaya precedido por el operador «&», es el identificador de la cadena
    • Es conveniente utilizar fflush(stdin) previamente para limpiar el buffer de entrada para eliminar los caracteres no recogidos
  • printf() permite, utilizando el especificador de formato %s, imprimir cadenas de caracteres
  • gets() lee una cadena completa y sustituye el return por el caracter nulo al almacenarla
  • puts() escribe una cadena completa, sustituyendo previamente el caracter nulo por el return
  • strcat(cadena1,cadena2) concatena cadena2 al final de cadena1
  • strcpy(cadena1,cadena2); copia cadena2 en cadena1
  • strcmp(cadena1,cadena2); compara cadena1 y cadena2
  • strlen(cadena); devuelve la longitud de la cadena
  • strlwr(cadena); convierte los caracteres de cadena a minúsculas
  • strupr(cadena); convierte los caracteres de cadena a mayúsculas
  • atof(cadena); convierte la cadena a un decimal en doble precisión equivalente al representado por los dígitos que cadena contiene
  • atoi(cadena); convierte la cadena a un entero
  • atol(cadena); convierte la cadena a un entero largo
  • fcvt(); convierte un número real a una cadena de caracteres.
Vectores de cadenas de caracteres

Un vector de cadenas es un vector bidimensional en el que el índice izquierdo señala el número de cadena y el índice derecho la longitud máxima de las cadenas

  char nombrearray[numcad][longcad];
  • Ejemplo:
  char frases[3][80]= {"Error de lectura",
                       "Error de escritura",
                       "Error de acceso" };
  /* frases[0] representa la cadena "Error de lectura"
     y puede mostrarse en pantalla escribiendo puts(frases[0]); */
  /* La longitud de 80 caracteres nos asegura que quepan todas las frases,
     aunque en algunas se desaproveche la memoria */

Estructuras

Una estructura es un tipo de variable especial que permite almacenar datos de tipos diferentes con un identificador común

  • Las variables que forman parte de la estructura se llaman elementos de la estructura
  • Definir una estructura consiste en crear el tipo de estructura:
  struct nombretipoestructura
  {
     tipodato1 nombrelemento1;
     tipodato2 nombrelemento2;
     ...
     tipodatoN nombrelementoN;
  };
  • La definición de un tipo de estructura no crea ninguna variable ni ocupa memoria
  • Declarar una estructura consiste en crear una variable de un tipo determinado de estructura:
  struct nombretipoestructura variablestructura;
  • Se pueden declarar variables de estructura en la misma sentencia que la declaración:
  struct nombretipoestructura
  {
     tipodato1 nombrelemento1;
     tipodato2 nombrelemento2;
     ...
     tipodatoN nombrelementoN;
  } listavariablestructura;
    • nombretipoestructura es el identificador del tipo de estructura que se está definiendo
    • tipodatoX representa al diferente tipo de dato de cada elemento
    • nombrelementoX son los identificadores propios de los elementos de la estructura
    • listavariablestructura representa al identificador (o una lista de estos) de la variable de estructura que se va a crear
  • La definición de las estructuras se suele hacer fuera de la función main() en los archivos cabecera .h
  • Los elementos de una estructura se almacenan en posiciones consecutivas de memoria (¡no del todo cierto!)
  • La cantidad de memoria ocupada por una variable estructura (más o menos las suma de la ocupada por sus elementos), puede obtenerse con el operador sizeof()
  • Ejemplos:
  struct Militar    /* Tipo de estructura */
  {
      char nombre[40];
      char apellidos[80];
      unsigned edad;
      float estatura;
      unsigned long telefono;
  } cabo, sargento, teniente;
 
  struct Militar capitan;    /* cabo, sargento, teniente y */
                             /* capitan son variables      */
                             /* estructuras del tipo militar */
  • Salvo la copia de una variable estructura en otra del mismo tipo (mediante el operador de asignación) no se pueden realizar operaciones con estructuras, deben realizarse con sus elementos y por separado
  • La referencia a un elemento de una estructura se hace mediante las etiquetas de la estructura y del elemento unidas por el operador punto «.»
  variablestructura.nombrelemento
  • * variablestructura es el nombre de la variable de estructura en la que se encuentra el elemento que se quiere referenciar
    • nombrelemento es el nombre del elemento de la estructura
    • El operador punto «.» une los dos identificadores en uno solo
    • En caso de anidación de estructuras, el operador punto aparecerá entre identificadores sucesivos
  • La dirección en memoria de un elemento de una estructura se obtiene aplicando el operador de dirección a la referencia a ese elemento
  &variablestructura.nombrelemento
  • Una variable de estructura se asimila a una ficha de una base de datos; los elementos de la estructura son los campos de la ficha.
  • Ejemplos:
  /* Inicialización de algunos campos de la variable cabo
     de tipo estructura militar */
  gets(cabo.nombre);
  cabo.telefono = 916830106;
  scanf("%f", &cabo.estatura);

Uniones

Constituyen una porción de memoria compartida por variables de diferentes tipos

  • Es una forma de interpretar los mismos datos de diferente manera
  • Se definen y declaranigual que las estructuras cambiando la palabra reservada struct por union
  • La referencia a los elementos de una unión se hace con el operador punto «.», del mismo modo que en las estructuras
  • Definición de un tipo unión:
  union nombretipounion
  {
    tipodato1 nombrelemento1;
    tipodato2 nombrelemento2;
    ...
    tipodatoN nombrelementoN;
  };
  • Creación de una variable unión de un tipo previamente definido:
  union nombretipounion variableunion;
  • La cantidad de memoria necesaria para almacenar una unión es la misma que la que ocupa el elemento de mayor tamaño
  • Es responsabilidad del programador conocer el dato almacenado en la variable de tipo unión
  • Ejemplos:
  union Talla
  {
    int numero;         /* 38, 40, 42 */
    char letra;         /* P, M, G */
    char letras[4];     /* L, XL, XXL */
  } camiseta, camisa, jersey;
 
  camiseta.numero = 44;
  scanf("%c",&camiseta.letra);
  gets(camiseta.letras);
 
  /* Primero se almacena el entero 44, después se almacena la letra leída con
     scanf() y al final se guarda una cadena de hasta 3 caracteres (más el nulo).
     Los anteriores se pierden */
  /* Podría utilizarse para guardar la talla en un formato diferente
     en cada variable */

Campos de bits

Un campo de bits es un tipo especial de elemento de una estructura en el que se puede definir su tamaño en bits

  • Definición de un campo de bits
  tipodato nombrecampo:longitud;
    • tipodato es el tipo del dato, que sólo puede ser entero (int, signed, unsigned, char, short, long, …)
    • nombrecampo representa el nombre del elemento que va a ser un campo de bits
    • longitud representa a un entero positivo que indica el número de bits de ese campo
  • En una estructura se pueden declarar elementos ordinarios o campos de bits, indistintamente
  • Restricciones en los campos de bits:
    • Su almacenamiento en memoria, depende de la máquina y del compilador
    • No se puede obtener la dirección en memoria de un campo de bits
    • Su tamaño no debe exceder del tamaño de un entero
  • Ejemplo:
  struct Campobit{
            int entero;
            unsigned sietebits:7;
            char letra
         } trescampos;
  trescampos.sietebits = dato;
  • Los campos de bits:
    • Facilitan las operaciones a nivel de bits
    • Facilitan el almacenamiento de variables lógicas (tipo boolean)
    • Aumenta el número de operaciones de la CPU
    • No suponen un ahorro de memoria importante

Definición de tipos con typedef

La expresión typedef permite dar un nombre particular a cualquier tipo de dato válido

  • Sintaxis:
  typedef tipodatovalido nuevonombre;
    • tipodatovalido representa un tipo de dato válido
    • nuevonombre es el nuevo identificador para ese tipo de dato
  • Ejemplo:
  typedef struct Militar  {
                          ...
                          } Midato;
  Midato soldado, cabo, sargento;

Un puntero es una variable que contiene la dirección de memoria de otra variable

  • Por eso se dice que un puntero apunta a una variable
  • Se trata de una indirección: se puede acceder a la variable indirectamente

Los punteros constituyen una importante herramienta en lenguaje C

  • Proporcionan acceso rápido y eficiente a los vectores
  • Facilitan el trabajo con listas enlazadas * Facilitan el intercambio de información entre funciones
  • Son imprescindibles para:
    • Asignar memoria dinámicamente
    • El trabajo con archivos
  • En el manejo de los punteros hay que extremar las precauciones para evitar errores de muy difícil localización
  • Ejemplo:
Memoria de datos
Etiqueta Dirección Contenido
dato1 0x00000F00 1234
dato2 0x00000F04 6789
puntdat1 0x00000F08 4321
puntdat2 0x00000F0C 7500

Declaración e inicialización

  • La declaración de una variable puntero asigna la memoria necesaria para almacenar una dirección:
  tipodato *nombrepuntero;
    • tipodato es el tipo de dato de la variable a la que apuntará el puntero.
    • nombrepuntero es la etiqueta de la zona de memoria que contendrá la dirección de una variable
    • *nombrepuntero hace referencia al contenido de la variable apuntada por el puntero
    • No se crea ni se reserva memoria para la variable a la que va a apuntar
  • El tamaño de la memoria necesaria para almacenar una dirección es siempre el mismo, independientemente del tipo de dato al que corresponda esa dirección
  • La Inicialización de un puntero consiste en hacerle apuntar a una variable válida
    • La variable debe existir antes de la inicialización
    • Que la variable exista no significa que deba contener un dato válido
  tipodato *punt;   /* Declaración del puntero*/
  tipodato var;     /* Declaración de la variable */
  punt = &var;      /* Inicialización del puntero */
  /* La variable var no contiene todavía ningún dato válido */
  • Ejemplos:
  float *punt;   /* Puntero */
  float var;     /* Variable */
  /* La variable y el puntero deben ser el mismo tipo de dato */
 
  punt = &var;   /* Inicialización del puntero */
  *punt = 7.98;  /* Inicialización de la variable */
                 /* Equivalente a lo anterior:
  var = 7.98;    */
  • * Una declaración de la forma int *punt=25; hace que el puntero apunte a la dirección 25 (y no sabemos qué contiene)
Operadores de memoria

Los operadores de punteros son «*» y «&», se asocian de derecha a izquierda y tienen mayor precedencia que todas las operaciones aritméticas y lógicas

  • Operador de dirección «&»
    • Aplicado a un identificador (a su derecha) obtiene la dirección de memoria de su operando o variable correspondiente
    • Sólo puede aplicarse a identificadores de variables y elementos de vectores, no es válida sobre expresiones
  • Operador de indirección «*»
    • Cuando precede a un identificador convierte al identificador en una dirección de memoria y el conjunto *identificador hace referencia al dato contenido por la dirección identificador
    • Sólo tiene sentido sobre variables que contienen una dirección de memoria válida (punteros)
    • Aplicado a una variable puntero permite utilizar esa expresión como una variable cualquiera, sin limitaciones
  • Operador/funcion «sizeof»
    • Aplicado a un operando nos devuelve el número de bytes que el operando ocupa en memoria

Operaciones

  • Las operaciones con punteros son operaciones con direcciones de memoria
    • Sólo tienen sentido la suma, la resta, incrementos y decrementos
  • Operaciones de asignación
    • Es posible asignar el contenido de un puntero a otro puntero
    • Lo que se consigue es hacer que ambos punteros apunten a la misma posición de memoria
    • Deben ser del mismo tipo
    • Ejemplo:
  int dato, *punt1, *punt2;  /* Declaraciones */
  punt1 = &dato;             /* Inicialización de punt1 */
  punt2 = punt1;             /* asignación del contenido de punt1 a punt2.
                                Ahora punt2 apunta a la variable dato */
Aritmética con punteros
  • La suma de un número entero n a un puntero hace que este apunte al n-ésimo elemento del mismo tipo a partir del originalmente apuntado.
  • La resta de un número entero n a un puntero hace que este apunte al elemento n-veces anterior al que apuntaba
    • Independientemente de que el dato al que se traslada el puntero exista o no.
  • Los incrementos o decrementos de un puntero hacen que este apunte al elemento siguiente o anterior del mismo tipo.
    • En valor numérico absoluto, el incremento o decremento de un puntero cambia su valor un número de unidades que depende del tipo de dato al que apunta
    • La aritmética de punteros sólo coincide con la aritmética ordinaria cuando se opera con punteros que apunten a elementos de tipo byte
  • Son posibles las comparaciones y las operaciones lógicas con punteros siempre que se realicen entre punteros del mismo tipo de dato
Tipos de punteros
  • Puntero genérico es aquel que no apunta a ningún tipo de dato
  void *nombrepuntero;
    • Se declaran así para que posteriormente se les pueda hacer apuntar a cualquier tipo de dato
  • Puntero nulo es aquel que apunta a la dirección NULL (= 0)
  tipodato *nombrepuntero = NULL;
    • NULL es una constante definida en stdio.h
    • Se utiliza porque la dirección 0 nunca es válida
  • Puntero constante es aquel que se declara como tal y, por tanto, siempre apunta a la misma posición
  tipodato *const nombrepuntero;
    • El contenido, el dato apuntado, si puede cambiar
    • Si es el dato lo que se declara como constante se escribe:
  const tipodato *nombrepuntero;
    • Si el puntero y el dato al que apunta se declaran constantes:
  const tipodato *const nombrepuntero;

Relación entre punteros y vectores

En general, todo lo que puede hacerse con vectores puede hacerse también con punteros, si bien, las versiones con punteros son más rápidas y más utilizadas

  • El identificador de un vector (sin índice) es un puntero constante al primer elemento del vector
  • En un vector de N elementos, se puede acceder al elemento M (tal que 0≤M<N:
  /* Mediante vectores: */
  elementoM = nombrearray[M];
  /* Mediante punteros: */
  elementoM = *(nombrearray+M);
  • Un puntero a un vector de caracteres (o cadena) es un puntero al carácter primero de la cadena
    • Se puede inicializar en la declaración:
  char *nombrepuntero = "cadena";
    • nombrepuntero es un puntero a carácter: contiene la dirección del primer elemento de la cadena
      • Si en el transcurso del programa, se le hace apuntar a otro sitio, ya no será posible volver a apuntar a la posición original
    • cadena es una cadena constante que se almacena en una tabla de constantes. Finaliza con el carácter nulo '0'
  • Cuando a una función se le pasa como argumento una constante de cadena (cadena de caracteres entre comillas dobles), en realidad se le pasa un puntero al primer elemento de esa cadena
  char *mensaje = "Error de lectura";
  puts(mensaje);

Un vector de punteros contiene elementos que son direcciones a elementos un tipo determinado

  • Se declara del siguiente modo:
  tipodato *nombrevariable[tamaño];
    • tipodato es el tipo de datos de los elementos que serán apuntados
    • nombrevariable es el nombre del vector de punteros
    • tamaño indica el número de punteros que contendrá el vector
  • El vector de punteros contiene elementos que son direcciones a elementos de tipo tipodato
    • Cada dirección apuntará a un dato del tipo declarado
    • Es preciso inicializar todos los punteros del vector (hacerles apuntar a un tipo de dato válido)
  • Un vector de punteros a carácter es similar a un vector bidimensional o a un vector de cadenas

  • Ejemplo de un vector bidimensional de caracteres:
  char mens[3][80] = {"Inicial", "Central", "Último"};
  /* Vector de 3 cadenas. Se reserva más memoria de la necesaria para que quepan todos los  mensajes */
  puts(mens[1]);    /* Saca el mensaje "Central" */
  • Ejemplo de vector de punteros a carácter:
  char *mens[3];          /* Vector de 3 punteros a carácter */
  mens[0]= "Inicial";     /* Inicialización de punteros */
  mens[1]= "Central";
  mens[2]= "Último";
  puts(mens[1]);          /* Saca el mensaje "Central" */

Puntero a un puntero

Un puntero a puntero representa una indirección múltiple: el primer puntero contiene la dirección del segundo puntero, el cual apunta al dato

  • Se declara del siguiente modo:
  tipodato **nombrepuntero;
    • nombrepuntero contiene la dirección de *nombrepuntero que, a su vez, contiene la dirección de nombrepuntero * nombrepuntero es el identificador del dato
  • Un puntero a puntero es equivalente al nombre de un vector bidimensional
  • Si datos[M][N] es un vector bidimensional, el elemento datos[i][j] puede accederse también mediante la aritmética de punteros: *(*(datos+i)+j)

false

Declaraciones complejas

La combinación de:

  • El operador puntero a «*»
  • Los corchetes indicadores de vector «[]»
  • Los paréntesis «()» que agrupan operaciones o los indicadores de función

dan lugar a declaraciones complejas y difíciles de descifrar

Para interpretar correctamente las declaraciones:

  1. Se debe comenzar con el identificador, mirando a su derecha
    • Los paréntesis indicarán que es una función
    • Los corchetes indicarán que es un vector
  2. Mirar si a la izquierda hay un asterisco, lo que indicará que es un puntero
  3. Aplicar las reglas anteriores a cada nivel de paréntesis y de dentro hacia fuera
  • Ejemplos
  int (*lista)[20];       /* lista es puntero a vector de 20 enteros */
  char *datos[20];        /* datos es vector de 20 punteros a carácter */
  void (*busc)();         /* busc es puntero a función que no devuelve nada */
  char (*(*Func())[])();  /* Func es función que devuelve un puntero a vector de
                             punteros a funciones que devuelven un carácter */
  int (*(*tim[5])())[3];  /* tim es vector de 5 punteros a funciones que devuelven
                             cada una un puntero a un vector de 3 enteros */

En la declaración de variables, el compilador de C reserva memoria para cada variable:

  • Si son globales, en la zona de datos asociada al propio programa
  • Si son locales, en la zona de pila (stack)

La asignación dinámica de memoria consiste en la reserva de memoria para los datos en tiempo de ejecución, es decir, tomándola de la que el sistema tiene libre en ese momento.

Un programa en ejecución es un proceso activo que tiene a su disposición:

  • El tiempo de utilización del procesador
  • La memoria que le asignó el sistema operativo:
    • Segmento de código
    • Segmento de datos (variables globales)
    • Segmento de pila
      • Variables locales
      • Direcciones de regreso de las llamadas a funciones

Asignación y liberación

  • En tiempo de ejecución es posible pedir memoria al sistema operativo:
  void *malloc(unsigned tamanio);
    • tamanio es un entero sin signo que indica el número de bytes que se solicitan al sistema operativo
    • La función devuelve un puntero genérico que apunta a la dirección de comienzo del bloque de memoria asignado. En caso de error devuelve un puntero nulo (NULL) *
      • En el prototipo se indica que devuelve un puntero de tipo void que puede apuntar a cualquier tipo válido
  • Está declarada en stdlib.h
  • Tras utilizar la memoria asignada dinámicamente, hay que devolverla al sistema operativo, liberándola de nuestro programa:
  void free(void *nombrepuntero);
    • nombrepuntero es el identificador del puntero (de cualquier tipo) que apunta al bloque de memoria que se desea liberar
    • La función no devuelve nada
  • Está declarada en stdlib.h
  • Ejemplo de asignación dinámica de memoria para un entero y liberación posterior:
  int *dato;  /* Puntero a int */
  dato = (int *)malloc(sizeof(int));
  if (dato==NULL)
     printf("Error en la asignación");
     /* ...
        Operaciones de utilización de la memoria asignada
        ...
     */
  free(dato); /* Liberación de la memoria asignada  dinámicamente */

Vectores dinámicos

Los vectores dinámicos son aquellos cuyo tamaño se determina en un proceso de asignación dinámica de memoria

  • Vectores dinámicos unidimensionales:
  void * calloc(numelementos, tamañoelemento);
    • Devuelve un puntero convertible a cualquier tipo de dato que apunta a la primera posición del bloque de memoria asignado (o puntero nulo en caso de error)
    • numelementos es un entero sin signo que representa el número de elementos del vector
    • tamañoelemento es un entero sin signo que representa el tamaño de un elemento del vector
  • Está declarada en stdlib.h

* Ejemplo de reserva de memoria para un vector de 10 elementos de tipo double:

  punt = (double*)calloc(10, sizeof(double));
  • Vectores dinámicos bidimensionales:
    1. Debemos crear un puntero a puntero al tipo de datos del vector bidimensional
    2. Debe asignarse dinámicamente un vector de punteros
    3. Debe asignarse dinámicamente un vector para datos a cada uno de los punteros del vector anterior
    4. Tras su utilización, debe liberarse la memoria en orden inverso al orden en el que fue asignada:
      • Primero, mediante un bucle, se libera cada uno de los vectores unidimensionales del tipo de dato utilizado
      • Después, mediante otro bucle, se libera el vector de punteros

  • Ejemplo de asignación dinámica de memoria para un vector unidimensional de N enteros y liberación posterior:
  int *lista;    /* Puntero a int */
  lista = (int *)calloc(N, sizeof(int));
  if (lista==NULL)
     printf("Error en la asignación");
     /* ...
        Operaciones de utilización de la memoria asignada
        ...
     */
  free(lista); /* Liberación de la memoria asignada  dinámicamente */
  • Ejemplo de reserva dinámica de memoria para un vector bidimensional de números reales (matriz de NFIL filas y NCOL columnas) y su liberación posterior:
  float **punt;  /* Puntero a puntero a float */
  int i,j;
  punt = (float **)calloc(NFIL , sizeof(float *))
                 /* Vector de NFIL punteros a float */
  for (i=0 ; i<NFIL ; i++;)
     punt[i] = (float *)calloc(NCOL , sizeof(float));
                 /* Se dispone de NFIL vectores de NCOL números reales.
                    Los datos pueden ser referenciados mediante
                    expresiones del tipo punt[i][j] */
  for (i=0 ; i<NFIL ; i++) free(punt[i]);
                 /* Se liberan los vectores unidimensionales
                    (las filas //  de la matriz de números reales) */
  free(punt);    /* Se libera la memoria del vector unidimensional de
                    punteros a float */

Reasignación de bloques de memoria

Es posible reasignar un bloque de memoria previamente asignado dinámicamente (para redimensionar ese bloque)

  void * realloc(void *puntbloc, numbytes);
    • puntbloc es un puntero que apunta al bloque de memoria que se quiere reasignar
    • numbytes es un número entero sin signo que indica el nuevo tamaño en bytes que deberá tener el bloque de memoria
  • Devuelve un puntero genérico al bloque de memoria asignado, cuya dirección de comienzo puede ser diferente de la dirección inicial. En caso de error devuelve un puntero nulo (NULL)
  • No se pierden los datos almacenados en el bloque de partida, se trasladan su ubicación
  • La función está definida en stdlib.h


  • pub/c_data.txt
  • Última modificación: 2020/09/28 11:44
  • (editor externo)