Structs and pointer

Chào các bạn đang theo dõi khóa học lập trình trực tuyến ngôn ngữ C++.

Chúng ta cùng tiếp tục tìm hiểu về kiểu dữ liệu tự định nghĩa thông qua từ khóa struct mà ngôn ngữ C++ hỗ trợ. Trong bài học này, mình sẽ trình bày về kiểu struct khi sử dụng kết hợp với con trỏ.

Như các bạn đã học trong bài trước, sau khi chúng ta tự định nghĩa một struct, compiler sẽ coi tên gọi của struct đó như là một kiểu dữ liệu. Điều này có nghĩa khi chúng ta sử dụng các kiểu dữ liệu built-in để tạo ra các biến, tham chiếu hoặc con trỏ thì chúng ta cũng có thể sử dụng kiểu struct để tạo ra biến struct, tham chiếu struct và con trỏ kiểu struct (Pointer to struct).

###Pointer to struct

Đầu tiên, chúng ta cùng định nghĩa một kiểu dữ liệu theo ý muốn. Dưới đây, mình định nghĩa một kiểu dữ liệu có tên là Letter:

struct Letter
{
	
};

Trong struct Letter mình chưa định nghĩa các trường dữ liệu, lúc này Letter là một kiểu dữ liệu rỗng. Nhưng ngôn ngữ C++ vẫn đặt kích thước của kiểu Letter này là 1 bytes.

Mục đích là để đảm bảo địa chỉ của 2 biến được tạo ra sẽ có địa chỉ khác nhau. Tuy nhiên, định nghĩa ra một struct rỗng không có tác dụng gì trong chương trình, chúng ta cùng thêm vào một số trường dữ liệu cho Letter:

struct Letter
{
	char from[50];
	char to[50];
};

Một lá thư sẽ có thông tin về người gửi và người nhận, nên mình thêm vào 2 trường dữ liệu kiểu C-style string dùng để lưu thông tin mà người dùng điền vào một lá thư.

Mình vừa định nghĩa xong một kiểu dữ liệu mới để phục vụ cho chương trình của mình. Bây giờ chúng ta cùng tạo ra một đơn vị từ kiểu dữ liệu trên (mình thao tác luôn trong hàm main):

int main()
{
	Letter myLetter;
	
	return 0;
}

Với mỗi biến kiểu Letter được tạo ra, chương trình sẽ yêu cầu cấp phát 100 bytes (50 bytes cho trường dữ liệu from và 50 bytes cho trường dữ liệu to), và chắc chắn rồi, biến đó sẽ có một địa chỉ xác định được thông qua toán tử address-of.

int main()
{
	Letter myLetter;
	std::cout << "Address of myLetter: " << &myLetter << std::endl;
	std::cout << "Address of from field: " << &myLetter.from << std::endl;
	
	return 0;
}

Ở đoạn chương trình trên, mình in ra địa chỉ của biến myLetter, đồng thời in ra luôn địa chỉ của trường dữ liệu from của biến myLetter. Kết quả cho thấy 2 địa chỉ được in ra có giá trị hoàn toàn giống nhau. Điều này có nghĩa địa chỉ của trường dữ liệu đầu tiên trong một biến struct cũng là địa chỉ của biến struct đó. Các bạn có thể liên hệ struct với mảng một chiều trong C/C++, khi mảng một chiều mà tập hợp các phần tử có cùng kiểu dữ liệu được bao bọc bởi tên mảng một chiều và địa chỉ của mảng một chiều cũng là địa chỉ của phần tử đầu tiên trong mảng, một biến struct sẽ bao gồm tập hợp các trường dữ liệu mà địa chỉ của biến struct sẽ là địa chỉ của trường dữ liệu được khai báo đầu tiên trong struct.

Và như các bạn cũng đã học về con trỏ (Pointer), kiểu dữ liệu của con trỏ dùng để xác định kiểu dữ liệu của vùng nhớ mà con trỏ có thể trỏ đến. Vậy thì để cho con trỏ trỏ đến một địa chỉ của biến kiểu struct, chúng ta cần có một con trỏ cùng kiểu struct với biến được trỏ đến.

Letter myLetter;
Letter *pLetter = &myLetter;

Dù kích thước của kiểu dữ liệu struct có lớn bao nhiêu, biến con trỏ cũng chỉ có kích thước 4 bytes trên hệ điều hành 32 bits và kích thước 8 bytes trên hệ điều hành 64 bits (đủ để trỏ đến toàn bộ địa chỉ trên bộ nhớ ảo).

#####Access struct members

Trong bài học trước, các bạn đã biết cách truy cập đến các trường dữ liệu của các biến struct thông qua member selection operator (dấu chấm). Nhưng khi sử dụng Pointer to struct, member selection operator được sử dụng dưới cách viết khác. Để phân biệt sự khác nhau khi sử dụng member selection operator cho biến struct thông thường và một Pointer to struct, các bạn cùng xem ví dụ bên dưới:

struct BankAccount
{
	__int64 accountNumber;
	__int64 balance;
};

int main()
{
	BankAccount myAccount = { 123456789, 50 }; // $50
	BankAccount *pAccount = &myAccount; 

	std::cout << "My bank account number: " << myAccount.accountNumber << std::endl;
	std::cout << "My bank account number: " << pAccount->accountNumber << std::endl;

	std::cout << "My balance: " << myAccount.balance << std::endl;
	std::cout << "My balance: " << pAccount->balance << std::endl;

	return 0;
}

Như các bạn thấy, kết quả của việc truy xuất giá trị thông qua tên biến struct và con trỏ kiểu struct là hoàn toàn giống nhau, và chúng đều dùng toán tử member selection. Tuy nhiên, để phân biệt biến con trỏ và biến thông thường, biến con trỏ kiểu struct sẽ truy cập đến các trường dữ liệu trong vùng nhớ bằng toán tử (->). Hai toán tử này cùng tên, chỉ khác nhau về cách biểu diễn.

#####Một số nhầm lần khi sử dụng struct và Pointer to struct

Khi mới tìm hiểu về Pointer to struct, các bạn có thể bị nhầm lẫn giữa cách khởi tạo hoặc gán giá trị cho biến struct thông thường và biến con trỏ struct.

struct BankAccount
{
	__int64 accountNumber;
	__int64 balance;
};

int main()
{
	BankAccount myAccount = { 12345, 50 };

	BankAccount *pAccount = { 12345, 50 }; //error

	return 0;
}

Đoạn chương trình trên báo lỗi vì biến con trỏ chỉ nhận giá trị là địa chỉ. Tuy nhiên, lỗi này có thể thấy dễ dàng vì Visual studio đưa ra thông báo lỗi ngay. Dưới đây là cách gán giá trị đúng khi mình sử dụng toán tử dereference cho biến con trỏ struct để thay đổi giá trị bên trong vùng nhớ:

struct BankAccount
{
	__int64 accountNumber;
	__int64 balance;
};

int main()
{
	BankAccount myAccount = { 0, 0 };

	BankAccount *pAccount = &myAccount;

	*pAccount = { 12345, 50 };

	std::cout << pAccount->accountNumber << " " << pAccount->balance << std::endl;

	return 0;
}

Hoặc một cách khác là chúng ta cấp phát vùng nhớ cho biến con trỏ struct, và dereference đến đó để gán giá trị cho nó:

BankAccount *pAccount = new BankAccount;
*pAccount = { 12345, 50 };

Và các bạn lưu ý khi sử dụng biến kiểu con trỏ struct thì chúng ta sử dụng toán tử member selection này (->). Có một số bạn nhầm lẫn giữa biến con trỏ struct và trường dữ liệu kiểu con trỏ. Ví dụ:

struct BankAccount
{
	char *name;
	__int64 accountNumber;
	__int64 balance;
};

Mình thêm vào struct một trường dữ liệu kiểu con trỏ char nhưng việc truy xuất đến trường dữ liệu này không có gì thay đổi khi mình sử dụng biến struct thông thường.

BankAccount myAccount = { "Le Tran Dat", 12345, 50 };

std::cout << myAccount.name << std::endl;
std::cout << myAccount.accountNumber << std::endl;
std::cout << myAccount.balance << std::endl;

Sẽ phức tạp hơn một chút khi các bạn sử dụng các nested struct. Ví dụ:

struct BankAccount
{
	Date registrationDate;
	__int64 accountNumber;
	__int64 balance;
};

int main()
{
	BankAccount *pAccount = new BankAccount;
	*pAccount = { {2, 5, 2016}, 12345, 50 };

	std::cout << pAccount->registrationDate.year << std::endl;

	return 0;
}

Như các bạn thấy, từ biến con trỏ pAccount truy xuất vào các trường dữ liệu bên trong thì mình dùng toán tử (->), nhưng trường dữ liệu Date trong struct BankAccount là biến thông thường, nên mình dùng dấu chấm để truy xuất dữ liệu ngày đăng kí.

Trên đây là một số vấn đề thường gặp khi sử dụng con trỏ và kiểu struct. Tuy nhiên, những lỗi này không phải là lỗi nghiêm trọng do compiler sẽ thông báo chính xác vị trí lỗi cho lập trình viên xử lý.


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

5 Likes

Đã có bài học trong nhập, xuất, streams(input & output) chưa ạ?

anh oi. Sao chua co bai ve smartpoiter , quan ly ma nguon, … vay a.

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