C语言学习 -- 指针
Posted 庸人冲
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C语言学习 -- 指针相关的知识,希望对你有一定的参考价值。
指针概述
指针是什么?
指针,是C语言中的一个重要概念及其特点,指针就是内存地址,指针变量是用来存放内存地址的变量,不同类型的指针变量所占用的存储单元长度是相同的,而存放数据的变量因数据的类型不同,所占用的存储空间长度也不同。有了指针以后,不仅可以对数据本身,也可以对存储数据的变量地址进行操作。
指针是一个占据存储空间的实体在这一段空间起始位置的相对距离值。在C/C++语言中,指针一般被认为是指针变量,指针变量的内容存储的是其指向的对象的首地址,指向的对象可以是变量(指针变量也是变量),数组,函数等占据存储空间的实体。
—转自百度百科
内存地址的产生
内存地址的编码是通过计算机中的地址线产生正负电来代表0、1产生的。
在32位平台上使用32根地址线,因此可以产生232中不同的排列顺序,每一种排列就代表内存中的一个地址,而一个地址可以存放1Byte的数据,因此32位平台的内存最大支持4GB的内存。
64位平台使用64根地址线,因此可以产生264个地址,理论上最大能支持2147483648GB的内存,实际上要看主板和操作系统支持的最大内存是多少。
指针变量的大小
当我们知道,内存地址是如何产生后,我们在分析指针变量的大小。
前面说过指针变量是用来存放内存地址的,这也就说明,指针变量在32位平台上,它必须要有能存储232bit大小的空间,而在64位平台上,它必须要有能存储264bit大小的空间。
1Byte = 1bit
所以32位平台上,指针变量的大小有4Byte。
64位平台上,指针变量的大小有8Byte。
这里和数据类型无关,因为指针变量存放的是地址,而地址的长度是固定的。
#include <stdio.h>
int main()
{// %zu 输出size_t型。size_t在库中定义为unsigned int
printf("%zu\\n", sizeof(char *)); // 32bit-4 b4bit-8
printf("%zu\\n", sizeof(short *)); // 32bit-4 b4bit-8
printf("%zu\\n", sizeof(int *)); // 32bit-4 b4bit-8
printf("%zu\\n", sizeof(double *)); // 32bit-4 b4bit-8
return 0;
// 结论:指针大小在32位平台是4个字节,64位平台是8个字节。
}
指针和指针类型
上面介绍了,指针变量的大小是固定的,和它说存地址变量的数据类型无关,那么既然变量类型不影响指针变量的大小,为什么还需要给指针变量也指定数据类型呢?
我们在使用指针变量存储地址时,存放的确实是32\\64位的地址,不管指针类型是int*
char*
double*
等等,它们所存的地址都是一样的。
int main()
{
int a = 0x11223344;
int* pi = &a;
char* pc = &a;
pirntf("%p\\n", pi);
pirntf("%p\\n", pc);
// 存放的是相同变量的地址,输出结果也相同
return 0;
}
指针变量解引用
但是当我们对指针变量解引用操作,来访问其中的变量时问题就出来了。
printf("*pi = %#x\\n", *pi);
printf("*pc = %#x\\n", *pc);
为什么pc和pi存放的地址相同,在解引用操作时,读取的值却不相同的?
a
是一个int
类型的变量,所占内存大小4个字节,当我们用int*
类型的指针变量去接收它的地址后,在进行解引用操作时,计算机会认为访问的是int
类型,所以这个指针变量会通过地址访问当前向后的共4块内存空间,a
的值就被保存在这4块内存空间中。
而是使用char*
类型的指针变量接收地址后,在进行解引用操作时,计算机会认为访问的是一个char
类型的数据,所以这个指针变量只会访问当前这一块地址的空间(首位置),而当前这块地址存放的是0x44,因此就将0x44获取到。
至于为什么访问的是0x44,而不是0x11,这是因为内存中存储数据时,分配到每一块内存空间的顺序是不一定的,可能是正序存储也可能是倒序存储,后面会解释。
所以指针类型的作用是当访问元素时,可以获取到访问空间有多大(能够操作几个字节),char*
只能访问当前地址所指向的那一块内存空间,short*
可以访问当前地址和之后的两块内存空间,int*
、long*
、 float*
… 都是按照所对应基本数据类型所占用空间大小,来决定它们可以访问多大的空间。
指针变量+-
整数
指针变量存放的是一个地址,而对指针变量+
或-
一个整数n时,它指不是对地址的值+n
,而是指增加或减少n个存储单元,这意味它会根据指针类型所对应的基本类型所占空间大小,来找到前面或后面第n个元素的位置。
例如:
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,10 };
int* p = arr;
printf("%p\\n", p);
printf("%d\\n", *p);
printf("==========\\n");
p -= 1; // 意思往前找到第4个内存单元,并获取它的地址
printf("%p\\n", p);
printf("%d\\n", *p);
printf("==========\\n");
p += 2; // 往后找到第8个内存单元,并获取它的地址
printf("%p\\n", p);
printf("%d\\n", *p);
}
从图片结果可以看出,指针变量p
类型是int*
,在-1
时,内存地址减少了4
,+2
时内存地址增加了8
,而两个地址都指向了存放数组内数值的那一块内存空间。
而指针变量pc
在-1
时,内存地址减少了1
,+2
时内存地址增加了2
,而两个地址都指向了存放数组内的为0的内存空间(因为int
占4块内存空间,而被赋予的数值都没有超过一个1Byte空间可以存放的大小,因此剩下三块内存空间中的值是全0
)
可以看出指针在加减n
时,实际上加减的是 n
* sizeof(int)
,因此,指针的移动距离
= n * sizeof(type)
这也是指针类型的另一个作用,决定了指针的移动距离,只有指定了合适的指针类型才能正确的访问元素。
野指针
野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)指针变量在定义时如果未初始化,其值是随机的,指针变量的值是别的变量的地址,意味着指针指向了一个地址是不确定的变量,此时去解引用就是去访问了一个不确定的地址,所以结果是不可知的。
—转自百度百科
野指针成因
- 指针未初始化
int main()
{
int* p; // 局部变量未初始化,默认为随机值
*p = 20;
return 0;
}
// 在vs2019编译器中,这段代码跑不过去
// 可能编译器也不清楚这家伙指向的是哪里,哈哈
- 指针越界访问
int main()
{
int arr[10] = { 0 };
int* p = arr;
for (int i = 0; i <= 10; i++)
{
// 当指针指向的范围超出数组arr的范围时,p就是野指针
*p++ = i;
}
printf("%d\\n", *(p+10));
return 0;
}
- 指针指向的被释放的空间
int* test()
{
int a = 10; // a是局部变量,当函数执行完毕,会释放这块内存空间
printf("&a = %p\\n", &a);
return &a;
}
int main()
{
int* ptr = test(); // 指针ptr接收的是一块被释放空间的地址,这也会导致野指针
printf("ptr = %p\\n", ptr);
printf("*ptr = %d\\n", *ptr);
return 0;
}
// 这种写法虽然编译器不会报错,但是解引用的值是随机值,输入非法访问内存空间
如何避免野指针
-
指针初始化
当不确定指针应该指向哪一块地址时,可以给指针变量赋值为**
NULL
**int* p = NULL; // NULL-用来初始化指针,给指针赋值
-
小心指针越界
-
指针指向空间释放时要将指针赋值为**
NULL
** -
指针使用之前检查有效性
int main() { int a = 10; int* p = &a; *p = 20; p = NULL; // 不用时赋值为空 // 使用时先检查指针有效性 if(p != NULL){ // 使用指针变量p } return 0; }
指针运算
指针 +-
整数
指针 +-
整数在前面已经介绍,
int main() {
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* ptr = arr;
int* ptr1 = &arr[9];
int i = 0;
for (i = 0; i < 10; i++) {
printf("%d ", *(ptr + i));
}
putchar('\\n');
for (i = 0; i < 10; i++) {
printf("%d ", *(ptr1 - i));
}
}
指针 -
指针
指针减去指针得到结果的绝对值是两个地址中间的元素个数。
int main() {
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
printf("%d\\n",& arr[9] - &arr[0]); // 9
printf("%d\\n",& arr[0] - &arr[9]); // -9
// 将两个指针类型不同的指针相减,得到的结果是未知的
// 错误写法
int* pi = arr;
char* pc = &arr[9];
printf("%d\\n", pc - pi); // 36
printf("%d\\n", pi - pc); // -9
// 在VS编译器下,是以左操作数的指针类型作为根据
return 0;
}
模拟实现strlen()
函数功能
int my_strlen(char* pstr) {
char* start = pstr; // 起始位置
char* end = pstr; // 终止位置,字符串中第一个'\\0'的位置
while (*end != '\\0') {
end++;
}
return end - start; // 中间的元素个数,即字符串长度,不包含'\\0'
}
int main() {
// 模拟实现Strlen函数
char arr[] = "bit";
int len = my_strlen(arr);
printf("%d\\n", len);
return 0;
}
指针的关系运算
指针可以比较大小
int main() {
int arr[5];
int* ptr = NULL;
for (ptr = arr; ptr < &arr[5];)
{
*ptr++ = 0; // 将数组所有元素初始化为5
//*(ptr++) = 0 // 等价写法
}
// 等价写法2
for (ptr = arr; ptr < &arr[5];ptr++)
{
*ptr = 0; // 将数组所有元素初始化为5
}
}
int main() {
int arr[5];
int* ptr = NULL;
// 写法1
for(ptr = &arr[5]; ptr > &arr[0];) // ptr第一次进入,指向的是数组最后一个元素后面一个元素的空间
{
*--ptr = 0; // 先--得到数组最后一个元素的地址,在解引用并赋值,直到ptr == &arr[0]结束循环
}
// 写法2(非法写法)
for(ptr = &arr[4]; ptr >= &arr[0];ptr--){ // ptr第一次进入,指向的是素组最后一个元素
*ptr = 0; // 每次进来将数组中一个元素赋值为0,直到ptr == & arr[-1]时,结束结束
}
// 写法2是不合法的,因为C语言中只规定了,在创建数组时,指向数组后第一个位置的指针是合法的,可以有效访问,数组前一个位置的指针则是非法的
return 0;
}
数组和指针
在数组章节介绍过数组名通常情况下是数组首元素的地址,因此假设创建一个数组arr[]
,那么arr
== &arr[0]
,两者都是常量,不会被改变。
int main()
{
int arr[] = { 1 };
int* p1 = arr;
int* p2 = &arr[0];
if (p1 == p2)
{
printf("p1 = %p\\n", p1);
printf("p2 = %p\\n", p2);
printf("*p1 = %d\\n", *p1);
printf("*p2 = %d\\n", *p2);
}
return 0;
}
从打印结果可以看出,数组名 == 数组首元素的地址。
我们可以将数组的地址赋值给指针变量,来修改指针变量的值,达到使用数组的效果。
int main()
{
int arr[5] = { 1,2,3,4,5 };
char str[5] = { 'a','b','c','d','e' };
int* pi = arr; // 将数组首元素的地址赋值给指针变量
char* pc = str;
int i = 0;
printf("%10s %25s\\n", "int", "char");
for (i = 0; i < 5; i++)
{
printf("arr[%d].val == %-8d | str[%d].val == %-8c\\n", i, *(pi + i),i, *(pc + i));
printf("arr[%d].addr == %p | str[%d].addr == %p\\n", i, pi + i,i, pc + i);
}
return 0;
}
通过 指针
+n
的方法可以高效的获取到数组中的元素,在C语言中指针
+n
是指增加n个存储单元,对于数组来说,它指向的当前地址后面第n
个元素的地址。
从打印结果可以看出int
类型的数组和char
类型的数组所增加的值是不同的,int
每次加4
,char
每次加1
,这是因为,int
类型的数据所占内存空间为4btye
,char
类型的数据所占内存空间为1btye
,这也是为什么在声明指针变量时需要在指针变量前加上数据类型,它代表的不是指针的数据类型,而是它所指向对象的数据类型。
数组和指针的关系
通过上面的例子,可以发现下面两种表示方式是等价的
// 取地址的结果相同
// arr + n == &arr[n] , n>=0 && n< sizeof(arr)
// str + n == &str[n] , n>=0 && n< sizeof(str)
// 解引用的结果相同
//*(arr+n) == arr[n]; , n>=0 && n< sizeof(arr)
//*(str+n) == str[n]; , n>=0 && n< sizeof(str)
// 上面验证使用的是指针变量pi和pc,不过pi == arr ,
代码验证
int main()
{
int arr[5] = { 1,2,3,4,5 };
char str[5] = { 'a','b','c','d','e' };
int* pi = arr; // 将数组首元素的地址赋值给指针变量
char* pc = str;
int i = 0;
printf("%10s %25s\\n", "int", "char");
printf("%p == %p | %p == %p\\n", arr + 2, &arr[2], str + 2, &str[2]);
printf("%8d == %-8d | %8c == %-8c\\n", *(arr + 2), arr[2], *(str + 2),str[2]);
return 0;
}
也就是说想获取数组中的一个元素,可以有*(addr + idx)
和 arr_name[idx]
两种方法,而获取它们的地址也有addr + idx
和 &arr_name[idx]
两种方法。
这里需要注意*(addr + idx)
和 *addr + idx
, 前者表示先指向后面的地址,再解引用获取地址里的值;而后者则是先解引用获取当前地址的值,再将这个值+ idx
,两种写法的结果是完全不同的,后者相当于(*addr) + idx
。
可以发现数组和指针的关系十分密切,我们对数组名的操作可以同样运用在存放相同数组元素地址的指针变量上,但是数组名和指针又不是完全相同。
数组名和指针变量的区别
数组名虽然存放的是首元素的地址,但是它不等于指针变量。数组名有两点不同于指针变量的地方。
sizeof
在计算数组名时,会返回整个数组所有元素大小的合,这个值是根据不同的数据类型来计算的,而指针变量返回的就是本身的大小,32bit
平台下4btye
,64bit
平台下8btye
,这是指针变量这种数据类型的特点,因为它是用来存放地址的。
int main()
{
int arr[] = {1,2,3,4,5,6,7,8,9,10 };
int* p = &arr[5];
printf("arr.sz == %d\\n", sizeof(arr));
printf("p.sz == %d\\n", sizeof(p));
return 0更新:C++ 指针片段