一. 类的静态成员

有的时候,类需要它的一些成员与类本身直接相关,而不是与类的各个对象保持关联

我们可以通过在成员的声明之前加上关键字 static 使得其与类关联在一起

注意:

  1. 类的静态成员存在于任何对象之外,在编译阶段就分配空间
  2. 静态成员函数也不与任何类绑定在一起,它们不包含 this 指针。因此静态成员函数不能声明为 const,不能在函数体内显式使用 this 指针,也不能调用非静态成员函数或使用非静态数据成员。
  3. 静态数据成员不属于类的任何一个对象,所以它们并不是在创建类的时候定义的,即它们不能由构造函数初始化。通常我们在类的外部定义和初始化每个静态成员。
#include <cstdio>

class Person{
private:
    static int obj_num;

public:
    Person(){
        ++this->obj_num;
    }

public:
    static int getObjNum(void){
        return obj_num;
    }
};

int Person::obj_num = 0;

int main(void){
    printf("Obj_num: %d\n", Person::getObjNum());
    Person me;
    Person you;
    printf("Obj_num: %d\n", Person::getObjNum());
    return 0;
}

静态成员函数只能使用静态数据成员或调用静态成员函数。如果一个类的成员既要实现共享,又要实现不可改变,那就用 static const 修饰。

二. 右值引用

右值引用就是必须绑定到右值的引用。我们通过 && 而不是 & 来获得右值引用

右值引用的特点之一是可以延长右值的生命周期

int main(void){
    int && rvalue_ref = 7;
    printf("%d\n", rvalue_ref);
    return 0;
}

对于字面量 7 可能看不出效果

MyInt Test(void){
    MyInt my_int(7);
    return my_int;
}

int main(void){
    MyInt you_int = Test();
    return 0;
}

上面代码在没有任何优化的情况下发生了 3 次构造,Test 函数中,my_int 发生了一次一般构造;return my_int 使用拷贝构造产生了临时对象;you_int 使用拷贝构造将临时对象复制到自身,最后临时对象被销毁

int main(void){
    MyInt &&rvalue_ref = Test();
    return 0;
}

上述流程在使用了右值引用后发生了微妙的变化,上面代码只发生了 2 次构造,Test 函数中,my_int 发生了一次一般构造;return my_int 使用拷贝构造产生了临时对象;不同的是,rvalue_ref 是一个右值引用,引用的对象是 Test 函数返回的临时对象,因此该对象的生命周期被延长,我们可以自由使用该对象。

三. 移动构造函数

3.1 简介

引入右值引用的主要目的是提高程序的运行效率,当类持有资源时,如动态分配的内存。行为像值的类会以深拷贝的方式复制对象的所有数据,深拷贝往往非常耗时,合理使用右值引用可以避免没有必要的深拷贝操作

编译器会在一些条件下生成移动构造函数,这些包括:

  1. 没有自定义的拷贝构造函数和拷贝赋值运算符
  2. 没有自定义的移动构造函数和移动赋值运算符
  3. 没有自定义的析构函数

虽然这些条件很严苛,但是也不用灰心,因为当你不需要自定义移动构造函数的时候,编译器生成的移动构造函数和自定义或编译器生成的拷贝构造函数并没有什么区别

移动构造函数和移动赋值运算符的参数为自身的右值引用

3.2 自定义移动语义

class MyInt{
private:
    int *int_pointer;

public:
    explicit MyInt(int value){
        this->int_pointer = new int{value};
    }

    MyInt(const MyInt &my_int)
    :int_pointer(new int{*my_int.int_pointer}){
    }


    MyInt &operator=(const MyInt &my_int){
        int *new_int_pointer = new int{*my_int.int_pointer};
        delete this->int_pointer;
        this->int_pointer = new_int_pointer;
        return *this;
    }

    MyInt(MyInt &&my_int) noexcept
    :int_pointer(my_int.int_pointer){
        my_int.int_pointer = nullptr;
    }

    MyInt &operator=(MyInt &&my_int) noexcept{
        if(this != &my_int){
            if(this->int_pointer != nullptr)
                delete this->int_pointer;
            this->int_pointer = my_int.int_pointer;
            my_int.int_pointer = nullptr;
        }
        return *this;
    }

    ~MyInt(){
        if(this->int_pointer != nullptr)
            delete this->int_pointer;
    }

public:
    int getValue(void) const{
        return *(this->int_pointer);
    }
};

在移动构造函数中,并没有拷贝构造中的内存复制,取而代之的是简单的指针替换操作。它将实参对象中的 int_pointer 赋值到当前对象,然后置空实参对象以保证实参对象析构的时候不会影响这片内存的生命周期

可以看到后面两次构造函数变成了移动构造函数,因为这两次操作中源对象都是右值(临时对象),因此编译器优先使用移动构造函数去构造目标对象。当移动构造函数不存在时,编译器会退而求其次地使用拷贝构造函数。移动构造函数使用指针转移的方式构造目标对象,所以整个程序的运行效率得到大幅提升。

3.3 移动语义应该是 noexcept

移动语义通常不会抛出任何异常 , 因为它“窃取”资源,并不分配新资源。当编写一个不抛出异常的移动语义时,我们应该将此事通知标准库,否则它会认为我们的移动语义可能会抛出异常,并且为了处理这种可能性而做一些额外工作

一种通知标准库的方法是在我们的移动语义中指明 noexcept , 它让我们承诺这个函数不会抛出异常

四. 将左值转换为右值

std::move 可以将左值转换为右值

int main(void){
    MyInt my_int(5);
    MyInt your_int(my_int);
    MyInt her_int(std::move(my_int));
    return 0;
}

std::move 是为了转移对象的所有权,具体的转移过程发生在移动构造函数和移动赋值运算符中。切记 std::move 并没有移动什么东西。

作业

给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词。

注意:若 s 和 t 中每个字符出现的次数都相同,则称 s 和 t 互为字母异位词。

示例 1:

输入: s = “anagram”, t = “nagaram” 输出: true 示例 2:

输入: s = “rat”, t = “car” 输出: false