在C语言中,指针是一个非常重要的概念,它提供了直接访问内存地址的能力。指针变量用于存储内存地址,而不是数据值,在某种意义上和门牌号具有相似含义:指针是一个变量,其存储的是另一个变量的内存地址,这个内存地址唯一的标识,用于指向特定的内存位置。门牌号也是用来唯一标识一个具体的房屋或地址的,但指针的使用要复杂得多,涉及到内存的管理、指针的运算、野指针的避免等多个方面。在处理数组、字符串、动态内存分配以及函数参数传递等方面使得程序员们能够编写出更灵活、更高效的代码。
指针的概念
指针是一个变量,其值为另一个变量的地址,即直接指向内存中的某个位置,指针的声明需要在变量类型前加上星号*
,像int *ptr;
就声明了一个指向整数的指针变量ptr
。
指针的用途和功能
-
动态内存管理 :C语言允许程序员在运行时动态地分配和释放内存,通过指针来实现,如使用
malloc
、calloc
和realloc
等函数分配内存,使用free
函数释放内存。 -
数组操作:指针可以用来遍历数组,因为数组名本质上是一个指向数组首元素的指针。使用指针进行数组操作比使用数组索引更加高效。
-
字符串处理:在C语言中字符串是通过字符数组实现的。因此,字符串操作(如复制、拼接等)可以通过指针操作来实现。
-
函数参数传递:通过使用指针作为函数参数,可以在函数内部修改外部变量的值,实现数据的双向传递。
-
指向函数的指针:指针也可以指向函数,这使得可以将函数作为参数传递给其他函数,或者通过指针调用函数。
-
指向指针的指针:C语言允许创建指向指针的指针,这在进行复杂数据结构(如链表、树等)的操作时非常有用。
指针操作的大概流程如下 :
指针的基础操作
声明并初始化指针
在定义指针前需要先声明一个整数变量(我定义的value
)并初始化,随后声明一个指向整数的指针变量(我声明的是pointer
,很多人习惯使用p
)并初始化为value
的地址
c
int value = 10;
int * pointer = &value;
在C语言中这里的三种写法都是可以的:
c
int* pointer = &value;
int * pointer = &value;
int *pointer = &value;
随后即可通过访问指针来查询这个整型变量(value
)的值:
c
printf("通过指针访问的值: %d \n", *pointer);
完整代码如下:
c
#include <stdio.h>
int main() {
int value = 10;
int *pointer = &value;
printf("通过指针访问的值: %d\n", *pointer);
return 0;
}
输出内容:通过指针访问的值: 10
修改指针指向的值
在C语言中,如果已经声明并初始化了一个指针,可以做到只修改指针所指向的值,原变量的数值不会改变。就好比结婚一样,老实的Java程序员大锤和小美去民政局登记结婚(为一个变量声明并初始化了一个指针),但大锤满足不了小美了(原数值因为各种原因在某项功能中需要进行改动),小美又不想离婚(修改原变量值),于是小美就找到了老王来满足她(修改指针所指向的值),以后小美还可以找老陈、老宋、老李(多次修改指针所指向的值),但是这样会造成:1.大锤没法活了(程序崩溃
),2.孩子不是大锤亲生(野指针
),3.家丑外扬(内存泄漏
),4.小美被玩坏了(数据损坏
)。根据刚才的代码继续编写:
c
#include <stdio.h>
int main() {
int value = 10;
int *pointer = &value;
printf("通过指针访问的值: %d\n", *pointer);
// 修改指针指向的值
*pointer = 20;
printf("修改后通过指针访问的值: %d\n", *pointer);
printf("直接访问变量value的值: %d\n", value);
return 0;
}
输出内容可以看出,指针所指向的值发生了更改,而原变量的值未发生任何变化:
取地址和解引用操作
在C语言中取地址操作是将变量的地址赋值给指针,而解引用操作则是通过指针访问它所指向的变量的值。这两个操作在C语言的指针使用中非常重要,它们允许我们通过指针间接地访问和操作内存中的数据。
修改一下之前的代码,通过&运算符将value的地址赋给了pointer,进行了取地址操作
c
#include <stdio.h>
int main() {
int value = 20;
int *pointer;
// 在指针变量中存储value的地址,即取地址操作
pointer = &value;
printf("value变量的地址: %p\n", &value);
printf("通过pointer指针访问value变量的值: %d\n", *pointer);
return 0;
}
输出中的0x7ffdee0e6ddc
是变量value在内存中的地址,内存地址是操作系统分配给程序用于存储数据的物理或虚拟内存位置,每个程序运行时,操作系统都会为其分配一块内存空间,程序中的变量就存储在这块空间的特定地址上,同时每次程序运行时,操作系统可能会分配不同的内存地址给程序中的变量,0x7ffdee0e6ddc
这个地址只是在这次运行程序时有效,下次运行时可能会有所不同。
指针的算术运算
指针的算术运算分为指针加减运算和指针相减运算
指针加减运算
:指针可以进行加减运算,其结果是指针向前或向后移动若干个元素的距离(不是字节),移动的字节数取决于指针指向的数据类型。
指针相减运算
:两个指针相减的结果是两个指针之间相隔的元素个数,要求两个指针指向同一块内存区域。
以下代码定义了两个数组:一个short
类型的dataset
数组和一个double
类型的bills
数组,每个数组都有SIZE 4
个元素。然后,它定义了两个指针变量pti
和ptf
,分别指向这两个数组的起始位置,随后代码进入一个循环,遍历这两个数组。在每次迭代中,它都会计算并打印出pti
和ptf
指针在加上index
值后的地址。这里pti + index
和ptf + index
分别表示pti
和ptf
指针向前移动index
个short
或double
元素的位置。由于指针的加减运算是以它指向的数据类型的大小为单位进行的,所以pti
每次增加2个字节(因为short类型通常占2个字节),而ptf
每次增加8个字节(因为double类型通常占8个字节)。在打印指针地址时,代码将指针转换为void*
类型。这是因为printf
函数使用%p
格式说明符来打印指针,而%p
期望一个void*
类型的参数。将指针转换为void*
类型可以确保无论指针指向什么类型的数据,都能以统一的方式打印其地址。
c
#include <stdio.h>
#define SIZE 4
int main()
{
short dataset[SIZE];
short *pti;
short index;
double bills[SIZE];
double *ptf;
pti = dataset;
ptf = bills;
printf("%23s %15s\n", "short pointers", "double pointers");
for (index = 0; index < SIZE; index++)
{
printf("pointers + %d: %10p %10p\n", index, (void*)(pti + index), (void*)(ptf + index));
}
return 0;
}
代码会在终端输出以下内容:
bash
short double
pointers + 0: 0x7ffc8b926ef8 0x7ffc8b926f00
pointers + 1: 0x7ffc8b926efc 0x7ffc8b926f10
pointers + 2: 0x7ffc8b926f00 0x7ffc8b926f20
pointers + 3: 0x7ffc8b926f04 0x7ffc8b926f30
指针的比较
在C语言中可以使用关系运算符(如==、<、>等)来比较两个指针,比较的是它们所指向的地址的大小。
这里定义了一个长度为10的数组array
,随后声明了三个指针ptr1、ptr2、ptr3
,ptr1
指向array
的第3个元素 ,ptr2
指向array
的第6个元素 ,ptr3
与ptr1
指向相同的地址:
c
#include <stdio.h>
int main() {
int array[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int *ptr1, *ptr2, *ptr3;
ptr1 = &array[2]; // ptr1指向array的第3个元素
ptr2 = &array[5]; // ptr2指向array的第6个元素
ptr3 = ptr1; // ptr3与ptr1指向相同的地址
// 比较ptr1和ptr2
if (ptr1 < ptr2) {
printf("ptr1 < ptr2\n");
} else {
printf("ptr1 >= ptr2\n");
}
// 比较ptr1和ptr3
if (ptr1 == ptr3) {
printf("ptr1 == ptr3\n");
} else {
printf("ptr1 != ptr3\n");
}
// 比较ptr2和ptr1
if (ptr2 > ptr1) {
printf("ptr2 > ptr1\n");
} else {
printf("ptr2 <= ptr1\n");
}
return 0;
}
代码运行后再终端输出:
bash
ptr1 < ptr2
ptr1 == ptr3
ptr2 > ptr1
指针与数组
在C语言中,数组名可以视为指向数组首元素的指针,因此可以使用指针来遍历数组元素,可以使用指针算术运算来访问数组中的元素。
以下代码中的dqys
不仅代表了一个包含12个整数的数组,同时也可以被看作是一个指向int
类型的指针,它指向dqys
数组的第一个元素,在随后的for
循环中使用了指针算术运算来遍历数组dqys
。表达式dqys + index
表示指针dqys
向前移动index
个int
类型元素的位置。因为dqys
是一个指向int的指针,所以每次递增都会使指针地址增加sizeof(int)
个字节。通过解引用操作符*
可以获取该地址处的值,即数组dqys
中索引为index
的元素的值。当index
为0时,*(dqys + 0)
就相当于dqys[0]
,它表示数组的第一个元素,其值为31。同理,*(dqys + 1)
则相当于dqys[1]
,它表示数组的第二个元素,其值为28:
c
#include <stdio.h>
#define MONTHS 12
int main()
{
int dqys[MONTHS] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
int index;
for (index = 0; index < MONTHS; index++)
{
printf("%2d 月有 %d 天. \n", index + 1, *(dqys + index));
}
return 0;
}
代码输出内容如下:
bash
1 月有 31 天.
2 月有 28 天.
3 月有 31 天.
4 月有 30 天.
5 月有 31 天.
6 月有 30 天.
7 月有 31 天.
8 月有 31 天.
9 月有 30 天.
10 月有 31 天.
11 月有 30 天.
12 月有 31 天.
指针与函数
C语言中允许指针指向函数,这使得可以将函数作为参数传递给其他函数,或者通过指针调用函数,同时也可以通过将指针作为函数参数传递,可以在函数内部修改外部变量的值。
这里的modifyValue
函数接收一个int
类型的指针作为参数,并通过该指针修改外部变量的值。add
函数用来返回两个整数的和。executeFunction
函数接收一个函数指针作为参数,并通过该函数指针调用函数。随后在main
函数中,首先使用modifyValue
函数通过指针修改外部变量的值。然后声明了一个函数指针functionPointer
,并将其指向add
函数。最后将该函数指针作为参数传递给executeFunction
函数,并通过该函数指针调用add
函数。
c
#include <stdio.h>
// 定义一个函数,该函数接收一个int类型的指针作为参数
void modifyValue(int *value) { //函数接收一个int类型的指针作为参数
*value = 10; // 通过指针修改外部变量的值
}
int add(int a, int b) { // 函数用来返回两个整数的和
return a + b;
}
void executeFunction(int (*func)(int, int), int a, int b) { // 接收一个函数指针作为参数,并调用该函数
int result = func(a, b); // 通过函数指针调用函数
printf("结果为: %d\n", result);
}
int main() {
int variable = 0;
printf("数值修改后: %d\n", variable);
modifyValue(&variable); // 将变量的地址传递给函数以修改其值
printf("数值修改前: %d\n", variable);
// 使用函数指针调用函数
int (*functionPointer)(int, int) = add; // 声明一个函数指针,并将其指向add函数
executeFunction(functionPointer, 5, 3); // 将函数指针作为参数传递给executeFunction函数
return 0;
}
动态内存分配
在C语言中提供了malloc
和free
函数用于动态内存分配和释放。malloc
函数用于分配指定大小的内存空间,并返回一个指向该空间的指针;free
函数用于释放已分配的内存空间。
下面代码中先声明了一个int
类型的指针ptr
,并将其初始化为NULL
。然后使用malloc
函数动态分配了足够存储5个整数的内存空间,并将返回的指针赋值给ptr
。随后使用指针算术运算访问和修改动态分配的内存空间中的内容。之后使用for
循环打印出动态分配的内存空间中的内容。最后使用free
函数释放了已分配的内存空间,并将ptr
重新设置为NULL
避免空指针。
c
#include <stdio.h>
#include <stdlib.h>
int main() {
int *ptr = NULL; // 声明一个int类型的指针,并初始化为NULL
int n = 5; // 存储5个整数
ptr = (int*)malloc(n * sizeof(int)); // 使用malloc函数动态分配内存空间,分配了n个int大小的内存空间,并将返回的指针转换为int类型的指针
// 检查malloc函数是否成功分配了内存,如果内存分配失败就退出程序
if (ptr == NULL) {
printf("内存分配GG了\n");
return 1;
}
// 使用指针访问和修改动态分配的内存空间中的内容,通过指针算术运算访问数组元素,并赋值
for (int i = 0; i < n; i++) {
*(ptr + i) = i + 1;
}
// 通过指针算术运算访问数组元素,并打印动态分配的内存空间中的内容
for (int i = 0; i < n; i++) {
printf("%d ", *(ptr + i));
}
printf("\n");
// 使用free函数释放ptr指向的内存空间
free(ptr);
ptr = NULL; // 释放内存后,ptr变成了悬空指针,建议将ptr重新设置为NULL以避免悬空指针的问题
return 0;
}
多级指针、指针数组、const指针、void指针
在C语言中出了基础的指针,以下的几种指针方式也很常见
多级指针 :指向指针的指针,用于实现更复杂的数据结构和操作,如动态内存分配中的二维数组。
指针数组 :数组中的元素是指针类型,常用于存储多个字符串或指向函数的指针。
const指针 :指向常量的指针或指针常量,用于限制指针的指向或指针所指向的值不可修改。
void指针:通用指针类型,可以指向任意类型的数据,但在使用前通常需要类型转换。
c
#include <stdio.h>
#include <stdlib.h>
int main() {
// 多级指针,指向指针的指针
int value = 10;
int *ptr1 = &value;
int **ptr2 = &ptr1;
int ***ptr3 = &ptr2;
int ****ptr4 = &ptr3;
int *****ptr5 = &ptr4;
int ******ptr6 = &ptr5;
int *******ptr7 = &ptr6;
printf("通过多级指针访问值:%d\n", *******ptr7);
// 存储字符串
char *strings[] = {"Hello", "Gayboy", "GGBond"};
int i;
for (i = 0; i < 3; i++) {
printf("指针数组中的字符串:%s\n", strings[i]);
}
const int constValue = 20;
const int *constPtr = &constValue;
// *constPtr = 30; 不能修改指向常量的指针所指向的值
printf("指向常量的指针:%d\n", *constPtr);
// 指针常量
int anotherValue = 30;
int *const constPtr2 = &anotherValue;
// constPtr2 = &value; 这样就是错误的,指针常量的值不可修改
*constPtr2 = 40;
printf("指针常量的指向值:%d\n", *constPtr2);
// 最后设一个通用指针
int intValue = 50;
float floatValue = 3.14f;
void *voidPtr;
voidPtr = &intValue;
printf("通过void指针访问int值:%d\n", *(int *)voidPtr);
voidPtr = &floatValue;
printf("通过void指针访问float值:%f\n", *(float *)voidPtr);
return 0;
}
运行结果如下: