【问题标题】:Why use pointers in C++?为什么在 C++ 中使用指针?
【发布时间】:2014-01-04 07:28:27
【问题描述】:

我从游戏开发的角度学习 C++,我在 C# 中与游戏无关的长期开发,但我很难掌握指针和取消引用的概念/使用。我已经将当前课程教科书中的这两章读了 3 遍,甚至在谷歌上搜索了一些与它们相关的不同页面,但似乎并没有很好地结合在一起。

我想我得到了这个部分:

#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?通过为另一个变量或指针添加的层的目的是什么?任何有用的输入、现实生活中的用途或一些有助于理解这一点的阅读链接将不胜感激!

【问题讨论】:

  • 在您发布的代码的非常狭窄的情况下,它没有任何作用;但是这个例子试图向你解释它是如何工作的,而不是你如何使用它来做有趣的事情。尝试添加另一个函数并使用代码来尝试弄清楚你可以做什么。

标签: c++ pointers dereference


【解决方案1】:

指针的用途是在你第一次真正需要它们之前你不会完全意识到的。您提供的示例是不需要指针但可以使用的情况。这实际上只是为了展示它们是如何工作的。指针是一种记住内存位置的方法,而无需复制它指向的所有内容。阅读本教程,因为它可能会为您提供与课堂书籍不同的观点:

http://www.cplusplus.com/doc/tutorial/pointers/

示例:如果您有一个这样定义的游戏实体数组:

std::vector<Entity*> entities;

你有一个可以“跟踪”特定实体的 Camera 类:

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

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

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

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

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

【讨论】:

  • +1 迄今为止唯一的答案显示了引用不起作用的情况(那是因为您提供了一个需要重新定位指针的设置器,这对于引用是不可能的)。我也喜欢你的例子与游戏开发有关。 :)
  • 虽然最好让向量包含值并且只在引用不属于你的对象的地方使用指针(即你的 Camera 类)
  • @Volearix 请记住,在 C# 和 Java 等语言中,所有对象变量都是指针,即此类语言使用 指针语义,但 C++ 具有 值语义。这意味着 C++ 中的所有非指针变量都是 instances 本身,将其分配给其他地方/将其传递给函数将复制实例,而不是引用实例。这是您应该理解的主要思想。
  • 将 C++ 中的指针或引用传递给函数的行为类似于将对象变量传递给 C# 中的函数(即函数内部所做的更改反映在您传入的对象上,而不是副本上)。在 C++ 中按值传递对象时,它将被复制,并且此类更改在外部不可见。
  • @DanWatkins:这段代码有一个微妙的问题,稳定性。将元素添加到vector 时,它可能会将之前保存的所有元素重新定位到另一个内存位置,从而使之前创建的所有引用/迭代器无效。而Camera 很快就会挂在一个悬空指针上。
【解决方案2】:

在编程中使用指针是一个很好的概念。对于动态分配内存,必须使用指针来存储我们保留的内存的第一个位置的地址,释放内存也是如此,我们需要指针。正如有人在上面的回答中所说的那样,在您需要它之前,您无法理解指针的使用,这是真的。一个例子是你可以使用指针和动态内存分配创建一个可变大小的数组。重要的一件事是,使用指针我们可以更改位置的实际值,因为我们正在间接访问该位置。更重要的是,当我们需要通过引用传递我们的值时,有时引用不起作用,所以我们需要指针。

你写的代码是使用解引用操作符。正如我所说,我们通过使用指针间接访问内存的位置,因此它会像引用对象一样更改位置的实际值,这就是它打印 10 的原因。

【讨论】:

    【解决方案3】:

    指针(或引用)对于在 C++ 中使用动态多态性至关重要。它们是您使用类层次结构的方式。

    Shape * myShape = new Circle();
    myShape->Draw(); // this draws a circle
    // in fact there is likely no implementation for Shape::Draw
    

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

    【讨论】:

      【解决方案4】:

      为另一个变量或指针遍历添加层的目的是什么?

      没有。这是一个故意设计的示例,旨在向您展示该机制的工作原理。

      实际上,对象通常是从代码库的遥远部分存储或访问的,或动态分配的,或者不能受范围限制。在任何这些场景中,您可能会发现自己需要间接引用对象,而这是使用指针和/或引用来实现的(取决于您的需要)。

      【讨论】:

        【解决方案5】:

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

        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-- 将修改原始对象,而不是将旧对象的副本创建为新对象。

        【讨论】:

        • 你可以传递一个指针或者一个对函数的引用。
        【解决方案6】:

        如果您按值传递 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。

        【讨论】:

          【解决方案7】:

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

          在您的示例中,它们并没有做太多事情,就像您说的那样,它只是展示了如何使用它们。使用指针的一件事是像在树中一样连接节点。如果你有这样的节点结构......

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

          您可以创建多个节点并将每个节点链接到前一个 myNode 的 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;
          

          【讨论】:

            【解决方案8】:

            从 C# 的角度来看,指针与 C# 中的对象引用完全相同 - 它只是内存中存储实际数据的地址,通过取消引用,您可以使用这些数据进行操作。

            第一个非指针数据(如示例中的 int)在堆栈上分配。这意味着然后它超出了它使用的内存将被释放的范围。另一方面,使用 operator new 分配的数据将被放置在堆中(就像您在 C# 中创建任何对象一样),导致这些数据不会被释放,因为您失去了它的指针。因此,在堆内存中使用数据会使您执行以下操作之一:

            • 稍后使用垃圾收集器删除数据(如在 C# 中所做的那样)
            • 手动释放内存然后你就不再需要它了(以 C++ 方式 使用运算符删除)。

            为什么需要它? 基本上有三个用例:

            1. 堆栈内存很快但有限,所以如果你需要存储大 您必须使用堆的数据量
            2. 复制大数据是 昂贵的。然后在堆栈上的函数之间传递简单的值 它会复制。然后你传递指针,唯一复制的就是 它是地址(就像在 C# 中一样)。
            3. C++ 中的某些对象可能是 由于其性质,不可复制,例如线程。

            【讨论】:

            • 指针(或引用)也用于多态性。形状指针列表可以指向许多不同的形状,而形状值列表只能指向基本形状。
            【解决方案9】:

            将指针传递给函数时更有意义,请参见以下示例:

            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
            

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

            【讨论】:

            • "如果不使用指针,这将是不可能的" => 错了,你可以使用引用,我个人什至发现它比在这种情况下使用指针更好。
            • 它们有点相似,但也有很大不同。引用更像是命名别名和更高级别;指针是内存地址和更低级别。引用 总是 引用现有对象,不能重新安装,不能用于遍历连续的元素数组,不能为 null,...
            • ...不暗示内存所有权或责任,可以指代临时,...可能差异大于相似。
            【解决方案10】:

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

            【讨论】:

              【解决方案11】:

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

              char s[] = "It is pointer?";
              
              if ( char *p = std::strchr( s, '?' ) ) *p = '!';  
              

              【讨论】:

                猜你喜欢
                • 2015-06-08
                • 1970-01-01
                • 2020-12-10
                • 1970-01-01
                • 1970-01-01
                • 2019-02-16
                • 2021-07-07
                • 1970-01-01
                • 1970-01-01
                相关资源
                最近更新 更多