#define macro để in gỡ lỗi trong C?


209

Cố gắng tạo một macro có thể được sử dụng để in thông báo gỡ lỗi khi DEBUG được xác định, như mã giả sau đây:

#define DEBUG 1
#define debug_print(args ...) if (DEBUG) fprintf(stderr, args)

Làm thế nào điều này được thực hiện với một macro?


Trình biên dịch (gcc) sẽ tối ưu hóa các câu lệnh như if (DEBUG) {...}, nếu trong mã sản xuất, macro DEBUG được đặt thành 0? Tôi hiểu rằng có những lý do chính đáng để trình duyệt gỡ lỗi hiển thị cho trình biên dịch, nhưng vẫn còn một cảm giác xấu. -Pat
Pat

Câu trả lời:


410

Nếu bạn sử dụng trình biên dịch C99 trở lên

#define debug_print(fmt, ...) \
            do { if (DEBUG) fprintf(stderr, fmt, __VA_ARGS__); } while (0)

Nó giả sử bạn đang sử dụng C99 (ký hiệu danh sách đối số biến không được hỗ trợ trong các phiên bản trước). Thành do { ... } while (0)ngữ đảm bảo rằng mã hoạt động như một câu lệnh (gọi hàm). Việc sử dụng mã vô điều kiện đảm bảo rằng trình biên dịch luôn kiểm tra xem mã gỡ lỗi của bạn có hợp lệ không - nhưng trình tối ưu hóa sẽ xóa mã khi DEBUG bằng 0.

Nếu bạn muốn làm việc với #ifdef DEBUG, thì hãy thay đổi điều kiện kiểm tra:

#ifdef DEBUG
#define DEBUG_TEST 1
#else
#define DEBUG_TEST 0
#endif

Và sau đó sử dụng DEBUG_TEST nơi tôi đã sử dụng DEBUG.

Nếu bạn nhấn mạnh vào một chuỗi ký tự cho chuỗi định dạng (dù sao cũng có thể là một ý tưởng tốt), bạn cũng có thể giới thiệu những thứ như __FILE__, __LINE____func__vào đầu ra, có thể cải thiện chẩn đoán:

#define debug_print(fmt, ...) \
        do { if (DEBUG) fprintf(stderr, "%s:%d:%s(): " fmt, __FILE__, \
                                __LINE__, __func__, __VA_ARGS__); } while (0)

Điều này phụ thuộc vào nối chuỗi để tạo ra một chuỗi định dạng lớn hơn so với lập trình viên viết.

Nếu bạn sử dụng trình biên dịch C89

Nếu bạn bị mắc kẹt với C89 và không có phần mở rộng trình biên dịch hữu ích, thì đó không phải là cách đặc biệt rõ ràng để xử lý nó. Kỹ thuật tôi từng sử dụng là:

#define TRACE(x) do { if (DEBUG) dbg_printf x; } while (0)

Và sau đó, trong mã, viết:

TRACE(("message %d\n", var));

Dấu ngoặc kép rất quan trọng - và là lý do tại sao bạn có ký hiệu vui trong việc mở rộng macro. Như trước đây, trình biên dịch luôn kiểm tra mã cho tính hợp lệ cú pháp (tốt) nhưng trình tối ưu hóa chỉ gọi hàm in nếu macro DEBUG ước lượng khác không.

Điều này không yêu cầu một hàm hỗ trợ - trong ví dụ dbg_printf () - để xử lý những thứ như 'stderr'. Nó đòi hỏi bạn phải biết cách viết các hàm varargs, nhưng điều đó không khó:

#include <stdarg.h>
#include <stdio.h>

void dbg_printf(const char *fmt, ...)
{
    va_list args;
    va_start(args, fmt);
    vfprintf(stderr, fmt, args);
    va_end(args);
}

Tất nhiên, bạn cũng có thể sử dụng kỹ thuật này trong C99, nhưng __VA_ARGS__kỹ thuật này gọn gàng hơn vì nó sử dụng ký hiệu chức năng thông thường, không phải là hack ngoặc kép.

Tại sao điều quan trọng là trình biên dịch luôn nhìn thấy mã gỡ lỗi?

[ Bình luận lại được thực hiện cho một câu trả lời khác. ]

Một ý tưởng trung tâm đằng sau cả hai triển khai C99 và C89 ở trên là trình biên dịch thích hợp luôn nhìn thấy các câu lệnh giống như printf. Điều này rất quan trọng đối với mã dài hạn - mã sẽ kéo dài một hoặc hai thập kỷ.

Giả sử một đoạn mã hầu hết không hoạt động (ổn định) trong một số năm, nhưng bây giờ cần phải thay đổi. Bạn kích hoạt lại dấu vết gỡ lỗi - nhưng thật bực bội khi phải gỡ lỗi mã gỡ lỗi (theo dõi) vì nó đề cập đến các biến đã được đổi tên hoặc thử lại, trong những năm bảo trì ổn định. Nếu trình biên dịch (bộ xử lý trước bài) luôn nhìn thấy câu lệnh in, nó đảm bảo rằng mọi thay đổi xung quanh không làm mất hiệu lực chẩn đoán. Nếu trình biên dịch không nhìn thấy câu lệnh in, nó không thể bảo vệ bạn trước sự bất cẩn của chính bạn (hoặc sự bất cẩn của đồng nghiệp hoặc cộng tác viên của bạn). Xem ' Thực hành lập trình ' của Kernighan và Pike, đặc biệt là Chương 8 (xem thêm Wikipedia về TPOP ).

Đây là "đã có, thực hiện được điều đó" - về cơ bản, tôi đã sử dụng kỹ thuật được mô tả trong các câu trả lời khác trong đó bản dựng không gỡ lỗi không thấy các câu lệnh giống như printf trong một số năm (hơn một thập kỷ). Nhưng tôi đã xem qua lời khuyên trong TPOP (xem nhận xét trước đây của tôi) và sau đó đã kích hoạt một số mã gỡ lỗi sau một số năm và gặp vấn đề về thay đổi bối cảnh phá vỡ việc gỡ lỗi. Một vài lần, việc in ấn luôn được xác nhận đã cứu tôi khỏi những vấn đề sau này.

Tôi chỉ sử dụng NDEBUG để chỉ kiểm soát các xác nhận và một macro riêng (thường là DEBUG) để kiểm soát xem theo dõi gỡ lỗi có được tích hợp vào chương trình hay không. Ngay cả khi theo dõi gỡ lỗi được tích hợp, tôi thường không muốn đầu ra gỡ lỗi xuất hiện vô điều kiện, vì vậy tôi có cơ chế kiểm soát xem đầu ra có xuất hiện không (mức gỡ lỗi và thay vì gọi fprintf()trực tiếp, tôi gọi hàm in gỡ lỗi chỉ in có điều kiện do đó, bản dựng mã giống nhau có thể in hoặc không in dựa trên các tùy chọn chương trình). Tôi cũng có một phiên bản mã 'nhiều hệ thống con' cho các chương trình lớn hơn, để tôi có thể có các phần khác nhau của chương trình tạo ra số lượng dấu vết khác nhau - trong điều khiển thời gian chạy.

Tôi ủng hộ rằng đối với tất cả các bản dựng, trình biên dịch sẽ thấy các báo cáo chẩn đoán; tuy nhiên, trình biên dịch sẽ không tạo bất kỳ mã nào cho các câu lệnh theo dõi gỡ lỗi trừ khi bật gỡ lỗi. Về cơ bản, điều đó có nghĩa là tất cả các mã của bạn được trình biên dịch kiểm tra mỗi khi bạn biên dịch - cho dù để phát hành hay gỡ lỗi. Đây là một điều tốt!

debug.h - phiên bản 1.2 (1990-05-01)

/*
@(#)File:            $RCSfile: debug.h,v $
@(#)Version:         $Revision: 1.2 $
@(#)Last changed:    $Date: 1990/05/01 12:55:39 $
@(#)Purpose:         Definitions for the debugging system
@(#)Author:          J Leffler
*/

#ifndef DEBUG_H
#define DEBUG_H

/* -- Macro Definitions */

#ifdef DEBUG
#define TRACE(x)    db_print x
#else
#define TRACE(x)
#endif /* DEBUG */

/* -- Declarations */

#ifdef DEBUG
extern  int     debug;
#endif

#endif  /* DEBUG_H */

debug.h - phiên bản 3.6 (2008/02/11)

/*
@(#)File:           $RCSfile: debug.h,v $
@(#)Version:        $Revision: 3.6 $
@(#)Last changed:   $Date: 2008/02/11 06:46:37 $
@(#)Purpose:        Definitions for the debugging system
@(#)Author:         J Leffler
@(#)Copyright:      (C) JLSS 1990-93,1997-99,2003,2005,2008
@(#)Product:        :PRODUCT:
*/

#ifndef DEBUG_H
#define DEBUG_H

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif /* HAVE_CONFIG_H */

/*
** Usage:  TRACE((level, fmt, ...))
** "level" is the debugging level which must be operational for the output
** to appear. "fmt" is a printf format string. "..." is whatever extra
** arguments fmt requires (possibly nothing).
** The non-debug macro means that the code is validated but never called.
** -- See chapter 8 of 'The Practice of Programming', by Kernighan and Pike.
*/
#ifdef DEBUG
#define TRACE(x)    db_print x
#else
#define TRACE(x)    do { if (0) db_print x; } while (0)
#endif /* DEBUG */

#ifndef lint
#ifdef DEBUG
/* This string can't be made extern - multiple definition in general */
static const char jlss_id_debug_enabled[] = "@(#)*** DEBUG ***";
#endif /* DEBUG */
#ifdef MAIN_PROGRAM
const char jlss_id_debug_h[] = "@(#)$Id: debug.h,v 3.6 2008/02/11 06:46:37 jleffler Exp $";
#endif /* MAIN_PROGRAM */
#endif /* lint */

#include <stdio.h>

extern int      db_getdebug(void);
extern int      db_newindent(void);
extern int      db_oldindent(void);
extern int      db_setdebug(int level);
extern int      db_setindent(int i);
extern void     db_print(int level, const char *fmt,...);
extern void     db_setfilename(const char *fn);
extern void     db_setfileptr(FILE *fp);
extern FILE    *db_getfileptr(void);

/* Semi-private function */
extern const char *db_indent(void);

/**************************************\
** MULTIPLE DEBUGGING SUBSYSTEMS CODE **
\**************************************/

/*
** Usage:  MDTRACE((subsys, level, fmt, ...))
** "subsys" is the debugging system to which this statement belongs.
** The significance of the subsystems is determined by the programmer,
** except that the functions such as db_print refer to subsystem 0.
** "level" is the debugging level which must be operational for the
** output to appear. "fmt" is a printf format string. "..." is
** whatever extra arguments fmt requires (possibly nothing).
** The non-debug macro means that the code is validated but never called.
*/
#ifdef DEBUG
#define MDTRACE(x)  db_mdprint x
#else
#define MDTRACE(x)  do { if (0) db_mdprint x; } while (0)
#endif /* DEBUG */

extern int      db_mdgetdebug(int subsys);
extern int      db_mdparsearg(char *arg);
extern int      db_mdsetdebug(int subsys, int level);
extern void     db_mdprint(int subsys, int level, const char *fmt,...);
extern void     db_mdsubsysnames(char const * const *names);

#endif /* DEBUG_H */

Biến thể đối số duy nhất cho C99 trở lên

Kyle Brandt hỏi:

Dù sao để làm điều này để debug_printvẫn hoạt động ngay cả khi không có đối số? Ví dụ:

    debug_print("Foo");

Có một cách đơn giản, lỗi thời:

debug_print("%s\n", "Foo");

Giải pháp chỉ dành cho GCC được hiển thị bên dưới cũng cung cấp hỗ trợ cho việc đó.

Tuy nhiên, bạn có thể làm điều đó với hệ thống C99 thẳng bằng cách sử dụng:

#define debug_print(...) \
            do { if (DEBUG) fprintf(stderr, __VA_ARGS__); } while (0)

So với phiên bản đầu tiên, bạn mất kiểm tra giới hạn yêu cầu đối số 'fmt', điều đó có nghĩa là ai đó có thể cố gắng gọi 'debug_print ()' mà không có đối số (nhưng dấu phẩy trong danh sách đối số fprintf()sẽ không được biên dịch) . Cho dù mất kiểm tra là một vấn đề ở tất cả là tranh cãi.

Kỹ thuật dành riêng cho GCC cho một đối số

Một số trình biên dịch có thể cung cấp tiện ích mở rộng cho các cách khác để xử lý danh sách đối số có độ dài thay đổi trong macro. Cụ thể, như được ghi nhận lần đầu trong các nhận xét của Hugo Ideler , GCC cho phép bạn bỏ qua dấu phẩy thường xuất hiện sau đối số 'cố định' cuối cùng đối với macro. Nó cũng cho phép bạn sử dụng ##__VA_ARGS__trong văn bản thay thế macro, sẽ xóa dấu phẩy trước ký hiệu if, nhưng chỉ khi, mã thông báo trước đó là dấu phẩy:

#define debug_print(fmt, ...) \
            do { if (DEBUG) fprintf(stderr, fmt, ##__VA_ARGS__); } while (0)

Giải pháp này vẫn giữ được lợi ích của việc yêu cầu đối số định dạng trong khi chấp nhận đối số tùy chọn sau định dạng.

Kỹ thuật này cũng được Clang hỗ trợ để tương thích GCC.


Tại sao vòng lặp do-while?

Mục đích của việc này là do whilegì?

Bạn muốn có thể sử dụng macro để nó trông giống như một lệnh gọi hàm, có nghĩa là nó sẽ được theo sau bởi dấu chấm phẩy. Do đó, bạn phải đóng gói cơ thể macro cho phù hợp. Nếu bạn sử dụng một ifcâu lệnh mà không có xung quanh do { ... } while (0), bạn sẽ có:

/* BAD - BAD - BAD */
#define debug_print(...) \
            if (DEBUG) fprintf(stderr, __VA_ARGS__)

Bây giờ, giả sử bạn viết:

if (x > y)
    debug_print("x (%d) > y (%d)\n", x, y);
else
    do_something_useful(x, y);

Thật không may, sự thụt lề đó không phản ánh sự kiểm soát thực tế của dòng chảy, bởi vì bộ tiền xử lý tạo ra mã tương đương với điều này (thụt lề và dấu ngoặc nhọn được thêm vào để nhấn mạnh ý nghĩa thực tế):

if (x > y)
{
    if (DEBUG)
        fprintf(stderr, "x (%d) > y (%d)\n", x, y);
    else
        do_something_useful(x, y);
}

Nỗ lực tiếp theo ở macro có thể là:

/* BAD - BAD - BAD */
#define debug_print(...) \
            if (DEBUG) { fprintf(stderr, __VA_ARGS__); }

Và đoạn mã tương tự bây giờ tạo ra:

if (x > y)
    if (DEBUG)
    {
        fprintf(stderr, "x (%d) > y (%d)\n", x, y);
    }
; // Null statement from semi-colon after macro
else
    do_something_useful(x, y);

elsebây giờ là một lỗi cú pháp. Các do { ... } while(0)tránh loop cả những vấn đề này.

Có một cách khác để viết macro có thể hoạt động:

/* BAD - BAD - BAD */
#define debug_print(...) \
            ((void)((DEBUG) ? fprintf(stderr, __VA_ARGS__) : 0))

Điều này để lại các đoạn chương trình được hiển thị là hợp lệ. Diễn (void)viên ngăn không cho nó được sử dụng trong bối cảnh yêu cầu một giá trị - nhưng nó có thể được sử dụng làm toán hạng bên trái của toán tử dấu phẩy nơi do { ... } while (0)phiên bản không thể. Nếu bạn nghĩ rằng bạn sẽ có thể nhúng mã gỡ lỗi vào các biểu thức như vậy, bạn có thể thích điều này. Nếu bạn muốn yêu cầu bản in gỡ lỗi hoạt động như một câu lệnh đầy đủ, thì do { ... } while (0)phiên bản sẽ tốt hơn. Lưu ý rằng nếu phần thân của macro liên quan đến bất kỳ dấu chấm phẩy nào (nói đại khái), thì bạn chỉ có thể sử dụng do { ... } while(0)ký hiệu. Nó luôn hoạt động; cơ chế phát biểu biểu thức có thể khó áp dụng hơn. Bạn cũng có thể nhận được các cảnh báo từ trình biên dịch với biểu thức mà bạn muốn tránh; nó sẽ phụ thuộc vào trình biên dịch và các cờ bạn sử dụng.


TPOP trước đây tại http://plan9.bell-labs.com/cm/cs/tpophttp://cm.bell-labs.com/cm/cs/tpop nhưng cả hai đều hiện tại (2015-08-10) bị hỏng.


Mã trong GitHub

Nếu bạn tò mò, bạn có thể nhìn vào mã này trong GitHub trong tôi SOQ (Câu hỏi Stack Overflow) kho dưới dạng file debug.c, debug.hmddebug.ctrong src / libsoq thư mục con.


1
Tôi nghĩ rằng cách tiếp cận GCC ## - từ gcc.gnu.org/onlinesocs/cpp/Variadic-Macros.html sẽ đáng để đề cập dưới tiêu đề "Biến thể C99 đối số đơn".
Hugo Ideler

2
Nhiều năm sau, và câu trả lời này vẫn hữu ích nhất trong số tất cả các internets, về cách bí danh printk! vfprintf không hoạt động trong không gian kernel vì stdio không khả dụng. Cảm ơn bạn! #define debug(...) \ do { if (DEBUG) \ printk("DRIVER_NAME:"); \ printk(__VA_ARGS__); \ printk("\n"); \ } while (0)
kevinf

6
Trong ví dụ của bạn với các từ khóa __FILE__, __LINE__, __func__, __VA_ARGS__, nó sẽ không biên dịch nếu bạn không có tham số printf, tức là nếu bạn chỉ gọi debug_print("Some msg\n"); Bạn có thể sửa lỗi này bằng cách sử dụng fprintf(stderr, "%s:%d:%s(): " fmt, __FILE__, __LINE__, __func__, ##__VA_ARGS__); ## __ VA_ARGS__ cho phép không truyền tham số nào cho hàm.
mc_electron

1
@LogicTom: sự khác biệt là giữa #define debug_print(fmt, ...)#define debug_print(...). Đầu tiên trong số này yêu cầu ít nhất một đối số, chuỗi định dạng ( fmt) và không hoặc nhiều đối số khác; thứ hai yêu cầu không có hoặc nhiều đối số trong tổng số. Nếu bạn sử dụng debug_print()với cái đầu tiên, bạn sẽ gặp lỗi từ bộ tiền xử lý về việc lạm dụng macro, trong khi cái thứ hai thì không. Tuy nhiên, bạn vẫn gặp phải lỗi biên dịch vì văn bản thay thế không hợp lệ C. Vì vậy, nó thực sự không có nhiều khác biệt - do đó sử dụng thuật ngữ 'kiểm tra giới hạn'.
Jonathan Leffler

1
Biến thể hiển thị abover, @ St.Antario, sử dụng một mức gỡ lỗi hoạt động duy nhất trên toàn bộ ứng dụng và tôi thường sử dụng các tùy chọn dòng lệnh để cho phép mức gỡ lỗi được đặt khi chương trình được chạy. Tôi cũng có một biến thể nhận ra nhiều hệ thống con khác nhau, mỗi hệ thống được đặt tên và mức độ gỡ lỗi riêng, để tôi có thể sử dụng -D input=4,macros=9,rules=2để đặt mức gỡ lỗi của hệ thống đầu vào thành 4, hệ thống macro thành 9 (trải qua quá trình kiểm tra nghiêm ngặt ) và hệ thống quy tắc thành 2. Có các biến thể vô tận về chủ đề; sử dụng bất cứ điều gì phù hợp với bạn.
Jonathan Leffler

28

Tôi sử dụng một cái gì đó như thế này:

#ifdef DEBUG
 #define D if(1) 
#else
 #define D if(0) 
#endif

Hơn tôi chỉ sử dụng D làm tiền tố:

D printf("x=%0.3f\n",x);

Trình biên dịch thấy mã gỡ lỗi, không có vấn đề dấu phẩy và nó hoạt động ở mọi nơi. Ngoài ra, nó hoạt động khi printfkhông đủ, giả sử khi bạn phải kết xuất một mảng hoặc tính toán một số giá trị chẩn đoán là dư thừa cho chính chương trình.

EDIT: Ok, nó có thể tạo ra một vấn đề khi có elsemột nơi nào đó gần đó có thể bị chặn bởi việc tiêm này if. Đây là một phiên bản đi qua nó:

#ifdef DEBUG
 #define D 
#else
 #define D for(;0;)
#endif

3
Đối với for(;0;), nó có thể tạo ra một vấn đề khi bạn viết một cái gì đó như D continue;hoặc D break;.
ACcreator

1
Có tôi; Dường như rất khó xảy ra tai nạn.
mbq

11

Để triển khai di động (ISO C90), bạn có thể sử dụng dấu ngoặc đơn, như thế này;

#include <stdio.h>
#include <stdarg.h>

#ifndef NDEBUG
#  define debug_print(msg) stderr_printf msg
#else
#  define debug_print(msg) (void)0
#endif

void
stderr_printf(const char *fmt, ...)
{
  va_list ap;
  va_start(ap, fmt);
  vfprintf(stderr, fmt, ap);
  va_end(ap);
}

int
main(int argc, char *argv[])
{
  debug_print(("argv[0] is %s, argc is %d\n", argv[0], argc));
  return 0;
}

hoặc (hackish, sẽ không đề nghị nó)

#include <stdio.h>

#define _ ,
#ifndef NDEBUG
#  define debug_print(msg) fprintf(stderr, msg)
#else
#  define debug_print(msg) (void)0
#endif

int
main(int argc, char *argv[])
{
  debug_print("argv[0] is %s, argc is %d"_ argv[0] _ argc);
  return 0;
}

3
@LB: để làm cho bộ tiền xử lý 'nghĩ' chỉ có một đối số, trong khi để _ được mở rộng ở giai đoạn sau.
Marcin Koziuk

10

Đây là phiên bản tôi sử dụng:

#ifdef NDEBUG
#define Dprintf(FORMAT, ...) ((void)0)
#define Dputs(MSG) ((void)0)
#else
#define Dprintf(FORMAT, ...) \
    fprintf(stderr, "%s() in %s, line %i: " FORMAT "\n", \
        __func__, __FILE__, __LINE__, __VA_ARGS__)
#define Dputs(MSG) Dprintf("%s", MSG)
#endif

9

Tôi sẽ làm một cái gì đó như

#ifdef DEBUG
#define debug_print(fmt, ...) fprintf(stderr, fmt, __VA_ARGS__)
#else
#define debug_print(fmt, ...) do {} while (0)
#endif

Tôi nghĩ rằng điều này là sạch hơn.


Tôi thực sự không thích ý tưởng sử dụng macro trong bài kiểm tra làm cờ. Bạn có thể giải thích tại sao việc in gỡ lỗi phải luôn được kiểm tra?
LB40

1
@Jonathan: Nếu mã chỉ được thực thi trong chế độ gỡ lỗi, tại sao bạn nên quan tâm nếu nó biên dịch ở chế độ không gỡ lỗi? assert()từ stdlib hoạt động theo cùng một cách và tôi thường chỉ sử dụng lại NDEBUGmacro cho mã gỡ lỗi của riêng mình ...
Christoph

sử dụng DEBUG trong thử nghiệm, nếu ai đó thực hiện DEBUG không được kiểm soát, mã của bạn sẽ không còn được biên dịch nữa. đúng ?
LB40

4
Thật khó chịu khi bật gỡ lỗi và sau đó phải gỡ lỗi mã gỡ lỗi vì nó đề cập đến các biến đã được đổi tên hoặc gõ lại, v.v. Nếu trình biên dịch (bộ xử lý trước bài) luôn thấy câu lệnh in, nó đảm bảo rằng mọi thay đổi xung quanh đều có không làm mất hiệu lực chẩn đoán. Nếu trình biên dịch không nhìn thấy câu lệnh in, nó không thể bảo vệ bạn trước sự bất cẩn của chính bạn (hoặc sự bất cẩn của đồng nghiệp hoặc cộng tác viên của bạn). Xem 'Thực hành lập trình' của Kernighan và Pike - plan9.bell-labs.com/cm/cs/tpop .
Jonathan Leffler

1
@Christoph: tốt, loại ... Tôi chỉ sử dụng NDEBUG để chỉ kiểm soát các xác nhận và một macro riêng (thường là DEBUG) để kiểm soát theo dõi gỡ lỗi. Tôi thường không muốn đầu ra gỡ lỗi xuất hiện vô điều kiện, vì vậy tôi có cơ chế kiểm soát xem đầu ra có xuất hiện không (mức gỡ lỗi và thay vì gọi trực tiếp fprintf (), tôi gọi hàm in gỡ lỗi chỉ in một cách có điều kiện mã có thể in hoặc không in dựa trên các tùy chọn chương trình). Tôi ủng hộ rằng đối với tất cả các bản dựng, trình biên dịch sẽ thấy các báo cáo chẩn đoán; tuy nhiên, nó sẽ không tạo mã trừ khi bật gỡ lỗi.
Jonathan Leffler

8

Theo http://gcc.gnu.org/onlinesocs/cpp/Variadic-Macros.html , cần phải có ##trước __VA_ARGS__.

Nếu không, một macro #define dbg_print(format, ...) printf(format, __VA_ARGS__)sẽ không biên dịch ví dụ sau : dbg_print("hello world");.


1
Chào mừng bạn đến với Stack Overflow. Bạn đúng là GCC có phần mở rộng không chuẩn mà bạn tham chiếu. Câu trả lời hiện được chấp nhận thực tế đề cập đến điều này, bao gồm chính xác URL tham chiếu bạn đưa ra.
Jonathan Leffler

7
#define debug_print(FMT, ARGS...) do { \
    if (DEBUG) \
        fprintf(stderr, "%s:%d " FMT "\n", __FUNCTION__, __LINE__, ## ARGS); \
    } while (0)

Phiên bản nào của C hỗ trợ ký hiệu đó? Và, nếu nó hoạt động, mã thông báo dán tất cả các đối số như thế có nghĩa là bạn chỉ có một bộ tùy chọn rất hạn chế cho chuỗi định dạng, phải không?
Jonathan Leffler

@Jonathan: gcc (Debian 4.3.3-13) 4.3.3
nhãn cầu

1
OK - đã đồng ý: nó được ghi lại dưới dạng phần mở rộng GNU cũ (phần 5.17 của hướng dẫn sử dụng GCC 4.4.1). Nhưng có lẽ bạn nên ghi lại rằng nó sẽ chỉ hoạt động với GCC - hoặc có thể chúng tôi đã làm điều đó giữa chúng tôi trong các nhận xét này.
Jonathan Leffler

1
Ý định của tôi là thể hiện một phong cách sử dụng args khác và chủ yếu là thể hiện việc sử dụng FUNCTIONLINE
eyealm

2

Đây là những gì tôi sử dụng:

#if DBG
#include <stdio.h>
#define DBGPRINT printf
#else
#define DBGPRINT(...) /**/  
#endif

Nó có lợi ích tốt để xử lý printf đúng cách, thậm chí không có đối số bổ sung. Trong trường hợp DBG == 0, ngay cả trình biên dịch ngu ngốc nhất cũng không có gì để nhai, do đó không có mã nào được tạo.


Tốt hơn là để trình biên dịch luôn kiểm tra mã gỡ lỗi.
Jonathan Leffler

1

Yêu thích của tôi dưới đây là var_dump, mà khi được gọi là:

var_dump("%d", count);

tạo ra sản lượng như:

patch.c:150:main(): count = 0

Tín dụng cho @ "Jonathan Leffler". Tất cả đều là C89-happy:

#define DEBUG 1
#include <stdarg.h>
#include <stdio.h>
void debug_vprintf(const char *fmt, ...)
{
    va_list args;
    va_start(args, fmt);
    vfprintf(stderr, fmt, args);
    va_end(args);
}

/* Call as: (DOUBLE PARENTHESES ARE MANDATORY) */
/* var_debug(("outfd = %d, somefailed = %d\n", outfd, somefailed)); */
#define var_debug(x) do { if (DEBUG) { debug_vprintf ("%s:%d:%s(): ", \
    __FILE__,  __LINE__, __func__); debug_vprintf x; }} while (0)

/* var_dump("%s" variable_name); */
#define var_dump(fmt, var) do { if (DEBUG) { debug_vprintf ("%s:%d:%s(): ", \
    __FILE__,  __LINE__, __func__); debug_vprintf ("%s = " fmt, #var, var); }} while (0)

#define DEBUG_HERE do { if (DEBUG) { debug_vprintf ("%s:%d:%s(): HERE\n", \
    __FILE__,  __LINE__, __func__); }} while (0)

1

Vì vậy, khi sử dụng gcc, tôi thích:

#define DBGI(expr) ({int g2rE3=expr; fprintf(stderr, "%s:%d:%s(): ""%s->%i\n", __FILE__,  __LINE__, __func__, #expr, g2rE3); g2rE3;})

Bởi vì nó có thể được chèn vào mã.

Giả sử bạn đang cố gắng gỡ lỗi

printf("%i\n", (1*2*3*4*5*6));

720

Sau đó, bạn có thể thay đổi nó thành:

printf("%i\n", DBGI(1*2*3*4*5*6));

hello.c:86:main(): 1*2*3*4*5*6->720
720

Và bạn có thể có được một phân tích về biểu thức được đánh giá là gì.

Nó được bảo vệ chống lại vấn đề đánh giá kép, nhưng sự vắng mặt của các máy phát điện sẽ khiến nó bị bỏ ngỏ cho các vụ va chạm tên.

Tuy nhiên, nó làm tổ:

DBGI(printf("%i\n", DBGI(1*2*3*4*5*6)));

hello.c:86:main(): 1*2*3*4*5*6->720
720
hello.c:86:main(): printf("%i\n", DBGI(1*2*3*4*5*6))->4

Vì vậy, tôi nghĩ rằng miễn là bạn tránh sử dụng g2rE3 làm tên biến, bạn sẽ ổn thôi.

Chắc chắn tôi đã tìm thấy nó (và các phiên bản đồng minh cho chuỗi và phiên bản cho các mức gỡ lỗi, v.v.) là vô giá.


1

Tôi đã cố gắng làm thế nào để làm điều này trong nhiều năm, và cuối cùng đưa ra một giải pháp. Tuy nhiên, tôi không biết rằng đã có giải pháp khác ở đây. Đầu tiên, khác với câu trả lời của Leffler , tôi không thấy lập luận của anh ấy rằng các bản in gỡ lỗi phải luôn được biên dịch. Tôi thà không có hàng tấn mã không cần thiết thực thi trong dự án của mình, khi không cần thiết, trong trường hợp tôi cần kiểm tra và chúng có thể không được tối ưu hóa.

Không biên dịch mỗi lần nghe có vẻ tệ hơn so với thực tế. Bạn kết thúc với các bản in gỡ lỗi đôi khi không biên dịch, nhưng không quá khó để biên dịch và kiểm tra chúng trước khi hoàn thành một dự án. Với hệ thống này, nếu bạn đang sử dụng ba cấp độ gỡ lỗi, chỉ cần đặt nó ở cấp độ thông báo gỡ lỗi cấp ba, sửa lỗi biên dịch của bạn và kiểm tra bất kỳ mức nào khác trước khi bạn hoàn tất mã yer. (Vì tất nhiên, việc biên dịch các câu lệnh gỡ lỗi không đảm bảo rằng chúng vẫn hoạt động như dự định.)

Giải pháp của tôi cung cấp cho các mức độ chi tiết gỡ lỗi cũng; và nếu bạn đặt nó ở mức cao nhất, tất cả chúng sẽ biên dịch. Nếu bạn đã sử dụng mức độ chi tiết gỡ lỗi cao gần đây, tất cả chúng đều có thể biên dịch vào thời điểm đó. Cập nhật cuối cùng nên khá dễ dàng. Tôi chưa bao giờ cần nhiều hơn ba cấp độ, nhưng Jonathan nói rằng anh ta đã sử dụng chín cấp độ. Phương pháp này (như của Leffler) có thể được mở rộng đến bất kỳ số cấp nào. Việc sử dụng phương pháp của tôi có thể đơn giản hơn; chỉ cần hai câu lệnh khi được sử dụng trong mã của bạn. Tuy nhiên, tôi cũng đang mã hóa macro ĐÓNG - mặc dù nó không làm gì cả. Nó có thể nếu tôi đang gửi đến một tập tin.

Chống lại chi phí thêm bước kiểm tra họ để thấy rằng họ sẽ biên dịch trước khi giao hàng, đó là

  1. Bạn phải tin tưởng họ để được tối ưu hóa, điều này thừa nhận NÊN xảy ra nếu bạn có đủ mức tối ưu hóa.
  2. Hơn nữa, họ có thể sẽ không làm được nếu bạn thực hiện biên dịch phát hành với tối ưu hóa bị tắt cho mục đích thử nghiệm (điều này được thừa nhận là hiếm); và họ gần như chắc chắn sẽ không bao giờ trong quá trình gỡ lỗi - do đó thực hiện hàng chục hoặc hàng trăm câu lệnh "if (DEBUG)" trong thời gian chạy; do đó làm chậm việc thực thi (đó là sự phản đối nguyên tắc của tôi) và ít quan trọng hơn, làm tăng kích thước thực thi hoặc dll của bạn; và do đó thực hiện và biên dịch thời gian. Jonathan, tuy nhiên, thông báo cho tôi phương pháp của anh ta có thể được thực hiện để cũng không biên dịch các báo cáo.

Các chi nhánh thực sự tương đối tốn kém trong các bộ xử lý tìm nạp hiện đại. Có thể không phải là vấn đề lớn nếu ứng dụng của bạn không phải là vấn đề quan trọng về thời gian; nhưng nếu hiệu suất là một vấn đề, thì, vâng, một thỏa thuận đủ lớn mà tôi muốn chọn cho mã gỡ lỗi có phần thực thi nhanh hơn (và có thể phát hành nhanh hơn, trong trường hợp hiếm, như đã lưu ý).

Vì vậy, những gì tôi muốn là một macro in gỡ lỗi không biên dịch nếu nó không được in, nhưng thực hiện nếu nó là. Tôi cũng muốn các mức độ gỡ lỗi, vì vậy, ví dụ như nếu tôi muốn các phần quan trọng của mã không được in vào một số thời điểm, nhưng để in ở các mức khác, tôi có thể đặt mức gỡ lỗi và có thêm các bản in gỡ lỗi. đã tìm ra cách để thực hiện các mức gỡ lỗi xác định xem bản in có được biên dịch hay không. Tôi đã đạt được nó theo cách này:

Gỡ lỗi.h:

// FILE: DebugLog.h
// REMARKS: This is a generic pair of files useful for debugging.  It provides three levels of 
// debug logging, currently; in addition to disabling it.  Level 3 is the most information.
// Levels 2 and 1 have progressively more.  Thus, you can write: 
//     DEBUGLOG_LOG(1, "a number=%d", 7);
// and it will be seen if DEBUG is anything other than undefined or zero.  If you write
//     DEBUGLOG_LOG(3, "another number=%d", 15);
// it will only be seen if DEBUG is 3.  When not being displayed, these routines compile
// to NOTHING.  I reject the argument that debug code needs to always be compiled so as to 
// keep it current.  I would rather have a leaner and faster app, and just not be lazy, and 
// maintain debugs as needed.  I don't know if this works with the C preprocessor or not, 
// but the rest of the code is fully C compliant also if it is.

#define DEBUG 1

#ifdef DEBUG
#define DEBUGLOG_INIT(filename) debuglog_init(filename)
#else
#define debuglog_init(...)
#endif

#ifdef DEBUG
#define DEBUGLOG_CLOSE debuglog_close
#else
#define debuglog_close(...)
#endif

#define DEBUGLOG_LOG(level, fmt, ...) DEBUGLOG_LOG ## level (fmt, ##__VA_ARGS__)

#if DEBUG == 0
#define DEBUGLOG_LOG0(...)
#endif

#if DEBUG >= 1
#define DEBUGLOG_LOG1(fmt, ...) debuglog_log (fmt, ##__VA_ARGS__)
#else
#define DEBUGLOG_LOG1(...)
#endif

#if DEBUG >= 2
#define DEBUGLOG_LOG2(fmt, ...) debuglog_log (fmt, ##__VA_ARGS__)
#else
#define DEBUGLOG_LOG2(...)
#endif

#if DEBUG == 3
#define DEBUGLOG_LOG3(fmt, ...) debuglog_log (fmt, ##__VA_ARGS__)
#else
#define DEBUGLOG_LOG3(...)
#endif

void debuglog_init(char *filename);
void debuglog_close(void);
void debuglog_log(char* format, ...);

Gỡ lỗi.cpp:

// FILE: DebugLog.h
// REMARKS: This is a generic pair of files useful for debugging.  It provides three levels of 
// debug logging, currently; in addition to disabling it.  See DebugLog.h's remarks for more 
// info.

#include <stdio.h>
#include <stdarg.h>

#include "DebugLog.h"

FILE *hndl;
char *savedFilename;

void debuglog_init(char *filename)
{
    savedFilename = filename;
    hndl = fopen(savedFilename, "wt");
    fclose(hndl);
}

void debuglog_close(void)
{
    //fclose(hndl);
}

void debuglog_log(char* format, ...)
{
    hndl = fopen(savedFilename,"at");
    va_list argptr;
    va_start(argptr, format);
    vfprintf(hndl, format, argptr);
    va_end(argptr);
    fputc('\n',hndl);
    fclose(hndl);
}

Sử dụng macro

Để sử dụng nó, chỉ cần làm:

DEBUGLOG_INIT("afile.log");

Để ghi vào tệp nhật ký, chỉ cần làm:

DEBUGLOG_LOG(1, "the value is: %d", anint);

Để đóng nó, bạn làm:

DEBUGLOG_CLOSE();

mặc dù hiện tại điều này thậm chí không cần thiết, về mặt kỹ thuật, vì nó không làm gì cả. Tôi vẫn đang sử dụng ĐÓNG ngay bây giờ, tuy nhiên, trong trường hợp tôi thay đổi suy nghĩ về cách thức hoạt động của nó và muốn để tệp mở giữa các báo cáo ghi nhật ký.

Sau đó, khi bạn muốn bật in gỡ lỗi, chỉ cần chỉnh sửa #define đầu tiên trong tệp tiêu đề để nói, ví dụ:

#define DEBUG 1

Để báo cáo ghi nhật ký được biên dịch thành không có gì, hãy làm

#define DEBUG 0

Nếu bạn cần thông tin từ một đoạn mã được thực thi thường xuyên (nghĩa là mức độ chi tiết cao), bạn có thể muốn viết:

 DEBUGLOG_LOG(3, "the value is: %d", anint);

Nếu bạn xác định DEBUG là 3, ghi nhật ký cấp 1, 2 & 3. Nếu bạn đặt nó thành 2, bạn sẽ nhận được mức ghi nhật ký 1 & 2. Nếu bạn đặt nó thành 1, bạn chỉ nhận được các báo cáo cấp 1 ghi nhật ký.

Đối với vòng lặp do-while, vì điều này đánh giá một hàm duy nhất hoặc không có gì, thay vì một câu lệnh if, vòng lặp là không cần thiết. OK, điều khiển tôi sử dụng C thay vì C ++ IO (và QString :: arg () của Qt là một cách định dạng các biến định dạng an toàn hơn khi ở Qt, cũng khá lắt léo, nhưng mất nhiều mã hơn và tài liệu định dạng không được tổ chức như có thể - nhưng tôi vẫn tìm thấy các trường hợp thích hợp hơn), nhưng bạn có thể đặt bất kỳ mã nào vào tệp .cpp bạn muốn. Nó cũng có thể là một lớp, nhưng sau đó bạn sẽ cần phải khởi tạo nó và theo kịp nó, hoặc làm một () mới và lưu trữ nó. Bằng cách này, bạn chỉ cần thả các câu lệnh #incoide, init và đóng tùy ý vào nguồn của mình và bạn đã sẵn sàng để bắt đầu sử dụng nó. Nó sẽ làm cho một lớp tốt, tuy nhiên, nếu bạn rất nghiêng.

Trước đây tôi đã thấy rất nhiều giải pháp, nhưng không có giải pháp nào phù hợp với tiêu chí của tôi cũng như giải pháp này.

  1. Nó có thể được mở rộng để làm nhiều cấp độ như bạn muốn.
  2. Nó biên dịch thành không có gì nếu không in.
  3. Nó tập trung IO ở một nơi dễ chỉnh sửa.
  4. Nó linh hoạt, sử dụng định dạng printf.
  5. Một lần nữa, nó không làm chậm quá trình gỡ lỗi, trong khi các bản in gỡ lỗi luôn được biên dịch luôn được thực thi trong chế độ gỡ lỗi. Nếu bạn đang làm khoa học máy tính và không dễ dàng hơn để viết xử lý thông tin, bạn có thể thấy mình đang chạy một trình giả lập tiêu thụ CPU, để xem ví dụ, nơi trình gỡ lỗi dừng nó với một chỉ mục nằm ngoài phạm vi cho một vectơ. Chúng chạy cực chậm trong chế độ gỡ lỗi. Việc thực thi bắt buộc của hàng trăm bản in gỡ lỗi sẽ nhất thiết làm chậm các lần chạy như vậy xuống hơn nữa. Đối với tôi, những lần chạy như vậy không phải là hiếm.

Không có ý nghĩa khủng khiếp, nhưng ngoài ra:

  1. Nó không yêu cầu hack để in mà không có đối số (ví dụ DEBUGLOG_LOG(3, "got here!");); do đó cho phép bạn sử dụng, ví dụ định dạng .arg () an toàn hơn của Qt. Nó hoạt động trên MSVC, và do đó, có thể là gcc. Nó sử dụng ##trong #defines, không chuẩn, như Leffler chỉ ra, nhưng được hỗ trợ rộng rãi. (Bạn có thể mã hóa lại nó không sử dụng ##nếu cần thiết, nhưng bạn sẽ phải sử dụng một bản hack như anh ta cung cấp.)

Cảnh báo: Nếu bạn quên cung cấp đối số mức ghi nhật ký, MSVC sẽ vô tình tuyên bố định danh không được xác định.

Bạn có thể muốn sử dụng tên biểu tượng tiền xử lý khác với DEBUG, vì một số nguồn cũng định nghĩa biểu tượng đó (ví dụ: pross sử dụng ./configurecác lệnh để chuẩn bị xây dựng). Nó dường như tự nhiên với tôi khi tôi phát triển nó. Tôi đã phát triển nó trong một ứng dụng mà DLL đang được sử dụng bởi một thứ khác và nó được quy ước nhiều hơn để gửi bản in nhật ký đến một tệp; nhưng thay đổi nó thành vprintf () cũng sẽ hoạt động tốt.

Tôi hy vọng điều này sẽ cứu nhiều bạn đau buồn về việc tìm ra cách tốt nhất để ghi nhật ký gỡ lỗi; hoặc cho bạn thấy một trong những bạn có thể thích. Tôi đã nửa vời cố gắng tìm ra điều này trong nhiều thập kỷ. Hoạt động trong MSVC 2012 & 2015, và do đó có thể trên gcc; cũng như có thể làm việc trên nhiều người khác, nhưng tôi đã không thử nó trên chúng.

Tôi có nghĩa là để thực hiện một phiên bản phát trực tuyến của một ngày này, quá.

Lưu ý: Cảm ơn Leffler, người đã giúp tôi định dạng tin nhắn của tôi tốt hơn cho StackOverflow.


2
Bạn nói "thực thi hàng chục hoặc hàng trăm if (DEBUG)câu lệnh trong thời gian chạy, không được tối ưu hóa" - vốn đang nghiêng về cối xay gió . Toàn bộ điểm của hệ thống tôi mô tả là mã được kiểm tra bởi trình biên dịch (quan trọng và tự động - không cần xây dựng đặc biệt) nhưng mã gỡ lỗi hoàn toàn không được tạo vì nó được tối ưu hóa (do đó không có tác động thời gian chạy nào kích thước mã hoặc hiệu suất vì mã không có trong thời gian chạy).
Jonathan Leffler

Jonathan Leffler: Thx đã chỉ ra từ ngữ sai lầm của tôi. Tôi để suy nghĩ của mình chạy nhanh hơn ngón tay của mình, rất vui mừng khi hoàn thành nó. Tôi đã sửa đổi các phản đối của mình với "... 1) bạn phải tin tưởng chúng để được tối ưu hóa, điều này phải thừa nhận sẽ xảy ra nếu bạn có mức tối ưu hóa đủ. 2) Hơn nữa, chúng sẽ không được nếu bạn thực hiện biên dịch phát hành với tối ưu hóa đã tắt cho mục đích thử nghiệm và có lẽ chúng sẽ không hoàn toàn trong quá trình gỡ lỗi - do đó thực thi hàng chục hoặc hàng trăm câu lệnh 'if (DEBUG)' trong thời gian chạy - do đó làm tăng kích thước thực thi hoặc kích thước dll của bạn và thời gian thực hiện. "
CodeLurker

Để bạn làm điều quan trọng khác mà tôi đang làm, bạn sẽ phải có các mức gỡ lỗi. Mặc dù tôi thường không cần bật nhiều ứng dụng, nhưng một vài ứng dụng thực sự có lợi từ việc có thể có được mức độ chi tiết tuyệt vời về vòng lặp thời gian quan trọng với "#define DEBUG 3" đơn giản, sau đó quay lại thông tin dài dòng ít hơn nhiều với "#define DEBUG 1". Tôi chưa bao giờ cần nhiều hơn ba cấp độ, và do đó, ít nhất khoảng 1/3 các bản sửa lỗi của tôi đã được biên dịch. Nếu tôi đã sử dụng cấp 3 gần đây, họ có thể TẤT CẢ.
CodeLurker

YMMV. Hệ thống hiện đại mà tôi đã trình bày hỗ trợ cài đặt động (thời gian chạy) động của các mức gỡ lỗi, do đó bạn có thể quyết định theo chương trình một lượng chương trình gỡ lỗi được tạo khi chạy. Tôi thường sử dụng các cấp 1-9, mặc dù không có giới hạn trên (hoặc giới hạn dưới; mức mặc định là 0 thường bị tắt, nhưng có thể được yêu cầu rõ ràng trong quá trình phát triển tích cực nếu phù hợp - không phù hợp với công việc lâu dài). Tôi đã chọn mức mặc định là 3; mọi thứ có thể được điều chỉnh. Điều này cho tôi rất nhiều kiểm soát. Nếu bạn thực sự không muốn kiểm tra mã gỡ lỗi khi không hoạt động, hãy thay đổi cách thay thế thành ((void)0)- thật dễ dàng.
Jonathan Leffler

1
À Nó sẽ có ích để đọc toàn bộ. Đó là một bài viết khá dài. Tôi nghĩ rằng đó là điểm quan trọng cho đến nay. Hóa ra của bạn, như của tôi, có thể được sử dụng để biên dịch hoặc không biên dịch tất cả các bản in gỡ lỗi và có thể hỗ trợ các cấp; mặc dù phải thừa nhận, bạn có thể biên dịch các mức bạn không sử dụng - với chi phí trong quá trình gỡ lỗi.
CodeLurker

0

Tôi tin rằng biến thể của chủ đề này cung cấp các danh mục gỡ lỗi mà không cần phải có tên macro riêng cho mỗi danh mục.

Tôi đã sử dụng biến thể này trong một dự án Arduino trong đó không gian chương trình bị giới hạn ở 32K và bộ nhớ động bị giới hạn ở mức 2K. Việc bổ sung các câu lệnh gỡ lỗi và chuỗi gỡ lỗi theo dõi nhanh chóng sử dụng không gian. Vì vậy, điều cần thiết là có thể giới hạn dấu vết gỡ lỗi được bao gồm tại thời điểm biên dịch đến mức tối thiểu cần thiết mỗi khi mã được xây dựng.

gỡ lỗi.h

#ifndef DEBUG_H
#define DEBUG_H

#define PRINT(DEBUG_CATEGORY, VALUE)  do { if (DEBUG_CATEGORY & DEBUG_MASK) Serial.print(VALUE);} while (0);

#endif

gọi tập tin .cpp

#define DEBUG_MASK 0x06
#include "Debug.h"

...
PRINT(4, "Time out error,\t");
...
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.