C语言中级_动态内存分配、指针和常量、各种指针类型、指针和数组、函数指针
0、前言:
- 动态内存分配是一个重要概念,要和静态数组对比着学习;
- 指针和数组搭配在一起,让指针理解的难度上了一个台阶,尤其是二维数组搭配指针,要获取数组的值,什么时候“取地址”,什么时候“解引用”都需要深刻理解一些概念才能正确使用指针和数组。
- 写这些东西,就是把自己学习的笔记记录下来,供自己日后翻找,若是与此同时能给别人提供些帮助,那就更好了。
1、动态内存分配:
- ★前提知识:内存分为栈和堆,
- “栈”是由编译器自动分配和释放,无需程序员手动操作。当函数执行结束时,其内部的局部变量会被自动弹出栈并释放内存。主要存储局部变量、函数参数、返回地址等。静态分配,编译时就能确定所需内存大小。注意如果一个数组定义在自定义函数当中,那它就位于栈当中,函数周期结束,该数组自动释放。
- “堆”需要程序员手动分配(如 C 语言的malloc)和释放(如free),否则可能导致内存泄漏。主要存储动态分配的对象、数组、大型数据结构等。动态分配,运行时才能确定所需内存大。
2、指针和常量:
- 常量指针:指针指向的值不可以通过指针改变;也就是说*p = num 这条语句失效。
- 指针常量:指针的指向不能变,也就是说 p = &num 这条语句失效。
// 常量指针
int a = 99;
int const* p1 = &a;
//*p1 = 100; // 会报错// 指针常量
int b = 99;
int* const p2 = &b;
*p2 = 100;
//p2 = &a; // 会报错
3、各种指针类型:一些指针在江湖上的诨名
- 万能指针:void* p,这种万能指针,可以强转为其他任何类型的指针:
int *p2 = (int*)vp; // 显式转换:void* → int*
- 悬空指针:指针曾经指向有效的内存,但该内存已被释放或失效。
int *p = (int*)malloc(sizeof(int));
*p = 42;
free(p); // 内存被释放
// 避免悬空指针:p = NULL;
printf("%d", *p); // 悬空指针!p 指向的内存已无效
- 空指针:指向为NULL的指针,int *p = NULL; // 空指针
- 野指针:未初始化的指针,指向地址是随机的,int *p; // 野指针!未初始化
3、指针和数组:
- ★首先搞明白什么是“指针数组”什么是“数组指针”这个很重要
- 指针数组:本质是数组,数组的每个元素是一个指针。
- 数组指针:指向一整个数组的指针。
- 具体的代码实例:
// ---------指针数组:
int a = 10, b = 20, c = 30;
int *arr[3]; // 指针数组:3个元素的数组,每个元素是 int* 类型
arr[0] = &a;
arr[1] = &b;
arr[2] = &c;printf("%d", *arr[1]); // 输出 20(通过指针访问 b 的值)
const char *names[] = {"Alice", "Bob", "Charlie"}; // 3个字符串的地址
printf("%s", names[0]); // 输出 "Alice"
printf("%c\n", (names[0])[2]); // i
/*
names 是一个数组([] 表示数组)。
数组的每个元素是 const char*(指向常量字符的指针)。
因此,names 是一个 指针数组(数组的元素是指针),且这些指针指向 const char(常量指针)
*/// ---------数组指针
int arr[5] = {1, 2, 3, 4, 5};
int (*p)[5]; // 数组指针:指向一个包含 5 个 int 的数组
p = &arr; // p 指向整个数组 arrprintf("%d", (*p)[2]); // 输出 3(解引用 p 得到数组 arr,再访问下标 2)
- 通过二维数组的例子深度理解下数组和指针之间的关系:
// 获取二维数组中第一个一维数组首地址
int arr[3][3] = {{1,2,3},{4,5,6},{7,8,9}
};
printf("*arr = %p\tarr[0]=%p\t&arr[0][0]=%p\tarr+0=%p\n", *arr, arr[0], &arr[0][0], arr + 0);
// 获取二维数组中第二个一维数组首地址
printf("tarr[1]=%p\t&arr[1][0]=%p\tarr+1=%p\n", arr[1], &arr[1][0], arr + 1);
// 获取二维数组中第三个一维数组首地址
printf("tarr[2]=%p\t&arr[2][0]=%p\tarr+2=%p\n", arr[2], &arr[2][0], arr + 2);
/*
理解 arr + 0/1/2作为地址的方式很简单,对于arr数组而言,里面的一维数组就是它的元素,arr
就是这个以一维数组作为元素的数组的首地址,因此,arr每加一个单位,就是移动一个元素的位置。
*/
- 检验一下,在下面的代码中,请说出常量指针是谁?指针数组又是谁?
const char *names[] = {"Alice", "Bob", "Charlie"};
// 在这个代码中,指针数组是names,其中存放的都是常量指针
4、函数指针:
- 类比数组指针记忆,数组指针是指向整个数组的指针,函数指针就是指向整个函数的指针。
- 函数指针的定义
#include<stdio.h>int add(int a, int b) {return a + b;
}
int sub(int a, int b) {return a - b;
}
int main() {// 定义函数指针int (*Add)(int, int);Add = add;int (*Sub)(int, int);Sub = sub;// 借助指针调用printf("%d\n", Add(1, 2)); // 3printf("%d\n", Sub(3, 1)); // 2typedef int(*Func)(int, int); // 使用了这个重命名之后,就相当于用AddFunc代替了 int 函数名 ( int 参数1名, int 参数2名) 这种类型的指针名Func p1 = add;Func p2 = sub;printf("%d\n", p1(2, 1)); // 3printf("%d", p2(2, 1)); // 1return 0;
}
- 在给函数指针类型用typedef 起别名的时候,发现对typedef起别名时,简单的类型还好写,这种复杂类型写起来就比较吃力了。因此总结如下:
1、给基本数据类型创建别名:typedef int Integer; // 为int起别名Integer
2、为指针类型创建别名:typedef int* IntPtr; // 为int*起别名IntPtr
3、为数组类型创建别名:typedef int IntArray5[5]; // 为数组类型创建别名(表示"包含5个int元素的数组")。例如:IntArray5 arr = {1, 2, 3, 4, 5}; // 等价于 int arr[5] = {1,2,3,4,5};
4、 ★为函数指针创建别名(最常用场景之一):typedef int (*CalcFunc)(int, int); // 定义一个函数指针类型(接收两个int,返回int)
CalcFunc func1 = add; // 假设add是已经定义好的函数,func1是CalcFunc类型的函数指针;
上述函数指针其实就相当于:int (*func1)(int, int) = add;
- 总结:在给数组或者函数指针起别名的时候,方法就和定义数组或者定义函数时写法一样,这两种相对其他数据类型起别名都比较特殊一点。
- 函数指针的用途之一:回调函数
- 回调函数就是往函数当中通过函数指针作为形参传递函数
- 我在学习回调函数的时候产生过这样的疑问,为什么明明可以在一个函数当中就调用另一个函数,还非得用回调函数?经过学习我想通了,函数调用函数固然可以,但每次都是调用固定函数,而采用回调函数,就可以动态选择传入函数当中的函数。
#include<stdio.h>
// 调用函数的函数:作用是判断数组当中有几个1
int oneNum(int(*arr), int len, int (*Fun)(int)) {int i, count = 0;for (i = 0; i < len; i++) {if (Fun(arr[i])) {count += 1;}}return count;
}// 被调用的函数:作为条件判断当前值是否为1
int oneNo(int a)
{if (a == 1) {return 1;}else {return 0;}
}
// c语言标准库中快速排序qsort的回调函数
int cmp(void const * a, const void* b) {//return *(int const*)a - *(int const*)b; // 升序排列return *(int const*)b - *(int const*)a; // 降序排列
}int main() {int a[5] = { 1,5,4,2,3 };printf("%d\n", oneNum(a, 5, oneNo)); // 3qsort(a, 5,sizeof(int) , cmp);int i;for (i = 0; i < 5; i++) {printf("%d ", a[i]);}return 0;
}
总结:
- ★指针往细节学习,就会发现每个指针的大小都是固定的,一般电脑如果是64位的,指针大小就是64位(8个字节),如果电脑是32位的,指针大小就是32位(4个字节),这是因为指针存放的地址。指针前面的类型表示的是这个指针指向的空间当中存放的是什么类型,声明指针类型,就是让程序明白这一点,顺着指针地址过去取值的时候取多大的空间,也就清清楚楚的告诉程序了。
- 数组指针&指针数组,其本质就是哪个词在后面它的本质就是什么。
- 函数指针是个挺好用的东西,有了函数指针,我们就可以使用回调函数,向函数当中传递函数了。