Си++/Основные отличия Си++ от Си
Основные отличия Си++ от Си
[править]Самые элементарные усовершенствования
[править]Комментарии
[править]Два слеша подряд (//) означают, что дальше до конца строки — комментарий. Вместе с тем комментарии старого образца (/*...*/) тоже разрешены. Внутри символьных строк, ограниченных кавычками, символы, определяющие комментарии, не распознаются компилятором и остаются частью строки.
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 имеют
// значения по умолчанию
// тут тело функции
}
Умолчания параметров строго равносильны конструкциям с 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;
// ещё 2000 строк без упоминания x
x = y + 15;
// читающий программу уже забыл тип х
// ему придётся лезть в начало блока
}
/*Вариант 2*/
{
// 2000 строк
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 состоит из тысячи строк, и везде, где употребляется 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);
гарантируют неизменность передаваемого значения.
Что такое ссылка и что с ней можно делать
[править]Ссылку в С++ можно понимать или как альтернативное имя объекта, или как безопасный вариант указателей. Ссылки имеют три особенности, отличающие их от указателей.
- При объявлении, ссылка обязательно инициализируется ссылкой на уже существующий объект данного типа.
- Ссылка пожизненно указывает на один и тот же адрес.
- При обращении к ссылке операция * производится автоматически.
Объявление ссылок очень похоже на объявление указателей, только вместо звёздочки «*» нужно писать амперсанд «&».
Что произошло в предыдущем разделе? Аргумент функции 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 типа T».
Ссылка, без ключевого слова const, обязательно требует lvalue как инициализирующего выражения. Сделано это потому, что такая ссылка подразумевает внесение изменений в объект через нее, а никому не нужно, чтобы эти изменения были выполнены над временным объектом и потом были забыты.
До появления в языке ссылок на rvalue было всего два способа указать тип T как тип формального параметра: f(T) и f(T&). Однако, оба эти способа имели недостатки:
- В первом случае происходило копирование фактического параметра в формальный, последний всегда был временным объектом
- Во втором же случае обязательно было использовать lvalue в фактическом параметре
Кроме того, запрещена перегрузка функций по отличию всего лишь T и 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:: опущена):
- T
- Z1 ⊕ Z3
- X1 ⊕ Y1 ⊕ Z2 ⊕ Y2
- X2
- 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 определён несколько раз». Это является прямым следствием открытости пространств имён.
Неименованные пространства имён
[править]Да, есть и такие. Собственно, странного в этом ничего нет. Пространство имён является неким контейнером для имён сущностей, а имеет ли сам контейнер имя или нет, на его функции не особо-то и влияет. Наличие имени не более чем позволяет ссылаться на обозначаемую им сущность. Неименованные пространства имён, не имея собственного имени, просто не позволяют на них ссылаться, однако свои функции при этом исполняют исправно.
Кажется странным, зачем такое нужно. Ведь если невозможно на него ссылаться ввиду отсутствия имени, нельзя и получить доступ к его содержимому. Однако у неименованных пространств имён есть две особенности, которые делают их небесполезными.
- Неименованное пространство имён единственно для единицы трансляции и уникально для каждой из них. Другими словами, открывая каждый раз неименованное пространство имён для дополнения его контентом, мы ссылаемся на одну и ту же область видимости, но при этом в каждой единице трансляции такая область видимости своя собственная, и её контент никак не коррелирует с другими единицами.
- Неименованное пространство имён неявно всегда видимо в текущей единице трансляции.
Фактически следующая конструкция
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. Все приведенные выше «различные обстоятельства» могут лишь быть сконвертированы в Си++ные исключения с помощью платформенно-зависимых функций, в итоге делающих соответствующий throw — set_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 (пока скипнуто, видимо будет)