Get Even More Visitors To Your Blog, Upgrade To A Business Listing >>

Operaciones básicas con cadenas en C++: capitalización, conversiones, recorte, recorrido y más

Como ha sucedido con otros lenguajes, C++ también ha evolucionado. Ha madurado mucho desde aquellos códigos que programábamos hace años y se nota. Por un lado, podemos pensar que al sumar abstracción en ciertos aspectos nos separa de la máquina y hace nuestro código más lento. Suma comprobaciones, hace más callbacks y en definitiva, una sencilla tarea que completaba en pocos cientos de operaciones, ahora son pocos miles. Aunque en su favor, podemos decir que aquello que programábamos en 15 o 20 líneas de código se ha reducido a una o dos, reduciendo así los puntos de ruptura, posibles bugs y calentamientos de cabeza futuros.

Eso sí, si queremos siempre podemos volver a los orígenes. Aunque será mucho más seguro utilizar estas nuevas formas de trabajo, si estamos seguros de lo que hacemos y queremos que el rendimiento de nuestro programa sea mucho mayor, podemos hacerlo. Es más, muchos programadores, todavía a 2017, suelen preferir programar en C cuando quieren ver un rendimiento superior en sus desarrollos.

He hecho esta pequeña recopilación de chuletas para realizar ciertas operaciones en C++ moderno, que podréis compilar si contáis con soporte para C++11 o superior. He decidido incluir los programas completos, no sólo la parte en que quiero hacer hincapié , para que podáis copiar, pegar y compilar, y así lo veis y lo sentís como lo hago yo.

Nota: Tenemos Glib, Boost y cientos de bibliotecas de C++ que pueden hacer cosas muy chulas (y lo que están preparando para C++17, que hasta finales de año no estará listo y que podremos disfrutar en su totalidad en 2018), pero en este post he querido centrarme en lo que podemos hacer con las herramientas que nos da el lenguaje sin utilizar bibliotecas de terceros

Pasar una cadena completa a mayúsculas/minúsculas

Recordemos los tiempos en los que en C pasábamos a mayúsculas y minúsculas una cadena. La recorríamos por completo y evaluábamos para cada letra cuál sería su mayúscula. No empezaremos con una novedad del lenguaje (podremos compilar con compiladores antiguos, pero sólo estamos abriendo boca). Cuando sólo tenemos un alfabeto inglés podemos hacer lo siguiente:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include
#include
#include
#include

int main()
{
    std::string s = "HoLa MuNDo DeSDe PoeSía BiNaRia";

    std::transform(s.begin(), s.end(), s.begin(), ::toupper);

    cout s endl;
}

Utilizaremos transform() para que recorra la cadena y llame a toupper con cada uno de los caracteres, lo típico, toupper de cctype. Y de la misma manera que llamamos a toupper, lo podemos hacer con tolower para las minúsculas.

Mayúsculas y minúsculas en un mundo plurilingüe

Pero, a estas alturas, un ordenador tiene que poder hablar inglés, español, francés y hasta ruso, y todos tenemos derecho a nuestras letras mayúsculas y minúsculas. Ahora tenemos que poner en práctica el uso de la locale con la que queremos hacer la transformación. Eso sí, en lugar de un string, también tendremos que utilizar un wstring, porque en codificaciones como UTF-8, un solo byte no siempre equivale a un carácter. Necesitaremos alguien que sea capaz de leer caracteres de nuestros bytes (y programarlo nosotros es horrible). Por ejemplo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include
#include
#include
#include

using namespace std;

int main()
{
    std::wstring s = L"HoLa MuNDo DeSDe PoeSía BiNaRia";
    std::locale::global(std::locale("es_ES.UTF-8"));

    std::transform(s.begin(), s.end(), s.begin(), [](wchar_t c){
            return std::toupper(c, std::locale()); });

    wcout "RESULTADO:"endl;
    wcout s endl;
}

O también:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include
#include
#include
#include

int main()
{
    std::wstring s = L"HoLa MuNDo DeSDe PoeSía BiNaRia";
    std::locale::global(std::locale("es_ES.UTF-8"));

    std::use_facetstd::ctypewchar_t>>(std::locale()).toupper(&s[0], &s[0] + s.size());

    std::wcout "RESULTADO:"std::endl;
    std::wcout s std::endl;
}

O si queremos que el código quede más limpio, podemos sustituir esta línea tan larga por:

1
2
auto& f = std::use_facetstd::ctypewchar_t>>(std::locale());
f.toupper(&s[0], &s[0] + s.size());

Caracteres al principio y al final

Para saber el primer carácter de una cadena s, podíamos utilizar:

  • s[0] : Primera posición del string, que está muy bien, pero damos una posición numérica y no decimos de forma explícita “lo primero que hay”.
  • *s.begin() : El valor del iterador al primer elemento. Aunque como programadores de C++ y no de C, ver muchos asteriscos nos agobia…

Para la última letra, ya lo tendríamos más complicado, tendríamos que usar:

  • *(s.end()-1) : El iterador al último carácter de la cadena será el terminador, pues resolvemos el puntero al carácter anterior. Esto queda un poco feo, hay asteriscos y hacemos cuentas con punteros. No es muy seguro.
  • s[s.length()-1] : Aunque no tenemos asteriscos. pedimos la longitud de la cadena, lo que puede que implica una llamada, aunque muchos compiladores están muy bien optimizados.

En C++11 tenemos front():

1
2
3
4
5
6
7
8
9
10
#include
#include

int main()
{
    std::string s = "Hola Mundo";

    std::cout "Primera: "s.front() std::endl
                "Última: "s.back() std::endl;
}

Si queremos, podemos añadir o eliminar caracteres al final con las funciones push_back() y pop_back, así:

1
2
3
4
5
6
7
8
9
10
11
12
#include
#include

int main()
{
    std::string s = "Hola Mundo";

    s.push_back('!');
    std::cout s std::endl;
    s.pop_back();
    std::cout s std::endl;
}

Comprobar que una cadena contiene un número

Vamos, lo que queremos hacer es saber si el contenido de una cadena, es numérico. Para ello, debemos asegurarnos de que todos los caracteres de la misma son números:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include
#include
#include

bool isNumeric(const std::string& input)
{
    return std::all_of(input.begin(), input.end(), ::isdigit);
}

int main()
{
    std::string s = "1239812357";
    std::cout ((isNumeric(s))?"Sí":"No") std::endl;
}

Sí, precioso, pero ¿qué pasa con los negativos? Bueno, podemos modificar isNumeric un poco para comprobar el primer carácter de la cadena:

1
2
3
4
5
6
7
bool isNumeric(const std::string& input)
{
    auto ib = input.begin();
    if ((input.front()=='+') || (input.front()=='-') )
        ib++;
    return std::all_of(ib, input.end(), ::isdigit);
}

Bueno, ya cubrimos los números enteros, y si comprobamos esto, podremos ver que daría igual la longitud de la cadena, es decir, pueden ser números muy grandes, números que desbordarían un long, pero al menos podemos comprobar su validez.
Pero, ¿qué pasaría con los números hexadecimales? ¿o los binarios? Aquí podemos especificar la base:

1
2
3
4
5
6
7
8
bool isNumeric(std::string in, int base=10 )
{
    std::string validos = "0123456789ABCDEF";
   
    return (in.find_first_not_of(validos.substr(0, base),
                                                             ((in.front()=='+') || (in.front()=='-') )
                    ) == std::string::npos);
}

Eso sí, ¿qué pasaría con los decimales? Nos estamos poniendo caprichosos ya, pero podríamos comprobar la existencia de la coma decimal y ver si hay más números a partir de ahí:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include
#include
#include
#include

bool isNumeric(std::string in, int base=10 )
{
    std::string validos = "0123456789ABCDEF";
   
    auto entera = in.find_first_not_of(validos.substr(0, base),
                                                                         ((in.front()=='+') || (in.front()=='-') ));

    // Podíamos haber utilizado == '.' pero no sabemos el idioma del usuario.
    // Para los decimales puede utilizar . , o cualquier otra cosa.
    if (in[entera] == std::use_facet std::numpunctchar> >(std::locale()).decimal_point())
        return (in.find_first_not_of(validos.substr(0, base),
                                                                 entera+1) == std::string::npos);
    else
        return (entera == std::string::npos);
}

int main()
{
    std::locale::global(std::locale("es_ES.UTF-8"));
    std::string s = "-123982357,";

    std::cout ((isNumeric(s, 16))?"Sí":"No") std::endl;
}

Comprobar que una cadena contiene un número II

Bueno, y como es C++ moderno, tenemos un extra muy interesante, ¡expresiones regulares! Pues nada, creemos una expresión regular para comprobar cadenas numéricas. Total, en otros lenguajes lo hacemos sin miedo, aunque, ¡cuidado! El tamaño del ejecutable puede crecer mucho.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include
#include
#include
#include

bool isNumeric(std::string in )
{
    std::regex num_regex("^[+|-]?(([0-9]*)|(([0-9]*)\\.([0-9]*)))$");
    return std::regex_match(in, num_regex);
}

int main()
{
    std::string s = "123982357";

    std::cout ((isNumeric(s))?"Sí":"No") std::endl;
}

Convertir de cadena a número

Si pensamos en C. Aunque tenemos atoi(), atod(), strtol() y demás familiares y derivados, debemos recorrer la cadena por completo y analizar carácter a carácter si es numérico y si lo es, darle un valor con respecto a la posición que ocupa dentro de la cadena. También es cierto que atoi() y demás pueden ser inseguras ya que C no comprueba tamaños de cadenas y si no controlamos el dato, los resultados pueden ser inesperados. Es más, atoi() es la función que nunca se queja, tanto si le pasas una cadena que no contiene un número (que devuelve 0) como si le pasas una cadena muy larga con números muy largos que desbordan la variable (ok, se desborda, devuelve lo que quiere, pero no falla). Aunque strtol() / strtod() nos dan más control, pero no son estilo C++.

En C++ podemos hacer lo siguiente:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include
#include

int main()
{
    std::string numero = "12347665";
    try
        {          
            std::cout std::stoi(numero)std::endl;
        }
    catch (std::logic_error e)
        {
            std::cout "Problemas!!" std::endl;
        }
}

Eso sí, stoi(), stol() y stoll() funcionan con strtol() y strtoll() por detrás, para eso de tener excepciones. Si queremos velocidad, seguramente nos vaya mejor utilizando las funciones de C, pero si queremos estilo y excepciones, las funciones de C++ nos vendrán bien.

Al igual que stoi(), tenemos también stof() para float, stod() para double y stold() para long double. Eso sí, ¿quieres números extremadamente grandes? Puedes probar GMP, que tiene una interfaz para C++. Aquí tienes un ejemplo de uso en C.

Convertir de número a cadena

En C, los que aprendimos hace mucho tiempo teníamos una función, itoa(), pero no era ANSI-C. De hecho, yo la conocí en Turbo C, y no la vemos en muchos compiladores. Aunque tenía el mismo problema de siempre: no hay comprobación del tamaño de cadenas, porque en C no es muy fácil.

De todas formas, aunque en C terminábamos haciendo las conversiones número-cadena con sprintf() o snprintf() (mejor esta segunda), antiguamente en C++, podíamos utilizar sstream, pero muchísimas veces da una pereza enorme y nos obligaba a tener wrappers a meno con estas funciones siempre cerca. Pero desde C++11 tenemos una nueva función para sacarnos las castañas del fuego, to_string(), fácil y preciosa, a la que le da igual que le pasemos un int, double, uint16_t, char o lo que queramos, siempre que sea numérico:

1
2
3
4
5
6
7
8
#include
#include

int main()
{
    uint16_t zz = 1239;
    std::cout std::to_string(zz);
}

Dividir cadenas (split, tokenize…)

Una ardua tarea es siempre separar una cadena en varios trozos, o bien por palabras, o con algún carácter comodín, o cualquier otro criterio. En C tenemos strtok(), con la inseguridad que conlleva, además porque en un mundo multihilo puede darnos problemas (y por eso vino su hermano strtok_r()), pero es otro cantar. En C++ tenemos a nuestra disposición multitud de estructuras de datos, y algo que hacemos en otros lenguajes como Javascript de forma muy fácil (un split de toda la vida), en C++ se nos puede atravesar un poco. Y tenemos varias opciones:

Separar palabras por espacios

En realidad, por los caracteres que utiliza cin para separar elementos. Lo haremos con istream_iterator y meteremos los valores en un vector. Lo bueno es que podemos utilizar strings, int, o cualquier cosa que podamos utilizar en un stream:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include
#include
#include
#include

int main()
{
    std::istringstream iss("Estoy probando un\n\n\n\ntokenizer desde Poesía Binaria");
    std::vectorstd::string> results { std::istream_iteratorstd::string>(iss), std::istream_iteratorstd::string>() };
   
    for (auto vi : results)
        std::cout vi std::endl;
}

…¡¡con expresiones regulares!!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include
#include
#include
#include

std::vectorstd::string> split(const std::string& str, const std::string& regex)
{
    std::regex _regex(regex);
    return { std::sregex_token_iterator (str.begin(), str.end(), _regex, -1), std::sregex_token_iterator() };
}

int main()
{
    std::string ss = "hola:mundo:de:codigo";
    std::string sep = ":";
   
    auto v = split(ss, sep);
    for (auto vi : v)
        std::cout vi std::endl;
}

Desde que el soporte de expresiones regulares es nativo. C++ se ha convertido en un lenguaje adulto. Es cierto que antes podíamos hacer multitud de cosas, y que seguro que muchos aprovechamos ese soporte de expresiones regulares para complicarnos la vida y hacer cosas menos eficientes, pero se agradece mucho en ocasiones y seguro que simplifica nuestros códigos.

…separadas con un carácter

Otro ejemplo más, puede ser este:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include
#include
#include

const std::vectorstd::string> split(const std::string& haystack, const char& needle)
{
    std::string buff{""};
    std::vectorstd::string> v;
   
    for(auto c:haystack)
    {
        if(c != needle)
            buff.push_back(c);
        else if ( (c == needle) && (!buff.empty()))
            {
                v.push_back(buff);
                buff = "";
            }
    }
   
    if (!buff.empty())
        v.push_back(buff);
   
    return v;
}



This post first appeared on Poesía Binaria - Programación, Tecnología Y Sof, please read the originial post: here

Share the post

Operaciones básicas con cadenas en C++: capitalización, conversiones, recorte, recorrido y más

×

Subscribe to Poesía Binaria - Programación, Tecnología Y Sof

Get updates delivered right to your inbox!

Thank you for your subscription

×