C语言——指针(上)

本文详细介绍了C语言中指针的基础知识,包括地址与指针的概念、取地址和解引用操作、指针变量的定义与初始化、void*指针、NULL值、指针运算、const修饰、野指针及其成因、assert断言以及传值调用与传址调用的区别。
摘要由CSDN通过智能技术生成

计算的目的不在于数据,而在于洞察事物。

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

传址调用,可以让函数和主调函数之间建立真正的联系,在函数内部可以修改主调函数中的变量

如果函数中只是需要主调函数中的变量值来实现计算,就可以使用传值变量

如果函数内部要修改主调函数中的变量的值,就需要传址调用

结束语

指针的内容好多,我预计会写三篇。

希望看到这篇文章的家人们能点点关注赞赞收藏!!!

感谢!!!

  • 53
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值