Sai lầm khi thao tác với tham số con trỏ trong C++ của mình



Thời gian gần đây mình có làm việc với C++ trong một dự án nhỏ của mình và mình đã gặp một lỗi rất ngớ ngẩn trong khi làm việc với con trỏ.
Cho đoạn chương trình sau đây
#include <iostream>

void sayHello(const char* c)
{
 c = "Hello World";
 std::cout << "String change to:" << std::endl;
 std::cout << c << std::endl;
}

int main() 
{
 const char* charArray = "This is a string";
 std::cout << "Before:" << std::endl;
 std::cout << charArray << std::endl;
 
 sayHello(charArray);
 
 std::cout << "After:" << std::endl;
 std::cout << charArray << std::endl;
 return 0;
} 
Các bạn có thể tự chạy thử chương trình trên. Kết quả của các bạn là gì?
Còn đây là của mình:

Lúc này, mình nghĩ trong đầu các bạn sẽ có 2 câu hỏi:
  • Ơ, thế quái nào mà lại có thể gán con trỏ const char vô giá trị khác nhỉ?
  • Ơ nếu gán được sao giá trị của charArray không đổi nhỉ? (Lúc đó mình cũng tự hỏi câu này)
Đây là khái niệm đôi lúc mình cũng hay nhầm lẫn và chúng ta sẽ cùng trả lời từng câu hỏi một khi đọc tiếp.

Con trỏ và biến

Lúc trước mình có đề cập một trường hợp khi bạn thay đổi giá trị của một tham số trong hàm ở bài “Tham trị, tham chiếu và con trỏ”. Cụ thể như sau, khi ta truyền một biến vào hàm. Compiler sẽ copy giá trị phía trong biến đó. Thứ mà chúng ta tương tác trong hàm thực chất chỉ là một biến khác mang giá trị giống y hệt biến chúng ta đã truyền vào. Đồng thời, biến này cũng sẽ bị giải phóng khi hàm kết thúc.

  Vậy, khi chúng ta thay đổi giá trị trong hàm. Bên ngoài, biến của chúng ta không thay đổi. Một cách xử lý đó chính là xử dụng tham chiếu hoặc con trỏ.
Tham chiếu thực chất cũng chỉ là con trỏ hằng trỏ đến biến tham số của chúng ta thôi nên mình sẽ nói trường hợp truyền tham chiếu vào là con trỏ.

Với con trỏ, thứ chúng lưu là giá trị cùng nhớ mà nó trỏ đến. Vậy nên khi truyền con trỏ vào hàm. Giá trị chứa trong con trỏ (địa chỉ trỏ đến) sẽ được copy và thế là ta đã có thể thay đổi trực tiếp trên vùng nhớ được trỏ đến này.

Tại sao lại thay đổi được const char*

Đầu tiên, có hai khái niệm hay bị nhầm lẫn với nhau đó là con trỏ đến hằngcon trỏ hằng.

Con trỏ hằng là con trỏ không thể trỏ đến vùng nhớ khác sau khi khởi tạo. Một ví dụ về con trỏ hằng đó chính là tham chiếu. Cách duy nhất để kêu con trỏ hằng trỏ đến một vùng nhớ đó chính là làm điều đó ngay từ lúc khai báo. Sau đó, giá trị bên trong con trỏ không thể thay đổi được nữa (khá giống biến hằng nhỉ). Cú pháp để tạo một con trỏ hằng là:
int val = 6;
int * const constPointer = &val;

*constPointer = 15; // legal

int anotherVal = 15;
constPointer = &anotherVal; // wrong
Con trỏ đến hằng đơn giản hơn. Nó là con trỏ và địa chỉ chúng trỏ đến là biến hằng. Tức ta không thể thay đổi giá trị của bộ nhớ được trỏ đến đó. Cú pháp là:
const int* pointerToConst = 6;
Hay một cách viết cụ thể hơn là:
const int val = 6;
const int* pointerToConst = &val;

*pointerToConst = 15; // wrong

const int anotherConst = 15;
pointerToConst = &anotherConst; // legal
Có thể bạn sẽ thắc mắc liệu có thể gán một biến bình thường cho con trỏ trỏ đến hằng hay ngược lại hay không? Mình đã làm thử và đây là kết quả.
int val = 6;
const int* ptr = &val; // legal

val++; // ok
(*ptr)++; // error
Ta có thể gán một biến bình thường cho con trỏ tới hằng. Tuy nhiên, ta sẽ không thể thay đổi giá trị của biến thông qua con trỏ đó.
const int val = 6;
int * const ptr = &val; // error
Do một cái là const int* còn một cái là int* nên lệnh gán này đơn giản không hợp lệ

Giải đáp vấn đề ban đầu

Bây giờ, ta đã biết đủ để giải mã vấn đề ban đầu. Cùng phân tích nhé.

Do tham số là const char* nên lệnh
c = "Hello World";
vẫn hợp lệ. Chúng ta thực chất đang trỏ con trỏ c sang một biến const char khác có giá trị là “Hello World”. Tuy nhiên mình đã mắc một sai lầm đó là cho rằng con trỏ này chính là con trỏ mình truyền vào.

Nhưng không phải, đây chỉ là một con trỏ khác mang giá trị là địa chỉ của vùng nhớ const char kia. Khi mình thay đổi nơi nó trỏ đến, con trỏ bên ngoài vẫn ko thay đổi.
Và thế là, mình đã mắc cái sai lầm đầu tiên mình nêu ra. Chỉ khác ở chỗ “con trỏ” hơn thôi.

Cảnh báo: Hack não zone

Cách giải quyết khá đơn giản, đó là truyền vào một tham chiếu con trỏ. Tức con trỏ hằng trỏ tới con trỏ trỏ tới hằng. (Sorry)
void sayHello(const char* &c);
và … problem solved
Bạn có thể hình dung rõ hơn thông qua hình sau:

Tổng kết

Ok, nếu bạn đã đọc đến đây mình nghĩ trình C++ của bạn đã tăng lên chút đỉnh đấy. Nếu bạn thấy thích những bài viết của mình và thấy rằng chúng bổ ích. Hãy share bài viết này của RootOnChair đến nhiều người hơn nhé.

Tái bút: Nếu bạn nào thắc mắc tại sao "Hello World" không bị giải phóng thì mọi biến hằng ngay từ ban đầu đã được khởi tạo trước khi chương trình chạy ở một vùng nhớ đặc biệt của chương trình chứ không trong bất kỳ hàm nào nhé. Bao gồm cả "Hello World".

Nhận xét

Đăng nhận xét

Bài đăng phổ biến từ blog này

Phép phân tích ma trận A=LU

Hướng dẫn đăng ký khóa học trên Coursera và edX miễn phí

Độc lập tuyến tính và phụ thuộc tuyến tính