指针变量的大小取决于系统架构,而非指针所指向的数据类型。这个特性是C语言指针最基础但也最容易让人困惑的概念之一。
让我们通过一个具体示例来理解这个特性:
c复制#include<stdio.h>
int main()
{
int a = 10;
int* pa = &a; // 整型指针
char* pc = &a; // 字符指针,虽然指向的是int但类型为char*
printf("%d\n", sizeof(pa)); // 输出指针变量pa的大小
printf("%d\n", sizeof(pc)); // 输出指针变量pc的大小
printf("pa = %p\n", pa); // 输出pa的值(地址)
printf("pa+1 = %p\n", pa+1); // 指针算术:pa+1
printf("pc = %p\n", pc); // 输出pc的值(地址)
printf("pc+1 = %p\n", pc+1); // 指针算术:pc+1
return 0;
}
这段代码的输出结果会因系统架构不同而有所差异:
关键点:指针变量的大小只与系统架构有关,与指针类型无关。无论是指向int、char还是其他类型的指针,在同一个平台下大小都相同。
虽然指针变量的大小与类型无关,但指针运算却与类型密切相关。观察上面代码中pa+1和pc+1的输出:
pa+1会跳过sizeof(int)个字节(通常是4字节)pc+1会跳过sizeof(char)个字节(1字节)这就是为什么虽然pa和pc都指向同一个变量a,但pa+1和pc+1的地址增量却不同。这种特性使得指针运算能够自动适应所指向数据类型的大小。
数组名在C语言中有着特殊的含义,理解这一点对掌握数组和指针的关系至关重要。
在大多数情况下,数组名会被编译器解释为指向数组首元素的指针:
c复制int arr[5] = {1, 2, 3, 4, 5};
int *p = arr; // 等价于 int *p = &arr[0];
这种情况下,arr和&arr[0]是完全等价的,都表示数组第一个元素的地址。
然而,数组名并不总是等同于首元素地址,有两种例外情况:
当数组名作为sizeof的操作数时,它表示整个数组的大小:
c复制int arr[5] = {1, 2, 3, 4, 5};
printf("%zu\n", sizeof(arr)); // 输出20(假设int为4字节)
这与指针的大小形成鲜明对比:
c复制int *p = arr;
printf("%zu\n", sizeof(p)); // 输出8(64位系统)
取数组名的地址时,得到的是指向整个数组的指针,而非指向首元素的指针:
c复制printf("%p\n", arr); // 类型为int*,指向第一个元素
printf("%p\n", &arr); // 类型为int(*)[5],指向整个数组
虽然这两个地址的数值相同,但它们的类型不同,这在进行指针运算时会体现出来:
c复制printf("%p\n", arr + 1); // 地址增加4(一个int的大小)
printf("%p\n", &arr + 1); // 地址增加20(整个数组的大小)
指针和数组在C语言中有着密不可分的关系,理解这一点可以写出更高效的代码。
以下代码展示了如何使用指针访问数组元素:
c复制#include <stdio.h>
int main()
{
int arr[10] = {0};
int sz = sizeof(arr)/sizeof(arr[0]);
// 使用指针输入
int *p = arr;
for(int i=0; i<sz; i++) {
scanf("%d", p+i); // 等价于&arr[i]
}
// 使用指针输出
for(int i=0; i<sz; i++) {
printf("%d ", *(p+i)); // 等价于arr[i]
}
return 0;
}
编译器在处理数组下标访问时,实际上会将其转换为指针运算:
c复制arr[i] ≡ *(arr + i)
当数组作为函数参数传递时,实际上传递的是数组首元素的地址:
c复制void printArray(int arr[], int size) {
// 这里的arr实际上是一个指针
for(int i=0; i<size; i++) {
printf("%d ", arr[i]);
}
}
// 调用方式
int main() {
int arr[5] = {1, 2, 3, 4, 5};
printArray(arr, 5);
return 0;
}
需要注意的是,在函数内部无法通过sizeof获取数组的实际大小,因为arr已经退化为指针:
c复制void printSize(int arr[]) {
printf("%zu\n", sizeof(arr)); // 输出指针大小,而非数组大小
}
冒泡排序是理解数组和指针关系的绝佳案例,下面我们深入分析其实现和优化。
c复制void bubble_sort(int arr[], int sz) {
for(int i=0; i<sz-1; i++) { // 外层循环控制趟数
for(int j=0; j<sz-i-1; j++) { // 内层循环进行比较
if(arr[j] > arr[j+1]) { // 相邻元素比较
// 交换元素
int tmp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = tmp;
}
}
}
}
算法特点:
通过引入标志变量,可以在数组已经有序时提前终止排序:
c复制void optimized_bubble_sort(int arr[], int sz) {
for(int i=0; i<sz-1; i++) {
int flag = 1; // 假设已经有序
for(int j=0; j<sz-i-1; j++) {
if(arr[j] > arr[j+1]) {
flag = 0; // 发生交换,说明无序
int tmp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = tmp;
}
}
if(flag) break; // 如果一趟没有交换,提前结束
}
}
优化点:
以数组{3,1,7,5,8}为例:
第一趟:
第二趟:
这两个概念经常被混淆,但它们完全不同:
c复制int *p1[5]; // 指针数组:包含5个int指针的数组
int (*p2)[5]; // 数组指针:指向包含5个int的数组的指针
对于二维数组,可以使用指针数组或数组指针来表示:
c复制int arr[3][4];
int (*p)[4] = arr; // 数组指针,指向包含4个int的数组
// 访问arr[i][j]可以写成
p[i][j] ≡ *(*(p + i) + j)
指针运算实际上是基于所指向类型的大小进行的:
c复制int *p = ...;
p + n ≡ (char*)p + n * sizeof(int)
这也是为什么不同类型的指针运算结果不同。
c复制int arr[5] = {1, 2, 3, 4, 5};
int *p = arr;
printf("%d", p[5]); // 越界访问!
解决方案:
c复制void printSize(int arr[]) {
// 错误:这里arr是指针,不是数组
printf("%zu\n", sizeof(arr));
}
正确做法:
c复制int a = 10;
char *p = &a; // 警告:指针类型不匹配
正确做法:
多次解引用同一指针会影响性能:
c复制// 不佳的实现
for(int i=0; i<n; i++) {
sum += *p++;
}
// 更好的实现
int *end = p + n;
while(p < end) {
sum += *p++;
}
在性能关键代码中,指针运算通常比数组下标更快:
c复制// 传统方式
for(int i=0; i<n; i++) {
arr[i] = 0;
}
// 指针优化方式
int *p = arr;
int *end = p + n;
while(p < end) {
*p++ = 0;
}
复杂的指针表达式可能影响可读性和性能:
c复制// 不易理解的代码
*(*(array + i) + j) = value;
// 更清晰的写法
array[i][j] = value;
C语言中字符串本质是字符数组,常用指针操作:
c复制char str[] = "Hello";
char *p = str;
while(*p) {
putchar(*p++);
}
指针与malloc/free配合实现动态内存管理:
c复制int *arr = malloc(10 * sizeof(int));
if(arr) {
// 使用指针操作动态数组
for(int i=0; i<10; i++) {
arr[i] = i;
}
free(arr);
}
通过指针数组实现多态行为:
c复制void func1() { printf("Function 1\n"); }
void func2() { printf("Function 2\n"); }
int main() {
void (*funcs[2])() = {func1, func2};
for(int i=0; i<2; i++) {
funcs[i]();
}
return 0;
}
c复制int a = 10;
int *p = &a;
printf("指针p的值:%p\n", (void*)p);
printf("指针p指向的值:%d\n", *p);
c复制if(p == NULL) {
printf("指针为空\n");
} else if(p == (void*)0xBADF00D) {
printf("检测到特定错误值\n");
}
GDB常用命令:
code复制print p # 打印指针值
print *p # 打印指针指向的值
x/4x p # 以16进制查看指针指向的内存
c复制int add(int a, int b) { return a + b; }
int (*funcPtr)(int, int) = add;
printf("%d\n", funcPtr(2, 3)); // 输出5
理解复杂指针声明的技巧:从内向外,从右向左:
c复制int *(*(*fp)(int))[10];
// fp是指针,指向函数,函数接受int参数,返回指针,该指针指向包含10个int指针的数组
C99引入的restrict关键字可以优化指针操作:
c复制void copy(int *restrict dest, const int *restrict src, size_t n) {
while(n--) {
*dest++ = *src++;
}
}
这个关键字告诉编译器这两个指针不会指向重叠的内存区域,允许更激进的优化。