Информация о курсе в систематизированном виде излагаются основные понятия и описываются возможности языка C++. При этом основное внимание уделяется объяснению того, как теми или иными возможностями пользоваться



бет11/15
Дата16.07.2016
өлшемі1.19 Mb.
#204086
түріИнформация
1   ...   7   8   9   10   11   12   13   14   15

#include "Book.h"

. . .

Book b;

Фактически оператор   #include подставляет содержимое файла Book.h в текущий файл перед тем, как начать его компиляцию. Эта подстановка осуществляется во время первого прохода компилятора по программе – препроцессора. Файл Book.h называется файлом заголовков.

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

Таким образом, текст программы на языке Си++ помещается в файлы двух типов – файлы заголовков и файлы программ. В большинстве случаев имеет смысл каждый класс помещать в отдельный файл, вернее, два файла – файл заголовков для объявления класса и файл программ для определения класса. Имя файла обычно состоит из имени класса. Для файла заголовков к нему добавляется окончание ".h" (иногда, особенно в системе Unix, ".hh" или ".H"). Имя файла программы – опять-таки имя класса с окончанием ".cpp" (иногда ".cc" или ".C").

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

Включение файлов может быть вложенным, т.е. файл заголовков может сам использовать оператор #include. Файл Book.h выглядит следующим образом:



#ifndef __BOOK_H__

#define __BOOK_H__

// включить файл с объявлением используемого

// здесь базового класса

#include "Item .h"

#include "String.h"

// объявление класса String

// объявление класса Book

class Book : public Item

{

public:

. . .

private:

String title;

. . .

}; #endif

Обратите внимание на первые две и последнюю строки этого файла. Оператор #ifndef начинает блок так называемой условной компиляции, который заканчивается оператором #endif. Блок условной компиляции – это кусок текста, который будет компилироваться, только если выполнено определенное условие. В данном случае условие заключается в том, что символ __BOOK_H__ не определен. Если этот символ определен, текст между #ifndef и #endif не будет включен в программу. Первым оператором в блоке условной компиляции стоит оператор #define, который определяет символ __BOOK_H__ как пустую строку.

Давайте посмотрим, что произойдет, если в какой-либо .cpp-файл будет дважды включен файл Book.h:

#include "Book.h"

. . .

#include "Book.h"

Перед началом компиляции текст файла Book.h будет подставлен вместо оператора #include:

#ifndef __BOOK_H__

#define __BOOK_H__

. . .

class Book

{

. . .

};

#endif

. . .

#ifndef __BOOK_H__

#define __BOOK_H__

. . .

class Book

{

. . .

};

#endif

В самом начале символ __BOOK_H__ не определен, и блок условной компиляции обрабатывается. В нем определяется символ __BOOK_H__ . Теперь условие для второго блока условной компиляции уже не выполняется, и он будет пропущен. Таким образом, объявление класса Book будет вставлено в файл только один раз. Разумеется, написание два раза подряд оператора #include с одинаковым аргументом легко поправить. Однако структура заголовков может быть очень сложной. Чтобы избежать необходимости отслеживать все вложенные заголовки и искать, почему какой-либо файл оказался вставленным дважды, можно применить изложенный выше прием и существенно упростить себе жизнь.

Еще одно замечание по составлению заголовков. Включайте в заголовок как можно меньше других заголовков. Например, в заголовок Book.h необходимо включить заголовки Item.h и String.h, поскольку класс Book использует их. Однако если используется лишь имя класса без упоминания его содержимого, можно обойтись и объявлением этого имени:

#include "Item.h"

#include "String.h"

class Annotation;

// Annotation – имя некого класса

class Book : public Item

{

public:

Annotation* CreateAnnotation();

private:

String title;

};

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

Общее правило таково, что если объявление класса использует указатель или ссылку на другой класс и не задействует никаких методов или атрибутов этого класса, достаточно объявления имени класса. Разумеется, полное объявление класса Annotation понадобится в определении метода CreateAnnotation.

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



#include

14.4 Препроцессор

В языке Си++ имеется несколько операторов, которые начинаются со знака #: #include, #define, #undef, #ifdef, #else, #if, #pragma. Все они обрабатываются так называемым препроцессором.

Иногда препроцессор называют макропроцессором, поскольку в нем определяются макросы. Директивы препроцессора начинаются со знака #, который должен быть первым символом в строке после пробелов.

14.5 Определение макросов

Форма директивы #define

#define имя определение

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



#define NAME "database"

Connect(NAME);

после препроцессора будет заменен на



Connect("database");

По умолчанию имя определяется как пустая строка, т.е. после директивы



#define XYZ

макроимя XYZ считается определенным со значением – пустой строкой.

Другая форма #define

#define имя ( список_имен ) определение

определяет макрос – текстовую подстановку с аргументами



#define max(X, Y) ((X > Y) ? X : Y)

Текст max(5, a) будет заменен на



((5 > a) ? 5 : a)

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

Директива #undef отменяет определение имени, после нее имя перестает быть определенным.

У препроцессора есть несколько макроимен, которые он определяет сам, их называют предопределенными именами. У разных компиляторов набор этих имен различен, но два определены всегда: __FILE__ и __LINE__. Значением макроимени __FILE__ является имя текущего исходного файла, заключенное в кавычки. Значением __LINE__ – номер текущей строки в файле. Эти макроимена часто используют для печати отладочной информации.

14.6 Условная компиляция

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



#if LEVEL > 3

текст1

#elif LEVEL > 1

текст2

#else

текст3

#endif

Предполагается, что LEVEL – это макроимя, поэтому выражение в директивах #if и #elif можно вычислить во время обработки исходного текста препроцессором.

Итак, если LEVEL больше 3, то компилироваться будет текст1, если LEVEL больше 1, то компилироваться будет текст2, в противном случае компилируется текст3. Блок условной компиляции должен завершаться директивой #endif.

В каком-то смысле директива #if похожа на условный оператор if. Однако, в отличие от него, условие – это константа, которая вычисляется на стадии препроцессора, и куски текста, не удовлетворяющие условию, просто игнорируются.

Директив #elif может быть несколько (либо вообще ни одной), директива #else также может быть опущена.

Директива   #ifdef – модификация условия компиляции. Условие считается выполненным, если указанное после нее макроимя определено. Соответственно, для директивы #ifndef условие выполнено, если имя не определено.

14.7 Дополнительные директивы препроцессора

Директива   #pragma используется для выдачи дополнительных указаний компилятору. Например, не выдавать предупреждений при компиляции, или вставить дополнительную информацию для отладчика. Конкретные возможности директивы #pragma у разных компиляторов различные.

Директива #error выдает сообщение и завершает компиляцию. Например, конструкция

#ifndef unix

#error "Программу можно компилировать

только для Unix!"

#endif

выдаст сообщение и не даст откомпилировать исходный файл, если макроимя unix не определено.

Директива #line изменяет номер строки и имя файла, которые хранятся в предопределенных макроименах __LINE__ и __FILE__.

Кроме директив, у препроцессора есть одна операция ##, которая соединяет строки, например A ## B.


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

15.1 Файлы и переменные

Автоматические переменные определены внутри какой-либо функции или метода класса. Назначение автоматических переменных – хранение каких-либо данных во время выполнения функции или метода. По завершении выполнения этой функции автоматические переменные уничтожаются и данные теряются. С этой точки зрения автоматические переменные представляют собой временные переменные.

Иногда временное хранилище данных требуется на более короткое время, чем выполнение всей функции. Во- первых, поскольку в Си++ необязательно, чтобы все используемые переменные были определены в самом начале функции или метода, переменную можно определить непосредственно перед тем, как она будет использоваться. Во-вторых, переменную можно определить внутри блока – группы операторов, заключенных в фигурные скобки. При выходе из блока такая переменная уничтожается еще до окончания выполнения функции. Третьей возможностью временного использования переменной является определение переменной в заголовке цикла 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;

// ошибка, переменная недоступна

}

15.2 Общие данные

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

Во-первых, определим класс 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

void

RandomGenerator::Init(unsigned long x)

{

previousNumber = x;

}

unsigned long

RandomGenerator::GetNumber(void)

{

unsigned long ltime;

// получить текущее время в секундах,

// прошедших с полуночи 1 января 1970 года

time(<ime);

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

15.3 Глобальные переменные

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

15.4 Повышение надежности обращения к общим данным

Определять глобальную переменную намного удобнее, чем передавать ссылку на генератор случайных чисел в каждый метод и функцию в качестве аргумента. Достаточно описать внешнюю глобальную переменную (включив соответствующий файл заголовков с помощью оператора #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

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::Method1(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()

{

. . .

}



Достарыңызбен бөлісу:
1   ...   7   8   9   10   11   12   13   14   15




©dereksiz.org 2024
әкімшілігінің қараңыз

    Басты бет