Читать интересную книгу Программирование. Принципы и практика использования C++ Исправленное издание - Бьёрн Страуструп

Шрифт:

-
+

Интервал:

-
+

Закладка:

Сделать
1 ... 263 264 265 266 267 268 269 270 271 ... 337
контексте программирования встроенных систем). В языке С++ основным источником проблем является неосторожное использование указателей.

Выделим две проблемы.

• Явные (непроверяемые и опасные) преобразования.

• Передача указателей на элементы массива.

Первую проблему можно решить, строго ограничив использование явных преобразований типов (приведения). Проблемы, связанные с указателями и массивами, имеют более тонкие причины, требуют понимания и лучше всего решаются с помощью (простых) классов или библиотечных средств (например, класса 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), поэтому схему распределения памяти можно проиллюстрировать так.

Другими словами, функция poor() вызывает функцию draw() с указателем, ссылающимся в середину объекта класса Circle! Это скорее всего приведет к немедленной катастрофе (краху)!

1 ... 263 264 265 266 267 268 269 270 271 ... 337
На этом сайте Вы можете читать книги онлайн бесплатно русская версия Программирование. Принципы и практика использования C++ Исправленное издание - Бьёрн Страуструп.
Книги, аналогичгные Программирование. Принципы и практика использования C++ Исправленное издание - Бьёрн Страуструп

Оставить комментарий