Xem mẫu
- 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
- - 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
- 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
- Đ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
- [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
- 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
- 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
- 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
- Để 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
- for (i=l; i< n; i++)
{
if (a[i]
- 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
- scaní("%d",&B[i]ũ]);
}
// phan in ra
printf("\n Ma ưan vua nhap la:\n");
for(i=0;i
- 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
- 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
- 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
- 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
- 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
- • 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
- 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
- {
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