Bài 6 : Con Trỏ Trong C (Pointers)

2523

Như vậy chúng ta đã đi qua những bài cơ bản đầu tiên của lập trình c bao gồm :

  1.  Giới thiệu lập trình c
  2. Các cấu trúc điều khiển phần 1phần 2
  3. HàmHàm đệ quy
  4. Mảng

Hôm nay chúng ta sẽ tiếp tục tìm hiểu về lập trình c qua những bài học nâng cao hơn :

 

Con Trỏ Trong C (Pointers)

  •  Định nghĩa và khai báo
  • Truyền tham số cho hàm sử dụng con trỏ„ trong c
  • Các phép toán với con trỏ trong c
  • „ Mối quan hệ giữa con trỏ và mảng
  • „ Mảng con trỏ trong c

 

Định nghĩa con trỏ trong c

  • Biến con trỏ là biến chứa địa chỉ của ô nhớ
  • Biến thường: chứa một giá trị cụ thể
  •  Biến con trỏ: chứa địa chỉ của một biến mà biến đó có giá trị cụ thể

bien-con-tro

Khai báo biến con trỏ trong c

  • Thêm kí tự “*” vào trước tên biến
    • Ví dụ: int *a; => a là con trỏ kiểu int
  • Khai báo nhiều biến con trỏ cùng kiểu: trước mỗi biến đều thêm một dấu “*”.
    • „ Ví dụ: double *a, *b; => a, b là hai con trỏ kiểu double
  • „ Có thể khai báo kiểu con trỏ cho mọi loại dữ liệu
  • „ Khởi tạo biến con trỏ: gán về 0 hoặc NULL
    • „ Nghĩa là con trỏ không trỏ vào đâu cả
    •  Gán về 0 chỉ với những con trỏ số nguyên
    • „ Thường thì gán về NULL được dùng nhiều hơn
    • „ NULL được định nghĩa trong <stddef.h> và <stdio.h>

Toán tử ” & ” và ” *

  • Toán tử &: lấy địa chỉ của một biến
    • Xét ví dụ

int y = 5;
int *yPtr;
yPtr = &y; /* yPtr nhận địa chỉ của y “yPtr trỏ tới y” */

toan-tu-yptr

  • Toán tử “*”: trả về giá trị chứa trong vùng nhớ mà biến con trỏ trỏ tới.
    • *yPtr = y => vì yPtr trỏ tới y (theo ví dụ trên)
    • Sử dụng “*” để gán giá trị: *yPtr = 10; => giá trị của y = 10
  • Toán tử “&” và “*” là đảo ngược của nhau

Ví dụ sử dụng toán tử “*” và “&

#include<stdio.h>
int main(void)
{
   int a; /* a là một biến int */
   int *aPtr; /* aPtr là một con trỏ int */
   a = 7;
   aPtr = &a; /* aPtr trỏ vào a */
   printf("The address of a is %p \nThe value of aPtr is &p", &a, aPtr");
   printf("\n\nThe value of a is %d\nThe value of *aPtr is %d", a, *aPtr);
   printf("\n\nShowing that * and & are complements of each other \n&*aPtr = %p\"
        "\n*&aPtr = %p\n", &*aPtr, *&aPtr);
   return 0;
}

vi-du-toan-tu

Truyền tham số cho hàm sử dụng con trỏ trong c

  • Dùng con trỏ để truyền tham biến biến cho hàm
    • Bản chất là truyền địa chỉ của biến cho tham số của hàm
    • Khai báo: thêm toán tử “*” vào trước tên tham biến của hàm
    • Khi gọi hàm có tham biến là con trỏ, thì phải truyền cho tham biến này là một địa chỉ, sử dụng toán tử &
  • Ví dụ

void timestwo( int *number) /* tham biến là con trỏ */
{
    *number =  2 * ( *number);
}

  • Gọi hàm:

int x = 5;
twotime(&x); /* truyền địa chỉ của biến x vào cho hàm */

Ví dụ 1 : Tính lập phương – Truyền tham số trị cho hàm –

#include<stdio.h>
int cubeByValue( int n); /* prototype */
int main( void )
{
   int number = 5; /* khởi tạo biến number */
   printf("The original value of number is %d", number);
   /* truyền giá trị của number vào hàm cubeByValue */
   number = cubeByValue(number);/* truyền tham biến trị */
   printf("\nThe new value of number is %d\n", number);
   return 0;
}
/* hàm tính lập phương */
int cubeByValue( int n)
{
   return n * n * n; /* trả về giá trị là lập phương của n */
}

tinh-lap-phuong

Ví dụ 2 :Tính lập phương – Truyền tham biến sử dụng con trỏ-

#include<stdio.h>
void cubeByReference( int *nPtr ); /* prototype */
int main( void )
{
   int number = 5; /* khởi tạo biến number */
   printf( "The original value of number is %d", number);
   /* truyền tham số sử dụng con trỏ */
   cubeByreference( &number );
   printf("\nThe new value of number is %d\n", number);
   return 0;
}
/* hàm tính lập phương của con trỏ nPtr */
void cubeByReference( int *nPtr)
{
   *nPtr = *nPtr * *nPtr * *nPtr; /* cube *nPtr */
}

tinh-lap-phuong

Khai báo hằng(const) với con trỏ trong c

  • Có thể sử dụng const để khai báo các hằng con trỏ
  • Có 4 cấp độ
    • Con trỏ thường, dữ liệu thường, int *myPtr; => Có thể thay đổi được con trỏ và dữ liệu
    • Con trỏ thường, dữ liệu hằng, const int *myPtr = 10; => Chỉ có thể thay đổi được con trỏ, không thay đổi được dữ liệu
      • myPtr = &x; // OK     *myPtr = x; // Error
    • Con trỏ hằng, dữ liệu thường, int *const myPtr = &x; => Chỉ có thể thay đổi được dữ liệu, không thay đổi được con trỏ
      • *myPtr = x; // OK     myPtr = &x; // Error
    • Con trỏ hằng, dữ liệu hằng, const int *const myPtr = &x; => Không được thay đổi cả con trỏ lẫn dữ liệu

Ví dụ : In một dòng chữ

#include<stdio.h>
void printCharacters( const char *sPtr);
int main(void)
{
   /*  khởi tạo mảng kí tự */
   char string[] = "HA NOI MUA TRO GIO";
   printf("The string is: \n");
   printCharacters( string );
   printf("\n");
   return 0;
} /* kết thúc hàm main */
/* hàm printCharacters với con trỏ trỏ vào dữ liệu hằng */
void printCharacters( const char *sPtr) /* sPtr : con trỏ trỏ vào dữ liệu hằng */
{
   /* lập cho đến hết xâu */
   for (; *sPtr != '\0'; sPtr++) { /* for: không có khởi đầu */
   printf(" %c ", *sPtr ); /* in kí tự hiện tại ra màn hình */
   }
} /* kết thúc hàm printCharacters

in-chu

Các phép toán với con trỏ trong c

  • Phép cộng: khi cộng thêm 1 vào con trỏ thì con trỏ sẽ trỏ vào vùng nhớ được cộng thêm x bytes (x – là số bytes của kiểu dữ liệu hiện thời của con trỏ)
  • Ví dụ: int v[10]; int *vPtr; vPtr = v[0]; và Ptr trỏ vào ô nhớ 3000 thì Ptr += 2; => Ptr trỏ vào ô nhớ3008

toan-con-tro

  • „ Phép trừ: tương tự phép cộng
  • Không có phép nhân/chia với con trỏ
  • Có thể sử dụng phép tăng giảm: vPtr++; ++vPtr;
  • Hai con trỏ cùng kiểu thì có thể gán cho nhau được
  • Có các phép toán <,>,==, đối với 2 con trỏ
  • Hàm sizeof(x); => trả về độ lớn của đối tượng x, x có thể là :
    • Một biến
    • Một kiểu dữ liệu
    • Một mảng

Ví dụ:
int a;
sizeof(a) = 4
sizeof(double) = 8
int A[4];
sizeof(A) = 16

Mối quan hệ của mảng và con trỏ trong c

  • Mảng và con trỏ có quan hệ mật thiết
    • Tên của mảng chính là 1 hằng con trỏ, trỏ vào phần tử đầu tiên của mảng
    • Có thể sử dụng con trỏ để thay cho chỉ số của mảng
  • Ví dụ: int a[100]; thì:
    • a chính là &a[0] và *a chính là a[0]
    • (a + i) chính là &a[i] và *(a+i) chính là a[i]
  • Có thể sử dụng con trỏ để truy cập vào các phần tử của mảng.

mang-con-tro

#include<stdio.h>
int main (void)
{
   int b[] = { 10, 20, 30, 40}; /* khởi tạo mảng b */
   int *bPtr = b; /* bPtr trỏ vào b */
   int i, offset ;
   /* đưa ra mảng b: theo cách thông thường */
   printf("Array b printed with :\nArray subscript notation \n");
   for ( i = 0;i < 4; i++) {
     printf( "b[ %d ] = %d\n", i, b[i]);
   }
   /* đưa ra mảng b theo cách sử dụng con trỏ */
   printf("\nPointer /offset notation where \nThe pointer is the array name\n");
   for ( offset = 0; offset < 4; offset++) {
     printf("*(b + %d) = %d\n", offset, *(b + offset ) );
   }
   /* đưa ra mảng b thông qua con trỏ bPtr, bằng chỉ số */
   printf("\nPointer subscript notation\n");
   for (i = 0; i < 4; i++) {
     printf("bPtr[%d] = %d\n", i , bPtr[ i ]);
   }
   /* đưa ra mảng b thông qua con tro bPtr theo kiểu con trỏ */
   printf("\nPointer /offset notation \n");
   for (offset = 0; offset < 4; offset++) {
     printf (" *(bPtr + %d) = %d\n", offset, *( bPtr + offset ) );
   }
  return 0;
}

quan-he-mang-pointer

Con trỏ trong c và mảng 1 chiều

Như chúng ta đã biết qua bài Mảng thì chúng ta có thể coi biến mảng như một con trỏ, vì vậy ta có thể sử dụng chính biến mảng đó để truy cập mảng theo cách của con trỏ.

#include <stdio.h>
  
void nhapMang(int a[], int n) {
    int i;
    for (i = 0; i < n; i++) {
        printf("Nhap a[%d] = ", i);
        scanf("%d", &a[i]);
    }
}
 
void nhapContro(int a[], int n){
    int i;
    for (i = 0; i < n; i++) {
        printf("Nhap a[%d] = ", i);
        scanf("%d", a + i);
    }
}
  
void xuatMang(int a[], int n) {
    int i;
    for (i = 0; i < n; i++) {
        printf ("%d \t", a[i]);
    }
}
  
int main() {
    /* khai báo mảng a có n phần tử */
    int n = 5;
    int a[n];
    nhapContro(a, n);
    xuatMang(a, n);
      
    return 0;
}

Nhập mảng trong hàm

Việc nhập mảng không phải lúc nào cũng thuận lợi vì như các ví dụ trước chúng ta cần phải có số lượng phần tử của mảng trước khi cấp phát hoặc nhập, vậy nếu chúng ta chưa biết trước số phần tử mà lại phải dùng hàm để nhập mảng thì sao. Các bạn xem ví dụ sau.

#include <stdio.h>
#include <stdlib.h>
  
void nhapContro(int *(*a), int *n) {
    int i;
 
    printf("Nhap so phan tu cua mang: ");
    scanf("%d", n); // khong phai &n
    *a = (int *) malloc ((*n) * sizeof(int));
    /* *a : lấy địa chỉ của mảng a chứ không phải giá trị của a */
     
    for (i = 0; i < *n; i++) {
        printf("Nhap a[%d] = ", i);
        scanf("%d", (*a + i));
    }
}
  
void xuatMang(int *a, int n) {
    int i;
    for (i = 0; i < n; i++) {
        printf ("%d \t", a[i]);
    }
}
  
int main() {
    int *a, n;
     
    nhapContro(&a, &n); /* lấy địa chỉ của a và n */
    xuatMang(a, n);
      
    return 0;
}

Trong VD này ta thực hiện nhập và xuất mảng trong hàm, cấp phát bộ nhớ cũng trong hàm luôn. Có 1 điểm chú ý là trong hàm nhập mảng a bằng con trỏ trong c thì có 2 dấu *. 1 dấu là của mảng a (dấu thứ 2), còn dấu đầu tiên là dùng để truyền địa chỉ làm giá trị của mảng có thể giữ nguyên khi ra khỏi hàm, nó giống như là dấu * trong hàm HoanVi(int *a,int *b) vậy.

Cấp phát và thu hồi vùng nhớ của biến con trỏ trong c

Cấp phát :

#include <stdio.h>
 
int main() {
    int *px;
    *px = 42;
    printf("Vi tri con tro px la %p \n", px);
    printf("Gia tri con tro px tro toi la %d \n", *px);
    return 0;
}

Khi biên dịch thì sẽ không co lỗi (có cảnh báo), khi chạy sẽ không thể chạy được mà chương trình sẽ thoát ra luôn.
Nguyên nhân là khi khai báo biến con trỏ px thì máy mới chỉ cung cấp 2 byte để lưu địa chỉ của biến con trỏ mà chưa cấp phát vùng nhớ để con trỏ px lưu trữ dữ liệu. (tương tự như hợp tác xã cung cấp 2 Kg thóc cho bạn để làm giống nhưng lại không cung cấp cho bạn ruộng đất để bạn reo mạ vậy ).

Lưu ý: Có một số trình dịch sẽ không báo lỗi mà vẫn chạy bình thường nhưng tốt nhất là ta nên cấp phát trước khi sử dụng. Lỗi này sẽ xuất hiện rõ nhất khi bạn sử dụng con trỏ với mảng mà lát nữa ta sẽ đề cập.

Thôi ta đi vào vấn đề chính, làm sao để cấp phát vùng nhớ cho con trỏ.
Để cấp phát vùng nhớ cho con trỏ ta dùng các hàm sau trong thư viện stdlib.h.

  • malloc : tên con trỏ = (kiểu con trỏ *) malloc (sizeof(kiểu con trỏ));
  • calloc : tên con trỏ = (kiểu con trỏ *) malloc (n, sizeof(kiểu con trỏ));

Trong đó sizeof(kiểu con trỏ) là kích thước của kiểu; n là số lần của sizeof(kiểu con trỏ) được cấp.

#include <stdio.h>
#include <stdlib.h>
 
int main() {
    int *px, *qx;
    px = (int *) malloc(sizeof(int));
    qx = (int *) calloc(1, sizeof(int));
     
    printf("Vi tri con tro px la %p \n", px);
    printf("Gia tri con tro px tro toi la %d \n", *px);
     
    printf("Vi tri con tro qx la %p \n", qx);
    printf("Gia tri con tro qx tro toi la %d \n", *qx);
    return 0;
}

Ở đây các bạn chú ý: sự khác nhau duy nhất giữa malloc và calloc mà các bạn hiểu đơn giản là với malloc thì khi cấp phát máy sẽ cấp phát cho px 1 ô bất kỳ mà không cần biết ô đó có dữ liệu là gì hay không có dữ liệu (do đó *px có giá trị như trên) còn calloc cũng vậy nhưng khác 1 điểm là sau khi cấp phát thì máy sẽ tự động gán luôn giá trị 0 cho ô nhớ mà biến qx trỏ tới, tức qx có giá trị mặc định là 0.
Khi cấp phát cho biến con trỏ 1 số lượng ô nhớ nào đó mà trong quá trình làm việc ta thiếu và cần cấp phát thêm thì ta sử dụng lệnh realloc:
tên con trỏ = (kiểu con trỏ *) realloc (tên con trỏ, số lượng cần cấp phát * sizeof(kiểu con trỏ));
Trong đó: số lượng cần cấp phát = cũ + mới.
VD: Ban đầu ta cấp phát cho con trỏ px là 10 ô nhớ. Sau đó muốn cấp phát thêm cho nó 5 ô nhớ nữa thì số lượng cấp phát = 15.

Thu hồi và kiểm tra vùng nhớ còn lại

Để thu hổi bộ nhớ đã cấp phát ta dùng hàm free(tên con trỏ);

No votes yet.
Please wait...

BÌNH LUẬN