лекции (2003) (Глазкова) (1160821), страница 9
Текст из файла (страница 9)
Например:
int j; int& i=j;
j=0; i=1;
Cout << j; // выдаётся 1
Здесь один объект данных имеет несколько имен. Во всех ЯП это считается дурным стилем программирования. Поэтому ссылки применяются, в основном, для передачи объектов. Инициализация ссылок происходит в момент передачи параметров.
Пример:
int f(int & i);
int x;
f(x) ;// инициализация ссылки i происходит в момент вызова f(x)
Рассмотрим понятие константной ссылки.
void f(T& x); /* объект передается по ссылке из соображений эффективности, и допустимо менять этот объект */
void f(const T& x);/*передачу параметров по ссылке организуем из соображений эффективности и не имеем права менять объект х */
Рассмотрим пример передачи параметров: (const int& i)
Это формально допустимо, но бессмысленно. Эта запись означает, что мы не хотим, чтобы объект i модифицировался. Зачем же мы передаем ссылку? Ведь теперь всякое присваивание j=i; - это не просто копирование переменных целого типа. Это влечёт обращение в оперативную память за адресом i, по этому адресу еще раз обращаемся в ОП и только тогда содержимое помещаем в j. Т.е. это требует много накладных расходов. Как видим, именно для объектов простых ТД передача параметров по ссылке никаких преимуществ не дает. Наука семиотика утверждает, что у каждого понятия есть синтаксис (его структура), семантика (смысл) и прагматика. У рассматриваемого объявления есть семантика, но нет прагматики.
Пункт 7. Процедурные типы данных.
Процедурные ТД очень тесно связаны с понятием указателя. Более того, в некоторых ЯП процедурный и указательный типы отождествляются. Например, в С и С++ процедурный ТД – это указатели на функцию.
Пример:
void f( );
typedef void (*f)( );//это означает, что ТД f – указатель на функцию
void p();
f x;
x=p;// допустимо
Т.е. здесь процедурный ТД обозначает функциональный ТД, значением которого являются адреса функций.
Процедурные типы есть во всех ЯП.
Например, в Modula2 и Oberon появляется понятие процедурного ТД, которого нет в стандартном Pascal.
Type T = procedure (var integer);
На языке Oberon эта запись означает, что значением типа Т являются процедур, у которых один параметр типа integer, который передается по ссылке.
procedure P(var x:integer);
var PP:T;
PP:=P; {допустимо}
Зачем нужны процедурные ТД?
В языке стандартный Pascal были следующие виды формальных параметров:
-
параметры значения
-
параметры ссылки
-
процедуры
-
функции
Например:
function Intregral (function f(x:real):real(*f))
В Turbo Pascal параметров процедур и функций не было, потому что там был процедурный ТД. Главное назначение такого процедурного ТД: передача функций и процедур как формальных параметров других функций и процедур.
Отметим, что для объектно-ориентированных языков необходимы понятия:
-
объект,
-
наследование,
-
динамический полиморфизм (виртуальные методы).
Понятие динамического полиморфизма можно смоделировать с помощью процедурных ТД. В языке, где есть понятие записи и функционального типа данных, можно программировать объектно-ориентированно.
Возникает вопрос – можно ли на С писать объектно-ориентированные программы? Можно. В языке С есть понятие записи (-> можно промоделировать понятие наследования) и есть процедурный ТД ( -> можно промоделировать динамический полиморфизм). Следовательно, на языке С можно программировать объектно-ориентированно.
Пример.
Xlib – это библиотека на языке С, которая определяет графические примитивы, которые общаются по Х-потоку. Xlib – не объектно-ориентированная библиотека, но на базе Xlib построена библиотека на языке С XTodhit, которая действительно является объектно-ориентированной. А на базе нее уже реализованы такие системы, как Athener и Motif, которые написаны тоже на С и являются объектно-ориентированными.
Обратим внимание на следующую особенность.
Процедурный тип в языках Oberon, Modula2, Pascal, C, C++ - это тип, значениями которого являются адреса процедур и функций в программе.
Рассмотрим языки C# и Java.
В этих языках отсутствует понятие как глобальной функции, так и глобальной переменной.
Переменные могут выступать как квазистатические объекты (т.е. объекты, описанные внутри блока), а функции в этих языках могут выступать только как функции-члены некоторых классов.
Иначе говоря, простое понятие процедурного типа в этих языках теряет свой смысл.
Заметим, что понятие процедурного типа тесно связано с понятием указателя, а мы уже отмечали, что понятие указателя ненадежно.
В языке Java понятие указателя отсутствует вообще. В языке C# есть понятие указателя, но оно используется в неконтролируемом коде.
Создатели языка Java вообще отказались от процедурного типа. Для чего нужен процедурный тип? Для передачи параметров (можно функции и процедуры передавать как параметры других процедур и функций ).
В языке C# появилось понятие типа Delegate:
Class C {
Delegate void SimpleDelegate();
…
}
Это означает, что SimpleDelegate является значением типа Delegate. Это аналог процедурного типа данных. Это означает, что значением типа Delegate могут являться статические функции, имеющие следующие прототип: void F();
Можно описать тип Delegate int NotSoSimple (int);
Значениями его будут статические функции с прототипом int F();
Порождение объектов типа Delegate.
Пусть внутри класса С есть функция
static void F( ) {…};
Тогда можно описать:
SimpleDelegate S=new SimpleDelegate (F);
И после этого вызывать S().
Мы видим, что тип Delegate – аналог процедурного типа.
В чем существенное отличие типа Delegate?
Delegate – это референциальный ТД. Он типозащищенный (проводится полный контроль типов).
Никаких преобразований между объектами типа Delegate нет (это повышает надежность). К объектам типа Delegate применима операции += и -=. Т.е. Delegate – это не просто указатель на функцию, может быть и список функций. И, когда мы вызываем Delegate, каждая функция из соответствующего списка вызывается. Операция += прибавление новой функции к списку Delegate.
В качестве значений типа Delegate выступают статические функции соответствующих классов. Поэтому вопрос об объекте здесь никогда не встает.
В описании языка C# написано, что объекты типа Delegate – анонимные функции, т.е., не видя инициализации функции, мы не знаем к какому классу она относится.
Итак, мы обсудили, что делать, если в языке вообще отсутствует понятие глобальных функций. Либо вообще исключить понятие процедурного типа, либо ввести что-то типа Delegate, т.е. аналогом глобальной функции является статическая функция. В C# фактически ввели понятие процедурного типа, расширив понятие Delegate. Значениями типа Delegate являются статические функции с заданным прототипом.
Далее, в языке С++ и Delphi пошли по другому пути.
Во-первых, в языке Delphi, как в Pascal и Oberon, есть процедурные ТД. В Delphi можно написать
Type T=function (X: integer): boolean;
В С++ - аналогично.
Кроме этого, язык С++ расширен и понятием указателя на член.
Например, объявление int (C::*) pint; означает, что значениями pint будут являться адреса целочисленных членов класса С.
Пусть
class C {
int i;
int j };
Тогда допустимо pint = &C::i;
Для работы с таким указателем вводятся две операции : . * и -> *
Пусть C x;
Можно записать x.*pint
Это то же самое, что x.i, т.е. разыменовывание соответствующего указателя на член.
Если же было присваивание pint = &C::j; то x.*pint будет означать x.j.
Пусть C* pc;
Тогда pc->*pint после присваивания pint = &C::i будет pc->i.
Самое большое применение указателей на члены для членов-функций. Представим, например, интерпретатор обратной польской записи. Вполне резонно функции интерпретации сделать функциями некоторого класса Poliz.
Пусть коды операций от 0 до N-1.
В массиве, состоящем из указателей на функции-члены этого класса, каждый указатель – это обработчик соответствующей операции.
И поэтому вместо огромного переключателя по кодам операций, мы просто вызываем соответствующую функцию из массива.
Синтаксис: void (C::*) (int) pmen;
Это означает, что значением переменной pmen являются функции-члены класса С, которые возвращают void и имеют int в качестве аргумента. Если в классе С есть функция void f(int); то pmen= C::f;
Итак, указатели на члены – это обобщение процедурного и указательного ТД.
Замечание.
Интересно, что в Ада83 процедурного ТД не было вообще. Почему?
В книге Кауфмана целый пункт посвящен обоснованию этого.
Поскольку значениями процедурного типа данных являются константы (имена процедур и функций, которые описаны и собраны редактором связей), то мы всегда знаем, какую функцию передаем.
Проблема передачи процедур и функций как параметров решается с помощью понятия родовых процедур.
Родовым процедурам в нашем курсе будет посвящена отдельная глава. Там будут рассматриваться родовые модули языка Ада и шаблоны языка С++.
Интересно, что в языке Ада 95 процедурный ТД появился.
Любые конструкции и решения, принятые в процессе дизайна ЯП, верны относительно какого-либо контекста.
Контекст Ада 83 – это контекст тотального ЯП. Контекст Ада95 – это один из многих ЯП, и поэтому особое внимание уделялось вопросам стандартизации языка, совместимости с другими ЯП. Именно для того, чтобы в программах на Ада можно было вызывать подпрограммы из Fortran или C, в Ада появилось понятие указателя на нединамические объекты и понятие процедурного типа. С помощью процедурных ТД в Ада95 можно стало осуществлять динамическое связывание.
Глава 2.
Составные типы данных.
К составным типам данных относятся:
-
массивы
-
записи
-
объединения (записи с вариантами языка Pascal)
-
множества
-
файлы
-
строки
-
таблицы.
Массивы есть в любом ЯП. В первых ЯП ( Algol, Fortran) было только понятие массива.
К концу 60-х оформилось понятие записи и объединения.
В традиционных ЯП массивы, записи и объединения составляют минимальный базис.
В современных ЯП осталось понятие массива, появилось понятие класса (как обобщение записи) и строки.
Пункт 1. Массивы.
Массив – это последовательность однотипных элементов.
Пусть Т – тип данных, n - натуральное, тогда n элементов типа Т – это и есть массив.
С массивами связано несколько проблем:
-
длина массива (самая главная)
-
описание границ массива
-
сечение.
Проблема границ.
В Fortran все массивы были от 1 до N. Достаточно было указывать длину массива; она же – граница массива по верхнему измерению. Это удобно с точки зрения математической нотации.
Уже начиная с языка Pascal, было введено понятие левой и правой границы массива.
В языке Ада и более развитых ЯП вообще появилась возможность задавать произвольные диапазоны относительно дискретных типов данных.
В конечном итоге все свелось к идеологии языка C, где массивы нумеровались от 0 до N-1. С точки зрения машинных операций, в этом случае доступ наиболее эффективный (никаких операций сдвига не надо.)
Рассмотрим эволюцию ЯП. В Pascal появилось понятие диапазона и индексы могли быть произвольными диапазонами.
В языке Modula2 ситуация та же. В Oberon же массивы описываются также как в С.