指针
- 指针是一种数据类型,它指向内存
- 地址编号本身是 8 个字节(64位系统)or 4个字节(32位系统)的 无符号整数
int *p;
// 1. 定义一个指针变量 p,规定它指向 int
// 2. 如果用 void *p 可以指向任何类型
int a;
p = &a; // 把 a 的内存赋值给 p
// 或者 int *p = &a;
a = 1;
printf("%p = %p \n",p,&a); // 打印内存编号
*p = 10; // 给指针指向的位置赋值,*p 就代表a
// *p 指的是,取出 p 对应那块内存的内容,
// &a 指的是,取出 a 分配的内存编号
register int a;
定义的变量是无法用 & 取地址的,因为它一直在寄存器中。- 一定要保证类型一致,不然会发生混乱
空指针和野指针
// 野指针,会产生错误
int *p ;
*p = 100;
// 只是声明了一个指针,却未指向内存
// 空指针
int *p = NULL;
// 空指针是允许的
// NULL 其实是0
指向常量的指针 与 指针常量
// 指向常量的指针:你可以修改指针,但不能修改它指向的那个值。
int a = 0;
const int *p = &a; // p 指向常量的指针
// 可以通过 *p 读取,但不能通过 *p 写。可以通过 a 写。
// *p = 1; // 这个是不允许的
// a = 1; // 这个是允许的
// 可以这么理解,获取了 a 的只读权限
// 常量指针
int *const p = &a;
// 这个 p 是常量指针,不能再指向其它变量,例如 p = &a2 是错的
// 错误用法
const int a = 100;
int *p = &a; // 必需 const int *p = &a;
*p = 1;
printf("%d != %d\n", a, *p); // 不相同,好像和编译器有关,不知道为啥
printf("%p == %p", p, &a); // 相同
// 上面这个用法是不符合规范的。
指针运算
p + 1; // 指向下一个 “数据单元”,而不是16进制地址加1
p - 1;
// 实际移动多少 16 进制大小呢?取决于定义指针时的指定类型。例如 int 是 4,double 是 8
// 如果指针类型和变量类型不一致(虽然不符合规范),以指针类型为准
// 同样,也可以有这个:
p += 5;
p -= 3;
p++;
p--;
*(p + 3) = 100;
p[3] = 100; // 等价写法,即使不是数组,也能这么写,位移3个单位
有趣的例子1
int a = 0x12345678;
char *p = &a; // 也可以 char *p = (char *) &a;
printf("%x\n", *p);
printf("%x\n", *(p + 1));
printf("%x\n", *(p + 2));
printf("%x\n", p[3]);
// 返回:
// 78
// 56
// 34
// 12
// 1. 颠倒过来是因为整数是“正向对其”的
// 2. 也可以写入
有趣的例子2:ip 其实对应一个 int
unsigned int a = 987654321;
unsigned char *p = (unsigned char *)&a;
for(int i=3;i>=0;i--){
printf("%u.",p[i]);
}
思考: ip 如何转回为 int
unsigned char a[4] = {58, 222, 104, 177};
unsigned char tmp[4];
for (int i = 0; i < 4; i++) {
tmp[i] = a[3 - i];
}
unsigned int *p = &tmp;
printf("%d",*p);
指针与数组
指向数组元素的指针 int *p
int arr[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
int *p1 = arr; // 写法1
int *p3 = &arr[1]; // 写法2
int *p2 = (int *) &arr; // 写法3,&arr 是指向整个数组的指针,转为 int * 类型
p[3] = 100; // 可以直接当数组名用
*(p + 3); // 等价用法
printf("%ld != %ld ", sizeof(arr), sizeof(p)); //但又不完全一样
int arr[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
int *p = &arr[1];
p[3] = 100; // 其实是 a[4]
指向整个数组的指针 int (*p)[10]
// 方法1:
int arr[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
int (*arr1_p)[10] = &arr;
// int (*arr1_p)[] = &arr; // 省略长度,但不能用 arr1_p + 1 了
// 方法2:
int arr[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
typedef int (MY_ARRAY)[10];
MY_ARRAY *arr1 = &arr;
// 方法3:
int arr[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
typedef int (*MY_ARRAY_p)[10];
MY_ARRAY_p arr1_p = &arr;
解释:
- 其“基本单元”是数组,
p+1
指向下一个数组
int arr[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
int (*arr1_p)[10] = &arr;
printf("%p = %p\n", arr1_p, *arr1_p);
// 普通的数组也是这样,不知道如何深入理解
arr1_p + 1; // 是把这个数组当做一个“整体”,然后移动到下一个位置:5*int 个 Byte
*arr1_p; // 是个指针,指向第一个元素,指针类型是 int *
*arr1_p + 1; // 一个指针,指向第二个元素
*(*arr1_p + 1) ; // 第1个元素
*(arr1_p + 1) - 1; // 一个指针,指向倒数第1个元素
arr1_p[1] - 1; // 等价于上面的
指针组成的数组 int *p[5]
指针数组:一个数组,数组的元素是指针
int *p[5]; // 一个数组,它的元素是5个指针(叫做指针数组)
p[2]; // 这是一个指针
*p[2]; // 这是那个指针指向的值
// 你需要先把指针的指向赋值好,才能用这个指针
int a = 2;
p[3] = &a;
*p[3] = 5;
二级指针
指向指针的指针
int a = 0;
int *p = &a;
int **pp;//二级指针,指向指针的指针
pp = &p;
// 或者 int **p = &p;
**pp = 10; // 通过二级指针访问 a
如果想用一个指针指向指针数组,必须用一个二级指针指向它
int value = 10;
int *arr[10];//一个指针数组
arr[2] = &value;//指针数组的第2个,指向一个 int
int **p = arr; //指向指针数组的指针,必须用二级指针
p[2]; // 其实是 arr[2] ,存放的是一个指针
*p[2] = 5; // 那么,这个指针其实指向 value
类似的,还有多级指针
二维数组与指针
int mat[4][3] = {
{1, 2, 3},
{4, 5, 6},
{7, 8, 9},
{10, 11, 12}
};
printf("%p\n", mat); // 指向第0行第0个元素: &arr[0][0]
printf("%p\n", mat + 1); // 指向第1行: &arr[1][0]
printf("%p\n", &mat + 1); //指向下一块: &arr[3][2] + 1
// 用一个指针,指向二维数组(元素是一行)
int(*p)[3] = mat;
// p[i][j] 等价于 *(*(p+i)+j)
printf("%d", *(*(p + 1) + 2)); // 指向第1行第2个元素
// 用一个指针,指向整个二维数组(基本单元是整个 mat)
int (*p_mat)[3][3] = &mat
// 一维 arr 行为类似 int *,其基本元素是 int,arr + 1 指向下一个 int
// 二维 mat 行为类似 int (*p)[10],其基本元素是一行,mat + 1 指向下一行
指针作为函数的入参
// 1. 一维数组作为入参
int my_func1(int *arr, int len_arr);
// 2. 二维数组作为入参
// c99可以,c90不行:
int my_func1(int height, int width, int p[height][width]) {
for (int i = 0; i < height; i++) {
for (int j = 0; j < width; j++) {
printf("%d\t", p[i][j]);
}
printf("\n");
}
return 0;
}
int mat[4][3] = {
{1, 2, 3},
{4, 5, 6},
{7, 8, 9},
{10, 11, 12}
};
my_func1(4, 3, mat);
// 2.2 c90之前的版本只能这样(以 int * 的形式传入指针):
int my_func1(int height, int width, const int *p) {
for (int i = 0; i < height; i++) {
for (int j = 0; j < width; j++) {
printf("%d\t", *((int *) p + width * i + j));
}
printf("\n");
}
return 0;
}
// 2.3 不推荐的做法,因为它们必须事先确定列数
//my_fun(int len1, int len2, int mat[3][3])
//my_fun(int len1, int len2, int mat[][3])
//my_fun(int len1, int len2, int (*p)[3])
// 3. 字符串组成的数组作为入参
void my_func1(int argc, char **args) {
for (int i = 0; i < argc; i++) {
printf("%s, ", args[i]);
}
}
char *str_lst[] = {"hello", "world"};
my_func1(2, str_lst);
// 4. 以指向 xx 的指针作为入参,功能是可以在函数中改它的值
// 展示入参是:指向 int 的指针 + 指向一维数组的指针
// 它们的形式是 int *(形式上和一维数组一样), int **(形式上和二维数组一样)
void my_fun(int *size, int **array) {
*size = 5;
// 在函数中让它真正的指向一个一维数组,因此必需配合上面的 *size,否则外面不可见它的大小
*array = malloc(sizeof(int) * 5);
for (int i = 0; i < 5; i++) {
(*array)[i] = i + 10;
}
}
// 如何调用:
int *row = malloc(sizeof(int));
int **cols = malloc(sizeof(int *));
my_fun(row, cols);
知识点:当一个数组名作为入参时,自动作为指针变量。
// 以下三个是等价的:
int tst(int *a); // 规范写法
int tst(int a[10]);
int tst(int a[]);
// 编译的时候,3个都会编译成第1个
如果函数不修改数组,可以约定一个 const
int tst(const int *a); // 这样,函数内部就无法修改数组 a 了
// 其实做个强转后,还是能改的 int *p = (int *)a; p[5] = 999;
// C++ 没这漏洞了
// 约定:函数只要不改数组,都加 const
如果数组作为入参,数组的大小在函数中是不可见的
int tst(const int *a) {
printf("%lu\n", sizeof(a));
// 是指针本身的大小,必然是8
// ???好像并不是,用“指向数组的指针”,与数组有关系
return 0;
}
// 因此,往往需要额外传入一个数组长度。不过字符串不需要,因为字符串是 0 结尾的
int tst(int len_arr,int *arr) {
return 0;
}
// 指针作为入参,分不清是指向数组还是数字
int tst(int *arr, int *size);
// arr是数组,size 是指向数字的指针,但看起来形式一样
// 额外:这些值不一样:
int *a1;
printf("打印指针本身的大小:8 %lu\n", sizeof(a));
// 8
int a2[10];
printf("打印整个数组的大小:4x10=40 %lu\n", sizeof(a));
// 40,如果是
char a3[10] // sizeof 返回 1x10 = 10
指针作为函数的返回值
一个函数也可以返回一个指针
int *tst(); // 函数返回指针
main函数的参数
// 适配使用命令行启动
int main(int argc, char **args) {
printf("argc = %d\n",argc);
for (int i = 0; i < argc; i++) {
printf("%s, ", args[i]);
}
printf("\n");
return 0;
}
// 如何使用?
// 编译后:./main -l -s 0
// 打印:
// argc = 4
// ./main, -l, -s, 0,
// !注意,在linux下,* 指的是通配符,被目录下的全部文件替换,要想传入星号,要传入 \*
// 实际上 args[argc] 也是有值的,是 NULL
指向函数的指针
// f 是一个指针,指向函数
int (*f)();
// f 是一个指针,指向函数,这个函数的返回值是整型指针
int *(*f2)();
例子
int res1 = f(5);
int res2 = (*pf)(5);
int res3 = pf(5);
printf("%d = %d =%d\n", res1, res2, res3);
用途:提前不知道调用哪个函数
int my_add(int x, int y) {
return x + y;
}
int my_sub(int x, int y) {
return x - y;
}
int main() {
// 声明一个函数指针
int (*f)();
enum type {
ADD, SUB
};
enum type t = SUB;
// 根据情况,给函数指针赋值
if (t == ADD) {
f = &my_add;
} else {
f = &my_sub;
}
printf("%d\n", f(1, 4));
}
使用 函数指针数组,上面的代码可以进一步简化
int my_add(int x, int y) {
return x + y;
}
int my_sub(int x, int y) {
return x - y;
}
int my_mul(int x, int y) {
return x * y;
}
int my_div(int x, int y) {
return x / y;
}
int main() {
// 一个指针,指向一个数组,数组的内容是函数,这个函数返回 int 类型
int (*f[])(int, int) ={my_add, my_sub, my_mul, my_div};
printf("%d\n", f[1](1, 4));
}
指针的应用
如果想用函数改变某个变量的值,必须通过指针
int swap(int *a, int *b) {
int tmp = *a;
*a = *b;
*b = tmp;
return 0;
}
结构体作为入参时,会发生大量复制,因此一般用一个指向结构体的指针作为入参。
内存布局与malloc
内存布局分为4部分
- 代码加载到 代码区
- 所有的static、external放到 静态区
- auto类型、函数的形参、函数的返回值,放到 栈区
- 每个线程有自己的栈,
- 栈的最大尺寸固定,超过会溢出
- 变量离开作用范围后,栈上的数据自动释放。例如,函数调完后,函数内部声明的变量,其内存被清空.
- 堆,容量远远大于栈,堆内存的申请和释放必须通过代码完成
- 栈大小是有限的,很大的数组适合用堆
- 如果数组定义时大小不能确定,适合用堆
栈
int func(int a, int b){ // 从右向左入栈。b先入栈
}
堆
需要 #include <stdlib.h>
char *s = malloc(n)
分配 n 个字节- 只分配内存,不会帮你初始化
- 推荐用
malloc(n * sizeof(int))
,因为它可移植
calloc(cnt, size)
分配 cnt 个单位,每个单位 size 大小- 分配的内存自动初始化为 0
s1 = realloc(s1, size_new)
重新分配内存大小- 有可能重新分配内存地址
- 原本的 s1 会自动释放
- 新空间不会自动初始化为 0
realloc(NULL, size_new)
的效果等同于malloc
free(p)
不要忘了了,否则容易内存溢出
char *s = malloc(10); // 在堆中分配10个字节的空间
printf("%p\n", s);
strcpy(s, "abcd");
printf("%p\n", s);
free(s); // 必须手动释放内存,不是释放变量s,而是释放s指向的内存空间
printf("%p\n", s);
s = malloc(20); //s是变量,所以可以再次使用
printf("%p\n", s);
堆可以和“返回指针的函数”连用
下面这段代码编译可以通过,但不符合规范
int *tst() {
int a = 10;
return &a;
}
int main() {
int *p = tst();
printf("%d", *p);// 函数结束后,a 对应的内存值已经没了,所以p指向一个无效的空间。这个结果是不可预知的
}
正确的做法:
#include<stdio.h>
#include <stdlib.h>
#include <string.h>
int *tst() {
int *p = malloc(1 * sizeof(int));
return p;
}
int main() {
int *p = tst();
printf("%d", *p);
free(p); // 不要忘记释放内存
}
当然,字符数组作为指针,被返回时也会又相同的问题
// !!! 这是错的!
char *tst() {
char a[100] = "hello";
return a;
}
// 这样才是对的,(调用后别忘了释放内存)
char *tst() {
char *a= malloc(100);
strcpy(a,"hello");
return a;
}
// 其实这样也是对的,因为静态变量,整个程序结束后才会释放内存
char *tst() {
static char a[100] = "hello";
return a;
}
// 这个也是对的,常量也是一直在内存中的
const char *tst() {
const char *a = "hello";
return a;
}
// 上面等价于,这也是可以的
const char *tst() {
return "hello";
}
一些注意:
p = malloc(10)
分配内存后,不要用p++
,因为free(p)
会释放之后的10个字节,导致整个程序崩溃malloc
的入参可以是变量,意味着你可以不用魔法数字,而是动态指定大小。- 错误:忘记检查分配内存是否成功。
if(p == NULL)
,不过有说法现代环境不太需要检查 - 错误:使用超过了内存分配的边界
- 错误:忘记 free
- 错误:使用已经被 free 的内存
操作系统分配内存时,会一次给出4k(windows,实测MacBook是动态值),而不是每次 malloc 都分配一次。
- 所以,如果你事先知道大概需要占用多少内存,你可以用
char *s = malloc(4*1024); free(*s)
先把内存空间申请下来