【面试宝典】C/C++ 基础
一. 语言基础
1.1 C++语言概览
简述C++语言特点
- C++有三大特性:封装、继承、多态
- C++语言编写出的程序结构清晰、易于扩充,程序可读性好
- C++生成的代码质量高,运行效率高,仅比汇编语言慢10%~20%
- C++更加安全,增加了const常量、引用、四类cast转换(static_cast、dynamic_cast、const_cast、reinterpret_cast)、智能指针、try—catch等
- C++可复用性高,C++引入了模板的概念,后面在此基础上,实现了方便开发的标准模板库STL(Standard Template Library)
- C++是不断在发展的语言。C++后续版本更是发展了不少新特性,如C++11中引入了nullptr、auto变量、Lambda匿名函数、右值引用、智能指针等
简述C语言和C++的区别
- C语言是面对过程的编程语言;C++是面对对象的编程语言
- C语言是C++的子集;C++可以很好兼容C语言,C++后续版本更是发展了不少新特性,如C++11中引入了nullptr、auto变量、Lambda匿名函数、右值引用、智能指针
- C语言有一些不安全的语言特性,如指针使用的潜在危险、强制转换的不确定性、内存泄露等;而C++对此增加了不少新特性来改善安全性,如const常量、引用、cast转换、智能指针、try—catch等
- C++可复用性高,C++引入了模板的概念,后面在此基础上,实现了方便开发的标准模板库STL。C++的STL库相对于C语言的函数库更灵活、更通用
简述面向对象七大设计原则
- ① 开闭原则 (The Open-Closed Principle ,OCP)
- 在进行面向对象设计中,设计类或其他程序单位时,软件实体(模块,类,方法等)应该遵循 “对拓展开放open,对修改关闭close” 这一设计原则
- 开闭原则可以提高程序的稳定性,让软件系统具有灵活的可扩展性,复用性高,且易于维护
- 软件系统的构建是一个需要不断重构的过程,模块的功能抽象,模块与模块间的关系,都不会从一开始就非常清晰明了,所以构建100%满足开闭原则的软件系统是相当困难的,但在设计过程中,通过对模块功能的抽象(接口定义),模块之间的关系的抽象(通过接口调用),抽象与实现的分离(面向接口的程序设计)等,可以尽量接近满足开闭原则
- ② 里氏代换原则 (Liskov Substitution Principle ,LSP)
- ③ 迪米特原则(最少知道原则)(Law of Demeter ,LoD)
- ④ 单一职责原则
- ⑤ 接口分隔原则 (Interface Segregation Principle ,ISP)
- ⑥ 依赖倒置原则 (Dependency Inversion Principle ,DIP)
- ⑦ 组合/聚合复用原则 (Composite/Aggregate Reuse Principle ,CARP)
1.2 XX 和 XX 的区别
简述C++中 struct 和 class 的区别
| struct | class | |
| 描述 | 数据结构集合 | 对象数据的封装 |
| 默认的访问控制权限 | public | private |
| 继承关系 | 公有继承 | 私有继承 |
| 定义模板参数 | 可以用于定义模板参数,就像 typename
int Func(const T& t, const Y& y) { //TODO } |
不能用于定义模板参数
template<typename T, typename Y> //可以把 typename 换成 class |
简述C++结构体和C结构体的区别
| struct(C) | struct(C++) | |
| 成员函数/静态成员 | 结构体内不允许有函数存在 | 允许有内部成员函数,且允许该函数是虚函数 |
| 默认的访问控制权限 | public,不能修改 | public / private / protected |
| 继承关系 | 不可以继承 | 可以从类或者其他结构体继承 |
| 初始化数据成员 | 不可以 | 可以 |
| 使用 | 使用结构体需要加上 struct 关键字,或者对结构体使用 typedef 取别名
struct Student{ int iAgeNum; string strName; } typedef struct Student Student2; //C中取别名 struct Student stu1; //C中正常使用 Student2 stu2; //C中通过取别名的使用 |
可以省略 struct 关键字直接使用
struct Student{ int iAgeNum; string strName; } typedef struct Student stu3; //C++ 中直接使用 |
简述include头文件双引号""和尖括号<>的区别
| include" " |
include< > |
|
| 文件类型 | 自定义文件 | 系统文件 |
| 查找路径 | 当前头文件目录-->编译器设置的头文件路径-->系统变量 | 编译器设置的头文件路径-->系统变量 |
简述静态全局变量,静态局部变量,全局变量,局部变量的区别
| 静态全局变量 | 静态局部变量 | 全局变量 | 局部变量(如函数的参数,函数内的局部变量等) | |
| 作用域 |
全局作用域+文件作用域 (无法在其他文件中使用) |
局部作用域 (只被初始化一次,直到程序结束) |
全局作用域 (可以通过extern作用于其他非定义的源文件) |
局部作用域 |
| 存储空间 |
全局区 (直到程序结束才会回收内存,所以下次调用函数的时候还是能取到原来的值) |
全局区 (直到程序结束才会回收内存,所以下次调用函数的时候还是能取到原来的值) |
全局区 (直到程序结束才会回收内存,所以下次调用函数的时候还是能取到原来的值) |
栈区 (出了作用域就回收内存,所以不要返回局部变量的地址) |
简述内联函数和宏的区别
| 内联函数(inline) | 宏函数(#define) | |
| 调用 | 内联函数的调用是传参(参数是有数据类型的,可以是各种各样的类型) | 宏是简单的字符串替换(注意是字符串的替换,不是其他类型参数的替换) |
| 内联函数在编译之后进行,因此占用的是执行的时间 | 宏在编译之前进行,因此占用的是编译的时间 | |
简述数组和指针的区别
| 数组 | 指针 | |
| 概念 | 是用于储存多个相同类型数据的集合。 数组名是首元素的地址 | 特殊的变量,存放的是其它变量在内存中的地址。 指针名指向了内存的首地址 |
| 赋值 | 只能一个一个元素的赋值或拷贝 | 同类型指针变量可以相互赋值 |
| 存放方式 | 连续存放(开辟一块连续的内存空间),根据数组下标访问 | 指针可以指向任意类型的数据,存储空间依变量所指向地址空间的内存而定 |
| sizeof | 变化。sizeof(数组名)/sizeof(数据类型) | 固定。4(32位平台),8(64位平台) |
1.3 简述题
简述C++从代码到可执行二进制文件的过程
- C++和C语言类似,一个C++程序从源码到执行文件,有四个过程:
- 预编译
- 删除所有的 #define,且展开所有的宏定义
- 处理所有的条件预编译指令,如 #if、#ifdef
- 处理 #include 预编译指令,将被包含的文件插入到该预编译指令的位置
- 过滤所有的注释
- 添加行号和文件名标识
- 删除所有的 #define,且展开所有的宏定义
- 编译
- 词法分析:将源代码的字符序列分割成一系列的记号
- 语法分析:对记号进行语法分析,产生语法树
- 语义分析:判断表达式是否有意义
- 代码优化
- 目标代码生成:生成汇编代码
- 目标代码优化
- 词法分析:将源代码的字符序列分割成一系列的记号
- 汇编
- 将汇编代码转变成机器可以执行的指令
- 将汇编代码转变成机器可以执行的指令
- 链接
- 将不同的源文件产生的目标文件进行链接,从而形成一个可以执行的程序
- 链接分为:
- 静态链接
- 在链接的时候就已经把要调用的函数或者过程链接到了生成的可执行文件中,就算删除静态库也不会影响可执行程序的执行
- 生成的静态链接库,Windows下以.lib为后缀,Linux下以.a为后缀
- 动态链接
- 在链接的时候没有把调用的函数代码链接进去,而是在执行的过程中,再去找要链接的函数,生成的可执行文件中没有函数代码,只包含函数的重定位信息,所以当删除动态库时,可执行程序就不能运行
- 生成的动态链接库,Windows下以.dll为后缀,Linux下以.so为后缀
- 静态链接
- 将不同的源文件产生的目标文件进行链接,从而形成一个可以执行的程序
简述 static 关键字的作用
- 修饰普通变量
- 局部变量:局部变量存放在栈区,随着函数的调用和返回被构造和析构,底层操作系统将该变量占用的内存空间给回收了
static int b = 2; //被static修饰的全局变量b的可见性会发生变化,其他文件将无法调用该全局变量,其余和普通全局变量没有区别
- 全局变量:全局变量加不加 static,除了可见性之外,没有什么区别。因为全局区的变量只会初始化一次,且在程序结束后被操作系统回收。这也就是为什么 static 修饰的变量的生命周期会和程序一样长的底层原理
void Func() { static int a = 0; //static修饰的局部变量a被初始化一次之后,每次函数调用都继续使用之前的值0,而不是重新进行初始化操作 }
- 局部变量:局部变量存放在栈区,随着函数的调用和返回被构造和析构,底层操作系统将该变量占用的内存空间给回收了
- 修饰普通函数
static int add(int a, int b) { return a + b; } //和修饰全局变量类似,被static修饰的普通函数只能在本文件内可以见,同一个程序的其他文件将无法调用该函数。
//可以在一定程度上解决命名冲突的问题,不过C++提供了namespace,所以一般不用于修饰普通函数 -
修饰成员变量:成员变量根据实例被声明的方式,如果是new关键字定义的就存放在堆区(堆区的对象,不会随着作用域的离开被析构,只能通过delete关键字手动释放或者程序结束后被操作系统自动回收),否则就在栈区
class Student { private: //static 修饰成员变量 c 后,该变量会属于该类,而不是某一个该类的对象(即:所有Student的对象共用这一个变量c) static int c; }; // static修饰的成员变量只能在类外初始化 int Student::c = 0; //需要通过类名+作用域:: + 变量名的方式进行调用 Studnet s1; cout << s1.count << '\n'; //会编译警告:Clang-Tidy: Static member accessed through instance 通过实例调用静态成员变量 cout << Studnet::count << '\n'; // ok class Student2 { private: // C++17之后可以通过inline的方式在类内初始化 static inline int c = 0; };
- 修饰成员函数
class Studnet { public: static int init(int number1, int number2) { age = number1; // 编译报错 Invalid use of member 'age' in static member function count = number2; // ok } private: static inline int count = 0; int age = 18; }; //和成员变量一样,使用 static 修饰的成员函数的生命周期和使用方式都发生了变化 //通过static修饰的函数,如果访问非 static 成员变量,编译器会直接报错
简述静态变量什么时候初始化?
-
在整个程序编译的初期,main()函数执行之前(或者说main中第一个用户语句执行之前)
-
静态全局变量 :全局作用域 + 文件作用域,所以无法在其他文件中使用。
静态局部变量 :局部作用域,只被初始化一次,直到程序结束。
类静态成员变量:类作用域。
简述函数指针,如何定义?有什么使用场景?
- 函数指针:就是指向函数的指针变量。每一个函数都有一个入口地址,该入口地址就是函数指针所指向的地址
- 定义函数指针:
int Func(int a); //一个函数Func int (*f)(int a); //一个指针变量f f = &func; //指向函数的指针
- 函数指针的应用场景:回调(callback)(调用别人提供的 API函数(Application Programming Interface,应用程序编程接口),称为Call;如果别人的库里面调用我们的函数,就叫Callback)
//以库函数qsort排序函数为例,它的原型如下: void qsort(void *base,//void*类型,代表原始数组 size_t nmemb, //第二个是size_t类型,代表数据数量 size_t size, //第三个是size_t类型,代表单个数据占用空间大小 int(*compar)(const void *,const void *) //第四个参数是函数指针,这个参数告诉qsort,应该使用哪个函数来比较元素 ); //即:只要我们告诉qsort比较大小的规则,它就可以对任意数据类型的数组进行排序,在库函数qsort调用我们自定义的比较函数,这就是回调的应用。 //示例 int num[100]; int cmp_int(const void* _a , const void* _b) {//参数格式固定 int* a = (int*)_a; //强制类型转换 int* b = (int*)_b; return *a - *b; } qsort(num,100,sizeof(num[0]),cmp_int); //回调
简述 nullptr 是否可以调用成员函数?为什么?
- 可以。
- 在编译时对象就绑定了函数地址,和指针空不空没关系
class animal { public: void sleep() { cout << "animal sleep" << endl; } void breathe() { cout << "animal breathe haha" << endl; } }; class fish :public animal { public: void breathe() { cout << "fish bubble" << endl; } }; int main() { animal* pAn = nullptr; pAn->breathe(); // 输出:animal breathe haha fish* pFish = nullptr; pFish->breathe(); // 输出:fish bubble return 0; } //pAn->breathe();编译的时候,函数的地址就和指针pAn绑定了,调用breath(*this), this就等于pAn。 //由于函数中没有需要解引用this的地方,所以函数运行不会出错,但是若用到this,因为this=nullptr,运行出错
简述什么是野指针,怎么产生的,如何避免?
- 概念:野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
- 产生原因:释放内存后指针不及时置空(野指针),依然指向了该内存,那么可能出现非法访问的错误。这些我们都要注意避免。
char *p = (char *)malloc(sizeof(char)*100); strcpy(p, "Douya"); free(p); //p所指向的内存被释放,但是p所指的地址仍然不变 ... if (p != NULL) //没有起到防错作用 { strcpy(p, "hello, Douya!"); //出错 }
- 避免办法:
- 初始化置NULL
- 申请内存后判空
- 指针释放后置NULL
- 使用智能指针
int *p = NULL; //初始化置NULL p = (int *)malloc(sizeof(int)*n); //申请n个int内存空间 assert(p != NULL); //判空,防错设计 p = (int *) realloc(p, 25);//重新分配内存, p 所指向的内存块会被释放并分配一个新的内存地址 free(p); p = NULL; //释放后置空 int *p1 = NULL; //初始化置NULL p1 = (int *)calloc(n, sizeof(int)); //申请n个int内存空间同时初始化为0 assert(p1 != NULL); //判空,防错设计 free(p1); p1 = NULL; //释放后置空 int *p2 = NULL; //初始化置NULL p2 = new int[n]; //申请n个int内存空间 assert(p2 != NULL); //判空,防错设计 delete []p2; p2 = nullptr; //释放后置空
二. 内存管理


三. 面向对象
四. STL
五. C++新特性
继续面经,继续更新...

浙公网安备 33010602011771号