functions - valor devuelto strcpy()
strlen c (6)
Muchas de las funciones de la biblioteca estándar de C, especialmente las de manipulación de cadenas, y más notablemente strcpy (), comparten el siguiente prototipo:
char *the_function (char *destination, ...)
El valor de retorno de estas funciones es, de hecho, el mismo que el destination
proporcionado. ¿Por qué perderías el valor de retorno por algo redundante? Tiene más sentido que una función de este tipo se anule o devuelva algo útil.
Mi única suposición de por qué esto es es que es más fácil y más conveniente anidar la llamada a la función en otra expresión, por ejemplo:
printf("%s/n", strcpy(dst, src));
¿Hay otras razones razonables para justificar este lenguaje?
Como señaló Evan, es posible hacer algo como
char* s = strcpy(malloc(10), "test");
Por ejemplo, asigne un valor a malloc()ed
memoria, sin usar la variable auxiliar.
(Este ejemplo no es el mejor, se bloqueará en las condiciones de memoria, pero la idea es obvia)
Creo que su suposición es correcta, hace que sea más fácil anidar la llamada.
El mismo concepto que las interfaces fluidas . Solo haciendo el código más rápido / fácil de leer.
No creo que esto esté realmente configurado de esta manera para propósitos de anidamiento, sino más bien para la comprobación de errores. Si la memoria no sirve ninguna de las funciones estándar de la biblioteca, la comprobación de errores por sí misma y por lo tanto tiene más sentido que esto sería determinar si algo salió mal durante la llamada de Strcpy.
if(strcpy(dest, source) == NULL) {
// Something went horribly wrong, now we deal with it
}
También es extremadamente fácil de codificar.
El valor de retorno se suele dejar en el registro de AX (no es obligatorio, pero suele ser el caso). Y el destino se coloca en el registro de AX cuando se inicia la función. Para devolver el destino, el programador debe hacer ... ¡exactamente nada! Solo deja el valor donde está.
El programador podría declarar la función como void
. ¡Pero ese valor de retorno ya está en el lugar correcto, solo esperando ser devuelto, y ni siquiera cuesta una instrucción adicional devolverlo! No importa cuán pequeña sea la mejora, es útil en algunos casos.
char *stpcpy(char *dest, const char *src);
devuelve un puntero al final de la cadena y forma parte de POSIX.1-2008 . Antes de eso, era una extensión de GNU libc desde 1992. Si apareció por primera vez en Lattice C AmigaDOS en 1986.
gcc -O3
en algunos casos optimizará strcpy
+ strcat
para usar stpcpy
o strlen
+ copia en línea, ver más abajo.
La biblioteca estándar de C se diseñó muy pronto, y es muy fácil argumentar que las funciones str*
no están diseñadas de manera óptima. Las funciones de E / S se diseñaron definitivamente muy temprano, en 1972 antes de que C tuviera un preprocesador, por lo que fopen(3)
toma una cadena de modo en lugar de un mapa de bits de bandera como Unix open(2)
.
No he podido encontrar una lista de funciones incluidas en el "paquete de E / S portátil" de Mike Lesk, por lo que no sé si strcpy
en su forma actual se remonta hasta allí o si esas funciones se agregaron más adelante. . (La única fuente real que he encontrado es el ampliamente conocido artículo de Historia C de Dennis Ritchie , que es excelente pero no tan profundo. No encontré ninguna documentación ni código fuente para el paquete de E / S real en sí).
Aparecen en su forma actual en la primera edición de K&R , 1978.
Las funciones deben devolver el resultado del cálculo que realizan, si es potencialmente útil para la persona que llama, en lugar de desecharlo . Ya sea como un puntero al final de la cadena, o una longitud entera. (Un puntero sería natural.)
Como dice @R:
Todos deseamos que estas funciones devolvieran un puntero al byte nulo de terminación (lo que reduciría muchas operaciones
O(n)
aO(1)
)
por ejemplo, llamar a strcat(bigstr, newstr[i])
en un bucle para construir una cadena larga a partir de muchas cadenas cortas (O (1) de longitud) tiene una complejidad de O(n^2)
aproximadamente, pero strlen
/ memcpy
solo mirará cada una carácter dos veces (una vez en strlen, una vez en memcpy).
Usando solo la biblioteca estándar ANSI C, no hay manera de mirar eficientemente solo a cada personaje una vez . Podría escribir manualmente un bucle de byte a la vez, pero para cadenas más largas que unos pocos bytes, eso es peor que mirar cada carácter dos veces con compiladores actuales (que no vectorizará automáticamente un bucle de búsqueda) en HW moderno, dado eficiente libc-proporcionado SIMD strlen y memcpy. Puede usar length = sprintf(bigstr, "%s", newstr[i]); bigstr+=length;
length = sprintf(bigstr, "%s", newstr[i]); bigstr+=length;
, pero sprintf()
tiene que analizar su cadena de formato y no es rápido.
Ni siquiera hay una versión de strcmp
o memcmp
que devuelva la posición de la diferencia . Si eso es lo que quieres, tienes el mismo problema que el ¿Por qué la comparación de cadenas es tan rápida en Python? : una función de biblioteca optimizada que se ejecuta más rápido que cualquier cosa que pueda hacer con un bucle compilado (a menos que tenga un ASM optimizado a mano para cada plataforma de destino que le interesa), que puede usar para acercarse al byte diferente antes de volver a un Bucle regular una vez que te acerques.
Parece que la biblioteca de cadenas de C fue diseñada sin tener en cuenta el costo O (n) de cualquier operación, no solo encontrando el final de las cadenas de longitud implícita, y el comportamiento de strcpy
definitivamente no es el único ejemplo.
Básicamente, tratan las cadenas de longitud implícita como objetos opacos completos, siempre devolviendo los punteros al inicio, nunca al final o a una posición dentro de una después de buscar o anexar.
Conjeturas de historia
A principios de C en un PDP-11 , sospecho que strcpy
no fue más eficiente que while(*dst++ = *src++) {}
(y probablemente se implementó de esa manera).
De hecho, la primera edición de K&R (página 101) muestra la implementación de strcpy
y dice:
Si bien esto puede parecer críptico a primera vista, la conveniencia de la notación es considerable, y el idioma se debe dominar, aunque solo sea por el motivo que lo verá con frecuencia en los programas.
Esto implica que esperaban que los programadores escribieran sus propios bucles en los casos en que quisieras el valor final de dst
o src
. Y, por lo tanto, tal vez no vieron la necesidad de rediseñar la API de la biblioteca estándar hasta que fue demasiado tarde para exponer las API más útiles para las funciones de la biblioteca asm optimizadas a mano.
Pero, ¿tiene sentido el devolver el valor original de dst
?
strcpy(dst, src)
devolver dst
es análogo a x=y
evaluar a x
. Así que hace que Strcpy funcione como un operador de asignación de cadenas.
Como señalan otras respuestas, esto permite el anidamiento, como foo( strcpy(buf,input) );
. Las primeras computadoras estaban muy limitadas por la memoria. Mantener su código fuente compacto era una práctica común . Las tarjetas perforadas y los terminales lentos probablemente fueron un factor en esto. No conozco los estándares históricos de codificación o las guías de estilo o lo que se consideró demasiado para poner en una línea.
Los viejos compiladores crujientes también fueron quizás un factor. Con los compiladores de optimización modernos, char *tmp = foo();
/ bar(tmp);
no es más lento que la bar(foo());
, pero es con gcc -O0
. No sé si los primeros compiladores podrían optimizar las variables completamente (sin reservar espacio de pila para ellas), pero espero que al menos puedan mantenerlas en los registros en casos simples (a diferencia del moderno gcc -O0
que a propósito derrama / recarga todo para depuración consistente). es decir, gcc -O0
no es un buen modelo para compiladores antiguos, porque es anti-optimizante a propósito para una depuración consistente.
Posible compilación generada-asm motivación.
Dada la falta de atención a la eficiencia en el diseño general de la API de la biblioteca de cadenas en C, esto podría ser poco probable. Pero tal vez hubo un beneficio de tamaño de código. (En las primeras computadoras, el tamaño del código era más un límite duro que el tiempo de CPU).
No sé mucho acerca de la calidad de los primeros compiladores de C, pero es una apuesta segura que no fueron impresionantes en la optimización, incluso para una arquitectura simple / ortogonal agradable como PDP-11.
Es común querer el puntero de cadena después de la llamada de función. A nivel de asm, usted (el compilador) probablemente lo tenga en un registro antes de la llamada. Dependiendo de la convención de llamada, o bien lo empuja en la pila o lo copia en el registro correcto donde la convención de llamada dice que el primer argumento va. (Es decir, donde strcpy
está esperando). O si está planeando con anticipación, ya tenía el puntero en el registro correcto para la convención de llamadas.
Pero la función llama a clobber algunos registros, incluidos todos los registros que pasan arg. (Entonces, cuando una función obtiene un argumento en un registro, puede incrementarlo allí en lugar de copiarlo en un registro nuevo).
Entonces, como persona que llama, su opción de código de código para mantener algo a través de una llamada de función incluye:
- almacenarlo / recargarlo en la memoria de pila local. (O simplemente recárguelo si todavía hay una copia actualizada en la memoria).
- guardar / restaurar un registro conservado en la llamada al inicio / final de su función completa, y copiar el puntero a uno de esos registros antes de la llamada a la función.
- La función devuelve el valor en un registro para usted. (Por supuesto, esto solo funciona si la fuente C está escrita para usar el valor de retorno en lugar de la variable de entrada. Por ejemplo,
dst = strcpy(dst, src);
si no lo está anidando).
Todas las convenciones de llamadas en todas las arquitecturas estoy al tanto de los valores de retorno de tamaño de puntero de retorno en un registro, por lo que tener tal vez una instrucción adicional en la función de biblioteca puede guardar el tamaño de código en todas las personas que llaman que desean usar ese valor de retorno.
Probablemente obtuviste mejor asm de los compiladores primitivos de C usando el valor de retorno de strcpy
(ya en un registro) que haciendo que el compilador guarde el puntero alrededor de la llamada en un registro conservado de llamadas o lo derrame en la pila. Este todavía puede ser el caso.
Por cierto, en muchas ISA, el registro de valor de retorno no es el primer registro de paso de arg. Y a menos que use los modos de direccionamiento de índice + base, cuesta una instrucción adicional (y vincula otra reg) para que Strcpy copie el registro para un bucle de incremento de puntero.
Las cadenas de herramientas PDP-11 normalmente usaban algún tipo de convención de llamada stack-args , siempre empujando args en la pila. No estoy seguro de cuántos registros de llamadas preservadas y de llamadas obstruidas eran normales, pero solo estaban disponibles 5 o 6 registros de GP ( R7 es el contador del programa, R6 es el puntero de pila, R5 se usa a menudo como puntero de marco ). Así que es similar, pero incluso más estrecho que el x86 de 32 bits.
char *bar(char *dst, const char *str1, const char *str2)
{
//return strcat(strcat(strcpy(dst, str1), "separator"), str2);
// more readable to modern eyes:
dst = strcpy(dst, str1);
dst = strcat(dst, "separator");
// dst = strcat(dst, str2);
return dst; // simulates further use of dst
}
# x86 32-bit gcc output, optimized for size (not speed)
# gcc8.1 -Os -fverbose-asm -m32
# input args are on the stack, above the return address
push ebp #
mov ebp, esp #, Create a stack frame.
sub esp, 16 #, This looks like a missed optimization, wasted insn
push DWORD PTR [ebp+12] # str1
push DWORD PTR [ebp+8] # dst
call strcpy #
add esp, 16 #,
mov DWORD PTR [ebp+12], OFFSET FLAT:.LC0 # store new args over our incoming args
mov DWORD PTR [ebp+8], eax # EAX = dst.
leave
jmp strcat # optimized tailcall of the last strcat
Esto es significativamente más compacto que una versión que no usa dst =
, y en su lugar reutiliza el argumento de entrada para el strcat
. (Ver ambos en el explorador del compilador Godbolt .)
La salida de -O3
es muy diferente: gcc para la versión que no usa el valor de retorno usa stpcpy
(devuelve un puntero a la cola) y luego mov
-immediate para almacenar los datos de la cadena literal directamente en el lugar correcto.
Pero desafortunadamente, la versión dst = strcpy(dst, src)
-O3 todavía usa strcpy
regular, luego en línea strcat
como strlen
+ mov
-immediate.
A la cadena C o no a la cadena C
Las cadenas C de longitud implícita no siempre son intrínsecamente malas y tienen ventajas interesantes (por ejemplo, un sufijo también es una cadena válida, sin tener que copiarla).
Pero la biblioteca de cadenas en C no está diseñada de una manera que haga posible un código eficiente, porque los bucles de tiempo de ejecución generalmente no se vectorizan automáticamente y las funciones de la biblioteca desechan los resultados del trabajo que tienen que hacer.
gcc y clang nunca auto-vectorizan bucles a menos que se conozca el recuento de iteraciones antes de la primera iteración, por ejemplo, for(int i=0; i<n ;i++)
. ICC puede vectorizar los bucles de búsqueda, pero aún así es poco probable que funcione tan bien como el asm escrito a mano.
strncpy
y así sucesivamente son básicamente un desastre . por ejemplo, strncpy
no copia la terminación ''/0''
si alcanza el límite de tamaño del búfer. Parece haber sido diseñado para escribir en el medio de cadenas más grandes, no para evitar desbordamientos de búfer. No devolver un puntero al final significa que tiene que arr[n] = 0;
antes o después, tocar potencialmente una página de la memoria que nunca fue necesario tocar.
Algunas funciones como snprintf
son utilizables y siempre terminan en forma nula. Recordar lo que hace es difícil, y un gran riesgo si recuerdas mal, por lo que debes verificar cada vez que sea necesario corregirlo.
Como dice Bruce Dawson: ¡Ya dejen de usar strncpy! . Al parecer, algunas extensiones de _snprintf
como _snprintf
son incluso peores.