假设以下代码段:
struct S {
S(int & value): value_(value) {}
int & value_;
};
S function() {
int value = 0;
return S(value); // implicitly returning reference to local value
}
如果编译器不产生警告(-Wall),这种错误可能很难发现。
有哪些工具可以帮助发现这些问题?
假设以下代码段:
struct S {
S(int & value): value_(value) {}
int & value_;
};
S function() {
int value = 0;
return S(value); // implicitly returning reference to local value
}
如果编译器不产生警告(-Wall),这种错误可能很难发现。
有哪些工具可以帮助发现这些问题?
有基于运行时的解决方案可以对代码进行插桩以检查无效指针访问。到目前为止,我只用过mudflap(自GCC 4.0版本以来已集成)。mudflap试图跟踪代码中的每个指针(和引用),并检查每个访问是否指向其基本类型的一个活动对象。以下是一个示例:
#include <stdio.h>
struct S {
S(int & value): value_(value) {}
int & value_;
};
S function() {
int value = 0;
return S(value); // implicitly returning reference to local value
}
int main()
{
S x=function();
printf("%s\n",x.value_); //<-oh noes!
}
启用mudflap编译这个程序:
g++ -fmudflap s.cc -lmudflap
运行后得到:
$ ./a.out
*******
mudflap violation 1 (check/read): time=1279282951.939061 ptr=0x7fff141aeb8c size=4
pc=0x7f53f4047391 location=`s.cc:14:24 (main)'
/opt/gcc-4.5.0/lib64/libmudflap.so.0(__mf_check+0x41) [0x7f53f4047391]
./a.out(main+0x7f) [0x400c06]
/lib64/libc.so.6(__libc_start_main+0xfd) [0x7f53f358aa7d]
Nearby object 1: checked region begins 332B before and ends 329B before
mudflap object 0x703430: name=`argv[]'
bounds=[0x7fff141aecd8,0x7fff141aece7] size=16 area=static check=0r/0w liveness=0
alloc time=1279282951.939012 pc=0x7f53f4046791
Nearby object 2: checked region begins 348B before and ends 345B before
mudflap object 0x708530: name=`environ[]'
bounds=[0x7fff141aece8,0x7fff141af03f] size=856 area=static check=0r/0w liveness=0
alloc time=1279282951.939049 pc=0x7f53f4046791
Nearby object 3: checked region begins 0B into and ends 3B into
mudflap dead object 0x7089e0: name=`s.cc:8:9 (function) int value'
bounds=[0x7fff141aeb8c,0x7fff141aeb8f] size=4 area=stack check=0r/0w liveness=0
alloc time=1279282951.939053 pc=0x7f53f4046791
dealloc time=1279282951.939059 pc=0x7f53f4046346
number of nearby objects: 3
Segmentation fault
需要考虑以下几点:
P.S. 一个需要考虑的事情是,在S()的复制构造函数中添加非可移植的检查,断言value_没有绑定到寿命更短的整数上(例如,如果*this位于比它绑定的整数“旧”的堆栈槽上)。这是高度依赖于机器的,可能很棘手,但只要仅用于调试就应该没问题。
我认为这不可能全部捕获,尽管某些编译器在某些情况下可能会发出警告。
需要记住的是,引用在底层实际上是指针,许多指针自我“射脚”的情况仍然存在。
为了澄清我的意思,以下是两个类。一个使用引用,另一个使用指针。
class Ref
{
int &ref;
public:
Ref(int &r) : ref(r) {};
int get() { return ref; };
};
class Ptr
{
int *ptr;
public:
Ptr(int *p) : ptr(p) {};
int get() { return *ptr; };
};
@@Ref@$bctr$qri proc near // Ref::Ref(int &ref)
push ebp
mov ebp,esp
mov eax,dword ptr [ebp+8]
mov edx,dword ptr [ebp+12]
mov dword ptr [eax],edx
pop ebp
ret
@@Ptr@$bctr$qpi proc near // Ptr::Ptr(int *ptr)
push ebp
mov ebp,esp
mov eax,dword ptr [ebp+8]
mov edx,dword ptr [ebp+12]
mov dword ptr [eax],edx
pop ebp
ret
@@Ref@get$qv proc near // int Ref:get()
push ebp
mov ebp,esp
mov eax,dword ptr [ebp+8]
mov eax,dword ptr [eax]
mov eax,dword ptr [eax]
pop ebp
ret
@@Ptr@get$qv proc near // int Ptr::get()
push ebp
mov ebp,esp
mov eax,dword ptr [ebp+8]
mov eax,dword ptr [eax]
mov eax,dword ptr [eax]
pop ebp
ret
看出区别了吗?其实没有。
我认为没有任何静态工具可以捕捉到这个问题,但是如果你使用Valgrind并结合一些单元测试或者崩溃的代码(段错误),你可以轻松地找到内存被引用的位置以及最初分配内存的位置。
在遭受过这种情况的打击后,我遵循一个准则:
当一个类有引用成员(或指向生命周期不受控制的内容的指针)时,请将对象设置为不可复制。
这样可以降低逃逸带有悬空引用的作用域的风险。
你的代码甚至不应该编译通过。我知道的编译器要么无法编译代码,要么至少会抛出一个警告。
如果你的意思是return S(value)
,那么拜托复制粘贴你在这里发布的代码。
重写并引入拼写错误只会使我们无法真正猜测你正在询问哪些错误,哪些是我们应该忽略的意外。
当你在互联网上的任何地方发布问题时,如果该问题包含代码,请发布完全相同的代码。
现在,假设这实际上是一个拼写错误,那么代码是完全合法的,没有任何工具应该警告你。
只要你不尝试解除悬空引用,代码就是完全安全的。
有些静态分析工具(例如Valgrind或带有/analyze的MSVC)可能会警告您,但似乎没有太大意义,因为您没有做错任何事情。 您正在返回一个包含悬空引用的对象。 您没有直接返回对局部对象的引用(编译器通常会发出警告),而是返回一个具有行为的高级对象,该行为可能使其完全安全使用,即使它包含对已超出范围的局部对象的引用。
这是完全有效的代码。
如果您调用函数并将临时变量绑定到const引用,则作用域会延长。
const S& s1 = function(); // valid
S& s2 = function(); // invalid
这在C++标准中是明确允许的。
参见12.2.4:
有两种情况下,临时对象的销毁时间不同于完整表达式的结束时间。
和12.2.5:
第二种情况是当引用绑定到临时对象时。除非:引用绑定的临时对象或作为引用绑定的子对象的完整对象在引用的生命周期内持久存在[...]。