C/C++基础知识点——C++面向对象

C++的特征有那些

封装

将同类型事物的属性和方法进行抽象,并对其进行封装。

继承

子类继承父类的,实现代码的可扩展性。继承分为单继承、多继承以及菱形继承(菱形继承可通过虚函数进行实现)

多态

定义

在基类前添加virtual关键字,在派生类中重写该函数,程序运行时,根据对象的实际类型调用对应的函数。如果对象类型是基类的,就调用基类的函数,如果对象类型是派生类的则调用派生类的函数。

分类

多态分为静态多态和动态多态。静态多态主要通过函数重载和泛型编程实现,发生在程序编译时;而动态多态主要通过虚函数进行实现,发生在程序运行时。

虚函数原理

编译器在编译的时候若发现基类中有virtual关键字,编译器会为其创建一个虚函数表,该表是一个一维数组,用来存放虚表地址;编译器也会为每一个对象创建虚表指针,该指针指向对象所属类的续表,程序运行时,根据对象的类型初始化虚表指针。

构造函数为什么不能是虚函数

  1. 虚函数的调用是在部分信息完成的机制,允许我们只知道接口而不知道对象的类型。要创建一个对象,首先要明确对象的完整类型
  2. 虚函数作用是通过基类的指针或者引用调用派生类的成员函数

析构函数可以是虚函数吗

分为两种情况,默认的析构函数不需要加virtual关键字来修饰,这是因为虚函数占用额外的内存空间,会造成内存空间的浪费

有继承关系的基类析构函数前需加virtual关键字来修饰,只是因为当基类的指针或者引用指向派生类对象时,如果未加virtual关键字,那么,当调用析构函数时,只会释放基类的内存空间,派生类的内存空间得不到释放,会导致内存泄漏。

对于有继承关系的类,构造函数和析构函数的调用情况

  1. 基类的指针或引用指向派生类对象时,有virtual关键字,先释放派生类后释放基类
  2. 基类的指针或引用指向派生类对象时,无virtual关键字,只释放基类的不释放派生类的,会造成内存泄漏
  3. 派生类的指针或引用指向派生类对象时,有virtual关键字,先释放派生类后释放基类

C++默认的空类有那些

默认构造函数、默认析构函数、默认赋值函数、默认拷贝构造函数

什么情况下调用拷贝构造函数

  1. 当用累的一个对象去初始化同类的另外一个对象时
  2. 当函数参数为类的对象时,调用函数进行实参和形参相结合
  3. 当函数返回类的对象时,函数执行完成后才能返回调用者时

类的静态成员和非静态成员的区别

  1. 名称:类的静态成员也叫类变量,而非静态成员叫成员变量
  2. 存储:类的静态成员存储在静态区,而非静态成员存储在栈上
  3. 作用域:类的静态成员属于整个类,而非类的某一个对象
  4. 有无this指针:类的静态成员没有this指针,而非静态成员有this指针

浅拷贝和深拷贝的区别

浅拷贝:是对一个已知的对象进行拷贝,如果用户没有定义拷贝构造函数,编译系统会自动调用默认拷贝构造函数。浅拷贝对指针类型进行拷贝时,拷贝后的指针指向同一块内存空间;而深拷贝不仅拷贝指针,还拷贝指针所指向的内容,经过深拷贝的指针指向两块不同的内存地址。

重载、重写及隐藏的区别

  1. 重载发生在同类中,参数一定不同,而重写发生在有继承关系的类中,参数一定相同,且必须加virtual关键字
  2. 隐藏与重写的区别:隐藏对virtual关键字可有可无。若函数参数不同,无论是否有virtual关键字,表明该函数被隐藏而不是被重写

友元函数

  1. 友元函数声明在类内部,定义在类外,普通函数前加friend关键字,以区分类的成员函数
  2. 友元函数不是类的成员函数,但是可以访问类的私有成员,打破了类的封装性和隐藏性
  3. 友元函数不能被继承
  4. 友元函数是单向的,不具备交换性
  5. 友元函数不具备传递性

解释一个空类的大小

空类同样会被初始化,每个实例在内存中都具有独一无二的地址,编译器会给一个空类隐含加一个字节的大小

C++中inline和宏定义的区别

1.内联函数是函数,而宏定义不是函数;
2.内联函数在编译时展开,而宏定义在预编译时展开;
3.内联函数编译时进行语法检测,而宏定义不需要;
4.内联函数可以直接嵌入到代码中,而宏定义只是文本替换;
5.内联函数不会产生歧义,而宏定义的参数不假括号会产生歧义;

explicit关键字

只是用来修饰只有一个参数的类构造函数(对于除第一个参数外其余参数都是默认值依然有效),表明该构造是显示的,而非隐式构造。

C++中构造函数参数列表与直接在构造函数中进行初始化的区别

  1. 对于内部类型(如int、double),无论是那种方式,没有本质区别
  2. 对于类中的const数据成员和引用数据成员,必须采用初始化列表的方式
  3. 对于无默认构造函数的继承关系中,必须采用初始化列表方式
    总之,初始化列表方式较在构造函数中初始化方式更为高效

类里面的成员变量为什么不能用memset()来进行设置,会有什么问题?

memset对于非POD(简单的说,就是一个类中,没有构造,没有析构,没有拷贝构造)崩溃的根本原因是执行memset之后,把指向虚函数表的指针置为NULL了,导致后续的一些函数调用会访问到非法内存,这才是崩溃的根本原因。

C++对象模型有哪些,目前用的是哪种模型?

分类:简单对象模型、表格对象模型及C++对象对象模型。

简单对象模型:

所有的成员占用相同的空间(与成员对象类型无关),对象并没有保存成员而是保存了成员的指针。

表格对象模型:

在简单对象模型上增加了一个间接层,将成员分为函数和变量,并用两个表格保存,对象只保存了两个指向表格的指针

C++对象模型:

结合了以上两种模型的特点,并对内存存取和空间进行了优化。非静态数据成员被放置在对象内部,而静态数据成员,静态函数成员和非静态函数成员均被放到了对象之外,

对于虚函数的执行分为两个部分:
  1. 每个类产生一堆指向虚函数的指针,放在表格中,该表格称之为虚函数表;
  2. 每个对象被添加一个指针,指向相关虚函数表,虚表指针的设定和重置都有没一个类的构造、析构和拷贝赋值运算符自动完成;
    相对于其他两种模型,C++对象模型从空间和内存存取上做了很大优化,但是,当所有的类的非静态数据成员添加或删除时,需要重新编译。
    C++对象模型中加入单继承:派生类中只扩展了基类的虚函数表
    C++对象模型中加入多继承
    C++对象模型中加入需继承

STL标准模板库【数据结构与算法】

什么是STL,STL包含哪些组件?

STL标准模板库,提供了6大组件,分别是容器,算法,迭代器,仿函数,配置器,空间配置器。

容器有哪些分类,详细说下每个元素?

容器分为顺序容器和关联容器。
顺序容器分为array,vector,list,deque;
关联容器分为set,map,mulitset,mulitmap。

vector扩容机制:

向vector容器中添加元素时,当容器中实际大小size和容器容量capacity相等时,会触发vector扩容机制。

vector扩容原理是:

开辟新空间;拷贝元素;清空旧空间;

vector扩容为什么按照倍数进行扩展,而不是按照一定的常量值进行扩展?

vector扩容的原理是开辟新空间,拷贝元素,释放旧空间,若是按照一定的常量进行扩展,则每次插入元素和拷贝元素时所用的时间复杂度是O(N),而按照一定的倍数扩容计算下来的时间复杂度是一个定值。

vector扩容为什么要按照1.5倍或者2倍的方式进行扩容,而不是3倍或四倍呢?

vector扩容第N次扩容若是能利用N-1次释放的内存是最好的,若扩容的倍数越大的话会造成更多内存浪费。

C++11中vector技巧

  1. 在添加元素之前,先调用reserve函数先对容器进行预留,防止触发扩容机制;
  2. vector容器用完之后,通过swap()+clear()两个方法对其进行回收,而不是通过erase或者clear方式;
  3. vector在访问的时候尽可能通过下标或者迭代器方式访问,不要通过at方法访问;
  4. 尽可能不要在vector容器前插入元素,减少使用frand_back()方法;
  5. vector在赋值时尽可能通过拷贝赋值方式,而不是insert();
  6. vector插入元素时建议使用emplace_back(),而非push_back().

除了容器还了解其他哪些?

迭代器:迭代器看起来类似指针,但实际不是指针,是一个类指针,用来指向容器。
迭代器分为四种,正向迭代器,反向迭代器,正向常量迭代器及反向常量迭代器。

vector迭代器失效问题?

vector在进行添加或者删除元素时,迭代器所指向的当前元素及该元素之后的所有元素的迭代器都会失效。删除迭代器不能直接通过erase(iter)方式来删除,可以通过返回该迭代器的下一个元素。

迭代器与指针的区别?

  1. 迭代器是类模板,而非指针;
  2. 迭代器只能指向某些特定的容器,而指针可以指向任何东西;
  3. 迭代器返回的是对象引用而非对象的值,而指针用来存放内存的地址

list底层实现及链表操作(单链表逆序,两种方式实现;两个单链表合并为有序链表)

单链表的逆序:

  1. 递归实现
  2. 非递归实现

list和vector的区别

  1. list底层是双向链表,而vector底层是数组;
  2. list内存地址不连续,而vector内存地址连续;
  3. list插入元素不会导致迭代器失效,删除会导致迭代器失效,而vector插入和删除元素都会导致迭代器失效;
  4. list在添加元素时不会触发扩容机制;
  5. list不可通过下表访问,而vector可以通过下表访问;
  6. list用于大量数据的添加和修改,而vector用于数据的查询。

Set和map底层原理(红黑树)

Set和map的底层都是通过红黑树来实现的,在说红黑树之前先来说下二叉查找树,

二叉查找树的特点是:

  1. 左子树的节点大小值小于或等于根节点;
  2. 右子树的节点大小值大于或等于根节点。

当根据二叉查找树的特征向二叉查找树中插入元素的时候,会导致所有的节点都插入到左子树上,形成线性结构,为了解决该问题提出了红黑树,

红黑树的特点:

  1. 红黑树的节点要么红色要么黑色;
  2. 根节点都是黑的;
  3. 每个叶子节点都是黑的(非空节点);
  4. 每个红节点的子节点都是黑的。
    当向红黑树中插入节点的时候,会打破红黑树特征,为了保持这种特征,在此基础上提出了旋转和变色两种操作。

红黑树和平衡二叉树的区别

  1. 红黑树牺牲了高度平衡的条件为代价,而平衡二叉树(度的绝对值最大是1)更讲究平衡;
  2. 红黑树时间复杂度为log(2n)进行搜索、插入和删除操作;

二叉树遍历思想(广度优先遍历和深度优先遍历)

  1. 广度优先遍历非递归做法通常是采用队列,而深度优先遍历非递归做法通常是采用栈;
  2. 广度优先遍历又叫层次遍历,从上往下每一层一次访问。而深度优先遍历,每一个分支尽可能的深入,且每个节点只能访问一次。二叉树的深度优先遍历又细分为:先序遍历、中序遍历及后序遍历。

STL中容器的线程安全问题

  1. 多个线程读取同一容器中的内容是线程安全的;
  2. 不同的容器对于对多个写入是线程安全的

循环队列如何判断队列满了?

队头指针在队尾指针的下一个位置时,队满;
队头与队尾杂志镇指向同一位置时,队空;

posted @ 2023-08-11 16:16  suntl  阅读(16)  评论(0编辑  收藏  举报