Lập trình C/C++

Con trỏ là gì? Có ăn được không?

Con trỏ là gì? Có ăn được không?
Được viết bởi Minh Hoàng

Series lập trình C/C++, ngôn ngữ lập trình hệ thống mạnh mẽ.

– Chắc hẳn khi học về lập trình C/C++ các bạn đều nghe sự đồn thổi về độ khó của con trỏ rồi nhỉ. Vậy con trỏ là gì? Có ăn được không? Chúng ta sẽ cùng làm sáng tỏ trong series về nó nhé.

– Đầu tiên, trong bài này chúng ta sẽ tìm hiểu về những nội dung sau đây:

  1. Con trỏ là gì?
  2. Tại sao lại có con trỏ? Sử dụng để làm gì?
  3. Cách khai báo con trỏ, con trỏ vô kiểu, con trỏ null là gì?
  4. Cách sử dụng con trỏ cơ bản
1. Con trỏ là gì?
1. Con trỏ là gì?

Con trỏ là một biến bình thường nhưng có thể trỏ đi lung tung trong bộ nhớ. Và giá trị của nó là địa chỉ của một biến khác (địa chỉ trực tiếp của một ô nhớ trong bộ nhớ).

– Về bản chất con trỏ cũng như một biến bình thường: nó cũng có tên biến, giá trị của biến, địa chỉ của biến. Nhưng có điểm khác là:

  • Những biến bình thường thì nó chỉ nằm cố định trong 1 ô nhớ, còn biến con trỏ thì nó còn có thể trỏ đến các ô nhớ khác nhau.
  • <Kiểu dữ liệu> khi khai báo cho con trỏ không phải là kiểu dữ liệu của nó, mà là kiểu dữ liệu của vùng nhớ mà nó đang trỏ đến.

– Để dễ hình dung các bạn xem hình sau minh họa bản chất của con trỏ:

Ta có 3 biến con trỏ a, b, c có địa chỉ lần lượt là 0x0022, 0x0800, 0x0006 trỏ đến vùng nhớ có địa chỉ 0x0099 lưu giá trị 69

Ta có 3 biến con trỏ a, b, c có địa chỉ lần lượt là 0x0022, 0x0800, 0x0006 trỏ đến vùng nhớ có địa chỉ 0x0099 lưu giá trị 69

2. Tại sao lại có con trỏ? Sử dụng để làm gì?
2. Tại sao lại có con trỏ? Sử dụng để làm gì?

– Vì biến con trỏ có thể trỏ đi lung tung trong bộ nhớ nên việc sử dụng bộ nhớ sẽ linh hoạt hơn.

– Áp dụng cho mảng động. Có nghĩa là khi chúng ta sử dụng mảng tĩnh với số lượng phần tử của mảng là cố định, chẳng hạn như mảng có 100 phần tử, thì dù chúng ta chỉ sử dụng 5 – 10 phần tử để thao tác tính toán thôi, thì bộ nhớ cũng sẽ cấp phát 100 ô nhớ, do đó mà sẽ gây ra lãng phí bộ nhớ không đáng có. Còn khi chúng ta sử dụng mảng động dùng con trỏ thì chúng ta sử dụng 5 thì bộ nhớ cấp phát 5 ô nhớ, sử dụng 10 thì bộ nhớ cấp phát 10 ô nhớ.

Có thể bạn quan tâm: Cách cấp phát và giải phóng bộ nhớ trong lập trình C.

– Sau này khi đến lập trình hướng đối tượng, thì phải có con trỏ mới thực hiện đa hình được. (Chúng ta sẽ đề cập đến vấn đề này ở bài Tính đa hình trong C++)

3. Cách khai báo con trỏ, con trỏ vô kiểu, con trỏ null là gì?
3. Cách khai báo con trỏ, con trỏ vô kiểu, con trỏ null là gì?

– Cú pháp:

<Kiểu dữ liệu> *<Tên của con trỏ>

– Trong đó:

  • <Kiểu dữ liệu> : Bao gồm các kiểu dữ liệu có sẵn (int, float, double, char, void) và kiểu dữ liệu do người dùng tự định nghĩa (Book, HocSinh, PhanSo,… )

  • Dấu * : biểu thị đây là biến con trỏ.
  • <Tên của con trỏ> : Tuân theo quy tắc đặt tên biến trong lập trình.

– Có thể khai báo một biến con trỏ theo 1 trong 3 dạng sau:

[code language=”c”] int *p; // Khai báo biến con trỏ p, trỏ đến ô nhớ có kiểu nguyên
int* p;
int * p;
[/code]

=> 3 cách khai báo trên là như nhau, nhưng để cho rõ ràng code thì lời khuyên là:
・Khi khai báo 1 biến là con trỏ thì dấu * để sát tên biến.

int *p;	// Khai báo biến con trỏ

int *a;

・Khi khai báo 1 hàm kiểu con trỏ thì dấu * để sát tên kiểu dữ liệu.

int* TimMax(int *a, int *b); // Khai báo hàm trả về 1 con trỏ

int* TinhTong(int *x, int *y);

・Chú ý khi khai báo 2 biến con trỏ trên cùng 1 dòng.

int *a, *b;	// Khai báo 2 biến con trỏ a, b

int* a, b;	// Khai báo 1 biến con trỏ a, 1 biến thường b

・Các con trỏ cùng kiểu có thể gán cho nhau.

– Dùng toán tử * để lấy giá trị của một biến con trỏ (và toán tử * cũng thể hiện cho một biến là con trỏ, nên các bạn khi sử dụng cần phân biệt rõ ràng là khi nào *p là con trỏ p, khi nào *p là đang muốn lấy giá trị của con trỏ p).

– Dùng toán tử & để lấy địa chỉ của một biến.

Con trỏ vô kiểu void* : Có thể nhận được mọi kiểu dữ liệu nhưng khi sử dụng cần phải ép về kiểu dữ liệu muốn sử dụng (Để hiểu hơn thì bạn xem minh họa ở mục Cách sử dụng con trỏ cơ bản).

Con trỏ khai báo kiểu int thì chỉ trỏ đến ô nhớ kiểu int, con trỏ khai báo kiểu float thì chỉ trỏ được đến ô nhớ có kiểu là float,… Nếu con trỏ khai báo kiểu int mà chúng ta cho trỏ đến ô nhớ kiểu float hoặc ngược lại thì sẽ lỗi. Nhưng đối với con trỏ vô kiểu void* thì nó có thể trỏ được đến ô nhớ bất kỳ không quan tâm đến kiểu dữ liệu của ô nhớ đó.

Con trỏ void là 1 con trỏ đặc biệt, thích trỏ đi đâu thì trỏ.

Con trỏ null : Một con trỏ được gán giá trị NULL thì được gọi là con trỏ null (giá trị NULL viết IN HOA nhé). Đây là cách tốt nhất và được khuyến cáo là nên sử dụng (good practice) trong trường hợp bạn không biết chính xác địa chỉ để biến con trỏ trỏ đến. Và thường được sử dụng để khởi tạo giá trị cho biến con trỏ ngay sau khi khai báo. Con trỏ null là một hằng số có giá trị là 0.

[code language=”c” highlight=”6″] #include <stdio.h>

int main()
{
// Khai báo và khởi tạo cho biến con trỏ
int *ptr = NULL;

// Hoặc:
// int *ptr = 0;

printf("The value of ptr is : %x\n", ptr);
// Output: The value of ptr is 0

return 0;
}
[/code]

・Trong hầu hết các hệ điều hành, các chương trình không được phép truy cập bộ nhớ tại địa chỉ 0 vì bộ nhớ đó được dành riêng bởi hệ điều hành.
Tuy nhiên, ở đây địa chỉ bộ nhớ 0 có ý nghĩa đặc biệt. Nó báo hiệu rằng con trỏ không nhằm trỏ đến một vị trí bộ nhớ, mà theo quy ước, nếu một con trỏ chứa giá trị NULL (zero), thì có nghĩa là không có gì cả.

・Để kiểm tra 1 biến con trỏ có phải là null hay không, thì bạn có thể dùng câu lệnh if như bên dưới:

if ( ptr ) {
	//...
	// Sẽ vào đây thực hiện các câu lệnh, nếu biến con trỏ là: NOT NULL
	//...
}
	
if ( !ptr ) {
	//...
	// Sẽ vào đây thực hiện các câu lệnh, nếu biến con trỏ là: NULL
	//...
}

– Trong hệ điều hành 32 bits thì con trỏ tốn 4 bytes.

Kích thước con trỏ trong hệ điều hành 32 bits

Kích thước con trỏ trong hệ điều hành 32 bits

– Trong hệ điều hành 64 bits thì con trỏ tốn 8 bytes.

Kích thước con trỏ trong hệ điều hành 64 bits

Kích thước con trỏ trong hệ điều hành 64 bits

4. Cách sử dụng con trỏ cơ bản
4. Cách sử dụng con trỏ cơ bản
[code language=”c” highlight=”8,9,49,50″] #include <stdio.h>
#include <conio.h>

int main()
{
int a = 5; // Khai báo biến a mang giá trị là 5

int *p; // Khai báo con trỏ p
p = &a; // Cho con trỏ p trỏ tới biến a

/* Ngoài ra có thể viết gộp là: int *p = &a */
/*
* Bản chất con trỏ p chính là 1 địa chỉ, &a cũng là 1 địa chỉ nên phép gán [p = &a] là OK
* (nếu gán p = a thì sẽ bị lỗi vì p là địa chỉ, mà a là giá trị)
*/

/*
* Đối với 1 BIẾN BÌNH THƯỜNG thì gồm 2 phần:
*/
printf("\n —– Thong tin cua bien binh thuong a: —–" );
//1. Giá trị: dùng đặc tả %d
printf("\n Gia tri cua bien a la: %d", a);

//2. Địa chỉ: dùng %p
printf("\n Dia chi cua bien a la: %p\n", &a);

/*
* Đối với 1 BIẾN CON TRỎ thì gồm 3 phần:
*/
printf("\n —– Thong tin cua bien con tro p: ———" );
//1. Giá trị:
// ★ Đây chính là giá trị của ô nhớ mà nó trỏ đến: Value của biến a
printf("\n Gia tri cua con tro p la: %d", *p);

//2. Địa chỉ:
printf("\n Dia chi cua con tro p la: %p", &p);

//3. Địa chỉ mà con trỏ đang trỏ tới:
// ★ Đây chính là giá trị thực của con trỏ, nó lưu: Address của biến a
printf("\n Dia chi ma con tro p dang tro toi la: %p\n", p);

/* Vì con trỏ p trỏ đến ô nhớ của biến a, nên lúc này cả 2 đã ở chung 1 nhà : D
* Do đó là khi 1 trong 2 thay đổi giá trị thì biến kia cũng thay đổi giá trị theo.
*/
// Thay đổi giá trị của biến con trỏ p
*p = 9;

printf("\n —– Gia tri sau khi thay doi: ————-" );
printf("\n Gia tri cua bien a la: %d", a);
printf("\n Gia tri cua con tro p la: %d", *p);

int *c; // Khai báo con trỏ c
c = p; // Cho con trỏ c trỏ tới con trỏ p, lúc này thì cả 3 biến a, biến con trỏ p, c cùng chung 1 ô nhớ
// do đó mà chỉ cần 1 biến thay đổi giá trị thì 2 biến kia cũng thay đổi giá trị theo.
getchar();
return 0;
}
[/code]

Kết quả

Kết quả

– Minh họa về con trỏ vô kiểu void*

[code language=”c” highlight=”16,34″] #include <stdio.h>

int main()
{
float a = 2.5; // Khai báo biến a mang giá trị là 2.5

//int *p; // Khai báo biến con trỏ p

/* Vì kiểu dữ liệu của biến a(float) và con trỏ p(int) khác nhau nên sẽ xảy ra lỗi.
* Error: a value of type "float *" cannot be assigned to an entity of type "int *"
*/
//p = &a;

// Khi đó chúng ta sẽ sử dụng con trỏ vô kiểu void*, nó có thể trỏ tới mọi kiểu dữ liệu khác,
// nhưng khi sử dụng cần phải ép kiểu về kiểu dữ liệu mà nó trỏ đến.
void *p = &a; // Khai báo con trỏ vô kiểu

/*
* Đối với 1 BIẾN BÌNH THƯỜNG thì gồm 2 phần:
*/
printf("\n —– Thong tin cua bien binh thuong a: —–" );
//1. Giá trị: dùng đặc tả %f
printf("\n Gia tri cua bien a la: %.3f", a);
//2. Địa chỉ: dùng %p
printf("\n Dia chi cua bien a la: %p\n", &a);

/*
* Đối với 1 BIẾN CON TRỎ thì gồm 3 phần:
*/
printf("\n —– Thong tin cua bien con tro p: ———" );
// Vì con trỏ p là vô kiểu nên khi sử dụng cần ép kiểu cho nó về đúng kiểu dữ liệu mà nó trỏ đến.
//printf("\n Gia tri cua con tro p la: %f", *p);
printf("\n Gia tri cua con tro p la: %0.3f", *(float *)p);
printf("\n Dia chi cua con tro p la: %p", &p);
printf("\n Dia chi ma con tro p dang tro toi la: %p\n", p);

getchar();
return 0;
}
[/code]

Kết quả

Kết quả

– Để làm quen với con trỏ vô kiểu void*, bạn thử viết 1 hàm xóa phần mở rộng của 1 tên file(chẳng hạn, input: tailieu.docx => output: tailieu hoặc input: bangtinh.xlsx => output: bangtinh), người dùng có thể truyền vào tên file có thể là kiểu char* hoặc wchar_t* đều được.

Cảm ơn bạn đã theo dõi. Đừng ngần ngại hãy cùng thảo luận với chúng tôi!

Giới thiệu

Minh Hoàng

Xin chào, tôi là Hoàng Ngọc Minh, hiện đang làm BrSE, tại công ty Toyota, Nhật Bản. Những gì tôi viết trên blog này là những trải nghiệm thực tế tôi đã đúc rút ra được trong cuộc sống, quá trình học tập và làm việc. Các bài viết được biên tập một cách chi tiết, linh hoạt để giúp bạn đọc có thể tiếp cận một cách dễ dàng nhất. Hi vọng nó sẽ có ích hoặc mang lại một góc nhìn khác cho bạn[...]

8 bình luận

Translate »