深入探寻指针奥秘(0基础教学)

引子------谈及指针这个知识点是不是会感到“不寒而栗”,没关系从这篇文章开始我们来一步步的揭开指针的神秘面纱。

一、指针到底是什么

那么指针到底为何物呢,我们来回想一个生活场景,我们在新生开学找宿舍的时候是通过什么东西来确认这是自己的房间-----门牌号。门牌号就是每个房间特有的编码。

对照这个例子,我们在计算机CPU处理数据的时候,我们需要的数据是从内存中读取的,处理后的数据会放回到内存中,将内存划分为一个个的内存单元(每个内存单元大小为一个字节),每个内存单元就相当于一个学生宿舍,一个字节空间中可以存放8个比特位(好比宿舍的八人间),每个内存单元也会有一个编号(我们把这个编号叫做地址,而地址在C语言中有一个特别的名字叫指针,所以内存单元的编号==地址==指针。

二、指针变量和地址

我们已经大致了解了内存与地址之间的关系,我们回到代码中

#include<stdio.h>
int main()
{
     int a = 5;
     return 0;
}

这是一段非常简单的代码,我们在创建变量a的时候就是在向地址申请空间,例如这个变量a,内存中申请了4个字节,用来存放5,每个字节都有地址下图中4个字节的地址分别为:

  1. 0x000000362D0FFB74  05 
  2. 0x000000362D0FFB75  00 
  3. 0x000000362D0FFB76  00 
  4. 0x000000362D0FFB77  00 

有细心的小伙伴肯定留意到了&这个符号,可能有些人并不陌生这个符号是取地址操作符,内存中的存储是从低到高连续存储的,所以只要找到了第一个字节地址,就能访问到4个字节的数据啦。

2.1 指针变量和解引用操作符(*)

2.1.1指针变量

我们通过&取地址操作符拿到的地址是一个数值,如上图:0x000000362D0FFB74,地址这个数值存放在哪里呢?----指针变量中

#include<stdio.h>
int main()
{
    int a = 5;
    int* pa = &a;

    return 0;
}

如上图中,我们将a的地址放在指针变量pa中,指针变量也是变量,用来存放地址。

这里我们着重来解释一下指针类型 pa前边写着 int*,这个*代表着pa为指针变量,int则表示pa指向的是int类型的对象。

2.1.2 解引用操作符(*)

我们将地址保存起来,之后要如何使用呢。在这个过程中我们就必不可少的要了解解引用操作符(*)

#include<stdio.h>
int main()
{
    int a = 5;
    int* pa = &a;
    *pa = 0;
    return 0;
}

在这段代码中*pa 的意思是通过pa中存放的地址,找到指向的空间(其实*pa就是a变量啦),所以*pa = 0;的意思就是将a改成了0。这不纯多余操作嘛?为啥不直接a=0;这样的编写方式能让代码更具灵活性,在我们的后续学习中就能越来越感知到指针的优越啦。

2.2指针变量的大小

我们前面提到我们创建a变量向空间申请了4个字节,我们的*pa指针变量又存放着a的地址,那是不是我们指针变量的大小也应该要4个字节的空间才行呢!

#include<stdio.h>
int main()
{
	printf("%zd\n", sizeof(char*));
	printf("%zd\n", sizeof(int*));
	printf("%zd\n", sizeof(short*));
	printf("%zd\n", sizeof(double*));
	return 0;
}

我们在32位(x86)和64位(x64)中得到不同的结果,在32位平台下地址是32个bit位(即4个字节)

在64位平台下地址是64个bit位(即8个字节),指针变量的大小取决于地址的大小,与类型无关。只要在相同的平台下,大小都是相同的。那既然如此是不是指针变量的类型就无意义呢。让我们继续往下面学习。

2.3指针变量类型的意义

#include<stdio.h>
int main()
{
	int n = 0x11223344;
	int* p = &n;
	*p = 0;
	return 0;
}

#include<stdio.h>
int main()
{
	int n = 0x11223344;
	char* p = (char*)&n;
	*p = 0;
	return 0;
}

我们看到上面的两段代码以及他们的内存使用情况,在第一段代码中我们会将n的4个字节全部改为0,但是第二段代码只是将n的第一个字节改为0。指针类型决定了,对指针解引用的时候有多大的权限(一次能操作多少个字节)

2.3.1指针+-整数

还是上代码,我们通过代码来学习知识

#include<stdio.h>
int main()
{
	int n = 10;
	char* p = (char*)&n;
	int* pi = &n;

	printf("&n = %p\n", &n);//%p为打印地址
	printf("p = %p\n", p);
	printf("pc+1 = %p\n", p+1);
	printf("pi = %p\n", pi);
	printf("pi+1 = %p\n", pi+1);
	return 0;
}

通过结果我们可以看到char*类型的指针+1是跳过一个字节,int*类型的指针+1是跳过四个字节,这也是指针变量类型的意义:指针的类型决定了指针向前或向后走多大的距离

2.3.2 void*指针

这是指针类型中一种很特殊的存在,可以理解为无具体类型的指针(泛型指针),这类指针可以用来接收任意类型的地址。同时也有着它的局限性:不能够直接进行指针+-运算和解引用运算

#include<stdio.h>
int main()
{
	int a = 10;
	void* pa = &a;
	void* pc = &a;

	*pa = 10;
	*pc = 0;
	return 0;
}

void* 类型的指针可以接收不同类型的地址,但是无法直接进行指针运算。

三、const修饰指针

3.1const修饰变量

我们的 变量是可以修改的,如果把变量的地址赋给指针,通过指针变量也可以修改这个变量。我们如何才能让变量不被修改呢,那我们就要用到const。

#include<stdio.h>
int main()
{
	int m = 0;
	m = 10;
	const int n = 0;
	n = 20;
	return 0;
}
#include<stdio.h>
int main()
{
	const int n = 0;
	printf("n = %d\n", n);
	int* p = &n;
	*p = 20;
	printf("%n = d\n", n);
	return 0;
}

这两段代码最后的结果只有第二个能够将n修改为20,而第一个则是报错了,第一个报错很好理解,我们在n的前面加了const修饰让他变得不可修改当然报错啦,那第二段代码为什么是用来const还是能修改呢,这是因为我们通过指针来绕过n,使用n的地址(虽然这样不符合语法规则)。那我们怎么才能让它拿到地址也没办法修改n呢,这就是我们接下来要介绍的部分了。

3.2const修饰指针变量‘

#include<stdio.h>
void test1()
{
	int n = 10;
	int m = 20;
	int* p = &n;
	*p = 20;
	p = &m;
}
void test2()
{
	int n = 10;
	int m = 20;
	const int* p = &n;
	*p = 20;
	p = &m;
}
void test3()
{
	int n = 10;
	int m = 20;
	int* const p = &n;
	*p = 20;
	p = &m;
}
void test4()
{
	int n = 10;
	int m = 20;
	int const * const p = &n;
	*p = 20;
	p = &m;
}
int main()
{
	test1();
	test2();
	test3();
	test4();
}

这是VS给到我们这段代码的报错结果,给大家一个const小口诀(左定值,右定向

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

四、指针的运算

指针的运算大致分为三种:

  1. 指针+-整数
  2. 指针-指针
  3. 指针的关系运算

4.1指针+-整数(运算)

因为数组在内存中是连续存放的,只要知道了第一个内存的地址,就能找到后面的所有元素。

#include<stdio.h>
int main()
{
	int arr[10] = { 0,1,2,3,4,5,6,7,8,9 };
	int* p = &arr[0];
	int n = sizeof(arr) / sizeof(arr[0]);
	for (int i = 0; i < n; i++)
	{
		printf("%d ", *(p + i));
	}
	return 0;
}

这就是一段简单的通过指针+整数来遍历打印数组内容的代码。

4.2指针-指针

#include<stdio.h>
int my_strlen(char* s)
{
	char* p = s;
	while (*p != '\0')
		p++;
	return p - s;
}
int main()
{
	printf("%d ", my_strlen("abc"));
	return 0;
}

这段代码中s为起始位置,p为终止位置,我们p-s可以得到它们之间的元素个数的绝对值,前提是两个指针要指向同一块空间。

4.3指针的关系运算

#include<stdio.h>
int main()
{
	int arr[10] = { 0,1,2,3,4,5,6,7,8,9 };
	int* p = &arr[0];
	int n = sizeof(arr) / sizeof(arr[0]);
	while (p < arr + n)
	{
		printf("%d ", *p);
		p++;
	}
	return 0;
}

这段代码中p<arr+n是通过指针的大小比较来循环遍历打印数组中的内容。

五、野指针

野指针是指针指向的位置是不可知的、随机的、不正确的、没有明确限制的。

5.1野指针的成因

① 指针未初始化:局部变量指针未初始化,默认为随机值。全局变量、静态变量不初始化默认0。

#include<stdio.h>
int main()
{
	int* p;
	*p = 20;
	return 0;
}

② 指针越界访问:当指针的范围超出数组arr的范围是,p就是野指针。

#include<stdio.h>
int main()
{
	int arr[10] = { 0 };
	int* p = &arr[0];
	for (int i = 0; i <= 11; i++)
	{
		*(p++) = i;
	}
	return 0;
}

③ 指针指向的空间释放

#include<stdio.h>
int test()
{
    int n = 100;
    return &n;
}
int main()
{
    int* p = test();
    printf("%d\n", *p);
    return 0;
}

5.2野指针的规避

1.指针初始化

C语言中定义一个标识符变量,值是0,0也是地址,这个地址是无法使用的,读写该地址会报错。不知道指针应该指向什么地方时,可以给指针赋值NULL。

#include <stdio.h>
int main()
{
    int num = 10;
    int* p1 = &num;
    int* p2 = NULL;
    
    return 0;
}

2.小心指针越界:程序向内存申请了哪些空间,通过指针就只能访问哪些空间,不能超出范围访问,否则就是越界访问。

3.指针变量不再使用时,及时置NULL,指针使用前检查有效性 :这里有一个约定俗成的规则就是只要NULL指针就不去访问,同时使用指针之前要判断指针是否为NULL。

#include<stdio.h>
int main()
{
    int arr[10] = { 0,1,2,3,4,5,6,7,8,9 };
    int* p = &arr[0];
    for (int i = 0; i < 10; i++)
    {
        *(p++) = i;//p已经越界了
    }
    p = NULL;
    p = &arr[0];
    if (p != NULL)
    {
        //...
    }
    return 0;
}

4.避免返回局部变量的地址: 比如上面的野指针成因③,局部变量的创建在使用后空间会被释放,如果指针指向局部变量,则会编程野指针。

六、assert断言

assert.h头文件中定义了宏assert(),用于在运行时确保程序符合条件,如果不符合就报错终止运行。返回值为0(判断表达式为假时返回,assert()报错终止)和非0(表达式为真,assert()不产生作用,程序继续运行)

#define NDEBUG

#include<asseert.h>

当确认程序没有问题,不需要assert断言时就定义一个宏NDEBUG,重新编译程序,编译器会禁止文件中所有的assert()语句。

assert的缺点就是,因为引入了额外的检查,所以增加了程序运行的时间。但是一般都是用在debug模式下排查问题,Release中禁用aeesrt就好了

七、指针调用

7.1传址调用和传值调用

#include<stdio.h>
void Swap(int x, int y)
{
    int temp = x;
    x = y;
    y = temp;
}
int main()
{
    int a, b;
    scanf("%d %d", &a, &b);
    printf("交换前:a=%d b=%d", a, b);
    Swap(a, b);
    printf("交换后:a=%d b=%d", a, b);
    return 0;
}

奇怪我们的两个数没有成功进行交换,这是什么原因呢?我们来看看调试结果

 我们发现a的地址为:0x008ffb54 {10},b的地址为:0x008ffb48 {20},而x的地址为0x008ffa70 {20},y的地址为0x008ffa74 {10}。相当于x,y是独立的空间。Swap函数在使用的时候,是把变量本身直接传递给了函数,这种叫传值调用

会出现这样的现象的原因是:实参传递给形参时,形参会单独创建一份临时空间来接收实参,对形参的修改不影响实参。

要解决这个问题我们只要将a,b的地址传递给Swap函数就好了。

#include<stdio.h>
void Swap(int *x, int *y)
{
    int temp = *x;
    *x = *y;
    *y = temp;
}
int main()
{
    int a, b;
    scanf("%d %d", &a, &b);
    printf("交换前:a=%d b=%d\n", a, b);
    Swap(&a, &b);
    printf("交换后:a=%d b=%d", a, b);
    return 0;
}

这样Swap函数就成功完成它的任务啦,这种函数的调用方法叫传址调用 ,如果函数内部要修改主调函数中的变量的值,就需要传址调用啦。

到这里我们就介绍完了关于指针的一些基本知识啦,下一篇我将为大家揭晓指针与数组之间的爱恨情仇!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值