非静态数据成员

来自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 位的位域
};

允许任何简单声明,但

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

另外,允许位域声明

布局

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

对于非联合体类类型,拥有相同成员访问非零大小 (C++20 起)的成员,始终按照较后声明的成员在类对象中拥有较高地址的方式分配。拥有不同访问控制的成员以未指明的顺序分配(编译器可以将它们组合在一起)。对齐要求可能需要在成员间,或在类的最后成员之后进行填充。

标准布局

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

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

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 限定符则忽略) (C++14 起),或是布局兼容的枚举(即拥有相同底层类型的枚举类型),或其共同起始序列由其所有非静态数据成员和位域 (C++14 起)组成,则称它们布局兼容(layout compatible)(上例中,AB 布局兼容)

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

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

  • 若标准布局联合体持有两个(或更多)标准布局类为其成员,且这些类拥有数据成员的共同起始序列,则检测该共同起始序列中的任意成员都是良好定义的,无关乎联合体的哪个成员活跃。
(C++14 前)
  • 在以非联合类类型 T1 作为活跃成员的标准布局联合体中,容许读取具有非联合类类型 T2 的另一联合体成员的非静态数据成员 m,只要 mT1T2 的共同起始序列的一部分(但通过非 volatile 泛左值读取 volatile 成员是未定义的)。
(C++14 起)
  • 指向标准布局类类型的指针可被 reinterpret_cast 成指向其首个非静态非位域数据成员的指针(若它拥有非静态数据成员),或指向其任何基类子对象的指针(若有基类),反之亦然。换言之,不允许标准布局类型的首个数据成员前有填充。注意严格别名化规则仍然适用于这种转型的结果。
  • 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() 
};
(C++11 起)

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

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 是悬垂引用

如果默认成员初始化器拥有执行聚合初始化的子表达式,而其将使用同一初始化器,则它不能初始化引用成员:

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" 对象
(C++03 起)

缺陷报告

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

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

参阅