一、问题引入
我们先编写一个交换两个变量数值的程序,名为change.c,代码如下:
#include <stdio.h>
void change(int a, int b){
int tmp = a;
a=b;
b=tmp;
}
int main(){
int anum = 2;
int bnum = 5;
change(anum, bnum);
printf("number a's value is %d, number b's value is %d\n", anum, bnum);
return 0;
}
逻辑很简单,对于a和b两个变量,我们先创建一个临时变量tmp,然后将a的值赋给tmp,然后把b的值赋给a,最后把原来a的值再赋给b。这在数学上是没错的,然后我们执行以下代码,看看结果:
我们可以看到它的结果并没有改变,这不科学!
为什么会出现这种情况?
首先我们要了解这些变量和他们的值,在内存中是如何存储的。
二、内存
1、地址总线
10年前使用32bit的操作系统,现在使用64bit的操作系统。这个32和64,就是计算机硬件地址总线的数量。
现在1个地址线只能标记2个内存地址,即0号和1号,2根地址线能标记4个内存地址,即00、01、10、11,64跟地址总线就可以标记264个内存地址。
1个内存地址有1Byte的空间,232个内存地址可以标记4G的内存,264可以给16EB的内存标记地址,在现实生活中还不存在这样大的内存。
二进制可以转换为16进制,将内存地址用16进制来表示即如下图所示,左边绿色的是内存地址,右边黑色的是每个内存单元,即1Byte:
2、操作系统
我们编写的所有程序都是运行在操作系统上的,操作系统会给内存指定编号,除此之外还给内存做了一些规划。例如在64位操作系统中,OS认为用户只要使用前48位内存就可以了,48位之后的内存都由系统来使用。
操作系统的内存和用户的内存分开使用,就可以避免被用户的程序占用,防止卡住、死机等状态,变得更安全。
内存划分区域如下图所示:
下方为低编号地址内存,上面为高编号地址内存。我们编写的程序在编译为编译码后,这些数据会存在代码段。在执行过程中产生的变量、声明的常量等数据会存放在数据段。
代码段和数据段已经在内存中规划好了,全都存在低编号的内存地址中。
自由可分配内存就是程序执行的时候可以使用的内存,理论上它越大电脑就越不卡。
栈内存通常是用来存储最开始执行的程序,比如C语言中的main函数。
三、代码解析
1、GDP工具
gcc 的调试工具 gdb(通过gdb -help
查看选项)
编译的时候使用-g选项可以进入调试模式,例如gcc -g main.c -main.out
调试命令:
gdb main.out
l(就是艾露)(list 显示源代码,l 或者 enter 继续执行l 继续显示)
1 break 12(行号) 打断点
2 start 单步调试
3 n(next 下一行)
4 p a (print 打印变量a)
5 s (step 进入方法,n执行下一行)
6 bt 查看函数堆栈
7 f 切换函数堆栈(f 1 切换到1)
8 q 退出调试
9 p *a(int *a 时 p a 打印出的是a的内存地址,p *a打印的是这个地址里对应的值.P &a 显示a的内存地址空间P &functionname p + &函数名,显示函数程序在代码段的内存地址)
2、变量和指针
首先编写如下代码:
#include <stdio.h>
int global = 0;
int rect(int a, int b){
static int count = 0;
count++;
global++;
int s = a * b;
return s;
}
int quadrate(int a){
static int count = 0;
count++;
global++;
int s = rect(a, a);
return s;
}
int main(){
int a = 3;
int b = 4;
int *pa = &a;
int *pb = &b;
int *pglobal = &global;
int (*pquadrate) (int a) = &quadrate;
int s = quadrate(a);
printf("The space of square is %d\n", s);
return 0;
}
上述代码分别是计算长方形和正方形的代码。然后使用调试模式编译代码:
通过调试模式我们可以知道,程序在执行的时候,记录运行到了哪一行,记录当前运行的函数是什么,记录当前所有的变量值是什么,这些数据就存储在内存的“栈”中。
(1)变量的本质
通过上图的方式可以查看到变量a在内存所在的地址。所以变量是什么?变量名只不过是一个代号,变量的本质就是内存。
现在再解释以下第一节的问题,变量a和b都是内存的代号,我们传入的都是内存代号,所以就把内存代号中的值给了change函数,而change函数中的a和b,是该函数内另外的两个内存代号,和main中的内存并无关系,所以change函数中的a和b交换了函数值并没有让main中的anum和bnum交换。
如果想让main函数中的anum和bnum对应的内存交换数值,就需要把main函数中的anum和bnum所对应的内存当作参数传入change函数,这样才能交换。词时change函数中的a和b所对应的就不是两个数值了,而是两个内存地址(即内存空间)。
方法就是在调用change函数的时候,传入&anum而不是anum,&bnum而不是bnum。
(2)指针的本质
上图中,pa的值是变量a的内存地址,而pa本身的内存地址则是0xbffff69c。
所以指针的本质就是保存变量的内存地址。
我们使用这样的方式来定义一个指针:
Type *p;
我们说p是指向type类型的指针,type可以是任意类型,除了可以是char,short, int, long等基本类型外,还可以是指针类型,例如int *, int **, 或者更多级的指针,也可是是结构体,类或者函数等。于是,我们说:
int * 是指向int类型的指针;
int **,也即(int *) *,是指向int *类型的指针,也就是指向指针的指针;
int ***,也即(int *) ,是指向int类型的指针,也就是指向指针的指针的指针;
…我想你应该懂了
struct xxx *,是指向struct xxx类型的指针;
其实,说这么多,只是希望大家在看到指针的时候,不要被int **这样的东西吓到,就像前面说的,指针就是指向某种类型的指针,我们只看最后一个号,前面的只不过是type类型罢了。
细心一点的人应该发现了,在“什么是指针”这一小节当中,已经表明了:指针的长度跟CPU的位数相等****,大部分的CPU是32位的,因此我们说,指针的长度是32bit,也就是4个字节!注意:任意指针的长度都是4个字节,不管是什么指针!(当然64位机自己去测一下,应该是8个字节吧。。。)
于是:
Type *p;
sizeof(p)的值是4,Type可以是任意类型,char,int, long, struct, class, int **…
以后大家看到什么sizeof(char*), sizeof(int *),sizeof(xxx *),不要理会,统统写4,只要是指针,长度就是4个字节,绝对不要被type类型迷惑!
3、函数指针
我们把之前的代码修改一下:
重新编译为调试模式,再p一下(*pquadrate)看一下有什么效果:
我们可以得出以下结论:
1.
int quadrate(int a);
是一个函数 int (*pquadrate)(int a)=&quadrate;
则是指向这个函数的指针!int s=(*pquadrate)(a);
可以调用函数!2.一个指针变量
*q
不加*
号:P q
取出自己地址中存储的值(一个地址)。 加*
号:P *q
取出指向地址中存储的值。
四、数组与字符串
1、数组的本质
Array数组其实是一种指针常量,而p则是一种指针变量(所以理论上,数组也是指针,数组和指针有一定的通用性,又有一定的差别,指针可以表达数组,而数组不可以表达指针);
若p是指针类型,则p++就是指针地址一次增加4个字节,也被称作指针偏移,其运行效率比数组高;
为什么p+4;*p=101;
与p[4]=101;
等价?p[4]=101;
代表从初始位置(以四个字节为偏移量进行偏移到达某个位置,然后对这个位置进行初始化赋值,即把101赋给这个地址所代表的内存空间。p+4代表从初始位置(a的地址就始)以四个字节为一步,向前走4步,到达某个位置,然后*p=101,代表此时指针指向的地址(即走了四步后所在位置)并对这个地址所在的内存空间进行初始化,赋值为101。
数组名本质是一个数组开头的地址,可以把它赋值给指针变量
int array[n];
int *p=array;
但是数组不可以array+=2;
,而指针可以p+=2;
,因为指针是变量,array是指针常量。
2、数组的声明和赋值
我们声明一个int类型的数组:
int a[5];
它表示声明了一个占5个内存地址的指针常量a,a中的数值都是int类型。
赋值如下:
a[0] = 1;
a[1] = 3;
a[2] = 4;
a[3] = 5;
a[4] = 6;
3、字符串
(1)字符串表示形式
①用字符数组实现
char string[] = “I love China!”;
② 用字符指针实现
char *string = “I love China!”;
(2)字符指针变量与字符数组
char *cp;
与char str[20];
①str由若干元素组成,每个元素放一个字符;而cp中存放字符串首地址
②char str[20]; str=“I love China!”;
(错)
char *cp; cp=“I love China!”;
(对)
③str是内存指针常量;cp是内存指针变量
④cp接受键入字符串时,必须先开辟存储空间
(3)字符串与数组的关系
①字符串用一维字符数组存放。
②字符数组具有一维数组的所有特点。
③数组名是指向数组首地址的地址常量。
④数组元素的引用方法可用指针法和下标法。
⑤数组名作函数参数是地址传递等
(4)字符串与数组区别
①存储格式:字符串有结束标志\000
②赋值方式与初始化不同
③输入输出方式:%s %c
例子:
char str[]={“Hello!”}; //对
char str[]=“Hello!”; //对
char str[]={‘H’,‘e’,‘l’,‘l’,‘o’,‘!’}; //对
char *cp=“Hello”; //对
int a[]={1,2,3,4,5}; //对
int *p={1,2,3,4,5}; //错
char str[10],*cp;
int a[10], *p;
str=“Hello”; //错
cp=“Hello!”; //对
a={1,2,3,4,5}; //错
p={1,2,3,4,5}; //错
4、字符指针变量使用注意事项
当字符指针指向字符串时,除了可以被赋值之外,与包含字符串的字符数组没有什么区别。