Общепринято характеризовать ОО-языки как "чистые" (полностью и исключительно реализующие ОО-концепции) либо как "гибридные" (представляющие смесь объектных и необъектных свойств). Наиболее известным представителем гибридных языков является язык С++, который создавался с целью предоставления С-программистам некоторых ОО-идей и механизмов. Язык С++ по духу отличается от нотации , принятой в остальной части книги, и более сложен.
Можно найти много книг и вводных статей о С++, как начинающихся с чистого листа, так и предполагающих знание языка С. Роль этого приложения другая: она ориентирована именно на вас, внимательных читателей этой книги, кто прочел уже сотни страниц и овладел ОО-программированием в его "чистой" форме. Цель в том, чтобы в случае необходимости программирования на С++ применять его конструкции в духе . По этой причине многие конструкции C++ будут объясняться - как это сделано для Java и C# - в стиле: "Вот как это можно сделать на и как получить такой же эффект на С++".
Язык С, на котором основан С++, сам является важным языком, кратко рассматриваемым в следующем приложении.
Сегодня идея использования ОО-языка едва ли может удивить кого-либо, но в конце восьмидесятых годов она воспринималась как насмешка, - многим программистам и менеджерам в индустрии и академических кругах ОО-концепции казались привлекательными, но возникали большие сомнения в их применимости. В 1979 году Бьёрн Страструп из Bell
(рис 13.1) Бьёрн Страуструп (2007)
Laboratories спроектировал и реализовал язык, изначально названный "С с классами", расширяющий язык С концепциями, которые были заимствованы у языка , - первого ОО-языка. Код транслировался в чистый С препроцессором. Язык вскоре стал хитом и, как было обещано, облегчил переходный период для С-программистов.
В следующие два десятилетия язык интенсивно развивался за счет введения таких конструкций, как шаблоны (форма универсальности) и множественное наследование.
Для ОО-подхода программа представляет множество классов. В С++, сохраняющем гибридный дух, не настаивают на соблюдении этого правила. Программа может включать классы, но и другие элементы, не входящие в классы: функции (соответствующие методам, реализуемым либо в виде настоящих функций, либо в виде процедур), переменные, константы и типы, не заданные классами.
Примером независимой функции, появляющейся в каждой исполняемой программе, является функция, называемая main, которая определяет точку входа в программу. В эту роль играет корневая процедура создания.
Программная система на С++ представляет скорее не множество классов, а множество единиц трансляции (юнитов или модулей), каждая из которых содержится в отдельном файле, который независимо может быть обработан С++ компилятором. Каждая единица трансляции может содержать объявления классов и других типов, функций, переменных и констант.
Объявление такого элемента может быть его определением, означающим его полное описание, либо может объявлением, не задающим определение, таким как:
class Person; enum Week_day;
При этом понимается, что полное определение этого элемента (здесь класс и тип) появится либо в другом модуле, либо в той же единице, но позже. Это дает возможность использовать элемент без знания его детальных свойств. Для переменной или константы неопределяющее объявление использует ключевое слово extern, указывающее, что определение еще появится:
extern bool has_error; extern const double pi;
Определение функции включает имя, сигнатуру и реализацию:
int factorial (int n)
{
if (n > 1) {
return n _ factorial (n - 1);
} else {
return 1;
}
}
Определение класса содержит список членов класса (компонентов):
class Person { // Объекты, представляющие персон
string name; // Имя персоны
Date birth_date; // Дата рождения
void set_name (string s)
{name = s;}
void set_birth_date (Date d)
{birth_date = d;}
… // Другие члены класса
};
В определении переменной задается ее тип, предшествующий переменной в отличие от , где действует соглашение variable_name: TYPE.
int n; bool has_error = false;
Второе из этих объявлений содержит инициализацию переменной, устанавливающую ее начальное значение. Определения не содержат extern: это означает, что переменные не являются внешними, а значит, могут использоваться только в данном модуле трансляции.
Определение переменной означает, что ей будет отведена память, как для .
Определение константы всегда включает ее инициализацию:
const double Pi = 3.14159265358
В отличие от порядок объявлений существенен: имена, объявленные в модуле трансляции, можно использовать только после точки объявления. Для применения элемента до его определения - такая необходимость возникает, например, в случае множества взаимно рекурсивных определений - предварительно следует задать неопределяющее объявление.
Во избежание трансляции слишком большого модуля можно разделить его на несколько файлов, используя затем директиву включения #include, как в примере:
#include "filename"
Как результат, все определения, содержащиеся в исходном файле filename, будут доступны текущему модулю трансляции. Чаще всего это свойство предполагает использование заголовочных файлов (по соглашению имеющих имена в форме name.h), которые содержат объявления элементов, используемых многими модулями, и включают соответствующую директиву #include.
Директива #include, подобно любым другим предложениям, начинающимся с символа решетки #, адресована препроцессору С++ - инструментарию, обрабатывающему файлы программы до начала компиляции. Другое использование препроцессора связано с
#ifdef LINUX
… Linux-specific code …
#endif
Код для платформы Linux будет подключен в зависимости от LINUX-переменной. Такие переменные, не связанные с переменными программы, рассматриваются как опции, действующие на этапе работы препроцессора и компилятора.
Подобно , каждая C++ переменная имеет тип, но, в отличие от , не все типы основаны на классах, и следовательно, не все значения являются объектами. Тип может быть встроенным, производным (с возможными комбинациями механизмов порождения), определенным пользователем.
Встроенные типы предустановлены в языке. Они включают bool - для булевских значений; char, short int, int, long int - для целых из разных диапазонов; float, double, long double - для вещественных с плавающей точкой. Тип char также служит для представления символов.
Каждый целочисленный тип включает две версии - со знаком и без знака, такие как short int и unsigned short int. Версии без знака включают только положительные значения. По умолчанию все целые типы, за исключением char, являются знаковыми, делая избыточным задание такого типа, как signed int. Является ли char типом со знаком, определяется платформой.
Встроенный тип void не имеет значений. Он служит признаком процедур - функций, не возвращающих результат:
void set_name (string s);
T.
Тип const T представляет неизменяемое значение типа T. Например, n будет иметь тип const int, если ее определить как:
const int n = 5;
Разрешается переменной типа T присвоить значение типа const T, но обратное преобразование недопустимо.
Указателем на тип Т является тип T*. Значения этого типа обозначают адреса памяти, где хранится переменная типа T. Для получения указателя на переменную x используется , как в x. Возможно обратное преобразование, называемое разыменованием - получением значения по адресу. Если p - указатель, то *p дает значение, хранящееся в области памяти, на которую указывает указатель. Если значением является объект, то возможен доступ к его полям, например, полю f, используя нотацию с точкой: (*p).f. Специальный синтаксис p->f является синонимом нотации с точкой.
Поскольку указатель представляет адрес памяти, в языке разрешено добавление или вычитание целого из указателя для получения нового адреса, как в*(p + n) . Смысл его в следующем. Представим, что в памяти подряд хранятся n + 1 значение типа T, каждое занимает фиксированное число байтов, которое можно получить, используя конструкцию sizeof T. Тогда, если p типа T* и указывает на первое хранимое значение, то *(p + n) возвращает последнее значение (здесь неявно n умножается на число байтов, отводимых элементу - sizeof T, так что (p + n) дает адрес начала соответствующего элемента, а операция * - значение по этому адресу). Это все называется С для получения доступа к нужным участкам памяти. Механизм мощный, но чреватый ошибками (трудно гарантировать, что на самом деле хранится в динамически вычи
сляемом адресе). Без необходимости его не следует применять в приложениях С++.
Все типы указателей согласуются со специальным типом void*, напоминающем класс ANY в . Но это встроенное соответствие, не индуцированное наследованием. Поскольку нельзя иметь переменные типа void, нельзя проводить разыменование указателей void*, никакие операции над ними не выполняются. Чтобы их использовать, необходим кастинг - явное C++ есть разные способы приведения, дающие разные результаты в случае, когда
Тип T является "ссылкой на T". Подобно указателю, ссылка является адресом, но при любом использовании происходит автоматическое разыменование. Ссылки являются наиболее прямым способом получения эффекта обычного (ссылочного) класса в . Рассмотрим класс , и пусть в этом классе определен некоторый метод:
call_her_izzy (p: PERSON)
do p.set_name ("izzy") end
Эквивалентом метода в С++ будет:
void call_her_izzy (Person p)
{p.set_name ("izzy");}
Рассмотрим вызов этого метода call_her_izzy (Isabelle) , где Isabelle типа Person. Эффект состоит в изменении значения поля имени ссылочного объекта. Если бы аргумент в C++ версии имел тип Person, то вызов создавал бы копию объекта Person.
Метод работал бы на этой копии, но без видимого эффекта, так как копия локальна и исчезла бы по завершении выполнения метода.
Реализовать ссылочное поведение можно и с указателями, но сложнее - с явной адресацией и разыменованием:
void call_her_izzy (Person* p)
{p->set_name ("izzy"); // или (_p).set_name ("izzy");
}
В этом случае вызов метода имеет вид: call_her_izzy (Isabelle) . Использование указателей считается хорошим стилем в сравнении с передачей аргументов по ссылке, поскольку делает явной ссылочную семантику.
Еще одна разница между указателями и ссылками в том, что ссылки требуют инициализации, присоединяющей значение типа T к T ссылке, в то время как указатели могут иметь нулевое значение (называемое также null и соответствующее void в ). Однако это не обеспечивает преимущества присоединенных типов , так как возможно присвоить указатель T* переменной типа T или T, что станет причиной ошибки периода выполнения, если указатель равен null.
В целом ссылочные типы обеспечивают более строгую дисциплину, в частности, они не позволяют
Теперь поговорим о массивах. Тип, задающий массив (иногда говорят "массивный тип"), - T[size] , где размер size является целой константой, известной во время компиляции, представляет последовательность значений типа T, хранимой в подряд идущих словах памяти. Если ar - это массив, то ar[i] обозначает i-й элемент этого массива, где i - выражение целого типа. В C++ массив рассматривается как адрес начала расположения элементов массива в предположении, что памяти для их хранения достаточно. Для безопасной работы с индексами, гарантирующей, что выход за границы контролируется, следует использовать библиотечные классы.
Наконец, функциональный тип. Он фактически является указателем на тип функции. Пусть R, A1, …, An являются типами (последовательность задает сигнатуру функции). Рассмотрим объявление:
R (*f) (A1, …, An);
Здесь объявлена переменная f, чьи значения являются указателями на функции, возвращающие значение типа R и имеющие n аргументов типа A1, … An. Например, рассмотрим объявление:
void (*f) (Person*)
Переменной f можно присвоить указатель на функцию:
f = call_her_izzy; //или f = call_her_izzy;
После этого можно выполнять непрямой вызов:
f (Isabelle); // или (*f) (Isabelle);
Эффект будет тот же, что и при прямом вызове call_her_izzy (Isabelle) . Разница в том, что f - это переменная, которой можно присвоить указатели на разные функции (с заданной сигнатурой).
Указатели функций могут обозначать не только независимые функции, но и функции, представляющие членов класса, как в примере:
void (Person::*p) (string) … p = Person::set_name;
Здесь объявляется p и ему присваивается указатель на функцию класса Person, принимающий один аргумент типа string. Эту функцию можно вызывать, используя следующий синтаксис:
(Isabelle.*p) ("Izzy");
Как вы уже понимаете, указатели на функции близко связаны с двумя ОО-механизмами, основанными на способности вызывать метод, оставляя на момент выполнения определение того, какой именно метод будет вызван. Этими механизмами являются динамическое связывание и агенты. Указатели функций C++ дают возможность эмуляции этих свойств.
C++ предоставляет механизм виртуальных функций, изучаемый ниже, который реализует динамическое связывание. Поскольку в чистом С виртуальных функций нет, у них остается только одна возможность - применение указателей функций. Вот почему компиляторы Eiffel , транслирующие программу в код на C, основываются на объясненной ранее схеме.Пять механизмов построения
Можно комбинировать модификаторы const и pointer, чтобы получить:
Подобным образом можно комбинировать const и ссылки. Следующие примеры иллюстрируют некоторые из возможностей:
const int* pointer_to_const; const int reference_to_const; int* const const_pointer; int const const_reference;
На практике важной является нотация typedef, позволяющая именовать создаваемые типы. Это позволяет ссылаться на сложный производный тип, используя его простое и понятное имя:
typedef const Person* Cp; typedef void (* Pf) (Person*);
Теперь возможно использовать в объявлениях Cp p вместо const Person *p. Тип Pf обозначает теперь соответствующий тип указателя на функцию.
В добавление к ниже изучаемым классам, типы, определенные пользователем, включают перечисления, представляющие обычно небольшое множество фиксированных значений:
enum Week_day {Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday};
enum Error {Division_by_zero, Null_pointer_dereference, File_error, Memory_error};
Внутренне значения перечисления являются целыми константами. Тип перечисления неявно отображается в целочисленный тип, но обратное неверно, поскольку значения переменной перечисления находится в определенных границах.
Еще одним видом типа, определяемого пользователем, является тип, задающий структуры, обычно называемый просто "struct" и представляющий некоторую форму класса. Классы изучаются ниже. У структур в основном те же свойства, что и у классов, но другая политика экспорта.
Наконец, тип "объединение" описывает объекты, которые могут быть нескольких видов, занимая одну и ту же область памяти. Вместо ключевых слов class или struct для этого типа применяется термин union. Этот механизм, перешедший от C, где он был предназначен для оптимизации памяти, не является типо-безопасным, так как при использовании p.a нельзя гарантировать, что p является правильным вариантом, имеющим атрибут a. По этой причине программисты С++ редко используют тип объединения, основываясь вместо этого на наследовании, как в других ОО-языках, для поддержки вариантов общего типа.
Класс C++ задается следующим синтаксисом:
class A{
…// список членов класса
};
Определение может появиться в любом месте модуля трансляции, где допускаются определения, включая появление в объявлении другого класса и даже внутри тела функции, хотя такая вложенность - нечто экзотическое. Предпочтительно, но не требуется, включать определение класса в отдельный файл, как это вы бы сделали в .
Класс можно использовать как тип при объявлении переменных и для конструирования других производных и определенных пользователем типов:
A var; // Переменная типа A const A const_var; // Константа типа A A* p; // Указатель на объекты типа A
В C++ члены класса разделяются на члены-переменные, также называемые членами данных (соответствующие атрибутам ), и члены-функции, называемые также методами.
Определение класса может содержать как определения, так и неопределяющие объявления членов. В последнем случае эти члены должны быть определены вне класса.
class A{
int n; // член-переменная
void f () // член-функция, определенная в классе
{…}
void g (); // член-функция, определенная вне класса
};
void A::g ()//Определение g
{…}
В C++ не применяется унифицированный принцип доступа - доступ к переменной и функции без аргументов рассматриваются как различные операции. Для функций всегда следует использовать круглые скобки, окаймляющие список аргументов, даже если он пуст:
A var; int i; i = var.n; var.f ();
Еще одно важное отличие от (и отклонение от принципа скрытия информации) состоит в предоставлении клиентам возможности непосредственного присваивания значений полям класса:
var.n = 5;
Нужно ли говорить, что это не рекомендуется, - используйте сеттер-процедуры для этого. Специальные члены функции, называемые конструкторами, служат для создания объектов. Вот пример конструктора:
class A{
int n;
// Следующие две строки определяют конструктор
A (int i)
{n = i;}
…
};
Конструктор играет ту же роль, что и процедура создания в , но есть важные различия. Как показывает пример, конструкторы не имеют своих собственных имен, а используют имя класса, здесь - A. Появляющееся в связи с этим ограничение требует, чтобы конструкторы отличались по сигнатуре (перегрузка подробнее будет изучаться ниже). Для инициализации переменной, тип которой задан классом, необходим вызов конструктора:
A var = A(2); A another_var(2);
Здесь вторая форма является сокращением первой. Класс может иметь конструктор по умолчанию без аргументов (вспомните аналог, процедуру создания default_create из класса ANY в , позволяющую опускать явную инициализацию):
A var; // Синоним для A var = A ();
Если не задать в классе ни одного конструктора, то автоматически будет добавлен конструктор по умолчанию, который всем данным класса даст значения по умолчанию. Если, однако, какой-либо член класса принадлежит типу, у которого нет конструктора по умолчанию, то необходимо явно задать, по меньшей мере, один конструктор.
Конструктор предполагает, что первым делом все члены-переменные получают значения по умолчанию. Эти значения определены в языке для всех встроенных типов, а для классов задаются конструктором по умолчанию, если он присутствует. В его отсутствие необходимо обеспечить значение по умолчанию через список, инициализирующий члены, который может также включать любые другие члены данных, как в примере:
class B {
int value;
Person a_friend;
B (int v, string name) : value (v), a_friend (name) {}
// Может также быть записано в виде:B (int v, string name) : a_friend
(name)
// {value = v;}
};
Процесс конструирования объекта включает конструирование полей, используя список инициализации, если он поставляется, или конструктор по умолчанию. Порядок инициализации определяется порядком определения переменных в классе. После этого выполняется тело конструктора.
Для обозначения текущего объекта(Current в ) используется ключевое слово this, определенное как константный указатель.
Для практики C++ характерно определение const:
class A {
int n;
int n_squared () const
{return n_n; }
};
Такие функции не могут модифицировать текущий объект, присваивая значения членам переменным или вызывая неконстантные функции
В C++ можно определить для каждого члена класса один из трех модификаторов доступа: public, protected и private. Public-доступ означает полные права для клиентов и потомков (как с экспортом для ANY in ). Модификатор protected разрешает доступ потомкам. Модификатор private запрещает доступ и для потомков, оставляя доступ только для самого класса (в потомки всегда имеют доступ для неквалифицированного вызова). Эти правила на самом деле одинаковы для квалифицированных и неквалифицированных вызовов (так что здесь нет эквивалента, позволяющего сделать компонент полностью недоступным для квалифицированного вызова, экспортируя его NONE). Следующие примеры используют некоторые модификаторы доступа:
class A {
private:
int n;
float secret_function () { … }
protected:
float variable_for_descendants;
int n_squared () { … }
public:
string variable_for_everyone;
do_everything () { … }
};
На практике члены-переменные редко задаются с модификатором public, так как это делает их доступными для присваивания, а не только для чтения. Общая практика состоит в определении геттеров, мы видели, почему в этом нет необходимости в .
Модификатором по умолчанию для членов класса является модификатор private. Для структур, однако, таковым является public.
Эти приемы не поддерживают в полной мере понятие "выборочного экспорта" (feature {A, B, C} как в , который позволяет экспортировать компонент классам, перечисленным в списке, и их потомкам). В C++ имеется, однако, близкий механизм "друзей" - класс может указать функцию или другой класс в качестве друга - friend:
class Linkable {
friend class Linked_list;
friend bool is_equal (const Linkable other);
…
};
Другу доступны все члены класса, включая private и protected (это означает, что уровень this и может получать доступ только квалифицированным путем.
Любая переменная, константа, функция или тип имеет область действия: локальную в блоке (часть программы, заключенная в фигурные скобки{…}), если там появилось ее объявление, в противном случае - глобальную (расширенную на весь модуль трансляции).
Как следствие, нет специального синтаксиса для локальных переменных, которые просто определяются где-либо в блоке, создаваемом в теле функции. Считается хорошей практикой для C++ определять локальные переменные как можно ближе к точке их первого использования.
Блоки могут быть вложенными. Объявление внутреннего блока скрывает элемент, определенный с тем же именем во внешнем блоке или глобальный:
int x; // глобальная переменная x
void f ()
{x = 1; // Присваивание глобальной x
int x; // Определение локальной переменной x
x = 2; // Присваивание локальной x
}
int y = x; // используется глобальная x
Скрытый элемент может быть все же доступен во внутреннем блоке, используя :: -нотацию - операцию разрешения области:
int x; // Глобальный x
class A {
int x; // Член x
void f ()
{int x; // Локальный x
int y = A::x; // Использование члена x
int z = ::x; // Использование глобального x
}
};
Эти свойства чреваты ошибками - предпочтительнее для переменных вложенных областей выбирать различные имена.
Как в , можно определять функции, вызываемые в стиле синтаксиса операций:
class Complex {
…
Complex operator+ (const Complex other) const {…}
Complex operator- (const Complex other) const {…}
Complex operator* (const Complex other) const {…}
Complex operator/ (const Complex other) const {…}
};
В отличие от , операции не имеют эквивалентного имени, заданного идентификатором, и ограничены фиксированным множеством предопределённых операций (таких как + и другие знаки), чьи синтаксические свойства - число аргументов, инфиксные или префиксные, лево- или правоассоциативные, приоритет - не могут быть изменены.
В C++ разрешается в одной и той же области объявлять несколько функций с одним и тем же именем, если они отличаются по сигнатуре:
void print (int n) {…}
void print (string s) {…}
…
print (5); // Использует функцию print печати целых
print ("hello"); // Использует функцию print печати строк
Этот механизм называется перегрузкой функций. Он часто используется, например, для операций (
В чистом ОО-каркасе все элементы программы являются относительными (по отношению к текущему объекту, мы называем это "общей относительностью"). В C++ добавляется механизм, описывающий члены-переменные и функции как статические. Такие члены принадлежат классу, но могут применяться независимо от его экземпляров (в подобный эффект имеет место для однократных - once-методов, не используемых в этой книге, но описанных в стандарте языка).
В отличие от обычных членов переменных, которые представляют поля каждого экземпляра класса, статические члены-переменные представляют данные уровня самого класса. Например, статический член данных может применяться для подсчета числа вызовов некоторой функции класса:
class Rocket_launcher {
…
static int rocket_count;
static const int max_count = 100;
void launch () // Запуск ракеты
{
…
rocket_count++; // Увеличение на 1 при очередном запуске
}
};
Статические члены функции оперируют только со статическими членами переменных и константами, как в
class Rocket_launcher {
… Rest of class as above …
static bool is_in_bounds ()
{return rocket_count <= max_count;}
};
Вызовы статических членов не требуют целевого объекта. Вместо
r = Rocket_launcher::rocket_count; m = Rocket_launcher::max_count; if (Rocket_launcher::is_in_bounds ()) …
Совсем другое по семантике использование ключевого слова static применяется при описании локальных переменных функции. Если локальная переменная объявляется как статическая, то сохраняется ее значение, полученное при предыдущем вызове функции, как показано в данном примере:
void f ()
{ static int invocation_count = 0; // локальная статическая
…
invocation_count++; // Увеличивается на 1 при каждом вызове
}
В C++ есть дифференциация объектов на тех, что автоматически управляются системой, и тех, что находятся под прямым контролем программиста.
Объекты, управляемые программистом, называются также new с соответствующим конструктором класса:
Complex* c; … c = new Complex (1.0, 2.0);
Вычисление выражения new создает указатель на объект типа Complex, который и присваивается соответствующей переменной, - эффект тот же, что и в при вызове процедуры создания create c.make (1.0, 2.0) с типом COMPLEX.
В отличие от не проектировался для delete:
delete c;
Для тех же самых целей C обеспечивает библиотечную функцию, называемую free. Если после выполнения этой операции некоторые указатели или ссылки будут все еще ссылаться на объект, который до этого был присоединен к c, то они станут висячими указателями: разыменование такого указателя означает попытку доступа к несуществующему объекту и приведет к ошибке периода выполнения или, что еще хуже, выработке произвольного значения. Но, с другой стороны, если не удалять недостижимые объекты, это может стать причиной исчерпания памяти.
В то время как динамические объекты всегда доступны через указатели - с возможностью нескольких указателей быть присоединенными к одному и тому же объекту, - нединамические объекты (которые все же могут иметь "вторичные" указатели и ссылки, присоединенные к ним) связываются с единственной переменной или константой, чья область и определяет время жизни объекта. Нединамические объекты могут быть двух видов.
main и существуют до конца выполнения системы.Время жизни полей объекта (соответствующих нестатическим членам-T в C++ может определить для этих целей специальный член-функцию - деструктор, со стандартно построенным именем ~T, который будет вызываться при разрушении объекта. Вот пример типичного деструктора:
class Person1 {
…
Passport* pp;
Person1 (string n, date d)// Конструктор, создающий объект Passport
{pp = new Passport (n, d); }
~Person1 () // Деструктор
{delete pp;} // Удаление объекта passport
};
Этот пример иллюстрирует общий C++ образец: инициализация как способ овладения ресурсом (Resource ) - использование конструктора для захвата ресурсов, необходимых объекту, и деструктора, для их освобождения. Это устраняет некоторые источники ошибок при уверенности, что операции удаления - delete - выполняются в правильном порядке. Систематическое применение RAII ограничивает использование динамической памяти специальными классами, часто библиотечными. В остальной части ПО экземпляры этих классов используются не динамически, ослабляя тем самым последствия отсутствия сборки мусора.
Образец RAII расширяется на ресурсы, иные, чем память, такие как файлы, сокеты (сетевые соединения) и блокировки (для многопоточности и других форм параллельного программирования), для которых освобождение ресурсов должно выполняться вручную, даже при наличии сборки мусора. Кроме того, RAII гарантирует, что в случае ненормального завершения (через исключения, обсуждаемые ниже) деструкторы будут вызваны подходящим образом. Эти преимущества приводят некоторые защитники, полагая, что RAII превосходит сборку мусора. Однако он остается ручным подходом, ограниченным специфическими образцами использования памяти.
В отличие от , автоматическая инициализация применима к
int n;
class Rocket_launcher {
static int rocket_count;
… Остальное как выше …
};
int Rocket_launcher::rocket_count;
Здесь как n, так и rocket_count инициализируются нулем (заметьте: необходимо включать второе объявление rocket_count, так как объявление статического члена переменной внутри класса не является определяющим объявлением).
Ссылки, константы и автоматические объекты необходимо инициализировать вручную.
Отсутствие инициализации автоматических объектов приводит к тому, что значение будет неопределенным, а это почти всегда становится причиной ошибки (и источником потенциальных нарушений безопасности). Так что следует тщательно проверять вручную, чтобы каждый автоматический объект имел подходящую инициализацию.
В C++ обработка исключений доступна через процесс исключительных событий, возникающих в период выполнения. Вместо стиля , основанного на принципе проектирования по контракту, используется стиль "try-catch".
Исключение, причиной которого, например, явилась ошибочная арифметическая операция (переполнение сверху или снизу - overflow или ), прервет нормальный поток выполнения. Блок кода со специальным синтаксисом, в котором контролируется возникновение исключительных ситуаций, называется try-блоком. Исключительная ситуация, возникшая в catch-блоке. Рассмотрим пример:
try {
… Код, который может включить исключение …
} catch (io_error e) {
… Обработка исключений ввода-вывода I/O …
} catch (memory_error e) {
… Обработка исключений, связанных с памятью …
}
Блок catch задает io_error, и имя объекта, задающего исключение, здесь e, используемое в операторах обработки (способом, подобным
Исключения можно также включать (говорят также - "выбрасывать"), применяя специальный оператор:
throw exp
Здесь exp является выражением. Хотя оно может быть любого типа, но на практике используются специальные библиотечные классы, спроектированные для описания исключений.
Любое исключение, встретившееся во время выполнения блока, прерывает выполнение этого блока (оставшиеся операторы не выполняются). После чего:
catch-блоков соответствует типу возникшего исключения, то выполнение будет передано в соответствующий catch-блок, затем будет выполняться следующая конструкция, если только сам catch-блок не выбросит повторно исключение - throw ();try-блока, текущая функция завершается, выбрасывая исключение в вызывателе - функции, вызвавшей функцию, в которой возникло исключение. И здесь рекурсивно применяется описанная схема.Если в цепочке вызовов не найдется соответствующий catch-блок, то выполнение закончится прерыванием начальной точки - функции main и, следовательно, завершением программы в состоянии, свидетельствующем об ошибке.
В этом RAII.
Для безопасной обработки исключений можно указать, как часть сигнатуры функции, множество throw - список исключений, которые могут быть выброшены при выполнении. Вот пример:
void read_and_store (string a_file_name) throw (Io_error, Memory_error)
{ … }
Отсутствие множества throw может означать, что функция может выбрасывать любое исключение (для указания, что она не может выбрасывать исключения, применяется конструкция throw ()). Систематическое включение множества throw в каждую функцию означает, что для данной функции это множество является надмножеством множеств throw для всех вызываемых функций. Это рекомендуемая дисциплина, помогающая избежать пропуски исключений. Но это трудная задача, поскольку библиотеки и существующий код, используемый новыми системами, может не следовать этой дисциплине.
Шаблоны являются C++-версией универсальности. Вот простой пример:
template <typename G> class Stack {
…
public:
G item () {…}
void push (G an_item) {…}
void pop () {…}
};
Здесь определен класс Stack с родовым параметром G, аналог класса STACK [G] в . Конкретный стек задается как:
Stack<int> s;
Главная разница между шаблонами C++ и ОО-языках, в том, что каждое родовое порождение, такое как выше, рассматривается как создание нового класса, - процесс, называемый конкретизацией шаблона.
Препроцессирование - преобразование текста программы до компиляции, используемое в данном случае, сохраняет мощь языка программирования и дает некоторые экзотические применения в продвинутом C++-программировании.
Здесь нет аналога понятия ограниченной универсальности: если применяется операция к переменной, тип которой задан формальным параметром, таким как G из примера выше, проверка типа будет применяться к каждому экземпляру, чтобы убедиться, что операция всегда правильна.
Шаблон класса позволяет задать полную или частичную специализацию. Полная специализация замораживает фактические параметры, как в этом примере, используя вышеприведенный Stack:
template<>
class Stack<bool> {
… Операции, специфические для булевских стеков …
};
Частичная спецификация оставляет формальные параметры, задавая некоторые ограничения:
template<typename G>
class Stack<G*> {
… Операции, специфические для стеков указателей …
}
Как показывают примеры, класс, полученный в результате специализации, может иметь собственные определения членов. Это не становится причиной конфликтов, так как компиляция всегда выбирает наиболее специализированную конкретную версию.
Помимо шаблонов классов, C++ поддерживает шаблоны функций:
template <typename G>
G max (G a, G b) { … Вычисление максимума ...}
При конкретизации функции можно опускать фактические параметры шаблона, если их возможно автоматически вывести из
int a, b, c; … c = max (a, b); // Вызов max<int>
Здесь автоматически порождается max с типом int для G.
Механизм шаблонов выходит за пределы универсальности, позволяя в качестве параметров шаблона задавать конкретные типы, такие как булевские или целые типы. Соответствующие фактические родовые параметры должны быть константами периода компиляции. В следующем примере используется эта возможность для определения умножения матриц с гарантией соответствия размеров:
template <int n, int m> class Matrix { … };
template <int n, int m, int k> Matrix<n, k> operator*
(const Matrix<n, m> m1, const Matrix<m, k> m2)
{… Алгоритм умножения матриц … }
Попытка умножить матрицы несовместимых (константы) размеров приведет к ошибке, обнаруживаемой на этапе компиляции.
Класс B можно определить как наследника (производный класс) класса A (базового класса для B):
class B : A {
…
};
Здесь нет эквивалента переименования и отмены определения, как в . Для переопределения наследуемой функции просто включите новое определение. Но следует быть внимательным - нужно сохранить исходную сигнатуру, так как в противном случае речь будет идти о перегрузке, и нет простого способа обнаружения таких ошибок.
По умолчанию наследуемые члены закрыты (private). Для изменения их статуса можно задать модификатор доступа - private, public или protected для
class B : public A {…};
При такой спецификации каждый наследуемый элемент получает статус доступа, представляющий минимум из двух статусов - оригинального и статуса наследования. В данном примере, где статус наследования самый высокий, все члены A сохранят свой статус доступа в классе B.
Для получения доступа к оригинальной версии переопределенной функции - эквивалент Precursor в - можно использовать операцию разрешения области, если только эта версия не является private:
class B : public A {
void b_function () // Не обязательно, чтобы это было переопределение r
{
A::r (); // Вызов метода из класса A
…
}};
Мы видели, что динамическое связывание является главным вкладом объектной технологии в архитектуру построения ПО. Основное различие между C++ и ОО-языками, такими как , в том, что связывание здесь по умолчанию статическое. Критерием, определяющим, какая версия функции будет вызываться для целевого объекта, является тип по объявлению, а не
class Rectangle {
…
virtual void draw() {…} // Следует объявить как виртуальную
virtual void rotate() {…}
};
class Rounded_rectangle : public Rectangle {
...
virtual void draw() {…} // Можно, но не обязательно объявлять виртуальной
//при переопределении
void rotate() {…} // … Эффект тот же - динамическое связывание
// будет применяться!)
};
Динамическое связывание применимо только для объектов, доступных через указатели или ссылки, как показано в следующем примере:
Rectangle r (1.2, 0.5); Rounded_rectangle rr (5.0, 3.2, 0.2); r = rr; // r все еще обычный прямоугольник с полями, скопированными из rr r.draw (); // Статическое связывание …: rectangle::draw() Rectangle*p = rr; p -> draw (); // Динамическое связывание…: rounded_rectangle::draw() Rectangle ref = rr; ref.draw (); // Динамическое связывание…): rounded_rectangle::draw()
Конструкторы, которые не могут применяться к существующему целевому объекту, не могут быть
Ближайшим эквивалентом отложенного метода является понятие чистой виртуальной функции, имеющей определение, но не реализацию:
class Figure {
virtual void draw() = 0;
… Другие члены класса …
};
Класс, у которого есть хотя бы одна чистая виртуальная функция, называется абстрактным, он подобен отложенному классу
В C++ поддерживается множественное наследование:
class Arrayed_stack : public Stack, private Array {…}
Механизм менее гибок, чем представленный в этой книге. В частности, не поддерживается переименование. Можно наследовать две функции с одним именем и устранять конфликт, используя операцию разрешения области:
class A {void f () {…}};
class B {void f () {…}};
class C : public A, public B {…};
void test()
{
C* p = new C();
// p->f(); Этот вызов неоднозначен и, следовательно, неправилен
p->A::f();
p->B::f();
}
Повторное наследование не позволяет выбирать между склеиванием и репликацией для каждого члена. Выбор делается глобально для класса в целом. Разрешение противоречий может требовать сложного использования области разрешения:
class D {int n;};
class E : public D{};
class F : public D {};
class G : public E, public F {};
void f()
{
G* p = new G();
// p->n = 0; // Это было бы неверным - какое n?
p->E::D::n = 0; // Правильно: присваивается версия из E
p->F::D::n = 0; // Правильно: присваивается версия из F
}
Если требуется, чтобы были присоединены общие поля предка вместо реплицированных, следует определить этого предка как
class T {int n;};
class U : public virtual T {};
class V : public virtual T {};
class W : public U, public V {};
void f()
{
W*p = new W;
p->n = 0; // Теперь правильно
}
Неудобство в том, что выбор делается не в точке использования дублирующего наследования, здесь, в классе W, но ранее, в классах U и V, которые могут ничего не знать о планах W наследовать от них обоих.
В C++ специфические правила создания экземпляров производных классов. Конструкторы не наследуются, но создание экземпляра производного класса становится причиной вызова конструктора родителя перед началом работы собственного конструктора (процесс рекурсивный). При вызове конструктора родителя ему могут быть переданы необходимые ему аргументы, как в следующем примере:
class Rounded_rectangle : public Rectangle {
public:
Rounded_rectangle (float w, float h, float r) : Rectangle (w, h), radius (r)
{…}
… Остальная часть класса как ранее …
};
В больших программных системах трудно избежать появления конфликта имен: система может использовать библиотеки, включающие классы с одинаковыми именами. 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 " даст ссылки на многие из этих инструментальных средств, чье использование остается ограниченным, так как они не интегрированы в язык.
Как отмечалось, C++ не имеет механизма агентов. Простой эффект вызова переменной, задающей функцию, может быть достигнут (как мы видели) через указатели функций. Более сложное решение использует понятие ()" - операции вызова функции. Объект "
Мы видели, что в C++ нет прямого соответствия ограниченной универсальности и что каждая конкретизация шаблона осуществляет собственную проверку типов. Это означает невозможность информировать клиентов, что родовой параметр представляет потомка определенного типа. Они узнают об этом, только нарушив это требование, когда конкретизация шаблона не будет компилироваться.
Методологическое правило - ограничение универсальности необходимо задавать неформально через комментарии в определении шаблона:
template <typename G> /* G должен быть потомком Comparable */
G max (G a, G b) { … }
В C++ нет эквивалента класса, представляющего вершину в ANY в , нет и класса - аналога NONE.
Мы уже встречались с несколькими свойствами C++, не доступными в . Сейчас рассмотрим две другие особенности: аргументы по умолчанию и вложенные классы.
Для формального аргумента можно задать значение по умолчанию, позволяя при вызове опускать задание соответствующего аргумента. Если не все формальные аргументы имеют значения по умолчанию, то аргументы, имеющие значения по умолчанию, задаются после них:
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++ библиотека покрывает фундаментальные структуры данных, в частности, контейнеры, требующие универсальности, так что большинство ее классов представляют шаблоны (отсюда название). В этой же библиотеке размещаются классы ввода-вывода и исключений.
Для ввода-вывода использует потоки, которые могут представлять окружение, такое как консоль (стандартные потоки 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. Таких схем лучше избегать.
Блоки соответствуют и состоят из списка операторов в фигурных скобках. Блоки, как мы видели, могут быть вложенными. Условный оператор имеет форму:
if (expression) statement else statement
Заметьте, выражение условия - expression - должно быть в круглых скобках. Оно не обязано быть 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, если она задана, в противном случае ничего не делается (в в отсутствие ветви else в операторе в подобной ситуации в период выполнения генерируется ошибка). 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 - обеспечить продвижение к следующей операции (в эта часть включается в тело цикла). Приведем пример цикла в :
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++ является любая последовательность букв и цифр, начинающаяся с буквы (подчеркивание относится к буквам, по соглашению, идентификаторы, начинающиеся с подчеркивания, резервируются для специальных переменных, управляющих компиляцией).
В отличие от , идентификаторы C++ чувствительны к регистру.
Здесь нет стандартных соглашений по наименованию, так что можно использовать соглашения этой книги или другие правила стиля. Заметьте, однако, что в имена классов заданы в нижнем регистре.
Литералы (манифестные константы) могут представлять целые, символы, строки и числа с плавающей точкой.
Целая константа может быть десятичной, восьмеричной, начинающейся цифрой 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.
Общепринято характеризовать ОО-языки как "чистые" (полностью и исключительно реализующие ОО-концепции) либо как "гибридные" (представляющие смесь объектных и необъектных свойств). Наиболее известным представителем гибридных языков является язык С++, который создавался с целью предоставления С-программистам некоторых ОО-идей и механизмов. Язык С++ по духу отличается от нотации , принятой в остальной части книги, и более сложен.
Можно найти много книг и вводных статей о С++, как начинающихся с чистого листа, так и предполагающих знание языка С. Роль этого приложения другая: она ориентирована именно на вас, внимательных читателей этой книги, кто прочел уже сотни страниц и овладел ОО-программированием в его "чистой" форме. Цель в том, чтобы в случае необходимости программирования на С++ применять его конструкции в духе . По этой причине многие конструкции C++ будут объясняться - как это сделано для Java и C# - в стиле: "Вот как это можно сделать на и как получить такой же эффект на С++".
Язык С, на котором основан С++, сам является важным языком, кратко рассматриваемым в следующем приложении.
Сегодня идея использования ОО-языка едва ли может удивить кого-либо, но в конце восьмидесятых годов она воспринималась как насмешка, - многим программистам и менеджерам в индустрии и академических кругах ОО-концепции казались привлекательными, но возникали большие сомнения в их применимости. В 1979 году Бьёрн Страструп из Bell
(рис 13.1) Бьёрн Страуструп (2007)
Laboratories спроектировал и реализовал язык, изначально названный "С с классами", расширяющий язык С концепциями, которые были заимствованы у языка , - первого ОО-языка. Код транслировался в чистый С препроцессором. Язык вскоре стал хитом и, как было обещано, облегчил переходный период для С-программистов.
В следующие два десятилетия язык интенсивно развивался за счет введения таких конструкций, как шаблоны (форма универсальности) и множественное наследование.
Для ОО-подхода программа представляет множество классов. В С++, сохраняющем гибридный дух, не настаивают на соблюдении этого правила. Программа может включать классы, но и другие элементы, не входящие в классы: функции (соответствующие методам, реализуемым либо в виде настоящих функций, либо в виде процедур), переменные, константы и типы, не заданные классами.
Примером независимой функции, появляющейся в каждой исполняемой программе, является функция, называемая main, которая определяет точку входа в программу. В эту роль играет корневая процедура создания.
Программная система на С++ представляет скорее не множество классов, а множество единиц трансляции (юнитов или модулей), каждая из которых содержится в отдельном файле, который независимо может быть обработан С++ компилятором. Каждая единица трансляции может содержать объявления классов и других типов, функций, переменных и констант.
Объявление такого элемента может быть его определением, означающим его полное описание, либо может объявлением, не задающим определение, таким как:
class Person; enum Week_day;
При этом понимается, что полное определение этого элемента (здесь класс и тип) появится либо в другом модуле, либо в той же единице, но позже. Это дает возможность использовать элемент без знания его детальных свойств. Для переменной или константы неопределяющее объявление использует ключевое слово extern, указывающее, что определение еще появится:
extern bool has_error; extern const double pi;
Определение функции включает имя, сигнатуру и реализацию:
int factorial (int n)
{
if (n > 1) {
return n _ factorial (n - 1);
} else {
return 1;
}
}
Определение класса содержит список членов класса (компонентов):
class Person { // Объекты, представляющие персон
string name; // Имя персоны
Date birth_date; // Дата рождения
void set_name (string s)
{name = s;}
void set_birth_date (Date d)
{birth_date = d;}
… // Другие члены класса
};
В определении переменной задается ее тип, предшествующий переменной в отличие от , где действует соглашение variable_name: TYPE.
int n; bool has_error = false;
Второе из этих объявлений содержит инициализацию переменной, устанавливающую ее начальное значение. Определения не содержат extern: это означает, что переменные не являются внешними, а значит, могут использоваться только в данном модуле трансляции.
Определение переменной означает, что ей будет отведена память, как для .
Определение константы всегда включает ее инициализацию:
const double Pi = 3.14159265358
В отличие от порядок объявлений существенен: имена, объявленные в модуле трансляции, можно использовать только после точки объявления. Для применения элемента до его определения - такая необходимость возникает, например, в случае множества взаимно рекурсивных определений - предварительно следует задать неопределяющее объявление.
Во избежание трансляции слишком большого модуля можно разделить его на несколько файлов, используя затем директиву включения #include, как в примере:
#include "filename"
Как результат, все определения, содержащиеся в исходном файле filename, будут доступны текущему модулю трансляции. Чаще всего это свойство предполагает использование заголовочных файлов (по соглашению имеющих имена в форме name.h), которые содержат объявления элементов, используемых многими модулями, и включают соответствующую директиву #include.
Директива #include, подобно любым другим предложениям, начинающимся с символа решетки #, адресована препроцессору С++ - инструментарию, обрабатывающему файлы программы до начала компиляции. Другое использование препроцессора связано с
#ifdef LINUX
… Linux-specific code …
#endif
Код для платформы Linux будет подключен в зависимости от LINUX-переменной. Такие переменные, не связанные с переменными программы, рассматриваются как опции, действующие на этапе работы препроцессора и компилятора.
Подобно , каждая C++ переменная имеет тип, но, в отличие от , не все типы основаны на классах, и следовательно, не все значения являются объектами. Тип может быть встроенным, производным (с возможными комбинациями механизмов порождения), определенным пользователем.
Встроенные типы предустановлены в языке. Они включают bool - для булевских значений; char, short int, int, long int - для целых из разных диапазонов; float, double, long double - для вещественных с плавающей точкой. Тип char также служит для представления символов.
Каждый целочисленный тип включает две версии - со знаком и без знака, такие как short int и unsigned short int. Версии без знака включают только положительные значения. По умолчанию все целые типы, за исключением char, являются знаковыми, делая избыточным задание такого типа, как signed int. Является ли char типом со знаком, определяется платформой.
Встроенный тип void не имеет значений. Он служит признаком процедур - функций, не возвращающих результат:
void set_name (string s);
T.
Тип const T представляет неизменяемое значение типа T. Например, n будет иметь тип const int, если ее определить как:
const int n = 5;
Разрешается переменной типа T присвоить значение типа const T, но обратное преобразование недопустимо.
Указателем на тип Т является тип T*. Значения этого типа обозначают адреса памяти, где хранится переменная типа T. Для получения указателя на переменную x используется , как в x. Возможно обратное преобразование, называемое разыменованием - получением значения по адресу. Если p - указатель, то *p дает значение, хранящееся в области памяти, на которую указывает указатель. Если значением является объект, то возможен доступ к его полям, например, полю f, используя нотацию с точкой: (*p).f. Специальный синтаксис p->f является синонимом нотации с точкой.
Поскольку указатель представляет адрес памяти, в языке разрешено добавление или вычитание целого из указателя для получения нового адреса, как в*(p + n) . Смысл его в следующем. Представим, что в памяти подряд хранятся n + 1 значение типа T, каждое занимает фиксированное число байтов, которое можно получить, используя конструкцию sizeof T. Тогда, если p типа T* и указывает на первое хранимое значение, то *(p + n) возвращает последнее значение (здесь неявно n умножается на число байтов, отводимых элементу - sizeof T, так что (p + n) дает адрес начала соответствующего элемента, а операция * - значение по этому адресу). Это все называется С для получения доступа к нужным участкам памяти. Механизм мощный, но чреватый ошибками (трудно гарантировать, что на самом деле хранится в динамически вычи
сляемом адресе). Без необходимости его не следует применять в приложениях С++.
Все типы указателей согласуются со специальным типом void*, напоминающем класс ANY в . Но это встроенное соответствие, не индуцированное наследованием. Поскольку нельзя иметь переменные типа void, нельзя проводить разыменование указателей void*, никакие операции над ними не выполняются. Чтобы их использовать, необходим кастинг - явное C++ есть разные способы приведения, дающие разные результаты в случае, когда
Тип T является "ссылкой на T". Подобно указателю, ссылка является адресом, но при любом использовании происходит автоматическое разыменование. Ссылки являются наиболее прямым способом получения эффекта обычного (ссылочного) класса в . Рассмотрим класс , и пусть в этом классе определен некоторый метод:
call_her_izzy (p: PERSON)
do p.set_name ("izzy") end
Эквивалентом метода в С++ будет:
void call_her_izzy (Person p)
{p.set_name ("izzy");}
Рассмотрим вызов этого метода call_her_izzy (Isabelle) , где Isabelle типа Person. Эффект состоит в изменении значения поля имени ссылочного объекта. Если бы аргумент в C++ версии имел тип Person, то вызов создавал бы копию объекта Person.
Метод работал бы на этой копии, но без видимого эффекта, так как копия локальна и исчезла бы по завершении выполнения метода.
Реализовать ссылочное поведение можно и с указателями, но сложнее - с явной адресацией и разыменованием:
void call_her_izzy (Person* p)
{p->set_name ("izzy"); // или (_p).set_name ("izzy");
}
В этом случае вызов метода имеет вид: call_her_izzy (Isabelle) . Использование указателей считается хорошим стилем в сравнении с передачей аргументов по ссылке, поскольку делает явной ссылочную семантику.
Еще одна разница между указателями и ссылками в том, что ссылки требуют инициализации, присоединяющей значение типа T к T ссылке, в то время как указатели могут иметь нулевое значение (называемое также null и соответствующее void в ). Однако это не обеспечивает преимущества присоединенных типов , так как возможно присвоить указатель T* переменной типа T или T, что станет причиной ошибки периода выполнения, если указатель равен null.
В целом ссылочные типы обеспечивают более строгую дисциплину, в частности, они не позволяют
Теперь поговорим о массивах. Тип, задающий массив (иногда говорят "массивный тип"), - T[size] , где размер size является целой константой, известной во время компиляции, представляет последовательность значений типа T, хранимой в подряд идущих словах памяти. Если ar - это массив, то ar[i] обозначает i-й элемент этого массива, где i - выражение целого типа. В C++ массив рассматривается как адрес начала расположения элементов массива в предположении, что памяти для их хранения достаточно. Для безопасной работы с индексами, гарантирующей, что выход за границы контролируется, следует использовать библиотечные классы.
Наконец, функциональный тип. Он фактически является указателем на тип функции. Пусть R, A1, …, An являются типами (последовательность задает сигнатуру функции). Рассмотрим объявление:
R (*f) (A1, …, An);
Здесь объявлена переменная f, чьи значения являются указателями на функции, возвращающие значение типа R и имеющие n аргументов типа A1, … An. Например, рассмотрим объявление:
void (*f) (Person*)
Переменной f можно присвоить указатель на функцию:
f = call_her_izzy; //или f = call_her_izzy;
После этого можно выполнять непрямой вызов:
f (Isabelle); // или (*f) (Isabelle);
Эффект будет тот же, что и при прямом вызове call_her_izzy (Isabelle) . Разница в том, что f - это переменная, которой можно присвоить указатели на разные функции (с заданной сигнатурой).
Указатели функций могут обозначать не только независимые функции, но и функции, представляющие членов класса, как в примере:
void (Person::*p) (string) … p = Person::set_name;
Здесь объявляется p и ему присваивается указатель на функцию класса Person, принимающий один аргумент типа string. Эту функцию можно вызывать, используя следующий синтаксис:
(Isabelle.*p) ("Izzy");
Как вы уже понимаете, указатели на функции близко связаны с двумя ОО-механизмами, основанными на способности вызывать метод, оставляя на момент выполнения определение того, какой именно метод будет вызван. Этими механизмами являются динамическое связывание и агенты. Указатели функций C++ дают возможность эмуляции этих свойств.
C++ предоставляет механизм виртуальных функций, изучаемый ниже, который реализует динамическое связывание. Поскольку в чистом С виртуальных функций нет, у них остается только одна возможность - применение указателей функций. Вот почему компиляторы Eiffel , транслирующие программу в код на C, основываются на объясненной ранее схеме.Пять механизмов построения
Можно комбинировать модификаторы const и pointer, чтобы получить:
Подобным образом можно комбинировать const и ссылки. Следующие примеры иллюстрируют некоторые из возможностей:
const int* pointer_to_const; const int reference_to_const; int* const const_pointer; int const const_reference;
На практике важной является нотация typedef, позволяющая именовать создаваемые типы. Это позволяет ссылаться на сложный производный тип, используя его простое и понятное имя:
typedef const Person* Cp; typedef void (* Pf) (Person*);
Теперь возможно использовать в объявлениях Cp p вместо const Person *p. Тип Pf обозначает теперь соответствующий тип указателя на функцию.
В добавление к ниже изучаемым классам, типы, определенные пользователем, включают перечисления, представляющие обычно небольшое множество фиксированных значений:
enum Week_day {Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday};
enum Error {Division_by_zero, Null_pointer_dereference, File_error, Memory_error};
Внутренне значения перечисления являются целыми константами. Тип перечисления неявно отображается в целочисленный тип, но обратное неверно, поскольку значения переменной перечисления находится в определенных границах.
Еще одним видом типа, определяемого пользователем, является тип, задающий структуры, обычно называемый просто "struct" и представляющий некоторую форму класса. Классы изучаются ниже. У структур в основном те же свойства, что и у классов, но другая политика экспорта.
Наконец, тип "объединение" описывает объекты, которые могут быть нескольких видов, занимая одну и ту же область памяти. Вместо ключевых слов class или struct для этого типа применяется термин union. Этот механизм, перешедший от C, где он был предназначен для оптимизации памяти, не является типо-безопасным, так как при использовании p.a нельзя гарантировать, что p является правильным вариантом, имеющим атрибут a. По этой причине программисты С++ редко используют тип объединения, основываясь вместо этого на наследовании, как в других ОО-языках, для поддержки вариантов общего типа.
Класс C++ задается следующим синтаксисом:
class A{
…// список членов класса
};
Определение может появиться в любом месте модуля трансляции, где допускаются определения, включая появление в объявлении другого класса и даже внутри тела функции, хотя такая вложенность - нечто экзотическое. Предпочтительно, но не требуется, включать определение класса в отдельный файл, как это вы бы сделали в .
Класс можно использовать как тип при объявлении переменных и для конструирования других производных и определенных пользователем типов:
A var; // Переменная типа A const A const_var; // Константа типа A A* p; // Указатель на объекты типа A
В C++ члены класса разделяются на члены-переменные, также называемые членами данных (соответствующие атрибутам ), и члены-функции, называемые также методами.
Определение класса может содержать как определения, так и неопределяющие объявления членов. В последнем случае эти члены должны быть определены вне класса.
class A{
int n; // член-переменная
void f () // член-функция, определенная в классе
{…}
void g (); // член-функция, определенная вне класса
};
void A::g ()//Определение g
{…}
В C++ не применяется унифицированный принцип доступа - доступ к переменной и функции без аргументов рассматриваются как различные операции. Для функций всегда следует использовать круглые скобки, окаймляющие список аргументов, даже если он пуст:
A var; int i; i = var.n; var.f ();
Еще одно важное отличие от (и отклонение от принципа скрытия информации) состоит в предоставлении клиентам возможности непосредственного присваивания значений полям класса:
var.n = 5;
Нужно ли говорить, что это не рекомендуется, - используйте сеттер-процедуры для этого. Специальные члены функции, называемые конструкторами, служат для создания объектов. Вот пример конструктора:
class A{
int n;
// Следующие две строки определяют конструктор
A (int i)
{n = i;}
…
};
Конструктор играет ту же роль, что и процедура создания в , но есть важные различия. Как показывает пример, конструкторы не имеют своих собственных имен, а используют имя класса, здесь - A. Появляющееся в связи с этим ограничение требует, чтобы конструкторы отличались по сигнатуре (перегрузка подробнее будет изучаться ниже). Для инициализации переменной, тип которой задан классом, необходим вызов конструктора:
A var = A(2); A another_var(2);
Здесь вторая форма является сокращением первой. Класс может иметь конструктор по умолчанию без аргументов (вспомните аналог, процедуру создания default_create из класса ANY в , позволяющую опускать явную инициализацию):
A var; // Синоним для A var = A ();
Если не задать в классе ни одного конструктора, то автоматически будет добавлен конструктор по умолчанию, который всем данным класса даст значения по умолчанию. Если, однако, какой-либо член класса принадлежит типу, у которого нет конструктора по умолчанию, то необходимо явно задать, по меньшей мере, один конструктор.
Конструктор предполагает, что первым делом все члены-переменные получают значения по умолчанию. Эти значения определены в языке для всех встроенных типов, а для классов задаются конструктором по умолчанию, если он присутствует. В его отсутствие необходимо обеспечить значение по умолчанию через список, инициализирующий члены, который может также включать любые другие члены данных, как в примере:
class B {
int value;
Person a_friend;
B (int v, string name) : value (v), a_friend (name) {}
// Может также быть записано в виде:B (int v, string name) : a_friend
(name)
// {value = v;}
};
Процесс конструирования объекта включает конструирование полей, используя список инициализации, если он поставляется, или конструктор по умолчанию. Порядок инициализации определяется порядком определения переменных в классе. После этого выполняется тело конструктора.
Для обозначения текущего объекта(Current в ) используется ключевое слово this, определенное как константный указатель.
Для практики C++ характерно определение const:
class A {
int n;
int n_squared () const
{return n_n; }
};
Такие функции не могут модифицировать текущий объект, присваивая значения членам переменным или вызывая неконстантные функции
В C++ можно определить для каждого члена класса один из трех модификаторов доступа: public, protected и private. Public-доступ означает полные права для клиентов и потомков (как с экспортом для ANY in ). Модификатор protected разрешает доступ потомкам. Модификатор private запрещает доступ и для потомков, оставляя доступ только для самого класса (в потомки всегда имеют доступ для неквалифицированного вызова). Эти правила на самом деле одинаковы для квалифицированных и неквалифицированных вызовов (так что здесь нет эквивалента, позволяющего сделать компонент полностью недоступным для квалифицированного вызова, экспортируя его NONE). Следующие примеры используют некоторые модификаторы доступа:
class A {
private:
int n;
float secret_function () { … }
protected:
float variable_for_descendants;
int n_squared () { … }
public:
string variable_for_everyone;
do_everything () { … }
};
На практике члены-переменные редко задаются с модификатором public, так как это делает их доступными для присваивания, а не только для чтения. Общая практика состоит в определении геттеров, мы видели, почему в этом нет необходимости в .
Модификатором по умолчанию для членов класса является модификатор private. Для структур, однако, таковым является public.
Эти приемы не поддерживают в полной мере понятие "выборочного экспорта" (feature {A, B, C} как в , который позволяет экспортировать компонент классам, перечисленным в списке, и их потомкам). В C++ имеется, однако, близкий механизм "друзей" - класс может указать функцию или другой класс в качестве друга - friend:
class Linkable {
friend class Linked_list;
friend bool is_equal (const Linkable other);
…
};
Другу доступны все члены класса, включая private и protected (это означает, что уровень this и может получать доступ только квалифицированным путем.
Любая переменная, константа, функция или тип имеет область действия: локальную в блоке (часть программы, заключенная в фигурные скобки{…}), если там появилось ее объявление, в противном случае - глобальную (расширенную на весь модуль трансляции).
Как следствие, нет специального синтаксиса для локальных переменных, которые просто определяются где-либо в блоке, создаваемом в теле функции. Считается хорошей практикой для C++ определять локальные переменные как можно ближе к точке их первого использования.
Блоки могут быть вложенными. Объявление внутреннего блока скрывает элемент, определенный с тем же именем во внешнем блоке или глобальный:
int x; // глобальная переменная x
void f ()
{x = 1; // Присваивание глобальной x
int x; // Определение локальной переменной x
x = 2; // Присваивание локальной x
}
int y = x; // используется глобальная x
Скрытый элемент может быть все же доступен во внутреннем блоке, используя :: -нотацию - операцию разрешения области:
int x; // Глобальный x
class A {
int x; // Член x
void f ()
{int x; // Локальный x
int y = A::x; // Использование члена x
int z = ::x; // Использование глобального x
}
};
Эти свойства чреваты ошибками - предпочтительнее для переменных вложенных областей выбирать различные имена.
Как в , можно определять функции, вызываемые в стиле синтаксиса операций:
class Complex {
…
Complex operator+ (const Complex other) const {…}
Complex operator- (const Complex other) const {…}
Complex operator* (const Complex other) const {…}
Complex operator/ (const Complex other) const {…}
};
В отличие от , операции не имеют эквивалентного имени, заданного идентификатором, и ограничены фиксированным множеством предопределённых операций (таких как + и другие знаки), чьи синтаксические свойства - число аргументов, инфиксные или префиксные, лево- или правоассоциативные, приоритет - не могут быть изменены.
В C++ разрешается в одной и той же области объявлять несколько функций с одним и тем же именем, если они отличаются по сигнатуре:
void print (int n) {…}
void print (string s) {…}
…
print (5); // Использует функцию print печати целых
print ("hello"); // Использует функцию print печати строк
Этот механизм называется перегрузкой функций. Он часто используется, например, для операций (
В чистом ОО-каркасе все элементы программы являются относительными (по отношению к текущему объекту, мы называем это "общей относительностью"). В C++ добавляется механизм, описывающий члены-переменные и функции как статические. Такие члены принадлежат классу, но могут применяться независимо от его экземпляров (в подобный эффект имеет место для однократных - once-методов, не используемых в этой книге, но описанных в стандарте языка).
В отличие от обычных членов переменных, которые представляют поля каждого экземпляра класса, статические члены-переменные представляют данные уровня самого класса. Например, статический член данных может применяться для подсчета числа вызовов некоторой функции класса:
class Rocket_launcher {
…
static int rocket_count;
static const int max_count = 100;
void launch () // Запуск ракеты
{
…
rocket_count++; // Увеличение на 1 при очередном запуске
}
};
Статические члены функции оперируют только со статическими членами переменных и константами, как в
class Rocket_launcher {
… Rest of class as above …
static bool is_in_bounds ()
{return rocket_count <= max_count;}
};
Вызовы статических членов не требуют целевого объекта. Вместо
r = Rocket_launcher::rocket_count; m = Rocket_launcher::max_count; if (Rocket_launcher::is_in_bounds ()) …
Совсем другое по семантике использование ключевого слова static применяется при описании локальных переменных функции. Если локальная переменная объявляется как статическая, то сохраняется ее значение, полученное при предыдущем вызове функции, как показано в данном примере:
void f ()
{ static int invocation_count = 0; // локальная статическая
…
invocation_count++; // Увеличивается на 1 при каждом вызове
}
В C++ есть дифференциация объектов на тех, что автоматически управляются системой, и тех, что находятся под прямым контролем программиста.
Объекты, управляемые программистом, называются также new с соответствующим конструктором класса:
Complex* c; … c = new Complex (1.0, 2.0);
Вычисление выражения new создает указатель на объект типа Complex, который и присваивается соответствующей переменной, - эффект тот же, что и в при вызове процедуры создания create c.make (1.0, 2.0) с типом COMPLEX.
В отличие от не проектировался для delete:
delete c;
Для тех же самых целей C обеспечивает библиотечную функцию, называемую free. Если после выполнения этой операции некоторые указатели или ссылки будут все еще ссылаться на объект, который до этого был присоединен к c, то они станут висячими указателями: разыменование такого указателя означает попытку доступа к несуществующему объекту и приведет к ошибке периода выполнения или, что еще хуже, выработке произвольного значения. Но, с другой стороны, если не удалять недостижимые объекты, это может стать причиной исчерпания памяти.
В то время как динамические объекты всегда доступны через указатели - с возможностью нескольких указателей быть присоединенными к одному и тому же объекту, - нединамические объекты (которые все же могут иметь "вторичные" указатели и ссылки, присоединенные к ним) связываются с единственной переменной или константой, чья область и определяет время жизни объекта. Нединамические объекты могут быть двух видов.
main и существуют до конца выполнения системы.Время жизни полей объекта (соответствующих нестатическим членам-T в C++ может определить для этих целей специальный член-функцию - деструктор, со стандартно построенным именем ~T, который будет вызываться при разрушении объекта. Вот пример типичного деструктора:
class Person1 {
…
Passport* pp;
Person1 (string n, date d)// Конструктор, создающий объект Passport
{pp = new Passport (n, d); }
~Person1 () // Деструктор
{delete pp;} // Удаление объекта passport
};
Этот пример иллюстрирует общий C++ образец: инициализация как способ овладения ресурсом (Resource ) - использование конструктора для захвата ресурсов, необходимых объекту, и деструктора, для их освобождения. Это устраняет некоторые источники ошибок при уверенности, что операции удаления - delete - выполняются в правильном порядке. Систематическое применение RAII ограничивает использование динамической памяти специальными классами, часто библиотечными. В остальной части ПО экземпляры этих классов используются не динамически, ослабляя тем самым последствия отсутствия сборки мусора.
Образец RAII расширяется на ресурсы, иные, чем память, такие как файлы, сокеты (сетевые соединения) и блокировки (для многопоточности и других форм параллельного программирования), для которых освобождение ресурсов должно выполняться вручную, даже при наличии сборки мусора. Кроме того, RAII гарантирует, что в случае ненормального завершения (через исключения, обсуждаемые ниже) деструкторы будут вызваны подходящим образом. Эти преимущества приводят некоторые защитники, полагая, что RAII превосходит сборку мусора. Однако он остается ручным подходом, ограниченным специфическими образцами использования памяти.
В отличие от , автоматическая инициализация применима к
int n;
class Rocket_launcher {
static int rocket_count;
… Остальное как выше …
};
int Rocket_launcher::rocket_count;
Здесь как n, так и rocket_count инициализируются нулем (заметьте: необходимо включать второе объявление rocket_count, так как объявление статического члена переменной внутри класса не является определяющим объявлением).
Ссылки, константы и автоматические объекты необходимо инициализировать вручную.
Отсутствие инициализации автоматических объектов приводит к тому, что значение будет неопределенным, а это почти всегда становится причиной ошибки (и источником потенциальных нарушений безопасности). Так что следует тщательно проверять вручную, чтобы каждый автоматический объект имел подходящую инициализацию.
В C++ обработка исключений доступна через процесс исключительных событий, возникающих в период выполнения. Вместо стиля , основанного на принципе проектирования по контракту, используется стиль "try-catch".
Исключение, причиной которого, например, явилась ошибочная арифметическая операция (переполнение сверху или снизу - overflow или ), прервет нормальный поток выполнения. Блок кода со специальным синтаксисом, в котором контролируется возникновение исключительных ситуаций, называется try-блоком. Исключительная ситуация, возникшая в catch-блоке. Рассмотрим пример:
try {
… Код, который может включить исключение …
} catch (io_error e) {
… Обработка исключений ввода-вывода I/O …
} catch (memory_error e) {
… Обработка исключений, связанных с памятью …
}
Блок catch задает io_error, и имя объекта, задающего исключение, здесь e, используемое в операторах обработки (способом, подобным
Исключения можно также включать (говорят также - "выбрасывать"), применяя специальный оператор:
throw exp
Здесь exp является выражением. Хотя оно может быть любого типа, но на практике используются специальные библиотечные классы, спроектированные для описания исключений.
Любое исключение, встретившееся во время выполнения блока, прерывает выполнение этого блока (оставшиеся операторы не выполняются). После чего:
catch-блоков соответствует типу возникшего исключения, то выполнение будет передано в соответствующий catch-блок, затем будет выполняться следующая конструкция, если только сам catch-блок не выбросит повторно исключение - throw ();try-блока, текущая функция завершается, выбрасывая исключение в вызывателе - функции, вызвавшей функцию, в которой возникло исключение. И здесь рекурсивно применяется описанная схема.Если в цепочке вызовов не найдется соответствующий catch-блок, то выполнение закончится прерыванием начальной точки - функции main и, следовательно, завершением программы в состоянии, свидетельствующем об ошибке.
В этом RAII.
Для безопасной обработки исключений можно указать, как часть сигнатуры функции, множество throw - список исключений, которые могут быть выброшены при выполнении. Вот пример:
void read_and_store (string a_file_name) throw (Io_error, Memory_error)
{ … }
Отсутствие множества throw может означать, что функция может выбрасывать любое исключение (для указания, что она не может выбрасывать исключения, применяется конструкция throw ()). Систематическое включение множества throw в каждую функцию означает, что для данной функции это множество является надмножеством множеств throw для всех вызываемых функций. Это рекомендуемая дисциплина, помогающая избежать пропуски исключений. Но это трудная задача, поскольку библиотеки и существующий код, используемый новыми системами, может не следовать этой дисциплине.
Шаблоны являются C++-версией универсальности. Вот простой пример:
template <typename G> class Stack {
…
public:
G item () {…}
void push (G an_item) {…}
void pop () {…}
};
Здесь определен класс Stack с родовым параметром G, аналог класса STACK [G] в . Конкретный стек задается как:
Stack<int> s;
Главная разница между шаблонами C++ и ОО-языках, в том, что каждое родовое порождение, такое как выше, рассматривается как создание нового класса, - процесс, называемый конкретизацией шаблона.
Препроцессирование - преобразование текста программы до компиляции, используемое в данном случае, сохраняет мощь языка программирования и дает некоторые экзотические применения в продвинутом C++-программировании.
Здесь нет аналога понятия ограниченной универсальности: если применяется операция к переменной, тип которой задан формальным параметром, таким как G из примера выше, проверка типа будет применяться к каждому экземпляру, чтобы убедиться, что операция всегда правильна.
Шаблон класса позволяет задать полную или частичную специализацию. Полная специализация замораживает фактические параметры, как в этом примере, используя вышеприведенный Stack:
template<>
class Stack<bool> {
… Операции, специфические для булевских стеков …
};
Частичная спецификация оставляет формальные параметры, задавая некоторые ограничения:
template<typename G>
class Stack<G*> {
… Операции, специфические для стеков указателей …
}
Как показывают примеры, класс, полученный в результате специализации, может иметь собственные определения членов. Это не становится причиной конфликтов, так как компиляция всегда выбирает наиболее специализированную конкретную версию.
Помимо шаблонов классов, C++ поддерживает шаблоны функций:
template <typename G>
G max (G a, G b) { … Вычисление максимума ...}
При конкретизации функции можно опускать фактические параметры шаблона, если их возможно автоматически вывести из
int a, b, c; … c = max (a, b); // Вызов max<int>
Здесь автоматически порождается max с типом int для G.
Механизм шаблонов выходит за пределы универсальности, позволяя в качестве параметров шаблона задавать конкретные типы, такие как булевские или целые типы. Соответствующие фактические родовые параметры должны быть константами периода компиляции. В следующем примере используется эта возможность для определения умножения матриц с гарантией соответствия размеров:
template <int n, int m> class Matrix { … };
template <int n, int m, int k> Matrix<n, k> operator*
(const Matrix<n, m> m1, const Matrix<m, k> m2)
{… Алгоритм умножения матриц … }
Попытка умножить матрицы несовместимых (константы) размеров приведет к ошибке, обнаруживаемой на этапе компиляции.
Класс B можно определить как наследника (производный класс) класса A (базового класса для B):
class B : A {
…
};
Здесь нет эквивалента переименования и отмены определения, как в . Для переопределения наследуемой функции просто включите новое определение. Но следует быть внимательным - нужно сохранить исходную сигнатуру, так как в противном случае речь будет идти о перегрузке, и нет простого способа обнаружения таких ошибок.
По умолчанию наследуемые члены закрыты (private). Для изменения их статуса можно задать модификатор доступа - private, public или protected для
class B : public A {…};
При такой спецификации каждый наследуемый элемент получает статус доступа, представляющий минимум из двух статусов - оригинального и статуса наследования. В данном примере, где статус наследования самый высокий, все члены A сохранят свой статус доступа в классе B.
Для получения доступа к оригинальной версии переопределенной функции - эквивалент Precursor в - можно использовать операцию разрешения области, если только эта версия не является private:
class B : public A {
void b_function () // Не обязательно, чтобы это было переопределение r
{
A::r (); // Вызов метода из класса A
…
}};
Мы видели, что динамическое связывание является главным вкладом объектной технологии в архитектуру построения ПО. Основное различие между C++ и ОО-языками, такими как , в том, что связывание здесь по умолчанию статическое. Критерием, определяющим, какая версия функции будет вызываться для целевого объекта, является тип по объявлению, а не
class Rectangle {
…
virtual void draw() {…} // Следует объявить как виртуальную
virtual void rotate() {…}
};
class Rounded_rectangle : public Rectangle {
...
virtual void draw() {…} // Можно, но не обязательно объявлять виртуальной
//при переопределении
void rotate() {…} // … Эффект тот же - динамическое связывание
// будет применяться!)
};
Динамическое связывание применимо только для объектов, доступных через указатели или ссылки, как показано в следующем примере:
Rectangle r (1.2, 0.5); Rounded_rectangle rr (5.0, 3.2, 0.2); r = rr; // r все еще обычный прямоугольник с полями, скопированными из rr r.draw (); // Статическое связывание …: rectangle::draw() Rectangle*p = rr; p -> draw (); // Динамическое связывание…: rounded_rectangle::draw() Rectangle ref = rr; ref.draw (); // Динамическое связывание…): rounded_rectangle::draw()
Конструкторы, которые не могут применяться к существующему целевому объекту, не могут быть
Ближайшим эквивалентом отложенного метода является понятие чистой виртуальной функции, имеющей определение, но не реализацию:
class Figure {
virtual void draw() = 0;
… Другие члены класса …
};
Класс, у которого есть хотя бы одна чистая виртуальная функция, называется абстрактным, он подобен отложенному классу
В C++ поддерживается множественное наследование:
class Arrayed_stack : public Stack, private Array {…}
Механизм менее гибок, чем представленный в этой книге. В частности, не поддерживается переименование. Можно наследовать две функции с одним именем и устранять конфликт, используя операцию разрешения области:
class A {void f () {…}};
class B {void f () {…}};
class C : public A, public B {…};
void test()
{
C* p = new C();
// p->f(); Этот вызов неоднозначен и, следовательно, неправилен
p->A::f();
p->B::f();
}
Повторное наследование не позволяет выбирать между склеиванием и репликацией для каждого члена. Выбор делается глобально для класса в целом. Разрешение противоречий может требовать сложного использования области разрешения:
class D {int n;};
class E : public D{};
class F : public D {};
class G : public E, public F {};
void f()
{
G* p = new G();
// p->n = 0; // Это было бы неверным - какое n?
p->E::D::n = 0; // Правильно: присваивается версия из E
p->F::D::n = 0; // Правильно: присваивается версия из F
}
Если требуется, чтобы были присоединены общие поля предка вместо реплицированных, следует определить этого предка как
class T {int n;};
class U : public virtual T {};
class V : public virtual T {};
class W : public U, public V {};
void f()
{
W*p = new W;
p->n = 0; // Теперь правильно
}
Неудобство в том, что выбор делается не в точке использования дублирующего наследования, здесь, в классе W, но ранее, в классах U и V, которые могут ничего не знать о планах W наследовать от них обоих.
В C++ специфические правила создания экземпляров производных классов. Конструкторы не наследуются, но создание экземпляра производного класса становится причиной вызова конструктора родителя перед началом работы собственного конструктора (процесс рекурсивный). При вызове конструктора родителя ему могут быть переданы необходимые ему аргументы, как в следующем примере:
class Rounded_rectangle : public Rectangle {
public:
Rounded_rectangle (float w, float h, float r) : Rectangle (w, h), radius (r)
{…}
… Остальная часть класса как ранее …
};
В больших программных системах трудно избежать появления конфликта имен: система может использовать библиотеки, включающие классы с одинаковыми именами. 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 " даст ссылки на многие из этих инструментальных средств, чье использование остается ограниченным, так как они не интегрированы в язык.
Как отмечалось, C++ не имеет механизма агентов. Простой эффект вызова переменной, задающей функцию, может быть достигнут (как мы видели) через указатели функций. Более сложное решение использует понятие ()" - операции вызова функции. Объект "
Мы видели, что в C++ нет прямого соответствия ограниченной универсальности и что каждая конкретизация шаблона осуществляет собственную проверку типов. Это означает невозможность информировать клиентов, что родовой параметр представляет потомка определенного типа. Они узнают об этом, только нарушив это требование, когда конкретизация шаблона не будет компилироваться.
Методологическое правило - ограничение универсальности необходимо задавать неформально через комментарии в определении шаблона:
template <typename G> /* G должен быть потомком Comparable */
G max (G a, G b) { … }
В C++ нет эквивалента класса, представляющего вершину в ANY в , нет и класса - аналога NONE.
Мы уже встречались с несколькими свойствами C++, не доступными в . Сейчас рассмотрим две другие особенности: аргументы по умолчанию и вложенные классы.
Для формального аргумента можно задать значение по умолчанию, позволяя при вызове опускать задание соответствующего аргумента. Если не все формальные аргументы имеют значения по умолчанию, то аргументы, имеющие значения по умолчанию, задаются после них:
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++ библиотека покрывает фундаментальные структуры данных, в частности, контейнеры, требующие универсальности, так что большинство ее классов представляют шаблоны (отсюда название). В этой же библиотеке размещаются классы ввода-вывода и исключений.
Для ввода-вывода использует потоки, которые могут представлять окружение, такое как консоль (стандартные потоки 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. Таких схем лучше избегать.
Блоки соответствуют и состоят из списка операторов в фигурных скобках. Блоки, как мы видели, могут быть вложенными. Условный оператор имеет форму:
if (expression) statement else statement
Заметьте, выражение условия - expression - должно быть в круглых скобках. Оно не обязано быть 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, если она задана, в противном случае ничего не делается (в в отсутствие ветви else в операторе в подобной ситуации в период выполнения генерируется ошибка). 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 - обеспечить продвижение к следующей операции (в эта часть включается в тело цикла). Приведем пример цикла в :
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++ является любая последовательность букв и цифр, начинающаяся с буквы (подчеркивание относится к буквам, по соглашению, идентификаторы, начинающиеся с подчеркивания, резервируются для специальных переменных, управляющих компиляцией).
В отличие от , идентификаторы C++ чувствительны к регистру.
Здесь нет стандартных соглашений по наименованию, так что можно использовать соглашения этой книги или другие правила стиля. Заметьте, однако, что в имена классов заданы в нижнем регистре.
Литералы (манифестные константы) могут представлять целые, символы, строки и числа с плавающей точкой.
Целая константа может быть десятичной, восьмеричной, начинающейся цифрой 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.
Для получения официальных документов о завершении программы дополнительного профессионального образования (удостоверения о повышении квалификации, дипломов о профессиональной переподготовке и MBA) необходимо предоставить:
Внимание! Вы можете не заказывать доставку бумажной версии официального документы, а скачать его в электронном виде и распечатать самостоятельно. Информация о выданном документе в течение 1 месяца загружается в Федеральную информационную систему «Федеральный реестр сведений о документах об образовании и (или) о квалификации, документах об обучении» - ФИС ФРДО.
Доступ на новый сайт осуществляется с использованием адреса электронной почты, который был указан вами при регистрации на "старом". Мы постарались перенести все ваши данные с прежнего ресурса, однако не исключена вероятность потери части информации.
При возникновении проблемы со входом, воспользуйтесь функцией сброса пароля
Если вы обнаружите несоответствия, пожалуйста, сообщите нам.