1.什么是地址
在介绍指针之前,我们首先需要知道什么是地址?——CPU访问内存某个空间时,必须知道这段空间的位置,内存会给相同大小不同位置的空间编址,地址可以指向某段空间的位置。
2.什么是指针
2.1知道什么是地址后,回到C语言中。C语言在创建变量时,会向内存申请空间。
指针 本质是一种变量,与普通变量不同的是,普通变量存放的是数据,指针变量存放的是该数据存放的地址(这里放一张普通变量的图,一张指针变量的图)
2.2指针变量的格式
int a = x ;
int* pa = &a ;
int* 是指针变量的类型。* 说明变量p是指针,int 说明该指针指向的变量a的类型是int
同理,可以知道char a = x; 的指针为
char* pa = &a;
指针变量的类型只能与其取地址的普通变量的类型相同(void* 类型除外)
2.3解引用操作符*
*的作用是通过地址得到数据
例如:int a=4;
int* pa=&a;
*pa = 0;
通过解引用操作符,可以将指针变量存放的地址转换为数据。上述代码中*pa其实就是a变量,*pa = 0;这个操作就是a = 0;。
2.4指针变量的大小
X86环境下指针变量的大小是4个字节,X64环境下指针变量的大小是8个字节。
地址的大小也是同理,X86环境下,一个地址占用4个字节;X64环境下,一个地址占用八个字节。
指针变量的大小与类型无关,只与运行环境有关,运行环境相同的话,不管是int* 的指针,还是char*的指针,所有指针变量的大小都是相同的。
3.指针变量类型的意义
3.1指针的解引用
上文提到过,指针的解引用就是将指针里存放的地址转换为该地址的数据。
int a = 0;
*pa = a;
我们都知道,int类型的变量占内存四个字节的空间。那么,*pa = 0时,改变的是多大的内存?
这由指针变量的类型决定。
int* pa = 0时,改变的是四个字节大小的内存,会将四个字节的地址全部改为0。
chat* pa = 0时,改变的是一个字节大小的内存,会将一个字节的地址全部改为0。
结论:所以指针变量的类型决定了该指针变量解引用时有多大权限(一次能改变多少字节)
3.2指针+-整数
(PPT里面的代码)
char* 类型的指针变量+1时,跳过一个字节;
int* 类型的指针变量+1时,跳过四个字节;
结论:指针的类型决定了指针向前或向后走一步的距离有多大
3.3void* 指针
void* 类型的指针可以接受任意类型的变量,但是不能进行指针的解引用和+-整数。
void* 类型的指针一般用来当做函数的参数,方便接收各种类型的数据
4.const修饰指针
4.1const修饰变量
变量是可以被修改的
int a = 0;
a = 1;
这样变量a就从0修改成1了。如果不想a被修改,就可以在int前面加上const。
const int a = 0;
这样在修改变量a时,编译器会报错,不让修改。但是,如果是通过指针修改,还是可以的
const int a = 0;
int* pa = &a;
*pa = 1;
2.2const 修饰指针变量
为了不让a被修改,我们可以用const修饰变量a的指针。const修饰指针变量时,有两种写法,一种是放在*的左边
const int * pa = &a;
一种是放在右边
int * const pa = &a;
这两种的区别是:
const放在左边,可以修改pa取地址的变量,不能修改变量的大小。
pa = &n;(✓)
*pa = 1;(×)
可以把取地址a换成取地址n,但是不能修改a的内容
放在右边,可以修改变量的大小,不能修改pa取地址的变量。
*pa = 1;(✓)
pa = &n;(×)
可以把a=0改成a=1。不能把取地址a改成取地址n
5.指针变量的运算
5.1指针+-整数
指针+-整数的前提是在内存中连续存放。
一般数组可以满足这个条件。因此一般情况下,指向数组的指针才能+-整数
#include <stdio. h>
//指针+-整数
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int *p = &arr[0];
int i = 0;
int sz = sizeof(arr)/sizeof(arr[0]);
for(i=0; i<sz; i++){
printf("%d ", *(p+i));//p+i 这里就是指针+整数
}
return 0;
}
(运行结果图)
5.2指针-指针
指针-指针可以理解为日期-日期,就像4月2日-4月1日 = 1一样,指针 - 指针 = 两个指针之间的空间大小
#include <stdio. h>
3 int my _ strlen(char *s)
4 {
5 char *p = s;
6 while(*p != '\0' )
7 p++;
8 return p-s;
9 }
10
11 int main()
12 {
13 printf("%d\n", my _ strlen("abc"));
14 return 0;
15 }
指针存放地址时,在存放该地址的空间的最后,会加上一个\n作为结束标志。
所以在指针p增大到\n时,指针p作为连续指针已经是最大的了。再增大则会出现越界的现象,程序会报错。
5.3指针的关系运算
6.野指针
野指针就是指针指向的位置是不可知的(随机的,不正确的,没有明确限制的)
6.1野指针的成因
6.1.1指针未初始化
6.1.2指针越界访问
6.1.3指针指向的空间释放
6.2如何规避野指针
6.2.1指针初始化
如果知道指针需要包含哪个变量,则直接给指针取地址该变量;不知道哪个变量的话,可以给指针赋值NULL。NULL是C语言中定义的标识符常量,值是0。
6.2.2小心越界访问
一个程序向内存申请了哪些空间,指针也只能访问这些空间,不能超出范围访问,超出就是越界访问了。
6.2.3指针不再使用时,及时置NULL,使用之前检查指针有效性
当指针变量使用时,给它指向某一段空间;当指针不再使用时,给它置NULL,NULL指针一般不运行。
但是NULL只能防止指针自行运行,如果主动运用指针,则还是会发生野指针的运行。
并且NULL指针需要编译器进行判断
if(*p != NULL)
才可以运行;如果不判断,程序则会报错
6.2.4避免返回局部变量的地址
编译器在运行函数时,会单独开辟一块空间(详细请看空间栈帧),在函数运行完成后,这块空间就会还给内存
因此,局部变量在函数完成后,会跟着函数一起被销毁地址,因此指针在指向局部变量时会因为该变量无地址而报错
7.assert宣言
#include<assert.h>
assert(p) 或 assert(表达式)
验证p或表达式是否为NULL。如果不是NULL,则程序正常运行,assert不发挥作用;如果是NULL,则assert会报错,阻止程序运行
assert一般用于程序进行debug。当确定程序没有bug后,可以在#include<assert.h>前面定义一个宏#define NDEBUG,这样编译器就会禁止程序中的assert
#define NDEBUG
#include<assert.h>
assert一般用于程序进行debug。修改完所以bug后,因为assert引用了额外的检查,增加了程序的运行时间,所以需要通过
#define NDEBUG
来禁止assert,不影响用户的使用效率
8.指针的使用和传址调用
8.1 传值调用和传值调用
学习指针是为了解决问题,有什么问题一定要用指针呢?
例如:写一个交换两个整型的函数
#include<stdio.h>
void Swap1(int x,int y)
{
int tmp = x;
x = y;
y = tmp;
}
int main()
{
int a = 0;
int b = 0;
scanf("%d %d",&a,&b);
printf("交换前:a = %d,b = %d",a,b);
Swap1(a,b)
printf("交换后:a = %d,b = %d",a,b);
}
(结果图)
这种我们称为传值调用,因为函数在运行时会创建一个临时空间,(x,y)的内存地址与(a,b)的内存地址不同,函数传递给main函数的值是(x,y),实际上并没有改变(a,b)的值,所以交换前后的(a,b)没有互换
结论:实参 (a,b) 传递给形参 (x,y) 的时候,形参 (x,y) 会单独创建一份临时空间来接收实参 (a,b) ,对形参 (x,y) 的修改不影响实参 (a,b)
#include<stdio.h>
void Swap2(int* px,int* py)
{
int tmp = 0;
int tmp = *px;
*px = *py;
*py = tmp;
}
int main()
{
int a = 0;
int b = 0;
scanf("%d %d",&a,&b);
printf("交换前:a = %d,b = %d",a,b);
Swap1(&a,&b)
printf("交换后:a = %d,b = %d",a,b);
}
(结果图)
这种我们称为传址调用,函数把变量的地址传递给main函数
传址调用,可以让函数与主函数之间建立真正的联系,在函数内部可以修改主函数的变量
所以,当函数只需要对参数进行计算的时候,可以使用传值调用;当函数需要修改主函数的参数的时候,就需要使用传址调用。
9.二级指针
指针变量也是变量,既然是变量,也就有地址,二级指针用来存放指针变量的地址。
int a = 0;
int* pa = &a;
int* *ppa = &pa;
10.回调函数
完。