0%

原因是很早之前按照知乎的说法启用了这个设置,本想禁用系统更新时自动安装旧版本的核显驱动,但也使得我的电脑核显驱动坏掉且不能自行安装。

恢复核显驱动首先应该关闭这项,然后去下载对应驱动安装,或者回退到旧的驱动。

这项配置启用时,我的 WSL 也无法正常进入!

常见问题

设置 apt 代理。编辑 /etc/apt/apt.conf,格式如下(ip 会变):

// 很奇怪,注释竟然是//开头,而不是#
Acquire::http::Proxy "http://192.168.1.56:12366/";
Acquire::https::Proxy "http://192.168.1.56:12366/";

sudo 没有 $http_proxy 代理的原因是默认不传递环境变量,使用 -E 选项传递环境变量就可以不设置上面的代理。

清华大学开源软件镜像站 https://mirrors.tuna.tsinghua.edu.cn/help/debian/

先用自己电脑给 apt 开代理,然后改镜像,安装一些 https 源需要的东西,然后 apt update。之后安装 build-essential mlocate 等等。

复制、默认构造都是按需生成的。对于平凡的情况不需要生成,只是在语意上满足“拥有构造函数”的含义。

x86-64 gcc 13.1 -std=c++20

struct Point {
    int x;
    int y;
    Point() = default; // 即便显式声明了默认构造函数,也不会合成
};

int main() {
    auto some_point = Point{}; // {}初始化对聚合类有清零的效果
}

Chapter 00-11

Java 对象构造顺序:1、基类 2、所有域 3、初始化块 4、自身的构造函数

jshell> a = new Foo()
Value initialization
Initialization block
Constructor

为接口提供默认实现:

逆变、协变、不变的关系:

关于 access section

不同 access section 数据不保证按序布局。

我在 Compiler Explorer 上测试了 gcc 和 clang,他们都是忽略 access 权限,将各个 access section 的变量布局直接拼接在一起的。

注:同一个权限也可以是不同的 access section:

  1. 指针偏移本身有少量开销,若需要偏移则得先判空,这样才能保证空指针永远为空。
  2. 指针偏移对设计虚函数表带来了困难。例如 thunk 技术用来对 this 指针做适配再调用对应函数。
  3. 指针偏移让指向成员函数的指针携带了 this 偏移量,变成了胖指针。
  4. 多继承引入了菱形继承困境,进而又引入了虚拟继承。
  5. 虚拟继承使得 vtable 中还要存储虚基类子对象的偏移。由于不同继承结构中 vtable 里虚基类子对象的偏移可能不同,又引入了 VTT,让子类调用基类构造函数时为基类的构造函数提供 vptr 参数。

多继承下重载签名相同的函数

结果是会把基类同签名的所有非 final 虚函数都重写了,而且实现方式相同。尽管基类的虚函数签名一样,但是他们没有关联性,所以在子类的虚表中占两个槽(slots)(一个槽是一个指针)。同样的,如果 Interface 中有虚函数 foo,而 A 和 B 都继承了 Interface,C 继承了 A 和 B。如果 A 和 B 没有虚拟继承 Interface,那么在 C 的对象调用函数 foo 时将出现 ambiguous 指代错误。如果 C 重写了 foo 函数,那么指代就还是明确的。或者,如果 A 和 B 都是虚拟继承自 Interface,那么也不会有编译错误。但这样通过指针/引用调用虚函数 foo 就需要先取虚基类子对象 this 的偏移,修改 this 之后再从 vptr 中读虚函数 foo,开销是 4 次访存(将虚拟继承和虚函数调用的代价累加起来了) 。

多继承下调用虚函数时修正 this 指针

为什么需要修正 this 指针?

重写签名相同的虚函数时,生成的函数代码只能对其中一个基类的虚函数做 this 修正。

#include <cstdio>
using namespace std;

struct A {
    int x;
    virtual void foo() { printf("A::foo() x=%d\n", x); }
};

struct B {
    virtual void foo() { printf("B::foo()\n"); }
};

struct C : A, B {
    void foo() override { printf("C::foo()\n"); }
};

int main() {
    C c;
    A *p = &c;    // <--
    p->foo();
    A *q = new A;
    q->foo();
    B *r = new C; // <--
    r->foo();
}

首先 C::foo() 代码只有一份(生成多份太浪费了),通过上面 p 指针调用 foo(),会从 vtable 中选中 C::foo(),然后用 C 的 this(这是因为 A 和 C 的 this 是重合的,不需要修改)。如果用 r 指针调用foo(),则需要将 B 的 this 修改成 C 的 this,那么直接复用 C::foo() 代码就不行了!

vptr

这个小节没有提到 vptr 的处理。

  1. 对于复制构造函数,其本质还是构造,所以和一般的构造函数流程相似,会正确设置好 vptr。
  2. 对于赋值运算符,原有 vptr 和新的 vptr 是一样的,不需要修改。

怎么写复制构造/赋值?

写复制构造函数和复制赋值操作符的时候要记得把基类和成员都正确初始化(用默认的最好)。复制构造函数也算是构造函数,所以要使用成员初始化列表来正确初始化。

复制赋值操作符对基类赋值需要先将 *this 转换成基类的引用,然后用赋值表达式调用基类的赋值操作符。毕竟赋值操作符也不是构造函数,构造过程需要手写的部分有很多还容易写错!这也是为什么复制构造临时对象 + swap 的方法很受欢迎。

#include <cstdio>

struct A {
    int a;
    A() = default;
    A(A const &) { puts("A(const A &)"); }
    A &operator=(A const &) {
        puts("A &operator=(A const &)");
        return *this;
    }
};

struct B {
    int b;
    B() = default;
    B(B const &) { puts("B(const B &)"); }
    B &operator=(B const &) {
        puts("B &operator=(B const &)");
        return *this;
    }
};

struct D {
    int d;
    D() = default;
    D(D const &) { puts("D(const D &)"); }
    D &operator=(D const &) {
        puts("D &operator=(D const &)");
        return *this;
    }
};

struct C : A, B {
    int c;
    D d;
    C() = default;

    // 错误写法:(没有通过初始化列表对基类和成员起到复制作用,它们被默认构造)
    // C(const C &) {
    // }

    // 等效于默认的复制构造函数
    C(const C &other) : A(other), B(other), d{other.d} {}

    // 等效于默认的赋值操作符
    // 很麻烦,不如先复制构造再swap
    C &operator=(C const &other) {
        if (this == &other) {
            return *this;
        }
        // 右边的 other 会隐式转换成对应类型
        static_cast<A &>(*this) = other;
        static_cast<B &>(*this) = other;
        d = other.d;

        return *this;
    }
};

int main() {
    C c;
    puts("copy construction");
    C c1 = c;
    puts("copy assignment");
    c1 = c;
}

初始化会按照变量声明的顺序进行。因此虽然下面的代码想要用 j 的新值初始化 i,但实际上是 i(j) 先被执行,然后才是 j(val)

不过,构造函数代码块中的初始化过程始终发生于成员初始化列表之后。