Главная
страница 1страница 2 ... страница 15страница 16

Раздел 1. Программирование,  объектно-ориентированное программирование.

  1. Оценка сложности алгоритмов. Классы P, NP. NP – полные задачи.


Для определения сложности алгоритма рассмотрим входные данные размера n.

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

f(n) = O(g(n)).

Если существуют с и N такие что f(n) <= c*g(n), для любых n>N, т.е. g(n) растет быстрее, чем f(n).

g(n) – функция от размера входных данных, характеризующая рост сложности алгоритма. Сложность может расти полиноминально, экспоненциально.

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

//* Оценка сложности

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

Пусть: A - алгоритм решения некоторого класса задач и n-размерность одной задачи из этого класса (в частном случае n может быть, например, длиной последовательности в рассмотренном выше алгоритме, количеством слов в анализируемом тексте и т.д.);

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

Тогда алгоритм называется полиномиальным, если fA(n) растет не быстрее чем полином от n. В противном случае алгоритм A называется экспоненциальным.

Время, затраченное на реализацию алгоритма, как функция размерности задачи называется временной сложностью алгоритма и обозначается как O[fA(n)]. Применительно к решению задачи на ЭВМ это время является в большинстве случаев свойством самого алгоритма и слабо зависит от машины, на которой решается соответствующая алгоритму программа.

Кроме временной сложности алгоритмизируют и другие меры сложности. Так, сложность можно характеризовать числом операций N (применительно к конкретной ЭВМ) и общим объемом информации P, то есть общим количеством слов, используемых при выполнении алгоритма. Тогда, время его выполнения на конкретной ЭВМ связано с числом операций, а объем информации связан с объемом памяти, необходимой машине для реализации соответствующей программы. Следовательно, время реализации алгоритма есть функция T=f(N). При таком подходе значение T и P называют соответственно вычислительной емкостной сложностью алгоритма.



Анализ трудоёмкости алгоритмов

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

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

Одним из упрощенных видов анализа, используемых на практике, является асимптотический анализ алгоритмов. Целью асимптотического анализа является сравнение затрат времени и других ресурсов различными алгоритмами, предназначенными для решения одной и той же задачи, при больших объемах входных данных. Используемая в асимптотическом анализе оценка функции трудоёмкости, называемая сложностью алгоритма, позволяет определить, как быстро растет трудоёмкость алгоритма с увеличением объема данных. *//


Индивидуальная задача - задача с конкретными данными.

Задача распознавания – для индивидуальной задачи и заданной константы L найти ответ на вопрос существует ли решение стоимость которого не больше (не меньше) заданной границы L.



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

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


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

КЛИКА – подграф в котором каждая вершина связана с каждой.

Задача распознавания: для графа и числа L нужно определить существует ли в графе клика с величиной размера L.

Удостоверение: сама клика, док-во удостоверения: проверка, что каждая вершина в клике связана с каждой.

КОМИВОЯЖЕР

Задача распознавания: для графа и числа L нужно определить существует ли в графе обход <= L.

Удостоверение: сам обход, док-во удостоверения: определить является ли это обходом и его длина <= L.

ВЫПОЛНИМОСТЬ

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

Удостоверение: может служить набор значений истинности.

Док-во удостоверения: будет просто проверять, что формула истинная на данном наборе значений.

Дополнение задачи коммивояжера: определить что в данном графе нет обходов меньше L. Эта задача не относиться к классу NP, т.е. перебрать все существующие обходы и доказать, что они меньше L, нельзя за полиномиальное время.

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

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

Пусть А1 и А2— задачи распознавания. Будем говорить, что А1 сводится за полиномиальное время к А2 тогда и только тогда, когда существует полиномиальный алгоритм А1 для А1, использующий несколько раз в качестве подпрограммы с единичной стоимостью алгоритм A2 для А2.

Теорема Кука: задача о выполнимости является NP полной.

Еще ряд задач из NP(Клика и ЗК) являются NP- полными, для доказательства этого рассматриваемую задачу преобразуют в задачу ВЫПОЛНИМОСТЬ или какую-нибудь другую задачу, NP-полнота которой уже доказана.

  1. Метод ветвей и границ в задаче коммивояжёра.


Метод ветвей и границ - эвристический.

Задача коммивояжера - задача дискретной оптимизации. Решение этой задачи конечно, но полный перебор в большинстве случаев не возможен. МВГ один из способов сокращения перебора. Но этот алгоритм эвристический, т.е. сокращение перебора не гарантируется. Мн-во всех решений делится на части. Отсекаем мн-ва, в которых скорее всего не будет оптимального решения. Сначала мы выбираем ребра с минимальным весом. Потом мы выбираем разделяющие элементы, те которые при удалении из обхода дают нам самое максимальное увеличение стоимости. Так как мы выбрали одно ребро, размерность задачи уменьшилась на единицу. Делаем это до тех пор, пока размерность не станет на столько маленькой, что можно применить перебор.



Алгоритм Литтла:

Дана матрица расстояний. Необходимо с помощью алгоритма Литтла решить задачу коммивояжера.






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




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




3) В результате вычислений получаем матрицу, приведенную по строкам и столбцам.

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



5) Находим степени нулей полностью приведенной матрицы. Для этого как бы заменяем в ней все нули на знак «» и устанавливаем сумму минимальных элементов соответствующей строки и столбца. Степени нулей записаны в правых верхних углах клеток, для которых .

6) Определяем максимальную степень нуля. Она равна 19 и соответствует клетке (1;5). Таким образом, претендентом на включение в гамильтонов контур является дуга (1;5).

7) Разбиваем множество всех гамильтоновых контуров на два: и . Матрицу с дугой (1;5) получаем путем вычеркивания строки 1 и столбца 5 из матрицы, приведенной по столбцам и строкам. Чтобы не допускать образования не гамильтонова контура, заменяем элемент (5;1) на знак «».



8) Матрицу гамильтоновых контуров получим путем замены элемента (1;5) на знак в матрице, приведенной по столбцам и строкам.

9) Делаем дополнительное приведение матрицы контуров : = 0. Нижняя граница множества равна .



10) Находим константу приведения для множества контуров :. Следовательно, нижняя граница множества равна .

11) Сравниваем нижние границы подмножеств и . Так как , то дальнейшему ветвлению подвергаем множество . На рисунке представлено ветвление по дуге (1;5):




Переходим к ветвлению подмножества . Его приведенная матрица представлена в таблице ниже.



12) Узнаем степени нулей матрицы. Претендентами на включение в гамильтонов контур будут несколько дуг (5;2) и (6;3).

Для дальнейших расчетов выберем дугу (6;3). Разбиваем множество на два подмножества: и (табл. 3 и 4). Чтобы не допустить образования не гамильтонова контура, в таблице 3 заменяем элемент (3;6) на знак «»




Определим константы приведения для этих матриц , . Следовательно, , . Так как , то дальнейшему ветвлению подлежит подмножество . Находим степени нулей матрицы.



Претендентом к включению в гамильтонов контур станет дуга (3;2). Разбиваем множество на два и .



Очевидно, , . Следовательно, очередному ветвлению нужно подвергнуть подмножество .








Переходим к ветвлению подмножества . Его приведенная матрица представлена в таблице ниже. Определяем степени нулей. Претендентом на включение в гамильтонов контур является (5;4).



Разбиваем множество на два подмножества: и .



Находим нижние границы , .



Следовательно, очередному ветвлению нужно подвергнуть подмножество . Но его матрица имеет размерность . Поэтому в гамильтонов контур следует включить дуги, соответствующие в матрице нулевым элементам. Это дуги (2;1) и (4;6). На рисунке ниже представлено дерево ветвлений. Определим полученный гамильтонов контур. В него вошли дуги . Длина контура равна . Так как границы оборванных ветвей больше длины контура 1 – 5 – 4 – 6 – 3 – 2 – 1, то этот контура имеет наименьшую длину.


  1. Алгоритмы поиска подстроки в строке.


Одна из задач поиска информации— поиск точно заданной подстроки в строке. Эта задача чрезвычайно важна— она применяется в текстовых редакторах, СУБД, поисковых машинах…

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

 Наивный метод.

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

T:  abcbca

P:  bca


       bca

Если обозначить через n длину Р и через m длину T, в самом худшем случае чис-



число сравнений, выполняемых этим методом, равно Θ(nm).

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

в Т.

Препроцессинг.

Эти пропуски получаются благодаря изучению внутренней структуры либо образца P, либо текста Т. Эта часть алгоритма называется препроцессной фазой. За ней следует фаза поиска.

Для данной строки S и позиции i > 1 определим Zi(S) как длину наибольшей подстроки S, которая начинается в i и совпадает с префиксом S.

S = ababcaba

Z1 = 1       Z5 = 0

Z2 = 0       Z6 = 3

Z3 = 2       …

Z4 = 0


Вычисление значений Z для строки S за линейное время и есть основной препроцессинг.

Пусть S = P$T — строка, состоящая из образца Р и текста T, между которыми стоит знак $, отсутствующий в обеих строках.

|P| = n, |T| = m и n  <= m. Поэтому |S| = n + m + 1 = О(n+m).

Вычислим Zi(S) для i от 2 до n+m+1. Так как $ не встречается в Р и T, то Zi <= n для любого i > 1. Любое значение i > n + 1, для которого Zi(S) = n, определяет вхождение Р в T, начинающееся с позиции i - (n + 1). Так как все Zi(S) можно вычислить за время О(n + m) = О(m), такой алгоритм находит все вхождения Р в T за время О(m).

S = bca$abcbca

Z5 = 0


Z6 = 2

Z7 = 0


Z5 = 3 = |P| - вхождение

Алгоритм Бойера-Мура.

Считается наиболее быстрым среди алгоритмов общего назначения, предназначенных для поиска подстроки в строке. Общая оценка вычислительной сложности алгоритма — O(n+m+|Σ|) на непериодических шаблонах и O(nm+|Σ|) на периодических, где Σ — алфавит, на котором проводится сравнение.

Алгоритм Бойера-Мура использует три здравые идеи, которых нет в наивном алгоритме: просмотр справа налево, правило сдвига по плохому символу и правило сдвига по хорошему суффиксу. Совместный эффект этих трех идей позволяет методу обычно проверять меньше чем n + m символов и (после некоторого улучшения) работать за линейное время в худшем случае.

1. При любом прикладывании Р к T алгоритм Бойера-Мура проверяет совпадение сканированием символов с правого конца P налево, а не слева направо, как в наивном алгоритме.

2. Для каждого символа алфавита х пусть R(x) — позиция крайнего правого вхождения х в Р. Если х в Р не входит, R(x) считается нулем.  R(y) = 2 для P.

Легко просмотреть Р за время О(n) и подготовить значения R(x).

Предположим, что при некотором сопоставлении Р с Т крайние правые

n - i символов Р совпадают со своими парами в T, но следующий слева символ P(i) не совпадает со своей парой, скажем, в позиции k текста Т. Правило плохого символа гласит, что Р следует сдвинуть вправо на max{l, i - R(T(k))} мест. Таким образом, если крайнее правое вхождение в Р символа Т(к) занимает позицию j < i (включая возможность j = 0), то Р сдвигается так, чтобы символ j в Р поравнялся с символом k в T. В противном случае Р сдвигается на одну позицию.

Цель этого правила — сдвиг Р, когда это возможно, больше чем на одно место.

T:    abxacaaa

P:    xayaca

           xayaca

Правило плохого символа полезно при несовпадениях, близких к правому концу Р,

но эффект от него мал, если несовпадающий символ из Т встречается в Р справа от точки несовпадения. (Если бы P = xayacx, т.к. x встречается правее y).Это бывает, когда алфавит мал и текст содержит много похожих, но не совпадающих подстрок.

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

Когда несовпадение случилось в позиции i образца Р и х — несовпадающий

символ в T, нужно сдвинуть Р вправо, совместив с этим х ближайшее вхождение х в Р слева от позиции i.

Так как расширенное правило дает большие сдвиги, единственный резон в пользу

более простого — это дополнительные издержки на реализацию расширенного правила.

Во время препроцессинга просмотрим Р справа налево, накапливая для каждого символа алфавита х список позиций, где х входит в Р. Так как просмотр идет

справа налево, каждый список будет расположен по убыванию. Например, если

Р = abacbabc, то для символа а получится список 6, 3, 1. Эти списки накапливаются за время О(n). На стадии поиска, когда нашлось несовпадение в позиции i и несовпадающий

символ из T — это х, будем просматривать список этого символа, пока не достигнем первого числа, меньшего чем i, или не установим, что такого числа нет.

Если его нет, то х до позиции i не появляется, и весь образец Р сдвигается за

позицию х в Т. В противном случае найденный элемент списка дает требуемую

позицию х.



3. Правило хорошего суффикса.

Пусть строка Р приложена к T и подстрока t из Т совпадает с суффиксом Р,

но следующий левый символ уже не совпадает. Найдем, если она существует, крайнюю правую копию t’ строки t в Р, такую что символ слева от t’ в Р отличается от символа слева от t в Р.

Сдвинем Р вправо, приложив подстроку t’ в Р к подстроке t в Т. Если t' не существует, то сдвинем левый конец Р за левый конец t в T на наименьший сдвиг, при котором префикс сдвинутого образца совпал бы с суффиксом t в Т. Если такого сдвига не существует, то сдвинем Р на n позиций вправо.

Если найдено вхождение Р, то сдвинем Р на наименьший сдвиг, при котором

собственный префикс сдвинутого Р совпадает с суффиксом вхождения Р в Т. Если такой сдвиг невозможен, нужно сдвинуть Р на n мест, т. е. сдвинуть Р за t в Т.

    0                         1

    1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8

Т: p r  s t  a b s  t  u b a b v q x r s  t

 

Р:        q c a b d  a b d a b



           1 2 3 4 5  6 7 8 9 0

Когда нашлось несовпадение в позиции 8 строки Р и позиции 10 строки T, мы

получили t = ab и строку t' в Р, начинающуюся с позиции 3. Следовательно, Р сдви-

сдвигается вправо на шесть позиций, создавая такое соответствие:

    0                         1

    1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8

Т: p r  s t  a b s  t  u b a b v q x r s  t

 

Р:                          q c a b d  a b d a b



                             1 2 3 4 5  6 7 8 9 0

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

Алгоритм Бойера-Мура сдвигает образец на наибольшее из расстояний, предлагаемых  правилами плохого символа и хорошего суффикса. Когда образец в тексте не встречается, исходный метод Бойера-Мура работает в худшем случае за время O(nm). Однако некоторые простые модификации метода справляются с этой трудностью, обеспечивая границу времени О(m) для всех случаев.
Алгоритм Кнута-Морриса –Пратта

Если Р = abcxabcde и при текущем расположении Р и Т несовпадение нашлось в позиции 8 строки Р, то есть возможность с двинуть Р на четыре места без пропуска вхождений Р в Т. Отметим, что это можно увидеть, ничего не зная о тексте T и расположении Р относительно Т. Алгоритм Кнута-Морриса-Пратта, основываясь на таком способе рассуждений, и делает сдвиг больше, чем наивный алгоритм.

Для каждой позиции i образца Р определим spi(P) как длину наибольшего собственного (не совпадающего со всей строкой) суффикса Р[1..i], который совпадает с

префиксом Р.

Например, если Р = abcaeabcabd, то sp2 = sp3 = 0, sp4 = 1, sp8 = 3 и sp10 = 2.

Для каждой позиции i образца Р определим sp’i как длину наибольшего собственного суффикса Р[1..i], который совпадает с префиксом Р с дополнительным условием, что символы P(i + 1) и P(sp’i + 1) не равны.

Для примера, если Р = bbccaebbcabd, то sp8 = 2, поскольку строка bb встречается и как собственный префикс Р[1..8], и как суффикс Р[1..8]. Однако обе копии этой строки

продолжаются одним и тем же символом с, так что sp’8 = 1.

Алгоритм Кнута-Морриса-Пратта прикладывает Р к T и затем сравнивает соответствующие пары символов слева направо, как в наивном алгоритме. Если первое несовпадение отмечается в позиции i + 1 образца Р и позиции k текста T, то сдвиг Р вправо таков, что P[1.. sp’i ] пристроится к Т[k — sp’i..k - 1]. Символ sp’i +1

из Р поравняется с символом k из T. В случае если вхождение Р найдено (несовпадений нет), Р сдвигается на n - sp'n мест.

В примере, где Р = abcxabcde и sp'7 = 3, если 8-й символ из Р не дает совпадения, то Р будет сдвинуто на 7-3 = 4 мест.

Обсуждаемое правило сдвига имеет два преимущества. Во-первых, Р часто сдвигается больше чем на один символ. Во-вторых, после сдвига гарантировано, что

левые sp’i символов Р совпадают со своими парами в T. Таким образом, для проверки полного совпадения Р с приложенным текстом Т надо начинать сравнивать Р и Т с позиции sp’i + 1 в Р.

В методе Кнута-Морриса-Пратта число сравнений символов не превосходит 2m.

Алгоритм Кнута – Мориса – Пратта один из первых алгоритмов с линейной оценкой в худшем случае, отсутствует регрессия на «плохих» данных, может превосходить алгоритм БМ на небольших длинах образца.

  1. Объекты. Поля и правила (методы). Инкапсуляция.


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

Поле класса (атрибут) — переменная, связанная с классом или объектом. Все данные объекта хранятся в его полях. Доступ к полям осуществляется по их имени. Обычно тип данных каждого поля задаётся в описании класса, членом которого является поле.

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

При этом пользователю предоставляется только спецификация (интерфейс) объекта. Пользователь может взаимодействовать с объектом только через этот интерфейс.

class A {

private:


int a, b; //скрытые свойства

void Do_Something (void); //скрытый метод.

public:

int Return_Something (void); //открытый интерфейс



};

Класс А инкапсулирует свойства a, b и метод Do_Something, представляя внешний интерфейс Return_Something.

Объект можно рассматривать как усовершенствованную переменную: он содержит данные, но ему также можно направлять запросы на выполнение операций «с самим собой». В терминологии ООП каждый объект является экземпляром класса, где термин «класс» является синонимом термина «тип».

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

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

Каждый объект обрабатывает только конкретные разновидности запросов. Совокупность этих запросов образует интерфейс, который определяется типом объекта.

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

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

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

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

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

В Java отличие синтаксиса в том, что модификатор доступа повторяется для каждого элемента класса. А конкретнее, по умолчанию в Java используется friendly, это значит, что элемент видим для других классов этого же пакета (или файла исходного кода). Подобным образом, protected означает видимость для подклассов, тогда как комбинация private protected соответствует protected в C++.

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

Дружественными могут объявляться глобальные функции, функции-члены других классов и даже целые классы.

Функция f класса Y объявляется другом класса X:

class X


   { friend void Y:: f ( int, char*);

       /* Другие компоненты класса X */

   }

Можно объявить все функции класса Y дружественными в классе X:



   class Y;

   class X

    { friend Y;

        /* Другие компоненты класса X */

    }

   Дружественной может быть и функция, не являющаяся компонентой какого-либо класса, например:



   class XX

     { friend  int printXX ( );

          /*  Другие компоненты класса ХХ */      }

Здесь функция printXX имеет доступ ко всем компонентам класса XX, независимо от закрепленного за ними уровня доступа.

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

Если статическая переменная объявлена, но не определена, компоновщик выдаст сообщение об ошибке.

Определение должно находиться за пределами класса и при этом в единственном экземпляре.

class A {

static int i;

public:


… }

int A::i = 1;

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

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

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

5. Наследование. Полиморфизм.


//* Наследование. Наследование есть свойство объектов порождать своих потомков. Объект-потомок автоматически наследует от родителя все поля и методы, может дополнять объекты новыми полями и заменять (перекрывать) методы родителя или дополнять их. Принцип наследования решает проблему модификации свойств объекта и придает ООП в целом исключительную гибкость. При работе с объектами программист обычно подбирает объект, наиболее близкий по своим свойствам для решения конкретной задачи, и создает одного или нескольких потомков от него, которые «умеют» делать то, что не реализовано в родителе.

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

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

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

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

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

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

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

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

Java для этого использует ключевое слово super. В этом языке нет возможности сослаться на другой предшествующий класс (еще выше по иерархии).

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

class Y : public X {

int 1:

public:  …



}

Перед именем базового класса находится ключевое слово public. При наследовании по умолчанию используется уровень доступа private, тогда все открытые члены базового класса стали бы закрытыми в производном классе.

При создании объекта компилятор гарантирует вызов конструктора для всех его подобъектов. Но что, если подобъекты не имеют конструкторов по умолчанию или вы захотите изменить аргумент конструктора? Конструктор нового класса не имеет доступа к закрытым данным подобъекта, а значит, не может инициализировать их напрямую. Однако проблема решается просто: следует вызвать конструктор подобъект. В С++ для этого предусмотрен специальный синтаксис, называемый списком инициализирующих значений конструктора. В списке инициализирующих значений вызовы конструкторов подобъектов размещаются после списка аргументов конструктора и двоеточия, но перед открывающей фигурной скобкой тела функции. Для класса МуТуре, наследующего от Ваr, это может выглядеть так (если у Ваr имеется конструктор, вызываемый с одним аргументом типа int):

MyType :: MyType(int i) : Bar(i){ ...}

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

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

Если вы хотите, чтобы некоторые из членов базового класса оставались видимыми в производном классе, перечислите их имена с ключевым словом using в секции public производного класса:

class Pet {

public:

char eat();

int speak();

float sleep(); }

class Goldfish : Pet {

public:


using Pet::eat;

using Pet::sleep; ...

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

Java использует слово extends для выражения единственного типа наследования, соответствующего публичному наследованию в C++.

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

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

C++ поддерживает множественное наследование, базовые классы перечисляются через запятую.

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

public interface CanFly {

 public void Fly(); }

public class Bat extends Animal implements CanFly {

 public void Fly() {// the bat flies...} }



Полиморфизм.

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

void doSttuff (Shape& s){

         s.erase();   …

         s.draw(); }

Функция работает с любыми объектами типа Shape и не зависит от конкретного типа выводимых и стираемых объектов.

Circle c;

Triangle t;

Line l;

doStuff (c);

doStuff (t);

doStuff (l);

Вызовы doStuff автоматически работают правильно независимо от фактического типа объекта.

Здесь происходит следующее: функции, которая ожидает получить объект типа Shape, передается объект типа Circle. Поскольку класс Circle является частным случаем Shape, он может интерпретироваться как таковой функцией doStuff.

При отправке сообщения draw () неизвестной разновидности Shape правильное поведение выбирается автоматически в соответствии с фактическим типом объекта. В обычной ситуации можно было бы предположить, что будут вызваны версии функций erase () и draw () класса Shape, а не конкретных подтипов Circle, Triangle и Line. Тем не менее благодаря полиморфизму все работает правильно. Если функция класса объявлена виртуальной, то при получении сообщения объект автоматически выполнит правильные действия, даже если при этом потребуется повышающее приведение типа.

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

class Shape{ ...

public:


         virtual void draw();

         virtual void erase(); … };

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

В процессе проектирования бывает нужно, чтобы базовый класс только определял интерфейс для своих производных классов. С этой целью он объявляется абстрактным, то есть в него включается хотя бы одна чисто виртуальная функция. Объявление чисто виртуальной функции должно начинаться с ключевого слова virtual и заканчиваться символами =0. Если кто-нибудь попытается создать объект абстрактного класса, компилятор пресекает эти попытки. Абстрактные классы вынуждают программистов использовать определенную архитектуру в своих программах.

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

class Shape{ …

public:

         virtual void draw()=0;

         virtual void erase()=0; ... };

class Circle : public Shape{ …

public:

         void draw(){   …   } ...}



В Java и абстрактные методы, и абстрактные классы отмечаются ключевым словом abstract. Производные классы, которые не переопределяют все абстрактные методы, должны быть отмечены как абстрактные. Как и в C++, нельзя создавать объекты абстрактных классов.

  1. Конструкторы. Таблица виртуальных методов. Раннее и позднее связывание.


Полиморфизм основывается на возможности включения в данные объекта информации о методах их обработки (в виде указателей на функции). Будучи доступным в некоторой точке программы, даже при отсутствии полной информации о его типе, он всегда может корректно вызвать свойственные ему методы.
Полиморфной называется функция, независимо определенная в каждом из группы производных классов и имеющая в них общее имя. Полиморфная функция обладает тем свойством, что при отсутствии полной информации о том, объект какого из производных классов в данный момент обрабатывается, она тем не менее корректно вызывается в том виде, в каком она была определена для данного конкретного класса. Практический смысл полиморфизма заключается в том, что он позволяет посылать общее сообщение о сборе данных любому классу, причем и родительский класс, и классы-потомки ответят на сообщения соответствующим образом, поскольку производные классы содержат дополнительную информацию. Программист может сделать регулярным процесс обработки несовместимых объектов различных типов при наличии у них такого полиморфного метода.
Полиморфный метод в Си++ называется виртуальной функцией, позволяющей получать ответы на сообщения, адресованные объектам, точный вид которых неизвестен. Такая возможность является результатом позднего связывания.
При позднем связывании адреса определяются динамически во время выполнения программы, а не статически  во время компиляции, как в традиционных компилируемых языках, в которых применяется раннее связывание. Сам процесс связывания заключается в замене виртуальных функций на адреса памяти.
Виртуальные функции определяются в родительском классе, а в производных классах происходит их доопределение и для них создаются новые реализации. При работе с виртуальными функциями сообщения передаются как указатели, которые указывают на объект вместо прямой передачи объекту. Виртуальные функции используют таблицу для адресной информации, которая инициализируется на этапе выполнения при помощи конструктора.
Для виртуального метода осуществляется и возможно только позднее связывание, т.е. привязка осуществляется на этапе выполнения. (Раннее связывание - привязка действия к объекту осуществляется на этапе компиляции.) При применении раннего связывания, мы как бы говорим компилятору: "Я точно знаю, чего я хочу. Поэтому жестко(статически) связывай все вызовы функций". При применении механизма позднего связывания мы как бы говорим компилятору: "Я пока не знаю чего я хочу. Когда придет время, я сообщу что и как я хочу". Таким образом, во время раннего связывания вызывающий и вызываемый методы связываются при первом удобном случае, обычно при компиляции. При позднем связывании вызываемого метода и вызывающего метода они не могут быть связаны во время компиляции. Поэтому реализован специальный механизм, который определяет как будет происходить связывание вызываемого и вызывающего методов, когда вызов будет сделан фактически. Очевидно, что скорость и эффективность при раннем связывании выше, чем при использовании позднего связывания. В то же время, позднее связывание обеспечивает некоторую универсальность связывания. Метод становится виртуальным, если за его объявлением в типе объекта стоит зарезервированное слово virtual. Если метод объявлен в родительском типе как virtual, то все методы с аналогичными именами в дочерних типах должны быть объявлены виртуальными. Каждый тип объекта, содержащий виртуальные методы, имеет таблицу виртуальных методов (ТВМ), хранящуюся в сегменте данных. ТВМ содержит размер типа объекта и для каждого виртуального метода указатель кода, исполняющий данный метод. Имеется только одна ТВМ для каждого типа объекта. Каждый вызов виртуального метода должен проходить через ТВМ, тогда как статические методы вызываются непосредственно. Причем, если объект наследует виртуальные методы, то он также наследует ТВМ. Инициализация ТВМ осуществляется конструктором. ТВМ создается компилятором автоматически и программа никогда не манипулирует ими непосредственно. Указатели на ТВМ также запоминаются автоматически.
Первое слово ТВМ содержит размер экземпляров соответствующего объектного типа. Эта информация нужна для конструктора или деструктора, для определения количества байт.                                                                                                        

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

  • Конструктор не возвращает значение.

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

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

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

  • Конструкторы не наследуются.

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

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

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

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

Конструктор распознается по тому, что имеет то же имя, что и сам класс.


Конструкторы могут иметь параметры, что позволяет определить начальное состояние объекта при его порождении. Конструкторы имеют то же имя, что и имя класса, в котором они определены, так что если класс имеет несколько конструкторов, то они должны различаться числом и типом своих параметров. Например:
class date {
date(int, int, int);};            


  1. Указатели. Динамические объекты. Деструкторы.


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

Деструкторы не наследуются, поэтому даже при отсутствии в производном классе деструктора он не передается из базового, а формируется компилятором как умалчиваемый со статусом доступа public. Этот деструктор вызывает деструкторы базовых классов. Деструкторы базовых классов выполняются в порядке, обратном перечислению классов в определении производного класса. Таким образом порядок уничтожения объекта противоположен по отношению к порядку его конструирования.


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

  • · Деструктор имеет то же имя, что и класс, в котором он объявляется, с префиксом ~;

  • · Деструктор не возвращает значения даже типа void;

  • · Деструктор не наследуется в производных классах;

  • · Производный класс может вызвать деструкторы для его базовых классов;

  • · Деструктор не имеет параметров (аргументов);

  • · Класс может иметь только один деструктор;

  • · Деструктор - это функция, и он может быть виртуальным, его можно объявить виртуальным;

  • · Невозможно получить в программе адрес деструктора (указатель на деструктор);

  • · Если деструктор не задан в программе, то он будет автоматически сгенерирован компилятором для уничтожения соответствующих объектов. Все деструкторы, сгенерированные компилятором, имеют атрибут public;

  • · Деструктор вызывается автоматически при разрушении объекта;

  • · Когда вызывается библиотечная функция exit, вызываются деструкторы только для глобальных объектов;

  • · Когда вызывается библиотечная функция abort, никакие деструкторы не вызываются.


Указателем называется компонент заданного типа, являющийся ссылкой на некоторую область памяти. Определение указателя имеет следующий вид: тип_данных *id1, *id2, *idn. Тип переменных id1, id2,idn определяется как тип указателей на тип-данных. Эти переменные служат ссылками на объекты типа тип-данных. Этот тип называется базовым типом переменных-указателей. Ниже приведены несколько примеров определений указателей: int *pi, *qi.

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

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

char *s = new char [nelem];                                                                          

unsigned nelem; /* число элементов для которых нужно выделить память */

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

delete ptr; для char *ptr;

delete[] ptr2; для char **ptr2;

Необходимо предпринимать меры предосторожности для избежания ошибок, связанных со ссылками на объект, память для которого уже освобождена - проблема висящей ссылки. Если реализация языка обеспечивает сборку мусора, то память, занимаемая объектами, к которым невозможен доступ, может быть автоматически подготовлена для повторного использования. Однако в языке Си, в отличие от языков Лисп и Снобол, такая возможность отсутствует.

Указатели могут обеспечивать ссылку на заранее определенные объекты. Адрес такого объекта может быть определен использованием оператора адресации. Например, рассмотрим переменные i и pi, определенные как int i, *pi; присваивание pi = &i; pi позволяет ссылаться на объект с именем i также с помощью указателя pi, используя обозначение *pi. Имена i и *pi - псевдоимена. Оператор & является также стандартным средством моделирования передачи параметров по ссылке. Однако его употребление может привести к проблеме висящей ссылки.

С помощью явных преобразований можно получить указатель на произвольную ячейку памяти. Например, предположим, что pt является указателем типа T*. Тогда указатель на ячейку памяти 0777000 можно получить с помощью следующей записи: pt = (T*)0777000.

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




  1. Конструкторы по умолчанию и их применение.


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

//конструкторы по умолчанию без параметров

Account::Account() { ... } 

// конструктор по умолчанию с параметрами, задаваемыми по умолчанию

iStack::iStack( int size = 0 ) { ... }

Complex::Complex(double re=0.0, double im=0.0) { ... }

Когда мы пишем:

int main()

{

   Account acct;



   // ... }

то компилятор сначала проверяет, определен ли для класса Account конструктор по умолчанию. Возникает одна из следующих ситуаций:

1. Такой конструктор определен. Тогда он применяется к acct.

2. Конструктор определен, но не является открытым (private). В данном случае определение acct помечается компилятором как ошибка: у функции main() нет прав доступа.

3. Конструктор по умолчанию не определен, но есть один или несколько конструкторов, требующих задания аргументов. Определение acct помечается как ошибка: слишком мало аргументов у конструктора.

4. Нет ни конструктора по умолчанию, ни какого-либо другого. Определение считается корректным, конструктор по умолчанию генерируется компилятором.

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

Рассмотрим, как работает сгенерированный конструктор по умолчанию (по идее он ничего не делает).

Допустим, что все члены класса Account объявлены открытыми и не объявлено никакого конструктора:

class Account {

public:

   char           *_name;

   unsigned int   _acct_nmbr;

   double         _balance;

};

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



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

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

Конструктор копирования имеет следующую общую форму:

имя_класса(const имя_класса &obj) {

// тело конструктора }

Здесь obj — ссылка на объект, для которого должна создаваться копия. Например, пусть имеется класс Coo, а y — объект типа Coo.

Конструктор копирования вызывается в трех ситуациях:

1. при инициализации нового объекта др. объектом в операторе объявления: Coo x=y;

2. при передаче объекта в функцию в качестве аргумента по значению: Func1(y);

3. при возврате объекта из функции по значению: y = Func2().

В двух первых случаях конструктору копирования передается ссылка на y. В последнем случае конструктору копирования передается ссылка на объект, возвращаемый функцией Func2().

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

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

Конструкторы по умолчанию имеют несколько специальных областей применения.

Во-первых, такой конструктор используется, если компилятор встречает определение массива объектов, например: MyClass Arr[25]. Здесь объявлен массив из 25 объектов типа MyClass, и каждый объект этого массива создан путем вызова конструктора по умолчанию! Поэтому если вы забудете снабдить класс конструктором по умолчанию (если даже определены любые другие конструкторы), то вы не сможете объявлять массивы объектов этого класса.

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

class TCurrency

{  public:

            int uint;

            TCurrency(const long double &t=0.0); // конструктор

            // методы

            …

private:
                        long double Summa; };

class Roubles: public TCurrency // ни один конструктор не определен

{  public:
                        // методы

            … };

В первую очередь разберемся с объявлениями новых объектов. Хотя в классе-наследнике отсутствуют конструкторы, мы тем не менее имеем возможность объявлять объекты без инициализации, а также объекты, инициализируемые другими объектами (копии): Roubles d; Roubles f = d.

Это означает, что и при наследовании для производного класса система по умолчанию создает конструктор без аргументов и конструктор копирования. Однако конструкторы не совсем “пустые”: поскольку класс Roubles является наследником от TCurrency, в этих конструкторах вызывается конструктор базового класса.

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

- если в базовом классе нет конструкторов или есть конструктор без параметров (или параметры присваиваются по умолчанию, как в нашем случае), то в производном классе конструктор можно не писать — будут созданы конструктор копирования и конструктор по умолчанию без параметров;

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

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




следующая страница >>
Смотрите также:
Программирование, объектно-ориентированное программирование. Оценка сложности алгоритмов. Классы P, np. Np – полные задачи
3210.51kb.
16 стр.
Методические указания к выполнению курсовой работы по дисциплине «Объектно-ориентированное программирования» для студентов направления подготовки 050101 «Компьютерные науки»
75.4kb.
1 стр.
В объектно -ориентированное программирование
43.45kb.
1 стр.
Объектно-ориентированное программирование
548.52kb.
4 стр.
Рабочая программа по курсу «Объектно-ориентированное программирование» для специализации «Компьютерные технологии в образовании и научной деятельности»
55.49kb.
1 стр.
Программа дисциплины "Объектно-ориентированное программирование" для подготовки инженеров
93.32kb.
1 стр.
Книга 2 Г. С иванова, Т. Н. Ничушкина, Е. К. Пугачев Объектно-ориентированное программирование
3396.37kb.
37 стр.
Рабочая программа дисциплины объектно-ориентированное программирование
292.06kb.
1 стр.
Объектно-ориентированное программирование: Разработка пользовательского интерфейса
94.69kb.
1 стр.
Программирование потоком данных Сжатие данных
223.56kb.
1 стр.
Контрольная работа является завершающей стадией процесса подготовки студента по дисциплине «Объектно-ориентированное программирование»
28.4kb.
1 стр.
Рабочий план практических занятий по курсу «Объектно-ориентированное программирование»
40.5kb.
1 стр.