cpp类对象作函数参数
cpp类对象作函数参数
参考:c++ Prime Plus(第六版) 第12章
传址or传值
函数传参有几种:指针、引用、值;前两个都是传入地址,最后一个传入对象的值
由于指针操作数据的灵活性高,且指针指的是一个地址,极容易直接修改这个地址里存储的数据,所以出现了引用(指针的语法糖)
引用的好处:
- 可以修改调用调用函数中的数据对象(在使用指针时,我们能加const就会加上,以免误操作)
- 传递的是引用,不是整个数据对象(提高层序运行速度)
为什么不能用传值代替传入const指针或者引用?
传值对于函数,它只能操作这个值,不能修改原数据;由此联想,我们传递一个值时可能会让程序更快的运行。但实际上传值并不安全,或者说容易在意想不到的地方挖坑,同时,在传递一个比较大类对象时,它的速度并不快
这里创建了一个test类,让我们在类外函数调用类对象看看会出现什么问题:
#include<string.h>
#include<iostream>
class test {
private:
char* str;
int len;
public:
test(const char* s);
~test();
friend std::ostream& operator <<(std::ostream& os, const test& one);
};
test::test(const char* s) {
len = strlen(s);
str = new char[len + 1]();// 这里需要有()!否则会有想不到的奇怪问题
strcpy_s(str,len+1, s);
}
test::~test() {
std::cout << "goodbye," << str << std::endl;
// 避免重复调用构造函数
if (str) {
delete[] str;
str = nullptr;
}
}
std::ostream& operator<<(std::ostream& os, const test& one) {
os << one.str;
return os;
}
void callme(test& rsb) {
std::cout << "wanna test 传址:" << rsb << "\n";
}
void callme2(test sb) {
std::cout << "wanna test 传值:" << sb << "\n";
}
int main() {
test t1("I'm t1");
test t2("俺是t2");
// test t3 = t2;
callme(t1);
callme2(t2);
std::cout << "end" << std::endl;
// system("pause");
return 0;
}
让我们执行一下,如果你也用的是VS,那么你应该也会跳出这个页面:
最主要的原因应该是内存管理出现了问题,野指针、溢出等等,而不是编译器出了问题
虽然编译器在这里下了断点停止,但我们依旧能查看输出:
可以看到在我们传值后,程序输出了一堆乱码(文本取决于内存的残留)
似乎传值并没有什么问题,它可以做到正常输出。但真的可以吗?事实上这个输出并不稳定,会随着编译器的改变收到影响;编译器运行时,无法解析传给callme2的类对象,函数也不能识别其原始字符串
这里可以参考c++ Prime Plus第351页内容
深拷贝和浅拷贝
在向callme2函数中传值时,我们原意是传入t2的str值,但是直接传值在cpp里是浅拷贝!
- 浅拷贝:单纯copy了指针的信息;副本指向了原数据的地址
- 深拷贝:copy数据的同时还拷贝了数据的结构;创建了一块新的地址存储了副本数据
同时,在如果我们去掉析构函数中这一段内容,我们还会发现对于t2,析构函数调用了两次
if(str){
delete[] str;
str =nullptr;
}
在callme2函数浅拷贝t2后,t2的生命周期将等同于callme2的函数周期(浅拷贝时副本指向了原有的t2地址,callme函数中的t2又仅为一个局部变量),函数结束时将调用一次析构函数,而main函数结尾时又将调用一次t2的析构函数,而在callme结束时,t2已经被删掉了,此时就会输出乱码
类对象作参
接着我们上面的代码,添加一个t3对象,调用callme函数看看
// 我们刚才用到的函数
class test{
private:
char* str;
int len;
public:
test(const char* s);
~test();
friend std::ostream& operator <<(std::ostream& os, const test& one);
}
void callme(test& rsb);
// 创建的对象
test t1("I'm t1");
test t2("I'm t2");
test t3 = t2; // t3由t2得到
callme(t3);
这里我们将使用t2来初始化t3,同样出现了乱码,说明我们这里仍为浅拷贝,t2和t3分别调用了同一地址的析构函数,在t3先一步消除了t2的结构后,t2的指针此时成为了野指针。这也就是指针悬挂问题。
我为什么说是先删除t3再删除t2:自动存储对象被删的循序与创建顺序相反(栈是先进后出嘛)
复制构造函数
在使用上述语句时,我们都是在用隐式复制构造函数(它是按值进行复制的)
这些问题,都可以通过定义显示复制构造函数解决:它可以copy一个类对象的副本到新的对象中。
test::test(const test& t){
len = strlen(t.str)+1;
str = new char [len]();
strcpy_s(str, len, t.str);
}
可以用以下几种方式创建t3:
test t3(t2);
test t3 = t2;
test t3 = test(t2);
test *t3 = new test(t2);
END
注:笔者才疏学浅,如有错误,请指教!