Logo de C

Lenguaje C

Laboratorio de Multimedia e Internet

Instructores

  • Carrillo Paniagua Yamil Emiliano
  • Correa Sam Said
  • Padilla Rodriguez Michelle
  • Pérez Jiménez Zoé Fernanda

Horario de clase

Inicia
9 de junio de 2025
Termina
13 de junio 2025
Horario
08 a 12 h

Evaluación

  • 10% - Participaciones
  • 20% - Tareas
  • 20% - Examen
  • 50% - Proyecto final
  • 8 para obtener constancia

Reglas

  • Se dan 5 minutos de tolerancia
  • En caso de presentar porblemas con la instalación del compilador, se les ayudará durante el receso de la primera clase

Objetivos

El alumno adquirirá los conocimientos necesarios para poder crear programas de dificultad básica e intermedia que trabajen sobre una línea de comandos, en el lenguaje de programación C.

Temario (I)

  1. Introduccion
  2. Variables
  3. Funciones (I)
  4. Operadores
  5. Sentencias
  6. Conversiones de tipo
  7. Cadenas de caracteres

Temario (II)

  1. Arrays
  2. Estructuras
  3. Punteros
  4. Archivos
  5. Uniones

Características

C es un lenguaje de programación...

Palabras reservadas

C tiene relativamente pocas: 32 en C89

  • auto
  • break
  • case
  • char
  • const
  • continue
  • default
  • do
  • double
  • else
  • enum
  • extern
  • float
  • for
  • goto
  • if
  • int
  • long
  • register
  • return
  • short
  • signed
  • sizeof
  • static
  • struct
  • switch
  • typedef
  • union
  • unsigned
  • void
  • volatile
  • while
  • C99 agregó 5 más:
    • inline
    • restrict
    • _Bool
    • _Complex
    • _Imaginary
  • Subsiguientes estándares incluyeron unas cuántas palabras más.
  • C no distingue con un sigilo palabras clave de valores definidos por el usuario.

Características

  • El acceso de bajo nivel a la memoria se logra convirtiendo direcciones de memoria a punteros.
  • El lenguaje define un preprocesador, para definir macros, inclusión de codigo, y compilación condicional.
  • Existe una modularización básica:
    • Compilación por separado, enlace juntos.
    • Control sobre miembros ocultos: atributos external y static.
  • Las funcionalidades no esenciales están disponibles en la biblioteca estándar.

Usos

  • Sistemas operativos y kernels
  • Controladores de dispositivo
    • Microcontroladores
  • Bibliotecas base
  • Pilas de protocolos

Ejemplos

Ejemplos de programas en C

Historia

Unix

  • La historia de C es también la historia de Unix.
  • Nuestros protagonistas: Dennis Ritchie y Ken Thompson.
Ken Thompson y Dennis Ritchie
Thompson y Ritchie en 1973
PDP-7
PDP-7
PDP-11
PDP-11

Lenguajes antecedentes: B

  • Thompson quería un lenguaje para poder escribir utilidades para el SO nuevo.
  • Desarrolló una versión reducida del lenguaje BCPL, a la que llamó B.
GET "libhdr"

LET start() = VALOF
{ writef("Hello world!")
  RESULTIS 0
}
main()
{
    putstr("Hello world!*n");
    return(0);
}

NB → C

  • Sin embargo, B no tenía tipos de datos más que la palabra manejada por el procesador.
  • Ademas, esta versión era muy lenta, por lo que prácticamente no se usó.
  • Ritchie se dedicó a mejorar su lenguaje, para usarlo en el desarrollo del kernel de Unix.
  • Después de muchas mejoras, por 1972, el lenguaje fue renombrado C.

K&R C

  • Para 1978, el lenguaje ya había adquirido muchas de las características que tiene hoy.
  • Kernighan y Ritchie decidieron publicar un libro describiendo el lenguaje: The C Progamming Language.
  • A la versión descrita se le conoce como K&R C.
Portada de The C Programming Language

ANSI C

  • Dada la creciente popularidad del lenguaje, diferentes compiladores empezaron a implementar versiones incompatibles.
  • En 1983, ANSI formó un comité para estandarizar el lenguaje.
    • Seis años después, el estándar fue publicado, por lo que a esta versión se le conoce como ANSI C o C89.

C95 y C99

  • Después de la estandarización de ANSI, la organización ISO estandarizó también el lenguaje, y desde entonces, es el encargado de mantener el estándar.
  • En 1995, ISO publicó una corrección menor, conocida como C95.
  • En 1999, se publicó un nuevo estándar, C99, que incluyó muchísimos cambios.
    • Es prácticamente compatible con C89, pero más estricto.

Más versiones

  • Desde entonces, ISO ha publicado tres revisiones más del estándar, y está en proceso otra:
    • C11, publicado en diciembre de 2011.
    • C17, publicado en junio de 2018.
    • C23, que se espera publicar este año.

¿Qué versión usaremos?

  • Trabajaremos con el estándar C99.
  • Es una versión con suficiente antigüedad para ser implementado bien por prácticamente todos los compiladores de C.
    • GCC y clang soportan todo el estándar.
    • Visual C++ empezó a soportar partes del lenguaje desde la versión 2013, y soporta la mayor parte de él desde la versión 2019.
  • Incluye ya suficientes características que lo hacen ser mas cómodo para trabajar.

Toolchain

  • El conjunto de utilidades y procedimientos necesarios para construir un programa, desde el código fuente hasta el binario final que el sistema operativo puede ejecutar.
  • En C (¡y en C++ también!), consta básicamente de tres pasos:
    • Preprocesado
    • Compilado
    • Enlazado

Conceptos básicos

Código fuente
La representación en texto de un programa, para uso del programador.
Código objeto
La representación en código máquina de un programa, que un procesador puede ejecutar directamente.
Ejecutable
Archivo que contiene el código objeto de un programa, sus dependencias, y demás archivos, en el formato que el SO entiende.

Preprocesado

  • La substitución de las directivas del preprocesador.
    • Las directivas son las instrucciones que comienzan por #: p. ej: #include, #define.
  • Estas directivas ayudan a facilitar la escritura de código C.
  • Después de este paso, estas directivas ya no estarán presentes.

Compilado

  • La traducción del código fuente a código máquina.
    • En este paso, el compilador analiza el código fuente, para generar el ensamblador equivalente.
    • Un error sintáctico cancelará este paso.
    • Muchas veces, el compilador también optimizará el código fuente.
  • Después de este paso, tendremos archivos con el código objeto.

Enlazado

  • El proceso de juntar todos los componentes del programa, parna formar al ejecutable final. ¿Qué componentes?
    • Códigos objeto de distintos procedimientos.
    • Códigos objeto de bibliotecas externas.
    • Archivos o blobs binarios.

¿Qué programas usaremos?

  • En general, usaremos lo que llamamos el GNU toolchain.
    • El uso de las herramientas de GNU para elaborar nuestros programas en C.
  • Las herramientas son las siguientes:
    GNU Binutils
    Entre otras cosas, el enlazador.
    GNU Compiler Collection (gcc)
    El compilador y preprocesador.
    GNU Debugger (gdb)
    El depurador.
    GNU make
    La utilidad para automatizar la compilación.
  • La otra herramienta será un analizador de memoria, Valgrind.

Entrada/salida en C

  • A diferencia de otros lenguajes de programación, las funciones de entrada/salida no son parte de la gramática del lenguaje.
  • En C, esas funciones se encuentran en una biblioteca de la biblioteca estándar, stdio.h.
  • Pero, ¿quién me dice qué es esto de entrada/salida (o E/S)?
    • ¡Es hora de conocer a la arquitectura Von Neumann!

Arquitectura de Von Neumann

Diagrama de la arquitectura

«Todo es un archivo»

  • El modelo de E/S de C se basa en el modelo de Unix.
    • Casi todos los recursos pueden ser utilizados como un archivo, incluso los dispositivos de entrada y salida.
      • Excepción: los procesos no son archivos, puesto que éstos son los que usan los archivos.
    • De un archivo se pueden leer (y escribir) flujos (o streams) de bytes.

Terminal

  • La terminal (o la consola, en Windows) es un componente del sistema que nos provee una interfaz de texto (o una CLI) para comunicarnos con un proceso/programa.
    • No confundir con una shell o un emulador de terminal.
  • Los programas que haremos en el curso tendrán asociados una terminal para poder usarlos.

Descriptores de archivos estándar

  • Cada proceso activo tiene tres archivos abiertos:
    stdin
    Entrada estándar, normalmente apunta al teclado, de solo lectura.
    stdout
    Salida estándar, normalmente apunta a la terminal, de solo escritura.
    stderr
    Salida de error estándar, normalmente también apunta a la terminal, de solo escritura.

Funciones más usadas

Las funciones de la cabecera stdio.h tienen funciones para leer y escribir en archivos.

gets
Lee una cadena de caracteres.
puts
Escribe una cadena de caracteres.
scanf
Lee una cadena de caracteres, según un formato determinado.
printf
Escribe una cadena de caracteres, según un formato determinado.

Formato de printf

La cadena es un texto, que puede tener dentro especificadores de formato:

%[bandera][ancho][.precisión][tamaño]tipo
Bandera
Modificadores varios.
Ancho
el número mínimo de caracteres a imprimir.
Precisión
El número máximo de caracteres a imprimir.
Tamaño
Para enteros, el tamaño del entero.
Tipo
El tipo de dato a leer.

Formato de scanf

La sintaxis es fundamentalmente la misma que la de printf.

Ejemplo:

"%7d%s %c%lf"

Ejemplo básico

Primer ejemplo

int main(void)
{
   int numero;

   numero = 2 + 2;
   return 0;
}

Para compilar, use el comando:

gcc ejemplo.c -o ejemplo

Segundo ejemplo

#include <stdio.h>

void main(void){
    printf("Hola Mundo en C");
    return 0;
}

Para compilar, use el comando:

gcc ejemplo.c -o ejemplo

Secciones de un ejecutable

Un ejecutable normalmente tiene estas secciones:

  • .text
  • .data
  • .bss
  • Heap
  • Pila
Diagrama

Errores

Fuga de memoria (memory leak)
Un programa que pide memoria al sistema y nunca la devuelve.
Violación de segmento (segmentation fault)
Un programa que intenta acceder a memoria a la que no tiene permiso.
Desbordamiento de segmento (buffer overflow)
Cuando un programa consume toda su memoria, y sigue escribiendo sobre más porciones de memoria.

Variables

¿Qué es una variable? La respuesta depende de quién pregunte:

  • Para un programador, es una entidad que puede tener un valor.
  • Para el compilador, es un alias para una dirección de memoria.

Declaración de variables

[cualificador de tipo] [especificador de almacenamiento] [especificador de tipo];

Especificadores de almacenamiento

Especifican la forma en la que se guardará una variable:

  • extern
  • static
  • auto
  • register

Especificadores de tipo

  • Como dijimos, C es un lenguaje tipado.
  • Las variables tienen tipos, que indican de qué forma debe interpretarse el valor que contienen.

Tipo char

  • Tipo entero con el tamaño mínimo direccionable.
  • Como mínimo, debe medir 8 bits.
  • Tipos:
    • char
    • signed char
    • unsigned char

Tipo short

  • Segundo tipo entero en tamaño.
  • Como mínimo, debe medir 16 bits.
  • Tipos:
    • short, short int, signed short, signed short int
    • unsigned short, unsigned short int

Tipo int

  • Tercer tipo entero en tamaño.
  • Como mínimo, debe medir 16 bits.
  • Tipos:
    • int, signed, signed int
    • unsigned, unsigned int

Tipo long

  • Cuarto tipo entero en tamaño.
  • Como mínimo, debe medir 32 bits.
  • Tipos:
    • long, long int, signed long, signed long int
    • unsigned long, unsigned long int

Tipo long long

  • Quinto tipo entero en tamaño.
  • Como mínimo, debe medir 64 bits.
  • Tipos:
    • long long, long long int, signed long long, signed long long int
    • unsigned long long, unsigned long long int

Tipos decimales

  • Existen tres tipos para almacenar tipos decimales.
    • Formato de punto flotante de precisión simple: float
      • Normalmente, mide 32 bits
    • Formato de punto flotante de precisión doble: double
      • Normalmente, mide 64 bits
    • Formato de punto flotante de precisión extendida: long double
      • Puede medir 80, 96 o 128 bits

Tipos complejos

  • Desde C99, podemos usar tipos para números imaginarios y complejos.
  • Tipos:
    • float _Complex
    • double _Complex
    • Long double _Complex

Ejemplos

Otros tipos

Existen algunos tipos más elaborados.

  • void
  • _Bool
  • struct
  • union
  • enum

enum

  • Tipo que permite crear constantes de tipo int, agrupadas bajo un identificador.
  • Sintaxis:
    enum identificador { constante [, constante]}
  • Al listado de constantes se le asigna un valor consecutivo, empezando por el valor 0.
    • Se le puede asignar un valor específico a una constante:
      enum Foo { A, B=14 }

Cualificadores de tipo

const
Hace que una variable sea de solo lectura.
restrict
Indica al compilador que un puntero no tendrá otros punteros apuntando a la misma dirección.
volatile
Indica que el valor de una variable puede cambiar en diferentes accesos.

Constantes

  • ¿Qué es una constante?
  • A veces, tenemos qué usar valores directamente, en vez de usar una variable.
    • ¿Cómo sabe el compilador de qué tipo es una constante?
    • ¿Cómo sabe el compilador en qué sistema de numeración está una constante?

Notación: sistemas de numeración

  • Decimales: 1234
  • Octales: 0377
  • Hexadecimales: 0xff

Notación: tipos de datos (I)

  • char: 'x'
  • signed char, unsigned char, short, unsigned short
  • int:
  • unsigned int: U
  • long: L

Notación: tipos de datos (II)

  • unsigned long: UL
  • long long: LL
  • unsigned long long: ULL
  • float: F
  • double:
  • long double: L

Funciones

  • Conjunto de instrucciones agrupadas, que puede llamarse múltiples veces.
  • Pueden admitir ciertos valores de entrada: parámetros.
  • Puede devolver un resultado: valor de retorno.

Declaración de funciones

  • Al igual que las variables, las funciones también deben ser declaradas.
    • A estas declaraciones las llamamos prototipos.
[extern|static] <tipo_valor_retorno> [modificadores] <identificador>(<lista_parámetros>);

Estructura de un programa

[directivas del pre-procesador: includes y defines]
[declaración de variables globales]
[prototipos de funciones]
función main
[definiciones de funciones]
  • Pregunta: ¿Por qué debo poner dos veces la firma de mi función? Al declaralo, y luego al definirlo.

Ejemplos

Expresión

  • ¿Qué es una expresión? Conjunto de operadores y operandos que pueden reducirse a un valor.
    • ¡Estas expresiones no son matemáticas, aunque se parezcan!
  • ¿Qué es un operador? Símbolo que representa una operación sobre cierta cantidad de valores.
  • ¿Qué es un operando? Elementos sobre los que se puede aplicar una operación.

Aridad

  • La cantidad de argumentos necesarios para una operación:
    Unaria
    Requiere un operando.
    Binaria
    Requiere dos operandos.
    Ternaria
    Requiere tres operandos.

Operadores aritméticos binarios

  • exp + exp (suma)
  • exp - exp (resta)
  • exp * exp (multiplicación)
  • exp / exp (división)
  • exp % exp (módulo)

Operadores aritméticos unarios

  • + exp (positivo)
  • - exp (negativo)
  • ++ var (preincremento)
  • var ++ (postincremento)
  • -- var (predecremento)
  • var -- (postdecremento)

Operadores de asignación

  • var = exp (asignación directa)
  • var op= exp (asignación con operación)
    • ¿Cómo asignación con operación? Es un atajo para ir "actualizando" una variable:
    • (var op= exp) == (var = var op exp).

Operador coma

  • El primer uso de la coma es el de separar los argumentos de una función.
  • Además, también se puede usar en expresiones de coma.
    • De la misma forma que el punto y coma separa sentencias, la coma separa expresiones.
    • Dentro de una expresión de coma, todas las expresiones son evaluadas, pero sus resultados son descartados, excepto el de la última expresión.
    • El valor de la "operación coma" es este último resultado.

Operadores de comparación

  • exp == exp (igual que)
  • exp != exp (distinto que)
  • exp > exp (mayor que)
  • exp < exp (menor que)
  • exp >= exp (mayor o igual que)
  • exp <= exp (menor o igual que)

Operadores lógicos

  • Son las operaciones básicas del álgebra booleana:
    • exp && exp (conjunción)
    • exp || exp (disyunción)
    • !exp (negación)
  • Cortocircuito: podemos aprovechar las propiedades de estas operaciones para, potencialmente, ahorrarnos pasos:
    • Para la conjunción, si el primer operando es falso, toda la expresión es falsa.
    • Para la disyunción, si el primer operando es verdadero, toda la expresión es verdadera.

Operadores de bits

  • ~exp (negación binaria)
  • exp & exp (conjunción binaria)
  • exp | exp (disyunción binaria)
  • exp ^ exp (disyunción exclusiva binaria)
  • exp << exp (desplazamiento a la izquierda)
  • exp >> exp (desplazamiento a la derecha)

Otros operadores

  • exp ? exp : exp (condicional ternario)
  • sizeof(exp) (tamaño en memoria)
  • var[exp] (subíndice)
  • *exp (indirección)
  • &var (dirección)
  • exp->m (dereferencia de estructura)
  • exp.m (referencia de estructura)

Precedencia (I)

OperadoresAsociatividad
++ (post), --, (), [], ., ->Izq-a-der
++ (pre), --, + (unario), -, !, ~, * (indirección), & (dirección), sizeofDer-a-izq
*, /, %Izq-a-der
+, -Izq-a-der
<<, >>Izq-a-der
<, <=, >, >=Izq-a-der

Precedencia (II)

OperadoresAsociatividad
==, !=Izq-a-der
&Izq-a-der
^Izq-a-der
|Izq-a-der
&&Izq-a-der
||Izq-a-der
?:, =, op=Der-a-izq
,Izq-a-der

Ejemplos

Sentencias

  • Una sentencia es un bloque de código que indica una instrucción a ejecutar.
  • Podemos agrupar varias sentencias, encerrándolas entre llaves, y formarán un bloque.
    • Los bloques forman dentro un ámbito.
    • Sintácticamente, un bloque es también una sentencia.

Expresiones

  • Las expresiones, si terminan en punto y coma, también son sentencias.
  • Esto no es extraño: las llamadas a funciones son también expresiones.
  • Aún así, hay qué tener cuidado de que nuestras expresiones tengan efectos secundarios para que sean válidas.

Sentencias de iteración

Bucle while (ejemplo)
while (condición) sentencia
Bucle do...while (ejemplo)
do sentencia while(condicion);
Bucle for (ejemplo)
for ( [inicialización]; [condición] ; [incremento] ) sentencia;

Sentencias de selección

Bloque if...else (ejemplo)
if (condición) sentencia1
  [else sentencia2]
Bloque switch (ejemplo)
switch (expresión entera)
{
   [case expresión_constante1: [sentencias1]]
   [case expresión_constante2: [sentencias2]]
   ...
   [case expresión_constanten: [sentenciasn]]
   [default : [sentencia]]
}

Sentencias de salto

Ruptura
break;
Siguiente iteración
continue;
Retorno
return expresión;

Etiquetas y goto

  • Durante el flujo del programa, podemos saltar directamente a cualquier otro punto dentro de la función en la que estemos.
    • Excepto si entramos en el ámbito de una VLA.
    • goto etiqueta;
  • Las etiquetas se declaran así:
    etiqueta:;
  • Usar goto está muy desaconsejado, puesto que va en contra del paradigma estructurado.
  • Ejemplo

Comentarios

  • Los comentarios no son sentencias, pero es el momento de introducirlos.
  • Existen dos tipos de comentarios:
    • Comentarios de línea: empiezan por //, y terminan cuando la línea termina.
    • Comentarios de bloque: empiezan por /*, y terminan con */.

Ámbito

  • Básicamente, el área temporal o espacial en la que una variable o función está disponible.
    • Al ámbito temporal le llamaremos tiempo de vida o lifetime.
    • Al ámbito espacial, visibilidad, linkage o visibility.
  • El ámbito es determinado por su tipo de almacenamiento.

Ámbito temporal

  • Existen los siguientes:
    Estático
    Disponibilidad durante todo el programa, alojado en la sección .data o .bss.
    Automático
    Disponibilidad durante el bloque, alojado en la pila.
    Manual
    Disponibilidad durante todo el programa, alojado en el heap.
  • Solo los dos primeros son manejados automáticamente por el compilador.

Ámbito espacial

  • Existen los siguientes:
    Externo
    Disponibilidad dentro y fuera de la unidad de compilación.
    Interno
    Disponibilidad dentro de la unidad de compilación.
    Bloque
    Disponibilidad dentro de un bloque, desde que es declarada.

Conversión de tipos

  • En general, solo podemos operar con variables del mismo tipo.
  • Pero, cuando es posible, si utilizamos dos variables de diferente tipo, el compilador los "emparejará": hablamos de la conversión implícita de tipos.
  • A veces, podemos nosotros forzar una conversión: hablamos de la conversión explícita de tipos, o casting.

Conversión implícita de tipos numéricos

  1. Cualquier tipo entero pequeño como char o short es convertido a int o unsigned int. En este punto cualquier pareja de operandos será int (con o sin signo), long, long long, double, float o long double.
  2. Después, si un operando es de tipo...
    • ...long double, el otro se convertirá a long double.
    • ...double, el otro se convertirá a double.
    • ...float, el otro se convertirá a float.
    • ...unsigned long long, el otro se convertirá a unsigned long long.
    • ...long long, el otro se convertirá a long long.
    • ...unsigned long, el otro se convertirá a unsigned long.
    • ...long, el otro se convertirá a long.
    • ...unsigned int, el otro se convertirá a unsigned int.
  3. Llegados a este punto ambos operandos son int.

Conversión implícita a booleanos

  • En realidad no es una conversión de tipos, sino de valor: en C no existe como tal un tipo booleano.
  • Esta regla de correspondencia puede aplicarse a cualquier expresión que se evalúe a un entero.
  • Un valor es tomado como falso si es 0, cualquier otro valor es asimilado como verdadero.
    • 0 == x equivale a !x.
    • 0 != x equivale a x.

Promoción y democión de tipos

  • Cuando trabajamos con valores y variables de distintos tipos, pueden ocurrir dos cosas:
    • Promoción de tipos
    • Democión de tipos
  • La democión puede afectar a un valor al ser asignado. ¿Qué asignaciones?
    • Al asignarlo a una variable
    • Al pasarlo como parámetro (donde habrá una asignación a una variable local)

Conversiones explícitas: casting

  • Hacer un casting indica que estamos conscientes de que puede haber una pérdida de precisión, y de qué tipo será el resultado de la expresión convertida.
  • El compilador nos detendrá ante cualquier intento de conversión "imposible".
  • Sintaxis:
    (tipo)expresión
    tipo(expresión)

Ejemplo de casting

char n;
int a, b, c;
float r, s, t;
c = (int)(r + b);
c = (int)(n + a + r);

Hacer un casting implica que sabemos que el resultado de estas operaciones no es un int, que la variable receptora sí lo es, y que lo que hacemos lo estamos haciendo a propósito.

Cadenas de caracteres

  • A diferencia de otros lenguajes, en C las cadenas de caracteres no son un tipo básico.
    • Una cadena es un array de chars, donde el último elemento del array contiene el valor 0.
    • Las constantes de cadena se colocan siempre entre comillas dobles (").
  • Ejemplos

Biblioteca estándar string.h

  • Archivo de cabecera de la biblioteca estándar de C.
  • Contiene funciones para tratar y manipular cadenas de caracteres.
  • En principio, soporta dos tipos de cadenas, según el tamaño de cada caracter:
    Byte strings
    Mide 1 byte por caracter.
    Wide strings
    Mide 2 bytes por caracter.
  • Nosotros siempre usaremos las del primer tipo.

Funciones de manipulación

strcpy
Copia una cadena en otra.
strncpy
Igual que el anterior, hasta el n-ésimo caracter.
strcat
Añade una cadena al final de otra.
strncat
Igual que el anterior, hasta el n-ésimo caracter.

Funciones de examinación

strlen
Devuelve el tamaño de la cadena.
strcmp
Compara dos cadenas de caracteres.
strncmp
Igual que el anterior, hasta el n-ésimo caracter.
strchr
Busca la primera ocurrencia de un caracter.
strrchr
Busca la última ocurrencia de un caracter.
strstr
Busca la primera ocurrencia de una subcadena.

Funciones de manipulación de memoria

memset
Rellena un buffer con un caracter.
memcopy
Copia un buffer a otra parte.
memmove
Igual que el anterior, pero tomando en cuenta la posibilidad de que los buffers se solapen.
memcmp
Compara dos buffers.
memchr
Busca la primera ocurrencia de un caracter.

Otros archivos de cabecera

ctype.h
Funciones de clasificación de caracteres.
locale.h
Funciones de localización.
stdlib.h
Algunas funciones de conversión de números.

Arrays

  • También llamados arreglos o vectores.
  • Un conjunto de datos del mismo tipo a los que se puede acceder individualmente mediante un índice.
  • Los elementos de un array se almacenan siempre en posiciones de memoria consecutivas.
  • Ejemplos

Declaración de arrays

  • Sintaxis:
    tipo identificador[núm_elemen][[núm_elemen]...];
  • El número de elementos del array siempre es constante, y no puede ser un valor determinado en tiempo de ejecución.
    • En C99 se introdujo la característica de Variable-Length Arrays, que posibilita esto.
    • Sin embargo, su implementación no es obligatoria, y no todos los compiladores lo implementan.

Inicialización de arrays (I)

  • Un array puede ser declarado con un tamaño, sin especificar su contenido:
    int array[núm_elems];
  • Si se desea, se puede especificar su contenido en la inicialización, encerrando el contenido entre llaves:
    int array[núm_elems] = { contenido }
    • El número de elementos del contenido debe coincidir con el indicado en la declaración del array.

Inicialización de arrays (II)

  • Si se inicializa el array en la declaración, no es necesario especificar el tamaño en la declaración, puesto que se puede calcular de los elementos a asignar:
    int array[] = { contenido }

Operaciones con arrays

  • Ya vimos que la primera operación que se puede hacer en un array es la de asignación.
  • La otra operación es sizeof, que devolverá el tamaño del array en bytes.
    • Aunque, dado que el tamaño de un array ya es conocido, puesto que es una constante, quizá esto no tenga mucha utilidad.

Estructuras

  • Las estructuras permiten agrupar datos, de cualquier tipo, que tengan relación semántica.
  • Tienen mucho parecido con las clases de POO, pero sin los métodos.
  • A cada dato dentro de una estructura se le llama campo.
  • Cuando tenemos un conjunto de estructuras, a cada una se le suele llamar registro.

Declaración de estructuras

  • Sintaxis:
    struct [identificador] {
       [tipo nombre_campo[,nombre_campo,...]];
    } [variable_estructura[,variable_estructura,...];
  • Tanto el identificador de la estructura como la variable estructura son opcionales, pero al menos uno de ellos debe existir para que sea posible utilizar la estructura.

Declaración de una variable de estructura

  • Una vez exista ya una estructura definida completamente, podemos empezar a crear variables con ese nuevo tipo:
    struct identificador variable_estructura
       [,variable_estructura...];
  • La estructura se puede inicializar aquí mismo, o más tarde.

Definición de una variable de estructura (I)

Existen tres formas de asignar valores a un registro.

Con una asignación
Podemos asignar individualmente a cada campo del registro su valor correspondiente, como si fuese una variable independiente.
struct identificador variable;
variable.campo1 = valor1;
variable.campo2 = valor2;
variable.campo3 = valor3;

Definición de una variable de estructura (II)

Con una lista de inicialización
También podemos pasar una lista de valores, al estilo array, que se asignarán a cada uno de los campos de la estructura.
struct identificador variable = { valor1, valor2, valor3 };
Con una lista de inicialización especificada
Dentro de llaves, pasar una lista de campo-valor, donde el campo empieza con un punto.
struct identificador variable = { .campo1=valor1, .campo2=valor2, .campo3=valor3 };

Tamaño de estructuras

  • Para saber el tamaño que ocupa una estructura en bytes, una vez más, tenemos el operador sizeof para ello.
  • En principio, el tamaño de una estructura es la suma del tamaño de sus componentes.
    • Pero hay casos en los que el todo es más que la suma de sus partes.

Structure padding

  • Los microprocesadores tienen un tamaño determinado para las direcciones de memoria
    • Casi siempre, es un valor en bits, y es un dato muy popular: 16, 32, 64 bits.
  • ...y es más eficiente acceder a posiciones de memoria que sean múltiplos de esa cantidad.
  • El compilador alinea los campos de una estructura para que comiencen siempre en direcciones múltiplo, lo que afecta al tamaño final de la estructura.

Campos de bits

  • En ciertos casos, podemos optimizar el consumo de memoria, si necesitamos usar tipos enteros, pero no necesitamos todo el rango que proponen.
  • La solución es usar campos de bits, una manera de indicar que un tipo solo ocupará n bits en memoria:
    struct [identificador] {
       unsigned tipo_entero identificador_entero:núm_de_bits;
    } [lista_variables];

Ejercicios

Introducción a punteros

  • Este es quizá el tema que más complicaciones trae a los estudiantes de C.
  • Implica desacostumbrarse a muchas de las abstracciones de lenguajes de alto nivel.
  • Pero es la característica que otorga a C casi toda su potencia.

Puntero

  • La idea del puntero es muy simple: un tipo que contiene una dirección de memoria.
  • En esa dirección normalmente habrá un objeto de cierto tipo.
  • Los punteros también tienen tipos: el tipo al que pueda apuntar: puntero a int, puntero a char, puntero a struct Estructura...
    • Un puntero de un tipo no puede apuntar a otro tipo de objeto.
    • Con una sola excepción: punteros void.
  • Ejemplo

Punteros inválidos

  • Que un puntero exista no quiere decir necesariamente que en esa dirección exista un objeto válido: puedo estar apuntando a una dirección...
    • ...que no existe.
    • ...donde no haya nada.
    • ...donde haya otro objeto.
  • Apuntar con un puntero no creará objetos nuevos.
  • No se debe usar jamás un puntero que no haya sido inicializado correctamente.

Declaración de punteros

  • Sintaxis:
    tipo *identificador;
    Nótese el asterisco.
  • El asterisco también puede ir junto al tipo, o separado de ambas partes:
    tipo * identificador;
    tipo* identificador;

Declaración y definición de punteros

  • Asignar la dirección de un objeto se hace con el operador de dirección:
    int A;
    int *pA;
    pA = &A;
  • Si un puntero no se usará desde el inicio, lo mejor es asignarle la dirección NULL.
    • Constante definida en stdlib.h.
    • Es garantizado que contiene una dirección de memoria inválida.

Dereferenciación

  • Dereferenciar es aplicar la operación de indirección a un puntero. El resultado es el objeto apuntado.
  • Sintaxis:
    *puntero = valor;
  • Debemos de tener muy en claro que *puntero no es un objeto en sí, sino una expresión.

Arrays y punteros (I)

  • En C, los arrays son una clase especial de punteros.
  • Al declarar un array:
    1. Se declara un puntero del tipo del array.
    2. Se reserva memoria para todos los elementos del array.
    3. Se asigna al puntero la dirección del primer elemento del array.
  • No se puede cambiar la dirección a la que "apunta" un array.
  • Ejemplo

Punteros void

  • Podemos tener punteros genéricos, que puedan apuntar a cualquier tipo de objeto.
  • Para hacerlo, el tipo al que debe apuntar es void:
    void *identificador;
  • Para usarlo con un tipo determinado, deberemos hacer un casting a ese tipo:
    (tipo *) puntero

Punteros a estructuras

  • Para asignar una estructura a un objeto, se hace como cualquier otro tipo.
  • Para acceder a los campos de una estructura a través de un puntero, existe un atajo:
    struct stEstructura* pEstructura;
    (*pEstructura).campo;
    pEstructura->campo;
  • Ejercicios: Estructura simple y con arrays

Uniones

  • Una unión es un tipo de dato cuyos miembros ocupan la misma ubicación de memoria.
  • Dentro de una unión, evidentemente solo puede guardarse un campo a la vez.
  • La sintaxis es muy similar a la de las estructuras:
    union [identificador] {
       [tipo nombre_variable[,nombre_variable,...]];
    } [variable_union[,variable_union,...];
  • Ejemplo

Declaración y definición

  • La declaración y la definición de una unión es idéntica a la de una estructura.
  • La declaración de una variable de tipo unión, también es idéntica.
  • Por otro lado, la definición de uno de sus miembros difiere un poco de la de las estructuras:
    • Al definir la variable de una unión, solo se puede definir un campo a la vez.

Reinterpretación de tipos

  • Uno de los principales usos de las uniones es el de tener la habilidad de leer un determinado valor como si fuera de otro tipo.
    • En C++ tenemos reinterpret_cast<tipo> para lograr este efecto.
  • Esto es diferente a hacer una conversión explícita:
    • El casting modifica la representación binaria, para tratar de conservar el mismo valor "humano" entre varios tipos.
    • Por otro lado, la unión no realiza ninguna modificación al valor binario.

Arrays multitipo

  • Tenemos el hecho de que un array solo puede almacenar valores de un solo tipo.
  • Pero una unión puede contener valores de varios tipos.
  • Podemos tener un array de uniones para poder guardar varios tipos en el mismo array.
  • Pero, un problema: ¿Cómo saber qué tipo está en uso dentro de una unión determinada? Discriminadores.

Archivos (de verdad)

  • Ya vimos cómo podemos realizar operaciones de lectura/escritura sobre "archivos terminal".
    • Ahora trabajaremos con archivos "de disco".
  • Veremos un concepto nuevo: el descriptor de archivo
    • Nos indica el archivo asociado, y el tipo de apertura.
    • Accederemos a él a través del tipo de dato FILE*.
    • También tendremos un cursor que apuntará al punto donde estaremos operando dentro del archivo.

Esquema de operación

  • Para trabajar con estos archivos, el esquema general a seguir será el siguiente:
    • Crear un puntero a FILE
    • Abrir el archivo con fopen, y asignar el descriptor al puntero
    • Realizamos las operaciones pertinentes
    • Cerrar el archivo
  • ¿Por qué no hacíamos esto con la terminal?
    • El sistema lo hace por nosotros.

Tipo de apertura

Determina las operaciones permitidas sobre el archivo.

"r"
abrir un archivo para lectura, el fichero debe existir.
"w"
abrir un archivo para escritura, se crea si no existe o se sobreescribe si existe.
"a"
abrir un archivo para escritura al final del contenido, si no existe se crea.
"r+"
abrir un archivo para lectura y escritura, el fichero debe existir.
"w+"
crear un archivo para lectura y escritura, se crea si no existe o se sobreescribe si existe.

Abrir y cerrar un archivo

FILE * fopen (const char *nombre, const char *tipo);

fopen abre un archivo, pasando el nombre y el tipo de apertura. Devolverá un descriptor válido si pudo abrir el archivo, y NULL si hubo un error.

int fclose (FILE *archivo);

fclose cierra un archivo. Devuelve 0 si se cerró correctamente, EOF si hubo un error.

Ejemplo

Funciones ya vistas

  • Podemos usar las funciones que vimos al inicio del curso, con una pequeña modificación:
    1. Añadir f al inicio del nombre de la función: putsfputs
    2. Pasar como parámetro el descriptor de archivo: fputs("cadena")fputs("cadena", archivo)
    3. La posición del descriptor depende de la función.
  • Al final, las funciones anteriores usan implícitamente los descriptores estándar: puts("cadena")fputs("cadena", stdout).

Funciones de lectura I

char fgetc(FILE *archivo);

fgetc lee un caracter del archivo. Devuelve el caracter leído, o EOF si hubo un error, o si ya es el final del archivo. Ej.

char *fgets(char *buffer, int tamano, FILE *archivo);

fgets lee caracteres del archivo hasta que el archivo termine, o hasta encontrar un caracter de salto de línea; y los guarda en el buffer especificado. Devuelve el puntero al buffer, o NULL si hubo un error, o si ya es el final del archivo. Ej.

Funciones de lectura II

size_t fread(void *puntero, size_t tamano, size_t cantidad, FILE *archivo);

fread lee bloques de datos de un tipo específico. La función recibe un puntero al buffer, el tamaño de un bloque, y la cantidad de bloques a leer del archivo. Devuelve el número de registros leídos.

int fscanf(FILE *fichero, const char *formato, argumento, ...);

fscanf lee una cadena con formato. Devuelve el número de elementos leídos, o EOF si la lectura falló o si ya es el final del archivo. Ej.

Funciones de escritura I

int fputc(int caracter, FILE *archivo);

fputc escribe un caracter en el archivo. Devuelve el caracter escrito, o EOF si falló. Ej.

int fputs(const char *buffer, FILE *archivo);

fputs escribe una cadena completa en el fichero, sin contar el caracter nulo. Devuelve el número de caracteres escritos, o EOF si falló. Ej.

Funciones de escritura II

size_t fwrite(void *puntero, size_t tamano, size_t cantidad, FILE *archivo);

fwrite escribe bloques de datos de un tipo específico. La función recibe un puntero al buffer de datos, el tamaño de un bloque, y la cantidad de bloques a escribir del archivo. Devuelve el número de registros escritos. Ej.

int fprintf(FILE *archivo, const char *formato, argumento, ...);

fprintf escribe una cadena con formato. Devuelve el número de caracteres escritos, o un número negativo si falló. Ej.

Comprobación de errores

  • Algunas funciones devuelven un valor especial si fallan.
    • fread y fwrite no lo hacen.
  • Tenemos tres funciones para tratar con errores al operar con archivos, en cualquier caso:
    • feof devuelve 0 mientras no se alcance el final del archivo.
    • ferror devuelve 0 mientras no haya ocurrido un error.
    • clearerr elimina la marca de error en el descriptor de archivo.
    • rewind regresa el cursor de un descriptor al inicio del archivo.

Tipado estático y dinámico

Se refiere a si los tipos de datos se comprueban durante la compilación o ejecución del programa:

  • Estático si se realiza durante la compilación.
  • Dinámico si se posterga hasta el momento de ejecutar la instrucción.
int main(void){
  int a = 5;
  char* b = "6";
  return a + b;
}
let a = 5;
let b = "6";
console.log(a+b);

Regresar

Tipado fuerte y débil

Se refiere a la severidad con la que se hacen cumplir las reglas de tipado del lenguaje.

  • Fuerte si no admite licencias sobre los tipos de una variable.
  • Débil si sí las permite.
int main(void){
  int a = 5;
  float b = 6.0;
  return a + b;
}
procedure Main is
   A : Integer := Integer'Last;
   B : Integer;
begin
   B := A + 5;
end Main;

Regresar

Tipado explícito e implícito

Se refiere a si el tipo de una variable se indica en código (explícito), o es inferido a partir del valor asignado (implícito).

int main(void){
    char s[] = "Test String";
    float x = 0.0f;
    int y = 0;
    return 0;
}
a = 4
b = 4.0
c = '4'
type(a)
type(b)
type(c)

Regresar

Tipado nominal y estructural

Se refiere a la forma de determinar si dos variables tienen el mismo tipo.

  • Nominal si se basan exclusivamente en el nombre de los tipos (aunque los tipos diferentes sean idénticos).
  • Estructural si se basan en la estructura interna de los datos.

Regresar