一、指针的概念
要知道指针的概念,要先了解变量在内存中如何存储的。在存储时,内存被分为一块一块的。每一块都有一个特有的编号。而这个编号可以暂时理解为指针,就像酒店的门牌号一样。
1.1内存与地址
在讲内存和地址之前,我们想有个⽣活中的案例:
假设你要去酒店,酒店有100个房间,但是房间没有编号,你的⼀个朋友来找你玩,如果他想要找到你,就得一个一个去找,这样的效率是很低的,那我们给定每个房间编号
⼀楼:101,102,103...
⼆楼:201,202,203....
...
你的朋友得到房间号,就可以快速找到你的房间,找到你。
如果把上面的例子对找到计算机中:
如: 你的朋友 就相当于计算器
而你就是房间(地址)的内存了
101 102 103 就相当于地址
在计算机中我们把内存单元的编号也称为地址。C语言中给地址起了新的名字叫:指针
例子:
void main() { int x = 1, int y = 2; }
这段代码非常简单,就是两个变量的声明,分别赋值了 1、2。我们把内存当做一个酒店,而每个房间就是一块内存。那么“int x = 1;”和“int y = 2;”的实际含义如下:
去酒店订了两个房间,门牌号暂时用 px、py 表示
让 1 住进 px,让 2 住进 py
其中门牌号就是 px、py 就是变量的地址
x 和 y 在这里可以理解为具体的房间,房间 x 的门牌号(地址)是 px,房间 y 的门牌号(地址)是 py。而 1和 2,通过 px、py 两个门牌,找到房间,住进 x、y。
二、变量的指针与指针变量
变量的指针就是变量的存储地址,指针变量就是存储指针的变量。
2.1、指针变量的定义及使用
1、指针变量的定义
指针变量的定义形式如:数据类型 *指针名;例如:
//分别定义了 int、float、char 类型的指针变量
int *x;
float *f;
char *ch;
这里的指针变量是x,f,ch,并非 *x , *f, *ch.
数据名为int* , float*, char*
2、指针变量的使用
我们要怎样取地址呢?这就要用到——取地址运算符& 和 指针运算符*(间接寻址符)
取地址运算符&:单目运算符&是用来取操作对象的地址。例:&i 为取变量 i 的地址。对于常量表达式、寄存器变量不能取地址(因为它们存储在存储器中,没有地址)。
指针运算符*(间接寻址符):与&为逆运算,作用是通过操作对象的地址,获取存储的内容。例:x = &i,x 为 i 的地址,*x 则为通过 i 的地址,获取 i 的内容。
int main() { int a = 10; //输入一个整型变量a,变量的值为10 int* pa = &a; //输入一个整型的指针变量pa来接收存放a的地址 printf("%d",*pa); //(*)表示对pa存放地址的解引用 return 0; }
2.2 指针变量的大小
前⾯的内容我们了解到,32位机器假设有32根地址总线,每根地址线出来的电信号转换成数字信号后是1或者0,那我们把32根地址线产生的2进制序列当做⼀个地址,那么⼀个地址就是32个bit位,需要4 个字节才能存储。
如果指针变量是⽤来存放地址的,那么指针变的大小就得是4个字节的空间才可以。
同理64位机器,假设有64根地址线,⼀个地址就是64个⼆进制位组成的⼆进制序列,存储起来就需要 8个字节的空间,指针变的大小就是8个字节。
#include <stdio.h> //指针变量的⼤⼩取决于地址的大小 //32位平台下地址是32个bit位(即4个字节) //64位平台下地址是64个bit位(即8个字节) int main() { printf("%zd\n", sizeof(char *)); printf("%zd\n", sizeof(short *)); printf("%zd\n", sizeof(int *)); printf("%zd\n", sizeof(double *)); return 0; }
怎么回事呢?想必你也发现些许奥秘,没错,对于指针来说,指针变量的大小与类型无关、只要指针类型的变量,在相同的平台下,大小都是相同的。
2.3、指针+-整数
#include <stdio.h> int main() { int n = 10; char *pc = (char*)&n; int *pi = &n; printf("%p\n", &n); printf("%p\n", pc); printf("%p\n", pc+1); printf("%p\n", pi); printf("%p\n", pi+1); return 0; }
让我们打印一下吧!!!
我们可以看得出来 ,char* 类型的指针变量+1跳过1个字节, int* 类型的指针变量+1跳过了4个字节。 这就是指针变量的类型差异带来的变化。
结论:指针的类型决定了指针向前或者向后⾛⼀步有多⼤(距离)。
2.4、void*指针
在指针类型中有⼀种特殊的类型是 void* 类型的,可以理解为无具体类型的指针(或者叫泛型指
针),这种类型的指针可以⽤来接受任意类型地址。但是也有局限性, void* 类型的指针不能直接进 行指针的+-整数和解引用的运算。
我们来看一下这个代码:
#include <stdio.h> int main() { int a = 10; int* pa = &a; char* pc = &a; return 0; }
显示从int*到char*的类型不兼容,编译器会给一个报错,用void*则不会出现这个问题
#include <stdio.h> int main() { int a = 10; void* pa = &a; void* pc = &a; return 0; }
这里我们看到,void*可以用来接收不同类型的指针,但是注意的是void*不能用来指针的运算。
三、指针的运算
指针的基本运算有三种,分别是:
• 指针+- 整数
• 指针-指针
• 指针的关系运算
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; }
2、指针-指针
//指针-指针 #include <stdio.h> int my_strlen(char *s) { char *p = s; //每次++,到达下一个元素,到最后一个为NULL时,p指向c while(*p != '\0' ) p++; return p-s; //返回地址的差值,因为是char*类型,每个地址间跳过一个字节,所以返回的 } int main( ) { printf("%d\n", my_strlen("abc")); 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 i = 0; int sz = sizeof(arr) / sizeof(arr[0]); while (p < arr + sz) //指针的大小比较 { printf("%d ", *p); p++; } return 0; }
6. 野指针
概念: 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
6.1、野指针成因
指针未初始化: #include <stdio.h> int main() { int *p;//局部变量指针未初始化,默认为随机值 *p = 20; return 0; }
指针越界访问 #include <stdio.h> int main() { int arr[10] = {0}; int *p = &arr[0]; int i = 0; for(i=0; i<=11; i++) { //当指针指向的范围超出数组arr的范围时,p就是野指针 *(p++) = i; } return 0; }
指针指向的空间释放 #include <stdio.h> int* test() { int n = 100; return &n; } int main() { int*p = test(); printf("%d\n", *p); return 0; }
那我们如何规避野指针的出现呢
6.2、如何规避野指针
指针初始化,如果我们明确指针的指向的话,该指哪就指向哪;当我们不知道指向哪时, 可以给指针赋值NULL. NULL 是C语⾔中定义的⼀个标识符常量,值是0;0也是地址,这个地址是⽆法使⽤的,读写该地址 会报错。
#include <stdio.h> int main() { int num = 10; int*p1 = &num int*p2 = NULL; return 0; }
6.3、注意指针不要越界
⼀个程序向内存申请了哪些空间,通过指针也就只能访问哪些空间,不能超出范围访问,超出了就是 越界访问。
6.4、当指针不再使用时,可以将其置为NULL,指针使用前,判断其有效性
当指针变量指向⼀块区域的时候,我们可以通过指针访问该区域,后期不再使⽤这个指针访问空间的 时候,我们可以把该指针置为NULL。因为约定俗成的⼀个规则就是:只要是NULL指针就不去访问, 同时使⽤指针之前可以判断指针是否为NULL。
我们可以把指针想为野狗,不管理的野狗是非常危险的,我们可以将野狗栓在一棵树(NULL),就相对安全了;
6.2、assert函数
assert.h 头文件定义了宏 assert() ,⽤于在运行时确保程序符合指定条件,如果不符合,就报
错终止运行。这个宏常常被称为“断⾔”。
assert(p != NULL );
运行这个语句的时候,判断p是否为空,如果不为空则,程序继续运行,如果为空,则终止程序,并给出错误的信息提示。
四、多级指针及指针数组
(1)多级指针
指针变量作为一个变量也有自己的存储地址,而指向指针变量的存储地址就被称为指针的指针,即二级指针。依次叠加,就形成了多级指针。
int p = 10; //设置一个变量为p int* pc = &p; //取p的地址存进pc int** pt = &pc; //取pc的地址存进pt,这里的pt为二级指针
先用一个简单的数组来举例:
int nums[2][2] = { {1, 2},{2, 3}};
#include<stdio.h> int main() { int arr[2][2] = { {1,2},{2,3} }; int* pc = &arr; printf("%d ", *pc); printf("%d\n", pc); printf("%d ", *(pc+1)); printf("%d\n", (pc + 1)); printf("%d ", *(pc+2)); printf("%d\n", (pc + 2)); printf("%d ", *(pc+3)); printf("%d\n", (pc + 3)); return 0; }
我们可以看出二维数组地址的运用,可以看作一维数组;以此来推断,面对多维数组地址的运用时,我们不必害怕,可以当作一维数组
4.3、指向函数的指针
C 语言中,函数不能嵌套定义,也不能将函数作为参数传递。但是函数有个特性,即函数名为该函数的入口地址。我们可以定义一个指针指向该地址,将指针作为参数传递。
对于函数参数传递时,如果是传址函数时,则可以改变该地址的函数值;如为传值,则不然;
#include<stdio.h> void comp_int(int*comp) { *comp = 10; } int main() { int a = 10; int b = 20; comp_int(&b); if (a == b) { printf("相等"); } return 0; }
这里的输出为:相等,则我们可以通过传址改该地址的值;
五、计算器(转移表)的使用
1、计算器的实现(switch)
对于一个计算器,需要有最基础的(加减乘除),来人,上代码:
这时候有人就会觉得这个代码有点冗长,说:小伞,小伞有没有办法,能将代码变得便洁吗?
那当然是有的呀!
#define _CRT_SECURE_NO_WARNINGS #include<stdio.h> int add(int x, int y) { return x + y; } int sub(int x, int y) { return x - y; } int mul(int x, int y) { return x * y; } int div(int x, int y) { return x / y; } int main() { int a = 0; int x, y; int ret; do { printf("*************************\n"); printf(" 1:add 2:sub \n"); printf(" 3:mul 4:div \n"); printf(" 0:exit \n"); printf("*************************\n"); printf("请选择:"); scanf("%d", &a); switch (a) { case 1: printf("输⼊操作数:"); scanf("%d %d", &x, &y); ret = add(x, y); printf("ret = %d\n", ret); break; case 2: printf("输⼊操作数:"); scanf("%d %d", &x, &y); ret = sub(x, y); printf("ret = %d\n", ret); break; case 3: printf("输⼊操作数:"); scanf("%d %d", &x, &y); ret = mul(x, y); printf("ret = %d\n", ret); break; case 4: printf("输⼊操作数:"); scanf("%d %d", &x, &y); ret = div(x, y); printf("ret = %d\n", ret); break; case 0: printf("退出程序\n"); break; default: printf("选择错误\n"); break; } } while (a); return 0; }
如果我们使用switch语句来实现这样一个简易的计算器我们会发现,每当我要添加一个功能的时候。都需要增加一个case语句,比如我要增加一个&运算,我得再加上一个case语句。因此我们可以使用函数指针数组(转移表)来实现,会简易很多。
什么是转移表?
其实很简单,它所指的就是运用函数指针数组以数组方式去调用里面的函数,从而在某些情况下替代冗长的switch函数,就叫转移表。
单纯的文字说明实在有些单调,这里通过模拟实现计算器来进一步解释说明转移表。
上代码:
#include <stdio.h> int add(int a, int b) { return a + b; } int sub(int a, int b) { return a - b; } int mul(int a, int b) { return a * b; } int div(int a, int b) { return a / b; } int main() { int x, y; int input = 1; int ret = 0; //p[]={add,sub,mul,div} //这里我们想要将函数的地址存进来 //int(*p[])(int,int) //返回类型 函数指针 指向函数的参数 int(*p[5])(int , int )= { 0, add, sub, mul, div }; //转移表 //函数指针的数组 下标 0 1 2 3 4 do { printf("*************************\n"); printf(" 1:add 2:sub \n"); printf(" 3:mul 4:div \n"); printf(" 0:exit \n"); printf("*************************\n"); printf("请选择:"); scanf("%d", &input); if ((input <= 4 && input >= 1)) { printf("输⼊操作数:"); scanf("%d %d", &x, &y); ret = (*p[input])(x, y); // printf("ret = %d\n", ret); } else if (input == 0) { printf("退出计算器\n"); } else { printf("输⼊有误\n"); } } while (input); return 0; }