Глюк с sprintf

Тема в разделе "Arduino & Shields", создана пользователем Dmitriy Abramov, 10 сен 2016.

  1. Всем доброго дня.
    Непонятная ситуация с функцией sprintf -- есть массив строк, пытаюсь менять с её помощью одну строку, и произвольным образом меняются другие.
    Проблема в чём -- идет вызов
    sprintf(initStr[numStr], "%s%d", initStr[numStr], IRData);
    где numStr например, 4, а вдобавок меняются строки 2 и 3.

    Код (C++):
    #include "U8glib.h"
    #include "rus6x10.h"
    #include "IRremote.h"

    #define MAXLOC 5

    U8GLIB_ST7920_128X64 u8g(10, 11, 12, U8G_PIN_NONE);
    byte NumLoc=0;
    byte idLoc[MAXLOC];
    byte secLoc[MAXLOC];
    byte dirLoc[MAXLOC];
    IRrecv irrecv(7);
    decode_results results;
       
    char* initStr[7];

    void scroll(void)
      {
        for (byte i=0;i<6;i++)
          {
            initStr[i]=initStr[i+1];
          }
      }

    void drawInit(byte num)
      {
        u8g.firstPage();
          do
            {
              for (byte i=0; i<num; i++)
                {
                  u8g.drawStr(2,10+10*i,initStr[i]);
                }
            }
          while(u8g.nextPage());
      }

    byte IRRead(void)
      {
        int IRRes;
       
        while(true)
          {
            if (irrecv.decode(&results))
              {
                IRRes=results.value;
                irrecv.resume();
                switch (IRRes)
                  {
                    case 0x2FD807F: return 1;
                    case 0x2FD40BF: return 2;
                    case 0x2FDC03F: return 3;
                    case 0x2FD20DF: return 4;
                    case 0x2FDA05F: return 5;
                    case 0x2FD609F: return 6;
                    case 0x2FDE01F: return 7;
                    case 0x2FD10EF: return 8;
                    case 0x2FD906F: return 9;
                    case 0x2FD00FF: return 0;
                    case 0x2FD50AF: return 96;
                  }
              }
          }
      }

    void setup(void)
      {
        byte IRData;
        byte numStr=2;
        initStr[0]="Ввод первичных данных";
        initStr[1]="Используйте пульт";
        initStr[2]="К-во составов: ";
       
        Serial.begin(9600);
        irrecv.enableIRIn();
        u8g.setFont(rus6x10);
        drawInit(numStr);
        while(IRData != 96)
          {
            IRData=IRRead();
            if (IRData != 96)
              {
                sprintf(initStr[numStr], "%s%d", initStr[numStr], IRData);
                drawInit(numStr);
                NumLoc=NumLoc*10+IRData;
              }
          }
        numStr++;
        if (NumLoc>MAXLOC)
          {
            initStr[numStr++]="К-во составов больше";
            initStr[numStr++]="предельного! Снимите";
            initStr[numStr++]="лишние и переинициа-";
            initStr[numStr++]="лизируйте систему.";
            scroll();
            drawInit(6);
            while(true){}
          }
        for (byte ii=0; ii<NumLoc; ii++)
          {
            sprintf(initStr[numStr], "%s%d%s", "id ",ii+1,"-го: ");
            if (numStr>6)
              {
                scroll();
                numStr--;
              }
            drawInit(numStr);
            IRData=-1;
            while(IRData != 96)
              {
                IRData=IRRead();
                if (IRData != 96)
                  {
                    sprintf(initStr[numStr], "%s%d", initStr[numStr], IRData);
                    drawInit(numStr);
                    idLoc[ii]=idLoc[ii]*10+IRData;
                  }
              }
             numStr++;
           }
      }
     
    void loop(void)
      {
      }
     
  2. Tomasina

    Tomasina Сушитель лампочек Модератор

    case должно заканчиваться break.
    Сейчас у тебя выполняются все строки из switch.
     
  3. AlexU

    AlexU Гуру

    case не обязательно должен заканчиваться break -- может заканчиваться return, а может и не заканчиваться, если это предусмотрено логикой программы.

    Первым аргументом функции sprintf должен быть выделенный участок памяти, а Вы используете случайно попавшийся. Вот это и является причиной странности.
    Например, следующий код:
    Код (C++):
    sprintf(initStr[numStr], "%s%d", initStr[numStr], IRData);
    initStr[numStr] -- это участок памяти определённого размера, который был выделен компилятором. Где был выделен, только одному компилятору известно (хотя если дизассемблировать программу, то можно узнать). Вы же в него (участок памяти) пытаетесь "сфорамтировать" строку явно большего размера, что соответственно приводит к затиранию соседней памяти и, в полне вероятно, строк, которые храняться в этой памяти.
     
  4. Я что-то в этом роду и подозревал, но ведь char* предполагает строку неопределённой длинны, и компилятор должен это учитывать. Или я что-то не так понимаю? Я на C что-либо серъёзное не писал уже лет 20, много забыл. Как мне с этой проблемой справиться?
    Причём, если я не использую sprintf, а просто присваиваю этой переменной строку произвольной длинны, то всё работает нормально.
     
    Последнее редактирование: 11 сен 2016
  5. AlexU

    AlexU Гуру

    char * -- это указатель на переменную типа char, т.е. на символ. В случае со строками -- это указатель на первый символ строки, в случае с массивами -- это указатель на первый элемент массива (хотя строка это и есть массив типа char).
    Компилятор знает только то, что (как уже сказал) 'char *' указывает на символ (в случае с AVR это один байт), а то что этот символ -- начало строки, которая имеет какую-то длину, знает только программист.
    Это не совсем корректное высказывание -- переменной присваивается не строка, а ссылка на первый символ строки, т.е. в случае с кодом:
    Код (C++):
    initStr[0]="Ввод первичных данных";
    компилятор сначала выделит массив нужного размера в памяти и заполнит этот массив значениями "Ввод первичных данных\0" (\0 в конце строки добавит компилятор, чтобы показать конец массива - строки), а потом адрес первого байта присвоит переменной initStr[0]. После этого при любых операциях по доступу к памяти посредством переменной initStr[0] компилятор думает, что "программист знает что делает -- если надо выйти за пределы массива, не проблема выйдем и перезапишем данные, а то, что это приведёт к порче других переменных, мне (компилятору) по-барабану".
    Выделить массив нужной длины и указатель на этот массив передать в качестве первого аргумента функции sprintf. Если заранее длина массива не известна, то лучше воспользоваться функцией snprintf (вобще этой функцией лучше пользоваться всегда, вместо sprintf, т.к. она более безопасна), пример кода:
    Код (C++):

    char *buffer = (char *) malloc(64);    // выделяем участок памяти предположительного размера
    int len = snprintf(buffer, 64, "%s%d", initStr[numStr], IRData);
    if (len > 64) { // выделенной памяти оказалось мало
        free(buffer); // освобождаем память
        buffer = (char *)malloc(len + 1);  // выделяем участок памяти нужного размера
        snprintf(buffer, len + 1, "%s%d", initStr[numStr], IRData);
    }
    .....
    free(buffer); // !!!!освобождаем память!!!!
     
    Вот только с динамическим выделением памяти (malloc/free) нужно быть осторожней и вроде как в AVR'ках это не приветствуется.