当前位置:   article > 正文

C++面试题和笔试题(五)

C++面试题和笔试题(五)

一、

  1. #include <iostream>
  2. using namespace std;
  3. class Base {
  4. public:
  5. Base(int j) : i(j) {}
  6. virtual ~Base() {}
  7. void func1() {
  8. i *= 10;
  9. func2();
  10. }
  11. int getvalue() {
  12. return i;
  13. }
  14. protected:
  15. virtual void func2() {
  16. i++;
  17. }
  18. protected:
  19. int i;
  20. };
  21. class Child : public Base {
  22. public:
  23. Child(int j) : Base(j) {}
  24. void func1() {
  25. i *= 100;
  26. func2();
  27. }
  28. protected:
  29. void func2() {
  30. i += 2;
  31. }
  32. };
  33. int main() {
  34. Base* pb = new Child(1);
  35. pb->func1();
  36. cout << pb->getvalue() << endl;
  37. delete pb;
  38. return 0;
  39. }

输出的结果是(102)

官方解释:

  1. 基类Base:定义了一个整数成员i和两个虚函数func1func2func1函数首先将i乘以10,然后调用func2func2在基类中的实现是将i加1。

  2. 派生类Child:从Base类公开继承。它重写了func1func2函数。在Child中,func1首先将i乘以100,然后调用func2func2派生类中的实现是将i加2。

  3. main函数:创建了一个指向Child对象的Base指针pb。通过pb调用func1函数。由于多态性,将调用Child类中的func1。最后,输出i的值并删除动态分配的对象。

程序的输出将取决于func1func2的调用顺序以及它们在基类和派生类中的实现。在这个例子中,由于Child类重写了func1func2,所以最终i的值将会是1 * 100 + 2 = 102Child类的func1乘以100,然后Child类的func2加2)。输出将是102

自己的理解:

假设现在我们有两个这样的魔法盒子,一个是爸爸的,一个是孩子的。爸爸的盒子只有“变大10倍并加1”这个按钮,而孩子的盒子既有“变大10倍并加1”按钮,又有“变大100倍并加2”按钮。

但是,孩子很调皮,他把他的盒子给了爸爸,并且让爸爸以为这就是他的盒子。当爸爸按下“变大10倍并加1”这个按钮时,其实他按的是孩子盒子的按钮,所以盒子的东西会按照孩子的规则来变大。

假设一开始盒子里有1个苹果,当爸爸按下按钮后,这个苹果会先变成100个(因为是按照孩子的盒子的“变大100倍”规则),然后再加2个,最后盒子里就有102个苹果了。

最后,爸爸会告诉我们盒子里有多少个苹果,我们就知道了。

在这个例子中,爸爸的盒子就像是基类Base,孩子的盒子就像是派生类Child。爸爸按下的按钮就像是基类的函数func1,而孩子盒子的特殊功能就像是派生类重写的函数。因为爸爸实际上拿的是孩子的盒子,所以按下按钮后,会按照孩子的规则来变化

考点

  1. 多态性:通过基类指针调用派生类对象的虚函数时,实际调用的是派生类中定义的版本。这里 pb->func1(); 和 pb->func2(); 展示了多态性的使用。

  2. 虚函数func2 被声明为 protected 和 virtual,这意味着它可以在派生类中被重写,同时保证多态性的行为。

  3. 构造函数和析构函数:构造函数用于初始化对象,析构函数用于清理对象。这里 Base 和 Child 类的构造函数展示了如何初始化基类成员变量,而析构函数则展示了如何安全地销毁对象。

  4. 继承Child 类公开继承自 Base 类,这意味着 Child 对象可以使用 Base 类的公有和保护成员。

  5. 访问控制Base 类中的 func2 和 i 被声明为 protected,这意味着它们可以在派生类中被访问,但不能从 Base 类的对象直接访问。

  6. 内存管理:使用 new 和 delete 进行动态内存分配和释放是 C++ 中的一个重要概念,这里展示了如何正确地使用它们。

  7. 错误处理:虽然代码中没有显式展示错误处理,但在实际编程中,处理动态内存分配失败(虽然在现代系统上很少见)或其他异常情况也是重要的考点。

 二、改错题

1)

  1. void GetMemory(char*p)
  2. {
  3. p=(char*)malloc(100);
  4. }
  5. void Test(void)
  6. {
  7. char *str=NULL;
  8. GetMemory(str);
  9. strcpy(str,"hello world");
  10. printf(str);
  11. }

问题一:GetMemory 函数中的指针修改不反映到 Test 函数中。
在 GetMemory 函数中,p 是一个指向 char 的指针的本地副本。当你将 p 指向 malloc 分配的内存时,Test 函数中的 str 并没有改变,因为它传递的是 str 的一个副本,而不是 str 本身的地址。因此,Test 函数中的 str 仍然是 NULL

问题二:内存泄漏。
由于 GetMemory 函数中的 malloc 分配的内存没有被释放,这会导致内存泄漏。

问题三:strcpy 可能导致未定义行为。
由于 str 是 NULL,尝试使用 strcpy 将字符串复制到它指向的位置会导致未定义行为,通常是程序崩溃。

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <string.h>
  4. void GetMemory(char **p) {
  5. *p = (char*)malloc(100);
  6. if (*p == NULL) {
  7. // 错误处理,分配内存失败
  8. exit(EXIT_FAILURE);
  9. }
  10. }
  11. void Test(void) {
  12. char *str = NULL;
  13. GetMemory(&str); // 传递str的地址
  14. if (str != NULL) {
  15. strcpy(str, "hello world");
  16. printf("%s\n", str);
  17. free(str); // 释放分配的内存
  18. }
  19. }
  20. int main() {
  21. Test();
  22. return 0;
  23. }
  1. GetMemory 函数现在接受一个指向 char 指针的指针(即 char **),这样它就可以修改 Test 函数中的 str
  2. 使用 malloc 分配内存后,我们检查是否成功分配了内存。
  3. 在 Test 函数中,我们检查 str 是否为 NULL,然后才尝试使用 strcpy
  4. 使用完分配的内存后,我们在 Test 函数中释放它,以避免内存泄漏。
  5. malloc 分配的内存应该用 free 来释放,而且应该在所有使用完这块内存的地方都这么做 

 自己的理解:

想象你有一个存钱罐,你想要放更多的钱进去,但是你还没有足够的硬币。于是,你决定去银行取一些钱。

GetMemory 函数就像是你去银行取钱的过程。你告诉银行你想要100个硬币(这就像是分配100个字节的内存)。银行给你一个装满硬币的袋子(这就像是malloc返回的内存地址)。

Test 函数就像是你回到家,想要把取回来的钱放进存钱罐里。但是,如果你忘记从银行带钱回来(也就是说,str 仍然是NULL),你尝试往存钱罐里放钱时,就会出问题,因为没有钱可以放(这会导致程序崩溃)。

为了避免这个问题,你在GetMemory函数里不是直接告诉银行你要取钱,而是告诉银行你存钱罐的地址(这就像是传递str的地址,即&str)。这样,银行就能直接把装满硬币的袋子放到你的存钱罐里。

当你把钱放进存钱罐后,记得以后不用了要把钱放回银行,不然你的家就会堆满钱,没地方放了(这就像是内存泄漏)。所以,在Test函数最后,我们使用free(str)来释放我们之前分配的内存。

这样,你就学会了如何正确地取钱(分配内存)和存钱(释放内存),而不会让家里变得乱七八糟。

2)

  1. #include<iostream>
  2. using namespace std;
  3. class A
  4. {
  5. public:
  6. A()
  7. {
  8. cout<<"new A"<<endl;
  9. m=(char*)malloc(10);
  10. }
  11. ~A()
  12. {
  13. cout<<"del A"<<endl;
  14. }
  15. protected:
  16. char *m;
  17. }
  18. class B:public A
  19. {
  20. public:
  21. B()
  22. {
  23. cout<<"new B"<<endl;
  24. m=(char*)malloc(100);
  25. }
  26. ~B()
  27. {
  28. cout<<"del B"<<endl;
  29. }
  30. protected:
  31. char*m;
  32. };
  33. int main()
  34. {
  35. A*c=new B;
  36. delete c;
  37. }

错误和需要注意的地方:

  1. class B 中重定义了 m 成员变量。在继承关系中,如果基类 A 和派生类 B 中都有名为 m 的成员变量,那么它们将被视为两个不同的变量。这可能会导致混淆和错误。

  2. 在 main 函数中,你使用 new B 创建了一个 B 类的对象,但是将其赋值给了一个 A 类型的指针 c。虽然这是合法的(因为 B 是从 A 公有继承的),但需要注意,当你通过 A 类型的指针访问 m 成员时,你会访问到基类 A 中的 m,而不是派生类 B 中的 m

  3. 在 A 和 B 的构造函数中,你使用 malloc 为 m 分配了内存,但在析构函数中并没有使用 free 来释放这些内存。在C++中,更推荐使用 new 和 delete 来管理动态内存,因为 new 会自动调用对象的构造函数,而 delete 会自动调用析构函数。

  1. #include<iostream>
  2. using namespace std;
  3. class A {
  4. public:
  5. A() {
  6. cout << "new A" << endl;
  7. m = new char[10]; // 使用 new 分配内存
  8. }
  9. virtual ~A() { // 声明为虚析构函数,确保派生类的析构函数也会被调用
  10. cout << "del A" << endl;
  11. delete[] m; // 使用 delete[] 释放内存
  12. }
  13. protected:
  14. char *m;
  15. };
  16. class B : public A {
  17. public:
  18. B() {
  19. cout << "new B" << endl;
  20. // 注意:这里不应该再次分配 m,因为 m 已经在 A 的构造函数中被分配了。
  21. // 如果你确实需要在 B 中分配新的内存,你应该使用一个不同的变量名。
  22. }
  23. ~B() {
  24. cout << "del B" << endl;
  25. // 在这里,我们不需要释放 m,因为 A 的析构函数会负责释放它。
  26. // 如果 B 有自己的内存需要释放,应该在这里做。
  27. }
  28. };
  29. int main() {
  30. A* c = new B; // 创建 B 的对象,但用 A 的指针指向它
  31. delete c; // 使用 delete 释放 c 指向的对象,这会先调用 B 的析构函数,然后调用 A 的析构函数
  32. return 0;
  33. }

 在这个修改后的版本中,我将 A 的析构函数声明为虚函数(virtual ~A())。这是为了确保当通过基类指针删除派生类对象时,派生类的析构函数也会被调用。此外,我还使用了 new 和 delete[] 来分配和释放内存,这是C++中更推荐的做法。

3)

  1. #include<iostrem>
  2. using namespace std;
  3. class Test
  4. {
  5. public:
  6. long a;
  7. long b;
  8. virtual void fun(){}
  9. Test(long temp1=0,long temp2=0)
  10. {
  11. a=temp1;
  12. b=temp2;
  13. }
  14. long getA(){return a;}
  15. long getB(){return b;}
  16. };
  17. int maint()
  18. {
  19. Test obj(5,10);
  20. long * pint =(long*)&obj;
  21. *(pint)=100;
  22. *(pint+1)=200
  23. cout<<"a="<<obj.getA()<<endl;
  24. cout<<"b="<<obj.getB()<<endl;
  25. return 0;
  26. }

程序在64位机器上打印输出结果是(a=100,b=200)

官方解释:

    在 main 函数中,你创建了一个 Test 类的对象 obj,并初始化了 a 为5,b 为10。然后,你创建了一个指向 long 的指针 pint,并将其设置为指向 obj 的地址。接下来,你通过 pint 修改了 obj 的内存内容。

  1. long * pint =(long*)&obj;
  2. *(pint)=100;
  3. *(pint+1)=200;

这里,*(pint)=100; 将 obj 的起始地址(即 a 的值)设置为100。*(pint+1)=200; 将 obj 起始地址后8字节的位置(即 b 的值)设置为200。

因此,obj 的成员变量 a 和 b 现在分别被设置为100和200。

输出将是:a=100 b=200。

自己的理解:

当然可以,让我们用小学生能懂的方式和生活中的例子来解释这段代码。

首先,想象一下你有一个魔法盒子,这个盒子里面可以放两个魔法球,每个魔法球都有一个数字。这个魔法盒子就是我们代码中的Test类,而两个魔法球就是ab这两个变量。

现在,我们有一个特别的咒语,可以让这个魔法盒子里的魔法球变出数字来。这个咒语就是Test类的构造函数,它可以在我们创建魔法盒子的时候,给两个魔法球分别赋予数字。

  1. Test(long temp1=0,long temp2=0)
  2. {
  3. a=temp1;
  4. b=temp2;
  5. }

比如说,我们念这个咒语:“魔法盒子,给我一个5和一个10!”然后,魔法盒子里的两个魔法球就分别变成了5和10。

main函数中,你创建了一个这样的魔法盒子,并给它念了咒语,让里面的两个魔法球变成了5和10。

接下来,你有一个魔法棒,这个魔法棒可以让你直接看到魔法盒子里面的东西。但是,你这次没有用魔法棒去看,而是直接用手去摸魔法盒子,并试图改变魔法球里的数字。

  1. long * pint =(long*)&obj;
  2. *(pint)=100;
  3. *(pint+1)=200;

你用手伸进了魔法盒子,摸到了第一个魔法球,然后把它变成了100。接着,你又摸到了第二个魔法球,把它变成了200。

最后,你用魔法棒去看魔法盒子里的魔法球,发现它们已经变成了100和200,而不是原来的5和10了。

  1. cout<<"a="<<obj.getA()<<endl;
  2. cout<<"b="<<obj.getB()<<endl;

所以,这个程序在64位机器上打印的输出结果是:a=100 b=200;

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家小花儿/article/detail/236872?site
推荐阅读
相关标签
  

闽ICP备14008679号