C指针
I. 目录
1. 指针概述
1.1 基本概念
指针变量的内容是某变量(数组)的地址(首地址)。
对*p进行自增(自减)操作时,其按照声明的对应变量类型的大小进行自增(自减)
& - 取地址, * - 解引用
指针是可以多重嵌套的,由此引申出多重指针,如下示例
1
2
3
4
5
6
7
8
9int a = 5;
//类似于积分
int* p = &a;
int** p0 = &p;
int*** p1 = &p0;
//类似于求导
*p1 == &p;
*(*p1) == &a;
*(*(*p1)) == 5;程序内存的分布主要为四大块,堆、栈、Static/Global、Code
Code - 代码的存储空间
Static/Global - 全局变量的存储空间
Stack - 局部变量、程序运行时的函数地址、变量参数等的存储空间
Heap -
1.2 注意事项
声明的指针类型要与所保存地址的变量类型一致,否则编译器会报错,需要强制类型转换。
强制类型转换会导致数据的截断。
int* p = &a;
这个方式的*并不是解引用,而是声明指针变量并初始化。指针的类型除了对应变量的类型之外,还有void
1.3 使用场景
1.3.1 无返回值,处理main内局部函数
通过设置函数传参为指针变量,那么就可以通过解引用的方式对main函数内的局部变量在另一个函数内处理,而不需要使用return。
优点:
- 不需要创建全局变量,节省内存
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18/*
* 功能:通过解引用的方式修改main函数内局部变量的值
*/
void incA(int *p)
{
*p = (*p)++;
}
int main()
{
int a = 5;
int* p = &a;
incA(p);
printf("After incA the value of a is: %d \r\n");
}
2.数组和指针
2.1 一般数组和指针
数组A[i]其中直接A保存的是数组的首地址,因此可以对其自增,自增的结果即是按变量类型进行地址的偏移
&A[i] == A+i
数组元素的地址可以通过指针偏移得到A[i] == *(A+i)
数组的值也可以通过指针偏移解引用得到注意不要用A++实现数组偏移,因为声明了数组A之后,其变量名为const类型,不允许自增,可以A[i++],此外还可以用*p = A,再 p++,这样是合法的。
数组传参到另一个函数的时候,在堆栈上的体现并不是像变量一样拷贝了一份,而是仅拷贝了数组的首地址指针。
也就是说传数组其实是传引用(指针)
那么函数参数可以写成int* A
而不是int A[]
2.2 字符数组和指针
- 基本认识 - 字符串数组末端要有
\0
,现代某些编译器会自动加,如果不会就需要自己处理
深刻理解数组不能自增寻址,而用指针进行间接自增寻址
例如
char A[10]
不能直接A++
得到下一个元素,而可以通过char *C = A
->C++
的方式得到下一个元素
- 注意:
char A[20] = "Hello"
虽然和char *A = "Hello"
最后在Printf函数中打印出来的结果是一致的,但是后者被分配到了常量区,而不是变量区,不可以进行读写 ,也就说明数组是const类型的指针。
2.3 指针和多维数组
1.由上面的数组和指针我们可以知道,数组是const类型的指针集合。
2.通过下面这个例子来了解多维数组和指针和解应用之间的嵌套关系
1
2
3
4
5
6
7
8
9
10int B[2][3] = {{2,3,6},{4,5,8}};
int (*p)[3] = B;
// 此时p就是指向B[0]的地址的指针
// B会返回一个一维数组
B == (*p)[3]
*B == B[0] == &B[0][0]
// 二维数组B的首地址就是B,也是B[0]所存的内容是个地址,这个地址就是B[0][0]在内存中的位置。
// 所以对B进行解引用得到的元素是B[0][0]的地址
// 得到B[0][0]的数值
*(*B) == B[0][0]
3. 指针和动态内存
3.1 栈
3.1.1 栈基本内容
- 1.栈 位于 Code区 和 Static/Global区 之上。
- 2.程序所有的局部变量和函数调用 都在 栈内完成。
- 3.出栈入栈方式是:先进后出
- 4.每一个函数入栈后都会分配一个区域,在这个区域内完成其局部变量的数据处理,术语称为:栈帧
- 5.基本的,main函数分配在栈底,当main调用另一个函数fn()时,会进入中断,此时main函数暂停运行,栈内会分配新的栈帧用于fn(),若fn()还有fn1()…等,那么也会继续向上分配栈帧,并进入中断,当所有的栈帧执行完之后,就会返回到父函数继续执行,执行结束的子函数所拥有的栈帧将随着中断的退出而销毁释放。
- 重点:数据结构的栈实现之后,就是此处讨论的栈,其特性就是先进后出。
3.1.2 栈实际问题
- 1.程序运行时栈空间就已经被分配成了固定大小
- 2.若程序中有递归函数或频繁调用函数的时候,会有栈溢出的风险,进而导致程序崩溃
- 3.变量在栈中创建其地址每一次都是随机的,不受控制,因此当程序涉及到不定长数据存储的问题时候,用栈变量就可能会导致程序崩溃,此时就引出了堆(Heap)
3.2 堆
3.2.1 堆基本内容
- 相反与栈,堆空间可自由被分配并且保存,不会被自动回收或销毁,需要自己手动通过free()函数进行销毁,否则会一直存在堆上被占用
- 堆也被称为:内存的空闲池 或 内存空闲存储区 或 动态存储区
- 数据结构中也有堆的概念,但是与此处不一致
- 数据结构中的堆是:
- 此处堆是空闲的内存区域(堆)
- C语言中操作栈所涉及的函数:
- malloc()
- calloc()
- realloc()
- free()
- C++中操作栈所涉及的操作符
- new
- delete
- 堆如果无法通过malloc()得到一块内存,将会返回NULL
3.2.2 堆的实际应用
- 申请对应变量类型的堆空间
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17//申请一个在堆上的变量,记得用free释放
int *p;
p = (int*)malloc(sizeof(int));
...
free(p);
//申请一个在堆上的数组
int *p;
p = (int*)malloc(20*sizeof(int));
...
free(p);
//申请其他类型的堆变量
void *p;
p = (char*)malloc(sizeof(char));
...
free(p);
3.3 动态分配函数
- malloc - 函数原型
void* malloc(size_t size)
- 为什么
int *p = (int*)malloc(x*sizeof(int))
中前面要加int*,这个问题涉及到malloc函数返回值是void型指针,所以要用到类型转换,才能够对指针解引用。
- calloc - 函数原型
void* calloc(size_t num, size_t size)
- calloc函数接收两个参数,一个是要分配的对应变量类型的数量,根据数量划分堆空间,并且初始化为0,malloc不会初始化对应变量。
- realloc - 函数原型
void* realloc(void* ptr, size_t size)
- 此函数接收两个参数:已分配的指针p,以及新的内存块大小,用于重新分配内存块的大小,分配方式取决于原内存块相邻地址空间是否满足新内存块的大小,满足则拓展,不满足即拷贝到新区域。
3.3.1 应用
- 在不适用C99的环境下需要用到不定长数组 malloc/calloc
1
2
3int n;
scanf("%d", &n);
int *A = (int*)malloc(n*sizeof(int)); - calloc可以做到类似于free的效果,但是取决于编译器
3.4 内存泄漏
- 注意malloc或者calloc分配的内存空间,要用free释放,否则在栈上调用函数时创建的堆变量会留着,每一次调用函数都将在堆上新建变量,随着时间的推移,堆空间会满,最后溢出,导致程序的崩溃。
4. 函数与指针
4.1 函数返回指针
- 由栈顶向栈底传参数一般是不允许的,因为会导致数据被覆盖
- 当main函数调用一个fn()并且这个fn会返回一个指针变量回到main,通常上来说main就能操作这个变量,并且这样用指针进行运算的方式会节省栈空间。但会出现一个重大的问题,如果这个fn()在操作语句中间夹入了其它的fn1(),那么此时fn1()会被压入栈中,上一个指针操作的内容从栈顶向下返回,如果没有及时在main内操作,就会被fn1()重新覆盖栈空间,最终解引用fn()返回的指针就会得到不正确的数据。
- 如何解决第一个被覆盖的问题呢?
- 此时就应用到malloc分配内存函数,将在fn()中需要保存的变量从堆中申请,就可以通过free来控制内存何时被释放,也不会将此内容压入栈区,避免被其它函数入栈时导致此变量被覆盖。
1 |
|
4.2 函数指针
- 既然指针能保存地址,而函数入栈的时候也会有对应的地址,那么能否利用指针这种数据类型来保存函数的地址呢?并且通过解引用的方式,实现函数的调用。
4.2.1 定义方式
- 设有一个函数:
int Add(int a, int b)
- 指针函数应为:
int (*p)(int, int)
参数数量以及类型需要一一对应,返回值类型也要一一对应
4.2.2 使用案例
1 | void PrintfHello(char *name) |
4.3 回调函数
- 回调函数指的是一个函数fn(),被另一个函数以参数的形式传入fn1(fn),并且在fn1中调用了,那么称fn()为回调函数
4.3.1 使用场景
- 使得代码更有逻辑性和通用性,以下用qsort()函数来示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24int compare(const void* a, const void *b)
{
int A = *((int*)a);
int B = *((int*)b);
return A-B;
//如果compare返回的值小于零,那么元素会被排在左边
//反之排在右边
//等于零为不确定
//因此我只需要通过这个函数判断两个值对比的时候谁放在左边或者右边就可以排序
//而排序的函数可以用qsort,也可以用自己写的冒泡排序。
//这就是回调函数的一种用法,低耦合,高内聚。
}
int main()
{
int i, A[] = {-23, 8, -1, 5, -11, 9};
qsort(A, 6, sizeof(int), compare);
for(i = 0; i<6; i++) printf("%d", A[i]);
}
5.嵌入式C使用案例
5.1 定义寄存器并修改寄存器的值
附
- 要注意编译器支持C的版本,C99可支持变长数组,数组元素可为变量
- 源代码经过编译器编译后得到可执行文件将存在ROM中,运行时将把二进制指令拷贝到RAM中进行运行。