Sistema de tipos de C++

El concepto de tipo es importante en C++. Cada variable, argumento de función y valor devuelto por una función debe tener un tipo para compilarse. Además, el compilador asigna implícitamente un tipo a todas las expresiones (incluidos los valores literales) antes de que se evalúen. Algunos ejemplos de tipos incluyen tipos integrados, como int almacenar valores enteros, double para almacenar valores de punto flotante o tipos de biblioteca estándar, como la clase std::basic_string para almacenar texto. Puede crear su propio tipo si define un objeto class o struct. El tipo especifica la cantidad de memoria asignada para la variable (o resultado de expresión). El tipo también especifica los tipos de valores que se pueden almacenar, cómo el compilador interpreta los patrones de bits en esos valores y las operaciones que puede realizar en ellos. Este artículo contiene información general sobre las principales características del sistema de tipos de C++.

Terminología

Tipo escalar: tipo que contiene un único valor de un intervalo definido. Los escalares incluyen tipos aritméticos (valores enteros o de punto flotante), miembros de tipo de enumeración, tipos de puntero, tipos de puntero a miembro y std::nullptr_t. Los tipos fundamentales suelen ser tipos escalares.

Tipo compuesto: tipo que no es un tipo escalar. Los tipos compuestos incluyen tipos de matriz, tipos de función, tipos de clase (o estructura), tipos de unión, enumeraciones, referencias y punteros a miembros de clase no estáticos.

Variable: nombre simbólico de una cantidad de datos. El nombre se puede usar para acceder a los datos a los que hace referencia en todo el ámbito del código donde se define. En C++, la variable se usa a menudo para hacer referencia a instancias de tipos de datos escalares, mientras que las instancias de otros tipos normalmente se denominan objetos.

Objeto: para simplificar y mantener la coherencia, en este artículo se usa el término objeto para hacer referencia a cualquier instancia de una clase o estructura. Cuando se usa en el sentido general, incluye todos los tipos, incluso variables escalares.

Tipo POD (datos antiguos sin formato): esta categoría informal de tipos de datos de C++ hace referencia a los tipos que son escalares (consulte la sección de tipos fundamentales) o que son clases POD. Una clase POD no tiene miembros de datos estáticos que tampoco son POD y no tiene constructores definidos por el usuario, destructores definidos por el usuario ni operadores de asignación definidos por el usuario. Además, las clases POD no tienen funciones virtuales, clases base ni ningún miembro de datos no estático privado o protegido. Los tipos POD suelen utilizarse para el intercambio de datos externos, por ejemplo, con un módulo escrito en lenguaje C (que solo tiene tipos POD).

Especificar tipos de variable y función

C++ es un lenguaje fuertemente tipado y un lenguaje con tipo estático; cada objeto tiene un tipo y ese tipo nunca cambia. Al declarar una variable en el código, debe especificar explícitamente su tipo o usar la palabra clave auto para indicar al compilador que deduzca el tipo desde el inicializador. Al declarar una función en el código, debe especificar el tipo de su valor devuelto y de cada argumento. Use el tipo void de valor devuelto si la función no devuelve ningún valor. La excepción es cuando se usan plantillas de función, que permiten argumentos de tipos arbitrarios.

Después de declarar por primera vez una variable, no puede cambiar su tipo en algún momento posterior. Sin embargo, puede copiar el valor devuelto de la variable o el valor devuelto de una función en otra variable de otro tipo. Este tipo de operaciones se denominan conversiones de tipo. Estas conversiones a veces resultan necesarias, aunque también pueden producir errores o pérdidas de datos.

Al declarar una variable de tipo POD, se recomienda encarecidamente inicializarla , lo que significa darle un valor inicial. Una variable, hasta que se inicializa, tiene el valor "no utilizado", que se compone de los bits que estaban previamente en esa ubicación de memoria. Es un aspecto importante de C++ recordar, especialmente si procede de otro lenguaje que controla la inicialización. Cuando se declara una variable de tipo de clase que no es POD, el constructor controla la inicialización.

En el ejemplo siguiente se muestran algunas sencillas declaraciones de variable con descripciones de cada una de ellas. En el ejemplo se muestra también cómo el compilador utiliza la información de tipo para permitir o no permitir que posteriormente se realicen ciertas operaciones en la variable.

int result = 0;              // Declare and initialize an integer.
double coefficient = 10.8;   // Declare and initialize a floating
                             // point value.
auto name = "Lady G.";       // Declare a variable and let compiler
                             // deduce the type.
auto address;                // error. Compiler cannot deduce a type
                             // without an intializing value.
age = 12;                    // error. Variable declaration must
                             // specify a type or use auto!
result = "Kenny G.";         // error. Can't assign text to an int.
string result = "zero";      // error. Can't redefine a variable with
                             // new type.
int maxValue;                // Not recommended! maxValue contains
                             // garbage bits until it is initialized.

Tipos (integrados) fundamentales

A diferencia de algunos lenguajes, C++ no tiene un tipo base universal del que se deriven todos los demás tipos. El lenguaje contiene muchos tipos fundamentales, también conocidos como tipos integrados. Estos tipos incluyen tipos numéricos como int, double, long, boolademás de los char tipos y wchar_t para caracteres ASCII y UNICODE, respectivamente. La mayoría de los tipos fundamentales (excepto bool, double, wchar_t y tipos relacionados) tienen versiones unsigned, que modifican el intervalo de valores que la variable puede almacenar. Por ejemplo, un valor int, que almacena un entero de 32 bits con signo, puede representar un valor comprendido entre -2 147 483 648 y 2 147 483 647. Un unsigned int, que también se almacena como 32 bits, puede almacenar un valor de 0 a 4.294.967.295. El número total de valores posibles en cada caso es el mismo; solo cambia el intervalo.

El compilador reconoce estos tipos integrados y tiene reglas integradas que rigen las operaciones que puede realizar en ellos y cómo se pueden convertir a otros tipos fundamentales. Para obtener una lista completa de los tipos integrados y sus límites numéricos y de tamaño, consulte Tipos integrados.

En la ilustración siguiente se muestran los tamaños relativos de los tipos integrados en la implementación de Microsoft C++:

Diagram of the relative size in bytes of several built in types.

En la tabla siguiente se muestran los tipos fundamentales usados con más frecuencia y sus tamaños en la implementación de Microsoft C++:

Tipo Tamaño Comentario
int 4 bytes Opción predeterminada para los valores enteros.
double 8 bytes Opción predeterminada para los valores de punto flotante.
bool 1 byte Representa valores que pueden ser true o false.
char 1 byte Se utiliza en los caracteres ASCII de cadenas de estilo C antiguas u objetos std::string que nunca tendrán que convertirse a UNICODE.
wchar_t 2 bytes Representa valores de caracteres “anchos” que se pueden codificar en formato UNICODE (UTF-16 en Windows; puede diferir en otros sistemas operativos). wchar_t es el tipo de carácter que se usa en cadenas de tipo std::wstring.
unsigned char 1 byte C++ no tiene un tipo byte integrado. Use unsigned char para representar un valor de byte.
unsigned int 4 bytes Opción predeterminada para los marcadores de bits.
long long 8 bytes Representa un intervalo mucho mayor de valores enteros.

Otras implementaciones de C++ pueden usar tamaños diferentes para determinados tipos numéricos. Para obtener más información sobre los tamaños y las relaciones de tamaño que requiere el estándar de C++, consulte Tipos integrados.

El tipo void

El void tipo es un tipo especial; no se puede declarar una variable de tipo void, pero puede declarar una variable de tipo void * (puntero a void), que a veces es necesaria al asignar memoria sin formato (sin tipo). Sin embargo, los punteros a void no son seguros para tipos y su uso no se recomienda en C++moderno. En una declaración de función, un void valor devuelto significa que la función no devuelve un valor; su uso como tipo de valor devuelto es un uso común y aceptable de void. Aunque las funciones necesarias del lenguaje C que tienen cero parámetros para declarar void en la lista de parámetros, por ejemplo, fn(void), esta práctica no se recomienda en C++moderno; se debe declarar fn()una función sin parámetros . Para obtener más información, consulte Conversiones de tipos y seguridad de tipos.

const calificador de tipo

Cualquier tipo integrado o definido por el usuario puede ser calificado por la const palabra clave . Además, las funciones miembro pueden calificarse con const e incluso sobrecargarse con const. El valor de un const tipo no se puede modificar después de inicializarlo.

const double PI = 3.1415;
PI = .75; //Error. Cannot modify const variable.

El const calificador se usa ampliamente en declaraciones de función y variable y "corrección const" es un concepto importante en C++; básicamente significa usar const para garantizar, en tiempo de compilación, que los valores no se modifican involuntariamente. Para obtener más información, vea const.

Un const tipo es distinto de su versión noconst ; por ejemplo, const int es un tipo distinto de int. Puede usar el operador const_cast de C++en las raras ocasiones en las que deba quitar la declaración como constante de una variable. Para obtener más información, consulte Conversiones de tipos y seguridad de tipos.

Tipos string

En sentido estricto, el lenguaje C++ no tiene un tipo string integrado. char y wchar_t almacenan caracteres individuales; es necesario declarar una matriz de estos tipos para aproximarse a una cadena y agregar un valor final null (por ejemplo, '\0' en ASCII) al elemento de matriz después del último carácter válido (también denominado cadena de estilo C). En las cadenas de estilo C, era necesario escribir mucho más código o usar funciones de bibliotecas de utilidades de cadena externas. Pero en el lenguaje C++ actual, tenemos los tipos de la biblioteca estándar std::string (para cadenas de caracteres de tipo char de 8 bits) o std::wstring (para cadenas de caracteres de tipo wchar_t de 16 bits). Estos contenedores de la biblioteca estándar de C++ se pueden considerar como tipos de cadena nativos porque forman parte de las bibliotecas estándar que se incluyen en cualquier entorno de compilación de C++ compatible. Use la #include <string> directiva para que estos tipos estén disponibles en el programa. (Si usa MFC o ATL, la CString clase también está disponible, pero no forma parte del estándar de C++). El uso de matrices de caracteres terminadas en NULL (las cadenas de estilo C mencionadas anteriormente) no se recomienda en C++moderno.

Tipos definidos por el usuario

Cuando se define un objeto class, struct, union o enum, esa construcción se usa en el resto del código como si fuera un tipo fundamental. Esa construcción tiene un tamaño conocido en memoria y se aplican ciertas reglas sobre su uso durante la comprobación en tiempo de compilación y, en tiempo de ejecución, durante la vida útil del programa. Las diferencias principales entre los tipos fundamentales integrados y los tipos definidos por el usuario son las siguientes:

  • El compilador no tiene conocimiento integrado de un tipo definido por el usuario. El compilador conoce el tipo la primera vez que encuentra la definición durante el proceso de compilación.

  • El usuario especifica las operaciones que se pueden realizar en el tipo y cómo se puede convertir en otros tipos definiendo (mediante sobrecarga) los operadores adecuados, como los miembros de clase o las funciones que no son miembro. Para obtener más información, consulte Sobrecarga de funciones.

Tipos de puntero

Como en las versiones más antiguas del lenguaje C, C++ sigue permitiendo declarar una variable de un tipo de puntero mediante el declarador * especial (asterisco). Un tipo de puntero almacena la dirección de la ubicación en memoria donde se almacena el valor de datos real. En C++moderno, estos tipos de puntero se conocen como punteros sin procesar y se acceden a ellos en el código a través de operadores especiales: * (asterisco) o -> (guión con mayor que, a menudo denominado flecha). Esta operación de acceso a memoria se denomina desreferenciación. El operador que use depende de si va a desreferenciar un puntero a un escalar o a un puntero a un miembro de un objeto .

Trabajar con tipos de puntero ha sido uno de los aspectos más difíciles y confusos del desarrollo de programación de C y C++. En esta sección se describen algunos hechos y prácticas para ayudar a usar punteros sin procesar si desea. Sin embargo, en C++moderno, ya no es necesario (o recomendado) usar punteros sin procesar para la propiedad del objeto en absoluto, debido a la evolución del puntero inteligente (descrito más al final de esta sección). Sigue siendo útil y seguro usar punteros sin procesar para observar objetos. Sin embargo, si debe usarlos para la propiedad del objeto, debe hacerlo con precaución y con una consideración cuidadosa de cómo se crean y destruyen los objetos que poseen.

Lo primero que debe saber es que una declaración de variable de puntero sin procesar solo asigna suficiente memoria para almacenar una dirección: la ubicación de memoria a la que hace referencia el puntero cuando se desreferencia. La declaración de puntero no asigna la memoria necesaria para almacenar el valor de datos. (Esa memoria también se denomina memoria auxiliar). Es decir, al declarar una variable de puntero sin procesar, se crea una variable de dirección de memoria, no una variable de datos real. Si desreferencia una variable de puntero antes de asegurarse de que contiene una dirección válida para un almacén de respaldo, provoca un comportamiento indefinido (normalmente un error irrecuperable) en el programa. En el siguiente ejemplo se muestra este tipo de error:

int* pNumber;       // Declare a pointer-to-int variable.
*pNumber = 10;      // error. Although this may compile, it is
                    // a serious error. We are dereferencing an
                    // uninitialized pointer variable with no
                    // allocated memory to point to.

En el ejemplo se desreferencia un tipo de puntero que no tiene ninguna memoria asignada para almacenar los datos enteros reales ni una dirección de memoria válida asignada. El código siguiente corrige esto errores:

    int number = 10;          // Declare and initialize a local integer
                              // variable for data backing store.
    int* pNumber = &number;   // Declare and initialize a local integer
                              // pointer variable to a valid memory
                              // address to that backing store.
...
    *pNumber = 41;            // Dereference and store a new value in
                              // the memory pointed to by
                              // pNumber, the integer variable called
                              // "number". Note "number" was changed, not
                              // "pNumber".

En el ejemplo de código corregido se utiliza la memoria local de la pila para crear la memoria auxiliar a la que pNumber apunta. Utilizamos un tipo fundamental para simplificar. En la práctica, los almacenes de respaldo para punteros suelen ser tipos definidos por el usuario que se asignan dinámicamente en un área de memoria denominada montón (o almacén libre) mediante una new expresión de palabra clave (en programación de estilo C, se usó la función anterior malloc() de la biblioteca en tiempo de ejecución de C). Una vez asignadas, estas variables se conocen normalmente como objetos, especialmente si se basan en una definición de clase. La memoria que se asigna con new debe eliminarse mediante la instrucción delete correspondiente (o, si usó la función malloc() para asignarlas, la función free() en tiempo de ejecución de C).

Sin embargo, es fácil olvidarse de eliminar un objeto asignado dinámicamente, especialmente en código complejo, lo que provoca un error de recurso denominado pérdida de memoria. Por este motivo, no se recomienda el uso de punteros sin procesar en C++moderno. Casi siempre es mejor encapsular un puntero sin procesar en un puntero inteligente, que libera automáticamente la memoria cuando se invoca su destructor. (Es decir, cuando el código sale del ámbito del puntero inteligente). Mediante el uso de punteros inteligentes, prácticamente elimina una clase completa de errores en los programas de C++. En el ejemplo siguiente, suponga que MyClass es un tipo definido por el usuario que tiene un método público DoSomeWork();

void someFunction() {
    unique_ptr<MyClass> pMc(new MyClass);
    pMc->DoSomeWork();
}
  // No memory leak. Out-of-scope automatically calls the destructor
  // for the unique_ptr, freeing the resource.

Para obtener más información sobre los punteros inteligentes, consulte Punteros inteligentes.

Para obtener más información sobre las conversiones de puntero, vea Conversiones de tipos y seguridad de tipos.

Para obtener información sobre los punteros en general, consulte Punteros.

Tipos de datos de Windows

En la programación Win32 clásica de C y C++, la mayoría de las funciones usan definiciones de tipos y macros windef.h (definidas en #define) específicas de Windows para indicar los tipos de parámetros y los valores devueltos. Estos tipos de datos de Windows son principalmente nombres especiales (alias) proporcionados a los tipos integrados de C/C++. Para obtener una lista completa de estas definiciones de tipo y las definiciones de preprocesador, consulte Tipos de datos de Windows. Algunas de estas definiciones de tipos, como HRESULT y LCID, son útiles y significativas. Otras, como INT, no tienen ningún significado especial y son solo alias para los tipos fundamentales de C++. Otros tipos de datos de Windows tienen nombres que se provienen de la época de programación de C y de los procesadores de 16 bits, y no tienen ningún propósito o significado en el hardware y sistemas operativos modernos. Hay también tipos de datos especiales asociados a la biblioteca de Windows Runtime, que se muestran como tipos de datos base de Windows Runtime. En C++moderno, la guía general es preferir los tipos fundamentales de C++, a menos que el tipo de Windows comunique algún significado adicional sobre cómo se interpretará el valor.

Más información

Para obtener más información sobre el sistema de tipos de C++, consulte los artículos siguientes.

Tipos de valor
Describe los tipos de valor junto con problemas relacionados con su uso.

Conversiones de tipos y seguridad de tipos
Describe problemas de conversión de tipos comunes y muestra cómo evitarlos.

Consulte también

Aquí está otra vez C++
Referencia del lenguaje C++
Biblioteca estándar de C++