计算的目的不在于数据,而在于洞察事物。
The purpose of computing is insight,not numbers。
——查理德·哈明,图灵奖得主
引言
指针是C语言中最富特色的内容,是C语言的精髓所在。今天我们来学习一下指针的相关内容。
指针是C语言中一种强大的数据类型,它存储变量的地址,允许直接访问内存中的数据。理解指针的基本概念对于编写高效、灵活的C语言程序至关重要。
地址与指针
1.概念
计算机的数据必须存储在内存里,为了正确地访问这些数据,必须为每个数据都编上号码,就像信箱邮编、身份证号一样,每个编号是唯一的,根据编号可以准确地找到某个数据。内存单元中的编号叫做地址,而存储这些地址的变量被称为指针。
2.取地址操作符(&)和解引用操作符(*)
当谈到指针时,取地址操作符(&)和解引用操作符(*)是两个非常重要的概念。它们与指针紧密相关,并且在指针的使用中起着关键作用。
2.1 取地址操作符(&)
在C语言中创建变量其实就是向内存申请空间,例如这样:
上面的代码创建了整型变量x,向内存申请了4个字节,用于存放整型变量x的值
那么我们要如何得到x的地址呢?这个时候就需要学习一下取地址操作符(&)
取得x的地址:
#include<stdio.h>
int main()
{
int x=1;
printf("%p\n",&x);
return 0;
}
2.2 解引用操作符(*)
解引用操作符*用于访问指针所指向的变量的值。通过解引用操作符,我们可以间接访问和修改指针所指向的内存中的数据。
#include<stdio.h>
int main()
{
int x=10;
int* p=&x;
*p=0;
return 0;
}
在这段代码中,*p的意思就是通过p中存放的地址,找到指向的空间,*p就是变量x。
*p=0,这个操作就是将x改为0。
2.3 直接访问与间接访问
(1)直接访问
用变量名直接得到要操作的内存单元的内容,即直接访问。
(2)间接访问——通过解引用操作符*实现
通过指向变量的指针变量得到要操作的内存单元的内容,即间接访问。
以下是解引用操作符的使用:
int x=10;
int* p=&x;
int value=*p; //将指针 p 所指向的变量的值赋给变量 value
3.指针变量
3.1 指针变量的定义
在C语言中,指针是一种数据类型,用于存储变量的内存地址。通过指针,可以直接访问或修改存储在该地址上的数据。
指针变量的定义遵循以下格式:
type *pointer_variable;
type 表示指针变量指向的数据类型,pointer_variable
是指针变量的名称,*
表示该变量是一个指针。
3.2 指针变量的声明
在使用指针变量之前,需要先声明它。声明指针变量时,需要指定指针所指向的数据类型。
以下是一些示例:
int* intPointer; // 整型指针
float* floatPointer; // 浮点型指针
char* charPointer; // 字符型指针
3.3 指针变量的初始化和赋值
和普通变量一样,定义后的指针变量里存储的是不确定的值,该值所指向的内存空间未必是允许程序正常访问的空间,因此,指针变量定义之后应当及时给予一个合法的值,避免在编程中使用具有随机值的指针变量
指针变量获得值的方法可以有以下几种方式:
(1)指针变量可以通过初始化或者赋值操作获得地址值
int x=10;
int* p=&x; /*初始化方法*/
(2)指针变量可以在初始化或者赋值后再改变它所指向的值
int main()
{
int x=10;
int* p=&x;
*p=0;
return 0;
}
(3)基类型相同的指针变量也能相互赋值
int main()
{
int x = 10;
int* p = &x;
int* q = 0;
q = p; // 将p的值赋给q
printf("%d %d", *p, *q); // 输出指针所指向的值
return 0;
}
输出结果为:10 10
4.void* 指针
void*
指针是C语言中的一种特殊类型的指针,被称为“无类型指针”或“泛型指针”。它是一种特殊的指针类型,可以指向任何数据类型的内存地址,因为它不关心所指向数据的类型。
在C语言中,通常情况下,指针必须指向特定类型的数据。例如,一个int*
指针指向一个整数,一个char*
指针指向一个字符等等。但是,当我们需要处理不同类型的数据,或者需要编写能够处理任意数据类型的代码时,void*
指针就发挥了作用。
例如我们可以这样
void* p1;
int* p2;
p1=p2
这种方式是被允许的,但是我们不能将void*赋值给其他指针类型,也不能对其解引用
void* p1;
int* p2;
p2=p1 //这是错误的赋值方法
*p1 //不能对void*进行解引用操作
5.NULL
在C和C++中,NULL
被定义为一个零值或者空地址,通常表示指针不指向任何有效的对象或函数。
如:
int* p=NULL; //将指针初始化为空指针
6.指针变量的大小
指针变量的大小通常取决于计算机体系结构和编译器的实现
现在常见的计算机分为32位机器和64位机器
32位平台下地址是32个bit位(即4个字节)
64位平台下地址是64个bit位(即8个字节)
#include<stdio.h>
int main()
{
printf("%zd ",sizeof(char *));
printf("%zd ",sizeof(short *));
printf("%zd ",sizeof(int *));
printf("%zd ",sizeof(double *));
return 0;
}
输出结果:
x86环境:4 4 4 4
x64环境:8 8 8 8
指针的基本运算
1.指针+(-)整数
指针的加减法运算是在C语言中对指针进行移动的一种重要方式,它们允许我们在内存中跳转到不同的位置,以便访问数据或进行其他操作。
我们先来看一段代码,观察一下地址的变化
#include <stdio.h>
int main()
{
int n = 10;
char* p1 = (char*)&n;
int* p2 = &n;
printf("%p\n", &n);
printf("%p\n", p1);
printf("%p\n", p1 + 1);
printf("%p\n", p2);
printf("%p\n", p2 + 1);
return 0;
}
代码运行结果如下:
char* 类型的指针变量+1跳过1个字节, int* 类型的指针变量+1跳过了4个字节。
结论:指针的类型决定了指针在加(减)整数之后移动的距离。
PS:每次重新运行程序,系统都会重新分配内存,所以每次的结果都会不同,但是规律是一致的
举个例子:我们知道数组在内存中是连续存放的,只要知道第一个元素的地址,我们就可以指针加减整数的方法找到数组中的所有元素
#include <stdio.h>
int main()
{
int arr[10] = { 0,1,2,3,4,5,6,7,8,9 };
int sz = sizeof(arr) / sizeof(arr[0]);
int* p = &arr[0];
for (int i = 0; i < sz; i++)
{
printf("%d ", *(p + i));
}
return 0;
}
输出结果为:
0 1 2 3 4 5 6 7 8 9
2.指针-指针
指针-指针其结果通常是一个整数,表示两个指针之间的距离,也可以说是相差的元素个数。
根据这一效果,我们或许可以自己模拟实现库函数strlen
代码如下:
#include <stdio.h>
int my_strlen(char* s)
{
char* p = s;
while (*p != '\0')
{
p++;
}
return p - s;
}
int main()
{
int ret = my_strlen("abcd");
printf("%d\n", ret);
return 0;
}
指针变量是用于存储地址值的变量,两个指针变量即使类型相同,也不能进行加法运算,因为两个地址值相加的结果毫无意义
3.指针的关系运算
指针变量的本质是地址,而地址是十六进制的整数,因此指针变量也可以比较大小
来看一段代码:
#include <stdio.h>
int main()
{
int arr[10] = { 0,1,2,3,4,5,6,7,8,9 };
int* p = &arr[0];
int sz = sizeof(arr) / sizeof(arr[0]);
int i = 0;
while (p < arr + sz) //指针的大小比较
{
printf("%d ", *p);
p++;
}
return 0;
}
const的使用
const
是C语言中的关键字,用于声明常量或者指示变量不可修改。
声明常量
使用 const
可以声明常量,使得变量的值在程序执行期间不可修改。
const int a=10;
被const修饰后的变量就相当于一个常量,无法改变。
修饰指针
const
可以用于修饰指针,表示指针所指向的数据不可通过该指针进行修改。
分为两种情况:
1.const
修饰指针本身,表示指针不能指向其他变量,但指向的数据可以修改。
int x = 5;
int *const ptr = &x; //const修饰指针,指针本身不可变
*ptr = 10; //合法,可以修改指向的数据
//ptr = &y; // 非法,指针本身不可变
2.const
修饰指针所指向的数据,表示指针可以指向其他变量,但指向的数据不可修改。
int x = 5;
const int *ptr = &x; // 指向const的指针,指向的数据不可变
// *ptr = 10; // 非法,指向的数据不可变
ptr = &y; // 合法,指针本身可变
总结:const修饰指针变量的时候
1.const如果放在*左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变,但是指针变量本身的内容可以改变。
2.const如果放在*右边,修饰的是指针变量本身,保证了指针变量的内容不能修改,但是指针指向的内容,可以通过指针改变
野指针
野指针是指指向未知内存位置的指针,通常是未初始化或者已经释放的指针。野指针可能会引发程序运行时的不确定行为,因为它们指向的内存位置可能包含任意数据,或者在内存管理上可能已经被重用或释放。
野指针成因
1.指针未初始化
如果指针变量没有被初始化,它将包含一个随机的内存地址,这可能会指向未知的内存位置。
#include <stdio.h>
int main()
{
int* p; //局部变量指针未初始化,默认为随机值
*p = 20;
return 0;
}
2.指针越界访问
当指针超出了其所指向内存块的范围,或者指向了一个不存在的内存地址时,它就成为了野指针。
#include <stdio.h>
int main()
{
int arr[10] = { 0 };
int i = 0;
for (i = 0; i < 11; i++) //当指针指向的范围超出arr的范围时,p就是野指针
{
printf("%d ", *(arr + i));
}
return 0;
}
3.指针指向的空间释放
#include <stdio.h>
int* test()
{
int n = 10;
return &n;
}
int main()
{
int* p = test();
printf("%d\n", *p);
return 0;
}
这段代码存在一个严重的问题:在函数 test() 中,它返回了一个局部变量 n 的地址。当函数 test() 执行完毕后,n 将被销毁,因此返回的指针指向的内存将不再有效。在 main() 函数中,当你尝试访问通过 test() 返回的指针所指向的内存时,实际上是在访问已经被销毁的内存,这个时候,指针就变成了野指针,指向了一个无效的地址。
解决方法
1.指针初始化
在声明指针变量时,始终确保初始化为 NULL 或者一个有效的地址,避免出现未初始化的指针。
NULL是C语言定义的一个标识符常量,值是0,0也是地址,该地址无法使用,读写该地址会报错
int* p=NULL;
2.避免指针越界访问
确保指针不会越界访问数组或其他数据结构的边界,以防止指针成为野指针。
3.当指针变量不再使用时,应当及时置NULL,指针使用之前应检查有效性
当指针变量指向⼀块区域的时候,我们可以通过指针访问该区域,后期不再使⽤这个指针访问空间的
时候,我们可以把该指针置为NULL。因为约定俗成的⼀个规则就是:只要是NULL指针就不去访问,
同时使⽤指针之前可以判断指针是否为NULL。
4.避免返回局部变量的地址
临时变量出了作用域就会销毁,系统会回收该空间,我们应当避免指针在超出其作用域后继续被引用。
assert断言
assert断言是一个调试程序常用的宏,定义在<assert.h>
头文件中。它的主要作用是判断程序中是否出现了非法的数据。
举个例子,看一下这个代码:
#include <stdio.h>
#include <assert.h>
int main() {
int x = 5;
int y = 0;
// 检查y是否不为0,如果不是,则程序终止
assert(y != 0);
// 如果上面的assert不终止程序,这里会尝试除以y
int z = x / y;
printf("Result is %d\n", z);
return 0;
}
在程序运行时,assert会计算括号内的表达式的值。如果表达式的值为false(0),程序会报告错误并终止运行,以避免导致严重后果,并便于查找错误。如果表达式的值不为0,则程序会继续执行后面的语句。
assert常常用于检查空指针问题,以防止程序因为空指针的问题而出错。
int *p=NULL;
assert(p!=NULL); //空指针是0,0为假,就会报错
assert() 的使用对程序员是非常友好的,使用 assert() 有几个好处:它不仅能自动标识文件和
出问题的行号,还有⼀种无需更改代码就能开启或关闭 assert() 的机制。如果已经确认程序没有问
题,不需要再做断言,就在 #include <assert.h> 语句的前面,定义⼀个宏 NDEBUG 。
#define NDEBUG
#include<assert.h>
需要注意的是:assert断言在Debug版本中存在,但在编译的Release版本中被忽略。
指针的传值调用和传址调用
我们来看看这段代码:
#include <stdio.h>
void Swap(int x, int y)
{
int t = x;
x = y;
y = t;
}
int main()
{
int a = 10;
int b = 20;
printf("交换前:%d %d\n", a, b);
Swap(a, b);
printf("交换后:%d %d", a, b);
}
结果为:交换前:10 20
交换后:10 20
为什么并不能实现我们的预期:实现两个数值的交换呢?
在这段代码中,Swap使用的是传值调用。函数接收的是参数的副本,而不是参数本身的引用或地址。因此,当在Swap函数内部交换x和y的值时,实际上只是交换了副本的值,并不会影响到main函数中的变量a和b
实参传递给实参的时候,形参会单独创建一份临时空间来接收实参,对形参的修改不会影响实参
如果要实现交换功能,我们需要使用传址调用
传址调用,即通过指针传递参数的地址,这样就可以在函数内部直接修改原始变量的值。
修改后的代码如下所示:
#include <stdio.h>
void Swap(int *x, int *y)
{
int t = *x;
*x = *y;
*y = t;
}
int main()
{
int a = 10;
int b = 20;
printf("交换前:%d %d\n", a, b);
Swap(&a, &b); // 传递变量的地址
printf("交换后:%d %d", a, b);
return 0;
}
输出结果为:
交换前:10 20
交换后:20 10
传址调用,可以让函数和主调函数之间建立真正的联系,在函数内部可以修改主调函数中的变量
如果函数中只是需要主调函数中的变量值来实现计算,就可以使用传值变量
如果函数内部要修改主调函数中的变量的值,就需要传址调用
结束语
指针的内容好多,我预计会写三篇。
希望看到这篇文章的家人们能点点关注,赞赞和收藏!!!
感谢!!!