写个笔记,未来要是忘了知识可以看看自己写的笔记,直接融会贯通
(吐槽:印象笔记非会员用户竟然一个月只能写60MB的内容,身为
白嫖党的我不能忍!)
所有编程语言本质上都是由算法+数据结构组成的。
语言发展史:机器语言 —>汇编语言 —>高级语言(c语言)
ps:世界第一个高级语言是Fortran,主要用在需要复杂数学计算的科学和工程领域,不适于编写系统程序。
目录
数据类型
基本数据类型
char //字符数据类型
short //短整型
int //整形
long //长整型
long long //更长的整形
float //单精度浮点数
double //双精度浮点数
C语言没有字符串类型
类型基本归类
整型
char //不加前缀默认是有无符号类型要看具体编译器
unsigned char
signed char
short //不加前缀默认是有符号类型
unsigned short [int]
signed short [int]
int //不加前缀默认是有符号类型
unsigned int
signed int
long //不加前缀默认是有符号类型
unsigned long [int]
signed long [int]
浮点数类型:
float
double
//没有无符号浮点数
构造类型(自定义类型):
数组类型 []
结构体类型 struct
枚举类型 enum
联合类型 union
指针类型:
int *pi;
char *pc;
float* pf;
void* pv; //空指针,不知道要指向什么类型的数据,就暂时放这
空类型:
void //表示空类型(无类型)
通常应用于函数的返回类型、函数的参数、指针类型
整形在内存中的存储
变量的创建是要在内存中开辟空间的。空间的大小是根据不同的类型而决定的。
原码、反码、补码
原码直接将数值按照正负数的形式翻译成二进制就可以得到原码。反码原码的符号位不变,其他位统统按位取反就可以得到反码。补码反码+1就得到补码
常见关键字
auto break case char const continue default do double else enum
extern float for goto if int long register return short signed
sizeof static struct switch typedef union unsigned void volatile while
--------基础类型
register-寄存器
//用法:
register int a =100;//建议把a存放入寄存器
ps:只起到建议的作用,放不放要看编译器的意思,如果编译器认为这个变量会被大量频繁地使用,那就会存入编译器。
这个我们无需担心,编译器会帮我们判断该不该放寄存器的。
typedef-类型重命名
//用法:
typedef unsigned long long ull; //将 unsigned long long这个数据类型起个别名叫ull
typedef 全称 type define,typedef 并没有创建任何新类型,它只是为某个已存在的类型增加了一个方便使用的标签
static-静态修饰符
链接解释:https://blog.csdn.net/xiawucha159/article/details/125268737
变量默认是内部链接, 函数默认是外部链接
(3)static修饰函数:函数是具有外部链接属性的,static修饰函数后,函数的外部链接属性就变成了内部链接属性,被static修饰的函数只能在自己所在的.c文件内使用,其他.c文件无法使用,相当于影响了作用域
#define 定义常量和宏
//用法
//定义常量
#define W 10
#define M 20
//定义宏
#define MAX(x,y) ((x)>(y)?(x):(y))
int main(){
int c=MAX(W,M);
}
原理就是把该.c文件中所有单个的W和N替换成10和20,把MAX(x,y)替换成了(x>y?x:y)
ps:在宏定义里给每个标识符加()是一种标准写法,可以避免一些例如运算优先级的问题
例如:
#define mult1(x,y) (x*y)
#define mult2(x,y) ((x)*(y))
int main() {
printf("%d ", mult1(2 + 1, 2));//结果是4
printf("%d ", mult2(2 + 1, 2));//结果是6
}
指针---地址
一些不务正业的内容...
在计算机内部中,根本没有啥C/C++、汇编、机器语言,它们眼里只有高低电平。
计算机使用电子器件(如晶体管)来进行逻辑运算和存储信息。这些电子器件的工作原理是基于高低电平的控制。
在机器语言中,高电平通常被表示为1,低电平通常被表示为0。这是因为电子器件(如晶体管)可以被控制为开或关状态,从而分别代表1和0。
在计算机内部,电子器件按照特定的电路设计连接在一起,形成逻辑门电路。逻辑门电路可以执行不同的逻辑操作,如与门、或门、非门等。这些逻辑门电路可以通过组合和重复连接,实现各种复杂的逻辑功能,如加法器、比较器、寄存器等。
当计算机执行指令时,它会将指令编码为一系列高低电平的信号,并将其传递到逻辑门电路中执行。通过这种方式,计算机能够进行算术运算、逻辑运算、存储和检索数据等操作。
总之,计算机通过高低电平的控制来实现逻辑运算和存储信息,并通过逻辑门电路来组合和重复连接实现不同的逻辑功能。
----------重学计算机组成原理(4)-还记得纸带编程吗?
----------高低电平怎么转化为数字信号0和1----------数字电路中的晶体振荡器与时钟信号
----------数字电路的时钟信号是怎么产生呢?
时钟信号是什么?
可以将时钟信号定义为在高电平和低电平状态之间振荡的特定类型的信号。信号的作用类似于节拍器,数字电路及时跟随节拍器以协调其动作序列,从而让CPU知道什么时候输出信号(即高低电平),有序地输出高低电平,成为序列。
时钟信号是如何产生的?
计算机的时钟信号是由时钟发生器产生的,时钟发生器是一种晶体振荡器,其主要功能是将直流电转换为周期性振荡信号或频率非常高的交流信号,即一系列的连续波形,比如矩形波、正弦波等。
为什么需要时钟信号?
任何电路都是有延迟的,从输入信号输入到电路完成计算输出结果是需要时间的。但是麻烦的是这个时间对于所有电路都是不一样的,所以我们不知道究竟要等多久上一个电路才算是完成了运算,可以读取输出了。对于人来说这个问题还不大,我们只要多等等保证算完了就好了,但是对于电路来说就麻烦了:每一级电路都需要上一级电路的输出结果作为输入,可是要等多久上一级才能完成计算?就算我知道上一级的计算需要10ns时间,但是我该从哪里开始计时?用什么进行计时?
于是电路设计师们不得不加入了非常复杂的“握手信号”来控制数据读取,简单的说就是前一级电路在完成运算之后向下一级发送一个“可以读取了”的信号,下一级收到这个信号之后才能读取;下一级读取完毕之后再发一个“我读完了”的信号返回上一级,上一级才能开始下一个运算。
这不仅浪费了大量的电路在握手上,还极大地加大了设计难度。
为了解决这个问题,工程师们设计了“同步电路”。同步电路加入了时钟信号,所有电路模块的读取与输出都受到时钟信号的控制。比如一个电路模块,每次计算需要至多100ns的时间,而电路的时钟周期是50ns。那么我就知道这个电路至多需要两个周期的时间就能完成运算。为了冗余安全,我将这个模块设计为每三个时钟周期进行一次运算,它的下一级电路也每隔三个周期执行一次读取即可。
有了时钟信号,我只需要对每个电路设计“隔几个周期进行读/写”即可,不再需要在所有模块之间都设计握手信号,这极大地降低了设计难度。
转自知乎用户acalephs ------- 为什么需要时钟
-----------为什么 CPU 需要时钟才能工作?
答:因为设置成1字节很合适。32位机器的32根地址线能提供32位的二进制序列,用byte做单位时,机器能管理4,294,967,296个内存单元(4,294,967,296=2^32)。如果单位用bit,因为8bit=1字节,所以4294967296 x 8=34,359,738,368个内存单元,从十亿级升到百亿级,每个内存单元都需要编号,并且每存一个字符,就需要分配8个内存单元,这会给CPU带来麻烦,降低效率,所以单位用bit不合适。而如果单位用kb或kb以上,一个字符能分到1kb的内存单元,又太过浪费,所以单位设置成1字节就很合适。
为什么1字节=8位?
“ 所谓字节,原意就是用来表示一个完整的字符的。最初的计算机性能和存储容量都比较差,所以普遍采用4位BCD编码(这个编码出现比计算机还早,最早是用在打孔卡上的)。
BCD编码表示数字还可以,但表示字母或符号就很不好用,需要用多个编码来表示。后来又演变出6位的BCD编码(BCDIC),以及至今仍在广泛使用的7位ASCII编码。
不过最终决定字节大小的,是大名鼎鼎的System/360。当时IBM为System/360设计了一套8位EBCDIC编码,涵盖了数字、大小写字母和大部分常用符号,同时又兼容广泛用于打孔卡的6位BCDIC编码。System/360很成功,也奠定了字符存储单位采用8位长度的基础,这就是1字节=8位的由来。” -----转自百度知道
指针变量
//用法
int a=10;
int* pa=&a;//int*中的int表示指向的对象是int类型,*则表示pa是个指针变量
//解引用操作
*pa=20;//效果等同于a=20
//引用就是指引用pa指向的对象的地址,解就是从这个地址里对应的东西解出来
指针和指针类型
定义格式:type + *
char *pc = NULL;
int *pi = NULL;
short *ps = NULL;
long *pl = NULL;
float *pf = NULL;
double *pd = NULL;
指针类型的意义:
#include <stdio.h>
//演示实例
int main()
{
int n = 10;
char *pc = (char*)&n;
int *pi = &n;
printf("%p\n", &n);
printf("%p\n", pc);
printf("%p\n", pc+1);
printf("%p\n", pi);
printf("%p\n", pi+1);
return 0;
}
总结:指针的类型决定了指针向前或者向后走一步有多大(距离)
指针的解引用
//演示实例
#include <stdio.h>
int main()
{
int n = 0x11223344;
char *pc = (char *)&n;
int *pi = &n;
*pc = 0; //重点在调试的过程中观察内存的变化。
*pi = 0; //重点在调试的过程中观察内存的变化。
return 0;
}
对pc进行解引用操作:
执行后,只有1个字节的数据被修改
对pi进行解引用操作
执行后,4个字节都被修改了
原因:
野指针
野指针成因
#include <stdio.h>
int main()
{
int *p;//局部变量指针未初始化,默认为随机值
*p = 20;
return 0;
}
int main()
{
int arr[10] = {0};
int *p = arr;
int i = 0;
for(i=0; i<=11; i++)
{
//当指针指向的范围超出数组arr的范围时,p就是野指针
*(p++) = i;
}
return 0;
}
int* test()
{
int a = 10;
return &a;
}
int main()
{
int* p = test(); //test()执行完后释放其所占内存空间
*p = 20; //因此原本属于a的空间随时能被覆盖
return 0; //所以*p=20只是暂时让该空间存了个20,未来还可能会被其他值覆盖
//从而导致做了无用功,这是我们需要避免的
}
int* test()
{
int a = 10;
return &a;
}
int main()
{
int* p = test();
//置为空指针
*p=null;
return 0;
}
4. 避免返回局部变量的地址
int main()
{
int a=1;
int* pa=&a;
//.....一通操作后....
if (pa != NULL)//判断下这指针是个什么情况
{
//如果非空代表可以使用,否则就是还没到使用的时机(取决于具体设计),就不操作了
*pa = 20;
}
}
指针运算
- 指针加减整数
- 指针减指针(指针之间不能相加) ---------C语言中地址为什么不能相加
- 指针的关系运算
指针加减整数
指针加减整数的本质是对指针进行偏移,以便在内存中访问相对于指针所指向的内存位置偏移量相应的内存单元。
偏移量:偏移量是一个值,表示某个位置与参考位置之间的差异或距离。在计算机科学中,偏移量通常用于指针或内存地址的计算,以便在内存中访问特定的数据或程序。例如,一个指针指向内存中的某个位置,通过加上偏移量,可以访问该位置后面的地址或前面的地址。偏移量可以为正数、负数或零,取决于参考位置和目标位置之间的距离。
偏移量=所加整数值*指针类型
当对指针进行加减整数操作时,实际上是将指针所指向的内存地址加上或减去一个偏移量,这个偏移量是指针加减整数的结果。这个偏移量是根据指针所指向的数据类型和加减整数的值来计算的,例如,对一个 int 类型的指针 p 加 1,其本质就是将 p 所指向的内存地址加上 4 个字节(因为一个 int 类型的数据占用 4 个字节)。
指针加减1不是让指针中的地址值加减1,而是加减偏移量,这是C语言的规定。
因为如果指针加减整数是对地址值进行加减,会出现很多问题。
//举个栗子
int main(){
int a = 1;
int b = 2;
int* pa = &a;
*(pa + 1)=3;
}
可以看到,不仅a的值被改变,b的值也被改变了,这就乱套了,所以设计指针加减的是偏移量的好处就体现出来了,*(pa+1)=3会先让指针跳到b的开头地址,然后往后覆盖4个字节存储数值3,这样只会影响b,其他不受影响。
指针减指针
int main() {
int a = 12;
int b = 13;
int* pa = &a;
int* pb = &b;
printf("%d",pa-pb);
}
指针减指针的结果是两个指针之间相差的元素个数,即偏移量。例如,如果一个指针指向数组中的某个元素,另一个指针指向该数组中的另一个元素,那么它们之间的偏移量将是它们指向的元素之间的距离,以元素为单位。这种操作常常用于计算两个指针之间的距离,以及计算数组中元素的个数。
//计算数组元素个数
int strlen(char* s)
{
char* p = s;
while (*p != '\0')
p++;
return p - s;
}
int main() {
char arr[] = "123456789";
printf("%d",strlen(arr)); //输出结果是9
}
应注意的事项
标准规定:允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。
指针和数组
即,数组名表示的是数组首元素的地址 (除了sizeof(arr)和&arr外,这两种情况表示的是整个数组的地址) 。
//那么就可以这么写
int arr[10] = {1,2,3,4,5,6,7,8,9,0};
int *p = arr; //p存放的是数组首元素的地址
既然可以把数组名当成地址存放到一个指针中,我们就可以使用指针来访问数组。
int main()
{
int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
int *p = arr; //指针存放数组首元素的地址
int sz = sizeof(arr) / sizeof(arr[0]);
int i = 0;
for (i = 0; i<sz; i++)
{
printf("%d ", *(p + i));//直接使用指针+i访问数组元素
}
return 0;
}
执行结果:
二级指针
存储指针地址的指针就是二级指针,然后存储二级指针地址的指针又叫三级指针,能存储三级指针地址的指针又叫四级指针....
int main()
{
int a = 1;
int* pa = &a;
int** ppa = &pa;
int*** pppa = &ppa;
int**** ppppa = &pppa;
int***** pppppa = &ppppa;
int****** ppppppa = &pppppa;
int******* pppppppa = &ppppppa;
//.......
}
n级指针与一级指针无任何区别,没啥高贵特殊,都只是存地址的,定义全都一模一样,不一样的只是地址罢了
二级指针的解引用操作
指针数组
指针数组就是存放指针的数组。
int main(){
int arr[5] = { 1,2,3,4,5 }; //一个int型数组
int* parr[5] = { &arr[0], &arr[1], &arr[2], &arr[3], &arr[4]}; //一个int*型数组
//上面说到,地址==指针,所以地址就是指针,指针就是地址
//别误认为不是存的指针变量就不是指针数组了
}
结构体
类似java的类与对象,结构体使得C语言有能力描述复杂类型。
基本用法:
//定义Stu类型
struct Stu
{
char name[20];
int age;
char sex[5];
char id[15];
};
int main(){
//创建Stu对象,并初始化
struct Stu s = {"张三", 20, "男", "20180101"};
//打印结构体信息的3种方法
//.结构成员访问操作符
printf("%s %d %s %s\n", s.name, s.age, s.sex, s.id);
//解引用操作符
struct Stu* ps = &s;
printf("%s %d %s %s\n", (*ps).name, (*ps).age, (*ps).sex, (*ps).id);
//->操作符
printf("%s %d %s %s\n", ps->name, ps->age, ps->sex, ps->id);
}
进阶用法:
重命名
//嫌名字长,就用typedef重命名
typedef struct (类型名称)
{
(一堆成员变量)
}(重命名的名称);
//例:将struct Stu重命名为Stu
typedef struct Stu
{
char name[20];
int age;
char sex[5];
char id[20];
}Stu;
结构的成员可以是标量、数组、指针,甚至是其他结构体。
struct Stu
{
char name[20];
}p1,p2,p3; //声明类型的同时定义变量p1,p2,p3
//在这种情况下定义的变量地位相当于全局变量
普通方法初始化:
int main(){
struct Stu s={"李大牛"};//在main函数里初始化
}
不普通方法初始化:
struct Stu
{
char name[20];
}s={"李大喜"};//直接在定义类型时定义变量并初始化
struct S
{
int data[1000];
int num;
};
struct S s = {{1,2,3,4}, 1000};
//结构体传参----形参是一个结构体类型,接收结构体变量
void print1(struct S s)
{
printf("%d\n", s.num);
}
//结构体地址传参-----形参是结构体指针,含蓄一点,就传一个结构体变量的地址过来就好了
void print2(struct S* ps)
{
printf("%d\n", ps->num);
}
int main()
{
print1(s); //传结构体
print2(&s); //传地址
return 0;
}
原因:函数传参的时候,参数是需要压栈的。如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。
操作符
算术操作符
移位操作符
注:
1、移位操作符的操作数只能是整数。
2、对于移位运算符,不要移动负数位,这个是标准未定义的。
3、移位只是说着玩,为了方便理解罢了,实际上并不会真的移位,而只是进行计算而已
4、符号位不会跟着移动
int main() {
int a = -1;
int b,c,d;
//>>右移操作符
b= a >> 1;//对补码算术右移一位,左边用原该值的符号位填充,右边丢弃
//<<左移操作符
c = a << 1;//对补码逻辑左移一位,左边抛弃、右边补0
printf("%d\n", b);
printf("%d\n", c);
}
//-1
//1000 0000 0000 0000 0000 0000 0000 0001 原码
//1111 1111 1111 1111 1111 1111 1111 1110 按位取反后成为反码
//1111 1111 1111 1111 1111 1111 1111 1111 +1后成为补码
// >>1后
// 1111 1111 1111 1111 1111 1111 1111 1111 补码
// 1111 1111 1111 1111 1111 1111 1111 1110 减1后得到反码
// 1000 0000 0000 0000 0000 0000 0000 0001 按位取反后得到原码,十进制表示为-1
// <<1后
// 1111 1111 1111 1111 1111 1111 1111 1110 补码
// 1111 1111 1111 1111 1111 1111 1111 1101 减1后得到反码
// 1000 0000 0000 0000 0000 0000 0000 0010 按位取反后得到原码,十进制表示为-2
位操作符
位运算符均是对补码进行操作
赋值操作符
单目操作符
关系操作符
逻辑操作符
当&&左边的运算为0的时候,就不再执行&&右边的运算了
当||左边的运算为1的时候,就不再执行||右边的运算了
1&2----->0
1&&2---->1
1|2----->3
1||2---->1
条件操作符 (三目运算符)
int a=1;
int b=2;
a>b?a:b;
//满足a>b就返回a,否则就返回b
逗号表达式
下标引用、函数调用和结构成员
int arr[10];//创建数组
arr[9] = 10;//使用下标引用操作符。
[ ]的两个操作数是arr和9。
int main()
{
test1(); //使用()作为函数调用操作符。
test2("hello bit.");//使用()作为函数调用操作符。
return 0;
}
#include <stdio.h>
struct Stu
{
char name[10];
int age;
};
int main() {
struct Stu s = { "独孤唯我",23 };
printf("%s\n", s.name);//.结构成员访问
printf("%d\n", s.age);//.结构成员访问
struct Stu* ps = &s;
printf("%s\n", ps->name);//->结构成员访问
printf("%d\n", ps->age);//->结构成员访问
}
类型转换
表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度 一般就是int的字节长度,同时也是CPU的通用寄存器的长度。因此,即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长度。通用CPU(general-purpose CPU)是难以直接实现两个8比特字节直接相加运算(虽然机器指令 中可能有这种字节相加指令)。所以,表达式中各种长度可能小于int长度的整型值,都必须先转 换为int或unsigned int,然后才能送入CPU去执行运算。
如何进行整体提升
正数的整型提升
char i=1;
//0000 0001 i的补码,符号位为0(正数的原、反、补码相同)
//0000 0000 0000 0000 0000 0000 0000 0001 高位补0,补够32位,此时i成为了int型
负数的整型提升
char j=-1;
//1000 0001 j的原码
//1111 1111 按位取反后+1,成为补码,符号位为1
//1111 1111 1111 1111 1111 1111 1111 1111 高位补1,补够32位,此时j成为了int型
无符号数的整形提升,高位补0就完了
整型提升是那些数据类型大小<整型的数据才会触发的机制。
打印时也会触发整型提升
如图所示,a和b只要参与表达式(+a、+b)运算,就会发生整形提升,所以sizeof(+a)和sizeof(+b) 是4,而sizeof(+c)依然是8,c为double型,比int大4个字节。8米高的房间住得好好的,没必要到4米高的房间凑合着住,所以保持原样即可。
注:整型提升是暂时的提升,只是为了让ALU方便计算才提的,算完了a和b依然是原来的类型。
类型名 所占字节数1、long double 82、double 83、float 44、unsigned long int 45、long int 46、unsigned int 47、int 4
float f = 3.14;
int num = f;//隐式转换,会有精度丢失
操作符的属性
操作符优先级排行榜
操作
符
|
描述
|
用法示例
|
结果类
型
|
结合
性
|
是否控制求值
顺序
|
---|---|---|---|---|---|
( ) |
聚组
|
(表达式)
|
rexp
|
N/A
|
否
|
( ) |
函数调用
|
rexp
(
rexp
,
...,rexp
)
|
lexp
|
L-R
|
否
|
[ ] |
下标引用
|
rexp[rexp]
|
lexp
|
L-R
|
否
|
. |
访问结构成员
|
lexp.member_name
|
lexp
|
L-R
|
否
|
-> |
访问结构指针成员
|
rexp->member_name
|
rexp
|
L-R
|
否
|
++ |
后缀自增
|
lexp ++
|
rexp
|
L-R
|
否
|
-- |
后缀自减
|
lexp --
|
rexp
|
L-R
|
否
|
! |
逻辑反
|
! rexp
|
rexp
|
R-L
|
否
|
~ |
按位取反
|
~ rexp
|
rexp
|
R-L
|
否
|
+ |
单目,表示正值
|
+ rexp
|
rexp
|
R-L
|
否
|
- |
单目,表示负值
|
- rexp
|
rexp
|
R-L
|
否
|
++ |
前缀自增
|
++ lexp
|
rexp
|
R-L
|
否
|
-- |
前缀自减
|
-- lexp
|
rexp
|
R-L
|
否
|
* |
间接访问
|
* rexp
|
lexp
|
R-L
|
否
|
& |
取地址
|
& lexp
|
rexp
|
R-L
|
否
|
sizeof |
取其长度,以字节
表示
| sizeof(rexp);sizeof(类型) |
rexp
|
R-L
|
否
|
(类型) |
类型转换
| (类型)rexp |
rexp
|
R-L
|
否
|
* |
乘法
| rexp*rexp |
rexp
|
L-R
|
否
|
/ |
除法
|
rexp / rexp
|
rexp
|
L-R
|
否
|
% |
整数取余
|
rexp % rexp
|
rexp
|
L-R
|
否
|
+ |
加法
|
rexp + rexp
|
rexp
|
L-R
|
否
|
- |
减法
|
rexp - rexp
|
rexp
|
L-R
|
否
|
<< |
左移位
|
rexp << rexp
|
rexp
|
L-R
|
否
|
>> |
右移位
|
rexp >> rexp
|
rexp
|
L-R
|
否
|
> |
大于
|
rexp > rexp
|
rexp
|
L-R
|
否
|
>= |
大于等于
|
rexp >= rexp
|
rexp
|
L-R
|
否
|
< |
小于
|
rexp < rexp
|
rexp
|
L-R
|
否
|
<= |
小于等于
|
rexp <= rexp
|
rexp
|
L-R
|
否
|
== |
等于
|
rexp == rexp
|
rexp
|
L-R
|
否
|
!= |
不等于
|
rexp != rexp
|
rexp
|
L-R
|
否
|
& |
按位与
|
rexp & rexp
|
rexp
|
L-R
|
否
|
^ |
按位异或
|
rexp ^ rexp
|
rexp
|
L-R
|
否
|
| |
按位或
|
rexp | rexp
|
rexp
|
L-R
|
否
|
&& |
逻辑与
|
rexp && rexp
|
rexp
|
L-R
|
是
|
| | |
逻辑或
|
rexp | | rexp
|
rexp
|
L-R
|
是
|
? : |
条件操作符
|
rexp ? rexp : rexp
|
rexp
|
N/A
|
是
|
= |
赋值
|
lexp = rexp
|
rexp
|
R-L
|
否
|
+= |
以
...
加
|
lexp += rexp
|
rexp
|
R-L
|
否
|
-= |
以
...
减
|
lexp -= rexp
|
rexp
|
R-L
|
否
|
*= |
以
...
乘
|
lexp *= rexp
|
rexp
|
R-L
|
否
|
/= |
以
...
除
|
lexp /= rexp
|
rexp
|
R-L
|
否
|
%= |
以
...
取模
|
lexp %= rexp
|
rexp
|
R-L
|
否
|
<<= |
以
...
左移
|
lexp <<= rexp
|
rexp
|
R-L
|
否
|
>>= |
以
...
右移
|
lexp >>= rexp
|
rexp
|
R-L
|
否
|
&= |
以
...
与
|
lexp &= rexp
|
rexp
|
R-L
|
否
|
^= |
以
...
异或
|
lexp ^= rexp
|
rexp
|
R-L
|
否
|
| = |
以
...
或
|
lexp |= rexp
|
rexp
|
R-L
|
否
|
, |
逗号
|
lexp, rexp
|
rexp
|
L-R
|
是
|
表达式的求值部分由操作符的优先级决定。
表达式1
a*b + c*d + e*f
//代码1在计算的时候,由于*比+的优先级高,只能保证,*的计算是比+早,但是优先级并不
//能决定第三个*比第一个+早执行。
所以表达式的计算机顺序就可能是:
a*b
c*d
a*b + c*d
e*f
a*b + c*d + e*f
或者
a*b
c*d
e*f
a*b + c*d
a*b + c*d + e*f
表达式2
c + --c;
//同上,操作符的优先级只能决定自减--的运算在+的运算的前面,但是我们并没有办法得
//知,+操作符的左操作数的获取在右操作数之前还是之后求值,所以结果是不可预测的,是有歧义
//的。
分支语句和循环语句
- if
注:if语句是否执行要看判断语句结果为0还是非0,0就不执行,非0就执行,非0包括负数。
- switch
- while
- for
- do while
- goto语句
悬空else问题 - - -不知道这个else跟谁匹配,像是凭空出现的一样,故此得名悬空else
int main()
{
int a = 0;
int b = 2;
if(a == 1)
if(b == 2)
printf("hehe\n");
else
printf("haha\n");
return 0;
}//输出结果是什么都没有输出
原因:在相同代码块中,else只与离它最近的if匹配,与排版对齐无关
当进行常量与变量的大小比较时,用代码2比代码1更好,更不容易出错
//代码1
int num = 1;
if(num == 5)
{
printf("233\n");
}
//代码2
int num = 1;
if(5 == num)
{
printf("233\n");
}
switch语句
switch(整型表达式)
{
case 整形常量表达式:
语句;
break;
case 整形常量表达式:
{
语句; //case中也可以加大括号
break; //从而管理多条语句
}
default:
语句;
break;//编程好习惯,即使是最后一行也加break
}
ps:因为存储字符时是存储的ASCII值,所以char类型也算整型
scanf默认读到空格就不读了 ------- C语言scanf函数用法完全攻略
//让scanf接着读的方法:
char arr[20] ={0};
scanf("%[^\n]",arr);//中括号里的^\n的意思是:一直读,直到遇到\n后停止
清理缓冲区
getchar( ):是个字符就读,但只读取一个字符 ------ 关于getchar的用法及实例解析
putchar( ):只会输出一个字符 ------ C语言中的putchar函数
int main()
{
int ch = 0;
while ((ch = getchar()) != EOF)
putchar(ch);
return 0;
}
while循环
break在循环中的作用:直接终止该循环体,跳出循环体
continue在循环中的作用:跳过continue下面所有代码不执行,直接执行条件判断语句,进行下一次循环
//语法
while(i<10){
循环语句;
i++;
}
for循环
语法:
for(初始化部分; 条件判断部分; 调整部分){
循环语句;
}
//注意事项:1、不可在for循环体内修改循环变量,防止for循环失去控制。
// 2、建议for语句的循环控制变量的取值采用“前闭后开区间”写法。
//前闭后开的写法
for(int i=0; i<10; i++)
{}
//两边都是闭区间
for(int i=0; i<=9; i++)
{}
//死循环写法
for(;;)
{}
语法:
do{
循环语句;
}while(表达式);
二分查找 ---- 有序数组的查找算法
只对有序数组有效,对无序无效
int main() {
int arr[] = { 1,2,3,4,5,6,7,8,9,10,11 };
//算出数组长度
int length = sizeof(arr) / sizeof(arr[0]);
//n为将查找的数。flag为标记,负责判断是否找到
int n = 0, flag = 0;
//left为左指针,right为右指针。两者相加的值表示范围大小
int left = 0, right = length - 1;
scanf("%d", &n);
//如果缩小范围到左右指针相等,则退出循环
while (left <= right) {
//计算出中间数值
//ps:此算式可以尽量避免整数溢出的情况
int sum = left + (left - right) / 2;
//如果n大于目前范围中间的数值,left就指向中间数值下标的下一位
if (n > arr[sum]) {
left = sum + 1;
}
//如果n小于目前范围中间的数值,right就指向中间数值下标的上一位
else if (n < arr[sum]) {
right = sum - 1;
}
//找到后就打印出坐标,置flag为1,跳出循环
else {
flag = 1;
printf("找到了,下标为%d", sum);
break;
}
}
if (flag == 0) {
printf("没找到");
}
}
函数
C语言中函数的分类
格式:
返回类型 函数名(形参的数据类型, 参数名){
语句项;
}
例如:
int add(int x,int y){
int z = x+y;
return z;
}
由上图可知:函数在调用的时候, x,y拥有自己的空间,同时拥有了和实参一模一样的内容。 所以我们可以简单的认为:形参实例化之后其实相当于实参的一份临时拷贝。
函数的嵌套调用和链式访问
嵌套调用
int A(){
printf("我被调用了");
}
int main(){
A(); //函数里面再调用一个函数就是嵌套调用
}
链式访问
把一个函数的返回值作为另外一个函数的参数。
printf("%d",printf("%d",123);//printf的返回值是打印出的字符个数
函数的声明和定义
函数递归---(传递后回归)
程序调用自身的编程技巧称为递归。
//待补充
函数栈帧的创建和销毁
- 局部变量是如何创建的?
- 为什么局部变量不初始化内容是随机的?
- 函数调用时参数时如何传递的?传参的顺序是怎样的?
- 函数的形参和实参分别是怎样实例化的?
- 函数的返回值是如何带回的?
理解了函数栈帧的创建和销毁,你便能对这些与底层有关的问题洞若观火一般。
首先来理解栈的概念。
ps:
x86:x86架构是重要地可变指令长度的CISC(复杂指令集计算机,Complex Instruction Set Computer),英特尔首先开发制造的一种微处理器体系结构的泛称,该系列较早期的处理器名称是以数字来表示,并以“86”作为结尾,包括Intel 8086、80186、80286、80386以及80486,因此其架构被称为“x86”。---------CPU架构关系
函数栈帧中存储的寄存器变量,例如指令寄存器(64位环境中用 rip 表示,32为环境中用 eip 表示)、堆栈基指针寄存器(64位环境用 rbp 表示,32位环境用 ebp 表示)
相关寄存器
esp -= 4;
平台:32位;
软件:VisualStudio2022社区版
//待补充
CC在汇编代码中表示为int 3,实际表示一个中断,在与硬件中断(CPU中加入的DR寄存器指示)做区别的时候也叫软中断,在debug模式下,会默认把栈内存都初始化为CC,当越界访问时就会响应中断来提示内存越界访问。
ptr是临时的类型转换,相当于C语言中的强制类型转换--------汇编语言中ptr的含义
byte(字节)、word(字)、dword(双字)、qword(四字)、tbyte(十字节)、far(远类型)和near(近类型)
i=0x0000007b,为什么显示结果是7b 00 00 00 呢?
原因是:英特尔的CPU采用了小端方式进行数据存储,因此低位在前、高位在后
小端模式--低位在前,高位在后
dword ptr表示内存操作数是4个字节(Double-WORD PoinTeR,双字指针),还有word ptr表示2字节,byte ptr表示一字节,qword ptr表示8字节。
一般只有目标是内存源是即时数的时候才需要明确写出来:mov dword ptr [eax], 0因为不写的话根本判断不出来要写几个字节,编译器默认会处理成byte ptr。
数组
一维数组的创建和初始化
语法格式:
数组类型 数组名[存放元素个数(可留空),即数组大小]={初始化元素内容(可不写)};
例:
int arr1[3]={1, 2, 3};//完全初始化
int arr2[3]={1, 2};//不完全初始化
int arr3[]={1,2,3};//数组大小会根据初始化的内容进行设置
int arr4[3];//只是声明,没有分配内存空间
1.数组的初始化是指,在创建数组的同时给数组的内容一些合理(符合数组类型)初始值(初始化)
2.数组在创建的时候如果想不指定数组的确定的大小就得初始化。数组的元素个数根据初始化的内容来确
定。
一维数组的使用
//计算数组的元素个数
int arr[10]={0};
int sz = sizeof(arr)/sizeof(arr[0]);
//sizeof(arr)可计算出数组所占空间的大小,即所占空间=元素个数x数据类型
//sizeof(arr[0])可计算出数组类型所占空间大小
//由于所占空间=元素个数x数组类型所占空间大小,所以除以数组类型所占空间大小即得到元素个数
一维数组在内存中的存储
可见,数组在内存中是连续存储的,由于int型占4个字节,而一个内存单元=一个地址=1字节大小,所以元素之间差了4个地址。