Hola, con este artículo doy inicio a una serie de posts explicando la sintaxis básica y expresiones usadas en las versiones modernas de C++ (C++11 en adelante). Hay mucho material dando vueltas por internet que explica lo mismo que quiero hacer, pero que suele estar enfocado en versiones anteriores de la norma y, hoy por hoy, están bastante desactualizados, considerando que en los últimos 10 años se han añadido bastantes características que han mejorado el uso del lenguaje considerablemente y lo han vuelto una herramienta aún más poderosa si se usa de forma apropiada.
En este primer artículo explicaré lo básico sobre estructuras de control, variables, tipos de variables y bibliotecas y la estructura de un programa sencillo. No explicaré todo desde cero porque asumo que quien lea esto entenderá lo básico sobre programación. No es necesariamente cosas que fueron añadidas en versiones recientes del lenguaje, pero esto nos permitirá sentar una base para lo que venga a futuro.
Variables
Una variable se podría definir como un contenedor o recipiente que alberga algún tipo de dato en memoria, como puede ser un entero, una cadena de caracteres, un arreglo de números decimales, entre muchos otros.
// Una variable se define de la forma
// <tipo> <nombre> [= <valor inicial>];
// Un número entero entre -2^31 y (2^31)-1
// en arquitecturas de PC modernas
int nice = 69;
// Un número sin signo entre 0 y (2^32)-1
unsigned poblacion = 18730000;
// Un número entre -128 y 127 (usualmente
// utilizado para caracteres ASCII básicos)
char a_mayus = 'A';
// Un número flotante de precisión simple (~7 decimales)
float un_medio = 0.5;
// Flotante de precisión doble (~15 decimales)
double pi = 3.141592653589793
// Un valor booleano (verdadero/falso)
bool es_verdad = false;
// Una cadena de caracteres terminada en 0 y
// de tamaño fijo (el 0 es implícito; lo
// añade el ompilador)
char saludo[] = "Hola";
// Una versión mejorada del ejemplo anterior
// (permite un manejo de texto más simple y es
// de tamaño variable)
std::string despedida = "Hola";
// Si no se asigna un valor al definir la variable, su
// contenido quedará indeterminado (podría tomar
// cualquier valor presente en memoria al momento de
// ejecutarse el programa)
// Sin embargo, esto no es necesariamente malo. Solo hay que
// tener cuidado y asignarle un valor antes de usarla
int indet;
indet = 420; // Aquí le asignamos un valor. NiceUna variable puede definirse como constante (const) si no se piensa modificar una vez creada:
const double phi = 1.618;
phi = 2.0; // INVÁLIDOSintaxis básica de un programa
Para crear un programa en C++, necesitamos crear un archivo fuente que contenga el código que queremos ejecutar. Para esto existen un sinnúmero de editores o entornos de desarrollo que pueden usar (Visual Studio, Visual Studio Code, Emacs, Vim, Atom, Sublime Text, etc.). Lo importante aquí es tener acceso a un compilador que convierta el código fuente en código máquina que el computador pueda entender. En este ejemplo voy a usar g++, parte de la suite GCC y que se encuentra disponible en toda distribución GNU/Linux (e inclusive en Windows, gracias a proyectos como MinGW, por dar un ejemplo).
La estructura de un programa C++ básico es como la siguiente:
// Bibliotecas a incluir
#include <iostream>
#include <vector>
void saludar() {
std::cout << "Hola\n";
}
// Esta no es la forma óptima de recibir un argumento,
// pero veremos cómo mejorarlo más adelante.
int suma(std::vector<int> vec) {
if (vec.empty())
return 0;
int acc = 0;
for (const int n : vec) {
acc += n;
}
return acc;
}
int main() {
saludar();
std::vector<int> nums{1, 2, 3, 4, 5, 6};
std::cout << suma(nums) << "\n";
return 0;
}Los tres bloques entre llaves que ven se denominan funciones. Estas son trozos de código que encapsulan alguna funcionalidad y permiten separar instrucciones que cumplan propósitos diferentes. La última de estas es la más importante, pues el compilador espera que en el código que le proveamos exista como mínimo una función llamada main. Esta cumple el rol de ser el punto de entrada desde el cual el sistema operativo empezará a ejecutar código.
Una función puede recibir valores como entrada (sobre los que puede trabajar) y entregar como salida otro valor. Tanto entradas como salidas son opcionales e implementarlas o no dependerá de las circunstancias en las que estén trabajando.
En el caso de la importante función main o la función saludar, podemos notar que no aceptan argumentos, esto porque los paréntesis que vemos después del nombre están vacíos (()). En cambio, la función suma acepta un vector de enteros de nombre vec (std::vector<int> vec). Con respecto a las salidas, tanto la función main como suma devuelven un entero. En el caso de suma, la suma de los números del vector que le entregamos. En el caso de main, este número lo recibe el SO e indica si la ejecución fue exitosa (retorna 0) o no (cualquier número distinto de 0).
Ahora, para convertir este código en un ejecutable, lo guardamos en un archivo (en este caso lo podemos llamar funciones.cpp) y ejecutaremos el siguiente comando en una terminal:
g++ -o funciones funciones.cppLo que hacemos aquí es invocar al compilador g++ para que lea el archivo funciones.cpp, lo procese y lo convierta en el ejecutable funciones (funciones.exe si se compila en Windows; el .exe se debería añadir implícitamente).
Este proceso es relativamente simple si solo queremos compilar un programa a partir de uno o dos archivos fuente. El problema viene cuando nuestro programa depende de varios archivos y no queremos llamar a g++ decenas de veces o escribir un comando que tome tres líneas de la pantalla. Para problemas como este existen soluciones como make o CMake, las que podemos ver más adelante. Para proyectos simples, basta con una llamada a g++ siguiendo la siguiente sintaxis:
g++ [-o <nombre_ejecutable>] <archivo_1>.cpp \
<archivo_2>.cpp ... <archivo_n>.cppSi no especifican un nombre para el ejecutable con -o, el compilador lo llamará a.out (nota histórica: este nombre se remonta a cuando Linux aún no existía y x86 no era la arquitectura principal para sistemas UNIX).
Bibliotecas
Además de las características propias del lenguaje (algunas de las cuales veremos después de esta sección), la librería estándar ofrece varias bibliotecas (que no son más que código ya escrito que implementa funcionalidades usadas a menudo por programadores) que nos permiten, por ejemplo, trabajar con entrada y salida de la terminal, lectura y escritura de archivos, el uso de contenedores para almacenar datos (como los vectores que ya he mencionado) y algoritmos de manipulación de datos, ordenamiento, entre muchos otros.
La biblioteca más importante para comenzar en mi opinión es <iostream>, que nos permite imprimir texto en pantalla y leerlo desde el teclado del usuario, esto por medio de los streams std::cin (la entrada estándar, usualmente el teclado), std::cout (la salida estándar, por lo general la pantalla) y std::cerr (el error estándar, que igual suele ser la pantalla, pero para mantener los errores separados de la salida normal). Se les denomina stream porque internamente podemos imaginar estos tres conductos como corrientes (streams) por las que fluye la información de manera constante, donde terminan llegando a un búfer que los va acumulando y permiten que el programa pueda captar datos que no necesariamente llegan a una tasa constante.
Más adelante veremos estructuras y objetos como std::vector, std::list o std::fstream, que nos otorgan más posiblidades para resolver problemas con nuestro código.
<iostream>: Entrada y salida de terminal
Si desean imprimir texto en pantalla, pueden hacer uso de std::cout, el cual recibirá cualquier texto, número o variable que le entreguen y lo imprimirá en la salida estándar o stdout:
#include <iostream> // Así se incluye una biblioteca.
int main() {
// El operador << indica que queremos "insertar"
// el texto "Hola\n" en el stream de salida
std::cout << "Hola\n";
return 0;
}Si necesitan retornar un error, es buena práctica mantenerlo separado de stdout y dirigirlo en cambio a stderr (el error estándar) usando std::cerr
#include <iostream>
int hacer_algo(); // Este es un prototipo. Indica que existe
// una función pero cuyo código aún no está
// definido (en este caso podría estar al
// fondo del archivo)
// Tanto C como C++ esperan que una función
// esté definida antes de llamarla. Una
// forma de sortear esto es poniendo el
// prototipo al inicio y definir la
// función después
int main() {
if (hacer_algo() == 0) {
// Si la función retorna 0, imprimimos a stdout
std::cout << "Éxito\n";
} else {
// De no ser así, indicamos el error en stderr
std::cerr << "Chale\n";
}
return 0;
}
// Aquí definimos la función ya declarada anteriormente
int hacer_algo() {
// ...
}Como pueden ver, para imprimir en una nueva línea, podemos escapar el carácter ASCII para nueva línea (también conocido como LF) usando el ya tradicional \n. Sin embargo, otra forma de hacer esto es insertando en el stream de salida (ya sea std::cerr o std::cout) el manipulador std::endl, que además de insertar el carácter \n fuerza la descarga el búfer de salida en pantalla (el texto que uno pone en stdout o stderr puede no ser impreso inmediatamente, pues depende de la implentación del sistema).
std::cout << "Estas dos líneas" << std::endl;
std::cout << "van a estar en líneas separadas.\n";Finalmente, si queremos leer información de parte del usuario (como un número o un texto), usaremos std::cin:
#include <iostream>
#include <string> // Para trabajar con el tipo std::string
int main() {
std::cout << "Cuál es tu nombre?\n";
std::string nombre;
// En este caso, el operador >> indica
// que queremos extraer un string desde
// stdin (la entrada estándar) y guar-
// darlo en una variable
std::cin >> nombre;
std::cout << "Hola, " << nombre << "!\n";
return 0;
}También es posible utilizar las funciones propias de la librería estándar de C (como printf, scanf y sus derviados). En este caso, en vez de incluir la biblioteca <stdio.h> usaremos <cstdio>, pues así se denomina el archivo de cabecera según la norma de C++. Técnicamente es posible usar <stdio.h> en archivos C++, pero esto no está contemplado en la norma y no es una característica garantizada en todos los compiladores. Todas las bibliotcas que vienen de la libería estándar de C existen en C++ usando la misma nomenclatura (eliminando el .h al final y anteponiendo una c).
Estructuras de control
Condicionales simples (if)
Los bloques if son el tipo de estructura más simple. El programa verifica si una condición es verdadera o falsa y, dependiendo del resultado, ejecuta código:
if (2 + 2 == 4) {
// El programa ejecutará el código entre las llaves
// si 2 + 2 es igual a 4
std::cout << "El universo está funcionando bien.\n";
}Es posible ejecutar otro bloque de código en caso de que la condición no sea cierta o inclusive comprobar más de una condición en caso de que la primera no sea verdadera:
if (2 - 2 == 0) {
std::cout << "Nada que ver por aquí.\n";
} else {
std::cerr << "Houston, tenemos un problema!\n";
}int numero = 3;
if (numero < 0) {
// Si el número es mayor a 0,
// salta a la siguiente condición
std::cout << "Negativo\n";
} else if (numero == 0) {
// Ídem
std::cout << "Cero\n";
} else {
// Si esta condición tampoco
// es verdadera, el programa no hará nada
std::cout << "Positivo\n";
}Condicionales de selección (switch)
Si tenemos que una variable pueda tener uno de varios valores, podemos comprobar cuál de estos es usando una secuencia de if-else if como en la sección anterior. Sin embargo, puede volver el código poco legible si se trata de operaciones sencillas. Para casos así, podemos usar la estructura switch:
char letra = 'a';
switch (letra) {
case 'y':
// Si la letra fuera 'y', ejecutaría este código y
// saldría del switch
std::cout << 'Semivocal\n';
// Si se omite la expresión break, el programa
// irá revisando los otros casos hasta
// encontrar otro break o salir del switch
break;
case 'a':
case 'e':
case 'i':
case 'o':
case 'u':
std::cout << 'Vocal\n';
break;
default:
// Este código se ejecuta sólo si ninguno de los
// valores anteriores coincide
std::cout << 'Consonante\n';
break;
}Bucles
while y do while (controlado por condición)
Los bucles while son los más simples en teoría. El código que envuelven es ejecutado si y sólo si la condición se mantiene verdadera. De serlo, se reejecuta. En caso contrario, se sale del bucle y se continúa con la ejecución del programa.
int n = 10;
while (n > 0) {
// El programa imprimirá "Hola!" en pantalla
// mientras n tenga un valor positivo
std::cout << "Hola!\n";
n--;
}Es posible, si así se desea, crear un bucle infinito, el cual ejecutará el código que envuelve infinitamente hasta que el programa se interrumpa o el equipo que lo ejecuta se apague:
while (true) {
std::cout << "El santuario de santa Teresita";
std::cout << " aún necesita tu aporte\n";
}Lo importante a considerar es que el chequeo de la condición se realiza antes de ejecutar el código del bucle, cosa contraria a lo que hace el bucle do while:
int n = 10;
do {
std::cout << "farts\n";
n--;
// En este caso la verificación se hace al final
} while (n > 0);for (controlado por conteo)
Cuando queremos ejecutar código una cierta cantidad fija de veces, podemos hacer uso de los bucles for, los cuales hacen uso de un contador que es chequeado en cada “vuelta” del ciclo e indica si debe seguirse ejecutando el bucle.
El bucle for es ligeramente más complicado que while, pues involucra la inicialización del contador, la condición que mantendrá el bucle activo y una instrucción que se ejecuta al final de cada iteración.
// Este bucle parte con el entero i igualado a 0.
// Se ejecutará siempre y cuando i sea menor que 10.
// Al final de cada vuelta, i será incrementado en 1
for (int i = 0; i < 10; ++i) {
std::cout << i << "\n";
}
// Este bucle va a imprimir el valor de i 10 veces.Si desean que la variable del contador sea inicializada antes, se puede omitir de la declaración:
int i = 42;
// ...
for (; i < 1000; i *= 2) {
funcion_rara(i, true);
}Lo mismo puede hacerse con la instrucción que se ejecuta al final de cada paso (el único elemento obligatorio es la condición del bucle):
for (int i = 100000; i > 0;) {
// ...
i = calcular_i();
}Una forma especial de for es una no se inicializa un contador, ni condición ni instrucción de término de iteración, volviendo al código envuelto en un bucle infinito:
for (;;) {
std::cout << "a\n";
}for basado en rangos (controlado por colección)
Esta es una mis características favoritas del lenguaje y que fue implementada en C++11.
Como veremos más adelante, la librería estándar de C++ nos ofrece una gran cantidad de estructuras que nos permiten almacenar información de diversas formas, siendo una de ellas std::vector, la cual nos permite crear un arreglo expandible:
// Arreglo tradicional de tamaño fijo
int arr[] = {1, 2, 3, 4, 5};
// Esto no va a funcionar aunque quisiéramos
arr[5] = 6;
std::vector<int> vec{1, 2, 3, 4, 5}; // Vector expandible
vec.push_back(10); // Añadimos otro número al final del vector¿Por qué menciono esto antes de explicar bien lo que es un vector? Porque algo que se hace muy a menudo con estructuras como esta es recorrer todos los elementos de esta y hacer alguna acción con ellos. La forma tradicional de hacer esto hasta 2011 era la siguiente:
std::vector<int> vec{1, 2, 3, 4, 5};
std::vector<int>::iterator it;
for (it = vec.begin(); it != vec.end(); ++it) {
// ...
}¿Engorroso, no es así? Fue por esto que en C++ se implementaron los for basados en rango, los cuales simplifican esta sintaxis y la convierten en esto:
for (int num : vec) {
// ...
}Como pueden ver, esta sintaxis es mucho más legible y cumple el mismo propósito. Al menos cuando se trate de recorrer una colección de cabo a rabo, esta última de escribir el for les ahorrará dolores de cabeza al necesitar menos código para cumplir el mismo objetivo, lo que hace al programa más mantenible y claro.