Сравнение переменных в языках программирования C и Python

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

Как известно C относится к языкам со строгим определением типа переменных, а Python – с динамическим. В основном именно этим обстоятельством обусловлены различия при работе с памятью и, как следствие, с переменными.

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

Когда в C мы объявляем переменные, то обязаны указывать их тип. Например, так: int base, float width. Тип переменной сообщает программе сколько памяти надо выделить под данные, и программа при обращении к переменной правильно может их прочитать (она читает определенное типом количество байт). В языке программирования C сведения о типе данных привязаны к переменным. Когда программа, созданная на C запускается, то все объявленные в ней переменные привязываются к определенным ячейкам памяти и уже не могут быть связаны с другими ячейками до конца выполнения программы.

В Python переменные не хранят информацию о типе данных, с которыми связаны. Сведения о типе привязаны к данным. Поэтому в Python не надо объявлять переменные заранее и не надо указывать их тип. Переменным все равно с чем их свяжут. Они появляются в момент присвоения им значения (count = 10, height = 1.45). Но, понятное дело, нельзя использовать в выражении переменную, которой не было присвоено значение ранее.

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

Рассмотрим конкретный пример. Пусть есть вот такой код на языке C:

int qty;
scanf("%d", &qty);
qty = qty + 2;

Если вы не знаете язык, отмечу, что во второй строке у пользователя просто запрашивается целое число и присваивается переменной qty. Когда программа, содержащая такой код, запускается, то в памяти "бронируется" ряд ячеек под эту переменную. Устанавливается связь между именем qty и первой ячейкой блока памяти, отведенного под данные. Программа анализирует тип переменной и получат сведения, что ее значение простирается на четыре байта (в языке C integer обычно имеет длину в 4 байта). Переменная qty на все время выполнения программы прикреплена к определенным ячейкам. Нельзя оторвать имя qty от этих ячеек и связать с другими. Мы можем только менять содержимое определенных ячеек. Далее сначала запрашивается число у пользователя и кладется в qty, пусть это будет число 5 (понятно, что в памяти хранится двоичный код, но для наглядности на картинке десятичные цифры), затем это число увеличивается на два и содержимое ячеек переписывается.

Создание переменной и изменение ее значение в языке C

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

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

int a = 3, num = 5;
int *pa; // это указатель
pa = &a; // записали адрес данных переменной a
printf("%d ", *pa); // получим 3
pa = #
printf("%d ", *pa); // получим 5

Да, указатели конечно тоже привязываются к определенным ячейкам. Под любой указатель (независимо от типа) в языке C выделяется 4 или 8 байтов (зависит от операционной системы). В указателе хранится адрес области памяти. А тип указателя сообщает на сколько простирается память определенного значения под этим адресом. Если в процессе выполнения программы сначала в указателе хранился один адрес, а затем другой, то получается интересная ситуация, когда с помощью одного указателя мы можем обращаться то к одному участку памяти, то к другому:

Изменение значения указателя

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

char *pch;
pch = (char *) malloc(10);
pch = "hello";
pch = NULL;

Динамическое выделение памяти в языке программирования C

В Python все переменные являются ссылками. Предположим, что они могут менять свои адреса "на корню", т.е. там, где хранится информация о переменных.

Все осложняется тем, что в Python есть изменяемые и неизменяемые типы данных. Числа и строки — это неизменяемые данные, а списки и словари — изменяемые. Разница не всегда очевидна, когда пытаются объяснить это так: строку и число мы не можем изменить, а список запросто. Например, a = "Hello". Попытка сделать так: a[0] = "h", чтобы получить "hello" вызовет ошибку. Но если есть список b = ['a', 'b', 'c'], то выражение b[0] = 'z' пройдет гладко, и b будет содержать ['z', 'b', 'c'].

Однако значение переменной a можно изменить так: a = "hello". На самом деле происходит следующее. Создается новый объект "hello", переменная привязывается к нему, а старый объект остается на прежнем месте. Поскольку на него больше не ссылается ни одна переменная, то он уничтожается.

a = "Hello"
a = "hello"
a = 12

Изменение значения переменной в языке программирования Python

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

nums = [100, 38]
nums[0] =  89
nums.append(26.5)

Изменение списка

В Python есть еще одна интересная вещь. Все изменяемые и некоторые простые значения неизменяемых объектов (0, 1 и т.п.) при присваивании их другим переменным не копируются, а просто связываются с еще одной переменной. В результате получаем множество ссылок на один объект. Эти объекты существуют до тех пор, пока есть хотя бы одна ссылка на них. Если на один изменяемый тип ссылается несколько переменных, то его изменение по одной из них можно увидеть через другую. А вот если тип неизменяемый, то изменение значения одной переменной создает новый объект, а другая переменная остается связанной с прежним значением.

dt = [1, 'a']
n = 0
mdt = dt
m = n
mdt[1] = 'b'
m = 100
del dt

Поведение изменяемых и неизменяемых типов в языке программирования Python

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

Все прекрасно, если полагать

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

Спасибо за публикации. Иногда

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

Я не знаю на сколько это согласуется с идеей вашей публикации, но все же рискну высказать свое видение. Мне кажется не совсем корректно говорить об указателях в Си без акцентирования внимания на том, что это не просто указатели вообще, то есть -- ячейка, в которой записан какой-то адрес). Указатели в Си имеют обязательный тип! То есть, нет "просто указателя", но есть "указатель на int".

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

Мои извинения за критику. Я ведь не знаю ваших замыслов.

Удачи вам!
И пожалуйста, продолжайте! Ваш труд реально нужен.

Спасибо за отзыв. Кстати, в

Спасибо за отзыв.

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