C语言--CH06--操作符(下)

C语言–CH06–操作符(下)

四、赋值操作符

1、 赋值和初始化的区别

赋值和初始化有显著的区别

int a = 10; //这是初始化
a = 20; //这是赋值
2、连续赋值

赋值是一种从左往右的运算,并且可以连续赋值:

int a = 0;
int b = 10;
int c = 20;
a = b = c+1;
printf("%d\n",a);

猜一猜a此时的输出为几呢?

推演:

因为赋值运算符是从左往右计算的,所以,首先计算c+1,并把c+1的值赋给b
,之后再把b的值赋给a。即把21的值赋给b,再赋给a。

*赋值运算的连续计算关键记住从左往右的原则

故我们推测输出结果为21。实际运行一下输出为:

21

警告⚠:我们不建议这样写代码,这样写不利于调试,并且可读性差。

3、复合赋值符
+=          %=
-=          &=
*=          |=
/=          >>=
<<=         ^=

相信大家都对这些符号很熟悉了,本文只以+=为例,其他的符号类比

a = a + 10;
a += 10;//两者等价

两者的含义一样,下面更简短。

五、单目操作符

单目操作符即只有一个操作数的操作符,双目操作符有两个操作数
日常所用单目操作符有:

!          //逻辑反
-          //负值
+          //正值
&          //取地址
sizeof     //算操作数的类型长度
~          //对一个数的二进制按位取反
++         //++a,a++
--         //--a,a--
*          //解引用操作符
(类型)	   //强制类型转换
1、!

早期C语言没有bool类型,可以用!来表示真假。我们已经知道,0为假,
非零为真。我们可以写这样一行代码

#include
int main()
{
	int a = 3;//a不为0,即a为真
	if(a)
	{
		printf("你是个好人\n");
	}
	if(!a)
	{
		printf("你不是个好人\n");
	}
	return 0;
}

这段代码是什么意思呢?一旦a非零,则打印你是个好人,如果a为0,
则a为假,则!a为真,进入第二个if,打印你不是个好人,一次达到bool
类型相同的作用。

2、+ -

这两个符号没什么实际意义。但是还是要讲一下。

int a = -10;
int b = +a;

请问:b = 10还是b = -10?
答案是:-10。
+a并不能改变a的值。

3、取地址操作符&

&是取地址操作符,他的作用就是取出一个变量在内存中的地址。
一般是用一个指针变量来储存地址。

int a = 10;
int* p = &a;
printf("%p",p);
printf("%p",&a);//%p是打印地址时使用

注意:a是一个整型变量,占四个字节,&a取的是第一个字节的地址。
之后的指针环节会详细介绍。

4、sizeof

sizeof有两种用法:(单位是字节Byte)

int a = 10;
int c = sizeof(a);
int n = sizeof(int);
printf("%d \n %d",c,a);
1、计算一个变量所占内存的大小:sizeof(a)
2、计算类型所创建变量所占内存大小:sizeof(int)
3、计算数组的大小

sizeof(数组名)计算的是整个数组的大小

#include
int main()
{
	int arr[5] = {0};
	printf("%d\n",sizeof(arr));
	
	return 0;
}

输出:

20

整型数组,共5个元素,共20个字节。

sizeof与strlen()有区别:
sizeof不是函数,而是一个单目操作符。能适用于各种类型。
strlen()是函数,只能用来求字符串的长度。

5、二进制按位取反 ~

什么是按位取反?
按位取反即按二进制位取反,是一个整数的补码某一位上为0则变成1,为1则变成0。

如:3的补码为:
00000000000000000000000000000011
按位取反之后为:
11111111111111111111111111111100
因为取反之后也为补码,但是打印出来的是原码,所以类似上一章所讲的:
反码:
11111111111111111111111111111011
原码:
10000000000000000000000000000100
整数:
-4
即~3 = -4

我们来验证一下:

int a = 3;
int c = ~a;
printf("%d\n",c);

输出:

-4

大招:如何把一个二进制的随便一位由0变成1,再把它变回来呢?
我们需要四个武器:按位或|、按位与&、按位取反~、对于数字1的运用。

a = 13
a的补码为:
00000000000000000000000000001101
比如我想要把a的补码中倒数第五位变成1:
00000000000000000000000000011101
怎么做呢?
我们邀请嘉宾————— 1 
1的二进制补码为:
00000000000000000000000000000001
如果1<<4:
00000000000000000000000000010000
再将a按位或(1<<4):
00000000000000000000000000011101
实现了我们想要的操作。此时整数输出为:29

那我们想要变回去怎么办呢?还记得(1<<4)等于多少吗?
00000000000000000000000000010000
如果对(1<<4)按位取反~(1<<4):得到:
11111111111111111111111111101111
再使29按位与~(1<<4):
00000000000000000000000000001101
又回到了13。

我们来验证一下:

#include
int main()
{
	int a = 13;
	a |= (1<<4);
	int b = a;
	a &= (~(1<<4));
	int c = a;
	printf("%d\n%d",b,c);
	return 0;
}

输出:

29
13
6、++c和c++

直接看例子

int a = 12;
int b = ++a;
printf("%d\n%d",a,b);

输出:

13
13
int a = 12;
int b = a++;
printf("%d\n%d",a,b);

输出

12
13

++a叫做前置++,他的特点是先加后使用。
这里的意思是,先对a进行操作,a+1 = 12 + 1 = 13。
把13这个值给a,再把a的值给b。

a = a + 1
b = a

a++叫做后置++,他的特点是先使用,再加。
这里的意思是,先把a的值给b,即b= 12,再执行b++,即b+1。所以b = 13

b = a
a = a + 1

另一比较难得例子:

#include
void test(int n)
{
	int b = n+1;
	printf("%d\n",b);
}


int main()
{
	int a = 13;
	int c = 13;
	test(a++);
	test(++c);
	return 0;
}

输出:

14
15

因为test(a++)传的参是a,不是a++。test(++c)传的是c = c + 1。

7、解引用操作符 *

首先看一段代码:

int a = 10;
int* p = &a;
*p = 20;
printf("%d\n",a);

输出:

20

什么意思呢?这里的p是一个指针变量。他相当于a的地址。
而*p相当于访问a地址里的值,即a的值。

*p = 20;就相当于一个指针p,指向了一个房子,*相当于房子的钥匙
*p操作相当于“找到一个房子,并且用钥匙打开门”这一个动作。这个
时候就可以随意更改房子里的东西 。

之后的指针会更详细地讲解。

8、强制类型转换

小学三年级以上都知道3.14是个浮点数。但是一个小学二年级学生写了这样一行抽象的代码:

int a = 3.14;

整型变量怎么能存储浮点数呢?
这个时候,热心大哥哥XXXGOASTDIE决定帮助这个小学二年级学生:我帮他多打了两个符号:

int a = (int)3.14;

这样就变成了一个合法的代码。这种操作叫强制类型转换。即系统默认的double类型3.14被我强制转化为了3.14。这就是强制类型转换。

我们之前学到过生成随机数地函数:

#include
#include
srand((unsigned int)time(NULL));

就用到过强制类型转换。

六、关系操作符

关系操作符很简单,没什么可讲的

>
<
>=
<=
==
!=

只需要注意:
1、==和=别搞混淆了!!!
2、不是什么都可以比较

"abc"== "abcde";

这里比较的只是首元素的地址。
两个字符串是否相等应该用string.h库里的strcmp()来比较。

3、老手都应该注意到if(i==1) if(1 == i)的区别,该链接里有讲到:
if(i==1)和if(1==i)的区别

七、逻辑运算符

&&    逻辑与
||    逻辑或

逻辑与逻辑或和按位与按位或有什么区别?按位与按位或是在二进制层面应用。而逻辑与逻辑或只关注逻辑上的真假。

int a = 5;
int b = 8;
printf("%d\n",a&&b);

输出:

1

因为真值与真值就是真值。

1、法则
a&&b:a和b都为真才真,a和b只要有一个为假就为假。
a||b:a和b只要有一个为真就为真,两个都为假才为假。
2、e.g.判断闰年

闰年如何判断?

能被4整除,且不能被100整除。
或者能被400整除。
#include
void is_leap_year(int year)
{
	if((year%4 == 0)&&(year%100 != 0)||(year%400 == 0))
	{
		printf("这是闰年.\n");
	}
	else{
		printf("这不是闰年\n");
	}
}


int main()
{
	int n = 0;
	printf("请输入年份:\n");
	scanf("%d",&n);
	is_leap_year(n);

	return 0;
}

这段代码就应用到了逻辑与和逻辑或。

3、360的一道面试题:
#include 
int main()
{
	int i = 0,a = 0,b = 2,c = 3,d = 4;
	i = a++ && ++b && d++;
	printf("%d %d %d %d %d",i,a,b,c,d);

	return 0;
}

请问输出为多少?

0 1 2 3 4

为什么呢?看以下解释:

a++是先用a,再++,所以a刚开始等于0,给i的值也是0,0为假,所以0&&任何值都为假,所以
之后的全为假,都不会执行。所以b、c、d的值都不会变。所以输出0 1 2 3 4。

变式:
第一问:a = 1,结果是什么?
第二问:a = 0,i = a++ || ++b || d++,结果是什么?
解析:

第一问:

a = 1,则把1传给了i,并且左边为真,即要执行++b,b变成了3,为真值,即a++&++b为真	
值,故需要执行d++,即d = 5;真值&真值&真值,所以i最终为1。所以输出为:1 2 3 3 5 

第二问:

首先a把0给i,a为假,a再加一,a为假,则执行++b,++b为真,假||真 = 真,则不要执行d++,
故d = 4,总体来看,假||真||真 = 真,故i = 1.所以输出为:
1 1 3 3 4
4、条件操作符(三目操作符)
表达式1 ? 表达式2 :表达式3
  真        ✔        ×
  假        ×        ✔

只要表达式1为真,则执行表达式2,为假则执行表达式3。

e.g.使用条件表达式求两个数的最大值

int max = 0;
int a = 3;
int b= 4;
max = (a>b ? a : b);

八、逗号表达式

exp1,exp2,exp3....expn;

逗号表达式从左向右依次进行,整个表达式结果为最右边表达式的结果。

int a = 2;
int b = 3;
int c = 4;
int d = (a>b,a = b+10,b = a + 1);

请问d等于多少?

a>b没有改变a,b的值,b+10 = 12,给了a,b= a + 1 = 12 +1 = 14。最后一个表达式
的值为14,给了d,所以d = 14

九、下标引用,函数调用和结构成员操作符

1、下标引用操作符[]

就是访问数组下标的操作符。

int arr[10] = {0};
arr[7] = 8;//这里就是下标引用操作符。

[]的操作数,一个是arr,一个是7。为了证明[]是个操作符,我们有如下操作。
我们都知道加法有交换律,那么[]的两个操作数能交换吗?答案是可以。可以自行验证。

7[arr] = 9;//也能达到跟上面代码一样的效果

证明:arr为首元素地址。arr + 7即为第8个元素的地址。所以*(arr +7)就是第8个元素。
所以:

*(arr+7) = arr[7];
*(arr+7) = *(7+arr) = 7[arr];
2、函数调用操作符()

函数调用操作符不可以省略。放在函数后面,表示调用函数

#include 
void Add(int a,int b)
{
	return a + b; 
}


int main()
{
	int a = 10;
	int b = 20;
	Add(a,b);
	return 0;
}
 

利用函数调用操作符不可以省略的性质,我们也可以知道sizeof不是一个函数。
因为sizeof(a)和sizeof a都是合法的,即sizeof可以省略扩号。

函数调用操作符有1+n个操作数:函数名(1个),内部参数(n个),n = 0合法。
如这里的Add(a,b);有操作数:

Add  a  b
3、结构成员操作符

首先我们定义一个结构体,以学生类型为例:

#include
#include
struct Stu
{
	char name;
	int age;
	double score;
};

包括姓名,年龄,学号。
声明一个Stu类型的变量s,初始化为{0}:

int main()
{
	struct Stu s = {0};
}

我想要设置两个函数,一个放s的值,一个打印s,形参为struct Stu n

void set_value(struct Stu n)
{
	strcpy(n.name,"张三");//不能直接n.name = "张三" ,
						 //因为name是一个地址,不能把字符串直接放进地址
	n.age = 20
	n.score = 2022105022.0}


void print_value(struct Stu n)
{
	printf("%s %d %lf\n",n.name,n.age,n.score);
}

之后我需要调用这两个函数,写在main()里。这里我汇总一下:

#include
#include


struct Stu
{
	char name[20];
	int age;
	double score;
};


void set_value(struct Stu n)
{
	strcpy(n.name,"张三");//不能直接n.name = "张三" ,
						 //因为name是一个地址,不能把字符串直接放进地址
	n.age = 20;
	n.score = 2022105022.0;
}


void print_value(struct Stu n)
{
	printf("%s %d %lf\n",n.name,n.age,n.score);
}


int main()
{
	struct Stu s = {0};
	set_value(s);
	print_value(s);
	return 0;
}

输出:

0 0.000000

输出的跟我们想象的不一样,是传参的问题:注意:形参是实参的一次拷贝。我们
定义了一个叫s的Stu类型的变量,开辟了一个空间,创建了实参n,拷贝了形参s,
并在拷贝的区域储存了值,之后返回的值并不是n的而是,s的,此时s的内存里还是
原来的值,所以打印出的都是0。所以我们要取s的地址传进去而不是把s传进去:

set_value(&s);

取地址就需要用指针变量接收,所以我们要把n改成指针变狼,并对set_value内部更改:

void set_value(struct Stu* n)
{
	strcpy((*n).name,"张三");//不能直接n.name = "张三" ,
						 //因为name是一个地址,不能把字符串直接放进地址
	(*n).age = 20;
	(*n).score = 2022105022.0;
}

这样才是正确的。但是这样会不会麻烦了一点,有没有更简单的方法呢?
有:用结构体成员操作符:->

void set_value(struct Stu* n)
{
	strcpy(n -> name,"张三");//不能直接n.name = "张三" ,
						 //因为name是一个地址,不能把字符串直接放进地址
	n -> age = 20;
	n -> score = 2022105022.0;
}

这是个更简单的方法。总结在一起:

#include
#include


struct Stu
{
	char name[20];
	int age;
	double score;
};


void set_value(struct Stu* n)
{
	strcpy(n -> name,"张三");//不能直接n.name = "张三" ,
						 //因为name是一个地址,不能把字符串直接放进地址
	n -> age = 20;
	n -> score = 2022105022.0;
}


void print_value(struct Stu n)
{
	printf("%s %d %lf\n",n.name,n.age,n.score);
}


int main()
{
	struct Stu s = {0};
	set_value(&s);
	print_value(s);
	return 0;
}

输出:

张三 20 2022105022.000000

(*p).age 跟p->age是一样的

当然,我们这里打印的还是n内存里的数据,我们想要直接打印s的数据,可以对print_value做同样的操作:

#include
#include


struct Stu
{
	char name[20];
	int age;
	double score;
};


void set_value(struct Stu* n)
{
	strcpy(n -> name,"张三");//不能直接n.name = "张三" ,
						 //因为name是一个地址,不能把字符串直接放进地址
	n -> age = 20;
	n -> score = 2022105022.0;
}


void print_value(struct Stu* n)
{
	printf("%s %d %lf\n",n->name,n->age,n->score);
}


int main()
{
	struct Stu s = {0};
	set_value(&s);
	print_value(&s);
	return 0;
}

十、表达式求值

表达式的求值顺序一般是由操作符的优先级和结合性来决定的。
有些操作数在求值的时候需要转换为其他类型才可以计算。

1、隐式类型转换

C的整型算术运算总是至少以缺省整型类型的精度来实现的,
为了获得这个精度,短整型或字符操作数在运算时会被转化为普通整型。
这就是整型提升

整型提升的意义和操作:

整型提升的意义和操作

2、算术转换

整型提升发生在大小小于整型的类型身上,而大于整型的则会发生算数转换。
如果某个操作符的操作数属于不同类型,除非某个操作数发生转换,否则操
作就无法继续。一般是由下向上转换。下面从上到下按从大到小排序。

long double
double
float
unsigned long int
long int 
unsigned int 
int

注意:隐式转换会有精度的丢失。

float f = 3.14;
int num = f;
printf("%d\n",num);
3、操作符属性

复杂表达式求值有三个影响因素:

操作符优先级
操作符结合性
是否控制操作顺序

两个相邻操作符先执行哪个,取决于优先级,如果一样则看结合性:
操作符优先级和结合性

e.g.

int a = 1int b = 0;
b = (++a)+(++a)+(++a);
printf("%d\n",b); 

不同的编译器输出不一样:这段代码就是有错误的!

从C语言的角度来看,这段代码试图在一个表达式中多次修改同一个变量a的值,
并且期望这些修改能够按照从左到右的顺序发生。然而,根据C语言的标准
(C99及之前的版本),这种表达式中的副作用(即修改a的值)的顺序是
未指定的(unspecified behavior),因此编译器可以自由选择顺序。在C11标准中
,这种行为甚至被明确地定义为未定义行为(undefined behavior)。

从汇编语言的角度来看,这段代码的问题在于它依赖于一个特定的副作用顺序,但编译器生成的汇编代码可能不会保证这种顺序。编译器可能会为了优化或其他原因重新排序这些操作,导致与预期不同的结果。这就是为什么不同的编译器编译这条代码结果是不同的。

推荐写法:

int a = 1;  
int b = 0;  
++a;  
b += a;  
++a;  
b += a;  
++a;  
b += a;  
printf("%d\n", b);

因此:如果通过优先级和结合性不能确定操作路径,这个表达式就是有问题的。

你可能感兴趣的:(c语言,开发语言)