C/C++八股面试题(六)
目录:
1.请你说说动态链接与静态链接两者有什么区别?
2.请简单说说静态成员函数与普通成员函数的区别?
3.简述C++从代码到可执行二进制文件的过程?
4.全局变量和局部变量的区别?
5.C语言的基本类型有哪些(32位系统),占用字节空间?
6.头文件#ifndef/#define/#endif的作用?
内容:
1.请你说说动态链接与静态链接两者有什么区别?
动态链接的基本思想:
- 是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序。
动态链接
- 从链接时机看:动态链接是在程序运行时完成的,即程序在运行时加载外部的共享库(如
.dll
、.so
文件)。 - 从文件大小看:生成的可执行文件较小,因为它只包含调用库的引用,而不包含库的实际代码。
- 从性能看:动态链接可能会略微影响程序启动速度,因为需要在运行时加载和解析库文件。
- 从依赖上看:程序在运行时依赖外部的动态库,如果系统中缺少这些库或版本不匹配,程序可能无法正常启动或运行。动态链接的好处是库的更新不需要重新编译应用程序,只要保持接口一致即可。
静态链接的基本思想:
- 我们不可能将所有代码放在一个源文件中,所以会出现多个源文件,而且多个源文件之间不是独立的,而会存在多种依赖关系,如一个源文件可能要调用另一个源文件中定义的函数,但是每个源文件都是独立编译的,即每个.c文件会形成一个.o文件,所以就会有依赖关系,则需要将这些源文件产生的目标文件进行链接,从而形成一个可以执行的程序。这个链接的过程就是静态链接。
静态链接
- 从链接时机看:静态链接在编译阶段完成,生成的可执行文件中包含了所有依赖库的代码和符号表。
- 从文件大小看:生成的可执行文件相对较大,因为它包括了所有依赖的库文件。
- 从性能看:由于库的代码已经被直接包含在可执行文件中,因此程序的加载速度较快,运行时不需要查找和加载外部库。
- 从依赖上看:程序在运行时不依赖外部的库文件,减少了运行时出现版本不兼容的问题。但如果库有更新,程序需要重新编译。
总结对比
链接时机 | 编译时 | 运行时 |
可执行文件大小 | 较大 | 较小 |
启动速度 | 较快 | 较慢(需要加载共享库) |
依赖管理 | 运行时不依赖外部库 | 运行时依赖外部库 |
库更新 | 库更新需要重新编译程序 | 库更新无需重新编译程序 |
共享性 | 每个程序都有独立的库副本 | 多个程序可以共享相同的库 |
2.请简单说说静态成员函数与普通成员函数的区别?
访问对象
- 普通成员函数:普通成员函数是与类的实例(对象)关联的函数。它必须通过对象来调用,并且在函数内部可以访问该对象的所有成员变量和成员函数。在成员函数内部,可以使用
this
指针来访问对象的成员变量。obj.func()
这种调用可以理解为:myClass::func(&obj)
- 静态成员函数:静态成员函数是与类本身关联的,而不是与类的某个具体对象关联。它可以直接通过类名来调用,不需要先创建对象。
访问成员变量
- 普通成员函数:可以访问类的所有成员,包括静态成员和非静态成员包括私有成员。
- 静态成员函数:只能访问静态成员变量和静态成员函数。它不能访问非静态成员变量和非静态成员函数,因为静态成员函数在类的实例化对象未创建时就已经存在,它无法指向某个具体对象。
this
指针
- 普通成员函数:每个普通成员函数都有一个隐式的
this
指针,指向当前调用该成员函数的对象。通过this
指针,可以访问对象的成员变量和成员函数。 - 静态成员函数:静态成员函数没有
this
指针,因为它不是与某个对象关联的。因此,静态成员函数无法通过this
指针来访问成员变量和成员函数。
调用方式
- 普通成员函数:必须通过类的对象来调用,或者通过指针/引用访问。
class MyClass { public: void normalFunc() { // 普通成员函数 std::cout << "This is a normal function" << std::endl; } }; MyClass obj; obj.normalFunc(); // 通过对象调用
- 静态成员函数:可以通过类名直接调用,也可以通过对象调用,但推荐通过类名调用。
class MyClass { public: static void staticFunc() { // 静态成员函数 std::cout << "This is a static function" << std::endl; } }; MyClass::staticFunc(); // 通过类名调用 MyClass obj; obj.staticFunc(); // 也可以通过对象调用
总结对比
调用方式 | 必须通过对象来调用 | 可以通过类名或对象来调用 |
访问成员变量 | 可以访问所有非静态和静态成员 | 只能访问静态成员,不能访问非静态成员 |
指针 | 有
指针,指向当前对象 | 没有
指针 |
适用场景 | 当需要操作对象的状态或成员时使用 | 当需要与类本身相关,但不需要访问对象时使用 |
3.简述C++从代码到可执行二进制文件的过程
预处理
- 预处理是编译过程的第一步,它的主要任务是处理所有的预处理指令(如
#include
、#define
等),并生成一个纯净的 C++ 源代码文件。生成一个预处理后的.i
文件,其中包含了宏替换、头文件内容和注释已去除的代码。
编译
- 编译阶段的主要任务是将预处理后的源代码转换成汇编语言代码。编译器会对源代码进行语法分析、词法分析、语义分析等,生成对应的汇编语言代码。生成一个汇编语言文件(
.s
文件),其中包含了计算机能理解的低级汇编指令。
汇编
- 汇编阶段将生成的汇编语言代码转化为机器码(对象代码),并生成目标文件(
.o
或.obj
文件)。汇编器将汇编代码转换成机器语言代码,但尚未处理代码中可能存在的外部依赖(如函数调用和全局变量)。生成一个目标文件(.o
或.obj
),它包含机器代码和符号信息,但还未能完全独立执行。
链接
- 链接是将一个或多个目标文件与所需的库文件(如标准库、第三方库等)合并成一个完整的可执行文件的过程。链接分为两个阶段:静态链接和动态链接。生成一个完整的可执行二进制文件(通常是
.exe
文件,在 Unix 系统中是没有扩展名或.out
),此时的程序可以独立运行。
可执行文件
- 最终,经过链接生成的可执行文件是程序可以运行的形式。它包含了所有的代码、数据和必要的符号信息,操作系统可以将其加载到内存中执行。
总结:
预处理 | 处理宏定义、头文件包含、条件编译等 | 预处理后的源代码文件( |
编译 | 将源代码转化为汇编语言 | 汇编语言文件( |
汇编 | 将汇编语言转化为机器码 | 目标文件( |
链接 | 合并多个目标文件和库文件,解析符号 | 可执行文件(如 |
4.全局变量和局部变量的区别?
作用域
- 全局变量:全局变量是在函数外部声明的变量,它的作用域是整个程序,即在整个源文件或多个源文件中(如果是通过
extern
引用)都可以访问。 - 局部变量:局部变量是在函数或代码块内部声明的变量,它的作用域仅限于该函数或代码块内,无法在函数外部访问。
例子:
#include <iostream> int globalVar = 10; // 全局变量 void func() { int localVar = 5; // 局部变量 std::cout << "Local variable: " << localVar << std::endl; std::cout << "Global variable: " << globalVar << std::endl; } int main() { func(); // std::cout << "Local variable: " << localVar << std::endl; // 错误:无法访问局部变量 std::cout << "Global variable: " << globalVar << std::endl; // 正确:全局变量可以在任何地方访问 return 0; }
globalVar
是全局变量,可以在func()
和main()
中访问。localVar
仅在func()
中可访问,无法在main()
中访问。
初始化
- 全局变量:如果全局变量没有显式初始化,它会被自动初始化为类型的零值。例如,
int
类型的全局变量默认初始化为0
,指针类型默认初始化为nullptr
。 - 局部变量:局部变量在声明时不会自动初始化。如果局部变量没有显式初始化,它的值是未定义的,使用这些未初始化的变量将导致不可预期的行为。
例子:
#include <iostream> int globalVar; // 全局变量,自动初始化为 0 void func() { int localVar; // 局部变量,未初始化 std::cout << "Global variable: " << globalVar << std::endl; std::cout << "Local variable: " << localVar << std::endl; // 使用未初始化的局部变量是不安全的 } int main() { func(); return 0; }
- 在
func()
中,globalVar
被自动初始化为 0,而localVar
的值是未定义的,访问它可能导致未定义行为。
存储位置
- 全局变量:全局变量通常存储在程序的 静态存储区,即程序的全局数据区域。它们的内存分配和释放由操作系统在程序启动时进行,并在程序结束时释放。
- 局部变量:局部变量存储在 栈(stack) 中,随着函数的调用和返回,栈会相应地分配和释放内存。
生命周期
- 全局变量:全局变量的生命周期从程序开始时就开始,到程序结束时才结束。也就是说,它在程序运行期间始终存在,并且保持其值直到程序结束。
- 局部变量:局部变量的生命周期仅限于其所在的函数或代码块的执行期间。当函数或代码块被调用时,局部变量被创建并分配内存;当函数或代码块执行结束时,局部变量被销毁,内存释放。
例子:
#include <iostream> int globalVar = 10; // 全局变量 void func() { int localVar = 5; // 局部变量 std::cout << "Inside func: localVar = " << localVar << std::endl; } int main() { func(); // std::cout << "Outside func: localVar = " << localVar << std::endl; // 错误:无法访问局部变量 std::cout << "Global variable: " << globalVar << std::endl; return 0; }
- 在上面的例子中,
localVar
仅在func()
执行时存在,而globalVar
在程序整个运行期间都存在。
可见性和链接
- 全局变量:全局变量对所有函数和文件可见。如果它们在多个源文件中被引用,通常需要使用
extern
来声明它们。 - 局部变量:局部变量只能在其所在的函数或代码块内可见,不会影响到其他函数或代码块。
例子:
// file1.cpp #include <iostream> int globalVar = 10; // 在 file1 中定义全局变量 // file2.cpp extern int globalVar; // 使用 extern 声明 file1 中的 globalVar void func() { std::cout << "Global variable from file2: " << globalVar << std::endl; }
globalVar
在file1.cpp
中定义,并且通过extern
在file2.cpp
中声明,使得globalVar
在不同文件中可见。
线程安全性
- 全局变量:全局变量在多线程程序中如果没有正确的同步机制(如互斥锁)可能导致线程安全问题。多个线程同时访问和修改全局变量时,可能会出现竞态条件,导致不一致的结果。
- 局部变量:局部变量通常是线程安全的,因为每个线程都有自己的局部变量副本,互不干扰。
内存管理
- 全局变量:全局变量通常由操作系统自动管理,它们的生命周期持续到程序结束。内存空间通常不会被回收,直到程序退出。
- 局部变量:局部变量的内存由栈管理,当函数调用时,局部变量的内存被分配,当函数返回时,内存被销毁。
总结
作用域 | 整个程序(可以跨多个文件) | 仅限于函数或代码块内 |
生命周期 | 从程序开始到程序结束 | 从函数调用开始,到函数结束 |
存储位置 | 静态存储区(全局数据区) | 栈(函数调用栈) |
初始化 | 默认初始化为零值(如 | 不自动初始化,使用前必须初始化 |
可见性 | 可以通过 | 仅在声明的函数或代码块内可见 |
线程安全性 | 非线程安全(需要同步机制) | 线程安全(每个线程有独立副本) |
内存管理 | 由操作系统管理,直到程序退出 | 由栈管理,随着函数调用结束自动销毁 |
5.C语言的基本类型有哪些(32位系统),占用字节空间?
数据类型总结
| 1 字节 | 存储单个字符,通常有符号或无符号 |
| 2 字节 | 存储较小整数(-32,768 到 32,767) |
| 4 字节 | 存储整数(-2,147,483,648 到 2,147,483,647) |
| 4 字节 | 存储较大整数,范围与 |
| 8 字节 | 存储更大的整数,范围 -9E18 到 9E18 |
| 4 字节 | 存储单精度浮点数(6-7 位有效数字) |
| 8 字节 | 存储双精度浮点数(15-16 位有效数字) |
| 8 字节或更多 | 存储高精度浮点数,具体依平台 |
| 无大小 | 表示无类型,如函数返回类型 |
6.头文件#ifndef/#define/#endif的作用?
在 C 和 C++ 编程中,#ifndef
、#define
和 #endif
是常用的预处理指令,用于实现 条件编译 和 头文件保护。这三者通常一起使用,确保头文件在程序中只被包含一次,避免重复定义和编译错误。
#ifndef
#ifndef
它检查给定的宏是否没有被定义,如果没有定义,则执行随后的代码。如果宏已经被定义,则跳过这些代码。
#define
#define
用于定义宏(宏常量或宏函数)。宏通常在代码中进行替换,或者用于条件编译控制。此指令的作用是将一个标识符(宏名)与某个值关联起来。
#endif
#endif
用来结束#if
、#ifdef
或#ifndef
条件编译块。当条件成立时,包含在这些指令之间的代码才会被编译。
最常见的用法是在头文件中防止头文件的多重包含。多重包含可能导致重复定义和链接错误,特别是在大型项目中,多个源文件可能包含同一个头文件。为了避免这种情况,通常使用 #ifndef
和 #define
来防止头文件被重复包含。
例子:
#ifndef MY_HEADER_H // 如果 MY_HEADER_H 没有被定义 #define MY_HEADER_H // 定义 MY_HEADER_H // 头文件的内容 void myFunction(); #endif // 结束条件编译
#ifndef MY_HEADER_H
:检查MY_HEADER_H
是否已被定义。如果没有定义,继续执行下面的代码。#define MY_HEADER_H
:定义MY_HEADER_H
,表示该头文件已经被处理过了。#endif
:结束#ifndef
条件编译块。
这样做的结果是,每个头文件只会被包含一次。即使该头文件被多个源文件或其他头文件包含,也只会执行一次,这避免了重复定义和链接错误。
工作原理:
- 当编译器第一次处理该头文件时,
MY_HEADER_H
并没有被定义,因此会进入#ifndef
的代码块。 - 在代码块内,
#define MY_HEADER_H
被定义,此时MY_HEADER_H
已经存在。 - 如果该头文件在后续的编译过程中再次被包含,编译器会发现
MY_HEADER_H
已经定义过了,#ifndef
条件为假,头文件内的代码不会被再次处理,从而避免了重复定义。
本人双飞本,校招上岸广和通。此专栏覆盖嵌入式常见面试题,有C/C++相关的知识,数据结构和算法也有嵌入式相关的知识,如操作系统、网络协议、硬件知识。本人也是校招过来的,大家底子甚至项目,可能都不错,但是在面试准备中常见八股可能准备不全。此专栏很适合新手学习基础也适合大佬备战复习,比较全面。最终希望各位友友们早日拿到心仪offer。也希望大家点点赞,收藏,送送小花。这是对我的肯定和鼓励。 持续更新中