c语言值得注意的知识

1.说明下列每对scanf格式串是否等价?如果不等价,请指出它们的差异。

(c) "%f"与"%f  "。

在 `scanf` 函数中,`"%f"` 和 `"%f "` 这两种格式的区别在于后面的空格。

1. `scanf("%f", &variable);` 这种情况下,`scanf` 会读取并解析用户输入的浮点数,然后将解析的值存入 `variable` 中。一旦读取到非数字字符(例如空格,换行,制表符等),`scanf` 就会停止读取。

2. `scanf("%f ", &variable);` 这种情况下,`scanf` 在解析并存储浮点数之后,还会继续读取并忽略掉后面的任何空白字符(包括空格,换行和制表符)。也就是说,`scanf` 将会读取直到下一个非空白字符为止。

简单地说,`"%f"` 读取一个浮点数,然后停在后面的第一个非数字字符上,而 `"%f "` 读取一个浮点数,并且会跳过其后的所有空白字符,直到遇到下一个非空白字符。

假设我们有下面这样的输入:`12.34 56.78`

如果你的代码如下:

float a, b;
scanf("%f", &a);
scanf("%f", &b);

 

`scanf("%f", &a);` 会读取 `12.34`,然后停止在空格处,因为空格是非数字字符。然后,`scanf("%f", &b);` 会从空格后的 `56.78` 开始读取,因此,`a` 将得到 `12.34`,`b` 将得到 `56.78`。

然而,如果你的代码是这样的:

float a, b;
scanf("%f ", &a);
scanf("%f", &b);

`scanf("%f ", &a);` 会读取 `12.34`,然后继续读取并忽略后面的空格,停在 `56.78` 的前面。然后,`scanf("%f", &b);` 将从 `56.78` 开始读取。所以,结果仍然是 `a` 得到 `12.34`,`b` 得到 `56.78`。

区别在于,如果你在 `"%f "` 后面还想继续读取空白字符或者做其他的 `scanf` 操作,可能就会出现预料之外的结果,因为 `"%f "` 会跳过浮点数后面的所有空白字符。

 

2. scanf 输入陷阱

假设scanf函数调用的格式如下:scanf("%d%f%d", &i, &x, &j); 如果用户输入10.3 5 6 调用执行后,变量i、x和j的值分别是多少?(假设变量i和变量j都是int型,变量x是float型。)

scanf("%f%d%f", &x, &i, &y);

12.3 45.6 789

x = 12.3
i = 45
y = 0.6

 

3. 关于 %

运算符%要求操作数是整数。如果两个操作数中有一个不是整数,程序将无法编译通过。

问:我想把%运算符用于浮点数,但程序无法通过编译,该怎么办?(p.41)

答:%运算符要求操作数是整数,这种情况下可以试试fmod函数

 

4. v+=e不等价于v = v + e。

*问:前面提到:如果v有副作用,那么v+=e不等价于v = v + e。可以解释一下吗?(p.46)

答:计算v += e只会求一次v的值,而计算v=v +e会求两次v的值。在后一种情况下,对v求值可能引起的任何副作用也都会出现两次。

在下面的例子中,i只自增一次:

a[i++] += 2;

如果用=代替+=,语句变成

a[i++] = a[i++] + 2; 

i的值在别处被修改和使用了,因此上述语句的结果是未定义的。i的值可能会自增两次,但我们无法确定到底会发生什么。

 

5. If i and j are positive integers, does (-i)/j always have the same value as -(i/j)? Justify your answer.

No. The C89 and C99 standards implement division of negative numbers differently: (-9)/7 can produce -1 or -2 in C89,

while -(9/7) will always produce -1. C99 will always truncate the remainder towards zero, however, so the answers produced by (-i)/j and -(i/j) will be equivalent.

例题:

What is the value of each of the following expressions in C89? (Give all possible values if an expression may have more than one value.)

(a) 8 / 5
(b) -8 / 5
(c) 8 / -5
(d) -8 / -5

Solution

(a) 1
(b) -1 or -2
(c) -1 or -2
(d) 1 or 2

例题:

What is the value of each of the following expressions in C89? (Give all possible values if an expression may have more than one value.)

(a) 8 % 5
(b) -8 % 5
(c) 8 % -5
(d) -8 % -5

Solution

(a) 3
(b) -3 or 5
(c) 3 or -5
(d) 3 or 5

 

6.数字的存储

 

7.不同进制常量的注意事项

十进制常量包含0~9中的数字,但是一定不能以零开头:

15 255 32767

八进制常量只包含0~7中的数字,而且必须要以零开头:

017 0377 077777 

十六进制常量包含0~9中的数字和a~f中的字母,而且总是以0x开头:

0xf 0xff 0x7fff

十六进制常量中的字母既可以是大写字母也可以是小写字母:

0xff 0xfF 0xFf 0xFF 0Xff 0XfF 0XFf 0XFF

请记住八进制和十六进制只是书写数的方式,它们不会对数的实际存储方式产生影响。(整数都是以二进制形式存储的,跟表示方式无关。)任何时候都可以从一种书写方式切换到另一种书写方式,甚至可以混合使用:10 + 015 + 0x20的值为55(十进制)

十进制整型常量的类型通常为int,但如果常量的值大得无法存储在int型中,那就用long int类型。如果出现long int不够用的罕见情况,编译器会用unsigned long int做最后的尝试。确定八进制和十六进制常量的规则略有不同:编译器会依次尝试int、unsigned int、long int和unsigned long int类型,直至找到能表示该常量的类型。

要强制编译器把常量作为长整数来处理,只需在后边加上一个字母L(或l):

15L 0377L 0x7fffL

要指明是无符号常量,可以在常量后边加上字母U(或u):

15U 0377U 0x7fffU

L和U可以结合使用,以表明常量既是长整型又是无符号的:

0xffffffffUL。(字母L、U的顺序和大小写无所谓。)

 

8. 整数溢出

有符号整数运算中发生溢出时,程序的行为是未定义的。

无符号整数运算过程中发生溢出时,结果是有定义的:正确答案对2n取模,其中n是用于存储结果的位数。例如,如果对无符号的16位数65 535加1,其结果可以保证为0。

 

9.读/写整数

 

10. 浮点常量

浮点常量可以有许多种书写方式。例如,下面这些常量全都是表示数57.0的有效方式:

57.0 57. 57.0e0 57E0 5.7e1 5.7e+1 .57e2 570.e-1

浮点常量必须包含小数点或指数;其中,指数指明了对前面的数进行缩放所需的10的幂次。

如果有指数,则需要在指数数值前放置字母E(或e)。可选符号+或-可以出现在字母E(或e)的后边。

默认情况下,浮点常量以双精度数的形式存储。

换句话说,当C语言编译器在程序中发现常量57.0时,它会安排数据以double类型变量的格式存储在内存中。这条规则通常不会引发任何问题,因为在需要时double类型的值可以自动转换为float类型值。

 

在某些极个别的情况下,可能会需要强制编译器以float或longdouble格式存储浮点常量。为了表明只需要单精度,可以在常量的末尾处加上字母F或f(如57.0F);而为了说明常量必须以longdouble格式存储,可以在常量的末尾处加上字母L或l(如57.0L)

 

11. 读写浮点数

读取double类型的值时,在e、f或g前放置字母l:

double d;

scanf("%lf", &d);

注意:只能在scanf函数格式串中使用l,不能在printf函数格式串中使用。在printf函数格式串中,转换e、f和g可以用来写float类型或double类型的值。(C99允许printf函数调用中使用%le、%lf和%lg,不过字母l不起作用。)

读写long double类型的值时,在e、f或g前放置字母L:

long double ld;

scanf("%Lf", &ld);

printf("%Lf", ld);

 

 

12. 字符常量

字符常量事实上是int类型而不是char类型

 

 

 

13. 用scanf和printf读/写字符

转换说明%c允许scanf函数和printf函数对单个字符进行读/写操作:

char ch;

scanf("%c", &ch); /* reads a single character */

printf("%c", ch); /* writes a single character */

在读入字符前,scanf函数不会跳过空白字符。

如果下一个未读字符是空格,那么在前面的例子中,scanf函数返回后变量ch将包含一个空格。

为了强制scanf函数在读入字符前跳过空白字符,需要在格式串中的转换说明%c前面加上一个空格

 scanf(" %c", &ch); /* skips white space, then reads ch */ 

回顾3.2节的内容,scanf格式串中的空白意味着“跳过零个或多个空白字符”。

因为通常情况下scanf函数不会跳过空白,所以它很容易检查到输入行的结尾:检查刚读入的字符是否为换行符。

例如,下面的循环将读入并且忽略掉当前输入行中剩下的所有字符:

do {

 scanf("%c", &ch);

} while (ch != '\n');

下次调用scanf函数时,将读入下一输入行中的第一个字符。

 

14. getchar 与 scanf 混用时需要注意些什么

 如果在同一个程序中混合使用getchar函数和scanf函数,请一定要注意。

scanf函数倾向于遗留下它“扫视”过但未读取的字符(包括换行符)。

思考一下,如果试图先读入数再读入字符的话,下面的程序片段会发生什么:

printf("Enter an integer: ");

scanf("%d", &i);

printf("Enter a command: ");

command = getchar();

在读入i的同时,scanf函数调用将留下没有消耗掉的任意字符,包括(但不限于)换行符。

getchar函数随后将取回第一个剩余字符,但这不是我们所希望的结果。

 

惯用方法:

while (getchar() != '\n') /* skips rest of line */ 

while ((ch = getchar()) == ' ') /* skips blanks */

 

 

15. 不同类型的变量进行比较或者算术运算时应该注意些什么:

执行常规算术转换的规则可以划分成两种情况。

1.任一操作数的类型是浮点类型的情况

 float -> double -> long double 
 
2. 两个操作数的类型都不是浮点类型的情况
 int -> unsigned int -> long int -> unsigned long int 
 
有一种特殊情况,只有在long int类型和unsignedint类型长度相同(比如32位)时才会发生。在这类情况下,如果一个操作数的类型是longint,而另一个操作数的类型是unsignedint,那么两个操作数都会转换成unsignedlongint类型。
当有符号操作数和无符号操作数组合起来时,有符号操作数会被“转换”为无符号的值。转换过程中需要加上或者减去n+1的倍数,其中n是无符号类型能表示的最大值。
这条规则可能会导致某些隐蔽的编程错误。假设int类型的变量i值为-10,而unsignedint类型的变量u值为10。如果用<运算符比较变量i和变量u,那么期望的结果应该是1(真)。
但是,在比较前,变量i转换为unsignedint类型。因为负数不能被表示成无符号整数,所以转换后的值将不再为-10,而是加上4 294 967 296的结果(假定4 294 967 295是最大的无符号整数) ,即4 294 967 286。因而i<u比较的结果将为0。有些编译器会在程序试图比较有符号数与无符号数时给出一条类似“comparison between signed and unsigned”的警告消息。
因为此类陷阱的存在,所以最好尽量避免使用无符号整数,特别是不要把它和有符号整数混合使用。
 

 

 

16. 类型转换

C语言把(类型名)视为一元运算符。一元运算符的优先级高于二元运算符,因此编译器会把表达式

 (float) dividend / divisor  解释为 ((float) dividend) / divisor 

如果感觉有点困惑,还有其他方法可以实现同样的效果:quotient = dividend / (float) divisor; 

 
 有些时候,需要使用强制类型转换来避免溢出。
思考下面这个例子:
long i; 
int j = 1000; 
i = j * j; /* overflow may occur */
乍看之下,这条语句没有问题。表达式j * j的值是1 000 000,并且变量i是long int类型的,所以i应该能很容易地存储这种大小的值,不是吗?
问题是,当两个int类型值相乘时,结果也应该是int类型的,但是j * j的结果太大,以致在某些机器上无法表示为int型,从而导致溢出。
幸运的是,可以使用强制类型转换避免这种问题发生: i = (long) j * j; 
因为强制运算符的优先级高于*,所以第一个变量j会被转换成long int类型,同时也迫使第二个j进行转换。
注意,语句 i = (long) (j * j); /*** WRONG ***/  是不对的,因为溢出在强制类型转换之前就已经发生了。
 
17. 使用sizeof 时需要注意什么
1. 注意加上括号,虽然不是必须的,但是可以防止运算优先级导致的出错
2. sizeof表达式的类型是size_t,最好将其强转为 unsigned long 类型 ,然后使用转换说明%lu显示
 
printf("Size of int: %lu\n", (unsigned long) sizeof(int)); 

 

18. 十六进制的浮点常量是什么样子?使用这种浮点常量有什么好处?

答:十六进制浮点常量以0x或0X开头,且必须包含指数(指数跟在字母P或p后面)。

指数可以有符号,常量可以以f、F、l或L结尾。指数以十进制数表示,但代表的是2的幂而不是10的幂。

例如,0x1.Bp3表示1.6875×2^3 = 13.5。

十六进制位B对应的位模式为1011;因为B出现在小数点的右边,所以其每一位代表一个2的负整数幂,把它们(2^ -1 + 2^ -3 + 2^ -4)相加得到0.6875。

十六进制浮点常量主要用于指定精度要求较高的浮点常量(包括e和π等数学常量)。十进制数具有精确的二进制表示,而十进制常量在转换为二进制时则可能受到舍入误差的些许影响。十六进制数对于定义极值(例如<float.h>头中宏的值)常量也是很有用的,这些常量很容易用十六进制表示,但难以用十进制表示

 

 

19. 类型定义和宏定义

 

20. 

Does the following statement always compute the fractional part of f correctly (assuming that f and frac_part are float variables)?

frac_part = f - (int) f;
 

If not, what's the problem?

No, the statement could fail if the value of f is larger than the largest value of int.

 

 

21. 指示器

 

 

22. 确定数组长度

#define SIZE ((int) (sizeof(a) / sizeof(a[0]))) 

 

23. 实际参数的转换

24. 传递数组的函数原型

多维数组:

int sum_two_dimensional_array(int n, int m, int a[n][m]); 
int sum_two_dimensional_array(int n, int m, int a[*][*]); 
int sum_two_dimensional_array(int n, int m, int a[][m]); 
int sum_two_dimensional_array(int n, int m, int a[][*]); 

一维数组:

int sum_array(int n, int a[n]);        /* Version 1 */ 
int sum_array(int n, int a[*]);        /* Version 2a */ 
int sum_array(int, int [*]);           /* Version 2b */ 
int sum_array(int n, int a[]);         /* Version 3a */ 
int sum_array(int, int []);            /* Version 3b */ 

 

25. 为什么可以留着数组中第一维的参数不进行说明,但是其他维数必须说明呢?

 

 

26. const int*, int const*以及int *const

const 修饰后面的元素

const int * 修饰 int ,所以指针指向的值不可以改变。

int const *p 修饰 *p, *p 代表解码之后的值,即const还是修饰值,所以指针指向的值不可以改变。

int * const p , const 直接修饰指针p,即指针指向的地址不可以改变,那么指针指向不可以改变。

 

27. 指针的算术运算

* 指针加上整数:

指针p加上整数j产生指向特定元素的指针,这个特定元素是p原先指向的元素后的j个位置。更确切地说,如果p指向数组元素a[i],那么p + j指向a[i + j](当然,前提是a[i + j]必须存在)

 

* 指针减去整数:

如果p指向数组元素a[i],那么p - j指向a[i - j]。
 
* 两个指针相减:
当两个指针相减时,结果为指针之间的距离(用数组元素的个数来度量)。因此,如果p指向a[i]且q指向a[j],那么p - q就等于i - j。
 
* 指针之间的比较
可以用关系运算符(<、<=、>和>=)和判等运算符(==和!=)进行指针比较。只有在两个指针指向同一数组时,用关系运算符进行的指针比较才有意义。
比较的结果依赖于数组中两个元素的相对位置。
例如,在下面的赋值后p <= q的值是0,而p >= q的值是1。
p = &a[5];
q = &a[1];
 
28. 指针用于数组处理
* 运算符 与++运算符的结合

 

 

29. 指针处理多维数组

    for (int *p = &a[0][0]; p <= &a[NUM_ROWS - 1][NUM_COLS - 1]; p++) {)

* 处理多维数组的行 *

* 处理多维数组的列 * 

 

30. 用多维数组名作为指针

 int a[NUM_ROWS][NUM_COLS];

 

 

假设a是一维数组而p是指针变量。如果刚执行了赋值操作p = a,下列哪些表达式会因为类型不匹配而不合法?其他的表达式中哪些为真(有非零值)?

p[0] == a[0]

 a[ i ] = *( a + i)

 

30. 如何向函数传入一个二维数组

 

在C语言中,你可以通过以下几种方式向函数传入一个二维数组:

1. 使用指针参数:
函数参数可以声明为指向二维数组的指针。你需要传递二维数组的首地址给函数,同时在函数参数中声明指针类型,并指定数组的列数(如果不固定,可以使用动态内存分配或额外参数传递)。

例如:

复制代码
void function(int (*arr)[N], int rows) {
// 使用arr来访问二维数组
}

int main() {
int arr[M][N];
function(arr, M);
return 0;
}
复制代码

2. 使用一维数组参数:
你可以将二维数组转换为一维数组,并将其作为函数参数传递。在函数内部,通过计算索引来模拟多维数组的访问。

例如:

复制代码
void function(int *arr, int rows, int cols) {
// 使用arr来模拟二维数组的访问
}

int main() {
int arr[M][N];
function(&arr[0][0], M, N);
return 0;
}
复制代码

3. 使用动态内存分配:
如果二维数组的大小在编译时是未知的,你可以使用动态内存分配来创建二维数组,并将其传递给函数。函数参数可以声明为指向适当类型的指针,并在函数内部进行内存管理。

例如:

复制代码
void function(int **arr, int rows, int cols) {
// 使用arr来访问二维数组
}

int main() {
int **arr;
int rows = M;
int cols = N;

// 动态分配二维数组的内存
arr = malloc(rows * sizeof(int *));
for (int i = 0; i < rows; i++) {
arr[i] = malloc(cols * sizeof(int));
}

// 调用函数并传递二维数组
function(arr, rows, cols);

// 释放动态分配的内存
for (int i = 0; i < rows; i++) {
free(arr[i]);
}
free(arr);

return 0;
}
复制代码

无论使用哪种方法,重要的是在函数内部正确处理二维数组的索引和大小,以确保访问和操作二维数组的正确性。

 

31. int *p = &a[0][0] 和 int *p = &a[0]

在C语言中,`int *p = &a[0][0];`和`int *p = a[0];`是有区别的。

1. `int *p = &a[0][0];`: 这行代码将p指向二维数组a的第一个元素的地址。p是一个整型指针,可以逐个遍历整个二维数组的元素。

2. `int *p = a[0];`: 这行代码将p指向二维数组a的第一个子数组(也就是a[0])的第一个元素的地址。换句话说,

在C语言中,`int *p = &a[0][0];`和`int *p = a[0];`是有区别的。

1. `int *p = &a[0][0];`: 这行代码将p指向二维数组a的第一个元素的地址。p是一个整型指针,可以逐个遍历整个二维数组的元素。

2. `int *p = a[0];`: 这行代码将p指向二维数组a的第一个子数组(也就是a[0])的第一个元素的地址。换句话说,p指向的是第一个子数组的开头。虽然这个表达式和上一个看起来可能会访问同一个元素,但这里的意义更多的是以一维数组的形式来处理a[0],如果你试图使用这个指针去访问二维数组a的其他子数组元素(例如a[1][0])可能会造成越界等问题。

所以这两种方式的主要区别在于,第一种方式允许你按照线性方式访问整个二维数组,而第二种方式更多的是让你能够处理二维数组的某一个子数组。

 

虽然这个表达式和上一个看起来可能会访问同一个元素,但这里的意义更多的是以一维数组的形式来处理a[0],如果你试图使用这个指针去访问二维数组a的其他子数组元素(例如a[1][0])可能会造成越界等问题。

所以这两种方式的主要区别在于,第一种方式允许你按照线性方式访问整个二维数组,而第二种方式更多的是让你能够处理二维数组的某一个子数组。

 

在C语言中,无论`p`指向何处,`*p++`都将发生两个操作:

1. 对`p`当前指向的值进行取值操作,就好像在执行`*p`。

2. 然后将指针`p`向前移动一个存储单位,这取决于`p`指向的数据类型的大小。对于`int`类型来说,`p`通常会向前移动4个字节(在大多数现代计算机系统中)。

但是,尽管`*p++`在两种情况下的行为相同,但是由于`p`的初始值不同,所以`p`遍历的元素将会不同。

1. 当`int *p = &a[0][0];`时,`p`开始指向二维数组的第一个元素。每次执行`*p++`,`p`都会移动到下一个元素,直到遍历整个二维数组。

2. 当`int *p = a[0];`时,`p`开始指向二维数组的第一个子数组(也就是a[0])的第一个元素。每次执行`*p++`,`p`都会移动到下一个元素,但只在子数组a[0]中移动。如果你继续执行`*p++`直到超出a[0]的范围,你可能会遇到未定义的行为,因为`p`可能会开始访问不属于原始数组的内存。

总的来说,`*p++`的行为在这两种情况下是相同的,但是它遍历的元素会根据`p`的初始值的不同而不同。

 

31. 字符数组与字符指针

chardate[]="June14"; 
char*date="June14";

在声明为数组时,就像任意数组元素一样,可以修改存储在date中的字符。

在声明为指针时,date指向字面串,在13.1节我们已经看到字面串是不可以修改的。

在声明为数组时,date是数组名。在声明为指针时,date是变量,这个变量可以在程序执行期间指向其他字符串。

 

32. 用scanf 读取字符串

scanf("%s", str);

不需要加取址符,因为str就是数组名,编译器会把他当做指针处理。

调用时,scanf函数会跳过空白字符(3.2节),然后读入字符并存储到str中,直到遇到空白字符为止。scanf函数始终会在字符串末尾存储一个空字符。

用scanf函数读入字符串永远不会包含空白字符。因此,scanf函数通常不会读入一整行输入。换行符会使scanf函数停止读入,空格符或制表符也会产生同样的结果。

 

33. 用gets 读取字符串

gets函数不会在开始读字符串之前跳过空白字符(scanf函数会跳过)。

gets函数会持续读入,直到找到换行符才停止(scanf函数会在任意空白字符处停止)。此外,gets函数会忽略换行符,不会把它存储到数组中,并用空字符代替换行符。

34. 字符串的复制比较

 

 

35. 字符串库 #include <string.h> 

字符串复制: strcpy(str2,"abcd"); 

字符串长度:  len = strlen(strl);  

字符串拼接函数: 

strcpy(str1, "abc"); 
strcat(str1, "def");   /* str1 now contains "abcdef" */ 
strcpy(str1, "abc"); 
strcpy(str2, "def");
strcat(str1, str2);    /* str1 now contains "abcdef" */

字符串比较: if(strcmp(str1,str2)<0) /*is str1 < str2? */ 

 

posted @   哎呦_不想学习哟~  阅读(120)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 【杭电多校比赛记录】2025“钉耙编程”中国大学生算法设计春季联赛(1)
点击右上角即可分享
微信分享提示