1.初识指针
在C语言中,指针是一种非常重要且强大的概念,它可以让我们直接访问和操作内存中的数据。理解和运用指针是C语言编程中的重要一环,能够帮助我们更灵活地操作内存和数据,提高程序的效率和灵活性。
指针是一个变量,其值为内存地址,即指向另一个变量的地址。
1.1地址和内存
我们知道计算的CPU(中央处理器)在处理数据的时候,需要的数据是在内存中读取的,处理后的数据也会放回内存中,那这些内存空间如何⾼效的管理呢?
其实内存会将自己划分为一个个的内存单元,每个内存单元的大小是1个字节。
1 byte = 8 bit
1 字节 = 8 比特位1个比特位中可以存储1个2进制中的0或1
在计算机中,我们把内存单元的编号称为地址,
而在C语言中,地址有了一个新的名字:指针!
一般的,通过使用 * 符号来声明指针,例如
int *ptr;
表示声明了一个指向整数类型的指针。
1.2指针变量与取地址操作符 &
在C语言中,创建变量其实就是在向内存申请空间,例如上图中,a是一个整形int变量,那么它会向内存申请4个字节,每一个字节都对应着一个地址。
&运算符表示获取变量的地址,例如&a表示变量a的地址。
&a取出的是变量a所占的4个字节中地址较小的字节的地址。
1.3指针变量与解引用操作符 *
通过取地址操作符(&)拿到的地址是⼀个数值,⽐如:0x006FFD70,这个数值有时候也是需要
存储起来,⽅便后期再使⽤的,那我们把这样的地址值存放在哪⾥呢?
答案是:指针变量中。
int main()
{
int a = 10;
int* pa = &a;//将a的地址放进指针变量pa中
return 0;
}
指针变量也是⼀种变量,这种变量就是⽤来存放地址的,存放在指针变量中的值都会理解为地址。
这⾥pa左边写的是 int* , * 是在说明pa是指针变量,⽽前⾯的 int 是在说明pa指向的是整型(int)
类型的对象。
上⾯代码中就使⽤了解引⽤操作符, *pa 的意思就是通过pa中存放的地址,找到指向的空间,
*pa其实就是a变量了;所以*pa=0,这个操作符是把a改成了0。
这⾥把a的修改通过指针交给了pa来操作,这样对a的修改,就多了⼀种的途径,写代码就会更加灵活。
2.指针运算
指针可以进行加减运算,移动到相邻的内存位置。
指针也可以比较大小,判断两个指针是否指向同一块内存区域。
2.1指针 + - 整数
数组在内存中是连续存放的,因此我们只需要知道第一个元素的地址,就能找出所有的元素。
通过指针定位数组元素,让指针起始位置指向数组开头,通过*(p + i)的循环来使指针每次循环后移一位,实现打印数组。原理如下:
2.2指针 - 指针
int my_strlen(char* s)
{
char* p = s;
while (*p != '\0')
{
p++;
}
return p - s;
}
int main()
{
printf("%d\n", my_strlen("abcdefg"));
return 0;
}
这段代码的作用是模拟strlen函数计算字符串的长度,并输出结果。在 my_strlen
函数中,通过接收一个指向字符数组的指针作为参数,在循环中逐个移动指针直到遇到字符串结束符 \0
,然后返回指针 p
减去初始指针 s
的值,即字符串的长度。
在 main
函数中,调用 my_strlen
函数并将字符串常量 "abcdefg" 作为参数传递给该函数,最终打印出字符串的长度。因为字符串 "abcdefg" 的长度是7,所以程序输出的结果应该是 7。
3.野指针
野指针是程序设计中的一个术语,是指向不确定的内存地址的指针。这种情况通常发生在指针变量没有被初始化时。使用野指针可能会导致不可预测的行为,包括程序崩溃、数据损坏等严重问题。因此,在编程时需要特别注意避免野指针的出现。
3.1野指针的成因
3.1.1指针未初始化
#include <stdio.h>
int main()
{
int *p;//局部变量指针未初始化,默认为随机值
*p = 20;
return 0;
}
可以看到:使用了未初始化的局部变量"p",此时指针变量中存储的地址是随机的,可以指向任何位置。环境报错。
3.1.2指针越界访问
int main()
{
int arr[10] = { 0 };
int* p = &arr[0];
int i = 0;
for (i = 0; i <= 11; i++)
{
//当指针指向的范围超出数组arr的范围时,p就是野指针
*(p++) = i;
}
return 0;
}
该数组存储了10个整型元素,当循环进行到i=10时,指针对数组进行了越界访问,超出了其原本的合法的内存范围。
3.1.3指针指向的空间被释放
int* test()
{
int n = 100;
return &n;
}
int main()
{
int*p = test();
printf("%d\n", *p);
return 0;
}
这段代码演示了一个常见的编程错误:从函数返回一个局部变量的地址。这里,test
函数中的变量n
是一个局部变量,它在栈上分配内存。当test
函数执行完毕并返回时,n
占用的栈内存会被释放,此时返回的指针指向的内存区域已经不再保留n
的值,成为了一个悬挂指针(dangling pointer)。
尽管在某些情况下,通过这个悬挂指针访问原来变量的内存位置可能仍然能够得到原来的值(因为内存可能还未被其他数据覆盖),但这是完全不可靠的。程序的行为会因编译器、操作系统等因素而异,且可能导致不可预测的结果,包括程序崩溃或数据损坏等。
3.2如何规避野指针
规避野指针最为有效的方法,就是将指针初始化。如果明确知道指针指向哪⾥就直接赋值地址,如果不知道指针应该指向哪⾥,可以给指针赋值NULL。
NULL 是C语⾔中定义的⼀个标识符常量,NULL通常被定义为(void*)0,用于表示空指针。
#include <stdio.h>
int main()
{
int num = 10; // 定义一个整型变量num,并初始化为10
int *p1 = # // 定义一个指向整型的指针p1,并将其初始化为指向num的地址
int *p2 = NULL; // 定义一个指向整型的指针p2,并将其初始化为NULL,即空指针
return 0;
}
p1被初始化为指向num的地址,这意味着p1可以用来访问num的值。
p2被初始化为NULL,是一种特殊的指针值,表示它不指向任何有效的内存地址。
在应用中,如果我需要定义一个指针变量,但是暂时还不明确它到底指向哪里,可以先将其初始化为NULL,后期再做修改,也不会影响代码的调试。
4.传值调用和传址调用
如果我需要一个函数用来交换两个整型变量的值,写出如下代码:
void Swap1(int x, int y)
{
int tmp = x;
x = y;
y = tmp;
}
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
printf("交换前:a=%d b=%d\n", a, b);
Swap1(a, b);
printf("交换后:a=%d b=%d\n", a, b);
return 0;
}
Ctrl + F5 程序运行:
整型变量a和b的值并没有被交换,这是为什么?
用最简单的话说:实参(main
函数中的变量a
和b
)的值会被复制给形参(Swap1
函数中的变量x
和y
),而形参和实参占据不同的内存地址。因此,即使在Swap1
函数内部交换了x
和y
的值,这种改变也不会影响到原始变量a
和b
。
Swap1函数在使⽤的时候,是把变量本⾝直接传递给了函数,这叫传值调⽤。
实参传递给形参的时候,形参会单独创建⼀份临时空间来接收实参,对形参的修改不影响实参。
现在,我们要解决的就是当调⽤Swap函数的时候,Swap函数内部操作的就是main函数中的a和b,直接将a和b的值交换。那么就可以使⽤指针了,在main函数中将a和b的地址传递给Swap函数,Swap函数⾥通过地址间接的操作main函数中的a和b,并达到交换的效果就好了。
void Swap2(int*px, int*py)
{
int tmp = 0;
tmp = *px;
*px = *py;
*py = tmp;
}
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
printf("交换前:a=%d b=%d\n", a, b);
Swap2(&a, &b);
printf("交换后:a=%d b=%d\n", a, b);
return 0;
}
Ctrl + F5 程序运行:
我们可以看到实现成Swap2的⽅式,顺利完成了实现,这⾥调⽤Swap2函数的时候是将变量的地址传递给了函数,这种函数调⽤⽅式叫:传址调⽤。
传址调⽤,可以让函数和主调函数之间建⽴真正的联系,在函数内部可以修改主调函数中的变量。
5.二级指针
指针变量也是变量,是变量就有地址,那么指针变量的地址存放在哪里?
这里就需要引入二级指针,二级指针是指向指针的指针,即它存储的是另一个指针变量的地址。
int main()
{
int a = 10;
int* pa = &a;
int** ppa = &*pa;
return 0;
}
这里,ppa是一个二级指针,它存储了一级指针pa的地址。通过ppa,我们可以间接访问到a的值。操作过程如下:
*ppa得到ppa所指向的内容,即pa的值(a的地址)。
**ppa再次解引用得到pa所指向的内容,即变量a的值。
同理,存在多级指针,例如三级指针,四级指针...
6.指针数组
整型数组,是存放整型的数组;字符数组,是存放字符的数组。
指针数组,是存放指针的数组,数组的每个元素都是指针。
6.1使用场景
指针数组经常用于:
字符串数组:在C语言中,字符串可以通过字符数组来表示,而多个字符串就可以通过指针数组来存储,其中每个指针指向一个字符串(字符数组)。
char *strArray[] = {"Hello", "World", "C Language"};
6.2访问和操作
访问指针数组的元素和普通数组类似,可以通过下标来访问。但要注意,访问的结果是一个指针,如果要访问指针指向的值,需要进行解引用操作。
例如,假设有一个指向整型的指针数组int *arr[5];
,给第一个元素赋值并访问它指向的值可以这样做:
int value = 10;
arr[0] = &value; // 将arr的第一个元素指向value
printf("%d\n", *arr[0]); // 输出10,使用解引用操作符*来访问指针指向的值
7.数组指针变量
刚刚我们介绍了指针数组,指针数组是⼀种数组,数组中存放的是地址(指针)。
而数组指针变量是指针变量?还是数组?
答案是:指针变量。
数组指针变量(也称为指向数组的指针)是指向一个数组的指针。这种指针变量指向的是整个数组,而不是数组中的单个元素。数组指针在C语言中非常有用,特别是在函数参数传递中,它可以用来指向多维数组的行,从而实现对多维数组的高效处理。
7.1数组指针变量是什么?
int (*p) [10];
这里,p先和*结合,说明p是⼀个指针变量变量,然后指着指向的是⼀个⼤小为10个整型的数组。所以:
p是⼀个指针,指向⼀个数组,叫数组指针。
这⾥要注意:[ ]的优先级要⾼于*号的,所以必须加上()来保证p先和*结合。
7.2数组指针变量初始化
数组指针变量的初始化意味着在声明数组指针变量的同时,让它指向一个已经存在的数组,或者是动态分配的内存空间(后者通常用于动态数组)。数组指针的正确初始化对于保证程序的稳定性和避免未定义行为至关重要。
int arr[5] = {1, 2, 3, 4, 5};
int (*ptr)[5] = &arr;
通过监视,我们可以发现ptr与arr的类型是一样的,都为 int (*) [5];
通过以上的介绍,希望各位可以对指针有一个基本的初步认识;
如有不足之处恳请各位多多指出;
今后我也会发布指针进阶内容的文章;
希望大家可以多多支持~
我是高耳机。