4.1 Cadenas de caracteres: strings



Vimos que para guardar caracteres de texto (letras)  usábamos datos de tipo char. Cuando se requieren guardar palabras o frases, se vuelve imposible hacerlo de esta manera. (Imaginemos sólo la complicación para guardar un listado de nombres: Habría que generar una variable para cada letra, pero a su vez cada nombre podría contener una cantidad distinta de letras, etc.)
Para guardar frases de texto, en C++ se pueden utilizar los datos de tipo string., también llamados cadenas o cadenas de caracteres.

#include <iostream>

using namespace std;



int main()

{

    string uno;

    uno = "¡Hola Mundo!";

    cout << uno << endl;


En este ejemplo, definimos una variable(*) de tipo string (la variable uno), le asignamos un valor ("¡Hola Mundo!"), y la mostramos por pantalla con la instrucción cout.
Análogamente, se puede ingresar un valor desde teclado con la instrucción cin:

cin >> uno;

Pero la instrucción cin considera el espacio en blanco como un separador de variables, por lo que la variable ingresada sólo podrá tener una palabra: Lo que se ingrese luego del espacio en blanco, será considerado otra variable diferente.
Como ejemplo, veamos el siguiente programa:

#include <iostream>

using namespace std;



int main()

{

    string nombre;

    cout << "Ingrese su nombre:"<< endl;

    cin >> nombre;

    cout << "¡Hola "<< nombre <<"!" << endl;

}


Si se ingresa como nombre “María”, se obtiene la respuesta esperada “¡Hola María!”. En cambio, si se ingresa como nombre “Juan Pablo”, la respuesta obtenida será “¡Hola Juan!”
Para evitar este problema, se utiliza la instrucción getline:

#include <iostream>

using namespace std;


int main()

{

    string nombre;

    cout << "Ingrese su nombre:"<< endl;

    getline (cin, nombre);

    cout << "¡Hola "<< nombre <<"!" << endl;

}


Esta instrucción sólo acepta el fin de línea (”Enter”) como separador de variables, así que toda la línea que se ingrese será interpretada como una sola variable de tipo string.



También es posible realizar comparaciones con datos de tipo string:

#include <iostream>

using namespace std;


int main()

{

    string clave, codigo;

    codigo = "qwertyuiop";

    do

    {

        cout << "Ingrese su contraseña:";

        cin >> clave;

    }

    while (clave != codigo);

    cout << "Bienvenido!";

}

Este programa seguirá repitiendo el ciclo do…while hasta que la clave ingresada por el usuario sea igual al código escrito por el programador.
Todos los operadores lógicos son válidos con variables de tipo string. También es posible usar la comparación para ver si un string es menor o mayor a otro. Un string se considera menor a otro si alfabéticamente es anterior. (O sea, si está antes en el diccionario.)
En el siguiente programa:

#include <iostream>
using namespace std;

int main()
{
    string secreto = "elefante";
    string intento;
    int i=0;
    do
    {
    cout<<"Realice un intento:";
    cin>>intento;
    i++;
    if (secreto<intento)
        cout <<"La palabra secreta es anterior."<<endl;
    else
        if (secreto>intento)
            cout<<"La palabra secreta es posterior."<<endl;
    }
    while(secreto!=intento);
    cout <<"Felicitaciones. Acertaste en "<<i<<" intentos.";

}

El usuario debe ingresar palabras intentando adivinar la palabra secreta "elefante". El programa responderá a cada intento si la palabra secreta es menor (anterior) o mayor (posterior) e irá contando la cantidad de intentos realizados.



(*) Estrictamente hablando, los strings no son variables ni datos, sino que son objetos. Un objeto es una variable que tiene además de datos, tiene métodos (funciones) que le son propios y trabajan sobre esos datos. Hay todo un paradigma de programación que trabaja con ellos, la Programación Orientada a Objetos (POO) y su explicación y desarrollo ocuparía un curso mucho más extenso que éste. Por ello, la simplificación de considerarlos simplemente como variables.

4.2 Operaciones con strings

Para realizar diferentes operaciones con los datos de tipo string, hay varias funciones que podemos utilizar.
  (Como mencionamos anteriormente, los string en realidad no son variables, sino que son objetos, por lo que técnicamente no poseen funciones sino métodos. Los métodos de un objeto actúan de forma análoga a las funciones y, dentro del alcance de este curso, podemos llamarlos con el mismo nombre.)

La primera de ellas es la función length(), que devuelve un entero indicando la cantidad de caracteres que posee el string analizado.

#include <iostream>
using namespace std;
int main()
{
    string nombre;
    int longitud;
    cout << "Ingrese su nombre:"<< endl;
    getline (cin, nombre);
    cout << "¡Hola "<< nombre <<"!" << endl;
    longitud = nombre.length();
    cout << "Tu nombre tiene "<< longitud <<" caracteres."<<endl<<endl;
}

El programa anterior pide al usuario que ingrese un nombre que se guarda en un string. En la línea resaltada en amarillo se llama al método length(). La forma de invocar a un método es algo distinta a la forma en que llamábamos a las funciones hasta ahora. Se coloca el nombre del string, un punto y el nombre del método (en este caso, length). Como el método pertenece al string nombre, no es necesario pasárselo como parámetro: el método ya sabe que se aplica a nombre. Length devuelve un entero indicando la cantidad de caracteres del nombre, que se guarda en la variable int longitud.

Subcadenas

La función substr() permite cortar una parte (subcadena) del string. Veremos su uso en el siguiente ejemplo:

#include <iostream>
using namespace std;

int main()
{
    string nombre;
    string apodo;
    cout << "Ingrese su nombre:"<< endl;
    getline (cin, nombre);
    cout << "¡Hola "<< nombre <<"!" << endl;

    cout <<" Vamos a buscarte un apodo"<<endl;
    for (int i = 0;i<=nombre.length()-3;i++)
    {
    apodo = nombre.substr(i,3);
    cout << "¡Hola "<< apodo <<"..." << endl;
    }
    cout<< "¿Cual te gusta mas?"<<endl;

}

La función substr() recibe dos parámetros de tipo int. El primero indica a partir de qué posición del string se quiere generar la subcadena, y el segundo indica la longitud de dicha subcadena (la cantidad de caracteres que se van a copiar). La función devuelve otro string en donde se guarda la subcadena generada. 

Concatenación

Para concatenar (unir) dos strings, se pueden usar los operadores "+" ó "+=", como se puede ver en el siguiente ejemplo:

#include <iostream>
using namespace std;
int main()
{
string s1,s2,s3;
s1 = "Hola";
s2 = "gato";
s3 = s1 + " " + s2;
cout<<s3<<endl;
s3 += "rade";
cout<<s3<<endl;
return 0;
}


Otras operaciones

Hay muchas otras operaciones que se pueden hacer con strings. Algunos de los métodos que permiten hacerlo son:

find: encontrar un caracter  o un conjunto de caracteres dentro de un string.
swap: intercambiar el contenido de dos strings.
erase: borrar el contenido de un string.
replace: reemplazar partes de un string.
insert: insertar un texto en el medio de un string

4.3 Estructuras

Vimos hasta ahora variables simples, que son aquellas que en un momento dado pueden guardar un único valor; y vimos después variables compuestas (vectores y matrices) que puede guardar varios valores simultáneamente, con el único requisito de que sean todos del mismo tipo.
Ahora veremos las variables de tipo struct (estructuras) que permiten guardar varios valores simultáneamente, pudiendo ser éstos de diferentes tipos.

Imaginemos por ejemplo que queremos guardar una agenda de contactos. Los datos a guardar van a ser de diferentes tipos: Nombre, apellido, calle, número, teléfono, celular, email, etc. Además de ser de distinto tipo tipo, muchos de esos datos están relacionados entre sí: de poco nos sirve saber la calle sin el número de la casa o viceversa.

 Para estos casos, utilizamos las estructuras, como en el ejemplo siguiente:

struct contacto
{
    string nombre;
    string apellido;
    string calle;
    int numero;
    string telefono;
    string celular;
    string email;
};

A partir de esta definición, contacto es un tipo de dato más, puedo crear variables de este tipo, cada una de las cuales tendrá sus campos individuales para guardar el nombre, el apellido, etc.

En el siguiente programa, se crea una struct llamada contacto con todos los campos mencionados, se define una variable llamada a de tipo contacto, luego el usuario ingresa los valores de los distintos campos de la estructura y por último se muestra la estructura por pantalla.

#include <iostream>
#include <cstdio>
using namespace std;
struct contacto
{
    string nombre;
    string apellido;
    string calle;
    int numero;
    string telefono;
    string celular;
    string email;
};

int main()
{
    setlocale(LC_ALL, "spanish"); // Para poder incluir acentos y eñes en los textos
    contacto a;
    cout<<"Ingrese los siguientes datos del contacto:"<<endl;
    cout<<"Apellido: ";
    getline (cin,a.apellido);
    cout<<"Nombre: ";
    getline (cin,a.nombre);
    cout<< "Domicilio"<<endl<<"Calle: ";
    getline (cin,a.calle);
    cout<<"Número: ";
    cin>> a.numero;
    fflush(stdin); //para que al ingresar los datos no saltee el celular
    cout<<"Celular: ";
    getline (cin,a.celular);
    cout<<"Teléfono(fijo):" ;
    getline (cin, a.telefono);
    cout<<"Correo electrónico: ";
    cin >> a.email;
    cout<<endl<<endl<<endl;
    cout<<"Los datos ingresados son:"<<endl<<endl;
    cout<<a.apellido<<", "<<a.nombre<<endl;
    cout<<"Domicilio: "<<a.calle<<" "<<a.numero<<endl;
    cout<<"Tel:       "<<a.telefono<<endl;
    cout<<"Cel:       "<<a.celular<<endl;
    cout<<"Email:     "<<a.email<<endl<<endl;
    return 0;
}

Las variables de tipo struct se pueden copiar con la instrucción de asignación, se pueden pasar como parámetros a una función. En cambio no es posible compararlas (no es posible definir cuándo un struct es menor a otro) ni es posible mostrarlas por pantalla o ingresarlas con cout y cin. Estas operaciones se deben hacer individualmente campo por campo. Para acceder a cada campo de la estructura se escribe el nombre de la variable, un punto y el nombre del campo.

Los campos de un struct pueden ser de cualquier tipo de dato, incluso de otra estructura definida previamente. Por ejemplo, se le puede agregar a contacto el campo fecha de nacimiento, de tipo fecha:

struct fecha
{
    int dia;
    int mes;
    int anio;    
};

struct contacto
{
    string nombre;
    string apellido;
    string calle;
    int numero;
    string telefono;
    string celular;
    string email;
    fecha fnac;
};

Para ingresar el año de nacimiento, se deberá ingresar:

cin>>a.fnac.anio;

También se pueden definir vectores de estructuras:

contacto b[10];

cin >>b[i].nombre;

O estructuras uno de cuyos campos es un vector:

struct alumno
{
     int padron;
     int notas [15];
};

alumno x;

for (i=0;i<15;i++)
{
     cout<< x.notas[i];

Estas variables combinadas con vectores nos permiten crear registros de datos mucho más complejos, lo que nos permite representar problemas más dificiles y con mucha  más información. Para no tener que ingresar esta información manualmente cada vez que se ejecuta el programa, se guardará en archivos, que permiten conservar la información aún después de cerrar el programa y apagar la computadora.  

4.4 Escritura de archivos

Una de las opciones más interesantes de la programación es la posibilidad de guardar el resultado de los programas en archivos, que podrán ser utilizados más adelante o compartidos con otros usuarios. Hay varias formas de manejar archivos, vamos a ver la más sencilla que son los archivos de texto.
La ventaja de los archivos de texto es que pueden ser leídos y generados desde cualquier editor de textos, no tienen formatos adicionales. La principal  desventaja es que no puedo acceder a un dato específico en el archivo, para encontrar un dato se debe leer todo el archivo (o por lo menos, todos los datos anteriores del archivo) Por esta razón se llaman archivos de acceso secuencial: Se debe leer o escribir todo el archivo completo hasta llegar al dato que me interesa.

Veamos el siguiente programa, que genera y muestra por pantalla los números primos menores que 10000:

#include <iostream>
#include <cmath>
using namespace std;

int main()
{
    int n,i;
    int max;
    int t=2;
    bool primo;
    cout<<2 <<"   "<<3<<"   ";
    n=5;    //Imprime directamente el 2 y el 3, empieza a buscar primos a partir de 5
    do
    {
        i=3;
        primo = true;
        max=sqrt(n);
        while ( i<=max)
        {
            if (n%i==0)
            {
                primo=false;
                i=n;     //En cuanto encuentra un divisor, sale del ciclo
            }
            i+=2;   //Si sólo considero los impares, no necesito probar con divisores pares
        }
        if (primo)
        {
            t++;
            cout<<n<<"   ";
            if (t==5)   //Uso t para que cada 5 números, imprima un salto de línea
            {
                t=0;
                cout<<endl;
            }
        }
        n=n+2;  //Luego del 2, sólo los impares pueden ser primos
    }
    while (n<10000);
    return 0;
}

Supongamos que quiero guardar los números primos generados en un archivo. El C++ maneja los archivos de texto como flujos de entrada y salida de caracteres. 
Para trabajar con estos objetos, se debe incluir el archivo <fstream>. Para este ejemplo, se debe generar un flujo de salida, al que llamaremos ArchSalida, y se lo debe vincular con un archivo físico en el disco. esto se hace con la siguiente instrucción: 

ofstream ArchSalida ("primos.txt",ios::out);

Esta instrucción genera un objeto de tipo ofstream llamado ArchSalida (de la misma manera que se definen las variables) con dos parámetros: El primero es un string conteniendo el nombre del archivo que se va a crear ("primos.txt"). El segundo parámetro (ios::out) indica que se crea un archivo nuevo y se va a escribir en el mismo. Si el archivo ya existía previamente, lo borra y empieza a escribir desde el inicio.

Una vez hecho esto, se puede trabajar con el archivo de la misma manera que el cout usa para trabajar con la pantalla. Si la instrucción cout << n; envía el valor de n a la salida estándar (la pantalla); la instrucción ArchSalida << n; envía el mismo valor al archivo vinculado al flujo ArchSalida. Modificando el programa anterior para que además de mostrar los números por pantalla los guarde en un archivo, queda de la siguiente manera:

#include <iostream>
#include <cmath>
#include <fstream>   //Necesario para manejar archivos
using namespace std;

int main()
{
    int n,i;
    int max;
    int t=2;
    bool primo;
    ofstream ArchSalida ("primos.csv",ios::out); //Defino un archivo de salida
    cout << 2 <<"   "<<3<<"   ";
    ArchSalida<< 2 <<"   "<<3<<"   "; //Escribo en el archivo
    n=5;
    do
    {
        i=3;
        primo = true;
        max=sqrt(n);
        while ( i<=max)
        {
            if (n%i==0)
            {
                primo=false;
                i=n;    
            }
            i+=2;   
        }
        if (primo)
        {
            t++;
            cout<<n<<"   ";
            ArchSalida<<n<<"   "; //Escribo en el archivo
            if (t==5)   
            {
                t=0;
                cout<<endl;
                ArchSalida<<endl; //Escribo en el archivo
            }
        }

        n=n+2;  
    }
    while (n<10000);
    return 0;
}

Este programa genera el archivo "primos.txt" que está en la misma carpeta que el programa y que contiene todos los números primos hasta 10.000.

Un formato de archivos muy útil para compartir información entre diferentes programas es el de valores separados por comas (csv).  Este formato es un archivo de texto en el que entre dato y dato se coloca un punto y coma. Este formato puede ser entendido por numerosos programas, Si cambiamos el nombre del archivo a "primos.csv" y escribimos un punto y coma en lugar de los espacios entre cada número, el archivo generado podrá ser leído sin problemas por cualquier planilla de cálculo, por ejemplo Microsoft Excel. Esta es una forma rápida de genera con nuestro programa datos que serán luego utilizados por otras aplicaciones.

#include <iostream>
#include <cmath>
#include <fstream>   //Necesario para manejar archivos

using namespace std;

int main()
{
    int n,i;
    int max;
    int t=2;
    bool primo;
    ofstream ArchSalida ("primos.csv",ios::out); //Defino un archivo de salida
    cout<<2 <<"   "<<3<<"   ";
    ArchSalida<<2 <<";"<<3<<";"; //Escribo en el archivo
    n=5;
    do
    {
        i=3;
        primo = true;
        max=sqrt(n);
        while ( i<=max)
        {
            if (n%i==0)
            {
                primo=false;
                i=n;     
            }
            i+=2;  
        }
        if (primo)
        {
            t++;
            cout<<n<<"   ";
            ArchSalida<<n<<";"; //Escribo en el archivo
            if (t==5)  
            {
                t=0;
                cout<<endl;
                ArchSalida<<endl; //Escribo en el archivo
            }
        }
        n=n+2;  
    }
    while (n<10000);
    return 0;
}

Se debe tener en cuenta que el programa mantendrá el archivo abierto (y bloqueado) hasta que termine de ejecutarse, impidiendo a otras aplicaciones el acceso al mismo. Si se necesita usar el archivo desde otro programa antes de que este finalice, se debe cerrar el archivo, con la función close().

ArchSalida.close();

Una vez ejecutada esta instrucción, se finaliza la escritura en el archivo, que ya queda liberado para otras aplicaciones.


4.5 Lectura de archivos

Luego de escribir los datos en un archivo, es muy posible que los querramos leer desde el mismo ú otro programa. Para ello, se sigue una estructura bastante similar a la de la escritura. El siguiente programa lee los números primos que se generaron en la sección anterior.

#include <iostream>
#include <fstream>   //Necesario para manejar archivos 

using namespace std;

int main()
{
    int n[50000],i=0,j;
    char pc;
    ifstream ArchEntrada ("primos.csv",ios::in); //Defino un archivo de entrada
    while (!ArchEntrada.eof())
    {
    ArchEntrada>>n[i]>>pc ;   //Leo un entero y el ; que le sigue
    i++;

    }
    ArchEntrada.close();  // Cierro el archivo una vez que terminé de leer
   

    for(j=0;j<100;j++)
        cout<<n[j]<<" ";
    return 0;
}


En este programa, se define un objeto de tipo ifstream que será un archivo de entrada. Los parámetros que recibe son el nombre del archivo físico, y el modo en que se abrirá (ios::in).
Es necesario leer todos los datos del archivo hasta que se llegue al final del mismo. Para esto se usa la función ArchEntrada.eof(). La función eof() devuelve un valor booleano que es true si se llegó al final del archivo, y false si todavía no se alcanzó el final. Lo que hace el ciclo es: Mientras no se llegue al final del archivo, leer otro número. La instrucción de flujo de entrada >> lee un número y lo guarda en una posición del vector n. Además de los números el archivo tiene los ";" que se escribieron entre número y número. También es necesario leerlos, y guardarlos en la variable pc.
Una vez que se alcanzó el fin del archivo, la función eof() devuelve un valor true y finaliza el ciclo. Entonces se cierra el archivo con la función close(), y el programa puede seguir trabajando con los números cargados en el vector. (En este ejemplo, sólo se muestran los primeros 100 elementos del vector)

En estas dos secciones sólo hemos visto una forma de escribir y leer datos en archivos de texto. Esta forma es de las más simples y aún así es muy versátil (los archivos creados pueden ser por ejemplo, leídos desde una planilla de cálculo).
Otro uso muy común es tener información guardada en un archivo. Al comenzar el programa se define un objeto ifstream vinculado al archivo y a través del cual se ingresan los datos que pueden guardarse en un vector de estructuras. Luego se cierra el archivo, y se trabaja con el vector consultando y modificando la información. Cuando ya no se desea seguir trabajando, se crea un objeto ofstream vinculado al mismo archivo (es por ello que se debe cerrar el ifstream primero) y se graba en el mismo toda la información del vector, quedando actualizada y lista para ser utilizada nuevamente.