本篇目录
- C语言 指针(特别篇)
- 内存地址
- 简要介绍C语言指针
- C语言的指针可以指向什么?
- 取地址符 ``&``(Address-of Operator)
- C语言中的 ``*`` 号运算符
- 示例集:
- 指向变量的指针
- 指向数组的指针
- 指向字符串的指针
- 二级指针
- 指针数组的数组名是一个二级指针
- 二维数组 || 矩阵 || 二级指针
- 指向结构体的指针
- 动态申请内存
- 指向函数的指针
- 指向函数的指针压入栈中实现递归调用
- 多级指针
- 三级指针
C语言 指针(特别篇)
C语言是一种十分重要的编程语言,广泛应用于计算机领域,尤其是操作系统、编译器、网络通信等方面。其中,指针是C语言中非常重要的概念和基础,本文主要介绍C语言中各种指针的用法。
内存地址
学好C语言指针的关键在于要深刻理解计算机中的内存地址。
计算机中的内存地址是指用来唯一标识存储单元的值。这些存储单元按照连续的方式构成了计算机的内存空间,每个存储单元可以存储一个字节(8位)的数据。
内存地址通常以十六进制表示,它们从0开始递增,直到最大地址。在32位系统中,最大地址为0xFFFFFFFF(4GB),而在64位系统中,最大地址可达到0xFFFFFFFFFFFFFFFF(18EB,1EB等于10^9GB)。
内存地址空间可分为以下几个部分:
-
代码段(Code Segment):
代码段存储程序的机器指令,也称为可执行代码。它是只读的,用来存放程序的指令集和常量数据。 -
数据段(Data Segment):
数据段存储程序的全局变量、静态变量和静态常量。它包含了已经初始化或默认初始化的数据,并且在程序运行期间不会发生变化。 -
BSS段(Block Started by Symbol):
BSS段存储未初始化的全局变量和静态变量。在程序加载时,BSS段的变量会自动被初始化为0或空指针。 -
堆(Heap):
堆是动态分配内存的区域,用于存储程序运行时动态申请的数据。在C语言中,通过函数如malloc()和free()来管理堆内存的分配与释放。 -
栈(Stack):
栈用于存储程序运行时的局部变量、函数参数和调用信息。栈是一种先进后出的数据结构,通过指针栈顶位置来实现栈帧的压入和弹出。 -
运行时堆栈:
运行时堆栈是保存函数调用过程中使用的局部变量、中间结果和返回地址的区域。每个函数调用都会在运行时创建一个新的堆栈帧,并在函数返回时销毁。
这些内存地址空间的划分使得程序能够有效地管理内存资源,并提供了不同类型数据的存储区域。对于程序员来说,理解计算机的内存地址模型是编写高效、可靠代码的基础之一。
简要介绍C语言指针
C语言的指针是一种变量,它可以存储内存地址作为值。指针的 本质是通过存储内存地址来提供对数据的间接访问和操作能力。
在计算机中,内存被划分成一个个存储单元,每个存储单元都有唯一的地址。在C语言中,指针允许我们将这些地址作为值存储起来,并通过指针来访问和修改所指向的内存单元中存储的数据。
指针的本质是在内存中存储的一个整数值,该值代表某个特定内存单元的地址。通过使用指针,我们可以直接操作内存中的数据,无需通过变量名来访问。这为程序员提供了更灵活、高效地处理数据的能力。
指针的特点有以下几点:
- 指针保存变量的内存地址,而不是变量的实际值。
- 通过指针可以直接读取或修改指向的内存单元中的数据。
- 指针在使用前需要进行初始化,即将指针指向特定地址。
- 可以通过指针进行数据的传递和共享,使得函数可以修改传入的变量的值。
总结起来,C语言的指针提供了一种强大的工具,能够在程序中灵活地操作内存中的数据。通过指针,我们可以实现动态内存管理、数组和字符串处理、数据结构的构建等功能。理解指针的本质能够帮助程序员更好地利用C语言进行开发和优化。C语言指针对于一些初学者来说可能很难,但是一旦掌握了C语言指针,将会大大提高编程的效率。
C语言的指针可以指向什么?
- 变量:指针可以指向不同类型的变量,包括整型、浮点型、字符型等。通过指针,可以访问和修改指向变量所存储的值。
- 数组元素:数组名本质上是指向数组首元素的指针,可以使用指针来遍历数组,访问和修改数组元素的值。
- 字符串:字符串实际上是由一系列字符组成的字符数组,在C语言中以空字符(‘\0’)结尾。可以使用指针来操作字符串,包括遍历、拷贝和连接等操作。
- 结构体:结构体是用户自定义的复合数据类型,可以包含多个不同类型的成员。指针可以指向结构体变量,允许通过指针访问和修改结构体中的成员。
- 动态分配的内存:C语言提供了动态内存管理的功能,可以使用指针来指向通过malloc()、calloc()等函数动态分配的内存块,并在不需要时释放该内存。
- 函数:在C语言中,函数也被视为一种特殊的数据类型。指针可以指向函数,称为函数指针。通过函数指针,可以调用相应的函数及传递函数作为参数。
取地址符 &
(Address-of Operator)
在C语言中,取地址符(Address-of Operator)用于获取变量的地址。取地址符使用符号"&"表示,放置在变量名之前。
以下是取地址符的使用示例:
int num = 10;
int *ptr = # // 取得变量num的地址,并将其赋值给指针ptr
printf("变量num的地址:%p\n", &num);
printf("指针ptr存储的地址:%p\n", ptr);
C语言中的 *
号运算符
在C语言中,星号(*)是一元运算符,具有多种作用,取决于它所用的上下文。以下是星号运算符在C语言中的几种常见作用:
- 声明指针类型:在变量声明时,星号可以用作指针类型的标识符。例如,
int *ptr;
声明了一个名为ptr
的指向整型变量的指针。 - 解引用操作符(Dereference Operator):使用星号对指针进行解引用操作,可以访问指针所指向的内存地址存储的值。例如,
int x = *ptr;
将会将指针ptr
指向的内存地址的值赋给变量x
。 - 定义函数指针:使用星号可以定义函数指针,用于存储函数的地址。例如,
int (*funcPtr)(int, int);
定义了一个指向接受两个int类型参数并返回int
类型值的函数的指针。 - 乘法运算符:星号还可以用于乘法运算,表示两个数相乘的结果,此时
*
号就变成了二元运算符。例如,int result = a * b;
将变量a
和b
相乘的结果赋给变量result
。
示例集:
指向变量的指针
int num = 10;
int *p; // 定义一个整型指针
p = # // 将指针p指向变量num的内存地址, & 为取地址符, 可以将变量的内存地址取出
printf("%d\n", *p); // 输出变量num的值
指向数组的指针
C语言中,数组名本身就是指向数组首个元素的指针,可以通过指针访问整个数组的元素。
int arr[5] = {1, 2, 3, 4, 5};
int *ptr = arr; // 数组名arr即是指向数组首个元素的指针
for (int i = 0; i < 5; i++) {
printf("%d ", *(ptr + i)); // 输出数组元素
}
数组名本身就是一个指针:
int arr[5] = {1, 2, 3, 4, 5};
printf("数组的首地址为 %d\n", arr);
for (int i = 0; i < 5; i++) {
printf("内存地址为 %d 中存储的数据为 %d\n", arr, *(arr + i)); // 输出数组元素
}
指向字符串的指针
#include <stdio.h>
int main() {
char *name = "John"; // 指向字符常量"John"的指针
printf("Name: %s\n", name); // 使用%s格式化输出字符串
return 0;
}
#include <stdio.h>
int main() {
char *fruits[] = {"Apple", "Banana", "Orange"}; // 字符串指针数组
int i;
for (i = 0; i < 3; i++) {
printf("Fruit: %s\n", fruits[i]); // 使用循环遍历并输出每个字符串
}
return 0;
}
字符串的本质就是字符数组,也可以用指向数组的指针来操作字符串。
二级指针
二级指针 就是 指向 指针变量 的 指针。指针变量本质也是变量,只不过这个变量存储的是其他变量的内存地址罢了。既然是变量就有内存地址,所以还可以再定义一个指针变量,用再定义的指针变量来存储前面那个指针变量的内存地址,于是后定义的指针变量就指向了前面的指针变量。这个后定义的指针变量就是二级指针。
int num = 10;
int *p1 = # // 指向变量num的指针
int **p2 = &p1; // 指向指针p1的指针(二级指针)
printf("%d\n", **p2); // 输出变量num的值
指针数组的数组名是一个二级指针
#include <stdio.h>
#include <string.h>
int main()
{
int i;
char *color[5]={"red","blue","yellow","green","black"}; //字符串数组, 或者说指针数组
char **pc; //二级指针
char str[20];
pc=color;
printf("Input a color:");
scanf("%s",str);
for(i=0;i<5;i++)
if(strcmp(str,*(pc+i))==0)
break;
if(i<5)
printf("position:%d\n",i+1);
else
printf("Not Found\n");
return 0;
}
二维数组 || 矩阵 || 二级指针
#include <stdio.h>
#include <stdlib.h>
// 函数:创建并初始化矩阵
int** createMatrix(int rows, int cols) {
int** matrix = (int**)malloc(rows * sizeof(int*)); // 分配行指针数组的内存空间
for (int i = 0; i < rows; i++) {
matrix[i] = (int*)malloc(cols * sizeof(int)); // 分配每一行的内存空间
for (int j = 0; j < cols; j++) {
matrix[i][j] = i + j; // 初始化矩阵元素的值
}
}
return matrix;
}
// 函数:释放矩阵的内存空间
void freeMatrix(int** matrix, int rows) {
for (int i = 0; i < rows; i++) {
free(matrix[i]); // 释放每一行的内存空间
}
free(matrix); // 释放行指针数组的内存空间
}
// 函数:打印矩阵
void printMatrix(int** matrix, int rows, int cols) {
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
printf("%d ", matrix[i][j]); // 输出矩阵元素的值
}
printf("\n");
}
}
int main() {
int rows = 3, cols = 3;
int** matrix = createMatrix(rows, cols);
printf("Matrix:\n");
printMatrix(matrix, rows, cols);
freeMatrix(matrix, rows);
return 0;
}
指向结构体的指针
typedef struct {
char name[20];
int age;
} Person;
Person person;
Person *ptr = &person; // 指向结构体Person的指针
strcpy(ptr->name, "Tom"); // 修改结构体成员name的值
ptr->age = 25; // 修改结构体成员age的值
printf("%s %d\n", ptr->name, ptr->age); // 输出结构体成员的值
对于指向结构体的指针可以参考我的往期文章,内容很详细 : C语言结构体数组+结构体类型指针+指向结构体数组的指针+typedef类型
动态申请内存
动态申请内存的两个函数 malloc()
和 calloc()
函数声明包含在 stdlib.h
头文件中。
#include <stdio.h>
#include <stdlib.h>
int main() {
int size;
int *arr;
printf("请输入数组大小:");
scanf("%d", &size);
// 动态分配内存
arr = (int *)malloc(size * sizeof(int));
if (arr == NULL) {
printf("内存分配失败\n");
return 1;
}
// 初始化数组元素
for (int i = 0; i < size; i++) {
arr[i] = i + 1;
}
// 打印数组元素
printf("数组元素:");
for (int i = 0; i < size; i++) {
printf("%d ", arr[i]);
}
// 释放内存
free(arr);
return 0;
}
上述代码中,首先通过scanf函数获取用户输入的数组大小。然后使用malloc函数动态分配大小为size乘以sizeof(int)的内存空间,并将返回的指针赋值给整型指针变量arr。
接着,我们通过遍历数组对其元素进行初始化。最后,使用循环打印数组元素。
在程序末尾,使用free函数释放动态分配的内存空间,防止内存泄漏。
请注意,在使用完动态分配的内存后,一定要记得及时释放,以确保不会造成内存泄漏。
以下是一个使用 calloc
函数的例子:
#include <stdio.h>
#include <stdlib.h>
int main() {
int size;
int *arr;
printf("请输入数组大小:");
scanf("%d", &size);
// 使用calloc动态分配内存,并初始化为零
arr = (int *)calloc(size, sizeof(int));
if (arr == NULL) {
printf("内存分配失败\n");
return 1;
}
// 打印数组元素
printf("数组元素:");
for (int i = 0; i < size; i++) {
printf("%d ", arr[i]);
}
// 释放内存
free(arr);
return 0;
}
在上述代码中,我们先通过scanf函数获取用户输入的数组大小。然后使用calloc函数动态分配大小为size乘以sizeof(int)的内存空间,并将返回的指针赋值给整型指针变量arr。
由于使用了calloc函数,所分配的内存会被自动初始化为零。因此,在打印数组元素时,我们可以看到初始时它们都是零。
同样地,在程序末尾,使用free函数释放动态分配的内存空间,防止内存泄漏。
指向函数的指针
C语言中,函数其实也有一个入口地址。我们可以用指针来指向函数。函数入口地址是指函数在内存中的起始位置的地址。每个函数都有一个唯一的函数入口地址,它表示函数在可执行程序中的位置,让程序能够定位并调用该函数。
函数入口地址通常由编译器在编译阶段确定,并在链接器将各个模块合并成可执行程序时进行填充。当程序调用一个函数时,实际上是通过函数入口地址来跳转到该函数的代码执行处。
在C语言中,函数入口地址可以使用函数指针来表示和操作。函数指针是一个特殊类型的指针,它可以存储函数的入口地址,以便后续调用该函数。通过获取函数的入口地址,我们可以将其赋值给函数指针,并通过该指针间接调用函数。
#include <stdio.h>
int add(int a, int b) {
return a + b;
}
int subtract(int a, int b) {
return a - b;
}
int multiply(int a, int b) {
return a * b;
}
int main() {
int (*operation)(int, int); // 声明一个指向函数的指针
operation = add; // 将add函数的地址赋值给指针
printf("Result of addition: %d\n", operation(5, 3)); // 通过指针调用add函数
operation = subtract; // 将subtract函数的地址赋值给指针
printf("Result of subtraction: %d\n", operation(5, 3)); // 通过指针调用subtract函数
operation = multiply; // 将multiply函数的地址赋值给指针
printf("Result of multiplication: %d\n", operation(5, 3)); // 通过指针调用multiply函数
return 0;
}
指向函数的指针压入栈中实现递归调用
当我们运行递归函数的时候,操作系统其实做的就是将每次调用的函数地址压入栈中。
在函数递归调用时,函数指针的压入栈中可以实现递归调用的原理。具体来说,当一个函数通过函数指针调用自身时,它需要将函数指针的值压入栈中,并保留其他必要的参数和局部变量。
下面是一个简单的示例代码,展示了利用函数指针实现递归调用的原理:
#include <stdio.h>
// 定义递归函数
int recursiveFunc(int n, int (*func)(int)) {
if (n <= 0) {
return 0;
}
// 调用函数指针指向的函数,并将结果与递归调用相加
return func(n) + recursiveFunc(n - 1, func);
}
// 定义一个打印数字的函数
int printNumber(int num) {
printf("%d ", num);
return num;
}
int main() {
int n = 5;
// 将打印数字的函数指针作为参数传递,并压入栈实现递归调用
int result = recursiveFunc(n, printNumber);
printf("\n结果:%d\n", result);
return 0;
}
在上面的例子中,我们定义了一个递归函数recursiveFunc,它接受两个参数:n表示递归的终止条件,func表示指向函数的指针。递归函数首先判断终止条件,如果满足则返回0;否则,调用函数指针指向的函数,并将结果与递归调用的结果相加。
我们还定义了一个打印数字的函数printNumber,它接收一个整数并在控制台上打印该数字。在main函数中,我们将打印数字的函数指针作为参数传递给递归函数,并将起始值设为5。
当程序运行时,递归函数会依次调用打印数字的函数,并在每次调用时打印当前的数字。最后,递归函数的结果被打印出来。
通过将指向函数的指针作为参数传递并压入栈,可以实现函数的递归调用。这种方法可以动态地指定需要执行的函数,并在函数执行过程中保持递归的状态。
多级指针
理论上指针的级数可以无限增长,但是通常没用应用的必要。记住一点,只有同级指针且指针指向的数据类型相同时,可以相互赋值。
三级指针
#include <stdio.h>
#include <stdlib.h>
void printMatrix(int **matrix, int rows, int cols) {
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
printf("%d ", matrix[i][j]);
}
printf("\n");
}
}
void createMatrix(int ***matrix, int rows, int cols) {
*matrix = (int **)malloc(rows * sizeof(int *));
for (int i = 0; i < rows; i++) {
(*matrix)[i] = (int *)malloc(cols * sizeof(int));
for (int j = 0; j < cols; j++) {
(*matrix)[i][j] = i * cols + j;
}
}
}
void freeMatrix(int ***matrix, int rows) {
for (int i = 0; i < rows; i++) {
free((*matrix)[i]);
}
free(*matrix);
*matrix = NULL;
}
int main() {
int **matrix = NULL;
int rows = 3;
int cols = 3;
createMatrix(&matrix, rows, cols);
printf("Matrix:\n");
printMatrix(matrix, rows, cols);
freeMatrix(&matrix, rows);
return 0;
}
在这个例子中,我们使用三级指针 int ***matrix
来操控矩阵。首先,在 createMatrix
函数中,我们通过传递指向指针的指针来分配内存并创建矩阵。然后,在 printMatrix
函数中,我们使用二级指针 int **matrix
来遍历和打印矩阵的元素。最后,在 freeMatrix
函数中,我们使用三级指针来释放矩阵占用的内存。
请注意,在操作三级指针时需要小心管理内存,并确保正确地分配和释放内存,以避免内存泄漏和错误。
三级指针的应用
三维数组在编程中有许多应用场景,特别是在涉及到多维数据的存储和处理时非常有用。以下是一些常见的三维数组的应用示例:
-
三维图像处理:在计算机图形学和图像处理领域,三维数组经常用于表示和处理彩色图像或体积数据。图像可以被看作是由像素组成的二维阵列,而每个像素又包含红、绿、蓝(RGB)或其他颜色通道的值,这样就可以使用三维数组来表示图像数据。
-
三维空间建模:在三维建模、游戏开发和虚拟现实中,三维数组可以用于表示三维空间中的物体、场景或地形。例如,一个三维场景可以被分成一个网格,每个网格单元包含物体的属性、纹理信息或碰撞检测数据等。
-
多维物理模拟:在物理模拟和科学计算中,三维数组可以用于存储和更新三维空间中的物理量,如速度场、压力场或温度场。通过使用三维数组,可以对不同位置上的物理量进行存储和操作,并模拟复杂的物理过程。
-
数据立方体:三维数组还可以用于表示和分析包含多个维度的数据集,例如销售数据、气象数据或市场调查数据。这些数据通常以数据立方体(data cube)的形式进行分析和查询,其中三维数组的每个维度对应于数据集中的一个属性。
这些只是三维数组的一些常见应用示例,实际上,它们在各种领域和问题中都具有广泛的应用。使用三维数组时,需要理解索引和访问元素的方式,并根据具体的问题进行适当的操作和算法设计。