0%

CPP八股(二)

C++语言基础

简述下C++语言的特点
  1. C++在C语言基础上引入了面对对象(三大特性:封装、继承、多态)的机制,同时也兼容C语言

  2. C++运行效率高,仅比汇编语言慢10%~20%

  3. C++更加安全, 增加了const常量、引用、四类cast转换(static_cast、dynamic_cast、const_cast、reinterpret_cast)、智能指针、try catch

    dynamic_cast<type>(a) // 用于类之间的转型,运行期间会做检查,转型失败会返回0并抛出bad_cast.
    static_cast<type>(a) // 这个其实和传统的C语言的强制转换是一样的,它不会做任何检查,如果转型失败就会发生未知错误。
    const_cast<type>(a) // 把a从const型转为非const型,常用于函数重载
    reinterpret_cast<type>(a) // 可进行任意转型,把a中的内存按type的方式来解读,需要程序员保证转换的正确性。
  4. C++可复用性高,C++引入了模板的概念,后面在此基础上,实现了方便开发的标准模板库STL(Standard Template Library)

  5. C++是不断在发展的语言。C++后续版本更是发展了不少新特性,如C++11中引入了nullptr、auto变量、Lambda函数、右值引用、智能指针


说说C语言和C++的区别
  1. C++面对对象;C是面对过程

  2. C是C++的子集,但是C++又有很多新特性,如引用、智能指针、auto变量等

  3. C不安全,如指针使用的潜在危险、强制转换的不确定性、内存泄露等。而C++更安全,如const常量、引用、cast转换、智能指针、try catch

  4. C++可复用性高,C++引入了模板的概念,实现了STL。相对于C语言的函数库更灵活、更通用


说说指针和引用的区别
  1. 指针用于保存地址类型的数据,而引用可以看成是变量的别名。
  2. 引用不可以为空,当被创建的时候必须初始化,而指针变量可以是空值,在任何时候初始化
  3. 指针可以有多级,但引用只能是一级
  4. 引用使用时无需解引用(*),指针需要解引用
  5. 指针的值在初始化后可以指向其它的存储单元,而引用在进行初始化后就不会再改变了
  6. sizeof 引用得到的是所指向的变量(对象)的大小,而 sizeof 指针得到的是指针变量本身的大小
  7. 指针作为函数参数传递时传递的是指针变量的值,而引用作为函数参数传递时传的是实参本身,不是副本
  8. 指针和引用进行++运算意义不一样

image-20230209194441920


说说使用指针需要注意什么
  1. 定义指针时,先初始化为NULL
  2. 用malloc或new申请内存之后,应该立即检查指针值是否不为NULL。防止使用空指针
  3. 用free或delete释放了内存之后,立即将指针设置为NULL,防止“野指针”
  4. 不要忘记为数组和动态内存赋初值
  5. 避免数字或指针的下标越界,特别要当心发生“多1”或者“少1”操作
  6. 动态内存的申请与释放必须配对,防止内存泄漏

什么是野指针,怎么产生的,如何避免?
  1. 概念:野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)

  2. 产生原因:释放内存后指针不及时置空(野指针),依然指向了该内存,那么可能出现非法访问的错误。这些我们都要注意避免

    char *p = (char *)malloc(sizeof(char)*100);  
    strcpy(p, "Douya");
    free(p);//p所指向的内存被释放,但是p所指的地址仍然不变
    ...
    if (p != NULL){//没有起到防错作用
    strcpy(p, "hello, Douya!");//出错
    }
  3. 避免办法:

    (1)初始化置NULL

    (2)申请内存后判空 不空

    (3)指针释放后置NULL

    (4)使用智能指针

    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; //释放后置空

说说运算符i++和++i的区别
  1. 效率不同:后置++ 执行速度比前置的慢

  2. i++ 不能作为左值,而++i 可以

    int i = 0;
    int* p1 = &(++i);//正确
    // int* p2 = &(i++);//错误
    ++i = 1;//正确
    // i++ = 1;//错误

final关键字
// C++11中允许将类标记为final,方法时直接在类名称后面使用关键字final,如此,意味着继承该类会导致编译错误
class Super final{
//......
};
// C++中还允许将方法标记为final,这意味着无法再子类中重写该方法。这时final关键字至于方法参数列表后面
class Super {
Super();
virtual void SomeMethod() final;
};

如何让两个类互相引用

如果两个类相互引用,需要使用前置声明(forward declaration)来解决循环依赖问题。前置声明可以让编译器知道类的存在,而不需要提供完整的类定义

class B; // 前置声明

class A {
public:
void setB(B* b) { m_b = b; }

private:
B* m_b;
};

class B {
public:
void setA(A* a) { m_a = a; }

private:
A* m_a;
};

C++内存

简述一下堆和栈的区别
  1. 堆栈空间分配不同。栈由操作系统自动分配释放 ,存放函数的参数值,局部变量的值等;堆一般由程序员分配释放。
  2. 堆栈缓存方式不同。栈使用的是一级缓存;堆则是存放在二级缓存中,速度要慢些。
  3. 堆栈数据结构不同。堆类似数组结构;栈类似栈结构,先进后出。

C++面向对象

简述一下面向对象的三大特征

面向对象的三大特征是封装、继承、多态。

  1. 封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行 交互。不想给别人看到的,我们使用protected/private把成员封装起来。开放一些共有的成员函数对成员合理的访问。

  2. 继承:可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。

    继承方式 private继承 protected继承 public继承
    基类的private成员 不可见 不可见 不可见
    基类的protected成员 变为private成员 仍为protected成员 仍为protected成员
    基类的public成员 变为private成员 变为protected成员 仍为public成员
  3. 多态:用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数


成员访问限定符
关键字 权限
public 可以被任意实体访问
protected 只允许子类及本类的成员函数访问
private 只允许本类的成员函数访问

拷贝构造函数的参数是什么传递方式,为什么
  1. 拷贝构造函数的参数必须使用引用传递

  2. 如果拷贝构造函数中的参数不是一个引用,即形如CClass(const CClass c_class)或CClass(const CClass* c_class),那么就相当于采用了传值的方式(pass-by-value),而传值的方式会调用该类的拷贝构造函数,从而造成无穷递归地调用拷贝构造函数。因此拷贝构造函数的参数必须是一个引用


拷贝构造和移动构造的不同

拷贝和移动是不同的操作:从A拷贝到B意味着,B分配了新内存,A的整个内容被拷贝到为B分配的新内存上。 而从A移动到B意味着分配给A的内存转移给了B,没有分配新的内存,它仅仅包含简单地拷贝指针


说说 C++ 类对象的初始化顺序,有多重继承的情况呢

父类构造函数 –> 成员类对象构造函数 –> 自身构造函数

其中成员变量的初始化与声明顺序有关,构造函数的调用顺序是类派生列表中的顺序

析构顺序和构造顺序相反


仿函数了解吗?有什么作用
  1. 仿函数(functor)又称为函数对象(function object)是一个能行使函数功能的类。仿函数的语法几乎和我们普通的函数调用一样,不过作为仿函数的类,都必须重载operator()运算符,举个例子:
class Func{
public:
void operator() (const string& str) const {
cout << str << endl;
}
};

Func myFunc;
myFunc("helloworld!");

>>>helloworld!
  1. 仿函数既能像普通函数一样传入给定数量的参数,还能存储或者处理更多我们需要的有用信息。我们可以举个例子:假设有一个vector,你的任务是统计长度小于5的string的个数,如果使用函数的话,你的代码可能长成这样:
bool LengthIsLessThanFive(const string& str) {
return str.length() < 5;
}
bool LenthIsLessThan(const string& str, int len) {
return str.length()<len;
}

C++STL

说说 map和 unordered_map 的区别
  1. map实现机理

    map内部实现了一个红黑树(红黑树是非严格平衡的二叉搜索树,而AVL是严格平衡二叉搜索树),红黑树有自动排序的功能,因此map内部所有元素都是有序的,红黑树的每一个节点都代表着map的一个元素。因此,对于map进行的查找、删除、添加等一系列的操作都相当于是对红黑树进行的操作。map中的元素是按照BST存储的,使用中序遍历可将键值按照从小到大遍历出来

  2. unordered_map实现机理

    unordered_map内部实现了一个哈希表(也叫散列表),通过把关键码值映射到Hash表中一个位置来访问记录,查找时间复杂度可达O(1),其中在海量数据处理中有着广泛应用。因此,元素的排列顺序是无序的


说说 vector 和 list 的区别, 分别适用于什么场景?

vector:一维数组

特点:在堆中分配内存,动态数组,元素连续存放,有保留内存,如果减少大小后内存也不会释放

扩容方式:

  1. 申请空间 2. 拷贝数据 3. 释放旧空间 因为 vector 扩容需要申请新的空间,所以扩容以后它的内存地址会发生改变

(1)当数组大小不够容纳新增元素时,开辟更大的内存空间,把旧空间上的数据复制过来,然后在新空间中继续增加

(2)新的更大的内存空间,一般是当前空间的1.5倍或者2倍,这个1.5或者2被称为扩容因子,不同系统实现扩容因子也不同

优点:和数组类似开辟一段连续的空间,并且支持随机访问,所以它的查找效率高其时间复杂度O(1)

缺点:由于开辟一段连续的空间, 所以插入删除会需要对数据进行移动比较麻烦, 时间复杂度O(n), 另外当空间不足时还需要进行扩容

list:双向链表

特点:元素在堆中存放,每个元素都是存放在一块内存中,它的内存空间可以是不连续的,通过指针来进行数据的访问

优点:底层实现是循环双链表,当对大量数据进行插入删除时,其时间复杂度O(1)

缺点:底层没有连续的空间,只能通过指针来访问,所以查找数据需要遍历其时间复杂度O(n), 没有提供[]操作符的重载


简述 STL 中的 map 的实现原理

map是关联式容器,它们的底层容器都是红黑树。map 的所有元素都是 pair,同时拥有实值(value)和键值(key)。pair 的第一元素被视为键值,第二元素被视为实值。所有元素都会根据元素的键值自动被排序。不允许键值重复。

map的特性如下

(1)map以RBTree作为底层容器;

(2)所有元素都是键+值存在;

(3)不允许键重复;

(4)所有元素是通过键进行自动排序的;

(5)map的键是不能修改的,但是其键对应的值是可以修改的。


说下 map 和 set 有什么区别, 分别是怎么实现的?

同:

  1. 都是一种关联式容器
  2. 以RBTree作为底层容器
  3. 不允许出现键值重复
  4. 所有的元素都会被自动排序

不同:

  1. set的元素的只有key没有value,value就是key

  2. 不能通过迭代器来改变set的值,因为set的值就是键,set的迭代器是const的

  3. map中所有元素都是键+值存在

  4. map的键是不能修改的,但是其键对应的值是可以修改的


说说 push_back 和 emplace_back 的区别

如果要将一个临时变量push到容器的末尾,push_back()需要先构造临时对象,再将这个对象拷贝到容器的末尾,而emplace_back()则直接在容器的末尾构造对象,这样就省去了拷贝的过程。

操作系统

进程五状态模型

img