class Node
{
private:
Node* pNext;
char* name;
public:
Node();
~Node();
Node* getNext();
char* getname();
void setNext(Node* pNode);
void setname(char* pname);
}
void linkedlist:: insert(Node* pNode){
Node *current;
pNode->setNext(NULL);
if(!pHead)
pHead=pNode;
else{
current=pHead;
while(current->getNext()!=NULL)
current=current->getNext();
current->setNext(pNode);
}
}
void main(){
linkedlist list;
Node *temp=NULL;
char i[200]={0,};
temp=new Node();
strcpy(i, "Anna");
temp->setname(i);
list.insert(temp);
temp=new Node();
strcpy(i, "Jane");
temp->setname(i);
list.insert(temp);
temp=new Node();
strcpy(i, "Peter");
temp->setname(i);
list.insert(temp);
temp=new Node();
strcpy(i, "Brooth");
temp->setname(i);
list.insert(temp);
temp=new Node();
strcpy(i, "Tim");
temp->setname(i);
list.insert(temp);
}
为什么结果不是Anna-> Jane-> Peter-> Brooth-> Tim 为什么结果是Tim-> Tim-> Tim-> Tim-> Tim 我该怎么办? 每当char i [200]改变所有节点数据都改变了。 为什么?当我使用int数据时它运行良好。 它只在我使用char数组时才会出现。
答案 0 :(得分:4)
他们都说“蒂姆”的原因是因为您每次拨打{{name
时都在Node
设置char i[200]
指向main
缓冲区1}}。
因此,每次setname
您都要替换所有节点中的字符串。因为“Tim”是你设置的姓氏,所以他们都会指出这一点!
更好,更多C ++的解决方案是将strcpy
存储为name
而不是:
std::string
答案 1 :(得分:1)
我认为问题是成员函数setname你没有显示的实现只是将pname分配给数据成员名称。由于pname总是相同,它等于数组i的第一个元素的地址,因此每个节点的所有数据成员名称包含相同的地址,并且存储在该地址的最后一个值是字符串文字“Tim”。 / p>
答案 2 :(得分:0)
目前,您的所有节点都在访问相同的内存(i)以获取其名称。他们都改变了我,他们都显示我。因此,存储在i中的任何内容都将用于所有节点。要为每个节点创建唯一值,需要为每个节点的name参数分配一组不同的内存。如果使用std :: string,则字符串类将为您处理此内存分配。如果您只是使用自己的char *成员,则由您决定在何处以及如何处理此内存分配,或者通过新增内存(并希望稍后将其删除)动态,或者为每个节点静态分配单独的数组创建。考虑到节点是自己动态分配的,并且您可能希望列表能够在运行时增长,您很可能希望为每个名称动态分配内存。你应该认真考虑让std :: string为你处理,或者如果你必须使用std中的智能指针。
答案 3 :(得分:-1)
当我开始写这个""解决方案已被OP接受。
然而,使用std::string
来减少处理内存管理和通过指针间接的复杂性的解决方案在我看来并不是学习处理内存管理和通过指针间接的最佳方法。换句话说,要了解做 X ,我认为使用现成的抽象来处理 X -doing并不是一个好主意。甚至更短,相关的"困难" std::string
自动处理的内容是OP显然需要了解的内容。
所以
发布的代码是不是真正的代码。这非常糟糕,因为它经常浪费每个人的时间(我们不是心灵感应)。它保证不是真正的代码,因为至少有一个缺少分号,所以它不会用任何编译器编译。
发布的代码不完整。大多数班级linked_list
都缺失了。特别是它的构造函数对于评估逻辑的正确性非常重要。
发布的代码为非标准,在本例中为Microsoft特定的。在标准C和C ++ void main
无效时,它必须是int main
。例如,g ++编译器拒绝编译给定代码中的void main
。
对于将来的问题,请发布完整的实际代码 (但尽可能少),以及标准 C ++(达到了那个实用的程度)。
所有节点都指向同一个名称缓冲区。
要在不使用std::string
的情况下修复,您需要确保每个节点都指向一个唯一的名称缓冲区。
这样做的一种方法是使用new[]
动态分配每个节点的缓冲区。
DRY 原则:不要重复自己。
根据这个原则,直接在每个要添加字符串的地方使用new[]
是个坏主意,而且将字符串复制到新的字符串也是一个坏主意。缓冲每个这样的地方。原因是所有这些代码重复很容易引入小错误,这增加了用于查找和修复错误的时间。更糟糕的是,如果通过方便的复制和修改方法完成代码重复,则错误可以重复,并且由于墨菲定律将会发生 - 通常。
相反,对new[]
的调用和字符串的复制应该是集中,例如在一个名为duplicate
的函数中,因为它(重复的字符串,而不是重复的bug)既是它的作用,也是它产生的内容:
#include <string.h> // strcpy, strlen
#include <stddef.h> // ptrdiff_t
typedef ptrdiff_t Size;
auto duplicate( char const* const s )
-> char*
{
Size const buffer_size = 1 + strlen( s );
return strcpy( new char[buffer_size], s );
}
ptrdiff_t
类型是指针差异表达式的类型。因此,它足以处理任何实际大小,而在32位和64位系统上,它是无符号size_t
的有符号等价物。上面使用它而不是size_t
作为一般的好习惯,以避免数字的无符号整数类型,因为隐式转换为无符号类型会产生非常意外的结果,从而导致错误。
duplicate
函数本质上是旧* nix C strdup
的“自己动手”C ++变体。我包含了一个实现,以便您可以看到它是如何工作的。要成为一个非常完整的DIY实现 - 您可以自己尝试一下 - 同时应该实现strlen
和strcpy
,可能还有更现代的未经训练的名称。
为什么C标准库不包含strdup
?我不知道,这是一个很大的谜。但是对于C ++编程它缺少很好,因为C函数会使用malloc
和free
,而在C ++中则非常强惯例是使用new[]
和delete[]
,以便可能无意中使用错误的释放函数...
对于每个已执行的new[]
,应该只有一个已执行delete[]
。这可能很难实现。如果没有相应的delete[]
程序泄漏内存,并且当同一对象有两个或更多delete[]
个时,程序有未定义的行为
一般的C ++解决方案是使用一个特殊的类,其构造函数接受新的new[]
ed指针,或者执行new[]
本身,并且其析构函数执行delete[]
。
这个对象 - 可以是Node
- 然后被称为拥有动态分配的对象,它的生命周期由它管理。
标准库提供了几个通用且可重用的所有者对象类,称为智能指针,包括std::unique_ptr
和std::shared_ptr
,但这里的重点是自己完成,所以...
例如,每个节点的构造函数只需调用duplicate
,以便为节点提供指定字符串的单独副本。构造函数中的new[]
(通过调用duplicate
执行)然后需要与析构函数中的相应delete[]
匹配,以免泄漏内存。定义Node
类的第一次尝试可能如下所示:
// 1st attempt:
struct Node
{
Node* p_next;
char const* name;
~Node()
{ delete[] name; }
Node( char const s[] )
: p_next( nullptr )
, name( duplicate( s ) )
{}
};
但这有一个潜在的问题,即如果重新分配节点name
会发生什么?
然后,除非执行赋值的代码首先执行delete[]
或复制现有指针,否则指向缓冲区的指针将丢失,并且无法释放该缓冲区(没有关于它的位置的信息)是),记忆将被泄露。
通过使用 getter 函数使name
保持不变(添加const
)或使其成为private
,也可以解决此问题访问者。
但也存在相反的问题,即如果复制节点会发生什么?
然后(使用上面的定义),您将拥有两个或多个具有相同名称缓冲区指针的节点对象,每个对象都负责调用delete[]
,从而导致两个或更多delete[]
个调用对象,结果为未定义的行为。
负责复制的最简单方法是禁止它。通过声明private
复制构造函数和复制赋值运算符,可以禁止复制。或者可以从一个类中继承,冒着某个编译器生成关于它无法为类生成副本赋值运算符的愚蠢警告的风险。另请注意,声明const
成员仅禁止复制分配,而不是复制构造。因此,要负责宣布至少一个复制构造函数,这被称为规则3 :如果您需要析构函数,复制构造函数或赋值运算符,那么(例如,负责复制)你可能需要这三个。
// 2nd. attempt, conforming to the rule of 3:
class Node
{
private:
char const* name_;
Node( Node const& ); // Copy constructor, no such.
Node& operator=( Node const& ); // Copy assignment, no such.
public:
Node* p_next_;
auto name() const
-> char const*
{ return name_; }
~Node() // Destructor.
{ delete[] name_; }
Node( char const s[] )
: name_( duplicate( s ) )
, p_next_( nullptr )
{}
};
现在没有技术问题,但是存在设计级别和使用问题,即一个逻辑属性,名称,通过成员函数访问,而另一个逻辑属性,即下一个指针,可以直接作为数据成员访问。使用没有自动完成功能的编辑器的程序员可能会因忘记为名称访问键入函数调用括号或键入不应该用于下一次指针访问的括号而感到有点恼火。
一种解决方案是使两个成员private
,并为下一个指针提供 setter 函数和getter函数。虽然这提供了统一的符号,但它通常很冗长。保护名称成员,以一些冗长的成本,用于防止错误使用,可能不正确地使用UB,这很重要。对下一个指针的类似保护只是用于统一表示法。而Node
是一个低级别的东西,它确实没有意义。
class String_value
{
private:
char const* chars_;
String_value( String_value const& ); // Copy constr., no such.
String_value& operator=( String_value const& ); // Copy assign, no such.
public:
auto pointer() const
-> char const*
{ return chars_; }
~String_value()
{ delete[] chars_; }
String_value( char const* const s )
: chars_( ::duplicate( s ) )
{}
};
// 3rd attempt: using a dedicated owner class.
struct Node
{
String_value name;
Node* p_next;
Node( char const s[] )
: name_( s )
, p_next_( nullptr )
{}
};
此定义既提供了生命周期管理的一些安全措施,又提供了便利性,例如:只写new Node( "Anna" )
。 : - )
虽然每个节点管理动态分配字符串的生命周期,管理节点生命周期的人员或管理者是什么?
自然所有者是专用列表对象。可以通过直接处理节点轻松地进行链表管理,但是专用列表对象可以通过集中生命周期管理和其他方面来增加一些安全措施。在问题的发布代码中有一个链表类,但是没有显示类定义或它的构造函数,所以它不清楚它的职责是什么。
与节点相比的一个区别是列表对象将拥有(管理整个对象集合的生命周期)。这通常称为集合类。例如,标准库&#34;容器&#34;类是集合类。
有很多设计选择可供选择。特别是列表类是否应该公开节点(适合学习和最少量的脚手架代码),或者它是否应该是一个不透明的值容器,它只是这些值的实现方面 - 这里是字符串 - 存储在列表节点中。标准库的std::list
充当不透明容器,而下面的类无耻地暴露节点:
class Linked_list
{
private:
Node head_;
Node* p_last_;
Linked_list( Linked_list const& ); // Copy constr., no such.
Linked_list& operator=( Linked_list const& ); // Copy assign., no such.
public:
auto first_node() const
-> Node const*
{ return head_.p_next; }
void append( Node* const p_node )
{
p_last_->p_next = p_node;
p_last_ = p_node;
}
~Linked_list()
{
while( Node* const p_doomed = head_.p_next )
{
head_.p_next = p_doomed->p_next;
delete p_doomed;
}
}
Linked_list()
: head_( "" )
, p_last_( &head_ )
{}
};
标题ndoe 直接位于列表对象的一部分。它不是值的逻辑顺序的一部分,而是节点的链接序列的一部分。作为链表的一部分,它简化了插入和删除,例如,在这里,即使值的序列为空,也可以维护指向最后一个节点的指针。
一个微妙的优化观察:标题节点通常不需要有一个值,所以通过安排事情&#34;就这样&#34;可以仅使用节点的链接部分作为标题节点。这或多或少等同于具有指向第一节点的指针而不是具有头节点的方案,并且具有指向最后节点的指针的指针,而不是具有指向最后节点的指针。好吧,眼睛可能会上釉,所以,现在如何使用它。
主程序重新编码以使用上述功能:
#include <iostream>
using namespace std;
int main()
{
Linked_list list;
list.append( new Node( "Anna" ) );
list.append( new Node( "Jane") );
list.append( new Node( "Peter") );
list.append( new Node( "Brooth") );
list.append( new Node( "Tim") );
for( Node const* p = list.first_node(); p != nullptr; p = p->p_next )
{
cout << p->name.pointer() << endl;
}
}