前言:指针作为 C 语言中的精髓,也是最难的一部分内容,只要理解指针与内存之间的关系,也许你会有一个新的认识。
指针的基本概念
首先先看下指针的概念(摘自百度):
指针是一个占据存储空间的实体在这一段空间起始位置的相对距离值。在C/C++语言中,指针一般被认为是指针变量,指针变量的内容存储的是其指向的对象的首地址,指向的对象可以是变量(指针变量也是变量),数组,函数等占据存储空间的实体。
通俗的来说就是:
- 内存区的每一个字节都有一个编号,这就是“地址”。
- 如果在程序中定义了一个变量,在对程序进行编译或运行时,系统就会给这个变量分配内存单元,并确定它的内存地址(编号)
- 指针的实质就是内存“地址”。指针就是地址,地址就是指针。
- 指针是内存单元的编号,指针变量是存放地址的变量。
- 通常我们叙述时会把指针变量简称为指针,实际他们含义并不一样。
普通变量对应内存存储情况:
不同的类型的数据占用的字节数不同,例如 int 占用 4 个字节,char 占用 1 个字节。 指针变量对应内存存储情况:
指针存储的值是内存地址,且占用的字节数都是不会变的,在 32 位操作系统下是 4 个字节,64 位操作系统下是 8 个字节。
指针变量的定义和使用
- 指针也是一种数据类型,指针变量也是一种变量
- 指针变量指向谁,就把谁的地址赋值给指针变量
- “*”操作符操作的是指针变量指向的内存空间
先看一段代码
#include <stdio.h>
int main() {
// 定义整型变量a
int a = 10;
// 定义指针变量p
int *p;
// 指针变量赋值,指针变量p指向变量a的地址
p = &a;
printf("%p\n", &a); // 000000000061FE14(a变量的内存地址)
printf("%p\n", p); // 000000000061FE14(p变量存储的指针值)
printf("%p\n", &p); // 000000000061FE10(p变量的内存地址)
printf("%d\n", a); // 10(a变量存储的值)
printf("%d\n", *p); // 10(通过*操作指针变量指向的内存)
printf("%d\n", sizeof(a)); // 4(int类型占用内存的大小)
printf("%d\n", sizeof(p)); // 8(指针类型占用内存的大小,32位操作系统下是4个字节,64位操作系统下是8个字节)
return 0;
}
以上代码对应的内存存储情况: 总结:
p 存放的值 = a 的内存地址,也就是说 p 存储的是 a 的内存地址;- 通过
* 操作指针变量指向的内存的值,也就是说 *p 取出来的是 a 存放的值。
& 是取地址符号,是升维度的。 * 是取值符号,是降维度的。
野指针和空指针
野指针:指针变量指向一个未知的空间
虽然指针变量也是变量,是变量就可以任意赋值,不要超过变量定义的数据类型长度即可,但是,指针存储的值一定是内存的地址,且占用内存的字节数是固定的(32位为4字节,64位为8字节),所以,如果指针变量存储的不是内存地址的话,也就没有任何意义了,因此就成了野指针。
野指针不会直接引发错误,操作野指针指向的内存区域才会出问题。
#include <stdio.h>
int main() {
char ch = 'a';
int *p = &ch;
printf("%d\n", sizeof(ch)); // 1
printf("%d\n", sizeof(p)); // 8
printf("%p\n", &ch); // 000000000061FE17
printf("%p\n", p); // 000000000061FE17
*p = ch; // 把 ch 的值赋值给指针变量 p,也就是说 *p = 'a',此时 p 存储的值是 'a',没有意义,所以 p 为野指针
printf("%p\n", &ch); // 000000000061FE17
printf("%p\n", p); // 0000000000000000
*p = 100; // 直接给指针变量 p 赋值,没有意义,此时 p 为野指针
printf("%p\n", &ch); // 000000000061FE17
printf("%p\n", p); // 0000000000000000
return 0;
}
空指针:指内存地址编号为0的空间
野指针和有效指针变量保存的都是数值,为了标志此指针变量没有指向任何变量(空闲可用),C语言中,可以把NULL赋值给此指针,这样就标志此指针为空指针。
#include <stdio.h>
int main() {
int *p = NULL; // 定义一个空指针
// 操作空指针对应的空间一定报错
printf("%d\n", *p); // 报错
// 赋值之后也报错
*p = 100;
printf("%d\n", *p); // 报错
return 0;
}
万能指针 void *
void * 指针可以指向任意变量的内存空间
指向 int 类型
#include <stdio.h>
int main() {
int a = 10;
/*int *p = &a;
printf("%p\n", p); // 000000000061FE14
printf("%d\n", *p); // 10*/
// 万能指针可以接收任意类型变量的内存地址
void *p = &a;
// 再通过万能指针修改变量的值时,需要找到变量对应的指针类型
// *p = 100; // 报错
*(int *) p = 100; // 正确
printf("%p\n", p); // 000000000061FE14
printf("%d\n", *(int *) p); // 100
printf("%d\n", sizeof(void *)); // 8
return 0;
}
指向 char 类型
#include <stdio.h>
int main() {
char ch = 'a';
// 万能指针可以接收任意类型变量的内存地址
void *p = &ch;
// 再通过万能指针修改变量的值时,需要找到变量对应的指针类型
// *p = 'b'; // 报错
*(char *) p = 'b'; // 正确
printf("%p\n", p); // 000000000061FE17
printf("%d\n", *(char *) p); // 98
printf("%d\n", sizeof(void *)); // 8
return 0;
}
const 修饰指针
const 修饰指针有三种情况:
-
const 修饰指针 — 常量指针 -
const 修饰常量 — 指针常量 -
const 即修饰指针,又修饰常量 — 只读指针
const 修饰普通变量(常量)的值,可通过指针间接修改
#include <stdio.h>
int main() {
const int a = 10;
// a = 100; // 错误,常量不允许修改
// 指针间接修改常量值
int *p = &a;
*p = 100;
printf("%d\n", a); // 100
return 0;
}
常量指针
可以修改指针变量的值,不可以修改指针指向内存空间的值
#include <stdio.h>
int main() {
int a = 10;
int b = 20;
// 定义常量指针
const int *p = &a;
p = &b; // ok,可以修改指针变量的值
// *p = 100; // 错误,不可以修改指针指向内存空间的值
printf("%d\n", *p); // 20
return 0;
}
指针常量
不可以修改指针变量的值,可以修改指针指向内存空间的值
#include <stdio.h>
int main() {
int a = 10;
int b = 20;
// 定义指针常量
int *const p = &a;
// p = &b; // 错误,不可以修改指针变量的值
*p = 100; // ok,可以修改指针指向内存空间的值
printf("%d\n", *p); // 100
return 0;
}
只读指针
不可以修改指针变量的值,不可以修改指针指向内存空间的值
#include <stdio.h>
int main() {
int a = 10;
int b = 20;
// const 即修饰指针,又修饰常量,只读指针
const int *const p = &a;
// p = &b; // 错误,不可以修改指针变量的值
// *p = 100; // 错误,不可以修改指针指向内存空间的值
return 0;
}
只读指针实际上可通过二级指针修改指针变量的值和内存空间的值
#include <stdio.h>
int main() {
int a = 10;
int b = 20;
// const 即修饰指针,又修饰常量,只读指针
const int *const p = &a;
// p = &b; // 错误,不可以修改指针变量的值
// *p = 100; // 错误,不可以修改指针指向内存空间的值
// 通过二级指针操作
int **pp = &p;
*pp = &b;
printf("%d\n", *p); // 20
**pp = 100;
printf("%d\n", *p); // 100
printf("%d\n", a); // 10
printf("%d\n", b); // 100
return 0;
}
修改前内存存储情况: 分析:
*pp 是一级指针 p 的值 0x10001 ,通过 *pp = &b ,修改 p 的值,所以此时 p 的值为 0x10002 **pp 是一级指针 p 变量的值 20 ,通过 **pp = 100 ,修改 p 变量的值,所以此时 p 变量的值为 100 ,即 b = 100
修改后内存存储情况: 总结:
一级只读指针可以通过二级指针修改值,以此类推…
指针和数组
数组名
数组名字是数组的首元素地址,但它是一个常量,不允许被修改。
#include <stdio.h>
int main() {
int arr[] = {1, 2, 3, 4, 5, 6, 7, 8, 'a', 'b', 'c'};
// 数组名是一个常量,不允许赋值
// arr = 100; // err
// 数组名是数组首元素的地址,所有直接赋值给指针变量即可,不需要&符号
int *p = arr;
printf("%p\n", arr); // 000000000061FDE0
printf("%p\n", p); // 000000000061FDE0
return 0;
}
指针操作数组元素
#include <stdio.h>
int main() {
int arr[] = {1, 2, 3, 4, 5, 6, 7, 8, 'a', 'b', 'c'};
int *p = arr;
for (int i = 0; i < 10; i++) {
printf("%d\n", *p);
p++; // 指针类型变量+1,等同于内存地址+sizeof(int)
}
return 0;
}
以上代码对应的内存存储情况: 总结:
- 数组名是数组首元素的地址,所以数组名
arr 对应的内存地址是 0x10001 - 指针类型变量+1,等同于内存地址
+sizeof(int) 两个指针的偏移量,所以 p+1 对应的内存地址是 0x10002 ,以此类推…
指针数组
指针数组,它是数组,数组的每个元素都是指针类型。
#include <stdio.h>
int main() {
int a = 10;
int b = 20;
int c = 30;
int *arr[3] = {&a, &b, &c};
int len = sizeof(arr) / sizeof(arr[0]);
for (int i = 0; i < len; ++i) {
printf("%d\n", *arr[i]);
}
return 0;
}
以上代码对应的内存存储情况: 指针和二维数组的应用:
#include <stdio.h>
int main() {
int a[] = {1, 2, 3};
int b[] = {4, 5, 6};
int c[] = {7, 8, 9};
// printf("%p\n", a); // 000000000061FE0C 数组名是指数组首元素的指针地址
// 指针数组里面元素存储的是指针
// 指针数组是一个特殊的二维数组模型
int *arr[] = {a, b, c};
// printf("%p\n", arr[0]); // 000000000061FE0C
// printf("%p\n", a); // 000000000061FE0C
// printf("%p\n", &a[0]); // 000000000061FE0C
// 遍历出二维数组的所有数据
for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++) {
for (int j = 0; j < 3; j++) {
// 二维数组
// printf("%d", arr[i][j]); // 下标方式获取
// printf("%d", *(arr[i] + j)); // 指针+下标方式获取
printf("%d", *(*(arr + i) + j)); // 纯指针方式获取
}
}
// printf("%p\n", arr); // 000000000061FDD0
// printf("%p\n", &arr); // 000000000061FDD0
// printf("%p\n", *arr); // 000000000061FE0C 此处*代表指针的地址
// printf("%d\n", *(*arr)); // 1 此处*代表指针的地址的值
printf("%p\n", arr + 1); // 000000000061FDD8
printf("%p\n", &arr[1]); // 000000000061FDD8
printf("%p\n", *(arr + 1)); // 000000000061FE00
printf("%p\n", &b); // 000000000061FE00
printf("%d\n", *(*(arr + 1) + 1)); // 5
return 0;
}
指针和函数
函数形参改变实参的值
利用指针作函数参数,可以修改实参的值。
#include <stdio.h>
void swap(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
}
int main() {
int a = 10;
int b = 20;
swap(&a, &b); // 地址传递
printf("%d\n", a); // 20
printf("%d\n", b); // 10
return 0;
}
数组名做函数参数
数组名做函数参数,函数的形参会退化为指针。
#include <stdio.h>
void printArray(int *a, int n) {
printf("%p\n", a); // 000000000061FDF0,指针
int i = 0;
for (i = 0; i < n; i++) {
printf("%d\n", a[i]);
}
}
int main() {
int a[] = {1, 2, 3, 4, 5, 6, 7, 8, 9};
int n = sizeof(a) / sizeof(a[0]);
// 数组名做函数参数
printArray(a, n);
return 0;
}
指针做为函数的返回值
#include <stdio.h>
int a = 10;
int *getA() {
return &a;
}
int main() {
*(getA()) = 111;
printf("%d\n", a); // 111
return 0;
}
|