C++说爱你不容易-6
C++软件与嵌入式软件面经解析大全(蒋豆芽的秋招打怪之旅)
本章讲解点
- 1.1 C++与C的区别——看看你的理解是否深刻
- 1.2 从代码到可执行文件的过程
- 1.3 extern "C"
- 1.4 宏——到底是什么
- 1.5 内联函数
- 1.6 条件编译
- 1.7 字节对齐详解
- 1.8 Const——今天必须把它搞懂
- 1.9 Static作用
- 1.10 volatile和mutable
- 1.11 volatile在嵌入式里的应用
- 1.12 原子操作
- 1.13 指针与引用的区别
- 1.14 右值引用
- 1.15 面向对象的编程思想
- 1.16 类
- 1.17 类的成员
- 1.18 友元函数
- 1.19 初始化列表
- 1.20 this指针
- 1.21 继承
- 1.22 多态
- 1.23 虚函数与重写
- 1.24 虚构造函数与虚析构函数
- 1.25 函数重载
- 1.26 操作符重载
- 1.27 迭代器与指针
- 1.28 模板
- 1.29 C++智能指针
- 1.30 四种cast转换
- 1.31 Lambda
- 1.32 function和bind
受众:本教程适合于**C/C++**已经入门的学生或人士,有一定的编程基础。
本教程适合于互联网、嵌入式软件求职的学生或人士。
故事背景
**蒋 豆 芽:**小名豆芽,芳龄十八,蜀中人氏。卑微小硕一枚,科研领域苟延残喘,研究的是如何炒好一盘豆芽。与大多数人一样,学习道路永无止境,间歇性踌躇满志,持续性混吃等死。会点编程,对了,是面对对象的那种。不知不觉研二到找工作的时候了,同时还在忙论文,豆芽都秃了,不过豆芽也没头发啊。
**隔壁老李:**大名老李,蒋豆芽的好朋友,技术高手,代码女神。给了蒋豆芽不少的人生指导意见。
**导 师:**蒋豆芽的老板,研究的课题是每天对豆芽嘘寒问暖。
故事引入
导 师:豆芽,论文要继续加油,知道吗?
蒋 豆 芽:好的!(豆芽脸上笑嘻嘻)
隔壁老李:豆芽,最近怎么没面试啊?我看你挺闲的啊。
蒋 豆 芽:(疑惑)嗯?老李,我哪里闲了啊,忙着复习。
隔壁老李:(笑容邪魅)豆芽,怎么样?大公司投了没?你不是想去大厂吗?
蒋 豆 芽:(支支吾吾)没诶,感觉没复习好,不敢投。
隔壁老李:(安慰)豆芽,你不要有畏难心理。你一定要明白,机遇不会等我们准备好了才来,往往是我们还没准备好就来了,或者准备的过程中就来了,不管有没有准备好,我们都得硬着头皮冲。
说来也很有意思,最近看了部电影叫面对巨人,里面我印象最深刻的就是教练说:你只管尽全力去做,剩下的交给上帝评判。
经历过秋招的人都深有感触,找工作除了实力以外,也有玄学的味道在里面,当然什么意思,你自己去经历就知道了。总而言之,投简历,赶紧投,提前批也不要错过!任何机会不要错过,然后就是好好准备,尽全力准备!剩下的交给天命,而努力的人运气都不会太差哦!
蒋 豆 芽:(疑惑)真的吗?
隔壁老李:(肯定)嗯嗯。
老李苦口婆心,豆芽还能说什么呢?那就投吧,豆芽所有大厂都投了。紧接着豆芽就收到了腾慢公司的面试邀请了。。。。。。
隔壁老李:怎么样啊?豆芽,昨天的面试可还行?
蒋 豆 芽:原来大公司的面试也不难嘛!都是基础知识啊!之前可是吓死我了。我感觉自己回答的不错,但是不知道为什么依然没过。
隔壁老李:(笑容邪魅)所以我才说面试偶尔有点“玄学”的味道,哈哈。
蒋 豆 芽:原来是这么个意思,好吧。不过已经结束了,今天我又收到了腾慢另一个部门的面试邀请,依然要好好准备,说不定就真的过了呢?哈哈。
不过我得复盘一下了。昨天的面试有三个问题我印象挺深刻的。
隔壁老李:哦?是哪些问题呢?
1.27 迭代器和指针
蒋 豆 芽:迭代器和指针有什么区别?
隔壁老李:这个豆芽你应该很清楚的,你再来回顾一下吧。
蒋 豆 芽:好,没问题!指针我们之前已经学过了,所以从迭代器说起。
迭代器是一个变量,从属于STL六大成分之一,这里我们简单介绍下STL,STL 是“Standard Template Library”的缩写,中文译为“标准模板库”。STL 是 C++ 标准库的一部分,不用单独安装。C++ 对模板(Template)支持得很好,STL 就是借助模板把常用的数据结构及其算法都实现了一遍,并且做到了数据结构和算法的分离。例如,vector 的底层为顺序表(数组),list 的底层为双向链表,deque 的底层为双端队列,set 的底层为红黑树,hash_set 的底层为哈希表。
通常认为,STL 是由容器、算法、迭代器、函数对象、适配器、内存分配器这 6 部分构成,其中后面 4 部分是为前 2 部分服务的,它们各自的含义如表所示。
STL的组成 | 含义 |
---|---|
容器 | 一些封装数据结构的模板类,例如 vector 向量容器、list 列表容器等。 |
算法 | STL 提供了非常多(大约 100 个)的数据结构算法,它们都被设计成一个个的模板函数,这些算法在 std 命名空间中定义,其中大部分算法都包含在头文件 中,少部分位于头文件 中。 |
迭代器 | 在 C++ STL 中,对容器中数据的读和写,是通过迭代器完成的,迭代器就是容器和算法之间的桥梁。 |
函数对象 | 如果一个类将 () 运算符重载为成员函数,这个类就称为函数对象类,这个类的对象就是函数对象(又称仿函数)。 |
适配器 | 可以使一个类的接口(模板的参数)适配成用户指定的形式,从而让原本不能在一起工作的两个类工作在一起。值得一提的是,容器、迭代器和函数都有适配器。 |
内存分配器 | 为容器类模板提供自定义的内存申请和释放功能,由于往往只有高级用户才有改变内存分配策略的需求,因此内存分配器对于一般用户来说,并不常用。 |
蒋 豆 芽:好了,我们继续讲迭代器。迭代器相当于容器和操纵容器的算法之间的中介。迭代器可以指向容器中的某个元素,通过迭代器就可以读写它指向的元素。从这一点上看,迭代器和指针类似。迭代器按照定义方式分成以下四种:
(1) 正向迭代器:容器类名::iterator 迭代器名;
(2) 常量正向迭代器:容器类名::const_iterator 迭代器名;
(3) 反向迭代器:容器类名::reverse_iterator 迭代器名;
(4) 常量反向迭代器:容器类名::const_reverse_iterator 迭代器名;
隔壁老李:嗯,说得很好,那豆芽,迭代器是怎么用的呢?
蒋 豆 芽:通过迭代器可以读取它指向的元素,*迭代器名
就表示迭代器指向的元素。
通过非常量迭代器还能修改其指向的元素。迭代器都可以进行++操作。
反向迭代器和正向迭代器的区别在于:
(1)对正向迭代器进行++操作时,迭代器会指向容器中的后一个元素;
(2)而对反向迭代器进行++操作时,迭代器会指向容器中的前一个元素。
我们举个例子说明:
#include <iostream>
#include <vector>
using namespace std;
int main(){
vector<int> v; //v是存放int类型变量的可变长数组,开始时没有元素
for (int n = 0; n<5; ++n)
v.push_back(n); //push_back成员函数在vector容器尾部添加一个元素
vector<int>::iterator i; //定义正向迭代器
for (i = v.begin(); i != v.end(); ++i) {
//用迭代器遍历容器,begin 成员函数返回指向容器中第一个元素的迭代器。++i 使得 i 指向容器中的下一个元素。end 成员函数返回的不是指向最后一个元素的迭代器,而是指向最后一个元素后面的位置的迭代器,因此循环的终止条件是i != v.end()
cout << *i << " "; //*i 就是迭代器i指向的元素
*i *= 2; //每个元素变为原来的2倍
}
cout << endl;
//用反向迭代器遍历容器
for (vector<int>::reverse_iterator j = v.rbegin(); j != v.rend(); ++j)
cout << *j << " ";
return 0;
}
运行结果如下:
0 1 2 3 4
8 6 4 2 0
而这里要特别注意,用迭代器遍历容器,begin 成员函数返回指向容器中第一个元素的迭代器。++i 使得 i 指向容器中的下一个元素。
end 成员函数返回的不是指向最后一个元素的迭代器,而是指向最后一个元素后面的位置的迭代器,因此循环的终止条件是i != v.end()
蒋 豆 芽:当然,定义迭代器类型我们总是嫌太麻烦,我们可以用auto关键字来定义,如:
for (auto i = v.begin(); i != v.end(); ++i)//定义正向迭代器
*i *= 2;
for (auto j = v.rbegin(); j != v.rend(); ++j)//定义反向迭代器
cout << *j << " ";
auto关键字实在是太省心了,它能在变量声明时根据初始化表达式自动推断该变量的类型。适用于类型冗长复杂,模板类型等。建议可以多多使用。
隔壁老李:(会心一笑)不错啊,豆芽,门儿清啊。那我问问你,下面代码有什么问题?
vector<int> vec;
vec.push_back(l);
vec.push_back(2);
vec.push_back(3);
vec.push_back(4);
vec.push_back(5);
for(vector<int>::iterator iter=vec.begin(); iter!=vec.end(); ++iter){
if(*iter == 3)
vec.erase(iter);
}
蒋 豆 芽:哼,怎么难得到我?乍一看这段代码很正确,但这里面隐藏着一个很严重的错误:当 vec.erase(iter)
语句执行后, iter 就变成了一个野指针,对一个野指针进行 ++iter
操作是肯定会出错的。
隔壁老李:iter 为什么就变成了一个野指针呢?
蒋 豆 芽:这是因为vector的数据结构依然是采用的数组。数组在删除元素时,后面的元素都会往前移动,自然原有的迭代器就失效了啊。
当移动完成后,4、5元素将获得新的迭代器。而erase将返回下一个元素的迭代器。
隔壁老李:你说得对,那我这样改进总可以了吧:
for(vector<int>::iterator iter=vec.begin(); iter!=vec.end(); ++iter){
if(*iter == 3)
iter = vec.erase(iter);
}
蒋 豆 芽:这样是解决了野指针的问题,但是无法删除两个连续的3。因为当iter获得下一个3的迭代器后,又经过了++iter操作,跳过了3。正确的解决方法应该如下:
vector<int>::iterator iter=vec.begin();
for(; iter!=vec.end();){
if(*iter == 3)
iter = vec.erase(iter);
else
++iter;
}
隔壁老李:bingo!豆芽你现在成长的很快啊。那我问你,vector的迭代器删除和map,set的迭代器删除、list的迭代器删除又有什么区别呢?
蒋 豆 芽:刚才我们讲了vector的迭代器删除的问题。而map,set则不一样,map,set的数据结构采用的红黑树,删除当前元素时,不会影响到下一个元素的迭代器,所以在调用erase之前,记录下一个元素的迭代器即可。
而对于list来说,它的数据结构是链表,使用了不连续分配的内存,并且它的erase方法也会返回下一个有效的迭代器,因此两种方式都可采用。
隔壁老李:嗯嗯,不错,豆芽,我认为你讲得很清楚了。我们回到我们原本的问题:迭代器和指针有什么区别?
蒋 豆 芽:迭代器不是指针,是类模板,表现的像指针。它只是模拟了指针的一些功能,通过重载了指针的一些操作符,如-->
、*
、++
、--
等。
迭代器封装了指针,是一个“可遍历STL容器内全部或部分元素”的对象,本质是封装了原生指针,是指针概念的一种提升,相当于智能指针。而迭代器的访问方式就是把不同集合类的访问逻辑抽象出来,使得不用暴露集合内部的结构而达到循环遍历集合的效果。这就是迭代器产生的原因。
隔壁老李:简直刮目相看了呀,不得了。哈哈。
蒋 豆 芽:(害羞)过奖了,使不得,使不得。对了,还要补充一下,前置 ++i 与后置 i++ 的区别。
我们先说C语言里,
先看到代码:
#include <stdio.h>
int main(){
int i = 2;
int j = 2;
j += i++; //先赋值后加
printf("i= %d, j= %d\n",i, j); //i= 3, j= 4
i = 2;
j = 2;
j += ++i; //先加后赋值
printf("i= %d, j= %d",i, j); //i= 3, j= 5
}
-
赋值顺序不同:++ i 是先加后赋值;i ++ 是先赋值后加;++i和i++都是分两步完成的。
-
效率不同:后置++执行速度比前置的慢。
-
i++ 不能作为左值,而++i 可以:
int i = 0; int *p1 = &(++i);//正确 int *p2 = &(i++);//错误 ++i = 1;//正确 i++ = 1;//错误
-
两者都不是原子操作。
这里我们要说一句,C语言是汇编层面的实现,后置++的汇编代码比前置++多了一行,那么执行就会多花一点时间。但是随着编译器的不断发展,这样的区别已经微乎其微了。
蒋 豆 芽:但是迭代器前置 ++i 与后置 i++ 的效率就有区别了。
我们来看看两者的实现:通过操作符重载实现。
CTest CTest::operator++(){ //前置++
*this += 1;
return *this;
}
CTest CTest::operator++(int){ //后置++
CTest tmp(*this); //记录修改前的对象
++(*this);
return tmp; //返回修改前的对象
}
后置++要多生成一个局部对象 tmp,这个对象有可能包含很多的成员,因此执行速度比前置的慢。在次数很多的循环中,++i和i++可能就会造成运行时间上可观的差别了。因此,特别提到,对循环控制变量i,要养成写++i、不写i++的习惯。
另外,我们之前讲过原子操作,从++i和i++的实现就可以看出,两者均不是原子操作,这一点要特别注意。
1.28 模板
隔壁老李:刚才我们提到了模板,豆芽你来讲讲吧。
蒋 豆 芽:没问题!有时两个或多个类的功能是相同的,但仅仅因为数据类型不同,就要分别定义多个类,如下:
//交换 int 变量的值
void Swap(int *a, int *b){
int temp = *a;
*a = *b;
*b = temp;
}
//交换 float 变量的值
void Swap(float *a, float *b){
float temp = *a;
*a = *b;
*b = temp;
}
//交换 char 变量的值
void Swap(char *a, char *b){
char temp = *a;
*a = *b;
*b = temp;
}
//交换 bool 变量的值
void Swap(bool *a, bool *b){
char temp = *a;
*a = *b;
*b = temp;
}
这些函数虽然在调用时方便了一些,但从本质上说还是定义了四个功能相同、函数体相同的函数,只是数据的类型不同而已,这看起来有点浪费代码,能不能把它们压缩成一个函数呢?
能!可以借助函数模板。
template<typename T> void Swap(T *a, T *b){
T temp = *a;
*a = *b;
*b = temp;
}
//交换 int 变量的值
int n1 =
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
<p> - 本专刊适合于C/C++已经入门的学生或人士,有一定的编程基础。 - 本专刊适合于互联网C++软件开发、嵌入式软件求职的学生或人士。 - 本专刊囊括了C语言、C++、操作系统、计算机网络、嵌入式、算法与数据结构等一系列知识点的讲解,并且最后总结出了高频面试考点(附有答案)共近400道,知识点讲解全面。不仅如此,教程还讲解了简历制作、笔试面试准备、面试技巧等内容。 </p> <p> <br /> </p>