当前位置: 首页 > article >正文

C语言指针操作

1、指针基础

1.1、指针的声明

        指针变量用于存储内存地址,声明时需要指定指向的数据类型:

/* 指针的声明 */
char* c;		/* 指向字符的指针 */
int* p;			/* 指向整型的指针 */
float* f;		/* 指向浮点数的指针 */

         指针只能指向某种特定类型的对象,即每个指针都必须指向某种特定的数据类型。例如上面示例中,“int *p;”就是说明p是一个指向整型对象的指针。但是有一个例外,指向void类型的指针可以存放指向任何类型的指针,但是它不能间接引用其自身。

        指向任何对象的指针都可以转换为void *类型,且不会丢失信息。如果将结果再转换为初始指针类型,则可以恢复初始指针。指针可以被赋值为void *类型的指针,也可以赋值给void *类型的指针,并可与void *类型的指针进行比较。

        但是要注意一个问题,上面的三个指针仅是作为声明,一般情况下不要这样写,是比较危险的,因为三个指针变量指向哪里我们无法确定。如果覆盖到其他的内存区域,甚至是系统正在使用的关键区域,十分危险,但是这种情况系统一般会驳回程序的运行,此时程序会被中止并报错。万一覆盖到一个合法的地址,那么接下来的赋值就会导致一些有用的数据被莫名其妙地修改,此类bug是十分不好排查的,所以使用指针的时候一定要注意初始化。

int a = 10;
int *p = &a;    /* 声明指针时立即初始化 */

// 或者将指针指向NULL
int *pa = NULL;    /* 暂时不知道该指向哪的时候先指向NULL */

         在C语言中,NULL被称为空指针,该指针不指向任何数据。本身是一个宏定义:

#define NULL ((void *)0)

         在大部分操作系统中,地址0通常都是一个不被使用的地址,如果一个指针指向NULL,意味着不指向任何东西。

1.2、取地址操作符(&)

        通过 & 获取变量的内存地址:

/* 通过 & 获取变量的内存地址 */
int a = 10;
int* p = &a;                    /* p存储变量a的地址 */
printf("address = %p\n", p);	/* address = 0000008655AFF774 */
printf("address = %p\n", &a);	/* address = 0000008655AFF774 */

1.3、解引用操作符(*)

        通过 * 访问指针指向的内存内容:

/* 通过 * 访问指针指向的内存内容 */
int a = 10;
int* p = &a;
printf("a = *p = %d\n", *p);	/* 输出10(通过p访问a的值) */

        直接通过变量名来访问变量的值称为直接访问,而通过指针这样的形式访问变量值称之为间接访问,因此解引用操作符也可称为间接运算符。

2、指针的常见操作

2.1、指针赋值

        指针可以直接指向另一变量的地址:

int a = 10, b = 20;
int* p = &a;
printf("a = *p = %d\n", *p);	/* 输出10(通过p访问a的值) */
p = &b;
printf("*p = %d\n", *p);		/* 输出20(通过p访问b的值) */

2.2、指针算术运算

        指针支持加减运算(按数据类型大小移动地址):

/* 指针算术运算 */
int arr[3] = { 10, 20, 30 };
int* p = arr;					/* p指向数组首元素 */
p++;							/* p指向数组第二个元素arr[1] */
printf("%d\n", *p);				/* 输出20,对应arr[1]的值 */

         上述示例中,p 是指向数组首元素的指针,那么 p++ 将对 p 进行自增运算并指向下一个元素arr[1];而 p += i 将对p进行加i的增量运算,使其指向指针 p 当前所指向的元素之后的第 i 个元素。

         指针的减法运算的意义:如果 p1 和 p2 指向相同数组中的不同元素,且 p1 < p2,那么 p2-p1+1 指向的元素之间的元素数目。可以编写一个获取字符串的长度的自定义函数strlen()。

int strlen(char* s)
{
	char* p = s;
	while (*p != '\0')
		p++;
	return p - s;
}

int main()
{
    char s[] = "Hello World!";				/* 定义一个数组 */
    char* p = "Hello World!";				/* 定义一个指针 */
    printf("strlen(s) = %d\n", strlen(s));	/* 12 */
    printf("strlen(p) = %d\n", strlen(p));	/* 12 */

    return 0;
}

         上述示例中,需要特别注意 s 和 p 的区别,尽管它们的长度是一样的。但是 s 只是一个存放初始化字符串以及空字符 '\0' 的一维数组,只是 s 作为数组首元素的地址进行传递,数组中的单个字符可以进行修改。但是 p 是一个指针,始终指向同一个存储位置,其初值指向一个字符串常量,之后它可以被修改以指向其他的地址,但不允许修改字符串的内容。

2.3、指针比较

        在某些情况下指针可以通过“==”、“=”、“>”、“<”来比较地址的大小:

/* 指针比较 */
int arr[5];
int* p1 = &arr[0];
int* p2 = &arr[4];
if (p1 < p2) {
	printf("p1的地址在p2之前!\n");
}

         上述指针进行比较的前提是,指针 p1 和 p2 是指向同一数组中不同元素的指针。任何指针与 0 进行相等或不等的比较运算都有意义。但是指向不同数组元素的指针之间的算术或比较运算没有定义。有一个特例:指针的算术运算中可使用数组最后一个元素的下一个元素的地址。

3、常量指针和指针常量

        常量指针和指针常量的记忆技巧:

  • 常量指针:const在 * 左侧,如const int *p,表示“指向常量的指针”。记忆口诀:内容不可变,指针可变。
  • 指针常量:const在 * 右侧,如int* const p,表示“指针本身是常量”。记忆口诀:指针不可变,内容可变。 

3.1、常量指针(Pointer to Constant)

        常量指针表示指针指向的内容是常量,不能通过该指针修改指向的值,但指针本身可以指向其他地址。其声明语法为:

const 数据类型 *指针名;

// 或

数据类型 const *指针名;

/* 常量指针 */
int a = 10;
const int* p = &a;			/* p是常量指针,指向的int不可变 */

// *p = 20;					/* 错误:不能通过p修改a的值 */
a = 20;						/* 正确:直接修改变量a的值是允许的 */
printf("*p = %d\n", *p);	/* *p = 20 */

int b = 30;
p = &b;						/* 正确:指针可以指向其他变量 */
printf("*p = %d\n", *p);	/* *p = 30 */

        特点:

  • 指向的值不可通过指针修改。
  • 指针本身可以重新指向其他地址。
  • 常用于函数参数,避免意外修改外部数据。 
void printData(const int* arr, int size)
{
    for (int i = 0; i < size; i++) {
        printf("%d ", arr[i]);        /* 确保不会修改数组内容 */
    }
}

 3.2、指针常量(Constant Pointer)

        指针常量表示指针本身是常量,即指针的指向不可变(必须初始化),但可以通过指针修改指向的值。其声明语法为:

数据类型 *const 指针名 = 初始地址;

/* 指针常量 */
int a = 10;
int* const p = &a;				/* p是指针常量,指向不可变 */
printf("p = %p\n", (void*)p);

*p = 20;						/* 正确:可以通过p修改a的值 */
printf("a = %d\n", a);			/* a = 20 */
printf("p = %p\n", (void*)p);

int b = 30;
// p = &b;						/* 错误:指针的指向不可变 */

        特点:

  • 指针的指向固定,不可修改。
  • 可以通过指针修改指向的值。
  • 常用于固定访问某个内存地址的场景(如硬件寄存器)。
int reg = 0x1000;
int* const REG_ADDR = (int*)reg;    /* 指向固定地址 */

*REG_ADDR = 1;                      /* 修改寄存器值 */

 3.3、指向常量的指针常量(Constant Pointer to Constant)

        指针和指向的内容都不可修改。其声明语法为:

const 数据类型 *const 指针名 = 初始地址;

/* 指向常量的指针常量 */
const int a = 10;
const int* const p = &a;			/* 指针和指向的内容均不可变 */

// *p = 20;							/* 错误:不可修改值 */
int b = 30;
// p = &b;							/* 错误:不可修改指向 */

         特点:

  • 指针的指向和指向的值均不可变。
  • 适用于完全只读的场景(如全局配置数据)。 
const float* const PI = &3.1415926;

4、指针与数组

4.1、数组名的指针特性

        数组名就是数组首元素的地址:

	/* 数组名的指针特性 */
	int arr[5] = { 10, 20, 30, 40, 50 };
	int* p = arr;				/* p指向arr[0] */
	printf("*p = %d\n", *p);	/* *p = 10 */
	printf("arr = %p\n", arr);	/* arr = 00000016BAEFF7D8 */
	printf("p = %p\n", p);		/* p = 00000016BAEFF7D8 */

        也就是说,“int *p = arr;”和“int *p = &arr[0];”是等价的。 

4.2、通过指针遍历数组

         如果指针变量p指向数组中的某个特定元素,可根据指针运算的定义,p+1 将转向下一个元素,p+i 将指向数组元素之后的第 i 个元素,而 p-i 将指向所致数组元素之前的第 i 个元素。

/* 通过指针遍历数组 */
int arr[5] = { 10, 20, 30, 40, 50 };
int* p = arr;
for (int i = 0; i < 5; i++) {
	printf("%d ", *(p + i));    /* 10 20 30 40 50 */
}

         上述示例中,指针p指向arr,也就是arr[0],所以解引用*(p+i)得到的是数组元素a[i]的内容。

         还有另一等价的遍历数组元素写法:

int arr[5] = { 10, 20, 30, 40, 50 };
for (int* p = arr; p < arr + 5; p++) {
    printf("%d ", *p);		/* 输出10 20 30 40 50 */
}

        上述示例中使用了指向数组最后一个元素的下一个元素的地址 arr+5,这是被允许使用的。 

5、指针与函数

5.1、指针作为函数参数(传址调用)

       由于C语言中是以传值的方式将参数值传递给被调用函数。也就是说,被调用函数不能直接修改主调函数中变量的值。 但是可以通过指针修改函数外部的变量:

void swap(int* a, int* b)
{
	int temp = *a;
	*a = *b;
	*b = temp;
}

int main()
{
    int x = 10, y = 20;
    swap(&x, &y);		
    printf("x = %d, y = %d\n", x, y);	/* x=20, y=10 */

    return 0;
}

         上述示例是将swap()函数的所有参数声明为指针,并且通过指针来间接地访问它们所指向的操作数,指针参数使得被调用函数能够访问和修改主调函数中对象的值。

        如果上述示例中的swap()函数的形式为swap(int a, int b),这样是无法达到目的的,此形式的swap()函数仅仅交换了a和b的副本的值。

void swap(int a, int b)
{
	int temp = a;
	a = b;
	b = temp;
}

int main()
{
    int x = 10, y = 20;
    swap(x, y);
    printf("x = %d, y = %d\n", x, y);	/* x = 10, y = 20 */

    return 0;
}

5.2、返回指针的函数(指针函数)

        指针作为函数的返回值,是指函数的返回类型是一个指针类型,即函数返回某个数据的内存地址。这种函数通常用于动态分配内存或返回数据结构的地址。函数可以返回指针,但需要确保指向的内存有效:

int* findMin(int* arr, int size)
{
	int* min = arr;
	for (int i = 0; i < size; i++) {
		if (arr[i] < *min)
			min = &arr[i];
	}
	return min;				/* 返回最小值的地址 */
}

int main()
{
    int arr[5] = { 3, 8, 6, 2, 4 };
    int* p = findMin(arr, 5);
    printf("p = %p\n", p);					/* p = 0000009D194FF724 */
    printf("&arr[3] = %p\n", &arr[3]);		/* &arr[3] = 0000009D194FF724 */

    return 0;
}

  注意事项:

  • 内存管理:如果返回动态分配的指针,一定要注意,调用者需要负责释放内存(如free)。
  • 局部变量:不要返回局部变量的地址,因为函数结束后内存失效。
  • 野指针:一定要确保返回的指针是指向的有效内存。

6、动态内存管理

6.1、malloc和free

        动态分配和释放内存:

/* 动态分配和释放内存 */
int* p = (int*)malloc(5 * sizeof(int));		/* 分配5个int的空间 */
if (p != NULL) {
	for (int i = 0; i < 5; i++) {
		p[i] = i + 1;
		printf("p[%d] = %d\n", i, p[i]);	/* 1 2 3 4 5 */
	}
	free(p);								/* 释放内存 */
}

         malloc并不是从一个在编译是就确定的固定大小的而数组中分配存储空间,而是在需要时向操作系统申请空间。所以,malloc管理的空间不一定时连续的。这样,空闲存储空间以空闲块链表的方式组织,每个块包含一个长度、一个指向下一块的指针以及指向自身存储空间的指针。当有申请请求时,malloc将扫描空闲块链表,直到找到一个足够大的块为止。如果块太大,则将它分成两部分:大小合适的块返回给用户,剩下的部分留在空闲块链表中。如果找不到一个足够大的块,则向操作系统申请一个大块并加入到空闲块链表中。

        释放过程也是首先搜索空闲块链表,以找到可以插入被释放块的合适位置。如果与被释放块相邻的任一边是一个空闲块,则将这两个块合成一个更大的块,这样存储空间就不会有太多的碎片。

6.2、避免内存泄漏

        确保每次malloc都有对应的free:

	/* 确保每次malloc都有对应的free */
	int* p = malloc(sizeof(int));
	int a = 10;
	p = &a;
	free(p);		/* 释放后,p变为野指针 */
	p = NULL;		/* 推荐置空 */

7、高级指针操作

7.1、指针数组

        由于指针也是变量,所以它们也可以像其他变量一样存储在一个数组中。下面示例中arr是一个存储指针的数组:

/* 指针数组 */
int a = 1, b = 2, c = 3;
int* arr[3] = { &a, &b, &c };    /* arr是一个指针数组,存储3个int指针 */

         内存布局:指针数组的每个元素独立指向内存中的某个地址。

arr[0] --> &a

arr[1] --> &b

arr[2] --> &c

        典型应用——存储多个字符串(字符串数组):

char* names[] = { "Alice", "Bob", "Klein" };
printf("names[0] = %s\n", names[0]);	/* names[0]指向"Alice" */
printf("names[1] = %s\n", names[1]);	/* names[1]指向"Bob" */
printf("names[2] = %s\n", names[2]);	/* names[2]指向"Klein" */

7.2、数组指针

        数组指针是一个指针 ,指向一个完整的数组。它保存的是整个数组的起始地址。下面示例中的 p 是一个数组指针。

int nums = { 10, 20, 30 };
int (*p)[3] = &nums;        /* p是一个数组指针,指向包含3个int的数组 */

        内存布局:数组指针指向整个数组的起始地址:

p --> nums[0](整个数组的首地址)

        典型应用1、操作二维数组。

/* 数组指针的应用 -- 操作二维数组 */
int matrix[3][4] = { {1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12} };
int (*p)[4] = matrix;			/* p指向二维数组的第一行(即matrix[0]) */
printf("%d\n", (*p)[2]);		/* 输出3(matrix[0][2]) */
printf("%d\n", (*(p+1))[3]);	/* 输出8(matrix[1][3] */
printf("%d\n", (*p)[10]);		/* 输出11(matrix[2][2],即二维数组第11个元素的值 */

        典型应用2、传递二维数组到函数。

/* 数组指针的应用 -- 传递二维数组到函数 */
void printMatrix(int (*mat)[4], int rows)
{
	for (int i = 0; i < rows; i++) {
		for (int j = 0; j < 4; j++) {
			printf("%d ", mat[i][j]);
		}
		printf("\n");
	}
}

int main()
{
    int matrix[3][4] = { {1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12} };
    printMatrix(matrix, 3);

    return 0;
}

        指针数组和数组指针的关键区别:

特性指针数组数组指针
本质数组,元素是指针指针,指向一个数组
声明语法int *arr[5];int (*p)[5];
内存占用多个指针的空间(如5个指针)单个指针的空间
用途存储多个独立指针(如字符串数组)操作整个数组(如二维数组的行指针)
访问方式arr[i]访问第 i 个指针(*p)[i]访问数组的第 i+1 个元素

         运算符优先级:

  • int *arr[5]:[ ]优先级高于*,因此是指针数组。
  • int (*p)[5]:()强制*优先结合,因此是数组指针。

         常见错误:

  • 错误赋值:
int nums[3] = { 1, 2, 3 };
int (*p)[3] = nums;        /* 错误,nums是首元素地址,类型为int* */
int (*p)[3] = &nums;       /* 正确,&nums是整个数组的地址 */ 
  • 越界访问: 
int nums[3] = { 1, 2, 3 };
int (*p)[3] = &nums;
printf("%d\n", (*p)[3]);    /* 越界访问(数组索引为0~2) */

7.3、多级指针(指针的指针)

         在C语言中,指针的指针(即二级指针)是一种指向指针的指针变量,常用于处理多级间接访问和动态内存管理。

/* 多级指针(指针的指针) */
int a = 10;
int* p = &a;
int** pp = &p;							/* pp指向指针p */
printf("p的地址:%p\n", (void*)pp);		/* 输出p的地址 */
printf("a的地址:%p\n", (void*)*pp);		/* 输出a的地址 */
printf("a = **pp = %d", **pp);			/* 输出10 */

         指针的指针存储的是另一个指针的地址,声明时使用两个星号(**)。示例中,pp 是 int 型指针的指针,指向 p。在解引用的时候,*pp 获取的是 p 的值(即a的地址),而 **pp 是获取 a 的值。

        指针的指针的应用场景: 

  • 动态修改指针的值

         若需要在函数中修改指针指向的地址,需要传递指针的指针:

void allocate(int** ptr)
{
	*ptr = malloc(sizeof(int));		/* 修改外部指针指向新内存 */
	**ptr = 100;					/* 设置值 */
}

int main()
{
    int* p = NULL;
    allocate(&p);		/* 传递指针的地址 */
    printf("%d\n", *p);	/* 输出100 */
    free(p);

    return 0;
}
  • 字符串数组(命令行参数) 

        main函数的参数char **argv是典型的指针的指针用法:

int main(int argc, char **argv)
{
    for (int i = 0; i < argc; i++) {
        printf("参数 %d:%s\n", i, argv[i]);     /* argv[i]是char*类型 */
    }

    return 0;
}

        常见错误:

  • 野指针:在动态申请后,释放内存后未置空指针的指针。
free(*pp);
*pp = NULL;    /* 避免野指针 */
  •  错误的解引用层级:
int **pp = NULL;
*pp = 10;        /* 错误:*pp是int*类型,不能直接赋值int */
**pp = 10;       /* 正确:需先确保*pp指向有效内存 */ 
  •  类型不匹配:
int a = 10;
int *p = &a;
char **pp = &p;    /* 错误:类型不兼容 */

7.4、函数指针

        在C语言中,函数本身不是变量,但可以定义指向函数的指针。函数指针是指向函数的指针变量,存储函数的入口地址,允许通过指针调用函数。这种类型的指针可以被赋值、存放在数组中、传递给函数以及作为函数的返回值等等。如下是一个指向函数的指针的示例。

int add(int a, int b)
{
	return a + b;
}
int (*funcPtr)(int, int) = add;	    /* 声明函数指针 */

int main()
{
    printf("%d\n", funcPtr(2, 3));	/* 输出5 */

	return 0;
}

        注意:函数指针的返回类型和参数列表必须与目标函数一致,并且函数指针需要指向实际存在的函数。

        实现一个回调函数:

int add(int a, int b)
{
	return a + b;
}

void process(int a, int b, int (*callback)(int, int))
{
	printf("结果:%d\n", callback(a, b));
}

int main()
{
    /* 回调函数 */
    process(5, 3, add);		/* 输出:8 */

    return 0;
}


http://www.kler.cn/a/572576.html

相关文章:

  • 类型转换
  • 【音视频】ffmpeg命令分类查询
  • SpringBoot 核心原理深度解析
  • 备考六级:词汇量积累(day3)
  • 并发编程(线程池)面试题及原理
  • 【Linux】之【Bug】VMware 虚拟机开机 一直卡在黑屏左上角下划线闪烁界面
  • 优选算法合集————双指针(专题三)
  • 前端基础之收集表单数据
  • 取消请求:axios.
  • 数据结构篇—队列(queue)
  • Windows 11 + Ubuntu 22.04双系统时间同步失败处理
  • 为AI聊天工具添加一个知识系统 之135 详细设计之76 通用编程语言 之6
  • A*算法路径规划_MATLAB实现
  • 十一、Redis Sentinel(哨兵)—— 高可用架构与配置指南
  • 【多模态目标检测】M2FNet:基于可见光与热红外图像的多模态融合目标检测网络
  • 【Linux】自定协议和序列化与反序列化
  • 新版 FMEA 七步法 - PFMEA 第2步“结构分析”的关键要点 | FMEA软件
  • 快速排序:深入解析算法核心与性能密码
  • LIUNX学习-线程
  • DeepSeek 各版本的区别