前排提醒:本文不适合初学者观看
目录
更加详细地了解类类型的默认函数
默认析构函数
默认构造函数
默认拷贝构造
复制赋值运算符
移动构造和移动赋值函数(C++11)
总结
不要继承任何STL容器
不要使用异常规范
不要在构造和析构中调用虚函数
熟悉c艹的人知道, 如果我们声明一个类类型(class, struct,Union),即使什么都不写,编译器也会为你声明很多东西,比如我们下面的代码并不会报错:
using namespace std;
class nums
{
};
int main()
{
nums n1;
nums n2(n1);
nums n3;
n3 = n1;
n3.~nums();
nums n4=move(n2);
system("pause");
}
也就是说,析构,拷贝构造,移动构造,赋值操作(包括移动赋值)和构造函数都已经写好了,而且毫无以为肯定是public的,而且默认inline,如果我们不进行对应的操作比如创建复制,编译器不会自动构造这些函数,只有在后面被使用到了才会被构建。
我们一个一个讲:
值得注意的是有几点:
例子1 无法访问析构:
using namespace std;
class nums {
private:
~nums()=default;
};
class nums1 :public nums {
};
int main() {
nums1 n3; //error
n3.~nums1();//error
system("pause");
}
例子2 析构弃置
using namespace std;
class nums{
public:
~nums() = delete;
};
class nums1:public nums {
};
int main(){
nums1 n3; //error
n3.~nums1();//error
system("pause");
}
值得注意的有几点:
例子1 构造无法访问
using namespace std;
class nums {
private:
nums()=default;
};
class nums1 :public nums {
};
int main() {
nums1 n3; //error
system("pause");
}
例子2 构造弃置
using namespace std;
class nums {
public:
nums() = delete;
};
class nums1 :public nums {
};
int main() {
nums1 n3; //error
system("pause");
}
例子3 析构弃置
using namespace std;
class nums {
public:
~nums()=delete;
};
class nums1 :public nums {
};
int main() {
nums1 n3; //error
system("pause");
}
例子4 析构无法访问
using namespace std;
class nums {
private:
~nums()=default;
};
class nums1 :public nums {
};
int main() {
nums1 n3; //error
system("pause");
}
例子5 右值引用
class upper
{
int&& num;
};
int main()
{
upper b1;//error
system("pause");
}
例子6 只定义拷贝构造,这个时候只是没有默认构造,定义后可以正常使用
#include
using namespace std;
class nums{
public:
int z = 0;
nums(const nums& n)
{
z = n.z;
}
};
int main() {
nums n; //error
system("pause");
}
例7 默认被删除使用有参构造
#include
using namespace std;
class nums {
public:
nums(const int n) { cout << n << endl; };
nums() = delete;
};
class nums1 :public nums {
public:
nums1(const int z):nums(z) {};
};
int main() {
nums1 n3(1);
n3.~nums1();
system("pause");
}
值得注意的几点:
T f() {
return T();
}
f(); // 仅调用一次 T 的默认构造函数
当初始化器表达式与变量类型为同一类型的纯右值时(C++17) T x = T(T(f())); // 仅调用一次 T 的默认构造函数以初始化 x
例1 基类拷贝构造被弃置
using namespace std;
class base
{
public:
base() {};
base(const base&) = delete;
};
class upper:public base
{
public:
};
int main()
{
upper b1;
upper b2(b1);//error
system("pause");
}
例2 右值引用的成员
class upper
{
public:
upper(int&& temp):num(forward(temp)) {};
int&& num;
};
int main()
{
upper b1(1);
upper b2(b1);//error
system("pause");
}
例子3 有无法复制的成员
using namespace std;
class base
{
public:
base() {};
base(const base&) = delete;
};
class upper
{
base b1;
public:
};
int main()
{
upper b1;
upper b2(b1);//error
system("pause");
}
例4 默认被删除调用其他拷贝构造
using namespace std;
class nums {
public:
nums() {};
nums(const nums& n,const int z) {
cout << z << endl;
};
nums(const nums& n) = delete;
};
int main() {
nums n1;
nums n2(n1,1);
system("pause");
}
值得注意的几点:
例1 移动赋值被定义
using namespace std;
class nums1{
public:
nums1 operator =(const nums1&& n) {
cout << 1 << endl;
return {};
};
};
int main() {
nums1 n1;
nums1 n2;
n2 = n1;//error
system("pause");
}
例2 有const成员变量
using namespace std;
class nums1{
public:
const int num = 1;
};
int main() {
nums1 n1;
nums1 n2;
n2 = n1;//error
system("pause");
}
例3 无法复制赋值的基类
using namespace std;
class nums {
private:
nums operator =(const nums& n) { return n};
};
class nums1 :public nums {
public:
};
int main() {
nums1 n1;
nums1 n2;
n2 = n1; //error
system("pause");
}
例4 因为析构无法赋值
using namespace std;
class nums{
const int n = 0;
public:
~nums() {};
};
int main() {
nums n;
nums n2;
n2 = n;//error
system("pause");
}
编译器将默认声明一个移动构造函数,作为其类的非 explicit 的 inline public 成员,签名为 T::T(T&&)。 或者一个移动赋值运算符:作为其类的 inline public 成员,并拥有签名 T& T::operator=(T&&)。
弃置和平凡的定义与对应的复制版本一致,不再赘述,加上一点:
移动赋值函数,如果等号右边是右值,会优先使用移动复制,否则也会用拷贝赋值,因此就算我们没有移动赋值,编译器同样不会报错。
例子1 声明移动构造后调用移动赋值
using namespace std;
class nums{
public:
nums(const nums&& n) {}
nums() {};
};
int main() {
nums n;
nums n2;
n2= move(n);//error
system("pause");
}
委员会真的有大病,难怪没人跟他们玩
相信各位对虚析构都很熟悉,它的作用也都知道,比如我们在使用工厂模式进行软件开发时候对内存泄漏有很好的防范作用。与纯虚析构的区别在与能不能实例化(instance)
但是遗憾的是c++中STL容器几乎都不是用虚析构的容器,比如我们常用的string,我们看一下源码:
异常规范是在程序后写一个throw表明希望抛出什么类型的错误,比如这样:
void func()throw(char*, exception) {
throw 100;
cout << "[1]This statement will not be executed." << endl;
}
这表明只能抛出string类型的exception,但是实际上如果我们运行下面代码:
using namespace std;
void f()throw(string, exception) {
throw 100;
}
int main() {
try {
f();
}
catch (int) {
cout << "22222222222" << endl;
}
return 0;
}
结果表明我们抛出了int类型的错误
这是在98版本中新增的功能,在C++11后被抛弃,但是并不会有语法错误。
此外我们注意到string的析构带有关键字noexcept,我们在定义析构的时候也建议使用,其实主要目的在于确保析构不会抛出异常,因为比如我们定义了一连串的存放某种类的数组,在delete的时候,如果每个析构都出问题,那么一连串的异常会导致不明确行为。
因此我们最好在析构中捕捉异常,不让其传播。
这一点很好理解,比如子类调用构造函数,父类构造函数调用一个虚函数,那么我们生成子类的时候就调用了父类定义的虚函数而不是子类的。解决方法就是用参数传递的方式讲子类需要自定义的东西传入给父类的非虚函数中,或者用placeholder。