Первую проблему можно решить, строго ограничив использование явных преобразований типов (приведения). Проблемы, связанные с указателями и массивами, имеют более тонкие причины, требуют понимания и лучше всего решаются с помощью (простых) классов или библиотечных средств (например, класса array; см. раздел 20.9). По этой причине в данном разделе мы сосредоточимся на решении второй задачи.
25.4.1. Непроверяемые преобразования
Физические ресурсы (например, регистры контроллеров во внешних устройствах) и их основные средства управления в низкоуровневой системе имеют конкретные адреса. Мы должны указать эти адреса в наших
программах и присвоить этим данных некий тип. Рассмотрим пример.
Device_driver* p = reinterpret_cast<Device_driver*>(0xffb8);
Эти преобразования описаны также в разделе 17.8. Именно этот вид программирования требует постоянного использования справочников. Между ресурсом аппаратного обеспечения — адресом регистра (выраженного в виде целого числа, часто шестнадцатеричного) — и указателями в программном обеспечении, управляющим аппаратным обеспечением, существует хрупкое соответствие. Вы должны обеспечить его корректность без помощи компилятора (поскольку эта проблема не относится к языку программирования). Обычно простой (ужасный, полностью непроверяемый) оператор
reinterpret_cast
, переводящий тип
int
в указатель, является основным звеном в цепочке связей между приложением и нетривиальными аппаратными ресурсами.
Если явные преобразования (
reinterpret_cast
,
static_cast
и т.д.; см. раздел A.5.7) не являются обязательными, избегайте их. Такие преобразования (приведения) бывают необходимыми намного реже, чем думают программисты, работающие в основном на языках C и C++ (в стиле языка С).
25.4.2. Проблема: дисфункциональный интерфейс
Как указывалось в разделе 18.5.1, массив часто передается функции как указатель на элемент (часто как указатель на первый элемент). В результате он “теряет” размер, поэтому получающая его функция не может непосредственно определить количество элементов, на которые ссылается указатель. Это может вызвать много трудноуловимых и сложно исправимых ошибок. Здесь мы рассмотрим проблемы, связанные с массивами и указателями, и покажем альтернативу. Начнем с примера очень плохого интерфейса (к сожалению, встречающегося довольно часто) и попытаемся его улучшить.
void poor(Shape* p, int sz) // плохой проект интерфейса
{
for (int i = 0; i<sz; ++i) p[i].draw;
}
void f(Shape* q, vector<Circle>& s0) // очень плохой код
{
Polygon s1[10];
Shape s2[10];
// инициализация
Shape* p1 = new Rectangle(Point(0,0),Point(10,20));
poor(&s0[0],s0.size); // #1 (передача массива из вектора)
poor(s1,10); // #2
poor(s2,20); // #3
poor(p1,1); // #4
delete p1;
p1 = 0;
poor(p1,1); // #5
poor(q,max); // #6
}
Функция
poor
представляет собой пример неудачной разработки интерфейса: она дает вызывающему модулю массу возможностей для ошибок и не оставляет никаких надежд защититься от них на этапе реализации.
ПОПРОБУЙТЕ
Прежде чем читать дальше, попробуйте выяснить, сколько ошибок вы можете найти в функции
f
? В частности, какой из вызовов функции
poor
может привести к краху программы?
На первый взгляд данный вызов выглядит отлично,
но это именно тот вид кода, который приносит программистам бессонные ночи отладки и вызывает кошмары у инженеров по качеству.
1. Передается элемент неправильного типа (например,
poor(&s0[0],s0.size
). Кроме того, вектор
s0
может быть пустым, а в этом случае выражение
&s0[0]
является неверным.
2. Используется “магическая константа” (в данном случае правильная):
poor(s1,10)
. И снова тип элемента неправильный.
3. Используется “магическая константа” (в данном случае неправильная):
poor(s2,20)
.
4. Первый вызов
poor(p1,1)
правильный (в чем легко убедиться).
5. Передача нулевого указателя при втором вызове:
poor(p1,1)
.
6. Вызов
poor(q,max)
, возможно, правильный. Об этом трудно судить, глядя лишь на фрагмент кода. Для того чтобы выяснить, ссылается ли указатель
q
на массив, содержащий хотя бы max элементов, мы должны найти определения указателя
q
и переменной
max
и их значения при данном вызове.
В каждом из перечисленных вариантов ошибки были простыми. Мы не столкнулись с какими-либо скрытыми ошибками, связанными с алгоритмами и структурами данных. Проблема заключается в интерфейсе функции
poor
, который предусматривает передачу массива по указателю и открывает возможности для появления массы ошибок. Кроме того, вы могли убедиться в том, насколько затрудняют анализ такие малопонятные имена, как
p1
и
s0
. Тем не менее мнемонические, но неправильные имена могут породить еще более сложные проблемы.
Теоретически компилятор может выявить некоторые из этих ошибок (например, второй вызов
poor(p1,1)
, где
p1==0
), но на практике мы избежали катастрофы в данном конкретном случае только потому, что компилятор предотвратил создание объектов абстрактного класса
Shape
. Однако эта ошибка никак не связана с плохим интерфейсом функции
poor
, поэтому мы не должны расслабляться. В дальнейшем будем использовать вариант класса
Shape
, который не является абстрактным, так что избежать проблем с интерфейсом нам не удастся.
Как мы пришли к выводу, что вызов
poor(&s0[0],s0.size)
является ошибкой. Адрес
&s0[0]
относится к первому элементу массива объектов класса
Circle
; он является значением указателя
Circle*
. Мы ожидаем аргумент типа
Shape*
и передаем указатель на объект класса, производного от класса
Shape
(в данном случае
Circle*
). Это вполне допустимо: нам необходимо такое преобразование, чтобы можно было обеспечить объектно-ориентированное программирование и доступ к объектам разных типов с помощью общего интерфейса (в данном случае с помощью класса
Shape
) (см. раздел 14.2). Однако функция
poor
не просто использует переменную
Shape*
как указатель; она использует ее как массив, индексируя ее элементы.
for (int i = 0; i<sz; ++i) p[i].draw;
Иначе говоря, она ищет элементы, начиная с ячеек
&p[0]
,
&p[1]
,
&p[2]
и т.д.
В терминах адресов ячеек памяти эти указатели находятся на расстоянии
sizeof(Shape)
друг от друга (см. раздел 17.3.1). К сожалению для модуля, вызывающего функцию
poor
, значение
sizeof(Circle)
больше, чем
sizeof(Shape)
, поэтому схему распределения памяти можно проиллюстрировать так.