Переменные

Теги: Си переменные. char, int, unsigned, long, long long, float, double, long double, long float, lexical scoping. Объявление переменных. Область видимости. Инициализация переменных. Имена переменных. Экспоненциальная форма.



Переменные

Переменные используются для хранения значений (sic!). Переменная характеризуется типом и именем. Начнём с имени. В си переменная может начинаться с подчерка или буквы, но не с числа. Переменная может включать в себя символы английского алфавита, цифры и знак подчёркивания. Переменная не должна совпадать с ключевыми словами (это специальные слова, которые используются в качестве управляющих конструкций, для определения типов и т.п.)

auto double int struct
break else long switch
register typedef char extern
return void case float
unsigned default for signed
union do if sizeof
volatile continue enum short
while inline
А также ряд других слов, специфичных для данной версии компилятора, например far, near, tiny, huge, asm, asm_ и пр.

Например, правильные идентификаторы
a, _, _1_, Sarkasm, a_long_variable, aLongVariable, var19, defaultX, char_type
неверные
1a, $value, a-long-value, short

Си - регистрозависимый язык. Переменные с именами a и A, или end и END, или perfectDark и PerfectDarK – это различные переменные.

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

Тип переменной определяет

  • 1) Размер переменной в байтах (сколько байт памяти выделит компьютер для хранения значения)
  • 2) Представление переменной в памяти (как в двоичном виде будут расположены биты в выделенной области памяти).
В си несколько основных типов. Разделим их на две группы - целые и числа с плавающей точкой.

Целые

  • char - размер 1 байт. Всегда! Это нужно запомнить.
  • short - размер 2 байта
  • int - размер 4 байта
  • long - размер 4 байта
  • long long - размер 8 байт.
Здесь следует сделать замечание. Размер переменных в си не определён явно, как размер в байтах. В стандарте только указано, что

char <= short <= int <= long <= long long

Указанные выше значения характерны для компилятора VC2012 на 32-разрядной машине. Так что, если ваша программа зависит от размера переменной, не поленитесь узнать её размер.

Теперь давайте определим максимальное и минимальное число, которое может хранить переменная каждого из типов. Числа могут быть как положительными, так и отрицательными. Отрицательные числа используют один бит для хранения знака. Иногда знак необходим (например, храним счёт в банке, температуру, координату и т.д.), а иногда в нём нет необходимости (вес, размер массива, возраст человека и т.д.). Для этого в си используется модификатор типа signed и unsigned. unsigned char - все 8 бит под число, итого имеем набор чисел от 00000000 до 11111111 в двоичном виде, то есть от 0 до 255 signed char от -128 до 128. В си переменные по умолчанию со знаком. Поэтому запись char и signed char эквивалентны.

Таб. 1 Размер целых типов в си.
Тип Размер, байт Минимальное значение Максимальное значение
unsigned char 1 0 255
signed char
( char )
1 -128 127
unsigned short 2 0 65535
signed short
( short )
2 -32768 32767
unsigned int
( unsigned )
4 0 4294967296
signed int
( int )
4 -2147483648 2147483647
unsigned long 4 0 4294967296
signed long
( long )
4 -2147483648 2147483647
unsigned long long 8 0 18446744073709551615
signed long long
( long long )
8 -9223372036854775808 9223372036854775807

sizeof

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

#include<conio.h>
#include<stdio.h>

int main() {
	char c;
	short s;
	int i;
	long l;
	long long L;

	//Вызов sizeof как "функции"
	printf("sizeof(char)  = %d\n", sizeof(c));
	printf("sizeof(short) = %d\n", sizeof(s));
	printf("sizeof(int)   = %d\n", sizeof(i));
	printf("sizeof(long)  = %d\n", sizeof(l));
	printf("sizeof(long long) = %d\n", sizeof(L));

	//Вызов как оператора
	printf("sizeof(char)  = %d\n", sizeof c);
	printf("sizeof(short) = %d\n", sizeof s);
	printf("sizeof(int)   = %d\n", sizeof i);
	printf("sizeof(long)  = %d\n", sizeof l);
	printf("sizeof(long long) = %d\n", sizeof L);
	
	_getch();
}

(Я думаю ясно, что переменные могут иметь любое валидное имя). Эту программу можно было написать и проще

#include<conio.h>
#include<stdio.h>

int main() {

	printf("sizeof(char)  = %d\n", sizeof(char));
	printf("sizeof(short) = %d\n", sizeof(short));
	printf("sizeof(int)   = %d\n", sizeof(int));
	printf("sizeof(long)  = %d\n", sizeof(long));
	printf("sizeof(long long) = %d\n", sizeof(long long));
	//нельзя произвести вызов sizeof как оператора для имени типа
	//sizeof int - ошибка компиляции

	_getch();
}

В си один и тот же тип может иметь несколько названий
short === short int
long === long int
long long === long long int
unsigned int === unsigned

Типы с плавающей точкой

  • float - 4 байта,
  • long float - 8 байт
  • double - 8 байт
  • long double - 8 байт.
Здесь также приведены значения для VC2012, по стандарту размер типов float <= long float <= double <= long double все числа с плавающей точкой - со знаком.
Таб. 2 Размер типов с плавающей точкой в си.
Тип Размер, байт Количество значащих знаков мантиссы Минимальное значение Максимальное значение
float 4 6-7 1.175494351 E – 38 3.402823466 E + 38
double 8 15-16 2.2250738585072014 E – 308 1.7976931348623158 E + 308

Переполнение переменных

Си не следит за переполнением переменных. Это значит, что постоянно увеличивая значение, скажем, переменной типа int в конце концов мы "сбросим значение"

#include <conio.h>
#include <stdio.h>

void main() {
	unsigned a = 4294967295;
	int b = 2147483647;
	//Переполнение беззнакового типа
	printf("%u\n", a);
	a += 1;
	printf("%u", a);
	//Переполнение знакового типа
	printf("%d\n", b);
	b += 1;
	printf("%d", b);
	getch();
}

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

Постфиксное обозначение типа

При работе с числами можно с помощью литер в конце числа явно указывать его тип, например

  • 11 - число типа int
  • 10u - unsigned
  • 22l или 22L - long
  • 3890ll или 3890LL - long long (а также lL или Ll)
  • 80.0f или 80.f или 80.0F - float (обязательно наличие десятичной точки в записи)
  • 3.0 - число типа double
Экспоненциальная форма записи также по умолчанию обозначает число типа double.

#include<conio.h>
#include<stdio.h>

int main() {

	printf("sizeof(int) = %d\n", sizeof(10));
	printf("sizeof(unigned) = %d\n", sizeof(10u));
	printf("sizeof(long) = %d\n", sizeof(10l));
	printf("sizeof(long long) = %d\n", sizeof(10ll));
	printf("sizeof(float) = %d\n", sizeof(10.f));
	printf("sizeof(double) = %d\n", sizeof(10.));
	printf("sizeof(double) = %d\n", sizeof(10e2));

	getch();
}

Следующий код, однако, не будет приводить к ошибкам, потому что происходит неявное преобразование типа

int a = 10u;
double g = 3.f;

Шестнадцатеричный и восьмеричный формат

Во время работы с числами можно использовать шестнадцатеричный и восьмеричный формат представления. Числа в шестнадцатиричной системе счисления начинаются с 0x, в восьмеричной системе с нуля. Соответственно, если число начинается с нуля, то в нём не должно быть цифр выше 7:

#include<conio.h>
#include<stdio.h>

void main() {
	int x = 0xFF;
	int y = 077;
	printf("hex x = %x\n", x);
	printf("dec x = %d\n", x);
	printf("oct x = %o\n", x);
	printf("oct y = %o\n", y);
	printf("dec y = %d\n", y);
	printf("hex y = %x", y);
	getch();
}

Экспоненциальная форма представления чисел

Экспоненциальной формой представления числа называют представление числа в виде M e ± p , где M - мантиса числа, p - степень десяти. При этом у мантисы должен быть один ненулевой знак перед десятичной запятой.
Например 1.25 === 1.25e0, 123.5 === 1.235e2, 0.0002341 === 2.341e-4 и т.д.
Представления 3.2435e7 эквивалентно 3.2435e+7
Существеут и другое представление ("инженерное"), в котором степень должна быть кратной тройке. Например 1.25 === 1.25e0, 123.5 === 123.5e0, 0.0002341 === 234.1e-6, 0.25873256 === 258.73256e-3 и т.д.

Объявление переменных

В си переменные объявляются всегда в начале блока (блок - участок кода ,ограниченный фигурными скобками)

<возвращаемый тип> <имя функции> (<тип> <аргумент>[, <тип> <аргумент>]) {
	объявление переменных

	всё остальное
}

При объявлении переменной пишется её тип и имя.

int a;
double parameter;

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

long long arg1, arg2, arg3;

Например

#include <stdio.h>
#include <conio.h>

int main() {
	int a = 10;
	int b;
	while (a>0){
		int z = a*a;
		b += z;
	}
}

Здесь объявлены переменные a и b внутри функции main, и переменная z внутри тела цикла. Следующий код вызовет ошибку компиляции

int main() {

	int i;
	i = 10;
	int j;
}

Это связано с тем, что объявление переменной стоит после оператора присваивания. При объявлении переменных можно их сразу инициализировать.
int i = 0;
При этом инициализация при объявлении переменной не считается за отдельный оператор, поэтому следующий код будет работать

int main() {

	int i = 10;
	int j;
}

Начальное значение переменной

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

#include<conio.h>
#include<stdio.h>

int main() {
	int i;
	printf("%d", i);
	getch();
}

Если выполнять эту программу на VC, то во время выполнения вылетит предупреждение
Run-Time Check Failure #3 - The variable 'i' is being used without being initialized.
Если нажать "Продолжить", то программа выведет "мусор". В многих других компиляторах при выполнении программы не будет предупреждения.

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

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

#include<conio.h>
#include<stdio.h>

int global = 100;

void foo() {
	printf("foo: %d\n", global);
}

void bar(int global) {
	printf("bar: %d\n", global);
}

int main() {
	foo();
	bar(333);
	getch();
}

Будет выведено
foo: 100
bar: 333
Здесь глобальная переменная global видна всем функциям. Но аргумент функции затирает глобальную переменную, поэтому при передаче аргумента 333 выводится локальное значение 333.
Вот другой пример

#include<conio.h>
#include<stdio.h>

int global = 100;

int main() {
	int global = 555;
	printf("%d\n", global);
	getch();
}

Программа выведет 555. Также, как и в прошлом случае, локальная переменная "важнее". Переменная, объявленная в некоторой области видимости не видна вне её, например

#include<conio.h>
#include<stdio.h>

int global = 100;

int main() {
	int x = 10;
	{
		int y = 30;
		printf("%d", x);
	}
	printf("%d", y);
}

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

#include<conio.h>
#include<stdio.h>

int global = 100;

int main() {
	int x = 10;
	{
		int x = 20;
		{
			int x = 30;
			printf("%d\n", x);
		}
		printf("%d\n", x);
	}
	printf("%d\n", x);
	getch();
}

Программа выведет
30
20
10
Глобальных переменных необходимо избегать. Очень часто можно услышать такое. Давайте попытаемся разобраться, почему. В ваших простых проектах глобальные переменные выглядят вполне нормально. Но представьте, что у вас приложение, которое

  • 1) Разрабатывается несколькими людьми и состоит из сотен тысяч строк кода
  • 2) Работает в несколько потоков

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

Безусловно, есть ситуации, когда глобальные переменные упрощают программу, но такие ситуации случаются не часто и не в ваших домашних заданиях, так что НЕ СОЗДАВАЙТЕ ГЛОБАЛЬНЫХ ПЕРЕМЕННЫХ!
Переменные могут быть не только целочисленными и с плавающей точкой. Существует множество других типов, которые мы будем изучать в дальнейшем.

Q&A

Всё ещё не понятно? – пиши вопросы на ящик email
Пишем первую программу (Linux)