Por mucho que ame C y C ++, no puedo evitar rascarme la cabeza al elegir cadenas terminadas en nulo:
- Las cadenas de longitud prefijadas (es decir, Pascal) existían antes de C
- Las cadenas prefijadas de longitud hacen que varios algoritmos sean más rápidos al permitir una búsqueda de longitud de tiempo constante.
- Las cadenas prefijadas de longitud hacen que sea más difícil causar errores de desbordamiento del búfer.
- Incluso en una máquina de 32 bits, si permite que la cadena sea del tamaño de la memoria disponible, una cadena prefijada de longitud es solo tres bytes más ancha que una cadena terminada en nulo. En máquinas de 16 bits, este es un solo byte. En máquinas de 64 bits, 4 GB es un límite de longitud de cadena razonable, pero incluso si desea expandirlo al tamaño de la palabra de máquina, las máquinas de 64 bits generalmente tienen memoria suficiente, lo que hace que los siete bytes adicionales sean un argumento nulo. Sé que el estándar C original fue escrito para máquinas increíblemente pobres (en términos de memoria), pero el argumento de la eficiencia no me vende aquí.
- Casi todos los demás idiomas (es decir, Perl, Pascal, Python, Java, C #, etc.) usan cadenas prefijadas de longitud. Estos lenguajes generalmente superan a C en los puntos de referencia de manipulación de cadenas porque son más eficientes con las cadenas.
- C ++ rectificó esto un poco con la
std::basic_string
plantilla, pero las matrices de caracteres simples que esperan cadenas terminadas en nulo siguen siendo dominantes. Esto también es imperfecto porque requiere la asignación del montón. - Las cadenas terminadas en nulo deben reservar un carácter (es decir, nulo), que no puede existir en la cadena, mientras que las cadenas con prefijo de longitud pueden contener nulos incrustados.
Varias de estas cosas han salido a la luz más recientemente que C, por lo que tendría sentido que C no las supiera. Sin embargo, varios eran evidentes mucho antes de que C surgiera. ¿Por qué se habrían elegido cadenas terminadas en cero en lugar del prefijo de longitud obviamente superior?
EDITAR : Dado que algunos pidieron datos (y no les gustaron los que ya proporcioné) en mi punto de eficiencia anterior, provienen de algunas cosas:
- Concat que utiliza cadenas terminadas en nulo requiere una complejidad de tiempo O (n + m). El prefijo de longitud a menudo requiere solo O (m).
- La longitud que usa cadenas terminadas en nulo requiere una complejidad de tiempo O (n). El prefijo de longitud es O (1).
- Longitud y concat son, con mucho, las operaciones de cadena más comunes. Hay varios casos en los que las cadenas terminadas en nulo pueden ser más eficientes, pero ocurren con mucha menos frecuencia.
De las respuestas a continuación, estos son algunos casos en los que las cadenas terminadas en nulo son más eficientes:
- Cuando necesita cortar el inicio de una cadena y necesita pasarla a algún método. Realmente no puede hacer esto en tiempo constante con el prefijo de longitud, incluso si se le permite destruir la cadena original, porque el prefijo de longitud probablemente deba seguir las reglas de alineación.
- En algunos casos en los que solo está recorriendo la cadena carácter por carácter, es posible que pueda guardar un registro de CPU. Tenga en cuenta que esto funciona solo en el caso de que no haya asignado dinámicamente la cadena (porque entonces tendría que liberarla, necesitando usar ese registro de CPU que guardó para contener el puntero que originalmente obtuvo de malloc y amigos).
Ninguno de los anteriores es tan común como la longitud y la concat.
Hay uno más afirmado en las respuestas a continuación:
- Necesitas cortar el final de la cuerda
pero este es incorrecto: es la misma cantidad de tiempo para las cadenas con terminación nula y con prefijo de longitud. (Las cadenas terminadas en nulo solo pegan un nulo donde desea que esté el nuevo final, los prefijos de longitud solo se restan del prefijo).
fuente
Respuestas:
De la boca del caballo
Dennis M Ritchie, Desarrollo del lenguaje C
fuente
C no tiene una cadena como parte del lenguaje. Una 'cadena' en C es solo un puntero a char. Entonces tal vez estás haciendo la pregunta equivocada.
"¿Cuál es la razón para omitir un tipo de cadena" podría ser más relevante. A eso señalaría que C no es un lenguaje orientado a objetos y solo tiene tipos de valores básicos. Una cadena es un concepto de nivel superior que debe implementarse de alguna manera combinando valores de otros tipos. C está en un nivel inferior de abstracción.
a la luz de la furiosa tormenta de abajo:
Solo quiero señalar que no estoy tratando de decir que esta es una pregunta estúpida o mala, o que la forma C de representar cadenas es la mejor opción. Estoy tratando de aclarar que la pregunta se plantearía de manera más sucinta si se tiene en cuenta el hecho de que C no tiene ningún mecanismo para diferenciar una cadena como tipo de datos de una matriz de bytes. ¿Es esta la mejor opción a la luz del poder de procesamiento y memoria de las computadoras de hoy? Probablemente no. Pero en retrospectiva siempre es 20/20 y todo eso :)
fuente
char *temp = "foo bar";
es una declaración válida en C ... hey! ¿No es eso una cuerda? ¿No es nulo terminado?La pregunta se hace como una cosa
Length Prefixed Strings (LPS)
vszero terminated strings (SZ)
, pero en su mayoría expone los beneficios de las cadenas prefijadas de longitud. Eso puede parecer abrumador, pero para ser sincero, también debemos considerar los inconvenientes de LPS y las ventajas de SZ.Según tengo entendido, la pregunta puede incluso entenderse como una forma sesgada de preguntar "¿cuáles son las ventajas de las cadenas terminadas en cero?".
Ventajas (veo) de las cadenas terminadas en cero:
"this\0is\0valid\0C"
. ¿Es una cuerda? o cuatro cuerdas? O un montón de bytes ...char a[3] = "foo";
es válido C (no C ++) y no pondrá un cero final en a.char*
. Es decir, no devolver la dirección de la cadena, sino devolver los datos reales.Dicho esto, no es necesario quejarse en el raro caso en que las cadenas C estándar son de hecho ineficientes. Libs están disponibles. Si seguí esa tendencia, debería quejarme de que el estándar C no incluye ninguna función de soporte de expresiones regulares ... pero realmente todos saben que no es un problema real ya que hay bibliotecas disponibles para ese propósito. Entonces, cuando se desea la eficiencia de la manipulación de cadenas, ¿por qué no usar una biblioteca como bstring ? ¿O incluso cadenas de C ++?
EDITAR : Hace poco tuve un vistazo a las cadenas D . Es lo suficientemente interesante como para ver que la solución elegida no es un prefijo de tamaño ni una terminación cero. Al igual que en C, las cadenas literales entre comillas dobles son solo una abreviatura de las matrices de caracteres inmutables, y el lenguaje también tiene una palabra clave de cadena que significa eso (matriz de caracteres inmutable).
Pero las matrices D son mucho más ricas que las matrices C. En el caso de matrices estáticas, la longitud se conoce en tiempo de ejecución, por lo que no es necesario almacenar la longitud. El compilador lo tiene en tiempo de compilación. En el caso de las matrices dinámicas, la longitud está disponible pero la documentación de D no indica dónde se guarda. Por lo que sabemos, el compilador podría optar por mantenerlo en algún registro, o en alguna variable almacenada lejos de los datos de los caracteres.
En matrices de caracteres normales o cadenas no literales no hay un cero final, por lo tanto, el programador tiene que ponerlo solo si quiere llamar a alguna función C desde D. En el caso particular de las cadenas literales, sin embargo, el compilador de D todavía pone un cero en el final de cada cadena (para permitir una fácil conversión a cadenas C para facilitar la llamada a la función C), pero este cero no es parte de la cadena (D no lo cuenta en el tamaño de la cadena).
Lo único que me decepcionó un poco es que se supone que las cadenas son utf-8, pero la longitud aparentemente aún devuelve una cantidad de bytes (al menos es cierto en mi compilador gdc) incluso cuando se usan caracteres de varios bytes. No me queda claro si es un error del compilador o por propósito. (OK, probablemente descubrí lo que sucedió. Para decirle al compilador D que su fuente usa utf-8, tiene que poner una estúpida marca de orden de bytes al principio. Escribo estúpido porque sé que el editor no está haciendo eso, especialmente para UTF- 8 que se supone que es compatible con ASCII).
fuente
std::basic_string
hace.\0
al final cuando los programadores quieran eso en lugar del implícito. La longitud de espera es mucho peor.Creo que tiene razones históricas y encontré esto en wikipedia :
fuente
Calavera tiene razón , pero como la gente no parece entender su punto, proporcionaré algunos ejemplos de código.
Primero, consideremos qué es C: un lenguaje simple, donde todo el código tiene una traducción bastante directa al lenguaje máquina. Todos los tipos encajan en los registros y en la pila, y no requiere un sistema operativo o una gran biblioteca de tiempo de ejecución para ejecutarse, ya que estaba destinado a escribir estas cosas (una tarea a la que se adapta perfectamente, teniendo en cuenta allí Ni siquiera es un competidor probable hasta el día de hoy).
Si C tuviera un
string
tipo, likeint
ochar
, sería un tipo que no cabía en un registro o en la pila, y requeriría la asignación de memoria (con toda su infraestructura de soporte) para ser manejada de alguna manera. Todo lo cual va en contra de los principios básicos de C.Entonces, una cadena en C es:
Entonces, supongamos que esto tenía un prefijo de longitud. Escribamos el código para concatenar dos cadenas:
Otra alternativa sería usar una estructura para definir una cadena:
En este punto, toda manipulación de cadenas requeriría que se realicen dos asignaciones, lo que, en la práctica, significa que pasaría por una biblioteca para manejarla.
Lo curioso es ... ¡ existen estructuras como esa en C! Simplemente no se utilizan para el día a día que muestra mensajes al usuario.
Por lo tanto, aquí está el punto de Calavera está haciendo: no hay ningún tipo cadena en C . Para hacer algo con él, tendría que tomar un puntero y decodificarlo como un puntero para dos tipos diferentes, y luego se vuelve muy relevante cuál es el tamaño de una cadena, y no puede dejarse simplemente como "implementación definida".
Ahora, C puede manejar la memoria de todos modos, y las
mem
funciones en la biblioteca (<string.h>
¡incluso!) Proporcionan todas las herramientas que necesita para manejar la memoria como un par de puntero y tamaño. Las llamadas "cadenas" en C se crearon con un solo propósito: mostrar mensajes en el contexto de escribir un sistema operativo destinado a terminales de texto. Y, para eso, la terminación nula es suficiente.fuente
strlen
y amigos en su lugar. En cuanto al problema de "dejarlo en manos de la implementación", se podría decir que el prefijo es el queshort
está en el cuadro de destino. Entonces todo tu casting aún funcionaría. 3. Puedo idear escenarios artificiales todo el día que hagan que uno u otro sistema se vea mal.short
efectivamente limita el tamaño de la cadena, que parece ser una cosa que no estaban interesados. Yo mismo, después de haber trabajado con cadenas BASIC y Pascal de 8 bits, cadenas COBOL de tamaño fijo y cosas similares, me convertí rápidamente en un gran fanático de las cadenas C de tamaño ilimitado. Hoy en día, un tamaño de 32 bits manejará cualquier cadena práctica, pero agregar esos bytes al principio fue problemático.string
tipo real : no es consciente de los caracteres. Es una serie de "char" (un "char" en la jerga de la máquina es tanto un personaje como una "palabra" es lo que los humanos llamarían una palabra en una oración). Una cadena de caracteres es un concepto de nivel superior que podría implementarse en la parte superior de una matrizchar
si se introduce la noción de codificación.buf
requiere una asignación), o usestruct string {int len; char buf[]};
y asigne todo con una asignación como miembro de matriz flexible, y páselo como astring*
. (O posiblemente,struct string {int capacity; int len; char buf[]};
por obvias razones de rendimiento)Obviamente, por su rendimiento y seguridad, querrás mantener la longitud de una cuerda mientras trabajas con ella en lugar de realizarla repetidamente
strlen
o el equivalente en ella. Sin embargo, almacenar la longitud en una ubicación fija justo antes del contenido de la cadena es un diseño increíblemente malo. Como Jörgen señaló en los comentarios sobre la respuesta de Sanjit, impide tratar la cola de una cadena como una cadena, lo que, por ejemplo, hace que muchas operaciones comunes sean imposiblespath_to_filename
ofilename_to_extension
sin asignar nueva memoria (e incurrir en la posibilidad de fallas y manejo de errores) . Y luego, por supuesto, está el problema de que nadie puede acordar cuántos bytes debe ocupar el campo de longitud de cadena (un montón de "cadena Pascal" incorrecta)El diseño de C de permitir que el programador elija si / dónde / cómo almacenar la longitud es mucho más flexible y potente. Pero, por supuesto, el programador tiene que ser inteligente. C castiga la estupidez con programas que se bloquean, se detienen o dan raíces a tus enemigos.
fuente
Pereza, registro de frugalidad y portabilidad teniendo en cuenta el instinto de ensamblaje de cualquier lenguaje, especialmente C, que está un paso por encima del ensamblaje (por lo tanto, hereda una gran cantidad de código heredado de ensamblaje). Usted estaría de acuerdo ya que un carácter nulo sería inútil en esos días ASCII (y probablemente tan bueno como un carácter de control EOF).
veamos en pseudocódigo
total 1 uso de registro
caso 2
total 2 registros utilizados
Eso puede parecer miope en ese momento, pero teniendo en cuenta la frugalidad en el código y el registro (que eran PREMIUM en ese momento, el momento en que sabes, usan tarjeta perforada). Por lo tanto, siendo más rápido (cuando la velocidad del procesador se podía contar en kHz), este "Hack" era bastante bueno y portátil para registrar el procesador sin facilidad con facilidad.
Por el bien del argumento, implementaré 2 operaciones de cadena común
complejidad O (n) donde, en la mayoría de los casos, la cadena PASCAL es O (1) porque la longitud de la cadena está preajustada a la estructura de la cadena (eso también significaría que esta operación debería llevarse a cabo en una etapa anterior).
complejidad O (n) y anteponer la longitud de la cadena no cambiaría la complejidad de la operación, aunque admito que tomaría 3 veces menos tiempo.
Por otro lado, si usa la cadena PASCAL, tendría que rediseñar su API para tener en cuenta la longitud del registro y la duración de bits, la cadena PASCAL obtuvo la conocida limitación de 255 caracteres (0xFF) porque la longitud se almacenó en 1 byte (8 bits) ), y si quisieras una cadena más larga (16bits-> cualquier cosa) tendrías que tener en cuenta la arquitectura en una capa de tu código, eso significaría en la mayoría de los casos API de cadenas incompatibles si quisieras una cadena más larga.
Ejemplo:
Se escribió un archivo con su api de cadena anexada en una computadora de 8 bits y luego tendría que leerse en una computadora de 32 bits, ¿qué haría el programa perezoso si considera que sus 4 bytes son la longitud de la cadena y luego asignan esa cantidad de memoria? luego intente leer tantos bytes. Otro caso sería PPC 32 byte string read (little endian) en un x86 (big endian), por supuesto, si no sabe que uno está escrito por el otro, habría problemas. La longitud de 1 byte (0x00000001) se convertiría en 16777216 (0x0100000), es decir, 16 MB para leer una cadena de 1 byte. Por supuesto, diría que las personas deberían ponerse de acuerdo en un estándar, pero incluso unicode de 16 bits tiene poca y gran resistencia.
Por supuesto, C también tendría sus problemas, pero se vería muy poco afectado por los problemas planteados aquí.
fuente
O(m+n)
con cadenas nulas,O(n)
típicas en cualquier otro lugar. LongitudO(n)
con cuerdas nulas, enO(1)
cualquier otro lugar. Únete:O(n^2)
con cadenas nullterm, enO(n)
cualquier otro lugar. Hay algunos casos en los que las cadenas terminadas en nulo son más eficientes (es decir, solo agregue uno al caso del puntero), pero concat y la longitud son, con mucho, las operaciones más comunes (la longitud al menos es necesaria para el formateo, la salida del archivo, la visualización de la consola, etc.) . Si almacena en caché la longitud para amortizarO(n)
, simplemente ha dicho que la longitud debe almacenarse con la cadena.En muchos sentidos, C era primitivo. Y me encantó.
Fue un paso por encima del lenguaje ensamblador, ofreciéndole casi el mismo rendimiento con un lenguaje que era mucho más fácil de escribir y mantener.
El terminador nulo es simple y no requiere soporte especial por parte del idioma.
Mirando hacia atrás, no parece tan conveniente. Pero usé lenguaje ensamblador en los años 80 y me pareció muy conveniente en ese momento. Simplemente creo que el software está en continua evolución, y las plataformas y herramientas se vuelven cada vez más sofisticadas.
fuente
Suponiendo por un momento que C implementó las cadenas al estilo Pascal, prefijándolas por longitud: ¿una cadena larga de 7 caracteres tiene el mismo TIPO DE DATOS que una cadena de 3 caracteres? Si la respuesta es sí, ¿qué tipo de código debe generar el compilador cuando asigno el primero al segundo? ¿Se debe truncar la cadena o cambiar su tamaño automáticamente? Si se cambia el tamaño, ¿esa operación debería estar protegida por una cerradura para que sea segura para la rosca? El lado del enfoque C superó todos estos problemas, nos guste o no :)
fuente
De alguna manera, entendí que la pregunta implica que no hay soporte para el compilador para cadenas con prefijo de longitud en C. El siguiente ejemplo muestra que, al menos, puede iniciar su propia biblioteca de cadenas C, donde las longitudes de las cadenas se cuentan en el momento de la compilación, con una construcción como esta:
Sin embargo, esto no vendrá sin problemas, ya que debe tener cuidado al liberar específicamente ese puntero de cadena y cuando se asigna estáticamente (
char
matriz literal ).Editar: como una respuesta más directa a la pregunta, mi opinión es que esta era la forma en que C podría admitir que ambos tuvieran una longitud de cadena disponible (como una constante de tiempo de compilación), en caso de que la necesite, pero aún sin sobrecarga de memoria si desea usar solo punteros y terminación cero.
Por supuesto, parece que trabajar con cadenas terminadas en cero fue la práctica recomendada, ya que la biblioteca estándar en general no toma longitudes de cadena como argumentos, y dado que extraer la longitud no es un código tan sencillo como
char * s = "abc"
, como muestra mi ejemplo.fuente
char*
, muchos métodos que no esperan terminación nula también esperan achar*
. Un beneficio más significativo de separar los tipos estaría relacionado con el comportamiento Unicode. Puede valer la pena que una implementación de cadena mantenga marcas para saber si se sabe que las cadenas contienen ciertos tipos de caracteres, o si se sabe que no los contienen [por ejemplo, encontrar el punto de código 999,990 en una cadena de un millón de caracteres que se sabe que no contiene cualquier personaje más allá del plano multilingüe básico será mucho más rápido ...Primero, 3 bytes adicionales pueden ser una sobrecarga considerable para cadenas cortas. En particular, una cadena de longitud cero ahora ocupa 4 veces más memoria. Algunos de nosotros estamos usando máquinas de 64 bits, por lo que necesitamos 8 bytes para almacenar una cadena de longitud cero, o el formato de cadena no puede hacer frente a las cadenas más largas que admite la plataforma.
También puede haber problemas de alineación con los que lidiar. Supongamos que tengo un bloque de memoria que contiene 7 cadenas, como "solo \ 0second \ 0 \ 0four \ 0five \ 0 \ 0seventh". La segunda cadena comienza en el desplazamiento 5. El hardware puede requerir que los enteros de 32 bits se alineen en una dirección que sea múltiplo de 4, por lo que debe agregar relleno, lo que aumenta aún más la sobrecarga. La representación C es muy eficiente en memoria en comparación. (La eficiencia de la memoria es buena; por ejemplo, ayuda al rendimiento de la memoria caché).
fuente
La terminación nula permite operaciones rápidas basadas en punteros.
fuente
strlen
. Diría que es un inconveniente.Un punto aún no mencionado: cuando se diseñó C, había muchas máquinas donde un 'char' no era de ocho bits (incluso hoy en día hay plataformas DSP donde no lo es). Si uno decide que las cadenas deben tener un prefijo de longitud, ¿cuántos prefijos de longitud de caracteres debería usar? El uso de dos impondría un límite artificial en la longitud de la cadena para máquinas con caracteres de 8 bits y espacio de direccionamiento de 32 bits, mientras que desperdicia espacio en máquinas con caracteres de 16 bits y espacio de direccionamiento de 16 bits.
Si uno quisiera permitir que las cadenas de longitud arbitraria se almacenen de manera eficiente, y si 'char' fuera siempre de 8 bits, uno podría, por algún gasto en velocidad y tamaño de código, definir un esquema que fuera una cadena prefijada por un número par N tendría una longitud de N / 2 bytes, una cadena prefijada con un valor impar N y un valor par M (lectura hacia atrás) podría ser ((N-1) + M * char_max) / 2, etc. y requeriría cualquier búfer que afirma ofrecer una cierta cantidad de espacio para contener una cadena debe permitir suficientes bytes que precedan a ese espacio para manejar la longitud máxima. Sin embargo, el hecho de que 'char' no siempre sea de 8 bits complicaría dicho esquema, ya que el número de 'char' requerido para contener la longitud de una cadena variará dependiendo de la arquitectura de la CPU.
fuente
sizeof(char)
.sizeof(char)
es uno. Siempre. Uno podría tener el prefijo de un tamaño definido por la implementación, pero sería incómodo. Además, no hay una forma real de saber cuál debería ser el tamaño "correcto". Si uno tiene muchas cadenas de 4 caracteres, el relleno cero impondría una sobrecarga del 25%, mientras que un prefijo de longitud de cuatro bytes impondría una sobrecarga del 100%. Además, el tiempo dedicado a empacar y desempacar prefijos de longitud de cuatro bytes podría exceder el costo de escanear cadenas de 4 bytes para el byte cero.size_t
prefijo ( maldición sea el desperdicio de memoria, sería la más sana --- permitir cadenas de cualquier longitud posible que puedan caber en la memoria). De hecho, esa es la clase de lo que hace D; las matrices sonstruct { size_t length; T* ptr; }
, y las cadenas son solo matrices deimmutable(char)
.Muchas decisiones de diseño que rodean a C surgen del hecho de que cuando se implementó originalmente, el paso de parámetros era algo costoso. Dada una elección entre, por ejemplo
versus
este último habría sido un poco más barato (y por lo tanto preferido) ya que solo requería pasar un parámetro en lugar de dos. Si el método al que se llama no necesita conocer la dirección base de la matriz ni el índice que contiene, pasar un solo puntero combinando los dos sería más barato que pasar los valores por separado.
Si bien hay muchas formas razonables en las que C podría haber codificado longitudes de cadena, los enfoques que se habían inventado hasta ese momento tendrían todas las funciones requeridas que deberían poder trabajar con parte de una cadena para aceptar la dirección base de la cadena y el índice deseado como dos parámetros separados. El uso de la terminación de byte cero permitió evitar ese requisito. Aunque otros enfoques serían mejores con las máquinas actuales (los compiladores modernos a menudo pasan parámetros en los registros, y memcpy se puede optimizar de manera strcpy () - los equivalentes no pueden) suficiente código de producción utiliza cadenas terminadas de cero bytes que es difícil cambiar a cualquier otra cosa.
PD: a cambio de una leve penalización de velocidad en algunas operaciones, y un poco de sobrecarga adicional en cadenas más largas, habría sido posible que los métodos que funcionan con cadenas acepten punteros directamente a cadenas, buffers de cadena con control de límites o estructuras de datos que identifican subcadenas de otra cadena. Una función como "strcat" se habría parecido a [sintaxis moderna]
Un poco más grande que el método strcat de K&R, pero admitiría la verificación de límites, lo que no hace el método K&R. Además, a diferencia del método actual, sería posible concatenar fácilmente una subcadena arbitraria, p. Ej.
Tenga en cuenta que la vida útil de la cadena devuelta por temp_substring estaría limitada por las de
s
ysrc
, que alguna vez fue más corta (por lo que el método requiereinf
ser pasado, si fuera local, moriría cuando el método regresara).En términos de costo de memoria, las cadenas y las memorias intermedias de hasta 64 bytes tendrían un byte de sobrecarga (igual que las cadenas terminadas en cero); las cadenas más largas tendrían un poco más (si una cantidad permitida de sobrecarga entre dos bytes y el máximo requerido sería una compensación tiempo / espacio). Se usaría un valor especial del byte longitud / modo para indicar que a una función de cadena se le dio una estructura que contiene un byte indicador, un puntero y una longitud de búfer (que luego podría indexarse arbitrariamente en cualquier otra cadena).
Por supuesto, K&R no implementó tal cosa, pero es muy probable porque no querían gastar mucho esfuerzo en el manejo de cadenas, un área donde incluso hoy en día muchos idiomas parecen bastante anémicos.
fuente
char* arr
apuntar a una estructura de la formastruct { int length; char characters[ANYSIZE_ARRAY] };
o similar que todavía sería aceptable como un solo parámetro.str[n]
referencia al char correcto. Estas son las cosas en las que la gente que discute esto no piensa .Según Joel Spolsky en esta publicación de blog ,
Después de ver todas las otras respuestas aquí, estoy convencido de que incluso si esto es cierto, es solo una de las razones por las que C tiene "cadenas" terminadas en nulo. Esa publicación es bastante esclarecedora sobre cómo las cosas simples como las cadenas pueden ser bastante difíciles.
fuente
.ASCIZ
era solo una declaración de ensamblador para construir una secuencia de bytes, seguida de0
. Simplemente significa que la cadena terminada en cero era un concepto bien establecido en ese momento. No , no quiere decir que cero cadenas terminadas eran algo relacionado con la arquitectura de un PDP *, excepto que se podría escribir bucles apretados que consisten enMOVB
(copiar un byte) yBNE
(sucursal si el último byte copiado no era cero).No es necesariamente una justificación, sino un contrapunto a la codificación de longitud
Ciertas formas de codificación de longitud dinámica son superiores a la codificación de longitud estática en lo que respecta a la memoria, todo depende del uso. Solo mire UTF-8 como prueba. Es esencialmente una matriz de caracteres extensible para codificar un solo carácter. Esto usa un solo bit para cada byte extendido. La terminación NUL usa 8 bits. Prefijo de longitud Creo que también se puede llamar razonablemente longitud infinita mediante el uso de 64 bits. La frecuencia con la que acierte el caso de sus bits adicionales es el factor decisivo. ¿Solo 1 cuerda extremadamente grande? ¿A quién le importa si estás usando 8 o 64 bits? ¿Muchas cadenas pequeñas (es decir, cadenas de palabras en inglés)? Entonces sus costos de prefijo son un gran porcentaje.
Las cadenas con longitud prefijada que permiten ahorrar tiempo no son reales . Ya sea que se requiera que se proporcione la longitud de sus datos suministrados, está contando en el momento de la compilación, o realmente se le están proporcionando datos dinámicos que debe codificar como una cadena. Estos tamaños se calculan en algún momento del algoritmo. Una variable independiente para almacenar el tamaño de una cadena terminada en nulo puede ser proporcionada. Lo que hace que la comparación en el ahorro de tiempo sea discutible. Uno solo tiene un NUL extra al final ... pero si la codificación de longitud no incluye ese NUL, literalmente no hay diferencia entre los dos. No se requiere ningún cambio algorítmico en absoluto. Solo un pase previo que debe diseñar manualmente usted mismo en lugar de tener un compilador / tiempo de ejecución que lo haga por usted. C se trata principalmente de hacer las cosas manualmente.
El prefijo de longitud es opcional es un punto de venta. No siempre necesito esa información adicional para un algoritmo, por lo que tener que hacerlo para cada cadena hace que mi tiempo de cálculo y precalculación nunca sea inferior a O (n). (Es decir, generador de números aleatorios de hardware 1-128. Puedo extraer de una "cadena infinita". Digamos que solo genera caracteres tan rápido. Por lo tanto, nuestra longitud de cadena cambia todo el tiempo. Pero mi uso de los datos probablemente no me importa cómo tengo muchos bytes aleatorios. Solo quiere el siguiente byte no utilizado disponible tan pronto como pueda obtenerlo después de una solicitud. Podría estar esperando en el dispositivo. Pero también podría tener un búfer de caracteres preleídos. Una comparación de longitud es un desperdicio innecesario de cómputo. Una verificación nula es más eficiente).
¿El prefijo de longitud es una buena protección contra el desbordamiento del búfer? También lo es el uso sensato de las funciones y la implementación de la biblioteca. ¿Qué pasa si paso datos mal formados? ¡Mi búfer tiene 2 bytes de largo pero le digo a la función que es 7! Ej: Si recibe estaba destinado a ser utilizado en datos conocidos, podría haber tenido una verificación interna del búfer que probara los búferes compilados y malloc () llamadas () y sigue las especificaciones. Si estaba destinado a usarse como una tubería para que STDIN desconocido llegue a un búfer desconocido, entonces claramente uno no puede saber sobre el tamaño del búfer, lo que significa que una longitud arg no tiene sentido, necesita algo más aquí, como un chequeo canario. Para el caso, no puede prefijar la longitud de algunas secuencias y entradas, simplemente no puede. Lo que significa que la verificación de longitud debe integrarse en el algoritmo y no en una parte mágica del sistema de escritura. TL; DR terminado en NUL nunca tuvo que ser inseguro, simplemente terminó de esa manera por mal uso.
contra-counter point: la terminación NUL es molesta en binario. Debe hacer un prefijo de longitud aquí o transformar bytes NUL de alguna manera: códigos de escape, reasignación de rango, etc., lo que por supuesto significa más uso de memoria / información reducida / más operaciones por byte. El prefijo de longitud gana principalmente la guerra aquí. La única ventaja de una transformación es que no es necesario escribir funciones adicionales para cubrir las cadenas de prefijo de longitud. Lo que significa que en sus rutinas sub-O (n) más optimizadas puede hacer que actúen automáticamente como sus equivalentes O (n) sin agregar más código. La desventaja es, por supuesto, el desperdicio de tiempo / memoria / compresión cuando se usa en cadenas pesadas NUL.Dependiendo de cuánto de su biblioteca termine duplicando para operar con datos binarios, puede tener sentido trabajar únicamente con cadenas de prefijo de longitud. Dicho esto, uno también podría hacer lo mismo con las cadenas de prefijo de longitud ... -1 longitud podría significar terminada en NUL y podría usar cadenas terminadas en NUL dentro de terminada en longitud.
Concat: "O (n + m) vs O (m)" Supongo que te refieres a m como la longitud total de la cadena después de la concatenación porque ambos tienen que tener ese número mínimo de operaciones (no puedes agregar -en la cadena 1, ¿qué pasa si tiene que reasignar?). Y supongo que n es una cantidad mítica de operaciones que ya no tiene que hacer debido a un cálculo previo. Si es así, la respuesta es simple: pre-cómputo. Siinsiste en que siempre tendrá suficiente memoria para no necesitar reasignar y esa es la base de la notación big-O, entonces la respuesta es aún más simple: haga una búsqueda binaria en la memoria asignada para el final de la cadena 1, claramente hay una gran muestra de ceros infinitos después de la cadena 1 para que no nos preocupemos por realloc. Allí, fácilmente logré ingresar n (n) y apenas lo intenté. Lo que si recuerda log (n) es esencialmente solo tan grande como 64 en una computadora real, que es esencialmente como decir O (64 + m), que es esencialmente O (m). (Y sí, esa lógica se ha utilizado en el análisis en tiempo de ejecución de estructuras de datos reales en uso hoy en día. No es una mierda de mi cabeza).
Concat () / Len () nuevamente : Memoize results. Fácil. Convierte todos los cálculos en cálculos previos si es posible / necesario. Esta es una decisión algorítmica. No es una restricción forzada del lenguaje.
El paso del sufijo de cadena es más fácil / posible con terminación NUL. Dependiendo de cómo se implemente el prefijo de longitud, puede ser destructivo en la cadena original y, a veces, incluso puede no ser posible. Requerir una copia y pasar O (n) en lugar de O (1).
El paso de argumento / desreferenciación es menor para el prefijo terminado en NUL frente al de longitud. Obviamente porque estás pasando menos información. Si no necesita longitud, esto ahorra mucho espacio y permite optimizaciones.
Puedes hacer trampa. Realmente es solo un puntero. ¿Quién dice que tienes que leerlo como una cadena? ¿Qué pasa si quieres leerlo como un solo personaje o un flotador? ¿Qué pasa si quieres hacer lo contrario y leer un flotador como una cadena? Si tiene cuidado, puede hacerlo con terminación NUL. No puede hacer esto con el prefijo de longitud, es un tipo de datos claramente diferente de un puntero típicamente. Lo más probable es que tenga que construir una cadena byte por byte y obtener la longitud. Por supuesto, si quisieras algo así como un flotador completo (probablemente tiene un NUL dentro), tendrías que leer byte a byte de todos modos, pero los detalles te quedan para decidir.
TL; DR ¿Está utilizando datos binarios? Si no, entonces la terminación NUL permite más libertad algorítmica. En caso afirmativo, su principal preocupación es la cantidad de código frente a la velocidad / memoria / compresión. Una combinación de los dos enfoques o la memorización podría ser la mejor.
fuente
No compro la respuesta "C no tiene cadena". Es cierto que C no admite tipos integrados de nivel superior, pero aún puede representar estructuras de datos en C y eso es lo que es una cadena. El hecho de que una cadena sea solo un puntero en C no significa que los primeros N bytes no puedan tener un significado especial como longitud.
Los desarrolladores de Windows / COM estarán muy familiarizados con el
BSTR
tipo que es exactamente así: una cadena C con prefijo de longitud donde los datos de caracteres reales no comienzan en el byte 0.Entonces parece que la decisión de usar terminación nula es simplemente lo que la gente prefiere, no una necesidad del lenguaje.
fuente
gcc acepta los siguientes códigos:
char s [4] = "abcd";
y está bien si tratamos es como una matriz de caracteres pero no como una cadena. Es decir, podemos acceder con s [0], s [1], s [2] y s [3], o incluso con memcpy (dest, s, 4). Pero obtendremos caracteres desordenados cuando lo intentemos con put (s), o peor, con strcpy (dest, s).
fuente