非静态数据成员

来自cppreference.com
< cpp‎ | language

非静态数据成员是在类的成员说明中声明的。

class S
{
    int n;              // 非静态数据成员
    int& r;             // 引用类型的非静态数据成员
    int a[2] = {1, 2};  // 带默认成员初始化器的非静态数据成员(C++11)
    std::string s, *ps; // 两个非静态数据成员
    struct NestedS
    {
        std::string s;
    } d5;               // 具有嵌套类型的非静态数据成员
    char bit : 2;       // 2 位的位域
};

允许任意简单声明,但

  • 不允许使用 thread_local 存储类说明符(但静态数据成员可以使用);
(C++11 起)
  • 不允许不完整类型抽象类类型及其数组:通常,类 C 不能拥有 C 类型的非静态数据成员,尽管它能拥有 C&(C 的引用)或 C*(C 的指针)类型的非静态数据成员;
  • 当存在至少一个用户定义的构造函数时,非静态数据成员不能拥有与类名相同的名字;
(C++11 起)

另外,允许位域声明

布局

当创建某个类 C 的对象时,每个非引用类型的非静态数据成员都在 C 的对象表示的某个部分中分配。引用是否占据存储是由实现定义的,但它们的存储期与以它们作为成员的对象相同。

对于非联合体类类型,非零大小 (C++20 起)未被访问说明符分隔 (C++11 前)拥有相同成员访问 (C++11 起)的成员,始终按照较后声明的成员在类对象中拥有较高地址的方式分配。被访问说明符分隔 (C++11 前)拥有不同访问控制 (C++11 起)的成员以未指明的顺序分配(编译器可以将它们组合在一起)。

(C++23 前)

对于非联合体类类型,非零大小的成员始终按照较后声明的成员在类对象中拥有较高地址的方式分配。注意成员的访问控制仍然影响标准布局属性(见后述)。

(C++23 起)

对齐要求可能需要在成员间,或在类的最后成员之后进行填充。

标准布局

一个类只有在它是简旧数据类型 (PODType) 的情况下是标准布局(standard-layout)的并拥有下述属性。

(C++11 前)

所有非静态数据成员均拥有相同访问控制,且满足其他特定条件的类被称作标准布局(standard-layout)类型(对其规定的列表见标准布局类型 (StandardLayoutType) )。

(C++11 起)

两个标准布局非联合类类型可以拥有非静态数据成员和位域的共同起始序列,它是一个或多个起始成员(按声明顺序)的序列,条件是这些成员拥有布局兼容的类型,均声明有 [[no_unique_address]] 属性或无该属性 (C++20 起),且当它们是位域时拥有相同宽度。

struct A { int a; char b; };
struct B { const int b1; volatile char b2; }; 
// A 与 B 的共同起始序列是 A.a, A.b 与 B.b1, B.b2
struct C { int c; unsigned : 0; char b; };
// A 与 C 的共同起始序列是 A.a 与 C.c
struct D { int d; char b : 4; };
// A 与 D 的共同起始序列是 A.a 与 D.d
struct E { unsigned int e; char b; };
// A 与 E 的共同起始序列为空

如果两个标准布局非联合类类型具有同一类型(如果存在 cv 限定符则忽略),或是布局兼容的枚举(即拥有相同底层类型的枚举类型),或它们的'共同起始序列由它们所有的非静态数据成员和位域组成,那么称它们布局兼容(layout compatible)(上例中,AB 布局兼容)

如果两个标准布局联合体类型拥有相同数量的非静态数据成员,且(以任何顺序)对应的非静态数据成员拥有布局兼容的类型,那么称它们布局兼容

标准布局类型拥有以下特殊性质:

  • 在以非联合类类型 T1 作为活跃成员的标准布局联合体中,容许读取具有非联合类类型 T2 的另一联合体成员的非静态数据成员 m,只要 mT1T2 的共同起始序列的一部分(但通过非 volatile 泛左值读取 volatile 成员是未定义的)。
  • 指向标准布局类类型的指针可以被 reinterpret_cast 成指向其首个非静态非位域数据成员的指针(如果它拥有非静态数据成员),或指向其任何基类子对象的指针(如果有基类) (C++11 起),反之亦然。换言之,不允许标准布局类型的首个数据成员前有填充。注意严格别名化规则仍然适用于这种转型的结果。
  • offsetof 可以用于确定任何成员距标准布局类起始的偏移量。

成员初始化

非静态数据成员可以用下列两种方式之一初始化:

1) 在构造函数的成员初始化器列表中。
struct S
{
    int n;
    std::string s;
    S() : n(7) {} // 直接初始化 n,默认初始化 s
};
2) 通过默认成员初始化器,它是成员声明中包含的花括号或等号初始化器,并在成员初始化器列表中忽略该成员的情况下得到使用。
struct S
{
    int n = 7;
    std::string s{'a', 'b', 'c'};
    S() {} // 默认成员初始化器将复制初始化 n,列表初始化 s
};

如果成员拥有默认成员初始化器,并且在构造函数的成员初始化器列表中也有出现,那么对该构造函数忽略默认成员初始化器。

#include <iostream>
 
int x = 0;
struct S
{
    int n = ++x;
    S() {}                 // 使用默认成员初始化器
    S(int arg) : n(arg) {} // 使用成员初始化器
};
 
int main()
{
    std::cout << x << '\n'; // 打印 0
    S s1;
    std::cout << x << '\n'; // 打印 1(运行默认初始化器)
    S s2(7);
    std::cout << x << '\n'; // 打印 1(未运行默认初始化器)
}

输出:

0
1
1

不能对位域成员使用默认成员初始化器。

(C++20 前)

数组类型成员不能从成员初始化器推导其大小:

struct X
{
   int a[] = {1, 2, 3};  // 错误
   int b[3] = {1, 2, 3}; // OK
};

默认成员初始化器不允许导致外围类的预置默认构造函数的隐式定义,或该构造函数的异常说明:

struct node
{
    node* p = new node; // 错误:使用隐式或预置的 node::node() 
};

在默认成员初始化器中,引用成员不能绑定到临时量(注意,成员初始化器列表有同样的规则)

struct A
{
    A() = default;     // OK
    A(int v) : v(v) {} // OK
    const int& v = 42; // OK
};
 
A a1;    // 错误:临时量到引用的非良构绑定
A a2(1); // OK(忽略默认成员初始化器,因为 v 在构造函数中出现)
         // 然而 a2.v 是悬垂引用
(C++11 起)

如果默认成员初始化器拥有会执行聚合初始化并会使用同一初始化器的子表达式,那么产生错误:

struct A;
extern A a;
 
struct A
{
    const A& a1{A{a, a}}; // OK
    const A& a2{A{}};     // 错误
};
 
A a{a, a};                        // OK
(C++14 起)

用法

非静态数据成员或非静态成员函数的名字只能出现在下列三种情形中:

1) 作为类成员访问表达式的一部分,其中的类要么拥有此成员,要么从拥有此成员的类派生,这包括在任何允许 this 的语境(成员函数体内,成员初始化器列表内,类内默认成员初始化器内)中使用非静态成员的名字时所出现的隐式 this-> 成员访问表达式。
struct S
{
    int m;
    int n;
    int x = m;            // OK:在默认初始化器中允许隐式的 this-> (C++11)
    S(int i) : m(i), n(m) // OK:在成员初始化器列表中允许隐式的 this-> 
    {
        this->f();        // 显式的成员访问表达式
        f();              // 在成员函数体内允许隐式的 this->
    }
    void f();
};
2) 构成指向非静态成员的指针
struct S
{
   int m;
   void f();
};
 
int S::*p = &S::m;       // OK:m 用于构成成员指针
void (S::*fp)() = &S::f; // OK:f 用于构成成员指针
3) (仅对数据成员,而非成员函数)当在不求值操作数中使用时。
struct S
{
   int m;
   static const std::size_t sz = sizeof m; // OK:不求值操作数中的 m
};
 
std::size_t j = sizeof(S::m + 42); // OK:即便没有 m 的 "this" 对象
注:这种用法是经由 N2253 中 CWG 问题 613 的解决方案允许的,某些编译器(例如 clang)认为它是 C++11 中的改动。

缺陷报告

下列更改行为的缺陷报告追溯地应用于以前出版的 C++ 标准。

DR 应用于 出版时的行为 正确行为
CWG 80 C++98 所有数据成员的名字不能与类名相同(与 C 不兼容) 非静态数据成员的名字在没有用户
声明的构造函数时可以与类名相同
CWG 613 C++98 不允许非静态数据成员的不求值使用 允许这种使用
CWG 1397 C++11 在默认成员初始化器中类被视为完整的 默认成员初始化不能触发默认构造函数的定义
CWG 1696 C++98 引用成员能初始化为临时量(其生存期会在构造函数结束时结束) 这种初始化非良构
CWG 1719 C++98 cv 限定不同的类型曾经不是布局兼容的 忽略 cv 限定,改进规范
CWG 2254 C++11 指向无数据成员的标准布局类的指针能 reinterpret_cast 到其首个基类 能 reinterpret_cast 到其任意基类

参阅

静态成员
非静态成员函数
检查是否是一个标准布局类型
(类模板)
从标准布局类型的起始到其指定成员的字节偏移量
(宏函数)