1、 include头文件的顺序以及双引号""和尖括号<>的区别?
头文件包含顺序
通常来说,头文件的包含顺序是有一定规则的,尽管这不是强制性的规定,但良好的编程实践建议按照以下顺序包含头文件:
- 系统头文件: 包含系统提供的头文件,例如
<iostream>、<cstdlib>等。 - 第三方库头文件: 如果你的程序中使用了第三方库,包含该库的头文件。
- 自定义头文件: 包含你自己编写的头文件。
这种顺序有助于确保你的代码对外部依赖的管理更为清晰,并且有助于预防一些潜在的问题,如头文件的依赖关系。
双引号""和尖括号<>的区别
在C和C++中,包含头文件时,可以使用双引号""或尖括号<>。这两者之间有一些区别:
- 双引号"": 当你使用双引号时,编译器会首先在当前源文件所在的目录中查找头文件。如果找不到,它会继续在系统标准目录中查找。
#include "myheader.h" // 会在当前目录查找myheader.h- 尖括号<>: 当你使用尖括号时,编译器只会在系统标准目录中查找头文件,而不会在当前目录中查找。
#include <iostream> // 会在系统标准目录查找iostream2、main函数有几个参数,分别代表什么?
- 形式一:不带参数
int main() {
// 代码逻辑
return 0;
}这种形式的 main 函数不带任何参数。在这种情况下,不提供命令行参数和环境变量给程序。
- 形式二:带参数
int main(int argc, char *argv[]) {
// 代码逻辑
return 0;
}这种形式的 main 函数带两个参数:
int argc: 表示命令行参数的数量(argument count),包括程序的名称在内。char *argv[]: 是一个指向字符串数组的指针,每个字符串都是一个命令行参数(argument vector)。
例如,运行程序时输入 ./myprogram arg1 arg2,那么 argc 的值将为 3,argv 数组中包含的字符串分别为 ./myprogram、arg1 和 arg2。
int main(int argc, char *argv[]) {
// 通过循环打印所有命令行参数
for (int i = 0; i < argc; ++i) {
printf("Argument %d: %s\n", i, argv[i]);
}
return 0;
}这种带参数的形式使得程序能够接受来自命令行的输入,使程序更加灵活。
3、static关键字
- 局部静态变量
void myFunction() {
static int count = 0; // 静态局部变量
count++;
printf("Count: %d\n", count);
}静态局部变量在函数内部声明,但其生命周期和程序运行时间相同。每次调用函数时,它的值会被保持,而不是在每次函数调用时重新初始化。这对于需要保留上一次调用状态的情况很有用。
- 文件作用域中的静态变量
// 文件1.c
static int globalVar = 10;在文件作用域中使用 static 关键字声明的变量,其作用域被限制在声明它的文件内。这样,其他文件无法访问这个变量。
- 静态函数
static void myStaticFunction() {
// 静态函数
}静态函数的作用域被限制在声明它的文件内,它不能被其他文件中的函数调用。这有助于实现文件内的封装。
- 静态全局变量
static int globalVar = 20;在全局作用域中使用 static 关键字声明的变量,其作用域被限制在声明它的文件内,类似于静态文件作用域变量。
4、const关键字
在C语言中,const用于声明常量,即指示程序员所声明的标识符是不可修改的。const 关键字可以用在多种上下文中,包括变量声明、指针声明以及函数参数等。
- 常量变量
const int myConstant = 10;myConstant 被声明为一个常量,其值不能被修改。试图在后续的代码中对其进行赋值会导致编译错误。
- 指针和
const
const int *ptr1; // 指向常量整数的指针
int const *ptr2; // 同上,const位于int之前
int *const ptr3; // 常量指针,指向整数const int *ptr和int const *ptr都表示指向常量整数的指针,即通过这些指针不能修改所指向的整数的值。int *const ptr表示一个常量指针,其指向不能改变,但可以通过指针修改所指向的整数的值。
- 函数参数中的
const
void myFunction(const int param) {
// 函数参数是常量
}在函数参数中使用 const 关键字表示该参数是只读的,函数内部不能修改它的值。
- 常量指针
int myVar = 5;
int anotherVar = 10;
int *const ptr = &myVar; // 常量指针
*ptr = 7; // 合法,可以修改指针指向的值
ptr = &anotherVar; // 非法,不能修改指针的指向ptr 是一个常量指针,它的指向不能修改,但可以通过指针修改所指向的值。
5、extern关键字
在C语言中,extern 用于声明一个全局变量或函数,但不分配内存空间,而是表明这个标识符是在其他文件中定义的。extern 关键字的主要作用是告诉编译器在其他文件中有一个相同名字的变量或函数,并且在链接阶段将其连接起来。
- 全局变量的 extern 声明
在一个源文件中使用 extern 关键字声明一个全局变量,而不进行定义。这是为了告诉编译器该变量在其他文件中有定义。
// File1.c
int globalVar = 42; // 定义全局变量
// File2.c
extern int globalVar; // 在另一个文件中声明全局变量,不进行定义
int main() {
// 使用全局变量
printf("%d\n", globalVar);
return 0;
}- 函数的 extern 声明
同样地,可以使用 extern 关键字声明一个在其他文件中定义的函数。
// File1.c
void myFunction() {
// 函数定义
}
// File2.c
extern void myFunction(); // 在另一个文件中声明函数,不进行定义
int main() {
// 调用函数
myFunction();
return 0;
}- 头文件中的 extern 声明
通常,extern 声明会放在头文件中,以便在多个文件中共享变量或函数的声明,而不是在每个文件中都进行声明。这有助于保持一致性,避免重复代码。
// myHeader.h
extern int globalVar;
extern void myFunction();
// File1.c
#include "myHeader.h"
int globalVar = 42;
// ...
// File2.c
#include "myHeader.h"
void myFunction() {
// ...
}
// ...总的来说,extern 关键字用于说明标识符是在其他文件中定义的,它在多文件项目中起到了关键的作用,使得各个文件能够正确地引用和使用彼此定义的全局变量和函数。
6、volatile关键字
在C语言中,volatile 用于告诉编译器,该关键字修饰的变量的值可能会在未经明确告知的情况下被改变。这主要是为了防止编译器在优化过程中对这类变量的读写进行优化,以确保对它们的读写操作不会被编译器所优化掉。
- 硬件寄存器: 当一个变量被声明为
volatile时,编译器不会对它进行优化,因为该变量的值可能会在程序的执行过程中由硬件或其他并发的代码改变。
volatile int hardwareStatus;- 中断服务程序(ISR): 在中断服务程序中,对共享的变量进行读写时,为了确保编译器不会进行不必要的优化,通常会使用
volatile。
volatile int interruptFlag;- 多线程环境: 在多线程环境中,如果一个变量被多个线程访问并且可能被其他线程修改,使用
volatile可以确保每次访问都是从内存中读取而不是从寄存器中读取。
volatile int sharedVariable;但是需要注意的是,虽然 volatile 提供了对特殊情况的支持,但它并不是线程安全的解决方案。在多线程环境下,更推荐使用互斥锁、原子操作等机制来确保数据的正确同步。
示例用法:
#include <stdio.h>
int main() {
volatile int counter = 0;
while (counter < 10) {
// 一些可能改变 counter 值的操作
counter++;
}
printf("Counter: %d\n", counter);
return 0;
}7、32位系统下,C语言的基本类型有哪些?占用字节空间?
- 整数类型:
char: 1 字节short: 2 字节int: 4 字节long: 4 字节
- 浮点类型:
float: 4 字节double: 8 字节
- 指针类型:
- 所有指针类型(包括函数指针):4 字节
- 枚举类型:
enum: 4 字节
需要注意的是,上面的大小是一般情况下的典型取值。在不同的编译器、操作系统和编译选项下,这些大小可能会有所不同。此外,一些特殊的硬件平台或编译器可能对这些大小有其他的规定。
此外,C语言标准并没有规定确切的大小,而是定义了一些最小值。具体的大小在实际系统中取决于编译器和架构的实现。因此,在特定系统下,可以使用 sizeof 运算符来获取具体数据类型的大小。
例如:
#include <stdio.h>
int main() {
printf("Size of int: %lu bytes\n", sizeof(int));
printf("Size of float: %lu bytes\n", sizeof(float));
// 其他数据类型的大小...
return 0;
}8、全局变量与局部变量的区别
全局变量:
- 作用域:全局变量在整个程序中都是可见的,可以在程序的任何地方访问。
- 生命周期:生命周期从程序的启动到结束,全局变量一直存在。
- 存储位置:存储在全局数据区,也称为静态数据区。
- 默认值:未经显式初始化时,全局变量会被自动初始化为0(对于数值类型)或NULL(对于指针类型)。
- 关键字:使用关键字
extern可以在一个文件中声明另一个文件中定义的全局变量。
// 全局变量的声明和定义
int globalVar; // 声明
int anotherGlobalVar = 42; // 声明并初始化局部变量:
- 作用域:局部变量仅在定义它的代码块(通常是函数)中可见。
- 生命周期:生命周期仅在其所属的代码块执行期间,当代码块结束时,局部变量将被销毁。
- 存储位置:存储在栈上,每次函数调用时都会创建一个新的局部变量。
- 默认值:不会被自动初始化,它们的值取决于它们在定义时是否被显式初始化。
- 关键字:通常不需要额外的关键字,除非你想明确指定存储类别(例如
static)。
// 局部变量的例子
void myFunction() {
int localVar; // 局部变量
localVar = 10; // 赋值
// ...
}9、从代码到可执行二进制文件的过程
预编译:处理源代码中的伪指令和一些特殊字符,并对一些相关的代码进行替换,源文件经过预处理后的结果扩展为 .i 。
伪指令主要包括以下四个方面
宏定义指令,如#define Name TokenString,#undef等。对于前一个伪指令,预编译所要做的是将程序中的所有Name用TokenString替换,但作为字符串常量的Name则不被替换;对于后者,则将取消对某个宏的定义,使以后该串的出现不再被替换。
条件编译指令,如#ifdef,#ifndef,#else,#endif,等等。这些伪指令的引入使得程序员可以通过定义不同的宏来决定编译程序对哪些代码进行处理。预编译程序将根据有关的文件,将那些不必要的代码过滤掉。
头文件包含指令,如#include “FileName"或者#include 等。在头文件中一般用伪指令#define定义了大量的宏(最常见的是字符常量),同时包含有各种外部符号的声明。采用头文件的目的主要是为了使某些定义可以供多个不同的C源程序使用。因为在需要用到这些定义的C源程序中,只需加上一条#include语句即可,而不必再在此文件中将这些定义重复一遍。预编译程序将把头文件中的定义统统都加入到它所产生的输出文件中,以供编译程序对之进行处理。
特殊符号,预编译程序可以识别一些特殊的符号。例如在源程序中出现的LINE标识将被解释为当前行号(十进制数),FILE则被解释为当前被编译的C源程序的名称。预编译程序对于在源程序中出现的这些串将用合适的值进行替换。
预编译程序所完成的基本上是对源程序的“替代”工作。经过此种替代,生成一个没有宏定义、没有条件编译指令、没有特殊符号的输出文件。这个文件的含义同没有经过预处理的源文件是相同的,但内容有所不同。下一步,此输出文件将作为编译程序的输出而被翻译成为机器指令。
编译:检查语法并对代码进行优化,将文本文件 .i 翻译成 .s 文件,得到汇编语言程序。
经过预编译得到的输出文件中,将只有常量。如数字、字符串、变量的定义,以及C语言的关键字,如main,if,else,for,while,{,},+,-,*,\,等等。编译程序所要作得工作就是通过词法分析和语法分析,在确认所有的指令都符合语法规则之后,将其翻译成等价的中间代码表示或汇编代码。
汇编:将 .s 文件转换成机器语言指令也就是二进制代码,并将结果保存在目标文件 .o 中。
- 汇编过程实际上指把汇编语言代码翻译成目标机器指令的过程。对于被翻译系统处理的每一个C语言源程序,都将最终经过这一处理而得到相应的目标文件。目标文件中所存放的也就是与源程序等效的目标的机器语言代码。目标文件由段组成。通常一个目标文件中至少有两个段:
- 代码段,该段中所包含的主要是程序的指令。该段一般是可读和可执行的,但一般却不可写。
- 数据段,主要存放程序中要用到的各种全局变量或静态的数据。一般数据段都是可读,可写,可执行的。
链接:将所有的目标文件链接到一起形成可执行文件,分为动态链接和静态链接。
- 由汇编程序生成的目标文件并不能立即就被执行,其中可能还有许多没有解决的问题。例如,某个源文件中的函数可能引用了另一个源文件中定义的某个符号(如变量或者函数调用等);在程序中可能调用了某个库文件中的函数,等等。所有的这些问题,都需要经链接程序的处理方能得以解决。
- 链接处理可分为两种:
- 静态链接,将链接库的代码复制到可执行程序中,使得可执行程序体积变大。
- 动态链接,需要链接的代码放到一个共享对象中,共享对象映射到进程虚地址空间,链接程序记录可执行程序将来需要用的代码信息,根据这些信息迅速定位相应的代码片段。
10、strcpy与memcpy的区别
strcpy函数:用途: 主要用于复制字符串。
头文件:
#include <string.h>函数签名:
char *strcpy(char *dest, const char *src);功能: 将源字符串
src复制到目标字符串dest中,直到遇到空字符\0为止。返回值: 返回指向目标字符串
dest的指针。- C++
#include <string.h> int main() { char source[] = "Hello"; char destination[20]; strcpy(destination, source); return 0; }
memcpy 函数:
- 用途: 用于复制一段内存块,不仅限于字符串。
- 头文件:
#include <string.h> - 函数签名:
void *memcpy(void *dest, const void *src, size_t n); - 功能: 将源内存块的前
n个字节复制到目标内存块中。 - 返回值: 返回指向目标内存块
dest的指针。
#include <string.h>
int main() {
char source[] = "Hello";
char destination[20];
memcpy(destination, source, sizeof(source));
return 0;
}区别:
- 对象类型:
strcpy专门用于字符串复制。memcpy用于复制任意类型的内存块,不仅限于字符串。
- 目标空间:
strcpy需要足够的目标空间来容纳整个字符串,它会一直复制直到遇到源字符串的结束符\0。memcpy通过指定复制的字节数来确定目标空间的大小。
- 返回值:
strcpy返回指向目标字符串的指针。memcpy返回指向目标内存块的指针。
- 使用场景:
- 如果是字符串操作,通常使用
strcpy。 - 如果是通用内存块复制,使用
memcpy。
- 如果是字符串操作,通常使用
11、数组与指针
数组(Array):
- 定义: 数组是一组相同类型的元素的集合,这些元素在内存中是连续存储的。
int myArray[5]; // 定义一个包含5个整数的数组- 内存布局: 数组元素在内存中是按照顺序连续存储的,通过索引可以直接访问元素。
myArray[0] = 10; // 第一个元素
myArray[1] = 20; // 第二个元素- 大小固定: 数组的大小在定义时就确定,不能动态改变。
指针(Pointer):
- 定义: 指针是一个变量,存储另一个变量的内存地址。
int x = 10;
int *ptr = &x; // 定义一个指向整数的指针- 间接访问: 通过指针可以间接访问存储在特定地址的值。
printf("%d\n", *ptr); // 打印指针指向的值- 动态分配: 指针可以用于动态分配内存,通过
malloc、calloc或realloc等函数。
int *dynamicArray = (int *)malloc(5 * sizeof(int));- 大小不固定: 指针本身的大小是固定的,但它可以指向不同类型的变量,可以通过类型转换实现。
关系和区别:
数组名与指针: 数组名可以看作是指向数组首元素的指针,但数组名是常量指针,不可修改。例如,
int arr[5];中arr可以看作是int* const arr。数组参数: 在函数参数中,数组会被隐式地转换为指向其首元素的指针。函数原型可以写成
void myFunction(int arr[])或void myFunction(int *arr)。动态内存分配: 数组的大小在编译时确定,而动态分配的内存通过指针实现,大小可以在运行时确定。
操作灵活性: 指针具有更大的灵活性,能够在运行时动态地指向不同的内存区域,而数组的大小在编译时就已经确定,不够灵活。
12、运算符i++和++i的区别
i++(后缀自增):首先,取当前值,然后再自增。
其值是运算前的原始值,然后才执行自增操作。
在表达式中使用时,先使用当前值,然后再自增。
int i = 5;
int result = i++; // result = 5, i = 6++i(前缀自增):首先,自增,然后再取新值。
其值是运算后的新值,即执行自增操作后的值。
在表达式中使用时,先自增,然后使用新值。
int i = 5;
int result = ++i; // result = 6, i = 6区别在于自增操作的执行时机。如果在表达式中没有其他操作数依赖于自增变量的当前值,那么两者的效果是相同的。然而,如果有其他操作数依赖于自增变量的当前值,那么选择使用前缀自增或后缀自增将会产生不同的结果。
13、 const和define的区别
const:
- 类型检查:
const是有类型的,它可以限定变量的类型。 - 作用域: 有作用域规则,常常用于局部范围或全局范围。
- 编译时分配内存:
const定义的常量在编译时分配内存。 - 推荐使用: 更安全,因为有类型检查,通常在C++中推荐使用。
const int MAX_VALUE = 100;#define:
- 类型无关:
#define是类型无关的,它只是简单地进行文本替换。 - 无作用域: 宏定义没有作用域规则,它是全局的,影响到文件中所有后续的代码。
- 预处理时替换:
#define定义的常量是在预处理阶段进行文本替换。 - 潜在问题: 由于没有类型检查,可能导致宏定义的一些潜在问题。
#define MAX_VALUE 100区别总结:
- 类型检查:
const有类型检查,#define无类型检查。 - 作用域:
const有作用域规则,#define无作用域规则。 - 内存分配:
const在编译时分配内存,#define在预处理时进行文本替换,没有内存分配。 - 推荐使用: 在C++中更推荐使用
const,因为它更安全、更具可读性。
14、使用指针需要注意什么
- 未初始化指针: 在使用指针之前,务必将其初始化为合适的值或者
NULL。
int *ptr = NULL; // 初始化为 NULL 或者其他有效地址- 野指针(悬挂指针): 在指针指向的内存被释放后,指针仍然保留着对该内存的引用。为了避免这种情况,及时将指针置为
NULL或者悬挂的内存地址。
int *ptr = (int *)malloc(sizeof(int));
free(ptr);
ptr = NULL; // 避免野指针- 内存泄漏: 在动态分配内存后,确保及时释放以避免内存泄漏。
int *ptr = (int *)malloc(sizeof(int));
// 使用 ptr
free(ptr); // 释放内存- 越界访问: 避免使用指针越界访问数组或其他数据结构,这可能导致未定义的行为。
int arr[5];
int *ptr = arr;
for (int i = 0; i < 5; ++i) {
// 正确:*(ptr + i)
// 错误:arr[i] 或者 *(ptr + i + 1)
}- 类型不匹配: 指针的类型必须与所指对象的类型匹配,否则可能导致错误。
int x = 10;
double *ptr = &x; // 错误:类型不匹配- 空指针解引用: 在解引用指针之前,确保它不是空指针。
int *ptr = NULL;
// 错误:*ptr- 指针算术: 确保进行指针算术时不越界,否则可能导致未定义的行为。
int arr[5];
int *ptr = arr;
// 错误:ptr += 10- 指针的生命周期: 确保在指针使用完毕后,它所指向的对象仍然有效。避免在指针超出其作用域后继续使用。
int *getPointer() {
int x = 42;
return &x; // 错误:返回了一个局部变量的地址
}- 指针的复制: 复制指针并不会复制它指向的内存,而只是复制了地址。修改一个复制后的指针可能会影响原指针。
int x = 10;
int *ptr1 = &x;
int *ptr2 = ptr1;
*ptr2 = 20; // 这会修改 x 的值- const 指针和指针 const: 了解
const修饰符在指针中的不同用法,以及它们的含义。
const int *ptr1; // 指向常量的指针
int const *ptr2; // 同上
int *const ptr3; // 常量指针
const int *const ptr4; // 常量指针,指向常量- 多级指针: 在使用多级指针时,确保理解每一级指针所指向的内容。
int x = 10;
int *ptr1 = &x;
int **ptr2 = &ptr1;
printf("%d\n", **ptr2); // 输出 x 的值- 函数返回指针: 当从函数返回指针时,确保返回的指针不指向局部变量的地址。
int *getPointer() {
int x = 42;
return &x; // 错误:返回了一个局部变量的地址
}15、sizeof与strlen的区别
sizeof:
- 用途:
sizeof是一个运算符,用于获取数据类型或对象的字节大小。 - 参数: 可以接受任何数据类型或对象,包括基本数据类型、结构体、数组等。
- 计算: 在编译时计算,返回数据类型或对象所占用的字节数。
- 例子:
int arr[5];
size_t size_of_array = sizeof(arr); // 返回整个数组占用的字节数strlen:
- 用途:
strlen是一个函数,用于计算字符串的长度,即不包括字符串末尾的空字符\0。 - 参数: 必须是以空字符结尾的字符数组(字符串)。
- 计算: 在运行时计算,遇到第一个空字符时停止计算。
- 例子:
const char *str = "Hello";
size_t length_of_string = strlen(str); // 返回字符串的长度区别:
sizeof是一个运算符,用于获取数据类型或对象的字节大小,可以用于任何数据类型。strlen是一个函数,用于计算字符串的长度,必须接受以空字符结尾的字符数组。sizeof在编译时计算,返回数据类型或对象的字节数。strlen在运行时计算,遇到第一个空字符时停止计算,返回字符串的长度(不包括空字符)。
示例:
#include <stdio.h>
#include <string.h>
int main() {
int arr[5];
size_t size_of_array = sizeof(arr); // 返回整个数组占用的字节数
const char *str = "Hello";
size_t length_of_string = strlen(str); // 返回字符串的长度
printf("Size of array: %zu\n", size_of_array);
printf("Length of string: %zu\n", length_of_string);
return 0;
}16、头文件#ifndef/#define/#endif的作用
#ifndef(If Not Defined):
#ifndef 用于检查某个标识符是否已经被定义。如果该标识符尚未被定义,则执行后续的代码,否则跳过。
#ifndef HEADER_FILE_NAME_H
#define HEADER_FILE_NAME_H
// 头文件的内容
#endif // HEADER_FILE_NAME_H#define:
#define 用于定义一个标识符,通常用于创建头文件的标志符,防止头文件被多次包含。
#define HEADER_FILE_NAME_H
// 其他宏定义和头文件的内容#endif:
#endif 用于结束条件编译块,与 #ifndef 配套使用,表示条件编译的结束。
#endif // HEADER_FILE_NAME_H17、结构体和共用体的区别
- 内存分配方式:
- 结构体(
struct): 在结构体中,每个成员都有自己的内存空间,成员之间不共享存储空间。
struct Point {
int x;
int y;
};例子中,struct Point 占用的内存大小是 sizeof(int) + sizeof(int)。
- 共用体(
union): 在共用体中,所有成员共享同一块内存空间,共用体的大小为最大成员的大小。
union Data {
int intValue;
float floatValue;
char charValue;
};例子中,union Data 占用的内存大小为 max(sizeof(int), sizeof(float), sizeof(char))。
- 访问方式:
- 结构体: 可以同时访问结构体的所有成员,每个成员都有自己的地址。
struct Point myPoint;
myPoint.x = 10;
myPoint.y = 20;- 共用体: 只能访问当前活动的成员,因为共用体的所有成员共享同一块内存。
union Data myData;
myData.intValue = 42;
printf("%d\n", myData.intValue); // 正确
printf("%f\n", myData.floatValue); // 不正确,未定义行为- 使用场景:
- 结构体: 用于表示一个包含多个不同数据类型的记录,每个成员都有意义。
struct Person {
char name[50];
int age;
float salary;
};- 共用体: 用于节省内存,当多个成员中只有一个会被使用时。
union Status {
int errorCode;
char errorMessage[50];
};- 初始化:
- 结构体: 可以对结构体的各个成员进行独立初始化。
struct Point p = {10, 20};- 共用体: 可以对共用体的第一个成员进行初始化,其他成员会共享同样的内存。
union Data d = {42}; // 对第一个成员进行初始化