目录
什么是指针?
-
指针就是地址。地址唯一标识一块地址空间。
指针变量:用于存放地址的变量。
-
地址的组成、指针的大小
/* * 内存是怎么样编号的?一个内存单元占用多大的空间? * 众所周知,我们的电脑分32位和64位的。 * - 32位的电脑,有32根地址线(这个地址线是物理线),通电就会产生电信号,强电信号就是1,弱电信号就是0 * 而电信号转换为数字信息,就是1和0组成的二进制序列。而每一位2进制有1和0两种可能,32位二进制,就是(2的32次方)个二进制序列。 * 其中的每一个二进制序列,都可以作为一个内存单元的编号。当一个序列成为内存的编号时,我们就把这个编号称为这个内存单元的地址。 * - 64位的电脑,有64根地址线(这个地址线是物理线)。其有(2的64次方)个二进制序列。 * * 为了有效使用内存,就把内存划分为一个个内存单元,因为光一个字符都要占用一个字节,所以每个内存单元的大小被划分成1个字节。 */ #include <stdio.h> int main() { //每个指针的大小是相同的。指针是用来存放地址的,指针占用空间多大,取决于地址的存储需要多大的空间。 //指针内存储的是内存单元的地址,也就是其二进制序列。 //在32位平台下,地址由32位二进制组成,地址就需要用4个字节的空间来存储,其一个指针变量要占4个字节 //在64位平台下,地址由64位二进制组成,一个指针变量就占用8个字节。 printf("%d\n",sizeof(char *)); //8 printf("%d\n",sizeof(short *)); //8 printf("%d\n",sizeof(int *)); //8 printf("%d\n",sizeof(long *)); //8 printf("%d\n",sizeof(long long *)); //8 printf("%d\n",sizeof(float *)); //8 printf("%d\n",sizeof(double *)); //8 return 0; }
-
指针的使用
#include <stdio.h> int main() { //取出变量地址 int num = 10; //因为num变量是int类型的,而int类型占用四个字节,所以num在内存中占用四个字节。 //这里num所占用的四个字节,每一个字节都有地址,但是打印出来的话,只取出第一个字节的地址 // 因为int类型变量所占用字节是固定的,所以只要知道了开头的地址,顺着往后取就可以。 printf("%p\n",&num);//%p专门用于打印地址 //000000dfa3fffbec //存储地址的变量就叫做:指针变量 //语法:存储的地址的变量的类型 * 变量名 = &要存储地址的变量; int * pn =#//pn是用来存放地址的,pn就叫指针变量 //* 说明pn是指针变量 //int 说明pn指向的对象是int类型的 char ch = 'k'; char * pc = &ch; //指针的使用 int d = 10; int * pd = &d; *pd = 555; // *——解引用操作。*pd就是通过pd里存储的变量地址,找到该变量,然后修改其中的数据。 printf("%d\n",d); //555 return 0; }
指针和指针类型
-
变量有类型,指针也有其对应的类型
// 数据类型 * 指针变量名 char * pc; //char *类型的指针用于存储char类型变量的地址 short *ps; //short *类型的指针用于存储short类型变量的地址 int * pi; //int * 类型的指针用于存储int类型变量的地址 long * pl; float * pf; double * pd;
-
注意
int i; //正常定义指针。 //int* p = &i; //这里*的意思是:p是一个指针,指向的变量是int类型的。这个*是p的,而不是给int类型的。 //所以这里定义出来的,p是int*指针,而q只是int类型。 //不管*靠近p或靠近int,*都是p的,而不是int int* p,q; int *p,q; //也就是说 *p是int类型,于是p是一个int类型指针。而并不是说p是int*这种类型 //如果要将p与q都定义为指针,则要这样写: int *p,*q; //单目运算符* //*用来访问指针的值所表示的地址上的变量。 //可以做右值,也可以做左值。可以将其值赋给另一个变量,也可以将值赋给*p int k = *p; *p = 10; //为什么叫做左值呢? //因为出现在赋值号左边的不是变量,而是值,是表达式运算的结果。如: a[0] = 2; *p = 3; //a[0],[]是运算符,a[0]是取出数组a下标为0的元素。a[0]是一个表达式,其运算结果是一个值,放在赋值号左边,就叫做左值,用来接收右边表达式的结果。 //为什么叫左值,而不是叫变量呢?因为a[0]和*p不是变量,他们是一个表达式运算的结果,是一个特殊的值,所以叫做左值。 //赋值号的左边叫左值,赋值号的右边叫右值。 //为什么编译不报错,而运行报错呢? int i; scanf("%d",i); //因为scanf和printf一样都是不定长参数的函数,而c语言的不定长参数实现,就注定没办法在编译时进行语法校验。你在后面加上一百个错误参数都一样可以编译通过。 //&a的意思就是变量a的地址。scanf("%d", &a)的编译效果就是从你的输入中找到整型值把它写到地址编号为(&a)的地址中去。比如变量a的地址是0x33333333,那么就是把数值写到地址0x33333333,那么自然,写到0x33333333地址的数值,也就成了变量a的值。因为变量a的值就储存在这个地址。 //scanf("%d", a),就是扫描你的输入,从中找出整形数值,把他写入地址编号为a的地址中。 //scanf("%d", 12345678),就是扫描输入,找到整形数值,把他写入地址编号为12345678的地址中去。 //不管你提供的地址是什么形式,编译器都认为你已经提供了地址,所以编译能通过,只是在程序运行过程中,往你提供的地址写数据一旦写不上去,程序运行将中止,并报错。写不上去的原因是此地址该程序没有写入权限,或者地址非法,当然这是操作系统管辖范围,编译器不管这些。
-
指针应用场景
//1、用来交换两个变量的值。将两个变量的地址传入函数中,然后解引用进行交换。 //2、函数返回多个值,某些值指针通过指针返回。也就是说,当作参数传入的指针,是为了带运算结果返回。 //例如:当需要函数函数返回两个值时。return只能返回一个值回来,此时可以在定义函数时,多定义一个指针类型,调用函数时,多传一个指针进去,用于待会结果。也可以定义为不返回值的函数,然后多传入两个参数,来待会需要返回的两个值。 //3、函数调用状态的返回,需要通过指针返回。 //常用套路:让函数返回特殊的不属于有效范围内的值来表示出错。如用于文件操作的很多库函数,就会返回-1和0来表示出错。 //但是当返回的数值都可能是有效结果时,就需要分开返回了。往往是,函数执行状态用return来返回,实际的值通过指针来返回,这样就容易将函数返回的值放到if函数中做判断。
-
同一个平台下,每个指针的大小都相同,那指针类型有什么意义?
/* * 指针类型决定了,对指针变量进行解引用的时候,可以操作几个字节。 * - 当我们解引用int类型指针的时候,可以操作4个字节。 * - 当我门解引用char类型指针的时候,只可以操作1个字节。 * * 指针类型还决定指针变量进行加减的时候,其每次跳过的字节。 * - int类型的指针变量pa。pa+1,就表示向后眺一个int类型大小,也就是跳四个字节。 * - long类型的指针变量pl。pl-2,就表示向前跳2个long类型大小,也就是跳16个字节。 */ #include <stdio.h> int main() { int a = 0x11223344; //在a中存放十六进制的数:11223344 int * pa = &a; //定义int类型的指针变量pa存储a变量的地址 *pa = 0; //*pa找到pa指向的a变量。调试,观察其内存变化:a占用的四个字节的空间,存储的数都变成了0 //pa——>pa+1,在内存中,是加了4个字节。 //因为pa是int类型指针,int类型占四个字节,pa+1,就表示跳过一个int类型大小,所以是加了四个字节。 printf("%p\n",pa); //0000004d515ffabc printf("%p\n",pa+1);//0000004d515ffac0 int c = 0x11223344; char * pc = &c;//定义char类型的指针变量pc,存储int类型变量c的地址 * pc = 0;//*pc找到pc指向的a变量。调试,观察其内存变化:c占用的四个字节的空间,只有一个字节的存储的内容变成了0 //pc——>pc+1,在内存中,是加了1个字节。 //因为pc是char类型指针,char类型占1个字节。pc+1,就表示跳过一个char类型大小,所以是加了一个字节。 printf("%p\n",pc); //0000004d515ffab8 printf("%p\n",pc+1); //0000004d515ffab9 return 0; }
-
指针的使用
#include <stdio.h> int main() { int arr[10] = {0}; int * p = arr; int i; for(i=0 ; i<10 ; i++) { //这里p中存储的是数组首元素的内存地址,所以p+i就是数组下表为i的元素的地址。因为p是int *类型的指针,所以每次跳4个字节。 //对其解引用,与arr[i]效果相同 *(p+i) = 1; } return 0; }
野指针
当指针指向的地址是未知的/随机的/不正确的/没有明确限制时,这个指针就是个野指针
-
什么时候出现野指针?
-
指针未初始化时,就使用指针
int main() { //这里的p就是一个野指针 int * p;//p是一个指针变量,但是并没有指向的地址 //对p进行解引用,因为p没有指向的地址,这就是非法访问内存 *p = 20; return 0; }
-
数组越界
int main() { int arr[10] = {0}; int *p = arr; int i; for(i=0 ; i<=11 ; i++) { //当指针指向的范围超出arr的范围时,p就是野指针 *p = i; p++; } return 0; }
-
指针指向的空间释放
#include <stdio.h> int * test() { int a = 10; return &a; } int main() { //a是一个局部变量,在test()方法调用结束之后a所占用的内存释放 //这里返回的a的地址,存放在p中。 int * p = test(); //p中存放的地址,已经不是原来的a变量了。可以说,这样的使用没有意义。 *p = 20; return 0; }
-
-
如何避免野指针?
-
初始化指针
#include <stdio.h> int main() { //当不知道p应该初始化为什么值的时候,直接初始化为NULL int * p = NULL; //明确指针初始化的值的时候再定义指针 int a =10; int * pa = &a; return 0; }
-
小心指针越界
因为C语言本身是不会检查数据的越界行为的,所以我们只能自己注意。
-
指针指向的变量释放时,即时将这个指针置成NULL
-
避免返回局部变量的地址
例如:一个方法中的局部变量,其有效范围仅仅是在该方法被调用时有效,在方法调用结束之后,局部变量自动销毁。就算是返回了这个局部变量的地址,也不再是之前的那个局部变量了。
-
指针使用之前检查其有效性
#include <stdio.h> int main() { int * p = NULL; int a = 10; p = &a; //当不确定这个指针是不是空的时候,进行判断,如果不是NULL,再进行操作 if(p!=NULL) { *p = 20; } printf("%d",a); //20 return 0; }
-
指针运算
-
指针±整数
#include <stdio.h> #define N_VALUES 5 int main() { float values[N_VALUES]; float *vp; for(vp=&values[0]; vp<&values[N_VALUES];) { //后置++,先赋值再运算。当下表为4的元素被赋值之后,++变成第5个,此时条件不成立,循环结束。 *vp++ =0; printf("1 "); } return 0; }
-
指针 - 指针 :两个指针之间的元素个数
如果是分别取出来之后,中间差的是字节。如果是指针-指针的运算,则返回的是元素个数。
#include <stdio.h> int main() { int arr[10] = {0}; //指针相减的前提:两个指针指向同一块空间 //指针与指针相减的结果是:这两个指针之间的元素个数 printf("%d\n",&arr[9]-&arr[0]); //9 }
求字符串长度
#include <stdio.h> //数组指针相减求长度的方法 int myStrLen(char * str) { //将数组首元素的地址保存到s中 char * s = str; //如果不是\0,就一直循环,直到str对应的地址是最后一个元素的地址 while(*str != '\0') { str++; } //最后一个元素的地址-第一个元素的地址,就是这两个元素之间的个数,也就是字符串的长度 return str-s; } //之前的方法 //int myStrLen(char * str) //{ // int count = 0; // while(*str != '\0') // { // count++; // str++; // } // return count; //} int main() { char arr[] ="abcdef"; int len = myStrLen(arr); printf("%d\n",len); return 0; }
-
指针的关系运算
#define N_VALUES 5 int main() { int values[N_VALUES]; int * vp; for(vp=&values[N_VALUES] ; vp>&values[0];) { *--vp = 0; } //要避免这样写: //for(vp=&values[N_VALUES-1] ; vp>=&values[0];vp--){*vp = 0;} //标准规定:允许数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较, //但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。 return 0; }
指针和数组
-
数组变量是特殊的指针
//数组变量(也就是数组名)本身就表示地址,所以无需用&取地址 int arr[5] = {0}; int *p = arr; //arr == &arr[0] 数组名存储的就是数组首元素的地址。 //[]运算符可以用于数组,也可以用于指针:p[0] == a[0] //*运算符用于指针变量,也可以用于数组: *a = 25; *(a+2) = 10; //数组变量类似于const修饰的指针,因为C语言规定数组名不能被赋值。也就是说,数组名不能再去指向另一个数组 int b[3] = {0}; //b =arr; //编译报错。
-
p+i取出的地址,与&arr[i]取出的地址相同。*(p+i)就可以操作下标为i的元素,与arr[i]相同
#include <stdio.h> int main() { int arr[10] = {0}; //数组名中存储的是数组首元素的地址。既然可以把数组名当成地址存放到指针中,我们就可以使用指针来访问数组。 int *p = arr; int i; for(i=0 ; i<10 ; i++) { //p+i的地址就是数组下标为i的元素对应的地址。*(p+i)解引用,与arr[i]效果相同。 printf("arr[%d] = %p <==>p+%d = %p\n",i,&arr[i],i,p+i); //arr[0] = 000000de9d3ffad0 <==>p+0 = 000000de9d3ffad0 //arr[1] = 000000de9d3ffad4 <==>p+1 = 000000de9d3ffad4 //arr[2] = 000000de9d3ffad8 <==>p+2 = 000000de9d3ffad8 //arr[3] = 000000de9d3ffadc <==>p+3 = 000000de9d3ffadc //arr[4] = 000000de9d3ffae0 <==>p+4 = 000000de9d3ffae0 //arr[5] = 000000de9d3ffae4 <==>p+5 = 000000de9d3ffae4 //arr[6] = 000000de9d3ffae8 <==>p+6 = 000000de9d3ffae8 //arr[7] = 000000de9d3ffaec <==>p+7 = 000000de9d3ffaec //arr[8] = 000000de9d3ffaf0 <==>p+8 = 000000de9d3ffaf0 //arr[9] = 000000de9d3ffaf4 <==>p+9 = 000000de9d3ffaf4 //p+i就是数组arr下标为i的地址,我们就可以通过*(p+i)来访问数组。 *(p+i) = i; } for(i=0 ; i<10 ; i++) { printf("%d ",*(p+i));//0 1 2 3 4 5 6 7 8 9 } return 0; }
-
数组中指针的使用
#include <stdio.h> int main() { int arr[] = {0,1,2,3,4,5,6,7,8,9}; int* p = arr; //[]是一个操作符,2和arr是两个操作数。根据加法交换原则:a+b=b+a //所以:arr[2] == 2[arr] == *(arr+2) == *(2+arr) == *(p+2) == *(2+p) printf("%d\n",arr[2]);//2 //arr[2]执行时时被解析为:*(arr+2) == *(2+arr),因为2[arr] == *(2+arr),所以arr[2]=2[arr], // 所以arr[2] == 2[arr] == p[2] == 2[p] printf("%d\n",2[arr]);//2 printf("%d\n",p[2]);//2 printf("%d\n",2[p]);//2 return 0; }
二级指针
#include <stdio.h>
int main()
{
int a = 10;
//*表示pa是指针变量,*前的int表示pa指向的变量a是int类型的。
int* pa = &a; //pa是一个指针变量,也被称为一级指针
//*pa可以找到a
*pa = 222;
printf("%d\n",a);//222
//ppa被叫做二级指针变量。
//最后一个*表示ppa是指针变量,*前的int*表示ppa指向的变量pa是int*类型的。
int** ppa = &pa; //ppa中存储pa变量的地址,pa中存储的也是a变量的地址。
//解引用:*ppa可以找到pa,*pa可以找到a,所以* *ppa 就可以找到a
** ppa = 333;
printf("%d\n",a);//333
//int b =66;
//*ppa = &b;//等价于 pa = &b;
//pppa被叫做三级指针变量
//最后一个*表示pppa是指针变量,*前的int**表示pppa指向的变量ppa是int**类型的。
int*** pppa = &ppa;
//解引用:*pppa可以找到ppa,*ppa可以找到pa,*pa可以找到a。所以* **pppa 就可以找到a
*** pppa = 444;
printf("%d\n",a);//444
return 0;
}
指针数组
int main()
{
int arr[10]; //整型数组:存放整型数据的数组
char ch[5]; //字符数组:存放字符的数组
//指针数组:存放指针的数组
int* parr[5]; //存放整型指针的数组
char* pch[5]; //存放字符型指针的数组
}
练习
-
使用指针打印数组内容
#include <stdio.h> int main() { int arr[] ={1,2,3,4,5,6,7,8,9,10}; int * pa = arr; //数组元素个数 int sz = sizeof(arr)/sizeof(arr[0]); int i; for(i=0 ; i < sz-1 ; i++) { printf("%d ",*(pa+i)); } }
-
编写函数,逆序字符串
#include <stdio.h> #include <string.h> #include <assert.h> void reverse(char * str) { //断言:如果str不为空,程序才会往下执行。 //assert(str != NULL); //简写:str如果是NULL,就是为0,为假,程序不执行。如果不是null,则非0就为真,程序往下执行。 assert(str); //计算字符串长度 int len = strlen(str); //左元素下标定义为首元素地址 char * left = str; //str是数组首元素地址,+长度-1,就是最后一个元素的下标 char * right = str+len-1; //当左边地址小于右边的地址时,循环交换 while(left < right) { //因为是要交换数组中的元素,所以要解引用找到其内容,然后进行修改。 char tmp = *left; *left = *right; *right = tmp; //这里是指针往后址,是其地址往后走,不是改变其中的内容,所以不用解引用。 left++; right--; } } int main() { char arr[] = "abcdef"; //char * arr = "abcdef"; //不能使用这种方式,因为"abcdef"是一个常量字符串,不能修改。 reverse(arr); printf("%s\n",arr); }
-
*p++解析
//取出p所指的哪个数据来,然后将p移动到下一个位置。 //注意:++的优先级比*要高。但是这里是后置++,所以*p++,p与++结合之后,需要等*p++这个表达式执行完才会+1,然后*p取出p所指向的变量。执行过后p移动到下一个位置。 //常用于数组类的连续空间操作。 //在某些CPU上,这可以直接被翻译成一条汇编指令。
-
0地址
/* * - 我们现在的操作系统,都是多进程的。 * 对于进程来说,运行起来之后,操作系统会给其分配一个虚拟的地址空间。 * 所有的进程在运行的时候,都以为自己具有从0开始的一片连续空间。 * - 所有的程序的0地址,都是虚拟的。但是我们不能往这个0地址中设置数据。 * 在某些系统中,设置不能读取0地址处的数据。 * - NULL是一个预定义的符号,表示0地址。 * 有的编译器不愿意让我们使用0来表示0地址,我们就可以使用NULL。 * 但是注意:有的编译器中0和NULL可能是不相等的。 */ #include <stdio.h> int main() { int* p=0; int* q=NULL; printf("0的地址: %p\n",p); printf("NULL的地址:%p\n",q); return 0; }