C/C++行号

143

为了调试目的,我能否在 C/C++编译器中获取行号?(标准方法或特定编译器的方法)

例如:

if(!Logical)
    printf("Not logical value at line number %d \n",LineNumber);
    // How to get LineNumber without writing it by my hand?(dynamic compilation)

17
@Lucas: 我们中的一些人更喜欢不使用调试器。这种“穷人风格的断言语句”有时更清晰,因为它是代码的永久部分,并且可以持久记录关于计算状态应该是真实的信息。 - S.Lott
13
调试器对于长时间运行的程序中的间歇性问题或在客户端部署的软件中收集问题信息也不是很有用。在这些情况下,唯一的选择是让程序记录尽可能多关于程序状态的信息以供稍后分析。 - KeithB
1
@Lucas 而且在某些嵌入式系统上,调试器无法很好地工作以获取此信息。 - George Stocker
10个回答

225
你应该使用预处理宏__LINE____FILE__。它们是预定义的宏,是C/C++标准的一部分。在预处理期间,它们分别被替换为一个常量字符串,该字符串包含表示当前行号的整数,并且当前文件名。
其他预处理变量:
  • __func__:函数名(这是C99的一部分,不是所有的C++编译器都支持它)
  • __DATE__:形式为"Mmm dd yyyy"的字符串
  • __TIME__:形式为"hh:mm:ss"的字符串

您的代码将是:

if(!Logical)
  printf("Not logical value at line number %d in file %s\n", __LINE__, __FILE__);

4
C99使用__func__而不是__FUNCTION__,我知道的部分功能已被弃用。 这种区别可能会破坏你的代码,因为__func__不能用于C语言的常量字符串连接。 - Joseph Quinsey
1
来自GCC手册的参考:“__FUNCTION__和__PRETTY_FUNCTION__被视为字符串文字;它们可以用于初始化char数组,并且可以与其他字符串文字连接。 GCC 3.4及更高版本将它们视为变量,就像__func__一样。在C ++中,__FUNCTION__和__PRETTY_FUNCTION__始终是变量。” - Joseph Quinsey
1
关于printf()的第一个参数的文本,根据这个答案中的示例,第二个参数中使用的宏实际上应该是__LINE__-1吗? - Toby
1
@sep332 是的,但是C++是一种奇怪的语言,所以必须通过宏参数进行两步操作。#define S1(N) #N #define S2(N) S1(N) #define LINESTR S2(__LINE__)。请参阅http://c-faq.com/ansi/stringize.html。 - Rasmus Kaj
2
严格来说,__func__不是宏,而是一个隐式声明的变量。 - HolyBlackCat
显示剩余3条评论

73
作为C++标准的一部分,存在一些预定义宏可以使用。C++标准的第16.8节定义了__LINE__宏,其中包括其他内容。

__LINE__: 当前源代码行的行号(一个十进制常量)。
__FILE__: 源文件的假定名称(一个字符串字面值)。
__DATE__: 源文件翻译的日期(一个字符串字面值...)
__TIME__: 源文件翻译的时间(一个字符串字面值...)
__STDC__: 是否预定义了__STDC__
__cplusplus: 编译C++翻译单元时,名称__cplusplus被定义为值199711L。

因此,您的代码将是:
if(!Logical)
  printf("Not logical value at line number %d \n",__LINE__);

20
C++20通过使用std::source_location提供了一种新的实现方式。
__LINE__这样的宏存在一个问题,就是如果你想创建一个输出当前行号和消息的日志函数,你总是需要将__LINE__作为函数参数传递,因为它在调用点被展开。 类似于这样的代码:
void log(const std::string msg) {
    std::cout << __LINE__ << " " << msg << std::endl;
}

将始终输出函数声明的行,而不是实际调用log的行。 另一方面,使用std::source_location,您可以这样写:

#include <source_location>
#include <iostream>

void log(const char* msg, const std::source_location loc = std::source_location::current())
{
    std::cout << loc.line() << " " << msg << std::endl;
}

int main() {
    log ("test"); // Will print "10 test"
}

在这里,loc被初始化为指向调用log的位置所在的行号。 您可以在此处在线尝试。 在较旧的编译器版本中,std::source_location可能只能作为std::experimental::source_location访问。

1
"std::experimental::source_location" 已于2019年7月合并入ISO C++标准主线。您现在可以在C++20中使用 "#include <source_location>" 和 "std::source_location"。 - CreativiTimothy
1
"std::experimental::source_location"已于2019年7月合并入主流的ISO C++标准。现在您可以在C++20中使用"#include <source_location>"和"std::source_location"。 - undefined

19

你可以使用一个宏来实现与 printf() 相同的行为,但它还包括调试信息,例如函数名称、类和行号:

#include <cstdio>  //needed for printf
#define print(a, args...) printf("%s(%s:%d) " a,  __func__,__FILE__, __LINE__, ##args)
#define println(a, args...) print(a "\n", ##args)

这些宏应该与 printf() 表现相同,同时包含类似 Java 堆栈跟踪的信息。以下是一个示例 main 函数:

void exampleMethod() {
    println("printf() syntax: string = %s, int = %d", "foobar", 42);
}

int main(int argc, char** argv) {
    print("Before exampleMethod()...\n");
    exampleMethod();
    println("Success!");
}

导致以下输出:

main(main.cpp:11) 在执行exampleMethod()函数之前...
exampleMethod(main.cpp:7) printf()语法:字符串 = foobar,整数 = 42
main(main.cpp:13) 成功!


对于C开发,您需要将#include更改为<stdio.h> - phyatt
printf 不是 异步信号安全 的。因此,这个宏不能在信号处理程序中使用。 - ceving

11

使用__LINE__(即双下划线 LINE 双下划线), 预处理器会将其替换为所在行的行号。


10

检查__FILE____LINE__


5
尝试使用__FILE____LINE__
你可能也会发现__DATE____TIME__有用。
但是,除非你必须在客户端调试程序并因此需要记录这些信息,否则应该使用普通调试。

为什么我的帖子被投票否决了,为什么mmyers编辑了我的帖子? - Sanctus2099
@Sanctus2099:这个回答被编辑了,因为Markdown将你的双下划线转换成了粗体字显示FILE和LINE(难道你不检查一下你的回答是什么样子吗?)。另外一个可能的原因是(至少在我看来是这样),你在已经有正确答案的情况下延迟了1小时才给出答案,所以没有增加任何价值。 - Felix Kling
双下划线是用于标记语法的 __粗体__。为了正确显示双下划线,您必须转义它们(像这样:\_\_)或使用反引号将它们标记为 原始代码(像这样:`__`)。@mmyers试图帮助,但他只转义了一个下划线,因此您留下了斜体的标记语法。尽管如此,对此进行负投票有点过分,我同意。 - mbauman
好的,我没有意识到双下划线会将文本变成粗体,而且我不得不离开,没有时间查看我的答案是什么样子的。现在我明白了。 即使我的答案晚了一个小时,它仍然是一个好答案。它没有增加任何价值,但也没有错,所以没有理由给它点踩。这就是你为帮助别人所得到的回报... - Sanctus2099
2
@Sanctus2099 有些人很快就会投反对票,这就是为什么确保你的答案正确非常重要。在这种情况下,你发布了一个错误的答案,并让它未经编辑地存在了4个小时。你只能责怪自己。 - user229044
抱歉,预览中看起来很好。我的道歉。 - Michael Myers

5

针对可能需要的人,提供“FILE_LINE”宏以便轻松打印文件和行号:

#define STRINGIZING(x) #x
#define STR(x) STRINGIZING(x)
#define FILE_LINE __FILE__ ":" STR(__LINE__)

1

由于我现在也面临这个问题,而且无法回答一个不同但同样有效的问题这里,我将提供一个解决方案的示例: 使用模板仅获取C ++中调用函数的行号。

背景:在C ++中,可以将非类型整数值用作模板参数。 这与将数据类型用作模板参数的典型用法不同。 因此,想法是使用这些整数值进行函数调用。

#include <iostream>

class Test{
    public:
        template<unsigned int L>
        int test(){
            std::cout << "the function has been called at line number: " << L << std::endl;
            return 0;
        }
        int test(){ return this->test<0>(); }
};

int main(int argc, char **argv){
    Test t;
    t.test();
    t.test<__LINE__>();
    return 0;
}

输出:

该函数已在第0行被调用

该函数已在第16行被调用

这里需要注意的一件事是,在C++11标准中,可以使用模板为函数提供默认模板值。在C++11之前,非类型参数的默认值似乎只适用于类模板参数。因此,在C++11中,就不需要像上面那样有重复的函数定义了。在C++11中,也可以使用const char*模板参数,但无法像这里所述那样与字面量一起使用,例如__FILE____func__

因此,如果您正在使用C++或C++11,则可能会发现这是一个非常有趣的替代方案,而不是使用宏来获取调用行。


1

使用__LINE__,但它的类型是什么?

LINE当前源文件中当前源行的预定行号(整数常量)。

作为一个整数常量,代码通常可以假设该值为__LINE__ <= INT_MAX,因此类型为int

在C语言中,要打印__LINE__,需要使用匹配的格式说明符:"%d"。但在C++中使用cout则不需要考虑这个问题。

严谨的问题:如果行号超过了INT_MAX1(在16位int的情况下可能会出现),希望编译器会产生警告。例如:

format '%d' expects argument of type 'int', but argument 2 has type 'long int' [-Wformat=]

或者,代码可以强制使用更宽的类型以防止出现此类警告。

printf("Not logical value at line number %ld\n", (long) __LINE__);
//or
#include <stdint.h>
printf("Not logical value at line number %jd\n", INTMAX_C(__LINE__));

避免使用printf()

为了避免所有整数限制:使用字符串化。代码可以直接打印而不需要printf()调用:这是在错误处理中避免的好事情2

#define xstr(a) str(a)
#define str(a) #a

fprintf(stderr, "Not logical value at line number %s\n", xstr(__LINE__));
fputs("Not logical value at line number " xstr(__LINE__) "\n", stderr);

1 这样一个如此庞大的文件显然是糟糕的编程实践,但或许机器生成的代码可能会变得更加高效。

2 在调试过程中,有时代码并不像希望的那样工作。调用像*printf()这样的复杂函数本身可能会引起问题,与简单的fputs()相比。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接