Си++

Материал из Викиучебника

(Перенаправлено с C++)
Перейти к: навигация, поиск

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

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

Содержание

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

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

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

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

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 */
}

[править] Что такое ссылка

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

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

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

Пример:

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; */  // Запрещено!  Ссылки надо инициализировать!

Ссылки в основном используют для передачи параметров функциям:

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

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

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

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

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

[править] Что дают ссылки

Есть в программировании такое понятие, как совмещение имён (по-английски — aliasing). Вернёмся к трём нашим примерам (приведу по первой строчке из каждого):

void foo (int x) 
Здесь имена x и z обозначают совершенно разные вещи. (Если хотите, это названия разных ячеек памяти.)
void foo (int *x) 
Здесь *x и z — два названия одного и того же. Есть в памяти место, которое внутри функции main назвается z, а внутри foo*x.
void foo (int &x) 
Здесь уже просто x и z — два названия одной и той же области памяти. Хотя по внешнему виду x невозможно отличить от обычной переменной, для компилятора это — просто указатель с другим лицом.

[править] Чем отличаются ссылки от переменных

Пусть мы написали:

int z;
int &x = z;

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

Во-первых, мы можем написать «int z = 7;», а можем:

int z;
z = 7;

Это будет практически одно и то же.

Ссылки ведут себя совсем иначе. Запись «int &x = z;» означает, что «x отныне является ссылкой на область памяти, которая раньше называлась z». Если после этого написать «x = z;» это означало бы, что области памяти, на которую ссылается x, нужно присвоить значение переменной z. В нашем случае (когда x ссылается на z) это эквивалентно записи «z = z;».

Заметим, что когда мы пишем «int z;», то выделяется память для хранения величины типа int. А когда мы пишем «int &x = z;», мы пользуемся «чужой» памятью, то есть памятью, выделенной когда-то кем-то. Что очень важно: в этом случае не вызывается конструктор для переменной x (В данный момент думаю, не надо говорить про конструктор, потому что еще не известно, что это такое).

Когда время жизни переменной z заканчивается (в нашем примере — когда мы возвращаемся из main()), выделенная память освобождается. Если на неё остались какие-то указатели и ссылки — это ваши проблемы, и ваша программа непременно вскоре упадёт. У нас такого не происходит, потому что пока жива ссылка x, жива и переменная z.

[править] Отличие ссылок от указателей

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

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

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

 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; /* можно инициализировать и через другую ссылку */

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

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; /* Ошибка выполнения */
  • Как известно, NULL является просто красивым названием для числа 0 (всегда верно для C++, хотя для C зависит от аппаратной платформы), в некоторых реализациях, приведенном к типу void *.

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

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

int x;
int *px=&x;
px=0;
*px++; // Ошибка! В предыдущей строке программист забыл написать * перед px

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

 enum color {red, green, blue};

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

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

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

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

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

1) В стандарте 0x появятся "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);

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

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

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

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

int a[10];

а в другом

extern int *a;

- компилятор, скорее всего, выдаст Вам ошибку.

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

Если создаётся глобальный массив, то изначально все его элементы по умолчанию нули, однако зачастую возникает необходимость присвоить им другие начальные значения. Процесс присваивания начальных значений и называется инициализацией. В 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";

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

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

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

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

Предположим, у нас имеется массив 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], на самом деле, не является LValue, т.е. объектом, имеющим физический адрес.

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

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

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

В Си для этого было две главных функции: одна называлась 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() не умеют автоматически запускать конструкторы, и потому непригодны для динамического создания объектов. В языке C++ им имеется адекватная замена - оператор new. Рассмотрим пример:

MyClass *mc = new MyClass(5);

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

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

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

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

Для каждого класса, помимо конструкторов, определён ещё и деструктор, то есть функция, отвечающая за корректное уничтожение объекта. Деструктор никогда никаких параметров не принимает, и потому не может быть перегружен. Проблема с деструктором возникает та же, что и с конструктором: функция free() не умеет его вызывать. Поэтому в C++ введён ещё один оператор - 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 позволило бы спастись от этой напасти, но увы и ах... В принципе, гибким решением этой пробемы является применение виртуальных деструкторов. Вообще же, как мы видим, при использовании 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), следите за тем, чтобы вызывались "правильные" деструкторы.

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

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

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

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

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

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

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

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

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

Этот идиотский термин придумали переводчики, встретив ангийское выражеие «function overloading». Были и нормальные люди (в издательстве «Мир»), которые перевели это как «совмещение функций», и было это 20 лет назад. Однако троечники-переводчики из современных издательств классических книг по программированию, разумеется, не читали, и «перегрузка» стала общеупотребимым термином.

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

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

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

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

Си++ как раз позволяет предоставить компилятору задачу по выбору нужного варианта функции. Для этого мы можем определить набор функций, каждая из которых имеет имя «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».)

NOTE: Стандарт Си++ не определяет, будет ли char знаковым или нет — это отдано на откуп реализации. Так что неверно предполагать, что char всегда означает signed char - может быть и unsigned. Если наличие или отсутсвие знака важно (например, при конвертации в int или сравнении больше/меньше), лучше указывать это явно.

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

void print (char v)
void print (char v, bool uc = true) /* Сигнатуры функций различны, здесь ошибки нет */
 
/* а где-то в программе ... */
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() */

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

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

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

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

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

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

[править] Пространства имён как общее понятие

Термин «пространство имён» (namespace) — один из основных в языках программирования.

Давайте проведём аналогию с адресом. Если вы скажете: «Я живу в доме 56», ваше заявление не будет иметь никакого смысла, пока вы не уточните, на какой улице расположен этот дом. На каждой улице могут быть свои номера домов; на разных улицах номера могут повторяться, и будут обозначать совершенно разные дома.

Теперь рассмотрим следующий фрагмент программы на Си (заметьте, сейчас всё, что мы говорим, относится как к Си, так и к Си++):

int x;  /* первая x */
void foo (char x) {  /* вторая x */
  x = 12; /* здесь x -- это вторая x */
}
void bar () {
  x = 12; /* здесь x -- это первая x */
}
int main () {
  double x;  /* третья x */
  x = 12; /* здесь x -- это третья x */
}

В этой программе есть три разных переменные «x». Это возможно потому, что каждая функция имеет своё пространство имён. Имена (в частности, имена переменных), определённые в нём, видны только из этого пространства (то есть — изнутри этой функции).

Также есть глобальное пространство имён. «Первое x» — именно оттуда. Глобальное пространство имён видно везде. Однако, если то же имя объявить в локальном пространстве имён (как мы поступили, определив второе и третье «x»), оно для этого пространства оказывается «ближе к сердцу».

Но это ещё не всё. Каждый блок имеет своё пространство имён. Например:

int main ()
{
  int x, y = 321;
  x = 1;
  {
    int x; 
    x = 2;
    { 
      int x;
      x = 3;
      y = 123;
    }
    x = 4;
  }
  x = 5;
}

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

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

[править] Пространства имён структур и объединений

В Си++ структуры («struct») и объединения («union») стали создавать полноправные типы данных. Кроме того, каждая структура и объединение (как и, разумеется, каждый класс) получили своё полноправное пространство имён.

Например, вы можете написать:

struct Foo {
  typedef unsigned char byte;
  byte data [16];
};

В структуре определено имя «byte». Но оно «просто так» не видно за пределами данной структуры. Если вы захотите воспользоваться типом «byte» из структуры «Foo», то вот как это говорят в Си++: «Foo::byte». Например:

int main () {
  Foo::byte x;
  x = 17;
}

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

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

struct Foo {
  struct Bar {
    struct Boz {
      int x, y;
    };
  };
};
int main () {
  Foo::Bar::Boz wow;
  wow.x = 7;
  wow.y = 11;
}

[править] Определяемые пользователем пространства имён в Си++

Во всех предыдущих примерах пространства имён были «приложением» к чему-то другому — будь то блок, функция, структура, класс или объединение. Однако в Си++ есть конструкция, которая позволяет создавать пространство имён в чистом виде. Она очень проста:

namespace MyFavouriteTypes {
  typedef unsigned char byte;
}
int main () {
  MyFavouriteTypes::byte data [16];
  …
}

Конструкция «namespace» обладает одним уникальным качеством. Пространство имён, ею созданное, можно дополнять в разных местах программы. Пример:

namespace MyFavouriteTypes {
  typedef unsigned char byte;
}
… … …
namespace MyFavouriteTypes {
  typedef