Desarrollo de programación C++

Introducción. Mejoras menores del C. Tipos fundamentales de datos. Operadores. Estructuras

  • Enviado por: María Gabriela Rodríguez
  • Idioma: castellano
  • País: España España
  • 52 páginas
publicidad

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