关于c:被称为程序员试金石的指针真的没有那么难不信的话你来看看

6次阅读

共计 6649 个字符,预计需要花费 17 分钟才能阅读完成。

很多敌人放弃 C 语言都是因为指针,说什么指针的 * 号很厌恶啦、分不清址与值啦,当然了,最烦的还是链表结点,原本链表操作就让人烦了,再加上指针这个货色真是让初学的敌人苦不堪言,最初放弃了 C 语言转投其余语言的怀抱,然而只有了解指针的实质,那么把握指针只是工夫问题,这篇文章就跟大家一起来看看 C 语言的指针到底是何方神圣。文末放了一张 c /c++ 入门学习纲要,有须要的敌人能够看一下。

有什么不了解的中央欢送进群 973961276 来跟大伙一下交换,或者间接到课堂上面对面求教 c /c++ 我的项目实战,闲话少说,咱们开启注释。


一、指针到底是什么?

计算机中所有的数据都必须放在内存中,不同类型的数据占用的字节数不一样,例如 int 占用 4 个字节,char 占用 1 个字节。为了正确地拜访这些数据,必须为每个字节都编上号码,就像门牌号、身份证号一样,每个字节的编号是惟一的,依据编号能够精确地找到某个字节。
下图是 4G 内存中每个字节的编号(以十六进制示意):

咱们将内存中字节的编号称为地址(Address)或指针(Pointer)。地址从 0 开始顺次减少,对于 32 位环境,程序可能应用的内存为 4GB,最小的地址为 0,最大的地址为 0XFFFFFFFF。
上面的代码演示了如何输入一个地址:

#include <stdio.h>

int main(){
int a = 100;
char str[20] = "c.biancheng.net";
printf("%#X, %#Xn", &a, str);
return 0;
}

运行后果:

0X28FF3C, 0X28FF10

%#X示意以十六进制模式输入,并附带前缀 0X。a 是一个变量,用来寄存整数,须要在后面加& 来取得它的地址;str 自身就示意字符串的首地址,不须要加&

C 语言中有一个控制符 %p,专门用来以十六进制模式输入地址,不过 %p 的输入格局并不对立,有的编译器带0x 前缀,有的不带,所以此处咱们并没有采纳。

一切都是地址

C 语言用变量来存储数据,用函数来定义一段能够重复使用的代码,它们最终都要放到内存中能力供 CPU 应用。
数据和代码都以二进制的模式存储在内存中,计算机无法从格局上辨别某块内存到底存储的是数据还是代码。当程序被加载到内存后,操作系统会给不同的内存块指定不同的权限,领有读取和执行权限的内存块就是代码,而领有读取和写入权限(也可能只有读取权限)的内存块就是数据。
CPU 只能通过地址来获得内存中的代码和数据,程序在执行过程中会告知 CPU 要执行的代码以及要读写的数据的地址。如果程序不小心出错,或者开发者无意为之,在 CPU 要写入数据时给它一个代码区域的地址,就会产生内存拜访谬误。这种内存拜访谬误会被硬件和操作系统拦挡,强制程序解体,程序员没有解救的机会。
CPU 拜访内存时须要的是地址,而不是变量名和函数名!变量名和函数名只是地址的一种助记符,当源文件被编译和链接成可执行程序后,它们都会被替换成地址。编译和链接过程的一项重要工作就是找到这些名称所对应的地址。
假如变量 a、b、c 在内存中的地址别离是 0X1000、0X2000、0X3000,那么加法运算 c = a + b; 将会被转换成相似上面的模式:

0X3000 = (0X1000) + (0X2000);

()示意取值操作,整个表达式的意思是,取出地址 0X1000 和 0X2000 上的值,将它们相加,把相加的后果赋值给地址为 0X3000 的内存
变量名和函数名为咱们提供了不便,让咱们在编写代码的过程中能够应用易于浏览和了解的英文字符串,不必间接面对二进制地址,那场景几乎让人解体。
须要留神的是,尽管变量名、函数名、字符串名和数组名在实质上是一样的,它们都是地址的助记符,但在编写代码的过程中,咱们认为变量名示意的是数据自身,而函数名、字符串名和数组名示意的是代码块或数据块的首地址。

对于程序内存、编译链接、可执行文件的构造以及如何找到名称对应的地址咱们前面会讲。

光看文字无奈齐全了解的敌人能够看这个视频 c /c++《指针详解》,以便更好的了解


二、指针变量的定义和应用

数据在内存中的地址也称为指针,如果一个变量存储了一份数据的指针,咱们就称它为 指针变量
在 C 语言中,容许用一个变量来寄存指针,这种变量称为指针变量。指针变量的值就是某份数据的地址,这样的一份数据能够是数组、字符串、函数,也能够是另外的一个一般变量或指针变量。
当初假如有一个 char 类型的变量 c,它存储了字符 ‘K’(ASCII 码为十进制数 75),并占用了地址为 0X11A 的内存(地址通常用十六进制示意)。另外有一个指针变量 p,它的值为 0X11A,正好等于变量 c 的地址,这种状况咱们就称 p 指向了 c,或者说 p 是指向变量 c 的指针。

定义指针变量

定义指针变量与定义一般变量十分相似,不过要在变量名后面加星号*,格局为:

datatype *name;

或者

datatype *name = value;

*示意这是一个指针变量,datatype示意该指针变量所指向的数据的类型。例如:

int *p1;

p1 是一个指向 int 类型数据的指针变量,至于 p1 到底指向哪一份数据,应该由赋予它的值决定。再如:

int a = 100;
int *p_a = &a;

在定义指针变量 p_a 的同时对它进行初始化,并将变量 a 的地址赋予它,此时 p_a 就指向了 a。值得注意的是,p_a 须要的一个地址,a 后面必须要加取地址符 &,否则是不对的。
和一般变量一样,指针变量也能够被屡次写入,只有你想,随时都可能扭转指针变量的值,请看上面的代码:

// 定义一般变量
float a = 99.5, b = 10.6;
char c = '@', d = '#';
// 定义指针变量
float *p1 = &a;
char *p2 = &c;
// 批改指针变量的值
p1 = &b;
p2 = &d;

*是一个特殊符号,表明一个变量是指针变量,定义 p1、p2 时必须带 *。而给 p1、p2 赋值时,因为曾经晓得了它是一个指针变量,就没必要多此一举再带上*,后边能够像应用一般变量一样来应用指针变量。也就是说,定义指针变量时必须带*,给指针变量赋值时不能带*
假如变量 a、b、c、d 的地址别离为 0X1000、0X1004、0X2000、0X2004,上面的示意图很好地反映了 p1、p2 指向的变动:

须要强调的是,p1、p2 的类型别离是 float*char*,而不是 floatchar,它们是齐全不同的数据类型,读者要引起留神。
指针变量也能够间断定义,例如:

  1. int a, b, c; //a、b、c 的类型都是 int

留神每个变量后面都要带*。如果写成上面的模式,那么只有 a 是指针变量,b、c 都是类型为 int 的一般变量:

  1. int *a, b, c;

通过指针变量获得数据

指针变量存储了数据的地址,通过指针变量可能取得该地址上的数据,格局为:

*pointer;

这里的 * 称为指针运算符,用来获得某个地址上的数据,请看上面的例子:

#include <stdio.h>

int main(){
int a = 15;
int *p = &a;
printf("%d, %dn", a, *p); // 两种形式都能够输入 a 的值
return 0;
}

运行后果:
15, 15
假如 a 的地址是 0X1000,p 指向 a 后,p 自身的值也会变为 0X1000,p 示意获取地址 0X1000 上的数据,也即变量 a 的值。从运行后果看,p 和 a 是等价的。
上节咱们说过,CPU 读写数据必须要晓得数据在内存中的地址,一般变量和指针变量都是地址的助记符,尽管通过 p 和 a 获取到的数据一样,但它们的运行过程稍有不同:a 只须要一次运算就可能获得数据,而 p 要通过两次运算,多了一层“间接”。
假如变量 a、p 的地址别离为 0X1000、0XF0A0,它们的指向关系如下图所示:

程序被编译和链接后,a、p 被替换成相应的地址。应用 *p 的话,要先通过地址 0XF0A0 获得变量 p 自身的值,这个值是变量 a 的地址,而后再通过这个值获得变量 a 的数据,前后共有两次运算;而应用 a 的话,能够通过地址 0X1000 间接获得它的数据,只须要一步运算。
也就是说,应用指针是间接获取数据,应用变量名是间接获取数据,前者比后者的代价要高。
指针除了能够获取内存上的数据,也能够批改内存上的数据,例如:

#include <stdio.h>

int main(){
int a = 15, b = 99, c = 222;
int *p = &a; // 定义指针变量
*p = b; // 通过指针变量批改内存上的数据
c = *p; // 通过指针变量获取内存上的数据
printf("%d, %d, %d, %dn", a, b, c, *p);
return 0;
}

运行后果:
99, 99, 99, 99
*p 代表的是 a 中的数据,它等价于 a,能够将另外的一份数据赋值给它,也能够将它赋值给另外的一个变量。
*在不同的场景下有不同的作用:*能够用在指针变量的定义中,表明这是一个指针变量,以和一般变量辨别开;应用指针变量时在后面加 * 示意获取指针指向的数据,或者说示意的是指针指向的数据自身。
也就是说,定义指针变量时的 * 和应用指针变量时的 * 意义齐全不同。以上面的语句为例:

int *p = &a;
*p = 100;

第 1 行代码中 * 用来指明 p 是一个指针变量,第 2 行代码中 * 用来获取指针指向的数据。
须要留神的是,给指针变量自身赋值时不能加*。批改下面的语句:

int *p;
p = &a;
*p = 100;

第 2 行代码中的 p 后面就不能加 *
指针变量也能够呈现在一般变量能呈现的任何表达式中,例如:

int x, y, *px = &x, *py = &y;
y = *px + 5; // 示意把 x 的内容加 5 并赋给 y,*px+ 5 相当于(*px)+5
y = ++*px; //px 的内容加上 1 之后赋给 y,++*px 相当于 ++(*px)
y = *px++; // 相当于 y =(*px)++
py = px; // 把一个指针的值赋给另一个指针

【示例】通过指针替换两个变量的值。

#include <stdio.h>

int main(){
int a = 100, b = 999, temp;
int *pa = &a, *pb = &b;
printf("a=%d, b=%dn", a, b);
/***** 开始替换 *****/
temp = *pa; // 将 a 的值先保存起来
*pa = *pb; // 将 b 的值交给 a
*pb = temp; // 再将保存起来的 a 的值交给 b
/***** 完结替换 *****/
printf("a=%d, b=%dn", a, b);
return 0;
}

运行后果:
a=100, b=999
a=999, b=100
从运行后果能够看出,a、b 的值曾经产生了替换。须要留神的是长期变量 temp,它的作用特地重要,因为执行 *pa = *pb; 语句后 a 的值会被 b 的值笼罩,如果不先将 a 的值保存起来当前就找不到了。

对于 * 和 & 的谜题

假如有一个 int 类型的变量 a,pa 是指向它的指针,那么 *&a&*pa别离是什么意思呢?
*&a能够了解为 *(&a)&a 示意取变量 a 的地址(等价于 pa),*(&a)示意取这个地址上的数据(等价于 *pa),绕来绕去,又回到了原点,*&a依然等价于 a。
&*pa能够了解为 &(*pa)*pa 示意获得 pa 指向的数据(等价于 a),&(*pa)示意数据的地址(等价于 &a),所以 &*pa 等价于 pa。

对星号 * 的总结

在咱们目前所学到的语法中,星号 * 次要有三种用处:

  • 示意乘法,例如int a = 3, b = 5, c;  c = a * b;,这是最容易了解的。
  • 示意定义一个指针变量,以和一般变量辨别开,例如int a = 100;  int *p = &a;
  • 示意获取指针指向的数据,是一种间接操作,例如int a, b, *p = &a;  *p = 100;  b = *p;

这里举荐一本书以便更好的了解《C 和指针》,有须要的敌人能够评论区留言或者进群获取。

三、C 语言指针变量的运算(加法、减法和比拟运算)

指针变量保留的是地址,而地址实质上是一个整数,所以指针变量能够进行局部运算,例如加法、减法、比拟等,请看上面的代码:

#include <stdio.h>

int main(){
int a = 10, *pa = &a, *paa = &a;
double b = 99.9, *pb = &b;
char c = '@', *pc = &c;
// 最后的值
printf("&a=%#X, &b=%#X, &c=%#Xn", &a, &b, &c);
printf("pa=%#X, pb=%#X, pc=%#Xn", pa, pb, pc);
// 加法运算
pa++; pb++; pc++;
printf("pa=%#X, pb=%#X, pc=%#Xn", pa, pb, pc);
// 减法运算
pa -= 2; pb -= 2; pc -= 2;
printf("pa=%#X, pb=%#X, pc=%#Xn", pa, pb, pc);
// 比拟运算
if(pa == paa){printf("%dn", *paa);
}else{printf("%dn", *pa);
}
return 0;
}

运行后果:

&a=0X28FF44, &b=0X28FF30, &c=0X28FF2B
pa=0X28FF44, pb=0X28FF30, pc=0X28FF2B
pa=0X28FF48, pb=0X28FF38, pc=0X28FF2C
pa=0X28FF40, pb=0X28FF28, pc=0X28FF2A
2686784

从运算后果能够看出:pa、pb、pc 每次加 1,它们的地址别离减少 4、8、1,正好是 int、double、char 类型的长度;减 2 时,地址别离缩小 8、16、2,正好是 int、double、char 类型长度的 2 倍。
这很奇怪,指针变量加减运算的后果跟数据类型的长度无关,而不是简略地加 1 或减 1,这是为什么呢?
以 a 和 pa 为例,a 的类型为 int,占用 4 个字节,pa 是指向 a 的指针,如下图所示:

刚开始的时候,pa 指向 a 的结尾,通过 *pa 读取数据时,从 pa 指向的地位向后挪动 4 个字节,把这 4 个字节的内容作为要获取的数据,这 4 个字节也正好是变量 a 占用的内存。
如果 pa++; 使得地址加 1 的话,就会变成如下图所示的指向关系:

这个时候 pa 指向整数 a 的两头,*pa 应用的是红色虚线画出的 4 个字节,其中前 3 个是变量 a 的,前面 1 个是其它数据的,把它们“搅和”在一起显然没有理论的意义,获得的数据也会十分怪异。
如果 pa++; 使得地址加 4 的话,正好可能齐全跳过整数 a,指向它前面的内存,如下图所示:

咱们晓得,数组中的所有元素在内存中是间断排列的,如果一个指针指向了数组中的某个元素,那么加 1 就示意指向下一个元素,减 1 就示意指向上一个元素,这样指针的加减运算就具备了事实的意义,咱们将在《C 语言数组指针》一节中深入探讨。
不过 C 语言并没有规定变量的存储形式,如果间断定义多个变量,它们有可能是挨着的,也有可能是扩散的,这取决于变量的类型、编译器的实现以及具体的编译模式,所以对于指向一般变量的指针,咱们往往不进行加减运算,尽管编译器并不会报错,但这样做没有意义,因为不晓得它前面指向的是什么数据。
上面的例子是一个反面教材,正告读者不要尝试通过指针获取下一个变量的地址:

#include <stdio.h>

int main(){
int a = 1, b = 2, c = 3;
int *p = &c;
int i;
for(i=0; i<8; i++){printf("%d,", *(p+i) );
}
return 0;
}

在 VS2010 Debug 模式下的运行后果为:

3, -858993460, -858993460, 2, -858993460, -858993460, 1, -858993460,

能够发现,变量 a、b、c 并不挨着,它们两头还参杂了别的辅助数据。
指针变量除了能够参加加减运算,还能够参加比拟运算。当对指针变量进行比拟运算时,比拟的是指针变量自身的值,也就是数据的地址。如果地址相等,那么两个指针就指向同一份数据,否则就指向不同的数据。
下面的代码(第一个例子)在比拟 pa 和 paa 的值时,pa 曾经指向了 a 的上一份数据,所以它们不相等。而 a 的上一份数据又不晓得是什么,所以会导致 printf() 输入一个没有意义的数,这正好印证了下面的观点,不要对指向一般变量的指针进行加减运算。
另外须要阐明的是,不能对指针变量进行乘法、除法、取余等其余运算,除了会产生语法错误,也没有理论的含意。


就先写这么多吧,如果有人看的话前面再更新,最初给大家发个福利,这是一本了解指针的电子书☞《C 和指针》

因为很多敌人学习 c /c++ 都不得门而入,所以最初再给大家贴个学习纲要☟

好了,这篇文章就到这里了,如果这篇文章没有解决到你的问题,能够进群来征询老师和支付优质视频跟电子书材料哦,感觉写的还不错的话请关注点赞珍藏三连!

正文完
 0