c艹进阶编程(2)

前排提醒:本文不适合初学者观看

目录

更加详细地了解类类型的默认函数

默认析构函数

默认构造函数

默认拷贝构造

复制赋值运算符

移动构造和移动赋值函数(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. 若这满足 constexpr 析构函数的要求,则生成的析构函数为 constexpr 。 (C++20 起)
  2. 默认的析构分为弃置析构(=delete)和平凡析构和nontrivial,需要满足一些条件才会针对不同类型的类对象产生对应的析构函数:
    1. 弃置析构:非静态类类型数据成员或基类弃置析构函数,或析构函数不能访问(例如将析构函数设置为私有)
      如果delete掉析构无法初始化任何对象,就算自定义也没用,但是类定义是没任何问题
    2. 平凡析构:当且仅当基类析构非虚,基类为平凡析构,所有成员对象都有平凡析构
      平凡析构函数是不进行任何动作的析构函数。有平凡析构函数的对象不要求 delete 表达式,并可以通过简单地解分配其存储进行释放。
    3. nontrivial,除了弃置和平凡析构
  3. 如果基类或者成员对象有虚析构,那么默认析构也是虚析构

例子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. 若它满足对于 constexpr 构造函数的要求,则生成的构造函数为 constexpr。 (C++11 起)
  2. 默认构造也分为弃置和平凡构造(trivial)和nontrivial:
    1. 弃置构造:非静态类类型数据成员或基类弃置或者无法访问无参构造函数。非静态类类型数据成员或基类弃置或者无法访问析构函数,含有右值引用成员
      这种情况下没办法使用这个类中的非静态对象和函数,就算自定义无参的也没用,但是类定义是没任何问题的
      注意弃置和没有默认构造是两回事,如果人为加上还是没问题的,但是弃置的意思是无论如何都没有。
    2. 平凡构造:当且仅当没有虚类成员,没有虚基类,没有默认初始化器的非静态成员,每个基类和成员都有平凡构造函数。
      平凡默认构造函数是不进行任何动作的构造函数。
    3. nontrivial:除了弃置和平凡

例子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");
}

默认拷贝构造

值得注意的几点:

  1. 若它满足对于 constexpr 构造函数的要求,则生成的复制构造函数为 constexpr。 (C++11 起)
  2. 类可以拥有多个复制构造函数,当存在用户定义的复制构造函数时,用户仍可用关键词 default 强迫编译器生成隐式声明的复制构造函数(C++11)。
  3. 省略复制及移动 (C++11 起)构造函数,导致零复制的按值传递语义。
    当操作数和返回类型是纯右值的时候,(C++17)
    T f() {
        return T();
    }
     
    f(); // 仅调用一次 T 的默认构造函数
    当初始化器表达式与变量类型为同一类型的纯右值时(C++17)
    T x = T(T(f())); // 仅调用一次 T 的默认构造函数以初始化 x
  4. 弃置复制构造:类内有无法复制非静态数据(弃置或者无法访问),或有无法复制直接或者虚基类(弃置或者无法访问),或有被弃置或者无法访问的析构, 或有右值引用类成员(c++11起),因为可以同时有多个复制构造,因此就算是弃置也可以用户自己定义
  5. 平凡复制构造:没有虚基类,没有虚成员函数,每个基类和每个类型的非静态成员的复制构造都是平凡的。非联合类的平凡复制构造函数,效果为复制实参的每个标量子对象(递归地包含子对象的子对象,以此类推),且不进行其他动作。不过不需要复制填充字节,甚至只要其值相同,每个复制的子对象的对象表示也不必相同。
  6. nontrivial复制构造:除了上面两个

例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. 类可以拥有多个复制赋值运算符,如 T& T::operator=(const T&) 和 T& T::operator=(T)。当存在用户定义的复制赋值运算符时,用户仍可用关键词 default 强迫编译器生成隐式声明的复制赋值运算符。 (C++11 )
  2. 若用户自定义移动赋值,则复制复制被弃置
  3. 有const限定的或引用类型的非静态数据成员,被弃置
  4. 有无法复制的非静态成员,直接或者(虚)基类,被弃置
  5. 用户自定义析构,被弃置
  6. 因为可以同时有多个复制赋值,因此就算是弃置也可以用户自己定义
  7. 如果没有虚成员或者虚基类,每个成员和基类的赋值运算符都是平凡的,没有volitile限定的非静态数据成员(C++14),那么它就是平凡的
  8. 否则为nontrivial

例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");
}

移动构造和移动赋值函数(C++11)

编译器将默认声明一个移动构造函数,作为其类的非 explicit 的 inline public 成员,签名为 T::T(T&&)。 或者一个移动赋值运算符:作为其类的 inline public 成员,并拥有签名 T& T::operator=(T&&)。

 弃置和平凡的定义与对应的复制版本一致,不再赘述,加上一点:

  1. 有用户声明的移动赋值(构造)运算符,则移动构造(赋值)被弃置。

移动赋值函数,如果等号右边是右值,会优先使用移动复制,否则也会用拷贝赋值,因此就算我们没有移动赋值,编译器同样不会报错。

例子1 声明移动构造后调用移动赋值

using namespace std;
class nums{
    
public:
    nums(const nums&& n) {}
    nums() {};
};
int main() {
    nums n;
    nums n2;
    n2= move(n);//error
    system("pause");
}

总结

委员会真的有大病,难怪没人跟他们玩

不要继承任何STL容器

相信各位对虚析构都很熟悉,它的作用也都知道,比如我们在使用工厂模式进行软件开发时候对内存泄漏有很好的防范作用。与纯虚析构的区别在与能不能实例化(instance)

但是遗憾的是c++中STL容器几乎都不是用虚析构的容器,比如我们常用的string,我们看一下源码: 

c艹进阶编程(2)_第1张图片

不要使用异常规范

异常规范是在程序后写一个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。

你可能感兴趣的:(c艹新特性集合,开发语言,后端,c++)