[深入浅出C语言]左值右值以及其他操作符

177 阅读11分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第25天,点击查看活动详情

前言

        本文就来分享一波作者对操作符的学习心得与见解,主要介绍左值右值和其余操作符。

        笔者水平有限,难免存在纰漏,欢迎指正交流。

操作符

赋值操作符

单独赋值

        赋值操作符就是将右边操作对象的值赋给左边的。

int a, x, y = 3;
a = 8;
x = a;
//连续赋值
a = x = y+1;
//不推荐,因为可读性较差且不易调试。

复合赋值

int x = 10;
x = x + 10;
x += 10;//复合赋值,将两个表达式复合在一起

        其他运算符一样的道理,比如-=、*=、/=、%=、|=、&=、^=等等,这样写更加简洁。

左值、右值与数据对象

        2022 = val;

        这样的语句在C语言中是无意义的,因为2022只是字面常量(所见即所得的值),而常量是不可以被赋值,常量本身就是它的值。

        实际上,赋值运算符的左侧必须引用一个存储单元,最简单的方法就是使用变量名(变量名标识特定存储单元),还可以使用指针(指向一个存储单元)。总的来说,C语言使用可修改的左值标记那些可赋值的实体。

        用于存储值的数据存储区域被统称为数据对象,不过要注意此对象非彼对象,这和面向对象编程的对象不是一回事,这里所谓数据对象简单来说就是单纯的存储单元。

        左值是用于标识特定数据对象的名称或表达式,重点在于它能引用一个存储单元,能对该存储单元进行访问和修改,这才是实体。可以这么说:数据对象指的是实际的数据存储,而左值则是用于表示或定位存储单元的标签。

image.png

        早期C语言中提到左值意味着:它指定一个数据对象并且可用在赋值运算符的左侧。

        但是由于const修饰符的出现,就需要对左值概念进行修改,因为用const修饰的变量创建后就不可被修改,该类变量仅能满足“指定一个数据对象”,但是不满足“可用在赋值运算符的左侧”。

        一方面C继续把表示对象的名称或表达式定义为左,另一方面新增了一个术语:可修改的左值

        可修改的左值用来标识可修改的数据对象,所以赋值运算符左侧的应该是可修改的左值。而它还有另一个名字:对象定位值,其实这个更形象和具体一点。

image.png

        你可能会问,既然有左值,那是否存在右值呢?当然,右值指的是能赋值给可修改的左值的量,且本身不是左值。比如:int val = 2022

        但是要注意的是,不要想当然地以为赋值运算符右侧的就是右值,右值其实更应该称为表达式的值

比如:

int length = 777;
length *= 2;
int newLength = length;

        这里的length在赋值运算符右侧了,但是它是右值吗?不是!它本身就已经是左值了,不能仅凭在赋值运算符左侧或右侧来判断该量是左值还是右值。

示例:

int a = 1;
int b;
int c;
const int d = 2;

b = a;
c = a * b + 2;

        在这里,a,b,c都是可修改的左值,它们可用于赋值运算符的左侧和右侧,而d是不可修改的左值,它仅能用于赋值运算符的右侧,不过这里给它值是初始化而非赋值。

        表达式a * b + 2是右值,该表达式不能表示特定内存位置,而且也不能给它赋值,仅仅只是程序计算的一个临时值,计算完毕后便会被丢弃。

单目操作符

!  逻辑反操作 对一个值逻辑取反,根据C语言非零为真零为假,比如!8=0

-  负值    对一个数取负数

+  正值    实际上对一个数没什么影响,比如a = -10; b = +a;算出来b的值还是-10

&  取地址  取出一个变量的地址

sizeof  操作数的类型长度(以字节为单位)

~  对一个数的二进制按位取反  比如~65,65补码(省略前面的24位0):01000001,按位取反得到10111110,再转成原码得到11000010即-66

--  前置、后置--

++  前置、后置++

*  间接访问操作符(解引用操作符)

(类型)  强制类型转换

sizeof()

        sizeof运算符以字节为单位返回运算对象的大小,返回值时无符号整数类型,运算对象可以是具体的数据对象(如变量名),也可以是类型(如int),如果是类型就必须用圆括号()括起来。

        siezof()是函数么?不是!它是关键字,在编译期间起作用,而函数是程序运行起来后才起作用的。

()里的操作数可以直接是一个变量,如:

int a = 10;
printf("%d\n", sizeof(a));

也可以是一个常数,如:

 printf("%d\n", sizeof(10));

会根据该常数对应类型来计算内存大小

还可以是类型关键字,如:

 printf("%d\n", sizeof(int));

        但要注意的是,这里计算的肯定不可能是int的内存,int只是规定的关键字,不是变量,压根没有存在内存中,那这里是什么意思呢?就是计算int类型的变量在内存所占用空间大小。

        等会,不是说计算变量的内存大小吗,那为什么常数的也可以求?实际上是根据操作数的类型来计算该类型的变量所占内存空间大小,你放入一个常数比如10到sizeof()里,它会根据10对应数据类型int来计算int类型的变量大小。

 printf("%d\n", sizeof a);//这样写行不行?

        可以,结果不影响。

printf("%d\n", sizeof int);//这样写行不行?

        不行,会出问题,需要括号把类型括起来。

看看sizeof()和数组以及函数传参

#include <stdio.h>
void test1(int arr[])
{
   printf("%d\n", sizeof(arr));//(2)
}

void test2(char ch[])
{
   printf("%d\n", sizeof(ch));//(4)
}

int main()
{
   int arr[10] = {0};
   char ch[10] = {0};
   printf("%d\n", sizeof(arr));//(1)
   printf("%d\n", sizeof(ch));//(3)
   test1(arr);
   test2(ch);
   return 0;
}

问:

(1)、(2)两个地方分别输出多少?

(3)、(4)两个地方分别输出多少?

        一个int四个字节,数组arr带有10个int就是40个字节,一个char一个字节,ch数组大小就是10个字节。

        函数传参传数组名实际上传的是数组首元素地址,而函数形参看起来是数组,但编译器会把它看成对应类型的指针,这时候再使用sizeof计算的就是指针大小,32位系统下指针大小4个字节,64位系统下指针大小8个字节。

初识自增++或自减--

        关于++和--,要分前置和后置的情况

        后置的话先使用后自增

        前置的话先自增后使用

        这里讲的使用是作为表达式去使用,其实这样理解还较为浅显,后面会出文章来深入分析。

        在数组中,比如arr[r++] = 10;,就是arr[r] = 10;r++;,而arr[++r] = 10;就是r++;arr[r] = 10;

        在函数调用中,比如add(r++);就是add(r);r++;,而add(++r);就是r++;add(r);

逻辑操作符

&&    逻辑与   两边操作数(运算对象)都为真表达式值才为真

||    逻辑或   两边操作数(运算对象)有一个为真表达式值即为真

逻辑与和按位与

逻辑与:左右操作数都为真(非0),表达式的值才为真(为1),否则为假(为0)

按位与:左右操作数的二进制位补码一一进行“全为1则为1,否则为0”的操作

逻辑或和按位或

逻辑或:左右操作数有一个为真(非0),表达式的值就为真(为1),否则为假(为0)

按位或:左右操作数的二进制位补码一一进行“有一个为1则为1,否则为0”的操作

        其实感觉比较相像,可以这么看,逻辑与和逻辑或是对于数值本身进行逻辑真假的判断,而按位与和按位或是对于数值的二进制位进行逻辑真假的判断,逻辑真假即非0与0。

典题剖析

 程序输出的结果是什么?

#include <stdio.h>

int main()
{
    int i = 0;
    int a=0, b=2, c =3, d=4;
    i = a++ && ++b && d++;

    printf("a = %d\n b = %d\n c = %d\nd = %d\n", a, b, c, d);
    return 0;

}

其实上面故意少提了一点:

        对于逻辑与和逻辑或,都是左结合性,逻辑与表达式要是第一个表达式值为0则整个表达式的值就为0,后面的表达式可以不用计算,相似地,逻辑或表达式要是第一个表达式值为1则整个表达式的值就为1,后面的表达式可以不用计算。

        再回到上面的例题,注意a++是后置++,所以(a++)&&…相当于a&&…再a++,而a的值为0,为假,则不管++b是否为真,a++&&++b都为假,则无论d++是否为真,整个逻辑与表达式的值都为假,后面的++b和d++表达式就不会再计算了,所以只有a自增了1,程序输出结果就为1 2 3 4。

如果稍微修改一下这段代码结果会怎样呢?看看:

程序输出的结果是什么?

#include <stdio.h>

int main()
{
    int i = 0;
    int a=1, b=2, c =3, d=4;
    i = a++ && ++b && d++;

    printf("a = %d\n b = %d\n c = %d\nd = %d\n", a, b, c, d);
    return 0;
}

        答案是2 3 3 5

        因为这次a为非0,后面的++b和d++都被执行了。

那我们再来看看逻辑或表达式:

程序输出的结果是什么?

#include <stdio.h>

int main()
{
    int i = 0;
    int a=1, b=2, c =3, d=4;
    i = a++ || ++b || d++;

    printf("a = %d\n b = %d\n c = %d\nd = %d\n", a, b, c, d);
    return 0;
}

        注意a为非0,不管++b是否为真,a++||++b都为真,则无论d++是否为真,整个逻辑或表达式都为真,所以后面的表达式不会再计算,只有a自增了1,输出结果就是2 2 3 5。

我们再改改看看:

程序输出的结果是什么?

#include <stdio.h>

int main()
{
    int i = 0;
    int a=0, b=2, c =3, d=4;
    i = a++||++b || d++;

    printf("a = %d\n b = %d\n c = %d\nd = %d\n", a, b, c, d);
    return 0;
}

        注意a为0,而++b非0,则a++||++b为真,所以无论d++是否为真,整个逻辑或表达式都为真,d++也就不会计算了,故输出结果为1 3 3 4。

条件操作符

exp1 ? exp2 : exp3

        如果exp1为真,则整个表达式的值为exp2的值,而exp3不执行;

        如果exp1为假,则整个表达式的值为exp3的值,而exp2不执行。

        要注意的就是不宜把表达式搞得太复杂,可以换成用if…else语句。

逗号操作符

exp1, exp2, exp3, …expN

        逗号表达式,从左向右依次执行。整个表达式的结果是最后一个表达式的结果。

        不过由于逗号的运算优先级是最低的,使用逗号表达式要使用圆括号。

//代码1
int a = 1;
int b = 2;
int c = (a>b, a=b+10, a, b=a+1);

//代码2
if (a =b + 1, c=a / 2, d > 0)//判断条件也可以是逗号表达式

       不用圆括号括起来的话,很容易出错!

比如:

        int d = ++a, b++, c++, a++;实际上是先执行赋值表达式(d = ++a),而不是将后面的逗号表达式都执行完取最后一个的值,那是有圆括号的情况,因为逗号运算符优先级最低啊。

[ ] 下标引用操作符

操作数: 一个数组名 + 一个索引值

比如:

        arr[9]的操作数是arr和9

骚操作: 9[arr] 等价于 arr[9]

        我直呼好家伙 Σ(っ °Д °;)っ

        为什么可以这样呢?

        究其本质,arr[9]等价于*(arr + 9),实际上[ ]就是*( )操作数放在圆括号里相加,所以9[arr] 等价于 *(9+arr),而加法具有交换律,这两种表示方式的意义都一样。

( ) 函数调用操作符

        接受一个或者多个操作数:第一个操作数是函数名,剩余的操作数就是传递给函数的参数。

#include <stdio.h>

 void test1()
 {
     printf("hehe\n");
 }

 void test2(const char *str)
 {
     printf("%s\n", str);
 }

 int main()
 {
     test1();           
     test2("hello bit.");
     return 0;
 }

        所以说函数调用其实也是表达式语句,操作符为()。

访问结构成员的操作符

. 结构体.成员名

-> 结构体指针->成员名

#include <stdio.h>

struct Stu
{
   char name[10];
   int age;
   char sex[5];
   double score;
};

int main()
{
   struct Stu stu;
   struct Stu* pStu = &stu;
   stu.age = 20;//直接访问结构成员
   pStu->age = 20;//间接访问结构成员
   return 0;

}

以上就是本文全部内容,感谢观看,你的支持就是对我最大的鼓励~

src=http___c-ssl.duitang.com_uploads_item_201708_07_20170807082850_kGsQF.thumb.400_0.gif&refer=http___c-ssl.duitang.gif