Методичка: Введение в объектно-ориентированное программирование на языке С++

Внимание! Если размещение файла нарушает Ваши авторские права, то обязательно сообщите нам

new Type name ;

new Type name Initializer ;

new (Type name) ; .

В любом случае в результате его выполнения, во-первых, захватывает-ся подходящий для размещения Type name кусок свободной памяти, и, во-вторых, адрес начала этого пространства памяти возвращается в качестве значения оператора. Возвращаемое оператором new значение типа void* может быть присвоено указателю любого типа. Использование оператора new с инициализацией относится к прерогативе классов и обсуждается ниже.

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

delete Expression ;

delete [ Expression ] Expression ; .

Обычно Expression - это указатель, которому присвоено значение оператором new. Наиболее общая первая форма записи оператора, вторая - используется, когда освобождаемая память выделялась типом массив. В этом случае выражение в скобках задает количество элементов массива. Оператор delete не возвращает никакого значения.

Пример

#include <stdio.h>

void main( void )

{

int size;

printf("Укажите размер массива - ");

scanf("%d",&size);

int* data = new int[size];

for ( int j=0, j<size; j++ ) data[j]=j;

delete data;

}

Потоки

До сих пор мы пользовались для ввода и вывода информации функциями scanf и printf, использовавшимися в С. В С++ для этих целей предназначены потоки. Не вдаваясь пока в подробности их организации и реализации, укажем, что в С++ определены три потока:

cin- для ввода информации,

cout- для вывода информации,

cerr- для вывода информации об ошибках.

Для помещения информации в поток cout или cerr служит операция <<, а для получения ее из потока cin - >>.

Например, с использованием потоков программа, приведенная в примере на стр. 20, могла бы выглядеть так:

#include <iostream.h>

#include <stdio.h>

#include <stdlib.h>

inline void ERR(char* s1, char* s2="") { cerr << s1 << " " << s2 << "\n"; exit(1); }

inline void ERR(char* s1, int s2) { cerr << s1 << " " << s2 << "\n"; exit(1); }

void main(int argn, char** argv)

{

FILE *f;

int c,n,i;

if (argn<2) ERR("Укажите имя файла");

f=fopen(*(argv+1),"r");

if( f==NULL ) ERR(*(argv+1),"- файл не найден");

cout << "Показать строку номер - ";

cin >> n; i=n;

if ( n<1 ) ERR("Номер строки должен быть больше нуля");

while( n && (c=fgetc(f)) != EOF ) if ( c=='\n' ) n--;

if ( n ) ERR("В файле нет строки номер ",i);

else while( (c=fgetc(f)) != EOF && c!='\n') cout << c;

cout << "\n";

}

Абстракция данных

Язык С++ предоставляет возможность программисту определить новые типы данных и использовать их аналогично стандартным типам. Определяемый пользователем тип задается с помощью классов - специального вида структур, членами которых могут быть не только данные, но и функции. Исходное название, которое было дано Страустрапом языку, - "С с классами". Класс - это расширение понятия структуры, обычного для языка С. От обычных структур классы отличаются тем, что описанные данные подразделяются на общедоступные и скрытые, доступные только для ограниченного набора функций - членов и "друзей" этого класса. Знакомство с классами начнем со структур, в описание которых в С++ могут входить также и функции, и скрытые объекты.

Структуры

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

Конструкция формы

Structure_variable.Member_name

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

Пример описания массива структур и инициализации компонент структуры можно найти на стр.14.

Если определен указатель на структуру Pointer to structure, то для доступа к члену Member name может быть выбрана одна из форм

Pointer to structure --> Member name

или

(*Pointer to structure).Member name

Например, присвоить начальные значения массиву комплексных чисел можно было бы так:

#include <iostream.h>

const n=10;

struct complex { float re,im; };

void main(void)

{

complex array[n], *ptr=array;

for ( int i=0; i<n; i++ ) { ptr-->re=i; (*ptr++).im=2.; }

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

cout << "(" << array[i].re << "," << array[i].im << ")\n";

}

Операторы --> и "." наряду с операторами [] и () имеют наивысший приоритет и выполняются слева направо.

Присваивание и печать комплексных чисел можно было бы определить в виде функций

void assign(complex num, float re, float im)

{

num.re = re;

num.im = im;

}

void print(complex num)

{

cout << "(" << num.re << "," << num.im << ")\n";

}

Но элегантнее на С++ то же самое могло бы быть записано, когда последние две функции являются членами структуры:

#include <iostream.h>

const n = 10;

struct complex

{

float re, im;

void assign ( float r, float i ) { re = r; im = i; }

void print();

};

void complex::print() { cout << "(" << re << "," << im << ")\n"; }

void main(void)

{

complex array[n], *ptr = array;

for ( int i=0; i<n; i++ ) { ptr++-->assign(i,2.); }

for ( i=0; i<n; i++ ) array[i].print();

}

Функции assign и print, являющиеся членами структуры complex, имеют прямой доступ к полям структуры re и im. Обращение к такого рода функции имеет ту же семантику, что и обращение к компонентам структуры. Собственно в описании структуры complex помещен лишь прототип функции print. В определение функции-члена print, вынесенное за рамки описания структуры, с помощью оператора расширения области видимости помещается указание на то, что эта функция относится к структуре complex.

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

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

Таким образом, описание структуры complex могло бы быть и таким:

struct complex

{

private:

float re,im;

public:

void assign (float, float);

void print();

};

Пример

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

#include <iostream.h>

#include <stdlib.h>

inline void ERR(char* s1) { cerr << s1 << "\n"; exit(1); }

const max_len=10;

struct Stack {

private:

int i;

char s[max_len];

public:

void ini() { i=0; }

void put(char c) { if(++i==max_len) ERR("Stack is full"); else s[i]=c; }

char get() { if (i) return s[i--]; else ERR("Stack is empty"); }

int top() { return i; }

};

void main(void)

{

char* s="abcdefgh";

Stack stack;

stack.ini();

cout << s << "\n";

while( *s ) stack.put(*s++);

while( stack.top() ) { cout << stack.get(); }

cout << "\n";

}

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

abcdefgh

hgfedcba

Классы

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

Описание структуры Stack поэтому могло бы выглядеть и так:

class Stack {

int i;

char s[max_len];

public:

void ini();

void put(char);

char get();

int top();

};

Для описания в классе переменных не могут быть использованы модификаторы auto, register, extern, тем не менее возможно использовать модификатор static. Такие переменные создаются один раз и постоянно находятся в памяти, играя роль общей памяти класса. Как обычно, воспользоваться ими можно, прибегнув к оператору расширения области видимости:

Class_name :: identifier ,

если только они имеют статус public.

Пример

enum boolean { false, true };

class str {

char s[100];

public:

static boolean read_only;

void print();

void assign(char*);

};

str::read_only = false;

void main(void)

{

str s1, s2;

if ( !str::read_only ) s1.assign("Example");

}

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

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

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

Пример

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

#include <iostream.h>

#include <stdlib.h>

#include <string.h>

#include <alloc.h>

inline void ERR ( char* str ) { cerr << str << "\n"; exit(1); }

class Stack {

int size;

char* top;

char* s;

public:

Stack ( int ) ;

Stack ( char* ) ;

~Stack () ;

void put ( char ) ;

char get () ;

int istop () ;

};

Stack :: Stack ( int sz )

{

top = s = new char[ size=sz ];

}

Stack :: Stack ( char* str )

{

top = s = new char[ size=strlen(str) ];

while ( *str ) *s++=*str++;

}

Stack :: ~Stack ()

{

delete top;

}

void Stack :: put ( char c )

{

if( top+size==s ) ERR("Stack is full");

*s++ = c;

}

char Stack :: get ()

{

if ( top==s ) ERR("Stack is empty");

return *--s;

}

int Stack :: istop ()

{

return s == top ? 0 : 1;

}

const n = 12;

void main ( void )

{

{

Stack a ( n ) ;

Stack b ( "Hello, Dolly!" ) ;

// Можно и так Stack b = "Hello, Dolly!";

for ( int i='a'; i<'a'+n; i++ ) a.put(i);

for ( i=0; i<n; i++ ) cout << a.get();

cout << "\n";

while( b.istop() ) cout << b.get();

cout << "\n";

cout << "Свободно " << coreleft() << " байт памяти\n";

}

cout << "Свободно " << coreleft() << " байт памяти\n";

}

Результат выполнения программы:

lkjihgfedcba

!ylloD ,olleH

Свободно 61008 байт памяти

Свободно 61040 байт памяти

При описании объектов a и b класса Stack использовались два различных конструктора, причем при описании объекта b он был также инициализирован начальным значением. Начальное значение объекту a присваивается при выполнении программы, а на этапе описания определяется только его размер. Увеличение объема доступной свободной памяти показывает, что при выходе из блока, содержащего объекты a и b, динамически захваченная память была освобождена, т.е. неявно произошло обращение к деструктору класса Stack.

Описание объекта без параметров

Stack c;

привело бы к ошибке, т.к. конструктор с пустым списком параметров в описании класса Stack не указан.

Указатель this

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

Пример

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

#include <iostream.h>

const n = 10;

class item {

char a[n] ; // слово

item* next ; // ссылка на следующий элемент

static item* top ; // вершина списка

public:

item ( char* ) ;

char* getvalue () { return a; }

item* getnext () { return next; }

void insert ( item* s ) { next = s; }

void print () ;

};

item :: item ( char* str )

{

char* ptr = a;

while ( *ptr++ = *str++ );

/* заполнить пустой список */

if ( !top ) { top=this; next=NULL; }

/* проверить вершину списка */

else if ( a[0] < *top-->getvalue() ) { next=top; top=this; }

/* найти нужное место внутри или в конце списка */

else {

item* ptr = top;

while ( ptr-->getnext() && a[0] > *ptr-->getnext()-->getvalue() )

ptr=ptr-->getnext();

next = ptr-->getnext();

ptr-->insert ( this ); }

}

void item::print()

{

item* ptr=top;

while ( ptr ) { cout < ptr-->getvalue() < "\n";

ptr=ptr-->getnext(); }

}

item* item::top=NULL;

void main(void)

{

item* ptr = new item("Zero");

ptr = new item("Hello");

ptr = new item("Allo");

ptr = new item("Bella");

ptr = new item("Cent");

ptr-->print();

}

Результатом выполнения программы является

Allo

Bella

Cent

Hello

Zero

Вершина списка размещается в статической переменной, что делает ее доступной для всех объектов класса item.

Определение статических членов класса

В приведенной выше программе встречается строка

item* item::top=NULL; ,

которая обычно вызывает некоторое недоумение.

Дело в том, что описание статической переменной top в классе item рас¬сматривается транслятором всего лишь как описание, т.е. транслятор не отводит памяти под статические члены класса и не обеспечивает их инициализацию. Это является изменением первоначального определения статических членов С++, которое предполагало их неявное определение и инициализацию нулем. От этого пришлось отказаться в силу того, что не все объекты различных типов могут или должны инициализироваться подобным образом. Например, после того, как пользователем определены какие-либо классы с конструкторами, на их основе возможно описание статических членов нового класса. Поэтому под статические члены класса необходимо явным образом выделить память и их инициализировать. Указанная выше строка именно для этих целей и присутствует в программе.

Статические функции члены класса

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