数组——参考《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();

五、总结(搬运)

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

相关文章
|
1月前
使用指针访问数组元素
【10月更文挑战第30天】使用指针访问数组元素。
38 3
|
29天前
|
存储 程序员 编译器
C 语言数组与指针的深度剖析与应用
在C语言中,数组与指针是核心概念,二者既独立又紧密相连。数组是在连续内存中存储相同类型数据的结构,而指针则存储内存地址,二者结合可在数据处理、函数传参等方面发挥巨大作用。掌握它们的特性和关系,对于优化程序性能、灵活处理数据结构至关重要。
|
1月前
|
存储 C语言 计算机视觉
在C语言中指针数组和数组指针在动态内存分配中的应用
在C语言中,指针数组和数组指针均可用于动态内存分配。指针数组是数组的每个元素都是指针,可用于指向多个动态分配的内存块;数组指针则指向一个数组,可动态分配和管理大型数据结构。两者结合使用,灵活高效地管理内存。
|
1月前
|
容器
在使用指针数组进行动态内存分配时,如何避免内存泄漏
在使用指针数组进行动态内存分配时,避免内存泄漏的关键在于确保每个分配的内存块都能被正确释放。具体做法包括:1. 分配后立即检查是否成功;2. 使用完成后及时释放内存;3. 避免重复释放同一内存地址;4. 尽量使用智能指针或容器类管理内存。
|
1月前
|
存储 NoSQL 编译器
C 语言中指针数组与数组指针的辨析与应用
在C语言中,指针数组和数组指针是两个容易混淆但用途不同的概念。指针数组是一个数组,其元素是指针类型;而数组指针是指向数组的指针。两者在声明、使用及内存布局上各有特点,正确理解它们有助于更高效地编程。
|
1月前
|
存储 人工智能 算法
数据结构实验之C 语言的函数数组指针结构体知识
本实验旨在复习C语言中的函数、数组、指针、结构体与共用体等核心概念,并通过具体编程任务加深理解。任务包括输出100以内所有素数、逆序排列一维数组、查找二维数组中的鞍点、利用指针输出二维数组元素,以及使用结构体和共用体处理教师与学生信息。每个任务不仅强化了基本语法的应用,还涉及到了算法逻辑的设计与优化。实验结果显示,学生能够有效掌握并运用这些知识完成指定任务。
60 4
|
1月前
使用指针访问数组元素
【10月更文挑战第31天】使用指针访问数组元素。
50 2
|
1月前
|
存储 C语言
C语言如何使用结构体和指针来操作动态分配的内存
在C语言中,通过定义结构体并使用指向该结构体的指针,可以对动态分配的内存进行操作。首先利用 `malloc` 或 `calloc` 分配内存,然后通过指针访问和修改结构体成员,最后用 `free` 释放内存,实现资源的有效管理。
129 13
|
2月前
|
C语言
无头链表二级指针方式实现(C语言描述)
本文介绍了如何在C语言中使用二级指针实现无头链表,并提供了创建节点、插入、删除、查找、销毁链表等操作的函数实现,以及一个示例程序来演示这些操作。
38 0
|
3月前
|
存储 人工智能 C语言
C语言程序设计核心详解 第八章 指针超详细讲解_指针变量_二维数组指针_指向字符串指针
本文详细讲解了C语言中的指针,包括指针变量的定义与引用、指向数组及字符串的指针变量等。首先介绍了指针变量的基本概念和定义格式,随后通过多个示例展示了如何使用指针变量来操作普通变量、数组和字符串。文章还深入探讨了指向函数的指针变量以及指针数组的概念,并解释了空指针的意义和使用场景。通过丰富的代码示例和图形化展示,帮助读者更好地理解和掌握C语言中的指针知识。
144 4