哈喽,大家好,今天我们来学习C语言中的指针,今天主要学习初阶指针,后期我们将继续学习指针进阶。
目录
1. 指针是什么
2. 指针和指针类型
2.1 指针+-整数
2.2 指针的解引用
3. 野指针
3.1 野指针成因
3.2 如何规避野指针
4. 指针运算
4.1 指针+-整数
4.2 指针-指针
4.3 指针的关系运算
5. 指针和数组
6. 二级指针
7. 指针数组
1. 指针是什么
指针是什么?
在 C 语言中,指针是一种特殊的变量,能够存储另一个变量的内存地址。指针变量可以用来访问、修改存储在内存中的数据。
将一个变量的地址存储在指针变量中,可以通过解引用操作符(*)来访问指针所指向的变量的值。例如,可以通过以下方式声明和使用一个整型变量和一个指向该变量的指针:
在这个例子中,我们定义了一个名为 a
的整型变量,它的值为 10
。然后,我们定义了一个指向 a
的指针变量 p
,使用地址运算符 &
来获取 a
的地址,并将该地址存储在指针 p
中。最后,我们通过解引用操作符 *
来输出指针 p
所指向的变量的值,即输出了变量 a
的值 10
。
指针理解的2个要点:
1. 指针是内存中一个最小单元的编号,也就是地址
2. 平时口语中说的指针,通常指的是指针变量,是用来存放内存地址的变量
我们可以用下图来理解内存:
- 在32位的机器上,地址是32个0或者1组成二进制序列,那地址就得用4个字节的空间来存储,所以 一个指针变量的大小就应该是4个字节。
- 那如果在64位机器上,如果有64个地址线,那一个指针变量的大小是8个字节,才能存放一个地 址。
指针的大小:
int main()
{
printf("%d\n", sizeof(char*));
printf("%d\n", sizeof(short*));
printf("%d\n", sizeof(int*));
printf("%d\n", sizeof(long*));
printf("%d\n", sizeof(float*));
printf("%d\n", sizeof(double*));
return 0;
}
在32位机器下指针大小都是4字节
在64位机器下指针大小都是8字节
总结
指针变量是用来存放地址的,地址是唯一标示一个内存单元的。
指针的大小在32位平台是4个字节,在64位平台是8个字节
2. 指针和指针类型
这里我们在讨论一下:
指针的类型
我们都知道,变量有不同的类型,整形,浮点型等。那指针有没有类型呢?
准确的说:有的。
char* 类型的指针是为了存放 char 类型变量的地址。
short* 类型的指针是为了存放 short 类型变量的地址。
int* 类型的指针是为了存放 int 类型变量的地址。
那指针类型的意义是什么?
作用一:
如果用int*类型的指针pa来接收a的地址,当改变*pa,a的4个字节中的内容都发生了改变:
如果用char类型的指针pa来接收a的地址,当改变*pa,只有一个字节中的内容发生了改变:
int*的指针解引用访问4个字节,char*类型指针解引用访问1个字节
总结: 指针的类型决定了,对指针解引用的时候有多大的权限(能操作几个字节)。
比如: char* 的指针解引用就只能访问一个字节,而 int* 的指针的解引用就能访问四个字节。
左用二:
观察下面代码:
我们发现:
int*类型的pa加1,地址向后增加了4位
char*类型的pb加1,地址向后增加了1位
总结:指针的类型决定了指针向前或者向后走一步有多大(距离)。
2.1 指针+-整数
在C语言中,指针可以进行加减整数的操作,这个操作的含义是在指针所指向的地址上加上或减去一个整数量,从而实现指针位置的移动。
以下是一些示例代码:
在这个例子中,我们定义了一个整型数组a和一个字符型指针s。
对于整型指针p,它指向数组a的第一个元素,并且可以使用指针加法将p加1,指向数组a的第二个元素。
对于字符指针s,它指向一个字符串的第一个字符,并且通过指针加法将s加2,指向字符串的第3个字符。
使用指针访问数组:
int main()
{
int arr[10] = { 0 };
int* p = &arr[0];
int i = 0;
int sz = sizeof(arr) / sizeof(arr[0]);
for (i = 0; i < sz; i++)
{
*(p + i) = i;
}
for (i = 0; i < sz; i++)
{
printf("%d ", *(p + i));
}
}
这段代码中,使用指针变量p直接访问数组元素,相当于p + i所指向的内存位置就是数组arr[i]的地址。在这个地址上进行赋值操作,实现了对数组值的修改。同时,使用指针加法能够更方便地对数组元素进行遍历。
需要注意的是,指针加减整数的操作有时会越过数组的边界,访问到不合法的内存位置。因此,在进行指针加减整数的操作时,应该保证指针不会越界。
2.2 指针的解引用
这段代码演示了如何对一个整型变量n的高/低位进行操作,并观察内存的变化。代码中定义了一个整型变量n,并给它赋予十六进制值0x11223344,然后分别使用指向char类型的指针pc和指向int类型的指针pi对n进行操作。
首先,将指针pc指向n,并将它转换为指向char类型的指针。这样,pc能够对n高/低位进行逐一操作,因为char类型只占用一个字节。接着,将pc指向的第一个字节赋值为0,这将会将n的最低字节设为0,其他字节不变。
然后,通过指针pi对整型变量n进行操作。因为pi是指向整型变量n的指针,所以它可以对整个n进行操作。将pi指向的值赋值为0,这将会将整个n的值设为0。
需要注意的是,在对整型变量n的高/低位进行操作时,必须要考虑机器的大小端问题,否则可能会导致数据的读取和写入出现问题。
总结:
指针的类型决定了,对指针解引用的时候有多大的权限(能操作几个字节)。 比如: char* 的指针解引用就只能访问一个字节,而 int* 的指针的解引用就能访问四个字节。
3. 野指针
概念: 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
野指针是指没有被初始化或已经被释放的指针,它指向内存中未知的地址或者不可访问的地址。野指针的出现可能会导致程序的崩溃、内存泄漏等问题。
3.1 野指针成因
1.指针未赋初值,或者被赋了一个未知的值,导致指针指向一个不可知的地址。
2.指针已经被释放,但是程序还尝试去访问该指针所指向的内存空间。
代码一:
代码二:
此程序存在返回局部变量的指针问题,即将一个指向局部变量的指针返回给调用者。具体问题在 test()
函数中,函数中定义的 a
变量是一个局部变量,函数执行完毕后,系统会收回这个变量的内存空间,在该函数中申请的内存空间随即被释放。所以 a
变量所处的内存空间在函数执行完毕后已经被释放了,返回的指针 &a
指向的内存空间已经不再是该变量的内存空间,因此这个指针是无效的,称为“悬挂指针”。
3.指针越界,指向了不属于该指针所指向内存空间的位置。
代码一:
代码二:
这段程序中存在指针p越界的问题,导致arr数组发生了内存越界的现象,它的具体问题在 “for” 循环中的语句 *(p++) = i;
循环次数超过了数组的长度,因此当指针 p 指向数组 arr 区域外时就会出现非法访问,是一种常见的野指针错误。
更改方法可以是检查循环范围,修改为循环10次即可.
3.2 如何规避野指针
1. 指针初始化
在定义指针变量时,最好将其初始化为空指针,防止指针未初始化就被使用,造成意料之外的行为.
2. 小心指针越界
要避免指针越界,应该注意以下几点:
确认数组长度:在定义数组时,要确保数组长度足够,避免出现数组访问越界的情况。
检查数组下标:在使用数组下标时,要确保数组下标不会越界,比如当下标小于 0 或大于等于数组长度时,就可能出现数组越界的情况。
指针的作用域:由于指针变量的生命周期很长,要确保指针变量所指向的内存空间还没有被释放,避免因为指针变量所指向的内存空间已经被释放导致的指针越界的问题。
指针的使用范围:在使用指针时,要确保指针变量所指向的内存空间的有效范围,避免访问指针所指向的内存空间以外的内存,造成指针越界的情况。
指针的类型:当使用指针访问一个内存块时,要确保指针的类型和所指向的内存块的类型匹配,避免出现类型不匹配导致的访问越界问题。
3. 指针指向空间释放,及时置NULL
4. 避免返回局部变量的地址
5. 指针使用之前检查有效性
在使用指针变量之前,最好先检查指针是否为空,以避免空指针引发的异常或错误
4. 指针运算
4.1 指针+-整数
这部分内容和2.1一样,补充新的例子:
通过将vp指向数组第一个元素的地址(&values[0]),可以使用for循环对整个数组进行遍历。
for循环中的语句使用了指针加法,每次使vp指向下一个数组元素。在数组元素前加上*,可以取得vp所指向的数组元素的值,并将这个值赋为0。通过这样的方式,vp指向了数组中的每一个元素,并将它们都设为了0。注意,在循环中没有定义循环变量,条件检查和迭代都在了循环语句中,这也是一种常见的for循环写法。
需要注意的是,循环中的条件判断是vp < &values[N_VALUES],在指针比较中,指针实际上是对应一个内存地址,比较的是这个地址的大小,它的判断依据是地址升序排列,所以vp指向的地址在比较时要小于&values[N_VALUES],否则就越界了。
4.2 指针-指针
分析下面代码的结果:
在这段代码中,计算了数组中两个元素的地址差,通过指针相减的方式得到了它们之间的元素个数。由于数组元素在内存中是连续存储的,它们之间的地址差就是它们之间的元素个数。
指针-指针运算的前提条件:
只有当指针指向同一数组的元素时,才能进行指针相减运算。
4.3 指针的关系运算
代码简化, 这将代码修改如下:
实际在绝大部分的编译器上是可以顺利完成任务的,然而我们还是应该避免这样写,因为标准并不保证它可行。
标准规定:
允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与 指向第一个元素之前的那个内存位置的指针进行比较。
5. 指针和数组
指针和数组之间的联系:
数组中,数组名其实是数组首元素的地址,数组名 == 地址 == 指针
当我们知道数组首元素的地址的时候,因为数组是连续存放的,所以通过指针就可以遍历访问数组
6. 二级指针
指针变量也是变量,是变量就有地址,那指针变量的地址存放在哪里?
这就是二级指针 。
二级指针的地址存放在三级指针里:
7. 指针数组
指针数组是一个包含指针的数组,即数组的每个元素都是指向某种类型的指针。指针数组可以很方便地表示和操作一组指针,用于存储和访问多个数据结构。
int main()
{
char arr1[] = "abcdef";
char arr2[] = "hello world";
char arr3[] = "cuihua";
//指针数组
char* parr[] = { arr1,arr2,arr3 };
int i = 0;
for (i = 0; i < 3; i++)
{
printf("%s\n", parr[i]);
}
return 0;
}
下面是int型指针数组使用例子:
int main()
{
int arr1[] = { 1,2,3,4,5 };
int arr2[] = { 2,3,4,5,6 };
int arr3[] = { 3,4,5,6,7 };
//指针数组
int* parr[] = { arr1,arr2,arr3 };
int i = 0, j = 0;
for (i = 0; i < 3; i++)
{
for (j = 0; j < 5; j++)
{
//printf("%d ", parr[i][j]);
printf("%d ", *(parr[i] + j));
}
printf("\n");
}
return 0;
}
这段代码演示了一个包含指针数组的简单例子。在这个例子中,我们定义了三个整型数组 arr1
、arr2
和 arr3
,然后创建了一个指针数组 parr
。parr
中的每个元素都是一个指向整型数组的指针,即 arr1
、arr2
或 arr3
。
在这个例子中,我们可以通过 *(parr[i] + j)
来访问第 i
个指针元素指向的整型数组中的第 j
个元素。
这种方式与使用 parr[i][j]
的效果是一样的,只是表达方式略有不同。这里使用指针和数组下标的方式是指针运算的一种形式,即先使用 parr[i]
获取指向整型数组的指针,然后使用 *(parr[i] + j)
访问指针所指向的数组。