1.数组与指针
- 数组下标,其实际上是编译系统的一种简写,其等价形式是:
a[i] = 100; 等价于 *(a+i) = 100;
- 根据加法交换律,以下的所有的语句均是等价的:
#include <stdio.h>
int main()
{
int arr[] = {99, 15, 100, 888, 252};
int len = sizeof(arr) / sizeof(int); // 求数组长度
int i;
for (i = 0; i < len; i++)
{
printf("%d ", *(arr + i)); // *(arr+i)等价于arr[i]
}
printf("\n");
return 0;
}
- C语言中指针和数组的关系是非常紧密的,理解指针和数组之间的关系对于熟练掌握C语言非常关键,需要知道的是,用指针处理数组的主要原因是效率,但是这里 的效率提升已经不再像当初那么重要了,这主要归功于编译器的改进。
- 有上述可知,指针可以指向数组元素。例如,假设已经声明 a 和 p 如下:
int a[10], *p;
- 通过下列写法可以使 p 指向 a[0] :
p = &a[0];
- 现在可以通过 p 访问 a[0] 。例如,可以通过下列写法把值 5 存入 a[0] 中:
*p = 5;
- C语言支持3种(而且只有3种)格式的指针算术运算:
- 指针加上整数
- 指针减去整数
- 两个指针相减
- 下面的所有例子都假设有如下声明
int a[10], *p, *q, i;
1.1 指针加上整数
- 指针p 加上整数 j ,实际就是指针p 指向的地址移动了 j 个单位。如果 p 指向数组元素 a[i] ,那么 p+j 指向 a[ i + j ] (当然,前提是a[ i + j ] 必须 存在)
p = &a[2];
q = p + 3;
p += 6;
- 指针减去整数和指针加上整数同理
1.2 两个指针相减
当两个指针相减时,结果为指针之间的距离(用数组元素的个数来度量)。因此,如果p 指向 a[ i ] 且q 指向 a[ j ] ,那么p - q 就等于 i - j
p = &a[5];
q = &a[1];
i = p - q; // 4
i = q - p; //-4
例:
#include <stdio.h>
int my_strlen(char *str)
{
char *start = str;
while (*str != '\0')
{
str++;
}
return (str - start);
}
int main()
{
int len = my_strlen("abcdef"); // 字符串作为参数传递时,传递的是首地址
printf("%d\n", len); // 6
return 0;
}
2.数组名含义
- 数组名在不同情形会表现为不同的含义:
- 第一含义是整个数组
- 第二含义是首元素地址
- 触发第一含义的情形:
- 在数组定义中 // 这里的数组名表示整个数组,定义的是整个数组
- 在 sizeof 运算表达式中 // sizeof(数组名),这里的数组名表示整个数组,计算的是整个数组的大小
- 在取址符&中 // &数组名,这里的数组名表示整个数组,取出的是整个数组的地址
- 除了以上情况以外,其他所有情况都是第二含义。
- 我们发现,数组名的第二个含义似乎跟指针有关,没错,指针的算术运算是数组和指针之间相互关联的一种方法,但这不是两 者之间唯一的联系。另外一种联系是:可以用数组的名字作为指向数组第一个元素的指针 。这种关系简化了指针的算术运算, 而且使数组和指针更加通用。
// 假设用如下形式声明a
int a[10];
// 用a 作为指向数组第一个元素的指针,可以修改a[0] :
*a = 7;
// 可以通过指针a + 1 来修改a[1]
*(a + 1) = 12;
注意:通常情况下,a + i 等同于&a[i] (两者都表示指向数组a 中元素 i 的指针),并且*(a+i) 等价于a[i] (两者都表示元素 i 本 身)。换句话说,可以把数组的取下标操作看成是指针算术运算的一 种形式。
- 下面的例子说明了数组名代表首元素地址和整个数组的区别:
#include <stdio.h>
int main()
{
/*
1.在数组定义中
int a[10];
2.在 sizeof 运算表达式中
printf("%ld\n",sizeof(a));
3.在取址符&中
printf("%p\n",&a);
*/
int arr[] = {10, 11, 12, 13, 14}; // 5*4=20
printf("arr:%p\n", arr); // arr:0x7ffe1b248030 表示的是数组的首元素地址
printf("&arr:%p\n", &arr); // &arr:0x7ffe1b248030 表示的是整个数组的首地址
printf("&arr[0]:%p\n", &arr[0]); // &arr[0]:0x7ffe1b248030 对数组首元素进行取址
// 虚拟地址有12位,16进制每一位在二进制中占4位
// 12*4=48/8=6字节(地址占六个字节) 2^48=2^18G(物理空间没有那么大,为虚拟地址)
// arr+1:arr+1:0x7ffe1b248034 对首元素地址+1,地址值往后偏移一个int单位(4字节)
printf("arr+1:%p\n", arr + 1);
// &arr+1:0x7ffe1b248044 是整个数组地址加一个单位(一整个数组20字节)
printf("&arr+1:%p\n", &arr + 1);
// &arr[0]+1:0x7ffe1b248034 是对数组首元素进行取址后再+1,地址往后偏移一个int单位(4字节)
printf("&arr[0]+1:%p\n", &arr[0] + 1);
printf("*arr:%d\n", *arr); // *arr:10 对数组首元素地址进行解引用
printf("sizeof(arr):%ld\n", sizeof(arr)); // sizeof(arr):20 // 求整个数组所占内存大小
printf("sizeof(arr+0):%ld\n", sizeof(arr + 0));
// sizeof(arr+0):8 arr先与0结合,表示数组首元素偏移0个单位,得到的结果仍然为一个地址
// sizeof()对地址求值相当于对指针变量求值,所以结果为8(地址占6个字节,变为8字节以提高存取效率)
// 指针在不同位数机器下的长度:16位:2个字节,32位:4个字节,64位:8个字节
int a = 100;
int *p = &a;
printf("%ld\n", sizeof(p)); // 8
return 0;
}
例:
#include <stdio.h>
int main(void)
{
int a[5] = {1, 2, 3, 4, 5};
int *ptr = (int *)(&a + 1); // 强制转换
printf("%d,%d\n", *(a + 1), *(ptr - 1)); // 2,5
return 0;
}
3.char型指针
- char型指针实质上跟别的类型的指针并无本质区别,但由于C语言中的字符串以字符数组的方式存储,而数组在大多数场合又会表现为指针,因此字符串在绝大多数场合就表现为char型指针。但是字符数组和字符指针在存储空间上有本质的区别(后面会说到)
#include <stdio.h>
int main()
{
char a[10] = "abcd";
char *p = "abcd";
// 是把"abcd"这个字符串放入指针变量p?
// 不是,是把"abcd"这个字符串首元素的空间内存地址给p
printf("%c\n", *(p + 2)); // c
printf("%c\n", p[2]); // c
return 0;
}
4.void型指针
- 空指针NULL是没有指向的指针,将暂时用不到的指针定义成空指针,能防止误用。
- 而void * 类型的指针是有指向的指针,但它的指向的数据的类型暂时不确定,后期一般要强制转换
- 无法明确指针所指向的数据类型时,可以将指针定义为 void 型指针
- void 型指针无法直接索引目标,必须将其转换为一种具体类型的指针方可索引目标
- void 型指针无法进行加减法运算
- void关键字的三个作用:
- 修饰指针,表示指针指向一个类型未知的数据
- 修饰函数参数列表,表示函数不接收任何参数
- 修饰函数返回类型,表示函数不会返回任何数据
#include <stdio.h>
#include <stdlib.h>
int main()
{
// 指针 p 指向一块 4 字节的内存,且这4字节数据类型未确定
void *p = malloc(4);
// 1,将这 4 字节内存用来存储 int 型数据
*(int *)p = 100;
printf("%d\n", *(int *)p); // 100
// 2,将这 4 字节内存用来存储 float 型数据
*(float *)p = 3.14;
printf("%f\n", *(float *)p); // 3.140000
return 0;
}
- 注意:以上示例仅仅是为了讲解 void 的语法,实际应用中一般不需要定义 void 型指针,而是根据具体的需要,直接定义具体类型的指针
5.const型指针
- const是一个C语言的关键字,它限定一个变量不允许被改变,产生静态作用,也就是说经过const 修饰的变量成为只读的变量之后,那么这个变量就只能作为右值(只能赋值给别人),绝对不能成为左值(不能接收别人的赋值)
// 经过 const 修饰的变量,在定义的时候,就要进行初始化
const int a = 10; // 正确
const int a; // 错误
- const型指针有两种形式:①常量指针 ②指针常量
1.常量指针:const修饰指针本身,表示指针变量本身无法修改。
2.指针常量:const修饰指针的目标,表示无法通过该指针修改其目标。
- 常量指针在实际应用中不常见。
- 常目标指针在实际应用中广泛可见,用来限制指针的读写权限
int a = 100;
int b = 200;
// 第1种形式,const修饰p1本身,导致p1本身无法修改
int * const p1 = &a;
// 第2种形式,const修饰p2的目标,导致无法通过p2修改a
int const *p2 = &a;
//const int *p2 = &a;
6.二级指针
- 不管是二级指针还是多级指针,考虑它们和一级指针都是一样的,一级指针保存的是普通变量的地址,而二级(多级)指针保存的是指针变量的地址,即地址的地址。
#include <stdio.h>
int main()
{
int data = 100;
int *p = &data;
printf("data的地址是: %p\n", &data);
printf("p保存data的地址是: %p,内容是: %d\n", p, *p);
int **p2;
p2 = &p;
printf("p2保存的是p的地址: %p\n", p2);
printf("*p2是: %p\n", *p2);
printf("**p2来访问data: %d\n", **p2);
return 0;
}
/*
data的地址是: 0x7ffe9729f1b4
p保存data的地址是: 0x7ffe9729f1b4,内容是: 100
p2保存的是p的地址: 0x7ffe9729f1b8
*p2是: 0x7ffe9729f1b4
**p2来访问data: 100
*/
7.二维数组与指针
- 某些时候在描述二维数组时称之为二维,有行和列,但在内存中所有的数组元素都是连续排列的,它们之间没有“缝隙”。
int a[3][4] = { {0, 1, 2, 3}, {4, 5, 6, 7}, {8, 9, 10, 11} };
在内存中,a 的分布是一维线性的,整个数组占用一块连续的内存:
C语言允许把一个二维数组分解成多个一维数组来处理。对于数组 a,它可以分解成三个一维数组,即 a[0]、a[1]、a[2]。每一个一维数组又包含了 4 个元素,例如 a[0] 包含 a[0][0]、a[0][1]、a[0][2]、a[0][3]。
假设数组 a 中第 0 个元素的地址为 1000,那么每个一维数组的首地址如下图所示:
- 定义一个指向 a 的指针变量 p:
int (*p)[4] = a;
括号中的*表明 p 是一个指针,它指向一个数组,数组的类型为int [4],这正是 a 所包含的每个一维数组的类型。[ ] 的优先级高于 *,( )是必须要加的,如果赤裸裸地写作 int *p[4],那么应该理解为 int *(p[4]),p 就成了一个指针数组,而不是二维数组指针
p 指向的数据类型是int [4],那么p+1就前进 4×4 = 16 个字节,p-1就后退 16 个字节,这正好是数组 a 所包含的每个一维数组的长度。也就是说,p+1会使得指针指向二维数组的下一行,p-1会使得指针指向数组的上一行。
问下列表示什么意思?
p+1 :p+1 是对指针变量p前进一个单位,结果还是指向一个地址;在这里表示指向下一行数组的地址。
*(p+1) :*(p+1)表示第二行所有数据,在数值上和第二行首元素地址值相等。
*(p+1)+1 : 表示第二行第二个数据元素的地址 ,a[1][1]的地址
*(*(p+1)+1) :表示第二行第二个数据元素的值,a[1][1]的值
- 使用指针遍历二维数组:
#include <stdio.h>
int main()
{
int arr[3][4] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};
int(*p)[4];
int i, j;
p = arr;
for (i = 0; i < 3; i++)
{
for (j = 0; j < 4; j++)
printf("%d ", *(*(p + i) + j));
printf("\n"); // 每一行一维数组输出完之后换行
}
return 0;
}
/*
0 1 2 3
4 5 6 7
8 9 10 11
*/
8.什么是零长数组/柔性数组?为什么使用零长数组?
GNU/GCC 在标准的 C/C++ 基础上做了有实用性的扩展, 零长度数组(Arrays of Length Zero) 就是其中一个知名的扩展
struct Packet
{
int state;
int len;
char arr[0]; // 这里的0长结构体就为变长结构体提供了非常好的支持
};
- 用途 : 长度为0的数组的主要用途是为了满足需要变长度的结构体
- 用法 : 在一个结构体的最后, 申明一个长度为0的数组, 就可以使得这个结构体是可变长的. 对于编译器来说, 此时长度为0的数组并不占用空间, 因为数组名本身不占空间, 它只是一个偏移量, 数组名这个符号本身代表了一个不可修改的地址常量
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct buffer
{
int len;
char a[0];
};
int main(void)
{
struct buffer *buf;
buf = (struct buffer *)malloc(sizeof(struct buffer) + 20);
buf->len = 20;
strcpy(buf->a, "hello");
puts(buf->a); // hello
// puts 函数输出字符串自动换行输出
free(buf);
buf = NULL;
return 0;
}
在这个程序中,我们使用 malloc 申请一片内存,大小为 sizeof(buffer) + 20,即24个字节大小。其中4个字节用来存储结构体指针 buf 指向的结构体类型变量,另外20个字节空间,才是我们真正使用的内存空间。我们可以通过结构体成员 a,直接访问这片内存。
通过这种灵活的动态内存申请方式,这个 buffer 结构体表示的一片内存缓冲区,就可以随时调整,可大可小。这个特性,在一些场合非常有用。
比如,现在很多在线视频网站,都支持多种格式的视频播放:标清、高清、超清、蓝光甚至4K。如果我们本地程序需要在内存中申请一个 buffer 用来缓存解码后的视频数据,那么,不同的播放格式,需要的 buffer 大小是不一样的。 如果我们按照 4K 的标准去申请内存,那么当播放标清视频时,就用不了这么大的缓冲区,白白浪费内存。 而使用变长结构体,我们就可以根据用户的播放格式设置,灵活地申请不同大小的 buffer,大大节省了内存空间。
- 为什么不使用指针来代替零长度数组?
数组名在作为函数参数传递时,确实传递的是一个地址,但数组名绝不是指针,两者不是同一个东西。数组名用来表征一块连续内存存储空间的地址,而指针是一个变量,编译器要给它单独再分配一个内存空间,用来存放它指向的变量的地址。
#include <stdio.h>
struct buffer1
{
int len;
int a[0];
};
struct buffer2
{
int len;
int *a;
} __attribute__((packed)); // 将结构体压实
int main(void)
{
printf("buffer1: %ld\n", sizeof(struct buffer1)); // buffer1: 4
printf("buffer2: %ld\n", sizeof(struct buffer2)); // buffer2: 12
return 0;
}
- 对于一个指针变量,编译器要为这个指针变量单独分配一个存储空间,然后在这个存储空间上存放另一个变量的地址,我们就说这个指针指向这个变量
- 而数组名,编译器不会再给其分配一个存储空间的,它仅仅是一个符号,跟函数名一样,用来表示一个地址
至于为什么不使用指针来代替零长度数组呢?
例如:
使用零长度数组来写一个班级结构体的话可以这样:
#include <stdio.h>
#include <stdlib.h>
typedef struct student
{
int num; // 学号
char name[20]; // 姓名
char sex; // 性别
} student;
typedef struct class
{
char *teacher;
int class_id;
student students[];
} class;
int main()
{
class *classA;
classA = (class *)malloc(sizeof(class) + sizeof(student) * 5);
// 遍历学生赋值
for (int i = 0; i < 5; i++)
{
classA->students[i].num = 230580 + i;
printf("请输入学号为%d的姓名:\n", classA->students[i].num);
scanf("%s", classA->students[i].name);
}
// 遍历学生输出
for (int i = 0; i < 5; i++)
{
printf("学号%d的姓名是%s:\n", classA->students[i].num, classA->students[i].name);
}
// 释放资源
free(classA);
classA = NULL; // 置空
return 0;
}
使用指针来写的话则会变成:
#include <stdio.h>
#include <stdlib.h>
typedef struct student
{
int num; // 学号
char name[20]; // 姓名
char sex; // 性别
} student;
typedef struct class
{
char *teacher;
int class_id;
student *students;
} class;
int main()
{
class *classA;
classA = (class *)malloc(sizeof(class));
classA->students = (student *)malloc(sizeof(student) * 5);
// 遍历学生赋值
for (int i = 0; i < 5; i++)
{
classA->students[i].num = 230580 + i;
printf("请输入学号为%d的姓名:\n", classA->students[i].num);
scanf("%s", classA->students[i].name);
}
// 遍历学生输出
for (int i = 0; i < 5; i++)
{
printf("学号%d的姓名是%s:\n", classA->students[i].num, classA->students[i].name);
}
// 释放资源
free(classA->students);
classA->students = NULL;
free(classA);
classA = NULL;
return 0;
}
总结:
- 零长数组一般和结构体搭配使用,其比起在结构体中声明一个指针变量、再进行动态分配的效率要高。因为在访问数组内容时,不需要间接访问,避免了两次访存。
- 同时因为零长数组表示数据内容时,其数据空间是动态分配的,所以比静态分配内存要灵活。