помогите с динамическим созданием массива

vde69
Offline
Зарегистрирован: 10.01.2016

Cуть хотелки - в объекте создать массив из элементов "структура" кратно одному параметру конструктора.

Вроде оно то, но что-то меня смущает, да и компилятор ругается на  последнюю точку (строка 18)

 

 

typedef struct {
  long ValueTec;           // текущее выведеное значение на экран
  char Mask;               // маска символов
  uint8_t FirstCol;        // номер колонки первого символа
  uint8_t EndCol;          // номер колонки последнего символа
  boolean Active;          // номер колонки последнего символа
} 
OutParamText;

class Lcd_I2C_4x20: public LiquidCrystal_I2C {
private:
  OutParamText *param; 
public:

  Lcd_I2C_4x20(uint8_t CountParam) : LiquidCrystal_I2C(0x27, 20, 4){
    param = (OutParamText*) malloc(CountParam*sizeof(OutParamText));
    for (uint8_t i = 0; i < CountParam; i++){ 
      (OutParamText)(&param + CountParam*sizeof(OutParamText)).Active = false;
    }
  }

vde69
Offline
Зарегистрирован: 10.01.2016

еще боюсь со звездочками чего напутал :) а это будет сложно определить потом...

kisoft
kisoft аватар
Offline
Зарегистрирован: 13.11.2012
(OutParamText)(&param + CountParam*sizeof(OutParamText)).Active = false;

замени на:

param[i].Active = false;

Кроме того, у тебя там ошибка и не одна, навскидку около 3-х ошибок, хотя смотря как считать.

По сути Object *array_ptr и Object array[] - это близкие вещи, хоть это и не совсем одно и тоже, но это не важно.

И еще, почитай уже о массивах, там всё достаточно просто и понятно, когда почитаешь и попробуешь на кошках, а не на мульти проектах. Массив структур - это вообще песня, удобно и просто, когда нужно.

Совет, если не хочешь, не читай. Не пытайся пользоваться приведением типов, забудь, что оно есть, сначала пойми, для чего нужно приведение типов, а потом сам поймешь, что в 95% случаев оно нафиг не нужно. А то ты почти в каждом сообщении про приведение типов говоришь, а зачем.

 

vde69
Offline
Зарегистрирован: 10.01.2016

си для меня тяжеловат, я начинал на фортране, потом паскаль потом AutoLisp, там типизация первейшие роли играет... в си конечно все более свободно в плане работы с указателями и памятью...

я все понимаю зачем и чего делается, и для чего можно использовать, но вот конкретика си для меня "не удобна"...  например в паскале есть record (с данными переменных типов) а вот как его сделать в си я не понимаю и так далее...

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

Ну, тяжело - не тяжело, а матчасть учить надо, а то уж больно заумно у Вас всё. Проще будьте.

typedef struct {
  long ValueTec;           // текущее выведеное значение на экран
  char Mask;               // маска символов
  uint8_t FirstCol;        // номер колонки первого символа
  uint8_t EndCol;          // номер колонки последнего символа
  boolean Active;          // номер колонки последнего символа
} 
OutParamText;

class Lcd_I2C_4x20 : public LiquidCrystal_I2C {
private:
  OutParamText *param; 
public:

  Lcd_I2C_4x20(uint8_t CountParam) : LiquidCrystal_I2C(0x27, 20, 4) {
    param = new OutParamText[CountParam]; // делов то!
    for (uint8_t i = 0; i < CountParam; i++){ 
      param[i].Active = false;
    }
  }

  // И не забыть!!!
  ~Lcd_I2C_4x20(void) {
  		delete [] param;
  }
};

 

kisoft
kisoft аватар
Offline
Зарегистрирован: 13.11.2012

Есть же микропаскаль от Mikroe или как его там. Или Си не нравится, а для Паскаля нет библиотек? Юзай Паскаль и не парься.

vde69
Offline
Зарегистрирован: 10.01.2016

не знаю как в СИ но в паскале было запрещено совместное использование new и malloc, по этому я изначально склонялся к последнему как более универсальному, про освобождение памяти - спасибо (забыл сделать), для меня это не нужно (ибо объект глобальный), но разумеется сделать надо в деструкторе.

-----------------------------------------------------------------------------------

all гуру:

еще вопрос: как лучше решить дилему неопределеного типа у указателя?

собственно чего мне надо в итоге:

есть класс, я в него передаю адрес статической переменной (предположим это глобальная переменная), где-то в классе этот адресс хранится и тут-же хранится тип данных.

далее в объекте есть процедура poll() которая дергается из основного цикла, внутри этой процедуры я получаю по адресу текущее значение и сравниваю его со значением ValueTec который хранится в объекте, если есть разница - вызываю print для LiquidCrystal.

То есть я делаю условно говоря настраиваемый тригер на несколько переменных, собственно проблемма в том, что переменные за которыми мне нужно следить могут иметь различные типы (и следовательно длинну и самое главное знаковость) и следовательно для вызова print мне нужно получить правильно типизированое значение.

я понимаю как можно это сделать наклепав кучу процедур с разным типом параметра (как сделан класс print), но что-то меня это коробит...

может есть где-то готовые реализации подобного или идеи как это реализовать например при ограничении "все переменные только числовые" или "все переменные только целые"?

 

ps

а вообще сейчас у меня веселый зоопарк из языков, на работе 1с в двух разных версиях, терминал написал и докручиваю на турбодельфи, здесь си, и еще жена с макросами к екселю задолбала :))) давно такого не было, обычно пара и все, а тут 5шт  паралельно...

brokly
brokly аватар
Offline
Зарегистрирован: 08.02.2014

Вас коробит, потому что к паскалю привыкли:) Если не ошибаюсь в дельфях вообще есть функция определяющая тип переменной :)

Только вот я не понял, что вызывает проблему ? Ну есть буфер переменной, ну есть признак типа, известны размеры данных того или иного типа. Делай что хочешь :) Одна беда, в си размер типа byte ясен, а все остальное зависит от разрядности системы, это может стать проблеммой.... Может не стоит извращаться ?

vde69
Offline
Зарегистрирован: 10.01.2016

мне не нравится то, что признак типа нужно кодировать числом и передавать в процедуру вместе с адресом...

 

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

vde69 пишет:

не знаю как в СИ но в паскале было запрещено совместное использование new и malloc, по этому я изначально склонялся к последнему как более универсальному, 

Между ними есть серьёзная разница. Для классов (а структура - это класс) лучше привыкать использовать именно new, т.к. malloc только выделяет память, а new ещё и вызывает конструктор. В данной структуре у Вас нет конструктора, но это ничего не значит - просто привыкните использовать new для классов - это должно быть на автомате. Не раз ещё выручит.

Если полуграмотные прогеры начнут орать, что malloc эффективнее new, посылайте их .... изучать файл <корень IDE>\hardware\arduino\avr\cores\arduino\new.cpp в котором эти самые new определены вот так:

#include <stdlib.h>

void *operator new(size_t size) {
  return malloc(size);
}

void *operator new[](size_t size) {
  return malloc(size);
}

void operator delete(void * ptr) {
  free(ptr);
}

void operator delete[](void * ptr) {
  free(ptr);
}

По другим вопросам сейчас посмотрю, могу ли чем-нибудь помочь.

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

vde69, старался, но не понял задачи. Давайте я буду объяснять, а Вы поправляйте,

У Вас в классе есть, скажем, три переменные разных типов, скажем, int, float и char *. Все они отображатся на экране. Каждая в своём месте. Теперь Вы хотите иметь одну функцию, которая будет принимать новое значение, каким-то образом понимать к какой именно переменной оно относится, сравнивать с текущим значением и. если не равно, то отображать новое значение на экране и, наконец, запоминать новое значение, как текущее.

Вот то, что набрано наклонным шрифтом мне непонятно. 

А вообще, эту задачу я бы делал без функции, я бы определил оператор присваивания для своих переменных. Тогда в программе я бы просто присваивал им значения и не парился бы, а сам оператор присваивания делал бы всё, что нужно, включая и отображение нового значения на дисплей.

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

Ладно, мне надо исчезать. Напишите, что думаете, завтра продолжим.

Logik
Offline
Зарегистрирован: 05.08.2014

vde69 пишет:

еще вопрос: как лучше решить дилему неопределеного типа у указателя?

Никак. Тип указателя определен только на этапе компиляции. На этапе исполнения это просто адрес, по крайней мере для простых типов, и без всяких извращений типа смартпоинтеров и т.д.

vde69 пишет:

я понимаю как можно это сделать наклепав кучу процедур с разным типом параметра (как сделан класс print), но что-то меня это коробит...

Правильно понимаете. Можете поиграть темплэйтами, но все сведется всеравнок куче функций, только за вас их наштампует компилятор. 

Logik
Offline
Зарегистрирован: 05.08.2014

brokly пишет:

Вас коробит, потому что к паскалю привыкли:) Если не ошибаюсь в дельфях вообще есть функция определяющая тип переменной :)

Чето такого не припомню. Для обектов можна реализовать если передается экземпляр, а параметром функции описан его предок, внутри которого есть свойство определяющее тип потомка. Тогда по этому свойству и разделяют в функции кто из потомков пришел. В общем, в С++ тоже можно, и активно юзается.

brokly пишет:

Только вот я не понял, что вызывает проблему ? Ну есть буфер переменной, ну есть признак типа, известны размеры данных того или иного типа. Делай что хочешь :) Одна беда, в си размер типа byte ясен, а все остальное зависит от разрядности системы, это может стать проблеммой.... Может не стоит извращаться ?

Влом тип передават - вот и проблема )) А по размеру данных низя вобще никак!!!

vde69
Offline
Зарегистрирован: 10.01.2016

я покажу, все предельно локанично:

//*************************************************
// главный цикл
//*************************************************
void loop() {

  time_loop_new = millis();  
    
  #ifdef BUS_RS485 
    Bus.poll();         
  #endif 
  
  #ifdef DATE_TIME_USE || DEVICE_RTC_I2C_DS1307
    DT.poll(time_loop_new);         
  #endif 
  
  #ifdef TEMPERATURE_DS18B20_ONE_WIRE 
    ds18b20.poll(time_loop_new); 
  #endif 

  #ifdef MODULE_MASTER  
    Master_poll();
  #endif 

  #ifdef DEVICE_LCD_4X20_I2C 
    LCD_4X20_I2C.poll(time_loop_new);
  #endif 

}

DT, ds18b20 - это классы которые получают и хранят данные внутри себя

LCD_4X20_I2C - это класс который должен выводить эти данные на экран, для этого в процедуре инициализации я в класс должен передать адреса этих переменных, и я их сохраняю в массиве (массив из структур с адресом, типом, форматом и т.д.), далее по вызову poll я прохожусь по массиву получаю данные и в случае необходимости вывожу на экран только те значения которые поменялись.

 

Logik
Offline
Зарегистрирован: 05.08.2014

давайте весь код. 

vde69
Offline
Зарегистрирован: 10.01.2016

Logik пишет:

Влом тип передават - вот и проблема )) А по размеру данных низя вобще никак!!!

целое в 2 байта может быть и int и безнаковое int, по этому одной длинны мало.

делать в классе кейс с 10 вариантами, и описание к классу - класс будет очень некрасивым и недружественным... что приемлемо в программе не приемлемо в классе...

vde69
Offline
Зарегистрирован: 10.01.2016

Logik пишет:

давайте весь код. 

так нету всего кода еще,

 


#ifndef LCD_4X20_I2C_H        
#define LCD_4X20_I2C_H

#include <LiquidCrystal_I2C.h>


typedef struct {
  long ValueTec;           // текущее выведеное значение на экран
  uint8_t *Value;          // адрес статической переменной которая контролируется
  char Mask;               // маска символов для незначащих нулей (как ведущих ток и после запятой)
  uint8_t FirstCol;        // номер колонки первого символа
  uint8_t EndCol;          // номер колонки последнего символа
  boolean Actove;          // признак активности
} OutParamText;


class Lcd_I2C_4x20: public LiquidCrystal_I2C {
private:
  unsigned long time_anime = 0;

  OutParamText *param; // массив параметров для вывода на экран

  void OutChar(char c, int col, int line)  {
    LiquidCrystal_I2C::setCursor(col, line);
    LiquidCrystal_I2C::print(c);
  }

public:

  Lcd_I2C_4x20(uint8_t CountParam) : LiquidCrystal_I2C(0x27, 20, 4){
    param = (OutParamText*) malloc(CountParam*sizeof(OutParamText));
    for (uint8_t i = 0; i < CountParam; i++){ param[i].Actove = false; }
  }


  void init(){
    LiquidCrystal_I2C::init();
    LiquidCrystal_I2C::backlight();
    LiquidCrystal_I2C::noCursor();
  }

  void SetParam(uint8_t num, char Mask, uint8_t FirstCol, uint8_t EndCol, boolean Actove) //тут нехватет параметров
  // ***************************************************************************
  { 
    param[num].ValueTec = 0; 
    param[num].Value = ???;  /// вот тут будет адрес статической переменной, в которой текущая температура и т.д.
    param[num].Mask = Mask; 
    param[num].FirstCol = FirstCol; 
    param[num].EndCol = EndCol; 
    param[num].Actove = Actove; 
  }
  // ***************************************************************************


  void OutPageDecor(char Str0[21], char Str1[21], char Str2[21], char Str3[21], char Mask)
  // ***************************************************************************
  { // процедура сделана "медленной", по сколько она вызывается редко
    // Str0, Str1, Str2, Str3 - строки маски экрана
    // символ маски для динамических полей (полей с изменяющимися полями)
    for (uint8_t i = 0; i < 20; i++){ if (Str0[i] != Mask) { OutChar(Str0[i], i, 0); } else {OutChar(' ', i, 0);}}
    for (uint8_t i = 0; i < 20; i++){ if (Str1[i] != Mask) { OutChar(Str1[i], i, 1); } else {OutChar(' ', i, 1);}}
    for (uint8_t i = 0; i < 20; i++){ if (Str2[i] != Mask) { OutChar(Str2[i], i, 2); } else {OutChar(' ', i, 2);}}
    for (uint8_t i = 0; i < 20; i++){ if (Str3[i] != Mask) { OutChar(Str3[i], i, 3); } else {OutChar(' ', i, 3);}}
  }
  // ***************************************************************************

  void poll(unsigned long time_loop)
  // ***************************************************************************
  {
// еще не написано...
    
  }
  // ***************************************************************************

};

 

Logik
Offline
Зарегистрирован: 05.08.2014

Блин написал много и глюконуло. Теперь коротко.

Варианты:

1. Сделать общего предка для DT, ds18b20 с нужным полем данных и работать через него. 

2. Инитить DT, ds18b20 указателями на элементы массива куда сохранять данные.

3. Передавать в DT, ds18b20 калбеки (или обекты) которые приймут данные и положат и сделают что надо.

4. Перестать мудрить и после получения данных вызыват соответствующий метод LCD_4X20_I2C а уж он разберется, сохранять или выводить.

Logik
Offline
Зарегистрирован: 05.08.2014

я так понимаю, хотите SetParam вызвать раз и передать указатель на данные из DT, ds18b20 ?

Если я правильго понял, то получим следующее. 

Данные хранятся в DT, ds18b20  в представлении специфичном для каждого класса, и таких классов в принципе может быть много. И у каждого свой тип данных. И что, под каждый из них делать преобразования в Lcd_I2C_4x20 учитывая что он эти типы  вообще не знанет? Нелогично.   Логичней в  DT, ds18b20 и подобных приводить данные к одному типу, например строке, учитывая что будет вывод на экран. И соответственно проблема снимется т.к. в SetParam будут передаватся указатели на эту строку. Можна два представления - строку и число, если надо математику делать в Lcd_I2C_4x20. В таком случае уместны те варианты что я выше писал. Типа родитель для DT, ds18b20 у которого есть GetInt и GetStr..

Но вобще если есть разные форматы одних данных у разных классов, то конвертировать эти фориаты надо внутри этих классов. Инкапсуляция специфического типа данных шоб была.

Кстати, а как в такой архитектуре будете решать проблему ошибки получения данных, например ds18b20 отключили или ошибка crc?

 

vde69
Offline
Зарегистрирован: 10.01.2016

Logik пишет:

я так понимаю, хотите SetParam вызвать раз и передать указатель на данные из DT, ds18b20 ?

Если я правильго понял, то получим следующее. 

Данные хранятся в DT, ds18b20  в представлении специфичном для каждого класса, и таких классов в принципе может быть много. И у каждого свой тип данных. И что, под каждый из них делать преобразования в Lcd_I2C_4x20 учитывая что он эти типы  вообще не знанет? Нелогично.   Логичней в  DT, ds18b20 и подобных приводить данные к одному типу, например строке, учитывая что будет вывод на экран. И соответственно проблема снимется т.к. в SetParam будут передаватся указатели на эту строку. Можна два представления - строку и число, если надо математику делать в Lcd_I2C_4x20. В таком случае уместны те варианты что я выше писал. Типа родитель для DT, ds18b20 у которого есть GetInt и GetStr..

Но вобще если есть разные форматы одних данных у разных классов, то конвертировать эти фориаты надо внутри этих классов. Инкапсуляция специфического типа данных шоб была.

Кстати, а как в такой архитектуре будете решать проблему ошибки получения данных, например ds18b20 отключили или ошибка crc?

 

Да Вы правильно поняли то что я хочу сделать,

про преобразование данных в родительских класса - не пойдет, ибо они использоваться будут во первых не только для экрана, а во вторых форматов вывода (экранов) будет несколько и разных (и по размерам и по моделям), по этому преобразование к формату вывода логичнее делать в классе который отвечает за вывод...

инкаспуляцию между классов разных предков мне совсем не хочется делать по причинам отрытости и многовариантности сборок (директив много, и поставщиков данных будет много, планирую около 50)

по поводу ошибок - каждый модуль затачиваю под автономность, вот пример, тут реализован авотномный режим


// ------------------------------------------------------------
#ifdef DATE_TIME_USE   

#include <avr/pgmspace.h>
const uint8_t daysInMonth [12] PROGMEM = { 31,28,31,30,31,30,31,31,30,31,30,31 };    
    
class addon_DateTime {
private:

  long time_to_long(uint16_t Y, uint8_t M, uint8_t D, uint8_t h, uint8_t m, uint8_t s) // количество секунд после 2000/01/01, действительно для периода 2001..2099
  // ***************************************************************************
  {
    if (Y >= 2000)
        Y -= 2000;
    uint16_t days = D;
    for (uint8_t i = 1; i < M; ++i)
        days += pgm_read_byte(daysInMonth + i - 1);
    if (M > 2 && Y % 4 == 0) ++days;
    days = days + 365 * Y + (Y + 3) / 4 - 1;
    return ((days * 24L + h) * 60 + m) * 60 + s;
  }
  // ***************************************************************************

  void SetDateTimeMillis ()
  // ***************************************************************************
  {
    long lt2 = offset + (millis() / 1000);
    if (lt > lt2) {              // видимо был переход через 0 счетчика millis()
      offset = offset + 4294967; // это примерно 47 дней, что равно времени переполнения счетчика millis() 
      lt = offset + (millis() / 1000);
    }
    else {
     lt = lt2;
    }

    lt2 = lt;
    ss = lt2 % 60;
    lt2 /= 60;
    mm = lt2 % 60;
    lt2 /= 60;
    hh = lt2 % 24;
 
    uint16_t days = lt2 / 24;
    uint8_t leap;
 
    for (yOff = 0; ; ++yOff) {
      leap = yOff % 4 == 0;
      if (days < 365 + leap)
         break;
      days -= 365 + leap;
    }
    for (m = 1; ; ++m) {
      uint8_t daysPerMonth = pgm_read_byte(daysInMonth + m - 1);
      if (leap && m == 2)
         ++daysPerMonth;
      if (days < daysPerMonth)
         break;
      days -= daysPerMonth;
    }
    d = days + 1; 
   }
  // ***************************************************************************


public:
  
  uint16_t year()        { return 2000 + yOff; }
  uint8_t month()        { return m; }
  uint8_t day()          { return d; }
  uint8_t hour()         { return hh; }
  uint8_t minute()       { return mm; }
  uint8_t second()       { return ss; }


  
  virtual void SetDateTime (uint16_t year, uint8_t month, uint8_t day, uint8_t hour, uint8_t min, uint8_t sec)
  // ***************************************************************************
   {
     if (year >= 2000)
         year -= 2000;
     yOff = year;
     m = month;
     d = day;
     hh = hour;
     mm = min;
     ss = sec;
     lt = time_to_long(yOff, m, d, hh, mm, ss); 
     offset = lt - (millis() / 1000);
   }
  // ***************************************************************************

  virtual void poll(unsigned long time_loop)
  // ***************************************************************************
   {
     long a = time_loop / 1000;
     if ((lt - offset) != a) { SetDateTimeMillis(); }  
   }
  // ***************************************************************************

protected:
  uint8_t yOff, m, d, hh, mm, ss; // хранит дату/время
  long lt;                        // хранит дату/время в секундах после 2000/01/01
  long offset;                    // хранит количество секунд после 2000/01/01 на время последнего обнуления счетчика millis()


};

#endif 

 

DIYMan
DIYMan аватар
Offline
Зарегистрирован: 23.11.2015

vde69, почитайте вкратце про архитектуру MVC - оно тут немного к месту будет, поясню почему: по сути ваш экран - это и есть View, в которое надо выводить разнородные данные. Модель этих данных может быть различная, контроллер данных - с натяжкой примем, что это ваши классы для получения информации с датчиков.

Так вот, задача отображения данных - это именно задача View. И логичнее некуда делать так, чтобы вся логика работы с выводом переменных разнородного типа во View была выделена отдельно, пусть это будет класс. А вот в этом классе уже для каждого типа данных пишете или операторы, или перегруженные функции. Это нормальный подход, и пусть вас не пугает, что класс будет выглядеть "недружественным" и т.п. Вот кусок кода из моего проекта, например (несущественное вырезал):

class OneState
{
    ModuleStates Type; // тип состояния     
    void* Data; // данные с датчика
    void* PreviousData; // предыдущие данные с датчика

    public:

    bool IsChanged(); // тестирует, есть ли изменения
    OneState& operator=(const OneState& rhs); // копирует состояние из одной структуры в другую
    friend OneState operator-(const OneState& left, const OneState& right); // оператор получения дельты состояний
    operator String(); // для удобства вывода информации
    operator TemperaturePair(); // получает температуру в виде пары предыдущее/текущее изменение
    operator HumidityPair(); // получает влажность в виде пары предыдущее/текущее изменение
    operator LuminosityPair(); // получает состояние освещенности в виде пары предыдущее/текущее изменение
    operator WaterFlowPair(); // получает значения расхода воды в виде пары предыдущее/текущее изменение
    
    OneState(ModuleStates s, uint8_t idx)
    {
      Init(s,idx);
    }
    ~OneState();

    private:
    OneState();
    OneState(const OneState& rhs);
    void Init(ModuleStates type, uint8_t idx); // инициализирует состояние
    
};

Вот примерно ваша задача - хранить разнородные типы данных с датчиков. И ничего страшного в классе нет - так, мутота одна. Зато работает как надо. И если надо ввести новый тип данных - это тоже несложно. Всеми преобразованиями данных класс занимается внутри себя.

Logik
Offline
Зарегистрирован: 05.08.2014

vde69 пишет:

про преобразование данных в родительских класса - не пойдет, ибо они использоваться будут во первых не только для экрана, а во вторых форматов вывода (экранов) будет несколько и разных (и по размерам и по моделям), по этому преобразование к формату вывода логичнее делать в классе который отвечает за вывод...

Ниразу не верно! Смотрите, допустим есть 5 классов ввода данных (подобных  DT, ds18b20) и 5 классов "экранов". У каждого абсолютно свой формат. Вам прийдется озаботится преобразованиями формата 5*5=25 вариантов. А через единый промежуточный формат (возможно родной для одного или нескольких) 5+5=10 вариантов (минус те у кого он родной). 

Мало того наличие 5 сущностей решающих сходную задачу, это четкое показание к ООП (что вобщем уже у вас есть) и выстраиванию их в иерархию классов, тут даже думать не о чем. И 5 экранов и 5 классов ввода данных - все сразу в иерархию с родителями и т.д. Соответственно у родителеу геттер обявлен, у потомков переопределен под конкретную реализацию. Аналогично у экранов. Абсолютно классический случай.

Logik
Offline
Зарегистрирован: 05.08.2014

DIYMan пишет:

Вот примерно ваша задача - хранить разнородные типы данных с датчиков. И ничего страшного в классе нет - так, мутота одна. Зато работает как надо. И если надо ввести новый тип данных - это тоже несложно. Всеми преобразованиями данных класс занимается внутри себя.

Это мало чего даст. Фактически тот же идентификатор  типа(он же Тип указателя, он же признак типа, как в постах выше называли)  всеравно надо явно указывать.  Просто если ранее данные и методы работы с ними лежали в классе ввода, то теперь они и методы работы с ними в отдельном классе данных, но существует соответствие один класс ввода-один  класс данных. Пока это соответствие не нарушается разницы не будет, разве что не желая сводить классы ввода в одну иерархию можна "спрятатся" за этим классом данных.     

DIYMan
DIYMan аватар
Offline
Зарегистрирован: 23.11.2015

Logik пишет:

Это мало чего даст. Фактически тот же идентификатор  типа(он же Тип указателя, он же признак типа, как в постах выше называли)  всеравно надо явно указывать.  Просто если ранее данные и методы работы с ними лежали в классе ввода, то теперь они и методы работы с ними в отдельном классе данных, но существует соответствие один класс ввода-один  класс данных. Пока это соответствие не нарушается разницы не будет, разве что не желая сводить классы ввода в одну иерархию можна "спрятатся" за этим классом данных.     

Ага, согласен. Именно по этой причине и упомянул про MVC - строго говоря, лобастые дядьки не зря думали в своё время - мухи отдельно, котлеты отдельно. Но всё равно на каком-то этапе мы спускаемся к модели данных (считаю её главной составляющей MVC, к слову). А вот как быть с этой моделью - вопрос серьёзный и нужный. В языках со строгой типизацией всё равно никак не обойтись без идентификатора типа (C# тут, пожалуй, исключение - хоть и строго типизирован, но там есть Reflection, по сути, тот же RTTI, что в Дельфях и C++ деБилдере - через него можно хоть чёрта лысого на свет вытащить) - иначе как понять, с какими данными мы работаем в текущий момент и каким образом интерпретировать содержимое ячеек памяти, в которых содержатся переданные сырые данные?

В общем, это всё теория, лично я предпочитаю особо не париться в таких вот не сильно сложных реализациях - всё равно набор типов данных достаточно ограничен, чтобы забивать себе голову на тему "класс будет некрасиво выглядеть и вообще - недружественно" :) Задача - ехать, а не шашечки.

Хотя да, признаюсь - самого коробит вынужденная избыточность кода, когда речь заходит о подобных "универсальных" вещах.

DIYMan
DIYMan аватар
Offline
Зарегистрирован: 23.11.2015

Logik пишет:

разве что не желая сводить классы ввода в одну иерархию можна "спрятатся" за этим классом данных.     

И да, тут ещё какой ключевой момент серьёзный в сложной иерархии классов, применительно к МК: гадские vtable, которые отжирают оперативу (которой и так не густо). Именно поэтому я все преобразования делаю в одном классе. Два байта тут, байт там - насобирали лукошко :)

Logik
Offline
Зарегистрирован: 05.08.2014

Уга. Какраз хотел добавить в свой пост, что все дико громоздко, учитывая что там пару байт данных температуры ))

Вот и я считаю, что для микроконтроллеров универсальность  - дело десятое, "ехать важней". Но тут такие хотелки... 50 разных классов... ну фиг его знает...

vde69
Offline
Зарегистрирован: 10.01.2016

почитав и поразмыслил я решил

1. оставить хранение данных в классах где они получаются, эти классы вообще не менять... (обзовем эти классы "source")

2. классы которые используют эти данные (обзовем их "use") не должны зависить от реализаций классов "source"

3. между этих классов должны стоять функции обеспечивающие конвертацию типов данных, эти функции будут лежать в отдельном внеклассовом модуле и в них будет куча директив  условной компиляции (для вырезания только необходимого кода)

4. настройка класса "use" будет сводится к записи адреса функции конвертера и требуемого формата вывода

 

недостатком будет - большое количество функций "конвертеров", и необходимость их добавлять при расширении системы

 

Logik
Offline
Зарегистрирован: 05.08.2014

А зачем вам эта условная компиляция в диких кол-вах? Компилятор отлично оптимизирует и вырезает неиспользуемый код.

vde69
Offline
Зарегистрирован: 10.01.2016

ну во первых я ретроград, и не сильно доверяю компилятору, а учитывая использования адресов процедур я не сильно уверен в автоматическом результате.

во вторых в этих функциях будут использоватся имена классов и объектов которые могут и не присутствовать, и что-бы банально компилятор не ругался нужно вырезать лишнее

в третьих с вырезками компиляция работает быстрее (по тому  как оптимизация происходит в самом конце, а вырезка в самом начале), на больших проектах это ощутимо заметно

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

Мне не нравится Ваш п №3.

Парни Вам здесь правильно написали, этим должен заниматься специальный класс (интерфеймс) от которого наследуются все классы source.

Это примерно так, как сделан интерфейм Printable в IDE. Все, кто хочет уметь печататься нследуются от Printable, и класс Print их всех отлично понимает.

vde69
Offline
Зарегистрирован: 10.01.2016

про создании базового класса:

такой класс будет узконаправленным на конкретику проекта и тогда все его потомки то-же будут узконаправлеными, кроме того метод "правка под проект базового типа" куда хуже чем изменение одного конкретного модуля, хотя-бы по тому, что может повлиять на потомков совершенно не очевидным образом...

мой подход такой основан на создании одного или нескольких стандартизированых интерфейсов между объектами источниками данных и получателями (и без разницы это дисплей или запись в файл или входящие данные для расчетов).

подобный подход широко распространен, например ADO работает по такому прицепу... да и вообще все провайдеры субд...  А по сколько я с СУБД в хороших отношениях, то и выбрал этот подход а не инкаспуляцию (которая то-же имеет право на жизнь для решения моей задачи)

 

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

Дело Ваше, но это не базовый класс. а интерфейс. Который ничего не делает (как тот же Printable). В нём вообще нет никакиз реальных методов - это интерфейс с единственным абстрактным методом. Вот, смотрите (из текстов IDE):

class Printable
{
  public:
    virtual size_t printTo(Print& p) const = 0;
};

И всё, там больше нихрена нет. И от него наследуются все, кто хочет печататься. Те, кто от него наследуются обязыаны сами у себя реализовать метод printTo. А функции печати принимают указатель на Printable и тем самым любой класс, унаследованный от этого интерфейса умеет печататься единообразно.

Т.е. Вы вправе делать как хотите, но профессионально это делается вот так.

DIYMan
DIYMan аватар
Offline
Зарегистрирован: 23.11.2015

Блин, ну задача же из ООП в чистом виде. Евгений вам всё правильно написал, куда вас понесло? Стандартизированный подход при работе с разнородными данными - прекрасно реализуется при помощи ООП. На примере класса вывода на дисплей: он наследуется от Print, переопределяет метод print, и внутри этого метода посылает команды по шине для записи на дисплей). Класс HardwareSerial - посылает данные по USART, а не на дисплей - и всё при помощи одного переопределённого метода. 

То есть вот вам и интерфейс между входящими данными и их выводом куда-либо: базовый класс с набором абстрактных методов, всё.

ADO, которое вы упомянули, кстати, построена как раз по такому принципу - иерархия наследования. Если вы посмотрите набор классов, то докопаетесь до интерфейса, который должны реализовывать все провайдеры ;)

Вообще не понимаю - в чём у вас проблема? В некрасивости кода? В избыточности кода? В непонимании, как сделать иерархию классов? В основах ООП? 

Logik
Offline
Зарегистрирован: 05.08.2014

ADO работает между СУБД и клиентом. Разработчики АДО не имеют возможности доработать ни клиента ни СУБД. Если бы могли - ADO не было бы, его интерфейсы реализовывались бы с СУБД :)  В МК вы пишете все и нет нужды копировать архитектуру ПО ПК. Это даже вредно, монструозно выходит. 

vde69
Offline
Зарегистрирован: 10.01.2016

блин, да не понимаете Вы меня...

да я согласен, что принт должен наследоватся (и это у меня именно так и есть), но я разделяю весь процесс на

1. получение и хранение данных

2. вывод данных (тут действительно наследники принта)

3. использование данных не для вывода (а например для расчетов, или для непосредственного управления)

по этому у меня есть 2 разных задачи вывод и расчет, но эти две разные задачи будут опиратся на общий интерфейс работы с данными

конечно можно делать включение света через потомок принта,  и даже расчет погоды по туче параметров то-же можно сделать через потомок принта, но я считаю, что везде нужно знать меру, это как нормализация и денормализация в субд, есть правила говорящие что нужно соблюдать основные формы нормализации, но в жизне это не так, есть куча методичек которые говорят о необходимости денормализации таблиц субд, как всегда истина где-то посередине...

мой подход гарантирует понятность промежуточного интерфейса (на выходе функций конверторов) улучшение читабельности, отладки и уменьшение ошибок, конечно то-же самое можно сделать на классе и будет то-же самое, но я буду делать на функциях исходя из необходимости использовать директивы компилятора, которые на классе то-же будут работать, но учитывая, что компилятор кеширует результаты работы можно нарватся на протухший кеш объекта который компилятор схавает, а протухший кеш модуля совсем мало вероятен...

DIYMan
DIYMan аватар
Offline
Зарегистрирован: 23.11.2015

По вашим пунктам 1,2,3 - MVC в чистом виде. Его и реализуйте, дальше обсуждать - ни о чём, всё уже придумано за нас.

Вот, в общем: https://ru.wikipedia.org/wiki/Model-View-Controller

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

Ну, удачи Вам!

vde69
Offline
Зарегистрирован: 10.01.2016

собственно отчитываюсь - все заработало, единственное - были заморочки с видимостью, но все решилось нормально...

конвертер:

//#ifdef DATE_TIME_USE || DEVICE_RTC_I2C_DS1307
// ------------------------------------------------------------
// конвертация даты и времени объекта DT, параметры не требуются
long DT_year   (uint8_t _i=0, int _k=0) { return DT.year();   }
long DT_month  (uint8_t _i=0, int _k=0) { return DT.month();  }
long DT_day    (uint8_t _i=0, int _k=0) { return DT.day();    }
long DT_hour   (uint8_t _i=0, int _k=0) { return DT.hour();   }
long DT_minute (uint8_t _i=0, int _k=0) { return DT.minute(); }
long DT_second (uint8_t _i=0, int _k=0) { return DT.second(); }
// ------------------------------------------------------------
//#endif 

инициализация экрана:

  #ifdef DEVICE_LCD_4X20_I2C 
    // начальная инициализация режимов
    lcd_text.init();                

    // оформление страницы
    lcd_text.OutPageDecor(          
      "--------------------", 
      "XXXX/XX/XX XX:XX:XX ", 
      "                    ", 
      "--------------------", 
      'X');

    // включение слежения за данными
    lcd_text.SetParam(0, DT_year,   0, 0, '0', 1,  0,  3, true);
    lcd_text.SetParam(1, DT_month,  0, 0, '0', 1,  5,  6, true);
    lcd_text.SetParam(2, DT_day,    0, 0, '0', 1,  8,  9, true);
    lcd_text.SetParam(3, DT_hour,   0, 0, '0', 1, 11, 12, true);
    lcd_text.SetParam(4, DT_minute, 0, 0, '0', 1, 14, 16, true);
    lcd_text.SetParam(5, DT_second, 0, 0, '0', 1, 18, 19, true);

  #endif 

сам класс

#ifndef LCD_4X20_I2C_H        
#define LCD_4X20_I2C_H

#include <LiquidCrystal_I2C.h>

class Lcd_I2C_4x20: public LiquidCrystal_I2C {
private:
  unsigned long time_anime = 0;
  uint8_t CountParam = 0;

  AccessParamS *param;   // массив параметров для вывода на экран
  OutTextParamS *decor;  // массив определяющий оформление на экране

  void OutChar(char c, int col, int line)  {
    LiquidCrystal_I2C::setCursor(col, line);
    LiquidCrystal_I2C::print(c);
  }
  
  void PrintLcd(String str, int col, int line) {
    setCursor(col, line);
    print(str);
  }

  void OutNumber(long value, byte col, uint8_t line, uint8_t count, char Mask)
  {
    String stringOne;
    int str_l;

    stringOne = String(value);
    str_l = count - stringOne.length();
    for (int i=1; i <= str_l; i++){ stringOne = String(Mask) + stringOne; }
    PrintLcd(stringOne, col, line);
  }  

public:

  Lcd_I2C_4x20(uint8_t CountParam) : LiquidCrystal_I2C(0x27, 20, 4){
    this->CountParam = CountParam;
    param = (AccessParamS*) malloc(CountParam*sizeof(AccessParamS));
    for (uint8_t i = 0; i < CountParam; i++){ 
      param[i].ValueTec = 0; 
    }
    decor = (OutTextParamS*) malloc(CountParam*sizeof(OutTextParamS));
    for (uint8_t i = 0; i < CountParam; i++){ 
      decor[i].Active = false; 
    }
  }
  

  uint8_t Count() { return CountParam; }

  void init(){
    LiquidCrystal_I2C::init();
    LiquidCrystal_I2C::backlight();
    LiquidCrystal_I2C::noCursor();
  }

  void SetParam(uint8_t num, 
                long (*ReadValue)(uint8_t, int),
                uint8_t ParamIndex,
                int ParamK,
                char Mask, 
                uint8_t line, 
                uint8_t FirstCol, 
                uint8_t EndCol, 
                boolean Active) 
  // ***************************************************************************
  { 
    param[num].ValueTec   = 0; 
    param[num].ReadValue  = ReadValue;  
    param[num].ParamK     = ParamK; 
    param[num].ParamIndex = ParamIndex; 
    
    decor[num].Mask     = Mask; 
    decor[num].line     = line; 
    decor[num].FirstCol = FirstCol; 
    decor[num].EndCol   = EndCol; 
    decor[num].Active   = Active; 
  }
  // ***************************************************************************


  void OutPageDecor(char Str0[21], char Str1[21], char Str2[21], char Str3[21], char Mask)
  // ***************************************************************************
  { // процедура сделана "медленной", по сколько она вызывается редко
    // Str0, Str1, Str2, Str3 - строки маски экрана
    // символ маски для динамических полей (полей с изменяющимися полями)
    for (uint8_t i = 0; i < 20; i++){ if (Str0[i] != Mask) { OutChar(Str0[i], i, 0); } else {OutChar(' ', i, 0);}}
    for (uint8_t i = 0; i < 20; i++){ if (Str1[i] != Mask) { OutChar(Str1[i], i, 1); } else {OutChar(' ', i, 1);}}
    for (uint8_t i = 0; i < 20; i++){ if (Str2[i] != Mask) { OutChar(Str2[i], i, 2); } else {OutChar(' ', i, 2);}}
    for (uint8_t i = 0; i < 20; i++){ if (Str3[i] != Mask) { OutChar(Str3[i], i, 3); } else {OutChar(' ', i, 3);}}
  }
  // ***************************************************************************

  void poll(unsigned long time_loop)
  // ***************************************************************************
  {
    long l;
    for (uint8_t i = 0; i < CountParam; i++){ 
      if (decor[i].Active) {
        l = param[i].ReadValue(param[i].ParamIndex, param[i].ParamK);
        if (l != param[i].ValueTec) {
          param[i].ValueTec = l;
          OutNumber(l, decor[i].FirstCol, decor[i].line, decor[i].EndCol - decor[i].FirstCol + 1, decor[i].Mask);
        }
      }
    } 
  }
  // ***************************************************************************


  ~Lcd_I2C_4x20(void) {  
     free (param); 
     free (decor); 
  };



};

#endif 

описание структур

// общая структура для организации доступа (с приведением типа к long)
// к данным проекта находящимся внутри обектов
typedef struct {
  long ValueTec;                        // текущее значение (с учетом коэффициента)
  long (*ReadValue)(uint8_t, int);      // указатель на функцию источник данных, возвращающую long и имеющет 2 параметра 
  uint8_t ParamIndex;                   // параметр поиска, или индекс массива или тип устройства
  int ParamK;                           // коэффициент, при хранении числа с исходным значением 1234.6575 и коэффициенте 100 
                                        // функция вернет 123465, при коэффициенте -100 вернет 12
} AccessParamS;

// общая структура для организации вывода на символьные устройства
typedef struct {
  char Mask;         // маска символов, использование варьируется
  uint8_t line;      // номер строки для вывода
  uint8_t FirstCol;  // номер колонки первого символа
  uint8_t EndCol;    // номер колонки последнего символа
  boolean Active;    // признак активности, если false вывод не используется
} OutTextParamS;

 

Logik
Offline
Зарегистрирован: 05.08.2014

del