关闭

关于函数和字符指针的一些看法

616人阅读 评论(0) 收藏 举报

一些初学者好像对涉汲到指针的函数的调用有些困惑,我献丑来说两句。希望对初学者有些帮助,同时也
请高手指正。互相学习,共同进步。

先请看下面这个小程序:*/

#include <iostream>
using namespace std;

char* MyStrcpy(char* p);

int main()
{
    char* p = NULL;

    cout << MyStrcpy(p) << endl;  //为了呆会讲解的方便这句不防称呼为str输出。

    cout << p << endl;            //这句不防称呼为p输出。
   
    return 0;
}

char* MyStrcpy(char* p)
{   
    char str[15];

    strcpy(str, "I lvoe C++!");

    p = str;
   
    return str;
}

//////////////////////////////////////////////////////////////////////
   
请问这个程序main中的str输出和p输出语句能得出正确结果吗?

如把MyStrcpy函数改成下面几种样子,结果又如何?
A:

char* MyStrcpy(char* p)
{
    char *str = new char[15];   //这里已修改。

    strcpy(str, "I lvoe C++!");

    p = str;
   
    return str;
}

/////////////////////////////////////
B:

char* MyStrcpy(char* p)
{
    static char str[15];        //这里已修改。

    strcpy(str, "I lvoe C++!");

    p = str;
   
    return str;
}

///////////////////////////////////////
C:

char* MyStrcpy(char* &p)            //这里已修改。另请相应修改程序头处该函数的声明。
{   
    char *str = "I lvoe C++!";      //这里已修改。

    //strcpy(str, "I lvoe C++!");   //这里已修改

    p = str;
   
    return str;
}

////////////////////////////////////////////////////////////////////////////

好,我们一一来看看,让我们先看原题:
char* MyStrcpy(char* p)
{
    char str[15];

    strcpy(str, "I lvoe C++!");

    p = str;
   
    return str;
}

我们知道str是一个局部变量,返回一个局部地址这肯定是不行的,但是p输出行不行呢?
因为p是在main中声明的啊?应该行的吧?!答案是也不行。
   因为str是在栈区分配的内存,当函数运行结束之后会把栈内存收回(但并没有把该内存置零,
只是说不应再访问,否则视为非法访问,其结果是“未定义的”,未定义意味着什么呢?意味着
其行为不确定,啥事都有可能发生。例如,把你的硬盘格式化,系统崩溃,恩,给你的
同性boss发求爱邮件。),所以在str输出中再访问该内存是非法的,应当拒绝写出返回
局部地址的函数来。p = str, p的值同样是一个局部地址,所以也不行。


再看看A情况:
A:

char* MyStrcpy(char* p)
{
    char *str = new char[15];   //这里已修改。

    strcpy(str, "I lvoe C++!");

    p = str;
   
    return str;
}

str输出可以得出正确答案,p输出还是不行。
因为str是在堆中分配的内存,堆中分配的内存如果不调delete(delete[],free),会一直占用,
直到main函数的结束。所以当不再使用时要及时释放。这里,函数返回的是堆中的内存地址,
所以当执行cout << MyStrcpy(p) << endl语句是,输出的是存放在堆中的一串字符串。可以
得出正确答案。但是这里存在一个堆内存释放问题,应该在调用该函数的函数中记得给以释放。
但是当MyScpy函数和main函数不是同一个人写的时候往往忘记释放,结果内存泄漏,最终有可能
造成系统蹦溃。也许你会说:切,不就是15个字节吗,算得了什么,现在内存便宜得很。要知道
该函数可能会多次调动到,比如

    for(int i=0; i<100000000; i++)
    {
        for(int j=0; j<100000000; j++)
        {
            char *p = NULL;
            char* str = MyStrcpy(p);
           
            //do something...
           
        }
    }
   
    而且该程序有可能一运行起来就是几年,吼吼。所以最好避免写出这样的函数,最好在一个函数中
    分配了堆内存,就要在该函数结束前释放该堆内存。如果确实需要在一个函数中分配堆内存
    而由另外一个函数来释放,就应该做些保护工作。比如:

    class object;  //这是一个在别处定义了的一个大型类。

    std::auto_ptr<object> func()
    {
        return std::auto_ptr<object>(new object);
    }

    这里函数的返回类型是:auto_ptr<object>, 使用了C++标准库的模板auto_ptr<class T>,
    在调用函数中就可以这样使用:

    std::auto_prt<object> apObjet = func();

    然后就可以通过apObject指针去调用object对象中的成员。当apObject的生命结束时它会自动释放
    堆中的内存。但是auto_ptr功能有限,有好些限制,详情请参考c++标准库,c++ primer一书
    中亦用相关的知识介绍。如func函数中使用的是 new[],那么函数的返回值就应该自己写一个模板类
    来包装了,在该模板类的析构函数中调用delete[].

    回到我们的题目,p输出也是不行的。

    让我们来了解一下函数调用的实质。在C++中函数的参数,返回值传递的方式有三种: 传值,传址
    (实际上也是传值),传引用(C中没有这项)。

    当一个函数被调用时,会在栈空间分配内存,首先会分配一些内存用来存放返回到主调动函数中
    去的地址,接着会分配形参的内存,接着把实参的东西copy过来,这才开始被调用函数的代码的执行
    当遇到return result; 语句时,会在函数的被调用处产生一个临时对象,然后把result的值传递
    过去(所以请注意,返回的不是result本身,而是该result的一个拷贝,或者该对象的地址),接着
    收回栈空间内存,最后接着在主调动函数中继续运行。

    让我们来看一个例子:我想写一个函数其功能是交换两个整数的值,然后返回他们的和。


    int MySwap( int val1, int avl2)
    {
        int result = val1;

        val1 = val2;

        val2 = val1;

        result = val1 + val2;

        return result;
    }

    int main()
    {
        int a = 3;

        int b = 5;

        int sun = MySwap(a, b);

        cout << "a = " << a << "/tb = " << b << "/ta+b = " << sun << endl;

        return 0;
    }

    这里当MySwap函数被调动时,首先在栈中分配内存,存放返回到main中的地址。接着分配变量
        val1和val2的内存空间,接着把a,b的值分别copy到val1和val2中。接着开始分配temp的内存空间
        并初始化(所以在属于该被调用函数的栈空间中分配了四个内存,具体每个多大与具体的数据类
        型和平台相关,在win32平台中这几个应该都是4字节。)。然后交换val1与val2的值,接着求和,
        当遇到return result;时,在函数的返回处产生一个临时对象temp,接着把result的值copy给temp,
        copy完之后收回栈区间,接着执行sun = temp;该语句结束之后,temp的生命也结束了。我们看到
        整个过程中交换的是val1和 val2的值,a, b的值并没有交换。还是原来的值。这就是传值的情形,
        整个过程发生了多次copy,如果参与交换的两个对象不是整数而自定义的对象,就要多次调动copy
        构造函数和析构函数,需要不少的开销。所以传值是一种效率较低的传递方式。

        做以下修改:

        int*  MySwap( int* val1, int *avl2)
    {
        int result = *val1;

        *val1 = *val2;

        *val2 = *val1;

        result = *val1 + *val2;

        return &result;
    }

    int main()
    {
        int a = 3;

        int b = 5;

        int *sun = MySwap(&a, &b);

        cout << "a = " << a << "/tb = " << b << "/ta+b = " << *sun << endl;

        return 0;
    }

    当函数被调动时,依次分配用来保存返回的地址的内存,指针变量 val1,val2的空间。
    接着分别把a, b的地址copy过来,接着分配result的空间,接着把取出val1内存空间
    中的地址,然后再把该地址中的数(即:a中的数3)赋给result,接着交换a,b 的值,
    接着求和,最后把result的地址copy给temp(此时temp为一指针),接着收回栈空间,
    接着把temp的内容赋给sun    这样,sun 指向了已被收回的栈中的一个内存,
    cout << *sun<< endl;属于非法访问。

    所以应该把程序改成:

    int result;  //全局变量。在全局数据区分配内存空间。

    int*  MySwap( int* val1, int *avl2)
    {
        result = *val1;

        *val1 = *val2;

        *val2 = *val1;

        result = *val1 + *val2;

        return &result;
    }

这样当遇到return &result;时,把全局变量result在全局数据区分配的内存的地址copy给
     temp,接着收回栈空间,(不能收回result的空间,因为其不是在栈在分配的内存),
     接着把temp的内容赋给sun,这样sun的内容就是全局数据区中的一段内存。一切都OK。
     这就是传址方式。

     最后看一下传递引用,把程序改为:

     int& MySwap( int &val1, int &avl2)
    {
        int result = val1;

        val1 = val2;

        val2 = val1;

        result = val1 + val2;

        return result;
    }

    int main()
    {
        int a = 3;

        int b = 5;

        int& sun = MySwap(a, b);

        cout << "a = " << a << "/tb = " << b << "/ta+b = " << sun << endl;

        return 0;
    }


  当函数被调用时,栈空间以下分配:返回地址, 引用val1和引用val2的内存空间,他们的
      大小和指针是一样的。接着把a,b的地址copy过来,接着分配result的空间,当使用到
      val1时取出其内容(是a的地址。)再取出a的数,把它赋给result,下面的和传址方式
      差不多。同样存在返回局部变量的引用的问题。

      改成:

      int result;  //全局变量

    int& MySwap( int &val1, int &avl2)
    {
        result = val1;

        val1 = val2;

        val2 = val1;

        result = val1 + val2;

        return result;
    }

    当遇到return result;时,把全局变量result的地址copy给temp,收回栈空间,把temp赋给sun
        这样一来,sun的内容就是全局变量resul的地址,只不过编译器知道他是一个引用,以后
        对它的一切操作都是对它的内存中保存的地址里面所存放的数进行操作。我们看到了,引用传
        递是通过指针来实现的,但是他的操作方式却和传值一样。指针的杀伤力太强了,如果只是
        引用一下该对象,那么请用引用不要用指针。

        这里顺便说一下关于传值的另一个技巧。当返回值是一个自定义的对象时,我们最好用引用返回
        而不是传值返回,但有时候我们的程序要求返回一个对象,返回引用并不合适时,下面的情况
        后者比较后:

        class object;

        object func( object &a, object &b)
        {
            //do something

            object sun(a + b);

            return sun;
        }

        应该写成:

        object func( object &a, object &b)
        {
            return object(a+b);
        }

        这两者有什么区别呢?前者先创建一个sun对象并初始化,然后创建temp对象,接着调用copy构造
            函数把sun拷贝给temp.后者直接建立temp对象,并直接用(a+b)初始化temp.也就是说前者
            相当于:object sun(a + b); object temp = sun;, 而后者相当于: temp(a+b);少了构造
            sun和析构sun这两步。相应的,我们也应该把 
           
            int res( x1 + x2);
            return res;

            直接写成:
                return int(x1 + x2);


            回到我们的题目,A情况的p输出为什么不行?

    char* MyStrcpy(char* p)
   {
       char *str = new char[15];   //这里已修改。

       strcpy(str, "I lvoe C++!");

       p = str;
   
       return str;
   }

    函数char* MyStrcpy(char* p)被调用时,栈空间以下分配:返回地址, 形参p的空间,
    接着把实参p的内容(即:NULL,注意,这里传递的是p的内容,而不是p本身的地址),接着分配
    str指针的空间,接着在堆中分配15字节的空间,str指向该堆空间的首地址,接着调用
    strcpy函数,使得堆中的内容是“I love C++!",接着把 str的赋给形参p,此时,形参p指向
    堆空间。接着返回str,然后收回栈空间,str 和 形参p,都没有了。大家看到了,除了把实参p
    的值NULL copy给形参p之后,两都再也没有任何关系了。函数调用之后main中的p还是为NULL。
    要想实参p与形参p有关系可以把函数的参数改为传递指针的指针:

    char* MyStrcpy(char** p)       //这里已修改,请相应修改函数的声明。
   {
       char *str = new char[15];   //这里已修改。

       strcpy(str, "I lvoe C++!");

       *p = str;                   //这里已修改。
   
       return str;
   }

   并且把主函数中MyStrcpy的调用改为: MyStrcpy(&p), 这样子p输出可以得出正确答案。

     
接下来看看B情况:
B:

char* MyStrcpy(char* p)
{
    static char str[15];        //这里已修改。

    strcpy(str, "I lvoe C++!");

    p = str;
   
    return str;
}

B情况下,str输出可以得出正确答案。p输出不行(原因同A情况)。

我们知道一个程序被执行时,内存空间分为四个区:

代码区(code area): 存放我的程序代码。
全局数据(data area): 全局变量,静态数据, 常量都在该区分配内存。
栈区(stack area): 为运行函数而分配的局部变量,函数参数,返回数据,返回地址等存放在栈区。
堆区(heap area):  由new,new[], malloc等函数分配的内存在该区。

其中尢以堆区的内存需要特别关注,分配了的内存用不到之后要及时调用delete ,delete[] ,free
等函数来释放,并且应该注意搭配,由new分配来的内存就应该调用 delete 来释放,不能调用 delete[]
来释放。而且只能释放一次。对于delete p; 如p是自定义类型的指针,那么其实现的伪码大至如下:

if( p == NULL)
{
    return ;
}

//调用p所指向的对象的析构函数;

p->destruction();

free(p);

我们看到了,对于一个值为NULL的指针p,不管调用多少次delete p都不成问题,但对于一个不等于NULL的指针
p,连续调用两次delete p就会出错了。因为第一次调用时,首先调用析构函数,接着释放堆内存。只是简单
的释放掉,但并没有把内存中的内容清除掉,只是说,当别的程序需要堆内存时就可以动用这一块已释放的
内存了,同时也没有把p的内容改为零,p的中的地址并没有改变,还是指向该堆区。有些初学者以为
delete p; 就是把p干掉,p的生命已结束。不是这样子的。对于下面的代码:

class object;

object* g_pObject = new object;

void func()
{
    //用g_pObject do something...

    delete g_pObject;

}

首先会在全局数据区分配4个字节的空间给变量g_pObject,接着在堆区分配sizeof(object)大小的内存,接着
调用object的构造函数,构造一个object对象,接着把堆空间的首地址copy一份存放到全局变量g_pObject
的内存空间中。当调用func函数时,delete g_pObject;意味着,到全局数据区为g_pObject所分配的四个字节
内存空间中取出存放在其中的内容,看看它指向堆中的那一块地方,接着把那一亩三分地扫为平地。整个过程
g_pObject还是原来的那个g_pObject;它在全局数据区中所分配的4字节内存空间还在,并没有delete掉,仍至
连他里面的值也没有改变。他的生命依然茂盛,和别的全局全量一样一直存活到main函数结束。所以这时候
只要我们确定原先分配的堆内存已经释放掉,我们就可以再使用 g_pObject ,让他指向别的地方。我们怎样
确定他原先指向的堆内存已释放掉了呢,像下面这样做行吗?

void func2()
{

    if ( g_pObject != NULL)
   {
        delete g_pObject;
   }

   g_pObject = new object;
}

这样做是不行的,如果在这之前已经调用了func函数,把堆内存释放掉了,但 g_pObject 并没有置零,接着
delete g_pObject;会再次被调用,出错。然,我们可以把程序改成这样子:

void func()
{
    if (p != NULL)
    {
    //用g_pObject do something...

      delete g_pObject;

      g_pObject = NULL;
    }

}

void func2()
{

    if ( g_pObject != NULL)
   {
        delete g_pObject;

        g_pObject = NULL:
   }
 
   g_pObject = new object;
}

good idea,delete 之后接着是个好的编程习惯。对于一个值为零的指针delete多少次都没关系。既然我们
常要在delete之后再写一句赋值语句,有没有更简便一些的方法,只写一句语句就可以实现了的呢?
我们重写operator delete()函数:

void operator delete(void *p)
{
    delete p;

    p = NULL:
}

void func()
{
    if (p != NULL)
    {
    //用g_pObject do something...

      delete g_pObject;
    }

}

void func2()
{  
      delete g_pObject;   
 
   g_pObject = new object;
}

这样子是不行的,因为p = NULL:只是把形参p置零,和实参g_pObject没有半点关系。
也许你马上会说传递指针的指针过来啊!呵呵,我也想啊,只是C++规定,operator delete函数
返回类型只能为void, 第一个参数只能为void*,有第二个数的话也只能为size_t。只好另想他法。

定义一个模板函数:

template<class T>
void destroy(T &p)
{
    delete p;

    p = NULL;
}

然后就可以这样使用了:
destroy(g_pObject);

只是从字面上看destroy只是清除的意思,并没有置零的意思,不知情的人看了会纳闷。我想啊想啊,
想了N久,想出了一个好名字:delete0, 够形象,delete大家都清楚,然后后面还有一个0,我想一个
不知情的人看了,都能猜出是什么意思:释放然后置零。只是开了先例,我从未见过带数字的函数名
,不知道大家见过了没有?这样子我们的模板函数可以写成:

template<class T>
inline void delete0( T &p)
{
    delete p;

    p = 0;
}

因为是内联的,所以写delete0(g_pObject); 和 写 delete g_pObject; g_pObject = 0; 开销是一样的。
当然还要写一个实现delete[]操作并置零的模板,我想了好久都不知道该叫delete1好,还是delete2好。


这里还是有点问题,有可能在某个函数中调用delete g_pObject直接把堆内存释放掉,而不是通过delete0,
这样问题又出来了,所以一般情况下,全局指针指向一个内存之后就不要再让它指向别的地方了。大家看到
了,关于动态内存的管理真的要很小心,一不小心就可能产生个未定义行为出来,未定义意味着什么?
意味着啥事都有可能发生。更可怕的是程序在你手里时一切安好。怎么测试都能通过,就在你把程序交付
给客户的时候,突然在客户的脸上爆发。哗啦啦,爆米花!更有甚者,客户使用一段时间之后才爆发,把客
户的一些重要的商业数据都给清除了,那就有官司打了。这一切都是指针惹的祸,但我们又不能不使用指针
(没有指针参与的程序也能叫C/C++程序吗?!!)只能说能尽量少用,能用引用代替的就用引用。不行的话
就要注意随时看管好我们的指针,不要让他闯祸了。坚持各人分配的内存各人管理的原则,尽量在一个函数
里分配了内存由该函数释放。如果需要一个函数分配的内存由另外一个函数释放就应该把输出参数设置为
auto_ptr,或者自己写的包装类。最最要紧记的是千万不能对不是由你分配的内存调用delete。当我们的
指针离开我们的视线范围时,要先给他来个包装,比如,我们需要把自己写的一个指针传给由别人写的一个模
块,你不知道这个模块里面是怎么样的,对你来说这是个黑盒子,把我们的指针不加保护就放进去会很危险,
所以先包装我们的指针,使得只能对指针使用*操作和->操,但不能delete,也不能修改。当指针从地狱出来
后,可以解包装,又变回我们原来的指针,又可以使用各种操作。最后要注意在delete之后置零。可以通过
写两个模板函数来操作。


回到我们的题目B情况:
因为str是静态局部变量,静态数据都是在全局数据区中分配内存空间的。接着调用strcpy函数,把
字符串“I love C++!"填入上面分配的内存中。接着把这段全局数据区中的内存返回。所以str输出
是OK的。

最后看看情况C:
C:

char* MyStrcpy(char* &p)            //这里已修改。另请相应修改程序头处该函数的声明。
{   
    char *str = "I lvoe C++!";      //这里已修改。

    //strcpy(str, "I lvoe C++!");   //这里已修改

    p = str;
   
    return str;
}

答案是str输出和p输出都OK。
通过上面的论讨,我们知道不可以返回栈内存,但可以返回堆内存,可以返回静态内存。现在的情况下
返回的str是那个区的内存呢?

C++中规定:凡是用一对两引号括起来的字符除了用来初始化字符数组的之外都视为字符串常量。

比如:
void func()
{
    char str1[12] = "I love C++!"; //这里不是字符串常量,相当于char str1[12] = { 'I', ' ',
                                   // 'l', 'o', 'v', 'e', ' ', 'C', '+', '+', '!', '/0'};
    char* str2 = "I love C++!";   //这里是字符串常量。

}

我们知道常量是在全局数据区中分配的内存。所以编译器在编译时看到:char *str = "I love C++!";
首先在全局数据区分配12个字节的内存,并且依次填入字符'I', ' ', 'l', 'o', 'v', 'e', ' ',
'C', '+', '+', '!', '/0',并把这段内存的首地址赋给字符串"I love C++!",当调用MyStrcpy函数时
首先分配变量p的空间,接着把实参p的地址(因为这里传递的是字符指针的引用)copy给形参p,之后对
形参p的一切操作都是针对实参p。接着分配指针变量str的空间,并用“I love C++!"初始化。刚才说了
"I love C++!"代表的是全局数据区中的一段内存的首地址。所以实际上是把全局数据区中的一段内存
的首地址放在str的内存空间中。接着把str的值copy一份给形参p,刚才说了给开参p的操作就是对实参
p的操作。所以main函数中的p也指向了全局数据区中的内存地址。最后把str返回,就是把str的值再
copy一份给临时变量temp,temp也指向了全局数据区,所以cout<< MyStrcpy(p) << endl;相当于
cout << temp << endl;而temp指向全局数据区。所以OK!

把程序写成直接return "I love C++!";也是可以的,别忘了字符串常量代表的是全局数据区中的
一段内存的首地址。

最后说道思考题:

#include <iostream>
using namespace std;

void func(int *p)
{
    cout << p << endl;

    delete p;

}

int main
{
    int i = 0;
    int *p = &i;

    func(p);

    delete p;    //这里会不会出错?

    p = NULL;

    return 0;
}

 

0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:4252次
    • 积分:82
    • 等级:
    • 排名:千里之外
    • 原创:5篇
    • 转载:0篇
    • 译文:0篇
    • 评论:0条
    文章存档