通过前面一段时间C语言的学习,我们了解了数组,函数,操作符等的相关知识,今天我们将要开始进行指针的学习,这是C语言中较难掌握的一个部分,一定要认真学习!!!
1.内存与地址
在正式学习指针前,我们首先要了解两个概念,内存与地址。该如何理解它们呢?
举个例子,一栋宿舍楼有不同房间,每间房门前都有属于他自己的门牌号,让我们可以正确的寻找到对应的宿舍。一栋宿舍楼对应一块内存,每间宿舍对应一小块内存,地址就是门牌号,方便我们快速找到。相对应的我们可以把内存划分为一个个的内存单元,每个内存单元的大小是一个字节。字节是是计算机中最基本的存储单位。以下是一些常见的计算机内存单位:
bit --比特位
Byte --字节 1Byte = 8bit
KB 1KB = 1024 Byte
MB 1MB = 1024 KB
GB 1GB = 1024 MB
TB 1TB = 1024 GB
其中,每个内存单元,相当于一个学生宿舍,一个字节空间能放8个比特位,就好比一个八人寝。每个内存单元也都有一个编号(相当于门牌号),有了这个编号(就是地址),CPU就能快速找到一个内存空间。C语言中给地址起了新的名字:指针。所以:
内存单元的编号==地址==指针
2.指针变量和地址
2.1取地址操作符(&)
理解了内存和地址的关系,我们再回到C语言,在C语言中创建变量其实就是向内存申请空间,比如:
上述代码创建了整型变量a,向内存申请了四个字节的空间,用于存放整数10,每个字节都有自己的地址,整数10会优先存放在四个字节中地址较小的字节。
那么我们要如何得到a的地址呢?这就需要用到一个取地址操作符&,还是上面这个代码,我们用&a来得到了a的地址,存放到p中,打印出来:
虽然整型变量占⽤4个字节,我们只要知道了第⼀个字节地址,顺藤摸瓜访问到4个字节的数据也是可行的。
2.2指针变量和解引用操作符
上面代码我们打印了&a的地址004FF818,这是一个十六进制的数值,这个数值是可以存储起来的,如整数存放在整型变量中,指针当然也要存放在指针变量中,为了存放&a的地址我们用了p来充当指针变量,它的类型是int*,int*中的*是在说明p是指针变量,与整形变量做区分,而为什么是int呢?因为p地址对应的内存空间存放的是整型变量。
那如果是char类型的变量,要存放它的地址,指针变量该是什么呢?当然是char*,*做区分,char表明存放的是字符类型,如:
char ch = 'a';
char* pc = &ch;
我们知道变量利用取地址操作符得到了地址,那么知道地址有没有对应的操作符得到变量呢?答案是肯定的,这时候就要拿出解引用操作符*了,没错,他跟上文的*一模一样,但意义截然不同,我们看代码:
我们先创建了一个整型变量num,然后把该变量地址命名为pz,将pz解引用,赋值32,num里的值也产生了相应变化,说明我们通过*pz找到了对应23的内存空间,改变了它。
2.3指针变量的大小
指针变量的大小取决于所处环境(x86环境和x64环境)
int main()
{//在x86环境下,指针变量的大小是四个字节printf("%zd\n", sizeof(char*));//4printf("%zd\n", sizeof(short*));//4printf("%zd\n", sizeof(int*));//4printf("%zd\n", sizeof(long*));//4//在x64环境下,指针变量的大小是四个字节printf("%zd\n", sizeof(char*));//8printf("%zd\n", sizeof(short*));//8printf("%zd\n", sizeof(int*));//8printf("%zd\n", sizeof(long*));//8return 0;
}
• 32位平台下地址是32个bit位,指针变量大小是4个字节• 64位平台下地址是64个bit位,指针变量大小是8个字节•从代码里,我们可以明白指针变量的大小和类型是无关的,只要在相同的环境下,大小都是相同的。
指针变量的大小既然和类型无关,为什么会有各种各样的指针类型呢?我们继续接下来的学习。
我们来看一个代码:
void test1()
{int n = 0x11223344;int* pi = &n;*pi = 0;printf("%0x\n", n);//0
}void test2()
{int n = 0x11223344;char* pc = (char*)&n;*pc = 0;printf("%0x\n", n);//11223300
}int main()
{test1();test2();return 0;
}
观察结果,我们会发现test1会将n的四个字节对应的内存空间全部改为0,但是test2只将n的第一个字节对应的内存空间改为0。
指针的类型决定了,对指针解引用的时候有多大的权限(⼀次能操作几个字节)。比如: char* 的指针解引用就只能访问⼀个字节,而 int* 的指针的解引用就能访问四个字节。
接下来我们再来看一个代码:
指针的类型决定了指针向前或者向后走一步有多⼤(距离)。
2.4 void*指针

我们看到,void*类型的指针并不能进行相应的计算,那么void*指针到底有什么用呢?它一般使用在函数参数的部分,用来接收不同类型数据的地址,可以达到范式编程的效果。我们在后面的学习中会慢慢接触到。
3.const修饰


可以看到,错误变成了警告,代码得以正常运行 ,打破了const的限制。但我们的目的是让变量无法被修改,有没有其他的限制办法呢?当然有,我们让const修饰指针就行了,看下面代码:
代码马上又报错了,指针指向的内存空间将无法被修改,但是我们马上又有了另一个发现 :
将const放置在指针变量类型之后,代码又可以正常运行了,这是为什么呢?
int main()
{int m = 10;m = 12;const n = 12;const int* p1 = &n;*p1 = 10;//errorp1 = &m;//rightreturn 0;
}int main()
{int m = 10;m = 12;const n = 12;int* const p1 = &n;p1 = &m;//error*p1 = 13;//rightreturn 0;
}
在上述代码中,我们发现:
• const如果放在*的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变。但是指针变量本身的内容可变。• const如果放在*的右边,修饰的是指针变量本身,保证了指针变量的内容不能修改,但是指针指向的内容,可以通过指针改变。
4.指针运算
指针的基本运算有三种:
• 指针+-整数• 指针-指针• 指针的关系运算
4.1指针+-整数
数组元素和下标
int main()
{int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };int* p1 = arr;int sz = sizeof(arr) / sizeof(arr[0]);for (int i = 0; i < sz; i++){printf("%d ", *(p1 + i));}return 0;
}
4.2指针减指针
int my_strlen(char* ch)
{char* p = ch;while (*p != '\0'){p++;}return p - ch;
}int main()
{printf("%d\n", my_strlen("abcdef"));return 0;
}
4.3指针的运算关系
int main()
{int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };int sz = sizeof(arr) / sizeof(arr[0]);int* p = arr;while (p < arr + sz){printf("%d ", *p);p++;}return 0;
}
5.野指针
概念: 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
5.1野指针成因
5.1.1指针未初始化
5.1.2指针越界访问
5.1.3指针指向的空间释放
int* test()
{int num = 100;return #
}int main()
{int* p = test();printf("%d\n", *p);return 0;
}
5.2如何规避野指针
5.2.1指针初始化
int main()
{int num1 = 12;int* p1 = &num1;int* p2 = NULL;return 0;
}
5.2.2小心指针越界访问
⼀个程序向内存申请了哪些空间,通过指针也就只能访问哪些空间,不能超出范围访问,超出了就是越界访问。
5.2.3指针变量不再使用时,及时置NULL,指针使用之前检查有效性
当指针变量指向⼀块区域的时候,我们可以通过指针访问该区域,后期不再使⽤这个指针访问空间的时候,我们可以把该指针置为NULL。因为约定俗成的⼀个规则就是:只要是NULL指针就不去访问,同时使⽤指针之前可以判断指针是否为NULL。
5.2.4避免返回局部变量的地址
6.assert断言
assert.h 头⽂件定义了宏 assert() ,⽤于在运⾏时确保程序符合指定条件,如果不符合,就报错终⽌运⾏。这个宏常常被称为“断⾔”。
#include <assert.h>
assert(expression);
这里的
expression
是一个布尔表达式,assert
会对其进行求值。如果expression
的值为真(非零),程序会继续正常执行;如果expression
的值为假(零),assert
会触发一个断言失败,程序会调用abort()
函数终止执行,并输出错误信息。
它不仅能自动标识⽂件和出问题的⾏号,还有⼀种⽆需更改代码就能开启或关闭 assert() 的机制。如果已经确认程序没有问题,不需要再做断⾔,就在 #include <assert.h> 语句的前⾯,定义⼀个宏 NDEBUG 。
#define NDEBUG
#include<assert.h>
然后,重新编译程序,编译器就会禁用文件中所有的 assert() 语句。如果程序又出现问题,可以移 除这条 #define NDBUG 指令(或者把它注释掉),再次编译,这样就重新启用了 assert() 语句。