C语言 语义陷阱超详细梳理总结

目录
  • 1 指针与数组
  • 2 非数组的指针
  • 3 作为参数的数组声明
  • 4 空指针并非空字符串
  • 5 边界计算与不对称边界
  • 6 求值顺序
  • 7 整数溢出
  • 8 为函数提供返回值

1 指针与数组

  • C语言中只有一维数组。数组中的元素可以是任意类型的对象,这也是多维数组构建的理论基础所在
  • 对于一个数组,我们只能做两件事:确定该数组的大小以及获得该数组下标为0的元素的指针。任何一个数组下标运算都等同于一个对应的指针运算。
  • 数组名代表首元素的地址,无法对其进行++或者–操作,换句话说,我们无法改变数组名(表示的值),因为数组名是个常量,无法进行修改。

2 非数组的指针

下面有一段程序,指出它的错误:

char *r;
r = malloc(strlen(s)+strlen(t));
strcpy(r,s);
strcat(r,t);
  • malloc有可能无法提供请求的内存,这种情况下malloc函数会通过返回一个空指针来作为“内存分配失败”事件的信号。
  • 给r分配的内存在使用完毕后应该及时释放。
  • 前面的例程在调用malloc函数时并未分配足够的内存,因为字符串还包含结束标志'\0'。

3 作为参数的数组声明

1.下面列举的两种写法是等价的:

char hello[] = "hello";
printf("%s\n",hello);//写法1
printf("%s\n",&hello);//写法2

原因:数组名hello代表数组hello首元素的地址。

2.下面的两种写法是等价的:

int strlen(char s[])
{
	/*具体内容*/
}
int strlen(char *s)
{
	/*具体内容*/
}

注意下面的两种写法:

extern char *hello;
extern char hello[];

这两种写法虽然是都是正确的,但是不同的形式传递给我们的意思却是完全不一致的,我们要根据具体情况进行使用。

4 空指针并非空字符串

注意:空指针不能对其进行解引用。

同时注意不能出现下述写法:

if(strcmp(p,(char*)0)==0)
	···

这种写法是非法的,原因在于库函数strcmp的实现中会包括一个操作,用于查看它的指针参数所指向的内容,即对空指针进行了解引用。

也不能出现下述写法:

假设p是空指针

printf(p);
printf("%s",p);
//当然,这两种写法是等价的

这种行为是未定义的。

5 边界计算与不对称边界

在我们写循环是最好这样来写:

int i = 0;
for(i = 0;i < 10; i++)
	···

这样写能够更好的看出循环的次数,即10次。

当数组中有10个元素时,下标的取值范围为0到9,但是当我们不需要引用这个元素时只需要引用这个元素的地址时,我们可以这样写

int arr[10] = {1,2,3,4,5,6,7,8,9,10};
for(int i = 0;&arr[i]<&(arr[10]);i++)
	···

这样可以顺利打印出数组元素从1到10的数字,

ANSI C标准明确允许这种用法:数组中实际不存在的"溢界"元素的地址位于数组之外所占内存之后,这个地址可以用于进行赋值和比较。当然,如果要引用该元素,那就是非法的了。对于实际去读取这个元素的值,这种做法的结果是未定义的,而且极少有编译器能偶检测出这个错误。当然,如果试图去修改这个元素,必然会导致程序崩溃,属于非法访问了!

6 求值顺序

C语言中只有四个运算符(&&、||、?:和,)存在规定的求值顺序。==运算符&&和运算符||首先对左侧操作数求值,只有在需要时才对右侧操作数求值。==运算符?:有三个操作数:在a?b:c中。操作数a首先被求值,根据a的值再求操作数b或c的值(此时b或c两个表达式根据前面a表达式的结果只会执行一个)。逗号运算符则首先对左侧操作数求值,然后"丢弃该值",再对右侧操作数求值。

注意:分割函数的参数并非逗号运算符。例如,x和y在函数f(x,y)中的求值顺序是未定义的,而在函数g((x,y))中却是确定的先x后y的循序。在后一个例子中,函数g只有一个参数。这个参数的值是这样求得的:先对x求值,然后“丢弃”x的值,接着求y的值。

这种求值顺序的存在使得某些“错误”的程序变为了正确,且在执行后得出正确的结果:

if(count!=0 && sum/count < smallaverage)
	···

注意:C语言中其它所有的运算符对其操作数求值的顺序是未定义的。特别是,赋值运算符并不保证任何求值循序。

例如:下面的这中从数组x中复制前n个元素到数组y中的做法是不正确的,因为它对求值顺序做了太多的假设:

i = 0;
while(i < n)
	y[i] = x[i++];

上面的代码假设y[i]的地址将在i的自增操作指向之前被求值,但这是不一定的,这依赖于编译器的具体实现。同样,下面的这种写法也是不正确的:

i = 0;
while(i<n)
	y[i++] = x[i];

修改成下面这种写法即可正常工作:

i = 0;
while(i<n)
{
	y[i] = x[i];
	i++;
}

当然,这种写法也可以简写为:

for(i = 0;i < n;i++)
	y[i] = x[i];

7 整数溢出

无符号整数不会发生溢出,这是C语言所规定的,如果结果大于所能表示的最大值M,则模(M+1),也就是发生了截断现象。

两个有符号整数进行相加时会发生溢出,而且溢出的结果是未定义的。

下面是一种错误的检查方式:

if(a + b < 0)
	complain();

因为当a+b却是发生溢出时,所有关于结果如何假设都不再可靠。

下面是两种正确的方式:

//方法一:
if((unsigned)a + (unsigned) > INT_MAX)
	complain();
//方法二:
if(a > INT_MAX - b)
	complain()

8 为函数提供返回值

C语言种常常通过return 返回一个值来告知操作系统的执行是成功还是失败,典型的处理方案是。返回值为0表示程序执行成功,返回值为非0则表示程序执行失败。我们常常会在程序的末尾加上return 0操作。

到此这篇关于C语言 语义陷阱超详细梳理总结的文章就介绍到这了,更多相关C语言 语义陷阱内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • C语言陷阱与缺陷之数组越界访问详解

    目录 1.问题引入 2.问题分析 (1)理论分析 (2)调试验证 总结 1.问题引入 一道经典的笔试题来了,请做好准备!!! 试问以下代码在Visual studio 2019环境下执行结果?原因? #include <stdio.h> int main() { int i = 0; int arr[10] = {0}; for(i=0; i<=12; i++) { arr[i] = 0; printf("Hello World!\n"); } return 0; }

  • C语言:陷阱与缺陷详解

    目录 一.前言 二.字符指针 三.边界计算与不对称边界 1.经典错误① 2.经典错误② 3.小结 四.求值顺序 五.运算符&& ||和! 总结 一.前言 二.字符指针 结论一:复制指针并不会复制指针所指向的内容.两个指针所指向位置相同,实际为同一个指针. 结论而:开辟两个数组,即使两个数组内容相同,地址也绝不相同. 三.边界计算与不对称边界 1.经典错误① int main() { int i = 0; int arr[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9,

  • C语言 语义陷阱超详细梳理总结

    目录 1 指针与数组 2 非数组的指针 3 作为参数的数组声明 4 空指针并非空字符串 5 边界计算与不对称边界 6 求值顺序 7 整数溢出 8 为函数提供返回值 1 指针与数组 C语言中只有一维数组.数组中的元素可以是任意类型的对象,这也是多维数组构建的理论基础所在 对于一个数组,我们只能做两件事:确定该数组的大小以及获得该数组下标为0的元素的指针.任何一个数组下标运算都等同于一个对应的指针运算. 数组名代表首元素的地址,无法对其进行++或者–操作,换句话说,我们无法改变数组名(表示的值),因

  • C语言自定义类型超详细梳理之结构体 枚举 联合体

    目录 一.什么是结构体 1.结构体实现 2.匿名结构体类型 3.结构体自引用 4.结构体的内存对齐 5.结构体位段  二.什么是枚举 1.枚举类型的定义 2.枚举的优点 三.联合(共用体) 1.什么是联合(共用体) 2.联合(共用体)的定义 3.联合(共用体)的初始化 总结 一.什么是结构体 结构是一些值的集合,这些值称为成员变量.结构的每个成员可以是不同类型的变量. //结构体声明 struct tag //struct:结构体关键字,tag:标签名,合起来是结构体类型(类型名) { memb

  • C语言超详细梳理排序算法的使用

    目录 排序的概念及其运用 排序的概念 排序运用 插入排序 直接插入排序 希尔排序 选择排序 直接选择排序 堆排序 交换排序之冒泡排序 总结 排序的概念及其运用 排序的概念 排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作. 稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次 序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排 序算法

  • C语言 超详细梳理总结动态内存管理

    目录 一.为什么存在动态内存分配 二.动态内存函数的介绍 1.malloc和free 2.calloc 3.realloc 三.常见的动态内存错误 1.对NULL指针的解引用操作 2.对动态开辟空间的越界访问 3.对非动态开辟的空间使用free释放 4.使用free释放一块动态开辟空间的一部分 5.对同一块开辟的空间多次释放 6.动态内存开辟忘记释放(内存泄漏) 四.几个经典的笔试题 一.为什么存在动态内存分配 我们已经掌握的内存开辟方式有: int a = 10://在栈空间开辟4个字节的连续

  • C++ 超详细梳理继承的概念与使用

    目录 继承的概念及定义 继承的概念 继承定义 定义格式 继承关系和访问限定符 继承基类成员访问方式的变化 基类和派生类对象赋值转换 继承中的作用域 派生类的默认成员函数 继承与友元 继承与静态成员 复杂的菱形继承及菱形虚拟继承 菱形继承 虚拟继承解决数据冗余和二义性的原理 继承的总结和反思 继承的概念及定义 继承的概念 继承机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类.继承呈现了面向对象程序设计的层次结构,体

  • Java超详细梳理IO流的使用方法上

    目录 Java语言的输入输出类库 1.流的概念 2.流的分类 3.流的作用 4.输入输出流类库 使用InputStream和OutputStream流类 1.基本输入输出流 1.InpitStream流类 2.OutputStream流类 2.输入输出流的应用 2.1文件输入输出流 2.2顺序输入流 2.3管道输入输出流 2.4过滤输入输出流 2.5标准输入输出 你要学会: 流的概念 处理字节流的类 处理字符流的类 Java标准输入输出 文件管理类 Java语言的输入输出类库 1.流的概念 流是

  • C++超详细梳理IO流操作

    目录 1.标准输出输入流-控制台流(iostream类) 2. 文件流(I/O操作) 文本文件 使用<< >> 进行读写 getline()读取一行 get() put()进行单个字符读写 二进制文件读写 get() put()进行单个字节读写 read() write()多个字节读写 注意事项 3.字符串输入输出流(sstream) sprintf sscanf 和 stringstream的使用 补充内容:C/C++中int和字符串类型的转换 string转int int转st

  • C++超详细梳理基础知识

    目录 命名空间的使用 来源 命名空间的使用 不展开 部分展开 全展开 函数重载 函数重载的规则 C++如何实现函数重载 引用 命名空间的使用 来源 在了解命名空间的原理和使用之前,我们先要理解,命名空间是为了解决什么问题. C++是在C的基础上发展而形成的一种语言,完全兼容C的语法,也加入了许多新的规则和语法来解决C的缺陷. 命名空间就是为了解决C语言中的重复命名的问题. 首先我们来看看这么一个代码: #include<stdio.h> int main() { int scanf = 20;

  • C++超详细梳理lambda和function的使用方法

    目录 lambda表达式 谈谈lambda的捕获 万能的function bind操作 lambda表达式 lambda表达式又称为匿名表达式,是C11提出的新语法.[]存储lambda表达式要捕获的值,()内的参数为形参,可供外部调用传值.lambda表达式可以直接调用 // 1 匿名调用 [](string name) { cout << "this is anonymous" << endl; cout << "hello "

  • python 字典常用方法超详细梳理总结

    目录 1.字典的概念 2.字典的主要特征 3.创建字典的三种方法 4.字典常用方法 1.clear() 2.copy() 3.get() 4.keys() 5.values() 6.items() 7.del() 8.zip() 1.字典的概念 字典和列表类似,也是可变序列,不过和列表不同,它是无序的可变序列,保存的内容是以键值对(key:value)形式存放的 字典的每个键值之间用冒号:分隔,每个键值对之间用,隔开,整个字典包含在{ }中 dict = {key1:value1,key2:va

随机推荐