指针
指针
指针是C语言的灵魂,想学好C语言必须要搞懂指针,如果不懂指针,相当于没学C语言
1. 指针的重要性
- 表示一些复杂的数据结构
- 快速的传递数据
- 使函数返回一个以上的值
- 能直接访问硬件
- 能够方便的处理字符串
- 是理解面向对象语言中引用的基础
总结:指针是C语言的灵魂
2. 指针的定义
2.1 地址
地址就是内存单元的编号
从零开始到非负整数
总体来说cpu操作内存条分为三条线,控制线负责数据的传输的方向,数据线负责数据的传输,地址线负责数据传输的地址
现在我们只讨论地址线
在地址线中cpu操作内存条,有很多条线,一条线只有0、1两种变化,所以只能操作两个字节,两条线则有22种变化,一般像32位的机器,也就是32条线,有232种变化,可以操作232个字节,也就是232B,而
1K = 2^10B
1M = 2^10K = 2^20B
1G = 2^10M = 2^20K = 2^30B
所以,2^32B = 2^30B * 2^2 = 4G ,也就是说,32位的计算机的内存条大小就是4G
故地址的范围为(按32位的情况):4G 【0 --- 4G-1】
2.2 指针
指针就是地址,地址就是指针
指针变量就是存放内存单元编号的变量,或者说指针变量就是存放地址的变量
指针和指针变量是两个不同的概念
注意:有时会把指针变量简称为指针,但是要明白他们不一样
指针的本质就是一个操作受限(意思就是不能加乘除,但是可以相减)的非负整数
3. 指针的分类
3.1 基本类型指针【重点】
3.1.1 基本类型指针例子如下:
# include <stdio.h>
int main(void)
{
int * p; //p是变量的名字,int * 表示p变量存放的是int类型变量的地址
//int * p; 不是表示定义了一个名字叫做*p的变量
//int * p; 应该理解为:p是变量名,p变量的数据类型是int *类型
// 所谓int * 类型就是存放int变量地址的类型
int i = 3;
int j;
p = &i;
/*
1. p保存了i的地址,因此p指向i
2. p不是i,i也不是p,更准确的说:修改p的值不影响i的值,修改i的值也不会影响p的值
3. 如果一个指针变量指向了某个普通变量,则
*指针变量 就完全等同于 普通变量
例子:
如果p是个指针变量,并且p存放了普通变量i的地址
则p指向了普通变量i
*p 就完全等同于 i
或者说:在所有出现*p的地方都可以替换成i
在所有出现i的地方都可以替换成*p
*/
j = *p; //等价于j = i;
/*
*p 就是以p的内容为地址的变量
*/
printf("i = %d, j = %d, \n", i, j);
return 0;
}
3.1.2 *的含义:
-
乘法
-
定义指针变量
int * p; //定义了一个名字为p的变量,int *表示p只能存放int变量的地址
-
指针运算符
*p = b; //该运算符表示以p的内容为地址的变量,完整例子如下
# include <stdio.h> int main(void) { int a; int b = 6; int * p; p = &a; *p = b; printf("a = %d, *p = %d\n",a ,*p);//运行结果为 a = 6, *p = 6 return 0; }
3.1.3 基本类型指针常见错误
在c语言中操作指针时,会有以下常见错误
-
# include <stdio.h> int main(void) { int i = 5; int * p; *p = i; // warning,因为 p变量 并没有初始化,里面是垃圾值,也就是里面保存的是我们这个程序外的内存空间的编号 // 所以这样操作会导致我们在这里修改了 这个程序外 的内存空间的数值 printf("%d\n", *p); return 0; }
-
# include <stdio.h> int main(void) { int i = 5; int * p; int * q; p = &i; //*q = p; //error 语法错误,编译报错 *q 为 int 类型,p 为 int * 类型 //*q = *p; //warning,因为变量q并没有初始化 p = q; //q是垃圾值,q赋给p,p也变成垃圾值 printf("%d\n", *p); //q的空间是属于本程序的,所以可以对里面的垃圾值进行操作 //但是*q是 以q里面的垃圾值为地址的内存空间里面的数值,是不属于本程序的内存,所以不能对其操作 return 0; }
3.1.4 经典指针程序_互换两个数字
定义以下三种函数来进行互换两个变量数值,三种函数虽然都是互换变量的内容,但是其本质含义各不相同,具体的区别由下面的例子进行解析
# include <stdio.h>
void huhuan(int , int );
void huhuan2(int *, int *);
void huhuan3(int *, int *);
//经典指针程序-互换两个数字
int main(void)
{
int a = 3;
int b = 5;
int t;
//huhuan(a, b);
//huhuan2(&a, &b);
huhuan3(&a, &b);
/*
t = a;
a = b;
b = t;
*/
printf("a = %d, b = %d\n", a, b);
return 0;
};
//不能完成互换功能
//因为只是互换了形参a,b的值,和主函数中的变量a,b并没有关系,并且当此函数运行完以后该形参a,b都会被释放
void huhuan(int a, int b)
{
int t;
t = a;
a = b;
b = t;
}
//不能完成互换功能
//因为该函数只是互换了p指针变量和q指针变量中的内容,和主函数的a,b变量也没有关系
/*
注意:没有任何一门语言可以改变一个静态变量已经分配好的空间,也就是说,不能改变一个静态变量(例如 int a)的内存空间地址(指针);
假如 int a 所分配的内存空间编号位1000H,那么在a变量释放之前,它的空间编号一直并且只能是1000H,不能修改
*/
void huhuan2(int * p, int * q)
{
int * t;
t = p;
p = q; //p是int *类型,存放int类型变量的地址
q = t;
}
//可以完成互换功能
/*
因为其中*p指的是 以指针变量p中的值为地址的内存空间中的值,相当于变量a的内存空间中的值;
所以改变*p的值相当于改变了变量a的值,改变了*q的值相当于改变了变量b的值
*/
void huhuan3(int * p, int * q)
{
int t;
t = *p;
*p = *q; //*p是int类型,以指针变量p中的值为地址的内存空间中的值
*q = t;
}
3.1.5 如何通过被调函数修改主函数普通变量的值
- 实参必须为该普通变量的地址
- 形参必须为指针变量
- 必须要在被调函数中修改 *变量名 的值
3.2 指针和数组
3.2.1 指针和一维数组
-
一维数组名
- 一维数组名是一个指针常量(不能被改变)
- 它存放的是一维数组第一个元素的地址
# include <stdio.h> int main(void) { int a[5]; //a是数组名 5是数组元素的个数 元素就是变量 a[0] -- a[4] int b[5]; // a = b; //error a是常量 printf("%#X\n", &a[0]); printf("%#X\n", a); return 0; } /* 在Vc++6.0中的输出结果是: ------------------ 0X19FF1C 0X19FF1C Press any key to continue ------------------ */
-
下标和指针的关系
- 如果p是个指针变量,则 p[i] 永远等价于 *(p+i)
# include <stdio.h> int main(void) { int a[5] = {1, 2, 3, 4, 5}; int i; for (i=0; i<5; ++i) { //printf("%d\n", a[i]); printf("%d\n", *a+i); //输出结果与上面相同,即 a[i] == *(a+i) } return 0; }
-
确定一个一维数组需要几个参数【如果一个函数要处理一个一维数组,则需要接收该数组的哪些信息】
- 需要两个参数
- 数组的第一个元素地址【数组名】
- 数组的长度
以下由两个例子来简述如何传递一维数组
-
# include <stdio.h> //f函数可以输出任何一个一维数组的内容 void f(int * pArr, int len) { /* 接收数组需要两个参数,分别为 数组的第一个元素地址【数组名】 数组长度 */ int i; for (i=0; i<len; ++i) { printf("%d ", *(pArr+i)); //*pArr *(pArr+1) *(pArr+2) //printf("%d ", pArr[i]); //此处的运行结果和上面相同 } printf("\n"); } int main(void) { int a[5] = {1, 2, 3, 4, 5}; int b[6] = {-1, -2, -3, -4, -5, -6}; int c[100] = {1, 99, 22, 33}; f(a, 5); //a是 int * 类型 f(b, 6); f(c, 100); return 0; } /* 在Vc++6.0运行结果 ------------------- 1 2 3 4 5 -1 -2 -3 -4 -5 -6 1 99 22 33 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 Press any key to continue ------------------- */
-
# include <stdio.h> void f(int * pArr, int len) { pArr[3] = 88; //这里的 aPrr[3] 其实和 a[3] 本质上是一个内存空间 } int main(void) { int a[6] = {1, 2, 3, 4, 5, 6}; printf("%d\n", a[3]); //输出结果:4 f(a, 6); printf("%d\n", a[3]); //输出结果:88 return 0; }
由以上两个例子可总结出
*(pArr+i) == pArr[i] == b[i] == *(b+i)
a == &a[0],a[0] == *(a+0)
- 需要两个参数
-
指针变量的运算
-
指针变量不能相加 不能相乘 也不能相除【只能在一定条件下相减】
-
如果两个指针变量指向的是同一块连续空间中的不同存储单元,则这两个指针变量才可以相减
# include <stdio.h> int main(void) { int i = 5; int j = 10; int * p = &i; int * q = &j; //p - q 没有实际意义 并且i、j空间不连续 int a[5]; p = &a[1]; q = &a[4]; printf("p和q各自指向的单元相隔 %d 个单元\n", q - p); }
-
-
一个指针变量到底占几个字节
- 假设p指向char类型变量(1个字节)
- 假设q指向int类型变量(4个字节)
- 假设r指向double类型变量(8个字节)
p q r 本身所占的字节数是否一样?
预备知识: sizeof(数据类型) 功能:返回该数据类型所占的字节数 sizeof(int)=4; sizeof(char)=1; sizeof(double)=8; sizeof(变量名) 功能: int i = 1; sizeof(i)=4;
# include <stdio.h> int main(void) { char ch = 'A'; int i = 99; double x = 66.6; char * p = &ch; int * q = &i; double * r = &x; printf("p 占 %d 个字节, q 占 %d 个字节, r 占 %d 个字节\n", sizeof(p), sizeof(q), sizeof(r)); return 0; } /* 在Vc++6.0中的输出结果是: ------------------ p 占 4 个字节, q 占 4 个字节, r 占 4 个字节 Press any key to continue ------------------ */
由以上例子可总结出,一个指针变量,无论它指向的变量占几个字节,该指针变量本身只占4个字节,它们所存放的变量地址都是首个字节的地址,如char类型变量只占一个字节,则char * 指针变量就存放该字节地址; int类型变量占4字节,则int * 指针变量只存放首个字节的地址,并标明该普通变量所占的空间是该地址字节再往下加3个字节;double类型变量占8字节, 则double * 指针变量只存放首个字节地址,并表明该普通变量所占的空间是该地址字节再往下加7个字节。
为什么指针变量保存的某个字节地址编号需要用到4个字节的空间?
答:例如32位机,有232种变化,可以操作232个字节,就像10人的团队,某人的编号就是01、02、03,100人的团队,某人的编号就是001,002,003,所以像2^32个字节的“团队”,它的第一个字节和最后一个字节的编号长度是一样的,所以每一个字节的地址编号数据就需要用到4个字节的空间来保存。
3.2.2 指针和二维数组
3.3 指针和函数
3.4 指针和结构体
3.5 多级指针
附录
1. 动态内存分配【重点难点】
1.1 传统数组的缺点:
-
(1). 数组长度必须提前指定,且只能是常整数(常量),不能是变量
例子:
int a[5]; //OK int len = 5; int a[len]; //error
-
(2). 传统形式定义的数组,该数组的内存程序员无法手动释放。
在一个函数运行期间,系统为该函数中数组所分配的空间会一直存在,直到该函数运行完毕时,数组的空间才会被系统释放。
-
(3). 数组的长度一旦定义,其长度就不能再更改
数组的长度不能在函数运行的过程中动态的扩充或缩小
-
(4). 传统方式定义的数组不能跨函数使用
A函数定义的传统数组,在A函数运行期间可以被其他函数使用,但A函数运行完毕之后,A函数中的数组将无法再被其他函数使用。
1.2 为什么需要动态分配内存
- 动态数组很好的解决了传统数组的这4个缺陷
- 传统数组也叫静态数组
1.3 动态内存分配举例——动态数组的构造
补充知识:
数据类型就相当于分配内存空间的数量,例如:
int就代表4个字节,char就代表1个字节,double就代表8个字节
1.3.1 malloc()函数例子:
-
/* malloc 是 memory(内存) allocate(分配) 的缩写 */ # include <stdio.h> # include <malloc.h> int main(void) { int i = 5; //分配了4个字节 静态分配 int * p = (int *)malloc(4); /* 1. 要使用malloc函数,必须添加malloc.h这个头文件 2. malloc函数只有一个形参,并且形参是整型 3. 形参 4 表示请求系统为本程序分配4个字节 4. malloc函数只能返回第一个字节的地址 5. 上一行代码 总共分配了8个字节,p变量占4个字节,p所指向的内存也占4个字节 6. p本身所占的内存是静态分配的,p所指向的内存是动态分配的 */ *P = 5; //*p 代表的就是一个int变量,只不过*p这个整形变量的内存分配方式和 int i = 5; 的分配方式不同 free(p); //free(p)表示把p指向的内存给释放掉 //p变量本身的内存是静态的,不能由程序员手动释放,p变量本身的内存只能在p变量所在的函数运行终止时由系统自动释放 printf("同志们好!\n"); return 0; }
-
# include <stdio.h> # include <malloc.h> void f(int * q) { *q = 200; //free(q); //这里释放后后面就不能继续使用该内存了,如果继续使用该地址操作内存,将会是一个危险的操作 //所以在该指针释放后,如果后面还有指针变量的值是该指针,应该设置为NULL,防止野指针的发生 //野指针:指针变量中的值是未申请的内存地址 } int main(void) { int * p = (int *)malloc(sizeof(int)); //sizeof(int)返回值是int所占的字节数 *p = 10; printf("%d\n", *p); //10 f(p); //p是int *类型 printf("%d\n", *p); //200 return 0; }
1.3.2 动态一维数组的构造
# include <stdio.h>
# include <malloc.h>
int main(void)
{
//静态数组
int a[5]; //如果int占4个字节的话,则本数组总共包含20个字节,每4个字节被当做了一个int变量来使用
//动态数组
int len;
int * pArr;
int i;
printf("请输入您要存放的整数元素个数:");
scanf("%d", &len); //输入5时
pArr = (int *)malloc(4 * len); /*
需要申请 4 * 5 个字节
并且pArr指向前4个字节 (在这4个字节中再分析,pArr就是第一个字节的地址);
pArr加一时,则pArr指向后4个字节 【不是第二个字节!!】
结论:pArr = (int *)malloc(4 * len); 就类似于
int pArr[len]; 【动态构造了一个一维数组】
数组名:pArr; 数组类型:int; 数组长度:len;
*/
//对一维数组进行操作【赋值】
for (i=0; i<len; ++i)
{
printf("您要保存的第 %d 个数:\n", i+1);
scanf("%d", &pArr[i]);
}
//对一维数进行输出
printf("该一维数组的内容为:");
for (i=0; i<len; ++i)
printf("%d ", pArr[i]);
printf("\n");
free(pArr); //动态数组可以在程序还在运行时释放内存
return 0;
}
1.3.3 realloc()函数
int len; //设len=5
int * pArr;
pArr = (int *)malloc(4 * len);
realloc(pArr, 100); /*当发现pArr内存不够时,可以用realloc函数来修改空间大小
若原来为20,修改为100后前20个字节的数据依然保留
若原来为120,修改为100后,后20个字节的数据将丢失
*/