Cadenas de caracteres en C++¶
Manipulación de caracteres y C-Strings¶
La biblioteca estándar incluye una serie de funcionalidades para trabajar con caracteres, aquí la tabla con ellas: https://en.cppreference.com/w/cpp/string/byte
Por citar algunas funciones muy sencillas, tenemos std::isdigit
para saber si un carácter es numérico o std::toupper para devolverlo en su forma mayúscula:
char letra{'h'};
if (std::isdigit(letra))
std::cout << letra << " es un digito" << std::endl;
else
std::cout << letra << " no es un digito" << std::endl;
std::cout << letra << " en mayuscula "
<< (char)std::toupper(letra) << std::endl;
Así mismo también ofrece un apartado <cstring>
para manejar cadenas de texto en el formato clásico heredado de C, aquí la tabla de funcionalidades: https://en.cppreference.com/w/cpp/header/cstring
Las C-Strings son arreglos de caracteres con un carácter nulo \0
al final que indica el final una cadena. Cuando las definimos mediante un arreglo podemos inicializarlas automáticamente en la definición, el sistema añadirá manualmente el carácter de fin de cadena:
#include <cstring>
char cadena[]{"Hola mundo"};
std::cout << cadena << std::endl;
std::cout << "Longitud cadena: " << std::strlen(cadena) << std::endl;
También podemos definirlas mediante punteros constantes de caracteres:
#include <cstring>
const char *p_cadena{"Hola mundo"};
std::cout << p_cadena << std::endl;
std::cout << "Longitud cadena: " << std::strlen(p_cadena) << std::endl;
Inclusive se pueden definir a partir de los caracteres para almacenarlas en el montón (con new), pero debemos añadir manualmente el carácter fin de cadena:
#include <cstring>
char *p_cadena2 = new char[]{'H','o','l','a',' ','m','u','n','d','o','\0'};
std::cout << p_cadena2 << std::endl;
std::cout << "Longitud cadena: " << std::strlen(p_cadena2) << std::endl;
Algunas funcionalidades interesantes, a parte de la longitud std::strlen
, son la concatenación std::strcat
y la copia std::strcpy
:
char cadena1[]{"Hola "};
char cadena2[]{"mundo"};
char cadena3[10]{};
// Concatenación de cadenas
std::strncat(cadena3, cadena1, std::size(cadena1));
std::strncat(cadena3, cadena2, std::size(cadena2));
std::cout << cadena3 << std::endl; // Hola mundo
// Copia de cadenas
char cadena4[10]{};
std::strcpy(cadena4, cadena3);
std::cout << cadena4 << std::endl; // Hola mundo
Cadenas std::string y sus métodos¶
Las cadenas son objetos que representan secuencias de caracteres. La biblioteca estándar incluye la clase std::string
que proporciona una alternativa sencilla, segura y versátil para la utilización de matrices explícitas de caracteres.
Hay muchas formas de definir una cadena de esta clase:
// Cadena vacía
std::string cadena;
// Desde const char * (c-string)
std::string cadena ("Hola mundo");
std::string cadena {"Hola mundo"};
std::string cadena = "Hola mundo";
// Usando el constructor de copia
std::string cadena1 ("Hola mundo");
std::string cadena2 (cadena1);
// A partir de subcadenas y/o buffers
std::string cadena1 ("Hola mundo");
std::string cadena2 (cadena1, 0, 4); // "Hola"
// Usando relleno (solo char)
std::string cadena (6, 'a'); // "aaaaaa"
// Usando el constructor de iteración
std::string cadena1 ("Hola mundo");
std::string cadena2 (cadena1.begin(), cadena1.begin() + 4); // "Hola"
Podemos concatenar std::string
utilizando los operadores sobrecargados +
:
std::string cadena1 = "Hola";
std::string cadena2 = "mundo";
std::string cadena3 = cadena1 + " " + cadena2; // "Hola mundo"
También mediante el operador de suma en asignación en la propia cadena +=
:
std::string cadena = "Hola";
cadena += " mundo"; // "Hola mundo"
Podemos agregar cadenas C-string, incluidos los literales de cadena:
std::string cadena1 = "Hola";
std::string cadena2 = "mundo";
const char *coma = ", ";
std::string cadena3 = cadena1 + coma + cadena2; // "Hola mundo"
O utilizar el método push_back()
para insertar un carácter al final del arreglo:
std::string cadena = "ab";
cadena.push_back('c'); // abc
De forma similar podemos utilizar el método append()
para añadir una cadena al final de otra:
std::string cadena = "Hola";
cadena.append(" mundo"); // Hola mundo
Podemos consultar y modificar los caracteres de una std::string
mediante índices o el método .at()
:
std::string cadena = "Hola mundo";
cadena[0] = 'M';
cadena.at(7) = 'c';
cadena.at(8) = 'h';
std::cout << cadena << std::endl; // Mola mucho
De forma similar podemos utilizar los métodos front()
y back()
para consultar y modificar los caracteres del principio y del final de la cadena:
std::string cadena = "Hola mundo";
cadena.front() = 'M';
cadena.back() = 'a';
std::cout << cadena << std::endl; // Mola munda
Además disponemos del método c_str()
, que devuelve la cadena en formato C-string que podemos almacenar en un const char*
:
std::string cadena1 = "Hola mundo";
const char *cadena2 = cadena1.c_str(); // C-string no modificable
Desde C++17 podemos utilizar el método data()
para modificar el arreglo char*
subyacente, lo que modificará la cadena std:string
original:
std::string cadena1 = "Hola mundo";
char *cadena2 = cadena1.data();
cadena2[0] = 'M';
std::cout << cadena1 << " = " << cadena2
<< std::endl; // Mola mundo = Mola mundo
Podemos determinar si una std:string
está vacía mediante le método empty()
:
std::string cadena;
if (cadena.empty()) std::cout << "Cadena vacia" << std::endl;
Y su longitud o tamaño (es lo mismo) mediante length()
y size()
:
std::string cadena = "Hola mundo";
std::cout << "Longitud: " << cadena.length() << std::endl;
std::cout << "Tamano: " << cadena.size() << std::endl;
El método max_size()
nos dirá el número máximo de caracteres que puede almacenar una cadena en nuestro sistema:
std::string cadena;
std::cout << "Tamano maximo: " << cadena.max_size() << std::endl;
// Tamano maximo: 9223372036854775807
Así mismo podemos consultar la capacidad de una cadena con el método capacity()
, el tamaño del espacio de almacenamiento asignado actualmente para la cadena, expresado en bytes.
std::string cadena;
std::cout << "Capacidad: " << cadena.capacity() << std::endl;
// Capacidad: 15
La capacidad se almacena por defecto, de manera que al intentar almacenar más caracteres de los que se pueden almacenar en el bloque actual, el objeto std::string
reclama más espacio en la memoria:
std::string cadena;
std::cout << "Capacidad actual: " << cadena.capacity() << "\n"; // 15
cadena = "Hola hola hola hola hola";
std::cout << "Capacidad actual: " << cadena.capacity() << "\n"; // 30
Podemos reservar de antemano una cantidad de espacio mediante reserve()
y luego ajustarlo al tamaño de la cadena actual mediante shrink_to_fit()
:
std::string cadena;
cadena.reserve(100);
cadena = "Hola hola hola hola hola";
std::cout << "Capacidad actual: " << cadena.capacity() << "\n"; // 100
cadena.shrink_to_fit();
std::cout << "Capacidad actual: " << cadena.capacity() << "\n"; // 24
Por último comentar que es posible comparar fácilmente std::string
, incluso con C-strings y cadenas literales, son iguales mediante los operadores ==
y !=
:
std::string cadena{"Hola"};
std::cout << std::boolalpha;
std::cout << (cadena == "Hola") << std::endl; // true
std::cout << (cadena != "Hola") << std::endl; // false
También se pueden comparar lexicográficamente (en el orden como aparecerían en el diccionario) usando el método compare()
que devolverá:
- Valor cero 0: cuando las cadenas que se comparan son lexicográficamente iguales.
- Valor negativo <0: cuando la primera cadena es lexicográficamente menor que la segunda.
- Valor positivo >0: cuando la primera cadena es lexicográficamente mayor que la segunda.
std::string cadena{"Hola"};
std::cout << cadena.compare("Hola") << std::endl; // 0
std::cout << cadena.compare("Holas") << std::endl; // -1 (después)
std::cout << cadena.compare("Hey") << std::endl; // 1 (antes)
Sin duda C++ pone a nuestra disposición un montón de utilidades para manejar y optimizar el uso de la memoria.
Buscar, substituir, intercambiar y convertir std::string¶
Repasemos ahora algunas utilidades de la clase std::string
, las cuales se pueden encontrar descritas en la documentación: https://en.cppreference.com/w/cpp/string/basic_string
Búsqueda
std::string cad{"Hola mundo"};
std::cout << cad.find("mundo") << std::endl; // 5
std::cout << cad.find("adios") << std::endl; // npos (no encontrado)
// Debemos comprobar siempre que no se devuelve un npos
if (cad.find("adios") == std::string::npos)
std::cout << "No encontrado" << std::endl;
Substitución
std::string cadena{"Hola buenos dias"}; // cadena original
std::string objetivo{"buenos dias"}; // porcion substituible
std::string porcion{"buenas noches"}; // porcion substituta
int size = porcion.size(); // nº de caracteres a substituir
int pos = cadena.find("buenos dias"); // buscar el indice encontrado
if (pos != std::string::npos)
cadena.replace(pos, size, porcion.substr(0, size));
std::cout << cadena << std::endl; // Hola buenas noches
Intercambio
std::string cad1{"Hola"};
std::string cad2{"Adios"};
cad1.swap(cad2);
std::cout << cad1 << ", " << cad2 << std::endl; // Adios, Hola
Conversión de número a cadena
std::cout << std::to_string(101) << std::endl; // "101"
std::cout << std::to_string(3.14f) << std::endl; // "3.140000"
std::cout << std::to_string(45.32) << std::endl; // "45.320000"
Conversión de cadena a número
std::cout << std::stoi("101") << std::endl; // 101
std::cout << std::stof("3.14") << std::endl; // 3.14
std::cout << std::stod("45.32") << std::endl; // 45.32
Cadenas en crudo (raw string literals)¶
El carácter de escapa \
en las cadenas nos sirve para establecer caracteres especiales que indican saltos de línea \n
, tabulaciones \t
, caracteres nulos \0
, etc. Por esa razón definir una cadena como la siguiente no es posible:
std::string ruta{"C:\Usuario\Documentos\Escritorio"}; // error
Una forma de definir estas cadenas con caracteres de escape es utilizar el propio carácter de escape \
para escapar el escape:
std::string ruta{"C:\\Usuario\\Documentos\\Escritorio"};
std::cout << ruta << std::endl;
// C:\Usuario\Documentos\Escritorio
O también podemos definir una cadena en crudo (raw) con el modificador R
de esta forma:
std::string ruta{R"(C:\Usuario\Documentos\Escritorio)"};
std::cout << ruta << std::endl;
// C:\Usuario\Documentos\Escritorio
En caso de que encontremos comillas con paréntesis, podemos evitar que la interprete delimitando la cadena entre tres guiones ---
:
std::string cadena{R"---(C:\("U")suario\("D")ocumentos)---"};
std::cout << cadena << std::endl;
// C:\("U")suario\("D")ocumentos
Vistas de cadenas (std::string_view)¶
Desde C++17 encontramos un tipo de dato algo extraño llamado std::string_view
o vista de cadena: https://en.cppreference.com/w/cpp/string/basic_string_view
Podemos suponer una vista como una ventana a través de la cuál podemos observar un texto, pero no tenemos acceso a él ni a su modificación.
Esto puede resolver un problema de copias múltiples en la memoria, tal como ilustra el siguiente código:
char texto[]{"Hola mundo"}; // Primera definición
std::string cadena1{texto}; // Segunda de definición
std::string cadena2{cadena1}; // Tercera definición
std::cout << sizeof(std::string) << "\n"; // 32 bytes
std::cout << &texto << " " << sizeof(texto) << "\n"; // 0x9d457ff870 11
std::cout << &cadena1 << " " << sizeof(cadena1) << "\n"; // 0x9d457ff860 32
std::cout << &cadena2 << " " << sizeof(cadena2) << "\n"; // 0x9d457ff850 32
En esta pieza de código definimos una C-string y a partir de ella una std::string
que utilizamos a su vez como origen para otra std::string
. ¿Cuántas copias de Hola mundo
existen en la memoria del programa?
En primer lugar tenemos la cadena literal Hola mundo
reconocida en tiempo de compilación y almacenada de forma binaria en la memoria. A partir de ella se crea la copia almacenada en el puntero para la C-string y luego las dos copias por valor para las std::string
, lo que hacen 4 Hola mundo
almacenados paralelamente en el programa.
Si aplicamos la misma lógica pero utilizando vistas de cadena reduciremos el tamaño en la memoria a 1 soloHola mundo
:
std::string_view texto{"Hola mundo"}; // Primera definición
std::string_view cadena1{texto}; // Segunda de definición
std::string_view cadena2{cadena1}; // Tercera definición
std::cout << sizeof(texto) << "\n"; // 16 bytes
std::cout << &texto << " " << sizeof(texto) << "\n"; // 0x9d457ff870 16
std::cout << &cadena1 << " " << sizeof(cadena1) << "\n"; // 0x9d457ff860 16
std::cout << &cadena2 << " " << sizeof(cadena2) << "\n"; // 0x9d457ff850 16
Las std::string_view
nativas no son modificables, pero si las creamos a partir de una std::string
o C-string y modificamos la original, las vistas también cambiarán, pues apuntan al fin y al cabo a la original:
std::string original{"Hola mundo"};
std::string_view texto{original};
std::string_view cadena1{texto};
std::string_view cadena2{cadena1};
original[0] = 'M';
std::cout << original << "\n"; // Mola mundo
std::cout << texto << "\n"; // Mola mundo
std::cout << cadena1 << "\n"; // Mola mundo
std::cout << cadena2 << "\n"; // Mola mundo
Se recomienda utilizar siempre std::string_view
antes que C-strings o cualquier tipo de cadena solo de lectura como sería una const std::string
, no solo para optimizar memoria sino también para incrementar la seguridad.
Última edición: 08 de Mayo de 2022