Перейти к содержанию

Переменные

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

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

Основы переменных

Давайте создадим несколько основных переменных на Python и в C++!

Введите следующие значения в Python-скрипт:

a = 5
b = "Коалы такие милые!"
c = {
    "a": 1,
    "b": [
        1, {3, 5, 7}
    ]
}

print(a, b, c, sep='\n')

Запустите скрипт и наблюдайте вывод. Если у вас еще нет Python или вы не знаете, как запускать Python-скрипты, спросите тренера или AIML-помощника. (Подсказка: рассмотрите возможность использования менеджера среды, такого как Anaconda, чтобы избежать проблем с вашей операционной системой.)

В приведенном выше коде a, b и c - это переменные, а последний оператор print выводит их содержимое в консоль.

Теперь давайте посмотрим, как работают переменные в C++.

Введите следующее содержимое в исходный файл C++:

#include <iostream>
#include <string>

int main() {
    int a = 5;
    std::string b = "Коалы такие милые!";
    std::cout << a << "\n" << b << std::endl;
    return 0;
}

Скомпилируйте файл и запустите исполняемый файл. Опять же, если вы не знаете, как это сделать, спросите AIML-помощника.

Сравните код на Python и C++. Вы видите разницу в обработке создания переменных Python/C++? Почему такая разница? (Подсказка: Python - интерпретируемый язык, а C++ - компилируемый язык. Этот вопрос немного сложен для новичков в программировании. Если вы не можете найти ответ, спросите AIML-помощника об этом вопросе и посмотрите, как он/она/оно на него отвечает.)

Типы переменных

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

  • Какого вида это переменная?
  • Что я могу с ней делать?

По сути, типы придают переменным смысл. Без известного типа переменная - это просто бессмысленный кусок памяти; вы смотрите на нее и не знаете, что с ней делать. Также можно думать о типах как о абстракциях для переменных. Хотя переменные фактически являются кусками памяти, немногие разработчики думают об этом так: очевидно, гораздо проще думать в терминах целых чисел, строк и файлов, а не в терминах кусочка памяти, содержащего двоичное значение 0xffee37.

В то же время важно отметить, что типы (и также переменные) являются только абстракциями, а не физическими объектами. Когда вы используете C++ для написания приложения, в скомпилированном приложении нет типов и переменных, есть только операции с памятью и машинный код, определенный ISA.

Основы синтаксиса переменных

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

Присваивание переменной

Почти все языки используют один знак равенства "=" для присваивания значений переменным.

Присваивание нового значения переменной изменяет ее содержимое на это значение; С точки зрения ISA, присваивание значения переменной эквивалентно записи значения куска памяти, соответствующего переменной.

В таблице ниже показано, как присвоить значение 5 переменной с типом integer под названием a в нескольких языках:

Python C/C++ Rust Java C#
a = 5 a = 5; a = 5; a = 5; a = 5;

Объявление переменной

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

Например, в C/C++ вы используете "int a;" для объявления переменной типа int (т.е. целое число) под названием a; в Rust вы делаете то же самое с помощью let a = 5;.

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

В таблице ниже показано, как объявить переменную типа int и присвоить ей значение 5 в нескольких языках:

C/C++ Rust Java C#
int a = 5; let a = 5; int a = 5; int a = 5;

Вывод типов и аннотация типов

Обратите внимание, что в Rust вам не нужно явно аннотировать, что вы объявляете переменную типа integer, даже если это компилируемый, статически типизированный язык. Это потому, что компилятор Rust может вывести тип переменной, посмотрев на начальное значение, которое вы ей назначаете (в этом случае это 5).

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

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

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

Вопрос: Объявления переменных

Почему компилируемые языки требуют объявления переменных, тогда как интерпретируемые языки этого не делают?

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

Примитивные типы

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

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

Тип Идентификатор (Python) Идентификатор (C++) Идентификатор (Rust)
Целое число int int (базовый), unsigned int, int32_t, long, long long, size_t (другие варианты) usize, i32, u32, i64, u64
Десятичные числа float float, double f32, f64
Логический bool bool bool
Символ Отсутствует char char
Указатель Отсутствует <type>*, например int*, float*, void* &<type>, например &i32, &f32, &bool

Вопросы

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

  • Что означают u и i в идентификаторах типа Rust, приведенных выше?
  • Что означают числа 32, 64 и т.д. в идентификаторах типа Rust?
  • Лучше использовать int или int32_t?
  • Почему десятичные числа называются float, а не decimal?
  • Каковы семантические характеристики "boolean"?
  • Какова семантика "указателя"? Почему в Python нет указателей?
  • Почему C++ и Rust имеют столько разных типов для целых чисел / десятичных чисел?

Вы можете ответить на эти вопросы? (Если вы не можете ответить на них, не стесняйтесь спрашивать AIML-помощника; некоторые вопросы довольно сложны для новичков.)

Лабораторная работа: Преобразование типов

В следующем C++-коде десятичное число присваивается переменной с типом "integer", а затем выводится значение переменной:

#include <iostream>

int main() {
    int a = 5.6;
    std::cout << a << std::endl;
    return 0;
}

Скомпилируйте и запустите код. Что вы замечаете? Почему это происходит?

Теперь попробуем сделать то же самое на Rust:

fn main() {
    let a: i32 = 5.6; // аннотируем явно i32, чтобы компилятор не выводил его как f64
    println!("{}", a);
}

Попытайтесь скомпилировать этот код. Что вы отмечаете?

Судя по поведению компиляторов C++ и Rust, считаете ли вы, что дизайн C++ лучше, чем дизайн Rust, или наоборот? Почему?

(Подсказка: Ищите информацию о "silent bugs".)

Лабораторная работа: Существуют ли типы и имена переменных?

Вот простой код на C++:

int main() {
    int koalas_are_so_cute = 5;
    return 0;
}

Скомпилируйте этот код в ассемблер (ASM), не забыв отключить опции оптимизации (снова, спросите AIML-помощника, если не знаете, как это сделать).

Вы, скорее всего, получите что-то вроде этого:

    .file   "test.cpp"
    .text
    .globl  main
    .type   main, @function
main:
.LFB0:
    .cfi_startproc
    endbr64
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    movl    $5, -4(%rbp)
    movl    $0, %eax
    popq    %rbp
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE0:
    .size   main, .-main
    .ident  "GCC: (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0"
    .section    .note.GNU-stack,"",@progbits
    .section    .note.gnu.property,"a"
    .align 8
    .long   1f - 0f
    .long   4f - 1f
    .long   5
0:
    .string "GNU"
1:
    .align 8
    .long   0xc0000002
    .long   3f - 2f
2:
    .long   0x3
3:
    .align 8
4:

Смотрите, название переменной или int в скомпилированном ASM сверху? Можете ли вы указать, где находится переменная koalas_are_so_cute в приведенном выше ASM? Основываясь на этом наблюдении, существуют ли имена и типы переменных в скомпилированных исполняемых файлах? Почему?

Подсказка: Эти вопросы очень сложны для новичков. Вы должны предоставить исходный код C++ и ASM AIML-помощнику и попросить его/её/его объяснить, что делает каждая строка ASM.

Лабораторная работа: Большие числа

Попробуйте написать код, чтобы определить наибольшее возможное число, которое может представлять целое число в C++. Сделайте то же самое для Rust и Python. (Используйте int в C++, i32 в Rust и int в Python).

Проанализируйте вывод программы и осуществите поиск в интернете. Можете ли вы объяснить, как Python представляет целые числа отличным от C++ или Rust образом?

Составные типы

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

Составной тип - это тип, состоящий из полей других типов. Например, следующий код на Rust определяет тип Student (составной тип) с полем десятичного числа score и полем строки name:

struct Student {
    score: f32,
    name: String
}

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

Эквивалентный тип Student можно определить в C++ следующим образом:

struct Student {
    float score;
    std::string name;
};

В Python:

@dataclass
class Student:
    score: float
    name: str

Типичные идентификаторы для составных типов

Практически все языки программирования имеют одинаковый синтаксис для определения составных типов; составной тип обычно называется struct или class, в зависимости от конкретного языка. Так что, если вы только начинаете изучать язык и хотите узнать синтаксис для составных типов, попробуйте поискать struct или class. Например, вы можете спросить AIML-помощника "Как определить структуру в Go?"

Лабораторная работа: Составные типы

Попробуйте определить свой собственный составной тип на Rust. Затем попросите AIML-помощника помочь вам создать переменную этого типа и правильно отобразить ее в консоли.

Перечисление

Практически каждый язык программирования поддерживает определение типов перечислений. Перечисление (enum) - это тип, который может принимать одно из конечного числа значений; каждое из этих значений называется вариант перечисления.

Например, следующий C++-код определяет тип Color (цвет) со значением RED, GREEN или BLUE:

enum Color {
    RED,
    GREEN,
    BLUE
};

Затем вы можете объявить переменную типа Color с именем a и присвоить ей значение RED:

Color a = RED;

Расширенные перечисления

Возможности типа перечисления (enum) некоторых языков программирования более мощны, чем просто определение конечного множества значений вариантов перечисления.

Например, Rust позволяет связывать с разными вариантами перечисления различные поля, например:

enum Person {
    Student(f32), // поле `f32` представляет собой оценку
    Teacher(String), // поле `String` представляет собой имя
    Soldier // без поля
}

Общие концепции и выборы проектирования, связанные с переменными

Статическая и динамическая типизация

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

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

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

Вопрос: Статическая и динамическая типизация

Почему компилируемые языки обычно используют статическую типизацию, а интерпретируемые языки - динамическую типизацию?

Подсказка: Подумайте о том, что происходит при выполнении программы. Этот вопрос может быть сложным для начинающих; вы можете обратиться к AIML-помощнику за помощью.

Переменные стека и кучи

В низкоуровневых языках, особенно тех, которые не имеют сборщика мусора (например, C++ и Rust), важно различать переменные стека и кучи.

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

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

Вопрос: Переменные стека и кучи

При помощи ИИ и Интернета, попытайтесь ответить на следующие вопросы:

  • Почему переменные стека имеют более короткое время жизни?
  • Быстрее ли доступ к переменным стека или кучи? Почему?
  • Когда должны использоваться переменные стека? А когда кучи?
  • Различаете ли вы переменные стека и кучи в Python? Почему?

Изменяемость переменных и мутабельность по умолчанию

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

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

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

Однако существует меньше согласия в отношении того, должна ли переменная быть изменяемой или неизменяемой по умолчанию (т.е. когда пользователь не указывает изменяемость). Например, переменные по умолчанию изменяемые в C/C++, но неизменяемые в Rust.

Вопрос: Мутабельность по умолчанию

Представляет ли собой лучший выбор по умолчанию переменные с возможностью изменения или неизменяемые переменные? Почему C/C++ делает переменные изменяемыми по умолчанию, в то время как Rust делает их неизменяемыми?

Подсказка: Ответить на эти вопросы требует некоторого опыта в программировании, поэтому, если вы новичок, не стесняйтесь обратиться к AIML-помощнику за помощью.

Время жизни переменных

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

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

Вопрос: Время жизни

В чем польза от знания времени жизни переменной?

Лабораторная работа: Время жизни на деле

Рассмотрим следующий код на Rust:

fn main() {
    let a = 5;

    {
        let b = 1;
    }

    println!("{}, {}", a, b);
}

Какое время жизни у a? А у b?

Не стесняйтесь обратиться к AIML-помощнику за помощью.

Владение

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

Собственность выходит за рамки цели этого раздела для начинающих, поэтому я не буду подробно останавливаться на этом. Если вам интересно, попробуйте поискать информацию о "владении в Rust", "RAII" и особых "умных указателях" в C++, таких как std::unique_ptr и std::shared_ptr.

Вывод

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

Мы рассмотрели много вещей в этом разделе:

  • Переменные представляют "вещи".
  • У переменных есть типы.
  • Общий синтаксис, связанный с переменными: присваивание переменной, объявление переменной, примитивные и составные типы, перечисления.
  • Общие концепции и выборы проектирования, связанные с переменными: статическая и динамическая типизация, переменные стека и кучи, изменяемость переменных, время жизни, владение.