cslt1 chuong 3

Màu nền
Font chữ
Font size
Chiều cao dòng

Chương 3: Mảng, xâu và con trỏ

3.1. Mảng và xâu

3.1.1. Mảng:

a. Khái niệm:

Trong chương 2, ta đã làm quen với các dữ liệu cơ sở, còn gọi là kiểu vô hướng (int, char, float, ...), chúng được thể hiện trong chương trình qua các đại lượng (hằng, biến, hàm). Một trường hợp đặc biệt của biến là biến mảng (còn gọi là biến có chỉ số) để mô tả các đại lượng mang chỉ số. Biến mảng là một thành phần của mảng.

Mảng là một kiểu dữ liệu có cấu trúc, nó gồm một tập hợp các biến cùng kiểu dữ liệu cơ sở, cùng tên và phân biệt với nhau bằng chỉ số. Muốn được phép sử dụng biến mảng, trước đó phải khai báo để tạo ra mảng chứa biến đó. Mảng có thể là một hay nhiều chiều: Nếu mảng chứa các biến có một chỉ số thì gọi là mảng một chiều; Nếu mảng chứa các biến có nhiều chỉ số thì gọi là mảng nhiều chiều.

b. Khai báo mảng:

·    Công dụng: Tạo ra mảng chứa các biến mảng.

·    Cú pháp:

       <kiểu> <tên mảng><danh sách biểu thức>;

Trong đó:

- Kiểu là một trong các kiểu dữ liệu cơ sở (int, char, float, ...)

- Tên mảng là một tên hợp lệ đặt theo quy tắc chung

- Danh sách biểu thức gồm các biểu thức có giá trị nguyên đặt trong cặp dấu [ ]. Số biểu thức xác định số chiều của mảng, giá trị biểu thức xác định kích thước cực đại của chiều tương ứng.

·  ý nghĩa: Máy sẽ tạo ra mảng có tên là tên mảng, có kiểu dữ liệu là <kiểu>; Mảng này chứa các biến mảng cùng tên và kiểu với mảng, với các chỉ số chạy từ 0 đến kích thước cực đại - 1 và sắp xếp sao cho chỉ số bên phải tăng trước.

·  Ví dụ:

int   a[10];                /*khai báo 1 */

char  hoten[30];      /*khai báo 2 */

float  x[2][3];          /*khai báo 3 */

Khai báo 1 tạo ra mảng a là mảng 1 chiều có kiểu int, gồm 10 phần tử đặt liên tiếp trong 10 trường nhớ 2 bytes, sắp xếp theo thứ tự là:  a[0], a[1], a[2], ... a[9] . Địa chỉ của a[0] là &a[0]

Khai báo 2 tạo ra mảng hoten là mảng 1 chiều có kiểu char, gồm 30 phần tử đặt liên tiếp trong 30 trường nhớ1 byte, được sắp xếp theo thứ tự là:  hoten[0], hoten[1], hoten[2], ... hoten[29]

Khai báo 3 tạo ra mảng x là mảng 2 chiều có kiểu float, gồm 2 x 3 = 6 phần tử đặt trong 6 trường nhớ 4 bytes liên tiếp, được sắp xếp theo thứ tự là: 

x[0][0], x[0][1], x[0][2],

x[1][0], x[1][1], x[1][2].

Vì bộ nhớ của máy tính là bộ nhớ tuyến tính nên các phần tử mảng được xếp tuần tự từ trái sang phải. Trong ví dụ, viết dạng bảng để dễ nhìn.

Sau khi khai báo mảng, ta có thể truy nhập đến từng phần tử mảng như các biến bình thường thông qua tên kèm với chỉ số đặt trong cặp dấu [ ]. Việc lấy địa chỉ của các phần tử mảng vẫn qua toán tử &.

·  Chú ý:

Ø    Có thể khai báo nhiều mảng cùng chung một kiểu dữ liệu trong cùng một khai báo, chẳng hạn như:  float a[5], b[2][3];  sẽ tạo ra hai mảng a (một chiều) và b (hai chiều).

ØCó thể khai báo mảng kích thước không tường minh, dạng khai báo như sau:

                <kiểu> <tên mảng> [ ];

Cách khai báo này thường được sử dụng trong ba trường hợp sau:

- Trường hợp 1: Vừa khai báo mảng vừa khởi tạo giá trị cho các phần tử mảng, với dạng khai báo sau đây:

<kiểu> <tên mảng> [ ] = {danh sách dữ liệu};

Trong đó: Danh sách dữ liệu gồm các hằng dùng để gán dữ liệu cho các phần tử mảng viết cách nhau dấu phảy, đặt trong cặp dấu { }

Ví dụ:

Khai báo:   int  a[] = {1, 2, 3, 4}; tương tự như:

                   int a[4]; a[0]=1; a[1]=2; a[2]=3; a[3]=4;

Khai báo:   int  t[] [] ={ {1, 2, 3}, { 4, 5, 6}};  giống như:

                   int  t[2][3]

Khai báo:   char  ten[] = { ‘A’, ‘N’, ‘H’ };     cũng giống như:

                  char  ten[] = {“ANH”};

- Trường hợp 2: Khai báo mảng ngoài hàm (extern) để mảng được sử dụng ở nhiều chỗ khác nhau (nhiều hàm dùng chung một mảng).

Ví dụ:

#include <stdio.h>

extern int a[];

main()

{

int i;

for (i=0, i<=3; ++i)

  printf(“a[%d] = %d ”, i, a[i]);

getch();

}

int a[] ={12, 23, 34, 45};

- Trường hợp 3: Khai báo mảng dùng làm tham số hình thức của hàm. Dạng khai báo như sau:

                  <Kiểu> <Tên hàm>(<kiểu> <tên mảng[ ], ...);

 

 

                   

c. Ví dụ ứng dụng:

- Ví dụ 1: Sử dụng mảng một chiều: Sắp xếp dãy số thực  a1, a2 ... ,an (n<=100) theo thứ tự tăng dần.

Thuật toán :

-   Nhập số phần tử (n) và giá trị từng phần tử (ai, i=1àn)

-   Lặp lại n-1 lần, trong lần thứ i (i=1àn-1), lấy ai so sánh với các aj (j=i+1 à n) nếu ai>aj thì đổi chỗ hai phần tử. Ví dụ: lấy a1 so sánh với các số còn lại nếu gặp số nào nhỏ hơn thì đổi chỗ, kết thúc quá trình này, ta có a1 là số nhỏ nhất dãy; Tương tự, xét a2, kết thúc lần thứ hai ta có số a2 nhỏ thứ nhì, ... 

-   Hiển thị dãy số đã sắp xếp.

Sơ đồ khối thuật toán như sau:

 

Chương trình:

#include <stdio.h>

#include <conio.h>

#define  MAX 100

main()

{

int i, j, n;

float a[MAX], trung_gian;

printf(“Cho n= ”); scanf(“%d”,&n);

printf(“Nhập từng phần tử

”;

for (i=0; i<=n; ++i)

  {

    printf(“a[%d] = ”, i +1);

     scanf(“%f”, &a[i]);

   }

for (i=0; i<n; ++i)

   for (j=i+1; j<=n; ++j)

      if (a[i]>a[j])

        { trung_gian= a[i];

           a[i] = a[j];

           a[j] = trung_gian;

         }

clrscr();

printf(“Dãy số đã sắp xếp như sau:

”);

for (i=0; i<n; ++i)

   printf(“%f, ”, a[i]);

}

Nhận xét: Người ta thường gọi phần tử đầu của dãy là a1 và lưu nó ở biến a[0]... Như vậy dễ nhầm lẫn. Ta có thể khai báo mảng thừa một phần tử a[0] để lưu a1 ở a[1], a2 ở a[2], ... và nếu có thể, a[0] sẽ được dùng vào việc khác. Chẳng hạn, trong ví dụ trên, có thể không khai báo biến trung_gian mà dùng biến a[0] để làm chỗ trung gian trong việc đổi chỗ hai số. Nếu sử dụng như vậy, phải lưu ý khi sử dụng biến con trỏ mà ta sẽ nghiên cứu trong phần tới đây.

- Ví dụ 2: Sử dụng mảng hai chiều: Có ma trận vuông cấp A = [aij]nxn, tính tích các phần tử nằm trên đường chéo chính của ma trận (cho n<100).

Các phần tử nằm trên đường chéo chính là các phần tử có chỉ số i=j

Chương trình:

#include <stdio.h>

#include <conio.h>

main()

{

/* Phần khai báo biến */

int i, j, n;

float a[100][100], tich=1;

/* Phần nhập liệu */

printf(“Vào cấp ma trận vuông :”); scanf(“%d”, &n);

for (i=0; i<n; i++)

   {  clrscr();

       printf(“Vào từng phần tử theo dòng”);

       for (j=0; j<n; j++)

       {

         printf(“

a[%d,%d] = ”, i, j);

         scanf(“%f”; &a[i][j]);

        }

     }

   /* Phần tính toán */

for (i=0; i<n; i++)

    tich = tich*a[i][i];

  /* Phần hiển thị kết quả */

clrscr();

printf(“Tích các phần tử trên đường chéo chính là P = %f”, tich);

}

Khuyến cáo: Khi nhập dữ liệu vào các biến mảng, có thể truy nhập trực tiếp vào các phần tử qua địa chỉ (chẳng hạn, để nhập một số nguyên vào biến a[1], ta viết: scanf(“%d”, &a[1]);) nhưng cách này chỉ nên sử dụng với mảng một chiều và hai chiều với các phần tử kiểu nguyên. Với các mảng nhiều chiều, nên truy nhập gián tiếp vào biến mảng qua phép gán, tức là  nhập vào qua một biến trung gian rồi gán biến trung gian đó cho phần tử mảng; Ví dụ: để nhập một số thực kiểu float vào biến mảng a[1][1], thay vì viết lệnh scanf(“%f”, &a[1][1]); , ta viết hai lệnh:

 scanf(“%f”, &trung_gian); a[1][1]=trung_gian;

(Tất nhiên, biến trung_gian phải đã khai báo).

Phần nhập dữ liệu trong ví dụ 2 ở trên có thể viết là:

float temp;

/* Phần nhập liệu */

printf(“Vào cấp ma trận vuông :”); scanf(“%d”, &n);

for (i=0; i<n; i++)

   {  clrscr();

       printf(“Vào từng phần tử theo dòng”);

       for (j=0; j<n; j++)

       {

         printf(“

a[%d,%d] = ”, i, j);

         scanf(“%f”; &temp);

          a[i][j] = temp;

        }

     }

 

3.1.2. Xâu (string)

a. Khái niệm:

Trong C, một giá trị kiểu xâu (còn gọi là xâu kí tự  hoặc chuỗi) gồm một dãy kí tự đặt trong cặp dấu nháy kép. Máy xác định một dữ liệu kiểu xâu được cấu tạo từ kiểu char và được tổ chức thành mảng một chiều, trong đó mỗi kí tự được chứa ở một phần tử mảng. Ngoài ra, để quản lí xâu, C cần dùng thêm một phần tử cuối mảng để ghi kí tự điều khiển kết thúc xâu là kí tự \0 (null). Mảng một chiều kiểu char dùng để chứa xâu phải có kích thước bằng độ dài xâu + 1. Vì thế, xâu vừa có tính chất của dữ liệu kiểu char, vừa có tính chất của mảng một chiều.

Muốn lưu trữ một xâu, phải khai báo một mảng kiểu char, mỗi xâu lưu trữ trong một mảng. Vì lí do này mà trong C không tồn tại các phép toán ghép xâu, so sánh hai xâu, gán nội dung xâu này cho xâu khác.

Ví dụ: Khi khai báo char  ten[8];

thì mảng ten được khởi tạo với 8 phần tử (8 bytes) để chứa 8 kí tự kể cả kí tự \0, do đó, chỉ có thể ghi 7 kí tự thực của xâu. Nếu dùng lệnh gets(ten); để đọc xâu “ABCDE” từ bàn phím, bộ nhớ sẽ được bố trí như sau:

0

1

2

3

4

5

6

7

A

B

C

D

E

\0

*

*

Trong đó, dấu * chỉ phần tử mảng chưa xác định giá trị.

b. Khai báo biến xâu: 

Khai báo biến xâu, bản chất là khai báo mảng, có hai dạng như sau:

Dạng 1:           char  <tên biến>[kích thước];

Dạng 2:           char  <tên biến>[ ] = <hằng xâu>;

Ví dụ: Có khai báo:

            char  xau1[20];

            char  xau2[ ] = “TELEPHONE”;

thì xau1, xau2 là tên hai biến xâu. Biến xau1 chứa được tối đa là 19 kí tự trong các phần tử từ xau1[0] đến xau1[18] vì  phải dành phần tử xau1[19] chứa kí tự \0. Biến xau2 được khởi tạo gồm 10 phần tử.

Lưu ý:

- Với cấu trúc mảng, các tên biến trên chính là địa chỉ của phần tử thứ nhất trong mảng.

- Khác với các kiểu dữ liệu cơ sở (int, char, float, ...) biến xâu chỉ có một kiểu duy nhất là kiểu char và có kích thước không cố định nằm trong khoảng từ 1 đến 255.

- Nếu phải chứa một xâu có độ dài lớn hơn kích thước được khai báo, máy dễ bị treo. Khi đó bấm Ctrl +Break để thoát ra.

c. Truy nhập đến các kí tự trong xâu:

Để truy nhập đến một kí tự trong xâu, phải truy nhập đến phần tử mảng chứa kí tự đó thông qua chỉ số. Mỗi phần tử này đều là một biến kiểu char.

Ví dụ:

- Ví dụ 1: Đọc xâu (không quá 127) kí tự gõ vào từ bàn phím và đếm xem có bao nhiêu kí tự.

Cách làm: Đọc xâu vào từ bàn phím rồi đếm các phần tử trong mảng cho đến khi gặp kí tự \0

Chương trình:

#include <stdio.h>

main()

{

  char  xau[128];  int dem;

  printf(“Vào một xâu bất kì: ”); gets(xau);

  for  (dem=0; xau[dem] != ‘\0’ ; dem++);  /* thân for rỗng */

  printf(“

Xâu vừa nhập có %d kí tự”, dem);

  getch();

}

- Ví dụ 2: Cho một dãy kí tự được nhập vào bất kì, hãy xếp các kí tự theo thứ tự tăng dần của mã ASCII rồi in ra.

Cách làm:

+ Nhập dãy kí tự

+ Đếm số kí tự vừa nhập

+ Sắp xếp các kí tự

+ Hiển thị dãy đã xếp

Chương trình:

#include <stdio.h>

#define MAX 128

main()

{

  char  doi_cho, xau[MAX];  int i, j, k;

  printf(“Vào một xâu kí tự : ”); gets(xau);

  for  (k = 0; xau[k] != ‘\0’ ; k++);  /* xâu nhập vào có k kí tự */

  for (i = 0; i < k-1; i++)

      for (j=i+1); j < k; j++)

        if (xau[i] > xau[j])

            { doi_cho = xau[i];

               xau[i] = xau[j];

               xau[j] = doi_cho;

            }

  printf(“

Xâu đã sắp xếp là:  %s”, xau)

  getch();

}

d. Một số hàm mẫu với xâu:

Trong C không tồn tại các phép toán với xâu. Để thuận tiện cho các thao tác trên xâu, ngoài các hàm mẫu trong tệp ctype.h đã được giới thiệu (ở phần các đại lượng), C còn một số hàm mẫu với xâu đặt trong tệp tiêu đề có tên string.h. Để sử dụng các hàm này, đầu chương trình phải có dòng khai báo #include <string.h>.

Bảng sau đây sẽ liệt kê một số hàm mẫu đó, với các kí hiệu:

s là một xâu, c là một kí tự, n là một số nguyên kiểu int.

#include <string.h>

Chức năng của hàm

strlen(s)

Cho giá trị là số nguyên kiểu int chỉ độ dài xâu s, chính là chỉ số của kí tự \0

strcat(s1, s2)

Ghép xâu s2 vào cuối xâu s1 với điều kiện mảng chứa s1 phải đủ lớn để chứa cả s2. (Không được viết: s1=s1+s2;)

strncat(s1, s2, n)

Ghép n kí tự đầu của xâu s2 vào cuối xâu s1 với điều kiện mảng chứa s1 phải đủ lớn. Nếu s2 không đủ n kí tự sẽ bổ sung khoảng trống ở cuối trước khi ghép.

strcpy(s1, s2)

Sao xâu s2 đè lên xâu s1 với điều kiện bộ nhớ của xâu s1 phải đủ chứa xâu s2. (Không được viết s1 = s2;)

strncpy(s1, s2, n)

Sao n kí tự đầu tiên của s2 đè lên s1. Nếu bộ nhớ cho s1 thiếu gây tràn. Nếu s2 không đủ n kí tự sẽ bổ sung khoảng trống phía sau.

strcmp(s1, s2)

So sánh hai xâu s1 với s2 theo kiểu sắp xếp từ điển dựa trên giá trị mã ASCII. Hàm cho giá trị kiểu int, =0 nếu s1= = s2,  <0 nếu s1<s2,  >0 nếu s1>s2.

strcmpi(s1, s2)

Tương tự như hàm strcmp nhưng không phân biết chữ cái in và thường

stricmp(s1, s2)

Tương tự như hàm strcmp nhưng có phân biết chữ cái in và thường

strncmp(s1, s2, n)

Tương tự như hàm strcmp nhưng chỉ so sánh n kí tự đầu tiên của s1 và s2.

strnicmp(s1, s2, n)

Tương tự như hàm strncmp nhưng không phân biệt chữ cái in và thường.

strchr(s, c)

Tìm lần xuất hiện đầu tiên của kí tự c trong xâu s. Trả về địa chỉ của kí tự này trong xâu s hoặc \0 nếu không tìm thấy

strchar(s, c)

Tương tự strchr nhưng tìm kiếm từ cuối xâu

strlwr(s)

Chuyển xâu s sang chữ thường

strupr(s)

Chuyển xâu s sang chữ in

strset(s, c)

Khởi tạo các kí tự của s bằng kí tự c

strnset(s, c, n)

Khởi tạo n kí tự đầu tiên của s bằng kí tự c

strstr(s1,s2)

Tìm kiếm vị trí xâu s2 trong xâu s1. Kết quả trả về địa chỉ của lần xuất hiện đầu tiên của s2 trong s1 hoặc null nếu không tìm thấy.

Ngoài ra,  hai hàm gets và puts nằm trong tệp stdio.h chuyên dùng để vào, ra các xâu kí tự.

Hàm gets(s) sẽ đọc tất các các kí tự từ bàn phím vào xâu s. Việc kết thúc đọc khi gặp kí tự

hoặc số kí tự đã bằng độ dài cực đại của xâu. Khi kết thúc đọc, kí tự \0 sẽ được tự động điền vào cuối xâu.

Hàm scanf(“%s”,&s); cũng có thể đọc một xâu từ bộ đệm vào biến xâu s và ghi vào mảng nhưng cách thức đọc của scanf là chỉ đọc đến khi gặp khoảng trống thì dừng nên nhiều xâu sẽ không được đọc hết nếu nó chứa khoảng trống. Chẳng hạn, ta gõ xâu: Ha  noi thì chỉ biến xâu chỉ nhận giá trị Ha.

Hàm puts(s) sẽ hiển thị nội dung xâu s lên màn hình nhưng thay \0 bằng kí tự xuống dòng

3.2. Con trỏ

3.2.1. Khái quát về con trỏ

3.2.1.1. Khái niệm

Trong hầu hết các ngôn ngữ lập trình bậc cao, khi sử dụng biến, người dùng chỉ quan tâm tới nó qua tên mà không cần quản lí tới việc nó nằm ở đâu trong bộ nhớ. Tuy nhiên, để can thiệp sâu vào quá trình làm việc của máy tính, người lập trình cần biết địa chỉ của các đại lượng được sử dụng. C cung cấp một loại biến đặc biệt để chứa địa chỉ của các biến khác, gọi là con trỏ (hay biến con trỏ).  Thông qua con trỏ, ta có thể biết địa chỉ của trường nhớ chứa biến để truy nhập tới biến đó một cách trực tiếp.

Vậy: Con trỏ (pointer) là một loại biến dùng để chứa địa chỉ biến (không chứa dữ liệu) có kích thước cố định là 2 bytes. Khi con trỏ chứa địa chỉ của biến nào thì ta nói nó trỏ vào biến đó, hay còn gọi là tham chiếu tới biến đó. Mỗi kiểu biến được lưu trữ trong một trường nhớ có địa chỉ và độ dài tương ứng và do đó, cần có một kiểu con trỏ tương ứng: con trỏ kiểu int dùng để chứa địa chỉ của biến kiển int, con trỏ kiểu float dùng để chứa địa chỉ của biến kiểu float, ...

Vì con trỏ cũng là một loại biến nên trước khi sử dụng phải khai báo. Dạng khai báo như sau:

          Dạng 1: Khai báo định kiểu:

<Kiểu> <*Tên con trỏ>;

Ví dụ:

    int  x, y, *px, *c;

    char  *pt;

    float  a[20][30], *pa, (*pm)[30];

Khi đó:

Lệnh 1: Khai báo 2 biến x và y có kiểu int, hai con trỏ kiểu int là px và c;

Lệnh 2: Khai báo biến pt là con trỏ kiểu char ;

Lệnh 3: Khai báo a là mảng 2 chiều kiểu float đồng thời a cũng là địa chỉ kiểu float[30], pa là con trỏ kiểu float, pm là mảng con trỏ kiểu float có 30 phần tử; Với khai báo này, có thể viết phép gán:

pm = a;

là đúng  nhưng

pa = a;

thì sai.

          Dạng 2: Khai báo không định kiểu:

void <*Tên con trỏ>;

Với cách khai báo này, con trỏ có thể trỏ tới bất kì một biến kiểu nào nhưng cách này sẽ làm hạn chế nhiều phép toán trên con trỏ. Loại con trỏ này còn được gọi là con trỏ void.

Ví dụ:

void  *pointer;

thì pointer là một con trỏ không định kiểu (void).

3.2.1.2. Định vị con trỏ

Sau khi khai báo xong, giá trị của biến con trỏ chưa xác định vì nó chưa trỏ tới biến nào. Để con trỏ có nghĩa, phải định vị con trỏ tới một biến nào đó, nghĩa là cho con trỏ trỏ vào một biến.

    a. Phép toán &

Phép toán & cho địa chỉ một biến (biến đơn, phần tử mảng) trong bộ nhớ. Phép toán này được dùng để định vị cho một con trỏ với cú pháp như sau:

Cú pháp:

            Tên con trỏ = &tên biến;

Ví dụ:

char  c=’A’, *p;   /p là con trỏ */

p = &c;   /*gán địa chỉ của biến c cho con trỏ p */

Chú ý: Phép toán & không được sử dụng cho biến kiểu register chứa trong các thanh ghi (sẽ nghiên cứu kiểu biến này ở phần sau).

b. Phép toán *

Phép toán * cho nội dung của một con trỏ, đó là địa của chỉ biến được con trỏ trỏ tới. Qua địa chỉ này, có thể truy nhập đến biến được trỏ bởi con trỏ.

Ví dụ:

main()

  {

int  x=1, y=2;

int *p;         /* p là biến con trỏ kiểu int */

p = &x;       /* p trỏ tới biến x, nghĩa là p chứa địa chỉ biến x */

y = p;          /* Biến y nhận giá trị là địa chỉ biến x */

printf(“

Địa chỉ x là: %X”, y);

y = *p;        /* gán cho y giá trị trường nhớ có địa chỉ p */

printf(“\ Giá trị y = %d”, y);

*p = *p +10;

printf(“\ n Bây giờ x = %d”, x);

getch();

}

Kết quả hiển thị sẽ là:

      Địa chỉ x là: FFD2

      Giá trị y = 1

            Bây giờ x = 11

c. Chú ý:

 - Khi p trỏ tới biến x, có thể viết *p thay cho x .

 - Phép toán & và * có độ ưu tiên thực hiện cao hơn các phép toán số học.

 - Cần phân biệt địa chỉ con trỏ và giá trị con trỏ. Địa chỉ con trỏ có thể không quan tâm nhưng giá trị con trỏ thì phải luôn lưu ý để gắn liền với một biến nào đó, chẳng hạn viết:

int x = 1000, *p;

*p = x;

thì thật tai hại vì con trỏ p trỏ tới một chỗ ngoài ý muốn. Một trường hợp khác:

int x=1, y=2, z=3,  *p;

p = &x;    /*  Viết *p  thì giá trị là 1  */

p = &y;    /*   Vẫn viết *p nhưng giá trị =2 */

p ++    ;    /*   Vẫn viết *p nhưng giá trị =3 */

3.2.2. Các phép toán trên con trỏ

Có 4 nhóm phép toán liên quan đến con trỏ và địa chỉ, đó là:

Phép gán

Phép tăng giảm địa chỉ

-  Phép truy nhập bộ nhớ

-  Phép so sánh

3.2.2.1. Phép gán:

Phép toán này dùng để gán cho con trỏ một giá trị là địa chỉ một biến nhớ nào đó. Nó được viết giống như lệnh gán cho một biến.

Ví dụ:

  int x=1, *px, *qx;

  px = &x;

  qx = px;

Khi đó: Lệnh 2 gán địa chỉ của biến x kiểu int cho con trỏ px; Lệnh 3 gán giá trị của con trỏ px cho con trỏ qx nên cả px và qx đều trỏ tới cùng một biến x.

Trong phép gán cho con trỏ, phải sử dụng đúng kiểu; Trường hợp gán cho con trỏ địa chỉ của biến khác kiểu phải sử dụng phép ép kiểu như trong các lệnh gán thông thường. Chẳng hạn:

int i;

char *pc;

pc = (char*)  (&i);

3.2.2.2. Phép tăng giảm địa chỉ:

Với px là một con trỏ định kiểu của biến x, i là một đại lượng kiểu nguyên, C xây dựng phép toán tăng,  giảm trên con trỏ px, đó là:

px + i  để dịch chuyển con trỏ i phần tử tính từ x. Việc dịch về phía trước hay sau so với x là do giá trị của i:

Nếu i > 0 thì dịch về phía sau x;

Nếu i < 0 thì dịch về phía trước x.

Thực chất của hai phép toán này là thay đổi giá trị của biến px đi một lượng là (i x độ dài của kiểu con trỏ) và từ đó mà trỏ sang một phần tử khác.

Ví dụ:

Ví dụ 1:

  #include  <stdio.h>

main()

  {        int a[10], *p1, i;

            for  (i=0; i<=9; i++)   a[i]=i;

              /*Các phần tử a[0], a[1], ..., a[9] nhận các giá trị từ 0 đến 9 */

            p1 = &a[0];

            for  (i=0; i<=9; i++)

            {

  printf(“

a%d có Địa chỉ: %p và Giá trị = %d ”, i, p1, *p1);

/*Hiển thị địa chỉ và giá trị các a[i] với i chạy từ 0 đến 9 */

  p1 = p1 +1;        

}

            getch();

   }                

Giải thích:

- Mã định dạng %p để đưa ra giá trị biến con trỏ; Cũng có thể dùng mã định dạng %X hay %x.

- Chương trình sẽ hiển thị như sau:

a0 có Địa chỉ là FF0C và Giá trị = 0

a1 có Địa chỉ là FF0E và Giá trị = 1

a2 có Địa chỉ là FF10 và Giá trị = 2

...

a9 có Địa chỉ là FF1E và Giá trị = 9

- Tuy không cần quan tâm đến giá trị của p1 nhưng nếu để ý sẽ thấy: Địa chỉ các phần tử trong mảng a cách nhau 2 (vì số kiểu int được ghi trong 2 bytes)

Ví dụ 2:

  #include  <stdio.h>

main()

  {

     int x=1, y=2, z=3;

     int  *p;

     p = &x;

     printf(“

p trỏ tới trường nhớ            %p và giá trị trường là %d”, p, *p);

     p = p + 2;

     printf(“

p trỏ tới trường nhớ            %p và giá trị trường là %d”, p, *p);

     p = p - 1;

     printf(“

p trỏ tới trường nhớ            %p và giá trị trường là %d”, p, *p);

    getch();

   }

Giải thích:

- Màn hình sẽ hiển thị:

p trỏ tới trường nhớ   FFCE và giá trị trường là 1   (của x)

p trỏ tới trường nhớ   FFD2 và giá trị trường là 3   (của z)

p trỏ tới trường nhớ   FFD0 và giá trị trường là 2   (của y)

- Các bytes:  FFCE và ffcf chứa biến x, FFD0 và ffd1 chứa y, FFD2 và ffd3 chứa biến z . Vậy, khi khai báo một dãy biến đơn liên tiếp cùng kiểu, nếu sử dụng qua con trỏ, ta liên tưởng nó như một mảng.

3.2.2.3. Phép truy nhập bộ nhớ:

Khi truy nhập vào bộ nhớ máy tính, thông qua con trỏ sẽ có thể truy nhập được một trường nhớ có địa chỉ  bằng giá trị con trỏ và độ dài bằng độ dài của kiểu tương ứng đó. Chẳng hạn như:

-  Con trỏ kiểu float truy nhập một trường nhớ 4 bytes;

-  Con trỏ kiểu int truy nhập một trường nhớ 2 bytes;

-  Con trỏ kiểu char truy nhập một trường nhớ 1 bytes;

................

Ví dụ:

float  f, *pf;

int  i, *pi;

char c, *pc;

pf = &f; pi = &i; pc = &c;

Nếu pf = FFCA thì *pf là trường nhớ 4 bytes (FFCA, ffcb, ffcc, ffcd) tên là f

Nếu pi = FFCE thì *pi là trường nhớ 2 bytes: (FFCE, ffcf) tên là i

Nếu pc = FFD0 thì *pc là trường nhớ 1 bytes: FFD0 tên là c

Qua đây lại càng thấy rõ, có thể truy nhập một trường nhớ gián tiếp qua tên hoặc trực tiếp qua địa chỉ của nó.

Chú ý: Hai phép toán tăng giảm địa chỉ và truy nhập bộ nhớ không dùng được đối với con trỏ void vì hai phép toán này sử dụng kiểu con trỏ để xác định độ dài trường nhớ mà void lại không định kiểu.

3.2.2.4. Phép so sánh:

Mỗi con trỏ là một số nguyên kiểu int được lưu trữ ở 2 bytes để chứa địa chỉ một biến nào đó. Vì thế, có thể so sánh hai con trỏ với nhau bằng một dấu phép toán quan hệ để biết trong hai biến, biến nào có địa chỉ thấp hơn.

Ví dụ: Trong ví dụ 2 phần 3.2.2.3. nêu trên, nếu viết biểu thức:  pf < pi  thì biểu thức này nhận giá trị 1 (đúng) vì pf = FFCA(16) và pi = FFCE(16)

3.2.3. Con trỏ và mảng

Sau khi nghiên cứu về con trỏ, ta biết, có thể truy nhập một biến thông qua tên biến (như đã làm từ trước đến nay) hoặc thông qua con trỏ để trực tiếp truy nhập vào địa chỉ của biến đó.

Khi xét một mảng, Turbo C quy định: tên mảng chính là một hằng địa chỉ, chứa địa chỉ của phần tử đầu tiên trong mảng.

Các phép toán với con trỏ làm cho ta liên tưởng đến một mối liên hệ giữa con trỏ và mảng.

3.2.3.1. Con trỏ và mảng một chiều

Xét một khai báo float x[10], *p; làm ví dụ  đặc trưng.

Cho  i và k là hai biến nguyên:

- Xét ở giác độ mảng:

    &x[0]  tương đương với  x

    &x[i]  tương đương với   x +i;

    x[i]     tương đương với   *(x+i)

- Xét ở giác độ con trỏ:

    p = &x[0]  tương đương với  p = x

    p + i  tương đương với   x +i;

    *(p+i)     tương đương với   *(x+i)

Thế thì, khi p =x, các cách viết sau đây là như nhau:

  x[i] ,     *(x +i) ,   p[i]   và *(p+i)

Ví dụ:

Ví dụ 1: Truy nhập tới phần tử mảng không qua chỉ số mà qua con trỏ

# include  <stdio.h>

main()

{

int a[3] = {10, 20, 30}, *p;

p = a;  /* p trỏ  vào a[0]  */

printf(“ Giá trị của a[0] =  %d

”, *p);

p = p +1;

printf(“ Giá trị của a[1] =  %d

”, *p);

printf(“ Giá trị của a[2] =  %d

”, *(p + 1));

getch();

}

Kết quả hiển thị:

Giá trị của a[0] =  10

Giá trị của a[1] =  20

Giá trị của a[2] =  30

Ví dụ 2: Vào - ra dữ liệu đối với mảng một chiều thông qua con trỏ

#include <stdio.h>

#include <conio.h>

main()

{

int a[5], *p;

int i;

p = a;

clrscr();

/*Nhập mảng */

for  (i=0; i<=4; i++)  

 {

   printf(“

a[%d] = ”, i);

   scanf(“%d”, p+i);

     /* Lệnh scanf có thể viết là: scanf( “%d”, a+i);

        hoặc:  scanf(“%d”, &p[i]);

        hoặc:  scanf(“%d”, p); p++;  */

  }

clrscr();

/* Xuất mảng */

printf(“Đã nhập các số:”);

for  (i = 0; i<=4; i++)

   printf(“       a[%d] =  %d

”, i, *(a +i) );

     /* Đừng viết: printf(“a[%d] = %d

”, i , *a); a=a+1  */

getch();

}

Khái quát: Có thể truy nhập các phần tử mảng bằng con trỏ. Khi đó, tên mảng có thể được sử dụng thay cho con trỏ, nhưng phải chú ý rằng: Con trỏ có thể thay đổi giá trị nhưng tên mảng thì không. Tên mảng chỉ có thể tham gia vào các biểu thức để trỏ đến các phần tử khác nhau trong mảng.

3.2.3.2. Con trỏ và xâu

Về bản chất, mỗi xâu được chứa trong một mảng kiểu char. Khi sử dụng một xâu, máy sẽ cấp một vùng nhớ để lưu trữ các kí tự của xâu kèm kí tụ null (\0) ở cuối, mỗi kí tự chứa trong một phần tử mảng. Khi con trỏ trỏ đến một xâu thì nó sẽ chứa địa chỉ phần tử đầu tiên của mảng lưu trữ xâu đó.

Điều vừa trình bày được minh hoạ trong ví dụ sau:

char  *p;

p = “Programming with C”;

thì:

*p = ‘P’, *(p+1) = ‘r’, *(p+2) = ‘o’,  ...

và lệnh:

     printf(“%s”, p);

sẽ hiển thị lên màn hình xâu:

    Programming with C

Ví dụ:  Sử dụng con trỏ và xác định địa chỉ con trỏ trong xâu

#include  <stdio.h>

char  str[] = “TURBO C”;

main()

{ char  i, *pt;

pt = str;

  for  ( i = 1; *pt != ‘\0’ ; pt++)

    printf(“ Kí tự thứ %d có giá trị:  %c  và ở địa chỉ:  %p 

”, i++ , *pt, pt);

  getch();

}

Kết quả hiển thị:

Kí tự thứ  1 có giá trị:  T  và ở địa chỉ:  0194

Kí tự thứ  2 có giá trị:  U  và ở địa chỉ:  0195

Kí tự thứ  3 có giá trị:  R  và ở địa chỉ:  0196

Kí tự thứ  4 có giá trị:  B  và ở địa chỉ:  0197

Kí tự thứ  5 có giá trị:  O  và ở địa chỉ:  0198

Kí tự thứ  6 có giá trị:       và ở địa chỉ:  0199

Kí tự thứ  7 có giá trị:  C  và ở địa chỉ:  019A

Ví dụ 2: Tìm chỗ sai về sử dụng con trỏ

char  *p,  kt[30];

kt = “Tp. HO CHI MINH”;

scanf(“%s”, kt);

scanf(“%s”, p);

Rõ là: kt là tên mảng; tên mảng là một hằng địa chỉ chứa địa chỉ mảng (cũng là địa chỉ phần tử đầu tiên). Vậy gán cho kt một xâu là không hợp lệ. Chỉ có thể gán cho con trỏ p một xâu thì được.

3.2.3.3. Con trỏ và mảng nhiều chiều

Việc xử lí mảng nhiều chiều phức tạp hơn so với mảng một chiều. Không phải mọi quy tắc đúng với mảng một chiều đều có thể áp dụng đối với mảng nhiều chiều.

Phép toán lấy địa chỉ, nói chung,  không dùng được với các thành phần của mảng nhiều chiều trừ mảng các số nguyên.

Ta xét chương trình sau đây, với ý định nhập số liệu cho một mảng hai chiều (ma trận) kiểu float.

# include “sdtio.h”

main()

{

float  a[10][20];

int  i, j,  n;

printf(“Kich thuoc ma tran n = ”); scanf(“%d”, &n);

for  (i = 0; i < n; i++) 

     for  (j = 0 ; j < n; j++)

          { printf(“ a(%d,%d) = ”, i, j);

             scanf(“ %f”, &a[i][j]);

           }

}

Chương trình chạy sai vì phép toán lấy địa chỉ &a[i][j] với a là mảng hai chiều các số thực là không đúng. Địa chỉ phần tử a[i][j] trong bộ nhớ, thực tế,  được xác định bằng: (float *) a+i*N+j   với N là kích thước cực đại của chiều thứ hai (ứng với chỉ số j) theo khai báo.

Thế thì, chương trình trên sẽ viết đúng như sau:

# include “sdtio.h”

main()

{

float  a[10][20];

int  i, j,  n;

printf(“Kich thuoc ma tran n = ”); scanf(“%d”, &n);

for  (i = 0; i < n; i++) 

     for  (j = 0 ; j < n; j++)

          { printf(“ a(%d,%d) = ”, i, j);

             scanf(“ %f”, (float *) a+i*20+j);

           }

}

Tóm lại: Để tính địa chỉ của phần tử trong mảng nhiều chiều, phải dùng phép ép kiểu (chuyển kiểu bắt buộc). Với một mảng có nhiều chiều, nếu dùng con trỏ để đại diện cho phần tử, phải tính toán địa chỉ một cách cẩn thận để di chuyển con trỏ. Một khuyến cáo là, nếu cần nhập dữ liệu vào các mảng nhiều chiều, bạn hãy nhập vào một biến trung gian rồi gán biến trung gian đó cho phần tử mảng.

Bài tập chương 3

1.  Viết chương trình nhập vào một số nguyên dương hệ 10 rồi hiển thị lên màn hình giá trị của số ở hệ 2.

2.  Viết chương trình nhập vào một số nguyên âm hệ 10 rồi hiển thị lên màn hình giá trị của số ở hệ 2. .

3.  Cho một dãy số a1, a2, ... an. Viết chương trình tách dãy số thành hai dãy dãy số âm và không âm được xếp tăng dần và hiển thị kết quả ra màn hình dạng:

            Dãy số âm có ... phần tử là :

            Dãy số không âm có ... phần tử là :

4.  Viết chương trình tính tích hai ma trận vuông cấp n và hiển thị ma trận tích lên màn hình.

5.  Nhập vào mảng một danh sách 100 sinh viên gồm 2 cột: Họ tên, điểm trung bình. Hãy in ra danh sách 10 sinh viên có điểm trung bình cao nhất.

6.  Không dùng hàm mẫu, viết chương trình để so sánh hai xâu bất kì.

7.  Nhập vào mảng một danh sách 10 sinh viên gồm 3 cột: Họ tên đệm, tên, điểm trung bình. Hãy in ra danh sách trên đã xếp theo thứ tự alphabet của tên sinh viên. Yêu cầu truy nhập phần tử mảng bằng con trỏ.

8.  Nhập vào mảng một xâu bất kì rồi loại bỏ tất cả các khoảng trống trong xâu và hiển thị kết quả đó ra màn hình.

9.  Nhập vào một số thực và đọc giá trị đó bằng chữ. Ví dụ, số nhập: 1234.56 được hiển thị là một nghìn hai trăm ba mươi tư phẩy năm mươi sáu.

10.      Cho n là một số tự nhiên lẻ. Điền các số từ 1 đến n2 vào một ma trận vuông cấp n để nó trở thành một ma phương, tức là một ma trận có tổng các phần tử trên các hàng, tổng các phần tử trên các cột và tổng các phần tử trên trên hai đường chéo là bằng nhau.

Thuật toán gợi ý:

- Các ô được khởi tạo = 0

- Điền 1 vào ô giữa dòng 1

-  Các số từ 2 đến n2 được điền vào các ô còn lại theo nguyên tắc:

·  Nếu a[1,n] ≠ 0 (đã được điền) thì điền vào ô a[2,n]

·  Nếu a[1,j] ≠ 0  thì điền vào ô a[n,j+1] (nhảy xuống dưới)

·  Nếu a[i,n] ≠ 0  thì điền vào ô a[i-1,1] (về cột đầu)

·  Nếu a[i,j] ≠ 0 và a[i-1, j+1] ≠ 0 thì điền vào ô a[i+1,j]

·  Nếu không gặp bốn trường hợp trên thì đã điền xong cho ô a[i,j], điền tiếp cho ô a[i-1,j+1]

Bạn đang đọc truyện trên: Truyen2U.Pro