chim bay

Image 1
Image 2
Image 3
Image 4
Showing posts with label Kinh nghiệm - Lập trình. Show all posts
Showing posts with label Kinh nghiệm - Lập trình. Show all posts

Saturday, November 28, 2015

merge và xử lý lỗi conflic Pull Request trên Git

Hướng dẫn merge và xử lý lỗi conflic Pull Request trên Git

Giả sử chúng ta chưa có gì bây giờ muốn merge các pull request từ các lập trình viên. Hãy tạo một thư mục nào đó và click chuột phải vào thư mục đó chọn Git Bash. Tiếp theo đánh lệnh:
$ git init
$ git clone https://github.com/nukeviet/nukeviet.git -b develop .
Hai lệnh trên để thiết lập một kho mới và tải kho nukeviet về thư mục vừa tạo. Khi clone xong mặc định sẽ chọn nhánh develop. (chú ý có dấu chấm ở cuối trong câu lệnh git clone, nếu không nó sẽ tạo vào trong 1 thư mục nữa )
Bây giờ ta tiếp tục nhập lệnh sau:
$ git remote add vuthao https://github.com/vuthao/nukeviet.git
$ git fetch vuthao
$ git merge vuthao/develop
3 lệnh đó sẽ thực hiện việc tạo một nhánh ảo từ thành viên vuthao trên github sau đó lấy toàn bộ pull request của thành viên đó, nhập nhánh develop của vuthao vào nhánh đang làm việc của kho code hiện tại (tức nhánh develop của nukeviet).
Nếu quá trình merge tự động diễn ra thành công ta chỉ cần thực hiện lệnh
$ git push origin develop
Để hoàn tất việc merge pull request
Nếu merge bị conflic thì ta phải xử lý các file conflic trước, sau đó đưa vào index, commit rồi mới push lên kho code. Thứ tự thực hiện như sau.
Để xem thử conflic chỗ nào ta đánh
$ git diff
Khi đó các file bị conflic sẽ hiển thị đoạn code lỗi. Ta chỉ cần mở các file đó lên, chỉnh sửa đoạn code đó. Sau khi chỉnh sửa xong ta add file đó vào index bằng cách dùng lệnh
$ git add [file]
trong đó [file] là đường dẫn đến file ví dụ
$ git add modules/news/admin/content.php
Để xem trạng thái các file, thư mục … dùng lệnh
$ git status
Để hiển thị status gọn hơn ta dùng option -s
$ git status -s
Sau khi đã hoàn tất xử lý conflic … ta commit các chỉnh sửa vừa rồi:
$ git commit -m "Message"
Trong đó message là nội dung commit. Cuối cùng là push lên kho code.
$ git push origin develop

Một số lệnh git khác

Để tạm thời bỏ qua những thay đổi trong một tập tin nhất định, không muốn tập tin đó hiển thị trong các file thay đổi sử dụng lệnh:
git update-index --assume-unchanged path_file
Trong NukeViet Không đưa các file sau lên kho code khi có thay đổi .htaccess data/config_global.php data/ip_files/*.php
Sau đó, khi muốn tập tin đó hiển thị trong các file thay đổi:
git update-index --no-assume-unchanged path_file
Thay đổi commit message cuối
git commit -a --amend -m "My new commit message"
Reset project về ID trước đó
git reset --hard idcommit
Sau đó dùng lệnh sau để cập nhật lên kho git
fix commit: git push -f
fix merge conflicts in Git?
git mergetool
Liệt kê tất cả các file thay đổi từ id 3bf2d29 đến id b2d0398
git diff-tree -r --name-only 3bf2d29 b2d0398
Liệt kê tất cả các file đã xóa từ id 3bf2d29 đến id b2d0398
git diff-tree -r --name-only --diff-filter=D 3bf2d29 b2d0398
Zip lại các file thay đổi từ id 3bf2d29 đến id b2d0398 (không có file đã xóa)
git archive -o update1.zip HEAD $(git diff-tree -r --name-only --diff-filter=ACMRT 3bf2d29 b2d0398)
Ghi chú: Các option của diff-filter –diff-filter=[ACDMRTUXB*]
  A Added
  C Copied
  D Deleted
  M Modified
  R Renamed
  T have their type (mode) changed
  U Unmerged
  X Unknown
  B have had their pairing Broken
  * All-or-none
Xóa 1 nhánh trên kho code
git push origin --delete BRANCH_NAME
Đưa các tag được đánh dầu lên kho code
git push --tags

Monday, October 26, 2015

Tiền xử lý và Chỉ định nghĩa macro

nguồn coppy : muteszhacker.wordpress.com.

1. Giới thiệu chung

Chắc hẳn những ai đã học qua C/C++ đều biết đến bộ tiền xử lý (preprocessor), nhưng không phải ai cũng biết rằng nhiều ngôn ngữ biên dịch khác cũng hỗ trợ chức năng này. Sự ra đời của preprocessor bắt nguồn từ sự hạn chế của bộ nhớ máy tính trước đây, không thể chứa hết toàn bộ chương trình nguồn để dịch, do vậy, C/C++ và nhiều ngôn ngữ khác như Visual Basic, Pascal hay thậm chí cả C# đã không dịch ngay chương trình nguồn mà tách riêng thành 2 giai đoạn: giai đoạn Tiền xử lý để xử lý các tệp nguồn, ghi nhận các tùy chọn biên dịch và giai đoạn Dịch. Sau khi thực hiện 2 giai đoạn này, ta mới được mã máy chương trình mong muốn. Bài viết này mạn phép xin được thống kê, trình bày về một số chỉ thị (directive) tiền xử lý trong C++.

2. Chỉ thị #include

Các chỉ thị tiền xử lý trong C/C++ đều bắt đầu bởi ký tự # (hash sign), có thể được đặt ở bất cứ đâu trong mã nguồn chương trình. Sau dấu #, bạn có thể đặt vào một vài dấu trắng (cách, tab), trình biên dịch sẽ bỏ qua chúng. #include là chỉ thị hay gặp nhất. Khi bộ tiền xử lý gặp chỉ thị này trong mã nguồn, nó sẽ thay thế chỉ thị này bằng nội dung trong file mà chỉ thị này đề cập. Có 2 cách sử dụng:
 #include <iostream> 
 #include "stdafx.h"
Điều đầu tiên phải nói: chỉ thị không phải là câu lệnh, cho nên không có dấu ;. Sau này một số ngôn ngữ khác dẫn xuất từ C/C++ như Java, Python kế thừa lại tính năng này, nhưng đã biến nó thành dòng lệnh.
Cách thứ nhất được sử dụng với tập tin header thư viện mà trình biên dịch cung cấp. Nếu trong C, đó có thể là stdio.h hay conio.h mà ta thường gặp. Trong C++, đó có thể là file iostream hay string. Còn cách thứ 2, được sử dụng với đường dẫn đến tập tin header thư viện (.h, .hpp) hoặc file nguồn khác (.c, .cc, .cpp) mà người dùng tự định nghĩa (đường dẫn tương đối hoặc tuyệt đối).
Ngoài ra, nếu ai đã làm quen với Microsoft Assembly 32bit, sẽ thấy có 2 loại câu lệnh:
 include kernel32.inc  ; Include header file
 includelib kernel32.lib  ; Include library file
 ; In high programming language, programmers need only include header files, 
 ; compiler / interpreter will automatically include library files
Trong nhiều ngôn ngữ khác, ta đều có lệnh tương đương với chỉ thị #include:
Java:
 import javax.swing.*;
Python:
 import math
Visual Basic:
 imports System
C#:
 using System;
Thậm chí, trong Pascal cũng cung cấp công cụ tương đương, đó là chỉ thị biên dịch:
 {$INCLUDE Test1.pas}
Hoặc ngắn gọn hơn:
 {$I Test1.pas}
Đến đây, ta nhận ra, hầu như mọi ngôn ngữ bậc cao đều hỗ trợ lệnh / chỉ thị cho phép chèn mã nguồn file khác vào file hiện tại, như đã nói ở đầu bài viết, nó là hệ quả của sự hạn chế của bộ nhớ trước đây. Ngoài ra, việc chia mã nguồn thành nhiều file cũng giúp lập trình viên quản lý code được thuận tiện hơn, tạo điều kiện để xây dựng chương trình lớn.

3. Chỉ thị định nghĩa macro

3.1 Chỉ thị #define

3.1.1 Hằng tượng trưng

Mọi người học C/C++ đều biết chỉ thị define được dùng để định nghĩa hằng tượng trưng (symbolic constant):
 #define MAX_CHILDREN_IN_CHINA 1
Sau lệnh này, mỗi khi trình biên dịch gặp phải xâu MAX_CHILDREN_IN_CHINA trong mã nguồn, nó sẽ thay thế bằng số 1.
Tương tự với #include, các ngôn ngữ khác cũng cung cấp công cụ tương đương nhưng bị giới hạn chức năng hơn. C# kế thừa từ C++, cũng dùng chỉ thị #define nhưng chỉ có thể định nghĩa macro rỗng (sẽ được đề cập bên dưới). Pascal cũng vậy, thông qua chỉ thị biên dịch:
 {$DEFINE UNIX} (*or $D UNIX*)

3.1.2 Macro rỗng

Macro rỗng được dùng để ngăn không cho phép được sử dụng một định danh nào đó trong mã nguồn. Ví dụ:
 #define EMPTY  // Nếu trong mã nguồn có EMPTY, 
                                // nó sẽ không được sử dụng.
Macro rỗng cũng thường được dùng khi ta muốn tạo chương trình chạy trên nhiều nền tảng hệ điều hành. Tham khảo ví dụ sau:
 #define UNIX
 // #define Windows
 #undef Windows

 #ifdef UNIX
  printf("This code will be executed in UNIX");
 #elif Windows
  printf("This code will be executed in Windows");
 #endif
Ý nghĩa của các chỉ thị #undef, #ifdef, #elif, #endif sẽ được đề cập đến ở các phần sau.

3.1.3 Macro có tham số

Như đã nói, khả năng của chỉ thị #define ở các ngôn ngữ khác bị giới hạn hơn C/C++ rất nhiều vì với chỉ thị này, C/C++ có thể cung cấp một công cụ chia nhỏ hàm rất thuận tiện, đặc biệt là khi các hàm này rất đơn giản – macro có tham số. Ví dụ:
Giả sử ta có hàm tính đa thức
 float polynomial(float x) {
  return (x>3.0) ? (x*x*x -1) : (3*x*x + x);
 }
Ta hoàn toàn có thể sử dụng macro sau thay thế:
 #define polynomial(x) ( (x>3.0) ? (x*x*x -1) : (3*x*x + x) )
Gọi hàm:
 printf( "Value is: %f", polynomial(1.2) );
Trong đó, x là tham số, nếu muốn bổ sung thêm tham số khác, ta thêm dấu phẩy để phân cách. Ưu điểm của macro so với hàm bình thường là nó nhận mọi kiểu tham số và không mất thời gian gọi hàm (do đó chương trình chạy nhanh hơn). Tuy nhiên, khuyết điểm đi liền với nó là ta sẽ không thể debug đoạn lệnh trong macro (chính vì vậy hàm inline mới có đất dụng võ). Macro chỉ nên dùng với các hàm đơn giản như tìm min, max, kiểm tra điều kiện…

3.1.4 Truyền một số lượng tham số không định trước vào macro

Bạn đã từng nghe qua varargs (variable arguments – tham biến) trong C/C++? Chắc là chỉ biết nó trong Java thôi. Nếu chưa biết C/C++ sử dụng nó như thế nào thì bạn có thể tham khảo ví dụ sau:
 #include <stdarg.h>

 // Calculate average value of a number list
 double average(int count, ...) {
  va_list ap; //  Required in any file which uses 
                                // the variable argument list (va_) macros

  va_start(ap, count);
                // Requires the last fixed parameter to get the address

  double total = 0;    
  for (int j=0; j<count; ++j)
   total += va_arg(ap, double); 
                        // Requires the type to cast to.
   // Increments ap to the next argument.

  va_end(ap);

  return total/count;
 }
Gọi hàm:
 double avg = average(5, 3.4, 4.0, 0.0, 50.1, 0.34);
Đại để varargs là một công cụ cho phép tạo hàm với số lượng tham số không xác định. va_list, va_start,va_end and va_arg là 4 macro được định nghĩa trong thư viện stdarg.h của C và cstdarg của C++.va_list dùng để khai báo danh sách tham biến. va_start định nghĩa biến ap có thêm tham số count là độ dài danh sách. va_arg nạp số tiếp theo từ danh sách tham số. Tham số thứ 2, như trong ví dụ là double, đó là kiểu tham số mà ta mong muốn, cần cẩn thận với tham số này vì nếu ta gọi hàm với tham số sai kiểu mong đợi sẽ dẫn đến lỗi. Cuối cùng, va_end sẽ thu dọn bộ nhớ đã được cấp phát cho danh sách tham biến.
Bạn thấy đấy, cách sử dụng varargs này khá là lằng nhằng mà lại còn tiềm ẩn nguy cơ lỗi lớn. Tuy nhiên ta lại có thể gọi hàm với số lượng và kiểu tham số biến đổi, cũng đáng để mạo hiểm đấy chứ.
Nói dông nói dài về hàm có số lượng tham số biến đổi, bây giờ mới là phần quan trọng nhất của mục này: Truyền một số lượng tham số không định trước vào macro. Một cách rất đơn giản, ta sử dụng macro__VA_ARGS__. Hãy xem ví dụ sau:
 #define LOG(format, ...) printf(format, __VA_ARGS__)
Gọi macro:
 LOG("%c%d%f", 'c', 10, 2.5);
Bạn thấy đấy, sử dụng macro nhiều khi còn tiện hơn hàm rất nhiều.

3.2 Chỉ thị undef

Trong ví dụ ở mục 3.1.2, ta đã thấy chỉ thị này. Đơn giản, nó có tác dụng hủy bỏ định nghĩa một symbol trước đó (do chỉ thị #define tạo ra). Nếu trước đó chưa có định nghĩa nào, thì cũng không ảnh hưởng gì. Tất nhiên, sau khi đã undef thì ta hoàn toàn có thể tái định nghĩa lại bằng #define.

3.3 Toán tử macro

Bộ tiền xử lý cung cấp 2 toán tử có thể được sử dụng để thay thế văn bản (text) này bằng văn bản khác là# và ##.

3.3.1 Toán tử #

Toán tử thứ nhất là stringization tức xâu ký tự hóa. Nếu x là một tham số hinh thức trong macro thì #x sẽ là tham số thực tương ứng. Ví dụ, nếu có:
 #define stringize(x) #x
Thì macro stringize(a+b) sẽ cho kết quả là xâu “a+b”. Kể cả khi tham số của macro la một hằng xâu ký tự, nó cũng trả về nguyên vẹn, tức là stringize(“3”) sẽ trả về “\”3\””.

3.3.2 Toán tử ##

Toán tử ## sẽ kết nối 2 tham số mà bỏ qua khoảng trống giữa chúng. Ví dụ, macro:
 #define glue(a,b) a ## b
Gọi macro:
 glue(c,out) << "test";
Sẽ được trình dịch dịch thành:
 cout << "test";

3.4. Toán tử defined

Toán tử dùng để xác định một từ định danh đã được định nghĩa (bằng chỉ thị #define) hay chưa. Toán tử này chỉ có thể sử dụng kèm với chỉ thị #if và #elif. Nếu định danh đã được định nghĩa thì trả về 1, ngược lại thì trả về 0. Ví dụ:
 #if defined(CREDIT)  // Same to #ifdef CREDIT
  credit();
 #elif defined(DEBIT)  // Same to  #ifdef DEBIT
      debit();

4. Chỉ thị có điều kiện (#ifdef, #ifndef, #if, #endif, #else and #elif)

Các chỉ thị này được dùng để viết nhiều đoạn chương trình khác nhau tương ứng với điều kiện dịch khác nhau. Bằng cách này chúng ta có thể chuyển chương trình từ dạng này sang dạng khác, từ máy này sang máy khác một cách dễ dàng. Có 3 mẫu sau:
 #ifdef ...  // not #ifdef is #ifndef
 ...
 #endif 

 #ifdef ...  // if 
 ...
 #else   // else 
 ...
 #endif 

 #ifdef ...  // if
 ...
 #elif ...  // else if
 ...
 #endif
Tất nhiên là ta hoàn toàn có thể kết hợp, lồng ghép các mẫu trên. Các chỉ thị này được dùng nhiều nhất là trong định nghĩa header thư viện. Ví dụ:
Trong file headerfile.h:
 #ifndef HEADERFILE_H_
 #define HEADERFILE_H_

 class NewClass {
  private:
   // Declare some variables and methods
  public:
   NewClass() {
    // Do something
   }

   void PrintMessage();

   // And more...
 };

 #endif
Trong file headerfile.cpp:
 #include "headerfile.h"

 void NewClass :: PrintMessage() {
  // Print something
 }
Thông qua ví dụ này, ta có thể hiểu cách chia mã nguồn chương trình ra header và mã nguồn chi tiết riêng biệt. Ngoài ra HEADERFILE_H_ là định danh của tên file header headerfile.h mà trình biên dịch biến đổi thành.

5. Chỉ thị #line

Cú pháp:
 #line number "path"
Trong đó number là số hiệu dòng, path là đường dẫn tương đối hoặc tuyệt đối đến file ta muốn. Nếu để trống thì trình biên dịch sẽ tham chiếu đến file nguồn hiện hành (mặc định).
Khi chúng ta dịch một chương trình và gặp lỗi, trình dịch sẽ hiển thị thông báo lỗi với tham chiếu đến dòng lệnh gây lỗi trong file cụ thể. Tuy nhiên ta có thể sử dụng chỉ thị #line để điều khiển file mà ta muốn tham chiếu đến khi gặp lỗi. Giả sử, ta có file errors.h có nội dung:
 int x, y;  // Line 1
 int a;   // Line 2
Và file main.cpp nằm cùng thư mục với errors.h
 #line 8 "errors.h" // Line 1
    // Line 2
 int main(void) { // Line 3
  a=5;  // Line 4
  return 0; // Line 5
 }   // Line 6
Khi biên dịch file main.cpp, đến lệnh gán a = 5, trình dịch phát hiện lỗi và thông báo, ví dụ CodeBlock đưa ra các thông tin sau:
  • File chứa lỗi (File): errors.h
  • Dòng lỗi (Line): 10
  • Thông báo (Message): error: ‘a’ was not declared in this scope
Như vậy, nhờ chỉ thị #line mà khi gặp lỗi, trình biên dịch đã không tham chiếu đến file main.cpp mà lại tham chiếu đến file errors.h. Thứ hai,
 Số của dòng lỗi = số trong chỉ thị #line (ở đây là 8) 
          + số hiệu dòng lỗi thực tế (4)  
          - số hiệu của dòng chứa chỉ thị #line (1) - 1

6. Chỉ thị #error

Chỉ thị này được dùng để nhúng vào trong các cấu trúc chỉ thị có điều kiện nhằm mục đích trình dịch sẽ in ra thông báo lỗi và ngừng biên dịch. Ví dụ:
 #if UNIX
 #error This program can not be run in UNIX system.
 #endif

7. Chỉ thị #pragma

#pragma là một chỉ thị cho compiler biết cách dịch chuơng trình theo một số “tùy chọn” đặc biệt, tùy thuộc vào từng trình biên dịch. Sau đây là một ứng dụng của chỉ thị này: Khai báo struct với kích thước không đổi với mọi trình dịch:
 #pragma pack (push)
 #pragma pack (1)

 struct NewStruct {
  int a;
  int b;
  char c;
 };

 #pragma pack (pop)
Thông thường trình biên dịch sẽ làm tròn để kích thước struct là số chia hết cho 4, (hoặc 2, 8, 16… tùy vào cấu hình build). Với cách khai báo trên trong VC++ và gcc, kích thước cấu trúc sẽ luôn luôn không đổi với mọi trình biên dịch, mọi cấu hình.

Một số lỗi và kỹ thuật debug

Programer và Error

Hai người này cực kì thân với nhau, hễ một chút là tới hỏi thăm nhau liền. Có những lỗi Visual có thể báo trong khi biên dịch, mấy cái đó thì dễ xử lý do đó là lỗi cú pháp. Riêng mấy cái mà build xong, F5 cái mới hiện lỗi thì khổ hơn nhiều. Mà tức nữa chứ, đang mừng vì build thành công thế mà lại error! Cheer!
Error gặp hoài, nên đừng ngại, bạn cần phải giết nó, không thể để lộng hành được. Để làm được điều này, bạn cần biết được nguyên nhân của lỗi sau đó thì xử lý. Bình tĩnh rút kinh nghiệm cho lần sau, OK?
Có hai loại lỗi, một là lỗi được phát hiện khi build gọi là Syntax Error (lỗi cú pháp), hai là Run-time Error (lỗi khi thực thi). Syntax Error xuất hiện khi bạn viết code, Visual Studio dựa vào quy định cú pháp của C++ để dò, nên nó luôn luôn có thể thông báo lỗi cho bạn bằng cách in một vệt lươn lẹo màu đỏ bên dưới:
Không xác định được biến
Như ở trên, tôi gặp lỗi cú pháp, thông báo rằng “adffg không xác định”, cứ viết đại là nó sẽ báo thế.
Còn run-time error là cái bảng này:
Lỗi Run-time
Chắc cũng thấy quen quen ha.

Syntax Error

Lỗi cú pháp. Khi mới học C++, lỗi cú pháp chúng ta gặp rất nhiều (kể cả tôi) vì lúc đó đã quen với cách viết đâu. Thường gặp nhất là chuyện quên mất dấu ;
Thiếu dấu ;
Bên trên là trường hợp dễ, Visual có thể dò chính xác, thông báo đúng lỗi cho bạn. Thường thì nó sẽ báo phía sau. Cách giải quyết Syntax Error là gì: bạn cần nắm vững cú pháp C++, không quên cái gì hết. Sau đó, thấy lỗi thì di chuyển tới, xem nội dung lỗi nghĩa là gì (cố gắng dịch hen), rồi quay lại chỉnh sửa cho phù hợp.
Để chắc rằng bạn không còn syntax error nào nữa, cứ thử build chương trình xem, nếu build không thành công, có thể bạn đang gặp lỗi cú pháp đó. Khi build thất bại, bạn có hai lựa chọn:
  1. Dùng cửa số Output, xem nội dung lỗi:
Xem lỗi bằng cửa sổ Output
Sau đó chọn Build như trên, cửa sổ output sẽ hiện ra tất cả thông tin được ghi lại trong quá trình build.
Thường thì, trong dự án lớn, nó sẽ in ra rất nhiều, có khi cả chục dòng. Nếu gặp lỗi, bạn luôn nên xử lý lỗi đầu tiên (các lỗi sau thường do lỗi đầu tiên tạo ra), ấn Next Mesage chuyển nhanh lên trên đầu. Sau đó bạn tìm dòng có khi chữ error, đọc rồi giải quyết nó đi.
Xem lỗi đầu tiên
Gợi ý: nhấp đúp chuột vào nội dung lỗi để Visual Studio chỉ đến chính xác nơi gặp lỗi giùm bạn, khỏi phải tìm tốn công.
  1. Dùng cửa sổ Error List, coi và cập nhật các lỗi đã xảy ra.
Cửa sổ Error List
Đó, có mỗi một dấu ; thôi mà hiện ra tới 2 lỗi. Cũng tương tự, ấn đúp vào lỗi để Viusal mở vị trí lỗi giùm bạn.
Dưới đây là danh sách các lỗi mà các bạn có thể gặp:
  • “Expected a ;” hay “missing ‘;’ before ‘}’” là do bạn thiếu dấu ; kết thúc lệnh, tìm và bổ sung vào.
  • “Identifier “???” is undefined”: có thể bạn viết sai, hoặc chưa khai báo kiểu dữ liệu. Sửa lại cho đúng.
  • “redefinition” hoặc “already defined”: khác với lỗi ở trên, lần này bạn đã khai báo một kiểu dữ liệu, một hàm gì đó đến hai lần, điều này không cho phép. Bạn bắt buộc phải dùng tên khác hoặc xoá nó đi.
  • “has no member ???”: trong struct hoặc class bạn đang dùng không có biến/hàm thành viên đó, coi lại khuôn mẫu (khai báo) của struct/class đó đi, xem có hàm/biến đó không? Không có thì thêm vào.
  • “using without initialzied”: biến khởi tạo nhưng chưa gán giá trị cho nó.
Ngoại trừ mấy lỗi dễ nhận thấy đó, nhiều lúc bạn vẫn có thể gõ sai, nhưng đối với Visual, bạn vẫn viết đúng cú pháp, mỗi tội nó hiểu không đúng ý bạn.
Lỗi loại này thường gặp nhất là so sánh bằng và gán bằng. So sánh bằng sử dụng hai dấu bằng (==) trong khi đó gán bằng chỉ có 1 (=), nhớ chú ý kĩ nhé, Visual không báo cho bạn biết chuyện này đâu!
Lỗi thứ hai thuộc về logic, khi sử dụng khối lệnh. Bạn đoán xem chương trình dưới đây sẽ in ra giá trị bao nhiêu?
1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
using namespace std;
 
void main(){
    int i = 0;
    i++;
    {
        int i = 4;
        i++;
    }
    cout << i;
}
Nhớ tới vòng đời của biến, cái trên thì dễ nhận ra, chứ vòng for như thế này thì coi chừng:
1
2
3
4
5
6
7
8
9
10
11
#include <iostream>
using namespace std;
 
void main(){
    int i = 0;
    i++;
    for (int i = 0; i < 10; i++){
        i++;
    }
    cout << i;
}

Run-time Error

Sau khi build thành công, bạn chỉ mới vượt qua giai đoạn Syntax Error, lỗi cú pháp. Chẳng vui mừng gì khi vẫn còn một dạng lỗi nữa, thường được gọi là lỗi ngữ nghĩa – nó xảy ra do bạn thao tác không đúng khiến chương trình chạy sai mục đích thiết kế ban đầu.
Lỗi run-time nhiều lắm, đa số do con trỏ gây ra không, chịu khó tưởng tượng ra quá trình chạy của chương trình để fix lỗi nhé. Nói ngắn gọn thế thôi.

Các công cụ Debug

Phần quan trọng là chỗ này. Trước tiên, các công cụ debug chỉ hỗ trợ bạn fix mấy cái lỗi run-time (lỗi ngữ nghĩa), còn việc bạn viết sai cú pháp thì nó không chịu trách nhiệm đâu nhé.
Point-to-view
Tính năng này chắc hẳn khá quen thuộc. Khi chương trình bạn gặp lỗi, nó dừng lại (ấn Break). Sau đó Visual sẽ thông báo cho bạn vị trí chương trình đang dừng lại
Ví trí chương trình đang dừng
Ngoài ra, để biết chương trình của bạn vẫn còn chạy, hay bạn đang ở trong edit mode, đó là thanh status bar bên dưới (màu xanh dương là edit mode):
Running mode
Như trên, bạn đang debug chương trình. Không phải edit mode.
Xong, tính năng point-to-view sẽ hiển thị giá trị mà biến đang lưu trữ khi bạn đưa con chuột lên trên tên biến đó:
Công cụ Debug: Point to view
Nhờ vậy bạn có thể lập tức biết giá trị hiện tại vào lúc đó của biến là bao nhiêu, đỡ tốn công in nó ra màn hình. Không chỉ dừng ở các biến bình thường, tính năng này còn có thể hiển thị dữ liệu của mảng, struct, class rất thú vị đó.
Breaking points
Để kích hoạt tính năng point-to-view, bạn phải làm chương trình bị lỗi giống như tôi ở trên. Có cách khác bắt chương trình tạm ngừng đó là break points. Bạn sẽ đặt break point tại vị trí, đoạn code mong muốn, sau đó F5, chương trình sẽ chạy tới đó, tự động dừng lại.
Công cụ Debug: Breaking point
Di chuyển con chuột qua trái, tới cái cột màu xám. Đó là nơi chuyên dụng của break point, khi click vào đó. Một dấu chấm đỏ như hình trên sẽ xuất hiện thông báo cho bạn biết. Khi debug, chương trình sẽ chạy tới đó rồi tự động dừng lại.
Thử xem nào:
Đặt breaking point và chương trình đang dừng ở đó
Đó, chương trình chạy tới dòng chúng ta đã chỉ định (có dấu mũi tên màu vàng đó), lúc này, bạn có thể sử dụng tính năng point-to-view cách thoải mái rồi.
Để xoá break point, bạn chỉ cần click chuột lần nữa vào chấm tròn đỏ đó, chấm tròn biến mất là xong.
Khi chương trình đang tạm ngừng vì break point như thế này, để tiếp tục chạy, bạn ấn F5 hoặc Debug -> Continue (nút play màu xanh lá).
Hoặc bạn có thể dừng nó lại ngay lập tức bằng cách ấn Shift + F5 hoặc:
Dừng Debug
Conditional breaking points
Break point còn có 1 tính năng rất thú vị nữa, đó là khả năng dừng khi thoả 1 điều kiện.
1
2
3
4
5
6
7
8
9
10
11
#include <iostream>
using namespace std;
 
void main(){
    int i = 0;
    i++;
    for (i = 10; i > 0; i--){
        cout << i;
    }
    i = 5 / i;
}
Bạn hãy đặt 1 break point bên trong vòng for và chạy thử, cứ mỗi vòng lặp, nó lại dừng. Điều này có vẻ khá nhức nhối, tại sao lại không dùng Conditional Break Point?
Sau khi đặt break point thành công, bạn ấn phải vào nó (chấm tròn đỏ), rồi chọn Condition… Trong ô condition, tôi ghi i == 5, nghĩa là tôi muốn dừng lại khi i = 5. Bên dưới cứ để nguyên “Is true”. Ấn OK và F5 thử liền nào.
Chương trình đã dừng lại, trỏ con chuột tới biến i, nó ghi giá trị của i là 5. Như vậy điều kiện đã có hiệu lực.
Step
Công cụ Debug: Step
Có hai loại là Step Into (F11) và Step Over (F10). Khi chương trình đang tạm ngừng, ấn F10 sẽ khiến cho chương trình của bạn chạy thêm 1 lệnh nữa rồi lại ngưng. OK, đơn giản có thế. F10 sẽ chạy một lệnh trong hàm đó, nếu bạn có gọi một hàm bên ngoài nữa, nó vẫn không nhảy vô hàm bạn gọi, mà cố thủ trong hàm mà nó đang dừng. Để nhảy vô bên trong hàm bạn gọi, thay vì ấn F10, hãy ấn F11.
Để kiểm tra, bạn đặt break point tại hàm cout. Sau đó F5 chờ chương trình break. Ấn F10 vài lần để hiểu rõ hơn chức năng của nó.
Calling stack
Thêm một công cụ nữa rất mạnh, đó là cửa số Calling Stack. Tôi có chương trình đệ quy dưới đây:
1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
using namespace std;
 
void Dec(int i)
{
    if (i == 0) return;
    Dec(i - 1);
}
 
void main(){
    Dec(10);
}
Bạn hãy đặt 1 conditional break point tại lệnh if, điều kiện là i == 5. Xong chạy chương trình và chờ nó break.
Công cụ Debug: Call Stack
Khi chương trình tạm ngưng, Call Stack sẽ như thế này. Bạn đoán ra nó muốn thông báo cái gì chưa? Quay lại chương trình của chúng ta, tôi gọi hàm Dec với tham số 10, hàm tạm dừng và gọi Dec với tham số 9, cứ thế có tổng cộng 5 lần hàm Dec phải tạm dừng. Đếm thử bên trên xem có mấy hàm Dec nào, có tới 6 lận vì lần thứ 6 dừng do gặp break point, không tính. Coi kĩ lại những gì mà Calling Stack muốn nói, bạn để ý tới chữ ‘Line 7’ không, nó là nơi mà hàm đang tạm dừng để chờ Dec kia kết thúc đó.
Cửa số này mô tả quá trình gọi hàm, thông báo cho bạn biết thứ tự gọi hàm là gì. Bắt đầu từ hệ thống, chúng ta trải qua 2 hàm phụ (của trình biên dịch) trước khi thực thi hàm main. Đúng không, tiếp đó, hàm main gọi hàm Dec, Dec lại gọi Dec,… cứ thế. Dấu chấm tròn đỏ có mũi tên vàng chỉ thị nơi chương trình đang dừng, oh, nó đang dừng ở đỉnh stack đó.
Bạn có thể nhấp đôi vào từng dòng, Visual sẽ hiển thị đoạn mã cho bạn, khỏi phải tìm. Cửa sổ này thực sự hữu ích khi bạn cần biết chương trình mình đang đi đâu, gọi hàm có đúng không,… Còn nhiều lắm mà chính tôi cũng không thể mô tả. Thật đó, có những lúc các công cụ debug trên không thể giúp được gì, mà Calling Stack có thể đó.
Watch
Tính năng này tương tự như cái point-to-view. Thay vì bạn phải di chuyển con chuột, nó tự hiển thị cho bạn luôn. Để hiển thị biến trong Watch, bạn chỉ cần ấn phải vào biến, chọn Add Watch… Tôi rất ít khi dùng tính năng này, thật sự nó cũng rất hữu ích, nhưng vì sự xuất hiện của bảng Autos với Local khiến tôi phải bỏ bê nó rồi.
Cửa sổ Auto sẽ tư động Watch những biến mà nó cho rằng bạn sẽ cần, nên tôi không phải thêm vào watch. Còn nữa, cửa sổ Locals sẽ hiện tất cả các biến cục bộ trong hàm mà chương trình bạn đang dừng. Thế là xong, tôi bỏ watch luôn.
Công cụ Debug: Auto Watch
Chiến lược debug
Trên đó là vài tính năng cơ bản cần thiết với bạn. Tuy nhiên, thật sự lãng phí nếu các bạn chưa có một chiến lược debug hoàn hảo để có thể nhanh chóng khoanh vùng vị trí lỗi. Tiếp theo đây tôi sẽ chỉ cho bạn một số mẹo vặt khá hữu ích.
Để biết hàm đó có chạy hay không, hãy để 1 break point tại đầu hàm, ấn F5. Nếu chương trình dừng lại, OK, nó có chạy. Không thì có vấn đề rồi.
Nếu bạn gặp một lỗi khá oái oăm (gặp rất nhiều đấy), nó kiến cho giá trị của biến bị thay đổi, gây lỗi hàng loại cho phần code đằng sau. Nhiệm vụ hàng đầu là phải tìm ra nơi đã thực hiện việc đó. Đầu tiên là một vùng lớn, từ lúc chạy cho tới chỗ bị lỗi. Bắt đầu khoanh vùng như sau:
  • Đánh giá một vị trí nào đó, nếu nghi ngờ, đặt break point tại khu vực bên trên. Chạy thử.
  • Nếu trước khi vào khu vực đó, biến chưa bị thay đổi, như vậy phần đằng trước OK, chạy tốt không vấn đề, đưa break point xuống dưới, thu hẹp phạm vi. Nếu đã bị thay đổi, dừng chương trình, dịch chuyển break point lên trên.
  • Sau khi xác định được vùng gây sự và vùng đó đủ nhỏ, đặt break point, sau đó kết hợp F10, chạy từng lệnh một cho đến khi phát hiện ra ví trí biến bị thay đổi không mong muốn.
Điều quan trọng là bạn phải hiểu dòng chảy chương trình của bạn. Nó đi đâu, làm gì bạn phải quản lý được. Thế nhé.
Set next to run
Khi chương trình đang tạm dừng (break), một dấu mũi tên màu vàng hiện lên, thông báo nơi mà chương trình của bạn đang tạm ngừng. Ngoài chức năng đó, chỉ cần sử dụng chuột, kéo thả nó ra vị trí mới. Lúc này, bạn đã thay đổi vị trí đang thực hiện của chương trình, nếu ấn F5, mũi tên màu vàng ở đâu, chương trình của bạn sẽ bắt đầu chạy tại đó.
Edit and run
Công cụ này tôi ít khi dùng, mà đôi lúc lại hữu ích lắm. Giờ, chương trình đang break, bạn vào code, sửa biến sửa sửa gì đó. Sau đó ấn F5, Visual Studio sẽ biên dịch lại phần đã chỉnh sửa đó rồi cho chương trình bạn chạy tiếp mà không phải khởi động lại. Hay quá luôn.

Lời kết

Các công cụ Debug ở trên tôi chỉ giới thiệu cách sơ qua cho bạn hiểu chức năng của chúng. Các kĩ năng sẽ dần hoàn thiện nếu bạn tiếp tục coding, coding.
Chuỗi bài học C++ cơ bản tới đây là kết thúc, rất vui nếu bạn có thể theo học tới đây. Tôi chắc rằng bạn đã có một khối lượng kinh nghiệm khá lớn rồi. Tự tin không? những gì bạn được nghe tôi trình bày chỉ là những thứ cơ bản, là nền móng để bạn có thể tiếp tục nghiên cứu sâu hơn. Thế giới C++ còn rất rộng lớn mà chuỗi bài cơ bản này không thể giải quyết hết được. Còn rất nhiều khái niệm, từ khoá nữa, nếu muốn tìm hiểu, bạn có thể lên cplusplus chẳng hạn (google đi). Nhớ rằng nên tìm kiếm bằng tiếng anh, kết quả thu được sẽ nhiều hơn bạn tưởng tượng đấy. Các bài viết tiếng anh rất chi tiết và dễ hiểu (đa số), bạn cứ tự tin, không có gì phải sợ cả.
Giả sử như bạn muốn tìm hiểu template, lên google gõ “template c++”, enter và khám phá nhé. Hay “Inheritance” thì gõ y chữ đó, thêm cái đuôi C++ vô là xong.