问题来源
在学习QT的过程中,看到有些内置的QT库中存在一些返回引用的方法,例如:
QByteArray &QByteArray::append(const char *str, int len);
QByteArray &QByteArray::append(char ch);
等等,所以有点好奇,返回值与返回引用之间有什么区别呢?遂有了这篇文章。
在写验证程序的时候又对c++的构造函数、拷贝构造函数、赋值构造函数有了新的理解,文章末会放出完整的测试代码。
第一步,我们需要一个类,给他起个名字,就叫A吧。
class A {
public:
A(int i = 0) : mi(i) { qDebug() << this << "A 对象通过构造函数创建" << mi; }
A(const A &y) {
qDebug() << this << "A 对象通过拷贝构造函数创建" << mi <<"被拷贝的对象是:"<<&y;
}
~A() { qDebug() << this << "~A()" << mi; }
void run() { qDebug() << this << "run()" << mi; }
public:
int mi;
};
这个A类,我们重写了它的构造函数、拷贝构造函数、析构函数以便使我们可以观测到程序的执行步骤。
第二步,我们还需要几个返回A的方法:
static A newA(A &a) {
//其作为
A aa(1);
return aa;
};
static A newA2(A &a) {
A aa(2);
return a;
};
static A &newA3(A &a) {
A aa(3);
return aa;
}
第三步,开始测试
int main(int argc, char *argv[]) {
QCoreApplication a(argc, argv);
{
A aaa(33);
A a4 = newA(aaa);
// A a4 = newA2(aaa);
// A a4 = newA3(aaa);
a4.run();
qDebug() << "a4.run();";
}
return a.exec();
}
上面程序的执行结果为:
0x65fe9c A 对象通过构造函数创建 33
0x65fe98 A 对象通过构造函数创建 1
0x65fe98 run() 1
a4.run();
0x65fe98 ~A() 1
0x65fe9c ~A() 33
aaa通过引用传递的方式传入newA方法,这个过程不会发生拷贝构造,然后newA方法内,A aa(1)
;语句通过构造函数创建了一个对象aa,然后它作为返回值返回,并且aa对象的生存周期延长至程序结束。
- 接着修改main函数,改为调用newA2方法。运行结果如下:
0x65fe9c A 对象通过构造函数创建 33
0x65fe6c A 对象通过构造函数创建 2
0x65fe98 A 对象通过拷贝构造函数创建 6684520 被拷贝的对象是: 0x65fe9c
0x65fe6c ~A() 2
0x65fe98 run() 6684520
a4.run();
0x65fe98 ~A() 6684520
0x65fe9c ~A() 33
newA2方法与newA方法唯一的不同便是返回的值不同,newA2方法返回的不是在newA2方法内创建的局部变量,而是传入的实参。
因此程序运行的结果在这发生了不同,在newA2方法的最后,调用拷贝构造函数构造了一个新的对象,这个新的对象作为返回值返回。
我们可以看到执行run()
方法时,mi值变为了6684520而不是33,这是因为我们重载了拷贝构造函数,并且啥也不干,如果我们不重载拷贝构造函数的话,mi的值会是33。不信你试试/滑稽。我不知道这是为啥,改天再研究下。
- 继续修改main函数,改为调用newA3方法。运行结果如下:
0x65fe9c A 对象通过构造函数创建 33
0x65fe6c A 对象通过构造函数创建 3
0x65fe6c ~A() 3
0x65fe98 A 对象通过拷贝构造函数创建 6684520 被拷贝的对象是: 0x0
0x65fe98 run() 6684520
a4.run();
0x65fe98 ~A() 6684520
0x65fe9c ~A() 33
这段输出乍看上去跟上面那个很相似,但是。。。。。
输出的不同点在于输出的第三、四行。调用newA3方法,是先析构局部变量aa,然后再拷贝构造实参aa。这个顺序就有问题了啊。已经被析构的局部变量,如何才能作为拷贝构造方法的参数呢? 虽然程序正常运行了,但是存在访问非法内存的风险。
下面放出完整的测试代码
#include <QCoreApplication>
#include <QDataStream>
#include <QtDebug>
class A {
public:
A(int i = 0) : mi(i) { qDebug() << this << "A 对象通过构造函数创建" << mi; }
//拷贝构造,一般在传参时使用,或者是返回非引用(仅限返回值为引用传参),或者是
A(const A &y) {
qDebug() << this << "A 对象通过拷贝构造函数创建" << mi << "被拷贝的对象是:"
<< &y << y.mi;
}
~A() { qDebug() << this << "~A()" << mi; }
void run() { qDebug() << this << "run()" << mi; }
//赋值构造函数
A &operator=(const A &other) {
qDebug() << "=赋值构造函数" << this << mi << &other << other.mi;
this->mi = other.mi;
return *this;
}
public:
int mi;
};
//若是实参以值传递方式传入,那么会调用拷贝构造函数构造a。
static A newA(A &a) {
//其作为
A aa(1);
return aa;
};
//以值传递方式返回,那么会调用拷贝构造函数构造一个返回值。
//若是实参以值传递方式传入,那么还会调用拷贝构造函数构造a,a的析构发生在上行拷贝构造之后。
static A newA2(A &a) {
//其作为
A aa(2);
return a;
};
//返回引用时,会调用拷贝构造函数构造出一个返回值,不同于newA2,拷贝构造发生在
// A aa(3)对象析构之后。!!!!!!该问题导致,被拷贝的对象已经被析构,导致程序会出现漏洞。
//实参为值传递时,仍会调用拷贝构造函数,但是该对象在拷贝构造之后才会析构
static A &newA3(A &a) {
A aa(3);
return aa;
}
//实参为值传递时,输出变得很奇怪
static A &newA4(A &a) {
A aa(4);
return a;
}
int main(int argc, char *argv[]) {
QCoreApplication a(argc, argv);
{
A aaa(33);
//调用拷贝构造函数
// A aa = aaa;
//
// A aa = newA(aaa);
//调用拷贝构造函数
// A aa = newA2(aaa);
//调用拷贝构造函数
// A aa = newA3(aaa);
//调用拷贝构造函数
A aa = newA4(aaa);
// aa = newA(aaa);
// aa = newA2(aaa);
// aa = newA3(aaa);//调用该方法,程序会gg
// aa = newA4(aaa);
// aa = newA2();
aa.run();
qDebug() << "aa.run();";
}
return a.exec();
}
刚开始学qt、c++,所以也没考虑编译器会不会优化代码啊之类的,如果有错误的地方,恳请大家评论指出。
第一次自己写,自己分析,自己研究,这不到百行的代码折腾了一下午,还没弄明白。c++果然好难。c#真的好好用,这些都不用自己操心。