C语言指针专项
指针
我们在讲什么是指针之前,我们先说一下指针中的一个必用参数
sizeof()
sizeof
是一个是一个运算符,给出某个类型或者变量在内存中所占据的字节数。就比如说
sizeof(int)
– ->int
类型在内存中占据的字节数
sizeof(i)
—> i 变量在内存中占据的字节数
1 |
|
运行结果:
sizeof(int)=4
我们知道,一个字节是 8bit
,那一个 int
有 4
个字节,也就是 32 个 bit。
如果说我们把 int
修改为 double
的话,那么就变成了 sizeof(double)=8
了。
运算符 &
熟悉吗?是不是很熟悉?我们在 scanf
里面是不是都会用到&符号
scanf(“%d”,&i);
那么这个&符号到底是干嘛的?
用来获取变量的地址的,&符会把这个变量的地址给你。
地址?什么地址?为啥一个变量会有地址呢?
在 C 语言中,我们所有的变量都是放在内存里面的,就好比如 int,int 在内存中占用了 4 个字节,你看,他既然占用了一定的地方,那他是不是就得有一个地址。所以&符就是把那个变量的地址告诉你,所以我们也把&符叫做取地址符。
哪地址长什么样子呢?
我们可以用一段代码来看看
我们发现输出了 a 这个变量的地址了。
然后,我们又发现,程序出现了一个 warring
Format specifies type ‘unsigned int’ but the argument has type ‘int *’
这个问题我们先留着,到后面讲。
但是我们可以先翻译一下
如果你想要输出这个 a 的地址的话,你不应该使用%x。(我说的翻译指的是这个问题的 bug 是啥,不是真的把这段英语翻译一遍啊)那我们不用%x,我们用啥呀?
我们可以使用%p,这样就可以觉得这个 warring 的问题了。
我们的取地址符,只能对变量进行取地址操作,如果说&的后面不是一个变量的话,就不能进行取地址操作。就好比如这样:
&(a+b)?
&a(++)?
&(a–)?
强制类型转换
我们为什么要做强制类型转换?
在C语言编程中,类型转换是一种常见的操作,用于确保数据类型的兼容性和正确性。程序中不同类型的数据可能需要进行不同的处理,而类型转换可以帮助程序员在不同数据类型之间进行灵活转换,从而提高代码的可读性和可维护性。
说人话就是,有时候我们在写代码的时候,会遇到,我想的逻辑是 int 类型,但是得到的结果是一个 float 或者 double 类型的时候,我们就可以使用强制类型转换来让我们的结果正确。
而且,通常是用于我们要把一个量,往小的地方转(就比如我们想要把一个 int 转化为一个 char)
这个我们可以这样理解,假如我现在定义了一个 int 型变量 a,并且赋值了 100.
然后我现在想让这 a 变成 float 类型的变量,那么我就可以使用强制转换了。
格式:
(type_name) expression
(类型)值
注意:
我们需要注意安全性,小的变量不总能表达大的量。
就好比如你把 32768 赋值给了 short 类型。
强制类型转换只是从那个变量,计算出了一个新的类型的值而已,并不是改变了原来的那个变量,无论是值还是类型,他都是不会改变的。
就好比如这样:
我们可以发现 i 的值本身是没有发生变化的。
强制运算的优先级,比四级运算要高,所以编译器会先算强制转换,再算其他的。
自动类型转换
这个其实是我们在学习强制类型转换要衍生的另一个东西。
当运算符的两边出现不一致的类型时,会自动转换成较大的类型。
什么是较大的类型?其实就是能表示数的范围更大
整数和整数在一起比较时,会按照以下的方式来进行转换
char -> short -> int - > long -> long long
如果整数和浮点数在一起比较是,则是这样转换的。
int - > float -> double
C 语言特别的地方在于,任何小于 int 的类似都会被转换成 int float 会被转换成 double
所以,这就是为什么我们在输入 double 的时候,可以直接用%f 而不是%lf。
但是 scanf 就不可以,如果要输入 short 类型的变量的话,就需要使用%hd 了。因为 scanf 需要明确的知道那个数的大小
指针的定义
我们问一个问题?
- 如果能够将取得的变量的地址传递给一个函数,能否通过这个地址在那个函数内访问这个变量?
- scanf( ) 的原型应该是怎么样的?我们需要一个参数能保存别的变量的地址,如果表达能够保存地址的变量?
这个时候我们就要开始引入我们的指针了,指针可以接收取地址符得到的那个地址,并且保存下来。
指针就是用来保存地址的变量。
int i = 10;
int* p;
p = &i;
我们可以这样取理解这个东西
假如,我们有一个变量 i,i 在内存中的地址是0x1fdffc44,然后我们再有一个指针 p,我们让这个指针来保存这个 i 的地址。我们就称为,p 指向了 i。
指针变量
- 变量的值是内存的地址
- 普通变量的值是实际的值
- 指针变量的值,是实际的值的地址
作为参数的指针
当我们想要创建一个函数的时候,但是函数里面的形参确实指针的时候,我们可以这样
void f(Int* p);
//在被调用的时候得到某个变量的值。
int i = 0;f(&i);
//在函数里面可以通过这个指针访问外面这个 i
“*”–> 单目运算符
*是一个单目运算符,用来访问指针的值所表示的地址上的变量。听不懂?我们直接举个例子给你看看吧
我们可以看到我们是*p 输出出来的东西是 a 变量的值,而不是地址。
现在懂了吧。
而且,这个运算符既可以作左值,也可以作右值。
意思是,放到等号的左边,可以用来写入值
放到等号的右边,我们可以用来输出值。
如果你对这个感觉不太清楚的话,我这里可以用这个代码做例子
你是不是感觉怪怪的,诶?为什么这里的值变了呢???
是这样的:
在C语言中,函数参数是通过值传递(pass by value)来传递的。这意味着当你将一个变量作为参数传递给一个函数时,实际上是传递了该变量的一个副本(或称为“值拷贝”)。函数内部对这个副本的任何修改都不会影响到原来的变量。
但是指针不同,因为我们指针储存的是这个变量的地址,我们在第 20 行的操作并不是转的传递,而是直接改变了对应的地址的变量的值。
指针本身存储的是一个内存地址,而不是变量的值的一个副本。当你将一个变量的地址传递给一个函数时(通过指针),函数内部就可以通过这个地址直接访问和修改原始变量。
这个时候我们又得回到 scanf 了。我们在最开始学习 C 语言的时候,应该是写过这样的代码
1 | int i; |
好,我们现在可以站在指针的角度上去思考这个代码的逻辑了。
我们在终端上输入一个数,比如说 6,我们在终端上输入 6,scanf 做的操作并不是将 6 传入到 i 这个变量里面,而是把 6 当作我们传入的地址,然后他又开始拿 6 来做一些其他的事情。但是 6 这个地址是个非常非常小的地址,所以我们运行的时候会出问题。
指针可以干嘛?
我们上文又是取地址的,又是输出地址的,又是改变对应地址的参数的,那我们做这些东西到底是在干嘛啊?指针指向地址有,又可以干嘛?
场景一:
交换两个变量的值:
交换两个变量的值,听着其实已经太熟悉了,我们都知道用一个参数来当作传递的桥梁,但是,如果我们这个操作实在函数里面运行的呢?还是按照原来的方法吗?试试
1 |
|
我们可以发现其实运行的结果并不是我们想要的,原因我们已经说过了。
函数参数是通过值传递的,fun
函数内部对 a
和 b
的修改并不会影响到 main
函数中的变量 i
和 j
。
但是,如果我们用的是指针就不一样了,我们直接修改对应地址的变量的值,然后在直接输出。
我们来试试:
成功转换,指针直接修改对应地址的变量的值。然后在输出出来。
场景二:
- 函数需要返回多个值,某些值就只能通过指针返回。
- 传入的参数实际上是需要保存带回的结果的变量
就好比如这个程序
1 |
|
我们看到这个程序,是一个输出最大最小值的一个程序,一般的返回值只能返回一个结果,如果说,我们两个结果都需要返回的话,我们就需要使用指针了。
然后我们就可以通过我们的minmax函数来修改内存中对应的变量的结果了。
应用场景二b
- 函数返回运算的状态,结果通过执政返回。
- 常用的套路是让执政返回特殊的不属于有效范围内的值来表示出错。
- -1或者0(在文件操作会看到大量的例子)
- 但是当仍和数值都是有效的可能结果是,就得分开返回了。
我们可以看一个例子来学习一下
1 |
|
这是一个非常简单的一个计算商的代码,我们的divide函数有三个参数而且,我们这个函数还有一个返回值,如果可以被除的话就返回1,不能就返回0.
指针常见错误
- 我定义了一个指针变量,但是我还没有只想任何变量,就开始使用指针了。
案例:
1 |
|
我们可以看看他异常
Process finished with exit code -1073741819 (0xC0000005)
这个异常其实就是程序在运行时发生了访问冲突,通常是由于非法内存访问引起的。
我们看看代码,发现我们首先是让我们的*p
去等于0,然后我们又让*p
去指向12。这样就会让我们的程序崩溃。
传入函数的数组变成了什么?
如果我们通过函数的参数将一个数组传入到函数里面去了,那在这个函数李里面,他接收到的是个什么东西?
我们先来看一段代码来做分析
我们知道,如果我们传入的是一个普通的变量的话,参数接收到的是一个值,我们如果传入的是要给指针变量,参数接收到的也是值。只不过这个时候的值是地址
1 |
|
首先第一个问题:
为什么我们这个minmax里面的这个
a[]
不能使用sizeof
来算出他的个数呢?他的sizeof
是多少呢?
这其实是数组退化为指针的问题,当a[]
被传递给minmax
函数时,函数接收的实际上是数组首元素的地址,并不是完整的数组信息。我们可以去再代码添加一些测试代码。
1 |
|
我在main
和minmax
里面都添加了两个printf
,我们来看看他们的输出结果是什么样子的
sizeof(a)=68
main a=000000dd3d9ff980
minmax——sizeof(a)=8
minmax——a=000000dd3d9ff980
min=1,max=55
我们可以发现,我们minmax
里面的a[]
就是我们main
里面的a[]
。
所以在minmax
里面我们的sizeof
不能使用,因为sizeof
返回的是指针变量在内存中占用的字节数,于指针指向的数据是无关的。
如果你是32位系统的话,则通常为4字节
64位则是8字节。
那既然我们main
传入minmax
的数组是指针的话,那我们minmax
里面的int a[]
可以写成是int *a
除了这样写还可以有其他的写法:
- int sum(int *ar,int n)
- int sum(int *,int)
- int sum(int ar[]),int n)
- int sum(int [],int )
上面这几种写法都是等价的。
但是为什么是等价的?
等价是因为与
int sum(int *ar, int n)
的参数类型完全相同,参数名在函数声明中仅起文档作用,不影响编译器判断。
数组变量是特殊的指针
数组变量本身表达的就是地址,所以
- 我们就不需要去使用
&
取地址,int a[10];int *p = a; - 但是数组的单元表达的是变量的话,就需要使用
&
取地址了。 - a == &a[0]
- 我们就不需要去使用
[]
运算符可以对数组做,也可以对指针做:- p[0] <==> a[0],也就像是*p = p[0]一样。
*
运算符可以对指针做,也可以对数组做*a = 25;
我们可以用
printf
去调试看看,printf(“*a = %d”, *a); 最后运行出来后发现结果等于的就是1,因为*a
表示的就是a[0]
数组变量是
const
的指针,不能被赋值
但是我们并没有一个p的数组,那我们的p[0]是什么?
程序以为p指向的地方是一个数组,我们minmax
函数里面计算完后,得到min
的值是1的话,我们就直接将1这个数存储在min
这个变量里面去了。
然后我们的p[0]
,可能就是以为我们这个min
这个变量的地方是一个数组,类似于这样min[1]
*p = 1
p[0] = 1
指针与CONST
只适用于C99
我们知道const
是一个修饰符,它加再变量的前面,使得这个变量不能被修改。
指针也是一个变量,它指向两个东西,一个是指针本身,另一个是指针所指向的那个变量。而且,指针本身可以是const
,指针指向的那个变量也可以是一个const
。
那指针的const
和指针指向的变量的const
有什么区别和联系呢?
指针是const:
表示的是,一旦得到了某个变量的地址,就不能再指向其他的变量了。
1
2
3
4
5
6
7
8
9
10 int * const q = &i; //这里的q就是const,而且q的值是不能被改变的,q的值也就是我们这里i的地址
/*
也就是说,q指向了i的这个事实是不能被改变的,q不能再指向别人了。
*/
*q = 26; //ok
q++; //error
/*
我们可以发现,我们将q去做一些运算的时候就不行了
*/但是,如果我们说我们在定义指针的时候说这个
p
所指向的int是一个const
1
2
3
4 const int *p = &i;
*p = 26; //ERROR
i = 26; //OK
p = &j; //OK我们不能通过
p
去做修改,但是我们的i
可以修改,除非这个i
本身也是一个const
。
我们看看下面这几种写法,再来熟悉一下const
1 | int i ; |
我们如果要判断哪个被const
了的标志就是看const
在*
的前面还是在后面
如果const
在*
号前面,表示指针指向的数据是常量不可修改。也就是说,我们的第二行和第三行代码是一样的意思。
如果const
在*
号的后面,表示指针不能被修改,可以通过指针修改指向的值。
转换
我们总是可以把一个非const
的值转换成const
的,比如说这段代码:
1 | void f(const int* x); // 函数f接受一个指向常量整数的指针 |
非const转const:函数
f
的参数是const int*
,即使a
是非const
的,传递&a
也是合法的。函数内部无法通过x
修改a
的值。const变量不可修改:
b
被声明为const
后,任何修改操作都会导致编译错误。
当要传递的参数的类型比地址大的时候,这是常用的手段,即能用较少的字节数传递值给参数,有可以避免函数对外的变量被修改。
const数组
我们的const
还能用在数组上面。
我们看看这个
const int a[ ] = {1,2,3,4,5,6};
我们知道,数组变量已经是const
的指针了,int
前面的这个const
是为了表明数组的每个单元都是const int
,所以必须通过初始化进行赋值。
保护数组值
因为把数组传入函数时传递的时地址,所以那个函数内部可以修改数组的值。而且为了保护数组不被函数破坏,可以设置参数为CONST
pint sum(const int a[],int len)
这样,我们的函数就不会对a
这个数组做任何的修改了。
指针运算
首先来看一段代码:
1 |
|
输出结果:
p = 000000ebdd5ff826
(p+1) = 000000ebdd5ff827
q = 000000ebdd5ff7f0
(q+1) = 000000ebdd5ff7f4
我们可以发现,p
和(p+1
)的地址的变化,和q
和(q+1)
的地址的变化。
为啥我们q
与(q+1)
之间,我们没有+1
而是+4
了?为啥这个(q+1)
是+4而我们的(p+1)
却是+1
?
因为:
sizeof(char) = 1;
sizeof(int) = 4;
所以我们这里的这个
+1
并不是在地址值上面做+1
而是在地址值上做了一个加一个sizeof
我们知道*p
代表的是ac[0]
,那我们的 *(p+1)
是不是代表的是ac[1]
呢?我们可以写一个printf
来试试看看
printf(“ *(q+1) = %d\n”, *(q+1));
printf(“” *(p+1) = %d\n”, *(p+1));
运行结果:
*(p+1) = 2
*(q+1) = 2
给一个指针加1表示让指针指向下一个变量,但是如果指针不是指向一片连续分配的空间,则这种运算是没有意义的。
指针计算
指针也是可以做算数运算的,我们可以给数组做加减一个整数(+,+=,-,-=)
,而且加是往前移动,减是往后移动
- 加减
- 递增递减
我们可以来试试加减
1 | char *p1 = &ac[5]; |
运行结果:
p1 - p = 5
q1 - q = 6
这就很奇怪了,为什么q1-q=6?我们ai数组中的6上面放的是7.按理我们使用q1-1=7才对。不妨我们现在看看q1等于多少吧
q = 00000010f15ffa50
q1 = 00000010f15ffa68
我们让计算器给我们算算这两个相减等于多少
我们发现10进制那里写的是24,那也就是说,q1-q相减应该等于24。也就是说,我们是两个值相减完之后,除了一个sizeof(int)
,我们知道一个int
是四个字节。
相除之后得到了6.
所以,两个指针相减,我们得到的不是地址的差,而是两个地址的差在除以sizeof(类型)
。这里的6表示的就是可以放6个int类型的东西。
我们在后面的代码里面常见的应该会是这个运算符
*p++
- 去除p所致的那个数据来,完事之后顺便把p移到下一个位置去
- *的优先级虽然高,但是没有++高。
- 常用于数组嘞的连续空间操作
- 在某些CPU上,这可以直接被翻译成一条汇编指令。
指针比较
<,<=,==,>,>=,!=
都可以对指针做。- 比较他们在内存中的地址
- 数组中的单元的地址肯定是线性递增的。
0地址
- 我们的内存中有
0地址
,但是0地址
通常是个不能随便碰的地址 - 所以你的指针不应该具有0值
- 因此可以使用
0地址
来做一些特殊的事情:- 返回的指针是无效的
- 指针没有被真正的初始化(先初始化为0)
NULL
是一个预定定义的负号,表示0地址
- 有的编译器不愿意你用0来表示
0地址
- 有的编译器不愿意你用0来表示
指针的类型
- 无论指向什么类型,所有的指针的大小都是一样的,因为都是地址。
- 但是指向不同类型的指针是不能直接互相赋值的。
- 这是为了避免用错指针。
指针的类型转换
void*
表示不知道指向什么东西的指针。- 计算时与
char*
相同(但不相通)
- 计算时与
- 指针也可以转换类型
- int *p = &i; void *q = (void *)p;
- 这斌没有改变
p
所指的变量的类型,而是让后人用不同的眼光通过p
看他所致的变量。- 我不在当你是
int
,我认为你就是一个void
- 我不在当你是
用指针来做什么
- 当我们需要传入较大的数据的时候,我们用指针作为参数的类型
- 我们传入数组之后,我们可以对那个数组做操作
- 当我们的函数返回不止一个结果的时候,我们可以使用函数做参数来带出结果。
- 当我们需要使用函数来修改不止一个变量的时候,我们可以传入一个指针进去,让他帮我们修改函数里面的值。
- 动态申请内存的时候,也可以用到指针。