Tipos de datos
Esta sentencia es una pseudo declaración de una variable en C++:
TipoDeDato NombreDeVariable;
En el lenguaje C++ existen dos categorías de tipos de datos: fundamentales y derivados XYZ. Los tipos de datos fundamentales son los propios del lenguaje, mientras que el otro tipo de dato lo forma el usuario por derivaciones de los datos fundamentales y combinaciones de tipos.
Antes de entrar en el análisis de los distintos tipos debemos considerar que hay algo que es común a todos los tipos: la necesidad de dar un nombre a la variable. El nombre de la variable es lo que permite poder recuperar su valor luego de almacenarla en alguna dirección de la memoria. Como sucederá a lo largo del análisis de los distintos componentes del lenguaje, C++ otorga gran flexibilidad para asignar nombre a las variables. Quizá más de la que muchos programadores se merecen. Por eso se recomienda que las variables tengan nombres significativos y legibles para quien no escribió el programa original: es mucho mejor una variable que se denomina CódigoPaís que otra que se denomina cp. La nomenclatura de variables de C++ tiene estas reglas básicas:
- Se pueden usar letras, números y el guión bajo. Pero no puede empezar por un número.
- La longitud de un nombre de variable no tiene un límite.
- C++ distingue entre minúsculas y mayúsculas. Contador es una variable y contador es otra variable. No obstante, por razones de legibilidad, no se recomienda que en el mismo programa haya dos nombres de variables que sólo difieran por el uso de minúsculas y mayúsculas.
- No se pueden usar palabras claves de C++.
- Los nombres que empiezan por guión bajo (_) están reservados para funcionalidades especiales de la implementación y no deberían utilizarse.
Aquí resumiremos algunas definiciones válidas e inválidas para C++:
int Contador; // correcta
int Contador_2; // correcta
int 2contador; // incorrecta no debe empezar con un número
int int; // incorrecta, usa una palabra reservada de C++
int Contador#2; // incorrecta, usa un carácter no permitido
int _Contador; // correcta, pero su uso está reservado
int Contador_para_sumar_todos_los_casos_que_pasan_por_aquí;
// correcta, pero algo exagerada
Tipos fundamentales
Básicamente, los tipos fundamentales representan números enteros y los reales (coma flotante). Los tipos enteros representan números sin parte fraccionaria. En C++ hay diversos tipos de enteros que se diferencian principalmente por su capacidad de almacenamiento y, por consecuencia, por el espacio de memoria ocupado. Otro factor que determina el tipo es su posibilidad de almacenar el signo del número. En la tabla 1.1 se detallan los tipos fundamentales.
TIPO | TAMAÑO | CARACTERÍSTICAS |
chr | 1 byte |
Normalmente un ASCII. Se describe entre apóstrofos. Se considera como tipo entero, lo que implica que se pueden hacer operaciones lógicas o aritméticas. Puede ser con signo (-128 a 127) o sin signo (0 a 255), signed o unsigned, respectivamente. Ejemplos: char a; // variable a sin asignación de valor char b = “b”; |
short | 2 bytes |
Es un tipo entero. Puede ser con signo o sin signo (signed o unsigned). Por defecto se asume con signo, rango de valores -32768 y 32767. Si es sin signo el rango de valores de la variable es 0 a 65535. También se puede especificar short int (es un sinónimo a short). Ejemplos: short n; // variable n ocupa 2 bytes y tiene signo short m = 3000; // definición y asignación en una sentencia |
int | 4 bytes |
Es un tipo entero. Puede ser con signo o sin signo (signed o unsigned). Por defecto se asume con signo. Es igual o mayor a short e igual o menor a long. Ejemplos: int n; // variable n ocupa 4 bytes y tiene signo int m = 3000; // definición y asignación en // una sentencia unsigned int p; // variable p entero sin signo |
_intn | var. |
Es un tipo entero. Puede ser con signo o sin signo (signed o unsigned). Por defecto se asume con signo. n indica la longitud en bits de la variable. Puede ser 8, 16, 32 y 64 bits. Ejemplos: _int16 n; // variable n ocupa 2 bytes y tiene signo _int64 m; // variable de 64 bits y tiene signo |
long | 4 bytes |
Es un tipo entero. Puede ser con signo o sin signo (signed o unsigned). Por defecto se asume con signo. También se puede especificar long int (es un sinónimo a long). Ejemplos: unsigned long int n; // variable n ocupa 4 bytes y no tiene signo signed long m; // long m sería una definición idéntica long n = 3000L; // el sufijo L define con seguridad el tipo unsigned long p ; |
float | 4 bytes |
Es de tipo flotante. Es el de menor precisión dentro de los tipos flotantes. Permite representar valores entre números enteros. Ejemplo: float q ; |
double | 8 bytes |
Es de tipo flotante. Es de precisión mayor que el tipo float. Permite representar valores entre números enteros. Ejemplo: double r ; |
long double | 8 bytes |
Es de tipo flotante. Es de precisión mayor que el tipo double. Permite representar valores entre números enteros. Ejemplo: long double r ; |
void | – |
Tipo nulo. No se pueden definir variables de tipo void, pero es un tipo de dato que tiene diversos usos especiales dentro un programa C++. Permite especificar que una función no devuelve valores. Permite definir el tipo base de apuntadores a objetos de tipo desconocido. Ejemplo: void suma(); //la función suma no devuelve valor void* xx; // apuntador a un objeto de tipo desconocido void var2; // error: no hay objetos de tipo void |
La enumeración es un tipo de dato especial que permite contener un conjunto de valores especificado por el usuario. En realidad, es un tipo que guarda muchas semejanzas con el tipo entero ya que con este tipo de dato se pueden hacer operaciones aritméticas.
Ejemplo:
enum color {ROJO, VERDE, AZUL}; // VERDE es de tipo color
// cada enumeración es de un tipo distinto
De manera predeterminada, el compilador asigna 0 a ROJO, 1 a VERDE y 2 a AZUL. Pero también se podría codificar:
enum color {ROJO=3, VERDE=1, AZUL}; //AZUL queda como 2
color nuevocolor;
nuevocolor = ROJO + AZUL; // podemos hacer operaciones aritméticas
Tipos derivados
Estos tipos se derivan directamente de otros. Estos tipos permiten hacer referencias a otros tipos de datos o permiten realizar la transformación de los tipos de datos, tal como ocurren con las funciones. Los tipos derivados son los siguientes:
- Punteros
- Matrices
- Referencias
- Estructuras
También se consideran tipos derivados a las funciones y a las clases, pero esos temas se tratarán más adelante.
Punteros (apuntador o pointer)
Para cualquier tipo T, T* es el tipo «apuntador a T» (también conocido como «puntero a T»). Dicho de otra manera, una variable de tipo T* puede contener una dirección de un objeto de tipo T. No puede haber apuntadores a referencias (se verá luego). Por ejemplo:
int k = 2;
int* pak = &k; // pak contiene la dirección de k
La operación principal de un apuntador es la indirección, o sea, devolver el valor del apuntador.
int k = 2;
int* pak = &k; // pak contiene la dirección de k
int j = *pak; // el valor apuntado por pak (o sea, 2) se asigna a j
Se podría pensar que si el apuntador es siempre una dirección, ¿para qué hay que declarar el tipo del apuntador? El apuntador es una dirección, por lo tanto, por su sólo contenido (una dirección) es imposible indicar la longitud y el formato de los datos que hay almacenados en esa dirección. Se sabe la dirección de comienzo, pero no la dirección de final, ni tampoco su formato interno. Por ese motivo, la única manera de conocer lo que hay en la dirección apuntada por un apuntador es mediante el tipo del apuntador.
Matrices de variables
Una matriz representa un conjunto de datos con un tipo y nombre común y en la cual se puede hacer referencia a sus elementos por medio de un campo índice. Puede haber, por ejemplo, matrices de elementos de tipo de dato float o matrices de tipo de dato char. La cantidad de elementos de la matriz se define mediante un valor entre corchetes. La matriz puede una dimensión (un vector) o las que permita el compilador. Los elementos de la matriz se direccionan desde 0, por lo cual si se define [8], el último elemento será [7]. A continuación se muestran varios ejemplos de definición de matrices:
int campo1[8]; // campo1 tiene 8 elementos de tipo int
float campo2[4] [10]; // La matriz es de 4 * 10: 40 ítems
char campo3[2] [5] [12]; // aquí tiene 2 * 5 * 12 ítems: 120 ítems
float campo4[2] ={5.1, 1.3};// Matriz de dos elementos inicializados
float campo5[] = {0.1, 3.2};// Define implícitamente que son 2 ítems
char campo6[4] ={‘C’, ‘+’, ‘+’, ‘\0’};// cadena C++
char campo7[4] = “C++”; // equivalente al anterior
El nombre de una matriz puede usarse como apuntador a su elemento inicial.
Si se tiene una matriz de caracteres (podría ser de otro tipo):
char z[]={‘h’,’o’,’l’,’a’);
char* pach = z; // apuntador al elemento inicial (‘h’)
// se produce una conversión implícita
double deudas[3] = {15000.0, 22000.0, 50000.0};
double * ptrd = deudas; // apuntador a deudas (0)
ptrd tiene la dirección de deudas, del primer elemento de deudas. Con el apuntador se pueden realizar operaciones aritméticas, pero se debe tener en cuenta que si se suma uno a un apuntador a tipo de dato double, ese apuntador se posicionará en la siguiente dirección double, o sea, 8 bytes más adelante.
ptrd = ptrd + 1; // incrementará el valor de ptrd en 8 bytes
Ahora ptrd apunta a la dirección de deudas (1).
Si ptrd hubiese apuntado a un tipo de dato short, ese incremento sólo hubiera sido de 2 bytes, pero la operación de suma tendría el mismo aspecto:
ptrs = ptrs + 1; // incrementará el valor de ptrs en 2 bytes
Referencias
Una referencia es una manera de llamar a un objeto con un nombre alternativo. Su principal uso es para especificar argumentos y valores de retorno de funciones. Por ejemplo:
char l1 = ‘H’;
char& lg = l1; // lg y l1 hacen referencia al mismo dato
// se pueden usar indistintamente
En el momento de la declaración de la referencia se asigna el valor y luego ya no puede hacer referencia a otro campo. Se puede hacer referencias a cualquier tipo, salvo a referencias. La referencia se puede asimilar a un apuntador constante a un objeto, pero sin las posibilidades de manipulación de los apuntadores.
Estructuras
Anteriormente se había visto que una matriz es un conjunto de elemento de igual tipo. Una estructura es un conjunto de elementos de distinto tipo. Por ejemplo:
struct inventario {
int nro;
char* descrip;
long int cantidad;
};
Aquí se ha definido un tipo de dato de usuario compuesto por datos de distinto tipo. Las variables del tipo definido se pueden declarar de la misma manera que otras variables y a sus miembros componentes (en este caso, número, descripción y cantidad) se puede acceder de la siguiente manera:
…
inventario p3;
p3.nro = 12 // se usa el punto para referirnos
// a un miembro de la estructura
p3.descrip = ‘clavos’;
Para acceder a la estructuras se suelen usar apuntadores usando el operador -> (indirección de apuntador de estructura).
p->nro es lo mismo que escribir (*p).nro.
Uniones
Una unión es similar sintácticamente a una estructura pero conceptualmente funciona distinto. Todas las variables que se definen dentro de una unión comparten el espacio físico. La longitud de una Unión es igual a la de la variable más larga. Dicho de otra manera, todos los elementos de la unión tiene desplazamiento cero. En la práctica, sólo se puede usar un elemento de la unión por vez.
union defineunion
{
int v1;
double v2;
char v3;
}
La longitud de esta unión coincide con la longitud de la variable double, o sea, ocho bytes.
defineunion redefine;
redefine.v1 = 8;
cout <<redefine.v1;
redefine.v2 = 3.1415;
cout <<redefine.p2;
Constantes
A la declaración de un objeto se puede agregar el calificador Const para indicar que ese objeto no cambiará después de su inicialización. Como un objeto constante no puede ser asignado, debe ser inicializado en su declaración. Nunca puede estar en el lado izquierdo de una asignación.
La declaración de una constante tiene esta sintaxis:
const int IVA1 = 16; // define el porcentaje de IVA como constante
const int IVA2[] = {0, 4, 4, 16, 33};
// define los porcentajes de IVA como una
// matriz de constantes enteras
const int IVA3; // error, no está inicializada
IVA1 = 12; // error, no se puede modificar
Operandos y Operadores
El lenguaje C++ tiene una cantidad apreciable de operadores. Podemos definir los operadores básicos:
- Aritméticos
- Relacionales
- Lógicos
- De bit
- Asignación
- De indirección
- Condicional
- sizeof
- De incremento/decremento
Los operadores actúan sobre operandos y los operandos pueden tener distintas formas: pueden ser variables, constantes, expresiones, etc. Antes de entrar en el tema de los operadores debemos definir el concepto del operando LValue. Cuando se indica que un operando es LValue se quiere expresar que es un operando que puede estar a la izquierda de una operación de asignación. Representa una dirección de memoria y es un objeto al que se le pueden asignar valores. Hay operandos que no cumplen estas condiciones (una constante o una expresión).
operadores aritméticos
Como es de suponer, C++ usa operadores para hacer los cálculos. Tiene cinco operadores aritméticos básicos: suma, resta, multiplicación, división y módulo. Una expresión es el conjunto de los operandos con su operador. Por ejemplo, las siguiente son expresiones:
int deuda = 100 – 40; // deuda es 60
int cuota = deuda / 10; // los operandos pueden ser variables
Orden de precedencia
i
nt cuota = deuda / 10 + 10; // ¿cuál es el resultado? 16 ó 3
Cuando en una operación aritmética hay más de un operador surge el problema de la precedencia. Por suerte, C++ sigue las mismas reglas que el álgebra. Primero se resuelven las operaciones de multiplicación, división y módulo y luego la suma y la resta. En el ejemplo anterior, el resultado correcto es 16. Tal como se utiliza en el álgebra, los paréntesis permiten modificar las prioridades.
int cuota = deuda / (10 + 10); // ¿cuál es el resultado? Ahora es 3.
Asociatividad
El orden de precedencia no resuelve todos los problemas. Si no se utilizan paréntesis pueden presentarse dudas sobre cómo actúa C++ ante ciertas expresiones. Por ejemplo,
int cuota = 600 * 10 / 10; // ¿cuál es el resultado? 60 o 600
La multiplicación y la división comparten el mismo orden de precedencia. En estos casos C++ utiliza la regla de asociatividad (izquierda a derecha o derecha a izquierda). La asociatividad izquierda a derecha indica que si dos operadores que actúan sobre un mismo operando tienen el mismo orden de precedencia, se aplica primero el operador de la izquierda. En el ejemplo anterior, el resultado correcto es 600, ya que la multiplicación y la división tienen asociatividad izquierda a derecha.
Más adelante se resumen los operadores y se indica, entre otras cosas, el tipo asociatividad.
Particularidad de la división
Al realizar una división se puede presentar la circunstancia que los dos operandos sean enteros, ental caso el resultado será entero y se perderá la parte fraccionaria. Si alguno (o los dos) de los operandos es de coma flotante, el resultado mantendrá esa precisión.
El operador módulo
El operador módulo nos permite obtener el resto de una división entre enteros.
int flanes = 5;
int hijos = 3;
int paraelpadre = flanes % hijos; // el padre come 2 flanes
Conversiones
Las operaciones aritméticas se pueden realizar con más de una docena de tipos diferentes. C++ simplifica gran parte del problema realizando una serie de conversiones de manera automática.
conversiones en asignaciones
Como regla general, cuando una variable se asigna a otra de distinto tipo, C++ se encarga de convertir la variable al tipo de la variable receptora antes de hacer la asignación.
Si el tipo de la variable receptora tiene mayor capacidad que el tipo de la variable emisora, no supone ningún problema. Por ejemplo, asignar una variable de tipo short a una variable de tipo long simplemente hará que la variable ocupe 2 bytes más.
El problema surge cuando el tipo de la variable receptora no permite almacenar el valor por excederse en el rango (en cuyo caso, el comportamiento de C++ varía entre las distintas implementaciones) o cuando el tipo de la variable receptora puede almacenar el valor pero perdiendo precisión en el resultado (por ejemplo, por perder la parte fraccionaria).
conversiones en expresiones
Cuando en una expresión se combinan dos tipos aritméticos diferentes C++ puede realizar conversiones automáticas. Hay dos tipos de conversiones automáticas. Primero, C++ realiza una conversión de promoción a int para eliminar una serie de tipos. Estos tipos que promueven a tipo int son los siguientes: short, char y bool (True es promovido a 1 y False es promovido a 0). Segundo, C++ realiza otra conversión automática cuando se encuentra con dos operandos de distinto tipo. Esta conversión generalmente supone que el tipo más pequeño se convierte al más grande.
El compilador realiza la conversión aplicando la siguiente lista en el orden en que se detalla. Es decir, cuando se encuentra el caso para el par de tipos aritméticos de la operación se realiza la conversión y ya no se sigue comprobando el resto de la lista.
Si un operando es | y el segundo es | se convierte a |
long double | resto (*) | long double |
double | resto | double |
float | resto | float |
unsigned long | resto | unsigned long |
long | unsignet int | unsigned int olong (**)long |
long | resto | long |
unsigned int | resto | insigned int |
int | int | no hay conversión |
(*) Resto significa cualquier otro tipo.
(**) Depende del tamaño relativo de los tipos, en lo psible se convierte a long.
Conversión explícita (CAST)
C++ nos permite realizar conversiones de manera explícita. De esa manera, podemos utilizar el tipo deseado para una operación. Por ejemplo, si para una operación nos conviene que una variable int sea considerada como char tendremos que codificar lo siguiente:
int k1;
char d;
….
d = char (k1);
También se puede codificar, con el mismo resultado, de la siguiente manera:
d = (char) k1; // sintaxis propia de C pero válida en C++
Pero se prefiere la primera forma que se parece a la llamada de una función.
Operadores relacionales
C++ provee seis operadores relacionales para poder comparar números. El resultado de la comparación se reduce a dos posibilidades: True y False. Los operadores relacionales son los siguientes:
- > mayor que
- < menor que
- >= mayor o igual
- <= menor o igual
- == igual
- != no igual
Los operadores tienen una precedencia menor que los operadores aritméticos. Por ejemplo, si se tiene la siguiente expresión:
a + 3 > 5 * k
Esta expresión podría leerse de la siguiente manera:
(a + 3) > (5 * k) // correcta
Pero si se convirtiese de esta manera, el resultado sería incorrecto:
a + ( 3 > 5) * k // incorrecta
Operadores lógicos
El operador lógico AND (&&) realiza el producto lógico entre dos expresiones. El operador lógico AND tiene menor precedencia que los operadores relacionales. Si la expresión de la izquierda es False, el resultado será False sin necesidad de evaluar la expresión de lado derecho. Estos son algunos ejemplos del operador &&:
-3 && 3 // expresión True, valor 1
6 < 2 && 8 > 7 // expresión False, valor 0
7 > 1 && 7 == 7 // expresión True, valor 1
El operador OR || indica una suma lógica de dos expresiones. El operador lógico OR tiene menor precedencia que los operadores relacionales y que el operador lógico AND. Estos son algunos ejemplos del operador OR:
4 == 4 || 2 > 4 // expresión True
0 || 0 // expresión False
El operador OR actúa como un separador de expresiones, es decir, se evalúa la expresión que queda a la izquierda del operador y luego la expresión de la derecha.
El operador lógico NOT (!) invierte el valor de la expresión que tiene a continuación. Este operador tiene una precedencia mayor que todas las operaciones aritméticas (y obviamente que las relacionales). Estos son algunos ejemplos del uso del operador NOT:
!(k > m) // si k es mayor a m, el resultado será False
!k > 1 // k puede ser True o False (0 o 1)
// según sea el valor de k 0 o no cero
// el resultado siempre será False
Operadores de bit
Los operadores de bit sólo operan con valores enteros. Los operadores de bit son los siguientes:
- & producto binario de bits
- | suma binaria de bits (OR)
- ^ suma exclusiva de bits (XOR)
- << desplazamiento de bits hacia la izquierda
- >> desplazamiento de bits hacia la derecha
- ~ unario. complemento a 1
- – unario. complemento a 2
operador AND de bit (&)
El operador & multiplica bit a bit dos números enteros y da como resultado un nuevo valor entero. Cada bit resultante será cero si alguno de los dos bits de la multiplicación son cero.
Los operadores a nivel de bit son análogos los operadores lógicos normales, pero actúan bit a bit. Por ejemplo,
2 & 1 // False, es cero
2 && 1 // true, valor 1
operador OR de bit (|)
El operador | suma bit a bit dos números enteros y da como resultado un nuevo valor entero. Cada bit resultante será uno si alguno (o ambos) de los dos bits de la suma es uno.
operador XOR de bit (^)
El operador ^ suma exclusiva bit a bit dos números enteros y da como resultado un nuevo valor entero. Cada bit resultante será uno sólo si uno de los dos bits de la suma es uno.
operador desplazamiento de bits a la izquierda (<<)
El operador << actúa sobre valores enteros y tiene la siguiente sintaxis:
valor1 << desp1
En donde desp1 es un valor entero que indica la cantidad de bits que hay que desplazar hacia la izquierda en valor1. Se produce un descarte de bits por la izquierda y una entrada de bits (en cero) por la derecha.
operador desplazamiento de bits a la derecha (>>)
El operador >> actúa sobre valores enteros y tiene la siguiente sintaxis:
valor1 >> desp1
En donde desp1 es un valor entero que indica la cantidad de bits que hay que desplazar hacia la derecha en valor1. Se produce un descarte de bits por la derecha y una entrada de bits (en cero) por la izquierda.
Operador unario negación de bit (~)
El operador ~ actúa sobre los bits realizando el complemento 1, o sea, cambia unos por ceros.
Hay una analogía en el funcionamiento del operador de negación NOT (si la expresión evalúa True, el resultado será False) con el operador de negación de bit (los bits que están en cero pasan a 1 y viceversa).
Operador unario (-)
El operador – actúa sobre los bits realizando el complemento 2, o sea, cambia el signo.
Operadores de asignación
El operador de asignación hace que el operando que está a la izquierda del signo igual tome el valor de la expresión que está a la derecha del signo igual.
a = b + 2;
a = a + 2;
a+= 2; // es igual al anterior. El operador de asignación
a*=2; // también se pueden combinar con otros operadores
a/=2;
Operadores de indirección
El apuntador contiene la dirección de un valor y el nombre de un apuntador representa esa dirección. El operador *, denominado operador de indirección (dereferencing operator), se refiere al valor de que hay en esa dirección. Por ejemplo, si ptr1 es un apuntador, entonces ptr1 representa una dirección y *ptr1 representa el valor que hay almacenado en esa dirección.
El símbolo * es el mismo que se utiliza para la multiplicación, C++ utiliza el contexto para determinar si lo que estamos haciendo es una multiplicación o una indirección.
Operador condicional
El operador condicional ? se suele utilizar como alternativa a la sentencia if else. Es el único operador que requiere tres operandos. La sintaxis es la siguiente:
expresión1 ? expresión2 : expresión3
Por ejemplo,
cmdOK = (ingresos > egresos) ? True : False;
Si la expresión1 es verdadera se asigna (en este caso) True a cmdOK, en caso contrario, se asigna False.
Se podría haber expresado de la siguiente manera:
if ingresos > egresos
cmdOK = True;
else
cmdOK = False;
La sintaxis con el operador ? es más concisa pero a veces no tan legible como la conocida sentencia if else.
Operador sizeof
El operador sizeof nos permite conocer el tamaño en bytes de un tipo o de una variable. Supóngase que se tienen estas definiciones:
int k;
long j:
char b;
sizeof (k) no da el valor 2
sizeof (j) nos da el valor 4
sizeof (b) nos da el valor 1
sizeof (float) nos da el valor 4 (tipo float ocupa 4 bytes)
Si se aplica sizeof a una referencia, nos dará como resultado la cantidad de bytes ocupados por el objeto que apunta la referencia.
Operador de incremento/decremento
Éste es un operador muy peculiar de C++. El operador ++ suma uno a la variable, la que debe ser de tipo LValue (variable que puede estar a la izquierda de una expresión, o sea, que puede recibir una asignación de valor).
i++; // suma uno a i
i = i +1; // expresión equivalente
k = 1;
k++; // k es igual a 2
j = k++; // j es igual a 2 pero k vale 3 ¿Porqué?
Existen dos tipos de incrementos:
- Preincremento: suma y asigna (++ está a la izquierda de la variable)
- Postincremento: asigna y suma (++ está a la derecha de la variable)
j = ++k; // j es igual a 4 y k es igual a 4
j = k++; // j es igual a 4 y k es igual a 5
Todo lo dicho para el operador incremento es transladable para el operador decremento (—), la diferencia es que este operador resta en lugar de sumar.
j = (++k)—; // j es igual a 6, k sigue siendo 5