指针是C语言的核心,没有指针C语言就没有灵魂,它的强大之处在哪呢?我们接下来慢慢讨论。
1.了解指针
我们从一段最简单的代码开始:
int a=10;
int p=&a;
很明显,a是一个整型变量,那么p是什么呢?
内存开辟了四个字节大小的空间,p就是记录这块空间的一个变量。
也就是说,p仍然是一个变量,只不过它的数据类型为整形指针。它存储的是另一个变量的内存地址。这个地址是计算机内存中的一个位置,我们可以通过这个地址找到存储在那里的值。听起来是不是很绕?不急,我们下面慢慢看。
我们首先认识两个操作符”*“和“&”。
- 取地址操作:使用&运算符可以获取一个变量的内存地址。例如,&a就是获取变量a的内存地址。
- 解引用操作:使用运算符可以获取指针指向的内存地址中的值。例如,p就是获取p指向的内存地址中的值。
我们接着看上一个例子:
int a = 10; // 定义一个整型变量a,并赋值为10
int *p = &a; // 定义一个整型指针p,并让p指向a的内存地址
printf("%d\n", *p); // 输出p指向的内存地址中的值,也就是10
2.指针的相关操作
其实"&"和“*”也是指针的操作,我们上面已经说过,这里的指针操作主要指的是指针的加减运算。
与其叫指针的加减运算,笔者其实更倾向与理解为指针的偏移,原因以下会说明。
- 指针的加法运算(指针加整数)
当我们对一个指针进行加法运算时,我们实际上是在移动这个指针在内存中的位置。例如,如果我们有一个整数指针p,那么表达式p+1会将p移动到下一个整数的位置。
例如:
int arr[] = {10, 20, 30, 40, 50};
int *p = arr;
p = p + 3;
在这个例子中,arr代表的意思是指针首元素的地址,也就是说p最初指向数组的第一个元素(10)。当我们执行p = p + 3时,p现在指向数组的第四个元素(40)。我们也可以理解为p指针向后偏移了3个单位,最终的位置就是指向40。
我们接着看第二个例子
char arr[] = {'a', 'b', 'c', 'd', 'e'};
char *p = arr;
p = p + 2;
在这个例子中,p最初指向数组的第一个元素('a')。当我们执行p = p + 2时,p现在指向数组的第三个元素('c')。
在这里可能有一些细心的小伙伴发现问题了:int类型是四个字节,char类型是一个字节,为什么它们指针加法运算的时候都能精准的移到合适的位置呢?
这是因为当我们对指针进行加法或减法运算时,实际上是在移动指针在内存中的位置。这个移动的距离是根据指针所指向的数据类型的大小来决定的。这就是为什么int类型的指针加1会移动4个字节,而char类型的指针加1会移动1个字节。
这种设计使得我们可以方便地通过指针来访问和操作内存中的数据。例如,如果我们有一个int数组,我们可以通过一个int类型的指针来遍历这个数组。每次将指针加1,就可以让指针移动到数组的下一个元素。
这也是为什么指针的加减运算可以精准地移动到合适的位置。因为编译器知道指针所指向的数据类型的大小,所以它可以根据这个大小来计算指针的移动距离。这样,无论我们是在操作int数组还是char数组,指针都可以正确地指向数组的每一个元素。
- 指针的减法运算(指针减整数)
指针的减法运算与加法运算类似,只不过方向相反。例如,如果我们有一个char类型的指针p,那么表达式p-1会将p移动到前一个元素的位置。
例如:
char arr[] = {'a', 'b', 'c', 'd', 'e'};
char *p = arr + 4;
p = p - 2;
在这个例子中,首先我们复习指针的加法运算,p指向了数组的最后一个元素('e')。当我们执行p = p - 2时,p现在指向数组的第三个元素('c')。
指针与指针的减法
两个指针之间的减法会得到这两个指针之间的元素数量。
例如:
char arr[] = {'a', 'b', 'c', 'd', 'e'};
char *p1 = arr;
char *p2 = arr + 4;
int diff = p2 - p1;
在这个例子中,diff的值为4,因为p2和p1之间有4个元素。
我曾经看到过一个很贴切地比喻:指针和指针的减法就像是日期减日期,得到的就是中间的天数。
那么就像日期加日期,没有任何意义,指针也一样,指针的加法运算(指针加指针)和乘法运算(指针乘以指针)在C/C++中是没有定义的。
3.多级指针
多级指针,顾名思义,就是指针的指针。也就是说,一个指针变量存储的是另一个指针变量的地址。这样的指针我们称之为二级指针。同样,我们也可以有三级指针,四级指针,等等。但是即使是我见过最复杂的项目,也只是用到二级指针,所以我们讨论二级指针即可。
例如:
int a = 10; // 定义一个整型变量a,并赋值为10
int *p = &a; // 定义一个整型指针p,并让p指向a的内存地址
int **pp = &p; // 定义一个二级整型指针pp,并让pp指向p的内存地址
printf(\"%d\n\", *pp); // 输出pp指向的指针所指向的内存地址中的值,也就是10
在这个例子中,我们首先定义了一个整型变量a和一个指向a的指针p。然后,我们定义了一个二级指针pp,它指向的是p的内存地址。当我们通过两次解引用操作(*)来访问pp指向的值时,我们实际上是在访问a的值。
它的用途在哪里呢?主要是在动态内存分配、函数参数传递等方面使用。
4.数组指针
数组指针就是数组的指针,它实际上是一种特殊的指针,它指向数组的第一个元素。这种指针非常有用,因为它允许我们通过指针来操作数组。其实我们上面提到的一个例子就是数组指针。
int arr[] = {10, 20, 30, 40, 50};
int *p = arr;
在这个例子中,我们定义了一个整型数组arr和一个指向arr的第一个元素的指针p。我们可以通过p来访问和操作arr中的元素。
例如,我们可以通过以下方式来输出arr中的第一个元素:
printf(\"%d\n\", p); // 输出10
我们也可以通过以下方式来修改arr中的第一个元素:
p = 100; printf(\"%d\n\", arr); // 输出100
这里,我们通过指针p来修改了arr中的第一个元素。这就是数组指针的强大之处。
5.指针的安全使用
在我们探索指针的神奇世界时,有一个重要的警告需要我们牢记:在使用指针时,要特别注意不要访问未分配的内存或释放的内存,这可能会导致程序崩溃或其他未定义的行为。
这听起来可能有点吓人,但别担心,我们将在这一部分详细解释这个问题,并讲讲如何来避免这种情况。
首先,让我们来理解一下什么是未分配的内存。当你声明一个指针变量时,它只是一个存储地址的容器,但这个地址可能指向任何地方。如果你没有明确地将它指向一个已经分配的内存区域(例如,一个已经声明的变量或一个通过malloc等函数分配的内存块),那么这个指针就是所谓的“野指针”。
试图通过这样的指针访问或修改内存是非常危险的,因为你可能会无意中改变程序的其他部分,或者访问到你的程序没有权限访问的内存。
例如:
int p; // 未初始化的指针,是一个野指针
p = 100; // 危险!我们不知道p指向哪里
同样,当你释放了一个内存块(例如,通过free函数),任何指向这个内存块的指针都会变成“已被释放的内存”。这些指针仍然保持着被释放的内存的地址,但这个地址已经不再属于你的程序,再次访问它就可能导致错误。
例如:
int p = malloc(sizeof(int)); // 分配一个整数的内存
free(p); // 释放内存
p = 100; // 危险!p现在是一个已被释放的内存!
为了避免这些问题,你应该遵循一些基本的规则:在声明指针变量时,总是将它初始化为NULL或一个已知的地址。在释放内存后,立即将任何指向它的指针设置为NULL。在使用指针之前,检查它是否为NULL。这样,你就可以确保你的指针总是指向有效的内存,或者至少不会无意中访问无效的内存。记住,指针是一个强大的工具,但是使用它需要谨慎和注意。
6.总结
以下是一些主要的要点:
1. 指针是一个变量,其值为另一个变量的内存地址。
2. "&"操作符用于获取一个变量的内存地址,"*"操作符用于获取指针指向的内存地址中的值。
3. 指针的加减运算实际上是在移动指针在内存中的位置,移动的距离根据指针所指向的数据类型的大小来决定。
4. 多级指针是指针的指针,一个指针变量存储的是另一个指针变量的地址。
5. 数组指针是指向数组的第一个元素的指针,可以通过数组指针来访问和操作数组中的元素。
注意事项:在使用指针时,要特别注意不要访问未分配的内存或释放的内存,这可能会导致程序崩溃或其他未定义的行为。同时,指针的加减运算需要根据指针所指向的数据类型的大小来进行,不能随意进行。
以上就是指针的基础,学到这里指针的大多数操作已经没有问题了,指针进阶我们下一篇讨论。