Punteros arduino.
Los punteros son uno de los temas más difíciles de la programación, intentaré explicarlo de forma más sencilla y cercana a la práctica. Empecemos por la representación de los datos en la memoria del microcontrolador: en la lección sobre operaciones de bits, comentamos que el bloque de memoria mínimo direccionable es un byte, es decir, podemos hacer referencia a cualquier byte de la memoria del microcontrolador. Cuando trabajamos con variables, no pensamos en las direcciones y la ubicación de los datos en la memoria, solo usamos sus nombres para leer / escribir, pasar los nombres como parámetros en la función y realizar otras acciones con los datos. Poseer las direcciones de los bloques de datos le permite hacer muchas cosas de manera más rápida y eficiente en términos de memoria. Algunos ejemplos de las posibilidades que ofrecen los punteros:
- Usando punteros, puede dividir cualquier dato (variables de todo tipo, estructuras) en bits (flujos de bits) para su posterior manipulación con ellos (transferencia / escritura / lectura);
- Puede pasar direcciones de bloques de datos como argumentos a funciones, de modo que cuando se llame a la función, no se creen copias de las variables y el código se ejecute más rápido. En otras palabras, habilita la función para cambiar el argumento pasado;
- Trabajar con memoria dinámica «directamente», creando matrices dinámicas de cualquier tamaño con acceso rápido a los datos. Lea la lección sobre memoria dinámica.
¿Qué es un puntero? Esta es una variable que contiene la dirección del área de datos (variable / estructura / objeto / función, etc.) en la memoria del microcontrolador, más precisamente, su primer bloque, un byte. Sabemos que todos los datos constan de bytes, de diferentes números, conociendo la dirección del primer byte en un bloque de datos, podemos tener control sobre los datos en esta dirección, pero necesitamos saber el tamaño de este bloque. En realidad, al crear un puntero, indicamos a qué tipo de datos apunta, un puntero puede apuntar a cualquier tipo de datos. Por sí mismo, el puntero almacena la dirección, agregando 1 al puntero, obtenemos la dirección del siguiente bloque de memoria.
Pasemos a los operadores que nos permiten trabajar con punteros.
Tenemos algunos operadores, son una de las características más poderosas del lenguaje C ++:
- & – devuelve la dirección de los datos en la memoria (la dirección del primer bloque de datos)
- * – devuelve el valor en la dirección especificada
- -> – operador de indirección a miembros y métodos (para punteros a estructuras y clases). Es una abreviatura de la construcción mediante un puntero: a –> b equivalente a: ( * a ) .b
¿Cómo funciona? Podemos crear un puntero al tipo de datos deseado como este:
tipo_datos* nombre_pointer; tipo_datos * nombre_pointer; tipo_datos *nombre_pointer;
Sí, se puede confundir con la multiplicación, pero el compilador no lo confundirá. Las tres opciones de notación son equivalentes, en diferentes artículos / códigos puede encontrar las tres opciones, esté preparado para esto. Después de la declaración, usted y yo tenemos un puntero, una variable que puede almacenar la dirección de otra «variable» del tipo de datos especificado: estas pueden ser variables ordinarias de todos los tipos, matrices, cadenas, funciones, estructuras, objetos e incluso vacío –void… Echemos un vistazo a los punteros a diferentes tipos de datos por separado para cubrir todas sus posibilidades a la vez.
Punteros a variables «regulares».
Trabajar con un puntero le permite leer / cambiar el valor de una variable a través de su dirección. Vea un ejemplo con comentarios:
byte b; // una variable de tipo byte b = 10; // b ahora es 10 byte * ptr; // ptr - variable "puntero a objeto de tipo byte" ptr = & b; // el puntero ptr almacena la dirección de la variable b * ptr = 24; // b ahora tiene 24 (escribe en & b) byte s; // variable s s = * ptr; // s ahora también es 24 (leer en & b)
Parece no ser nada complicado: creó un puntero ptr en byte, es decir byte * ptr y escribí la dirección de la variable b en él: ptr = & b… Ahora tenemos acceso sobre la variable b, a través del puntero ptr, podemos cambiar su valor de esta manera: * ptr = valor;
Intentemos pasar una dirección a una función y cambiar el valor de una variable por su dirección dentro de la función. Tengamos una función que tome la dirección de una variable de tipo int y eleva al cuadrado esta variable.
void square(int* val) { *val = *val * *val; }
Así es como lo usaremos:
int value = 7; creó una variable square(&value); // pasó su dirección a la función // aquí el valor ya es == 49
¿Por qué es bueno este enfoque? No creamos una copia de la variable, como hicimos en la lección sobre funciones, pasamos la dirección y cambiamos los valores directamente. Esto es de lo que estoy hablando:
void setup () { valor int = 7; // creó una variable valor = cuadrado ( valor ) ; // aquí el valor ya es == 49 } int cuadrado ( int val ) { return val * val; }
Aquí, se crea una copia de la variable en RAM, interactuamos con esta copia, y luego la devolvemos y la asignamos. ¡Este código corre mucho más lento!
Punteros a matriz en Arduino.
Tuvimos una lección separada sobre matrices, y allí no te cargué y te dije «de dónde vienen las matrices», porque las matrices son en realidad un puntero y sus amigos. ¿Qué es una matriz en general y cómo funciona? El nombre de la matriz es un puntero al primer elemento de esta matriz (establecemos el tipo de elemento al declarar la matriz), es decir myArray [ 0 ] == * myArray, o así: myArray == & myArray [ 0 ]… Para que sea más fácil escribir y leer el código, se introducen corchetes, pero de hecho funciona así: a [ b ] == * ( a + b )! Una matriz es un área de memoria llena de «variables» del tipo especificado, y podemos referirnos a ellas. Un par de ejemplos sobre cómo trabajar con una matriz sin usar corchetes:
void setup() { Serial.begin(9600); // trabajar sin [] corchetes byte myArray[] = {1, 2, 3, 4, 5}; // imprime 1 2 3 4 5 for (byte i = 0; i < 5; i++) { Serial.print(*(myArray + i)); Serial.print(' '); } Serial.println(); // trabajar con un puntero separado int myArray2[] = {10, 20, 30, 40, 50}; int* ptr2 = myArray2; // puntero a la matriz // imprime 10 20 30 40 50 for (byte i = 0; i < 5; i++) { Serial.print(*ptr2); ptr2++; Serial.print(' '); } }
Preste atención al segundo ejemplo: el bucle aumenta el puntero en uno, ptr2 ++, por lo tanto, se lleva a cabo el «cambio» al siguiente elemento de la matriz.
Esta disposición de matrices le permite pasarlas como argumentos a funciones sin ningún problema. Ejemplo: una función que devuelve la suma de todos los elementos de una matriz:
void setup() { Serial.begin(9600); int myArray[] = {1, 2, 3, 4, 5, 6}; Serial.println(sumArray(myArray)); } int sumArray(int* arrayPtr) { int sum = 0; / suma la matriz for (byte i = 0; i < 6; i++) { sum += arrayPtr[i]; } return sum; // regresa }
Un punto importante: la matriz “no sabe” de qué tamaño es, es solo un área de memoria asignada. Para la universalidad de este enfoque, es necesario conocer el tamaño de la matriz de antemano o pasarlo como un argumento:
void setup() { Serial.begin(9600); int myArray[] = {1, 2, 3, 4, 5, 6}; //muestra la suma de la matriz Serial.println( sumArray(myArray, sizeof(myArray)) ); // pasó la matriz y su tamaño (en bytes !!!) } int sumArray(int* arrayPtr, int arrSize) { int sum = 0; // encuentra el tamaño de la matriz en el número de elementos, // dividiendo el tamaño en bytes por el peso de cualquier miembro de la matriz arrSize = arrSize / sizeof(arrayPtr[0]); // suma la matriz for (byte i = 0; i < arrSize; i++) { sum += arrayPtr[i];} return sum; // regreso }
Puntero a función en Arduino.
La función también tiene su propia dirección en la memoria, en la que se puede acceder. Una función puede ser llamada simplemente por un puntero, o puede pasarla como argumento a otra función, y esto se puede hacer en otros archivos, en clases y bibliotecas. Un puntero de función se declara así:
return_dattype ( * nombre ) ( argumentos )
Luego, al puntero se le puede pasar la dirección de cualquier función (solo con el nombre, el operador de dirección, como en el caso de las matrices, no es necesario):
void setup() { Serial.begin(9600); void (*ptrF)(byte a); //puntero de función (se declara a continuación) ptrF = printByte; //da la dirección de la función printByte ptrF(125); // llamar a printByte a través de un puntero (salidas 125) int (*ptrFunc)(byte a, byte b); // hacer otro puntero ptrFunc = sumFunc; //para la función sumFunc // llama a printByte, que pasaremos el resultado a sumFunc // a través del puntero ptrFunc ptrF(ptrFunc(10, 30)); // imprimirá 40 } void printByte(byte b) { Serial.println(b); } int sumFunc(byte a, byte b) { return (a + b); } void loop() {}
De esta manera, puede implementar una característica de estilo «attachInterrupt()» en la biblioteca: almacenar la dirección de la función en la clase y llamarla desde la clase.
Puntero a estructuras y clases en Arduino.
Las estructuras y clases (también hay enumeraciones) son tipos de datos compuestos, el mecanismo de interacción a través de punteros es ligeramente diferente aquí. Creemos una estructura, un puntero a ella y accedamos a la estructura a través de él:
struct myStruct { byte myByte; int myInt; } ; // crear estructura someStruct myStruct someStruct; // puntero de tipo myStruct * para estructurar someStruct myStruct * p = & someStruct; // escribe en la dirección en someStruct.myInt p- > myInt = -666; //(*p).myInt = -666; // más o menos, vea el comienzo de la lección
De esta manera, puede pasar grandes estructuras sin hacer una copia en variables formales, ¡mucho más rápido! Todo será exactamente igual con las clases.
Puntero a Void.
En todos los ejemplos anteriores, creamos un puntero a un tipo de datos conocido. Pero, ¿qué sucede si desea transferir una dirección a un tipo de datos desconocido? Se puede hacer void * ptr – puntero a vacío, para cualquier tipo! Pero esto solo se suma a los problemas, ¿cómo trabajamos con este puntero? Para empezar, el puntero «void» se puede convertir más tarde a cualquier tipo deseado mediante una conversión:
float Fval = 0,254 ; // variable flotante void * ptrV = & Fval; // puntero a ?, (le dio un float, no le importa) // creado Fptr - puntero para float // y convirtió el ptrV desconocido en float float * Fptr = ( float * ) ptrV; // ahora * Fptr es 0.254
Aquí hemos transformado ptrV, que era void * (puntero a void), en un puntero a float con ayuda ( float* ). A veces, esto puede resultar conveniente, por ejemplo, cuando se transfieren datos de diferentes formatos utilizando una «función universal». También puede ver la conversión del tipo de puntero a través de cast (para obtener más detalles, consulte la lección sobre tipos de datos ):
float* Fptr = static_cast<float*>(ptrV);
Desglose en Bytes.
A veces es necesario transferir algunos datos y luego recibirlos del otro lado. O escribir estos datos en algún medio externo (EEPROM, tarjeta de memoria, etc.) y luego volver a leerlos. Necesitamos una herramienta universal que anote cualquier fecha y luego la lea correctamente. Para resolver este problema, puede usar punteros, esto se hace de la siguiente manera: cree un puntero al tipo byte, y asígnele la dirección de un bloque de datos de cualquier tipo realizando la transformación ( byte * ), solo obtenemos un puntero al primer byte de datos. Sabiendo la longitud (tamaño en bytes) de nuestro fragmento de datos, podemos leerlo byte a byte simplemente agregando uno a la dirección. Veamos un ejemplo simple con división de números de 4 bytes de largo usando punteros:
// Número grande uint32_t bigVal = 123456789; // puntero ptrB a la dirección & bigVal // convertir a (byte *) byte * ptrB = ( byte * ) & bigVal; // dividir en bytes byte bigVal_1 = * ptrB; byte bigVal_2 = * ( ptrB + 1 ) ; byte bigVal_3 = * ( ptrB + 2 ) ; byte bigVal_4 = * ( ptrB + 3 ) ; // intenta volver a armarlo // necesita una nueva variable // mismo tipo que el primero (uint32_t) uint32_t newBig; // toma su dirección byte * ptrN = ( byte * ) & newBig; // ¡y recupera 4 bytes! * ptrN = bigVal_1; * ( ptrN + 1 ) = bigVal_2; * ( ptrN + 2 ) = bigVal_3; * ( ptrN + 3 ) = bigVal_4; // en este punto newBig es 123456789
Por lo tanto, puede «analizar» y «recopilar» cualquier dato (matriz de cualquier tipo, estructura), conociendo su tamaño.
El problema se puede resolver de una manera más bella utilizando una matriz de bytes para leer y escribir. Considere un ejemplo con la conversión de un tipo de puntero a través de( byte * ), mediante void* y mediante template:
Ejemplo vía (byte*)
// buffer byte buffer[20]; // estructura para la prueba struct myStruct { byte val1; int val2; long val3; float val4; } ; void setup () { // === prueba con variables === long a = 123456789; long b = 0; // dividir el bloque de datos a en bytes // y almacenar en búfer writeData (( byte * ) & a, sizeof ( a )) ; // recopilar el bloque de datos b // desde el búfer del búfer readData (( byte * ) & b, sizeof ( b )) ; // aquí b == 123456789 // === prueba con estructuras === // crear estructura myStruct transmit; // asigna un valor al miembro val4 transmit. val4 = 3,1415 ; // estructura "receptora" myStruct recieve; // dividir el bloque de datos de transmisión en bytes // y almacenar en búfer writeData (( byte * ) & transmit, sizeof ( transmit )) ; // recopila el bloque de datos de recepción // desde el búfer del búfer readData (( byte * ) & recieve, sizeof ( recieve )) ; // recieve.val4 aquí == 3.1415 } void writeData ( byte * datos, int longitud ) { int i = 0; while ( longitud-- ) { buffer [ i ] = * ( datos + i ) ; i ++; } } void readData ( byte * datos, int longitud ) { int i = 0; while ( longitud-- ) { * ( datos + i ) = buffer [ i ] ; i ++; } } void loop ()
Ejemplo a través de void*
// buffer byte buffer[20]; // estructura para la prueba struct myStruct { byte val1; int val2; long val3; float val4; } ; void setup() { // === prueba con variables === long a = 123456789; long b = 0; // dividir el bloque de datos a en bytes // y almacenar en búfer writeData ( & a, sizeof ( a )) ; // recopilar el bloque de datos b // desde el búfer del búfer readData ( & b, sizeof ( b )) ; // aquí b == 123456789 // === prueba con estructuras === // crear estructura myStruct transmit; // asigna un valor al miembro val4 transmit. val4 = 3,1415 ; // estructura "receptora" myStruct recieve; // dividir el bloque de datos de transmisión en bytes // y almacenar en búfer writeData ( & transmit, sizeof ( transmit )) ; // recopila el bloque de datos de recepción // desde el búfer del búfer readData ( & recieve, sizeof ( recieve )) ; // recieve.val4 aquí == 3.1415 } void writeData ( void * data, int length ) { uint8_t * dataByte = ( uint8_t * ) data; int i = 0; while ( length-- ) { búfer [ i ] = * ( dataByte + i ) ; i ++; } } void readData ( void * data, int length ) { uint8_t * dataByte = ( uint8_t * ) data; int i = 0; while ( length-- ) { * ( dataByte + i ) = buffer [ i ] ; i ++; } } void loop () {}
Ejemplo mediante template
// buffer byte buffer[20]; // estructura para la prueba struct myStruct { byte val1; int val2; long val3; float val4; } ; void setup() { // === prueba con variables === long a = 123456789 ; long b = 0 ; // dividir el bloque de datos a en bytes // y almacenar en búfer writeData ( a ) ; // recopilar el bloque de datos b // desde el búfer readData ( b ) ; // aquí b == 123456789 // === prueba con estructuras === // crear estructura myStruct transmit; // asigna un valor al miembro val4 transmit. val4 = 3,1415 ; // estructura "receptora" myStruct recieve; // dividir el bloque de datos de transmisión en bytes // y almacenar en búfer writeData ( transmit ) ; // recopila el bloque de datos de recepción // desde el búfer del búfer readData ( recieve ) ; // recieve.val4 aquí == 3.1415 } template < typename T > void writeData ( T & data ) { const uint8_t * ptr = ( const uint8_t * ) & data; for ( uint16_t i = 0 ; i < sizeof ( T ) ; i ++ ) { buffer [ i ] = * ptr ++; } } plantilla < typename T > void readData ( T & data ) { uint8_t * ptr = ( uint8_t * ) & data; for ( uint16_t i = 0 ; i < sizeof ( T ) ; i ++ ) { * ptr ++ = buffer [ i ] ; } } bucle vacío () {}
Estos ejemplos difieren solo en la forma en que se pasa y procesa el argumento de la dirección:
- En el primer caso, lanzamos el puntero a ( byte * ) al pasar un argumento a una función. También pasamos el tamaño del bloque de datos usando sizeof ().
- En el segundo caso, tenemos void* y no le importa qué tipo de datos se le pasarán, luego transferimos el puntero a uint8_t mediante reinterpret_cast. También pasamos el tamaño del bloque de datos usando sizeof ().
- La tercera opción es a través de una función de plantilla, que no importa en absoluto el tipo y tamaño de los datos: acepta datos por referencia, luego hacemos un puntero al primer byte y justo dentro de la función calculamos el tamaño del bloque de datos a través sizeof (). Esta es la opción más poderosa y versátil.
Nota: los tres ejemplos ocupan la misma cantidad de memoria.
Esta lección es lo más breve y de «referencia» posible, recomiendo leer con más detalle sobre punteros, enlaces (no los analizamos) y sus características en la referencia de C ++ en la web C con Clase.