写点什么

掌握 C 语言指针,轻松解锁代码高效性与灵活性

  • 2024-02-26
    福建
  • 本文字数:4342 字

    阅读完需:约 14 分钟

1. 指针与地址

1.1 概念


我们都知道计算机的数据必须存储在内存里,为了正确地访问这些数据,必须为每个数据都编上号码,就像门牌号、身份证号一样,每个编号是唯一的,根据编号可以准确地找到某个数据。而这些编号我们就将其称为地址或者指针


1.2 指针变量


数据在内存中的地址称为指针,如果一个变量存储了一份数据的指针(地址),我们就称它为指针变量

那我们如何使用指针变量呢?


  1. datatype *name;

*表示这是一个指针变量,datatype表示该指针变量所指向的数据的类型


例如:


int* p1;//指向一个整型的指针char* p2;//指向一个字符的指针float* p3;//指向一个单精度浮点数的指针double* p4;//指向一个双精度浮点数的指针
复制代码


1.3 &和*


我们早在学习 scanf 时候就用过取地址符 &,它是将某个变量的地址取出来,而解引用*的意思就是通过某个地址找到该地址存储的变量。可能解释起来比较抽象,我们可以通过一个不恰当的例子形象说明一下。



首先我们可以得到如下几个关系:


	int a = 1;//第一个客户,&a为0x00000001	int b = 2;//第二个客户,&b为0x00000002	int c = 3;//第三个客户,&c为0x00000003
复制代码


然后我们可以通过指针变量把他们地址存储进去


int* pa = &a;//把a的地址存进去int* pb = &b;//把b的地址存进去int* pc = &c;//把c的地址存进去
复制代码


在酒店中,我们可以通过门牌号准确找到每个客户。同理,我们也可以通过每个地址准确找到每个变量。


printf("a=%db=%dc=%d", *pa, *pb, *pc);//通过*解引用地址找到对应的值
复制代码


输出结果 a=1 b=2 c=3


并且我们可以通过指针变量进行赋值。

*pa = 4;*pb = 5;*pc = 6;printf("a=%d b=%d c=%d\n", *pa, *pb, *pc);
复制代码


输出结果:a=4 b=5 c=6


1.4 void*指针和 NULL


(1)void*是一种特殊的指针类型,它可以指向任意类型的数据,就是说可以用任意类型的指针对 void 指针赋值。


void*p1;int*p2;p1=p2;//这是被允许的
复制代码


  • 但是却不能把 void*指针赋值给任意指针类型,也不能直接对其解引用


void*p1;int *p2;p2=p1;//不能这样赋值*p1//不能直接对void*解引用
复制代码


(2)NULL 是 C 语⾔中定义的⼀个标识符常量,值是 0,0 也是地址,这个地址是⽆法使⽤的,读写该地址会报错


int*p=NULL;//初始化指针
复制代码


1.5 指针变量的大小


我们知道,现在常见的计算机分为 32 位机器64 位机器。32 位机器假设有 32 根地址总线,每根地址线出来的电信号转换成数字信号后是 1 或者 0,那我们把 32 根地址线产⽣的 2 进制序列当做⼀个地址,那么⼀个地址就是 32 个 bit 位,需要 4 个字节才能存储。同理,64 位机器需要 8 个字节才能存储。

我们可以通过以下代码来验证一下。


int main(){	printf("%zd ", sizeof(char*));	printf("%zd ", sizeof(short*));	printf("%zd ", sizeof(int*));	printf("%zd ", sizeof(double*));	return 0;}
复制代码


输出结果:

32 位机器:4 4 4 4

64 位机器:8 8 8 8


2. 指针的基本运算


2.1 指针+-整数


我们先观察一下如下代码的地址变化


#include <stdio.h>int main(){	int n = 10;	char* p1 = (char*)&n;//将int*强转为char*	int* p2 = &n;	printf("%p\n", &n);	printf("%p\n", p1);	printf("%p\n", p1 + 1);//p1向后移动一位	printf("%p\n", p2);	printf("%p\n", p2 + 1);//p2向后移动一位	return 0;}
复制代码


输出 :

&n=005DF8D4p1=005DF8D4p1+1=005DF8D5p2=005DF8D4p2+1=005DF8D8


我们可以看出, char* 类型的指针变量+1 跳过 1 个字节, int* 类型的指针变量+1 跳过了 4 个字节。由此我们得出结论:指针的类型决定了指针向前或者向后⾛⼀步有多⼤(距离)。


  • 因为每次代码运行时,系统都会重新分配内存,所以输出结果每次都不会一样,但是规律是一样的

我们知道数组在内存中是连续存储的(地址由低到高),所以我们只需要只要首元素的地址就能找到数组所有元素的地址,而一维数组的数组名恰恰就是我们首元素的地址。


 假设有数组int arr[10]={1,2,3,4,5,6,7,8,9,10} 
复制代码



那我们如何通过指针访问每个元素呢?


代码参考如下:


#include <stdio.h>int main(){	int arr[] = { 1,2,3,4,5,6,7,8,9,10 };	int* p = &arr[0];//&arr[0]=arr	int i = 0;	int sz = sizeof(arr) / sizeof(arr[0]);//计算数组元素个数	for (i = 0; i < sz; i++)	{		printf("%d ", *(p + i));//因为数组元素连续存储,所以可以通过+-整数找到之后元素	}	return 0;}
复制代码


输出结果:1 2 3 4 5 6 7 8 9 10


2.2 指针-指针


指针-指针其实是指在同一空间内,两个指针之间的元素个数


知道这点之后,我们可不可以自己实现一个字符串库函数 strlen()呢?


思路如下:


思路:首先定义两个指针 p1,p2,让两个指针指向首元素,然后让一个指针 p2 循环++,直到指向‘\0’就停止,最后返回 p2-p1,就能得到字符串的长度



代码如下:


int my_strlen(char* p1){	char* p2 = p1;//使两个指针都指向首元素	while (*p2)	{		p2++;	}	return p2 - p1;//返回两指针直接的元素的个数就是其长度}int main(){	char arr[] = "abcdef";	int len = my_strlen(arr);//计算arr字符串的长度	printf("%d\n", len);	return 0;}
复制代码


2.3 指针的关系运算


我们知道了指针变量本质是存放的地址,而地址本质就是十六进制的整数,所以指针变量也是可以比较大小的

代码示例:


#include <stdio.h>int main(){	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };	int* p = &arr[0];	int i = 0;	int sz = sizeof(arr) / sizeof(arr[0]);	while (p < arr + sz) //指针的⼤⼩⽐较	{		printf("%d ", *p);//打印数组每个元素		p++;	}	return 0;}
复制代码


3. const 修饰


我们知道变量是可以改变的,但是在有些场景下,我们不希望变量改变,那我们该怎么办呢?这就是我们接下来要讲的 const 的作用啦。


3.1 const 修饰变量


简单来说,经过 const 修饰的变量,可以当做一个常量,而常量是不能改变的。


	int a = 1;//a可修改的	const int b = 2;	b=3;//b不可修改的
复制代码


但是可以通过指针间接修改.

代码如下:


int main(){	const int b = 2;	int* p = &b;	*p = 3;//通过指针间接修改	return 0;}
复制代码


3.2 const 修饰指针我们知道 const 的作用后,就可以看看下面几段代码。


int a = 10;const int* p = &a;*p = 20;//是否可以p = p + 1;//是否可以
复制代码


通过测试我们发现,*p 无法改变成 20,但是 p 可以改变成 p+1.


那如果把 const 调换一下位置,又会出现什么情况呢~


int a = 10;int* const p = &a;*p = 20;//是否可以p = p + 1;//是否可以
复制代码


再次测试之后我们发现,*p 可以被赋值为 20,但是 p 不能赋值为 p+1 了

通过上述测试,我们大致可以总结出两个结论。


• const 如果放在的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变。但是指针变量本⾝的内容可变。• const 如果放在*的右边,修饰的是指针变量本⾝,保证了指针变量的内容不能修改,但是指针指向的内容,可以通过指针改变。


4. assert 断言


assert 是一个宏,它的头文件为<assert.h>,⽤于在运⾏时确保程序符合指定条件,如果不符合,就报错终⽌运⾏。这个宏常常被称为“断⾔”。


举一个简单的例子:


assert(a>0);
复制代码


  1. 如果 a 的确大于 0,assert 判断为真,就会通过。

  2. 如果 a 不大于 0,assert 判断为假,就会报错。


所以 assert 常常用于检查空指针问题,以防止程序因为空指针的问题而出错。


int *p=NULL;assert(p);//空指针是0,0为假,就会报错
复制代码


5. 传值调用与传址调用


5.1 传值调用


我们前面学习函数时候,遇到过这样一段代码。


#include<stdio.h>void swap(int x, int y)//返回类型为void表示不返回值{    int temp = 0;//定义一个临时变量    temp = x;//把x的值赋给temp    x = y;//把y的值赋给x    y = temp;//把temp的值赋给y,完成交换操作}int main(){    int a = 0;    int b = 0;    scanf("%d %d", &a, &b);    printf("交换前:a=%d,b=%d\n", a, b);    swap(a, b);//交换函数    printf("交换后:a=%d,b=%d\n", a, b);    return 0;}
复制代码


输入:3 5

输出:交换后 a=3 ,b=5


为什么两个值并没有交换呢,这是因为形参只是实参的一份临时拷贝,对形参改变,根本不会改变实参。如果忘记的同学可以再去温习一下


5.2 传址调用


那我们想在函数中改变实参的值,那又该如何改变呢?

其实很简单,我们学了指针,知道可以通过地址间接访问该变量的值,所以我们只需要把地址传给函数,在函数中通过地址访问实参,并进行交换。

代码如下:


#include<stdio.h>void swap(int*x, int*y)//通过指针变量接受地址{    int temp = 0;//定义一个临时变量    temp = *x;//把*x的值赋给temp    *x = *y;//把*y的值赋给*x    *y = temp;//把temp的值赋给*y,完成交换操作}int main(){    int a = 0;    int b = 0;    scanf("%d %d", &a, &b);    printf("交换前:a=%d,b=%d\n", a, b);    swap(&a, &b);//将地址传给函数    printf("交换后:a=%d,b=%d\n", a, b);    return 0;}
复制代码



6. 野指针

概念:野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)

6.1 野指针成因

(1)指针未初始化


#include <stdio.h>int main(){	int* p; //局部变量指针未初始化,默认为随机值	*p = 20;	return 0;}
复制代码


因为 p 是随机值,所以对 p 解引用,系统无法通过 p 的地址找到对应的空间,所以出错造成野指针

(2)数组越界访问


#include <stdio.h>int main(){	int arr[10] = { 0 };	int i = 0;	for (i = 0; i < 11; i++)	{		//数组下标是0到9		printf("%d ", *(arr + i));	}	return 0;}
复制代码



  • 一般出现这种较大的随机值,一般都是数组越界访问

(3)指针指向空间释放


#include <stdio.h>int* test(){	int n = 10;	return &n;//返回n的地址}int main(){	int* p = test();//用p接受n的地址	printf("%d\n", *p);//打印出n的值	return 0;}
复制代码


这段代码乍一看,好像并没有什么问题,但是大家在学习函数的时候知道,在函数中定义的变量是临时变量,一旦出了作用域就会销毁。

一旦销毁,系统就无法访问该空间,而通过指针我们还可以访问该空间,这就造成了冲突,所以出错,造成野指针。

6.2 解决方法

(1) 初始化


NULL 是 C 语⾔中定义的⼀个标识符常量,值是 0,0 也是地址,这个地址是⽆法使⽤的,读写该地址会报错。如下是 NULL 在编译器中的定义:


ifdef __cplusplus

define NULL 0

else

define NULL ((void *)0)

endif


#include <stdio.h>int main(){	int* p=NULL;//用空指针初始化,让其有指向位置	//*p = 20;NULL地址不能读写	return 0;}
复制代码


(2) 小心越界访问

我们在使用数组时候,一定要对数组的元素个数有一个清晰的把控,不然就很容易出现越界访问的情况。


(3) 不能返回临时变量的地址

临时变量出了作用域就会销毁,系统会回收该空间,所以我们要尽量避免指针指向已经销毁的空间,尤其在函数中,不能返回临时变量的地址。


文章转载自:Betty’sSweet

原文链接:https://www.cnblogs.com/bett/p/18032316

体验地址:http://www.jnpfsoft.com/?from=001

用户头像

还未添加个人签名 2023-06-19 加入

还未添加个人简介

评论

发布
暂无评论
掌握C语言指针,轻松解锁代码高效性与灵活性_编程语言_快乐非自愿限量之名_InfoQ写作社区