C语言初阶——数组与操作符

一、一维数组与二维数组

1.1 一维数组的创建

数组是一组相同类型元素的集合;

数组的创建方式:
typr_t           arr_name[const_n];
数组的元素类型    数组名[常量表达式]//常量表达式用来指定数组的大小

int n = 10;
scanf("%d" , &n);
int arr[n];
//在c99标准之前,数组的大小必须是常量或者常量表达式;
//在c99标准之后,数组的大小可以是变量,这是为了支持变长数组;
//变长数组不是说可以随意变长,而是在数组创建之前根据需要用户自己指定数组大小

1.2 一维数组的初始化

数组的初始化指的是:在创建数组的同时给数组的内容一些合理的初始值;数组在创建的时候如果想不指定数组的确定的大小就得初始化,数组的元素个数根据初始化的内容来确定。

int main() {
	int arr1[10] = { 1,2,3 };//不完全初始化,未初始化的元素默认初始化为0;
	int arr2[10] = { 0,1,2,3,4,5,6,7,8,9 };//完全初始化,每一个元素都初始化;
	int arr3[] = { 1,2,3 };//根据给定的元素个数分配大小,arr3大小为3;

	char ch1[10] = { 'a','b','c' };//a b c 0 0 0 0 0 0 0
	char ch2[10] = "abc";         //a b c \0 0 0 0 0 0 0
	//以上两个数组内容上看似一至,但是是有区别的,通过下方数组的创建可知区别
	char ch3[] = { 'a','b','c' };// a b c ;
	char ch4[] = "abc";          // a b c \0 ;

	return 0;
}

1.3 一维数组的使用

数组的使用就是使用 [] 下标引用操作符来使用的;

int main() {
	int arr[10] = { 0 };//数组的不完全初始化
	int sz = sizeof(arr) / sizeof(arr[0]);//计算数组的元素个数
	//对数组内容赋值,数组是使用下标来访问的,下标从零开始;
	int i = 0;
	for (i = 0; i < 10; i++) {
		arr[i] = i;
		//如果想自己赋值:scanf("%d",&arr[i]);
	}
	//输出数组元素(正序)
	for (i = 0; i < sz; i++) {
		printf("%d ", arr[i]);
	}
	printf("\n");//换行打印更清晰
	//输出数组元素(倒序)
	for (i = sz - 1; i >= 0; i--) {
		printf("%d ", arr[i]);
	}
	return 0;
}

1. 数组是使用下标来访问的,下标从0开始;

2. 数组的大小可以通过计算得到;——sizeof(arr) / sizeof(arr[0]);

1.4 一维数组在内存中的存储

通过打印数组各个元素的地址我们可以知道,一维数组随着下标的增长,元素的地址也在有规律的递增,由此可得:数组在内存中是连续存放的

1.5 二维数组创建与初始化

int main() {
	//二维数组的创建:
	int arr[3][4];//三行四列的数组(三个横行,四个竖行);
	char ch[3][5];//三行五列的字符数组(三个横行,五个竖行);
	//二维数组初始化;‘
	int arr1[3][4] = { 1,2,3,4,2,3,4,5,3,4,5,6 };
	//1 2 3 4
	//2 3 4 5
	//3 4 5 6
	int arr2[3][4] = { {1,2},{3,4},{5,6} };
	//1 2 0 0
	//3 4 0 0
	//5 6 0 0
	int arr3[][4] = { {1,2,3,4},{2,3},{4,5} };
	//只规定列是可以的,但是只规定行是不可以的
	//1 2 3 4
	//2 3 0 0
	//4 5 0 0
	
	//打印二维数组(二维数组下标可以参照一维数组,行列都从0开始)
	for (int i = 0; i < 3; i++) {
		for (int j = 0; j < 4; j++) {
			printf("%d",arr1[i][j]);
		}
		printf("\n");
	}

	return 0;
}

二维数组可以看做是几个一位数组的数组,在内存中也是连续存放的;

二、数组越界与数组作为函数参数

2.1 数组越界

数组的下标是有范围限制的。数组的下标规定是从0开始的,如果数组有n个元素,最后一个元素的下标就是n-1,所以数组的下标如果小于0,或者大于n-1,就是数组越界访问了,超出了数组合法空间的访问。

不管是一维数组还是二维数组,都存在数组越界访问的问题,但是在C语言中编译器可能会将错就错,所以作为程序员我们一定要做好越界访问的检查。

2.2 数组作为函数参数

#include<stdio.h>

void bubble_sort(int arr[],int sz){
//地址是应该使用指针来接收的,这里arr看似是数组,本质是指针变量
	for (int i = 0; i < sz-1 ; i++) {
	int flag = 1;
	for (int j = 0; j < sz-1-i ; j++) {
		if (arr[j] > arr[j + 1]) {
			int t = arr[j + 1];
			arr[j + 1] = arr[j];
			arr[j] = t;
			flag = 0;
		}
	}
	if (flag == 0) {
		break;//优化,如果flag=0,则说明已经排序完成,直接退出
	}
}

int main() {
	//把数组的数据排成升序
	int arr[] = { 9,8,7,6,5,4,3,2,1,0 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	//采用冒泡排序算法:对数组进行排序
	bubble_sort(arr,sz);
    //数组名本质上是:数组首元素的地址;
	for (int i = 0; i < sz; i++) {
		printf("%d ", arr[i]);
	}
}

三、数组名

数组名确实能表示首元素的地址,但是有两个例外:

(1)sizeof(数组名),这里的数组名表示整个数组,计算的是整个数组的大小,单位是字节;

(2)&数组名,这里的数组名表示整个数组,取出的是整个数组的地址;

#include<stdio.h>

int main() {
	int arr[10] = { 0 };
	printf("%p\n", arr);//arr是首元素的地址
	printf("%p\n", arr+1);//4个字节
	printf("--------------------");
	printf("%p\n", &arr[0]);//首元素的地址
	printf("%p\n", &arr[0]+1);//4个字节
	printf("--------------------");
	printf("%p\n", &arr);//数组的地址
	printf("%p\n", &arr+1);//40个字节
}

四、操作符详解

4.1 操作符分类

算术操作符、移位操作符、位操作符、赋值操作符、单目操作符、关系操作符、逻辑操作符、条件操作符、逗号操作符、下标引用、函数调用和结构成员。

4.2 算术操作符

算术操作符:+、-、*、/、%;

(1)除了%取模操作符之外,其他的几个操作符都可以作用域整数和浮点数;

(2)对于/操作符如果两个数都为整数,执行整数除法,只要有一个浮点数就执行浮点数除法;

(3)%取模操作符的两个操作数必须为整数,返回值是整除之后的余数;

4.3 移位操作符

(<<)左移操作符和(>>)右移操作符;在二进制的基础上进行左移右移——即把其他进制的数转换为二进制,然后进行左移右移。

整数的二进制表示有三种,原码、反码、补码。

(1)正整数的原码反码补码相同,无区别;

(2)负整数的原码反码补码需要计算:(以-7为例)

        1、(10000111)- 原码:第一位是符号位,1代表负数,0代表正数;

        2、(11111000)- 反码:将符号位保持不变,其他各位按位取反。

        3、(11111001)- 补码:反码+1就是补码

(3)整数在内存中存储的是补码,移位操作符移动的就是补码。

(4)移位操作符的规则:

        1、左移操作符:左边丢弃,右边补零;

        2、右移操作符:算术移位:右边丢弃,左边补符号位;

                                   逻辑移位:右边丢弃,左边补0;

4.4 位操作符

按位与 &两个数的补码的对应位置进行与操作,全真才为真,即全1 才为1,否则为0
按位或 |两个数的补码的对应位置进行或操作,全假才为假,即全0 才为0,否则为1
按位异或 ^两个数的补码的对应位置进行异或操作,相同为0,相异为1

利用位操作符进行两个数的交换 

int main() {
	int a = 3, b = 5;
	printf("交换前:a=%d b=%d\n", a, b);
	a = a ^ b;//a = 3^5;
	b = a ^ b;//3^5^5 --> b = 3;
	a = a ^ b;//3^5^3 --> a = 5;
	printf("交换后:a=%d b=%d\n", a, b);
	return 0;
}

实际开发中,更多的还是创建临时变量进行交换两个数,因为位操作符只能对两个整数使用。

4.5 单目操作符

逻辑反操作-负值+正值&取地址符
sizeof操作数类型大小~对一个数的二进制按位取反
++前置、后置++--前置、后置--
*解引用操作符(类型)强制类型转换

4.6 其他操作符

4.6.1 赋值操作符

支持连续赋值,但不建议:a = b = y - 1 = 3 ;

4.6.2 关系操作符

并不是所有的比大小都可以用关系操作符,同样的不是所有的判断相等都可以用关系操作符;判断字符串是否相等需要使用其他的方法进行判断,如果直接使用==判断,那么进行比较的是两个字符串的地址;

4.6.3 逻辑操作符

对于逻辑操作符和位操作符一定要注意特点:(举个例子)

对于&(按位与)和&&(逻辑与):

(1)使用按位与连接几个变量/表达式,直到运算到最后一位才结束运算;

(2)使用逻辑与连接几个变量/表达式,按照从左往右计算,如果有表达式为0(假),那么后面的表达式不管是什么,都不会再参与运算。

比如说:a = 0 ; i = a++ && ++b ;

因为左边 i = a++的结果为0,即假,所以后面的++b不会运行。

(3)结论:&&左边为假,右边不再计算;||左边为真,右边不再计算

4.6.4 条件操作符

类似于选择结构的简便运算,当计算过程和操作比较简单是,可以使用条件操作符进行简便运算。

4.6.5 逗号表达式

逗号表达式就是用逗号隔开的多个表达式,逗号表达式从左向右依次执行,整个表达式的结果是最后一个表达式的结果

4.7 下标引用、函数调用和结构成员

(1)[]——下标引用操作符——操作数:一个数组名+一个索引值—— arr [ 0 ] ;

(2)()——函数调用操作符——接受一个或者多个操作数:第一个操作数是函数名,剩余的操作数是传递给函数的参数;

(3)结构体访问:

        1  . ——结构体.成员名;

        2  -> ——结构体指针->成员名;

#include<stdio.h>
#include<string.h>

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

void set_stu(struct Stu* ps) {
	strcpy((*ps).name, "zhangsan");
	//strcpy((ps->name, "zhangsan");
	(*ps).age = 20;//ps->age = 20;
	(*ps).score = 100.0;//ps->score = 100.0;

}

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

int main() {
	struct Stu s = { 0 };
	set_stu(&s);
	print_stu(s);
	return 0;
}

五、表达式求值

表达式求值的顺序一部分是由操作符的优先级和结合性决定的,同样,有些表达式的操作数在求职的过程当中可能需要转换为其他的类型。

5.1 隐式类型转换

c的整型算术运算总是至少以缺省整型类型的精度来进行的;(也就是以四个字节来进行计算)

为了获得这个精度,表达式中的字符和短整型操作数在使用之前被转化为普通整型,这种转换被称为整型提升

整型提升的意义:

表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度一般就是int的字节长度,同时也是CPU通用寄存器的长度。因此,及时两个插入类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长度。

通用CPU是难以直接实现两个8比特字节直接相加运算(即便机器指令中可能含有这种字节相加的指令)。所以,表达式中各种长度可能小于int长度的整型值,都必须先转换为int或者unsigned int类型,然后才能送入CPU中执行运算。

如何进行整型提升——按照变量的数据类型的符号位进行提升。无符号数的整型提升就是补上0。如下方代码所示:

#include<stdio.h>

int main() {
	char a = 5;
	//00000101——存放在char类型中的字符5;
	char b = 126;
	//01111110——存放在char类型中的字符126;
	char c = a + b;
	//00000000000000000000000000000101——对a进行整型提升
	//00000000000000000000000001111110——对b进行整型提升
	// 整型提升:通过符号位进行补位,从而达成整型提升的目的
	//a+b=00000000000000000000000010000011;
	//存放在c中的二进制序列:10000011
	printf("%d\n", c);
	//11111111111111111111111110000011——取出c,对c进行整型提升
	//以下是从补码返回原码的过程
	//11111111111111111111111110000010——补码-1
	//10000000000000000000000001111101——除符号位外各位取反——得-125
	return 0;
}

5.2 算术转换

如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数转换为另一个操作数的类型,否则操作无法进行。下面的层次体系称为寻常算数转换——从下往上转换

long double

double

float

unsigned long int

long int

unsigned int

int

如果某个操作数的类型在上面这个列表中排名较低,那么首先要转换为另外一个操作数的类型后执行运算。

//警告:算术转换要合理,要不然会有一些潜在的问题
float f = 3.14;
int num = f;//隐式类型转换,会丢失精度

5.3 操作符的属性

5.3.1 操作符的优先级

操作符描述用法示例结合性是否控制求和顺序
()聚组(表达式)n/a
()函数调用exp(a,b)从左到右
[]下标引用arr [ r ]从左到右
.访问结构成员s.name从左到右
->访问结构指针成员ps->name从左到右
++后缀自增s++从左到右
--后缀自减 s - -从左到右
!逻辑反!s从右到左
~按位取反~s从右到左
+单目,表示正值+r从右到左
-单目,表示负值-r从右到左
++前缀自增++s从左到右
--前缀自减 - - s从左到右
*间接访问(解引用)*ps从右到左
&取地址&s从右到左
sizeof取长度(字节)sizeof(类型)从右到左
(类型)类型转换(类型)r从右到左
算术操作符+、-、*、/、%r+r从左到右
移位操作符<<、>>r<<r从左到右
关系操作符>、<、>=、<=、==、!=r==r从左到右
位操作符&、|、^r&r从左到右
逻辑操作符&&、||r&&r从左到右
条件操作符?:r?:r1;r2从左到右
赋值操作符+=、-=、%=等a+=1从右到左
逗号操作符表达式,表达式从左到右

影响表达式求值的三个因素:

(1)操作符的优先级

(2)操作符的结合性

(3)是否控制求值顺序:比如逻辑&&,如果左边是真,右边将不再计算

在计算的过程当中,有一些问题表达式,如下:

a*b +c*d +e*f——表达式的求值路径不止一条——可以是先算乘法再算加法;也可以先算第一个乘法和第二个乘法,相加之后,再算第三个乘法,再相加,所以求值路径不唯一,当abcdef为表达式且相互影响的时候,可能会出现问题,所以应该尽量避免。

c+ --c;——当编译器不同,值不同:如果寄存器先存储c在进行--c的计算的话,那么结果应该就是2c-1;如果先进行--c的计算再存储c的话,那么结果应该是2c-2,结果不同.

总结:只要我们写出的表达式不能通过操作符的属性确定唯一的计算路径,那么这个表达式就是存在问题的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值