Variables de tipo char que se reescriben

Estoy teniendo problemas para desde una función devolver un array de caracteres.
Cada vez que uso la función, todas las variables que han usado la función actualizan su valor a la ultima ejecución.
Entiendo que es un problema de como trata la memoria Arduino pero no consigo resolverlo. Con otros tipos de variables, esto no me sucede..

Un ejemplo de código donde reproduzco el problema:

char* randChar() {
    static char randChar[10];
    const char *letters = "abcdefghijklmnopqrstuvwxyz0123456789";
    for(int i = 0; i<10; i++) {
        randChar[i] = letters[random(0, 36)];
    }
    return randChar;
}

int randInt() {
    return random(0,1000);
}

String randStr() {
    String randString;
    const char *letters = "abcdefghijklmnopqrstuvwxyz0123456789";
    for(int i = 0; i<10; i++) {
        randString = randString + letters[random(0, 36)];
    }
    return randString;
}


void setup() {
    Serial.begin(115200);
    delay(1000);
    Serial.println("[char]");
    char * chrA = randChar();
    Serial.println((String)"  chrA: "+chrA);
    char * chrB = randChar();
    Serial.println((String)"  chrB: "+chrB);
    Serial.println((String)"  chrA: "+chrA+" chrB: "+chrB);

    Serial.println("\n[int]");
    int intA = randInt();
    Serial.println((String)"  intA: "+intA);
    int intB = randInt();
    Serial.println((String)"  intB: "+intB);
    Serial.println((String)"  intA: "+intA+" intB: "+intB);

    Serial.println("\n[String]");
    String strA = randStr();
    Serial.println((String)"  strA: "+strA);
    String strB = randStr();
    Serial.println((String)"  strB: "+strB);
    Serial.println((String)"  strA: "+strA+" strB: "+strB);
}

void loop() {
    // put your main code here, to run repeatedly:

}

La salida:

[char]
  chrA: rswyfz3whc
  chrB: gokp808225
  chrA: gokp808225 chrB: gokp808225

[int]
  intA: 995
  intB: 825
  intA: 995 intB: 825

[String]
  strA: fufbm68246
  strB: eyo2o7rthr
  strA: fufbm68246 strB: eyo2o7rthr

Es en la tercera línea donde se puede ver el error, entiendo que chrA y chrB deberían de tener valores diferentes.

Es confuso, pero.. ¿Estoy devolviendo un puntero al array de caracteres?
Siendo así estaría leyendo siempre el mismo trozo de memoria, tendría sentido

Pero estas usando

char* randChar() {
    static char randChar[10]; //<=== aca esta el problema

lo que lo obliga a guardarlo en memoria.

Además, porque usas este random(0,36) si no tienes 36 caracteres? Eso siempre genera problemas.

randChar[i] = letters[random(0, 36)];

usa en su lugar

randChar[i] = letters[random(0, sizeof(letters)];

No estoy muy familiarizado con los punteros, de hecho hasta ayer no sabia apenas nada....
Lo que he entendido es que estoy devolviendo un puntero a memoria char* exactamente donde se almacena el valor de randChar.

Intenté no hacer static la variable randChar dentro de la función, pero tenia reinicios.
Creo que entiendo porque se reiniciaba: Al terminar la función, se destruye la variable y como devuelvo un puntero cuando voy a leer el dato ya no está en memoria.
Corregidme si me equivoco, me importa más entenderlo bien.

PD. El array de letras fue algo rápido para realizar el ejemplo, evidentemente está mejor como indicas.

Eso es correcto.

Lo del static he visto que no es el responsable.

No puedo afirmarlo porque tampoco lo se con certeza.

cuando defines ***char * chrA = *** defines un puntero.
cuando defines ***char * chrB = *** defines un puntero.
¿donde apuntan ambos punteros? a static char randChar[10];

Si haces:

    Serial.println ((long)&chrA,HEX);
    Serial.println ((long)&chrB,HEX);
    
    Serial.println ((long)chrA,HEX);
    Serial.println ((long)chrB,HEX);
    
    Serial.println ((char)*chrA);
    Serial.println ((char)*chrB);

Verás que las dos primeras líneas han de dar resultados diferentes (ocupan diferentes zonas de memoria, son dos variables diferentes).
La tercera y cuarta línea darán resultados iguales (dónde apuntan los punteros).
Y las dos últimas líneas imprimirán el primer carácter de dónde están apuntando cada variable (debe dar el mismo resultado).
Es decir que cuando haces char * chrA = randChar(); ejecuta la función, guarda el valor en la variable static y chrA APUNTA a esa variable estática.
Cuando haces char * chrB = randChar(); vuelves a ejecutar la función, cambia el valor de la variable estática y devuelves otra vez donde APUNTA a la variable estática.
Es normal que chrA y chrB te den el mismo resultado dado que ambas apuntan a la misma variable estática.
¿comprendes?
Con las otras dos funciones no tienes problemas porque no devuelves un puntero.
Para aclarártelo más imagina esta función:

static int i=0;
static int ip=0;
int randInt() {
    i=random(0,1000);
    return i;
}
int *randIntP() {
    ip=random(0,1000);
    Serial.print("Ahora 'ip' vale ");
    Serial.println(ip);
    return &ip; //devolvemos la dirección de 'ip', para aclararlo.
}
void setup() {
    Serial.begin(115200);
    Serial.println("Inicio:");

    int intA = randInt();
    Serial.println(intA);
    int intB = randInt();
    Serial.println(intB);
    Serial.println("Resultados sin punteros:");
    Serial.println(intA);
    Serial.println(intB);
    
    Serial.println();
    Serial.println("---------------");
   
    int *intPa = randIntP();
    Serial.println(*intPa);
    int *intPb = randIntP();
    Serial.println(*intPb);
    Serial.println("Resultados CON punteros:");
    Serial.println(*intPa);
    Serial.println(*intPb);
    
}
void loop() {
}

Esto da este resultado:

Inicio:
807
249
Resultados sin punteros:
807
249

---------------
Ahora 'ip' vale 73
73
Ahora 'ip' vale 658
658
Resultados CON punteros:
658
658

como ves las dos últimas salidas son iguales dado que intPa e intPb apuntan a ip

Esto es más complicado de explicar pero lo intento:
Cuando vemos el mapa de memoria de cualquier microprocesador, tiene zonas para las variables públicas, código máquina, etc.
Pero hay una parte que se usa para crear variables y destruirlas así como para que el microprocesador pueda saber dónde ha de volver después de una llamada a una función.
Esta zona se llama "pila de llamadas" o "stack" en inglés. Y es controlada mediante un puntero (stack pointer en inglés). Esto funciona así en TODOS los microprocesadores existentes sean del tipo que sean.

Imagina un programa simple así:

void __attribute__ ((noinline))  a(){
  Serial.println("Soy a");
}
void setup(){ 
}
void loop() {
  Serial.begin(115200);
  a();
  Serial.print("fin");
  while(1);
}

Creo que es bastante fácil el código, ¿no? (obvia la parte "rara" al definir la función a() que es una directiva de compilación para que nos funcione bien lo que viene más abajo)

Cuando desde loop() se invoca a() el microprocesador guarda en la pila de llamadas (stack) la dirección de memoria donde se invocó.

El microprocesador "salta" a la zona de memoria donde está definida la función a() y empieza a ejecutarla, hace el println pero cuando llega al cierre de llave de la función a(), rescata de la pila de llamadas (stack) dónde tiene que volver y vuelve a saltar a la línea de loop de regreso para seguir por donde iba.
Ahora modificamos un poco el código:

void __attribute__ ((noinline))  a(){
  Serial.println("Soy a ");
  Serial.print("Puntero a pila de llamadas vale:");
  Serial.println(SP);
}
void setup(){ 
}
void loop() {
  Serial.begin(115200);
  Serial.print("Puntero a pila de llamadas vale:");
  Serial.println(SP);
  a();
  Serial.print("Puntero a pila de llamadas vale:");
  Serial.println(SP);
  Serial.print("fin");
  while(1);
}

Y esta es la salida que nos da:


Puntero a pila de llamadas vale:2301
Soy a 
Puntero a pila de llamadas vale:2299
Puntero a pila de llamadas vale:2301
fin

Como ves dentro de la función a() el puntero a la pila (Stack Pointer) es diferente.
Por eso es mala praxis el anidar muchas funciones y crear variables grandes dentro de funciones y procedimientos porque puedes desbordar la pila de llamadas (error típico "stack overflow" en inglés)

Y error grave es crear variables dentro de la pila e intentar después acceder a ella dado que si o si tarde o temprano se destruirán. prueba esto y verás que el resultado no te da 1234:

int *a(){
  int x=1234;
  return &x;
}
void setup(){ 
}
void loop() {
  Serial.begin(115200);
  int valor=a();
  Serial.println(valor);
  while(1);
}

Personalmente intento huir de la definición de grandes estructuras o grandes variables dentro de funciones porque finalmente ocupan la misma memoria, si son cadenas obligan al procesador a obtener nueva zona de memoria cada vez que se invoca, mayor lentitud, es más difícil calcular cuánta memoria real libre nos queda. etc. pero esto es algo personal y seguro que tiene detractores.

Perdona el tocho pero estos temas de programación me gustan mucho.
Saludos

Pues lo has explicado perfectamente!

Me costó entender el concepto de punteros, porque normalmente programo en lenguajes de alto nivel, pero es que tiene todo el sentido!

Me encanta poder entender como funciona algo y mirar debajo del capó. Nunca me había parado a pensar en como un micro realiza la gestión de memoria, como crea y destruye los espacios de memoria.

De ahí que se diga que usar variables de tipo String en micros con poca memoria dejan a esta como un queso gruyere.

Lo dicho, muchas gracias por las explicaciones, han sido muy ilustrativas

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.