《C陷阱与缺陷》第三章
文章目录
- 前言:
- 语义”陷阱“
- 指针与数组
- 操作符:sizeof()
- 指针
- 非数组的指针
- 作为参数的数组声明
- 避免”举隅法“
- 空指针并非空字符串
- 边界计算与不对称边界
- 数组边界”溢界“问题
- 求值顺序
- 整数“溢出”
- 为main函数提供返回值
前言:
博主实力有限,博文有什么错误,请你斧正,非常感谢!
博主目前只掌握的c语言,因此本文主要以c语言为背景讨论问题。编译器:VS2019
本文是《C陷阱与缺陷》专栏第3章
《C陷阱与缺陷》第一章,我们认识了词法“陷阱”,第二章认识了语法“陷阱”
第三章让我们了解一下语义”陷阱“(指针与数组底层原理,求值顺序)
《C陷阱与缺陷》
“词法”陷阱 “语法”陷阱 、
语义”陷阱“
一句话,哪怕,单词,语法都对,仍然可能存在歧义或者非我们表达希望的意思。
同样对于c程序,即使语法正确,编译器对其也可能是我们非希望的运算,因此本博文主要讨论:语义“陷阱“。
指针与数组
指针与数组之间的联系是密不可分,理解数组必然需要理解指针。
- 数组名是不能进行自增,自减运算的,因为数组名是常地址。
- C语言只有
一维数组
,而且数组的大小必须在编译期间
就作为常数
确定下来。
| C99标准允许变长数组(int arr[m] [n]).但是市面上大部分
C编译器
(VS2019)还没有完全实现C99
标准,但是对于在线OJ系统
允许变长数组。另外GCC中实现了变长数组。 |
| :----------------------------------------------------------- |为什么说C语言只有一维数组呢?
- 我们知道变量在内存是连续存放的,同样一维数组也是。Exp;arr[m];当我们定义一个一维数组时,内存会为arr开辟大小为:
m*sizeof(arr[0])
的字节空间。而对于二维数组,因为内存连续性的原因,内存并不会真真的开辟一个二维空间,而是连续依次存入二维数组的每个数据。之所以有二维数组的说法是为了分析问题方便。Exp: int arr[ 3 ] [3 ] ;
声明 arr是一个数组,该数组有3个元素,每个元素类型是数组大小为3的一维数组。
- 二维数组的实质是一维数组,只是其元素类型是一维数组类型。
- 多维数组同理
- 为什么要在编译期间确定大小
为了给数组开辟内存
- 对于数组我们只要知道2件事:1.数组
大小
;2.获得指向数组首元素
地址的指针。对于数组的运算就没问题了
- 为什么说知道数组首元素地址就可以了
1.指针的运算是根据其指向
数据类型来
进行计算。int main() {int arr1[4] = { 5,6,8,4}; int* p1 = arr1;//数组名是一维数组arr1首元素地址,而首元素是int 类型,因此p类型为int printf("%d\n", *arr1); printf("%d\n", *p1); printf("%d\n", *(arr1+1)); printf("%d\n", *(p1+1));//p的类型是int,因此加一,跳过int 字节内存。//二维数组 printf("\n"); int arr2[3][2] = {45,4,5,6,78,75 }; int(*p2)[2] = arr2;//数组名是二维数组首元素地址,而首元素的,类型是 int [2](含有2个元素的一维数组)//因此p在定义的类型为:int [2]; printf("%d\n", arr2[0][0]); printf("%d\n", **p2);//*p后的地址类型为int,而%d需要int型数据,因此再*;printf("%d\n", arr2[1][0]); printf("%d\n", *(*(p2 + 1)+0));return 0; }
2.以数组下标的形式进行数组的运算很正常,但是实质底层原理是指针的运算(编译器在遇到数组都将其转化为
同类型
的指针
)。任何一个数组元素的下标都可以通过指针找到。因此我们完全可以依据指针进行数组的运算。
- 许多程序设计语言中都内建有索引运算,在C语言中索引运算是以指针算术的形式定义的。
int *p=arr; //数组下标为0的元素是arr[0] //实质是*(p+0); //数组下标为1的元素是arr[1] //实质是 *(p+1)int arr[2][3]={0}; int (*p)[3]=arr; //数组下标为0的元素是arr[0],但是其类型int*即int的地址,因此二维数组下标 (0,0)的arr[0][0] //其实质:*(*(p+0)+0) //数组下标为1的元素是arr[1],但是其类型int*即int的地址,因此二维数组下标 (1,0)的arr[1][0] //其实质:*(*(p+1)+0)
操作符:sizeof()
- sizeof()是
操作符
,不是函数
- sizeof()用于只用于求
数据
在内存
中所占内存的大小
单位字节
,在()里面不产生的任何影响
- sizeof对
数组
的一些运算,有特殊规定C中
数组名
代表其数组首元素
地址。但是对于sizeof()来说,其代表整个数组。而&arr代表取数组的地址
,因此对地址求其大小,在32位系统4字节大小64位系统下8字节大小。
指针
任何指针都是指向某种类型的变量,类型决定了指针运算跳过的字节大小。
只有
同类型
的指针之间,才能进行有效
运算int 型指针与int型指针间进行运算,数组型指针与数组型指针间进行运算。等。。。
- 同类型指针间的运算是有意义的
- 比如同时指向数组的2个指针,其相减就可以得到2个指针间的元素个数
一旦定义指针,就
必须
指定其指向,或者对指针赋值NULL
对于一维数组arr[i]与* (p+i)意义一样,但是[ ]的形式更容易理解,尤其是二维数组。
因为*(p+i)== *(i+p),即p[i]=i[p],但是
强烈不推荐
这种i[p]这种形式
非数组的指针
- 字符串
C语言的
字符串常量
存放在常量区
,其代表一块包括字符串中所有字符以及字符串``结束标志‘\0’`组成 的内存区域的首地址.因此对于字符型指针,其不是指向整个字符串,而指向的是字符串的
首地址
。另外不同的指针,指向相同字符串,不同指针指向同一地址的字符串
- 字符串的实质是地址,但是因为其是常量,因此不能修改其值
char *str="hello"; *str='G';//在C语言在是违法,禁止的
- 打印字符串%s的规则
- 其打印的
地址
必须是字符串类型
- 打印结束标志是
‘\0’
;
作为参数的数组声明
- | 在C语言中,我们没办法可以将一个数组作为函数参数直接传递。我们用数组名传参,数组名会被编译器自动转换为同类型的指针。 |
| :----------------------------------------------------------- |Exp:
int fun(int arr[])//编译器自动转换数组名 int fun(int *p)
避免”举隅法“
- ”举隅法“是一种文学修辞上的手段,类似以微笑代替喜悦,赞许之情。而对于C语言中,指针是指向某个数据,但是并不意味这指针就是该数据,指针存的是该数据的地址。即不要混淆
指针
与指针所指向的数据
空指针并非空字符串
C语言的
强制转换操作符
,可以将一个整数X强制转换为指针
(即x将变成对应16进制内存编号的地址),但是对于常数0这个特殊情况
,编译器保证由0
转换的指针
不等于任何有效
的指针。当0转换为指针时,绝对不可以对其
解引用(*)
if(p==(char*)0) {}//合法 if(strcmp(p,(char *)0)) {}//非法,库函数strcmp中有对指针的``解引用``
边界计算与不对称边界
边界计算:
对一个数组有10个元素,那么数组下标的范围是什么呢?
- 对于Fortran,Pl/I,Snobol4等程序语言,下标从1开始,而且这些语言允许自定义数组下标的起始地址。
- 对于Algol,Pascal,编程必须
显示
的指定数组下标上界与下界
- 在Basic中声明一个10个元素的数组,实际编译器分配11个元素的空间,下标从0到10
不对称边界:
问题:修建一个100米的护栏,护栏间的距离是10米,问需要多少栏杆?
答案: 11
这是典型的”栏杆错误“,也被称谓”差一错误“
针对这种错误有一种好的方法:
- 首先考虑最简单情况下的特例,然后推广
- 仔细计算边界。
而在C语言编程时如何更好的避免差一错误呢?
这就需要“不对称边界”:
- 用第一个入界点和最后一个出界点来表示数值范围
- 取值范围大小是出界点与入界点差
- 上界永远不小于下界
Exp1:
Exp2:
数组边界”溢界“问题
int i =0; int arr[10]={0}; for(i =0;i<=12;i++) {arr[i]=0; printf("%d\n",arr[i])} //这种用法在C中是允许的,因为底层是指针。 //程序是个死循环,下面解释 //在Vs2019中,编译器会在变量与数组之间放2个空内内存,目的就是防止"溢界"问题 //内存的利用是先高地址,后低地址。 //正是因为这个规则和变量与数组的定义顺序,当arr[12]即是i //此时i又重新赋值为0,因此进入死循环
ANSI C标志明确允许这种用法:数组中实际不存在的”溢界“元素地址位于数组所占内存之后,这个地址可以被用于赋值和比较,但是引用该元素就是非法的。
求值顺序
- 运算符的优先级并不决定求值顺序。
int a=b*c+d *e+f * g;
//编译器只知道*比+先计算,但是不知道开始的顺序是怎样的。
即a=(b*c+d *e)+f * g;
或者a=b*c+(d *e+f * g);
- 但是C语言在规定了4个运算符的求值顺序
四个运算符是:&&;|| ; ?: ; ‘,’ ;
&&和||首先对左侧操作数求值,具有改变运算顺序的性质即(左侧为真,右侧就不需要求值)
a?b:c 先算a,后根据a算b,c;
,首先对左侧操作数求值,然后丢弃该值,再对下一个操作数求值。
分隔函数参数的‘’逗号‘’不是“逗号运算符”。
- 所有赋值运算都不决定求值顺序
int i=0; while(i<n) {y[i]=x[i++] } //因为赋值"="的性质,无法确定y,x中的i是哪个值,另外不同编译器会有自己的赋值运算符求值顺序。为避免这个有争议的”垃圾代码“我们可以 while(i<n) {y[i]=x[i]; i++; }
整数“溢出”
C中的每种数据类型都有其取值范围。
如signed char -128~127
int (-2^31) ~(2^31-1)
等…
- C语言中存在2种整数算术运算,有符号和无符号
- 在无符号运算中,无”溢出“一说
- 在无符号与有符号运算中,有符号会转化为无符号型,进行运算,不在有“”溢出“一说
- 在有符号运算中,存在”溢出“一说。另外“溢出”的结果是
未定义
的,当发生“溢出”,任何的运算都是不安全的。假设a和b是2个非负整形变量,我们检验是否会“溢出”,溢出后就会成为”负数“。
if(a+b<0) {.. }
在某些机器上,加分运算器讲设置一个寄存器的4种状态之一:正,负,0,溢出。在这种编译器上,a+b后判断寄存器是否未”负“,但是此时寄存器的状态是”溢出“,那么if的检测就会不安全。 更改为:
//方法一 if((unsingned)a+(unsigned)b>INT_MAX) {} //INT_MAX是整形数据的最大取值,定义在<limits.h>库中 //方法二 if(a>INT_MAX-b) {}
为main函数提供返回值
函数为说明返回值的类型时,默认为int,main也同理
对于大多数C语言都是通过main的返回值来告诉操作系统该函数执行是成功还是失败。
返回0代表成功,非0代表失败
因此return具有结束函数执行的效果,在循环中合理运用会产生奇妙的效果
《C陷阱与缺陷》第三章相关推荐
- 第三章 UT单元测试——CPU与内存使用率限制
系列文章目录 第一章 UT单元测试--GoogleTest通用构建说明 第二章 UT单元测试--GTest框架实例 第三章 UT单元测试--CPU与内存使用率限制 文章目录 系列文章目录 前言 一.环 ...
- 慕课软件质量保证与测试(第三章.单元测试)
慕课金陵科技学院.软件质量保证与测试.第三章.黑盒测试.单元测试 0 目录 3 黑盒测试 3.9 单元测试 3.9.1课堂重点 3.9.2测试与作业 4 下一章 0 目录 3 黑盒测试 3.9 单元测 ...
- 《构建之法》前三章读后感
通过第一章讲述的概论,理解到软件工程到底是什么,又为何要叫软件工程,他对我们的生活又有什么影响. 通过一些实例我也认识到客户需求分析的重要,就阿超那样的四则运算一样,渐渐的功能和需求就多了. 在第二章 ...
- 走向.NET架构设计—第三章—分层设计,初涉架构(后篇)
走向.NET架构设计-第三章-分层设计,初涉架构(后篇) 前言:本篇主要是接着前两篇文章继续讲述! 本篇的议题如下: 4. 数据访问层设计 5. 显示层设计 6. UI层设计 4. 数据访问层设 ...
- 软考中项第三章 信息系统集成专业知识
第三章 信息系统集成专业知识 信息系统的生命周期可以分为立项.开发.运维及消亡四个阶段 立项阶段:概念阶段或需求阶段,这一阶段根据用户业务发展和经营管理的需要,提出建设信息系统的初步构想,然后对企业信 ...
- 构建之法前三章读后感—软件工程
本教材不同于其他教材一贯的理知识直接灌溉,而是以对话形式向我们传授知识的,以使我们更好地理解知识点,更加清晰明确. 第一章 第一章的概述中,书本以多种方式,形象生动地向我们阐述了软件工程的内容,也让我 ...
- 关于对《Spring Security3》翻译 (第一章 - 第三章)
原文:http://lengyun3566.iteye.com/category/153689?page=2 翻译说明 最近阅读了<Spring Security3>一书,颇有收获(封面见 ...
- C++ API 设计 08 第三章 模式
第三章 模式 前一章所讨论的品质是用来区分设计良好和糟糕的API.在接下来的几个章节将重点关注构建高品质的API的技术和原则.这个特殊的章节将涵盖一些有用的设计模式和C++ API设计的相关习惯用法. ...
- 敏捷整洁之道 -- 第三章 业务实践
敏捷整洁之道 -- 第三章 业务实践 0. 引子 1. 计划游戏 1.1 三元分析 1.2 故事和点数 1.3 故事 1.4 故事估算 1.5 对迭代进行管理 1.6 速率 2. 小步发布 3. 验收 ...
- 第三章 信息系统集成专业技术知识
第三章 信息系统集成专业技术知识 知识点 1.信息系统的生命周期有哪几个过程 2.信息系统开发的方法有几种:各种用于什么情况的项目. 3.软件需求的定义及分类: 4.软件设计的基本原则是什么: 5.软 ...
最新文章
- 判断屏幕宽高比是否为16:9
- 飞天技术汇|阿里云推出全新开发者服务,技术赋能开发者
- 七天学习计划_c#_[2][3][4][5]委托、事件、委托与事件的区别、泛型委托、Func\Action\predicate
- Firefox 的一个HTTP分析器扩展
- WI-FI无线数据解密
- priority case语句
- MyBatis中动态SQL
- Launcher结构之home screen
- 黑客借“甲型流感”传毒 挂马疾病预防控制中心网站
- pytorch学习笔记(十):MLP
- 高中信息技术——进制与编码刷题点整理
- 如何编写可怕的 Java 代码?
- windows10下载安装jdk1.7教程
- 线程优先级的设定pthread_setschedparam
- 【2021 年终总结】一年涨粉100倍,有规划始执行~成功一半
- Normalize异常报错
- 2022年数学类保研经验整理(信息与计算科学、计算数学、计算机)
- VB控件实现IObjectSafety安全接口(zt)
- 2005计算机885编程题
- 网络直播的发展和视频直播APP系统软件的简单介绍
热门文章
- c语言编程题输入两个直角边,C语言编程 直角三角形已知两边求第三边
- python打印26个字母的四种方法
- MongoDB安装(Linux)
- c++游戏五子棋游戏
- Mac 如何选择 选择Pro还是Air
- liteon460w服务器电源管理系统,处理器电源管理 (PPM) 优化 Windows Server 平衡电源计划...
- 机器学习day12 机器学习实战adaboost集成方法与重新进行疝马病的预测
- 刘国军:异构融合加速赋能数字化升级
- Android命令之ps
- 安卓搜不到airpods_AirPods Pro支不支持安卓手机 AirPods Pro配对无弹窗怎么办