【问题标题】:Why use pointers? [closed]为什么要使用指针? [关闭]
【发布时间】:2010-09-14 20:16:47
【问题描述】:

我知道这是一个非常基本的问题,但是在使用高级语言编写了几个项目之后,我才刚刚开始进行一些基本的 C++ 编程。

基本上我有三个问题:

  1. 为什么在普通变量上使用指针?
  2. 我应该在何时何地使用指针?
  3. 如何在数组中使用指针?

【问题讨论】:

  • 之前在this question讨论过希望对您有所帮助!
  • 有关书籍列表,请参阅stackoverflow.com/questions/388242/…。在 Java 之后,我发现 Accelerated C++ 非常有用。
  • 我们使用指针是因为向某人提供您家的地址比向所有人提供您家的副本更容易。
  • @RishiDua 这是我遇到过的对指针的最佳解释。谢谢你,它增加了我的理解:)
  • 当您希望返回多个值/对象时,也可以使用指针。

标签: c++ c pointers


【解决方案1】:
  • 为什么在普通变量上使用指针?

简短的回答是:不要。 ;-) 指针将用于您不能使用其他任何东西的地方。要么是因为缺乏适当的功能,缺少数据类型,要么是为了纯粹的性能。更多下文...

  • 应该在何时何地使用指针?

这里的简短回答是:你不能使用其他任何东西。在 C 中,您不支持诸如字符串之类的复杂数据类型。也无法将变量“通过引用”传递给函数。这就是你必须使用指针的地方。你也可以让它们指向几乎任何东西,链表,结构的成员等等。但我们先不在这里讨论。

  • 如何在数组中使用指针?

不费吹灰之力。 ;-) 如果我们谈论简单的数据类型,例如 int 和 char,那么数组和指针之间几乎没有区别。 这些声明非常相似(但不一样——例如,sizeof 将返回不同的值):

char* a = "Hello";
char a[] = "Hello";

你可以像这样访问数组中的任何元素

printf("Second char is: %c", a[1]);

索引 1,因为数组从元素 0 开始。:-)

或者你也可以这样做

printf("Second char is: %c", *(a+1));

需要指针运算符(*),因为我们告诉 printf 我们要打印一个字符。如果没有 *,将打印内存地址本身的字符表示。现在我们使用角色本身来代替。如果我们使用 %s 而不是 %c,我们会要求 printf 打印由 'a' 加一所指向的内存地址的内容(在上面的这个例子中),我们就不必把 *在前面:

printf("Second char is: %s", (a+1)); /* WRONG */

但这不会只打印第二个字符,而是打印下一个内存地址中的所有字符,直到找到空字符 (\0)。这就是事情开始变得危险的地方。如果您不小心尝试使用 %s 格式化程序打印整数类型的变量而不是 char 指针怎么办?

char* a = "Hello";
int b = 120;
printf("Second char is: %s", b);

这将打印在内存地址 120 上找到的任何内容并继续打印,直到找到一个空字符。执行这个 printf 语句是错误和非法的,但它可能无论如何都可以工作,因为在许多环境中指针实际上是 int 类型的。想象一下,如果您改为使用 sprintf() 并将这种方式太长的“char 数组”分配给另一个变量,那么您可能会导致的问题,而该变量只分配了一定的有限空间。您很可能最终会覆盖内存中的其他内容并导致程序崩溃(如果幸运的话)。

哦,如果您在声明 char 数组/指针时没有为其分配字符串值,则必须在为其分配值之前为其分配足够的内存。使用 malloc、calloc 或类似的。这是因为您只在数组中声明了一个元素/一个要指向的内存地址。所以这里有几个例子:

char* x;
/* Allocate 6 bytes of memory for me and point x to the first of them. */
x = (char*) malloc(6);
x[0] = 'H';
x[1] = 'e';
x[2] = 'l';
x[3] = 'l';
x[4] = 'o';
x[5] = '\0';
printf("String \"%s\" at address: %d\n", x, x);
/* Delete the allocation (reservation) of the memory. */
/* The char pointer x is still pointing to this address in memory though! */
free(x);
/* Same as malloc but here the allocated space is filled with null characters!*/
x = (char *) calloc(6, sizeof(x));
x[0] = 'H';
x[1] = 'e';
x[2] = 'l';
x[3] = 'l';
x[4] = 'o';
x[5] = '\0';
printf("String \"%s\" at address: %d\n", x, x);
/* And delete the allocation again... */
free(x);
/* We can set the size at declaration time as well */
char xx[6];
xx[0] = 'H';
xx[1] = 'e';
xx[2] = 'l';
xx[3] = 'l';
xx[4] = 'o';
xx[5] = '\0';
printf("String \"%s\" at address: %d\n", xx, xx);

请注意,在对分配的内存执行 free() 之后,您仍然可以使用变量 x,但您不知道其中有什么。另请注意,两个 printf() 可能会为您提供不同的地址,因为不能保证第二次内存分配与第一次在同一空间中执行。

【讨论】:

  • 您的示例中包含 120 是错误的。它使用 %c 而不是 %s 所以没有错误;它只打印小写字母 x。此外,您随后声称指针是 int 类型的说法是错误的,并且给没有指针经验的 C 程序员提供了非常糟糕的建议。
  • -1 你开始很好,但第一个例子是错误的。不,不一样。在第一种情况下,a 是一个指针,在第二种情况下,a 是一个数组。我已经提到了吗?这是不一样的!自己检查一下:比较 sizeof(a),尝试为数组分配一个新地址。它不会起作用。
  • “执行此 printf 语句并没有错或非法”。这是完全错误的。该 printf 语句具有未定义的行为。在许多实现中,它会导致访问冲突。指针实际上不是 int 类型,它们实际上是指针(仅此而已)。
  • -1,你在这里弄错了很多事实,我会直截了当地说你不应该得到支持的数量或接受的答案状态。
  • comp.lang.c FAQ 的第 6 节很好地解释了 C 中数组和指针之间的关系。这个答案没有。
【解决方案2】:

使用指针的一个原因是可以在被调用函数中修改变量或对象。

在 C++ 中,使用引用比使用指针更好。尽管引用本质上是指针,但 C++ 在某种程度上隐藏了这一事实,使您看起来好像是按值传递。这样可以轻松更改调用函数接收值的方式,而无需修改传递它的语义。

考虑以下示例:

使用参考:

public void doSomething()
{
    int i = 10;
    doSomethingElse(i);  // passes i by references since doSomethingElse() receives it
                         // by reference, but the syntax makes it appear as if i is passed
                         // by value
}

public void doSomethingElse(int& i)  // receives i as a reference
{
    cout << i << endl;
}

使用指针:

public void doSomething()
{
    int i = 10;
    doSomethingElse(&i);
}

public void doSomethingElse(int* i)
{
    cout << *i << endl;
}

【讨论】:

  • 提及引用更安全可能是个好主意,因为您不能传递空引用。
  • 是的,这可能是使用引用的最大优势。感谢您指出。没有双关语:)
  • 你肯定可以传递空引用。这不像传递空指针那么容易。
  • 同意@n0rd。如果你认为你不能传递一个空引用,那你就错了。传递悬空引用比传递空引用更容易,但最终你可以做任何一个都足够简单。参考文献并不是保护工程师免于射中自己脚的灵丹妙药。 See it live.
  • @n0rd :这样做是明确未定义的行为。
【解决方案3】:
  1. 指针允许您从多个位置引用内存中的同一空间。这意味着您可以在一个位置更新内存,并且可以从程序中的另一个位置看到更改。您还可以通过共享数据结构中的组件来节省空间。
  2. 您应该在需要获取地址并将地址传递到内存中特定位置的任何地方使用指针。您还可以使用指针来导航数组:
  3. 数组是分配有特定类型的连续内存块。数组的名称包含数组起始点的值。当您添加 1 时,您将进入第二个位置。这允许您编写循环来增加一个向下滑动数组的指针,而无需使用显式计数器来访问数组。

这是一个 C 语言示例:

char hello[] = "hello";

char *p = hello;

while (*p)
{
    *p += 1; // increase the character by one

    p += 1; // move to the next spot
}

printf(hello);

打印

ifmmp

因为它获取每个字符的值并将其加一。

【讨论】:

  • because it takes the value for each character and increments it by one。是用 ascii 表示还是怎样?
  • p=0; 会重置指针吗?
【解决方案4】:

指针是间接引用另一个变量的一种方式。它们不是保存变量的,而是告诉你它的地址。这在处理数组时特别有用,因为使用指向数组中第一个元素(其地址)的指针,您可以通过递增指针(指向下一个地址位置)快速找到下一个元素。

我读过的关于指针和指针算法的最佳解释是在 K & R 的The C Programming Language 中。一本入门学习 C++ 的好书是C++ Primer

【讨论】:

  • 谢谢!最后,实际解释一下使用堆栈的好处!指向数组中位置的指针是否也提高了访问值 @ 和相对于指针的性能?
【解决方案5】:

让我也试着回答一下。

指针类似于引用。换句话说,它们不是副本,而是一种引用原始值的方式。

首先,您通常必须经常使用指针的一个地方是当您处理嵌入式硬件时。也许您需要切换数字 IO 引脚的状态。也许您正在处理中断并需要在特定位置存储一个值。你得到图片。但是,如果您不直接处理硬件并且只是想知道要使用哪些类型,请继续阅读。

为什么使用指针而不是普通变量?当您处理复杂类型(如类、结构和数组)时,答案会变得更加清晰。如果您要使用普通变量,您最终可能会创建一个副本(编译器足够聪明,可以在某些情况下防止这种情况发生,C++11 也有帮助,但我们现在暂不讨论该讨论)。

现在如果你想修改原始值会发生什么?你可以使用这样的东西:

MyType a; //let's ignore what MyType actually is right now.
a = modify(a); 

这样就可以了,如果你不知道为什么要使用指针,你就不应该使用它们。当心“他们可能更快”的原因。运行您自己的测试,如果它们确实更快,则使用它们。

但是,假设您正在解决一个需要分配内存的问题。当你分配内存时,你需要释放它。内存分配可能成功也可能不成功。这就是 指针 派上用场的地方 - 它们允许您测试您分配的对象是否存在,它们允许您访问分配内存的对象通过取消引用指针。

MyType *p = NULL; //empty pointer
if(p)
{
    //we never reach here, because the pointer points to nothing
}
//now, let's allocate some memory
p = new MyType[50000];
if(p) //if the memory was allocated, this test will pass
{
    //we can do something with our allocated array
    for(size_t i=0; i!=50000; i++)
    {
        MyType &v = *(p+i); //get a reference to the ith object
        //do something with it
        //...
    }
    delete[] p; //we're done. de-allocate the memory
}

这是您使用指针的关键 - 引用假定您引用的元素已经存在。指针没有。

您会使用指针(或至少最终不得不处理它们)的另一个原因是因为它们是在引用之前存在的数据类型。因此,如果你最终使用库来做你知道它们更擅长的事情,你会发现很多这些库到处都使用指针,仅仅是因为它们已经存在了多长时间(很多其中一些是在 C++ 之前编写的)。

如果您不使用任何库,您可以将代码设计为远离指针,但鉴于指针是该语言的基本类型之一,您越快习惯使用它们,您的 C++ 技能将更具可移植性。

从可维护性的角度来看,我还应该提到,当您使用指针时,您要么必须测试它们的有效性并在它们无效时处理这种情况,或者假设它们是有效的并接受当假设被打破时,您的程序将崩溃或更糟的事实。换句话说,您对指针的选择是在出现问题时引入代码复杂性或更多的维护工作,并且您试图追踪属于指针引入的整个错误类别的错误,比如内存损坏。

因此,如果您控制所有代码,请远离指针,而是使用引用,尽可能保持它们为 const。这将迫使您考虑对象的生命周期,并最终使您的代码更易于理解。

请记住这个区别:引用本质上是一个有效的指针。指针并不总是有效的。

我是说不可能创建无效引用吗?不,它完全有可能,因为 C++ 几乎可以让你做任何事情。无意中更难做到,你会惊讶于有多少错误是无意的:)

【讨论】:

  • 您可以为内存映射 IO 编写漂亮的包装类,这样您就可以从根本上避免使用指针。
【解决方案6】:

对于 C 的许多特性为何有意义,这里有一个稍微不同但富有洞察力的看法:http://steve.yegge.googlepages.com/tour-de-babel#C

基本上,标准 CPU 架构是冯诺依曼架构,能够在这样的机器上引用内存中数据项的位置并使用它进行算术运算非常有用。如果您了解汇编语言的任何变体,您将很快看到这在低级别是多么重要。

C++ 使指针有点混乱,因为它有时会为您管理它们并以“引用”的形式隐藏它们的效果。如果你直接使用 C,那么对指针的需求就更明显了:没有其他方法可以进行引用调用,它是存储字符串的最佳方式,它是遍历数组的最佳方式,等等。

【讨论】:

    【解决方案7】:

    指针的一种用途(我不会提及其他人的帖子中已经涉及的内容)是访问您尚未分配的内存。这对于 PC 编程没有多大用处,但它在嵌入式编程中用于访问内存映射的硬件设备。

    在过去的 DOS 时代,您曾经可以通过声明指向以下内容的指针直接访问显卡的显存:

    unsigned char *pVideoMemory = (unsigned char *)0xA0000000;
    

    许多嵌入式设备仍在使用这种技术。

    【讨论】:

    • 不是使用指针的理由 - 一个类似跨度的结构 - 它也保存长度 - 更合适。这些天是gsl::span,很快就会是std::span
    【解决方案8】:

    在很大程度上,指针是数组(在 C/C++ 中)——它们是内存中的地址,如果需要(在“正常”情况下)可以像数组一样访问。

    因为它们是项目的地址,所以它们很小:它们只占用地址的空间。由于它们很小,因此将它们发送到函数很便宜。然后他们允许该功能处理实际项目而不是副本。

    如果要进行动态存储分配(例如链表),则必须使用指针,因为它们是从堆中获取内存的唯一方法。

    【讨论】:

    • 我认为说指针是数组是一种误导。实际上,数组名称是指向数组第一个元素的 const 指针。仅仅因为您可以像访问数组一样访问任意点并不意味着它是......您可能会遇到访问冲突:)
    • 指针不是数组。阅读comp.lang.c FAQ 的第 6 节。
    • 指针真的不是数组。此外,原始数组也没有太多用处——当然不是在 C++11 和 std::array 之后。
    • @einpoklum - 指针与数组足够接近,在引用操作和遍历数组时是等价的2008年
    • @einpoklum - 很明显你在这里持有不同的观点。
    【解决方案9】:

    指针在许多数据结构中很重要,这些数据结构的设计需要能够有效地将一个“节点”链接或链接到另一个“节点”。你不会“选择”指针而不是像浮点这样的普通数据类型,它们只是有不同的用途。

    指针在您需要高性能和/或紧凑的内存占用时非常有用。

    数组中第一个元素的地址可以分配给一个指针。然后,这允许您直接访问底层分配的字节。数组的全部意义在于避免您需要这样做。

    【讨论】:

      【解决方案10】:

      在变量上使用指针的一种方法是消除所需的重复内存。例如,如果您有一些大型复杂对象,您可以使用指针来指向您所做的每个引用的该变量。使用变量,您需要为每个副本复制内存。

      【讨论】:

      • 这是使用引用(或最多 - 引用包装器)而不是指针的原因。
      【解决方案11】:

      在 C++ 中,如果您想使用子类型 polymorphism,您必须使用指针。看到这个帖子:C++ Polymorphism without pointers

      真的,当您考虑它时,这是有道理的。当您使用子类型多态性时,最终您不知道会调用哪个类或子类的方法实现,因为您不知道实际的类是什么。

      这种拥有一个保存未知类对象的变量的想法与 C++ 在堆栈上存储对象的默认(非指针)模式不兼容,其中分配的空间量直接对应于类。注意:如果一个类有 5 个实例字段而不是 3 个,则需要分配更多空间。


      请注意,如果您使用 '&' 通过引用传递参数,间接(即指针)仍然在幕后涉及。 '&' 只是语法糖,(1)省去了使用指针语法的麻烦,(2)允许编译器更严格(例如禁止空指针)。

      【讨论】:

      • 不,你没有使用指针——你可以使用引用。当你写“指针在幕后”时——那是没有意义的。 goto 指令也在幕后使用——在目标机器的指令中。我们仍然没有声称使用它们。
      • @einpoklum-reinstateMonica 如果您有一组要操作的对象,将每个元素依次分配给一个临时变量并在该变量上调用多态方法,那么是的,您需要一个指针,因为您无法重新绑定引用。
      • 如果你有一个派生类的基类引用并在该引用上调用一个虚方法,那么派生类的覆盖将被调用。我错了吗?
      • @einpoklum-reinstateMonica 没错。但是您不能更改引用的对象。因此,如果您在这些对象的列表/集合/数组上循环,引用变量将不起作用。
      • 我最近有过类似的经历,除了在我的情况下,我在抽象基类中有一个类似构造函数的假静态方法,它必须创建并返回任意一个数字的新对象子类类型。无法使用引用,因为它们必须绑定到堆栈上的变量,并且我无法返回对父类型的引用,因为这样子方法将被切掉(另外,我无法将引用传递给当重点是创建一个 new 对象时是临时的)。返回指向基址的指针是:)
      【解决方案12】:

      因为到处复制大对象会浪费时间和内存。

      【讨论】:

      • 指针对此有何帮助?我认为来自 Java 或 .Net 的人不知道堆栈和堆之间的区别,所以这个答案非常没用..
      • 你可以通过引用。这样可以防止复制。
      • @MatsFredriksson - 无需传递(复制)一个大型数据结构,然后再次将结果复制回来,您只需指向它在 RAM 中的位置,然后直接修改它。
      • 所以不要复制大对象。没有人说你需要指针。
      【解决方案13】:

      C语言中对指针的需求描述here

      基本思想是语言中的许多限制(如使用数组、字符串和在函数中修改多个变量)可以通过操纵数据的内存位置来消除。为了克服这些限制,在 C 中引入了指针。

      此外,还可以看出,在将大数据类型(如具有许多字段的结构)传递给函数的情况下,使用指针可以更快地运行代码并节省内存。在传递之前制作此类数据类型的副本需要时间并且会消耗内存。这是程序员更喜欢大数据类型指针的另一个原因。

      PS:详细解释请参考link provided,附上示例代码。

      【讨论】:

      • 问题是关于C++的,这个答案是关于C的。
      • 不,问题被标记为 c 和 c++。也许 c 标签无关紧要,但它从一开始就在这里。
      【解决方案14】:

      这是我的回答者,我不保证自己会成为专家,但我在我正在尝试编写的一个库中发现了一些很棒的指针。在这个库中(它是一个带有 OpenGL 的图形 API :-)),您可以创建一个三角形,并将顶点对象传递给它们。 draw 方法采用这些三角形对象,并且......根据我创建的顶点对象绘制它们。嗯,没关系。

      但是,如果我改变一个顶点坐标呢?在顶点类中使用 moveX() 移动它或其他东西?好吧,现在我必须更新三角形,添加更多方法和性能被浪费了,因为每次顶点移动时我都必须更新三角形。仍然不是什么大不了的事,但也不是那么好。

      现在,如果我有一个包含大量顶点和大量三角形的网格,并且网格正在旋转、移动等等。我必须更新使用这些顶点的每个三角形,可能还有场景中的每个三角形,因为我不知道哪些三角形使用了哪些顶点。那是计算机密集型的,如果我在风景上面有几个网格,天哪!我遇到了麻烦,因为我几乎每一帧都更新了每个三角形,因为这些顶点一直在变化!

      使用指针,您不必更新三角形。

      如果我每个三角形类有三个 *Vertex 对象,我不仅可以节省空间,因为无数个三角形没有三个本身很大的顶点对象,而且这些指针将始终指向它们要指向的顶点,无论顶点多久改变一次。由于指针仍然指向同一个顶点,三角形不会改变,更新过程更容易处理。如果我把你弄糊涂了,我不会怀疑,我不假装自己是专家,只是把我的两分钱扔进讨论中。

      【讨论】:

        【解决方案15】:

        在 java 和 C# 中,所有的对象引用都是指针,而 c++ 的问题是您可以更好地控制指针指向的位置。记住,权力越大,责任越大。

        【讨论】:

          【解决方案16】:

          关于您的第二个问题,通常您在编程时不需要使用指针,但是有一个例外,那就是您制作公共 API 时。

          人们通常用来替换指针的 C++ 结构的问题在很大程度上取决于您使用的工具集,当您对源代码拥有所需的所有控制权时,这很好,但是如果您使用 Visual Studio 编译静态库例如 2008 并尝试在 Visual Studio 2010 中使用它,您将收到大量链接器错误,因为新项目与不向后兼容的较新版本的 STL 链接。如果你编译一个 DLL 并提供一个人们在不同工具集中使用的导入库,事情会变得更加糟糕,因为在这种情况下,你的程序迟早会无缘无故地崩溃。

          因此,为了将大型数据集从一个库移动到另一个库,如果您不想强迫其他人使用相同的工具,您可以考虑将指向数组的指针指向应该复制数据的函数你使用的。这样做的好处是它甚至不必是 C 样式的数组,您可以使用 std::vector 并通过给出第一个元素的地址来给出指针 &vector[0] 例如,然后使用std::vector 在内部管理数组。

          在 C++ 中再次使用指针的另一个很好的理由与库有关,请考虑在程序运行时有一个无法加载的 dll,因此如果您使用导入库,则依赖关系不满足并且程序崩溃。例如,当您在应用程序旁边的 dll 中提供公共 api 并且您想从其他应用程序访问它时,就是这种情况。在这种情况下,为了使用 API,您需要从其位置加载 dll(通常位于注册表项中),然后您需要使用函数指针才能调用 DLL 中的函数。有时,制作 API 的人会很好地给你一个 .h 文件,其中包含帮助函数来自动化这个过程并给你所有你需要的函数指针,但如果不是,你可以在 windows 和 dlopen 上使用 LoadLibrary 和 GetProcAddress 和dlsym 在 unix 上获取它们(考虑到您知道函数的整个签名)。

          【讨论】:

            【解决方案17】:
            • 在某些情况下,需要函数指针才能使用共享库(.DLL 或 .so)中的函数。这包括跨语言执行任务,通常会提供 DLL 接口。
            • 制作编译器
            • 制作科学计算器,你有一个数组或向量或函数指针的字符串映射?
            • 尝试直接修改显存 - 制作自己的图形包
            • 制作 API!
            • 数据结构 - 您正在制作的特殊树的节点链接指针

            指针有很多原因。如果您想保持跨语言兼容性,那么在 DLL 中进行 C 名称修饰尤其重要。

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 2017-05-15
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多