C语言指针的用法

2023-08-11   


  学习 C 语言的指针既简单又有趣。通过指针,可以简化一些 C 编程任务的执行,还有一些任务,如动态内存分配,没有指针是无法执行的。所以,想要成为一名优秀的 C 程序员,学习指针是很有必要的。下面是相关的知识,欢迎阅读。

  (1)关于指针与数组的存储

  a、指针和数组在内存中的存储形式

  数组p[N]创建时,对应着内存中一个数组空间的分配,其地址和容量在数组生命周期内一般不可改变。数组名p本身是一个常量,即分配数组空间的地址值,这个值在编译时会替换成一个常数,在运行时没有任何内存空间来存储这个值,它和数组长度一起存在于代码中(应该是符号表中),在链接时已经制定好了;而指针*p创建时,对应内存中这个指针变量的空间分配,至于这个空间内填什么值即这个指针变量的值是多少,要看它在程序中被如何初始化,这也决定了指针指向哪一块内存地址。

  b、指针和数组的赋值与初始化

  根据上文,一般情况下,数组的地址不能修改,内容可以修改;而指针的内容可以修改,指针指向的内容也可以修改,但这之前要为指针初始化。

  如:

  int p[5];

  p=p+1; 是不允许的

  而p[0]=1; 是可以的;

  //

  int *p;

  p=p+1; 是允许的

  p[0]=1; 是不允许的,因为指针没有初始化;

  //

  int i;

  int *p=&i;

  p[0]=1; 是允许的;

  对于字符指针还有比较特殊的情况。

  如:

  char * p="abc";

  p[0]='d'; 是不允许的

  为什么初始化了的字符指针不能改变其指向的内容呢?这是因为p指向的是“常量”字符串,字符串"abc"实际是存储在程序的静态存储区的,因此内容不能改变。这里常量字符串的地址确定在先,将指针指向其在后。

  而

  char p[]="abc";

  p[0]='d'; 是允许的

  这是因为,这个初始化实际上是把常量直接赋值给数组,即写到为数组分配的内存空间。这里数组内存分配在先,赋值在后。

  (2)关于一些表达式的含义

  char *p, **p, ***p;

  char p[],p[][],p[][][];

  char *p[],*p[][],**p[],**p[][],*(*p)[],(**p)[],(**p)[][];

  能清晰地知道以上表达式的含义吗?(知道的去死!)

  第一组:char *p, **p, ***p;

  分别为char指针;char*指针,即指向char*类型数据地址的指针;char**指针,即指向char**类型数据的指针;他们都是占4字节空间的指针。

  如:

  char c='a';

  char *p=&c;

  char **p1=&p;

  char ***p2=&p1;

  cout<<***p2<<endl;< p="">

  第二组:char p[],p[][],p[][][];

  分别为一维,二维和三维char型数组,即数组,数组的数组,<数组的数组>的数组。可以如下的方式进行初始化:

  char pp[3]="ab";

  char pp1[3][3]="ab";

  char pp2[3][3][3]="ab";

  现在我们尝试使用第二组三个数组名对应为第一组三个指针赋值,直接赋值的结果如下:

  p=pp; //正确

  p1=pp1; //错误

  p2=pp2; //错误

  为什么p1和p2的赋值会出错呢?原因是数组名为给指针赋值的规则不是递归的\\\',即数组的数组可以为数组的指针赋值,而不可以为指针的指针赋值。这里先了解到这个抽象的规则,下面讲完第三组表达式,等我们知道数组的指针和指针的数组如何书写后再对这一问题举例说明。

  第三组:char *p[],*p[][],**p[],**p[][],*(*p)[],(**p)[],(**p)[][];

  这一类表达式的解析方法如下:

  首先把整个表达式分为三部分,

  数据类型和星号部分+p或括号内内容部分+中括号部分

  如:char *(*p)[]分为char* ,(*p) 和 []

   “char*”表示最内层存储的数据类型“(*p)”表示最外层指针“[]”表示中间层数组(维数=中括号数目),因此上式表示一个一维数组的指针p,数组中的元素的数据类型是指针char*。同理,char (**p)[][]表示,一个二维数组的指针的指针,数组元素的数据类型是char。这里如果表达式中间没有括号(如**p[]),则实际上是一个数组,如果最右没有中括号(如**p),则实际上是一个指针。下面通过赋值表达式来理解这些表达式的含义:

  char c='a';

  char *pc=&c;

  char *p[3],*p1[3][3],**p2[3],**p3[3][3],*(*p4)[3],(**p5)[3],(**p6)[3][3],(*(*p7))[3];

  p[1]=pc;

  p1[0][0]=pc;

  p2[0]=&pc;

  p3[0][0]=&pc;

  (*p4)[0]=pc;

  (**p5)[0]=c;

  (**p6)[0][0]=c;

  (**p7)[0]=c;

  注意,(*(*p7))[3]和(**p5)[3]是等价的。

  这里再继续上一小节讲一下数组名给指针赋值的问题。

  事实上可以对等赋值的数组和指针关系如下(——>表示“赋值给”):

  数组——>指针 : p[]——>*p

  指针的数组——>指针的指针 : *p[]——>**p

  指针的指针的数组的——>指针的指针的指针 : **p[]——>***p

  。。。。。。

  或

  数组的数组——>数组的指针 : p[][]——>(*p)[]

  数组的数组的数组的——>数组的数组的指针 : p[][][]——>(*p)[][]

  总之,最外层的数组可以转换指针,往内层不递归。

  (3)关于上述表达式的长度

  求一个表达式的“长度”,首先分清表达式实际表示的是一个数组还是一个指针;如果是指针,则长度为4byte;如果为数组则要计算实际存储的总元素个数和元素的数据类型。另外要注意要求的是数组元素个数还是数组总字节数;

  如:

  *(*p)[3][3]

  由上文可知上式表示一个指针,因此长度为4byte;而

  **p3[3][3]

  表示一个二维数组,数组元素类型为指针的指针,因此长度为3*3*4=36;

  注意,标准C中sizeof函数求得的是总字节数而非数组长度。

  (4)关于函数的指针返回值和指针参数

  指针作为返回值要注意的地方是不要返回局部数据的指针。

  如:

  char * fun(void)

  

  char i='a';

  return (&i);

  

  调用函数fun得不到值'a',原因是函数返回后,局部数据(在栈中)被析构,数据内存被回收,指针指向的数据没有意义;

  可以改为:

  char * fun(void)

  

  char i='a';

  char *p=(char *)malloc(5);

  If(p!=NULL) p[0]=i, p[1]='';

  return (p);

  

  这里使用malloc分配了内存(在堆中)在函数返回后依然有效。

  这里还没完,因为有一天我使用了下面的代码:

  char * fun(void)

  

  char *p="abc";

  return (p);

  

  发现虽然p定义为局部变量,但返回也是正确的。还记得上面讲到的常量字符串存储位置吗?指针p实际指向了静态数据区,这里的char *p相当于const char *p,这样的数据在函数返回后是不会被立刻回收掉的。

  指针作为参数主要用于修改指针指向的数据内容,但修改指针的值无效,

  如

  char * fun(char *p)

  

  char i='a';

  p=(char *)malloc(5);

  p[0]=i;

  return p;

  

  因为传递的是一个指针副本(形参指针和实参指针的值相同,但不是同一个指针),不会影响调用方的实参值。(诡异的vs2012貌似可以通过形参将实参的值改掉!不过还是不要冒这个险为好了)。

  (5)关于const修饰符

  const修饰符用于指针时也非常纠结。

  首先要分清const char *p和char* const p。

  const char *p是指向const对象的指针,即对象是只读的,而指针不是。使用const对象的指针要注意两点:

  一是不能将其赋值给非const对象的指针,

  如:

  const char* p;

  char *p1=p; //不允许的

  当然,直接使用非const指针指向const对象也是不合法的,

  如:

  const char c;

  char *p1=&c;//不允许的,

  这是要避免通过上述p1改变const对象的值。

  二是可以将非const对象的地址赋值给指向const对象的指针,但是试图使用这个指针改变变量的值是非法的,

  如:

  char c='a';

  const char* p=&c;//允许的

  *p='b';//不允许的

  char* const p是const指针,即指向对象可编辑,但指针本身不可修改,这一点类似于数组。

  如:

  char c='a';

  char* const p=&c;

  *p='b';//允许的

  p++;//不允许的

  区分两者的方法是,看const是否靠近指针名,如果是则为const指针,否则为const对象。这个助记方法的前提是char要和*号靠在一起,因为const char* p=char const *p。

  另外,还有const char* const p,自然是指向const对象的const指针了。

  (6)关于指针函数

  首先注意指针函数和函数指针的区别,前者是指“返回指针的函数”,这在上文中有提到,而后者是指“指向函数的指针”。

  函数指针的定义方法为,将“函数名”替换为“(*函数指针名)”,

  如:

  指向一个声明为void fun(int a)的函数指针可以定义为 void (*pFun)(int a)或void (*pFun)(int ),注意这里函数指针pFun只能指向和fun有相同返回类型(void)和参数类型(int)的一类函数,另外定义中()也不是摆设,去掉()会被看做是返回值为void*类型的函数声明。举个例子:

  void fun(int a)

  

  cout<<a<<endl;< p="">

  

  int main()

  

  void (*pFun)(int);

  pFun=&fun; //(1)

  *(pFun)(1); //(2)

  

  事实上,上式中的(1)(2)行做如下几种替换也是正确的:

  a、pFun=fun;

  pFun(1);

  b、pFun=&fun;

  pFun(1);

  c、pFun=fun;

  *(pFun)(1);

  如果有什么疑问的话,可以接着尝试用如下方式直接调用函数fun:

  (*fun)(1);

  运行的结果也是正确的!这要怎么解释呢?

  其实,fun不仅仅作为函数名,它同pFun一样也是一个函数指针,只不过是函数指针常量。为了书写方便,c语言开发者允许将函数指针调用直接写成类似fun()的形式,同样函数指针变量赋值也可以写成类似pFun=&fun的形式。值得注意的是,函数声明的格式还是比较严格的,如:

  void fun(int ); //不能写成void (*fun)(int )。

  同样,

  void (*pFun)(int ); //不能写成void pFun(int )。

  为了方便,我们还可以定义函数指针类型,针对上述例子的定义方法如下:

  typedef void (*PFUN)(int);

  这样一来我们就可以用

  PFUN pFun;

  来声明一个函数指针了。

  有了函数指针之后,函数的参数也可以设为某一类函数类型。

  如:

  typedef void (*PFUN)(int);

  void fun(int a)

  

  cout<<a<<endl;< p="">

  

  void topfun(PFUN f1, int a)

  

  f1(a);

  

  int main()

  

  topfun(fun,1);

  return 1;

  

  指针可以说是集C语言精华之所在,一个C语言达人怎么可以不会指针呢。




相关内容:

  1. C语言中的指针解读
  2. 空指针到底是什么
  3. c语言指针面试常见问题
  4. 在C语言中"指针和数组等价"到底是什么意思?
  5. C语言中一个结构不能包含指向自己的指针吗
  6. C语言中一个结构不能包含指向自己的指针吗