Desarrollo de programación C++

Anuncio
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' ;
1
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
2
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
3
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
4
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 :
5
* 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);
6
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
7
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
8
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 :
9
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
10
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
11
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 :
12
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
13
// 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 :
14
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
15
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)
{
16
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.
17
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
18
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};
19
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;
20
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
21
// 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()
{
22
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)
23
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”);
24
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*);
25
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)
26
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)
27
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;}
28
};
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;
}
29
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
30
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;
31
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;
}
32
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
33
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];
34
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
35
Descargar