文章目录
- [**深入指针3 - 完全精讲版**](#深入指针3 - 完全精讲版)
-
- **一、数组名的深度理解**
-
- [**1. 验证:数组名 = 数组首地址**](#1. 验证:数组名 = 数组首地址)
- [**2. 验证:sizeof(数组名)计算整个数组大小**](#2. 验证:sizeof(数组名)计算整个数组大小)
- [**3. 验证:&arr取的是整个数组的地址**](#3. 验证:&arr取的是整个数组的地址)
- **二、区分数组名地址和数组地址有什么用?**
-
- [**指针类型决定步长 - 深入剖析**](#指针类型决定步长 - 深入剖析)
- **实际应用场景**
- [**三、使用指针访问数组 - 全方位解析**](#三、使用指针访问数组 - 全方位解析)
-
- [**1. 指针访问的多种写法**](#1. 指针访问的多种写法)
- [**2. 指针运算的细节**](#2. 指针运算的细节)
- [**3. 指针访问的边界问题**](#3. 指针访问的边界问题)
- [**四、一维数组传参的本质 - 深入剖析**](#四、一维数组传参的本质 - 深入剖析)
-
- [**1. 验证传参本质**](#1. 验证传参本质)
- [**2. 为什么必须传递数组长度**](#2. 为什么必须传递数组长度)
- [**3. 多维数组传参**](#3. 多维数组传参)
- [**五、冒泡排序和选择排序 - 完整实现与优化**](#五、冒泡排序和选择排序 - 完整实现与优化)
-
- [**1. 冒泡排序详解**](#1. 冒泡排序详解)
- [**2. 选择排序详解**](#2. 选择排序详解)
- [**3. 排序算法对比**](#3. 排序算法对比)
- [**六、二级指针 - 深入理解**](#六、二级指针 - 深入理解)
-
- [**1. 二级指针的基本概念**](#1. 二级指针的基本概念)
- [**2. 二级指针的典型应用**](#2. 二级指针的典型应用)
- [**3. 多级指针的灵活运用**](#3. 多级指针的灵活运用)
- [**七、指针数组 - 完整解析**](#七、指针数组 - 完整解析)
-
- [**1. 指针数组的基本概念**](#1. 指针数组的基本概念)
- [**2. 不同类型指针数组**](#2. 不同类型指针数组)
- **八、指针数组格式书写举例**
-
- [**1. 各种指针数组定义示例**](#1. 各种指针数组定义示例)
- [**2. 复杂指针数组声明解析**](#2. 复杂指针数组声明解析)
- [**九、指针数组模拟二维数组 - 深入剖析**](#九、指针数组模拟二维数组 - 深入剖析)
-
- [**1. 基本原理和内存布局**](#1. 基本原理和内存布局)
- [**2. 与真实二维数组的对比**](#2. 与真实二维数组的对比)
- [**3. 动态创建和操作**](#3. 动态创建和操作)
- [**十、字符指针变量 - 全方位解析**](#十、字符指针变量 - 全方位解析)
-
- [**1. 字符指针的基础用法**](#1. 字符指针的基础用法)
- [**2. 字符指针的运算**](#2. 字符指针的运算)
- [**3. 字符指针数组 - 字符串数组**](#3. 字符指针数组 - 字符串数组)
- [**十一、不可以修改常量字符串的值 - 深入理解**](#十一、不可以修改常量字符串的值 - 深入理解)
-
- [**1. 为什么不能修改**](#1. 为什么不能修改)
- [**2. 内存分区详解**](#2. 内存分区详解)
- [**十二、《剑指offer》字符串笔试题 - 详细解析**](#十二、《剑指offer》字符串笔试题 - 详细解析)
-
- [**1. 原题解析**](#1. 原题解析)
- [**2. 深入扩展**](#2. 深入扩展)
- [**十三、数组指针 - 完整精讲**](#十三、数组指针 - 完整精讲)
-
- [**1. 数组指针的基本概念**](#1. 数组指针的基本概念)
- [**2. 数组指针的声明解析**](#2. 数组指针的声明解析)
- [**3. 数组指针的应用场景**](#3. 数组指针的应用场景)
- [**4. 数组指针与指针数组对比总结**](#4. 数组指针与指针数组对比总结)
- **总结:指针核心概念速查表**
深入指针3 - 完全精讲版
一、数组名的深度理解
数组名在C语言中是一个特殊的存在,它既是地址,又不完全是地址,需要深入理解其本质。
1. 验证:数组名 = 数组首地址
c
#include <stdio.h>
int main() {
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
printf("&arr[0] = %p\n", &arr[0]); // 第一个元素的地址
printf("arr = %p\n", arr); // 数组名
printf("&arr = %p\n", &arr); // 整个数组的地址
// 额外验证:打印地址的整数值
printf("&arr[0] = %lu\n", (unsigned long)&arr[0]);
printf("arr = %lu\n", (unsigned long)arr);
return 0;
}
运行结果:
&arr[0] = 0060FEF0
arr = 0060FEF0
&arr[0] = 6355696
arr = 6355696
为什么是这样?
- 数组在内存中是连续存储的
- 数组名就是这块连续内存的起始地址
- 第一个元素的地址自然就是数组的起始地址
2. 验证:sizeof(数组名)计算整个数组大小
c
#include <stdio.h>
int main() {
// 测试不同数据类型的数组
int arr_int[10] = {0};
char arr_char[10] = {0};
double arr_double[10] = {0};
printf("int数组:\n");
printf(" sizeof(arr_int[0]) = %d\n", sizeof(arr_int[0])); // 4
printf(" sizeof(arr_int) = %d\n", sizeof(arr_int)); // 40
printf(" 数组长度 = %d\n", sizeof(arr_int) / sizeof(arr_int[0])); // 10
printf("\nchar数组:\n");
printf(" sizeof(arr_char[0]) = %d\n", sizeof(arr_char[0])); // 1
printf(" sizeof(arr_char) = %d\n", sizeof(arr_char)); // 10
printf(" 数组长度 = %d\n", sizeof(arr_char) / sizeof(arr_char[0])); // 10
printf("\ndouble数组:\n");
printf(" sizeof(arr_double[0]) = %d\n", sizeof(arr_double[0])); // 8
printf(" sizeof(arr_double) = %d\n", sizeof(arr_double)); // 80
printf(" 数组长度 = %d\n", sizeof(arr_double) / sizeof(arr_double[0])); // 10
return 0;
}
重要发现:
- sizeof(数组名) = 数组元素类型大小 × 元素个数
- 这就是为什么常用
sizeof(arr)/sizeof(arr[0])计算数组长度 - 注意: 这个公式只在数组定义的作用域内有效
3. 验证:&arr取的是整个数组的地址
c
#include <stdio.h>
int main() {
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
printf("三个地址的值相同:\n");
printf("&arr[0] = %p\n", &arr[0]);
printf("arr = %p\n", arr);
printf("&arr = %p\n", &arr);
printf("\n但它们的类型不同,导致+1结果不同:\n");
printf("&arr[0]+1 = %p (跳过一个int)\n", &arr[0]+1);
printf("arr+1 = %p (跳过一个int)\n", arr+1);
printf("&arr+1 = %p (跳过整个数组)\n", &arr+1);
// 计算跳过的字节数
printf("\n跳过的字节数:\n");
printf("arr+1 跳过: %d 字节\n", (char*)(arr+1) - (char*)arr);
printf("&arr+1 跳过: %d 字节\n", (char*)(&arr+1) - (char*)&arr);
return 0;
}
内存布局图解:
数组arr[10]的内存布局:
+----+----+----+----+----+----+----+----+----+----+
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
+----+----+----+----+----+----+----+----+----+----+
^ ^ ^ ^
| | | |
arr arr+1 arr+5 &arr+1
(arr[0]) (arr[1]) (arr[5]) (越过数组)
arr = 0x1000
arr+1 = 0x1004
&arr+1 = 0x1028 (0x1000 + 40)
二、区分数组名地址和数组地址有什么用?
指针类型决定步长 - 深入剖析
c
#include <stdio.h>
int main() {
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
// 1. 验证不同类型的指针步长
int* p1 = arr; // 首元素指针,类型 int*
int* p2 = &arr[0]; // 首元素指针,类型 int*
int(*p3)[10] = &arr; // 整个数组指针,类型 int(*)[10]
printf("p1 类型: int*, 大小: %d 字节\n", sizeof(p1));
printf("p3 类型: int(*)[10], 大小: %d 字节\n", sizeof(p3));
printf("但p1和p3本身都是指针,都占4字节\n\n");
printf("p1 指向的内容大小: %d 字节\n", sizeof(*p1)); // 4字节
printf("p3 指向的内容大小: %d 字节\n", sizeof(*p3)); // 40字节
// 2. 实际应用:数组遍历方式对比
printf("\n使用int*遍历数组:\n");
for(int i = 0; i < 10; i++) {
printf("%d ", *(p1 + i));
}
// 3. 使用数组指针的错误示例
// for(int i = 0; i < 10; i++) {
// printf("%d ", *(*p3 + i)); // *p3得到数组首地址,再加i
// }
// 4. 二级指针和数组指针的关系
int** pp = &p1; // 二级指针,指向int*
// int(*p3)[10] = &arr; // 数组指针,指向int[10]
return 0;
}
实际应用场景
c
#include <stdio.h>
// 场景1:函数需要处理整个数组的地址
void print_array(int (*p)[10]) { // 接收数组指针
for(int i = 0; i < 10; i++) {
printf("%d ", (*p)[i]); // (*p)得到数组名,再下标访问
}
}
// 场景2:函数处理首元素地址(常见方式)
void print_array2(int* p, int n) {
for(int i = 0; i < n; i++) {
printf("%d ", p[i]);
}
}
int main() {
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
printf("使用数组指针访问:");
print_array(&arr); // 必须传&arr
printf("\n使用首元素指针访问:");
print_array2(arr, 10); // 传arr
return 0;
}
三、使用指针访问数组 - 全方位解析
1. 指针访问的多种写法
c
#include <stdio.h>
int main() {
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int* p = arr;
int sz = sizeof(arr) / sizeof(arr[0]);
printf("数组元素:\n");
for (int i = 0; i < sz; i++) {
// 所有写法等价
printf("arr[%d] = ", i);
printf("%d ", arr[i]); // 标准写法
printf("%d ", *(arr + i)); // 指针形式
printf("%d ", *(p + i)); // 用p指针
printf("%d ", p[i]); // p[i] 等价于 arr[i]
printf("%d ", i[arr]); // 奇特的交换写法
printf("\n");
}
// 验证 [] 的本质
printf("\n汇编角度理解:\n");
printf("arr[5] = %d\n", arr[5]);
printf("*(arr+5) = %d\n", *(arr+5));
printf("5[arr] = %d\n", 5[arr]);
return 0;
}
2. 指针运算的细节
c
#include <stdio.h>
int main() {
int arr[10] = {0};
int* p = arr;
// 指针加法
printf("p = %p\n", p);
printf("p + 1 = %p (加4字节)\n", p + 1);
printf("p + 2 = %p (加8字节)\n", p + 2);
// 指针减法
printf("\n指针减法:\n");
printf("(p+5) - p = %ld (相差5个元素)\n", (p+5) - p);
// 指针比较
printf("\n指针比较:\n");
if(p < p+5) {
printf("p 在 p+5 之前\n");
}
// 通过指针修改数组
for(int i = 0; i < 10; i++) {
*(p + i) = i * 10; // 赋值:0,10,20,...
}
printf("\n修改后的数组:");
for(int i = 0; i < 10; i++) {
printf("%d ", arr[i]);
}
return 0;
}
3. 指针访问的边界问题
c
#include <stdio.h>
int main() {
int arr[5] = {1,2,3,4,5};
int* p = arr;
// 合法访问
printf("合法访问:\n");
for(int i = 0; i < 5; i++) {
printf("%d ", *(p + i));
}
// 危险访问(越界)
printf("\n\n危险访问(越界):\n");
printf("p-1 = %p, 值 = %d (未知)\n", p-1, *(p-1));
printf("p+5 = %p, 值 = %d (未知)\n", p+5, *(p+5));
// 使用指针访问时一定要确保不越界
return 0;
}
四、一维数组传参的本质 - 深入剖析
1. 验证传参本质
c
#include <stdio.h>
// 不同的形参写法,本质都是指针
void test1(int arr[10]) {
printf("test1: sizeof(arr) = %d\n", sizeof(arr)); // 4
printf("test1: arr = %p\n", arr);
}
void test2(int arr[]) {
printf("test2: sizeof(arr) = %d\n", sizeof(arr)); // 4
printf("test2: arr = %p\n", arr);
}
void test3(int* arr) {
printf("test3: sizeof(arr) = %d\n", sizeof(arr)); // 4
printf("test3: arr = %p\n", arr);
}
int main() {
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
printf("main中: sizeof(arr) = %d\n", sizeof(arr)); // 40
printf("main中: arr = %p\n\n", arr);
test1(arr);
test2(arr);
test3(arr);
return 0;
}
输出分析:
main中: sizeof(arr) = 40
main中: arr = 0060FEF0
test1: sizeof(arr) = 4
test1: arr = 0060FEF0
test2: sizeof(arr) = 4
test2: arr = 0060FEF0
test3: sizeof(arr) = 4
test3: arr = 0060FEF0
2. 为什么必须传递数组长度
c
#include <stdio.h>
// 错误的函数:无法知道数组长度
void bad_function(int arr[]) {
// 无法知道数组实际长度
int len = sizeof(arr) / sizeof(arr[0]); // len = 1 (4/4)
printf("错误计算的长度: %d\n", len);
// 危险!可能越界
for(int i = 0; i < 10; i++) {
printf("%d ", arr[i]); // 如果传入的数组小于10,会越界
}
}
// 正确的函数:传入长度
void good_function(int arr[], int n) {
printf("正确的访问: ");
for(int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}
int main() {
int arr1[10] = {1,2,3,4,5,6,7,8,9,10};
int arr2[5] = {1,2,3,4,5};
printf("处理arr1:\n");
bad_function(arr1); // 危险!
good_function(arr1, 10); // 正确
printf("\n处理arr2:\n");
bad_function(arr2); // 危险!会访问越界
good_function(arr2, 5); // 正确
return 0;
}
3. 多维数组传参
c
#include <stdio.h>
// 二维数组传参
void print_2d_array(int arr[][3], int rows) {
for(int i = 0; i < rows; i++) {
for(int j = 0; j < 3; j++) {
printf("%d ", arr[i][j]);
}
printf("\n");
}
}
// 使用数组指针
void print_2d_array2(int (*arr)[3], int rows) {
for(int i = 0; i < rows; i++) {
for(int j = 0; j < 3; j++) {
printf("%d ", arr[i][j]);
}
printf("\n");
}
}
int main() {
int arr[2][3] = {{1,2,3}, {4,5,6}};
printf("方式1:\n");
print_2d_array(arr, 2);
printf("\n方式2:\n");
print_2d_array2(arr, 2);
return 0;
}
五、冒泡排序和选择排序 - 完整实现与优化
1. 冒泡排序详解
c
#include <stdio.h>
#include <stdbool.h>
// 打印数组
void print_array(int arr[], int n) {
for(int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}
// 基础版冒泡排序
void bubble_sort_basic(int arr[], int n) {
int compare_count = 0;
int swap_count = 0;
for(int i = 0; i < n-1; i++) {
for(int j = 0; j < n-1-i; j++) {
compare_count++;
if(arr[j] > arr[j+1]) {
swap_count++;
int temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
printf("第%d趟后: ", i+1);
print_array(arr, n);
}
printf("总比较次数: %d, 总交换次数: %d\n", compare_count, swap_count);
}
// 优化版冒泡排序
void bubble_sort_optimized(int arr[], int n) {
int compare_count = 0;
int swap_count = 0;
for(int i = 0; i < n-1; i++) {
bool swapped = false; // 标志位
for(int j = 0; j < n-1-i; j++) {
compare_count++;
if(arr[j] > arr[j+1]) {
swapped = true;
swap_count++;
int temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
printf("第%d趟后: ", i+1);
print_array(arr, n);
if(!swapped) {
printf("第%d趟没有交换,排序完成\n", i+1);
break;
}
}
printf("总比较次数: %d, 总交换次数: %d\n", compare_count, swap_count);
}
int main() {
// 测试不同情况
int arr1[] = {5, 4, 3, 2, 1}; // 逆序
int arr2[] = {1, 2, 3, 4, 5}; // 已有序
int arr3[] = {3, 1, 4, 2, 5}; // 乱序
int n1 = sizeof(arr1)/sizeof(arr1[0]);
int n2 = sizeof(arr2)/sizeof(arr2[0]);
int n3 = sizeof(arr3)/sizeof(arr3[0]);
printf("=== 逆序数组 冒泡排序 ===\n");
bubble_sort_optimized(arr1, n1);
printf("\n=== 有序数组 基础版 ===\n");
bubble_sort_basic(arr2, n2);
int arr2_2[] = {1, 2, 3, 4, 5};
printf("\n=== 有序数组 优化版 ===\n");
bubble_sort_optimized(arr2_2, n2);
printf("\n=== 乱序数组 优化版 ===\n");
bubble_sort_optimized(arr3, n3);
return 0;
}
2. 选择排序详解
c
#include <stdio.h>
// 选择排序实现
void selection_sort(int arr[], int n) {
int compare_count = 0;
int swap_count = 0;
for(int i = 0; i < n-1; i++) {
int min_index = i; // 假设当前位置是最小值
// 在未排序部分找最小值
for(int j = i+1; j < n; j++) {
compare_count++;
if(arr[j] < arr[min_index]) {
min_index = j;
}
}
// 将最小值交换到当前位置
if(min_index != i) {
swap_count++;
int temp = arr[i];
arr[i] = arr[min_index];
arr[min_index] = temp;
}
printf("第%d次选择后: ", i+1);
for(int k = 0; k < n; k++) {
printf("%d ", arr[k]);
}
printf(" (选择了arr[%d]=%d)\n", min_index, arr[i]);
}
printf("总比较次数: %d, 总交换次数: %d\n\n", compare_count, swap_count);
}
// 双向选择排序(同时找最大和最小)
void double_selection_sort(int arr[], int n) {
int left = 0, right = n-1;
int compare_count = 0;
int swap_count = 0;
while(left < right) {
int min_index = left;
int max_index = left;
// 在一趟中同时找最大和最小
for(int i = left; i <= right; i++) {
compare_count += 2;
if(arr[i] < arr[min_index]) {
min_index = i;
}
if(arr[i] > arr[max_index]) {
max_index = i;
}
}
// 交换最小值到左边
if(min_index != left) {
swap_count++;
int temp = arr[left];
arr[left] = arr[min_index];
arr[min_index] = temp;
}
// 如果最大值在left位置,它可能被刚才的交换移走了
if(max_index == left) {
max_index = min_index;
}
// 交换最大值到右边
if(max_index != right) {
swap_count++;
int temp = arr[right];
arr[right] = arr[max_index];
arr[max_index] = temp;
}
left++;
right--;
printf("第%d次双向选择: ", left);
for(int k = 0; k < n; k++) {
printf("%d ", arr[k]);
}
printf("\n");
}
printf("总比较次数: %d, 总交换次数: %d\n", compare_count, swap_count);
}
int main() {
int arr1[] = {64, 25, 12, 22, 11};
int arr2[] = {64, 25, 12, 22, 11};
int n = sizeof(arr1)/sizeof(arr1[0]);
printf("原始数组: ");
for(int i = 0; i < n; i++) printf("%d ", arr1[i]);
printf("\n\n");
printf("=== 普通选择排序 ===\n");
selection_sort(arr1, n);
printf("\n最终结果: ");
for(int i = 0; i < n; i++) printf("%d ", arr1[i]);
printf("\n\n=== 双向选择排序 ===\n");
double_selection_sort(arr2, n);
printf("\n最终结果: ");
for(int i = 0; i < n; i++) printf("%d ", arr2[i]);
return 0;
}
3. 排序算法对比
c
#include <stdio.h>
#include <time.h>
// 冒泡排序
void bubble_sort(int arr[], int n) {
for(int i = 0; i < n-1; i++) {
for(int j = 0; j < n-1-i; j++) {
if(arr[j] > arr[j+1]) {
int temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
}
}
// 选择排序
void selection_sort(int arr[], int n) {
for(int i = 0; i < n-1; i++) {
int min = i;
for(int j = i+1; j < n; j++) {
if(arr[j] < arr[min]) {
min = j;
}
}
if(min != i) {
int temp = arr[i];
arr[i] = arr[min];
arr[min] = temp;
}
}
}
// 性能测试
void test_performance() {
const int SIZE = 10000;
int arr1[SIZE], arr2[SIZE];
clock_t start, end;
// 初始化随机数组
for(int i = 0; i < SIZE; i++) {
arr1[i] = rand() % 10000;
arr2[i] = arr1[i];
}
printf("排序 %d 个元素:\n", SIZE);
// 测试冒泡排序
start = clock();
bubble_sort(arr1, SIZE);
end = clock();
printf("冒泡排序时间: %.3f 秒\n", (double)(end-start)/CLOCKS_PER_SEC);
// 测试选择排序
start = clock();
selection_sort(arr2, SIZE);
end = clock();
printf("选择排序时间: %.3f 秒\n", (double)(end-start)/CLOCKS_PER_SEC);
}
int main() {
test_performance();
return 0;
}
六、二级指针 - 深入理解
1. 二级指针的基本概念
c
#include <stdio.h>
int main() {
int a = 10;
int* pa = &a; // 一级指针:指向int变量
int** ppa = &pa; // 二级指针:指向int*变量
int*** pppa = &ppa; // 三级指针:指向int**变量
printf("a = %d\n", a);
printf("&a = %p\n", &a);
printf("\npa = %p\n", pa);
printf("&pa = %p\n", &pa);
printf("*pa = %d\n", *pa);
printf("\nppa = %p\n", ppa);
printf("&ppa = %p\n", &ppa);
printf("*ppa = %p\n", *ppa); // 得到pa的值(a的地址)
printf("**ppa = %d\n", **ppa); // 得到a的值
printf("\n三级指针:\n");
printf("***pppa = %d\n", ***pppa);
return 0;
}
内存示意图:
内存地址 内容 变量名
0x1000 10 a
0x2000 0x1000 pa
0x3000 0x2000 ppa
0x4000 0x3000 pppa
2. 二级指针的典型应用
c
#include <stdio.h>
#include <stdlib.h>
// 应用1:在函数中修改一级指针的值
void allocate_memory(int** p, int size) {
*p = (int*)malloc(size * sizeof(int));
if(*p != NULL) {
printf("内存分配成功,地址: %p\n", *p);
}
}
void test_allocate() {
int* arr = NULL;
allocate_memory(&arr, 10); // 传一级指针的地址
if(arr != NULL) {
// 使用内存
for(int i = 0; i < 10; i++) {
arr[i] = i;
}
free(arr);
}
}
// 应用2:二维数组的动态创建
int** create_2d_array(int rows, int cols) {
int** matrix = (int**)malloc(rows * sizeof(int*));
for(int i = 0; i < rows; i++) {
matrix[i] = (int*)malloc(cols * sizeof(int));
}
return matrix;
}
void free_2d_array(int** matrix, int rows) {
for(int i = 0; i < rows; i++) {
free(matrix[i]);
}
free(matrix);
}
// 应用3:指针数组的操作
void print_strings(char** strings, int count) {
for(int i = 0; i < count; i++) {
printf("字符串%d: %s (地址: %p)\n",
i+1, strings[i], strings[i]);
}
}
int main() {
// 测试内存分配
printf("=== 测试内存分配 ===\n");
test_allocate();
// 测试二维数组
printf("\n=== 测试二维数组 ===\n");
int** matrix = create_2d_array(3, 4);
for(int i = 0; i < 3; i++) {
for(int j = 0; j < 4; j++) {
matrix[i][j] = i * 4 + j;
}
}
free_2d_array(matrix, 3);
// 测试字符串数组
printf("\n=== 测试字符串数组 ===\n");
char* fruits[] = {"苹果", "香蕉", "橙子"};
print_strings(fruits, 3);
return 0;
}
3. 多级指针的灵活运用
c
#include <stdio.h>
void demonstrate_multi_level() {
int x = 100;
int* p1 = &x;
int** p2 = &p1;
int*** p3 = &p2;
int**** p4 = &p3;
printf("x = %d\n", x);
printf("*p1 = %d\n", *p1);
printf("**p2 = %d\n", **p2);
printf("***p3 = %d\n", ***p3);
printf("****p4 = %d\n", ****p4);
// 修改值
****p4 = 200;
printf("\n修改后:\n");
printf("x = %d\n", x);
}
int main() {
demonstrate_multi_level();
return 0;
}
七、指针数组 - 完整解析
1. 指针数组的基本概念
c
#include <stdio.h>
int main() {
// 定义指针数组的多种方式
int a = 10, b = 20, c = 30;
// 方式1:直接初始化
int* parr1[3] = {&a, &b, &c};
// 方式2:先定义后赋值
int* parr2[3];
parr2[0] = &a;
parr2[1] = &b;
parr2[2] = &c;
// 访问方式
printf("通过指针数组访问变量:\n");
for(int i = 0; i < 3; i++) {
printf("parr1[%d] = %p, 指向的值 = %d\n",
i, parr1[i], *parr1[i]);
}
// 指针数组的内存布局
printf("\n内存布局:\n");
printf("parr1数组起始地址: %p\n", parr1);
printf("parr1[0]地址: %p, 内容: %p\n", &parr1[0], parr1[0]);
printf("parr1[1]地址: %p, 内容: %p\n", &parr1[1], parr1[1]);
printf("parr1[2]地址: %p, 内容: %p\n", &parr1[2], parr1[2]);
return 0;
}
2. 不同类型指针数组
c
#include <stdio.h>
int main() {
// 1. int* 类型的指针数组
int int_arr[] = {1,2,3};
int* int_ptrs[3];
for(int i = 0; i < 3; i++) {
int_ptrs[i] = &int_arr[i];
}
// 2. char* 类型的指针数组(字符串数组)
char* str_ptrs[3] = {
"Hello",
"World",
"Pointer"
};
// 3. double* 类型的指针数组
double d1 = 3.14, d2 = 2.718, d3 = 1.414;
double* d_ptrs[3] = {&d1, &d2, &d3};
// 4. void* 类型的指针数组(可以指向任何类型)
void* v_ptrs[3];
v_ptrs[0] = &d1;
v_ptrs[1] = str_ptrs[0];
v_ptrs[2] = &int_arr[0];
printf("字符串指针数组内容:\n");
for(int i = 0; i < 3; i++) {
printf("str_ptrs[%d] = %s\n", i, str_ptrs[i]);
}
return 0;
}
八、指针数组格式书写举例
1. 各种指针数组定义示例
c
#include <stdio.h>
// 定义一些数据供指针指向
int global_a = 100;
int global_b = 200;
double global_pi = 3.14159;
char global_ch = 'Z';
int main() {
// 示例1:指向局部变量的指针数组
int local_x = 10, local_y = 20, local_z = 30;
int* ptr_to_locals[3] = {&local_x, &local_y, &local_z};
// 示例2:指向全局变量的指针数组
int* ptr_to_globals[2] = {&global_a, &global_b};
// 示例3:指向不同类型变量的指针数组
void* mixed_ptrs[3] = {&global_a, &global_pi, &global_ch};
// 示例4:指向数组的指针数组
int arr1[] = {1,2,3};
int arr2[] = {4,5,6};
int arr3[] = {7,8,9};
int* ptr_to_arrays[3] = {arr1, arr2, arr3};
// 示例5:函数指针数组(高级)
// int (*func_ptrs[3])(int, int);
// 示例6:指向指针的指针数组
int** ptr_to_ptrs[3];
ptr_to_ptrs[0] = &ptr_to_locals[0];
ptr_to_ptrs[1] = &ptr_to_locals[1];
ptr_to_ptrs[2] = &ptr_to_locals[2];
// 打印验证
printf("指向数组的指针数组:\n");
for(int i = 0; i < 3; i++) {
printf("ptr_to_arrays[%d] 指向数组首元素 %d\n",
i, ptr_to_arrays[i][0]);
}
return 0;
}
2. 复杂指针数组声明解析
c
#include <stdio.h>
int main() {
// 声明1:int* 数组
int* a[5]; // a是一个数组,包含5个int*元素
// 声明2:const int* 数组
const int* b[5]; // b是数组,元素指向const int
// 声明3:int* const 数组
int* const c[5] = {NULL}; // c是数组,元素是const指针(必须初始化)
// 声明4:指向char数组的指针数组
char* d[5]; // 常用作字符串数组
// 声明5:二维指针数组
int** e[5]; // e是数组,元素是int**
// 声明6:函数指针数组
int (*f[5])(int, int); // f是数组,元素是函数指针
printf("各种指针数组声明示例\n");
printf("sizeof(a) = %d (5个int*)\n", sizeof(a));
printf("sizeof(d) = %d (5个char*)\n", sizeof(d));
return 0;
}
九、指针数组模拟二维数组 - 深入剖析
1. 基本原理和内存布局
c
#include <stdio.h>
int main() {
// 三个一维数组
int row0[4] = {1, 2, 3, 4};
int row1[4] = {5, 6, 7, 8};
int row2[4] = {9, 10, 11, 12};
// 指针数组模拟二维数组
int* matrix[3] = {row0, row1, row2};
printf("内存布局分析:\n");
printf("matrix数组地址: %p\n", matrix);
for(int i = 0; i < 3; i++) {
printf("matrix[%d] = %p (指向row%d首地址)\n", i, matrix[i], i);
printf(" row%d实际地址: %p\n", i,
i==0 ? (void*)row0 : (i==1 ? (void*)row1 : (void*)row2));
}
printf("\n模拟二维数组访问:\n");
for(int i = 0; i < 3; i++) {
for(int j = 0; j < 4; j++) {
// 四种访问方式
printf("matrix[%d][%d] = ", i, j);
printf("%d ", matrix[i][j]); // 方式1
printf("%d ", *(matrix[i] + j)); // 方式2
printf("%d ", *(*(matrix + i) + j)); // 方式3
printf("\n");
}
}
return 0;
}
2. 与真实二维数组的对比
c
#include <stdio.h>
int main() {
// 真实二维数组(内存连续)
int real_2d[3][4] = {
{1,2,3,4},
{5,6,7,8},
{9,10,11,12}
};
// 指针数组模拟(内存可能不连续)
int row1[4] = {1,2,3,4};
int row2[4] = {5,6,7,8};
int row3[4] = {9,10,11,12};
int* simulated_2d[3] = {row1, row2, row3};
printf("=== 内存布局对比 ===\n");
printf("\n真实二维数组内存:\n");
for(int i = 0; i < 3; i++) {
for(int j = 0; j < 4; j++) {
printf("%p ", &real_2d[i][j]);
}
printf("\n");
}
printf("\n模拟二维数组内存:\n");
for(int i = 0; i < 3; i++) {
for(int j = 0; j < 4; j++) {
printf("%p ", &simulated_2d[i][j]);
}
printf("\n");
}
printf("\n=== 访问方式对比 ===\n");
printf("真实二维数组的大小: %d\n", sizeof(real_2d));
printf("模拟二维数组的大小: %d\n", sizeof(simulated_2d));
return 0;
}
3. 动态创建和操作
c
#include <stdio.h>
#include <stdlib.h>
// 动态创建指针数组模拟的二维矩阵
int** create_matrix(int rows, int cols) {
// 分配指针数组
int** matrix = (int**)malloc(rows * sizeof(int*));
if(matrix == NULL) return NULL;
// 为每一行分配空间
for(int i = 0; i < rows; i++) {
matrix[i] = (int*)malloc(cols * sizeof(int));
if(matrix[i] == NULL) {
// 分配失败,释放已分配的内存
for(int j = 0; j < i; j++) {
free(matrix[j]);
}
free(matrix);
return NULL;
}
}
return matrix;
}
// 初始化矩阵
void init_matrix(int** matrix, int rows, int cols) {
for(int i = 0; i < rows; i++) {
for(int j = 0; j < cols; j++) {
matrix[i][j] = i * cols + j + 1;
}
}
}
// 打印矩阵
void print_matrix(int** matrix, int rows, int cols) {
for(int i = 0; i < rows; i++) {
for(int j = 0; j < cols; j++) {
printf("%4d ", matrix[i][j]);
}
printf("\n");
}
}
// 释放矩阵
void free_matrix(int** matrix, int rows) {
for(int i = 0; i < rows; i++) {
free(matrix[i]);
}
free(matrix);
}
// 矩阵转置
int** transpose_matrix(int** matrix, int rows, int cols) {
int** result = create_matrix(cols, rows);
if(result == NULL) return NULL;
for(int i = 0; i < rows; i++) {
for(int j = 0; j < cols; j++) {
result[j][i] = matrix[i][j];
}
}
return result;
}
int main() {
int rows = 3, cols = 4;
// 创建矩阵
int** mat = create_matrix(rows, cols);
if(mat == NULL) {
printf("内存分配失败!\n");
return 1;
}
// 初始化并打印
init_matrix(mat, rows, cols);
printf("原始矩阵:\n");
print_matrix(mat, rows, cols);
// 转置
int** trans = transpose_matrix(mat, rows, cols);
printf("\n转置矩阵:\n");
print_matrix(trans, cols, rows);
// 释放内存
free_matrix(mat, rows);
free_matrix(trans, cols);
return 0;
}
十、字符指针变量 - 全方位解析
1. 字符指针的基础用法
c
#include <stdio.h>
#include <string.h>
int main() {
// 1. 指向单个字符
char ch = 'A';
char* pc = &ch;
printf("指向单个字符:\n");
printf("ch = %c\n", ch);
printf("*pc = %c\n", *pc);
*pc = 'B'; // 通过指针修改
printf("修改后 ch = %c\n", ch);
// 2. 指向字符串(字符数组)
char str[] = "Hello";
char* pstr = str; // 或 char* pstr = &str[0];
printf("\n指向字符数组:\n");
printf("str = %s\n", str);
printf("pstr = %s\n", pstr);
// 通过指针修改字符数组
pstr[0] = 'h';
printf("修改后 str = %s\n", str);
// 3. 指向常量字符串
const char* pconst = "World"; // 推荐加const
printf("\n指向常量字符串:\n");
printf("pconst = %s\n", pconst);
// pconst[0] = 'w'; // 错误!不能修改常量字符串
return 0;
}
2. 字符指针的运算
c
#include <stdio.h>
int main() {
const char* str = "Hello, World!";
const char* p = str;
printf("字符串: %s\n", str);
printf("逐个字符输出:\n");
// 使用指针遍历字符串
while(*p != '\0') {
printf("当前字符: %c, 地址: %p\n", *p, p);
p++; // 指针后移
}
printf("字符串长度: %d\n", p - str); // 指针减法得到长度
// 指针的比较
p = str;
const char* q = str + 7; // 指向'W'
if(p < q) {
printf("p 在 q 之前\n");
}
// 查找字符
while(*p != '\0' && *p != 'W') {
p++;
}
if(*p == 'W') {
printf("找到字符 'W' 在偏移 %d 位置\n", p - str);
}
return 0;
}
3. 字符指针数组 - 字符串数组
c
#include <stdio.h>
int main() {
// 方式1:使用字符指针数组存储字符串
const char* weekdays[] = {
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
"Sunday"
};
int num_days = sizeof(weekdays) / sizeof(weekdays[0]);
printf("一周七天:\n");
for(int i = 0; i < num_days; i++) {
printf("星期%d: %s (地址: %p)\n",
i+1, weekdays[i], weekdays[i]);
}
// 方式2:二维字符数组(内存连续)
char weekdays2[7][10] = {
"Monday", "Tuesday", "Wednesday",
"Thursday", "Friday", "Saturday", "Sunday"
};
printf("\n二维字符数组内存布局:\n");
for(int i = 0; i < 7; i++) {
printf("weekdays2[%d] = %s (地址: %p)\n",
i, weekdays2[i], weekdays2[i]);
}
// 对比两种方式的区别
printf("\n区别:\n");
printf("weekdays[0] 指向: %p\n", weekdays[0]);
printf("weekdays[1] 指向: %p\n", weekdays[1]);
printf("两个地址不一定连续\n");
printf("weekdays2[0] 地址: %p\n", weekdays2[0]);
printf("weekdays2[1] 地址: %p\n", weekdays2[1]);
printf("相差10字节(连续)\n");
return 0;
}
十一、不可以修改常量字符串的值 - 深入理解
1. 为什么不能修改
c
#include <stdio.h>
#include <string.h>
int main() {
// 情况1:字符数组 - 可修改
char arr[] = "Hello";
printf("字符数组(栈区):%s\n", arr);
arr[0] = 'h'; // 可以修改
printf("修改后:%s\n", arr);
// 情况2:常量字符串 - 不可修改
char* p = "Hello";
printf("\n常量字符串(只读数据区):%s\n", p);
// p[0] = 'h'; // 运行时错误!段错误
// 验证内存位置
printf("\n内存地址对比:\n");
printf("arr的地址: %p\n", arr);
printf("p指向的地址: %p\n", p);
// 情况3:使用const保护
const char* cp = "Hello";
// cp[0] = 'h'; // 编译错误
// 情况4:动态分配内存 - 可修改
char* heap_str = (char*)malloc(10);
strcpy(heap_str, "Hello");
printf("\n堆区字符串:%s\n", heap_str);
heap_str[0] = 'h'; // 可以修改
printf("修改后:%s\n", heap_str);
free(heap_str);
return 0;
}
2. 内存分区详解
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// 全局变量区
char global_arr[] = "Global"; // 数据区
const char* global_ptr = "GlobalConst"; // 指针在数据区,指向只读数据区
int main() {
// 栈区
char stack_arr[] = "Stack";
// 只读数据区
const char* rodata_ptr = "ReadOnly";
// 堆区
char* heap_str = (char*)malloc(20);
strcpy(heap_str, "Heap");
printf("=== 不同内存区域的字符串 ===\n\n");
printf("栈区字符串:\n");
printf(" 地址: %p\n", stack_arr);
printf(" 内容: %s\n", stack_arr);
printf(" 可修改: %s\n\n", "是");
printf("全局数据区字符串:\n");
printf(" 地址: %p\n", global_arr);
printf(" 内容: %s\n", global_arr);
printf(" 可修改: %s\n\n", "是");
printf("只读数据区字符串:\n");
printf(" 地址: %p\n", rodata_ptr);
printf(" 内容: %s\n", rodata_ptr);
printf(" 可修改: %s\n\n", "否");
printf("堆区字符串:\n");
printf(" 地址: %p\n", heap_str);
printf(" 内容: %s\n", heap_str);
printf(" 可修改: %s\n", "是");
free(heap_str);
return 0;
}
十二、《剑指offer》字符串笔试题 - 详细解析
1. 原题解析
c
#include <stdio.h>
#include <string.h>
int main() {
char str1[] = "hello bit.";
char str2[] = "hello bit.";
const char *str3 = "hello bit.";
const char *str4 = "hello bit.";
printf("=== 地址比较 ===\n");
printf("str1 = %p\n", str1);
printf("str2 = %p\n", str2);
printf("str3 = %p\n", str3);
printf("str4 = %p\n\n", str4);
printf("=== 内存布局分析 ===\n");
printf("str1数组位于栈区:\n");
for(int i = 0; i <= strlen(str1); i++) {
printf(" %p: '%c' (%d)\n", &str1[i], str1[i], str1[i]);
}
printf("\nstr2数组位于栈区(不同位置):\n");
for(int i = 0; i <= strlen(str2); i++) {
printf(" %p: '%c' (%d)\n", &str2[i], str2[i], str2[i]);
}
printf("\nstr3指向只读数据区:\n");
printf(" str3 = %p\n", str3);
printf(" str3指向的内容:%s\n", str3);
if(str1 == str2)
printf("\nstr1 and str2 are same\n");
else
printf("\nstr1 and str2 are not same\n"); // 输出这一行
if(str3 == str4)
printf("str3 and str4 are same\n"); // 输出这一行
else
printf("str3 and str4 are not same\n");
return 0;
}
2. 深入扩展
c
#include <stdio.h>
#include <string.h>
// 验证字符串常量池
void test_string_pool() {
const char* s1 = "Hello";
const char* s2 = "Hello";
const char* s3 = "World";
char arr1[] = "Hello";
char arr2[] = "Hello";
printf("=== 字符串常量池测试 ===\n");
printf("s1 = %p\n", s1);
printf("s2 = %p\n", s2);
printf("s3 = %p\n", s3);
printf("arr1 = %p\n", arr1);
printf("arr2 = %p\n\n", arr2);
if(s1 == s2) {
printf("s1和s2指向同一块内存(常量池优化)\n");
}
if(arr1 != arr2) {
printf("arr1和arr2是不同的数组(栈上不同位置)\n");
}
}
// 字符串比较的正确方式
void correct_comparison() {
char str1[] = "hello";
char str2[] = "hello";
const char* str3 = "hello";
const char* str4 = "hello";
printf("\n=== 正确比较字符串内容 ===\n");
// 错误:比较地址
printf("错误比较(比较地址):\n");
if(str1 == str2) {
printf("str1 == str2\n");
} else {
printf("str1 != str2\n");
}
// 正确:使用strcmp比较内容
printf("正确比较(使用strcmp):\n");
if(strcmp(str1, str2) == 0) {
printf("str1 和 str2 内容相同\n");
}
if(strcmp(str3, str4) == 0) {
printf("str3 和 str4 内容相同\n");
}
}
int main() {
test_string_pool();
correct_comparison();
return 0;
}
十三、数组指针 - 完整精讲
1. 数组指针的基本概念
c
#include <stdio.h>
int main() {
// 1. 定义数组指针的语法
int arr[5] = {1, 2, 3, 4, 5};
// int* p = arr; // 指向首元素
int (*pa)[5] = &arr; // 指向整个数组
printf("arr = %p\n", arr);
printf("&arr = %p\n", &arr);
printf("pa = %p\n", pa);
// 2. 使用数组指针访问元素
printf("\n使用数组指针访问:\n");
for(int i = 0; i < 5; i++) {
printf("(*pa)[%d] = %d\n", i, (*pa)[i]);
}
// 3. 数组指针的运算
printf("\n数组指针运算:\n");
printf("pa = %p\n", pa);
printf("pa + 1 = %p (跳过一个数组)\n", pa + 1);
// 4. 对比普通指针
int* p = arr;
printf("\n普通指针运算:\n");
printf("p = %p\n", p);
printf("p + 1 = %p (跳过一个元素)\n", p + 1);
return 0;
}
2. 数组指针的声明解析
c
#include <stdio.h>
int main() {
// 声明解析练习
int a[5]; // a: 包含5个int的数组
int* b[5]; // b: 包含5个int*的数组(指针数组)
int (*c)[5]; // c: 指向包含5个int的数组的指针(数组指针)
int (*d[5])[3]; // d: 包含5个元素的数组,每个元素是指向包含3个int的数组的指针
printf("各种声明的大小:\n");
printf("sizeof(a) = %d\n", sizeof(a)); // 20
printf("sizeof(b) = %d\n", sizeof(b)); // 20 (5个指针)
printf("sizeof(c) = %d\n", sizeof(c)); // 4 (一个指针)
printf("sizeof(d) = %d\n", sizeof(d)); // 20 (5个指针)
// 复杂声明的解析技巧:右左法则
// 1. int (*c)[5] -- c是指针,指向int[5]
// 2. int *d[5] -- d是数组,元素是int*
// 3. int (*e[5])[3] -- e是数组,元素是指针,指向int[3]
return 0;
}
3. 数组指针的应用场景
c
#include <stdio.h>
// 场景1:二维数组传参
void print_2d_array(int (*arr)[4], int rows) {
for(int i = 0; i < rows; i++) {
for(int j = 0; j < 4; j++) {
printf("%d ", arr[i][j]); // arr[i][j] 等价于 *(*(arr+i)+j)
}
printf("\n");
}
}
// 场景2:返回数组指针
int (*create_array())[5] {
static int arr[5] = {1,2,3,4,5};
return &arr;
}
// 场景3:数组指针数组
void demonstrate_array_of_array_pointers() {
int arr1[3] = {1,2,3};
int arr2[3] = {4,5,6};
int arr3[3] = {7,8,9};
// 数组指针数组:每个元素指向一个包含3个int的数组
int (*arr_ptrs[3])[3] = {&arr1, &arr2, &arr3};
for(int i = 0; i < 3; i++) {
for(int j = 0; j < 3; j++) {
printf("%d ", (*arr_ptrs[i])[j]);
}
printf("\n");
}
}
// 场景4:动态分配二维数组
void dynamic_2d_array() {
int (*matrix)[4] = malloc(3 * sizeof(int[4])); // 分配3行4列的二维数组
if(matrix != NULL) {
// 初始化
for(int i = 0; i < 3; i++) {
for(int j = 0; j < 4; j++) {
matrix[i][j] = i * 4 + j + 1;
}
}
// 打印
printf("动态分配的二维数组:\n");
for(int i = 0; i < 3; i++) {
for(int j = 0; j < 4; j++) {
printf("%3d ", matrix[i][j]);
}
printf("\n");
}
free(matrix);
}
}
int main() {
// 场景1测试
int arr[3][4] = {
{1,2,3,4},
{5,6,7,8},
{9,10,11,12}
};
printf("场景1:二维数组传参\n");
print_2d_array(arr, 3);
// 场景2测试
printf("\n场景2:返回数组指针\n");
int (*p)[5] = create_array();
for(int i = 0; i < 5; i++) {
printf("%d ", (*p)[i]);
}
printf("\n");
// 场景3测试
printf("\n场景3:数组指针数组\n");
demonstrate_array_of_array_pointers();
// 场景4测试
printf("\n场景4:动态分配\n");
dynamic_2d_array();
return 0;
}
4. 数组指针与指针数组对比总结
c
#include <stdio.h>
int main() {
// 指针数组 vs 数组指针 全面对比
// 定义数据
int arr[3][4] = {
{1,2,3,4},
{5,6,7,8},
{9,10,11,12}
};
// 1. 指针数组:存放每一行的首地址
int* parr[3] = {arr[0], arr[1], arr[2]};
// 2. 数组指针:指向二维数组的一行
int (*parr2)[4] = arr; // 指向第一行
printf("=== 指针数组 vs 数组指针 ===\n\n");
printf("指针数组 parr[3]:\n");
printf(" parr类型: %s\n", "int* [3]");
printf(" sizeof(parr) = %d\n", sizeof(parr));
printf(" 访问方式: parr[i][j]\n");
for(int i = 0; i < 3; i++) {
for(int j = 0; j < 4; j++) {
printf("%d ", parr[i][j]);
}
printf("\n");
}
printf("\n数组指针 parr2:\n");
printf(" parr2类型: %s\n", "int (*)[4]");
printf(" sizeof(parr2) = %d\n", sizeof(parr2));
printf(" 访问方式: parr2[i][j] 或 (*(parr2+i))[j]\n");
for(int i = 0; i < 3; i++) {
for(int j = 0; j < 4; j++) {
printf("%d ", parr2[i][j]);
}
printf("\n");
}
// 指针运算对比
printf("\n指针运算对比:\n");
printf("parr + 1 = %p (指向parr[1])\n", parr + 1);
printf("parr2 + 1 = %p (指向arr[1])\n", parr2 + 1);
return 0;
}
总结:指针核心概念速查表
| 概念 | 定义格式 | 本质 | 内存大小(x86) | 典型用法 |
|---|---|---|---|---|
| 普通指针 | int* p |
指向int变量 | 4字节 | p = &a |
| 数组名 | int arr[5] |
首元素地址 | 20字节(整个数组) | arr[0] |
| 指针数组 | int* p[5] |
数组,元素是指针 | 20字节(5个指针) | p[0] = &a |
| 数组指针 | int (*p)[5] |
指针,指向数组 | 4字节 | p = &arr |
| 二级指针 | int** p |
指向指针的指针 | 4字节 | p = &pa |
| 字符指针 | char* p |
指向字符或字符串 | 4字节 | p = "hello" |
| 函数指针 | int (*p)(int) |
指向函数 | 4字节 | p = &func |