1.static关键字的作用
-
对于全局变量,使用 static 修饰可以将其作用域限制在当前文件内部,也就是说,在其他文件中无法访问该变量。
-
对于局部变量,使用 static修饰可以将其生命周期延长至整个程序运行期间,也就是说,在变量所在的函数执行完毕后,该变量的值不会被销毁,而是一直存在于内存中。
-
对于函数,使用 static 修饰可以将其作用域限制在当前文件内部,也就是说,在其他文件中无法调用该函数。
综上所述,static 关键字的作用有两个方面,一方面是限制了作用域,另一方面是增强了生命周期。
2.c语言什么时候会出现野指针
-
未初始化指针变量:当定义一个指针变量但是没有赋初值时,该指针变量是一个野指针,使用该指针变量的值或者指向该指针变量的地址都可能会导致程序崩溃。
-
指针变量指向已释放的内存:在动态分配内存的时候,如果没有正确释放内存,会导致该内存被泄漏,并且指向该内存的指针变量也就成了野指针,再次使用该指针变量就可能会导致程序崩溃。
1.内存泄漏(Memory Leak)指程序在动态分配内存后,由于某种原因未能将其释放,导致系统无法再次利用这些内存资源的现象。简单来说,就是指向堆内存进行的动态分配后,没有被取消分配,而一直被占用,导致该部分内存不能被重新被利用,造成内存的浪费。
内存泄漏对于程序运行的影响并不会立即出现,因为计算机的内存总量是有限的,一旦内存泄漏的数量足够多,就会导致内存使用率飙升,程序运行变得缓慢,最终可能导致程序崩溃。在长时间运行的程序中,内存泄漏可能导致运行速度变慢,资源占用越来越高,特别是在嵌入式系统或移动设备等资源受限的场景中,内存泄漏会更加严重。
避免内存泄漏的方法包括:1)程序员要负责为每一个动态分配的内存块分配相应的释放动作,并保证在合适的时机执行这些代码;2)注意变量作用域的范围,尽量缩短变量的生命周期,防止变量长时间占用内存;3)使用适当的内存管理工具,比如内存池等。
2.发生内存泄漏后,该内存地址上存储的数据依然存在,但是这块内存没有被显式的释放,指向该内存的指针变量“丢失”了该内存的地址,也就是说,程序运行时不再能够通过指针变量来访问这块内存,但是这块内存仍然会一直占用系统内存,直到程序结束并退出操作系统。
例如,如果一个指针变量 p 指向了一块动态分配的内存,但是在使用完这块内存后忘记释放,也没有将指针变量 p 置为 NULL,这个时候该内存就成了内存泄露,指向该内存的指针变量 p 就失去了指向该内存的指针,但是该内存依然会占用系统内存,直到程序结束。
因此,内存泄漏不仅会使程序变得不稳定,还会占用系统的内存资源,导致系统的性能下降,因此在编写程序时我们需要特别注意内存的管理,避免出现内存泄漏。
- 使用指针操作超出数组范围(越界):如果使用指针变量来操作数组时超出数组的范围,就会出现野指针的情况,因为我们无法确定这块超出数组范围的内存是否已经被其他程序或系统使用,如果使用野指针访问这块内存,那么就有可能出现不可预知的后果。
- 空指针操作:如果对空指针进行解引用或赋值等操作,就有可能出现野指针的情况。
总之,野指针的出现可以由多种原因造成,如果我们不小心使用了野指针,很可能会导致程序的崩溃或者不可预知的后果,因此在编写 C 语言程序时,我们一定要谨慎使用指针,并尽量避免出现野指针。
3.malloc
- malloc函数的返回值类型
malloc 函数的返回值是 void 类型的指针(void *),也称为通用指针或万能指针。void 指针是不确定类型指针,在指针的使用前需要进行类型转换才能正确访问指针所指向的内存。因此,在使用 malloc 函数动态分配内存后,通常需要将返回的指针转换成相应的数据类型的指针,才能使用分配的内存。例如:
int* p = (int*) malloc(sizeof(int));
这里,对 malloc 函数返回的指针进行了 int 类型的强制类型转换
赋值给了指针变量 p,p 指向了一块大小为 int 类型的内存块
该内存块可以通过 p 访问和操作。
malloc 函数的返回值类型只有一种,即 void 类型的指针(void *),也称为通用指针或万能指针。因为 malloc 函数并不知道所分配的内存具体类型,因此它返回的指针类型是 void *,需要在之后根据使用要求进行类型的强制转换,才能正确使用分配的内存。例如,如果需要分配一个字符数组内存,代码如下所示:
char *p = (char*)malloc(sizeof(char)*10);
在分配内存后,需要将分配返回的 void
指针转换成字符指针类型,以便可以访问分配的内存区域。为了避免内存泄漏和其他问题,使用完毕后,应该及时使用 free 函数释放已分配的内存。
4.c语言编译过程
C语言编译过程可以分为四个阶段:
预处理阶段(Preprocessing):处理以 # 开头的预编译指令,如宏定义、头文件包含等。将预处理后的代码输出到中间文件中(.i 文件)。
.i文件的全称是Intermediate file,即中间文件。在C语言编译过程中,预处理阶段将处理后的代码输出到中间文件中,中间文件是经过预处理的代码,尚未翻译成汇编代码。这个中间文件的主要作用是代码调试和优化。
编译阶段(Compiling):将预处理后的代码翻译成汇编语言代码。将翻译后的汇编代码输出到.s文件中(.s 文件)。
词法分析:
语法分析:
语义分析:
中间代码生成:
中间代码(Intermediate Code)是在编译过程中生成的一种形式的代码,位于源代码和最终目标代码之间。它是一种抽象的表示形式,旨在捕捉源代码的语义,并为接下来的优化和目标代码生成提供一个中间层。
中间代码通常相对于源代码和目标代码来说更加简洁和抽象,以便于编译器进行优化和转换。它可以是多种形式,如三地址码、虚拟机指令、抽象语法树(Abstract Syntax Tree,AST)等。
通过生成中间代码,编译器可以进行各种优化,例如常量折叠、公共子表达式消除、死代码消除等。这些优化可以在保持程序语义不变的前提下,提高程序的执行效率和资源利用率。
中间代码还可以提供更好的可移植性,因为它更加接近于高级语言的抽象层级,将目标机器特定的细节隐藏在后续的目标代码生成阶段。这意味着可以在不同的目标平台上重新生成目标代码,而不需要重新进行整个编译过程。
总而言之,中间代码在编译过程中提供了一个方便的中间表示形式,用于进一步的优化和目标代码生成。它旨在捕捉源代码的语义,并提供更高效、更可移植的编译结果。
.s文件的全称是Assembly source file,即汇编源文件。在C语言编译过程中,汇编器将.i文件翻译成汇编代码,在一些特定的场景下,需要手动编写汇编代码实现一些需要低层次操作硬件的代码,此时就需要用到.s文件。汇编源文件是经过汇编器处理后生成的机器码,但是还没有被转化成可执行程序。汇编源文件可以用文本编辑器打开和编辑,其语法和结构类似汇编语言。汇编源文件经过汇编程序的处理之后最终会被转换成机器代码。
汇编阶段(Assembling):将汇编代码翻译成机器语言代码,即目标文件(.o 文件)。
.o文件的全称是Object file,即目标文件。在C语言编译过程中,编译器将汇编源文件翻译成目标文件,目标文件是包含二进制代码和符号表的文件,它是静态链接的基本单位。目标文件中包含了指令代码、数据和符号表等信息,并且还包含了一些重定位信息,用于在链接时确定每个符号的最终地址。目标文件还可以包含代码的调试信息,便于在开发过程中进行调试。最终,目标文件会链接到一起,生成可执行文件。
巧记每个文件后缀:ISO
链接阶段(Linking):将多个目标文件及库文件链接成一个可执行文件或者共享库。在此阶段进行符号解析、重定位等,最终生成可执行文件。
每个阶段的作用如上所述,经过这四个阶段,C程序最终才能被计算机执行。
5.对于const的理解
- 在C语言中,const是一个关键字,用于定义常量。常量是指在程序执行过程中不能被改变的值,比如数组大小、常量字符串、常量表达式等,使用const关键字可以将这些常量进行声明和定义。
//const关键字可以用于定义常量、指针、函数声明、函数参数等。
//在定义常量时,即修饰关键字时,const关键字要与变量名连在一起进行定义。
//例如:
const int a = 1;
//在修饰指针时
//表示该指针指向的变量里面的值不能被修改。例如:
const int *p = &a;
//该指针的值不能被修改
int *const p = &a;
//在定义函数声明或函数参数时
//const关键字可以用来限制参数或返回值的内容不可被修改。例如:
void func(const int *a);
- 如何修改const修饰的值
在C语言中,使用const关键字修饰的变量是只读的,不能用指针进行修改。但是存在一些特殊情况下可以通过指针修改const修饰的变量,如下:
//非const指针修改const变量:虽然const限制了该变量的值不能被修改
//但是如果定义了一个非const类型的指针,可以通过该指针间接地修改指向的const变量。例如:
const int a = 1;
int *p = (int*)&a; // 将const类型的地址强制转换为非const类型的地址
*p = 2; // 通过指针修改const变量的值
printf("%d", a); // 输出2
//const指针通过类型强制转换修改变量:虽然const指针本身不能修改所指向的变量
//但是如果使用类型强制转换将const指针转换为非const指针,则可以通过该指针修改所指向的变量。例如:
const int a = 1;
const int *p = &a;
*(int*)p = 2; // 通过类型强制转换将const指针转换为非const指针,然后修改所指向的变量
printf("%d", a); // 输出2
//这种方法虽然能达到修改const变量的效果,但是会破坏const的意义和作用,因此不建议使用。
需要注意的是,这些方法可以修改const变量,但是都是不安全的,容易导致程序运行时出现错误。因此,在程序开发中,应严格遵循const变量的只读原则,不要通过任何方式进行修改,从而保证程序的正确性和稳定性。
- const修饰的局部变量存储在栈区
const修饰的全局变量存储在数据段(常量区)中
6.结构体,联合体,枚举
- 结构体
结构体的数据成员是按定义顺序在内存中连续存放的,每个成员的地址相对于结构体首地址的偏移量是固定的,因此可以通过指针和偏移量访问结构体的成员。
结构体内存大小的计算方法是将结构体中所有成员的字节大小加起来,再考虑结构体对齐的问题。对于结构体对齐,一般遵循以下原则:
1.结构体成员中的第一个成员变量在结构体变量内存中的偏移量为0,每个成员变量的偏移量必须是其数据类型大小的整数倍。
2.结构体变量的总大小必须是每个成员变量大小的整数倍,否则会进行字节填充。
3.结构体的大小不能超过系统规定的最大值。 - 联合体
联合体是多个不同数据类型的成员共享同一个内存空间的数据类型。在内存中,联合体的大小取决于成员变量中最大的变量大小,所有成员变量在同一个内存地址中存储。在一个时刻只能有一个成员变量存在,修改一个成员变量会影响到其他成员变量。 - 枚举
枚举的存储方式依赖于具体实现,通常会分配一个整数值给每个枚举成员,这个值可以是任何整数类型(通常是 int 或 unsigned int)。如果没有给出初始值,第一个枚举成员的值默认是 0,后面的成员值递增。 - 区别
结构体(struct)是一个由多个不同类型的成员组成的数据类型,每个成员都可以单独访问,且每个成员占用不同的内存空间。结构体的大小等于所包含的所有成员大小之和。结构体以关键字struct开头,需要通过实例化来使用。
联合体(union)是一种特殊的结构体,所有成员共享一个内存空间,并且仅存储其中一个成员。联合体的大小等于它最大成员的大小,实际使用中只能访问其中一个成员。联合体以关键字union开头,需要通过实例化来使用。
枚举(enum)是一种用于定义符号常量的数据类型,它可以将一组有限的名字映射到一组预定义的值。枚举使用的值是整数类型,可以是char、short、int或long类型。枚举以关键字enum开头,不需要实例化就可以直接使用。
//结构体指针、联合体指针和枚举指针指向的区别主要在于它们指向的数据类型不同。
//结构体指针
//结构体指针指向的是结构体类型的数据,可以通过指针来访问结构体中的各个成员。结构体指针可以通过以下方式进行声明和定义:
struct student {
char name[20];
int age;
float score;
};
struct student *ptr_stu; // 声明结构体指针
ptr_stu = (struct student*) malloc(sizeof(struct student)); // 分配内存并赋值给结构体指针
//上述代码定义了一个名为student的结构体类型,通过指针ptr_stu来指向结构体类型的数据。使用malloc()函数为结构体指针分配了一块大小为sizeof(struct student)的内存空间。结构体指针可以通过如下方式访问结构体中各个成员:
strcpy(ptr_stu->name, "John");
ptr_stu->age = 20;
ptr_stu->score = 89.5;
//联合体指针
//联合体指针指向的是联合体类型的数据,所有成员共享同一块内存空间,而联合体指针可以访问该内存空间中的每个成员。联合体指针可以通过以下方式进行声明和定义:
union data {
int n;
float f;
char ch[20];
};
union data *ptr_data; // 声明联合体指针
ptr_data = (union data*) malloc(sizeof(union data)); // 分配内存并赋值给联合体指针
//上述代码定义了一个名为data的联合体类型,通过指针ptr_data来指向联合体类型的数据。使用malloc()函数为联合体指针分配了一块大小为sizeof(union data)的内存空间。联合体指针可以通过如下方式访问联合体中各个成员:
ptr_data->n = 100;
printf("%d\n", ptr_data->n);
ptr_data->f = 3.14;
printf("%.2f\n", ptr_data->f);
strcpy(ptr_data->ch, "hello");
printf("%s\n", ptr_data->ch);
//枚举指针
//枚举指针指向的是枚举类型的数据,枚举数据类型是一组有限的命名常量,枚举指针可以通过赋值来访问其指向的枚举常量。枚举指针可以通过以下方式进行声明和定义:
enum week {
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday,
Sunday
};
enum week *ptr_week; // 声明枚举指针
enum week day = Monday;
ptr_week = &day; // 将枚举变量的地址赋值给枚举指针
//上述代码定义了一个名为week的枚举类型,通过指针ptr_week来指向枚举类型的数据。将枚举变量day的地址赋值给枚举指针ptr_week。枚举指针可以通过如下方式访问枚举常量:
printf("%d\n", *ptr_week);
ptr_week++; // 枚举指针可以自增,指向下一个枚举常量
printf("%d\n", *ptr_week);
//总的来说,结构体指针、联合体指针和枚举指针都是指向不同类型数据的指针,它们的访问方式也有所不同。程序开发中需要根据具体需要选择使用不同类型的指针。
//结构体指针指向的是结构体类型数据的首地址,也就是结构体第一个成员的地址。可以通过如下代码来理解结构体指针的首地址:
#include <stdio.h>
struct student {
char name[20];
int age;
float score;
};
int main() {
struct student s = {"John", 20, 89.5};
struct student *ptr = &s; // 定义结构体指针并将其指向s所在的内存空间
printf("s: %p\n", &s); // 打印s的内存地址
printf("ptr: %p\n", ptr); // 打印结构体指针指向的地址
printf("&s.name[0]: %p\n", &s.name[0]);
//联合体指针指向的是联合体中所有成员共享的内存空间的首地址。因为联合体中所有成员在内存中共享同一块空间,所以指向联合体数据的指针与指向联合体中任意一个成员的指针在数值上是相同的。可以通过如下代码来理解联合体指针的首地址:
#include <stdio.h>
union data {
int n;
float f;
};
int main() {
union data d = {100}; // 使用联合体初始化器初始化联合体,第一个成员n赋值为100
union data *ptr = &d; // 定义联合体指针并将其指向d所在的内存空间
printf("d: %p\n", &d); // 打印联合体d的内存地址
printf("ptr: %p\n", ptr); // 打印联合体指针ptr指向的地址
printf("&d.n: %p\n", &d.n); // 打印d中n成员的地址
printf("&d.f: %p\n", &d.f); // 打印d中f成员的地址
return 0;
}
//输出结果为:
d: 0x7ffee53169b0
ptr: 0x7ffee53169b0
&d.n: 0x7ffee53169b0
&d.f: 0x7ffee53169b0
//可以看到,联合体d的内存地址与联合体指针ptr的指向地址是相同的,这个地址即为联合体中所有成员共享的内存空间的地址。在访问联合体的成员时,可以通过联合体指针来访问,例如ptr->n和ptr->f分别访问了联合体d中的n和f成员。
//总的来说,联合体指针的首地址指向的是联合体中所有成员共享的内存空间的首地址,也是联合体的首地址。因为联合体中所有成员在内存中共享同一块空间,所以可以通过任意一个成员的地址来访问整个联合体的内容。
//枚举类型是一种用户自定义的数据类型,其中枚举成员是按照顺序排列的整数常量。定义枚举类型的语法如下:
enum enum_name { enumerator1, enumerator2, ... };
//其中,enum_name是枚举类型的名称,enumerator1, enumerator2等是枚举成员。枚举成员默认从0开始递增,也可以手动指定数值,例如:
enum colors { RED = 1, GREEN = 2, BLUE = 4 };
//在使用枚举类型时,可以通过枚举成员来表示具体的值,例如:
enum colors c = RED; // 定义枚举类型变量c并初始化为RED
//枚举类型的变量在内存中占用的空间大小和整型变量相同,通常为4个字节。因此,枚举指针首地址和整型指针首地址是相同的,指向的是枚举类型变量在内存中的存储地址。可以通过如下代码来理解枚举指针的首地址:
#include <stdio.h>
enum colors { RED = 1, GREEN = 2, BLUE = 4 };
int main() {
enum colors c = RED; // 定义枚举类型变量c并初始化为RED
enum colors *ptr = &c; // 定义枚举指针并将其指向c所在的内存空间
printf("c: %p\n", &c); // 打印枚举类型变量c的内存地址
printf("ptr: %p\n", ptr); // 打印枚举指针ptr指向的地址
return 0;
}
//输出结果为:
c: 0x7ffee12adb1c
ptr: 0x7ffee12adb1c
//可以看到,枚举变量c的内存地址和枚举指针ptr的指向地址是相同的,这个地址指向枚举类型变量在内存中的存储地址。
//总的来说,枚举指针的首地址指向的是枚举类型变量在内存中的存储地址,与整型指针相同。