这篇文章上次修改于 613 天前,可能其部分内容已经发生变化,如有疑问可询问作者。

C Primer Plus 笔记1

3.4.5 可移植类型:stdint.h 和 intypes.h

3.4.6 浮点型常量

默认情况下,编译器假定浮点型常量是double类型的精度,假设以下:

float sum;
sum = 4.0 * 2.0

4.0和2.0均被认定为更高精度的double,相乘的结果也会是double,于是这里进行了截断损失精度,减慢程序的运行速度。

但是可以在常量后面加上F或者f来代表常量的类型是float,同理在后面加上L或者l即可代表常量的类型是long double(没有 long float所以不用想太多 )。

常量还可以以0X或者0x再用p计数法,例如0xa.1fp10

%e代表以指数计数法格式化成字符串,%a代表格式化成16进制的指数计数法

%Lf或者%La或者%Le

关于一个常规的单精度浮点数float在IEEE 754标准中使用32位表示

最小正规范化数(smallest normalized positive value):约为 1.175494e-38

最大正规化数(largest normalized positive value):约为 3.402823e38

浮点值的上溢会变成无穷大,C99定义了上溢的结果INF

浮点值的下溢会导致精度的损失,具体原因是因为指数部分的位数已经不够了,它将会占用表示精度部分的位来表示指数,此时精度就损失了

4.4.3 printf() 的转换说明修饰符

%zd 是用来格式化输出 size_t 类型的值

printf("Type int has a size of %zd bytes.\n", sizeof(int));

其他见表

4.4.4 转换说明的意义

float类型的值作为printf的参数时会被转换成double类型。

printf如果有不正确的转换说明,会导致其他的转换声明异常

printf的返回值是它输出字符的个数,如果失败则是负数

4.4.5 使用scanf()

scanf的转换说明(转换说明修饰符)见表

scanf("%c", &ch)      //从输入的第一个字符开始读取
scanf(" %c", &ch)     //从输入的第一个非空白字符开始读取

scanf()的返回值是它成功读取的项数,当遇到文件结尾时会返回EOF (通常被预处理为-1)

4.4.6 printf() 和 scanf() 的 * 修饰符

printf() 的 * 修饰符

int precision = 3;
printf("%.*f\n", precision, 3.14159);

上述代码将打印浮点数3.14159,并将其精度设置为3,即打印小数点后3位。在这种情况下,precision变量的值会用于指定浮点数的精度。这是用于动态设置精度的。

scanf() 的 * 修饰符

int num1;
scanf("%d %*d", &num1);

在这个例子中,scanf会读取两个整数,但第二个整数前面的*修饰符会将它忽略,不赋给任何变量。用于忽略读取的部分值。

5.3.3 递增运算符

说到底 i++ 和 ++i 都属于右值,以下为个人理解:

i++ 和 ++i 让我联想到了python新的象牙运算符,即在表达式中对左值变量进行赋值。

++i就是在得出表达式的值之前进行递增,i++相反。并且++i 和 i++ 的表达式值都是i

于是我看到了这个

int main(void)
{
    int j = 3, m;
    m = (++j) + (++j) + (j++);

    printf("m:%d\n", m);
    printf("j:%d\n", j);
    return 0;
}

最后发现不同编译器给出来的结果是不一样的。即未定义的行为。

应该是没有给出对应的规范,所以应该完全避免对同一个变量进行多次修改并在同一表达式中使用

书中给出建议:

  • 如果一个变量出现在一个函数的多个参数中,不要对该变量使用递增或者递减运算符
  • 如果一个变量多次出现在一个表达式中,不要对该变量使用递增或者递减运算符

5.4 表达式和语句

译者注了两句话:

根据C标准,声明不是语句。这与C++有所不同。

在C语言中,赋值和函数调用都是表达式。没有所谓的“赋值语句”和“函数调用语句”,这些语句实际上都是表达式语句。

副作用和序列点

副作用

从C语言的角度,主要的目的是对表达式求知,但如果途中有变量的值变化,那这种变化就是副作用,除了主要目的之外的就是副作用。例如,在调用printf()函数的时候,它显示的信息就是副作用(因为它的返回值是待显示的字符的个数)

序列点

是程序执行的点,在该点上,所有的副作用都在进入下一步之前发生,在C语言中,语句中的分号标记了一个序列点。意思是,在一个语句中,赋值运算符、递增运算符和递减运算符对运算对象做的改变必须在程序执行下一条语句之前完成。

完整表达式

指一个表达式不是另一个更大表达式的子表达式,并且一个完整表达式的结尾就是一个序列点。

现在我们来看这个语句

y = (4 + x++) + (6 + x++);

表达式4 + x++并不是一个完整表达式,所以C语言无法保证x在子表达式上求值之后立即递增x。这里分号标记了我们的序列点,所以在下一句语句之前,我们要保证所有的副作用也就是递增行为完成,但是C没有指明是在对子表达式求值完后递增,还是整个求完之后递增

类型转换

  • 当类型转换出现在表达式时都是从较小类型转换为较大类型
  • 设计两种类型的运算,两个值会被分辨转换为两种类型的更高级别
  • 类型的级别从高到低依次是 long double > double > float > unsigned long long > long long >unsigned long > long > unsigned int > int .

运用强制类型转换可以明确表达转换类型的意图,保护程序免受不同版本的编译器的影响。

6.1.2 C风格读取循环

while(scanf("%ld", &num) == 1){
    /*循环行为*/
}

6.3.4 新的bool类型

C99在头文件stdbool.h的头文件里面将bool作为_Bool的别名,并且把true和false定义为1和0。

6.7 逗号运算符

ounces++, cost = ounces * FIRST_OZ

作为逗号的序列点保证了在左侧递增完成之后再进行右侧的运算和赋值。

其次,整个逗号表达式的值是右侧项的值,例如

x = (y = 3, (z = ++y + 2) + 5) // x的值为11
    
h = 249, 500;   // h的值是249
h = (249, 500); // h的值是500

其他地方的逗号一般是逗号分隔符而并非运算符。

7.2.1 介绍 getchar() 和 putchar()

ch = getchar();
scanf("%c", &ch);
//两者相同
putchar(ch);
printf("%c", ch);
//两者相同

它们运行更快,而且它们通常是预处理宏,而不是真正的函数。

C特有的编程风格——把两个行为合并成一个表达式

while((ch = getchat()) != "\n"){
    /*循环行为*/
}

7.3.1 备选拼写:iso646.h头文件

里面可以使and,or,not对应逻辑关系符

缓冲区

缓冲分为两类:完全缓冲I/O行缓冲I/O。完全缓冲输入指的是当缓冲区被填满才刷新缓冲区(内容被发送至目的地),通常出现在文件输入中。缓冲区的大小取决于系统,常见的大小是512字节和4096字节。行缓冲指的是再出现换行符的时候才会刷新缓冲区。键盘输入通常是行缓冲输入。

8.3.2 文件尾

"stdio.h"中定义了文件尾EOF的值(通常为-1)

int ch;
while((ch = getchar()) != EOF)
    putchar(ch);

getchar()返回的类型是int,如果定义char将会无法接收EOF。

8.5.1 使用缓冲输入

直接放两块代码自己对比

while(getchar() != 'y'){
    /*show something*/
}
while(getchar() != 'y'){
    /*show something*/
    while(getchar() != "\n"){
        continue;
    }
}

10.1.2 指定初始化器

例1:

int arr[6] = {0, 0, 0, 0, 0, 212}; // 两式含义一致
int arr[6] = {[5] = 212}; // 未被赋值的默认为 0

例2:

int arr[12] = {31, 28, [4] = 31, 30, 31, [1] = 29};
int arr[12] = {31, 29, 0, 0, 31, 30, 31, 0, 0, 0, 0, 0}

如果指定初始化器后面有更多的值,如该例中的初始化列表片段[4] = 31, 30, 31嘛,那么后面的这些值将被用于初始化指定元素后面的元素。

如果指定初始化指定了已经初始化的元素,将覆盖原来的值。

例3:

int stuff[] = {1, [6] = 23}; // 将被初始化为7个元素
int stuff[] = {1, [6] = 4, 9, 10}; // 将被初始化为9个元素

10.6.2 const的其他内容

把const数据或非const数据的地址初始化为指向const的指针或为其赋值是合法的,然而只能把非const数据的地址赋给普通指针。使用非const标识符修改const数据是未定义的。

const指针和指向const数据的指针

const double * pc = // 指向const数据
double * const pc = // 指针本身const

10.7 指针与多维数组

储存方法,多维数组按照一维顺序储存例如一个三维数组a(假设3x3x3),储存顺序为a[0][0][0], a[0][0][1], a[0][0][2], a[0][1][0]等等顺序储存。

假设有以下情况

int zippo[3][3] = {{1, 2, 3}, 
                   {4, 5, 6}, 
                   {7, 8, 9}};

zippo即第一组元素的地址,它身为指针类型为 int (*)[3]意思为指向有三个值的int数组,同时,它的递增代表往后3个int类型的字节大小。

zippo[0]即第一组元素第一个元素的指针,它的类型为int *即一个int指针,它的递增代表往后一个int类型的字节大小。

zippo[0][0]即第一组第一个元素的值,它的类型为int,不是一个指针。

按照该原理zippo[0][0] == *(zippo[0])同样*zippo == &zippo[0][0]同理

*&zippo[0][0] == **zippo 这些例子只是为了深刻理解。

zippo[m][n]等价的指针表示法就是*(*(zippo + m) + n)

10.7.1 指向多维数组的指针

int *arr1; 	       //指向一维数组的指针
int (*arr2)[4];    // 指向具有4列的二维数组的指针
int (*arr3)[3][4]; // 指向具有3行4列的三维数组的指针

10.7.2 指针的兼容性

把非const指针赋给const指针没问题,前提是只进行一次解引用。

下面这个例子解释了使用两次解引用可能会发生的问题:

const int **pp2;
int *p1;
const int n = 13;
pp2 = &p1;
*pp2 = &n;
*p1 = 10

C const 和 C++ const

c++允许在声明数组大小的时候使用const整数,而C不允许(这里不考虑变长数组即C99之前)

C++不允许把const指针赋给非const指针,而C允许但未定义

10.8 变长数组(VLA)

变长数组就是指用变量定义数组的长度,但是定义之后数组长度还是不能改变。

需要注意的是,变长数组的长度不能是负数或者是零。另外,变长数组只能在其定义的作用域内使用,并且不可以作为静态数组或全局数组。

于是我们可以利用变长数组声明以下函数

int sum2d(int rows, int cols, int arr[rows][cols]);

注意:rows 和 cols需要定义在使用它们之前

C99/C11 标准规定,可以省略声明中的参数名,但是在这种情况下,必须用星号代替省略的维度

int sum2d(int, int, int arr[*][*]);

10.9 复合字面量

复合字面量(Compound Literals)是C语言中的一种特性,它允许在代码中创建一个匿名的复合数据类型(例如数组或结构体)并为其初始化,而无需先定义一个具名的变量。例如5是int的复合字面量,'Y’是char的复合字面量。

复合字面量的语法形式是在一对括号内指定数据类型,并在括号内使用初始化列表来为复合类型的元素赋值。它的基本形式如下:

(type) { initializer_list }

其中,type是指定的数据类型,可以是数组类型、结构体类型或联合体类型,initializer_list是初始化列表,用于为复合类型的元素赋初值。

这里主要说明的是创建数组和结构内容的复合字面量

(int [2]){10, 20} // 复合字面量,它相当于一个匿名的变量,必须在创建的时候使用它

int *arr = (int[]) {1, 2, 3, 4, 5}; // 你可以省略掉数字

11.1 在程序中定义字符串

用双引号括起来的内容就是字符串的复合字面量例如"hello world!"

字符串的复合字面量被视为指向该字符串的指针,即*"hello" == 'h'

可以省略数组初始化声明的大小让它自动计算字符串大小或者使用指针

const char m2[] = "hello world!";
const char * m2 = "hello world!";

但这两个行为看似一致,实则有区别:

首先会将该字符串存入静态储存区(程序的二进制文件也载入该字符串)

数组形式:在运行时,拷贝静态储存区的数组,分配在栈上,属于是一份完整的副本,在不带const的情况下可以修改字符串。

指针形式:将静态储存区的字符串地址交给指针,并不会拷贝内容,但需要注意的是,无论是否使用了const,m2都将指向一个字符串常量,其内容不可修改,所以正确的方式是使用 const char* 来声明指向字符串字面量的指针,以表明你不会修改字符串内容。

另外,数组名虽然是一个指向第一个元素的指针,但它也是一个常量地址,不可更改其地址,例如:arr++。对比之下,指针就可以自递增并且修改。

字符串数组

对于建立字符串数组,同样有两种方式

char strArray[][10] = {"Hello", "Hi", "Greetings"};
char * strArray[] = {"Hello", "Hi", "Greetings"};

同样含义也是不同的,对于数组形式,必须设置最大的字符串的长度+1作为宽度,同样是从静态储存区拷贝。对于指针形式,相当于是包含三个指针的指针数组。

请使用指针数组,因为它比二维字符数组的效率高,但它也有缺点,它内部的指针指向的字符串不能修改,而另一者可以修改。所以还是按照你是否需要修改的需求来判断哪个作为选择。

11.2 字符串输入

get()函数读取整行输入,直到遇到换行符,然后丢弃换行符,将读取的字符后加上空字符成为字符串,储存其余字符。

其参数是用来储存读取数据的地址,它的问题出在只知道数组的开始处,而如果输入的字符串过长会导致缓冲区溢出,发生不确定的行为。

C11在标准中废弃了这个函数

gets()的替代品

  1. fgets()函数(和fput())

它有三个参数,分别是储存的开始地址, 最大读取数量, 要读入的流(例如stdin)。

它返回第一个参数,但是一旦读取到文件尾,它将会返回空指针(在代码中可以用NULL来代替)

fput() 有两个参数,第二个参数指明它要写入的流(例如stdout)

注意:fget()会读取换行符,可以通过判断最后一个字符是否为换行符来判断是否读取一整行。

while (words[i] != '\n' && words[i] != '\0')
    i++
if(words[i] == '\n')
	words[i] = '\0' // 替换掉结尾的换行符
else
    while (getchar() != '\n')
    	continue; // 丢弃多余的未读取到的字符
  1. gets_s() 函数

C++11新增的函数

它只有两个参数,因为它只读取标准输入,其他与fgets()相同

如果读到换行符,将会丢弃而不是储存。

如果读到最大字符没有读到换行符,会先把目标数组的首字符设置为空字符,读取并丢弃随后的输入直至读到换行·符或者文件结尾,然后返回空指针。

scanf() 输入函数

可指定字符宽度读取字符串

count = scanf("%5s %10s", name1, name2);

scanf会读取到指定个数或者遇到空字符完成一个输入,剩余部分将保留。

11.3 字符串输出

  1. puts() 函数

把字符串的地址作为参数输出字符直至遇到空字符并在结尾放置一个\n

  1. fputs() 函数

多一个输出流的参数,并且它不会在结尾放置\n

char line[81];
while(gets(line))
    puts(line);