【C语言】:深入理解指针

指针是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;
}



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值