C语言-指针的应用(指针的概念、常用案列、常见错误)
指针的概念
在C语言中,指针是一种特殊的变量(特殊的数据类型),它存储的是另一个变量的内存地址,从而可以直接访问和修改内存中的数据。这个变量可以是基本数据类型(如int、char等),也可以是数组、结构体等复杂数据类型。
指针的定义和初始化
-
在C中使用指针之前,您需要定义和初始化它,如:
char* ptr1; // 定义一个字符型指针 int* ptr2; // 定义一个整型指针
现在是一个空指针,不能使用它,必须让它指向一个对象地址进行初始化,从而可以访问/操作指定对象地址上的数据。
-
指针初始化,如下:
int val = 0x11223344; // 定义整型,并赋值10 ptr1 = (char*)&val; // 字符型指针赋值初始化, & 运算符获取val变量的地址,取址后 &val = int* 类型,(char*) 强制转换后赋值给 char* 类型的指针; ptr2 = &val; // 整型指针赋值初始化 printf("ptr1=0x%x, ptr2=0x%x\n", ptr1, ptr2 ); // 打印指针地址,结果输出:ptr1=0x404038, ptr2=0x404038 ,2个指向的指针地址一样 printf("ptr1=%x, ptr2=%x\n", *ptr1, *ptr2 ); // 打印指针地址上的数值 // 小端序(高位字节存放在高位地址) ptr1=44, ptr2=11223344 // 大端序(高位字节存放在低位地址) ptr1=11, ptr2=11223344 // 可见,指针是指向对象的首地址; char* 指针是跟随数据类型按单个字节取值, int* 指针是按4个字节取值
指针类型
- char *:用于存储单个字符或字符串。
- int *:用于存储整数。
- float *:用于存储浮点数。
- double *:用于存储双精度浮点数。
- void *:用于存储任何类型的值,但不能直接访问其内容。需根据原指针和数据类型地址,作类型强制转换。
- struct *:用于存储结构体变量。
- union *:用于存储联合体变量。
- enum *:用于存储枚举类型变量。
- typedef *:用于定义新的数据类型,并将其与已存在的数据类型关联起来。
- const *:用于创建常量指针,即无法修改其指向的内容。
指针的主要作用和使用案例
-
动态内存分配:使用指针,我们可以动态地分配和释放内存,这在处理变长数据结构或需要根据运行时情况创建和删除数据时非常有用。如下,使用案列:
#include <stdlib.h> // 包含动态内存分配和释放的函数需要用到这个头文件 #include <string.h> // 包含 memset() 、memcpy() 等函数 char* str = "hello word!"; // 定义指针,指向常字符串 char *ptr = (char*)malloc(100); // 动态分配一块可以存储100字节char型的内存空间,并将其地址赋值给ptr if(ptr == NULL) { // 如果分配失败,malloc会返回NULL,需要进行错误处理 printf("动态内存分配失败!\n"); return -1; } // 对 ptr 内存中的内容全部设置为0,以进行初始化; // ptr 分配内存成功后,内存空间里是上一次使用该地址空间时遗留的数据,需要清空一次,不然作字符串处理时会出现意想不到的bug memset(ptr, 0, 100); // 拷贝数据到 ptr 内存 memcpy(ptr, str, strlen(str)); printf("动态内存拷贝数据成功:%s \n", ptr); free(ptr); /// 若该指针后续不再使用,记得释放这块内存空间, 避免内存泄漏问题出现
-
通过指针修改变量的值:
int x = 10; int *ptr = &x; // 指针 ptr指向x的地址 printf("opt1:x值:%d, ptr值:%d \n", x, (*ptr)); // (*ptr) 对 ptr指针进行解引用(相当于 (*ptr) == x) *ptr = 20; // 通过指针修改x的值为20 printf("opt2:x值:%d, ptr值:%d \n", x, (*ptr));
-
指针作为函数参数传递:可以将指针作为函数参数传递,以便函数能够直接修改调用者提供的变量。
void add(int *a, int *b, int *c) { *c = *a + *b; // 将a和b的和赋值给c } int main() { int x = 10, y = 20, z; add(&x, &y, &z); // 传递x和y的地址给add函数,并将结果赋值给z printf("%d + %d = %d\n", x, y, z); // 输出10 + 20 = 30 return 0; }
-
节省空间:如果我们有一个数据结构(如数组或字符串)的引用,那么我们可以使用一个指针来代替整个数据结构,从而节省存储空间。
// 定义结构体数据类型 typedef struct{ int a; int b; int arrry[100]; }PTR_STRUCT_TEST; PTR_STRUCT_TEST ptrStructTest; // 申明结构体变量 unsigned long long ln = 0; // 定义long long 型变量 // 定义函数,进行参数传递和打印 void funcTest(unsigned long long ln1, PTR_STRUCT_TEST ptrStructTest1) { printf("opt2:ln大小:%d, ptrStructTest大小:%d \n", sizeof(ln1), sizeof(ptrStructTest1)); // 输出: ln大小:8, ptrStructTest大小:408 // 对ln1和结构体部分数值进行更改 ln1 = 0xAA; ptrStructTest1.a = 0xAA; ptrStructTest1.b = 0xBB; printf("opt2_1:ln=:%x, ptrStructTest.a:%x, ptrStructTest.b:%x \n", ln1, ptrStructTest1.a, ptrStructTest1.b ); // 输出: opt2_1:ln=:aa, ptrStructTest.a:aa, ptrStructTest.b:bb } // 定义函数,通过指针传递参数 void funcTestPtr(unsigned long long* ln1, PTR_STRUCT_TEST* ptrStructTest1) { printf("opt3:ln大小:%d, ptrStructTest大小:%d \n", sizeof(ln1), sizeof(ptrStructTest1)); // 输出: opt3:ln大小:8, ptrStructTest大小:8 // 对ln1和结构体部分数值进行更改 *ln1 = 0xAA; ptrStructTest1->a = 0xAA; ptrStructTest1->b = 0xBB; printf("opt3_1:ln=:%x, ptrStructTest.a:%x, ptrStructTest.b:%x \n", *ln1, ptrStructTest1->a, ptrStructTest1->b ); // 输出: opt3_1:ln=:aa, ptrStructTest.a:aa, ptrStructTest.b:bb } // 将结构体变量通过指针的方式进行返回,在其他文件获取该结构体参数时,不用重新定义结构体变量并赋值,可方便调用和节省空间 PTR_STRUCT_TEST* funcGetPtrStructTest(void) { return &ptrStructTest; } int main() { printf("opt1:ln大小:%d, ptrStructTest大小:%d \n", sizeof(ln), sizeof(ptrStructTest)); // 输出: opt1:ln大小:8, ptrStructTest大小:408 printf("opt1_1:ln=:%x, ptrStructTest.a:%d, ptrStructTest.b:%d \n", ln, ptrStructTest.a, ptrStructTest.b ); // 输出:opt1_1:ln=:0, ptrStructTest.a:0, ptrStructTest.b:0 // 调用函数,通过参数变量传递实参 funcTest(ln, ptrStructTest); printf("opt2_2:ln=:%x, ptrStructTest.a:%x, ptrStructTest.b:%x \n", ln, ptrStructTest.a, ptrStructTest.b ); //输出: opt2_2:ln=:0, ptrStructTest.a:0, ptrStructTest.b:0 // 调用函数,通过指针传递实参 funcTestPtr(&ln, funcGetPtrStructTest()); printf("opt3_2:ln=:%x, ptrStructTest.a:%x, ptrStructTest.b:%x \n", ln, ptrStructTest.a, ptrStructTest.b ); // 输出: opt3_2:ln=:aa, ptrStructTest.a:aa, ptrStructTest.b:bb // 可见,通过指针传递函数参数,不仅可以实现变量参数更改,而且可以极大地减小函数堆栈大小的开销 }
-
提升代码效率:通过指针,我们可以直接操作内存地址,可以避免对数据的重复复制和解析,从而提高程序的运行效率。
-
对于复杂数据结构的访问:例如,如果我们有一个多维数组或者链表等复杂的数据结构,使用指针可以方便地访问和操作这些数据结构中的元素。
指针使用的常见错误
- 空指针引用:如果试图访问一个未初始化的指针,或者尝试访问一个已经释放的指针,那么就会发生空指针引用错误。对这样的指针进行操作将导致程序崩溃。
- 指针越界:如果试图访问一个超出数组范围的指针,或者试图访问一个不存在的对象,那么就会发生指针越界错误。这将导致访问到其他变量的内存。如果这些变量保存了重要的数据,那么这些数据可能会被修改,从而引发程序错误。
- 指针算术运算:如果对指针进行不正确的算术运算,例如加法或减法,那么就可能会导致意外的结果。
- 类型错误:不同类型的指针在内存中占据不同的字节数,如果将一个类型的指针赋值给另一个类型,可能会引发未定义行为。如将一个整型指针转换为一个字符型指针,或者反之,那么就可能会导致数据丢失或错误。
函数指针和指针函数
函数指针和指针函数是C语言中两个容易混淆的概念,它们在本质和用途上有很大的不同。
-
函数指针:
函数指针是指向函数的指针变量。简单来说,函数指针是一种指针,它指向了一个函数而不是一个具体的对象。函数指针的作用主要是用于调用函数或者传递函数作为参数。通过使用函数指针,我们可以在不创建函数副本的情况下多次调用同一个函数,也可以将函数作为参数传递给其他函数。
例如,我们可以定义一个函数指针类型,如下所示:typedef void (*func_ptr)(int); // 这里,func_ptr 是一个函数指针类型,它指向了一个接受一个整数参数并返回void的函数。 // 我们可以通过以下方式定义和使用一个函数指针: void my_function(int x) { printf("%d\n", x); } int main() { func_ptr my_func = my_function; my_func(10); // 调用函数my_function,并传递参数10 return 0; }
-
指针函数:
指针函数是指返回指针类型的函数。简单来说,指针函数是一种函数,它返回了一个指针而不是一个具体的值。指针函数的作用主要是用于返回一个指向特定数据结构的指针,或者用于实现更复杂的数据处理逻辑。通过使用指针函数,我们可以从函数中获得一个指向内存中特定位置的指针,从而可以访问和修改该位置的值。
例如,我们可以定义一个指针函数类型,如下所示:int* my_ptr_function(int x) { static int my_variable = 0; my_variable += x; return &my_variable; } // 这里,my_ptr_function 是一个指针函数类型,它接受一个整数参数并返回一个指向整数的指针。 // 我们可以通过以下方式调用和使用这个指针函数: int main() { int* my_ptr = my_ptr_function(10); printf("%d\n", *my_ptr); // 输出10,因为my_ptr指向的变量值为10 return 0; }