定义与实例化 `struct` / `class`
中文

定义与实例化 struct / class

structclass 能把一组相关字段收拢到同一个名字下面。这样一来,代码就不 用到处传好几个分离的值,而是可以先定义一个“这些字段天然属于一起”的类型。

在 scpp 里,structclass 不是可互换的两种写法:

这种划分是刻意为之的。选择 struct,等于在说“这个类型不会进入继承、 interface 与虚调用的世界”。选择 class,则是从一开始就显式进入那个世界:每 一个 class 都必须声明 virtual 析构函数,即使它什么都不做。这样一来,以后 再加入虚函数或 interface base 时,就不会悄悄改变这个类型的形状;同时,scpp 也避免了 C++ 里那种“把类当基类用了,却忘了写 virtual 析构函数”的经典陷阱。

后面的章节会详细介绍继承和 interface。眼下先记住一条分界线:只有 class 能 参与那套系统。struct 不能声明虚成员,不能写 base-clause,也不能标成 [[scpp::interface]];反过来,一个用 struct 声明出来的类型,后面也不能被某 个 class 拿去当 base。

下面每个可执行示例都可以保存成 records.scpp,然后这样构建并运行:

scpp records.scpp -o records
./records

对于那些本来就应该被编译器拒绝的示例,如果你希望得到与书里逐字一致的诊断输 出,请把文件保存成诊断块里显示的那个描述性文件名。

定义一个带命名字段的基础 struct

只有字段的 struct,是把相关数据放在一起的最简单方式。

import std;

struct User {
    int id{};
    const char* name{""};
};

int main() {
    User user{};
    user.id = 7;
    user.name = "Ada";
    std::println("{} {}", user.id, user.name);
    return 0;
}

输出:

7 Ada

User user{}; 会创建一个 User 值,并先对字段做默认初始化。之后,字段就可以 用普通的点语法来读写。

在当前 scpp 里,像 User user{7, "Ada"}; 这样的带参数花括号,并不会自动把值 填进 public 字段里。如果你想在构造时就传参数,就要定义构造函数。

struct 仍然可以隐藏字段并定义行为

在 scpp 里,如果你只是想隐藏数据或定义构造函数,并不需要因此切到 classstruct 仍然可以有 private: 区段、default constructor、parameterized constructor,以及普通的非虚成员函数。

import std;

struct Size {
private:
    int width{};
    int height{};

public:
    Size() {
        return;
    }

    Size(int initial_width, int initial_height)
        : width{initial_width}, height{initial_height} {
        return;
    }

    void grow_width(int delta) {
        this->width = this->width + delta;
        return;
    }

    int area() const {
        return this->width * this->height;
    }
};

int main() {
    Size empty{};
    Size window{3, 4};
    window.grow_width(1);
    std::println("{} {}", empty.area(), window.area());
    return 0;
}

输出:

0 16

这里的 Size 仍然是一个 struct,尽管它把字段藏了起来,还围绕这些字段定义了 行为。我们会在第 5.3 节再回到方法语法;现在更重要的点是:只要一个类型应该保 持“非虚、非继承”的数据形态,struct 仍然就是那个普通而正统的工具。

单参数构造函数也可以在调用点触发转换

单参数构造函数还可以充当 converting constructor。这意味着:如果一个函数按值接 收该类型,那么调用点也可以直接传入那个构造参数。

import std;

struct Meters {
    int value{};

public:
    Meters(int initial_value) : value{initial_value} {
        return;
    }
};

int read(Meters meters) {
    return meters.value;
}

int main() {
    Meters direct{8};
    std::println("{} {}", read(5), direct.value);
    return 0;
}

输出:

5 8

这仍然是普通的构造。read(5) 之所以可行,是因为 scpp 会先从 5 构造一个临时 的 Meters,再用它来满足这个按值参数。

struct 的字段必须保持 plain data

scpp 里的 struct 仍然只接受 plain-data 字段类型。如果某一个字段需要 std::string 这样的 class 行为,那么外围这个类型就必须改成 class

import std;

struct Bad {
    std::string name{"hi"};
};

int main() {
    Bad value{};
    return 0;
}

编译器输出:

struct_string_field_fail.scpp: error: struct 'Bad' field 'name': a class type 'std::string' cannot be a struct field; use class instead (only scalars, pointers, trivial structs/unions, and fixed-size arrays of trivial types are allowed here; see spec ch04)

这也是它和普通 C++ 的一大区别:在普通 C++ 里,structclass 往往主要只差 默认访问级别;但在 scpp 里,它们承诺的能力边界本身就不同。

定义并实例化一个 class

在使用点上,class 看起来仍然很熟悉:你同样会定义字段、用花括号构造一个值, 并且用点语法访问 public 字段。

import std;

class DisplayName {
public:
    std::string text;

    DisplayName(const char* initial_text) : text{initial_text} {
        return;
    }

    virtual ~DisplayName() {
        return;
    }
};

int main() {
    DisplayName name{"scpp"};
    std::println("{}", name.text.c_str());
    return 0;
}

输出:

scpp

表面语法很简单,但这里的设计选择已经和 struct 不一样了。这个类型现在可以持 有 std::string,而且因为它是 class,它也进入了语言里“以后可以有一个普通 base class,再加任意多个 interface base”的那一侧。

每个 class 都必须显式声明 virtual 析构函数

如果你省掉这个析构函数,那么即使这个类没有别的虚成员,程序也仍然是 ill-formed 的。

class Account {
public:
    Account() {
        return;
    }
};

int main() {
    Account account{};
    return 0;
}

编译器输出:

class_without_virtual_dtor_fail.scpp: error: class 'Account' must declare an explicit virtual destructor (spec §11.5(1))

所以在 scpp 里,选择 class 并不只是风格偏好。它是语言里专门留给继承与多态 的那种形式,而析构函数规则就是让这个选择从一开始就明确、稳定的一部分。

默认花括号初始化仍然需要 default constructor

Type value{}; 的意思是“用零个构造参数来构造一个值”。如果一个类型只声明了带 参数构造函数,那么这种初始化会被正常地以“构造函数选择失败”的方式拒绝。

struct CtorOnly {
    int value;

public:
    CtorOnly(int x) : value{x} {
        return;
    }
};

int main() {
    CtorOnly value{};
    return 0;
}

编译器输出:

struct_default_ctor_fail.scpp:11:5: error: type 'CtorOnly' has no default constructor; no constructor of 'CtorOnly' matches 0 arguments
 11 |     CtorOnly value{};
    |     ^

同样的规则也适用于 class。如果你希望 Type value{}; 成立,那么这个类型就真 的必须有 default constructor。

struct 不能声明虚成员

反过来,struct 这一侧的限制也同样重要:struct 永远不是虚的。

struct Plain {
    virtual void f() {
        return;
    }
};

int main() {
    Plain value{};
    return 0;
}

编译器输出:

struct_virtual_member_fail.scpp:2:5: error: a declaration introduced by 'struct' shall not declare a virtual member function or virtual destructor (spec §11.1(2.3))
 2 |     virtual void f() {
   |     ^

如果一个类型需要 virtual 行为,它就必须是 class

struct 不能继承

同样地,在 scpp 里,struct 也不是用来继承的那种形式。

class Base {
public:
    Base() {
        return;
    }

    virtual ~Base() {
        return;
    }
};

struct Derived : public Base {
    Derived() {
        return;
    }
};

int main() {
    Derived value{};
    return 0;
}

编译器输出:

struct_inherit_fail.scpp:12:16: error: a declaration introduced by 'struct' shall not have a base-clause (spec §11.1(2.1))
 12 | struct Derived : public Base {
    |                ^

同一条边界在线的另一侧也成立:后面某个 class 也不能把 struct 当作 base, 而且 struct 也不能标成 [[scpp::interface]]。如果某个类型将来可能参与继承或 interface,就应该从一开始把它定义成 class

structclass 规则小结

到这里,工作规则可以总结成:

下一节会围绕一个“受检查的 class”搭一个小示例程序。


← 上一章:std::span 与其它非拥有视图 · 目录