本书针对c11标准编写。
创建编程环境
使用clion创建c可执行文件项目(项目路径不含中文,因为cmake不支持): ~code\clionProjects_C_Primer_Plus
,标准c23.
Clion,设置,build、Execution、deployment,toolchains,使用默认的MinGW
控制台输出中文乱码设置:文件编码修改为GBK。
第1章 初识C语言
C语言是最重要的编程语言之一,将来也是如此。
C语言诞生于1972年,到2024年已经存在50+年了!
C语言具有通常是汇编语言才具有的微调控制能力,可以根据具体情况微调程序以获得最大运行速度或最有效地使用内存。
很多语言的编译器和解释器是用C语言编写的。
程序员利用C可以访问硬件、操控内存中的位。
C语言使用指针,而涉及指针的编程错误往往难以察觉。要享受用语言自由编程的乐趣,就必须承担更多的责任,想拥有自由就必须时刻保持警惕。
CPU如何执行指令
CPU 的工作非常简单
- 从内存中获取并执行一条指令,然后再从内存中获取并执行下一条指令
- CPU执行指令的速度用时钟速度表示:CPU 每秒要处理来自不同程序的众多指令(如算术等低级计算),时钟速度则测量 CPU 每秒执行的周期数,以 GHz(千兆赫)为单位。在每个周期中,处理器内数十亿个晶体管会打开和关闭。
- GHz是频率的单位,代表“吉赫兹”,其中“吉”(giga)是国际单位制中的一个前缀,表示10的9次方,即1,000,000,000(十亿)。因此,1GHz等于10的9次方赫兹,也就是十亿赫兹。
- 一个GHz的 CPU一秒钟能重复这样的操作大约十亿次,时钟速度为 3.2 GHz 的 CPU 每秒执行 32 亿个周期,因此,CPU 能以惊人的速度从事枯燥的工作)。
- CPU 有自己的小工作区–由若干个寄存器组成,每个寄存器都可以储存一个数字。一个寄存器储存下一条指令的内存地址,CPU 使用该地址来获取和更新下一条指令。在获取指令后,CPU在另一个寄存器中储存该指令,并更新第1个寄存器储存下一条指令的地址。
- CPU能理解的指令有限(这些指令的集合叫作指令集),而且,这些指令相当具体,其中的许多指令都是用于请求计算机把一个数字从一个位置移动到另一个位置。例如,从内存移动到寄存器。
- 储存在计算机中的所有内容都是数字。
- 计算机以数字形式储存数字和字符(如,在文本文档中使用的字母)。每个字符都有一个数字码。
- 计算机载入寄存器的指令也以数字形式储存,指令集中的每条指令都有一个数字码。
- 计算机程序最终必须以数字指令码(即,机器语言)来表示。
- 简而言之,计算机的工作原理是:如果希望计算机做某些事,就必须为其提供特殊的指令列表(程序),确切地告诉计算机要做的事以及如何做。必须用计算机能直接明白的语言(机器语言)创建程序。这是-项繁琐、乏味、费力的任务。
- 计算机要完成诸如两数相加这样简单的事,就得分成类似以下几个步骤(机器语言编程)。
- 1.从内存位置2000上把一个数字拷贝到寄存器1。
- 2.从内存位置2004上把另一个数字拷贝到寄存器2。
- 3.把寄存器2中的内容与寄存器1中的内容相加,把结果储存在寄存器1中。
- 4.把寄存器1中的内容拷贝到内存位置2008。
- 必须用数字码来表示以上的每个步骤!
高级语言与编译器(对机器语言的抽象)
高级编程语言(如,C)以多种方式简化了编程工作。
-
首先,不必用数字码表示指令;
-
其次,使用的指令更贴近你如何想这个问题,而不是类似计算机那样繁琐的步骤。
使用高级编程语言,可以在更抽象的层面表达你的想法,不用考虑CPU在完成任务时具体需要哪些步骤。
例如,对于两数相加,可以这样写:
total=mine + yours;
对我们而言,光看这行代码就知道要计算机做什么;而看用机器语言写成的等价指令(多条以数字码形式表现的指令)则费劲得多。但是,对计算机而言却恰恰相反。在计算机看来,高级指令就是一堆无法理解的无用数据。
编译器在这里派上了用场。编译器是把高级语言程序翻译成计算机能理解的机器语言指令集的程序。程序员进行高级思维活动,而编译器则负责处理冗长乏味的细节工作。
编译器还有一个优势。一般而言,不同CPU制造商使用的指令系统和编码格式不同。例如,用Imntel Core i7(英特尔酷睿i7)CPU编写的机器语言程序对于 ARM Cortex-A57CPU 而言什么都不是。但是,可以找到与特定类型 CPU 匹配的编译器。因此,使用合适的编译器或编译器集,便可把一种高级语言程序转换成供各种不同类型 CPU使用的机器语言程序。一旦解决了一个编程问题,便可让编译器集翻译成不同CPU 使用的机器语言。简而言之,高级语言(如C、Java、Pascal)以更抽象的方式描述行为,不受限于特定 CPU 或指令集。
C标准
- C99: 支持国际化编程,提供多种方法处理国际字符集
- C11:2011年发布
编译与链接
不同的计算机使用不同的机器语言方案。C编译器负责把C代码翻译成特定的机器语言。典型的C实现通过编译和链接两个步骤来完成将源代码转换为可执行代码的过程。
-
可执行代码是用计算机的机器语言表示的代码。这种语言由数字码表示的指令组成。
-
编译器是把源代码转换成中间代码,链接器把中间代码和其他代码合并(包括预编译的库代码和启动代码),生成可执行文件。这种分而治之的方法方便对程序进行模块化,可以独立编译单独的模块,稍后再用链接器合并已编译的模块。通过这种方式,如果只更改某个模块,不必因此重新编译其他模块。
-
中间文件有多种形式。我们在这里描述的是最普遍的一种形式,即把源代码转换为机器语言代码,并把结果放在目标代码文件(或简称目标文件)中(这里假设源代码只有一个文件)。虽然目标文件中包含机器语言代码,但是并不能直接运行该文件。因为目标文件中储存的是编译器翻译的源代码,这还不是一个完整的程序。目标代码文件缺失启动代码(startupcode)。
-
启动代码充当着程序和操作系统之间的接口。例如,可以在 MS Windows或 Linux 系统下运行IBM PC兼容机。这两种情况所使用的硬件相同,所以目标代码相同,但是 Windows 和 Linux所需的启动代码不同,因为这些系统处理程序的方式不同。
-
目标代码缺少库函数。编译器通过运行链接器,将源代码中与C库(库中包含大量的标准函数供用户使用,如printf()和scanf())的代码合并。
-
链接器的作用是,把你编写的目标代码、系统的标准启动代码和库代码这3部分合并成一个文件,即可执行文件。对于库代码,链接器只会把程序中要用到的库函数代码提取出来
-
最终程序是一个用户可以运行的可执行文件,其中包含着计算机能理解的代码。
-
编译器还会检查C语言程序是否有效。如果C编译器发现错误,就不生成可执行文件并报错。理解特定编译器报告的错误或警告信息是程序员要掌握的另一项技能。
等待用于确认退出程序:getchar()
getchar()读取一次键的按下,所以程序在用户按下任意键之前会暂停。有时根据程序的需要,可能还需要一个击键等待。这种情况下,必须用两次getchar():
getchar();
getchar();
例如,程序在最后提示用户输入体重。用户键入体重后,按下Enter键以输入数据。程序将读取体重。第1个 getchar()读取 Enter键,第2个 getchar()会导致程序暂停,直至用户再次按下 Enter 键。
要经过一段时间的实践,才会熟悉编译器 & IDE的工作方式。必要时,还需阅读使用手册或网上教程。
第2章 C语言概述
示例程序
#include <stdio.h>
int main(void) {
int dogs;
printf("你有多少条狗?\n");
scanf("%d", &dogs);
printf("哦,你有%d\条狗!\n", dogs);
getchar();
return 0;
}
/*
你有多少条狗?
12
哦,你有12条狗!
*/
#include<stdio.h>
的作用相当于把 stdio.h文件中的所有内容都输入该行所在的位置。实际上,这是一种“拷贝-粘贴”的操作。include 文件提供了一种方便的途径共享许多程序共有的信息。#include
这行代码是一条C预处理器指令(preprocessor directive)。通常,C编译器在编译前会对源代码做一些准备工作,即预处理(preprocessing)。- void: main 函数无参数
变量声明
int dogs;
这行代码叫作声明(declaration)。
- 声明是C语言最重要的特性之一。
- 在该例中,声明完成了两件事。其一,在函数中有一个名为dogs的变量(variable)。其二,int表明 dogs 是一个整数(即,没有小数点或小数部分的数)。int是一种数据类型。编译器使用这些信息为dogs变量在内存中分配存储空间。
- 示例中的 dogs 是一个标识符(idenier),声明把特定标识符与计算机内存中的特定位置联系起来,同时也确定了储存在某位置的信息类型或数据类型。
- 在C语言中,所有变量都必须先声明才能使用。这意味着必须列出程序中用到的所有变量名及其类型以前的C语言
实参与形参
在C语言中,实际参数(简称实参)是传递给函数的特定值,形式参数(简称形参)是函数中用于储存值的变量。第5章中将详述相关内容。
函数声明(函数原型)
类似变量声明,函数声明也是先声明后定义。函数原型是一种声明形式,告知编译器正在使用某函数,因此函数原型也被称为函数声明(fumnctiondeclararion)。
C标准建议,要为程序中用到的所有函数提供函数原型。标准include文件(包含文件)为标准库函数提供可函数原型。例如,在C标准中,stdio.h文件包含了printf()的函数原型。
#include <stdio.h>
int doubleInt(int i);
int main(void) {
printf("你有多少条狗?\n");
int dogs;
scanf("%d", &dogs);
printf("哦,你有%d\条狗!\n", dogs);
printf("如果你的狗狗都是母狗,每条狗明年生一条小狗,你明年会有%d\n条狗。", doubleInt(dogs));
return 0;
}
int doubleInt(int i) {
return i * 2;
}
/*
你有多少条狗?
5
哦,你有5条狗!
如果你的狗狗都是母狗,每条狗明年生一条小狗,你明年会有10
条狗。
*/
第3章 数据和C
/*
* 输入你的体重,计算等量黄金价格
* 黄金单价按 568.38 RMB/g 计算
*/
#include <stdio.h>
int main(void) {
float weight;
printf("你的体重?(kg)\n");
scanf("%f", &weight);
float value = 568.38 * (weight*1000) / 10000; // 转为RMB万元
printf("你的体重为%.2f公斤,等同的黄金为%.2f万元\n", weight, value);//显示2位小数75
return 0;
}
/*
你的体重?(kg)
75
你的体重为75.000000公斤,等同的黄金为4262.850098万元
*/
- 在 printf()中使用%f来处理浮点值。%.2f中的.2用于精确控制输出,指定输出的浮点数只显示小数点后面两位。
- scanf()函数用于读取键盘的输入。%f说明 scanf()要读取用户从键盘输入的浮点数,&weight告诉 scanf()把输入的值赋给名为 weight 的变量。scanf()函数使用&符号表明找到 weight变量的地点。
数据类型的关键字
- int 关键字来表示基本的整数类型。后3个关键字(1ong、short 和 unsigned)和 C90 新增的 signed用于提供基本整数类型的变式,例如 unsigned short int和 long long int。
- char 关键字用于指定字母和其他字符(如,#、$、和)。另外,char 类型也可以表示较小的整数*。
- float,double 和 long double表示带小数点的数。
- _Bool类型表示布尔值(true或false)
- _Complex和 _Imaginary 分别表示复数和虚数。
通过这些关键字创建的类型,按计算机的储存方式可分为两大基本类型:整数类型和浮点数类型。
物理器件的特性限制决定了需要用有限的位来表示数字,对应的数字表示精度是有限的。
位,字节和字(存储视角)
位、字节和字是描述计算机数据单元或存储单元的术语。这里主要指存储单元。
- 最小的存储单元是位(bit),可以储存0或|(或者说,位用于设置“开”或“关”)。虽然|位储存的信息有限,但是计算机中位的数量十分庞大。位是计算机内存的基本构建块。
- 字节(byle)是常用的计算机存储单位。对于几乎所有的机器,1字节均为8位。这是字节的标准定义,至少在衡量存储单位时是这样。
- 字(word)是设计计算机时给定的自然存储单位。对于8位的微型计算机(如,最初的苹果机),1个字长只有8位。从那以后,个人计算机字长增至 16 位、32 位,直到目前的64 位。计算机的字长越大,其数据转移越快,允许的内存访问也更多。
整数和浮点数
对人类而言,整数和浮点数的区别是它们的书写方式不同。对计算机而言,它们的区别是储存方式不同。
7 在计算机中的表示, 等于不同位上的0 或1 对应的值相加, 就像十进制中不同位上对应的值相加一样。
浮点数和整数的储存方案不同。计算机把浮点数分成小数部分和指数部分来表示,面且分开储存这两部分。因此,虽然7.00和7在数值上相同,但是它们的储存方式不同。在十进制下,可以把 7.0写成 0.7E1。这里,0.7是小数部分,1是指数部分。
下图演示了一个储存浮点数的例子。当然,计算机在内部使用二进制和2的幂进行储存,而不是10的幂。
整数和浮点数的区别:
- 整数没有小数部分,浮点数有小数部分
- 浮点数可以表示的范围比整数大。
- 对于一些算术运算(如,两个很大的数相减),浮点数损失的精度更多。
- 因为在任何区间内(如,1.0到2.0之间)都存在无穷多个实数,所以计算机的浮点数不能表示区间内所有的值。浮点数通常只是实际值的近似值。例如,7.0可能被储存为浮点值6.99999。
- 过去,浮点运算比整数运算慢。不过,现在许多CPU都包含浮点处理器,缩小了速度上的差距。
int类型
- 一般而言,储存一个int要占用一个机器字长。因此,早期的16位IBM PC 兼容机使用6位来储存一个 int 值,其取值范围(即 int 值的取值范围)是-32768~32767。
- ISO C规定int的取值范用最小为-32768~32767。
- 一般而言,系统用一个特殊位的值表示有符号整数的正负号。
- 字面量表示:前缀0x或0X表示十六进制,前缀0表示八进制。
- 使用printf打印整数
- %d 打印十进制,%o 打印八进制,%x打印十六进制
- 要显示前缀0,0x,0X, 加上#号,使用 %#o,%#x,%#X
#include <stdio.h>
int main(void) {
int i = 100;
printf("用十进制,八进制,十六进制打印整数100 \n");
printf("%d %o %x \n", i, i, i);
printf("%d %#o %#x %#X \n", i, i, i, i);
return 0;
}
/*
用十进制,八进制,十六进制打印整数100
100 144 64
100 0144 0x64 0X64
*/
取值范围:
- C89/C90: 定义了基本数据类型,但没有明确规定每种类型的具体大小。
- C99/C11: 引入了
<stdint.h>
头文件,提供了固定宽度整数类型,如int8_t
,uint8_t
,int16_t
,uint16_t
,int32_t
,uint32_t
,int64_t
,uint64_t
,确保在不同平台上的一致性。 inttypes.h
是 C 语言标准库中的一个头文件,它定义了一些格式化输入/输出的宏和固定宽度整数类型。它主要用于确保在不同的平台上处理整数类型时的一致性和可移植性。- 由于不同平台和编译器可能会对这些类型的实际大小有所不同,因此建议使用
<limits.h>
和<stdint.h>
中的宏来获取整数类型的具体取值范围。例如,可以用CHAR_MIN
,CHAR_MAX
,SHRT_MIN
,SHRT_MAX
,INT_MIN
,INT_MAX
,LONG_MIN
,LONG_MAX
等来获取这些值。 - 一些程序员更关心速度而非空间。为此,C99和C11定义了一组可使计算达到最快的类型集合。这组类型集合被称为最快最小宽度类型(fastst minimum widih type)。例如,int_fast8_t被定义为系统中对8位有符号值而言运算最快的整数类型的别名。
#include <inttypes.h>
#include <stdint.h>
#include <stdio.h>
int main(void) {
int8_t i8;
int16_t i16;
int32_t i32;
int64_t i64;
uint8_t u8;
uint16_t u16;
uint32_t u32;
uint64_t u64;
printf("--------------有符号整数取值范围,使用 stdint.h ------------------\n");
printf("max int8_t %" PRId8 "\n", INT8_MAX);
printf("min int8_t %" PRId8 "\n", INT8_MIN);
printf("max int16_t %" PRId16 "\n", INT16_MAX);
printf("min int16_t %" PRId16 "\n", INT16_MIN);
printf("max int32_t %" PRId32 "\n", INT32_MAX); // 21亿
printf("min int32_t %" PRId32 "\n", INT32_MIN);
printf("max int64_t %" PRId64 "\n", INT64_MAX); // 非常大....
printf("min int64_t %" PRId64 "\n", INT64_MIN);
printf("\n\n--------------无符号整数最大值,使用 stdint.h ------------------\n");
printf("max uint8_t %" PRIu8 "\n", UINT8_MAX); //255
printf("max uint16_t %" PRIu16 "\n", UINT16_MAX); // 65535
printf("max uint32_t %" PRIu32 "\n", UINT32_MAX); //42亿
printf("max uint64_t %" PRIu64 "\n", UINT64_MAX); // 非常大
return 0;
}
/*
--------------有符号整数取值范围,使用 stdint.h ------------------
max int8_t 127
min int8_t -128
max int16_t 32767
min int16_t -32768
max int32_t 2147483647
min int32_t -2147483648
max int64_t 9223372036854775807
min int64_t -9223372036854775808
--------------无符号整数最大值,使用 stdint.h ------------------
max uint8_t 255
max uint16_t 65535
max uint32_t 4294967295
max uint64_t 18446744073709551615
*/
char类型
- 从技术角度看,char是整数类型,因为char实际上存储的是整数而不是字符。因此可以直接用整数赋值给char, 但必须在 char 的范围内
- 把字符常量赋值给char类型,需要用单引号: 双引号是字符串。
- 可以用转义字符赋值给 char类型
- 有些C编译器把 char 实现为有符号类型,这意味着char 可表示的范围是-128~127。而有些C编译器把 char 实现为无符号类型,那么char 可表示的范围是0~255。请查阅相应的编译器手册,确定正在使用的编译器如何实现 char 类型。或者,可以査阅 limits.h头文件。
- 根据 C90 标准,C语言允许在关键字 char 前面使用 signed 或 unsigned。这样,无论编译器默认char 是什么类型,signed char 表示有符号类型,而unsigned char 表示无符号类型。这在用 char类型处理小整数时很有用。
- 如果只用char处理字符,那么char前面无需使用任何修饰符。
整形常量的常用写法:
#include <stdio.h>
int main(void) {
char c1, c2;
c1 = 'A';
c2 = 'a';
printf("c1: %c %d\n", c1, c1);
printf("c2: %c %d\n", c2, c2);
return 0;
}
/*
c1: A 65
c2: a 97
*/
_Bool 类型
C99标准添加了 _Bool类型,用于表示布尔值,即逻辑值true 和false。因为C语言用值1表示true,值0表示false,所以 _Bool类型实际上也是一种整数类型。但原则上它仅占用1位存储空间,因为对0和1而言,1位的存储空间足够了。
浮点数:float,double,long double
float类型:
- C标准规定,float类型必须至少能表示6位有效数字,且取值范围至少是 $$10^(-37)~10^(+37)$$ 。
- 前一项规定指 float 类型必须至少精确表示小数点后的6位有效数字,如 33.333333。
- 后一项规定用于方便地表示诸如太阳质量(2.0e30千克)、一个质子的电荷量(1.6e-19库仑)或国家债务之类的数字。
- 通常,系统储存一个浮点数要占用 32 位。其中8位用于表示指数的值和符号,剩下24位用于表示非指数部分(也叫作尾数或有效数)及其符号。
double类型:
-
double(意为双精度)。double 类型和 float 类型的最小取值范围相同,但至少必须能表示 10位有效数字。
-
一般情况下,double占用64位而不是32位。一些系统将多出的 32位全部用来表示非指数部分,这不仅增加了有效数字的位数(即提高了精度),而且还减少了舍入误差。另一些系统把其中的一些位分配给指数部分,以容纳更大的指数,从而增加了可表示数的范围。无论哪种方法,double类型的值至少有13位有效数字,超过了标准的最低位数规定。
C语言的第3种浮点类型是1ong double,以满足比 double 类型更高的精度要求。不过,C只保证long double 类型至少与 double 类型的精度相同。
默认情况下,编译器假定浮点型常量是 double 类型的精度。例如,假设 some 是 float 类型的变量,编写下面的语句:
float some;
some=4.0*2.0;
通常,4.0和2.0被储存为 64位的 double类型,使用双精度进行乘法运算,然后将乘积截断成 float类型的宽度。这样做虽然计算精度更高,但是会减慢程序的运行速度。
在浮点数后面加上f或F后缀可覆盖默认设置,编译器会将浮点型常量看作 f1oat 类型,如 2.3f 和9.11E9F。使用小写字母l或L后缀使得数字成为long double类型,如3.14l和4.32L。注意,建议使用L后缀,因为字母l和数字1很容易混淆。没有后缀的浮点型常量是double类型。
打印浮点数:
- float,double: %f 或 e
- long double:%Lf或 %Le
溢出
当计算导致数字过大,超过当前类型能表达的范围时,就会发生上溢。这种行为在过去是未定义的,不过现在C语言规定,在这种情况下会给toobig 赋一个表示无穷大的特定值,而且printf()显示该值为inf或infinity(或者具有无穷含义的其他内容)。得出这些奇怪答案的原因是,计算机缺少足够的小数位来完成正确的运算:
#include <stdio.h>
int main(void) {
float f = 3.4e38;
f += 1.0;
printf("%f\n", f); // 339999995214436424907732413799364296704.000000
f += 1.0;
printf("%f\n", f); // 339999995214436424907732413799364296704.000000
f *= 100.0f;
printf("%f\n", f); //inf
return 0;
}
sizeof函数:以字节为单位给出类型的大小
print用 %zd 打印返回值 size_t 类型
#include <stdio.h>
#include <inttypes.h>
int main(void) {
printf("int8_t 类型的大小是 %zd 个字节\n",sizeof(int8_t));
printf("int16_t 类型的大小是 %zd 个字节\n",sizeof(int16_t));
printf("int32_t 类型的大小是 %zd 个字节\n",sizeof(int32_t));
printf("int64_t 类型的大小是 %zd 个字节\n",sizeof(int64_t));
printf("char 类型的大小是 %zd 个字节\n",sizeof(char));
printf("_Bool 类型的大小是 %zd 个字节\n",sizeof(_Bool));
printf("float 类型的大小是 %zd 个字节\n",sizeof(float));
printf("double 类型的大小是 %zd 个字节\n",sizeof(double));
printf("long double 类型的大小是 %zd 个字节\n",sizeof(long double));
return 0;
}
/*
int8_t 类型的大小是 1 个字节
int16_t 类型的大小是 2 个字节
int32_t 类型的大小是 4 个字节
int64_t 类型的大小是 8 个字节
char 类型的大小是 1 个字节
_Bool 类型的大小是 1 个字节
float 类型的大小是 4 个字节
double 类型的大小是 8 个字节
long double 类型的大小是 16 个字节
*/
精度损失
#include <stdio.h>
int main(void) {
int cost = 12.99; /*用double类型的值初始化 float 类型的变量 */
float pi = 3.1415926536;
printf("cost = %i\n", cost);
printf("pi = %f\n", pi);
return 0;
}
/*
cost = 12
pi = 3.141593
*/
- 第1个声明,cost的值是12。C编译器把浮点数转换成整数时,会直接丢弃(截断)小数部分,而不进行四舍五入。
- 第2个声明会损失一些精度,因为C只保证了float 类型前6位的精度。编译器对这样的初始化可能给出警告(clion并不会给出警告)。
许多程序员和公司内部都有系统化的命名约定,在变量名中体现其类型。例如,用i前缀表示int类型,us 前缀表示 unsigned short 类型。这样,一眼就能看出来i_smart是 int 类型的变量,us_ versmart是unsiqned short类型的变量。