前言
指针,存在于C语言中,用途广泛。有了它就可以直接使用地址找到存储数据的空间直接修改,或者使用。那么在学习指针之前我一直有一个疑惑,那就是为什么要有指针?函数的内容可以直接通过名字直接访问,为什么还需要绕一圈?这不是更复杂了吗?事实上指针给我们提供了许多便利,接下来就来具体介绍什么是指针,以及指针的作用。
一. 简单的指针
指针类型:
char* - 指向字符的指针
short* - 指向短整型的指针
int*-指向整型的指针
float* -指向单精度浮点型的指针
void*--无具体类型的指针
除了这些指针之外,还有许多复杂的指针,将在之后的大题里讲到。
1. 指针的大小
指针类型的大小在32位的计算机中是4个字节,在64位的计算机中是8个字节,是固定的。因此只要是指针他的大小就是4或者8。
2. 指针类型的意义
既然指针的大小是固定的,那么为什么会有这么多类型都是指针呢?
这是因为指针要访问空间的大小是由类型决定的,相当于原来的类型去掉1个‘*’号。比如说‘char*’类型解引用所访问的类型是‘char’,而‘int*‘类型访问的就是’int‘。他们分别访问1个字节和4个字节的空间,并且识别为个自类型。
如果将’int*‘型的地址放到其他类型的地址处就会报错。例如:
int main()
{
int a = 0;
//char* p = &a;//err
return 0;
}
正确的方式应该存到对应的类型里,实在需要存储就需要强制类型转换。
3. const在指针中的作用
const修饰的变量将无法使用变量改变,但是如果使用指针的话还是可以改变它。因为限定的是变量而不是指针。
int main()
{
const int a = 0;
int b = 20;
const int* p = &a;
p = &b;
//*p = 10;
printf("a = %d\n", a);
return 0;
}
这里还会报一个错,说明编译器在提醒你,’a'被‘const’修饰了但还是被改变了。
如果想让指针‘p’无法修改‘a',那么也可以用const修饰它。
如果‘const’在‘*’前面,那么‘*p’就无法再被解引用修改‘a',直的注意的是,虽然’*p‘无法再被修改,但是’p'里面所存的地址是可以修改的。
int main()
{
const int a = 0;
int b = 20;
const int* p = &a;
p = &b;
//*p = 10;
printf("a = %d\n", a);
return 0;
}
那么如果需要控制‘p'也不变,则需要将’const‘放在’*‘后面。
int main()
{
const int a = 0;
int b = 20;
int* const p = &a;
p = &b;
*p = 10;
printf("a = %d\n", a);
return 0;
}
如果需要控制’p‘和’*p'都不变,则需要分别在‘*’前后增加‘const’。
int main()
{
const int a = 0;
int b = 20;
const int* const p = &a;
p = &b;
*p = 10;
printf("a = %d\n", a);
return 0;
}
4. 指针的运算
指针也是可以通过加减来计算的。但是指针的计算结果并不是单纯的数学计算。
例如‘int*’型的指针‘+1’的话,地址会向后偏移4位。而‘char*’类型‘+1’只会偏移1位。这是因为指针及偏移量是由指针所指向的类型的大小所决定的,int是4个字节的大小,char是1个字节。所以结果很明显了。
int main()
{
int a = 0;
char b = 0;
int *pa = &a;
char *pb = &b;
printf("%p\n", pa);
printf("%p\n", pa + 1);
printf("%p\n", pb);
printf("%p\n", pb + 1);
return 0;
}
pa+n将其转化为整形计算,相当于pa+n*sizeof(a)。其中‘pa’表示‘a’的地址,n表示常数。
5. 指针里内容打印
(1)int*型
打印可以分为2种方法,一种是类似于数组的写法,一种是指针解引用的写法。两种方法的到的结果都是一样的。
int main()
{
int arr[]= {1,2,3,4,5,6,7,8,9,10};
int* pa = arr;
int i = 0;
for(i = 0; i < 10; i++)
{
printf("%d ", pa[i]);
}
printf("\n");
for(i = 0; i < 10; i++)
{
printf("%d ", *(pa + i));
}
printf("\n");
return 0;
}
pa[i]和*(p+i)的效果是等价的。
(2)char*类型
int main()
{
char arr[]= {'a','b','c','d'};
char* pa = arr;
char i = 0;
int sz = sizeof(arr) / sizeof(arr[0]);
for(i = 0; i < sz; i++)
{
printf("%c", pa[i]);
}
printf("\n");
for(i = 0; i < sz; i++)
{
printf("%c", *(pa + i));
}
printf("\n");
printf("%s\n", pa);
return 0;
}
‘%s’是打印中比较特殊的一种,他专门用来打印字符串。它的用法是后面传的是地址,从这个地址开始向后打印字符串直到遇到‘\0’结束。这里没有放‘\0'也结束了,说明这个字符传后面刚好有个’\0‘。实际上如果不自己添加‘\0’的话,‘%s’会打印乱码出来。切记。
(3)其他简单类型
如果是其他类型如double或者float类型,只需要把打印的%d换成%lf和%f就可以。实际上在简单类型里会有所区别的只有char类型。
6. 指针的大小
指针可以通过地址的位置来分别大小,相同类型的指针还可以进行+-运算。
做差的时候相减得出的是中间间隔的元素个数。比如说这里相减的结果是1而不是4。
int main()
{
int arr[]= {1,2,3,4,5,6,7,8,9,10};
int* pa = arr;
int* pb = &arr[1];
if(pa > pb)
{
printf("pa = %p\n", pa);
}
else
{
printf("pb = %p\n", pb);
}
printf("pb - pa = %d\n", pb - pa);
return 0;
}
这里的pb>pa所以pa的地址没有打印。
7. 野指针
野指针的意思就是指针指向的空间没有被软件使用,这个时候造成了非法访问。这种样子大概有2种情况。
(1)未被初始化
int main()
{
int* pa;
*pa = 10;
printf("%d\n", *pa);
return 0;
}
这种情况下修改‘*pa’会导致程序直接崩溃,‘pa’指向了一个随机的地方,如果这个地方指向的系统的模块可能会导致系统直接崩溃。为了避免这种情况,我们可以在开始的时候直接给pa赋值或者给一个NULL。
int main()
{
int a = 10;
int* pa = &a;
*pa = 10;
printf("%d\n", *pa);
return 0;
}
或者
int main()
{
int* pa = NULL;
return 0;
}
养成好习惯,这样程序就不会简单的出错。
当然pa=NULL的时候,是不能使用,修改‘*p’的。
(2)使用的空间还给了内存
这种情况在于用指针在函数里指向了一块函数内的空间,函数结束了仍然使用该指针访问已经销毁了的函数空间,这样也会出错。这种情况同样出现于free()函数里。所以指针在使用完成之后最好将其置空。
8. 指针的作用
既然可已使用变量自己修改自己,那为什么需要使用地址呢?
这是因为函数的返回值只能返回一个内容,所以如果要在函数内部修改2个或者以上的变量就必须要用到指针。因为函数形参只是实参的一份临时拷贝,如果不使用指针将无法修改实参。例如交换两个数的值。
void Swap1(int a, int b)
{
a = 10;
b = 20;
}
void Swap2(int* pa, int* pb)
{
*pa = 10;
*pb = 20;
}
int main()
{
int a = 0, b = 0;
Swap1(a, b);
printf("a = %d, b = %d\n", a, b);
a = 0;
b = 0;
Swap2(&a, &b);
printf("a = %d, b = %d\n", a, b);
return 0;
}
从上面的例子就能够体现指针的作用了。还有一句重要的话要送给大家,要在函数内修改外面的数,就需要用到该函数的指针。也就是说改int就用int*,改int*就用int**。依次类推。
二. 指针与数组的关系
1. 数组名的理解
(1) sizeof(数组名)
sizeof中单独放数组名这里的数组名表示整个数组,计算的是整个数组的大小,单位是字节。
int main()
{
int arr[10] = {0};
printf("%d\n", sizeof(arr));//整个数组的大小
printf("%d\n", sizeof(arr[0]));//的一个元素的大小
printf("%d\n", sizeof(arr + 1));//第二个元素地址的大小
return 0;
}
(2)&数组名
这里的数组名表示整个数组,取出的是整个数组的地址(整个数组的地址和数组首元素的地址是相同的)除此之外,任何地方使用数组名,数组名都表示首元素的地址。
int main()
{
int arr[10] = {0};
printf("arr = %p\n", arr);
printf("arr = %p\n", arr + 1);
printf("&arr[0] = %p\n", &arr[0]);
printf("&arr[0] = %p\n", &arr[0] + 1);
printf("&arr = %p\n", &arr);
printf("&arr = %p\n", &arr + 1);
}
可以看出来arr,&arr[0],&arr,它们指向位置是相同的,但是类型是不同的,这里的arr和&arr[0]是一样的,+1跳过了4个字节。但是&arr表示整个数组,+1跳过了40个字节。
2. 数组与指针的关系
1.数组就是数组,是一块连续的空间 (数组的大小和数组元素个数和元素类型都有关系)
2.指针(变量)就是指针(变量),是一个变量(4/8个字节)
3.数组名是地址,是首元素的地址
4.可以使用指针来访问数组
int main()
{
int arr[10]= {0};
int* pa = arr;
int i = 0;
int sz = sizeof(arr) / sizeof(arr[0]);
for(i = 0; i < sz; i++)
{
scanf("%d", pa + i);
}
for(i = 0; i < sz; i++)
{
printf("%d ", *(pa + i));
}
printf("\n");
return 0;
}
3. 函数形参中的指针和数组
数组传参的时候,形参是可以写成数组的形式的但是本质上还是指针变量。
(1)数组传参的本质是传递了数组首元素的地址,所以形参访问的数组和实参的数组是同一个数组的。
(2)形参的数组是不会单独再创建数组空间的,所以形参的数组是可以省略掉数组大小的。
void Print(int arr[10])//等同于int *arr
{
int sz = sizeof(arr) / sizeof(arr[0]);
for(int i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
}
int main()
{
int arr[]= {1,2,3,4,5,6,7,8,9,10};
Print(arr);
return 0;
}
所以sz的计算最好创建在原函数里面然后传到函数的形参上。
4. 指针数组
指针数组,顾名思义就是存放指针的数组,数组的每一个元素都是同类型的指针。
int main()
{
int arr1[] = {1,2,3,4,5};
int arr2[] = {2,3,4,5,6};
int arr3[] = {3,4,5,6,7};
int* arr[] = {arr1,arr2,arr3};
//打印数组
int i = 0;
for(i = 0; i < 3; i++)
{
int j = 0;
for(j = 0; j < 5; j++)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
return 0;
}
使用起来的结果,如上。“arr”就是指针数组。
5. char*指针的特殊用法
int main()
{
char* p = "abcdef";
*p = 'w';//出现异常
printf("%s\n", p);
return 0;
}
说到特殊用法,就是用指针接收一个字符串的地址,如图所示。这样子写的字符串是可以被打印的,但是不能修改。因为这样使用的字符串不是存在栈区的,而是在静态区内。这就和计算机内存的分配有关了。计算机主要分为:栈区、堆区、静态区。还有一些其他的部分,但一般不能访问。
int main()
{
char* p = "abcdef";
//*p = 'w';//出现异常
printf("%s\n", p);
return 0;
}
直接打印时可以的。除此之外,存放的地址也和数组的不同。
int main()
{
char str1[] = "hello bit.";
char str2[] = "hello bit.";
const char* str3 = "hello bit.";
const char* str4 = "hello bit.";
if (str1 == str2)
printf("str1 and str2 are same\n");//1
else
printf("str1 and str2 are not same\n");//2
if (str3 == str4)
printf("str3 and str4 are same\n");//3
else
printf("str3 and str4 are not same\n");//4
return 0;
}
对于以上代码,可以知道的是“str1”!=“str2”。但是“str3”和“str4”呢?
结果如下,静态区相同的量只创建一次,所以“str3”==“str4”。
6. 数组指针
数组指针和指针数组刚好相反,数组指针表示的是指向数组的指针。
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int* p1 = arr;
//int* int*
int (*p2)[10] = &arr;
// int(*)[10] int(*)[10]
printf("%p\n", p1);
printf("%p\n", p1 + 1);
printf("%p\n", p2);
printf("%p\n", p2 + 1);
return 0;
}
这里的“p2”就是数组指针,它指向的是整个数组。
这一点从运行结果上也可以看出。需要注意的是指针的类型不同是不能被赋值的,虽然地址相同但是解引用所提取的空间大小不同。
void Print(int arr[3][5], int r, int c)
{
int i = 0;
for(i = 0; i < 3; i++)
{
int j = 0;
for(j = 0; j < 5; j++)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
}
int main()
{
int arr[3][5] = {1,2,3,4,5,6,7,8,9,10,5,4,3,2,1};
Print(arr, 3, 5);
return 0;
}
形参里的二维数组实际上也是个指针,如果改成一下形式运行结果不变。
void Print(int (*arr)[5], int r, int c)
所以说数组与指针其实是有着密切关联的,具体细节需要多编程,多体会。
三. 函数与指针
1. 回调函数
函数名本质上是一个地址,把这个地址赋给指针,用这个指针访问函数称为回调函数。
int (*p)(int, int) = Add;
通过这样的定义可以获得Add函数的地址,直接访问Add。当然加上&在Add前也是一样的。
int Add(int x, int y)
{
return x + y;
}
int main()
{
int (*p)(int, int) = Add;
printf("%d\n", p(3, 5));
printf("%d\n", (*p)(3, 5));
printf("%d\n", Add(3, 5));
return 0;
}
得到的结果如下。
以上三种方案调用函数都是可行的。
2. 回调函数的作用
介绍了回调函数的组成,那么接下来来讲讲回调函数的作用。
回调函数的作用主要用在在函数里调用不固定的函数,这个说法就很明显了,比如在qsort()函数里就需要一个比较大小的函数,最后一个地方需要传一个函数名进去。因为不同种类的元素比较的方法不同,需要单独设计满足要求的函数在传函数名进去。
//void qsort(void* base, //指针,指向的是待排序的数组的第一个元素
// size_t num, //是base指向的待排序数组的元素个数
// size_t size, //base指向的待排序数组的元素的大小
// int (*compar)(const void*, const void*)//函数指针 - 指向的就是两个元素的比较函数
// );
//void* 类型的指针是无具体类型的指针,这种类型的指针不能直接解引用,也不能进行+-整数的运算
//qsort 函数有实现者 -
//qsort 函数的使用者 - 明确的知道要排序的是什么数据,这些数据应该如何比较,所以提供两个元素的比较函数
struct Stu
{
char name[20];
int age;
};
int cmp_stu_by_name(const void* p1, const void* p2)
{
return strcmp(((struct Stu*)p1)->name, ((struct Stu*)p2)->name);
}
int cmp_stu_by_age(const void* p1, const void* p2)
{
return ((struct Stu*)p1)->age-((struct Stu*)p2)->age;
}
void main()
{
struct Stu arr[3] = { {"zhangsan", 20},{"lisi", 35},{"wangwu", 18}};
int sz = sizeof(arr) / sizeof(arr[0]);
//qsort(arr, sz, sizeof(arr[0]), cmp_stu_by_name);
qsort(arr, sz, sizeof(arr[0]), cmp_stu_by_age);
}
这样就可以方便排序,qsort()不需要针对不同的排序方式写不同的函数内容,只用把回调比较的函数就行。
四. sizeof()和strlen()
1. sizeof
sizeof()用来计算里面内容所占的字节,在这之中有两个例外:
(1)sizeof(数组名) - 数组名表示整个数组,计算的是整个数组的大小,单位是字节。
(2)&数组名 - 数组名表示整个数组,取出的是整个数组的地址
那么接下来举一些简单的例子来说明。
为了方便比较strlen与sizeof的区别,这里的数组使用的是char类型。
int main()
{
char arr[] = { 'a','b','c','d','e','f' };
printf("%d\n", sizeof(arr));//数组名单独放在sizeof内部了,计算的是数组的大小,单位是字节-6
printf("%d\n", sizeof(arr + 0));//arr是数组名表示首元素的地址,arr+0还是首元素的地址,是地址就是4/8个字节
printf("%d\n", sizeof(*arr));//arr是首元素的地址,*arr就是首元素,大小就是1个字节
//*arr -- arr[0] - *(arr+0)
printf("%d\n", sizeof(arr[1]));//arr[1] 是第二个元素,大小也是1个字节
printf("%d\n", sizeof(&arr));//&arr 是数组地址,数组的地址也是地址,大小是4/8个字节
//&arr -- char (*)[6]
printf("%d\n", sizeof(&arr + 1));//&arr+1, 跳过整个数组,指向了数组后边的空间,4/8个字节
printf("%d\n", sizeof(&arr[0] + 1));//第二个元素的地址,是地址就是4/8字节
return 0;
}
以上就是sizeof里面包含不同内容时的结果,有些复杂。
2. strlen
对于strlen来说,就是从给的地址往后数,数到“\0”停止。需要注意的是strlen专门用在求字符串长度,其他的是不行的,而且一定传的是一个地址。
(1)如果字符串中没有“\0”。
#include <string.h>
int main()
{
char arr[] = { 'a','b','c','d','e','f' };
printf("%d\n", strlen(arr));//arr是首元素的地址,数组中没有\0,就会导致越界访问,结果就是随机的
printf("%d\n", strlen(arr + 0));//arr+0是数组首元素的地址,数组中没有\0,就会导致越界访问,结果就是随机的
printf("%d\n", strlen(&arr));//&arr是数组的地址,起始位置是数组的第一个元素的位置,随机值 x
printf("%d\n", strlen(&arr + 1));//随机值 x-6
printf("%d\n", strlen(&arr[0] + 1));//从第2个元素开始向后统计的,得到的也是随机值 x-1
return 0;
}
如果传的不是地址会被转化为地址,这个地址就相当于野指针,而电脑中有部分内存是无法被访问的,所以编译会出错。
//printf("%d\n", strlen(*arr));//arr是首元素的地址,*arr是首元素,就是'a','a'的ascii码值是97
//就相当于把97作为地址传递给了strlen,strlen得到的就是野指针, 代码是有问题的
//printf("%d\n", strlen(arr[1]));//arr[1]--'b'--98,传给strlen函数也是错误的
(1)如果字符串中有“\0”。
替换调定义数组的代码换成如下代码。
char arr[] = "abcdef";
其他打印的代码不变,结果会如何呢?
int main()
{
char arr[] = "abcdef";
printf("%d\n", strlen(arr));//6
printf("%d\n", strlen(arr + 0));//arr首元素的地址,arr+0还是首元素的地址,向后在\0之前有6个字符
printf("%d\n", strlen(&arr));//&arr是数组的地址,也是从数组第一个元素开始向后找,6
//&arr -- char (*)[7]
//size_t strlen(const char* s);
printf("%d\n", strlen(&arr + 1));//随机值
printf("%d\n", strlen(&arr[0] + 1));//5
return 0;
}
其实就是多了一个“\0”的结尾,那么字符串大小就是确定的了,当然strlen(&arr + 1)越界了,不算。
3. 二维数组中的sizeof
这个玩意,及比一维数组难多了,什么时候表示元素,什么时候表示一行,什么时候表示整个数组。
当然判断大小的分水岭在于里面的东西是不是指针,是指针就是4/8。不是指针再具体来看是什么就好了。
int main()
{
int a[3][4] = { 0 };
printf("%d\n", sizeof(a));//a是数组名,单独放在sizeof内部,计算的是数组的大小,单位是字节 - 48 = 3*4*sizeof(int)
printf("%d\n", sizeof(a[0][0]));//a[0][0] 是第一行第一个元素,大小4个字节
printf("%d\n", sizeof(a[0]));//a[0]第一行的数组名,数组名单独放在sizeof内部了,计算的是数组的总大小 16 个字节
printf("%d\n", sizeof(a[0] + 1));//a[0]第一行的数组名,但是a[0]并没有单独放在sizeof内部,所以这里的数组名a[0]就是
//数组首元素的地址,就是&a[0][0],+1后是a[0][1]的地址,大小是4/8个字节
printf("%d\n", sizeof(*(a[0] + 1)));//*(a[0] + 1)表示第一行第二个元素,大小就是4
printf("%d\n", sizeof(a + 1));//a作为数组名并没有单独放在sizeof内部,a表示数组首元素的地址,是二维数组首元素的地址,也就是
//第一行的地址,a+1,跳过一行,指向了第二行,a+1是第二行的地址,a+1是数组指针,是地址大小就是4/8个字节
printf("%d\n", sizeof(*(a + 1)));//1.a+1是第二行的地址,*(a+1)就是第二行,计算的是第二行的大小 - 16
//2. *(a + 1) == a[1], a[1]是第二行的数组名,sizeof(*(a + 1))就相当于sizeof(a[1]),意思是把第二行的数组名单独放在
//sizeof内部,计算的是第二行的大小
printf("%d\n", sizeof(&a[0] + 1));//a[0]是第一行的数组名,&a[0]取出的就是数组的地址,就是第一行的地址
//&a[0]+1 就是第二行的地址,是地址大小就是4/8个字节
printf("%d\n", sizeof(*(&a[0] + 1)));//*(&a[0] + 1)意思是对第二行的地址解引用,访问的就是第二行,大小是16字节
printf("%d\n", sizeof(*a));//a作为数组名并没有单独放在sizeof内部,a表示数组首元素的地址,是二维数组首元素的地址,也就是
//第一行的地址,*a就是第一行,计算的就是第一行的大小,16字节
//*a == *(a+0) == a[0]
printf("%d\n", sizeof(a[3]));//a[3]无需真实存在,仅仅通过类型的推断就能算出长度
//a[3]是第四行的数组名,单独放在sizeof内部,计算的是第四行的大小,16个字节
//sizeof(int);//4
//sizeof(3 + 5);//4
return 0;
}
读起来有点绕,建议多看几遍。
关于sizeof的特点再提几句:
1)里面可以放类型,直接计算类型的大小
2)sizeof里面的内容不会真的拿去运算,只会判断类型。
3)sizeof里面的数组越界了也没关系,因为不会真的访问它。
五. 作者感想
指针是个蛮复杂的东西,想要学好需要打下牢固的基础。
除此之外,写博客好累,而且这才写第一遍。感觉写的不太好,之后回头再润润色。