Tác giả phạm hồng thái bài giảng ngôn ngữ LẬp trình c/C++



tải về 1.98 Mb.
trang26/55
Chuyển đổi dữ liệu07.07.2016
Kích1.98 Mb.
1   ...   22   23   24   25   26   27   28   29   ...   55

Hàm và mảng dữ liệu

Truyền mảng 1 chiều cho hàm


Thông thường chúng ta hay xây dựng các hàm làm việc trên mảng như vectơ hay ma trận các phần tử. Khi đó tham đối thực sự của hàm sẽ là các mảng dữ liệu này. Trong trường hợp này ta có 2 cách khai báo đối. Cách thứ nhất đối được khai báo bình thường như khai báo biến mảng nhưng không cần có số phần tử kèm theo, ví dụ:

int x[];


float x[];

Cách thứ hai khai báo đối như một con trỏ kiểu phần tử mảng, ví dụ:

int *p;

float *p


Trong lời gọi hàm tên mảng a sẽ được viết vào danh sách tham đối thực sự, vì a là địa chỉ của phần tử đầu tiên của mảng a, nên khi hàm được gọi địa chỉ này sẽ gán cho con trỏ p. Vì vậy giá trị của phần tử thứ i của a có thể được truy cập bởi x[i] (theo khai báo 1) hoặc *(p+i) (theo khai báo 2) và nó cũng có thể được thay đổi thực sự (do đây cũng là cách truyền theo dẫn trỏ).

Sau đây là ví dụ đơn giản, nhập và in vectơ, minh hoạ cho cả 2 kiểu khai báo đối.

: Hàm nhập và in giá trị 1 vectơ

void nhap(int x[], int n) // n: số phần tử

{

int i;


for (i=0; i> x[i]; // hoặc cin >> *(x+i)

}

void in(int *p, int n)



{

int i;


for (i=0; i}

main()



{

int a[10] ; // mảng a chứa tối đa 10 phần tử

nhap(a,7); // vào 7 phần tử đầu tiên cho a

in(a,3); // ra 3 phần tử đầu tiên của a

}

Truyền mảng 2 chiều cho hàm


Đối với mảng 2 chiều khai báo đối cũng như lời gọi là phức tạp hơn nhiều so với mảng 1 chiều. Ta có hai cách khai báo đối như sau:

Khai báo theo đúng bản chất của mảng 2 chiều float x[m][n] do C++ qui định, tức x là mảng 1 chiều m phần tử, mỗi phần tử của nó có kiểu float[n]. Từ đó, đối được khai báo như một mảng hình thức 1 chiều (khồng cần số phần tử - ở đây là số dòng) của kiểu float[n]. Tức có thể khai báo như sau:

float x[][n] ; // mảng với số phần tử không định trước, mỗi phần tử là n số

float (*x)[n] ; // một con trỏ, có kiểu là mảng n số (float[n])

Để truy nhập đến đến phần tử thứ i, j ta vẫn sử dụng cú pháp x[i][j]. Tên của mảng a được viết bình thường trong lời gọi hàm. Nói chung theo cách khai báo này việc truy nhập là đơn giản nhưng phương pháp cũng có hạn chế đó là số cột của mảng truyền cho hàm phải cố định bằng n.

Xem mảng float x[m][n] thực sự là mảng một chiều float x[m*n] và sử dụng cách khai báo như trong mảng một chiều, đó là sử dụng con trỏ float *p để truy cập được đến từng phần tử của mảng. Cách này có hạn chế trong lời gọi: địa chỉ truyền cho hàm không phải là mảng a mà cần phải ép kiểu về (float*) (để phù hợp với p). Với cách này gọi k là thứ tự của phần tử a[i][j] trong mảng một chiều (m*n), ta có quan hệ giữa k, i, j như sau:

k = *(p + i*n + j)

i = k/n


j = k%n

trong đó n là số cột của mảng truyền cho hàm. Điều này có nghĩa để truy cập đến a[i][j] ta có thể viết *(p+i*n+j), ngược lại biết chỉ số k có thể tính được dòng i, cột j của phần tử này. Ưu điểm của cách khai báo này là ta có thể truyền mảng với kích thước bất kỳ (số cột không cần định trước) cho hàm.

Sau đây là các ví dụ minh hoạ cho 2 cách khai báo trên.

: Tính tổng các số hạng trong ma trận

float tong(float x[][10], int m, int n) // hoặc float tong(float (*x)[10], int m, int n)

{ // m: số dòng, n: số cột

float t = 0;

int i, j ;

for (i=0; i

for (j=0; j

return t;

}
main()

{

float a[8][10], b[5][7] ;



int i, j, ma, na, mb, nb;

cout << "nhập số dòng, số cột ma trận a: " ; cin >> ma >> na;

for (i=0; i

for (j=0; j

{ cout << "a[" << i << "," << j << "] = " ; cin >> a[i][j] ; }

cout << "nhập số dòng, số cột ma trận b: " ; cin >> mb >> nb;

for (i=0; i

for (j=0; j

{ cout << "b[" << i << "," << j << "] = " ; cin >> b[i][j] ; }

cout << tong(a, ma, na); // in tổng các số trong ma trận

cout << tong(b, mb, nb); // sai vì số cột của b khác 10

}

: Tìm phần tử bé nhất của ma trận



void minmt(float *x, int m, int n) // m: số dòng, n: số cột

{

float min = *x; // gán phần tử đầu tiên cho min



int k, kmin;

for (k=1; k

if (min > *(x+k)) { min = *(x+k) ; kmin = k; }

cout << "Giá trị min la: " << min << " tại dòng " << k/n << " cột " << k%n;

}
main()

{

float a[8][10], b[5][7] ;



int i, j ;

for (i=0; i<8; i++) // nhập ma trận a

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

{ cout << "a[" << i << "," << j << "] = " ; cin >> a[i][j] ; }

for (i=0; i<5; i++) // nhập ma trận b

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

{ cout << "b[" << i << "," << j << "] = " ; cin >> b[i][j] ; }

minmt((float*)a, 8, 10) ; // in giá trị và vị trí số bé nhất trong a

minmt((float*)b, 5, 7) ; // in giá trị và vị trí số bé nhất trong b

}

: Cộng 2 ma trận và in kết quả.



void inmt(float *x, int m, int n)

{

int i, j;



for (i=0; i

{

for (j=0; j

cout << endl;

}

}


void cong(float *x, float *y, int m, int n)

{

float *t = new float[m*n]; // t là ma trận kết quả (xem như dãy số)



int k, i, j ;

for (k = 0; k < m*n; k++) *(t+k) = *(x+k) + *(y+k) ;

inmt((float*)t, m, n);

}
main()

{

float a[8][10], b[5][7] ;



int i, j, m, n;

cout << "nhập số dòng, số cột ma trận: " ; cin >> m >> n;

for (i=0; i

for (j=0; j

{

cout << "a[" << i << "," << j << "] = " ; cin >> a[i][j] ;



cout << "b[" << i << "," << j << "] = " ; cin >> b[i][j] ;

}

cong((float*)a, (float*)b, m, n); // cộng và in kết quả a+b



}

Xu hướng chung là chúng ta xem mảng (1 hoặc 2 chiều) như là một dãy liên tiếp các số trong bộ nhớ, tức một ma trận là một đối con trỏ trỏ đến thành phần của mảng. Đối với mảng 2 chiều m*n khi truyền đối địa chỉ của ma trận cần phải ép kiểu về kiểu con trỏ. Ngoài ra bước chạy k của con trỏ (từ 0 đến m*n-1) tương ứng với các toạ độ của phần tử a[i][j] trong mảng như sau:

k = *(p + i*n + j)

i = k/n


j = k%n

từ đó, chúng ta có thể viết các hàm mà không cần phải băn khoăn gì về kích thước của ma trận sẽ truyền cho hàm.


Giá trị trả lại của hàm là một mảng


Không có cách nào để giá trị trả lại của một hàm là mảng. Tuy nhiên thực sự mỗi mảng cũng chính là một con trỏ, vì vậy việc hàm trả lại một con trỏ trỏ đến dãy dữ liệu kết quả là tương đương với việc trả lại mảng. Ngoài ra còn một cách dễ dùng hơn đối với mảng 2 chiều là mảng kết quả được trả lại vào trong tham đối của hàm (giống như nghiệm của phương trình bậc 2 được trả lại vào trong các tham đối). Ở đây chúng ta sẽ lần lượt xét 2 cách làm việc này.

Giá trị trả lại là con trỏ trỏ đến mảng kết quả. Trước hết chúng ta xét ví dụ nhỏ sau đây:

int* tragiatri1() // giá trị trả lại là con trỏ trỏ đến dãy số nguyên

{

int kq[3] = { 1, 2, 3 }; // tạo mảng kết quả với 3 giá trị 1, 2, 3



return kq ; // trả lại địa chỉ cho con trỏ kết quả hàm

}
int* tragiatri2() // giá trị trả lại là con trỏ trỏ đến dãy số nguyên

{

int *kq = new int[4]; // cấp phát 3 ô nhớ nguyên



*kq = *(kq+1) = *(kq+2) = 0 ; // tạo mảng kết quả với 3 giá trị 1, 2, 3

return kq ; // trả lại địa chỉ cho con trỏ kết quả hàm

}
main()

{

int *a, i;



a = tragiatri1();

for (i=0; i<3; i++) cout *(a+i); // không phải là 1, 2, 3

a = tragiatri2();

for (i=0; i<3; i++) cout *(a+i); // 1, 2, 3

}

Qua ví dụ trên ta thấy hai hàm trả giá trị đều tạo bên trong nó một mảng 3 số nguyên và trả lại địa chỉ mảng này cho con trỏ kết quả hàm. Tuy nhiên, chỉ có tragiatri2() là cho lại kết quả đúng. Tại sao ? Xét mảng kq được khai báo và khởi tạo trong tragiatri1(), đây là một mảng cục bộ (được tạo bên trong hàm) như sau này chúng ta sẽ thấy, các loại biến "tạm thời" này (và cả các tham đối) chỉ tồn tại trong quá trình hàm hoạt động. Khi hàm kết thúc các biến này sẽ mất đi. Do vậy tuy hàm đã trả lại địa chỉ của kq trước khi nó kết thúc, thế nhưng sau khi hàm thực hiện xong, toàn bộ kq sẽ được xoá khỏi bộ nhớ và vì vậy con trỏ kết quả hàm đã trỏ đến vùng nhớ không còn các giá trị như kq đã có. Từ điều này việc sử dụng hàm trả lại con trỏ là phải hết sức cẩn thận. Muốn trả lại con trỏ cho hàm thì con trỏ này phải trỏ đến dãy dữ liệu nào sao cho nó không mất đi sau khi hàm kết thúc, hay nói khác hơn đó phải là những dãy dữ liệu được khởi tạo bên ngoài hàm hoặc có thể sử dụng theo phương pháp trong hàm tragiatri2(). Trong tragiatri2() một mảng kết quả 3 số cũng được tạo ra nhưng bằng cách xin cấp phát vùng nhớ. Vùng nhớ được cấp phát này sẽ vẫn còn tồn tại sau khi hàm kết thúc (nó chỉ bị xoá đi khi sử dụng toán tử delete). Do vậy hoạt động của tragiatri2() là chính xác.



Tóm lại, ví dụ trên cho thấy nếu muốn trả lại giá trị con trỏ thì vùng dữ liệu mà nó trỏ đến phải được cấp phát một cách tường minh (bằng toán tử new), chứ không để chương trình tự động cấp phát và tự động thu hồi. Ví dụ sau minh hoạ hàm cộng 2 vectơ và trả lại vectơ kết quả (thực chất là con trỏ trỏ đến vùng nhớ đặt kết quả)

int* congvt(int *x, int *y, int n) // n số phần tử của vectơ

{

int* z = new int[n]; // xin cấp phát bộ nhớ



for (int i=0; i

return c;

}
main()

{

int i, n, a[10], b[10], c[10] ;



cout << "n = " ; cin >> n; // nhập số phần tử

for (i=0; i> a[i] ; // nhập vectơ a

for (i=0; i> b[i] ; // nhập vectơ b

c = congvt(a, b, n);

for (i=0; i

}

Chú ý: a[i], b[i], c[i] còn được viết dưới dạng tương đương *(a+i), *(b+i), *(c+i).

Trong cách này, mảng cần trả lại được khai báo như một tham đối trong danh sách đối của hàm. Tham đối này là một con trỏ nên hiển nhiên khi truyền mảng đã khai báo sẵn (để chứa kết quả) từ ngoài vào cho hàm thì mảng sẽ thực sự nhận được nội dung kết quả (tức có thay đổi trước và sau khi gọi hàm - xem mục truyền tham đối thực sự theo dẫn trỏ). Ở đây ta xét 2 ví dụ: bài toán cộng 2 vectơ trong ví dụ trước và nhân 2 ma trận.

: Cộng 2 vectơ, vectơ kết quả trả lại trong tham đối của hàm. So với ví dụ trước giá trị trả lại là void (không trả lại giá trị) còn danh sách đối có thêm con trỏ z để chứa kết quả.

void congvt(int *x, int *y, int *z, int n) // z lưu kết quả

{

for (int i=0; i

}
main()

{

int i, n, a[10], b[10], c[10] ;



cout << "n = " ; cin >> n; // nhập số phần tử

for (i=0; i> a[i] ; // nhập vectơ a

for (i=0; i> b[i] ; // nhập vectơ b

congvt(a, b, c, n);

for (i=0; i

}

: Nhân 2 ma trận kích thước m*n và n*p. Hai ma trận đầu vào và ma trận kết quả (kích thước m*p) đều được khai báo dưới dạng con trỏ và là đối của hàm nhanmt(). Nhắc lại, trong lời gọi hàm địa chỉ của 3 mảng cần được ép kiểu về (int*) để phù hợp với các con trỏ tham đối.



void nhanmt(int *x, int *y, int *z, int m, int n, int p) // z lưu kết quả

{

int i, j, k ;



for (i=0; i

for (j=0; j

{

*(z+i*p+j) = 0; // tức z[i][j] = 0



for (k=0; k

*(z+i*p+j) += *(x+i*n+k)**(y+k*p+j) ; // tức z[i][j] += x[i][k]*y[k][j]

}

}
main()



{

int a[10][10], b[10][10], c[10][10] ; // khai báo 3 mảng a, b, c

int m, n, p ; // kích thước các mảng

cout << "m, n, p = " ; cin >> m >> n >> p ; // nhập số phần tử

for (i=0; i

for (j=0; j

cout << "a[" << i << "," << j << "] = " ; cin >> a[i][j] ;

for (i=0; i

for (j=0; j

cout << "b[" << i << "," << j << "] = " ; cin >> b[i][j] ;

nhanmt((int*)a, (int*)b, (int*)c, m, n, p); // gọi hàm

for (i=0; i

{

for (j=0; j



cout << endl;

}

}


Đối và giá trị trả lại là xâu kí tự


Giống các trường hợp đã xét với mảng 1 chiều, đối của các hàm xâu kí tự có thể khai báo dưới 2 dạng: mảng kí tự hoặc con trỏ kí tự. Giá trị trả lại luôn luôn là con trỏ kí tự. Ngoài ra hàm cũng có thể trả lại giá trị vào trong các đối con trỏ trong danh sách đối.

Ví dụ sau đây dùng để tách họ, tên của một xâu họ và tên. Ví dụ gồm 3 hàm. Hàm họ trả lại xâu họ (con trỏ kí tự) với đối là xâu họ và tên được khai báo dạng mảng. Hàm tên trả lại xâu tên (con trỏ kí tự) với đối là xâu họ và tên được khai báo dạng con trỏ kí tự. Thực chất đối họ và tên trong hai hàm họ, tên có thể được khai báo theo cùng cách thức, ở đây chương trình muốn minh hoạ các cách khai báo đối khác nhau (đã đề cập đến trong phần đối mảng 1 chiều). Hàm thứ ba cũng trả lại họ, tên nhưng cho vào trong danh sách tham đối, do vậy hàm không trả lại giá trị (void). Để đơn giản ta qui ước xâu họ và tên không chứa các dấu cách đầu và cuối xâu, trong đó họ là dãy kí tự từ đầu cho đến khi gặp dấu cách đầu tiên và tên là dãy kí tự từ sau dấu cách cuối cùng đến kí tự cuối xâu.

char* ho(char hoten[]) // hàm trả lại họ

{

char* kq = new char[10]; // cấp bộ nhớ để chứa họ



int i=0;

while (hoten[i] != '\40') i++; // i dừng tại dấu cách đầu tiên

strncpy(kq, hoten, i) ; // copy i kí tự của hoten vào kq

return kq;

}

char* ten(char* hoten) // hàm trả lại tên



{

char* kq = new char[10]; // cấp bộ nhớ để chứa tên

int i=strlen(hoten);

while (hoten[i] != '\40') i--; // i dừng tại dấu cách cuối cùng

strncpy(kq, hoten+i+1, strlen(hoten)-i-1) ; // copy tên vào kq

return kq;

}
void tachht(char* hoten, char* ho, char* ten)

{

int i=0;



while (hoten[i] != '\40') i++; // i dừng tại dấu cách đầu tiên

strncpy(ho, hoten, i) ; // copy i kí tự của hoten vào ho

i=strlen(hoten);

while (hoten[i] != '\40') i--; // i dừng tại dấu cách cuối cùng

strncpy(ten, hoten+i+1, strlen(hoten)-i-1) ; // copy tên vào ten

}
main()

{

char ht[30], *h, *t ; // các biến họ tên, họ, tên



cout << "Họ và tên = " ; cin.getline(ht,30) ; // nhập họ tên

h = ho(ht); t = ten(ht);

cout << "Họ = " << h << ", tên = " << t << endl;

tachht(ht, h, t);

cout << "Họ = " << h << ", tên = " << t << endl;

}

Đối là hằng con trỏ


Theo phần truyền đối cho hàm ta đã biết để thay đổi biến ngoài đối tương ứng phải được khai báo dưới dạng con trỏ. Tuy nhiên, trong nhiều trường hợp các biến ngoài không có nhu cầu thay đổi nhưng đối tương ứng với nó vẫn phải khai báo dưới dạng con trỏ (ví dụ đối là mảng hoặc xâu kí tự). Điều này có khả năng do nhầm lẫn, các biến ngoài này sẽ bị thay đổi ngoài ý muốn. Trong trường hợp như vậy để cẩn thận, các đối con trỏ nếu không muốn thay đổi (chỉ lấy giá trị) cần được khai báo như là một hằng con trỏ bằng cách thêm trước khai báo kiểu của chúng từ khoá const. Từ khoá này khẳng định biến tuy là con trỏ nhưng nó là một hằng không thay đổi được giá trị. Nếu trong thân hàm ta cố tình thay đổi chúng thì chương trình sẽ báo lỗi. Ví dụ đối hoten trong cả 3 hàm ở trên có thể được khai báo dạng const char* hoten.

: Đối là hằng con trỏ. In hoa một xâu kí tự

void inhoa(const char* s)

{

char *t;



strcpy(t, s);

cout << s << strupr(t); // không dùng được strupr(s)

}

main()


{

char *s = "abcde" ;

inhoa(s); // abcdeABCDE

}

1   ...   22   23   24   25   26   27   28   29   ...   55


Cơ sở dữ liệu được bảo vệ bởi bản quyền ©hocday.com 2016
được sử dụng cho việc quản lý

    Quê hương