个人笔记—C语言(测试)

写个笔记,未来要是忘了知识可以看看自己写的笔记,直接融会贯通

吐槽:印象笔记非会员用户竟然一个月只能写60MB的内容,身为白嫖党的我不能忍!

所有编程语言本质上都是由算法+数据结构组成的。

语言发展史:机器语言 —>汇编语言 —>高级语言(c语言)

ps:世界第一个高级语言是Fortran,主要用在需要复杂数学计算的科学和工程领域,不适于编写系统程序。

目录

常见关键字

register-寄存器

typedef-类型重命名

static-静态修饰符

#define 定义常量和宏

指针---地址

指针变量

指针和指针类型

指针的解引用

野指针

结构体

操作符分类:

算术操作符

移位操作符

位操作符

赋值操作符

单目操作符

关系操作符

逻辑操作符

条件操作符 (三目运算符)

逗号表达式

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

类型转换

操作符的属性

分支语句和循环语句

函数

函数的嵌套调用和链式访问

函数的声明和定义

函数递归---(传递后回归)

函数栈帧的创建和销毁

数组

一维数组的创建和初始化

一维数组的使用

一维数组在内存中的存储



数据类型

基本数据类型

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    //表示空类型(无类型)

通常应用于函数的返回类型、函数的参数、指针类型

整形在内存中的存储

变量的创建是要在内存中开辟空间的。空间的大小是根据不同的类型而决定的。

原码、反码、补码

计算机中的整数有三种2进制表示方法,即原码、反码和补码。
三种表示方法均有符号位数值位两部分,符号位都是用0表示“正”,用1表示“负”,而数值位,
正整数的原、反、补码的数值位都相同。
负整数的原、反、补码的数值位都不相同。
原码
直接将数值按照正负数的形式翻译成二进制就可以得到原码。
反码
原码的符号位不变,其他位统统按位取反就可以得到反码。
补码
反码+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

--------基础类型

C 语言提供了丰富的关键字,这些关键字都是语言本身预先设定好的,用户自己是不能创造关键字的。

register-寄存器

//用法:
register int a =100;//建议把a存放入寄存器

ps:只起到建议的作用,放不放要看编译器的意思,如果编译器认为这个变量会被大量频繁地使用,那就会存入编译器。
这个我们无需担心,编译器会帮我们判断该不该放寄存器的。

typedef-类型重命名

//用法:
typedef unsigned long long ull; //将 unsigned long long这个数据类型起个别名叫ull

typedef 全称 type define,typedef 并没有创建任何新类型,它只是为某个已存在的类型增加了一个方便使用的标签

详细介绍:https://www.cooboys.com/zhzs/202209/341459.html

static-静态修饰符

作用
(1)static修饰局部变量:普通的局部变量是放在栈区的,而被static的修饰的局部变量,是放在内存的静态区的,这使得局部变量出了作用域不会销毁,改变了变量的生命周期。
(2)static修饰全局变量:全局变量在整个工程(简单理解就是整个文件夹)中都可以使用的,全局变量具有 外部链接属性,在其他源文件(.c文件)内部只要适当地声明(用extern声明)就可以使用。当用static修饰全局变量时, 外部链接属性将变成 内部链接属性,只能在自己所在的.c文件内部使用,其他.c文件无法使用

链接解释: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
}

指针---地址


内存是电脑上特别重要的存储器,计算机中程序的运行都是在内存中进行的 。
为了有效的使用内存,就把内存划分成一个个小的内存单元,每个内存单元的大小是1个字节
为了能够有效的访问到内存的每个单元,就给内存单元进行了编号,这些编号被称为该内存单元的地址
指针理解的2个要点:
1. 指针是内存中一个最小单元的编号,也就是地址
2. 平时口语中说的指针,通常指的是指针变量,是用来存放内存地址的变量
总结:指针就是地址,口语中说的指针通常指的是指针变量
--------------------内存到底是个啥?
地址是怎么产生的?
答:
在32位机器上,有32根地址线(ps:地址总线就是地址线数量之和,说32位地址总线就是在指32根地址线),地址线就是根电线,一根地址线能表示1和0两个数,而1/0则是由 高低电平转换为数字信号来表示的。
地址的本质就是一个整型数值,它可以进行加减乘除等运算。

指针变量的大小
在32位的机器上,地址是32个0或者1组成二进制序列,那地址就得用4个字节的空间来存储,所以
一个指针变量的大小就应该是4个字节。
那如果在64位机器上,如果有64个地址线,那一个指针变量的大小是8个字节,才能存放一个地
址。
总结:
指针变量是用来存放地址的,地址是唯一标示一个内存单元的。
指针的大小在 32 位平台是 4 个字节,在 64 位平台是 8 个字节。


一些不务正业的内容...

在计算机内部中,根本没有啥C/C++、汇编、机器语言,它们眼里只有高低电平。

计算机使用电子器件(如晶体管)来进行逻辑运算和存储信息。这些电子器件的工作原理是基于高低电平的控制。

在机器语言中,高电平通常被表示为1,低电平通常被表示为0。这是因为电子器件(如晶体管)可以被控制为开或关状态,从而分别代表1和0。

在计算机内部,电子器件按照特定的电路设计连接在一起,形成逻辑门电路。逻辑门电路可以执行不同的逻辑操作,如与门、或门、非门等。这些逻辑门电路可以通过组合和重复连接,实现各种复杂的逻辑功能,如加法器、比较器、寄存器等。

当计算机执行指令时,它会将指令编码为一系列高低电平的信号,并将其传递到逻辑门电路中执行。通过这种方式,计算机能够进行算术运算、逻辑运算、存储和检索数据等操作。

总之,计算机通过高低电平的控制来实现逻辑运算和存储信息,并通过逻辑门电路来组合和重复连接实现不同的逻辑功能。

----------重学计算机组成原理(4)-还记得纸带编程吗?

机器语言的1010这一堆数字怎么变成高低电平的

----------高低电平怎么转化为数字信号0和1
 

----------数字电路中的晶体振荡器与时钟信号

----------数字电路的时钟信号是怎么产生呢?

时钟信号是什么?

  可以将时钟信号定义为在高电平和低电平状态之间振荡的特定类型的信号。信号的作用类似于节拍器,数字电路及时跟随节拍器以协调其动作序列,从而让CPU知道什么时候输出信号(即高低电平),有序地输出高低电平,成为序列。

时钟信号是如何产生的?

  计算机的时钟信号是由时钟发生器产生的,时钟发生器是一种晶体振荡器,其主要功能是将直流电转换为周期性振荡信号或频率非常高的交流信号,即一系列的连续波形,比如矩形波、正弦波等。

为什么需要时钟信号?

任何电路都是有延迟的,从输入信号输入到电路完成计算输出结果是需要时间的。但是麻烦的是这个时间对于所有电路都是不一样的,所以我们不知道究竟要等多久上一个电路才算是完成了运算,可以读取输出了。对于人来说这个问题还不大,我们只要多等等保证算完了就好了,但是对于电路来说就麻烦了:每一级电路都需要上一级电路的输出结果作为输入,可是要等多久上一级才能完成计算?就算我知道上一级的计算需要10ns时间,但是我该从哪里开始计时?用什么进行计时?
于是电路设计师们不得不加入了非常复杂的“握手信号”来控制数据读取,简单的说就是前一级电路在完成运算之后向下一级发送一个“可以读取了”的信号,下一级收到这个信号之后才能读取;下一级读取完毕之后再发一个“我读完了”的信号返回上一级,上一级才能开始下一个运算。
这不仅浪费了大量的电路在握手上,还极大地加大了设计难度。

为了解决这个问题,工程师们设计了“同步电路”。同步电路加入了时钟信号,所有电路模块的读取与输出都受到时钟信号的控制。比如一个电路模块,每次计算需要至多100ns的时间,而电路的时钟周期是50ns。那么我就知道这个电路至多需要两个周期的时间就能完成运算。为了冗余安全,我将这个模块设计为每三个时钟周期进行一次运算,它的下一级电路也每隔三个周期执行一次读取即可。
有了时钟信号,我只需要对每个电路设计“隔几个周期进行读/写”即可,不再需要在所有模块之间都设计握手信号,这极大地降低了设计难度。

转自知乎用户acalephs ------- 为什么需要时钟

-----------为什么 CPU 需要时钟才能工作?


为什么内存单元的大小是1个字节?
答:
因为设置成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 变量能放哪指针变量也能放哪。栈区、堆区、静态存储区,都能放。
总结:
指针变量,用来存放地址的变量。(存放在指针中的值都被当成地址处理)。
————————   一文读懂指针的本质
//用法

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个字节都被修改了

 原因:

总结:
指针的类型决定了,对指针解引用的时候有多大的权限(能操作几个字节)。
比如: char* 的指针解引用就只能访问一个字节,而 int* 的指针的解引用就能访问四个字节。

野指针

概念: 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)

野指针成因

1. 指针未初始化------随机的
比如在一个银行系统中,程序员弄个了野指针p,因为野指针的地址是随机的,万一指向了某个用户的存款余额,存有1000万,结果一个*p=20给干到了20块钱。这种事情是要杜绝的,所以必须要让指针初始化。
#include <stdio.h>
int main()
{ 
 int *p;//局部变量指针未初始化,默认为随机值
    *p = 20;
 return 0;
}

2. 指针越界访问------不正确的,即非法的
比如下例的数组
int main()
{
    int arr[10] = {0};
    int *p = arr;
    int i = 0;
    for(i=0; i<=11; i++)
   {
        //当指针指向的范围超出数组arr的范围时,p就是野指针
        *(p++) = i;
   }
    return 0;
}

3. 指针指向的空间释放
如下例
int* test()
{
	int a = 10;
	return &a;
}
int main()
{
	int* p = test();    //test()执行完后释放其所占内存空间
	*p = 20;            //因此原本属于a的空间随时能被覆盖
	return 0;           //所以*p=20只是暂时让该空间存了个20,未来还可能会被其他值覆盖
                        //从而导致做了无用功,这是我们需要避免的
}

如何规避野指针
1. 指针初始化
让指针有个明确指向的地址,或者先置其为null指针,反正就是先把指针控制起来,让其可控。
2. 小心指针越界
字面意思,把眼睛放亮点
3. 指针指向空间释放,及时置NULL
int* test()
{
	int a = 10;
	return &a;
}
int main()
{
	int* p = test();
    //置为空指针   
	*p=null;            
	return 0; 
}          
4. 避免返回局部变量的地址
5. 指针使用之前检查有效性
int main()
{
    int a=1;
    int* pa=&a;
	//.....一通操作后....
	 if (pa != NULL)//判断下这指针是个什么情况
	 {
        //如果非空代表可以使用,否则就是还没到使用的时机(取决于具体设计),就不操作了
		 *pa = 20;
	 }
	
}

指针运算

指针加减整数

 指针加减整数的本质是对指针进行偏移,以便在内存中访问相对于指针所指向的内存位置偏移量相应的内存单元。

偏移量:偏移量是一个值,表示某个位置与参考位置之间的差异或距离。在计算机科学中,偏移量通常用于指针或内存地址的计算,以便在内存中访问特定的数据或程序。例如,一个指针指向内存中的某个位置,通过加上偏移量,可以访问该位置后面的地址或前面的地址。偏移量可以为正数、负数或零,取决于参考位置和目标位置之间的距离。

偏移量=所加整数值*指针类型

当对指针进行加减整数操作时,实际上是将指针所指向的内存地址加上或减去一个偏移量,这个偏移量是指针加减整数的结果。这个偏移量是根据指针所指向的数据类型和加减整数的值来计算的,例如,对一个 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;
}
上面的 print1 print2 函数哪个好些?
答案是:首选 print2 函数。
原因:
函数传参的时候,参数是需要压栈的。
如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。
结论:
结构体传参的时候,要传结构体的地址。

操作符


算术操作符

+    -   *   /   %
1. 除了 % 操作符之外,其他的几个操作符可以作用于整数和浮点数。
2. 对于 / 操作符如果两个操作数都为整数,执行整数除法。而只要有浮点数执行的就是浮点数除      法。
3. % 操作符的两个操作数必须为整数。返回的是整除之后的余数。

移位操作符

<< 左移操作符
为逻辑移位,左边抛弃、右边补0,对应汇编指令shl
>> 右移操作符
为算术移位,左边用原该值的符号位填充,右边丢弃,对应汇编指令sar

 注:

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

位操作符

位运算符均是对补码进行操作

&       按位与,1&1=1;1&0=0;0&0=0
|         按位或,1|1=1;1|0=1;0|0=0
^        按位异或,1^1=0;0^0=0;1^0=1
注: 他们的操作数必须是整数 ,而且符号位也会参与运算

赋值操作符

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

单目操作符

!           逻辑反操作
-           负值
+           正值
&           取地址
sizeof      操作数的类型长度(以字节为单位)
~           对一个数的二进制按位取反,符号位也会被取反
--          前置、后置--
++          前置、后置++
*           间接访问操作符(解引用操作符)
(类型)       强制类型转换

关系操作符

>
>=
<
<=
!=   用于测试“不相等” ,不相等结果为1
==      用于测试“相等”,相等结果为1

逻辑操作符

&&     逻辑与
||          逻辑或
区分 逻辑与 按位与
区分 逻辑或 按位或
注:两者都拥有“短路”的特点,即:
当&&左边的运算为0的时候,就不再执行&&右边的运算了
当||左边的运算为1的时候,就不再执行||右边的运算了
区分 逻辑与 按位与
区分 逻辑或 按位或
1&2----->0
1&&2---->1
1|2----->3
1||2---->1

条件操作符 (三目运算符)

exp1 ? exp2 : exp3
int a=1;
int b=2;

a>b?a:b;
//满足a>b就返回a,否则就返回b

逗号表达式

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

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

[ ] 下标引用操作符
操作数:一个数组名 + 一个索引值
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);//->结构成员访问

}
	

类型转换


表达式求值
表达式求值的顺序一部分是由操作符的(优先级)和(结合性)决定。
同样,有些表达式的操作数在求值的过程中可能需要转换为其他类型。
隐式类型转换
C的整型算术运算总是至少以默认整型类型的精度来进行的。
为了获得这个精度,表达式中的字符和短整型操作数在使用之前被转换为普通整型,这种转换称为整型提升。
整型提升的意义:
表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度 一般就是int的字节长度,同时也是CPU的通用寄存器的长度。
因此,即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长度。
通用CPU(general-purpose CPU)是难以直接实现两个8比特字节直接相加运算(虽然机器指令 中可能有这种字节相加指令)。所以,表达式中各种长度可能小于int长度的整型值,都必须先转 换为int或unsigned int,然后才能送入CPU去执行运算。

如何进行整体提升

整形提升是按照变量的数据类型的符号位来提升的,就是说这个数在内存中存储的二进制形式(补码), 高位补充符号位,即 符号位为0,就用0来填充。为1就用1来填充。
正数的整型提升
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                        8
2、double                                8    
3、float                                    4
4、unsigned long int                4
5、long int                                4
6、unsigned int                        4
7、int                                        4
如果某个操作数的类型在上面这个列表中排名较低,那么首先要转换为另外一个操作数的类型后执行运算。
但是算术转换要合理,要不然会有一些潜在的问题。
float f = 3.14;
int num = f;//隐式转换,会有精度丢失

操作符的属性

复杂表达式的求值有三个影响的因素。
1. 操作符的优先级
2. 操作符的结合性
3. 是否控制求值顺序。
两个相邻的操作符先执行哪个?取决于他们的优先级。如果两者的优先级相同,取决于他们的结合性。

操作符优先级排行榜

操作符优先级大全----from 比特
操作
描述
用法示例
结果类
结合
是否控制求值
顺序
( )
聚组
(表达式)
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循环
不管三七二十一,上来就执行循环语句,然后才执行判断语句,所以至少都会执行一次
语法:
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语言中函数的分类

1. 库函数
C语言的基础库提供的函数称为库函数,它们不是业务性的代码。我们在开发的过程中每个程序员都可能用的到,为了支持可移植性和提高程序的效率,所以C语言的基础库中提供了一系列类似的库函数,方便程序员进行软件开发。
C语言常用的库函数都有:
  • IO函数
  • 字符串操作函数
  • 字符操作函数
  • 内存操作函数
  • 时间/日期函数
  • 数学函数
  • 其他库函数
使用库函数,必须包含 #include 对应的头文件。
2. 自定义函数
由程序员自己定义的函数称为自定义函数。
格式:

返回类型 函数名(形参的数据类型, 参数名){
 语句项;
}

例如:
int add(int x,int y){
   int z = x+y;
    return z;
}
函数的参数 —— 实参和形参
实际参数(实参):
真实传给函数的参数,叫实参。 实参可以是:常量、变量、表达式、函数等
无论实参是何种类型的量,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形
形式参数(形参):
形式参数是指函数名后括号中的变量,因为形式参数只有在函数被调用的过程中才实例化(分配内
存单元),所以叫形式参数。
形式参数当函数调用完成之后就自动销毁了,因此形式参数只在函数中有效。

由上图可知:函数在调用的时候, xy拥有自己的空间,同时拥有了和实参一模一样的内容。 所以我们可以简单的认为:形参实例化之后其实相当于实参的一份临时拷贝

传值调用
函数的形参和实参分别占有不同内存块,对形参的修改不会影响实参。(以上的图就是传值调用)

传址调用
传址调用是把函数外部的实参变量的内存地址传递给函数形参的一种调用函数的方式。
这种传参方式可以让函数和函数外边的变量建立起真正的联系,此时对变量的修改可以直接影响到变量的本体(存放在其内存地址的值)而不是分身(临时拷贝),也就是函数内部可以直接操 作函数外部传过来的实参。

函数的嵌套调用和链式访问

嵌套调用

int A(){
    printf("我被调用了");
}

int main(){

    A();  //函数里面再调用一个函数就是嵌套调用
}
函数可以嵌套调用,但是不能嵌套定义。 (嵌套定义会报语法错误

链式访问

把一个函数的返回值作为另外一个函数的参数。

printf("%d",printf("%d",123);//printf的返回值是打印出的字符个数

函数的声明和定义

函数声明:
1. 只是 告诉编译器有一个函数叫什么,参数是什么,返回类型是什么。但是具体是不是存在,函数
声明决定不了。
2. 函数的声明一般出现在函数的使用之前。要满足 先声明后使用
3. 函数的声明一般要放在头文件中的
函数定义:
函数的定义是指函数的具体实现,交待函数的功能实现。

函数递归---(传递后回归)


程序调用自身的编程技巧称为递归。

//待补充

函数栈帧的创建和销毁


什么是函数栈帧
我们在写C语言代码的时候,经常会把一个独立的功能抽象为函数,所以C程序是以函数为基本单位的。
 
函数栈帧(stack frame)就是函数调用过程中在程序的调用栈(call stack)所开辟的空间,这些空间是用来存放: 函数参数和函数返回值,临时变量(包括函数的非静态的局部变量以及编译器自动生产的其他临时变量),保存上下文信息(包括在函数调用前后需要保持不变的寄存器)。
以下的问题你能否回答出来?
  • 局部变量是如何创建的?
  • 为什么局部变量不初始化内容是随机的?
  • 函数调用时参数时如何传递的?传参的顺序是怎样的?
  • 函数的形参和实参分别是怎样实例化的?
  • 函数的返回值是如何带回的?

理解了函数栈帧的创建和销毁,你便能对这些与底层有关的问题洞若观火一般。

首先来理解栈的概念。

  栈( stack )是现代计算机程序里最为重要的概念之一,几乎每一个程序都使用了栈,没有栈就没有函数,就没有局部变量,也就没有我们如今看到的所有的计算机语言。
  栈是一种数据存储结构,用户可以将数据压入栈中(入栈, push ),也可 以将已经压入栈中的数据弹出(出栈, pop )。

  但是栈这个容器必须遵守一条规则:先入栈的数据后出 栈( First In Last Out FILO )。
在计算机系统中,栈则是一个具有以上属性的动态内存区域。程序可以将数据压入栈中,也可以将数据 从栈顶弹出。压栈操作使得栈增大,而弹出操作使得栈减小。
在经典的操作系统中,栈总是向下增长(由高地址向低地址)的。
在我们常见的 i386(32位微处理器 )或者x86 下,栈顶由成为 esp 的寄存器进行定位的。

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 表示)



相关寄存器
eax:通用寄存器,保留临时数据,常用于返回值
ebx:通用寄存器,保留临时数据
ebp:栈底寄存器,也称栈基址指针,永远指向栈基址(高地址)。栈基址就是栈底,一个意思
esp:栈顶寄存器 ,永远指向栈顶(低地址)
eip :指令寄存器,保存当前指令的下一条指令的地址
相关汇编命令
mov :数据转移指令
push :数据入栈,同时 esp 栈顶寄存器也要发生改变
*esp = x;
esp -= 4;
pop :数据弹出至指定位置,同时 esp 栈顶寄存器也要发生改变
出入栈时esp减多少要看是多少位模式,如果是32位,则存储一个地址要4字节,所以
sub :减法命令
add :加法命令
call :函数调用, 1 . 压入返回地址 2. 转入目标函数
jump :通过修改 eip ,转入目标函数,进行调用
ret :恢复返回地址,压入 eip ,类似 pop eip 命令

平台: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.数组在创建的时候如果想不指定数组的确定的大小就得初始化。数组的元素个数根据初始化的内容来确
定。
注: 数组创建,在 C99 标准之前, [] 中要给一个 常量 才可以,不能使用变量。在 C99 标准支持了变长数组的概念,数组的大小可以使用变量指定,但是数组不能初始化。 const修饰的变量也不行,因为const修饰的变量其实只是个不可修改的变量罢了。

一维数组的使用

1. 数组是使用下标来访问的,下标是从 0 开始。
2. 数组的大小可以通过计算得到。
 //计算数组的元素个数
    int arr[10]={0};
    int sz = sizeof(arr)/sizeof(arr[0]);

  //sizeof(arr)可计算出数组所占空间的大小,即所占空间=元素个数x数据类型

  //sizeof(arr[0])可计算出数组类型所占空间大小

  //由于所占空间=元素个数x数组类型所占空间大小,所以除以数组类型所占空间大小即得到元素个数

一维数组在内存中的存储

可见,数组在内存中是连续存储的,由于int型占4个字节,而一个内存单元=一个地址=1字节大小,所以元素之间差了4个地址。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值