Programación · Lenguajes de programación
Tutorial de C++ para principiantes
Respuesta corta
C++ es un lenguaje compilado de tipado estático que combina la velocidad y el control de bajo nivel de C con clases, plantillas y una amplia biblioteca estándar. Compila hello.cpp con g++ -std=c++20 hello.cpp -o hello y ejecuta ./hello. El código C++ moderno usa std::string, std::vector y std::map en lugar de arrays en bruto, punteros inteligentes en lugar de delete manual y RAII para que los recursos se liberen automáticamente.
Qué es C++ y para qué se usa
C++ fue creado por Bjarne Stroustrup en Bell Labs a partir de 1979 como «C with Classes». Está estandarizado por ISO; C++11 inauguró la era del «C++ moderno», y después llegaron C++17, C++20 y C++23. Se usa donde importan el rendimiento y el control: motores de videojuegos (Unreal Engine), navegadores web (Chrome, Firefox), bases de datos, sistemas de trading, dispositivos embebidos, compiladores y entornos de ejecución de aprendizaje automático. Su idea rectora es la abstracción de coste cero: las características que no usas no cuestan nada.
Instalar un compilador y ejecutar Hello, world
Usa GCC (g++) o Clang (clang++) en Linux y macOS, o Microsoft Visual C++ en Windows (mediante Visual Studio). Elige una versión del estándar de forma explícita.
#include <iostream>
int main() {
std::cout << "Hello, world\n";
return 0;
}g++ -std=c++20 -Wall -Wextra hello.cpp -o hello
./hello
# Hello, worldstd::cout es el flujo de salida estándar y << escribe en él. Todo lo de la biblioteca estándar vive en el espacio de nombres std.
Variables, tipos y auto
C++ tiene los mismos tipos integrados que C, además de bool como palabra clave, referencias y tipos de biblioteca como std::string. auto pide al compilador que deduzca el tipo a partir del inicializador. La inicialización con llaves {} rechaza las conversiones con estrechamiento.
| Tipo | Ejemplo | Notas |
|---|---|---|
int, long long | int n{42}; | Enteros con signo |
double | double x{3.14}; | Punto flotante de 64 bits |
bool | bool ok{true}; | true o false |
char | char c{'A'}; | Un byte |
std::string | std::string s{"hi"}; | Texto que puede crecer, de <string> |
auto | auto v = 2.5; | Deducido, aquí double |
T& | int& r = n; | Una referencia: otro nombre para n |
#include <iostream>
#include <string>
int main() {
int count{7};
auto avg = count / 2.0; // double, 3.5
std::string name{"Ada"};
name += " Lovelace";
const int& ref = count; // read-only alias of count
std::cout << name << " " << name.size() << " " << avg << " " << ref << "\n";
// Ada Lovelace 12 3.5 7
}Control de flujo y funciones
El control de flujo es el mismo que en C, más el bucle for basado en rangos. Pasa los objetos grandes por referencia const para evitar copias, y por referencia no const cuando la función deba modificarlos.
#include <iostream>
#include <string>
#include <vector>
int square(int x) { return x * x; }
void addSuffix(std::string& s, const std::string& suffix = "!") {
s += suffix;
}
int main() {
int score = 72;
if (score >= 90) std::cout << "A\n";
else if (score >= 70) std::cout << "B\n"; // B
else std::cout << "C\n";
std::vector<int> nums{1, 2, 3};
for (int n : nums) std::cout << square(n) << " "; // 1 4 9
std::cout << "\n";
std::string word = "hi";
addSuffix(word);
std::cout << word << "\n"; // hi!
}Clases
Una clase agrupa miembros de datos y funciones miembro. Los constructores preparan un objeto; los miembros private quedan ocultos al código externo. Un struct es lo mismo que una clase, salvo que sus miembros son públicos por defecto.
#include <iostream>
#include <string>
#include <utility>
class Account {
public:
explicit Account(std::string owner) : owner_(std::move(owner)) {}
void deposit(double amount) {
if (amount > 0) balance_ += amount;
}
double balance() const { return balance_; }
const std::string& owner() const { return owner_; }
private:
std::string owner_;
double balance_ = 0.0;
};
int main() {
Account acc{"Ann"};
acc.deposit(50);
acc.deposit(-5); // ignored
std::cout << acc.owner() << ": " << acc.balance() << "\n"; // Ann: 50
}La herencia (class Savings : public Account) y las funciones virtual dan polimorfismo en tiempo de ejecución. Marca las funciones que sobrescriben con override para que el compilador las compruebe.
Contenedores y algoritmos de la biblioteca estándar
La biblioteca estándar (a menudo llamada STL) ofrece contenedores y algoritmos que funcionan juntos mediante iteradores.
| Contenedor | Equivale a | Úsalo para |
|---|---|---|
std::vector<T> | Array dinámico | El contenedor secuencial por defecto |
std::array<T, N> | Array fijo | Tamaño conocido en tiempo de compilación |
std::map<K, V> | Diccionario ordenado | Claves ordenadas, búsqueda en O(log n) |
std::unordered_map<K, V> | Tabla hash | Búsqueda rápida, O(1) de media |
std::set<T> | Conjunto ordenado de elementos únicos | Pertenencia, salida ordenada |
#include <algorithm>
#include <iostream>
#include <map>
#include <string>
#include <vector>
int main() {
std::vector<int> nums{3, 1, 2, 5};
std::sort(nums.begin(), nums.end());
auto evens = std::count_if(nums.begin(), nums.end(),
[](int n) { return n % 2 == 0; });
std::cout << nums.front() << " " << nums.back() << " " << evens << "\n"; // 1 5 1
std::map<std::string, int> ages{{"bob", 25}, {"ann", 31}};
ages["cy"] = 40;
for (const auto& [name, age] : ages) { // structured bindings
std::cout << name << "=" << age << " "; // ann=31 bob=25 cy=40
}
std::cout << "\n";
}[](int n) { ... } es una lambda, una función anónima escrita en línea. Las plantillas son lo que permite que un mismo std::vector funcione con cualquier tipo de elemento; puedes escribir las tuyas con template <typename T>.
Característica distintiva: RAII y punteros inteligentes
RAII (Resource Acquisition Is Initialization) significa que un objeto es dueño de un recurso: el constructor lo adquiere y el destructor lo libera. Como los destructores se ejecutan automáticamente cuando un objeto sale del ámbito, incluso si se lanza una excepción, la memoria, los archivos y los bloqueos no pueden filtrarse. Los punteros inteligentes aplican RAII a la memoria del heap.
#include <iostream>
#include <memory>
struct Noisy {
int id;
explicit Noisy(int i) : id(i) { std::cout << "create " << id << "\n"; }
~Noisy() { std::cout << "destroy " << id << "\n"; }
};
int main() {
auto a = std::make_unique<Noisy>(1); // sole owner
{
auto b = std::make_shared<Noisy>(2); // shared, reference-counted
auto c = b; // two owners now
std::cout << "owners: " << b.use_count() << "\n";
} // destroy 2 happens here
std::cout << "end of main\n";
} // destroy 1 happens hereSalida: create 1, create 2, owners: 2, destroy 2, end of main, destroy 1. En código moderno casi nunca deberías escribir new y delete tú mismo.
Manejo de errores con excepciones
Lanza un objeto de excepción y captúralo por referencia const. Los tipos de excepción estándar están en <stdexcept>. Las funciones que nunca lanzan excepciones pueden marcarse como noexcept.
#include <iostream>
#include <stdexcept>
#include <string>
int parseAge(const std::string& text) {
int age = std::stoi(text); // throws std::invalid_argument on "abc"
if (age < 0) throw std::out_of_range("negative age");
return age;
}
int main() {
try {
std::cout << parseAge("41") << "\n"; // 41
std::cout << parseAge("abc") << "\n";
} catch (const std::invalid_argument&) {
std::cout << "not a number\n"; // not a number
} catch (const std::exception& e) {
std::cout << "error: " << e.what() << "\n";
}
}Para fallos esperados, muchos proyectos devuelven std::optional<T> (C++17) o std::expected<T, E> (C++23) en lugar de lanzar excepciones.
Un programa completo pequeño: frecuencia de palabras
#include <algorithm>
#include <cctype>
#include <fstream>
#include <iostream>
#include <string>
#include <unordered_map>
#include <vector>
int main(int argc, char* argv[]) {
if (argc != 2) {
std::cerr << "usage: " << argv[0] << " FILE\n";
return 1;
}
std::ifstream in(argv[1]);
if (!in) {
std::cerr << "cannot open " << argv[1] << "\n";
return 1;
}
std::unordered_map<std::string, int> counts;
std::string word;
while (in >> word) {
std::string clean;
for (unsigned char ch : word)
if (std::isalpha(ch)) clean += static_cast<char>(std::tolower(ch));
if (!clean.empty()) ++counts[clean];
}
std::vector<std::pair<std::string, int>> top(counts.begin(), counts.end());
std::sort(top.begin(), top.end(),
[](const auto& a, const auto& b) { return a.second > b.second; });
for (size_t i = 0; i < top.size() && i < 5; ++i)
std::cout << top[i].second << " " << top[i].first << "\n";
}Compila con g++ -std=c++20 -O2 words.cpp -o words y ejecuta ./words notes.txt. El archivo se cierra automáticamente cuando in sale del ámbito, lo cual es de nuevo RAII.
Errores comunes de principiantes
- Escribir
using namespace std;en archivos de cabecera, lo que contamina todos los archivos que los incluyen. - Pasar por valor objetos grandes como
std::vectorcuando bastaría unconst&. - Mezclar con signo y sin signo en comparaciones, como
int ifrente av.size(). Activa las advertencias para verlo. - Conservar un iterador o una referencia a un
std::vectordespués depush_back, que puede mover los elementos. - Usar
newydeleteen bruto en lugar destd::make_unique,std::vectoru otros tipos propietarios.
Adónde ir después
Usa cppreference.com como referencia del lenguaje y la biblioteca, y las C++ Core Guidelines (editadas por Bjarne Stroustrup y Herb Sutter) para las buenas prácticas. El sitio de la Standard C++ Foundation, isocpp.org, tiene una página de inicio con libros recomendados. Aprende CMake para construir proyectos y pasa después a la semántica de movimiento, las plantillas, constexpr y la concurrencia con std::thread.
Preguntas frecuentes
¿Debo aprender C antes que C++?
No es obligatorio. La enseñanza del C++ moderno empieza con std::string, std::vector y las clases, y presenta más tarde los punteros en bruto y los arrays al estilo C. Aprender C primero sí ayuda a entender la memoria.
¿Qué estándar de C++ debo usar?
C++17 y C++20 están bien soportados por los GCC, Clang y MSVC actuales, y el soporte de C++23 está creciendo. Elige uno de forma explícita con una opción como -std=c++20.
¿Cuál es la diferencia entre un puntero y una referencia?
Una referencia es un alias que debe enlazarse al crearse y no puede reasignarse ni ser nula. Un puntero almacena una dirección, puede ser nulo y se puede cambiar para que apunte a otro sitio.
¿Es difícil aprender C++?
El lenguaje es grande, así que cuenta con una curva de aprendizaje más larga que la de Python o Go. Ceñirte a las características modernas, los contenedores estándar y los punteros inteligentes evita la mayoría de los problemas clásicos.