C 编程/指针和数组
一个指针是一个值,它指定某个值的地址(即内存中的位置)。指针是存储内存位置的变量。
关于指针,你需要了解四个基本内容
- 如何声明它们(使用地址运算符 '
&
':int *pointer = &variable;
) - 如何给它们赋值(
pointer = NULL;
) - 如何引用指针指向的值(称为 *解引用*,使用解引用运算符 '
*
':value = *pointer;
) - 它们与数组的关系(C 中绝大多数数组都是简单的列表,也称为“一维数组”,但我们将在 后面的章节 中简要介绍一些指针的多维数组)。
指针可以引用任何数据类型,甚至函数。我们还将讨论指针与文本字符串的关系以及更高级的函数指针概念。
考虑以下代码片段,它声明了两个指针
struct MyStruct {
int m_aNumber;
float num2;
}
int main()
{
int *pJ2;
struct MyStruct *pAnItem;
}
第 1-4 行定义了一个 结构。第 8 行声明一个指向一个int的变量,第 9 行声明一个指向具有结构 MyStruct 的东西的变量。因此,要将一个变量声明为指向某个类型而不是包含某个类型的东西,星号(*
)将放在变量名前面。
在下面,第 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; /* pointer assigned using & operator */
d = c; /* assign one pointer to another */
strPointer = ASillyFunction( 3 ); /* pointer returned from a function. */
从函数返回指针时,不要返回指向函数局部变量的值的指针,也不要返回指向函数参数的指针。指向局部变量的指针在函数退出时变得无效。在上面的函数中,返回的值指向一个静态变量。返回指向动态分配内存的指针也是有效的。
要访问指针指向的值,使用 *
运算符。另一个运算符 ->
与指向结构的指针一起使用。以下是一个简短的示例。
int c, d;
int *pj;
struct MyStruct astruct;
struct MyStruct *bb;
c = 10;
pj = &c; /* pj points to c */
d = *pj; /* d is assigned the value to which pj points, 10 */
pj = &d; /* now points to d */
*pj = 12; /* d is now 12 */
bb = &astruct;
(*bb).m_aNumber = 3; /* assigns 3 to the m_aNumber member of astruct */
bb->num2 = 44.3; /* assigns 44.3 to the num2 member of astruct */
*pj = bb->m_aNumber; /* equivalent to d = astruct.m_aNumber; */
表达式 bb->m_aNumber
与 (*bb).m_aNumber
完全等效。它们都访问 bb
指向的结构的 m_aNumber
元素。还有一种解引用指针的方法,将在下一节中讨论。
解引用指向无效内存位置的指针时,通常会导致错误,从而导致程序终止。该错误通常报告为分段错误。一个常见的原因是尝试解引用指针之前未对其进行初始化。
C 以给你足够的绳索让你吊死自己而闻名,指针解引用就是一个典型的例子。你可以随意编写访问超出你明确从系统请求的内存的代码。而且很多时候,由于系统内存分配的反复无常,这些内存可能看起来对你的程序可用。但是,即使 99 次执行都允许你的程序运行无误,第 100 次执行可能是系统捕获你的“内存窃取”并导致程序失败的时候。请务必确保你的指针偏移量在分配内存的范围内!
声明 void *somePointer;
用于声明一个某种非指定类型的指针。你可以给 void 指针赋值,但必须将变量强制转换为指向某种指定类型的指针,然后才能解引用它。void 指针也不支持指针算术运算。
到目前为止,我们一直小心地避免在指针的上下文中讨论数组。指针和数组的交互可能令人困惑,但这里有关于它们的两个基本陈述
- 声明为某种类型数组的变量充当指向该类型的指针。当单独使用时,它指向数组的第一个元素。
- 指针可以像数组名称一样被索引。
第一种情况通常发生在将数组作为参数传递给函数时。函数将参数声明为指针,但实际参数可能是数组的名称。第二种情况通常发生在访问动态分配的内存时。
让我们看看每个示例。在以下代码中,对 calloc()
的调用有效地分配了一个 struct MyStruct 项的数组。
struct MyStruct {
int someNumber;
float otherNumber;
};
float returnSameIfAnyEquals(struct MyStruct *workingArray, int size, int bb)
{
/* Go through the array and check if any value in someNumber is equal to bb. If
* any value is, return the value in otherNumber. If no values are equal to bb,
* return 0.0f. */
for (int i = 0; i < size; i++) {
if (workingArray[i].someNumber == bb ) {
return workingArray[i].otherNumber;
}
}
return 0.0f;
}
// Declare our variables
float someResult;
int someSize;
struct MyStruct myArray[4];
struct MyStruct *secondArray; // Notice that this is a pointer
const int ArraySize = sizeof(myArray) / sizeof(*myArray);
// Initialization of myArray occurs
someResult = returnSameIfAnyEquals(myArray, ArraySize, 4);
secondArray = calloc(someSize, sizeof(struct MyStruct));
for (int i = 0; i < someSize; i++) {
/* Fill secondArray with some data */
secondArray[i].someNumber = i * 2;
secondArray[i].otherNumber = 0.304f * i * i;
}
指针和数组名称几乎可以互换使用;但是,也有一些例外。你不能给数组名称赋予新的指针值。数组名称将始终指向数组的第一个元素。在函数 returnSameIfAnyEquals
中,你可以给 workingArray 赋予一个新值,因为它只是一个指向 workingArray 的第一个元素的指针。对于函数从作为函数参数传递的数组中返回指向数组元素之一的指针也是有效的。函数永远不应该返回指向局部变量的指针,即使编译器可能不会抱怨。
在声明函数的参数时,声明没有大小的数组变量等同于声明一个指针。这通常是为了强调指针变量将以类似于数组的方式使用。
/* Two equivalent function prototypes */
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;
/* array initialization .. */
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]。数组的元素以行优先顺序存储。
float A[6][8];
float *pf;
pf = &A[0][0];
*(pf+1) = 1.3; /* assigns 1.3 to A[0][1] */
*(pf+8) = 2.3; /* assigns 2.3 to A[1][0] */
让我们看看一个稍微不同的问题。我们想要一个二维数组,但我们不需要所有行都具有相同的长度。我们要做的是声明一个指针数组。下面的第二行将 A 声明为一个指针数组。每个指针都指向一个浮点数。以下是一些适用的代码
float linearA[30];
float *A[6];
A[0] = linearA; /* 5 - 0 = 5 elements in row */
A[1] = linearA + 5; /* 11 - 5 = 6 elements in row */
A[2] = linearA + 11; /* 15 - 11 = 4 elements in row */
A[3] = linearA + 15; /* 21 - 15 = 6 elements */
A[4] = linearA + 21; /* 25 - 21 = 4 elements */
A[5] = linearA + 25; /* 30 - 25 = 5 elements */
*A[3][2] = 3.66; /* assigns 3.66 to linearA[17]; */
*A[3][-3] = 1.44; /* refers to linearA[12];
negative indices are sometimes useful. But avoid using them as much as possible. */
我们在此还注意到数组索引的有趣之处。假设 myArray
是一个数组,i
是一个整数值。表达式 myArray[i]
等同于 i[myArray]
。第一个等同于 *(myArray+i)
,第二个等同于 *(i+myArray)
。由于加法是可交换的,所以它们最终是一样的。
指针可以与前置递增或后置递减一起使用,有时在循环中完成,如下例所示。递增和递减适用于指针,而不是指针所指向的对象。换句话说,*pArray++
等效于 *(pArray++)
。
long myArray[20];
long *pArray;
int i;
/* Assign values to the entries of 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)
{
/* do some stuff ... */
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 already holds the address that the pointer will point to; there is no need to get the address of anything.*/
return j;
}
从历史上看,C 中的文本字符串已实现为字符数组,字符串中的最后一个字节为零或空字符 '\0'。大多数 C 实现都附带一个用于操作字符串的标准库函数。许多更常用的函数期望字符串是空终止的字符字符串。要使用这些函数,需要包含标准 C 头文件 "string.h"。
静态声明的初始化字符串将类似于以下内容
static const char *myFormat = "Total Amount Due: %d";
变量 myFormat
可以被视为一个包含 21 个字符的数组。在 'd' 之后作为数组中的第 21 个项目,隐含地添加了一个空字符 ('\0') 到字符串的末尾。你也可以按如下方式初始化数组的各个字符
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) {
/* function returning integer pointer, not pointer to function */
return &Z;
}
int get_Z(int x) {
return Z;
}
int (*function_pointer_to_Z)(int); // pointer to function taking an int as argument and returning an 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 *); /* a typedef for a function pointer */
#define THE_BIGGEST 100
int DoSomethingNice( int aVariable, MyFunctionType aFunction, void *dataPointer )
{
int rv = 0;
if (aVariable < THE_BIGGEST) {
/* invoke function through function pointer (old style) */
rv = (*aFunction)(aVariable, dataPointer );
} else {
/* invoke function through function pointer (new style) */
rv = aFunction(aVariable, dataPointer );
};
return rv;
}
typedef struct {
int colorSpec;
char *phrase;
} DataINeed;
int TalkJive( int myNumber, void *someStuff )
{
/* recast void * to pointer type specifically needed for this function */
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 中实现 状态机。定义一个包含函数指针的结构,用于处理可能在状态内发生的事件,以及在进入和退出状态时要调用的函数。此结构的实例对应于一个状态。每个状态都使用指向适用于该状态的函数的指针进行初始化。状态机的当前状态实际上是指向其中一个状态的指针。更改当前状态指针的值实际上会更改当前状态。当发生某些事件时,将通过当前状态中的函数指针调用相应的函数。
函数指针主要用于降低 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;
}
用于实现此指针(以下代码必须放在库中)。
static struct device_data
{
/* ... here goes data of structure ... */
};
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;
/* ... here goes constructor ... */
return my_device_data;
}
/* here I omit the my_device_open, my_device_close and register_device functions */
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; // integer variable 'i'
int *p; // pointer 'p' to an integer
int a[]; // array 'a' of integers
int f(); // function 'f' with return value of type integer
int **pp; // pointer 'pp' to a pointer to an integer
int (*pa)[]; // pointer 'pa' to an array of integer
int (*pf)(); // pointer 'pf' to a function with return value integer
int *ap[]; // array 'ap' of pointers to an integer
int *fp(); // function 'fp' which returns a pointer to an integer
int ***ppp; // pointer 'ppp' to a pointer to a pointer to an integer
int (**ppa)[]; // pointer 'ppa' to a pointer to an array of integers
int (**ppf)(); // pointer 'ppf' to a pointer to a function with return value of type integer
int *(*pap)[]; // pointer 'pap' to an array of pointers to an integer
int *(*pfp)(); // pointer 'pfp' to function with return value of type pointer to an integer
int **app[]; // array of pointers 'app' that point to pointers to integer values
int (*apa[])[]; // array of pointers 'apa' to arrays of integers
int (*apf[])(); // array of pointers 'apf' to functions with return values of type integer
int ***fpp(); // function 'fpp' which returns a pointer to a pointer to a pointer to an int
int (*fpa())[]; // function 'fpa' with return value of a pointer to array of integers
int (*fpf())(); // function 'fpf' with return value of a pointer to function which returns an integer
sizeof 运算符通常用于引用在同一函数中较早声明的静态数组的大小。
要找到数组的结尾(来自 wikipedia:Buffer overflow 的示例)
#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++ )
{
/* do something with array[i] */
;
}
请注意,sizeof
运算符仅对在同一函数中较早定义的内容起作用。编译器会将其替换为某些固定的常数。在本例中,buffer
较早地在同一函数中声明为包含 10 个 char 的数组,编译器会将 sizeof(buffer)
在编译时替换为数字 10(相当于我们在代码中将 sizeof(buffer)
硬编码为 10)。有关 buffer
长度的信息实际上没有存储在内存中的任何位置(除非我们单独跟踪它),并且无法在运行时从数组/指针本身以编程方式获取。
通常,函数需要知道传入数组的大小 - 在其他函数中定义的数组。例如,
/* broken.c - demonstrates a flaw */
#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++ ) // WON'T WORK -- input_array wasn't defined in this function.
{
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++ 中的
string
或vector
类。
/* fixed.c - demonstrates one work-around */
#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) ); // works here, because left_array is defined in this function
printf( "the sum of left_array is: %d", the_sum );
the_sum = sum( right_array, NUM_ELEM(right_array) ); // works here, because right_array is defined in this function
printf( "the sum of right_array is: %d", the_sum );
return 0;
}
值得一提的是 sizeof 运算符有两种变体sizeof (类型)(例如sizeof (int)或sizeof (struct some_structure)) 和sizeof 表达式(例如sizeof some_variable.some_field或sizeof 1).
- "常见的指针陷阱" 由 Dave Marshall 撰写