深入理解指针(1)

目录

指针变量:

1.指针的定义:

1.1指针变量的申明 

1.2指针变量的初始化

2.指针变量的大小:

3.指针变量类型的意义: 

3.1指针的解引用

3.2void* 指针

4.const 修饰指针

4.1const 修饰变量

4.2const 修饰指针变量

5.指针运算 

5.1指针 +- 整数

5.2 指针 - 指针

5.3指针的关系运算

6.野指针 

6.1野指针的成因 

6.1.1.指针未初始化

6.1.2.指针越界访问 

3.指针指向的空间释放 

6.2如何避免野指针

6.2.1.指针初始化

 ​编辑

6.2.2.小心指针越界

6.2.3.指针不用置NULL

7.assert断言

7.1.assert优点

7.2.assert缺点

8.指针的使用和传址调用

8.1.传值和传址

8.2.传值&传址

小结:


C语言中引入指针的概念,其作用是用来访问内存,今天就来详细地了解一下指针的相关知识。

在介绍指针变量之前,我们得明确,数据都是记录在一个地址上的,不管数据如何变化,其地址是一成不变的。所以就算指针存储了一个变量的地址,改变指针的值,只能说明指针指向了别的地址上的数据,不能说明原本变量的地址发生变化。


指针变量:

1.指针的定义

计算机中处理数据时,得先去通过地址总线去寻找数据,这就得引入地址的概念。地址可以抽象成数据存放的位置。而指针就是用来存放地址

1.1指针变量的申明 

在C语言中 ,指针变量 = 指针指向的类型 + 变量名。如下:

int* a   float* b   double * c   char* d 

1.2指针变量的初始化

1、直接初始化

int* a = NULL;        int* a = 0x11223344

2、指向某一变量(常用) 

int a = 10;        int* p = &a;

&是取址符号,就是取a的地址存储给p 


2.指针变量的大小

指针变量就是用来存放数据的,但是指针变量的大小与指针指向的数据类型无关

列如下面两种指针变量在同个操作系统,指针变量大小都是相同的。

int* a        char& s;

指针变量大小取决于地址线的个数。

结论:32位环境中=下指针变量大小为4个字节;64位环境下为8个字节。


3.指针变量类型的意义

大家可能会有疑问:指针变量的⼤⼩和类型⽆关,只要是指针变量,在同⼀个平台下,⼤⼩都是⼀
样的,为什么还要有各种各样的指针类型呢?
3.1指针的解引用

指针变量想通过地址找到其地址对应的数据得通过解引用 " * " 符号。

例如:

int a = 10;        int* p = &a;        int b = *p;

执行完后 b = 10

但是如果指针类型和指向的数据类型不匹配的话会出现错误。

如下面代码

	int a = 123123;
	char* p = &a;
	*p = 0;
	printf("%d", a);

输出结果为

122880

因为指针类型决定了指针进行解引用操作符的时候访问几个字节,也就是决定指针的权限!

所以我们在使用指针时,类型一定要相互匹配。

3.2void* 指针
可以理解为⽆具体类型的指针(或者叫泛型指针),这种类型的指针可以⽤来接受任意类型地址。但是也有局限性, void* 类型的指针不能直接进行指针的+-整数和解引用的运算。
⼀般 void* 类型的指针是使⽤在函数参数的部分,⽤来接收不同类型数据的地址,这样的设计可以
实现泛型编程的效果。使得⼀个函数来处理多种类型的数据。

4.const 修饰指针

4.1const 修饰变量

如果将一个变量前 + const 那么就给这个变量赋予了常属性,可以认为是常变量(本质还是变量但是无法直接修改变量的值)

如下面这段报错代码:

const int a = 10;
a = 2;

但是我们依然有办法去修改常变量的值,那就是借助指针去访问地址来进行修改。

如下面这段代码:

const int a = 10;
int* p = &a;
*p = 1;

 但是这么做就破坏了想将 a 变成常量的做法。所以我们采用了const + 指针的用法。

4.2const 修饰指针变量

先说结论:const修饰指针变量的时候

1、const如果放在*的左边const int* p(int const* p),修饰的是指针指向的内容不能通过指针来改变。但是指针变量本身的内容可变。

2、const如果放在*的右边int* const p,修饰的是指针变量的本身,保证了指针变量的内容不能修改,但是指针指向的内容,可以通过指针改变。

我们分成两种类型的const修饰指针进行展开解释

1.const int* p (int * const p)

int a = 10;
int b = 20;
const int* p = &a;
*p = 20;    // error报错
p = &b;     // 正常运行 p重新指向变量b,存储变量b的地址

2.int * const p

int a = 10;
int b = 20;
int * const p = &a;
*p = 20;    // 正常运行,将a 的值变为20
p = &b;     // error报错

5.指针运算 

5.1指针 +- 整数

看下面这段代码 

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

	printf("pa     = %p \n", pa);
	printf("pa + 1 = %p \n", pa + 1);

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

执行结果为

pa     = 00AFFAB4
pa + 1 = 00AFFAB8
pc     = 00AFFAB4
pc + 1 = 00AFFAB5

我们可以看出, char* 类型的指针变量+1跳过1个字节, int* 类型的指针变量+1跳过了4个字节。

公式:+/- 整数 --> 向左/右跳过 1 * sizeof(type) 个字节 

结论:指针的类型决定了指针向前或者向后走一步有多大(距离),可以和数组的下标所联系在一起。意义便是当指针指向一个数组时,可以直接通过指针+整数的形式访问对应的下标数据。

 下面就是用指针代替下标遍历数组的代码:

int arr[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int *p = &arr[0];
int sz = sizeof(arr) / sizeof(arr[0]);

for (int i = 0; i < sz; i++)
{
    printf("%d ", *p);
    p++;
}

执行结果: 

1 2 3 4 5 6 7 8 9 10

关于指针解引用和前置后置++符号的综合运用可以参考下面文章

区分*p++,(*p)++,++*p,*++p

5.2 指针 - 指针

前提条件:两个指针指向了同一个空间(比如说数组)这样才有意义。

指针 - 指针 可以类比成 日期 - 日期 得到的是一个数,而这个数就是两个指针的地址差值。

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 i = 0;
     int sz = sizeof(arr)/sizeof(arr[0]);
     while(p<arr+sz) //指针的⼤⼩⽐较
     {
         printf("%d ", *p);
         p++;
     }
     return 0;
}

6.野指针 

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

6.1野指针的成因 
6.1.1.指针未初始化

不合法: 

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

合法:

#include <stdio.h>
int main()
{
    int a = 10;
    int* p = &a;
    *p = 20;
    return 0;
}
6.1.2.指针越界访问 
#include <stdio.h>
int main()
{
    int arr[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
    int* p = &arr[0];    // p 此时不是野指针
    int sz = sizeof(arr) / sizeof(arr[0]);
    for (int i = 0; i <= sz; i++)    // i = sz 时越界访问
    {
        printf("%d ", *p);
        p++;
    }
	return 0;
}
3.指针指向的空间释放 

示例一:动态分配内存后释放

#include <stdio.h>
int main()
{
	int* p = (int*)malloc(sizeof(int));
	p[0] = 10;
	printf("%d\n", *p);
	free(p);
	printf("%d\n", *p);
	return 0;
}

运行结果为:

10
-572662307

 示例二:函数栈帧创建和销毁

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

但是由于函数栈帧销毁只是进行指针的移动,并不是直接释放空间,所以仍然可以通过*p访问。 

6.2如何避免野指针
6.2.1.指针初始化

要么直接将指针指向一个变量,要么将指针赋值为NULL ((void *)0)。如下面代码:

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

但是需要注意的是如果赋值为 NULL 时,不可以直接解引用。只为 0 其实也是一个地址,但是计算机规定一个区域是给系统内核的,无法直接访问。 

6.2.2.小心指针越界

明确一个程序向内存申请了多大的空间,避免指针越界,越界访问。

6.2.3.指针不用置NULL
我们将后续不再使用的指针置为NULL,每次使用指针的时候进行判断(一般用assert进行断言判断)只是是否为NULL,便可以杜绝访问野指针的现象。
约定俗成的规则:只要是NULL指针就不去访问, 同时使⽤指针之前可以判断指针是否为NULL。


7.assert断言

assert.h 头⽂件定义了宏 assert() ,⽤于在运⾏时确保程序符合指定条件,如果不符合,就报
错终⽌运⾏。这个宏常常被称为“断⾔”。
如下面代码:
#include <stdio.h>
#include <assert.h>
int main()
{
	int* p = NULL;
	assert(p != NULL);
	return 0;
}

执行结果:

7.1.assert优点

1. assert 出现错误的时候,直接会报错,指明在什么文件,哪一行,便于程序员直接找到错误的地方

2、如果已排查后,发现无错误,可以直接关闭 assert() 机制。只需要在assert头文件前加上一个宏 #define NDEBUG 就可以一键关闭。

7.2.assert缺点

引入了额外的检查,增加了程序的运行时间

        一般我们在Debug版本中使用assert,在Release版本中选择禁用assert,在VS这样的集成开发环境中,在Release版本中,直接就是优化掉了。这样在Debug版本写有利于程序员排查问题,Release版本不影响用户使用时程序的效率。


8.指针的使用和传址调用

8.1.传值和传址

这里借助两个数值交换的函数功能进行阐述。

下面是普通的两数值交换的代码:

#include <stdio.h>
int main()
{
	int a = 10, b = 20;
	int tmp = a;
	a = b;
	b = tmp;
	return 0;
}

而如果想采用函数实现该功能的话,普通的传参是满足不了的例如

#include <stdio.h>
void swap(int a, int b)
{
	int tmp = a;
	a = b;
	b = tmp;
}
int main()
{
	int a = 10, b = 20;
	swap(a, b);
    printf("a = %d, b = %d\n", a, b);
	return 0;
}

执行结果为

a = 10, b = 20

 由此可知普通传参是无法实现的 

 因为实参传递给形参,形参只是实参的一个临时拷贝,对形参的修改不会影响到实参。

下面是借助地址,传给函数进行两数值交换:

#include <stdio.h>
void swap(int* a, int* b)
{
	int tmp = *a;
	*a = *b;
	*b = tmp;
}
int main()
{
	int a = 10, b = 20;
	swap(&a, &b);
    printf("a = %d, b = %d\n", a, b);
	return 0;
}

 执行结果为:

a = 20, b = 10

由此可知如果采用传址调用可以完成该功能。在函数中只需要解引用就可以远程的找到传入地址所对应的数据。

8.2.传值&传址

那么我们什么时候采用传值传参,什么时候采用传址传参。 

1、传值调用

        实参传递给形参的时候,形参会单独创建⼀份临时空间来接收实参,对形参的修改不影响实
参。
2、传址调用
        可以让函数和主调函数之间建⽴真正的联系,在函数内部可以修改主调函数中的变量;所以未来函数中只是需要主调函数中的变量值来实现计算,就可以采⽤传值调⽤。如果函数内部要修改主调函数中的变量的值,就需要传址调用。

小结:

今天我们了解到了指针的基本用法,取址操作和解引用操作,了解了指针的基本运算规则,和使用 指针时的注意事项,以及使用习惯。也说明了指针在函数传参时的用法。

之后会出对指针的不同类型进行区分以及运用。

如果这篇文章对你有帮助的话,不妨点个免费的赞和关注,谢谢支持!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值