C语言:第17天笔记
内容提要
- 指针
- 二级指针
- main函数原型
- 常量指针与指针常量
- 野指针、空指针、空悬指针
- void与void*的区别
- 内存管理
指针
二级指针
定义
二级指针(多重指针)用于储存一级指针的地址,需要两次解引用才能访问原始数据。其他多重指针的用法类似,但实际开发中最常见的指针是二级指针。
c
int a = 10; // a是普通变量,也就是原始数据
int *p = &a; // 一级指针,p指向a,解引用1次就可以获取a的值
printf("%d\n", *p); // 10
int **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;
语法
c
数据类型 **指针变量名 = 指针数组的数组名 | 一级指针的地址
特点
① 与指针数组的等效性 二级指针与指针数组在某些时候存在等效性,但与二维数组不等效。二维数组名是数组指针类型,如int (*)[3]
,而非二级指针。
c
// 指针数组
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:列元素
c
#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;
}
② 与二维数组的差异 二维数组名是数组指针类型,直接赋值给二级指针会导致类型不匹配
c
// 数组指针可以指向一个二维数组
int arr[2][3] = {{1,3,5},{11,33,55}};
int (*p)[3] = arr; // 数组名默认代表首元素地址,在二维数组中,就是行地址
// 二级指针不等效二维数组
int **k = arr; // 编译报错 arr类型 int(*)[3] 不兼容 k类型 int**
解引用
① 字符型二级指针 可直接遍历字符串数组,类似一维数组操作:
c
#include <stdio.h>
void fun1()
{
// 定义一个字符类型的指针数组(字符串数组)
char *arr[] = {"orange","apple","grape","banana","kiwi"};
int len = sizeof(arr) / sizeof(arr[0]); // int len = 5 * 8(指针) / 8(指针) = 5
for (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的首元素,也就是orange
for (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;
}
注意:如果需要一个字符串类型的数组,我们可以选择使用二级指针或者指针数组,此时两者完全等价。
② 其他类型的二级指针 需要两次解引用访问数据,常用于操作指针数组
c
#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函数的完整写法:
c
int main(int argc, char *argv[]){..}
int main(int argc, char **argv){..}
扩展写法:
c
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函数是系统通过函数指针的回调调用。
演示
代码:
c
#include <stdio.h>
int main(int argc, char **argv) // {"abc","aaa"} 对行地址解引用,得到首列地址
{
// 访问参数个数 argc
printf("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
修饰的变量,初始化之后不可修改。
c
const int a = 10; // 只读常量
a = 21; // 编译报错
常量指针
-
本质:指向常量数据的指针
-
语法:
cconst 数据类型 *变量名; const 数据类型* 变量名;
-
举例:
cconst int *p; // p是常量指针
-
特性:
- 指向对象的数据不可改变(
int a = 10; const int *p = &a; *p = 20;
,非法) - 指针本身的指向可以改变(
int a = 10, b = 20; const int *p = &a; p = &b;
,合法)
- 指向对象的数据不可改变(
-
案例:
c#include <stdio.h> int main() { int a = 10; // 变量 const int *p = &a; // 常量指针 // *p = 100; // 错误,指针指向的数据不可改变 printf("%d\n", *p);// 10 int b = 20; // 变量 p = &b; // 正确,指针指向可以改变 printf("%d\n", *p);// 20 }
指针常量
-
本质:指针本身是常量,指向固定地址
-
语法:
c数据类型* const 变量名; 数据类型 *const 变量名;
-
特性:
- 指向对象的数据可以改变(
int a = 10; int* const p = &a; *p = 20;
,合法) - 指针本身的指向不可改变(
int a = 10, b = 20; int* const p = &a; p = &b;
,非法)
- 指向对象的数据可以改变(
-
注意:
定义时必须初始化:
cint a = 10; int* const p = &a; // 正确
-
案例:
c#include <stdio.h> int main() { int a = 10; // 变量 int* const p = &a; // 指针常量 *p = 100; // 正确,指针指向的数据可以改变 printf("%d\n", *p);// 100 int b = 20; // 变量 // p = &b; // 错误,指针指向不可改变 printf("%d\n", *p);// 100 }
常量指针常量
-
本质:指针指向和指向对象的数据都不可改变
-
语法:
cconst 数据类型* const 变量名; const 数据类型 *const 变量名;
-
举例:
cconst int* const p; // p是常量指针常量
-
特性:
- 指向对象的数据不可改变(
int a = 10; int* const p = &a; *p = 20;
,非法) - 指针本身的指向不可改变(
int a = 10, b = 20; int* const p = &a; p = &b;
,非法)
- 指向对象的数据不可改变(
-
注意:
定义时需要初始化:
cint a = 10; const int *const p = &a; // 正确
简单理解:不管是常量指针、指针常量还是常量指针常量,本质上都是一个赋值受到限制的指针变量。
总结对比
类型 | 语法 | 指向可变 | 数据可变 |
---|---|---|---|
常量指针 | const int *p |
✔️ | ❌ |
指针常量 | int *const p |
❌ | ✔️ |
常量指针常量 | const int *const p |
❌ | ❌ |
关键点
const
在*
左侧:修饰数据(常量指针)const
在*
右侧:修饰指针(指针常量)- 函数参数优先使用常量指针,提高代码安全性
- 指针常量必须初始化,且不可重新指向
野指针、空指针、空悬指针
野指针
定义:
指向无效内存区域(比如未初始化、已释放或者越界访问)的指针称之为野指针。野指针会导致未定义(UB)行为。
危害:
- 访问野指针可能引发段错误(Segmentation Fault)
- 可能破坏关键内存数据,导致程序崩溃。
产生场景:
-
指针变量未初始化
cint *p; // p未初始化,是野指针 printf("%d\n", *p); // 危险操作:p就是野指针
-
指针指向已释放的内存
cint *p = malloc(sizeof(int)); // 在堆区申请1个int大小的内存空间,将该空间地址赋值给指针变量p free(p); // 释放指针p指向的空间内存 printf("%d\n", *p); // 危险操作:p就是野指针
-
返回局部变量的地址
cint* 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
-
避免返回局部变量的地址
-
使用前检查指针有效性(非空校验,边界检查)。
cint 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
(系统保留,不可访问)

**作用:**明确表示指针当前不指向有效内存,一般用作指针的初始化。
示例:
c
int *p = NULL; // 初始化为空指针
free(p); // 释放后置空
p = NULL;
空悬指针
**定义:**指针指向的内存已经被释放,但未重新赋值。空悬指针是野指针的一种特例。
示例:
c
char *p = malloc(100); // 在堆区分配100个char的空间给p
free(p); // 释放指针p指向的内存空间
printf("%p,%d\n", p, *p); // p可以正常输出,*p此时属于危险操作
// p指向的内存空间被回收,但是p指向空间的地址依然保留,此时这个指针被称作空悬指针
void与void*的区别
定义
-
void: 表示"无类型/空类型",用于函数返回类型或者参数。
cvoid func(void); // 没有返回值也没有参数,一般简写:void func();
-
*void :**通用指针类型(万能指针),可指向任意类型数据,但需要强制类型转换后才能解引用。
cvoid* 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*
需要外部接收的时候明确类型(不明确类型,就无法解引用)
示例
c
#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 之间也有一段禁闭的区段,该区段也是不可访问的。
虚拟内存中各个区段的详细内容:

栈内存
- 什么东西存储在栈内存中?
- 环境变量
- 命令行参数
- 局部变量(包括形参)
- 栈内存有什么特点?
- 空间有限,尤其在嵌入式环境下。因此不可以用来存储尺寸太大的变量。
- 每当一个函数被调用,栈就会向下增长一段,用以存储该函数的局部变量。
- 每当一个函数退出,栈就会向上缩减一段,将该函数的局部变量所占内存归还给系统。
- 注意: 栈内存的分配和释放,都是由系统规定的,我们无法干预。

- 示例代码:
c
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修饰的变量。
- 示例:
c
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()

- 示例:
c
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()只能释放堆内存,并且只能释放整块堆内存,不能释放别的区段的内存或者释放一部分堆内存。
- 释放内存的含义:
- 释放内存意味着将内存的使用权归还给系统。
- 释放内存并不会改变指针的指向。
- 释放内存并不会对内存做任何修改,更不会将内存清零。