指针参数传递和.Net中String类型的表现

本来不想对这两个老掉牙的问题进行讨论,但联系起来看确实有用。先看代码:

    class Program {
        static void Foo( string s ) {
            s = "变了";
        }

        static void Main( string[] args ) {
            string s = "原始String";
            Foo( s );
            Console.WriteLine( s );
            Console.ReadKey();
        }
    }

没错,这就是最经典的“作为引用类型的String类传参时候表现值类型特点”的例子。上述程序意料之中输出为“原始String”,但其实这并不是因为String自身特殊到所谓“引用类型表现值类型特点”,为什么这么说?看下面这个例子:

    class Person {
        public String Name;
    }

    class Program {
        static void Foo( Person p ) {
            p = new Person { Name = "李四" };
        }

        static void Main( string[] args ) {
            Person p = new Person { Name = "张三" };
            Foo( p );
            Console.WriteLine( p.Name );
            Console.ReadKey();
        }
    }

这次就清楚多了,基本都能看出输出的是“张三”,关键在于自定义class用了new这个关键字、而String类的等号便是最大的迷惑点。所以说String类在当作参数传递的时候和其他引用类型并无二致。

作为本地参数的s和p同原来的变量是完全不同的引用存在,只不过引用到的是同一块内存。使用new或者重载的=进行新内存分配后,原来的变量指向并没有发生变化。

好,那么就引出标题里面的第一个问题了“指针参数传递”,在C/C++中很容易见到这样的错误:

void Foo( int* a, int* b )
    {
    int* tmp = a;
    a = b;
    b = tmp;
    }

int main(int argc, char *argv[])
    {
    int a = 0;
    int b = 5;
    Foo( &a, &b );
    cout<<a<<","<<b<<endl;
    }

对于形参a和b而言,它们只是在栈上对于原本变量的一份拷贝,指向同一块内存,改变它的指向是没有意义的。

a(变量) --|
         |--->  0
a(参数) --|

如果没有在这上面碰过钉子的请受我一拜。如果希望改变原有指针的内容,需要直接改变被指向区域的值或者传递二级指针:

void Foo( int* a, int* b )
    {
    int tmp = *a;
    *a = *b;
    *b = tmp;
    }

void FooAlloc( int** p, int size )
    {
    *p = malloc( sizeof( int ) * size );
    }

可以说这个问题和开始提到的String的特殊表现殊途同归:

  • 归根结底,不管是指针还是引用,当作参数进行传递的时候仍然是值。
  • 指针本身的操作和指针指向内容的操作是不同的,星星太多就容易乱。同理的还有const int* i和int* const i。

还是那句话,String并不特殊,它在参数传递上表现得和其他引用类型没什么不同,但不同点在于String是一个immutable类型,所有对其进行的修改,例如SubString、连接(+)、赋值(=)等等,都会当作创建新拷贝的操作对待,这也就是导致String表现出类似于值类型特点的原因。

参考资料