C语言基础05——指针
一、引言
指针是 C 语言中一个非常重要且强大的概念,也是C语言的一个重要特色。它允许程序直接操作内存地址,这使得 C 语言具有极高的灵活性和效率。正确而灵活地运用它,可以使程序简洁、紧凑,高效。下面将详细讲解 C 语言中的指针:
二、指针的基本概念
在此之前,请务必弄清楚存储单元的地址和存储单元的内容这两个概念的区别。
2.1 指针是什么?
- 指针本质上是一个变量,不过它存储的不是普通变量的值,而是另一个变量的内存地址。
- 我们知道,计算机内存中每个字节都有唯一的编号,这个编号就是内存地址。
- 通过指针,我们可以间接访问和修改它所指向的变量的值。
2.2 指针的作用
1.操作其他函数中的变量。
2.函数返回多个值。
3.函数的结果和计算状态分开。
4.方便操作数组和函数。
三、指针变量
3.1 什么是指针变量
如果有一个变量专门用来存放另一变量的地址(即指针),则它称为“指针变量”。指针变量就是地址变量,用来存放地址,指针变量的值是地址(即指针)。
注意:区分“指针”和“指针变量”这两个概念。例如,可以说变量i的指针是 2000,而不能说i的指针变量是 2000。指针是一个地址,而指针变量是存放地址的变量。
3.2 怎样定义指针变量
声明指针的基本语法:
数据类型 *指针变量名;
例如:
int *p; // 声明一个指向int类型变量的指针p
float *f_ptr; // 声明一个指向float类型变量的指针f_ptr
char *c; // 声明一个指向char类型变量的指针c
*
在这里表示 "指向... 的指针"
注意:
1.指针变量的数据类型要跟指向变量的类型保持一致。左端的int是在定义指针变量时必须指定的“基类型”。指针变量的基类型用来指定此指针变量可以指向的变量的类型。例如,上面定义的、基类型为int的指针变量p,可以用来指向整型的变量,但不能指向浮点型变量。
2.指针变量占用的大小,跟数据类型无关,跟编译器有关。(32位:4字节;64位:8字节),如下代码:
#include <iostream>int main()
{int a = 10;int* p1 = &a;char c = 'max';char* p2 = &c;long long n = 10000;long long* p3 = &n;printf("%zu\n", sizeof(p1));printf("%zu\n", sizeof(p2));printf("%zu\n", sizeof(p3));
}
在这里使用64位编译器,运行结果如图:
再用32位编译器,运行结果如图:
3.给指针变量赋值的时候,不能把一个数值赋值给指针变量。
3.3 怎样引用指针变量
在引用指针变量时,可能有3种情况:
(1)给指针变量赋值。如
p=&a; //把a的地址赋给指针变量P
指针变量p的值是变量a的地址,p指向a。
(2)引用指针变量指向的变量。
如果已执行“p=&a;”,即指针变量p指向了整型变量a.,则
printf("%d,*p);
其作用是以整数形式输出指针变量p所指向的变量的值,即变量a的值.
如果有以下赋值语句:
*p = 1;
表示将整数1赋给p当前所指向的变量,如果p指向变量a,则相当于把1赋给a,即“a=1;”.
(3)引用指针变量的值。如:
printf("%o",p);
作用是以八进制数形式输出指针变量p的值,如果p指向了a,就是输出了a的地址,即&a。
四、取地址运算符 &
&
运算符用于获取变量的内存地址:
int a = 10;
int *p;
p = &a; // 将变量a的地址赋值给指针p,此时p指向a
通过&a,我们得到了变量a在内存中的地址,并把它赋给了指针p,这样p就指向了a。
五、解引用运算符 *(指针运算符或间接访问运算符)
*
运算符用于访问指针所指向的变量的值:
int a = 10;
int *p = &a;
printf("%d", *p); // 输出10,*p表示访问p所指向的变量的值
*p = 20; // 修改p所指向的变量的值
printf("%d", a); // 输出20,a的值已被修改
在这个例子中,*p访问了p所指向的a的值,并且我们还通过*p = 20修改了a的值。
六、指针的初始化
1.可以在声明指针的同时进行初始化:
int a = 10;
int *p = &a; // 正确,p指向a
2.需要注意的是,未初始化的指针称为野指针,使用野指针非常危险,可能导致程序崩溃。
3.可以将指针初始化为NULL,表示它不指向任何有效内存:
int *p = NULL; // 空指针
七、指针与函数
7.1 指针可以作为函数参数,实现函数对实参的修改(操作其他函数中的变量)。
函数的参数不仅可以是整型、浮点型、字符型等数据,还可以是指针类型。它的作用是将一个变量的地址传送到另一个函数中。
例如交换两个数的值:
我们先引入一个无法用函数完成交换的例子(后称代码1):
#include<stdio.h>
void swap(int num1, int num2);
int main() {//定义两个变量,要求交换变量中记录的值//注意 : 交换的代码写在一个新的函数swap中//1.定义两个变量int a = 10;int b = 20;// 2.调用swap函数printf("调用前:%d,%d", a, b);swap(a, b);printf("调用后:%d,%d", a, b);
return 0;
}
void swap(int num1, int num2) {//仅仅交换的是num1和num2里面记录的值int temp = num1;num1 = num2;num2 = temp;
}
结果如图:
这是因为,a和b的值是传给了函数中的num1与num2,交换的是num1与num2的值。
再来用指针(后称代码2):
#include<stdio.h>void swap(int* a, int* b) {int temp = *a;*a = *b;*b = temp;
}int main() {int x = 10, y = 20;printf("调用前:%d,%d\n", x, y);swap(&x, &y); // 传递x和y的地址// 现在x=20,y=10printf("调用后:%d,%d\n", x, y);return 0;
}
结果如图:
在这个swap函数中,我们通过指针参数接收到了x和y的地址,然后利用解引用运算符修改了它们的值。
7.1.1 如果想通过函数调用得到n个要改变的值,可以这样做:
- 在主调函数中设n个变量,用n个指针变量指向它们;
- 设计一个函数,有n个指针形参。在这个函数中改变这n个形参的值;
- 在主调函数中调用这个函数,在调用时将这n个指针变量作实参,将它们的值,也就是相关变量的地址传给该函数的形参;
- 在执行该函数的过程中,通过形参指针变量,改变它们所指向的n个变量的值;
- 主调函数中就可以使用这些改变了值的变量。
注意:
1.不能企图通过改变指针形参的值而使指针实参的值改变。
2.函数的调用可以(而且只可以)得到一个返回值(即函数值),而使用指针变量作参数,可以得到多个变化了的值。如果不用指针变量是难以做到这一点的。要善于利用指针法。
7.1.2 关于指针操作函数变量的疑问
1.为什么代码块2中用了指针后可以修改。
在 C 语言中,用指针能够修改函数外部变量的值,核心原因是指针传递的是变量的地址,而不是变量的副本。
具体来说:
(1)普通值传递的问题:
当你直接传递变量a和b给swap函数时,函数接收的是a和b的副本(num1和num2)。这就像你把文件复印了一份给别人,别人修改复印件不会影响你的原件。所以普通的swap函数只能交换副本,无法改变原始变量。
(2)指针传递的原理:
当你传递&a和&b(变量的地址)给指针参数*num1和*num2时:
指针num1保存了变量a的内存地址
指针num2保存了变量b的内存地址
通过*num1和*num2(解引用操作),可以直接访问和修改a和b所在的内存空间
简单说,指针就像给函数提供了一把 “钥匙”,让函数能够直接找到原始变量在内存中的位置并修改它,而不是只操作副本。这就是为什么用指针后能够真正交换a和b的值。
2.代码块1中直接传递变量a和b给swap函数时,函数接收的是a和b的副本(num1和num2),但是同理,在代码块2中用指针的话不也是传递函数指针的副本吗
指针作为参数传递时,函数也会接收指针的 “副本”。但关键区别在于:这个指针副本指向的是原始变量的内存地址。
我们用 “地址” 和 “内容” 的关系来类比,就能明白:
假设变量a=10存放在内存地址0x1000,b=20存放在0x2000。
1. 普通值传递(失败的交换)
调用swap(a, b)时,函数收到的是:
num1 = 10(a的副本,存放在新地址0x3000)
num2 = 20(b的副本,存放在新地址0x4000)
函数内部交换的是0x3000和0x4000里的内容,而原始变量a(0x1000)和b(0x2000)的地址和内容都没变。
就像:你告诉朋友 “我家有 10 个苹果”,朋友自己记成 “我有 10 个苹果”,他把自己的 10 改成20,跟你家的苹果数量无关。
2. 指针传递(成功的交换)
调用swap(&a, &b)时,函数收到的是指针的副本:
num1 = 0x1000(a的地址,这个指针副本存放在新地址0x5000)
num2 = 0x2000(b的地址,这个指针副本存放在新地址0x6000)
函数内部的*num1和*num2操作,是通过0x1000和0x2000这两个地址,直接找到原始变量a和b的内存空间。
交换的是0x1000和0x2000里的内容(即a和b本身的值)。
就像:你告诉朋友 “我家在 0x1000 号房”,朋友自己记下这个地址(副本),然后直接去你家(原始地址)把苹果换成了 20 个,你回家后自然能看到变化。
核心总结
普通传递:副本是 “值”,修改副本不影响原始值。
指针传递:副本是 “地址”,通过地址能直接操作原始值。
指针的副本虽然也是临时的,但它保存的 “地址信息” 能帮我们找到原始变量的家,这就是指针能实现修改的关键。
3.按照上面说的,这样运行swap前num1 = 0x1000,num2 = 0x2000;运行函数后,num1 = 0x2000,num2 = 0x1000;这个函数运行后num1和num2变量被销毁,不是还是不能完成交换吗
这个疑问很关键,我们需要明确一个核心点:swap 函数中交换的不是指针本身的值,而是指针所指向的内存空间中的值。
用之前的地址例子再详细拆解:
假设:
a=10 存放在 0x1000 地址
b=20 存放在 0x2000 地址
指针参数 num1 副本存着 0x1000(指向 a)
指针参数 num2 副本存着 0x2000(指向 b)
函数内部实际执行的操作:
void swap(int *num1, int *num2) {int temp = *num1; // 取出0x1000地址中的值(10)存到temp*num1 = *num2; // 把0x2000地址中的值(20)放到0x1000地址*num2 = temp; // 把temp中的值(10)放到0x2000地址
}
注意这里的关键:
我们操作的是 *num1 和 *num2(指针指向的地址里的内容),而不是 num1 和 num2 本身
执行后:
0x1000 地址中的值变成了 20(a 的值被修改)
0x2000 地址中的值变成了 10(b 的值被修改)
num1 仍然是 0x1000,num2 仍然是 0x2000(我们根本没有交换指针本身)
为什么函数结束后修改仍然有效?
虽然函数结束后,指针副本 num1 和 num2 会被销毁
但它们已经完成了使命:通过地址找到 a 和 b 的内存空间,并修改了里面的值
a 和 b 是在 main 函数中定义的变量,它们的生命周期直到程序结束,所以修改后的值会保留
简单说:指针副本就像一个临时的 "信使",它带着地址找到原始变量,帮我们完成修改后就消失了,但原始变量的变化已经被保留下来了。
7.2 函数可以返回指针,比如下面这个获取两个数中较大数的指针的函数:
int *get_max(int *a, int *b) {if (*a > *b)return a;elsereturn b;
}
细节:
函数中变量的生命周期跟函数相关,函数结束了,变量也会消失。
此时在其他函数中,就无法通过指针使用了。
如果不想函数中的变量被回收,可以在变量前加static关键字。
八、指针与数组
8.1 数组元素的指针
一个变量有地址,一个数组包含若干元素,每个数组元素都在内存中占用存储单元,它们都有相应的地址。指针变量既然可以指向变量,当然也可以指向数组元素(把某一元素的地址放到一个指针变量中)。所谓数组元素的指针就是数组元素的地址。
可以用一个指针变量指向一个数组元素。例如:
int a[10]={1,3,5,7,9,11,13,15,17,19}; //定义a为包含10个整型数据的数组
int *p; //定义P为指向整型变量的指针变量
p=&.a[0]; //把a[0]元素的地址赋给指针变量p
以上是使指针变量p指向a数组的第0号元素,见下图,引用数组元素可以用下标法(如a[3]),也可以用指针法,即通过指向数组元素的指针找到所需的元素。使用指针法能使目标程序质量高(占内存少,运行速度快)。
在C语言中,数组名(不包括形参数组名)代表数组中首元素(即序号为0的元素)的地址。因此,下面两个语句等价:
p=&a[0]; //p的值是a[0]的地址
p=a; //p的值是数组a首元素(即a[0])的地址
注意:程序中的数组名不代表整个数组,只代表数组首元素的地址。上述“p=a;”的作用是“把a数组的首元素的地址赋给指针变量p”,而不是“把数组a各元素的值赋给p”。
在定义指针变量时可以对它初始化,如:
int *p=&a[0];
它等效于下面两行:
int * p;
p=&a[0]; //不应写成*p=&.a[0];
当然定义时也可以写成
int *p=a;
它的作用是将a数组首元素(即a[0])的地址赋给指针变量p(而不是赋给*p)。
8.2 在引用数组元素时指针的运算
我们知道,常见运算有加减乘除,但是对指针进行运算又是什么意思呢?下面我们探讨有意义的运算——加和减。
在指针已指向一个数组元素时,可以对指针进行以下运算:
(1)加一个整数(用+或+=),如p+1;
(2)减一个整数(用-或-=),如p-1;
(3)自加运算,如p++,++p;
(4)自减运算,如p--,--p。
(5)两个指针相减,如p1-p2(只有p1和p2都指向同一数组中的元素时才有意义)
如果指针变量p已指向数组中的一个元素,则p+1指向同一数组中的下一个元素,p-1指向同一数组中的上一个元素。
注意:执行p+1时并不是将p的值(地址)简单地加1,而是加上一个数组元素所占用的字节数。
说明:[ ] 实际上是变址运算符,即将a[i]按a+i计算地址,然后找出此地址单元中的值。
如果指针变量p1和p2都指向同一数组中的元素,如执行p2-p1,结果是p2-p1的值(两个地址之差)除以数组元素的长度。
注意:两个地址不能相加,如p1十p2是无实际意义的。
8.3通过指针引用数组元素
根据以上叙述,引用一个数组元素,可以用下面两种方法:
(1)下标法,如a[i]形式;
(2)指针法,如*(a+i)或*(p+i)。其中a是数组名,p是指向数组元素的指针变量,其初值 p=a。
数组名本质上是数组首元素的地址:
#include<stdio.h>
int main() {int arr[5] = { 1, 2, 3, 4, 5 };int* p = arr; // 等价于 p = &arr[0]int len = sizeof(arr) / sizeof(int);// 通过指针访问数组元素printf("%d\n", *p); // 输出1,访问arr[0]printf("%d\n", arr[0]); // 输出1,访问arr[0]printf("%d\n", *(p + 1)); // 输出2,访问arr[1]printf("%d\n", arr[1]); // 输出2,访问arr[1]printf("%d\n", *(p + 2)); // 输出3,访问arr[2]printf("%d\n", arr[2]); // 输出3,访问arr[2]printf("-------------------------------\n");//利用循环和指针遍历数组获取里面的每一个元素for (int i = 0; i < len; i++){printf("%d\n", *p++);}
}
运行结果如图:
九、二级指针和多级指针
指向指针的指针,也就是二级指针,它用于存储指针变量的地址:
int a = 10;
int *p = &a; // p是一级指针,指向a
int **pp = &p; // pp是二级指针,指向p// 访问a的值
printf("%d", a); // 直接访问
printf("%d", *p); // 通过一级指针访问
printf("%d", **pp); // 通过二级指针访问
通过二级指针pp,我们先解引用得到一级指针p,再解引用p就得到了a的值。
十、指针与动态内存分配
C 语言提供了动态内存分配函数,这些函数需要使用指针来操作,常见的有malloc函数,使用示例如下:
#include <stdlib.h>int main() {int *p = (int*)malloc(5 * sizeof(int)); // 分配可以存储5个int的内存if (p == NULL) {// 内存分配失败处理}// 使用分配的内存for (int i = 0; i < 5; i++) {p[i] = i;}free(p); // 释放动态分配的内存p = NULL; // 避免野指针return 0;
}
在使用完动态分配的内存后,一定要用free函数释放,并且将指针置为NULL,避免成为野指针。
十一、指针使用注意事项
- 避免使用未初始化的指针(野指针),野指针可能指向任意内存地址,操作它会导致不可预测的错误。
- 不要访问已经释放的内存,释放后的内存可能被系统重新分配,访问它会干扰其他数据。
- 避免指针越界访问,比如在数组中,指针超出数组的范围进行访问,会破坏其他内存的数据。
- 指针类型必须与所指向变量的类型匹配,否则可能会导致数据解析错误。
- 使用完动态分配的内存后要及时释放,否则会造成内存泄漏。
- 不要返回指向函数内部局部变量的指针,函数调用结束后,局部变量会被销毁,其内存地址不再有效。
十二、总结
指针是 C 语言的灵魂,它为我们提供了直接操作内存的能力,让程序更加高效和灵活。但同时,指针的使用也需要我们格外小心,遵循相关的注意事项,避免出现内存错误。希望通过本次学习,大家能对指针有更清晰的认识,多进行编程练习,逐步掌握指针的使用。