当前位置: C语言 -- 基础 -- 声明

声明(二)

二、存储类说明符

C语言中存储类说明符(storage-class specifiers)包括:autoconstexprexternregisterstaticthread_localtypedef;其中typedef说明符称为存储类说明符只是为了语法方便,本节不对typedef说明符作专门讨论。


在一个声明中最多只能存在一个存储类说明符,但以下情况除外:

-- 存储类说明符thread_local可以与存储类说明符static或者extern一起使用。

thread_local static int sum;
thread_local extern unsigned size;

-- 存储类说明符auto可以与其它存储类说明符(typedef除外。)一起使用。

static auto i = 5;

ISO/IEC 9899:2024标准开始,关键词auto增加了类型推断(type inference)功能,即根据初始化器的类型确定声明标识符的类型。这里标识符i的类型是int类型。未来ISO/IEC C标准可能会将auto存储类说明符修改为类型说明符。上述代码如果按ISO/IEC 9899:2024之前标准编译将给出类似error: multiple storage classes in declaration specifiers的出错信息。


-- 存储类说明符constexpr可以与存储类说明符auto、register或者static一起使用。

constexpr auto i = 5;
constexpr static unsigned arr[] = {1,3,5};

如果声明缺省存储类说明符,对于函数,默认存储类说明符是extern;对于具有块作用域(block scope)的对象,默认存储类说明符是auto

int func(int);  //等同于extern int func(int);。

int main(void)
{
    float f;   //等同于auto float f;。
    ...
    return 0;
}

使用存储类说明符(typedef除外)声明的聚合对象(aggregate object)和联合对象,存储类说明符生成的特性(链接除外)递归地适用于其成员对象。


1、auto存储类说明符

auto存储类说明符仅用于具有块作用域对象(函数形式参数除外。)的声明;声明的对象具有自动存储期限(automatic storage duration),并且无链接。auto存储类说明符是此类对象默认的存储类说明符,声明时auto存储类说明符通常是缺省的。

auto int i;   //非法,i具有文件作用域。

int main(void)
{
  auto double d;  //合法,d具有块作用域。

  return 0;
}

ISO/IEC 9899:2024标准规定:如果auto存储类说明符与其它存储类说明符一起使用,或者出现在具有文件作用域的声明中,在确定存储期限或者链接时,auto存储类说明符将被忽略;这种情况下auto存储类说明符只表示类型推断(type inference)。


1.1、类型推断

类型推断是ISO/IEC 9899:2024标准新增内容,对于类型推断对象的定义声明,初始声明符应具有以下语法格式:

direct-declarator = assignment-expression

其中direct-declarator是直接声明符,assignment-expression是赋值表达式。

类型推断声明应包含存储类说明符auto。声明对象推断得到的类型是经过左值转换、数组到指针的转换或者函数到指针的转换后赋值表达式的类型,如果存在类型限定符或者属性,会受类型限定符限定或者属性影响。

auto d = 3.14;      //d的类型是double类型。
auto i = 5;         //i的类型是int类型。
const auto ci = 7;  //ci的类型是const int类型。

unsigned arr[] = {1u, 3u, 5u};
auto p1 = arr;	//p1的类型是unsigned *类型。
auto p2 = &arr;	//p2的类型是unsigned (*)[3]类型。

如果一个声明内同时定义了结构类型或者联合类型,其行为将由实现定义。

auto a = (struct s1{int i;}){5};  //实现定义行为。

struct s2;
auto b = (struct s2{int i;}){7};  //实现定义行为。

如果直接将结构定义用作类型说明符,程序可具有更好的可移植性。

struct {int i;} a = {5};

类型推断标识符的作用域从初始化器结束后开始,因此赋值表达式不能引用声明的对象或者函数。在初始化器中对声明标识符的任何使用都是无效的,即使在外层作用域存在同名实体。

{
  int a = 3;  //A
  int b = 4;  //B
  
  {
    //未定义行为,C处声明的a会屏蔽A处声明的a。
    //C处赋值表达式中的a未初始化。
    int a = a * a;  //C
    
    //非法。B处声明的b在D处赋值表达式中不可见。
    auto b = b * b; //D
  }
  
  {
    //合法。E处赋值表达式使用B处的b值。
    auto a = b * b; //E
    
    //合法。F处赋值表达式使用E处的a值。
    auto b = a;     //F
  }
}

2、register存储类说明符

register存储类说明符仅用于具有块作用域和函数原型作用域的对象声明;声明的对象具有自动存储期限(automatic storage duration),并且无链接。

int func(register int);
...
for(register unsigned u=0; u<1000; u++)

使用register存储类说明符声明对象标识符表明对该对象的访问应尽可能快;但这只是建议,该建议的有效程度取决于实现。

实现可能将register声明等同于auto声明;但无论实现是否使用可寻址存储空间,都无法使用存储类说明符register声明对象的地址,无论是显式地(例如:使用取地址运算符&。),还是隐式地(例如:将数组名转换成指针。);唯一可以用于使用register存储类说明符声明数组的运算符是sizeof运算符。

register int i;
register int arr[5];
int *ptr = NULL;

ptr = &i;   //非法。
ptr = arr;  //非法。
printf("%zu\n", sizeof(arr));   //合法。

函数的形式参数不能使用除register外的任何其它存储类说明符。


3、static存储类说明符

static存储类说明符可用于具有文件作用域的函数以及具有文件作用域或者块作用域(函数的形式参数除外)的对象的声明。

static int func(int); //合法,函数具有文件作用域。
static int i = 10;    //合法,对象具有文件作用域。

int main(void)
{
  static double d = 3.14;	//合法,对象具有块作用域。
  static int f(int);		//非法,函数具有块作用域。
  ...

  return 0;
}

使用static存储类说明符(与thread_local存储类说明符组合使用的情况除外。)声明的对象和函数具有静态存储期限;对于函数和具有文件作用域的对象具有内部链接,对于具有块作用域的对象无链接。

static int func(int); //静态存储期限、内部链接。
static int i = 10;    //静态存储期限、内部链接。

int main(void)
{
  static double d = 3.14; //静态存储期限、无链接。
  ...

  return 0;
}

4、extern存储类说明符

extern存储类说明符可用于具有文件作用域或者块作用域(函数形式参数除外。)的对象或者函数的声明。

extern int func(int); //合法。
extern int i;         //合法。

int main(void)
{
  extern double d;    //合法。
  extern int f(int);  //合法。
  ...

  return 0;
}

使用extern存储类说明符(与thread_local存储类说明符组合使用的情况除外。)声明的对象和函数具有静态存储期限。

如果一个标识符已声明为具有内部链接的标识符,再使用extern存储类说明符声明该标识符,该标识符仍具有内部链接;其它情况下使用extern存储类说明符声明的标识符具有外部链接。

static int func(int); //内部链接。
static int i;         //内部链接。

int main(void)
{
  extern int func(int);   //内部链接。
  extern int i;           //内部链接。
  ...

  return 0;
}

具有块作用域函数标识符的声明不能使用除extern外的任何其它存储类说明符。


5、thread_local存储类说明符

thread_localISO/IEC 9899:2024标准新增关键词,用于替代ISO/IEC 9899:2024之前标准中的_Thread_local关键词。

thread_local存储类说明符只能用于对象声明,不能用于函数声明。如果thread_local存储类说明符用于具有块作用域对象的声明,存储类说明符还应包括static或者extern存储类说明符。如果thread_local出现在对象的任一声明中,则应出现在该对象的每个声明中。

使用thread_local存储类说明符声明的对象具有线程存储期限(thread storage duration),对象的链接属性应结合static或者extern存储类说明符确定。

thread_local int ta = 5;		//外部链接。
static thread_local int tb = 15;	//内部链接。

/*新线程。*/
...
extern thread_local int ta;    //外部链接。
extern thread_local int tb;    //内部链接。
...

/*主线程。*/
...
static thread_local int ta;    //无链接。
static thread_local int tb;    //无链接。
...

6、constexpr存储类说明符

constexpr存储类说明符是ISO/IEC 9899:2024标准新增关键词。使用存储类说明符constexpr声明的对象,其值在编译时固定;如果没有,则会在对象类型中隐式地添加const类型限定符进行限定。声明的标识符视为相应类型的常量表达式。

constexpr int i = 5;    	//i是int类型的常量表达式。
constexpr auto u = 3u;  	//u是unsigned类型的常量表达式。
constexpr int *ptr = nullptr;	//ptr是int *类型的常量表达式。

使用constexpr存储类说明符声明的标识符可以在常量表达式中使用。

constexpr auto size = 5;

int arr[size];  //arr是定长数组,不是变长数组。

使用存储类说明符constexpr声明的对象及其成员不能具有原子类型或者可变修改类型(variably modified type),也不能使用volatile或者restrict类型限定符限定。

constexpr声明应是定义,并应存在初始化器;初始化器中的赋值表达式(如果存在。)应是常量表达式或者字符串字面量。常量表达式的值或者字符串字面量中的任何字符值在对应目标类型中应能准确表示,且不能更改。浮点类型的初始化器应使用编译时浮点环境评估。

//{}是空初始化器,执行默认初始化。
constexpr int i = {};       //i初始化为0。
constexpr double *ptr = {}; //ptr初始化为空指针。

使用存储类说明符constexpr且未使用存储类说明符static声明的具有块作用域的对象,具有自动存储期限,并且无链接;如果声明时未使用存储类说明符register,每个对象实例都存在唯一地址。

//两处i都具有自动存储期限,并且无链接。
//两处i的地址不同。
{
  constexpr int i = 3; 
}

{
  constexpr int i = 3;  
}

使用存储类说明符constexpr且未使用存储类说明符static声明的具有文件作用域的对象,具有静态存储期限,并且具有内部链接;不同编译单元内的相同文本定义则定义了具有不同地址的独立对象。

//假设i具有文件作用域,则i具有静态存储期限,内部链接。
//两处i的地址不同。

//编译单元A。
constexpr int i = 3; 

//编译单元B。
constexpr int i = 3; 

如果使用存储类说明符constexpr声明的对象或者子对象具有指针类型、整数类型或者算术类型,则其显式初始化值应分别为空、整数常量表达式或者算术常量表达式。如果声明的对象具有实数浮点类型,初始化器应具有整数或者实数浮点类型。如果声明的对象具有虚数类型,初始化器应具有虚数类型。如果初始化器具有十进制浮点类型,则声明的对象也应具有十进制浮点类型,并且转换时应保留初始化器的量子值(:保留量子值意味着转换后的值需保留原始值的有效数字和指数范围。这与标准浮点类型转换可能丢失精度的情况形成对比。)。如果初始化器具有实数类型,并且是信号非数值(signaling NaN),初始化器类型的非限定版本与声明对象对应的实数类型应兼容(:信号非数值表示无效或者未定义的计算结果,例如:0.0/0.0。与安静非数值不同,当计算操作中使用或者尝试加载信号非数值时,通常会触发浮点异常。)。

//复数类型对象的初始化器可以是实数类型、虚数类型、复数类型。
constexpr double _Complex dc1 = 3.0;              //实数类型。
constexpr double _Complex dc2 = 3.0*I;            //虚数类型。
constexpr double _Complex dc3 = CMPLX(3.0, 5.0);  //复数类型。

//实数浮点类型对象的初始化器应具有整数或者实数浮点类型。
constexpr double  d1 = 3.0;       
constexpr double  d2 = (double _Imaginary)3.0;  //违反约束。
constexpr double  d3 = (double _Complex)3.0;    //违反约束。

//如果声明的对象具有虚数类型,初始化器应具有虚数类型。      
constexpr double _Imaginary i1 = 3.0*I;   //合法。
constexpr double _Imaginary i2 = 3.0;     //违反约束。

//宏INFINITY表示float类型的无限大。
//INFINITY、(double)INFINITY、(long double)INFINITY认为具有相同值。
constexpr float f1 = INFINITY;				//合法。       
constexpr float f2 = (double)INFINITY;			//合法。
constexpr float f3 = (long double)INFINITY;		//合法。

//宏NAN表示float类型的安静非数值(quiet NaNs)。
//实数浮点类型的安静非数值认为具有相同值。
constexpr float f4 = NAN;		//合法。       
constexpr float f5 = (double)NAN;	//合法。
constexpr float f6 = (long double)NAN;	//合法。

//宏FLT_SNAN、DBL_SNAN、LDBL_SNAN分别表示
//float类型、double类型、long double类型的信号非数值;
//即使具有相同格式,也应认为是不同值。
constexpr double d4 = FLT_SNAN;   //违反约束。       
constexpr double d5 = DBL_SNAN;   //合法。
constexpr double d6 = LDBL_SNAN;  //违反约束。

//如果声明的对象具有十进制浮点类型,
//初始化器应具有整数或者实数浮点类型。
constexpr _Decimal32 df1 = 3;		//合法。
constexpr _Decimal32 df2 = 3.0;	//合法。
constexpr _Decimal32 df3 = INFINITY;	//合法。
constexpr _Decimal32 df4 = NAN;	//合法。

//宏DEC_INFINITY表示_Decimal32类型的无限大。
//宏DEC_NAN表示_Decimal32类型的安静非数值(quiet NaN)。
constexpr _Decimal32 df6 = DEC_INFINITY;    //合法。
constexpr _Decimal32 df7 = DEC_NAN;         //合法。

//宏DEC32_SNAN、DEC64_SNAN、DEC128_SNAN分别表示
//_Decimal32类型、_Decimal64类型、_Decimal128类型的信号非数值。
constexpr _Decimal32 df6 = DEC32_SNAN;   //合法。
constexpr _Decimal64 dd1 = DEC32_SNAN;   //违反约束。

使用constexpr存储类说明符声明的对象会存储初始化器的精确值,不会隐式地执行值转换;只要在算术转换中有必要进行数值转换,就会违反约束条件。

int a = 3.14;			//合法。
constexpr int b = 3.14;	//值转换,违反约束。

constexpr unsigned c = -1;  	//违反约束。
constexpr unsigned d = -1U; 	//合法。以32位unsigned类型为例,值为0XFFFFFFFF。

//如果宏FLT_EVAL_METHOD值为2,
//根据long double类型的范围和精度评估5.0/6.0,将违反约束。
//如果宏FLT_EVAL_METHOD值为0或者1,这种情况下合法。
constexpr double e = 5.0/6.0;
constexpr double f = (double)(5.0/6.0); //合法。

//初始化器的类型是char类型。
constexpr char g[] = {'\xFF','\0'};   //合法。

//如果char类型与unsigned char类型兼容,合法。
//如果char类型与signed char类型兼容,将违反约束。
constexpr unsigned char h[] = {'\xFF','\0'};

constexpr char i = 65U; //合法,65在char类型可表示的值域范围内。
constexpr int j = UINT_MAX; //违反约束,UINT_MAX不在int类型可表示的值域范围内。
constexpr float k = 33; //合法,float类型能够精确表示33。

//0.1在二进制中是无限循环小数。
//如果float类型和double具有相同的精度,并以相同的精度评估,
//则合法,否则违反约束。
constexpr float l = 0.1;