полный C++ полностью одобренные Димой и одобренные Мишей (1119617), страница 5
Текст из файла (страница 5)
pd = &d;
pb = dynamic_cast<Base*> (pd); // а так тоже можно
В C++ рекомендуется не делать статических явных преобразований типа. В нашем случае можно было написать pd = (Derived*)pb, но это плохо, так как нет контроля, а при динамическом кастинке тип преобразуется, только если это будет разумно.
Кстати, есть ещё один вариант операции: dynamic_cast<T&> (r);
Если ни одно из двух условий не выполняется, то генерируется исключение bad_cast (только для ссылок!).
Также есть операция typeid (об’ект) или typeid (имя_типа), которая возвращает ссылку на об’ект класса typeinfo. В нём перегружены операции сравнения и определена функция, возвращающая имя типа:
bool operator== (const typeinfo&);
bool operator!= (const typeinfo&);
const char* name ();
А вот так их можно использовать:
if (typeid(*pb) == typeid(Derived))
pd = dynamic_cast<Derived*> (pb);
// уже не нужно проверять pd != NULL
cout<<typeid(*pb).name()<<endl; // визуализация – узнать тип *pb
Есть ещё три оператора, которыми можно не пользоваться, потому что их не было в C. Но при программировании на C++ их лучше писать.
static_cast – отличается от dynamic_cast отсутствием проверок:
pd = static_cast<Derived*> (pb);
reinterpret_cast – значению одного указателя можно присваивать значение любого другого указателя (обычно используется для «принципиально различных» типов):
p = reinterpret_cast<int*> (q); // эквивалентно p = (int*)q;
const_cast – преобразование снятия константности.
Множественное наследование.
Множественное наследование - ситуация, когда производный класс создаётся на базе нескольких базовых классов.
Чтобы понять, зачем это нужно, можно привести пример от Бьярна Страуструпа. При программировании некого GUI у нас есть класс ‘окно’. От него есть производные классы: ‘окно с рамкой’ и ‘окно с меню’. А что если мы хотим создать окно с рамкой и с меню? Нам на помощь приходит множественное наследование.
Пример. Далее приведём чисто технический пример, который не несёт никакой смысловой нагрузки, но на котором можно показать синтаксис.
class A { ... };
class B { ... };
class C: public A, public B { ... };
Базовый класс не может появиться несколько раз в этом списке явно (ситуация ‘одна база – два раза’). Однако может возникнуть такая ситуация:
class L { public: int n; ... };
class A: public L { ... };
class B: public L { ... };
class C: public A, public B { ... };
Например, мы напишем: C.c; c.n = 0; Возникает неоднозначность: какую c нам вызвать, ту которая пришла к нам через A, или же ту, которая наследовалась через B. Здесь мы можем уточнить: c.A::n = 5; или c.B::n = 7;. Перед оператором разрешения контекста мы указываем точку, от которой начинается поиск переменной.
Теперь понятно, что мы имели ввиду под однозначным классом ранее, когда говорили о динамическом полиморфизме.
C *pc = new C;
L *pl = pc; // неверно
C неоднозначен, так как в нём ‘содержатся’ два L.
L *pl = (L*)(A*)pc; // а так можно!
L *pl = (L*)(B*)pc; // или так...
Но у нас может возникнуть вполне законное желание, чтобы в C был только один экземпляр L. Для этого придумали виртуальные базовые классы.
Виртуальные базовые классы.
c lass L { public: int n; ... };
class A: virtual public L { ... };
class B: virtual public L { ... };
class C: public A, public B { ... };
C *pc = new C;
L *pl = pc; // верно
Теперь мы можем присваивать указателю на базовый класс указатель на производный, поскольку второй доступен и однозначен. Заметим, что при наследовании со словом virtual никакой виртуализации функций не происходит, просто совпадают ключевые слова, видимо, из-за плохой фантазии создателей языка.
Есть ещё одна проблема, возникающая в том случае, когда есть два базовых класса, внутри которых совпадают имена членов. Рассмотрим этот случай. Не важно, совпадают имена методов или полей, или имени поля с именем метода. Неоднозначность может возникнуть на уровне имён.
class A {
public: int a;
void f (int);
void g();
...
};
class B {
int a;
void h (char);
public: void f ();
int g;
void h ();
void h (int);
...
};
class C: public A, public B { ... /* нет плохих имён */};
void blablabla (){
C c;
c.a = 1; // неоднозначность
Используемые для имён классов проверки производятся в таком порядке:
-
контроль однозначности в разных классах (втупую, считая все члены равноправными и доступными) - в разных классах в подобных ситуациях не должно быть одинаковых имён;
-
попытка разрешить вызов (определение, какая из перегруженных функций вызовется);
-
контроль доступности того, что выбрали.
c.f(1); // ошибка, 1 пункт
c.f(); // ошибка, 1 пункт
c.g(); // ошибка, 1 пункт
c.g = 1; // ошибка, 1 пункт
c.h(); // всё хорошо
c.h(1); // всё хорошо
c.h(‘a’); // ошибка доступа, пункт 3
c.A::g(); // можно, мы явно сказали, что вызывать
}
Лучше явно указывать, откуда мы хотим вызвать функцию, так как может оказаться, что мы случайно унаследовали что-то лишнее. Особенно это актуально, когда мы пользуемся библиотечными классами из разных библиотек.
Агрегация. Композиция.
При агрегации возникает проблема вызова конструктора.
Пусть нам нужно построить частотный словарь. Для строк есть конструктор
class String { ...
public: String (const char *);
};
class Word{
String data;
int count;
public: Word (const char *s, int count = 1): data(s)
{ this->count = count;}
};
Здесь мы инициализируем поле класса аргументом функции. Если нужно несколько инициализаторов, они перечисляются через запятую.
Если членом класса является константа или ссылка, то тоже необходимо использовать инициализаторы.
class X {
int t;
const int k;
int& r;
...
public: X (int i): k(i), r(t) { t = i; }
};
Статические члены класса.
В языке C статическая локальная переменная инициализируется один раз, при первом входе в функцию, а память, занимаемая ею, не удаляется из стека при выходе из функции. Также можно на файловом уровне определить статическую функцию. Тогда она будет глобальной функцией, на которую нельзя будет сослаться из другого файла. Если всё же попытаемся, получим ошибку линковки.
В C++ также можно использовать ключевое слово static внутри класса. Если мы описываем статическое поле, то оно создаётся в одном экземпляре и используется всеми об’ектами данного класса. Переменная инициализируется ещё до создания первого об’екта данного класса.
class X {
public: int a;
static int count; // счётчик созданных об’ектов
X (int i) { a = i; ++count;}
...
};
int X::count = 0; // в классе этого сделать нельзя!
По умолчанию все статические члены инициализируются нулями.
int main (){
cout<<X::count<<endl; // получим ноль. count инициализирован
X g(1), z(10);
cout<<g.count<<endl<<z.count<<endl; // две двойки
return 0;
}
Статические переменные появляются в тот момент, когда инициализируется класс. Как показано в примере, можно обращаться к ним и через об’екты, но это некрасиво.
Даже если сделать статический член класса скрытым, его всё равно можно будет инициализировать. Но тогда мы не сможем её напрямую получить, и в классе нужно описать функцию-геттер или выводящую функцию:
void printCount () { cout<<count; }
Эту функцию мы сможем вызвать, только если есть об’екты данного класса. Однако эту функцию можно тоже сделать статической. Нужно учитывать, что статические функции используют только статические поля класса, то есть она должна уметь работать независимо от об’ектов класса. Естественно, в статической функции не определён указатель this.
static void printCount () { cout<<count; }
Библия специальных функций-членов класса by ТВ.
Обычный конструктор вызывается:
-
при создании об’екта соответствующего класса, в том числе для элементов массива – конструктор умолчания;
-
при создании об’ектов в динамической памяти;
-
если об’ект – член другого класса (агрегация);
-
при создании об’екта производного класса вызываются конструкторы базовых (если ничего специально не напишем, то дефолтный конструктор будет пытаться вызваться);
-
для статических членов, являющихся об’ектами другого класса.
Конструктор копирования вызывается:
-
при инициализации об’екта;
-
при передаче в функцию параметра по значению (без амперсанда);
-
при возврате результата функции по значению;
-
при выбрасывании исключения пользовательского типа (читай дальше).
Деструктор вызывается:
-
при выходе из зоны описания локального об’екта;
-
при выполнении delete для указателя, полученного по new;
-
при явном вызове деструктора;
-
при свёртке стека (читай дальше).
Исключительные ситуации в C++
Существуют различные способы обработки ошибок:
-
прекратить работу программы (вызов exit()); метод плох тем, что глубинные функции не должны иметь таких полномочий;
-
специальный результат функции (так любят делать библиотечные функции C), например, NULL или -1; однако не всегда можно придумать значение, которое не может получиться в результате работы функции;
-
формирование некоторого признака (например, переменная errno); однако программисты не любят постоянно проверять его значение, также метод даёт сбои в некоторых ситуациях при параллельном программировании;
-
вывод на экран диагностики; это не даёт ответа, что делать дальше.
Идея исключений в следующем: отделить код, обрабатывающий входные данные, от кода, реагирующего на проблему.
Общая схема:
try {
// подверженный ошибкам код
if (ошибка) throw выражение;
}
catch (тип1 параметр) { ... }
catch (тип2 параметр) { ... }
Сначала мы входим в try, как будто его и нет вовсе. Если вдруг мы попадаем на инструкцию throw, то код ниже неё не выполняется, а управление передаётся. Среди параметров catch ищем подходящий тип исключения. Если находим, то выполняются действия в нём. Там может быть любой код, можно также использовать значение параметра. Когда инструкции выполнятся, переходим на оператор после последнего catch.
Обработчики могут быть трёх видов:
-
catch (тип параметр) { ... }
-
catch (тип) { ... } // не интересно значение параметра
-
catch (...) { ... } // обрабатывает любое исключение
Обработчик исключения catch типа T может обработать исключение типа E, если:
-
T и E одного и того же типа (с точностью до typedef)
-
T однозначный и доступный базовый класс к E;
-
T, E – указатели, E может быть преобразован к T с помощью стандартных преобразований (указатель на производный класс приводится к указателю на однозначный доступный базовый класс, любой указатель приводится к void* ; здесь 0 не преобразуется к NULL)
Вместо T также могут появиться const T, T&, const T&.
Важен порядок, в котором перечислены обработчики обработчики:
catch (void *) { ... } // все указатели попадут сюда
catch (char *) { ... } // так что этот обработчик не работает
catch (...) { ... }
catch (тип) { ... } // а до этого обработчика не доберется
// вообще ни одно исключение