目录
- 🚀前言
- 🤔指针是什么
- 🌟指针基础
- 💯内存与地址
- 💯指针变量
- 💯 指针类型
- 💯const 修饰指针
- 💯指针运算
- 💯野指针和 assert 断言
- 💻数组与指针
- 💯数组名的理解
- 💯使用指针访问数组
- 💯一维数组传参的本质
- 💯指针数组
- ✍️高级指针概念
- 💯二级指针
- 💯函数指针
- 💯函数指针数组
- 💯回调函数
- 💯qsort 的使用与模拟实现
- ⚙️指针与字符串
- 💯字符指针
- 💯字符串处理函数(如strlen和sizeof的对比)
- 🐧总结
🚀前言
大家好!我是 EnigmaCoder。本文收录于我的专栏 C,感谢您的支持!
- 本文我们将进入C语言进阶篇的指针部分。
- 在C语言的编程世界里,指针堪称核心要素,发挥着不可替代的关键作用。从硬件交互层面看,指针赋予了开发者直接访问内存地址的能力,这在嵌入式系统开发等领域极为关键,能精准操控硬件设备的寄存器,达成对硬件的有效控制。从数据处理角度而言,指针极大提升了效率。例如在面对数组时,相比传统下标法,指针能更便捷、快速地遍历和操作数组元素,尤其在处理大型数组时优势尽显。同时,指针是构建复杂数据结构的基石,借助它可以巧妙连接链表节点,搭建树形、图形结构,让数据有序存储与流转。并且,在函数间的数据传递场景中,指针不仅实现了高效传递与共享,还能突破函数作用域限制,直接修改调用函数中的变量值 。
- 话不多说,我们开始吧。
🤔指针是什么
在 C 语言中,指针是一种特殊的变量,它存储的不是普通的数据值,而是内存地址
。可以把内存想象成一个庞大的、按顺序编号的仓库,每个存储单元都有对应的编号,这个编号就是地址,而指针就像是记录这些编号的小纸条,通过它能精准找到对应的数据存放位置。指针的存在,使得 C 语言能够直接对内存进行操作,极大地增强了语言的灵活性和效率,同时也是构建复杂数据结构和实现高级编程技巧的基础。
🌟指针基础
💯内存与地址
计算机的内存是由一系列连续的存储单元组成的,每个存储单元都被分配了一个唯一的编号,这个编号就称为内存地址。地址就如同房间的门牌号,通过它可以准确地找到存储在内存中的数据。例如,当我们定义一个变量int num = 10;
时,系统会在内存中为num
分配一块合适的空间,并赋予这块空间一个地址。
💯指针变量
- 指针变量是专门用来存储内存地址的变量。其定义方式为在变量名前加上
*
符号,同时需要指定所指向的数据类型。 - 以下是一个简单的示例:
#include <stdio.h>
int main() {
int num = 10;
int *ptr; // 定义一个指向int类型的指针变量ptr。这里的int表示ptr所指向的数据是int类型,*表明这是一个指针变量
ptr = # // 将num的地址赋给ptr,&是取地址运算符,通过它获取变量num在内存中的地址,并将其存储到指针变量ptr中
printf("num的地址是:%p\n", (void *)ptr); // %p是用于输出地址的格式说明符,(void *)是类型转换,将指针转换为通用指针类型进行输出
return 0;
}
💯 指针类型
- 指针的类型决定了它在解引用(即获取指针所指向的数据)时,从内存中读取多少个字节的数据。不同类型的指针占用的内存空间通常是相同的(在大多数常见系统中,指针占用 4 个字节或 8 个字节,取决于系统的位数),但它们对内存的访问方式有所不同。
- 例如:
#include <stdio.h>
int main() {
int num = 10;
int *int_ptr = #
char ch = 'a';
char *char_ptr = &ch;
printf("int指针大小:%zu字节\n", sizeof(int_ptr)); // sizeof运算符用于获取变量或数据类型占用的字节数,这里输出int类型指针的大小
printf("char指针大小:%zu字节\n", sizeof(char_ptr)); // 输出char类型指针的大小
return 0;
}
尽管
int_ptr和char_ptr
的大小相同,但当解引用int_ptr
时,会按照int
类型的大小(通常是 4 个字节,取决于系统和编译器)从内存中读取数据;而解引用char_ptr
时,只会读取 1 个字节的数据。
💯const 修饰指针
const
关键字用于修饰指针时,有三种不同的情况,它们对指针的可修改性产生不同的限制:
- 指向常量的指针:这种指针所指向的值不能通过该指针进行修改,但指针本身可以改变指向其他地址。
#include <stdio.h>
int main() {
int num1 = 10;
int num2 = 20;
const int *ptr = &num1;
// *ptr = 15; // 这是错误的,因为ptr是指向常量的指针,不能通过它修改指向的值
ptr = &num2; // 这是正确的,指针ptr可以改变指向,指向num2
return 0;
}
- 指针常量:指针本身的值(即所指向的地址)不能改变,但指向的值可以修改。
#include <stdio.h>
int main() {
int num1 = 10;
int num2 = 20;
int *const ptr = &num1;
// ptr = &num2; // 这是错误的,因为ptr是指针常量,不能改变其指向的地址
*ptr = 15; // 这是正确的,可以通过指针ptr修改其指向的值
return 0;
}
- 指向常量的指针常量:指针本身和它所指向的值都不能被修改。
#include <stdio.h>
int main() {
int num1 = 10;
const int *const ptr = &num1;
// *ptr = 15; // 错误,不能通过该指针修改指向的值
// ptr = &num2; // 错误,不能改变指针的指向
return 0;
}
💯指针运算
指针可以进行一些特定的算术运算,主要包括加法和减法运算。这些运算的结果与指针的类型密切相关。
#include <stdio.h>
int main() {
int arr[5] = {1, 2, 3, 4, 5};
int *ptr = arr;
printf("ptr指向的值:%d\n", *ptr); // 输出数组arr的第一个元素的值
ptr++; // 指针向后移动一个int类型的位置。因为ptr是指向int类型的指针,所以这里的++操作会使ptr向后移动sizeof(int)个字节
printf("移动后ptr指向的值:%d\n", *ptr); // 输出数组arr的第二个元素的值
return 0;
}
指针的减法运算常用于计算两个指针之间的元素个数。例如,在一个数组中,如果有两个指针
ptr1
和ptr2
都指向该数组的元素,ptr2 - ptr1
的结果就是它们之间相隔的元素个数(前提是两个指针类型相同)。
💯野指针和 assert 断言
野指针是指那些指向未定义内存区域的指针,例如,指向已释放的内存或者未初始化的指针。使用野指针会导致程序出现不可预测的错误,甚至崩溃。为了避免野指针带来的问题,可以使用assert
断言进行检测。assert
是 C 标准库中的一个宏,它接受一个表达式作为参数,如果表达式的值为假(通常是 0
),程序就会终止并输出错误信息。
#include <stdio.h>
#include <assert.h>
int main() {
int *ptr = NULL;
assert(ptr != NULL); // 如果ptr为空,程序会在这里终止,并输出断言失败的错误信息,提示具体的文件和行号等信息
return 0;
}
💻数组与指针
💯数组名的理解
在 C 语言中,数组名在大多数情况下会被隐式转换为指向数组首元素的指针。也就是说,数组名代表了数组在内存中的起始地址。
#include <stdio.h>
int main() {
int arr[5] = {1, 2, 3, 4, 5};
printf("数组首元素地址:%p\n", (void *)arr); // 输出数组arr的首元素地址
printf("数组首元素地址:%p\n", (void *)&arr[0]); // 输出数组第一个元素的地址,和arr代表的地址相同
return 0;
}
然而,需要注意的是,在两种情况下数组名不会被转换为指针:一是使用
sizeof
运算符对数组名进行操作时,sizeof(arr)
返回的是整个数组占用的字节数;二是使用&
运算符取数组名的地址时,&arr
得到的是整个数组的地址,虽然其数值可能和数组首元素地址相同,但类型是不同的。
💯使用指针访问数组
由于数组名可以看作是指向首元素的指针,所以可以通过指针来访问数组中的元素。常见的方式有两种:一是使用指针的算术运算,通过*(指针 + 偏移量)
的形式来访问;二是使用下标运算符[ ]
,实际上arr[i]
和*(arr + i)
是等价的。
#include <stdio.h>
int main() {
int arr[5] = {1, 2, 3, 4, 5};
int *ptr = arr;
for (int i = 0; i < 5; i++) {
printf("arr[%d]的值:%d(通过指针访问:%d)\n", i, arr[i], *(ptr + i));
// arr[i]使用数组下标访问元素,*(ptr + i)通过指针算术运算访问元素,两者结果相同
}
return 0;
}
💯一维数组传参的本质
当一维数组作为函数的参数进行传递时,实际上传递的是数组首元素的地址,而不是整个数组的副本。在函数内部,形参可以看作是一个指针。
#include <stdio.h>
void printArray(int *arr, int size) {
for (int i = 0; i < size; i++) {
printf("%d ", *(arr + i)); // 通过指针访问数组元素并输出
}
printf("\n");
}
int main() {
int arr[5] = {1, 2, 3, 4, 5};
printArray(arr, 5); // 将数组名arr作为参数传递给函数,实际上传递的是数组首元素的地址
return 0;
}
在
printArray
函数中,int *arr
接收的是数组的起始地址,因此在函数内部对arr
的操作实际上是对原数组的操作。
💯指针数组
指针数组是一种特殊的数组,它的每个元素都是一个指针。通常用于处理多个字符串或者指向不同数据类型的指针集合。
#include <stdio.h>
int main() {
int num1 = 10, num2 = 20, num3 = 30;
int *arr[3] = {&num1, &num2, &num3}; // 定义一个指针数组arr,其元素分别指向num1, num2, num3
for (int i = 0; i < 3; i++) {
printf("num的值:%d\n", *arr[i]); // 通过解引用指针数组的元素,获取所指向的变量的值
}
return 0;
}
在处理字符串时,指针数组非常有用,因为每个指针可以指向一个字符串的起始地址,方便对多个字符串进行管理和操作。
✍️高级指针概念
💯二级指针
二级指针是指向指针的指针,也就是说,它存储的是一个指针变量的地址。二级指针通常用于需要对指针进行间接操作或者处理指针数组的情况。
#include <stdio.h>
int main() {
int num = 10;
int *ptr = #
int **ptr2 = &ptr; // 定义二级指针ptr2,使其指向指针ptr
printf("num的值:%d\n", **ptr2); // 两次解引用二级指针,第一次得到ptr所指向的地址(即num的地址),第二次解引用得到num的值
return 0;
}
二级指针在动态内存分配和处理多维数组等场景中经常被使用。
💯函数指针
函数指针是一种特殊的指针,它指向的是函数在内存中的入口地址。通过函数指针,可以像调用普通函数一样调用它所指向的函数。函数指针的定义需要指定函数的返回类型和参数列表。
#include <stdio.h>
int add(int a, int b) {
return a + b;
}
int main() {
int (*func_ptr)(int, int); // 定义一个函数指针func_ptr,其指向的函数返回类型为int,接受两个int类型的参数
func_ptr = add; // 让函数指针指向add函数
int result = func_ptr(3, 5); // 通过函数指针调用add函数
printf("结果:%d\n", result);
return 0;
}
函数指针在实现回调函数、函数表等功能时非常有用,可以提高程序的灵活性和可扩展性。
💯函数指针数组
函数指针数组是一个数组,数组的每个元素都是一个函数指针。它常用于根据不同的条件选择调用不同的函数,类似于一个函数选择表。
#include <stdio.h>
int add(int a, int b) {
return a + b;
}
int subtract(int a, int b) {
return a - b;
}
int main() {
int (*func_arr[2])(int, int) = {add, subtract}; // 定义函数指针数组func_arr,包含两个函数指针,分别指向add和subtract函数
int result1 = func_arr[0](5, 3); // 通过函数指针数组调用add函数
int result2 = func_arr[1](5, 3); // 通过函数指针数组调用subtract函数
printf("加法结果:%d\n", result1);
printf("减法结果:%d\n", result2);
return 0;
}
在一些复杂的程序中,使用函数指针数组可以使代码结构更加清晰,便于维护和扩展。
💯回调函数
回调函数是通过函数指针实现的一种编程机制,即一个函数将另一个函数的指针作为参数传递给其他函数,在适当的时候,被调用的函数通过这个指针调用传入的函数。回调函数常用于异步操作、事件处理等场景。
#include <stdio.h>
void callback(int (*func)(int, int), int a, int b) {
int result = func(a, b); // 通过传入的函数指针调用函数
printf("回调函数结果:%d\n", result);
}
int multiply(int a, int b) {
return a * b;
}
int main() {
callback(multiply, 4, 5); // 将multiply函数的指针作为参数传递给callback函数
return 0;
}
这个例子中,
multiply
函数作为回调函数被callback
函数调用,实现了灵活的函数调用方式。
💯qsort 的使用与模拟实现
qsort
是 C 标准库中的一个通用排序函数,它可以对任意类型的数组进行排序。qsort
函数使用函数指针来比较数组元素,以确定它们的顺序。
- 以下是
qsort
的使用示例:
#include <stdio.h>
#include <stdlib.h>
int compare(const void *a, const void *b) {
return *(int *)a - *(int *)b; // 比较两个int类型元素的大小,用于qsort函数进行排序
}
int main() {
int arr[5] = {5, 4, 3, 2, 1};
qsort(arr, 5, sizeof(int), compare); // 对数组arr进行排序,参数分别为数组首地址、元素个数、每个元素的大小、比较函数
for (int i = 0; i < 5; i++) {
printf("%d ", arr[i]);
}
return 0;
}
模拟实现
qsort
函数可以帮助我们更好地理解其内部工作原理,虽然实际使用中直接调用标准库的qsort
更为方便,但模拟过程能加深对指针和排序算法的理解。
⚙️指针与字符串
💯字符指针
字符指针可以指向字符串常量或者字符数组。当字符指针指向字符串常量时,该字符串存储在内存的只读区域,不能被修改;当指向字符数组时,可以对数组中的字符进行修改。
#include <stdio.h>
int main() {
char *str = "Hello, World!"; // 字符指针str指向一个字符串常量。这里的字符串常量存储在只读内存区域,不能通过str修改其内容
printf("%s\n", str);
char arr[] = "C语言";
char *ptr = arr; // 字符指针ptr指向字符数组arr,此时可以通过ptr修改数组中的字符
printf ("% s\n", ptr);
ptr [0] = 'C'; // 修改字符数组中的字符,合法操作
//str [0] = 'h'; // 错误操作,试图修改字符串常量会导致运行时错误
return 0;
}
💯字符串处理函数(如strlen和sizeof的对比)
strlen
和sizeof
是两个在处理字符串时常用但功能完全不同的操作。
strlen
是C标准库中的一个函数,用于计算字符串的实际长度(不包括字符串结束标志\0
)。它从字符串的起始地址开始,逐个字符进行检查,直到遇到\0
为止,并返回已经检查过的字符个数。例如:
#include <stdio.h>
#include <string.h>
int main() {
char str[] = "Hello";
printf("strlen结果:%zu\n", strlen(str)); // 输出字符串"Hello"的长度,结果为5
return 0;
}
sizeof
是 C 语言的一个操作符,用于计算变量或数据类型占用的内存字节数。对于字符数组,sizeof
返回的是整个数组占用的内存空间大小,包括字符串结束标志\0
。例如:
#include <stdio.h>
#include <string.h>
int main() {
char str[] = "Hello";
printf("sizeof结果:%zu\n", sizeof(str)); // 输出字符数组str占用的字节数,结果为6(包括\0)
return 0;
}
理解
strlen
和sizeof
的区别对于正确处理字符串长度和内存分配等问题至关重要。
🐧总结
- 本文围绕 C 语言指针知识展开,从基础的内存地址、指针变量等概念,到数组与指针的关联应用,如用指针访问数组及一维数组传参本质;还涵盖了二级指针、函数指针等高级概念,以及指针在字符串处理中的运用,深入探讨复杂指针的实际应用 ,全面且系统地呈现了指针相关的知识体系。
- 希望能帮助到您!