C/C++

Pointers and Arrays in C

Xin chào, trong bài viết này chúng ta sẽ tìm hiểu về Pointer (con trỏ) của ngôn ngữ C và mối quan hệ giữa Pointer và Array.


POINTER LÀ GÌ?

Pointer (hay còn gọi là con trỏ) là một kiểu dữ liệu của ngôn ngữ C (và cả C++) được dùng để lưu địa chỉ của các biến hoặc vùng nhớ. Việc sử dụng pointer một cách hợp lý và đúng đắn sẽ giúp cho chương trình trở nên rất gọn gàng và hiệu quả, tuy nhiên, vì pointer khá trừu tượng nên cũng rất dễ mắc lỗi khi sử dụng.

Vì pointer chứa địa chỉ của vùng nhớ nên kích thước của nó phụ thuộc vào môi trường thực thi, có thể là 4 bytes hoặc 8 bytes. Con số này thực sự rất nhỏ so với một vùng nhớ có kích thước hàng nghìn bytes, do đó, sử dụng pointer để chia sẻ dữ liệu giữa các functions sẽ rất hiệu quả.

Để khai báo một pointer, chúng ta cần cung cấp kiểu dữ liệu của vùng nhớ mà nó sẽ trỏ đến, ví dụ: int *ptr; (pointer ptr trỏ đến vùng nhớ của một biến integer). Nếu kiểu dữ liệu của pointer khác với của vùng nhớ mà nó trỏ đến thì khi sử dụng pointer để truy cập vào vùng nhớ, chắc chắn dữ liệu nhận được sẽ không như ý muốn, xem ví dụ 1.

Ví dụ 1:

#include <stdio.h>
#include <stdint.h>

int main(int argc, char **argv) {
	uint8_t array[4] = {0,1,2,3};
	uint16_t *ptr = a;

	printf("array[0] = %d\n", *ptr);
	printf("array[1] = %d\n", *(ptr+1));
}

Giải thích:
– Dòng 6: khai báo mảng array chứa 4 phần tử có kiểu dữ liệu uint8_t (1 byte);
– Dòng 7: khai báo pointer ptr trỏ đến vùng nhớ có kiểu dữ liệu uint16_t (2 bytes);
– Dòng 9: truy cập vào phần tử đầu tiên của mảng array với pointer ptr.

Kết quả:
array[0] = 256
array[1] = 770

Tại sao lại là 256 ? Tại sao không phải là 0 ???

Trả lời: vì pointer ptr có kiểu dữ liệu uint16_t nên mỗi phần tử của vùng nhớ mà nó trỏ đến sẽ có kích thước 2 bytes. Trong khi đó mảng array có kiểu dữ liệu uint8_t, vì thế khi ptr trỏ đến phần tử đầu tiên, nó sẽ bao gồm luôn 2 phần tử đầu tiên của array, tức là 0x0100 (ở hệ HEX, tương đương 256 ở hệ DEC), hình 1.

Hình 1

CẤP PHÁT VÀ GIẢI PHÓNG BỘ NHỚ

Để cấp phát bộ nhớ cho pointer, ngôn ngữ C cung cấp 2 functions là calloc() và malloc(); để giải phóng bộ nhớ mà pointer đã được cấp phát, sử dụng function free().

Chú ý: vùng nhớ được cấp phát cho pointer sẽ tồn tại cho đến khi được giải phóng bởi function free(), kể cả khi pointer là một local variable.

Functions:

void *malloc(size_t n)
– trả về pointer đến vùng nhớ chứa n bytes chưa được khởi tạo, hoặc NULL nếu không thể cấp phát.

void *calloc(size_t n, size_t size)
– trả về pointer đến vùng nhớ (đã được khởi tạo về 0) đủ để chứa n phần tử có kích thước size, hoặc NULL nếu không thể cấp phát.

void free(p)
– giải phóng vùng nhớ đã được cấp phát cho pointer p bởi function malloc hoặc calloc.

Ví dụ 2:

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char **argv) {
	size_t p1size = 10;
	size_t p2size = 5;

	// cấp phát bộ nhớ cho pointer p1
	int *p1 = (int*)calloc(p1size, sizeof(int));

	// cấp phát bộ nhớ cho pointer p2
	int **p2 = (int**)calloc(p2size, sizeof(int*));

	for (size_t i = 0; i < p2size; i++) {
		p2[i] = (int*)calloc(2, sizeof(int));
	}

	// giải phóng bộ nhớ đã cấp phát cho pointer p1
	free(p1);

	// giải phóng bộ nhớ đã cấp phát cho pointer p2
	for (size_t i = 0; i < p2size; i++)
		free(p2[i]);
	free(p2);
}

CÁC PHÉP TOÁN DÙNG CHO ĐỊA CHỈ

Vì pointer đơn giản là một kiểu dữ liệu nên chúng ta có thể thực hiện các phép toán cộng trừ đại số giữa pointer integer. Ví dụ: với pointer p, phép toán p++ sẽ đưa pointer p đến phần tử kế tiếp trong vùng nhớ, phép toán p-- thì ngược lại, trong khi đó phép toán p += 2 sẽ đưa p đến phần tử thứ 2 ngay sau vị trí hiện tại.

Ngoài ra chúng ta có thể thực hiện phép toán trừ và so sánh giữa 2 pointers trỏ đến 2 phần tử của cùng một vùng nhớ, cũng như gán và so sánh pointer với số không (NULL). Phép gán pointer cho một pointer khác phải được thực hiện trên cùng một kiểu dữ liệu.

Ngoài các phép toán đã nêu trên, các phép toán khác (ví dụ như cộng, nhân, chia,… giữa 2 pointers) đều không hợp lệ.


POINTER TO CONST VÀ CONST POINTER

  • Pointer to const: pointer p trỏ đến một vùng nhớ mà giá trị của vùng nhớ đó không thể thay đổi bởi pointer p, nhưng có thể thay đổi bởi một pointer khác;
  • Const pointer: pointer có giá trị không thể thay đổi, tức là không thể gán tới một địa chỉ khác.

Ví dụ:

#include <stdio.h>

void main() {
	int array[5] = {1,2,3,4,5};

	int *ptr = array;
	// pointer to const
	int const *ptrToConst = array;
	// const pointer
	int * const constPtr = array;

	for (size_t i = 0; i < 5; i++)
		ptr[i] = i;

		// error!
		// ptrToConst[i] = i;
		
	// error!
	//constPtr++;
}

POINTER TO FUNCTION

Mặc dù function không phải là một biến (variable) nhưng bản thân nó được lưu trữ tại một vùng nhớ, do đó chúng ta có thể sử dụng pointer để trỏ đến vùng nhớ đó. Điều thú vị là tên của một function cũng chính là địa chỉ của vùng nhớ chứa nó.

Khai báo:

returnType (*pointerName)(argDataTypes)

Ví dụ:

#include <stdio.h>

int sum(int a, int b) {
    return a+b;
}
 
void main() {
    int (*ptrToFunc)(int, int) = sum;

    printf("sum: %d\n", ptrToFunc(10,20));
}

Giải thích:
– Dòng 8: khai báo pointer đến function sum;
– Dòng 9: gọi function với pointer ptrToFunc.


POINTER VÀ ARRAY

Tồn tại một mối quan hệ khăng khít giữa pointer và array, điều này dẫn đến sự hiểu lầm rằng “array cũng chính là pointer!”. Chúng ta sẽ lần lượt tìm hiểu sự tương đồng và khác biệt giữa pointer và array.

+ TƯƠNG ĐỒNG:

  • Các thao tác truy cập phần tử của array đều có thể được thực hiện bởi cú pháp của pointer và ngược lại.

Ví dụ:

int array[10] = {0,1,2,3,4,5,6,7,8,9};
int *ptr = array;
int a = array[0];
int b = *(array+1);
int c = *(ptr+2);
int d = ptr[3];
  • Khi được sử dụng làm đối số của một function, hoặc trong function header, array và pointer là tương đương.

Ví dụ:

int sumA(const int *a) {
    // nothing
    return 0;
}

int sumB(const int a[]) {
    // nothing
    return 0;
}

void main() {
    int array[10] = {0,1,2,3,4,5,6,7,8,9};
    int A = sumA(array);
    int B = sumB(&array[0]);
}

+ KHÁC BIỆT:
Chú ý: mình dùng “array name” để chỉ tên của một mảng.

  • pointer là một biến (variable), còn array name thì không; array name chỉ biểu diễn địa chỉ của phần tử đầu tiên của mảng, do đó các phép toán đại số làm thay đổi giá trị của array name là KHÔNG HỢP LỆ;
  • khi định nghĩa một pointer, chỉ có bộ nhớ dùng để chứa bản thân pointer được cấp phát, vùng nhớ mà nó sẽ trỏ đến thì vẫn chưa (trừ khi nó được gán cho một literal string). Trong khi đó, định nghĩa một array sẽ cấp phát bộ nhớ mà array đó yêu cầu;
  • kích thước của vùng nhớ được cấp phát cho pointer có thể được thay đổi (cấp phát động bởi function calloc() hoặc malloc()) và cần phải được giải phóng bởi function free(). Trong khi đó, vùng nhớ được cấp phát cho array có kích thước cố định và tự động được giải phóng khi ra khỏi scope của nó. Bên cạnh đó, địa chỉ mà array name biểu diễn KHÔNG thể thay đổi;
  • đối với pointer array (mảng chứa các pointers), vùng nhớ được cấp phát cho mỗi pointer có thể nằm rải rác trong bộ nhớ. Tuy nhiên, đối với multi-dimensional array (mảng nhiều chiều), vùng nhớ được cấp phát nằm liên tục trong bộ nhớ máy tính.

array hay &array

#include <stdio.h>

void main() {
    int row = 4;
    int col = 3;
    char array[row][col];

    char *pa = &array[0][0];
    printf("+ pa:\n");
    for (size_t i = 0; i < row*col; i++)
    	printf("%p\n", pa + i);
    printf("\n");

    char (*pb)[col] = &array[0];
    printf("+ pb:\n");
    for (size_t i = 0; i < row; i++)
    	printf("%p\n", pb + i);
    printf("\n");
}

Giải thích:
– Dòng 6: định nghĩa mảng array gồm 12 phần tử;
– Dòng 8: khởi tạo pointer pa trỏ đến phần tử đầu tiên của mảng array;
– Dòng 13: khởi tạo pointer pb trỏ đến cả vùng nhớ chứa hàng đầu tiên của mảng array.

Kết quả:
+ pa:
0028FEB8
0028FEB9
0028FEBA
0028FEBB
0028FEBC
0028FEBD
0028FEBE
0028FEBF
0028FEC0
0028FEC1
0028FEC2
0028FEC3

+ pb:
0028FEB8
0028FEBB
0028FEBE
0028FEC1

Với kết quả nhận được như trên, chúng ta có thể nhận thấy rằng, tất cả các phần tử của mảng 2 chiều array đều nằm trên một vùng nhớ liên tục. Mặc dù giá trị của pa và pb là giống nhau, nhưng pa+1 và pb+1 lại hoàn toàn khác nhau, đó là do pa trỏ đến từng phần tử của array còn pb trỏ đến từng hàng của array.


Reference:
[1] Chapter 4. Expert C programming – Deep C secrets.
[2] Chapter 9. Expert C programming – Deep C secrets.
[3] Chapter 10. Expert C programming – Deep C secrets.
[4] Function Pointer in C.
[5] Programs as Data: Function Pointers.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s