指针是指定一些值的地址(即,在存储器中的位置),一个值。指针是保存内存位置的变量。
关于指针,您需要了解四个基本要素:
- 如何申报他们(与地址运算符’
&
“:int *pointer = &variable;
) - 如何分配给他们(
pointer = NULL;
) - 如何引用指针指向的值(称为解除引用,通过使用解除引用运算符’
*
‘value = *pointer;
) - 它们与数组的关系(C中的绝大多数数组都是简单的列表,也称为“1维数组”,但我们将在后面的章节中简要介绍多维数组和一些指针)。
指针可以引用任何数据类型,甚至函数。我们还将讨论指针与文本字符串的关系以及更高级的函数指针概念。
指针一指向变量b。注意,b存储数字,而a存储b的地址(1462)
声明指针
考虑下面的代码片段,它声明了两个指针:
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 ; 结构 MYSTRUCT 德沃夏克; 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 ; 否则 如果 (b == 2 ) myReturn = &val2 ; 否则 如果 (b ==3 ) myReturn = &val3 ; 否则 myReturn = &val4 ; 返回 myReturn ; } struct MyStruct * strPointer ; int * c , * d ; int j ; c = &j ; / *使用&operator * / d = c 指定的指针; / *将一个指针指向另一个* / strPointer = ASillyFunction ( 3 ); / *从函数返回的指针。* /
从函数返回指针时,不要返回指向函数本地值的指针或指向函数参数的指针。当函数退出时,指向局部变量的指针变为无效。在上面的函数中,返回的值指向静态变量。返回指向动态分配的内存的指针也是有效的。
指针解除引用
要访问指针指向的值,请使用*
运算符。另一个运算符,->
运算符与指向结构的指针一起使用。这是一个简短的例子。
int c , d ; int * pj ; struct MyStruct astruct ; struct MyStruct * bb ; c = 10 ; pj = &c ; / * pj指向c * / d = * pj ; / * d被赋值为pj指向的值,10 * / pj = &d ; / *现在指向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
。它们都访问m_aNumber
指向的结构元素bb
。还有一种解除引用指针的方法,将在下一节中讨论。
取消引用指向无效内存位置的指针时,通常会发生错误,导致程序终止。该错误通常被报告为分段错误。导致此问题的常见原因是在尝试取消引用指针之前无法初始化指针。
C以给你足够的绳索来自己挂起而闻名,而指针解除引用就是一个很好的例子。您可以自由编写访问内存的代码,这些代码是您从系统明确请求的内存。很多时候,由于系统内存分配的变幻莫测,内存可能会出现在您的程序中。但是,即使99次执行允许程序无故障运行,第100次执行可能是系统捕获“内存盗窃”并且程序失败的时间。注意确保指针偏移在分配的内存范围内!
声明void *somePointer;
用于声明某些非指定类型的指针。您可以为void指针赋值,但必须先将变量强制转换为某个指定的类型,然后才能取消引用它。指针算法对void *
指针也无效。
指针和数组
到目前为止,我们已经小心翼翼地避免在指针的上下文中讨论数组。指针和数组的交互可能令人困惑,但这里有两个基本的陈述:
- 声明为某种类型数组的变量充当指向该类型的指针。当它本身使用时,它指向数组的第一个元素。
- 指针可以像数组名一样索引。
当数组作为参数传递给函数时,通常会发生第一种情况。该函数将参数声明为指针,但实际参数可以是数组的名称。第二种情况通常在访问动态分配的内存时发生。我们来看看每个例子。在下面的代码中,对calloc()的调用有效地分配了一个struct MyStruct项的数组。
float KrazyFunction ( struct MyStruct * parm1 , int p1size , int bb ) { int ix ; //声明一个整数变量// for (ix = 0 ; ix < p1size ; ix ++ ) { if (parm1 [ ix ] .m_aNumber == bb ) 返回 parm1 [ ix ]。num2 ; } 返回 0.0f ; } / * ... * / struct MyStruct myArray [ 4 ]; #define MY_ARRAY_SIZE(sizeof(myArray)/ sizeof(* myArray)) float v3 ; struct MyStruct * secondArray ; int someSize ; int ix ; / *初始化myArray ... * / v3 = KrazyFunction ( myArray , MY_ARRAY_SIZE , 4 ); / * ... * / secondArray = calloc ( someSize , sizeof (myArray ) ); for (ix = 0 ; ix < someSize ; ix ++ ) { secondArray [ ix ]。m_aNumber = ix * 2 ; secondArray [ ix ]。num2 = .304 * ix * ix ; }
指针和数组名称几乎可以互换使用。也有例外。您不能将新指针值分配给数组名称。数组名称将始终指向数组的第一个元素。在KrazyFunction
上面的函数中,您可以为parm1分配一个新值,因为它只是指向myArray的第一个元素的指针。它对于函数返回指向从作为参数传递给函数的数组中的一个数组元素的函数也是有效的。函数永远不会返回指向局部变量的指针,即使编译器可能不会抱怨。
在向函数声明参数时,声明没有大小的数组变量等同于声明指针。通常这样做是为了强调指针变量将以与数组等效的方式使用的事实。
/ *两个等价的函数定义* / int LittleFunction ( int * paramN ); int LittleFunction ( int paramN [] );
现在我们准备讨论指针算法。您可以向/从指针添加和减去整数值。如果声明myArray是某种类型的数组,则表达式*(myArray+j)
(其中j是整数)等效于myArray[j]
。因此,例如在我们使用表达式secondArray [i] .num2的上述示例中,我们可以将其写成*(secondArray+i).num2
或更简单(secondArray+i)->num2
。
请注意,对于整数和指针的加法和减法,指针的值不会被整数量调整,而是通过数量乘以指针所引用类型的大小(以字节为单位)来调整。一个指针也可以从另一个指针中减去,只要它们指向同一个数组的元素(或者恰好超出数组末尾的位置)。如果您有一个指向数组元素的指针,则该元素的索引是从指针中减去数组名称时的结果。这是一个例子。
struct MyStruct someArray [ 20 ]; struct MyStruct * p2 ; int idx ; 。 / *数组初始化.. * / 。 for (p2 = someArray ; p2 < someArray + 20 ; ++ p2 ) { if (p2 - > num2 > testValue ) break ; } idx = p2 - someArray ;
您可能想知道指针和多维数组如何交互。让我们来看一下这个细节。假设A被声明为float(float A[D1][D2];
)的二维数组,并且pf被声明为指向float的指针。如果pf被初始化为指向A [0] [0],则*(pf + 1)等效于A [0] [1]并且*(pf + D2)等效于A [1] [0]。数组的元素以行主顺序存储。
浮 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是一个数组,idx是一个整数值。表达式myArray [idx]等同于idx [myArray]。第一个相当于*(myArray + idx),第二个相当于*(idx + myArray)。结果是相同的,因为加法是可交换的。
指针可用于预增量或后减量,有时在循环内完成,如下例所示。递增和递减适用于指针,而不适用于指针所引用的对象。换句话说,* pArray ++相当于*(pArray ++)。
长的 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 ; 长 AnArray [ 25 ]; long * pAA ; 的pAA = &AnArray [13 ]; Ĵ = FunctOne ( pStruct , * mValue , 的pAA ); 返回 j ; }
指针和文字字符串
从历史上看,C中的文本字符串已实现为字符数组,字符串中的最后一个字节为零,或者为空字符“\ 0”。大多数C实现都带有一个用于操作字符串的标准函数库。许多更常用的函数都希望字符串是以空字符结尾的字符串。要使用这些函数,需要包含标准C头文件“string.h”。
静态声明的初始化字符串看起来类似于以下内容:
static const char * myFormat = “总金额:%d” ;
变量myFormat
可以视为21个字符的数组。在作为数组中第21项的“d”之后,在字符串的末尾添加了隐含的空字符(’\ 0’)。您还可以按如下方式初始化数组的各个字符:
static const char myFlower [] = { 'P' , 'e' , 't' , 'u' , 'n' , 'i' , 'a' , '\ 0' };
初始化字符串数组通常按如下方式完成:
static const char * myColors [] = { “Red” , “Orange” , “Yellow” , “Green” , “Blue” , “Violet” };
特殊长字符串的初始化可以跨源代码行分割,如下所示。
静态 字符 * longString = “你好。我的名字是鲁道夫,我作为驯鹿工作” “圣诞节期间在北极。我的老板是一个真正膨胀的家伙。” “他喜欢给每个人送礼物。” ;
与字符串一起使用的库函数将在后面的章节中讨论。
指向函数的指针
C还允许您创建指向函数的指针。指向函数的指针可能会变得相当混乱。将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 ); }; 返回 rv ; } typedef struct { int colorSpec ; char * 短语; } DataINeed ; int TalkJive ( int myNumber , void * someStuff ) { / * recast void *到此函数特别需要的指针类型* / DataINeed * myData = someStuff ; / *谈jive。* / 返回 5 ; } 静态 DataINeed sillyStuff = { BLUE , “干麻说话“回合威利斯?” }; / * ... * / DoSomethingNice ( 41 , &TalkJive , &sillyStuff );
某些版本的C可能不需要TalkJive
在DoSomethingNice
调用中的参数前面加上&符号。某些实现可能需要专门将参数转换为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 (“输入0到3之间的值:” ); scanf (“%d” ,&i ); switch (i ) { case 0 : result = add (a ,b ); 打破; 案例 1 : 结果 = sub (a ,b ); 打破; 案例 2 : 结果 = mul (a ,b ); 打破; 案件 3 : result = div (a ,b ); 打破; } } INT 加载(诠释 我, INT Ĵ ) { 返回 (我+ Ĵ ); } int sub (int i , int j ) { return (i - j ); } int mul (int i , int j ) { 返回 (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 (“输入0到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 ); } 设备; 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 { / * ...这里有结构数据... * / }; static struct device_data obj ; typedef struct { int (* open )(void ); void (* close )(void ); int (* reg )(void ); } 设备; 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功能* / 设备 create_device (空隙) { 设备 my_device ; my_device 。open = my_device_open ; my_device 。close = my_device_close ; my_device 。reg = register_device ; my_device 。reg (); 返回 my_device; }
指针构造的示例
下面是一些可能有助于创建指针的示例构造。
int i ; //整数变量' i'int * p ; //指针'p'到整数 int a []; //数组'a'的整数 int f (); //函数'f',返回值为integer int ** pp ; //指针'pp'指向整数 int (* pa )[]; //指向'pa'到整数数组 int (* pf )(); //指针'pf'到一个返回值为integer int * ap [] 的函数; //数组'ap'指向整数的指针 int * fp (); // function'fp'返回一个指向整数的指针 int *** ppp ; //指针'ppp'指向指向整数 int 的指针(** ppa )[]; //指向'ppa'指向整数数组的指针 int (** ppf )(); //指向'ppf'指向函数的指针,返回值类型为integer int * (* pap )[]; //指针'pap'指向整数 int * (* pfp)(); //指针'pfp'以函数的形式返回值类型指针指向一个整数 int ** app []; //指向'app'的指针指向指向整数值的指针 int (* apa [])[]; //数组指针'apa'到整数数组 int (* apf [])(); //返回值为integer int *** fpp ()的 函数的指针'apf'数组 // function'fpp'返回指向指向int int (* fpa ())[] 的指针的指针; // function'fpa',返回值为整数数组的指针 int (* fpf ())(); // function'fpf',返回一个返回整数的函数指针的返回值
sizeof
sizeof运算符通常用于引用先前在同一函数中声明的静态数组的大小。
要查找数组的结尾(例如维基百科:缓冲区溢出):
/ * better.c - 演示了解决问题的一种方法* / #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 ]); 返回 1 ; } strncpy (buffer , argv [ 1 ], sizeof (buffer )); 缓冲[sizeof (buffer ) - 1 ] = '\ 0' ; 返回 0 ; }
要迭代数组的每个元素,请使用
#define NUM_ELEM(x)(sizeof(x)/ sizeof(*(x))) for ( i = 0 ; i < NUM_ELEM (array ); i ++ ) { / *用array [i]做一些事情* / ; }
请注意,sizeof
运算符仅适用于先前在同一函数中定义的内容。编译器用一些固定的常数替换它。在这种情况下,buffer
在同一个函数中声明为10个字符串的数组,编译时编译器替换sizeof(buffer)
为数字10(相当于我们将代码硬代码10代入代码sizeof(buffer)
)。有关长度的信息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 , 字符 * 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 ( “left_array的总和是:%d” , the_sum ); the_sum = sum ( right_array ); printf ( “right_array的总和是:%d” , the_sum ); 返回 0 ; }
不幸的是,(在C和C ++中)无法从运行时传入的数组中获取数组的长度,因为(如上所述)数组的大小不会存储在任何地方。编译器总是用常量替换sizeof。这个sum()例程需要处理的不仅仅是数组的一个常量长度。
有一些常见的方法可以解决这个问题:
- 对每个数组参数写入要求的函数“length”参数(其类型为“size_t”)。(通常我们在调用此函数的位置使用sizeof)。
- 使用约定(例如以空值终止的字符串)来标记数组的结尾。
- 传递一个包含数组长度的结构(例如“。length”)以及数组(或指向第一个元素的指针),而不是传递原始数组; 类似于C ++中的
string
或vector
类。
/ * 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 , 炭 * 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中定义( “left_array的总和是:%d” , the_sum ); the_sum = sum ( right_array , NUM_ELEM (right_array ) ); //在这里工作,因为right_array在这个函数 printf中定义( “right_array的总和是:%d” , the_sum ); 返回 0 ; }
值得一提的是,sizeof运算符有两个变体:sizeof(type)(例如:sizeof(int)或sizeof(struct some_structure))和sizeof 表达式(例如:sizeof some_variable.some_field或sizeof 1)。
猜你想读:《C编程.中级C》3.内存管理