技术问题工作生活

面试常见问题01 - C++相关(施工ing)

2019-07-02  本文已影响0人  第八天的蝉啊

目录

一. C、C++的区别和优缺点
二. C++的语言特性
三. 多种关键字的作用和用法
四. 内联函数
五. 虚函数
六. 函数调用
七. 动态编译和静态编译
八. 引用和指针的区别
九. RTTI(Runtime Type Information,运行时类型信息)
十. 类型转换:static_cast、dynamic_cast、const_cast、reinterpret_cast
十一. 智能指针:shared_ptr、weak_ptr、unique_ptr
十二. 标准模版库 STL
十三. C++11 的新特性
十四. 内存泄漏
十五. C++中类会自动生成哪些函数
十六. 左值与右值
十七. new和malloc的区别

一. C、C++的区别和优缺点

1. 区别:C++是C的超集
2. C语言的特点
3. C++语言的特点

二. C++的语言特性

1. C++的三大特性
2. 重载、重写(覆盖)和隐藏
3. 多态的实现原理
4. 构造函数和析构函数的调用顺序

三. C++ 关键字

1. extern
2. static
3. const
1)常对象只能访问常成员函数
2) 常成员函数中能够修改有mutable关键字修饰的数据成员
3)常数据成员只能在类构造函数的初始化列表中进行
4)const 数据类型* 指针名:表示不允许修改该指针指向的对象,但指针可以指向其他对象
   数据类型* const 指针名:表示指针不可以指向其他对象,但可以修改所指向的对象
5)const修饰函数返回值时表示不允许对返回值进行修改
4. explict
5. volatile
1)易变性:所谓的易变性,在汇编层面反映出来,就是两条语句,下一条语句不会直接使用上一条语
   句对应的volatile变量的寄存器内容,而是重新从内存中读取
2)不可优化:告诉编译器不要对这个变量进行各种激进的优化,甚至将变量直接消除,保证程序员写
   在代码中的指令,一定会被执行
3)顺序性:保证Volatile变量间的顺序性,编译器不会进行乱序优化,但Volatile变量与非Volatile
   变量之间的操作,是可能被编译器交换顺序的
struct SharedDataStructure gSharedStructure; 
int gFlag;

// 线程A
gSharedStructure.foo = ...; 
gSharedStructure.bar = ...; 
gSharedStructure.baz = ...; 
gFlag = 1; 

// 线程B
 if (gFlag) 
        UseSharedStructure(&gSharedStructure); 

在上面的这一段代码当中,结构体和flag之间因为不存在依赖关系,执行的顺序可能会被编译器修改(compiler reorder)。线程A的代码可能是flag先被置为1,然后处理结构体,这样线程B的if语句内就是不安全的 。这种情况下,可以将gFlag和gSharedStructure 都加上 volatile 关键字,保证编译器按照代码顺序产生机器码。但在加了volatile关键字之后,上述代码在实际执行时仍不能保证安全,因为CPU会激进的reorder以加快执行速度, 因此内部执行机器码的顺序仍然是未知的,只保证最终结束时的结果与原顺序一致。
结论:不要预期在多线程中使用volatile来解决数据的同步问题,该加锁时就加锁

四. 内联函数

1. 作用
2. 特点
3. 优缺点

五. 虚函数

1. 作用:C++中虚函数的作用主要是实现了多态
2. 底层机制
3. 工作原理
4. 析构函数与虚函数
5. 类里面还有什么函数不能为虚

六. 函数调用

  1. 程序内存区域
1)堆区:动态分配内存,向地址增大方向增长
2)栈区:存放局部变量、函数参数、当前状态、函数调用信息,向地址减小方向增长
3)全局区:存放全局变量和静态变量
4)常量区:存放常量
5)代码区:存放程序代码
  1. 函数调用过程


    压栈过程
    返回过程

七. 动态编译和静态编译

  1. 区别
    1)动态编译的可执行文件需要附带一个的动态链接库,在执行时,需要调用其对应动态链接库中的命令
    2)静态编译就是编译器在编译可执行文件的时候,将可执行文件需要调用的对应动态链接库中的部分提取出来,链接到可执行文件中,使可执行文件在运行的时候不依赖于动态链接库
  2. 静态链接的特点
  1. 动态链接的特点

八. 引用和指针的区别

  1. 引用是变量的别名,没有分配新的内存空间,引用必须进行初始化
  2. 指针本身是一个变量,拥有自己的内存空间,值为所指向的对象的地址,可以不进行初始化
  3. 指针更加自由,可以改变所指向的对象,而引用不行

十. RTTI(Runtime Type Information,运行时类型信息)

1. 获取类、对象的类名
int i = 3;
cout << typeid(i).name() << endl;  // int
cout << typeid(string).name() << endl;   // class std::basic_string< ... >

2. 当类中不存在虚函数时,typeid()是静态的,在编译时确定类名
class A
{
public:
     void Print() { cout << "This is class A." << endl; }
};

class B : public A
{
public:
     void Print() { cout << "This is class B." << endl; }
};

int main()
{
     A *pA = new B();
     cout << typeid(pA).name() << endl; // class A *
     cout << typeid(*pA).name() << endl; // class A
     return 0;
}

3. 当类中存在虚函数时,typeid()是动态的,在运行时确定类名
class A
{
public:
     virtual void Print() { cout << "This is class A." << endl; }
};

class B : public A
{
public:
     virtual void Print() { cout << "This is class B." << endl; }
};

int main()
{
     A *pA = new B();
     cout << typeid(pA).name() << endl; // class A *
     cout << typeid(*pA).name() << endl; // class B
     return 0;
}
A *pA = new B();

if (typeid(*pA) == typeid(A))
     cout << "I am a A truly." << endl;  
else (typeid(*pA) == typeid(B))
     cout << "I am a B truly." << endl; //  I am a B truly.

十一. 类型转换:static_cast、dynamic_cast、const_cast、reinterpret_cast

1. 基础类型之间互转,如:float转成int、int转成unsigned int等
2. 指针与void之间互转,如:float*转成void*、Bean*转成void*、函数指针转成void*等
3.子类指针/引用与父类指针/引用之间转换,向上转换安全,向下则不安全
class Parent {
public:
    void test() {
        cout << "Parent " << endl;
    }
};
class Child : public Parent {
public:
     void test() {
        cout << "Child " << endl;
    }
};
Parent  *p = new Parent;
Child  *c = static_cast<Child*>(p);

c->test();   // Child( test 若为虚函数则输出 Parent )
Parent  *p = new Child ();

if (dynamic_cast<Child1 *>(p) )
     cout<<"I am a Child1 truly."<<endl;
else if (dynamic_cast<Child2 *>(p) )
     cout<<"I am a Child2 truly."<<endl;  // I am a Child1 truly.
const char* a;
char* b = const_cast<char*>(a);
const char* c = const_cast<const char*>(b);
1. 从指针类型到一个足够大的整数类型
int* p = new int(1);  // p = 00C3D8A8 (0 0 1100 0011 1101 1000 1010 1000)
int i = reinterpret_cast<int>(p); // i = 12834984
2. 从整数类型或者枚举类型到指针类型
void* p = reinterpret_cast<void* >(1024);  // p = 00000400 (..... 0100 0000 0000)
3. 从一个指向函数的指针到另一个不同类型的指向函数的指针
4. 从一个指向对象的指针到另一个不同类型的指向对象的指针
5. 从一个指向类函数成员的指针到另一个指向不同类型的函数成员的指针
5. 从一个指向类数据成员的指针到另一个指向不同类型的数据成员的指针

十二. 智能指针:shared_ptr、weak_ptr、unique_ptr

shared_ptr<int> sp1(new int(5));
cout << "sp1 引用计数:" << sp1.use_count() << endl; // sp1 引用计数:1
shared_ptr<int> sp2 = sp1; // 指向内存new int(5)的指针数量加1
shared_ptr<int> sp3(sp1); // 指向内存new int(5)的指针数量加1
cout << "sp1 引用计数:" << sp1.use_count() << endl; // sp1 引用计数:3
cout << "sp2 引用计数:" << sp2.use_count() << endl; // sp2 引用计数:3
cout << "sp3 引用计数:" << sp3.use_count() << endl; // sp3 引用计数:3
sp3.reset(); // 指向内存new int(5)的指针数量减1
cout << "sp1 引用计数:" << sp1.use_count() << endl; // sp1 引用计数:2
cout << "sp2 引用计数:" << sp2.use_count() << endl; // sp2 引用计数:2
cout << "sp3 引用计数:" << sp3.use_count() << endl; // sp3 引用计数:0
sp2.reset(new int(3)); // 指向内存new int(5)的指针数量减1,指向内存new int(3)
                       // 的指针数量加1
cout << "sp1 引用计数:" << sp1.use_count() << endl; // sp1 引用计数:1
cout << "sp2 引用计数:" << sp2.use_count() << endl; // sp2 引用计数:1
cout << "sp3 引用计数:" << sp3.use_count() << endl; // sp3 引用计数:0

*  shared_ptr在多线程的情况下不是线程安全的,指向的内存和本身的引用计数器会被多个线程访问和
   修改。多线程的条件线,每个线程需要互斥的访问和修改其指向的内存和shared_ptr的引用计数器
shared_ptr<int> sp(new int(5));
cout << "创建wp前sp的引用计数:" << sp.use_count() << endl;    // 创建wp前sp的引用计数:1

weak_ptr<int> wp(sp);
cout << "创建wp后sp的引用计数:" << sp.use_count() << endl;    // 创建wp后sp的引用计数:1

shared_ptr<int> sp2 = wp.lock();
cout << "创建 sp2后sp的引用计数:" << sp.use_count() << endl;    // 创建sp2后sp的引用计数:2

sp.reset();
sp2.reset();
if ( shared_ptr<int> sp3 = wp.lock() )
    cout << *sp3 << endl;
else
    cout << "wp指向对象为空" << endl;   // wp指向对象为空
1. unique_ptr不具备拷贝语义和赋值语义**,不能通过copy constructor和operator=转移资源所有
   权,而在auto_ptr中实现了拷贝语义和赋值语义
   unique_ptr<int> ptr(new int(4));
   unique_ptr<int> ptr1(ptr);//错误,因为没有实现拷贝语义
   unique_ptr<int> ptr2 = ptr;//错误,因为没有实现赋值语义

2. C++中实现了“move语义”,使用“move语义”可以将资源所有权从一个对象转移到另一个对象
   unqiue_ptr<int> ptr(new int(4));
   unique_ptr<int> ptr1(std::move(ptr));//正确
   unique_ptr<int> ptr2 = std::move(ptr);//正确

3. unique_ptr对象可以借助于“move语义”存放在容器中,而auto_ptr不允许存放在容器中
   vector<unqiue_ptr<int> >v;
   unique_ptr<int>ptr (new int(5));
   v.push_back(std::move(ptr));//v.push_back(ptr);是不对的

4. unique_ptr可以用于管理数组,auto_ptr不支持数组

十三. 标准模版库 STL

STL体系结构
array(C++ 11):相对于数组,增加了迭代器等函数
vector:用连续内存存储元素,起始地址不变;当元素个数大于当前容量时,会分配原来2倍容量,从而
        将元素复制到新空间,原空间释放;可以快速随机访问元素,快速在末尾插入删除元素,此过程
        中不用移动内存;对中间元素插入删除时要移动内存

vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
cout << v[0] << ": " << &(v[0]) << ", " << v[1] << ": " << &(v[1]) << ", " << v[2] << ": " << &(v[2]) << endl;
v.erase(v.begin());
cout << v[0] << ": " << &(v[0]) << ", " << v[1] << ": " << &(v[1]) << endl;
// 1: 000D9B20, 2: 000D9B24, 3: 000D9B28
// 2: 000D9B20, 3: 000D9B24
list:用双向链表而非连续内存存储元素,只能顺序访问,不支持随机访问,在任意位置插入或删除操作
      效率较高
forward_list(C++ 11):用单向链表实现
deque:用连续内存存储元素,起始地址可以改变;可以快速地随机访问元素,快速地在开始和末尾插
       入删除元素,随机插入比两端插入要慢;重新分配空间后,原空间保持,只是新建立了一个内存块,
       将新数据插入这个内存块,没有空间的复制删除过程,故deque是由多个分段连续的内存空间组成,
       各小片内存间用链表相连,还是可以使用[],只是没有vector快
deque<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
cout << v[0] << ": " << &(v[0]) << ", " << v[1] << ": " << &(v[1]) << ", " << v[2] << ": " << &(v[2]) << endl;
v.erase(v.begin());
cout << v[0] << ": " << &(v[0]) << ", " << v[1] << ": " << &(v[1]) << endl;
// 1: 00169B20, 2: 00169B24, 3: 00169B28
// 2: 00169B24, 3: 00169B28

  2. 关联式容器:根据键值key来快速高效的检索数据,容器中的元素初始化后都按一定的顺序排列,使用非连续的内存空间存储元素,主要有set、multiset、map、multimap、unordered_map(C++ 11)、unordered_set(C++ 11)

set:支持快速查找,不允许有重复值,用红黑树结构来存储;multiset支持快速查找,允许有重复值。
map:一对一映射,支持关键字快速查找,不允许有重复值,使用红黑树结构存储;multimap是一对多映
     射,基于关键字查找,允许有重复值
unordered_map(C++ 11):元素不进行排序,使用hash表实现,查找效率高,但需要增加内存开销
unordered_set(C++ 11):元素不进行排序,使用hash表实现,查找效率高,但需要增加内存开销

  3. 容器适配器:对已有容器进行某些特性的再封装,它不是一个新容器,主要有stack、queue

template<typename Iter>
void Filter(Iter _itIndex)
{
    int arr[] = {1, 2, 3, 4, 5};
    for (auto val : arr)
        *_itIndex = val;
}

int main()
{
    vector<int> vec;
    // Filter(vec.begin());  //  出错,vec的未分配内存,使用*_itIndex = val导
                             //  致内存错误,需要提前为vec分配内存
    Filter(back_inserter(vec));  // 相当于将Filter()中的 *_itIndex = val 替换
                                 // 为 vec.push_back(val)

    return 0;
}

十四. C++11 的新特性

1. auto:编译器根据上下文情况,确定auto变量的真正类型,auto作为函数返回值
         时,只能用于定义函数,不能用于声明函数
2. nullptr:用于解决NULL数据类型实际为int导致的函数重载问题
3. override:可以显式的在派生类中声明,哪些成员函数需要被重写,如果没被重
             写,则编译器会报错
4. final:类被final修饰,不能被继承;基类虚函数被final修饰,不能被重写
5. for-each循环
6. 右值引用:右值引用标记为T&&,引用一个临时对象,减少内存拷贝,优化性能
9. 委托构造函数:使用它所属的类的其他构造函数执行自己的初始化过程,或者说
                它把自己的一些(或者全部)职责委托给了其他构造函数,被委
                托的构造函数需要能够完整地构造出这个对象
10. 继承构造函数:在派生类中声明 using BaseClass::BaseClass; 使派生类能
                 够使用基类的构造函数,
11. 移动构造函数:参数是一个右值引用类型的参数,避免深拷贝,移动构造函数只
                 是将临时对象的资源做了浅拷贝,不需要对其进行深拷贝,从而
                 避免了额外的拷贝,提高性能
1. std::array
2. std::forward_list:与list区别在于它是单向链表
3. std::unordered_map:插入时不会自动排序, 使用哈希表实现
4. std::unordered_set:插入时不会自动排序, 使用哈希表实现
1. std::thread:创建一个std::thread的实例,线程在实例成功构造成时启动,可以通过
                std::thread的构造函数的参数将参数传递给线程函数,类成员函数做为
                线程入口时,把this做为第一个参数传递进去即可。
   1) 成员函数:get_id()、join()、detach()、joinable()
   get_id():获取线程id(native_handle()获取系统分配的线程id)
   join():等待线程运行结束
   detach():从 thread 对象分离执行的线程,允许执行独立地持续,thread 对象失去
             对线程的所有权,但线程仍会继续执行,直到线程退出并释放所有资源
   joinable():判断 thread 对象是否还有对线程的所有权,返回true则有,否则为无
   2) 操作函数:
   std::this_thread::sleep_for():使当前线程暂停一段时间再运行
   std::this_thread::sleep_until:使当前线程暂停到某一时间再运行
2. std::atomic:原子数据类型,原子数据类型不会发生数据竞争,能直接用在多线程中而
                不必用户对其进行添加互斥资源锁的类型
3. std::condition_variable:条件变量,用于线程同步
   成员函数:wait()、notify()
   1) wait():线程的等待动作,直到其它线程将其唤醒后,才会继续往下执行
   2) notify_xxx():唤醒wait在该条件变量上的线程,notify_one 唤醒等待的一个线程;
                 notify_all 唤醒所有等待的线程
1. std::shared_ptr:共享式指针,维护一个引用计数器,当引用计数器为0时自动释放内存
2. std::weak_ptr:与shared_ptr搭配使用,解决循环引用的问题
1. 基本语法:[ caputrue ] ( params ) opt -> ret { body; };
   1) capture是捕获列表: []不捕获任何变量;
                         [&]捕获外部作用域中所有变量,按引用捕获
                         [=]捕获外部作用域中所有变量,按值捕获
   2) params是参数表(选填) 
   3) opt是函数选项;可以填mutable,exception,attribute(选填) 
      mutable说明lambda表达式体内的代码可以修改被捕获的变量,并且可以访问被捕获的
      对象的non-const方法。 
      exception说明lambda表达式是否抛出异常以及何种异常
      attribute用来声明属性
   4) ret是返回值类型(拖尾返回类型)
   5) body是函数体

十五. 内存泄漏

1. 使用分配未成功的内存
2. 内存分配成功,但未初始化
3. 操作超出内存边界
4. 内存使用结束未释放
5. 使用已释放的内存

十六. C++中类会自动生成哪些函数

  1. 对于空类,声明时,编译器不会生成任何的成员函数,只会生成1个字节的占位符
  2. 空类定义时会生成6个成员函数:缺省的构造函数、拷贝构造函数、析构函数、赋值运算符、两个取址运算符
class Empty
{
public:
    Empty();                            //缺省构造函数
    Empty(const Empty &rhs);            //拷贝构造函数
    ~Empty();                           //析构函数 
    Empty& operator=(const Empty &rhs); //赋值运算符
    Empty* operator&();                 //取址运算符
    const Empty* operator&() const;     //取址运算符(const版本)
};
  1. 拷贝构造函数参数必须为引用,否则会造成拷贝构造函数无限递归;C++对于临时对象的引用只能是const,当使用临时变量(如函数的返回值)初始化对象时,拷贝构造函数的参数必须用const修饰

十七. 左值与右值

十八. new和malloc的区别

上一篇 下一篇

猜你喜欢

热点阅读