'printf' frente a 'cout' en C++
¿ Cuál es la diferencia entre printf()
y cout
en C++?
Me sorprende que todos en esta pregunta afirmen que std::cout
es mucho mejor que printf
, incluso si la pregunta solo pedía diferencias. Ahora, hay una diferencia: std::cout
es C++ y printf
es C (sin embargo, puedes usarlo en C++, como casi cualquier otra cosa de C). Ahora, seré honesto aquí; Ambos printf
y std::cout
tienen sus ventajas.
Diferencias reales
Extensibilidad
std::cout
es extensible. Sé que la gente dirá que eso printf
también es extensible, pero dicha extensión no se menciona en el estándar C (por lo que tendría que usar funciones no estándar, pero ni siquiera existe una característica no estándar común), y dichas extensiones son una letra. (por lo que es fácil entrar en conflicto con un formato ya existente).
A diferencia de printf
, std::cout
depende completamente de la sobrecarga del operador, por lo que no hay problema con los formatos personalizados: todo lo que debe hacer es definir una subrutina tomando std::ostream
como primer argumento y su tipo como segundo. Como tal, no hay problemas de espacio de nombres: siempre que tenga una clase (que no esté limitada a un carácter), puede tener una std::ostream
sobrecarga de trabajo para ella.
Sin embargo, dudo que mucha gente quiera ampliar ostream
(para ser honesto, rara vez vi extensiones de este tipo, incluso si son fáciles de hacer). Sin embargo, está aquí si lo necesita.
Sintaxis
Como se puede observar fácilmente, ambos printf
usan std::cout
una sintaxis diferente. printf
utiliza la sintaxis de función estándar utilizando cadenas de patrones y listas de argumentos de longitud variable. En realidad, printf
es una de las razones por las que C los tiene: printf
los formatos son demasiado complejos para poder utilizarse sin ellos. Sin embargo, std::cout
utiliza una API diferente: la operator <<
API que se devuelve sola.
Generalmente, eso significa que la versión C será más corta, pero en la mayoría de los casos no importará. La diferencia se nota cuando imprimes muchos argumentos. Si tiene que escribir algo como Error 2: File not found.
, asumiendo el número de error, y su descripción es un marcador de posición, el código se vería así. Ambos ejemplos funcionan de manera idéntica (bueno, más o menos, std::endl
en realidad vacía el búfer).
printf("Error %d: %s.\n", id, errors[id]);
std::cout << "Error " << id << ": " << errors[id] << "." << std::endl;
Si bien esto no parece demasiado loco (es solo dos veces más largo), las cosas se vuelven más locas cuando realmente formateas los argumentos, en lugar de simplemente imprimirlos. Por ejemplo, imprimir algo así 0x0424
es una locura. Esto se debe a std::cout
la mezcla de valores de estado y reales. Nunca vi un lenguaje en el que algo así std::setfill
fuera un tipo (que no sea C++, por supuesto). printf
separa claramente los argumentos y el tipo real. Realmente preferiría mantener la printf
versión (incluso si parece un poco críptica) en comparación con iostream
la versión (ya que contiene demasiado ruido).
printf("0x%04x\n", 0x424);
std::cout << "0x" << std::hex << std::setfill('0') << std::setw(4) << 0x424 << std::endl;
Traducción
Aquí es donde printf
reside la verdadera ventaja. La printf
cadena de formato es bueno... una cadena. Eso hace que sea realmente fácil de traducir, en comparación con operator <<
el abuso de iostream
. Suponiendo que la gettext()
función se traduce y desea mostrarla Error 2: File not found.
, el código para obtener la traducción de la cadena de formato mostrada anteriormente se vería así:
printf(gettext("Error %d: %s.\n"), id, errors[id]);
Ahora, supongamos que traducimos a Fictionish, donde el número de error está después de la descripción. La cadena traducida se vería así %2$s oru %1$d.\n
. Ahora bien, ¿cómo hacerlo en C++? Bueno, no tengo idea. Supongo que puedes falsificar iostream
las construcciones printf
a las que puedes pasar gettext
, o algo así, con fines de traducción. Por supuesto, $
no es el estándar C, pero es tan común que, en mi opinión, es seguro usarlo.
No tener que recordar/buscar la sintaxis específica del tipo de entero
C tiene muchos tipos de números enteros, al igual que C++. std::cout
maneja todos los tipos por usted, mientras que printf
requiere una sintaxis específica dependiendo de un tipo entero (hay tipos no enteros, pero el único tipo no entero que usará en la práctica printf
es const char *
(cadena C, se puede obtener usando to_c
el método de std::string
)). Por ejemplo, para imprimir size_t
, necesita usar %zu
, mientras que int64_t
será necesario usar %"PRId64"
. Las tablas están disponibles en http://en.cppreference.com/w/cpp/io/c/fprintf y http://en.cppreference.com/w/cpp/types/integer .
No puedes imprimir el byte NUL,\0
Debido a que printf
utiliza cadenas C en lugar de cadenas C++, no puede imprimir bytes NUL sin trucos específicos. En ciertos casos, es posible utilizar %c
with '\0'
como argumento, aunque es claramente un truco.
Diferencias que a nadie le importan
Actuación
Actualización: Resulta que iostream
es tan lento que generalmente es más lento que su disco duro (si redirige su programa al archivo). Deshabilitar la sincronización con stdio
puede ayudar si necesita generar muchos datos. Si el rendimiento es una preocupación real (en lugar de escribir varias líneas en STDOUT), simplemente use printf
.
Todo el mundo piensa que le importa el rendimiento, pero nadie se molesta en medirlo. Mi respuesta es que la E/S es un cuello de botella de todos modos, sin importar si usas printf
o iostream
. Creo que printf
podría ser más rápido con un vistazo rápido al ensamblaje (compilado con clang usando la -O3
opción del compilador). Suponiendo mi ejemplo de error, printf
el ejemplo realiza muchas menos llamadas que el cout
ejemplo. Esto es int main
con printf
:
main: @ @main
@ BB#0:
push {lr}
ldr r0, .LCPI0_0
ldr r2, .LCPI0_1
mov r1, #2
bl printf
mov r0, #0
pop {lr}
mov pc, lr
.align 2
@ BB#1:
Puede notar fácilmente que dos cadenas y 2
(número) se incluyen como printf
argumentos. Eso es todo; no hay nada más. A modo de comparación, esto se iostream
compila en ensamblador. No, no hay alineación; cada operator <<
llamada significa otra llamada con otro conjunto de argumentos.
main: @ @main
@ BB#0:
push {r4, r5, lr}
ldr r4, .LCPI0_0
ldr r1, .LCPI0_1
mov r2, #6
mov r3, #0
mov r0, r4
bl _ZSt16__ostream_insertIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_PKS3_l
mov r0, r4
mov r1, #2
bl _ZNSolsEi
ldr r1, .LCPI0_2
mov r2, #2
mov r3, #0
mov r4, r0
bl _ZSt16__ostream_insertIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_PKS3_l
ldr r1, .LCPI0_3
mov r0, r4
mov r2, #14
mov r3, #0
bl _ZSt16__ostream_insertIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_PKS3_l
ldr r1, .LCPI0_4
mov r0, r4
mov r2, #1
mov r3, #0
bl _ZSt16__ostream_insertIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_PKS3_l
ldr r0, [r4]
sub r0, r0, #24
ldr r0, [r0]
add r0, r0, r4
ldr r5, [r0, #240]
cmp r5, #0
beq .LBB0_5
@ BB#1: @ %_ZSt13__check_facetISt5ctypeIcEERKT_PS3_.exit
ldrb r0, [r5, #28]
cmp r0, #0
beq .LBB0_3
@ BB#2:
ldrb r0, [r5, #39]
b .LBB0_4
.LBB0_3:
mov r0, r5
bl _ZNKSt5ctypeIcE13_M_widen_initEv
ldr r0, [r5]
mov r1, #10
ldr r2, [r0, #24]
mov r0, r5
mov lr, pc
mov pc, r2
.LBB0_4: @ %_ZNKSt5ctypeIcE5widenEc.exit
lsl r0, r0, #24
asr r1, r0, #24
mov r0, r4
bl _ZNSo3putEc
bl _ZNSo5flushEv
mov r0, #0
pop {r4, r5, lr}
mov pc, lr
.LBB0_5:
bl _ZSt16__throw_bad_castv
.align 2
@ BB#6:
Sin embargo, para ser honesto, esto no significa nada, ya que la E/S es el cuello de botella de todos modos. Sólo quería mostrar que iostream
no es más rápido porque es "seguro para escribir". La mayoría de las implementaciones de C implementan printf
formatos usando goto calculado, por lo que printf
es lo más rápido posible, incluso sin que el compilador lo sepa printf
(no es que no lo sean; algunos compiladores pueden optimizar printf
en ciertos casos; la cadena constante que termina en \n
generalmente se optimiza para puts
) .
Herencia
No sé por qué querrías heredar ostream
, pero no me importa. También es posible FILE
.
class MyFile : public FILE {}
Tipo de seguridad
Es cierto que las listas de argumentos de longitud variable no son seguras, pero eso no importa, ya que los compiladores de C populares pueden detectar problemas con el printf
formato de cadena si habilita las advertencias. De hecho, Clang puede hacerlo sin habilitar advertencias.
$ cat safety.c
#include <stdio.h>
int main(void) {
printf("String: %s\n", 42);
return 0;
}
$ clang safety.c
safety.c:4:28: warning: format specifies type 'char *' but the argument has type 'int' [-Wformat]
printf("String: %s\n", 42);
~~ ^~
%d
1 warning generated.
$ gcc -Wall safety.c
safety.c: In function ‘main’:
safety.c:4:5: warning: format ‘%s’ expects argument of type ‘char *’, but argument 2 has type ‘int’ [-Wformat=]
printf("String: %s\n", 42);
^
De las preguntas frecuentes sobre C++ :
[15.1] ¿Por qué debería utilizar
<iostream>
en lugar del tradicional<cstdio>
?Aumente la seguridad de los tipos, reduzca los errores, permita la extensibilidad y proporcione heredabilidad.
printf()
Podría decirse que no está roto yscanf()
quizás sea habitable a pesar de ser propenso a errores; sin embargo, ambos están limitados con respecto a lo que puede hacer la E/S de C++. La E/S de C++ (usando<<
y>>
) es, en relación con C (usandoprintf()
yscanf()
):
- Más seguridad de tipos: con
<iostream>
, el compilador conoce estáticamente el tipo de objeto que se está realizando E/S. Por el contrario,<cstdio>
utiliza campos "%" para determinar los tipos dinámicamente.- Menos propenso a errores: con
<iostream>
, no hay tokens "%" redundantes que deban ser coherentes con los objetos reales a los que se realiza E/S. Al eliminar la redundancia se elimina una clase de errores.- Extensible: el
<iostream>
mecanismo de C++ permite realizar E/S de nuevos tipos definidos por el usuario sin romper el código existente. ¡Imagínese el caos si todos agregaran simultáneamente nuevos campos "%" incompatibles aprintf()
yscanf()
?!- Heredable: el
<iostream>
mecanismo de C++ se construye a partir de clases reales comostd::ostream
ystd::istream
. A diferencia de<cstdio>
'sFILE*
, estas son clases reales y, por tanto, heredables. Esto significa que puedes tener otras cosas definidas por el usuario que se ven y actúan como transmisiones, pero que hacen las cosas extrañas y maravillosas que quieras. Automáticamente podrá utilizar millones de líneas de código de E/S escritas por usuarios que ni siquiera conoce, y ellos no necesitan conocer su clase de "flujo extendido".
Por otro lado, printf
es significativamente más rápido, lo que puede justificar su uso preferente cout
en casos muy concretos y limitados. Perfil siempre primero. (Ver, por ejemplo, http://programming-designs.com/2009/02/c-speed-test-part-2-printf-vs-cout/ )
La gente suele afirmar que printf
es mucho más rápido. Esto es en gran medida un mito. Lo acabo de probar, con los siguientes resultados:
cout with only endl 1461.310252 ms
cout with only '\n' 343.080217 ms
printf with only '\n' 90.295948 ms
cout with string constant and endl 1892.975381 ms
cout with string constant and '\n' 416.123446 ms
printf with string constant and '\n' 472.073070 ms
cout with some stuff and endl 3496.489748 ms
cout with some stuff and '\n' 2638.272046 ms
printf with some stuff and '\n' 2520.318314 ms
Conclusión: si solo quieres nuevas líneas, usa printf
; de lo contrario, cout
es casi tan rápido, o incluso más rápido. Se pueden encontrar más detalles en mi blog .
Para ser claro, no estoy tratando de decir que iostream
s siempre sea mejor que printf
; Sólo estoy tratando de decir que usted debe tomar una decisión informada basada en datos reales, no una suposición descabellada basada en alguna suposición común y engañosa.
Actualización: aquí está el código completo que utilicé para las pruebas. Compilado g++
sin opciones adicionales (aparte -lrt
del tiempo).
#include <stdio.h>
#include <iostream>
#include <ctime>
class TimedSection {
char const *d_name;
timespec d_start;
public:
TimedSection(char const *name) :
d_name(name)
{
clock_gettime(CLOCK_REALTIME, &d_start);
}
~TimedSection() {
timespec end;
clock_gettime(CLOCK_REALTIME, &end);
double duration = 1e3 * (end.tv_sec - d_start.tv_sec) +
1e-6 * (end.tv_nsec - d_start.tv_nsec);
std::cerr << d_name << '\t' << std::fixed << duration << " ms\n";
}
};
int main() {
const int iters = 10000000;
char const *text = "01234567890123456789";
{
TimedSection s("cout with only endl");
for (int i = 0; i < iters; ++i)
std::cout << std::endl;
}
{
TimedSection s("cout with only '\\n'");
for (int i = 0; i < iters; ++i)
std::cout << '\n';
}
{
TimedSection s("printf with only '\\n'");
for (int i = 0; i < iters; ++i)
printf("\n");
}
{
TimedSection s("cout with string constant and endl");
for (int i = 0; i < iters; ++i)
std::cout << "01234567890123456789" << std::endl;
}
{
TimedSection s("cout with string constant and '\\n'");
for (int i = 0; i < iters; ++i)
std::cout << "01234567890123456789\n";
}
{
TimedSection s("printf with string constant and '\\n'");
for (int i = 0; i < iters; ++i)
printf("01234567890123456789\n");
}
{
TimedSection s("cout with some stuff and endl");
for (int i = 0; i < iters; ++i)
std::cout << text << "01234567890123456789" << i << std::endl;
}
{
TimedSection s("cout with some stuff and '\\n'");
for (int i = 0; i < iters; ++i)
std::cout << text << "01234567890123456789" << i << '\n';
}
{
TimedSection s("printf with some stuff and '\\n'");
for (int i = 0; i < iters; ++i)
printf("%s01234567890123456789%i\n", text, i);
}
}
Y cito :
En términos de alto nivel, las principales diferencias son la seguridad de tipos (cstdio no la tiene), el rendimiento (la mayoría de las implementaciones de iostreams son más lentas que las de cstdio) y la extensibilidad (iostreams permite objetivos de salida personalizados y una salida perfecta de tipos definidos por el usuario).