文章目录
一、指针类型的意义
●既然指针变量的大小和类型无关,只要是指针变量,在同一个平台下,大小都是一样的,那为什么还要有各种各样的指针类型呢?
我们通过下面的例子来理解指针类型的意义。
1.指针的解引用
对比下面2段代码,在调试时观察内存及变量值的变化。
#include<stdio.h>
//代码1:
int main()
{
int n = 0x11223344;
int* p = &n;
*p = 0;
return 0;
}
在代码1中,通过逐过程(F10)调试,由上图可知,系统为整型变量n申请了4个字节的空间存放值0x11223344,整型变量n的地址0x010FFAAC存放在了指针变量p中,通过对指针p解引用,将变量n的值赋为0,由上图可知,内存中原先4个字节存储的0x11223344也全部变为0了。
#include<stdio.h>
//代码2:
int main()
{
int n = 0x11223344;
char* p = (char*)&n;
*p = 0;
return 0;
}
在代码2中,还是同样的整型变量n,n中存储的还是0x11223344这个值,通过逐语句(F10)调试起来,我们发现,变量n的地址和代码1中的地址不一样,虽然是同样的变量同样的值,但是当vs编译器再次调试起来的时候,系统会重新为变量n分配内存空间,内存空间的地址也就不一样。随着程序运行结束,这块空间会释放,还给操作系统。待下一次程序运行时,会重新为变量分配空间。
但是这里将变量n的地址存储在了char*类型的指针变量中,而变量n的地址又是一个int*类型的指针,所以这里就将n的地址强制类型转换为了char*的指针,再对指针p解引用将n的值赋为0,发现只有n的第一个字节被改为了0,其他三个字节的值没变。这跟代码1中的不一样,代码1中是将n的4个字节的值全部改为了0。
●所以这里得到一个结论:
指针的类型决定了,对指针解引时有多大的权限(一次能访问几个字节)。
像char*的指针解引用就只能访问一个字节,而int* 的指针解引用访问四个字节。
2.指针的运算
(1) 指针±整数
先看一段代码:
#include<stdio.h>
int main()
{
int n = 10;
char* pc = (char*)&n;
int* pi = &n;
printf("&n = %p\n", &n);
printf("pc = %p\n", pc);
printf("pc+1 = %p\n", pc + 1);
printf("pi = %p\n", pi);
printf("pi+1 = %p\n", pi + 1);
return 0;
}
程序运行结果:
由上图可以看出,char* 类型的指针变量+1跳过1个字节,int*类型的指针变量+1跳过了4个字节。这就是指针变量的类型差异带来的变化。指针加1,是加了一个指针所指向对象类型的大小(单位字节),而不是地址直接加一个1。所以指针加上一个整数,表示指针加上这个整数乘以指针所指向对象类型的大小,即:指针+整数*sizeof(类型)。而指针减去一个整数也是一样的道理,即指针-整数*sizeof(类型)。
结论:指针的类型决定了指针向前或者向后走一步有多大(距离)。
需要注意的是在数组中,指针±整数可能会出现越界访问的问题。
(2) 指针-指针
指针与指针之间不适合做加法运算,因为地址加地址没有意义,但是指针减指针就不一样了,指针减指针,得到是两个指针之间的元素个数,但前提是这两个指针一定要指向同一个数组:
#include<stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,0 };
int* p1 = &arr[0];
int* p2 = &arr[9];
printf("%d\n", p2 - p1);
return 0;
}
程序运行结果:
(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;
}
程序运行的结果:
二、void* 类型的指针
在指针类型中有一种特殊的类型叫void *类型,可以理解为无具体类型的指针(或者叫泛型指针),这种类型的指针可以用来接受任意类型的指针(地址)。但是也有局限性,void*类型的指针不能直接进行指针±整数和解引用的运算。
例如:
#include<stdio.h>
int main()
{
int n = 10;
int* pi = &n;
char* pc = &n;
return 0;
}
在上面的代码中,创建了一个整型变量n,那它的地址就要用int*类型的指针变量来存储,上面指针变量pi是int*类型的,用来存储变量n的地址没有问题,但是pc是char*类型的指针变量,用来存储整型变量n的地址,编译器就会报警告,因为类型不兼容。
如果使用void* 类型的指针变量来接收n的地址就不会出现上面的警告:
#include<stdio.h>
int main()
{
int n = 10;
int* pi = &n;
void* p = &n;
return 0;
}
但是对void*类型的指针解引用,编译器就会直接报错:
#include<stdio.h>
int main()
{
int n = 10;
int* pi = &n;
void* p = &n;
*p = 0;
return 0;
}
可以看到上面是在vs2022编译器中运行的结果,void*类型的指针可以接收不同类型的地址,但是无法直接对其进行解引用。
那么void*类型的指针到底有什么用呢?
一般void*类型的指针是使用在函数参数的部分,用来接收不同类型数据的地址,这样的设计可以实现泛型编程的效果。使得一个函数用来处理多种类型的数据,后面还会深入讲解这部分的知识。
三、const修饰的指针
1.const修饰变量
变量是可以修改的,如果把变量的地址交给一个指针变量,通过指针变量的解引用也可以修改这个变量。但是如果我们希望一个变量加上一些限制,不能被修改,那要怎么做呢?
这就要使用const来修饰变量:
#include<stdio.h>
int main()
{
int m = 0;
m = 10;//m可以被修改
const int n = 0;
n = 20;//n不可以被修改
return 0;
}
上面的代码运行后会报错,由上图可知,变量n被const修饰,n就不能被修改了,n本质上是变量,只不过被const修饰后,在语法上就加了限制,只要我们在代码中直接对n进行修改,就不符合语法规则,就会报错,致使没法直接修改n。
那有没有其他办法可以修改被const修饰的变量的值呢?
如果不直接通过n去赋值,而通过n的地址,去修改n的值就可以做到。这样做是在打破语法规则。
#include<stdio.h>
int main()
{
int n = 0;
printf("n = %d\n", n);
int* p = &n;
*p = 10;
printf("n = %d\n", n);
return 0;
}
程序运行的结果:
可以看到通过这样的方法确实能把n的值改掉,我们知道const修饰n就是为了让n的值不被修改,那如果p拿到n的地址就能修改n,这样就打破了const的限制,这是不合理的,所以应该让p即使拿到n的地址也不能修改n。看下面:
2.const修饰指针变量
const修饰指针变量,可以放在*的左边,也可以放在*的右边,但是意义不一样:
int * p; //没有const修饰
const int * p; //const放在*的左边做修饰
int * const p; //const放在*的右边做修饰
那const放在*号左边或是右边,具体的作用是什么?通过下面四段代码来具体学习:
#include<stdio.h>
//代码1:无const修饰的情况
int main()
{
int n = 10;
int m = 20;
int * p = &n;
*p = 0;
p = &m;
return 0;
}
由上图的运行结果可知,运行成功,这可以知道在没有对变量进行const修饰时,无论是对指针p解引用,还是改变指针变量p中存储的地址,都没有问题。
#include<stdio.h>
//代码2:const放在*的左边
int main()
{
int n = 10;
int m = 20;
const int * p = &n;
*p = 0;
p = &m;
return 0;
}
由上图,代码2运行的结果报错,出错的是*p = 0这条语句,而p = &m这条语句没有问题。
#include<stdio.h>
//代码3:const放在*的右边
int main()
{
int n = 10;
int m = 20;
int * const p = &n;
*p = 0;
p = &m;
return 0;
}
由上图,代码3运行的结果报错,出错的是p = &m这条语句,而*p = 0这条语句没有问题。
#include<stdio.h>
//代码4:*的左右两边都有const
int main()
{
int n = 10;
int m = 20;
int const * const p = &n;
*p = 0;
p = &m;
return 0;
}
由上图可知,代码4中,在*的左右两边都加上const修饰的后,程序运行的结果报错,出错的是 p = &m 和 *p = 0 这两条语句。
结论上面的情况:const修饰指针变量p的时候
●const如果放在*的左边,修饰的是指针变量p所指向的内容,表示指针变量p所指向的内存单元里面的内容不可变,但是指针变量p是可变的。
●const如果放在*的右边,修饰的是指针变量p,所以指针变量p不可变,但指针变量p所指向的内容是可变的。
●const如果既放在*的左边,又放在*的右边,那就既修饰指针变量p指向的内容,也修饰指针变量p,指针变量p所指向的内容和指针变量p都不能被修改。
再补充一点:const放在*的左边时,无论是放在类型的前面,还是放在类型的后面都是等价的。
四、指针在函数中的使用
1.函数的传值调用
现在写一个swap函数,用来交换两个整型变量的值,一般我们会想到的方法是:
#include<stdio.h>
void swap1(int x, int y)
{
int tmp = x;
x = y;
y = tmp;
}
int main()
{
int a = 10;
int b = 20;
printf("交换前:a=%d b=%d\n", a, b);
swap1(a, b);
printf("交换后:a=%d b=%d\n", a, b);
return 0;
}
运行结果:
通过上面的代码及运行结果发现,a和b的值并没有交换,这是为什么呢?我们通过逐语句(F11)调试来看:
我们发现在main函数内部,创建了变量a和b,a的地址是0x005cfe6c,b的地址是0x005cfe60,在调用swap1函数时,将a和b传递给了swap1函数,在swap1函数内部创建了形参x和y接收a和b的值,但是x的地址是0x005cfd88,y的地址是0x005cfd8c,x和y确实接收到了a和b的值,不过x的地址和a的地址不一样,y的地址和b的地址也不一样,相当于x和y是独立的空间,那么在swap1函数内部交换x和y的值,自然不会影响a和b,当swap1函数调用结束后回到main函数,a和b的值就没法交换。swap1函数在使用的时候,是把实参a、b的值赋给了形参x、y,这种调用函数的方式叫做传值调用。
结论:
函数的传值调用:是直接将实参的值传递给形参,实参传递给形参的时候,形参会单独创建一份临时空间来接收实参,即形参此时是实参的一份临时拷贝,对形参的修改不会影响实参。
2.函数的传址调用
对于上面通过函数交换两个变量的值,如果函数是传值调用的话,形参和实参之间就没有建立真正的联系。不能够交换两个变量的值,那怎么办呢?
我们现在要解决的就是当调用swap函数的时候,swap函数内部操作的就是main函数中的a和b,直接将a和b的值交换。那么这里就可以使用指针,在main函数中将a和b的地址传递给swap函数,swap函数里边通过地址间接的操作main函数中的a和b,就可以达到交换的效果了。
#include<stdio.h>
void swap2(int* px, int* py)
{
int tmp = *px;
*px = *py;
*py = tmp;
}
int main()
{
int a = 10;
int b = 20;
printf("交换前:a=%d b=%d\n", a, b);
swap2(&a, &b);
printf("交换后:a=%d b=%d\n", a, b);
return 0;
}
运行结果:
由上图可看出,通过调用swap2函数成功的将a和b的值交换了。通过将变量的地址(指针)传递给函数,让函数和函数外边的变量建立起了联系,也就是函数内部可以直接操作函数外部的变量。像这种将变量的地址传递给函数的方式,叫做传址调用。
结论:
函数的传址调用:是把函数外部创建变量的地址传递给函数参数的一种调用函数的方式。
所以在创建和调用函数时,如果只是需要主调函数中的变量值来实现计算,就可以采用传值调用。如果想要在函数内部修改主调函数中的变量的值,就需要传址调用。