C++面试笔记(持续更新中)

目录

1,纯虚函数和抽象类?

2,虚函数和多态?

3,区分函数重载、虚函数重写(override)、函数重定义?

4,多重继承和多继承时,子类对象的虚函数表指针和虚函数表情况?

5,虚基类和虚继承?

6,构造函数为什么一般不写成虚函数?析构函数为什么一般要写成虚函数?

7,在C++中,有一些情况不能将函数定义为虚函数?

8,constexpr关键字?

9,左值引用和右值引用?

10,override和final?

11,委托构造函数?

12,lambda表达式?

13,list(链表)、vector(动态数组)、deque(双端动态数组)?

14,二叉查找树,红黑树,哈希表?

15,排序算法?

16,二叉树的前中后序遍历?

17,const和static关键字?

18,C++内存分配情况?

19,什么是指针?指针和引用?

20,C++中指针参数传递和引用参数传递?


1,纯虚函数和抽象类?

答:纯虚函数的格式:virtual+返回值类型+函数名+参数列表 = 0;

纯虚函数是不被实现的函数,只提供函数接口。包含纯虚函数的类叫做抽象类,继承该抽象类的派生类必须重写该函数并予以实现,否者,派生类依旧是抽象类。抽象类无法实例化对象,只能实例化其派生类对象(即抽象类的指针指向派生类对象的地址或者抽象类的引用绑定派生类对象)。

2,虚函数和多态?

答:这里说的多态默认为动态多态。多态的实现方式一般是由继承+虚函数实现的。函数的重载是在一个类范围中,在编译期间就能决定调用哪个函数,因为函数重载的参数列表不同,编译器默认会在函数名前加上形参数据类型当作函数名。因此重载的函数在编译器看来是不同名字的函数。而多态只能在运行期间才能决定函数的调用。通过在子类重写父类中的同名同参数列表(即完全相同)的虚函数,父类中的虚函数必须包含virtual关键字,子类中可以不写。利用父类指针或引用指向的对象决定调用哪个类中的同名函数,如果指针指向的是父类中的对象,调用父类中的同名函数,指向子类中的对象则调用子类中的同名函数。实现了通过一个接口调用不同的类中的同名函数的功能,这就是多态性。

底层原理:多态的实现与虚函数表指针,虚函数表相关。

当一个类中有虚函数时,创建一个类对象时,编译器会默认生成一个看不见的成员变量,虚函数表指针,这是属于对象的,会在调用构造函数时完成初始化。同时,编译器会为该类生成一个虚函数表,虚函数表存放着虚函数的地址,分别指向对应的虚函数入口,这是属于类的。虚函数表指针指向虚函数表。后续根据父类指针的指向的对象,根据所指向对象的虚函数表指针去寻找对应的虚函数表,从而确定调用的是父类还是子类的虚函数,以此实现多态。

编译器处理虚函数表应该如何处理?对于子类来说,编译器建立虚函数表的过程其实一共三个步骤:a,拷贝父类的虚函数表,如果是多继承,则拷贝所有有虚函数的父类的虚函数表;b,其中一个基类和子类公用一个虚函数表;c,检查虚函数表,如果子类重写了父类中的虚函数,则替换为子类的虚函数地址,如果是新出现的虚函数,则追加到自身虚函数表末尾。

3,区分函数重载、虚函数重写(override)、函数重定义?

答:函数重载:在一个类的范围内,重载同名函数,参数列表不同(顺序不同,类型不同,数量不同),在编译期间即能确定为不同函数,因为编译器会在编译期间给函数名前加上参数数据类型。virtual关键字可有可无;

虚函数重写:在父类和子类之间,在子类中重写父类中完全相同(同返回值类型同函数名同参数列表)的函数,父类中虚函数必须带virtual关键字,实现根据父类指针所指向的对象决定调用哪个类中的虚函数;

函数重定义(隐藏父类同名函数):范围在父类和子类之间,如果函数名相同,参数列表不同,不管有无virtual,父类函数被隐藏;如果函数名和参数列表和返回值类型都相同,父类中有virtual时,就是虚函数重写,没有virtual时,父类函数被隐藏。

4,多重继承和多继承时,子类对象的虚函数表指针和虚函数表情况?

答:多重继承:指的是多次单继承,比如类B继承类A,类C继承类B。这时,子类对象只有一个虚函数表指针,虚函数表也只有一个;

多继承:指的是单次继承多个父类。这时,有多少个有虚函数的父类,就有多少个虚函数表指针,子类虚函数表中存在新的虚函数时,加在其中一个父类虚函数后(子类本身虚函数表和其中一个父类公用,一般是第一个继承的),如果子类中重写父类中虚函数,覆盖原虚函数表中虚函数地址。

更多细致讲解来自于https://blog.csdn.net/qq_36359022/article/details/81870219

5,虚基类和虚继承?

答:多继承很容易产生命名冲突的情况,会产生二义性问题。典型的是菱形继承。我们在继承类型(public,private,protected)之前加上virtual即为虚继承,所继承的基类叫做虚基类。虚基类提供了让其派生类承诺共享该虚基类的目的。虚基类对其派生类本身是没有影响的,受影响的是派生类的派生类,这时,派生类的派生类中只会出现一份最开始的虚基类的成员。详解来自于C++虚继承和虚基类详解 - 知乎

6,构造函数为什么一般不写成虚函数?析构函数为什么一般要写成虚函数?

答:首先,明确构造函数的作用,创建对象。在对象的创建过程中,我们已经确定对象的具体类型,不需要动态的选择调用哪个函数,因此,不必定义为虚函数。虚函数主要是用于动态多态机制;析构函数是用来销毁对象的,析构函数一般要写成虚函数是为了应对基类指针指向派生类对象时的情况,如果不写成虚函数,我们在释放资源时只会调用基类的析构函数,而不会调用派生类的析构函数,这会造成派生类对象的资源无法得到释放,造成内存泄漏。只有当我们写成虚函数时,才能正确的调用派生类对象的析构函数,正确释放资源。

7,在C++中,有一些情况不能将函数定义为虚函数?

答:静态函数无法被定义为虚函数,因为静态函数是属于类而不是对象的函数,它们不依赖于对象的状态。因为虚函数的调用是基于对象的动态类型,而静态函数与对象无关,所以不能将静态函数定义为虚函数;

构造函数:构造函数是用于创建对象的,此时已知对象的具体类型,不需要定义为虚函数,动态地根据基类指针指向的对象类型来决定调用哪个函数;

内联函数:内联函数是通过内联展开(inline expansion)来代替函数调用的机制,以减少函数调用的开销。是在编译期间就能确定的。虚函数的调用涉及虚函数表指针的选取和动态绑定,需要在运行期才能确定,与内联函数的机制冲突,所以不能将内联函数定义为虚函数;

普通全局函数:全局函数不属于任何类,无法被继承和重写,因此不能将全局函数定义为虚函数;

友元函数:因为在一个类里声明友元时,由于友元不是自己的成员函数,自然不能在自己的类里声明为虚函数。

8,constexpr关键字?

答:功能:使指定的常量表达式获得在编译期间计算出结果的能力,而不必等到程序运行期间。比如声明数组时,大小必须为常量,若以函数返回值为大小,此时会报错,若在函数前加上constexpr,则可解决该问题。带有constexpr关键字的函数被视为内联函数,若能够在编译期间计算出结果,则替换函数调用为计算得到的结果。(35条消息) C++11关键字constexpr看这篇就够了_c++11 constexpr_令狐掌门的博客-CSDN博客

9,左值引用和右值引用?

答:左值引用可以对左值进行引用,当我们需要对右值进行引用时,左值引用无法做到,当然,可以说使用常量左值引用,但这时我们虽然实现了引用右值的功能,但由于我们是常量左值引用,故无法修改对象内容。因此C++11引入了右值引用的概念。左值引用是在数据类型后加一个取址符,右值引用是在数据类型后面加俩个取址符(ex:int & a = b;int && c = 10;)C++11右值引用(一看即懂) (biancheng.net).

右值引用的使用场景:移动语义,完美转发。c++11的移动语义和完美转发 - 知乎 (zhihu.com) 

右值引用的意义在于它能够延长右值的生命周期。

移动语义:当拷贝构造函数的被拷贝对象是一个右值或者说是一个临时对象时,拷贝构造函数的效率不高,会实现多次构造函数的调用,这时,更聪明的做法是,将即将被销毁的对象的资源内容移动到目标对象中,这会提高预计效率。

完美转发:先说万能引用,引用为具体类型时是右值引用,引用类型为模板时是万能引用。引用则折叠:模板推导后可能会出现双重引用,故制定折叠规则,双重引用中只要有一方是左值引用都为左值引用,只有当两者都是右值引用时才为右值引用。完美转发是利用std::forward()函数起到参数转发的作用,因为有时候会出现右值引用本身是一个左值,故需要来实现参数完美转发,(即左值(右值)从形参传递给函数体时还是左值(右值))。

10,override和final?

答:override关键字用于子类重写父类的虚函数时,当子类的某一函数参数列表后加上override关键字时,编译器会检查该函数是否重写了父类完全相同的虚函数,如果未重写,则会编译报错;final关键字,阻止类的进一步派生和虚函数的进一步重写。当我们在类名后加final或者虚函数参数列表后加final,则该类无法被继承或虚函数无法被子类重写。否者编译器报错。

11,委托构造函数?

答:当我们构造一个类对象时,都会执行我们类的构造函数,虽然C++每次只允许我们调用一个构造函数,但是我们可以执行多个构造函数的功能。

A,被委托的构造函数在委托构造函数的初始化列表中调用,而不在函数体中调用;

B,委托构造函数的初始化列表中只允许出现被委托构造函数,不允许给成员变量初始化;

C,执行顺序:先执行被委托构造函数的初始化列表,然后是被委托构造函数的函数体,最后是委托构造函数的函数体;

D,被委托构造函数同样可以是一个委托构造函数,继续委托。

12,lambda表达式?

答:格式:[ capture ] ( params ) opt -> ret { body; };依次是捕获列表,参数列表,可选项,返回值类型,函数体实现。只有当函数实现只有一句return语句时,可省略ret,实现自动推导返回值类型。详解见c++ lambda 看这篇就够了!(有点详细)_c++ 运行时 构建 lamda_速趴赛亚金的博客-CSDN博客

13,list(链表)、vector(动态数组)、deque(双端动态数组)?

答:链表由一系列节点(链表中的每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域(node->val),一个是存储下一个结点地址的指针域(node->next)。List有一条重要的性质,插入和删除操作都不会造成原有list迭代器失效,这在vector是不成立的,因为vector的插入和删除操作可能会造成记忆体重新配置,导致原有迭代器全部失效。

vector,它和array容器非常类似,都可以看作是对C++普通数组的“升级版”。不同之处在于,aray实现的是静态数组(容量固定的数组),而vector实现的是一个动态数组,即可以进行元素的插入和删除,在此过程中,vector会动态地调整所占用的内存空间,整个过程无需人工干预。vector主要是用三个迭代器来表示的,first, last, end。first指向vector容器对象的起始字节;last指向vector容器的最后一个元素末尾的字节位置,end指向整个vector容器所占内存空间末尾字节的位置。vector扩容本质:当size == capacity时,即满载,如果再向其添加元素,vector就需要扩容,扩容分为三步:a,完全弃用现有的内存空间,重新申请更大的内存空间;b,将旧内存空间的数据按原有顺序全部移动到新的内存空间;c,最后将旧的内存空间释放。【扩容的大小根据编译器不一样倍数不一样,linux下是2倍,vs里是1.5倍】。vector是最常用的容器之一,其底层所采用的数据结构非常简单,就只是一段连续的线性内存空间。

当vector容器使用下标[]访问时,一旦[i],下标内的数大于vector容器的容量(capacity()函数可获取容量)减一(下标从0开始时),则会发生数组越界访问的错误。如果此时采用at()方式访问时,一旦越界,会抛出std::out_of_range异常。

deque容器,vector容器是单项开口的连续线性内存空间,deque则是一种双向开口的多段连续线性内存空间,又称双端数组,所谓的双向开口,指的是可以在头尾两端进行元素的插入删除操作,当然,vector容器也可以在头尾两端进行元素的插入删除,但是在头部的操作效率很低,无法接受。

deque容器和vector容器最大的差异:一是,deque允许使用常数项时间对头端元素进行插入删除操作;二是,deque没有容量的概念。因为它是动态的以分段连续空间组合而成,随时可以增加一段新的空间并链接起来,换句话说,像vector那样,因为旧空间不足,重新申请更大的内存空间,拷贝所有元素到新空间,然后释放旧空间这样的情况在deque身上是不会发生的。

由于deque容器中存储数据的空间是一段一段等长的连续空间,各段空间之间不一定连续,可以位于内存中的不同区域,为了管理这些连续空间,deque采用数组(数组名假设为map)存储着各段连续内存空间的首地址,也就是说,map数组中存储的都是指针,指向那些真正用来存储数据的各个连续空间。由于deque容器底层将序列中的元素分别存储到了不同段的连续空间中,因此要想实现迭代器的功能,必须先解决一下2个问题:a,迭代器在遍历deque容器时,必须能够确认各个连续空间在map数组中的位置;b,迭代器在遍历某个具体的连续内存空间时,必须能够判断自己是否已经处于空间的边缘位置,如果是,一旦前进后者后退,就需要跳跃到上一个或者下一个连续空间中。STL教程(五):C++ STL常用容器之deque - 知乎 (zhihu.com)

以deque为底层容器的适配器(stack, queue, priority_queue):

stack:栈,先进后出,只允许在栈顶添加和删除元素,称为入栈和出栈;

queue:队列,先进先出,只允许尾端插入,头端删除。

两者都没有迭代器,访问元素的唯一方式是遍历容器,并移除访问过的每一个元素。

list容器优缺点:插入删除方便,时间复杂度为常数项时间,可在两端进行高效插入和删除;随机访问复杂,只能从head结点不断遍历,相较于vector占用内存较多。

vector容器优缺点:随机访问方便(下标访问),动态扩容,占用内存较少;只能尾端高效插入删除,头端和中间插入删除时间复杂度O(n),当满载时,需要重新申请更大的内存空间,拷贝旧空间数据到新空间,释放旧空间内存。

deque容器优缺点:随机访问方便,不存在容量概念,随时可增加一段新的连续内存空间接在后面,内部插入删除方便,结合了vector和list的优点,可在头尾两端进行插入和删除。因为 deque 的是能够双向操作,所以其 push 和 pop 操作都类似于 list 都可以直接有对应的操作,需要注意的是list 是链表,并不会涉及到界线的判断, ⽽deque 是由数组来存储的,就需要随时对界线进⾏判断;因为涉及比较复杂,使用多段连续内存空间,占用较多内存。

我们知道数组的特点,寻址容易,插入和删除困难,而链表,插入和删除容易,寻址困难。结合二者的优点特性的就是哈希表。

14,二叉查找树,红黑树,哈希表?

答:二叉查找树,是特殊的二叉树,必须满足规则,根节点的左子树的所有结点值都必须小于当前结点值,右子树所有结点值都必须大于当前结点值,子树递归满足此规则,因此,二叉查找树不存在重复结点。

前面提到的vector,list,deque都是序列式容器,序列式容器只有实值,不涉及到排序;关联式容器中有键值对(key-val),内部自动排序。本质区别:序列式容器是通过元素在容器中的位置顺序存储和访问元素;而关联式容器则是通过key值访问和存储元素。

关联容器中的有序容器和无序容器的区别:有序容器的底层数据结构是红黑树(自平衡的二叉查找树),无序容器底层数据结构是哈希表。红黑树的自平衡是通过如下规则实现的:a,节点是红色或者黑色;b,根节点是黑色;c,所有叶子节点是黑色;d,每个红色节点必须有两个黑色子节点(即从根到每个叶子节点的所有路径不能出现连续两个红色节点);e,从任一节点到其每个叶子节点的所有简单路径都包含相同的黑色节点。一文带你彻底读懂红黑树(附详细图解) - 知乎 (zhihu.com)

哈希表就是在key和val之间建立对应关系,使得元素的查找可以以O(1)效率进行,key和val之间的对应关系是通过散列函数建立。解决哈希冲突——闭散列和开散列(数据结构)_构造闭散列表_dhdhdhdhg的博客-CSDN博客

15,排序算法?

答:冒泡排序:比较相邻的元素。如果第一个比第二个大,就交换它们两个;对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;针对所有的元素重复以上的步骤,除了最后一个;重复前面3步,直到排序完成。

插入排序:分为已排序和未排序区间,初始已排序区间只有一个元素,就是数组第一个,遍历未排序的每一个元素,在已排序区间里找到合适的位置插入并保证已排序区间一直有序。

排序(上):为什么插入排序比冒泡排序更受欢迎 - 知乎 (zhihu.com)

【从代码实现上来看,冒泡排序的数据交换要比插入排序的数据移动复杂,冒泡排序需要三个赋值操作,而插入排序只需要一个】

选择排序:分已排序和未排序区间。每次会从未排序区间中找到最小的元素,将其放到已排序区间的末尾(有一个交换的操作)。不稳定。

快速排序:先找到一个枢纽;在原来的元素里根据这个枢纽划分,比这个枢纽小的元素排前面;比这个枢纽大的元素排后面;两部分数据依次递归排序下去直到最终有序。快速排序法(详解)_李小白~的博客-CSDN博客

堆排序:首先对数组进行建堆的操作,然后根据最大(小)堆的特点找到数组最大(小)值,然后利用删除堆顶元素的操作反复堆化的操作,重新建堆,直到数组完成排序。

终于讲清楚了!为什么说堆排序没有快速排序快?_快速排序和堆排序哪个好_码农架构的博客-CSDN博客

归并排序:归并排序是一个稳定的排序算法,归并排序的时间复杂度任何情况下都是O(nlogn),归并排序不是原地排序算法。用两个游标i和j,分别指向A[p...q]和A[q+1...r]的第一个元素。比较这两个元素A[i]和A[j],如果A[i]<=A[j],我们就把A[i]放入到临时数组tmp,并且i后移一位,否则将A[j]放入到数组tmp,j后移一位。先分治,直到每一个分治的数组中只有一个元素,然后一步一步合并。其中递归地操作。排序——归并排序(Merge sort)_努力的老周的博客-CSDN博客

16,二叉树的前中后序遍历?

答:有中序遍历和前(后)序遍历即可还原二叉树,只有前后序遍历无法还原二叉树。

前序遍历(根左右):根左右

根左右正好符合根结点寻找子结点的顺序,因此每次循环时弹栈,输出此弹栈结点并将其右结点和左结点按照顺序依次入栈。至于为什么要右结点先入栈,是因为栈后进先出的特性。右结点先入栈,就会后输出右结点。(前面的“节点”二字都写错了,应该是“结点”,没改的请大家不要在意。)

class Solution{
public:
    vector<int> preorderTraversal(TreeNode* root){
        if (root == nullptr)
            return {};
        vector<int> ans;
        stack<TreeNode*> st;
        st.push(root);
        while (!st.empty()){
            TreeNode* curr = st.top();
            st.pop();
            ans.emplace_back(curr->val);
            if (curr->right != nullptr){
                st.push(curr->right);}
            if (curr->left != nullptr){
                st.push(curr->left);}
}
        return ans;
}
};

中序遍历(左根右):中序遍历思路相较于前序遍历有很大的改变。前序遍历遇到根结点直接输出即可,但中序遍历“左中右”需先找到此根结点的左结点,因此事实上第一个被输出的结点会是整个二叉树的最左侧结点。依据这一特性,我们每遇到一个结点,首先寻找其最左侧的子结点,同时用栈记录寻找经过的路径结点,这些是输出最左侧结点之后的返回路径。之后每次向上层父结点返回,弹栈输出上层父结点的同时判断此结点是否包含右子结点,如果存在则此右结点入栈并到达新的一轮循环,对此右结点也进行上述操作。

初始化:curr定义为将要入栈的结点,初始化为root;top定义为栈顶的弹栈结点;

步骤:寻找当前结点的最左侧结点直到curr为空(此时栈顶结点即为最左侧结点);弹栈栈顶结点top并输出,判断top是否具有右结点,如果存在则令curr指向右结点,并在下一轮循环入栈;重复上述过程。

结束条件:这里可以看到结束条件有两个:1栈为空,2curr为空。这是因为中序遍历优中后右的特性,会有一个时刻栈为空但右结点并未被遍历,因此只有再curr也为空证明右结点不存在的情况下,才能结束遍历。

class Solution{
public:
    vector<int> inorderTraversal(TreeNode* root){
        vector<int> ans;  
        stack<TreeNode*> st;      
        if (root == nullptr)
            return ans;       
        TreeNode* curr = root;
        while (curr != nullptr || !st.empty()){
            while (curr != nullptr){
                st.push(curr);
                curr = curr->left;
            }
            TreeNode* top = st.top();
            st.pop();
            ans.emplace_back(top->val);
            if (top->right != nullptr){
                curr = top->right;}
}
        return ans;
}
};

后序遍历(左右根):前序遍历的过程是根左右,将其转换为根右左,也就是压栈的过程中优先压入左子树,后压入右子树;在弹栈的同时将此弹栈结点压入另一个栈,完成逆序;对新栈中的元素直接顺序弹栈并输出。

class Solution{
public:
    vector<int> postorderTraversal(TreeNode* root){
        vector<int> ans;
        if (root == nullptr){
            return ans;}
        stack<TreeNode*> st1;
        stack<TreeNode*> st2;
        TreeNode* curr = root;
        st1.push(curr);
        while (!st1.empty()){
            TreeNode* top = st1.top();
            st1.pop();
            st2.push(top);
            if (top->left != nullptr){
                st1.push(top->left);}
            if (top->right != nullptr){
                st1.push(top->right);}                    
}
        while (!st2.empty()){
            TreeNode* top2 = st2.top();
            st2.pop();
            ans.emplace_back(top2->val);
}
        return ans;
}
};
17,const和static关键字?

答:static作用:控制变量的存储方式和可见性。

static用于局部变量时,延长局部变量的生命周期,有时候我们希望局部变量在该次函数调用结束之后不被销毁,下次再调用该函数时,直接使用上一次结束时的局部变量值。这时,可以使用static修饰局部变量,这样会将原本在栈中的局部变量变成在静态存储区的静态变量,保证了局部变量不会再当前函数块结束就被销毁,会延长到整个程序结束,延长了局部变量的生命周期,但是局部变量的作用域并没有发生变化,依旧在函数里。

static用于全局变量时,全局变量要想在工程下别的文件使用,本来只需用extern关键字声明即可,但是加上static之后,改变了其作用域,由原本的工程文件下可见变成本文件下可见。

static用于函数时,作用和全局变量类似,同样是改变了作用域,使得其原本工程文件下可见变成本文件下可见。

static用于类,1),用于成员变量时,表示该成员变量由类及其所有对象共享一个副本,此时,该成员变量必须在类外初始化(静态常量成员变量除外),因为static成员变量先于对象存在,如果在创建对象时在构造函数的初始化列表里初始化,那么会出现static成员变量被多次初始化,和前面讲的所有对象共享一个副本冲突,故必须类外初始化静态成员变量。2),用于成员函数时,表示该成员函数由类及其所有对象共享一个副本,此时,该成员函数无this指针,因为该成员函数不属于特定类对象,this指针指向本对象的内容,正因为没有this指针,所有static成员函数只能访问static成员变量。static成员函数不能被virtual关键字修饰,因为虚函数的实现实际上是为对象分配一个vptr,而vptr是通过this指针调用的,故不能被virtual关键字修饰,虚函数的调用关系:this->vptr->ctable->virtual func。

const:只读。

const修饰基本类型数据类型:基本数据类型,修饰符const在类型说明符前后结果一样。确保变量为常量,值不可修改;

const修饰指针变量和引用变量:const位于*号左侧,指针所指向的变量不可改;const位于*号右侧,指针本身为常量,不可改变地址。

const应用到函数中:1),作为参数的const修饰符,调用函数时,用相应的变量初始化const常量,则在函数体中,按照const所修饰的部分进行常量化,保护了原对象的属性。(参数const通常用于参数为指针或引用的情况,因为普通的参数就是值传递,形参的改变不会影响实参的改变,一般不会使用const);2),作为函数返回值的const修饰符,声明了返回值后,const按照“修饰原则”进行修饰,起到相应的保护作用。

const在类中的用法:1),const成员变量,只在某个对象的生命周期内是常量,而对于整个类而言是可以改变的。因为类可以创建多个对象,不同的对象其const成员变量值可以不同。所以不能在类的声明中初始化const成员变量,因为类的对象在没有创建时候,编译器不知道const成员变量值是多少。【const成员变量的初始化只能在类的构造函数的初始化列表中进行】2),const成员函数的主要目的是防止成员函数修改对象的内容。const关键字和static关键字对于成员函数来说是不可以同时使用的,因为static关键字修饰静态成员函数不含有this指针,即不能实例化,const成员函数又必须具体到某一个函数。

18,C++内存分配情况?

答:栈:编译器分配和回收,存放局部变量和函数参数;

堆:程序员管理,需要手动new,delete;

全局/静态存储区:分为初始化和未初始化两个相邻区域,分别存储初始化和未初始化的全局变量和静态变量;

常量存储区:存放常量,一般不允许修改;

代码区:存放二进制代码。

19,什么是指针?指针和引用?

答:指针的引入(函数返回中return语句的局限性)

函数的缺陷,一个函数只能返回一个值,就算我们在函数里面写了多条return语句。但是只要执行任何一条return语句,整个函数调用就结束了。数组可以帮助我们返回多个值,但是数组是相同数据类型的结合,对于不同数据类型则不能使用数组。使用指针可以有效解决这个问题。程序中的数据(变量、数组等)对象总是存放在内存中,在生命周期内这些对象占据一定的内存空间,有确定的存储位置,实际上,每个内存单元都有一个地址,即以字节为单位连续编码。指针就是用来表示地址的。假如有一个整形变量int a = 10;int *p = &a;p表示a的地址,即p指向a,我们可以通过p知道a的地址,从而间接访问和操作a的值。即通过对象的地址来存储对象的方式称为指针间接访问。指针可以提高程序的效率,更重要的是能使一个函数访问另一个函数的局部变量,指针是两个函数进行数据交换必不可少的工具。

指针和引用的同异?

同:指针和引用都是地址的概念。

异:指针占用一块内存,该内存中存放的是所指对象的地址,引用则是某个对象的别名,

故程序需要为指针变量分配内存空间,而不必为引用分配;引用在定义时就被初始化,之后无法改变;指针可以发生改变。即引用的对象不能改变,指针的对象可以改变;没有空引用,但有空指针。这使得使用引用的代码效率比使用指针的更高,因为在使用引用之前不需要测试它的合法性。相反,指针应该总是被测试,防止其为空;对引用使用sizeof得到的是变量的大小,对指针使用sizeof得到的是变量的地址的大小;理论上指针的级数没有限制,但引用只有一级。即不存在引用的引用,但可以有指针的指针;++引用和++指针效果不一样,对引用的++操作直接反应到引用对象,对指针的++操作会使指针指向下一个对象,而不是改变所指对象的值。
最详细的讲解C++中指针的使用方法(通俗易懂)_c++指针_Pink&Sakura的博客-CSDN博客

20,C++中指针参数传递和引用参数传递?

答:值传递:形参是实参的拷贝,改变形参的值并不会影响外部实参的值。从被调函数的角度来说,值传递是单向的(实参到形参),参数的值只能传入,不能传出,当函数内部需要修改参数,并且不希望这个改变影响调用者时,采用值传递。

指针传递:形参为指向实参地址的指针,当对形参的指向对象操作时,就相当于对实参本身进行操作。

引用传递:形参相当于实参的别名,对形参的操作其实就是对实参的操作。虽然被调函数此时的形参也在栈中开辟了空间,但这时存放的是主调函数的实参地址,被调函数的任何操作都被当做是一种间接寻址,即通过栈中存放的地址访问主调函数中的实参变量,此时,任何被调函数的形参操作都会影响主调函数中的实参变化。

指针参数传递本质上是一种值传递,它所传递的是一个地址。值传递过程中,被调函数的形参作为被调函数的局部变量处理,即在栈中开辟了内存空间用以存放由主调函数放进来的实参的值,从而成为了实参的一个副本。值传递的特点是被调函数对形参的任何操作都是作为局部变量进行,不会影响主调函数的实参变量的值。(这里的不会影响主调函数的实参变量的值,指的是实参指针本身的地址不会变)。

指针传递和引用传递一般适用于:函数内部修改参数并且希望改动影响调用者。对比指针/引用传递可以将改变由形参“传给”实参(实际上就是直接在实参的内存上修改,不向值传递将实参的值拷贝到另外的内存地址中才修改)。

另一种用法是,当一个函数实际需要返回多个值,而只能显示的返回一个值时,可以将另外需要返回的变量以指针/引用传递给函数,这样在函数内部修改并且返回后,调用者可以拿到被修改过后的变量,也相当于一个隐式的返回值。

C++笔记——参数传递中的指针传递和引用传递 - 知乎 (zhihu.com)

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值