Xem mẫu

  1. Chương 3 MẢNG, XÂU KÍ Tự VÀ CON TRỎ 3.1. MẢNG VÀ XÂU 3.1.1. Mảng Để giải quyết các trường hợp cần phải làm việc với một số lượng lớn các biến có cùng kiểu dữ liệu với nhau, ngôn ngữ c cung cấp một loại biến đặc biệt gọi là mảng. Màng là một dãy các phần tử có cùng kiểu dữ liệu được đặt liên tiếp trong bộ nhớ và có thể truy xuất đến từng phần tử thông qua chỉ số mảng. Chỉ số màng là thành phần được đặt trong cặp [ ] và đứng sau tên của mảng. Điều này có nghĩa là, chúng ta có thể lưu 5 giá trị kiểu int mà không cần phải khai báo 5 biến khác nhau.Ví dụ, một mảng chứa 5 giá trị nguyên kiểu int có tên là a có thể được biểu diễn như sau: 0 i 2 3 4 2B 2B 2B 2B 2B Trong đó mỗi một ô trống biểu diễn một phần tử của mảng, trong trường hợp này là các giá trị nguyên kiểu int. Chúng được đánh số từ 0 đến 4 vì phần tử đầu tiên của mảng luôn là 0 bất kể độ dài của nó là bao nhiêu. Như vậy với một mảng được hiểu như sau: - Tập hợp các phần tử cùng kiểu. - Các phần tử phân biệt bởi chi số mảng - Mỗi phần tử như một biến đơn có địa chỉ liên tiếp trong ô nhớ - Kiểu mảng là kiểu các phần tử Thông tin về mảng sẽ phải bao gồm: - Kiểu mảng. Ví dụ như int 77
  2. - Tên mảng. Ví dụ như a. Tên mảng được đặt cũng phải tuân thủ theo qui tắc đặt tên. - Số các phần tử hay kích thước của mảng. Như ví dụ trên thì số các phần tử của mảng là 5. Cú pháp chung nhất khi khai báo một mảng được định nghĩa như sau: [sizel][[size2][...[sizeN]]]; Trong đó: Kiểu dữ liệu là các kiểu dữ liệu cơ sở như đà định nghĩa ở trên; Tên mảng là tên được đặt cho mảng. Sizel, size2,.., sizeN là các số qui định kích cỡ của mảng hoặc số phần tử ưong mảng, số lượng thành phần [] được đặt sau tên mảng sẽ qui định chiều của mảng đó. Ví dụ int a[10] ; a là mảng một chiều kiểu nguyên gồm có 10 phần từ là a[0], a[l],.., a[8], a[9] hoặcint arr[2][3]; arr là mảng hai chiều kiểu nguyên gồm có 2x3 = 6 phần tử. arr[0][0], arr[0][l], arr[0][2], arr[l][0]> arr[l][l], arr[l][2]. Chú ý: Các lỗi thường gặp: int n,m; int a[n][m]; //n, m chưa xác định Chú ý: sizel,size2..sizeN của biến màng ở bên trong cặp ngoặc n phải là một giá trị hằng khi khai báo một mảng, vì mảng là một khối nhớ tĩnh có kích cỡ xác định và trình biên dịch phải có khả năng xác định xem cần bao nhiêu bộ nhớ để cấp phát cho mảng trước khi các lệnh có thể được thực hiện. Vì thế các câu lệnh viết như trên là sai, chương trình sẽ báo các lỗi như: 78
  3. expected constant expression (cần một biểu thức hằng) cannot allocate an array of constant size 0 (không thể cấp phát một mảng kích thước 0) 'a': unknown size (a: không biết kích cỡ) 3.1.1.1. Khởi tạo mảng Khi khai báo một màng với tầm hoạt động địa phương (trong một hàm), theo mặc định nó sẽ không được khởi tạo, vì vậy nội dung của nó là không xác định cho đến khi chúng ta lưu các giá trị lên đó. Nếu chúng ta khai báo một màng toàn cục (bên ngoài tất cả các hàm) nó sẽ đựợc khởi tạo và tất cà các phần tử được đặt băng 0 nếu là mảng có dữ liệu kiểu số và NULL nếu màng có dữ liệu kiểu con trỏ. Vì vậy nếu chúng ta khai báo mảng toàn cục: char a[5]; thì mọi phần tử của a sẽ được khởi tạo là 0: 0 12 3 4 00000000 00000000 00000000 00000000 00000000 Tuy nhiên, khi khai báo một mảng, chúng ta có thể gán các giá trị khởi tạo cho từng phần tử của nó. Ví dụ: char a[5] = {0,1,4,3,2}; lệnh trên sẽ khai báo một màng như sau: 0 12 3 4 00000000 00000001 00000100 00000011 00000010 Hay nếu viết theo giá trị thập phân sẽ là: 0 12 3 4 0 1 4 3 2 79
  4. Điều này tương đương với khi khởi tạo a[0] =0, a[l] = 1, a[2] = 4, a[3] = 3,a[4] = 2 Tuy nhiên, khi khỏi tạo, có thể không cần khởi tạo hết tất cả các phần tử của mảng. Có thể tổng quát như sau. Với mảng 1 chiều'. [size]={gtl,gt2,gt3,..,gtk}; (k
  5. [sizel] [size2] ={ {an,ai2,ai3,...}, {a2i,a22,a23,—}, ••• }; Khi đó: aỊ ] ,a12,a23,... là giá trị khởi đầu của hàng đầu tiên của mảng. 321,322,323,... là giá trị khởi đầu của hàng thứ hai của mảng. Ở đây số giá trị khởi đầu của mỗi hàng không yêu cầu phải giống nhau. Và giá trị khởi đầu cùa các phần tử trong mỗi hàng sẽ được gán đứng vị trí của chúng trong màng. Ví dụ int a[][4]={ {0},{l,3,5}, {2,4,6,8} } Lúc này: a[0] [0] =0 a[l][0]=l, a[l][l]=3, a[l][2]=5, và a[2][0]=2, a[2][l]=4, a[2][2]=6, a[2][3]=8 Ngoài ra mảng hai chiều còn có cách khởi tạo khác như sau: [sizel][size2] ={gtl, gt2,..,gtk}; (k
  6. Ví dụ a[3][2]={l,2,3,4,5,6}; Hoặc: [] [size2J ={gtl, gt2,..,gtk}; thì mảng có được sẽ được cấp phát cho số dòng là sizel = [k/m] +1 và các phần tử của mảng lại được khởi tạo theo đúng qui trình lần lượt hết các phần tử ở dòngl, rồi đến dòng2,.. đến dòng k. 3.1.1.2. Truy xuất đến các phần tử của mảng Ờ bất kì điểm nào của chương trình trong tầm hoạt động của mảng, chúng ta có thể truy xuất từng phần tử của mảng để đọc hay chỉnh sửa như là đối với một biến bình thường, cấu trúc của nó như sau: Tên_mảng\chỉ_số Như ở trong ví dụ trước ta có mảng a gồm 5 phần tử có kiểu int, chúng ta có thể truy xuất đến từng phần tử cùa mảng như sau: a[OJ 3(1] 3(2] 3(3] 3(4] 0 1 4 3 2 Ví dụ, để lưu giá trị 75 vào phần tử thứ ba của a ta viết như sau: a[2] = 75; và ví dụ để gán giá trị của phần tử thứ 3 cùa b cho biến X, chúng ta viết: X = a[2] ; Vì vậy, xét về mọi phương diện, biểu thức 3(2] giống như bất kì một biến kiểu int khác mà thôi. Chú ý rằng phần tử thứ ba của a là a [2], vì màng bắt đầu từ chỉ số 0. Vì vậy, phần tử cuối cùng sẽ là a [4]. Vì vậy nếu chúng ta viết a [5], chúng ta sẽ truy xuất đến phần tử thứ 6 của mảng và vượt quá giới hạn của mảng. Việc vượt quá giới hạn chỉ số của màng là hoàn toàn hợp lệ, Tuy nhiên, nó có thể gây ra những vấn đề thực sự khó phát hiện bởi vì chúng không tạo ra những lỗi bong quá trình dịch nhưng chúng có thể tạo ra những kết quả không mong muốn frong quá trình thực hiện. 82
  7. Nguyên nhân của việc này sẽ được nói đến kĩ hơn ở phần sử dụng con trỏ (mục 3.2). Cần phải nhấn mạnh rằng chúng ta sử dụng cặp ngoặc vuông cho hai mục đích: Đầu tiên là đặt kích thước cho mảng khi khai báo chúng và thứ hai, để chỉ định chỉ số cho một phần tử cụ thể của mảng khi xem xét đến nó. int a[5]; // khai báo một mảng mới. int x,i; a[2) = 75; // truy xuất đến một phần tử của mảng. Một vài thao tác hợp lệ khác với màng: a[0] = x; // Gán giá trị biến X cho phần tử a[OJ a[i] = 75; // Gán giá trị 75 cho phần tử a[i] , i đã khai báo ở hên X = a [i+2]; // Gán cho X giá trị của phần tử a[i+2] a[a[i]] = a[2) + 5; // Gán cho a[a[i]] giá fri của phần tử a[2) + 5. Nói chung chỉ được viết a[i] với i cũng là một biến khi mà i đã được khai báo là một biến kiểu nguyên. Với mảng hai chiều, việc truy xuất tới các phàn tử của mảng cũng tương tự như mảng một chiều. Mảng hai chiều cũng phải bắt đầu từ phần tử có chỉ số 0, tức là a[0][0] . Truy xuất tới phần tử a[i][j] có nghĩa là phần tử ở dòng i cột j và cũng như mảng một chiều. Với mảng hai chiều, thường sử dụng define để định nghĩa trước kích cỡ của mảng. Việc khai báo define này cũng sẽ tiện lợi khi muốn thay đổi giới hạn kích cỡ của mảng. Ví dụ 3.1.1: Nhập và in ra một ma trận số nguyên a(n,m) với n và m không vượt quá 100. #include #include #defineN 100 #define M 100 83
  8. int main() { _ int a[N][M]; int n,m,i j; // kiem tra neu so dong hoac so cot vuot qua 100 Do { printf("\n Nhap so dong:"); scanf("%d",&n); printf("\n Nhap so cot:"); scanf("%d",&m); } while (n>=N||m>=M); // phan nhap cac phan tu ma tran printf("\n Nhap cac phan tu ma tran:"); for(i=0;i
  9. Để in ra dưới dạng ma trận phải sử dụng hai vòng lặp for cho số dòng và số cột của ma trận. Trong đó, cứ hết một dòng lại phải in xuống dòng. Cụ thể phần cài đặt của in ma ưận là: printf("\n Ma tran vua nhap la:\n"); for(ì=0;i
  10. for (i=l; i< n; i++) { if (a[i]
  11. int n,m,ij; // kiem fra neu so dong hoac so cot vuot qua 100 do { printf("\n Nhap so dong:"); scanf("%d",&n); printf("\n Nhap so cot:"); scanf("%d",&m); } while (n>=N||m>=M); // phan nhap cac phan tu ma fran printf("\n Nhap cac phan tu ma tran thu nhat:"); for(i=0;i
  12. scaní("%d",&B[i]ũ]); } // phan in ra printf("\n Ma ưan vua nhap la:\n"); for(i=0;i
  13. nhanh hơn nhiều và hiệu quả hơn. Để có thể nhận mảng là tham số chúng ta phải làm khi khai báo hàm là chỉ định trong phần tham số kiểu dữ liệu cơ bản của màng, tên màng và cặp ngoặc vuông trống. Ví dụ, hàm sau: void proc (int arg[]) nhận vào một tham số có kiểu "mảng của int" và có tên arg. Hoặc viết: void proc(int arg[10]); hoặc khai báo: void proc(int []); Để truyền tham số cho hàm này một màng được khai báo: int myarray [40]; Lời gọi hàm như sau: procedure (myarray); Dưới đây là một ví dụ cụ thể: Ví dụ 3.1.4 // Vi du mang tham so #include #include void printarray (int arg[], int length) { int i; for (i=0; i
  14. int secondarray[] = {2,4, 6, 8, 10}; printarray (firstarray,3); printf(“\n”); printarray (secondarray,5); return 1; } Chương trình này sẽ in ra: 5 10 15 2 4 6 8 10 Thậm chí nếu có viết chương trình như sau: Ví dụ 3.1.5 // Vi du mang tham so //include //include void printarray (int arg[100], int length) { int i; for (i=0; i
  15. 5 10 15 2 4 6 8 10 Tham số cùa mảng ở hai ví dụ trên được truyền theo tham trị khi gọi printarray (firstarray,3); Nghĩa là ba phần tử đầu tiên của mảng arg được gán 3 giá trị tương ứng của các phần tử mảng firstarray. Khi in ra, chúng ta cũng giới hạn là chỉ in ra ba phần tử đầu tiên của màng arg cho nên các giá trị tiếp theo của mảng arg không cần phải quan tâm. Tương tự với mảng secondarray. Vậy một câu hỏi đặt ra là với ví dụ sau, thì kết quả sẽ là gì? (ví dụ này giới hạn số phần tử của arg là 2). Ví dụ 3.1.6: // Vi du mang tham so #include #include void printarray (int arg[2], int length) { int i; for (i=0; i
  16. sẽ truy cập đến một vùng nhớ bên ngoài màng. Tức là các giá trị của firstarray sau khi truyền cho arg[0], arg[l] thì nó lại truyền tiếp cho các ô nhớ bên cạnh hai phần tử này. Vì length = 3 cho nên vòng for vẫn in ra tất các giá trị của ba ô nhớ arg[0], arg[l] và vùng nhớ kế tiếp bên ngoài mảng. Tuy nhiên, việc khai báo và truyền tham trị như thế này rất nguy hiểm ở chỗ không kiểm soát được là các ô nhớ sau arg[l] còn trống hay chứa một giá trị nào đó rồi, cho nên tốt nhất là tránh trường hợp kiểu như trên xảy ra. 3.1.2. Xâu ký tự 3.1.2.1. Khai báo và truy nhập vào phần tử của xâu Trong c xâu được xây dựng như là một mảng các kí tự kể cả khoảng trắng. Một hằng xâu có nội dung được đặt trong cặp “ ” và kết thúc xâu là kí tự ‘\0’ hay còn có tên là ký tự NULL, cần phải phân biệt kí tự ‘a’ và xâu “a”. Xâu “a” gồm hai kí tự là ‘a’và ’\0’ Có hai khai báo một xâu: • Dùng màng kí tự: Char a[size]; • Dùng con trỏ kí tự: Char * str; Do vậy nên khi khởi tạo một xâu có thể dùng cách khởi tạo của mảng hoặc dùng cách khởi tạo của con trỏ như đã giới thiệu ở các phần trên: • char *str • char *str = (char*) malloc (9*sizeof (char)) Do xâu ký tự được xem như là một màng các kí tự nên để truy nhập vào từng phần tử của xâu ta truy cập như đối với mảng. Ví dụ char a[l 0] = “hi”; hoặc chara[10] = {‘h’,’i’,’\0’}; 92
  17. n Tương đương với a[0] = ‘h’, a[l] =’i’, a[2] = ‘\0 hoặc char *str=”hello world”; 3.I.2.2. Nhập và xuất dữ liệu cho xâu kỷ tự Đe nhập giá trị cho một xâu, có thể dùng các cách sau: • Khỏi tạo giá trị mặc định: Char *str = “Hello”; • Nhập bằng scanf như ví dụ sau: Ví dụ char *s = (char*) malloc (9*sizeof (char)); for(i =0; i< 9; i++) scanf(“%c”, s+i); Tuy nhiên, như chúng ta đã biết do cơ chế làm việc của vùng nhớ đệm nên việc nhập dữ liệu là các ký tự bằng hàm scanf này rất có thể nhận được kết quả không theo mong muốn. Vì vậy để đoạn chương trình trên chạy đúng theo yêu cầu thì ta nên có lệnh làm sạch vùng đệm. Lúc đó ta viết lại đoạn chương trình như sau: char *s = (char*) malloc (9*sizeof (char)); for(i =0; i< 9; i++) { fflush(stdin) ; scanf(“%c”, s+i); } Nhận xét: Từ ví dụ trên ta thấy nhược điểm dài dòng và phức tạp của việc sử dụng hàm scanf trong quá trình nhập dữ liệu cho xâu. Vì vậy ta nên hạn chế cách dùng này trong việc nhập dữ liệu cho xâu. Và để khắc phục những nhược điểm đó, c đã cung cấp một hàm phục vụ cho việc nhập dữ liệu cho xâu. Đó là hàm gets mà ta giới thiệu ngay sau đây. 93
  18. • Hàm gets Cú pháp char * gets(char *sfr); Hàm gets cho phép nhập vào một dãy kí tự cho đến khi gặp kí hiệu ‘\n’. Kí tự ‘\n’ bị loại khỏi stdin và không được đặt vào chuỗi sfr, để đánh dấu kết thúc xâu, trình biên dịch sẽ thêm vào cuối str là kí tự ‘\0’ Chú ý: Các hàm nhập xâu hay nhập kí tự sau khi nhập, bấm Enter để kết thúc, để lại kí tự ‘\n’ trên dòng nhập, vì vậy nó sẽ làm trôi các hàm nhập nói trên nếu sử dụng nhiều lần. Vì thế cần phải làm sạch vùng đệm trước khi gọi các hàm này bằng hàm fflush(stdin); Khi nhập bằng scanf, có thể loại bỏ ‘\n’ bằng cách sau: type t; scanf(“%%*c”, &t); Trong tất cả các cách nhập xâu như đã nêu, cách phổ biến hay sử dụng nhất đó là dùng gets. Còn khi xuất một xâu ra màn hình ta có thể dùng hàm printf hoặc hàm puts. Sự khác nhau giữa hai hàm này là puts sau khi in xong xâu ký tự sẽ đưa con trỏ màn hình về đầu dòng tiếp theo. Ví dụ printf(“Hello world”); hoặc khi sử dụng mảng: int i; chara[10] = {‘h’,’i’,’\0’}; for(i=0;i
  19. hoặc khi sử dụng con trỏ char *str - ’Hello world”; có hai cách in. Cách thứ nhất có thể in như in mảng như trên. Cách thứ hai đó là dùng hàm strlen. Để sử dụng hàm này, đầu chương trình phải chèn file string.h. Sau đó thì viết lệnh in ra như sau: for(i=0;i
  20. { tmp[i]=s[n-i-l]; ++i; } tmp[i]=O; return tmp; } void main() { char hello[] = "Hello World"; char *s; printf("\nChuoi ban dau = %s", hello); s = dnchuoi(hello); printf("\nChuoi dao nguoc = %s", s); getch(); } Ví dụ 3.1.8: Viết lại hàm tính độ dài của xâu. Sử dụng hàm này để khi nhập vào một xâu, in ra được độ dài của xâu đó là bao nhiêu. #include #include #include #include intlen(char *str) { int count=0; char temp; int i =0; emp = str[O); while(temp !='\0') { count ++; temp = str[++i]; 96
nguon tai.lieu . vn