searchusermenu
  • 发布文章
  • 消息中心
点赞
收藏
评论
分享
原创

C++中的段错误

2024-09-18 09:21:23
0
0

数组越界

例子

C/C++ 中的内置数组没有任何边界检查,这使得访问数组的时候必须十分小心,如果出现越界访问,那么行为将是不可确定的。这里我们展示一个越界访问的例子:

{% highlight cpp linenos %}
#include <iostream>

int main(int argc, char *argv[]) {

    int array[5] = { 0 };

    for(int i = 0; ; ++i) {
        std::cout << i << '\t' << array[i] << std::endl;
    }

    return 0;
}

{%endhighlight%}

解说

代码中声明了一个大小为 5 的数组,为了观察访问越界的行为,我们用一个循环进行越界访问,因为打印出来的结果可能会很长,所以这里只说下结论:

  • 对于前五个元素,打印结果是正常的,正如我们初始化的一样,array[i] 为 0 .
  • 对于第五个之后的元素,一般来说,会打印出一个不确定的整数,直到在打印某个元素的时候,出现了 "segmentation fault".

我们用 gdb 可以看到 segmentation fault 出现的堆栈情况,下面是我一次实际运行的情况:

Program received signal SIGSEGV, Segmentation fault.
0x0000000100001214 in main (argc=1, argv=0x7fff5fbffb70) at eg1.cpp:6
6	        std::cout << i << '\t' << array[i] << std::endl;
(gdb) where
#0  0x0000000100001214 in main (argc=1, argv=0x7fff5fbffb70) at eg1.cpp:6
(gdb) p i
$1 = 140604
(gdb)

可以看到,这里 i 的值已经相当大了,所以这里需要强调的就是,数组越界不是必然导致段错误,有时候只是访问到了一些脏数据,这会导致重现该段错误的重现可能会很困难,配置好环境以便在出现段错误的时候能够保存有检查错误用的 core dump 文件非常重要。

空指针

例子 1

代码

{% highlight cpp linenos %}
int main(int argc, char argv[]) {
int
p = NULL;

std::cout << *p << std::endl;

return 0;

}
{%endhighlight%}

解说

上面是一个最小的空指针异常的例子,当我们尝试对一个空指针解引用时,就会导致段错误。用 gdb 查看崩溃时栈信息如下:

Program received signal SIGSEGV, Segmentation fault.
0x0000000100000d09 in main (argc=1, argv=0x7fff5fbffb70) at eg2.cpp:6
6	    std::cout << *p << std::endl;
(gdb) p p
$1 = (int *) 0x0
(gdb)

例子2

代码

{% highlight cpp linenos %}
#include <iostream>

class Test {
public:
void nonVirtualFunc() {
std::cout << "non virtual function called" << std::endl;
}

void nonVirtualFuncWithStaticMember() {
    std::cout << "non virtual function with static member called, " << static_number << std::endl;
}

void nonVirtualFuncWithMember() {
    std::cout << "non virtual function with member called" << non_static_number << std::endl;
}


virtual void virtualFunc() {
    std::cout << "virtual function called" << std::endl;
}

static void staticFunc() {
    std::cout << "static function called" << std::endl;
}

int non_static_number {0};
static int static_number;

};

int Test::static_number = 0;

int main(int argc, char argv[]) {
Test
p = NULL;
p->staticFunc(); // ok
p->nonVirtualFunc(); // ok
p->nonVirtualFuncWithStaticMember(); // ok
p->nonVirtualFuncWithMember(); // error
p->virtualFunc(); // error
return 0;
}
{%endhighlight%}

解说

这个例子考虑类 Test 的对象空指针在什么情况会导致段错误。这里分为 5 种情况:

  1. 访问类的静态成员函数. 对应函数 staticFunc.
  2. 访问类的成员函数,但是成员函数不访问类的成员. 对应函数 nonVirtualFunc.
  3. 访问类的成员函数,成员函数访问类的静态成员. 对应函数 nonVirtualFuncWithStaticMember.
  4. 访问类的成员函数,成员函数访问类的普通成员. 对应函数 nonVirtualFuncWithMember.
  5. 访问类的虚函数. 对应函数 virtualFunc.

相应的情况标注在代码中,error表示会出现段错误。从原理上说,是否会出现段错误,关键是是否访问了指针所指向的内存。细分来说,有以下几点:

  • 静态成员是独立于每一个类对象存在的,所以只访问静态成员或者调用静态函数不会导致段错误。
  • 非虚成员函数,如果不访问类成员,也不需要访问类对象内存,不会导致段错误。
  • 如果访问了类成员,必然导致段错误。
  • 如果成员函数是虚函数,那么由于需要访问对象的虚函数表,也需要访问类对象内存,所以不管虚函数本身是否访问了类成员,都会导致段错误。

prinf

问题

printf/sprinf 一类与字符串格式化相关的 C 语言风格函数即使在 c++ 中也常有应用. 也有很多日志库也以类似的风格设计,于是就出现一个问题,如果在格式化中,原本应该是一个字符串的地方放置了一个整形,或者是一个整形的位置放置了一个字符串,会出现什么情况。比如以下例子:

{% highlight cpp linenos %}
#include<stdio.h>

int main(int argc, char *argv[]) {
int i; //不赋值,表示 i 可以为任意整形数
printf("%s", i);

return 0;

}
{% endhighlight %}

解说

类似问题中的例子,出现的行为是不确定的,因为 i 会被强制转型为一个 char* 来解释,随着 i 的值的不同,行为会有所不同.

  • 如果 i 所指向地址可以访问,则打印该地址解释为字符串之后的内容。当然,也有可能地址可以访问,但是无法解释为 c 风格的字符串,也可能产生段错误。
  • 如果 i 指向地址不可访问,则产生段错误。

除了以字符串形式去解释整形,printf 还可能出现其他错误:

  1. 以整形解释字符串,应该出现 %s 的地方出现了 %d (或者其他整形的格式符).
  2. 误用了不同长度的整形的描述符,该用 %ld 的地方使用了 %d,或者相反.
  3. 格式符和参数数量不匹配。

对于头两种情况,准确的说法,行为都是不确定的,也可能出现段错误,但是大部分时候出现的是打印错误,因为访问到不可访问的内存的可能性较低. 对最后一种情况,如果是格式符多的话,出现段错误可能性就很大了,反过来倒是没什么问题,但也是一种错误。

总结

不管怎样,printf 一类函数误用格式符都是非常危险的。利用编译器的警告可以检查出这种问题,只要不偷懒,这一类问题还是相当容易避免的。如果是自己封装的类似的函数,gcc也有相应的扩展可以检查到(可以搜索 "attribute format" ).

0条评论
0 / 1000
杨****嘉
4文章数
1粉丝数
杨****嘉
4 文章 | 1 粉丝
杨****嘉
4文章数
1粉丝数
杨****嘉
4 文章 | 1 粉丝
原创

C++中的段错误

2024-09-18 09:21:23
0
0

数组越界

例子

C/C++ 中的内置数组没有任何边界检查,这使得访问数组的时候必须十分小心,如果出现越界访问,那么行为将是不可确定的。这里我们展示一个越界访问的例子:

{% highlight cpp linenos %}
#include <iostream>

int main(int argc, char *argv[]) {

    int array[5] = { 0 };

    for(int i = 0; ; ++i) {
        std::cout << i << '\t' << array[i] << std::endl;
    }

    return 0;
}

{%endhighlight%}

解说

代码中声明了一个大小为 5 的数组,为了观察访问越界的行为,我们用一个循环进行越界访问,因为打印出来的结果可能会很长,所以这里只说下结论:

  • 对于前五个元素,打印结果是正常的,正如我们初始化的一样,array[i] 为 0 .
  • 对于第五个之后的元素,一般来说,会打印出一个不确定的整数,直到在打印某个元素的时候,出现了 "segmentation fault".

我们用 gdb 可以看到 segmentation fault 出现的堆栈情况,下面是我一次实际运行的情况:

Program received signal SIGSEGV, Segmentation fault.
0x0000000100001214 in main (argc=1, argv=0x7fff5fbffb70) at eg1.cpp:6
6	        std::cout << i << '\t' << array[i] << std::endl;
(gdb) where
#0  0x0000000100001214 in main (argc=1, argv=0x7fff5fbffb70) at eg1.cpp:6
(gdb) p i
$1 = 140604
(gdb)

可以看到,这里 i 的值已经相当大了,所以这里需要强调的就是,数组越界不是必然导致段错误,有时候只是访问到了一些脏数据,这会导致重现该段错误的重现可能会很困难,配置好环境以便在出现段错误的时候能够保存有检查错误用的 core dump 文件非常重要。

空指针

例子 1

代码

{% highlight cpp linenos %}
int main(int argc, char argv[]) {
int
p = NULL;

std::cout << *p << std::endl;

return 0;

}
{%endhighlight%}

解说

上面是一个最小的空指针异常的例子,当我们尝试对一个空指针解引用时,就会导致段错误。用 gdb 查看崩溃时栈信息如下:

Program received signal SIGSEGV, Segmentation fault.
0x0000000100000d09 in main (argc=1, argv=0x7fff5fbffb70) at eg2.cpp:6
6	    std::cout << *p << std::endl;
(gdb) p p
$1 = (int *) 0x0
(gdb)

例子2

代码

{% highlight cpp linenos %}
#include <iostream>

class Test {
public:
void nonVirtualFunc() {
std::cout << "non virtual function called" << std::endl;
}

void nonVirtualFuncWithStaticMember() {
    std::cout << "non virtual function with static member called, " << static_number << std::endl;
}

void nonVirtualFuncWithMember() {
    std::cout << "non virtual function with member called" << non_static_number << std::endl;
}


virtual void virtualFunc() {
    std::cout << "virtual function called" << std::endl;
}

static void staticFunc() {
    std::cout << "static function called" << std::endl;
}

int non_static_number {0};
static int static_number;

};

int Test::static_number = 0;

int main(int argc, char argv[]) {
Test
p = NULL;
p->staticFunc(); // ok
p->nonVirtualFunc(); // ok
p->nonVirtualFuncWithStaticMember(); // ok
p->nonVirtualFuncWithMember(); // error
p->virtualFunc(); // error
return 0;
}
{%endhighlight%}

解说

这个例子考虑类 Test 的对象空指针在什么情况会导致段错误。这里分为 5 种情况:

  1. 访问类的静态成员函数. 对应函数 staticFunc.
  2. 访问类的成员函数,但是成员函数不访问类的成员. 对应函数 nonVirtualFunc.
  3. 访问类的成员函数,成员函数访问类的静态成员. 对应函数 nonVirtualFuncWithStaticMember.
  4. 访问类的成员函数,成员函数访问类的普通成员. 对应函数 nonVirtualFuncWithMember.
  5. 访问类的虚函数. 对应函数 virtualFunc.

相应的情况标注在代码中,error表示会出现段错误。从原理上说,是否会出现段错误,关键是是否访问了指针所指向的内存。细分来说,有以下几点:

  • 静态成员是独立于每一个类对象存在的,所以只访问静态成员或者调用静态函数不会导致段错误。
  • 非虚成员函数,如果不访问类成员,也不需要访问类对象内存,不会导致段错误。
  • 如果访问了类成员,必然导致段错误。
  • 如果成员函数是虚函数,那么由于需要访问对象的虚函数表,也需要访问类对象内存,所以不管虚函数本身是否访问了类成员,都会导致段错误。

prinf

问题

printf/sprinf 一类与字符串格式化相关的 C 语言风格函数即使在 c++ 中也常有应用. 也有很多日志库也以类似的风格设计,于是就出现一个问题,如果在格式化中,原本应该是一个字符串的地方放置了一个整形,或者是一个整形的位置放置了一个字符串,会出现什么情况。比如以下例子:

{% highlight cpp linenos %}
#include<stdio.h>

int main(int argc, char *argv[]) {
int i; //不赋值,表示 i 可以为任意整形数
printf("%s", i);

return 0;

}
{% endhighlight %}

解说

类似问题中的例子,出现的行为是不确定的,因为 i 会被强制转型为一个 char* 来解释,随着 i 的值的不同,行为会有所不同.

  • 如果 i 所指向地址可以访问,则打印该地址解释为字符串之后的内容。当然,也有可能地址可以访问,但是无法解释为 c 风格的字符串,也可能产生段错误。
  • 如果 i 指向地址不可访问,则产生段错误。

除了以字符串形式去解释整形,printf 还可能出现其他错误:

  1. 以整形解释字符串,应该出现 %s 的地方出现了 %d (或者其他整形的格式符).
  2. 误用了不同长度的整形的描述符,该用 %ld 的地方使用了 %d,或者相反.
  3. 格式符和参数数量不匹配。

对于头两种情况,准确的说法,行为都是不确定的,也可能出现段错误,但是大部分时候出现的是打印错误,因为访问到不可访问的内存的可能性较低. 对最后一种情况,如果是格式符多的话,出现段错误可能性就很大了,反过来倒是没什么问题,但也是一种错误。

总结

不管怎样,printf 一类函数误用格式符都是非常危险的。利用编译器的警告可以检查出这种问题,只要不偷懒,这一类问题还是相当容易避免的。如果是自己封装的类似的函数,gcc也有相应的扩展可以检查到(可以搜索 "attribute format" ).

文章来自个人专栏
斐波那契之影
4 文章 | 1 订阅
0条评论
0 / 1000
请输入你的评论
0
0