指针复习 ( 上 )

预计与指针相关的博客会有三篇,这是第一篇,主要介绍一些指针相关的基本概念与使用

一、指针的概念

在正式说明指针之前,我们先来说内存与地址的概念

  1. 什么是内存 ? 什么是地址 ?
    先举一个生活中的实际例子
    想想我们所住的宿舍,宿舍中每个寝室有对应的编号,假设寝室不是空的,则里面会有学生居住
    再将上面的概念引入到计算机中
    上例的对应关系如下
    宿舍 ⇒{\Rightarrow} 内存 (存储数据的物理空间)
    每个寝室 ⇒{\Rightarrow} 内存单元 (存储数据的最小单位,大小为 1字节)
    编号 ⇒{\Rightarrow} 地址 (如 “301室”,计算机中用十六进制表示,如 0x7ffd1234→{\rightarrow} 编号具有唯一性
    居住在寝室中的学生 ⇒{\Rightarrow} 存储的数据
    也就是说,内存中有许多的内存单元,每个内存单元有自己的地址 ( 内存单元编号 ) 与存储的数据
  2. 指针与地址的关系
    在C语言中给地址起了一个新的名字 : 指针
    所以可以理解为 指针 == 地址 == 内存单元编号

二、指针变量和地址

为了方便观察,后面代码都是在x86环境下运行

2.1 取址运算符 ( & )

当我们在C语言中创建变量时,其实就是向内存申请空间

#include <stdio.h>

int main(){
    int a = 10;
    return 0;
}

在这里插入图片描述

图中所表示出来的就是创建了一个变量 a 时,向内存申请4字节的空间 ( 因为 a 是整型 ) 用于存放整数 10 而 每个字节都有地址

但是我们要怎么取到 a的地址呢? 这时我们就得使用取址运算符 ( & )

#include <stdio.h>

int main(){
    int a = 10;
    printf("%p\n",&a); // %p 是用来打印地址的占位符
    return 0;
}

在这里插入图片描述

我们可以从图片中的输出结果得出两个结论

  1. &a 取了 a 的地址
  2. 取出来的地址是 a 所占四个字节中最小的字节的地址

因为 a 是整型,申请了4个字节的空间,这4个字节是连续的,因此只要知道最小字节的地址,就能访问到4个字节的数据

2.2 指针变量

那我们既然可以取地址,那可不可以把地址存储起来呢? 答案是可以的,这时候就需要指针变量

指针变量用来存储变量的地址

指针变量的定义

Datatype* 指针变量名称 = &变量

注意 ! 这里的 * 不是解引用运算符,当*与数据类型写在一起时,是在说明定义了一个指针变量

储存什么数据类型的地址,就定义什么数据类型的指针变量

#include <stdio.h>

int main(){
	int a = 10;
    int* pa = &a; // 取出a的地址,并将其存储至指针变量 pa 中
	return 0;
}

如何理解指针类型 ?

在这里插入图片描述

2.3 解引用运算符 ( * )

再回顾我们前面的例子,我们可以藉由寝室编号去拿取东西或放置东西。

而我们拿到了变量的地址,就能去拿到这个地址中的内容,要怎么拿到地址中的内容呢? 这时就需要使用解引用运算符

#include <stdio.h>

int main(){
    int a = 10;
    int *p = &a;
    printf("a = %d\n", a);
    printf("*p = %d\n", *p);
    return 0;
}

在这里插入图片描述

我们不仅可以藉由解引用运算符取得指针变量存储的地址中的内容,我们也可以修改内容

#include <stdio.h>

int main(){
    int a = 10;
    int *p = &a;
    printf("a = %d\n", a);
    printf("*p = %d\n", *p);
    *p = 20; // 修改指针变量p中所存储的内容为20 
    printf("a = %d\n", a);
    printf("*p = %d\n", *p);
    return 0;
}

在这里插入图片描述

2.4 指针变量的大小

那指针变量有多大呢 ?

#include <stdio.h>
// 指针的大小事实上会根据 x86 还是 x32 而变动
int main(){
    printf("%zd\n",sizeof(char*));
    printf("%zd\n",sizeof(int*));
    printf("%zd\n",sizeof(short*));
    return 0;
}

x86
在这里插入图片描述

x64

在这里插入图片描述

因为指针变量存储的地址,地址大小不是 4 Bytes ( 32 bits ) 就是 8 Bytes ( 64 bits )

2.5 指针类型的意义

只要平台( x86 or x64 ) 相同,指针类型的大小就相同,那指针类型的存在有什么意义呢 ?

让我们看看下面两段代码

//代码1
#include <stdio.h>
int main()
{
 int n = 0x11223344;
 int *pi = &n;
 *pi = 0;
 return 0;
}
//代码2
#include <stdio.h>
int main()
{
 int n = 0x11223344;
 char *pc = (char *)&n;
 *pc = 0;
 return 0;
}

我们藉由调试来观察这两个代码的差异

代码1

在这里插入图片描述

代码2
在这里插入图片描述

我们可以看到两段代码的运行结果不一样。在代码1中会将4个字节全部改为0,而代码2中只是将第1个字节改为0

因此我们可以得出第一个结论 : 指针类型决定指针解引用时有多大的权限 ( 也就是一次可以操作多少字节 )

比如上面的代码 : char* 的解引用一次只能访问一个字节;int* 解引用一次可以访问4个字节

指针的意义不仅如此

我们再来看下面这段代码

#include <stdio.h>

int main()
{
	int n = 10;
	char* pc = (char*)&n;
	int* pi = &n;

	printf("%p\n", &n);
	printf("%p\n", pc);
	printf("%p\n", pc + 1);
	printf("%p\n", pi);
	printf("%p\n", pi + 1);
	return 0;
}

在这里插入图片描述

我们可以从这段代码中看到,char*类型的指针变量+1后,会跳过一个字节;int*类型的指针变量+1后会跳过4个字节

因此我们可以得出第二个结论 : 指针类型决定了指针向前或向后走一步有多大 ( 距离 )

总结指针类型的意义

  1. 指针类型决定指针解引用时有多大的权限 ( 也就是一次可以操作多少字节 )
  2. 指针类型决定了指针向前或向后走一步有多大 ( 距离 )

三、void* 指针

指针类型中有一种特殊的指针类型 void* 可以将其理解成无具体类型指针或泛用型指针

这种指针的特色是可以接收来自任意类型的地址,但是 void* 具有局限性,它无法进行指针的±运算与解引用

#include <stdio.h>

int main()
{
 int a = 10;
 int* pa = &a;
 char* pc = &a;
 return 0;
}

上面这段代码试图将一个 int 类型的变量地址赋值给一个 char* 类型的指针变量,此时编译器会报警告,因为类型不兼容
在这里插入图片描述

但如果使用void* 就不会有这个问题

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

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

编译结果
在这里插入图片描述

这里可以看到,void* 可以接收任意类型的地址,但是无法直接进行指针运算与解引用

那这样 void* 有什么用处呢 ?

一般来说,void* 会用在函数参数部份,用来接收不同类型数据的地址,这样设计的好处是可以实现泛型编程。使得一个函数可以处里多种类型的数据。

四、const 修饰指针

4.1 const的功能

变量可以被修改,如果将变量的地址给一个指针变量,则也可以透过指针变量来修改该变量。如果我们希望一个变量不能被修改,该如何做呢 ? 这时,我们就需要使用 const 关键字

#include <stdio.h>
int main(){
    int a = 10;
    a = 30; // a 可以被修改
   	const int b = 20;
   	b = 40; // b 不可以被修改
    return 0;
}

虽然我们说 const 可以修饰变量,让变量的数据无法被修改。但事实上,我们依然可以透过指针变量来修改被const 修饰的变量

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

在这里插入图片描述

很明显这有失我们的本意,我们就是不希望变量 a 的内容被改变才加上const,但却可以透过指针来修改

因此就必须要导出一个概念 ⇒{\Rightarrow} const 修饰指针变量

4.2 const 修饰指针变量

用关键字 const 修饰指针变量时需要注意, const放置的位置不同,对于指针变量的限制也就不同

  1. int const *p; →{\rightarrow} const 放在 * 左边,修饰的是 *p 这代表的意义是, *p不能被改变,也就是说我们不能透过 *p 来改变指针变量 p 所指向的地址的内容

    也可以写成 const int *p 两者是等价的

  2. int* const p; →{\rightarrow} const 放在 p 的左边, 修饰的是 p 本身,p 是什么? p是指针变量,这也就代表说,我们不能改变 p 所保存的变量地址

以代码来分析指针变量的限制

//代码1 - 测试⽆const修饰的情况
void test1()
{
 int n = 10;
 int m = 20;
 int *p = &n;
 *p = 20;//ok? // ok
 p = &m; //ok? // ok 
}
//代码2 - 测试const放在*的左边情况
void test2()
{
 int n = 10;
 int m = 20;
 const int* p = &n;
 *p = 20;//ok? // no
 p = &m; //ok? // ok
}
//代码3 - 测试const放在*的右边情况
void test3()
{
 int n = 10;
 int m = 20;
 int * const p = &n;
 *p = 20; //ok? // ok
 p = &m; //ok? // no
}
//代码4 - 测试*的左右两边都有const
void test4()
{
 int n = 10;
 int m = 20;
 int const * const p = &n;
 *p = 20; //ok? // no
 p = &m; //ok? // no
}

代码 1
在这里插入图片描述

可以顺利运行,因此代码 1 是没问题的

代码 2

在这里插入图片描述
因为我们限制了 *p 因此 *p = 20; 是不可行的,但 p = &m 是可行的

代码 3
在这里插入图片描述
虽然报的错误和代码 2 是一样的,但事实上两者是不同的

在代码 3 中, const 写在 p 的左边, 因此错误报的是 p = &m 这行代码, 无法改变p所存储的地址,但可以改变p所存储的地址的内容

代码 4
在这里插入图片描述
代码 4 中就是既不能修改 指针变量 p 所存储的地址的内容,也不能修改 p 所存储的地址

五、指针的运算

5.1 指针±整数

指针±运算最常见的就是在于访问数组,因为数组在内存中是连续存放的,因此我们只要知道第一个元素的地址,就能访问整个数组的内容

#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]);
 for(i=0; i<sz; i++)
 {
 printf("%d ", *(p+i));//p+i 这⾥就是指针+整数
 // *(p+i) 相当于 arr[i]
 }
 return 0;

5.2 指针-指针

指针-指针所得的结果就是数据的个数

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

在这里插入图片描述

5.3 指针的运算关系

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

在这里插入图片描述

六、野指针

什么是野指针 ? 野指针就是指针指向的位置是不可知的 ( 是随机的、不正确的 )

6.1 野指针的成因

野指针的成因通常避不开以下三点

  1. 指针未初始化
  2. 指针越界访问
  3. 指针指向的空间释放
// 1. 指针未初始化
#include <stdio.h>
int main()
{
 int *p;//局部变量指针未初始化,默认为随机值
 *p = 20;
 return 0;
}
// 2. 指针越界访问
#include <stdio.h>
int main()
{
 int arr[10] = {0};
 int *p = &arr[0];
 int i = 0;
 for(i=0; i<=11; i++)
 {
 //当指针指向的范围超出数组arr的范围时,p就是野指针
 *(p++) = i;
 }
 return 0;
}
// 3. 指针指向的空间释放
#include <stdio.h>
int* test()
{
    int n = 100;
    return &n;
}
int main()
{
    int*p = test();
    printf("%d\n", *p);
    return 0;
}
/*
因为 n 是局部变量,当 test()函数被调用时,会创建一个栈桢, 变量 n 存储在该栈桢中。当test函数调用结束后该栈桢就会销毁,n也就跟着被销毁,但是返回的是 n 的地址,但 n 已经被销毁了,因此就会引发野指针的问题
*/

6.2 避免野指针的方法

  1. 初始化指针
  2. 避免越界访问
  3. 避免返回局部变量的地址
  4. 指针不使用时,就将其设成NULL (在动态配置内存时较常使用)
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif

如果明确知道指针要存储哪个变量的地址,就直接初始化。如果还不知道指针变量要存储的地址,就将其设成 NULL

NULL 是C语言中定义的一个标识符常量,值是0,0也是地址,但我们无法对这个地址进行读写

#include <stdio.h>
int main()
{
 int num = 10;
 int*p1 = &num;
 int*p2 = NULL; // 初始化

 return 0;
}

七、指针的使用与传址

7.1 传值调用与传址调用

以交换两个变量的值为例

#include <stdio.h>
void Swap1(int x, int y)
{
    int tmp = x;
    x = y;
    y = tmp;
}
int main()
{
    int a = 0;
    int b = 0;
    scanf("%d %d", &a, &b);
    printf("交换前:a=%d b=%d\n", a, b);
    Swap1(a, b);
    printf("交换后:a=%d b=%d\n", a, b);
    return 0;
}
// 当我们这样写代码时, a 和 b 的值可以成功交换吗 ?

在这里插入图片描述

我们从输出结果可以看到并没有像我们所预想的交换,为什么呢 ?

在这里插入图片描述

我们可以看到,在main函数中创建 a 和 b 时,a的地址是 0x00bcfc8c b的地址是 0x00bcfc80

在调用 Swap1 函数的时后,将 a 和 b 传递给了 Swap1 函数,在 Swap1 函数内部创建了形参 x 和 y 来接收 a 和 b 的值

但是 x 的地址是 0x00bcfba8,y 的地址是0x00bcfbac,相当于 x 和 y 是独立的空间,那么在 Swap1 函数中进行交换自然就不会影响到 a 和 b, 因此在输出结果中 a 和 b 的值并没有改变。这种传递的方式就称为传值调用

传值调用的特色是,实参传给形参时,形参会单独建立一份临时的空间来接收实参。形参的改变不会影响到实参

如果我们希望 调用 Swap1 后可以成功交换 a 和 b 的值,此时就会需要使用指针。我们知道传值调用之所以没有办法成功交换 a 和 b 的值是因为 x、y 与 a、b的地址是不一样的,那我们只要让传入的形参是 a 和 b 的地址,不就能顺利交换了 ?

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

在这里插入图片描述
在这里插入图片描述
可以看到,藉由传入 a 和 b 的地址当做 Swap2 函数的参数,就可以成功让 a 和 b 的值进行交换,这种方式就称为传址调用

如果我们希望可以藉由其他函数来修改变量,那就可以考虑使用传址调用

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值