§5 Целые типы. Арифметические операции с целыми типами

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

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

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

Общие сведения о целых типах

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

  • signed
  • unsigned
Для определения размера (или ширины) используются модификаторы:
  • short
  • long
  • long long
Сочетание модификаторов определяет спецификатор целочисленного типа.

Спецификаторы целых типов
Size MIN MAX
short
2 -32768 32767
unsigned short
2 0 65535
int
4 -2147483648 2147483647
unsigned int
4 0 4294967295
long
8 -9223372036854775808 9223372036854775807
unsigned long
8 0 18446744073709551615
long long
8 -9223372036854775808 9223372036854775807
unsigned long long
8 0 18446744073709551615

Типы определяют лишь небольшое множество соответствующих значений из бесконечной совокупности целых чисел. Чем больше количество различных значений, которые могут принимать переменные данного типа, тем шире используемый тип. Модификатор signed используется по умолчанию, поэтому его можно опустить (т. е. int на самом деле signed int). Спецификатор int также можно опустить во всех типах, в которых используется один из модификаторов определяющих знак или ширину. Поэтому, на практике, сокращение int используется только для типа signed int.

Примечание. Для вывода таблицы размеров максимального и минимального значений целых типов использовался заголовок limits и операция sizeof(). Константы numeric_limits<type>::min() и numeric_limits<type>::max() хранят максимальные и минимальные значения соответствующих типов.

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

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

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

size_t

В стандартной библиотеке (не виден вне std) определен еще один беззнаковый целочисленный тип - std::size_t. Объект этого типа может хранить максимально возможный размер объекта любого типа (включая массив). size_t обычно используется для индексации массива и для подсчета в циклах. Тип size_t возвращают операции:

  • sizeof
  • sizeof ...
  • alignof

функция стандартной библиотеки std::size() и другие функции.

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

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

auto c = -20 * 300000000 / 7, d = 150 * с;

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

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

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

Арифметические операции с целыми типами

В C++ используются операции, которые реализуют все основные арифметические действия: сложение, вычитание, умножение, деление (для целых и действительных типов), а также взятие остатка отделения (только для целых типов). Эти операции являются бинарными, т. е. используют два операнда. Как мы уже говорили на прошлом уроке, операции могут иметь приоритет, иначе говоря - старшинство. Операции *, /, % имеют более высокий (и при этом равный) приоритет, чем операции + и -. Для изменения порядка вычисления используются ().
Любое арифметическое выражение (математическая формула) должно быть представлено в линейной нотации с помощью функций и операций языка C++. Приведем пример. Дана формула нахождения одного из двух корней квадратного уравнения:
 x_1=\frac{-b+\sqrt{b^2-4ac}}{2a}

Представить эту формулу в линейной нотации можно так

x1 = (-b + sqrt(b * b - 4 * a * c)) / (2 * a) // или так
x1 = (-b + sqrt(b * b - 4 * a * c)) / 2 / a

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

int a, b;
a = b = 5;
cout << "a = "   << a
     << "\nb = " << b
     << endl;
Примечание. Инициализация и присваивание - разные вещи; в инструкции определения так сделать не получится. Это ошибка: int a = b = 5

Арифметические операции, наоборот, обладают ассоциативностью слева.
Скажем, нам нужно получить произведение цифр трехзначного числа 236 (см. программу 3.2):
n % 10 * n / 10 % 10 * n / 100
В этом выражении используются операции с равным приоритетом. Если мы не будем использовать скобки, то, при равном приоритете всех операций, выражение будет вычисляться в следующем порядке:

n % 10 => 6
n % 10 * n => 1416
n % 10 * n / 10 => 141
n % 10 * n / 10 % 10 => 1
n % 10 * n / 10 % 10 * n => 236
n % 10 * n / 10 % 10 * n / 100 => 2

При выводе даст нам неверный ответ - 2, а это не то, что мы ожидали увидеть! Здесь без скобок не обойтись. В скобки должно заключаться каждое выражение вычисляющее разряд:
(n % 10) * (n / 10 % 10) * (n / 100)

Постфиксный и префиксный инкремент и декремент

В случае, если аргумент приращения в операциях сложения и вычитания равен 1, используются унарные операции инкремента и декремента. Операции инкремента и декремента имеют две формы: постфиксную и префиксную:

Операции инкремента и декремента
Постфиксный (суффиксальный) Префексный
Инкремент
a++ ++a
Декремент
a-- --a

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

Программа cpp-5.1
#include <iostream>
using namespace std;

int main() {
	int a = 5;
	cout << a++ << "\n"
	     << a   << "\n"
	     << ++a << endl;
	return 0;
}

На выводе:

5
6
7

Сначала выводится выражение с постфиксным инкрементом. Будет выведено первоначальное значение переменной (5), а затем происходит приращение и в следующей строке выводится уже измененное значение (5+1). В третьей строке вывода - сначала происходит приращение (6+1, префиксный инкремент), а затем вывод измененного значения (7).
Операции декремента --a и a-- эквивалентны присваиваниям a = a - 1 или a -= 1 . Аналогично операции инкремента ++a и a++ эквивалентны присваиваниям a = a + 1 или a += 1 . Однако, поскольку операции инкремента и декремента применяются довольно часто их реализация отличается от операций с присваиванием, что позволяет им работать быстрее аналогичных операций с присваиванием.
Унарные операции инкремента и декремента обладают высшим приоритетом. Они могут применяться в различных выражениях, но делать это нужно чрезвычайно осторожно, поскольку очередность выполнения операций при большой сложности выражения становится не очевидной. Типичное применение этих операций - счетчики циклов.

Операции инкркмента и декремента нельзя применять к объекту типа bool, но можно перегружать в применении к абстрактным типам.
Операция взятия остатка от деления "%"

Для целых типов предусмотрены два вида операций деления:

  1. / - целочисленное деление с отбрасыванием дробной части и
  2. % - получение остатка от деления

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

Программа cpp-5.2
#include <iostream>
using namespace std;

int main() {
	int n;
	cout << "n = "; cin >> n;
	cout << "Сумма цифр числа = "
		 << n % 10 + (n / 10) % 10 + n / 100
		 << "\nПроизведение цифр = "
		 << (n % 10) * ((n / 10) % 10) * (n / 100)
		 << endl;
	return 0;
}

Результат в консоли:

n = 236
Сумма цифр числа = 11
Произведение цифр = 36

Однако, операция взятия остатка от деления, с точки зрения математики, не совсем корректна. Это иллюстрирует следующий пример:

Программа cpp-5.3
#include <iostream>
using namespace std;

int main() {
	int a = 7, b = 2, c = -7, d = -2;
	cout << a % b << "\n"
		 << a % d << "\n"
		 << c % d << "\n"
		 << c % b << endl;
	return 0;
}

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

1
1
-1
-1

Как видно из примера остаток может быть отрицательным, когда делимое - отрицательное число.
Часто операции целочисленного деления используются в задачах на определение долей времени. Рассмотрим одну из таких задач. Дано время в виде целого числа секунд (3600 < t < 1000000000000000000). Определить количество прошедших полных суток и количество часов минут и секунд прошедших с начала последних суток.

Программа cpp-5.4
#include <iostream>
using namespace std;

int main() {
	cout << "Введите время в секундах\n"
			"(3600 < t < 1000000000000000000):\n";
	unsigned long t;
	cin >> t;
	unsigned long d = t / 3600 / 24;
	int h = t / 3600 % 24;
	int m = t % 3600 / 60;
	int s = t % 3600 % 60;
	cout << "Дней = " << d
		 << "\nЧасов = " << h
		 << "\nМинут = " << m
		 << "\nСекунд = " << s
		 << endl;
	return 0;
}
Введите время в секундах
(3600 < t < 10000000000000000000):
123456
Дней = 1
Часов = 10
Минут = 17
Секунд = 36
Побитовые операции

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

  • Побитовое НЕ – ~
  • Это унарная операция, действие которой эквивалентно применению логического отрицания к каждому биту двоичного представления операнда. Другими словами, на той позиции, где в двоичном представлении операнда был 0, в результате будет 1, и, наоборот, где была 1, там будет 0.

    int a = 0b00000000000000000000000000101011; // 43
    int b = 0b11111111111111111111111111010100; // -44
    cout << a << endl;
    cout << ~a << endl;
    cout << b << endl;
    
  • Побитовое И - &
  • Это бинарная операция, действие которой эквивалентно применению логического «И» к каждой паре битов, которые стоят на одинаковых позициях в двоичных представлениях операндов. Другими словами, если оба соответствующих бита операндов равны 1, результирующий двоичный разряд равен 1; если же хотя бы один бит из пары равен 0, результирующий двоичный разряд равен 0.

    int a = 0b00000000000000000000000000101011; // 43
    int b = 0b00000000000000000000000000110111; // 55
    // res    00000000000000000000000000100011  => 35
    cout << (a & b) << endl;
    
  • Побитовое ИЛИ - |
  • Это бинарная операция, действие которой эквивалентно применению логического «ИЛИ» к каждой паре битов, которые стоят на одинаковых позициях в двоичных представлениях операндов. Другими словами, если оба соответствующих бита операндов равны 0, двоичный разряд результата равен 0; если же хотя бы один бит из пары равен 1, двоичный разряд результата равен 1.

    int a = 0b00000000000000000000000000101011; // 43
    int b = 0b00000000000000000000000000110111; // 55
    // res    00000000000000000000000000111111  => 63
    cout << (a | b) << endl;
    
  • Побитовое Исключающее ИЛИ (XOR) - ^
  • Это бинарная операция, результат действия которой равен 1, если число складываемых единичных битов нечётно, и равен 0, если чётно. Другими словами, если оба соответствующих бита операндов равны между собой, двоичный разряд результата равен 0; в противном случае, двоичный разряд результата равен 1.

    int a = 0b00000000000000000000000000101011; // 43
    int b = 0b00000000000000000000000000110111; // 55
    // res    00000000000000000000000000011100  => 28
    cout << (a ^ b) << endl;
    

    Данная операция часто применяется для обмена значениями между двумя целыми переменными без использования третьей:

    Программа cpp-5.5
    #include <iostream>
    using namespace std;
    
    int main() {
    	int a = 2;
    	int b = 5;
    	cout << "Было \ta = " << a
    		 << "\n\tb = "    << b
    		 << endl;
    	a = a ^ b;
    	b = a ^ b;
    	a = a ^ b;
    	cout << "Стало \ta = " << a
    		 << "\n\tb = "     << b
    		 << endl;
        return 0;
    }
    
  • Побитовые сдвиги влево - << и вправо - >>
  • Операции сдвига << и >> сдвигают биты в переменной влево или вправо на указанное число. При этом на освободившиеся позиции устанавливаются нули (кроме сдвига вправо отрицательного числа, в этом случае на свободные позиции устанавливаются единицы, так как числа представляются в двоичном дополнительном коде и необходимо поддерживать знаковый бит). Сдвиг влево на 1 может применяться для быстрого умножения числа на два, сдвиг вправо — для деления на два.

    int a = 0b00000000000000000000000000000001; // 1
    // res    00000000000000000000000000010000  => 16
    int b = 0b00000000000000000000001000000000; // 512
    // res    00000000000000000000000000100000  => 32
    cout << (a << 4) << endl;
    cout << (b >> 4) << endl;
    
Сокращенная форма присваивания

Разработчики языка C++ стремились сделать кодинг (верстку программного кода) более лаконичным и компактным. Еще одним способом сократить запись выражений является сокращенная форма присваивания. Используется она тогда, когда значение данной переменной с помощью арифметических операций изменяется значением другой переменной или более сложным выражением:

Полная форма    Краткая форма
a = a + b       a += b
a = a - b       a -= b
a = a * b       a *= b
a = a / b       a /= b
a = a % b       a %= b
a = a << b      a <<= b
a = a >> b      a >>= b
a = a & b       a &= b
a = a ^ b       a ^= b
a = a | b       a |= b

Пробелы между операцией "=" и арифметической операцией не допускаются.

Примеры решения задач
Темы сообщений
Вопросы
  1. В чем отличие префиксной формы инкремента и декремента от постфиксной?
  2. Что такое приоритет и ассоциативность?
  3. Когда появляется необходимость использовать скобки в выражениях?
  4. Когда можно применить сокращенную форму присваивания?
  5. В чем отличие явного преобразования типа от неявного?
  6. Какая форма явного преобразования типа используется в С++?
  7. Приведите примеры использования знаковых и беззнаковых типов.
  8. Перечислите формы записи вещественных чисел.
  9. Почему в процессе вычислений с вещественными числами падает точность вычислений?
  10. Какие действительные типы вам известны? Какой тип наиболее точный при вычислениях?
  11. Какой тип данных мы получим если целое число разделим на вещественное? Вещественное на целое?
  12. Когда нужно выбирать для вычислений целый, а когда действительный тип?
Задания А
Задания Б
Задания С
1. Дано трехзначное число n. Получить новое значение n образованное прочтением исходного числа справа налево (то есть поменять разряды числа. Например 123 -> 321).
2. Дни недели пронумерованы следующим образом: 0 — воскресенье, 1 — понедельник, 2 — вторник, …, 6 — суббота. Дано целое число K, лежащее в диапазоне 1–365. Определить номер дня недели для K-го дня года, если известно, что в этом году 1 января было понедельником. (См. ссылку).
3. Составить программу. Даны длины сторон треугольника a, b, c. Найти длины высот. Длина высоты вычисляется по формуле:
 h_a = \frac{2S}{a} , где S – площадь треугольника (вычисляется по формуле Герона).
4. Составить программу. По заданным величинам радиусов оснований r и R и высоты h найти объем и площадь поверхности усеченного конуса.
1 Звезда2 Звезды3 Звезды4 Звезды5 Звезд (Пока оценок нет)
Загрузка...

Если вы нашли ошибку, пожалуйста, выделите фрагмент текста и нажмите Ctrl+Enter.

Print Friendly, PDF & Email

Обсуждение закрыто.