解释指向Javascript开发人员的指针

时间:2014-07-11 00:51:18

标签: pointers

我开始向后学习编码:首先是高级编码。这显然有责任缺少一些我应该知道的基本概念,当我尝试学习低级语言时,它会引发我的兴趣。

我已多次尝试理解指针,但是解释很快就会超出我的想法,通常是因为所有的示例代码都使用了使用指针的语言,我不了解其他的东西,然后我旋转

我是Javascript中最流利的(也是非常的)。

您如何解释指向像我这样悲伤的Javascript开发人员的指针?有人能为我提供一个实际的,真实的例子吗?

甚至可能会说明,如果Javascript有指针,你可以做x,并且由于y,指针与原始变量不同。

2 个答案:

答案 0 :(得分:2)

C ++规则相当简单且一致。我实际上发现Javascript如何处理对象引用和原型更加不直观。

前言A:为什么Javascript开始不好?

在解决指针之前,你需要从根本上理解的第一件事是变量。您需要知道它们是什么以及计算机如何跟踪它们。

来自Javascript背景,您习惯于分配给作为参考的对象的每个变量。也就是说,两个变量可以引用同一个对象。这本质上是指针,没有任何语法允许更复杂的使用。你也习惯于" basic"的隐式副本。数字类型。也就是说:

var a = MyObject;
var b = a;

现在如果你改变b,你也改变了。您需要显式复制MyObject才能让两个变量指向它的不同实例!

var a = 5;
var b = a;

现在,如果你改变b,a实际上并没有改变。这是因为当a是简单类型时将a分配给b将自动为您复制。您不能获得与具有简单数字的对象相同的行为,反之亦然,因此当您希望两个变量引用相同的数字时,您必须将其包装在对象中。没有明确的方法来指示您希望如何处理基本类型的引用与副本。

您可以看到这种不一致的行为,语法没有变化(但行为的极端变化)可以使变量之间的关系和它们包含的内容变得混乱。出于这个原因,我强烈建议在我们继续探索明确指针的过程中暂时消除这种心理模型。

前言B:YOLO:堆栈上的可变生命周期

所以,让我们从这里开始用C ++术语进行讨论。就变量和指针而言,C ++是最明确的语言之一。 C ++是一个很好的切入点,因为它的低级别足以在内存和生命周期方面进行讨论,但是高水平足以在相当的抽象级别上理解事物。

因此,在C ++中创建任何变量时,它存在于某个范围内。有两种方法可以在堆栈和堆上创建变量。

堆栈指的是应用程序的调用堆栈。每个支撑对都会将一个新的上下文推送到堆栈上(当它用完时弹出它)。当您创建局部变量时,它存在于该特定堆栈帧中,当弹出该堆栈帧时,变量将被销毁。

范围的一个简单示例:

#include <iostream>
#include <string>

struct ScopeTest{
    ScopeTest(std::string a_name):
        name(a_name){
        std::cout << "Create " << name << std::endl;
    }
    ~ScopeTest(){
        std::cout << "Destroy " << name << std::endl;
    }
    ScopeTest(ScopeTest &a_copied){
        std::cout << "Copy " << a_copied.name << std::endl;
        name = a_copied.name + "(copy)";
        a_copied.name += "(original)";
    }

    std::string name;
};

ScopeTest getVariable(){ //Stack frame push
    ScopeTest c("c"); //Create c
    return c; //Copy c + Destroy c(original)
}

int main(){
    ScopeTest a("a"); //Create a
    {
        ScopeTest b("b"); //Create b
        ScopeTest d = getVariable();
    } //Destroy c(copy) + Destroy b
} //Destroy a

输出:

Create a
Create b
Create c
Copy c
Destroy c(original)
Destroy c(copy)
Destroy b
Destroy a

这应该明确说明变量如何将其生命与堆栈联系起来,如何复制它以及它何时死亡。

前言C:堆上的YOLO可变生命周期

所以,这在概念上很有趣,但变量也可以在堆栈外部分配,这称为&#34;堆&#34;记忆因为它在很大程度上是无结构的。堆内存的问题是你根本没有基于范围的自动清理。所以你需要一种方法将它与某种&#34;手柄&#34;跟踪它。

我将在这里说明:

{
    new ScopeTest("a"); //Create a
} //Whoa, we haven't destroyed it!  Now we are leaking memory!

所以,显然我们不能说&#34;新X&#34;没有跟踪它。内存得到了分配,但并没有将自己与生命联系在一起,所以它永远存在(就像一个记忆吸血鬼!)

在Javascript中,你可以将它绑定到一个变量,当对象的最后一个引用消失时,对象就会死掉。稍后我将讨论C ++中一个更高级的主题,但是现在让我们看一下简单的指针。

在C ++中,当您使用new分配变量时,跟踪它的最佳方法是将其分配给指针。

前言D:指针和堆

正如我所说,我们可以使用指针跟踪堆上的已分配内存。我们之前的漏洞程序可以修复如下:

{
    ScopeTest *a = new ScopeTest("a"); //Create a
    delete a; //Destroy a
}

ScopeTest * a;创建一个指针,并将其分配给新的ScopeTest(&#34; a&#34;)为我们提供了一个句柄,我们可以实际使用它来清理和引用堆内存中存在的变量。我知道堆内存听起来有点令人困惑,但它基本上是一个混乱的记忆,你可以指出并说'嘿嘿你,我想要一个没有寿命的变量,做一个让我指出它&# 34。

使用new关键字创建的任何变量必须后跟正好1(且不超过1)的删除,否则它将永远存在,使用内存。如果您尝试删除多于0的任何内存地址(这是一个无操作),您可能会删除不在程序控件下的内存,这会导致未定义的行为。

ScopeTest * a;声明一个指针。从现在开始,任何时候你说&#34; a&#34;你指的是一个特定的内存地址。 * a将引用该内存地址处的实际对象,您可以访问它的属性(* a).name。 A-&GT;在C ++中是一个特殊的运算符,与(* a)完全相同。

{
    ScopeTest *a = new ScopeTest("a"); //Create a
    std::cout << a << ": " << (*a).name << ", " << a->name << std::endl;
    delete a; //Destroy a
}

以上输出如下所示:

007FB430: a, a

其中007FB430是存储器地址的十六进制表示。

因此,从最纯粹的意义上说,指针实际上是一个内存地址,并且能够将该地址视为变量。

指针和变量之间的关系

我们不必使用堆分配内存的指针!我们可以指定一个指向任何内存的指针,甚至是存储在堆栈中的内存。小心你的指针不会超出它所指向的记忆,否则你会有一个悬空指针,如果你继续尝试使用它,可能会做坏事。 / p>

确保指针有效总是程序员的工作,在C ++中实际上有0个检查来帮助你处理裸存储器。

int a = 5; //variable named a has a value of 5.
int *pA = &a; //pointer named pA is now referencing the memory address of a (we reference "a" with & to get the address).

现在pA指的是与&amp; a相同的值,也就是说,它是a的地址。

* pA指的是与。

相同的值

你可以治疗* pA = 6;与a = 6相同。观察(从上面的两行代码继续):

std::cout << *pA << ", " << a << std::endl; //output 5, 5
a = 6;
std::cout << *pA << ", " << a << std::endl; //output 6, 6
*pA = 7;
std::cout << *pA << ", " << a << std::endl; //output 7, 7

你可以看到为什么* pA被称为&#34;指针&#34;。它字面上指向内存中的相同地址。到目前为止,我们一直使用* pA来取消引用指针并访问它指向的地址的值。

指针有一些有趣的属性。其中一个属性是它可以改变它所指向的对象。

int b = 20;
pA = &b;
std::cout << *pA << ", " << a << ", " << b << std::endl; //output 20, 7, 20
*pA = 25;
std::cout << *pA << ", " << a << ", " << b << std::endl; //output 25, 7, 25
pA = &a;
std::cout << *pA << ", " << a << ", " << b << std::endl; //output 7, 7, 25
*pA = 8;
std::cout << *pA << ", " << a << ", " << b << std::endl; //output 8, 8, 25
b = 30;
pA = &b;
std::cout << *pA << ", " << a << ", " << b << std::endl; //output 30, 8, 30

所以你可以看到指针实际上只是内存中一个点的句柄。在许多情况下,这可能非常有用,不要因为此示例过于简单而将其删除。


现在,您需要了解的关于指针的下一件事是,只要您增加的内存属于您的程序,就可以增加它们。最常见的例子是C字符串。在现代C ++中,字符串存储在一个名为std :: string的容器中,使用它,但我将使用旧的C样式字符串来演示带指针的数组访问。

密切关注++字母。这样做是通过指向它的类型的大小来增加指针所看到的内存地址。

让我们稍微分解一下,重读上面几句,然后继续。

如果我有一个sizeof(T)== 4的类型,那么每个++ myPointerValue将在内存中移动4个空格以指向下一个&#34;值&#34;那种这是指针&#34; type&#34;的事项。

char text[] { 'H', 'e', 'l', 'l', 'o', '\0' }; //could be char text[] = "Hello"; but I want to show the \0 explicitly
char* letter = text;

for (char* letter = &text[0]; *letter != '\0';++letter){
    std::cout << "[" << *letter << "]";
}
std::cout << std::endl;

只要没有&#39; \ 0&#39;上面就会循环上面的字符串。 (null)字符。请记住,这可能是危险的,并且是程序中不安全的常见来源。假设您的数组被某个值终止,但随后获得一个溢出的数组,允许您读取任意内存。无论如何,这是一个高级别的描述。

因此,明确使用字符串长度并在常规使用中使用更安全的方法(例如std :: string)会好得多。


好的,作为将事情置于背景中的最后一个例子。让我们说我有几个谨慎的&#34;细胞&#34;我希望将它们链接成一个连贯的&#34;列表&#34;。使用非连续内存的最自然的实现方法是使用指针将每个节点定向到序列中的下一个节点。

使用指针,您可以创建各种复杂的数据结构,树,列表等等!

struct Node {
    int value = 0;
    Node* previous = nullptr;
    Node* next = nullptr;
};
struct List {
    List(){
        head = new Node();
        tail = head;
    }
    ~List(){
        std::cout << "Destructor: " << std::endl;
        Node* current = head;
        while (current != nullptr){
            Node* next = current->next;
            std::cout << "Deleting: " << current->value << std::endl;
            delete current;
            current = next;
        }
    }
    void Append(int value){
        Node* previous = tail;
        tail = new Node();
        tail->value = value;
        tail->previous = previous;
        previous->next = tail;
    }

    void Print(){
        std::cout << "Printing the List:" << std::endl;
        Node* current = head;
        for (Node* current = head; current != nullptr;current = current->next){
            std::cout << current->value << std::endl;
        }
    }
    Node* tail;
    Node* head;
};

并将其投入使用:

List sampleList;
sampleList.Append(5);
sampleList.Append(6);
sampleList.Append(7);
sampleList.Append(8);
sampleList.Print();

列表看起来似乎很复杂,但我不会在这里介绍任何新概念。这与我上面提到的完全相同,只是为了实现目的。

完全理解指针的功课将是在List中提供两种方法:

  1. Node * NodeForIndex(int index)
  2. void InsertNodeAtIndex(int index,int value)
  3. 此列表实现非常差。 std :: list是一个更好的例子,但大多数情况下由于数据局部性你真的想坚持使用std :: vector。指针是非常强大的工具,也是计算机科学的基础。您需要了解它们以了解您每天依赖的常见数据类型是如何组合的,并且随着时间的推移,您将会欣赏C ++中指针与值的明确分离。

    超越简单指针:std :: shared_ptr

    std :: shared_ptr使C ++能够处理引用计数指针。也就是说,它给出了与Javascript对象赋值类似的行为(当对象的最后一个引用被设置为null或被破坏时,对象被销毁)。

    std :: shared_ptr就像任何其他基于堆栈的变量一样。它将其生命周期与堆栈联系起来,然后保存指向堆上分配的内存的指针。在这方面,它以更安全的方式封装了指针的概念,而不是必须记住删除。

    让我们重新访问我们之前发生泄漏记忆的例子:

    {
        new ScopeTest("a"); //Create a
    } //Whoa, we haven't destroyed it!  Now we are leaking memory!
    

    使用shared_ptr我们可以执行以下操作:

    {
        std::shared_ptr<ScopeTest> a(new ScopeTest("a")); //Create a
    }//Destroy a
    

    而且,有点复杂:

    {
        std::shared_ptr<ScopeTest> showingSharedOwnership;
        {
            std::shared_ptr<ScopeTest> a(new ScopeTest("a")); //"Create a" (ref count 1)
            showingSharedOwnership = a; //increments a's ref count by 1. (now 2)
        } //the shared_ptr named a is destroyed, decrements ref count by 1. (now 1)
    } //"Destroy a" showingSharedOwnership dies and decrements the ref count by 1. (now 0)
    

    我在这里不太进一步,但这应该让你的思想开阔。

答案 1 :(得分:2)

这是对第一原则的独立答案的尝试。

指针是允许实现引用语义的类型系统的一部分。这是如何做。我们假设我们的语言有一个类型系统,每个变量都是某种类型。 C是一个很好的例子,但许多语言都是这样的。所以我们可以有一堆变量:

int a = 10;
int b = 25;

此外,我们假设函数参数始终从调用者范围复制到函数范围。 (对于许多真实语言也是如此,但是当类型系统从用户“隐藏”时(例如在Java中),细节可能很快变得微妙)。所以我们有一个功能:

void foo(int x, int y);

调用foo(a, b)时,变量ab会被复制到与形式参数对应的局部变量xy中,并且这些副本是在功能范围内可见。无论函数对xy执行什么操作,对调用网站上的变量ab都没有影响。整个函数调用对调用者来说是不透明的。

现在让我们转到指针。对于每个对象类型T,带有指针的语言包含相关类型T *,它是“指向T的指针”类型。类型T *的值由获取类型为T的现有对象的地址生成。所以一个有指针的语言也需要有一种方法来产生指针,这就是“取一些东西的地址”。指针的目的是存储对象的地址。

但这只是图片的一半。另一半是如何处理对象的地址。关心对象地址的主要原因是能够引用其地址被存储的对象。该对象是通过第二个操作获得的,适当地称为解除引用,当应用于指针时,产生“指向”的对象。重要的是,我们没有对象的副本,但我们得到实际对象

在C中,address-of运算符拼写为&,解除引用运算符拼写为*

int * p = &a;    // p stores the address of 'a'

*p = 12;         // now a == 12

最终作业的第一个操作数*p 对象a本身。 a*p都是同一个对象。

现在为什么这有用?因为我们可以将指针传递给函数,以允许函数在函数自己的范围之外更改函数。指针允许间接,因此用于引用。你可以告诉函数“别的东西”。这是标准的例子:

void swap(int * p, int * q)
{
    int tmp = *p;
    *p = *q;
    *q = tmp;
}

我们可以告诉函数swap 关于我们的变量ab,为它们提供这些变量的地址:

swap(&a, &b);

通过这种方式,我们使用指针为函数swap实现引用语义。该函数在其他地方引用变量并可以修改它们。

因此可以概括参考语义的基本机制:

  • 调用者获取要引用的对象的地址:

     T a;
     mangle_me(&a);
    
  • 被调用者接受一个指针参数,并取消指针以访问所引用的值。

     void mangle_me(T * p)
     {
         // use *p
     }
    

参考语义对编程的各个方面都很重要,许多编程语言以某种方式提供它们。例如,C ++为该语言添加了本机引用支持,在很大程度上消除了对指针的需求。 Go使用显式指针,但有时通过自动解除引用指针来提供一些符号“方便”。 Java和Python在其类型系统中“隐藏”指针,例如变量的类型在某种意义上是指向对象类型的指针。在某些语言中,某些类型(如int)是裸值类型,而其他类型(如列表和词典)是“隐藏指针包含”引用类型。你的milage可能会有所不同。