lunes, 25 de marzo de 2024
Threads (Hilos)
Threads (Hilos)
En esta entrada, se hablará sobre los Threads de la librería estándar de C++11. Se verá lo básico para poder empezar a utilizarlos.
Empecemos con una introducción: ¿Qué son los threads, o hilos? A la hora de crear un programa, generalmente nos encontraremos con que nuestro programa ejecuta las instrucciones en orden, una por una. Esto es correcto, hasta el momento en que necesitamos hacer varias tareas a la vez, ya sea por necesidad, por acelerar un proceso, o cualquier otra razón que se nos presente. Es en este punto, en que podemos utilizar los llamados "hilos" ("threads", por su nombre en inglés, y que usaré a partir de ahora).
Un thread se ejecutará de forma paralela a nuestro programa principal, ejecutando las mismas instrucciones u otras. Un thread tiene un comienzo (normalmente, cuando es creado), y tiene un final, que suele ser cuando ha terminado su propósito. Es posible tener todos los threads que se deseen a la vez. La única limitación será la que pudiese poner el Sistema Operativo y la lógica. También hay que tener en cuenta, que no por tener más threads van a ir los procesos necesariamente más rápido.
Antes de empezar con ellos, una anotación más: Los threads pueden compartir recursos. Por ejemplo, en C++, si tenemos una variable global y lanzamos un thread, ese thread puede acceder también a ella. Es importante tener en cuenta que si dos threads acceden a la vez al mismo recurso (especialmente si lo van a modificar), se pueden generar un comportamiento "aleatorio" ya que quizás un thread lo modifique primero y luego el otro, o viceversa. Lo mismo va para funciones y clases que trabajen con streams, como es hacer "cout << var;". Esto no lo voy a extender en esta entrada, pero aviso de forma anticipada de posibles problemas que pudiera ocurrir.
Dicho esto, vamos al código. Utilizaremos la librería , y de esta, la clase std::thread. La clase thread, tras ser construida, generará un thread, sin necesidad de llamar a ningún método suyo. Digo esto, porque en otros lenguajes, como Java, nos podemos encontrar con que haya que llamar a algún método suyo para iniciarlo.
El constructor de thread que utilizaremos, recibe de primer argumento, la función que ejecutará. Véase que los threads ejecutarán una función. Cuando termine la función, el thread terminará:
#include
#include
using namespace std;
void func(){
}
int main(){
thread th(func);
}
Si la función que llama el thread tiene parámetros, se los podemos pasar también. Para ello, usaremos el mismo constructor, pero agregaremos los parámetros al final.
#include
#include
using namespace std;
void func(int n, double m){
}
int main(){
thread th(func, 1, 5.7);
}
Es probable que al ejecutar estos códigos, el programa termine con un error. Esto ocurre si hay threads trabajando cuando se termina el programa (se termina la función main).
Para solucionar esto, hay 2 opciones. La primera, y la más recomendable generalmente, es esperar a que los threads terminen. Para ello, tenemos 2 métodos: join() y joinable(). Join espera hasta que el thread termine. Joinable nos dice si podemos hacer join a ese thread.
#include
#include
using namespace std;
void func(int n, double m){
cout << n << " " << m << endl;
}
int main(){
thread th(func, 1, 5.7);
if(th.joinable()) {
th.join();
}
}
La otra opción, es llamar al método detach(). Con ello, el thread deja de ser joinable. Además, ese thread no dará error si el programa termina mientras el thread está trabajando. Sin embargo, el thread será interrumpido bruscamente, cosa que no nos interesa.
Ahora que hemos visto lo básico, veamos algunos detalles.
Los parámetros de la función que le pasemos al thread pueden ser de cualquier tipo: tipos nativos, objetos, punteros, referencias... El único detalle, es que una referencia no se pasa de forma trivial. Hay que utilizar la función std::ref():
#include
#include
#include
using namespace std;
void func(bool& empezar){
while(!empezar){
this_thread::sleep_for(chrono::milliseconds(1));
}
cout << "B" << endl;
}
int main(){
bool empezar = false;
thread th(func, ref(empezar));
cout << "A" << endl;
empezar = true;
if(th.joinable()) {
th.join();
}
}
En este ejemplo, el thread esperará hasta que nuestra variable "empezar" se ponga a true.
La línea this_thread::sleep_for(chrono::milliseconds(1)); lo único que hace es esperar 1 milisegundo (para así evitar sobrecargar la CPU) (Los bucles vacíos como el del ejemplo suelen consumir bastante CPU).
Por supuesto, tal como he modificado "empezar" desde el main, también se puede modificar desde el thread. La memoria de la variable, es la misma. También se podría hacer con una variable global, aunque no recomiendo tener muchas variables globales en los programas.
Y aquí termina esta entrada. Hay muchos temas que ver sobre los threads, pero esos los examinaré en siguientes entradas. Si esta entrada os da la información suficiente para hacer algún programa con varios threads, habrá cumplido su propósito.
Serializacion de Clases
Hola de nuevo, tras tanto, tanto tiempo. En esta entrada, pondré algún ejemplo de cómo serializar una clase o estructura, para posteriormente guardarla en un archivo, enviarla por sockets, o cualquier otro fin que requiera convertir la clase o estructura a un arreglo de bytes.
/*Este texto no es de mi autoría, pertenece al blog Code 0x66*/
Para empezar, mostrar la clase que usaré en este ejemplo:
class Prueba{
public:
int n;
string s;
};
La clase Prueba tiene un campo entero, y otro campo string.
En caso de haber solo campos con tipos básicos, como por ejemplo 3 int, existe la posibilidad de hacer un casting del objeto a char*, de este modo:
Prueba1 p;
char* c = (char*)&p; // Tamaño: sizeof(p)
Convirtiendo la dirección del objeto a char* obtendremos una serialización inmediata. Pero, en caso de haber otras clases, o punteros dentro de esta clase, no es posible hacerlo mediante este método. La razón es simple: un campo int* solo guarda una dirección de memoria, y no los valores int.
Así que para serializar (y luego poder deserializar), estableceremos unas reglas. Los números, los guardaremos en formato binario, no en representación decimal.
Datos primitivos, como int o double: Los guardaremos tal como están en memoria, es decir, copiaremos su memoria directamente. Estos son los tipos de datos en los que funciona este método.
String y char*: Guardaremos, en primer lugar, un entero sin signo representando el número de caracteres que almacenan estas variables. Luego, naturalmente, los caracteres.
Esas reglas bastan para lo que se trata aquí. vamos al código:
class Prueba{
public:
int n;
string s;
string serializar(){
string t;
// int n
t.append((char*)&n, sizeof(n));
// string s
unsigned int size = s.size();
t.append((char*)&size, sizeof(size));
t.append(s.c_str(), size);
return t;
}
};
Al guardar el tamaño de la variable string, hemos antes guardado su tamaño en una variable a parte, ya que necesitamos acceder a sus bytes para guardarla de forma binaria. Si el campo que queremos serializar es una clase que también tiene su propio método para serializar, basta guardar su serialización en una variable string, y serializar esa string como tal (Numero de caracteres, Caracteres).
Si se guarda el número de elementos siempre antes de un arreglo o de una string, es para luego, a la hora de deserializar, poder leer del archivo la cantidad justa de bytes (Nº bytes = Nº de elementos * Bytes por elemento).
Y hasta aquí esta entrada. Es una pregunta muy recurrente que me he encontrado, y quería dar una explicación por aquí, "para que quede por si acaso". Obvio decir que este método de serialización es el que uso yo, pero habrá más, y cada uno puede serializar como quiera, según la clase, sus campos, etc. Lo más importante al serializar, es que se pueda deserializar, obteniendo un objeto idéntico al serializado.
Deserializacion de Clases
Deserialización de clases
En esta entrada vamos a terminar lo empezado en esta otra: Serialización de clases
Lo primero: Aquí voy a completar la clase creada en la otra entrada. Doy por supuesto que se sabe lo explicado en la entrada anterior.
Recordemos el código final de la entrada anterior:
class Prueba{
public:
int n;
string s;
string serializar(){
string t;
// int n
t.append((char*)&n, sizeof(n));
// string s
unsigned int size = s.size();
t.append((char*)&size, sizeof(size));
t.append(s.c_str(), size);
return t;
}
};
La idea ahora es deserializarlo, es decir, recuperar los datos guardados (serializados) en una string (la generada por el método "serializar").
Lo primero, veamos un ejemplo de main que utilizaremos para probar el correcto funcionamiento de ambas funciones:
int main() {
Prueba prueba;
prueba.n = 66;
prueba.s = "Code0x66";
String objetoSerializado = prueba.serializar();
prueba.n = 0;
prueba.s = "";
prueba.deserializar(objetoSerializado);
cout << prueba.n << '\n' << prueba.s << endl;
}
En orden:
Guardamos datos en el objeto "prueba"
Serializamos el objeto y guardamos el resultado en la variable "objetoSerializado"
Cambiamos los valores del objeto para asegurarnos de que la deserialización es correcta
Deserializamos a partir de la string almacenada en la variable, y mostramos los valores para corroborar su funcionamiento
Ahora hablemos del método "deserializar". Qué parámetros tendrá, qué retornará, cómo será...
Lo primero es que una deserialización puede ser correcta o no. En caso de que no lo sea, podemos informar lanzando una excepción, o, como haremos ahora, retornando un bool, que será "false" en caso de que hubiera error. (Un error se genera si la string a deserializar no tiene un formato correcto)
Una de las cosas más tediosas de la deserialización es aplacar errores y verificar la string. Esto lo haremos de último, para ver los problemas que puede dar.
El método recibirá como parámetro la string a deserializar, y retornará un bool indicando si ha deserializado correctamente.
Empecemos. Observando el método "serializar", podemos ver que genera una cadena con este formato (en binario):
[n (int)][s.size() (unsigned int)][s (char...)]
Lo primero, el orden. En el mismo orden que serializamos, deserializamos. Así que empezaremos por la variable "n", que es int. En el comienzo del método, guardaremos en un char* la cadena, ya que será más cómodo trabajar así con ella.
bool deserializar(const string& str){
const char* cadena = str.c_str();
int indice = 0; // Indice de la cadena por el que vamos
// Haciendo un cast de char* a int*, interpretamos la memoria como int
// Con lo cual, al desreferenciarla (*), obtenemos el int tal y como lo guardamos
int nuevaN = * (int*) cadena;
// Incrementamos el indice de la cadena
indice += sizeof(int);
// Aquí ya sumamos el indice a la cadena, para saltar lo ya leido
// Podría haberse hecho la primera vez también
// Pero es superfluo y lo dejo asi como ejemplo
unsigned int sizeS = * (unsigned int*) (cadena + indice);
indice += sizeof(unsigned int);
// Aqui toca leer caracteres. Pero aprovecharemos que tenemos la string
// con su metodo substr(indiceInicial, cantidad)
string nuevaS = str.substr(indice, sizeS);
// Por ultimo, asignamos las variables
s = nuevaS;
n = nuevaN;
}
Ya está el código explicado. Comentemos algunos detalles, empezando por lo último. La razón de que se asignen los campos del objeto al final es simple: Asegurar la integridad del objeto. O se deserializa todo, o no se deserializa nada. En caso de error, el objeto estará exactamente igual que antes.
Y hablando de errores, veréis que no he puesto ningún return, y es una función bool.
¿Qué clase de errores se pueden generar aquí? Por orden de aparición:
El primer int son sizeof(int) bytes (generalmente, 4 char). Habría error si la cadena a deserializar tuviera menos de 4 caracteres (estaríamos leyendo memoria fuera de la cadena).
Lo mismo con el tamaño del campo "s", solo que acumulamos tamaño. 4 anteriores más 4 bytes del unsigned int hacen 8 caracteres. Si la string tiene menos de 8 caracteres, habrá un error. Esto lo podemos comprobar ya al principio, y obviamos la primera comprobación. 8 es el tamaño mínimo de la cadena. Ahora, veamos el tamaño justo.
Cuando leamos el tamaño de la cadena, sabremos el tamaño total exacto de la string a deserializar, que será 4 + 4 + tamañoCadena. Lo único a remarcar, y no menos importante, es que esta comprobación a de ir exactamente en el momento en que tengamos los datos necesarios (el tamaño de la cadena).
Bien pues, veamos como quedaría si retornamos "false" cuando la cadena no tiene el tamaño mínimo (sin los comentarios de antes):
bool deserializar(const string& str){
const char* cadena = str.c_str();
int indice = 0;
// Primera condición
if(str.size() < sizeof(int) + sizeof(unsigned int)){
return false;
}
int nuevaN = * (int*) cadena;
indice += sizeof(int);
unsigned int sizeS = * (unsigned int*) (cadena + indice);
indice += sizeof(unsigned int);
// Segunda condición
if(str.size() < sizeof(int) + sizeof(unsigned int) + sizeS){
return false;
}
string nuevaS = str.substr(indice, sizeS);
s = nuevaS;
n = nuevaN;
// No olvidar el retorno "true" por defecto
return true;
}
Con esto, queda casi listo. Queda hacer el código para probarlo. He aquí el main:
int main() {
Prueba prueba;
prueba.n = 66;
prueba.s = "Code0x66";
string objetoSerializado = prueba.serializar();
prueba.n = 0;
prueba.s = "";
if(prueba.deserializar(objetoSerializado)){
cout << prueba.n << '\n' << prueba.s << endl;
}else{
cout << "Error deserializando" << endl;
}
}
Podéis probar el retorno false dándole una cadena incorrecta, por ejemplo: prueba.deserializar("test");
Un detalle más. También podéis retornar false en caso de que la cadena no tenga exactamente el tamaño que debe tener, es decir, que es más grande de lo que vosotros vais a consumir. Eso podría hacerse cambiando el operador "<" del último if por el operador "!=". Si es diferente al tamaño que pedimos, error: if(str.size() != sizeof(int) + sizeof(unsigned int) + sizeS)
Para probarlo, basta hacer: prueba.deserializar(objetoSerializado + "datos extra")
Y con esto, queda lista la deserialización. Es un proceso más lento y tedioso que la serialización en cuanto a tiempo programando, principalmente por todas las comprobaciones que haremos, pero es un proceso necesario.
Por último, y para despedir esta entrada, hay otra forma de hacer un método de deserialización. Deserializar no necesita que exista un objeto; deserializar va a generar un objeto a partir de una cadena (en este ejemplo, realmente estamos generando otro objeto y poniéndolo encima del que ya teníamos). Así pues, otra forma de hacerlo, sería con un método static. La forma de llamarlo podría ser esta, tirando excepción si falla (no lo voy a definir, eso es cosa del que lo quiera hacer):
Prueba prueba = Prueba::deserializar(objetoSerializado);
Y ahora sí, cierro entrada. Hace tiempo que tenía esta entrada pendiente; fue gracias a un comentario que lo recordé ;)
Un saludo, hasta próximas entradas!
Introduccion a ASM
Introducción a Ensamblador
/*Este texto no es de mi autoria, es del blog code 0x66.*/
Hola de nuevo. Hace ya dos semanas que no subo ninguna entrada, así que aquí va. Vamos a ver una breve introducción a Ensamblador, para quién lo quiera programar.
No es el lenguaje que recomiendo para empezar a programar, pero es un lenguaje muy importante.
Con el Cheat-Engine, con el botón "Memory View", podéis ver todas las instrucciones del proceso elegido. Las instrucciones de los programas, están en hexadecimal, que se traduce a Ensamblador. Con el Cheat-Engine podéis echarle un vistazo. Ahora nos centraremos en programar Ensamblador, y hacer programas con él.
Para empezar, un compilador. Yo uso Flat Assembler. Si lo queréis, lo tenéis en su web: flatassembler.net.
Ensamblador es un lenguaje sencillo, en el sentido de que hay unas 256 instrucciones diferentes (de las cuales la mayoría son parecidas entre sí). Además, también habrá que llamar a funciones de librerías, como la WinApi, por ejemplo, o hacer vuestras propias funciones. Por otra parte, ASM es un lenguaje rápido, por el simple hecho de que las instrucciones que escribáis, serán aproximadamente las mismas que haya en memoria. Es decir: el código que escribáis, será el código del proceso.
Una vez descargado el FASM (u otro), lo abrimos, y empezamos un nuevo programa. Para los nuevos en ASM, usaré esta entrada para explicar algunas isntrucciones en ASM, y otras cosas básicas.
Empecemos:
En ASM, no hay variables, como en otros lenguajes. Hay registros y la pila o stack. Para tratar con datos, usaremos los registros. Para almacenar datos, usaremos la pila. (Generalmente: En la pila se suele guardar punteros a datos además de datos en sí)
Los registros básicos son: AX, BX, CX y DX. Estos registros, además, se pueden descomponer en 2 partes: HIGH y LOW (AH y AL o BH y BL).
Veamos más claramente cómo son los registros (en hexadecimal):
(8 bits = 1 byte = '00')
RAX: 64bits 00 00 00 00 00 00 00 00
EAX: 32 bits 00 00 00 00
AX: 16 bits 00 00
AH: 8 bits 00 . .
AL: 8 bits . . 00
Según el hardware, se suelen usar actualmente RAX o EAX. Esto no excluye de usar los demás.
La pila es donde se guarda información, llamadas a funciones, etc. Es una zona de memoria, donde "Lo último en escribirse, es lo primero en leerse", o lo que es lo mismo, "Lo último en entrar, es lo primero en salir". Simplemente, para que os hagáis una idea, es como una pila de libros. El último libro que apiles, será el primero en cogerse.
Igulamente, también podréis acceder al "centro" de la pila. Pero eso ya se verá más adelante.
Para acceder a la pila, necesitamos un puntero. Un puntero que apunte a ella. Para ello, tenemos otros 2 registros: EBP y ESP. Cada uno de ellos apunta a la pila.Ya veremos más adelante las diferencias entre los dos.
Además, hay otro pequeño pero muy importante "registro": las flags o banderas. Estas tienen 2 valores: 1 y 0. Cada una de ellas, se activa o desactiva (1 o 0) dependiendo del resultado de algunas operaciones. Por ejemplo, la flag zero, se activa si alguna operación da 0.
Ahora que vimos lo básico, empezaremos con algunas instrucciones:
MOV A,B: Esta instrucción, copia B sobre A. A será igual a B. Se puede poner: MOV EAX, 8. Así, el registro EAX será igual a 8. Suponiendo que EAX fuera un puntero a una zona de memoria, y quisieramos copiar 8 a esa dirección, haríamos: MOV [EAX],8. Los corchetes indican que tratamos ese valor como un puntero. También podríamos poner: MOV EAX,[EBX]. Esto copiaría el valor del puntero EBX a EAX. Múltiples posibilidades.
INC A: Incrementa A en 1. Si EAX fuera 10, y ejecutamos INC EAX, EAX pasaría a ser 11.
DEC A: Decrementa A. Igual que INC, pero decrementando.
ADD A,B: Añade B a A. A será igual a A+B.
DEC A,B: Resta B a A. A será igual a A-B.
AND A,B: Operación AND a nivel de bit. El resultado se guarda en A.
OR A,B: Operación OR.
XOR A,B: Operación XOR u OR-exclusiva.
CMP A.B: Esta es muy impotante. Resta B a A, pero no guarda el resultado. Simplemente, se activan las flags correspondientes. Se usa en conjunto con saltos condicionales.
TEST A,B: Como CMP, pero en evz de una resta, hace una operación AND a nivel de bit.
JMP A: Mueve el registro IP (guarda la dirección de la instrucción actual) a la dirección A. Es decir, salta a otra parte del código.
Saltos condicionales: Actuan como JMP, pero saltan o no según las flags activas. Por ejemplo, el salto JZ, "jump if zero" salta solo si está activa la flag zero. JNZ, sería lo contrario. Salta si la flag zero NO está activa. Generalmente se usa CMP antes de un salto condicional.
CALL A: Hace un salto como JMP, pero, además, guarda la dirección de la instrucción en la pila, entre otras. Se usa para llamar a subprogramas, a "funciones".
RET A: Pone el registro de instrucción en la instrucción marcada por el registro de pila EBP más A. Se puede no poner A, y dejarlo como "RET". En resumen, esto retorna a la dirección desde la que se llamó a la función con un CALL.
;comentario: Esto no es una instrucción, y no aparecerá en el ejecutable compilado, pero cuando se coloca ';', lo que le sigue es un comentario.
Además, hay instrucciones para trabajar con decimales (float). Pero eso ya no entra aquí. Para que podáis compilar, y hacer pruebas, os dejo un pequeño código, para que vayáis descifrando. Además, FASM trae en su carpeta, la carpeta "EXAMPLES", donde veréis códigos variados, todos ellos en ASM.
format PE GUI 4.0
section '.data' data readable writeable
var db 'Cadena' ; db : Pone en memoria la cadena
; para acceder a ella, se usa el identificador,
; en este caso "var"
include 'win32ax.inc' ; WinApi. Include sirve para usar funciones de librerías
.code ;Empieza el código
start: ; Esto es una etiqueta. Con esto se puede hacer JMP .
call icf_init ;llamada a una función. (Más abajo está la etiqueta)
mov eax,var ;EAX tendrá la dirección de la cadena (var)
call write ;otra etiqueta
invoke MessageBoxA,HWND_DESKTOP,var,"Titulo",0 ; Una ventana (MessageBox)
invoke Sleep,-1 ;WinApi. Deja el programa en pausa
.end start
icf_init:
invoke AllocConsole
ret
malloc: ; EAX: Size
invoke VirtualAlloc,NULL,eax,MEM_COMMIT+MEM_RESERVE,PAGE_READWRITE
ret
free: ; EAX: Address
invoke VirtualFree,eax,edx,MEM_RELEASE
ret
write: ; EAX: String
push ECX ;Uso esta función para escribir en consola
push EDX
mov edx,eax
xor ecx,ecx ;en ECX se guardará el tamaño de la string
dec edx ;para compensar el inc que sobra en el loop
write_loop_1:
inc edx
inc ecx
cmp byte[edx],0 ;hasta que el char de la string sea 0
jnz write_loop_1
dec ecx ;se elimina el contador sobrante.
invoke WriteConsole,,eax,ecx,1,0
pop EDX
pop ECX
ret
Suerte, y hasta una próxima entrada!
PD: Las funciones las hice yo, y quizás tengan fallos. Tomadlas solo como ejemplos. Si algo no os funciona, podéis dejar un comentario, y veré que puede pasar.
Algoritmos Recursivos vs Iterativos
Algoritmos
Recursivos vs Iterativos
En esta entrada, veremos, en lenguaje de programación C++, diferencias y ejemplos entre estos dos tipos de algoritmos: Los algoritmos recursivos, y los algoritmos iterativos.
Veremos esto tomando como ejemplo la Sucesión de Fibonacci (0,1,1,2,3,5,8...).
Para hayar un término n de la sucesión, tenemos esta función:
F(n) = 0 si n= 0.
F(n) = 1 si n= 1. y para cada numero se cunple que: F(n-1)+F(n-2) si n > 1
En resumen: cada término, es la suma de los dos anteriores. Salvo el término 0 y el 1, que son 0 y 1, respectivamente.
Una función recursiva, es la que se llama a si misma. Para que una función recursiva tenga fin, ha de tener una condición, como en este ejemplo: "si n=0" o "si n=1".
Veamos la función Fibonacci recursiva en C++:
uint64_t fib_recursivo(uint64_t n){
if(n==0) return 0;
if(n==1) return 1;
return fib_recursivo(n-1) + fib_recursivo(n-2);
}
PD: uint64_t es lo mismo que unsigned long long int.
En esta sencilla función, vemos claramente la definición de la serie de Fibonacci. N es el término de la serie que queremos conseguir.
Ahora veamos la forma iterativa:
uint64_t fib_iterativo(uint64_t n){
if(n==0) return 0;
if(n==1) return 1;
uint64_t a=0, b=1, c=0;;
for(int i=2; i<=n; i++){
c = a + b;
a = b;
b = c;
}
return c;
}
En la iterativa, vamos generando los términos en orden hasta llegar al que buscamos. en la variable 'a', guardamos lo que sería f(n-2), y en 'b', f(n-1). Luego, con el ciclo, igualamos 'c' a 'a' + 'b' ( f(n-2) + f(n-1) ).
Entre los algoritmos iterativos y los recursivos, suele haber estas diferencias básicas:
La forma recursiva, es mucho más lenta que la forma iterativa, especialmente para números "grandes" (Ej. Para N=100, fib_recursivo() tardará mucho en terminar)
La forma recursiva suele ser más sencilla de realizar que la iterativa. Aunque esto depende de la sucesión/algoritmo que busquemos.
A parte, os muestro otra función, parecida a la iterativa, pero con algunos cambios:
uint64_t fib_mezcla(uint64_t n){
static vector v;
if(!v.size()){
v.push_back(0);
v.push_back(1);
}
if(v.size()<=n)
for(uint64_t i=v.size(); i<=n; i++)
v.push_back(v[i-1] + v[i-2]);
return v[n];
}
Esta, lo que hace es guardar los valores que obtiene (por el método iterativo) en un vector static.
Al ser una variable static, no será borrada al acabar la función, lo que significa que guardará los valores que tiene cada vez q llamemos a la función. De esta manera, ahorramos tiempo, ya que no tenemos que calcular (salvo la primera vez), cada término de fibbonaci. A cambio, tiene un mayor gasto de memoria. Pero en este caso, dado que apenas guardará más de 87 términos (a partir de ahí se sale del tamaño de una variable de 64 bits), apenas notaremos su gasto de memoria.
Aquí he hecho unas pruebas comparativas de las 3 funciones:
En la primera prueba, se mide el tiempo que tarda en llamar 1 vez a la función para el término 40. Como podemos observar, el método recursivo tarda más de 4 segundos, mientras que los demás, apenas tardan unos milisegundos.
En la segunda prueba, omito la función recursiva, ya que tardaría un tiempo o años en acabar. Aquí podemos ver lo que tardan los otros métodos al ser llamados 1.000.000 veces, para el término 85. Aquí es donde podremos apreciar la diferencia entre el método iterativo tradicional, y el método donde se guardan los valores. El método iterativo, se llama 1.000.000 veces, y las 999.999 veces hace todo el ciclo desde el 1 hasta el 85. En cambio, el otro método hace solo 1 vez el ciclo, y las otras 999.999 veces, simplemente retorna el valor guardado en el vector.
Y hasta aquí este resumen sobre los pros y contras de las funciones recursivas e iterativas. En vuestros programas, os recomiendo poner un límite para las funciones recursivas que podrían tardar mucho, como la de fibonacci, para así evitar que el programa se detenga.
Manejo de Archivos
Manejo de archivos[Python]
/*Este texto no es de mi autoría, es de Overload's blog.*/
Aquí va el primer miniartículo de la revista, como dice en el título manejo de archivos mediante la función open.
Bueno, la función open toma como parámetros el archivo a abrir y el modo, es decir, lectura, escritura, etc.
Sería así:
#!usr/bin/env python
open('archivo.txt', 'r')
Con este código se abriría al archivo 'archivo.txt' en modo de lectura.
Los parámetros son estos:
'r' - Modo lectura ('r'ead), si no existe el archivo nos salta una excepción IOError
'w' - Modo escritura ('w'rite), se abre el archivo para poder escribirlo, si no existe se crea. Además
sobreescribe las cosas en el archivo, es decir, las borra y escribe las nuevas.
'b' - Abre el archivo en modo binario ('b'inary)
'a' - Modo añadir('a'ppend), abre el archivo en modo w, es decir, modo escritura. Pero este en cambio no
sobreescribe sino que escribe las cosas al final del archivo.
Bien ahora veamos los métodos que tiene open para manejar archivos:
Métodos relacionados al parámetro 'r':
Método read:
Su nombre lo indica todo...(leer :-p)
abrir = open('archivo.txt', 'r') #abrimos el archivo en modo lectura
leer = abrir.read() #leemos el archivo
print leer #y mostramos por pantalla lo leído
Método readline:
Lee las líneas del archivo una por una:
abrir = open('archivo.txt', 'r') #abrimos el archivo en modo lectura
leer = abrir.readline() #leemos el archivo linea a linea
print leer #y mostramos por pantalla lo leído
Método readlines:
Lee las líneas pero devuelve una lista con cada una de ellas es decir:
Tenemos un archivo.txt:
hola
adiós
hasta luego
Y el código python:
abrir = open('archivo.txt', 'r') #abrimos el archivo en modo lectura
leer = abrir.readlines() #leemos el archivo
print leer #y mostramos por pantalla lo leído
Python nos mostrará:
['hola\n', 'adiós\n', 'hasta luego']
Métodos relacionados al parámetro 'w' y 'a':
Nota*: El método write pierde la capacidad de sobreescribir(borrar lo escrito y escribir algo nuevo) cuando especificamos el parámetro 'a'.
Método write:
Escribe en el archivo tomando como parámetro una cadena y la sobreescribe.(Como explique antes si ponemos de parámetro 'a' pierde la capacidad de sobreescribirse)
Ejemplo:
abrir = open('archivo.txt', 'w')
escribir = abrir.write('bon dia')
Método writelines:
Escribe el archivo tomando como parámetro una cadena pero no la sobreescribe sino que la pone al lado:
abrir = open('archivo.txt', 'w')
escri = abrir.writelines('bon dia')
¿Qué pasa si quiero añadir un salto de línea?, pues añadimos \n.
abrir = open('archivo.txt', 'a')
escri = abrir.writelines('bon dia'+'\n')
Métodos relacionados con el parámetro 'b':
Se utiliza normalmente para el manejo de datos externos a tu ordenador, datos ftp, http, tcp/ip, etc.
En fin, esto es todo!
Un saludo a todos!!!
Recoger datos de CMD
Minitutorial recoger datos de cmd [Python]
/*Este texto no es de mi autoria, pertenece al Overload's blog.*/
En algunos exploits veréis que os dice en las instrucciones:
Modo de uso: exploit.py
Y supongo que os habréis preguntado como hace python para recoger esos datos que tu introduces ahí.
Bien pues os lo explico que es muy facilito.
Existe una librería llamada sys para manejar con cosas del sistema de ahí viene su nombre(sys - system).
Una de esas cosas es recoger datos de la linea de comandos de cmd. ¿Cómo se hace esto?, pues con
el método argv...
Os lo explico paso a paso:
#!/usr/bin/env python
import sys #importamos la librería sys
host = sys.argv[1] #bien, aquí es donde se recogen los datos: el método argv #contiene un array con los datos que tu introduzcas, en este caso se asignará al #segundo dato del array el string que vaya después de exploit.py (el primer dato #sería exploit.py y el segundo lo que hayamos introducido sys.argv =
#['exploit.py','loquehayasintroducido'])
print host # nos imprimiria sys.argv[1] que es lo que hayamos introducido
Bueno esto es, no es más porque es muy fácil ^^
Un saludo!!
Suscribirse a:
Entradas (Atom)