2010-06-24 7 views
7

Como C++ programador veces necesito tratar con buffers de memoria usando técnicas de C. Por ejemplo:Tratar con tampones de char

char buffer[512]; 
sprintf(buffer, "Hello %s!", userName.c_str()); 

o en Windows:

TCHAR buffer[MAX_PATH+1]; // edit: +1 added 
::GetCurrentDirectory(sizeof(buffer)/sizeof(TCHAR), &buffer[0]); 

La muestra anterior es cómo Por lo general, crea buffers locales (una matriz de caracteres asignada localmente). Sin embargo, hay muchas variaciones posibles y por lo que estoy muy interesado en sus respuestas a las siguientes preguntas:

  • está pasando el búfer como &buffer[0] mejor estilo de programación de pasar buffer? (Prefiero &buffer[0].)
  • ¿Existe un tamaño máximo que se considera seguro para los almacenamientos intermedios asignados a la pila?
    • Actualización: Me refiero, por ejemplo, el valor más alto que puede ser considerado seguro para aplicaciones de escritorio multiplataforma en Mac, Windows, Linux ordenadores de sobremesa (no móvil).
  • ¿Es un buffer estático (static char buffer[N];) más rápido? ¿Hay otros argumentos a favor o en contra?
  • Al utilizar memorias intermedias estáticas puede usar el tipo de retorno const char *. ¿Es esto (generalmente) una buena o una mala idea? (Me doy cuenta de que la persona que llama tendrá que hacer su propia copia para evitar que la próxima llamada cambie el valor de retorno anterior.)
  • ¿Qué pasa con el uso de static char * buffer = new char[N];, nunca eliminando el búfer y reutilizándolo en cada llamada?
  • Entiendo que la asignación de montón debe usarse cuando (1) se trata de búferes grandes o (2) el tamaño máximo de búfer se desconoce en el momento de la compilación. ¿Hay algún otro factor que juegue en la decisión de asignación pila/montón?
  • ¿Prefiere las variantes sprintf_s, memcpy_s, ...? (Visual Studio ha estado tratando de convencerme de esto desde hace mucho tiempo, pero yo quiero una segunda opinión: p)
+0

¿Qué pasa con boost :: filesystem? –

+0

Olvidó un par de líneas: 'userName.resize (1024, 'A'); userName.insert (0, "world of buffer overflows!"); ' – bk1e

Respuesta

4

Asumo su interés surge principalmente de una perspectiva del rendimiento, ya que las soluciones como vector, cadena, cadena de caracteres, etc. generalmente funcionarán incluso para interactuar con las API de C. Recomiendo aprender cómo usarlos y cómo usarlos de manera eficiente. Si realmente lo necesita, puede incluso escribir su propio asignador de memoria para que sea súper rápido. Si está seguro de que no es lo que necesita, no hay excusa para no escribir un envoltorio simple para manejar estos almacenamientos intermedios de cadena con RAII para los casos dinámicos.

Con eso fuera del camino:

está pasando el búfer como & buffer [0] mejor estilo de programación de pasar búfer? (Prefiero & buffer [0].)

No. Me parece que es el estilo que sea un poco menos útil (por ser incontestablemente subjetiva aquí) ya que no se puede utilizar para pasar una memoria intermedia nula y por lo tanto tendría que hacer excepciones a su estilo para pasar punteros a matrices que pueden ser nulas. Sin embargo, es necesario si transfiere datos de std :: vector a una API de C esperando un puntero.

¿Hay un tamaño máximo que se considera seguro para pila asignado tampones?

Esto depende de la plataforma y la configuración del compilador. Regla general: si tiene dudas sobre si su código se desbordará en la pila, escríbalo de una manera que no pueda.

Es un búfer estático (carga estática búfer [N];) ¿más rápido? ¿Hay algún otro argumento a favor o en contra del ?

Sí, hay un gran argumento en contra, y es que hace que su función ya no vuelva a entrar. Si su aplicación se convierte en multiproceso, estas funciones no serán seguras para subprocesos. Incluso en una aplicación de subproceso único, compartir el mismo búfer cuando estas funciones son llamadas recursivamente puede ocasionar problemas.

¿Qué pasa con el uso del búfer de char * estático = new char [N]; y nunca borrar el buffer? (Reutilizando el mismo buffer cada llamada a )

Todavía tenemos los mismos problemas de reentrada.

entiendo que asignación de montón se debe utilizar cuando (1) se trata de grandes tampones o (2) de tamaño máximo de memoria intermedia es desconocida en tiempo de compilación. ¿Hay algún otro factor que juegue en la decisión de asignación de pila/montón?

El desenrollado de la pila destruye los objetos de la pila. Esto es especialmente importante para la seguridad de excepciones. Por lo tanto, incluso si asigna memoria en el montón dentro de una función, generalmente debe ser administrado por un objeto en la pila (por ejemplo: puntero inteligente). /// @ ver RAII.

¿Prefiere sprintf_s, memcpy_s, ... variantes? (Visual Studio ha estado tratando de convencerme de esta durante mucho tiempo, pero yo quiero una segunda opinión : p)

MS tenía razón acerca de estas funciones son alternativas más seguras, ya que no tienen memoria intermedia problemas de desbordamiento, pero si escribe tal código tal como está (sin variantes de escritura para otras plataformas), su código estará casado con Microsoft, ya que no será portátil.

Al usar memorias intermedias estáticas puede usar tipo de retorno const char *. ¿Es esto (generalmente) una buena o una mala idea? (I doy cuenta de que la persona que llama deberá para hacer su propia copia para evitar que el siguiente llamada que cambiaría el valor anterior retorno.)

yo diría que en casi todos los casos, usted quiere utilice const char * para los tipos de retorno para una función que devuelve un puntero a un búfer de caracteres.Para que una función devuelva un carácter * mutable, generalmente es confuso y problemático. O bien está devolviendo una dirección a datos globales/estáticos que no debería estar usando en primer lugar (ver reentrada arriba), datos locales de una clase (si es un método) en cuyo caso devolverlos arruina la capacidad de la clase para mantener invariantes permitiendo a los clientes manipularlo como quieran (ej: la cadena almacenada siempre debe ser válida), o devolver la memoria que fue especificada por un puntero pasado a la función (el único caso en el que razonablemente podría argumentarse que ese carácter mutable * debe ser devuelto).

3
  • Todo depende de ti, sólo haciendo buffer es más concisa pero si se tratara de un vector , deberías hacer &buffer[0] de todos modos.
  • Depende de la plataforma deseada.
  • ¿Importa? ¿Has determinado que es un problema? Escriba el código que es más fácil de leer y mantener antes de preocuparse si puede ofuscarlo en algo más rápido. Pero por lo que vale, la asignación en la pila es muy rápida (solo cambia el valor del puntero de pila)
  • Deberías estar usando std::string. Si el rendimiento se convierte en un problema, podrá reducir las asignaciones dinámicas simplemente devolviendo el búfer interno. Pero la interfaz de devolución std::string es mucho más agradable y segura, y el rendimiento es su última preocupación.
  • Eso es una pérdida de memoria. Muchos argumentarán que está bien, ya que el sistema operativo es libre de todos modos, pero siento que es una práctica terrible simplemente filtrar cosas. Utilice un std::vector estático, debe nunca hacer cualquier asignación sin procesar! Si te estás colocando en una posición en la que podrías tener fugas (porque debe hacerse de forma explícita), lo estás haciendo mal.
  • Creo que su (1) y (2) apenas lo cubren. La asignación dinámica es casi siempre más lenta que la asignación de la pila, pero debería preocuparse más por cuál tiene sentido en su situación.
  • No deberías estar usando esos en absoluto. Utilice std::string, std::stringstream, std::copy, etc.
+0

Siempre olvido que puedo usar un std :: vector y crear un buffer con malloc/new. Gracias por recordarme :) Estoy de acuerdo en que las construcciones C++ deben ser preferidas, pero me gustaría hacer una excepción para sprintf/printf. Lo encuentro más elegante que ensuciar mi código con los operadores de flujo. – StackedCrooked

+0

@Stack: Debería usar Boost.Format luego, o ajustar cosas en una función como 'boost :: lexical_cast'. – GManNickG

3

Tienes un montón de preguntas! Haré todo lo posible para responder a una pareja y darte un lugar para buscar a los demás.

¿Existe un tamaño máximo que se considera seguro para los almacenamientos intermedios asignados a la pila?

Sí, pero el tamaño de la pila en sí varía en función de la plataforma en la que esté trabajando. Ver When do you worry about stack size? para una pregunta muy similar.

Is static char buffer [N]; ¿Más rápido? ¿Hay algún otro argumento para o en su contra?

El significado de estática depende de donde se declara el buffer, pero supongo que usted está hablando de un static declarada dentro de una función, por lo que se inicializa una sola vez. En las funciones llamadas muchas veces, usar los búferes estáticos puede ser una buena idea para evitar el desbordamiento de la pila, pero por lo demás, tenga en cuenta que la asignación de búferes es una operación económica. Además, los búferes estáticos son mucho más difíciles de manejar cuando se trata de múltiples hilos.

Para respuestas a la mayoría de sus otras preguntas, vea Large buffers vs Large static buffers, is there an advantage?.

8
  1. Manténgase alejado de los búfers estáticos si alguna vez quiere usar su código de manera retrógrada.

  2. use snprintf() en lugar de sprintf() para que pueda controlar los desbordamientos de búfer.

  3. Nunca se sabe cuánto espacio de pila queda en el contexto de su llamada, por lo que ningún tamaño es técnicamente "seguro". Tienes mucho margen para jugar la mayor parte del tiempo. Pero esa vez te hará bien. Utilizo una regla general para nunca poner matrices en la pila.

  4. Haga que el cliente posea el buffer y páselo y su tamaño a su función. Eso lo hace reentrante y no deja ambigüedad sobre quién necesita administrar la vida del buffer.

  5. Si se trata de datos de cadena, compruebe dos veces las funciones de cadena para asegurarse de que terminan especialmente cuando tocan el final del búfer. La biblioteca C es muy inconsistente cuando se trata de manejar la terminación de cadena entre las diversas funciones.

+0

No puedo estar de acuerdo con el n. ° 3. ¡Conoce tu código!Sin los búferes asignados a la pila, siempre tendrá que usar un mutex en una aplicación de subprocesos múltiples, y yo personalmente intentaré lo más posible para evitar el mutex, o en otras palabras, escribir un código sin candado. Rendimiento garantizado. Si no conoce el comportamiento de su aplicación, tiene un problema (: – Poni

+0

@Poni: el bloqueo de Mutex no es la única alternativa para almacenar búferes cuando se mantiene la reentrada. Puede pasar búferes de la persona que llama, usar el montón directamente , o utilice grupos de almacenamientos intermedios de almacenamiento preajustados. –

+0

Sin embargo, siempre que acceda a la misma colección (que es el montón al final del día) de manera multiproceso, deberá sincronizar. (Casi) no forma de evitarlo – Poni

0
  • Buffer o & Buffer [0] es exactamente el mismo. Incluso puedes escribir Buffer + 0. Personalmente, prefiero solo escribir Buffer (y creo que la mayoría de los desarrolladores también prefieren esto), pero esta es tu elección personal
  • El máximo depende de cuán grande y profundo sea tu stack. Si ya tienes 100 funciones en la pila, el tamaño máximo será más pequeño.Si puede usar C++, podría escribir una clase de buffer que elija dinámicamente si usará la pila (para tamaños pequeños) o la pila (para tamaños grandes). Encontrarás el código a continuación.
  • Un búfer estático es más rápido ya que el compilador reservará el espacio para usted de antemano. Un buffer de pila también es rápido. Para un buffer de pila la aplicación solo tiene que aumentar el puntero de la pila. Para un almacenamiento intermedio de almacenamiento dinámico, el administrador de memoria tiene que encontrar espacio libre, solicitar al sistema operativo la nueva memoria, luego liberarla de nuevo, hacer algunos registros, ...
  • Si es posible, use cadenas de C++ para evitar fugas de memoria. De lo contrario, la persona que llama tiene que saber si tiene que liberar la memoria después o no. La desventaja es que las cadenas de C++ son más lentas que los búferes estáticos (ya que están asignados en el montón).
  • No utilizaría la asignación de memoria en variables globales. ¿Cuándo vas a eliminarlo? ¿Y puede estar seguro de que ninguna otra variable global necesitará la memoria asignada (y se utilizará antes de asignar su memoria intermedia estática)?
  • Cualquiera que sea el tipo de búfer que use, intente ocultar la implementación de la persona que llama de su función. Podría tratar de ocultar el puntero del búfer en una clase, deje que la clase recuerde si el búfer está dinámicamente asignado o no (y por lo tanto debería eliminarlo en su destructor o no). Después, es fácil cambiar el tipo de buffer, lo cual no puedes hacer si solo devuelves un char-pointer.
  • Personalmente prefiero las variantes sprintf normales, pero eso es probablemente porque todavía tengo muchos códigos anteriores y no quiero una situación mixta. En cualquier caso, considere usar snprintf, donde puede pasar el tamaño del búfer.
  • Código

de búfer de pila dinámica/heap:

template<size_t BUFSIZE,typename eltType=char> 
class DynamicBuffer 
    { 
    private: 
     const static size_t MAXSIZE=1000; 
    public: 
     DynamicBuffer() : m_pointer(0) {if (BUFSIZE>=MAXSIZE) m_pointer = new eltType[BUFSIZE];} 
     ~DynamicBuffer() {if (BUFSIZE>=MAXSIZE) delete[] m_pointer;}; 
     operator eltType *() { return BUFSIZE>=MAXSIZE ? m_pointer : m_buffer; } 
     operator const eltType *() const { return BUFSIZE>=MAXSIZE ? m_pointer : m_buffer; } 
    private: 
     eltType m_buffer[BUFSIZE<MAXSIZE?BUFSIZE:1]; 
     eltType *m_pointer; 
    }; 
1
está pasando el búfer como búfer y [0] mejor estilo de programación de pasar tampón? (Yo prefiero y buffer [0].)

&buffer[0] hace que el código menos legible para mí. Tengo que hacer una pausa por un segundo y me pregunto por qué alguien lo usó en lugar de simplemente pasar el buffer. A veces tiene que usar &buffer[0] (si buffer es std::vector), pero de lo contrario, cumpla con el estilo C estándar.

¿Hay un tamaño máximo que se considera seguro para los almacenamientos intermedios asignados a la pila?

Dudo que haya algún límite práctico, siempre y cuando esté utilizando la pila de manera razonable. Nunca he tenido ningún problema en mi desarrollo.

Si estoy leyendo MSDN correctamente, los hilos en Windows tienen un tamaño predeterminado de 1MB. Esto es configurable. Otras plataformas tienen otros límites.

Is static char buffer [N]; ¿Más rápido? ¿Hay otros argumentos a favor o en contra?

Por un lado, podría reducir la necesidad de asignar páginas de memoria para la pila, por lo que su aplicación podría funcionar más rápido. Por otro lado, ir al BSS segment o su equivalente podría reducir la localidad de caché en comparación con la pila, por lo que su aplicación podría funcionar más despacio. Dudo seriamente que noten la diferencia de cualquier manera.

El uso de static no es seguro para los hilos, mientras se usa la pila. Esa es una gran ventaja para la pila. (Incluso si no crees que serás multiproceso, ¿por qué hacer la vida más difícil si eso cambia en el futuro?)

Al utilizar memorias intermedias estáticas puede hacer que su función vuelva a tener el tipo const char * return. ¿Es esta una buena idea? (Me doy cuenta de que la persona que llama tendrá que hacer su propia copia para evitar que la próxima llamada cambie el valor de retorno anterior.)

Const correctness siempre es una buena cosa.

Devolver punteros a almacenamientos intermedios estáticos es propenso a errores; una llamada posterior puede modificarlo, otro hilo podría modificarlo, etc. Use std::string lugar o en otra memoria de auto-asignado su lugar (incluso si su función tiene que lidiar internamente con tampones de char como su ejemplo GetCurrentDirectory.)

Lo sobre el uso de static char * buffer = new char [N]; y nunca borrar el buffer? (Reutilizando el mismo búfer en cada llamada)

Menos eficiente que simplemente usar static char buffer[N], ya que necesita una asignación de pila.

Entiendo que la asignación de montón debe utilizarse cuando (1) se trata de grandes almacenamientos intermedios o (2) el tamaño máximo de almacenamiento intermedio se desconoce en tiempo de compilación. ¿Hay algún otro factor que juegue en la decisión de asignación pila/montón?

Ver la respuesta de Justin Ardini.

¿Prefiere las variantes sprintf_s, memcpy_s, ...? (Visual Studio ha estado tratando de convencerme de esto durante mucho tiempo, pero quiero una segunda opinión: p)

Esto es un tema de debate. Personalmente, creo que estas funciones son una buena idea, y si se está enfocando en Windows exclusivamente, entonces tiene algún beneficio tomar el enfoque preferido de Windows y usar esas funciones. (Y son bastante sencillos de volver a implementar si luego tiene que apuntar a algo que no sea Windows, siempre y cuando no confíe en su comportamiento de manejo de errores.) Otros piensan que las funciones de Secure CRT no son más seguras que las que se usan correctamente. C e introducir otras desventajas; Wikipedia links a algunos argumentos en contra de ellos.

0

Is passing the buffer as &buffer[0] better programming style than passing buffer? (I prefer &buffer[0].)

Depende de las normas de codificación. Yo personalmente prefiero: buffer + index en lugar de &buffer[index], pero es una cuestión de gusto.

Is there a maximum size that is considered safe for stack allocated buffers?

Depende del tamaño de la pila. Si la cantidad de stack necesaria para su buffer excede la cantidad disponible en la pila, resulta un stack-overflow.

Is static char buffer[N]; faster? Are there any other arguments for or against it?

Sí, debe ser más rápido. Ver también esta pregunta: Is it bad practice to declare an array mid-function

When using static buffers you can have your function return have the const char * return type. Is this a good idea? (I do realize that the caller will need to make his own copy to avoid that the next call would change the previous return value.)

No está seguro de lo que significa en este caso que estática:

  1. Si variable se declara en la pila (char buf[100]): Usted debe no devolver referencias a cosas que están declaradas en la pila. Se eliminarán en la siguiente llamada/declaración de función (por ejemplo, cuando la pila se usa nuevamente).

  2. Si la variable se declara como estática static hará que su código no sea reentrante. strtok es un ejemplo en este caso.

What about using static char * buffer = new char[N]; and never deleting the buffer? (Reusing the same buffer each call.)

es una posibilidad, aunque no se recomienda, ya que hace su código non-reentrant.

I understand that heap allocation should be used when (1) dealing with large buffers or (2) maximum buffer size is unknown at compile time. Are there any other factors that play in the stack/heap allocation decision?

Tamaño de la pila del hilo conductor es demasiado pequeño como para caber declaración pila (anteriormente mencionado).

Should you prefer the sprintf_s, memcpy_s, ... variants? (Visual Studio has been trying to convince me of this for a long time, but I want a second opinion :p)

si desea que su código sea portable: No. Pero el esfuerzo en la creación de una macro portátil es bastante pequeño en este caso:

// this is not tested - it is just an example 
#ifdef _WINDOWS 
#define SPRINTF sprintf_s 
#else 
#define SPRINTF sprintf 
#endif 
+0

su idea de mcaro para la portabilidad no funcionará, ya que la mayoría de las versiones toman argumentos adicionales. – smerlin

1

Si una función es un método de sabiendo cuántos caracteres devolverá, úselo. Su GetCurrentDirectory muestra es un buen ejemplo:

DWORD length = ::GetCurrentDirectory(0, NULL); 

A continuación, puede utilizar una matriz asignada dinámicamente (ya sea de cadena o vector) para obtener el resultado:

std::vector<TCHAR> buffer(length, 0); 
// assert(buffer.capacity() >= length); // should always be true 
GetCurrentDirectory(length, &buffer[0]); 
1

1) buffer y &buffer[0] deben ser equivalentes.

2) Los límites de tamaño de pila dependerán de su plataforma. Para la mayoría de las funciones simples, mi regla empírica personal es cualquier cosa que sobre ~ 256KB se declara dinámicamente; sin embargo, no hay ninguna razón o rima para ese número, es solo mi propia convención y actualmente está dentro de los tamaños de pila predeterminados para todas las plataformas en las que desarrollo.

3) Los búfers estáticos no son más rápidos o más lentos (para todos los efectos). La única diferencia es el mecanismo de control de acceso. El compilador generalmente coloca los datos estáticos en una sección separada del archivo binario que los datos no estáticos, pero no hay ningún beneficio de rendimiento o penalización apreciable. La única manera real de decirlo con certeza es escribir el programa en ambos sentidos y cronometrarlos (ya que muchos de los aspectos de velocidad involucrados aquí dependen de su plataforma/compilador).

4) No devuelva un puntero const si la persona que llama tendrá que modificarlo (que derrota el punto de const). Use const para los parámetros de funciones y los tipos de retorno si y solo si no están diseñados para ser modificados. Si la persona que llama debe modificar el valor, la mejor opción es que la persona que llama pase la función un puntero a un búfer preasignado (junto con el tamaño del búfer) y para que la función escriba los datos en ese búfer.

5) Reutilizando un tampón puede dar lugar a una mejora de rendimiento para los búferes más grandes debido a pasar por la sobrecarga que está implicado en llamar malloc/free o new/delete cada vez. Sin embargo, corre el riesgo de utilizar accidentalmente datos antiguos si olvida borrar el búfer cada vez o si intenta ejecutar dos copias de la función en paralelo. Una vez más, la única forma real de saberlo con certeza es intentarlo de ambas maneras y medir cuánto tarda el código en ejecutarse.

6) Otro factor en la asignación de pila/montón es el alcance. Una variable de pila sale del ámbito cuando devuelve la función en la que vive, pero una variable que se asignó dinámicamente en el montón puede devolverse a la persona que llama de forma segura o acceder a ella la próxima vez que se llame a la función (a la strtok).

7) Yo recomendaría contra el uso de sprintf_s, memcpy_s, y amigos. No son parte de la biblioteca estándar y no son portátiles. Cuanto más utilice estas funciones, más trabajo extra tendrá cuando desee ejecutar su código en una plataforma diferente o utilizar un compilador diferente.

+0

Por supuesto, el comentario en el n. ° 6 no se aplica a las variables de pila declaradas como 'estáticas'. – bta