cv.cm

Programación · Lenguajes de programación

Tutorial de C++ para principiantes

8 min de lectura

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.

Abrir Convertidor binario

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, world

std::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.

TipoEjemploNotas
int, long longint n{42};Enteros con signo
doubledouble x{3.14};Punto flotante de 64 bits
boolbool ok{true};true o false
charchar c{'A'};Un byte
std::stringstd::string s{"hi"};Texto que puede crecer, de <string>
autoauto 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.

ContenedorEquivale aÚsalo para
std::vector<T>Array dinámicoEl contenedor secuencial por defecto
std::array<T, N>Array fijoTamaño conocido en tiempo de compilación
std::map<K, V>Diccionario ordenadoClaves ordenadas, búsqueda en O(log n)
std::unordered_map<K, V>Tabla hashBúsqueda rápida, O(1) de media
std::set<T>Conjunto ordenado de elementos únicosPertenencia, 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 here

Salida: 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::vector cuando bastaría un const&.
  • Mezclar con signo y sin signo en comparaciones, como int i frente a v.size(). Activa las advertencias para verlo.
  • Conservar un iterador o una referencia a un std::vector después de push_back, que puede mover los elementos.
  • Usar new y delete en bruto en lugar de std::make_unique, std::vector u 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.

Todos los tutoriales de programación