Зачем в си указывать тип аргумента (%d %s %c и т.д.), если он заранее определён?
Зачем мне писать это, если например тип аргументов заранее определён? Ну или у строки он может определяется в процессе, потому что всё равно всё, что в "" - это строка. А с символами это вообще бред какой-то, мне не понятный.
Например:
int myAge = 15;
char myName[] = "Andrew";
printf("Hello, my name is %s, and my age is %d!", myName, myAge);
Здесь же очевидно указано, что myName это строка, а myAge это int. Не понимаю...
Ответы (4 шт):
Всё просто, %d - это цифра или число, %s - это строка, %c - это символ. Тоесть:
#include <stdio.h>
int main() {
int myAge = 15;
char myName[] = "Andrew";
printf("Hello, my name is %s, and my age is %d!", myName, myAge);
}
Это выведет: Hello, my name is Andrew, and my age is 15!
Эти %* нужны для удобного формата строки.
Кратко: функция printf внутри не знает типы передаваемых параметров.
Подробно:
printf - это функция с переменным количеством параметров. Особенность таких функций как раз в том, что ни тип, ни количество параметров не определены. В случае с функцией printf определён только первый аргумент:
int printf(const char *format, ...);
Как видите, первый аргумент имеет тип const char*. Типы остальных аргументов не приведены.
Функции с переменным количеством параметров перебирают параметры с помощью функций/макросов va_start, va_arg. Параметр макроса va_arg - это тип очередной переменной. То есть для того, чтобы извлечь безымянный параметр функции, нужно знать его тип. А как его узнать?
В функции printf для этого как раз и существует формат. По строке формата определяют как количество параметров (количество знаков процента), так и их типы (символы, указанные после знаков процента).
Тому есть несколько причин. Во-первых, (%d %s %c и т.д.), это не только тип аргумента, а желаемое представление.
Во-вторых, если в прототипе не указан тип аргумента, а он не указан, указано ..., то при вызове применяется "default argument promotion", т.е. происходит преобразование всего, что только можно, к соответствующему целому типу (integer promotion) и преобразование (promotion) к double.
В частности:
%dи%cполучаютint(charпередаётся в функцию, какint), но интерпретируют его различным образом;%sи%pполучают указатель, но интерпретируют его по-разному.
Так исторически сложилось в языке C (но это уже не "зачем", а "почему", это другая долгая интересная история).
P.S. Да, этот самый "integer promotion", происходит во многих выражениях языка C/C++. Его надо знать!
P.P.S.
Компиляторы, часто, поддерживают расширения, типа, __attribute__((__format__ (__printf__, m, n))) или _Printf_format_string_ fmt, что позволяет им ругаться на ошибки применения формата при вызове функций семейства printf.
Чтобы ответить на вопрос, надо сделать небольшой исторический экскурс.
Конец 60-х годов XX века. Время разработки большого количества языков программирования, ставших впоследствии популярными.
Чуть раньше, чем Си, появился язык Паскаль. Вывод на экран в Паскале делается с помощью как бы процедуры WriteLn:
var
age: Integer;
name: String;
begin
WriteLn('Hello, my name is ', name, ', my age is ', age, '!');
end.
Вызов WriteLn похож на вызов процедуры, но в действительности это конструкция языка, потому что у обычных процедур не может быть переменного числа параметров. Чтобы «вызов» WriteLn работал, компилятор языка распознаёт эту конструкцию и обрабатывает её особенным образом.
Помимо прочего, вы можете указывать формат вывода, например, написать age:8, что означает, что число age надо дополнить пробелами так, чтобы оно занимало 8 позиций. Конечно никаких форматов при вызове обычных процедур у нас нет.
С определённой точки зрения можно сказать, что такой подход усложняет и язык, и компилятор. У вас есть обычные процедуры и функции, и есть особенные, похожие на обычные, но на самом деле другие. С другой стороны, в данном случае компилятор действительно знает тип переменных и использует это знание для того, чтобы правильно их выводить и форматировать.
Создатели языка Си пошли по другому пути. Они решили, что в языке не будет особых конструкций, а весь ввод и вывод будет делаться обычными функциями. Для этого им пришлось придумать две вещи:
- Как передавать в функцию переменное число параметров и уметь их обрабатывать.
- Как изменять параметры форматирования.
Ответ на первый вопрос достаточно сложный. Обычной практикой является передача параметров через стек. Вызывающая процедура записывает параметры в стек и передаёт управление вызываемой процедуре. Та точно знает число и тип своих параметров, поэтому может обращаться к ним. Она знает, где они находятся относительно вершины стека. По завершении работы, процедура удаляет из стека свои параметры, потому что знает, сколько байт надо удалить.
Но как только вы пытаетесь передать в стек переменное число параметров, у вас возникают проблемы. Вы не знаете, где находится первый параметр, потому что у вас в стеке их может быть 10, а может быть 30. Вы не знаете, сколько байтов надо удалять из стека.
Чтобы вызов таких функций работал, создателям Си пришлось пойти на некоторые ухищрения. Во-первых, в Си переменные записываются в стек в обратном порядке — от последнего к первому. При вызове на вершине стека окажется самый первый параметр.
Во-вторых, вызывающая функция знает, сколько параметров она положила в стек, поэтому удаляет их из стека тоже она.
Остаётся главный вопрос: как именно вызываемая функция разберётся, что находится в стеке за первым параметром? У неё нет информации о типах и размере параметров. Ответ в данном случае простой — технический трюк.
Тип и количество параметров мы указываем в строке форматирования. Адрес строки форматирования (первый параметр) известен, потому что он попал в стек последним и находится по фиксированному смещению относительно вершины стека. Размер указателя также известен, так что мы можем вычислить, где в стеке начинается второй параметр.
Дальше идёт магия.
Функция printf бежит по строке форматирования и выводит любой символ, пока не встретит символ процента. Далее она смотрит на него и понимает тип и размер следующего параметра. Она выводит его из стека и переводит указатель в стеке дальше.
%d означает целое число, обычно 32 бита или 4 байта. Функция извлекает 4 байта из стека, выводит их в десятичном виде, сдвигая указатель в стеке на 4 байта.
%f — число с плавающей точкой (double) размером 8 байт. Компилятор при передаче параметров преобразует float в double, как отметили в комментарии. Функция выводит 8 байт как число с плавающей точкой и сдвигает указатель на 8 байт.
%s означает, что параметр — указатель на начало строки, который занимает 4 байта в 32-битном приложении и 8 байт в 64-битном. Функция интерпретирует значение, как адрес и выводит строку, которая находится по этому адресу, а затем сдвигает указатель в стеке на 4 или 8 байт.
Вы можете ошибиться, передав параметр одного типа и размера, и записав в строке форматирования что-то неподходящее. Последствия могут быть разными: от не очень опасного некорректного вывода до краха приложения, потому что оно пытается показать строку, находящуюся по неправильному адресу.
Такие ошибки неприятны, а самое обидное, компилятор не может их обнаружить, по крайней мере, до тех пор, пока не начнёт особым образом обрабатывать вызовы printf и scanf.
Корректность программ важнее, так что сейчас компиляторы следят за соответствием параметров строке форматирования.