Curso completo de programación con lenguaje C

gonya707

Tambien puedes usar Eclipse si estas loco y te gusta Eclipse

si quieres software libre el Dev C++ tiene una branch llevada por un tio que lo ha mantenido durante estos últimos años, se llama Dev CPP Orwell. Esta completillo, aunque sigue teniendo el aspecto espartano

1 1 respuesta
Skiuv

#151 Voy a probar ese :D

¿Lo de eclipse y loco por qué? xD

B

#147 Hasta ahora usaba Sublime Text como editor y gcc como compilador.

VS tiene buen soporte para C?

gonya707

7 - Punteros y memoria

Entramos en una de las lecciones que más gente suele asustar, y es que los punteros es algo "muy de C" que no está presente en muchos otros lenguajes de alto nivel, como Java. El uso de punteros llega a ser inevitable cuando se empiezan programas de relativa complejidad, de hecho, en alguna ocasión los hemos usado anteriormente.

reserva de memoria

Como ya se había comentado antes, cuando se crea una variable lo que se está haciendo en realidad se está reservando un espacio dentro de nuestra memoria para que podamos asignar ahi valores. Una prueba de que lo único que pasa es que se reserva el lugar en la memoria es que cuando la variable es creada no se inicializa ni se "pone a 0", de hecho toma un valor residual, que es lo que hay en ese hueco de memoria en el momento de reservarse, que es un valor residual que ha dejado otro proceso.

int residuo;
fprintf(stdout, "valor residual: %d", residuo);

Aunque este proceso de reserva de memoria es prácticamente transparente al usuario, es de una importancia vital. Cuando disponemos de una variable con nombre podemos averiguar su dirección en la memoria con el operador ampersand "&".

int numero;
numero = 7;
fprintf(stdout, "numero es %d y esta en la posicion %d", numero, &numero);

Ese & os debería sonar a estas alturas, porque es el mismo que ponemos siempre en el fscanf, el tercer parámetro de la función fscanf es un puntero, y esto en el prototipo de una función se indica con el símbolo asterisco "*". Por ejemplo el prototipo de una función que recibe un entero y un puntero a entero como parámetros de entrada sería así:

void funcion1(int, int*);

Por tanto disponemos de un monton de nuevos tipos de variable que usar en nuestras funciones, int, char, float*,.... todas las variables que conocemos hasta el momento y todas las que descubráis en el futuro tienen su "versión" puntero, ya que todas ocupan memoria y puede ser útil saber dónde se encuentran.

uso de punteros

Vale, ahora sabemos como acceder al número que indica dónde se encuentra una variable en la memoria, esto para qué puede servirnos? La mayor utilidad es superar una limitación que conocíamos que tenía C: que las funciones sólo podían tener un parámetro de salida.

Como ya se explicó anteriormente, cuando se tiene una variable dentro de una función, esa variable tiene un rango local, ya que será destruida en cuanto termine la función. Si queremos modificar desde una función la misma variable que teniamos fuera de ella, la idea sería no pasar como parámetro de entrada la propia variable, sino el puntero a ella.

#include <stdio.h>
void funcion1(int );

int main(void){
   int var = 0;
   funcion1(var);
   fprintf(stdout, "%d", var )
   return 0;
}

void funcion1(int v){
    v = 1;
}

Aquí tenemos dos ejemplos que ilustran todo esto. En el primero la función es del tipo que ya conocíamos. La función recibe una variable entera, cuando la variable entra a funcion1 se crea otra variable entera llamada v, cuyo contenido es el que le hemos pasado desde el main, en este caso 'var', que era 0. Desde dentro de la función cambiamos v a 1. Hemos cambiado la variable var del main? No, solo hemos cambiado la variable local v, que justo que ahora se acaba la funcion1, queda destruida, y volvemos al main. Al imprimir ahora la variable var por pantalla el resultado es 0, aunque hemos intentado modificar su valor dentro de funcion1, lo único que hemos hecho es modificaro el valor de la variable local, que ha quedado destruida.

#include <stdio.h>
void funcion1(int* var);

int main(void){
   int var = 0;
   funcion1(&var);
   fprintf(stdout, "%d", var )
   return 0;
}

void funcion1(int* v){
    *v = 1;
}

En el segundo ejemplo en cambio, funcion1 no recibe un entero, recibe un puntero a entero (int), por lo que en el main tenemos que pasarle ese tipo de variable. var es un entero, y su dirección de memoria, es decir su puntero es &var. Ahora en funcion1 se vuelve a crear una variable local al entrar, esta vez del tipo puntero a entero int, llamada v. Si asignásemos un valor a v directamente estaríamos cambiando la posición de memoria, algo que no tiene mucho sentido, lo que queremos cambiar es su contenido, y eso se consigue con el operador * a la izquierda. El asterisco a la izquierda es el operador dual de &, mientras uno señala a la dirección de memoria de un dato, el * señala al dato que contiene cierta dirección de memoria. Puesto que dentro de la función estamos manejando un puntero, para acceder al dato interior tenemos que asignar valores a *v. Una vez terminada la función se destruye la variable local v, es decir se destruye el puntero, pero la variable real con su valor dentro sigue viva, ya que es la misma que existía en el main. Ahora al imprimir var desde main el valor es 1.

#include <stdio.h>
void division(double dividendo, double divisor, int *cociente, int *resto);

int main(void){
   int resultado1, resultado2;

   void division(8, 3, &resultado1, &resultado2);

   fprintf(stdout, "cociente: %lf, resto: ", resultado1, resultado2 );
   return 0;
}

void division(double dividendo, double divisor, int *cociente, int *resto){
    *resto = dividendo % divisor;
    *cociente = (dividendo - (*resto)) / divisor;
}

En este caso tenemos hecha una función que divide dos números, devolviendo tanto el cociente como el resto. Puesto que no es posible en C devolver más de una cosa en una función mediante la instrucción return, la única solucion posible para este problema sin usar variables globales es usar punteros. Se deja como ejercicio metal al alumno analizar el código, teniendo en todo momento controladas las variables que puede "ver" tanto la función main como la función division, teniendo especial cuidado en el uso de los asteriscos dentro de division.

Resumiendo esta introducción a los punteros:

  • Cada variable tiene una dirección de memoria asociada, y la variable que contenga ese número se conoce como puntero.

  • El acceso a ese número de memoria se consigue con el signo &

  • Por el contrario, el acceso al contenido de una dirección de memoria se consigue con un asterisco a la izquierda

  • Los punteros pueden servir para tener todos los parámetros de entrada/salida que se deseen en una función.

Deberes lección 7

Este tema ha sido muy teórico y no tiene gran aplicación práctica hasta que se de el uso de Arrays (que es lo que viene a continuación), asi que sin que sirva de precedente, en este capítulo no se me ha ocurrido ningún ejercicio, aunque no descarto que ponga algo a lo largo de la semana ;)

4 2 respuestas
HeXaN

Y a partir de aquí la gente empieza a dropear C xD

11
m4andg4

Está lección la esperaba con ansias xDD de hecho me arriesgo a decir que C es un 80% punteros y manejo de memoria. ¡Ánimo!

1
preguntitas

No se si me ha quedado claro, pero para que usar punteros si se puede conseguir el mismo resultado usando variables globales y para el tema de retornar mas de un valor se puede hacer devolviendo un array con todos los valores que quieras...

Lo unico que veo es que se puede "convertir temporalmente" una variable local en global, y asi ahorrar memoria(?¿). Pero con los ordenadores de hoy en dia(con la cantidad de memoria que llevan), no se si realmente son necesarios.

¿que me estoy dejando? por que fijo que hay algo :-(

3 respuestas
HeXaN

#157 Son totalmente necesarios, ya te irás dando cuenta conforme avances en la materia.

1 1 respuesta
m4andg4

#157 Para resolver el problema de paso por referencia de los procedimientos en C, entre otras muchas cosas...

1 1 respuesta
preguntitas

#159, pero eso con variables globales se soluciona.

#158, me imagino que si.

gracias a los dos.

3 respuestas
HeXaN

#160 Las variables globales son el demonio.

6 1 respuesta
m4andg4

#160 Lo pone en la lección que Gonya se ha currado... pero voy a intentar explicarme mejor para que lo entiendas (voy a usar el ejemplo que él mismo ha puesto):

Tu creas una función por ejemplo:

Void division (int dividendo, int divisor, int cociente, int resto)

Lo que nosotros buscamos con esta función es que al introducirle los parámetros "dividendo" y "divisor" nos devuelva el cociente y el resto y nos lo almacene en dos variables globales que previamente hemos declarado.

int main (void){
    
int cocienteResultado=0; int restoResultado=0; division(10, 3, cocienteResultado, restoResultado); fprintf(stdout,"El cociente es: %d y el resto: %d", cocienteResultado, restoResultado); return 0; }

Si lo hacemos de esta manera (tu mismo puedes comprobarlo copiando el codigo), cuando mostremos por pantalla el contenido de las variables cocienteResultado y restoResultado va a ser 0, porque la función no ha pasado el valor a las variables globales. Para esto están los punteros.

Para que suceda de la manera inicialmente planteada tendríamos que declarar los argumentos de la función "int cociente" y "int resto" como apuntadores "int* cociente" e "int* resto". Estos argumentos apuntadores, almacenarán la dirección de memoria de la variables que le pasemos como parámetros a la hora de usar la función.


Void division (int dividendo, int divisor, int* cociente, int* resto){   //Con punteros   
*resto = dividendo % divisor; *cociente = (dividendo - (*resto)) / divisor; return 0; } int main (void){
int cocienteResultado=0; int restoResultado=0; division(10, 3, &cocienteResultado, &restoResultado); /* El operador "&" devuelve la dirección de memoría de la variable que opera. Luego, aquí se pone para que la función division modifique las direcciones de memoria que el operador & devuelve, que son las de nuestras variables globales declaradas al principio del main */
fprintf(stdout,"El cociente es: %d y el resto: %d", cocienteResultado, restoResultado); return 0; }

Perdón si me ha faltado algo por aclarar, de todas formas hace mucho que no uso C como tal y estoy algo oxidado...

PD: Los tamaños de la ventana me han des-estructurado un poco los comentarios, jajajaja :P
PD2: Esta todo bien explicado en el la lección incluso el uso de los operadores " * " y "&" leela bien! jaja

1 1 respuesta
gonya707

#160 la respuesta rapida es lo que dice #161, las variables globales son el demonio xD

Adicionalmente te comento que el uso de punteros en C no sólo se limita a lo que he puesto en este último capítulo, que es introductorio, sino que sirve también y mucho para el tema de arrays y matrices. Ya verás ya

2 1 respuesta
preguntitas

#162, gracias por la respuesta y la aclaracion.Yo me refiero a algo asi

#include <stdio.h>
int cociente,resto;
void division(int dividendo, int divisor);
int main(void){
   division(8, 3);
   fprintf(stdout, "cociente: %d, resto: %d", cociente, resto );
   return 0;
}
void division(int dividendo, int divisor){
    resto = dividendo % divisor;
    cociente = (dividendo - (resto)) / divisor;
}

Creo que he entendido el uso de punteros para este caso. Y para casos así, si es recomendable el uso de punteros, me lo apunto.Muchisimas gracias

#163, ¿por que no se deben usar variables globales?

1 respuesta
gonya707

#164 Segun la gente de mundogeek, cuando usas variables globales:

El código es más difícil de entender
El código es más difícil de depurar
El código es más difícil de testear
El código es más difícil de mantener
El código es más difícil de reutilizar
Las variables globales matan gatitos

Ahora más en serio, Los mayores problemas son el reciclaje de código (si lo pones todo con variables globales olvidate de copiar y pegar código para otros programas) y que estás ocupando cantidades absurdas de memoria, que deberías limpiar cuando no estés utilizando. Java tiene el llamado recolector de basura que se encarga de pasar de vez en cuando mirando variables que ya no usas y quitandolas del workspace, C no tiene recolector de basura y si quieres limpiar tienes que hacerlo manualmente, si quieres evitarte limpiar variables, usa punteros.

Y si no te parece razón suficiente es porque estos programas son a muy pequeña escala, como mucho tenemos unos números que ocupan unos solos bytes, pero imagina que estás haciendo un programa para dar efectos a canciones. Las muestras de una canción de 4 minutos son un poco más de 20 MBytes. Imagina que quieres poner un efecto de reverberación; para eso neceistas tener la canción entera repetida muchas veces a distintos niveles y retardos, digamos que tienes 100 copias, lo cual no es descabellado, ya el programa ocupa 2 GB de memoria RAM permanentemente, ya que al estar los 100 vectores hechos globales están siempre ocupando el espacio. No sería más logico crear los 100 vectores cuando estás aplicando el efecto de reverberación y luego destruirlos?

Para poner otro ejemplo más bestia, un solo segundo de vídeo a 1080 ocupa 148 MBytes en raw. El uso de memoria se puede disparar fácilmente y no es asunto trivial.

3 1 respuesta
preguntitas

#165, gracias por la aclaracion. Más claro imposible. Da gusto leeros y que a las primeras de cambio no nos dejeis por imposibles.

un saludo.

1 respuesta
m4andg4

#166 Cuando venga el tema de gestión dinámico de memoria sabrás la verdad joven padawan jajajaja

2
B

Variables globales -> más posibilidades de side effects.

B

No estaría de más algún que otro ejemplo :)

lifk

Yo en clase estoy dando c y usar una variable global equivale a suspenso directo XDD

Realmente no usamos mucho punteros tampoco, aunque siempre suele ser necesario para pasar alguna variable por referencia aunque por cosas de la vida casi todos los ejercicios son con arrays y el array siempre se pasa por referencia.

willy_chaos

#157 La explicacion es facil. Como haces en C un array de dimension variable? Vease imaginemos que en tu programa tienes un array de coches, pero no sabes cuantos te van a entrar.

  • Solucion guarra
    Creas un array de dimensiones bestias? Pero si luego resulta que solo quieren 3, estas malgastando recursos y vale para estos ejercicios es una tonteria donde tienes gb de ram, pero si luego tienes que programar para algo mas limitado que una raspberry pues ahi si que necesitas usar la memoria justa. O te puede pasar que necesiten mas registros que los que tu has declarado en tu array y quedarte corto.

  • Solucion buena
    Te creas un array dinamico mediante punteros. A cada nuevo registro que necesitas, pides memoria al sistema operativo. Cuando borras algun registro, limpias memoria -> usas solo la ram necesaria

1 respuesta
preguntitas

#171 muchas gracias por la explicación y el ejemplo. Ya lo veo mucho mas claro.

gonya, hoy es lunes... toca lección ;-)

gonya707

8 - Arrays, vectores y matrices

Uno de los elementos fundamentales presentes en cualquier lenguaje de programación son los vectores (unidimensionales) y las matrices (multidimensionales), englobados ambos en el termino "array". Un array es el conjunto ordenado de un determinado número de variables del mismo tipo, por ejemplo 100 números enteros, 50 caracteres o 10 boleanos. Estos vectores se reservan en memoria en C como si fuesen variables normales, salvo que indicando entre corchetes la longitud necesaria.

int numeros[100];
char frase[50];
bool respuestas[10];

constantes

Pero antes de meternos más al tema de arrays, he de comentar otro elemento importante en el lenguaje y que es útil para esta lección, las constantes.

Una constante es una reserva de memoria, al igual que la variable, salvo por el detalle de que el valor de la constante se especifica en su creación y nunca más puede volver a ser cambiado. La definición (nunca mejor dicho) de constantes se realiza con la etiqueta #define

#define PI 3.141592
#define ANCHO 1920
#define ALTO 1080

Detalles sobre estas líneas para crear constantes:

  • No tienen ; al final

  • Se colocan fuera del main, junto a los #include

  • Sus nombres se escriben en mayúsculas y con _ en el caso de que sea necesario (esto por convención, no es una regla fija)

El uso de constantes dentro de nuestro programa se hace igual que cualquier variable.

area = PI * pow(r, 2);
perimetro = 2 * PI * r;

Una práctica muy habitual es definir la longitud de nuestros arrays con constantes. Supongamos que tenemos un programa con varios arrays del mismo tamaño, y en el que vamos a usar mucho la cifra de ese tamaño en bucles o cualquier cosa, lo más lógico entonces es crear arrays de esta manera:

#define MAX_TAMANO 1024

int numeros[MAX_TAMANO];
char letras[MAX_TAMANO];

valores en arrays

En la creación de un array pueden llenarse sus valores directamente de esta manera:

char letras[10] = {'h', 'o', 'l', 'a', 'q', 'u', 'e', 't', 'a', 'l'};

Mientras que para acceder a sus valores y cambiarlos desde cualquier punto del programa, la sintaxis para ambas cosas es la misma:

numeros[10] = 64;
fprintf(stdout, "%d", numeros[10]);

En este caso la posicion 10 de 'numeros' se ha llenado con el valor 64 y posteriormente se ha accedido a ese valor para que se imprima por pantalla.

Un detalle muy importante a tener en cuenta es que en C la primera posicion de un array es la posicion [0], y si el array ha sido creado con longitud A, su último valor está en [A-1].

Arrays, punteros y funciones

Al crear un array, su nombre dentro de un programa es un puntero que hace referencia al primer miembro del vector, es decir teniendo:

int numeros[MAX_TAMANO];
int *a = numeros;

en el puntero a entero a estamos metiendo la dirección al primer número del array, es decir, esa línea sería completamente equivalente a:

int *a = &(numeros[0]);

Otro detalle importante es que en la reserva de memoria cuando se crea un array las posiciones reservadas van seguidas. Es decir si la primera posicion array[0] es 10000, array[1] estará en 10001, y así en adelante.

Este detalle de que el nombre del array es un puntero es muy importante, ya que no hay manera de poner un array como parámetro ni de entrada ni de salida de una función, es decir, estos prototipos son incorrectos:

funcion1 (int a[10]);
funcion2 (int a[]);
int [20] funcion3 (void); 

Puesto que el nombre de un array es un puntero, si queremos enviar o recibir un array en una función, el parámetro correcto es usar la dirección de memoria:


#define LARGO 10

//recibe un array y devuelve la suma de sus miembros
int sumatorio (int* vector); 

int main(void){
    int impares[LARGO] = {1,3,5,7,9,11,13,15,17,19};

int total;

total = sumatorio(impares) //el nombre tal cual 'impares' ya es un puntero

}

Deberes - lección 8

El procesado de texto con C y arrays de variables char es más complejo que en otros lenguajes, y eso le hace merecer una lección complementaria en este curso, que será a continuación, por lo que de momento, los deberes van a estar basados en números. Siempre que el objetivo de los deberes sea crear una función se recomienda probarla en un pequeño programa para comprobar que funciona como debería.

1- Crear varios arrays de distintos tamaños. Probar definir su longitud con constantes, con una variable, dejando los corchetes vacíos, poniendo un número muy grande... qué es lo que ocurre en cada caso?

2- Crear una función que reciba un array de enteros y la imprima por pantalla de la siguiente forma:

dado:

int vector[10] = {1,2,3,4,5,6,7,8,9,10};

el resultado es:

[1 2 3 4 5 6 7 8 9 10]

Realizar también la versión para variables float. En este caso además del array de entrada, otro parámetro de entrada puede ser el número de decimales a imprimir. En ambos casos puede ser útil que se especifique siempre la longitud a imprimir:

void imprimirArrayInt(int* array, int longitud);
void imprimirArrayFloat(float* array, int decimales, int longitud);

3- Hacer una familia de funciones que sirvan para 'inicializar' los arrays, es decir, que llenen de ceros todas las posiciones del array en el caso de que sean números, o espacios en blanco en el caso de letras.

4- Realizar una función que cuente los elementos de un array hasta que encuentre un número concreto, por ejemplo:

contarHasta(array , 0);

siendo 'array'

[1 5 2 7 2 7 2 0 4 6 2]

deberia contar los huecos que hay hasta el primer 0, es decir, 7.

5- Una función que reciba dos arrays enteros y devuelva otro array con la suma punto a punto, es decir, que haga la operación estilo:

[1 2 3] + [4 5 6] = [5 7 9]

Cuidado que los arrays deberán ser del mismo tamaño.

6-crear las funciones sumatorio y productorio del contenido de un array.

7- Crear un pequeño programa de gestión con la función menú que hicimos unas lecciones atrás. Al comenzar el programa se debe crear un array, inicializarlo a ceros y a continuación gestionar su estado mediante las opciones del menú, que deberán ser:

-borrar todo el contenido del array
-mostrar por pantalla el contenido
-añadir un número distinto de 0 al array
-comprobar cuantos huecos libres quedan en el array
-mostrar el sumatorio de los valores del array
-salir

2 2 respuestas
Leirlux

#173 ¿Eres consciente de que estás ayudando de una forma bestial a más de uno? Enhorabuena, favorito y a darle desde la lección 1 :P

Gracias ^^

1
B

Veamos.

Me "prohibiste" usar fprintf en las funciones, así que oído cocina.

asin ta' bien?

Siempre devuelve 1.

Mi intención es hacerlo así

Sigo sin entender los punteros.

3 respuestas
Lecherito

#175 Un puntero es una variable, pero en vez de contener un valor, o cadena, o lo que sea, contiene una dirección de memoria, por lo que en vez de manipular el valor en sí, manipulas lo que hay en esa dirección de memoria. Esto es lo básico, a partir de ahí puedes sacar lo demás, qué es lo que se te atasca?

1 1 respuesta
gonya707

#175 hombre obviamente si la funcion es para imprimir algpo tendras que usar fprintf dentro XD no tr lo tomes tan rigidamente. Tu segunda funcion deberia funcionar bien, el la primera es logico que te devuelva 1, haces solo una llamada a la funcion y en cuanto llega al return la primera vez ya se para

1 respuesta
B

#176 Cuando llega aquí

int *a = &(numeros[0]);

Mi cabeza hace tal que

El ejemplo que puso de la división lo logré entender, pero no sabría aplicarlo ahora a la lección de array.

#177 Entonces la 2º esta bien? (imprime todo correctamente).

2 respuestas
gonya707

#178 simplementeel nombre del array, numeros, es exactamente lo mismo que la direccion al primer miempre del array, un puntero, y ese puntero puedes almacenarlo en una variable int* como cualquier otro

1
Skiuv

#178 El "&" te da la dirección de "numeros[0]". Después la guarda en el puntero (apuntador, referencia...) "a".

int *a = &(numeros[0]);
int *b = &(numeros[1]);

En memoria se vería algo así:

FF |  0 | --> num[0]
FE | 23 | --> num[1]
FD | 82 | --> num[2]
FC | -3 | --> num[3]

a = 0xFF
b = 0xFE

1

Usuarios habituales