随手记:




printf()

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>

int main() {
printf("11111111\n");
int a = 1111;
printf("%8d\n", a); // 右对齐
float b = 3.2;
printf("%8.2f\n", b); // 占位.精度
float c = 0.032;
printf("%e\n", c); // 指数形式输出实数
printf("%p", &c); // %p通常以十六进制显示指针的值
return 0;
}
查看输出
1
2
3
4
5
11111111
1111
3.20
3.200000e-002
000000000061FE14

%o:输出八进制

%f:默认输出小数点后六位

其他print

C语言的printf函数族提供了多种格式化输出功能,主要用于向标准输出设备(如屏幕)或其他输出流(如文件)打印格式化的字符串和数据。这些函数包括printffprintfsprintfsnprintf等,它们的基本用法如下:

  1. printf

printf函数用于向标准输出流(通常是屏幕)打印格式化的字符串。

  • 原型int printf(const char *format, ...);
  • 用法示例
    1
    2
    int a = 5;
    printf("Value of a is %d\n", a);
  1. fprintf

fprintf函数用于向指定的文件流打印格式化的字符串,允许将输出重定向到文件。

  • 原型int fprintf(FILE *stream, const char *format, ...);
  • 用法示例
    1
    2
    3
    4
    5
    6
    FILE *fp = fopen("output.txt", "w");
    int b = 10;
    if(fp) {
    fprintf(fp, "Value of b is %d\n", b);
    fclose(fp);
    }
  1. sprintf

sprintf函数用于将格式化的字符串打印到字符串中,而不是输出到标准输出或文件。

  • 原型int sprintf(char *str, const char *format, ...);

  • 用法示例

    1
    2
    3
    char buffer[50];
    int age = 23;
    sprintf(buffer, "I am %d years old.", age);
  1. snprintf

snprintf函数与sprintf类似,但它允许指定最大字符数以防止缓冲区溢出,是一种更安全的选择。

  • 原型int snprintf(char *str, size_t size, const char *format, ...);
  • 用法示例
    1
    2
    3
    char buffer[20];
    int temperature = 30;
    snprintf(buffer, sizeof(buffer), "Temperature is %d C.", temperature);
  1. vprintf, vfprintf, vsprintf, vsnprintf

这些函数是上述函数的变体,它们接受一个va_list类型的参数,而不是可变数量的参数。这允许它们被其他函数用于处理可变参数列表。

  • 用法示例(vfprintf为例)
    1
    2
    3
    4
    5
    6
    void my_vfprintf(FILE *fp, const char *format, ...) {
    va_list arg;
    va_start(arg, format);
    vfprintf(fp, format, arg);
    va_end(arg);
    }

这些函数使得C语言在处理格式化输出时非常灵活和强大,允许程序员以控制的方式向不同的输出目的地输出格式化文本。





转义字符

ASCII码一览表,ASCII码对照表 (biancheng.net)

ASSCII 对应序
0~9 48~57
a~z 97~122
A~Z 65~90

对于 ASCII 编码,0~31(十进制)范围内的字符为控制字符,它们都是看不见的,不能在显示器上显示,甚至无法从键盘输入,只能用转义字符的形式来表示。不过,直接使用 ASCII 码记忆不方便,也不容易理解,所以,针对常用的控制字符,C语言又定义了简写方式,完整的列表如下:

转义字符 意义 ASCII码值(十进制)
\a 响铃(BEL) 007
\b 退格(BS) ,将当前位置移到前一列 008
\f 换页(FF),将当前位置移到下页开头 012
\n 换行(LF) ,将当前位置移到下一行开头 010
\r 回车(CR) ,将当前位置移到本行开头 013
\t 水平制表(HT) 009
\v 垂直制表(VT) 011
单引号 039
" 双引号 034
\ 反斜杠 092




内存

  1. 栈(Stack)
    • 存放局部变量、函数参数、函数调用的上下文信息(例如函数返回地址、调用者的栈帧指针等)。
  2. 堆(Heap)
    • 存放动态分配的内存,包括通过malloccallocrealloc等函数动态分配的内存。堆的大小在程序运行时是动态变化的。
  3. 全局区/静态区(Global/Static area)
    • 存放全局变量和静态变量。全局变量存储在全局区的静态内存中,它们在整个程序的生命周期内都存在。静态变量也存储在静态内存中,但作用域限制在定义它们的函数内部。
  4. 常量区(Constants area)
    • 存放字符串常量和其他常量数据。这些数据在程序运行时是只读的,不能被修改。常量区通常在可执行文件的只读段中。
  5. 代码区(Code area)
    • 存放程序的机器指令,即可执行代码。代码区在程序运行时被加载到内存中供 CPU 执行。代码区通常也是只读的。



类型

Usual Arithmetic Conversion

截断

c语言中int a = -8;
a中的二进制补码存储为:11111111111111111111111111111000
现在将char b = a; b中的整数是不是直接截断a后半部分

ChatGPT:

在C语言中,当你将一个int类型的值赋值给一个char类型的变量时,发生的确实是截断操作,但具体行为取决于char在你的编译器或平台上是作为有符号类型还是无符号类型处理的。对于大多数现代平台,char默认为有符号类型(但这不是一个严格的标准要求)。

给定你的例子,int a = -8; 的二进制补码表示为 11111111111111111111111111111000。当这个值赋值给一个char类型的变量b时,确实会截断至char类型所能表示的位数,通常是8位。因此,只有这个二进制数的最后8位(11111000)会被赋值给b

如果char被视为有符号类型(在大多数平台上是这样),那么b的值将会是11111000的补码表示的值,即-8。如果char是无符号的,b将表示二进制数11111000直接对应的无符号数,即248。

因此,对于你的问题,是的,b中的值是通过直接截断a的二进制表示的后8位得到的,结果可能是-8或248,具体取决于char的符号性。在大多数情况下,如果char是有符号的,b将保持为-8,因为截断后的二进制补码仍然表示-8。

类型大小

几乎所有C语言中的基本数据类型的大小都是依赖于编译器和运行时环境的。这意味着它们在不同的操作系统、硬件架构、甚至不同的编译器配置下可能会有不同的大小。这种设计使得C语言能够在广泛的硬件上运行,从嵌入式系统到高性能计算服务器,但也意味着编写可移植代码时需要特别注意数据类型的选择。

C语言基本数据类型的环境依赖性

以下是一些C语言中常见的基本数据类型,以及它们大小的环境依赖性:

  • charchar类型用来存储单个字符。按照标准,char的大小是1个字节。不过,字节的大小并不是在所有系统上都是8位;虽然如今几乎所有现代系统上都是8位。
  • intint的大小旨在反映机器上自然的整数大小,以便提供最佳的性能。它通常是16位或32位,但在某些系统上可能是64位。
  • shortshort int(简写为short)至少为16位。
  • longlong int(简写为long)至少为32位,在许多64位系统上是64位。
  • long long:C99标准添加的long long int(简写为long long)类型至少为64位。
  • 浮点类型floatdoublelong double):这些类型用于表示浮点数,它们的大小和精度也因环境而异。通常,float是32位,double是64位,而long double的大小至少和double一样,但具体大小取决于实现。
  • 指针类型:指针类型的大小依赖于系统的地址空间。例如,在32位系统上,指针通常是32位的,在64位系统上,指针通常是64位的。

为了确保程序的可移植性,C语言提供了sizeof运算符,允许程序在运行时查询数据类型的大小。此外,C标准库中的<stdint.h>头文件定义了一组整数类型,它们具有明确的大小(如int16_tint32_tint64_t等),这些类型有助于编写在不同环境下表现一致的程序。

64位机器上,C语言各个类型的大小

在64位机器上,C语言中基本数据类型的大小可能会根据编译器和操作系统的不同有所差异,但通常遵循以下规则:

  • char: 1字节。char 类型保证为1字节,用于存储单个字符。
  • short: 通常是2字节。
  • int: 通常是4字节。在许多64位系统上,int 保持为32位以保持与32位系统的兼容性。
  • long: 在64位系统上,long 通常是8字节(64位)。这是与32位系统上的long(通常为4字节)的主要区别之一。
  • long long: 至少8字节。long long 类型保证至少64位。
  • float: 通常是4字节。float 类型用于单精度浮点数。
  • double: 通常是8字节。double 类型用于双精度浮点数。
  • long double: 大小可能变化,但通常是8字节或更大,具体取决于编译器和系统。long double 用于提供比double更高的精度和范围。
  • 指针: 在64位系统上,所有类型的指针(无论是指向charintfloat还是任何其他类型的指针)都是8字节。指针大小增加是为了能够寻址更大的内存空间。

请注意,这些大小是典型值,实际上可能会有所不同。特别是longlong double类型的大小可能会根据不同的编译器和操作系统而变化。为了精确地了解特定环境下每种类型的大小,可以使用sizeof运算符进行检查,例如sizeof(int)。这会给出在当前编译环境下int类型的确切字节数。

电子科技大学

char:2

int:2

指针:4





数组

  1. 整形数组

数组作为局部变量未初始化:

1
2
3
4
5
6
7
8
9
#include <stdio.h>

int main() {
int array[10];
int i = 0;
for (; i<10; i++)
printf("%d\n", array[i]);
return 0;
}
查看输出
1
2
3
4
5
6
7
8
9
10
11
-1
-1
10897040
0
10884000
0
1
0
141774120
32763

数组作为全局变量未初始化:(初始化为0

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>

int array[10];

int main() {
int i = 0;
for (; i<10; i++)
printf("%d\n", array[i]);
return 0;
}
查看输出
1
2
3
4
5
6
7
8
9
10
11
0
0
0
0
0
0
0
0
0
0

  1. 下面研究字符型数组

数组作为局部变量未初始化:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>

int main() {
char array[10];
int i = 0;
for (; i<10; i++) {
if (array[i] == '\0')
printf("1\n");
else
printf("\n");
}
return 0;
}
查看输出
1
2
3
4
5
6
7
8
9
10





1
1



数组作为全局变量未初始化:(初始化为\0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>

char array[10];

int main() {
int i = 0;
for (; i<10; i++) {
if (array[i] == '\0')
printf("1\n");
else
printf("\n");
}
return 0;
}
查看输出
1
2
3
4
5
6
7
8
9
10
11
1
1
1
1
1
1
1
1
1
1

其他初始化问题

当你用char a[10] = {'a', 'b', 'b', 'b'};这样的方式初始化一个字符数组时,数组的前四个元素会被设置为'a', 'b', 'b', 'b',而剩下的六个元素会自动被初始化为\0(即空字符,其ASCII码值为0)。这是因为在C语言中,当数组被部分初始化时,未明确初始化的元素会自动初始化为该类型的零值。对于字符数组,零值就是空字符\0

因此,数组a在内存中的布局会是这样的:

1
2
3
4
5
6
7
8
9
10
a[0] = 'a'
a[1] = 'b'
a[2] = 'b'
a[3] = 'b'
a[4] = '\0'
a[5] = '\0'
a[6] = '\0'
a[7] = '\0'
a[8] = '\0'
a[9] = '\0'

这个特性使得部分初始化的字符数组可以很方便地用作C风格的字符串,因为C风格的字符串总是以\0作为结束标志。在这个例子中,尽管数组a的大小是10个字符,但它实际上存储的字符串为"abbb",后面跟随足够的\0字符以确保字符串正确结束。


当你使用char a[] = "aaaa";这样的语句初始化一个字符数组时,C编译器会自动在字符串的末尾添加一个'\0'字符作为字符串的终止符。因此,即使你没有显式地添加'\0',字符串"aaaa"实际上在内存中被存储为"aaaa\0"。这就是为什么strlen(a)能够正确返回4的原因:strlen()函数计算字符串的长度时,会从字符串的开头开始计数,直到遇到第一个'\0'字符为止,但不包括'\0'本身。

这种自动添加'\0'的行为确保了使用双引号定义的字符串字面量总是合法的、以'\0'结尾的C风格字符串,使得这些字符串可以安全地与C标准库中的字符串处理函数一起使用。这是C语言设计中的一个方便特性,免除了程序员手动添加字符串终止符的麻烦。


1
2
3
4
5
6
7
8
#include <stdio.h>
#include <string.h>

int main() {
char a[4] = {'a', 'b', 'b', 'b'};
printf("%d", strlen(a));
return 0;
}

这样也输出为4了

ChatGPT:

你的代码示例中,char a[4] = {'a', 'b', 'b', 'b'};初始化了一个没有为'\0'终止符留出空间的字符数组。这意味着,数组a并没有被正确地以'\0'终结,从而不符合C字符串的要求。

当你使用strlen(a)尝试计算这个数组的“长度”时,strlen()会从数组的第一个元素开始,一直数下去直到遇到第一个'\0'字符。由于a数组实际上没有包含'\0'字符(因为没有空间给它),strlen()会继续读取a之后的内存,直到偶然遇到一个'\0'字符为止。这是未定义行为(undefined behavior),可能导致各种各样的问题,包括程序崩溃或输出不可预测的结果。

在你的情况下,strlen(a)返回4可能是因为恰好在数组a紧接着的内存位置上存储了一个'\0'字符。但这完全取决于运行时的具体内存布局,是不可靠的。

要解决这个问题,你需要确保字符数组有足够的空间来包含所有字符以及最后的'\0'终止符,例如:

1
char a[5] = {'a', 'b', 'b', 'b', '\0'};

或者,更简单地,当你使用字符串字面量初始化数组时,编译器会自动加上'\0'

1
char a[] = "abbb"; // 自动成为 {'a', 'b', 'b', 'b', '\0'}

这样,strlen(a)就会正确地返回4,而且是安全和可预测的。

扩展

C语言的一个简便功能,允许程序员将多个相邻的字符串字面量写成分开的部分,编译器在编译代码时会自动将它们连接成一个单一的字符串。这可以用于多种情况,比如当一行字符串过长需要分割成多行书写时,或者像上文例子中,通过宏动态生成部分字符串内容。

让我们通过一个简单的例子来理解这个特性:

1
printf("Hello, " "world!");

在这个例子中,字符串"Hello, ""world!"是相邻的字符串字面量,它们之间没有任何其他符号(比如加号)。在C语言中,这两个字符串会在编译时自动连接成一个字符串,所以上面的代码实际上等同于:

1
printf("Hello, world!");

这意味着输出将会是:

1
Hello, world!

另一个例子:

1
2
3
4
5
6
7
#include <stdio.h>

int main() {
char str[] = "hello""world"; // 输出为helloworld
printf("%s", str);
return 0;
}
  1. 指针型数组

数组作为局部变量未初始化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>

typedef struct {
int val;
}Node;

int main() {
Node * array[10];
int i = 0;
for (; i<10; i++) {
if (array[i])
printf("1\n");
else
printf("\n");
}
return 0;
}
查看输出
1
2
3
4
5
6
7
8
9
10
11
1
1
1
1
1

1
1
1
1

数组作为全局变量未初始化:(初始化为NULL空指针)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>

typedef struct {
int val;
}Node;

Node * array[10];

int main() {
int i = 0;
for (; i<10; i++) {
if (array[i])
printf("1\n");
else
printf("\n");
}
return 0;
}
查看输出
1
2
3
4
5
6
7
8
9
10














寄存器变量

只有函数内定义的变量或形参可以声明为寄存器变量,其他不行

ChatGPT:

是的,你的理解是正确的。在C语言中,register关键字只能用于局部变量和函数参数的声明。这意味着只有在函数内部定义的变量或函数的形式参数可以被声明为寄存器变量。全局变量和静态变量(即使是在函数内部静态声明的)不能声明为寄存器变量。

这样的设计主要是因为register关键字的目的是向编译器提出建议,尽可能地将变量存储在CPU的寄存器中,以提高访问速度。局部变量和函数参数是最有可能从这种存储方式中获益的,因为它们的生命周期短且访问频繁,特别是在循环和频繁调用的函数中。

下面是一些使用register关键字的示例:

1
2
3
4
5
6
7
8
void function(int a) {
register int localVar = 10; // 局部变量声明为寄存器变量
register int i; // 循环变量声明为寄存器变量

for (i = 0; i < localVar; i++) {
// 循环体
}
}

在这个例子中,localVari都被声明为寄存器变量,这向编译器建议使用寄存器来存储这些变量,以期望能够提高程序的执行效率。

然而,正如之前提到的,register声明仅仅是一个建议,编译器可以根据自己的优化策略来决定是否遵循这个建议。现代编译器具有高度的优化能力,能够自动决定哪些变量应该放在寄存器中,因此在现代C语言编程中,register关键字的实际使用相对较少。





NULL

如何理解c语言中的NULL,‘\0’,0

ChatGPT:

在C语言中,NULL'\0'0有着不同的含义和用途,尽管它们在某些情况下看起来可能相似。理解它们之间的区别对于编写清晰、正确的C程序至关重要。

'\0'

  • '\0'表示的是空字符(null character),用于标记C字符串的结束。它是一个字符字面量,其ASCII码值为0。在内存中,'\0'占用一个字节,并且其字节值为0。
  • 用途:主要用于字符串的结束标志。当字符串处理函数(如strlenstrcpy)操作字符串时,它们依赖于'\0'来确定字符串的结束位置。

0

  • 0是一个整数字面量,代表数值零。在不同的上下文中,0可以有不同的含义,但本质上,它表示数值上的零。
  • 用途:
    • 作为整数使用,表示数值0。
    • 在指针上下文中,0被隐式地视为NULL,用于表示空指针。然而,使用NULL而不是0来表示空指针是更好的做法,因为这样做可以提高代码的清晰度和可读性。

NULL

  • NULL是一个宏,用于表示空指针常量。在C标准库中,通常通过#define NULL ((void*)0)来定义。这意味着NULL代表一个指向无类型(void类型)的零地址的指针。
  • 用途:主要用于指针变量,表示指针不指向内存中的任何有效地址。使用NULL来初始化或赋值给指针变量可以清晰地表示该指针目前没有指向任何有效的对象或数据。

总结

  • '\0'是一个空字符,用于字符串的结束。
  • 0是数值零,可以表示整数零或在指针上下文中隐式表示空指针。
  • NULL是一个宏,用于表示空指针,使代码更清晰地表达出指针的“空”状态。

在编程时选择正确的表示形式对于保证代码的意图清晰和准确非常重要。尤其是在涉及指针和字符串操作时,正确使用这些概念可以避免许多常见的错误。





指针

地址运算符:&
一般注解:后跟一个变量名时,&给出该变量的地址。
示例:&nurse表示变量nurse的地址。

地址运算符:*
一般注解:后跟一个指针名或地址时,*给出储存在指针指向地址上的值。
示例:
nurse = 22;
ptr = &nurse; // 指向nurse的指针
val = *ptr; // 把ptr指向的地址上的值赋给val
执行以上3条语句的最终结果是把22赋给val。

  • flizny&flizny[0]都表示数组首元素的内存地址

  • *&优先级相同,方向从右到左

    对于int *p = &a;
    *p就是变量a,而&*p就是&a

    *&a就是变量a

    (*p)++就是a++,而*p++则是先将地址p+1再取值,原因看运算符优先级

指针的算数操作

在C语言中,指针的算术操作有以下规则:

  1. 指针加上或减去一个整数:当你对一个指针加上一个整数n时,结果是一个新的指针,它指向原来的指针所指向的元素之后的第n个元素。类似地,当你从一个指针减去一个整数时,结果是一个新的指针,它指向原来的指针所指向的元素之前的第n个元素。这种操作依赖于指针所指向的类型的大小。

    1
    2
    3
    int arr[5] = {10, 20, 30, 40, 50};
    int* p = &arr[0]; // 指向arr的第一个元素
    p = p + 3; // 现在p指向arr的第四个元素,即40

    为什么p + 3能正好跨越3个int?

    这个行为是由于C语言中指针算术的设计原则所决定的。当你对一个指针执行算术操作时,加上或减去的整数值会根据指针所指向的数据类型的大小进行缩放。这意味着,当你对指针p加上3(p = p + 3;),实际上你不是简单地在p的数值上加3,而是让p前进三个由p所指类型的数据大小决定的内存单位。

    在你的例子中,p是一个指向int的指针。假设在你的系统中,int类型占用4个字节的内存空间。因此,当你写p + 3时,实际上是将p的值增加了3 * sizeof(int)字节。这就意味着p会前进到内存中的下一个位置,该位置恰好是数组中第四个元素的位置。

    这个行为使得指针算术非常适用于遍历数组或者访问数组中的特定元素,因为你可以简单地通过加上或减去一个整数来移动到数组中的不同位置。这也保证了不同数据类型的数组可以使用统一的方式来进行指针操作,而不用担心数据类型大小的问题。

    验证:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    #include <stdio.h>

    int main() {
    int a[10] = {1, 2, 3, 4};
    int *p = a;
    printf("%d\n", p); // 6422000
    printf("%d", p + 3); // 6422012
    return 0;
    }
    1
    2
    3
    4
    5
    6
    7
    8
    #include <stdio.h>

    int main() {
    int *p = NULL;
    printf("%d\n", p); // 0
    printf("%d", p + 3); // 12
    return 0;
    }
  2. 两个指针之间的减法:当你从一个指针减去另一个指针时,结果是这两个指针之间的元素数量。这两个指针通常指向同一个数组或数据结构的不同部分。这种操作的结果是ptrdiff_t类型,这是一个有符号整型,足以存储两个指针之间的差异。

    1
    2
    3
    4
    int arr[5] = {10, 20, 30, 40, 50};
    int* p1 = &arr[1]; // 指向arr的第二个元素
    int* p2 = &arr[4]; // 指向arr的第五个元素
    ptrdiff_t diff = p2 - p1; // diff的值是3,因为p2和p1之间有3个元素
  3. 指针之间的加法是不允许的:你不能将两个指针相加,因为这样的操作没有明确的语义意义,并且在大多数情况下也没有实际的用途。

这些规则确保了指针算术操作的结果是有意义的,并且与内存布局直接相关。尤其是对于数组的操作,这些规则让指针成为访问和遍历数组元素的强大工具。

二维数组相关
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>

int main() {
int a[3][4] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
printf("%d %d\n", a, *a); // 6422000 6422000
printf("%d %d\n", a[0], *(a+0)); // 6422000 6422000
printf("%d %d\n", &a[0], &a[0][0]); // 6422000 6422000
printf("%d %d\n", a[1], a + 1); // 6422016 6422016
printf("%d %d\n", a + 1, *(a + 1)); // 6422016 6422016
printf("%d %d\n", &a[1][0], *(a + 1) + 0); // 6422016 6422016
printf("%d %d\n", a[1][1], *(*(a + 1) + 1)); // 6 6
return 0;
}

在C语言中,定义一个指向二维数组的指针需要考虑数组的维度和元素类型。下面是一些示例,演示了如何定义指向二维数组的指针:
  1. 指向具有固定列数的二维数组的指针

假设我们有一个二维数组 int arr[3][4],我们可以定义一个指向这个数组的指针。

1
2
int (*ptr)[4]; // 指向具有4列的 int 类型的二维数组的指针
ptr = arr; // 将 ptr 指向数组 arr
  1. 指向具有不定列数的二维数组的指针

在这种情况下,我们需要使用指针的指针来处理不定列数的二维数组。

1
int **ptr;     // 指向 int 类型的指针的指针

然而,对于不定列数的二维数组,通常情况下更常见的做法是使用动态内存分配和指针数组,而不是直接使用指向指针的指针。例如:

1
2
3
4
5
int **ptr;
ptr = (int **)malloc(rows * sizeof(int *));
for (int i = 0; i < rows; i++) {
ptr[i] = (int *)malloc(cols * sizeof(int));
}

这样,ptr 就是一个指针数组,每个元素都是指向一个 int 类型的一维数组的指针,这个数组的大小可以根据需要动态分配。

总之,定义一个指向二维数组的指针需要考虑数组的维度和元素类型,以及是否具有固定列数或不定列数。

指针和字符串的问题

You

char *str = “hello”;到底做了什么

ChatGPT

当你在C语言中写下这行代码:

1
char *str = "hello";

你实际上做了几件事情:

  1. 分配了字符串字面量:“hello” 字符串字面量被存储在程序的只读数据段(或类似的内存区域)中。这个字面值包括字符 ‘h’, ‘e’, ‘l’, ‘l’, ‘o’, 和一个末尾的空字符 ‘\0’,以标识字符串的结束。
  2. 声明了一个字符指针char *str 声明了一个指向字符的指针变量 str。这意味着 str 可以存储一个字符的内存地址。
  3. 初始化指针:通过 = "hello",你将 str 指针初始化为指向字符串字面量 “hello” 的首个字符 ‘h’ 的地址。换句话说,str 现在包含了字符串字面量 “hello” 存储位置的起始地址。

综上所述,这行代码声明了一个指向字符的指针 str,并使它指向包含 “hello” 文本的内存区域的开始。需要注意的是,这个字符串是存储在程序的只读段中的,这意味着通过 str 指针试图修改字符串中的任何字符,如 str[0] = 'x';,可能会导致未定义行为,如程序崩溃,因为只读内存段不允许写入操作。


You:

为什么不能改啊,char str[] = “hello”;可以吗

ChatGPT:

在C语言中,处理字符串字面量时存在两种不同的情况,这会影响你能否修改字符串中的字符。这两种情况对应于你提出的两种声明方式:

  1. 使用字符指针指向字符串字面量:

    1
    char *str = "hello";

    在这种情况下,str 是一个指针,指向存储字符串 “hello” 字面量的内存位置。这个位置通常位于程序的只读数据段。因为它是只读的,所以尝试修改该字符串(例如 str[0] = 'x';)会导致未定义行为,如运行时错误或程序崩溃。这种保护是为了防止程序意外或恶意地修改文字字符串,可能影响程序的正确执行或安全性。

  2. 使用字符数组来初始化字符串字面量:

    1
    char str[] = "hello";

    在这种情况下,str 实际上是一个字符数组,它的大小自动被初始化为字符串字面量的长度加上一个终止字符 ‘\0’。这里的关键区别是,字符串 “hello” 的内容被复制到了栈上 str 数组的新分配的空间中。因为这个数组位于栈上,你可以自由地修改数组中的字符(例如 str[0] = 'x';),而不会导致未定义行为。

总结来说,char str[] = "hello"; 声明了一个字符数组,并使用字符串 “hello” 初始化了这个数组。与指向字符串字面量的指针不同,这种方法允许你修改数组中的字符,因为这些字符存储在数组自己的内存空间中,这个空间不是只读的。


You

1
2
3
4
5
6
7
8
#include <stdio.h>

int main() {
char *str = "hello";
str[0] = 'w';
printf("%s", str);
return 0;
}

可是我这么写,没有报错,就是啥也不输出了

ChatGPT

如果你尝试运行你提供的代码,并且观察到它没有报错但什么也没有输出,这是一个典型的未定义行为的示例。在C语言中,对字符串字面量进行修改是未定义行为(Undefined Behavior, UB),意味着编译器不保证程序的行为,包括但不限于:

  • 程序崩溃
  • 程序正常运行但不输出任何内容
  • 程序输出意外的结果
  • 程序的行为可能在不同的运行环境、操作系统、编译器版本等因素下有所不同

在你的例子中,由于尝试修改了一个指向只读内存段的字符串字面量,运行时环境可能采取了某种形式的错误处理,这导致了程序没有按预期输出。某些编译器或运行时环境可能会更宽容,不会立即导致程序崩溃,但这并不意味着代码是安全或正确的。正确的做法是避免修改指向字符串字面量的指针所指内容。

如果你需要修改字符串,应该使用字符数组的形式:

1
2
3
4
5
6
7
8
#include <stdio.h>

int main() {
char str[] = "hello"; // str现在是一个数组,可以被修改
str[0] = 'w'; // 修改数组的第一个元素
printf("%s", str); // 正常输出 wello
return 0;
}

这段代码创建了一个字符数组str,并使用字符串字面量"hello"进行初始化。由于它是数组的一个副本,你可以自由地修改str中的字符,而不会引起未定义行为。

函数指针

函数指针在C语言中是一个非常强大的特性,它们允许程序在运行时调用不同的函数。这提供了程序设计上的灵活性和动态性,使得代码能够根据不同的情况选择不同的操作。函数指针的声明看起来可能有些复杂,但其核心思想是相对直接的。以你给出的例子为说明:

1
int (*p)();

这行代码声明了一个名为 p 的变量,它是指向函数的指针,而且这个函数返回一个 int 类型的值,并且没有指定参数类型(在标准C中,这意味着它可以接受任意数量和类型的参数,但这种用法现在不推荐,应该明确指出参数类型)。

函数指针的用途包括但不限于:

  1. 回调函数

回调函数是由用户定义的,可以在库函数或其他函数中被调用。通过函数指针,你可以将自定义的函数作为参数传递给其他函数。这在事件驱动编程或异步编程中特别有用,例如,排序函数允许用户指定比较函数,或者GUI库允许用户指定事件处理函数。

  1. 动态链接

函数指针可以用于动态链接到其他程序或库中的函数。这意味着程序可以在运行时决定调用哪个函数,而不是在编译时静态绑定。这在实现插件架构或模块化设计时非常有用。

  1. 接口和抽象化

通过函数指针数组或结构体包含的函数指针,可以创建一组操作的抽象接口。这允许编程时可以针对接口编程,而不是具体的实现,从而增加了代码的复用性和模块性。

  1. 跳转表

在需要根据输入或状态执行不同操作的情况下,函数指针数组可以作为跳转表使用。这使得代码更加紧凑和易于维护,特别是在实现有限状态机或解释器时。

  1. 封装

函数指针可以用来封装函数,为其提供额外的上下文或数据。通过绑定函数指针和特定的数据结构,可以创建更加灵活和强大的抽象。

使用示例

假设我们有两个函数,都返回 int 类型,并接受两个 int 类型的参数:

1
2
3
4
5
6
7
int add(int a, int b) {
return a + b;
}

int subtract(int a, int b) {
return a - b;
}

你可以使用函数指针来在运行时决定是调用 add 还是 subtract

1
2
3
4
5
int (*operation)(int, int) = NULL;
operation = add; // 现在 operation 指向 add 函数
int result = operation(5, 3); // 调用 add(5, 3)
operation = subtract; // 现在 operation 指向 subtract 函数
result = operation(5, 3); // 调用 subtract(5, 3)

函数指针的这些用途展示了C语言的灵活性和强大功能,使得开发者能够编写更加动态和模块化的代码。

空类型指针

C语言中的空类型指针(void pointer)是一种特殊的指针类型,它可以指向任何类型的数据。空类型指针通常被称为“泛型指针”。在C语言中,void 关键字用于表示没有具体类型的情况,因此,一个 void 类型的指针就是一个没有指定所指向的数据类型的指针。

空类型指针的特点

  • 泛型性:因为它们没有指定数据类型,所以 void 指针可以被用来指向任何类型的数据。这使得 void 指针在处理不同数据类型时非常灵活。

  • 不可直接解引用:由于 void 指针不知道它指向的数据的类型和大小,所以不能直接对它进行解引用操作。如果需要解引用,必须先将 void 指针转换成具体的数据类型指针。

  • 不可进行算术运算:与其他类型的指针不同,void 指针不能直接参与指针算术运算。如果需要进行算术运算,同样需要先转换成具体类型的指针。

使用空类型指针

  1. 声明和初始化

    1
    2
    3
    void *ptr;
    int x = 10;
    ptr = &x; // 将 ptr 指向 x
  2. 类型转换和解引用
    要使用 void 指针所指向的数据,您需要将其转换为适当的类型的指针:

    1
    2
    int *intPtr = (int *)ptr; // 将 void 指针转换为 int 指针
    printf("%d\n", *intPtr); // 现在可以解引用 intPtr 来获取 x 的值
  3. 作为函数参数
    void 指针在函数参数中非常有用,它允许函数接受不同类型的指针作为参数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    void printValue(void *ptr, char type) {
    switch (type) {
    case 'i':
    printf("%d\n", *(int *)ptr);
    break;
    case 'f':
    printf("%f\n", *(float *)ptr);
    break;
    // 其他类型的处理
    }
    }
  4. 作为函数返回类型
    函数也可以返回 void 类型的指针,以提供对不同类型数据的泛型接口。

空类型指针在C语言中提供了一种灵活的方式来处理不同类型的数据,特别是在需要编写泛型数据结构和函数时。然而,使用它们时需要小心,确保在解引用之前正确地将它们转换为适当的类型。

概念:引用、解引用

在C语言中,引用和解引用是与指针操作紧密相关的两个概念。它们是理解和使用指针的基础。

引用(Address-of Operator)

引用,在C语言中,通常指的是使用地址操作符 & 获取某个变量的内存地址。当你对一个变量进行引用操作时,你得到的是该变量在内存中的地址,而不是它的值。这个地址可以赋值给指针变量。

举例来说:

1
2
int x = 10;
int *ptr = &x;

在这个例子中,&x 表示引用变量 x,即获取 x 的内存地址。然后,这个地址被赋值给指针 ptr。现在,ptr 指向 x 的位置。

解引用(Dereference Operator)

解引用是指通过指针访问或操作它指向的内存地址中存储的值的过程。在C语言中,解引用操作符是 *。当你对一个指针进行解引用操作时,你得到的是该指针所指向的内存地址中存储的值。

继续上面的例子:

1
int value = *ptr;

这里,*ptr 表示解引用指针 ptr,即访问 ptr 指向的地址中存储的值(在这个例子中,是变量 x 的值 10)。然后,这个值被赋值给变量 value

总结

  • 引用(&:获取变量的内存地址。
  • 解引用(*:通过指针访问或操作其指向的内存地址中的数据。

理解引用和解引用对于有效地使用指针来说是非常重要的。它们允许程序直接访问和操作内存,提供了强大的功能,但同时也需要谨慎使用以避免错误和内存问题。

C语言引用和C++引用

C++中的“函数引用”与C语言中讨论的“引用(&)”和“解引用(*)”操作有着本质的不同。C++的引用是对C语言中指针概念的一种扩展和封装,旨在提供一种更安全、更方便的方式来操作变量的别名,而不需要像操作指针那样直接处理内存地址。以下是C++引用和C语言中指针操作的主要区别:

C++引用

  1. 别名:C++中的引用就是变量的别名。创建引用时,它就成了被引用变量的另一个名字。
  2. 初始化:引用在创建时必须被初始化,并且一旦被初始化指向一个变量,就不能改变指向其他变量。引用的这个特性保证了引用的稳定性和安全性。
  3. 使用简便:使用引用就像使用普通变量一样,不需要特殊的操作符来访问引用的值。
  4. 类型安全:C++引用更加类型安全,编译器会进行类型检查,减少了出错的可能。
1
2
3
int x = 10;
int& ref = x; // ref是x的引用
ref = 20; // 通过ref修改x的值

C语言中的指针操作

  1. 内存地址:指针本质上是存储另一个变量的内存地址的变量。通过指针,可以间接访问和操作该地址处的数据。
  2. 灵活性:指针可以在任何时候改变指向,指向另一个变量或者动态分配的内存。
  3. 操作复杂:使用指针访问变量的值需要使用解引用操作符(*),而且需要正确管理内存(如分配和释放动态内存)。
  4. 潜在危险:如果不正确使用,指针可能导致程序错误,如空指针解引用、野指针等。
1
2
3
int x = 10;
int *ptr = &x; // ptr是指向x的指针
*ptr = 20; // 通过解引用ptr修改x的值

结论

C++中的引用相比于C语言的指针操作,提供了一种更为安全、简洁的方式来引用另一个变量。它封装了指针的一些复杂性,使得代码更易读、易写、易维护。然而,指针因其提供的灵活性和直接的内存操作能力,在很多场景下仍然是必不可少的。


是的,您的理解是正确的。在C++中,当我们使用引用来为一个变量取另一个别名时,从程序员的角度看,它好像是两个变量名。但是,从编译器和内存的角度来看,这两个名字实际上指向的是同一个变量的同一个内存地址。引用不会像变量那样分配新的内存空间,而是创建了一个新的符号名称,它与原始变量共享同一块内存空间。

例子

考虑以下C++代码:

1
2
int original = 10;
int& alias = original;

在这里,original 是一个整型变量,aliasoriginal 的引用(别名)。对 alias 进行的任何操作实际上都在直接影响 original,因为它们指向相同的内存地址。无论你是通过 original 还是 alias 读取或修改变量的值,效果都是相同的。

编译后的视角

  • 编译时:编译器知道 aliasoriginal 的引用,并将对 alias 的所有引用都转换为对 original 的直接操作。
  • 运行时:在程序执行时,不管是通过 alias 还是 original 访问或修改变量,实际上都是在同一个内存地址上进行操作。因此,从机器的角度看,aliasoriginal 是同一个变量的两个不同名字。

这种行为让引用在某些场合下非常有用,比如函数参数传递时,可以避免复制大型对象的开销,同时保持对原始数据的直接操作能力,提高效率和减少内存使用。此外,使用引用还可以增加代码的清晰度和易于理解,因为它提供了另一种形式的直接操作变量的方式,而不是通过指针间接操作。





运算符优先级





函数

  • 传参

一维数组

void fun(int arr[])等价于void fun(int *arr)

二维数组

  1. 使用数组的指针表示法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>

void printArray1(int (*arr)[4], int rows) {
for (int i = 0; i < rows; i++) {
for (int j = 0; j < 4; j++) { // 列数为 4
printf("%d ", arr[i][j]);
}
printf("\n");
}
}

int main() {
int a[3][4] = { {1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12} };
printArray1(a, 3);
return 0;
}

说明:在 printArray1 函数中,参数 arr 是一个指向具有4列的 int 类型的二维数组的指针。我们使用嵌套循环来遍历数组,并打印其内容。在 main 函数中,我们创建了一个 3x4 的二维数组 a,然后调用 printArray1 函数打印其内容。

  1. 使用指针的指针表示法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <stdio.h>
#include <stdlib.h>

void printArray2(int **arr, int rows, int cols) {
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) { // 列数为参数 cols
printf("%d ", arr[i][j]);
}
printf("\n");
}
}

int main() {
int **a;
int rows = 3;
int cols = 4;

// 分配内存并初始化二维数组
a = (int **)malloc(rows * sizeof(int *));
for (int i = 0; i < rows; i++) {
a[i] = (int *)malloc(cols * sizeof(int));
}

// 初始化数组内容
int count = 1;
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
a[i][j] = count++;
}
}

printArray2(a, rows, cols);

// 释放内存
for (int i = 0; i < rows; i++) {
free(a[i]);
}
free(a);

return 0;
}

说明:在 printArray2 函数中,参数 arr 是一个 int ** 类型的指针,表示指向指针的指针。我们使用嵌套循环遍历数组,并打印其内容。在 main 函数中,我们首先动态分配了一个指向指针的指针,并为每个指针分配了一个 int 类型的数组。然后我们使用嵌套循环初始化了数组内容,并调用 printArray2 函数打印其内容。最后,我们释放了动态分配的内存。

解释

问:

第二种方法,创建数组后,为什么又能当二维数组来打印,还是说他的每一列地址并没有连续,并不是二维数组了,而arr[][]这种写法,只是单纯的当做变址运算符来使用

ChatGPT:

你的理解很正确。第二种方法中,我们使用了指针的指针来表示二维数组,但实际上它并不是一个真正的二维数组。在这种情况下,我们使用了动态内存分配来模拟二维数组,但是它实际上是一组指针,每个指针指向一个一维数组,这些一维数组的内存空间不一定是连续的。

当我们使用指针的指针表示法时,我们通常将数组的每一行看作一个独立的一维数组,而每个指针指向这个一维数组的开头。这种方式不会像真正的二维数组那样在内存中连续存储所有元素,因此在使用指针的指针表示法时,我们需要动态分配内存以确保每一行都被正确地存储。

虽然指针的指针表示法可以模拟二维数组的行为,但是它们的内存布局并不同,所以不能像使用真正的二维数组那样直接使用索引访问元素。在这种情况下,我们必须通过使用指针来对数组进行操作。

所以,即使我们使用了指针的指针来表示二维数组,并且可以通过类似 arr[i][j] 的方式来访问元素,但是它实际上并不是一个真正的二维数组,而是一组独立的一维数组。

  1. 使用一维数组表示法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>

void printArray3(int *arr, int rows, int cols) {
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) { // 列数为参数 cols
printf("%d ", *(arr + i * cols + j));
}
printf("\n");
}
}

int main() {
int a[3][4] = { {1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12} };
printArray3(&a[0][0], 3, 4);
return 0;
}

说明:在 printArray3 函数中,参数 arr 是一个 int * 类型的指针,表示指向一维数组的指针。我们使用一维数组的表示方法来访问二维数组的元素,并打印其内容。在 main 函数中,我们传递了数组的首地址以及数组的行列数给 printArray3 函数,来打印二维数组的内容。

以上是三种不同的方法,你可以根据实际情况选择合适的方法来处理二维数组。





宏定义

宏定义又称为宏替换、宏代换,简称“宏”,是C提供的三种预处理功能的其中一种。其主要目的是为程序员在编程时提供一定的方便,并能在一定程度上提高程序的运行效率。

1. #define命令

\#define命令是C语言中的一个宏定义命令,它用来讲一个标识符定义为一个字符串,该标识符被称为宏名,被定义的字符串称为替换文本。该命令有两种格式:一种是简单的宏定义(不带参数的宏定义),另一种是带参数的宏定义


(1) 简单的宏定义

格式:#define <宏名/标识符> <字符串>

eg: #define PI 3.1415926

说明:

① 宏名一般用大写

② 宏定义末尾不加分好;

③ 可以用#undef命令终止宏定义的作用域

解释

在C语言中,#undef预处理指令用于取消已定义的宏。如果在代码的某个部分之后你不想再使用某个宏,可以使用#undef来取消它。

应用示例

1
2
3
4
5
#define MAX 100
int array[MAX]; // 使用MAX宏
#undef MAX // 取消MAX宏的定义

// 此处不能使用MAX,因为它已经被#undef取消了

④ 宏定义可以嵌套

解释
1
2
3
4
5
6
#define SUM(x, y) ((x) + (y))
#define SQUARE(x) ((x) * (x))
#define SUM_SQUARES(x, y) (SQUARE(SUM(x, y))) // 嵌套使用宏

// 使用嵌套宏
int result = SUM_SQUARES(3, 4); // 计算(3+4)^2

⑤ 字符串“”中永远不包含宏

解释

在C语言的预处理中,字符串字面量中的内容不会被视为预处理指令的一部分,即使其中包含了宏名。这意味着在字符串中提到的宏不会被展开。

应用示例

1
2
#define VERSION 1.0
printf("Version: VERSION\n"); // 打印的是 "Version: VERSION\n" 而不是 "Version: 1.0\n"

如果你想在字符串中包含宏的值,可以使用宏来生成整个字符串,或者使用其他技巧,如下所示:

1
2
3
#define TO_STRING(x) #x // 宏转换数字到字符串
#define VERSION 1.0
printf("Version: " TO_STRING(VERSION) "\n"); // 使用宏生成字符串

或者更常用的是,通过使用printf的格式化功能来插入宏的值:

1
2
#define VERSION 1.0
printf("Version: %f\n", VERSION); // 使用格式化输出宏的值

这些特性和技巧展示了C预处理器的灵活性,允许开发者以多种方式操作和使用宏来简化编程任务。

⑥ 宏替换在编译前进行,不分配内存,变量定义分配内存,函数调用在编译后程序运行时进行,并且分配内存

⑦ 预处理是在编译之前的处理,而编译工作的任务之一就是语法检查,预处理不做语法检查

⑧ 使用宏可提高程序的通用性和易读性,减少不一致性,减少输入错误和便于修改。例如:数组大小常用宏定义


(2) 带参数的宏定义(除了一般的字符串替换,还要做参数代换)

格式:#define <宏名>(<参数表>) <字符串>

eg:

1
2
#define S(a,b) a*b
area=S(3,2);

第一步被换为area=a*b;第二步换为area=3*2;

一个标识符被宏定义后,该标识符便是一个宏名。这时,在程序中出现的是宏名,在该程序被编译前,先将宏名用被定义的字符串替换,这称为宏替换,替换后才进行编译,宏替换是简单的替换。

说明:

①实参如果是表达式容易出问题

1
2
\#define S(r) r*r
area=S(a+b);

第一步换为area=r*r;第二步换成area=a+b*a+b;

当定义为#define S® (®*®)时,area=((a+b)*(a+b))

②宏名和参数的括号间不能有空格

③宏替换只做替换不做计算,不做表达式求解

④宏的哑实结合不存在类型,也没有类型转换

⑤宏展开不占用运行时间,只占用编译时间,函数调用占运行时间(分配内存、保留现场、值传递、返回值)


2. 宏定义易错点示例总结

#define 第一位置 第二位置

(1)“”内的东西不会被宏替换

#define NAME zhang

程序中有"NAME"则,它会不会被替换呢?

答:否

(2)宏定义前面的那个必须是合法的用户标识符(可以使关键字)

#define 0x abcd

可以吗?也就是说,可不可以用把标识符的字母替换成别的东西?

答:否

(3)第二位置如果有字符串,必须“”配对

#define NAME "zhang

这个可以吗?

答:否

(4)只替换与第一位置完全相同的标识符

#define NAME “zhangyuncong”

程序中有上面的宏定义,并且,程序里有句:NAMELIST这样,会 不会被替换成"zhangyuncong"LIST

答:否

(5)带参数宏的一般用法

例如:

①#define MAX(a,b) ((a)>(b)?(a):(b))

则遇到MAX(1+2,value)则会把它替换成:

((1+2)>(value)?(1+2):(value))

②#define FUN(a) “a”

则,输入FUN(345)会被替换成什么?

其实,如果这么写,无论宏的实参是什么,都不会影响其被替换成 “a”。也就是说,""内的字符不被当成形参,即使它和一模一样。

扩展: 字符串化操作符`#`

在C语言的预处理器中,字符串化操作符#是一种特殊的操作符,用于将宏参数转换成字符串字面量。当它出现在宏定义中,紧跟在它后面的宏参数会被转换为一个由双引号包围的字符串。这个过程称为字符串化。

字符串化的工作方式如下:

1
#define TO_STRING(x) #x

在这个宏定义中,TO_STRING是一个宏,接受一个参数x。使用#操作符后,x会被转换成一个字符串。如果你使用这个宏如下:

1
TO_STRING(1.2)

预处理器会将其转换为:

1
"1.2"

这里,1.2不再是一个数值,而是变成了一个字符串字面量。

这个特性在需要将宏参数直接转换为字符串的场合非常有用,例如,在生成错误消息、打印日志、或者创建格式化的文本时。字符串化操作符#使得预处理器在编译之前就完成了这种转换,从而避免了在运行时进行字符串操作,这可以提高程序的效率。

③#define N 2+2

void main()

{

int a=N*N;

printf(“%d”,a);

}

出现问题:在此程序中存在着宏定义命令,宏N代表的字符串是2+2,在程序中有对宏N的使用,一般同学在读该程序时,容易产生的问题是先求解N为 2+2=4,然后在程序中计算a时使用乘法,即N*N=4*4=16,其实该题的结果为8,为什么结果有这么大的偏差?

问题解析:如1节所述,宏展开是在预处理阶段完成的,这个阶段把替换文本只是看作一个字符串,并不会有任何的计算发生,在展开时是在宏N出现的地方 只是简单地使用串2+2来代替N,并不会增添任何的符号,所以对该程序展开后的结果是a=2+2*2+2,计算后=8,这就是宏替换的实质,如何写程序才 能完成结果为16的运算呢?

解决办法:将宏定义写成如下形式

#define N (2+2)

这样就可替换成(2+2)*(2+2)=16

④#define area(x) x*x

void main()

{

int y=area(2+2);

printf(“%d”,y);

}

按理说给的参数是2+2,所得的结果应该为4*4=16,但是错了,因为该程序的实际结果为8,仍然是没能遵循纯粹的简单替换的规则,又是先计算再替换 了,在这道程序里,2+2即为area宏中的参数,应该由它来替换宏定义中的x,即替换成2+2*2+2=8了。那如果遵循(1)中的解决办法,把2+2 括起来,即把宏体中的x括起来,是否可以呢?#define area(x) (x)*(x),对于area(2+2),替换为(2+2)*(2+2)=16,可以解决,但是对于area(2+2)/area(2+2)又会怎么样呢,有的学生一看到这道题马上给出结果,因为分子分母一样,又错了,还是忘了遵循先替换再计算的规则了,这道题替换后会变为(2+2)*(2+2)/(2+2)*(2+2)即4*4/4*4按照乘除运算规则,结果为16/4*4=4*4=16,那应该怎么呢?解决方法是在整个 宏体上再加一个括号,即#definearea(x) ((x)*(x)),不要觉得这没必要,没有它,是不行的。

要想能够真正使用好宏定义,那么在读别人的程序时,一定要记住先将程序中对宏的使用全部替换成它所代表的字符串,不要自作主张地添加任何其他符号,完全展开后再进行相应的计算,就不会写错运行结果。如果是自己编程使用宏替换,则在使用简单宏定义时,当字符串中不只一个符号时,加上括号表现出优先级,如果是 带参数的宏定义,则要给宏体中的每个参数加上括号,并在整个宏体上再加一个括号。

⑤多行宏定义

#define doit (m,n) for(inti=0;i<(n);++i) { m+=i; }

3. 宏定义其他冷门、重点知识

#define Conn(x,y) x##y

#define ToChar(x) #@x

#define ToString(x) #x

x##y表示什么?表示x连接y,举例说:

int n = Conn(123,456); 结果就是n=123456;

char* str = Conn(“asdf”,“adf”)结果就是 str = “asdfadf”;

#@x,其实就是给x加上单引号,结果返回是一个constchar。

举例说:

char a = ToChar(1);结果就是a=‘1’;

做个越界试验char a = ToChar(123);结果是a=‘3’;

但是如果你的参数超过四个字符,编译器就给给你报错了!error C2015:too many characters in constant :P

#x是给x加双引号

char* str = ToString(123132);就成了str=“123132”;

如果有#define FUN(a,b) vo##a##b()那么FUN(idma,in)会被替换成void main()

附录:

① 预处理功能:

(1)文件包含:可以把源程序中的#define扩展为文件正文,即把包含的.h文件找到并展开到#include所在处。

(2)条件编译:预处理器根据#if和#ifdef等编译命令及其后的条件,把源程序中的某些部分包含进来或排除在外,通常把排除在外的语句转换成空行。

(3)宏展开:预处理器将源程序文件中出现的对宏的引用展开成相应的宏定义,经过预处理器处理的源程序与之前的源程序有所不同,在这个阶段所进行的工作只是纯粹的替换和展开,没有任何计算功能。

②使用带参数的宏定义可完成函数调用的功能,又能减少系统开销,提高运行效率。

正如C语言中所讲,函数的使用可以使程序更加模块化,便于组织,而且可重复利用,但在发生函数调用时,需要保留调用函数的现场,以便子函数执行结束后能返回继续执行,同样在子函数执行完后要恢复调用函数的现场,这都需要一定的时间,如果子函数执行的操作比较多,这种转换时间开销可以忽略,但如果子函数完成的功能比较少,甚至只完成一点操作,如一个乘法语句的操作,则这部分转换开销就相对较大了,但使用带参数的宏定义就不会出现这个问题,因为它是在预处理阶段即进行了宏展开,在执行时不需要转换,即在当地执行。宏定义可完成简单的操作,但复杂的操作还是要由函数调用来完成,而且宏定义所占用的目标代码空间相对较大。所以在使用时要依据具体情况来决定是否使用宏定义。





结构体&共用体&枚举

  • 共用体

在C语言中,共用体(Union)是一种特殊的数据类型,允许你在相同的内存位置存储不同的数据类型(即,不同的成员可以占用同一块内存空间)。共用体的一个重要特性是,任何时候它只能存储其中一个成员的值;如果对一个成员赋值,它会覆盖其他成员的值。

共用体的定义使用关键字union。共用体用途之一是当一个变量可能需要存储多种类型的值时,可以使用共用体来节省内存。但是,需要谨慎使用,因为共用体的不同成员共享内存,所以对一个成员的修改会影响到其他成员。

定义共用体

共用体的定义语法如下:

1
2
3
4
5
6
union UnionName {
Type1 member1;
Type2 member2;
Type3 member3;
...
};

示例

下面是一个使用共用体的例子,定义了一个共用体data,它可以用来存储整数、浮点数和字符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <stdio.h>

union data {
int i;
float f;
char str[20];
};

int main() {
union data x;

x.i = 10;
printf("x.i = %d\n", x.i);

x.f = 220.5;
printf("x.f = %.2f\n", x.f);

// 注意:此时x.i的值已经被覆盖,不能再预期其为10

strcpy(x.str, "C Programming");
printf("x.str = %s\n", x.str);

// 注意:此时x.f的值也被覆盖,因为x.i, x.f, x.str共享相同的内存空间

return 0;
}

在这个例子中,x是一个union data类型的变量,可以用来存储一个整数、一个浮点数或一个字符串。由于所有成员共享相同的内存空间,所以给x.f赋值会覆盖x.i的值,给x.str赋值又会覆盖x.f的值。

共用体的大小

共用体的大小至少等于其最大成员的大小。编译器可能会根据成员的对齐要求在共用体的末尾添加填充字节,所以共用体的实际大小可能大于其最大成员的大小。

使用场景

共用体通常用于数据结构中需要按需存储不同类型数据的场景,例如,解析不同类型的数据包,或者在有限的内存空间中存储可能采用多种形式表示的数据。然而,使用共用体时需要格外小心,因为错误的读写操作容易导致数据损坏。


  • 结构体

  • 枚举

C语言中的枚举(Enumeration)是一种用户定义的数据类型,它允许程序员为整数类型的值指定更有意义的名字。枚举通过关键字enum来定义,它提供了一种将一组整数值与一组对应的标识符(名称)相关联的方式。

定义枚举

枚举的定义使用enum关键字,后跟枚举类型的名称(可选),再后跟枚举体,枚举体内部是一组以逗号分隔的枚举器(Enumerator)。枚举器是为整数常量指定的标识符。

1
enum season {SPRING, SUMMER, AUTUMN, WINTER};

在这个例子中,season是一个枚举类型,包含四个枚举器:SPRINGSUMMERAUTUMNWINTER。默认情况下,第一个枚举器的值是0,每个后续的枚举器的值依次增加1。因此,在这个例子中,SPRING的值是0SUMMER的值是1,依此类推。

枚举的性质

  1. 类型安全:枚举增加了程序的类型安全。使用枚举可以使得代码更加清晰易读,且更容易维护。

  2. 整数值:尽管枚举提供了一种为整数值指定标识符的方法,但枚举的每个成员在内部仍然是整数类型,并且可以像整数一样进行操作。

  3. 自定义值:可以为枚举中的某个枚举器指定一个特定的整数值。未显式指定值的枚举器将继续从上一个枚举器的值递增。

    1
    enum season {SPRING = 1, SUMMER, AUTUMN = 4, WINTER};

    在这个例子中,SPRING的值被显式设置为1SUMMER将自动设置为2AUTUMN显式设置为4,而WINTER则为5

  4. 类型定义:可以使用typedef关键字为枚举类型定义一个新的名字,这样可以更方便地声明该枚举类型的变量。

    1
    2
    typedef enum {RED, GREEN, BLUE} Color;
    Color myColor = RED;
  5. 与整数类型兼容:枚举类型的变量可以赋值为任何整数值,即使那个值没有在枚举的定义中列出。但这样做可能会降低代码的清晰度和安全性。

  6. 作为类型:枚举类型可以在定义函数参数、返回类型以及定义结构体成员时使用,提高代码的可读性和维护性。

枚举的使用提供了一种在程序中使用命名常量的强大工具,使得源代码更加简洁和易于理解。通过使用枚举,程序员可以创建更加模块化和可维护的代码。





位运算

>> <<

在C语言中,>>运算符是位右移运算符。它将其左操作数的位向右移动指定的位数。例如,如果你有一个整数值a和你想将它的位向右移动n位,你可以这样做:

1
result = a >> n;

这里,a的位向右移动n位,移出边界的位被丢弃,而新空出的位的填充方式取决于a的类型(有符号还是无符号类型)和编译器:

  • 对于无符号类型,右移通常会在左侧填充0。这意味着对于无符号类型的右移是逻辑右移。

  • 对于有符号类型(如int,在大多数情况下是有符号的),行为可能依赖于编译器和机器。许多编译器实现的是算术右移,即:

    • 如果a是正数,则在左侧填充0。
    • 如果a是负数,则在左侧填充1,保持负数的符号位不变。

这种区分主要是因为,算术右移保持了数的符号(正或负),而逻辑右移则不考虑符号,仅仅是纯粹地移位和填充。

示例:

考虑以下例子,假设int是32位宽:

1
2
3
4
5
unsigned int a = 4; // 二进制表示为 0000 0000 0000 0000 0000 0000 0000 0100
int b = a >> 1; // 结果是 2,二进制表示为 0000 0000 0000 0000 0000 0000 0000 0010

int c = -4; // 假设二进制表示为 1111 1111 1111 1111 1111 1111 1111 1100(补码形式)
int d = c >> 1; // 如果是算术右移,结果可能是 -2,二进制表示为 1111 1111 1111 1111 1111 1111 1111 1110

在这个例子中,无论是正数还是负数,右移都减小了数的绝对值(对于负数,考虑补码形式)。对于无符号数,总是使用逻辑右移;对于有符号数,使用的是算术右移或逻辑右移,具体取决于编译器和平台的实现细节。

  1. 问:取一个整数a从右端开始的4~7位(a的最低位为第0位)
查看解析

答:(a>>4)&~(~0<<4)

这个表达式是用来提取整数a从右端开始的第4位到第7位的值。让我们分步解析这个表达式的含义:

第一步: a >> 4

这一步将a的所有位向右移动4位。这意味着原来在第4位到第7位的内容现在移动到了最低的4位位置(即第0位到第3位)。

第二步: ~(~0<<4)

  • ~0:这部分首先将0的所有位取反,得到一个所有位都是1的数。在32位系统上,这将是0xFFFFFFFF,即-1。
  • (~0<<4):然后,这个数向左移动4位。移位后的结果是高位填充0,因此最低的4位变成0,其余位都是1。在32位系统上,这将是0xFFFFFFF0
  • ~(~0<<4):最后,对上一步的结果再次取反,这会使最低的4位变成1,其余位都是0。在32位系统上,这将是0x0000000F,即十进制的15,二进制表示为0000 0000 0000 0000 0000 0000 0000 1111

第三步: (a >> 4) & ~(~0<<4)

  • 这一步将a右移4位后的结果与~(~0<<4)做逻辑与操作(&)。逻辑与操作的结果是只有当两个操作数对应的位都为1时,结果的那一位才为1。

因此,这个表达式的效果是保留a右移4位后的最低4位(即原来的第4位到第7位),并将其他位都清零。

这是一种常见的位操作技巧,用于提取整数中的特定位段。通过调整移位和掩码的值,你可以提取整数中的任何位段。





文件操作

一个文件,无论它是文本文件还是二进制文件,都是代表了一系列的字节。C 语言不仅提供了访问顶层的函数,也提供了底层(OS)调用来处理存储设备上的文件。本章将讲解文件管理的重要调用。

重定位输入输出

1
2
if (freopen("sample1.txt", "r", stdin) == NULL) { printf("样例文件不存在\n"); return 1;}
if (freopen("output.txt", "w", stdout) == NULL) { printf("输出文件不存在\n"); return 1;}

  • 打开文件

您可以使用 fopen( ) 函数来创建一个新的文件或者打开一个已有的文件,这个调用会初始化类型 FILE 的一个对象,类型 FILE 包含了所有用来控制流的必要的信息。下面是这个函数调用的原型:

1
FILE *fopen( const char *filename, const char *mode );

在这里,filename 是字符串,用来命名文件,访问模式 mode 的值可以是下列值中的一个:

模式 描述
r 打开一个已有的文本文件,允许读取文件。
w 打开一个文本文件,允许写入文件。如果文件不存在,则会创建一个新文件。在这里,您的程序会从文件的开头写入内容。如果文件存在,则该会被截断为零长度,重新写入。
a 打开一个文本文件,以追加模式写入文件。如果文件不存在,则会创建一个新文件。在这里,您的程序会在已有的文件内容中追加内容。
r+ 打开一个文本文件,允许读写文件。
w+ 打开一个文本文件,允许读写文件。如果文件已存在,则文件会被截断为零长度,如果文件不存在,则会创建一个新文件。
a+ 打开一个文本文件,允许读写文件。如果文件不存在,则会创建一个新文件。读取会从文件的开头开始,写入则只能是追加模式。

如果处理的是二进制文件,则需使用下面的访问模式来取代上面的访问模式:

1
"rb", "wb", "ab", "rb+", "r+b", "wb+", "w+b", "ab+", "a+b"

  • 关闭文件

为了关闭文件,请使用 fclose( ) 函数。函数的原型如下:

1
int fclose( FILE *fp );

如果成功关闭文件,fclose( ) 函数返回零,如果关闭文件时发生错误,函数返回 EOF。这个函数实际上,会清空缓冲区中的数据,关闭文件,并释放用于该文件的所有内存。EOF 是一个定义在头文件 stdio.h 中的常量。

C 标准库提供了各种函数来按字符或者以固定长度字符串的形式读写文件。


  • 写入文件

下面是把字符写入到流中的最简单的函数:

1
int fputc( int c, FILE *fp );

函数 fputc() 把参数 c 的字符值写入到 fp 所指向的输出流中。如果写入成功,它会返回写入的字符,如果发生错误,则会返回 EOF。您可以使用下面的函数来把一个以 null 结尾的字符串写入到流中:

1
int fputs( const char *s, FILE *fp );

函数 fputs() 把字符串 s 写入到 fp 所指向的输出流中。如果写入成功,它会返回一个非负值,如果发生错误,则会返回 EOF。您也可以使用 int fprintf(FILE *fp,const char *format, …) 函数把一个字符串写入到文件中。尝试下面的实例:

实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>

int main()
{
FILE *fp = NULL;

fp = fopen("./tmp/test.txt", "w+");
if (!fp)
printf("文件打开或创建失败");
fprintf(fp, "This is testing for fprintf...\n");
fputs("This is testing for fputs...\n", fp);
fclose(fp);
}

当上面的代码被编译和执行时,它会在 ./tmp 目录中创建一个新的文件 test.txt,并使用两个不同的函数写入两行。接下来让我们来读取这个文件。

**注意:**请确保您有可用的 tmp 目录,如果不存在该目录,则需要在您的计算机上先创建该目录。

/tmp 一般是 Linux 系统上的临时目录,如果你在 Windows 系统上运行,则需要修改为本地环境中已存在的目录,例如: C:\tmpD:\tmp等。


  • 读取文件

下面是从文件读取单个字符的最简单的函数:

1
int fgetc( FILE * fp );

fgetc() 函数从 fp 所指向的输入文件中读取一个字符。返回值是读取的字符,如果发生错误则返回 EOF。下面的函数允许您从流中读取一个字符串:

1
char *fgets( char *buf, int n, FILE *fp );

函数 fgets() 从 fp 所指向的输入流中读取 n - 1 个字符。它会把读取的字符串复制到缓冲区 buf,并在最后追加一个 null 字符来终止字符串。

如果这个函数在读取最后一个字符之前就遇到一个换行符 ‘\n’ 或文件的末尾 EOF,则只会返回读取到的字符,包括换行符。您也可以使用 int fscanf(FILE *fp, const char *format, …) 函数来从文件中读取字符串,但是在遇到第一个空格和换行符时,它会停止读取。

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>

int main()
{
FILE *fp = NULL;
char buff[255];

fp = fopen("./tmp/test.txt", "r");
if (!fp)
printf("文件打开或创建失败");
fscanf(fp, "%s", buff);
printf("1: %s\n", buff );

fgets(buff, 255, (FILE*)fp);
printf("2: %s\n", buff );

fgets(buff, 255, (FILE*)fp);
printf("3: %s\n", buff );
fclose(fp);

}

当上面的代码被编译和执行时,它会读取上一部分创建的文件,产生下列结果:

1
2
3
4
1: This
2: is testing for fprintf...

3: This is testing for fputs...

首先,fscanf() 方法只读取了 This,因为它在后边遇到了一个空格。其次,调用 fgets() 读取剩余的部分,直到行尾。最后,调用 fgets() 完整地读取第二行。





位域

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
int main()
{
struct bits
{
unsigned b0 : 1;
unsigned b1 : 1;
unsigned b2 : 1;
unsigned b3 : 1;
unsigned b4 : 4;
} modern = {0, 1, 1, 0, 1};
printf("b0:%d,b1:%d,b2:%d,b3:%d,b4:%d\n", modern.b0, modern.b1, modern.b2, modern.b3, modern.b4);

}

这段代码演示了C语言中的一种高级特性:位域(Bit Fields)。位域允许你在结构体中为每个成员分配一个特定数量的位,而不是完整的字节,这在需要精确控制内存布局、尤其是对于硬件访问或协议实现时非常有用。

代码解析:

  • 定义位域结构体struct bits是一个结构体,其中的每个成员都被分配了特定的位宽。unsigned关键字表示成员是无符号的。

    • unsigned b0:1; 分配了1位,可以存储的值是0或1。
    • unsigned b1:1; 分配了1位,可以存储的值是0或1。
    • unsigned b2:1; 分配了1位,可以存储的值是0或1。
    • unsigned b3:1; 分配了1位,可以存储的值是0或1。
    • unsigned b4:4; 分配了4位,可以存储的值是0到15(2^4-1)。
  • 初始化位域结构体:在声明modern这个struct bits类型的变量时,使用初始化列表{0,1,1,0,1}来分别设置b0b4的值。这里b4被设置为1,剩余的b0b1b2b3根据列表中的值进行设置。

  • 打印位域的值:使用printf函数打印出modern的每个位域成员的值。

输出结果:

因此,这段代码的输出将会是:

1
b0:0,b1:1,b2:1,b3:0,b4:1

这反映了modern变量在初始化时分配给每个位域的值。

位域的性质:

位域在C语言中的使用有几个关键的性质和用途:

  • 内存效率:位域可以减少内存的使用,因为你可以为变量分配比一个完整字节还少的存储空间。
  • 精确控制:位域允许程序员精确地控制数据结构的布局,这在需要按位打包数据时非常有用。
  • 可读性:通过位域,代码可以更清晰地表达其意图,特别是在处理硬件寄存器或特定协议字段时。
  • 端依赖性:位域的布局(例如,哪个位是最高位)可能依赖于机器的字节序(大端或小端),这可能影响跨平台代码的可移植性。

使用位域时需要注意的是,尽管它们提供了对内存的紧凑控制,但也可能引入特定于平台的行为,特别是在位域跨越字节边界时。此外,访问位域的速度可能不如访问未分割的整数快,因为可能需要额外的位掩码和移位操作。





命令行参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{
// 检查是否提供了正确数量的命令行参数
if (argc != 2)
{
printf("Usage: %s <filename>\n", argv[0]);
return 1; // 返回非零表示程序异常结束
}
// 尝试打开文件进行写入
FILE *fp = fopen(argv[1], "w");
if (fp == NULL)
{
perror("Error opening file");
return 1; // 文件打开失败,返回非零
}

printf("Enter text to be written to the file. Press CTRL+D (Linux/macOS) or CTRL+Z (Windows) to end input:\n");

// 读取键盘输入,直到遇到文件结束符(EOF)
int ch;
while ((ch = getchar()) != EOF)
{
fputc(ch, fp); // 写入单个字符到文件
}

// 关闭文件
fclose(fp);
printf("File saved successfully.\n");

return 0; // 程序成功结束
}

当你在命令行运行一个程序并传递参数时,如在Windows 10中使用.\save_to_file.exe output.txt命令,操作系统会将这些参数传递给程序的main函数。在这个上下文中,argcargv[]main函数用来接收命令行参数的两个参数:

  • argc(Argument Count): 代表命令行参数的数量。在这个例子中,argc的值是2。argc至少为1,因为程序的名称也算作一个参数。

  • argv[](Argument Vector): 是一个字符串数组,包含了所有的命令行参数。argv[0]总是程序的名称(这个例子中是.\save_to_file.exe),argv[1]是传递给程序的第一个参数(这个例子中是output.txt)。argv[argc]是一个空指针(NULL),标记数组的结束。

参数详解:

  • argv[0]: 程序名称。在这个例子中,它是.\save_to_file.exe。这表示程序是在当前目录下执行的,.代表当前目录,\是路径分隔符。

  • argv[1]: 第一个用户提供的命令行参数。在这个例子中,它是output.txt,告诉程序要将输出保存到名为output.txt的文件中。

使用这些参数的代码片段:

考虑到上述参数,以下是main函数中使用这些参数的相关代码片段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>

int main(int argc, char *argv[]) {
// 检查是否提供了正确数量的命令行参数
if (argc != 2) {
printf("Usage: %s <filename>\n", argv[0]);
return 1; // 返回非零表示程序异常结束
}

FILE *fp = fopen(argv[1], "w");
if (fp == NULL) {
printf("Can't open %s\n", argv[1]);
return 1; // 文件打开失败,返回非零
}

// 文件打开成功,接下来的代码用于处理文件写入...
}

在这段代码中:

  • 首先检查argc是否等于2,确保用户提供了恰当数量的参数(程序名称加一个额外的参数)。
  • 使用argv[1]获取文件名,尝试以写模式打开这个文件。如果打开失败,会输出错误信息并以状态码1退出程序。

通过argcargv[],程序能够灵活地处理来自外部的输入,使得程序的用途和功能更加广泛和动态。





编译相关

从C语言源代码到最终的可执行程序,整个过程经历了几个关键步骤。这个转换过程通常由编译器(如GCC)和其他工具链组件自动完成。下面是这一过程的简化概述:

1. 预处理(Preprocessing)

  • 目的:处理源代码文件中的预处理指令,如#include(插入头文件),#define(定义宏),以及#if#ifdef#ifndef等条件编译指令。
  • 结果:生成一个扩展的源代码文件,其中包含了所有通过预处理指令包含或定义的内容,不再含有任何预处理指令。

2. 编译(Compilation)

  • 目的:将预处理后的源代码转换成中间表示或汇编语言。
  • 结果:生成汇编语言文件。这一步涉及源代码的语法和语义分析,错误检查,以及优化代码。

3. 汇编(Assembly)

  • 目的:将汇编语言代码转换为机器语言指令,即生成目标代码。
  • 结果:生成目标文件(通常是.o.obj文件)。汇编器将汇编语言指令转换成机器码。

4. 链接(Linking)

  • 目的:将一个或多个目标文件与库文件一起合并,解决符号引用/定义之间的依赖,生成最终的可执行文件。
  • 结果:生成可执行程序(在Windows上是.exe文件,在Unix-like系统上通常没有扩展名)。

这个过程可以被分解为更细的步骤,具体取决于编译器和操作系统,但上述给出的是一个通用和高层次的概述。每个步骤都可能引入不同的优化和处理,以提高代码的性能或减小可执行文件的大小。此外,现代开发环境和编译器可能会提供额外的功能,如跨编译(为不同于编译它的系统构建程序)、增量编译(只重新编译改动的部分)等。


一些C语言函数

  • memset

memset 是 C 语言标准库中的一个函数,用于快速地将一块内存中的所有字节设置为特定的值。它通常用于初始化数组或者清零内存块。在幻方程序中,memset 被用来将整个幻方矩阵初始化为0。

函数原型:

memset 函数的原型定义在 <string.h> 头文件中,其原型如下:

1
void *memset(void *s, int c, size_t n);
  • s 是指向要填充的内存块的指针。
  • c 是要设置的字节值。虽然这个参数的类型是 int,但是 memset 实际上只使用该值的最低字节(也就是 unsigned char 类型)来填充内存块。
  • n 是要设置的字节数。

使用示例:

假设有一个二维整数数组(即矩阵),我们想将其所有元素初始化为0。使用 memset 可以这样做:

1
2
int matrix[10][10];
memset(matrix, 0, sizeof(matrix));

这里,sizeof(matrix) 会计算整个矩阵所占用的字节大小,然后 memset 会将这些字节全部设置为0,从而实现了快速初始化。

memset 与双循环初始化的比较:

使用 memset 初始化内存的主要优点是速度。对于简单的值初始化(如将所有元素设置为0或-1等),memset 通常比手写循环更快,因为它是专门为快速内存操作优化的。

然而,memset 的限制在于它只能设置单一的字节值,这对于初始化所有元素为相同值的一维数组很有用,但对于需要初始化为不同值的复杂数据结构或更高维度数组(除非是初始化为0),直接使用双循环可能更直观、更灵活。

例如,如果要将二维数组中的每个元素初始化为其索引之和,就必须使用嵌套循环:

1
2
3
4
5
6
int matrix[10][10];
for (int i = 0; i < 10; i++) {
for (int j = 0; j < 10; j++) {
matrix[i][j] = i + j;
}
}

在这种情况下,memset 无法直接使用,因为它无法设置多个不同的值。

memset的局限性

对于非0的初始化,memset 函数就不那么方便了,因为它按字节操作。尽管理论上可以用 memset 来设置非0值(如255),但在实际应用中这通常不是初始化为特定整数值(尤其是大于255或非单字节值)的好方法。对于整数数组,特别是当你想要的初始化值是多字节的(如整型的1或2),直接使用循环初始化是更清晰和直接的方法。

总结

总的来说,对于初始化为特定非零值的情况,使用循环是最直接、最灵活的方法。这种方法可以轻松处理任何值的初始化,包括1、2或任何其他整数,同时也适用于更复杂的初始化逻辑。尽管memset在某些情况下为快速初始化提供了方便,但它主要适用于单字节值的快速设置,特别是将内存清零时最为有效。


  • malloc

头文件:#include <stdlib.h>


  • gets()

在C语言中,接收一行文本的确有多种方法,但是使用 gets() 并不推荐,原因如下:

gets() 的问题

  • 安全性gets() 函数从标准输入读取字符,直到遇到换行符(newline)为止,然后将换行符替换为空字符(null terminator)以形成字符串。这个函数不执行边界检查,因此非常容易造成缓冲区溢出,这是一种常见的安全漏洞。
  • 弃用:由于安全性问题,gets() 函数在C11标准中已被正式弃用,取而代之的是 gets_s(),后者包括了额外的参数来指定缓冲区的大小。

推荐的替代方法

  1. fgets():这是一个更安全的选择,因为它允许你指定缓冲区的大小,从而避免了缓冲区溢出的风险。fgets() 会读取最多 n-1 个字符(n 是你指定的缓冲区大小),或者直到遇到一个换行符或EOF,以先到者为准。如果读取到换行符,它会存储在字符串中,然后再添加一个空字符。

    1
    2
    3
    4
    char buffer[100];
    if (fgets(buffer, sizeof(buffer), stdin) != NULL) {
    // 处理输入
    }
  2. scanf() with %[^\n]:虽然 scanf() 通常不推荐用于读取整行文本(因为它在遇到空格时会停止读取),但你可以使用 %[^\n] 这样的格式字符串来读取直到换行符为止的整行。注意,这种方法同样存在缓冲区溢出的风险,除非你明确限制了输入的长度。

    1
    2
    char buffer[100];
    scanf("%99[^\n]", buffer); // 读取直到换行符,最多99字符以留空间给结尾的null
  3. getline():在某些平台(特别是POSIX兼容系统)上,你可以使用 getline() 函数,这个函数可以自动分配足够大小的缓冲区来存储整行输入,从而避免缓冲区溢出的问题。使用完后需要释放分配的内存。请注意,这不是标准C函数,但在许多系统上可用。

总的来说,考虑到安全性和可移植性,fgets() 通常是读取一行文本的最佳选择。使用 gets() 因其安全性问题应当避免。


  • 输入输出

当然可以。gets(), getchar(), puts(), putchar(), fputs(), 和 fgets() 是C语言中用于输入和输出的标准库函数,每个函数都有其特定用途和行为特点:

gets()

  • 用途:从标准输入(通常是键盘)读取一行直到遇到换行符(\n),然后将这个换行符替换为字符串结束符(\0)。
  • 注意gets()已经被废弃,因为它存在严重的安全问题,容易导致缓冲区溢出。推荐使用fgets()替代。

getchar()

  • 用途:从标准输入读取下一个可用的字符,并返回它。如果遇到文件结束符(EOF)或读取出错,则返回EOF。
  • 返回类型:返回int以确保能够表示所有字符和EOF。

puts()

  • 用途:向标准输出写入一个字符串,并在字符串末尾自动添加换行符\n
  • 参数:接受一个指向以\0结尾的字符串的指针。
  • 返回类型:成功时返回非负值,失败时返回EOF。

putchar()

  • 用途:向标准输出写入一个字符。
  • 参数:接受一个字符(虽然参数类型是int,但实际上只使用了参数的低8位来表示字符)。
  • 返回类型:成功时返回写入的字符,失败时返回EOF。

fputs()

  • 用途:向指定的文件流写入一个字符串,不自动添加换行符\n
  • 参数:第一个参数是指向以\0结尾的字符串的指针,第二个参数是指向FILE对象的指针,表示要写入的文件流。
  • 返回类型:成功时返回非负值,失败时返回EOF。

fgets()

  • 用途:从指定的文件流读取最多n-1个字符,或直到遇到换行符\n为止,并将读取的字符串存储到指定的缓冲区,包括换行符(如果有的话),最后添加字符串结束符\0
  • 参数:第一个参数是指向用于接收读取数据的缓冲区的指针,第二个参数是缓冲区的大小,第三个参数是指向FILE对象的指针,表示要读取的文件流。
  • 返回类型:成功时返回指向缓冲区的指针,遇到文件结束符(EOF)且没有读取到任何字符时返回NULL。

这些函数为处理标准输入输出(stdinstdout)以及文件I/O提供了基本的工具,它们在许多C语言程序中被广泛使用。安全性和正确性是使用这些函数时需要特别注意的两个方面。特别是要避免使用gets()函数,因为它可能导致安全漏洞。


fputcfputs都是C语言标准库中用于文件输出的函数,它们的主要区别在于处理的数据类型和一些行为细节:

fputc

  • 用途fputc函数用于将一个字符写入到指定的文件流中。
  • 参数fputc接受两个参数:第一个参数是要写入的字符(虽然参数类型是int,但只会使用该int值的低8位来表示字符),第二个参数是指向FILE对象的指针,表示目标文件流。
  • 返回类型:如果写入成功,fputc返回写入的字符;如果发生错误,则返回EOF

fputs

  • 用途fputs函数用于将一个字符串(不包括字符串的终结符\0)写入到指定的文件流中。
  • 参数fputs同样接受两个参数:第一个参数是指向要写入的以\0终结的字符串的指针,第二个参数是指向FILE对象的指针,表示目标文件流。
  • 行为特点:与puts不同,fputs不会在字符串的末尾自动添加换行符\n
  • 返回类型:如果成功,fputs返回一个非负值;如果发生错误,则返回EOF

主要区别

  • 数据类型fputc用于写入单个字符,而fputs用于写入一个字符串。
  • 自动换行fputs不会在写入的字符串末尾自动添加换行符\n,这一点与puts函数不同(puts用于向标准输出写入字符串且会添加换行符)。
  • 参数内容fputc的第一个参数是int类型的字符,fputs的第一个参数是指向字符串的char*类型指针。

在选择使用fputc还是fputs时,可以根据你的需求(是否需要逐字符操作或写入整个字符串)来决定。


extern

  • extern

在C语言中,对于函数和变量的extern关键字使用存在一定的差异,这主要归因于它们的链接属性和如何在编译单元之间共享。理解这一点需要了解C语言的编译和链接过程,以及默认的存储类别(storage class)。

函数

  • 函数默认具有外部链接(external linkage),这意味着它们在不同的编译单元中是可见的,除非被明确声明为static。这就是为什么你可以在一个.c文件中定义一个函数,在其他文件中使用它,而不需要在使用它的文件中用extern关键字声明它。编译器期望函数的定义在链接过程中某处可用,如果找不到,链接器会报错。
  • 当你在一个文件中声明函数时(不含定义),你实际上是在告诉编译器:“这个函数在别的地方定义了。”因为函数默认是外部链接的,所以即便不使用extern关键字,这一声明也是有效的。

变量

  • 变量不同于函数,因为它们不仅可以声明为具有外部链接(通过extern关键字),还可以定义为具有内部链接(通过static关键字)或无链接(如函数内的局部变量)。
  • 当你在一个文件中使用extern int a;声明时,你告诉编译器:“变量a的定义在程序的其他地方。”这样,编译器不会在当前编译单元为a分配存储空间;相反,它期望在链接过程中解决a的引用。
  • 如果省略了extern并直接在多个编译单元中声明同一个变量(例如,int a;),这会导致每个编译单元都尝试定义并分配存储空间给该变量,从而在链接时产生重定义错误。

总结

简而言之,函数默认具有外部链接属性,而变量则需要明确使用extern来表明它们是在其他地方定义的。这种设计允许函数被不同的文件共享而无需额外声明,同时提供了机制通过extern关键字来共享变量,避免重复定义的问题。






概念题:

  1. C语言中为什么char d=678;不对
查看解析

在C语言中,char类型通常是一个字节(8位),其范围是-128到127(有符号)或0到255(无符号),取决于编译器和平台的具体实现。因此,当你尝试将一个大于127或小于-128的整数赋给char类型时,会发生截断,结果可能不符合你的预期。

在你提到的情况下,当你尝试将678赋给一个char类型的变量d时,这个值超出了char类型的范围,因此会发生截断。具体来说,通常情况下会将678模256(即678 % 256),得到的结果是146,然后将其转换成char类型。因此,d的值实际上会是146,而不是你可能期望的678

  1. 10 + 567.345的结果是什么类型
查看解析

在C语言中,整数字面值默认是int类型,而小数(包括常数567.345)默认是double类型。

因此,在表达式10 + 567.345中,整数10会被转换为double类型,然后与567.345进行加法运算,最终结果的类型将是double类型。

  1. c语言中不能用来表示常整数的进制是?
查看解析

在C语言中,不能用来表示常整数的进制是二进制点表示法。二进制点表示法是一种表示实数的方法,它使用二进制表示小数部分,例如101.1101。在C语言中,整数常量通常是十进制的(如10),也可以使用八进制(如017)或十六进制(如0xFF)。

问:二进制不行?

答:实际上,C语言是支持二进制表示整数的,但是在标准C语言中,二进制常量是C99标准中引入的新特性。你可以使用0b0B作为前缀来表示一个二进制整数。例如,0b1010表示十进制的10

所以,C语言中不能用来表示常整数的进制是不支持二进制常量的过去的C标准版本,而不是二进制本身。

  1. 字面量相关
查看解析
  • 错误: ‘105’
    正确: '\105’或者直接赋值整数105
  • 错误: 123e2.3
    正确: 基数e指数(指数要是整数)
    123e2表示123乘以10的2次方,即12300。
  1. 左++和右边++
查看解析
1
2
3
4
5
6
7
8
9
#include <stdio.h>

int main()
{
int i = 3;
int k = (++i) + (++i) + (++i);
printf("i=%d k=%d", i, k);
return 0;
}
  1. 计算相关
查看解析

先做除法,再回来算余数还好点

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>

int main()
{
printf("%d %d %d\n", -10 / -3, -10 / 3, 10 / -3);
printf("%d %d %d", -10 % -3, -10 % 3, 10 % -3);
return 0;
}
/* 输出为:
3 -3 -3
-1 -1 1
*/
  1. **以下合法的字符常量为: **
    A: ‘\084’ B: ‘\x43’ C: ‘0’ D: “\0”
查看解析
  • A: '\084' - 不合法,因为八进制数字只能是0-7。
  • B: '\x43' - 合法的字符常量,使用十六进制表示法表示的ASCII字符C
  • C: '0' - 合法的字符常量,表示字符0
  • D: "\0" - 是一个合法的字符串常量,表示包含单个null字符(字符串结束符)的字符串。
  1. 结构体的大小计算





编程题:

  1. 输入一行字符,统计其中包括多少单词,单词之间用空格分隔
查看代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <stdio.h>
#include <string.h>

int main() {
/* 思路:空格进单词的时候统计一次 */
char buffer[1000];
int lenText = 0; // 文本的长度
int cnt = 0; // 统计单词数
int status = 0; // 0空格,1单词
printf("请输入一行文本:\n");
// gets(buffer);
fgets(buffer, sizeof(buffer), stdin); // 使用fgets代替gets
lenText = strlen(buffer);
// 特判第一次
if (buffer[0] != ' ') {
status = 1;
cnt++;
}
int i;
for (i = 0; i < lenText; i++) {
int old_status = status;
if (buffer[i] == ' ')
status = 0;
else
status = 1;
// 如果有空格到字母的过渡,单词+1
if (old_status == 0 && status == 1)
cnt++;
}
printf("单词总数为%d", cnt);
return 0;
}
  1. 读取一个文本文件,统计其中不同单词的数量以及对应出现几次。要求用结构体数组完成,可以用strcmp函数判断字符串是否已经存在

  2. 奇数阶幻方

查看解析

编写一个奇数阶幻方的算法,你可以遵循以下经典的解题思路,称为“西塔法”或“魔方阵法”:

  1. 初始位置:将 1 放在第一行的中间位置。
  2. 下一个数字的位置
    • 从当前数字的位置,向上移动一行并向右移动一列填入下一个数字。如果这样做会超出数组的边界(即超出上边界或右边界),则需要进行调整:
      • 如果下一个数字的位置超出了上边界但没有超出右边界,那么将下一个数字放在数组的最底行同列位置。
      • 如果下一个数字的位置超出了右边界但没有超出上边界,那么将下一个数字放在同一行的最左边。
      • 如果下一个数字正好位于右上角的对角线外,那么将其放在当前位置的正下方。
    • 如果新位置已经被填入过数字,则将下一个数字填入当前位置的正下方。
  3. 继续填充:重复步骤 2,直到所有数字都被放入幻方中。

以下是这个算法的一个简单实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include <stdio.h>
#include <string.h>

#define MAX_SIZE 100 // 定义最大的幻方尺寸

void generateMagicSquare(int n) {
int magicSquare[MAX_SIZE][MAX_SIZE];
memset(magicSquare, 0, sizeof(magicSquare)); // 初始化幻方矩阵为0

int num = 1; // 当前要放置的数字
int i = 0, j = n/2; // 开始位置:第一行的中间
while (num <= n*n) {
if (i < 0 && j >= n) { // 右上角对角线外的特殊情况
i += 2;
j--;
}
if (i < 0) { // 超出上边界
i = n - 1;
}
if (j == n) { // 超出右边界
j = 0;
}
if (magicSquare[i][j]) { // 如果当前位置已经填充过数字
i += 2;
j--;
continue;
}

magicSquare[i][j] = num++; // 放置数字并准备放置下一个数字
i--; // 向上移动
j++; // 向右移动
}

// 打印幻方
for (i = 0; i < n; i++) {
for (j = 0; j < n; j++)
printf("%4d ", magicSquare[i][j]);
printf("\n");
}
}

int main() {
int n;
printf("请输入奇数n生成n阶幻方:");
scanf("%d", &n);
if (n % 2 == 0 || n <= 0 || n > MAX_SIZE) {
printf("输入的数字必须是正奇数,且不超过%d。\n", MAX_SIZE);
return 1;
}

generateMagicSquare(n);
return 0;
}

这段代码定义了一个 generateMagicSquare 函数来生成奇数阶幻方,然后在 main 函数中读取用户输入的奇数 n 并调用 generateMagicSquare 来显示幻方。需要注意的是,这里设置了一个 MAX_SIZE 来限制幻方的最大尺寸,以防止数组越界,同时也检查输入的 n 是否为正奇数。

  1. 编写一个程序,输入两个字符串,比较它们是否相等
查看代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>

int main() {
char str1[1000], str2[1000];
scanf("%s %s", str1, str2);
char *s1 = str1, *s2 = str2;
while (*s1 != '\0' && *s2 != '\0') {
if (*s1 != *s2) {
printf("不相同");
return 0;
}
s1++;
s2++;
}
if (*s1 == '\0' && *s2 == '\0')
printf("相同");
else
printf("不相同");
return 0;
}
  1. 1000以内的素数
查看解析

找到1000以内的所有素数,一个有效且常用的算法是埃拉托斯特尼筛法(Sieve of Eratosthenes)。该算法的基本思想是:从最小的素数开始,去除其所有的倍数;剩下的未被去除的数就是素数。

以下是实现该算法的步骤:

  1. 创建一个列表,列出从2开始到1000的所有自然数(因为1不是素数)。
  2. 设置当前数为2(最小的素数)。
  3. 去除当前数的所有倍数(除了它自己)。
  4. 找到下一个还没有被去除的数,它就是下一个素数。
  5. 重复步骤3和4,直到处理完所有小于等于1000的数。
  6. 剩下的未被去除的数就是素数。

让我们用C语言来实现这个算法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <stdio.h>
#define N 1000 // 定义搜索的最大值

int main() {
int isPrime[N+1]; // 使用数组标记是否为素数,0为未处理,1为非素数,2为素数
int i, j;

// 初始化数组,假设所有数都是素数
for (i = 2; i <= N; i++) {
isPrime[i] = 2;
}

// 埃拉托斯特尼筛法
for (i = 2; i * i <= N; i++) {
if (isPrime[i] == 2) { // 当前数为素数
for (j = i * i; j <= N; j += i) { // 例如,当你使用2筛选时,你已经去除了所有2的倍数;当你增加到3时,2和3的所有小于3*3的倍数(即2, 4, 6, 8; 3, 6, 9)已经被标记过了。
isPrime[j] = 1; // 标记倍数为非素数
}
}
}

// 输出所有素数
printf("1000以内的素数有:\n");
for (i = 2; i <= N; i++) {
if (isPrime[i] == 2) {
printf("%d ", i);
}
}
printf("\n");

return 0;
}

这段代码将打印出1000以内所有的素数。埃拉托斯特尼筛法非常高效,特别是当需要找到较小范围内的所有素数时。其时间复杂度大致为O(n log log n),空间复杂度为O(n)。

  1. 使用函数 swap()将输入数据的高位字节和低位字节交换后返回。main()函数调用这个函数,实现程序的功能
查看代码
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>

unsigned int swap(unsigned int tar) {
return (tar >> 16 & 0x0000ffff) | (tar << 16 & 0xffff0000);
}

int main()
{
unsigned int tar;
scanf("%x", &tar);
printf("%x -> %x", tar, swap(tar));
return 0;
}





代码模块:

  1. 辗转相除法
查看代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <stdio.h>

int main()
{
int x, y;
printf("请输入两个正整数:\n");
scanf("%d %d", &x, &y);
int num1 = x, num2 = y;
// 1. 保证 num1 大于 num2
if (num1 < num2)
{
int tmp = num1;
num1 = num2;
num2 = tmp;
}
// 2. 辗转相除法
while (num2)
{
int tmp = num1 % num2;
num1 = num2;
num2 = tmp;
}
printf("二者的最大公因数为:%d\n", num1);
printf("二者的最小公倍数为:%d", x * y / num1);
return 0;
}
  1. 排序
查看代码

简单选择排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <stdio.h>

#define MAX_SIZE 100000

void sort(int num[], int n) {
int i, j;
for (i = 0; i < n; i++) {
int targetIndex = i; // 最小数字的索引
for (j = i+1; j < n; j++) {
if (num[j] < num[targetIndex])
targetIndex = j;
}
int tmp = num[i];
num[i] = num[targetIndex];
num[targetIndex] = tmp;
}
}

int main()
{
printf("第一个数表示输入数字个数,接下来输入的是待排序数字(整数):\n");
int num[MAX_SIZE] = {0};
int n; // 数字个数
scanf("%d", &n);
int i;
for (i = 0; i < n; i++)
scanf("%d", &num[i]);
sort(num, n);
printf("排序结果为:\n");
for (i = 0; i < n; i++)
printf("%d ", num[i]);
return 0;
}
  1. 快速排序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <stdio.h>

#define MAX_SIZE 100000

int partition(int num[], int left, int right) {
int cur = num[left];
while (left < right) {
while (left < right && num[right] >= cur) right--;
num[left] = num[right];
while (left < right && num[left] <= cur) left++;
num[right] = num[left];
}
num[left] = cur;
return left;
}

void quicksort(int num[], int left, int right) {
if (left < right) {
int pivotpos = partition(num, left, right);
quicksort(num, left, pivotpos-1);
quicksort(num, pivotpos + 1, right);
}
}


int main() {
printf("第一个数表示输入数字个数,接下来输入的是待排序数字(整数):\n");
int num[MAX_SIZE] = {0};
int n; // 数字个数
scanf("%d", &n);
int i;
for (i = 0; i < n; i++)
scanf("%d", &num[i]);
quicksort(num, 0, n-1);
printf("排序结果为:\n");
for (i = 0; i < n; i++)
printf("%d ", num[i]);
return 0;
}
  1. 初始化
查看内容

对于二维数组初始化为0,以下是可行的方法:

  1. 循环赋值:使用双层循环显式地遍历数组的每一个元素,并将每个元素设置为0。这种方法非常直观,且可以用于任何类型的初始化值,不仅仅是0。

    1
    2
    3
    4
    5
    for (int i = 0; i < SIZE; i++) {
    for (int j = 0; j < SIZE; j++) {
    num[i][j] = 0;
    }
    }
  2. 直接初始化:在声明二维数组时,使用{0}初始化表达式。这将确保数组的所有元素都被初始化为0。C语言保证未显式初始化的静态和全局变量会被自动初始化为0,局部变量也可以使用这种方式进行显式初始化。

    1
    int num[SIZE][SIZE] = {0};
  3. 使用memsetmemset函数可以用来将一块内存全部设置为特定的值,适用于快速地将二维数组的所有元素初始化为0。这是因为memset按字节操作,而数字0的内存表示全为0字节,因此这种方法对于初始化为0的场景非常有效。

    1
    memset(num, 0, sizeof(num));

这三种方法各有利弊:

  • 循环赋值最为直观灵活,适用于任何值的初始化,特别是当初始化值不是0时。
  • 直接初始化用于静态或全局数组初始化为0时非常简洁,也适用于局部数组的0初始化。
  • **使用memset**非常适合于大型数据结构的快速清零,特别是当性能是一个考虑因素时。

选择哪种方法取决于具体的场景和个人偏好。






深研院23笔试真题

  1. 数组冒泡排序
查看代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <stdio.h>

#define MAX_SIZE 100000

void bsort(int num[], int n) {
int i, j;
for (i = 0; i < n; i++) {
for (j = 0; j < n - i - 1; j++) {
if (num[j] > num[j+1]) {
int tmp = num[j];
num[j] = num[j + 1];
num[j + 1] = tmp;
}
}
}
}

int main()
{
printf("第一个数表示输入数字个数,接下来输入的是待排序数字(整数):\n");
int num[MAX_SIZE] = {0};
int n; // 数字个数
scanf("%d", &n);
int i;
for (i = 0; i < n; i++)
scanf("%d", &num[i]);
bsort(num, n);
printf("排序结果为:\n");
for (i = 0; i < n; i++)
printf("%d ", num[i]);
return 0;
}
  1. 输入10个整数,去掉一个最大的,去掉一个最小的,求剩下的8个数平均值,结果保留两位小数,并输出最大数和最小数
查看代码

注意平均数为浮点数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>

#define MAX(a, b) a > b ? a : b
#define MIN(a, b) a < b ? a : b

int main() {
int max = -0x7fffffff, min = 0x7fffffff;
int sum = 0;
printf("请输入10个数:\n");
int i;
for (i = 0; i < 10; i++){
int tmp;
scanf("%d", &tmp);
max = MAX(max, tmp);
min = MIN(min, tmp);
sum += tmp;
}
printf("其中最大的数为%d, 最小的数为%d, 其余8个数的平均值为%.2f", max, min, (float)(sum - max - min) / 8);
return 0;
}
  1. 判断成绩等级
查看代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>

int main()
{
int num, c;
printf("请输入你的成绩:\n");
scanf("%d", &num);
if (num >= 90) c = 'A';
else if (num >= 80) c = 'B';
else if (num >= 70) c = 'C';
else if (num >= 60) c = 'D';
else c = 'E';
printf("你的成绩为等级为%c", c);
}
  1. 计算
查看代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>

int main() {
double ans = 100;
int i;
for (i = 1; i <= 99; i++) {
if (i % 3 == 0) {
ans -= (double)2 / i;
}
else {
ans += (double)2 / i;
}
}
printf("%.5lf", ans);
return 0;
}
  1. 求电费
查看代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>

int main() {
double num;
printf("请输入用电量:\n");
scanf("%lf", &num);
if (num < 0) {
printf("invalid value");
return 0;
}
else if (num <= 50) {
printf("电费为%.2lf", num * 0.53);
}
else {
printf("电费为%.2lf", 50 * 0.53 + (num - 50) * 0.58);
}
return 0;
}





计院23笔试真题

  1. 给俩串,第二个串就两字符,判断第一个串中有几个第二个串
查看解析

strstr()函数是一个标准的C库函数,定义在<string.h>头文件中。它的作用是在一个字符串(称为“主字符串”)中查找第一次出现的另一个字符串(称为“子字符串”)的首字符的位置。如果找到了子字符串,则strstr()返回一个指向主字符串中子字符串第一次出现位置的指针;如果没有找到子字符串,则返回NULL

函数原型

1
char *strstr(const char *haystack, const char *needle);
  • haystack是主字符串,你想在其中查找另一个字符串。
  • needle是子字符串,你想在主字符串中查找的内容。

返回值

  • 如果needle是空字符串,strstr()将返回haystack的指针。
  • 如果needle非空,且在haystack中找到了匹配,strstr()返回一个指向匹配子字符串首字符的指针。
  • 如果在haystack中没有找到needle,则返回NULL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <stdio.h>
#include <string.h>

int countSubstring(const char *str, const char *sub) {
int count = 0;
const char *tmp = str;
while ((tmp = strstr(tmp, sub)) != NULL) {
count++;
tmp++; // 移动到下一个字符开始查找,避免重复计算同一个位置
}
return count;
}

int main() {
char str[100];
char sub[3]; // 子串加上空字符

printf("请输入主串: ");
fgets(str, sizeof(str), stdin); // 读取包含空格的字符串
str[strcspn(str, "\n")] = 0; // 去除fgets读取的换行符

printf("请输入包含两个字符的子串: ");
fgets(sub, sizeof(sub), stdin);
sub[strcspn(sub, "\n")] = 0; // 同样去除换行符

int count = countSubstring(str, sub);
printf("子串 '%s' 在主串中出现的次数为: %d\n", sub, count);

return 0;
}

让我们一一解答你的问题:

  1. 为什么const变量可以自加改变?

在提供的代码中,const修饰符用于指针变量指向的内容,而不是指针变量本身。这意味着指针指向的数据不能被修改,但指针本身可以改变,指向字符串中的不同位置。例如,tmp++操作使指针tmp指向下一个字符,但不允许修改tmp所指向的内容。这是const在指针声明中的常见用途:保护数据不被修改,同时允许指针在数据结构中移动。

  1. 为什么要去掉换行符?

fgets()函数从标准输入读取字符串时,会将换行符(如果在给定的最大字符数范围内遇到)一同读入并存放在字符串中。在大多数情况下,换行符不是我们处理字符串时想要的部分,尤其是当我们用字符串来做进一步处理(如查找子串)时,换行符可能会导致不符合预期的结果。因此,使用strcspn()函数去除换行符是为了确保字符串仅包含用户实际输入的内容。

  1. fgets()会读取换行符吗?

是的,fgets()会读取换行符。如果用户在输入后按下回车键,回车键生成的换行符也会被fgets()读取并添加到所读取的字符串末尾,前提是字符串的长度限制允许换行符被包括在内。

  1. strcspn()的作用是什么?

strcspn()函数计算两个字符串的第一个交集字符之前的字符数。具体来说,它返回第一个字符串中的起始段长度,这个起始段中的所有字符都不包含在第二个字符串中。在提供的代码中,strcspn(str, "\n")用于找出字符串str中换行符\n第一次出现的位置。因为在这个上下文中,第二个字符串只包含一个换行符,所以strcspn()实际上返回的是输入字符串中换行符的位置。这样,通过将该位置的字符设置为\0(字符串结束符),可以有效地去除字符串末尾的换行符。

  1. 给一个英语文件,统计有多少不同的单词,还有每个单词的数量
查看代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define MAX_WORD_SIZE 100

struct WordStruct {
char word[MAX_WORD_SIZE];
int cnt;
struct WordStruct *next;
};
typedef struct WordStruct Word;

int isCharacter(char c) {
if (c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z')
return 1;
return 0;
}

int main() {
FILE *file = fopen("./tmp/test.txt", "r");
if (file == NULL) {
printf("样例文件不存在\n");
return 1;
}
char cur = fgetc(file);
int status = 0; // 0 表示cur为非字母, 1表示字母,有0到1的跳变时,记录一个单词
Word *head = (Word *)malloc(sizeof(Word));
head->next = NULL;
Word *curWord = head;
while (cur != EOF) {
int old_status = status;
if (isCharacter(cur))
status = 1;
else
status = 0;
if (old_status == 0 && status == 1) {
// 记录单词
Word *tmp = (Word *)malloc(sizeof(Word));
int i = 0;
while (isCharacter(cur)) {
tmp->word[i++] = cur;
cur = fgetc(file);
}
tmp->word[i] = '\0';
tmp->cnt = 1;
tmp->next = NULL;
// 如果是第一个单词的话
if (head->next == NULL) {
head->next = tmp;
continue;
}
// 看是否有相同的单词
Word *p = head->next;
while (p) {
// 相同的话,计数,并释放tmp
if (strcmp(p->word, tmp->word) == 0) {
p->cnt++;
free(tmp); // 避免内存爆炸
break;
}
// 到最后一个都没有相同的
if (p->next == NULL) {
p->next = tmp;
break;
}
p = p->next;
}
}
else
cur = fgetc(file);
}
Word *p = head->next; // 去掉哨兵节点
free(head);
while (p) {
printf("单词:%s\t\t出现次数%d\n", p->word, p->cnt);
Word *tmp = p;
p = p->next;
free(tmp);
}
fclose(file);
return 0;
}
  1. 结构体存学生姓名,学号,分数三个信息。用成绩由低到高排序,用折半查找分数为87的学生信息
查看代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
#include <stdio.h>
#include <math.h>

#define MAX_SIZE 10000

struct StudentStruct{
double score;
char number[30];
char name[30];
};
typedef struct StudentStruct Student;

Student students[MAX_SIZE];
int n; // 学生数量

void inputData() {
scanf("%d", &n);
int i;
for (i = 0; i < n; i++) {
scanf("%s", students[i].name);
scanf("%s", students[i].number);
scanf("%lf", &students[i].score);
}
}

int partition(Student students[], int left, int right) {
Student pivot = students[left];
while (left < right) {
while (left < right && students[right].score >= pivot.score) right--;
students[left] = students[right];
while (left < right && students[left].score <= pivot.score) left++;
students[right] = students[left];
}
students[left] = pivot;
return left;
}

void quicksort(Student students[], int left, int right) {
if (left < right) {
int pivot = partition(students, left, right);
quicksort(students, left, pivot - 1);
quicksort(students, pivot + 1, right);
}
}

void find(double key) {
int left = 0, right = n - 1;
int flag = 1;
int mid;
while (left <= right) {
mid = (left + right) / 2;
if (key < students[mid].score) right = mid - 1;
if (key > students[mid].score) left = mid + 1;
if (fabs(key - students[mid].score) < 1e-7) {
printf("name:%s\tnumber:%s\n", students[mid].name, students[mid].number);
flag = 0;
break;
}
}
if (flag) printf("没有分数为%.2lf的学生\n", key);
else {
// 输出所有同样分数的同学, 在上面找到的基础上接着找
int tmp = mid;
while (tmp>=0) {
tmp--;
if (fabs(key - students[tmp].score) < 1e-7) {
printf("name:%s\tnumber:%s\n", students[tmp].name, students[tmp].number);
}
else break;
}
tmp = mid;
while (++tmp) {
if (tmp > n - 1) break;
if (fabs(key - students[tmp].score) < 1e-7) {
printf("name:%s\tnumber:%s\n", students[tmp].name, students[tmp].number);
}
else break;
}
}
return;
}

int main() {
inputData();
quicksort(students, 0, n - 1);
double key;
scanf("%lf", &key);
find(key);
return 0;
}
  1. 头插法链表
查看代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#include <stdio.h>
#include <stdlib.h>

struct NodeStruct{
int n;
struct NodeStruct *next;
};
typedef struct NodeStruct Node;

Node * head = NULL;

void init() {
head = (Node *)malloc(sizeof(Node));
Node *p = head;
int i;
for (i = 1; i <= 10; i++) {
Node *tmp = (Node *)malloc(sizeof(Node));
tmp->n = i;
tmp->next = NULL;
p->next = tmp;
p = tmp;
}
}

void change(int key) {
Node *p = head;
// 为了找到待操作的前一个,记录为p
while (p->next) {
if (p->next->n == key) break;
p = p->next;
}
Node *tmp = p->next;
p->next = p->next->next;
tmp->next = head->next;
head->next = tmp;
}

int main() {
init();
int n;
scanf("%d", &n);
while(n--) {
int key;
scanf("%d", &key);
change(key);
}
int i = 0;
Node *p = head->next;
free(head);
while (p->next) {
printf("%d-", p->n);
Node *tmp = p;
p = p->next;
free(tmp);
}
printf("%d", p->n);
free(p);
return 0;
}





计院18笔试真题

  1. 输入若干整数(以0结束),如何逆序构建双向链表
查看代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include <stdio.h>
#include <stdlib.h>

struct NodeStruct {
int n;
struct NodeStruct *pre, *next;
};
typedef struct NodeStruct Node;

Node *head = NULL, *tail = NULL;

int main() {
int n;
scanf("%d", &n);
head = (Node *)malloc(sizeof(Node)); // 创建哨兵节点
head->pre = NULL;
head->next = NULL;
tail = head; // 初始时,tail也是head,表示链表为空

while (n != 0) {
Node *tmp = (Node *)malloc(sizeof(Node));
tmp->n = n;
tmp->pre = NULL; // 新节点成为第一个节点,没有前驱
tmp->next = head->next; // 新节点的下一个节点是当前的第一个节点

if (head->next != NULL) { // 如果链表非空
head->next->pre = tmp; // 当前第一个节点的前驱设置为新节点
}
head->next = tmp; // 哨兵的下一个节点更新为新节点
if (tail == head) { // 如果添加的是第一个实际节点
tail = tmp; // 更新tail为这个节点
}

scanf("%d", &n);
}

// 遍历链表并释放内存
Node *p = head->next; // 从第一个实际节点开始
while (p != NULL) {
printf("%d ", p->n);
Node *tmp = p;
p = p->next;
free(tmp);
}

free(head); // 最后释放哨兵节点
return 0;
}





电子科技大学复试面试题准备:

  1. 人工智能、机器学习、深度学习
查看解析

深度学习、机器学习和人工智能之间存在着层次性的关系,可以通过它们的定义和应用来理解它们之间的联系:

  1. 人工智能 (AI): 人工智能是计算机科学的一个分支,它致力于创造能够执行通常需要人类智能才能完成的任务的机器或程序。这包括诸如语言理解、视觉感知、决策制定和学习等能力。人工智能是一个广泛的领域,它不仅包括基于规则的系统,也包括能够从数据中学习和适应的系统。

  2. 机器学习 (ML): 机器学习是实现人工智能的一种方法。它侧重于开发算法和技术,让计算机系统能够从数据中学习模式和规律,而不需要为每种情况编写具体的指令。机器学习包括多种算法,如线性回归、决策树、支持向量机等,这些算法能够使计算机通过经验改进其性能。

  3. 深度学习 (DL): 深度学习是机器学习的一个子集,它使用了一类特殊的算法——神经网络,尤其是深层神经网络,来学习数据中的复杂模式。深度学习在许多任务上,特别是图像和语音识别、自然语言处理等领域,表现出了优异的性能。深度学习之所以称为“深度”,是因为它使用了多层的神经网络结构来自动提取并学习数据的高级特征。

简单来说,人工智能是最广泛的概念,它的目标是创造能模仿人类智能的机器;机器学习是实现人工智能的一种手段,它通过让机器从数据中学习来获得智能;深度学习是机器学习中的一种具体方法,通过使用深层神经网络来处理复杂的数据模式,实现了在某些任务上超越人类表现的能力。深度学习的成功推动了人工智能领域的许多最近的突破。

  1. 如何避免C语言头文件重复引用
查看解析

在C语言中,头文件重复引用(或称之为重复包含)可能会导致编译错误,例如重复定义的变量或函数。为了避免这种情况,通常会使用预处理器指令来确保头文件只被包含(引用)一次。最常用的方法是通过预处理器的条件编译功能,使用“头文件保护”(也称为“包含卫士”)技术。以下是避免头文件重复引用的步骤:

使用预处理器定义宏(Include Guards)

  1. 定义一个唯一的宏:在头文件的最开始定义一个唯一的宏,这个宏的名称通常是头文件名的大写形式,加上一些前缀或后缀,如_H,以减少命名冲突的可能性。

  2. 使用#ifndef#define指令:在头文件最开始处,使用#ifndef预处理器指令检查这个宏是否已经定义。如果未定义,使用#define指令定义它。这样,只有在宏未被定义的情况下,头文件的内容才会被包含。

  3. 使用#endif结束条件编译:在头文件的最末尾使用#endif指令结束条件编译。

示例

假设你有一个头文件叫example.h,避免它被重复引用的方法如下:

1
2
3
4
5
6
7
8
9
10
#ifndef EXAMPLE_H // 如果宏EXAMPLE_H未定义,则进入
#define EXAMPLE_H // 定义宏EXAMPLE_H

// 头文件内容开始

// 函数声明、宏定义、类型定义等

// 头文件内容结束

#endif // 结束宏EXAMPLE_H的条件编译

这种方法可以确保头文件内容在一个源文件或多个源文件中被包含(引用)时,其内容只被编译一次,从而避免了重复定义的问题。

注意事项:

  • 确保宏名称的唯一性,以避免与其他头文件中的宏发生命名冲突。
  • 对于大型项目,有时还会看到使用#pragma once指令作为头文件保护的方法。这是一个非标准但在许多编译器中支持的特性,它告诉编译器当前头文件在同一个编译单元中只需要被包含一次。然而,由于它不是C语言的标准特性,所以在跨平台项目中可能会遇到兼容性问题。因此,传统的基于宏的头文件保护方法更加通用。
  1. C语言编译器有哪些
查看解析
  • GCC (GNU Compiler Collection):GCC是一个开源的编译器集合,支持C语言以及其他多种编程语言。它在开源社区非常流行,适用于各种操作系统。
  • Clang:Clang是另一个开源的编译器,重点在于提供优秀的编译速度和内存使用效率,同时生成有用的错误和警告信息。它通常与LLVM后端一起使用,生成优化的代码。
  • Microsoft Visual C++ (MSVC):虽然它主要是一个C++编译器,MSVC也支持C语言编程。它是Windows平台上开发C和C++应用程序的常用选择。