指针让c语言更像是结构化的底层语言,所有在内存中的数据结构均可用指针来访问
1. 认识内存
1.1. 线性的内存
内存是线性的,内存的线性是物理基础
32位主机最大的寻址能力就是32G
内存是以字节为单位进行编址的,内存中的每个字节对应一个地址,通过地址才能找到每个字节。
1.2. 变量的地址与大小
1.2.1. 变量的地址
变量对应内存中的一段存储空间,该段存储空间占用一定的字节数,可能是1个字节,也可能是4或8个字节,用这段存储空间的第一个字节地址来表示变量的地址,即低位字节的地址.
变量的地址,可以通过&引用运算符取得,在此可以称为取地址运算符
int main(void)
{
int a;
int b;
printf("&a = %p\n", &a); // &a = 0x16b0bf318
printf("&b = %p\n", &b); // &b = 0x16b0bf314
return 0;
}
1.2.2. 地址的大小
int main(void)
{
char a;
short b;
int c;
long d;
float e;
double f;
printf("&a = %p\n", &a); // &a = 0x16d28b31b
printf("&b = %p\n", &b); // &b = 0x16d28b318
printf("&c = %p\n", &c); // &c = 0x16d28b314
printf("&d = %p\n", &d); // &d = 0x16d28b308
printf("&e = %p\n", &e); // &e = 0x16d28b304
printf("&f = %p\n", &f); // &f = 0x16d28b2f8
// 类型不同,大小相同,均为4字节
printf("sizeof(&a) = %d\n", sizeof(&a)); // sizeof(&a) = 4
printf("sizeof(&b) = %d\n", sizeof(&b)); // sizeof(&b) = 4
printf("sizeof(&c) = %d\n", sizeof(&c)); // sizeof(&c) = 4
printf("sizeof(&d) = %d\n", sizeof(&d)); // sizeof(&d) = 4
printf("sizeof(&e) = %d\n", sizeof(&e)); // sizeof(&e) = 4
printf("sizeof(&f) = %d\n", sizeof(&f)); // sizeof(&f) = 4
return 0;
}
1.3. 间接访问内存
除了变量,还可以通过指针的方式间接访问内存,*解引用运算符,在此可以称为取内容运算符。
int main()
{
char a = 1;
short b = 2;
int c = 3;
long d = 4;
float e = 5.0;
double f = 6.5;
printf("&a = %p\n", &a); // &a = 0x16d2e731b
printf("&b = %p\n", &b); // &b = 0x16d2e7318
printf("&c = %p\n", &c); // &c = 0x16d2e7314
printf("&d = %p\n", &d); // &d = 0x16d2e7308
printf("&e = %p\n", &e); // &e = 0x16d2e7304
printf("&f = %p\n", &f); // &f = 0x16d2e72f8
printf("a = %d\n", *(&a)); // a = 1
printf("b = %d\n", *(&b)); // b = 2
printf("c = %d\n", *(&c)); // c = 3
printf("d = %d\n", *(&d)); // d = 4
printf("e = %f\n", *(&e)); // e = 5.000000
printf("f = %f\n", *(&f)); // f = 6.500000
return 0;
}
2. 指针常量
2.1. 指针是有类型的地址常量
// 上述输出等价于
printf("a = %d\n", *((char *)0x16d2e731b));
printf("b = %d\n", *((short *)0x16d2e7318));
printf("c = %d\n", *((int *)0x16d2e7314));
printf("d = %d\n", *((long *)0x16d2e7308));
printf("e = %f\n", *((float *)0x16d2e7304));
printf("f = %f\n", *((double *)0x16d2e72f8));
也就是说,&a进行取地址,取出来的地址是有类型的。
所以,指针其实就是一个有类型的4字节的整形常量
指针的类型,决定了该指针的寻址能力。即从指针所代表的地址处的寻址范围。
3. 指针变量
一个指针是一个有类型地址,是一个有类型的常量
指针变量:用以存放指针的量
一个指针变量可以被赋予不同的指针值,可以通过指针变量改变指向和间接操作
指针既可以指指针常量,也可以指指针变量,但通常说的指针,是指针变量
3.1. 指向/被指向/更改指向
通常说的谁指向了谁,就是一种描述指针的指向关系,指向谁,就是保存了谁的地址。
3.3. NULL
3.3.1. 野指针
野指针:一个指针变量,如果指向一段无效的空间,那么该指针就称为野指针
常见的野指针:一种是未初始化的指针;一种是指向一种已经被释放的空间
对野指针的写入成功,造成的后果是不可估量的,对野指针的读写操作,是危险且无意义的
3.3.2. NULL指针
NULL俗称空指针,等价于(void *)0。
C标准中定义的NULL指针:define NULL ((void *)0)
因此,常用NULL来给临时不需要初始化的指针变量来初始化,或对已经被释放指向内存空间的指针赋值。
NULL用于作标志位来使用
3.3.3. void本质
void即无类型,可以赋给任意类型的指针,本质代表内存的最小单位,32位机上等同于char
4. 指针运算
指针运算本质是指针中存储地址的运算。
4.1. 赋值运算
注意:不兼容的类型赋值会发生类型丢失。为了避免隐式转化可能出现的错误,最好用强制转化显示的区别。
4.2. 算术运算+,-,++,—
指针的算术运算,是数值加类型运算,将指针加上或减去某个整数值(以n*sizeof(T))为单位进行操作的。
int main()
{
int a = 0x0001;
printf("a = %#x a+1 = %#x\n", a, a + 1); // a = 0x1 a+1 = 0x2
int *p = (int *)0x0001;
printf("p = %#x p+1 = %#x\n", p, p + 1); // p = 0x1 p+1 = 0x5
int aa = 0x0010;
printf("aa = %#x aa-1 = %#x\n", aa, aa - 1); // aa = 0x10 aa-1 = 0xf
int *q = (int *)0x0010;
printf("q = %#x q-1 = %#x\n", q, q - 1); // q = 0x10 q-1 = 0xc
return 0;
}
注意:只有当指针指向一串连续的存储单元时,指针的移动才有意义,才能将一个指针变量与一个整数n做加减运算。
4.3. 关系运算==,>,<
int main()
{
int *ptrnum1, *ptrnum2;
int value = 1;
ptrnum1 = &value;
value += 10;
ptrnum2 = &value;
if (ptrnum1 == ptrnum2)
printf("\n 两个指针指向同一个地址\n"); // 打印
else
printf("\n 两个指针指向不同的地址\n");
return 0;
}
总结:
- 指针的运算只能发生在同类型或整形之间,否则会报错或是警告
- 指针的运算,除了数值以外,还有类型在里面
5. 数组遇上指针
5.1. 一维数组的访问方式
5.1.1. 传统方式(下标/偏移法)
数组名是数组的唯一标识符,数组名代表数组首元素的地址。可以用下标的方式对数组进行访问。
除此之外,还可以用本质方法进行访问。
int main()
{
int array[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 0};
for (int i = 0; i < 10; i++)
{
printf("array[%d] = %d\n", i, array[i]);
}
printf("+++++++\n");
for (int i = 0; i < 10; i++)
{
printf("array[%d] = %d\n", i, *(array + i));
}
// 输出
// array[0] = 1
// array[1] = 2
// array[2] = 3
// array[3] = 4
// array[4] = 5
// array[5] = 6
// array[6] = 7
// array[7] = 8
// array[8] = 9
// array[9] = 0
return 0;
}
5.1.2. 数组名是常量指针
数组名是常量,才可以唯一的确定数组元素的起始地址
5.1.3. 一维数组名跟一级指针的关系
数组除了可以用下标法和本质法访问以外,还可以用指针法去访问。
能用数组名解决的问题,都可以用指针来解决,而能用指针来解决问题的,并不一定能用数组名来解决。
5.1.4. 指针访问方式
int main()
{
int array[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 0};
printf("array = %p\n", array); // array = 0x16d0832f0
printf("&array[0] = %p\n", &array[0]); // &array[0] = 0x16d0832f0
// array代表首元素地址,array[0]就是数组的首元素,类型是int类型,其地址类型就是int *,故可将一维数组跟一级指针联系在一起
int *p = array;
for (int i = 0; i < 10; i++)
{
printf("array[%d] = %d\n", i, /* p[i] */ *(p + i));
}
/* printf("*********\n");
for (int i = 0; i < 10; i++)
{
printf("array[%d] = %d\n", i, *p);
p++;
} */
/* printf("*********\n");
for (int i = 0; i < 10; i++)
{
printf("array[%d] = %d\n", i, *(p++));
} */
return 0;
}
5.1.5. 小结
- 数组名是一个常量,不允许重新赋值
- 指针变量是一个变量,可以重新赋值
- p+i和a+i均表示数组元素,a[i]的首地址,均指向a[i]
- *(p+i)和*(a+i)均表示p+i和a+i所指对象的内容a[i]
- *p++:等价于*(p++)。其作用:先得到*p,再使p=p+1
- (*p)++:表示将p所指向的变量(元素)的值加1。即等价于a[i]++
- 指向数组元素的指针也可以表示成数组的形式,即允许指针变量带下标,如*(p+i)可以表示成p[i]
5.2. 二维数组的访问方式
5.2.1. 下标法
数组元素的表示方法是:数组名称[行][列]
5.2.2. 本质偏移法
从a到a[0][0]都经历了什么
int main()
{
int arr[3][4] = {1, 2, 3, 4, 10, 20, 30, 40, 100, 200, 300, 400};
for (int i = 2; i >= 0; i--)
{
for (int j = 3; j >= 0; j--)
{
printf("%#x\n", &arr[i][j]);
}
}
printf("&arr[0] = %#p &arr[0] + 1 = %#x &arr[0] + 2 = %#x\n", &arr[0], &arr[0] + 1, &arr[0] + 2); // &arr[0] = 0x16bb4b2e8 &arr[0] + 1 = 0x6bb4b2f8 &arr[0] + 2 = 0x6bb4b308
printf("arr = %#p arr[0] + 1 = %#x arr[0] + 2 = %#x\n", arr[0], arr[0] + 1, arr[0] + 2); // arr = 0x16bb4b2e8 arr[0] + 1 = 0x6bb4b2ec arr[0] + 2 = 0x6bb4b2f0
printf("arr[0] = %#p arr[0] + 1 = %#x arr[0] + 2 = %#x\n", arr[0], arr[0] + 1, arr[0] + 2); // arr[0] = 0x16b3432e8 arr[0] + 1 = 0x6b3432ec arr[0] + 2 = 0x6b3432f0
printf("%d\n", arr[1][1]); // 20
printf("%d\n", *(*(arr + 1) + 1)); // 20
return 0;
}
总结:
arr[i][j]:
(arr+i)第i行的地址
*(arr+i)第i行第0列的地址
*(arr+i)+j第i行第j列的地址
*(*(arr+i)+j)第i行第j列的内容