title: C语言教程-13_1-初识指针
tags: [C]
categories: C语言教程
description: 接触C语言的魂魄-指针
概要:
- 扼要讲解内存地址与内存模子
- 简朴介绍C语言的指针这一数据范例
- 把握指针相关最基本的两种互逆运算
前置知识:
- 理解能力和想象能力
- 耐烦和实验精神
- 数组与函数的知识
互换两个变量的问题
我们从一个问题开始引入指针.
考虑这个问题:在main()函数中有两个int变量a和b,我该如何互换这两个变量的值?
如果我们要求仅在一个函数中解决这个问题,那么很轻易想到,最简朴的办法就是新建一个int范例的中间变量,好比定名为temp.那么我们就有如下操作进行互换(非常简朴不详细表明):
- #include <stdio.h>
- int main() {
- int a=3,b=4;
- int c; // 中间变量
- // 经典3步进行交换
- c = a;
- a = b;
- b = c;
- printf("a=%d,b=%d\n",a,b); // 输出结果 a=4,b=3
- return 0;
- }
复制代码 只需要这3步即可进行互换.
现在问题来了,如果我们要求创建一个函数swap()来实现这个操作,该如何实现?
也许我们可以这样:
- #include <stdio.h>
- void swap(int a, int b) {
- int temp;
- temp = a;
- a = b;
- b = temp;
- }
- int main() {
- int a = 3, b = 4;
- swap(a, b);
- printf("a=%d,b=%d\n", a, b); // 输出错误的结果:a=3,b=4
- return 0;
- }
复制代码 我们实验简朴地把a和b传递给swap()函数,运行一下,效果显然是错误的,a和b的值并没有互换.
回顾前面函数的知识,前面讲过,C函数的参数都是按值传递,这里也就是将main()中的a和b的值简朴地复制给swap()的两个参数a和b,换句话说,此a,b非彼a,b.
效果就是,在swap()中的a,b确实被成功地互换了,但是main()中的a,b完全没有变革.
我们想要在swap()函数中互换main()中的a和b,根本的问题在于我们需要访问到他们,C语言的指针范例提供了这种功能.
地址和指针
计算机内存与地址
计算机运行时需要的各种数据都存储于内存中(就是寻常说的内存条),从逻辑上来看,一整个内存可以视为一个超级巨大的数组,例如我们的内存是4GB,那么这个数组的总大小就是4GB.我们仅仅讲解内存地址这个概念,详细的内存结构这里并不关心.
程序的相关数据就存储在内存中,例如实行的机器代码,局部变量,全局变量(定义在函数外的变量),常量字面值.他们以某种特定的模式进行存储,存储的位置各不相同,为了找到他们,我们需要以字节为单元为整个内存进行编号.也就是所谓的内存地址.需要注意的是,我们通常以16进制来表示地址值(究竟内存如此巨大,2,10进制是不够方便的).
以4GB内存举例,我们需要8个16进制位来完整编号,即16^8==4,294,967,296,也就是4GB的大小.从0开始,第一个字节编号为0x00000000,最后一个字节编号为0xFFFFFFFF.当然,如此巨大的内存范围不大概全部让我们恣意取用,现实上我们自己的应用程序只能使用操作系统(例如Windows)规定的一块内存,当然这完全够用.
例如,我们在内存地址0x2~0x8存储了一些特定的数据,内容如下:
可以看到每一个地址都指向内存中的一个特定字节,这个1字节大小的空间中存储了某些特定的数据,程序根据数据的地址从内存中找到他们,以进行运算.
另一方面,尽管我们对每一个字节都进行了编号,但是往往我们将多少个字节组合起来使用,例如一个int变量,就直接占用了4个字节,此时我们将4个连续的字节视为一个团体,取最开头的那个字节作为代表,指代整个int变量.
例如我们有int i=2;则i在内存中的结构如下:
注意,我们上面的地址值仅仅作为演示,在现在流行的x64机器中并不是这样的:
- #include <stdio.h>
- int main() {
- int i = 2;
- // 使用循环,逐字节输出i在内存中的值(十六进制),同时输出每个字节的地址
- for (int j = 0; j < sizeof(i); j++) {
- printf("0x%p %02x\n", (char *)&i + j, *((char *)&i + j));
- }
- return 0;
- }
复制代码 在我的条记本电脑运行如下:
先不管上面的代码是什么原理,仅仅看一下效果,输出了i占用的这4个字节内部的值,同时可以看到前面的内存是连续的.如果我们使用printf("%p",&i);输出i的地址,效果将会是第一个地址,这意味着使用最小的那个地址指代整个变量i.
读者无需关心为什么是02 00 00 00而不是00 00 00 02,这涉及到大小端序的问题.
处理地址-C语言的指针
现在我们已经相识了最基本的内存知识,并举了一个int变量的例子,相识了变量的存储.下面引入指针.
用最简朴的一句话概括指针就是:指针就是地址.我们有时间需要在程序中获取到某个变量的地址,C语言提供了指针这一数据范例,用指针范例声明的变量就叫指针变量,其内部存储一个无符号的整数(往往是4或8字节大小),代表一个地址.
顾名思义,指针,就像一个箭头指向一个地方,和地址的作用相同.只不外前面说的地址是指计算机内存的编址,而指针,是C语言为了能够处理内存地址而引入的一种机制.某种角度而言,地址值仍旧是一个整数,以是我们想要存储他,和平凡的整数无异,但是为了特殊化,C语言引入了指针范例这种数据范例,这种(类)范例的变量存储的是一个特殊整数代表一个指针(地址).
获取一个变量的地址非常简朴,使用&取地址运算符,输入一个指针也很简朴,在printf()中使用%p即可:
- #include <stdio.h>
- int main() {
- int i = 3; // 声明并初始化为3一个int类型变量i
- printf("%p",&i); // &i这个表达式代表获取到i在内存中的地址
- return 0;
- }
复制代码 输出如下:
这代表着变量i就存储在这个地址值指向的内存块中.
如果我们想要将这个地址存储下来,那么就需要使用C语言的指针.
声明一个最简朴的指针变量仅仅需要在变量名前多加一个*,语法如下:
- #include <stdio.h>
- int main() {
- int i = 3; // 声明并初始化为3一个int类型变量i
- int *p; // 声明一个变量p,其类型为int*,代表它是一个指针
- p = &i; // 将i的地址赋值给p
- printf("%p",p); // 此时不再需要&,因为p存储的就是i的地址
- return 0;
- }
复制代码 输出效果同样,也是一个地址.
再进一步,我们既然存储了某个变量的地址(指针),那么就意味着我们想要根据这个地址(指针)去访问其指向的内存单元,我们使用另一种运算符,即*解引用运算符(或者叫指针运算符),与&相反,*用于对一个地址进行访问,反向获取到此处存储的详细变量(值),这种相对于取地址操作的逆操作称为解引用操作.
仍旧是上面的例子,我们实验使用p存储的地址去间接访问变量i:
- #include <stdio.h>
- int main() {
- int i = 3; // 声明并初始化为3一个int类型变量i
- int *p; // 声明一个变量p,其类型为int*,代表它是一个指针
- p = &i; // 将i的地址赋值给p
- printf("%d",*p); // 此时对p的值进行*运算,也就是解引用,效果就是将p中的值作为一个
- // 指针,去访问对应的内存,从而取出变量i的值
- // 换句话说,这里的*p和i是等价的.
- return 0;
- }
复制代码 运行效果:
注意:在对一个指针进行解引用时,一定要确保其指向了有用的地址!!!这是一个非常重要的问题!对未精确赋值的指针进行解引用(访问)是非常危险的行为.
指针声明的问题
我们在声明指针的时间,一定要注意和平凡范例进行区分.平凡范例与其对应的指针可以在一个声明中出现:
这里声明确2个int*范例的指针p1和p2,和一个int范例的变量a.
要注意的是,p2前面仍旧需要一个*代表它是一个指针,另外,尽管p1前面已经有了一个*,但是a仍旧仅仅是一个int的变量而已.
解决互换问题
现在我们可以实验使用指针进行互换两个变量的值.
我们既然要使用函数互换两个变量,那么就要求函数能够访问到这两个变量,现在,我们可以使用两个指针参数实现.
- // 在函数中使用指针进行交换两个变量的值
- #include <stdio.h>
- void swap(int *a, int *b) {
- int temp = *a;
- *a = *b;
- *b = temp;
- }
- int main() {
- int a = 10, b = 20;
- printf("a = %d, b = %d\n", a, b);
- swap(&a, &b);
- printf("a = %d, b = %d\n", a, b);
- return 0;
- }
复制代码 我们的swap()函数的两个参数不再是两个int范例的参数,而是int *范例的指针,代表着这个指针可以指向一个int变量.
main()函数中,swap(&a,&b);使用&取地址运算符计算a和b的地址,传递给swap的两个形参.
接下来,在swap()中使用一个中间变量(temp仍旧需要),进行互换,对指针变量使用*进行解引用,获取到两个要互换的int值,然后进行互换即可.
运行效果如下:
上面的例子我们相识了如下内容:
- 如何获取一个地址(取地址运算符)
- 如何存储一个地址(指针变量)
- 如何使用一个地址去访问内存(解引用运算符)
接下来探究指针范例.
指针的范例
差别基本范例的指针
前面的例子都是使用了int *这个范例,代表着对应的指针变量(应该)指向的是一个int范例的变量.
其他范例的变量同理,如果我们需要指向一个float范例的变量,那么就使用float *即可:
- // 其他类型的变量同理,如果我们需要指向一个float类型的变量,那么就使用`float *`即可:
- #include <stdio.h>
- int main(){
- float var = 3.1415;
- float *ptr = &var;
- printf("var == %f\n", var);
- return 0;
- }
复制代码 使用ptr指针就能访问到var.
别的,指针范例不匹配是不允许的操作:
- // 此外,指针类型不匹配是不允许的操作
- #include <stdio.h>
- int main(){
- int a = 10;
- int *p = &a;
- char *q = p; // 这里报错: cannot convert 'int*' to 'char*' in initialization
- return 0;
- }
复制代码 代表着两个指针不兼容,即一个int *的指针不能赋值给一个char *的指针.
空范例指针
有时间,我们大概仅仅想要存一个地址,而不关心其范例,那么可以使用void *范例,即空范例指针,任何范例的指针都能赋值给void *:
- #include <stdio.h>
- int main() {
- int a = 1024;
- void *p = &a; // &a为指针,其类型为int*,可以直接赋值给void*而无需任何处理
- printf("%d\n", *(int *)p); // void*指针不允许直接解引用,必须进行强制类型转换
- return 0;
- }
复制代码 上面的代码使用void*指针p存储了a的地址,在使用p访问a的时间,必须使用逼迫范例转换将void*转换为int*才能进行解引用.由于对void*解引用的话,无法判断现实占用了多少内存,以是下面的代码编译器报错"不允许使用不完整的范例":
- #include <stdio.h>
- int main() {
- int a = 1024;
- void *p = &a;
- printf("%d\n", *p);
- return 0;
- }
复制代码 这就是一个重要的问题:指针指向的数据范例的大小.
后面我们会渐渐的接触到void* 指针的重要作用.
特殊的指针值-NULL
还有的时间,我们希望一个指针变量不指向任何有用的地址,那么我们可以对其赋值为NULL空指针值.
- #include <stdio.h>
- int main() {
- int *p = NULL;
- printf("%p\n", p);
- // 实际上, NULL 就是 0
- return 0;
- }
复制代码 运行效果:
可以看到,p的值就是0. NULL是一个宏(宏定义),定义在stdio.h中(宏定义将在后面的头文件部分讲解):
- // stdio.h
- #define NULL ((void *)0)
复制代码 这个宏意味着NULL在预处理的时间直接替换为((void *)0)
也就是说,当一个指针值为NULL时,我们以为他不指向任何地址,并且以为NULL是安全的—我们查抄一个指针是否等于NULL来判断这个指针是否被初始化等…
后面会深入强调初始化的问题.
逼迫范例转换
指针本质上还是一个整数(无符号的),但是指针范例仍旧不能和平凡的整型互相赋值,如果我们想要将某个数值作为指针值进行赋值,可以使用逼迫范例转换.
- int value=0x7fffffff;
- int *p = (int *)value;
复制代码 这样,指针p就指向了0x7fffffff这个内存地址对应的内存单元.
别的,这里也能看出,int和int*是完全差别的两种范例!
多级指针
指针用于指向某种范例的变量(地址),同样,指针变量也可以被另外一个指针变量所指向,即指向指针的指针,这就是多级指针.
使用二级指针
可以这样声明一个二级指针:
- #include <stdio.h>
- int main(){
- int a = 3,*p = &a;
- int **p2 = &p; // p2是一个二级指针,指向p
- printf("%d\n",**p2); // 输出3, **p2 == *p == a == 3
- return 0;
- }
复制代码 运行效果:
声明int **p2就等价于int *(*p2);,也就是说p2是一个指针,指向的范例为int*,因此显然p2是一个二级指针,int *p可以称为一级指针.
二级指针仍旧是一个指针,只不外我们可以对它进行2次解引用:
- #include <stdio.h>
- int main() {
- int a = 3, *p = &a; // 声明一个int类型的指针变量p,指向a
- int **p2 = &p; // 声明一个int类型的二级指针变量p2,指向p
- /* 输出a的值 */
- printf("a = %d\n", a); // a = 3
- printf("*p = %d\n", *p); // *p = 3
- printf("**p2 = %d\n", **p2); // **p2 = 3
- /* 输出a的地址 */
- printf("&a = %p\n", &a); // &a即为a的地址
- printf("p = %p\n", p); // p存储的值即为a的地址
- /* 输出指针变量p的地址 */
- printf("&p = %p\n", &p); // &p即为p的地址
- printf("p2 = %p\n", p2); // p2存储的值即为p的地址
- return 0;
- }
复制代码 运行效果:
对二级指针的解引用
我们可以看出,二级指针可以进行2次解引用,第一次解引用的效果是访问其指向的变量,例如上面的例子中,
*p2即为p,p仍旧是一个指针,指向整型变量a,则对其再次解引用**p即可访问到a.
换言之,**p2可以视为*(*p2),读者应该清楚地意识到,这里的**完全是2步操作,你甚至可以在中间加一个空格.
- #include <stdio.h>
- int main() {
- int a = 3, *p = &a; // 声明一个int类型的指针变量p,指向a
- int **p2 = &p; // 声明一个int类型的二级指针变量p2,指向p
- /* 输出a的值 */
- printf("* *p2 = %d\n", * *p2); // **p2 = 3
-
- return 0;
- }
复制代码 运行效果:
以上使用二级指针进行了举例,三级指针等更"高级"的指针同理,只不外可以指向级数更高的指针而已,现实应用中,基本只用到二级指针.
- #include <stdio.h>
- int main() {
- int a = 3, *p = &a; // 声明一个int类型的指针变量p,指向a
- int **p2 = &p; // 声明一个int类型的二级指针变量p2,指向p
- int ***p3 = &p2; // 声明一个int类型的三级指针变量p3,指向p2
- return 0;
- }
复制代码 二级指针非常重要,特别是在使用C实现各种数据结构时,需要修改某些指针的指向时非常关键,后面的学习会频繁碰到.
指针是C语言的"魂魄",指针的内容险些占有了C语言的半壁山河,本部分简朴讲解了指针的基本概念和使用方法,后面会详细展开讲解.
---WAHAHA
注:文章原文在本人博客https://gngtwhh.github.io/上发布
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |