目录
一、字符指针
首先,我们要知道指针的概念,指针就是用来储存内存块所在的地址(关于内存块的地址是如何分布的,可以看我另一条博客)。下面举例说明:
int main()
{
int i = 0;//定义i为整型,想内存申请4个字节的空间(内存块)
int *p = &i;//取地址符&,&i取出i所在内存块的地址,由指针p接收
return 0;
}
上面定义的指针p的类型为int(整型),但是我们知道,在c语言中,类型由7种,下面举例说明
int main()
{
char *p;//字符型指针
short *p;//短整形指针
int *p;//整型指针
long *p;//长整型指针
long long *p;//长整型指针
float *p;//单浮点型指针
double *p;//双浮点型指针
return 0;
}
下面,我们讲字符指针,其他类型指针则类似,依旧是我们的举例说明,毕竟举例更加直观嘛
int main()
{
char i = 'w';//将字符w由字符型变量i接收,变量i就在内存空间中开辟一个内存块
char *p =&i;//将i所在内存块的地址由字符型指针接收
}
//一般来说,指针的类型应该和它接收地址的那个变量类型相同,如上所示
//原因是因为通过解引用符 * 的时候,可以改变i的值
下面,我们来看一段代码,看看它的运行结果是不是和你想的一样了?
int main()
{
char *p = "hello hello";
char arr[] = "hello hello";
printf("%s\n",p);
printf("%s\n",arr);
return 0;
}
下面是运行的结果:
hello hello
hello hello
其实char *p = " hello hello ";本质上是将“hello hello”这个字符串的首字符地址储存在了指针p中,类似于数组名放到指针里面,都是首元素地址,所以打印的时候,可以完整的打印出来
看到这里,你肯定对字符指针有所了解,下面我们看这一段代码,看看运行的结果是否和你想的一样。
int main()
{
char str1[] = "hello hello";
char str2[] = "hello hello";
char *str3 = "hello hello";
char *str4 = "hello hello";
if(str1 == str2)
{
printf("str1 and str2 are same\n");
}
else
{
printf("str1 and str2 are not same\n");
}
if(str3 == str4)
{
printf("str3 and str4 are same\n");
}
else
{
printf("str1 and str2 are not same\n");
}
return 0;
}
下面是运行的结果
str1 and str2 are not same
str3 and str4 are same
先来解释第一个,原因是数组 str1[ ] 和 str2[ ] 分别向空间申请了内存块进行字符串的存储,所以占用了不同的内存块,所以每个内存块的地址不同,而数组名 str1 和 str2 代表的是首元素地址,所以地址不相同。
要按照这么说,第二个打印出来应该和第一个一样啊。其实不然,我们当把字符串放在数组里面的时候,是一个字符一个字符的放在对应的内存块的位置,所以每个字符是可以通过相应的代码修改的,所以要放在不同的内存块中。
but!!!我们上面说的用指针接收字符串常量的时候,是把首字符的地址传给了指针,而且,最重要的是字符串常量不能被修改!!!所以这个字符串常量只需要申请一块内存块即可,所以指针str3和str3里面存放的地址相同。
二、指针数组
指针数组,简单来说就是一个存放指针的数组,所以它还是一个数组,即数组里面存放的都是指针。
int *arr[10];//存放整型指针的数组
char *arr[3];//存放字符型指针的数组
char **arr[8];//存放二级字符指针的数组
依旧来举例
int main()
{
int a[5] = {0,1,2,3,4,5};
int b[] = {2,4,6};
int c[] = {3,6,8};
int* arr[3] = {a,b,c};
return 0;
}
我来解释一下,如果所示,三个整型数组有不同的内存块,内存块的地址不相同, 而指针数组里面放的是数组名a、b、c,而数组名代表的是首元素的地址,而这个地址所指向的数组首元素是整型,所以这个指针数组的类型即 int* 。
这里肯定有人会问,咦~,这个指针数组里面明明放的是数组名,数组名是代表的首元素地址,这里不是指针啊。我们来这么理解,指针是用来干嘛的,是不是用来接收地址的,所以指针代表的就是地址,可以这么说指针即地址,地址即指针。
当指针数组遇到加法的时候,是否和指针加法一样了?我们来看下一个代码 ,是否结果和一想的一致了?
int main()
{
int i = 0;
int a[5] = {0,1,2,3,4,5};
int b[] = {2,4,6};
int c[] = {3,6,8};
int* arr[]={a,b,c};
for( i=0 ; i<5 ; i++ )
{
printf( "%d " , *(arr[0]+i) );
}
return 0;
}
运行结果是:
0 1 2 3 4 5
首先 arr[0] 指的是指针数组的第一个元素int* a,上面我们说了这个是数组 a[ 5 ] 首元素的地址,那么通过从加0到加4,就可以依次获得数组 a[ 5 ] 里的每个元素地址,再通过解引用符 * ,就可以获得数组 a[ 5 ] 的每个元素,进而打印出来。
三、数组指针
首先,我们来看它的定义,数组指针是指针,还是数组?
答案是:指针!!!
我们已经熟悉的:整型指针:int* p是指向整型数据的指针。单浮点指针:float* t是指向单浮点数据的指针。
那么同理,我i们可以得到数组指针,就是指向数组的指针。
我们老规矩,直接上代码解释
上面关于数组指针的写法已经很明确了,如果还有老铁不太清楚。那么就一句话:数组指针就是存放整个数组地址的指针,本质上任然是指针,用来存放地址。
还有了,就是关于数组指针的类型如何写
int arr[10] = {0};
int (*p)[10] = &arr;
那么这个数组指针p的类型就是 int (*)[10] ,没错,就是将数组指针p去掉就是了。
数组指针的加法又是怎么样的了,上代码解释:
从上面的代码可以看出 &arr 和 arr 的地址是一样的,但是为什么一个加了4,一个加了40了?
我们说,虽然取出的地址一样,但是意义却不一样。&arr 取出来的地址是整个数组的地址,
而 arr 取出来的是数组首元素的地址,所以前者是跳过10个整型的字节(因为这里数组里面是10个整型),后者一个是跳过一个整型的字节。
我们来看下一段代码,看看结果如何
#include<stdio.h>
void print(int (*p)[5],int r,int c)
{
int i = 0;
int j = 0;
for(i=0;i<r;i++)
{
for(j=0;j<c;j++)
{
printf("%d ", *(*(p+i)+j) );
}
printf("\n");
}
}
int main()
{
int arr[3][5] = {(1,2,3,0,0),(4,5,6,0,0),(7,8,9,0,0)};
print(arr,3,5);
return 0;
}
下面是运行的结果:
1 2 3 0 0
4 5 6 0 0
7 8 9 0 0
这里有三个点需要解释
第一:我们知道数组名代表的是首元素地址,一维数组我们很清楚,但是二维数组的数组名代表的是首元素地址就是第一行的地址,即用 &arr[0] 表示第一行的地址,同理,第二行和第三行的地址就是 &arr[1] 和 &arr[2] ,是不是感觉和一维数组很像,没错,因为二维数组其实本质上就是几个一维数组的组合,如本代码这个二维数组可以看成由3个一维数组组成 。
第二:在我们自定义的函数中,形参数组指针 int (*p)[5] 就是用来接收二维数组第一行的地址,因为函数调用的实参是直接给的二维数组名 arr 。
第三:来解释一下 *( *( p + i ) + j ),初看一下很懵,这种样式的,我们需要从里往外看,首先p是数组指针,接受的是来自二维数组第一行的地址,即 &arr[0] 。
此时 i = 0,*p 就是指向的第一行的数组名,即 arr[0] ,注意!!!这是一个一维数组的数组名,所以代表的是首元素地址,再通过加 j ,就能得到这个一维数组里面元素的地址,最后整体解引用,就得到这个一维数组的所有元素。然后进入 i =1 的下一次循环。
此时 i =1,( p+1) 就是指向二维数组第二行的地址,再解引用 *( p+1) 就是指向的第二行的数组名,即 arr[1] ,同上,这是一个一维数组的数组名,所以代表的是首元素地址,再同过加 j 和解引用就能得到二维数组第二行的元素。然后进入 i =1 的下一次循环。
i = 2 同上,所以打印的结果如上。
指针数组和数组指针,当这两个结合的时候,会是什么样的了?
指针数组:int* arr[3],存放指针的数组
数组指针:int (*p)[5],存放数组地址的指针,该指针可以指向一个数组,数组的5个元素, 每个元素的类型是 int 。
int (*parr[10])[5],这里的 parr 就是一个存放数组指针的数组,该数组可以存放10个数组指针,每个数组指针指向一个数组,数组由5个元素,每个元素的类型是 int 。
我们可以这么看: int (*)[5],这是一个数组指针的类型,parr[10]是一个数组,加起来parr就是一个存放数组指针的数组。
四、数组传参
在写代码的时候,难免会把数组传递给函数,那么函数的参数应该如何设计了?
一维数组传参
#include<stdio.h>
void test1(int arr[]) //第一种
{}
void test1(int arr[10]) //第二种
{}
void test1(int* arr) //第三种
{}
void test2(int* arr[]) //第四种
{}
void test2(int* arr[20]) //第五种
{}
void test2(int** arr) //第六种
{}
int main()
{
int arr1[10]={0};
int* arr2[20]={0};
test1(arr1);
test2(arr2)
return 0;
}
如上代码所示,在一维数组传参时,以上函数的形参设计都对。
前三种是对整型数组进行传参,第一种和第二种可以看成用一个形参整型数组接收数组,形参数组的数组元素可写可不写,但是实际传递过去的整型数组首元素地址,第三种就是将实际的首元素地址接收,用指针进行接收。
后三种是对指针数组进行传参,第四种和第五种可以看成用一个形参指针数组接收数组,形参数组的数组元素可写可不写,但是实际传递过去的指针数组首元素地址,而指针数组的首元素本身是一个一级指针,而一级指针必须用二级指针接收,所以用 int** arr 。
二级数组传参
#include<stdio.h>
void test1(int arr[][4])
{}
void test1(int arr[3][5])
{}
void test1(int (*arr)[5]);
{}
int main()
{
int arr[3][5]={0};
test(arr);
return 0;
}
对于二维整型数组,上面有三种,我觉得第一和第二种不需要解释了,但是在省略形参整型数组的元素的时候,列绝对不能省略,行可省可不省。
第三种,arr 是二维数组的数组名,代表的是二维数组第一行的地址,即一个一维数组的地址,所以我们需要用一个数组指针来接收,而第一行的数组元素个数是5,所以第三如代码所示。
五、指针传参
在写代码的时候,难免会把指针传递给函数,那么函数的参数应该如何设计了?
一级指针传参
一级指针传参很简单,函数的形参就用一个指针接收这个指针即可。
但是,这里有一个需要特别注意的点:就是传递过去的指针和函数形参的指针必须是相同级别,即一级指针用一级指针接收,二级指针用二级指针接收。如果实参是一级指针,形参是二级指针,那么这个形参接收的内容就是一级指针本身的地址,就不是一级指针所指向的那个数据的地址。
我们来思考一个问题,当一个函数参数为一级指针的时候,函数能接收什么实参?
如上图所示,形参一级指针可以接受一级指针和地址。
二级指针传参
一级指针传参很简单,函数的形参就用一个指针接收这个指针即可。依旧和上面一样,需要注意指针级别的问题。
我们又来思考一个问题,当一个函数参数为二级指针的时候,函数能接收什么实参?
#include<stdio.h>
void test(int** p)
{}
int main()
{
int a = 1;
int* p1 = &a;
int** p2 = &p1;
test(p2); //第一种
test(&p1); //第二种
int* arr[10] = {0};
test(arr); //第三种
return 0;
}
当函数形参为二级指针的时候,函数的实参又三种。
第一种:直接将二级指针传递过去。
第二种:将一级指针的地址传递过去,这里是一级指针本身的地址,而不是所指向变量的地址,因为二级指针就是用来接收一级指针地址的。
第三种:如果是一个指针数组,可以将数组名 arr 传递过去,因为数组名就是首元素的地址,而指针数组里面存放的是指针,所以相当于第二种,把一级指针本身的地址传递了过去。
后面我会继续分享函数指针、 函数指针数组,指向函数指针数组的指针和回调函数。(后面内容博客连接关于C语言数组、指针和函数的相关内容2_昵称就是昵称吧的博客-优快云博客)