C Primer Plus 第6版中文版 1~3章

本书假定读者为非专业的程序员。

配套源码: https://box.lenovo.com/l/60jQJf

第 1章 初识C语言

想拥有自由就必须时刻保持警惕.

OOP是一门哲学.

1.4 计算机能做什么

在学习如何用 C 语言编程之前,最好先了解一下计算机的工作原理。这些知识有助于你理解用 C 语言编写程序和 运行 C 程序时所发生的事情之间有什么联系。

CPU的工作非常简单,至少从以下简短的描述中看是这样。它从内存中获取并执行一条指令,然后再从内存中获取并执行下一条指令,诸如此类.

  • CPU有自己的小工作区,由若干个寄存器组成,每个寄存器都可以存储一个数字。
  • 一个寄存器存储下一条指令的内存地址,CPU使用该地址来获取和更新下一条指令。
  • 在获取指令后,CPU在另一个寄存器中存储该指令,并更新第1个寄存器存储下一条指令的地址。
  • CPU能理解的指令有限〈这些指令的集合叫作指令集)。而且,这些指令相当具体,其中的许多指令都是用于请求计算机把一个数字从一个位置移动到另一个位置。例如,从内存移动到寄存器。

下面介绍两个有趣的知识。

其一, 存储在计算机中的所有内容都是数字。 计算机以数字形式存储数字和字符( 如, 在文本文档中使用的字母)。 每个字符都有一个数字码。 计算机载入寄存器的指令也以数字形式存储, 指令集中的每条指令都有一个数字码。

其二, 计算机程序最终必须以数字指令码( 即, 机器语言 )来表示。

简而言之, 计算机的工作原理是: 如果希望计算机做某些事, 就必须为其提供特殊的指令列表( 程序), 确切地告诉计算机要做的事以及如何做。 你必须用计算机能直接明白的语言( 机器语言) 创建程序。 这是一项繁琐、 乏味、 费力的任务。 计算机要完成诸如两数相加这样简单的事, 就得分成类似以下几个步骤。

  • 1. 从内存位置 2000 上把一个数字拷贝到寄存器 1。
  • 2. 从内存位置 2004 上把另一个数字拷贝到寄存器 2。
  • 3. 把寄存器 2 中的内容与寄存器 1 中的内容相加, 把结果存储在寄存器 1 中。
  • 4. 把寄存器 1 中的内容拷贝到内存位置 2008。

而你要做的是, 必须用数字码来表示以上的每个步骤!

高级编程语言( 如, C) 以多种方式简化了编程工作。 首先, 不必用数字码表示指令; 其次, 使用的指令更贴近你如何想这个问题, 而不是类似计算机那样繁琐的步骤。 使用高级编程语言, 可以在更抽象的层面表达你的想法, 不用考虑 CPU 在完成任务时具体需要哪些步骤。

要计算机做某个操作时,最后的操作步骤一个不少. 高级语言解决的是抽象问题, 而不是硬件指令(包括CPU,GPU等)层面的具体操作.

编译器是把高级语言程序翻译成计算机能理解的机器语言指令集的程序。 程序员进行高级思维活动, 而编译器则负责处理冗长乏味的细节工作。

美国国家标准协会( ANSI) 于 1983 年组建了一个委员会( X3J11), 开发了一套新标准, 并于 1989 年正式公布。 该标准( ANSI C) 定义了 C 语言和 C 标准库。国际标准化组织于 1990 年采用了这套 C 标准( ISO C)。 ISO C 和 ANSI C 是完全相同的标准。 ANSI/ ISO 标准的最终版本通常叫作 C89 (因为 ANSI 于 1989 年批准该标准) 或 C90 (因为 ISO 于 1990 年批准该标准)。 另外, 由于 ANSI 先公布 C 标准, 因此业界人士通常使用 ANSI C。

在该委员会制定的指导原则中, 最有趣的可能是: 保持 C 的精神。 委员会在表述这一精神时列出了以下几点:

  • 信任程序员;
  • 不要妨碍程序员做需要做的事;
  • 保持语言精练简单;
  • 只提供一种方法执行一项操作;
  • 让程序运行更快, 即使不能保证其可移植性。

在最后一点上, 标准委员会的用意是: 作为实现, 应该针对目标计算机来定义最合适的某特定操作, 而不是强加一个抽象、 统一的定义。 在学习 C 语言过程中, 许多方面都反映了这一哲学思想。

1994 年, ANSI/ ISO 联合委员会( C9X 委员会) 开始修订 C 标准, 最终发布了 C99 标准。 该委员会遵循了最初 C90 标准的原则, 包括保持语言的精练简单。 委员会的用意不是在 C 语言中添加新特性, 而是为了达到新的目标。

  • 第 1 个目标是, 支持国际化编程。 例如, 提供多种方法处理国际字符集。
  • 第 2 个目标是,“ 调整现有实践致力于解决明显的缺陷”。 因此, 在遇到需要将 C 移至 64 位处理器时, 委员会根据现实生活中处理问题的经验来添加标准。
  • 第 3 个目标是, 为适应科学和工程项目中的关键数值计算, 提高 C 的适应性, 让 C 比 FORTRAN 更有竞争力。

并非所有的编译器都完全实现 C99 的所有改动。 因此, 你可能发现 C99 的一些改动在自己的系统中不可用, 或者只有改变编译器的设置才可用。

标准委员会在 2007 年承诺 C 标准的下一个版本是 C1X, 2011 年终于发布了 C11 标准。 此次, 委员会提出了一些新的指导原则。 出于对当前编程安全的担忧, 不那么强调“ 信任程序员” 目标了。 而且, 供应商并未像对 C90 那样很好地接受和支持 C99。 这使得 C99 的一些特性成为 C11 的可选项。另外需要强调的是, 修订标准的原因不是因为原标准不能用, 而是需要跟进新的技术。 例如, 新标准添加了可选项支持当前使用多处理器的计算机。 对于 C11 标准, 我们浅尝辄止, 深入分析这部分内容已超出本书讨论的范围。

应该用一般术语来描述问题, 而不是用具体的代码。数学是比较好的问题描述工具.

C 编译器负责把 C 代码翻译成特定的机器语言。 此外, C 编译器还将源代码与 C 库( 库中包含大量的标准函数供用户使用, 如 printf() 和 scanf() )的代码合并成最终的程序( 更精确地说, 应该是由一个被称为链接器的程序来链接库函数, 但是在大多数系统中, 编译器运行链接器)。 其结果是, 生成一个用户可以运行的可执行文件, 其中包含着计算机能理解的代码。

启动代码充当着程序和操作系统之间的接口。链接器的作用是, 把你编写的目标代码、 系统的标准启动代码和库代码这 3 部分合并成一个文件, 即可执行文件。 对于库代码, 链接器只会把程序中要用到的库函数代码提取出来.

简而言之, 目标文件和可执行文件都由机器语言指令组成的。 然而, 目标文件中只包含编译器为你编写的代码翻译的机器语言代码, 可执行文件中还包含你编写的程序中使用的库函数和启动代码的机器代码。

在有些系统中, 必须分别运行编译程序和链接程序, 而在另一些系统中, 编译器会自动启动链接器, 用户只需给出编译命令即可。

示例程序:

# include <stdio.h> //标准库

int main(void)
{
    int dogs;
    printf("How many dogs do you have?\n");
    scanf("%d", & dogs); //&dogs 表示变量 dogs的地址,&是取地址符
    printf(" So you have %d dog( s)!\ n", dogs);
    return 0;
}

scanf 在 Visual Studio 中需要设置一下才能使用.

第 2章  C语言概述

组成 C 程序的几个部分:

Kenneth·A·Reek; Stephen Prata; David Vandevoorde; Nicolai M·Josuttis; 史蒂夫·萨米特; Anthony Williams; 安德鲁·凯尼格; 彼得·范德林登; 史蒂芬•普拉达. 人邮C语言系列丛书:你必须知道的C/C++(套装全8册)【重量级C大百科全书!C图书领域的独孤求败!中文版累计销售百万册!豆瓣评分9.4!】 (Kindle位置1186

为何不内置输入和输出读者一定很好奇, 为何不把输入和输出这些基本功能内置在语言中。 原因之一是, 并非所有的程序都会用到 I/ O( 输入/ 输出) 包。 轻装上阵表现了 C 语言的哲学。 正是这种经济使用资源的原则, 使得 C 语言成为流行的嵌入式编程语言( 例如, 编写控制汽车自动燃油系统或蓝光播放机芯片的代码)。

操作系统和 C 库经常使用以一个或两个下划线字符开始的标识符( 如,_kcab ), 因此最好避免在自己的程序中使用这种名称。

C99 之前的标准要求把声明都置于块的顶部, 这样规定的好处是: 把声明放在一起更容易理解程序的用途。 C99 允许在需要时时才声明变量, 这样做的好处是: 在给变量赋值之前声明变量, 就不会忘记给变量赋值。 但是实际上, 许多编译器都还不支持 C99。

在 C 语言中, 实际参数 (简称实参 )是传递给函数的特定值, 形式参数 (简称形参 )是函数中用于存储值的变量。

示例函数:

// first.c

/*
# include < stdio.h > 的作用相当于把 stdio.h 文件中的所有内容都输入该行所在的位置。
实际上, 这是一种“ 拷贝 - 粘贴” 的操作。 include 文件提供了一种方便的途径共享许多程序共有的信息。
#include 这行代码是一条 C 预处理器指令 ( preprocessor directive )。
通常, C 编译器在编译前会对源代码做一些准备工作, 即预处理 ( preprocessing )。
*/
#include <stdio.h> 

/* 
圆括号表明 main() 是一个函数名。
int 表明 main() 函数返回一个整数
void 表明 main() 不带任何参数。
int 和 void 是标准 ANSI C 定义 main() 的一部分
*/
int main(void)                /* a simple program             */
{
    int num;                  /* define a variable called num */
    num = 1;                  /* assign a value to num        */

    printf("I am a simple "); /* use the printf() function    */
    printf("computer.\n");

    /* 
    % d 相当于是一个占位符, 其作用是指明输出 num 值的位置
    % 提醒程序, 要在该处打印一个变量
    d 表明把变量作为十进制整数打印。
    printf() 函数名中的 f 提醒用户, 这是一种格式化打印函数(format)
    */
    printf("My favorite number is %d because it is first.\n", num);

    // 如果遗漏 main() 函数中的 return 语句, 程序在运行至最外面的右花括号(} )时会返回 0
    //强烈建议读者养成在 main() 函数中保留 return 语句的好习惯。 在这种情况下, 可将其看作是统一代码风格。
    //对于某些操作系统( 包括 Linux 和 UNIX ), return 语句有实际的用途。
    return 0;
}
//I am a simple computer.
//My favorite number is 1 because it is first.

多个函数

  • 函数原型是一种声明形式, 告知编译器正在使用某函数, 因此函数原型也被称为函数声明 ( function declaration )。 函数原型还指明了函数的属性。 void butler( void);函数原型中的第 1 个 void 表明, butler() 函数没有返回值( 通常, 被调函数会向主调函数返回一个值, 但是 butler() 函数没有)。 第 2 个 void (butler( void) 中的 void )的意思是 butler() 函数不带参数。 因此, 当编译器运行至此, 会检查 butler() 是否使用得当。 注意, void 在这里的意思是“ 空的”, 而不是“ 无效”。
  • C 标准建议, 要为程序中用到的所有函数提供函数原型。 标准 include 文件( 包含文件) 为标准库函数提供了函数原型。 例如, 在 C 标准中, stdio.h 文件包含了 printf() 的函数原型。
  • 无论 main() 在程序文件中处于什么位置, 所有的 C 程序都从 main() 开始执行。 但是, C 的惯例是把 main() 放在开头, 因为它提供了程序的基本框架。
#include <stdio.h> 

double add(double a, double b); // 函数原型也被称为函数声明,需要在后续有实际的定义
void say_Hi(void); // (void)表示没有参数

int main(void)                /* a simple program             */
{
    double a = 2, b = 3;
    say_Hi();
    add(a, b);
    return 0;
}


double add(double a, double b) {
    printf("%f + %f =%f", a, b, a + b);
    return a + b;
}

void say_Hi() {
    // 没有参数, 但不需要在()内注明 void
    printf("Hi!\n");
}

//Hi!
//2.000000 + 3.000000 = 5.000000

监控程序状态的方法

程序的错误通常叫作 bug, 找出并修正错误的过程叫作调试 (debug )。

  • printf函数:在程序中的关键点插入额外的 printf() 语句, 以监视指定变量值的变化。 通过查看值的变化可以了解程序的执行情况。 对程序的执行满意后, 便可删除或注释掉额外的 printf() 语句, 然后重新编译。
  • 使用调试器。 调试器( debugger )是一种程序, 让你一步一步运行另一个程序, 并检查该程序变量的值。 调试器有不同的使用难度和复杂度。 较高级的调试器会显示正在执行的源代码行号。 这在检查有多条执行路径的程序时很方便, 因为很容易知道正在执行哪条路径。

程序员要具备抽象和逻辑的思维, 并谨慎地处理细节问题( 编译器会强迫你注意细节问题)。

C 程序由一个或多个 C 函数组成。 每个 C 程序必须包含一个 main() 函数, 这是 C 程序要调用的第 1 个函数。

函数调用本身是一个表达式, 圆括号是运算符, 圆括号左边的函数名是运算对象。

第3章 数据和C

/* platinum.c  -- your weight in platinum */
#include <stdio.h>
int main(void)
{
    float weight;    /* user weight             */
    float value;     /* platinum equivalent     */

    printf("Are you worth your weight in platinum?\n");
    printf("Let's check it out.\n");
    printf("Please enter your weight in pounds: ");

    /* get input from the user                     */
    // scanf() 函数用于读取键盘的输入。
    // % f 说明 scanf() 要读取用户从键盘输入的浮点数,
    // & weight 告诉 scanf() 把输入的值赋给名为 weight 的变量。
    // scanf() 函数使用& 符号表明找到 weight 变量的地点。
    scanf("%f", &weight);
    /* assume platinum is $1700 per ounce          */
    /* 14.5833 converts pounds avd. to ounces troy */
    value = 1700.0 * weight * 14.5833;
    printf("Your weight in platinum is worth $%.2f.\n", value);
    printf("You are easily worth that! If platinum prices drop,\n");
    printf("eat more to maintain your value.\n");

    return 0;
}

// Are you worth your weight in platinum?
// Let's check it out.
// Please enter your weight in KG: 60
// Your weight in platinum is worth $1487496.62.
// You are easily worth that! If platinum prices drop,
// Eat more to maintain your value.

C 语言的数据类型关键字

C 语言的基本类型关键字。 K& R C 给出了 7 个与类型相关的关键字。 C90 标准添加了 2 个关键字, C99 标准又添加了 3 个关键字.

通过这些关键字创建的类型, 按计算机的存储方式可分为两大基本类型: 整数类型和浮点数类型 :

  • 在 C 语言中, 用 int 关键字来表示基本的整数类型。 后 3 个关键字( long 、short 和 unsigned )和 C90 新增的 signed 用于提供基本整数类型的变式, 例如 unsigned short int 和 long long int 。
  • char 关键字用于指定字母和其他字符( 如,# 、$ 、% 和* )。 另外, char 类型也可以表示较小的整数。
  • float 、double 和 long double 表示带小数点的数。
  • _Bool 类型表示布尔值( true 或 false )
  • _Complex 和_Imaginary 分别表示复数和虚数。
最初 K& R 给出的关键字 C90 标准添加的关键字 C99 标准添加的关键字
int signed _Bool
long void _Complex
short _Imaginary
unsigned
char
float
double

位、 字节和字

位、 字节和字是描述计算机数据单元或存储单元的术语。 这里主要指存储单元。

  • 最小的存储单元是位( bit ), 可以存储 0 或 1( 或者说, 位用于设置“ 开” 或“ 关”)。 虽然 1 位存储的信息有限, 但是计算机中位的数量十分庞大。 位是计算机内存的基本构建块。
  • 字节( byte )是常用的计算机存储单位。 对于几乎所有的机器, 1 字节均为 8 位。 这是字节的标准定义, 至少在衡量存储单位时是这样( 但是, C 语言对此有不同的定义, 请参阅本章 3.4.3 节)。 既然 1 位可以表示 0 或 1, 那么 8 位字节就有 256( 2 的 8 次方) 种可能的 0、 1 的组合。 通过二进制编码( 仅用 0 和 1 便可表示数字), 便可表示 0 ~ 255 的整数或一组字符
  • 字( word )是设计计算机时给定的自然存储单位。 对于 8 位的微型计算机( 如, 最初的苹果机), 1 个字长只有 8 位。 从那以后, 个人计算机字长增至 16 位、 32 位, 直到目前的 64 位。 计算机的字长越大, 其数据转移越快, 允许的内存访问也更多。

整数,浮点数

  • 和数学的概念一样, 在 C 语言中, 整数是没有小数部分的数。计算机以二进制数字存储整数, 例如, 整数 7 以二进制写是 111。 因此, 要在 8 位字节中存储该数字, 需要把前 5 位都设置成 0, 后 3 位设置成 1.
  • 浮点数与数学中实数的概念差不多。在一个值后面加上一个小数点, 该值就成为一个浮点值。 所以, 7 是整数, 7.00 是浮点数。
    • 这里关键要理解浮点数和整数的存储方案不同。 计算机把浮点数分成小数部分和指数部分来表示, 而且分开存储这两部分。 因此, 虽然 7.00 和 7 在数值上相同, 但是它们的存储方式不同。计算机在内部使用二进制和 2 的幂进行存储, 而不是 10 的幂。
    • 因为在任何区间内( 如, 1.0 到 2.0 之间) 都存在无穷多个实数, 所以计算机的浮点数不能表示区间内所有的值。 浮点数通常只是实际值的近似值。 例如, 7.0 可能被存储为浮点值 6.99999。
    • 过去, 浮点运算比整数运算慢。 不过, 现在许多 CPU 都包含浮点处理器, 缩小了速度上的差距。

int

  • int 类型是有符号整型, 即 int 类型的值必须是整数, 可以是正整数、 负整数或零。 其取值范围依计算机系统而异。 一般而言, 存储一个 int 要占用一个机器字长。
  • 一般而言, 系统用一个特殊位的值表示有符号整数的正负号。
  • ISO C 规定 int 的取值范围最小为-32768 ~ 32767
  • 在 C 语言中, 用特定的前缀表示使用哪种进制。 0x 或 0X 前缀表示十六进制值(Hexadecimal), 所以十进制数 16 表示成十六进制是 0x10 或 0X10。 与此类似, 0 前缀表示八进制(Octal)。 例如, 十进制数 16 表示成八进制是 020。
  • printf 打印整数用 %d,Decimal,十进制的意思.
  • printf 打印八进制,十六进制时如果要显示前缀, 需要加 #
  • C 语言提供 3 个附属关键字修饰基本整数类型: short 、long 和 unsigned , 以支持更多整数类型
#include <stdio.h>
int main(void)
{
    // 最好不要把初始化的变量和未初始化的变量放在同一条声明中。
    // 只初始化了 cats ,并未初始化 dogs 。
    //这种写法很容易让人误认为 dogs 也被初始化为 94 
    int dogs, cats = 94; /* 有效, 但是这种格式很糟糕 */

    int a = 16;

    // 不显示前缀
    printf("Decimal a=%d,Hexadecimal a=%x,Octal a=%o \n", a,a,a);
    // 显示前缀
    printf("Decimal a=%d,Hexadecimal a=%#x,Hexadecimal a=%#X,Octal a=%#o", a, a, a);
    return 0;
}

//Decimal a = 16, Hexadecimal a = 10, Octal a = 20
//Decimal a = 16, Hexadecimal a = 0x10, Hexadecimal a = 0X10, Octal a = 033210050

printf() 不寻常的设计: 大部分函数都需要指定数目的参数, 编译器会检查参数的数目是否正确。 但是, printf() 函数的参数数目不定, 可以有 1 个、 2 个、 3 个或更多, 编译器也爱莫能助。 记住, 使用 printf() 函数时, 要确保转换说明的数量与待打印值的数量相等。

#include <stdio.h>
int main(void)
{
    int a, b, c;
    a = 2;
    b = 3;
    c = 4;

    // 由于没有给后两个% d 提供任何值, 所以打印出的值是内存中的任意值
    printf("a=%d,b=%d,c=%", a);

    return 0;
}

// a=2,b=14946344,c=

更多的整数类型:

  • C 标准对基本数据类型只规定了允许的最小大小。
  • C 语言只规定了 short 占用的存储空间不能多于 int ,long 占用的存储空间不能少于 int 。这样规定是为了适应不同的机器。
  • short int 类型( 或者简写为 short )占用的存储空间可能比 int 类型少, 常用于较小数值的场合以节省空间。 与 int 类似, short 是有符号类型。
  • long int 或 long 占用的存储空间可能比 int 多, 适用于较大数值的场合。 与 int 类似, long 是有符号类型。
  • long long int 或 long long (C99 标准加入) 占用的存储空间可能比 long 多, 适用于更大数值的场合。 该类型至少占 64 位。 与 int 类似, long long 是有符号类型。
  • unsigned int 或 unsigned 只用于非负值的场合。 这种类型与有符号类型表示的范围不同。 例如, 16 位 unsigned int 允许的取值范围是 0 ~ 65535 ,而不是-32768 ~ 32767 。用于表示正负号的位现在用于表示另一个二进制位, 所以 无符号整型可以表示更大的数。 在 C90 标准中, 添加了 unsigned long int 或 unsigned long 和 unsigned short int 或 unsigned short 类型。 C99 标准又添加了 unsigned long long int 或 unsigned long long 。
  • 在任何有符号类型前面添加关键字 signed ,可强调使用有符号类型的意图。 例如, short 、short int 、signed short 、signed short int 都表示同一种类型。signed 只是为了可读性更好.
  • printf() 函数使用% u 说明显示 unsigned int 类型的值

整数溢出

在超过最大值时, unsigned int 类型的变量 j 从 0 开始; 而 int 类型的变量 i 则从 − 2147483648 开始。 注意, 当 i 超出( 溢出) 其相应类型所能表示的最大值时, 系统并未通知用户。 因此, 在编程时必须自己注意这类问题。

需要注意的是printf函数打印不同类型整数需要不同类型的格式化参数,具体参考 https://docs.microsoft.com/zh-cn/cpp/c-runtime-library/format-specification-syntax-printf-and-wprintf-functions?view=msvc-160

  • 打印 unsigned int 类型的值, 使用%u 转换说明;
  • 打印 long 类型的值, 使用%ld 转换说明。
  • 如果系统中 int 和 long 的大小相同, 使使用% d 就行。 但是, 这样的程序被移植到其他系统( int 和 long 类型的大小不同) 中会无法正常工作。
  • 在 x 和 o 前面可以使用 l 前缀,% lx 表示以十六进制格式打印 long 类型整数,% lo 表示以八进制格式打印 long 类型整数。 注意, 虽然 C 允许使用大写或小写的常量后缀, 但是在 转换说明中只能用小写。
  • 对于 short 类型, 可以使用 h 前缀。% hd 表示以十进制显示 short 类型的整数,% ho 表示以八进制显示 short 类型的整数。 h 和 l 前缀都可以和 u 一起使用, 用于表示无符号类型。 例如,% lu 表示打印 unsigned long 类型的值。
  • 对于支持 long long 类型的系统,%lld 和%llu 分别表示有符号和无符号类型。
  • printf的 d,ld,u,x,o 格式化参数可以看作将需要打印输出的变量转为该对应类型再输出
  • printf() 函数用% c 指明待打印的字符。

printf() 函数中的转换说明决定了数据的显示方式, 而不是数据的存储方式

# include <stdio.h>
# include <math.h>

int main(void)
{
    // 64位机器, 可能是 int -2^63 ~  2^63-1
    int int_min = 0, int_max = 0;
    unsigned int uint_min = 0, uint_max = 0;

    // 获取类型位数大小, 计算 int类型能表示的最大值和最小值
    int int_bits = sizeof(int_min)*8;
    int uint_bits = sizeof(uint_min) * 8;

    // (long long)pow(2, int_bits-1): 强制转换, 防止 pow 执行计算时溢出
    int_min = -(long long)pow(2, int_bits-1);
    int_max = (long long)pow(2, int_bits - 1) - 1;
    uint_max = (long long)pow(2, uint_bits)-1;

    // int 类型printf 用 %d, usigned int 用 %u
    printf("int_bits:%d,uint_bits:%d\n\n", int_bits, uint_bits);

    // 整数溢出
    printf("int_min: % d,int_max:%d \n", int_min, int_max); 
    printf("int_min-1: %d,int_max+1:%d\n", int_min-1, int_max+1);
    printf("int_min-2: %d,int_max+2:%d\n\n", int_min - 2, int_max + 2);

    printf("uint_min:%u, uint_max: %u \n", uint_min, uint_max);
    printf("uint_min-1: %u,uint_max+1:%u \n", uint_min-1,uint_max +1);
    printf("uint_min-2: %u,uint_max+2:%u \n", uint_min -2, uint_max + 2);
    return 0;
}

/*
最大值溢出变成最小值, 最小值溢出变成最大值:

int_bits:32, uint_bits : 32

int_min : -2147483648, int_max : 2147483647
int_min - 1 : 2147483647, int_max + 1 : -2147483648
int_min - 2 : 2147483646, int_max + 2 : -2147483647

uint_min : 0, uint_max : 4294967295
uint_min - 1 : 4294967295, uint_max + 1 : 0
uint_min - 2 : 4294967294, uint_max + 2 : 1
*/

溢出行为是未定义的行为, C 标准并未定义有符号类型的溢出规则。 以上描述的溢出行为比较有代表性, 但是也可能会出现其他情况。所以需要将溢出作为一种错误情况处理, 而不能作为一种计算处理.

使用printf函数, 参数错误输出会出现异常.程序员必须确保转换说明的数量和待打印值的数量相同。 以上内容也提醒读者, 程序员还必须根据待打印值的类型使用正确的转换说明。

#include <stdio.h>
int main(void)
{
    unsigned int uint = 3000000000; /* system with 32-bit int */
    long long llint = 12345678908642; /* 64位 */

    printf("uint = %u ,%%d uint输出 %d\n", uint, uint);
    printf("llint= %lld, %%ld llint只输出一部分 %ld\n", llint, llint);

    return 0;
}

//uint = 3000000000, % d uint输出 - 1294967296
//llint = 12345678908642, % ld llint只输出一部分 1942899938

int 类型被认为是计算机处理整数类型时最高效的类型。 因此, 在 short 和 int 类型的大小不同的计算机中, 用 int 类型的参数传递速度更快。

char类型

char 类型用于存储字符( 如, 字母或标点符号), 但是从技术层面看, char 是整数类型。 因为 char 类型实际上存储的是整数而不是字符。 计算机使用数字编码来处理字符, 即用特定的整数表示特定的字符。 美国最常用的编码是 ASCII 编码, 本书也使用此编码。 例如, 在 ASCII 码中, 整数 65 代表大写字母 A 。因此, 存储字母 A 实际上存储的是整数 65

C 语言把 1 字节定义为 char 类型占用的位( bit )数, 因此无论是 16 位还是 32 位系统, 都可以使用 char 类型。

在 C 语言中, 用单引号括起来的单个字符被称为字符常量 ( character constant )。 编译器一发现' A' ,就会将其转换成相应的代码值。如果省略单引号, 编译器认为 A 是一个变量名; 如果把 A 用双引号括起来, 编译器则认为"A" 是一个字符串。

字符是以数值形式存储的, 所以也可使用数字代码值来赋值:

#include <stdio.h>
int main(void)
{
    char c = 65;
    printf("%c \n", c);// A
    printf("%d", c); //65

    return 0;
}

对c赋值时, 用' A' 代替 65 才是较为妥当的做法, 这样在任何系统中都不会出问题。 因此, 最好使用字符常量, 而不是数字代码值。

根据 C90 标准, C 语言允许在关键字 char 前面使用 signed 或 unsigned 。这样, 无论编译器默认 char 是什么类型, signed char 表示有符号类型, 而 unsigned char 表示无符号类型。 这在用 char 类型处理小整数时很有用。 如果只用 char 处理字符, 那么 char 前面无需使用任何修饰符。

3. 非打印字符

转义序列

转义序列 含义
\a 提醒/(警报)
\b Backspace
\f 换页
\n 换行
\r 回车
\t 水平制表符
\v 垂直制表符
\' 单引号
\" 双引号
\\ 反斜杠
\? 文本问号
\ooo 八进制表示法的 ASCII 字符
\xhh 十六进制表示法的 ASCII 字符
\xhhhh 以十六进制表示法,则此转义序列用于常量宽字符或 Unicode 字符串的 Unicode 字符。例如,WCHAR f = L'\x4e00' 或 WCHAR b[] = L"The Chinese character for one is \x4e00"。
#include <stdio.h>
int main(void)
{
    // 1.使用 ASCII 码。
    char beep = 7;
    //printf("%c", beep);

    // 2 使用特殊的符号序列表示一些特殊的字符。 这些符号序列叫作转义序列 (escape sequence ), \t, \n 等

    printf("\a" );
    return 0;
}

_Bool 类型

C99 标准添加了_Bool 类型, 用于表示布尔值, 即逻辑值 true 和 false 。因为 C 语言用值 1 表示 true ,值 0 表示 false ,所以_Bool 类型实际上也是一种整数类型。 但原则上它仅占用 1 位存储空间, 因为对 0 和 1 而言, 1 位的存储空间足够了。

查看visual studio 的C/C++编译器所实现的标准

需要先设置c语言标准版本.

#include <stdio.h>
#include <stdlib.h>

int main()
{
    printf("%ld\n", __STDC_VERSION__); // 201710
    return 0;
}
/*
名字  宏   标准
C94 __STDC_VERSION__= 199409L   ISO/IEC 9899-1:1994
C99 __STDC_VERSION__ = 199901L  ISO/IEC 9899:1999
C11 __STDC_VERSION__ = 201112L  ISO/IEC 9899:2011
C18 __STDC_VERSION__ = 201710L  ISO/IEC 9899:2018
*/

可移植类型:

C99 新增了两个头文件 stdint.h 和 inttypes.h ,以确保 C 语言的类型在各系统中的功能相同。

C 语言为现有类型创建了更多类型名。 这些新的类型名定义在 stdint.h 头文件中。 例如,

  • int32_t 表示 32 位的有符号整数类型。 在使用 32 位 int 的系统中, 头文件会把 int32_t 作为 int 的别名。
  • 不同的系统也可以定义相同的类型名。 例如, int 为 16 位、 long 为 32 位的系统会把 int32_t 作为 long 的别名。 然后,
  • 使用 int32_t 类型编写程序, 并包含 stdint.h 头文件时, 编译器会把 int 或 long 替换成与当前系统匹配的类型。

1.精确宽度整数类型:精确指定整数的位数, 但系统有可能不支持

上面讨论的类型别名是精确宽度整数类型 ( exact-width integer type )的示例。 int32_t 表示整数类型的宽度正好是 32 位。 但是, 计算机的底层系统可能不支持。 因此, 精确宽度整数类型是可选项。

如果计算机底层系统支持, 使用类似 int32_t 这样的类型, 可以直接锁定变量的取值范围.

兼容性更好的做法是, 使用最小宽度类型来保证变量的最小取值范围.

2.最小宽度类型: 确保变量满足最小位数

如果系统不支持精确宽度整数类型怎么办? C99 和 C11 提供了第 2 类别名集合。 一些类型名保证所表示的类型一定是至少有指定宽度的最小整数类型。 这组类型集合被称为最小宽度类型 ( minimum width type )。 例如, int_least8_t 是可容纳 8 位有符号整数值的类型中宽度最小的类型的一个别名。 如果某系统的最小整数类型是 16 位, 可能不会定义 int8_t 类型。 尽管如此, 该系统仍可使用 int_least8_t 类型, 但可能把该类型实现为 16 位的整数类型。

3.最快最小宽度类型: 对指定宽度范围内的整数运算速度最快的类型

当然, 一些程序员更关心速度而非空间。 为此, C99 和 C11 定义了一组可使计算达到最快的类型集合。 这组类型集合被称为最快最小宽度类型 ( fastst minimum width type )。 例如, int_fast8_t 被定义为系统中对 8 位有符号值而言运算最快的整数类型的别名。

4.最大整数类型:能存储的最大整数

有些程序员需要系统的最大整数类型。 为此, C99 定义了最大的有符号整数类型 intmax_t ,可存储任何有效的有符号整数值。 类似地, uintmax_t 表示最大的无符号整数类型。 顺带一提, 这些类型有可能比 long long 和 unsigned long 类型更大, 因为 C 编译器除除了实现标准规定的类型以外, 还可利用 C 语言实现其他类型。 例如, 一些编译器在标准引入 long long 类型之前, 已提前实现了该类型。

附录 B 中的参考资料 VI “扩展的整数类型” 介绍了完整的 inttypes.h 和 stdint.h 头文件。

float 、double 和 long double

  • C 标准规定, float 类型必须至少能表示 6 位有效数字, 且取值范围至少是 10-37 ~ 10 + 37 。
  • 前一项规定指 float 类型必须能够表示 33.333333 的前 6 位数字, 而不是精确到小数点后 6 位数字。
  • 后一项规定用于方便地表示诸如太阳质量( 2.0e30 千克)、 一个质子的电荷量( 1.6e-19 库仑) 或国家债务之类的数字。
  • 通常, 系统存储一个浮点数要占用 32 位。 其中 8 位用于表示指数的值和符号, 剩下 24 位用于表示非指数部分( 也叫作尾数或有效数 )及其符号。
  • C 语言提供的另一种浮点类型是 double (意为双精度)。
  • double 类型和 float 类型的最小取值范围相同, 但至少必须能表示 10 位有效数字。
  • 一般情况下, double 占用 64 位而不是 32 位。
  • 一些系统将多出的 32 位全部用来表示非指数部分, 这不仅增加了有效数字的位数( 即提高了精度), 而且还减少了舍入误差。
  • 另一些系统把其中的一些位分配给指数部分, 以容纳更大的指数, 从而增加了可表示数的范围。
  • 无论哪哪种方法, double 类型的值至少有 13 位有效数字, 超过了标准的最低位数规定。
  • C 语言的第 3 种浮点类型是 long double ,以满足比 double 类型更高的精度要求。 不过, C 只保证 long double 类型至少与 double 类型的精度相同。

浮点型常量

  • 浮点型常量的基本形式是: 有符号的数字( 包括小数点), 后面紧跟 e 或 E, 最后是一个有符号数表示 10 的指数。
  • 默认情况下, 编译器假定浮点型常量是 double 类型的精度。
  • 语句 some = 4.0 * 2.0; 中, 4.0 和 2.0 被存储为 64 位的 double 类型, 使用双精度进行乘法运算, 然后将乘积截断成 float 类型的宽度。 这样做虽然计算精度更高, 但是会减慢程序的运行速度。
  • 在浮点数后面加上 f 或 F 后缀可覆盖默认设置, 编译器会将浮点型常量看作 float 类型, 如 2.3f 和 9.11E9F ,从而加快程序的运行速度.
  • 使用 l 或 L 后缀使得数字成为 long double 类型, 如 54.3l 和 4.32L 。注意, 建议使用 L 后缀, 因为字母 l 和数字 1 很容易混淆。
  • 没有后缀的浮点型常量是 double 类型
  • C99 标准添加了一种新的浮点型常量格式—— 用十六进制表示浮点型常量, 即在十六进制数前加上十六进制前缀( 0x 或 0X ), 用 p 和 P 分别代替 e 和 E ,用 2 的幂代替 10 的幂( 即, p 计数法)。
  • 0xa.1fp10: 十六进制 a 等于十进制 10 ,. 1f 是 1/ 16 加上 15/ 256 (十六进制 f 等于十进制 15 ), p10 是 210 或 1024 。0xa. 1fp10 表示的值是( 10 + 1/ 16 + 15/ 256) × 1024 (即, 十进制 10364.0 )。

打印浮点值

  • printf() 函数使用% f 转换说明打印十进制记数法的 float 和 double 类型浮点数,
  • 用% e 打印指数记数法的浮点数。 如果系统支持十六进制格式的浮点数, 可用 a 和 A 分别代替 e 和 E 。
  • 打印 long double 类型要使用% Lf 、% Le 或%La 转换说明。
  • 给那些未在函数原型中显式说明参数类型的函数( 如, printf() )传递参数时, C 编译器会把 float 类型的值自动转换成 double 类型。
#include <stdio.h>
int main(void)
{
    float aboat = 32000.0;
    double abet = 2.14e9;
    long double dip = 5.32e-5;

    printf("10进制,指数表示: %f can be written %e\n", aboat, aboat);
    // next line requires C99 or later compliance
    printf("16进制,以2为底的指数表示:And it's %a in hexadecimal, powers of 2 notation\n", aboat);
    printf("10进制常规表示法转为指数表示法: %f can be written %e\n", abet, abet);
    printf("10进制常规表示法转为指数表示法: %Lf can be written %Le\n", dip, dip);

    return 0;
}

//10进制, 指数表示: 32000.000000 can be written 3.200000e+04
//16进制, 以2为底的指数表示 : And it's 0x1.f400000000000p+14 in hexadecimal, powers of 2 notation
//10进制常规表示法转为指数表示法 : 2140000000.000000 can be written 2.140000e+09
//10进制常规表示法转为指数表示法 : 0.000053 can be written 5.320000e-05

浮点值的上溢和下溢

上溢 ( overflow ): 当计算导致数字过大, 超过当前类型能表达的范围时, 就会发生上溢。 这种行为在过去是未定义的, 不过现在 C 语言规定, 在这种情况下会给 toobig 赋一个表示无穷大的特定值, 而且 printf() 显示该值为 inf 或 infinity (或者具有无穷含义的其他内容)。

把一个有 4 位有效数字的数( 如, 0.1234E-10 )除以 10 ,得到的结果是 0.0123E-10 。虽然得到了结果, 但是在计算过程中却损失了原末尾有效位上的数字。 这种情况叫作下溢 ( underflow )。 C 语言把损失了类型全精度的浮点值称为低于正常的 (

#include <stdio.h>

int main(void)
{

    float f_a = 1.2345678e10;
    double d_b = 1.2345678e10;
    float f_sum = (float)f_a + (float)1.0;
    double d_sum = d_b + 1.0;

    printf("f_a: %f, f_sum: %f \n", f_a, f_sum);
    // f_a 的定义中,有效位数已经突破了 float 的限制, 所以 f_a 本身和 f_a 参与的运算的结果都是错的
    // f_a: 12345677824.000000, f_sum: 12345677824.000000



    printf("d_b: %f, d_sum: %f \n", d_b, d_sum);
    // d_b 和 d_b 参与的运算的结果的有效位数都没有溢出, 结果是对的
    // d_b: 12345678000.000000, d_sum : 12345678001.000000

    d_b = 1.2345678e21;
    d_sum = d_b + 1.0;
    printf("d_b: %f, d_sum: %f \n", d_b, d_sum);
    // double 也有有效位数的限制, 同样会溢出
    //d_b: 1234567800000000032768.000000, d_sum: 1234567800000000032768.000000

    return 0;
}

复数和虚数类型

  • C 语言有 3 种复数类型: float _Complex 、double _Complex 和 long double _Complex 。例如, float _Complex 类型的变量应包含两个 float 类型的值,分别表示复数的实部和虚部。 类似地, C 语言的 3 种虚数类型是 float _Imaginary 、double _Imaginary 和 long double _Imaginary 。
  • 如果包含 complex.h 头文件, 便可用 complex 代替_Complex ,用 imaginary 代替_Imaginary ,还可以用 I 代替-1 的平方根。
    • 为何 C 标准不直接用 complex 作为关键字来代替_Complex ,而要添加一个头文件( 该头文件中把 complex 定义为_Complex )? 因为标准委员会考虑到, 如果使用新的关键字, 会导致以该关键字作为标识符的现有代码全部失效。 例如, 之前的 C99 ,许多程序员已经使用 struct complex 定义一个结构来表示复数或者心理学程序中的心理状况( 关键字 struct 用于定义能存储多个值的结构, 详见第 14 章)。 让 complex 成为关键字会导致之前的这些代码出现语法错误。 但是, 使用 struct _Complex 的人很少, 特别是标准使用首字母是下划线的标识符作为预留字以后。 因此, 标准委员会选定_Complex 作为关键字, 在不用考虑名称冲突的情况下可选择使用 complex 。

其他类型

虽然 C 语言没有字符串类型, 但也能很好地处理字符串。

C 语言还有一些从基本类型衍生的其他类型, 包括数组、 指针、 结构和联合。

sizeof 是 C 语言的内置运算符, 以字节为单位给出指定类型的大小。 C99 和 C11 提供% zd 转换说明匹配 sizeof 的返回类型 。一些不支持 C99 和 C11 的编译器可用% u 或% lu 代替% zd 。

把一个类型的数值初始化给不同类型的变量时, 编译器会把值转换成与变量匹配的类型, 这将导致部分数据丢失。

赋值时,小数位数决定精度损失的多少.

#include <stdio.h>

int main(void)
{

    int a = 12.99;
    float b = 12.123456789;
    double c = 12.123456789;
    printf("a: %d \n", a); //a: 12 直接舍弃小数部分
    printf("b: %f \n", b); // b: 12.123457, 会做四舍五入保持一定的精度
    printf("c: %f \n", c); // c: 12.123457
    printf("size of a, b, c: % zd % zd % zd \n", sizeof(a), sizeof(b), sizeof(c)); // size of a, b, c:  4  4  8

    b = 0.12123456789;
    c = 0.12123456789;
    printf("b: %f \n", b); 
    printf("c: %f \n", c); 
    // b: 0.121235   
    // c: 0.121235

    b = 121234.56789;
    c = 121234.56789;
    printf("b: %f \n", b);
    printf("c: %f \n", c);
    // b: 121234.570312
    // c: 121234.567890


    b = 1.2123456789e10;
    c = 1.2123456789e10;
    printf("b: %f \n", b);
    printf("c: %f \n", c);
    // b: 12123456512.000000
    // c: 12123456789.000000

    b = 121234.56789e5;
    c = 121234.56789e5;
    printf("b: %f \n", b);
    printf("c: %f \n", c);
    // 没有变化
    // b: 12123456512.000000
    // c: 12123456789.000000


    return 0;
}

许多程序员和公司内部都有系统化的命名约定, 在变量名中体现其类型。 例如, 用 i_ 前缀表示 int 类型, us_ 前缀表示 unsigned short 类型。 这样, 一眼就能看出来 i_smart 是 int 类型的变量, us_versmart 是 unsigned short 类型的变量。

© Licensed under CC BY-NC-SA 4.0

没有人足够完美,以至可以未经别人同意就支配别人。 ——林肯

发表我的评论
取消评论
表情

Hi,您需要填写昵称和邮箱!