Переменные
Переменные
Переменные используются для хранения значений (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 |
Например, правильные идентификаторы
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 эквивалентны.
| Тип | Размер, байт | Минимальное значение | Максимальное значение |
|---|---|---|---|
| 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 байт.
| Тип | Размер, байт | Количество значащих знаков мантиссы | Минимальное значение | Максимальное значение |
|---|---|---|---|---|
| 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
#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 - мантиса числа, 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) Работает в несколько потоков
Во-первых, глобальная переменная, если она видна всем, может быть изменена любой частью программы. Вы изменили глобальную переменную, хотите её записать, а другая часть программы уже перезаписала в неё другое значение (на самом деле это целый класс проблем, которые возникают в многопоточной среде). Во-вторых, при больших размерах проекта не уследить, кто и когда насоздавал глобальных переменных. В приведённых выше примерах видно, как переменные могут перекрывать друг друга, то же произойдёт и в крупном проекте.
Безусловно, есть ситуации, когда глобальные переменные упрощают программу, но такие ситуации случаются не часто и не в ваших домашних заданиях, так что НЕ СОЗДАВАЙТЕ ГЛОБАЛЬНЫХ ПЕРЕМЕННЫХ!
Переменные могут быть не только целочисленными и с плавающей точкой. Существует множество других типов, которые мы будем изучать в дальнейшем.
