Зачем в си указывать тип аргумента (%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 шт):

Автор решения: GamerDreemurr_X

Всё просто, %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!

Эти %* нужны для удобного формата строки.

→ Ссылка
Автор решения: maestro

Кратко: функция printf внутри не знает типы передаваемых параметров.

Подробно:

printf - это функция с переменным количеством параметров. Особенность таких функций как раз в том, что ни тип, ни количество параметров не определены. В случае с функцией printf определён только первый аргумент:

int printf(const char *format, ...);

Как видите, первый аргумент имеет тип const char*. Типы остальных аргументов не приведены.

Функции с переменным количеством параметров перебирают параметры с помощью функций/макросов va_start, va_arg. Параметр макроса va_arg - это тип очередной переменной. То есть для того, чтобы извлечь безымянный параметр функции, нужно знать его тип. А как его узнать?

В функции printf для этого как раз и существует формат. По строке формата определяют как количество параметров (количество знаков процента), так и их типы (символы, указанные после знаков процента).

→ Ссылка
Автор решения: Serge3leo

Тому есть несколько причин. Во-первых, (%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.

→ Ссылка
Автор решения: Mark Shevchenko

Чтобы ответить на вопрос, надо сделать небольшой исторический экскурс.

Конец 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 позиций. Конечно никаких форматов при вызове обычных процедур у нас нет.

С определённой точки зрения можно сказать, что такой подход усложняет и язык, и компилятор. У вас есть обычные процедуры и функции, и есть особенные, похожие на обычные, но на самом деле другие. С другой стороны, в данном случае компилятор действительно знает тип переменных и использует это знание для того, чтобы правильно их выводить и форматировать.

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

  1. Как передавать в функцию переменное число параметров и уметь их обрабатывать.
  2. Как изменять параметры форматирования.

Ответ на первый вопрос достаточно сложный. Обычной практикой является передача параметров через стек. Вызывающая процедура записывает параметры в стек и передаёт управление вызываемой процедуре. Та точно знает число и тип своих параметров, поэтому может обращаться к ним. Она знает, где они находятся относительно вершины стека. По завершении работы, процедура удаляет из стека свои параметры, потому что знает, сколько байт надо удалить.

Но как только вы пытаетесь передать в стек переменное число параметров, у вас возникают проблемы. Вы не знаете, где находится первый параметр, потому что у вас в стеке их может быть 10, а может быть 30. Вы не знаете, сколько байтов надо удалять из стека.

Чтобы вызов таких функций работал, создателям Си пришлось пойти на некоторые ухищрения. Во-первых, в Си переменные записываются в стек в обратном порядке — от последнего к первому. При вызове на вершине стека окажется самый первый параметр.

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

Остаётся главный вопрос: как именно вызываемая функция разберётся, что находится в стеке за первым параметром? У неё нет информации о типах и размере параметров. Ответ в данном случае простой — технический трюк.

Тип и количество параметров мы указываем в строке форматирования. Адрес строки форматирования (первый параметр) известен, потому что он попал в стек последним и находится по фиксированному смещению относительно вершины стека. Размер указателя также известен, так что мы можем вычислить, где в стеке начинается второй параметр.

Дальше идёт магия.

Функция printf бежит по строке форматирования и выводит любой символ, пока не встретит символ процента. Далее она смотрит на него и понимает тип и размер следующего параметра. Она выводит его из стека и переводит указатель в стеке дальше.

%d означает целое число, обычно 32 бита или 4 байта. Функция извлекает 4 байта из стека, выводит их в десятичном виде, сдвигая указатель в стеке на 4 байта.

%f — число с плавающей точкой (double) размером 8 байт. Компилятор при передаче параметров преобразует float в double, как отметили в комментарии. Функция выводит 8 байт как число с плавающей точкой и сдвигает указатель на 8 байт.

%s означает, что параметр — указатель на начало строки, который занимает 4 байта в 32-битном приложении и 8 байт в 64-битном. Функция интерпретирует значение, как адрес и выводит строку, которая находится по этому адресу, а затем сдвигает указатель в стеке на 4 или 8 байт.

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

Такие ошибки неприятны, а самое обидное, компилятор не может их обнаружить, по крайней мере, до тех пор, пока не начнёт особым образом обрабатывать вызовы printf и scanf.

Корректность программ важнее, так что сейчас компиляторы следят за соответствием параметров строке форматирования.

→ Ссылка