指针

我们在讲什么是指针之前,我们先说一下指针中的一个必用参数

sizeof()

sizeof 是一个是一个运算符,给出某个类型或者变量在内存中所占据的字节数。就比如说

sizeof(int) – ->int 类型在内存中占据的字节数

sizeof(i) —> i 变量在内存中占据的字节数

1
2
3
4
5
6
7
8
9
#include "stdio.h"

int main()
{
int a;
a = 6;
printf("%ld", sizeof(int));
return 0;
}

运行结果:

sizeof(int)=4

我们知道,一个字节是 8bit,那一个 int4 个字节,也就是 32 个 bit。

如果说我们把 int 修改为 double 的话,那么就变成了 sizeof(double)=8 了。

运算符 &

熟悉吗?是不是很熟悉?我们在 scanf 里面是不是都会用到&符号

scanf(“%d”,&i);

那么这个&符号到底是干嘛的?

用来获取变量的地址的,&符会把这个变量的地址给你。

地址?什么地址?为啥一个变量会有地址呢?

在 C 语言中,我们所有的变量都是放在内存里面的,就好比如 int,int 在内存中占用了 4 个字节,你看,他既然占用了一定的地方,那他是不是就得有一个地址。所以&符就是把那个变量的地址告诉你,所以我们也把&符叫做取地址符

哪地址长什么样子呢?

我们可以用一段代码来看看

img

我们发现输出了 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 类型。

强制类型转换只是从那个变量,计算出了一个新的类型的值而已,并不是改变了原来的那个变量,无论是值还是类型,他都是不会改变的。

就好比如这样:
img

我们可以发现 i 的值本身是没有发生变化的。

强制运算的优先级,比四级运算要高,所以编译器会先算强制转换,再算其他的。

自动类型转换

这个其实是我们在学习强制类型转换要衍生的另一个东西。

当运算符的两边出现不一致的类型时,会自动转换成较大的类型。

什么是较大的类型?其实就是能表示数的范围更大

整数和整数在一起比较时,会按照以下的方式来进行转换

char -> short -> int - > long -> long long

如果整数和浮点数在一起比较是,则是这样转换的。

int - > float -> double

C 语言特别的地方在于,任何小于 int 的类似都会被转换成 int float 会被转换成 double

所以,这就是为什么我们在输入 double 的时候,可以直接用%f 而不是%lf。

但是 scanf 就不可以,如果要输入 short 类型的变量的话,就需要使用%hd 了。因为 scanf 需要明确的知道那个数的大小

指针的定义

我们问一个问题?

  1. 如果能够将取得的变量的地址传递给一个函数,能否通过这个地址在那个函数内访问这个变量?
  2. scanf( ) 的原型应该是怎么样的?我们需要一个参数能保存别的变量的地址,如果表达能够保存地址的变量?

这个时候我们就要开始引入我们的指针了,指针可以接收取地址符得到的那个地址,并且保存下来。

指针就是用来保存地址的变量。

int i = 10;

int* p;

p = &i;

我们可以这样取理解这个东西

假如,我们有一个变量 i,i 在内存中的地址是0x1fdffc44,然后我们再有一个指针 p,我们让这个指针来保存这个 i 的地址。我们就称为,p 指向了 i。

指针变量

  1. 变量的值是内存的地址
  2. 普通变量的值是实际的值
  3. 指针变量的值,是实际的值的地址

作为参数的指针

当我们想要创建一个函数的时候,但是函数里面的形参确实指针的时候,我们可以这样

void f(Int* p);

//在被调用的时候得到某个变量的值。

int i = 0;f(&i);

//在函数里面可以通过这个指针访问外面这个 i

“*”–> 单目运算符

*是一个单目运算符,用来访问指针的值所表示的地址上的变量。听不懂?我们直接举个例子给你看看吧

img

我们可以看到我们是*p 输出出来的东西是 a 变量的值,而不是地址。

现在懂了吧。

而且,这个运算符既可以作左值,也可以作右值。

意思是,放到等号的左边,可以用来写入值

放到等号的右边,我们可以用来输出值。

如果你对这个感觉不太清楚的话,我这里可以用这个代码做例子

img

你是不是感觉怪怪的,诶?为什么这里的值变了呢???

是这样的:

在C语言中,函数参数是通过值传递(pass by value)来传递的。这意味着当你将一个变量作为参数传递给一个函数时,实际上是传递了该变量的一个副本(或称为“值拷贝”)。函数内部对这个副本的任何修改都不会影响到原来的变量。

但是指针不同,因为我们指针储存的是这个变量的地址,我们在第 20 行的操作并不是转的传递,而是直接改变了对应的地址的变量的值。

指针本身存储的是一个内存地址,而不是变量的值的一个副本。当你将一个变量的地址传递给一个函数时(通过指针),函数内部就可以通过这个地址直接访问和修改原始变量。

这个时候我们又得回到 scanf 了。我们在最开始学习 C 语言的时候,应该是写过这样的代码

1
2
int i;
scanf("%d",i);

好,我们现在可以站在指针的角度上去思考这个代码的逻辑了。

我们在终端上输入一个数,比如说 6,我们在终端上输入 6,scanf 做的操作并不是将 6 传入到 i 这个变量里面,而是把 6 当作我们传入的地址,然后他又开始拿 6 来做一些其他的事情。但是 6 这个地址是个非常非常小的地址,所以我们运行的时候会出问题。

指针可以干嘛?

我们上文又是取地址的,又是输出地址的,又是改变对应地址的参数的,那我们做这些东西到底是在干嘛啊?指针指向地址有,又可以干嘛?

场景一:

交换两个变量的值:

交换两个变量的值,听着其实已经太熟悉了,我们都知道用一个参数来当作传递的桥梁,但是,如果我们这个操作实在函数里面运行的呢?还是按照原来的方法吗?试试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>

void fun(int a,int b);

int main()
{
int i ;
int j ;
scanf("%d",&i);
scanf("%d", &j);
fun(i,j);
printf("%d %d\n", i, j);
return 0;
}

void fun(int a,int b)
{
//两数交换
int temp = 0;
temp = a;
a = b;
b = temp;
}

img

我们可以发现其实运行的结果并不是我们想要的,原因我们已经说过了。

函数参数是通过值传递的,fun 函数内部对 ab 的修改并不会影响到 main 函数中的变量 ij

但是,如果我们用的是指针就不一样了,我们直接修改对应地址的变量的值,然后在直接输出。

我们来试试:

img

成功转换,指针直接修改对应地址的变量的值。然后在输出出来。

场景二:

  1. 函数需要返回多个值,某些值就只能通过指针返回。
  2. 传入的参数实际上是需要保存带回的结果的变量

就好比如这个程序

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>

void minmax(int a[], int len, int *min, int *max); // 修正参数顺序

int main(void) {
int a[] = {1,2,3,4,5,6,7,8,9,12,13,14,16,17,21,23,55};
int min, max;
minmax(a, sizeof(a)/sizeof(a[0]), &min, &max);
printf("min=%d, max=%d\n", min, max); // 输出结果
return 0;
}

void minmax(int a[], int len, int *min, int *max) {
*min = *max = a[0];
for (int i = 1; i < len; i++) {
if (a[i] < *min) *min = a[i];
if (a[i] > *max) *max = a[i];
}
}

我们看到这个程序,是一个输出最大最小值的一个程序,一般的返回值只能返回一个结果,如果说,我们两个结果都需要返回的话,我们就需要使用指针了。

然后我们就可以通过我们的minmax函数来修改内存中对应的变量的结果了。

应用场景二b

  1. 函数返回运算的状态,结果通过执政返回。
  2. 常用的套路是让执政返回特殊的不属于有效范围内的值来表示出错。
    1. -1或者0(在文件操作会看到大量的例子)
  3. 但是当仍和数值都是有效的可能结果是,就得分开返回了。

我们可以看一个例子来学习一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <stdio.h>

/**
* @return 如果除法成功,返回1;否则返回0
*/
int divide(int a, int b, int *result);

int main(void)
{
int a = 5;
int b = 2;
int c;
if (divide(a, b, &c))
{
printf("%d/%d=%d\n", a, b, c); // 修正:$d -> %d
}
return 0;
}

int divide(int a, int b, int *result)
{
int ret = 1;
if (b == 0)
{
ret = 0;
}
else {
*result = a / b;
}
return ret;
}

这是一个非常简单的一个计算商的代码,我们的divide函数有三个参数而且,我们这个函数还有一个返回值,如果可以被除的话就返回1,不能就返回0.

指针常见错误

  1. 我定义了一个指针变量,但是我还没有只想任何变量,就开始使用指针了。

案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <stdio.h>

void f(int *p);
void g(int k);

int main(void)
{
int i = 6;
int *p = 0;
int k;
k = 12;
*p = 12;
printf("&i=%p\n", (void *)&i); // 修正:转义字符错误,&i=6% 应为 &i=%p
f(&i);
g(i);

return 0;
}

void f(int *p)
{
printf(" p=%p\n", (void *)p); // 修正:转义字符错误,p=6% 应为 p=%p
printf(" *p=%d\n", *p);
*p = 26;
}

void g(int k)
{
printf("k=%d\n", k);
}

我们可以看看他异常

​ Process finished with exit code -1073741819 (0xC0000005)

这个异常其实就是程序在运行时发生了访问冲突,通常是由于非法内存访问引起的。

我们看看代码,发现我们首先是让我们的*p去等于0,然后我们又让*p去指向12。这样就会让我们的程序崩溃。

传入函数的数组变成了什么?

如果我们通过函数的参数将一个数组传入到函数里面去了,那在这个函数李里面,他接收到的是个什么东西?

我们先来看一段代码来做分析

我们知道,如果我们传入的是一个普通的变量的话,参数接收到的是一个值,我们如果传入的是要给指针变量,参数接收到的也是值。只不过这个时候的值是地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <stdio.h>

void minmax(int a[], int len, int *max, int *min);

int main(void)
{
int a[] = {1,2,3,4,5,6,7,8,9,12,13,14,16,17,21,23,55};
int min, max;
minmax(a, sizeof(a)/sizeof(a[0]), &min, &max);
printf("min=%d,max=%d\n", min, max);

return 0;
}

void minmax(int a[], int len, int *min, int *max)
{
int i;
*min = *max = a[0];
for (i = 1; i < len; i++) {
if (a[i] < *min) {
*min = a[i];
}
if (a[i] > *max) {
*max = a[i];
}
}
}

首先第一个问题:

为什么我们这个minmax里面的这个a[]不能使用sizeof来算出他的个数呢?他的sizeof是多少呢?

这其实是数组退化为指针的问题,当a[]被传递给minmax函数时,函数接收的实际上是数组首元素的地址,并不是完整的数组信息。我们可以去再代码添加一些测试代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <stdio.h>

void minmax(int a[], int len, int *max, int *min);

int main(void)
{
int a[] = {1,2,3,4,5,6,7,8,9,12,13,14,16,17,21,23,55};
int min, max;
printf("sizeof(a)=%lu\n", sizeof(a));
printf("main a=%p\n", a);
minmax(a, sizeof(a)/sizeof(a[0]), &min, &max);
printf("min=%d,max=%d\n", min, max);

return 0;
}

void minmax(int a[], int len, int *min, int *max)
{
int i;
printf("minmax——sizeof(a)=%lu\n", sizeof(a));
printf("minmax——a=%p\n", a);
*min = *max = a[0];
for (i = 1; i < len; i++) {
if (a[i] < *min) {
*min = a[i];
}
if (a[i] > *max) {
*max = a[i];
}
}
}

我在mainminmax里面都添加了两个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

除了这样写还可以有其他的写法:

  1. int sum(int *ar,int n)
  2. int sum(int *,int)
  3. int sum(int ar[]),int n)
  4. int sum(int [],int )

上面这几种写法都是等价的。

但是为什么是等价的?

等价是因为与 int sum(int *ar, int n)参数类型完全相同,参数名在函数声明中仅起文档作用,不影响编译器判断。

数组变量是特殊的指针

  1. 数组变量本身表达的就是地址,所以

    1. 我们就不需要去使用&取地址,int a[10];int *p = a;
    2. 但是数组的单元表达的是变量的话,就需要使用&取地址了。
    3. a == &a[0]
  2. []运算符可以对数组做,也可以对指针做:

    1. p[0] <==> a[0],也就像是*p = p[0]一样。
  3. *运算符可以对指针做,也可以对数组做

    1. *a = 25;

      我们可以用printf去调试看看,printf(“*a = %d”, *a); 最后运行出来后发现结果等于的就是1,因为 *a表示的就是a[0]

  4. 数组变量是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
2
3
4
int i ;
const int* p1 = &i;
int const* p2 = &i;
int *const p3 = &i;

我们如果要判断哪个被const了的标志就是看const*的前面还是在后面

如果const*前面,表示指针指向的数据是常量不可修改。也就是说,我们的第二行和第三行代码是一样的意思。

如果const*号的后面,表示指针不能被修改,可以通过指针修改指向的值。

转换

我们总是可以把一个非const的值转换成const的,比如说这段代码:

1
2
3
4
5
6
7
void f(const int* x);  // 函数f接受一个指向常量整数的指针

int a = 15;
f(&a); // 正确:将非const变量a的地址传递给const指针
const int b = a;
f(&b); // 正确:将const变量b的地址传递给const指针
b = a + 1; // 错误:const变量不可修改
  • 非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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>

int main(void)
{
char ac[] = {1,2,3,4,5,6,7,8,9,10};
char *p = &ac;
printf("p = %p\n", p);
printf("(p+1) = %p\n", p+1);

int ai[] = {1,2,3,4,5,6,7,8,9,10};
int *q = &ai;
printf("q = %p\n", q);
printf("(q+1) = %p\n", q+1);
return 0;
}

输出结果:
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. 加减
  2. 递增递减

我们可以来试试加减

1
2
3
4
5
char *p1 = &ac[5];
printf("p1 - p = %d\n", p1 - p);

int *q1 = &ai[6];
printf("q1 - q = %d\n", q1 - q);

运行结果:

p1 - p = 5

q1 - q = 6

这就很奇怪了,为什么q1-q=6?我们ai数组中的6上面放的是7.按理我们使用q1-1=7才对。不妨我们现在看看q1等于多少吧

q = 00000010f15ffa50
q1 = 00000010f15ffa68

我们让计算器给我们算算这两个相减等于多少

image-20250330001625569

我们发现10进制那里写的是24,那也就是说,q1-q相减应该等于24。也就是说,我们是两个值相减完之后,除了一个sizeof(int),我们知道一个int是四个字节。

相除之后得到了6.

所以,两个指针相减,我们得到的不是地址的差,而是两个地址的差在除以sizeof(类型)。这里的6表示的就是可以放6个int类型的东西。

我们在后面的代码里面常见的应该会是这个运算符

*p++

  1. 去除p所致的那个数据来,完事之后顺便把p移到下一个位置去
  2. *的优先级虽然高,但是没有++高。
  3. 常用于数组嘞的连续空间操作
  4. 在某些CPU上,这可以直接被翻译成一条汇编指令。

指针比较

  1. <,<=,==,>,>=,!=都可以对指针做。
  2. 比较他们在内存中的地址
  3. 数组中的单元的地址肯定是线性递增的。

0地址

  1. 我们的内存中有0地址,但是0地址通常是个不能随便碰的地址
  2. 所以你的指针不应该具有0值
  3. 因此可以使用0地址来做一些特殊的事情:
    1. 返回的指针是无效的
    2. 指针没有被真正的初始化(先初始化为0)
  4. NULL是一个预定定义的负号,表示0地址
    1. 有的编译器不愿意你用0来表示0地址

指针的类型

  1. 无论指向什么类型,所有的指针的大小都是一样的,因为都是地址。
  2. 但是指向不同类型的指针是不能直接互相赋值的。
  3. 这是为了避免用错指针。

指针的类型转换

  1. void* 表示不知道指向什么东西的指针。
    1. 计算时与char* 相同(但不相通)
  2. 指针也可以转换类型
    1. int *p = &i; void *q = (void *)p;
  3. 这斌没有改变p所指的变量的类型,而是让后人用不同的眼光通过p看他所致的变量。
    1. 我不在当你是int,我认为你就是一个void

用指针来做什么

  1. 当我们需要传入较大的数据的时候,我们用指针作为参数的类型
  2. 我们传入数组之后,我们可以对那个数组做操作
  3. 当我们的函数返回不止一个结果的时候,我们可以使用函数做参数来带出结果。
    1. 当我们需要使用函数来修改不止一个变量的时候,我们可以传入一个指针进去,让他帮我们修改函数里面的值。
  4. 动态申请内存的时候,也可以用到指针。