Отправляет email-рассылки с помощью сервиса Sendsay

C++ для всех

  Все выпуски  

C++ для всех


Информационный Канал Subscribe.Ru

C++ для всех. Выпуск 9

Приведение типов

Здравствуйте, уважаемые подписчики

Сегодня разговор пойдет о способах приведения типов, распространенных в языке С++. Итак, приведение типов как таковое может происходить явно и неявно, причем за неявное приведение типов отвечает компилятор со всеми вытекающими последствиями. Рассмотрим пару примеров неявного приведения:


 class A
 {
 public:
  A(bool b=true){ printf("A(bool b=true)\n"); }
 };

 int main()
 {
  A a;
  a = 10;

  char i;
  double d = 1000;

  i = d;//усечение значения
  
  return 0;
 }

На экране после выполнения видим:


 A(bool b=true)
 A(bool b=true)

Т.е. в данном случае компилятор сгенерировал такой код:


 A a;
 a = A(bool(10));

Цифра 10 в этом примере приведена только для того, чтобы показать, что компилятор к некоторым типам может преобразовывать почти все. Существуют определенные правила неявного приведения типов, вкратце скажем так - любые численные и булевые типы могут неявно преобразовываться один в другой, при преобразовании возможна потеря величины значений (усечение), например в случае приведения int к char или int к uint. Как правило, компиляторы выдают предупреждения при возможном некорректном приведении чисел. Булевым типам соответствуют такие числовые значения: true - 1, false - 0.

Бывают случаи, когда необходимо обезопасить себя от возможного случайного создания временных объектов при приведении типов. В этом случае при объявлении конструктора класса необходимо использовать ключевое слово explicit:


 class A
 {
 public:
  explicit A(bool b=true){ printf("A(bool b=true)\n"); }
 };

 int main()
 {
  A a;
  a = 10; //ошибка компиляции

  a = A(10); //нормально

  return 0;
 }

Кстати, большинство печатающихся программистов рекомендуют explicit использовать как можно чаще.

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

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

В стиле языке С для явного приведения указателя к требуемому типу используется такая форма записи:


 class A{};
 class B : public A{};
 class C : public B{};

 int main()
 {
  A *pa = new C;

  C *pc = (C*)pa;
  B *pb = (B*)pa;

  ...
  delete (C*)pa;

  return 0;
 }

Проблема в том, что для подобных случаев мы обязаны либо четко знать, что за тип мы имеем либо вводить дополнительный идентификатор типа, - первый случай чреват ошибками, второй - неудобен. Именно поэтому подобное явное приведение крайне не рекомендуется к использованию в языке С++ и применяется в исключительных случаях вроде приведения типа void*.

В С++ существую более безопасные механизмы приведения типов.

Сразу выделим два варианта явного приведения - динамическое приведение и статическое.

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

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

В С++ существует несколько операторов приведения типов: static_cast, const_cast, dynamic_cast, reinterpret_cast. Опишем каждый из них подробнее.

static_cast

Фактически, для большинства случаев там, где Вы применяли (type)expression следует писать const_cast<type>(expression):


 int main()
 {
         int v;
         double d;

         d = static_cast<double>(v);
  
  return 0;
 } 

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

const_cast

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


 struct STest{
  int key;
  char str[128];
 };

 const STest* getValue(int key){
  STest *ret;
  ...
  return ret;
 }

 int main()
 {
  STest *p = const_cast<STest*>(getValue(10));
  if(p) strcpy(p->str,"Hello");

  return 0;
 }

При использовании оператора const_cast важно понимать, что снятие константности не меняет логики программы, поэтому если функция возвращает константное выражение, то предполагается, что его нельзя изменять. В приведенном примере подразумевается, что член структуры STest::key является немодифицируемым ключем, а STest::str изменять можно.

dynamic_cast

Данный оператор предназначен для динамической идентификации типов и как правило, применяется в иерархии классов. Смотрим пример:


 class A
 {
 public:
  virtual ~A(){}
  void a(){ printf("A::a()\n"); }

 };

 class B : public A
 {
 public:
  void b(){ printf("B::b()\n"); }
 };

 class C : public B
 {
 public:
  void c(){ printf("C::c()\n"); }
 };

 int main()
 {
  A *pa = new C;

  if(A *p=dynamic_cast<A*>(pa)) p->a();
  if(B *p=dynamic_cast<B*>(pa)) p->b();
  if(C *p=dynamic_cast<C*>(pa)) p->c();

  delete pa;

  return 0;
 }

На экране видим:


 A::a()
 B::b()
 C::c()

А теперь несколько изменим функцию main:


 int main()
 {
  A *pa = new B;

  if(A *p=dynamic_cast<A*>(pa)) p->a();
  if(B *p=dynamic_cast<B*>(pa)) p->b();
  if(C *p=dynamic_cast<C*>(pa)) p->c();

  delete pa;

  return 0;
 }

На экране видим:


 A::a()
 B::b()

Нельзя применять dynamic_cast для константных выражений:


 ...

 int main()
 {
  A *pa = new B;
  const A *pa1 = pa;

  A *p=dynamic_cast<A*>(pa1); //ошибка компиляции
  const A *p=dynamic_cast<const A*>(pa1); //нормально

  delete pa;

  return 0;
 }

reinterpret_cast

Данный оператор настоятельно не рекомендуется использовать, т.к. он является платформозависимым, но знать что он делает нужно. reinterpret_cast оперирует внутренним представлением объекта, причем вся ответственность за корректность применения ложится на программиста. По-сути, это эквивалент явному преобразованию в стиле С, только называется по другому - именно это название и служит для того, чтобы программист внимательно контролировал преобразование. Посмотрите на пример ошибочного использования reinterpret_cast:


 int main()
 {
  int *val;
  char *pc = reinterpret_cast<char*>(val);

  std::string s(pc); //неопределенное поведение

  return 0;
 }

Фактически, вышеприведенный пример эквивалентен следующему:


 int main()
 {
  int *val;
  char *pc = (char*)val;

  std::string s(pc); //неопределенное поведение

  return 0;
 }

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

Практическое занятие

Регулярно стали поступать предложения написать что-нибудь полезное с использованием ранее описанного материала. Не могу с этим не согласиться. Возник только вопрос: что писать? Так как данная рассылка не является платформенно зависимой, соответственно и писать мы будем не зависящее от операционных систем.

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

Итак, постановка задачи. Наш класс должен хранить в себе иерархичный список параметров. Уровень вложенности параметров не ограничен. Графически структура параметров выглядит так:


 <val1>
  <val2>
   name=www
   ddd=1212
   dddfff=1091029
   <valx>
    ccc=10
   </valx>
  </val2>
 </val1>
 <val2>
  name=1111111
  vvv=111
 </val2>

Короче, структура хранения данных чем-то напоминает xml-файл. Строка поиска и инициализации, например параметра ссс будет выглядеть так:

 
 val1.val2.valx.ccc=

Так как число вложенных уровней не ограничено, целесообразно определить базовый класс с древовидной структурой:


---tree.h---start-------------------------------------------------------------------------------------------------

 #ifndef TREE_H
 #define TREE_H

 template<class T> class TTree
 {
  public:
  class Item
  {
   friend class TTree;
  public:
   Item(Item &to_node,T *d,bool as_last=false)
    :ttree(to_node.ttree),data(d),echild(0),eparent(&to_node)
   {
    ++(ttree->count_i);
    if(as_last){
     eprev = to_node.echild;
     if(eprev)
      while(eprev->enext) eprev=eprev->enext;
     if(eprev)
      eprev->enext = this;
     else
      to_node.echild = this;
     enext = 0;
    }
    else{
     eprev = 0;
     enext = to_node.echild;
     to_node.echild = this;
    }
    if(enext) enext->eprev = this;
   }
  
   Item(TTree &tr,Item *after,T *d)
    :ttree(&tr),data(d),echild(0),eprev(after)
   {
    ++(ttree->count_i);
    if(after){
     eparent = after->eparent;
     enext = after->enext;
     if(enext) enext->eprev = this;
     after->enext = this;
    }
    else{
     eparent = 0;
     enext = tr.first_e;
     if(enext) enext->eprev = this;
     tr.first_e = this;
    }
   }
   ~Item()
   {
    if(eprev)
     eprev->enext = enext;
    else if(this == ttree->first_e)
     ttree->first_e = enext;
    if(enext) enext->eprev = eprev;
    if(eparent && eparent->echild==this) eparent->echild = enext;
   
    if(ttree->autoDelete()) delete data;

    Item *p;
    while(echild){
     p = echild;
     delete p;//для предотвращения возможного обнуления указателя
    }
    --(ttree->count_i);
   }

   bool isNode()const{ return echild!=0; }
   Item* firstChild()const{ return echild; }  
   Item* prev()const{ return eprev; }
   Item* next()const{ return enext; }
   Item* parent()const{ return eparent; }
   TTree* tree()const{ return ttree; }

   T* getData()const{ return data; }
   void setData(T *d){ data=d; }
   T* operator->()const{ return data; }

   void moveItem(Item &after)
   {
    if(&after!=this){
     if(eparent && (eparent->echild==this)) eparent->echild = enext;
     if(eprev) eprev->enext = enext;
     if(enext) enext->eprev = eprev;
     if(ttree->first_e==this) ttree->first_e = enext;

     enext = after.enext;
     if(enext) enext->eprev = this;
     after.enext = this;
     eprev = &after;

     eparent = after.eparent;
    }
   }

  private:
   Item *eparent;
   Item *echild;
   Item *eprev;
   Item *enext;

   T *data;

   TTree *ttree;
  };

  TTree(bool ad=false):auto_delete(ad),first_e(0),count_i(0){}
  virtual ~TTree(){ clear(); }

  int count()const{ return count_i; }
  
  void setAutoDelete(bool ad){ auto_delete = ad; }
  bool autoDelete()const{ return auto_delete; }

  Item* firstItem()const{ return first_e; }
  Item* lastItem(Item *item=0)const{
   if(!item){
    if(!first_e) return 0;
    item=first_e;
   }
   while(item->enext) item = item->enext;
   return item;
  }

  Item* findItem(const T *d,const Item *from=0)const{ return finditem(from ? from : first_e,d); }
  bool itemExists(const Item *item)const{ return itemexists(first_e,item); }

  virtual void clear();

 protected:
  Item* finditem(const Item *ret,const T *d)const{
   Item *item;
   while(ret){
    if(ret->data==d) return ret;
    item = finditem(ret->echild,d);
    if(item) return item;
    ret = ret->enext;
   }
   return ret;
  }
  bool itemexists(const Item*,const Item*)const;

 private:
  Item *first_e;
  int count_i;
  bool auto_delete;
 };

 //-------------------------------------------------------------------------

 template<class T>
 inline void TTree<T>::clear()
 {
  Item *p;
  while(first_e){
   p = first_e;
   delete p;
  }
 }

 template<class T>
 bool TTree<T>::itemexists(const Item *item,const Item *find)const
 {
  while(item){
   if(item==find) return true;
   if(itemexists(item->echild,find)) return true;
   item = item->enext;
  }
  return false;
 }

 #endif //TREE_H

---tree.h---end---------------------------------------------------------------------------------------------------

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


На сегодня все. Пожелания, предложения, вопросы прошу на iqsoft@cg.ukrtel.net

В следующем выпуске - исключения


(с) Юрий Гордиенко



http://subscribe.ru/
E-mail: ask@subscribe.ru
Отписаться
Убрать рекламу

В избранное