目录
1.c指针
C语言中的指针是一项强大而灵活的特性,能极大地提升程序的性能和灵活性。篇初,我声明c指针的主要作用,也就是为什么我们要用指针,在什么情况下使用,接下来我会按照作用进行代码示例和解析。下图是此篇学习的思维导图。
正篇开始-------------------------------------------------------------------------------------------------------------------
1.1 c指针的定义
指针就是内存地址,我们所说的指针变量也就是存放内存地址的变量,变量有int,float等等类型,那我们的指针变量也需要不同的类型来存放内存地址变量。下图是四种类型指针的声明:
int *ip; /* 一个整型的指针 */
double *dp; /* 一个 double 型的指针 */
float *fp; /* 一个浮点型的指针 */
char *ch; /* 一个字符型的指针 */
下面一个int类型指针的声明和输出。
#include <stdio.h>
int main() {
// 定义一个int类型的变量
int num = 42;
// 定义一个int类型的指针变量,并指向num
int *ptr = #
// 输出指针ptr的值(地址)和它所指向的值
printf("指针ptr的地址: %p\n", ptr);
printf("变量num的地址: %p\n", &num);
printf("ptr所指向的值: %d\n", *ptr);
return 0;
}
在c语言中,指针的占位符用“%p”,上图我们定义了一个int类型的num变量并赋值42,接下来我们定义一个int 类型的指针变量ptr,通过“&”符号取到变量num的地址并将地址赋给指针变量ptr。
上面的示例中,三个输出分别展示了,指针指向的地址,变量num的地址,以及通过“*”取值的输出。
1.2 c指针作用
1. 直接访问内存:指针让你如同掌控一把钥匙,能够直接锁定内存地址,从而实现低级别的内存操作。
2. 动态内存分配:使用指针,你可以在运行时灵活地申请和释放内存,就像在自家花园中随意种植和剪除植物一样。通过 malloc和 free 等函数,你能够根据需要管理内存,避免资源浪费。
3. 数组和字符串处理:指针使得遍历数组和处理字符串的过程更为高效。想象一下,指针就像一条快速的通道,让你迅速访问每一个元素,而无需逐一复制。
4. 函数参数传递(将指针作为函数参数进行传递):指针让函数可以直接对变量进行操作,而不是简单地传递值。就好比你传递了一张地图,指向了真实的目的地,这样函数可以在原地进行改变,而不必返回一份副本。
5. 构建数据结构:指针是实现复杂数据结构(如链表、树和图)的基石。它们就像是连接不同节点的桥梁,使得数据在内存中灵活流动,构建出复杂的关系网络。
6. 多级指针:C语言的多级指针让你能够构建更复杂的数据结构,甚至可以创建指向指针的指针。这种能力为你提供了更多的灵活性,适用于更复杂的场景。
7. 提高效率:指针能有效减少数据复制,尤其在处理大数据时,显著提升程序的运行效率。想象你在厨房里,使用指针就像是直接操作食材,而不是每次都准备新的份量,节省了时间和空间。
1.2.1 直接访问内存:
c语言是面向过程的编程语言,涉及到对于内存的操作时候,在c语言,我们访问内存的方式就是通过指针,那么指针式如何直接访问内存的,下面是一个指针定义(int * p)以及访问内存的简单示例:
int a = 10;
int *p = &a; // p指向a的地址
printf("%d\n", *p); // 输出10,直接访问a的值
通过示例,我们了解可以通过指针进行内存的访问,访问内存的步骤第一步声明一个指针(p),并指向要访问的变量地址(&a),最后通过*对指针进行值的访问。
1.2.2 动态的内存分配:
内存分配在c语言中也叫动态管理,在进行动态内存分配时要引入头文件#include<stdlib.h>
在<stdlib.h>头文件中,提供以下四个函数为我们进行内存操作时使用。
1.calloc函数
calloc 函数用于分配一块内存,返回指向这块内存的指针,并将这块内存初始化为零。
下面我举例一个使用calloc分配内存,解释各参数的含义。这里需要明白:在C语言中,使用动态内存分配函数(如 calloc
和 malloc
)可以创建指针类型的内存块。虽然 arr
是一个指针,但由于它指向了一系列连续的内存位置,您可以像访问数组一样使用下标语法 arr[i]
来访问这些内存位置
int *arr = (int *)calloc(5, sizeof(int));代表分配一个可以存放5个整数内存块,这段代码的作用是通过calloc分配了一块可以存5个int类型元素的内存,int类型的字节数是4byte,所以通过该代码为内存分配了20个字节。(int *)
作用是将 calloc
函数返回的 void *
类型指针转换为 int *
,保持指针指向内存的数据类型一致。
需要注意的是:calloc
不仅分配内存,还会将分配的内存区域初始化为零。
#include <stdio.h>
#include <stdlib.h>
int main() {
int num = 5;
int *arr = (int *)calloc(num, sizeof(int)); // 分配5个int整型空间
if (arr == NULL) {
fprintf(stderr, "内存分配失败\n");
return 1;
}
for (int i = 0; i < num; i++) {
printf("arr[%d] = %d\n", i, arr[i]);
}
free(arr); // 释放内存
return 0;
}
2.malloc函数
该函数用于分配指定大小的内存块,但不会初始化。返回指向内存的指针。malloc 函数的参数需要用 * 号连接
// void *malloc(int num)
#include <stdio.h>
#include <stdlib.h>
int main() {
int num = 5;
int *arr = (int *)malloc(num * sizeof(int)); // 分配5个整型空间
if (arr == NULL) {
fprintf(stderr, "内存分配失败\n");
return 1;
}
for (int i = 0; i < num; i++) {
printf("arr[%d] = %d\n", i, arr[i]); // 值是未知的
}
free(arr); // 释放内存
return 0;
}
3.realloc函数
realloc函数用于重新分配内存,可以增加或减少已分配的内存块大小
#include <stdio.h>
#include <stdlib.h>
int main() {
int num = 5;
int *arr = (int *)malloc(num * sizeof(int)); // 初始分配5个整型空间
for (int i = 0; i < num; i++) {
arr[i] = i + 1; // 初始化数组
}
int newNum = 10;
arr = (int *)realloc(arr, newNum * sizeof(int)); // 扩展到10个整型空间
if (arr == NULL) {
fprintf(stderr, "内存重新分配失败\n");
return 1;
}
for (int i = 0; i < newNum; i++) {
printf("arr[%d] = %d\n", i, arr[i]); // 新分配的部分内容未定义
}
free(arr); // 释放内存
return 0;
}
4.free函数
在上面的示例中,我们已经使用free(arr);来释放分配的内存。 使用 `free` 释放动态分配的内存,以防止内存泄漏。
1.2.3 数组和字符串处理(指针指向数组)
在C语言中,字符串实际上是字符数组,可以通过指针来实现字符串的操作和遍历,可以通过解引用来访问字符,例如 *ptr
,并通过指针递增来遍历字符。(我们也可以通过字符串索引访问元素)。
#include <stdio.h>
int main() {
// 定义字符数组
char str[] = "Hello, World!";
char *ptr = str; // 指针指向字符串的首字符
// 使用指针遍历字符串并打印每个字符
while (*ptr != '\0') { // 当指针指向的字符不是 '\0'
printf("%c ", *ptr); // 打印当前字符
ptr++; // 移动指针到下一个字符
}
printf("\n");
return 0;
}
在 C 语言中,数组名可以被视为指向数组第一个元素的指针。这是理解数组和指针之间关系的关键。
- 数组名本身并不是一个指针,但它在表达式中通常会被视为指向数组第一个元素的指针。
- 在 C 语言中,数组名(如
arr
)代表了数组的首地址。当你将数组作为参数传递给函数时,传递的是指向数组第一个元素的指针。这意味着在函数内部,你可以使用指针算术或数组下标来访问数组的元素(在1.2.7里有提到)
#include <stdio.h>
int main() {
// 定义一个整型数组
int arr[] = {10, 20, 30, 40, 50};
// 使用数组名访问第一个元素
printf("First element using array name: %d\n", arr[0]);
// 使用指针访问第一个元素
printf("First element using pointer: %d\n", *arr); // arr 被当作指向第一个元素的指针
// 遍历数组
for (int i = 0; i < 5; i++) {
// 使用数组名和指针形式访问元素
printf("Element %d: %d\n", i, *(arr + i)); // arr+i 是指向第 i 个元素的指针
}
return 0;
}
1.2.4 指针作为函数参数传递(改变参数值)
这里(*p)++涉及到一个解自用和自增的操作,具体如下解释:
- (*p) 通过操作符 * 获取 p 指向的地址中的值。在这个上下文中,(*p) 实际上就是 number 的值。
- ++ 是自增运算符,它会将 (*p) 的值增加 1。在这里,(*p)++ 有两种操作: (*p) 会被解引用,取得 number 的当前值(例如,5)。然后,++ 操作符会将这个值增加 1,变成 6,并把新的值写回到 number 的内存地址。
#include <stdio.h>
// 函数声明,接受一个int类型的指针作为参数
void increment(int *p) {
// 通过指针修改原始变量的值
(*p)++;
}
int main() {
int number = 5;
printf("Before increment: %d\n", number); // 输出原始值
// 将number的地址传递给increment函数
increment(&number);
printf("After increment: %d\n", number); // 输出修改后的值
return 0;
}
1.2.5 单向链表(使用指针)
链表由多个节点组成,每个节点包含数据和一个指向下一个节点的指针。这里的代码示例会使用到结构体,代码结构稍微复杂。
#include <stdio.h>
#include <stdlib.h>
// 定义链表节点结构
struct Node {
int data; // 节点的数据部分
struct Node* next; // 指向下一个节点的指针
};
// 在链表末尾添加新节点的函数
void appendNode(struct Node** head_ref, int new_data) {
struct Node* new_node = (struct Node*)malloc(sizeof(struct Node)); // 分配新节点的内存
struct Node* last = *head_ref; // 用于遍历链表
new_node->data = new_data; // 设置新节点的数据为传入参数
new_node->next = NULL; // 新节点的下一个指针初始化为 NULL(表示它是最后一个节点)
// 如果链表为空,则将新节点设置为头节点
if (*head_ref == NULL) {
*head_ref = new_node; // 将新节点赋值给链表的头指针
return; // 结束函数
}
// 否则,找到链表的最后一个节点
while (last->next != NULL) { // 遍历直到找到最后一个节点
last = last->next; // 移动到下一个节点
}
// 将最后一个节点的 next 指针指向新节点
last->next = new_node; // 链接新节点
}
// 打印链表内容的函数
void printList(struct Node* node) {
while (node != NULL) { // 当当前节点不为空时
printf("%d -> ", node->data); // 打印当前节点的数据
node = node->next; // 移动到下一个节点
}
printf("NULL\n"); // 打印链表结束标志
}
// 主函数
int main() {
struct Node* head = NULL; // 初始化链表的头指针为 NULL,表示链表为空
// 向链表中添加节点
appendNode(&head, 1); // 添加节点数据为 1
appendNode(&head, 2); // 添加节点数据为 2
appendNode(&head, 3); // 添加节点数据为 3
appendNode(&head, 4); // 添加节点数据为 4
// 打印链表
printf("链表内容: ");
printList(head); // 输出: 1 -> 2 -> 3 -> 4 -> NULL
// 释放链表内存
struct Node* current = head; // 当前节点初始化为头节点
struct Node* next_node; // 下一个节点指针
while (current != NULL) { // 当当前节点不为空时
next_node = current->next; // 保存下一个节点的指针
free(current); // 释放当前节点的内存
current = next_node; // 移动到下一个节点
}
return 0; // 程序结束
}
1.2.6 多级指针(二级指针)
我们这里以二级指针举例,一个指针包含一个变量的地址。当我们定义一个指向指针的指针时(二级指针),第一个指针包含了第二个指针的地址,第二个指针指向包含实际值的位置。
使用双指针可以方便地创建动态的二维数组。例如,假设你需要一个不规则的矩阵,可以通过多级指针来实现:
row代表行,cols代表列,int **matrix
: 这是一个指向指针的指针(二级指针),也就是一个二维数组的类型。matrix
是一个数组,包含 rows
个指针,每个指针指向一个包含 cols
个整数的数组。matrix
用来存储指向每一行的指针
#include <stdio.h> // 引入标准输入输出库
#include <stdlib.h> // 引入标准库以使用malloc和free函数
int main() {
int rows = 3; // 定义矩阵的行数
int cols = 4; // 定义矩阵的列数
int **matrix = (int **)malloc(rows * sizeof(int *)); // 这行代码分配了一个指针数组 (大小为 rows),其中每个元素都是一个指向整数数组的指针,连续内存。
for (int i = 0; i < rows; i++) {
matrix[i] = (int *)malloc(cols * sizeof(int)); // 为每一行分配内存,分配4个int类型的内存
}
for (int i = 0; i < rows; i++) { // 填充矩阵
for (int j = 0; j < cols; j++) {
matrix[i][j] = i * cols + j; // 生成连续数字
}
}
for (int i = 0; i < rows; i++) { // 打印矩阵
for (int j = 0; j < cols; j++) {
printf("%d ", matrix[i][j]); // 输出每个元素
}
printf("\n"); // 每行结束后换行
}
for (int i = 0; i < rows; i++) {
free(matrix[i]); // 释放每一行的内存
}
free(matrix); // 释放存储行指针的总指针
return 0; // 返回0表示程序成功结束
}
1.2.7 提高效率
#include <stdio.h>
void processArray(int* arr, int size) {
for (int i = 0; i < size; i++) {
arr[i] *= 2; // 对数组中的每个元素乘以2
}
}
int main() {
int size = 1000000;
int arr[size]; // 在堆栈上定义一个数组
// 初始化数组
for (int i = 0; i < size; i++) {
arr[i] = i;
}
// 只需要传递 arr,不需要取地址
processArray(arr, size); // 将 arr 作为参数传递
// 输出部分结果以验证
for (int i = 0; i < 5; i++) {
printf("%d ", arr[i]); // 输出前5个元素
}
return 0;
}
当你将一个大型数组直接作为参数传递给函数时,如果不使用指针,编译器会复制整个数组。这意味着内存中的每个元素都需要被逐一复制到新创建的数组中,这在处理大数组时会导致显著的性能开销。
1.3 补充:指针函数和函数指针的区别
这里做一个指针函数和函数指针的说明,本来应该将指针函数放入c函数来讲的,这里讲到了指针方便区分。
最简单的辨别方式就是看函数名前面的指针*号有没有被括号( )包含,如果被包含就是函数指针反之则是指针函数。
1.3.1 指针函数:
本质是一个函数,此函数返回某一类型的指针。
#include <stdio.h>
#include <stdlib.h>
// 定义一个指针函数,它返回一个整型指针
int* createArray(int size) {
int* arr = (int*)malloc(size * sizeof(int)); // 动态分配内存
for (int i = 0; i < size; i++) {
arr[i] = i + 1; // 填充数组
}
return arr; // 返回数组指针
}
int main() {
int size = 5;
int* array = createArray(size); // 调用指针函数
// 打印数组内容
for (int i = 0; i < size; i++) {
printf("%d ", array[i]);
}
printf("\n");
free(array); // 释放动态分配的内存
return 0;
}
这里释放内存需要注意:
使用 free(array);
是正确的,因为 array
持有有效的动态内存地址。free(arr);
是不正确的,因为 arr
的作用域已经结束,无法安全地访问。
1.3.2 函数指针:
本质是一个指针,指向函数的指针变量,其包含了函数的地址,通过它来调用函数。 它是指向函数的指针变量,即本质是一个指针变量
#include <stdio.h>
// 定义一个简单的加法函数
int add(int a, int b) {
return a + b;
}
// 定义一个简单的减法函数
int subtract(int a, int b) {
return a - b;
}
// 主函数
int main() {
// 定义一个函数指针,指向接受两个整数并返回整数的函数
int (*operation)(int, int);
operation = add; // 将函数指针指向加法函数
printf("Add: %d\n", operation(5, 3)); // 调用加法函数
operation = subtract; // 将函数指针指向减法函数
printf("Subtract: %d\n", operation(5, 3)); // 调用减法函数
return 0;
}