C语言学习之指针的使用详解
蜗牛牛啊 人气:0一、指针概念
在学习指针之前我们先要了解一下内存,内存是存储区域,我们可以把内存划分成一个一个的内存单元,最小的内存单元是字节
计算机将数据存储在内存中,而为了能够快速查找到所需数据,计算机会将内存进行编号,通过查找编号,可以快速查找到数据。指针是内存中一个最小单元的编号,内存单元的编号也被称为地址,指针就是地址。
平时我们说的指针通常指的是指针变量,是用来存放内存地址的变量
1个字节(byte)=8个bit(bit即比特位,由0和1组成)
在32位的机器上(X86环境),地址是32个0或者1组成二进制序列,那地址就得用4个字节的空间来存储,所以一个指针变量的大小就应该是4个字节。在64位的机器上(X64环境),地址是64个0或者1组成二进制序列,那地址就得用8个字节的空间来存储,所以一个指针变量的大小是8个字节。
无论何种数据类型的指针,指针的大小在32位(X86环境)平台下是4个字节,在64位(X64环境)平台下是8个字节。
1.指针变量
我们可以通过&(取地址操作符)取出变量的内存起始地址,把地址可以存放到一个变量中,这个变量就是指针变量。
*(解引用操作符又叫间接访问操作符):对指针变量进行解引用操作,通过指针变量里面的地址找到指向的内容。
#include <stdio.h> int main() { int a = 20; char ch; int* pa = &a;//a向内存申请了4个字节的空间,所以&a的时候应该拿出第1个字节的地址 //拿出a的第1个字节的地址放在整型指针变量pa里 char* pc = &ch;//拿出ch的地址放在字符指针变量pc里 printf("%p\n",&a);//%p打印的是地址 //printf("%p\n",pa);打印出来的地址和&a打印出来的一样 printf("%p\n",&ch); return 0; }
指针变量是用来存放地址的变量,存放在指针变量中的值都被当成地址处理,同时地址是唯一标识一个内存单元的
2.指针类型
指针的类型根据你所需要的数据类型进行使用,如
- //
char* pc
;指针类型是char* - //
char** pcc
;指针类型是char** - //
int* pa
;指针类型是int* - //
double* pd
;指针类型是double* - // …
但是要注意区分指针指向的类型和指针类型之间的区别
- //
char* pc
;其中*
指pc是指针,指向的类型是char - //
char** pcc
;其中*
指pcc是指针,指向的类型是char* - //
int* pa
;*
指pa是指针,指向的类型是int - //
double* pd
;*
指pd是指针,指针指向的类型是double - // …
指针类型决定了指针进行解引用操作时访问几个字节
char* 类型的指针解引用访问1个字节;
int* 类型的指针解引用访问4个字节;
double* 类型的指针解引用访问8个字节;
例1:
#include <stdio.h> int main() { int a = 0x11223344;//用16进制表示 int* pa = &a; *pa = 0; return 0; }
上述代码在VS(小端存储)调试过程中,&a在内存中显示的是
执行完*pa=0;
后,&a在内存中变为
将例1中的指针类型改为char* 类型,用char* 类型的指针接收a的地址
int* 和 char*类型的指针都能将int类型的a存放,因为在32位平台下,指针类型的大小都是4个字节。
#include <stdio.h> int main() { int a = 0x11223344;//用16进制表示 char* pc = &a; *pc = 0; return 0; }
上述代码在VS(小端存储)调试过程中,&a在内存中显示的是
执行完*pc=0;
语句后,内存变化为
通过上述两个代码块执行后内存中的变化可以验证指针类型决定了指针进行解引用操作时访问几个字节
指针类型决定了指针的步长,指针加减整数的时候向前或者向后走一步走多大距离
(char*)+1------跳过一个char类型的大小,也就是向后走1个字节;
(int*)+1------跳过一个int类型的大小,也就是向后走4个字节;
(double*)+1------跳过一个double类型的大小,也就是向后走8个字节;
#include <stdio.h> int main() { int a = 0x11223344; int* pa = &a; char* pc = &a; printf("%p\n",pa); printf("%p\n",pc); printf("%p\n",pa+1); printf("%p\n",pc+1); return 0; }
上述代码的运行结果为
说明指针加减整数时的步长和指针指向的类型有关
练习1:
我们可以通过一个练习对其有更好的理解:将数组arr中每个元素赋值为对应下标,arr[0]为0;arr[1]为1……
#include <stdio.h> int main() { int arr[10] = { 0 }; int* p = arr;//p指向arr数组的首元素地址 int i = 0; for(i = 0;i < 10; i++) { *(p++) = i;//将数组元素赋值; //后置++,对p进行解引用,赋值之后,再向后走int类型的大小,即4个字节,依次循环 } for(i = 0;i < 10; i++) { printf("%d ",arr[i]); } return 0; }
运行结果如图
3.二级指针
#include <stdio.h> int main() { int a = 10; int* p = &a; int** pp = &p;//pp就是二级指针 *p = 20;//可以将a修改成20 **pp = 30;//可以将a修改成30 printf("%d\n",a); return 0; }
二、野指针
野指针就是指针指向的位置是不可知的,(随机的、不正确的、没有明确限制的)
1.野指针成因
1.指针未初始化
#include <stdio.h> int main() { int* p;//p没有初始化,p就是野指针 *p = 20;//赋值 return 0; }
2.指针越界访问
#include <stdio.h> int main() { int arr[10] = { 0 }; int i = 0; int sz = sizeof(arr)/sizeof(arr[0]);//计算arr数组中元素个数 int* p = arr; for(i = 0;i <= sz; i++)//共循环11次 { *p++ = i; } return 0; }
当p越过arr已有空间去越界访问的时候就是野指针了,之前不是野指针。
当指针指向的范围超出数组arr的范围时,p就是野指针
3.指针指向的空间释放
#include <stdio.h> int* test() { int num = 100;//num是局部变量,进函数创建,函数结束时销毁 return #//返回的是栈空间的地址 } int main() { int* p = test();//接收到地址,但是地址并不指向num,不知道指向的是哪里,所以*p = 200并不知道修改的是哪里 //虽然接收到地址,但是num已经销毁 *p = 200; return 0; }
2.规避野指针
1.指针初始化
#include <stdio.h> int main() { int a = 10; int* pa = &a;//明确指针 int* pa = NULL;//当pa不知道指向哪里时,置空 //NULL就是用来初始化指针的 //*pa = 20;//error,在对指针置空之后不能这样使用,NULL属于系统(内核),不能访问 return 0; }
2.小心指针越界问题
3.指针指向空间释放,及时置NULL;
#include <stdio.h> #include <stdlib.h>//malloc的头文件 int main() { //动态申请内存空间 int* p=(int*)malloc(40); //使用 //释放 free(p); p=NULL; return 0; }
4.避免返回局部变量的地址,即避免返回栈空间的地址,这样可以尽量避免指针指向的空间释放这种情况。
5.指针使用之前检查有效性
三、指针运算
1.指针±整数
指针±整数是根据指针指向的数据类型大小向前或者向后走多少个字节。
- //char* pc;pc+1就是向后走一个char类型大小的字节;
- //int* pa;pa+1就是向后走一个int类型大小的字节;
- //double* pd;pd+1就是向后走一个double类型大小的字节;
- //……
在练习1中已经对其用法进行简单演示。
2.指针-指针
两个指针相减的前提是:指针指向的是同一块连续的空间,且是同一种类型的指针。
指针和指针相减的绝对值是指针之间的元素个数
#include <stdio.h> int main() { int arr[10] = { 0 }; printf("%d\n",&arr[9]-&arr[0]);//9 printf("%d\n",&arr[9]-&arr[0]);//-9 return 0; }
练习2:
模拟实现strlen,strlen函数在对字符串中的字符进行统计时,遇见’\0’停止。
#include <stdio.h> int my_strlen(const char* arr) { const char* start = arr;//将arr数组首元素地址赋给start const char* end = arr; while(*end) { end++; } return end-start; } int main() { char arr[] = "abcdef"; int len = my_strlen(arr); printf("%d",len);//6 return 0; }
3.指针关系运算
#include <stdio.h> int main() { int i = 0; int arr[10] = { 0 }; int* p = &arr[10]; for(p = &arr[10];p > &arr[0]) { *--p = 0;//先将p向前移动4个字节,再对p赋值; } for (i = 0; i < 10; i++) { printf("%d ", arr[i]);// 全为0 } return 0; }
指针关系运算的标准规定:允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许指向数组元素的指针与指向数组第一个元素前面的那个内存位置的指针进行比较。
四、指针数组
1.指针和数组
指针和数组不是一个东西,指针是一个变量,是用来存放地址的,4/8个字节;数组能够存放一组数,是一个连续的空间,数组的大小取决于元素个数。
联系:数组名就是地址(指针);数组把首元素的地址交给一个指针变量之后,可以通过指针来访问数组。
2.指针数组的概念
//char arr[n]——字符数组:存放字符的数组
//int arr[n]——整型数组:存放整型的数组
指针数组:存放指针的数组
//char* arr[n]——字符指针数组:存放字符指针的数组
//int* arr[n]——整型指针数组:存放整型指针的数组
练习3:
通过指针数组打印a,b,c,d的值
#include <stdio.h> int main() { int a = 10; int b = 20; int c = 30; int d = 40; int* arr[4] = {&a,&b,&c,&d}; for(i = 0;i < 4;i++) { printf("%d ",*(arr[i])); } return 0; }
运行结果为
练习4:
用一维数组模拟二维数组,打印出各个数组的值
#include <stdio.h> int main() { int arr1[4] = {1,2,3,4}; int arr2[4] = {2,3,4,5}; int* arr[2] = {arr1, arr2};//指针数组中存放的是每个数组中首元素的地址 int i = 0; for(i = 0;i < 2;i++) { int j = 0; for(j = 0;j < 4;j++) { printf("%d ",arr[i][j]); //arr[i][j]还可以写为*(arr[i]+j)或者*(*(arr+i)+j);arr是数组首元素的地址 } printf("\n"); } return 0; }
运行结果为
练习5:
用指针数组打印字符串
#include <stdio.h> int main() { char* arr[4] = {"abc","bcd","cde","def"};//常量字符串 //arr中存放的是每个字符串中首个字符的地址 int i = 0; for(i = 0;i < 4;i++) { printf("%s\n",arr[i]);//%s打印字符串,只要有字符串起始地址就可以打印 } return 0; }
运行结果为
五、字符指针
在指针的类型中,我们知道有一种指针类型叫字符指针char*。
#include <stdio.h> int main() { char* pc = "abcdef"; printf("%s\n", pc);//abcdef printf("%c", *pc);//a return 0; }
字符指针可以存放字符串的起始地址,把字符串首元素的地址存在pc中,但这种做法不合理;"abcdef"
是常量字符串,不能修改,这时让"*pc = 'w'"
程序会崩,可以将char* pc = "abcdef"
修改为const char* pc = "abcdef"
,const放在* 的左边,限制*pc,不能改变字符串内容。
练习6:
判断下列代码的输出内容;
#include <stdio.h> int main() { char arr1[] = "abcdef"; char arr2[] = "abcdef"; const char* arr3 = "abcdef"; const char* arr4 = "abcdef"; if(arr1 == arr2)//数组名表示数组首元素的地址,比较地址是否相同 printf("arr1 and arr2 are same\n"); else printf("arr1 and arr2 are not same\n"); if(arr3 == arr4) printf("arr3 and arr4 are same\n"); else printf("arr3 and arr4 are not same\n"); return 0; }
输出结果为
if(arr1 == arr2)
和if(arr3 == arr4)
比较的是他们的首元素地址是否相同。
数组名是数组首元素的地址,arr1和arr2是数组,他们的首元素地址不同,所以他们不相同。
而arr3和arr4是char*类型的指针,他们指向的是常量字符串,常量字符串在内存中只保存一份,地址相同,所以arr3和arr4指向的都是a的地址。
六、数组指针
//char* pc——字符指针-指向字符的指针,存放字符变量的地址
//int* pa ——整型指针-指向整型的指针,存放整型变量的地址
//int(*p)[n]——数组指针-指向数组的指针,存放数组的地址,p是数组指针变量
数组指针在一维数组里面的应用
void print(int(*p)[5]) { int i = 0; for (i = 0; i < 5; i++) { printf("%d ", (*p)[i]); } } int main() { int arr[5] = { 1,2,3,4,5 }; print(&arr);//&arr取的是整个数组的地址 return 0; }
运行结果为
注意是数组指针也是指针,p应该先与 * 结合,再与[ ]结合, (*p)要用( )括起来
数组指针很少在一维数组中应用,一般在二维数组中应用
void print(int(*p)[5], int row, int col) { int i = 0; for (i = 0; i < row; i++) { int j = 0; for (j = 0; j < col; j++) { printf("%d ", *( * (p + i) + j)); } printf("\n"); } } int main() { int arr[3][5] = { {1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7} }; print(&arr, 3, 5); return 0; }
运行结果
数组名相当于数组首元素的地址,对于二维数组来说,首元素理解为他的第0行,p里面存的是第0行的地址,p是数组指针类型,p+1相当于向后走了5个int类型的大小,p+1指向第1行,(p+i)指向的就是第i行的地址。* (p+i) 相当于第i行的数组名; * (p+i)=p[i]。
扩展:int(*p[10])[5]——p是数组,有10个元素,每个元素都是数组指针,每个数组指针指向一个具有5个int类型的数组的地址。我们可以说p是存放数组指针的数组。
七、数组传参和指针传参
1.一维数组传参
数组传参,形参可以是数组,也可以是指针。
int arr[10] = {0}
传参,他的参数用数组接收可以是int arr[]
或者int arr[10]
;用指针接收是int* arr
。
int* arr[10] = {0}
传参,他的参数用数组接收是int* arr[10]
,用指针接收是int** arr
。
2.二维数组传参
int arr[3][5]
传参,他的参数用数组接收是int arr[3][5]
或者int arr[][5]
,用指针接收int (*arr)[5]
。
3.一级指针传参
int* p
传参,参数部分直接写成指针形式:int* p
。
4.二级指针传参
int** p
传参,参数部分直接写成指针形式:int** p
。
八、函数指针
#include <stdio.h> int Add(int x,int y) { return x+y; } int main() { int a = 10; int b = 20; int(*pf)(int,int)=Add; //int(*pf)(int,int)中,int是函数返回类型,pf是函数指针变量,(int,int)是函数的参数类型 //int ret = Add(a,b); int ret = (*pf)(a,b);//输出结果是30 //使用函数指针时,也可以省略* //int ret = pf(a,b); printf("%d",ret); return 0; }
九、函数指针数组
函数指针数组:可以存放多个返回类型相同和参数相同的函数的地址。
我们通过构造一个简单的计算器来实现对函数指针数组的应用
void menu() { printf("******************\n"); printf("**1.Add 2.Sub**\n"); printf("**3.Mul 4.Div**\n"); printf("**0.exit **\n"); printf("******************\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)//除法 { return x / y; } int main() { int input = 0; int x = 0; int y = 0; int ret = 0; int(*pf[5])(int, int) = { 0,Add,Sub,Mul,Div };//利用数组下标引用函数 do { menu(); printf("请选择>:"); scanf("%d", &input); if (input == 0)//input为0时,直接退出计算器 { printf("退出计算器\n"); break; } if (input >= 1 && input <= 4)//判断input是否符合要求 { printf("请输入两个操作数:"); scanf("%d%d", &x, &y); ret = (*pf[input])(x, y); printf("结果为%d\n", ret); } else { printf("选择错误\n");//input不符合要求,给与提示 } } while (input); return 0; }
扩展:指向函数指针数组的指针int(*(*pf)[5])(int,int)=&pf
。
十、回调函数
回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或发生时由另外的一方调用的,用于对该事件或条件进行响应。简言之,我们拿到一个函数,通过函数的地址调用一个函数,被调用的函数就被称为回调函数。
我们通过对上面那个计算器进行改写,从而让我们能够更好的理解回调函数。
void menu() { printf("******************\n"); printf("**1.Add 2.Sub**\n"); printf("**3.Mul 4.Div**\n"); printf("**0.exit **\n"); printf("******************\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)//除法 { return x / y; } void calc(int(*p)(int, int)) { int x = 0; int y = 0; printf("请输入两个操作数:"); scanf("%d%d", &x, &y); printf("%d\n", (*p)(x,y)); } int main() { int input = 0; int x = 0; int y = 0; int ret = 0; int(*pf[5])(int, int) = { 0,Add,Sub,Mul,Div }; do { menu(); printf("请选择>:"); scanf("%d", &input); switch (input) { case 1: calc(Add); break; case 2: calc(Sub); break; case 3: calc(Mul); break; case 4: calc(Div); break; case 0: printf("EXIT"); } } while (input); return 0; }
把函数地址传给一个函数calc,在calc函数内部通过函数指针p调用Add时,Add就被称为回调函数。
加载全部内容