Notas de lectura de "Programación avanzada en C++" (Trece: Secreto de E/S de C++)

1. Referencias

2. Se recomienda leer el libro "21 Días para aprender C++" para empezar, el enlace de las notas es el siguiente

1. Uso de flujos

1.1 El significado de corriente

  • Flujos predefinidos en C++

inserte la descripción de la imagen aquí

  • La diferencia entre un flujo con búfer y un flujo sin búfer es que, en lugar de enviar los datos al destino de inmediato, el primero almacena en búfer los datos entrantes y los envía en fragmentos, mientras que un flujo sin búfer envía los datos al destino de inmediato.
    • El propósito del almacenamiento en búfer generalmente es mejorar el rendimiento y, para algunos destinos (como archivos), es más rápido escribir fragmentos más grandes a la vez.
    • Tenga en cuenta que los búferes siempre se pueden vaciar usando el método flush(), lo que obliga a un flujo almacenado en el búfer a enviar todos sus datos almacenados actualmente en el búfer al destino.
  • Todos los flujos de entrada tienen una fuente asociada y todos los flujos de salida tienen un destino asociado
  • Otro punto importante sobre las secuencias: las secuencias contienen no solo datos ordinarios, sino también datos especiales llamados posición actual . La posición actual se refiere a la posición donde la transmisión realizará la siguiente operación de lectura y escritura.

1.2 Fuentes y destinos de los flujos

  • Los flujos se pueden aplicar a cualquier objeto que reciba o genere datos. Los flujos en C++ pueden usar tres orígenes y destinos comunes: consola, archivo y cadena.
    • 1. Consola
      • El flujo de entrada de la consola permite que el programa obtenga información del usuario en tiempo de ejecución, lo que hace que el programa sea interactivo.
      • El flujo de salida de la consola proporciona retroalimentación y salida al usuario
    • 2. Archivo : el flujo de archivos lee datos del sistema de archivos y escribe datos en el sistema de archivos
      • El flujo de entrada de archivos es adecuado para leer datos de configuración, leer archivos guardados y también para tareas como el procesamiento por lotes de datos basados ​​en archivos.
      • El flujo de salida del archivo es adecuado para tareas como guardar datos de estado y proporcionar salida
      • El flujo de archivos contiene las funciones de salida del lenguaje C fprintf(), fwrite() y fputs(), y las funciones de entrada fscanf(), fread() y fgets()
    • 3. Cadenas : las secuencias de secuencias son un ejemplo de aplicación de la metáfora de la secuencia al tipo de secuencia.
      • Al usar flujos de cadenas, los datos de caracteres se pueden procesar como cualquier otro flujo
      • El uso de la sintaxis fluida brinda oportunidades de optimización y es más conveniente que usar la clase de cadena directamente.
      • Los flujos de cadenas contienen la funcionalidad de sprintf(), sprintf_s() y sscanf(), así como la funcionalidad de muchas de las funciones de formato de cadenas del lenguaje C.

1.3 Salida de flujo

1.3.1 Concepto básico de salida
  • El flujo de salida se define en el archivo de encabezado <ostream> y la mayoría de los programas incluyen el archivo de encabezado <iostream>, que contiene los archivos de encabezado para los flujos de entrada y salida.
  • La forma más fácil de usar un flujo de salida es usar el operador <<
    int i = 7;
    cout << i << endl;
    
    char ch = 'a';
    cout << ch << endl;
    
    string myString = "Hello World!";
    cout << myString << endl;
    
    // 输出
    7
    a
    Hello World!
    
  • La secuencia cout es una secuencia integrada que escribe en la consola, también conocida como salida estándar. Los usos de << se pueden encadenar para generar múltiples segmentos de datos. Esto se debe a que el operador << devuelve una referencia a una secuencia, por lo que el operador << se puede volver a aplicar inmediatamente en la misma secuencia
    int j = 11;
    cout << "The value of j is " << j << "!" << endl;
    
    // 输出
    The value of j is 11!
    
  • La diferencia entre \n y endl es que \n solo inicia una nueva línea, mientras que endl también actualiza el búfer
    • Tenga cuidado al usar endl, ya que el vaciado excesivo del búfer puede degradar el rendimiento.
1.3.2 El método de flujo de salida
  • poner () y escribir ()
    • put() acepta un solo carácter, write() acepta una matriz de caracteres
    • Los datos pasados ​​a estos métodos se envían en su forma original, sin operaciones especiales de formato y procesamiento.
    cout.put('a');
    
    const char* test = "hello there\n";
    cout.write(test, strlen(test));
    
  • enjuagar()
    • Al escribir datos en un flujo de salida, el flujo no necesariamente escribe los datos en el destino de inmediato. La mayoría de los flujos de salida están almacenados en búfer, es decir, acumulan datos en lugar de escribirlos inmediatamente. El propósito del almacenamiento en búfer suele ser mejorar el rendimiento y, para algunos destinos (como archivos), es más rápido escribir fragmentos más grandes a la vez que escribir carácter por carácter (pero el flujo de salida cerr no almacena en caché su salida )
    • La forma de pedirle explícitamente a una transmisión que vacíe el caché es llamar al método flush() de la transmisión.
    cout << "abc";
    cout.flush();
    cout << "def";
    cout << endl;
    
1.3.3 Manejo de errores de salida
  • Los errores de salida pueden ocurrir en varias situaciones: intentar abrir un archivo que no existe; operaciones de escritura que fallan debido a un error de disco, como un disco lleno

  • Llame al método good() de la transmisión para determinar si la transmisión se encuentra actualmente en un estado normal

    if (cout.good()) {
          
          
        cout << "All good" << endl;
    }
    
  • El método fail() devuelve verdadero si la operación más reciente falló, pero no dice si la próxima operación también fallará. Por ejemplo, después de llamar a flush() en un flujo de salida, llame a fail() para asegurarse de que el flujo aún esté disponible

    cout.flush();
    if (cout.fail()) {
          
          
        cerr << "Unable to flush to standard out" << endl;
    }
    
  • Los flujos tienen un operador de conversión que convierte a tipo bool. El operador de conversión devuelve el mismo resultado que al llamar a !fail(). Por lo tanto, el fragmento de código anterior se puede reescribir como

    cout.flush();
    if (!cout) {
          
          
        cerr << "Unable to flush to standard out" << endl;
    }
    
  • También se puede requerir que los flujos generen excepciones cuando ocurran fallas . A continuación, escriba un controlador catch para detectar la excepción ios_base::failure y, a continuación, llame al método what() en esta excepción para obtener la descripción del error y llame al método code() para obtener el código de error.

    cout.exceptions(ios::failbit | ios::badbit | ios::eofbit);
    try {
          
          
        cout << "Hello World." << endl;
    } catch (const ios_base::failure& ex) {
          
          
        cerr << "Caught exception: " << ex.what() << ", error code = " << ex.code() << endl;
    }
    
  • El estado de error de la transmisión se restablece mediante el método clear()

    cout.clear();
    

1.4 Entrada de transmisión

1.4.1 Concepto básico de entrada
  • Con un flujo de entrada, hay dos formas sencillas de leer datos. El primer método es similar al operador <<, << envía datos al flujo de salida. El operador correspondiente a los datos de lectura es >>, al leer datos del flujo de entrada a través de >>, la variable proporcionada por el código guarda el valor recibido
    string userInput;
    cin >> userInut;
    cout << "User input was " << userInput << endl;
    
    • De forma predeterminada, el operador >> tokeniza los valores de entrada en función de los caracteres de espacio en blanco . Por ejemplo, si el usuario ejecuta el programa anterior y escribe hola allí como entrada, solo los caracteres antes del primer carácter de espacio en blanco (carácter de espacio en este ejemplo) se almacenarán en la variable userInput y la salida será la siguiente
    User input was hello // 在输入中包含空白字符的一种方法是使用 get()
    
  • Se pueden leer múltiples valores a través de un flujo de entrada, y los tipos se pueden mezclar y combinar según sea necesario
    • El operador >> tokeniza contra espacios en blanco, por lo que la función getReservationData() no permite que se ingresen nombres con espacios en blanco. Una solución es usar el método unget()
    void getReservationData {
          
          
        string guestName;
        int partySize;
        cout << "Name and number of guests: ";
        cin >> guestName >> partySize; // 使用 cin 会立即刷新 cout 缓存区
        cout << "Thank you, " << guestName << "." << endl;
    }
    
1.4.2 Manejo de errores de entrada
  • El flujo de entrada proporciona algunos métodos para detectar condiciones anormales. La mayoría de las condiciones de error relacionadas con los flujos de entrada ocurren cuando no hay datos para leer. Por ejemplo, es posible llegar al final de un flujo (llamado fin de archivo, aunque no sea un flujo de archivos). La forma más común de consultar el estado de un flujo de entrada es acceder al flujo de entrada en una declaración condicional . Por ejemplo, el siguiente ciclo continúa mientras cin permanezca en un estado "bueno".
    while (cin) {
          
          
        // ...
    }
    
    // 同时还可输入数据
    while (cin >> ch) {
          
          
        // ...
    }
    
  • Los métodos good(), bad() y fail() también se pueden llamar en flujos de entrada, al igual que los flujos de salida
1.4.3 Método de entrada
  • conseguir()

    • El método get() permite leer datos de entrada sin procesar de la secuencia. La versión más simple de get() devuelve el siguiente carácter en la secuencia, otras versiones leen varios caracteres a la vez. get() se usa a menudo para evitar la tokenización automática del operador >> ( puede contener espacios )
    string readName(istream& stream) {
          
          
        string name;
        while (stream) {
          
          
            int next = stream.get();
            // ...
        }
    }
    
  • furia()

    • Llamar a unget() hace que la secuencia retroceda una posición, volviendo a colocar el carácter anterior leído en la secuencia . Llame al método fail() para ver si unget() tuvo éxito. Por ejemplo, unget() fallará si la posición actual es el comienzo de la transmisión.
  • volver()

    • putback(), como unget(), permite retroceder un carácter en el flujo de entrada . La diferencia es que el método putback() recibe como parámetro los caracteres que se devuelven al flujo
    char ch1;
    cin >> ch1;
    cin.putback('e'); // 'e' 将是从流中读出的下一个字符
    
  • ojeada()

    • Use el método peek() para obtener una vista previa del siguiente valor devuelto después de llamar a get()
    • Ideal para: Cuando necesita buscar un valor antes de leerlo
  • obtener línea()

    • Obtener una línea de datos del flujo de entrada es un requisito común y existe un método para realizar esta tarea: el método getline() llena el búfer de caracteres con una línea de datos hasta el tamaño especificado , y el tamaño especificado incluye \ 0 caracteres. Entonces, el código a continuación lee como máximo kBufferSize: 1 carácter desde cin, o hasta el final de la línea
    char buffer[kBufferSize] = {
          
          0};
    cin.getline(buffer, kBufferSize);
    
    • También hay una función std::getline() para cadenas C++ . Esta función, definida en el archivo de encabezado <string> y en el espacio de nombres estándar, toma como argumentos una referencia de flujo, una referencia de cadena y un delimitador opcional. La ventaja de esta versión de la función getline() es que no es necesario especificar el tamaño del búfer
    string myString;
    std::getline(cin, myString);
    

2. Flujo de cuerdas

  • La semántica de flujo se puede aplicar a las cadenas a través de stringstreams, de esta manera, se puede obtener un flujo en memoria para representar datos textuales.
    • Por ejemplo, en una aplicación GUI, puede ser necesario usar secuencias para construir datos de texto , pero en lugar de enviar el texto a la consola o a un archivo, los resultados se muestran en elementos GUI como cuadros de mensaje y cuadros de edición.
    • Otro ejemplo es pasar un flujo de cadena como argumento a diferentes funciones mientras se mantiene la posición de lectura actual para que cada función pueda procesar la siguiente parte del flujo. Los flujos de cadenas también son muy adecuados para analizar texto, porque los flujos tienen capacidades de tokenización integradas.
  • La clase std::ostringstream se usa para escribir datos en una cadena y la clase std:istringstream se usa para leer datos de una cadena. Ambas clases se definen en el archivo de encabezado <sstream> . ostringstream e istringstream heredan el mismo comportamiento que ostream e istream respectivamente
    ostringstream outStream;
    while (cin) {
          
          
        string nextToken;
        cin >> nextToken;
        outStream << nextToken << "\t";
    }
    
    Muffin creatMuffin(istringstream& stream) {
          
          
        string description;
        int size;
        bool hasChips;
    
        // ...
    
        stream >> description >> size >> boolalpha >> hasChips;
    }
    

En comparación con las cadenas estándar de C++, la principal ventaja de los flujos de cadenas es que, además de los datos, este objeto también sabe dónde realizar la siguiente operación de lectura o escritura. Esta posición también se denomina posición actual. En comparación con las cadenas, otra ventaja de los flujos de cadenas es que admiten operadores y localización, y las funciones de formato son más potentes.

3. Flujo de archivos

  • El archivo en sí es muy consistente con la abstracción de la secuencia, porque al leer y escribir un archivo, además de los datos, también involucra la ubicación de la lectura y escritura. En C++, las clases std:ofstream y std::ifstream proporcionan funciones de entrada y salida de archivos. Estas dos clases se definen en el archivo de encabezado <fstream>
  • La única diferencia importante entre un flujo de archivos de salida y otros flujos de salida es que el constructor de un flujo de archivos puede recibir un nombre de archivo y un modo para abrir el archivo como parámetros.
    • El modo predeterminado es escribir el archivo (ios base:out), este modo escribe el archivo desde el principio del archivo, sobrescribiendo cualquier dato existente
    • Asigne la constante ios base::app al segundo parámetro del constructor de flujo de archivos, y también abra el flujo de archivos de salida en modo de adición

inserte la descripción de la imagen aquí

  • Modos componibles, por ejemplo, si desea abrir un archivo para la salida (en modo binario) mientras trunca los datos existentes, puede especificar el modo abierto de la siguiente manera

    ios_base::out | ios_base::binary | ios_base::trunc
    
  • ifstream incluye automáticamente el esquema ios_base::in y ofstream incluye automáticamente el esquema ios_base::out, incluso si no especifica explícitamente in o out como esquema. El siguiente programa abre el archivo test.txt y genera los parámetros del programa. Los destructores ifstream y ofstream cierran automáticamente el archivo subyacente, por lo que no es necesario llamar explícitamente a close()

    int main() {
          
          
        ofstream outFile("test.txt", ios_base::trunc);
        if (!outFile.good()) {
          
          
            cerr << "..." << endl;
            return -1;
        }
        // ...
    }
    

3.1 Modo texto y modo binario

  • De forma predeterminada, los flujos de archivos se abren en modo de texto. Si se especifica el indicador ios_base::binary, el archivo se abrirá en modo binario. En modo binario, se solicita que los bytes transmitidos se escriban en un archivo . Al leer, los bytes se devuelven exactamente como están en el archivo
  • En el modo de texto, se realizan algunas conversiones implícitas y cada línea escrita o leída del archivo termina con \n

3.2 Mover el archivo con seek() y tell()

  • Todos los flujos de entrada y salida tienen métodos seek() y tell()
    • El método seek() permite moverse a una posición arbitraria en el flujo de entrada o salida
    • La posición actual de la secuencia se puede consultar a través del método tell() , que devuelve un valor streampos que representa la posición actual. Con este resultado, puede recordar la posición de la marca actual antes de ejecutar seek(), y también puede consultar si se encuentra en una posición específica.

3.3 Encadenamiento de flujos

  • Se pueden establecer enlaces entre cualquier flujo de entrada y flujo de salida , lo que permite un comportamiento de "actualización al acceder". En otras palabras, el flujo de salida vinculado se vacía automáticamente cuando se solicitan datos del flujo de entrada. Este comportamiento está disponible para todos los flujos, pero es especialmente útil para los flujos de archivos que pueden depender unos de otros.

  • El encadenamiento de flujos se realiza a través del método tie(). Para vincular un flujo de salida a un flujo de entrada, llame al método tie() en el flujo de entrada, pasando la dirección del flujo de salida. Para desvincular, pase nullptr

  • El siguiente programa vincula el flujo de entrada de un archivo con el flujo de salida de un archivo completamente diferente, y también puede vincular el flujo de salida del mismo archivo, pero la E/S bidireccional puede ser una forma más elegante de lograr lectura y escritura simultáneas. del mismo archivo

    ifstream inFile("input.txt");
    ofstream outFile("output.txt");
    
    inFile.tie(&outFile); // 建立链接
    
    outFile << "Hello there!"; // outFile 未被刷新,因为 std::endl 未发送
    
    // 会触发 flush()
    string nextToken;
    inFile >> nextToken; // outFile 被刷新
    
  • Este mecanismo se puede usar para mantener sincronizados dos archivos relacionados: cada vez que se escribe un archivo, los datos almacenados en caché enviados al otro archivo se vacían.

    • Un ejemplo de este enlace de flujo es el enlace entre cout y cin. Cada vez que se ingresan datos desde cin, cout se actualiza automáticamente
    • También hay un vínculo entre cerr y cout, lo que significa que cualquier salida a cerr hará que cout se elimine, mientras que clog no está vinculado a cout.

4. E/S bidireccional

  • Por ahora, este capítulo analiza los flujos de entrada y salida como clases separadas pero relacionadas. De hecho, hay un flujo que puede realizar la entrada y la salida al mismo tiempo, y un flujo bidireccional puede funcionar como un flujo de entrada y un flujo de salida al mismo tiempo.
  • Un flujo bidireccional es una subclase de iostream, que es una subclase de istream y ostream, por lo que este es un ejemplo de herencia múltiple . Aparentemente, los flujos bidireccionales admiten operadores >> y <<, así como métodos para flujos de entrada y salida.
  • La clase fstream proporciona transmisión de archivos bidireccional. fstream es especialmente útil para aplicaciones que necesitan reemplazar datos en un archivo , porque la ubicación correcta se puede encontrar leyendo el archivo y luego cambiar inmediatamente a escribir en el archivo
  • También se puede acceder a los flujos de cadenas bidireccionalmente a través de la clase stringstream

Los flujos bidireccionales usan punteros separados para almacenar las posiciones de lectura y escritura. Al cambiar entre lectura y escritura, debe colocarse en la posición correcta

Supongo que te gusta

Origin blog.csdn.net/qq_42994487/article/details/131451279
Recomendado
Clasificación