Bootstrap

C语言中sizeof操作符与strlen函数的辨析

引言:

本文围绕sizeof操作符与strlen函数展开,首先列出两者之间的区别,然后介绍了strlen函数的三种实现方法即递归,创建计数变量,指针相减,后分析练习打下基础,最后给出了一系列辨析二者区别的代码,并给出了详细的解释,以帮助我们加强对着二者区别的理解,然后总结了对于二者辨析的心得。

目录

引言:

sizeof与strlen的区别:

strlen函数的三种实现方法

实现效果:

第一种创建计数变量:

第二种使用指针相减:

第三种使用函数递归:

代码概览

辨析代码练习

热身组(基础回顾):

答案:

解释:

常规组(一维数组):

解释1:

答案2:

解释2:

答案3:

解释3:

答案4:

解释4:

超级组(二维数组)

答案:

解释:

总结:


sizeof与strlen的区别:

  • sizeof是操作符,strlen是库函数,使用头文件<string.h>
  • sizeof更关心的是操作数所占内存的大小,strlen是求字符串长度,统计的是'\0'前的字符个数
  • sizeof不在乎内存中存放的数据,所以sizeof(int a+int b)不论括号内的值为多少,总是返回4,即sizeof中的表达式不计算,strlen关注内存中是否有‘\0’,如果没有'\0"就会一直向后寻找,从而出现越界。

strlen函数的三种实现方法

值得注意的几个易忽略点的是,strlen函数的返回值是size_t类型,即没有符号,这一点在后文中的辨析代码练习中会用到,同时strlen函数的参数是所求字符串的地址即const char*。以下是具体实现。

实现效果:

第一种创建计数变量:

assert断言防止所传p为空指针,同时p形参我们不希望所指向的值被修改,所以加const进行修饰。循环部分涉及指针加减一个整数跳过的大小由所指向的类型限制,最后count与函数返回的类型不符合,自动进行强制类型转化。

第二种使用指针相减:

涉及指针相减得到指针之间相差的元素个数

第三种使用函数递归:

函数的结束标志是p解引用为'\0','\0'的ASCII码值为0,取反后为真,否则进行函数递归,但应该注意栈溢出。

代码概览

//strlen函数的三种实现方法 
#include<stdio.h>
#include<string.h>
#include<assert.h>


//计算变量
size_t j_strlen(const char* p)
{
	assert(p);
	int count = 0;
	while (*(p+count))
	{
		count++;
	}
	return count;
}

//递归实现
size_t d_strlen(const char* p)
{
	assert(p);
	if (!*p)
	{
		return 0;
	}
	else
	{
		return 1 + d_strlen(p + 1);
	}
}

//指针实现
size_t z_strlen(const char* p)
{
	assert(p);
	char* p1 = p;
	while (*++p1)
		;
	return(p1 - p);

}

int main()
{
	char arr[] = "上帝的归上帝,凯撒的归凯撒";
	printf("计算变量:%zd\n", j_strlen(arr));
	printf("递归实现:%zd\n", d_strlen(arr));
	printf("指针实现:%zd\n", z_strlen(arr));
	printf("原库函数:%zd\n", strlen(arr));


	return 0;
}

辨析代码练习

本节为了加深对sizeof操作符和strlen函数的理解列举了大量代码练习,并且逐一个给出解释。

热身组(基础回顾):

  • 9行没有'\0'返回随机值,10.12.13均返回3

(使用%zd打印更严谨)

答案:

1~5:16,4/8,4,4/8,4

6~10:4/8,16,4/8,4/8,4/8

解释:

  1. 该处a单独放在sizeof中,代表该数组的整个地址,计算的是整个数组在内存中占的大小。值得注意的是,该处是两个特例中的一个,当数组名单独放在sizeof操作符中时,返回的是该数组整体在内存中所占的大小,当&加数组名时,访问的是整个数组的地址。所以,4个元素乘以int类型大小4,结果为16.
  2. 此处数组名未单独放在sizeof操作符中,所以表示数组首元素的地址,是地址而不知道环境是x86还是x64,所以答案是4个或者8个字节。
  3. 该处为数组首元素地址解引用,得到的是int类型的数字1,所以结果为4.
  4. 该处为数组的第二个元素的地址,同2,为4/8。
  5. 该处表示数组的第二个元素,大小为int,返回4 .
  6. 此处为对数组首元素地址取地址,是二级指针,仍为4/8。
  7. 此处有两种解释方法,解引用和取地址是相反的过程,相互抵消,相当于数组名a单独放在sizeof操作符中,返回的是整个数组在内存中的大小,结果为16.
  8. 此处&加数组名返回的是整个数组的地址,是一个int(*)[4]类型的指针,加1,跳过16个字节,指向的是未知指针,但是仍为指针,所以答案是4/8
  9. 最后两个分别为第一个元素的地址,第二个元素的地址。所以答案是4/8。

常规组(一维数组):

(使用%zd打印更严谨)
答案1:

1~5:6,4/8,1,1,4/8

6~7:4/8,4/8

解释1:

  1. arr单独放在sizeof操作符中,访问的是整个数组的地址,返回的是整个数组的大小,6个元素乘以char类型大小一个字节,答案为6.
  2. arr不是单独放置,代表的是首元素的地址,地址的大小同前,为4/8。
  3. arr为单独放在sizeof操作符中,代表的是首元素的地址,解引用,为首元素,所以结果为1
  4. 代表的是第二个元素,大小为1。
  5. 后三个分别为整个元素的地址,跳过该整个数组指针后的未知指针,数组第二个元素的地址。

(使用%zd打印更严谨)

答案2:

1~5:烫,烫,error,error,烫

6~7:烫,烫

解释2:

  1. arr代表首元素的地址,而arr中没有'\0'会越界访问。
  2. 仍是首元素的地址,同上
  3. 为数组首元素,但是strlen函数的参数是const char*类型的指针,所以程序报错。
  4. 为数组第二个元素,同上。
  5. 该处取的是整个元素的地址,同1。
  6. 该处是跳过整个数组后的未知指针。
  7. 该处为数组第二个元素的指针,同1.

(使用%zd打印更严谨)

答案3:

1~5:7,4/8,1,1,4/8

6~7:4/8,4/8

解释3:

  1. arr单独放在sizeof操作符中,计算的是整个数组在内存中所占的大小,但是该处容易忽略\0的存在,错误的认为是6。
  2. arr未单独放在sizeof操作符中,表示的是数组首元素的地址,是地址所以答案是4/8。
  3. arr未单独放在sizeof操作符中,表示数组首元素的地址,解引用结果为'a',返回1个字节。
  4. 该处为数组第二个元素,返回1个字节
  5. &加数组名,访问的是整个数组的地址,是地址答案为4/8,后两个同理为未知数组和数组第二个元素的地址。故可总结,因为sizeof更关心的是对象的数据类型,所以只要看到&并且没有*的出现,基本上可以判定结果为4/8。

(使用%zd打印更严谨)

答案4:

1~5:6,6,error,error,6

6~7:随机值,5

解释4:

  1. 该处为常规的strlen求字符串长度,'\0'前面有6个字符。

  2. 数组名代表首元素的地址,加0不变。

  3. 首元素地址解引用,得到首元素,不是指针,报错。

  4. 是首元素,不是地址,报错。

  5. &加数组名取出的是整个数组的地址,,但在strlen函数只关注'\0'前的字符数,所以仍然得到6。

  6. 该处是跳过整个数组的未知指针,所以是随机值

  7. 传入的是第二个元素的地址,从第二个元素开始计算,得到5
     

超级组(二维数组)

(使用%zd打印更严谨)

答案:

1~6:48,4,16,4/8,4,4/8

7~11:16,4/8,16,16,16

解释:

逐个解释之前,复习arr [ i ] [ j ] 等效于*( * ( arr + i ) + j ),二维数组的数组名指向的是一维数组的数组名,即是一维数组的首元素的地址的地址。一维数组的数组名,指向的是数组首元素。二维数组相当于,把其他数组的数组名作为自己的元素,这些元素是地址,解引用一次,变为所储存的数组的数组名,即一维数组首元素的地址。

  1. 二维数组的数组名单独放在sizeof操作符内部,求的是整个数组在内存中所占的大小,3*4*4=48
  2. 相当于解引用两次(这么称述方便后面理解),求的是单个整型的大小,为4
  3. 解引用一次,是第一行的数组名,即第一行的的数组名单独放在sizeof操作符中,求的是整个数组占内存空间的大小
  4. 该处表示第一行的第二个元素的地址,相当于( * ( arr + 0 ) + 1 )
  5. 相当于*( * ( arr + 0 ) + 1 ),二维数组储存的第一个数组的地址解引用,得到第一个数组的首元素的地址,即第一个数组的数组名,后加1再解引用,得到int类型的数字内容。(这里容易在理解第四点后,被误导,应该认识到第四点是属于特例的)
  6. 没有进行解引用或者数组名单独放在sizeof操作符中的情况,该处代表被存储数组的数组名的地址加一,得到第二个数组数组名的地址,是地址
  7. 该处为第二个数组的数组名的地址解引用,得到第二个数组的数组名,数组命单独放在sizeof操作符中,求的是该数组在内存中占的总大小.
  8. a [ 0 ]相当于解引用一次,地址变到首个被储存的数组的数组名,而后&,相当于抵消了*的作用,仍然是地址,而后加一,表示的是第二个被存储的数组的数组名的地址,是地址。
  9. 对第二个被存储的数组的数组名的地址解引用,得到第二个被储存的数组的数组名,满足数组名单独放在sizeof操作符中,求的是整个数组占内存空间的大小。
  10. a未单独放在 sizeof操作符中,表示的是二维数组首元素的地址,即第一个被储存数组的数组名的地址,解引用为第一个数组的数组名,数组名单独放在sizeof操作符中,求的是整个数组在内存空间的大小。
  11. a [ 3 ]乍一眼看存在越界访问,但是sizeof不关心内存数据的内容,就像之前说的,放进sizeof的表达式不会计算,两个int类型的数据相加,不管结果是否为4,都返回4。这里只认是数组名的地址解引用,结果是数组名,并且是a中所存数组的数组名,结果为16.

总结:

  • 对于sizeof操作符,我们更应该关注其所求对象的数据类型,比如只要看到&而且没有*,大部分情况下因为都是求地址,所以结果都是4/8。同时我们要牢记两个特例,当数组名单独放入sizeof操作符的时候,得出的是整个数组在内存中所占的大小,&加数组名取出的是整个数组的地址,+1跳过整个数组。
  • 对于strlen函数,我们要清楚,它的参数是const chat*,是一个字符指针,牢记返回值是size_t类型的值,没有符号,同时没有'\0'可能会越界访问,输出烫。
  • 二维数组的首元素是第一个数组的数组名的地址,及二维数组的数组名是二级指针,最高可为三级指针(即所存的一维数组中存的是常量字符串)
  • 注意二维数组解引用的次数,牢记二维数组的数组名解引用得到的是一维数组的数组名,换句话说,二维数组存的是一维数组数组名的地址,而一维数组的数组名,解引用得到的是首元素。

幸甚至哉,歌以咏志。

;