Пространства имен (namespace)
Пространство имен namespace позволяет группировать под одним именем переменные, функции, классы и т.д. Это позволяет в более «понятном» для человека стиле располагать код программы, делать более простым и интуитивным обращение к тем или иным объектам в самой программе. Проще всего будет рассмотреть использование namespace на примере. Создайте новый проект CLR Console, и добавьте следующий код:
namespace Kirov {
int a = 1;
}
int main(array<System::String ^> ^args)
{
Console::WriteLine(Kirov::a);
Console::ReadKey();
return 0;
}
Объявляется пространство namespace имен Kirov, внутри которого располагается переменная. Чтобы обратиться к содержимому namespace используется оператор двойное двоеточие “::”. Если попытаться обратиться к переменной а напрямую, например:
Console::WriteLine(a);
то компилятор выдаст сообщение об ошибке. Внутри namespace также можно объявлять не только переменные, но и, например, функции:
namespace Kirov {
int a = 1;
void Print() {
Console::WriteLine(a);
}
}
int main(array<System::String ^> ^args)
{
Kirov::Print();
Console::ReadKey();
return 0;
}
Заметьте, что в самой функции Print обращение к переменной идет напрямую без указания Kirov::, так как она объявлена в том же пространстве, что и сама переменная. Данных механизм похож на обращение к локальным переменным внутри функции, где они объявлены, который используется в языке С. Само по себе использование одного namespace в нашем случае не имеет большого смысла. И правда - объявив функцию и переменную просто глобальными, получился бы тот же эффект, и не пришлось бы писать Kirov:: каждый раз перед ними.
Преимущество использования пространств имен появляется при их вложенности, т.е. создание namespace внутри другого namespace. Попробуем на примере описать namespace Kirov, который будет содержать улицы и дома:
namespace Kirov {
namespace Lenina {
namespace Dom9 {
int Kvartira = 1;
}
namespace Dom18 {
int Kvartira = 2;
}
}
namespace Lepse {
namespace Dom37 {
int Kvartira = 3;
}
}
}
int main(array<System::String ^> ^args)
{
Console::WriteLine(Kirov::Lenina::Dom9::Kvartira);
Console::WriteLine(Kirov::Lenina::Dom18::Kvartira);
Console::WriteLine(Kirov::Lepse::Dom37::Kvartira);
Console::ReadKey();
return 0;
}
Первое, что стоит отметить – это использование глобальной переменной Kvartira с одним и тем же именем в разных пространствах. Просто создать три переменные с одним именем нам бы не позволил компилятор, пришлось бы объявлять переменные, например в виде:
int Kirov_Lenina_Dom9_Kvartira = 1;
что не очень удобно с точки зрения программирования.
Во-вторых, использование строгой иерархии позволяет логически располагать переменные, функции, классы в заданном «дереве» имен. Данные способ как раз использует платформа .NET для организации своих стандартных библиотек. Если набрать в коде System:: то откроется выпадающий список переменных, классов и других пространств имен внутри «корневого» пространства System.
Конечно, каждый раз писать Kirov::Lenina::Dom9::Kvartira логично, но не всегда удобно, особенно если идет частое обращение к содержимому пространства имен. Для решения этой проблемы используется ключевое слово using namespace:
using namespace Kirov::Lenina;
int main(array<System::String ^> ^args)
{
Console::WriteLine(Dom9::Kvartira);
Console::WriteLine(Dom18::Kvartira);
Console::WriteLine(Kirov::Lepse::Dom37::Kvartira);
Console::ReadKey();
return 0;
}
который делает «видимым» для кода программы содержимое пространства Kirov::Lenina. Так же поступает Visual Studio, помещая во вновь создаваемые проекты строчку (как вы уже заметили)
using namespace System;
Это сообщает компилятору, что программа будет использовать системную библиотеку System, и позволяет обращаться к ее компонентам без префикса System::, например, при вызове функции вывода на экран:
Console::WriteLine();
На самом деле полный «путь» к этой функции System::Console::WriteLine(). Следует отметить, что оператор using namespace применяется только по отношению к namespace, поэтому нельзя, например, написать такой код:
using namespace System::Console;
WriteLine();
так как System::Console является классом, а не пространством имен (в выпадающем списке содержимого пространства другие namespace обозначаются значком {})
Классы
В предыдущем примере мы рассмотрели организацию пространства имен на примере пространства город Kirov, который содержал улицы и дома. В пространстве мы объявляли переменные и функции, которые обращались к этим переменным. А что если нам потребовалось бы два таких пространства – например с одним мы бы производили какие либо действия, меняли значения переменных и т.п., а другой namespace Kirov оставался бы неизменным как эталон для последующего сравнения. Конечно, можно объявить два таких пространства, например Kirov1 и Kirov2, но это приведет к дублированию кода при их объявлении. А теперь представьте, что улиц внутри будет не две как у нас, а например 50, а позже при разработке программы пространств Kirov понадобится не два, а 100, или еще больше, что тогда будет? Тысячи и тысячи строк громоздкого однотипного кода.
К счастью, уже многие годы большинство языков программирования, в том числе и С++, являются объектно-ориентированными, и оперируют понятиями класс и объект. Класс представляет из себя тип данных, содержащий данные и функции для работы с этими данными:
ref class Kirov {
public:
int a;
};
int main(array<System::String ^> ^args)
{
Kirov k;
Console::WriteLine(k.a);
Console::ReadKey();
return 0;
}
Объявление класса в управляемом C++ осуществляется с помощью ключевого слова ref class. В отличие от namespace, у классов есть область видимости своего содержимого для остальной программы: public (доступно всем) и private (доступно только функциям, объявленным в самом классе). По-умолчанию, содержимое класса недоступно коду извне, поэтому использовалось ключевое слово public:
Как уже было сказано выше, класс является типом данных, поэтому чтобы начать его использовать, надо объявить переменную этого типа. Переменные, типом которых являются классы, называются объектами.
Объекты можно создавать статически, т.е. указав имя класса и имя переменной вслед за ним (как у обычных переменных), тогда доступ к полям класса будет осуществляться через точку «.»:
Kirov k;
Console::WriteLine(k.a);
или же, что предпочтительнее, и наиболее часто используется, создание объектов динамически по ссылке, доступ к полям через оператор ->
Kirov ^k = gcnew Kirov();
Console::WriteLine(k->a);
Ссылка в управляемом С++\CLI имеет тот же смысл, что и указатель в традиционном С++, только обозначается символом ^ вместо звездочки *, и хранит не адрес объекта в памяти, а идентификатор его расположения в «куче мусора» garbage collection библиотеки .NET. Использование менеджера garbage collector позволяет не заботится об очистке памяти, занятой объектами (в С++ приходилось использовать free() или delete), все объекты система будет освобождать сама.
Основное преимущество классов – совмещение данных и кода внутри одной переменной-объекта:
ref class Kirov {
public:
int a;
Void print() {
Console::WriteLine(a);
}
};
int main(array<System::String ^> ^args)
{
Kirov ^k = gcnew Kirov();
k->print();
Console::ReadKey();
return 0;
}
Функции, объявленные внутри класса, называются методами. Если имя функции совпадает с именем класса, то такая функция называется конструктором, и она автоматически вызывается при создании нового объекта класса. Конструктор не возвращает никакого значения, даже void, но может принимать несколько значений как обычная функция, и используется для инициализации переменных в классе при создании объекта. Добавим в наш класс конструктор, позволяющий инициализировать переменную а заданным значением при создании объекта:
ref class Kirov {
public:
int a;
Kirov(int startvalue) {
a = startvalue;
}
Void print() {
Console::WriteLine(a);
}
};
int main(array<System::String ^> ^args)
{
Kirov ^k = gcnew Kirov(777);
k->print();
Console::ReadKey();
return 0;
}
Следует отметить, что оператор gcnew как раз вызывает конструктор класса при создании объекта.
Часть переменных и методов в классе можно закрыть от доступа извне с помощью использования секции private:
ref class Kirov {
private:
String ^Closet;
public:
int Dom;
Kirov(int d, String ^s) {
Dom = d;
Closet = s;
}
Void print() {
Console::WriteLine("Дом №{0}, тип санузла:{1}", Dom, Closet);
}
};
int main(array<System::String ^> ^args)
{
Kirov ^k = gcnew Kirov(5, "Фаянс");
// Console::WriteLine(k->Closet); ошибка доступа
k->print();
Console::ReadKey();
return 0;
}
Если попытаться напрямую обратиться к переменной Closet, объявленной как private, то компилятор выдаст ошибку «cannot access private member declared in class». Например, при создании новой формы Windows Form дизайнер Visual Studio помещает объекты, расположенные на форме (кнопки, поля ввода, метки и т.п.), в секцию private, поэтому доступ к элементам формы невозможен из других форм, или кода, не являющегося методом класса самой формы. Секции private и public можно чередовать в любой последовательности при объявлении класса и в любом количестве. Данные ключевые слова также можно указывать непосредственно перед именем класса, чтобы дать понять компилятору, возможно ли создания объектов этого класса из других модулей:
public ref class Kirov
По-умолчанию используется private, и объекты данного класса можно, проще говоря, создавать только в этом же CPP файле.
Зачастую классы содержат огромное количество переменных, объектов других классов и методов (например, те же формы), и, чтобы не загромождать объявление класса большим количеством кода, допускается описывать методы класса вне его самого:
ref class Kirov {
private:
String ^Closet;
public:
int Dom;
Kirov(int d, String ^s) {
Dom = d;
Closet = s;
}
Void print();
};
void Kirov::print() {
Console::WriteLine("Дом №{0}, тип санузла:{1}", Dom, Closet);
}
int main(array<System::String ^> ^args)
{
Kirov ^k = gcnew Kirov(5, "Фаянс");
k->print();
k->a = 6;
k->print();
Console::ReadKey();
return 0;
}
Внутри самого класса остается только прототип метода void print(); , а сам метод описывается за его пределами как обычная функция, но с указанием имени класса и символов :: перед ее именем void Kirov::print().
Оператор :: может использоваться не только для доступа к пространствам имен, но и для доступа к статическим переменным и методам классов без создания объектов этих классов. «Статическое» означает отсутствие выделения памяти, например статические переменные с заранее заданным значением:
ref class Kirov {
public:
static int a = 666;
static void method1() {
Console::WriteLine(a+1);
}
};
int main(array<System::String ^> ^args)
{
Console::WriteLine(Kirov::a);
Kirov::method1();
Console::ReadKey();
return 0;
}
К слову, функции WriteLine() и Readkey() являются статическими методами класса Console, описанного в пространстве имен System, и мы их тоже используем напрямую без неспосредственного создания объекта класса Console.
Разговор о классах в начале раздела начинался с того, что можно создавать большое количество объектов одного класса если потребуется:
ref class Kirov {
private:
String ^Closet;
public:
int Dom;
Kirov(int d, String ^s) {
Dom = d;
Closet = s;
}