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;
}
运行结果:
注意:在对一个指针进行解引用时,一定要确保其指向了有效的地址!!!这是一个十分重要的问题!对未正确赋值的指针进行解引用(访问)是十分危险的行为.
指针声明的问题
我们在声明指针的时候,一定要注意和普通类型进行区分.普通类型与其对应的指针可以在一个声明中出现:
int *p1,a,*p2;
这里声明了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/上发布