一、一维数组与二维数组
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,结果不同.
总结:只要我们写出的表达式不能通过操作符的属性确定唯一的计算路径,那么这个表达式就是存在问题的。