Lời nói đầu
Java không giống như C ++, trái với sự cường điệu. Máy thổi phồng Java muốn bạn tin rằng vì Java có cú pháp giống C ++, nên các ngôn ngữ tương tự nhau. Không có gì có thể được thêm từ sự thật. Thông tin sai lệch này là một phần lý do tại sao các lập trình viên Java tìm đến C ++ và sử dụng cú pháp giống như Java mà không hiểu ý nghĩa của mã của họ.
Trở đi chúng tôi đi
Nhưng tôi không thể hiểu tại sao chúng ta nên làm theo cách này. Tôi cho rằng nó phải liên quan đến hiệu quả và tốc độ vì chúng ta có quyền truy cập trực tiếp vào địa chỉ bộ nhớ. Tôi có đúng không
Ngược lại, thực sự. Heap chậm hơn nhiều so với stack, vì stack rất đơn giản so với heap. Biến lưu trữ tự động (còn gọi là biến ngăn xếp) có hàm hủy của chúng được gọi khi chúng đi ra khỏi phạm vi. Ví dụ:
{
std::string s;
}
// s is destroyed here
Mặt khác, nếu bạn sử dụng một con trỏ được phân bổ động, hàm hủy của nó phải được gọi thủ công. delete
gọi hàm hủy này cho bạn.
{
std::string* s = new std::string;
}
delete s; // destructor called
Điều này không liên quan gì đến new
cú pháp phổ biến trong C # và Java. Chúng được sử dụng cho các mục đích hoàn toàn khác nhau.
Lợi ích của phân bổ động
1. Bạn không cần phải biết kích thước của mảng trước
Một trong những vấn đề đầu tiên mà nhiều lập trình viên C ++ gặp phải là khi họ chấp nhận đầu vào tùy ý từ người dùng, bạn chỉ có thể phân bổ kích thước cố định cho biến stack. Bạn cũng không thể thay đổi kích thước của mảng. Ví dụ:
char buffer[100];
std::cin >> buffer;
// bad input = buffer overflow
Tất nhiên, nếu bạn đã sử dụng std::string
thay thế, std::string
nội bộ sẽ tự thay đổi kích thước để không gặp sự cố. Nhưng thực chất giải pháp cho vấn đề này là phân bổ động. Bạn có thể phân bổ bộ nhớ động dựa trên đầu vào của người dùng, ví dụ:
int * pointer;
std::cout << "How many items do you need?";
std::cin >> n;
pointer = new int[n];
Lưu ý bên lề : Một sai lầm mà nhiều người mới bắt đầu mắc phải là việc sử dụng mảng có chiều dài thay đổi. Đây là một phần mở rộng GNU và cũng là một phần mở rộng trong Clang vì chúng phản ánh nhiều phần mở rộng của GCC. Vì vậy, những điều sau đây
int arr[n]
không nên dựa vào.
Bởi vì heap lớn hơn nhiều so với stack, người ta có thể tùy ý phân bổ / phân bổ lại nhiều bộ nhớ theo nhu cầu của mình, trong khi stack có giới hạn.
2. Mảng không phải là con trỏ
Làm thế nào đây là một lợi ích bạn yêu cầu? Câu trả lời sẽ trở nên rõ ràng khi bạn hiểu được sự nhầm lẫn / huyền thoại đằng sau mảng và con trỏ. Người ta thường cho rằng chúng giống nhau, nhưng chúng không giống nhau. Huyền thoại này xuất phát từ thực tế là các con trỏ có thể được đăng ký giống như các mảng và do các mảng phân rã thành các con trỏ ở cấp cao nhất trong một khai báo hàm. Tuy nhiên, một khi một mảng phân rã thành một con trỏ, con trỏ sẽ mất sizeof
thông tin của nó . Vì vậy, sizeof(pointer)
sẽ cho kích thước của con trỏ theo byte, thường là 8 byte trên hệ thống 64 bit.
Bạn không thể gán cho mảng, chỉ khởi tạo chúng. Ví dụ:
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
Mặt khác, bạn có thể làm bất cứ điều gì bạn muốn với con trỏ. Thật không may, vì sự khác biệt giữa con trỏ và mảng được vẫy tay trong Java và C #, người mới bắt đầu không hiểu sự khác biệt.
3. Đa hình
Java và C # có các phương tiện cho phép bạn coi các đối tượng như một đối tượng khác, ví dụ như sử dụng as
từ khóa. Vì vậy, nếu ai đó muốn coi một Entity
đối tượng là một Player
đối tượng, người ta có thể làm Player player = Entity as Player;
Điều này rất hữu ích nếu bạn có ý định gọi các hàm trên một thùng chứa đồng nhất chỉ nên áp dụng cho một loại cụ thể. Các chức năng có thể đạt được theo cách tương tự dưới đây:
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();
}
Vì vậy, giả sử nếu chỉ có Tam giác có chức năng Xoay, thì đó sẽ là lỗi trình biên dịch nếu bạn cố gọi nó trên tất cả các đối tượng của lớp. Sử dụng dynamic_cast
, bạn có thể mô phỏng as
từ khóa. Để rõ ràng, nếu một cast thất bại, nó trả về một con trỏ không hợp lệ. Vì vậy, !test
về cơ bản là một tốc ký để kiểm tra nếu test
là NULL hoặc một con trỏ không hợp lệ, điều đó có nghĩa là việc truyền không thành công.
Lợi ích của biến tự động
Sau khi thấy tất cả những điều tuyệt vời mà phân bổ động có thể làm, có lẽ bạn sẽ tự hỏi tại sao mọi người KHÔNG sử dụng phân bổ động mọi lúc? Tôi đã nói với bạn một lý do, đống là chậm. Và nếu bạn không cần tất cả bộ nhớ đó, bạn không nên lạm dụng nó. Vì vậy, đây là một số nhược điểm không theo thứ tự cụ thể:
Nó dễ bị lỗi. Cấp phát bộ nhớ thủ công là nguy hiểm và bạn dễ bị rò rỉ. Nếu bạn không thành thạo sử dụng trình gỡ lỗi hoặc valgrind
(một công cụ rò rỉ bộ nhớ), bạn có thể nhổ tóc ra khỏi đầu. May mắn thay, thành ngữ RAII và con trỏ thông minh làm giảm bớt điều này một chút, nhưng bạn phải làm quen với các thực tiễn như Quy tắc ba và Quy tắc năm. Đó là rất nhiều thông tin để đưa vào, và những người mới bắt đầu không biết hoặc không quan tâm sẽ rơi vào cái bẫy này.
Nó không phải là cần thiết. Không giống như Java và C # nơi sử dụng new
từ khóa ở mọi nơi, trong C ++, bạn chỉ nên sử dụng nó nếu bạn cần. Các cụm từ phổ biến đi, mọi thứ trông giống như một cái đinh nếu bạn có một cái búa. Trong khi những người mới bắt đầu với C ++ sợ các con trỏ và học cách sử dụng các biến stack theo thói quen, các lập trình viên Java và C # bắt đầu bằng cách sử dụng các con trỏ mà không hiểu nó! Đó là nghĩa đen bước trên chân sai. Bạn phải từ bỏ mọi thứ bạn biết vì cú pháp là một chuyện, học ngôn ngữ là chuyện khác.
1. (N) RVO - Aka, (Được đặt tên) Tối ưu hóa giá trị trả về
Một tối ưu hóa nhiều trình biên dịch thực hiện những điều gọi là sự bỏ bớt và tối ưu hóa giá trị trả về . Những thứ này có thể làm mờ các bản sao không cần thiết, hữu ích cho các đối tượng rất lớn, chẳng hạn như một vectơ chứa nhiều phần tử. Thông thường, thông lệ là sử dụng các con trỏ để chuyển quyền sở hữu thay vì sao chép các đối tượng lớn để di chuyển chúng xung quanh. Điều này đã dẫn đến sự khởi đầu của ngữ nghĩa di chuyển và con trỏ thông minh .
Nếu bạn đang sử dụng con trỏ, (N) RVO KHÔNG xảy ra. Sẽ có lợi hơn và ít bị lỗi hơn khi tận dụng (N) RVO thay vì trả lại hoặc chuyển con trỏ nếu bạn lo lắng về tối ưu hóa. Rò rỉ lỗi có thể xảy ra nếu người gọi hàm có trách nhiệm lấy delete
một đối tượng được phân bổ động và như vậy. Có thể khó theo dõi quyền sở hữu của một đối tượng nếu con trỏ được truyền xung quanh như một củ khoai tây nóng. Chỉ cần sử dụng biến stack vì nó đơn giản và tốt hơn.