учебники, программирование, основы, введение в,

 

Определение, время жизни и области видимости переменных в больших программах

Файлы и переменные
Автоматические переменные определены внутри какой-либо функции или метода класса. Назначение автоматических переменных – хранение каких-либо данных во время выполнения функции или метода. По завершении выполнения этой функции автоматические переменные уничтожаются и данные теряются. С этой точки зрения автоматические переменные представляют собой временные переменные.
Иногда временное хранилище данных требуется на более короткое время, чем выполнение всей функции. Во- первых, поскольку в Си++ необязательно, чтобы все используемые переменные были определены в самом начале функции или метода, переменную можно определить непосредственно перед тем, как она будет использоваться. Во-вторых, переменную можно определить внутри блока – группы операторов, заключенных в фигурные скобки. При выходе из блока такая переменная уничтожается еще до окончания выполнения функции. Третьей возможностью временного использования переменной является определение переменной в заголовке цикла for только для итераций этого цикла:
funct(int N, Book[]& bookArray)

int x;    // автоматическая переменная x 
for (int i = 0; i < N; i++) { 
// переменная i определена только на время
// выполнения цикла for 
String s; 
// новая автоматическая переменная создается
// при каждой итерации цикла заново 
s.Append(bookArray[i].Title());
s.Append(bookArray[i].Author());
cout << s;

cout << s;
}    // ошибка, переменная s не существует 
Если переменную, определенную внутри функции или блока, описать как статическую, она не будет уничтожаться при выходе из этого блока и будет хранить свое значение между вызовами функции. Однако при выходе из соответствующего блока эта переменная станет недоступна, иными словами, невидима для программы. В следующем примере переменная allAuthors накапливает список авторов книг, переданных в качестве аргументов функции funct за все ее вызовы:
funct(int n, Book[]& bookArray)
{     
for (int i = 0; i < n; i++) {
static String allAuthors;
allAuthors.Append(bookArray[i].Author());
cout << allAuthors;
// авторы всех ранее обработанных книг, в
// том числе в предыдущих вызовах функции
}
cout << allAuthors;  
// ошибка, переменная недоступна
}
Общие данные
Иногда необходимо, чтобы к одной переменной можно было обращаться из разных функций. Предположим, в нашей программе используется генератор случайных чисел. Мы хотим инициализировать его один раз, в начале выполнения программы, а затем обращаться к нему из разных частей программы. Рассмотрим несколько возможных реализаций.
Во-первых, определим класс RandomGenerator с двумя методами: Init, для инициализации генератора, и GetNumber — для получения следующего числа.
//
// файл RandomGenerator.h
//
class RandomGenerator
{
public:
RandomGenerator();
~RandomGenerator();
void Init(unsigned long start);
unsigned long GetNumber();
private:
unsigned long previousNumber;
};
//
// файл RandomGenerator.cpp
//
#include "RandomGenerator.h"
#include <time.h>
void
RandomGenerator::Init(unsigned long x)
{
previousNumber = x;
}
unsigned long
RandomGenerator::GetNumber(void)
{
unsigned long ltime;
// получить текущее время в секундах,
// прошедших с полуночи 1 января 1970 года
time(&ltime);
ltime <<= 16;
ltime >>= 16;  
// взять младшие 16 битов
previousNumber = previousNumber * ltime;
return previousNumber;
}
Первый вариант состоит в создании объекта класса RandomGenerator в функции  main и передаче ссылки на него во все функции и методы, где он потребуется.
// файл main.cpp
#include "RandomGenerator.h"
main()
{
RandomGenerator rgen;
rgen.Init(1000);
fun1(rgen);
. . .
Class1 b(rgen);
. . .
fun2(rgen);
}
void
fun1(RandomGenerator& r)
{
unsigned long x = r.GetNumber();
. . .
}
// файл class.cpp
#include "RandomGenerator.h"
Class1::Class1(RandomGenerator& r)
{
. . .
}
void
fun2(RandomGenerator& r)
{
unsigned long x = r.GetNumber();
. . .
}
Поскольку функция main завершает работу программы, все необходимые условия выполнены: генератор случайных чисел создается в самом начале программы, все объекты и функции обращаются к одному и тому же генератору, и генератор уничтожается по завершении программы. Такой стиль программирования допустимо использовать только в том случае, если передавать ссылку на используемый экземпляр объекта требуется нечасто. В противном случае этот способ крайне неудобен. Передавать ссылку на один и тот же объект утомительно, к тому же это загромождает интерфейс классов.
Глобальные переменные
Язык Си++ предоставляет возможность определения глобальной переменной. Если переменная определена вне функции, она создается в самом начале выполнения программы (еще до начала выполнения main). Эта переменная доступна во всех функциях того файла, где она определена. Аналогично прототипу функции, имя глобальной переменной можно объявить в других файлах и тем самым предоставить возможность обращаться к ней и в других файлах:
// файл main.cpp
#include "RandomGenerator.h"
// определение глобальной переменной
RandomGenerator rgen;
main()
{
rgen.Init(1000);
}
void
fun1(void)
{
unsigned long x = rgen.GetNumber();
. . .
}
// файл class.cpp

#include "RandomGenerator.h"
// объявление глобальной переменной,
// внешней по отношению к данному файлу
extern RandomGenerator rgen;
Class1::Class1()
{
. . .
}
void
fun2()
{
unsigned long x = rgen.GetNumber();
. . .
}

Объявление внешней переменной можно поместить в файл-заголовок. Тогда не нужно будет повторять объявление переменной с описателем extern в каждом файле, который ее использует.
Модификацией определения глобальной переменной является добавление описателя static. Для глобальной переменной описатель static означает то, что эта переменная доступна только в одном файле – в том, в котором она определена. (Правда, в данном примере такая модификация недопустима – нам-то как раз нужно, чтобы к глобальной переменной rgen можно было обращаться из разных файлов.)
Повышение надежности обращения к общим данным
Определять глобальную переменную намного удобнее, чем передавать ссылку на генератор случайных чисел в каждый метод и функцию в качестве аргумента. Достаточно описать внешнюю глобальную переменную (включив соответствующий файл заголовков с помощью оператора #include), и генератор становится доступен. Не нужно менять интерфейс, если вдруг понадобится обратиться к генератору. Не следует передавать один и тот же объект в разные функции.
Тем не менее, использование глобальных переменных может привести к ошибкам. В нашем случае с генератором при его использовании нужно твердо помнить, что глобальная переменная уже определена. Простая забывчивость может привести к тому, что будет определен второй объект – генератор случайных чисел, например с именем randomGen. Поскольку с точки зрения правил языка никаких ошибок допущено не было, компиляция пройдет нормально. Однако результат работы программы будет не тот, которого мы ожидаем. (Исходя из определения класса, ответьте, почему).
При составлении программ самым лучшим решением будет то, которое не позволит ошибиться, т.е. неправильная программа не будет компилироваться. Не всегда это возможно, но в данном случае, как и во многих других, соответствующие средства имеются в языке Си++.
Изменим описание класса RandomGenerator:
class RandomGenerator
{
public:
static void Init(unsigned long start);
static unsigned long GetNumber(void);
private:
static unsigned long previousNumber;
};
Определения методов Init и GetNumber не изменятся. Единственное, что надо будет добавить в файл RandomGenerator.cpp, это определение переменной previousNumber:
//
// файл RandomGenerator.cpp
//
#include "RandomGenerator.h"
#include <time.h>
unsigned long RandomGenerator::previousNumber;
. . .
Методы и атрибуты класса, описанные static, существуют независимо от объектов этого класса. Вызов статического метода имеет вид имя_класса::имя_метода, например RandomGenerator::Init(x). У статического метода не существует указателя this, таким образом, он имеет доступ либо к статическим атрибутам класса, либо к атрибутам передаваемых ему в качестве аргументов объектов. Например:
class A
{
public:
static void Method(const A& a);
private:
static int a1;
int a2;
};
void
A::Method(const A& a)

int x = a1;
int y = a2;
int z = a.a2;
}    // обращение к статическому атрибуту
// ошибка, a2 не определен
// правильно 
Статический атрибут класса во многом подобен глобальной переменной, но доступ к нему контролируется классом. Один статический атрибут класса создается в начале программы для всех объектов данного класса (даже если ни одного объекта создано не было). Можно считать, что статический атрибут – это атрибут класса, а не объекта.
Теперь программа, использующая генератор случайных чисел, будет выглядеть так:
// файл main.cpp
#include "RandomGenerator.h"
main()
{
RandomGenerator::Init(1000);
}
void
fun1(void)
{
unsigned long x=RandomGenerator::GetNumber();
. . .
}

// файл class.cpp
#include "RandomGenerator.h"
Class1::Class1()
{
. . .
}
void
fun2()
{
unsigned long x=RandomGenerator::GetNumber();
. . .
}
Такое построение программы и удобно, и надежно. В отличие от глобальной переменной, второй раз определить генератор невозможно – мы и первый-то раз определили его лишь фактом включения класса RandomGenerator в программу, а два раза определить один и тот же класс компилятор нам не позволит.
Разумеется, существуют и другие способы сделать так, чтобы существовал только один объект какого-либо класса.
Кратко суммируем результаты этого параграфа:

  1. Автоматические переменные заново создаются каждый раз, когда управление передается в соответствующую функцию или блок.
  2. Статические и глобальные переменные создаются один раз, в самом начале выполнения программы.
  3. К глобальным переменным можно обращаться из всей программы.
  4. К статическим переменным, определенным вне функций, можно обращаться из всех функций данного файла.
  5. Хотя использовать глобальные переменные иногда удобно, делать это следует с большой осторожностью, поскольку легко допустить ошибку (нет контроля доступа к ним, можно переопределить глобальную переменную).
  6. Статические атрибуты класса существуют в единственном экземпляре и создаются в самом начале выполнения программы. Статические атрибуты применяют тогда, когда нужно иметь одну переменную, к которой могут обращаться все объекты данного класса. Доступ к статическим атрибутам контролируется теми же правилами, что и к обычным атрибутам.
  7. Статические методы класса используются для функций, по сути являющихся глобальными, но логически относящихся к какому-либо классу.

Область видимости имен
Между именами переменных, функций, типов и т.п. при использовании одного и того же имени в разных частях программы могут возникать конфликты. Для того чтобы эти конфликты можно было разрешать, в языке существует такое понятие как область видимости имени.
Минимальной областью видимости имен является блок. Имена, определяемые в блоке, должны быть различны. При попытке объявить две переменные с одним и тем же именем произойдет ошибка. Имена, определенные в блоке, видимы (доступны) в этом блоке после описания и во всех вложенных блоках. Аргументы функции, описанные в ее заголовке, рассматриваются как определенные в теле этой функции.
Имена, объявленные в классе, видимы внутри этого класса, т.е. во всех его методах. Для того чтобы обратиться к атрибуту класса, нужно использовать операции ".", "->" или "::".
Для имен, объявленных вне блоков, областью видимости является весь текст файла, следующий за объявлением.
Объявление может перекрывать такое же имя, объявленное во внешней области.
int x = 7;
class A
{
public:
void foo(int y);
int x;
};
int main()
{
A a; 
a.foo(x);   
// используется глобальная переменная x
// и передается значение 7 
cout << x;
return 1;
}
void
A::foo(int y)

x = y + 1;
{
double x = 3.14;

          cout << x;
}
cout << x;
}    // x – атрибут объекта типа A

  // новая переменная x перекрывает
// атрибут класса x
В результате выполнения приведенной программы будет напечатано 3.14, 8 и 7.
Несмотря на то, что имя во внутренней области видимости перекрывает имя, объявленное во внешней области, перекрываемая переменная продолжает существовать. В некоторых случаях к ней можно обратиться, явно указав область видимости с помощью квалификатора "::". Обозначение ::имя говорит о том, что имя относится к глобальной области видимости. (Попробуйте поставить :: перед переменной x в приведенном примере.) Два двоеточия часто употребляют перед именами стандартных функций библиотеки языка Си++, чтобы, во-первых, подчеркнуть, что это глобальные имена, и, во-вторых, избежать возможных конфликтов с именами методов класса, в котором они употребляются.
Если перед квалификатором поставить имя класса, то поиск имени будет производиться в указанном классе. Например, обозначение A::x показало бы, что речь идет об атрибуте класса A. Аналогично можно обращаться к атрибутам структур и объединений. Поскольку определения классов и структур могут быть вложенными, у имени может быть несколько квалификаторов:
class Example
{
public:
enum Color { RED, WHITE, BLUE };
struct Structure
{
static int Flag;
int x;
};
int y;
void Method();
};
Следующие обращения допустимы извне класса:
Example::BLUE
Example::Structure::Flag
При реализации метода Method обращения к тем же именам могут быть проще:
void
Example::Method()
{
Color x = BLUE;
y = Structure::flag;
}
При попытке обратиться извне класса к атрибуту набора BLUE компилятор выдаст ошибку, поскольку имя BLUE определено только в контексте класса.
Отметим одну особенность типа enum. Его атрибуты как бы экспортируются во внешнюю область имен. Несмотря на наличие фигурных скобок, к атрибутам перечисленного типа Color не обязательно (хотя и не воспрещается) обращаться Color::BLUE.
Оператор определения контекста namespace
Несмотря на столь развитую систему областей видимости имен, иногда и ее недостаточно. В больших программах возможность возникновения конфликтов на глобальном уровне достаточно реальна. Имена всех классов верхнего уровня должны быть различны. Хорошо, если вся программа разрабатывается одним человеком. А если группой? Особенно при использовании готовых библиотек классов. Чтобы избежать конфликтов, обычно договариваются о системе имен классов. Договариваться о стиле имен всегда полезно, однако проблема остается, особенно в случае разработки классов, которыми будут пользоваться другие.
Одно из сравнительно поздних добавлений к языку Си++ – контексты, определяемые с помощью оператора namespace. Они позволяют заключить группу объявлений классов, переменных и функций в отдельный контекст со своим именем. Предположим, мы разработали набор классов для вычисления различных математических функций. Все эти классы, константы и функции можно заключить в контекст   math для того, чтобы, разрабатывая программу, использующую наши классы, другой программист не должен был бы выбирать имена, обязательно отличные от тех, что мы использовали.
namespace math
{
double const pi = 3.1415;
double sqrt(double x);
class Complex
{
public:
. . .
};
};
Теперь к константе pi следует обращаться math::pi.
Контекст может содержать как объявления, так и определения переменных, функций и классов. Если функция или метод определяется вне контекста, ее имя должно быть полностью квалифицировано
double math::sqrt(double x)
{
. . .
}
Контексты могут быть вложенными, соответственно, имя должно быть квалифицировано несколько раз:
namespace first
{
int i;
namespace second    // первый контекст

 

  // второй контекст 
{
int i;
int whati() { return first::i; }
// возвращается значение первого i
int anotherwhat { return i; }
// возвращается значение второго i

}
first::second::whati();   // вызов функции 
Если в каком-либо участке программы интенсивно используется определенный контекст, и все имена уникальны по отношению к нему, можно сократить полные имена, объявив контекст текущим с помощью оператора using.
double x = pi;
using namespace math;
// ошибка, надо использовать math::pi
double y = pi;   
// использовать контекст math
// теперь правильно 

 

 
На главную | Содержание | < Назад....Вперёд >
С вопросами и предложениями можно обращаться по nicivas@bk.ru. 2013 г.Яндекс.Метрика