指针的基本概念
一、内存和地址
想要理解指针,就先要理解内存和地址。我们知道计算机上的CPU(中央处理器)在处理数据时,需要的数据是在内存中读取的,处理之后的数据也会放回到内存中。为了高效管理内存空间,计算机是将内存划分为一个个的内存单元,而每个内存单元的大小是 1字节(byte)。
计算机中最小的单位是比特(bit),1 byte = 8 bit。一个比特能够存储一个二进制的1或0。为了高效管理,即快速准确地到达每一个数据所在的内存,我们就给每个内存单元进行编号,这个编号就是相应数据的地址。在C语言中地址又被称为指针。通常所讲的指针的实质就是地址。
二、指针变量
①:取地址操作符“&”
取地址操作符可以取出其后数据的地址,如果想要观察这一地址,可以用%p的形式进行输出。
int a=1;
printf("%p",&a);
//结果将以16进制的形式输出
实际上,int类型的变量a会在内存上申请4个字节的空间,而取地址操作符取出的是四个字节中较小的地址。
②:指针变量
int a=1;
int * pa=&a;
上述代码中的“*”说明我们创建的变量pa是指针变量,“int”则保证了pa指向的变量是int类型,由此我们就创建了指向a的地址(实质是四个字节中最小的地址)的指针变量。
指针变量是存放指针的,即是存放a的地址的变量。我们口头上说的指针通常是指针变量。其他类型的指针变量的创建结构同理,如:
char ch='x';
char * pc=&ch;
③:解引用操作符“*”
*pa=2;
解引用操作符是用来对指针变量解引用的,*pa就等价于a,上述代码进行的操作就是将a赋值为2。
④:指针变量的大小
指针变量的大小取决于其内地址的存放需要多大的空间。例如在32位机器上(x86环境中),有32根地址线,地址的二进制序列就有32bit位,存放起来,需要4个字节,32bit位的空间。故32位机器上指针变量的大小都是4个字节,而与指针变量的类型无关。在64位机器上(x64环境中),则是8个字节。当然我们也可以用如下代码进行验证:
char* pc = NULL;//这里的NULL仅代表0
int* pi = NULL; //用以表示无特定的指向
printf("%zd\n", sizeof(pc));
printf("%zd\n", sizeof(pi));
三、指针变量类型的意义
指针变量的类型决定了解引用指针时的权限,也就是一次能操作几个字节,或者说向前向后走一步有多大距离。比如 int*p,p+1 是一次走了四个字节,char* 类型,+1一次走一个字节。
需要特别指出的是,void* 类型的指针是无具体类型的,又叫泛型指针,它可以接受任何类型的地址,但是不能直接进行解引用、+1、-1等操作。
四、const修饰指针
const意味着常属性、不变,可使修饰的变量无法修改。例如 const int a=1; a=2; 这样的赋值就是错误的。但a的本质还是变量,const只在语法上限制,习惯上称a为常变量。
如果仍要进行修改,可以使用如下代码:
const int a=1;
int *pa =&a;
*pa=2;
这时a已经被修改了。当然,也可以用const来修饰指针,如: const int *pa=&a; 这时上述代码将无法修改。
而const修饰指针有如下两种类型:
int *const p=&a;//const放在*右边,限制p
int const *p=&a;//const放在*左边,限制*p
限制p,则p变量无法再指向其他变量,但*p不受限制,还是可以通过p来修改p指向的空间的内容。可以写 *p=2;限制*p是不能通过p来修改p指向的空间的内容,但是p并不受限制。不能写 *p =2; 但可以写 int b=1;p=&b; 另外,我们也可以同时限制*p与p。
指针的运算与操作
一、指针运算
包括指针加减整数:这常用于打印数组等操作,比如用p++表示向后移相应类型的长度。
int a[10] = { 1,2,3,4,5,6,7,8,9,0 };
int* p = a;
for (int i = 0; i < 10; i++)
printf("%d ", *(p + i));
指针减指针:计算结果的绝对值是指针与指针之间的元素个数。这里有一个前提条件是两个指针指向同一块空间。
指针的关系运算:实质就是指针比较大小(比较地址的大小)。
二、野指针
①:概念
野指针就是指针指向的位置随机的、不正确的、没有限制的,是指向未分配或者已经释放的内存地址。
②:成因
1,未初始化,比如直接 int* p; 然后对*p进行操作。2,越界访问。
③:规避野指针
1,初始化 int*pa=&a;或者int*pa=NULL; 2,小心指针越界。 3,指针变量不在使用之后,及时置为NULL,在使用之前也要检查有效性。
三、指针的使用与传址调用
在调用函数时,如果实参传入变量,就是传值调用,传入的是地址,就是传址调用。这两者也就是是否使用指针的区别,而有时一些问题是必须使用指针的。
值得指出的是,传值调用函数时,函数的实参传给形参,形参是实参的一份临时拷贝,形参有自己独立的空间,对形参的改变不会影响实参。传址调用可以让函数与主调函数之间建立起真正的联系。如下是用函数实现两个数交换。
void swap(int *pa,int *pb)
{
int z=0;
z=*pa;*pa=*pb;*pb=z;
}
int main
{int a=1,b=2;
swap(&a,&b);}
四、指针与数组
①:对数组名的理解
数组名是首元素的地址。
有两个例外:(1)sizeof(arr)计算的是整个数组的大小。(2)&arr 取出的整个数组的地址。
举例:对于int a[10]={0};
&arr[0]和&arr[0]+1,arr和arr+1 均相差四个字节,但&arr和&arr+1相差40个字节,此时加1跳过了整个数组。
②:使用指针访问数组
//以下为部分代码,sz为数组元素个数
int *p =arr;
for(i=0;i<sz;i++)
scanf("%d",p+i);//p+i代表相应位置的地址
for(i=0;i<sz;i++)
printf("%d",*(p+i));
实际上,也可以将 *(p+i)换成p[i],这就是说arr[i]等价于*(arr+i)。
③:一维数组传参的本质
数组传参时,传递的数组名就只是数组首元素的地址。形参不会创建一个数组,需要视作地址来操作。以下是指针和函数实现冒泡排序:
void arrange(int*p, int n)
{
int i, j, flag, temp;
for (i = 0; i < n - 1; i++)
{
for (flag = 0, j = 0; j < n - 1; j++)
{
if (*(p + j) > *(p + j + 1))
{
temp = *(p + j);
*(p + j) = *(p + j + 1);
*(p + j + 1) = temp;
flag = 1;
}
}
if (flag == 0)
break;
}
for (i = 0; i < n; i++)
printf("%d ", *(p + i));
}
调用时,用arrange(arr,n),arr是数组名,n是元素个数。
④:二级指针
二级指针是指向指针的指针,存放一级指针。
int a=1;
int *p=&a;
int* * pp=&p;
//int*表示pp指向对象的类型是int*,后一个*表示pp是指针变量
printf("%d",**pp);
当然,同样的有三级指针以及更高级的指针。
⑤指针数组
是存放指针的数组。
int* arr[4]; 这里的int*表示数组arr中的每个元素都是整形指针。下例是指针数组模拟二维数组:
int arr1[]={1,2,3,4,5};
int arr2[]={2,3,4,5,6};
int arr3[]={3,4,5,6,7};
int*arr[3]={arr1,arr2,arr3};
for(i=0;i<3;i++)
for(j=0;j<5;j++)
printf("%d",arr[i][j]);//这的arr[i][j]与平常的二维数组并不相同
//arr+i == arr[i]
指针变量与函数
一、字符指针变量
char*p="abcdef"; 这是将第一个字符的地址放在p中。
(1)可以把字符串想象成一个字符数组,可以使用p[3],"abcdef"[3],但是这个数组是不能修改的,是常量字符串,可以用const char*p= 来避免出错。
(2)当常量字符串出现在表达式中的时候,他的值是第一个字符的地址。内容相同的常量字符串在内存中只会保存一份。
二、数组指针变量
数组指针变量是指针变量,存放的是数组的地址。
int arr[10]={1,2,3,4,5,6,7,8,9,10};
int (*parr)[10]=&arr;
//parr就是数组指针,括号是让parr先与*结合
//变量parr的类型是 “ int(* )[10] ”
不过事实上一维数组很少使用数组指针。
三、二维数组传参的本质
二维数组是元素是一维数组的数组,首元素的地址是第一行的地址。
void Print(int (*arr)[5],int r,int c)//用到数组指针变量
{
for(int i=0;i<r;i++)
for(int j=0;j<c;j++)
printf("%d",*(*(arr+i)+j));//这里可以逐层看
}
int main()
{
int arr[3][5]={0};
Print(arr,3,5);
}
四、函数指针变量
存放函数的地址。函数名,&函数名 都是函数的地址,意义相同。实例:
int Add(int x,int y)
{return x+y;}
//在主函数中创建变量
//...
int (*pf)(int,int)=&Add;//或者用Add
//调用
int ret=(*pf)(1,2);//此时ret即为3
//事实上,pf旁的括号和*可以不写
上述pf的类型即为int(* )(int ,int),创建函数指针变量时,一定要注意返回类型与参数类型与函数的对应。
五、函数指针数组
存放函数指针的数组。类型与数组元素(函数)对应,本质是数组。
如下是转移表的代码:(实现简单的计算器)
#include<stdio.h>
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)
{
return x / y;
}
int del(int x, int y)
{
return x % y;
}
int(*pf[])(int, int) = { NULL,add,sub,mul,div,del };
//这里的pf[]就是函数指针数组
int all(int input)
{
int x, y;
printf("输入两个操作数:");
scanf("%d %d", &x, &y);
int ret = pf[input](x, y);
return ret;
}
int main()
{
int input;
printf("加法:1 减法:2 乘法:3 除法:4 取模;5\n");
printf("选择运算:");
scanf("%d", &input);
printf("%d",all(input));
return 0;
}
六、回调函数
把函数的指针作为参数传给另一个函数,通过该指针调用的函数称作回调函数。
void calc(int (*pf)(int,int))
{//...
ret=pf(1,2);
//...
}
函数指针的应用——qsort函数
qsort函数可以完成任意类型数据的排序,需包含stdlib.h。有四个参数void*base(指针指向第一个元素),size_t num,(元素数量),size_t size,(元素大小,单位字节),int(*compare)(const void*p1,const void*p2)(指向的函数是用来比较数组中的两个元素的));
以下是对整形数据和结构体数据的比较:
#include<stdio.h>
#include<stdlib.h>
int cmp_int(const void* p1, const void* p2)
{
return *(int*)p1 - *(int*)p2; //这是升序排序,降序排只需交换p1,p2位置
}
struct stu
{
char name[15];
int high;
};
int cmp_name(const void* p1, const void* p2)
{
return strcmp(((struct stu*)p1)->name, ((struct stu*)p2)->name);
//strcmp函数用来比较字符串,实质按对应位置的ASCII码值比较
}
int cmp_high(const void* p1, const void* p2)
{
return ((struct stu*)p1)->high-((struct stu*)p2)->high;
//强制类型转换
}
int main()
{
int i;
printf("整形排序:\n");
int arr1[] = { 2,3,1,4,5 };
int sz1 = sizeof(arr1) / sizeof(arr1[0]);
printf("排序前:");
for (i = 0; i < sz1; i++)
printf("%d ", arr1[i]);
printf("\n");
qsort(arr1, sz1, sizeof(arr1[0]), cmp_int);
printf("排序后:");
for (i = 0; i < sz1; i++)
printf("%d ", arr1[i]);
printf("\n");
printf("结构体排序:\n"); //两个结构体数据不能用<,>,==来比较
struct stu arr2[] = { {"Peter",168},{"Jack",175},{"Andy",170} };
int sz2 = sizeof(arr2) / sizeof(arr2[0]);
printf("排序前:");
for (i = 0; i < sz2; i++)
printf("%s %d ", arr2[i].name,arr2[i].high);
printf("\n");
qsort(arr2, sz2, sizeof(arr2[0]), cmp_name);
printf("按名字排序:");
for (i = 0; i < sz2; i++)
printf("%s ", arr2[i].name);
printf("\n");
qsort(arr2, sz2, sizeof(arr2[0]), cmp_high);
printf("按身高排序:");
for (i = 0; i < sz2; i++)
printf("%d ", arr2[i].high);
printf("\n");
return 0;
}