每天一点小进步!
我还想说,文章我会慢慢对其改进,目前使用md编译器还待成熟,暑假会对其进行专门深造敬请期待!
文章排版问题,文章内容提高还有待加强,同志们对其不满意的地方可以直接私聊我,我会慢慢改进,还有对其知识没讲到位的地方,也请各位大佬多多指点,总结我不会说的太多,内容大体都会概括到,我会在文章开头指出自己的不足,还望大佬们多多指教。我们一起进步!
我是一个代码发烧友,对问题,数学逻辑都感兴趣,有考研的小伙伴,我们可以一起进步,我的弱项是英语,数学方面还行吧!感谢老铁们的支持!
对博客我会总结知识要点,实例后期gitee维护起来了,可以开发!暂时实力有限,我也有信心去做好。加油!
先来写一段代码来直接run起来。看看switch语句中一些注意点。
#include
int main()
{
int day = 0;
scanf("%d", &day);
switch (day)
{
case 1:
printf("星期一\n");
break;
case 2:
printf("星期二\n");
break;
case 3:
printf("星期三\n");
break;
case 4:
printf("星期四\n");
break;
case 5:
printf("星期五\n");
break;
case 6:
printf("星期六\n");
break;
case 7:
printf("星期天\n");
break;
default:
break;
}
return 0;
}
提一句:scanf函数在visual studio 2022中是会出现报警的。有两种方法可以解决:
- #define _CRT_SECURE_NO_WARNINGS 1(这个必须放在文件顶部)
- #pragma warining(disable:4996)(但是建议放在文件顶部)
这就是switch语句基础语法。那么我们需要知道什么呢?
if语句和switch语句很相似。但是if语句判断的条件更丰富,switch只能用在整形或者整型表达式判定。在switch语句中case执行的是判断功能,break执行的是分支功能。
注意:
#include
int main()
{
int day = 0;
scanf("%d", &day);
switch (day)
{
case 1:
printf("星期一\n");
printf("星期一\n");
printf("星期一\n");
printf("星期一\n");
break;
case 2:
printf("星期二\n");
break;
case 3:
printf("星期三\n");
break;
default:
break;
}
return 0;
}
像这种是可以一个判断,多个语句的。那么这样可以吗?
#include
int main()
{
int day = 0;
scanf("%d", &day);
switch (day)
{
case 1:
int a = 1;
printf("星期一\n");
printf("星期一\n");
printf("星期一\n");
printf("星期一\n");
printf("%d\n",a);
break;
case 2:
printf("星期二\n");
break;
case 3:
printf("星期三\n");
break;
default:
break;
}
return 0;
}
在visual studio 2022中是可以的,但是会有报错。怎么解决呢?
int main()
{
int day = 0;
scanf("%d", &day);
switch (day)
{
case 1:
{
int a = 1;
printf("星期一\n");
printf("星期一\n");
printf("星期一\n");
printf("星期一\n");
printf("%d\n", a);
}
break;
case 2:
printf("星期二\n");
break;
case 3:
printf("星期三\n");
break;
default:
break;
}
return 0;
}
在case语句中直接加上大括号,就可以解决这个问题了。(不建议这样写,建议套用函数)
同样也可以一个多个判断条件执行同一语句。
default问题:default可以在switch语句中任何一个位置。
规则:
#include
int main()
{
int day = 0;
int quit = 0;
while (!quit)//!quit表示没有退出
{
scanf("%d", &day);
switch (day)
{
case 1:
printf("星期一\n");
break;
case 2:
printf("星期二\n");
break;
case 3:
printf("星期三\n");
quit = 1;//quit=1就表退出
default:
break;
}
}
return 0;
}
不建议这种方法:
#include
int main()
{
int day = 0;
int quit = 0;
while (!quit)//!quit表示没有退出
{
scanf("%d", &day);
switch (day == 1)
{
case 1:
printf("星期一\n");
break;
case 2:
printf("星期二\n");
break;
case 3:
printf("星期三\n");
quit = 1;//quit=1就表退出
default:
break;
}
}
return 0;
}
3. const修饰的变量是否满足switch整型判断的要求呢?
答案是不满足的。
4. 按执行频率排列case语句。
总结:
- switch语法结构中,case完成判断功能,break完成分支功能,default处理异常情况。
- 去掉break的方法:执行语句 == 1(不要用有效的bool类型);case多条语句中不能定义变量,如果要,就用{}或者函数。
3.执行多条语句时,case语句后不用写break。
4.default可以出现在switch语句中的任何地方,建议写在结尾。
5.case中不要用const修饰的常属性变量,建议用好的布局case方式。
#include
int main()
{
int a = 10;//循环初始化
while (a > 0)//循环判断条件
{
printf("%d\n", a);
a--;//循环条件更新
}
return 0;
}
int main()
{
for (int i = 1; i < 10; i++)//初始化,条件判断,条件更新
{
printf("%d\n", i);
}
return 0;
}
先执行后循环,至少执行一次。
int main()
{
int a = 10;//循环初始化
do {
printf("%d\n", a);
a--;//条件更新
} while (a > 0);//条件判断
}
#include
int main()
{
while(1){
printf("hello\n");
}
return 0;
}
#include
int main()
{
for (;;)
{
printf("hello\n");
}
return 0;
}
#include
int main()
{
do
{
printf("hello\n");
} while (1);
return 0;
}
这就没什么好说的了。
下面我们来看看测试。
首先初步了解基本的接口getchar()。
getchar就是从流中获取一个字符。
首先我们了解在任何c程序之后,运行时,都会打开三个输入输出流(流:简单理解就是文件输入数据,然后保存下来并显示。)
默认打开stdin(标准输入)、stdout(标准输出)、stderr(标准错误),三个文件对应的类型都是file*,程序运行时默认就打开了这三个文件,stdin对应的设备就是键盘,stdout和stderr对应的设备就是显示器。
说了上面这么多,那为什么c语言从来没有打开过文件,可以通过键盘输入数据,还能显示屏输出?就是因为默认打开了三个输入输出流文件。
#include
int main()
{
while(1)
{
int c = getchar();
if('#' == c){
break;
}
printf("%c\n",c);
}
pringf("循环结束\n");
return 0;
}
输出结果:
那么我们可以看到输出后隔了两行 ,这是什么问题呢?
当我们输入时,我们会按下回车(enter)键,相当于一个\n,就多换行了一次。也就出现现在的结果。怎么解决呢?其实很简单,就是把%c后的\n删掉就ok了。所以,注意getchar会获取回车。
补充:getchar返回是int类型,如果返回不在八个比特位[0 ~ 255]取值范围,就会出现数据丢失,不能读取全部的二进制数据,发生截断。
通过键盘输入输出的内容全是字符,怎么证明呢?
显然就是输出的全是字符。
那么补充一点:键盘和显示器都是字符设备。
#include
int main()
{
while (1) {
int c = getchar();
if ('#' == c)
{
break;
}
putchar(c);
}
printf("\nbreak out\n");
return 0;
}
运行结果:
可见:break是跳出循环。直接跳出到循环作用域外。
#include
int main()
{
while (1) {
int c = getchar();
if ('#' == c)
{
continue;
}
putchar(c);
}
printf("\nbreak out\n");
return 0;
}
运行结果:
可见:continue的作用是本次循环结束。下次循环再从循环开始,什么意思?看图就知。
这是在while循环中,在do while循环中:
int main()
{
do
{
int c = getchar();
if ('#' == c)
continue;
putchar(c);
} while (1);
return 0;
}
continue是从条件判断处开始。
在for循环中,continue是从哪里开始呢?
int main()
{
int i = 0;
for (; i < 10; i++)
{
printf("continue before:%d\n", i);
if (5 == i)
continue;
printf("continue after:%d\n", i);
}
return 0;
}
while和do while循环中的continue都是从条件判断开始,而for循环是从条件调整开始的。
#include
int main()
{
int i = 0;
for(i=0;i<10;i++){
printf("%d ",i);
}
return 0;
}//更直观的知道了循环次数,如果是i<10写成i<=9,则不是很直观
#include
int main()
{
int i = 3;
for(i=3;i<10;i++){
printf("%d ",i);
}
return 0;
}//这样直接做减法,直接求出次数
直接run代码起来。
//后goto死循环
#include
int main()
{
end:
printf("hello 1\n");
printf("hello 2\n");
printf("hello 3\n");
goto end;
printf("hello 4\n");
printf("hello 5\n");
printf("hello 6\n");
return 0;
}
//前goto不是死循环
#include
int main()
{
goto end;
printf("hello 4\n");
printf("hello 5\n");
printf("hello 6\n");
end:
printf("hello 1\n");
printf("hello 2\n");
printf("hello 3\n");
return 0;
}
基本goto语法知道了,下面我们来测试一下。
int main()
{
int i = 0;
start:
printf("[%d] goto running ...\n",i);
i++;
if (i < 10)
{
goto start;
}
printf("goto end ... \n");
return 0;
}
能力强可以使用goto语句,并有奇效。
#include
int main()
{
void v;//err
printf("%d\n",sizeof(void));//打印结果是0
return 0;
}
在不同的visual studio 2022中会出现报错,在别的编译器上就不一定,大家都应该理解void没有开辟空间,其实不是。在visual studio 2022中打印的是0,但是gcc编译器是1,所以不能用来定义变量是因为void在编译器中被约定的是空类型,强制的不能用来定义变量。
#include
void test() //test()
{
printf("success");
return 1;
}
int main()
{
//test(); //ok
int tes = test();//err
return 0;
}
总结:
- c语言中函数可以不带返回值,默认返回值是int类型。(不需要返回值时,应该带上void类型而不是不写返回类型,这要会让读者造成误解,到底是默认值还是你忘写了返回类型)
- void修饰函数返回值时,1.作为占位符,让用户明确不需要返回值。2.告知编译器,这个返回值无法接受。
#include
int test1()
{
return 1;
}
int test2(void)//告知这个函数不需要传参
{
return 1;
}
int main()
{
test1(1, 2, 3);//可以传参
test2(1, 2, 3);//也可以运行,但是有警告,void参数列表声明
return 0;
}
当void作为函数参数时,作用是告知用户或者编译器不需要传参。
#include
int main()
{
void *p = NULL;//OK
double* d = NULL;
int* i = NULL;
d = p;//ok
i = p;//ok
p = d;//ok
p = i;//ok
return 0;
}//编译通过(无报错)
因此,void * 可以被任何类型的指针接收,void *可以接收任意类型指针。
#include
int main()
{
void *p = NULL;
p++;//err
p--;//err
return 0;
}
在visual studio 2022是不能运行的,在linux中是可以的。不同平台有不同的现象。
为什么不同平台,编译器会有不同的结果?
因为不同的平台使用的c标准也是不一样的。
补充:
void *是用来设计通用接口的。
void *是不能解引用的。
首先,有两个问题引出。
#include
char *show()
{
char str[] = { "hello bit" };
return str;
}
int main()
{
char* s = show();
printf("%s\n", s);
return 0;
}
先来看看运行结果:
显然乱码了,那么我们一起来分析一下,为什么乱码了。
首先,我们知道c语言中,函数只要被调用就要向内存开辟空间,下面我们来看看这张图:
看完这张图,我们来整理一下这段代码的思路:首先有main函数,只要是函数就要在栈区开辟空间,那么首先编译器会提前预估main函数中的需要空间大小有多少,然后开辟特定的空间够main函数使用,然后调用show函数,在栈区开辟show函数栈帧,当show函数返回,栈帧释放,数据无效,然后执行main函数中的printf函数,因为show函数栈帧释放了,所以printf函数覆盖了show函数,变成了乱码。
怎么证明show函数数据并没有丢失,而是无效呢?下面来看两张监视图便知。
printf函数调用前:
printf函数调用后:
显然,show函数中的数据并不是清空了,而是无效了被pirntf函数覆盖了。
解释了show函数中的变量都是临时的,那么为什么临时变量具有临时性?
因为,函数调用,形成栈帧;函数返回,栈帧释放。
推荐:return语句不可返回指向“栈内存”的指针,因为该内存在函数体内结束时被自动销毁。
知道了这些,我们来看看返回值临时变量接收的本质。通过一段代码解释。
#include
int GetData()
{
int x = 0x11223344;
printf("run get data!\n");
return x;
}
int main()
{
int g = GetData();
printf("g = %x\n", g);
return 0;
}
运行结果是这样的:
我们就在想,临时变量不是具有临时性吗?不应该直接释放空间,没有返回值吗?下面我们用反汇编来看看是为什么出现这种情况。
来看两站反汇编图就能解决这个问题:
GetData函数中:
mian函数中:
所以,函数返回值是通过寄存器方式返回给函数调用方。
当然,如果不接受也是放在eax寄存器中,并不处理eax寄存器。
const是干什么的?
是在修饰变量时,为变量附上只读属性。
#include
int main()
{
int a = 10;
a = 40;//ok
const int b = 20;//不可直接被修改
//b = 30;//err
printf("%d\n", a);
printf("%d\n", b);
return 0;
}
从上述代码可以看出const修饰变量时,不可直接被修改,但是可以通过指针间接修改。
#include
int main()
{
const int a = 10;
//a = 20;//err
int* p = (int*)&a;
*p = 20;
printf("%d\n", a);
return 0;
}
//输出结果是20
可以通过指针来修改,那么这么说const不就没有意义了吗?
不是的,const的意义:
- 告知编译器这里不能修改,如果修改就会出现报错。
- 告知程序员这里不能修改,具有“自描述”意义。
另外注意一下:
#include
int main()
{
char* p = "hello world!";
*p = "hello";
printf("%s\n", p);
return 0;
}
注意这段代码,字符串常量是真正意义上的不能被修改,是被系统约束的。
int mian()
{
const int a = 10;
int arr[a];//err
return 0;
}
在visual studio 2022中不能被编过的,原因是数组表达式中必须是真正意义上的常量。
先通过变量赋值了解指针赋值:
#include
int main()
{
int a = 20;
a = 30;//a指的是空间
printf("%d\n", a);//输出30
return 0;
}
再来了解指针赋值(重温:指针变量就是存储地址的空间,指针是地址):
#include
int main()
{
int a = 20;
int b = 10;
int* p = &a;
p = &b;//p指针变量的空间
int* q = NULL;
q = p;//p指的是p存储的地址
printf("%d\n", *p);//输出10
printf("%d\n", *q);//输出10
return 0;
}
理解了指针变量和指针的区别,下面我们再来看看const修饰指针的四种情况。
- const int *P;
- int const *p;
- int *const p;
- const int *const p;
#include
int main()
{
int a = 10;
const int *p = &a;
*p = 100;//err
p = (int*)100;//ok
return 0;
}
注意:这里的const修饰的是int *类型(也就是修饰的是指针变量,并不是p这个指针),指的是int *指针空间不能直接被改变,换言之,就是p指针指向的内容不能被改变。p指针是可以被赋值的,所以p=100可以被编译器编过。
#include
int main()
{
int a = 10;
int *const p = &a;
*p = 100;//ok
p = (int*)100;//err
return 0;
}
注意:这里的const修饰的是p这个指针,而不是p这个指针变量,所以p=100是error,p这个指针是不能直接被修改的,这里的int并没有别修饰,所以p这个指针变量是可以被修改的,所以p=100是OK的。
#include
int main()
{
int a = 10;
const int* const p = &a;
*p = 100;//err
p = 100;//err
return 0;
}
有了上面第一种情况和第三种情况,我们就清楚第四种情况为什么*p和p都不能被修改。
重温:函数传参就要形成临时变量。
证明:
#include
void show(int* _p)
{
printf("in_show:%p\n", &_p);
}
int main()
{
int a = 10;
int* p = &a;
printf("int_main:%p\n", &p);
show(p);
return 0;
}
#include
const int* GetValue()
{
static int a = 10;
return &a;
}
int main()
{
const int* p = GetValue();//两边类型一样就不会报错
//*p = 100;//err
return 0;
}
小结:通过这段代码就能很好的解释了const修饰函数返回值,因为GetValue返回的是const修饰的指针类型,所以隐藏含义就是不让其更改。一般内置类型返回加const修饰是没有意义。
volatile关键字是用来干什么的?
编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。
看到这段文字就是懵的。优化是什么?
正常逻辑就是把数据读取到内存中,然后再加载到CPU中的寄存器,进行运算;但是优化是什么,就是不再返回到内存再次读取,而是直接保存在寄存器中,下一次直接运算即可。
通过这段代码解析:
#include
int pass = 1;
int main()
{
while (pass) {
}
return 0;
}
#include
volatile int pass = 1;
int main()
{
while (pass) {
}
return 0;
}
再来看看这段代码在Lunix的反汇编:
所以,对比看出来volatile忽略了编译器的优化,保持内存可见性。
#include
int main()
{
const volatile int a = 10;
printf("%d\n", a);
return 0;
}
这里visual studio 2022是不会报错的,原因是const是从写入内存角度,而volatile是从读取内存数据角度谈的,二者不冲突。
extern在多文件使用
声明变量、声明函数
extern int a; //声明变量
extern void function();//声明函数
声明函数时不建议舍掉extern,在上一篇中,我们已经说了extern,这里就不再赘述了。
这里我们不说语法细节部分,我们只是单纯理解一下空结构体多大和柔性数组,后期我们针对结构体再次解释。
#include
struct stu {
};
int main()
{
printf("%d\n", sizeof(struct stu));
return 0;
}//直接这样写会出现报错
//在visual studio 2022中提示结构体中必须至少要有一个成员
柔性数组就是C99中,结构中的最后一个元素允许是未知大小的数组,但是结构中的柔性数组成员前面必须至少有一个其他成员。
介绍了基本柔性数组的概念,我们一起来通过一段代码来知道柔性数组怎么用这个问题。
首先,柔性数组占用空间吗?
通过这个图就能说明柔性数组是不占空间的。
我们再来看看柔性数组是用来干什么的?
#include
struct stu{
int num;
//........还有很多的成员,直接省略简化
int arr[0];
};
int main()
{
int i = 0;
struct stu* p = malloc(sizeof(struct stu) + sizeof(int) * 10);//加大空间
p->num = 10;
for (i = 0; i < p->num; i++) {
p->arr[i] = i;
}
free(p);//free释放空间
return 0;
}
这段代码怎么理解呢?调试起来通过一张图去理解。
这里暂时只说这么多,理解一下即可,后期会对柔性数组再次全面认识。
这里就不谈语法知识了,后期还会对union进行再一次的认识。
#include
union un {
int a;
char c;
}u;
int main()
{
printf("%zd\n", sizeof(union un));
return 0;
}//输出结果是4
通过这段代码,我们一起来探讨一下union的内存分布问题:
这个图解释了上面的代码的内存分布,这里有个问题,就是这个char c 空间是在高地址处还是低地址处?
通过打印地址进行检验:
#include
union un {
int a;
char c;
}u;
int main()
{
printf("%p\n", &u);
printf("%p\n", &(u.a));
printf("%p\n", &(u.c));
return 0;
}
运行结果:
通过这个问题可以看出,每个共用体元素都是第一个元素,每个元素的起始地址都是相同的,都是从低地址处开始的。
再来看看大小端对union共用体的影响,首先还是把代码run起来。
#include
union un {
int a;
char c;
};
int main()
{
union un x;
x.a = 1;
if (x.c == 1)
{
printf("小端\n");
}
else
{
printf("大端\n");
}
return 0;
}//输出结果是小端
怎么对这里的x.a和x.c分析呢?看图:
小端的话值就是1,大端的话值为0。
还是一样把代码run起来:
#include
union un {
int a;
char c[5];
};
int main()
{
printf("%d\n", sizeof(union un));
return 0;
}输出结果是8
这个就很好理解,就是当共用体中空间大小不是定义类型空间大小的倍数的时候,就按共用体中所有类型空间大小的大于整个共用体的空间的最小倍数。例如这里的:整个共用体的空间大小是5,这里面有两个类型int和char,在高于5的情况下,两个类型的最小公倍数是8,所以打印出8。
再来看一个例子:
#include
union un {
int i;
char a[4];
}*p,u;
int main()
{
p = &u;
p->a[0] = 0x39;
p->a[1] = 0x38;
p->a[2] = 0x37;
p->a[3] = 0x36;
printf("ox%x\n", p->i);
return 0;
}//输出结果是ox36373839
这就很简单理解,因为数据储存是以小端方式存储的,a数组和i都是从低地址处进入共用体的空间的,低地址对应的是低权值,所以打印结果是:ox36373839。
enum是枚举关键字,它的作用是什么呢?
列举很多常量,这些常量都是有相关性的。
#include
enum color {
RED,
YELLOW,
WHITE,
BLUE,
BLACK,
GREEN
};
int main()
{
printf("%d\n", RED);
printf("%d\n", YELLOW);
printf("%d\n", WHITE);
printf("%d\n", BLUE);
printf("%d\n", BLACK);
printf("%d\n", GREEN);
return 0;
}
看一下这段代码,运行结果是:
012345。所以可以看出枚举常量标识符代表的是一个整型值,并不是字符串。
再来看看这段代码:
#include
enum color {
red=3,
yellow,
white,
blue,
black=5,
green
};
int main()
{
printf("%d\n", red);
printf("%d\n", yellow);
printf("%d\n", white);
printf("%d\n", blue);
printf("%d\n", black);
printf("%d\n", green);
return 0;
}
输出结果是:345656。
可以看出,枚举常量整型值是从初始开始依次递增的。
typedef作用是什么?
本质上就是对类型进行重命名。
typedef int * ptr_t;//重新定义新类型,相当于一个独立的类型。
ptr_t p1,p2;
//问:p1,p2分别是什么类型?
看图:
看图便知:p1,p2两个变量的类型都是指针类型。
在这里补充一点:
typedef int * ptr_t;
int* p1,p2;
//问:这里的a,b变量都是一样的类型吗?
看图:
看图便知,p1的类型是int*,p2的类型是int,为什么呢?你可以理解为就近原则吧,p1离 * 更近,所以p1变量是int 指针类型,从本质上来说这是规定,不要问为什么。
这里我们看到int修饰 两个变量时一个是int *,一个是int。那么我们怎么用同一形式定义两个指针呢?
int *p1,*p2;
这样就是定义两个指针类型。
#define PTR_T int*
PTR_T p1,p2;
//问:p1,p2分别是什么类型?
看图:
看图便知,p1变量是int*指针类型给,p2变量是int类型。和上述的(int * p1,p2;)一个道理。
先看两段代码,再一次理解typedef和define宏定义的区别:
typedef int int32;
int main()
{
unsigned int32 b;//err
return 0;
}//typedef定义的是一个独立类型,不能再对类型进行修饰
#define INT32 int
int main()
{
unsigned INT32 a;//ok
return 0;
}
不行的。报错显示:指定了一个以上的存储类。
对这个报错显示进行解释:
存储类型关键字有五个:auto、extern、register、static、typedef这五个。存储关键字,不可以同时出现,也就是说,在变量定义的时候只能有一个。