指针(一)
指针第一曲即将开讲,别走神~
1. 内存
在计算机中,为了高效管理内存空间,就把内存空间划分为一个个的小内存单元,一个内存单元占用一个字节(1 byte = 8 bit )。
举个简单的例子,一栋宿舍楼就是整个内存,每一间学生宿舍就是一个内存单元( 1 byte ),学生宿舍里面的床位就是比特位( bit )。
2. 取地址操作符(&)
现在大多数人都有网购的行为,而想要拿到网购的物品,就需要商家通过快递的方式邮寄。邮寄最重要的一点就是填写地址,快递员会根据地址送到指定位置。计算机也同样会通过地址对变量进行操作。(创建变量就是向内存申请空间,变量存放的位置就是地址)
所以可以说指针就是地址
想要查看变量的地址,就要用到取地址操作符( & ),看到 & 可不要忘记操作符中的双目操作符 & (按位与),逻辑运算符逻辑与( && )。自己要注意区分哦
下面看代码:
#include<stdio.h>
int main()
{
int x = 1;
&x; //取 x 的地址
printf("%p", &x); //%p是打印地址的占位符
return 0;
}
运行结果:
00000044C47BF594
图解:
创建一个整型变量( int ),向内存申请一块空间,占用四个字节,而 x 取地址取的是地址较小的地址,并不会全部取出(计算机会根据取到的地址接着向后访问,访问到 4 个字节)。
3. 指针变量
当我们取了地址之后,为了方便后续的使用,就需要创建变量存储这个地址,这个变量叫做指针变量。
指针变量是变量,作用是存放地址,一般表现形式为:
类型 * 变量名
例如:int* p 、char* p 、void* p ,重点是变量名前的 *。
#include<stdio.h>
int main()
{
int x = 1;
int* p = &x; //存放 x 的地址
printf("%p", p); // p 中是 x 的地址
return 0;
}
3.1. 解引用操作符( * )
当我们有了变量的地址,要找到该变量就轻而易举了,更改变量的值也可以实现,不过需要借助解引用操作符( * )。
#include<stdio.h>
int main()
{
int x = 1;
int* p = &x;
*p = 666; //解引用 p 赋值
printf("*p=%d\n", *p);
printf(" x=%d\n", x);
return 0;
}
输出结果为:
*p=666
x=666 // *p = x
由此可见,对地址解引用之后就可以对该地址所对应的变量进行赋值了。
3.2 指针变量大小
指针变量作为一个变量,肯定是有大小的。但是指针变量的大小与类型无关,指针类型在相同平台上,大小是相同的。
#include<stdio.h>
int main()
{
printf("%zd\n", sizeof(short*));//sizeof 计算字节数
printf("%zd\n", sizeof(char*));
printf("%zd\n", sizeof(int*));
printf("%zd\n", sizeof(double*));
return 0;
}
32位平台运行结果:
4
4
4
4
64位平台运行结果:
8
8
8
8
4. const
const是一个关键字,const 可以修饰指针变量,起到一定的限制作用。
- 当 const 在 * 左边时,限制了解引用操作,不能修改指针指向的内容(值)。
- 当 const 在 * 右边时,限制了解引用操作,不能修改指针变量的内容(地址)。
- 如果 * 两边都有 const ,那就都不能修改了。
判断小 tips:1.只需观察 const 右边,从 const 右边开始向后观察,如果先看到 * ,那么就是限制 *p;
2.如果先看见变量名 p ,那么就限制了指针变量 p 。
5.指针的运算
5.1 指针 ± 整数
例1:
#include<stdio.h>
int main()
{
int a = 10;
int* pa = &a;
char b = 'x';
char* pb = &b;
printf("pa =%p\n", pa);
printf("pa+1=%p\n", pa+1);
printf("pb =%p\n", pa);
printf("pb+1=%p\n", pb+1);
return 0;
}
运行结果1:
pa =0000000158BBF654
pa+1=0000000158BBF658 //整型 pa 加一后跳过 4 个字节;
pb =0000000158BBF654
pb+1=0000000158BBF695 //字符型 pb 加一后跳过 1 个字节;
int 是 4 个字节,char 是 1 个字节。
例2:
#include<stdio.h>
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int sz = sizeof(arr) / sizeof(arr[0]); //计算数组中元素个数
int* p = arr; //arr 代表首元素地址
for (int i = 0; i < sz; i++)
{
printf("%d ", *(p + i)); //首元素地址加 i 逐个遍历数组,最后解引用
}
return 0;
}
运行结果2:
1 2 3 4 5 6 7 8 9 10
*( p + i )等同于 p [ i ] 等同于 arr [ i ] (而 p 又等同于 arr ,p 与 arr 可以互换)
以上只演示了加法,减法可以类推得到,也可以自行尝试。
5.2 指针 - 指针
注意:指针 - 指针的前提是指针指向同一空间。
例:
#include<stdio.h>
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int p = &arr[9] - &arr[5]; //指针 - 指针
printf("%d", p);
return 0;
}
运行结果:
4
5.3 指针关系运算
例:
#include<stdio.h>
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = arr;
int sz = sizeof(arr) / sizeof(arr[0]); //计算数组元素个数
while (p < arr + sz) //指针的关系运算
{
printf("%d ", *p);
p++;
}
return 0;
}
运行结果:
1 2 3 4 5 6 7 8 9 10
6. 野指针
野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
既然称之为野指针,表面意思上就表明该指针是 “ 野生 ” 的,不规范,不标准。
6.1 指针未初始化
#include<stdio.h>
int main()
{
int* p; //指针未初始化
*p = 10;
printf("%d", *p);
return 0;
}
既然要存放数值,就应该向内存申请一块空间,而指针可没有这功能。最后运行就会报错(使用了未初始化的局部变量 “ p ”)。
6.2 指针越界访问
#include <stdio.h>
int main()
{
int arr[10] = { 0 };
int* p = &arr[0];
for (int i = 0; i < 11; i++)//超出数组范围
{
*(p++) = i; // p[10] 就是野指针
}
return 0;
}
指针越界访问,运行后会崩!!!
6.3 指针指向已销毁的空间
#include<stdio.h>
int ret()
{
int n = 10;
return &n; //返回了 n 的地址后,内存还给系统
}
int main()
{
int* p = ret(); //调用 ret 函数
printf("%p", *p);//想要打印 n 的值,会不会成功呢?
return 0;
}
以上代码最后什么都不会打印,因为在 ret 函数中,最后返回了 n 的地址,而 n 的生命周期只在 ret 函数内,出了 ret 函数就销毁了,内存还给系统,主函数中的 *p 找不到 n 。所以 printf 想要打印 n 的值,就不会成功!
6.4 规避野指针
6.4.1 初始化野指针
对野指针初始化,只需要给野指针赋值NULL即可。
int* p = NULL;
6.4.2 空间释放时置NULL
#include<stdio.h>
int ret()
{
int n = 10;
return &n;
}
int main()
{
int* p = ret();
p = NULL; //及时置NULL
printf("%d ", *p);
return 0;
}
以上方法虽然规避了野指针,但是本质上还是野指针,置 NULL 只是将野指针限定了一个范围,但是任然改变不了野指针危险的本质。所以我们在使用指针时一定要擦亮眼睛,遇到野指针 “绕着走” 。
7. assert断言
assert宏定义在头文件 <assert.h> 中,我们先了解代码的使用。
代码1:
#include<stdio.h>
//#define NDEBUG
#include<assert.h>
int main()
{
int a = 10;
//int* p = &a;
int* p = NULL;
assert(p != NULL); //assert断言
printf("%d", a);
return 0;
}
运行结果1:
Assertion failed: p != NULL, file ( 此文件路径 ), line ( assert 当前所在行数)
代码2:
#include<stdio.h>
#define NDEBUG
#include<assert.h>
int main()
{
int a = 10;
//int* p = &a;
int* p = NULL;
assert(p != NULL);
printf("%d", a);
return 0;
}
运行结果2:
10
代码3:
#include<stdio.h>
//#define NDEBUG
#include<assert.h>
int main()
{
int a = 10;
int* p = &a;
//int* p = NULL;
assert(p != NULL);
printf("%d", a);
return 0;
}
运行结果3:
10
综上,
- assert 语句中的表达式如果为真,assert 语句就会执行它断言的功能。
- assert 语句中的表达式如果为假,assert 语句就不会执行它断言的功能。
- assert 语句还有一个开关,就是代码 2 中的 NDEBUG ,NDEBUG 必须定义在 <assert.h> 头文件之前才能正常使用,有了 #define NDEBUG,相当于就没有了 assert 断言语句。
8. 指针的传址调用
之前在函数一章中讲到的是传值调用,而指针自然是进行传地址调用。
下面就观察一下两组代码(使用函数实现常量的交换):
代码1:
#include<stdio.h>
void swap(int x,int y)
{
int tmp = x;
x = y;
y = tmp;
}
int main()
{
int a = 30;
int b = 60;
printf("交换前:");
printf(" a=%d ", a);
printf(" b=%d ", b);
swap(a, b);
printf("\n");
printf("交换后:");
printf(" a=%d ", a);
printf(" b=%d ", b);
return 0;
}
代码2:
#include<stdio.h>
void swap(int* x, int* y) //使用 int* 接收
{
int tmp = *x;
*x = *y;
*y = tmp;
}
int main()
{
int a = 30;
int b = 60;
printf("交换前:");
printf(" a=%d ", a);
printf(" b=%d ", b);
swap(&a, &b); //传地址
printf("\n");
printf("交换后:");
printf(" a=%d ", a);
printf(" b=%d ", b);
return 0;
}
以上两组代码唯一的区别在于 swap 函数中形参和实参,代码1中是传的是数值,代码2中是传的是地址。
解析:
代码1中 x、y 的地址和主函数中 a、b 的地址不同,x、y 处在一个独立的空间,并不会影响函数外 a、b 的值,所以交换不会成功。而代码2传递地址,就间接地交换了 a、b 的值了。
最后真正能够实现交换的是代码2