文章目录

  • 前言:
  • 语义”陷阱“
    • 指针与数组
    • 操作符: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的规则
  1. 其打印的地址必须是字符串类型

  1. 打印结束标志是‘\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陷阱与缺陷》第三章相关推荐

  1. 第三章 UT单元测试——CPU与内存使用率限制

    系列文章目录 第一章 UT单元测试--GoogleTest通用构建说明 第二章 UT单元测试--GTest框架实例 第三章 UT单元测试--CPU与内存使用率限制 文章目录 系列文章目录 前言 一.环 ...

  2. 慕课软件质量保证与测试(第三章.单元测试)

    慕课金陵科技学院.软件质量保证与测试.第三章.黑盒测试.单元测试 0 目录 3 黑盒测试 3.9 单元测试 3.9.1课堂重点 3.9.2测试与作业 4 下一章 0 目录 3 黑盒测试 3.9 单元测 ...

  3. 《构建之法》前三章读后感

    通过第一章讲述的概论,理解到软件工程到底是什么,又为何要叫软件工程,他对我们的生活又有什么影响. 通过一些实例我也认识到客户需求分析的重要,就阿超那样的四则运算一样,渐渐的功能和需求就多了. 在第二章 ...

  4. 走向.NET架构设计—第三章—分层设计,初涉架构(后篇)

    走向.NET架构设计-第三章-分层设计,初涉架构(后篇) 前言:本篇主要是接着前两篇文章继续讲述! 本篇的议题如下: 4. 数据访问层设计 5. 显示层设计 6. UI层设计   4.  数据访问层设 ...

  5. 软考中项第三章 信息系统集成专业知识

    第三章 信息系统集成专业知识 信息系统的生命周期可以分为立项.开发.运维及消亡四个阶段 立项阶段:概念阶段或需求阶段,这一阶段根据用户业务发展和经营管理的需要,提出建设信息系统的初步构想,然后对企业信 ...

  6. 构建之法前三章读后感—软件工程

    本教材不同于其他教材一贯的理知识直接灌溉,而是以对话形式向我们传授知识的,以使我们更好地理解知识点,更加清晰明确. 第一章 第一章的概述中,书本以多种方式,形象生动地向我们阐述了软件工程的内容,也让我 ...

  7. 关于对《Spring Security3》翻译 (第一章 - 第三章)

    原文:http://lengyun3566.iteye.com/category/153689?page=2 翻译说明 最近阅读了<Spring Security3>一书,颇有收获(封面见 ...

  8. C++ API 设计 08 第三章 模式

    第三章 模式 前一章所讨论的品质是用来区分设计良好和糟糕的API.在接下来的几个章节将重点关注构建高品质的API的技术和原则.这个特殊的章节将涵盖一些有用的设计模式和C++ API设计的相关习惯用法. ...

  9. 敏捷整洁之道 -- 第三章 业务实践

    敏捷整洁之道 -- 第三章 业务实践 0. 引子 1. 计划游戏 1.1 三元分析 1.2 故事和点数 1.3 故事 1.4 故事估算 1.5 对迭代进行管理 1.6 速率 2. 小步发布 3. 验收 ...

  10. 第三章 信息系统集成专业技术知识

    第三章 信息系统集成专业技术知识 知识点 1.信息系统的生命周期有哪几个过程 2.信息系统开发的方法有几种:各种用于什么情况的项目. 3.软件需求的定义及分类: 4.软件设计的基本原则是什么: 5.软 ...

最新文章

  1. 判断屏幕宽高比是否为16:9
  2. 飞天技术汇|阿里云推出全新开发者服务,技术赋能开发者
  3. 七天学习计划_c#_[2][3][4][5]委托、事件、委托与事件的区别、泛型委托、Func\Action\predicate
  4. Firefox 的一个HTTP分析器扩展
  5. WI-FI无线数据解密
  6. priority case语句
  7. MyBatis中动态SQL
  8. Launcher结构之home screen
  9. 黑客借“甲型流感”传毒 挂马疾病预防控制中心网站
  10. pytorch学习笔记(十):MLP
  11. 高中信息技术——进制与编码刷题点整理
  12. 如何编写可怕的 Java 代码?
  13. windows10下载安装jdk1.7教程
  14. 线程优先级的设定pthread_setschedparam
  15. 【2021 年终总结】一年涨粉100倍,有规划始执行~成功一半
  16. Normalize异常报错
  17. 2022年数学类保研经验整理(信息与计算科学、计算数学、计算机)
  18. VB控件实现IObjectSafety安全接口(zt)
  19. 2005计算机885编程题
  20. 网络直播的发展和视频直播APP系统软件的简单介绍

热门文章

  1. c语言编程题输入两个直角边,C语言编程 直角三角形已知两边求第三边
  2. python打印26个字母的四种方法
  3. MongoDB安装(Linux)
  4. c++游戏五子棋游戏
  5. Mac 如何选择 选择Pro还是Air
  6. liteon460w服务器电源管理系统,处理器电源管理 (PPM) 优化 Windows Server 平衡电源计划...
  7. 机器学习day12 机器学习实战adaboost集成方法与重新进行疝马病的预测
  8. 刘国军:异构融合加速赋能数字化升级
  9. Android命令之ps
  10. 安卓搜不到airpods_AirPods Pro支不支持安卓手机 AirPods Pro配对无弹窗怎么办