Prefacio
Java no se parece en nada a C ++, a diferencia del bombo publicitario. La máquina exagerada de Java quiere que creas que debido a que Java tiene una sintaxis similar a C ++, los lenguajes son similares. Nada puede estar más lejos de la verdad. Esta información errónea es parte de la razón por la cual los programadores de Java van a C ++ y usan una sintaxis similar a Java sin comprender las implicaciones de su código.
En adelante vamos
Pero no puedo entender por qué deberíamos hacerlo de esta manera. Supongo que tiene que ver con la eficiencia y la velocidad, ya que tenemos acceso directo a la dirección de memoria. Estoy en lo cierto?
Por el contrario, en realidad. El montón es mucho más lento que la pila, porque la pila es muy simple en comparación con el montón. Las variables de almacenamiento automático (también conocidas como variables de pila) tienen sus destructores llamados una vez que salen del alcance. Por ejemplo:
{
std::string s;
}
// s is destroyed here
Por otro lado, si usa un puntero asignado dinámicamente, su destructor debe llamarse manualmente. delete
llama a este destructor por ti.
{
std::string* s = new std::string;
}
delete s; // destructor called
Esto no tiene nada que ver con la new
sintaxis prevalente en C # y Java. Se usan para propósitos completamente diferentes.
Beneficios de la asignación dinámica
1. No tiene que saber el tamaño de la matriz de antemano
Uno de los primeros problemas con los que se encuentran muchos programadores de C ++ es que cuando aceptan entradas arbitrarias de los usuarios, solo se puede asignar un tamaño fijo para una variable de la pila. Tampoco puede cambiar el tamaño de las matrices. Por ejemplo:
char buffer[100];
std::cin >> buffer;
// bad input = buffer overflow
Por supuesto, si usó un std::string
en su lugar, std::string
internamente cambia su tamaño para que no sea un problema. Pero esencialmente la solución a este problema es la asignación dinámica. Puede asignar memoria dinámica en función de la entrada del usuario, por ejemplo:
int * pointer;
std::cout << "How many items do you need?";
std::cin >> n;
pointer = new int[n];
Nota al margen : un error que cometen muchos principiantes es el uso de matrices de longitud variable. Esta es una extensión de GNU y también una en Clang porque reflejan muchas de las extensiones de GCC. Por lo tanto
int arr[n]
, no se debe confiar en lo siguiente.
Debido a que el montón es mucho más grande que la pila, uno puede asignar / reasignar arbitrariamente tanta memoria como necesite, mientras que la pila tiene una limitación.
2. Las matrices no son punteros
¿Cómo es esto un beneficio que pides? La respuesta quedará clara una vez que comprenda la confusión / mito detrás de las matrices y los punteros. Se supone comúnmente que son iguales, pero no lo son. Este mito proviene del hecho de que los punteros se pueden suscribir al igual que las matrices y debido a que las matrices decaen a punteros en el nivel superior en una declaración de función. Sin embargo, una vez que una matriz se descompone en un puntero, el puntero pierde su sizeof
información. Por sizeof(pointer)
lo tanto , dará el tamaño del puntero en bytes, que generalmente es de 8 bytes en un sistema de 64 bits.
No puede asignar a matrices, solo inicializarlas. Por ejemplo:
int arr[5] = {1, 2, 3, 4, 5}; // initialization
int arr[] = {1, 2, 3, 4, 5}; // The standard dictates that the size of the array
// be given by the amount of members in the initializer
arr = { 1, 2, 3, 4, 5 }; // ERROR
Por otro lado, puedes hacer lo que quieras con punteros. Desafortunadamente, debido a que la distinción entre punteros y matrices se agita a mano en Java y C #, los principiantes no entienden la diferencia.
3. Polimorfismo
Java y C # tienen funciones que le permiten tratar los objetos como otro, por ejemplo, utilizando la as
palabra clave. Entonces, si alguien quisiera tratar un Entity
objeto como un Player
objeto, uno podría hacerlo. Player player = Entity as Player;
Esto es muy útil si tiene la intención de llamar a funciones en un contenedor homogéneo que solo debería aplicarse a un tipo específico. La funcionalidad se puede lograr de manera similar a continuación:
std::vector<Base*> vector;
vector.push_back(&square);
vector.push_back(&triangle);
for (auto& e : vector)
{
auto test = dynamic_cast<Triangle*>(e); // I only care about triangles
if (!test) // not a triangle
e.GenericFunction();
else
e.TriangleOnlyMagic();
}
Entonces, si solo Triángulos tuviera una función Rotar, sería un error del compilador si intentara llamarlo en todos los objetos de la clase. Usando dynamic_cast
, puede simular la as
palabra clave. Para ser claros, si un lanzamiento falla, devuelve un puntero no válido. Entonces, !test
es esencialmente una abreviatura para verificar si test
es NULL o un puntero no válido, lo que significa que el lanzamiento falló.
Beneficios de las variables automáticas.
Después de ver todas las excelentes cosas que puede hacer la asignación dinámica, probablemente se esté preguntando por qué nadie NO usaría la asignación dinámica todo el tiempo. Ya te dije una razón, el montón es lento. Y si no necesita toda esa memoria, no debe abusar de ella. Así que aquí hay algunas desventajas sin ningún orden en particular:
Es propenso a errores. La asignación manual de memoria es peligrosa y es propenso a fugas. Si no dominas el uso del depurador o valgrind
(una herramienta de pérdida de memoria), puedes sacarte el cabello de la cabeza. Afortunadamente, los modismos RAII y los punteros inteligentes alivian un poco esto, pero debes estar familiarizado con prácticas como La regla de tres y La regla de cinco. Es una gran cantidad de información, y los principiantes que no saben o no les importa caerán en esta trampa.
No es necesario. A diferencia de Java y C #, donde es idiomático usar la new
palabra clave en todas partes, en C ++, solo debe usarla si es necesario. La frase común dice, todo parece un clavo si tienes un martillo. Mientras que los principiantes que comienzan con C ++ tienen miedo de los punteros y aprenden a usar variables de pila por hábito, los programadores de Java y C # comienzan usando punteros sin entenderlo. Eso es literalmente pisar el pie equivocado. Debes abandonar todo lo que sabes porque la sintaxis es una cosa, aprender el idioma es otra.
1. (N) RVO - Aka, (Nombre) Optimización del valor de retorno
Una optimización que hacen muchos compiladores son las llamadas elisión y optimización del valor de retorno . Estas cosas pueden obviar copias innecesarias que son útiles para objetos que son muy grandes, como un vector que contiene muchos elementos. Normalmente, la práctica común es usar punteros para transferir la propiedad en lugar de copiar los objetos grandes para moverlos . Esto ha llevado al inicio de la semántica del movimiento y los punteros inteligentes .
Si está utilizando punteros, (N) RVO NO se produce. Es más beneficioso y menos propenso a errores aprovechar (N) RVO en lugar de devolver o pasar punteros si le preocupa la optimización. Pueden producirse fugas de error si el llamador de una función es responsable de delete
crear un objeto asignado dinámicamente y tal. Puede ser difícil rastrear la propiedad de un objeto si los punteros se pasan como una papa caliente. Simplemente use variables de pila porque es más simple y mejor.