§4 Фундаментальные типы данных. Переменные. Константы. Преобразование типов

Фундаментальные типы данных

Любая программа работает с данными. Тип данных указывает как организована эта информация в памяти компьютера и какие операции можно производить с этим типом данных. Язык C++ является типизированным языком программирования. Это означает, что тип данных должен быть описан прежде использования программного объекта, такого как переменная, функция, массив, структура или класс. C++, помимо фундаментальных типов, содержит составные типы и поддерживает пользовательские типы. Стандартная библиотека определяет абстрактные типы: string, vector и др. В продолжении данного курса мы будем постепенно знакомиться со всеми разновидностями типов. Здесь же мы начнем рассмотрение фундаментальных (или базовых) типов. К фундаментальным типам относятся:

  • Целые типы
  • Действительные типы
  • Логический тип
  • Символьный тип
  • Специальный тип void

Целые, действительные, логический и символьные типы относятся к арифметическому типу. Тип void используется с функциями и указывает, что функция (процедура) не возвращает какого-либо значения. Void является базовым типом для указателей, а также используется в приведении типов.

Арифметические типы

Целые типы

Базовым целым типом в C++ является тип int. С помощью спецификаторов типа можно получить всё разнообразие целых типов (перечисленных в таблице ниже). Спецификаторы типа могут определять знак и/или размер. Знак определяют спецификаторы:

  • signed
  • unsigned

Для определения размера (или ширины) используются спецификаторы:

  • short
  • long
  • long long

Типы определяют лишь небольшое множество соответствующих значений из бесконечной совокупности целых чисел. Чем больше количество различных значений, которые могут принимать переменные данного типа, тем шире используемый тип. Спецификатор signed используется по умолчанию, поэтому его можно опустить (т. е. int на самом деле signed int). Спецификатор int также можно опустить во всех типах, в которых используется один из спецификаторов определяющих знак или ширину. Поэтому, на практике, сокращение int используется только для типа signed int.
Для вывода таблицы размеров максимального и минимального значений целых типов воспользуемся заголовком limits и операцией sizeof(). Константы numeric_limits<type>::min() и numeric_limits<type>::max() имеют максимальные и минимальные значения соответствующих типов.
Программа 4.1

Исходный код

Вывод программы:

Types               Size                   MIN                   MAX
short                  2                -32768                 32767
unsigned short         2                     0                 65535
int                    4           -2147483648            2147483647
unsigned int           4                     0            4294967295
long                   4           -2147483648            2147483647
unsigned long          4                     0            4294967295
long long              8  -9223372036854775808   9223372036854775807
unsigned long long     8                     0  18446744073709551615

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

1 == sizeof(char) <= sizeof(short) <= sizeof(int) <= sizeof(long) <= sizeof(long long)

Размер конкретного типа будет зависеть от платформы и реализации C++. Компилятор GCC не делает разницы между int и long (а также unsigned int и unsigned long), но, для переносимости программ, это соответствие целых типов следует учитывать.
При таком обилии целых типов перед разработчиком возникает вопрос: какой из этих типов выбрать для обработки целых чисел? Тип int имеет размер, который равен машинному слову. Поэтому работа с этим типом будет наиболее эффективна.
Примечание: целочисленным константам определяется тип в соответствии с их видом, для указания конкретного типа необходимо использовать суффиксы (см. предыдущий урок).

Переполнение типа

Если величина значения переменной становится больше допустимого максимально возможного значения для данного типа, происходит переполнение. При этом компилятор не сообщит об ошибке. Следовательно, при арифметических вычислениях необходимо предусмотреть преобразования типа к более широкому типу (см. ниже). В программе 4.2 переменные инициализированы максимальным значением для знаковой и беззнаковой разновидности типа int. На выводе осуществляется попытка увеличить значение этих переменных на единицу:
Программа 4.2

#include <iostream>
using namespace std;

int main() {
	int a = 2147483647;
	unsigned b = 4294967295;
	cout << a + 1 << "\n"
		 << b + 1 << endl;
	return 0;
}

В результате мы получим следующий вывод:

-2147483648
0

Объясняется этот результат способом представления целых чисел со знаком и без знака в памяти компьютера (Повторение: см. п. 27, 10 кл., в 1-ом томе учебника). На схемах ниже показано как будут изменяться значения для переменных обоих типов с одинаковой шириной (long) после переполнения:
int-sig
int-uns
Существуют несколько способов контроля переполнения не являющихся частью стандарта. В реализации для GCC имеются специальные функции осуществляющие контроль переполнения на этапе выполнения операций сложения, вычитания и умножения для всех целых базовых типов.
Приведем пример программы для операции сложения.
Программа 4.3

#include <iostream>
#include <limits>
using namespace std;

int main() {
	int x = numeric_limits<int>::max() - 10;
	for (int i = 0; i < 10; i++) {
		int res;
		if (__builtin_add_overflow(x, i, &res)) {
			cout << "Произошло переполнение!" << endl;
		} else {
			x = res;
			cout << x << endl;
		}
	}
	return 0;
}
2147483637
2147483638
2147483640
2147483643
2147483647
Произошло переполнение!
Произошло переполнение!
Произошло переполнение!
Произошло переполнение!
Произошло переполнение!

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

Действительные типы

Действительные (вещественные) типы или типы с плавающей точкой представлены типами одинарной, двойной и расширенной точности. Построим аналогичную таблицу для действительных типов.
Программа 4.4

Исходный код

Вывод программы:

Types        Size                           MIN                           MAX
float           4                 1.1754944e-38                 3.4028235e+38
double          8        2.225073858507201e-308        1.797693134862316e+308
long double    12   3.3621031431120935063e-4932    1.189731495357231765e+4932

Представление вещественных чисел существенно отличается от представления целых чисел. Вещественное число представлено в памяти в виде двух частей: мантиссы и порядка. Мантисса - это число >= 1, но < 2, поэтому старшая цифра мантиссы, равная 1, никогда не хранится. Например, для хранения числа типа float выделяется 32 бита, из них: 1 разряд выделяется для хранения знака мантиссы, 8 разрядов для хранения порядка числа и 23 разряда для хранения мантиссы (повторение §29).
Размеры мантиссы (дробной части) чисел действительных типов float, double и long double, в количестве десятичных разрядов, соответственно равны: 7, 15 и 19. Это предел точности этих типов, сверх которой, будет накапливаться ошибка. Отсюда следует, что наибольшей точностью вычислений обладает тип long double.
Науке известны как слишком огромные величины, так и слишком маленькие: Объем Метагалактики  3,5\cdot10^{30} m^3 , радиус протона  0,8768\cdot10^{-15} m , масса электрона  9.10938291\cdot10^{-31} kg , масса Солнца 1.98892\cdot10^{30} kg , а Галактики, и того больше - 6\cdot10^{42} kg!! Очевидно, что производя вычисления с подобными числами мы будем терять точность вычислений, так как в мантиссе представлено лишь небольшое число десятичных разрядов по сравнению с порядком. Если необходимо соблюсти точность вычислений при работе с целыми числами, то не нужно использовать для этих целей любой из вещественных типов! Необходимо выбирать подходящий по размеру целый тип. Если же размер числа больше любого целого типа, то прибегать к программированию больших чисел с помощью алгоритмов длинной арифметики с массивами.
Заметим, что для точных вычислений с большими числами (как с целыми, так и с действительными) для языка С++ существуют несколько свободных библиотек, например GMP и MPIR.
Примечание: по умолчанию вещественные константы имеют тип double, для указания иного типа используются суффиксы.
Вещественные типы могут быть представлены следующим способом:

1.91315
.5110034
-2.
3E8
6.6720e-11

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

Переменные

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

Определение переменных

Простое определение (definition) переменных одного типа выглядит следующим образом:

спецификатор типа var1, var2, ..., varN;

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

type1 var1;
type2 var2;
...;
typeN varN;

Например:

int a, b, c, d;
float w;
char Ch;
string myStr;
long long z;
Инициализация

Инициализация (initialization) - это процесс присваивания определенной переменной конкретного значения в момент её создания.
Например:

int a = 0, b = 1, c = 200, d = b;
int q = 5, 
float w(6.2);
char Ch = '@';
string t("Инициализация строки с помощью скобок");
double y {.103e-9}; // списочная инициализация

Инициализацией может быть сложным выражением в котором используются арифметические операции и функции.

double c = hypot(a, b);
int k = (m + t) / 2;

Переменная может быть инициализирована значением другой переменной, если та, в свою очередь, проинициализирована в этом же выражении, но раньше. Инициализация может быть выполнена с помощью круглых и фигурных скобок. Последний способ называется списочной инициализацией и появился в стандарте C++11. Списочная инициализация обладает преимуществом перед другими формами, поскольку компилятор проверяет на сужение типа данных и если такое произойдет, то в инициализации будет отказано.
Обратите внимание, что в одном выражении могут определяться переменные одного типа как с инициализацией, так и без таковой:

int a = 2, b, c = 20, d;

Параметры в функциях инициализируются аргументами.

Инициализация по умолчанию

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

Константы

Если перед именем переменной используется ключевое слово const - то это означает, что описана константа. Const называется квалификатором (qualifier), поскольку уточняет, что означает это объявление. Константа инициализируется значением в момент описания. Попытка присвоить ей новое значение приведет к ошибке. Существует практика давать имена константам с заглавной буквы. Квалификатор const принято использовать до спецификатора типа:

[квалификатор const][спецификатор типа][Name_const] = [значение];

Например:

const int Buff = 255;
const double Alpha = 0.0072973506;

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

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

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

  • Неявные преобразования
  • Явное преобразование

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

Неявные преобразования

К неявным преобразованиям (implicit conversion) относят:

  1. Преобразования при инициализации и присваивании
  2. Преобразования при передаче аргументов
  3. Арифметические преобразования (arithmetic conversion)

Преобразования при инициализации и присваивании
C++ позволяет присваивать значения переменных одного типа, переменным другого типа. Даже не взирая на предупреждения компилятора (в некоторых случаях) преобразования будут произведены и программа завершится успешно, но, возможно, с неожиданным для вас результатом. (Программа 4.3). Правила преобразований:

  • Если перемененной целочисленного типа с большим диапазоном присваивается значение типа с меньшим, то значение не будет изменено. Аналогично для действительных типов.
  • Если переменной float присваивается тип long, то значение усекается, а точность теряется.
  • Если переменной int присваивается значение переменной double, то результат: либо усечение дробной части, либо неопределенный.
  • Если целой переменной знакового типа присваивается значение не из диапазона результат неопределен.
  • Если переменной беззнакового типа присваивается значение не из диапазона, то результатом будет остаток от деления по модулю значения, которое может содержать тип назначения.

Отсюда вытекает правило:

  1. чтобы получить предсказуемый результат (без потери точности) необходимо типу с большим диапазоном присваивать тип с меньшим диапазоном, с учетом знака;
  2. избегать в присваиваниях знаковому типу значение беззнакового типа и наоборот.

Программа 4.5

//============================================================================
// Name        : lesson-4-5.cpp
//============================================================================
#include <iostream>
using namespace std;

int main() {
	cout << "----------------------------------------------\n";
	unsigned short a;
	a = -100000; cout << a << endl;
	a =  70000;  cout << a << endl; // 70000 % 65536;
	cout << "----------------------------------------------\n";
	int b;
	b = 12.123;	 cout << b << endl; // отсекается дробная часть
	b = -0.5e+6; cout << b << endl; // Ok!
	b = -1e+26;  cout << b << endl; // min_int -2147483648
	b = 6e15;    cout << b << endl; // max_int 2147483647
	cout << "----------------------------------------------\n";
	unsigned c;
	c = -2e-20;  cout << c << endl; // 0!
	c = 18446744073709551615; cout << c << endl; // max_unsigned
	c = '\x07\x07'; cout << c << endl; // Ок! (707 - 16-ричное)
	cout << "----------------------------------------------\n";
	float d;
	d = 18446744073709551615; cout << d << endl; // потеря точности
	d = 1.7976931348623e+308; cout << d << endl; // бесконечность
	d = 0B11110001111; cout << d << endl; // Ok! (1935 - двоичное)
	cout << "----------------------------------------------\n";
	return 0;
}

Вывод программы:

----------------------------------------------
31072
4464
----------------------------------------------
12
-500000
-2147483648
2147483647
----------------------------------------------
0
4294967295
1799
----------------------------------------------
1.84467e+19
inf
1935
----------------------------------------------

Арифметические преобразования (arithmetic conversion)
Арифметические преобразования происходят в выражениях с арифметическими операциями в которых операнды имеют разные числовые типы. В арифметических преобразованиях важно знать каков будет итоговый тип данных. Арифметические преобразования выполняются по следующим правилам:

  • В выражениях с младшими целочисленными типами (bool, char и short) итоговый результат преобразуется в int, если результат соответствует диапазону int, иначе преобразуется в unsigned int;
  • если один из операндов имеет тип float, double или long double, то и другой операнд преобразуется в тип float, double или long double, соответственно;
  • если оба операнда имеют одинаковый знак (то есть: либо знакового, либо беззнакового типа), то операнд с младшим типом преобразуется к старшему типу;
  • если операнды в выражении и знакового, и беззнакового типа:
    • если беззнаковый тип операнда старше, чем операнд со знаком, то последний преобразуется в тип беззнакового операнда;
    • иначе, если тип со знаком может представить весь диапазон беззнакового типа, беззнаковый операнд преобразуется к типу операнда со знаком;
    • иначе операнды преобразуются в беззнаковую версию типа со знаком
Явное преобразование

В C++ для явного преобразования типов используется приведение типов (cast) в старом стиле и с помощью операции static_cast.

Приведение типов в старом стиле.

Приведение типов в старом стиле использует две формы:

  1. type(expression) - в стиле C++
  2. (type)expression - в стиле C

где type — тип к которому приводится выражение expression.
И та, и другая форма является устаревшей и небезопасной. Она существует до сих пор по причине совместимости с языком С. В С++ рекомендуется использовать новый формат преобразования типов.

Операция static_cast

Операция static_cast является рекомендуемой формой явного приведения типов в C++. Она позволяет предупреждать некорректную попытку преобразования типа. Формат операции таков:

static_cast<type>(expression)

В программе 4.6 результат операции сложения будет верным, если один из операндов будет преобразован в тип, диапазон которого вместит результат (long long). (Один из способов приведения использовать нельзя!)
Программа 4.6

//============================================================================
// Name        : lesson-4-6.cpp
//============================================================================
#include <iostream>
using namespace std;

int main() {
    long d = 2147483647;
    auto e = 4294967295ul; // см. ниже об auto
    cout << static_cast<long long>(d) + e << endl;
    cout << (long long)d + e << endl;
    // long long(d) + e => ошибка!
    return 0;
}

Спецификатор auto

В некоторых случаях, определение типа результата выражения может стать нетривиальной задачей для разработчика. Стандарт С++11 вводит спецификатор auto, облегчающий решение данной проблемы. В отличие от спецификатора типа, назначающего конкретный тип переменной, спецификатор auto приказывает компилятору вывести тип автоматически, из инициализатора. Отсюда следует, что создавать описание переменной со спецификатором auto без инициализации нельзя. При описании нескольких переменных с использованием спецификатора auto все они должны иметь совместимый тип.
Например:

auto c = 2 * 3.14 / 3, d = 0.15 * с;

Применять auto для описания переменных, тип которых и так очевиден - не имеет смысла, но может сделать код более лаконичным и изящным:

auto r = 1000000ull;
// вместо
// unsigned long long r = 1000000;

Действительно полезное значение auto приобретает при работе с составными типами.

Вопросы и задания
  1. Какого типа ожидается результат?
    • 10 + 10ull
    • 2.4 + 1.5f
    • 5.0 + 6l
    • 17L + 12
    • 10U + 10
  2. Дано описание переменных. Найдите ошибки:
  3. a, int b;
    float k = b;
    double m, s = m, 10;
    unsigned (20), w(20);
    short a1{2}, a2{3}, a3{4};
    signed int c = 10; h = 15; s = 25;
    k = a;
    l = s;
    long long v = 22.433;
    
  4. В чем недостатки вещественных типов?
  5. Почему спецификатор auto не следует применять, когда тип данных инициализатора очевиден?
  6. Почему когда переменной типа float присваивается значение переменной типа long, теряется точность?
  7. Почему используется большое количество целых типов?
  8. Требуется работать с числом превышающим диапазон типа unsigned long long не прибегая к приведению типа. Какие возможности имеются у разработчика?
  9. Приведите примеры неявного преобразования типов приводящих к потере точности.
  10. Дан фрагмент исходного кода:
  11. const float R = 0.5;
    const fact = -5e+20;
    const int = 2000;
    const short a = 1;
    int q;
    double s = 0.0;
    R = s * 3.14;
    s = R - 1;
    q = a++;
    

    Найдите ошибки в программе.

  12. Каким будет тип переменной в каждом из таких описаний?
  13. auto a = '\n';
    auto b = 12.f;
    auto c = 0.6 + 0.7L;
    auto d = .5 + 10U;
    auto e = 10ull + 12.1l;
Учебник

§55 вопросы 6, 7, 8, 9

Презентация к уроку
Литература
  1. Прата, Стивен. Язык программирования C++. Лекции и упражнения, 6-е изд.: Пер. с англ. — М.: ООО «И.Д. Вильяме», 2012
  2. Стенли Липпман, Жози Лажойе, Барбара Му. Язык программирования С++. Вводный курс. — Вильямс: 2007
  3. Павловская Т. А. C/C++. Программирование на языке высокого уровня. - СПб.: Питер, 2003.
  4. Язык C++
  5. C++ для начинающих. Часть II. Основы языка
  6. C++11 — Википедия
  7. C++14 — Википедия


Добавить комментарий