指针是C语言的核心特性,本质是存储内存地址的变量。通过指针,可间接访问和修改数据,在函数传参时避免数据拷贝,提升效率;指针运算能灵活遍历数组。它在动态内存分配和构建链表、树等复杂数据结构中不可或缺。但使用不当易产生野指针、内存泄漏等问题,掌握指针是进阶C语言编程的关键。
一.内存单元的编号 == 地址 == 指针
#include <stdio.h>
int main() {
int i = 0;
int* p = &i;(取出i的地址放到p中去)
return 0;
}
指针变量
类型名 *指针变量名
指针变量是一种变量,用来存放地址,存放在指针变量中的值都会被理解为地址。
这里的 * 是指针声明符,说明被定义的变量是一个指针。
二.操作符(1)
1.&(取地址操作符)
#include <stdio.h>
int main() {
int a = 0;
&a;//取出a的地址
printf("%p", &a);
return 0;
}
&a实际上取出的是a所占4个字节中地址较小的地址。(整形变量占用4个字节)
2.*(解引用操作符)
#include <stdio.h>
int main() {
int a = 100;
int* pa = &a;
*pa = 0;
return 0;
}
作用:通过地址(指针)找到所指的对象。
注意与指针声明符(*)区别开。
三.指针的大小和类型
1.指针大小
32位平台下:整数占4字节,指针占4字节,地址范围2^32,
64位平台下:整数占4字节,指针占8字节,地址范围2^64.
指针大小与类型无关,只要是指针,在相同的操作系统下大小是相同的;并且每个字节都有独立的地址。
2.指针类型
指针类型的作用:决定了对指针解引用时能访问多大权限,即一次能操作几个字节。
如:int*类型解引用能访问4字节,而char*类型解引用能访问1个字节。
void*指针(泛型指针)
作用:可以用来接收任意类型的指针。
局限性:不能直接进行指针运算(指针加减整数,解引用)。
3.二级指针
四.指针的基本运算
1.指针+-整数
int* pi + 1//指针+1跳过4个字节
char* pa + 1//指针+1跳过1个字节
结论:指针+-整数时,指针的类型决定了指针向前或向后的距离有多大。
2.| 指针-指针 | (取绝对值)
得到的是两个指针之间的元素个数,前提是两个指针指向同一空间。
3.赋值运算
int a = 3, *p1, *p2;
p1 = &a;
p2 = p1;
五.const修饰指针,assert断言
1.const 修饰指针
在使用 const
修饰指针变量时,const
的位置不同,其含义也会有所不同。
const在*左边( int const* p )
int const* ptr;
这种情况下,ptr
是一个指向常量整数的指针。指针本身可以被修改,指向不同的地址,但不能通过 ptr
修改所指向的值。
const在*右边( int* const p )
int* const ptr = &someVariable;
这种情况下,ptr
是一个常量指针,指向一个整数。指针本身不能被修改,指向的地址是固定的,但可以通过 ptr
修改所指向的值。
实例:
int main() {
int a = 10;
int b = 20;
// 指向常量整数的指针
const int* ptr1 = &a;
// ptr1 = &b; // 合法,可以修改指针指向的地址
// *ptr1 = 30; // 非法,不能通过 ptr1 修改所指向的值
// 常量指针,指向整数
int* const ptr2 = &a;
// *ptr2 = 30; // 合法,可以通过 ptr2 修改所指向的值
// ptr2 = &b; // 非法,不能修改指针指向的地址
// 指向常量整数的常量指针
const int* const ptr3 = &a;
// ptr3 = &b; // 非法,不能修改指针指向的地址
// *ptr3 = 30; // 非法,不能通过 ptr3 修改所指向的值
return 0;
}
2.assert断言
使用assert断言的基本概念
在C语言中,assert
是一个宏,用于在程序运行时检查某个条件是否为真。如果条件为假,assert
会输出错误信息并终止程序(显示错误的文件名和行号)。assert
通常用于调试阶段,帮助开发者快速定位问题。
在release版本中,assert无法正常使用。
包含头文件和禁用assert
#define NDEBUG
#include <assert.h>
使用assert
之前,需要包含<assert.h>
头文件。
在发布版本中,通常不希望assert
影响程序运行。可以通过定义NDEBUG
宏来禁用assert
。
在定义了NDEBUG
之后,所有的assert
语句都会被忽略,程序会继续执行而不进行断言检查。
简单示例:
以下是一个简单的示例,展示了如何使用assert
来检查一个变量是否为正数。
#include <stdio.h>
#include <assert.h>
int main() {
int number = -5;
// 使用assert检查number是否为正数
assert(number > 0);
printf("Number is positive: %d\n", number);
return 0;
}
运行结果
Assertion failed: number > 0, file example.c, line 7
如果number
为正数,程序会正常执行并输出结果。
如果number
为负数或零,assert
会触发,程序会终止并输出错误信息:
六.野指针
野指针的成因
野指针是指指向无效内存地址的指针。在C语言中,野指针的成因主要包括以下几种情况:
1.指针未初始化:声明指针后未对其进行初始化,指针的值是随机的,可能指向任意内存地址。
int *p; // 未初始化,p是野指针
2.指针释放后未置空:使用free
释放动态分配的内存后,指针仍然指向原来的内存地址,但该地址已经无效。
int *p = (int *)malloc(sizeof(int));
free(p); // p成为野指针
3.指针超出作用域:局部指针变量在函数返回后,其指向的内存地址可能被回收,导致指针变为野指针。
int *func() {
int x = 10;
return &x; // 返回局部变量的地址,func()返回后,指针变为野指针
}
解决野指针的方式
1.初始化指针:声明指针时立即初始化为NULL
,避免指针指向随机内存地址。
int *p = NULL;
2.释放指针后置空:使用free
释放内存后,将指针置为NULL
,避免误用。
int *p = (int *)malloc(sizeof(int));
free(p);
p = NULL; // 释放后置空
3.避免返回局部变量地址:函数中避免返回局部变量的地址,使用动态内存分配或静态变量替代。
4.避免指针越界访问
七.指针和数组
1.数组名的理解
数组名就是首元素的地址。
例外:
sizeof(数组名):在sizeof中单独放数组名,计算的是数组整个空间的大小。
&数组名 : 数组名表示整个数组,取出的是整个数组的地址 。
&arr+1跳过整个数组;arr+1和&arr[0]+1只跳过一个元素。
2.一维数组传参的本质
本质上数组传参,传递的是数组首元素(第一个元素arr[0])的地址。
使得在函数内部无法求数组元素个数
总结:一维数组传参,形参部分可以写成数组形式,也可以写成指针的形式。
arr[i] == *(arr+i) == *(i+arr) == i[arr]
3.二维数组传参的本质( int(*)[5] )
二维数组是以一维数组为元素的数组。
传递二维数组的数组名称就是第一个一维数组的地址。
同样的二维数组传参,形参的部分可以写成数组,也可以写成指针的形式
*(*(p+i)+j) == p[i][j]
4.指针数组
存放指针的数组,指针数组可以用来模拟二维数组
int*parr[3] = {arr1, arr2, arr3};
5.数组指针变量( int(*p)[10] )
存放数组的地址,能够指向数组的指针变量。
对比:int *p1 [10];指向首元素,+1跳过一个元素。
int (*P2)[10];指向数组10个元素,+1跳过整个数组。 p和*结合,说明p是一个指针变量,指向数组;由于[ ]的优先级高于 * 所以加()
初始化:int arr[10] = {0};
int (*p)[10] = &arr;
八.指针与函数
1.函数指针变量( int(*) (int x, int y) )
用来存放函数的地址,未来通过地址能够调用函数
函数名就是函数的地址,也可以使用&函数名
函数指针变量使用举例
#include <stdio.h>
void test() {
printf("hehe\n");
}
int main() {
void(*pf1)() = &test;
void(*pf2)() = test;//可以省略&
return 0;
}
#include<stdio.h>
int Add(int x, int y) {
return x + y;
}
int main() {
int (*pf3)(int, int) = Add;
int (*pf3)(int x , int y ) = &Add;//可以省略选择x,y
return 0;
}
解析:
2.函数指针数组( int(*) ( ) )
把函数的地址存放到一个数组中,
例子:int (*parr1[3])();
应用(转移表):
定义函数指针类型
typedef int (*func_ptr)(int, int);
这里typedef关键字
为了简化函数指针数组的定义,通常先定义一个函数指针类型。假设我们有一组函数,它们都接受两个int
参数并返回一个int
值:
定义功能函数
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 divide(int a, int b) {
if (b != 0) {
return a / b;
}
return 0; // 处理除零情况
}
创建函数指针数组
func_ptr func_array[] = {add, subtract, multiply, divide};
使用之前定义的函数指针类型,创建一个函数指针数组:
使用函数指针数组
#include <stdio.h>
int main() {
int a = 10, b = 5;
for (int i = 0; i < 4; i++) {
printf("Result of operation %d: %d\n", i, func_array[i](a, b));
}
return 0;
}