Опубликован: 06.10.2011 | Доступ: свободный | Студентов: 1678 / 101 | Оценка: 4.67 / 3.67 | Длительность: 18:18:00
Дополнительный материал 3:

Введение в C++ (по материалам Надежды Поликарповой)

Дополнительные механизмы структурирования программ

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

          // "some_library.h": 
namespace some_library {
       class Parser {…};
       class Lexer {…}
       … 
}
         // "your_program.cpp": 
#include "some_library.h" 
namespace your_program {
       class Parser {…}; // Неопределенности нет: области разные 
}

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

using some_library::Lexer;
Lexer lexer;        // Сокращение для some_library::Lexer lexer

Это можно сделать глобально для всего пространства имен

using namespace some_library;

Отсутствующие элементы

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

Контракты

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

Некоторым утешением является то, что в C++ разрешается использовать оператор утверждения - assert:

assert b;

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

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

Многие люди предложили расширения C++ или пакеты макросов (макрос - набор операторов препроцессора - своего рода процедура) для эмуляции проектирования по контракту. Поиск в Web с запросом "Design by Contract in C++" даст ссылки на многие из этих инструментальных средств, чье использование остается ограниченным, так как они не интегрированы в язык.

Агенты

Как отмечалось, C++ не имеет механизма агентов. Простой эффект вызова переменной, задающей функцию, может быть достигнут (как мы видели) через указатели функций. Более сложное решение использует понятие функтора, реализованного перегрузкой операции " ()" - операции вызова функции. Объект "функтор" можно вызывать подобно любой другой функции. Неудобство в том, что требуется определить отдельный класс для каждого возможного числа формальных аргументов.

Ограниченная универсальность

Мы видели, что в C++ нет прямого соответствия ограниченной универсальности и что каждая конкретизация шаблона осуществляет собственную проверку типов. Это означает невозможность информировать клиентов, что родовой параметр представляет потомка определенного типа. Они узнают об этом, только нарушив это требование, когда конкретизация шаблона не будет компилироваться.

Методологическое правило - ограничение универсальности необходимо задавать неформально через комментарии в определении шаблона:

template <typename G> /* G должен быть потомком Comparable */ 
G max (G a, G b) { … }
Общая структура наследования

В C++ нет эквивалента класса, представляющего вершину в иерархии наследования, такого, как ANY в Eiffel, нет и класса - аналога NONE.

Специфические свойства языка

Мы уже встречались с несколькими свойствами C++, не доступными в Eiffel. Сейчас рассмотрим две другие особенности: аргументы по умолчанию и вложенные классы.

Аргументы по умолчанию

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

void f (float x, float y,	int n = 1, char c = !) { … }
f (1.2, 5.0, 2, 'a');      //можно задать значения всех фактических аргументов
f (1.2, 5.0, 2);	       //c имеет значение по умолчанию '!'
f (1.2, 5.0);	           //n и c имеют значения по умолчанию 1, '!'

Можно также определить функцию с переменным числом аргументов, используя многоточие вместо списка формальных аргументов. Этот механизм не является безопасным по типу и более подходит для программирования на уровне С, чем для приложений, написанных для C++.

Вложенные классы

В C++, как отмечалось, допускаются многие формы вложенности. В частности, класс может быть объявлен внутри другого класса и даже функции. Вложенный класс называется членом того класса, в котором он объявлен и может иметь такой же статус доступа, как и другие члены класса, - private, protected или public.

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

Библиотеки

Часто используемая в приложениях C++ библиотека STL (Standard Template Library) покрывает фундаментальные структуры данных, в частности, контейнеры, требующие универсальности, так что большинство ее классов представляют шаблоны (отсюда название). В этой же библиотеке размещаются классы ввода-вывода и исключений.

Для ввода-вывода STL использует потоки, которые могут представлять окружение, такое как консоль (стандартные потоки cin и cout), файлы и строки. При чтении и записи применяются перегруженные операции побитового сдвига >> и << , так что типичное взаимодействие с консолью выглядит примерно так:

Person p (…);
int my_age;
cout << "Name: " << p.name << endl << "Age: " << p.age () << endl;
cout << "Enter your age: " << endl;
cin >> my_age;

Здесь endl устанавливает конец строки. Доступны и библиотеки третьих компаний.

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

Синтаксические и лексические аспекты

Грамматика C++ для операторов и выражений сложна; только базисные ее элементы будут рассмотрены.

Операторы как выражения

Ключевым понятием является операторное выражение - выражение, заканчивающееся точкой с запятой. Это понятие кажется парадоксальным, так как в этой книге проводится четкое разделение между операторами и выражениями, в соответствии с различием команд и запросов. В C++, однако, не настаивают на таком разделении, так что операторное выражение является как оператором, так и выражением, возвращающим значение, если только тип его отличен от void.

Соответственно, функция, возвращающая значение, может иметь побочный эффект. В C++ общепринято вызывать такую функцию, как оператор. В этом случае теряется возвращаемый результат.

Одним из следствий смешения концепций является то, что присваивание рассматривается как выражение, чье значение присваивается цели (в качестве побочного эффекта). Это делает возможным такие комбинации, как:

a = b = 5;

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

Управляющие структуры

Блоки соответствуют составному оператору Eiffel и состоят из списка операторов в фигурных скобках. Блоки, как мы видели, могут быть вложенными. Условный оператор имеет форму:

if (expression) statement else statement

Заметьте, выражение условия - expression - должно быть в круглых скобках. Оно не обязано быть булевского типа, а может быть числовым и даже указателем - 0 и Null интерпретируются как false, остальные значение эквивалентны true. Здесь и в других структурах оператор может быть блоком. Принято даже одиночный оператор заключать в фигурные скобки, чтобы облегчить возможные добавления в будущем.

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

if (expression) statement
else if (expression) statement
else if (expression) statement
…
else statement
Оператор выбора имеет форму:
switch (expression) {
case value: statement; break;
case value: statement; break;
…
default: statement
}

Здесь expression задается булевским или целочисленным выражением, а каждое value представляет вычислимую в период компиляции константу. Если значение выражения не совпадает ни с одной константой, то выполняется ветвь default, если она задана, в противном случае ничего не делается (в Eiffel в отсутствие ветви else в операторе inspect в подобной ситуации в период выполнения генерируется ошибка). Оператор switch не задает конструкцию с одним входом и одним выходом, а представляет многоцелевой goto. Для правильной структурированности следует четко следовать показанной схеме. Оператор break, завершающий каждую ветвь, позволяет избежать типичной ошибки для C++ и C, когда управление проваливается в другую ветвь.

В C++ возможны три вида циклов:

while (expression) statement
do statement while (expression);
for (init_statement ; expression ; advance_statement) body_statement

Во всех этих вариантах expression служит условием продолжения. Это отличается от соглашения для формы from … until … loop … end, используемой в этой книге, где until-выражение используется как условие выхода. Для преобразования условия из одной формы в другую достаточно применить операцию отрицания.

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

Цикл for - наиболее общий и наиболее часто используемый. Цель advance_statement - обеспечить продвижение к следующей операции (в Eiffel эта часть включается в тело цикла). Приведем пример цикла в Eiffel:

from i := 1 until i > n loop
      …
      i := i + 1 
end

Его эквивалент в C++:

for (int i = 1; i <= n; i++) 
        {…}

В C++ используются goto-подобные операторы: сам goto, применять который не рекомендуется, оператор break, появившийся в связи со switch, и return, применяемый для возврата значения функцией:

return expression;

Оператор завершает выполнение и возвращает заданное значение (для процедур C++ - функций, возвращающих void - выражение expression опускается).

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

Присваивание и его расширения

Цель присваивания не обязана быть переменной - она должна обозначать область в памяти (называемую "left-value" или "l-value", так как появляется слева от символа присваивания). Вот несколько примеров:

int a; Person p;
a = 5;	                       // Правильно
// a + 2 = 5;	                     Возникла бы ошибка, поскольку a +2 не связана с областью
                                  // памяти 
*(&a + 1) = 5;                // Правильно: присваивание области памяти, следующей за a 
p.name = "Izzy";                  // Правильно: присваивание области памяти, отведенной полю
                                  // name объекта p

Некоторые C++ операции вместе с присваиванием выполняют заданную операцию. В частности:

  • a += b - это краткая запись для a = a + b, аналогичный смысл и для других операций, отличных от +;
  • для выражения a = a + 1 существует еще более краткая форма: a++ или ++a. Как обычно, это выражения, которые могут играть роль операторов. Разница в том, что первое выражение возвращает в качестве результата a, второе - увеличенное на 1 значение. Потом уже возникает побочный эффект (тест: каков эффект присваиваний a = a++ и a = ++a? ).

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

if (x = y) {Some_instructions}

На этом попадаются даже опытные программисты - скорее всего, требовалась проверка на равенство, а получилось присваивание. Компилятор все это пропустит и с большой вероятностью выполнит Some_instructuions, если только y отлично от нуля.

Выражения и операции

В C++ выражение является литералом, идентификатором, this или выражением со знаками операций. Следующая таблица включает все операции C++. Унарными операциями являются:

Операция Роль Пример Операция Роль Пример
+ Унарный плюс +a Delete Освобождение памяти delete p
- Унарный минус -a Sizeof Размер типа выражения sizeof (a + b)
* Разыменование *p ++ префиксное увеличение ++a
~ Побитовое отрицание ~a ++ постфиксное увеличение a++
! Логическое отрицание !b -- префиксное уменьшение --a
new Выделение памяти new int (5) -- постфиксное уменьшение a--

Бинарными операциями являются:

Операция Роль Пример Операция Роль Пример
+ Бинарный плюс a + b = Присваивание a = 5
- Бинарный минус a - b += Присваивание плюс a += 5
* Умножение a * b -= Присваивание минус a -= 5
/ Деление a / b *= Присваивание умножить a *= 5
% Взятие по модулю a % b /= Присваивание делить a /= 5
^ Побитовое xor a ^ b %= Присваивание по модулю a %= 5
& Побитовое and a & b ^= Присваивание xor a ^= b
| Побитовое or a | b &= Присваивание and a &= b
&& Логическое and b1 && b2 |= Присваивание or a |= b
|| Логическое or b1 || b2 <<= Присваивание сдвиг влево a <<= 1
== Эквивалентно a == 5 >>= Присваивание сдвиг вправо a >>= 1
!= Не эквивалентно a != 5 […] Взятие индекса a [i]
< Меньше чем a < 5 , Последовательность a, b = 2
<= Меньше или равно a <= 5 . Доступ к члену x.f
> Больше чем a > 5 ._ Непрямой доступ к члену x.*pf
>= Больше или равно a >= 5 -> Доступ через указатель px->f
<< Побитовый сдвиг влево a << 1 ->* Непрямой доступ через указатель px->*pf
>> Побитовый сдвиг вправо a >> 1 :: Разрешение области Per son::name

Операция деления адаптирована к типам операндов: для целых операндов - это деление нацело; если хотя бы один операнд с плавающей точкой, то деление с плавающей точкой.

Операция "последовательность" (запятая) в духе языка - слияние операторов и выражений; последовательность выражений вычисляется слева направо, результат - значение последнего выражения. Пример демонстрирует возможность краткой записи свопинга - обмена значениями двух переменных - с помощью одного присваивания:

b = (temp = a, a = b, temp)

Нужно ли говорить, что предпочитать нужно ясную запись, даже если она длиннее.

В C++ поддерживается понятие условного выражения, задаваемого в форме:

x ? a : b

В этой тернарной операции x, a, b - выражения; x интерпретируется как булевское, если оно истинно, то результатом является значение a, в противном случае - b. Вот образец типичного использования:

template <typename G>
G max (G a, G b) { return a > b ? a : b; }

При вызове функции применяются круглые скобки, окаймляющие список аргументов. Скобки также рассматриваются как операция.

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

.    .*   ::    :?   sizeof
Идентификаторы

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

В отличие от Eiffel, идентификаторы C++ чувствительны к регистру.

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

Литералы

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

Целая константа может быть десятичной, восьмеричной, начинающейся цифрой 0, ше-стнадцатеричной с предшествующими символами 0x. Десятичное число 12 можно записать тремя константами:

12	   // Десятичная
014	   // Восьмеричная
0xC	   // Шестнадцатеричная

Будьте внимательны, не начинайте нулем десятичные константы - константа 012 интерпретируется как восьмеричная, ее значение -10.

Символьные константы заключаются в одиночные кавычки - 'A'. Константы с плавающей точкой состоят из целой части, десятичной точки, дробной части и, возможно, целой экспоненты, состоящей из символа e, за которым следует целое, возможно со знаком. По умолчанию такая константа относится к типу double, если только она не заканчивается символом f, указывающим на тип float, или l - тогда тип long double. Строковая константа - это последовательность символов, заключенная в парные кавычки.

Ключевые слова

Следующие имена зарезервированы в C++ для использования в качестве ключевых слов:

asm, auto, break, case, catch, char, class, const, continue, default, delete,
 do, double, else, enum,extern, float, for, friend, goto, if, inline, int,
 long, new, operator, private, protected, public, register,return, short, 
signed, sizeof, static, struct, switch, template, this, throw, try, typedef,
 union,unsigned, virtual, void, volatile, while.

Дальнейшее чтение

Руководство от автора языка (всякий, кто серьезно интересуется C++ должен прочитать его):

Bjarne Stroustrup: The C++ programming language, 3rd edition, Addison-Wesley, 2000.

Последнее издание на русском языке: Бьёрн Страуструп, Язык программирования С++, Специальное издание, Бином, 2008 г.

Вводные тексты:

Herbert Schildt: C++: A Beginner's Guide, McGraw-Hill, 2003

На русском языке: Герберт Шилдт, Самоучитель по С++, 3-е издание, БХВ -Петербург, 2002 г.

Bruce Eckel: Thinking in C++: Introduction to Standard C++, Prentice Hall, 2000.

Для продвинутых свойств, особенно для программирования, основанного на шаблонах:

Andrei Alexandrescu: Modern C++ Design: Generic Programming and Design Patterns Applied, Addison-Wesley, 2001.

David Vandevoorde and Nicolai M. Josuttis: C++ Templates: The Complete Guide, Addison-Wesley, 2002.

David Abrahams and Aleksey Gurtovoy: C++ Template Metaprogramming: Concepts, Tools and Techniques from Boost and Beyond, Pearson, 2004.