指针的进化—sizeof和strlen对比(字符串和字符数组的区分)

news/2025/2/2 22:27:24 标签: c语言, visual studio, 学习方法, 笔记

1.前言

        如果你对各个数组的内容存放是什么没有个清晰的概念,对指针偏移之后的数量算不出来或者模棱两可,那么本篇就来详细介绍sizeof和strlen来具象化的显示数组的内容存放了多少内容,偏移量变化后的变化,这个数组进行运算后会不会构成越界访问......看完这篇你就懂了。

2.sizeof和strlen的对比

        sizeof,是关键字,它只计算变量所占的内存空间大小,不在乎内存存放的内容,要是把类型放进去,就会计算该类型所创建的变量所占内存的大小。需要注意的是,sizeof里面的操作数,不会真的参与计算。

        strlen,是库函数,是专门求字符串长度的,即从首字符开始,计算到“\0”之前的字符串个数,要是所创建字符串没有带有"\0",那么极有可能会存在一个问题:越界访问。因为strlen会一直往后访问直接遇到"\0"字符为止。

        这是它的函数形式。

size_t strlen ( const char * str );
strlen库函数

2.1 sizeof

#inculde <stdio.h>
int main()
{
 int a = 10;
 printf("%d\n", sizeof(a));
 printf("%d\n", sizeof a);
 printf("%d\n", sizeof(int));
 
 return 0;
}

2.2 strlen

strlen 计算的是字符串的长度,计算'\0'之前的,是库函数
int main()
{
    char arr1[3] = { 'a', 'b', 'c' };
	char arr2[] = "abc";
	printf("%d\n", strlen(arr1));//随机
	printf("%d\n", strlen(arr2));//3

	printf("%d\n", sizeof(arr1));//3
	printf("%d\n", sizeof(arr2));//4
	return 0;
}

         arr1是字符数组,不带'\0',\0'的位置是随机的,在vs2022,x86的情况下,第一个打印的是随机值,arr2数组是个字符串数组,自带'\0'字符。不相信的话可以按一下F10,打开监视窗口看看。

        那么既然strlen是返回的是开头到null终止符'\0'之前,arr1是没有'\0'的,看作一个npc一样随机刷新的,所以是随机值。arr2是有的,返回abc三个字符,sizeof返回的是内存空间大小。abc'\0',4个字符,每个1个字节,所以就是4。

arr1和arr2的数组内容

3.数组和指针题目解析

        了解,清楚sizeof和srlen的区别与使用,那么本篇文章的重点就来了,就是在不同数组类型下sizeof和strlen显示数组有多少内容,其内在还是熟悉对数组的内容存放是什么,偏移量的变化,是否构成越界访问了如指掌。上练习题,看代码来说话。

3.1一维数组 

        这里首先要说明2个规则:

  1. 数组名表示数组首元素的地址
  2. 以下两种情况下除外:

        数组名单独放在sizeof内部,表示计算的是整个数组的大小。

        &数组名,表示为取出整个数组的地址。

int a[] = {1,2,3,4};
printf("%d\n",sizeof(a));
printf("%d\n",sizeof(a+0));
printf("%d\n",sizeof(*a));
printf("%d\n",sizeof(a+1));
printf("%d\n",sizeof(a[1]));
printf("%d\n",sizeof(&a));
printf("%d\n",sizeof(*&a));
printf("%d\n",sizeof(&a+1));
printf("%d\n",sizeof(&a[0]));
printf("%d\n",sizeof(&a[0]+1));

         依次来看,(a)数组名单独放在siezof内部,计算整个数组大小,这是个一维整形数组,4个元素,每个元素int类型,4个字节,4*4=16。

        第二个,(a+0) a不是单独放在内部,即使是+0也不算是单独放置,所以是首元素的地址+0,是1这个元素地址的大小,是地址,地址是指针,大小就是4/8,32位是4个字节,64位是8个字节。

        第三个,(*a)数组名没有单独放,是首元素地址,对它解引用,得到第一个元素的内容1,1是个整形,大小是4。

        第四个,(a+1)根据第二个类推,首元素地址+1,是第二个元素的地址的大小,是地址,大小就是4/8。

        第五个,很简单吧,a[1]不就是下标为1的元素2吗,对它计算大小,肯定是4了。

        第六个,(&a)对数组名进行取地址,取出整个数组的地址,计算大小,是地址,大小也是4/8.

        第七个,(*&a)取地址后解引用,取出整个数组地址,加上*,那就是得到整个数组的内容了,计算它的大小,和第一个一样,4*4=16。

        第八个,&a是整个元素的地址,&a+1,跳过这个数组到下一个数组的地址,指向数组后面的空间。这里没有构成非法访问,虽然指向的是没有初始化的空间,但是我们没有对他进行实际操作,只是访问了一下而已。

        第九个,&a[0],对第一个元素取地址,得到第一个元素的地址,是地址大小就是4/8。

        第十个,&a[0]+1,对第二个元素取地址,是地址大小就是4/8。

        如下图,在x86环境下,指针的大小是4位的,那么但凡涉及地址大小的,其实也就是指针大小,2,4,6,8,9,10都是计算指针大小,所以都是4,和我们上面的解释一样。

        在64位环境下就比较清楚了,无论什么类型的指针,大小都是8个字节,所以 2,4,6,8,9,10。是8,计算元素大小是4,整个数组大小是16,图例多了一个元素,所以显示20。

3.2字符数组

        讨论完一维整形数组,那么接下来就讨论以下字符数组,这里会分两种,一种是字符数组,一种是字符串。

3.2.1 字符串和字符数组区别

        这两者虽然只一字相差,但却是区别甚大。字符数组与字符串在本质上的区别就在于“字符串结束标志”的使用。字符数组中的每个元素都可以存放任意的字符,最后一个字符可以是‘\0’,也可以不是。但作为字符串使用时,就必须以‘\0’结束,因为很多有关字符串的处理都要以‘\0’作为操作时的辨别标志,缺少这一标志,系统并不会报错,有时甚至可以得到看似正确的运行结果,只会报个警告。但这种潜在的错误可能会导致严重的后果。程序在未遇到'\0'之前,会一直访问,所以会超出空间,形成越界访问

        那么定义字符数组的形式不是唯一的,不同情况,系统会自动或不会补'\0',没有补的话就一定要我们添加才可以,没有添加结束标志的只能看作字符,而不能看作字符串。

(1)所赋值的个数少于数组元素大小时,系统会补'\0'。形成字符串。

char c[10]={‘p’,‘r’,‘o’,‘g’,‘r’,‘a’,‘m’};

        当然你不放心也可以自己补上,但是效果是一样的 。

(2)所赋值个数和数组元素一样大。 数组里面7个字符,元素也是7个,就没有空余空间留给'\0' 来存放了。不形成字符串,是个字符数组

char c[7]={‘p’,‘r’,‘o’,‘g’,‘r’,‘a’,‘m’};

(3)当你想用单个字符赋值来决定你的数组大小时,系统是不会给你添加'\0'的,你需要自己添加上去才可以。这里会形成字符串。

char c[ ]={‘p’,‘r’,‘o’,‘g’,‘r’,‘a’,‘m’,’\0’};

         这样子则只会给你申请7个空间,不会预留位置给'\0',那么也就不能作为字符串来使用,只是一个字符数组。

char c[ ]={‘p’,‘r’,‘o’,‘g’,‘r’,‘a’,‘m’};

(4)直接赋值常量字符串,这几种都可以,不用人为去添加'\0',但是得存放空间足够大,系统会自动补上的。

char str1[15]={“I am happy”};

        以字符串常量赋值来决定数组大小,10个字符加一个'\0',一共11个字符大小。 

char str1[]=“I am happy”;

        可以去掉花括号 

char str1[15]=“I am happy”;

        但是下面这个定义就错,虽然系统会自动补'\0        ',但是因为空间不够,没有存放'\0'的位置,也构不成字符串。

char str3[10]=“I am happy”;

     3.2.2 字符数组

  来看看例题:

         一组是sizeof,一组是strlen,注意这里是没有写明数组大小,也没有看到有'\0'字符,所以构不成字符串。那么根据规则来仔细思考吧。

第一组 第一个,根据规则和sizeof的特点,计算的是整个数组的大小,共6个,每个1个字节。

            第二个,arr + 0是数组首元素的地址,大小是4/8。

            第三个,*arr是数组的首元素,计算的是首元素的大小-1字节。

            第四个,arr[1]第二个元素大小,当然是1了。

            第五个,取出的数组的地址,数组的地址也是地址,是地址大小就是4/8

            第六个,&arr+1是跳过整个,指向数组后边空间的地址,4/8。

            第七个,&arr[0] + 1是数组第二个元素的地址,是地址4/8字节。

char arr[] = {'a','b','c','d','e','f'};
printf("%d\n", sizeof(arr));
printf("%d\n", sizeof(arr+0));
printf("%d\n", sizeof(*arr));
printf("%d\n", sizeof(arr[1]));
printf("%d\n", sizeof(&arr));
printf("%d\n", sizeof(&arr+1));
printf("%d\n", sizeof(&arr[0]+1));

这里就以注释来说明了 。

char arr[] = {'a','b','c','d','e','f'};
printf("%d\n", strlen(arr));//随机值,没有结束标识符,会一直访问直到出现'\0'
printf("%d\n", strlen(arr+0));//随机值
printf("%d\n", strlen(*arr));//strlen('a')->strlen(97),非法访问-err
printf("%d\n", strlen(arr[1]));//'b'==98,和上面的代码类似,是非法访问 - err
printf("%d\n", strlen(&arr));//&arr虽然是数组地址,但是也是从数组起始位置开始的,计算的还是随机值
printf("%d\n", strlen(&arr+1));//&arr是数组的地址,&arr+1是跳过整个数组的地址,求字符串长度也是随机值
printf("%d\n", strlen(&arr[0]+1));//&arr[0] + 1是第二个元素的地址,是'b'的地址,求字符串长度也是随机值

       由此可见,在以单个字符来赋值字符数组的时候,'\0'的添加是很有必要的,很可能就会造成越界访问导致随机值的出现。那么接下来就看看字符串。

3.2.3 字符串

        字符串,系统是自动补'\0'的,而且没有写明数组大小,所以是以字符串常量来决定数组大小的。

char arr[] = "abcdef";//数组是7个元素
printf("%d\n",strlen(arr));//6,arr是数组首元素的地址,strlen从首元素的地址开始统计\0之前出现的字符个数
printf("%d\n", strlen(arr+0));//arr + 0是数组首元素的地址,同第一个,结果是6
printf("%d\n", strlen(*arr));//*arr是'a',是97,传给strlen是一个非法的地址,造成非法访问
printf("%d\n", strlen(arr[1]));//同上
printf("%d\n",strlen(&arr));//取出的是整个数组的地址,从首元素开始算,一共6个
printf("%d\n", strlen(&arr+1));//&arr + 1是跳过数组后的地址,是没有被赋值的,统计字符串的长度是随机值
printf("%d\n", strlen(&arr[0]+1));//&arr[0]+1是b的地址,从第二个字符往后统计字符串的长度,大小是5
char arr[] = "abcdef";
printf("%d\n", sizeof(arr));//7 - 数组名单独放在sizeof内部,计算的是数组的总大小,单位是字节
printf("%d\n", sizeof(arr+0));//arr+0是首元素的地址,大小是4/8
printf("%d\n", sizeof(*arr));//*arr是数组首元素,大小是1字节
printf("%d\n", sizeof(arr[1]));//arr[1]是数组的第二个元素,大小是1字节
printf("%d\n", sizeof(&arr));//&arr是数组的地址,是4/8字节
printf("%d\n", sizeof(&arr+1));//&arr + 1是跳过整个数组的地址,是4/8字节
printf("%d\n", sizeof(&arr[0]+1));//&arr[0] + 1是第二个元素的地址,是4/8字节

 3.3.4 把字符常量存放在指针

char *p = "abcdef";
printf("%d\n", sizeof(p));//p是指针变量,大小就是4/8字节
printf("%d\n", sizeof(p+1));//p + 1是b的地址,就是4/8个字节
printf("%d\n", sizeof(*p));//*p是'a',sizeof(*p)计算的是字符的大小,是1字节
printf("%d\n", sizeof(p[0]));//p[0]->*(p+0) -> *p  就同上一个,1字节
printf("%d\n", sizeof(&p));//&p是二级指针,是指针,大小就是4/8
printf("%d\n", sizeof(&p+1)); //&p + 1是跳过p变量后的地址,4/8字节
printf("%d\n", sizeof(&p[0]+1));//p[0]就是‘a’,&p[0]就是a的地址,+1,就是b的地址,就是4/8
char *p = "abcdef";
printf("%d\n", strlen(p));//6- 求字符串长度
printf("%d\n", strlen(p+1));//p + 1是b的地址,字符串长度从b到f就是5
printf("%d\n", strlen(*p));//err,*p是'a'
printf("%d\n", strlen(p[0]));//err
printf("%d\n", strlen(&p));&p拿到的是p这个指针变量的起始地址,从这里开始求字符串长度完全是随机值
printf("%d\n", strlen(&p+1));//&p+1是跳过p变量的地址,从这里开始求字符串长度也是随机值
printf("%d\n", strlen(&p[0]+1));//&p[0] + 1是b的地址,从b的地址向后数字符串的长度是5

3.3 二维数组

        二维数组需要注意的在sizeof中,a表示什么,a[0]表示什么,a[0][0]表示什么,第一个,单独放,表示整个数组的大小,没有什么问题,第二个呢,是表示第一行的大小还是什么?a[0][0]表示的是数组第一行第一个元素应该也是没问题。

        想不明白的话,简单,用代码测一下不就能倒推了吗

int a[3][4] = {0};
printf("%d\n",sizeof(a));//48 
printf("%d\n",sizeof(a[0][0]));//4
printf("%d\n",sizeof(a[0]));//16
printf("%d\n",sizeof(a[0]+1));//4
printf("%d\n",sizeof(*(a[0]+1)));//4
printf("%d\n",sizeof(a+1));//4
printf("%d\n",sizeof(*(a+1)));//16
printf("%d\n",sizeof(&a[0]+1));//4
printf("%d\n",sizeof(*(&a[0]+1)));//16
printf("%d\n",sizeof(*a));//16
printf("%d\n",sizeof(a[3]));//16

        由显示可以看出,a[0]是二维数组中第一行的数组名,数组名是首元素地址,规则是不变的,a[0]单独放在sizeof里面,求的是整个第一行数组的大小,16个字节;而第四个a[0]作为第一行的数组名,没有单独放在sizeof内部,没有取地址,表示的就是数组首元素的地址,那就是第一行第一个元素a[0][0]的地址,a[0]+1就是第一行第二个元素的地址,是地址就是4/8个字节。其他应该都是比较熟悉的了。

        注意最后一个,感觉是越界访问了,其实没有,如果数组存在第四行,a[3]就是第四行的数组名,数组名单独放在sizeof内部,计算的是第四行的大小。

再次强调下数组名的意义:

        1. sizeof(数组名),这里的数组名表示整个数组,计算的是整个数组的大小。

        2. &数组名,这⾥的数组名表示整个数组,取出的是整个数组的地址。

        3. 除此之外所有的数组名都表示首元素的地址。  

4.总结

        诸如上述的各种环境各种数组下的练习题,需要时不时就去翻看,复习,最好是不同形式的代码都手撕一遍代码,这样有助于我们复习,增加印象。

        不是常有一句话吗,自己写的代码,比较复杂的情况下,如果你没有经常看,甚至没有注释解释,第一天写完你的代码你能看懂,天也能看懂,三天,五天后,只有天知道了。虽然这是个网上的玩笑话,但是我一开始也会懒得写注释,导致一星期后我看着我好久前的代码迷惑不已,还得重新思考,然后我就开始不偷懒,勤快点好,良好的习惯需要去养成:

        一是要加注释,勤注释,增加自己理解,方便自己复习。也方便日后自己要去修改很早之前的代码的时候,能看懂自己当初写了啥是吧。二是要形成良好的书写规范,便于自己,也方便别人观摩,莫一天别人看到你的代码感到惊叹,也是一件好事呀,最后的最后祝大家新年快乐,新的一年在技术上又突飞猛进!


http://www.niftyadmin.cn/n/5840337.html

相关文章

c语言操作符(详细讲解)

目录 前言 一、算术操作符 一元操作符&#xff1a; 二元操作符&#xff1a; 二、赋值操作符 代码例子&#xff1a; 三、比较操作符 相等与不相等比较操作符&#xff1a; 大于和小于比较操作符&#xff1a; 大于等于和小于等于比较操作符&#xff1a; 四、逻辑操作符 逻辑与&…

docker直接运行arm下的docker

运行环境是树莓派A 处理器是 arm32v6 安装了docker&#xff0c;运行lamp 编译安装php的时候发现要按天来算&#xff0c;于是用电脑vm下的Ubuntu系统运行arm的docker 然后打包到a直接导入运行就可以了 第一种方法 sudo apt install qemu-user-static 导入直接运行就可以了…

实践Rust:编写一个猜数字游戏

如果你正在学习Rust&#xff0c;并且想通过一个有趣的小项目来巩固所学知识&#xff0c;那么“猜数字游戏”是一个绝佳的选择&#xff01;这个游戏的逻辑非常简单&#xff1a;程序会随机生成一个数字&#xff0c;玩家需要猜测这个数字是多少&#xff0c;程序会告诉玩家猜大了还…

【SSH】如何通过 SSH 跳板实现免密码登录(跨平台通用)

1. 前言 在实际开发或运维中&#xff0c;面对复杂网络环境&#xff0c;我们常常需要通过跳板机&#xff08;Jump Host&#xff09;连接到目标服务器。为了提高操作效率&#xff0c;SSH 免密码登录是一种高效且安全的选择。本文将从基础概念到实际操作&#xff0c;详细介绍如何…

【SQL】SQL注入知识总结

介绍 SQL是操作数据库数据的结构化查询语言&#xff0c;网页的应用数据和后台数据库中的数据进行交互时会采用SQL。SQL注入是将Web页面的原URL、表单域或数据包输入的参数&#xff0c;修改拼接成SQL语句&#xff0c;传递给Web服务器&#xff0c;进而传给数据库服务器以执行数据…

记4(可训练对象+自动求导机制+波士顿房价回归预测

目录 1、TensorFlow提供自动求导机制2、自动求导机制&#xff1a;2.1、GradientTape&#xff1a;&#xff08;是一个上下文管理器对象&#xff09;2.2、 watch_accessed_variables、tape.wahtch&#xff1a;监视可训练变量2.3、二阶导数&#xff1a;两次GradientTape2.4、对向量…

C语言------指针从入门到精通

第一部分: 前言: 本篇文章主要划分为两大部分: 第一部分适合零基础的同学,主要学习了解指针的概念&#xff0c;对指针大概有个概念。如果你已经有基础,即可跳过第一部分的内容。 第二部分主要是分解指针的实现逻辑,通过19个例子,再结合代码公式把不同类型的指针及指针的应用详细…

Web3技术详解

Web3技术代表着互联网技术的最新进展&#xff0c;它致力于打造一个去中心化的互联网生态系统。以下是对Web3技术的详细解析&#xff1a; 一、Web3技术的核心概念 Web3是第三代互联网技术的代名词&#xff0c;代表着去中心化、区块链驱动和用户自有控制的理念。在Web3的世界中…