数组名
数组名在大多数情况下会被隐式地转换为指向数组第一个元素的指针,在特定情况下数组名可以被视为一个指针,具有一些指针的特性
数组名和指针的不同点
但是数组名与真正的指针是不同的,主要有以下几点区别:
(1)使用 sizeof 运算符,数组名得到的是整个数组的大小;指针得到的是本身的大小
// 定义数组 int nums[5] = {10, 20, 30, 40, 50}; // 定义指针指向nums的首元素 int *ptr = &nums[0]; // 1. sizeof 计算结果不同 printf("sizeof nums: %zu \n", sizeof nums); // 20 printf("sizeof nums: %zu \n", sizeof ptr); // 8(2)数组名不能自增自减
// 2. 数组名不能进行自增、自减运算 // nums ++; // 报错 ptr++; // 地址指向下一个元素 printf("%p, %d \n", ptr, *ptr);(3)数组名的指向不能更改
// 3. nums不可以重新赋值; ptr可以重新赋值 // nums = {100, 200, 300, 400, 500}; //因为刚开始初始化了一个数组,这里就不能直接更改数组中的元素 // ptr = &nums[1];数组与指针的相同点
1. 使用 %p 打印数组名,可以得到首元素地址
2. 使用 * 解应用数组名,可以得到首元素的值
3. 数组名可以进行指针运算:加减整数、同类型指针相减、指针比较大小
4. 数组名[下标] 是个语法糖,本质是 *(数组名+下标)
int main() { // 定义数组 int nums[5] = {10, 20, 30, 40, 50}; // 定义指针指向nums的首元素 int *ptr = &nums[0]; // 1. 使用 %p 打印两个标识符的值 // 数组名可以当做首元素指针 printf("ptr=%p \n", ptr); printf("nums=%p \n", nums); printf("\n"); // 2. 解引用两个标识符 printf("*ptr=%d \n", *ptr); printf("*nums=%d \n", *nums); printf("\n"); // 3. nums 可以指针运算 printf("ptr+2: %p, *(ptr+2): %d \n", ptr + 2, *(ptr + 2)); printf("nums+2: %p, *(nums+2): %d \n", nums + 2, *(nums + 2)); printf("nums==ptr: %d \n", nums == ptr); printf("\n"); // 4. ptr使用[下标]访问数组元素 // 数组名[下标] 是个语法糖,本质是 *(数组名+下标) ptr[4] += 100; printf("第1个元素: %d \n", ptr[0]); printf("第2个元素: %d \n", ptr[1]); printf("第3个元素: %d \n", ptr[2]); printf("第4个元素: %d \n", ptr[3]); // 本质 *(ptr+3) nums[3]本质 *(nums+3) printf("第5个元素: %d \n", ptr[4]); printf("\n"); return 0; }核心疑问
ptr 是指针,存的是地址;int 占 4 字节,第 4 个元素下标 3,按理地址应该
ptr + 3*4,为什么 C 语言写的是*(ptr+3),不是*(ptr+12)?
关键点:C 语言指针加法,不是直接加字节!!指针 + 整数,整数代表元素个数,不是字节数。简单来说就是往后数三位
数组作为函数形参:数组退化为指针
⚠核心知识点:把数组传给函数的时候,不会把整个数组拷贝一份传进去,传递的仅仅是数组首元素的地址。
函数形参写成int arr[]只是语法糖,编译器在编译阶段会直接把它等价转换成int *arr。 也就是说,形参位置的arr本质就是一个普通指针变量,里面存放传入过来的首元素地址。
带来 2 个非常重要的后果
- 在函数内部,
sizeof(arr)得不到数组真实总字节大小,得到的是指针本身的字节大小(64 位系统得到 8),所以数组传参必须额外传入数组长度len。 - 实参可以传多种形式:数组名、已经保存首地址的指针、
&nums[0]首元素地址,三者效果完全一样。
#include <stdio.h> // int arr[] 只是语法糖,编译器等价处理为 int *arr int sum_array(int *arr, int len) { int sum = 0; for (int i = 0; i < len; i++) { // arr[i] 语法糖,底层等价 *(arr + i) sum += arr[i]; } return sum; } int main() { int nums[5] = {10, 20, 30, 40, 50}; int *ptr = &nums[0]; // 三种实参写法效果完全相同,都是传递首元素地址 printf("和: %d \n", sum_array(nums, 5)); printf("和: %d \n", sum_array(ptr, 5)); printf("和: %d \n", sum_array(&nums[0], 5)); return 0; }指针数组
定义
顾名思义就是存放指针的数组,其中的每个元素都是指针
#include <stdio.h> int main() { // 定义三个变量 int num1 = 100, num2 = 200, num3 = 300; // 定义指针数组,对三个变量进行取址,存放早数组中 int *ptr_arr[3] = {&num1, &num2, &num3}; // 遍历指针数组 for (int i = 0; i < 3; i++) { //元素地址指的是数组中元素的地址,//元素值指的是存放在数组中变量的地址,即为元素值 printf("下标:%d; 元素地址:%p; 元素值:%p; 元素指向的值:%d \n", i, &ptr_arr[i], ptr_arr[i], *ptr_arr[i]); } printf("数组ptr_arr所使用的内存空间:%zu \n", sizeof ptr_arr); return 0; }应用
可以定义一个二维字符数组里面可以存放不同长度的字符串
注:首字符地址凭什么能代表整个字符串?
地址本身只记录开头位置,它并不知道字符串有多长!
真正完整的字符串靠两件东西配合:
- 首字符地址(起点)
- 内存末尾隐藏的结束符
'\0'(终点标记)
#include <stdio.h> int main() { // 将多个城市名称保存到一个数组中 char city1[] = "Beijing"; char city2[] = "Shanghai"; char city3[] = "Bayinguolemeng"; char city4[] = "Xian"; // 定义数组 字符串名就表示首字符地址, 首字符地址就可以代表整个字符串(首元素地址代表整个数组) char *citys[4] = {city1, city2, city3, city4}; // 遍历 for (int i = 0; i < 4; i++) { printf("%s \n", citys[i]); //%s:打印整个字符串,只需要传首字符地址,不需要解引用,函数内部自己向后遍历直到\0 } printf("citys的内存空间大小:%zu \n", sizeof citys);// 32 return 0; }这样子,我们可以通过指针来实现存放不同长度的字符串的数组,相当于一个二维数组,通过遍历能打每一个字符串
为什么sizeof citys算出来是32?
因为我指针数组中存放的是字符串首字符地址,里面的每个元素都是指针
- 64 位系统,一个指针占 8 字节
- 数组一共 4 个指针元素
4 * 8 = 32→ 所以sizeof citys结果是 32
数组指针
数组指针(Array Pointer)是一个指针,它指向一个数组,而指针数组是存放指针的数组,两者要区分开。注意,数组指针指向的是整个数组的地址而不是第一个元素的地址,虽然二者值是相同的,但在运算中会表现出不同。
语法规则:数据类型(*数组指针名)[长度]
#include <stdio.h> int main() { // 定义数组 int nums[5] = {10, 20, 30, 40, 50}; // 显示定义指向首元素的指针 // int *ptr = &nums[0]; int *ptr = nums; // 与上一行等价 // 定义数组指针,指向整个数组 int(*arr_ptr)[5] = &nums; //%p 打印 printf("nums: %p \n", nums); // 00000015f1bff8f0 printf("arr_ptr: %p \n", arr_ptr); // 00000015f1bff8f0 printf("\n"); // 地址+1 printf("nums+1: %p \n", nums + 1); // 00000015f1bff8f4 printf("arr_ptr+1: %p \n", arr_ptr + 1); // 00000015f1bff904 因为数组占了20个字节,+1之后要移动到另一个数组指针,所以要移动20 // 两个标识符类型不同 // nums 的类型 作为数组时:int [5] 或者 作为首元素指针时:int * // arr_ptr 的类型是 int (*)[5] // 解引用 nums解应用解出来时首元素的值 arr_ptr解引用解出来是数组 printf("*nums: %d \n", *nums); printf("*arr_ptr: %d \n", (*arr_ptr)[0]); printf("**arr_ptr: %d \n", **arr_ptr);//因为int(*arr_ptr)[5] = &nums;所以arr_ptr解引用解出来的是nums首元素地址,再进行解引用才能拿首元素的值给解出来 return 0; }printf("**arr_ptr: %d \n", **arr_ptr);这里**是什么意思?
因为int(*arr_ptr)[5] = &nums;所以arr_ptr存放的是nums的地址,对其进行解引用,得到的是nums数组名--首元素地址,因为还是个地址所以要对他再次解引用,从而得到数组首元素的值也就是10.
字符指针
指向字符串字面量常量的字符指针,具有如下特点:
① 可以作为字符串使用,可以%s打印,可以遍历输出每个字符,可以strlen()获取长度
② 相对于字符数组名,字符指针可以修改指针,指向新的字符串
③ 不可以使用下标修改其中某个字符
④ 不能用于获取用户输入的字符串
#include <stdio.h> #include <string.h> int main() { // 定义字符数组 char msg[] = "Shanghaishi"; // 定义字符指针,指向msg首地址 char *str1 = msg; // 定义字符指针 指向字符串字面量常量 (这是与其他指针的区别) char *str2 = "Beijingshi"; // 使用 %s 打印 printf("msg: %s \n", msg); printf("str1: %s \n", str1); printf("str2: %s \n", str2); printf("\n"); // 使用 %p 打印 printf("msg: %p \n", msg); printf("str1: %p \n", str1); printf("str2: %p \n", str2); printf("\n"); // 使用 strlen() 计算字符串长度 printf("msg: %zu \n", strlen(msg));// 11 printf("str1: %zu \n", strlen(str1));// 11 printf("str2: %zu \n", strlen(str2));// 10 printf("\n"); // 使用 sizeof 计算 printf("msg: %zu \n", sizeof(msg));// 12 printf("str1: %zu \n", sizeof(str1));// 8 printf("str2: %zu \n", sizeof(str2));// 8 printf("\n"); return 0; }从中可以看到字符指针可以指向字面量常量而其他指针不行。
因str1指向的是msg的首元素地址,又因为msg是数组名也代表首元素地址,因此用%s和%p打印出来两者的内容是相同的。
疑问:为什么strlen算出来的长度和sizeof算出来的长度是不一样的?
strlen():库函数,统计字符串有效字符个数,不包含末尾\0结束符sizeof:运算符,算整个内存占用字节,会把字符串末尾'\0'也算进去
strlen(只看字符串内容,走到\0停下)
strlen(msg):11strlen(str1):str1 是 char * 指针,指向 msg 首字符,顺着内存读到\0,结果也是 11strlen(str2):统计字面量"Beijingshi"有效字符数量,不含\0
✔
strlen接收的是首字符地址,不管是数组名还是字符指针,只要给到字符串起始地址,就向后遍历直到遇见\0。
sizeof(算变量本身占多少内存)
sizeof(msg):msg 是真实字符数组,算出数组全部字节,包含末尾\0→12sizeof(str1):str1 只是普通char*指针变量,64 位系统指针占 8 字节 →8sizeof(str2):str2 同样是char*指针,也是 8 字节 →8
⚠重点!
sizeof(str2)不会去算字符串字面量 "Beijingshi" 的长度,sizeof 只算指针变量本身的内存大小!字符串字面量本体存放在只读常量区,str2 仅仅保存它的首地址。
重新赋值
// 重新赋值 // msg = "Xianshi";// 报错 // str1 = "Xianshi"; str2 = "Jiangsusheng"; // ----------------------------------------------- // 使用下标修改其中某个字符的值 msg[0] = 'H'; str1[0] = 'O'; // str2[0] = 'O';// 报错
- 数组不可重新赋值:msg = "Xianshi";会导致编译错误,因为数组名是常量指针指针可以
- 重新赋值:str1 = "Xianshi";和str2 = "Jiangsuzheng";都合法,因为指针变量可以改变指向
- 本质区别:数组名是地址常量,指针是地址变量
| 代码 | 内存位置 | 能否修改字符内容 |
|---|---|---|
char msg[] = "xxx"; | 栈内存,可读可写 | ✅可以msg[0]='H' |
char *str1 = msg; | 指针指向栈数组 | ✅str1[0]='O'修改栈数组 |
char *str2 = "xxx"; | 指针指向只读常量区 | ❌禁止str2[0]='O';但是可以改变指针指向str2="yyy" |
使用字符数组获取用户输入的字符串
char s1[100]; printf("请输入字符串:"); scanf("%s", s1);这里scanf中为什么不需要加&s1?
- 声明:char s1[100]分配固定大小的连续内存空间
- 输入:scanf("%s", s1)不需要取地址符,因为数组名本身就是首元素地址,所以就不再需要取地址了。