接下来我们就开始学习指针,首先将从下面几个部分来初步的认识指针。
1. 内存和地址
2. 指针变量和地址
3. 指针变量类型的意义
4. const修饰指针
5. 指针运算
6. 野指针
7. 指针的使⽤和传址调⽤
一、内存和地址
1.内存
在讲述内存之前先讲一个现实生活中的例子:有一栋公寓,公寓里有若干个房间但是没有门牌号。当我们想要去这个公寓里找人的时候,由于没有门牌号这会使得我们非常的困难。因此我们就需要给每个房间编上门牌号。
我们在购买电脑的时候可以看到像8GB/16GB内存等字眼,这就是内存,相当于一栋栋的公寓。在公寓中又细分的一个个的房间,这就相当于内存单元。每个内存单元的大小为1个字节(1 Byte )。一个字节就等于八个比特位(bit),这也可以理解为一个房间内住着八个人,每个人的大小是一个比特位。
下面是计算机中常见的单位:
2.地址
地址也就相当于每个房间的门牌号,而房间相当与内存单元, 那么内存单元的编号==地址。而这个地址就是我们的指针。即 内存单元的编号==地址==指针。
二、指针变量和地址
1.取地址操作符(&) 与 解引用操作符(*)
想要创建指针变量即向内存申请空间我们就需要学习 & 跟 * 这两个操作符。
1.1取地址操作符
取地址操作符顾名思义他的作用就是将一个地址给取出来。
#include<stdio.h>
int main(){
int i = 0;
printf("%p\n", &i);
return 0;
}
运行上述代码的结果为:
那么这个 ‘0000003275CF5C4’ 就是我们创建的 i 变量的地址。
1.2指针变量
那么当我们取出一个地址的时候我们需要把他存起来,那么保存他的那个变量就是指针变量,指针变量中保存的内容默认是地址。
#include<stdio.h>
int main() {
int i = 0;
int* p = &i;
printf("%p\n", &i);
printf("%p\n", p);
return 0;
}
可以看到运行的结果是一样的。
int* 就是指针变量的类型,那么我们如何确定指针变量的类型呢?
结论是指针变量中所存的地址的原类型是什么类型,那么指针变量的类型就是该类型 * 。比如上述中,变量 i 的类型是 int 那么保存他地址的指针变量 p 的类型就为 int* 。
1.3解引用操作符
当我们将地址保存起来的后我们要使用他,那么就需要解引用操作符(*)。话不多说,先上例子:
#include<stdio.h>
int main() {
int i = 100;
int* p = &i;
printf("%d\n", *p);
return 0;
}
运行结果如下:
通过解引用操作符,我们将保存在 p 中的地址进行了解引用即将 p 中保存的地址中的值取了出来。我们也可以理解为 & 和 * 这两个操作符的作用是相反的。这也可以通过例子来证实。
#include<stdio.h>
int main() {
int i = 100;
printf("%d\n", *&i);
return 0;
}
运行得到:
那么通过上述的内容,我们就可以做到不改变 i 的值的情况下改变 i 的内容。
#include<stdio.h>
int main() {
int i = 100;
int* p = &i;
*p = 200;
printf("%d\n", *p);
return 0;
}
2.指针变量的大小
那我们就会好奇, int 类型的大小是4个字节那么 int* 的大小是不是也是四个字节呢?
#include<stdio.h>
int main() {
printf("%d\n", sizeof(int*));
printf("%d\n", sizeof(double*));
printf("%d\n", sizeof(char*));
printf("%d\n", sizeof(short*));
return 0;
}
可以看到 int* 的大小并不是4而是8,不仅 int* , double* , char* 等的大小都是8,那么这是怎么回事呢?
这是因为在x64(64位操作系统)的环境下,一个地址的大小为64个比特位也就是8个字节。同理的在x86(32位操作系统)的环境下,他们的大小就为4。
因此指针变量的大小和他的类型无关,在相同的环境下,不同类型的指针变量的大小相同。
三、指针变量类型的意义
到这就又有疑问了,既然指针变量的大小都一样,为什么要定义不同类型的指针变量呢?指针变量类型的意义是什么呢?我们将通过继续学习解决这些疑问。
1.意义
下面有两段代码:
//代码1
#include<stdio.h>
int main() {
int i = 0x11223344;
int* pi = &i;
*pi = 0;
return 0;
}
通过调试观察可以发现:
*pi = 0 前:
*pi = 0 后:
//代码2
#include<stdio.h>
int main() {
int i = 0x11223344;
char* pc = &i;
*pc = 0;
return 0;
}
*pc = 0 前:
*pc = 0 后:
通过对比我们可以发现,代码1中会将 i 中的四个字节全部改为0而代码2中只是将 i 中的第一个字节改为0。
因此我们可以得出结论,指针变量的类型决定了对指针解引用时的权限即一次性能操作多少个字符。
2.指针+-整数
有如下代码:
#include<stdio.h>
int main() {
int i = 10;
int* pi = &i;
char* pc = &i;
printf("%p\n", pi);
printf("%p\n", pi+1);
printf("%p\n", pc);
printf("%p\n", pc+1);
return 0;
}
运行如下:
可以看到 int* 类型的指针+1从18➡1C(16进制)差了4,而 char* 从18→19差了1,这种不同也是由于指针变量类型的不同所导致的。因此又可得出:指针的类型决定了指针+-整数的大小是多少。
3.void* 指针
有一种 void* 类型的指针有所不同,我们知道 void 是空,因此 void* 类型的指针可以接收各种类型的指针,但是他也有缺点,那就是无法进行指针+-整数,因为他无法决定+-的大小为多少。
四、const修饰指针
1.const修饰变量
当我们定义一个变量后他是可以被修改的,那么如何做到让他无法修改呢?这就是const的作用。
#include<stdio.h>
int main() {
const int i = 10;
i = 100;
return 0;
}
当我们运行之后他就会报错。
但是我们仍然可以通过指针的作用来绕过 i 来修改 i 的值。
2.const修饰指针变量
那么如何做到彻底的无法修改呢?那就需要const修饰指针变量。
const修饰指针变量有两种类型,一种是const在*的左边,另一种是const在*的右边。const的位置不同所造成的效果也不同。下面通过举例说明。
#include<stdio.h>
int main() {
int i = 10;
int n = 100;
int* const p = &i;
p = &n; //err
*p = 200;
return 0;
}
可以看到当const在*的右边的时候p是无法修改的,但是*p可以修改。
#include<stdio.h>
int main() {
int i = 10;
int n = 100;
int const * p = &i;
p = &n;
*p = 200; //err
return 0;
}
但是当const在*左边的时候结果又反了过来,p可以修改而*p不能修改。
那么我们也就能得出,当const在*两边都有的时候,p与*p都将无法修改。
五、指针的运算
指针的运算有三种:
1.指针+-整数
2.指针-指针
3.指针的关系运算
指针+-整数我在上面已经讲过了下面就不再论述。我们从指针-指针开始。
指针-指针
指针-指针的运算有一个前提那就是两个指针必须都是指向同一个。
#include<stdio.h>
int main() {
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* p1 = &arr[5];
int* p2 = &arr[0];
printf("%d\n", p1 - p2);
return 0;
}
运行得到:
可以看到指针-指针所得到的结果是这两个指针间的元素个数,这也是指针-指针为什么会存在运算前提的原因,当两个指针指向不同时,计算机无法运算出他们之间的元素个数。
指针的关系运算
指针的关系运算比较的时指针所指向的内存地址,需要注意的是,指针的关系运算只能在相同类型的指针变量之间进行。
六、野指针
1.什么是野指针
野指针就是指向的位置是不可知(随机,不正确,没有明确限制)的。
2.野指针的成因
2.1指针未初始化
#include<stdio.h>
int main() {
int i = 0;
int* p1;
*p = 20;
return 0;
}
以上代码就是因为指针未初始化所造成的野指针,当运行时程序就会报错。
2.2指针越界访问
#include<stdio.h>
int main() {
int i[2] = {1,2};
int* p = &i[0];
for (int n = 0; n < 11; n++) {
*(p++) = n;
}
return 0;
}
运行上述代码会因死循环造成程序会报错, 这是因为p在循环中不断自增的时候由于指针越界访问了,指针指向的地址会与n的地址重合从而改变了n的量,因此造成死循环。
2.3指针指向的空间已释放
#include<stdio.h>
int* ret() {
int i = 0;
return &i;
}
int main() {
int* p = ret();
return 0;
}
上述代码当ret返回&i后就自动释放了,因此指针p拿到的是一个已经释放了的地址,那么这个指针就是野指针。
3.如何避免
想要避免野指针我们就需要注意上述的细节。
七、指针的传值和传址调用
1.传值调用
#include<stdio.h>
int ret(int a) {
int i = 1;
i = i + a;
return i;
}
int main() {
int a = 10;
int num = ret(a);
printf("%d\n", num);
return 0;
}
上述代码就是传值调用,他只是将a的值传了过去,然后开辟一块新的临时空间保存传过来的值,当我们想要达到交换的效果的时候,传值调用是行不通的。
#include<stdio.h>
void ret(int a, int b) {
int tmp = 0;
tmp = a;
a = b;
b = tmp;
}
int main() {
int a = 10;
int b = 20;
ret(a,b);
printf("a = %d, b = %d\n", a,b);
return 0;
}
可以看到并没有达到a与b交换的效果。
2.传址调用
那么想要达到交换的效果就需要传址调用,使用对应的类型的指针来接收穿过来的地址,直接在地址上进行改变。
#include<stdio.h>
void ret(int* a, int* b) {
int tmp = 0;
tmp = *a;
*a = *b;
*b = tmp;
}
int main() {
int a = 10;
int b = 20;
ret(&a, &b);
printf("a = %d, b = %d\n", a, b);
return 0;
}
可以看到我们想要的效果确实达到了。
那么什么时候使用传值什么时候使用传址呢?当我们只需要的是单一的值的时候用传值,而其他情况用传址。当然也可以全都使用传址。
(封面图源百度)