在C/C++开发中,指针操作堪称程序员的基本功,而sizeof和strlen这两个看似简单的操作却成为困扰无数初学者的"拦路虎"。要真正掌握它们,我们需要从计算机内存的底层视角来理解其工作原理。
当我们在程序中声明一个变量时,系统会在内存中为其分配一块连续的空间。以32位系统为例,指针变量本身占用4字节内存空间,存储的是目标数据的内存地址。理解这一点至关重要,因为指针的所有操作本质上都是对内存地址的计算。
内存中的数据存储有几个关键特性:
例如,对于以下声明:
c复制int arr[5] = {1, 2, 3, 4, 5};
在内存中的布局大致如下(假设起始地址为0x1000):
code复制地址 值
0x1000: 1
0x1004: 2
0x1008: 3
0x100C: 4
0x1010: 5
sizeof是C/C++中的一个运算符(注意不是函数),它的独特之处在于:
关键点在于sizeof的计算不涉及运行时内存访问。编译器根据变量的类型信息就能确定其大小。例如:
c复制int a = 10;
printf("%zu\n", sizeof(a)); // 输出4(在32位系统中)
即使a的值是10,sizeof关心的只是int类型的大小。
与sizeof不同,strlen是一个库函数,它的工作方式是:
这意味着strlen必须实际访问内存内容,且结果取决于内存中的具体数据。例如:
c复制char str[] = "hello";
printf("%zu\n", strlen(str)); // 输出5
这里strlen会从'h'的地址开始,依次检查直到找到'\0'。
重要提示:strlen的参数必须是以'\0'结尾的有效字符串地址,否则会导致未定义行为(通常是内存越界访问)。
让我们通过具体案例来深入理解这些概念。首先看一个典型的一维数组例子:
c复制int a[] = {1, 2, 3, 4};
printf("%zu\n", sizeof(a)); // 16(整个数组大小)
printf("%zu\n", sizeof(a+0)); // 4或8(指针大小)
printf("%zu\n", sizeof(*a)); // 4(第一个元素大小)
这里的关键区别在于数组名在表达式中的含义:
字符数组因为可以表示字符串,情况更为复杂:
c复制char arr1[] = {'a', 'b', 'c'};
char arr2[] = "abc";
printf("%zu\n", sizeof(arr1)); // 3
printf("%zu\n", sizeof(arr2)); // 4(包含'\0')
printf("%zu\n", strlen(arr1)); // 未定义行为(缺少'\0')
printf("%zu\n", strlen(arr2)); // 3
这里arr1是普通的字符数组,而arr2是字符串字面量初始化,自动添加了'\0'。使用strlen时,arr1因为没有终止符会导致未定义行为。
初学者常混淆指针和数组名,虽然它们有时可以互换使用,但本质不同:
c复制char *p = "hello";
char arr[] = "hello";
printf("%zu\n", sizeof(p)); // 4或8(指针大小)
printf("%zu\n", sizeof(arr)); // 6(数组大小)
printf("%zu\n", strlen(p)); // 5
printf("%zu\n", strlen(arr)); // 5
虽然strlen的结果相同,但sizeof的结果差异明显,因为p是指针而arr是数组。
理解二维数组对于掌握指针运算至关重要。在内存中,二维数组实际上是"数组的数组",按行优先顺序连续存储:
c复制int a[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
内存布局(假设int为4字节):
code复制地址 值
0x1000: 1
0x1004: 2
...
0x102C: 12
对于二维数组,指针运算变得更加复杂:
c复制printf("%zu\n", sizeof(a)); // 48(3×4×4)
printf("%zu\n", sizeof(a[0])); // 16(一行的大小)
printf("%zu\n", sizeof(a[0]+1)); // 4或8(指针运算)
这里a[0]单独出现在sizeof中表示第一行数组,而a[0]+1则退化为指针运算。
这两个概念经常被混淆:
c复制int (*p)[4]; // 指向含有4个int的数组的指针
c复制int *p[4]; // 含有4个int指针的数组
理解它们的区别对于正确使用sizeof和strlen至关重要。
c复制int a[5] = {1, 2, 3, 4, 5};
int *ptr = (int *)(&a + 1);
printf("%d\n", *(ptr - 1)); // 输出5
解析:
c复制struct Test {
int Num;
char *pcName;
short sDate;
char cha[2];
short sBa[4];
} *p = (struct Test*)0x100000;
printf("%p\n", p + 0x1); // 0x100014(结构体大小20字节)
解析:
c复制char *c[] = {"ENTER","NEW","POINT","FIRST"};
char **cp[] = {c+3,c+2,c+1,c};
char ***cpp = cp;
printf("%s\n", **++cpp); // 输出"POINT"
解析:
c复制int arr[10];
size_t count = sizeof(arr) / sizeof(arr[0]); // 正确获取元素数
c复制char *p = "hello";
printf("%zu\n", sizeof(p)); // 输出指针大小,不是字符串长度
c复制char str[5] = "hello"; // 没有空间存放'\0',strlen不安全
c复制int num = 123;
printf("%zu\n", strlen((char*)&num)); // 危险!
c复制int *p;
p + 1; // 实际地址增加sizeof(int)
c复制int arr[5];
int *p = arr + 5; // 合法指针,但*p是未定义行为
由于sizeof在编译时确定,不会带来运行时开销。合理利用可以优化代码:
c复制// 避免硬编码
memcpy(dest, src, sizeof(*dest) * count);
strlen需要遍历整个字符串,时间复杂度O(n)。在性能敏感场景应避免重复调用:
c复制// 不好的做法
for (int i = 0; i < strlen(s); i++) {...}
// 优化方案
size_t len = strlen(s);
for (int i = 0; i < len; i++) {...}
理解内存布局有助于编写缓存友好的代码:
c复制// 按行访问二维数组(缓存友好)
for (int i = 0; i < rows; i++)
for (int j = 0; j < cols; j++)
arr[i][j] = ...;
// 按列访问(缓存不友好)
for (int j = 0; j < cols; j++)
for (int i = 0; i < rows; i++)
arr[i][j] = ...;
C/C++的类型系统要求通过适当类型的指针访问对象:
c复制int a = 0x12345678;
char *p = (char*)&a;
printf("%x\n", *p); // 合法:通过char*访问int
但以下行为是未定义的:
c复制float f = 1.0;
int *p = (int*)&f; // 违反严格别名规则
printf("%d\n", *p);
进行指针类型转换时需要格外小心:
c复制int arr[4] = {1, 2, 3, 4};
short *p = (short*)arr;
printf("%d\n", p[1]); // 输出什么?取决于字节序
在C++中,可以考虑使用更安全的替代方案:
cpp复制std::array<int, 5> arr = {1, 2, 3, 4, 5};
std::cout << arr.size(); // 安全获取元素数量