martes, 22 de mayo de 2012

Implementación de polimorfismo en funciones y funciones virtuales de objetos


POLIMORFISMO



En programación orientada a objetos (POO), una función virtual o método virtual es una función cuyo comportamiento, al ser declarado "virtual", es determinado por la definición de una función con la misma cabecera en alguna de sus subclases. Este concepto es una parte muy importante del polimorfismo en la POO.

El concepto de función virtual soluciona los siguientes problemas:


En POO, cuando una clase derivada hereda de una clase base, un objeto de la clase derivada puede ser referido (o coercionado) tanto como del tipo de la clase base como del tipo de la clase derivada. Si hay funciones de la clase base redefinidas por la clase derivada, aparece un problema cuando un objeto derivado ha sido cohercionado como del tipo de la clase base. Cuando un objeto derivado es referido como del tipo de la base, el comportamiento de la llamada a la función deseado es ambiguo.
Distinguir entre virtual y no virtual sirve para resolver este problema. Si la función en cuestión es designada "virtual", se llamará a la función de la clase derivada (si existe). Si no es virtual, se llamará a la función de la clase base.


Ejemplo

Por ejemplo, una clase base Animal podría tener una función virtual come. La subclase Pez implementaría come() de forma diferente que la subclase Lobo, pero se podría invocar a come() en cualquier instancia de una clase referida como Animal, y obtener el comportamiento de come() de la subclase específica.

Esto permitiría a un programador procesar una lista de objetos de la clase Animal, diciendo a cada uno que coma (llamando a come()), sin saber qué tipo de animales hay en la lista. Tampoco tendría que saber cómo come cada animal, o cuántos tipos de animales puede llegar a existir.

El siguiente, es un ejemplo en C++:

# include <iostream>
using namespace std;
class Animal
{
public:
virtual void come() { cout << "Yo como como un animal genérico.\n"; }
};

class Lobo : public Animal
{
public:
void come() { cout << "¡Yo como como un lobo!\n"; }
};

class Pez : public Animal
{
public:
void come() { cout << "¡Yo como como un pez!\n"; }
};

class OtroAnimal : public Animal
{
};

int main()
{
Animal *unAnimal[4];
unAnimal[0] = new Animal();
unAnimal[1] = new Lobo();
unAnimal[2] = new Pez();
unAnimal[3] = new OtroAnimal();

for(int i = 0; i < 4; i++) {
unAnimal[i]->come();
}

for (int i = 0; i < 4; i++) {
delete unAnimal[i];
}
return 0;
}

Salida con el método virtual come:
Yo como como un animal genérico.
¡Yo como como un lobo!
¡Yo como como un pez!
Yo como como un animal genérico.
Salida sin el método virtual come:
Yo como como un animal genérico.
Yo como como un animal genérico.
Yo como como un animal genérico.
Yo como como un animal genérico.


Las Clases abstractas y funciones virtuales puras


Una función virtual pura o método virtual puro es una función virtual que necesita ser implementada por una clase derivada que no sea abstracta. Las clases que contienen métodos virtuales puros son denominadas "abstractas". Éstas no pueden ser instanciadas directamente, y una subclase de una clase abstracta sólo puede ser instanciada directamente si todos los métodos virtuales puros han sido implementados por esa clase o una clase padre.

Los métodos virtuales puros normalmente tienen una declaración (cabecera) pero no tienen definición (implementación). Como ejemplo, una clase base abstracta como "SimboloMatematico" puede ofrecer una función virtual pura como hazOperacion, y las clases derivadas "Suma" y "Resta" pueden implementar hazOperacion para ofercer implementaciones concretas. La implementación de hazOperacion no tendría sentido en la clase "SimboloMatematico" porque "SimboloMatematico" es un concepto abstracto cuyo comportamiento es definido solamente por cada tipo (subclase) de "SimboloMatematico" dado.
De forma similar, una subclase dada de "SimboloMatematico" no sería completa sin una implementación de hazOperacion. Aunque los métodos virtuales puros normalmente no tienen implementación en la clase que los declara, en C++ permite hacer esto, ofreciendo un comportamiento por omisión en el que la clase derivada puede delegar si es apropiado.

Las funciones virtuales puras también son utilizadas donde las declaraciones de métodos se utilizan para definir una interfaz para la que las clases derivadas proveerán todas las implementaciones. Una clase abstracta sirviendo como interfaz contiene sólo funciones virtuales puras, y ningún miembro de datos (variables, constantes, etc.) ni métodos ordinarios. El uso de clases puramente abstractas como interfaces funciona en C++ ya que éste soporta herencia múltiple. Debido a que muchos lenguajes orientados a objetos no soportan herencia múltiple, normalmente ofrecen un mecanismo por separado para hacer interfaces. Esto es así por ejemplo en Java.

C++

En C++, las funciones virtuales puras son declaradas utilizando una sintaxis especial = 0 como se muestra a continuación:
class B {
virtual void una_funcion_virtual_pura() = 0;
};
La declaración de la función virtual pura ofrece sólo la cabecera del método. Normalmente no se ofrece una implementación de la función virtual pura en una clase abstracta, pero puede ofrecerse. Toda clase hija no-abstracta continúa necesitando redefinir el método, pero la implementación ofrecida por la clase abstracta puede ser llamada de esta forma:
void Abstracta::virtual_pura() {
// haz algo
}

class Hija : Abstracta {
virtual void virtual_pura(); // ya no es abstracta; puede ser instanciada
};

void Hija::virtual_pura() {
Abstracta::virtual_pura(); // se ejecuta la implementación de la clase abstracta
}
El compilador sabe a qué implementación del método llamar en tiempo de ejecución creando una tabla de punteros a todas las funciones virtuales de una clase, llamada vtable o tabla virtual.


Destructores virtuales

Los lenguajes orientados a objetos normalmente gestionan la reserva y la liberación de memoria automáticamente cuando los objetos son creados y destruidos. Sin embargo, algunos lenguajes orientados a objetos permiten implementar un método destructor personalizado si se desea. Uno de estos lenguajes es C++, y como se ilustra en el siguiente ejemplo, es importante para una clase base de C++ el tener un destructor virtual para asegurar que se llamará siempre al destructor de la mayoría de clases derivadas. En el ejemplo siguiente, sin destructor virtual, mientras que borrar una instancia de la clase B llamará correctamente a los destructores para la clase B y para la clase A si se borra como instancia de B, una instancia de B borrada mediante un puntero a su clase base A fallará al no llamar al destructor para B.

# include <iostream>

class A
{
public:

A() { }
~A() { std::cout << "Destruye A" << std::endl; }
};

class B : public A
{
public:

B() { }
~B() { std::cout << "Destruye B" << std::endl; }
};

int main()
{
A* b1 = new B;
B* b2 = new B;

delete b1; // Sólo se llama a ~A() aunque b1 sea una instancia de la clase B
// porque ~A() no se ha declarado como virtual
delete b2; // Llama a los destructores ~B() y ~A()

return 0;
}
Salida:
Destruye A
Destruye B
Destruye A

La declaración correcta del destructor para la clase A como virtual ~A() asegurará que el destructor para la clase B es llamado en ambos casos del ejemplo anterior.


Polimorfismo y Funciones virtuales

El Polimorfismo (implementado en C++ con funciones virtuales) es la tercera característica esencial de un lenguaje orientado a objetos, después de la abstracción de datos y la herencia.
De hecho, nos provee de otra dimensión para la separación entre interfaz y la implementación, desacoplando el qué del cómo. El Polimorfismo permite mejorar la organización del código y su legibilidad así como la creación de programas extensibles que pueden "crecer" no sólo durante el desarrollo del proyecto, si no también cuando se deseen nuevas características.
La encapsulación crea nuevos tipos de datos combinando características y comportamientos. El control de acceso separa la interfaz de la implementación haciendo privados (private) los detalles. Estos tipos de organización son fácilmente entendibles por cualquiera que venga de la programación procedimental. Pero las funciones virtuales tratan de desunir en términos de tipos. En el Capítulo 14, usted vió como la herencia permitía tratar a un objeto como su propio tipo o como a su tipo base. Esta habilidad es básica debido a que permite a diferentes tipos (derivados del mismo tipo base) ser tratados como si fueran un único tipo, y un único trozo de código es capaz de trabajar indistintamente con todos. Las funciones virtuales permiten a un tipo expresar sus diferencias con respecto a otro similar si ambos han sido derivados del mismo tipo base. Esta distinción se consigue modificando las conductas de las funciones a las que se puede llamar a través de la clase base.

Evolución de los programadores de C++

Los programadores de C parecen conseguir pasarse a C++ en tres pasos. Al principio, como un "C mejorado", debido a que C++ le fuerza a declarar todas las funciones antes de usarlas y a que es mucho más sensible a la forma de usar las variables. A menudo se pueden encontrar errores en un programa C simplemente recompilándolo con un compilador de C++.

El segundo paso es la "programación basada en objetos", que significa que se pueden ver fácilmente los beneficios de la organización del código al agrupar estructuras de datos junto con las funciones que las manejan, la potencia de los constructores y los destructores, y quizás algo de herencia simple. La mayoría de los programadores que han trabajado durante un tiempo con C ven la utilidad de esto porque es lo que intentan hacer cuando crean una librería. Con C++ usted recibe la ayuda del compilador.
Usted se puede encontrar atascado en el nivel de "programación basada en objetos" debido a que es de fácil acceso y no requiere mucho esfuerzo mental. Es también sencillo sentir cómo está creando tipos de datos - usted hace clases y objetos, envía mensajes a esos objetos, y todo es bonito y pulcro.

Pero no sea tonto. Si se para aquí, se está perdiendo una de las más importantes partes del lenguaje, que significa el salto a la verdadera programación orientada a objetos. Y esto se consigue únicamente con las funciones virtuales.

Las funciones virtuales realzan el concepto de tipo en lugar de simplemente encapsular código dentro de estructuras y dejarlo detrás de un muro, por lo que son, sin lugar a dudas, el concepto más difícil a desentrañar por los nuevos programadores en C++. Sin embargo, son también el punto decisivo para comprender la programación orientada a objetos. Si no usa funciones virtuales, todavía no entiende la POO.

Debido a que las funciones virtuales están intimamente unidas al concepto de tipo, y los tipos son el núcleo de la programación orientada a objetos, no existe analogía a las funciones virtuales dentro de los lenguajes procedurales. Como programador procedural, usted no tiene referente con el que comparar las funciones virtuales, al contrario de las otras características del lenguaje. Las características de un lenguaje procedural pueden ser entendidas en un nivel algorítmico, pero las funciones virtuales deben ser entendidas desde el punto de vista del diseño.

Uso de sobrecarga de operadores.


OPERADORES UNARIOS

El not no es el único operador de bits que toma sólo un argumento. Su compañero, el not lógico (!), toma un valor true y produce un valor false. El menos unario (-) y el más unario (+) son los mismos operadores que los binarios menos y más; el compilador deduce que uso se le pretende dar por el modo en el que se escribe la expresión.


De hecho, la sentencia:

x = -a;

tiene un significado obvio.
El compilador puede deducir:

x = a * -b;

pero el lector se puede confundir, de modo que es más seguro escribir
:
x = a * (-b);

El menos unario produce el valor negativo.
El más unario ofrece simetría con el menos unario, aunque en realidad no hace nada.

Los operadores de incremento y decremento (++ y --) se comentaron ya en este capítulo. Son los únicos operadores, además de los que involucran asignación, que tienen efectos colaterales. Estos operadores incrementan o decrementan la variable en una unidad, aunque «unidad» puede tener diferentes significados dependiendo del tipo de dato - esto es especialmente importante en el caso de los punteros.
Los últimos operadores unarios son dirección-de (&), indirección (* y ->), los operadores de moldeado en C y C++, y new y delete en C++.


OPERADORES BINARIOS


Operador a nivel de bits

Se define como operación binaria un procedimiento entre dos o más variables en base 2 (o también llamado en módulo 2). Desde el punto de vista de la informática, estas operaciones, aunque son puramente matemáticas, ocupan un gran rol en el funcionamiento de la computadora. Esta es la razón por la que se encuentran muchas veces en los microprocesadores y más específicamente en las ALU (Unidades Aritmético Lógicas).

Tipos de operaciones

1. Operaciones Lógicas: Evalúan el resultado de una expresión, dando verdadero o falso.
2. Operaciones Aritméticas: Realizan alguna de las siguientes operaciones: suma, resta, multiplicación y división
3. Operaciones Comparativas: Evalúan una doble operación lógica sobre una variable o expresión para determinar si es mayor, menor ó igual a dicho operando
4. Operaciones de Desplazamiento: Conocidos como «shifts» consisten en alterar el orden de los bits de un registro.
5. Operaciones de Rotación: similares a los shifts pero aprovechando características del procesador (para rotar los bits en un registro).

  • Operación Lógica


Una operación lógica evalúa 2 o más operandos mediante uno de los operadores lógicos (OR, AND, XOR,NAND ó NOR). El operador NOT sólo se aplica a un operando o expresión. Las operaciones lógicas sirven para tomar decisiones y en su forma más compleja realizan comparaciones. También son llamadas Operaciones Booleanas en mención al Álgebra de Boole, estas son usualmente: En los lenguajes de programación algunas operaciones no están implementadas toda vez que se consigue al aplicar 2 de ellas. Estas operaciones son típicamente NAND' y NOR que se consiguen mediante AND y OR y después se les aplica una operación NOT.

A nivel de hardware, por cuestiones de rendimiento y espacio pueden o no implementarse cada una de ellas ó sólo AND, OR y NOT, lo cual depende del diseño.
A nivel de hardware existen circuitos lógicos que representan cada una de las operaciones como puertas lógicas. E igualmente existen bloques compuestos únicamente de un tipo determinado de ellos. Al hardware diseñado con estos bloques se les denomina circuitos combinacionales. Los primeros ordenadores se construyeron de este modo.

  • Operación Aritmética


Las operaciones aritméticas permiten manejar los datos para producir resultados de expresiones. Varios circuitos se fabrican y diseñan para tales propósitos, los más típicos son los de: suma, resta, multiplicación y división otros menos conocidos pero menos frecuentes son el resultado de uno o más de ellos, por ejemplo los contadores, el circuito contador es básicamente un circuito cuya operación principal es la suma, con la particularidad de que al llegar al tope deseado, retorna al valor inicial. Por ejemplo un contador sexagesimal debe volver a valer 00 después de alcanzar el valor 59 (una aplicación típica son los relojes, en las funciones de minutos y horas).

Todo programa necesita hacer infinidad de operaciones de cálculo, que se efectúan con tales operaciones. A nivel de hardware, las operaciones de suma se llevan a cabo con el mismo circuito de suma ligeramente modificado. Las operaciones aritméticas a su vez están compuestas de operaciones lógicas.

  • Operación Comparativa


Las operaciones de comparación pueden entenderse como operaciones lógicas de mayor complejidad, y normalmente diseñadas y establecidas para la interacción humana. Dadas dos variables me permite conocer si una de ellas es mayor, menor o igual a la otra. El mecanismo utilizado internamente es la resta de las dos variables. A partir de esta operación y, basándose en el estado final de los flags, se determina cómo es la relación entre las dos variables. Hay otras comparaciones posibles, como saber si un valor es par o impar.
Las operaciones de comparación son una forma más elevada de tomar decisiones ya que puede aglutinar complejas expresiones que las utilizadas por simples operaciones lógicas.

En los lenguajes de alto nivel, las comparaciones suelen usarse en los mecanismos de decisión, como son la bifurcación y la ejecución de bucles.
Operación de Desplazamiento


Suma binaria

La suma binaria se puede realizar cómodamente siguiendo las tres reglas descritas:
1. Si el número de unos (en sentido vertical) es par el resultado es 0.
2. Si el número de unos (en sentido vertical) es impar el resultado es 1.
3. Acarreo tantos unos como parejas (completas) de números 1 haya.

Por ejemplo:
0 + 0 = 0,
0 + 1 = 1,
1 + 0 = 1,
1 + 1 = 0 se pone 0 y se acarrea un 1 a la posición siguiente.

Para sumar 1010 (que en decimal es 10) y 1111 (que en decimal es 15). 10 + 15 = 25


Resta binaria

Las cuatro reglas básicas para la resta de números binarios son:
0 - 0 = 0
1 – 1 = 0
1 – 0 = 1
0 – 1 = 1 ( con acarreo negativo de 1)

Al restarse números algunas veces se genera un acarreo negativo que pasa a la siguiente columna de la izquierda. En binario solo se produce este acarreo cuando se intenta restar 1 de 0 (4ª regla).

Ejemplo sobre esta situación, restar 011 de 101:

101 – 011 = 010


Multiplicación binaria

La multiplicación binaria es tan sencilla como la decimal, y es que funcionan de la misma manera. Aquí tienen un ejemplo de multiplicación binaria. Supongamos que multipliquemos 10110 por 1001:

Vamos multiplicando por cada dígito de 1001 el conjunto 10110 y luego procedemos a hacer la suma. Hay otro tipo de procedimientos para realizar esta multiplicación sin signo y es el llamado "Multiplicación por el método de Suma-Desplazamiento


División binaria

Reglas de la división binaria: 0/0 no permitida, 1/0 no permitida,0/1=0, 1/1=1 .

• División: Se hace igual como el sistema decimal.

Ejemplo de división binaria: En este ejemplo dividiremos 101101 entre 111:



hay que comenzar cogiendo 4 cifras del dividendo para sobrepasar al divisor. Así resulta que 1011 entre 111 toca a 1 (solo puede ser 1 o 0):


1 por 111 es 111:


y falta 100 hasta llegar a 1011:



Bajando la siguiente cifra (un 0) resulta que 1000 entre 111 toca a 1.

Así sucesivamente.



Como operación digital

Exactamente igual que la operación binaria no digital, salvo que cambia la suma "1 + 1 = 0 y acarreo 1" por 1+1=1. Es decir, que en la suma digital no se acarrea ninguna cifra. Por lo demás, es igual que la suma binaria.

Como operación de un subsistema de ejecución

Hay muchos tipos más para tratar convenientemente la información en un sistema microprocesador, tales como manejo de variables de cadenas, movimientos de datos entre secciones físicas etc. Para una mayor profundidad en detalles sobre el tema, ver juego básico de instrucciones de un sistema microprocesador o juego de instrucciones de un microprocesador determinado.

Como proceso de datos numéricos

• Debe tenerse en consideración que esta clasificación es una aproximación para afrontar el tema con cierto orden, basado en el registro enciclopédico, ya que estrictamente unos campos entran en otros, y resulta imposible separar de modo absoluto cada uno de ellos, considerando esta, una clasificación plenamente aceptable.


Conjunción lógica

En matemáticas , una conjunción lógica (comúnmente conocida como Y, ó ) es un operador lógico que resulta en verdadero si los dos operadores son verdadero.

Definición

En lógica y matemáticas una conjunción es un "enunciado con dos o más elementos simultáneos". Una lampara eléctrica se enciende si hay corriente eléctrica, el interruptor esta conectado, el fusible esta bien y la lampara no esta fundida, en cualquier otro caso la lampara no se encenderá.
Para dos entradas A y B, la tabla de verdad de la función conjunción es:

Símbolo

El símbolo matemático para la conjuncion lógica varia en la literatura. Además de utilizar "Y", el símbolo en forma de es comúnmente utilizado para la conjunción. Por ejemplo:

se lee como "A y B". Esta Conjunción es cierta si ambas A y B son ciertas a la vez. En todos los demás casos es falsa.

La noción equivalente en teoría de conjuntos es la Intersección de conjuntos. Y el símbolo representativo es "y" y


Operación con bits

La conjunción es utilizada a menudo para operaciones con bits. Por ejemplo:

• Cero y cero:


• Cero y uno:


• Uno y cero:


• Uno y uno:



• Para cuatro bit:


Disyunción lógica


En matemáticas, una disyunción lógica (comúnmente conocida como O, ó ) es un operador lógico que resulta en verdadero si cualquiera de los operadores es verdadero.

Definición

En lógica y matemáticas una disyunción es un "enunciado con dos o más elementos optativos". Por ejemplo "Puedes leer este artículo o editarlo", es una disyunción con dos elementos, mientras que "Puedes leer este artículo, imprimirlo o editarlo" es una disyunción con tres elementos.
Nótese que en el lenguaje cotidiano el uso de la palabra "o" significa a veces "alguno, pero sólo uno", por ejemplo: "¿Vas a ir mañana a México o a España?". En lógica, a esto se le llama "disyunción exclusiva" u "o exclusivo". Cuando se utiliza formalmente, "o", permite que uno o más de los elementos de la disyunción sean válidos, por lo cuál "o" es también llamado "disyunción inclusiva" Plantilla:Rf.
Para dos entradas A y B, la tabla de verdad de la función disyuntiva es: también la disyucción, , es cuando hay dos elementos en dos conjuntos que forman una propocicion:

Más generalmente la disyunción es una fórmula lógica que puede tener una o más literales separadas con "o". Una sola literal se considera una disyunción degenerada.

Símbolo

El símbolo matemático para la disyunción lógica varia en la literatura. Además de utilizar "o", el símbolo en forma de "v" ("?") es comúnmente utilizado para la disyunción. Por ejemplo: "A ? B" se lee como "A o B". Esta disyunción es falsa si ambas A y B son falsas a la vez. En todos los demás casos es verdadera.
Todas las expresiones siguientes son disyunciones:
A ? B
¬A ? B
A ? ¬B ? ¬C ? D ? ¬E

La noción equivalente en teoría de conjuntos es la unión. Y el símbolo representativo es "O" y "V"

Asociatividad y Conmutatividad

Para más de dos elementos de entrada o puede ser aplicada a los primeros dos, y el resultado obtenido operado con o al siguiente elemento y así sucesivamente:
(A o (B o C)) ? ((A o B) o C)
Debido a que o es asociativo, el orden de las entradas no importa: el mismo resultado se obtiene sin importar la asociación que se haga.
El operador xor es también conmutativo y por consiguiente el orden de los operandos no importa:
A or B ? B or A

Operación con bits

La disyunción es utilizada a menudo para operaciones con bits. Por ejemplo:
• Cero ó cero:

• Cero ó uno:

• Uno ó cero:

• Uno ó uno:

• Para cuatro bit:

Nótese que en ciencias computacionales el operador o puede ser utilizado para llevar un bit a 1 aplicando una operación o entre el bit y un 1.

Unión

La unión utilizada en teoría de conjuntos se define en términos de la disyunción lógica: x ? A ? B si y solo si (x ? A) ? (x ? B). Debido a esto, la disyunción lógica satisface muchas de las mismas identidades que la unión de la teoría de conjuntos, como la asociatividad, conmutatividad, distributividad y las Leyes de De Morgan.

Negación lógica

En lógica y matemática, la negación, también llamada complemento lógico, es una operación sobre proposiciones, valores de verdad, o en general, valores semánticos. Intuitivamente, la negación de una proposición es verdadera cuando dicha proposición es falsa, y viceversa. En lógica clásica la negación está normalmente identificada con la función de verdad que cambia su valor de verdadero a falsity y viceversa. En Lógica intuicionista, de acuerdo a la interpretación BHK, la negación de una proposiciónp es la proposición cuyas pruebas con las refutaciones de p. En la semántica de Kripke, donde los valores semánticos de las fórmulas son conjuntos de posibles mundos, la negación de p, es su complemento.

Definición

La negación clásica es una operación sobre un valor de verdad, típicamente, el valor de una proposición, que produce un valor de verdadero cuando su operando es falso, y un valor de falso cuando su operando es verdadero. Por tanto, si el enunciado A es verdadero, entonces ¬A (pronunciado "no A") sería consecuentemente falso; y lo contrario, si ¬A es verdadero, entonces A sería falso.
La tabla de verdad de ¬p es la siguiente:
Tabla de verdad de ¬p
p ¬p
Verdadero Falso
Falso Verdadero
La genación clásica se puede definir en términos de otras operaciones lógicas. Por ejemplo, ¬p se puede definir como p ? F, donde "?" es una implicación lógica y F es una falsedad absoluta. Por el contrario, se puede definir F como p & ¬p para cualquier proposición p, donde "&" es una conjunción lógica. La idea aquí es que cualquier contradicción es falsa. Mientras estas ideas funcionan tanto en la lógica clásica como en la instuicionista, no funcionan en la [lógica paraconsistente]], donde las contradicciones no son necesariamente falsas.

En lógica clásica tenemos una identidad adicional: p ? q se puede definir como ¬p ? q, donde "?" es la disyunción lógica: "no p, o q".
Algebraicamente, la negación clásica corresponde con el complemento en un álgebra booleana, y la negación intuicionista al seudocomplemento en un álgebra de Heyting. Estas álgebras proveen una semántica para las lógicas clásica e intuicionista respectivamente.

Notación

La negación de una proposición p se denota dediferentes maneras en varios contextos de discisión y campos de aplicación. Entre estas variantes, tenemos las siguientes:

Notación Vocalización
¬p no p
-p no p
~p no p
Np ene p
p prima ,
complemento de p
p barra,
barra p
exclamación p
Independientemente de la notación o símbolo utilizados, la negación ¬p / ~p se puede leer como "no es el caso que p", "no es cierto que p", o por lo común, simplemente (aunque no gramaticalmente) como "no p".

Propiedades
Doble negación
Dentro de un sistema de lógica clásica, la doble negación, esto es, la negación de la negación de una proposición p, es lógicamente equivalente a p. Expresado simbólicamente, ¬(¬p) ? p. En lógica intuicionista, una proposición implica su doble negación, pero no al revés. Esto marca una importante diferencias entre las genaciones clásica e intuicionista. Algebraicamente, la negación clásica es llamada una involución de periodo dos.
Sim embargo, en lógica intuicionista, sí tenemos la equivalencia entre ¬¬¬p y ¬p. Es más, en el caso proposicional, una oración es demostrable de forma clásica, si su doble negación es demostrable de manera intuicionista. Este resultado es conocido como el teorema de Glivenko.


Linealidad
En el álgebra de Boole, una función lineal es una función tal que:
Si existe a0, a1, ... , an {0,1} tal que f(b1, ... , bn) = a0 ? (a1 b1) ? ... ? (an bn), para todo b1, ... , bn {0,1}.
Otra forma de expresar esto es que cada variable siempre cambia su valor de verdad de la operación o nunca cambia. La negación es un operador lógico lineal.

Autodualidad

En el álgebra de Boole, una función autodual es una función tal que:
Si f(a1, ... , an) = ~f(~a1, ... , ~an) para todo a1, ... , an {0,1}. La negación es un operador lógico de autodualidad.
Reglas de inferencia
Hay varias formas equivalentes entre sí, de formular reglas para la negación. Una forma usual de formular la negación clásica, al establecer una deducción natural, es tomar como reglas primitivas de inferencia:
• Introducción de la negación (Si p implica a q y ¬q, inferimos ¬p; esta regla también se llama reductio ad absurdum),

• Eliminación de la negación (Dado p y ¬p inferimos q; esta regla también se llama ex falso quodlibet),
• Eliminación de la doble negación (Dado ¬¬p inferimos p).
Las reglas para negación intuicionista se obtienen de la misma forma, pero excluyendo la eliminación de la doble negación.

La introducción de la negación establece que si se puede obtener un absurdo como conclusión de p, entonces p no debe ser el caso (p es falso (clásico), o refutable (intuicionista), etc.). La eliminación de la negación establece que cualquier cosa se desprende de un absurdo. A veces, la negación de la elimincación es formulada usando el signo primitivo de absurdo ?. En este caso, la regla dice que dado p y ¬p concluimos en un absurdity. Junto a la eliminación de la doble negación, se puede inferir la regla originalmente formulada, a saber, que cualquier cosa que se desprende de un absurdo.
Típicamente, la negación intuicionista ¬p de p se define como p??. Entonces la introducción de la negación y la eliminación son sólo casos especiales de introducción de la implicación (prueba condicional) y eliminación (modus ponens). En este caso, podemos también agregar como regla primitiva ex falso quodlibet.
Programación
Así como en matemática, la negación es ampliamente usada en computación para construir expresiones lógicas.
if (!(r == t))
{
/*...sentencias ejecutadas cuando r NO ES IGUAL a t...*/
}

El signo "!" significa NO lógico en B, C, y otros lenguajes inspirados en la sitaxis de C como C++, Java, Perl, PHP, etc. "NOT" es el operador usado en ALGOL 60, BASIC, COBOL, y lenguanes inspirados en la sintaxis de ALGOL como Pascal, Ada, Seed7, etc. Algunos lenguajes (C++, Perl, etc.) proveen más de un operador para la negación. Algunos lenguajes como PL/I y Ratfor, usan ¬ para la negación. Algunas computadoras y sistemas operativos modernos muestran ¬ como ! en archivos condificados en ASCII.
Existe también la negación a nivel de bits. Ésta toma el valor dado, y cambia todo el binario; los 1 cambian a 0 y los 0 a 1. Esta operación es usada normalmente para generar el complemento a uno o "~" en C o C++ y el complemento a dos (sólo simplificado a "-" o el signo negativo ya que esto es equivalente a tomar el valor aritmético negativo del número) ya que básicamente genera el opuesto (valor negativo equivalente) o complemento matemático del valor (donde ambos valores se agregan juntos para crear un todo).
Para obtener el valor absoluto (equivalente positivo) de un entero dado, el siguiente código trabajaría cambiando el signo de negativo a positivo (es negativo porque "x < 0" resulta verdadero)
unsigned int abs(int x)
{
if (x < 0)
return -x;
else
return x;
}
Para demostrar la negación lógica:
unsigned int abs(int x)
{
if (!(x < 0))
return x;
else
return -x;
}
Invirtiendo la condición y revirtiendo las salidas, se genera código que es lógicamente equivalente al código original, es decir, que obtendremos idénticos resultados para cualquier entrada. (Nota: Dependiendo del compilador utilizado, las instrucciones reales ejecutadas por la computadora pueden diferir).

jueves, 17 de mayo de 2012

HERENCIA EN OBJETOS

IMPLEMENTACIÓN DE HERENCIA EN OBJETOS

* Herencia Simple

Herencia Simple La herencia en C++ es un mecanismo de abstracción creado para poder facilitar, y mejorar el diseño de las clases de un programa. Con ella se pueden crear nuevas clases a partir de clases ya hechas, siempre y cuando tengan un tipo de relación especial. En la herencia, las clases derivadas “heredan” los datos y las funciones miembro de las clases base, pudiendo las clases derivadas redefinir estos comportamientos (polimorfismo) y añadir comportamientos nuevos propios de las clases derivadas. Para no romper el principio de encapsulamiento (ocultar datos cuyo conocimiento no es necesario para el uso de las clases), se proporciona un nuevo modo de visibilidad de los datos/funciones: “protected”. Cualquier cosa que tenga visibilidad protected se comportará como pública en la clase Base y en las que componen la jerarquía de herencia, y como privada en las clases que NO sean de la jerarquía de la herencia. Antes de utilizar la herencia, nos tenemos que hacer una pregunta, y si tiene sentido, podemos intentar usar esta jerarquía: Si la frase <claseB> ES-UN <claseA> tiene sentido, entonces estamos ante un posible caso de herencia donde clase A será la clase base y clase B la derivada. Ejemplo: clases Barco, Acorazado, Carguero, etc… un Acorazado ES-UN Barco, un Carguero ES-UN Barco, un Trasatlántico ES-UN Barco, etc.
En este ejemplo tendríamos las cosas generales de un Barco (en C++) class Barco {
protected:
char* nombre;
float peso;
public:


//Constructores y demás funciones básicas de barco
}; y ahora las características de las clases derivadas, podrían (a la vez que heredan las de barco) añadir cosas propias del subtipo de barco que vamos a crear, por ejemplo: Class Carguero: public Barco { // Esta es la manera de especificar que hereda de Barco
private:
float carga;
//El resto de cosas
};
Class Acorazado: public Barco {
private:
int numeroArmas;
int Soldados;


// Elresto de cosas
}; Por último, hay que mencionar que existen 3 clases de herencia que se diferencian en el modo de manejar la visibilidad de los componentes de la clase resultante: • Herencia publica (class Derivada: public Base ) : Con este tipo de herencia se respetan los comportamientos originales de las visibilidades de la clase Base en la clase Derivada. • Herencia privada (clase Derivada: private Base) : Con este tipo de herencia todo componente de la clase Base, será privado en la clase Derivada (ojo! siempre será privado aunque ese dato fuese público en la clase Base) • Herencia protegida (clase Derivada: protected Base) : Con este tipo de herencia, todo componente publico y protegido de la clase Base, será protegido en la clase Derivada, y los componentes privados, siguen siendo privados. Herencia Múltiple La herencia múltiple es el mecanismo que permite al programador hacer clases derivadas a partir, no de una sola clase base, sino de varias. Para entender esto mejor, pongamos un ejemplo: Cuando ves a quien te atiende en una tienda, como persona que es, podrás suponer que puede hablar, comer, andar, pero, por otro lado, como empleado que es, también podrás suponer que tiene un jefe, que puede cobrarte dinero por la compra, que puede devolverte el cambio, etc.

Si esto lo trasladamos a la programación sería herencia múltiple (clase empleado_tienda): class Persona {

Hablar();
Caminar();

};
class Empleado {
Persona jefe;
int sueldo;
Cobrar();

};
class empleado_tienda: public Persona, Empleado {

Almacenar Stock();
Comprobar Existencias?();

}; Por tanto, es posible utilizar más de una clase para que otra herede sus características. 
* Herencia Multiple


Herencia múltiple hace referencia a una característica de los lenguajes de programación orientada a objetos en la que una clase puede heredar comportamientos y características de más de una superclase. Esto contrasta con la herencia simple, donde una clase sólo puede heredar de una superclase.

Lenguajes que soportan herencia múltiple en su mayor parte son: C++, Centura SQL Windows, CLOS, Eiffel, Object REXX, Perl y Python.

La herencia múltiple permite a una clase tomar funcionalidades de otras clases, como permitir a una clase llamada MusicoEstudiante heredar de una clase llamada Persona, una clase llamada Músico, y una clase llamada Trabajador. Esto puede ser abreviado como MusicoEstudiante : Persona, Músico, Trabajador.

Ambigüedades

En la herencia múltiple aparecen ambigüedades, como en el ejemplo de encima: si la clase Músico heredaba de Persona y Trabajador, y la clase Trabajador heredaba de Persona. Existirían las siguientes reglas:

• MusicoEstudiante : Persona, Músico, Trabajador
• Músico : Persona, Trabajador
• Trabajador: Persona

Si un compilador está mirando la clase MusicoEstudiante necesita saber si debe juntar las características iguales o si deben estar separadas. Por ejemplo, tendría sentido unir las características "Edad" de Persona para MusicoEstudiante. La edad de una persona no cambia si le consideras una Persona, un Trabajador o un Músico. Sin embargo, tendría sentido separar la característica "Nombre" de Persona y Músico si los músicos usan un nombre artístico diferente de su nombre real. Las opciones de juntar y separar son válidas según el contexto, y sólo el programador sabe qué opción es correcta para la clase que está diseñando.

Cada lenguaje de programación trata estos problemas de herencia repetida de diferente forma:
• C++ requiere que el programador establezca de qué clase padre vendrá la característica a usar. Por ejemplo con "Trabajador::Persona.Edad". C++ no soporta herencia repetida explícita porque no habría forma de indicar qué superclase usar.

• CLOS permite al programador control total del método de combinación, y si no es suficiente, el protocolo de metaobjetos ofrece al programador formas de modificar la herencia, envío de métodos, instanciación de clases, y otros mecanismos internos sin afectar a la estabilidad del sistema.

• Eiffel permite al programador explicitar si junta o separa características que son heredadas de superclases. Eiffel juntará características automáticamente si tienen el mismo nombre e implementación. El programador tiene la opción de renombrar las características para separarlas. Eiffel también permite explicitar herencia repetida como A: B, B.

• Logtalk soporta tanto interfaces como multi-herencia de implementación, permitiendo declarar alias de métodos que ofrecen renombrar y acceder a métodos que quedarían ocultados por el mecanismo de resolución de conflictos convencional.

• Perl usa la lista de clases para heredar de una lista ordenada. El compilador usa el primer método que encuentra mediante búsqueda en profundidad por la lista de superclases.
Java, Nemerle, Delphi, C# y Objective-C no permiten herencia múltiple; esto hace que no haya ambigüedad. Sin embargo, permiten a las clases implementar múltiples interfaces.

Debate

Hay debate sobre si la herencia múltiple puede ser implementada de forma simple y sin ambigüedad. Con frecuencia es criticada por su aumentada complejidad y su ambigüedad, así como los problemas de versiones y mantenimiento que puede causar (a menudo resumido como el problema del diamante).

Los detractores también señalan que hay problemas de implementación de la herencia múltiple como no ser capaces de explicitar herencia de múltiple clases y el orden de las semánticas de clase que cambian con la herencia. Hay lenguajes que solucionan todos los problemas técnicos de la herencia múltiple, pero el debate principal sigue sobre si implementar y usar herencia múltiple es más fácil que usar herencia simple y patrones de diseño de software.