Языки программирования, базовый курс.

Данные, имена и значения

Как корабль назовете, так он и поплывет.

(Густав II Адольф)

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

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

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

Рассмотрим фрагмент программы

X = X+2;

Первый X обозначает место, куда может быть записано значение. Второй X обозначает само значение. Фактически такая запись - это syntax sugar для

X = value(X)+2;

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

Понятие переменной.

В соответствии со сказанным выше, различают так называемые “левый” и “правый” контексты для переменных. Еще раз внимательно разберемся с терминологией. Пусть переменная X равна числу пи. Тогда у нас есть :

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

Простая переменная имеет следующий вид

имя — > ссылка —> значение

Здесь стрелки означают, что зная имя, можно получить ссылку (адрес), а зная адрес, можно получить значение. Конкретный способ получения для нас сейчас не очень важен. Заметим только, что память, в которую можно записать значение, или адрес этой памяти, может ассоциироваться со ссылкой как в процессе компиляции программы, так и во время выполнения. Реальное значение адреса обычно скрыто от програмиста, но во многих языках его можно получить, например, с помощью операции & в языке C, или @ в Pascal. В других языках связь между переменной и адресом не такая простая, и получить адрес в непосредственно машинной форме невозможно. Но он всегда где-то есть, ведь значения переменных больше негде хранить, кроме как в памяти компьютера. В случае простой переменной на компилятор (или интерпретатор, или среду исполнения) возлагается относительно простая задача: поставить в соответсвие переменной число (адрес) и следить за тем, чтобы тот же адрес не был выделен какой-то другой переменной.

Константы

Константа имеет вид

имя —> значение

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

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

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

Существует интересная разновидность констант, у которых есть имена, а вот фактические значения, им соответствующие, нас не интересуют. Так, например, кодируя пол человека, можно мужскому поставить в соответствие 1, а женскому 0, а можно мужскому 95, а женскому 34, и ровным счетом ничего от этого не изменится. Подобные константы в Паскале задаются посредством перечислимых типов, а в C с помощью перечислений (enum).

В качестве примера рассмотрим описание колоды карт (например, для игры пасьянс в Windows)

Pascal:

type cardsuit =
(clubs,diamonds,hearts,spades);

C:

enum cardsuit
{CLUBS,DIAMONDS,HEARTS,SPADES};

Числовые типы и подтипы.

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

Натуральный, Целый, Вещественный, Комплексный. Не в любом языке программирования существуют все эти типы, но их обычно стараются вводить в язык, не в первой версии, так в десятой. Например, в языке С изначально не было комплексных чисел, но в стандарте C99 их ввели. Почему различают именно эти типы, и какая между ними разница ? Разница возникает в том случае, если какие-то стандартные операции (+ - * /) для одних и тех же чисел разных типов выполняются по-разному. Для натурального и целого типов отличаются результаты операции вычитания. Значение выражения 10 - 20 для целых чисел будет равно -10, а для натуральных это скорее всего ошибка, хотя можно получить достаточно неожиданный результат вроде 4294967286 или 65526. На разных машинах он может быть разным, но все равно неправильным. Разница между целыми и вещественными числами, очевидно, состоит в результате операции деления. Обычно деление целых чисел рассматривают как деление нацело, то есть 4/3 = 1.

Разные языки по-разному относятся к смешению разных числовых типов в одном выражении. Если мы складываем вещественное число и целое, то в действительности нужно сначала преобразовать целое в вещественное, а потом сложить эти два числа. Здесь все просто. Но вот с анализом выражения вида (real/real + integer/integer) уже не все так просто. Можно сначала преобразовать целые к вещественному типу, а потом выполнить все операции. А можно сначала поделить целые (нацело), а потом результат преобразовать к вещественному типу. В любом случае легко получить совсем не то, что хотел программист. Например, по первому способу

// вычисляем 1/2 + 3/2
int k,i,j; float p,q,r;
i = 1; j=2;
k = i/j + 3/2;
p = 1.0; r=2.0;
q = p/r + 3/2;
// в результате k станет равным 1, а q равным 2

По второму способу

// вычисляем 1.5 + 0.5
int k,i,j; float p,q,r;
p = 1.5;
q = p + 1/2;
// в результате q станет равным 1.5

Сложно сказать, какой способ хуже. Во многих языках есть правила относительно приведения типов, то есть преобразования типов в выражении. В языке MODULA-2, например, вообще запрещено смешивать целые числа с вещественными без явного их преобразования. Другое “соломоново решение” было принято в некоторых вариантах языка BASIC - там вообще нет целых чисел. В заключение темы попробуйте определить, что делает вот такая программа :

int i,j;
int a[10,10];
for (i=0; i<10; i++)
   for (j=0; j<10;j++)
      a[i,j] = (i/j) * (j/i);

Числовые подтипы

В компьютере обычно имеется несколько вариантов представления чисел того или иного типа. Например, натуральное число может занимать 1 байт, 2 байта, 4 байта итд. Если 1 байт это 8 бит, то с помощью одного байта можно представить числа от 0 до 255, всего 256 значений. Логично было бы как-то выразить такую машинно-зависимую возможность в языке. Хотя бы с целью экономии памяти. С другой стороны, существует много видов данных, представляемых в виде строго ограниченных числовых диапазонов. Месяцев в году всего 12, дней в месяце не более 31, а химических элементов непонятно сколько, но вряд ли более 200. Для таких типов можно ввести в язык отображаемую на реальные типы данных возможность - указать диапазон изменения типа, а компилятор пусть подберет наиболее подходящее внутреннее представление. В некоторых языках идут еще дальше, проверяя, какие именно значения программист пытается присвоить переменным, и проверяя, какие действия можно, а какие нельзя совершать со значениями тех или иных типов.

Башня типов.

Если в языке имеется N типов и мы хотим уметь преобразовывать каждый тип в любой другой, нам потребуется N2-N процедур преобразования. При добвлении N+1-го типа придется обеспечить еще 2N процедур преобразования. Чтобы избежать этой излишней работы была придумана концепция "башни типов", системы, в которой типы данных выстроены в линейную иерархию. Наглядный пример - числовые типы Натуральный, Целый, Вещественный, Комплексный. Каждый из этих типов, кроме последнего, может быть без потерь преобразован к последующему, и каждый, кроме первого, может быть преобразован к предыдущему ценой потери некоторой информации. Так, при преобразовании вещественного числа к целому теряется дробная часть, а при преобразовании целого к натуральному утрачивается знак числа. Тем не менее, все эти преобразования логичны и понятны. Любой новый тип может расширять башню сверху или снизу или же "встраиваться" в ее середину. В любом случае количество преобразований резко сокращается при сохранении возможности преобразований между любыми двумя типами.

Подтипы в языке ADA. Язык ADA имеет одну из наиболее развитых систем типов и подтипов, поэтому мы рассмотрим ее более внимательно. В этом языке есть две возможности. Во-первых, можно явно объявить один тип как подтип другого. При этом переменным базового типа можно присваивать значения производного типа.

type WeekDays is range 1 .. 7;	-- Дни недели это числа от 1 до 7
subtype WorkDays is WeekDays range 1 .. 5; -- Рабочие дни это числа от 1 до 5
A : WeekDays := 8; -- ОШИБКА!
B : WorkDays := 4;
C : WeekDays := B; -- OK

Другой пример:

subtype Natural is Integer range 0 .. Integer'Last; -- Натуральные числа
subtype Positive is Integer range 1 .. Integer'Last; -- Положительные числа

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

type Apples is new Integer ;
type Oranges is new Integer ;
A : Apples := 8;
B : Oranges := A+B; -- ОШИБКА! Яблоки нельзя складывать с апельсинами.

Атрибуты данных.

Каким же образом компилятор узнает, что можно делать с переменными, а что нельзя ? Посмотрим еще раз на диаграмму чуть выше, изображающую переменную. В общем случае ссылка это не просто адрес в памяти, но еще и информация о типе данных. Если программа компилируется, то эта информация зачастую просто “выбрасывается” после генерации кода. Все проверки соответствия типов производятся во время компиляции. Другой подход состоит в том, чтобы хранить где-то информацию о типе данных и проверять соответствие типов во время выполнения. Часто комбинируют оба подхода, или же один и тот же компилятор может сохранять информацию о типе, а может и не сохранять в зависимости от настроек. В компиляторах языка C++ эта информация называется RTTI (Run-Time Type Information), и относится не ко всем переменным.

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

Языки L-типа и R-типа. В языках L-типа информация о типе является частью ссылки, а в языках R-типа - частью значения. При этом неважно, когда именно используется информация о типе - во время компиляции или во время выполнения программы. Если в языке L-типа переменной каким-нибудь способом подсунуть данные не того типа, среда исполнения все равно не будет «знать», что данные неправильные. Наиболее распространенным примером является выход за границы массива. В языках R-типа такой трюк невозможен. К языкам L-типа относятся, например, Pascal и C. Пример языка R-типа - PHP. В нем выйти за границы массива не удастся - они хранятся вместе со всем массивом.

Проблема состоит в том, что, хотя в языке L-типа всегда можно сделать проверки на безопасность, но многие программисты полагают, что основное достоинство таких языков - это именно возможность делать что угодно, обходя проверки. Действительно, некоторые алгоритмы системного программирования работают быстрее и реализуются удобнее при наличии возможности обойти проверку типов. Поэтому возможности так или иначе «обмануть компилятор» рано или поздно добавляют в большинство языков L-типа. Например, в языке Pascal записать значение за границей статического массива невозможно, но уже в варианте Turbo Pascal такая возможность есть.

Покажем, как в Turbo Pascal можно записать что-то в память за границами массива.

type small_array = array[1..10] of integer;
big_array = array[1..100] of integer;
cheat = record
		case integer of
		1: (x: ^small_array);
		2: (y: ^big_array);
		end;

var test:cheat;
    v:small_array; (* это жертва нашего теста *)
begin
  (* v[11]:=5; *)  (* так сделать не получится *)
  test.x := @v;
  test.y^[11]:=3;
  (* 11й элемент находится за границей массива *)
end.

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

Языки L-типа можно приблизить к языкам R-типа с помощью программного моделирования. Например, в языке C++ существует стандартная библиотека STL, реализуюшая различные структуры данных, в том числе и хранящие часть информации о типе. Наоборот сделать нельзя.

Пример на С++

#include <vector>
using namespace std;

vector<int> vec_one; // объявили массив

vec_one.push_back(2);
// Натолкали туда значений

vec_one.push_back(9);
vec_one.push_back(-5);

// теперь там лежат : 2, 9, -5

unsigned int indx;

for (indx = 0; indx < vec_one.size(); indx++)
{ 
  cout << vec_one[indx] << endl;
}

Пример на языке Object Pascal

var MyList: Tlist;
begin
  MyList := Tlist.Create;
  MyList.Add(2);
  MyList.Add(9);
  MyList.Add(-5);
  ...

В дополнение к L и R языкам, формально существуют еще и языки N-типа, где атрибуты — часть имени. В практически вымершем уже диалекте языка BASIC все переменные, имена которых заканчиваются на $ являются строковыми.

Basic

REM ВЕЩЕСТВЕННОЕ
X = 5.5
REM СТРОКА
X$ = "SOME TEXT"

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

$foo # переменная
@foo # массив
%foo # таблица
FOO  # файл
&foo # подпрограмма

Логические значения.

Практически в любом языке есть какой-то вариант операторов if, while и других, требующих для своей работы значения вида “правда”-“ложь”, иначе называемых логическими. Даже если в языке нет специальных значений этого типа, они все равно неявно присутствуют. Обычно существует набор операций, дающих в результате значения логического типа. Например <, >, >= и другие. Для представления логических значений используют следующие приемы:

1. Выделенный тип. В этом случае можно явно объявлять логические переменные, способные принимать всего 2 значения. Тип этот часто называют Boolean, bool или logical, а значения - true и false.

PASCAL:

var x : Boolean;
x := 4 < 5;
if x then
 …

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

BASIC

IF X < 3 THEN GOTO 44
REM а так нельзя :
P = X < 3 

3. Использование другого типа, например целого. Такой подход использован в языке С - любое целое число, не равное 0 означает true, а 0 означает false.

int x;
x = 4 < 5;
if (x) {
 …

Описания переменных.

Некоторые языки, например Pascal и C, требуют описания переменных до их использования. В других языках первое появление имени переменной в тексте программы является описанием этой переменной. Иногда переменные можно описывать, но делать это не обязательно. Таким образом, можно выделить два основных признака языков по отношению к описанию переменных: можно ли описывать переменные в языке и необходимо ли это делать.

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

s1 = "foo" # s1 теперь имеет тип "строка"

def one(): 
	return 1 # Функция one() возвращает 1, следовательно, 
	         # тип возвращаемого значения - int

un = one()   # переменная um имеет тип int

s1 = 25      # ошибка! s1 уже известна как строковая переменная.
un = "one"   # опять ошибка! un - целочисленная переменная.

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

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

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

3. Использование неинициализированных переменных может быть запрещено или невозможно.

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

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

C

int mass = 84.5;
int length = 50;

Выведение типов

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

mass = 84.5; // вещественная переменная
length = 50; // целая переменная

Такой метод описания носит название type inference или выведение типов. Здесь тип переменной логически выводится из начального значения. Так можно делать во многих современных языках, таких как Boo, Haxe.

Переменные-ссылки

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

имя — > ссылка —> значение (само является ссылкой) —> значение

Иногда их так и называют - ссылочные переменные. Впервые такие переменные появились в языке Algol-68.

C

int q = 5;
int *p; 	// p - ссылка "в никуда"
p = &q; // теперь p "указывает" на q
*p = 3; 	// теперь q равно 3

PASCAL

var p,q: ^integer;
new (q); // запросим у среды исполнения адрес для q
q^:=5;
p:=q; // p и q теперь обозначают одно и то же место в памяти
p^:=3  // теперь q^ тоже равно 3

Обратите внимание на операторы * в C и ^ в Pascal. Они осуществляют так называемую операцию разыменования, или получения переменной по ссылке. Таким образом, если у нас переменная p является ссылкой на целое

переменная p — > ссылка —> ссылка на целое —> целочисленное значение

то разыменование выглядит так, как если бы у нас была переменная с именем *p

*p — > ссылка на целое —> значение

Важно заметить, что *p ведет себя как полноценная переменная - слева от знака присваивания она означает ссылку, справа - значение. Значением переменной p является ссылка. Почему сама p имеет тип "ссылка на целое" а не просто "ссылка" ? Потому, что у нас язык L-типа, и тип ассоциируется со ссылкой. Сравните обычную переменную и ссылочную:

переменная i — > ссылка —>вещественное число
переменная p — > ссылка —> ссылка на вещественное значение
Логично предположить, что если в первом случае мы считаем i переменной вещественного типа, то во втором она будет переменной типа "ссылка на вещественное значение". Напомним, что ссылка это просто адрес в памяти, по которому лежит значение. Знание этого адреса дает нам возможность как считывать значение, так и изменять его. В связи с этим можно дать другое, более широкое определение ссылки. Ссылка это некое число специального типа, зная которое, можно считывать и записывать значение определенного типа. А именно "ссылка на целое" дает нам доступ к значению целого типа, "ссылка на вещественное" - доступ к значению вещественного типа и так далее. Сама ссылка, как и любое другое число, может быть значением переменной в тех языках, в которых это допустимо. Соответственно, никто не мешает иметь значения (и переменные) типа "ссылка на ссылку на целое", "ссылка на ссылку на ссылку на ссылку на вещественное" и так далее.

Для чего нужны ссылочные переменные ? В основном они призваны решать три ответственные задачи.

При внимательном рассмотрении процесса разыменования возникает вопрос: а на какую переменную указывает ссылка с самого начала, если ей не присвоено начальное значение ? Дело в том, что ссылка вообще не обязана указывать на какую-то память, и для реализации динамического распределения памяти нам как раз и нужны ссылки, “ни на что не указывающие”, для которых мы впоследствии выделим память. Как мы помним, в языке L-типа определить, что именно содержится в данном участке памяти, во время выполнения программы невозможно. Да и просто попытка читать даные по произвольному адресу может закончится серьезным сбоем программы. Имеется существенное отличие между начальными значениями числовых переменных и начальными значениями ссылок. Для первых большинство (или все) возможные сочетания бит представляют собой разрешенные значения, за редкими исключениями, а для ссылок наоборот - подавляющее большинство адресов вызовет сбой при попытке их разыменования (хотя являться значением ссылочной переменной они могут) . Иначе говоря, далеко не все возможные адреса содержат осмысленные данные определенного типа.

Для решения этой проблемы в языках L-типа обычно выделяется специальная константа, называемая NULL или nil, которую можно присваивать любой переменной ссылочного типа. При этом гарантируется, что ни один “легальный” адрес такого значения иметь не будет. Чаще всего константа NULL численно равна 0, но это не обязательно так. Присваивая значение NULL или nil, можно получить указатель, не ссылающийся ни на какую область памяти, и более того, читая это значение, можно проверить, что указатель ни на что не ссылается. Таким образом, “пожертвовав” одним значением из многих тысяч возможных, решили важную проблему с указателями. Заметим, что эта константа имеет еще одно важное свойство: она может быть присвоена переменной любого ссылочного типа. Иначе говоря, в нашем компьютере есть полки для хранения целых чисел, есть полки для хранения вещественных или комплексных чисел, есть полки для хранения адресов других полок, но табличку "тут ничего нет, и не ищите" мы можем повесить на любую полку.

Всегда имеется возможность проверить, не является ли указатель NULL или nil, а попытка записать по этому адресу какое-то значение должна заканчиваться аварией (но не всегда заканчивается).

C

int *a = NULL;
…
if (a == NULL) // память не выделена
  a = malloc(100); // выделение памяти

Pascal

type arr = array[1..20] of integer;
     parr = ^arr;
	 var p:parr;
	 begin
	   p:=nil;
       …
       if  p=nil then
	     new(p);
     end.

Область действия переменных

Вводя в языки переменные, их создатели обеспечили програмистам возможность безо всяких сложностей называть разными именами разные области памяти. Ссылки дают возможность разными именами обозначить один и тот же участок памяти. Логично было бы спросить, можно ли одним и тем же именем назвать разные вещи ? Разумеется, можно. Представим себе, что было бы, если бы это было не так. Если бы каждое имя в программе было закреплено за строго определенным участком памяти, то прежде всего, намного осложнилась бы совместная работа нескольких программистов над одной и той же программой. Им пришлось бы договариваться о том, какие имена кто из них использует. Например, один мог бы все идентификаторы начинать с буквы alex_, второй с bob_ и так далее. Кроме того, многие переменные, не несущие особенной смысловой нагрузки, например, вездесущий параметр цикла i, пришлось бы каждый раз называть по-новому. Да еще и учитывать, какие имена уже использованы, а какие еще нет. К счастью, в большинстве языков идентификаторы имеют строго определенную область действия. Как правило, областью действия идентификатора является блок, в котором он описан.

Блоком называется участок программы, заключенный между открывающей и закрывающей операторными скобками, например begin…end в Pascal или {…} в C/C++. Примеры

ALGOL 60

begin integer x,i,z;
	x := 3;
		begin real x
			x := 5.5;  comment: это уже другой x;
			i := 6;
		end;
end;

C++

{	int x,i,z;
	x = 3;
	{	double x;
		x = 5.5;  // это уже другой x
		i = 6;
	}
	x = 7;  // возвращаемся к прежнему пониманию x
}

// проблема :

for (int i = 0; i<3; i++)
 	cout << i << endl;
for (int i = 0; i<5; i++)   // это та же самая i или другая или это ошибка?
	cout << i << endl;

В связи с областями действия идентификаторов возникает целый ряд своеобразных проблем. Одна из них может показаться надуманной: а что если программист, только что “закрывший” вложенным описанием внешнее, вдруг решит все-таки обратиться к внешней переменной (она же никуда не девается во время выполнения блока, просто ее имя временно отдается другой переменной). Несмотря на явную абсурдность такого вопроса (зачем тогда было называть внутреннюю переменную тем же именем ?), некоторые языки предоставляют такую возможность. Так, в C++ можно обратиться к переменной, находящейся на самом верхнем (глобальном) уровне, поставив перед именем двойное двоеточие ::.

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

Распределение памяти.

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

Статическое распределение памяти - наиболее простой способ. Каждой переменной ставится в соответствие строго определенный адрес памяти, по которому находится ее значение. Этот адрес не меняется на протяжении всего выполнения программы. К этому классу относятся переменные, описанные в языке C на верхнем уровне, вне функций, или с ключевым словом static. В языке FORTAN-IV этот способ - единственный.

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

int function_auto()
{
	int p = 10; // переменная p - автоматическая
	p=p+1;
	return p;
}

// значение p больше не существует

int p = 10;

int function_static()
// переменная p - статическая
{
	p=p+1;
	return p;
}

// значение p является “внешним” по отношению к функции

int main()
{
	int i;
	for(i=0;i<10;i++)
	{
		printf("i=%d, function_auto() returns %d\n",i,fun_auto());
		printf("i=%d, function_static() returns %d\n",i,fun_ static());
	}
}

Динамическое распределение памяти это постановка в соответствие ссылочным переменным каких-то подходящих адресов памяти.

С

int * a;
a = (int *) malloc(100);
a[5] = 33;
free(a);

Pascal

type intarray = array [1..100] of integer;
var a : ^intarray;
begin
	new(a);
	a^[5] := 33;
	dispose(a);
end.

Java

int [ ] a;
a = new int[100];
a[5] = 33;

Функция malloc в C, операторы new в Pascal и Java выделяют память для переменной. Заметим, что сама переменная-ссылка, которая используется для доступа к выделенной памяти, относится к классу автоматической памяти. Таким образом, при завершении блока, память останется распределенной, а единственное средство доступа к ней будет потеряно. Поэтому в C и Pascal существуют средства (free и dispose соответственно) чтобы освободить память перед тем, как средство доступа к ней будет потеряно. Для того, чтобы понять, почему память не освобождается автоматически, достаточно вспомнить п 1) из списка “для чего нужны ссылки”. Может существовать другая ссылочная переменная, указывающая на тот же участок памяти. Освобождать память автоматически можно только тогда, когда не останется ни одной переменной, на нее ссылающейся. В C и Pascal следить за тем, чтобы вся полученная от системы память возвращалась обратно, должен программист. Однако, в языке Java никакого аналога функции free мы не находим. Получается, что есть какой-то способ автоматически освобождать память ? Есть, но он связан с большим объемом программного моделирования. Проверять, не найдется ли где-нибудь еще одна переменная, указывающая на тот же адрес, каждый раз при выходе из блока было бы слоишком накладно. Поэтому такую проверку выполняют сразу для всех переменных и для всех выделенных блоков памяти. Те участки памяти, к которым нет доступа, освобождаются. Эта довольно сложная процедура носит название “сборка мусора”.

Задания

  1. Напишите программу, которая выделяет память с помощью указателя, а потом печатает полученное значение в виде числа. Выясните, меняется ли полученное значение от запуска к запуску, зависит ли оно от размера выделяемой памяти, от того, выделялась ли память для других переменных.
  2. Получите адрес, по которому размещена обычная переменная. Зависит ли он от класса памяти (статическая, автоматическая), от того, были ли раньше описаны какие-то переменные.
  3. Напишите свою реализацию функции malloc.