Bài này sẽ trình bày sâu hơn về con trỏ trong ngôn ngữ lập trình C:
1. Kích thước con trỏ
Lưu ý: Hàm
Xét ví dụ sau:
Nếu sử dụng visual studio và chọn x86:
Nếu sử dụng visual studio và chọn x64:
Từ ví dụ trên, ta rút ra một kết luận rằng kích thước biến con trỏ không phụ thuộc vào kiểu dữ liệu mà nó trỏ tới.
Nó phụ thuộc vào số đường địa chỉ cần để xác định một ô nhớ (bytes) trong bộ nhớ.
Trong ví dụ trên, nếu ta build chương trình x86 (32-bit) thì để xác định một ô nhớ cần 32 đường địa chỉ => kích thước biến con trỏ là 4 byte. Nếu ta build loại x64 (64-bit) thì để xác định một ô nhớ cần 64 đường địa chỉ => kích thước biến con trỏ là 8 byte.
Tương tự trong VĐK STM32, bộ nhớ RAM STM32 có 32 đường địa chỉ, do đó, bất kì biến con trỏ nào trong STM32 cũng có size là 32 bit hay 4 byte.
2. Phép toán cộng trừ với con trỏ
Giả sử ta có biến con trỏ
Thực chất complier sẽ tự hiểu:
Để hiểu rõ, ta xem xét ví dụ sau:
Giải thích:
Ép kiểu biến con trỏ là thay đổi kiểu dữ liệu mà con trỏ trỏ tới.
Thông thường ta không nên ép kiểu con trỏ vì điều này gây thêm sự phức tạp cho code.
Tuy nhiên trong một số trường hợp, việc ép kiểu biến con trỏ là cần thiết khi ta muốn truyền một kiểu dữ liệu bất kì vào hàm số.
Do đó ta cần hiểu về việc ép kiểu dữ liệu của biến con trỏ.
Xét ví dụ sau:
Giải thích:
Biến
Con trỏ chỉ có thể lưu địa chỉ của một byte.
Nếu dữ liệu mà con trỏ trỏ tới lớn hơn 1 byte thì con trỏ sẽ lưu vị trí byte đầu tiên của dữ liệu.
Compile sẽ dựa vào kiểu dữ liệu mà con trỏ trỏ tới để thực hiện toán tử * (lấy giá trị biến).
Ta có
4. Con trỏ và mảng
Cú pháp khai báo:
Ta cũng có thể vừa khai báo, vừa khởi tạo mảng
Truy xuất phần tử trong mảng bằng cách đặt chỉ số vị trí của phần tử (bắt đầu từ 0) trong dấu ngoặc vuông ngay sau tên mảng.
Ta có thể sử dụng tên của mảng như một biến con trỏ.
Hãy xem xét ví dụ sau:
Giải thích :
Khi ta khai báo mảng, một vùng nhớ liên tiếp trong bộ nhớ có kích thước là sizeof(<kiểu dữ liệu của phần tử lưu trong mảng>) * <kích thước mảng> bytes. Lúc này, tên mảng
Vì ages là biến con trỏ trỏ tới kiểu dữ liệu int nên để lấy dữ liệu lưu ở đia chỉ mà con trỏ trỏ tới, ta dùng toán tử *.
Ta có nhận xét rằng: địa chỉ của phần tử đầu tiên của mảng chính là địa chỉ của mảng.
Để hiểu rõ hơn, ta xem xét ví dụ sau:
Bạn hãy dự đoán kết quả trước khi spoil nhé
Giải thích :
5. Con trỏ và struct
Để truy xuất phần tử của struct bằng con trỏ ta dùng ký hiệu
Ví dụ:
Bài trước: [C] Variable Pointer (Con trỏ biến) - P1
Bài tiếp: [C] Function Pointer (Con trỏ hàm) - P1
1. Kích thước con trỏ
Lưu ý: Hàm
sizeof()
là hàm lấy kích thước của một đối tượng tính theo byte.Xét ví dụ sau:
C:
#include <stdio.h>
#include <stdint.h>
char var0 = 19;
int var1 = 1000;
float var2 = 10.2;
double var3 = 12.34;
int main(){
char *var0_ptr = &var0;
printf("size of var0_ptr = %ld bytes\n", sizeof(var0_ptr));
int *var1_ptr = &var1;
printf("size of var1_ptr = %ld bytes\n", sizeof(var1_ptr));
float *var2_ptr = &var2;
printf("size of var2_ptr = %ld bytes\n", sizeof(var2_ptr));
double *var3_ptr = &var3;
printf("size of var3_ptr = %ld bytes\n", sizeof(var3_ptr));
return 0;
}
size of var0_ptr = 4 bytes
size of var1_ptr = 4 bytes
size of var2_ptr = 4 bytes
size of var3_ptr = 4 bytes
size of var1_ptr = 4 bytes
size of var2_ptr = 4 bytes
size of var3_ptr = 4 bytes
size of var0_ptr = 8 bytes
size of var1_ptr = 8 bytes
size of var2_ptr = 8 bytes
size of var3_ptr = 8 bytes
size of var1_ptr = 8 bytes
size of var2_ptr = 8 bytes
size of var3_ptr = 8 bytes
Nó phụ thuộc vào số đường địa chỉ cần để xác định một ô nhớ (bytes) trong bộ nhớ.
Trong ví dụ trên, nếu ta build chương trình x86 (32-bit) thì để xác định một ô nhớ cần 32 đường địa chỉ => kích thước biến con trỏ là 4 byte. Nếu ta build loại x64 (64-bit) thì để xác định một ô nhớ cần 64 đường địa chỉ => kích thước biến con trỏ là 8 byte.
Tương tự trong VĐK STM32, bộ nhớ RAM STM32 có 32 đường địa chỉ, do đó, bất kì biến con trỏ nào trong STM32 cũng có size là 32 bit hay 4 byte.
2. Phép toán cộng trừ với con trỏ
Giả sử ta có biến con trỏ
var_ptr
, khi ta thực hiện phép cộng:var_ptr = var_ptr + offset;
<giá trị của mới var_ptr> = <giá trị cũ của var_ptr> + offset * sizeof( <kiểu dữ liệu mà pointer trỏ tới> )
Phép trừ tương tự phép cộng.Để hiểu rõ, ta xem xét ví dụ sau:
C:
#include <stdio.h>
#include <stdint.h>
char var0 = 10;
int var1 = 100;
int main() {
char* var0_ptr = &var0;
printf("var0_ptr = %d\n", (unsigned int)var0_ptr);
var0_ptr++;
printf("var0_ptr = %d \n", (unsigned int)var0_ptr);
var0_ptr += 2;
printf("var0_ptr = %d \n", (unsigned int)var0_ptr);
printf("\n");
int* var1_ptr = &var1;
printf("var1_ptr = %d\n", (unsigned int)var1_ptr);
var1_ptr++;
printf("var1_ptr = %d \n", (unsigned int)var1_ptr);
var1_ptr += 2;
printf("var1_ptr = %d \n", (unsigned int)var1_ptr);
return 0;
}
var0_ptr = 15179832
var0_ptr = 15179833
var0_ptr = 15179835
var1_ptr = 15179836
var1_ptr = 15179840
var1_ptr = 15179848
var0_ptr = 15179833
var0_ptr = 15179835
var1_ptr = 15179836
var1_ptr = 15179840
var1_ptr = 15179848
var0_ptr
trỏ tới kiểu dữ liệuchar
. Vì biếnchar
luôn có size 1 byte nênvar0_ptr = 15179832
,var0_ptr+1 = 15179832 + 1*1 =15179833
. Sau đóvar0_ptr+2 = 15179833 + 2*1 = 15179835
.var1_ptr
trỏ tới kiểu dữ liệuint
. Vì chọn build x86 (32-bit) nên biến int có size bằng 4 byte. Khi đóvar1_ptr = 15179836
,var1_ptr+1 = 15179836+ 1*4 =15179840
. Sau đóvar1_ptr+2 = 15179840+ 2*4 = 15179848
.
Ép kiểu biến con trỏ là thay đổi kiểu dữ liệu mà con trỏ trỏ tới.
Thông thường ta không nên ép kiểu con trỏ vì điều này gây thêm sự phức tạp cho code.
Tuy nhiên trong một số trường hợp, việc ép kiểu biến con trỏ là cần thiết khi ta muốn truyền một kiểu dữ liệu bất kì vào hàm số.
Do đó ta cần hiểu về việc ép kiểu dữ liệu của biến con trỏ.
Xét ví dụ sau:
C:
#include <stdio.h>
#include <stdint.h>
int var = 0x12345678;
int main() {
char *var_ptr = (char*)&var;
printf("var address = 0x%lX \n", var_ptr);
printf("1st (least significant) byte = 0x%x\n", *var_ptr);
var_ptr++;
printf("2rd byte = 0x%x\n", *var_ptr);
var_ptr++;
printf("3rd byte = 0x%x\n", *var_ptr);
var_ptr++;
printf("4rd (most significant) byte = 0x%x\n", *var_ptr);
return 0;
}
var address = 0x5AA038
1st (least significant) byte = 0x78
2rd byte = 0x56
3rd byte = 0x34
4rd (most significant) byte = 0x12
1st (least significant) byte = 0x78
2rd byte = 0x56
3rd byte = 0x34
4rd (most significant) byte = 0x12
Biến
var
được lưu trong bộ nhớ như sau:Address | 0x5AA038 | 0x5AA039 | 0x5AA03A | 0x5aa03B |
---|---|---|---|---|
Value | 0x78 | 0x56 | 0x34 | 0x12 |
Nếu dữ liệu mà con trỏ trỏ tới lớn hơn 1 byte thì con trỏ sẽ lưu vị trí byte đầu tiên của dữ liệu.
Compile sẽ dựa vào kiểu dữ liệu mà con trỏ trỏ tới để thực hiện toán tử * (lấy giá trị biến).
Ta có
&var
có giá trị là 0x5AA038
và có kiểu dữ liệu là (int*)
. Ta ép thành kiể (char*)
để lưu vào biến var_ptr
4. Con trỏ và mảng
Cú pháp khai báo:
<kiểu dữ liệu của phần tử lưu trong mảng> <tên mảng>[<kích thước mảng>];
Ví dụ:
C:
int ages[5];
double height[5];
C:
double height[5] = {1.68, 1.72, 1.77, 1.69, 1.9};
C:
double tom = height[0]; // = 1.68
double jack = height[1]; // = 1.72
height[2] = 1.76;
Hãy xem xét ví dụ sau:
C:
#include <stdio.h>
#include <stdint.h>
int ages[5] = { 10, 11, 12,13, 14 };
int main() {
printf("Array address: 0x%lx\n", ages);
printf("First age: %d\n", *ages);
return 0;
}
Array address: 0x99a038
First age: 10
First age: 10
Khi ta khai báo mảng, một vùng nhớ liên tiếp trong bộ nhớ có kích thước là sizeof(<kiểu dữ liệu của phần tử lưu trong mảng>) * <kích thước mảng> bytes. Lúc này, tên mảng
ages
có thể được xem là một con trỏ, trỏ tới địa chỉ của mảng.Vì ages là biến con trỏ trỏ tới kiểu dữ liệu int nên để lấy dữ liệu lưu ở đia chỉ mà con trỏ trỏ tới, ta dùng toán tử *.
Ta có nhận xét rằng: địa chỉ của phần tử đầu tiên của mảng chính là địa chỉ của mảng.
Để hiểu rõ hơn, ta xem xét ví dụ sau:
C:
#include <stdio.h>
#include <stdint.h>
int ages[5] = {10, 11, 12, 13, 14};
int main(){
int *ages_ptr = ages;
printf("First age %d\n", *ages_ptr);
ages_ptr++;
printf("Second age %d\n", *ages_ptr);
ages_ptr = ages_ptr + 2;
printf("Ages = %d\n", *ages_ptr);
return 0;
}
First age 10
Second age 11
Ages = 13
Second age 11
Ages = 13
int *ages_ptr = ages;
Ta khai báo biến con trỏ ages_ptr
và gán giá trị cho nó bằng địa chỉ của mảng. printf("First age %d\n", *ages_ptr);
Ta in ra phần tử đầu tiên của mảng ages_ptr++;
Lưu ý đây là phép cộng địa chỉ nên complier sẽ hiểu như sau ages_ptr = ages_ptr + sizeof(<int>)*1 printf("Second age %d\n", *ages_ptr);
Lúc này ages_ptr
trỏ tới phần tử thứ 2 của mảng ages_ptr = ages_ptr + 2;
Lưu ý đây là phép cộng địa chỉ nên complier sẽ hiểu như sau ages_ptr = ages_ptr + sizeof(<int>)*2 printf("Ages = %d\n", *ages_ptr);
Lúc này ages_ptr
trỏ tới phần tử thứ 4 của mảng5. Con trỏ và struct
Để truy xuất phần tử của struct bằng con trỏ ta dùng ký hiệu
->
Ví dụ:
C:
#include <stdio.h>
#include <stdint.h>
typedef struct {
float width;
float height;
}rect_t;
float area(rect_t rect) {
return rect.height * rect.width;
}
float area_ptr(rect_t* rect) {
return rect->height * rect->width;
}
int main() {
rect_t rect;
rect.width = 1.2;
rect.height = 3.4;
float a = area(rect);
float b = area_ptr(&rect);
printf("Area %f = %f ", a, b);
return 0;
}
Area 4.080000 = 4.080000
Bài trước: [C] Variable Pointer (Con trỏ biến) - P1
Bài tiếp: [C] Function Pointer (Con trỏ hàm) - P1