深入浅出C语言:(三)C 语言数组指针(指向数组的指针)

深入浅出C语言 专栏收录该内容
20 篇文章 9 订阅

目录

一、C 语言数组指针(指向数组的指针)

二、C 语言字符串指针(指向字符串的指针)

三、C 语言指针数组(数组每个元素都是指针)

四、二维数组指针(指向二维数组的指针)

五、指针数组和二维数组指针的区别:

六、常见指针变量的例子集合:见下图


一、C 语言数组指针(指向数组的指针)

定义数组时,要给出数组名和数组长度,数组名可以认为是一个指针,它指向数组的第 0 个元素。 在 C 语言中,我们将第 0 个元素的地址称为数组的首地址。 以上面的数组为例,下图是 arr 的指向:

#include <stdio.h>
int main(){
int arr[] = { 99, 15, 100, 888, 252 };
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;
}

运行结果:
99 15 100 888 252

② 定义一个指向数组的指针:

int arr[] = { 99, 15, 100, 888, 252 };
int *p = arr;

       arr 本身就是一个指针,可以直接赋值给指针变量 p。 arr 是数组第 0 个元素的地址,所以 int *p = arr;也可以写作int *p = &arr[0];。也就是说, arr、 p、 &arr[0] 这三种写法都是等价的,它们都指向数组第 0 个元素,或者说指向数组的开头。
③ 两种方案来访问数组元素:
1) 使用下标
也就是采用 arr[i] 的形式访问数组元素。如果 p 是指向数组 arr 的指针,那么也可以使用 p[i] 来访问数组元素,它等价于 arr[i]。
2) 使用指针
也就是使用 *(p+i) 的形式访问数组元素。另外数组名本身也是指针,也可以使用 *(arr+i) 来访问数组元素,它等价于 *(p+i)。

④ 关于数组指针的谜题:

*和++优先级(一样,从右往左)

假设 p 是指向数组 arr 中第 n 个元素的指针,那么 *p++、 *++p、 (*p)++ 分别是什么意思呢?
       *p++ 等价于 *(p++),表示先取得第 n 个元素的值,再将 p 指向下一个元素。

       *++p 等价于 *(++p),会先进行 ++p 运算,使得 p 的值增加,指向下一个元素,整体上相当于 *(p+1),所以会获得第 n+1 个数组元素的值。
       (*p)++ 就非常简单了,会先取得第 n 个元素的值,再对该元素的值加 1。假设 p 指向第 0 个元素,并且第 0个元素的值为 99,执行完该语句后,第 0 个元素的值就会变为 100。

二、C 语言字符串指针(指向字符串的指针)

① 字符串数组:

char str[] = "http://c.biancheng.net";
int len = strlen(str), i;
//直接输出字符串
printf("%s\n", str);
//每次输出一个字符
for(i=0; i<len; i++){
printf("%c", str[i]);
}

② 字符串指针:

C 语言还支持另外一种表示字符串的方法,就是直接使用一个指针指向字符串:

char *str = "http://c.biancheng.net";

char *str;
str = "http://c.biancheng.net";

上面两个相同

字符串中的所有字符在内存中是连续排列的, str 指向的是字符串的第 0 个字符。

char *str = "http://c.biancheng.net";
int len = strlen(str), i;

//直接输出字符串
printf("%s\n", str);

//使用*(str+i)
for(i=0; i<len; i++){
printf("%c", *(str+i));

//使用str[i]
for(i=0; i<len; i++){
printf("%c", str[i]);
}

以上三种均可

:都可以使用*或[ ]获取单个字符,这两种表示字符串的方式是不是就没有区别了呢?
:有!它们最根本的区别是在内存中的存储区域不一样字符数组存储在全局数据区栈区,第二种形式的字符串存储在常量区。全局数据区和栈区的字符串(也包括其他数据)有读取和写入的权限,而常量区的字符串(也包括其他数据)只有读取权限,没有写入权限。

三、C 语言指针数组(数组每个元素都是指针)

一个数组中的所有元素保存的都是指针,那么我们就称它为指针数组
此处一定要类比普通的指针使用方法

int a = 16, b = 932, c = 100;

//定义一个指针数组
int *arr[3] = {&a, &b, &c};//也可以不指定长度,直接写作 int *parr[]

//定义一个指向指针数组的指针
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));

运行结果:
16, 932, 100
16, 932, 100

       arr 是一个指针数组,它包含了 3 个元素,每个元素都是一个指针,在定义 arr 的同时,我们使用变量 a、 b、 c 的地址对它进行了初始化,这和普通数组是多么地类似。
       parr 是指向数组 arr 的指针,确切地说是指向 arr 第 0 个元素的指针,它的定义形式应该理解为 int *(*parr),括号中的*表示 parr 是一个指针,括号外面的 int *表示 parr 指向的数据的类型。 arr 第 0 个元素的类型为 int *,所以在定义 parr 时要加两个 *。
       第一个 printf() 语句中, arr[i] 表示获取第 i 个元素的值,该元素是一个指针,还需要在前面增加一个 * 才能取得它指向的数据,也即 *arr[i] 的形式。
       第二个 printf() 语句中, parr+i 表示第 i 个元素的地址, *(parr+i) 表示获取第 i 个元素的值(该元素是一个指针),**(parr+i) 表示获取第 i 个元素指向的数据。

#include <stdio.h>
int main(){
char *str[3] = {
"https://blog.csdn.net/qq_38351824/article/category/9343538",
"百度搜索",
"sumjess"
};
printf("%s\n%s\n%s\n", str[0], str[1], str[2]);
return 0;
}

等价于

#include <stdio.h>
int main(){
char *str0 = "https://blog.csdn.net/qq_38351824/article/category/9343538";
char *str1 = "百度搜索";
char *str2 = "sumjess";
char *str[3] = {str0, str1, str2};
printf("%s\n%s\n%s\n", str[0], str[1], str[2]);
return 0;
}

需要注意的是,字符数组 str 中存放的是字符串的首地址不是字符串本身,字符串本身位于其他的内存区域, 和字符数组是分开的。
也只有当指针数组中每个元素的类型都是 char *时,才能像上面那样给指针数组赋值,其他类型不行。

四、二维数组指针(指向二维数组的指针)

二维数组在概念上是二维的,有行和列,但在内存中所有的数组元素都是连续排列的,它们之间没有“缝隙”。以下面的二维数组 a 为例:

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


C 语言允许把一个二维数组分解成多个一维数组来处理。对于数组 a,它可以分解成三个一维数组,即 a[0]、 a[1]、a[2]。每一个一维数组又包含了 4 个元素,例如 a[0] 包含 a[0][0]、 a[0][1]、 a[0][2]、 a[0][3]。

假设数组 a 中第 0 个元素的地址为 1000,那么每个一维数组的首地址如下图所示:

       为了更好的理解指针和二维数组的关系,我们先来定义一个指向 a 的指针变量 p:int (*p)[4] = a;括号中的*表明 p 是一个指针,它指向一个数组,数组的类型为 int [4],这正是 a 所包含的每个一维数组的类型。[ ]的优先级高于*, ( )是必须要加的,如果赤裸裸地写作 int *p[4],那么应该理解为 int *(p[4]), p 就成了一个指针数组,而不是二维数组指针。
       对指针进行加法(减法)运算时,它前进(后退)的步长与它指向的数据类型有关, p 指向的数据类型是 int [4],那么 p+1 就前进 4×4 = 16 个字节, p-1 就后退 16 个字节,这正好是数组 a 所包含的每个一维数组的长度。也就是说, p+1 会使得指针指向二维数组的下一行, p-1 会使得指针指向数组的上一行。
       数组名 a 在表达式中也会被转换为和 p 等价的指针!
       下面我们就来探索一下如何使用指针 p 来访问二维数组中的每个元素。按照上面的定义:

  1. p 指向数组 a 的开头,也即第 0 行; p+1 前进一行,指向第 1 行
  2. *(p+1)表示取地址上的数据,也就是整个第 1 行数据。注意是一行数据,是多个数据,不是第 1 行中的第 0 个。
  3. *(p+1)+1 表示第 1 行第 1 个元素的地址。如何理解呢?
    *(p+1)单独使用时表示的是第 1 行数据,放在表达式中会被转换为第 1 行数据的首地址,也就是第 1 行第 0 个
    元素的地址,因为使用整行数据没有实际的含义,编译器遇到这种情况都会转换为指向该行第 0 个元素的指针;
    就像一维数组的名字,在定义时或者和 sizeof、 & 一起使用时才表示整个数组,出现在表达式中就会被转换为指向数组第 0 个元素的指针。
  4. *(*(p+1)+1)表示第 1 行第 1 个元素的。很明显,增加一个 * 表示取地址上的数据。
    a+i == p+i
    a[i] == p[i] == *(a+i) == *(p+i)
    a[i][j] == p[i][j] == *(a[i]+j) == *(p[i]+j) == *(*(a+i)+j) == *(*(p+i)+j)
    
    【实例】使用指针遍历二维数组
    
    #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[5]);    指针数组,可以去掉括号直接写作 int *p1[5];
int (*p2)[5];    二维数组指针,不能去掉括号

       指针数组和二维数组指针有着本质上的区别:指针数组是一个数组,只是每个元素保存的都是指针,以上面的 p1为例,在 32 位环境下它占用 4×5 = 20 个字节的内存。二维数组指针是一个指针,它指向一个二维数组,以上面的 p2 为例,它占用 4 个字节的内存。

六、常见指针变量的例子集合:

常见指针变量的定义
定 义含 义
int *p;p 可以指向 int 类型的数据,也可以指向类似 int arr[n] 的数组。
int **p;p 为二级指针,指向 int * 类型的数据。
int *p[n];p 为指针数组。 [ ] 的优先级高于 *,所以应该理解为 int *(p[n]);
int (*p)[n];p 为二维数组指针。
int *p();p 是一个函数,它的返回值类型为 int *。
int (*p)();p 是一个函数指针,指向原型为 int func() 的函数。

① 二级指针:指向一级指针的地址

#include <stdio.h>
int main(){

int a[2] = {0,1};

int *pt1 = &a[1];   // 将 a[1] 的地址赋值给pt1 

int **pt = &pt1;    // 二级指针 将指针pt1的地址赋值给pt 

printf("利用二级指针pt 获取pt1的指针地址 :%p\n",pt );
printf("显示自己(指针pt1)的地址 :         %p\n",&pt1 );

return 0;
}

② 指针数组:[ ]优先级高于 *,所以应该理解为  int * ( p[n] );

#include <stdio.h>
int main(){

int a[4] = {0,1,2,3};

int *pt[4] = { a, &a[1], &a[2], &a[3]};

printf("显示  %p\n",&pt );     //打印数组的首地址, 
printf("显示  %p\n",&pt[0] );  //也是数组第一个元素的地址 

printf("显示  %p \n",pt[2]);   //打印第三个元素的地址
printf("显示  %p \n", &a[2]);  //验证第三个元素的地址 是否正确 

printf("显示  %d \n", a[2]);    //打印数组第三个数据 
printf("显示  %d \n",*pt[2]);   //验证pt[2]到底是不是表示第三个元素的地址
printf("显示  %d \n", *&a[2]);  //再次验证第三个元素的地址 是否正确 

return 0;
}

③ 二维数组指针

#include <stdio.h>
int main()
{
int a[3][4]={0,1,2,3,4,5,6,7,8,9,10,11};

int i = 1 ,j = 2;

int(*p)[4];  //二维数组指针p 

p=a;  //二维数组指针p 指向 数组a的首地址

printf("第i行,第j个元素的地址是 : %p \n",   (*(p+i)+j) );  //第i行,第j个元素的地址 
printf("第i行,第j个元素的数值是 : %d \n", * (*(p+i)+j) );  //第i行,第j个元素的数值

printf("验证:\n");

printf("第i行,第j个元素的地址是 : %p \n",   &a[1][2] );  //第i行,第j个元素的地址 
printf("第i行,第j个元素的数值是 : %d \n",    a[1][2] );  //第i行,第j个元素的数值

return 0;
}

④ int *p();  函数返回值类型为  int *

#include <stdio.h>

int n = 100;

int *test(void) //返回一个int *类型(地址) 
{
 return &n;
}

int main()
{
 int *p , n;
 
 p = test();  //指针p指向这个地址 
 n = *p;      //从这个地址获取值 
 
 printf("value = %d\n", n); //该地址的值是否为 100 
 
 return 0;
}

⑤ 函数指针:int (*p)() ; 

#include <stdio.h>

int m = 99;

int *test(int * a) //第一个*代表返回一个int *类型(返回一个地址) 
{                  //第二个*传入一个地址
 return a;         //return 返回一个地址 
}

int main()
{
 int *p ;         //定义一个指针变量 
 int *(*pt)(int *c) = test ;  //第一个*函数指针:int (*p)() ;第一个* 是要与 test 函数参数列表一致。  
                              //第二个*函数指针:int (*p)() ;
                              //第三个*函数指针:int (*p)() ;第二个括号中的int *c 是要与 test 函数参数列表一致。 
                              //下面再对比解释下。                              
                              //int *(*pt)(int *c)     对比看一下实际就是将*pt = test ;其他部分一致 
                              //int * test(int * a)    而*pt = test 就是pt指向函数首地址 	
							  					  
 p = (*pt)(&m);   //函数(*pt)(&m)返回一个地址,指针p指向这个地址 
//---------------------------------------------------------------------------------------------------//
//上面的内容其实就做了一个简单的事情,将指针p指向全局变量m的地址(即p=&m),转这么一圈使用来学习指针的//
// 下面就来验证,我们之前做的是对。                          
	
 printf("m的地址为          :%p\n",&m);//这是m的地址 
 printf("验证指针:m的地址为:%p\n",p); //这是经过风波的m的地址,用来验证我们之前的一通指针操作是否正确
  
 printf("m的value =          %d\n",m); //该地址的值是否为 99
 printf("验证指针:m的value= %d\n",*p); //这是经过风波的m的值,用来验证我们之前的一通指针操作是否正确 
 
 return 0;
}

 

©️2021 CSDN 皮肤主题: 技术黑板 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值