指针作为C语言中的一个部分,可以说指针是C语言的核心,那么它的难度肯定是不言而喻的,总是能把人给绕得找不到方向。
今天我就好好的说一说指针这个东西。
1、何为指针?
指针是C语言中用来存放地址的一个变量类型。我们可以将指针看作独特的一种变量,只用来存放地址。根据指针存放不同类型变量的地址,也可以将指针分为:int型指针,char型指针,等等。由于指针都是存放地址,而所有类型的地址又没有本质上的差异,所以无论任何类型的指针大小都是同样的。同为4个字节或者8个字节。
指针是 C 语言中的一种数据类型,可以用来存储内存地址。每个变量或对象都占据着计算机内存中的某个地址,指针变量可以保存这些地址,并允许对这些变量或对象进行间接访问。
2、一级指针
想要了解指针,咱就得从最基础的一级指针开始。
1、指针的声明
在 C 语言中,指针变量声明时需要指定所指向的数据类型,以便编译器能够在使用指针时自动对其进行类型转换和内存管理等操作。
例如,以下为一个整型指针变量的定义:
int a = 10;
int* pa = &a;
*即代表这个pa是一个int型指针,指向的是a的地址。
注意:声明指针之后,必须对其进行初始化!!!(否则它就是一个空指针)不能直接使用它!!!对空指针进行解引用操作系统可能会崩溃!!!后果非常严重!
2、通过指针对变量或对象进行访问
int a = 10;
int* pa = &a;
*pa = 20;
这里我们解引用pa,然后将pa的值赋值为20。(对pa解引用后,就相当于a。因为pa是a的地址,解引用a的地址,那就是找到了a)
这里引出一个问题:
int a = 10, b = 20;
int* pa = &a, * pb = &b;
pa = pb;
此时a的值是20吗???相信大部分对指针不了解的人都会认为此时a已经变成了20,因为将b的地址pb赋值给了pa。但是结果是:a仍然是10。首先:pa存的确实是a的地址,然后我们确实是对pa存放的地址进行了改变。但是!a是a,pa是pa,在这里改变pa,对a有影响吗?没有!!!!
a的地址并没有发生改变!!!所以a仍然是10。但是如果我们解引用pa后,会发现此时*pa的值是20,因为我们改变的是pa存放的地址。
既然讲到这,那我们就得提出以下指针的运算了。指针没有复杂的运算,不能指针间进行运算。
3、指针的运算
赋值运算:相同类型的指针才能赋值。 算术运算:+,-,++,-- (这里的+和-,指的是+-多少,即对地址的运算,指向前面的地址或者后面的地址。并不是说简单的pa+pb,其实指针间是可以进行运算的,我们后面再谈论)关系运算 :==,!=, <=,>=,<,>。
int a[10];
int* p1 = &a[2];
int* p2 = &a[5];
printf("%d", p2 - p1);//answer == 3
answer == 3,why?
这里先不说,后面再说。
1、算术运算
a = *p++ ---->结果为:a = *p ,p++。
a = (*p)++ --->结果为 : a = *p,*p++。
2、关系运算
若p1和p2指向同一数组,则 p1<p2 表示p1指的元素在前 。 p1>p2 表示p1指的元素在后。 p1==p2 表示p1与p2指向同一元素 。 若p1与p2不指向同一数组,则比较无意义。
4、指针与函数
先来个最简单的交换函数Swap。
1、交换函数
void Swap(int x, int y)
{
int c = x;
x = y;
y = c;
}
int main()
{
int a = 10, b = 20;
printf(" a = %d,b = %d", a, b);
Swap(a, b);
printf(" a = %d,b = %d", a, b);
return 0;
}
这个函数会起到交换a,b的值的作用吗???答案是不会!这个函数我们在传参的时候,只是将a,b的值传过去了,但没有影响地址。在Swap函数中创建临时变量来交换x,y的值,但是原来a,b的地址并没有改变。而地址才是决定一个变量的值。故a,b没有改变。
所以应该用地址即指针来修改二者的值,才能改变a,b的地址,进而改变a,b的值。
void Swap(int* x, int* y)
{
int c = *x;
*x = *y;
*y = c;
}
int main()
{
int a = 10, b = 20;
printf(" a = %d,b = %d", a, b);
Swap(&a, &b);
printf(" a = %d,b = %d", a, b);
return 0;
}
因此:函数参数为指针的时候,可以改变实参的值。
3、指针与数组名的关系
1、数组名
对于数组名,在大部分情况下数组名都代表首元素的地址。一维数组的数组名代表第一个元素的地址,二维数组的数组名代表第一行元素的地址(即二维数组的数组名代表的是一个一维数组的地址)。那么数组名就可以看作是一个指针。
2、指针与数组访问元素的等价关系
既然说了数组名就是指针,那么用数组名可以访问元素,用指针也可以访问元素了。
int arr[5] = { 1,2,3,4,5 };
int* parr = arr;
printf("%d ", *(parr + 1)); // 2
printf("%d ", *(arr + 1)); // 2
由此可以看出来,对于一维数组,*(parr+i) 等价于arr[i]。同理对于二维数组,不同的是,对于二维数组需要用二级指针,*(*(parr+i)+j)就等价于arr[i][j]。为什么呢?对于二维数组:*(parr+i)得到的是第i-1行的数组,而数组名又是一个首元素的地址,那么*(parr+i)得到的就是第i-1行的首元素地址,至此+j再解引用就和一维数组一样了。但是如果传参二维数组名,并不是以二级指针接收。二维数组名代表首行数组的地址,接收数组的地址,需要用数组指针接收。下面再细讲。
3、字符数组和字符指针的区别
1、字符数组
char str1[10] = "abcdef";
char str2[10];
str2 = "abcd";//错误!
虽然数组名代表首元素地址,“abcd”也代表首元素a的地址,但是这样赋值给数组是错误的!我们只能通过拷贝来赋值给数组!
char str2[10];
strcpy(str2, "abcd");
对于此类赋值,我们是可以通过覆盖等操作来修改数组值的。下面将介绍另一种赋值法,这种赋值法是不能修改数组的值的。
2、字符指针
char* str;
str = "abcde";
通过指针来给字符数组赋值,这样赋值的时候,“abcde”叫做字符串常量,字符串常量是不能修改的。则此时str是不能修改它的内容的。
4、数组指针
1、数组指针的定义
数组指针:就是一个指向数组的指针,这里的指针代表的是整个数组的地址,并非数组首元素的地址。注意区分。
2、数组指针的表示
int arr[3] = { 1,2,3 };
int(*p)[3] = &arr;
我们来解析一下数组指针:p代表指针名称,*p代表p是一个指针,[ 10 ]代表这个指针指向一个拥有十个元素的数组,int代表这个数组是int类型的。
注意: 这里需要用括号将*p结合起来,因为[ ]的优先级比*的优先级高。如果不用括号结合的话,这其实是一个指针数组。
接下来我们将用代码来分析数组指针和数组名的差别:
int arr[3] = { 1,2,3 };
int(*p)[3] = &arr;
printf("p = %p\n", p);
printf("arr = %p\n", arr);
printf("p + 1 = %p\n", p + 1);
printf("arr + 1 = %p\n", arr + 1);
可知:p和arr的地址都是一样的,都是指向数组的起始位置的地址。
但是p+1和arr+1的地址就不一样了:发现:p+1比p的地址增加了12个字节,即三个地址的大小。而arr+1比arr只增加了4个字节,即一个地址的大小。
由此可以看出:数组指针代表整个数组的地址,数组名只代表数组首元素的地址。
3、数组指针与二维数组传参
通过上面我们知道,二维数组名代表首元素地址,这里的首元素地址,代表二维数组的第一行元素的地址,即看作一个一维数组的地址。既然如此,一维数组名传参我们需要用一级指针来接收地址。那么二维数组名传参,我们是否需要用数组指针来接收一维数组的地址呢?答案是:是的,要用数组指针。
void fun(int(*arr)[3], int row, int col)
{
for (int i = 0; i < row; i++)
{
for (int j = 0; j < col; j++)
{
printf("%d ", arr[i][j]);//或者*(*(arr+i)+j)
}
printf("\n");
}
}
int main()
{
int arr[2][3] = { {1,2,3},{4,5,6} };
fun(arr, 2, 3);
return 0;
}
注意:这里新参的[ ]里边,必须是那个一维数组的元素个数,否则打印输出的时候就不会正常打印。(如果小于元素个数,它会打印你第二行的元素,因为二维数组在内存中其实是线性连续的。如果大于数组个数,它会乱输出空间中数组末尾后边的数据) 但是:对于二维的字符数组,可能每一个一维数组的字符数是不同的,那么这里使用数组指针接收,[ ]里面就不知道写多少。那么对于这种情况,我们需要用到二级指针,但是这里的二维数组我们需要动态开辟,不能直接自己简单定义二维数组。
5、二级指针
二级指针,显然就是接收二维数组名的指针,但是这里并不是简单的直接传入二维数组名,这里二维数组名需要动态开辟。
1、二级指针与动态开辟字符二维数组
一般使用二级指针是为了来应对二维字符数组的每个一维数组元素个数不同的情况。普通情况下,二维数组还是用数组指针来接收。
void fun(char** str, int row)
{
for (int i = 0; i < row; i++)
{
printf("%s\n", str[i]);
}
}
int main()
{
char** str = (char**)malloc(sizeof(char*) * 3);//开辟一个二维字符数组,一共有三个字符串
for (int i = 0; i < 3; i++)
{
str[i] = (char*)malloc(sizeof(char) * 10);//给每个字符串开辟空间
scanf("%s", str[i]);//输入
}
fun(str, 3);
return 0;
}
6、其他指针类型
1、函数指针
函数指针:顾名思义就是存放函数地址的指针咯。那么问题来了:函数也有地址吗? 函数当然有地址,不然在调用函数的时候,在内存中哪里去寻找这个函数呢?
1、函数名
首先来说一下函数名(地址):
int Add(int x, int y)
{
return x + y;
}
int main()
{
printf("%p\n", &Add);
printf("%p\n", Add);
return 0;
}
由此我们可以看出,对于函数名,无论是对它取地址,还是直接是函数名,都代表的这个函数的地址。
2、函数指针的定义
int Add(int x, int y)
{
return x + y;
}
int main()
{
int* p1 = Add;
printf("%p\n",p1);
int (*p2)(int, int) = Add;
printf("%p\n", p2);
return 0;
}
对于如上两种定义,虽然二者打印结果相同,但是第一种定义是错的。在好的编译器下,会发出警告:“初始化”:“int *”与“int (__cdecl *)(int,int)”的间接级别不同。这个警告就说明这种定义函数指针的方法其实是错的,二者并不相同。
所以正确的函数指针定义方法应该是第二种
int (*p2)(int, int) = Add;
3、解析函数指针
int (*p2)(int, int) = Add;
p2:指针名称;*p2代表是一个指针类型;前面的int:代表函数的返回值是int;(int,int):代表这个函数的两个参数是int和int类型的。
4、调用函数指针
int Add(int x, int y)
{
return x + y;
}
int main()
{
int (*p2)(int, int) = Add;
int ret = p2(1, 2);
printf("%d", ret);
return 0;
}
我们可以直接运用函数指针来执行该函数的功能(虽然多此一举哈哈哈哈哈)。
那有人会问了,为什么这里p2没有解引用就可以使用?前面讲了,函数名取地址和不取地址的结果都是一样的,那么解引用和不解引用结果也是一样的。
2、其他
对于指针,我们其实可以引出很多不同种类的指针,越往后,指针越头大,我们只需要掌握这些常用的就行了。
6、总结
写了这么久,都给我写累了,但是指针的奥妙远不止于此,还有很多细节需要自己去琢磨。哎,路漫漫其修远兮~