计算机中运行的程序是为了解决现实世界中的问题,因此撰写程序的代码也必须拥有描述现实事物的能力,比如数字、文字,又比如人的年龄、商品的价格等。
为了实现这种能力,C语言拥有一套类型系统:
注:float(和double)进行算术运算时会比int类型慢。且float类型的变量存储的数值往往只是实际数值的一个近似值,如存进0.1,会得到0.099 999 999 999 999 87,这是舍入造成的误差。float类型的数值通常分为两部分存储:小数部分(或称尾数部分)和指数部分。如12.0就可以以1.5×2^3的形式存储,其中1.5是小数部分,而3是指数部分。有些编程语言将其称为real类型。
并且每种类型都在存储的空间中占有一定的大小,我们可以用sizeof
这个操作符来计算类型或变量所占空间的大小。
printf("%d\n", sizeof(char)); // 1
printf("%d\n", sizeof(short)); // 2
printf("%d\n", sizeof(int)); // 4
printf("%d\n", sizeof(long)); // 4 C语言标准:sizeof(long) >= sizeof(int)
printf("%d\n", sizeof(long long)); // 8
printf("%d\n", sizeof(float)); // 4
printf("%d\n", sizeof(double)); // 8
注意,char
类型指的是如a、b这样的字符;而不是Hello world这样的字符串。
C语言有没有字符串类型呢?答案是没有,但实际的使用中是存在的,因为可以用一连串的字符来组成一个字符串,可以将其理解为字符的数组。
字符一般用单引号''
括起来,而字符串则用双引号""
括起来。
注1:上面说的类型的大小是以字节为单位的。
注2:在计算机存储中有如下单位及其换算:
- bit - 比特位
- byte - 字节 == 8 bit
- kb - 千字节 == 1024 byte
- mb - 兆字节 == 1024 kb
- gb - 千兆字节 == 1024 mb
- tb - 太字节 == 1024 gb
- pb - 拍字节 == 1024tb
刚刚提到sizeof
可以计算变量的大小,变量是个什么样的概念呢?
在数学题中,我们经常会见到未知数x,在不同的题目x可以被直接赋值,也可以被计算出来再赋值。变量与之含义类似。
我们再思考一下,在现实世界中,有许多数据是不可变的,比如一个人的血型,比如圆周率。但与此同时,有些数据却是不断改变的,比如一个人从出生后不断增长的年龄,又比如一座工厂每年的产量。
在C语言中也有描述这样的变与不变的数据的概念,即变量和常量,它们的功能便是存储数据。
变量和常量同样有自己的数据类型,类型会影响变量的存储方式以及允许对变量进行的操作。
C语言中有两种变量,分别是全局变量和局部变量。
简单理解的话,全局变量是在{}
外部定义的,整个工程都能够使用它;而局部变量是在{}
内部定义的,只能在{}
内部使用——但实际上只能在单独的.c文件里使用的静态变量也算是局部变量的一种。
当全局变量与局部变量的名字冲突时,局部优先,但一般不建议把全局变量和局部变量的名字设成相同的。
使用变量前需要先进行声明(为编译器所做的描述),一般流程是指定变量类型,陈述变量名(也称标识符identifier,变量名由程序员自定义)。
//写一个代码求2个整数的和
int main()
{
int a = 0;
int b = 0;
int sum = 0;
scanf("%d %d", &a, &b); //scanf函数是与printf对应的输入函数,f同样是format的意思
//双引号中的格式串用于指定输入的格式
//&运算符是用于读取其后变量的地址的,后续会做更多解释
sum = a + b;
printf("sum = %d\n", sum); return 0;
}
注1:当报错
'scanf': This function or variable may be unsafe. Consider using scanf_s instead. To disable deprecation, use _CRT_SECURE_NO_WARNINGS. See online help for details.
时,在文件第一行写上#define _CRT_SECURE_NO_WARNINGS 1
便可。注2:scanf_s函数 是由VS编译器提供的,并非C语言标准规定的,因此学习时不建议使用。
{}
中的第1至第3行的意义是相似的,都是声明变量并赋予初始值,开头的int
说明它们是int类型的变量, a、b、sum 则是它们的名字,=
是赋值(assignment)的意思,=
后的内容则是赋给它们的值,是一些int类型的字面常量(后面会解释)。
比如第一行int a = 0;
的意思就是声明一个整型变量a,并赋初始值0。
实际上,,当几个变量具有相同的类型时,可以将其声明合并:
int a = 0, b = 0, sum = 0;
注1:在C语言在内的许多编程语言中,
=
的意思是赋值,==
的意思才是相等。注2:变量的声明往往在其被使用之前(C99标准不然,但本文主要陈述C89标准)。
注3:每条完整的语句(不止是声明语句)都要以分号结尾。
当然,声明变量的时候也可以不赋值:
int a, b, sum;
但这就失去了主动给变量初始化(initialize,常用缩写init)的机会,在未初始化的(uninitialized)变量被赋值之前,访问它可能会导致某些错误,甚至程序崩溃。
所以在声明的时候便进行赋值,是良好的习惯。
注1:初始化时
=
右边的值或公式被称为初始化式(initializer)。注2:赋值可以在声明之后,但不能在声明之前。
赋值运算的右侧可以是一个含有常量、变量和运算符的公式(C语言中称为表达式):
int a = 1, b = 1
int c = a + b;
同样,printf函数中的赋值部分也可以使用如下的形式:
printf("sum = %d\n", a + b);
这是C语言的一个通用原则:在任何需要数值的地方,都可以使用具有相同类型的表达式。
另外,当将包含小数点的常量赋予float
类型的变量时,最好在该常量后面加上一个字母f
(即float):
float num;
num = 2150.48f;
否则编译器可能会产生警告,因为某些编译器默认带小数的数字为double类型,直接使用可能丢失精度。
值得注意的是,将某个类型的值赋给另一个类型的变量,不一定会报错,但可能会出错。
作用域(scope),是一个程序设计概念,通常而言,一段程序代码中所用到的名字并不总是有效/可用的,而限定这个名字的可用性的代码范围就是这个名字的作用域。其中:
//在其他文件定义的全局变量,本文件使用前需要用关键字extern声明一下变量
extern int g_val;
int main()
{
printf("%d\n", g_val);
return 0;
}
生命周期:变量的生命周期指的是变量的创建到变量的销毁之间的一个时间段。
C语言中的常量与变量的定义形式有所差异,前者分为以下几种:
const int n = 10
初始化后,就不能对n重新赋值了;int arr2[n] = { 0 }
,声明数组时[]
里的内容必须是常量,但常变量不行。#define
定义的标识符常量(identifier constant):使用宏定义的方式定义的常量,如#define MAX 10000
,在大括号内外都可定义。enum Sex //enum是枚举关键字
{
//枚举常量是常量,其值默认是从0开始,但也可以赋值
MALE,
FEMALE = 3, //赋初值
SECRET
};
int main()
{
enum Sex s = FEMALE;
MALE = 3; //枚举常量不能在其他地方赋值,此行报错
printf("%d\n", MALE); //0
printf("%d\n", FEMALE); //3
printf("%d\n", SECRET); //4
printf("%d\n", s); //虽然被赋值为3,但显示是1;
}
类似"Hello World.\n"
这样由双引号引起来的一连串字符,称为字符串字面量/值(string literal),或简称字符串(string)。
注:字符串的结束标志是一个
'\0'
的转义字符。在计算字符串长度的时候'\0'
是结束标志,不算作字符串内容。
下面是关于字符串的打印和长度计算,可以先看看,完全看不懂就先跳过
//包含头文件,这些文件里有我们需要的函数或宏等
#include //库名为 standard input and output 的缩写,意为“标准的输入和输出”
#include
int main()
{
//字符串可以视为字符数组 - 数组指的是一组相同类型的元素
//字符串字面量在结尾的位置隐藏了一个 \0 的字符
char arr[] = "Hello";
char arr1[] = "abc";
char arr2[] = {'a', 'b', 'c'};
char arr3[] = {'a', 'b', 'c', '\0'};
//打印字符串
printf("%s\n", arr1); //abc
printf("%s\n", arr2); //abc烫烫烫烫蘟bc
//乱码的原因是arr2没有 \0 结束标志,如果加上就会变得正常
printf("%s\n", arr3); //abc
//求字符串长度
int len = strlen("abc"); //strlen是String Length的缩写
printf("%d\n", len); //3
printf("%d\n", strlen(arr1)); //3
printf("%d\n", strlen(arr2)); //随机值
//此行报错:C6054:可能没有为字符串"arr2"添加字符串零终止符
printf("%d\n", strlen(arr3)); //3 return 0;
}
转义字符(或称字符转义序列)即转变了某个字符原来的意义,是字符串包括该特殊字符而不会使编译器引发问题。
C语言中的转义字符如下:
转义字符 | 释义 |
---|---|
\? | 书写连续多个问号时使用,防止被解析成三字母词(非所有编译器都会出现)。 |
\' | 用于表示字符常量。 |
\" | 用于表示一个字符串内部的双引号。 |
\\ | 用于表示一个反斜杠,防止它被解释为一个转义序列符。 |
\a | 警告字符,蜂鸣。 |
\b | 退格符:类似Backspace键,也是一个字符,但显示时是将光标退回前一个字符,而不会删除光标位置的字符;如果后边有新的字符,将覆盖退回的那个字符,这与在文本编器中按Backspace的效果不一样。 |
\f | 进纸符。 |
\n | 换行。 |
\r | 回车。 |
\t | 水平制表符:按8个字符宽度跳到下一个制表位置,但本身算1个字符。 |
\v | 垂直制表符。 |
\ddd | 表示1~3个八进制的数字,如:\130 X。 |
\xdd | 表示2个十六进制的数字,如:\x30 0。 |
注:在ASCII码表(又叫ASCII字符集)中,每一个字符都有一个值——ASCII码值。ASCII(美国信息交换标准码)码表用7位代码表示128个字符。
int main()
{
//int a = 10; //C++注释风格,允许嵌套
/*
* int b = 0; //C语言注释风格,不允许嵌套
*/
return 0;
}
选择语句可能是最常见的流程控制语句,能够根据表达的值执行多条语句中的一条。
示例:
int input = 0; //输入的值
printf("现在是白天还是晚上(1/0)?>:");
scanf("%d", &input);
if (input == 1)
{
printf("白天\n");
}
else
{
printf("晚上\n");
}
当内容只有一句的时候,选择语句的大括号可加可不加:
if (input == 1)
printf("白天\n");
else
printf("晚上\n");
在不少实际问题中有许多具有规律性的重复操作,因此在程序中就需要重复执行某些语句,此时就可以用到循环语句(loop statement)。
循环语句的结构中包括循环变量、循环体和循环终止条件。
示例:
//while语句,for语句和do...while语句后面讲
int line = 0;
//以下开始循环,使line增加到50000
while (line < 50000)
{
printf("写代码:%d\n", line);
line++;
}
if (line == 50000)
printf("line == %d\n", line); //line == 50000
一般的,在一个变化过程中,假设有两个变量x、y,如果对于任意一个x都有唯一确定的一个y和它对应,那么就称x是自变量,y是x的函数。x的取值范围叫做这个函数的定义域,相应y的取值范围叫做函数的值域。
设A,B是非空的数集,如果按照某种确定的对应关系f,使对于集合A中的任意一个数x,在集合B中都有唯一确定的数 和它对应,那么就称映射 为从集合A到集合B的一个函数,记作 y=f(x),x∈Ay=f(x), x∈Ay=f(x),x∈A 或 f(A)=y∣f(x)=y,y∈Bf(A)={y|f(x)=y,y∈B}f(A)=y∣f(x)=y,y∈B 。
其中x叫作自变量,yyy 叫做x的函数,集合 AAA 叫做函数的定义域,与x对应的y叫做函数值,函数值的集合 f(x)∣x∈A{f(x)|x∈A}f(x)∣x∈A 叫做函数的值域, 叫做对应法则。
其中,定义域、值域和对应法则被称为函数三要素。一般书写为 y=f(x),x∈Dy = f(x),x∈Dy=f(x),x∈D 。若省略定义域,一般是指使函数有意义的集合。
函数过程中的这些语句用于完成某些有意义的工作——通常是处理文本,控制输入或计算数值。通过在程序代码中引入函数名称和所需的参数,可在该程序中执行(或称调用)该函数。
类似过程,不过函数一般都有一个返回值。它们都可在自己结构里面调用自己,称为递归。
大多数编程语言构建函数的方法里都含有函数关键字(或称保留字),但C语言没有。
编程中的函数与数学中的相似,但也有所不同。相同之处在于它们都能完成一定的任务,行使特定的功能。但编程中的函数更应该理解为一段预处理好的程序。
示例:写一个函数,用于输出用户输入的两个整数的和
int Add(int x, int y) //一般不建议把函数的名字写成全大写
{
//int z = 0;
//z = x + y;
int z = x + y;
return z;
}
int main()
{
int num1 = 0;
int num2 = 0;
scanf("%d%d", &num1, &num2);
//int sum = num1 + num2; //直接计算
//函数方式解决
int sum = Add(num1, num2);
printf("%d\n", sum);
return 0;
}
C语言中的定义:一组相同类型元素的集合。
示例:
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 }; //整型数组,完全初始化
char ch[5] = { 'a', 'b','c' }; //字符数组,不完全初始化,剩余的默认为0
//数组内的元素是用下标来访问的
int i = 0;
while (i < 10)
{
printf("%d ", arr[i]);
i++;
}
指定数组大小时一般使用常量,而不能用变量。除非使用C99标准中引用一个概念:变长数组 / 柔性数组(flexible array),变长数组支持数组创建的时候用变量指定大小,但这个数组不能初始化。
C语言的输入和输出实际上有多种方式,这里先介绍scanf
和printf
这两个使用最频繁的函数。
pinrtf
函数被设计用来显示格式串(format string)的内容,并在该串中的指定位置插入可能的值。其使用格式为:
printf(格式串, 表达式1, 表达式2, ...);
显示的值可以是常量、变量或者更加复杂的表达式。调用printf函数一次可以打印的值的个数没有限制。
格式串包含普通字符和转换说明(conversion sprcification),其中转换说明以字符%
开头,用来表示打印过程中待填充的值的占位符,%
后的信息指定了把数值从内部形式(二进制)转换成打印形式(字符)的方法,即printf
在打印数据的时候,可以指定打印的格式。
printf
的f实际上是格式化(format)的意思,即以用户指定的格式显示到窗口上。占位符
%d
指明变量的类型以及显示时位置,d是decimal(十进制格式)的意思。双引号中的内容为格式串(format string),用于指定输出的格式,主要有以下几种:
- %d - int
- %f - float
- %lf - double
- %c - char
- %s - string
- %p - 地址
- %x - 十六进制打印
当表达式多于转换说明时,多余的表达式结果不会被展示;而表达式少于转换说明时,多出来的转换说明会显示一个无意义的值,值的格式与转换说明指定的格式相同。一般尽量保证两者相等。
另外,表达式的结果会服从转换说明指定的数据类型,如果不匹配,则会强制转换——但往往也失去了其意义。
与用特定格式显示输出的printf
函数相对,scanf
根据特定的格式读取输入,它同样也包含普通字符和转换说明两个部分。
但在许多情况下,scanf
函数只包含转换说明:
int x = 0, y = 0;
scanf("%d%d", &x, &y);
scanf
函数将读入用户输入的信息如:1 20
,然后将其分别赋值给x、y。
与printf
函数一样,scanf
函数也需要关注转换说明与输入变量的数量是否匹配,例子中的%d
与printf
中的是一样的,说明它会读取整型类型的输入值。%d
有两个,说明会读取两个值。而&
操作符则是用来取程序员想要更新的变量地址的。
可以通过设置转换说明里的内容来限定scanf
读取的值,比如:
scanf("%1d%1d", &x, &y);
那么它只会读取两个一位数,哪怕用户连续地输入11
,也会被读取为1 1
。
(1)格式串中的普通字符同样具有意义。
scanf
函数从输入中重复读空白字符直到遇到一个非空白字符(把该字符“放回原处”)为止。格式串中空白字符的数量无关紧要,格式串中的一个空白字符可以与输入中任意数量的空白字符相匹配。scanf
函数将把它与下一个输入字符进行比较。如果两个字符相匹配,那么scanf
函数会放弃输入字符而继续处理格式串。如果两个字符不匹配,则会把不匹配的字符放回输入中,然后异常退出,而不进一步处理格式串或从输入中读取字符。注:格式串中的普通字符都是可选的。
(2)%i
也可以用于读写整数,它与%d
之间是否有区别?
答:在printf
格式串中使用时没有区别,但在scanf
格式串中%d
只能与十进制数相匹配,而%i
则可以匹配八进制、十进制或者十六进制数。如果输入的前缀有0(如012),那么%i
会将其当作八进制来处理;如果前缀有0x或0X,则会被作为十六进制数来处理。
(3)如果需要在printf中打印出%,则连续写两个%,此时printf
将显示出一个字符%
。
(4)scanf
函数如何把字符“放回原处”并在以后再次读取?
用户键入时,程序并没有立刻读取,而是将用户的输入放在一个隐藏的缓冲区中,由scanf
函数来读取。scanf
函数把字符放回缓冲区中供后续读取是非常容易的。