0%

静态成员函数出现之前

很久以前没有静态成员函数(直到 Cfront 2.0 才加入),那个时候静态方法可以用这种方式调用:

该方法没有通过 this 访存,因而不会出现段错误。尽管这种方法还在某些编译器上能够使用,但这种写法在现在是未定义行为

在表达式上调用静态成员函数

可以在一个表达式上调用静态成员函数。表达式的返回值被忽略,但是有副作用;另外表达式的返回值类型帮助确定了静态成员函数应该在哪里寻找。

尽管现在的编译器能够正确理解数据成员(含有隐式的 this 指针)的使用,并在看到整个类定义之后再查找名字,类型名的查找则发生的很早,导致使用时可能看不到、或者使用了错误的类型声明。

所以,应该尽可能在类定义的开头写好类型别名。

#include <iostream>
#include <type_traits>

typedef int length;

class Point3d {
public:
  **void mumble(length val) { _val = val; }
  length mumble() { return _val; }

  static_assert(std::is_same_v<length, int>);

private:
  typedef float length;
  length _val;
};

int main() {}

#include <iostream>
void print(std::ostream &os, const char *s) {
    while (*s) {
        char ch = *s++;
        // Treat "{}" as a normal substring when no arguments are left
        // if (ch == '{' && *s == '}') {
        // }
        if ((ch == '{' && *s == '{') || (ch == '}' && *s == '}')) {
            os << *s++;
        } else {
            os << ch;
        }
    }
}

template <class A, class... Ts>
void print(std::ostream &os, const char *s, A &&a, Ts &&...args) {
    while (*s) {
        char ch = *s++;
        if (ch == '{' && *s == '}') {
            os << a;
            break;
        } else if ((ch == '{' && *s == '{') || (ch == '}' && *s == '}')) {
            os << *s++;
        } else {
            os << ch;
        }
    }
    if (*s) print(os, s + 1, std::forward<Ts>(args)...);
}

int main(int argc, char **argv) {
    std::cout << std::unitbuf;
    print(std::cout, "numbers : {} {} {}\\n", 10, 5, -7);
}

上面的写法依赖了 std::ostream,而且不支持格式指定符。如果能够构造一个模板类 PrintArg 包装 1 个任意类型参数和 1 个输出说明字符串,然后重载 std::ostream<< 操作符,那么可以不用动 print 函数的逻辑。

构造函数分为完整构造函数(Complete Object Constructor)和基类构造函数(Base Object Constructor)。

初始化顺序

显式构造一个对象的时候调用的是完整构造函数。该函数执行的初始化流程如下:

  1. 初始化所有基类子对象。基类子对象包括有直接关系的具体继承基类,也包括有直接或间接关系的虚基类。其中虚基类先于具体基类被初始化,尽管虚基类子对象被放在对象的最后面。
  2. 初始化自己的 vptr。
  3. 按声明顺序初始化成员。若在初始化列表中提供了初始化规则,则使用;否则,如果在声明时给出了初始化表达式,则使用;否则,采用默认初始化。
  4. 执行用户提供的代码块中的代码。

可以参考书中第 216 页。

如何初始化虚基类

对于每个直接或间接拥有虚基类的类,其构造函数都必须在成员初始化列表中调用虚基类的构造函数来初始化它。如果不显式调用虚基类构造函数,则视为使用其默认构造函数。下面的代码中取消注释 VirtualBase() = default; 并删掉 C(): A(), B(), VirtualBase("C") {} 中的 VirtualBase("C"),则代码仍能编译。

析构函数和构造函数一样,也分完整析构函数和基类析构函数两种。而且析构函数的工作流程和构造函数相反。

如果一个类的基类有未实现的析构函数(未提供定义或者是纯虚函数),会导致链接失败。这是因为子类的析构函数有调用基类析构函数的逻辑!

什么时候需要虚析构函数

https://stackoverflow.com/a/461237

Declare destructors virtual in polymorphic base classes. This is Item 7 in Scott Meyers’ Effective C++. Meyers goes on to summarize that if a class has any virtual function, it should have a virtual destructor, and that classes not designed to be base classes or not designed to be used polymorphically should not declare virtual destructors.

**自动生成的析构函数不是虚函数,但是可以显式声明其为 virtual 然后用 = default 引入。**从基类继承的析构函数后虚实性质保持不变。

基本概念

cfront 实现

#include <iostream>

struct X { public: int i; };       // size: 4
struct A: virtual X { int j; };    // size: 16
struct B: virtual X { double d; }; // size: 24
struct C: A, B { int k; };         // size: 40

int main() {
    C c;
    std::cout << c.i << '\n';
}

目的不同

虚拟继承用来动态访问数据(节约存储空间或保证数据地址唯一),虚函数用来动态访问方法。

虚表的访问方式

如果 A 类有虚函数,那么 A 类已经有 vptr、是多态类,通过 A 类指针/引用访问虚函数则需要查找虚表。

如果 A 类是普通类并且被虚拟继承,那么 vptr 并不会放到 A 类中,通过 A 类指针/引用访问虚函数也不会查表。

考虑 A *pa = new B;,其中 B 类虚拟继承于 A 类,通过 A 类指针 pa 访问到的成员就是真正要找的 A 中的成员,因为从 B 类指针转换到 A 类指针时编译器已经正确处理好了指针偏移问题,从而不需要担心没有 vptr 导致访问不到正确的成员。

考虑菱形继承的情况:

sed -i

macOS sed 的 -i 需要指定参数,如果不需要备份文件,需要显式给出 sed -i ''。不然可以用 gsed 命令(用 brew 安装)。

xargs

如果没有收到输入,就不会运行。而 GNU 的 xargs 在没有收到输入时会只运行右侧命令而不附带参数。要想 GNU xargs 在此时不运行命令,需要使用 -r 选项。

有人说从 Cfront 转向专门的 C++ 编译器的一大原因就是支持异常处理,因为异常处理在 C 语言中很难做。

一个函数的指令区域分成三种:

  1. try 以外,且没有活跃对象
  2. try 以外,有活跃对象,发生异常时需要析构
  3. try 以内

有一个活跃对象和两个活跃对象应该会有区别吧?这样制表/查表的压力很大。