Студопедия

КАТЕГОРИИ:

АстрономияБиологияГеографияДругие языкиДругоеИнформатикаИсторияКультураЛитератураЛогикаМатематикаМедицинаМеханикаОбразованиеОхрана трудаПедагогикаПолитикаПравоПсихологияРиторикаСоциологияСпортСтроительствоТехнологияФизикаФилософияФинансыХимияЧерчениеЭкологияЭкономикаЭлектроника


Использование символа NULL




Символьная строка представляет собой массив символов, за которыми следует символ NULL ('\0'). При объявлении символьной строки вы объявляете массив типа char. Когда программа позднее присваивает символы строке, она отвечает за добавление символа NULL, который представляет конец строки.

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

Следующая программа LOOPNULL.CPP слегка изменяет предыдущую программу, используя цикл for для вывода содержимого строки:

#include <iostream.h>

void main(void)

{
char alphabet[34]; //33 символа плюс NULL char letter;
int index;
for (letter = 'A', index = 0; letter <= 'Я'; letter++, index++) alphabet[index] = letter;
alphabet[index] = NULL;
for (index = 0; alphabet[index] 1= NULL; index++) cout << alphabet[index];
cout << endl;
}

Как видите, цикл for по одному исследует символы строки. Если символ не NULL (не последний символ строки), цикл выводит символ, увеличивает индекс, и процесс продолжается.

Как 'А' отличается от "А"

При рассмотрении программ на C++ вы можете встретить символы, заключенные в одинарные кавычки (например, 'А') и символы, заключенные в

Рис. 17.3.Как компилятор C++ хранит символьную константу 'А' и строковую константу "А".

двойные кавычки ("А"). Символ внутри одинарных кавычек представляет собой символьную константу. Компилятор C++ выделяет только один байт памяти для хранения символьной константы. Однако символ в двойных кавычках представляет собой строковую константу —указанный символ и символ NULL (добавляемый компилятором). Таким образом, компилятор будет выделять два байта для символьной строки. Рисунок 17.3 иллюстрирует, как компилятор C++ хранит символьную константу 'А' и строковую константу "А".

  1. Назначение и состав класса String.

Применение встроенного строкового типа чревато ошибками и не очень удобно из-за того, что он реализован на слишком низком уровне. Поэтому достаточно распространена разработка собственного класса или классов для представления строкового типа – чуть ли не каждая компания, отдел или индивидуальный проект имели свою собственную реализацию строки. Да что говорить, в предыдущих двух изданиях этой книги мы делали то же самое! Это порождало проблемы совместимости и переносимости программ. Реализация стандартного класса string стандартной библиотекой С++ призвана была положить конец этому изобретению велосипедов.
Попробуем специфицировать минимальный набор операций, которыми должен обладать класс string:

  • инициализация массивом символов (строкой встроенного типа) или другим объектом типа string. Встроенный тип не обладает второй возможностью;
  • копирование одной строки в другую. Для встроенного типа приходится использовать функцию strcpy();
  • доступ к отдельным символам строки для чтения и записи. Во встроенном массиве для этого применяется операция взятия индекса или косвенная адресация;
  • сравнение двух строк на равенство. Для встроенного типа используется функция strcmp();
  • конкатенация двух строк, получая результат либо как третью строку, либо вместо одной из исходных. Для встроенного типа применяется функция strcat(), однако чтобы получить результат в новой строке, необходимо последовательно задействовать функции strcpy() и strcat();
  • вычисление длины строки. Узнать длину строки встроенного типа можно с помощью функции strlen();
  • возможность узнать, пуста ли строка. У встроенных строк для этой цели приходится проверять два условия:

char str = 0;

//...

if ( ! str || ! *str )

return;

 

27. Класс String. Функции определения размера строки, установки и извлечения символа.

 

Определение длины строки strlen():

 

Функция strlen( ) позволяет определять длину строки числом символов

int String::strlen(const char *s){

int i;

for(i=0; *s++; i++);

return i;

}

 

Извлечение символа с указанным индексом at():

char String::at (int index) {

return s[index];

}

Установка символа строки setchar():

void String::setchar (const char *symbol, int index) {

if (index >= 0 && index <= n)

s[index] = symbol[0];

}

28. Класс String. Функция удаления фрагмента строки со склеиванием.

// Удаление фрагмента строки

void String::erase(int start, int end) {

int i, j, lp = n + start - end -1;

char *new_str = new char[lp+1];

for(i=0; i<start; i++)

new_str[i] = s[i];

for(i--, j=end; i<lp; i++, j++)

new_str[i] = s[j];

new_str[i]='\0';

delete [] s;

s = new char [lp+1];

n = lp;

for(int i=0; i<n; i++)

s[i] = new_str[i];

s[n]='\0';

}

29. Класс String. Функция вставки строки в текст с определенного места.

 

// Вставка фрагмента в строку

void String::insert(int index, const char *st) {

int i=0, j=0, u=0, len = strlen(st), k = n + len;

char *new_str = new char[k+1];

for(i; i<index; i++, u++)

new_str[i] = s[i];

for(j; j<len; j++, i++)

new_str[i] = st[j];

for(i; i<k; i++, u++)

new_str[i] = s[u];

delete [] s;

s = new char [k+1];

n = k;

s = new_str;

s[n]='\0';

}

30. Класс String. Добавление строки в конец текста.

 

// Добавление фрагмента в конец строки

void String::append(const char *st) {

int len = strlen(st), i, k = n + len;

char *new_str = new char[k+1];

for(i=0; i<n; i++)

new_str[i]=s[i];

for(i; i<k; i++) {

new_str[i]=*st++;

}

new_str[i]='\0';

delete[] s;

s= new char [k+1];

s = new_str;

n = k;

}

 

31. Конструкторы класса String.

 

// Конструктор без параметров

String::String(void) {

s = new char [6];

strcpy (s, "empty");

n = 5;

}

 

// Конструктор с параметрами

String::String (const char * s) {

n = strlen (s);

String::s = new char [n+1];

for(int i=0; i<n; i++)

String::s[i]=s[i];

String::s[n]='\0';

}

 

// Конструктор копий

String::String (const String &ob) {

String::n = ob.n;

String::s = new char [ob.n+1];

for(int i=0; i<n; i++)

String::s[i]=ob.s[i];

String::s[n]='\0';

}

 

32. Структуры. Инициализация и обращение к элементам структуры

 

Структура – тип данных, задаваемый пользователем. В общем случае при работе со структурами следует выделить четыре момента:

- объявление и определение типа структуры,

- объявление структурной переменной,

- инициализация структурной переменной,

- использование структурной переменной.

Определение типа структуры представляется в виде

struct ID

{

<тип> <имя 1-го элемента>;

<тип> <имя 2-го элемента>;

…………

<тип> <имя последнего элемента>;

};

Определение типа структуры начинается с ключевого слова struct и содержит список объявлений, заключенных в фигурные скобки. За словом struct следует имя типа, называемое тегом структуры (tag – ярлык, этикетка). Элементы списка объявлений называются членами структуры или полями. Каждый элемент списка имеет уникальное для данного структурного типа имя. Однако следует заметить, что одни и те же имена полей могут быть использованы в различных структурных типах.

Определение типа структуры представляет собой шаблон (template), предназначенный для создания структурных переменных.

Объявление переменной структурного типа имеет следующий вид:

struct ID var1;

при этом в программе создается переменная с именем var1 типа ID. Все переменные, использующие один шаблон (тип) структуры, имеют одинаковый набор полей, однако различные наборы значений, присвоенные этим полям. При объявлении переменной происходит выделение памяти для размещения переменной. Шаблон структуры позволяет определить размер выделяемой памяти.

В общем случае, под структурную переменную выделяется область памяти не менее суммы длин всех полей структуры, например,

struct list

{

char name[20];

char first_name[40];

int;

}L;

В данном примере объявляется тип структура с именем list, состоящая из трех полей, и переменная с именем L типа struct list,при этом для переменной L выделяется 64 байта памяти.

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

Создание структурной переменной возможно двумя способами: с использованием шаблона (типа) или без него.

Создание структурной переменной pt на основе шаблона выполняется следующим образом:

struct point //Определение типа структуры

{

int x;int y;

};

……

struct point pt; //Создание структурной переменной

Структурная переменная может быть задана уникальным образом:

struct //Определение анонимного типа структуры

{

char name[20];

char f_name[40];

char s_name[20];

} copymy; //Создание структурной переменной

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

Формат: struct ID name_1={значение1, … значениеN};

Внутри фигурных скобок указываются значения полей структуры, например,

struct point pt={105,17};

при этом первое значение записывается в первое поле, второе значение – во второе поле и т. д., а сами значения должны иметь тип, совместимый с типом поля.

Над структурами возможны следующие операции:

- присваивание значений одной структурной переменной другой структурной переменной, при этом обе переменные должны иметь один и тот же тип;

- получение адреса переменной с помощью операции &;

- осуществление доступа к членам структуры.

Присваивание значения одной переменной другой выполняется путем копирования значений соответствующих полей, например:

struct point pt={105,15},pt1;

pt1=pt;

В результате выполнения этого присваивания в pt1.x будет записано значение 105, а в pt1.y – число 15.

Работа со структурной переменной обычно сводится к работе с отдельными полями структуры. Доступ к полю структуры осуществляется с помощью операции. (точка) посредством конструкции вида:

имя_структуры.имя_поля_структуры;

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

Например,

struct list copy = {"Ivanov","Petr",1980};

Обращение к "Ivanov" имеет вид copy.name. И это будет переменная типа указатель на char. Вывод на экран структуры copy будет иметь вид:printf("%s%s%d\n",copy.name,copy.first_name,copy.i);

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

33. Структуры и функции

 

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

Существует три способа передачи структур функциям:

- передача компонентов структуры по частям;

- передача целиком структуры;

- передача указателя на структуру.

Например, в функцию передаются координаты двух точек:

void showrect(struct point p1,struct point p2)

{

printf("Левый верхний угол прямоугольника:%d %d\n",

p1.x, p1.y);

printf("Правый нижний угол прямоугольника:

%d %d\n", p2.x, p2.y);

}

При вызове такой функции ей надо передать две структуры:

struct point pt1={5,5},

pt2={50,50};

showrect(pt1,pt2);

Теперь рассмотрим функцию, возвращающую структуру:

struct point makepoint (int x,int y) /*makepoint – формирует точку по компонентам x и y*/

{

struct point temp;

temp.x = x;

temp.y = y;

return temp;

}

Результат работы этой функции может быть сохранен в специальной переменной и выведен на экран:

struct point buf;

buf=makepoint(10,40);

printf("%d %d\n",buf.x,buf.y);

После выполнения этого фрагмента на экран будут выведены два числа: 10 и 40.

34. Средство typedef

 

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

typedef тип новое_имя;

где тип — это любой тип данных языка С, а новое_имя — новое имя этого типа. Новое имя является дополнением к уже существующему, а не его заменой.

Например, для float можно создать новое имя с помощью

typedef float balance;

Это выражение дает компилятору указание считать balance еще одним именем float. Затем, используя balance, можно создать переменную типа float:

balance over_due;

Теперь имеется переменная с плавающей точкой over_due типа balance, a balance является еще одним именем типа float.

Теперь, когда имя balance определено, его можно использовать и в другом операторе typedef. Например, выражение

typedef balance overdraft;

дает компилятору указание признавать overdraft в качестве еще одного имени balance, которое в свою очередь является еще одним именем float.

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

35. Система ввода-вывода С++.

 

Ввод/вывод в C++ осуществляется с помощью потоков библиотеки C++, доступных при подключении заголовочного файла iostream.h (в VC++.NET – объекта-заголовка iostream). Поток представляет собой объект какого-либо потокового класса.

Потоковые классы сконструированы на основе базового класса ios:

ios – базовый потоковый класс;

istream – класс входных потоков;

ostraem – класс выходных потоков;

iostream – класс двунаправленных потоков ввода/вывода.

В потоковые классы включены операторы добавления данных в поток << и извлечения данных из потока >>.

На основе класса istream в библиотеке C++ объявлен объект-поток cin, представляющий собой стандартный буферизованный входной поток, связанный обычно с клавиатурой консоли. Извлечение данных из потока имеет следующую форму записи:

int a;

float b;

cin >> a >> b;

где a и b – переменные заданного типа, в которые помещаются данные из потока cin.

В роли разделителей значений в потоке используются пробельные символы (пробел, знак табуляции, перевод строки), поэтому для ввода данных с помощью cin при выполнении программы следует ввести с клавиатуры значения следующими способами:

34 5.78 Enter

или

34 Enter 5.78 Enter

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

На основе класса ostream объявлен объект-поток cout, представляющий собой стандартный буферизованный выходной поток, связанный обычно с дисплеем консоли.

Форма записи добавления данных в поток следующая:

cout << a << b;

при этом значения переменных a и b выводятся на экран без разделителя и в формате, заданном по умолчанию. Перемещение курсора на следующую строку экрана после вывода данных не происходит.

Для перевода курсора на новую строку используется манипулятор endl:

cout << a <<" "<< b << endl;

В этом примере значения переменных a и b на экране разделены пробелом, после вывода данных происходит переход на новую строку, а сами значения выводятся на экран в виде, соответствующем их типу: 34 5.78.

Для потока cin определен специальный метод для ввода символов – getline(Str,Count), позволяющий в строковую переменную Str ввести из потока заданное количество символов(Count−1):

char str1[128];

cout <<"STR1-->";

cin.getline(str1,9);

cout << str1 << endl;

Если при выполнении этого фрагмента программы ввести с клавиатуры последовательность символов abcdefghj, в переменную str1 будут помещены 8 символов и символ '\0', а на экране появится строка abcdefgh.

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

Пример

Форматирование вывода в потоке cout:

#include <iostream.h>

#include <iomanip.h>

int main(void)

{

//Выравнивание по левому краю – left, по правому краю – right

cout.setf(ios_base::left);

//Вывод с плавающей точкой – scientific, с фиксированной – fixed

cout.setf(ios_base::scientific);

//Точность вывода числа на экране

cout << setprecision(3);

double x=2.5,y=125.76435;

//Ширина поля вывода числа – 15 знаков

cout << setw(15) << x << setw(15) << y << endl;

return 0;

}

Ответ на экране

. e +           . e +          

 

 

36. Форматирование с помощью манипуляторов.

 

К ним относятся разнообразные операции, которые приходится применять сразу перед или сразу после операции ввода-вывода. Например:

 

cout << x;
cout.flush();
cout << y;

cin.eatwhite();
cin >> x;

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

 

Идея манипуляторов позволяет такие операции как flush() или eatwhite() прямо вставлять в список операций ввода-вывода. Рассмотрим операцию flush(). Можно определить класс с операцией operator<<(), в котором вызывается flush():

class Flushtype { };

ostream& operator<<(ostream& os, Flushtype)
{
return flush(os);
}

определить объект такого типа

 

Flushtype FLUSH;

и добиться выдачи буфера, включив FLUSH в список объектов, подлежащих выводу:

 

cout << x << FLUSH << y << FLUSH;

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

 

typedef ostream& (*Omanip) (ostream&);

ostream& operator<<(ostream& os, Omanip f)
{
return f(os);
}

Здесь операция вывода использует параметры типа "указатель на функцию, имеющую аргумент ostream& и возвращающую ostream&". Отметив, что flush() есть функция типа "функция с аргументом ostream& и возвращающая iostream&", мы можем писать

 

cout << x << flush << y << flush;

получив вызов функции flush(). На самом деле в файле <iostream.h> функция flush() описана как

 

ostream& flush(ostream&);

а в классе есть операция operator<<, которая использует указатель на функцию, как указано выше:

 

class ostream : public virtual ios {
// ...
public:
ostream& operator<<(ostream& ostream& (*)(ostream&));
// ...
};

В приведенной ниже строке буфер выталкивается в поток cout дважды в подходящее время:

 

cout << x << flush << y << flush;

Похожие определения существуют и для класса istream:

 

istream& ws(istream& is ) { return is.eatwhite(); }

class istream : public virtual ios {
// ...
public:
istream& operator>>(istream&, istream& (*) (istream&));
// ...
};

поэтому в строке

 

cin >> ws >> x;

действительно обобщенные пробелы будут убраны до попытки чтения в x. Однако, поскольку по умолчанию для операции >>пробелы "съедаются" и так, данное применение ws() избыточно.
Находят применение и манипуляторы с параметрами. Например, может появиться желание с помощью

 

cout << setprecision(4) << angle;

напечатать значение вещественной переменной angle с точностью до четырех знаков после точки. Для этого нужно уметь вызывать функцию, которая установит значение переменной, управляющей в потоке точностью вещественных. Это достигается, если определить setprecision(4) как объект, который можно "выводить" с помощью operator<<():

 

class Omanip_int {
int i;
ostream& (*f) (ostream&,int);
public:
Omanip_int(ostream& (*ff) (ostream&,int), int ii)
: f(ff), i(ii) { }
friend ostream& operator<<(ostream& os, Omanip& m)
{ return m.f(os,m.i); }
};

Конструктор Omanip_int хранит свои аргументы в i и f, а с помощью operator<< вызывается f() с параметром i. Часто объекты таких классов называют объект-функция. Чтобы результат строки

 

cout << setprecision(4) << angle

был таким, как мы хотели, необходимо чтобы обращение setprecision(4) создавало безымянный объект класса Omanip_int, содержащий значение 4 и указатель на функцию, которая устанавливает в потоке ostream значение переменной, задающей точность вещественных:

 

ostream& _set_precision(ostream&,int);

Omanip_int setprecision(int i)
{
return Omanip_int(&_set_precision,i);
}

Учитывая сделанные определения, operator<<() приведет к вызову precision(i). Утомительно определять классы наподобие Omanip_int для всех типов аргументов, поэтомуопределим шаблон типа:

 

template<class T> class OMANIP {
T i;
ostream& (*f) (ostream&,T);
public:
OMANIP(ostream (*ff) (ostream&,T), T ii)
: f(ff), i(ii) { }

friend ostream& operator<<(ostream& os, OMANIP& m)
{ return m.f(os,m.i) }
};

С помощью OMANIP пример с установкой точности можно сократить так:

 

ostream& precision(ostream& os,int)
{
os.precision(i);
return os;
}

OMANIP<int> setprecision(int i)
{
return OMANIP<int>(&precision,i);
}

В файле <iomanip.h>можно найти шаблон типа OMANIP,его двойник для istream - шаблон типа SMANIP, а SMANIP - двойник для ioss. Некоторые из стандартных манипуляторов,предлагаемых поточной библиотекой, описаны ниже. Отметим,что программист может определить новые необходимые ему манипуляторы, не затрагивая определений istream, ostream, OMANIP или SMANIP.

Идею манипуляторов предложил А. Кениг. Его вдохновили процедуры разметки (layout ) системы ввода-вывода Алгола68. Такая техника имеет много интересных приложений помимо ввода-вывода. Суть ее в том, что создается объект, который можно передавать куда угодно и который используется как функция. Передача объекта является более гибким решением, поскольку детали выполнения частично определяются создателем объекта, а частично тем, кто к нему обращается.

37. Файловый ввод-вывод

 

Простейший способ выполнить чтение из файла или запись в файл – использовать функции getc() или putc().

Функция getc() выбирает из файла очередной символ; ей нужно только знать указатель на файл, например, char Symb=getc(f_in);

Если при обработке достигается конец файла, то функция getc() возвращает значение EOF(end of file).

Функция putc() заносит значение символа Symb в файл, на который указывает f_out. Формат вызова функции: putc(Symb,f_out);

Пример 1. Текст из файла my_char.txtвыводится на экран. Если файл не найден, на экран выводится сообщение "File not found!":

#include <stdio.h>

int main(void)

{

ch=getc(prt);

FILE *ptr;

unsigned char ch;

if ((ptr=fopen("my_char.txt","r"))!=NULL)

{

ch=getc(ptr);

while (!feof(ptr))

{

printf("%c",ch);

ch=getc(prt);

}

fclose(ptr);

}

else printf("\nFile not found!");

return 0;

}

В этом примере для чтения из файла используется переменная ptr. При открытии файла производится проверка. Если переменной ptr присвоено значение NULL, то файл не найден; на экран выводится соответствующее сообщение, и программа завершается. Если ptr получила ненулевое значение, то файл открыт. Далее выполняется чтение символов из файла до тех пор, пока не будет достигнут конец файла (!feof(ptr)). Прочитанные символы помещаются в переменную ch, а затем выводятся на экран.

Пример 2. Записать в файл буквы, вводимые с клавиатуры. Ввод продолжается до нажатия клавиши F6 или CTRL/z (ввод символа EOF – конца файла):

#include <stdio.h>

int main(void)

{

char c;

FILE *out;

out=fopen("Liter","w");

while ((c=getchar( ) )!=EOF)

fputc(c,out);

fclose(out);

return 0;

}

Функции fscanf() и fprintf() выполняют форматированный ввод/вывод. Чтение из файла выполняет функция fscanf():

fscanf(f_in,[строка формата],[список адресов переменных]);

Функция возвращает количество введенных значений или EOF.

Запись в файл осуществляет функция fprintf():

fprintf(f_out,[строка формата],[список переменных, констант]);

Возвращает количество выведенных байт (символов) или EOF.

Строка формата функций fscanf() и fprintf() формируется так же, как было описано ранее в гл. 5, посвященной консольному вводу/выводу и функциям printf() и scanf().

Следует заметить, что вызов функции

fscanf(stdin,[строка формата],[список адресов переменных]);

эквивалентен вызову

scanf([строка формата],[список адресов переменных]);

Аналогично,

fprintf(stdout, [строка формата],[список переменных, констант]);

эквивалентно

printf([строка формата],[список переменных, констант]);

Рассмотрим примеры программ, использующих эти функции.

Пример 3. В программе создается массив, состоящий из четырех целых чисел. Вывести массив в файл:

#include <stdio.h>

#define n 4

int main()

{

int i=0;

int array[n]={4,44,446,4466};

FILE *out;

out=fopen("num_arr.txt","w");

for(;i<n;i++)

fprintf(out,"%6.2d",array[i]);

fclose(out);

return 0;

}

В результате выполнения программы в файл num_arr.txt будет помещена следующая информация:

                         

Пример 4. Имеется файл данных, содержащий целые числа, разделенные пробелами. Количество чисел в файле неизвестно. Требуется найти среднее арифметическое значение этих чисел:

#include <stdio.h>

int main()

{

int S=0, count=0, numb;

FILE *in;

if ((in=fopen("num_arr.txt","r"))!=NULL)

{

while (!feof(in))

{

fscanf(in,"%d",&numb);

S+=numb;

count++;

printf("%d\n", numb);

}

double aver=(double)S/count;

printf("Average=%lf\n", aver);

fclose(in);

}

else

printf("\nФайл не найден!");

return 0;

}

Чтение чисел из файла выполняется в переменную numb до тех пор, пока не будет достигнут конец файла. Одновременно ведется подсчет количества прочитанных символов в переменнойcount и накопление суммы прочитанных чисел в переменной S. Переменные S и count целые, поэтому для правильного вычисления среднего арифметического, необходимо выполнить преобразование одной из этих переменных в формат double.

38. Пространство имен. Использование namespace.

 

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

Язык C++ позволяет ограничить действие имен некоторой областью, специально для этого объявленной. Такая область называется пространством имен и создается с помощью ключевого словаnamespace. В пространство имен могут быть включены переменные, функции.

Создается пространство имен на внешнем уровне:

namespace имя

{

объявления членов пространства

}

Например,

namespace smp

{

int count;

void calc();

}

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

void smp::calc()

{

count++;

}

Доступ к членам пространства имен извне возможен двумя способами.

1. При помощи явного указания пространства имен и оператора разрешения области видимости:

smp::calc();

cout << smp::count << endl;

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

using namespace smp;

calc();

cout << count << endl;

Если предложение using объявлено на внешнем уровне, видимость членов пространства имен становится глобальной.

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

Требования стандарта соблюдены в компиляторе, входящем в состав Microsoft Visual C++.NET, поэтому при разработке программы в этой среде для обращения к потокам cin, cout следует использовать один из приведенных выше способов:

int a;

std::cout <<"Введите число";

std::cin >> a;

std::cout <<"Вы ввели"<<a<<std::endl;

или

using namespace std;

int a;

cout <<"Введите число";

cin >> a;

cout <<"Вы ввели"<<a<<endl;

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

 

Функция-шаблон определяет общий набор операций, который будет применен к данным различ­ных типов. Используя этот механизм, можно применять некоторые общие алгоритмы к широко­му кругу данных. Как известно, многие алгоритмы логически одинаковы вне зависимости от типа данных, с которыми они оперируют. Например, алгоритм быстрой сортировки Quicksort один и тот же и для массива целых чисел, и для массива чисел с плавающей запятой. Отличается только тип данных, подлежащих сортировке. При помощи создания функции-шаблона (generic function) можно определить сущность алгоритма безотносительно к типу данных. После этого компилятор автоматически генерирует корректный код для того типа данных, для которого создается данная конкретная реализация функции на этапе компиляции. По существу, когда создается функция- шаблон, создается функция, которая может автоматически перегружать сама себя.

Функции-шаблоны создаются с использованием ключевого слова template (шаблон). Обычное значение слова «шаблон» достаточно полно отражает его использование в С++. Шаблон исполь­зуется для создания каркаса функции, оставляя компилятору реализацию подробностей. Общая форма функции-шаблона имеет следующий вид:

template <class птип> возвращаемый_тип имя_функции(список параметров)
{
// тело функции
}

Здесь птип является параметром-типом, «держателем места» (placeholder) для имени типа дан­ных, которое используется функцией. Этот параметр-тип может быть использован в определении функции. Однако это только «держатель места», который будет автоматически заменен компи­лятором на фактический тип данных во время создания конкретной версии функции.

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

// пример шаблона функции
#include <iostream.h>
// шаблон функции
template <class X> void swap(X &a, X &b)
{
X temp;
temp = a;
a = b;
b = temp;
}
int main()
{
int i=10, j = 20;
float x=10.1, у= 23.3;
char a='x', b='z';
cout << "Original i, j: " << i << ' ' << j << endl;
cout << "Original x, y: " << x << ' ' << у << endl;
cout << "Original a, b: " << a << ' ' << b << endl;
swap(i, j); // обмен целых
swap(x, у); // обмен вещественных значений
swap(a, b); // обмен символов
cout << "Swapped i, j : " << i << ' ' << j << endl;
cout << "Swapped x, y: " << x << ' ' << у << endl;
cout << "Swapped a, b: " << a << ' ' << b << endl;
return 0;
}

Рассмотрим эту программу более внимательно. Строка

template <class X> void swap (X &а, X &b)

указывает компилятору, что создается шаблон. Здесь X — шаблон типа, используемый в качестве параметра-типа. Далее следует объявление функции swap() с использованием типа данных X для тех параметров, которые будут обмениваться значениями. В функции main() функция swap() вы­зывается с передачей ей данных трех различных типов: целых чисел, чисел с плавающей запятой и символов. Поскольку функция swap() является функцией-шаблоном, то компилятор автоматичес­ки создаст три разные версии функции swap() — одну для работы с целыми числами, другую для работы с числами с плавающей запятой и, наконец, третью для работы с переменными символь­ного типа.

 

40. Шаблон класса.

 

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

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

template < class птип> class имя_класса {
...
}

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

После создания класса-шаблона можно создать конкретный экземпляр этого класса, используя следующую общую форму:

имя_класса <тип> объект;

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

Функции-члены класса-шаблона являются сами по себе автоматически шаблонами. Нет необхо­димости особым образом указывать на то, что они являются шаблонами с использованием ключе­вого слова template.

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

// демонстрация класса-шаблона stack
#include <iostream.h>
const int SIZE = 100;
// создание класса-шаблона stack
template <class SType> class stack {
SType stck[SIZE];
int tos;
public:
stack();
~stack();
void push(SType i);
SType pop();
};
// функция-конструктор stack
template <class SType> stack<SType>::stack()
{
tos = 0;
cout << "Stack Initialized\n";
}
/* функция-деструктор stack
This function is not required. It is included for illustration only. */
template <class SType> stack<SType>::~stack()
{
cout << "Stack Destroyed\n";
}
// помещение объекта в стек
template <class SType> void stack<SType>::push(SType i)
{
if (tos==SIZE) {
cout << "Stack is full. \n";
return;
}
stck[tos] = i;
tos++;
}
// извлечение объекта из стека
template <class SType> SType stack<SType>::pop()
{
if(tos==0) {
cout << "Stack underflow.\n";
return 0;
}
tos --;
return stck[tos];
}
int main()
{
stack<int> a; // создание целочисленного стека
stack<double> b; // создание вещественного стека
stack<char> с; //создание символьного стека
int i;
// использование целого и вещественного стеков
a.push (1);
b.push (99.3);
a.push(2);
b.push(-12.23);
cout << a.pop() << " ";
cout << a.pop() << " ";
cout << b.pop() << " ";
cout << b.pop() << "\n";
// демонстрация символьного стека
for (i=0; i<10; i++) с.push ( (char) 'A'+i);
for (i=0; i<10; i+ + ) cout << c.pop();
cout << "\n";
return 0;
}

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

stack<int> а; // создание целочисленного стека
stack<double> b; // создание вещественного стека
stack<char> с; // создание символьного стека

Обратим внимание, каким образом нужный тип данных подставляется в угловые скобки. Изме­няя тип данных, указываемый при создании объектов класса stack, одновременно изменяется тип данных, хранящихся в стеке. Например, можно создать другой стек, хранящий указатели на символы:

stack<char *> chrptrstck;

Также можно создать стек, содержащий определенный тип данных. Например, можно хра­нить адреса, используя структуру:

struct addr {
char name[40];
char street[40];
char city[30];
char state[3];
char zip[12];
}

Далее можно использовать класс stack для создания стека, в котором хранятся объекты типа addr:

stack<addr> obj;

 

41. Библиотека стандартных шаблонов. Состав.

 

42. Классы-контейнеры.

 

Контейнерные классы -- это универсальные шаблонные классы, предназначенные для хранения элементов заданного типа в смежных областях памяти. Стандарт C++ уже включает в себя большое количество контейнеров, как часть STL (Standard Template Library -- Стандартная Библиотека Шаблонов).

Qt имеет свой набор шаблонных классов. Таким образом, при создании программ разработчик может использовать как контейнерные классы из библиотеки Qt, так и классы из STL

43. Шаблон класса vector.

 

Классы векторов, списков и словарей (map) -- это шаблонные классы, параметризуемые типом объектов, которые предполагается хранить в контейнере. Значения, которые хранятся в контейнерах, могут быть базового типа (например int или double), указателями или классами, которые имеют конструктор по-умолчанию (конструктор, у которого нет входных аргументов или все входные аргументы имеют значения по-умолчанию), конструктор копирования и перегруженный оператор присваивания. Среди классов, которые отвечают этим требованиям, можно назватьQDateTime, QRegExp, QString и QVariant. Классы Qt, которые наследуют QObject, не могут быть помещены в контейнеры, поскольку у них нет конструктора копирования и оператора присваивания. Однако, это не является большой проблемой, поскольку сохраняется возможность помещать в контейнеры указатели этих типов.

В этом разделе мы рассмотрим наиболее общие операции над векторами, а в следующих двух разделах расскажем о списках и словарях (map). Большая часть примеров, рассматриваемых в этой главе, будет основана на классе Film, который хранит название фильма и его продолжительность. (Мы отказались от более подходящего для этого случая названия Movie, потому что это имя очень похоже на QMovie -- класс Qt, который предназначен для показа анимированных изображений.)

Ниже приводится определение класса Film:

class Film

{

public:

Film(int id = 0, const QString &title = "", int duration = 0);

 

int id() const { return myId; }

void setId(int catalogId) { myId = catalogId; }

QString title() const { return myTitle; }

void setTitle(const QString &title) { myTitle = title; }

int duration() const { return myDuration; }

void setDuration(int minutes) { myDuration = minutes; }

 

private:

int myId;

QString myTitle;

int myDuration;

};

 

int operator==(const Film &film1, const Film &film2);

int operator<(const Film &film1, const Film &film2);

 

Мы не включили в класс явное определение конструктора копирования и оператора присваивания, потому что они предоставляются C++ автоматически. Если бы наш класс выполнял дополнительное резервирование памяти, под данные-члены, тогда нам пришлось бы включить в него явную реализацию конструктора копирования и оператора присваивания.

В дополнение к классу мы реализовали два оператора сравнения -- "равно" и "меньше". Оператор "равно" используется для поиска элемента в контейнере. Оператор "меньше" -- используется для нужд сортировки. В данной ситуации нет необходимости реализовать четыре других оператора сравнения ("!=", "<=", ">", ">="), поскольку STL никогда ими не пользуется.

Ниже приводится исходный код трех функций:

Film::Film(int id, const QString &title, int duration)

{

myId = id;

myTitle = title;

myDuration = duration;

}

 

int operator==(const Film &film1, const Film &film2)

{

return film1.id() == film2.id();

}

 

int operator<(const Film &film1, const Film &film2)

{

return film1.id() < film2.id();

}

 

При сравнивании экземпляров Film, используются их числовые идентификаторы, а не названия, поскольку к названию фильма не предъявляется требование уникальности.

Рисунок 11.1. Вектор экземпляров класса Film.


Вектор -- это структура данных, которая хранит элементы, подобно обычному массиву. Главное отличие вектора от массива C++ состоит в том, что вектор всегда "знает", сколько элементов он хранит, и может динамически изменять свой размер. Добавление новых элементов в конец вектора выполняется очень быстро, но операция по вставке новых элементов в начало или в середину вектора требует значительных затрат по времени.

В STL, класс вектора носит имя std::vector<T> и определен в заголовке <vector>. Объявить вектор, который будет хранить массив экземпляров класса Film, можно так:

vector<Film> films;

 

Эквивалентное объявление, использующее класс Qt -- QValueVector<T>:

QValueVector<Film> films;

 

Вектор, созданный подобным образом, изначально имеет размер 0. Если заранее известно количество элементов в векторе, можно явно указать начальный размер в определении и с помощью оператора "[ ]" присвоить значения его элементам.

Очень удобно заполнять вектор с помощью функции push_back(). Она добавляет указанный элемент в конец вектора, увеличивая его размер на 1:

films.push_back(Film(4812, "A Hard Day's Night", 85));

films.push_back(Film(5051, "Seven Days to Noon", 94));

films.push_back(Film(1301, "Day of Wrath", 105));

films.push_back(Film(9227, "A Special Day", 110));

films.push_back(Film(1817, "Day for Night", 116));

 

Как правило, Qt предоставляет функции с теми же именами, что и STL, но в некоторых случаях Qt добавляет к классам дополнительные методы, с более интуитивно понятными именами. Например, классы Qt могут добавлять элементы как с помощью метода push_back(), так и с помощью дополнительного метода append().

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

vector<Film> films(5);

 

films[0] = Film(4812, "A Hard Day's Night", 85);

films[1] = Film(5051, "Seven Days to Noon", 94);

films[2] = Film(1301, "Day of Wrath", 105);

films[3] = Film(9227, "A Special Day", 110);

films[4] = Film(1817, "Day for Night", 116);

 

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

Векторы допускают обход всех элементов в цикле, с использованием оператора "[ ]":

for (int i = 0; i < (int)films.size(); ++i)

cerr << films[i].title().ascii() << endl;

 

В качестве альтернативы -- можно использовать итератор:

vector<Film>::const_iterator it = films.begin();

while (it != films.end()) {

cerr << (*it).title().ascii() << endl;

++it;

}

 

Каждый контейнерный класс имеет два типа итераторов: iterator и const_iterator. Различие между ними заключается в том, что const_iterator не позволяет модифицировать элементы вектора.

Функция-член контейнера -- begin() возвращает итератор, который ссылается на первый элемент в контейнере (например, films[0]). Функция-член контейнера -- end() возвращает итератор, который ссылается на элемент "следующий за последним" (например, films[5]). Если контейнер пуст, значения, возвращаемые функциями begin() и end(), эквивалентны. Это обстоятельство может использоваться для проверки наличия элементов в контейнере, хотя для этой цели гораздо удобнее использовать функцию empty().

Итераторы обладают интуитивно понятным синтаксисом, который напоминает синтаксис указателей языка C++. Для перемещения к следующему или предыдущему элементу, можно использовать операторы "++" b "--", а унарный "*" -- для получения доступа к элементу контейнера, находящемуся в позиции итератора.

Если необходимо отыскать некоторый элемент в векторе, можно воспользоваться функцией STL -- find():

vector<Film>::iterator it = find(films.begin(), films.end(),

Film(4812));

if (it != films.end())

films.erase(it);

 

Она возвращает итератор, указывающий на первый встретившийся элемент вектора, отвечающий критериям поиска (элементы контейнера сравниваются перегруженным operator==() с последним аргументом функции). Определение функции находится в заголовке <algorithm>, где вы найдете множество других шаблонных функций. Qt предоставляет аналоги некоторых из них, правда под другими именами (например, qFind()). Вы можете использовать их, если не желаете пользоваться библиотекой STL.

Сортировка элементов вектора может быть произведена функцией sort():

sort(films.begin(), films.end());

 

Для сравнения элементов вектора она использует оператор "<", если явно не указывается другая функция сравнения. На отсортированных векторах, для поиска некоторого элемента может использоваться функция binary_search(). Она дает результат, аналогичный find() (при условии, что в векторе нет двух фильмов с одинаковыми числовыми идентификаторами), но при этом работает намного быстрее.

int id = 1817;

if (binary_search(films.begin(), films.end(), Film(id)))

cerr << "Found " << id << endl;

 

В позицию итератора, с помощью функции insert(), может быть вставлен новый элемент или удален существующий, с помощью функции erase():

films.erase(it);

 

Элементы, которые следуют за удаляемым будут перемещены на одну позицию влево (или выше, если хотите) и размер вектора будет уменьшен на 1 элемент.

44. Технология программирования и основные этапы её развития

 

Технология программирования представляет собой набор технологических инструкций, включающих:

 


  • указание последовательности выполнения технологических операций;

  • перечисление условий, при которых выполняется та или иная операция;

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


Кроме набора операций и их последовательности, технология определяет способ описания проектируемой системы (модели), используемой на конкретном этапе разработки.

Различают технологии:

 


  • технологии для конкретных этапов разработки или для решения отдельных задач этих этапов, в основе которых лежит ограниченно применимыйметод для конкретной задачи;

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

 

^ Первый этап – «стихийное» программирование. (С момента появления первых вычислительных машин до середины 60-х годов ХХ в.) Практически отсутствовали сформулированные технологии, программирование практически было искусством. Первые программы имели простейшую структуру, состояли из собственно программы на машинном языке и обрабатываемых ею данных. Сложность программ в машинных кодах ограничивалась способностью программиста одновременно мысленно отслеживать последовательность выполняемых операций и местонахождение данных при программировании.

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

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

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

Типичная программа того времени состояла из основной программы, области глобальных данных и набора подпрограмм.

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

Чтобы сократить количество ошибок предложено использовать локальные данные в подпрограммах.

Появление средств поддержки подпрограмм позволило разрабатывать программное обеспечение нескольким программистам параллельно.

В начале 60-х годов ХХ в. – «кризис программирования». Разрабатываемый проект устаревал раньше, чем был готов к внедрению, увеличивалась его стоимость. Многие проекты оставались незавершенными. Все это было вызвано несовершенством технологии программирования. В отсутствии четких моделей описания подпрограмм и методов их проектирования интерфейсы получались сложными, при сборке программного продукта большое количество ошибок согласования. Процесс тестирования занимал более 80% времени разработки. Встает вопрос разработки технологии создания сложных программных продуктов, снижающий вероятность ошибок проектирования.

^ Второй этап – структурный подход к программированию (60 – 70-е годы ХХ в.)

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

Структурный подход – представление задачи в виде иерархии подзадач простейшей структуры. Проектирование «сверху-вниз». Вводились ограничения на конструкции алгоритмов, рекомендовались формальные модели их описания, специальный метод проектирования алгоритмов – метод пошаговой детализации.

Поддержка принципы структурного программирования заложена в основу процедурных языков программирования (PL-1, ALGOL-68, Pascal, C).

Развитие структурирования данных. В языках появилась возможность определения пользовательских типов данных. Начала развиваться технология модульного программирования.

^ Модульное программирование предполагает выделение групп подпрограмм, использующих одни и те же глобальные данные в отдельно компилируемыемодули. Эту технологию поддерживают современные версии языков Pascal и C (C++), языки Ада и Modula.

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


Поделиться:

Дата добавления: 2015-01-19; просмотров: 178; Мы поможем в написании вашей работы!; Нарушение авторских прав





lektsii.com - Лекции.Ком - 2014-2024 год. (0.01 сек.) Все материалы представленные на сайте исключительно с целью ознакомления читателями и не преследуют коммерческих целей или нарушение авторских прав
Главная страница Случайная страница Контакты