一、指针的本质
1.1 指针的定义
内存区的每一个字节有一个编号,这就是“地址”。如果在程序中定义了一个变量,在对程序进行编译时,系统就会给这个变量分配内存单元。
按变量地址存取变量值的方式称为“直接访问”方式。另一种存取变量值的方式称为“间接访问”的方式。即,将变量i的地址存放在另一个变量中。
# include <stdio.h>
# include <stdlib.h>
int main()
{
int i=10;
int *i_pointer; //i_pointer是指针变量
i_pointer=&i;
printf("i=%d\n",i); //直接访问
printf("*i_pointer=%d\n",*i_pointer); //间接访问
}
在C语言中,指针变量是一种特殊的变量,它是存放地址的。
定义一个指针变量 :
基类型* 指针变量名;eg. int*i_pointer;
指针的本质是“间接访问”
1.2 取地址与取值操作符
取地址操作符 & :引用 (获取一个变量的地址值)
取值操作符 * :解引用(取值,可以拿到一个地址对应位置的数据)
这两个运算符的优先级相同,但按自右而左方向结合。
注:整形变量的地址才能放到指向整型变量的指针变量中,其它同理。
1.3 指针的使用场景
(1)传递
函数:只能传入字母a的值但没法直接修改字母a的值!
默认情况下,参数是通过值传递的方式传递给函数的。这意味着函数内部接收到的是参数值的一个副本,对副本的任何修改都不会影响到原始的变量。
#include <stdio.h>
#include <stdlib.h>
void change(int j)
{
j = 5;
}
void change_dizhi(int *j)
{
*j = 5;
}
int main()
{
int i = 10;
printf("before change,i=%d\n", i); //i=10
change(i); // j = i;j = 5;
printf("after change,i=%d\n", i); //i=10
change_dizhi(&i);
printf("after change_dizhi,i=%d\n", i); //i=5
system("pause");
}
函数:只能传入字母a的值但没法直接修改字母a的值!
#include<stdio.h>
#include<stdlib.h>
void transform(int a)
{
int num = 10;
num = a;
printf("num形参的值为%d\n", num);
}
void change(int a)
{
a = 30;
}
int main()
{
int a = 20;
transform(a); //20
change(a);
printf("a的值为:%d\n",a); //20
return 0;
}
(2)偏移
指针即地址,就像你找到了一栋楼,该栋楼叫B栋,那么往前就是A栋,往后就是C栋,所以指针的另一个场景就是对其进行加和减,地址进行乘除是没有意义的,就像你家的地址乘5是代表什么,没有意义。工作中,我们把对指针的加减,称之为指针的偏移,加就是向后偏移,减就是向前偏移。目的是获得某地之前后的地址及其地址处的内容。
注: 1. "+1"加的是基类型的大小,即sizeof(int)、sizeof(结构体)等等。
2. 只能偏移有基类型的,即void类型的指针不能偏移。
#include <stdio.h>
#include <stdlib.h>
int main()
{
int a[5] = { 1,2,3,4,5 };
int* p;
int index;
p = a;
for (index = 0; index < 5; index++)
{
printf("%d\n", *(p + index));
// p[index]
}
return 0;
}
指针与自增自减运算符
++、--、*、&的优先级,从右到左结合
*p++ -> *p;p++
p[0]++ ->
1.4 指针与动态内存申请
很多人一直都非常想使用动态数组,觉的数组长度固定很不爽,其实是因为我们定义的整型,浮点型,字符型变量,数组变量,都在栈空间,栈空间的使用是在编译时确定大小,如果使用的空间大小不确定,那么我们就需要使用堆空间。
malloc
语法:
void *malloc(size_t size) :
返回的是void*类型,因为不知道这段空间要用来干什么,故要自己进行类型转换。
功能:函数指向一个大小为size的空间,存储空间的指针不能是栈。这样以便以后用free函数释放空间。
注:1.malloc不free,就会内存泄漏。
2.free之后把指针要赋为NULL(free之后p中依然存有该地址值,只是该段空间不应该再访问了,别人在用了)。如果free之后不赋为NULL,拿着指针去访问对应空间(可能会拿到别人的数据,与自己预想的不同,则难以找到错误),称为野指针。
#include <stdio.h>
#include <stdlib.h>
int main()
{
int *p, *p1 ,*p2;
p = (int*)malloc(4);
*p = 1;
p1 = (int*)malloc(4);
*p1 = 2;
free(p);
p2 = (int*)malloc(4);
*p2 = 3;
*p = 100;
printf("*p1=%d,*p2=%d\n", *p1, *p2); // 2 100
system("pause");
}
1.5 堆与栈的差异
既然都是内存空间,为什么还要分栈和堆呢?栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是C/C++函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多(申请后free太多等)),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。
注:1.堆只有我们free才会释放,否则在进程执行过程中一直有效。
2.栈空间在函数使用完后就会释放。
#define N 1000000
#include <stdio.h>
#include<stdlib.h>
//函数栈大小是1M
char* print_stack()
{
char c[] = "I am stack";//在print_stack()的栈空间中
puts(c);
return c;
}
char* print_malloc()
{
char* p = (char*)malloc(20);
strcpy(p, "I am malloc");
puts(p);
return p;
}
int main()
{
char *p;
p = print_stack();//该函数返回的时候空间还没被清理掉(延迟退栈)
puts(p);//print_stack()的空间已被退掉
p = print_malloc();
puts(p);
system("pause");
}
二级指针
一级指针的使用场景是传递与偏移,服务的对象是整型变量,浮点型变量,字符型变量等,那么二级指针既然是指针,其作用也是传递与偏移,服务对象更加简单,只服务于一级指针的传递与偏移!
(1)传递
在子函数改变主函数某一个一级指针变量的值
#include <stdio.h>
#include<stdlib.h>
void change(int **p2, int* pj)
{
*p2 = pj;
}
int main()
{
int i = 10;
int j = 5;
int* pi = &i;
int* pj = &j;
printf("i=%d,j=%d,*pi=%d,*pj=%d\n", i, j, *pi, *pj);
change(&pi, pj);
printf("i=%d,j=%d,*pi=%d,*pj=%d\n", i, j, *pi, *pj);
system("pause");
}
(2)偏移
一级指针的偏移服务于数组,整型一级指针服务于整型数组,所以二级指针的偏移也服务于数组,服务对象为指针数组,请看下面实例,实际在淘宝购物过程中,大家搜索的商品信息存在内存中,如果以某个查询条件搜索商品,淘宝需要把商品按你的要求进行排序,比如价格从低到高,这时交换内存中商品的信息会极大的降低效率(因为每个结构体的内存更大),因为不同用户会有不同的查询需求,每件商品本身的信息存储又较大,假如我们让每个指针指向商品信息,排序比较时,我们比较实际的商品信息,但交换的是指针,这样交换成本将会极大降低,这种思想称为索引式排序,下面的例子把字符串看成商品信息即可,将指针数组p赋值给二级指针p2,目的是为了演示二级指针的偏移,p2+1偏移的就是一个指针变量空间的大小,即sizeof(char*),对于win32控制台应用程序,就是偏移4个字节。