前文补充
#include <iostream>
using namespace std;int main()
{int a = 10;int c = 20; // 将变量c定义在switch语句之前switch(a){case 1:{cout << ".........." << endl;cout << c << endl;}break;default:cout << ".........." << endl;break;}return 0;
}
这段代码存在一些问题,以下是代码的分析和问题说明:
代码分析
switch
语句的结构问题switch
语句的语法要求case
标签必须位于switch
语句块的最外层,而不能嵌套在其他语句(如printf
)之后。在你的代码中,printf("..........\n");
和int c = 20;
以及printf("..........\n");
出现在case
标签之前,这是不符合C++语法的。switch
语句的执行流程是从匹配的case
标签开始执行,直到遇到break
或switch
语句结束。因此,case
标签不能嵌套在其他语句中。
变量
c
的作用域问题在
switch
语句中,case
标签后的代码块(用大括号{}
括起来的部分)会创建一个新的作用域。在你的代码中,int c = 20;
定义在switch
语句块的外面,但在case 1
中尝试访问c
。由于c
的作用域问题,这会导致编译错误。
修改后的代码
以下是修改后的代码,修复了上述问题:
#include <iostream>
using namespace std;int main()
{int a = 10;int c = 20; // 将变量c定义在switch语句之前switch(a){case 1:{cout << ".........." << endl;cout << c << endl;}break;default:cout << ".........." << endl;break;}return 0;
}
修改说明
变量
c
的定义位置将
int c = 20;
定义在switch
语句之前,确保在switch
语句中可以正确访问c
。
switch
语句的结构将
printf
语句和case
标签的顺序调整为符合C++语法的结构。case
标签必须位于switch
语句块的最外层。
使用
cout
代替printf
为了保持代码风格的一致性,将
printf
替换为C++标准库中的cout
。
输出结果
假设a
的值为10,程序的输出将是:
..........
如果将a
的值改为1,程序的输出将是:
..........
20
本章重点
循环的语法理解
break
和continue
的作用goto
真的没人用吗void
的理解void
修饰函数和函数参数的作用void
指针的用途
1.9 do、while、for 关键字
三种循环各自语法 - 补充内容
while
循环
// while
条件初始化
while(条件判定){// 业务更新条件更新
}
for
循环
// for
for(条件初始化;条件判定;条件更新){// 业务代码
}
do while
循环
条件判定是放在后面的,是直接先执行代码块中的代码逻辑的!--- 至少会跑一遍
// do while
条件初始化
do{条件更新
}while(条件判定);do{}while(0);
三种循环对应的死循环写法 - 补充内容
while(1){
}for(;;){}do{}while(1);
书中代码,优化后
#include <stdio.h>
#include <windows.h>int main()
{while (1){char c = getchar();if ('#' == c){break;}printf("echo: %c\n", c);}system("pause");return 0;
}
在C语言中,使用 int c
而不是 char c
来接收 getchar()
的返回值是一个常见的最佳实践,原因主要与 getchar()
函数的返回值类型以及字符和整数的表示范围有关。下面详细解释为什么使用 int c
更好:
getchar()
的返回值类型
getchar()
函数的返回值类型是 int
,而不是 char
。其原型定义为:
int getchar(void);
这意味着 getchar()
返回的是一个整数(int
类型),而不是一个字符(char
类型)。返回的整数值可以表示一个字符的ASCII码,也可以是一个特殊的值 EOF
(表示文件结束)。
EOF
的值
EOF
是一个特殊的值,用于表示输入流的结束。在标准C中,EOF
的值通常定义为 -1
。由于 char
类型的范围通常是 0 到 255(对于无符号字符)或 -128 到 127(对于有符号字符),而 EOF
的值是 -1
,这可能会导致问题。
如果使用 char c
来接收 getchar()
的返回值,可能会出现以下问题:
有符号字符问题 :如果
char
是有符号类型(这是默认情况,但具体取决于编译器和平台),EOF
的值-1
可以被正确存储在char
中。但是,当char
被用作条件表达式时,可能会出现意外的行为。例如,if (c == EOF)
可能不会按预期工作,因为c
的值可能会被隐式转换为int
,导致比较失败。无符号字符问题 :如果
char
是无符号类型(可以通过编译器选项或平台设置来改变),EOF
的值-1
会被解释为一个非常大的正数(通常是 255)。这会导致if (c == EOF)
的比较永远失败,因为c
的值永远不会等于-1
。
使用 int c
的好处
使用 int c
来接收 getchar()
的返回值可以避免上述问题。int
类型的范围足够大,可以正确表示所有可能的字符值以及 EOF
。这样可以确保 if (c == EOF)
的比较总是按预期工作。
以下是修改后的代码,使用 int c
来接收 getchar()
的返回值:
#include <stdio.h>
#include <windows.h>int main()
{int c; // 使用int来接收getchar()的返回值while (1){c = getchar();if ('#' == c){break;}printf("echo: %c\n", c);}system("pause");return 0;
}
使用 int c
而不是 char c
来接收 getchar()
的返回值,可以确保程序能够正确处理 EOF
的值,避免因字符类型范围问题导致的错误。这是C语言编程中的一个常见最佳实践,有助于提高代码的健壮性和可移植性。
我们的1234的数字,可以打印出来,我们也就可以发现 printf 的价值,有作用,就是将一个整型转化成一个一个字符,显示到显示器当中(程序运行会默认打开三个文件:输入输出错误)
对于 ASCLL码表,在后面,我们会谈论到,这也就是机器不是只认识二进制吗?为什么还认识abcd这样的字符,更多详情,后续会介绍!
补充:
与getchar:从键盘上获取一个字符的操作相对应的是putchar,往标准输出上显示一个字符!
1.9.1 break
& continue
区别
在C语言中,break
和 continue
都是用于控制循环流程的语句,但它们的作用和使用场景有很大区别。
1. break
作用 :用于完全终止最内层的循环(
for
、while
、do-while
),跳出循环体,继续执行循环体之后的代码。使用场景 :当你在循环中遇到了某种条件,使得你不再需要继续执行循环,就可以使用
break
。比如在一个查找算法中,一旦找到了目标元素,就可以用break
跳出循环,避免无意义的继续循环。示例代码 :
#include <stdio.h>int main() {for (int i = 0; i < 10; i++) {if (i == 5) {break; // 当i等于5时,跳出循环}printf("%d ", i);}printf("\nLoop ended.\n");return 0;
}
0 1 2 3 4
Loop ended.
因为当 i
等于 5 时,break
语句执行,循环被终止,直接跳到循环体之后的代码。
2. continue
作用 :用于跳过当前循环体中的剩余代码,直接进入下一次循环迭代。也就是说,
continue
只是中断当前这一次循环的执行,而不是整个循环。直接跳到条件判定,而不是内部代码块!do while 也是如此!!!也是先到while的条件判定!都是跳转到条件判定!!!
但是对于for来说:我们有一个示例代码:也是证明代码:
int main()
{int i = 10;for( ; i < 10; ++i){printf("continue before\n");if(i == 5){printf("continue\n");continue;}printf("continue after\n");}return 0;
}
这就不是跳转到1了,想想,如果跳转到1,就会造成死循环了,所以,这个应该是跳到2,即++i!
#include <stdio.h>int main()
{int i = 0;for( ; i < 10; ){if(i == 5){printf("%d\n", i);continue;}++i;}return 0;
}
但是我们这么写的话就是会造成死循环!
continue
语句在 for
循环中的行为是:跳过当前迭代中 continue
之后的所有代码,然后执行 for
循环的第三部分(即增量表达式 ++i
),最后再进行条件判断开始下一次循环;而在您第二个没有增量表达式的 for
循环中,由于 continue
跳过了 ++i
语句,导致 i
的值永远停留在 5 无法增加,从而造成了死循环。
简单来说:
有增量表达式:
continue
→ 执行++i
→ 条件判断 → 下一次循环无增量表达式:
continue
→ 条件判断 → 下一次循环(i
不变)→ 死循环
使用场景 :当你在循环中遇到某些情况,不想执行当前循环体中的剩余代码,但仍然希望循环能够继续进行,就可以使用
continue
。例如在筛选数据时,跳过不符合条件的数据,继续处理符合条件的数据。示例代码 :
#include <stdio.h>int main() {for (int i = 0; i < 10; i++) {if (i % 2 == 0) {continue; // 当i为偶数时,跳过当前循环的剩余代码}printf("%d ", i);}printf("\nLoop ended.\n");return 0;
}
输出结果为:
1 3 5 7 9
Loop ended.
当 i
为偶数时,continue
语句执行,跳过了当前循环的 printf
语句,直接进入下一次循环迭代。
作用范围 :
break
是终止整个循环,而continue
只是中断当前这一次循环的执行。代码执行流程 :使用
break
后,循环直接结束,执行循环体之后的代码;使用continue
后,会跳过当前循环的剩余代码,直接进入下一次循环迭代。适用场景 :
break
适用于你确定不需要继续循环的情况;continue
适用于你只是不想执行当前循环的某些代码,但仍然希望循环继续进行的情况。
break
的细节补充
在嵌套循环中的表现 :
break
只能终止它所在的最内层循环。如果你有嵌套循环(即一个循环体内部还有另一个循环),break
只会终止最内层的循环,不会影响外层循环。例如:
#include <stdio.h>int main() {for (int i = 0; i < 3; i++) {for (int j = 0; j < 3; j++) {if (j == 1) {break; // 只终止内层循环}printf("i=%d, j=%d\n", i, j);}printf("Inner loop ended.\n");}printf("Outer loop ended.\n");return 0;
}
输出结果为:
i=0, j=0
Inner loop ended.
i=1, j=0
Inner loop ended.
i=2, j=0
Inner loop ended.
Outer loop ended.
可以看到,break
只终止了内层循环,外层循环仍然继续执行。
在
switch
语句中的使用 :break
也可以用在switch
语句中,用于防止代码的“穿透”。在switch
语句中,如果没有break
,程序会从匹配的case
开始执行,直到遇到break
或者switch
语句结束。例如:
#include <stdio.h>int main() {int num = 2;switch (num) {case 1:printf("One\n");case 2:printf("Two\n");break;case 3:printf("Three\n");break;default:printf("Default\n");}return 0;
}
输出结果为:
Two
如果没有在 case 2
后面加 break
,程序会继续执行 case 3
和 default
,这就是所谓的“穿透”。
continue
的细节补充
对循环控制变量的影响 :
continue
语句不会影响循环控制变量的更新。例如在for
循环中,continue
之后循环控制变量仍然会按照正常的逻辑更新。例如:
#include <stdio.h>int main() {for (int i = 0; i < 5; i++) {if (i == 2) {continue; // 跳过当前循环的剩余代码}printf("i = %d\n", i);}return 0;
}
输出结果为:
i = 0
i = 1
i = 3
i = 4
可以看到,i
仍然按照正常的逻辑从 0 增加到 4,continue
只是跳过了 i == 2
时的 printf
语句。
在嵌套循环中的表现 :和
break
一样,continue
也只影响它所在的最内层循环。例如:
#include <stdio.h>int main() {for (int i = 0; i < 3; i++) {for (int j = 0; j < 3; j++) {if (j == 1) {continue; // 只影响内层循环}printf("i=%d, j=%d\n", i, j);}}return 0;
}
输出结果为:
i=0, j=0
i=0, j=2
i=1, j=0
i=1, j=2
i=2, j=0
i=2, j=2
可以看到,当 j == 1
时,continue
语句跳过了内层循环中 j == 1
的那次迭代,但外层循环仍然正常进行。
3. 其他细节
可读性 :在使用
break
和continue
时,要注意代码的可读性。过度使用这些语句可能会使代码逻辑变得复杂,难以理解。例如,如果一个循环中有多个break
或continue
,可能会让人难以跟踪程序的执行流程。在一些情况下,可以通过调整循环条件或者使用标志变量来避免使用break
和continue
,使代码更加清晰。性能影响 :在大多数情况下,
break
和continue
对性能的影响可以忽略不计。它们只是改变了程序的控制流,并没有进行复杂的计算。但是,在一些极端情况下(例如在非常大的循环中频繁使用break
或continue
),可能会对性能产生一定的影响。不过,这种影响通常比代码的可读性和逻辑正确性要次要得多。与其他控制语句的配合 :
break
和continue
可以与其他控制语句(如if
、else
、switch
等)配合使用,以实现复杂的控制逻辑。在使用时,要注意它们的优先级和作用范围,确保程序的逻辑符合预期。例如,在一个if
语句中使用break
或continue
,要清楚它们会终止或跳过的是哪个循环。
总之,break
和 continue
是C语言中非常有用的控制语句,但要根据具体的情况合理使用,避免滥用导致代码难以理解和维护。
1.9.2 循环语句的注意点
推荐书中的内容
[规则1 - 36] 思想推荐,但是不强制。另外书中代码严重不推荐,外小内大,一般效率是会高一些,但是差别不会特别大,实际测试的时候,出现效率现象出现反直觉现象,也不要意外。
[建议1 - 37] 推荐,给两个理由:循环次数明确,便于进行十数计算
[规则1 - 38] 推荐,代码量问题要结合场景
[规则1 - 39] 部分推荐,代码量问题要结合场景
[规则1 - 40] 推荐,实际工作中,也基本很少遇到
[规则1 - 41] 推荐
1.10 goto
关键字
基本使用
// 使用goto模拟实现循环
#include <stdio.h>
#include <windows.h>int main()
{int i = 0;START:printf("[\xad]goto running .... \n", i);Sleep(1000);++i;if (i < 10){goto START;}printf("goto end ....\n");system("pause");return 0;
}
可以根据goto关键字,直接跳转到goto后面所指定的标签所对应的位置处,也就是说如果START:放在后面,goto到标签的中间的部分就不会执行了!其实我们想要怎么跳转就怎么跳转!
goto只能在本代码块内进行使用,函数的都不能跨越,文件间就更不用说了!
有什么问题
[规则1 - 42] 推荐
我的建议
很多公司确实禁止使用goto
,不过,这个问题我们还是灵活对待,goto
在解决很多问题是有有效的。我们可以认为goto
引用场景较少,一般不使用,但是必须得知道goto
,需要的时候,也必须会用
有人用吗
Linux内核源代码中充满了大量的goto
,只能说我们目前,或者很多公司的业务逻辑不是那么复杂
1.11 void
关键字
1.11.1 void a
// 问是否可以定义变量
#include <stdio.h>
#include <windows.h>int main()
{void a;system("pause");return 0;
}
为何 void
不能定义变量
定义变量的本质:开辟空间
而void
作为空类型,理论上是不应该开辟空间的。即使开了空间,也仅仅作为一个占位符看待
所以,既然无法开辟空间,那么也就无法作为正常变量使用,既然无法使用,编译器干脆就不让他定义变量了。
- 在vs2013中,
sizeof(void)=1
(但编译器依旧理解成,无法定义变量) - 在Linux中,
sizeof(void)=1
(但编译器依旧理解成,无法定义变量)
1.11.2 void修饰函数返回值和参数
场景1:void作为函数返回值
// void修饰函数返回值和参数
#include <stdio.h>
#include <windows.h>void show()
{printf("no return value!\n");
}
int main()
{show();system("pause");return 0;
}
如果自定义函数,或者库函数不需要返回值,那么就可以写成void
那么问题来了,可以不写吗?不可以,自定义函数的默认返回值是int!!!
所以,没有返回值,如果不是void,会让阅读你代码的人产生误解:他是不是忘了写,还是想默认int?
结论:void作为函数返回值,代表不需要,这里是一个“占位符”的概念,是告知编译器和给阅读源代码的工程师看的。
【规则1 - 43】推荐
场景2:void 作为函数参数
// void 作为函数参数
// 如果一个函数没有参数,我们可以不写,如test10
#include <stdio.h>
#include <windows.h>int test1() // 函数默认不需要参数
{return 1;
}
int test2(void) // 明确函数不需要参数
{return 1;
}
int main()
{printf("%d\n", test1(10)); // 依旧传入参数,编译器不会告警或者报错printf("%d\n", test2(10)); // 依旧传入参数,编译器会告警(vs)或者报错(gcc)system("pause");return 0;
}
结论:如果一个函数没有参数,将参数列表设置成void,是一个不错的习惯,因为可以将错误明确提前发现
另外,阅读你代码的人,也一眼看出,不需要参数。相当于“自解释”。
阅读书中内容,【规则1-44】推荐
题外话,尽管如此,如果这点你不习惯,也不勉强。
1.11.3 void指针
void不能定义变量,那么void*呢?
#include <stdio.h>#include <windows.h>int main()
{void *p = NULL; // 可以system("pause");return 0;
}
为什么void可以呢?因为void是指针,是指针,空间大小就能明确出来
场景:void* 能够接受任意指针类型
#include <stdio.h>
#include <windows.h>int main()
{void *p = NULL;int *x = NULL;double *y = NULL;p = x; // 同上p = y; // 同上system("pause");return 0;
}
反过来,在vs/gcc中也没有报错。书中编译器很老了,我们严重不推荐
结论:但是我们依旧认为,void*的作用是用来接受任意指针类型的。这块在后面如果想设计出通用接口,很有用
比如:
void *memset ( void * ptr, int value, size_t num );
void * 定义的指针变量可以进行运算操作吗
【规则1 - 45】现场验证,各种标准我们了解一下
//在vs2013中
#include <stdio.h>
#include <windows.h>int main()
{void *p = NULL;p++; // 报错p += 1; // 报错system("pause");return 0;
}
//在gcc4.8.5中
#include <stdio.h>
#include <stdio.h>int main()
{void *p = NULL; // NULL在数值层面,就是0p++; // 能通过printf("%d\n", p); // 输出1p += 1; // 能通过printf("%d\n", p); // 输出2return 0;
}
为什么在不同的平台下,编译器会表现出不同的现象呢?
根本原因是因为使用的c标准版本的问题。具体阅读书。
void * 用来设计通用接口
【规则1 - 46】推荐
1.11.4 void不能代表一个真实的变量
已经讲过,此处略过