C语言:指针
指针
二级指针
定义
二级指针(多重指针)用于储存一级指针的地址,需要两次解引用才能访问原始数据。其他多重指针的用法类似,但实际开发中最常见的指针是二级指针。
int a = 10; // a是普通变量,也就是原始数据int *p = &a; // 一级指针,p指向a,解引用1次就可以获取a的值printf("%d\n", *p); // 10int **w = &p; // 二级指针,w指向p,解引用2次就可以获取a的值printf("%d\n", **w);// 10-----------------------------------------------------------------int ***k = &w; // 三级指针,k指向w,解引用3次就可以获取a的值printf("%d\n", ***k); // 10 int a1 = ***k; int *a2 = **k; int **a3 = *k; int ***a4 = k;
语法
数据类型 **指针变量名 = 指针数组的数组名 | 一级指针的地址
特点
① 与指针数组的等效性 二级指针与指针数组在某些时候存在等效性,但与二维数组不等效。二维数组名是数组指针类型,如int (*)[3]
,而非二级指针。
// 指针数组int arr[] = {11,22,33};int *arr_[] = {&arr[0],&arr[1],&arr[2]};// 二级指针接收指针数组char *str[3] = {"abc","aaa034","12a12"};char **p = str; // p:数组首地址,行地址,默认0行 *p:列地址,默认0行0列 **p:列元素
/*************************************************************************> File Name: demo03.c> Author: 久念昕> Description: > Created Time: 2025年07月29日 星期二 10时27分05秒************************************************************************/#include <stdio.h>int main(int argc,char *argv[]){// 字符串数组,字符类型的指针数组char *str[3] = {"abc","aaa034","12a12"};char **p = str;// str表示数组首地址,其实就是首元素地址 abc 这个字符串// p 存储的就是 abc的地址,指向的行,*p 访问到abc的列,默认首列 // char str[] = "hello";// printf("%s\n", str);// char str2[20];// scanf("%s", str2); // str2就是一个地址// 打印字符串// for (int i = 0; i < 3; i++)// {// printf("%s\n", *p);// p++;// }// 打印字符int i = 0;while(**p != '\0'){printf("%-2c",**p);(*p)++;}printf("\n");return 0;}
② 与二维数组的差异 二维数组名是数组指针类型,直接赋值给二级指针会导致类型不匹配
// 数组指针可以指向一个二维数组int arr[2][3] = {{1,3,5},{11,33,55}};int (*p)[3] = arr; // 数组名默认代表首元素地址,在二维数组中,就是行地址// 二级指针不等效二维数组int **k = arr; // 编译报错 arr类型 int(*)[3] 不兼容 k类型 int**
解引用
① 字符型二级指针 可直接遍历字符串数组,类似一维数组操作:
/*************************************************************************> File Name: demo01.c> Author: 久念昕> Description: > Created Time: 2025年07月30日 星期三 09时44分42秒************************************************************************/#include <stdio.h>void fun1(){// 定义一个字符类型的指针数组(字符串数组)char *arr[] = {"orange","apple","grape","banana","kiwi"};int len = sizeof(arr) / sizeof(arr[0]); // int len = 5 * 8(指针) / 8(指针) = 5for (int i = 0; i < len; i++) printf("%s\n", arr[i]); printf("\n");}void fun2(){char *arr[] = {"orange", "apple", "grape", "banana", "kiwi"};int len = sizeof(arr) / sizeof(arr[0]);// 此时二级指针完全等价于指针数组char **p = arr; // p 指向 arr的首元素,也就是orangefor (int i = 0; i < len; i++){// printf("%s\n", p[i]); // 下标法printf("%s\n", *(p + i));// 指针法}printf("\n");}void fun3(){char *arr[] = {"orange","apple","grape","banana","kiwi"};int len = sizeof(arr) / sizeof(arr[0]);char **p;int i = 0;// 遍历数组do{p = arr + i; // arr代表行,+i,此时是行偏移,返回的行地址 p指向字符串printf("%s\n", *p);// 对行地址解引用得到列地址 int a = 10; int *p = &a;i++;} while (i < len);printf("\n");}int main(int argc,char *argv[]){fun1();fun2();fun3();return 0;}
注意:如果需要一个字符串类型的数组,我们可以选择使用二级指针或者指针数组,此时两者完全等价。
② 其他类型的二级指针 需要两次解引用访问数据,常用于操作指针数组
#include <stdio.h>int main(){// 创建一个一维数组int arr1[] = {11,22,33,44,55,66}; // 11:0x11// 创建一个指针数组int *arr[] = {&arr1[0],&arr1[1],&arr1[2],&arr1[3],&arr1[4],&arr1[5]}; // [0]:0x22 --> 0x11// 用一个二级指针接收指针数组int **p = arr; // p 指向 arr,p存储的arr第一个元素的地址// 遍历数组for(int i=0;i<sizeof(arr)/sizeof(arr[0]);i++){printf("%-6d", *p[i]); // 下标法(1.指针偏移,2.对新指针解引用)printf("%-6d", **(p+i));// 指针法 p+i 元素地址偏移 元素地址,对元素地址解引用,返回元素值(11..对应的地址)}printf("\n");}
总结
类型 | 本质 | 内存布局 | 等效性 |
---|---|---|---|
二级指针(int** ) | 指向指针的指针 | 指针的指针 | 与指针数组等效 |
指针数组(int*[] ) | 元素为指针的数组 | 分散的指针 | 退化为二级指针 |
二维数组(int[][3] ) | 数组的数组 | 连续的数据块 | 数组指针(int(*)[3] ) |
main函数原型
定义
main函数有多种定义格式,main函数也是函数,函数相关的结论对main函数也有效。
main函数的完整写法:
int main(int argc, char *argv[]){..}int main(int argc, char **argv){..}
扩展写法:
main(){} 等价 int main(){} // C11之后不再支持 缺省 返回类型int main(void){} 等价 int main(){}void main(void){} 等价 void main(){}int main(int a){}int main(int a, int b, int c){}...
说明
① argc,argv是形参,他们俩可以修改
② main函数的扩展写法有些编译器不支持,编译报警告
③ argc和argv的常规写法
argc:存储了参数的个数,默认是1个,也就是运行程序的名字
argv:存储了所有参数的字符串形式
④ main函数是系统通过函数指针的回调调用。
演示
代码:
#include <stdio.h>int main(int argc, char **argv) // {"abc","aaa"} 对行地址解引用,得到首列地址{// 访问参数个数 argcprintf("argc=%d\n", argc);// 遍历参数(每一个参数都是一个字符串常量)for(int i=0;i< argc; i++){printf("%s,%s\n", argv[i], *(argv+i));}printf("\n");}
运行结果:
常量指针与指针常量
常量类型
① 字面量:直接使用固定值(如:12,hello,orange, 杨家辉三角),符号常量和枚举在编译器转换为了字面量
② 只读常量:用const
修饰的变量,初始化之后不可修改。
const int a = 10; // 只读常量a = 21; // 编译报错
常量指针
本质:指向常量数据的指针
语法:
const 数据类型 *变量名;const 数据类型* 变量名;
举例:
const int *p; // p是常量指针
特性:
指向对象的数据不可改变(
int a = 10; const int *p = &a; *p = 20;
,非法)指针本身的指向可以改变(
int a = 10, b = 20; const int *p = &a; p = &b;
,合法)
案例:
#include <stdio.h>int main(){int a = 10; // 变量const int *p = &a; // 常量指针// *p = 100; // 错误,指针指向的数据不可改变printf("%d\n", *p);// 10int b = 20; // 变量p = &b; // 正确,指针指向可以改变printf("%d\n", *p);// 20}
指针常量
本质:指针本身是常量,指向固定地址
语法:
数据类型* const 变量名;数据类型 *const 变量名;
特性:
指向对象的数据可以改变(
int a = 10; int* const p = &a; *p = 20;
,合法)指针本身的指向不可改变(
int a = 10, b = 20; int* const p = &a; p = &b;
,非法)
注意:
定义时必须初始化:
int a = 10;int* const p = &a; // 正确
案例:
#include <stdio.h>int main(){int a = 10; // 变量int* const p = &a; // 指针常量*p = 100; // 正确,指针指向的数据可以改变printf("%d\n", *p);// 100int b = 20; // 变量// p = &b; // 错误,指针指向不可改变printf("%d\n", *p);// 100}
常量指针常量
本质:指针指向和指向对象的数据都不可改变
语法:
const 数据类型* const 变量名;const 数据类型 *const 变量名;
举例:
const int* const p; // p是常量指针常量
特性:
指向对象的数据不可改变(
int a = 10; int* const p = &a; *p = 20;
,非法)指针本身的指向不可改变(
int a = 10, b = 20; int* const p = &a; p = &b;
,非法)
注意:
定义时需要初始化:
int a = 10;const int *const p = &a; // 正确
简单理解:不管是常量指针、指针常量还是常量指针常量,本质上都是一个赋值受到限制的指针变量。
总结对比
类型 | 语法 | 指向可变 | 数据可变 |
---|---|---|---|
常量指针 | const int *p | ✔️ | ❌ |
指针常量 | int *const p | ❌ | ✔️ |
常量指针常量 | const int *const p | ❌ | ❌ |
关键点
const
在*
左侧:修饰数据(常量指针)const
在*
右侧:修饰指针(指针常量)函数参数优先使用常量指针,提高代码安全性
指针常量必须初始化,且不可重新指向
野指针、空指针、空悬指针
野指针
定义:
指向无效内存区域(比如未初始化、已释放或者越界访问)的指针称之为野指针。野指针会导致未定义(UB)行为。
危害:
访问野指针可能引发段错误(Segmentation Fault)
可能破坏关键内存数据,导致程序崩溃。
产生场景:
指针变量未初始化
int *p; // p未初始化,是野指针printf("%d\n", *p); // 危险操作:p就是野指针
指针指向已释放的内存
int *p = malloc(sizeof(int)); // 在堆区申请1个int大小的内存空间,将该空间地址赋值给指针变量pfree(p); // 释放指针p指向的空间内存printf("%d\n", *p); // 危险操作:p就是野指针
返回局部变量的地址
int* fun(int a, int b){int sum = a + b; // sum就是一个局部变量return ∑ // 将局部变量的地址返回给主调函数}int main(){int *p = fun(2,3);printf("%d\n", *p); // 危险操作:p就是野指针}
如何避免野指针:
初始化指针为NULL
释放内存后立即置指针为NULL
避免返回局部变量的地址
使用前检查指针有效性(非空校验,边界检查)。
int fun(int *pt){int *p = pt;// 校验指针if(p == NULL) // 结果为假 等价于 if(!p) 其实底层: if(p == 0){printf("错误!");return -1;}printf("%d\n", *p);return 0;}
空指针
定义:值为NULL
的指针,指向地址0x000000000000
(系统保留,不可访问)
作用:明确表示指针当前不指向有效内存,一般用作指针的初始化。
示例:
int *p = NULL; // 初始化为空指针free(p); // 释放后置空p = NULL;
空悬指针
定义:指针指向的内存已经被释放,但未重新赋值。空悬指针是野指针的一种特例。
示例:
char *p = malloc(100); // 在堆区分配100个char的空间给pfree(p); // 释放指针p指向的内存空间printf("%p,%d\n", p, *p); // p可以正常输出,*p此时属于危险操作 // p指向的内存空间被回收,但是p指向空间的地址依然保留,此时这个指针被称作空悬指针
void与void*的区别
定义
void: 表示“无类型/空类型”,用于函数返回类型或者参数。
void func(void); // 没有返回值也没有参数,一般简写:void func();
void*:通用指针类型(万能指针),可指向任意类型数据,但需要强制类型转换后才能解引用。
void* ptr = malloc(4); // ptr指向4个字节大小的堆内存空间 // 存放int类型数据int *p = (int*)ptr;*p = 10;// 存放float类型数据float* p1 = (float*)ptr;*p = 12.5f;// 存放char类型数组char* p2 = (char*)ptr;// 以下写法完全错误float* ptr = malloc(4);int *p = (int*)ptr; // 此时编译报错,类型不兼容 float* int*
注意:只能是具体的类型(
int*,double*,float*,char*...
)和void*
之间转换
注意事项
void
不能直接解引用(*ptr 会报错
)函数返回
void*
需要外部接收的时候明确类型(不明确类型,就无法解引用)
示例
/*************************************************************************> File Name: demo05.c> Author: 久念昕> Description: > Created Time: 2025-07-30 15:52:36************************************************************************/#include <stdio.h>/*** 定义一个返回类型为void*类型的指针函数*/void* proces_data(void* p){return p;}int main(int argc, char *argv[]){// int类型int m = 10;int* p_int = &m;int* result_int = (int*)proces_data(p_int);printf("Integer value:%d\n", *result_int);// double类型double pi = 3.1415926;double* p_double = πdouble* result_double = (double*)proces_data(p_double);printf("Double value:%lf\n", *result_double);// void* p_void = proces_data(p_double);// printf("Void value:%lf\n", *p_void);// *p_void = 20;// 注意:void* 修饰的指针是可以进行赋值操作的,但是不能对其解引用return 0;}
内存管理【扩展资料】
C进程内存布局
任何一个程序,正常运行都需要内存资源,用来存放诸如变量、常量、函数代码等等。这些不同的内容,所存储的内存区域是不同的,且不同的区域有不同的特性。因此我们需要研究C语言进程的内存布局,逐个了解不同内存区域的特性。
每个C语言进程都拥有一片结构相同的虚拟内存,所谓的虚拟内存,就是从实际物理内存映射出来的地址规范范围,最重要的特征是所有的虚拟内存布局都是相同的,极大地方便内核管理不同的进程。例如三个完全不相干的进程p1、p2、p3,它们很显然会占据不同区段的物理内存,但经过系统的变换和映射,它们的虚拟内存的布局是完全一样的。
PM:Physical Memory,物理内存。
VM:Virtual Memory,虚拟内存。
将其中一个C语言含如进程的虚拟内存放大来看,会发现其内部包下区域:
栈(stack)
堆(heap)
数据段
代码段
虚拟内存中,内核区段对于应用程序而言是禁闭的,它们用于存放操作系统的关键性代码,另外由于 Linux 系统的历史性原因,在虚拟内存的最底端 0x00000000 ~ 0x08048000 之间也有一段禁闭的区段,该区段也是不可访问的。
虚拟内存中各个区段的详细内容:
栈内存
什么东西存储在栈内存中?
环境变量
命令行参数
局部变量(包括形参)
栈内存有什么特点?
空间有限,尤其在嵌入式环境下。因此不可以用来存储尺寸太大的变量。
每当一个函数被调用,栈就会向下增长一段,用以存储该函数的局部变量。
每当一个函数退出,栈就会向上缩减一段,将该函数的局部变量所占内存归还给系统。
注意: 栈内存的分配和释放,都是由系统规定的,我们无法干预。
示例代码:
void func(int a, int *p) // 在函数 func 的栈内存中分配{double f1, f2; // 在函数 func 的栈内存中分配... // 退出函数 func 时,系统的栈向上缩减,释放内存}int main(void){int m = 100; // 在函数 main 的栈内存中分配func(m, &m); // 调用func时,系统的栈内存向下增长}
静态数据
C语言中,静态数据有两种:
全局变量:定义在函数外部的变量。
静态局部变量:定义在函数内部,且被static修饰的变量。
示例:
int a; // 全局变量,退出整个程序之前不会释放void f(void){static int b; // 静态局部变量,退出整个程序之前不会释放printf("%d\n", b);b++;}int main(void){f();f(); // 重复调用函数 f(),会使静态局部变量 b 的值不断增大}
为什么需要静态数据?
全局变量在默认的情况下,对所有文件可见,为某些需要在各个不同文件和函数间访问的数据提供操作上的方便。
当我们希望一个函数退出后依然能保留局部变量的值,以便于下次调用时还能用时,静态局部变量可帮助实现这样的功能。
注意1:
若定义时未初始化,则系统会将所有的静态数据自动初始化为0
静态数据初始化语句,只会执行一遍。
静态数据从程序开始运行时便已存在,直到程序退出时才释放。
注意2:
static修饰局部变量:使之由栈内存临时数据,变成了静态数据。
static修饰全局变量:使之由各文件可见的静态数据,变成了本文件可见的静态数据。
static修饰函数:使之由各文件可见的函数,变成了本文件可见的静态函数。
数据段与代码段
数据段细分成如下几个区域:
.bss 段:存放未初始化的静态数据,它们将被系统自动初始化为0
.data段:存放已初始化的静态数据
.rodata段:存放常量数据
代码段细分成如下几个区域:
.text段:存放用户代码
.init段:存放系统初始化代码
int a; // 未初始化的全局变量,放置在.bss 中int b = 100; // 已初始化的全局变量,放置在.data 中int main(void){static int c; // 未初始化的静态局部变量,放置在.bss 中static int d = 200; // 已初始化的静态局部变量,放置在.data 中// 以上代码中的常量100、200防止在.rodata 中}
注意:数据段和代码段内存的分配和释放,都是由系统规定的,我们无法干预。
堆内存
堆内存(heap)又被称为动态内存、自由内存,简称堆。堆是唯一可被开发者自定义的区段,开发者可以根据需要申请内存的大小、决定使用的时间长短等。但又由于这是一块系统“飞地”,所有的细节均由开发者自己把握,系统不对此做任何干预,给予开发者绝对的“自由”,但也正因如此,对开发者的内存管理提出了很高的要求。对堆内存的合理使用,几乎是软件开发中的一个永恒的话题。
堆内存基本特征:
相比栈内存,堆的总大小仅受限于物理内存,在物理内存允许的范围内,系统对堆内存的申请不做限制。
相比栈内存,堆内存从下往上增长。
堆内存是匿名的,只能由指针来访问。
自定义分配的堆内存,除非开发者主动释放,否则永不释放,直到程序退出。
相关API:
申请堆内存:malloc() / calloc() int *p = malloc(4);
清零堆内存:bzero()
释放堆内存:free()
示例:
int *p = malloc(sizeof(int)); // 申请1块大小为 sizeof(int) 的堆内存bzero(p, sizeof(int)); // 将刚申请的堆内存清零*p = 100; // 将整型数据 100 放入堆内存中free(p); // 释放堆内存// 申请3块连续的大小为 sizeof(double) 的堆内存double *k = calloc(3, sizeof(double));k[0] = 0.618;k[1] = 2.718;k[2] = 3.142;free(k); // 释放堆内存
注意:
malloc()申请的堆内存,默认情况下是随机值,一般需要用 bzero() 来清零。
calloc()申请的堆内存,默认情况下是已经清零了的,不需要再清零。
free()只能释放堆内存,并且只能释放整块堆内存,不能释放别的区段的内存或者释放一部分堆内存。
释放内存的含义:
释放内存意味着将内存的使用权归还给系统。
释放内存并不会改变指针的指向。
释放内存并不会对内存做任何修改,更不会将内存清零。