为什么要在C++中使用指针?

5
我是一名从C#开发领域转向游戏开发的学习者,现在正在学习C++,但是我对指针和解除引用的概念/使用感到相当困难。我已经三次阅读了当前课程教材中的两个章节,甚至还在谷歌上搜索了一些相关页面,但似乎并没有很好地理解。
我认为我理解了这部分内容:
#include <iostream>

int main()
{
    int myValue   = 5;
    int* myPointer = nullptr;

    std::cout << "My value: " << myValue << std::endl; // Returns value of 5.
    std::cout << "My Pointer: " << &myValue << std::endl; // Returns some hex address.

    myPointer  = &myValue; // This would set it to the address of memory.
    *myPointer = 10; // Essentially sets myValue to 10.

    std::cout << "My value: " << myValue << std::endl; // Returns value of 10.
    std::cout << "My Pointer: " << &myValue << std::endl; // Returns same hex address.
}

我认为我没有理解的是为什么要这样做?为什么不直接赋值 myValue = 5,然后 myValue = 10 呢?为什么要通过另一个变量或指针添加一个额外层级?如果有任何有帮助的输入、现实生活用例或链接可帮助理解这一点,将非常感激!


3
在你所发布的代码非常狭窄的情况下,它没有任何作用。但是这个例子旨在向你解释它是如何工作的,而不是告诉你如何使用它来完成有趣的事情。尝试添加另一个函数并玩弄代码,以尝试找出你可以做什么。 - vanza
11个回答

15
指针的目的是直到你第一次真正需要它们时才会完全意识到。您提供的示例是不需要指针但可以使用指针的情况。这只是为了展示它们的工作原理。指针是一种记住内存位置而无需复制其指向的所有内容的方法。阅读此教程,因为它可能会给您不同于课本的视角: http://www.cplusplus.com/doc/tutorial/pointers/ 例如:如果您定义了一个游戏实体数组如下:
std::vector<Entity*> entities;

而且您有一个相机类,可以“跟踪”特定实体:

class Camera
{
private:
   Entity *mTarget;  //Entity to track

public:
   void setTarget(Entity *target) { mTarget = target; }
}

在这种情况下,相机引用实体的唯一方式是使用指针。

entities.push_back(new Entity());
Camera camera;
camera.setTarget(entities.front());

现在,无论何时实体在你的游戏世界中改变位置,相机在渲染到屏幕时将自动访问最新的位置。如果你没有使用指向实体的指针并传递了一个副本,那么你将有一个过期的位置来渲染相机。


1
到目前为止,唯一展示引用无法使用的情况的答案是您提供了一个需要重新设置指针的setter(这在引用中是不可能的)。我也喜欢您的例子与游戏开发有关。 :) - leemes
1
虽然最好将包含值的向量与仅在引用您不拥有的对象(即您的相机类)时使用指针的方式分开。 - leemes
1
请记住,在C#和Java等语言中,所有对象变量都是指针,即这些语言使用“指针语义”,但C++具有“值语义”。这意味着在C++中,所有非指针变量本身就是实例,将其分配给其他地方/传递给函数将复制该实例,而不是引用该实例。这就是您应该理解的主要思想。 - leemes
1
在C++中将指针或引用传递给函数的行为类似于在C#中将对象变量传递给函数(即在函数内部进行的更改会反映在您传递的对象上,而不是在副本上)。当通过值传递对象时,在C++中它将被复制,这样的更改在外部是不可见的。 - leemes
2
@DanWatkins:这段代码存在一个微妙的问题,即稳定性。当向vector添加元素时,它可能会将先前保存的所有元素重新定位到另一个内存位置,从而使之前创建的所有引用/迭代器无效。因此,Camera很快就会保留一个悬空指针 - Matthieu M.
显示剩余9条评论

2
如果你按值传递一个int,你将无法更改调用者的值。但是,如果你传递一个指向int的指针,你可以更改它。这就是C如何改变参数的。C ++可以通过引用传递值,因此这种方式不太有用。
f(int i)
{
  i= 10;
  std::cout << "f value: " << i << std::endl;
}
f2(int *pi)
{
    *pi = 10;
    std::cout << "f2 value: " << pi << std::endl;
}

main()
{
    i = 5
    f(i)
    std::cout << "main f value: " << i << std::endl;
    f2(&i)
    std::cout << "main f2 value: " << i << std::endl;
}

在main函数中,第一个打印输出应该仍为5。第二个打印输出应为10。

2
TL;DR: 当多个地方需要访问相同信息时,指针很有用。

在你的例子中,它们并没有做太多的事情,就像你所说的那样,只是展示了它们的使用方式。指针的一种用途是连接树中的节点。如果你有一个如下的节点结构...

struct myNode
{
    myNode *next;
    int someData;
};

你可以创建多个节点,并将每个节点链接到前一个节点的next成员。你可以不使用指针来做到这一点,但指针的好处在于它们都链接在一起,当你传递myNode列表时,你只需要传递第一个(根)节点。
指针的酷炫之处在于,如果两个指针引用同一内存地址,对该内存地址所做的任何更改都会被所有引用该内存地址的内容识别。因此,如果你这样做:
int a = 5; // set a to 5
int *b = &a; // tell b to point to a
int *c = b; // tell c to point to b (which points to a)

*b = 3; // set the value at 'a' to 3
cout << c << endl; // this would print '3' because c points to the same place as b

这有一些实际用途。假设你有一个节点列表链接在一起。每个节点中的数据定义了需要完成的某种任务,该任务将由某个函数处理。随着新任务被添加到列表中,它们被追加到末尾。由于函数具有指向节点列表的指针,因此当任务被添加时,它也会接收到这些任务。另一方面,该函数还可以在完成任务时删除任务,并将这些更改反映回正在查看节点列表的任何其他指针。
指针也用于动态内存。假设您希望用户输入一系列数字,并告诉您他们想要使用多少个数字。您可以定义一个包含100个元素的数组,以允许最多100个数字,或者您可以使用动态内存。
int count = 0;

cout << "How many numbers do you want?\n> ";
cin >> count;

// Create a dynamic array with size 'count'
int *myArray = new int[count];

for(int i = 0; i < count; i++)
{
    // Ask for numbers here
}

// Make sure to delete it afterwars
delete[] myArray;

2

为什么要增加另一个变量或指针的层次?

实际上没有必要。这只是一个刻意构造的例子,展示了机制的工作原理。

在现实中,对象经常存储在代码库的远程部分,或者动态分配,或者以其他方式无法限定范围。在任何这些情况下,您可能需要间接地引用对象,并且可以使用指针和/或引用(取决于您的需求)来实现。


1
例如,有些对象没有名称。它可以是分配的内存或从函数返回的地址,也可以是迭代器。 在您的简单示例中,当然不需要声明指针。然而,在许多情况下,例如当您处理C字符串函数时,您需要使用指针。一个简单的例子。
char s[] = "It is pointer?";

if ( char *p = std::strchr( s, '?' ) ) *p = '!';  

1

以一个指向类的指针为例。

struct A
{
    int thing;
    double other;
    A() {
        thing = 4;
        other = 7.2;
    }
};

假设我们有一个接受参数为'A'的方法:
void otherMethod()
{
    int num = 12;
    A mine;
    doMethod(num, mine);
    std::cout << "doobie " << mine.thing;
}

void doMethod(int num, A foo)
{
    for(int i = 0; i < num; ++i)
        std::cout << "blargh " << foo.other;
    foo.thing--;
}

当调用doMethod时,传递了A对象的值。这意味着会创建一个新的A对象副本。由于它们是两个单独的对象,所以foo.thing--行不会修改mine对象。
您需要做的是传递指向原始对象的指针。当传递指针时,foo.thing--将修改原始对象而不是创建旧对象的副本。

2
你可以将指针或引用传递给函数。 - leemes

1
指针(或引用)在 C++ 中使用动态多态性非常重要。它们是你使用类层次结构的方法。
Shape * myShape = new Circle();
myShape->Draw(); // this draws a circle
// in fact there is likely no implementation for Shape::Draw

试图通过基类的值(而不是指针或引用)使用派生类往往会导致切片并丢失对象的派生数据部分。


1
我们主要在需要动态分配内存时使用指针。例如,实现一些数据结构如链表、树等。

1

从C#的角度来看,指针与C#中的对象引用非常相似——它只是内存中实际数据存储的地址,通过取消引用可以操作这些数据。

首先,像你例子中的int一样的非指针数据分配在堆栈上。这意味着当它超出作用域时,其使用的内存将被释放。另一方面,使用operator new分配的数据将被放置在堆中(就像您在C#中创建任何对象一样),导致这些数据不会被释放,除非您失去了它的指针。因此,在堆内存中使用数据使您需要执行以下操作之一:

  • 使用垃圾收集器稍后删除数据(如在C#中所做)
  • 手动释放内存,然后不再需要它(以C++方式使用operator delete)。

为什么需要呢? 基本上有三种用例:

  1. 堆栈内存虽然速度快,但容量有限,如果需要存储大量数据,则必须使用堆。
  2. 复制大量数据是昂贵的。当您在堆栈上在函数之间传递简单值时,会进行复制。当您传递指针时,唯一复制的是其地址(就像在C#中一样)。
  3. C ++中的某些对象可能是不可复制的,例如线程,因为它们的性质。

1
指针(或引用)也可用于多态性。形状指针列表可以指向许多不同的形状,而形状值列表只能指向基本形状。 - YoungJohn

0

当你将指针传递到函数中时,它会变得更加合理。请看这个例子:

void setNumber(int *number, int value) {
    *number = value;
}

int aNumber = 5;
setNumber(&aNumber, 10);
// aNumber is now 10

我们在这里做的是设置*number的值,如果没有指针的使用,这是不可能实现的。

如果你改为这样定义:

void setNumber(int number, int value) {
    number = value;
}

int aNumber = 5;
setNumber(aNumber, 10);
// aNumber is still 5 since you're only copying its value

它还可以提供更好的性能,当您将对较大对象(如类)的引用传递给函数时,而不是传递整个对象时,您不会浪费太多内存。


1
错误的,你可以使用引用,我个人甚至认为对于这种情况使用引用比使用指针更好。 - leemes
1
它们有点相似,但也非常不同。引用更像是命名别名和更高级别;指针是内存地址和更低级别。引用始终引用现有对象,不能重新设置,不能用于迭代连续的元素数组,不能为null,... - leemes
1
不意味着内存所有权或责任,可以指临时变量,可能存在更多的差异而非相似之处。 - MSalters

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