Стандартные математические функции в языке Си
Математические вычисления не ограничиваются лишь арифметическими действиями. Кроме них, можно ещё встретить корни, модули, логарифмы, тригонометрические функции и пр. Научимся же использовать подобные функции в своих программах.
Для использования математических функций нужно подключить заголовочный файл math.h.
Некоторые математические функции:
fabs(x)— модуль числаxsqrt(x)— квадратный корень из числаxsin(x)— синус числаx(хв радианах)cos(x)— косинус числаx(хв радианах)pow(x, y)— вычислениеxyexp(x)— вычислениеexlog(x)— натуральный логарифм числаxlog10(x)— десятичный логарифм числаx
Два важных момента.
Все указанные функции возвращают значение типа
double.В качестве аргументов эти функции принимают вещественные числа типа
double, но можно передать и числа других типов (например,int,float). При этом произойдёт неявное преобразование (приведение) типа. Компилятор из целого числа, например3, сделает число3.0, которое будет иметь типdouble.
Примеры использования математический функций:
Задача 1: Даны длины катетов прямоугольного треугольника. Вычислить длину гипотенузы.
Простая задачка на знание теоремы Пифагора.
Листинг 1.
#include <stdio.h>
#include <math.h> // подключаем math.h
int main (void)
{
int a, b, c2;
scanf("%d", &a);
scanf("%d", &b);
c2 = a*a + b*b;
printf("c = %.2f\n", sqrt(c2)); // так как sqrt возвращает double,
// то используем спецификатор %f
return 0;
}Из интересного в этой программе лишь неявное преобразование типа, которое происходит, когда мы вызываем функцию sqrt. Допустим, мы запустили программу и ввели 3 и 4. В переменную c2 (типа int) будет записано значение 3*3 + 4*4 = 25. Когда мы пишем вызов функции sqrt(c2), то вместо c2, как мы уже знаем, подставляется значение, которое в ней хранится, т.е. 25, получаем: sqrt(25). Но как нам уже известно, функция sqrt ждёт от нас значения типа double, а мы ей передали значение типа int. Поэтому компилятор сначала выполнит неявное приведение типа: преобразует целое значение 25 в вещественное значение 25.0.
Задача 2: Вычислить синус угла, введённого с клавиатуры. Угол вводится в градусах.
Листинг 2.
#include <stdio.h>
#include <math.h> // подключаем math.h
int main (void)
{
double alpha, sin_a, pi = 3.1415926;
scanf("%lf", &alpha);
alpha = alpha * pi / 180;
sin_a = sin(alpha);
printf("%.2f\n", sin_a);
return 0;
}Тригонометрические функции математической библиотеки языка Си работают с радианной мерой угла (угол, заданный в радианах). Людям же привычнее работать с градусной мерой угла (углом, заданным в градусах). Поэтому в данной программе мы предварительно переводим значение из градусов в радианы.
Если этого не сделать, результат получится неправильным. Проверьте это самостоятельно.
Неявное преобразование типов
При явном преобразовании типа мы в скобках перед значением указывали тип, к которому нужно привести данное значение. В неявном преобразовании этого делать не нужно. Компилятор автоматически подберёт необходимый тип.
Неявное преобразование типов осуществляется в следующих случаях:
- перед передачей аргументов в функцию
- выполнение арифметических операций с разными типами аргументов
- перед выполнением присваивания
Неявное преобразование типа при передаче аргумента в функцию мы обсудили выше (вызов функции sqrt в Листинге 1).
Пример неявного преобразования типа из пункта 2 мы встречали в прошлой заметке, когда разбирались с тем, как получить правильный результат деления 7 на 2. Давайте посмотрим на Листинг 3 из прошлого шага:
Листинг 3.
#include <stdio.h>
int main(void)
{
int a = 7, b = 2;
float res;
res = (float) a / b;
printf("%d / %d = %f\n", a, b, res);
return 0;
}Разберём поподробнее, как будет обрабатываться строка res = (float) a / b;.
Сначала подставим значения переменных a и b: res = (float) 7 / 2;
Далее произведём явное приведение типа для значения 7: res = 7.0 / 2;
Т.к. процессоры умеют выполнять арифметические операции только с операндами одного и того же типа, то компилятору нужно произвести неявное приведение типа для значения 2. Поэтому на этом этапе значение 2 преобразуется в значение 2.0. Получаем: res = 7.0 / 2.0;
Получается, что в прошлом уроке мы изучили небольшой программистский "хак", связанный с преобразованием типов. Для правильного выполнения деления мы должны были бы сами явно привести оба числа к типу float, но мы схитрили и явно привели к типу float только одно число, а второе число преобразовал уже компилятор неявно. =)
Правила неявного преобразования типов
Важно!
- если выполняются арифметические операции с разными типами аргументов, Оба аргумента приводятся к большему типу. Старшинство типов:
int<float<double - при вызове функций, переданные аргументы преобразуются к типам данных, которые ожидает функция.
- при присваивании. Значение справа от оператора присваивания приводится к типу переменной слева от оператора присваивания.
Снова обращаю ваше внимание на то, что если при неявном преобразовании типов может произойти потеря точности.
Примеры:
int + floatбудет автоматически преобразовано кfloat + floatfloat / intбудет автоматически преобразовано кfloat / floatdouble * floatбудет преобразовано кdouble * doubleint = doubledoubleбудет преобразовано кintс потерей дробной частиfloat = intintбудет преобразовано кfloat
Учитывая разобранные правила, посмотрим на Листинг 4 из прошлого урока.
Листинг 4.
int a = 7, b;
float g = 9.81, v;
b = (int) g; // приводим значение 9.81 к типу int, получим 9
v = (float) a; // приводим значение 7 к типу float, получим 7.0В этом кусочке кода мы явно преобразовали тип значений справа от оператора присваивания. Но теперь, зная правила неявного преобразования типов, мы понимаем, что этого можно было бы и не делать, т.к. компилятор провёл бы эту процедуру самостоятельно. Т.е. можно было бы просто записать:
b = g; \\ значение 9.81 будет неявно преобразовано к типу int, получим 9
v = a; \\ значение 7 будет неявно преобразовано к типу float, получим 7.0И в конце давайте глянем на несколько усложнённый пример (листинг 5). К какой одной переменной достаточно применить явное приведение типа, чтобы всё выражение было неявно приведено к типу float?
Листинг 5.
int a = 20, b = 8, c = 3, d = 2;
float result;
result = a / (b/c) / d;Чтобы ответить на этот вопрос, достаточно знать правило: чтобы неявное приведение применилось по цепочке ко всем переменным, достаточно явно привести тип переменной, по приоритету операций первой вычисляемой в выражении. Иначе говоря, что будет вычисляться самым первым - то и нужно приводить. В нашем примере это b или c.
Практика
Вычислите диапазон типа
intв вашей системе (минимальное и максимальное число, которое можно сохранить в переменную типаint). Используйте решение задачи и следующие факты:- Количество байт, выделенное под тип
intв вашей системе, можно узнать с помощью оператораsizeof. Он возвращает целое число — количество байт, выделенное под хранение этого типа данных.printf("%d\n", sizeof (int)); 1 байт = 8 бит- Тип
intпо умолчанию является знаковым, а поэтому один бит в нём отводится на хранение знака числа. - Нуль тоже число и его тоже нужно хранить в памяти.
Попробуйте сохранить полученные значения в переменные типа
intи вывести их на экран. О результатах пишите в комментариях к этому уроку.- Количество байт, выделенное под тип
Исследовательские задачи для хакеров
1. Разберитесь, почему следующая программа работает некорректно.
#include <stdio.h>
#include <stdlib.h> // чтобы работала функция abs()
int main(void)
{
int a = -12345;
int b = -2147483647;
int x = -2147483648;
int o = -2147483649;
printf("abs(%d) = %d\n", a, abs(a));
printf("abs(%d) = %d\n", b, abs(b));
printf("abs(%d) = %d\n", x, abs(x));
printf("abs(%d) = %d\n", o, abs(o));
return 0;
}2. Найдите на своём компьютере заголовочный файл math.h и изучите его содержимое. Думаю, что вы удивитесь тому, что в этом файле будут только заголовки функций без описания тела функций (т.е. без описания того, как эти функции работают). Выясните самостоятельно, почему это так и где найти внутреннюю реализацию математических функций.
Дополнительные материалы
1. Изначально math.h создавался для различных математических функций, работающих с числами с плавающей точкой (это способ приближённого представления вещественных чисел в памяти компьютера). Поэтому в заголовочном файле math.h вычисление модуля реализовано только для вещественных чисел. Например, fabs() для чисел типа double или fabsf() для чисел типа float.
Но существует также функция abs(), которая вычисляет модуль для целых чисел, но она объявлена в заголовочном файле stdlib.h.
2. Может показаться, что потеря точности происходит только при преобразовании вещественных чисел в целые, когда отбрасывается дробная часть. Но это не так. Следующий программа демонстрирует, что проблемы могут происходить и при преобразовании целых чисел в вещественные (int в float).
#include <stdio.h>
int main(void)
{
int big_num = 123456789;
float float_num = big_num; // неявно преобразуем int в float ПРОБЛЕМА
int back_to_int = float_num; // неявно преобразуем float в int
printf("source: %d\n", big_num);
// printf("float: %f\n", float_num); // раскомментируйте, чтобы посмотреть на проблему
printf("after conversation: %d\n", back_to_int);
printf("difference: %d\n", big_num - back_to_int);
return 0;
}Поэтому, если у вас где-то происходят преобразования типов (явные или неявные), то внимательно проверяйте получившийся результат вычислений!