C++ 之函数返回局部变量

1. 返回字符串字面量的指针,即存放在常量存储区的数据,不会因为函数调用栈被释放而消失,所以操作可行。

char* const_str()
{
    char* p = "Hello World!"; // 指向常量区字符串的指针
    return p;
}

另外,其实这里写的不规范,C++11 标准要求在字面量指针增加 const,以防止猿们随意改变其内容导致段错误。

 

2. 返回局部变量的 reference 引用

首先,不管是基本数据类型还是对象,返回局部引用都是错误的。返回的别名引用了函数内局部变量,它的数据区是放在栈空间的,在函数调用结束后,栈空间被释放之后就不存在了,其数据可能会被覆盖掉。

2.1 普通变量的引用 

通常编译器会在 return i; 处放出警告,如 xcode Reference to stack memory associated with local variable 'i' returned

打印出的地址是一样的,说明返回的引用并没有真正的重新拷贝复制一份到新的栈空间,即 main 函数栈中,而是直接将它给了引用变量 p。

如果不是引用,则会拷贝一份新的内存,两者地址是不同的。

int& refer()
{
    int i = 99;
    cout << &i << endl; // 0x7ffeefbff4ac
    return i;
}

void main() 
{
    int& p = refer();
    cout << &p << endl; // 0x7ffeefbff4ac
    cout << p << endl; // 32767(???)
}
int refer()
{
    int i = 99;
    cout << &i << endl; // 0x7ffeefbff4ac
    return i;
}

void main() 
{   
    int p = refer();
    cout << &p << endl; // 0x7ffeefbff4e4
    cout << p << endl; // 99
}

2.2 局部对象的引用

自定义了一个 Tools,data 初始化为 "Hello, World"

问:什么情况,A 跟 B 的结果居然不一样?

答:原因在于编译器在碰到 “=” 时在背后调用了默认的拷贝构造函数,即 Tools:Tools(const Tools& rhs);

data 指针也默认来自 rhs,即等号右手边的对象。这时如果把析构函数的注释去掉,运行时就会报错 effectiveC++(6837,0x1000f45c0) malloc: *** error for object 0x1005401d0: pointer being freed was not allocated

 问:是不是真的调用了拷贝构造呢?

答:自己增加该函数 cout 一下便可以清楚看到。

注意⚠️,从这里可以看出:如果对象中包含非普通数据成员变量(int, double...),最好明确给出拷贝构造函数的拷贝方式,保证可靠。

class Tools
{
public:
    const char* data;
    Tools()
    {
        data = new char[20]{'H','e', 'l', 'l', 'o', ','
                            , 'W', 'o', 'r', 'l', 'd'};
    }
    /** 
    ~Tools()
    {
        delete [] data; // release memery
    }
    **/
};

Tools& fun()
{
    Tools tool;
    cout << &tool << endl; // 0x7ffeefbff4b8
    return tool;
}
/**
A. 没有赋值操作
**/
void main()
{
    cout << &fun() << endl; // 0x7ffeefbff4b8
}

/**
B. 有赋值操作
*/
void main()
{
    Tools tool = fun();
    cout << &tool << endl; // 0x7ffeefbff4e0
}

3. 局部对象

这个是本笔记📒要关注的,返回局部对象并非通常所说的拷贝一个临时变量作为返回,即调用拷贝构造函数(不论有无赋值操作)。现代编译器都进行了优化,即 return value optimization (RVO)。编译器遇到这样的函数其实做了不少的更改,以保证最后的运行结果是你预期的,同时避免了拷贝内存这样费时的工作。

首先来看看下面的代码,想想会产生什么样的输出:

class C
{
public:
    C() = default;
    C(const C&)
    {
        cout << "copy C" << endl;
    }
};

C F()
{
    return C();
}
void main()
{
    C c = F();
}

是的,什么都不会输出。问题又来了,如果按照刚刚的解释,栈空间被释放,F 返回的局部对象 C 应该被重新拷贝一份才对,这时会调用拷贝构造函数。如果用不同的编译器,你可能会看到三种不同的情况,这里是一种,下面是另外两种:

copy C
copy C
copy C

原因都在于编译器生成了不同的"优化"代码。

我这里简单写一下大致第三种的优化情况👇

C* F(C* _hiddenAddress)
{
    C local; // local Object in F
    *_hiddenAddress = local;// copy once after assignment
    return _hiddenAddress;
}

void main()
{
    C _c; // 之所以不用指针是因为最后还要 delete _c;
    C c = *F(&_c); // copy twice after return
}

这个优化真的是太棒了!成功拷贝了两次,超乎想象👍

所以后来怎么改的呢,是的,用过库函数那些字符串处理函数的应该清楚,都是外部先声明变量开辟空间,再将地址传入函数体内进行赋值,这样就避免了所有的拷贝操作。于是,上面的调用就被改成这样,对的,F 什么都没干。

void F(C* p)
{
}

void main()
{
    C c;
    F(&c);
}

为了证明这一点,我们可以在 F 中打印地址进行对比,结果是一致的。当然,为了保证程序的正常执行顺序,优化的过程会比这复杂。

C F()
{
    C c;
    cout << "c1: " << &c << endl; // c1: 0x7ffeefbff4e0
    return c;
}

void main()
{
    C c = F();
    cout << "c2: " << &c << endl; // c2: 0x7ffeefbff4e0
}

注意⚠️,还记得刚刚那个析构函数重复释放 data 的错误吗,为了说明返回的 c 并不会因为指向相同的内存区域,被函数返回时释放第二次出错,现在再重新验证。

class C
{
public:
    const char* data;
    C()
    {
        data = new char[20]{'H','e', 'l', 'l', 'o', ','
            , 'W', 'o', 'r', 'l', 'd'};
    }

    C(const C&)
    {
        cout << "copy C" << endl;
    }

    ~C()
    {
        cout << "delete C" << endl;
        delete [] data;
    }
};

C F()
{
    C c;
    return c; // 局部对象 c 并不会被释放
}

void main()
{
    C c = F(); // ~C 也只会被执行一次
    // ~C()
}

4. 返回的是局部对象的指针,但赋值时是对象 Object 类型

这时候就像[3]所讲,要重新拷贝新的内存到等号左边赋值。

同样是上边的例子

class C
{
public:
    C() = default;
    C(const C&)
    {
        cout << "copy C" << endl;
    }
    ~C()
    {
        cout << "delete C" << endl;
    }
};

C* F()
{
    C c;
    return &c; 
    // delete c
}
void main()
{
    C c = *F(); // copy c
    // delete c
}
delete C
copy C
delete C

5. 返回的是局部对象指针,直接赋值指针,不允许❌

原因很清楚,局部对象已经被释放了。

void main()
{
    C *c = F();
    cout << "c 还要用呢" << endl;
}
delete C
c 还要用呢

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章