1.1. Переменные - StriderAJR/StudentCpp GitHub Wiki

Они такие непостоянные

Обзор

Переменная - это область память с заданным символьным именем.

int a;

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

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

Объявление переменной - процесс резервирования в памяти нужного объема памяти и присвоения этой области памяти какого-то имени.

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

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

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

int a1;		// Зарезервировали 4 байта для целочисленного числа
			// и присвоили этому участку памяти имя "а1"
char a2;	// Зарезервировали 1 байт для символа
			// и присвоили этому участку памяти имя "а2"		
bool a3;	// Зарезервировали 1 байт для логического значения
			// и присвоили этому участку памяти имя "а3"	
float a4;	// Зарезервировали 8 байт для числа с плавающей запятой
			// и присвоили этому участку памяти имя "а4"

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

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

a2 = 65; // Инициализация переменной а2
a3 = 65; // Инициализация переменной а3
cout << a2 << endl; // Выведется символ 'A'
cout << a3 << endl; // Выведется 1 - истина

Маленькое замечение: в языке С++ логический тип данных ничем не отличается от числового. Все, что отлично от 0 - истина, и только сам 0 - ложь.

Итак, обратите внимание, что на деле переменные а2 и а3 обе в одном байте памяти хранят число 65. Но при выводе на экран, а2 выводится как символ, а а3 как число (более продвинутый язык С# бы вывел как и положено логическому типу данных фразу "true" - истина, но это С++, поэтому сильно не переживайте по этому поводу). Почему?

Все из-за разных типов данных этих переменных.

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

Объявление и инициализацию можно совмещать

	float d = 2.5;
        double e = 3.4;

ВАЖНО! Считается обязательным давать переменным вразумительные имена, чтобы только по самому имени переменной было понятно, для чего она создана Например, переменные счетчики называют

	int i;
        int j;

Чтобы посчитать что-то, то используют имя

int count;

Для символов

char  c; 

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

строки

        char* str1;
        string str2;

ну и т.д.

        bool flag;
        bool isGood;
        bool isToFinish;
        int sum;
        double mult; // mult - от multiply, т.е. результат перемножения
        float part; // part - доля
        int** matrix; // матрица
        int* array; // массив
        char* buf; // буфер
        int min; // минимальное значение
        int max; // максимальное значение

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

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

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

Кстати:

Инициализация переменной - процесс записи в область памяти какого-то значения.

Объявление переменной и инициализацию можно объядинать в одном выражении:

int a5 = 10; // Объявили и сразу проинициализировали переменную а5

Локальные и глобальные переменные

Тут вижу, тут не вижу

Если кто-то изучал раньше Паскаль, то, возможно, поймет меня. Лично меня страшно бесило, что все переменные в этом языке нужно было объявлять в начале процедуры.

Procedure Example();
Var
  a, b, c : real;
begin
  a := 5.2;
  b := 2.1;
  c := a-b;
end ;

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

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

Однако, согласитесь может возникнуть премерзкая ситуация:

bool flag = true;
int a;
if(flag)
{	
	int b;
	cin >> b; // Просим пользователя ввести с клавиатуры число b
	a = b*2;
}
else
{
	int b = 4;
	a = b/2;
}

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

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

Логически мы понимаем, что в двух разных блоках кода - две разные переменные b. И это правильно. Чтобы избежать таких конфликтов в С++ тоже есть понятия, которые позволяют избежать таких неоднозначностей: локальная переменные и глобальные переменные.

#pragma once

#include <iostream>
using namespace std;

namespace LocalGlobalVariables
{
    int count; // "Глобальная"

    void func1()
    {
        int x = 10;

        count++; 
    }

    void func2()
    {
        int x = -199;

        count++; 
    }

    void main()
    {
        count = 0; 
        setlocale(LC_ALL, "rus");

        int t; // А вот это уже локальная переменная
        cin >> t;

        if (t == 1)
        {
            char buf[80];

            cout << "Введите имя:";
            cin >> buf;
        }

Внутри ф-ции мы не объявляли переменную count. Но она все равно доступна, и мы увеличиваем ее значение

void func1()
    {
        int x = 10;

        count++; 
    }

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

  void main()
    {
        count = 0;
        setlocale(LC_ALL, "rus");
	int t; 
        cin >> t;

Если кратко:

  1. Глобальные переменные хранятся на протяжение выполнения всей программы. Т.е. занимают под себя место.
  2. Имя глобальной переменной занято. Создание локальной переменной в таким же именем затруднит чтение кода.
  3. Алгоритм программы становится менее прозрачным, потому что вам нужно знать про все глобальные переменные, какие есть в коде.

Если очень кратко: ГЛОБАЛЬНЫЕ ПЕРЕМЕННЫЕ - ПЛОХО! НЕ ИСПОЛЬЗУЙТЕ ИХ.

Если хотите более развернутого ответа, то можете погуглить. Например, http://www.gunsmoker.ru/2011/04/blog-post.html

Глобальные переменные

Глобальные переменные доступны из любого места в модуле программы. (Т.к. с понятием модуля в языке С++ кто-то может быть пока что не знаком, считайте, что это тот файл, в котором вы пишете код, например, main.cpp)

int tmp = 5; // tmp - глобальная переменная

void Func()
{
	tmp = 0;
}

int main()
{
	tmp = tmp+100;
	Func();
	cout << tmp; // Выведется 0
}

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

Блок кода - код расположенный между символами '{' и '}'

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

Дело в том, что в памяти программы глобальные переменные занимают свое особое пространство. И не всегда вы можете быть уверены, что это пространство безгранично. Но даже не это страшно в бездумном использовании глобальных переменных.

Самое плохое - это снижение читаемости и понятности кода. Когда вы используете глобальную переменную в программах сложнее "Hello, World!" вам становится сложно сказать, где и в какой момент меняется состояние этой переменной. Вы не контролируете уровни доступа до нее. Вы можете забыть, где меняется эта переменная, а потом долга искать, почему данные в памяти имеют такой странный вид. Все это сделало использование глобальных переменных нежелательным.

Глобальные переменные такой же инструмент языка программирования, как и многие другие. Нужно уметь применять это с умом! Но всегда, когда возможно обойтись без использования шлобальных переменных, не используйте их.

Глобальные переменные плохи по двум причинам:

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

  2. Снижение читабельности кода программы

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

Локальные переменные

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

void Func()
{ // <-- Начало блока кода фун-ции Func
	int c = 3;

	cout << a; // Ошибка: Идентификатор 'а' не найден
	cout << b; // Ошибка: Идентификатор 'b' не найден
	cout << c; // Ошибки не будет
} // <-- Конец блока кода фун-ции Func

int main()
{ // <-- Начало блока кода фун-ции main
	int a = 1;
	for(int i = 0; i < 10; i++)
	{ // <-- Начало блока кода цикла for
		int b = 2;
		b = b+1;

		cout << a; // Ошибки не будет
		cout << b; // Ошибки не будет
		cout << c; // Ошибка: Идентификатор 'с' не найден
		cout << i; // Ошибки не будет
	} // <-- Конец блока кода цикла for

	cout << a; // Ошибки не будет
	cout << b; // Ошибка: Идентификатор 'b' не найден
	cout << c; // Ошибка: Идентификатор 'с' не найден
	cout << i; // Ошибка: Идентификатор 'i' не найден
} // <-- Конец блока кода фун-ции main

Переменная c объявлена в фун-ции Func, поэтому существует только в рамках этой функции. Внутри функции main ее не существует, поэтому при попытке любого к ней обращения будет выдаваться ошибка.

Переменная а объявлена в фун-ции main и существует как в самой функции main, так и во всех дочерних блоках кода любой вложенности. Поэтому переменная а видна в любом месте функции main, в том числе внутри цикла for.

Переменные b и i (!) объявлены внутри цикла for. И да, переменная i является локальной переменной цикла for! Обе этих пермеенные существуют только в рамках цикла и после его завершения уже не существуют.

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

Поэтому на протяжение цикла, переменная i будет выводиться на экран как 0, 1, 2, 3, ..., 9, а переменная b все время будет выводиться на экран равное 2.

Где хранятся локальные переменные

Еще важно помнить, что локальные переменные создаются в стеке программы. И стек ограничен в своем размере.

Архитектурно такое решение оправдано тем, что в стек легко добавлять переменные при их создании с помощью команды push и достаточно помнить, сколько переменных было создано в каждом блоке кода. Тогда доатсточно сделать нужно количество команд pop по завершении блока кода, и стек всегда будет чистым и опрятным, без мусора.

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

Типы данных

Типы данных в любом языке программирования делятся на 2 вида:

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

Пользовательские типы данных будут рассмотрены в темах "Структуры" и "Классы". Сейчас рассмотрим перечень основных базовых типов данных:

Логический тип данных bool

Хранит всего 2 значения: истина (true) или ложь (false). Но естественно не смотря на это занимает в памяти полноценный байт памяти по правилу "наименьшая адресуемая ячейка памяти = 1 байту".

Причем в языке С++ истоной является все, что отлично от 0 (нуля), а ложью будет только значение равное нулю. Например,

  • 10 - это true
  • -1 - это true
  • 1 - это true
  • 0 - это false

Причем, в С++ работает даже такой код:

bool f1 = 5;
bool f2 = -1;
bool f3 = 0;
bool f4 = true;
bool f5 = false;

Но все же правильнее с логическим типом данных работать именно с константами true и false. Это правильнее, логичнее и не будет вам стоить вредной привычки в дальнейшем. Например, в языке C# логической переменной уже нельзя присваивать числовую константу, только логическую.

Символьный тип данных char

Переменная типа данных char будет хранить символ и занимать в памяти 1 байт памяти. Соответственно, char переменная изменяется в диапазоне от 0 до 255. Причем это число является кодом символа из таблицы ASCII (Полный перечень кодов таблицы ASCII).

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

int c1 = 33;
char c2 = 33;
cout << c1; // Выведет число 33
cout << c2; // Выведет символ '!', чей код равен 33 (см. таблицу ASCII)

Целочисленные типы данных

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

  • short занимает 2 байта
  • int занимает 4 байта
  • long занимает 8 байт

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

2 байта - 16 бит, т.е. 16 разрядов. Максимальное число, которое может храниться в двоичном представлении это:

1111 1111 1111 1111

или это число еще можно записать как:

1 0000 0000 0000 0000 - 1

А это число легко можно записать уже на "человеческом языке" как

2^16 - 1

2 в степени 16 (потому что единица стоит в 16-ом разряде) минус единица. Возьмите калькулятор и посчитайте (или если вы крутой, то посчитайте в уме), какое получается число.

Итак, это число 65535. Таким образом диапазон типа данных short изменяет от 0 до 65535. Но на самом деле так изменяется unsigned short - беззнаковый вариант этого типа данных.

Сам же short изменяется в диапазоне от -32768 до 32767 - беззнаковый диапазон поделился по сути пополам.

Теперь вы можете легко высчитывать диапазон изменения целочисленных переменных в зависимости от типа данных.

Кстати, маленькое замечание: short, int, long - это всего лишь псевдонимы типов данных. И эти псевдонимы могут на разных платформах и архитектурах скрывать различные типы данных с разными размерами. На самом деле, при написании кросс-платформенных приложений лучше пользоваться системными названиями __int16, __int32, __int64 и т.д. Но когда это не жизненно важно, то обычно пользуются именами-псевдонимами.

Типы данных с плавающей запятой

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

  • float занимат 4 байта и хранит значения от 1,1754943 x 10^-38 до 3,4028234 х 10^38
  • double занимает 8 байт и хранит значения от 2,2250738585072014 x 10^-308 до 1,7976931348623157 х 10^308

Видно, что double хранит намного более точные значения.

Ссылки на исходники

⚠️ **GitHub.com Fallback** ⚠️