三/五法则

当定义一个类时,我们显式地或隐式地指定了此类型的对象在拷贝、赋值和销毁时做什么。一个类通过定义三种特殊的成员函数来控制这些操作:拷贝构造函数拷贝赋值运算符析构函数

  1. 拷贝构造函数定义了当用同类型的另一个对象初始化新对象时做什么
  2. 拷贝赋值运算符定义了将一个对象赋予同类型的另一个对象时做什么
  3. 析构函数定义了此类型的对象销毁时做什么。

我们将这些操作称为拷贝控制操作

由于拷贝控制操作是由三个特殊的成员函数来完成的,所以我们称此为“C++三法则”。在较新的 C++11 标准中,为了支持移动语义,又增加了移动构造函数和移动赋值运算符,这样共有五个特殊的成员函数,所以又称为“C++五法则”。

也就是说,“三法则”是针对较旧的 C++89 标准说的,“五法则”是针对较新的 C++11 标准说的。
为了统一称呼,后来人们把它叫做“C++ 三/五法则”。

法则一、需要析构函数的类也需要拷贝构造和拷贝赋值函数”

从“需要析构函数”可知,类中必然出现了指针类型的成员(否则不需要我们写析构函数,默认的析构函数就够了),所以,我们需要自己写析构函数来释放给指针所分配的内存来防止内存泄漏。

那么为什么说也需要拷贝构造和拷贝赋值函数呢?

原因是:类中出现了指针类型的成员,必须防止浅拷贝问题。所以需要自己书写拷贝构造函数和拷贝赋值运算符,而不能使用默认的拷贝构造函数和默认的拷贝赋值运算符。

法则二、需要拷贝操作的类也需要赋值操作,反之亦然”

为啥?因为拷贝赋值操作实际上是 析构+拷贝 所以拷贝需要做的,赋值也需要做同样的事

法则三、析构函数不能是删除的成员

如果析构函数是删除的(即析构函数被声明为= delete),那么无法销毁此类型的对象。
对于一个删除了析构函数的类型,编译器不允许定义该类型的变量或创建该类的临时对象。而且,如果一个类有某个成员的类型删除了析构函数,我们也不能定义该类的变量或临时对象。

在需要特殊设计的情况下,可以把析构函数设为private的。此时一定要提供一个static destroy函数,否则将造成内存泄漏。但是这样需要手动管理资源,其实也是非常不推荐的。

为什么我们需要三五法则?

三/五法则的核心是确保资源的安全管理生命周期可控。若析构函数不可用(删除或私有):

  • 拷贝和移动操作失效:即使定义了拷贝构造函数或移动构造函数,若无法销毁对象,资源最终仍会泄漏。
  • 破坏 RAII 机制:C++ 依赖析构函数自动释放资源,手动管理易出错。

现在,我们应当怎么做?

随着时间的流逝,C++语言也在不断成长,在C++11标准下,我们最好:

  • 优先使用智能指针和容器,减少手动资源管理。
  • 若必须手动管理资源,严格遵循三/五法则,确保析构、拷贝、移动操作正确实现。

constexpr和const的区别

这样想:

C++的const关键字一直以来都有双重语义问题,也就是变量的“Readonly”和“No Change”两个语义

C++11为了解决这个问题,提出了constexpr关键字,让它来强调“No Change”这个语义

const只强调“只读”语义。

在const中,“只读”不就意味着其不能被修改吗?答案是否定的,“只读”和“不允许被修改”之间并没有必然的联系

1
2
3
4
5
int x = 42;
const int & con_x = x;
cout << con_x << endl; // 输出42
x = 43;
cout << con_x << endl; //输出43

在这个例子中,con_x是const修饰的“只读”引用变量,但是这并不代表con_x的“值”不会被改变,只是表示不可以通过con_x去修改,con_x是“ReadOnly”而已,无法通过变量自身去修改自己的值。但这并不意味着 con_x 的值不能借助其它变量间接改变,通过改变 x 的值就可以使 con_x 的值发生变化。

而使用constexpr就不一样了,constexpr不但强调了“只读”,同时强调了“常量”这一点,也就是它的值永远不会改变。

例:可以使用constexpr修饰的int“变量”(其实已经是常量了)来初始化数组,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
constexpr int sqr1(int arg)
{
return arg*arg;
}

const int sqr2(int arg)
{
return arg*arg;
}

int main()
{
array<int,sqr1(10)> mylist1; //可以,因为sqr1时constexpr函数
array<int,sqr2(10)> mylist1; //不可以,因为sqr2不是constexpr函数
return 0;
}

其中,因为 sqr2() 函数的返回值仅有 const 修饰,而没有用更明确的 constexpr 修饰,导致其无法用于初始化 array 容器(只有常量才能初始化 array 容器),并且编译器其实会在编译阶段就计算数组的大小

decltype关键字

核心特性

  • 非求值上下文:不会对表达式进行实际求值
  • 精确类型推导:保留表达式的值类别(value category)和常量性

基本用法

推导表达式类型

1
2
int i = 4;
decltype(i) a; // 推导结果为int,a的类型为int

与类型别名合用

1
2
3
using size_t = decltype(sizeof(0));    // 获取sizeof操作的标准返回类型
using ptrdiff_t = decltype((int*)0 - (int*)0); // 指针差值类型
using nullptr_t = decltype(nullptr); // 空指针类型

重用匿名类型

1
2
3
4
5
6
struct {  // 匿名结构体
int d;
double b; // 修正拼写错误 doubel → double
} anon_s;

decltype(anon_s) as; // 复用匿名类型定义新变量

泛型编程(C++11起)

1
2
3
4
5
// 配合auto追踪函数返回类型(尾置返回类型)
template<typename T, typename U>
auto add(T t, U u) -> decltype(t + u) {
return t + u;
}

💡 关键应用场景

  1. 元编程:配合SFINAE实现类型萃取
  2. 完美转发:保持值类别的decltype(auto)
  3. 接口适配:自动推导依赖其他类型的复杂返回类型
1
2
3
// 类型萃取示例
template<typename T>
using remove_reference_t = decltype(std::remove_reference<T>::type);

特殊情形处理

表达式类型 decltype推导规则 示例
变量名 变量声明类型 decltype(i) → int
左值表达式 T& decltype((i)) → int&
纯右值表达式 T decltype(1+2) → int
将亡值表达式 T&& decltype(std::move(i)) → int&&

顶层const与底层const

顶层const:指针常量

  • 指针本身是一个常量
  • 指针不可以被修改以指向新的地址
  • 可以通过指针修改所指对象

底层const:常量指针

  • 指向一个常量的指针
  • 可以指向新地址
  • 不可以修改所指对象
1
2
3
4
5
// 语法要点
// 星号在前,顶层const
int *const p = &i
// 星号在后,底层const
const int *const p = &i

(注意:示例代码中i应为已定义变量,实际使用时建议添加变量定义)