Си++

Материал из Викиучебника
Перейти к: навигация, поиск

Это — вводный курс по объектно-ориентированному программированию на языке Си++.

Материал изложения примерно соответствует части курса ООП ФИТ НГУ (Новосибирский государственный университет) за третий семестр, касающейся Си++, а также классическим книгам Страуструп/Эллис (около 1990) и Саттер/Александреску (около 2000). Смотри список тем к экзамену.

Содержание

Введение[править]

Язык C++ сравнивается с С, который считается уже известным читателю. За отправную точку принят стандарт C89 (он же C90). По следующим причинам:

  • Стандарт C99 не реализован полностью ни в GCC [1], ни Visual C++ [2].
  • В учебнике брать за исходную точку следует стандарт, который реализован полностью и уже давно, а не тот, работа по реализации которого ещё не закончена. Си++ и без того довольно сложный язык, чтобы ещё запутывать читателя подробностями о том, какие свойства C99 где реализованы.

Некоторые конструкции („два слеша“) и inline давно реализованы во многих компиляторах Си. Это не делает их частью стандартa C99.

Основные отличия Си++ от Си[править]

Самые элементарные усовершенствования[править]

Комментарии[править]

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

int a;   // это комментарий
float b; /* и это тоже комментарий */
char *s1="Текстовая строка с двумя слешами //, которые не обозначают начало комментария."; 
char *s2="/* Это тоже не комментарий! */";

Однако, учитывая правила синтаксического разбора текста, описываемого Стандартом языка, не все символы комментария будут игнорироваться компилятором. Так, следующий код может поначалу удивить:

int x = 0, y = 0, z = 0;
 
++x; // увеличиваем x? \
++y; // увеличиваем y??/
++z; // увеличиваем z???
std::cout << x << '\t' << y << '\t' << z << std::endl;

Здесь только первый инкремент будет распознан компилятором как не комментарий. Т.к. сначала компилятор выполняет разбор триграфов, строка со вторым инкрементом в результате будет преобразована в заканчивающуюся на \, затем он выполняет "склейку" соседних строк, в которых первая заканчивается на \, в единую физическую строку, и лишь только затем он начинает делить текст программы на токены, подготавливая его для обработки препроцессором, и удалять комментарии, заменяя каждый одним пробелом. Т.о. все три строки с инкрементами на самом деле являются одной строкой, где начавшийся с // комментарий продолжает вплоть до конца всех трёх "склеенных" строк.

Встраиваемые функции[править]

Допустим, мы хотим определить возведение в квадрат для вещественных чисел и назвать его Sqr. В Си это можно сделать двумя способами:

/* Способ 1 */
double Sqr(double x) {return x*x;}


/* Способ 2 */
#define Sqr(x) ((x)*(x))

Какой из них лучше? В первом случае помимо собственно умножения имеют место накладные расходы, связанные с вызовом функции: помещение параметра в стек или регистр (в последнем случае возможно возникнет необходимость сохранить старое значение регистра и восстановить его впоследствии), передача управления функции, возврат из функции, освобождение стека. Компилятор может подставить тело функции вместо её вызова и избавиться таким образом от накладных расходов лишь в том случае, если это тело находится в текущей единице трансляции (модуле) (в противном случае можно лишь надеяться на встраивание компоновщиком). Но обычную функцию стандарт разрешает описывать лишь в одной единице трансляции. Может быть, второй? Но макросы в Си приводят к куче проблем, o чём см. любое руководство по Си или Си++. Так что на вопрос «какой лучше» правильный ответ — оба хуже.

В Си++ введено слово inline, означающее рекомендацию компилятору сделать функцию встраиваемой, то есть вместо генерации вызывающего её кода подставлять непосредственно её тело. Помимо «рекомендации», стандарт закрепляет за словом inline требование (а следовательно, и разрешение) описывать функцию в каждой единице трансляции, в которой она используется, что, собственно, и играет ключевую роль в вопросе встраивания.

Так например, если написать

inline double Sqr(double x) {return x*x;}

то Sqr(x) будет (почти во всех реализациях) вычисляться так же быстро, как x*x, но x предварительно приводится к типу double. По сути, Sqr(x) будет заменено на что-то вроде (double y=x, y*y), если бы такие выражения были разрешены.

Встроенные функции обладают всеми достоинствами функций, которых лишены макросы: они приводят к типу, они знают об области видимости, они могут содержать условные операторы, они могут быть (см. ниже) перегруженными, или функциями-членами, или членами пространств имён, или функциями-друзьями, или задаваться шаблонами, или быть параметрами шаблонов. Макросам всё это недоступно.

Даже более, Стандарт чётко требует от компиляторов, чтобы программисту не требовалось задумываться на тему "а что, если вдруг", т.к. семантика программы, независимо от того, встраивается ли какая-либо функция или нет, оставалась неизменной. Поэтому, например, если в inline-функции присутствуют локальные static-переменные с неконстантным выражением инициализации, то соблюсти единственность и своевременность этой инициализации независимо от того, какое именно встраивание этой функции будет выполнено первым после запуска программы — это задача компилятора. В прежней редакции Стандарта, называемой С++03, это правило могло не соблюдаться в случае многопоточных приложений, т.к. многопоточность в той редакции на уровне языка явно не поддерживалась. Однако в редакции C++11 эта проблема уже явно оговорена как долженствующая быть решена средствами компилятора и/или библиотек. Также вполне возможно взять указатель на встроенную функцию, и этот указатель будет её чётко и однозначно идентифицировать. (Есть некоторые оговорки касательно виртуальных методов классов, однако их вызовы далеко не всегда могут быть встроены.) И по этой же причине компилятор всегда обязан генерировать вызываемый аналог встраиваемой функции и помещать его в объектный код, даже если ему никогда этот вызываемый аналог не потребовался. Это связано с тем, что ввиду раздельной компиляции единиц трансляции компилятор не знает наверняка, будет ли ему доступно тело функции везде в программе, ибо если нет, тогда ему придётся сгенерировать не встраивание, а вызов, разрешить который будет являться задачей линкера. Исключений тут может быть всего одно: если inline функция является static, т.е. имеет внутреннее связывание. Только в этом случае компилятор может быть уверен, что ниоткуда извне такая функция вызвана быть не может. Но даже в этом случае мы можем "заставить" его сгенерировать вызываемый вариант, стоит только получить на неё указатель.

И в то же, несмотря ни на что, время тело встроенной функции, подобно макросу, пишется там же, где её заголовок; обычно это заголовочный файл. Так, например, неправильно, если sqr.h включается более чем в одном cpp-шнике, а потому так писать очень нежелательно:

// Файл sqr.h
inline double Sqr(double x);
 
// Файл sqr.cpp
#include "sqr.h"
inline double Sqr(double x) {return x*x;}
 
// Файл foo.cpp
#include <sqr.h>

А так правильно:

// Файл sqr.h
inline double Sqr(double x) {return x*x;}
 
// Файл foo.cpp
#include <sqr.h>

И так тоже правильно:

// Файл sqr.h
inline double Sqr(double x);
 
// ...
 
inline double Sqr(double x) {return x*x;}
 
// Файл foo.cpp
#include <sqr.h>

Следует отметить, что inline — лишь рекомендация (а не команда) компилятору заменять вызов функции её телом. Компилятор может посчитать встраивание нецелесообразным, так как оно ведёт к разбуханию кода, а это, в свою очередь, к более частым промахам кэша команд, что в конечном итоге может даже снизить производительность. В некоторых случаях компилятор попросту не может встроить функцию (например, если в текущей единице трансляции отсутствует её тело). Кроме того, современные компиляторы обычно встраивают функции с описателем inline, если нет серьёзных причин обратному, и даже могут встраивать функции и без подсказки программиста, если сочтут это уместным и возможным.

Следует также отметить, что вызовы рекурсивных функций могут быть частично или даже полностью «размотаны». Например

long long int Factorial(long long int x){x > 1 ? x*Factorial(x - 1) : 1;}

Вызов функции Factorial(5) хороший компилятор может заменить числом 120, или, например, 5*(4*(3*Factorial(2))), или 20*Factorial(3) и т. д.

Задание аргументов по умолчанию[править]

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

void f(int x, int y=5, int z=10); // f(1), f(1,5) и f(1,5,10) - одно и то же
 
// void g(int x=5, int y); /* Неправильно! По умолчанию задаются только последние аргументы */
 
f(1);       // будет вызвано f(1, 5, 10)
f(1, 2);    // будет вызвано f(1, 2, 10)
f(1, 2, 3); // будет вызвано f(1, 2, 3)

Для каждого параметра значение по умолчанию можно указать не более одного раза, но каждое последующее объявление функции, а также определение функции может назначать параметрам значения по умолчанию. При этом после каждого объявления/определения значения по умолчанию должны иметь лишь последние параметры:

// Файл foo.h
void foo(int a, int b=3); // OK, параметр b имеет значение по умолчанию
void foo(int a, int b=3); // Ошибка: параметр b уже имеет значение по умолчанию
 
void foo2(int a=3, int b); // Ошибка: значения по умолчанию могут иметь лишь последние параметры
 
void foo3(int a, int b, int c=4); // OK, параметр c имеет значение по умолчанию
void foo3(int a=1, int b, int c); // Ошибка: значения по умолчанию могут иметь лишь
                                  // последние параметры
 
void foo4(int x, int y, float z, double t=3, double u=4); // OK, параметры t и u имеют значения
                                                          // по умолчанию
 
inline void h(int x, int y){foo4(x, y);} // Ошибка: z не имеет значения по умолчанию
 
void foo4(int x, int y, float z=4.0f, double t, double u); // OK, параметры z, t и u имеют значения
                                                           // по умолчанию
 
inline void k(int x, int y){foo4(x, y);} // OK
 
// Файл foo.cpp
...
void foo4(int x, int y=6, double z, double t, double u) { // OK, параметры y, z, t и u имеют
                                                          // значения по умолчанию
 <br />
// тут тело функции
 <br />
}

Умолчания параметров строго равносильны конструкциям с inline функциями:

void foo(int x, int y, double z);
inline void foo(int x, int y) { foo(x, y, 10); }
inline void foo(int x) { foo(x, 5); }

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

Если же такое очень хочется, то можно написать явно:

void f(int x, int y);
inline void f(int x){ f(x, x); }

Функции без аргументов и с неизвестными аргументами[править]

При описании функции, отсутствие аргументов в скобках означает, что их нет, а не что они неизвестны (как в Си). В Си++ записи f() и f(void) строго равносильны.

Если количество или типы некоторых аргументов неизвестны, надо заменить их многоточием. При этом:

  • хотя бы один аргумент должен быть известен (иначе откуда функция узнает количество и типы аргументов?)
  • Известные аргументы должны быть в начале списка
void f();  // функция без аргументов
void g(void); // тоже функция без аргументов
int printf(const char* fmt, ...); // функция с неизвестными аргументами
int sprintf(char* s, const char* fmt, ...); // ещё одна
// int bad1(...); // Неправильно! Нужен хотя бы один известный аргумент.
                  // (Стандарт позволяет, проверьте!)
// int bad2(..., char* fmt); // Неправильно! Известные аргументы должны быть в начале списка

Доступ к дополнительным аргументам такой функции в её теле требует средств из stdarg.h

При разрешении перегрузки многоточие (иначе ещё называемое эллипсис) имеет наинизший приоритет.

Аргументы, соответствующие многоточию и имеющие тип float, перед передачей автоматически приводятся к типу double.

Описание переменных в середине блока[править]

Если в Си переменные могут быть описаны только в начале блока, в Си++ их можно описывать где угодно: например, в середине блока

{
    // тут что угодно
    int i;
    // область видимости i - отсюда до конца блока
}

и даже внутри for:

for (int i=0; i<10; i++) {
    // областью видимости i должен быть этот блок, согласно стандарту ANSI C++
    // впрочем, старые версии Visual C++ игнорируют этот стандарт
    // и расширяют область видимости i до конца внешнего блока
}

Зачем это нужно? Затем что согласно философии Cи++ описание переменной сразу же инициализирует её легитимным объектом данного типа. Поэтому переменные описываются по крайней мере не раньше, чем их можно инициализировать. Более того, хорошим тоном считается описать переменную именно тогда, когда она нужна. Помимо прочего, это делает программу читабельнее.

/*Вариант 1*/
{
    float x;
    // ещё 200 строк без упоминания x
    x = y + 15;
    // читающий программу уже забыл тип х
    // ему придётся лезть в начало блока
}


/*Вариант 2*/
{
    // 200 строк 
    float x = y + 15;
    // х описан, когда он понадобился.
}

Особенно полезно описание в середине блока для классов с конструктором (см. ниже).

Использование ссылок; передача аргументов по ссылке[править]

Передача параметров в Си[править]

В Си аргументы всегда передаются единственным образом, часто называемым «передачей по значению». Например, пусть есть функция

void foo (int x) 
{
  x = 17;
}

Мы можем вызывать её любым из следующих способов:

int main () 
{
  int z = 5;
  foo (z);
  foo (z + 1);
  foo (125);
}

Разумеется, ни один из этих вызовов не изменяет значения переменной z. (Для последних двух вызовов это совершенно естественно, а вот в первом случае вы могли засомневаться.)

Что же делать, если мы хотим дать возможность функции foo изменять значение переданной переменной? Мы можем передать ей не саму переменную, а указатель (и для C, и для C++). Перепишем предыдущий пример так:

void foo (int *x) 
{
  *x = 17;
}
int main () 
{
  int z = 5;
  foo (&z);
  /* остальные варианты больше не имеют смысла, т.к. z теперь равно 17 */
}

Такая передача аргументов, однако, опасна: легко забыть звёздочку в теле функции foo или амперсанд — в её вызове. Представьте себе, что foo состоит из 1234 строк, и везде, где употребляется x, нужна звёздочка. А ещё она вызывается 56 раз в разных местах программы — при этом иногда нужен амперсанд, иногда нет. Это — стандартная ситуация для реальных, а не учебных, программ. Итак, указатели опасны, а передача параметров по указателю опасна вдвойне (утверждение спорное и зависит от вкуса разработчика. Некоторые разработчики, напротив, предпочитают наличие амперсанда в f(&obj) как подсказки о том, что значение obj может измениться внутри вызова).

Передача параметров по ссылке в Си++[править]

В Си++ можно передавать параметры по ссылкам. Пример из предыдущего абзаца теперь выглядит так:

void foo (int& x) 
{
  x = 17;
}
int main () 
{
  int z = 5;
  foo (z); // теперь z=17
}

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

void foo (const int& x);

гарантируют неизменность передаваемого значения.

Что такое ссылка и что с ней можно делать[править]

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

  1. При объявлении ссылка обязательно инициализируется ссылкой на уже существующий объект данного типа.
  2. Ссылка пожизненно указывает на один и тот же адрес.
  3. При обращении к ссылке операция * производится автоматически.

Объявление ссылок очень похоже на объявление указателей, только вместо звёздочки «*» нужно писать амперсанд «&».

Что произошло в предыдущем разделе? Аргумент функции foo стал не указателем, а ссылкой. Поэтому теперь:

  • при вызове функции foo компилятор сам передаст адрес переменной z, нет необходимости специально его просить;
  • внутри функции foo мы обращаемся с x, как с обычной переменной, и только компилятор знает, что внутри это — указатель.

Итак, мы можем передавать по ссылкам параметры. Но ссылки могут существовать и сами по себе, вне всякой связи с передачей параметров.

Пример:

int x = 10; 
int& y = x; // Теперь y - ссылка на int, указывающая на x.
y = 20;     // теперь x==20
printf("x=%d y=%d\n", x, y); // напечатается "x=20 y=20"
 
/* int& z; */  // Запрещено!  Ссылки надо инициализировать!
 
// Ссылка может указывать и на элемент массива
double a[5];
double& b=a[3]; // Теперь b указывает на a[3].
b = 1.5;        // то же самое, что а[3]=1.5;
b++;            // то же самое, что а[3]++ . Теперь а[3]==2.5

Кроме того, функции могут и возвращать значение по ссылке:

double& Third(double* x) {return x[2];}
double a[5];
Third(a)=1.5; // теперь а[2]=1.5
Third(a)++;   // теперь а[2]=2.5

В таком случае вызов функции становится lvalue. Более того, вызов функции есть lvalue тогда и только тогда, когда функция возвращает ссылку.

Чего нельзя делать со ссылками[править]

По сути ссылка — это указатель, который

  • обязательно при создании инициализировать каким-то значением;
  • нельзя изменять после этого.

Например, вот так написать вообще нельзя:

 int &x;

Ссылку обязательно инициализировать! Подойдёт любой из следующих способов:

int z;
int *pz = &z;
int &x1 = z;
int &x2 = *pz;
int &x3 = * (int*) malloc (sizeof (int));  /* но это извращение */
int &x4 = *new int;                        /* точно такое же извращение */
int &x5 = x1;                              /* можно инициализировать и через другую ссылку */
 
/* Следует понимать, что x3, x4 инициализированны динамически. 
Память не будет освобождена автоматически, в отличии от других случаев. 
Это нужно будет сделать вручную. */

Ссылку, как только что было сказано, нельзя заставить ссылаться на другой объект. Раз уж она начала на что-то ссылаться, то она будет на это ссылаться до конца жизни. Например, заведём указатель и ссылку и попытаемся их поменять:

int z = 3, zz = 5;
int *pz = &z; /* теперь pz указывает на z,  *pz == z == 3 */
int &x = z;   /* теперь x ссылается на z,  x == z == 3 */
pz = &zz;     /* теперь pz указывает на zz,  *pz == zz == 5 */
x = zz;       /* но x ведь ссылалась на z! значит, мы написали «z = zz».
                 теперь x == z == zz == 5 */

Изменить указатель, скрытый за ссылкой x, нельзя — просто нет в Си++ оператора, позволяющего это сделать.

Ещё одно важное отличие — ссылка не может ссылаться «ни на что». То есть, если указатель может иметь значение NULL, то ссылка — нет.

Разве что мы специально постараемся...:

int& g = *(int*)(0);
int* h = &g; /* h == 0 */
g = 2; /* Ошибка выполнения */

Не бывает массивов ссылок и указателей на ссылки — такое не предусмотрено синтаксисом языка Си++. Сделано это намеренно, ибо позволение указателей на ссылки разрешало бы менять значение ссылки, что противоречит семантической неизменности ссылок после их определения и инициализации. Запрет на массивы ссылок следует из запрета на указатели на ссылки. Кроме того, ссылки на ссылки в редакции C++98 также запрещены, как и указатели на ссылки, и по той же причине. Однако в шаблонном обобщённом коде это правило может мешать, т.к. делает ссылочные типы несимметричными по свойствам с нессылочными. Поэтому в редакции C++03 были введены правила т.н. сведения ссылок, которые по-прежнему не позволяет существовать ссылкам на ссылки, однако сводит исходно ссылочные типы к ним же, не меняя ничего в итоге и не добавляя ещё одного уровня косвенности, но в то же время нессылочные типы сводит к ссылкам. Именно такое поведение обычно и желают программисты, создавая обобщённый шаблонный код.

Если ссылка есть поле класса, то класс обязан иметь явно написанный конструктор, и все такие поля обязаны быть инициализированы в ctor-инициализаторе:

int i;
 
class Ref
{
public:
 Ref() : m_r(i) {}
private:
 int& m_r;
};

Неконстантная ссылка на тип T может быть инициализирована только lvalue, и только типа T — запрещены даже простейшие преобразования типа short в long и Derived к Base.

Показанное ниже неверно:

Derived d;
Base& br = d; // требуется приведение
short i;
long& ri = i; // требуется приведение
long& lr = 1; // не есть lvalue

С константной ссылкой эти ограничения снимаются путем создания временного безымянного объекта, на который и будет ссылаться ссылка.

Derived d;
const Base& cbr = d; // работает

На деле это означает:

Derived d;
Base __tmp1 = d;
const Base& cbr = __tmp1;

Зачем нужны ссылки?[править]

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

Но даже если вам это не нужно, для большого класса или структуры передача по ссылке гораздо быстрее и экономит память.

void f(Monster x);
void g(const Monster& x);
...
Monster barmaley;
f(barmaley); // будет создаваться копия монстра barmaley, а после выполнения функции — уничтожаться. 
g(barmaley); // Функция получит адрес barmaley.  Kопию создавать не надо.


По этой причине параметры длинее 8 байтов почти всегда передаются по ссылке.

Ссылки полезны и в циклах по элементам массива.

for (int i = 0; i < 10; i ++)
{
    double& x = a[i];
    if (x > 1)
        x = 1;
    if (x < 0)
        x = 0;
    x = x * x;
}

Во всех этих случаях можно было бы использовать указатель, и в Си так и делается, но, как сказано выше, указатели опасны. К тому же писать звёздочки и амперсанды — утомительно.

Но по-настоящему необходимы ссылки для перегрузки операторов, о которой пойдёт речь в следующих разделах. Так, например, что ++ обычно понимается как изменяющая свой аргумент, потому ::operator++(T) обычно обязана иметь ссылку в параметре — т.е. ::operator++(T&).

Точно так же operator[] обычно понимается как возвращающая lvalue, что требует T& Arr::operator[](int index);.

Использование констант[править]

Общие соображения[править]

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

Константы есть и в Си, но их никто не использует, ибо они были кривые. Числовые константы в C делают с помощью #define, и это неплохо работает, хотя и имеет свои минусы. Константы всех остальных типов в Cи используют редко.

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

…А главное отличие — их теперь можно использовать! Например, можно написать так:

const int N = 10;
int A [N];

Возможно, вы не поверите, но в Си этого сделать было нельзя. Хотя значение N известно во время компиляции, но в Си компилятор «закрывал на это глаза». (Безусловно, у них были свои причины на это, для интересующихся — причины зовут «extern», но всё равно получилось не очень хорошо.)

А раз константы можно использовать, отчего же этого не делать? Они многим лучше, чем #define:

  • они имеют тип, а значит, позволяют «на халяву» найти парочку ошибок в вашей программе;
  • они могут быть не просто числом или строкой, но и какой-нибудь сложной структурой;
  • их имена можно использовать при отладке (хотя с современными средствами это почти не актуально);
  • они не имеют побочных эффектов и других классических проблем с макросами (отсылаю вас к предостережениям о директиве #define, написанных в любой хорошей книге по Си или Си++).

Константы обязательно инициализировать, например:

const int foo = 10; /* можно */
const int bar; /* нельзя */

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

Константы и ссылки/указатели[править]

Константы очень интересно сочетаются с указателями и ссылками. По крайней мере, это интересно выглядит. Посмотрите сами:

const int *foo

или

int const *foo
Указатель на const int. Значение указателя изменить можно (так, чтобы он указывал на что-нибудь другое), а вот значение переменной, на которую он указывает, менять нельзя.
int *const foo = &x 
Константный (неизменный) указатель на int. Значение указателя менять нельзя (будто это ссылка, а не указатель). Значение того, на что он указывает, менять можно. Заметьте, что константный указатель обязательно инициализировать, как и любую другую константу.
const int *const foo = &x 
Смесь двух предыдущих пунктов. Ничего нельзя изменить: ни значение указателя, ни значение того, на что он указывает. Опять же, инициализация обязательна.

У ссылок разнообразия значительно меньше, ибо «указательная» часть ссылки и так всегда константна. Значит, бывает только:

const int &foo = x 
Ссылка на int, который мы (с помощью этой ссылки) не сможем изменить.

Для константных ссылок можно:

const int& i = 1;
// равносильно
int __tmp = 1;
const int& i = __tmp;

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

int &foo = x;
int *const bar = &x;

Фактически, после этого foo и bar отличаются только синтаксически (везде нужно писать *bar, но просто foo), и если мы везде заменим foo на *bar или наоборот, ничего не изменится.

Ссылки на правосторонние значения[править]

Значения в языке Си++ делятся на левосторонние (lvalue) и правосторонние (rvalue).

Терминология связана с тем, что только левостороннее значение можно использовать в левой части присваивания.

Левосторонние значения ссылаются на некое место в памяти. К ним относятся:

  • имя переменной или поля
  • выражение a[i] (кроме случаев написания operator[] для класса, о них см. ниже).
  • выражение *a, где а есть указатель
  • выражение a.b или p->b
  • выражение вызова функции, в том и только в том случае, если функция возвращает ссылку
  • все вышеперечисленные выражения, переопределенные для классов с помощью operator Нечто(), в том и только том случае, если operator возвращает ссылку.

Наличие скобок не влияет на то, является ли выражением lvalue или же нет. Т.е. (a[i]) = 1; — это правильно.

В некоторых случаях обязательно требуется lvalue:

  • левая часть присваивания
  • операнд операций ++ и --, а также взятия адреса &.

Правосторонние значения - это, например, (a + 1) или f(a), если f возвращает не ссылку. Rvalue классового типа всегда представляет собой временный объект (безымянную локальную переменную, введенную компилятором для своих нужд).

В языке не существует значений типа «ссылка» (именно потому не бывает массивов ссылок и указателей на ссылки). Существуют лишь значения «lvalue типа T» и «rvalue типа Т».

Ссылка без ключевого слова const обязательно требует lvalue как инициализирующего выражения. Сделано это потому, что такая ссылка подразумевает внесение изменений в объект через нее, а никому не нужно, чтобы эти изменения были выполнены над временным объектом и потом были забыты.

До появления в языке ссылок на rvalue было всего два способа указать тип T как тип формального параметра: f(T) и f(T&). Однако оба эти способа имели недостатки: в первом случае происходило копирование фактического параметра в формальный, последний всегда был временным объектом. Во втором же случае обязательно было использовать lvalue в фактическом параметре.

Кроме того, запрещен overloading функций по отличию всего лишь T и T& в параметре — это есть неоднозначность.

Это приводило к проблемам с конструкторами копирования. Чтобы избежать бесконечной рекурсии, он не может принимать тип Т, только T& и const T&. Несмотря на то, что для такого конструктора сделано исключение и ему может передаваться rvalue (что в общем запрещено для T&), конструктор не имел возможности определить, снимает ли он копию с более ненужного объекта или же с нужного, и вынужден был во всех случаях применять медленный код с полным копированием, выделением ресурсов и возможностью отказа.

Таким образом, конструктор копирования был вынужден всегда копировать объект, даже если «оригинал» есть временный и его скоро забудут, или же копия есть временная и опять же короткоживущая.

Потому в C++11 в язык были добавлены rvalue references ссылки на правосторонние значения.

Синтаксис — T&&.

Что это дает? Совместное использование T&& и T&. f(T&&) будет использоваться, если фактический параметр есть rvalue (т.е. временный объект). f(T&) будет использоваться, если фактический параметр есть lvalue.

Так, например, Func(complex(1,2)); вызовет Func(complex&&), а Complex c(1,2); Func(c); — вызовет Func(complex&).

Использование этого в конструкторах копирования позволяет реализовать «конструктор перемещения», который извлекает из параметра все внутренности и перекладывает их в создаваемый объект. Это делается очень быстро и с гарантией отсутствия отказов (не делается попыток выделить ресурсы). Параметр же корректно заполняется как пустой объект, опустошение не создает проблем, ибо данный объект в любом случае является временным.

Однако же для случая, когда «оригинал» есть lvalue, необходимо реализовать также и обычный конструктор копирования, который даст гарантию неизменности этого lvalue.

Иногда хочется легко реализовать перемещение (с «выниманием внутренностей») и для lvalue, явно указав на это. Для этого в STL есть функций std::move, которая превращает lvalue в rvalue. Возможность новейшая и появилась недавно. Так, например, компилятор Microsoft поддерживает ее только начиная с Visual Studio 2010. Аналогично перемещающим конструкторам существуют так же и перемещающие операции присваивания. Это перегруженные operator =, принимающие вот такую же rvalue-ссылку. Пользоваться перемещающим присваиванием можно точно так же и тогда же, как и перемещающим копированием. Например:

class Object
{
  /* ... */
public:
  Object(const Object&);
  Object(Object&&);
 
  Object& operator=(const Object&);
  Object& operator=(Object&&);
 
  /* ... */
};
 
Object o1;
Object o2(o1); // это Object::Object(Object&), полное копирование, с полным выделением ресурсов для o2
               // о1 не меняется
Object o3(std::move(o2)); // это Object::Object(Object&&), быстрое перемещение без аллокаций и
                          // возможности отказа после этого
                          // o2 становится объектом с неопределённым, но инвариантным состоянием
Object o4, o5;
 
o4 = o3;                  // полное присваивание, с полным выделением ресурсов для o4, о3 не меняется
o5 = std::move(o4);       // быстрое присваивание, но очень вероятно, что с разрушением прежнего состояния o4

Эти возможности широко используются внутри современных STL, для перекладывания объектов при росте вектора и т.д. Потому у объектов, которые будут помещаться в контейнеры STL, желательно реализовывать move constructor и move assignment.

Не следует думать, что перемещение всегда "зануляет" объект. Стандарт не требует никакого конкретного состояния для объекта-источника после его перемещения, но требует, чтобы он оставался в неком целостном, инвариантном состоянии. Во-первых потому, что для него позже ещё будет вызван деструктор, ибо перемещение само по себе объект не уничтожает, оно только переносит его состояние "на новое место". Во-вторых, если для настоящих rvalue перемещение обычно сопровождается скорой кончиной объекта, то для lvalue, перемещаемых посредством std::move(), до конца жизни объекта времени может пройти ещё немало. И всё время после перемещение и до конца времени жизни объекта он остаётся доступен программисту для работы с ним. Так что объект после перемещения обязан оставаться работоспособен, учтите это в своих перегруженных перемещающих конструкторах и операций присваивания. Да, обычно проще всего при перемещении объекту-источнику придать вид сконструированного по-умолчанию. Но реальный его вид конечно же определяется автором перемещающих конструктора и операции присваивания, поэтому нельзя давать гарантии, что так будет всегда и для всех объектов.

Логический тип и перечисления[править]

Логический тип[править]

В Си классически ложь и истина были просто нулём и не-нулём. (Если хотите — нулём и единицей.) Например, «3 > 2» равно единице; «5 < 3» равно нулю.

В Си++ придумали тип bool, который может иметь два значения — «false» (ложь) и «true» (истина). Это ничего нового не даёт, но делает программы понятнее, потому что в соответствующих местах теперь можно писать «честные» bool, true и false вместо int, 1 и 0.

Безусловно, если вы используете bool в арифметическом выражении, то true станет единицей, а false нулём. Помните, что Си++ почти полностью совместим с Си, поэтому, как правило, старые приёмы работают так же.

Перечислимые типы[править]

Как и константы, они были и в Си, однако использовались даже реже, потому что были ещё кривее (источник?). (Некоторые эстеты их всё же использовали, но это ничего не значит (источник?).)

Ключевое слово «enum» теперь создаёт новый полноценный тип. Если в Си (стандарт 1989-го года (C89)) после

 enum color {red, green, blue};

нужно было всегда называть тип «enum color», то в Си++ и С99 (Си стандарт от 1999-го года) его название — просто «color». (Аналогичную метаморфозу претерпели и типы, определяемые ключевыми словами «struct» и «union». Это действительно почти никого не волнует, ибо такие типы в практически любом реальном коде на Си объявлялись через typedef).

Для хранения enum'ов используются целочисленные значения. Без явного указания нумерация начинается с 0 и увеличивается на 1. Если вы хотите указать значение — можете его записать после знака = и последующие значения будут увеличиваться начиная с этого индекса. Имена в перечислении получают значение, начиная с нуля. Если же указано явное значение, то последующие имена получают значения явное+1, явное+2 и так далее, и так до конца типа или до следующего явного значения.

enum color {red, blue, green};
cout << red << ' ' << blue << ' ' << green; // "0 1 2"
enum color2 {yellow, cyan=100, magenta};
cout << yellow << ' ' << cyan << ' ' << magenta; // "0 100 101"

Как вы могли заметить, использовать имена, записанные в enum'е, можно как константы.

Имя из enum'а всегда молча приводится к соответствующему целочисленному типу в арифметике, побитовой логике, присваивании в целочисленные типа и так далее.

Обратное приведение автоматически не делается, но может быть сделано вручную:

enum color {red, green, blue};
color c1 = 1; // ошибка
color c2 = static_cast<color>(1); // правильно

sizeof() перечисления совсем не обязан быть равен sizeof(int), может быть и 1, и может меняться от перечисления к перечислению. Компилятор вправе выбирать представление перечислений (как один из целочисленных типов) персонально для каждого перечисления. Некоторые компиляторы имеют ключ командной строки «treat enums as ints», который подавляет эту возможность. Строго говоря, C++ в отличие от C, вводит понятие представимого типа для перечисления, которым должен быть любой подходящий целочисленный тип, обладающий достаточной мощностью для хранения всех указанных (включая неявные, полученные инкрементом предыдущего значения) значений элементов перечисления. Суть требований Стандарта сводится к определению минимального количества бит, которые обязан вмещать представимый тип. При этом учитывается также знак значений. Например, для

enum some_enum { val1 = 1, val2, val3 = 5 };

для хранения всех указанных значений достаточно трёх бит. Поэтому минимальным значением перечисления является 0, максимальным - 7. Однако стоит добавить какое-нибудь val4 = -1, как количество бит увеличивается до четырёх, а минимальным значением типа перечисления должно быть уже -8 (если используется дополнительное кодирование целочисленных значений; в случае использования прямого или обратного кодирования оно будет равно -7), а вот val4 = -10 не только увеличит количество бит до пяти, и минимальное значение станет уже -16, но и максимальное будет равно 15.

Понятие представимого типа определяет значения, которые могут быть помещены в переменную типа перечисления, например, операцией присваивания. Допустимыми будут все значения в диапазоне от минимального до максимального, в нашем примере это от 0 и до 7 включительно. Пусть даже для некоторых значений нет явных имён-идентификаторов, они могут быть получены явным преобразованием, например, посредством static_cast<>, из целочисленных выражений. Однако остальные значения, выходящие за определённый т.о. диапазон, недопустимы. Для указанного примера в качестве представимого типа компилятор может использовать любой тип, хоть даже и char. И хотя char на подавляющем большинстве платформ включает 8 бит, однако использование даже значения 8 по Стандарту ведёт уже к неопределённому поведению.

Главное отличие перечислений Си++ от Си[править]

В стандарте 2000-x появятся «strongly typed enums»:

enum class E{value1=100,value2,value3};
...
...
if(E::value1==100){  // не будет конвертироваться в int
// этого можно добиться с помощью
enum class E:unsigned int{value1=100,value2,value3};

Статические массивы: что да как?[править]

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

Массивы — это та часть языка Си, которая не подверглась изменениям при эволюционировании языка в C++. Поэтому их объявление и работа с ними на обоих этих языках совпадает. Чтобы создать в памяти новый массив, используется такая запись:

int m[10];

int — это тип элементов массива, одинаковый для всех них. Конечно, Вы можете использовать любой другой тип (кроме void, ссылок и типов с неизвестным размером), чтобы задавать массивы. Квадратные скобки обозначают, что это массив, целое число в скобках (обязательно константа, известная на момент компиляции) — его размер, т.е. количество элементов. m — это имя нашей переменной-массива. Важно заметить, что в C/C++ типы «массив» и «указатель» тесно связаны, а именно: массив практически всегда автоматически приводится к указателю на свой первый элемент. Поэтому в функцию, которая требует указатель, вполне законно передавать массив:

void MyFunc( int *arr );
MyFunc(m);

С другой стороны, среди формальных параметров функции можно объявить и обычный массив, и даже массив без указания размера. Если тип параметра — T[N] или T[], то он автоматически меняется на T*. Так что следующие три объявления функций абсолютно идентичны:

void MyFunc1( int *arr );    // компилятор посчитает
void MyFunc2( int arr[] );   // эти три объявления
void MyFunc3( int arr[15] ); // абсолютно идентичными

Однако можно передать массив по ссылке, так как для параметров-ссылок таких замен не производится:

void MyFunc4( int (&arr)[15] ); // Сюда можно передать массив из 15 элементов типа int (но ни из
                                // какого другого количества)
void MyFunc5( int (&arr)[3] );  // А сюда можно передать массив из 3 элементов типа int
                                // указатели сюда передавать нельзя. Менять параметры внутри функций 
                                // тоже нельзя, так как они являются псевдонимами массивов, а не 
                                // указателями.

Если нужно передать массив по значению, то нужно объявить структуру с полем типа массив и передавать объект этой структуры.

Имя переменной-массива трактуется тождественно указателю на первый элемент только в value context.

К value context не относятся:

  • а) аргумент операций sizeof, typeid
  • б) lvalue context — т.е. левый операнд присваивания, операнд операции адресации &, инкремента/декремента (++/--), аргумент конструктора ссылки, как частный случай — аргумент для функции, ожидающей ссылку в качестве параметра и т. д.

sizeof() массива запрещен для массивов с хотя бы одним неуказанным размером, и является размером всего массива в ином случае. sizeof() указателя имеет совсем иной смысл.

Операции ++/-- и использование слева от присваивания для массива запрещены. Операция адресации, применённая к массиву (как и ко всякой lvalue) возвращает его адрес, численно равный указателю на первый элемент массива, но имеющий другой тип: указатель на массив, а не указатель на элемент. Например, для массива mas из трёх элементов типа int верно:

typeid(&mas) == typeid(int(*)[3])
typeid((int*)mas) == typeid(int*)

при этом:

(void*)&mas == (void*)(int*)mas


Следует отметить, что операция индексации [] применяется именно к указателям, а не к массивам, и x[y] тождественно равно *(x + y) (где один из х и у — указатель, а другой — целое). То есть в контекстах mas[i] и i[mas] происходит автоматическое приведение типа mas к int*.

Страуструп отмечал, что понятие массива в Си (которое было взято в Си++ без изменений) является слабым местом, так, например, невозможно различать указатель на элемент массива и на одиночный объект. Потому в Си++ для массивов настоятельно рекомендуется использовать std::vector<T>

Но при всём том, массив и указатель — это различные типы. Так, если написать в одном файле

int a[10];

а в другом

extern int *a;

— компилятор, скорее всего, выдаст Вам ошибку (компилятор об этом даже не узнает, ошибку даст линкер, и только в том случае, если переменная объявлена вне блока extern "C").

Инициализация[править]

Если создаётся глобальный массив, то изначально все его элементы по умолчанию нули, однако зачастую возникает необходимость присвоить им другие начальные значения. Процесс присваивания начальных значений и называется инициализацией. В C/C++ инициализация осуществляется с помощью знака =, который пишется после имени переменной. Этот знак не есть оператор присваивания, поэтому если Вы будете таким образом инициализировать экземпляры классов, то будет вызван конструктор, а не функция operator=.

double dbl = 1.0; /* инициализация простой переменной */
double *m[5] = {NULL, NULL, &dbl}; /* инициализация массива указателей */

Как видно из данного примера, значения для инициализации массива пишутся через запятую, причём вся группа значений берётся в фигурные скобки. Если указано n значений для инициализации, и n меньше числа элементов в массиве, то первые n элементов инициализируется согласно списку значений, а остальные элементы становятся нулями. То есть, массив m после инициализации будет выглядеть таким образом: NULL(0), NULL(0), &dbl, 0(NULL), 0(NULL).

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

char str[10] = "Aspid";

Но при этом надо помнить, что sizeof строки на самом деле на единицу больше, чем видимое число символов в ней, поскольку она содержит ещё и символ завершающего нуля.

Наконец, инициализация массива позволяет избежать явного объявления размера. Массив автоматически будет создан такого размера, сколько элементов содержится в списке инициализации:

int m[] = {2, 4, 6}; // создаётся массив из трёх элементов 
char c[] = "Sample String"; // создаётся массив содержащий строку, размерность 
                            // подсчитывается автоматически

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

complex ca[2] = { complex(1, 2), complex(3, 4) };

Но в случае, когда такой массив создается оператором new, это невозможно. Запись new MyClass[size] обязательно требует наличия у класса конструктора по умолчанию.

Использование массивов[править]

Предположим, у нас имеется массив m (или, что то же самое, указатель на его начало). Как нам обратиться к самому первому его элементу? Ко второму? К (k + 1)-му? Правильно, так:

*m
*(m + 1)
*(m + k)

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

m[k] = 17;

В данном примере в ячейку с индексом k записывается число 17. Существует также альтернативный способ записи, который приводит к ровно такому же результату. Возможность такой записи вытекает из коммутативности сложения указателя и целого числа. Вот он, этот альтернативный способ:

k[m] = 17;
0[&x] = x + x; // контрольный вопрос : что делает эта строчка?

Правда, я ещё ни разу не видел, чтобы такая экзотическая запись где-нибудь использовалась.

Конечно, ничто не в силах нам запретить обратиться к элементу 20-элементного массива с индексом 138, равно как и к минус первому элементу. Всё дело в том, что язык не располагает встроенными средствами проверки выхода за границы массива. Предполагается, что пользователь должен самостоятельно следить за тем, чтобы не вылезти за границы массива.

Кстати, если массив m объявлен в программе где-то далеко, то вы можете «на ходу» узнать количество элементов в нём в помощью вот такой конструкции:

sizeof(m) / sizeof(m[0])

На самом деле, этот метод предпочтительнее, чем прямое указание размера, потому что если вдруг размер массива нужно изменить, то переписать нужно всего одно число при объявлении, вместо того, чтобы ползать по коду, выискивая места, где ещё понадобилось это значение.

Основное удобство при работе с массивами заключается в том, что с ними можно работать посредством циклов, а не обращаясь к каждому элементу по отдельности

int* a=new int [10];
for(int i=0;i<10;i++)
{
 a[i]=random(25);
}
//...
delete []a;

Многомерные массивы[править]

Одна из самых приятных особенностей языка — возможность создавать массив из массивов (т.н. двухмерные массивы), из таких массивов собрать ещё один массив (тогда получится трёхмерный массив) и т.д. Интуитивно понятно, как это делается:

int m[5][8];

Такой код генерирует массив из 5 элементов, каждый из которых является массивом из 8 элементов типа int. Можно обратиться к любому из 5 подмассивов непосредственно (m[3]), либо к конкретному элементу конкретного подмассива (m[3][7]).

m[3] имеет тип «массив из 8-ми int», т.е. int[8], и, как любой массив, автоматически приводится к int* в любом value context.

sizeof(m[3]) будет 8*sizeof(int), а использование m[3] слева от присваивания и как операнд ++/-- запрещено.

Операторы управления динамической памятью[править]

При написании серьезных проектов всегда возникает необходимость выделить дополнительный кусок памяти. Динамическая память — это отнюдь не «барство дикое», а необходимый инструмент. Просто зачастую (например, если мы описываем деревья или списки) изначально нам неизвестно, сколько ячеек памяти может понадобиться. На самом деле, такая проблема была на всем протяжении существования науки/искусства программирования, поэтому неудивительно, что ещё в Си были функции для динамической работы с памятью.

Как это делалось в старом добром Си[править]

В Си для этого было две главных функции: одна называлась malloc() и выделяла непрерывный кусок памяти, другая, free(), этот кусок освобождала. Вот как выглядит код на Си для работы с динамической памятью (она на жаргоне называется кучей, heap):

int *Piece;
Piece = (int*)malloc(sizeof(int)); /* аргумент функции malloc() - число байт, которые надо выделить */
if (Piece == NULL) /* malloc() возвращает NULL, если не может выделить память */
{
  printf("Ошибка выделения памяти: видимо, недостаточно места в ОЗУ\n")
  return;
}
...
free(Piece); /* аргумент free() - указатель на уже ненужный кусок памяти */

Если возникала необходимость выделить память под несколько переменных одного типа, расположенных рядом (то есть под массив), аргумент malloc()'а просто домножали на нужное количество ячеек массива:

int *Piece = (int*)malloc(15 * sizeof(int));

Был, правда, у malloc()'а один «недостаток»: выделяя память, он не изменял содержимое ячеек, поэтому там могло оказаться совершенно произвольное значение. С этим боролись либо с помощью специальной функции memset(ptr, c, n) (она заполняет n байт памяти начиная с места, на которое указывает ptr, значением c), либо с помощью calloc()'а. Функция calloc() принимает два параметра: число ячеек массива, под которые надо выделить память, и размер этой ячейки в байтах; делает эта функция следующее: выделяет нужное количество памяти (непрерывный кусок) и обнуляет все значения в нём. Таким образом такой код:

int *Piece = (int*)malloc(15 * sizeof(int));
memset(Piece, 0, 15 * sizeof(int));

эквивалентен такому:

int *Piece = (int*)calloc(15, sizeof(int));

Операторы new и delete[править]

Идеология языка C++ предполагает, что каждый объект создаётся (объявляется) именно в том месте, где он нужен, и является работоспособным сразу после создания. Для этого каждый класс имеет определённый набор конструкторов — функций, которые должны автоматически запускаться при создании объекта (экземпляра данного класса) и инициализировать его члены (data members). Конструкторы одного класса отличаются только количеством и типом передаваемых параметров, то есть являются перегруженными функциями. Однако, к сожалению, функции malloc() и сalloc() не умеют автоматически запускать конструкторы, и потому непригодны для динамического создания объектов. В языке Си++ им имеется адекватная замена — оператор new. Рассмотрим пример:

MyClass *mc = new MyClass(5);

В данном случае создаётся экземпляр класса MyClass, после чего с помощью его конструктора, принимающего в качестве параметра целое число (в данном случае, число 5), объект "инициализируется" этим числом. Адрес вновь созданного объекта присваивается указателю mc. Если для класса определён конструктор по умолчанию, после имени класса допускается не указывать пустые скобки. Писать их или нет — это, как говорится, дело вкуса:

new MyClass(); // эти две строки кода
new MyClass;   // абсолютно эквивалентны

Естественно, ничто не мешает использовать оператор new для простых скалярных переменных (например, целых чисел или других указателей).

Важное отличие оператора new от функции malloc() заключается в том, что он возвращает значение типа «указатель-на-объект» (то есть MyClass *), в то время как функция malloc()«указатель-на-что-угодно» (void *). Подобная типизация в Си++ — не редкость, она строже, чем та, что используется в Си, и, следовательно, менее ошибкоопасна. Извратившись, и в Си++ можно скомпилировать код, где указатель на один класс приводится к указателю на другой класс, никак не связанный с первым — но в Си++ это можно сделать только специально. Точнее, в Си разрешено неявное автоприведение void* в любой указатель, в Си++ — запрещено.

Для каждого класса, помимо конструкторов, определён ещё и деструктор, то есть функция, отвечающая за корректное уничтожение объекта. Деструктор никогда никаких параметров не принимает, и потому не может быть перегружен. Проблема с деструктором возникает та же, что и с конструктором: функция free() не умеет его вызывать. Поэтому в Си++ введён ещё один оператор — delete. Синтаксис его очень прост:

delete mc;

где mc — указатель на класс. Именно для этого класса и вызовется деструктор, поэтому, если Вы объявили его как «указатель-на-что-угодно», деструктор не будет вызван вообще. Собственно, именно поэтому void * не рекомендуется использовать. Другой пример:

class Base
{
. . .
};
 
class Derived : public Base
{
. . .
};
 
int main( void )
{
  Base *ptr = new Derived; // присваивать указателю на предка адрес потомка -
                           // можно, но почти всегда требует виртуального деструктора
 
  ...
  delete ptr;
  return 0;
}

В этом случае оператором delete вызовется деструктор базового класса Base, хотя требуется вызвать деструктор класса-потомка Derived. Казалось бы, применение RTTI (Run-Time Type Info) в среде Microsoft Visual Studio позволило бы спастись от этой напасти, но увы и ах... (автор языка Страуструп считал RTTI злом, ибо это средство провоцирует разработчиков ломать полиморфизм и уходить из ООП-парадигмы в сторону спагетти). В принципе, гибким решением этой пробемы является применение виртуальных деструкторов. Страуструп рекомендует делать деструктор виртуальным всегда, если в классе есть хотя бы одна иная виртуальная функция. Также имеет смысл делать деструктор виртуальным всегда, когда класс будет создаваться по new. Вообще же, как мы видим, при использовании delete надо проявлять особую осторожность.

Диномассивы на C++[править]

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

MyClass *mc = new MyClass[15];

С формальной точки зрения, эта конструкция (называемая оператором new[] (разница в скобках)) не есть оператор new, описанный ранее. В частности, при перегрузке оба эти оператора описываются отдельно друг от друга и вообще никак не связываются.

Итак, в нашем примере будет выделена память под массив из 15 объектов класса MyClass, и каждый из них будет инициализирован с помощью конструктора по умолчанию. Если такой конструктор не определён, то попытка использовать new[] приведёт к ошибке. Применить к элементам созданного динамического массива какой-либо другой конструктор, увы, нельзя; поэтому такая запись вызовет легкое недоумение компилятора:

MyClass *mc1 = new MyClass("hello, world!")[134];
MyClass *mc2 = new MyClass()[2]; // это тоже ошибка, нельзя комбинировать
                                 // два типа скобок в одном new

В пару к оператору new[] введён оператор delete[]. Система сама помнит, сколько памяти было выделено под этот динамический массив, поэтому указывать число элементов не требуется. Просто напишите:

delete[] mc;

и компьютер сделает всё за Вас. Предостережение при использовании delete[] такие же, как и для delete: если хотите избежать утечек памяти (memory leak), следите за тем, чтобы вызывались "правильные" деструкторы.

Категорически запрещено путать delete и delete[] — то, что создано как массив, обязательно требует delete[], и обратно. Компилятор как правило не может отследить такое, потому ошибиться (и вызвать крах программы) довольно легко для начинающих.

TODO : перегрузка операторов new, delete, new[] и delete[] в классах

Структура программы, раздельная компиляция и особенности использования статической памяти[править]

TODO (пропущено)

NOTE: интересно, что здесь имели в виду под «особенностями использования статической памяти»? Уж не модификатор ли static для глобальных переменных и функций? да, у Страуструп/Эллис - static и extern

ADDED: Думаю, скорее имеются в виду переменные и массивы, размещаемые в стеке, в отличие от динамической памяти (куча, heap), которая выделяется при использовании оператора new и/или функции malloc().

Функциональный полиморфизм[править]

Это про перегрузку?! Или про настоящий полиморфизм — но тогда почему он «функциональный», неужели в противоположность SmallTalk'овским сообщениям? (Наш курс ООП вовсе не такой продвинутый!) И почему тогда так рано? Не, видимо, это всего лишь перегрузка. (Тогда я бы «полиморфизмом» её назвал с большой натяжкой, ибо обычно под этим термином понимают «run-time polymorphism», а у нас налицо оный в «compile-time». И всё равно, ни фига это не полиморфизм.)

Итак, напишу пока что про перегрузку.

Перегрузка функций: введение[править]

Что же такое перегрузка? В Си может быть только одна функция с одним именем. Например, представим себе семейство функций, которые выводят на экран некоторое значение (число или строку). В Си мы могли бы описать их так:

void print_int (int v);
void print_float (float v);
void print_zts (const char *v);
… … …
int main () {
  print_int (10);
  print_float (5.5);
  print_zts ("Hello, world!\n");
}

Однако на самом деле, когда мы вызываем «print_нечто (10)», компилятор уже знает, что 10 — это целое число. Но он никак не даёт нам воспользоваться этим знанием, и нам приходится вручную говорить, что печатаем мы именно int: «print_int (10)».

(Если вам интересно, почему функция названа «print_zts», я открою секрет: «zts» расшифровывается как «zero terminated string» — обозначение для строк, заканчивающихся нулевым символом.)

Си++ как раз позволяет предоставить компилятору задачу по выбору нужного варианта функции. Для этого мы можем определить набор функций, каждая из которых имеет имя «print», но которые принимают разные аргументы. Предыдущий пример тогда будет выглядеть следующим образом:

void print (int v);
void print (float v);
void print (const char *v);
… … …
int main () {
  print (10);
  print (5.5);
  print ("Hello, world!\n");
}

Как видим, компилятор и сам неплохо справляется с выбором — поэтому не нужно утруждать себя, указывая print_int или print_float.

Правила перегрузки[править]

Они очень просты (на деле очень сложны, здесь не приведено и половины, от неоднозначностей страдают даже опытные девелоперы). Функции должны отличаться количеством или хотя бы типом аргументов. Причём типы должны отличаться принципиально (см. ниже). Это то, что влияет на перегрузку. Когда вы будете работать с классами, их функции-члены ещё можно перегружать так: все аргументы имеют одинаковые или похожие типы, но одна функция имеет слово «const» в конце (то есть является константной функцией-членом), а другая — не имеет.

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

Не влияет на сигнатуру, и, следовательно, перегрузку тип возвращаемого значения. Они могут быть одинаковые или разные; на возможность перегрузки это не отразится. Так делать нельзя, хотя иногда и хочется:

/* нельзя! */
int get_item (int index);
const char *get_item (int index);

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

Что же такое «принципиально разные» типы? Рассмотрим следующий неправильный пример:

/* так нельзя! */
typedef char byte;
void print (char v); /* напечатать символ */
void print (byte v); /* напечатать значение байта (НЕ как символ) */

Для вас byte и char — разные вещи, однако для компилятора это одно и то же. Поэтому, хотя они и выглядят по-разному, на самом деле это одинаковые типы. Перегруженные функции не могут отличаться только тем, что аргумент одной имеет тип byte, а соответствующий аргумент другой — тип char.

(Лирическое отступление: если вы захотите повторить трюк с «typedef … byte» в своей программе, то нужно писать «typedef unsigned char byte», ибо под байтом обычно разумеют число от 0 до 255, а не число от -128 до 127. Кстати, если вы работаете с русскими символами, то при использовании типа «char» они будут иметь отрицательные коды, так что имеет смысл всегда писать «unsigned char» вместо просто «char». Кроме того, стандарт Си++ не определяет, будет ли char знаковым или нет — это отдано на откуп реализации. Если наличие или отсутствие знака важно (например, при конвертации в int или сравнении больше/меньше), лучше указывать это явно. Вообще, лучше запомнить и никогда не поступать наоборот: char — для символов, а signed или unsigned char — для целых. В C++ в отличие от C все три варианта char — с указанием той или иной знаковости и без оной — являются тремя разными типами. Этим он отличается от остальных целочисленных типов, каковые существуют только в двух вариантах. Например long и signed long — это один и тот же тип.)

Также стоит остерегаться неоднозначности другого вида. Смотрим пример:

void print (char v);
void print (char v, bool uc = true); /* Сигнатуры функций различны, здесь ошибки нет */
// Это ошибка, ибо второе объявление понимается как:
 
void print(char v, bool uc);
inline void print(char v){ print(v, true); }
 
// и вызовет "столкновение" по именам с первым объявлением, если не прямо здесь,
// то ниже, где будет попытка написать тело для void print(char);
 
/* а где-то в программе ... */
print ('a'); /* Ошибка, т.к. компилятор не может выбрать подходящую функцию */

Константные и неконстантные версии функций с одинаковыми аргументами также различны. Например:

class foo {
  void print (char v);
  void print (char v) const;
}
 
foo F1;
F1.print('a'); /* используется неконстантная версия print() */
 
const foo F2;
F2.print('z'); /* используется константная версия print() */

Когда использовать перегрузку[править]

Положительные стороны перегрузки:

  • удобнее писать программу (меньше приходится набирать, меньше думать);
  • меньше кода исправлять, когда тип какой-то переменной меняется.
  • перегрузка необходима для обобщённого программирования (см. ниже).

Отрицательные стороны:

  • иногда компилятор может выбрать не ту функцию, которую вы имели в виду, и вам не сказать — бывает редко, куда чаще возникает ошибка «неоднозначность», нужно подсказывать явным приведением типов параметров к чему надо
  • некоторые типы, которые вы считаете разными, компилятор сочтёт одинаковыми, и не даст перегрузить по ним. Впрочем, вы получите всего лишь ошибку компиляции.

Области видимости и Пространства имён[править]

Понятие области видимости проще всего определить как блок, т.е. фрагмент кода между {}. Это не всегда так, однако особых случаев немного. Основное назначение области видимости заключается в локализации имён сущностей программы: переменных, типов, констант итп. Любая сущность, определённая в некой области видимости, начинает своё существование в точке окончания её определения и заканчивает в точке завершения блока, в пределах которого она определена.

Области видимости играют важную роль в процессе связывания имён. Когда компилятор встречает некое имя, он выполняет его связывание с ранее определённой (или хотя бы объявленной) и следовательно уже известной ему сущностью. На процесс связывания имён области видимости влияют непосредственным образом. Имена в одной и той же области видимости не имеют никаких друг относительно друга особых взаимоотношений, и т.к. имя должно уникальным образом идентифицировать конкретный объект, то совпадающие имена в пределах одной области видимости недопустимы. Однако совпадающие имена в разных областях видимости явным образом друг с другом не коррелируют, поэтому вполне могут совпадать и даже иметь разный смысл, не вызывая при этом конфликтов в идентификации именованных ими объектов. В частности области видимости могут быть вложенными, и в этом случае внутренняя область видимости в некотором смысле имеет приоритет над внешней (окаймляющей). Так, всем известно, что некая локальная в функции переменная может иметь то же имя, что и некая глобальная, и при этом локальная при обращении к ней из кода внутри этой функции скрывает глобальную. Это частный случай взаимоотношений имён объектов из разных областей видимости. Аналогично внутри такой функции может быть вложенный блок кода, внутри которого это же имя используется для определения, скажем, typedef-типа, и это тоже не вызовет конфликта: внутри этого блока имя типа будет иметь приоритет и скроет имя переменной из окаймляющего блока - блока тела функции.

Специальным образом различают понятия область видимости блока, область видимости класса, область видимости пространства имён и глобальная область видимости.

Область видимости блока кода[править]

Области видимости блока в какой-то мере только что были рассмотрены. Обычно вложенные {} являются телами структурных операторов вроде for(), while(), if(), try() итп. Однако в соответствии с синтаксисом C++ программист имеет право окаймить {} любой фрагмент исполняемого кода по своему усмотрению, не применяя для этого особых структурных операторов, и это создаст новую вложенную область видимости со всеми её свойствами и вытекающими отсюда следствиями. Например:

int x;
 
semaphore locker;
 
void foo () {
  /* здесь делается что-то полезное */
  {
    guard lock(locker);   /* здесь начинается критическая секция - в конструкторе объекта lock захватывается семафор,
                             управляющего монопольным доступом к глобальному объекту x */
 
    ++x;
  }                       /* здесь заканчивается область видимости, внутри которой определён объект lock,
                             и в результате отработки его деструктора критическая секция освобождается */
  /* здесь продолжает делаться что-то полезное */
}
 
void bar () {
  /* здесь делается что-то другое полезное */
  {
    guard lock(locker);   // такой же монопольный захват, как и в функции foo()
 
    --x;
  }                       // здесь критическая секция так же освобождается
  /* здесь продолжает делаться что-то "другое полезное" */
}

В целом работа с блоками кода и свойства вводимых ими областей видимости в С++ не отличаются от таковых в C. Есть только два отличия:

  • в C определения локальных в блоке сущностей могут располагаться только в его начале, тогда как в C++ — в любом месте блока (но не внутри других самостоятельных конструкций, например, внутри выражений);
  • сущности, определённые в заголовках циклов (for(), while()) или условных операторов (if(), switch()), т.е. за пределами их блоков {} (даже если его нет, и тело оператора т.о. состоит из одного-единственного оператора, наличие блока подразумевается синтаксисом), она тем не менее считается локальной в этом блоке, т.е. как будто бы определённой внутри него:
for (int i=0; i<10; ++i) {
  /* здесь делается что-то полезное */
}
std::cout << "Цикл закончился. i равно " << i;        // ошибка: переменная i является локальной для блока цикла и не существует за его пределами

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

int j;
for (int i=0, j=0; i<10 || j<10; ++i, ++j) {
  /* здесь делается что-то полезное */
}
std::cout << "Цикл закончился. j равно " << j;        // ошибка: использование неинициализированной j

Здесь инициализация j=0 на самом деле является продолжением определения, начатого int i=0, поэтому внутри цикла используется локальная j, скрывшая ту, что определена в окаймляющей области видимости.

Область видимости класса[править]

Класс, неважно, определён ли он посредством class или же struct или даже union, вводит свою область видимости. Любые сущности, определённые внутри класса, являются локальными в нём. Это позволяет классу иметь свои уникальные поля данных и методы с именами, совпадающими из других областей видимости, как других классов, так и блоков, и пространств имён. Тут в C++ нет отличий от C (конечно, не считая невозможности в структурах C определять методы), где поля структур и объединений также были локальными. Однако благодаря гораздо более расширенным свойствам классов в C++ по сравнению со структурами в C, свойства областей видимости классов также сильно расширены.

Наследование[править]

Самое главное отличие (в рассматриваемом контексте) классов C++ от структур C — это возможность наследования. Каждый класс в иерархии имеет свою область видимости. При этом производный класс имеет окаймляющей областью область видимости базового, что позволяет любому производному классу иметь сущности с теми же именами, что встречаются у базового. О сокрытии имён помнят все, но многие забывают о нём в контексте перегрузки. Например:

struct X
{
  void foo(int);
};
 
struct Y: X
{
  void foo(float);
};

Здесь не так уж мало людей посчитают, что метод foo(float) в производном классе перегружает метод foo(int) из базового. На самом деле этого не произойдёт — в области видимости производного класса имя foo() скрыло это же имя из окаймляющей области видимости базового. Чтобы перегрузка всё-таки работала, что обычно и является целью автора производного класса, следует имя X::foo явным образом внести в область видимости класса Y:

struct Y: X
{
  using X::foo;
  void foo(float);
};

Вот теперь перегрузка будет работать, как задумывалось.

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

struct X
{
  void foo(int);
};
 
struct Y
{
  void foo(float);
};
 
struct Z: X, Y
{
  void bar() { foo(123u); }      // неоднозначность вызова
};

Другими словами окаймляющая область видимости производного класса наполняется всеми именами из всех его непосредственных базовых классов. Однако это не значит, что области видимости базовых классов просто слепо объединяются в одну. Если имена из разных базовых классов конфликтуют друг с другом так, что будучи размещёнными в некой действительно единой отдельной области видимости, вызовут ошибку, то в случае наполнения такими именами окаймляющей области видимости производного класса ошибки не будет, но безусловно ошибка будет детектирована, когда к таким конфликтующим именам произойдёт обращение, если только специально не озаботиться разрешением неоднозначности:

struct X
{
  void foo(int);
};
 
struct Y
{
  void foo(int);
};
 
struct Z: X, Y
{
  void bar1() {    foo(123); }      // неоднозначность вызова
  void bar2() { X::foo(123); }      // нет неоднозначности
};

Заметим, что в наполнении именами окаймляющей области видимости участвуют только непосредственные базовые классы. Если те в свою очередь также имеют базовые классы, те будут наполнять их окаймляющие области видимости, что являются ещё более внешними для рассматриваемого производного. Это создаёт довольно сложное вложение областей видимости, которые, подобно эдаким мультиматрёшкам, кольцами вложений опоясывают область видимости производного. Если по некоему направлению роста иерархии базовые классы закончились, а по другим нет, более короткие маршруты больше не участвуют в наполнении именами более внешних слоёв областей видимости, и во внимание принимаются только оставшиеся. И только когда с очередным кольцом вложения базовых классов больше не остаётся, окаймляющей областью видимости становится область видимости определения рассматриваемого производного класса. И причём только его, области видимости определений никаких его базовых классов, ни непосредственных, ни опосредованных, не рассматриваются. Например:

namespace XY
{
 
struct X1 { /* тут что-то есть */ };
struct X2 { /* ............... */ };
 
struct Y1 { /* ............... */ };
struct Y2 { /* ............... */ };
 
}
 
struct Z1: XY::X1, XY::Y1 { /* ............... */ };
struct Z2: XY::X2         { /* ............... */ };
struct Z3: Z2,     XY::Y2 { /* ............... */ };
 
void f()
{
  struct T:  Z1, Z3 { /* ............... */ };
 
  /* ну и тут тоже */
}

Здесь кольца областей видимости будут сформированы следующим образом (для простоты квалификация XY:: опущена):

  1. T
  2. Z1 ⊕ Z3
  3. X1 ⊕ Y1 ⊕ Z2 ⊕ Y2
  4. X2
  5. f()

Итого имеем четыре вложенных области видимости класса, начиная с T, и только на пятом уровне всплывает область видимости блока функции f(). Заметим, что область видимости пространства имён XY нигде не рассматривается, несмотря на то, что является окаймляющей для (в данном случае опосредованных, но это неважно, для непосредственных было бы то же самое) базовых классов.

Вложение[править]

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

struct X
{
  struct Y
  {
    int y;
  };
};
 
struct    Y y1; // вполне законная конструкция в C, но вызывающая ошибку компиляции в C++
struct X::Y y2; // правильная конструкция в C++, но невозможная в C, т.к. в нём отсутствует операция ::

Окаймляющей областью видимости для вложенных классов является область видимости класса, в пределах которого определён рассматриваемый вложенный класс. Так, если в предыдущем примере класс T был бы определён не в блоке функции f(), а внутри некоего класса, то в пятом кольце вложения фигурировал бы именно он. Далее же, в сторону ещё более внешних колец вложения, рассматривались бы его окаймляющие области видимости, как было описано выше.

Глобальная область видимости[править]

Рассмотренные в предыдущих подразделах понятия областей видимости блоков кода и классов так или иначе присутствуют в обоих языках и имеют в них одинаковые смыслы. Даже область видимости классов в C++, несмотря на огромную сложность по сравнению с областью видимости структур в C, не отличается от последней качественно. И это не случайно. Оба типа этих областей видимости создают локальность для определённых в них имён, а локальность видимости имён очень важна в контексте декомпозиции, когда большая задача делится на малые подзадачи, каждая из которых может подвергнуться повторному делению, и так вплоть до относительно малых по сложности задач, могущих быть решёнными независимо друг от друга несложными средствами. (Как многие знают, этот процесс называется проектированием сверху вниз.) Гарантия того, что каждая задача имеет свои наборы имён сущностей, не влияющие на наборы других задач, позволяет свободно выбирать имена без риска непредвиденным и непреднамеренным способом вызвать конфликты. Локальность имён из областей видимости настолько хорошо показала себя на практике, особенно на исходно больших задачах, что менять что-либо в сложившихся традициях просто нет смысла.

Тем не менее, как бы ни была хороша локальность имён, разные подзадачи, в совокупности композируемые в одну большую задачу, должны как-то обмениваться информацией. И делать это средствами только посредством имён из локальных областей видимости самих подзадач невозможно. Поэтому вполне естественно, что они это делают посредством окаймляющих областей видимости. Если разные локальные области видимости имеют для себя одну и ту же, непосредственную или опосредованную, окаймляющую область видимости, именно имена из неё могут использоваться для взаимодействия подзадач друг с другом.

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

Методы декомпозиции в C более ограничены, нежели в C++. Структуры как таковые являются просто контейнером, группирующим имена переменных (называемые в этом контексте полями структуры) в нечто цельное, удобное для манипулирования ими всеми одновременно и инкапсулирующими логически связанные характеристики в единую сущность, рассматриваемую как цельную. Собственно это и всё. Если такую структуру представить как абстрактное отражение некоего представляемого задачей объекта, то тут явно присутствуют только атрибуты этого объекта, представленные полями структуры, но недостаёт множества других полезных средств выражения. В частности манипулирование объектом может быть осуществлено только посредством функций, расположенных в той же области видимости, что и сама структура. Т.о. подзадачи, которые обмениваются такими объектами и воздействуют на них путём предусмотренных для этого функций, не имеют другого выбора, кроме как использовать для размещения имён соответствующих функций и типов структур глобальную область видимости.

Однако сами подзадачи также нужно отделять друг от друга. В языке C для этого предусмотрена возможность деления программы на раздельные единицы трансляции, которые обрабатываются компилятором строго отдельно друг от друга, и компонуемые в единую программу линкером уже после того, как компилятор закончит их все обрабатывать. Поэтому имена в глобальной области видимости обычно являются доступными для обращения к ним и из других единиц трансляции, отличных от той, которая их там определяет. Но с другой стороны, не все имена должны быть доступны извне, но в то же время должны быть расположены в глобальной области видимости. Ведь подзадача не обязана ограничиваться одним единственным типом структуры и набором функций по манипулированию ими. В общем случае типов структур может потребоваться несколько. Да и функциям может понадобиться обмениваться некой внутренней информацией, а делать её доступной всем единицам трансляции просто неразумно, ибо она не является частью интерфейса взаимодействия подзадач, а предназначена лишь для внутреннего использования самой подзадачей.

В итоге глобальная область видимости в языке C имеет два уровня локальности: уровня единицы трансляции (иногда называемой модулем за сходство с термином "модуль" из других языков, но это не является официальным названием, да и сходство большей частью внешнее) и уровня приложения. Возможность объявить некое имя локальным для единицы трансляции предоставляется путём ключевого слова static. Если его нет в объявлении сущности, имя получает видимость уровня приложения. Локальные в единице трансляции имена, хоть и расположены в глобальной области видимости, но невидимы из других единиц, тогда как остальные видны отовсюду (если только не скрыты, конечно, какой-нибудь локальной областью видимости). Например:

/* файл source1.c */
 
       int x;  /* глобальная, видимая во всех единицах трансляции */
static int y;  /* первая глобальная, невидимая из других единиц трансляции */
 
void foo (char x) {  /* первая локальная x */
  x = 12; /* здесь x -- это скрывшая глобальную переменная */
  y = 21; /* обращение к приватной первой глобальной y */
}
 
/* файл source2.c */
static int y;  /* вторая глобальная, тоже невидимая из других единиц трансляции */
 
void bar () {
  x = 12; /* здесь x -- это глобальная x (естественно, где-то ранее должно быть её объявление компилятору) */
  y = 21; /* обращение к своей приватной второй глобальной y, конфликта имён нет */
}
 
/* файл source3.c */
 
int main () {
  double x;  /* вторая локальная x */
  x = 12; /* здесь x тоже срывает глобальную, и это совсем другая переменная, нежели первая локальная */
  y = 21; /* здесь будет ошибка компиляции, ибо в глобальной области видимости текущей единицы трансляции нет имени y */
          /* если же компилятору дать объявление y, то будет ошибка линковки, ибо он не найдёт публичного глобального имени y */
}

Грамотное использование static позволяет имитировать приватные сущности классов, если в качестве класса рассматривать единицу трансляции. В этом ключе отсутствие static определяет публичную сущность "класса". Обычно объявления всех публичных сущностей помещают в заголовочный файл единицы трансляции, и он т.о. документирует её публичный интерфейс. Все, кого интересует подзадача, решаемая в рамках этой единицы трансляции (в частности и другие заголовки, которые определяют свои интерфейсы, если они зависят от этого интерфейса), просто подключают этот заголовок, и компилятору содержащихся там объявлений вполне хватает, а ежели чего не хватает, он отдаёт на откуп линкеру. Определения же в заголовках никогда не размещают (за исключением типов и встраиваемых — inline — функций... ну ещё макросов, но они не относится к рассматриваемому здесь аспекту языка). Определения всегда располагаются в том .c файле, который реализует задокументированный в заголовочном файле интерфейс.

Следует отметить, что сокрытие имён не означает "потерю" объекта. Когда внутри локальной области видимости определяется очередная «x», она скрывает все предыдущие переменные с таким же именем, но как только она заканчивается заканчивается, «её собственный» x исчезает, и вновь становится доступным x из окаймляющей области. Кроме того, в C++ путём квалифицирования имени зачастую можно ссылаться на сущности в окаймляющих областях видимости, даже если их имена скрыты.

Пространства имён[править]

Термин "пространство имён" присутствует и в C, и в C++. Однако это совершенно разные термины, и не имеют между собой ничего общего. Однако по порядку.

Пространства имён в C[править]

В C структуры («struct»), объединения («union») и перечисления («enum») могли не просто иметь (а могли и не иметь) имена, они могли иметь совпадающие имена. Дело в том, что их определения не создавали новых именованных типов, они только давали имена типам структуры, объединения или перечисления. Формально в C имеется четыре пространства имён: имена структур, объединений, перечислений и все остальные. Т.о.

struct Foo { /* что-нибудь */ };
union  Foo { /* что-либо   */ };
enum   Foo { /* что-то     */ };
int    Foo;  /* и даже так */

вполне могли сосуществовать и не конфликтовать. Когда некое имя встречается в программе, оно обязательно должно быть квалифицировано одним из ключевых слов struct, union или enum, чтобы указать, в каком пространстве имён его искать. Не увидев ничего, компилятор осуществлял поиск имени в общем пространстве, где размещались все остальные идентификаторы — типы, функции, переменные. Зачастую можно встретить в C-программе конструкции вида

typedef struct Foo_tag
{
  /* ... */
} Foo;

которые использовали для имён в не общих пространствах никогда больше нигде не встречающиеся идентификаторы. typedef является единственным способом внести имена пользовательских типов в общее пространство, и после такого внесения им можно пользоваться на общих основаниях без квалификации, что конечно в большинстве случаев удобнее.

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

struct Foo
{
  /* ... */
};
 
Foo var;

Т.о. пространства имён в терминах языка C в языке C++ более не нужны, и этот термин был переопределён с совершенно иным смыслом.

Пространства имён в C++[править]

Вернёмся к глобальной области видимости. Как уже говорилось, взаимодействие подзадач друг с другом в общем случае возможно только посредством размещения интерфейсных имён в некой общей для них окаймляющей области видимости. Ввиду того, что в C вложение областей видимости структур очень ограничено, обычно кольца областей создаются на основе блоков кода. Наименьшей самодостаточной единицей декомпозиции в C является функция. Однако функции не могут быть вложенными (вложенными могут быть их вызовы, но не определения), а глобальная уровня единицы трансляции область видимости не подходит для документирования публичных интерфейсов, поэтому в распоряжении программиста остаётся всего один уровень — глобальная уровня приложения область видимости. В результате глобальная область видимости приложения зачастую оказывается захламлённой очень большим количеством имён, из которых только небольшая часть в одно и то же время используется подзадачами для взаимодействия. И при этом велика вероятность случайного пересечения имён, когда одно и то же имя непреднамеренно будет использовано в разных интерфейсах.

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

struct ISomeInterface
{
  typedef /* ... */ type1;
  typedef /* ... */ type2;
  typedef /* ... */ type3;
 
  class SomeBlackBox { /* ... */ };
 
  static const type1 var1;
  static       type2 var2, var3;
 
  static type3 func1(const SomeBlackBox&);
  static void  func2(type1, type3);
  /* ... */
};
 
const ISomeInterface::type1 ISomeInterface::var1 = /* ... */;
      ISomeInterface::type2 ISomeInterface::var2, ISomeInterface::var3;
 
/* ... */
 
ISomeInterface::type1 someVar;
ISomeInterface::type3 f(ISomeInterface::type2);
 
ISomeInterface::func2(ISomeInterface::var1, ISomeInterface::func1(ISomeInterface::SomeBlackBox());

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

Пространства имён были призваны избавить проектные решения от этих недостатков. И с честью с этим справились. Давайте посмотрим на тот же код, но в "правильном" виде:

namespace ISomeInterface
{
 
typedef /* ... */ type1;
typedef /* ... */ type2;
typedef /* ... */ type3;
 
class SomeBlackBox { /* ... */ };
 
const type1 var1 = /* ... */;
      type2 var2, var3;
 
type3 func1(int,   type3);
void  func2(type1, type2);
 
/* ... */
 
}
 
/* ... */
 
using ISomeInterface::type1;
using ISomeInterface::type2;
using ISomeInterface::type3;
 
type1 someVar;
type3 f(type2);
 
func2(ISomeInterface::var1, func1(ISomeInterface::SomeBlackBox());

Текст программы почти не изменился, разве что исчезли "паразитные" static, возможно совмещение объявлений и определений сущностей, где это выгодно, а использование имён из локальной области видимости пространства имён стало возможным упростить подавлением обязательной их квалификации (что и было сделано в примере).

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

namespace Foo {
  namespace Bar {
    namespace Baz {
      int z;
    }
  }
}
int main () {
  Foo::Bar::Baz::z = 17;
}

Тут также видно, что именованные пространства имён, т.е. те, которые сами имеют имена, позволяют однозначно идентифицировать их локальные сущности посредством квалификации, причём используемый при этом синтаксис не отличается от квалификации имён для сущностей из классовых областей видимости. Фактически именованные пространства имён похожи на классы с публичными статическими элементами. Но есть и важные отличия.

  • Пространства имён открыты. Их можно дополнять в разных местах программы. Пример:
namespace MyFavouriteTypes {
  typedef unsigned char byte;
}
 
/* … … … */
 
namespace MyFavouriteTypes {
  typedef char *string;
  typedef const char *const_string;
}
 
int main () {
  MyFavouriteTypes::byte data [16];
  MyFavouriteTypes::const_string prompt = "Please enter your name: ";
/* … */
}

При этом неважно записано ли всё это в одном файле или разных. Вот ещё пример:

mytypes.h
namespace MyFavouriteTypes {
  typedef unsigned char byte;
}
histypes.h
namespace MyFavouriteTypes {
  typedef char *string;
  typedef const char *const_string;
}
main.cpp
#include "mytypes.h"
#include "histypes.h"
 
int main () {
  MyFavouriteTypes::byte data [16];
  MyFavouriteTypes::const_string prompt = "Please enter your name: ";}
  • Пространства имён не имеют экземпляров. Класс - это всего лишь тип, чтобы с ним работать, следует создать его экземпляр. И это неудивительно, ибо каждый экземпляр класса имеет уникальное внутреннее состояние, и все они в общем случае не взаимозаменяемы. Исключение в виде статических полей и методов по факту просто означает совмещение атрибутов класса разных экземпляров в чьей-либо одной копии, когда и если это требуется. Пространства имён же всегда существуют только в одном экземпляре, и этим они идеально подходят для документирования интерфейсов, т.к. понятия "экземпляр" и "внутреннее состояние" для интерфейсов не имеют смысла.

Важное замечание. Возможно, вам понравилась идея с совмещением определения переменных из пространств имён с их объявлением. Тогда сразу нужно вас предостеречь. Во-первых, глобальные переменные — это вообще зло (несмотря на то, что их активно используют во многих существующих проектах, они плохо соответствуют принципам модульности). Во-вторых, чтобы переменной можно было пользоваться из нескольких файлов, единственно правильный способ её описать — такой:

var.h
namespace Foo {
  namespace Bar {
    namespace Baz {
      extern int z;
    }
  }
}
var.c
#include "var.h"
int Foo::Bar::Baz::z = 0;
user1.c
#include "var.h"
void func1 () {
  Foo::Bar::Baz::z = 1;
}
user2.c
#include "var.h"
void func2 () {
  Foo::Bar::Baz::z = 2;
}

В файле «var.h» переменная «Foo::Bar::Baz::z» только объявляется, то есть вы заявляете, что она существует где-то. В файле «var.c» эта переменная определяется, то есть вы заявляете, что в данном месте нужно отвести память для её хранения.

Если бы мы определили переменную сразу в файле «var.h» (не написав слово «extern»), память под неё была бы отведена в каждом файле, в который включается «var.h». В нашем случае — память была бы выделена два раза. Но как у одной переменной может быть две разных области памяти? Никак. И редактор связей выдаст вам соответствующую ошибку: «Идентификатор Foo::Bar::Baz::z определён несколько раз». Это является прямым следствием открытости пространств имён.

Неименованные пространства имён[править]

Да, есть и такие. Собственно, странного в этом ничего нет. Пространство имён является неким контейнером для имён сущностей, а имеет ли сам контейнер имя или нет, на его функции не особо-то и влияет. Наличие имени не более чем позволяет ссылаться на обозначаемую им сущность. Неименованные пространства имён, не имея собственного имени, просто не позволяют на них ссылаться, однако свои функции при этом исполняют исправно.

Кажется странным, зачем такое нужно. Ведь если невозможно на него ссылаться ввиду отсутствия имени, нельзя и получить доступ к его содержимому. Однако у неименованных пространств имён есть две особенности, которые делают их небесполезными.

  1. Неименованное пространство имён единственно для единицы трансляции и уникально для каждой из них. Другими словами, открывая каждый раз неименованное пространство имён для дополнения его контентом, мы ссылаемся на одну и ту же область видимости, но при этом в каждой единице трансляции такая область видимости своя собственная, и её контент никак не коррелирует с другими единицами.
  2. Неименованное пространство имён неявно всегда видимо в текущей единице трансляции.

Фактически следующая конструкция

namespace
{
/* ... */
}

аналогична

namespace Некое_Уникальное_Имя
{
/* ... */
}
using namespace Некое_Уникальное_Имя;

где Некое_Уникальное_Имя единственно и неповторимо для единицы трансляции, компилятор должен сам гарантировать их уникальность для каждой. Собственно Стандарт языка именно так неименованные пространства имён и определяет.

Можно заметить простую аналогию. Неименованные пространства имён позволяют создавать глобальные сущности с областью видимости единицы трансляции, а не приложения. Т.е. их можно считать заменителем для static. Но это упрощённая аналогия. Во-первых, неименованные пространства имён могут иметь окаймлящей областью видимости не обязательно глобальную, ею может быть и любое именованное пространство имён. Во-вторых, static не может использоваться для типов, включая структуры/классы/объединения, а также typedef-определения, а неименованные пространства имён вполне могут таковые в себя включать. В прежнем Стандарте языка, C++03, существовало ещё одно отличие: static-сущности были internal linkage, тогда как сущности из неименованных пространств имён — external linkage, в новом же C++11 этой разницы больше нет, они все имеют internal linkage. Как следствие бывшей разницы: аргументами шаблонов не могли быть static-сущности, т.к. ими могли выступать только сущности с внешним связыванием, но могли сущности из неименованных пространств имён; как следствие нынешнего отсутствия разницы: аргументами шаблонов могут быть и те, и другие, т.к. в новом Стандарте языка сущности с внутренним связыванием могут выступать в качестве аргументов шаблонов.

Пространства имён и инкапсуляция интерфейсов[править]

Зачастую в учебных пособиях роль пространств имён преподносится как простая возможность избавится от конфликтов имён в глобальной области видимости. В общем-то это так, но это является весьма частным случаем их применения, фактически очень прозрачным следствием. Ранее было показано, что пространства имён в C++ предназначены главным образом для инкапсуляции интерфейсов. Действительно, предположив, что математическая абстракция "комплексное число" может быть адекватно отображена на C++ просто классом, мы столкнулись бы с рядом неудобств. Нетрудно написать что-то вроде

class complex
{
  double re;
  double im;
 
public:
  complex(double, double = 0.0);
  complex operator+=(const complex&);
/* ... */
};

Инкапсуляция методов позволяет чётко документировать интерфейс нашего класса, вопросов нет. Однако одной только арифметикой комплексные числа не исчерпываются. Для них вполне определены и элементарные функции, для них было бы удобным иметь перегруженные операции ввода/вывода и т.п. Но мы как-то привыкли писать sin(x) вместо x.sin(), а библиотека потоков ввода/вывода оперирует глобальными перегруженными операторами, а отнюдь не методами классов. Делать нечего, ради удобства использования без глобальных сущностей не обойтись.

Совсем другое дело, если инкапсулировать интерфейс не в класс, а в пространство имён.

namespace complex_numbers
{
 
class complex
{
  double re;
  double im;
 
public:
  complex(double, double = 0.0);
  complex operator+=(const complex&);
/* ... */
};
 
complex sin(const complex&);
double  abs(const complex&);
/* ... */
 
enum implForms {Alg, Exp, Trig};
enum implAngle {Deg, Grd, Rad };
 
template <typename Ch, typename Tr>
std::basic_ostream<Ch, Tr>& operator <<(std::basic_ostream<Ch, Tr>&, implForms);
template <typename Ch, typename Tr>
std::basic_ostream<Ch, Tr>& operator <<(std::basic_ostream<Ch, Tr>&, implAngle);
template <typename Ch, typename Tr>
std::basic_ostream<Ch, Tr>& operator <<(std::basic_ostream<Ch, Tr>&, const complex&);
 
/* ... */
 
const complex i = complex(0, 1);
}

Несложно видеть, как пространство имён изящно решает эту проблему. Интерфейс нашей сущности состоит из класса (одного или более), свободных функций, переменных, типов, но в то же время совершенно не захламляет глобальную область видимости и чётко обозначает границы распространения влияния имён принадлежащих интерфейсу сущностей. Так, никакое другое имя sin или i более не будет конфликтовать с complex_numbers::sin или complex_numbers::i.

Итак, цель достигнута. Интерфейс оказался чётко ограниченным, все имена локализованы. Но ведь это значит, что теперь придётся имя любой сущности из этого интерфейса квалифицировать именем этого интерфейса, т.е. именем пространства имён. По-хорошему так и надо делать. Не стоит пытаться избавляться от такой квалификации только потому, что так получается длинно и на первый взгляд неудобно. Может быть и неудобно иногда, пишущему или читающему. Но тот, который сталкивается с изучением кода в большом проекте, где ваш интерфейс суть всего лишь маленькая его подзадачка, скорее наоборот скажет вам большое спасибо. К примеру, видя перед собой std::string вместо просто string, уже не встанет вопрос о том, что это такое, то ли стандартная STL-строка, то ли некий собственный велосипед.

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

void foo()
{
  namespace CM = complex_numbers;
 
  using CM::complex;
/* ... */
  double d = b*b - 4.0*a*c;
 
  complex x1 = (-b + CM::sqrt(d)) / (2.0*a),
          x2 = (-b - CM::sqrt(d)) / (2.0*a);
 
  std::cout << CM::Trig << CM::Deg << x1 << '\t' << x2 << std::endl;
/* ... */
}

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

Несмотря на то, что квалификация более полезна, чем неудобна, тем не менее, когда целевые сущности явно оговорены (например, в документации) или локализованы (например, внутри функции), то явная квалификация уже перестаёт играть настолько важную роль. Поэтому для подавления обязательности явной квалификации имеются средства. Ключевое слово «using» — одно из них.

Ключевое слово using в контексте инкапсуляции интерфейсов[править]

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

#include <iostream>
 
using std::cout;
using std::endl;
 
int main () {
  cout << "Hello, world!" << endl;
}

Эта конструкция называется using-объявлением. Здесь имена cout и endl были внесены в глобальную область видимости, что позволило обойтись без std при их использовании. В принципе подобные имена действительно весьма редко используются в других интерфейсах, тогда как интерфейс std используется наоборот, очень часто. Формально такое внесение часто оправдано. Однако не всегда. using-объявление называется так не случайно. Дело в том, что она имеет семантику именно что объявления. Другими словами теперь в глобальной области видимости объявлены имена cout и endl так, как будто бы (ну почти, есть некоторая разница, но не весьма существенная) они там определены изначально. Это играет свою роль в точности так же, как и любые другие объявления. Например:

//double f;       // точка 0
namespace X
{
int   f(int);
float f(float);
}
 
using X::f;       // точка 1
 
namespace X
{
short f(short);
}
                  // точка 2
double f;         // точка 3

В точке 1 в текущей области видимости переобъявлены оба имени f из пространства имён X. Т.к. пространства имён открыты, то всегда, в частности и позже такого переобъявления, их можно дополнить, что и осуществляется чуть ниже. Однако это никак не отразится на списке (пере)объявленных ранее имён, так что в точке 2 в текущей области новое перегруженное имя не появится. Что же касается точки 3, то с ней вообще всё плохо. Функции-то могут быть перегружены, но имя переменной по-любому не может перегружать что-либо, так что там будет банальная ошибка компиляции из-за конфликта с именами, внесённых в точке 1. И наоборот, если перенести определение из точки 3 в точку 0, то теперь уже using-объявление в точке 1 вызовет точно такую же ошибку.

Итог: использование using-объявлений следует делать как можно более локальными. Иначе от пространств имён не будет никакого толку. Т.о. исходный пример лучше было бы написать вот так:

#include <iostream>
 
int main () {
  using std::cout;
  using std::endl;
 
  cout << "Hello, world!" << endl;
}

Здесь текущей областью видимости является блок функции, а не глобальная, так что влияние переобъявления тоже локализовано, и более не мешает ничему снаружи функции main().

Во-вторых, можно указать на "переобъявление" пространства имён целиком. (Обратите внимание на кавычки в предыдущем предложении, они там не случайно.) Достигается это т.н. «using»-директивой. Примерно так:

mytypes.h
namespace MyFavouriteTypes
{
typedef unsigned char byte;
}
main.c
#include "mytypes.h"
 
using namespace MyFavouriteTypes;
 
int main () {
  byte data [16];
/* … */
}

С помощью директивы «using namespace» вы говорите, что «дальше я буду ссылаться на упомянутое пространство имён без указания его имени». Часто можно услышать, что это просто сокращённый вариант целой стопки «using»-директив, по одной для каждого имени из пространства имён. На самом же деле это не так, и разница куда более существенная. «using»-директива ничего не переобъявляет, она именно что даёт указание компилятору, чтобы тот при случае заглядывал в указанную область видимости в процессе связывания имён. И ничего более. При этом если и обнаруживается конфликт имён, то при прочих равных условиях (но только при прочих равных) подсмотренные имена имеют меньший приоритет перед остальными и просто не принимаются во внимание.

Теперь, если в приведённом выше примере попробовать изменить using X::f на using namespace X, то в точке 2 будут видны все три функции, а компиляция в точке 3 (или 1, если перенести её в точку 0) пройдёт успешно. Тем не менее использование имени f всё равно будет вызывать ошибку из-за конфликта имён, разрешать который придётся явно и вручную (конечно же явной квалификацией). Поэтому и эту директиву имеет смысл локализовывать, например внутри функции. Тогда её действие распространяется только на эту функцию:

#include "mytypes.h"
 
int main () {
  using namespace MyFavouriteTypes;
  byte data [16];
/* … */
}

В заключение следует отметить, что многие пишут «using namespace std» и радуются, но, возможно, пример, приведённый выше, демонстрирует несколько лучший стиль программирования.

Внимание, правило. Никогда не пишите «using» в заголовочных («.h») файлах. Это лишает тех, кто их включает (#include), возможности самим решать, нужно ли им «using» или они хотят использовать полное имя. Из этого правила есть только одно исключение, которое мы сейчас и рассмотрим.

Интерфейсы, представляя методы взаимодействия подзадач друг с другом, естественно зачастую друг с другом связаны. Некий интерфейс может использовать другой интерфейс для своих нужд. В частности некая сущность одного интерфейса может использоваться как часть другого. В этом случае явное на то указание путём using-объявления в принципе вполне вписывается в парадигму ИОП (Интерфейсно-Ориентированного Программирования). Также вполне можно представить ситуацию, когда некий интерфейс целиком предназначен для использования другим интерфейсом, или же этот другой является его расширением, новой версией, например. В таком случае и using-директива будет вполне на своём месте. Эти (и только эти) ситуации т.о. позволяют использовать using в заголовочных файлах. Однако обратите внимание, что даже в этих случаях using оказывается локализованной в области видимости некоего пространства имён.

Связывание имён и ADL[править]

Любая сущность в программе может иметь имя. А может и не иметь, язык в ряде случаев допускает существование неименованных сущностей. Но ежели имя дано, то на эту сущность впоследствии можно ссылаться. Когда компилятор встречает в программе некое имя, он первым делом определяет, какая сущность за ним скрывается, и этот процесс и называется связыванием. Процесс связывания имён в C был довольно прост. Однако выразительное богатство C++ сделало этот процесс гораздо более сложным. Тем не менее он включает правила связывания языка C как составную часть.

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

Рассмотрим все варианты по порядку.

Связывание неквалифицированных имён[править]

Когда у имени нет квалификации, компилятор сталкивается с наиболее общим случаем связывания: ему требуется ещё определить область видимости, в которой такое имя объявлено. Общее правило просто: поиск начинается с текущей области видимости, и если в ней такого имени не обнаружено, компилятор переходит к окаймляющей области и продолжает поиск, считая текущей теперь её; поиск на каждой итерации продолжается однотипно и останавливается в двух случаях — если окаймляющей области видимости не существует, либо если найдено хотя бы одно имя. Первый вариант остановки обычно (однако не всегда) означает, что достигнута глобальная область видимости, и коли имени так и не нашлось, регистрируется ошибка. Второй вариант означает, что имя найдено (однако это не означает, что ошибка не будет зарегистрировано, ибо само использование имени может нарушать семантику языка, например, использование имени типа как аргумента в арифметической операции). Даже если в окаймляющей области видимости могут быть обнаружены такие же имена, они уже не рассматриваются из-за остановки поиска, компилятор просто их не увидит, и тем самым реализуется механизм сокрытия локальными именами имён из окаймляющих областей видимости.

Однако из этого общего правила имеются исключения. Одно из них связано с ADL и рассмотрено ниже. Другое же обычно воспринимается как само собой разумеющееся, потому что интуитивно, однако без специальных действий со стороны компилятора не имевшего бы места: если имя является операцией, которая потенциально перегружаема, а её операндом является класс, область видимости этого класса также включается в поиск. Если операция бинарная, оба операнда рассматриваются на равных, и в итоге могут быть включены области видимости их обоих. Т.о. неквалифицированное имя потенциально перегружаемой операции может заставить компилятор выполнить поиск по трём разным направлениям: из текущей области видимости, из области видимости класса первого операнда и из области видимости класса второго операнда. Каждый поиск выполняется по тем же правилам, и все их результаты объединяются. Разумеется, ошибка не обязательно будет зарегистрирована, если по какому-то направлению искомого имени найдено не будет. Ошибка будет, только если результат объединения покажет пустое множество, иначе же связывание пройдёт успешно. В целом это позволяет перегружать операции как методы классов, но использовать их в выражениях на общих основаниях, т.е. без явной квалификации, и при этом также успешно учитывается наследуемость перегруженных операций. Фактически только благодаря этому для операторов-методов возможно использовать привычный синтаксис a + b вместо a.operator(b), в противном случае бывшего бы единственно возможным.

Связывание полностью квалифицированных имён[править]

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

С именами из областей видимости классов вообще не всё так просто. Поначалу компоненты пути могут представлять имена пространств имён. Но если с некоей позиции маршрута компонентом пути встретилось имя класса, то последующими компонентами могут быть только базовые или вложенные классы. И только самым последним компонентом квалифицированного имени (собственно искомым именем) может являться любая сущность класса. Вернуться же обратно к областям видимости пространств имён более невозможно. Любопытно, что вложенные классы по-прежнему ведут к внутренним кольцам областей, тогда как базовые наоборот, к внешним. Но ведь это означает совершенно разные направления! Смотрите:

struct A1
{
  struct B1
  {
    static int x;
  };
};
 
struct B2
{
  static int x;
};
struct A2: B2
{
};
 
::A1::B1::x;
::A2::B2::x;

Из этого следует, что компилятор не просто должен идти по указанному маршруту, он обязан связывать каждое имя маршрута, чтобы выяснить, что оно обозначает, и т.с. правильно расшифровать семантику квалифицированного имени.

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

void g();
 
struct A
{
  void f();
};
 
struct B: A
{
  void f();
};
 
int main()
{
  A  a;
  B *b = new B;
 
//a.g();
  a.f();
  b->f();
  b->A::f();
 
  delete b;
}

Здесь полная квалификация имени подразумевает, что над областью видимости самого базового класса для самого внешнего класса больше нет никаких окаймляющих, поэтому выйти на уровень областей видимости пространств имён невозможно. Так что если раскомментировать строку с вызовом якобы метода g() класса A для экземпляра a, компилятор выдаст ошибку, а не попробует вызвать глобальную функцию g().

Разумеется эти два способа полной квалификации можно комбинировать. В уже приведённом примере операция :: использовалась для перехода в окаймляющую область видимости A — класса, базового для B, чьим экземпляром является b, и в результате был вызван нестатический метод. Аналогично, начиная с некоторого компонента пути от глобальной области видимости, могут быть встречены операции . и -> в маршруте, и стало быть далее будут рассматриваться экземпляры классов или указатели на них, что позволяет использовать их нестатические поля и методы.

Несмотря на описательную сложность связывания квалифицированных имён, процесс их связывания на самом деле проще и интуитивнее, т.к. не требуется выполнять поиск. Маршрут однозначно указывает, где и какое имя должно быть видимо, и если это вдруг не так — вариант один: ошибка компиляции. Это справедливо даже для имён, видимых посредством using-директив. И это естественно, ибо иначе квалификация имени не способна была бы разрешать неоднозначности, если некое имя доступно по разным маршрутам. И также естественно, что эта оговорка не относится к именам, внесённым посредством using-объявлений, ибо они имеют семантику объявлений, а не просто видимы, да и компилятор не допустит конфликтов, ругаясь на повторные объявления имён. Но и тут имеется исключение: если именем является сущность в области видимости класса, то её имя после прохождения по всему маршруту далее рассматривается как неквалифицированное, а текущей областью видимости считается конец маршрута. И в дальнейшем компилятор выполняет его связывание сообразно правилам предыдущего подраздела. Это позволяет ссылаться на имена в производных классах, явно таковых не имеющих, но имеющих их унаследованными из базовых. Но и в этом случае область видимости самого базового из самого внешнего класса считается не имеющей окаймляющей.

Связывание неполных квалифицированных имён[править]

Неполные квалифицированные имена — это все остальные. По факту таковыми остались только те, которые аналогичны полностью квалифицированным, но начинающиеся не с ::, а с идентификатора. Можно провести аналогию с маршрутами к файлам в файловых системах: полностью квалифицированные имена представляют собой абсолютные маршруты, не полные — относительные. Аналогия, однако, неполная: относительный маршрут не обязательно начинается с текущей области видимости.

На самом деле всё просто. Первый компонент маршрута компилятор связывает по правилам неквалифицированного поиска, а все остальные компоненты маршрута - по правилам полностью квалифицированного, начиная с только что найденной как будто бы с глобальной. Собственно всё, тут даже добавить нечего. Тем не менее следует пояснить пару моментов.

Во-первых, если вы были достаточно внимательными, то заметили в примере с b->A::f(); некоторую неувязку: здесь после -> упоминается имя A, которого, во-первых, нет в текущей области видимости, во-вторых, оно обозначает не объект, а тип, в третьих, после него через операцию :: упомянуто имя нестатического метода. Как-то это не очень вяжется с указанными ранее правилами. Да, описание правил было упрощено, ибо, как уже говорилось, связывание имён - это один из наиболее ёмких аспектов C++, и если его описывать тезисами строго из Стандарта языка, то эта статья вряд ли влезла бы в рамки учебника. Тем не менее, упрощённо эту неувязку можно попробовать объяснить, не отрицая однако сказанного. Операция :: имеет более высокий приоритет, а значит выполняется до ->, поэтому компилятор сначала связывает A и f (тоже по правилам поиска неполных квалифицированных имён), и т.к. разбор имени ещё не закончен, «ошибка» использования нестатического элемента класса в операции :: пока не детектируется, т.к. не исключено, что полный контекст его использования попадает под разрешающее такое его использование исключение, и только затем компилятор, продолжая разбор исходного неполного квалифицированного имени, связывает b и только что полученную сущность, а тут уже использование (только что связанного имени) нестатического метода допустимо.

Во-вторых, есть интересное следствие из описанных правил, которое лежит на поверхности, однако мало кто обращает на него внимание. Следует сразу сказать, что и тут описание упрощено, иначе пришлось бы разговаривать о (автоматическом) внесении имён в область видимости класса. Пока обойдёмся без этого. Рассмотрим пример:

struct A
{
  void f();
};
 
struct B: private A {};
 
struct C: public B
{
  void g()
  {
          f();                          // 1
       A::f();                          // 2
       B::f();                          // 3
    B::A::f();                          // 4
 
      A().f();                          // 5
    ::A().f();                          // 6
 
    static_cast<  A*>(this)->f();       // 7
              ((  A*) this)->f();       // 8
    static_cast<::A*>(this)->f();       // 9
              ((::A*) this)->f();       // 10
  }
};

Мы видим у B приватный базовый класс A, который в C уже полностью недоступен. Причина такой жёсткой инкапсуляции может быть какой угодно, не суть важно, главное то, что B т.о. закрыл доступ к своему подобъекту A. Но что делать C, если ему требуется работать с экземплярами класса A? Ведь со стороны B глупо закрывать доступ к любым экземплярам A, его интересов касается только его собственный подобъект, а не вообще всевозможные объекты этого типа. Почему же C не должен иметь возможности работать с экземплярами A просто через их публичный интерфейс, если они никак не связаны с this? Строки 1-4 демонстрируют безуспешные попытки добраться до публичного интерфейса A, и их безуспешность правильна: неполное квалифицированное имя метода f() видимо, успешно связывается, но в этих точках недоступно для использования.

Хорошо, а вообще можно ли создать новый экземпляр A? В точке 5 снова получаем недоступность, что не должно удивлять, ибо неквалифицированный поиск имени A по-прежнему идёт по маршруту через B транзитом… и тут возникает идея использовать другой маршрут к A: снаружи, из области видимости пространства имён. Точка 6 успешно решает задачу создания экземпляра класса A и демонстрирует возможность работать с ним посредством его публичного интерфейса. Задача решена, класс C может работать с экземплярами A, если только они не часть самого C. Надо только получить доступ к имени класса A извне, а не унаследованным от B.

Или всё-таки… может и с ними? Можно ли преобразовать —- понятное дело явно, ибо неявные приведения типов компилятор отвергнет —- тип this в указатель на A? Точки 7 и 8 ожидаемо не компилируются, но и точка 9 тоже, только по другой причине: приведение к неоднозначному или недоступному базовому классу недопустимо, даже если это приведение явное, и уже неважно, что имя ::A само по себе доступно. И наконец апофеоз: точка 10 неожиданно оказывается законной. Только немногие знают, что заглянув в Стандарт языка, можно увидеть, что операция приведения типов в C-стиле может использоваться для преобразования указателей и ссылок на производный класс к недоступному базовому классу.

Это может выглядеть как ошибка в дизайне языка. На самом же деле эта „лазейка“ была оставлена для обратной совместимости, когда на заре развития C++ конструкторам или деструкторам требовались иногда нетривиальные действия (тогда нового стиля операций приведения типов ещё не было). Сейчас использование этой „лазейки“ невостребовано чуть менее, чем полностью, ибо возможности языка теперь дают всё необходимое безо всяких хитростей. Однако факт остаётся фактом: старый стиль преобразований типов имеет одну выгоду перед новым. И слава богу, что только одну и практически не востребованную.

ADL (Argument Dependent Lookup)[править]

Поиск имён, зависящий от типов аргументов, впервые был предложен Эндрю Кёнигом (Andrew Koenig) для решения следующей проблемы использования пространств имён. Рассмотрим пример:

namespace X
{
class Foo { /* … */ };
 
Foo operator+(const Foo&, const Foo&);
/* … */
}
 
X::Foo x, y;
 
/* … */
 
x + y;                // 1
X::operator+(x, y);   // 2

С точки зрения организации программы тут всё сделано правильно. Имеется интерфейс, опубликованный пространством имён X. Им предлагается класс Foo и перегруженная для него операция сложения. Пользователь интерфейса X не пожелал использовать using, и это не просто его право, как было сказано выше, неиспользование using без настоятельной необходимости только приветствуется. Вот только использование перегруженной операции весьма затруднено, ведь она расположена в области видимости, недоступной без явной квалификации. Поэтому пользователь вместо интуитивного и привычного синтаксиса использования операторов в точке 1 вынужден или использовать функциональный стиль, как в точке 2, или так или иначе применить using.

Эндрю Кёниг предложил для операторов ввести в язык расширение поиска при связывании, как это уже сделано для классов: если операндом операции выступает класс, следует заглянуть в связанное с ним пространство имён, выполнить поиск там и добавить найденное. Его предложение было внимательно рассмотрено в Комитете и не просто принято, а даже расширено. Т.о. "подглядывание" в связанные пространства имён выполняется не только для операторов, но и для обычных функций тоже. Основная причина такого решения в том, что если функция имеет аргументом некий тип, опубликованный неким интерфейсом, то естественно предположить, что и сама функция связана с этим интерфейсом, а значит очень вероятно тоже опубликована им. Действительно, вернёмся к интерфейсу complex_numbers. Упомянутые там функции sin() и abs(), принимающие параметром complex_numbers::complex, являются именно таковыми. Почему бы компилятору самому не сообразить, что коли функции передаётся аргумент, имеющий тип из области видимости complex_numbers, то и сама функция скорее всего определена там же?

Т.о. общее правило ADL таково: если функции, включая потенциально перегружаемые операторы, передаётся значение, чей тип определён в области видимости некоего пространства имён, то это пространство имён автоматически включается в список для поиска при связывании имени этой функции, даже если нет ни явной квалификации, ни using-директив или -объявлений; если у функции/оператора больше одного аргумента, каждый из них обрабатывается аналогично. В результате список просматриваемых пространств имён при связывании имени функции может оказаться довольно внушительным. Все найденные кандидаты равноправны, т.е. все найденные имена объединяются в единое множество кандидатов на перегрузку.

Следует отметить несколько фактов.

  • ADL применяется только к полностью неквалифицированным именам. Если имя квалифицированно хоть как-то, ADL не применяется. Это естественно, иначе квалификация была бы неспособна разрешать неоднозначности.
  • ADL можно подавить, заключив имя функции в круглые скобки. Т.е. встретив (abs)(CM::i) вместо abs(CM::i), компилятор не будет заглядывать в пространство имён CM, а ограничится при поиске имени abs только текущей областью видимости. Об этом есть явное упоминание в Стандарте языка, но на самом деле оно там необязательно. Отключение ADL для имён в () следует из остальной грамматики языка, однако это следствие неочевидно, потому оговорено явно для акцентирования внимания на этой возможности.
  • ADL конфликтует с using-директивой в том смысле, что имена, найденные посредством ADL и видимые посредством using-директивы, равноправны. Например:
namespace Y { class A {}; void f(A);    }
namespace X {             void f(Y::A); }
 
using namespace X;
 
Y::A a;
 
f(a);         // неоднозначность

Может сложиться впечатление, что ADL - это не более чем просто удобно. Ну за исключением операторов, там да, ADL даёт более ощутимое удобство, чем для функций. Однако нет. В контексте шаблонов ADL выходит на качественно новый уровень значимости. Абсолютно без преувеличения можно сказать, что без ADL использование шаблонов было бы ужасно неудобным и в ряде случаев просто невозможным.

Исключения[править]

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

Это так, но Си++ исключения могут быть вызваны только явным оператором throw. Все приведенные выше «различные обстоятельства» могут лишь быть сконвертированы в Си++ные исключения с помощью платформенно-зависимых функций, в итоге делающих соответствующий throwset_se_translator() в Win32.

Это отличает Си++ от managed языков Java и C#, где «различные обстоятельства» немедленно сами вызывают исключение на уровне языка.

Блок отлавливающий (try) и обрабатывающий (catch) исключения заданного типа выглядит так:

try
{
 ...
}
catch(MyException e)
{
 ...
}

Оператор throw имеет необязательный параметр — объект. Использование throw без параметра возможно только в catch-частях блоков, где оно равносильно «throw e;» — т.е. продолжению обработке этого же исключения с «размоткой» стека далее.

Оператор throw исполняется так: проход вверх по стеку вызовов функции, ищется ближайший «try», у которого catch-часть имеет тип объекта в операторе «throw», или же тип базового класса для этого объекта. Все более глубокие «try», у которых не отождествился тип, игнорируются и понимаются просто как блоки «{}».

Далее, после нахождения места возврата, происходит «размотка» (unwind) стека — возвраты из всех функций и блоков, для каждой }, пересекаемой в этом процессе, зовутся деструкторы всех объектов, которые успели проинициализироваться в этом блоке.

Операция заканчивается входом в правильную catch-часть места возврата. Объект e есть копия того, что было указано в «throw».

Если «throw» был вызван в одном из деструкторов во время «размотки» — это означает фатальный крах (вызов abnormal_termination()), средствами «try/catch» блоков это не улавливается.

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

Автоматическая проверка типа исключения убирает необходимость анализа кода ошибки и тому подобного в catch-части.

Возможно несколько catch-частей, каждая для своего типа — это сокращенная запись нескольких вложенных try-блоков.

Возможно catch(...) — поймать все исключения.

Возможно опустить имя объекта в catch — catch(MyException), это применяется, если информации о типе достаточно и содержимое объекта не нужно.

Любая функция может быть помечена спецификацией исключений:

void f() throw(CFileException, CAccessDeniedException);

Функция с такой пометкой может вызывать только те исключения, что перечислены в пометке. Если она зовет функции с более широкой спецификацией исключений, то все такие вызовы должны быть обернуты в «try/catch», где catch-часть возбуждает уже «легальное» для данной функции исключение.

Функция без такой пометки может вызывать любые исключения (в отличие от Java).

Пустая throw() означает, что функция не вызывает исключений.

Библиотека ввода-вывода (iostream)[править]

TODO (пока скипнуто, видимо будет)

Средства объектно-ориентированного программирования[править]

Основными механизмами в объектно-ориентированном программировании есть:

  • полиморфизм;
  • наследование;
  • инкапсуляция.

Инкапсуляция[править]

- объединение переменных и функций (свойств и методов) в классе.

Полем класса может быть функция:

class C
{
public:
 void f();
};

В этом случае её можно вызывать только для конкретного объекта этого классового типа:

f(); // вызовется глобальная f
C::f(); // ошибка
С obj;
obj.f(); // можно
C* ptr;
ptr->f(); // тоже можно

Такую функцию можно описать прямо внутри класса, это означает то же, что inline для глобальных функций:

class C
{
public:
 void f()
 {
   printf("We are %p\n", this);
 }
};

Но можно её описать (т.е. написать тело) и ниже, например, в .cpp при помещении объявления класса в .h:

class C
{
public:
 void f();
};
 
void С::f()
{
  printf("We are %p\n", this);
}

Такие функции называются «функции-члены» или «методы».

Так как метод всегда зовется для какого-то объекта, ему всегда передается адрес этого объекта. Этот указатель доступен в теле метода как ключевое слово this. Типа псевдо-переменной this — C* const, если метод описан без слова const в конце, и const C* const — иначе:

class C
{
 int i;
public:
 void f();
 void fc() const;
};
 
void С::f()
{
  this->i = 0; // работает
}
 
void С::fc() const
{
  this->i = 0; // ошибка, this указывает на константу, все её поля тоже константы
}

Т.е. метод со словом const не может изменять свой объект.

Кроме того, в методах не обязательно писать this->f, достаточно просто написать "f".

Имена, примененные в методе, ищутся: а) по {} блокам в самом методе, формальные параметры есть часть самого внешнего блока;

б) поля класса, если нашлось такое — то понимается как this->name; в) глобалы.
class C
{
 int i;
public:
 void f();
};
 
void С::f()
{
  i = 0; // это this->i
}

static поля и методы[править]

Вот тут var — не поле класса, а просто такая хитрая глобальная переменная:

class C
{
public:
 static int var;
};

Её надо обязательно где-то описать, в классе она лишь объявлена:

int C::var = 1;

Отличия от глобала: а) для нее действуют метки private/protected/public;

б) имя объявлено в пространстве имен класса, а не в глобальном пространстве имен.

Точно так же statfunc — не настоящий метод, а просто такая функция:

class C
{
public:
 static void statfunc();
};

Описывается она так же, как и метод, но в ней не бывает this и она не может прямо использовать имена нестатических (настоящих) полей и методов класса.

Вызвана она может быть: а) из любого метода, и настоящего, и статического, как statfunc();

б) извне класса как C::statfunc, если позволяют метки доступа;
в) как obj.statfunc() или pobj->statfunc(), в этих случаях obj и pobj
 не более чем указывают на тип и не используются при вызове.

Права доступа к именам в классах[править]

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

Единственное отличие struct от class в том, что по умолчанию до первой такой метки объявления считаются private в class и public в struct.

Все, что объявлено private, может быть использовано только в контексте данного класса — внутри кода методов данного класса, в инициализаторах статических полей-данных и т.д. Эти сущности нельзя использовать даже в потомках, не говоря уже о внешнем коде.

Все, что объявлено public, может быть использовано где угодно.

Контролируется доступ, а не видимость — если что-то объявлено private, то имя не будет пропущено при отождествлении, будет честно найдено и будет често выдана ошибка доступа. Таким образом, изменение расстановки меток доступа не может сделать правильный код по-прежнему правильным, но имеющим другой смысл.

То, что объявлено protected, может быть использовано как в контексте данного класса, так и в контексте его непосредственных наследников, более нигде.

Это есть инкапсуляция, т.е. комплекс мер, дающих гарантию, что авторы внешнего кода не используют приватные детали реализации классов и не попадают от них в зависимость, что облегчает переделку классов — уменьшаются шансы сломать внешний код.

Наследование[править]

Наследование — это помещение в производный класс (который наследует) свойств и методов базового класса (которого наследуют).

Возможно:

class B
{
 int fb;
};
class D : public B
{
 int fd;
};

Это означает, что в объекте класса D есть все поля класса B (fb), плюс ещё и дополнительно объявленные, специфичные поля для D (fd). Концептуально это означает, что любой объект класса D — это, помимо всего прочего, ещё и полноценный объект класса B. Это и есть наследование.

Кроме того, указатель D* там, где это нужно, молча автоматически приводится к B*. Обратное приведение тоже возможно, но только с явным оператором приведения. Использовать можно только тогда, когда есть уверенность, что данный B* действительно указывает на B-часть в D.

Таким образом, любой объект D есть разновидность объекта B, такая, что к ней добавлено что-то ещё.

Класс B называется базовым, D — производным.

Возможно указать несколько базовых классов (т.н. «множественное наследование»):

class B
{
 int fb;
};
class B2
{
 int fb2;
};
class D : public B, public B2
{
 int fd;
};

Тут в объекте D будет подобъекты B и B2, и три поля: fb, fb2 и fd.

Преобразование указателей от D* к B* работает и здесь. При этом, если преобразование к единственной (и вообще к первой) базе есть умозрительная операция внутри компилятора, не генерирующая кода, то преобразование ко второй и далее базе — D* к B2* — означает генерацию кода, который прибавит к указателю значение смещения B2 внутри D.

Аналогично работает и приведение «вниз» — от B2* к D*.

Возможно использовать и приватное наследование — class D : private B. В этом случае вся B-часть класса D доступна только из контекста класса D, т.е. аналогична тому, что объявлено private в классе B. Даже приведение указателя к B* возможно только в контексте класса D — приватный базовый класс считается деталью реализации, неизвестной внешнему миру.

В публичном же случае вся публичная часть B становится публичной частью D, а protected и private части В — приватными частями D.

Полиморфизм[править]

Полиморфизм — вызов виртуальной функции производного класса через указатель на базовый.

Представим себе:

class B
{
public:
 void f();
};
class D : public B
{
public:
 void f();
};
 
и далее:
 
B* pb = new D;
pb->f();

то в данном случае решение о том, какую функцию звать — B::f или D::f — принимается на основе известного компилятору типа указателя, т.е. B::f.

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

Такое требует специальной поддержки во время выполнения, ибо компилятор, увидев B*, не знает, есть ли это объект, созданный как B, или же B-часть в объекте класса D.

Эта возможность есть, она выглядит так:

class B
{
public:
 virtual void f();
};
class D : public B
{
public:
 void f();
};

В этом случае pb->f() будет исполнять совсем иной код. Обычно этот код находит указатель на таблицу виртуальных методов в начале объекта и выбирает функцию из этой таблицы. Указатель же на таблицу зависит от того, как создавался объект B — как подобъект в D или же непосредственно как B. Заполнение этого указателя обычно делается специальным кодом, автоматически сгенерированным внутри конструктора.

Достаточно объявить метод как virtual на вершине дерева наследования, где он встречается впервые. Писать virtual в потомках уже не обязательно, хотя и можно (ничего не делает, все равно virtual).

Допустимо ещё и такое:

class B
{
public:
 virtual void f() = 0;
};

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

Класс, в котором не объявлено ничего, кроме чисто-виртуальных функций, совпадает с понятием «интерфейс» в других языках программирования. На такой класс можно объявлять указатели и потом звать через них эти виртуальные методы, не зная на самом деле, что именно позовется. На таком основана технология COM фирмы Microsoft (а равно все дизайн-паттерны «банды четырех»).

Явная квалификация pb->D::f() пересиливает виртуальный механизм вызова, зовется D::f.

Полиморфизм и множественное наследование[править]

struct B
{
 virtual void b();
};
 
struct B2
{ 
 virtual void b2();
};
 
struct D : public B, public B2
{
 void b();
 void b2();
};
 
void D::b2()
{
 // здесь this указывает на D
}
 
D d;
B2* pb2 = &d; // автоприведение вверх по решетке наследования к не-первой базе, к значению
              // адреса D добавилось смещение
 
// в другом файле
 
pb2->b2();    // а здесь компилятор не знает, что pb2 на самом деле указывает на B2-часть
              // в D, потому он действует в лоб, а именно генерит что-то вроде
              // (pb2->__vtbl[0])(pb2);
              // Все бы хорошо, но вызвать в итоге надо D::b2, а у нее this - это D*, а не B2*,
              // приведение требует вычитания смещения, а где его делать?
              // Поступают так: компилятор генерит "переходничок", который приводит B2 к D
              // и потом зовет D::b2, и именно адрес этого "переходничка" и кладут в __vtbl

См. ассемблерный код, сгенеренный при использовании библиотеки Microsoft ATL — там такое на каждом шагу.

Конструктор[править]

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

Эта возможность реализуется конструктором.

Синтаксически конструктор есть метод, имя которого совпадает с именем класса:

class C
{
 int i;
public:
 C(int p)
 { 
  i = p; 
 }
};

Создание объекта такого класса обязательно требует указания параметра типа int, который помещается в поле i объекта.

Может быть несколько (совместно используемых) конструкторов, отличающихся параметрами, как и любые совместно используемые функции.

Конструктор является специальной функцией, у него может, например, быть сгенерирован не такой пролог/эпилог, как у обычных функций. Потому конструктор: а) не возвращает значения, даже void и б) нельзя брать адрес конструктора.

Объект классового типа может возникнуть в коде как:

  • глобал

Конструкторы глобалов зовутся до входа в main платформо-зависимым кодом.

  • локал

Конструкторы локалов зовутся явно по ходу исполнения операторов в блоке. Синтаксис для глобалов и локалов:

MyClass c;
MyClass c1(); // одно и то же, используется конструктор без параметров - УЖЕ НЕТ в новом
              // стандарте языка! для явно заданного конструктора таки одно и то же, но
              // при отсутствии конструкторов эти записи различны тем, делается ли заполнение
              // нулями или нет. См. value и default initialization в описании языка.
              // Язык изменен для того, чтобы T(), которое используется в std::string<T> как
              // завершающий символ строки, всегда заполнялось нулями
MyClass c2(1, 2); // зовется конструктор с 2мя параметрами целочисленных типов
MyClass c3 = MyClass(1, 2); // строго говоря, это создание временного объекта с копированием
                            // в c3, но компилятор имеет право исключать временные объекты,
                            // потому это то же, что и c2
MyClass c4(1);
MyClass c5 = MyClass(1);
MyClass c6 = 1;  // три равносильные записи, последняя использует преобразование типа посредством
                 // конструктора из int во временный объект MyClass, который потом копируется
                 // в c6 - временный объект исключается компилятором

В С++ есть ключевое слово explicit, позволяющее запретить неявное преобразование аргумента конструктора. Применяется только к конструкторам.

class MyClass
{
  int i;
public:
  explicit MyClass(int r)
  {
    i = r;
  }
};
 
int main()
{
  MyClass A(9); // правильно
  MyClass B = 9; // ошибка компиляции, неявное преобразование запрещено explicit-конструктором
 
  return 0;
}

Вообще, если конструктор имеет 1 параметр, рекомендуется использовать explicit, т.к. неявное преобразование типов потенциально опасно.

  • создание по new

Создание массива по new обязательно использует конструктор без параметров. Иначе же:

 MyClass* obj = new MyClass(1, 2);

Скобки круглые (квадратные скобки - массив) и идут за именем класса (скобки за словом new передают параметры в operator new, а не в конструктор).

  • поле класса или подобъект базового класса.

Для таких случаев в синтаксисе конструктора предусмотрен так называемый ctor-инициализатор (список инициализации):

class B
{
public:
 B(int);
};
 
class D : public B
{
 B fb;
public:
 D();
}
 
D::D()
 : B(1), fb(2)   // это и есть ctor-инициализатор (список инициализации), сначала
                 // для подобъекта базы, потом для поля fb
{
}

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

  • временный объект

Явный синтаксис для временного объекта — MyClass(1, 2). Это выражение, его значение есть временный объект типа MyClass, проинициализированный вызовом MyClass::MyClass(1, 2).

Также временные объекты создаются компилятором для передачи параметров классовых типов в функцию по значению, и возврата таких значений из функций.

Также временный объект создается для исполнения оператора приведения к типу (MyClass), если тип назначения классовый. То же самое может быть сделано и неявно, если требуется объект данного классового типа, а значение выражения какого-то иного типа - ищется подходящий конструктор, и делается временный объект. Это т.н. «преобразование типа посредством конструктора».

Таким образом мы получаем, что (MyClass)1 и MyClass(1) — строго одно и то же.

Более того, Си++ не делает разницы в синтаксисе для классовых и неклассовых типов, т.е. синтаксис long(1) вполне возможен и опять же есть то же самое, что и (long)1. Точно так же можно:

 long i = 1;
 long i(1); // одно и то же

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

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

Конструкторы не могут быть виртуальными, т.е. вызываемыми тем механизмом, что и virtual функции. Понятие "виртуальный конструктор" означает совсем иное (паттерн factory, реализованный на объекте типа type_info).

В конструкторах невозможны классические способы обработки ошибок (через код ошибки), возможно лишь: а) throw; б) никогда не писать способный к отказу код в конструкторе, заведя отдельный метод Init.

Конструктор без параметров называется «конструктор по умолчанию». Если у класса таковой не написан явно, и если при этом у класса нет вообще никаких явно написанных конструкторов - то компилятор генерирует C::C() автоматически, сгенеренный конструктор зовет такие же конструкторы у всех базовых классов и полей. Неклассовые поля не инициализируются.

Если же у класса есть поля, обязательно требующие ctor-инициализатора (базы без конструктора по умолчанию/такие же поля/ссылки) — то такая генерация невозможна, и требуется обязательное написание конструкторов явно.

Конструктор с 1 параметром типа «ссылка на этот же класс» — C::C(C&) — называется «конструктор копирования». Если у класса таковой не написан явно, то компилятор генерирует его автоматически (независимо от наличия каких-то ещё конструкторов), сгенеренный конструктор зовет такие же конструкторы у всех базовых классов и полей. Неклассовые поля копируются побитово.

Конструктор копирования может вызываться компилятором неявно для инициализации временных объектов, а также передачи по значению параметров вызовов и возвращаемых значений. Он же зовется при создании объекта с 1 параметром, когда параметр есть объект того же класса.

Следует отличать C::C(C&) и C::operator=(C&). Вторая функция вызывается для уже построенного объекта слева, конструктор же копирования — нет, он и строит этот объект (разница инициализации и присваивания).

Деструктор[править]

В Си++ есть возможность гарантированного освобождения занятых объектом ресурсов при выходе объекта из области его жизни, т.е. гарантированное разрушение.

Для такого разрушения используется деструктор. Синтаксически это функция с именем ~MyClass:

class C
{
public:
 ~C();
};

Деструктор вызывается когда-то для любого объекта.

  • глобал — после main платформо-зависимым кодом
  • локал — по закрывающей } (включая переходы return/break/continue/goto, а также исключения)
  • по new — в delete или delete[]
  • поле или база класса — автосгенеренным кодом в прологе/эпилоге деструктора производного класса после исполнения явного тела этого деструктора
  • временный — когда-то по усмотрению компилятора.

Как и с конструктором, нельзя брать адрес деструктора и он не возвращает значений (даже void).

Деструкторы не наследуются. Если у класса не написан деструктор явно - он генерится компилятором, сгенеренный деструктор аналогичен пустому телу — ~C() {}.

Деструкторы могут быть виртуальными, и это рекомендуется практически всегда, кроме случаев, когда у класса вообще нет виртуальных методов.

Тело деструктора должно разрушить все поля объекта — позвать close(), CloseHandle, ->Release() и тому подобные функции.

Деструктор может быть объявлен private. Объекты такого класса могут создаваться только оператором new без квадратных скобок, любая другая попытка создания даст ошибку "разрушение невыполнимо-деструктор приватен". Такие объекты должны иметь метод разрушения, в котором будет исполняться delete this;

Обобщённое программирование (Generic programming)[править]

Введение, или альтернативные подходы[править]

Предположим, мы написали суперконтейнер (то есть тип, предназначенный для хранения) чисел типа int:

class IntContainer {
public:
  void add (int value);
  int index_of (int value);
  int get_count ();
  int get_value (int index);
private: 
  … … …
};

Однако нам грустно тратить силы, создавая суперконтейнеры для каждого типа данных, который мы собираемся использовать. Тем более, что их код будет похож, как две капли воды. Что же делать?

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

#define DEFINE_CONTAINER(T,N) \
  class N ## Container { \
  public: \
    void add (T value); \
    int index_of (T value); \
    int get_count (); \
    T get_value (int index); \
  private: \ 
    … … … \
  };

Это — длинное определение макроса «DEFINE_CONTAINER». Оно занимает несколько строк благодаря использованию «символа продолжения макроса на следующую строку» — «\». Символы «##» используются, чтобы соединить вместе аргумент «N» и слово «Container», получив одно слово — название определяемого класса.

Используется это так:

DEFINE_CONTAINER (int, Int)
IntContainer apples;
int main () {
  apples.add (11);
  apples.add (13);
}

Строка «DEFINE_CONTAINER (int, Int)» раскрывается в определение класса «IntContainer», который мы и используем далее. Если нам понадобится контейнер для другого типа, мы можем написать «DEFINE_CONTAINER (double, Double)», «DEFINE_CONTAINER (const char *, ConstString)». Получатся классы «DoubleContainer» и «ConstStringContainer».

Можно обойтись и без макросов. Мы могли бы создать файлы «FooContainer._h_» и «FooContainer._cpp_». В них можно было определить контейнер «FooContainer» для типа «foo». Далее, вы используете ваш любимый текстовый редактор, чтобы скопировать их, например, в «IntContainer.h» и «IntContainer.cpp», заменив все вхождения слов «Foo» и «foo» на «Int» и «int» соответственно. Этот процесс можно автоматизировать. В нужный момент вы просто включите файл «IntContainer.h» и будете им пользоваться, как будто он написан вручную.

То, чем вы занимаетесь при любом из этих подходов, можно назвать словом «generic programming», или просто программирование шаблонов. (Вполне понятно, почему «DEFINE_CONTAINER» и «FooContainer._h_/._cpp_» можно назвать шаблонами контейнеров.)

Однако, в Си++ есть родной (встроенный) способ создавать шаблоны, без некрасивых макросов и неудобных манипуляций с файлами.

Шаблоны классов[править]

Шаблоны в Си++ создаются с помощью ключевого слова «template». Предыдущий пример можно переписать в виде:

template <typename T>
class Container {
public:
  void add (T value);
  int index_of (T value);
  int get_count ();
  T get_value (int index);
private: 
 … … …
};

Мы имеем шаблон, называемый «Container». Этим шаблоном, как и любым другим, в чистом виде пользоваться нельзя; пользоваться можно только его экземплярами для конкретных типов.

В Си++ экземпляр шаблона «Container» для типа «int» будет называться «Container<int>»; угловые скобки и всё между ними следует рассматривать как часть названия класса. (Хотя, безусловно, вы можете вставлять пробелы по вкусу, например «Container < int >».)

Си++ сам создаёт нужные экземпляры шаблона, то есть никаких «DEFINE_CONTAINER» и тому подобных вещей вы не пишете вообще. Вы просто пользуетесь нужными экземплярами шаблонов. Например, мы можем написать:

Container <int> apples;
int main () {
  apples.add (7);
  apples.add (11);
}

Что ещё нужно вам узнать прямо сейчас?

  • В чистом виде название «Container» не используется почти ни в каких случаях. (Исключения упомянем позже.) Всегда используется или «template <typename T> class Container», если мы говорим о шаблоне в общем, или же «Container <int>», если мы говорим о конкретном экземпляре шаблона.
  • Безусловно, у шаблонов может быть несколько параметров.
  • Параметрами шаблонов могут быть не только типы. Например, вы можете описать шаблон «template <typename T, unsigned size> class Array», и воспользоваться его экземпляром «Array <float, 20>».

Шаблоны функций[править]

Бывает, что нет смысла городить «целый класс», и шаблона одной-единственной функции вам вполне достаточно. Например, напишем функцию «swap», которая меняет местами значения переданных аргументов.

Вот как она выглядит на Си++:

template <typename T>
void swap (T &left, T &right) {
  T temp = left;
  left = right;
  right = temp;
}

Аналогично шаблонам классов, это — шаблон функции. (Часто говорят — «шаблонный класс», «шаблонная функция».) Экземпляр этой функции, обменивающий местами два значения типа «int», называется «swap <int>».

Однако, в отличие от классов, при вызове функций обычно предоставляют компилятору право выяснить параметры шаблона. Вот так:

int main () {
  int a = 3, b = 5;
  swap (a, b);
  /* теперь a = 5, b = 3 */
}

Поскольку оба аргумента функции имеют тип «int», компилятор знает, что нужно вызвать «swap <int> (a, b)». Разумеется, всегда можно указать экземпляр функции вручную, самостоятельно вызвав «swap <int> (a, b)».

Во всём остальном шаблоны функций не отличаются от шаблонов классов. Поэтому дальнейшее обсуждение относится к ним обоим.

Параметры шаблонов[править]

Вы уже знаете почти всё о параметрах. Осталось сказать только самую малость.

Параметры, как вы уже знаете, бывают двух видов:

  • типы;
  • числа, строки и другие простые типы.

Теперь о них поподробнее.

«typename» и «class»[править]

Чаще всего применяются, конечно, параметры-типы. Есть два совершенно равноценных способа сделать такой параметр:

/* первый способ */
template <typename T> class Foo {};
/* второй способ */
template <class T> class Foo {};

Об этом не стоило бы и говорить. Слова «typename» и «class» в этом месте — полные синонимы. Видимо, предпочтительнее использовать «typename». На самом деле, в Си++ слова «тип» и «класс» — почти всегда синонимы.

Заметим одну важную вещь. В качестве параметра вам может быть передан абсолютно любой тип. Вы никак не можете ограничить, какой тип там может стоять. Однако, например, если вы напишете:

template <typename T>
class Foo {
  T baz;
  void bar () {
    baz.someCoolMethod (15);
  }
};

то явно предполагается, что у переданного типа есть метод «someCoolMethod», принимающий один аргумент типа «int». Но компилятор об этом не знает, поэтому в качестве параметра вы всё равно можете передать любой тип. Однако, когда компилятор попытается создать экземпляр шаблона, он будет ругаться: «У типа T не определён метод someCoolMethod!» (и дальше ещё длинная информация о том, что он создаёт экземпляр шаблона, определённого там-то, с такими-то параметрами).

Применений у параметров шаблона, являющихся типами, — тьма. Вот самое обычное применение:

template <typename T>
class Foo {
  T baz;
  … … …
}

Вот необычное применение:

template <typename T>
class Foo : public T {
  … … …
}

Простые типы как параметры шаблона[править]

Параметры в виде простых типов используются сравнительно реже. Выглядят они так:

template <unsigned BufSize>
class Foo {
  char buffer [BufSize];
};

TODO: какие именно типы можно использовать в качестве параметров? Интегральные, указатели/ссылки на экспортированные статические объекты (например, на функции).

TODO: найти ещё примеры применения этого вида параметров. static assert.

Значения по-умолчанию у параметров шаблона[править]

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

template <typename ValueType, unsigned, unsigned MaxSize = 100>
class Foo {
public:
  int add (ValueType value);
  int index_of (ValueType value);
  int get_count ();
  ValueType get_item (int index);
protected:
  ValueType data [MaxSize];
  unsigned count;
};

Здесь мы имеем контейнер, в котором может храниться до «MaxSize» объектов типа «ValueType». Если мы не укажем параметр «MaxSize» при создании экземпляра, то будет использовано значение по умолчанию — 100. (Заметьте, что это ужасно плохой стиль программирования. Пользователь вашего класса может встроить в свою программу ограничение в 100 объектов, даже не подозревая об этом. Никогда так не делайте.)

Параметры-типы тоже могут иметь значения по умолчанию. Например, в STL повсеместно применяются объекты-аллокаторы. Однако вы чаще всего о них не задумываетесь — именно благодаря значениям по умолчанию. Например, класс «list» из STL может иметь такой заголовок:

template <typename ValueType, typename AllocatorType = allocator <ValueType> >
class list {
  … … …
};

Значением по умолчанию для параметра «AllocatorType» является тип «allocator <ValueType>». Заметьте, как мы используем значение одного параметра («ValueType»), чтобы задать значение по умолчанию для другого параметра. Это возможно, потому что второй параметр описан после первого. Вы везде пишете:

int main () {
  list <int> oranges;
  oranges.push_back (11);
  oranges.push_back (13);
}

и можете вообще не знать, что такое аллокатор. Но самые продвинутые могут написать свой аллокатор, и использовать экземпляр «list» по имени «list <int, MyAllocator>».

Синтаксические особенности шаблонов[править]

До сих пор мы не касались того, что скрывается за троеточиями в многочисленных примерах кода. Между тем, чтобы вы могли написать законченный шаблон, вам нужно узнать несколько вещей.

Определение функций-членов (методов)[править]

Как и всегда, определить небольшой метод можно и внутри класса:

template <typename T>
class Foo {
  void bar () { 
    … … …
  }
};

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

template <typename T>
void Foo <T>::bar () { 
  … … …
}

Он может показаться ужасным, но иного выхода нет. Кроме того, если вы класс определяете в заголовочном файле, то и все определения его методов вы должны поместить в заголовочный файл, а не в соответствующий «.cpp». (Как правило, у шаблонных классов вообще нет «.cpp»-файла, весь код помещается в «.h»-файл.) ЗЫ. Но иногда исхитряются следующим образом: описывают интерфейс шаблона в «.h»-файле, но в его конце добавляют строчку #include template_file.cpp, где файл template_file.cpp содержит реализацию шаблона.

Конструктор и деструктор[править]

Конструктор и деструктор определяются также, как и обычные методы:

template <typename T>
Foo <T>::Foo () { 
  … … …
}
 
template <typename T>
Foo <T>::~Foo () { 
  … … …
}

Заметьте, имена конструкторов и деструктора — немногие случаи, когда имя класса употребляется «в чистом виде».

Определение статических данных-членов[править]

Если вы знаете, о чём идёт речь, то следующий пример скажет вам всё необходимое:

template <typename T>
class Foo {
public:
    static int x;
};
 
template <typename T>
int Foo <T>::x;

Экземпляр шаблона как параметр другого шаблона[править]

Мы уже видели пример того, как это делается, выше, в разделе «Значения по-умолчанию для параметров». Немного другой пример:

template <typename T>
class Foo {};
 
template <typename T>
class Bar {};
 
int main () {
  Bar <Foo <int> > bar;
}

Единственный важный момент, который нужно знать: между закрывающими угловыми скобками обязательно должен быть пробел («> >»). Иначе компилятор будет считать их оператором поразрядного сдвига «>>», и выдаст много малопонятных ошибок.

Шаблон как параметр другого шаблона[править]

В качестве параметра шаблона можно также передавать другой шаблон:

template< template< class T > class U >
class Foo {
 public:
   U< int > member;
};

Это можно использовать, например, для того, чтобы указать алгоритм, используемый шаблонным классом, или стратегию хранения:

#include <vector>  
/*
    Шаблон из стандартной библиотеки C++
  template<class Type, class Allocator = std::allocator<Type> 
  class vector
  {
    ... ...
  }
*/
 
#include <list>
/*
    Шаблон из стандартной библиотеки C++
  template<class Type, class Allocator = std::allocator<Type> 
  class list
  {
    ... ...
  }
 */
 
template< 
    template<class Type, class Allocator = std::allocator<Type> > class Container, 
    class T 
>
 
class Bar {
    ... ... ... 
    Container<T> collection;
    ... ... ... 
};
 
class Qux
{
   ... ... ... 
};
 
Bar< std::vector, int >  bar1;  // bar1.collection имеет тип std::vector<int>
Bar< std::list,   Qux >  bar2;  // bar2.collection имеет тип std::list<Qux>

Теперь bar1 использует для хранения int std::vector, а bar2 - std::list для хранения Qux (std::vector и std::list — шаблоны из стандартной библиотеки C++. Обратите внимание, что они имеют два шаблонных параметра, один из которых задан в стандартной библиотеке по умолчанию, и мы при написании шаблонного параметра шаблона должны указать их оба).

Ключевое слово «typename»[править]

Рассмотрим класс:

struct Foo {
  typedef int my_type;
};

Вся суть этого класса в том, что «Foo::my_type» обозначает некий тип. Теперь напишем шаблонный класс, использующий этот факт. (Внимание, пример неправильный!)

template <typename T>
class Bar {
  T::my_type x;
};

Мы пытаемся воспользоваться типом «my_type», определённым в «T». Однако в силу некоторых причин компилятор сам не хочет понимать, что «T::my_type» в данном случае обозначает тип. Мы должны ему в этом помочь, поставив перед всеми такими типами ключевое слово «typename»:

template <typename T>
class Bar {
  typename T::my_type x;
};

В этом фрагменте кода утверждается, что «T::my_type» — это именно тип. На современных компиляторах всегда, ссылаясь на тип, определённый внутри класса, передаваемого параметром шаблона, необходимо использовать «typename».

Заметьте, все определения методов шаблонного класса должны быть видны изо всех мест, где он используется. Это значит, что если вы, например, попытаетесь использовать механизм раздельной компиляции, включая в заголовочный файл(*.h) только обявление шаблона, то компилятор просто не сможет его инстанцировать, т.ё. вывести из шаблона конретный тип. Поэтому приходится включать в заголовочные файл(или в единицу трансляции где шаблон объявлен и используется) помимо объявления ещё и определение шаблона, что приводит к разбуханию кода. Существует решение этой проблемы в виде механизма экспорта шаблона, предложенного компанией Silicon Graphics и в своём виде поддерживаемого некоторыми компиляторами различных разработчиков. Однако это решение потребовало изменение стандарта Си++ 1996г. и нашло реализацию далеко не во всех компиляторах, например в компиляторе от Microsoft включённого в продукт Microsoft Visual Studio.

Особенности компиляции шаблонов[править]

Помещаем шаблоны в файлы[править]

Проверка ошибок при работе с шаблонами[править]

Специализация[править]

Наследование и шаблоны[править]

Стандартная библиотека[править]

STL[править]

Общие сведения[править]

Контейнеры. Итераторы. Функторы. Предикаты.

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

Соответственно итератор — это класс, предназначенный для перебора элементов контейнера.

Функторы и предикаты используются в библиотеке алгоритмов STL

Функтор — объект класса в котором перегружен оператор (). Количество аргументов определяется задачей, для которой нужен функтор.

Предикат — функция, возвращающая bool, также это может быть функтор оператор () которого возвращает bool. Унарный предикат — предикат принимающий 1 аргумент, к примеру !a. Бинарный предикат — предикат, принимающий 2 аргумента, примеры: a>b, a<b и др.

Контейнеры[править]

Виды. Последовательные и ассоциативные. Адаптеры (??!, что это?).

  • vector. Обертка вокруг массива, выделяемого по new. Поддерживает проверку границы (если использовать v.at(i), а не v[i]), а также автоматическую реаллокацию при добавлении в хвост и вставке/удалении в середину.

Очень быстрый operator[], линейный по времени (из-за умной реаллокации) push_back(), но медленная вставка в середину, кроме того, любая вставка инвалидует все итераторы (STL, построенная в отладочном режиме, сама отлавливает ошибки использования инвалидованных итераторов). Совместим с массивом языка Си — std::vector::operator T* вернет указатель на массив Си, который можно передавать, например, в вызовы операционной системы.

Саттер и Александреску в своей книге пишут: «если вы не уверены, какой именно контейнер вам нужен — смело используйте vector».

  • deque. Двунаправленная очередь, реализованная как коллекция страниц стандартного размера.

Очень быстрая вставка/удаление и с головы, и с хвоста, кроме того, эти операции не инвалидуют итераторы. operator[] чуть медленнее, чем у vector, но тем не менее использование deque «только с хвоста» (семантика аналогична vector, класс называется stack) оправдано и имеет свои особенности — operator[] медленнее, зато не инвалидуются итераторы (кроме вставки/удаления в середину) и куда лучше общий паттерн аллокации и фрагментации памяти (важно при большом количестве объектов).

  • queue. deque, используемая в режиме «добавление только в голову, удаления только с хвоста».
  • list. Список.

Не имеет operator[], и его итераторы не являются random-access. Паттерн аллокации памяти — по блочку на объект. Преимущества: не инвалидует итераторы никогда, и очень быстрые вставки/удаление в середину.

  • set

Красно-черное дерево из объектов с операцией поиска по ключу, и тип ключа, и тип объекта — типовые параметры. Хранится в отсортированном виде, алгоритм sort для него ничего не делает.

  • map

set из пар объектов, ключ — тип первого объекта. Ассоциативный массив (как в Perl), у которого в квадратных скобках может стоять любой тип, а не только целое число.

Аллокаторы[править]

Все контейнеры STL используют т.н. аллокаторы для размещения объектов в памяти. Аллокатор можно указать как типовой параметр, по умолчанию используется std::allocator.

Аллокатор делает следующее:

  • определяет (как AlType::pointer) класс «обобщенного указателя» на объект. Этот указатель

должен иметь все операции, определенные для указателя языка Си, а также operator T* (возвращает указатель Си) и operator * (возвращает ссылку). Для операций должны поддерживаться гарантии языка Си (типа *(a + i) тождественно a[i] и так далее). Как именно реализован этот указатель — личное дело аллокатора.

  • определяет методы allocate() — выделяет блочок памяти для хранения N объектов типа T,

возвращает обобщенный указатель на его начало, с гарантией, что прибавления к этому указателю дадут указатели на последующие объекты, free()undo для allocate(), construct() — создает объект по адресу, на который ссылается обобщенный указатель, и free()undo для construct().

  • std::allocator::pointer есть обычный указатель языка Си, методы реализованы примерно так:
pointer std::allocator::allocate(size_t N)
{
  return (pointer)new char( N * sizeof(T) );
}
void std::allocator::free(pointer p)
{
  delete p;
}
void std::allocator::construct(pointer p)
{
  (void)new(p)T(); // placement new
}
void std::allocator::construct(pointer p, T& o)
{
  (void)new(p)T(o); // placement new
}
void std::allocator::destroy(pointer p)
{
  p->~T();
}
  • возможно писать свои реализации аллокаторов, при этом с ними будут работать все контейнеры, и общий внутренний код контейнеров, ответственный за "раздвижение" контейнера при вставке в середину и т.д.

Итераторы[править]

Обобщение указателя. Классы итераторов.

Позволяют писать код для работы с контейнером (например, алгоритмы STL — см. ниже), не зависящий от типа контейнера.

В любом контейнере объявлен (через typedef) тип его итератора — Cont::iterator

У любого контейнера есть метод Cont::iterator begin(), возвращающий итератор на первый (в порядке обхода) элемент.

У любого итератора перегружена операция ++, означает шаг на следующий элемент.

У любого контейнера есть метод end(), возвращающий лже-итератор (не ссылается на валидную память!), который есть результат ++ над итератором, ссылающимся на последний (в порядке обхода) элемент.

Итераторы можно сравнивать на равенство.

У итератора есть operator T*, возвращает указатель на объект, на который ссылается итератор, и operator *, который преобразует итератор к ссылке на этот же объект.

Правильный код для обхода контейнера:

c.iterator itBegin = c.begin();
c.iterator itEnd = c.end();
for( c.iterator it = itBegin; it != itEnd; ++it )
{
 // на каждом шаге *it есть следующий объект внутри c
}

Заметим, что это позволяет писать код, работающий с диапазонами итераторов, а не с контейнерами как таковыми, что повышает легкость замены в программе одного итератора на другой — vector на list и т.д.

Алгоритмы STL (см. ниже) реализованы именно так.

После внесения изменений в контейнер некоторые (или все) итераторы на него могут прийти в невалидное состояние. Какие именно - зависит от типа контейнера и типа изменения (в хвост/голову/середину). Обращение к инвалидованному итератору есть грубая ошибка, в релизной версии обычно вызывающая крах программы. Отладочные версии STL умеют проверять на такие ошибки.

Итераторы бывают однонаправленные, двунаправленные (есть operator--), и с произвольным доступом (есть [] и прибавления/вычитания целых чисел, семантика строго как у указателя Си). Каков данный итератор в этом смысле - зависит от контейнера, vector и deque имеют итераторы с произвольным доступом, а list — нет.

Алгоритмы[править]

Алгоритмы над контейнерами позволяют, независимо от типа контейнера, работать с его данными с помощью итератора контейнера. Ниже приведены некоторые примеры из библиотеки алгоритмов STL, для её подключения нужно указать #include <algorithm>. Каждый алгоритм в это библиотеке описывает целевую задачу и поэтому очень мал, поэтому советую не лениться и смотреть определение неизвестных классов и функций.

for_each[править]

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

namespace std {
template<class _II, class _Fn> inline 
_Fn for_each(_II _F, _II _L, _Fn _Op)
{
  for (; _F != _L; ++_F)
  _Op(*_F);
  return (_Op); 
}
} // end namespace std

рассмотрим аргументы:

_F — итератор на элемент контейнера, с которого нужно начинать

_L — итератор на элемент контейнера, которым нужно заканчивать

_Op — функция от одного аргумента или функтор, у которого перегружена операция () для одного аргумента. Тип аргумента должен соответствовать типу данных, содержащихся в контейнере.

Как видим, функция for_each всего-то-навсего для каждого элемента в контейнере вызывает функцию или оператор функтора. Также видно, что возвращаемое значение не играет роли. И все. Пример использования:

//наш функтор
class FunctorToAdd
{
public:
  void operator()(int& zn)
  {
  //что-то делаем с аргументом
  ++zn;
  }
};
 
void main()
{
  vector<int> mass;
  mass.push_back(1);
  mass.push_back(2);
  mass.push_back(3);
 
  //заметим, что мы создаем объект класса FunctorToAdd с помощью возвращающего конструктора
  std::for_each(mass.begin(),mass.end(),FunctorToAdd());
 
  cout<<mass[0]<<endl;
  cout<<mass[1]<<endl;
  cout<<mass[2]<<endl;
}

В данной задаче (увеличение каждого элемента на 1) можно обойтись и простой функцией:

void FuncToAdd(int& zn)
{
  ++zn;
}
 
std::for_each(mass.begin(),mass.end(),FuncToAdd);

но в более сложных задачах может пригодится объект.

transform[править]

Тут проще посмотреть на определение:

namespace std {
template<class _II, class _OI, class _Uop> inline
_OI transform(_II _F, _II _L, _OI _X, _Uop _U)
{
  for (; _F != _L; ++_F, ++_X)
  *_X = _U(*_F);
  return (_X); 
}
} // end namespace std

transform помещает в новый контейнер (_X — итератор на его начало) значения, которые вернет наша функция (_U) или функтор от аргумента из исходного контейнера. Если указать источник равный приемнику, то будут соответственно изменены значения исходного контейнера. Пример использования:

//нужно создать другую функцию, т.к. в transform используется возвращаемое значение
int FuncToAdd2(int zn)
{
  return ++zn;
}
 
list<int> mass2;
std::transform(mass.begin(),mass.end(), back_inserter( mass2 ), FuncToAdd2 );

back_inserter — функция, которая возвращает объект (back_insert_iterator), который в свою очередь определяет оператор * (возвращает *this) и оператор = , в котором вызывает push_back для нашего контейнера mass2. Понятно в чем фишка? Это как раз нам и нужно, т.к. mass2 у нас пуст, а в функции transfrom как раз выполняется присваивание.

count_if[править]

Подсчитывает количество элементов в указанном диапазоне контейнера, для которых выполняется унарный предикат (третий аргумент). К примеру, если у нас массив mass2=[0,1,2,3,4], то:

bool Count(int z)
{
  return z>2;
}
 
//вернет 2
cout<< count_if(mass2.begin(),mass2.end(),Count) <<endl;

Ниже будет показан пример как этого же результата можно добится используя адаптеры вместо функции.

partition[править]

Переставляет элементы контейнера так, что сначала идут элементы, меньшие, чем данный, а потом - большие.

Линейное время исполнения. Не выделяет дополнительную память. (уточнить требования к bidir и random у итераторов! кажется, обязателен BidIt).

stable_partition[править]

То же, что partition, но с гарантией, что не будут меняться местами элементы, «равные» друг другу.

Выделяет временную дополнительную память для всего множества.

nth_element[править]

«Недосортировка». Переставляет элементы так, что в позиции N окажется тот элемент, который оказался бы там при полной сортировке. При этом не дается никаких гарантий на остальные элементы (уточнить! кажется, предыдущие гарантированно все меньше, а последующие — все больше).

«Половина» qsort с рекурсией только по одной половинке, а не по обеим.

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

partial_sort[править]

Находит N наименьших элементов, и располагает их в порядке возрастания в начале контейнера. Никаких гарантий на остальные элементы.

«Половина» от heap sort.

partial_sort всего контейнера есть полный heap sort.

Время N * log N (без деградации на неудачных данных), память не нужна.

sort[править]

В зависимости от параметра «порог» — либо qsort (по умолчанию), либо heap sort, либо сначала quick или heap, а затем insertion sort для мелких отрезков.

Требования по памяти и времени — см. классические описания алгоритмов.

stable_sort[править]

То же, что sort, но гарантирует не-перестановку равных элементов.

Merge sort. Память нужна сразу и на все сортируемое множество, о времени выполнения — см. классическо описание алгоритма (обычно медленнее qsort, но не деградирует на неудачных входных данных).

Адаптеры[править]

Для работы нам потребуется добавить #include <functional>

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

 cout<< count_if(mass2.begin(),mass2.end(), bind2nd(greater<int>(),2)) <<endl;

Страшно? Ну ничего, сейчас всё объясню. greater — класс в котором перегружен оператор (), в котором происходит сравнение 2-х аргументов (т.е. return (_X > _Y)). Т.е. это настоящий бинарный предикат. А теперь давайте представим, как было бы хорошо, если бы на место первого аргумента (_X) подставлялся элемент из массива, а во второй какое-нибудь заданное нами число (2). bind2nd как раз это и делает. При его конструировании мы указываем бинарный предикат и значение, которое сохраняется и подставляется в greater<int>::operator() при вызове bind2nd::operator(). Тем самым мы превращаем наш бинарный предикат в унарный. Что и требуется для функции count_if.

Для тех кто не понял bind2nd и есть адаптер. Существует также bind1st, который заменяет не второй аргумент, а первый. Поэкспериментируйте.

Библиотека ввода-вывода[править]

Потоки вывода[править]

Вывод пользовательских типов.

Потоки ввода[править]

Ввод пользовательских типов.

Форматирование[править]

Буферизация[править]

Разные вопросы[править]

В чём великий смысл «volatile»?[править]

Напишем следующую программу:

int x = 0;
int main () {
  while (x < 10) {
  }
}

Как несложно заметить, она работает вечно. Более того, она полностью загружает весь ваш процессор очень сложными вычислениями…

Что с ней сделает хороший компилятор? Он заметит, что значение переменной «x» внутри цикла не изменяется. Поэтому он просто не будет каждый раз читать значение «x» из памяти и сравнивать его с 10. Зачем? Какое значение «x» было при входе в цикл, таким оно и останется. Поэтому он превратит ваш код в нечто похожее на:

int x = 0;
int main () {
  if (x < 10) {
    while (1) {
    }
  }
}

Замечательно! Но предположим, что есть ещё кто-то. Этот кто-то вашу переменную «x» каждую секунду увеличивает на единицу. Поэтому ваш первоначальный код на самом деле должен был через 10 секунд завершаться! Но компилятор-то этого не знал, и сделал из неё бесконечный цикл.

Для того, чтобы сообщить компилятору о присутствии кого-то, и служит ключевое слово «volatile». Если переписать пример в виде:

volatile int x = 0;
int main () {
  while (x < 10) {
  }
}

то компилятор при каждом обращении к «x» будет честно заново считывать её из памяти. Ведь её значение могли изменить некие тёмные силы.

Спецификатор volatile подавляет оптимизацию common subexpression elimination (исключение общих подвыражений), заставляя компилятор действительно читать значение из памяти каждый раз.

Что касается правил языка, то снятие спецификатора volatile невозможно без написания явного преобразования к типу (в этом volatile аналогичен const).

NB: Хорошим примером такого рода тёмных сил является другой процесс, имеющий с вашим общую память. Как правило, практическая нужда в volatile возникает только при использовании многонитевости.

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

Эпилог[править]

Некоторые люди интересовались, откуда берутся так часто употребляемые метасинтатические переменные foo, bar и тому подобные. На английском происхождение данных слов очень хорошо описано в RFC 3092. (Также, если вы знаете английский, см. полный список первоапрельских RFC.)

Если Вы возьмётесь править этот викиучебник, пожалуйста, соблюдайте правила русской типографики. О том, как это делается см. w:Википедия:Специальные символы.

См. также[править]

Ссылки[править]