Bài giảng Lập trình nâng cao - Nguyễn Hải Minh
MỤC LỤC
CHƯƠNG 1: CON TRỎ 4
1.1 KHÁI NIỆM CON TRỎ 4
1.1.1 Khai báo con trỏ 4
1.1.2 Sử dụng con trỏ 4
1.2 CON TRỎ VÀ MẢNG 7
1.2.1 Con trỏ và mảng một chiều 7
1.2.2 Con trỏ và mảng nhiều chiều 10
1.3. CON TRỎ HÀM 11
1.4 CẤP PHÁT BỘ NHỚ ĐỘNG 14
1.4.1 Cấp phát bộ nhớ động cho biến 14
1.4.2 Cấp phát bộ nhớ cho mảng động một chiều 15
1.4.3 Cấp phát bộ nhớ cho mảng động nhiều chiều 17
CHƯƠNG 2: CÁC DÒNG NHẬP XUẤT VÀ TỆP TIN 23
2.1. NHẬP/XUẤT VỚI CIN/COUT 24
2.1.1. Toán tử nhập >> 24
2.1.2. Các hàm nhập kí tự và xâu kí tự 25
2.1.3. Toán tử xuất << 28
2.2. ĐỊNH DẠNG 28
2.2.1.Các phương thức định dạng 28
2.2.2. Các cờ định dạng 29
2.2.3. Các bộ và hàm định dạng 31
2.3. IN RA MÁY IN 32
2.4. LÀM VIỆC VỚI FILE 33
2.4.1. Tạo đối tượng gắn với file 33
2.4.2 Đóng file và giải phóng đối tượng 34
2.4.3. Kiểm tra sự tồn tại của file, kiểm tra hết file 37
2.4.4. Đọc ghi đồng thời trên file 38
2.4.5. Di chuyển con trỏ file 39
2.5. NHẬP/XUẤT NHỊ PHÂN 41
2.5.1. Khái niệm về 2 loại file: văn bản và nhị phân 41
2.5.2.Đọc, ghi kí tự 41
2.5.3. Đọc, ghi dãy kí tự 42
2.5.4. Đọc ghi đồng thời 43
CHƯƠNG 3: DỮ LIỆU KIỂU CẤU TRÚC VÀ HỢP 48
3.1. KIỂU CẤU TRÚC 48
3.1.1. Khai báo, khởi tạo 48
3.1.2.Truy nhập các thành phần kiểu cấu trúc 50
3.1.3. Phép toán gán cấu trúc 52
3.1.4. Các ví dụ minh hoạ 53
3.1.5.Hàm với cấu trúc 56
3.1.6.Cấu trúc với thành phần kiểu bit 67
3.1.7.Câu lệnh typedef 68
3.1.8.Hàm sizeof() 69
3.2. CẤU TRÚC TỰ TRỎ VÀ DANH SÁCH LIÊN KẾT 69
3.2.1.Cấu trúc tự trỏ 70
3.2.2. Khái niệm danh sách liên kết 72
3.2.3. Các phép toán trên danh sách liên kết 73
3.3. KIỂU HỢP 79
3.3.1. Khai báo 79
3.3.2. Truy cập 79
3.4. KIỂU LIỆT KÊ 80
CHƯƠNG 4: XỬ LÝ NGOẠI LỆ 86
4.1. Xử lý ngoại lệ 86
4.2. Ném Exception trong C++ 87
4.3. Bắt Exception trong C++ 87
4.4.Chuẩn Exception trong C++ 88
4.5. Định nghĩa Exception mới trong C++ 90
CHƯƠNG 5: MỘT SỐ VẤN ĐỀ 92
5.1. Một số quy tắc trong lập trình C++ 92
5.1.1. Tổ chức chương trình 92
5.1.2. Chuẩn tài liệu 93
5.1.3. Tên 93
5.1.4. Định dạng 95
5.1.5. Thiết kế 96
5.1.6. Code 97
5.2. Namespaces 103
5.1.1. Using namespace 104
5.1.2. Định nghĩa bí danh 106
5.1.3. Namespace std 106
Tài liệu tham khảo 108
ức ngắn kiểu như sau đây: max = ( a > b ) ? a : b; hoặc: printf( “The list has %d item%s\n”, n, n == 1 ? “” : “s” ); Cẩn thận với dấu = = và == là 2 toán tử gây nhần lẫn nhất trên C, nhưng bạn có thể tránh gặp nó bằng thói quen viết r-value (biểu thức bên phải phép gán) sang bên trái phép so sánh: if ( a == 42 ) { ... }// Cách viết thông thường. if ( 42 == a ) { ... }// Nên viết thế này. Và đây là sự khác biệt, khi bạn nhầm ... if ( a = 42 ) { ... }// Chạy bình thường, khó tìm ra lỗi if ( 42 = a ) { ... }// Báo lỗi ngay chỗ này Các idiom Cũng giống như ngôn ngữ tự nhiên, ngôn ngữ lập trình cũng có các idiom (thành ngữ !?), là các cách viết code chính tắc cho các trường hợp thông dụng, tạm hiểu idiom là các chuẩn không bắt buộc nhưng được đa số người dùng tuân theo. Sử dụng các idiom giúp giảm bớt khả năng mắc lỗi đồng thời làm chương trình dễ đọc hơn và nhất là có vẻ “chuyên nghiệp” hơn Sau đây là một số idiom phổ biến: Các idiom cho mảng Để duyệt qua n phần tử của một mảng và khởi tạo chúng, có các cách viết sau đây: i = 0; while ( i <= n – 1 ) array[ i++ ] = 1.0; hoặc for( i = 0; i < n; ) array[ i++ ] = 1.0; hoặc for( i = n; -–i >= 0; ) array[ i ] = 1.0; Tất cả những cách viết trên đều đúng, tuy nhiên idiom cho trường hợp này là: for( i = 0; i < n; ++i ) array[ i ] = 1.0; Một lưu ý nhỏ là sự khác biệt giữa i++ và ++i: • i++ lấy giá trị của i trước rồi tăng nó lên. • ++i tăng giá trị của i rồi lấy giá trị mới. Do đó đối với các con đếm vòng lặp (for(),while()) nên dùng ++i để tăng tốc độ. Idiom của vòng lặp duyệt qua các phần tử của một danh sách (list) là for( p = list; p != NULL; p = p->next ) Đối với container: vector::iterator it; for(it = v.begin(); it != v.end(); ++it) std::cout << *it; Đối với các vòng lặp vô hạn, idiom là: for ( ; ; ) hoặc while( 1 ) Khởi tạo danh sách: struct info { char *name; char *job; char *address; }; info *array[] = { { "name1", "job1", "add1" }, { "name1", "job1", "add1" }, { "name1", "job1", "add1" }, //... }; Hàm tìm kiếm tuyến tính: template int find (T obj,T* array,int size,int from = 0) { for(int i = from; i<size; ++i) if(array[i] == T) return i; return size; } Sao chép mảng: Giả sử 2 mảng double *a,*b; thay vì: for(int i=0; i<n; ++i) b[i]=a[i]; ta có thể dùng: //#include memcpy(b,a,n*sizeof(double)); Cấp phát động cho mảng 2 chiều: nt **pp = new type*[n]; int *p = new type[n*m]; for (int i = 0; i < n; ++i) pp[i] = p + i * m; //... //use array here delete[] p; delete[] pp; Idiom cho lệnh if Tiếp theo là một idiom dành cho câu lệnh if. Hãy xem đoạn mã loằng ngoằng sau đây làm gì if ( argc==3 ) if ( ( fin = fopen(argv[l] , “r” ) ) != NULL ) if ( ( fout = fopen( argv[2], “w” ) ) != NULL ) { while ( ( c = getc( fin ) ) != EOF ) putc( c, fout ); fclose( fin ); fclose( fout ); } else printf ( “Can’t open output file %s\n”, argv[2] ) ; else printf( “Can’t open input file %s\n”, argv[l] ) ; else printf ( “Usage: cp input file outputfile\n” ) ; Viết lại đoạn mã này theo đúng idiom như sau: if ( argc != 3 ) printf ( “Usage: cp input file outputfile\n” ) ; else if ( ( fin = fopen( argv[l] , “r” ) ) == NULL ) printf( “Can’t open input file %s\n”, argv[l] ); else if ( ( fout = fopen( argv[2], “w” ) ) == NULL ) { printf ( “Can’t open output file %s\n”, argv[2] ) ; fclose( fin ) ; } else { while ( ( c = getc( fin ) ) != EOF) putc( c, fout ); fclose( fin ) ; fclose( fout ) ; } Nguyên tắc khi viết các lệnh if() là đặt các phép toán kiểm tra điều kiện càng gần các hành động tương ứng càng tốt. Idiom cho switch() case: Xét ví dụ: switch (c) { case '-': sign = -1; case '+': c = getchar(); case '.': break; case '0': case 'o': default: if (!isdigit(c)) return 0; } cách viết sau tuy dài nhưng dễ đọc hơn: switch (c) { case '-': sign = -1; case '+': c = getchar(); break; case '.': break; default: case '0': case 'o': if (!isdigit(c)) return 0; break; } Số 0 trong chương trình Số 0 thường xuyên xuất hiện trong các chương trình với nhiều ý nghĩa khác nhau. Trình dịch sẽ tự động chuyển số 0 thành kiểu thích hợp. Tuy nhiên nên viết ra một cách tường minh bản chất của số 0 mà chúng ta đang nói đến. Cụ thể, hãy sử dụng ( void* )0 hoặc NULL để biểu diễn con trỏ null trong C, sử dụng ‘\0′ cho kí tự null ở cuối mỗi xâu và sử dụng 0.0 cho các số float hoặc double có giá trị không. Đừng viết đoạn mã như sau p = 0; name[ i ] = 0; x = 0; Hãy viết: p = NULL; name[ i ] = '\ 0'; x = 0.0; Số 0 nên để dành cho các số nguyên có giá trị bằng không. Tuy nhiên trong C++, 0 (thay vì NULL) lại được sử dụng rộng rãi cho các con trỏ null, điều này không được khuyến khích. 5.2. Namespaces Namespaces cho phép chúng ta gộp một nhóm các lớp, các đối tượng toàn cục và các hàm dưới một cái tên. Nói một cách cụ thể hơn, chúng dùng để chia phạm vi toàn cụ thành những phạm vi nhỏ hơn với tên gọi namespaces. Khuông mẫu để sử dụng namespaces là: namespace identifier { namespace-body } Trong đó identifier là bất kì một tên hợp lệ nào và namespace-body là một tập hợp những lớp, đối tượng và hàm được gộp trong namespace. Ví dụ: namespace general { int a, b; } Trong trường hợp này, a và b là những biến bình thường được tích hợp bên trong namespace general. Để có thể truy xuất vào các biến này từ bên ngoài namespace chúng ta phải sử dụng toán tử ::. Ví dụ, để truy xuất vào các biến đó chúng ta viết: general::a general::b Namespace đặc biệt hữu dụng trong trường hợp có thể có một đối tượng toàn cục hoặc một hàm có cùng tên với một cái khác, gây ra lỗi định nghĩa lại. Ví dụ: // namespaces #include namespace first { int var = 5; } namespace second { double var = 3.1416; } int main () { cout << first::var << endl; cout << second::var << endl; return 0; } kết quả: 5 3.1416 Trong ví dụ này có hai biến toàn cục cùng có tên var, một được định nghĩa trong namespace first và cái còn lại nằm trong second. Chương trình vẫn chạy tốt, 5.1.1. Using namespace Chỉ thị using theo sau là namespace dùng để kết hợp mức truy xuất hiện thời với một namespace cụ thể để các đối tượng và hàm thuộc namespace có thể được truy xuất trực tiếp như thể chúng được khai báo toàn cục. Cách sử dụng như sau: using namespace identifier; Ví dụ: // using namespace example #include namespace first { int var = 5; } namespace second { double var = 3.1416; } int main () { using namespace second; cout << var << endl; cout << (var*2) << endl; return 0; } Kết quả: 3.1416 6.2832 Trong trường hợp này chúng ta có thể sử dụng var mà không phải đặt trước nó bất kì toán tử phạm vi nào. Bạn phải để ý một điều rằng câu lệnh using namespace chỉ có tác dụng trong khối lệnh mà nó được khai báo hoặc trong toàn bộ chương trình nếu nó được dùng trong phạm vi toàn cục. Ví dụ, nếu chúng ta định đầu tiên sử dụng một đối tượng thuộc một namespace và sau đó sử dụng một đối tượng thuộc một namespace khác chúng ta có thể làm như sau: // using namespace example #include namespace first { int var = 5; } namespace second { double var = 3.1416; } int main () { { using namespace first; cout << var << endl; } { using namespace second; cout << var << endl; } return 0; } Kết quả: 5 3.1416 5.1.2. Định nghĩa bí danh Chúng ta cũng có thể định nghĩa những tên thay thế cho các namespaces đã được khai báo. Cách thức để làm việc này như sau: namespace new_name = current_name ; 5.1.3. Namespace std Một trong những ví dụ tốt nhất mà chúng ta có thể tìm thấy về namespaces chính là bản thân thư viện chuẩn của C++. Theo chuẩn ANSI C++, tất cả định nghĩa của các lớp, đối tượng và hàm của thư viện chuẩn đều được định nghĩa trong namespace std. Bạn có thể thấy rằng chúng ta đã bỏ qua luật này trong suốt tutorial này. Tôi đã quyết định làm vậy vì luật này cũng mới như chuẩn ANSI (1997) và nhiều trình biên dịch cũ không tương thích với nó. Hầu hết các trình biên dịch, thậm chí cả những cái tuân theo chuẩn ANSI, cho phép sử dụng các file header truyền thống (như là iostream.h, stdlib.h), những cái mà chúng ta trong suốt tutorial này. Tuy nhiên, chuẩn ANSI đã hoàn toàn thiết kế lại những thư viện này để tận dụng lợi thế của tính năng templates và để tuân theo luật phải khai báo tất cả các hàm và biến trong namespace std. Chuẩn ANSI đã chỉ định những tên mới cho những file "header" này, cơ bản là dùng cùng tên với các file của chuẩn C++ nhưng không có phần mở rộng .h. Ví dụ, iostream.h trở thành iostream. Nếu chúng ta sử dụng các file include của chuẩn ANSI-C++ chúng ta phải luôn nhớ rằng tất cả các hàm, lớp và đối tượng sẽ được khai báo trong std. Ví dụ: // ANSI-C++ compliant hello world #include int main () { std::cout << "Hello world in ANSI-C++\n"; return 0; } KQ: Hello world in ANSI-C++ Mặc dù vậy chúng ta nên sử dụng using namespace để khỏi phải viết toán tử :: khi tam chiếu đến các đối tượng chuẩn: // ANSI-C++ compliant hello world (II) #include using namespace std; int main () { cout << "Hello world in ANSI-C++\n"; return 0; } Kết quả: Hello world in ANSI-C+ Tên của các file C cũng có một số thay đổi. Bạn có thể tìm thêm thông tin về tên mới của các file header chuẩn trong tài liệu Các file header chuẩn. TÀI LIỆU THAM KHẢO [1] Phạm Văn Ất, Kỹ thuật lập trình C, NXB Thống kê, 2003 [2] Lê Hoài Bắc, Nguyễn Thanh Nghị, Kỹ năng lập trình, nhà xuất bản Khoa học và kỹ thuật – Hà Nội, 2005 [3] Đinh Mạnh Tường, Cấu trúc dữ liệu và giải thuật, NXB Giáo dục, 2002
File đính kèm:
- bai_giang_lap_trinh_nang_cao_nguyen_hai_minh.docx