Trong bài này, bạn sẽ học cách khai báo cấu trúc (structure) và khái niệm kiểu dữ liệu trừu tượng (abstract data type, ADT). Ngoài ra, bạn sẽ học thêm một số chức năng khác của trình biên dịch C, bao gồm: các chỉ thị tiền xử lý (preprocessor directives), cách biên dịch nhiều file.
I. Cấu trúc và kiểu dữ liệu trừu tượng:
1. Khái niệm kiểu dữ liệu trừu tượng và cấu trúc:
Bạn đã học một số kiểu dữ liệu trong ngôn ngữ lập trình C, ví dụ: kiểu nguyên (int), kiểu thực (double), kiểu ký tự (char), kiểu con trỏ (pointer như void*, char*, int*, ...). Khi có kiểu dữ liệu, bạn có thể khai báo biến số thuộc kiểu dữ liệu đó (ví dụ có thể khai báo "int x;" để có một biến x kiểu số nguyên và ánh xạ vào trong bộ nhớ bởi 4 bytes).
Tuy nhiên, cho đến nay bạn chỉ có thể sử dụng các kiểu dữ liệu đã được định nghĩa sẵn. Trong các vấn đề thực tế, nhiều khi ta cần các kiểu dữ liệu do ta tự định nghĩa, ví dụ: khi bạn muốn viết một chương trình game trong không gian 2 chiều, bạn rất hay xử lý các dữ liệu liên quan đến các điểm trong không gian đó vì thế cần có 1 kiểu dữ liệu mới là kiểu điểm (point) bao gồm 2 phần tử (x, y) là 2 tọa độ. Hai phần tử cấu tạo nên kiểu điểm (tạm gọi là kiểu Point2D) có thể là 2 số nguyên, 2 số thực ... Như vậy, kiểu Point2D là một kiểu dữ liệu mới, được cấu thành từ 2 phần tử của kiểu nguyên đã được định nghĩa sẵn trong C.
Các kiểu dữ liệu không được định nghĩa sẵn trong ngôn ngữ lập trình mà do người lập trình tự định nghĩa từ những kiểu cơ bản có sẵn hoặc các kiểu tự định nghĩa khác gọi là kiểu dữ liệu trừu tượng (abstract data type, ADT).
Trong ví dụ trên, kiểu Point2D là một kiểu dữ liệu trừu tượng cấu thành từ 2 phần tử kiểu int. Trong ngôn ngữ lập trình C, bạn có thể sử dụng kiểu cấu trúc (construct) để định nghĩa những kiểu dữ liệu trừu tượng này, cách định nghĩa kiểu Point2D như sau:
struct Point2D {
int x;
int y;
};
Sau khi định nghĩa cấu trúc Point2D, ta có thể khai báo các biến thuộc kiểu cấu trúc này như sau:
struct Point2D p, q;
Các biến p, q là các biến kiểu cấu trúc Point2D. Các biến này là các điểm trong không gian 2 chiều, có 2 thành phần là x và y. Hai thành phần này được truy cập (đọc / ghi) thông qua toán tử dot (dấu chấm) như sau:
p.x = 1;
p.y = 2;
printf( "Point p = (%d, %d)\n", p.x, p.y );
Toán tử dot gọi là "toán tử truy cập nội dung cấu trúc".
Ngoài ra, bạn có thể khai báo con trỏ kiểu cấu trúc Point2D (giống như khi có kiểu int có thể khai báo kiểu int*) như sau:
struct Point2D *p;
p = (struct Point2D *)malloc(sizeof( struct Point2D ) );
p->x = 1;
(*p).y = 2;
printf( "Point (%d, %d)\n", p->x, p->y );
Với con trỏ trỏ đến một kiểu cấu trúc, bạn có thể truy cập thành phần của cấu trúc đó thông qua con trỏ nhờ một toán tử đặc biệt, gọi là toán tử mũi tên ("->") (xem ví dụ ở trên).
Như vậy, ta thấy "struct Point2D" hoàn toàn tương đương với một kiểu dữ liệu được định nghĩa sẵn (như int, double, ...) vì ta có thể khai báo các biến thuộc kiểu struct Point2D, hơn nữa ta cũng có thể khai báo các con trỏ thuộc kiểu đó bằng cách thêm dấu sao (*).
Tuy nhiên, "struct Point2D" chỉ là một kiểu cấu trúc, chưa phải là một kiểu dữ liệu mới (vì nó vẫn là kiểu con của kiểu cấu trúc). Trong phần tới ta sẽ học cách định nghĩa kiểu dữ liệu mới.
2. Cách định nghĩa kiểu dữ liệu mới:
Ngôn ngữ lập trình C cho phép định nghĩa kiểu dữ liệu mới dựa trên kiểu dữ liệu đã được định nghĩa trước. Sau khi định nghĩa kiểu mới, ta có thể dùng kiểu dữ liệu này giống như mọi kiểu thông thường.
Từ khóa typedef dùng để định nghĩa kiểu dữ liệu mới, cú pháp định nghĩa như sau:
type_definition ::= "typedef" old_datatype new_datatype
Ví dụ, ta có thể định nghĩa kiểu SoNguyen bằng cách sau:
typedef int SoNguyen;
SoNguyen x, y;
SoNguyen *pX;
x = 1;
pX = (SoNguyen*)malloc( sizeof( SoNguyen ) );
*pX = 2;
printf( "x = %d, *pX = %d\n", x, *pX );
Khi đó kiểu SoNguyen hoàn toàn giống với kiểu int.
Vì thế, ta có thể dùng từ khóa typedef để khai báo kiểu dữ liệu mới Point2D dựa trên kiểu cấu trúc struct _Point2D (thêm dấu gạch vào identifier Point2D để cho đỡ nhầm giữa kiểu dữ liệu Point2D và cấu trúc _Point2D):
#include < stdio.h >
int main() {
struct _Point2D {
int x;
int y;
};
typedef struct _Point2D Point2D;
Point2D p, q;
Point2D *r;
r = (Point2D*)malloc( sizeof(Point2D) );
p.x = 1; p.y = 2;
r->x = 1; r->y = 2;
printf( "p = (%d, %d)\n", p.x, p.y );
printf( "r = (%d, %d)\n", r->x, (*r).y );
return 0;
}
Ngoài ra, ta có thể vừa định nghĩa cấu trúc, vừa khai báo kiểu dữ liệu mới như sau:
#include < stdio.h >
int main() {
typedef struct _Point2D {
int x;
int y;
} Point2D;
Point2D p, q;
Point2D *r;
r = (Point2D*)malloc( sizeof(Point2D) );
p.x = 1; p.y = 2;
r->x = 1; r->y = 2;
printf( "p = (%d, %d)\n", p.x, p.y );
printf( "r = (%d, %d)\n", r->x, (*r).y );
return 0;
}
Hơn nữa, ta có thể bỏ qua không cần khai báo tên của cấu trúc (_Point2D) mà chỉ cần khai báo tên của kiểu dữ liệu mới:
#include < stdio.h >
int main() {
typedef struct {
int x;
int y;
} Point2D;
Point2D p, q;
Point2D *r;
r = (Point2D*)malloc( sizeof(Point2D) );
p.x = 1; p.y = 2;
r->x = 1; r->y = 2;
printf( "p = (%d, %d)\n", p.x, p.y );
printf( "r = (%d, %d)\n", r->x, (*r).y );
return 0;
}
Bạn hãy thử nghĩ xem trong các ví dụ trên, ở câu lệnh typedef thì old_datatype là gì và new_datatype là gì?.
3. Một số ví dụ về kiểu dữ liệu trừu tượng:
Kiểu dữ liệu trừu tượng rất có ích khi phải mô hình hóa những vấn đề phức tạp, nó giúp việc xử lý những dữ liệu phức tạp trở nên đơn giản hơn. Trong phần này, ta sẽ xem xét một số ví dụ để thấy rõ lợi ích của kiểu dữ liệu trừu tượng.
3.1. Mô hình các điểm trong hệ tọa độ 2 chiều:
Giả sử ta phải viết chương trình nhập vào tọa độ của 2 điểm và tính khoảng cách giữa 2 điểm đó. Nếu không có kiểu dữ liệu trừu tượng Point2D ta có thể làm như sau:
/*
File point_demo1.c
Compile with -lm: $ gcc -o point_demo1 point_demo1.c -lm
*/
#include < stdio.h >
#include < math.h >
double distance( int x1, int y1, int x2, int y2 )
{
double r, retVal;
r = (x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2);
retVal = sqrt( r );
return retVal;
}
int main() {
int x1, y1;
int x2, y2;
printf( "Point1: (x, y) = " );
scanf( "%d %d", &x1, &y1 );
printf( "Point2: (x, y) = " );
scanf( "%d %d", &x2, &y2 );
printf( "Distance = %lf\n", distance( x1, y1, x2, y2 ) );
return 0;
}
Ta dùng 4 biến nguyên x1, y1 và x2, y2 là tọa độ của 2 điểm nhập vào. Tuy nhiên, không có gì đảm bảo là x1 luôn luôn đi với y1 để tạo thành tọa độ điểm p1, x2 luôn luôn đi với y2 để tạo thành tọa độ điểm p2. Ví dụ, ta có thể nhầm lẫn (x1, x2) là tọa độ của điểm p1 và (y1, y2) là tọa độ của điểm p2. Hoặc ta cũng có thể nhầm (x1, y2) là tọa độ của điểm p1, ...
Khi dùng kiểu dữ liệu trừu tượng Point2D ta có thể tránh được những nhầm lẫn này, vì các tọa độ luôn được ghép đúng cặp với nhau:
/*
File point_demo2.c
Compile with -lm: $ gcc -o point_demo2 point_demo2.c -lm
*/
#include < stdio.h >
#include < math.h >
typedef struct _Point2D {
int x;
int y;
} Point2D;
double distance( Point2D p, Point2D q )
{
double r, retVal;
r = (p.x - q.x) * (p.x - q.x) + (p.y - q.y) * (p.y - q.y);
retVal = sqrt( r );
return retVal;
}
int main()
{
Point2D p1, p2;
printf( "Point1 (x, y) = " );
scanf( "%d %d", &p1.x, &p1.y );
printf( "Point2 (x, y) = " );
scanf( "%d %d", &p2.x, &p2.y );
printf( "Distance = %lf\n", distance( p1, p2 ) );
return 0;
}
Vì các biến x, y ở trong cấu trúc Point2D nên không thể nhầm lẫn ghép tọa độ x của p1 với y của p2. Ngoài ra, hàm distance cũng chỉ cần 2 tham số là 2 điểm (không cần tới 4 tham số int).
Khi mô hình hóa những bài toán phức tạp như mạch điện, quản lý sinh viên, mạng lưới thành phố, ... thì kiểu dữ liệu trừu tượng không thể thiếu vì nó làm chương trình dễ đọc và dễ hiểu.
3.2. Kiểu dữ liệu trừu tượng cho ngăn xếp (stack):
Trong bài trước bạn đã biết ngăn xếp dùng khi gọi hàm, nó có tác dụng kỳ diệu là thứ tự push vào và thứ tự pop ra ngược với nhau nên có thể dùng để đảo thứ tự một dãy. Ở phần này ta sẽ implement một ngăn xếp đơn giản bằng cách khai báo kiểu dữ liệu trừu tượng Stack.
Kiểu Stack gồm 2 thành phần: thành phần thứ nhất là một biến nguyên (int) để mô tả vị trí hiện thời của đỉnh ngăn xếp, thành phần thứ 2 là một mảng tĩnh các số nguyên để chứa các phần tử của ngăn xếp.
Ta dùng một preprocessor directive mới có tên là "#define" để định nghĩa một hằng số MAX_SIZE là số phần tử lớn nhất có thể cho vào ngăn xếp. Trình biên dịch sẽ thay thế MAX_SIZE bằng số được định nghĩa ở phần sau của #define mỗi khi gặp identifier "MAX_SIZE".
#include < stdio.h >
#define MAX_SIZE 10
typedef struct {
int m_nCurPos; // member m_nCurPos is current position of stack's top
int m_arrElems[MAX_SIZE]; // array of elements
} Stack;
void push( Stack* stk, int elem )
{
if( stk->m_nCurPos >= MAX_SIZE ) {
printf( "Error: Stack is full!\n" );
} else {
stk->m_nCurPos ++;
stk->m_arrElems[stk->m_nCurPos] = elem;
}
}
int pop( Stack* stk )
{
int retVal;
if( stk->m_nCurPos < 0 ) {
printf( "Error: stack is empty\n" );
return 0;
} else {
retVal = stk->m_arrElems[stk->m_nCurPos];
stk->m_nCurPos --;
return retVal;
}
}
int isEmpty( Stack* stk )
{
if( stk->m_nCurPos < 0 ) return 1;
return 0;
}
int main()
{
int i, x;
Stack st;
st.m_nCurPos = -1;
for( i = 1; i < MAX_SIZE + 5; i++ ) {
printf( "Push %d into stack\n", i );
push( &st, i );
}
printf( "\n\n" );
while( ! isEmpty( &st ) ) {
printf( "pop( &st ) = %d\n", pop( &st ) );
}
printf( "Testing: pop( &st ) while st is empty\n" );
pop( &st );
return 0;
}
II. Các công cụ của trình biên dịch:
Đây là phần quan trọng cuối cùng bạn cần biết để lập trình với C. Phần này là phần lý thuyết cuối cùng của toàn bộ giáo trình này. Sau khi đọc xong phần này bạn coi như đã nắm được cơ bản ngôn ngữ C!.
1. Toán tử chọn ternary:
Toán tử chọn ternary là toán tử gồm 3 toán hạng, nếu toán hạng đầu có giá trị true, toán tử này trả về toán hạng thứ 2, ngược lại nó trả về toán hạng thứ 3. Toán tử này có cú pháp như sau:
ternary_selector ::= operand1 "?" operand2 ":" operand3
Như vậy toán tử này bao gồm toán hạng 1, đi theo sau bởi dấu chấm hỏi (?) rồi đến toán hạng 2, sau đó là dấu hai chấm (:) và cuối cùng là toán hạng 3. Vì là toán tử nên nó cũng có giá trị trả về, giá trị trả về như đã đề cập ở trên (trả về operand2 hoặc operand3 tùy vào điều kiện operand1 đúng hay sai).
Ví dụ:
int a, x;
a = 6;
x = ((a > 5) ? (a + 1) : (a - 1));
Sau khi thực hiện lệnh gán, x sẽ có giá trị bằng 7 vì a > 5 là đúng.
2. Một số chỉ thị tiền xử lý hữu dụng:
2.1. Chỉ thị #define:
Như bạn đã gặp ở phần trên, dạng đơn giản của chỉ thị #define có cú pháp như sau:
define_directive ::= "#define" identifier something
Chỉ thị này báo cho trình biên dịch C biết là khi gặp identifier sẽ phải thay thế bằng something. Vì nó là chỉ thị tiền xử lý nên trình biên dịch sẽ thực hiện việc thay thế trước khi biên dịch chương trình. Something có thể là bất kỳ cái gì bạn viết được trong editor. Ví dụ:
#include < stdio.h >
#define MAX_SIZE 100
#define HELLO "Xin chao!\n"
#define SoNguyen int
SoNguyen main()
{
SoNguyen x;
printf( HELLO );
printf( "max size is %d\n", MAX_SIZE );
x = 1;
printf( "x = %d\n", x );
return 0;
}
Chú ý: cần phân biệt giữa "typedef int SoNguyen;" và "#define SoNguyen int". Câu lệnh typedef được thực hiện khi biên dịch, còn chỉ thị define SoNguyen int được thực hiện trước khi biên dịch, nó chỉ đơn giản là thay thế chỗ nào có "SoNguyen" bằng từ "int".
Ở dạng phức tạp hơn, chỉ thị #define có cú pháp như sau:
define_directive ::= "#define" identifier ["(" formal_parameter_list ")] something
formal_parameter_list ::= empty | identifier ( ", " identifier)*
Khi có formal parameter (như khi khai báo hàm số), chỉ thị này sẽ thay thế những chỗ nào có formal parameter name ở trong something bằng giá trị thực của formal parameter đó:
#define max( a, b ) (( a > b ) ? a : b)
int x, y;
x = 5;
y = 6;
printf( "max(%d, %d) = %d\n", x, y, max(x, y) );
Quá trình thay thế đó gọi là quá trình "triển khai macro", định nghĩa của max được gọi là định nghĩa của 1 macro (lệnh gộp) và max được gọi là một macro (không phải là 1 hàm).
Trông max(x, y) gần giống như lời gọi hàm nhưng thực chất khác hẳn: trước khi thực hiện biên dịch, trình biên dịch sẽ thay thế max(x, y) bởi định nghĩa của nó, vì thế chương trình thực chất được biên dịch là:
int x, y;
x = 5;
y = 6;
printf( "max(%d, %d) = %d\n", x, y, (( x > y ) ? x : y ) );
Bạn phải hết sức cẩn thận khi sử dụng chỉ thị #define với tham số vì khi bộ tiền xử lý thay thế tham số có thể gây ra nhầm lẫn nếu không có đủ dấu ngoặc.
Ví dụ chương trình sau định tính bình phương của max nhưng sẽ sai vì phép nhân được hiểu sai:
#define max( a, b ) ( a > b ) ? a : b
int x, y;
x = 6;
y = 5;
printf( "max^2(%d, %d) = %d\n", x, y, max(x, y) * max(x, y) );
Sau khi triển khai macro max, ta có kết quả của biểu thức max(x, y) * max(x, y) là:
(x > y) ? x : y * (x > y) ? x : y
Như vậy operand thứ 3 của toán tử chọn ternary đầu tiên sẽ là y * (x > y) ? x : y, chứ không phải là y!.
2.2. Chỉ thị #if, #ifdef:
Chỉ thị #if và #ifdef dùng để hướng dẫn trình biên dịch lựa chọn hoặc bỏ qua một đoạn mã lệnh nào đó. Cú pháp của chỉ thị #if như sau:
if_directive ::= "#if" expression something ["#else" some_other_thing] "#endif"
ifdef_directive ::= "#ifdef" identifier something ["#else" some_other_thing] "#endif"
Bộ tiền xử lý sẽ bỏ qua 1 some_other_thing nếu expression đúng, ngược lại nó bỏ qua something. Với chỉ thị #ifdef, bộ tiền xử lý sẽ bỏ qua some_other_thing nếu định danh identifier đã được định nghĩa bởi chỉ thị #define hoặc được định nghĩa bởi tham số dòng lệnh khi biên dịch, ngược lại nó bỏ qua something.
Ví dụ, đoạn mã sau có thể dùng để nhận biết trình biên dịch đang chạy trên hệ điều hành nào. Khi bạn dùng Cygwin, định danh __CYGWIN__ sẽ được define và định danh __linux__ cũng được define vì Cygwin làm giả Linux. Ngược lại, nếu bạn dịch chương trình bằng MSVC thì cả 2 định danh này đều không được định nghĩa. Nếu bạn dịch chương trình với 1 máy Linux thật thì chỉ __linux__ được định nghĩa.
#include < stdio.h >
int main()
{
#ifdef __CYGWIN__
#define MOCK_LINUX 1
#define LINUX 1
#else
#ifdef __linux__
#define MOCK_LINUX 0
#define LINUX 1
#else
#define LINUX 0
#endif
#endif
#if LINUX == 0
printf( "NG, your OS is not Linux (may be Windows?)!" );
#else
printf( "OK, you seem to be using something like Linux\n" );
printf( "Let me see ...\n" );
#if MOCK_LINUX
printf( "Blah, you are simulating Linux on Cygwin\n" );
#else
printf( "Congratulation, you passed our test: You are using Linux!\n" );
#endif
#endif
return 0;
}
Lưu ý là chương trình được dịch thật chỉ bao gồm nhiều nhất là 3 lệnh printf (mặc dù có tới 4 lần viết printf trong chương trình) vì khi LINUX != 0 thì trình biên dịch sẽ bỏ qua đoạn printf( "NG, ..." );.
2.3. Chỉ thị #undef:
Chỉ thị #undef ngược lại với chỉ thị #define: nó xóa bỏ định nghĩa của 1 định danh nào đó, ví dụ:
#ifdef __CYGWIN__
#undef __linux__
#endif
3. Dịch từng phần và Makefile:
3.1. Dịch từng phần:
Như bạn đã biết chỉ thị #include dùng để chèn 1 file vào vị trí có chỉ thị đó. Bạn có thể dùng chỉ thị #include để đính các file có các định nghĩa hàm thường dùng vào 1 chương trình nào đó.
Ví dụ, giả sử bạn có 3 file: file matrix_funcs.c có các hàm để malloc 1 ma trận, free ma trận và hiển thị ma trận. Khi bạn muốn viết chương trình nhập vào 2 ma trận và tính tổng bạn có thể làm như sau:
// matrix_sum.c
#include <stdio.h>
// matrix_funcs.c must be in the same folder with this file (matrix_sum.c)
#include "matrix_funcs.c"
int main()
{
// use malloc_matrix here to malloc matrices
// calculate matrix sum
// use print_matrix here!
// use free_matrix here!
}
Khi bạn muốn viết chương trình nhập vào 2 ma trận và tính tích của 2 ma trận, bạn chỉ việc include file "matrix_funcs.c" giống như là với chương trình matrix_sum.c mà không cần viết lại các hàm malloc_matrix, free_matrix, ...
Hãy thử làm với chương trình matrix mà bạn đã viết ở bài học trước.
Tuy nhiên, giả sử bạn phải viết 1 chương trình vừa tính tổng, vừa tính tích của ma trận, và bạn phân ra 4 files: matrix_funcs.c, matrix_sum.c, matrix_prot.c, matrix.c (file matrix_sum.c chỉ chứa hàm để cộng ma trận, matrix_prot.c chỉ chứa hàm để nhân ma trận và matrix.c phải dùng các hàm của cả 3 file trước) thì có vấn đề xảy ra: trong file matrix_sum.c bạn phải dùng hàm malloc_matrix (để malloc ma trận kết quả) nên bạn phải include file này, trong file matrix_prot.c bạn cũng phải dùng hàm này và cũng phải include file matrix_funcs.c. Trong file matrix.c bạn phải include cả 2 file matrix_sum.c và matrix_prot.c, điều này dẫn đến việc các hàm trong matrix_funcs.c được định nghĩa đi định nghĩa lại 2 lần và gây ra lỗi biên dịch.
Để tránh điều này, C cho phép khai báo prototype của hàm mà không cần khai báo thân hàm. Chỉ cần có prototype là bạn có thể dùng hàm, mặc dù khai báo thân hàm chưa xuất hiện (tất nhiên cuối cùng thân hàm cũng phải xuất hiện):
// file: prototype_demo.c
#include < stdio.h >
// prototype
int add( int x, int y );
// main
int main()
{
int x, y, z;
x = 1;
y = 2;
z = add( x, y );
printf( "z = %d\n", z );
return 0;
}
// implementation of add function:
int add( int x, int y )
{
return x + y;
}
Trong ví dụ trên, bạn dùng hàm add trong hàm main khi chưa định nghĩa body của hàm add. Vì có prototype nên trình biên dịch không báo lỗi gì.
Thông thường, prototype của hàm được khai báo ở trong các file header (.h) còn phần implementation được khai báo trong các file .c. Ví dụ, chương trình prototype_demo.c ở trên có thể chia làm 3 file: add.c, add.h và add_main.c như sau:
// file: add.h
int add( int x, int y );
// file: add.c
#include "add.h" // include add.h to get the prototype of add.
int add( int x, int y )
{
return x + y;
}
// file: add_main.c
#include < stdio.h >
#include "add.h"
int main()
{
int x, y, z;
x = 1;
y = 2;
z = add( x, y );
printf( "z = %d\n", z );
return 0;
}
Khi biên dịch, bạn cần biên dịch file add.c và file add_main.c trước. Sau đó bạn link kết quả biên dịch của 2 file này lại để có chương trình chạy:
$ gcc -c add.c # chỉ biên dịch, không link
$ gcc -c add_main.c # chỉ biên dịch, không link (không tạo ra file chạy)
$ ls # xác nhận đã có 2 file: add.o và add_main.o
$ gcc -o add add.o add_main.o # link 2 file add.o và add_main.o để tạo ra file chạy add.
Khi bạn dùng option "-c" với gcc, bạn chỉ cho trình biên dịch biết là chỉ dịch ra file object (.o) chứ không dịch và link chương trình để ra 1 chương trình toàn vẹn chạy được. Sau khi có 2 file object, bạn phải link 2 file này để có file add (hoặc add.exe trong Windows).
Tương tự, với chương trình matrix, bạn cần phải viết 3 file .h, bao gồm: matrix_funcs.h, matrix_sum.h, matrix_prot.h. Sau đó phải viết 4 file .c bao gồm: matrix_funcs.c, matrix_sum.c, matrix_prot.c và matrix.c. Trong file matrix_sum.h, matrix_sum.c có thể include file "matrix_funcs.h" (tương tự cho matrix_prot). Trong file matrix.c bạn cần include cả 3 file .h, quá trình biên dịch sẽ như sau:
$ gcc -c matrix_funcs.c
$ gcc -c matrix_sum.c
$ gcc -c matrix_prot.c
$ gcc -c matrix.c
$ gcc -o matrix matrix_funcs.o matrix_sum.o matrix_prot.o matrix.o
Quá trình biên dịch như trên gọi là "biên dịch từng phần". Để tránh phải viết đi viết lại nhiều lệnh ở mỗi lần biên dịch ta dùng 1 công cụ gọi là GNU Make.
3.2. Makefile:
Trong các shell của Unix (hoặc các môi trường giống Unix như Linux, Cygwin) có thể dùng một file đặc biệt tên là "Makefile" (chú ý chữ hoa chữ thường) để ghi các lệnh muốn thực hiện khi biên dịch. Sau khi có Makefile ta chỉ việc gõ lệnh
$ make
và chương trình GNU Make sẽ tự mò xem đã có những thay đổi gì từ lần biên dịch trước và thực hiện các lệnh trong Makefile cho ta. Ví dụ, với chương trình matrix ở trên, ta có thể tạo 1 file tên là "Makefile" và đặt vào cùng folder với các file .h và .c, với nội dung như sau:
# Makefile for matrix, everything after sharp is comment
gcc -c matrix_funcs.c
gcc -c matrix_sum.c
gcc -c matrix_prot.c
gcc -c matrix.c
gcc -o matrix matrix_funcs.o matrix_sum.o matrix_prot.o matrix.o
khi muốn biên dịch chương trình chỉ việc gõ lệnh
$ make
Cú pháp của Makefile còn rất phức tạp để mô tả dependencies (sự phụ thuộc) giữa các file, bạn có thể tìm hiểu thêm về cách dùng Makefile ở các tutorial trên Internet (search với từ khóa "Makefile tutorial").
III. Giới thiệu một số hàm thư viện:
1. Thư viện stdio:
Thư viện stdio (standard input/output) chứa các hàm để phục vụ cho việc xuất nhập dữ liệu. Bạn đã quen thuộc với các hàm printf và scanf của thư viện này.
Có 4 hàm hoàn toàn tương tự như printf và scanf (để xuất/nhập dữ liệu), đó là các hàm:
- fprintf: thay vì xuất ra standard output (màn hình) như printf thì xuất dữ liệu ra file.
- fscanf: thay vì đọc dữ liệu từ standard input (bàn phím) như scanf thì đọc dữ liệu từ file.
- sprintf: thay vì xuất dữ liệu ra màn hình như printf, xuất dữ liệu ra chuỗi ký tự (string).
- sscanf: thay vì đọc dữ liệu từ bàn phím thì đọc dữ liệu từ chuỗi ký tự.
Dưới đây là cách dùng cụ thể của các hàm này:
1.1. Xuất nhập dữ liệu với file:
Đối hàm fprintf gần giống như hàm printf, chỉ thêm duy nhất 1 đối số ở đầu, đó là một con trỏ kiểu FILE*:
int fprintf( FILE* fp, char* strFormat, ... );
(tham khảo: khai báo của printf là: int printf( char* strFormat, ...);)
Đối số FILE* này gọi là "file handle" đến file mà bạn muốn ghi dữ liệu ra. Con trỏ FILE* này cần được khởi tạo trước khi có thể truyền cho hàm fprintf để thực hiện việc output ra file.
Muốn khởi tạo con trỏ FILE*, bạn cần cho biết tên file (path đến file) và kiểu truy cập, như trong ví dụ sau đây:
FILE* fp;
int age;
age = 26;
fp = fopen( "example.txt", "at" );
fprintf( fp, "Hello, My name is %s and I am %d years old", "CBD", age );
Đối số đầu tiên của hàm fopen (file open) là path đến file (trong trường hợp chỉ chỉ định tên file, hàm này hiểu là file đó ở cùng current directory). Đối số thứ 2 là một chuỗi ký tự biểu thị cách truy cập file:
- a: mở file để append (thêm vào cuối), nếu file chưa tồn tại thì tạo file.
- r: mở file để đọc (read), nếu chưa tồn tại sẽ báo lỗi.
- w: mở file để ghi (write), nếu chưa tồn tại, tạo file.
- t: mở file kiểu văn bản (text).
- b: mở file kiểu nhị phân (ví dụ các file ảnh, không phải file text) (binary).
Có thể kết hợp a, r, w với b, t để thành "at", "wt", "rb", "wb", ....
Hàm fscanf cũng giống hệt như hàm scanf, chỉ khác là tham số đầu là FILE*. File mở ra phải ở chế độ "r" thì mới có thể đọc file:
#include<stdio.h>
int main()
{
int x;
double d;
FILE* fp;
x = 0;
d = 0;
fp = fopen( "data.txt", "rt" );
if( fp == NULL ) {
printf( "Could not open data.txt, check file\n" );
return 1; // error
}
fscanf( fp, "%d", &x ); // read an integer from the file
fscanf( fp, "%lf", &d ); // read a double
fclose( fp ); // do not forget to close the opened file
printf( "Input data: x = %d, d = %lf\n", x, d );
return 0;
}
(để chạy chương trình trên, bạn cần tạo 1 file text có tên là "data.txt" (tạo bằng Notepad, ...) và ghi vào đó 2 số: 1 số nguyên và 1 số thực, cách nhau ít nhất 1 dấu cách: ví dụ 100 50.3).
1.2. Đọc/ghi từng dòng vào text file:
Muốn đọc/ghi một dòng của một file văn bản, ta dùng hàm fputs và fgets: hàm fputs sẽ ghi một chuỗi ký tự vào file, hàm fgets đọc một file cho đến khi gặp dấu xuống dòng hoặc số ký tự đọc được vượt quá một số max nào đó (chỉ định ở tham số):
fputs( char* strToWrite, FILE* fp );
fgets( char* strBuffer, int nMaxChars, FILE* fp);
(chú ý: strBuffer là một con trỏ kiểu char* để chứa dữ liệu đọc được, nó phải được malloc cẩn thận với cỡ bằng nMaxChars, hàm fgets sẽ đọc cho đến khi gặp ký tự xuống dòng hoặc nếu dòng đó dài quá nMaxChars thì nó sẽ dừng lại ở nMaxChars ký tự).
Nếu đã đọc hết file thì hàm fgets sẽ trả về NULL pointer.
Ví dụ, chương trình sau để copy file văn bản:
// file: mycopy.c
#include<stdio.h>
int main( int argc, char** argv )
{
/* the main function can be declared like this:
argc: argument count (number of arguments provided from the command line)
argv: argument vector (the actual place where arguments are stored)
Example: $ mycopy file1.txt file2.txt
--> argc = 3
--> argv[0] = "mycopy" (name of the program), argv[1] = "file1.txt", argv[2] = "file2.txt"
*/
char* strInputFile;
char* strOutputFile;
char* buffer;
FILE *fpIn, *fpOut;
if( argc < 3 ) {
printf( "Error: missing input/ouput files\n" );
return 1;
}
strInputFile = argv[1];
strOutputFile = argv[2];
fpIn = fopen( strInputFile, "rt" );
if( fpIn == NULL ) {
printf( "Error: could not read file %s\n", strInputFile );
return 1;
}
fpOut = fopen( strOutputFile, "at" );
buffer = (char*)malloc( 256 * sizeof(char) ); // maximum 255 chars / line
while( fgets( buffer, 255, fpIn ) != NULL ) { // read file line by line until reaching end of file
fputs( buffer, fpOut );
}
fclose( fpIn );
fclose( fpOut );
printf( "Okie, 1 file copied!\n" );
return 0;
}
(chương trình trên dùng cách khai báo thứ 2 của hàm main (int main(int argc, char** argv)) để lấy tham số nhập vào từ dòng lệnh khi chương trình được gọi. Để chạy chương trình trên bạn cần có 1 file văn bản nào đó, ví dụ "file1.txt", sau đó bạn gọi chương trình như sau:
$ mycopy file1.txt file2.txt
nếu gọi như trên, chương trình sẽ copy file1.txt ra file2.txt.
1.3. Đọc dữ liệu / ghi dữ liệu với chuỗi ký tự:
Nhiều khi bạn cần đổi một số nguyên/thực thành một chuỗi và ngược lại, hàm sprintf và sscanf sẽ giúp bạn làm việc đó. Hàm này cũng giống hệt như hàm fprintf, fscanf, chỉ khác là thay vì FILE*, bạn truyền cho tham số đầu tiên là một chuỗi ký tự đã được khai báo và khởi tạo:
#include<stdio.h>
int main()
{
double x = 100.34;
int y;
char* strX;
char* strInt = "345";
strX = (char*)malloc( 20 * sizeof(char) );
sprintf( strX, "%lf", x );
sscanf( strInt, "%d", &y );
printf( "x = %d, strInt = %s\n", x, strInt );
printf( "strX = %s, y = %d\n", strX, y );
return 0;
}
2. Các hàm trong string.h:
String.h là file header khai báo các hàm liên quan đến chuỗi ký tự.
* Hàm strcpy để copy nội dung một chuỗi vào một chuỗi khác:
char* strcpy( char* strDestination, char* strSource );
Ví dụ, muốn khởi tạo một chuỗi ký tự mà không dùng phép gán ngay sau khi khai báo (kiểu char* str = "Con Bo Dien"), bạn có thể malloc chuỗi đó và dùng strcpy để gán:
char* strCustomerName;
strCustomerName = (char*)malloc( 256 * sizeof( char ) );
strcpy( strCustomerName, "Nguyen Van Abc" );
printf( "Customer Name = %s\n", strCustomerName );
* Hàm strcat để nối (concatenate) một chuỗi vào chuỗi khác: char* strcat( char* strDestination, char* strToAttach );
Ví dụ, str1 = "Nguyen Van ", str2 = "Abc" thì strcat( str1, str2 ) sẽ cho str1 = "Nguyen Van Abc".
* Hàm strlen để tính chiều dài của chuỗi ký tự, ví dụ: int nLen = strlen( strInput );
* Hàm strcmp để compare 2 chuỗi xem chuỗi nào có thứ tự ABC lớn hơn (xem manual).
* Ngoài ra còn các hàm như strncpy, strncmp, strdup, strstr, strtok, ... hãy tự tham khảo manual.
3. Các hàm trong math.h:
Math.h gồm các hàm toán học: sin, cos, tan, exp, log (cơ số e), sqrt, sqr, ...
4. Các hàm trong stdlib.h:
Stdlib.h gồm các hàm thư viện thông dụng (general purpose standard library). Ở đây chỉ giới thiệu 3 hàm rất hay dùng:
4.1. Thoát khỏi chương trình bằng hàm exit:
Hàm này để thoát ngay khỏi chương trình, nó sẽ đóng các file đang mở, và thông báo cho hệ điều hành biết trạng thái kết thúc mà ta truyền cho nó (thường trạng thái 0 là không có lỗi, 1 là có lỗi):
void exit( int nStatus );
Ví dụ: exit( 1 ); hay exit( 0 );
4.2. Sinh số ngẫu nhiên theo phân bố đều:
Hàm rand và srand để điều khiển việc sinh số ngẫu nhiên. Hàm rand sẽ sinh ra 1 số nguyên ngẫu nhiên trong khoảng từ 0 đến RAND_MAX (một hằng số được define sẵn). Hàm srand sẽ đặt nhân (seed) cho thuật toán sinh số ngẫu nhiên. Để mỗi lần chạy chương trình thì dãy số ngẫu nhiên sinh ra khác nhau bạn cần đặt nhân khác nhau, vì thế cách tốt nhất là dùng hàm time để lấy thời gian hiện hành làm nhân.
Ví dụ:
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
int main()
{
int i, v;
srand( time( NULL ) ); // set random seed
for( i = 0; i < 1000; i++ ) {
v = rand();
printf( "%d\n", v );
}
}
Muốn sinh số thực ngẫu nhiên trong khoảng (0, 1) bạn chỉ việc lấy rand()/(double)RAND_MAX.
5. Đo thời gian chạy chương trình bằng hàm gettimeofday:
Hàm gettimeofday sẽ trả về số giây (chính xác đến micro giây) đã trôi qua kể từ 00:00:00 giờ ngày 1/1/1970.
Hàm này có thể dùng để đo thời gian chạy của chương trình chính xác đến micro giây (1 phần triệu giây). Hàm getttimeofday có đối số là struct timeval, gồm 2 field là tv_sec và tv_usec thể hiện số giây và số micro giây đã trôi qua kể từ 00:00:00 ngày 1/1/1970.
Chương trình sau sẽ đo xem mất bao nhiêu thời gian để tính 33 số đầu trong dãy Fibonacci:
#include<stdio.h>
#include<time.h>
#include<sys/time.h>
int fib( int n )
{
if( n <= 1 ) return 1;
return fib( n - 1 ) + fib( n - 2 );
}
int main()
{
struct timeval tv1, tv2;
int i, v;
double ellapsed_time;
printf( "Testing your processor speed, please wait ...\n" );
gettimeofday( &tv1, NULL );
for( i = 0; i < 33; i++ ) {
v = fib( i );
}
gettimeofday( &tv2, NULL );
ellapsed_time = 1000000.0 * (tv2.tv_sec - tv1.tv_sec) + 1.0 * (tv2.tv_usec - tv1.tv_usec);
printf( "Ellapsed time = %lf microsec\n", ellapsed_time );
printf( "That means your processor can compute Fibonacci series from 0..32 in the above time\n" );
return 0;
}
Chú ý: hàm gettimeofday chỉ dùng được với GCC (không dùng được với MS VC ...). Trong MSVC có thể đo thời gian bằng hàm QueryPerformanceCounter và QueryPerformanceFrequency.
Tổng kết: Bạn đã học qua toàn bộ các phần thiết yếu của ngôn ngữ lập trình C: biến số, hàm số, toán tử, cách gọi hàm, các kiểu dữ liệu cơ bản, con trỏ, quản lý bộ nhớ, kiểu dữ liệu trừu tượng và các chỉ thị tiền xử lý. Nắm vững những kiến thức này sẽ giúp bạn tự tin trong quá trình lập trình với ngôn ngữ C. Muốn hiểu rõ hơn nữa và sử dụng thành thạo ngôn ngữ C bạn cần luyện tập thật nhiều thông qua những bài tập ở cuối mỗi bài học, cũng như những bài tập ở phần bài tập đi theo sau phần lý thuyết này và các chương trình trong thực tế. Chúc bạn thành công!.
Câu hỏi ôn tập:
1. Thế nào là kiểu dữ liệu trừu tượng (abstract data type, ADT). Nêu ý nghĩa của ADT.
2. Kiểu dữ liệu trừu tượng được biểu diễn trong C bằng công cụ gì?
3. Dùng từ khóa nào để định nghĩa một kiểu dữ liệu mới?.
4. Nêu sự khác nhau giữa 2 identifier: _Complex và Complex trong khai báo sau:
typedef struct _Complex {
double real;
double img;
} Complex;
5. Hãy viết chương trình cho phép nhập vào 2 số phức, sau đó in ra màn hình Tổng, Hiệu, Tích, Thương của chúng. Chương trình của bạn phải sử dụng kiểu dữ liệu Complex được định nghĩa ở trên.
6. Hãy viết chương trình giải phương trình bậc 2 trong trường số phức (hiển thị cả nghiệm khi delta âm).
7. Nêu cú pháp của toán tử chọn ternary?
8. Giữa #define SoNguyen int và typedef int SoNguyen; có gì khác nhau?
9. Macro (lệnh gộp) là gì?
10. Hãy dùng toán tử ternary để định nghĩa một macro tên là "myabs" để tính giá trị tuyệt đối của một số.
11. Chỉ thị (directive) #if khác gì với lệnh (statement) if?
12. Hãy viết lại chương trình ở bài 6 sao cho thỏa mãn yêu cầu sau:
Khi biên dịch bình thường thì chương trình vẫn giống như chương trình ban đầu ở bài 6.
Khi biên dịch với định nghĩa -D__DEBUG__ thì chương trình in ra cả giá trị delta ra màn hình trước khi in kết quả.
Cho biết, với gcc, có thể định nghĩa một địa danh bằng cách dùng option -D như sau:
$ gcc -D__DEBUG__ file.c
Khi có -D__DEBUG__ thì chỉ thị #ifdef( __DEBUG__ ) sẽ thực hiện nhánh đúng.
13. Hãy viết các file matrix_funcs.c, matrix_sum.c, matrix_prot.c và matrix.c như ở phần 3.1 của bài học và thực hiện dịch từng phần.
14. Hãy viết Makefile cho bài 13.
15. Hãy viết chương trình đếm số dòng có trong một file văn bản. Hãy hỏi tên file và cho phép người dùng nhập tên file từ bàn phím.
16. Hãy sửa chương trình 15 để người dùng có thể nhập ngay tên file từ dòng lệnh:
$ line_count file.txt
17. Hãy viết chương trình đọc 2 ma trận từ 1 file và hiển thị ra màn hình kết quả ma trận tổng. Định dạng của file dữ liệu như sau:
m n
a11 a12 a13 ...
a21 a22 a23 ...
....
a_m1 a_m2 ... a_mn
b11 b12 b13 ...
...
b_m1 b_m2 ... b_mn
Hai số đầu là cỡ của ma trận (row, col). m dòng tiếp theo là m hàng của ma trận số 1 (mỗi hàng chứa n số nguyên). m dòng tiếp theo nữa là m hàng của ma trận số 2.
18. Hãy sửa lại bài 17 để kết quả cũng được xuất ra 1 file, tên file input và output được nhập từ command line.
19. Cho biết một file handle được khai báo sẵn có tên là stdin, nó biểu diễn standard input (bàn phím). Hãy dùng hàm fgets để đọc một chuỗi từ bàn phím như sau: fgets( strBuf, 255, stdin ); (lưu ý: biến stdin đã được khai báo sẵn trong stdio.h, bạn không cần khai báo và khởi tạo, chỉ việc dùng).
20. Cho biết stdard output (màn hình) được biểu diễn bằng 1 file handle (FILE*) có tên là stdout. Hãy dùng hàm fprintf để xuất một chuỗi dữ liệu ra màn hình.
21. Hãy sử dụng cấu trúc Complex ở trên để nhập vào một dãy các số phức và tính tổng của chúng. Mảng của số phức có thể được khai báo như sau:
Complex *arrComplex;
arrComplex = (Complex*)malloc( nSize * sizeof(Complex) );
arrComplex[0].real = 1;
arrComplex[0].img = 2;
22. Hãy định nghĩa một cấu trúc dữ liệu mới có tên là Student. Cấu trúc này bao gồm tên của sinh viên (dạng char*, cần malloc và free cẩn thận, dài tối đa 256 ký tự), và điểm Toán, Lý, Hóa của sinh viên đó. Sau đó, hãy viết chương trình cho phép người dùng nhập vào từ bàn phím tên và điểm của 10 sinh viên ( dùng hàm fgets để đọc tên sinh viên như bài 19).
Cuối cùng, hãy hiển thị danh sách sinh viên bao gồm 5 cột: Tên, Điểm Toán, Điểm Lý, Điểm Hóa và một cột tên là Đỗ/Trượt. Trong cột Đỗ/Trượt hãy hiển thị "Đỗ" nếu tổng điểm lớn hơn hay bằng 15, ngược lại hiển thị "Trượt".
Chú ý: malloc và free cẩn thận.
23. Hãy viết 2 hàm cùng để tính giai thừa của một số, một hàm dùng đệ quy, một hàm không dùng đệ quy.
Hãy đo thời gian thực hiện việc tính giai thừa từ 1..12 dùng 2 hàm này.
So sánh thời gian và thử giải thích kết quả?
24. Sau đây là ý nghĩa (bằng lời) của lệnh gán các struct trong ngôn ngữ C:
Giả sử có định nghĩa struct và 2 biến số kiểu struct như sau:
typedef struct _StructName {
type1 field1;
type2 field2;
...
type_n field_n;
} ADTName;
ADTName data1, data2; // 2 biến kiểu struct
Khi đó, lệnh gán "data1 = data2;" có ý nghĩa tương đương với khối lệnh sau:
data1.field1 = data2.field2;
data1.field2 = data2.field2;
...
data1.field_n = data2.field_n;
Có nghĩa là, lệnh gán struct sẽ làm một việc đơn giản là copy từng bit của data2 vào data1, do đó nó tương đương với việc gán từng trường của data2 cho các trường tương ứng của data1.
a)(*) Viết phát biểu trên dưới dạng operational semantics? (chỉ cần viết từ đoạn "Khi đó, ...", không cần quan tâm đến đoạn "Giả sử ..." ở phía trước).
b) Hãy đoán kết quả của chương trình sau:
// file: swap_struct.c
#include<stdio.h>
typedef struct _Point {
int x;
int y;
} Point;
void swap( Point p, Point q )
{
Point r;
r = p;
p = q;
q = r;
}
void xchg( Point* pP, Point* pQ )
{
Point r;
r = *pP;
*pP = *pQ;
*pQ = r;
}
int main()
{
Point p1, p2;
p1.x = 1; p1.y = 2;
p2.x = -3; p2.y = -4;
printf( "Before swap: p1 = (%d, %d), p2 = (%d, %d)\n", p1.x, p1.y, p2.x, p2.y );
swap( p1, p2 );
printf( "After swap: p1 = (%d, %d), p2 = (%d, %d)\n", p1.x, p1.y, p2.x, p2.y );
printf( "Before xchg: p1 = (%d, %d), p2 = (%d, %d)\n", p1.x, p1.y, p2.x, p2.y );
xchg( &p1, &p2 );
printf( "After xchg: p1 = (%d, %d), p2 = (%d, %d)\n", p1.x, p1.y, p2.x, p2.y );
return 0;
}
Sau đó, hãy chạy chương trình và giải thích kết quả.
25. Hãy đoán kết quả chương trình sau:
// file: modify_struct.c
#include<stdio.h>
typedef struct _Point {
int x;
int y;
} Point;
void modify_struct1( Point* pP )
{
pP->x += 1;
pP->y += 1;
}
void modify_struct2( Point p )
{
p.x += 1;
p.y += 1;
}
void main()
{
Point q;
q.x = 10;
q.y = 20;
printf( "Step0: q = (%d, %d)\n", q.x, q.y );
modify_struct1( &q );
printf( "Step1: q = (%d, %d)\n", q.x, q.y );
modify_struct2( q );
printf( "Step2: q = (%d, %d)\n", q.x, q.y );
return 0;
}
Hãy chạy chương trình xem bạn đoán có đúng không và giải thích kết quả?
26. Hãy đoán kết quả chương trình sau. Sau đó, chạy và giải thích kết quả?.
// file: sm_struct.c
// swap and modify structs
#include<stdio.h>
typedef struct _Point {
int x;
int y;
} Point;
void swap_and_mod1( Point* pP, Point* pQ )
{
Point* pR;
// swap
pR = pP;
pP = pQ;
pQ = pR;
// modify
(*pP).x += 1;
(*pP).y += 1;
}
void swap_and_mod2( Point* pP, Point *pQ )
{
Point* pR;
pR = (Point*)malloc( sizeof(Point) );
// swap
*pR = *pP;
*pP = *pQ;
*pQ = *pR;
free( pR );
pR = NULL;
// modify
pP->x += 1;
pP->y += 1;
}
void swap_and_mod3( Point p, Point q )
{
Point *pR;
// swap
pR = &p;
p = q;
q = *pR;
// modify
p.x += 1;
p.y += 1;
}
void swap_and_mod4( Point* pP, Point* pQ )
{
Point r;
// swap
r = *pP;
*pP = *pQ;
*pQ = r;
// modify
r.x += 1;
(&r)->y += 1;
}
void swap_and_mod5( Point p, Point q )
{
Point r;
/* swap */
// r = p
r.x = p.x;
r.y = p.y;
// p = q
p.x = q.x;
p.y = q.y;
// q = r
q.x = r.x;
q.y = r.y;
/* modify */
p.x += 1;
p.y += 1;
}
void print_result( Point* pP, Point* pQ, int nStep )
{
printf( "Step%d: p = (%d, %d), q = (%d, %d)\n", nStep, pP->x, pP->y, pQ->x, pQ->y );
}
int main()
{
Point p, q;
p.x = 100; p.y = 200;
q.x = -300; q.y = -400;
print_result( &p, &q, 0 );
swap_and_mod1( &p, &q );
print_result( &p, &q, 1 );
swap_and_mod2( &p, &q );
print_result( &p, &q, 2 );
swap_and_mod3( p, q );
print_result( &p, &q, 3 );
swap_and_mod4( &p, &q );
print_result( &p, &q, 4 );
swap_and_mod5( p, q );
print_result( &p, &q, 5 );
return 0;
}
Hãy cho biết hàm swap_and_mod thứ mấy đáp ứng được cả 2 yêu cầu: vừa swap vừa modify?.
27. Hãy đoán kết quả chương trình sau, sau đó chạy để kiểm chứng. Giải thích kết quả?.
// file: arr_struct.c
// array of struct
#include<stdio.h>
typedef struct _Point {
int x;
int y;
} Point;
void modify( Point* arrInput, int nSize )
{
int i;
for( i = 0; i < nSize; i++ ) {
arrInput[i].x += 1;
arrInput[i].y += 1;
}
}
void modify_by_pointer( Point* arrInput, int nSize )
{
int i;
Point *pQ;
pQ = arrInput;
for( i = 0; i < nSize; i++ ) {
pQ->x += 1;
pQ->y += 1;
pQ++;
}
}
/* begin modify by value */
void modify_value( Point p )
{
p.x += 1;
p.y += 1;
}
void modify_by_value( Point* arrInput, int nSize ) {
int i;
for( i = 0; i < nSize; i++ ) {
modify_value( arrInput[i] );
}
}
/* end modify by value */
Point* allocate_points( int nSize )
{
Point* arrRet;
int i;
arrRet = (Point*)malloc( nSize * sizeof(Point) );
for( i = 0; i < nSize; i++ ) {
arrRet[i].x = 3;
arrRet[i].y = 3;
}
return arrRet;
}
void print_points( Point* arrInput, int nSize, int nStep )
{
int i;
printf( "Step%d: ", nStep );
for( i = 0; i < nSize; i++ ) {
printf( "p%d = (%d, %d), ", i, arrInput[i].x, arrInput[i].y );
}
printf( "\n" );
}
int main()
{
Point* arrPoints;
int nSize;
nSize = 2; // you can change this value!
arrPoints = allocate_points( nSize );
print_points( arrPoints, nSize, 0 );
modify( arrPoints, nSize );
print_points( arrPoints, nSize, 1 );
modify_by_pointer( arrPoints, nSize );
print_points( arrPoints, nSize, 2 );
modify_by_value( arrPoints, nSize );
print_points( arrPoints, nSize, 3 );
return 0;
}
28.(**) Hãy chạy chương trình sau và giải thích điều gì xảy ra?
Gợi ý: hãy dùng hàm printf để in ra mọi thứ cần in!.
#include<stdio.h>
// a point in space-time coordinate (4D)
typedef struct _Point {
int x;
int y;
int z;
int t;
} Point;
int main()
{
int i;
Point arrPoints[16];
int j;
j = 0;
for( i = 0; i <= 16; i++ ) {
arrPoints[i].x = 1;
arrPoints[i].y = 2;
arrPoints[i].z = 3;
arrPoints[i].t = 4;
printf( "j = %d\n", j );
j++;
if( j > 10000 ) j = 0;
}
return 0;
}