Địa chỉ của biến

####Chào tất cả các bạn học viên đang theo dõi khóa học lập trình trực tuyến ngôn ngữ C++ hướng thực hành.

Trong bài học này, mình sẽ đề cập đến vấn đề cũng khá quan trọng liên quan đến việc truy xuất biến (variable).

###Điều gì xảy ra sau khi khai báo biến?

Đối với các kiểu dữ liệu cơ bản mà các bạn đã học trong loạt bài trước đây, và khi các bạn khai báo biến cục bộ (local variable), sau khi các bạn khai báo biến, hệ điều hành sẽ tìm đến 1 vùng nhớ trống trên các thiết bị lưu trữ tạm thời (RAM hoặc các vùng nhớ khác), nếu tìm được vùng nhớ có khoảng trống đủ cho kích thước của biến, biến đó sẽ nắm giữ vùng nhớ vừa tìm được.

Giả sử chương trình của chúng ta có khai báo biến int32_t var, và tạm thời mình cho rằng RAM là thiết bị lưu trữ duy nhất mà máy tính của bạn đang có, chương trình sẽ tìm đến vị trí có 4 bytes bộ nhớ trống và giao cho biến var quản lý.

Với việc khai báo biến cục bộ, hoặc sử dụng các kiểu dữ liệu cơ bản mà các bạn đã học, chương trình sẽ cấp phát vùng nhớ cho các biến này trên một vùng nhớ được gọi là call stack (chúng ta sẽ có 1 bài học nói về vấn đề này).

Vậy thì sau khi 1 vùng nhớ đã được giao cho biến quản lý, làm sao compiler biết được chính xác vị trí của biến đó trên vùng nhớ để thực hiện các lệnh truy xuất hoặc thay đổi giá trị trên biến đó?

Compiler sẽ biết được vị trí của biến vì mỗi biến có 1 địa chỉ vùng nhớ trên thiết bị lưu trữ mà biến đó đang nắm giữ.

###Địa chỉ của biến

RAM hay các thiết bị cung cấp bộ nhớ tạm thời khác đều được tạo nên bởi các ô nhớ liên tiếp nhau, mỗi ô nhớ đều có 1 số thứ tự đại diện cho vị trí của ô nhớ đó trong thiết bị lưu trữ. Chúng ta có thể gọi con số đó địa chỉ của ô nhớ.

Những địa chỉ của ô nhớ chỉ là những con số ảo được tạo ra do hệ điều hành, còn về bản chất bên trong việc quản lý bộ nhớ của máy tính thì máy tính của chúng ta có những thiết bị riêng để làm điều đó.

Các bạn cứ tưởng tượng 1 ô nhớ trong thiết bị lưu trữ là một cái nhà trên con đường, để xác định được vị trí của 1 cái nhà, chúng ta cần biết địa chỉ của nhà cần tìm.

Địa chỉ ô nhớ đầu tiên được đánh số 0, và địa chỉ cuối cùng tương đương với số ô nhớ có trên thiết bị đó.

Giả sử biến var được khai báo bằng kiểu dữ liệu int32_t, và hệ điều hành tìm được vùng nhớ trống đủ 4 bytes để cung cấp cho biến var tại vị trí 125 đến 128, biến var sau khi được cấp phát vùng nhớ sẽ có địa chỉ 125 (là địa chỉ của ô nhớ đầu tiên mà biến nắm giữ).

Ở hình trên chỉ là minh họa cho việc cấp phát vùng nhớ cho biến có kích thước 4 bytes. Trên thực tế, địa chỉ của ô nhớ được cấp phát cho biến trong chương trình của chúng ta sẽ có giá trị rất lớn do các chương trình đang chạy trong hệ điều hành của chúng ta đã chiếm giữ trước đó.

Ở trên đây là kết quả của một chương trình mà mình viết. Mình đã tạo ra hai biến kiểu số nguyên int32_t có vùng nhớ nằm cạnh nhau, và mình thực hiện in ra địa chỉ của 2 biến đó.

Như các bạn thấy, biến đầu tiên có địa chỉ 14324216 thì biến tiếp theo sẽ có địa chỉ cách biến đầu tiên 4 bytes (là 14324220). Ở các bài học sau, bạn sẽ biết cách cấp phát những vùng nhớ liên tiếp nhau cho biến.

###Làm thế nào để lấy được địa chỉ của biến trong ngôn ngữ C++?

Ví dụ ta khai báo biến có tên var với kiểu dữ liệu bất kì mà bạn đã được học. Để lấy ra địa chỉ của biến var này, chúng ta đặt toán tử & trước tên của biến.

int32_t var;
cout << "Address of var: " << &var << endl;

Toán tử & được gọi là toán tử tham chiếu (address-of operator). Đoạn chương trình trên sẽ tìm đến chính xác địa chỉ mà biến var đang nắm giữ và in địa chỉ đó ra màn hình. Các bạn cùng xem kết quả bên dưới:

Thử chạy lại chương trình một lần nữa:

Chúng ta thấy qua 2 lần chạy chương trình thì địa chỉ của biến này có 2 vị trí khác nhau. Đồng nghĩa với việc chọn vị trí vùng nhớ để cấp phát cho biến hoàn toàn được thực thi tự động bởi hệ điều hành.

Địa chỉ của biến được định dạng theo hệ cơ số 16 chứ không phải hệ thập phân như chúng ta thường thấy.

###Tham chiếu (Reference)

Một tham chiếu (reference) trong ngôn ngữ C++ cũng là một kiểu dữ liệu cơ bản, nó hoạt động như một tên giả của biến nó tham chiếu đến.

1) Cách khai báo 1 tham chiếu (reference)

Đặt toán tử & giữa kiểu dữ liệu và tên biến trong khi khai báo biến sẽ tạo thành một tham chiếu.

int32_t & var_reference; //use to refer to another int32_t variable

Khi viết đến đây, compiler sẽ báo lỗi tại dòng khai báo tham chiếu, vì 1 tham chiếu cần có giá trị khởi tạo là tên biến mà nó sẽ tham chiếu đến.

Một biến tham chiếu chỉ có thể tham chiếu đến một biến khác có cùng kiểu dữ liệu.

2) Thực hiện tham chiếu đến biến khác:

int32_t 	var = 10;
int32_t & 	var_reference = var;

3) Thử in ra giá trị của 2 biến varvar_reference:

cout << "Value of var: " << var << endl;
cout << "Value of var_reference: " << var_reference << endl;

Kết quả cho thấy giá trị của biến var_reference hoàn toàn giống với biến var ban đầu.

Điều gì đã xảy ra? Chúng ta cùng làm thêm 1 bước nữa trước khi đi vào kết luận.

4) In ra địa chỉ của 2 biến varvar_reference:

cout << "Address of var: " << &var << endl;
cout << "Address of var_reference: " << &var_reference << endl;

Và đây là kết quả chương trình:

Kết quả cho thấy giá trị của tham chiếu var_reference và địa chỉ của var_reference hoàn toàn giống với biến var ban đầu. Vậy nó có phải là một bản sao của biến var? Hoàn toàn không phải nhé các bạn.

Về mặt ngữ nghĩa của dòng lệnh

int32_t & var_reference = var;

Toán tử & không mang ý nghĩa “địa chỉ của”, mà nó có nghĩa “tham chiếu đến”.

Khi thực hiện tham chiếu từ biến var_reference đến biến var, biến var_reference sẽ kiểm soát vùng nhớ có địa chỉ là địa chỉ của biến var.

Lúc này, biến var và biến var_reference vẫn là 2 tên biến khác nhau, nhưng chúng có cùng địa chỉ.

Điều này có nghĩa khi chúng ta thực hiện thay đổi giá trị cho biến var_reference, giá trị của biến var cũng thay đổi và ngược lại.

int32_t var = 10;
int32_t & var_reference = var;

cout << "Value of var: " << var << endl;
cout << "Value of var_reference: " << var_reference << endl;

var++;				//Increase value of var
var_reference++;	//Increase value of var_reference

cout << endl << "========================================" << endl << endl;

cout << "New value of var: " << var << endl;
cout << "New value of var_reference: " << var_reference << endl;

Một số lưu ý khi sử dụng tham chiếu

1) Các bạn không thể khởi tạo tham chiếu không phải hằng số bằng một biến hằng số.

Đoạn code sau sẽ báo lỗi:

const int32_t var = 10;
int32_t & ref = var;

Vì biến tham chiếu ref có thể thay đổi giá trị bên trong vùng nhớ, nhưng lúc này, var là hằng số nên giá trị vùng nhớ không được phép thay đổi. Điều này dẫn đến xung đột nên compiler ngăn chặn chúng ta biên dịch chương trình.

2) Nhưng chúng ta có thể tham chiếu một biến tham chiếu hằng số đến một hằng số.

const int32_t var = 10;
const int32_t & ref = var;

3) Hoặc chúng ta có thể tham chiếu một biến tham chiếu hằng số đến một biến bình thường.

int32_t var = 10;
const int32_t & ref = var;

4) Chúng ta không có thể thực hiện nhiều lần tham chiếu đến nhiều biến khác nhau.

#include <iostream>

using namespace std;

int main() {

    int32_t i_value1 = 10;
    int32_t i_value2 = 20;

    cout << "Address of i_value1: " << &i_value1 << endl;

    int32_t & ref = i_value1;
    cout << "Address of ref: " << &ref << endl;
    cout << "Value of ref:" << ref << endl;

    ref = i_value2;
    cout << "Address of ref: " << &ref << endl;
    cout << "Value of ref:" << ref << endl;
    cout << "Value of i_value1:" << i_value1 << endl;
}

Đây là kết quả của chương trình này:

Như các bạn thấy, địa chỉ của ref không bị thay đổi, nghĩa là phép gán thứ hai chỉ là phép gán giá trị thông thường, chứ không phải tham chiếu.

Lưu ý: Biến tham chiếu chỉ có thể tham chiếu một lần duy nhất ngay khi khai báo và khởi tạo. Chúng ta không thể tham chiếu đến biến có địa chỉ khác sau khi đã khởi tạo.


###Tổng kết

Việc hiểu được địa chỉ của biến khá là quan trọng. Sau này khi học đến phần con trỏ trong C++, mình sẽ còn nhắc lại khái niệm này.


Hẹn gặp lại các bạn trong bài học tiếp theo trong khóa học lập trình C++ hướng thực hành.

Mọi ý kiến đóng góp hoặc thắc mắc có thể đặt câu hỏi trực tiếp tại diễn đàn

www.daynhauhoc.com


Link Videos khóa học

https://www.udemy.com/c-co-ban-danh-cho-nguoi-moi-hoc-lap-trinh/learn/v4/overview

7 Likes

ref = & i_value2;
chứ nhỉ?

Biến tham chiếu không lưu trữ địa chỉ của biến được tham chiếu, nó chỉ trở thành một tên biến khác của vùng nhớ mà biến được tham chiếu đang nắm giữ.

&i_value2; //dòng này trả về giá trị kiểu int* trong khi ref vẫn là kiểu int
           //nên không thể gán &i_value2 cho biến kiểu int& được.

Có nghĩa là cách khai báo có thể là thế này

nhưng mà khi ta muốn nhiều lần tham chiếu đến đến nhiều biến khác nhau thì ở lần đâu tiên ta tham thiếu đến biết có kiểu int thì những lần sau khi tham chiếu đến những biến cũng có kiểu int thì ta có thể bỏ & đặt giữa hai toán tử đúng không anh?
Em chỉ đọc vì thấy bài viết hay thôi chứ không có biết về lập trình.

Mình có ghi đầy đủ mà bạn.

Lưu ý: Biến tham chiếu chỉ có thể tham chiếu một lần duy nhất ngay khi khai báo và khởi tạo. Chúng ta không thể tham chiếu đến biến có địa chỉ khác sau khi đã khởi tạo.

Cái bạn đang nói là phần con trỏ (ở chương 8) rồi. Sau này bạn học tới bài con trỏ sẽ gặp lại việc gán địa chỉ nhiều lần khác nhau cho biến con trỏ.

Bên cạnh đó, bạn cần phân biệt:

  • Toán tử tham chiếu: đặt giữa kiểu dữ liệu và tên biến khi khai báo.
  • Toán tử address-of: là toán tử một ngôi đặt trước tên biến.

Em không hiểu chỗ này lắm ?
Nếu thực hiện thế gì phải gán cho i_value1 giá trị của i_value2 chứ tại sao lại không ảnh hưởng ạ?

2 Likes

Cái đoạn đó hình như mình viết nhầm. Để mình viết lại. Thanks bạn!

Lưu ý: Biến tham chiếu chỉ có thể tham chiếu một lần duy nhất ngay khi khai báo và khởi tạo. Chúng ta không thể tham chiếu đến biến có địa chỉ khác sau khi đã khởi tạo.

Thông thường, khi khai báo một biến, compiler sẽ yêu cầu hệ điều hành cấp phát bộ nhớ (đi kèm là địa chỉ của biến). Nhưng biến tham chiếu khi khai báo không được hệ điều hành cung cấp địa chỉ. Nó chỉ được cung cấp địa chỉ khi thực hiện phép gán đầu tiên (thông qua khởi tạo). Do đó, biến tham chiếu khi khai báo bắt buộc phải khởi tạo. Sau đó thì tham chiếu sẽ có địa chỉ cố định.

Có thể tạm hiểu phép gán đầu tiên (khi khởi tạo) là để tham chiếu xác định địa chỉ. Các phép gán còn lại sẽ trở thành phép gán thông thường.

int32_t i_value1 = 10;
int32_t i_value2 = 20;

cout << "Address of i_value1: " << &i_value1 << endl;

int32_t & ref = i_value1;
cout << "Address of ref: " << &ref << endl;

ref = i_value2;
cout << "Address of ref: " << &ref << endl;

Có nghĩa đây là dòng code đã được sửa sau khi gặp lỗi tham chiếu một biến hai lần. Vậy đáng nhẽ anh phải viết dòng code chưa sửa chứ bởi vì đây đang muốn lấy ví dụ một trường hợp sai mà.
Em nghĩ bạn @dgra thắc mắc như vậy.

:sweat_smile:

Không phải mình cố tình đưa ví dụ sai đâu, bài này hôm bữa mình viết khuya quá nên mơ mơ màng màng nhớ nhầm ra bài con trỏ (trong chương 8 ấy) nên viết sai.

Bạn làm cách nào mà màn hình console trong suốt được vậy? :open_mouth:

Khi console hiện lên bấm chuột phải vào thanh cửa sổ chọn properties

1 Like

Mở console -> chuột phải vào ControlBar của console chọn Properties -> chọn thẻ Colors -> chỉnh mục Opacity (giá trị càng nhỏ càng mờ) -> Lưu lại

Sao em in địa chỉ của mấy biến ra mà nó ra cái gì mà 0x22ff38 ấy. Làm sao chuyển về hexa với cả thập phân ạ ?? Xin cho phương pháp đơn giản thôi ạ. Thanks nhiều !!

Cái 0X22FF38 là địa chỉ vùng nhớ mà biến của bạn đang chiếm giữ trong RAM.

#include <iostream>

using namespace std;

int main()
{
    int a;

    cout << (uintptr_t)&a << endl;

    return 0;
}

Bạn thử đi :wink: (chỉ áp dụng cho chuẩn C++11 trở lên)

Theo mình hiểu thì biến tham chiếu phải được gán với biến mà nó tham chiếu đến ngay khi nó được khai báo, chứ không giống như biến thông thường có thể khai báo xong rồi gán giá trị cho nó sau cũng được. Biến tham chiếu cũng giống những biến thông thường khác, tuy nhiên nó dùng chung bộ nhớ với một biến nó tham chiếu đến, mọi thay đổi trên biến tham chiếu đều làm ảnh hưởng đến biến và nó tham chiếu đến và ngược lại. Tuy nhiên mình vẫn chưa hiểu ứng dụng của biến tham chiếu này trong thực tế như thế nào, bạn có thể cho mình một vài ví dụ được không?

Nó thường được dùng cho tham số của hàm. Khi học đến phần hàm bạn sẽ rõ hơn về tác dụng của nó.

Bạn dùng ép kiểu trong C (C - style_casts)

(int)&var
83% thành viên diễn đàn không hỏi bài tập, còn bạn thì sao?