数组——参考《C和指针》

简介: 数组——参考《C和指针》

一、操作符(operator)

①初识概念:

其实“操作符”这个概念,早在小学一年级甚至幼儿园,我们就接触过了。那时候我们常见的操作符无非:+、—,X,÷,现如今我们学习了计算机,无非在原来的基础上拓展一些个例子。下面依依列举出来一系列操作符。

②算术操作符:+ - * / %

特殊: 其中除了%外其余操作符既适用于浮点型,又适用于整数类型。(%只适用于整形取余)。其中/的两个操作数都是整型的时候它执行整形操作数,别的情况执行浮点型触发。

③移位操作符 << >>

用处: 将数值向左向右移动,对于十进制来说就是实现放大十倍和缩小十倍的效果,而对于二进制而言就是放大两倍和缩小两倍的效果。
逻辑移位和算术移位: 逻辑移位不考虑符号位,而算术因为我们的数有正负之分所以我们要考虑符号位。可参考TuneG的博客。算术左移和逻辑左移的效果是一样的,效果都是2,逻辑右移最高位补0,算术右移最高位补符号位。下图就是一个左移动三位的操作,这里算术,逻辑移位都一样的。如果值为10010110的数右移动两位,逻辑右移:00100101,算术右移动(补符号位):*11100101
在这里插入图片描述

总结: 就这个负数的右移比较特殊,别的移动空出来的位置都补0,负数右移空出来的位置补1。当然这里要注意一个点:负数的二进制形式不妨我们下面再回顾一下。有符号数的右移操作是不可移植的。
负数的二进制形式: 可参考这篇博客:storm_fury的博客
正数是直接用原码表示的,如单字节5,在计算机中就表示为:0000 0101。
负数以其正值的补码形式表示,如单字节-5,在计算机中表示为1111 1011。
警告⚠: 那么如何确定是逻辑移位还是算术移位呢?标准规定:无符号数的所有操作都是逻辑移位,有符号数的话根据编译器来确定。可以编写简单测试程序,看看自己编译器使用哪一种移位。
应用: 计算二进制无符号数中有多少个‘1’

for(int i=0;num!=0;num=num>>1)//num为此无符号数,cout为计数器(计算有多少个1)
{
   
   
    if(num%2!=0)//末尾不是0,那么取余!=0,那么该位就是1。
        count++;
}

④位操作符 & | ^

初识: 这个小学可能没有接触过,但是读了高中之后我们也有接触过,其实也是相应简单的规则。
在这里插入图片描述
应用: 我们经常用位操作+移位操作符 来控制特定位的值的时候。下面我们通过例子理解:也可以参考wewooseo的博客
把指定位置设置为1: (假设指定位置为bit)

value= value | 1 << bit;

把指定位置清0:

value= value & ~(1 << bit);//~优先级比&高

把指定位置取反:

value=value^(1<<bit);

⑤赋值操作符 =

初识: 这个操作符应该也是我们从小见到大的。
理解: 赋值操作符就是把右操作数的值,存储于左操作数指定的位置。但是这个赋值也是一个表达式,表达式就具有一个值。赋值表达式的值就是左操作数的新值。结合性是:R-L
复合赋值符: += -= *= /= %= <<= >>= &= ^= |=
举个例子:
a+=expression <==>a=a+expression
这种复合赋值在面临较长的语句时候,更不容易敲错。且如果有下标表达式的时候,只需要计算一次下标,这是一种更高效的形式。

⑥单目操作符 ++ -- & sizeof ~ - + * (类型)

! 逻辑取反,真变成假,假变成真。这个操作产生一个整形结果:0或者1
- 让操作符产生操作数的负值。
~ 针对 整数类型型 操作求补。整型操作数二进制中原来的1变成0,原来的0变成1。
+ 产生操作数的值,换句话说,什么也不干,为了和成对。
(类型) 操作符被称为强制类型转化,把表示的值转化位其他类型,
& 取地址,可能是某个变量的地址,即在内存中的位置。
* 间接访问,和指针配合使用,用于访问指针所指的值。
sizeof 判断操作数的类型长度,并且操作数既可以是表达式也可以是两边加上括号的类型名
++ -- 自增或者自减。参考isjun26的博客
单独使用时,++或者--无论是放在变量的前面还是后面,结果是一样的。
参与操作时,如果++或者--在变量的后面,先拿变量参与操作,后变量做++或者--。
例如 int a = a++;
如果++或者--在变量的前面,先++或者--,后拿变量参与操作。
例如 int a = ++a;
关于++ --操作:无论是前缀和后缀的增值操作符都复制一份变量的拷贝。前缀操作符在进行复制之前增加变量的值,后缀操作符在进行复制之后增加变量的值。这些操作符的结果不是被他们所修改的变量,而是变量值的拷贝。理解了这点就能解释,为什么不能这样使用操作符了:++a=10; ++a的结果是a值的副本,并不是变量本身,因此无法向一个值进行赋值。
操作符的副作用:
| 副作用(多方面) |side effects,翻译成 “副作用”,不对。翻译成“多方面的作用”。 |
|--|--|
| ()|本身并无任何副作用,但是它调用的函数可能有副作用。 |
|++ -- | 不论是前缀还是后缀形式,这些操作符都会修改它们的操作数;|
| = | 包括所有其他的复合赋值符:它们都修改作为左值的操作数。|

例子: c + --c操作符的优先级规则要求自减运算在加法运算之前进行,但是我们没法得知,+操作符的左操作数实在右操作数之前还是之后求值,所以这点有歧义的。这就是++ --副作用

⑥关系操作符> >= < <= != ==

陷阱: 这些个操作符的功能,我想就算我们在读小学,应该也很好理解。但是也和我们想的不大一样:这些操作符产生的结果都是整型,而并非想象中的布尔型。如果关系成立结果为:1,关系不成立结果为:0。
练习:

int a=20;
if(1<=a<=10)
    printf("U Are Clever");
else
    printf("U Are Foolish");

答案: “U Are Clever” 才是正经答案,理由如下:这个if语句中先判断 1 <= a,结果作为真,再判断 1 <= 10,结果为真。

⑦逻辑操作符 && ||

区分: 逻辑操作符和位操作符:它们看起来很像,具体操作上大相径庭。在expression1 && expression2中,两个都为真则真,其中一个为假,那么表达式便为假。
短路求值: &&||操作符的操作数表达式进行求值时,只要最终的结果已经可以确定是真或假,求值过程便告终止。&&操作符的左操作数总是首先进行求值,如果它的值为真,然后就紧接着对右操作数进行求值。如果左操作数的值为假,那么右操作数便不再进行求值,因为整个表达式的值肯定是假的,右操作数的值已无关紧要。||操作符也具有相同的特点,它首先对左操作数进行求值,如果它的值是真,右操作数便不再求值,因为整个表达式的值此时已经确定。
练习: 可以被4整除的年份是闰年,但是其中能够被100整除的年份又不是闰年。但是,这其中能够被400整除的年份又是闰年。请用一条赋值语句,如果变量year的值是闰年,把变量leap year 设置为真。如果year的值不是闰年,把leap year设置为假。

leap_year = (year % 400 ==0 || year % 4 ==0 && year % 100 != 0);

思考: 如果换了顺序会怎么样?

⑧条件操作符

关键点: expression1 ? expression2 : expression3 接受3个操作数,它的作用也是控制子表达式的求值顺序。条件操作符的优先级非常低,所以就算各个操作数不加括号也没有问题。、
例子:

if(a>5)
    b=3;
else
    b=20;

改写成:

b=a >5?3 :20;

优势: 在某些需要书写很长的表达式中,减少代码量和书写错误的概率。
思考: 使用条件表达式操作符是不是效率更高呢?并没有,你可能回想,使用if语句中不是有两条赋值语句吗?其实虽然它有两条赋值语句,但是它只执行一次。

⑨逗号操作符

用法: expression1 , expression2 , expression3
用处: 讲真的,这个操作符貌似是一个小透明,不过它还真是一个操作符,有些时候用于把两条赋值语句整合成一句,从而避免了在它们的两端加括号,但是大多数都是多此一举。

⑩下标引用、函数调用、结构体

这些在后续章节(博客)中都会详细介绍,在这里只是标记一下,它们也是,操作符。

二、布尔值(operator)

①规则:

C不具备显示的布尔类型,所以用整数来代替:零是假,任何非零是真

②易混淆:

我们经常见到这样的定义:

#define FALSE 0
#define TRUE  1

其实说实在,我一直没搞懂,这样define的好处是什么,希望后续章节能弄清楚。
但是这样存在着一些容易混淆的地方:

#define FALSE 0
#define TRUE  1if (flag==FALSE)if (!flag)if (flag== TRUE)if (flag)

如果flag是一个任意的整型值,那么这里的③④就是不等价的,第③句判断这个句子是否为TRUE,第而这里的TRUE又是1,所以当flag不为1时候,得到的结果就是假。④句判断这个句子是否为非0,如果flag不为1,假设是2,那么的到的结果是真。所以当整型和布尔型混用的时候容易出现混淆

③解决混淆

避免混合使用布尔型和整型。 如果一个变量包含一个任意的整型值,就应该显示的对它进行测试,(如果不是布尔类型地就不要用简写。)当然:不要使用简写法来测试变量是零还是非零,因为这类形式错误地暗示该变量在本质上是布尔型。

if(value!=0)

当然如果这个值本身就是布尔类型地,那么用简写的方式来测试:

if(value)
if(!value)

三、左值和右值

①简明定义:

左值就是 = 左边的值,而右值就是 = 右边的值。

②左值和右值的区别:

a=b+25 其中a是左值,因为它标记了一个可以存储结果值的地址,b+25是右值,因为它指定了一个值。那能写成b+25=a 的形式吗?b+25不能作为左值,因为它并未标识一个特定的位置,因此这个赋值语句是非法的。 当然b+25自然存储在内存的某个地方,但是我们并没有办法直到会存储在什么地方,也无法保证,b+25=a每次都存储在一个地方。
判断题: 变量可以作为左值而表达式不能作为左值? (X)理由:int a[30]; a[b+10]=0这个例子中 b+10是表达式,但它确实合法的,根本原因:b+10解引用使得它识别了一个特定得位置int a,*pi; pi=&a;*pi=20;pi=&a的左值是一个表达式,但是它却是合法的,其中根本原因:在于pi的值是内存中某个特定的地址,*操作符使得机器指向那个位置。

③总结:

左值意味着一个位置,右值以为着一个值。
区分能否作为左值的关键点在于是否能在计算机的内存中找到一个合理、确定的位置。
左值是个表达式,它可以出现在赋值符的左边,表示计算机内存中的一个位置。右值表示一个值,所以它只能出现在赋值符的右边,每个左值表达式同时也是个右值表达,但反过来不是这样的。

四、表达式求值

①隐式类型转化

定义: 隐式类型转换顾名思义就是偷偷的进行转换。这种类型转换所做的动作我们平时都不会注意到,因为他是偷偷的发生的。
整型提升: 的C整型算术运算总是至少以默认整型类型的精度来进行。为了获得这个精度,表达式中的(字符型、短整型)操作数在使用之前被转化为普通整型。我的理解: 在我们计算过程中,我们发现操作数达不到标准的整型大小时,就整型提升,变成标准整型再运算。(高位是符号位)参考叫嚣的泡芙的博客
例子: (设32位计算机)

int main()
{
   
   
    char a=3;
    //                             00000011    (整型提升前)
    //00000000 00000000 00000000 00000011    (整型提升后)
    char b=127;
    //                             01111111    (整型提升前)
    //00000000 00000000 00000000 01111111    (整型提升后)
    char c=a+b;
    //00000000 00000000 00000000 00000011    (a)
    //00000000 00000000 00000000 01111111    (b)
    //现在要将结果放到c中,而c的类型又是字符型,又要发生截断
    //                             10000010    (c=a+b)
    //此时还不能直接打印输出,因为printf函数中是以%d的形式进行打印。又要对c进行整型提升
    //c的类型是有符号字符型,最高位1为他的符号位。高位通通补1,补全32个比特位
    printf("%d",c);
    //11111111 11111111 11111111 10000010    (在内存中是补码)
    //11111111 11111111 11111111 10000001    (反码)
    //10000000 00000000 00000000 11111110    (原码)打印出来是-126
    }

整型提升的意义: 表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度一般就是int的字节长度,同时也是CPU的通用寄存器的长度。因此,即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长度。通用CPU( general-purpose CPU)是难以直接实现两个8比特字节直接相加运算(虽然机器指令中可能有这种字节相加指令)。所以,表达式中各种长度可能小于int长度的整型值,都必须先转换为int或unsigned int,然后才能送入CPU去执行运算。

②算术转化

定义: 算术转化:如果某个操作符的各个操作数属于不通过的类型,那么除非其中一个操作数转换为另外一个操作数的类型,否则操作就无法进行。参考观赏的博客
下面的层次体系称为寻常算术转换(usual arithmetic conversion)

long double
double
float
unsigned long int
unsigned int
int

如果任一操作数是 long double 类型,则将另一个操作数转换为 long double 类型。
如果未满足上述条件,并且任一操作数是 double 类型,则将另一个操作数转换为 double 类型
.....
如果未满足上述任何条件,则将两个操作数转换为 int 类型。
一句话总结: 如果某个操作数的类型在上面这个列表中排名较低,那么它首先将转换成另外一个操作数的类型然后执行

例子:

int a=20000;
int b=25;
long c=a*b;

表达式a*b是以整型计算的,在16位机器上,这个乘法会产生一处,这样c就会被初识话为错误的值。
解决办法:long c= (long) a * b
注意:float类型转化为整型的时候也可能会损失精度,float型值要求6位数组的精度,如果将一个超过6位的整型赋值给一个float型变量,其结果可能是只能得到该整型的近似值。当float转化为
整型时,小数部分被舍弃,如果浮点型的值过于庞大,无法容纳整型值,那么其结果是未定义的。

③操作符的属性(3大属性)

优先级: 两个相邻的优先级那个先执行取决于它们的优先级,如果两者的优先级相同,那么它们的执行顺序又结合性决定。
结合性: 一串操作符是从左向右还是依次执行还是从右向左依次执行。
是否控制执行顺序: 有4个操作符可以对整个表达式求值顺序施加控制,它们或者保证某个子表达式能够在宁外一个子表达式的所有求值过程完成之前,进行求值,或者可能使某个表达式被完全跳过不再求值。
操作符优先级表:
用法实例:提示它是否需要操作符为左值。lexp rexp表示左值表达式和右值表达式,左值意味着一个位置,右值意味着一个值。 用右值的地方可以用左值,但是需要左值的地方不能使用右值。
在这里插入图片描述
在这里插入图片描述

④优先级和求值的顺序

规则: 两个相邻操作符的执行顺序由它们的优先级决定。如果它们的优先级相同,则指向顺序由它们的结合性决定。换句话说:操作符的优先级只决定表达式的各个组成部分在求值过程中如何聚组的。

a*b+c*d+e*f

这个式子中,只要保证每个乘法运算在相邻的加法运算之前即可:

a*b
c*d
(a*b)+(c*d)
e*f
(a*b)+(c*d)+(e*f)

警告: 由于表达式的求值顺序并非完全由操作符决定,因此这种语句很危险:
c + --c操作符的优先级规则要求自减运算在加法运算之前进行,但是我们没法得知,+操作符的左操作数实在右操作数之前还是之后求值,所以这点有歧义的。这就是++ --副作用
例子:

f()+g()+h()

左边的加法先执行,但是各个函数的调用顺序并没有加以限制。如果它们的执行具有副作用,比如执行一些I/O任务或者修改全局变量,那么函数调用顺序的不同可能会产生不同的结果。因此,如果顺序会导致结果产生区别,则最好使用,临时变量让每个函数调用都在单词的语句中进行。

temp=f();
temp+=g();
temp+=h();

五、总结(搬运)

在这里插入图片描述
在这里插入图片描述

相关文章
|
4天前
|
C语言
C语言:数组和指针笔试题解析(包括一些容易混淆的指针题目)
C语言:数组和指针笔试题解析(包括一些容易混淆的指针题目)
|
4天前
|
存储 人工智能
指针的应用练习(数组与指针的关系)
指针的应用练习(数组与指针的关系)
12 0
|
4天前
|
存储 人工智能
字符指针变量和字符数组注意事项(区别)
字符指针变量和字符数组注意事项(区别)
7 0
|
4天前
指针的基础应用(数组的颠倒和排序,二维数组的表示)
指针的基础应用(数组的颠倒和排序,二维数组的表示)
7 0
数组指针、函数指针、指针数组、函数 指针数组、指针函数详细总结
数组指针、函数指针、指针数组、函数 指针数组、指针函数详细总结
|
4天前
指针指向数组
指针指向数组
17 0
|
4天前
|
存储 人工智能 C++
【重学C++】【指针】详解让人迷茫的指针数组和数组指针
【重学C++】【指针】详解让人迷茫的指针数组和数组指针
34 1
|
4天前
|
存储 程序员 编译器
爱上C语言:指针很难?来来来,看看这篇(基础篇)
爱上C语言:指针很难?来来来,看看这篇(基础篇)
|
4天前
|
C语言
c语言指针总结
c语言指针总结
15 1
|
2天前
|
存储 安全 编译器
C语言详解指针(指针海洋的探索,将传值与传址刻在心里)
C语言详解指针(指针海洋的探索,将传值与传址刻在心里)
5 0

热门文章

最新文章