C++面试《葵花宝典》
一、C++语言基础
1.1 C++三大特性
封装、继承、多态
1.2 struct与class
控制权限
- Struct默认访问控制权限为public
- Class默认访问控制权限为private
是否允许有成员函数
- C中struct不允许有函数
- C++中的struct几乎拥有class的一切属性
C 、C++中使用结构体的区别
- C中使用结构体
struct Student stu1
- C++中使用结构体
Student stu1
1.3 include ‘’ ’’ 和 <> 的区别
- 在预编译阶段查找头文件的路径不一样
<>: 编译器设置的头文件路径 -> 系统变量
‘’ ’’:当前头文件目录 -> 编译器设置的头文件路径 -> 系统变量
1.4 导入C函数的关键字extern “C”
- extern “C”
- extern “C” int strcmp(const char *s1, const char s2);
1.5 预编译、编译、汇编、链接
- 预编译:处理#define、#include等预编译指令,将文件插入该位置。过滤所有注释
- 编译:生成汇编代码
- 汇编:将汇编代码转变成可执行的指令
- 链接:将目标文件进行链接,从而形成可执行程序
- 静态链接:在链接的时候就已经吧要调用的函数或过程链接到生成的可执行文件中
- 动态链接:生成的可执行文件没有函数代码,只有重定位信息
1.6 static
内存分配
- 内存分配在堆上,其值可改变
- 对于C而言,static在编译时就分配内存
- 对于C++而言,static在首次使用时分配内存
- 在程序结束时释放内存
使用方式
- 静态函数、静态变量都只能在本源文件中使用
- 类的静态变量、函数被所有对象共用
- 静态成员函数没有this指针,不可以通过非静态成员访问
1.7 数组名与指针
意义
- 数组名是首元素的地址
- 指针是一个变量,其值为一个内存地址
内存大小
- 数组所占的内存大小:sizeof(数组名)/sizeof(数据类型)
- 指针的大小与系统有关,32位是4byte;64位是8byte
1.8 函数指针
- 就是每一个函数的入口地址
- 定义:
int func(int a);
int (*f)(int a);
f = &func;
- 可以用于:使用同一的方式调用不同的方法
1.9 野指针
- 野指针就是指针指向的位置是不正确的,不符合预期的
- 例如,某指针指向的内存被释放后,该指针没有置空,而是继续指向该内存。若这块内存被填入了新的数据,那么指针就出现了非法访问的错误,是野指针
- 规避野指针,可采用智能指针
1.10 malloc和free
malloc函数是一种分配长度为num_bytes字节的内存块的函数,可以向系统申请分配指定size个字节的内存空间
void* malloc(long NumBytes)
- 该函数分配了NumBytes个字节,并返回了指向这块内存的指针。
- 如果分配失败,则返回一个空指针(NULL)。
- 返回类型是 void* 类型。void* 表示未确定类型的指针。
- C,C++规定,void* 类型可以通过类型转换强制转换为任何其它类型的指针。
malloc的用法
include <malloc.h>
char* p = (char*)malloc(100*sizeof(char));
If (p == NULL)
exit(1);
free(P);
P = NULL;
1.11 const与define
- 它们定义的都是常量,其值不可再改变
- define在预处理阶段生效,const在编译阶段生效
- define是宏定义替换,常量不在内存中
- const定义的常量存放在内存中
- define常量不带类型,const常量带类型
1.12 使用指针的注意事项
- 初始化置NULL
- 使用new、malloc分配内存后立刻判空NULL
- 内存申请与释放必须配对
- 指针释放后置NULL
1.13 内联函数inline
- 内联函数在编译时将代码嵌入
- 内联函数不需要寻址,避免了函数调用的开销
- 内联函数要求代码简单,无循环
1.14 C++值传递的方式
“值传递”是说 调用函数传参 的这个过程
- 值传递:形参改变,不影响实参
- 指针传递:形参改变,影响实参
- 应用传递:形参改变,影响实参
二、C++内存
2.1 堆和栈
- 分配空间方式不同。栈由操作系统自动分配释放,堆由程序员主动分配释放
- 缓存方式不同。栈使用一级缓存,调用完毕立即释放。堆使用二级缓存
- 数据结构不同。堆类似数组,栈类似FILO栈结构
2.2 C++的5个内存区
- 堆
- 栈
- 自由存储区:类似堆,由malloc分配
- 全局/静态存储区:存放全局变量和静态变量static
- 常量存储区:存放常量,不允许修改
2.3 常见的内存错误
内存分配失败,但程序员不知
- 初始化置NULL
- 使用new、malloc分配内存后立刻判空NULL,依然为NULL则分配失败
内存未被初始化就被引用
- 要为动态内存或数组赋初值
内存越界
- 注意使用数组时的下标问题
释放内存后,形成了野指针
- 释放内存后,立即将指针置空
内存泄漏
-
内存泄露的两种情况:
- new / malloc申请内存后没有释放
- 继承时,父类析构函数不是虚函数
-
如何避免:
- 养成良好习惯,使用完内存后立即释放
- 将分配内存的指针以链表的形式进行管理,释放内存后从链表删除
- 程序结束后检查链表
- 可以使用智能指针
2.4 为什么父类析构函数一定要是virtual
- 首先,子类在父类的基础上,可能新申请了一些动态内存
- 所以,子类的析构函数要比父类的多一些释放操作
- 我们使用C++继承时,常常用基类指针,指向子类对象,如:Parent* p = new Child();
- 若父类析构函数不为virtual,执行delete p时,调用的就是父类析构函数
- 这会导致少了一些操作,一些子类的内存没有被释放
- 也就是说,父类析构函数不为virtual,会导致内存泄漏
- (但是,C++默认的析构函数不是虚函数)
2.5 程序运行时内存有哪些section
在执行时,从低地址到高地址:
- 代码段:存放代码
- 数据段:已初始化的全局和静态变量
- BSS段:未初始化(或初始化为0)的全局和静态变量
- 堆:动态内存
- 共享区:位于堆栈之间
- 栈:局部变量等
2.6 程序启动的过程
- 首先,操作系统创建进程并分配空间
- 加载器把可执行文件的代码与数据段映射到虚拟内存空间中
- 加载器读入“导入符号表”,从而查找所依赖的动态链接库,并调用
- 初始化应用程序的全局变量或全局对象
- 进入应用程序入口函数,开始执行
2.7 内存对齐
什么是内存对齐
- Struct、class、union的各数据按照一定的规则在空间上排列,而不是简单的顺序存放
为什么要内存对齐
- 提高cpu的访问速率
- 一些平台对内存对齐有严格要求
对齐原则
- Struct、class、union第一个数据成员放置在offset为0的地方
- 以后每个数据成员存储的起始位置,都要从该成员大小的整数倍开始
- Struct的总大小(sizeof),必须是其内部“最宽基本类型成员”的整数倍。不足的要补齐
三、面向对象
3.1 面向对象的三大特征
(就是C++的三大特性)封装、继承、多态
- 封装:将数据与操作数据的方法结合,对外隐藏对象的属性和实现细节,仅提供对外接口用以交互
- 继承:在无需重新编写原来的类的情况下对类的功能进行拓展
- 多态:用父类指针指向子类对象,从而调用子类对象的成员函数(重写、重载)
- 静态多态(重写)
- 动态多态(重载)
3.2 重写与重载
重写和重载是什么:
- 重写:(对于继承而言)在派生类中重新定义函数。
- 重新定义的函数只是函数体不同,参数、返回值等都相同。
- 被重写的函数在基类中必须用virtual声明。
- 多态会调用派生类中重写的函数。
- 重载:(对函数而言)用一个函数名来定义多个函数
- 这些函数的参数、返回值类型可能不同。
- 多态会根据参数列表确定调用哪个函数。
重写和重载是如何实现的:
- 重写:是用虚函数表来实现。
- 存在虚函数的类,都有一个虚函数表。
- 类的对象都有一个虚函数指针。
- 在调用时,该指针在表中查询对应的虚函数。
- 重载:是通过命名倾轧来实现的。
- C++通过命名倾轧,在编译阶段,修改重载函数的函数名(添加参数类型和返回类型作为函数编译后的名称)。
3.3 构造函数
默认构造函数
- 无参
- 自动提供,也可以自己定义
- 若定义了构造函数,则不自动提供
初始化构造函数
- 有参
- 可重载
- 可写成函数体形式
- 也可写成初始化列表形式
Student(string s, int n) : name(s), number(n) {}
移动构造函数
- 有参
- 用于将其他类型的变量,隐式转换为本类对象
拷贝构造函数
- 有参
- 自动提供的是浅拷贝,也可自己定义
- 拷贝构造函数参数必须是引用传递
- 可以显式调用拷贝构造函数 Student s1(s2);
- 也可以隐式调用拷贝构造函数 Student s1 = s2;
- 为了实现隐式调用,类会默认生成一个 = 的运算符重载
3.4 深拷贝和浅拷贝
- 深拷贝和浅拷贝是对指针而言的
- 浅拷贝(值拷贝)后的指针,仍指向原内存地址,造成指针悬挂
- 深拷贝后的指针,会新开辟一块存储空间,并指向新空间
- 当类中存在指针时,必须定义深拷贝的拷贝构造函数
3.5 什么时候构造函数必须使用初始化列表
- 派生类想要调用基类构造函数时
class A {
private:
int a;
public:
A() {}
A(int n) : a(n) {}
}
// 派生类想要调用基类构造函数,对部分成员初始化
class B : public A {
private:
int b;
public:
B() {}
B(m, n) : A(m), b(n) {}
}
- 类的成员为另一个类对象
class B {
private:
int x;
public:
B(int n) : x(n) {}
}
// A 必须使用初始化列表
class A {
private:
B b;
public:
A() : b(0) {}
A(int n) : b(n) {}
}
- 类中有const成员时
class A {
private:
const int x;
public:
A() : x(0) {}
A(int n) : x(n) {}
}
- 类中有引用成员时
class A {
private:
int &x;
public:
A() : x(0) {}
A(int n) : x(n) {}
}
3.6 派生类调用构造函数的顺序
- 基类构造函数 -> 成员类对象构造函数 -> 自身构造函数
- 若为多重继承,则调用基类构造函数的顺序,是基类在派生表中出现的顺序
3.7 移动构造函数
- 移动构造函数与拷贝类似,也是使用一个对象的值设置另一个对象
- 移动实现真正的转移:源对象将丢失其内容
- 什么时候使用:当使用临时(未命名)变量对对象进行初始化的时候
3.8 模板实例化
显式实例化
template<class T>
struct Student {
string name;
T cores;
}
template struct Z<int>;
- 显式实例化只允许出现一次
隐式实例化
template<class T>
struct Student {
string name;
T cores;
}
Student<float> stu1;
3.9 模板具体化
- 当模板实例化后不能满足需要时,可以考虑具体化
template<class T>
struct Student {
string name;
T cores;
}
template<>
stuct Student<double> {
sting name;
double cores;
int grade;
}
3.10 常函数
- 常函数就是成员函数后面加const
Class A {
private:
int a;
public:
A() {};
void show() const;
}
- 常函数不改变对象数据成员的值
- 常对象可以调用常函数,不能调用非const函数
3.11 虚继承
-
虚继承是解决同源多重继承问题(菱形继承)的一种手段
-
菱形继承的问题:基类中有多个基类数据副本、存在二义性
class A {}
class B : virtual public A {}
class C : virtual public A {}
class D : public B, public C {}
-
B和C同源继承A,使用virtual
-
这样D继承B、C时,就只保留一份基类数据
3.12 纯虚函数
- 在基类(抽象类)中用 = 0 来申明
- 在抽象类中没有被定义,且要求所有派生类必须实现(重写)此纯虚函数
- 也就是说,纯虚函数提供一个统一的接口,被用来规范派生类的行为
- 抽象类不能生成对象
3.13 仿函数
- 仿函数又称函数对象,是一个能行使函数功能的类
- 可以像使用函数一样,使用函数对象(仿函数)
- 仿函数(类)必须重载 () 运算符
class Func {
private:
int index;
public:
Func() {}
void operator () (int n) {
index = n;
cout << index;
}
}
// 像使用函数一样使用仿函数(类)
Func fun();
fun(0);
- 仿函数与函数一样方便使用,也能提供更强大的功能
3.14 C++中哪些函数不能被声明为virtual
-
普通函数(非成员函数)
-
静态成员函数
-
内联成员函数
-
构造函数
-
友元函数
四、STL
4.1 STL的组件
- 容器:各种数据结构,如vector、list、deque、set、map。以模板类的方式提供
- 算法:对容器中的数据执行操作的模板函数。如sort()、find()。在std命名空间中
- 迭代器:一种访问容器的方法,可以理解为指针。算法借助迭代器访问容器
- 仿函数:一种类,可以像使用函数一样来使用。可以理解为高级的运算符重载
- 容器适配器:一种接口类,封装了一些基本的容器,如stack、queue等
- 空间配置器:自动配置和管理内存空间。使用STL时无需手动管理内存
4.2 常见的容器
序列容器
- vector向量
- list列表(双向链表)
- deque双端队列(双向队列)
- 元素在容器中的位置与元素值无关,即容器是非排序的
排序容器
- set集合
- multiset多重集合
- map映射(红黑树)
- multimap多重映射
- 元素是排序好的(按键排序),查找性能好
哈希容器
- unordered_set哈希集合
- unordered_multiset哈希多重集合
- unordered_map哈希映射
- unordered_multimap哈希多重映射
- 底层由哈希函数实现,查找性能很好
4.3 排序容器和哈希容器的时间复杂度
- 排序容器 O(logN)
- 哈希容器 O(1),最坏情况为O(N)
- 排序容器使用红黑树实现,红黑树是一颗非平衡的搜索二叉树
4.4 迭代器和指针
- 迭代器是类模板,表现得像指针
- 迭代器重载了一些操作符 -> ++ 等,它可以像指针一样使用
- 迭代器返回对象的引用
4.5 使用迭代器删除元素的注意事项
对于序列容器:
- 使用erase,其后每个元素都向前移动迭代器失效
- erase返回下一个有效的迭代器
对于关联容器:
- 使用erase,其他元素不会移动,仍有效
- 调用erase前,记录下一个元素的迭代器
4.6 STL容器动态链接可能产生的问题
- 主要是内存问题
- 在动态链接库函数中使用容器,参数只能传递容器的引用
- 容器不能超出其初始大小,否则会导致内存堆栈的破坏
4.7 vector增删元素
- 增加:若预分配空间已满,vector将申请一个两倍或三倍的空间,将原内存中的数据拷贝到新空间中去,再增加新元素
- 删除:删除元素是通过迭代器调用了erase,这个操作并不释放内存,而是将元素置为初始值。在容器析构时才释放内存。
4.8 push_back 和 emplace_back
- push_back先构造临时对象,再将对象拷贝到容器末尾
- emplace_back直接在容器末尾构造对象
五、C++11新特性
5.1 C++11有哪些新特性
语法改进
- 成员变量可以默认初始化
struct Student {
string name;
int cores = 0;
};
- 加入auto关键字
auto num = 10.0; // 使用auto必须进行初始化
-
加入智能指针
shared_ptr
- 多个
shared_ptr
可以指向同一块内存 - 使用计数器机制,引用计数器为0时,内存才被释放
- 多个
-
加入空指针
nullptr
替代NULL
nullptr
不会像NULL一样被编译器认做0- 另外,nullptr是一个nullptr_t类的一个对象
- 可以隐式转换为其他任何指针
int* a = nullptr;
-
加入基于范围的for循环
for (int num : nums) {...}
- 允许右值引用
int&& a = 10; // &&右值引用
a = 100; // 且可对右值进行修改
标准库STL扩充
-
加入哈希容器
unorder_map
\unordered_multimap
\unorder_set
\unordered_multiset
-
加入正则表达式
- 实质上是一个字符串,描述了一种特定模式的字符串
-
加入Lambda匿名函数
//[外部遍历访问方式说明符](参数)mutable noexcept/throw() -> 返回值类型
[>] (int x, inty) -> bool {
return (x > y) ? true : false;
}
// 匿名函数可以用作谓词函数,在STL中使用
5.2 智能指针
- 智能指针
unique_ptr
/shared_ptr
/weak_ptr
- 智能指针是一个类模板,超出作用域时调用析构函数自动释放内存
unique_ptr
- 独占式指针,保证同一时间只有一个智能指针指向该内存
unique_ptr<string> str1(new string(“Hello World!”)); // allow
unique_ptr<string> str2 = str1; // not allowed
- 可以使用move函数,将unique_ptr的独占性转移
unique_ptr<string> str1, str2;
str1 = demo(“Hello World!”)
str2 = move(str1); // allowed
shared_ptr
- 共享式指针,同一时间可以有多个指针指向该内存
- 通过计数器机制,引用计数器为0时,内存被释放
- shared_ptr可以通过传入其他智能指针来构造(直接赋值)
- 使用release()时,会释放该指针所有权
- 成员函数use_count返回引用计数器个数
- 成员函数unique返回是否引用计数器为1
weak_ptr
- 指向一个share_ptr管理的内存,不控制该内存的生命周期
- weak_ptr只作为访问手段,它不会引起引用计数器的改变
- weak_ptr的引入是为了解决两个shared_ptr相互引用的死锁问题(内存泄露)
- 不能通过weak_ptr直接访问对象的方法,必须将其转为shared_ptr
5.3 C++的右值引用
-
一般来说,不能取地址的表达式,就是右值。右值一般没有名称
-
C++98不允许对右值进行引用
int a = 10;
int& b = a; // allowed
int& c = 10; // not allowed
- C++98允许以const常量的方式引用右值,此时不能对右值进行修改
const int& a = 10; // allowed
a = 100; // not allowed
- C++11增加了&&右值引用,且可以对右值进行修改
int&& a = 10; // allowed
a = 100; // allowed
5.4 shared_ptr的多线程安全性
- 多线程环境下,读操作是安全的
- 写操作可能会产生冲突,需要进行“加锁”
5.5 C++11的四种类型转换
const_cast
:将const转为非conststatic_cast
:最常用。可将非const转为const,也可以用于类向上转换dynamic_cast:用于含虚函数的类转换reinterpret_cast
:可以做任何类型的转换,但不安全
5.6 auto的具体用法
用于定义变量。
- auto只是一个占位符,在编译期间被编译器推导出的类型所取代
auto cores = 10;
用于定义迭代器
vector<int> nums;
for (auto it = nums.begin(); it != nums.end(); it++) {...}
-
用于泛型编程
-
auto与const结合使用,可自动判断是否为const
- 当类型不为引用时,auto不保留const属性
- 当类型为引用时,auto保留const属性
5.7 C++11的可变参数模板
- 模板函数(或模板类)可以接受数量可变的参