前言
各位读者朋友们大家好!上期我们讲完了C语言中的函数,这期我们来讲一下C语言中的指针。指针是C语言最重要也是最常被误解的特性之一,指针也是我们在学习C语言中最大的困难。那接下来我们对指针的内容来逐个击破。
一. 内存和地址
1. 内存
大家都知道内存是用来存储信息的,在计算机中处理数据时,需要的数据是在内存中读取的。在计算机中,内存被划分为一个个小的内存单元,每个小的内存单元的大小是一个字节。
2. 地址
有了内存,我们往内存中存放或读取数据时需要借助内存编号,这个编号就叫做地址。通过地址我们就可以访问到内存空间了。
因此可以这样理解:内存单元的编号 == 地址 == 指针。
二. 指针变量和地址
1. 取地址操作符(&)
在C语言中,变量的创建其实就是在向内存申请空间。
例如,在程序中创建一个整型变量a并初始化为10,那么内存中就会有一块空间来存放a。
并且a的每一个字节都有地址。
那应该如何得到a的地址呢?这就需要用到我们要讲的取地址操作符(&)了。
这样就可以找出a的地址了。
2. 指针变量和解引用操作符(*)
2.1 指针变量
对于变量我们很熟悉了,那什么是指针变量呢?顾名思义,指针变量就是用来存放指针(也就是地址)的变量。
int main()
{
int a = 10;
int* p = &a;//取出a的地址存放在指针变量p中
return 0;
}
像这样,就创建好了一个指针变量p用来存放a的地址。
2.2 如何拆解指针变量类型
上面代码中,指针变量p的类型是int* 类型,那我们如何理解指针类型呢?
我们把int * p拆分了来理解:
- p是变量名
- *说明p是一个指针变量
- int 说明p所指向的内存空间是存储int类型的
如果要创建一个指向char类型的指针应该如何创建呢?char* p
2.3 解引用操作符(*)
我们将地址存放在指针变量中,如果我们要使用内存中的内容要怎么办呢?这就要学习解引用操作符来解决这个问题了。
int main()
{
int a = 10;
int* p = &a;
printf("%d", *p);
return 0;
}
我们创建了a变量,并把它的地址用指针变量p储存,对p解引用我们就拿到了a的值。
对于&和* 我们可以理解成&是一把锁,* 是一把钥匙,通过钥匙打开锁,就能访问到内存中存储的数据了。也可以理解为只要p指向a,那么*p就是a的别名。
通过解引用操作我们也可以修改该指针指向的内存空间中所存储数据的内容。
int main()
{
int a = 10;
int* p = &a;
printf("a==%d\n", a);
*p = 20;
printf("a==%d\n", a);
return 0;
}
a的内容就通过解引用会被修改为了20。
2.4 指针变量的大小
我们知道,一个int类型的大小是4个字节,一个char类型的大小是1个字节,那一个指针变量的占多少字节呢?
在大多数现代计算机系统中,指针变量的大小通常与计算机的字长(word size)或地址总线的位数相同。例如,在32位系统中,指针变量的大小通常是4个字节(32位/8位/字节 = 4字节),因为每个地址需要32位来表示。同样地,在64位系统中,指针变量的大小通常是8个字节(64位/8位/字节 = 8字节)。
确实这样,在编译器中64位环境下是8个字节,32位环境下是4个字节。指针的大小只与环境有关,与指针的类型无关!!!
三. 指针变量类型的意义
上面提到,指针的大小只与环境有关,与指针类型无关,那我们研究指针类型有什么意义呢?
1. 指针的解引用
观察下面的代码,猜猜有什么不一样?
通过调试我们发现:
第一段代码把a的4个字节的内容全部修改成了0;而第二段代码只把第一个字节的内容修改成了0。
因此我们可以得出结论:
指针的类型决定了解引用可以访问几个字节的内容,例如:(char*)类型的指针解引用后只能访问一个字节的内容,而(int*)的指针解引用后可以访问四个字节的内容。
2. 指针加减整数
int main()
{
int a = 0;
char ch = 'a';
int* p = &a;
char* str = (char*)&a;
printf("%p\n", p);
printf("%p\n", p+1);
printf("%p\n", str);
printf("%p\n", str+1);
}
上面带码运行结果如下:
我们可以看出,(int*)类型的指针+1跳过了4个字节也就是一个整型的大小;而一个char*类型的指针+1跳过1个字节,也就是一个字符类型的大小。
指针的类型决定了指针+1或者-1可以跳过的字节的个数。
3. void* 指针
在上篇博客中我们讲到了返回值为void的函数,void表示什么都不返回。但在指针这里,void*类型的指针就不是什么都不是的指针了,它指的是无具体类型的指针。它可以用来接收任何类型的指针,但是不可以进行解引用和加减整数的运算。
int main()
{
int a = 1;
int* p1 = &a;
void* p2 = &a;
return 0;
}
我们对p2解引用看一下会发生什么?
这里编译器报错了,所以我们不能对void类型的指针直接进行解引用操作,可以对void类型进行强制类型转换后再解引用。
四. const修饰指针
1. const修饰变量
我们想要修改b的值,但是b被const修饰无法修改
int main()
{
int a = 0;
const int b = 10;
a = 20;
int* p = &b;
*p = 20;
printf("a==%d\n", a);
printf("b==%d\n", b);
return 0;
}
我们通过取地址解引用的方式修改了b的值,如果我们想通过拿到b的地址也不能通过解引用来修改b的值要怎么做呢?
2. const修饰指针变量
先给结论:
- const 修饰指针变量时放在*的右侧(*const p),限制的是p的内容,也就是指针变量指向的变量不能被修改了,但是可以修改指向的变量的内容。
- const 修饰指针变量时放在*的左侧 (const * p),限制的是(*p)的内容,也就是指针变量指向的变量的内容不能被修改了,但是可以修改指向的变量。
- const在*两侧,指向的变量和变量的内容都不能修改。
我们验证一下
五. 指针运算
1. 指针加减整数
上面讲到了指针的类型决定了+1或者-1可以跳过的字节数,我们还知道数组在内存中是连续存储的,是不是我们拿到了数组首元素的地址然后通过指针运算再解引用就能访问整个数组呢?
上代码试试!
int main()
{
int arr[10] = { 9,8,7,6,5,4,3,2,1,0 };
int* p = &arr;
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", *(p+i));
}
return 0;
}
确实这样!
2. 指针-指针
这里的整数和指针类型有关,如果是int类型的指针,+整数就是跳过该整数个元素。(例如,int类型的指针+6,就是跳过6个整型;char*类型+6,就是跳过6个字符类型)
int main()
{
int arr[10] = { 9,8,7,6,5,4,3,2,1,0 };
int* p1 = &arr[0];
int i = 0;
int* p2 = p1+6;
int ret = p2 - p1;
printf("%d ", ret);
return 0;
}
|指针-指针|得到的结果是两个指针之间的元素个数,前提是这两个指针指向同一块空间。
3. 指针的关系运算(指针大小的比较)
int main()
{
int arr[10] = { 9,8,7,6,5,4,3,2,1,0 };
int* p1 = &arr[0];
while (p1 < arr + 10)
{
printf("%d ", *p1);
p1++;
}
return 0;
}
这样同样可以访问到整个数组。
六. 野指针
野指针是指指针指向的位置是不可知的、随机的、不确定的、没有明确限制的指针。
1. 野指针成因
1.1 指针未初始化
int main()
{
int* p;
*p = 20;
return 0;
}
1.2 指针越界访问
int main()
{
int arr[10] = { 0,1,2,3,4,5,6,7,8,9 };
int* p = &arr;
for (int i = 0; i <= 10; i++)//这里进行11次遍历,第11次时p就成了野指针
{
printf("%d ", *(p + i));
}
return 0;
}
1.3 指针指向的空间被释放
int* Func()
{
int c = 0;
return &c;
}
int main()
{
int* ret = Func();
printf("%p", ret);
return 0;
}
这里Func函数走到return语句后就被销毁了,c的空间就被释放了,指针再指向c的地址就是野指针。
2. 如何规避野指针
2.1 指针初始化(NULL)
如果我们明确知道指针指向的地址就直接赋值指向的地址,如果不知道就赋值NULL。NULL是C语言中定义的标识符常量,值是0,地址也是0,我们无法访问0地址,读写该地址会报错。
初始化如下:
int main()
{
int a = 10;
int* p1 = &a;
int* p2 = NULL;
return 0;
}
2.2 小心指针越界
要注意程序向内存申请了多少空间,通过指针访问时不能超过这些空间,如果超过了就是越界访问。
2.3 指针不用时及时置为NULL
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = &arr;
for (int i = 0; i < 10; i++)
{
printf("%d ", *(p + i));
}
p = NULL;
return 0;
}
2.4 避免返回局部变量的地址
七. assert断言
<assert.h>头文件定义了宏assert(),用于运行时确保程序符合指定条件,如果不符合就会报错并且终止运行。
#include <assert.h>
int Add(int x, int y)
{
assert(x > 0 && y > 0);//断言两个值均为非负值
return x + y;
}
int main()
{
int ret = Add(3, -2);
printf("%d", ret);
return 0;
}
assert()的使用对程序员是非常友好的,使用 assert()有几个好处:它不仅能自动标识文件和出问题的行号,还有一种无需更改代码就能开启或关闭 assert()的机制。如果已经确认程序没有问题,不需要再做断言,就在 #include<assert.h>语句的前面,定义一个宏 NDEBUG。
#define NDEBUG
#include <assert.h>
如果程序又出问题可以把#define NDEBUG删去或者注释掉,再次编译就又可以使用assert断言了。
assert的缺点是引入了额外的检查,增加了程序运行时间。assert一般在Debug中使用,在Release版本中assert会被优化掉。
八. 传值调用和传址调用
1. 传值调用
int Add(int x, int y)
{
return x + y;
}
int main()
{
int a = 3;
int b = 2;
int ret= Add(a,b);
printf("%d", ret);
return 0;
}
void Sweap(int x, int y)
{
int tmp = x;
x = y;
y = tmp;
}
int main()
{
int a = 10;
int b = 20;
Sweap(a, b);
printf("%d ", a);
printf("%d ", b);
}
我想通过Sweap函数来交换a和b的值,但是运行结果没有交换这是为什么呢?
通过监视,可以看到在Sweap函数中,只改变了x和y的值,并没有改变a和b的值,我们传过去的是a和b的值,sweap函数用x和y来接收,并向内存申请了一块空间来存放a和b的值,x和y是a和b的一份临时拷贝,修改x和y并不会影响实参ab。
只进行了形参的交换,但没有对实参产生影响。 如果想用Sweap函数完成两个数的交换应该如何做呢?
2. 传址调用
在上面的内容中讲到,通过对指针解引用可以修改指针变量指向的空间的内容。那我们是不是可以通过传过ab的地址来完成交换呢?
void Sweap(int* x, int* y)
{
int tmp = *x;
*x = *y;
*y = tmp;
}
int main()
{
int a = 10;
int b = 20;
Sweap(&a, &b);
printf("%d ", a);
printf("%d ", b);
}
这样确实改变了a和b的值。通过图片了解一下:
传址调用可以将被调函数和主调函数之间建立真正的联系,可以在被调函数中修改主调函数中的变量的值。
- 当我们只需要主调函数中的值参与而不需要改变主调函数中变量的值时,我们可以采用传值调用。
- 当需要修改主调函数中变量的值时,就需要用传址调用。
以上就讲完了指针第一讲的内容,希望对大家有帮助,也欢迎大家指出错误。