奇怪的C++链表错误



我在不同版本的GNU机器上测试了这个代码片段,只有一台Clang 1001.0.46.3的奇怪Mac报告了分段错误。这段代码中有地址问题或指针问题吗?

#include <iostream>
#include <vector>
using namespace std;
class Node
{
public:
Node* next;
int val;
};
class List
{
private:
Node* head;
Node* tail;
public:
List()
{
head = tail = nullptr;
}
bool isEmpty()
{
if(!head && !tail) return true;
return false;
}
void pushBack(int num)
{
Node* newNode = new Node;
newNode->val = num;
if(isEmpty()) head = tail = newNode;
else
{
tail->next = newNode;
tail = tail->next; 
}
}
void pushFront(int num)
{
Node* newNode = new Node;
newNode->val = num;
if(isEmpty()) head = tail = newNode;
else
{
Node* tmp = head;
newNode->next = tmp;
head = newNode;
}
}
void popBack()
{
if(head == tail) {delete head; head = nullptr; return;}
Node* tmp = head;
while(tmp->next != tail) tmp = tmp->next;
tail = tmp;
delete tmp->next;
tmp->next = nullptr;
}
void popFront()
{
if(head == tail) {delete head; head = nullptr; return;}
Node* tmp = head;
tmp = tmp->next;
delete head;
head = nullptr;
head = tmp;
}
int findLen()
{
if(isEmpty()) return 0;
int len = 0 ;   
Node* tmp = head;
while(tmp)
{
len++;
//if(tmp == tail) break;
tmp = tmp->next;
}
return len;
}
void inserter(int position, int num)
{
if(position > findLen() || isEmpty()) return;
int index = 0;
Node* tmp = head;
Node* newNode = new Node;
newNode->val = num;
if(position == 0) {pushFront(num); return;}
else if(position == findLen()) {pushBack(num); return;}
while(tmp->next)
{
index++;
if(index == position)
{
Node* tmp2 = tmp->next;
tmp->next = newNode;
newNode->next = tmp2;
return;
}   
tmp = tmp->next;
}
}
void print()
{
if(isEmpty()) return;
cout << "list = ";
Node* tmp = head;
while(tmp)
{
cout << tmp->val << " ";
if(tmp == tail) break;
tmp = tmp->next;
}
cout << endl;
}
};

int main()
{   
cout << "delete added" << endl;
List testList;
testList.pushBack(5);
testList.pushBack(10);
testList.pushBack(15);
testList.pushBack(20);                          // after this line, we got segmentation fault
cout << "len = " << testList.findLen() << endl;
testList.print();
testList.pushFront(5);
testList.pushFront(10);
testList.pushFront(15);
testList.pushFront(20);
cout << "len = " << testList.findLen() << endl;
testList.print();
testList.inserter(0,8);
cout << "len = " << testList.findLen() << endl;
testList.print();
testList.inserter(9,555);
cout << "len = " << testList.findLen() << endl;
testList.print();
testList.inserter(5,333);
cout << "len = " << testList.findLen() << endl;
testList.print();
cout << "popBack" << endl;
testList.popBack();
cout << "len = " << testList.findLen() << endl;
testList.print();
cout << "popFront" << endl;
testList.popFront();
cout << "len = " << testList.findLen() << endl;
testList.print();
cout << "popBack" << endl;
testList.popBack();
cout << "len = " << testList.findLen() << endl;
testList.print();
cout << "popFront" << endl;
testList.popFront();
cout << "len = " << testList.findLen() << endl;
testList.print();
return 0;
}

后续:嘿,伙计们,我只是自己得到了一些线索。我认为问题应该出在操作系统方面。在检查了相关的程序集代码后,我注意到,即使本地变量的默认初始化值为0,它们也不总是零。我认为问题应该是操作系统的分页方案。我将尽我所能弄清楚MacOS(内核10.15.1(和linux如何选择页面,以及如何为局部变量生成随机值。如果有人知道这个地区,或者有任何线索可以弄清楚,请随时告诉我。干杯

您的问题是,在每个Node中,next成员都没有初始化。

取消注释您的评论行//if(tmp == tail) break;方法CCD_ 4也是一个解决方案。

解决这个问题的正确方法是将Node类重写为

class Node
{
public:
Node* next = nullptr;
int val;
};

不管怎样,我希望这只是家庭作业或练习,否则就去好的旧std::list

我认为next默认情况下会初始化为nullptr。

默认情况下,只有静态存储是零初始化的,即使没有任何显式初始化器,也可以安全读取

动态和自动存储可以而且确实保存随机垃圾(在理论上和实际实现中(。

在实践中,前几个动态分配也可能是零初始化的,因为分配器必须从操作系统获得新页面。但是,在删除一些对象并分配更多对象之后,您就可以从空闲列表中回收内存。(除非CRT启动代码在main顶部之前就已经这样做了,在这种情况下,即使是第一个小的分配也会得到脏内存。(

像valgrind这样的工具可以帮助识别正在读取的未初始化内存。

MSVC的调试模式也有一个有用的功能:它用一个可识别的毒值填充"未初始化"变量,即memset(0xCC(,它不是有效的指针,并且(如果作为x86机器代码执行(是int3指令:调试断点。在其他上下文中(如整数(往往是可识别的。

在检查了相关的程序集代码后,我注意到即使我将局部变量的默认初始化值设置为0,它们也不总是为零。我认为问题应该是操作系统的分页方案。

来自操作系统的匿名内存的新页面始终为零,以避免在用户之间泄露潜在的敏感信息。(例如,先前用于读取/etc/shadow的内容的页面或ssh密钥(。当new必须在后台使用mmapbrk来获得更多内存时,或者函数调用的第一次时间将RSP递减到新页面时,这适用。

通常情况下,你不会得到一个新的页面,而是在重用已经分配的页面(通过空闲列表,或者通过重用上一个函数调用弄脏的内存来为堆栈(。除非您的函数是有史以来调用堆栈最深的函数,否则可能会出现脏内存。

您的实验在未初始化的变量中看到了大部分零,这可能是您的程序所做的唯一一件事,因此也是调用堆栈第一次变得如此深入。

或者对于动态分配,空闲列表中可能没有脏内存,因此new必须从操作系统中获取一个新的虚拟页面。


页面大小仅决定逻辑零堆栈和BSS何时物理归零,以及以何种粒度归零。

根据对另一个答案的评论,我认为你对操作系统用于清零的机制感到困惑。"获取更多页面"作为一个概念是有意义的,对于动态存储来说很有用,但对于堆栈来说会让您感到困惑。

静态存储和堆栈空间具有固定布局。函数调用的堆栈帧位于其父帧的正下方,并在返回时被释放。callstack是一个堆栈数据结构

所以重要的是,这是否是你所经历过的最深的一次;页面边界无关紧要。函数在运行时不会弄脏整个页面的堆栈空间,它只会弄脏实际使用的内存。堆栈通常被限制为8MIB,在CRT启动代码调用main时,只有几个4k页在使用。所以你基本上有一个8MiB的零内存数组的大部分作为堆栈空间。

最新更新