pode receber mais de um argumento (valor_batata + valor_cerveja + valor hamburguer)

Não é "mais de um argumento", é um só. Mas vamos por partes.

A função foi definida assim:

float half(float bill, float tax, int tip) {
    // etc...
}

Ou seja, ela possui três parâmetros: o primeiro é um float e o nome é bill, o segundo também é float e o nome é tax, e o terceiro é um int chamado tip.

Agora, de onde vêm esses valores? Isso é algo definido por quem for chamar a função.

Por exemplo, se eu chamo a função assim:

half(100, 6.25, 18);

Repare que os argumentos são separados por vírgula. Ou seja, os três argumentos são os números 100, 6.25 e 18.

E como o primeiro argumento é 100, este é o valor que será colocado em bill.

Agora, se eu chamo a função assim:

half(valor_batata + valor_cervejas + valor_hamburguer, 2.5, 10);

Ainda são três argumentos separados por vírgula. O segundo é 2.5 e o terceiro é 10.

E o primeiro argumento? Ele é toda a expressão valor_batata + valor_cervejas + valor_hamburguer. Não são vários argumentos, é um só. E o resultado desta expressão é o valor que será colocado em bill.

Isso quer dizer que primeiro ele calcula a expressão (no caso, soma os 3 valores), e só depois o resultado é passado para a função. Seria o equivalente a fazer isso:

float total = valor_batata + valor_cervejas + valor_hamburguer;
half(total, 2.5, 10);

Mas neste caso eu optei por não usar a variável total, e em disso passar o resultado da soma diretamente para a função.

Claro, tudo isso assumindo que as variáveis existem e possuem valores adequados. Mas o ponto aqui é que continua sendo um único argumento. Só que não precisa ser necessariamente uma variável: ele pode ser qualquer expressão válida, desde que o resultado seja um valor de um tipo compatível com o que a função espera.

Isso quer dizer que primeiro ele calcula a expressão (no caso, soma os 3 valores), e só depois o resultado é passado para a função.

Sua explicação fez sentido para mim, obrigado.

Agora é só continuar praticando...