¿Preguntas?

¿Como venimos hasta acá? ¿TPs... Como les fue?

estructura básica de la computadora

¿Donde están las variables? En la memoria RAM, que se organiza en "celdas". Cada una de estas "celdas" tiene un posición que nos permite encontrar nuestra variable.

Diagrama de la memoria

Lo que vemos a la izquierda serán posiciones, y en las "celdas" de la derecha guardaremos variables.

    Pasaje por valor

    Vamos a copiar las variables para que las use la funcion.

    Pasaje por referencia

    Vamos a decirle a la funcion donde están las variables, para que la función opere directamente sobre ellas.

Pasaje por valor:

Cuando entramos a una funcion se crea un área de memoria que contiene copias de los valores pasados como argumentos y también variables locales de la función. A este área de memoria con la cuál trabaja la función se denomina pila o stack.

Introducción a punteros

Definición de arreglos, estructuras y cadenas de texto

Punteros

  • Las variables que vimos hasta ahora son variables que contienen datos que tienen correlación con el mundo real/matemático
  • Las variables puntero tienen direcciones de memoria. Sirven para guardar donde se encuentran las variables comunes en la computadora.
  • Las variables puntero permiten acceso por referencia a variables comunes
  • Los punteros son evidentes en cuando se usa código assembler y son una de las cosas que hacen potente (y complicado) usar al lenguaje C

Punteros: Ejemplo1




    
    int a=100;
    int b=100;
    
    int *p=&a;
    
    int *q=&a;
    /* O también:
     *     int *q=p;
     */
    
    int *r=&b;
    	

Punteros, corrida ejemplo1:


En decimal:
	a: 100, b: 100
Como hexa:
	a: 64, b: 64
Como hexa:
	p: 7ffc54ca3898, q: 7ffc54ca3898, r: 7ffc54ca389c
Como punteros:
	p: 0x7ffc54ca3898, q: 0x7ffc54ca3898, r: 0x7ffc54ca389c
Direccion de los punteros:
	&p: 0x7ffc54ca38a0, &q: 0x7ffc54ca38a8, &r: 0x7ffc54ca38b0
    Importante:
  • p igual a q : Por qué apuntan a lo mismo.
  • &p distinta de &q : Por qué son distintas variables.

Punteros: Ejemplo2

Mostrando pasaje por valor y referencia...


#include <stdio.h>

void proc(int valor) {
	printf("2: %d\n", valor);
	valor = 10;
	printf("3: %d\n", valor);
}

int main(void) {
	int valor = 7;
	printf("1: %d\n", valor);
	proc(valor);
	printf("4: %d\n", valor);
}

Punteros: Ejemplo2

¿Como logramos hacer pasaje por referencia?

  • punt01.c : Pasaje por valor, implicancias.
  • punt02.c : Visualizando las direcciones de memoria.
  • punt03.c : Pasaje por referencia.
  • punt04.c : ¿Pasaje por referencia o por valor?
  • punt05.c : Haciendo explicito el puntero a puntero.

Punteros: Ejemplo3, Código


int x=4;

int *px=&x;
/* Donde:
 * *px es - Es una variable puntero a un entero */
/*  &x es - Direccion de x */

int **ppx=&px;
/* Donde:
 * &px es - Direccion de px */
/* **px es - Es una variable puntero a un puntero a un entero */

Punteros: Ejemplo3, Gráfico

¿Cómo usar punteros en C?

  • Hay cuatro usos u operaciones básicas:
    • Declarar una variable puntero a un tipo
    • Conocer y guardar la dirección de una variable
    • Obtener o recuperar el valor al qué apunta un puntero
    • Recibir un puntero como parámetro (en una función)

Declarar una variable tipo puntero

  • Declaración de un puntero
    • Sintaxis:
      
      tipo_de_variable_apuntada *nombre_del_puntero;
  • Ejemplos:
    • 
      int *p;
      • Declara un puntero p, que se utilizará para apuntar a un entero, se dice que p es un puntero a int
    • 
      char *pc;
      • Declara el puntero pc, que se utilizará para apuntar a un carácter, se dice que pc es un puntero a char

Conocer la dirección de una variable

(operador &)

  • &variable
    • Devuelve la dirección en memoria de la variable

Ejemplo:


/* Declaro y creo en memoria una variable entera */

int v1, v2 = 10 ;

/* Declaro y creo un puntero a un entero.
 * Contendrá la dirección de una variable entera
 */

int *pint ;

/* Inicializo el puntero con la dirección de memoria de la 
 * variable. Donde está el puntero se guardó la dirección
 * de la variable entera
 */

pint = &v2;

Saber a qué apunta un puntero

(operador *)

* puntero devuelve el valor de lo
que está siendo apuntado por puntero

    Ejemplo:

    
    int n = 10, m;
    int *p;
    p = &n;
    m = *p;
    

    En el ejemplo...

    • ¿cuánto vale p?
    • ¿cuánto vale m?

Regla práctica operadores puntero


  • El operador * tiene dos usos, en la definición/declaración indica que la variable es del tipo puntero.

  • El operador * significa "contenido de lo apuntado por" cuando se usa en el cuerpo del programa.

  • El operador & significa "dirección de".

Ejercicio de punteros:

Dada la declaración de las siguientes variables y su representación en memoria ilustrada:

int a, b, *p, *q;

Complete la siguiente tabla con los valores que tomarán las variables y lo apuntado por sus contenidos en las columnas que se lo solicita.

Cuando no sea posible indicar el valor señálelo con el signo: ?

Las letras α, β, χ, etc. simbolizan las direcciones de memoria en las que se encuentran ubicadas las variables declaradas anteriormente.

Ejercicio de punteros: Tabla

Ejercicio de punteros: Memoria

Ejercicio de punteros 2


int a, b, *p, *q, **r, **s;
p=q=NULL; r=s=NULL;
a=9; b=4;
p=&a; q=&b;
r=&p; s=&q;
**r=*q;
// ...mas... (ver enunciado)

Ahora que vimos el operador &...

¿Pueden ver por qué scanf() usa &?

Recibir un puntero como parámetro



void fn(char *s);

  • La función fn recibe un puntero a carácter
    • Se denomina pasaje por referencia

  • Cuando se invoca fn, en la posición del argumento s se debe pasar una dirección de una variable de tipo char

  • Se utiliza o sirve para hacer funciones que trabajan directamente sobre los valores de la variables originales y no sobre copias locales de los valores de esas variables

Paso de argumentos a funciones (I)

Paso por valor


  • El paso de parámetros por valor consiste en copiar el contenido de la variable que queremos pasar en otra dentro del ámbito local de la subrutina

  • Se tendrán dos valores duplicados e independientes, con lo que la modificación de uno no afecta al otro.

Paso de argumentos a funciones (I)

Paso por valor


Ejemplo:


int mifunc(int a, int b) {
	a++;
	b=b+2;
	return a+b;
}


Ver correr: func_por_valor.c

Paso de argumentos a funciones (II)

Paso por referencia


  • El paso de parámetros por referencia consiste en proporcionar a la subrutina a la que se le quiere pasar el argumento la dirección de memoria del dato.

  • En este caso se tiene un único valor referenciado (o apuntado) desde dos puntos diferentes, el programa principal y la subrutina a la que se le pasa el argumento, por lo que cualquier acción sobre el parámetro se realiza sobre la misma posición de memoria.

Paso de argumentos a funciones (II)

Paso por referencia


Ejemplo:


int mifunc(int *a, int *b) {
	(*a)++;
	(*b)=(*b)+2;
	return (*a)+(*b);
}


Ver correr: func_por_referencia.c y func_por_referencia_v2.c

Entendiendo como funciona scanf()

  • Código:
    
    scanf("%i", &a);
    		
    • Llama a la función scanf, lo ingresado por el usuario lo almacena en la variable "a"
    • "a" es una variable del main, y cambia en el main
    • Localmente (dentro de scanf) solo tendremos un puntero o referencia a la variable del main.

  • Otros lenguajes que no usan punteros, tienen funciones de ingreso de datos de esta forma:
      
      a = input("%i")
      			

Arquitectura de 64bits

¿Punteros de 64bits o 48bits?


En las máquinas actuales de 64 bits se manejan punteros de 48 bits. Esto es por que con eso ya pueden direccionar 256 terabyte de memoria cosa que les pareció suficiente a los fabricantes de procesadores (Wikipedia X86-64 Virtual address space y StackOverflow Q&A). De esta manera los fabricantes se ahorran los transitores de esos últimos 16 bits.


Por esto los punteros que veremos con %p serán de
48 bits o 6 bytes. Pero si hacemos un sizeof() nos va a decir 8 bytes. La arquitectura puede direccionar 64bits/8bytes, pero el hardware se queda con los 48bits/6bytes inferiores.

Arreglos


(Clase 2)

Arreglos (I)

  • Los arreglos son agrupaciones de un mismo tipo de dato que pueden ser accedidos mediante un subindice
  • A los arreglos también se los llama array
  • Se pueden declarar arreglos de cualquiera de los tipos básicos de C (char, float, int, etc...)
  • Tienen una base (puntero) común y a partir de la misma se van "apilando" la cantidad de elementos que esté definida.

Arreglos (II)

  • El tamaño del arreglo se define en la declaración del mismo y no puede cambiar:
    • El límite no es reforzado o protegido por el lenguaje.
    • Puedo leer/escribir elementos que no esten definidos y el compilador no me va avisar
    • Esto trae muchos problemas y errores en C

Declaración de arreglos unidimensionales

  • Declaración
    • <tipo-base> <identificador>[<NumElementos>];


Ejemplo:


int vec[20];

Declaración de arreglos unidimensionales

  • Declaración con asignación
    • <tipo-base> <identificador>[<NumElementos>] = {valor1, valor2, valor3,...};


Ejemplo:


int edades[5] = {17, 19, 21, 20, 18};

int edades[ ] = {17, 19, 21, 20, 18};

Declaración de Arreglos: Ejemplos (I)


  • Declaración de un arreglo de 50 enteros (0 a 49):
    
    int A[50];
    

  • Declaración de un arreglo de 100 caracteres (0 a 99):
    
    char A[100];
    

  • Declaración e inicialización de un arreglo de 10 enteros:
    
    int A[10] = {2, 5, 8, 100, 1, 2, 100, 5, 5, 5}
    

Declaración de Arreglos: Ejemplos (II)


  • Inicialización parcial: El resto no se inicializa (contiene basura)
    
    int A[100] = {25, 5, 100, 25, 5}
    

  • Declaración e inicialización de un arreglo de 10 caracteres:
    
    char A[10] = {'a', 'z', 'E', 'e', 65, '\x41', '@', 'U', '*', '\n'}
    

  • Determinación del tamaño de un arreglo en forma implícita:
    
    int A[] = {5, 10, 2, 15, 20}
    

Caracteres de escape en C


  • La barra (\) es un caracter de escape en C.
    • '\n' es un salto de linea
    • '\t' es una tabulación

  • También se puede hacer (ver ejemplo):
    • '\nnn' donde nnn es interpretado como el valor octal en la tabla ASCII
    • '\xhh' donde hh es interpretado como el valor hexadecimal en la tabla ASCII

Mas ejemplos de uso (I)

  • Asignación de un valor a la sexta posición de un arreglo de enteros:
    
    A[5] = 200;
    
  • Impresión de un arreglo de 100 enteros mediante un ciclo for:
    
    int i, B[100];
    for (i=0; i < 100; i++)
        printf("%d\n", B[i]);
    
  • Ingreso de 20 enteros a un arreglo mediante un ciclo for:
    
    int i, C[20];
    for (i=0; i < 20; i++)
        /* scanf() requiere puntero: & */
        scanf("%d", &C[i]);
    

Mas ejemplos de uso (II)

  • Función que recibe un arreglo de enteros como argumento y calcula el promedio:
    
    float promedioArray (int D[ ], int num_elementos) {
        long prom = 0;
        int i;
        for (i=0; i < num_elementos; i++)
            prom = prom + D[i];
        return (prom / num_elementos);
        }
    


Se pueden bajar una versión completa (con llamado y main) se adjunta usando además casting para la conversión long/int y int/float.

Importante: A una función, además de pasarle la dirección de comienzo del array, hay que pasarle los límites del array. El lenguaje C no comprueba los limites de los arreglos.

Pasaje de arrays a funciones (I)


  • Los array a una función se pasan por referencia (dirección), en vez de por valor como con las variables simples, esto es así, porque seria muy ineficiente copiar todos los valores de los componentes de un array para pasarlos por valor.

  • El pasaje por referencia implica, que en vez de copiar el array, se pasa su dirección de comienzo. Esta dirección de comienzo, coincide con la dirección del nombre del array, o lo que es lo mismo, con la dirección del primer elemento.

Pasaje de arrays a funciones (II)

  • A partir de esa dirección inicial (nombre del array), se encuentran consecutivamente uno a continuación de otro, todos los elementos del array.

  • Como todos los elementos del array son del mismo tipo (ocupan igual cantidad de bytes), los subsiguientes elementos se encuentran sumando un valor fijo a la dirección del elemento anterior, así hasta el último.

  • Y reiteramos... A una función, además de pasarle la dirección de comienzo del array, hay que pasarle los límites del array, ya que el lenguaje C no comprueba estos limites.

Ejercicios de Arrays Unidimensionales

  • Realizar el estructograma y la codificación en C de:
    • Un programa que solicita 5 números y luego los imprime en orden inverso
    • Un programa que pregunta los coeficientes de un polinomio de grado 5



    • Y finalmente evalúe el polinomio desde:
      • x = -10 a x = 10 en pasos de a 1 entero.


También pueden ver un ejemplo de arreglos/vectores y notas.

Inicialización en C++ de arrays


#include <stdio.h>

int main(void){
	int vec[3]={};
}
	

Este código es invalido en C, pero valido en C++.

En C++ inicializa el vector con ceros.



Dependiendo de los modificadores que le pasamos al compilador, vemos o no el error.


$ gcc -pedantic -o prueba arreglo_con_llaves.c
	

Finales Informática 1



Código que compila en gcc pero es invalido en C, equivale a un aplazo en el final. Se lo considera un error de concepto.


Son importantes en este sentido las evoluciones del estándard de C: C99, C11, C17 y C2x (sin terminar)


En el archivo modificadores_gcc.txt se pueden ver modificadores de gcc para ver/evitar estos tipo de confusiones.

Strings

Solo un caso particular de arreglo unidimesional...

Variables char y strings


  • Tipo de dato char:

    • En una variable de tipo char, lo que se almacena es valor del código ASCII correspondiente a un carácter.

    • Puede verse al tipo char como un subtipo del tipo int (short int), que puede almacenar enteros de 0 a 255.

Variables char y ASCII


#include <stdio.h>

void main(void) {
	char c;
	printf("Ingrese un caracter: ");
	scanf("%c", &c);
	printf("Usted ingreso el caracter: \'%c\' \
		cuyo codigo ASCII es %d en decimal y %x hexa.\n", c, c, c);
}

Ver correr: codigo_ascii.c

Recordemos la diferencia entre representación e interpretación...

Programa que imprime la tabla ASCII


#include <stdio.h>

void main(void) {
	int i, j;
	int c;
	printf("\nTabla ASCII\n");
	printf("Sume el valor de la fila con el de la columna para obtener el codigo\n\n");
	printf("     0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15\n\n");
	for (i = 2; i < 8; i++) {
		printf("%4d", i*16);
		for(j = 0; j < 16; j++) {
			c = 16*i + j;
			printf("%2c ", c);
		}
		printf("\n\n");
	}
	printf("Nota: Solo imprimimos la parte baja (primeros 128 códigos) \n\
	y evitamos los primeros 32 que no son imprimibles\n\n");
}

Ver correr: tabla_ascii.c

¿Qué es un string? (I)

En muchos lenguajes existe el tipo de dato string,
pero C en un intento de simplificación (y refuerzo
del concepto de punteros) no lo tiene.


¿Qué es un string? (II)

  • En lugar de un tipo especial de dato especial para el manejo de string se usa una agrupación de caracteres con un formato particular.
  • Los strings serán un arreglo de caracteres, denominado también cadena de caracteres, que se usa para contener texto y finaliza con un delimitador de valor NULL ('\0')
  • Los string sirven para hacer programas que manipulan texto.

Inicialización de un string (I)

  • Formas de declarar strings:
    
    char s[5] = "Hola";
    char s[15] = "Hola";
    char s[5] = {'H', 'o', 'l', 'a', NULL};
    		

  • Notas:
    • El fin de un string se marca con NULL, que equivale 0 decimal/binario, o '\0' que es el ASCII cero.
    • Al poner el texto entre comillas dobles el compilador crea el string

Inicialización de un string (II)


Otra formas de declarar strings:


char *pS = "Hola";

Esto falla por que en pS[i] no se puede escribir mas tarde
ya que *pS apunta a un: const string



#include <stdio.h>

int main(void) {
	char *pS = "ABCD";
	printf("Ingrese el string a grabar: ");
	scanf("%s", pS);
}

Resultado: Segmentation fault

Ejemplos de declaración y
usos de strings (I)


  • Declaración del tamaño de un string a partir de una constante
  • 
    #define N 50
    char str[N];
    

  • Declaración e inicialización de un string
  • 
    char str[50] = "Este es el contenido inicial";
    

  • Definición implícita de tamaño
  • 
    char str[] = "El tamaño se autodetermina";
    

Ejemplos de declaración y
usos de strings (II)


  • Lectura mediante scanf de un string
  • 
    scanf("%s", &str[0]);
    

  • Lectura de string (equivalente a la anterior)
  • 
    scanf("%s", str);  /* En caso de vectores str==&str */
    

  • Impresión de un string mediante printf()
  • 
    printf("%s\n", str);
    

Mejor usar fgets() [I]

scanf() para strings no tiene proteccion por tamaño...


#define N 5
#include <stdio.h>

int main(void) {
	char str[N];
	scanf("%s", &str[0]);
		// Tambien pueden &str[0] hacerlo str
	printf("%s\n", str);
}

Mejor usar fgets() [II]

El teclado es stdin, y la pantalla es stdout...


#define N 5
#include <stdio.h>

int main(void) {
	char str[N] = "ABCD";
		// Protegido por tamaño del string
	fgets(str, N, stdin);
	printf("%s\n", str);
}

Mejor usar fgets() [III]

¿Por qué esto es importante?

¡Seguridad en el código que ejecutamos a ataques!


Problemas con scanf() y los strings (I)

Scanf() y el buffer stdin:


  • scanf("%c");
  • scanf("%s");

  • ¿Como evitamos el buffer?
    • fflush(stdin); /* no suele andar */
    • setbuf(stdin,NULL); /* Anda mejor */
    • scanf(" %c"); /* mejor para Linux */
    • scanf("%3.2f"); scanf("%.2f);

Problemas con scanf() y los strings (II)

Scanf() y el buffer stdin:


  • También podemos usar funciones especificas para string y chars de stdio.h:
    • int getchar(void);
    • char *gets(char *s);
    • int putchar(int c);
    • int puts(const char *s);



Nota: Ver código de fflush()/setbuf()