Informática
Desarrollo de programación C++
INTRODUCCION a C++
-
El Lenguaje de Programación C++ se diseñó para :
-
mejorar el lenguaje C
-
apoyar la abstracción de datos
-
apoyar la programación orientada a objetos
-
Un lenguaje apoya un estilo de programación cuando contiene elementos que facilitan la programación con esa técnica.
-
C++ es un superconjunto de C. C++ hereda de C lo siguiente: funciones, aritméticas, selección y construcciones cíclicas, operaciones de E/S, manejo de punteros
-
El mínimo programa en C++ es :
main () {}
al igual que en C, todo programa en C++ debe tener una función main y el programa comienza ejecutando esa función
ALGUNAS MEJORAS MENORES DEL C
Comentarios : // permite introducir comentarios hasta el fin de línea
Nombres de datos enumerados : el nombre de una enumeración es un nombre de tipo, por lo tanto simplifica la escritura de un programa :
enum { a, b, c=0 }
enum { d, e, f=e+2 }
enum color { rojo, amarillo, verde=20, azul }
color col = rojo
color* cp = &col
if(*cp == azul) // …
Nombres de clases y estructuras : los nombres de clases y estructuras son nombres de tipos en C++. Las clases no existen en C. En C++ no se necesita colocar el calificador struct o class delante de un nombre de estructura o clase.
Declaraciones en bloques : C++ permite la introducción de declaraciones de variables dentro de bloques y antes de sentencias ejecutables. Esto permite declarar un identificador más cerca de su punto de aplicación.
for (int k=0; k<10; k++)
cout << “el valor de k es : ” << k << `\n' ;
Operador de alcance : el operador de alcance :: es un nuevo operador y permite resolver conflictos de nombre. Por ejemplo si se tiene una función local con una variable llamada vector y también se tiene una variable global llamada vector, el calificador ::vector permite acceder a los valores de la variable global. Lo contrario no se puede hacer.
Especificador const: sirve para : fijar el valor de una entidad dentro de su alcance, fijar el dato apuntado por una variable puntero, el valor de la dirección del puntero o ambos a la vez (puntero y valor).
Conversiones explícitas de tipo : se puede emplear un tipo predefinido o tipo definido por el programador como una función para convertir datos de un tipo a otro. Bajo ciertas circunstancias se puede emplear la conversión explícita de tipo como una alternativa a una conversión por asignación.
Sobrecarga de funciones : En C++ varias funciones pueden utilizar los mismos nombres de función, cada una de las funciones sobrecargadas puede distinguirse gracias al número y tipo de parámetros.
Valores por omisión de los parámetros de una función : Podemos asignar valores por omisión a los parámetros finales de las funciones de C++. De modo tal que se puede llamar a una función con menos parámetros de los definidos.
Funciones con número no especificado de parámetros : Empleando … se pueden definir funciones con un número no definido de parámetros. No se chequean los tipos de los parámetros utilizados para permitir flexibilidad en el uso.
Parámetros por referencia en una función : Utilizando el operador &, podemos declarar un parámetro de función formal como un parámetro por referencia :
void incremento (int& valor)
{
valor++;
}
int i;
incremento(i);
Cuando se llama a la función incremento se asigna a la dirección de valor la dirección de i. En incremento se incrementa el valor de i y se devuelve a la función que lo llamó. No es necesario pasar la dirección de i como ocurría en C, con lo que simplifica la codificación.
Operadores new y delete : Los operadores new y delete son introducidos en C++ para asignar y desasignar memoria dinámicamente.
MEJORAS IMPORTANTES RESPECTO a C
Estas mejoras están directamente relacionadas con la Programación Orientada a Objetos (POO)
Constructores de clases y encapsulamiento de datos : Una clase puede contener en su definición las declaraciones de datos, los valores iniciales y el conjunto de operaciones (métodos) para la abstracción de datos. Se crean objetos a partir de una clase dada. Entre objetos se envían mensajes. Cada objeto puede contener un conjunto público y privado de datos.
Estructura de clases : Una struct en C++ es un subconjunto de una definición de clase, con todos sus miembros públicos. La struct puede contener datos y funciones.
Constructores y destructores : se emplean para inicializar objetos de una clase determinada. Cuando se declara un objeto se activa el constructor. Los destructores desasignan la memoria del objeto involucrado. Esto se puede hacer explícitamente o automáticamente cuando se sale del ámbito de declaración del objeto.
Mensajes : En C++ los mensajes se envían con un mecanismo similar al llamado de una función. Por lo general se invoca a una función miembro del objeto especificado pasando los parámetros definidos para esa función.
mi_objeto.mi_metodo(5);
Sobrecarga de operadores : C++ podemos redefinir el conjunto de operadores suministrados por el compilador, de modo tal que puedan ser adaptados a los tipos definidos por el usuario. Esto permitiría definir operadores (+,*,/, etc.) para que operen con un tipo definido por el usuario del mismo modo que los tipos fundamentales.
Clases derivadas : una clase derivada es una subclase de una clase. Este es lo que permite implementar herencia entre los objetos. Un objeto puede heredar el todo o un parte de la superclase. Las subclases heredan la parte pública de la clase madre (superclase), pero no puede heredar la parte privada.
Polimorfismo : es la capacidad que tienen los objetos de responder de distinto modo a un mismo mensaje. En C++ la especificación de las clases derivadas, la sobrecarga de funciones y operadores permiten implementar esta importante característica de la POO.
Facilidades para las E/S : Las entradas y salidas pueden ser rápidamente definidas para ser adaptadas a los distintos objetos definidos por el usuario. cin, cout y cerr, forman una jerarquía de clases y objetos que pueden ser empleados fácilmente.
Elementos del Lenguaje
Un programa C++ está compuesto por una secuencia de componentes léxicos. Existen cinco componentes léxicos: identificadores, palabras claves, operadores, constantes y otro separadores.
Identificadores : en general los nombres que asigna un programador a los: objetos, funciones, enumerador, tipo, miembro de clase, patrón, un valor ó un rótulo y o subprogramas (funciones)
Palabras claves : palabras reservadas que el programador no puede utilizar de ninguna otra manera que no sea la asignada por el lenguaje.
asm continue float new signed try
auto default for operator sizeof typedef
break delete friend private static union
case do goto protected struct unsigned
catch double if public switch virtual
char else inline register template void
class enum int return this volatile
const extern long short throw while
Los identificadores con doble subrayado _ _ son empleados por algunos compiladores de C++ y bibliotecas estándar por lo que se recomienda no usarlos.
Operadores : tienen una función específica asignada por el programa y van acompañados de identificadores, literales, otros operadores, etc.. Los siguientes caracteres simples se emplean como operadores o signos de puntuación:
! % ^ & * ( ) - + = { }
| [ ] \ ; ` : “ < > ? ,
. /
también se emplea la siguiente combinación de caracteres como operadores:
-> ++ -- .* ->* << >> <= >= == != &&
|| *= /= %= += -= <<= >>= &= ^= |= ::
Literales : son los que por lo general se conocen como “constantes” y pueden ser : enteras, de caracteres, flotante (decimal ó real) cadena de caracteres.
numérica : 123456
de caracteres :
nueva línea: \n tabulador horizontal: \t tabulador vertical: \v
retroceso: \b retorno de carro: \r avance de página: \f
alerta: \a diagonal invertida: \\ interrogación: \?
apóstrofe: \' comillas: \”
flotantes : 0.1233, 1233.e-04
cadena de caracteres : encerrada entre comillas “abcde”
TIPOS FUNDAMENTALES DE DATOS
char
unsigned short
int
long
float
double
long double
OPERACIONES SOBRE TIPOS
sizeof : tamaño de
new : asignar memoria del tipo
delete : liberar memoria del tipo
int main()
{
int *p = new int;
cout << “tamaño de : ” << sizeof (p) << `\n' ;
delete(p);
}
En C++ el nombre de tipo se emplea para la conversión
explícita de un tipo a otro :
float f;
char *p;
// …
long k = long(p); // convertir p a un long
int l = int (f) ; // convertir f a un int
TIPOS DERIVADOS
Por medio de los operadores de declaración se pueden
derivar otros tipos :
* Puntero
& Referencia
[] Arreglo
() Función
además podemos definir estructuras (registros) por medio de la palabra struct
int *a ; // puntero a un entero
float v[10]; //arreglo 10 posiciones para nros. reales
char *p[20]; // arreglo de 20 punteros a caracteres
void f(int);
struct estr{ short longitud; char *p);
OPERADORES ARITMETICOS
+ sumar * multiplicar % residuo
- restar / dividir
En C++ en las operaciones de asignación o aritméticas las conversiones entre los tipos básicos se realizan automáticamente, esto implica que los tipos se pueden mezclar libremente
OPERADORES DE COMPARACION
= = igual que < menor que <= menor o igual que
!= distinto de > mayor que >= mayor o igual que
DECLARACIONES
Antes de que un nombre sea utilizado este debe haberse declarado y además debe haberse definido su tipo :
char car;
int cuenta = 1;
char *nombre=”Juan”;
struct complejo {float re, im};
complejo varcom;
extern complejo sqrt (complejo);
extern int numero_error;
const double pi=3.1415926535897932385;
enum perro{Bulldog, Terrier, Pekines};
struct usuario;
La mayor parte de estas declaraciones son también definiciones definen la entidad a la que se referirá el nombre
car, cuenta, nombre, varcom un lugar en la memoria con un valor asociado
struct usuario no son definiciones
extern complejo sqrt (complejo); son declaraciones
extern int numero_error; de nombres
las entidades a las que se refieren se definirán en otro lugar
ALCANCE
Una declaración tiene un alcance determinado un nombre se puede emplear en un determinado lugar del programa
locales : se emplean dentro de una función específica
globales : no pertenecen a una función, su alcance va
desde donde se declaró hasta el fin de archivo
La declaración de una variable local
OCULTA
a la de la variable global
int x; // x global
void f()
{
int x; // x local oculta a x global
x = 1; // asignación a x local
{
int x; // oculta a la primera x local
x = 2; // asignación a la segunda x local
}
x = 3; // asignación a la primera x local
}
int *p = &x; // toma la dirección de x global
En programas grandes la ocultación de nombres es inevitable
su uso debe evitarse por ser frecuente fuente de errores
Es posible emplear el nombre de una variable global
dentro del ámbito local ( operador alcance :: )
int x;
void f2()
{
int x = 2; // oculta a x global
::x = 3; // asigna el valor 3 a x global
}
No hay modo de utilizar un nombre local oculto
NOMBRES
-
Identificador : secuencia de letras y dígitos
-
1er. caracter debe ser una letra
-
subrayado bajo _ se considera una letra
-
C++ no impone límite a los nombres pero las implementaciones si lo hacen (Borland C++ los 32 primeros son significativos)
-
Se pueden admitir ASCII extendidos limitan la portabilidad
-
Mayúsculas y minúsculas son distintas
Identificadores válidos :
hola este_es_un_nombre_largo DEFINIDO
fo0 bAr var0 var10 CLASS _class
Identificadores no_válidos :
012 un gato $sist class 3 var
num-cta dir~emp .nombre if
TIEMPO DE VIDA
(ámbito o visibilidad)
Un objeto se crea cuando se llega a su definición
Se destruye cuando se sale de su alcance (ámbito)
Los objetos globales se crean e inicializan una sola vez, se destruyen cuando el programa termina
Cuatro posibilidades de visibilidad :
-
bloque
-
función
-
archivo (static)
-
programa (extern)
Objetos definidos con la palabra clave static se crean una sola vez y se destruyen al final del programa. Se inicializan la primera vez que el programa pasa por la declaración
# include <iostream.h>
int a
void f()
{
int b=1; // se inicializa cada vez que se llama a f
static int c=a; // inicializado una sola vez
cout << “ a = ” << a++
<< “ b = ” << b++
<< “ c = ” << c++ << `\n' ;
}
int main()
{
while (a<4) f();
}
Salida del programa :
a = 1 b= 1 c=1
a = 2 b= 1 c=2
a = 3 b= 1 c=3
El programador puede controlar el tiempo de vida de los objetos que crea con los operadores new y delete
PUNTEROS
T : Tipo fundamental de datos
T* : puntero a un objeto del tipo T
int *pi; // puntero a un entero
char **aac; // puntero a un puntero de tipo char
Para arreglos y funciones se tiene :
int (*vp)[10]; // puntero a un arreglo de 10 enteros
int (*fp)(char, char *); // puntero a una función que
// recibe argumentos tipo char y char* y devuelve int
Operación indirección : hace referencia al objeto que apunta el puntero
char c1 = `a';
char *p = &c1; // p tiene la dirección de c1
char c2 = *p; // a c2 se le asigna el valor `a'
Es posible realizar operaciones aritméticas con los punteros :
int strlen (char *p) // calcula la longitud en caracteres
{ // de una cadena que termina en `\0'
int i=0; // sin contar el cero final
while (*p++) i++;
return i;
}
ARREGLOS
tipo T[tam] : especifica el arreglo de nombre T de tipo tipo
de tamaño tam, indizado de 0 a tam-1
float v[3]; // arreglo de tres float, v[0], v[1] y v[2]
int a[2][5]; // matriz de enteros de 2 filas y 5 colum.
char* vpc[32];// arreglo punteros char 32 posiciones
Ejemplo :
#include <string.h>
char alfa[]=”abcdefghijklmnñopqrstuvwxyz”;
main()
{
int tam=strlen(alfa);
for (int i=0; i<tam; i++) {
char car = alfa[i];
cout << car <<” = “<< int(car) <<'\n”;
}
}
Salida :
a = 97
b = 98
c = 99
………………….
No hace falta especificar el tamaño del arreglo alfa. El compilador asigna como tamaño la cadena especificada. Es el único caso en que se puede emplear el operador de asignación para una cadena de caracteres.
char v[10];
v = “una cadena”; // error !!!!!!!!
strcpy(v, “una cadena”);
Para inicializar arreglos de otro tipo se necesita otra notación
int v1[] = {1, 2, 3, 4}
int v2[] = {`a', `b', `c', `d'}
char v3[]={1,2,3,4}
char v4[]={`a', `b', `c', `d'}
v3 y v4 son arreglos caracteres de 4 elementos que no tienen la marca de fin de cadena posibilidad de cometer errores
PUNTEROS Y ARREGLOS
El nombre de un arreglo puntero al primer elemento
#include <string.h>
char alfa[]=”abcdefghijklmnñopqrstuvwxyz”;
main()
{
char *p=alfa, car; // otra alternativa char *p=&alfa[0]
while(car = *p++)
cout<<car<<” = “<< int(car) <<'\n”;
}
Cuando un arreglo se pasa como argumento a una función siempre se pasa por referencia (puntero al primer elemento del arreglo)
#include <string.h>
main()
{
char v[]= “Alejandra”;
char *p= v;
strlen(p); // en ambas llamadas se pasa el mismo valor
strlen(v);// a strlen
}
Operaciones sobre punteros :
p apunta a un elemento del tipo T
p+1 apunta al siguiente elemento
p-1 apunta al elemento anterior
Resta de punteros permitida cuando se apunta a elementos del mismo arreglo = al nro. de elemento que existen entre los punteros. Se pueden sumar o restar valores enteros a un puntero si se sale de los límites resultado imprevisible
void f()
{
int v1[10];
int v2[10];
int i=&v1[5]-&v1[3]; // resultado =2
i=&v1[5]-&v2[3]; // resultado imprevisible
int *p= v2+2; // p=&v2[2]
p=v2-3; //*p no definido
}
La mayoría de los compiladores C++ no chequean los límites de los arreglos.
ESTRUCTURAS
Arreglo : agregado de elementos del mismo tipo
Estructura ≡ Registro : agregado de elementos de ≠ tipo
struct domicilio{
char *nombre;
char *calle;
long numero;
char* ciudad;
char estado[2];
int cod_post;
}; // este es uno de los pocos lugares donde el usuario
// además de la llave debe poner un punto y coma
Ahora se pueden declarar variables del tipo domicilio :
domicilio js; //constructor para estructuras
js.nombre= “Juan Samora”;
js.numero= 61;
También se puede declarar un arreglo de estructuras :
domicilio SantaFe[100];
SantaFe[1].nombre= “Alberto Sosa”;
Otro modo de inicializar una estructura es :
domicilio js = {
“Juan Samora”;
“Avenida de los Naranjos”, 61
“Buenos Aires”,{`N', `L'},1021
};
Las estructuras se pueden asignar, pasar como argumento de función y devolver como resultado de función :
domicilio actual;
domicilio fijar_actual(domicilio siguiente)
{
domicilio previo=actual;
actual = siguiente;
return previo;
}
Operaciones como igualdad o diferencia no están definidas, por lo tanto no se deben utilizar, si se puede definir una función que lo haga.
Tamaño de la estructura ≠ de la suma de los tipos individuales sizeof(domicilio)
El nombre de una estructura está disponible inmediatamente después de haberla declarado :
struct lista_doble{
int num;
lista_doble* siguiente;
lista_doble* previo;
};
No es posible declarar un objeto de una estructura que no se ha definido completamente todavía :
struct muestra{
muestra nueva; // error!! muestra no está
}; // totalmente definida todavía
Es posible reservar un nombre para que sea empleado más adelante :
struct lista;
struct nodo {
nodo* next;
nodo* previo;
lista* nuevo;
};
struct lista {
nodo *tope;
};
Ahorro de espacio
Existen dos modos de exprimir (en el sentido de tratar de aprovechar al máximo) espacio en la memoria disponible :
-
Campos : colocar un objeto pequeño en un byte
-
Uniones : utilizar el mismo espacio para contener objetos diferentes en momentos distintos
Estos recursos no son portables, por lo tanto, se debe pensar bien antes de usarlos.
Campos
Cuando se quiere emplear una variable binaria (0-1, verdadero-falso) se emplea generalmente un char que ocupa un byte, pero se pueden reunir una o más unidades pequeñas como campos de una struct. Un campo de esta struct se especifica por medio del nombre seguido de la cantidad de bits que ocupa.
struct regest {
unsigned habilitar :1;
unsigned pagina : 3;
unsigned : 1; // no se usa sirve para mejorar la
// disposición de bits
unsigned modo : 2;
unsigned : 4;
unsigned acceso : 1;
unsigned longitud : 1;
};
Los campos se emplean como cualquier otra variable entera, pero no es posible obtener su dirección. Emplear campos no siempre ahorra espacio, porque por lo general se incrementa el tamaño del código requerido para manejar variables. Se hace referencia a un campo de la siguiente forma:
struct regest reg1;
reg1.acceso = 0;
if (reg1.longitud == 0)....
Uniones
Supongamos una tabla de entrada que contiene nombre y valor, y el valor es una cadena de caracteres o un entero :
struct entrada {
char* nombre;
char tipo;
char* valor_cadena;
int valor_entero;
}
void imprimir_entrada(entrada *p)
{
switch(p->tipo) {
case `c':
printf(“%s”, p->valor_cadena) ;
break;
case `e':
printf(“%d”, p->valor_entero) ;
break;
default :
printf(“tipo corrompido\n”);
break;
}
}
Como no se puede emplear valor_cadena y valor_entero
al mismo tiempo, se desperdiciará espacio, especificando que ambos son miembros de una unión se ahorra espacio :
struct entrada {
char* nombre;
char tipo;
union {
char* valor_cadena; // utilizado si tipo ='c'
int valor_entero; // utilizado si tipo = `e'
};
};
El código que se escribió anteriormente sigue inalterado, al asignar un valor a una entrada, valor_entero y valor_cadena tienen la misma dirección los miembros de una unión ocupan el espacio requerido por el miembro más grande.FUNCIONES Y ARCHIVOS
Por lo general, un programa está compuesto por varias unidades compiladas en forma independiente y que se encuentran en diferentes archivos. Esto facilita la legibilidad, modificación y/ o corrección del código.
Calificadores extern y static
A no ser que se especifique lo contrario un nombre que no es local respecto de una función o clase debe referirse al mismo tipo, valor, función u objeto en todas las partes de un programa compiladas individualmente un programa sólo puede existir un tipo, valor, función u objeto no local con ese nombre
// arch1.c
int a=1;
int f() {/* hacer algo */}
// arch2.c
extern int a;
int f();
void g() { a = f(); }
-
La variable a y la función f() empleadas por g() en arch2.c son las que se definieron en arch1.c.
-
La palabra clave extern indica que la declaración de a en arch2.c es solo eso una declaración y no una definición.
-
Si se hubiera inicializado a se habría ignorado la palabra extern porque una declaración con una inicialización es una definición.
Un objeto se debe definir una y solo una vez en un programa, se puede declarar muchas veces pero los tipos deben concordar con exactitud
// arch1.c
int a=1;
int b=1;
extern int c;
// arch2.c
int a; // error !! a se define dos veces
extern double b; // error!! b se declara dos veces con tipos
// diferentes
extern int c; // error!! c se declara dos veces pero no se
// define
Estos errores no los detecta el compilador (mira un archivo por vez), el ensamblador es el que lo hace
Con la declaración static es posible hacer que un
nombre sea local a un archivo
// arch1.c
static int a=6;
static int f() {/* …*/};
// arch2.c
static int a=7 ;
static int f() { /* …*}
Cada archivo tiene su variable a y su función f()
ARCHIVOS DE ENCABEZADO
Los tipos de todas las declaraciones del mismo objeto o función deben ser consistentes. Un método para facilitar esto es la inclusión de archivos de encabezado que contienen código fuente y/o definiciones de datos.
Directiva include sirve para poner fragmentos de un programa en un solo archivo.
#include “archivo.cpp” se reemplaza esta línea por el
contenido de archivo.cpp
(archivo fuente con código C++)
#include <iostream.h> // búsqueda en el directorio estándar
#include “iostream.h” // búsqueda en el directorio actual
CÓMO ORGANIZAR UN ARCHIVO DE ENCABEZADO
Definiciones de tipos struct punto{int c,y;};
Patrones template<class T> class V{…}
Declaraciones de funciones extern int strlen (const char*);
Definiciones de funciones
en línea inline char obt(){return *p++;}
Declaraciones de datos extern int a;
Definiciones de constantes const float pi=3.141593;
Enumeraciones enum bool {falso, verdadero};
Declaraciones de nombres class Matriz;
Directivas de inclusión #include <signal.h>
Comentarios //comprobar si abrió el archivo
Un archivo de encabezado no debería incluir :
Definición de funciones ordinarias char obt(){return *p++;}
Definición de datos int a;
Por convención los archivos de encabezado llevan la extensión .h y los que tienen definiciones de funciones o datos llevan la extensión .c, .cpp, .cc, .cxx
FUNCIONES
Declaración - nombre de la función
- valor devuelto (si lo hay)
-
tipo del/los argumento/s de llamada
extern double sqrt(double);
extern char* strcpy(char* a, const char* de);
extern void exit(int);
El compilador ignora los nombres de los argumentos que se ponen en la declaración
Paso de argumentos :
por valor : cuando se pasa el argumento se realiza
una copia del mismo
por referencia : la función emplea el argumento que
se está pasando
void f(int val, int &ref)
{ val++;
ref++;
}
void g()
{ int i;
int j;
f(i,j); }
Llamadas por referencia :
-
pueden dificultar la lectura del programa
-
son útiles cuando se quieren pasar argumentos largos
-
si se quiere evitar que la función modifique el valor se los puede pasar como argumento const
void f(const grande &arg)
{
// no se puede alterar el valor de arg
// sin emplear una conversión explícita de tipo
}
Arreglos como argumentos
Siempre se pasan por referencia no por valor.
El tamaño no está disponible para la función de llamada. Si en la función de llamada necesitamos trabajar con las dimensiones del arreglo, se debe conocer la dimensión del mismo.
void imprimir_matriz34(int m[3][4])
{
for (int i=0; i<3; i++)
for (int j=0; j<4; j++)
cout << ` ` << m[i] [j] << `\n';
} // no hay problemas porque las dimensiones se conocen en
// tiempo de compilación
void imprimir_matriz34(int m[][4], int dim1)
{
for (int i=0; i<dim1; i++)
for (int j=0; j<4; j++)
cout << ` ` << m[i] [j] << `\n';
} // no hay problemas porque la 2da. dimensión se conoce en
// tiempo de compilación se puede calcular la ubicación
// de un elemento
void imprimir_matriz34(int m[][], int dim1, int dim2) //error
{
for (int i=0; i<dim1; i++)
for (int j=0; j<dim2; j++)
cout << ` ` << m[i] [j] << `\n';
} //PROBLEMAS porque las dimensiones NO SE CONOCEN
// en tiempo de compilación
Una posible solución para esto es :
void imprimir_matriz34(int** m, int dim1, int dim2)
{
for (int i=0; i<dim1; i++)
for (int j=0; j<dim2; j++)
cout << ` ` << ((int*)m)[i*dim2+j] << `\n';
}
Nombres de función sobrecargados
Sobrecarga : mismo nombre de función para realizar tareas diferentes, generalmente manipulan objetos de distinto tipo
Ejemplo : solo hay un nombre para la suma, +, pero puede manipular objetos del tipo entero, punto flotante y punteros
void imprimir(int); // para imprimir un entero
void imprimir(const char*) // para imprimir una cadena de // caracteres
void imprimir(double);
void imprimir(long);
void f()
{
imprimir(1L); //imprimir(long)
imprimir(1.0); //imprimir(double)
imprimir(1); //imprimir(int)
}
El compilador determina la función que debe llamar de
acuerdo con los argumentos de llamada a la misma. Para ello
determina las siguientes reglas de concordancia :
REGLAS DE CONCORDANCIA DE ARGUMENTOS
1) Concordancia exacta : verifica que los argumentos concuerden exactamente sin emplear conversiones o haciéndolo con sólo las inevitables (nombre de arreglo a puntero, nombre de funciónpuntero a función, T a const T)
2) Concordancia empleando promociones integrales : char a int, short a int y sus contrapartes unsigned, float a double.
3) Concordancia empleando conversiones estándar : int a double, derivado* a base*, unsigned int a int.
4) Concordancia empleando conversiones definidas por el usuario
5) Concordancia empleando … en declaración de la función
void imprimir(int);
void imprimir(const char*)
void imprimir(double);
void imprimir(long);
void imprimir(char);
void h(char c, int i, short s, float f)
{
imprimir(c); // concordancia exacta
imprimir(i); // concordancia exacta
imprimir(s); // promoción integral imprimir(int)
imprimir(f); // promoción integral imprimir(double)
imprimir(`a'); // concordancia exacta
imprimir(49); // concordancia exacta
imprimir(“a”); // concordancia exacta imprimir(const
// char*) }
Argumentos por omisión
Se emplean cuando se necesitan más argumentos en el caso general que en el caso más simple que es el más frecuente
-
Solo es posible incluir argumentos al final de la lista de argumentos
-
Los argumentos que pueden omitirse deben tener su valor inicializado
-
El argumento se fija en la llamada a la función
void imprimir(int valor, int base=10)
void f()
{
imprimir(31);
imprimir(31,10);
imprimir(31,16);
imprimir(31,2);
}
int f(int, int=0, char* =0);
int g(int = 0; int = 0; char*) // error!! se debe fijar un valor
// por omisión
Número no-especificado de argumentos
Se emplea cuando no es posible especificar el número y tipo de todos los argumentos de una llamada, se termina la declaración de una función de este tipo con …
int printf(const char* …) // printf debe tener al menos un
//argumento que es una cadena de caracteres
printf(“Hola todo el mndo \n”);
printf(“Mi nombre es %s %s \n”, nombre, apellido);
printf(“%d + %d = %d \n”,2,3,5);
La sentencia :
printf (“Mi nombre es %s %s \n”, 2);
se compilará bien, pero tendrá una salida rara, en tiempo de compilación no es posible verificar los argumentos que tendrá.
Un programa bien diseñado no debería necesitar funciones de este tipo. Las funciones sobrecargadas y los argumentos por omisión hace que se tengan alternativas que haga que no sea necesario emplear este recurso
Punteros a funciones
Dos cosas se pueden hacer con una función :
-
llamarla
-
obtener su dirección
El puntero de una función puede servir para llamarla, pero se debe poner el operador indirección * encerrado entre paréntesis porque el operador llamada a función () tiene mayor precedencia que el indirección
void error (char*p) {…}
void (*pointf) (char); // puntero a función
void f()
{
pointf = &error; // pointf apunta error
(*pointf) (“error”) // llama a la función error
}
si escribiéramos *pointf(“error”) *(pointf(“error”))
nos daría un error de tipo
Cuando se declara un puntero a una función se debe tener en cuenta los tipos de los argumentos. Debe haber una concordancia exacta.
void (*pf) (char*);
void f1(char*);
int f2(char*);
int f3(char*);
void f()
{
pf = &f1; // correcto
pf = &f2; // error!! tipo devuelto incorrecto
pf = &f3 // error de tipo de argumento
(*pf) (“asfd”); // correcto
(*pf) (1); // error de tipo de argumento
int i=(*pf) (“qwer”); // error!! void asignado a int
}
SOBRECARGA DE OPERADORES
Tipos básicos definidas operaciones manejo fácil,
cómodo, breve, convencional
Las clases nos permiten especificar objetos no primitivos además de un conjunto de operaciones que se pueden llevar a cabo con esos objetos
class complejo {
double re, im;
public:
complejo(double r, double i) { re=r; im=i; }
friend complejo operator+(complejo, complejo);
friend complejo operator*(complejo, complejo);
};
La definición de operator+ y operator* da al + y * un significado especial. Dados dos complejos a y b, a+b significa por definición operator+(a,b)
void f()
{
complejo a = complejo(1,3.1)
complejo b = complejo(1.2, 2)
complejo c = a;
a=b+c;
b = b+c*a; c = a*b + complejo(1,2);
}
Valen las mismas reglas de precedencia que con cualquier otro operador.
Se pueden definir funciones para los siguientes operadores :
+ - * % ^ & | ~ !
= < > += -= *= /= %= ^=
/= << >> >>= <<= == != <= >= &&
|| ++ -- ->* , -> [] () new delete
No es posible :
-
alterar el orden de precedencia
-
no se puede modificar la sintaxis (ej.: no se puede utilizar un operador unario como binario o viceversa)
-
no es posible definir nuevos operadores solo se pueden redefinir los que están
Una función operador es la que se define utilizando la palabra clave operator seguida del operador (ej.: operator<<). Se pueden invocar como cualquier otra función. Cuando se emplea el operador únicamente estamos empleando una abreviatura del operador.
void f(complejo a, complejo b)
{
complejo c = a + b; // abreviado
complejo d = operator+ (a,b) // llamada explícita
}
Operadores Binarios y Unarios
Para cualquier operador genérico @ tenemos :
si es binario puede ser implementado como una función miembro que recibe un argumento ó una función global que recibe dos argumentos
aa@bb aa.operator@(bb)
operator@(aa,bb)
si se definen ambas, la concordancia de argumentos le determinará que función le corresponde
si el operador es unario y prefijo @aa aa.operator@()
operator@(aa)
si es posfijo : aa@ aa.operator@()
operator@(aa)
class X{
// miembros con apuntador this implícito
X* operator&(); //&(dirección de) unario prefijo
X operator&(X); //&(and) binario
X operator++(int); // incremento posfijo
X operator&(X,X); //error!! ternario
X operator/(); // error!! unario
};
// funciones globales (con frecuencia amigas);
X* operator-(X); //- prefijo unario
X operator-(X,X); //- binario
X operator --(X&,int); // decremento posfijo
X operator-(); //error!! falta operando
X operator-(X,X,X); // error!! ternario
Asignación e inicialización
struct cadena{
char* p;
int tamaño; // del vector que apunta p
cadena(int tam) {p=new char[tamaño=tam];}
~cadena(){delete p;}
};
Cadena puntero a vector de caracteres y tamaño vector
void f()
{
cadena c1(10);
cadena c2(20);
c1=c2; // problemas porque al salir de f se llama al
//destructor de c1 y de c2, además
} // c1 tiene ≠ tamaño que c2
Redefiniendo el operador =
struct cadena {
char* p;
int tamaño;
cadena(int tam) {p = new char[tamaño=tam];}
~cadena() {delete p;}
cadena & operator = (const cadena &);
};
cadena& cadena::operator=(const cadena& a)
{
if (this != &a) { // tener en cuenta a=a
delete p;
p = new char[tamaño = a.tamaño];
strcpy(p,a.p);
}
return *this;
}
Con esto se mejora la situación anterior pero no se evita lo siguiente :
void f()
{
cadena c1(10);
cadena c2=c1; // inicialización no es asignación
}
El constructor construye una cadena pero destruye dos. El operador asignación no se aplica a un objeto no inicializado.
struct cadena {
char* p;
int tamaño;
cadena(int tam) {p = new char[tamaño=tam];}
~cadena() {delete p;}
cadena & operator = (const cadena &);
cadena(const cadena&); //constructor de copia
};
cadena::cadena(const cadena& a)
{
p = new char[tamaño = a.tamaño];
strcpy(p,a.p);
}
Para un tipo X, el constructor de copia X(const X&) se ocupa de la inicialización con un objeto del mismo tipo X.
Un constructor debe poseer la máxima cantidad de funciones posibles :
class X{
//…
X(algo); // constructor de nuevos objetos
X(const X&); // constructor de copia
operator=(const X&) //asignación : limpieza y copia
~X(); //destructor : limpieza
};
Subíndices
Una función operator[] sirve para asignar subíndices a objetos de clase, el segundo argumento (el subíndice de una función operator[], puede ser de cualquier tipo.
class asoc{
struct pareja{
char *nombre
int val;
};
pareja* vec;
int max;
int libre;
asoc(const asoc&); // evitar la copia
asoc& operator=(const asoc&); // evitar la copia
public:
asoc(int);
int &operator[](const char*);
void imprimir_todo();
};
asoc es un vector de objetos pareja de tamaño max. El constructor de copia y el operador de asignación se mantienen privados para evitar la copia de arreglos asoc.
asoc::asoc(int s)
{
max = (s<16)?s:16;
libre=0;
vec=new pareja[max];
}
#include <string.h>
int& asoc::operator[](const char*p)
/* administrador de objetos del tipo “pareja” :
-
buscar p
-
devolver una referencia a la parte entera de pareja
-
crear una nueva “pareja” si no se encuentra p
*/
{
register pareja* pp;
for (pp=&vec[libre-1]; vec<=pp; pp--)
if strcmp(p,pp->nombre==0) return pp->val;
if(libre==max) { // desborde amplair arreglo
pareja* nvec= new pareja[max*2]
for (int i=0; i<max; i++) nvec[i] = vec [i];
delete vec;
vec=nvec;
max=2*max;
}
pp=&vec[libre++];
pp->nombre=new char[strlen(p)+1]
strcpy(pp->nombre,p);
pp->val=0; //valor inicial=0
return pp->val;
}
Conversiones de tipo
Ejemplo de números complejos :
class complejo {
double re,im
public:
complejo(double r, double i) {re=r; im=i;}
friend complejo operator+(complejo, complejo);
friend complejo operator+(complejo, double);
friend complejo operator+(double, complejo);
friend complejo operator-(complejo, complejo);
friend complejo operator-(complejo, double);
friend complejo operator-(double, complejo);
complejo operator-(); //unario
friend complejo operator*(complejo, complejo);
friend complejo operator*(complejo, double);
friend complejo operator*(double, complejo);
}
void f()
{
complejo a(1,1), b(2,2), c(3,3), d(4,4),e(5,5)
a= -b-c;
b=c*2.0*c;
c=d+e*a;
}
Problema : tedioso escribir una función para combinación complejo - double
Solución : constructor que dado un double cree un complejo
class complejo {
//…
complejo(double r) {re r, im=0)
};
esto especifica como crear un complejo a partir de un double
Operadores incremento y decremento
Son los únicos en C++ que se pueden especificar tanto como operadores prefijos como postfijos
class VerifyPointer{
T* p;
T* arreglo;
int tamaño;
public:
// encadenar con arreglo `a' de tamaño `t' valor inicial `p'
VerifyPointer(T* p, T* a, int T);
// encadenar con un solo objeto valor inicial `p'
VerifyPointer(T* p);
T* operator++(); // prefijo
T* operator++(int); //posfijo
T* operator--(); // prefijo
T* operator--(int); //posfijo
T& operator*(); //prefijo
}
El argumento int sirve para indicar que la función debe llamar a la función posfija de ++. El argumento no se usa solo sirve para distinguir la implementación prefija de la posfija.
void f3(T a)
{
T v[200];
VerifyPointer p(&v[0],v,200)
p.operator-(1);
p.operator*()=a; //error `p' fuera del intervalo
p.operator++();
p.operator*()=a; // correcto
}
Un excesivo empleo de la sobrecarga de operadores puede dar como resultado programas incomprensibles
Su uso debería limitarse para imitar el empleo convencional de los operadores, cuando esto no es posible, el empleo de llamadas a función es el mecanismo más adecuado
Universidad Tecnológica Nacional - Santa Fe - Departamento Sistemas -
Curso : Desarrollos de Programación en C++
para representar valores enteros
para representar valores reales
Descargar
Enviado por: | María Gabriela Rodríguez |
Idioma: | castellano |
País: | España |