Конспект лекций по курсу «Объектно-ориентированное программирование»




НазваниеКонспект лекций по курсу «Объектно-ориентированное программирование»
страница2/4
Дата публикации11.08.2013
Размер0.59 Mb.
ТипКонспект
lit-yaz.ru > Информатика > Конспект
1   2   3   4

^ Класс памяти (storage class)

Класс памяти управляет продолжительностью жизни и связыванием объектов (переменных) в C++. (auto, register, static и extern)


объекты

где существуют

Время жизни

создаются

уничтожаются

статические

в коде

глобальные – на этапе компиляции

локальные статические – на этапе выполнения в точке определения

после завершения программы

автоматические

в стеке

на этапе выполнения в точке определения

при выходе из блока

динамические

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

на этапе выполнения при вызове оператора new

при вызове оператора delete



^ Свободная память

Унарные операторы new и delete служат для управления свободной памятью. Свободная память – это предоставляемая системой область памяти для объектов, время жизни которых напрямую управляется программистом. Программист создает объект с помощью ключевого слова new, а уничтожает его, используя delete. Это важно при работе с динамическими структурами данных, такими как списки и деревья. Еще один пример – создание объектов, которые используются после возвращения из функции, в которой они были созданы.

Оператор new принимает следующие формы:

new имя_типа

new имя_типа инициализатор

new имя_типа [выражение]

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

int *p, *q;

p=new int(5);

q=new int[10];
delete выражение

delete [] выражение
Структуры
Массив – набор элементов одинакового типа.

Структура – набор элементов произвольных типов. Элемент структуры называется членом.
struct human{

char *name;

int age;

}; //точка с запятой после '}'

void f(){

human vova;

vova.name="Vova";

vova.age=70;

human piter={"Петя",100};

human *pp=&vova;

pp->age+=1;

}

Имя структуры (human) называется теговым именем и является типом. Можно объявлять переменные типа human (vova, piter). Их можно присваивать, передавать в качестве аргументов и возвращать в качестве значения функции.

Инициализация структуры (см.прог.)

Для доступа к членам структуры используется оператор «точка» – «.» (vova.name). Конструкция вида

переменная_структуры.имя_члена

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

^ Оператор указателя структуры. К объектам типа структуры часто обращаются посредством указателей с помощью оператора «->» :

указатель_на_структуру->имя_члена

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

(*указатель_на_структуру).имя_члена

Структура – тип данных, определяемый пользователем.

Определим тип стека.

const int max_len=1000;

struct stack{

int top;

char s[max_len];

};

void reset(stack *stk){stk->top=0;}

void push(stack *stk, char c){stk->s[stk->top++]=c;}

char pop(stack *stk){return stk->s[--stk->top];}

bool is_empty(const stack *stk){return stk->top==0;}

bool is_full(const stack *stk){return stk->top==max_len;}
void use_stack()

{

stack s;

char str[]="Using Stack!!!";

int i=0;

cout<
reset(&s);

while(str[i]) push(&s,str[i++]);

while(!is_empty(&s))cout<

cout<
// pop(&s);

}

Напечатается строка в прямом и обратном порядках.
КЛАССЫ
В C++ существуют два вида типов: встроенные и типы классов. Встроенные типы включают в себя: char, int, double. Типы классов включают в себя, например: string, vector, istream, CFile, CDialog.

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

Класс применяется для определения типов, соответствующих концепциям приложения.
^ Функции-члены класса

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

Перепишем стек: (сначала определим функции в пределах struct)

struct stack2

{

enum{ max_len=100};

int top;

char s[max_len];
void reset();

void push(char c);

char pop();

bool is_empty()const;

bool is_full()const;

};
void stack2::reset() {top=0;}

void stack2::push(char c){s[top++]=c;}

char stack2::pop(){return s[--top];}

bool stack2::is_empty()const {return top==0;}

bool stack2::is_full()const {return top==max_len;}

Эти функции отличаются тем, что могут обращаться к именам членов класса (top, s) «как есть», непосредственно.

void use_stack2()

{

stack2 s;

char str[]="Using Stack!!!";

int i=0;

cout<
s.reset();

while(str[i])

s.push(str[i++]);

while(!s.is_empty())

cout<
cout<
// pop(&s);

}

При вызове функций-членов применительно к конкретному объекту типа stack2 они действуют на указанные члены именно этого (своего) объекта:

stack2 data, operands;//создаются 2 отдельных объекта со своими

//top и s[]

//но функции не отдельные, а одни и те же

data.reset();//data.top=0;

operands.reset();//operands.top=0;

stack2 *pstack=&operands;

pstack->push('A');//operands.s[operands.top++]='A'

Согласно правилам C++, если функция-член определена (а не только объявлена) в пределах структуры, то она является встраиваемой. Чтобы сделать ее обычной, нужно вынести ее определение за пределы структуры, а оставить только ее объявление (прототип).
< Определение stack2, как написано выше >

Используется оператор разрешения области видимости “::” .
^ Управление доступом

Понятие структуры расширено в С++ так, что появилась возможность вводить закрытые (private) и открытые (public) члены. Это касается как членов данных, так и функций-членов структуры.

К открытым членам структуры имеется непосредственный доступ из любой функции, имеющей доступ к объекту структуры.

К закрытым членам имеют доступ не все функции, а только те, чьи полномочия включают право доступа к этим членам. Таковыми являются функции-члены структур. Другие категории, имеющие такое право, будут рассмотрены позднее.
struct primer{

public:

int a; //public member

double b(int); //public member

private:

int c; //private member

double d(int); //private member

};

double primer::b(int p)

{

a=7; //ok, can access all members

b(3); //ok

c=2; //ok

return d(1); //ok

}
double primer::d(int p)

{

a=7; //ok, can access all members

b(3); //ok

c=2; //ok

return d(1); //ok

}
double use(int p)

{

primer pr;
pr.a=7; //ok, public member

pr.b(3); //ok, public member

// pr.c=2; //error: cannot access private member

// return pr.d(1);//error: cannot access private member

return 1;

}

Переменная primer::c может быть изменена с помощью вызова функции primer::b(), но не непосредственно.

Полезно рассматривать закрытую часть, как код, доступный только разработчику, а открытую часть – как описание интерфейса, который используется клиентами. Разработчик может изменить закрытую часть, и это не повлияет на правильность использования структуры клиентом. То есть при изменении закрытой части структуры не нужно переписывать код, который использует эту структуру (хотя может потребоваться его перекомпиляция).

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

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

class a{

int b;//private

}

Конструкторы

Использование функций типа reset() или init() для инициализации объектов класса неэлегантно и подвержено ошибкам. Программист может забыть проинициализировать объект или сделать это дважды. Лучшим подходом будет предоставление программисту возможности объявить функцию, имеющую явное назначение – инициализация объектов. Ввиду того, что такая функция создает (конструирует) значения данного типа, она называется конструктором. Конструктор распознается по имени, которое совпадает с именем класса.

Конструкторы – это специальные функции-члены, которые определяют, как инициализируются объекты.

Бывает удобно иметь несколько способов инициализации объекта класса. Этого можно добиться, введя несколько конструкторов.
class stack3

{

public:

stack3(); //конструктор по умолчанию

stack3(char);//stack that contains 1 element

void push(char c);

char pop();

bool is_empty()const;

bool is_full()const;

private:

enum{ max_len=100};

int top;

char s[max_len];
};
Конструктор выполняется в момент создания объекта.

void use_stack3()

{

stack3 s; //выполняется конструктор по умолчанию

stack3 s1(’d’);//выполняется конструктор с одним аргументом

stack3 *ps=new stack3();//выполняется конструктор по умолч.

stack3 *ps1=new stack3(’d’);//выполн. конструктор c 1 арг.

delete ps;

delete ps1;

}
Определение конструктора

stack3::stack3():top(0){}
stack3::stack3(char c)//stack that contains 1 element

:top(1)

{

s[0]=c;

}

В определении конструктора используется новый для вас синтаксис. Между символом «:» и открывающей фигурной скобкой ({) находится ряд инициализаторов конструктора (constructor initializers), в данном случае 1 инициализатор. Инициализаторы конструктора велят компилятору инициализировать заданные члены значениями, указанными в соответствующих круглых скобках. В частности, top устанавливается равным 0. Второй конструктор иниц.1 и выполняет присваивание.

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


  1. C++ - среда выделяет память для хранения объекта.

  2. C++ - среда инициализирует объект в соответствии со списком инициализации конструктора.

  3. C++ - среда выполняет тело конструктора.


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

Стоит повторить, что смысл существования конструкторов состоит в гарантии того, что в результате создания объектов эти объекты перейдут в некоторое имеющее определенный смысл состояние.
Пример с 2 инициализаторами конструктора.
class complex{

complex(double real, double imag);

complex(double real);

};
complex::complex(double real, double imag)

{re=real; im=imag;}

complex::complex(double real)

{re=real; im=0;}

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

complex c3(2,3); //инициализация
Конструктор не имеет возвращаемого значения (даже void).
Если мы не определим ни одного конструктора, компилятор синтезирует его за нас. Синтезированный конструктор вызывает конструкторы по умолчанию для всех членов данных-объектов. Те члены данных, которые имеют встроенный тип, не инициализируются (содержат мусор).

Деструкторы

Некоторым классам требуется функция, которая гарантированно вызывается при уничтожении объекта (для закрытия файла, снятия блокировки и т.д.). Такие функции называются деструкторами. Имя деструктора – это имя конструктора с предшествующим символом «тильда»:
~stack3(){delete[] s};


^ Статические члены
Члены данных могут быть объявлены с использованием модификатора класса памяти static. Член данных, объявленный как static, разделяется всеми объектами своего класса и хранится в одном месте. Нестатические члены данных создаются для каждого экземпляра (т.е. объекта) класса. Если бы не наличие статических членов данных, сведения, необходимые всем экземплярам класса, должны были бы объявляться глобальными. Это разорвало бы отношения между данными и их классом. Статический член позволяет данным класса, которые не специфичны для отдельного экземпляра, существовать в области видимости класса.

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

имя_класса :: идентификатор

Здесь используется оператор разрешения области видимости.
Пример:

<сначала how_many_,

затем how_many(),

затем const max_len_>
//stat.h

class stack_stat

{

public:

stack_stat();

~stack_stat();

...

bool is_full()const;

static int how_many();

private:

static const int max_len_=100;

int top;

char s[max_len_];

static int how_many_;

};
//stat.cpp

#include "stat.h"

#include
int stack_stat::how_many_=0;
stack_stat::stack_stat():top(0){++how_many_;}

stack_stat::~stack_stat(){--how_many_;}
...
bool stack_stat::is_full()const {return top==max_len_;}
int stack_stat::how_many(){return how_many_;}


//use.cpp

#include "stat.h"

#include

using namespace std;

void use_stack_stat()

{

stack_stat *ps=new stack_stat();

stack_stat s;

// cout<
cout<
// ps->~stack_stat();

char str[]="Using Stack!!!";

int i=0;

cout<while(str[i])

ps->push(str[i++]);

//*ps=reverse_order(*ps);

while(!ps->is_empty())

cout<
pop();

cout<
// s.pop();

delete ps;

cout<}

В этом примере в переменной how_many отслеживается, сколько объектов класса stack_stat было создано.


^ Функции-члены типа static и const
C++ позволяет использовать функции-члены типа static и const. Синтаксически статическая функция-член содержит модификатор static, предшествующий возвращаемому типу функции внутри объявления класса. Определение вне класса не должно включать этот модификатор:

<пример с stat1>

Синтаксически, функция-член типа const вводится модификатором const, следующим за списком аргументов внутри объявления класса. Определение вне класса также должно включать этот модификатор:

<Пример из stack3>

Обычная функция-член может и читать, и изменять члены данных объекта.

Константная функция-член может читать, но не может изменять члены данных объекта. То есть такая функция не изменяет состояние объекта.

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

double re,im;

public:

double real()const{return re;}

double real_bad_version(){return re;}

}

void f(){

const complex c1(1,0);

complex cx(2,3);

cout<
cout<
cout<
}

Эффективные типы, определяемые пользователем
Для типа, определяемого пользователем, характерен набор операций:

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

  2. Набор функций доступа (функций-селекторов). Эти функции имеют модификатор const, который указывает, что они не должны изменять состояние объектов, для которых они вызваны.

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

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

  5. Перегруженные операторы – функции, обеспечивающие привычную (удобную) форму записи.


class complex{

public:

complex(double re=0,double im=0)

:_re(re),_im(im){}

double real()const{return _re;}

double imag()const{return _im;}

void add(complex);

private:

double _re,_im;

};
complex plus(complex a, complex b);

void print(complex a);

bool operator==(complex a, complex b);//перегруженный оператор

^ Способы использования объектов
Объект может быть создан в качестве:

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

void f(complex c){complex c1(2,3);}

  1. Объекта в свободной памяти, создаваемого при помощи оператора new и уничтожаемого оператором delete.

complex *pc=new complex(2,3);

  1. Нестатического члена-объекта, который создается и уничтожается тогда, когда создается и уничтожается содержащий его объект.

class X{

complex _c;

stack _s;

public:

X(complex &)

};

Аргументы конструкторов-членов указываются в списке инициализации членов в определении конструктора объемлющего класса.

X::X(complex &c)

:_c(c),_s(100)//список инициализации

{}

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

  1. Элемента массива, который создается и уничтожается тогда, когда и создается и уничтожается массив, элементом которого он является.

complex cmas[10];

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

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

{static complex c=sin(3);}

  1. Глобального объекта, объекта в пространстве имен или статического объекта класса, которые создаются один раз «во время запуска программы» и уничтожаются один раз, при ее завершении.

complex c(1,0);

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

k=plus(c1,complex(1,2)).imag(); return c1;

  1. Объекта, размещенного в определенной области памяти.

  2. Члена объединения union, который не может иметь ни конструктора, ни деструктора.

union economy{ rec c; double d};
Копирование объектов класса

Объекты можно копировать.
Существует 2 вида копирования объектов:

  1. инициализация при помощи копирования

stack s2=s1;

  1. присваивание

s2=s1;

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

Если это не то, что требуется для класса, можно реализовать подходящее поведение, написав для копирования собственную(ые) функцию(и). Для случая 1) это будет копирующий конструктор, а для случая 2) – копирующее присваивание.

class stack{

int cap;

int top;

char *s;//копирование по умолчанию создает

//стеки-сиамские близнецы

public:

stack(int capacity=10):cap(capacity),top(0),

s(new char[capacity]){}

~stack(){delete[] s;}

stack(const stack&);

stack& operator=(const stack&);//returns value for s1=s2=s3;

};

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

stack a(100),c;a.push(‘a’);

stack b=a;с=a;
Для правильного копирования объектов данного класса копирующие функции нужно определить так:
#include //for memcpy

stack::stack(const stack& st)

:cap(st.cap),top(st.top),s(new char[st.cap]){

//for(int i=0;i
memcpy(s,st.s,cap);

}

stack& stack::operator=(const stack&st){

if(this!=&st){

delete[] s;
s=new char[cap=st.cap];

//for(int i=0;i
memcpy(s,st.s,cap);
top=st.top;

}

return *this;

}

Копирующий конструктор и копирующее присваивание отличаются из-за того, что копирующий конструктор инициализирует «чистую» память, а копирующее присваивание должно правильно работать с уже созданным объектом (защита от присваивания самому себе, инициализация и копирование новых элементов).
^ Перегрузка операторов
Для того, чтобы перемножить две переменные типа double и сложить с третьей типа double, мы можем написать:

x+y*z

Для переменных cx, cy, cz определенного нами ранее типа complex аналогичные действия мы выполняем так:

complex cx,cy,cz;

cy.mul(cz);

cx.add(cy);

Если же мы запишем

cx+cy*cz

то компилятор выдаст ошибку, т.к. встроенные операции «+» и «*» применимы только ко встроенным типам.

Однако C++ позволяет определить смысл многих операций (в том числе сложения и умножения) для определяемых пользователем типов, например:

class complex{

public:

...

complex operator+(complex);

complex operator*(complex);

private:

double re,im;

};
complex complex::operator +(complex a)

{

return complex(re+a.re,im+a.im);

}

complex complex::operator *(complex a)

{

return complex(re*a.re-im*a.im, re*a.im+im*a.re);

}

Если cy и cz имеют тип complex, то cy+cz означает cy.operator+(cz).

Теперь мы можем записывать действия над комплексными числами в форме, близкой к общепринятой:

complex c1(r1,i1),c2(r2,i2);

c2=с2+c1*complex(2,3);

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

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

+ - * / % = < > += -= *= ++ -- , -> [] () new delete

и других (всего 42).

Следующие операторы не могут быть определены пользователем:

:: (разрешение области видимости)

. (выбор члена класса или структуры)

.* (выбор через указатель на член класса)

?: (тернарный оператор)

Допускается переопределять существующие операторы, но нельзя создавать новые (например ** для степени).

^ Бинарные и унарные операторы.

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

Например: a+b, a*=b, a
и унарными (один операнд)

Например: -a, --a, &a, a++

Унарные операторы, в свою очередь, делятся на префиксные (-a, ++a) и постфиксные (a++).

Есть также один тернарный оператор, a?b:c , но переопределять его действие нельзя.
Оператор на основе своих операндов вычисляет результат, который может использоваться в качестве операнда для последующих операторов:

a*b+c>d  (((a*b)+c)>d)

Порядок, в котором выполняются операторы, определяется их приоритетом. Например, приоритет оператора * выше, чем оператора +.

Кроме того, некоторые операторы изменяют (или могут изменять) свои операнды

++a, a++, a*=b, a=b

а некоторые – не изменяют

a+b, a-b, -a.
Правила опеределения операторных функций
Для любого бинарного оператора @ выражение aa@bb интерпретируется либо как

а) нестатическая функция-член с одним аргументом

aa.operator@(bb)

либо как

б) функция-не-член с двумя аргументами

operator@(aa,bb)

Если определены обе функции, то для выяснения того, какую из них использовать, применяется механизм разрешения перегрузки (см. перегрузка функций). Например:

class X{

public:

void operator+(int);

X(int);

};
void operator+(X,X);

void operator+(X,double);
void f(X a)

{

a+1; //a.operator+(1)

1+a; //::operator+(X(1),a)

a+1.0;//::operator+(a,1.0)

}
^ Унарные префиксные операторы

Для любого префиксного оператора @ выражение @aa интерпретируется либо как

а) нестатическая функция-член без аргументов

aa.operator@()

либо как

б) функция-не-член с одним аргументом

operator@(aa)

^ Унарные постфиксные операторы

Для любого постфиксного оператора @ выражение aa@ интерпретируется либо как

а) нестатическая функция-член с аргументом типа int

aa.operator@(int)

либо как

б) функция-не-член следующего вида:

operator@(aa,int).

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

class X{//члены

X* operator&();//префиксный унарный оператор & (чей-то адрес)

X operator&(X); //бинарный оператор & (И)

X operator++(int); //постфиксный инкремент

X operator&(X,X); //ошибка: 3 операнда

X operator/(); //ошибка: унарный оператор /

};

//функции-не-члены

X operator-(X); //префиксный унарный -

X operator-(X,X); //бинарный -

X operator--(X&,int); //постфиксный декремент

X operator-(); //ошибка: отсутствует операнд

X operator-(X,X,X); //ошибка: три операнда

X operator%(X); //ошибка: унарный оператор %

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

Конструирование. Мы должны обеспечить создание объекта-комплексного числа следующими способами:
complex c1,//1

c2(1),

c3=2,

c4(3,4),

c5=complex(5,6);
Для этого мы определяем набор конструкторов:

class complex{

public:

complex():re(0),im(0){}//1.0

complex(double real):re(real),im(0){}

complex(double real,double imag):re(real),im(imag){}

private:

double re,im;

}

В данном случае имеет место перегрузка функций (несколько функций с одинаковым именем complex). Используя аргументы по умолчанию, мы можем добиться того же результата, определив всего один конструктор:
complex(double real=0,double imag=0):re(real),im(imag){}//1
^ Операторы, модифицирующие операнды. Мы хотим, чтобы добавление значения к комплексному числу выглядело в стиле C++:

c1+=c4;//2

c2+=7;

Для этого мы определяем оператор +=. У нас есть выбор: определить его в классе или как функцию-не-член класса. Так как нам нужен доступ к представлению класса (re, im), то объявляем в классе:

complex& operator+=(const complex& a);//2

Обратите внимание на объявление аргумента const complex& a. В этом случае формальный параметр a является ссылкой на фактический параметр. При этом в функцию передается адрес фактического параметра (4 байта). Альтернативой было бы сделать объявление следующим образом:

complex& operator+=(complex a);

В этом случае фактический параметр a инициализируется значением формального параметра (в данном случае 16 байт). По соображениям эффективности выбираем первый вариант. Возникает вопрос: почему бы для сокращения передаваемой в функцию информации использовать не ссылку, а указатель? То есть:

complex& operator+=(const complex *a);

Но дело в том, что данная оператор-функция вызовется только в случае, когда правый операнд – адрес, т.е.:

c1+=&c4;

Но такой синтаксис – это не то, что мы хотели бы использовать. Поэтому в качестве формального параметра применяем все-таки ссылку.

Для добавления действительного числа объявляем функцию-член класса

complex& operator+=(double a);//2
Определяем эти функции-операторы в .cpp – файле.

complex& complex::operator+=(const complex& a)//2

{

re+=a.re;//добавление действительной части аргумента к действительной части данного объекта

im+=a.im;//добавление мнимой части

return *this;//возврат значения

}

В данной функции выполняется два действия: 1) выполняется операция добавления и 2) выполняется возврат значения. Возврат значения нужен, чтобы результат оператора += можно было использовать в качестве операнда. Например:

с1=(с2+=с3)+7;

Результатом оператора += должно быть значение его левого операнда (в примере – c2) после того, как к нему добавлен правый операнд (в примере c3). Когда вызывается функция complex::operator+=, левый операнд для нее доступен как объект, для которого она вызвана. Таким образом, эта функция должна вернуть значение самого объекта. Это можно сделать, разименовав указатель this.

Почему же мы возвращаем ссылку, а не объект? Потому, что для типа int следующее выражение должно увеличить значение переменной a, а не копии переменной a:

(a+=b)++;// после того, как к a добавили b, увеличить a на 1.

Это же должно быть справедливо и для проектируемого нами типа complex, а для этого нужно возвращать ссылку на левый операнд.
Реализация второго оператора += несколько проще, так как добавление идет только к действительной части комплексного числа:

complex complex::operator+=(double a){

re+=a;

return *this;

}

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

c1=3+c5;//3

c1=c5+3;

c1=c4+c5;

Для этого определим оператор +. Опять у нас есть выбор: определить его как член класса или как не-член-класса. В пользу последнего решения есть два довода. Во-первых, мы не можем определить оператор-член-класса так, чтобы он был вызван для случая 3+c5, т.к. левый операнд не является объектом. Во-вторых, для выполнения сложения не требуется иметь доступ к представлению класса complex. Итак, определяем функции-не-члены класса:

complex operator+(const complex& a, const complex& b);//3.1

complex operator+(const complex& a, double b);//3.2

complex operator+(double a, const complex& b);
Эти три формы оператора учитывают все интересующие нас комбинации типов операндов. Определения этих функций используют оператор += для добавления к локальной переменной:
complex operator+(const complex& a,const complex& b){

complex r=a;

return r+=b;

}
complex operator+(const complex& a,double b){

complex r=a;

return r+=b;

}
complex operator+(double a,const complex& b){

complex r=b;

return r+=a;

}

И опять, как и в случае с +=, сложение complex с double проще, чем сложение двух complex.
Сравнение. Мы должны иметь возможность сравнивать комплексные числа друг с другом и с double в виде, принятом для встроенных типов, например:

c2==c4;//4

c2==8;

3==c2;

Для этого определим оператор ==. Как и в случае с оператором +, мы не можем определить оператор == как функцию-член, т.к. она не может быть вызвана для случая 3==с2. Следовательно, это должна быть функция-не-член. Сравнение двух комплексных чисел:
bool operator==(const complex& a,const complex& b);//4.2
Модификаторы const используются для указания того, что функция не изменяет аргументы. Этой функции требуется доступ к представлению класса (re, im), однако представление класса имеет модификатор private: и к нему нельзя получить доступ из этой функции напрямую. Проблема решается введением функций доступа для извлечения действительной и мнимой частей:
public:

double real()const{return re;}

double imag()const{return im;}
Эти функции не только объявлены, но и определены внутри класса. Этим самым мы сделали их встраиваемыми в место вызова по соображениям эффективности (тот же эффект дает использование ключевого слова inline). Теперь мы можем определить оператор сравнения:

bool operator==(const complex& a,const complex& b){

return a.real()==b.real()&&a.imag()==b.imag();

}

Для выполнения сравнения complex с double и double с complex мы могли бы определить еще два оператора ==:

bool operator==(const complex& a,const double& b){

return a.real()==b && a.imag()==0;

}

bool operator==(const double& a,const complex& b){

return a==b.real() && 0==b.imag();

}

Следует заметить, что в этих функциях выполняются те же действия, что и в первом варианте оператора==, но вместо действительной части используется сам аргумент типа double, а вместо мнимой части – 0. То есть сравнение производится с комплексным числом, у которого вместо действительной части – аргумент типа double, а мнимая часть равна 0. Если бы мы могли из действительного числа получить комплексное указанным способом, мы могли бы работу двух последних операторов == возложить на первый оператор ==. Оказывается, такой механизм преобразования у нас уже имеется – это конструктор complex, в случае, когда он вызывается с одним аргументом типа double. Поэтому определять две последние функции operator== мы не будем. А для того, чтобы их работа была выполнена первым оператором ==, нам не нужно ничего определять дополнительно. Компилятор сам в нужных местах вызовет конструктор, чтобы из double получить complex. Например:
c2==c4;//operator==(c2,c4)

c2==8;//operator==(c2,complex(8))

3==c2;//operator==(complex(3),c2)
Ввод-вывод. И, наконец, мы хотим выводить комплексные числа принятым в C++ способом:
std::cout<
,

Для этого определим оператор вывода для комплексного числа:
std::ostream& operator<<(std::ostream& os, const complex& a)

{

return os<<'('<
}

Теперь запись std::cout<
operator<<(operator<<(operator<<(std::cout,c1),c2),c3);
^ Класс Matrix
Перегрузку операторов вызова функции и индексирования рассмотрим на примере класса Matrix. Этот класс предоставляет динамически размещаемые двумерные массивы.

class Matrix{

public:

Matrix(size_t,size_t);

Matrix(const Matrix&);

~Matrix();

private:

size_t d1,d2;

int* m;

};

Определение класса содержит количество элементов d1 по первой и d2 по второй размерности массива. Тип size_t – это тип значения, возвращаемого оператором sizeof. В конструкторе выделяется память для хранения всех элементов данного массива, а базовый адрес выделенной памяти является инициализирующим значением для указателя m.

Matrix::Matrix(size_t dim1,size_t dim2)

:d1(dim1),d2(dim2),m(new int[size()]){}

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

Деструктор выполняет delete[] для освобождения памяти, выделенной оператором new[] в конструкторе.

Matrix::~Matrix(){delete[] m;}
В конструкторе используется вспомогательная функция-член size(), возвращающая количество элементов массива. Так как эта функция используется только внутри класса, мы помещаем ее в раздел private:

private:

size_t size() const {return d1*d2;}

Создание матрицы размером 2x3 выглядит следующим образом:

Matrix m(2,3);
Мы хотим заполнять матрицу в цикле, например, так:

for(size_t i=0;i
for(size_t j=0;j
m(i,j)=rand();

Здесь функции-селекторы dim1() и dim2() возвращают размеры массива по соответствующим размерностям. Эти функции определены в пределах класса:

size_t dim1() const {return d1;}

size_t dim2() const {return d2;}

Обратите внимание на то, в какой форме записано обращение к элементу матрицы – m(i,j). К имени объекта справа приписаны скобки, что означает вызов функции с именем m с двумя параметрами (i,j). Оказывается, вызов функции – это тоже оператор C++. Чтобы запись m(i,j) действительно означала обращение к элементу матрицы, мы должны перегрузить этот оператор. Оператор (), а также операторы =, [] и -> можно перегружать только как нестатические функции-члены класса. Это ограничение гарантирует, что левый операнд указанных операторов будет объектом. Перегружаем оператор ():

int& Matrix::operator()(size_t dd1,size_t dd2){//1

assert(dd1
assert(dd2
return m[dd1*dim2()+dd2];

}
int Matrix::operator()(size_t dd1,size_t dd2)const{//2

assert(dd1
assert(dd2
return m[dd1*dim2()+dd2];

}

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

Тела обеих функций operator() одинаковы: выполняется проверка на допустимость индексирующих значений, после чего вычисляется позиция элемента в одномерном массиве m. Затем первая функция возвращает ссылку на данный элемент, а вторая – значение данного элемента. Именно возврат ссылки, а не значения позволяет изменять значение элемента массива, записывая m(i,j)=rand().

Теперь напишем функцию print, печатающую содержимое любой матрицы, переданной ей в качестве параметра:

void print(const Matrix& m){

for(size_t i=0;i
for(size_t j=0;j
std::cout<
}

std::cout<
}

}

Параметр функции print является ссылкой на константную матрицу. Так как функция print не может изменить матрицу, компилятор выберет версию оператора () с модификатором const. Возврат значения обходится дешевле, чем возврат ссылки, т.к. ссылку требуется разименовывать, а значение – нет, а объем передаваемой памяти одинаков (4 байта).
int* Matrix::operator[](size_t dd1)const{

assert(dd1
return &m[dd1*dim2()];

}
Вызов: m[3][4]

(m.operator[](3))[4]
Matrix& Matrix::operator=(const Matrix& t){

if(this!=&t){

delete[] m;

m=new int[(d1=t.dim1())*(d2=t.dim2())];

copy(t);

}

return *this;

}

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

Давайте посмотрим, как мы заполняли массив matrix на предыдущем занятии:

for(size_t i=0;i
for(size_t j=0;j
m(i,j)=rand();

Для этого мы организовали два вложенных цикла. При этом нам понадобилось использовать две индексные переменные, вызывать две функции, а также следить за тем, чтобы переменная, наращиваемая до dim1() (это i), была первым индексом в m(i,j), а переменная, наращиваемая до dim2() (здесь это j), была вторым индексом.

Однако перебор элементов можно организовать с помощью специального объекта – итератора. Имея в своем распоряжении итератор, мы можем сделать две вещи:

а) получить доступ к текущему элементу,

б) перейти к следующему элементу.
Изменим класс Matrix, добавив в него определение итератора и две функции:

typedef int* iterator;

iterator begin()const{return m;}

iterator end()const{return m+size();}

Функции begin() и end() возвращают значения, которые являются «границами» матрицы для итератора. Теперь заполнение массива мы можем организовать так:

for(Matrix::iterator p=m.begin();p!=m.end();++p)

*p=rand();
Здесь с помощью итератора p мы

а) получаем доступ к текущему элементу путем разименования указателя,

б) переходим к следующему элементу, производя инкремент указателя.

Следуя идиоме итерационного перебора, напишем функцию, печатающую матрицу:

void print_iter(const Matrix& m){

for(Matrix::iterator p=m.begin();p!=m.end();++p)

std::cout<<*p<<',';

}

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

Так как этот класс используется исключительно с классом Matrix, то определим этот класс внутри класса Matrix
class Matrix{

...

class reverse_iterator{

public:

reverse_iterator(int *p):_p(p){}

int& operator*(){return *_p;}

void operator++(){--_p;}

bool operator!=(reverse_iterator r)const{return _p!=r._p;}

private:

int *_p;

};

...

};
Функции-члены класса Matrix, возвращающие граничные значения для данного итератора:

reverse_iterator rbegin()const{return m+size()-1;}

reverse_iterator rend()const{return m-1;}
Теперь перебор в обратном порядке выглядит так:

for(Matrix::reverse_iterator r=m.rbegin();r!=m.rend();++r)

*r=rand();
Недостатки приведенной концепции: необходимо определять по две дополнительные функции-члены контейнера (begin,end) для каждого типа итератора.

Другая идиома для итератора, не имеющая этих недостатков:

class m_iter{

public:

m_iter(Matrix& m):_begin(m.m),_end(m.m+m.size()){}

void First(){_p=_begin;}

bool NotDone()const{return _p!=_end;}

int& CurrentItem()const{return *_p;}

void Next(){++_p;}

private:

int *_begin, *_end, *_p;

};

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

m_iter mi(m);

for(mi.First();mi.NotDone();mi.Next())

mi.CurrentItem()=rand();
Однако класс такого итератора должен иметь доступ к представлению контейнера, поэтому его нужно сделать другом контейнера:
class Matrix{

...

friend class m_iter;

};

1   2   3   4

Похожие:

Конспект лекций по курсу «Объектно-ориентированное программирование» iconОбъектно-ориентированное программирование
Курсовая работа по специальности 230201 «Информационные системы и технологии»: М. 2012 г., Мирэа, факультет Информационных технологий,...

Конспект лекций по курсу «Объектно-ориентированное программирование» iconКонспект лекций «Логистика. Конспект лекций»
Конспект лекций соответствует требованиям Государственного образовательного стандарта высшего профессионального образования

Конспект лекций по курсу «Объектно-ориентированное программирование» iconГрегори К. Использование Visual C++ Специальное издание. М.; Спб
Буч Г. Объектно-ориентированное проектирование с примерами применения. – М.: Конкорд, 1992. – 519 с

Конспект лекций по курсу «Объектно-ориентированное программирование» iconКонспект лекций доцента и. А. Волковой по курсу «системы программирования»
Система программирования – комплекс программных инструментов и библиотек, который поддерживает создание и существование программного...

Конспект лекций по курсу «Объектно-ориентированное программирование» iconКонспект лекций по дисциплине вгипу, 2009 Конспект лекций по дисциплине «Управление персоналом»
Крупица В. В., Яшкова Е. В., Егоров Е. Е. Управление персоналом: Конспект лекций по дисциплине – вгипу, 2009

Конспект лекций по курсу «Объектно-ориентированное программирование» iconКонспект лекций. (Электронный учебник) Минск: бгэу, 2010. Тема 1...
Короленок Г. А. Менеджмент в торговле. Конспект лекций. (Электронный учебник) Минск: бгэу, 2010

Конспект лекций по курсу «Объектно-ориентированное программирование» iconПоляков Д. Б., Круглов И. Ю. Программирование в среде турбо паскаль (версия 5)
Зуев Е. А. Программирование на языке turbo pascal 0, М.: Радио и связь, 1993. 384

Конспект лекций по курсу «Объектно-ориентированное программирование» iconУчебное пособие к курсу лекций «Введение в современную литературу»
Предлагаемое издание является учебным пособием к вузовскому курсу «Введение в современную литературу», который читается для студентов...

Конспект лекций по курсу «Объектно-ориентированное программирование» iconКонспект лекций, которые проф. Пугинский Б. И. читал в весеннем семестре...
Закончились а для кого-то они даже и не начинались, жалко. Закончились объединив как никогда прежде два потока. Закончились превратившись...

Конспект лекций по курсу «Объектно-ориентированное программирование» iconЛитература к курсу лекций
Алексеев П. В. Философы в России 19-20 столетий. Биографии, идеи, труды. 3-е изд. М., 1999



Образовательный материал



При копировании материала укажите ссылку © 2013
контакты
lit-yaz.ru
главная страница