【C语言】—— 指针二 : 初识指针(下)

一、

c

o

n

s

t

const

const 修饰指针

1.1、

c

o

n

s

t

const

const 修饰变量

  
  当我们创建一个变量后,不想被修改,该怎么办呢?这时,我们就可以请出

c

o

n

s

t

const

const 来修饰变量了
  

int main()
{
	int m = 0;
	m = 20;//m可以被修改

	const int n = 0;
	n = 20;//n不能被修改
	return 0;
}

  
运行结果:
在这里插入图片描述
如上图,若修改被

c

o

n

s

t

const

const 修饰的变量,则程序报错
  
  其实,

n

n

n 本质上还是变量,只是被

c

o

n

s

t

const

const 修饰后,在语法上加了限制(此时的

n

n

n 称作常变量),只要我们在代码中对

n

n

n 进行修改,就不符合语法规则,程序就会报错。
  
  虽然通过正常手段无法修改

n

n

n 的值,但我们可以用间接手段:通过

n

n

n 的地址,来修改

n

n

n

#include<stdio.h>

int main()
{
	const int n = 0;
	printf("n = %dn", n);
	int* p = &n;
	*p = 20;
	printf("n = %dn", n);
	return 0;
}

在这里插入图片描述

  但我们用

c

o

n

s

t

const

const 修饰变量,本质上是不希望变量被修改的,你现在通过指针把我给改了,是不是有点不讲武德。就好像我不希望你进我家,我把家门给锁,你现在翻窗进来,是不是有点不合适?
  
  所以我们应该让

p

p

p 拿到

n

n

n 的地址也修改不了

n

n

n ,那该怎么办呢?

  我们可以用

c

o

n

s

t

const

const 来修饰指针。
  

1.2、

c

o

n

s

t

const

const 修饰指针

  

c

o

n

s

t

const

const 修饰指针变量的时候

  • c

    o

    n

    s

    t

    const

    const 如果放在

    *

    的左边,修饰的是指针所指向的内容,保证指针所指向的内容不能通过指针来修改,但是指针变量本身的内容可以改变。

  • c

    o

    n

    s

    t

    const

    const 如果放在

    *

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

注:当然,

c

o

n

s

t

const

const 也可以在

*

两边都放的,但一般很少这么做。(你两边都不给我改,是不是太过分了)
  
通过代码来感受一下:
  
(1)

c

o

n

s

t

const

const

*

左边

#include<stdio.h>

int main()
{
	int n = 0;
	int m = 10;
	const int* p = &n;
	p = &m;
	printf("%dn", *p);
	return 0;
}

运行结果:
在这里插入图片描述

  

c

o

n

s

t

const

const

左边 : 指针变量本身的内容可以改变

  

参考代码:

#include<stdio.h>

int main()
{
	int n = 0;
	int m = 10;
	const int* p = &n;
	*p = 20;
	printf("%dn", *p);
	return 0;
}

运行结果:

在这里插入图片描述
  

c

o

n

s

t

const

const

*

左边 : 指针所指向的内容不能通过指针来修改
  
(2)const 在

*

右边
  
参考代码:

#include<stdio.h>

int main()
{
	int n = 0;
	int m = 10;
	int* const p = &n;
	*p = 20;
	printf("%dn", *p);
	return 0;
}

运行结果:
在这里插入图片描述

  

c

o

n

s

t

const

const

*

右边 : 指针指向的内容可以通过指针来改变
  
参考代码:

#include<stdio.h>

int main()
{
	int n = 0;
	int m = 10;
	int* const p = &n;
	p = &m;
	printf("%dn", *p);
	return 0;
}

运行结果:
在这里插入图片描述

  

c

o

n

s

t

const

const

*

右边 : 指针变量本身不能被修改

  
  

二、野指针

  

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

  

2.1野指针的成因

(1)指针未初始化
int main()
{
	int* p;//局部变量指针未初始化,默认为随机值
	*p = 20;
	return 0;
}

注:局部变量指针不初始化,默认为随机值
  

(2)指针越界访问
#include<stdio.h>

int mian()
{
	int arr[10] = { 0 };
	int* p = &arr[0];
	int i = 0;
	for (i = 0; i <= 11; i++)
	{
		//当指针指向的范围超出数组arr的范围时,p就是野指针
		*p(p++) = i;
	}
	return 0;
}

注:当指针指向超出

a

r

r

arr

arr 数组的范围时,

p

p

p 就是野指针
  

(3)指针指向的空间释放
#include<stdio.h>

int* test()
{
	int n = 100;
	return &n;
}
int mian()
{
	int* p = test();
	printf("%dn", *p);
	return 0;
}

注:在【C语言】——详解函数一文中,曾提到函数中形参只是实参的一份拷贝,变量

n

n

n 在出函数范围时已释放,这时的指针变量就是野指针。
  

2.2、如何规避野指针

(1)指针初始化
  • 明确知道指针指向哪里,就初始化一个明确的地址
  • 如果创建时,还不知道指针具体指向位置,则给指针赋值

    N

    U

    L

    L

    NULL

    NULL

  
注:

N

U

L

L

NULL

NULL 是C语言中定义的一个标识符常量,本质是 00 也是地址,但该地址位于内核之中,我们用户是无法使用的,读写该地址会报错。
  

#ifndef NULL
    #ifdef __cplusplus
        #define NULL 0
    #else
        #define NULL ((void *)0)
    #endif
#endif

  

  我们可以把野指针看成野狗,很危险。给野指针赋值 NULL 相当于把野狗拴在树上,这时,你只要不靠近就不会有危险,相对安全 。

这里放张野狗图

  

(2)小心指针越界

  
  创建变量时,向内存中申请了多少空间,通过指针也只能访问这些空间。超出这些空间访问,就是越界访问,指针也就成了野指针。上述例子中,数组的越界访问就是如此。
  

(3)指针变量不再使用时,及时置 NULL,指针使用之前检查有效性

  当我们创建指针变量想访问某个区域,后期不再访问时,可以将指针变量置为 NULL
  
  使用指针变量前,我们先检查他的有效性,即检查它是否为空指针,如果为空指针,我们就不再访问
  
  还是上面野狗的例子,虽然野狗被拴起来,但是靠近他还是很危险,要和他保持一定距离才算安全。
  
  事实上,我们使用指针最合理的方式是:先判断,再使用
  

(4)避免返回局部变量的地址

  
  这一点,上述第三个例子中,不要返回函数中创建的局部变量的地址。
  
  

三、

a

s

s

e

r

t

assert

assert 断言

  
  C语言 <assert.h> 头文件中,定义了宏

a

s

s

e

r

t

assert

assert,那么

a

s

s

e

r

t

assert

assert 的功能是什么呢?
  
  

a

s

s

e

r

t

assert

assert 用于在运行时程序符合规定条件,如不符合,则报错程序终止运行并给出报错信息提示

a

s

s

e

r

t

assert

assert 常常被称作断言
  

assert(p != NULL)

  
  程序在运行到上述语句时,会先判断指针

p

p

p 是否为空指针,如果为空指针,则报错给出信息并终止程序运行。如果不是空指针,则程序正常运行。

  

a

s

s

e

r

t

()

assert()

assert()宏接受一个表达式作为参数(该表达式不一定非要指针),该表达式为真

a

s

s

e

r

t

()

assert()

assert()宏不会产生任何作用,程序正常运行,当表达式为假,

a

s

s

e

r

t

()

assert()

assert()宏就会报错,在标准错误流

s

t

d

e

r

r

stderr

stderr 中写入一条错误信息,表示没有通过的表达式,以及包含这个表达式的文件名和行号。

那么

a

s

s

e

r

t

assert

assert 有什么好处呢?

  • 他能自动表示文件和出问题的行号
  • 他有一种无需更改代码就开启和关闭的机制。如果想关闭

    a

    s

    s

    e

    r

    t

    assert

    assert 断言,只需在 #include <assert.h> 前面,加上一个

    N

    D

    E

    B

    U

    G

    NDEBUG

    NDEBUG

#define NDEBUG
#include <assert.h>

  
  而如果程序又出现问题,我们只需要将

N

D

E

B

U

G

NDEBUG

NDEBUG 删除掉,

a

s

s

e

r

t

()

assert()

assert()就能重新启动了。
  
  这时,可能还有小伙伴会问,我可以直接用

i

f

if

if 语句来判断啊,为什么要用

a

s

s

e

r

t

assert

assert 断言呢?
  

相比与if语句,

a

s

s

e

r

t

assert

assert 断言:

  • 出现错误,直接报错,并指明文件哪一行
  • i

    f

    if

    if 语句在不用时,要把他注释掉,因为让他放在那会占用内存空间

  
  当然

a

s

s

e

r

t

assert

assert 断言也有缺点:因为引用了额外的检查,增加了程序运行时间。

  同时,

a

s

s

e

r

t

assert

assert 断言一般在

d

e

b

u

g

debug

debug 版本中使用,在

r

e

l

e

a

s

e

release

release 版本我们将它禁用就行,因为在

r

e

l

e

a

s

e

release

release 版本

a

s

s

e

r

t

assert

assert 断言会直接被优化掉。
  
  

四、传值调用与传址调用

4.1、传值调用

  
  在之前的学习中,我们知道,函数传参中,形参仅仅是实参的一份临时拷贝,对形参的修改并不会改变实参的值(详情请看【C语言】——详解函数)。这种函数调用方式叫做传值调用。
  
  但如果我们想封装一个函数,让他交换两个变量的值,该怎么实现呢?这时我们就需要传址调用来实现,让我们一起来看看。
  

4.2、传址调用

  
  当我们函数传参时,将变量的地址传给函数,这种函数的调用方式叫做:传址调用。
  
我们来看下面代码:

#include<stdio.h>

void Swap(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=%dn", a, b);
	Swap(&a, &b);
	printf("交换后: a=%d b=%dn", a, b);

	return 0;
}

运行结果:
在这里插入图片描述

  我们可以看到,在

m

a

i

n

main

main 函数中将

a

a

a

b

b

b 的地址传给了

s

w

a

p

swap

swap 函数,

s

w

a

p

swap

swap 函数通过地址,简介访问两个变量,实现了两个变量间的交换

  

结论:

  • 当我们调用函数,只需要主函数中变量的值时,可以使用传值调用
  • 当我们需要函数内部修改主函数中的值,就需要传址调用

  
  

五、二级指针

  
  在讲解二级指针之前,我们先来理清一级指针变量

p

(p)

p的三种关系:
  

  • p

    p

    p 中可以放

    a

    a

    a 的地址

  • p

    p

    p 可以通过解引用找到

    a

    a

    a

  • p

    p

    p 自己本身也有一个地址

  
如图:

在这里插入图片描述

  
  那什么又是二级指针呢?
  
  二级指针就是存放指针变量地址的变量
  

#include<stdio.h>

int main()
{
	int a = 10;
	int* pa = &a;
	int** ppa = &pa;

	return 0;
}

  
图示:

在这里插入图片描述

  
  在上述代码中,

p

p

a

ppa

ppa 就是二级指针变量,变量类型

i

n

t

int

int**,怎么来理解呢?

  • 前面

    i

    n

    t

    int

    int* 表示他所指向的变量为指针变量,

  • 第二颗

    *

    表示他一个指针变量

  
对于二级指针的运算有:

  • *

    p

    p

    a

    ppa

    ppa 通过对

    p

    p

    a

    ppa

    ppa 中的地址进行解引用,这样找的是

    p

    a

    pa

    pa, *

    p

    p

    a

    ppa

    ppa 其实访问的就是

    p

    a

    pa

    pa

int b = 20;
*ppa = &b;//等价于 pa = &b;
  • **

    p

    p

    a

    ppa

    ppa 先通过 *

    p

    p

    a

    ppa

    ppa 找到

    p

    a

    pa

    pa,然后对

    p

    a

    pa

    pa 进行解引用从操作: *

    p

    a

    pa

    pa,找到的是

    a

    a

    a

**ppa = 30;
//等价于 *pa = 30;
//等价于 a = 30;

  
  

六、指针数组

6.1、指针数组的概念

  
  首先,我先来问一个问题,指针数组是指针还是数组?
  
  我们学习新知识的时候,可以用类似的旧知识来类比:整形数组是整形变量还是数组?答案很明显嘛,就是数组,同理,指针数组也是数组
  
  就像整形数组存放的元素类型是整形,指针数组存放的每个元素为指针
  

在这里插入图片描述

指针数组的每个元素是地址,又可以指向一块区域。

  

6.2、指针数组模拟二维数组

  

#include<stdio.h>

int main()
{
	int arr1[] = { 1,2,3,4,5 };
	int arr2[] = { 2,3,4,5,6 };
	int arr3[] = { 3,4,5,6,7 };

	int* parr[3] = { &arr1[0],&arr2[0],&arr3[0] };

	int i = 0;
	int j = 0;
	for (i = 0; i < 3; i++)
	{
		for (j = 0; j < 5; j++)
		{
			printf("%d ", parr[i][j]);
		}
		printf("n");
	}
}

  
运行结果:
在这里插入图片描述
  
在这里插入图片描述
  
  
  

p

a

r

r

parr

parr

[

[

[

i

i

i

]

]

] 是访问

p

a

r

r

parr

parr 数组的元素,

p

a

r

r

parr

parr

[

[

[

i

i

i

]

]

] 找到的数组元素指向了整型一维数组,

p

a

r

r

parr

parr

[

[

[

i

i

i

]

]

]

[

[

[

j

j

j

]

]

] 就是整形一维数组中的元素。
  
  上述代码模拟出二维数组的效果,实际上并非完全是二维数组,因为二维数组在内存中是连续存储的,而上述代码每一行的存储可能差了十万八千里。
  
  
  
  


  好啦,本期关于指针就介绍到这里啦,希望本期博客能对你有所帮助,同时,如果有错误的地方请多多指正,让我们在C语言的学习路上一起进步!

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
内是否存在10000的质因式即可,因为大于100的质因式一定会被舍去。

)">
< <上一篇
下一篇>>