C++ ОПП


Классы

В принципе простейший класс – это любая структура (struct), доступ к элементам, которой открыт для любого объекта не члена структуры.

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

Конструктор (от construct – создавать) — это особый метод класса, который выполняется автоматически в момент создания объекта класса. То есть, если мы пропишем в нем, какими значениями надо инициализировать поля во время объявления объекта класса, он сработает без «особого приглашения». Его не надо специально вызывать, как обычный метод класса.

Деструктор (от destruct — разрушать) — так же особый метод класса, который срабатывает во время уничтожения объектов класса. Чаще всего его роль заключается в том, чтобы освободить динамическую память, которую выделял конструктор для объекта. Имя его, как и у конструктора, должно соответствовать имени класса. Только перед именем надо добавить символ ~

Важное:
- Конструктор и деструктор должны быть public;
- Конструктор и деструктор не имеют типа возвращаемого значения;
- Имена класса, конструктора и деструктора должны совпадать;
- Конструктор может принимать параметры. Деструктор не принимает параметры;
- При определении деструктора перед именем надо добавить символ ~ ;
- Конструкторов может быть несколько, но их сигнатура должна отличаться (количеством принимаемых параметров, например);
- Деструктор в классе должен быть определен только один.

Конструктор копирования и операция присваивания

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

Klass(const Klass &); // Прототип конструктора копирования

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

Klass k2(k1);
Klass k3 = k2;
Klass k4 = Klass(k3);
Klass * pKlass = new Klass(k4);

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

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

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

#include <iostream>
struct Klass {
	int size;
	double * data;
	Klass() {
		std::cout << "Конструктор по умолчанию" << std::endl;
		size = 100;
		data = new double[size];
	}

	Klass(const Klass & klass) {
		std::cout << "Конструктор копирования" << std::endl;
		size = klass.size;
		data = new double[size];
		for (int i = 0; i < size; i++) {
			data[i] = klass.data[i];
		}
	}

	Klass & operator=(const Klass & klass) {
		if (this != &klass) {
			std::cout << "Перегруженный оператор присваивания" << std::endl;
			delete[] data;
			size = klass.size;
			data = new double[size];
			for (int i = 0; i < size; i++) {
				data[i] = klass.data[i];
			}
		}
		else {
			std::cout << "Самоприсваивание" << std::endl;
		}
		return *this;
	}

	~Klass() {
		std::cout << "Деструктор" << std::endl;
		delete[] data;
	}
};

int main() {
	setlocale(LC_ALL, "Russian");
	Klass k1;
	Klass k2(k1);
	Klass k3 = k2;
	Klass k4 = Klass(k3);
	Klass * pKlass = new Klass(k4);
	k1 = k2;
	k2 = k2;
}

Стоить отметить, что во всех трёх функциях память должна выделяться и удаляться одинаковым образом. Т. е. нельзя в одном случае использовать delete, а в другом delete[].


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

class имяКласса
{
private:
  объявления данных-членов
public:
  прототипы функций-членов
};

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

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

Заголовок функции должен выглядеть примерно так:

char * Bozo::Retort ()

Функция-член класса, или метод, вызывается с использованием объекта класса. Это делается с помощью операции членства (точки).


class Stock  // Ключевое слово class идентифицирует объявление класса.
            // Имя класса становится именем этого определенного пользователем типа. 
{
private:  // Ключевое слово private идентифицирует члены класса, которые могут быть         .         // доступны только через функции-члены public (сокрытие данных)
  
  char company[30];
  int shares;   //  Члены класса могут быть типами данных 
  double share_val;
  double total_val;
  void set_tot() { total_val = shares *share_val; }
public:  // Ключевое слово public идентифицирует члены класса, которые образуют 
         // открытый интерфейс класса (абстракция)
  void acquire(const char *со, int n, double pr);
  void buy(int num, double price);
  void sell(int num, double price);  // Члены класса могут быть функциями.
  void update(double price);
  void show() ;
};

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

Поскольку одним из главных принципов ООП является сокрытие данных, то единицы данных обычно размещаются в разделе private. Функции-члены, которые образуют интерфейс класса, размещаются в разделе public; в противном случае вызвать эти функции из программы не удастся.


Реализация функций-членов класса

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

  • При определении функции-члена для идентификации класса, которому
    принадлежит функция, используется операция разрешения контекста (: :).
  • Методы класса имеют доступ к private-компонентам класса.

Указатели на функции, методы и члены данных

Дружественная функция — это функция, которая не является членом класса, но имеет доступ к членам класса, объявленным в полях private или protected.

Для описания дружественной функции она должна быть объявлена внутри класса со спецификатором friend. Функция может быть описана в любой части класса, как закрытой, так и открытой. Это не влияет на ее сущность, так как она не принадлежит классу. Естественно, и доступ к такой функции осуществляется как к обычной функции (без точки).

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

Дружественная функция, имеющая доступ к обоим классам, в обоих классах и должна быть определена как дружественная.

this -> в дружественных функциях не работает.

Определению дружественной функции в классе, абсолютно всё равно где оно указанно (public, protected, private).

Указатели на функции

тип_функции(* имя_указателя)(спецификация_параметров);
void Foo() {}
int main()
{
    void(* fooPointer)(); // Указатель
    fooPointer = Foo; // Приравняли
    fooPointer(); // Вызвали
}

Встроенные методы

Любая функция с определением внутри объявления класса автоматически
становится встроенной.

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

Дружественные методы:

class A
{
public:
    A(int a, string b){}
private:
    int a;
    string b;
    friend B::void Foo(A &cloneA) // Указание дружественного метода
}

class A
{
public:
    void Foo(A &cloneA)
};

Конструкторы и деструкторы

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

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

имяКласса объект = значение;

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

Klunk::Klunk() { } // неявный конструктор по умолчанию

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

Имя_класса(const Имя_класса &);

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

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


Массив объектов

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

const int STKS = 4;
Stock stocks[STKS] = {
  Stock("NanoSmart", 12.5, 20),
  Stock("Boffo Objects", 200, 2.0),
  Stock("Monolithic Obelisks", 130, 3.25),
  Stock("Fleep Enterprises", 60, 6.5)
};

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

const int STKS = 10;
Stock stocks[STKS] = {
  Stock("NanoSmart", 12.5, 20),
  StockO ,
  Stock("Monolithic Obelisks", 130, 3.25),
};

Область видимости класса

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

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

При определении функций-членов должна применяться операция разрешения контекста:

void Stock::update(double price)
{
  ...
}

Работа с классами

Перегрузка операций

Для перегрузки операции используется специальная форма функции, называемая функцией операции. Функция операции имеет следующую форму, в которой ор — это символ перегружаемой операции:

operatorop (список-аргументов)

Имя функции operator+ () позволяет вызывать ее как в нотации
с функцией, так и в нотации с операцией.

total = coding.operator+(fixing); // нотация с функцией
total = coding + fixing; // нотация с операцией
  • Перегруженные операции должны иметь как минимум один операнд типа,
    определяемого пользователем. Это предотвращает перегрузку операций,
    работающих со стандартными типами. То есть переопределить операцию "минус" (-) так, чтобы она вычисляла сумму двух вещественных чисел вместо разности, не получится. Это ограничение сохраняет здравый смысл, заложенный в программу, хотя и несколько препятствует полету творчества.
  • Нельзя использовать операцию в такой манере, которая нарушает
    правила синтаксиса исходной операции.

Друзья

Существует три разновидности друзей:

  • дружественные функции;
  • дружественные классы;
  • дружественные функции-члены.

Первый шаг в создании дружественной функции предусматривает помещение
прототипа в объявление класса и предварение его префиксом в виде ключевого слова friend:

friend Time operator* (double m, const Time & t); // размещается в объявлении класса

На основе этого прототипа можно сделать два вывода.

  • Несмотря на то что функция operator* () присутствует в объявлении класса,
    она не является функцией-членом класса. Поэтому она не вызывается через
    операцию членства (.).
  • Несмотря на то что функция operator* () не является функцией-членом класса,
    она имеет те же права доступа, что и функции-члены.

Для перегрузки операции « с целью отображения объекта класса с_name используется дружественная функция со следующим определением:

ostream & operator<<(ostream & os, const c_name & obj)
{ 
  os «... ; // отображение содержимого объекта
  return os;
}

Ключевое слово friend используется т��лько в прототипе, представленном в объявлении класса. В определении функции оно указывается, только если не присутствует в самом прототипе.

Автоматические преобразования и приведения типов в классах

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

long count = 8; // значение 8 типа int преобразуется в тип long
double time =11; // значение 11 типа int преобразуется в тип double

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

int * р = 10; // конфликт типов.

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

int * р = (int *) 10; // нормально, р и (int *) 10 — оба указатели

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

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

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

operator имяТипа();

Классы и динамическое выделение памяти

  • Если для инициализации указателя-члена в конструкторе применяется операция new, то в деструкторе нужно использовать операцию delete.
  • Операции new и delete должны быть согласованными. Операции new должна
    соответствовать операция delete, а операции new [ ] —
    операция delete [ ].
  • Если применяется несколько конструкторов, все они должны единообразно
    использовать операцию new — либо все со скобками, либо все без скобок. В классе существует только один деструктор, и все конструкторы должны быть
    совместимы с ним.
  • Необходимо определить конструктор копирования, в котором инициализация
    одного объекта другим выполняется с помощью глубокого копирования. Обычно конструктор должен быть построен по следующему образцу:
String::String(const String & st)
{
  num_strings++;               // при необходимости обработка обновления
                               // статического члена
  len = st.len;                // та же длина, что и у копируемой строки
  str = new char [len + 1] ;   // выделение памяти
  std:istrcpy(str, st.str)     // копирование строки в новое место
}

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

  • Необходимо определить операцию присваивания, в которой копирование
    одного объекта в другой осуществляется с помощью глубокого копирования. Обычно метод класса должен быть построен по следующему образцу:
String & String::operator=(const String & st)
{
  if (this == &st)  // присваивание объекта самому себе
    return *this;   // готово
  delete [] str;    // освобождение старой строки
  len = st. len;    
  str = new char [len + 1];  // получение памяти для новой строки
  std:istrcpy(str, st.str);  // копирование строки
  return *this;              // возврат ссылки на вызвавший объект
}

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


Наследование классов

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

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

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

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

Порождение класса:

class Имя_Производного_Класса : (public | protected | private) Имя_Базового_Класса
{
    объявления членов класса
};

Ключевые слова public, protected, private используются для указания того, насколько члены базового класса будут доступны из производного. Использование в заголовке производного класса public означает, защищенные и открытые члены базового класса должны наследоваться как защищенные и открытые члены производного класса. Это означает, что и защищенные (protected), и открытые (public) члены базового класса доступны в производном классе и также являются защищенными и закрытыми. Закрытые (private) члены базового класса недоступны для производного класса. Заметим, что закрытые и защищенные члены базового класса недоступны из классов, не являющихся производными базового. Открытое наследование, называемое также интерфейсным наследованием, означает, что производный тип является подтипом базового (отношение ISA). Каждый объект производного класса является объектом базового типа.

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

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

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

Язык C++ позволяет создавать классы одновременно на основе нескольких базовых классов (множественное наследование).

В случае множественного наследования класс определяется следующим образом:

class имя_класса : (public | protected | private) имя_базового_класса1, (public | protected | private) имя_базового_класса2, … 
{
    объявления членов класса
};

Использование множественного наследования может создавать неоднозначности. Например, можно представить себе следующую ситуацию: Class1 является базовым для классов Class11 и Class12. Они в свою очередь являются базовыми для класса Class2.

Предположим теперь, что в классах Class11 и Class12 переопределяется некоторый метод method1 базового класса Class1. Возникает вопрос, какой же экземпляр метода будет использоваться в классе Class2? Решить эту проблему можно, добавляя в качестве префиксов имя класса-источника: Class12::method1.

Закрытое наследование не носит характера отношения подтипов. При закрытом наследовании мы повторно используем код базового класса, но не предполагаем рассматривать объекты производного класса как объекты базового. Закрытое наследование называется отношением LIKEA, или наследованием реализации. Закрытое и защищенное наследование не создает иерархии типов.
В основном такое наследование используется для повторного использования кода. Практически закрытое наследование не используется.

Списки инициализаторов членов

Конструктор для производного класса может использовать механизм списка
инициализаторов для передачи значений конструктору базового класса. Вот пример:

derived::derived(тип! х, тип2 у) : base(x,y) // список инициализаторов
{
  ...
}

Здесь derived — производный класс, base — базовый класс, а х и у —
переменные, которые используются конструктором базового класса.

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

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


Виртуальные функции. Абстрактные классы.

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

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

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

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

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

Деструкторы могут быть виртуальными. Если класс имеет хотя бы одну виртуальную функцию, рекомендуется деструкторы также объявлять виртуальными.

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

virtual прототип_функции = 0;

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


Пространства имен

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

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

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

namespace MyNames
{
// Классы, отдельные функции, глобальные переменные. 
const int MAXLEN = 9999; 
void func(); 
class A;
}

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

Блок namespace с одним и тем же именем можно записать несколько раз каждый блок добавляет в пространство имен новые имена. Можно написать:

namespace MyNames
{
doublе f(double);
}

В этом случае в пространстве имен MyNames будет четыре имени.

Уточнение имени

Вне своего пространства имена уточняются с помощью операции разрешения видимости:

if (k < MyNames::MAXLEN) fl(a[k]); 
MyNames::func();
MyNames::A a = new MyNames::A();

Пространство имен можно сравнить с городом. Все улицы города должны носить разные имена. Но в разных городах названия улиц могут совпадать. В каждом городе есть Садовая улица, Шоссейная улица, Центральная ули­ца. Поэтому, говоря о разных городах, мы уточняем название улицы, добав­ляя город, в котором она расположена. Если же разговор идет об одном го­роде, в таком уточнении нет нужды, ясно, о какой улице идет речь.

Директива using namespace

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

using namespace MyNames; 
if (k < MAXLEN) fl(a[k]); 
func();
A a = new AO;

Увидев директиву using namespace, компилятор будет во всех следующих строках отыскивать встреченные имена в текущем пространстве и в про­странстве имен MyNames. Разумеется, имена в этих пространствах должны быть различны. Совпадающие имена придется уточнять именем пространст­ва имен.

Вложение пространств имен

Директиву using namespace можно записать и внутри блока, определяюще­го пространство имен:

namespace MyNewNames{
using namespace MyNames; void f2();
}

Тем самым пространство имен MyNames вкладывается в пространство имен MyNewNames. Имена из пространства имен MyNames теперь лежат в пространстве имен MyNewNames и можно написать:

if (k < MyNewNames::MAXLEN) fl(a[k]); 
MyNewNames::func();
MyNewNames::A a = new MyNewNames::A();

Такие имена будут сначала отыскиваться в пространстве имен MyNewNames, затем в пространствах имен, указанных в директивах using namespace.

Объявление using

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

using MyNames::MAXLEN;
using MyNames::A;
if (k < MAXLEN) fl(a[k]);
MyNames::func();
A a = new AO;

Для имен из стандартной библиотеки классов языка С++ выделено пространство имен, названное std. В этой книге мы часто будем использовать данные имена и применять для сокращения записи директиву using namespace std.

Неименованное пространство имен

namespace{
double fmod(double, int); const double FM = 2.4523;
}

На самом деле компилятор даст какое-то уникальное имя этому простран­ству. Поскольку к именам из этого пространства как-то надо обращаться, компилятор тут же вставит директиву using namespace.

Вследствие этого именами из неименованного пространства имен можно будет пользоваться в пределах файла, в котором все это напи­сано. Итак, неименованное пространство имен ограничивает видимость имени файлом.

Псевдонимы пространства имен

Для уже введенного имени пространства имен можно создать псевдоним:

namespace mnn = MyNewNames;

Псевдоним можно использовать для сокращения записи — вместо MyNewNames :: MAXLEN писать mnn:: MAXLEN. Кроме того, псевдоним можно использовать так же, как мы обычно используем константы — при необходимости сменить истинное имя пространства имен, например, при обновлении библиотеки классов, нам достаточно сменить это имя только в одном месте.


Шаблоны

Шаблоны Класса

template <class T>
struct Array{
 T& operator [] (size_t i){
 return data_[i];
 }
private:
 T* data_;
 size_t size_;
};
Примечание: вместо class можно писать typename

Компилятор подставит вместо T формальный параметр.

Array<int> m;
Array<double> d;

Без этих строк код шаблона не будет скомпилирован и не попадёт в объектный файл. Шаблон - это декларация, в нём нет выполняемого кода.

Процесс создания класса называется инстанцированием.

Если ниже создать другой экземпляр с тем же типом:

Array<int> q;

то не возникнет повторения, это будет экземпляр того же самого класса Array<int>, т.о. шаблон - не просто подстановка.

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

//объявление
template <class T>
struct Array{
 T& operator [] (size_t i);
private:
 T* data_;
 size_t size_;
};

//определение
template<class T>
T& Array<T>::operator [] (size_t i){
 return data_[i];
}

Из эстетических соображений можно вынести определение в отдельный заголовочный файл (например “array_impl.h”) и подключить его после объявления.

Итог:

+ Шаблоны - конструкции языка, компилятор понимает, что это.

- Это довольно громоздко, реализация должна быть известна.

Шаблонные функции.

template<class T>
void swap(T& a, T& b){
 T t(a);
 a = b;
 b = t;
}
 
int i=10, j=20;
swap<int>(i, j);

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

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

Написав просто: swap(i, j) компилятор попытается угадать какой тип имеют i и j и сам подставит int.

Однако:

int i = 10;
long j = 20;
swap(i, j);

вызовет ошибку, т.е. long это не и тоже самое что int. Компилятор в этом случае не может выбрать между swap<long> и swap<int>.

Если же написать: swap<long>(i, j); то привести int к long возможно, и код будет работать.

Шаблонные методы

template <class T>
struct Array{
 template<class V>
 Array<T>& operator = (Array<V> const & m);
};
 
template<class T>
template<class V>
Array<T>& Array<T>::operator = (Array const & m) { … }
 
//использование
Array<int> m;
Array<double> d;
d = m;

Просто запись Array внутри класса означает Array с уже подставленным параметром. Вне класса это не действует.

Ещё пример умного определения типа компилятором:

template <class T>
void sort(Array<T>& m) { … }
sort(d);

Компилятор поймёт, что в sort передан массив из double.

Пример применения двух типов для шаблона:

template <class F, class S>
struct pair{
 F first;
 S second;
};