C语言----指针入门篇

目录

1. 指针是什么?

1.1 什么是内存呢?

1.2 变量与内存的关系

1.3 指针的本质

1.4 指针的基本操作

(1) 声明指针

(2) 取地址(&)和解引用(*)

(3) 指针的算术运算

2. 指针类型

3. 野指针

3.1 野指针产生的原因?

3.2 野指针的解决方法

4. 指针运算

4.1 指针+- 正数

4.2 指针-指针

4.3 指针的关系运算

5. 指针和数组

6. 二级指针

7. 指针数组


1. 指针是什么?

指针理解的两个要点:

1. 指针是内存中一个最小单元的编号  也就是地址

2. 平时口语中说的指针 通常指的是指针变量 是用来存放内存地址的变量

下面我将会具体解释上面两个要点   这时我们就不得不提一提内存了 

1.1 什么是内存呢?

在C语言中,内存是程序运行时用于存储数据和指令的硬件资源,由操作系统管理并由程序动态或静态分配。它是计算机RAM(随机存取存储器)的一部分,程序通过内存地址直接或间接访问数据。

C语言中的内存布局

程序运行时,内存被划分为几个主要区域(由低地址到高地址):

  1. 代码区(Text Segment):存储编译后的机器指令(如函数代码)。
  2. 全局/静态区(Data Segment):存储全局变量和静态变量(如 static int x;)。
  3. 堆区(Heap):动态分配的内存(如 malloc 分配的空间),需手动管理。
  4. 栈区(Stack):存储局部变量、函数参数等,由系统自动分配和释放。
高地址
┌─────────────┐
│   栈区      │ ← 局部变量、函数调用(向下增长)
├─────────────┤
│   堆区      │ ← 动态内存(向上增长)
├─────────────┤
│ 全局/静态区 │ ← 全局变量、static变量
├─────────────┤
│   代码区    │ ← 程序指令
└─────────────┘
低地址

1.2 变量与内存的关系

在C语言中,变量是内存的抽象。例如:

int a = 10; // 在栈上分配4字节(假设int占4字节)
  • a 的值 10 存储在某个内存地址(如 0x7ffd1234)。
  • 通过 &a 可以获取它的地址。
  • 但a是int类型 占4个字节 每一个字节都有地址 那&a取出的是哪个字节的地址呢?
  • &a实际上是取的第一个字节的地址(较小的地址)

1.3 指针的本质

指针是一种变量,专门用于存储另一个变量的内存地址
语法:类型 *指针变量名;
示例:

int a = 10;
int *p = &a; // p 存储 a 的地址
  • p 指向 a 的内存地址(如 0x7ffd1234)。
  • 通过 *p 可以访问或修改 a 的值(解引用)。

我们可以试着打印看看 a的地址和pa的值

如上图 &a对a取地址的值和pa的值是一样的 

总结:

指针变量是用来存放地址的  地址是唯一表示一个内存单元的

指针的大小在32位平台是4个字节 在64位平台是8个字节

那怎么使用指针呢?  下面我将简单解释指针的使用

1.4 指针的基本操作

(1) 声明指针

int *p;      // 指向int类型的指针
char *str;   // 指向char类型的指针
float *fp;   // 指向float类型的指针

指针类型的意义

指针类型决定了 指针进行解引用操作的时候 一次性访问几个字节

// 如果是char*的指针  解引用访问1个字节

// 如果是int*的指针     解引用访问4个字节

// 如果是float*的指针  解引用访问4个字节

(2) 取地址(&)和解引用(*)

1. 取地址运算符 &

  • 作用:获取变量的内存地址。
  • 语法&变量名
  • 返回值:该变量的内存地址(指针类型)。
  • 关键点
    • 只能用于已定义的变量(如 int a;),不能用于常量(如 &10 是错的)。

2. 解引用运算符 *

  • 作用:通过指针访问或修改该指针指向的内存中的值。
  • 语法*指针变量名
  • 关键点
    • 必须对已初始化的指针使用(否则是野指针)。
    • 解引用时,指针的类型决定了如何解释内存中的数据(如 int* 按4字节读取,char* 按1字节读

用法如下:

int a = 10;
int *p = &a;  // p 存储 a 的地址

printf("%d\n", *p); // 输出 10(通过指针访问a的值)
*p = 20;            // 修改 a 的值为 20

运行结果如下:

(3) 指针的算术运算

指针加减整数时,移动的单位是指向类型的大小

#define _CRT_SECURE_NO_WARNINGS 
#include<stdio.h>
int main() {
	int arr[3] = { 1, 2, 3 };
	int* p = arr;       // p 指向 arr[0]

	printf("%d\n", *p);     // 输出 1
	printf("%d\n", *(p + 1)); // 输出 2(p+1 指向 arr[1])
	return 0;
}

运行结果如下:

我们可以试着输出他们的地址 来观察指针+1时 地址的具体变化

#define _CRT_SECURE_NO_WARNINGS 
#include<stdio.h>
int main() {
	int arr[3] = { 1, 2, 3 };
	int* p = arr;       // p 指向 arr[0]
	printf("%p\n", p);     // 输出 1
	printf("%p\n", (p + 1)); // 输出 2(p+1 指向 arr[1])
	return 0;
}

运行结果如下:

可以看出来p和p+1的地址相差了4个字节 这是因为int类型的数据在内存中 占4个字节

下面我们来验证我们的猜想 char类型的是否是1个字节

#define _CRT_SECURE_NO_WARNINGS 
#include<stdio.h>
int main() {
	char arr[3] = {'a','b','c'};
	char* p = arr;       // p 指向 arr[0]
	printf("*p=%c\n", *p);     // 输出 a
	printf("*(p+1)=%c\n", *(p + 1)); // 输出 b(p+1 指向 arr[1])

	printf("p的地址%p\n", p);     // 输出 a的地址
	printf("p+1的地址%p\n", (p + 1)); // 输出 b(p+1 指向 arr[1])的地址
	return 0;
}

运行结果如下:

可以看出 验证成功  char类型的指针+1 会让地址增加一个字节

2. 指针类型

 指针类型的意义:

指针的不同类型 其实提供了不同的视角区观看和访问内存

char*    一次访问1个字节     +1跳过一个字节

int*       一次访问4个字节     +1跳过四个字节 同理也可以+2 +3等等

既 int* p+4跳过了4*sizeof(int)个字节 既16个字节  其他类型同理

总结:指针类型决定了指针向前或向后走一步有多大

指针的类型决定了指针解引用时候 有多大的权限 既操作几个字节

比如 char*的指针只能访问一个字节 而int*可以访问四个字节

3. 野指针

野指针是什么呢?

野指针就是指针指向的位置是不可知的(随机的  不正确的  没有明确限制的)

3.1 野指针产生的原因?

下面介绍第一种野指针

1. 指针未初始化

未赋初值:指针声明后未初始化,可能随机指向无效地址 

下面我给大家举个例子 就知道它是如何产生的

int main()
{
    int* p;
    *p = 20;
    printf("%d", *p);
    return 0;
}

上面的代码粗看一遍感觉没什么问题 但实际上是有问题的 如下

这就是第一种野指针 未初始化的指针 可能引发程序崩溃、数据损坏或安全漏洞。

下面介绍第二种野指针

2. 越界访问

定义:指针访问超出其指向的内存边界(如数组、动态分配的内存块等)。

如下代码

int main()
{
    int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
    int* p = arr;
    for (int i = 0; i <= 10; i++)
    {
        printf("%d", *p);
        p++;
    }
    return 0;
}

错误原因:

细心的同学不难发现 arr数组只有10个元素 但是却循环了11次 既访问了arr[10]

但他并没有被定义  我们可以来看一下运行结果

返回了一个随机数 这就是越界访问  可能会导致缓冲区溢出问题

下面介绍第三种野指针

3. 悬垂指针

定义:指向已经被释放或失效的内存地址的指针  由于该内存可能已被系统回收或重新分配 解引用使用悬垂指针会导致 未定义行为

int* text()
{
    int a;
    return &a;
}
int main()
{
    int* p = text();
    printf("%d", *p);
    return 0;
}

错误原因如下 :

text() 函数返回了局部变量 a 的地址,但 a 在函数返回后会被销毁(栈内存自动回收),此时 p 指向的是一块已被释放的内存,访问 *p 是未定义行为 可能会导致以下行为

程序崩溃:最常见的是段错误(Segmentation Fault),因为访问了非法内存。

输出随机值:若系统未立即回收栈内存,可能读到残留数据(但不可依赖)。

3.2 野指针的解决方法

1. 初始化指针

#define _CRT_SECURE_NO_WARNINGS 
#include<stdio.h>
int main()
{
    int a = 10;
    int* p = &a;//指针的初始化

    int* p = NULL;//NULL-空指针,专门用来初始化指针
    *p = 20;//安全检查
}

优点

  • 如果忘记初始化,解引用 NULL 指针通常会 立即崩溃(比随机内存访问更安全)。
  • 既指针指向空间释放 即时置NULL
  • 便于调试(gdb 或 Valgrind 能检测 NULL 解引用)

2. 指向已有变量

#define _CRT_SECURE_NO_WARNINGS 
#include<stdio.h>
int main()
{
	int x = 10;
	int* p = &x;  // 指向栈变量
	printf("%d", *p);  // 安全
}

优点

  • 指针始终指向有效内存。

3. 使用静态/全局变量

#define _CRT_SECURE_NO_WARNINGS 
#include<stdio.h>
int main()
{
	static int x = 10;
	int* p = &x;  // 指向静态变量
	printf("%d", *p);  // 安全

}

优点

  • 静态变量的生命周期持续到程序结束,指针不会悬空。

4.小心指针越界

5. 避免返回局部变量的地址

6.指针使用前 检查有效性

4. 指针运算

4.1 指针+- 正数

下面我们通过具体的代码来理解指针+-正数的使用

#define _CRT_SECURE_NO_WARNINGS 
#include<stdio.h>
int main()
{
	float* values[5];
	float* vp;
//指针+-正数: 指针的关系运算
	for(vp=&values[0];vp<&values[5];)
		{
		*vp++ = 0;
		}
	for (int i = 0; i < 5; i++)
	{
		printf("%lf   ", values[i]);
	}
}

定义了指针vp 并且把values[0]的地址赋给了vp 通过vp的++ 使得p指向的地址发生改变

并且是后置++ 先使用后改变 每次都往后走一步 指向下一个元素 直到数组元素都被更改为0

通过运行 不难看出 可以讲values数组元素都赋值位0.000000

4.2 指针-指针

下面我们通过具体的代码来理解指针-指针的使用

#define _CRT_SECURE_NO_WARNINGS 
#include<stdio.h>
int main()
{
	int arr[10] = { 0 };
	printf("%d\n", &arr[9] - &arr[0]);
}

上面这段代码运行结果是什么呢?

运行结果是9 为什么呢?

那是因为两个指针相减 得到的是指针直接的个数的绝对值  arr[9]和arr[0]直接相差9个元素 既arr[0~8]共9个元素

前提是 指针_指针 需要指针在同一块空间(指针类型相同)  如下 int类型和char类型指针相减是没有意义的

现在我们通过练习 写一个mystrlen的代码来练习一下

通过指针来实现计算字符串长度

#define _CRT_SECURE_NO_WARNINGS 
#include<stdio.h>
int My_strlen(char* arr)
{
	int count = 0;
	while (*arr != '\0')
	{
		arr++;
		count++;
	}
	return count;
}
int main()
{
	char arr[] = "abcdefg";
	int a = My_strlen(arr);
	printf("%d",a);
	return 0;
}

上面是我们之前学过的代码 现在看起来似乎更加浅显易懂了

我们对此稍加修改就能运用指针相减来实现目的 如下

#define _CRT_SECURE_NO_WARNINGS 
#include<stdio.h>
int My_strlen(char* arr)
{
	char* start = arr;
	while (*arr != '\0')
	{
		arr++;
	}
	return arr-start;
}
int main()
{
	char arr[] = "abcdefg";
	int a = My_strlen(arr);
	printf("%d",a);
	return 0;
}

运行结果也的确没有问题

4.3 指针的关系运算

关系运算其实就一个要点既需要满足标准规定

允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较  但是不允许与指向第一个元素之前的那个内存位置的指针进行比较

如下代码  虽然可以运行 但是并不符合规定 应当避免

#define _CRT_SECURE_NO_WARNINGS 
#include<stdio.h>
int main()
{
	float values[5];
	float* vp;
	//指针+-正数: 指针的关系运算
	for (vp = &values[4]; vp >=&values[0];vp--)
	{
		*vp= 0;
	}
	for (int i = 0; i < 5; i++)
	{
		printf("%lf\n", values[i]);
	}
}

上述代码将values[0]与它之前的地址进行比较 这并不符合规定 即使运算结果正确 也应该避免

5. 指针和数组

指针和数组的有什么关系呢? 回想之前的学习 其实我们也大概能猜出来  下面让我来为大家讲解他们之间的联系

指针和数组

1. 指针和数组是不同的对象

指针是一种变量 是存放地址的  大小是4或8字节的 

而数组是一种相同元素的集合 可以放多个元素 大小是取决于元素个数和元素的类型的

2. 数组的数组名是首元素的地址 地址可以存放在指针变量中 可以通过指针访问变量  

让我们通过指针来为数组赋值来熟悉一下

#define _CRT_SECURE_NO_WARNINGS 
#include<stdio.h>
int main()
{
	int arr[10] = { 0 };
	int* p = arr;
	int i = 0;
	int sz = sizeof(arr) / sizeof(arr[0]);
		for (i = 0; i < sz; i++)
		{
			*(p+i) = i + 1;
		}
		for (i = 0; i < sz; i++)
		{
			printf("%d  ", *p);
			p++;
		}
	return 0;
}

运行结果如下

我们使用了指针的运算来对数组进行了赋值 并且通过指针将数组的元素打印了出来

不仅可以对指针进行=-   其实数组也可以+-

int* p=arr;  证明了arr其实和p是等价的         *(p+i)=arr[i]=*(arr+i)=i[arr]

让我们验证一下

原因是什么呢?

因为 [ ] 是操作符  i和arr是[]这个操作符的操作数而已 相当于a+b=b+a

重点:数组名是数组首元素的地址

6. 二级指针

什么是二级指针呢?

在C语言中,二级指针int **char **等)是指向指针的指针 如下

int main() {
    int a = 10;
    int* p = &a;      // 一级指针,存储 a 的地址
    int** pp = &p;    // 二级指针,存储 p 的地址
    return 0;
}

如何理解**呢  你可以把int*看成一个整体 后面的*pp是一个整体   *pp说明pp是一个指针 而前面的int* 说明pp这个指针存放的是一个int类型的指针的地址

同理

int main() {
    int a = 10;
    int* p = &a;      // 一级指针,存储 a 的地址
    int** pp = &p;    // 二级指针,存储 p 的地址
    int*** ppp = &pp; // 三级指针, 存储 pp 的地址
    return 0;
}

 但是三级及以上基本不怎么使用 了解即可

让我们观察一下他们三者的关系 让我们运行以下代码

int main() {
    int a = 10;
    int* p = &a;      // 一级指针,存储 a 的地址
    int** pp = &p;    // 二级指针,存储 p 的地址
    int*** ppp = &pp; // 三级指针, 存储 pp 的地址
    printf("***ppp = %d\n", ***ppp);
    printf("**pp = %d\n", **pp);
    printf("*p= %d\n",*p);
    printf("a= %d\n", a);

    return 0;
}

运行结果如下图

可以看出

***ppp=**pp=*p=a  

说明通过ppp找到pp 通过pp找到p 通过p找到a

ppp存放pp的地址  pp存放p的地址  p存放a的地址 通过三次解引用ppp就可以找到a的值

可以总结为下图所示的关系

对于二级指针的运算有:

*pp 通过对pp中的地址进行解引用 这样就能找到p  *pp其实就是访问p

int a=10;

*pp=&a;//等价于p=&a

**pp先通过*pp找到p 再对p进行解引用操作:*p 找到a

**pp=30;
//等价于*p=30;
//等价于a=30;

7. 指针数组

首先我们需要明确指针数组是指针还是数组

中国人喜欢把重点放在后面 比如说好基友 是的 类似 指针数组的主语是数组 

比如说字符数组--存放的是字符的数组--char[10]     整型数组--存放的是整型的数组--int[10]

int main() {
    int a = 10;
    int b = 20;
    int c = 30;
    int d = 40;
    int e = 50;
    int* arr[5] = { &a ,&b ,&c ,&d ,&e };
    for (int i = 0; i < 5; i++)
    {
        printf("%d  ", *arr[i]);
    }
    return 0;
}

如图 成功通过指针arr数组访问了a b c d e的值

因此arr数组的确是一个指针数组 但我们平时并不会这样使用


int main() {
    int a[] = { 1,2,3,4,5 };
    int b[] = { 6,7,8,9,10 };
    int c[] = { 11,12,13,14,15 };
    int* arr[5] = { a,b,c };
    for (int j = 0; j < 5; j++)
    {
    for (int i = 0; i < 5; i++)
    {
        printf("%d  ", arr[j][i]);
    }
    printf("%\n");
    }
    return 0;
}

上述代码 我们成功实现了 使用一维数模拟了三维数组来使用 运行结果如下

以上就是指针初阶的全部内容 若能够提供帮助 请点赞支持一下 您的支持就是我最大的动力 谢谢

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值