• C/C++笔试易错与高频题型&图解知识点(二)—— C++部分(持续更新中)


    目录

    1.构造函数初始化列表

    1.1 构造函数初始化列表与函数体内初始化区别

    1.2 必须在初始化列表初始化的成员

    2 引用&引用与指针的区别

    2.1 引用初始化以后不能被改变,指针可以改变所指的对象

     2.2 引用和指针的区别

    3 构造函数与析构函数系列题

    3.1构造函数与析构函数的调用次数

    4 类的运算符重载

    5 类的静态数据成员

    5.1 malloc/new/new[]

    5.2 new的实现步骤与细节

    6 this指针相关题目 

    6.1 this可以为空吗?

    6.2 this指针存放在哪里?

    6.3 delete this

    7 其他于类相关的题目

    7.1 空类的大小

    7.2 对const变量的修改

      volatile

     7.3 赋值运算符重载



    1.构造函数初始化列表

    有一个类A,其数据成员如下: 则构造函数中,成员变量一定要通过初始化列表来初始化的是:______。

    1. class A {
    2. ...
    3. private:
    4. int a;
    5. public:
    6. const int b;
    7. float* &c;
    8. static const char* d;
    9. static double* e;
    10. };

    A. a b c

    B. b c

    C. b c d e

    D. b c d

    E. b

    F. c

    答案:B

    知识点:

    1.1 构造函数初始化列表与函数体内初始化区别

    一个类,其包含一个类类型成员,对于它的构造函数,如果在函数体内初始化,会先调用其类类型成员的默认构造函数,再调用赋值运算符;而在构造函数初始化时会直接调用它的拷贝构造函数进行初始化

    函数体类初始化:

    1. #include
    2. class B {
    3. public:
    4. B() { std::cout << "B defualt construct" << '\n'; }
    5. B(int t) : _t(t) { std::cout << "B construct" << '\n'; }
    6. B(const B& b) : _t(b._t) { std::cout << "B copy construct" << '\n'; }
    7. B& operator=(const B& b) {
    8. _t = b._t;
    9. std::cout << "B assign operator"<< '\n';
    10. return *this;
    11. }
    12. private:
    13. int _t = 0;
    14. };
    15. class A {
    16. public:
    17. A() { std::cout << "A defualt construct" << '\n'; }
    18. A(const B& b){
    19. puts("---------------------");
    20. _b = b;
    21. std::cout << "A construct" << '\n';
    22. }
    23. A(const A& a) : _b(a._b) { std::cout << "A copy construct" << '\n'; }
    24. A& operator=(const A& a) {
    25. _b = a._b;
    26. std::cout << "A assign operator" << '\n';
    27. return *this;
    28. }
    29. private:
    30. B _b;
    31. };
    32. int main() {
    33. B b(1);
    34. A a(b);
    35. }

    初始化列表初始化:

    1. #include
    2. class B {
    3. public:
    4. B() { std::cout << "B defualt construct" << '\n'; }
    5. B(int t) : _t(t) { std::cout << "B construct" << '\n'; }
    6. B(const B& b) : _t(b._t) { std::cout << "B copy construct" << '\n'; }
    7. B& operator=(const B& b) {
    8. _t = b._t;
    9. std::cout << "B assign operator"<< '\n';
    10. return *this;
    11. }
    12. private:
    13. int _t = 0;
    14. };
    15. class A {
    16. public:
    17. A() { std::cout << "A defualt construct" << '\n'; }
    18. A(const B& b) : _b(b) {
    19. puts("---------------------");
    20. std::cout << "A construct" << '\n';
    21. }
    22. /*A(const B& b){
    23. puts("---------------------");
    24. _b = b;
    25. std::cout << "A construct" << '\n';
    26. }*/
    27. A(const A& a) : _b(a._b) { std::cout << "A copy construct" << '\n'; }
    28. A& operator=(const A& a) {
    29. _b = a._b;
    30. std::cout << "A assign operator" << '\n';
    31. return *this;
    32. }
    33. private:
    34. B _b;
    35. };
    36. int main() {
    37. B b(1);
    38. A a(b);
    39. }

    1.2 必须在初始化列表初始化的成员

    • const修饰的成员变量

    • 引用类型成员

    • 类类型成员,且该类没有默认构造函数(由1.1内容可得)

    2 引用&引用与指针的区别

    2.1 引用初始化以后不能被改变,指针可以改变所指的对象

    1. int main() {
    2. int a = 10;
    3. int& ref = a;
    4. int b = 20;
    5. ref = b;
    6. std::cout << "a:" << a << " ref:" << ref << " b:" << b;
    7. //output:a:20 ref:20 b:20
    8. }

     2.2 引用和指针的区别

    引用和指针,下面说法不正确的是()

    A. 引用和指针在声明后都有自己的内存空间

    B. 引用必须在声明时初始化,而指针不用

    C. 引用声明后,引用的对象不可改变,对象的值可以改变,非const指针可以随时改变指向的对象以及对象的值

    D. 空值NULL不能引用,而指针可以指向NULL

    答案:A

    1. #include
    2. int main() {
    3. int a = 10;
    4. int& ref = a;
    5. std::cout << "a:" << &a << '\n' << "ref:" << &ref << '\n';
    6. //a:00FCF8D4 ref:00FCF8D4
    7. int b = 10;
    8. int* ptr = &b;
    9. std::cout << "b:" << &b << '\n' << "ptr:" << &ptr << '\n';
    10. //b : 00FCF8BC ptr: 00FCF8B0
    11. return 0;
    12. }

     从定义内存上看,引用和被引用变量公用同一块空间

    3 构造函数与析构函数系列题

    3.1构造函数与析构函数的调用次数

    1)

    C++语言中,类ClassA的构造函数和析构函数的执行次数分别为()

    1. ClassA *pclassa=new ClassA[5];
    2. delete pclassa;

    A. 5,1

    B. 1,1

    C. 5,5(错误)

    D. 1,5

    答案:A 

    2)

    1. #include
    2. #include
    3. using namespace std;
    4. class Test {
    5. public:
    6. Test(){ std::cout << this << "B defualt construct" << '\n'; }
    7. ~Test() { std::cout << this << "B destory" << '\n'; }
    8. };
    9. int main() {
    10. Test t1;
    11. puts("------------");
    12. Test* t2;
    13. puts("------------");
    14. Test t3[3];
    15. puts("------------");
    16. Test* t4[3]; //t4是存放三个类型Test*的对象的数组
    17. puts("------------");
    18. Test(*t5)[3]; //t5是数组指针,指向一个存放三个类型为Test的对象的数组
    19. puts("------------");
    20. }

     打印结果:

    4 类的运算符重载

    在重载一个运算符为成员函数时,其参数表中没有任何参数,这说明该运算符是 ( )。

    A. 无操作数的运算符

    B. 二元运算符

    C. 前缀一元运算符

    D. 后缀一元运算符(错误)

    答案:C

    例如:

    前置++:T& operator++() {} 

    后置++:T operator++(int) {}

    5 类的静态数据成员

    下面有关c++静态数据成员,说法正确的是()

    A. 不能在类内初始化(错误)

    B. 不能被类的对象调用

    C. 不能受private修饰符的作用

    D. 可以直接用类名调用  

    答案:D : 

    知识点:const修饰的静态成员可以在类内初始化,所以A错误

    5.1 malloc/new/new[]

    malloc/calloc/realloc <----> free        new <----> delete        new [] <----> delete[]三者一定要匹配使用,否则会产生内存泄漏或者程序崩溃

    5.2 new的实现步骤与细节

    1) 对于 T*p = new T;

    -第一步: 调用operator new(size_t size)申请空间(内部调用malloc循环申请)

    -第二步: 调用构造函数完成对申请空间的初始化

         对于 delete p;

    -第一步:调用析构函数释放p指向的对象中的资源

    -第二步:调用operator delete释放p所指向的空间(内部调用free)

    2)对于 T*p = new T[N];

    -第一步: 调用operator new[](size_t size)申请空间(内部调用operator new(size_t size))

    -第二步: 调用N次T的构造函数完成对申请空间的初始化

         对于 delete p;

    -第一步:调用N次T的析构函数释放p指向的N个对象中的资源

    -第二步:调用operator delete[]释放p所指向的空间(内部调用operator delete)

    6 this指针相关题目 

    6.1 this指针存放在哪里?

    this指针是对象调用自身方法时的第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,通过下面三张图就可以自然的理解到this指针

     第一张图可以看到,寄存器ecx存放的值是对象t的起始四个字节的地址

    指令lea:加载有效地址(load effective address)指令就是lea,他的指令形式就是从内存读取数据到寄存器,但是实际上他没有引用内存,而是将有效地址写入到目的的操作数,就像是C语言地址操作符&一样的功能,可以获取数据的地址。

    callfunc1或者func2之前会将对象t的起始四个字节的地址加载到ecx寄存器

     第二张图,打印对象t的地址,调用func2,将n的值对象成员n的值改为1

    调用函数,将eax寄存器push如栈,此时在监视窗口可以看到this指针和ecx的值相同,我的理解是this是函数的隐含参数,函数调用时创建,其值为eax的值,也就是最开始存放在eax中的对象的起始地址;

     6.2 this可以为空吗?

    答案是可以的,我们之前说到,this指针的值是对象的起始地址,如果当对象的地址为nullptr时,那么this就为空。

    但是这是对this的解引用或者访问成员操作就是错误的。

    函数func1调用完成没有异常,func2函数内部异常

    6.3 delete this 以及 delete细节解析

    如果有一个类是 myClass , 关于下面代码正确描述的是:

    1. myClass::~myClass(){
    2. delete this;
    3. this = NULL;
    4. }

    A. 正确,我们避免了内存泄漏

    B. 它会导致栈溢出

    C. 无法编译通过                            

    D. 这是不正确的,它没有释放任何成员变量。(错误) 

    答案:C

    对于上述代码,首先它是不能被编译通过的,因为this指针本身被const修饰(对于上述例子而言this指针的类型为myClass *const), this指针本身无法被修改

    如果删去`this = NULL`这一段代码,程序还是有错,我们通过下面几个例子说明⬇️

    首先我们需要了解:调用delete函数之后会依次执行下面两个步骤 

    ① 对目标调用的析构函数

    ② 调用operator delete释放内存

    通过下面几种了解:

    1)

    1. #include
    2. using namespace std;
    3. class Test {
    4. public:
    5. Test() {
    6. puts("Test()");
    7. x = 0;
    8. ptr = new int(0);
    9. }
    10. ~Test() {
    11. puts("~Test() before");
    12. delete this;
    13. //this = nullptr; //编译错误 C2106“ = ”: 左操作数必须为左
    14. puts("~Test() after");
    15. }
    16. private:
    17. int x;
    18. int* ptr;
    19. };
    20. int main() {
    21. Test t;
    22. }

     上面这段代码执行会不断打印~Test() before,直至程序栈溢出

    解释了调用operator delete之后的执行步骤,上述代码会this指针指向对象的析构函数,而析构函数中又有delete函数,导致死循环,如下图⬇️

    2)

    1. #include
    2. using namespace std;
    3. class Test2 {
    4. public:
    5. Test2() {
    6. ptr = new int(0);
    7. }
    8. ~Test2() {
    9. puts("~Test2");
    10. delete ptr;
    11. ptr = nullptr;
    12. }
    13. void deletefunc() {
    14. delete this; //先析构,再delete this指向的堆空间(当this指向的是栈上的空间时,程序崩溃)
    15. }
    16. private:
    17. int* ptr;
    18. int x = 0;
    19. };
    20. int main() {
    21. Test2* tptr = new Test2();
    22. tptr->deletefunc();
    23. }

    通过上述代码和动画演示巩固delete的两个步骤;

    如过将对象创建再栈中,上述程序又会出现bug:编译阶段不会报错,但是再运行到delete this的时候程序崩溃了,原因是对栈上的空间进行了释放

    1. Test2 obj = Test2();
    2. obj.deletefunc();

    3)

    1. #include
    2. using namespace std;
    3. void operator delete(void* ptr) {
    4. puts("operator delete");
    5. }
    6. class Test2 {
    7. public:
    8. Test2() {
    9. ptr = new int(0);
    10. }
    11. ~Test2() {
    12. puts("~Test2");
    13. delete ptr;
    14. ptr = nullptr;
    15. }
    16. void deletefunc() {
    17. delete this;
    18. }
    19. private:
    20. int* ptr;
    21. int x = 0;
    22. };
    23. int main() {
    24. Test2* ptr = new Test2();
    25. ptr->deletefunc();
    26. }

    调试上述代码

    7. 继承与多态

    几乎所有题目知识点这篇文章有覆盖,即使复习!!!

    ⌈C++⌋从无到有了解并掌握C++面向对象三大特性——封装、继承、多态-CSDN博客

    7.1 虚析构函数

    下面说法正确的是()

    A. 一个空类默认一定生成构造函数,拷贝构造函数,赋值操作符,取地址操作符,析构函数

    B. 可以有多个析构函数

    C. 析构函数可以为virtual,可以被重载(错误)

    D. 类的构造函数如果都不是public访问属性,则类的实例无法创建

    答案:A

    析构函数可以是虚函数,但是由于只能由一个析构函数,所以自然不存在重载 

    知识点:重载、覆盖、隐藏的区别

    7.2 纯虚函数

    1)

    以下关于纯虚函数的说法,正确的是()

    A. 声明纯虚函数的类不能实例化

    B. 声明纯虚函数的类成虚基类

    C. 子类必须实现基类的纯虚函数(错误)

    D. 纯虚函数必须是空函数

    答案:A 

    具体知识点在《⌈C++⌋从无到有了解并掌握C++面向对象三大特性——封装、继承、多态》的第三章的第三节

    含有(或者未经覆盖直接继承)纯虚函数的类是抽象基类(abstract base class)。抽象基类负责定义接口(因此又叫做接口类),而后续的其他类可以覆盖该接口。   (C++ Primer   P540)

    抽象基类(或未覆盖纯虚函数直接继承的派生类)无法实例化出对象

    2)

    关于抽象类和纯虚函数的描述中,错误的是

    A. 纯虚函数的声明以“=0;”结束

    B. 有纯虚函数的类叫抽象类,它不能用来定义对象 

    C. 抽象类的派生类如果不实现纯虚函数,它也是抽象类(错误答案)

    D. 纯虚函数不能有函数体

    答案:D        纯虚函数是可以由函数体的 

    1. #include
    2. using namespace std;
    3. class A {
    4. public:
    5. virtual void fun() = 0 {
    6. puts("A:virtual void fun() = 0");
    7. }
    8. };
    9. class C : public A {
    10. public:
    11. virtual void fun() {
    12. puts("C:virtual void fun()");
    13. }
    14. };
    15. int main() {
    16. C c;
    17. c.fun(); //output: C:virtual void fun()
    18. return 0;
    19. }

    7.3 继承与组合

    具体知识点在《⌈C++⌋从无到有了解并掌握C++面向对象三大特性——封装、继承、多态》的第二章的第7节

    面向对象设计中的继承和组合,下面说法错误的是?()

    A. 继承允许我们覆盖重写父类的实现细节,父类的实现对于子类是可见的,是一种静态复用,也称为白盒复用

    B. 组合的对象不需要关心各自的实现细节,之间的关系是在运行时候才确定的,是一种动态复用,也称为黑盒复用(错误)

    C. 优先使用继承,而不是组合,是面向对象设计的第二原则

    D. 继承可以使子类能自动继承父类的接口,但在设计模式中认为这是一种破坏了父类的封装性的表现

    答案:C

    7.4 继承体系中的构造与析构顺序

    7.4.1 基类部分隐式销毁

    具体知识点在《⌈C++⌋从无到有了解并掌握C++面向对象三大特性——封装、继承、多态》的第二章的第5.4小节

    C++将父类的析构函数定义为虚函数,下列正确的是哪个()

    A. 释放父类指针时能正确释放子类对象

    B. 释放子类指针时能正确释放父类对象

    C. 这样做是错误的

    D. 以上全错

    答案:A 

    知识点:

    在析构函数题执行完成后,对象成员会被隐式销毁。类似的,对象的基类部分也是隐式销毁的。因此,和构造函数及赋值运算符不同的是,派生类析构函数只负责销毁由派生类自己分配的资源    (C++ Primer   P556)

    为什么先析构子类再析构父类?如果先析构父类会怎么样?

    如果先析构父类,父类析构后,若子类再析构之前需要访问父类成员访问的则是一个空指针;而先析构子类则没有这个风险,因为父类不能访问子类成员

     7.4.2 多继承构造顺序

    由下图可知:

    1)

     2)

    下面这段代码会打印出什么? 

    1. class A
    2. {
    3. public:
    4. A()
    5. {
    6. printf("A ");
    7. }
    8. ~A()
    9. {
    10. printf("deA ");
    11. }
    12. };
    13. class B
    14. {
    15. public:
    16. B()
    17. {
    18. printf("B ");
    19. }
    20. ~B()
    21. {
    22. printf("deB ");
    23. }
    24. };
    25. class C : public A, public B
    26. {
    27. public:
    28. C()
    29. {
    30. printf("C ");
    31. }
    32. ~C()
    33. {
    34. printf("deC ");
    35. }
    36. };
    37. int main()
    38. {
    39. A* a = new C();
    40. delete a;
    41. return 0;
    42. }

    A. A B C deA

    B. C A B deA

    C. A B C deC(错误答案)

    D. C A B deC

     答案:A 

    8 其他于类相关的题目

    8.1 空类的大小

    在Windows 32位操作系统中,假设字节对齐为4,对于一个空的类A,sizeof(A)的值为()? A. 0

    B. 1

    C. 2

    D. 4(错误)

    答案:B

    类大小的计算方式:与结构体大小的计算方式类似,将类中非静态成员的大小按内存对齐规则计算,并且不用计算成员函数;

    特别的,空类的大小在主流的编译器中设置成了1

    8.2 对const变量的修改

    以下程序输出是____。

    1. #include
    2. using namespace std;
    3. int main(void)
    4. {
    5. const int a = 10;
    6. int * p = (int *)(&a);
    7. *p = 20;
    8. cout<<"a = "<", *p = "<<*p<
    9. return 0;
    10. }

    A. 编译阶段报错运行阶段报错

    B. a = 10, *p = 10

    C. a = 20, *p = 20(错误)

    D. a = 10, *p = 20

    E. a = 20, *p = 10

     答案:D

    知识点:

    1)编译器在编译阶段会对const修饰的变量进行优化,将其替换成变量的值

    由图中的汇编代码可以看到,打印变量a时,他被直接替换成了10这个常量

      volatile

    C/C++ 中的 volatile 关键字和 const 对应,用来修饰变量,volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改,比如:操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。

    1. #include
    2. using namespace std;
    3. int main(void)
    4. {
    5. const int volatile a = 10;
    6. int* p = (int*)(&a);
    7. *p = 20;
    8. cout << "a = " << a << ", *p = " << *p << endl;
    9. return 0;
    10. }

    当用volatile修饰a之后打印结果为:

     8.3 赋值运算符重载

    下列关于赋值运算符“=”重载的叙述中,正确的是

    A. 赋值运算符只能作为类的成员函数重载

    B. 默认的赋值运算符实现了“深层复制”功能

    C. 重载的赋值运算符函数有两个本类对象作为形参(错误)

    D. 如果己经定义了复制拷贝构造函数,就不能重载赋值运算符

    答案:A

  • 相关阅读:
    [hadoop全分布部署]虚拟机Hadoop集群交换 SSH 密钥与验证SSh无密码登录
    RS485modbus转Profinet网关协议连接富凌DZB300系列变频器配置方法
    计算机毕业设计Java研究生实验室综合管理系统(源码+系统+mysql数据库+Lw文档)
    基于分时电价策略的家庭能量系统优化附Matlab代码
    将算力普惠到底 阿里云开启金秋上云季:数百款爆品享专属特惠价阿里云上新金秋云创季:上百款云产品享特惠 全能爆品仅99元/年
    熟悉c语言指针
    4.MidBook项目经验之MonogoDB和easyExcel导入导出
    使用curl执行Http请求
    深度分析React源码中的合成事件
    前端代码统计工具cloc的安装与使用
  • 原文地址:https://blog.csdn.net/Dusong_/article/details/133800895