[C] Variable Pointer (Con trỏ biến) - P2

Ngô Văn Tuân

Gà con
Staff member
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 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;
}
Nếu sử dụng visual studio và chọn x86:
x86.PNG

size of var0_ptr = 4 bytes
size of var1_ptr = 4 bytes
size of var2_ptr = 4 bytes
size of var3_ptr = 4 bytes
Nếu sử dụng visual studio và chọn x64:
x64.PNG

size of var0_ptr = 8 bytes
size of var1_ptr = 8 bytes
size of var2_ptr = 8 bytes
size of var3_ptr = 8 bytes
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ỏ var_ptr, khi ta thực hiện phép cộng:
var_ptr = var_ptr + offset;
Thực chất complier sẽ tự hiểu:
<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
Giải thích:
  • var0_ptr trỏ tới kiểu dữ liệu char. Vì biến char luôn có size 1 byte nên var0_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ệu int. 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.
3. Ép kiểu con trỏ
É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
Giải thích:
Biến var được lưu trong bộ nhớ như sau:
Address0x5AA0380x5AA0390x5AA03A0x5aa03B
Value​
0x78​
0x56​
0x34​
0x12​
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ó &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];
Ta cũng có thể vừa khai báo, vừa khởi tạo mảng
C:
double height[5] = {1.68, 1.72, 1.77, 1.69, 1.9};
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.
C:
double tom = height[0];  // = 1.68
double jack = height[1]; // = 1.72
height[2] = 1.76;
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:
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
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 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;
}
Bạn hãy dự đoán kết quả trước khi spoil nhé :batting eyelashes:
First age 10
Second age 11
Ages = 13
Giải thích :
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ảng

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ụ:

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
 
Top