C++
确实麻烦,我们要小心翼翼的躲着她的各种各样的坑。
最可怕的事情,莫过于C++
编译器在背后为我们干了一些事情,但是我们却不知道她究竟干了一些啥,所以了解她的一些默认的行为,对编写高质量的代码,还是很有裨益的。
我的这篇文章,算是一个总结吧,虽然我已经了解这些事情几遍了,但是几次不看,几次不写,这些个玩意忘得很快,所以,我记下这些个知识点,算是填一个坑吧。
C++默认给我们的类添加了什么函数?
我们从一个例子开始。
#include <iostream>
using namespace std;
class CTest
{
private:
int _num;
};
int main()
{
CTest a; // ok,调用无参的构造函数
CTest b(a); // ok,调用默认的拷贝构造函数
CTest c; // ok,调用默认的无参的构造函数
c = a; // ok,调用默认的赋值函数
// c = 3; // no,这个是行不通的
system("pause");
return 0;
}
从上面的例子中可以看到,一个几乎什么函数都没有写的类CTest
,我们在main
函数中写的代码也可以通过编译,这说明C++
默认帮我们干了一些事情。
首先,它添加了一个默认的无参的构造函数:
CTest::CTest()
{
...
}
当然,在这个类里,这个函数什么也没有干,然后它添加一个默认的拷贝构造函数:
CTest::CTest(const CTest &rhs)
{
...
}
当然,它干了一些事情,基本上是将rhs
中的各个成员的值赋给调用这个函数的实例。当然,这不是这篇文章的一个重点。
它肯定添加了一个析构函数:
CTest::~ CTest()
{
...
}
同样的,在这个类里,这个析构函数也什么都没有干。
然后,编译器为我们默认添加了一个赋值函数,大概长这个样子:
CTest& CTest::operator=(const operator &rhs)
{
...
}
这个赋值函数干的事情,和上面的拷贝构造函数类似,当然,这里可能涉及到深拷贝以及浅拷贝的问题,这里也不可能深入了。
如果我们添加了自己的构造函数,那么编译器不会再为我们添加默认的无参构造函数
好吧,我们继续测试:
#include <iostream>
using namespace std;
class CTest
{
public:
CTest(const CTest &arg);
private:
int _num;
};
CTest::CTest(const CTest &arg)
{
}
int main()
{
//CTest a; // no, 一旦我们添加了自己的构造函数,编译器便不为我们添加默认的构造函数了
system("pause");
return 0;
}
上面注释掉的CTest a;
是通不过编译的,因为一旦我们添加了自己的构造函数,编译器便不为我们添加默认的构造函数了。
当然,如果我们没有书写自己的拷贝构造函数,编译器还是会替我们自动添加的。我们继续看:
#include <iostream>
using namespace std;
class CTest
{
public:
//CTest(const CTest &arg);
CTest() {};
private:
int _num;
};
int main()
{
CTest a; // ok, 调用无参的构造函数
CTest b(a); // ok,但是令我吃惊的是,居然还保有拷贝构造函数
system("pause");
return 0;
}
我们继续:
#include <iostream>
using namespace std;
class CTest
{
public:
CTest(const CTest &arg)
{
cout << "调用拷贝构造函数" << endl;
this->_num = arg._num;
}
CTest()
{
cout << "调用无参的构造函数" << endl;
_num = 0;
}
private:
int _num;
};
int main()
{
CTest a; // ok, 调用无参的构造函数
CTest b(a); // ok,但是令我吃惊的是,居然还保有拷贝构造函数
CTest c = a; // ok,调用拷贝构造函数
// 等价于 CTest c(a);
system("pause");
return 0;
}
输出的结果如下:
调用无参的构造函数
调用拷贝构造函数
调用拷贝构造函数
关于CTest c = a;
,居然在调用拷贝构造函数,我知道你很难受,什么鬼,好吧,恭喜你,踩到了C++
的坑了。
用正式一点的话讲,是这样的:
在
C++
中, 如果的构造函数只有一个参数时, 那么在编译的时候就会有一个缺省的转换操作:将该构造函数对应数据类型的数据转换为该类对象.
你还可以看到更坑爹的呢:
#include <iostream>
using namespace std;
class CTest
{
public:
CTest(const CTest &arg)
{
cout << "调用拷贝构造函数" << endl;
this->_num = arg._num;
}
CTest()
{
cout << "调用无参的构造函数" << endl;
_num = 0;
}
CTest(int num)
{
cout << "调用int参数的构造函数" << endl;
_num = num;
}
private:
int _num;
};
int main()
{
CTest a; // ok, 调用无参的构造函数
CTest b(a); // ok,但是令我吃惊的是,居然还保有拷贝构造函数
CTest c = 3; // ok
system("pause");
return 0;
}
输出的结果是这样的:
调用无参的构造函数
调用拷贝构造函数
调用int参数的构造函数
好吧,看到了没有,CTest c = 3;
更加坑爹,好吧,其实这个句子等价于:
CTest c(3);
我们继续来看赋值函数:
#include <iostream>
using namespace std;
class CTest
{
public:
CTest(const CTest &arg)
{
cout << "调用拷贝构造函数" << endl;
this->_num = arg._num;
}
CTest()
{
cout << "调用无参的构造函数" << endl;
_num = 0;
}
CTest(int num)
{
cout << "调用int参数的构造函数" << endl;
_num = num;
}
~CTest()
{
cout << "正在调用析构函数" << endl;
}
private:
int _num;
};
int main()
{
CTest a; // ok, 调用无参的构造函数
CTest b(a); // ok,但是令我吃惊的是,居然还保有拷贝构造函数
CTest c = 3; // ok
a = 3; // ok,坑爹呢,这是,3又不是CTest类型的,为什么能够赋值给a?
//a = "ko"; // no,无法赋值
system("pause");
return 0;
}
我们来看一下结果:
调用无参的构造函数
调用拷贝构造函数
调用int参数的构造函数
调用int参数的构造函数
正在调用析构函数
恭喜你看到了更加坑爹的,我想说的是,上面的a = 3;
这一句,其实可以等价于下面的一堆语句:
CTest temp = new CTest(3);
a = temp; // 调用默认的赋值函数
delete temp; // 调用析构函数
至于上面的a = "ko";
,编译是通不过的,因为CTest
没有接收char*
类型的构造函数,所以"ko"无法转化为CTest
类型。也就无法调用默认的赋值函数。
有的时候,你或许会奇怪,如果我重载了=
号,使其能够处理CTest = int
类型呢?好吧,我们就来看一下吧:
#include <iostream>
using namespace std;
class CTest
{
public:
CTest(const CTest &arg)
{
cout << "调用拷贝构造函数" << endl;
this->_num = arg._num;
}
void operator=(int k)
{
cout << "调用重载的赋值函数CTest = int" << endl;
_num = k;
}
CTest()
{
cout << "调用无参的构造函数" << endl;
_num = 0;
}
CTest(int num)
{
cout << "调用int参数的构造函数" << endl;
_num = num;
}
~CTest()
{
cout << "正在调用析构函数" << endl;
}
private:
int _num;
};
int main()
{
CTest a; // ok, 调用无参的构造函数
CTest b(a); // ok,但是令我吃惊的是,居然还保有拷贝构造函数
CTest c = 3; // ok
a = 3; // ok,坑爹呢,这是,3又不是CTest类型的,为什么能够赋值给a?
system("pause");
return 0;
}
我们看一下运行的结果:
调用无参的构造函数
调用拷贝构造函数
调用int参数的构造函数
调用重载的赋值函数CTest = int
可以看得到,如果我们重载了CTest
与int
类型的=
操作,那么编译器便按照我们的意愿。
人民的救星:explicit
你不告诉我的话,我怎么知道你不想要呢?
话说,C++
的语义真是繁杂,有的时候,我们肯定会厌恶编译器在背后干的恶心的事情,所以,C++
变得更加复杂了,它提供了explicit
关键字。
然后,我们来看一看这玩意的作用吧:
#include <iostream>
using namespace std;
class CTest
{
public:
explicit CTest(const CTest &arg)
{
cout << "调用拷贝构造函数" << endl;
this->_num = arg._num;
}
CTest()
{
cout << "调用无参的构造函数" << endl;
_num = 0;
}
CTest(int num)
{
cout << "调用int参数的构造函数" << endl;
_num = num;
}
~CTest()
{
cout << "正在调用析构函数" << endl;
}
private:
int _num;
};
int main()
{
CTest a; // ok, 调用无参的构造函数
CTest b(a); // ok,但是令我吃惊的是,居然还保有拷贝构造函数
CTest c = 3; // ok,这个语句依然可以编译通过,因为CTest的带int行的构造函数上没有添加explicit
//CTest d = a; // 在拷贝构造函数上不添加explicit时,这个语句是可以编译通过的
// 然而在拷贝构造函数上添加了explicit后,这种行为被禁止了。
system("pause");
return 0;
}
好吧,看到explicit
关键字的用处了没有,我们可以防止编译器的傻逼行为。
好,我们继续为CTest
的带int
型参数的构造函数上添加上explicit
:
#include <iostream>
using namespace std;
class CTest
{
public:
explicit CTest(const CTest &arg)
{
cout << "调用拷贝构造函数" << endl;
this->_num = arg._num;
}
CTest()
{
cout << "调用无参的构造函数" << endl;
_num = 0;
}
explicit CTest(int num)
{
cout << "调用int参数的构造函数" << endl;
_num = num;
}
~CTest()
{
cout << "正在调用析构函数" << endl;
}
private:
int _num;
};
int main()
{
CTest a; // ok, 调用无参的构造函数
CTest b(a); // ok,但是令我吃惊的是,居然还保有拷贝构造函数
//CTest c = 3; // 这个语句编译通不过了,因为CTest的带int型参数的构造函数上添加了explicit
//CTest d = a; // 在拷贝构造函数上不添加explicit时,这个语句是可以编译通过的
// 然而在拷贝构造函数上添加了explicit后,这种行为被禁止了。
system("pause");
return 0;
}
是的,如果你很讨厌编译器的自以为是,所以,请多多使用explicit
关键字。我下面详细说一下explicit
的用法:
explicit
关键字的作用就是防止类构造函数的隐式自动转换。
一般而言,explicit
关键字只对有一个参数的类构造函数有效, 如果类构造函数参数大于或等于两个时, 是不会产生隐式转换的, 所以explicit
关键字也就无效了. 例如:
CTest::CTest(int a, int b)
{
...
}
当然,凡事都有例外,那就是当除了第一个参数以外的其他参数都有默认值的时候, explicit
关键字依然有效, 此时, 当调用构造函数时只传入一个参数, 等效于只有一个参数的类构造函数, 例子如下:
CTest::CTest(int a, int b = 2)
{
...
}