目录
一、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 来访问二维数组中的每个元素。按照上面的定义:
- p 指向数组 a 的开头,也即第 0 行; p+1 前进一行,指向第 1 行。
- *(p+1)表示取地址上的数据,也就是整个第 1 行数据。注意是一行数据,是多个数据,不是第 1 行中的第 0 个。
- *(p+1)+1 表示第 1 行第 1 个元素的地址。如何理解呢?
*(p+1)单独使用时表示的是第 1 行数据,放在表达式中会被转换为第 1 行数据的首地址,也就是第 1 行第 0 个
元素的地址,因为使用整行数据没有实际的含义,编译器遇到这种情况都会转换为指向该行第 0 个元素的指针;
就像一维数组的名字,在定义时或者和 sizeof、 & 一起使用时才表示整个数组,出现在表达式中就会被转换为指向数组第 0 个元素的指针。 - *(*(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;
}