C编程
指针(Pointers)
指针是一个值,用来指定某个值的地址(即在内存中的位置)。指针是存储内存位置的变量。
你需要了解关于指针的四个基本概念:
- 如何声明指针(使用地址操作符
&
):int *pointer = &variable;
- 如何为指针赋值(
pointer = NULL;
) - 如何引用指针指向的值(通过解引用操作符
*
,称为解引用):value = *pointer;
- 指针与数组的关系(C 中绝大多数数组是简单的列表,称为“1维数组”,我们将在后面的章节简要讨论多维数组和指针的关系)。
指针可以引用任何数据类型,甚至是函数。我们还会讨论指针与文本字符串的关系以及函数指针的更高级概念。
声明指针
考虑以下代码片段,它声明了两个指针:
struct MyStruct {
int m_aNumber;
float num2;
};
int main() {
int *pJ2;
struct MyStruct *pAnItem;
}
第 1-4 行定义了一个结构体。第 8 行声明了一个指向 int
类型的指针变量 pJ2
,第 9 行声明了一个指向 MyStruct
结构体的指针变量 pAnItem
。因此,要声明一个变量作为指向某种类型的指针,而不是包含某种类型的变量,必须在变量名前加上星号(*
)。
在下面的例子中,第 1 行声明了 var1
为指向 long
类型的指针,var2
为 long
类型变量,而不是指向 long
的指针。第 2 行声明了 p3
为指向指针的指针(即指向 int
类型的指针的指针)。
long *var1, var2;
int **p3;
指针类型通常作为函数调用的参数。以下示例展示了如何声明一个使用指针作为参数的函数。由于 C 语言是按值传递函数参数,为了允许函数修改调用例程中的值,必须传递指向该值的指针。即使结构体中的内容在函数中没有被修改,指向结构体的指针也常作为函数参数传递。这样做是为了避免将结构体的完整内容复制到栈上。稍后会详细讨论指针作为函数参数的用法。
int MyFunction(struct MyStruct *pStruct);
为指针赋值
到目前为止,我们讨论了如何声明指针。接下来是如何为指针赋值。要将变量的地址赋给指针,使用 &
操作符(取地址操作符)。
int myInt;
int *pPointer;
struct MyStruct dvorak;
struct MyStruct *pKeyboard;
pPointer = &myInt;
pKeyboard = &dvorak;
在这里,pPointer
现在将引用 myInt
,pKeyboard
将引用 dvorak
。
指针还可以被赋值来引用动态分配的内存。malloc()
和 calloc()
函数通常用于执行此操作。
#include <stdlib.h>
/* ... */
struct MyStruct *pKeyboard;
/* ... */
pKeyboard = malloc(sizeof *pKeyboard);
malloc
函数返回指向动态分配内存的指针(如果失败则返回 NULL)。这块内存的大小会根据 MyStruct
结构体的大小来适当分配。
指针赋值示例
以下是一个示例,展示了如何将一个指针赋值给另一个指针,以及如何将一个指针赋值为函数的返回值:
static struct MyStruct val1, val2, val3, val4;
struct MyStruct *ASillyFunction(int b) {
struct MyStruct *myReturn;
if (b == 1) myReturn = &val1;
else if (b == 2) myReturn = &val2;
else if (b == 3) myReturn = &val3;
else myReturn = &val4;
return myReturn;
}
struct MyStruct *strPointer;
int *c, *d;
int j;
c = &j; /* 使用 & 操作符为指针赋值 */
d = c; /* 将一个指针赋值给另一个指针 */
strPointer = ASillyFunction(3); /* 从函数返回的指针 */
当从函数返回指针时,不要返回指向函数局部变量的指针,或指向函数参数的指针。因为指向局部变量的指针在函数退出时会变得无效。在上面的函数中,返回的指针指向的是静态变量。返回指向动态分配内存的指针也是有效的。
指针解引用
指针 p
指向变量 a
。要访问指针所指向的值,使用解引用操作符 *
。另外,操作符 ->
与指针和结构体一起使用。以下是一个简短的示例:
int c, d;
int *pj;
struct MyStruct astruct;
struct MyStruct *bb;
c = 10;
pj = &c; /* pj 指向 c */
d = *pj; /* d 被赋值为 pj 所指向的值,即 10 */
pj = &d; /* 现在 pj 指向 d */
*pj = 12; /* d 现在是 12 */
bb = &astruct;
(*bb).m_aNumber = 3; /* 将 3 赋值给 astruct 的 m_aNumber 成员 */
bb->num2 = 44.3; /* 将 44.3 赋值给 astruct 的 num2 成员 */
*pj = bb->m_aNumber; /* 等同于 d = astruct.m_aNumber; */
表达式 bb->m_aNumber
完全等同于 (*bb).m_aNumber
。它们都访问由 bb
指向的结构体中的 m_aNumber
元素。接下来,我们将讨论另一种解引用指针的方法。
解引用无效指针时的错误
当解引用一个指向无效内存位置的指针时,通常会发生错误,导致程序终止。错误通常会报告为“段错误”(segmentation fault)。常见的原因是未初始化指针就尝试解引用它。
C 语言的特点之一是它给了你足够的自由,但也很容易出错。指针解引用就是一个典型的例子。你可以自由地编写代码访问你没有明确请求的内存区域。而且,系统内存分配的细节可能会让这些内存看起来是可用的。因此,尽管 99 次执行没有错误,但第 100 次执行时,系统可能会因为你“盗用”了内存而导致程序失败。所以,务必确保你的指针偏移量在分配的内存范围内!
void 指针
void *somePointer;
声明了一个指向某个未指定类型的指针。你可以为 void
指针赋值,但在解引用之前,必须将它强制转换为指向某个指定类型的指针。对于 void *
指针,指针运算也是无效的。
指针与数组
到目前为止,我们已经小心地避免在指针的上下文中讨论数组。指针与数组的相互作用可能会令人困惑,但有两个基本的陈述可以帮助我们理解:
- 声明为数组的变量充当该类型的指针。当它单独使用时,它指向数组的第一个元素。
- 指针可以像数组名一样被索引。
第一个情况通常出现在数组作为函数参数传递时。函数声明该参数为指针,但实际传入的参数可能是数组的名称。第二种情况通常发生在访问动态分配的内存时。
让我们通过几个例子来进一步理解。以下代码中,调用 calloc()
实际上分配了一个 struct
MyStruct
类型的数组。
struct MyStruct {
int someNumber;
float otherNumber;
};
float returnSameIfAnyEquals(struct MyStruct *workingArray, int size, int bb) {
/* 遍历数组并检查 any someNumber 是否等于 bb。如果是,返回 otherNumber 的值。
* 如果没有值等于 bb,则返回 0.0f。 */
for (int i = 0; i < size; i++) {
if (workingArray[i].someNumber == bb) {
return workingArray[i].otherNumber;
}
}
return 0.0f;
}
// 声明变量
float someResult;
int someSize;
struct MyStruct myArray[4];
struct MyStruct *secondArray; // 注意,这是一个指针
const int ArraySize = sizeof(myArray) / sizeof(*myArray);
// 初始化 myArray
someResult = returnSameIfAnyEquals(myArray, ArraySize, 4);
secondArray = calloc(someSize, sizeof(struct MyStruct));
for (int i = 0; i < someSize; i++) {
/* 用一些数据填充 secondArray */
secondArray[i].someNumber = i * 2;
secondArray[i].otherNumber = 0.304f * i * i;
}
指针和数组名称几乎可以互换使用;但是也有一些例外。你不能将新指针值赋给数组名称。数组名称始终指向数组的第一个元素。在 returnSameIfAnyEquals
函数中,workingArray
可以被重新赋值,因为它仅是指向 workingArray
第一个元素的指针。函数也可以返回指向数组元素的指针,这是有效的,前提是数组是作为参数传递的。函数永远不应返回指向局部变量的指针,尽管编译器可能不会报错。
声明函数参数时
声明一个没有大小的数组变量,相当于声明一个指针。通常这样做是为了强调指针变量将像数组一样使用。
/* 两个等效的函数原型 */
int LittleFunction(int *paramN);
int LittleFunction(int paramN[]);
指针运算
现在我们准备讨论指针运算。你可以对指针加减整数值。如果 myArray
被声明为某种类型的数组,那么表达式 *(myArray + j)
,其中 j
是一个整数,等价于 myArray[j]
。例如,在上面的代码中,我们使用了表达式 secondArray[i].otherNumber
,我们也可以将其写为 (*(secondArray +
i)).otherNumber
或更简洁地写为 (secondArray +
i)->otherNumber
。
需要注意的是,在对指针和整数进行加减运算时,指针的值不会直接根据整数调整,而是根据指针所指向类型的大小(以字节为单位)进行调整。例如,pointer
+ x
可以看作 pointer + (x * sizeof(*type))
。
你还可以将一个指针从另一个指针中减去,前提是它们指向同一个数组的元素(或者数组结束位置后的一个位置)。如果你有一个指向数组元素的指针,那么当你从该指针中减去数组名时,结果就是该元素的索引。以下是一个示例:
struct MyStruct someArray[20];
struct MyStruct *p2;
int i;
/* 数组初始化 ... */
for (p2 = someArray; p2 < someArray + 20; ++p2) {
if (p2->num2 > testValue)
break;
}
i = p2 - someArray;
多维数组与指针的相互作用
你可能会想知道指针和多维数组如何相互作用。让我们详细看看这个问题。假设 A
被声明为一个二维浮点数组(float
A[D1][D2];
),并且 pf
被声明为指向浮点数的指针。如果 pf
被初始化为指向 A[0][0]
,那么 *(pf + 1)
就等价于 A[0][1]
,而 *(pf + D2)
就等价于 A[1][0]
。数组元素是按行主序(row-major order)存储的。
float A[6][8];
float *pf;
pf = &A[0][0];
*(pf + 1) = 1.3; /* 将 1.3 赋给 A[0][1] */
*(pf + 8) = 2.3; /* 将 2.3 赋给 A[1][0] */
非规则二维数组
让我们来看一个稍微不同的问题。我们希望拥有一个二维数组,但不需要所有行的长度相同。我们通过声明一个指针数组来实现。下面的第二行声明 A
为一个指针数组,每个指针指向一个浮点数。以下是相应的代码:
float linearA[30];
float *A[6];
A[0] = linearA; /* 5 - 0 = 5 个元素 */
A[1] = linearA + 5; /* 11 - 5 = 6 个元素 */
A[2] = linearA + 11; /* 15 - 11 = 4 个元素 */
A[3] = linearA + 15; /* 21 - 15 = 6 个元素 */
A[4] = linearA + 21; /* 25 - 21 = 4 个元素 */
A[5] = linearA + 25; /* 30 - 25 = 5 个元素 */
*A[3][2] = 3.66; /* 将 3.66 赋给 linearA[17] */
*A[3][-3] = 1.44; /* 引用 linearA[12];
负索引有时很有用,但尽量避免使用。 */
通过使用指针数组,我们能够实现一个不规则长度的二维数组。
数组索引的一个有趣之处
我们在此还注意到数组索引的一个有趣现象。假设 myArray
是一个数组,i
是一个整数值。表达式 myArray[i]
等价于 i[myArray]
。前者等价于 *(myArray +
i)
,后者等价于 *(i + myArray)
。这两者是相同的,因为加法是交换律的。
指针与前增量或后递减运算
指针可以与前增量(++
)或后递减(--
)一起使用,这种操作有时会在循环中出现,如以下示例所示。增量和递减运算应用于指针,而不是指针所指向的对象。换句话说,*pArray++
等价于 *(pArray++)
。
long myArray[20];
long *pArray;
int i;
/* 给 myArray 的每个元素赋值 */
pArray = myArray;
for (i = 0; i < 10; ++i) {
*pArray++ = 5 + 3*i + 12*i*i;
*pArray++ = 6 + 2*i + 7*i*i;
}
函数参数中的指针
通常我们需要用指针作为参数调用函数。在许多情况下,变量本身就是当前函数的一个参数,并且可能是指向某种结构体类型的指针。在这种情况下,不需要使用 &
符号来获取指针值,因为该变量本身已经是一个指针。下面的示例中,变量 pStruct
是一个指针,作为参数传递给 FunctTwo
函数,并作为参数传递给 FunctOne
函数。
FunctOne
函数的第二个参数是 int
类型。由于在 FunctTwo
函数中,mValue
是指向 int
的指针,因此必须首先使用 *
运算符对指针进行解引用,因此函数调用中的第二个参数是 *mValue
。FunctOne
函数的第三个参数是一个指向 long
类型的指针。由于 pAA
本身是指向 long
的指针,因此在作为第三个参数传递给函数时,不需要 &
符号。
int FunctOne(struct someStruct *pValue, int iValue, long *lValue)
{
/* 做一些事情... */
return 0;
}
int FunctTwo(struct someStruct *pStruct, int *mValue)
{
int j;
long AnArray[25];
long *pAA;
pAA = &AnArray[13];
j = FunctOne(pStruct, *mValue, pAA); /* pStruct 已经持有指针指向的地址,无需获取任何东西的地址。*/
return j;
}
指针与文本字符串
在 C 语言中,文本字符串历史上是作为字符数组实现的,字符串的最后一个字节为零,即空字符 '\0'
。大多数 C 语言实现都带有标准库函数用于操作字符串。许多常用的函数期望字符串是以 null 结尾的字符数组。要使用这些函数,需要包含标准 C 头文件 "string.h"
。
一个静态声明并初始化的字符串可能看起来像以下示例:
static const char *myFormat = "Total Amount Due: %d";
变量 myFormat
可以视为一个包含 21 个字符的数组。最后的隐式 null 字符('\0'
)被添加到字符串的末尾,作为数组的第 21 个元素。你也可以如下初始化数组中的单个字符:
static const char myFlower[] = { 'P', 'e', 't', 'u', 'n', 'i', 'a', '\0' };
一个初始化的字符串数组通常如下声明:
static const char *myColors[] = {
"Red", "Orange", "Yellow", "Green", "Blue", "Violet" };
对于一个特别长的字符串初始化,可以将字符串跨多行写,如下所示:
static char *longString = "Hello. My name is Rudolph and I work as a reindeer "
"around Christmas time up at the North Pole. My boss is a really swell guy."
" He likes to give everybody gifts.";
与字符串相关的库函数将在后续章节中讨论。
指向函数的指针
C 语言还允许你创建指向函数的指针。指向函数的指针语法可能会比较复杂。以下是一个示例:
static int Z = 0;
int *pointer_to_Z(int x) {
/* 返回整数指针的函数,而不是指向函数的指针 */
return &Z;
}
int get_Z(int x) {
return Z;
}
int (*function_pointer_to_Z)(int); // 指向一个接受 int 作为参数并返回 int 的函数的指针
function_pointer_to_Z = &get_Z;
printf("pointer_to_Z output: %d\n", *pointer_to_Z(3));
printf("function_pointer_to_Z output: %d", (*function_pointer_to_Z)(3));
声明 typedef 类型的函数指针通常能使代码更加清晰。以下是一个使用函数指针和 void *
指针实现回调的示例。DoSomethingNice
函数调用一个由调用者提供的函数 TalkJive
,并传入调用者的数据。需要注意的是,DoSomethingNice
实际上并不知道 dataPointer
指向的是什么。
typedef int (*MyFunctionType)(int, void *); /* 函数指针的 typedef */
#define THE_BIGGEST 100
int DoSomethingNice(int aVariable, MyFunctionType aFunction, void *dataPointer)
{
int rv = 0;
if (aVariable < THE_BIGGEST) {
/* 通过函数指针调用函数(旧方式) */
rv = (*aFunction)(aVariable, dataPointer );
} else {
/* 通过函数指针调用函数(新方式) */
rv = aFunction(aVariable, dataPointer );
};
return rv;
}
typedef struct {
int colorSpec;
char *phrase;
} DataINeed;
int TalkJive(int myNumber, void *someStuff)
{
/* 将 void * 转换为该函数所需的指针类型 */
DataINeed *myData = someStuff;
/* talk jive. */
return 5;
}
static DataINeed sillyStuff = { BLUE, "Whatcha talkin 'bout Willis?" };
DoSomethingNice(41, &TalkJive, &sillyStuff);
在某些 C 版本中,调用 DoSomethingNice
时可能不需要在 TalkJive
参数前加 &
符号。有些实现可能要求显式地将参数强制转换为 MyFunctionType
类型,尽管函数签名完全符合 typedef 的定义。
函数指针的应用
函数指针在 C 语言中非常有用,尤其是在实现多态性时。首先,声明一个结构体,结构体的元素是指向各个操作的函数指针,这些操作可以通过多态性来指定。其次,声明一个包含指向上述结构体的指针的基类结构体。通过扩展第二个结构体来定义类,并为类指定特定的数据,使用第一种结构体类型的静态变量,包含与类相关联的函数地址。这种多态性机制在标准库中调用文件 I/O 函数时得到了应用。
类似的机制也可以用于在 C 语言中实现状态机。声明一个结构体,结构体中包含指向函数的指针,用于处理可能发生的状态内事件,并为状态的进入和退出调用函数。该结构体的实例对应一个状态。每个状态都初始化为指向适合该状态的函数的指针。状态机的当前状态实际上是指向这些状态之一的指针。通过改变当前状态指针的值,可以有效地改变当前状态。当某个事件发生时,通过当前状态中的函数指针调用适当的函数。
C 语言中函数指针的实际使用
函数指针主要用于减少 switch
语句的复杂性。以下是使用 switch
语句的示例:
#include <stdio.h>
int add(int a, int b);
int sub(int a, int b);
int mul(int a, int b);
int div(int a, int b);
int main() {
int i, result;
int a = 10;
int b = 5;
printf("Enter the value between 0 and 3: ");
scanf("%d", &i);
switch(i) {
case 0: result = add(a, b); break;
case 1: result = sub(a, b); break;
case 2: result = mul(a, b); break;
case 3: result = div(a, b); break;
}
}
int add(int i, int j) {
return (i + j);
}
int sub(int i, int j) {
return (i - j);
}
int mul(int i, int j) {
return (i * j);
}
int div(int i, int j) {
return (i / j);
}
不使用 switch
语句:
#include <stdio.h>
int add(int a, int b);
int sub(int a, int b);
int mul(int a, int b);
int div(int a, int b);
int (*oper[4])(int a, int b) = {add, sub, mul, div};
int main() {
int i, result;
int a = 10;
int b = 5;
printf("Enter the value between 0 and 3: ");
scanf("%d", &i);
result = oper[i](a, b);
}
int add(int i, int j) {
return (i + j);
}
int sub(int i, int j) {
return (i - j);
}
int mul(int i, int j) {
return (i * j);
}
int div(int i, int j) {
return (i / j);
}
函数指针用于创建结构体成员函数
typedef struct {
int (*open)(void);
void (*close)(void);
int (*reg)(void);
} device;
int my_device_open(void) {
/* ... */
}
void my_device_close(void) {
/* ... */
}
void register_device(void) {
/* ... */
}
device create(void) {
device my_device;
my_device.open = my_device_open;
my_device.close = my_device_close;
my_device.reg = register_device;
my_device.reg();
return my_device;
}
用于实现 this
指针(以下代码必须放在库中)
static struct device_data {
/* ... 这里放置结构体的数据 ... */
};
static struct device_data obj;
typedef struct {
int (*open)(void);
void (*close)(void);
int (*reg)(void);
} device;
static struct device_data create_device_data(void) {
struct device_data my_device_data;
/* ... 这里放置构造函数 ... */
return my_device_data;
}
/* 这里省略了 my_device_open、my_device_close 和 register_device 函数 */
device create_device(void) {
device my_device;
my_device.open = my_device_open;
my_device.close = my_device_close;
my_device.reg = register_device;
my_device.reg();
return my_device;
}
指针构造示例
以下是一些可能有助于创建指针的构造示例:
int i; // 整型变量 'i'
int *p; // 整型指针 'p'
int a[]; // 整型数组 'a'
int f(); // 返回类型为整数的函数 'f'
int **pp; // 指向整型指针的指针 'pp'
int (*pa)[]; // 指向整型数组的指针 'pa'
int (*pf)(); // 指向返回类型为整型的函数的指针 'pf'
int *ap[]; // 整型指针数组 'ap'
int *fp(); // 返回整型指针的函数 'fp'
int ***ppp; // 指向指向整型指针的指针的指针 'ppp'
int (**ppa)[]; // 指向整型数组的指针的指针 'ppa'
int (**ppf)(); // 指向返回类型为整型的函数的指针的指针 'ppf'
int *(*pap)[]; // 指向整型指针数组的指针 'pap'
int *(*pfp)(); // 返回指向整型指针的函数的指针 'pfp'
int **app[]; // 指向整型指针的数组 'app'
int (*apa[])[]; // 指向整型数组的指针的数组 'apa'
int (*apf[])(); // 指向返回类型为整型的函数的指针的数组 'apf'
int ***fpp(); // 返回指向整型指针的指针的函数 'fpp'
int (*fpa())[]; // 返回指向整型数组的指针的函数 'fpa'
int (*fpf())(); // 返回指向返回类型为整型的函数的指针的函数 'fpf'
sizeof
运算符
sizeof
运算符通常用于获取同一函数中先前声明的静态数组的大小。
查找数组的末尾(例如来自 Wikipedia 的缓冲区溢出示例):
#include <stdio.h>
#include <string.h>
int main(int argc, char *argv[]) {
char buffer[10];
if (argc < 2) {
fprintf(stderr, "USAGE: %s string\n", argv[0]);
return 1;
}
strncpy(buffer, argv[1], sizeof(buffer));
buffer[sizeof(buffer) - 1] = '\0';
return 0;
}
要遍历数组中的每个元素,可以使用:
#define NUM_ELEM(x) (sizeof(x) / sizeof(*(x)))
for (i = 0; i < NUM_ELEM(array); i++) {
/* 对 array[i] 做些事情 */
}
需要注意的是,sizeof
运算符只适用于函数中先前定义的内容。编译器会将其替换为一个固定的常数。在这个例子中,buffer
被声明为一个包含 10 个 char
的数组,编译器会在编译时将 sizeof(buffer)
替换为 10(等同于我们直接在代码中硬编码 10)。关于 buffer
长度的信息实际上并没有存储在内存中(除非我们单独跟踪它),也不能从数组/指针本身在运行时获得。
解决方案:数组长度问题
许多函数需要知道它们收到的数组的大小——一个在其他函数中定义的数组。例如:
/* broken.c - 演示一个缺陷 */
#include <stdio.h>
#include <string.h>
#define NUM_ELEM(x) (sizeof(x) / sizeof(*(x)))
int sum(int input_array[]) {
int sum_so_far = 0;
int i;
for (i = 0; i < NUM_ELEM(input_array); i++) { // 这不会工作 - input_array 没有在此函数中定义。
sum_so_far += input_array[i];
}
return sum_so_far;
}
int main(int argc, char *argv[]) {
int left_array[] = {1, 2, 3};
int right_array[] = {10, 9, 8, 7, 6, 5, 4, 3, 2, 1};
int the_sum = sum(left_array);
printf("the sum of left_array is: %d", the_sum);
the_sum = sum(right_array);
printf("the sum of right_array is: %d", the_sum);
return 0;
}
不幸的是,在 C 和 C++ 中,无法从运行时传递的数组中获取其长度,因为(如上所述)数组的大小不会被存储。编译器总是将 sizeof
替换为常量。因此,sum()
函数需要处理超过一个常量长度的数组。
有一些常见的方法来解决这个问题:
- 编写函数时,要求每个数组参数都有一个“长度”参数(类型为
size_t
)。通常我们在调用此函数时使用sizeof
。 - 使用约定,例如使用空字符结尾的字符串来标记数组的结束。
- 替代传递原始数组,传递一个包含数组长度(如
.length
)和数组(或指向第一个元素的指针)的结构体;这类似于 C++ 中的字符串或向量类。
解决方法示例:
/* fixed.c - 演示一种解决方法 */
#include <stdio.h>
#include <string.h>
#define NUM_ELEM(x) (sizeof(x) / sizeof(*(x)))
int sum(int input_array[], size_t length) {
int sum_so_far = 0;
int i;
for (i = 0; i < length; i++) {
sum_so_far += input_array[i];
}
return sum_so_far;
}
int main(int argc, char *argv[]) {
int left_array[] = {1, 2, 3, 4};
int right_array[] = {10, 9, 8, 7, 6, 5, 4, 3, 2, 1};
int the_sum = sum(left_array, NUM_ELEM(left_array)); // 在这里工作,因为 left_array 在此函数中定义
printf("the sum of left_array is: %d", the_sum);
the_sum = sum(right_array, NUM_ELEM(right_array)); // 在这里工作,因为 right_array 在此函数中定义
printf("the sum of right_array is: %d", the_sum);
return 0;
}
值得一提的是,sizeof 运算符有两种变体:sizeof(type)(例如:sizeof(int) 或 sizeof(struct some_structure))
和 sizeof expression(例如:sizeof some_variable.some_field 或 sizeof 1)。