C语言指针

指针的概念

百度百科:指针也就是内存地址,指针变量是用来存放内存地址的变量,在同一CPU构架下(32/64),不同类型的指针变量所占用的存储单元长度是相同的(64是8字节),而存放数据的变量因数据的类型不同,所占用的存储空间长度也不同。有了指针以后,不仅可以对数据本身,也可以对存储数据的变量地址进行操作。

示例如下代码所示:

//64位ubuntu
printf("%ld\n", sizeof(int *));     //任意类型的指针都占8字节
printf("%ld\n", sizeof(char *));
printf("%ld\n", sizeof(double *));
printf("%ld\n", sizeof(short *));
//将会输出4个8

需要注意的是:

int *pc = &ch; //会报错,error: conflicting types for ‘pc’ 类型不兼容

另外不同类型的指针,步长不一样,详见下示例:

int a = 1;
	char ch = 'a';

	int *pa = &a;
	//int *pc = &ch;            //类型不兼容
	char *pc = &ch;

	*pc = 'x';           //等价于 ch = 'x';
	printf("%c\n", ch);

	printf("%p\n", pa);
	printf("%p\n", pc);

	printf("%p\n", pa + 1);    //不同类型的指针,步长不一样 pa加4个字节 pc加1个字节
	printf("%p\n", pc + 1);

输出如下:
在这里插入图片描述

附代码

#include <stdio.h>

int main()
{
	printf("%ld\n", sizeof(int *));     //任意类型的指针都占8字节
	printf("%ld\n", sizeof(char *));
	printf("%ld\n", sizeof(double *));
	printf("%ld\n", sizeof(short *));

	int a = 1;
	char ch = 'a';

	int *pa = &a;
	//int *pc = &ch;            //类型不兼容
	char *pc = &ch;

	*pc = 'x';           //等价于 ch = 'x';
	printf("%c\n", ch);

	printf("%p\n", pa);
	printf("%p\n", pc);

	printf("%p\n", pa + 1);    //不同类型的指针,步长不一样 pa加4个字节 pc加1个字节
	printf("%p\n", pc + 1);

	return 0;
}

指针变量的运算

指针变量保存的是地址,本质上是一个整数,可以进行部分运算,例如加法、减法、比较等,不能对指针变量进行乘法、除法、取余等其他运算,除了会发生语法错误,也没有实际的含义。

指针变量加减运算的结果跟数据类型的长度有关,上一节末尾已给出示例,本节不再赘述。这里只强调说明为何指针直接加减是地址的变化而不是值的变化。
设有指针int *p指向int型变量a:
在这里插入图片描述
若此时,p++(或p+1),指针p会指向0x104(加了int型的4字节步长)。
综上可知,如若比较指针p1和p2,比较的是两指针变量本身的值,即地址,只有两个指针指向的是同一个地址,它们才会相等。

与数组不同的是,C语言并没有规定普通变量的存储顺序,意思是如果连续定义多个变量,它们有可能是连续存储在内存中,也有可能是分散间隔的。
示例:(下面代码在不同电脑输出的值是不一样的)

#include <stdio.h>
int main(){
    int a = 1, b = 2, c = 3;
    int *p = &c;
    int i;
    for(i=0; i<8; i++){
        printf("%d, ", *(p+i) );
    }
    return 0;
}

在本人64ubuntu上输出为:
3, 1, 1, 2, 714628976, 32765, 157841920, 431432819,
也有可能出现3个变量中间是垃圾值被间隔开,也有概率出现刚刚好三个变量顺序挨着。

数组指针

结合之前数组章节的学习,我们知道,数组中的所有元素在内存中是连续排列的,如果一个指针指向了数组中的某个元素,那么加 1 就表示指向下一个元素,减 1 就表示指向上一个元素(实现时需要加取值符号*,详见下述)。
也就是说,数组中的所有元素在内存中是连续排列的,整个数组占用的是一块内存。
在这里插入图片描述
由上图可知,数组名可以认为(严格上来说不是,只是可以认为)是一个指针,指向数组第0个元素。在C语言中,我们将第 0 个元素的地址称为数组的首地址。
需要再次强调的是,数组名和数组首地址并不总是等价,但一般情况下来说,或者对初学者来说可以认为他就是一个指针,指向首元素地址。
示例:

 #include <stdio.h>
int main()
{
    int arr[] = { 11, 22, 66, 99 };
    int len = sizeof(arr) / sizeof(int);  //求数组长度
    int i;
    for(i=0; i<len; i++){
        printf("%d  ", *(arr+i) );  //*(arr+i)等价于arr[i]
    }
    printf("\n");
    return 0;
}

运行会依次输出数组内元素。
若我们定义一个指向数组的指针,即数组指针

int arr[] = { 11, 22, 66, 99  };
int *p = arr;

arr 本身就是一个指针,可以直接赋值给指针变量 p。arr 是数组第 0 个元素的地址,所以int *p = arr;也可以写作int *p = &arr[0];。即arr、p、&arr[0] 这三种写法都是等价的,它们都指向数组第 0 个元素,或者说指向数组的开头。
数组指针指向的是数组中的一个具体元素,而不是整个数组,所以数组指针的类型和数组元素的类型有关,上面的例子中,p 指向的数组元素是 int 类型,所以 p 的类型必须也是int *。
示例数组指针遍历数组元素:

 #include <stdio.h>
int main(){
    int arr[] = { 11, 22, 66, 99 };
    int i, *p = arr, len = sizeof(arr) / sizeof(int);
 
    for(i=0; i<len; i++){
        printf("%d  ", *(p+i) );
    }
    printf("\n");
    return 0;
}

需要注意的是,数组在内存中只是数组元素的简单排列,没有开始和结束标志,在求数组的长度时不能使用sizeof§ / sizeof(int),因为 p 只是一个指向 int 类型的指针,编译器并不知道它指向的到底是一个整数还是一系列整数(数组),所以 sizeof§ 求得的是 p 这个指针变量本身所占用的字节数,而不是整个数组占用的字节数。
综上,我们现在有两种方案来访问数组元素,一种是使用下标,另外一种是使用指针。不同的是,数组名是常量,它的值不能更改,而数组指针是变量,可更改。即,数组名只能指向数组开头首元素,而数组指针可以改变去指向其它元素。
综上所述,那么,问题来了:假设 p 是指向数组 arr 中第 n 个元素的指针,那么 *p++、*++p、(*p)++ 分别是什么意思?
示例分三次编译,三次输出,避免干扰。

 #include <stdio.h>
   int main()
   {
      int arr[] = {11,22,66,99,88};
      int *p = &arr[2];//or int *p = arr + 2;
      //int a = *p++;
      //printf("a = %d\n",a;)//输出66
     //printf("*p++ = %d\n",*p++); //输出99
  
  	  //int a = *++p;
      //printf("a = %d\n",a;)//输出99
      //printf("*++p = %d\n",*++p);//输出88 (此处再次+1)
      
      int a = *p++;
      printf("a = %d\n",a;)//输出66
      printf("*(p)++ = %d\n",(*p)++);//输出67(此处会再次+1故下面会=68)
      printf("arr[2] = %d\n",arr[2]);//输出68 
      return 0;
  }

也就是说,*p++ 等价于 *(p++),表示先取得第 n 个元素的值,再将 p 指向下一个元素,上面已经进行了详细讲解。

*++p 等价于 *(++p),会先进行 ++p 运算,使得 p 的值增加,指向下一个元素,整体上相当于 *(p+1),所以会获得第 n+1 个数组元素的值。

(*p)++ 会先取得第 n 个元素的值,再对该元素的值加 1。假设 p 指向第 0 个元素,并且第 0 个元素的值为 1,执行完该语句后,第 0 个元素的值就会变为2。
总结:前两者是改变指向的数组下标,最后一个是直接改变了指向地址上的元素的值。即,若指针变量带取值符号*在小括号内,则外部的增减符号造成的是值变化,若只对指针变量名进行增减,则造成的是地址的变化
数组指针示例:

#include <stdio.h>

int main()
{
	int a[5] = {1, 2, 3, 4, 5};

	//int (*p)[5] = a;    //错误,因为a是数组首元素地址,不是数组的地址 详见上一篇关于数组地址的相关内容
	int (*p)[5] = &a;     //p是数组指针

	int i;
	for (i = 0; i < 5; i++)
	{	
		printf("%d ", (*p)[i]);   //通过数组指针访问数组
	}

	return 0;
}

字符串指针

因为C语言中没有特定字符串类型,一般都是将字符串放在一个字符数组中。
而此前所述的关于指针和数组的规则同样适用字符数组,除此之外,可以用一个指针指向字符串,示例如下:

 char *str = "hello";
或者:
char *str;//野指针:还未指向具体地址 里面存有垃圾值 char *str = NULL;->空指针
str = "hello";

字符串中的所有字符在内存中是顺序排列的,str指向的是第0个字符,即第0个字符的地址为首地址,又因字符串中每个字符都是char类型,故指针类型也得是char。
那么,两者有什么区别吗?
最根本的区别是在内存中存储区域不一样,字符数组存储在全局数据区或栈区,第二种形式(指针字符串)的字符串存储在常量区。全局数据区和栈区的字符串(也包括其他数据)有读取和写入的权限,而常量区的字符串(也包括其他数据)只有读取权限,没有写入权限。
即字符数组在定义后可以读取和修改每个字符,而指针字符串一旦被定义后就不能被修改,任何赋值都将报错。
示例如下:

 #include <stdio.h>
int main(){
    char *str = "Hello World!";
    str = "hello!";  //正确 要明白这里是修改指针指向的地址 而不是修改内容
    str[3] = 'P';  //错误 这里是试图修改指针指向的字符串内容
 
    return 0;
}

二级指针

一个指针指向另一个指针,就被称为二级指针,即指针的指针。
在这里插入图片描述

int a = 100;
int *p1 = &a;
int **p2 = &p1;//1.指针变量也是变量,一样可以通过取地址符&获得其地址
//2. 每多一级指针前面就多一个星号,常用一二级指针。

指针数组

若一个数组中所有元素都是指针,则称其为指针数组(与上面讲的数组指针要区别开来)。
除了元素类型都是指针外,其余特性和普通数组一样。

 #include <stdio.h>
int main(){
    int a = 66, b = 88, c = 99;
    //定义一个指针数组
    int *arr[] = {&a, &b, &c};
    //定义一个指向指针数组的指针
    int **parr = arr;
    printf("%d, %d, %d\n", *arr[0], *arr[1], *arr[2]);
    printf("%d, %d, %d\n", **(parr+0), **(parr+1), **(parr+2));//上文说过只对指针变量名增变化,故改变的是指向地址
 
    return 0;
}
运行结果:
6, 88, 99
6, 88, 99

指针与二维数组

二维数组在概念上是二维的,但在内存中,所有的数组元素都是顺序排列的。
示意如下:

int a[3][4] = {{0, 1, 2, 3},{4, 5, 6, 7},{8, 9, 10 ,11}};

按常规的二维概念,二维数组啊的分布应该如下图:
在这里插入图片描述
但实际上,在内存中是如下分布:
在这里插入图片描述
即是按行排列的,先放a[0]行,再a[1],a[2]行。因为是int型,则每个元素占4字节,总共占48字节。
定义的时候有个比较绕的地方,如下所示:

//定义一个指向 a 的指针变量 p:
int (*p)[4] = a;

[ ]的优先级高于*,( )是必须要加的,如果直接写为int *p[4],那么就变成int (p[4]),p 就成了一个指针数组(详见前述),而不是二维数组指针,此时会初始化错误。
结合前面所述,应知
1.若p + 1,改变的是地址,此时会前进16个字节,即前进一行的长度,到下一行。
2.
(p+1)表示取地址上的数据,也就是整个第 1 行数据。在定义时或者和 sizeof、& 一起使用时才表示整个数组,出现在表达式中就会被转换为指向数组第 0 个元素的指针。也就是说放在表达式中会被转换为第 1 行数据的首地址,也就是第 1 行第 0 个元素的地址,因为使用整行数据没有实际的含义,编译器遇到这种情况都会转换为指向该行第 0 个元素的指针。
举例如下:

sizeof*(p+1);//输出的是16 代表整行大小,而不是首元素的大小
//但若是输出*(p+1),输出的是首元素的地址

3.(* ( p)),输出的是第0行第0个元素,(* ( p+1)+1)表示第1行第1个元素。

星号前面加类型时是用来声明指针,当用到指针变量上时,就变成一个取值符号,即取指针指向的地址上面的值。

#include <stdio.h>
int main(){
    int a[3][4]={0,1,2,3,4,5,6,7,8,9,10,11};
    int(*p)[4];
    int i,j;
    p=a;
    for(i=0; i<3; i++){
        for(j=0; j<4; j++) printf("%2d  ",*(*(p+i)+j));
        printf("\n");
    }
 
    return 0;
}
运行结果:
 0   1   2   3
 4   5   6   7
 8   9  10  11

指针数组和二维数组指针的区别

两者很容易混淆,两者声明上只是括号位置的不同:
int *(p1[6]); //指针数组,可以去掉括号直接写作 int *p1[5];
int (*p2)[6]; //二维数组指针,不能去掉括号
指针数组和二维数组指针有着本质上的区别:指针数组是一个数组,只是每个元素保存的都是指针,以上面的 p1 为例,在32位环境下它占用 4×6 = 24 个字节的内存。二维数组指针是一个指针,它指向一个二维数组,以上面的 p2 为例,它占用 4 个字节的内存。
说简单点:指针数组是一个数组,只是每个元素保存的都是指针。
二维数组指针是一个指针,它指向一个二维数组。

函数与指针的关系见下一章。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值