指针是什么?
指针是编程语言中的一个对象,利用地址,它的值可以指向存在电脑存储器中另一个地方的值。
一般这样说指针就是地址。指针变量就是变量(用来存放地址的变量)。
变量有不同的类型,整型,浮点型。指针同样也有类型。
char *pc = NULL;
short *ps = NULL;
int *pi = NULL;
long *pl = NULL;
float *pf = NULL;
double *pd = NULL
char *
类型指针存放char
类型变量的地址;
int *
类型指针存放int
类型变量的地址。
1.指针+ - 整数、解引用:
#include
int main()
{
int n = 10;
char *pc = (char *)&n;
int *pi = &n;
printf("%p\n", &n); //n的地址
printf("%p\n", pc);//pc里面放的n的地址
printf("%p\n", pc+1);//pc+1,实际是加上自己类型的大小
printf("%p\n", pi);//pi里面放的n的地址
printf("%p\n", pi+1);//pi+1,实际是加上自己类型的大小
return 0;
}
解引用这块看这样一段代码:
int n = 0x11223344;
char *pc = (char *)&n;
int *pi = &n;
*pc = 0;
*pi = 0;
执行完初始化语句时,内存为这样:
执行完*pc=0
之后,内存里面发生了变化:
pc
指向n的第一个地址发生了改变。
执行完pi
之后:
pi
指向n的地址都发生了改变(清0).。
为什么会这样呢?
因为类型决定了能访问几个字节,char*
类型只能访问一个字节。int*
类型能访问4个字节。
总结:
2.野指针: 野指针就是指针所指向的位置是不可知的(随机的,不正确的,没有明确限制的)。
形成野指针的两个原因:
int *p;//指针变量未初始化,默认为随机值
*p = 20;
int arr[10] = { 0 };
int *p = arr;
int i = 0;
for (i = 0; i <= 11; i++)
{
*(p++) = i;//在这里,指针指向已经越界了,超出了arr的范围,p就是野指针
}
如何避免野指针:
3.指针运算
int my_strlen(char *arr)
{
char *p = arr;
while (*p != NULL)
{
p++;
}
return p - arr;
}
通过指针 - 指针的方式,可求出,该字符串的元素个数。若想求数组,字符串所经历的元素个数,可用指针 - 指针的方式。
总结:
1.指针 - 指针代表:两个指针之间所经历的“元素”个数。(不一定是1个字节的数)
2.两个指针相减,必须做到,这个指针类型是一致的。
3.两个指针相减,一般建议两个指针指向同一块内存。
①:
#define N 5
float values[5];
float *p = NULL;
for (p = &values[N]; p > &values[0];)
{
*--p = 0;
}
②:
#define N 5
float values[5];
float *p = NULL;
for (p = &values[N-1]; p >= &values[0];p--)
{
*p = 0;
}
在vs环境下,两种方案都可以运行,但应该尽量避免第二种方案。因为标准不保证第二种是可行的。
标准规定:
允许指向数组元素的指针与指向数组 最后一个元素后面的那个内存位置 的指针比较,但是不允许与指向 第一个元素之前的那个内存位置 的指针进行比较。
4.指针和数组:两者无任何关系。
数组名在大部分情况下表示的是数组首元素的地址。
二级指针:存放一级指针的地址。(指针本身也包含着地址)
指针数组:是数组,用来存放指针的数组。如:int *arr[5];
数组指针:是指针,指向数组的指针。如:int (*arr)[5];
在二级指针中,我们经常用到关键字const
来修饰变量,使指针的指向和内容不能做更改。练习:
int a = 20;
int *p = &a;//p指向a,对p解引用,就是a值
int **q = &p;//q指向p,一次解引用,为p=&a,二次解引用,为a的值
const int *p = &a;//p的内容可以更改,p的指向不能更改。不能通过解引用的方式对a值进行更改,即*p = 5,是错误的
int const *p = &a;//同上
const int *const p = &a;//p的内容和p的指向都不能更改。
const int **q = &p;//q的内容和指向可以更改,*q的指向(**q)不能更改
int const **q = &p;//同上
int *const *q = &p;//q的内容可以更改,q的指向不能更改(*q的内容不能更改)。*q的指向(**q)可以更改
int **const q = &p;//q的内容不能更改,q的指向(*q)可以更改,*q的指向(**q)可以更改
int *const *const q = &p;//q的内容和指向(*q)都不能更改,*q的指向(**q)可以更改
int const *const *q = &p;//q的内容可以更改,q的指向(*q)不能更改,*q的指向(**q)不可以更改
int const * const *const q = &p;//q的内容和指向(*q)都不能更改,*q的指向(**q)不可以更改
字符指针:char *
两种用法:
const char *s
char arr[]
char *s="hello world!"
是把字符串“hello world!”
的首地址放到字符指针s
中。一般在前面加const
,表示,该字符串不允许被修改。
char str1[] = "hello world!";
char str2[] = "hello world!";
char *str3 = "hello world!";
char *str4 = "hello world!";
if (str1 == str2)
{
printf("1 and 2 are same");
}
else
{
printf("1 and 2 are not same");
}
if (str3 == str4)
{
printf("3 and 4 are same");
}
else
{
printf("3 and 4 are not same");
}
在此例子中,str1
和str2
是不同的变量,开辟了不同的地址空间。
str3
和str4
的地址是一样的。因为"hello world!"
存在于字符常量区,只会保存一份,所以只读就可以了,故*str3
和 *str4
只要指向它就行了。
&数组名和数组名:
当对进行输出的时候,他们的值是一样的,但是其含义和类型是不同。
数组名:数组首元素的地址。
&数组名 :对整个数组进行取地址,但输出的时候,输出的是第一个元素的地址。
数组类型的理解:不能只看前面的类型,更要看[]里面的标定元素个数。
int arr[10];
int arr1[10];
int arr2[9];
前两个类型相同,第三个和前面两个类型不相同。
数组指针的使用:int (*p)[5]
在二维数组传参时,方便降维,降维成一维数组指针。
void print_arr(int (*arr)[5], int row, int col)
{
int i = 0;
for (; i < row; i++)
{
int j = 0;
for (; j < col; j++)
{
//printf("%d ", arr[i][j]);//1
printf("%d ", *(*(arr + i) + j));//2
}
}
}
int main()
{
int arr[3][5] = { 1,2,3,4,5,6,7,8,9,10,15 };
print_arr(arr, 3, 5);
return 0;
}
其中第二种方式的意思是:arr+1
是指向下一个一维数组,对其解引用,代表的是,当前的一维数组,+j
呢,是当前数组中第j个
元素。
数组传参要注意类型是否可以传参,同时,一维数组中,不能省略数组的个数。二维数组中,不能省略第二个维度的个数。
指针传参:
void print(int *p, int size)
{
int i = 0;
for (; i < size; i++)
{
printf("%d ", p[i]);
}
}
int main()
{
int arr[11] = { 1,2,3,4,5,6,7,8,9,10,11 };
int *p = arr;
print(p, 11);
return 0;
}
指针也是变量,在传参的时候,也要进行形参实例化,指针也要形成临时变量,不过,指针最大的特征是形成的临时变量内容和传入指针内容是相同的。决定了,上述代码中,两个指针指向的目标是一样的。上面两个P是不一样的,但指向相同的目标。
函数的参数为一级指针时,可以接受什么参数?
一维数组:int arr[] test(arr)
一级指针 int *p=xxx test§
具体变量 int a=10 test(&a)
函数的参数为二级指针时,可以接受什么参数?
二级指针:int **q test(q)
一级指针:int *q test(&q)
指针数组:int *q[10] test[q]
函数指针:函数本质是代码块,代码本质是包含多组代码的,也就决定了会包含一个地址序列。
函数名(only),代码的是代码块的起始地址。
void print(int *p, int size)
{
int i = 0;
for (; i < size; i++)
{
printf("%d ", p[i]);
}
}
printf("%p\n", print);
printf("%p\n", &print);
对函数输出地址和&函数地址是一样的。
如何保存一个函数的地址呢?
使用函数指针: void (*p)();
函数指针数组:int (*p[5])();
将函数的地址存放到一个数组中,那么这个数组就叫做函数指针数组。
函数指针数组的一个用途为转移表:比如在实现计算器功能中
void menu()
{
printf("############################################\n");
printf("###### 1.add 2.sub ######\n");
printf("###### 3.mul 4.div ######\n");
printf("###### 0.exit ######\n");
}
int add(int x, int y)
{
return x + y;
}
int sub(int x, int y)
{
return x - y;
}
int mul(int x, int y)
{
return x * y;
}
int div(int x, int y)
{
if (0 == y)
{
return 0;
}
return x / y;
}
int main()
{
int choice = 1;
menu();
printf("请做出你的选择:>");
scanf("%d", &choice);
int(*p[5])(int x, int y) = {0, add,sub,mul,div };//函数指针数组,可以很方便的实现许多相同参数的函数的调用,转移表
while (choice)
{
int x = 0;
int y = 0;
printf("请输入两个数:>");
scanf("%d %d", &x, &y);
int ret = (*p[choice])(x, y);
printf("%d\n", ret);
menu();
printf("请做出你的选择:>");
scanf("%d", &choice);
}
return 0;
}
指向函数指针数组的指针
函数指针:int (*p)();
函数指针数组:int (p[5])();
指向函数指针数组的指针:int ((*p)[5])();
回调函数: 通过一个函数指针调用的函数,如果把这个函数的地址作为另一个函数的参数传递,当这个指针被用来调用其所指向的函数时,这就是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
你到一个商店买东西,刚好你要的东西没有货,于是你在店员那里留下了你的电话,过了几天店里有货了,店员就打了你的电话,然后你接到电话后就到店里去取了货。在这个例子里,你的电话号码就叫回调函数,你把电话留给店员就叫登记回调函数,店里后来有货了叫做触发了回调关联的事件,店员给你打电话叫做调用回调函数,你到店里去取货叫做响应回调事件。–by常溪玲
回调函数模拟实现qsort(冒泡排序法):
qsort函数是C函数库中,可以比较任意相同类型(char int double float),并将之排序
int int_cmp(void *s, void *p)//回调函数
{
int *s1 = (int *)s;
int *p1 = (int *)p;
return *s1-*p1;
}
void swap(void *s, void *p, int size)
{
int i = 0;
char *s1 = (char *)s;
char *p1 = (char *)p;
for (; i < size; i++)
{
s1[i] ^= p1[i];
p1[i] ^= s1[i];
s1[i] ^= p1[i];
}
}
void my_qsort(void *base, int num, int size, int (*cmp)(void *, void *))//实现调用回调函数的函数
{
int i = 0;
int j = 0;
for (; i < num - 1 ; i++)
{
int flag = 1;
for (j = 0; j < num - i - 1; j++)
{
if (cmp((char *)base + j * size, (char *)base + (j + 1) * size) > 0)
{
flag = 0;
swap((char *)base + j * size, (char *)base + size * (j + 1),size);
}
}
if (1 == flag)
{
break;
}
}
}
int main()
{
int arr[] = { 5,7,8,1,5,4,1,6,3,2 };
int num = sizeof(arr) / sizeof(arr[0]);
my_qsort(arr, num, sizeof(int), int_cmp);
for (int i = 0; i < num; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
一些指针和数组的练习题:
int a[] = { 1,2,3,4 };
printf("%d\n", sizeof(a));
printf("%d\n", sizeof(a+0));
printf("%d\n", sizeof(*a));
printf("%d\n", sizeof(a+1));
printf("%d\n", sizeof(a[1]));
printf("%d\n", sizeof(&a));
printf("%d\n", sizeof(*&a));
printf("%d\n", sizeof(&a+1));
printf("%d\n", sizeof(&a[0]));
printf("%d\n", sizeof(&a[0]+1));
sizeof(a),求的是整个数组的字节大小,故为16
sizeof(a+0),a是首元素的地址,加0,仍然为第0个元素的地址,地址(指针)大小为4
sizeof(a),a是首元素的地址,对a解引用,为1,字节大小为4
sizeof(a + 1);//a为首元素的地址,加 1,为下一个元素(2)的地址,地址(指针)大小为4
sizeof(a[1]);//a[1]=2,字节大小为4
sizeof(&a);//&a,对整个数组进行取地址,即数组的地址,地址(指针)大小为4
sizeof(&a);//对&a解引用,数组的地址解引用,一定为一个数组,大小为16
sizeof(&a+1),&a是对整个数组取地址,加+1之后,指向最后一个元素的下一位置处。为4
sizeof(&a[0]);//a[0]=1,对其取地址,大小为4
sizeof(&a[0] + 1);//a[0]=1,对其取地址,再加1,对下一个元素(2)进行取地址,地址(指针)大小为4
char arr[] = { 'a','b','c','d','e','f' };
printf("%d\n", sizeof(arr));//数组的大小为6
printf("%d\n", sizeof(arr+0));//第0个元素(a)的地址,4
printf("%d\n", sizeof(*arr));//第0个元素(a)解引用,大小为1
printf("%d\n", sizeof(arr[1]));//第1个元素(b)解引用,大小为1
printf("%d\n", sizeof(&arr));//对整个数组取地址,数组的地址,地址(指针)大小为4
printf("%d\n", sizeof(&arr+1));//对整个数组取地址,数组的地址,再加1,指向数组最后一个元素的下一位置,地址(指针)大小为4
printf("%d\n", sizeof(&arr[0]+1));//对第0个元素取地址,再加1,为b元素的地址,地址(指针)大小为4
char arr[] = { 'a','b','c','d','e','f' };
printf("%d\n", strlen(arr));//数组里面无‘\0’,随机值
printf("%d\n", strlen(arr+0));//随机值
//printf("%d\n", strlen(*arr));//传参错误,类型不匹配
//printf("%d\n", strlen(arr[1]));//传参错误,类型不匹配
printf("%d\n", strlen(&arr));//随机值,&arr是数组的地址,数组的地址char (*p)[6],告警,类型不匹配
printf("%d\n", strlen(&arr+1));//随机值,数组的地址char (*p)[6],告警,类型不匹配
printf("%d\n", strlen(&arr[0]+1));//随机值
在这里面,为什么会发生传参错误问题?
strlen定义为:size_t strlen(const char *str);
传入的必须为一个地址,所以上面34类型不匹配,导致错误。
5:&arr是数组的地址,数组的地址要用数组指针char (*p)[6],但它和数组的起始地址&arr[0],数值是一样的,含义不一样,所以传给strlen时,发生隐式类型转换,转换为字符指针,故最后可得到一个随机值
6:&arr+1,就相当于&arr[6],类型不匹配,char (*p)[6],也发生了隐式类型转换。
结果:
char arr[] = "abcdef";
printf("%d\n", sizeof(arr));//字符串,有'\0',大小为7
printf("%d\n", sizeof(arr+0));//arr+0,为首元素的地址,地址(指针)大小为4
printf("%d\n", sizeof(*arr));//首元素的字节大小1
printf("%d\n", sizeof(arr[1]));//第一个元素的字节大小1
printf("%d\n", sizeof(&arr));//数组的地址,地址(指针)大小为4
printf("%d\n", sizeof(&arr+1));//数组的地址+1,数组末下一个地址,地址(指针)大小为4
printf("%d\n", sizeof(&arr[0]+1));//第1个元素的地址(b),地址(指针)大小为4
char arr[] = "abcdef";
printf("%d\n", strlen(arr));//字符串长度6
printf("%d\n", strlen(arr+0));//传入首元素的地址,字符串长度6
//printf("%d\n", strlen(*arr));//传参错误
//printf("%d\n", strlen(arr[1]));//传参错误
printf("%d\n", strlen(&arr));//结果为6,但会有告警,类型不匹配,char(*p)[7]
printf("%d\n", strlen(&arr+1));//随机值,有告警,类型不匹配,char(*p)[7]
printf("%d\n", strlen(&arr[0]+1));//5
char *p = "abcdef";
printf("%d\n", sizeof(p));//p存放的地址,大小为4
printf("%d\n", sizeof(p+1));//p+1为b的地址,大小为4
printf("%d\n", sizeof(*p));//char *解引用,为char,大小为1
printf("%d\n", sizeof(p[0]));//p[0]='a',大小为1
printf("%d\n", sizeof(&p));//&p,p本身的地址,大小为4
printf("%d\n", sizeof(&p+1));//&p+1,为地址,大小为4
printf("%d\n", sizeof(&p[0]+1)); //&p[0] + 1,‘b’的地址,大小为4
int a[3][4] = { 0 };
printf("%d\n", sizeof(a));//4*3*4=48,字节大小为48
printf("%d\n", sizeof(a[0][0]));//第0个数组,第0个元素,int型,大小为4个字节
printf("%d\n", sizeof(a[0]));//第0个数组,里面有4个元素,大小为16
printf("%d\n", sizeof(a[0]+1));//第0个数组,第一个元素,大小为4
printf("%d\n", sizeof(*(a[0]+1)));//第0个数组,第一个元素解引用,大小为4
printf("%d\n", sizeof(a+1));//第一个数组的地址,大小为4
printf("%d\n", sizeof(*(a+1)));//第一个数组的地址,解引用,为第一个数组,大小为16
printf("%d\n", sizeof(&a[0]+1));//第0个数组的地址+1,为第1个数组的地址,大小为4
printf("%d\n", sizeof(*(&a[0]+1)));//第0个数组的地址+1,为第1个数组的地址,解引用大小为16
printf("%d\n", sizeof(*a));//首元素的地址,解引用,为16
printf("%d\n", sizeof(a[3]));//16,不会报错,含义一致,但不能写入
综合题:
int main()
{
int a[5] = { 1,2,3,4,5 };
int *ptr = (int *)(&a + 1);
printf("%d %d\n", *(a + 1), *(ptr - 1));
return 0;
}
&a,是整个数组的地址,+1,指向数组最后一个元素的下一个地址。
*(a+1),a是首元素的地址,加1,为第一个元素的地址,解引用为2
*(ptr-1),为数组最后一个元素的地址,解引用为5
2.
struct Test
{
int num;
char *name;
short sdate;
char cha[2];
short sba[4];
}*p;
int main()
{
p = 0x100000;
printf("%p\n", p + 0x1);
printf("%p\n", (unsigned long)p+0x1);
printf("%p\n", (unsigned int*)p + 0x1);
return 0;
}
这个结构体的大小为20字节。
p + 0x1,实际是加上自己类型的大小,20个字节,则为0x100014
(unsigned long)p+0x1,p被强转为了无符号长整数,是整数,加1就是直接加1,为0x100001
(unsigned int*)p + 0x1,p被强转为了unsigned int*,大小为4个字节,加1就是加类型的大小,为0x100004
3.
int main()
{
int a[4] = { 1,2,3,4 };
int *ptr = (int *)(&a + 1);
int *ptr2 = (int *)((int)a + 1);
printf("%x %x\n", ptr[-1], *ptr2);
return 0;
}
int main()
{
int a[3][2] = { (0,1),(2,3),(4,5) };
int *p;
p = a[0];
printf("%d\n", p[0]);
return 0;
}
int main()
{
int a[5][5];
int(*p)[4];
p = a;
printf("%p,%d\n", &p[4][2] - &a[4][2], &p[4][2] - &a[4][2]);
return 0;
}
%p为无符号整数,所以被解释为了一个特别大的数
指针-指针,为两指针所经历的元素的个数
结果:
6.
int main()
{
int a[2][5] = { 1,2,3,4,5,6,7,8,9,10 };
int *ptr1 = (int *)(&a + 1);
int *ptr2 = (int *)(*(a + 1));
printf("%d %d\n", *(ptr1 - 1), *(ptr2 - 1));
return 0;
}
ptr1指向数组末下一个地址。
ptr2指向二维数组中,第二个数组
则结果分别为:10,5
int main()
{
char *a[] = { "work","at","school" };
char **pa = a;
pa++;
printf("%s\n", *pa);
return 0;
}
a为指针数组。
二级指针pa指向a的起始地址,对pa++,指向指针数组中下一个数组,即“at”
结果为:
int main()
{
char *c[] = { "enter","new","point","first" };
char **cp[] = { c + 3,c + 2,c + 1,c };
char ***cpp = cp;
printf("%s\n", **++cpp);
printf("%s\n", *--*++cpp + 3);
printf("%s\n", *cpp[-2] + 3);
printf("%s\n", cpp[-1][-1] + 1);
return 0;
}