该系列文章系个人读书笔记及总结性内容,任何组织和个人不得转载进行商业活动!
本文旨在重识C语言,可能没有那么完成和深入,他可能不是一扇门,但作为一扇窗他已经够了,哈哈……
你需要在电脑上安装的编译器:
gcc:GNU编译器套装;Unix的话已经带了,Mac 下载Xcode开发工具即可;
1 C语言入门:进入C语言的世界
C语言用来创建空间小、速度快的程序:抽象层次更低,用C语言写的代码更接近于机器语言;
计算机只能理解一种语言——机器语言,即一串二进制0、1流;
运行示例:
可以在编译器的帮助下将C代码转化为机器代码;
1)我们创建一个源文件开始(Code1_1)
/*
* 先看看一个源码示例
*/
#include <stdio.h>//标准输入输出头文件
int main(){
puts("C Rocks!");
return 0;
}
2)通过gcc 1_rocks.c编译源代码,编译器会检查错误;-o 1_rocks 编译输出到指定执行文件;
3)输出:编译器会创建一个叫可执行文件的新文件,文件中是机器代码,及计算机能够理解的0、1流,而这个文件就是可以运行的程序;输入 ./1_rocks运行;
(图1)
3种C标准:
你可能会遇到3种C标准:
1)ANSI C始于20世纪80年代后期,适用于最古老的代码;
2)C99,99年开始的,有很大改进;
3)C11,11年发布的最新标准,加入了很新的语言特性;
(Code1_2)
/*
* 猜猜这个程序运行会是什么样?
*/
#include <stdio.h>//标准输入输出头文件
int main() {
char ex[20];
puts("输入男友的姓名:");//在命令提示符 或 终端上 显示字符串
scanf("%19s",ex);//将用户输入的信息存在数组中
printf("亲爱的%s,我们分手吧。\n",ex);//显示一条包含输入文本的消息
}
一个典型的C源文件:
1)C语言通常以注释开头:代码意图,许可证或版权信息;
2)接下来是include部分:C语言很小,需要使用外部库;为了告诉编译器程序使用了哪些外部库,需要包含(include)相关库的头文件;
stdio.h:最常见,库中包含了那些能在终端上读写数据的代码;
3)源文件找到最后一件东西是函数:所有的C代码都运行在函数中,最重要的main()函数,是程序中所有代码的起点;
(Code1_3)
/*
* 计算牌盒属数量的程序。
* 本代码使用“拉斯维加斯公共许可证”。
* [c]1024,学院21点扑克游戏小组。
*/
#include <stdio.h>
int main() {
int decks;
puts("输入几副牌");
scanf("%i",&decks);
if (decks < 1) {
puts("无效的副牌");
return 1;
}
printf("一共有%i张牌\n",(decks * 52));
return 0;
}
main函数聚焦:
1)计算机会从main()函数开始运行,如果没有该函数,程序将无法启动;早起main()函数是void类型,C99标准中返回类型是int;2
2)main()函数返回0表示正常,返回1表示有问题;
函数名在返回值类型之后出现,如果函数有参数,可以跟在函数名后面;最后是函数体,函数体必须被花括号包围;
printf()函数:
显示格式化输出,用变量值来替换格式符;可以包含任意类型参数,但是需确保每个参数都要有一个对应的%格式符;
可以输入(Mac下)echo $?,来检查程序的退出状态,执行是否成功(返回0表示执行成功);
单行注释是从C99开始的;
C语言是一种编译型语言,需要源代码转化(或编译)为机器能够理解的机器代码,这样计算机才能执行;
编译器就被需要了,GNU编译器套件(GNU Complier Collection),也叫gcc;
编译运行:
可以使用一个技巧来编译并运行代码:
gcc 1_4-example.c -o 1_4-example && ./1_4example
&&表示如果前面成功,则做后面的;
./?的使用:
因为在类Unix操作系统中,运行程序必须指定程序所在目录,除非程序的目录已经列在了PATH环境变量中,./表示的就是在当前目录执行相应文件;
字符串/字符数组:
为什么不用String呢?
因为C语言不支持现成的字符串,但很多C语言的扩展库提供字符串;
C语言的抽象层次比较低,不提供字符串,而是以字符为元素的数组来进行代替;
char card_name[2];
这种,数组是一张有名有姓的事物清单,所以card_name只是一个变量名,用来引用你在命令提示符输入的那张字符列表;
(Code4)
/*
* 计算牌面点数的程序。
* 使用“拉斯维加斯公开许可证”。
* (c)1024学院21点扑克游戏小组。
*/
#include <stdio.h>
#include <stdlib.h>
int main() {
char card_name[3];
puts("输入牌名:");
scanf("%2s",card_name);
int val = 0;
if (card_name[0] == 'K') {
val = 10;
}else if (card_name[0] == 'Q') {
val = 10;
} else if (card_name[0] == 'J') {
val = 10;
}else if (card_name[0] == 'A') {
val = 11;
}else{
val = atoi(card_name);//返回值:每个函数返回 int 值,此值由将输入字符作为数字解析而生成。 如果该输入无法转换为该类型的值,则atoi的返回值为 0。
// 说明:当第一个字符不能识别为数字时,函数将停止读入输入字符串
}
printf("这张牌的点数:%i\n",val);
return 0;
}
2表示大小,card_name[0]和card_name[1]分别引用第一第二字符;
深入计算机存储器,看看C语言是如何处理文本的,来理解字符串的工作原理:
1)字符串聚焦:
字符串只是字符数组;s = "ssss"; -> s = ['s','s','s','s','\0'];
2)别在字符串尽头掉下去:
C语言较低级,他无法确切知道数组有多长;如果C语言想要在屏幕上显示字符串,他就需要知道什么时候会到达字符串的尾部;
为此C语言加入了哨兵字符;
哨兵字符:
哨兵字符是一个出现在字符串末尾的附加字符,它的值为\0;当计算机要读取字符串的内容时,他会注意扫描字符数组中的所有元素,直到碰到\0;
s = "ssss"时,计算机实际存储的还有一个\0(\0是ASCII字符,值为0,C程序员通常叫它空(NULL)字符);
这就是为什么代码中的card_name[3],而实际录入的只有两个字符,多余的一个是为了放入哨兵字符作为结尾;
字符索引的值是一个偏移量,表示当前要引用的这个字符到数组中第一个字符之间有多少个字符,所以从0开始;
之所以这样做,是因为计算机在存储器中以连续字节的形式保存字符,并利用索引计算出字符在存储器中的位置;如果c[0]位于存储器1000000号单元,那么c[96]就是在1000000+96号单元;计算数组长度不是C语言的强项,字符串其实就是个数组,C语言希望你自己记录数组长度;
单引号通常表示单个字符,双引号表示字符串;使用双引号定义的字符串叫字符串字面值(string literal);和字符数组相比字符串字面值是常量(创建之后不能再修改),但用起来更方便;
强行修改常量,会显示总线错误(bus error):C语言采用不同的方式在存储器中保存字符串字面值,总线错误意味着程序无法更新那一块存储器空间;
‘=’等号用来赋值;
‘==’双等号用来检查两个值是否相等;
C语言大部分命令都是语句;
被花括号包围的诸多命令,形成了块语句;
C语言中,布尔值是用数字表示的;对于C语言来讲,数字0代表假的值,任何不等于0的数字都被当成真处理;
(Code5)
/*
* 算牌的程序
*/
#include <stdio.h> //puts()
#include <stdlib.h> //atoi()
int main() {
char card_name[3];
puts("输入牌名:");
scanf("%2s",card_name);
int val = 0;
if (card_name[0] == 'K') {
val = 10;
} else if (card_name[0] == 'Q') {
val = 10;
} else if (card_name[0] == 'J') {
val = 10;
} else if (card_name[0] == 'A'){
val = 11;
}else {
val = atoi(card_name);
}
/**/
if (val >= 3 && val <= 6) {
puts("计数增加");
}else if (val == 10 ){
puts("计数减少");
}
return 0;
}
C99允许使用true和false关键字表示布尔值,但编译器还是会把它们当做1和0两个值来处理;
&和|,与 &&和||的区别在于:前者总是计算两个条件,而后者可以跳过第二个条件;前者还可以对二进制数进行按位的逻辑运算;
gcc:
GNU编译器套件(GNU Complier Collection)
1)支持C c++ OC Pascal Fortran PL/I Go...
2)可以生成任何处理器对应的机器码;
3)gcc前端将源代码转换为中间代码;
4)gcc后端,是一个能将中间代码转化为多种平台的机器代码的系统(gcc知道每种操作系统特定的可执行文件格式);
5)纠错和优化;
Switch分支:
相较于if语句,C语言可以用Switch进行逻辑测试,以改进一连串if语句;
Switch语句和if语句有点像,他可以测试一个变量的多种赋值:
(Code1_1)
/*
* Switch语句示例
*/
#include <stdio.h>
int main() {
int val;
int winnings = 0;
puts("1~5的值,请输入:");
scanf("%i",&val);
switch (val) {
case 1:
winnings += 10;
break;
case 2:
winnings += 20;
break;
case 3:
winnings += 30;
break;
case 4:
winnings += 40;
break;
case 5:
winnings += 50;
break;
default:
winnings = 0;
break;
}
printf("winnings = %i\n",winnings);
return 0;
}
当计算机遇到Switch语句,会检查给出的值,然后寻找匹配case;找到后,运行case之后的所有代码直到遇到break;否则,计算机会一直运行下去,直到有人告诉他退出;
漏掉break可能会出错;
(Code1_2)
/*
* 改写if为Switch
*/
#include <stdio.h>
#include <stdlib.h>
int main() {
char card_name[3];
puts("输入牌名:");
scanf("%2s",card_name);
int val = 0;
switch (card_name[0]) {
case 'K':
case 'Q':
case 'J':
val = 10;
break;
case 'A':
val = 11;
break;
default:
val = atoi(card_name);
break;
}
printf("这张牌的点数:%i\n",val);
return 0;
}
要点:
可以取代if;检查单一值;从第一个case开始执行;遇到break或到达switch语句末尾之前,会一直运行;别把break放错地方;
既能检查变量,也能检查值,仅仅检查是否相等;不能用他来检查字符串或任何形式的数组,switch只能检查值,这单请注意;
循环:
While循环:
循环是一种特殊的控制语句;控制语句决定一段代码是否运行,而循环语句决定了一段代码运行几次;
while(<某个条件>){
//循环体(只有一行时可以不加花括号)
}
do while:
while循环的另一种形式,他总是在循环体运行后才检查循环的条件;循环体至少被执行一次;
循环的结构都相同:
1)设置计数器;-准备循环变量;
2)条件测试;-在每一轮循环前检查条件;
3)进行循环,更新计数器;-在循环末尾更新计数器或实现类似功能;
(Code1_3)
/*
* while循环
*/
#include <stdio.h>
int main() {
int counter = 1;
while (counter < 11) {
printf("%i个枣\n",counter);
counter ++;
}
return 0;
}
for循环:
(Code1_4)
/*
* for循环
*/
#include <stdio.h>
int main() {
int counter;
for (counter = 1; counter < 11; counter ++) {
printf("%i个枣\n",counter);
}
return 0;
}
1)每个for循环的循环体都需要有点东西;
2)用break语句可以退/跳出循环;
使用break是需看清在哪里,并不是所有的地方都可以用break:break语句可以用来退出循环语句和switch语句;break不能从if语句中退出;
continue:
使用continue继续循环:如果想跳过循环体的其余部分,然后回到循环的开始,就可以用continue语句;
函数聚焦:
以main函数为例:
int main(){
}
它包括:
1)int的返回值类型;
2)main的函数名;
3)小括号中是参数列表;
4)被花括号包围的函数体,做事情的那部分;
5)事情做完后返回一个值;
(Code1_5)
/*
* 函数聚焦
*/
#include <stdio.h>
int larger(int a , int b) {
if (a > b) {
return a;
}
return b;
}
int main() {
int greatest = larger(100 , 1000);
printf( "%i最大!\n",greatest );
return 0;
}
函数参数是局部变量,从调用它的代码的那里得到参数的两个值;
如果想让编译器遵循C99标准,可以使用-std=c99选项;
bogon:0731 huaqiang$ gcc 1_5-func.c -o 1_5-func -std=c99
void函数聚焦:
函数没有信息返回时,可以将函数的返回值类型声明为void,此时的return语句没有也无妨;
C语言中,void关键字意味着无所谓;
void函数中的return语句可以用来提前退出函数;
链式赋值:
比如x=4;这个表达式的值是4,即赋给x的值;这个很有用;
如y = (x = 4);那么y也是4了;也可以写成这样:
y = x = 4; 这就是链式赋值;
重回循环:
(Code1_6)
/*
* 修改发牌计数的程序 使之在游戏期间保持计数;
* 如果输入X就终止程序;
* 输入错误信息则提示
*/
#include <stdio.h>
#include <stdlib.h>
int main() {
char card_name[3];
int count = 0;
do {
puts("输入牌名:");
scanf("%2s",card_name);
int val = 0;
switch (card_name[0]) {
case 'K':
case 'Q':
case 'J':
val = 10;
break;
case 'A':
val = 11;
break;
case 'X':
continue;//回到循环开始,再进行检查
default:
val = atoi(card_name);
if (val < 1 || val > 10) {
puts("我无法理解这个值\n");
continue;
}
break;
}
if (val > 2 && val < 7) {
count ++;
}else if (val == 10){
count --;
}
printf("当期计数:%i\n",count);
} while (card_name[0] != 'X');
return 0;
}
这会让之前的算牌程序,在程序运行期间一直保持计数;我们借助C的语句、循环、条件,让这个算牌程序工作了!
小结:
1)之所以C代码运行前需要编译,是因为这可以让代码运行起来更快;
(面向对象是一种抗软件复杂化的技术,这是一句题外话)
2)GNU Complier Collection(gcc)编译器套装,中的套装表示他可以用来编译很多语言,而C可能是应用gcc时使用最多的语言;
3)可以用break在任意时刻退出循环;
4)可以用continue随时跳到循环条件处;
5)return 会从函数中返回一个值;
6)void函数不需要return语句;
7)C中,所有的表达式都是值;(赋值表达式有一个值,因此可以链起来写);
工具箱:
1)简单语句就是命令;
2)块语句被{}包围;
3)#include将外部代码包含进来;
4)&& || 可以把多个条件进行组合;
5)gcc是最流行的C编译器;
6)源文件.c结尾;
7)count++表示计数加一;count--表示计数减一;
8)如果条件为真,if语句会运行代码;
9)switch语句高效的检查一个变量的多种取值;
10)每个程序都有一个main()函数;
11)运行前需要先编译;
12)可以在命令行中使用&&操作符在编译之后马上运行程序,前提是编译成功;
13)-o指定输出文件;
14)条件为真,while会一直执行;
15)do-while至少执行一次代码;
16)for循环更简洁;