牛客网C++选择题笔记

这是牛客网上的C/C++的选择题的错题,随做随更,有一些题还是有点意思。

当然不是所有都有总结,有明显歧义的不管,过于简单的不管(简不简单因人而异,我先阅读了C++ Primer后再开始做的练习,因此对于原书中重点强调过的知识会记忆更深刻,书中没怎么提到的知识就相对容易出错)。

地址:https://www.nowcoder.com/exam/intelligent中C++板块


1. switch的本质

#include "stdio.h"
int main() {
    int c = 0,k;
    for(k = 1;k < 3;k++) {
        switch (k) {
            default : c += k;
            case   2: c++;
                break;
            case   4: c += 2;
                break;
        }
    }
    printf("%d\n", c);
    return 0;
}

以下程序的输出结果是3。

解析:switch的规则是

  • 首先寻找匹配的case,若找不到就匹配default(即使default写在前面)
  • 然后忽略掉其他所有标签,从匹配到的标签开始一直执行到末位或break处(这就是可以叠加case的原因和一般要写break的原因)

如果没有匹配到case又没有default则不执行。实际编码中不应像上文这么写,如果有default应该写最后。

2. 只能内重载的运算符

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

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

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

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

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

答案为A。解析::: * . ?:是特殊的不能重载的运算符,而更特殊一类运算符的是= [] () ->,他们可以重载,但是必须重载在函数内部,不允许重载为外部友元函数,即CLASS& operator=(CLASS &x, CLASS &y)这样的形式是不被允许的。

原因见此:CSDN 关于有些运算符只能用成员函数重载

3. 常量字符串的赋值

const char *p1 ="hello";
char *const p2 = "world";

给出以下定义,下列哪些操作是合法的?

A. p1++;

B. p1[2] = 'w';

C. p2[2] = 'l';

D. p2++;

选A。解析:首先我们要理解顶层和底层const,const*代表指针指向的值不能修改,*const代表指针自身不能被修改,因此B和D错误。而一个字面量字符串其实是一个常量字符数组,被单独存放在只读区,他虽然可以赋值给没有顶层const的指针,但是却仍然不可修改,因此C错误。在实际测试中,C选项会给出警告,并在运行时发生错误。

4. 传指针却不修改值

#include <stdio.h>
#include <stdlib.h>
void fun(int *p1, int *p2, int *s) {
    s = (int *)malloc(sizeof(int));
    *s = *p1 + *(p2++);
}
int main() {
    int a[2] = {1, 2}, b[2] = {10, 20}, *s = a;
    fun(a, b, s);
    printf("%d \n", *s);
}

答案为1,不是11。虽然s作为传入了函数,但s自己不是引用,仍是按值传递,因此改变的s,与main里的s没有关系。

5. dynamic_cast的用法

struct A1{
    virtual ~A1(){}
};
struct A2{
    virtual ~A2(){}
};
struct B1 : A1, A2{};
int main()
{
 B1 d;
 A1* pb1 = &d;
 A2* pb2 = dynamic_cast<A2*>(pb1);  //L1
 A2* pb22 = static_cast<A2*>(pb1);  //L2
 return 0;
}

A. L1语句编译失败,L2语句编译通过

B. L1语句编译通过,L2语句编译失败

C. L1,L2都编译失败

D. L1,L2都编译通过

选B。static_cast用于普通的类型转换,但是pb1是A1的指针,A1和A2之间没有继承关系,所以转换不被允许。而dymamic_cast是运行时转换,一定可以通过编译;在实际运行时,程序检测pb1的实际类型(这里是B1)能否通过继承关系转化为A2*(在这里可以),若存在关系则转换,否则返回空指针。

当然我们知道所有指针大小是一样的,如果非要在编译期转换,可以使用C风格强制转换A2* pb22 = (A2*)pb1

6. 局部变量的引用

std::string& test_str(){
   std::string str = "test";
   return str;
}
int main(){
   std::string& str_ref = test_str();
   std::cout << str_ref << std::endl;
   return 0;
}

局部变量返回后即销毁,这样的写法是不对的,但是实际上可以通过编译,只会给出警告,并在运行期发生错误。C++秉承“相信程序员”的原则不做检查,我们只能自己小心。

7. 友元声明问题

#include <math.h>
#include <iostream>
using namespace std;

class Point
{
    friend double Distance(const Point &p1, const Point &p2) /* ① */
    {
        double dx = p1.x_ - p2.x_;
        double dy = p1.y_ - p2.y_;
        return (sqrt(dx * dx + dy * dy));
    }

public:
    Point(int x, int y) : x_(x), y_(y) /* ② */
    {
    }

private:
    int x_;
    int y_;
};

int main(void)
{
    Point p1(3, 4);
    Point p2(6, 9);

    cout << Distance(p1, p2) << endl; /* ③ */
    return (0);
}

请问程序会在哪个地方发生编译错误?答案是不会。虽然Distance定义在类中,但他不是成员函数,是一个外部函数。

但是这道题有不合理的地方,友元声明和声明是不一样的,首先来说一说规范写法:

  • 类中的friend double Distance(const Point &p1, const Point &p2);是类中的“友元声明”,表示“我把秘密告诉你”。

  • 类外的double Distance(const Point &p1, const Point &p2);,是函数自己的声明。

  • 具体定义提供在声明后。

“友元声明”只是表明友元关系,与外部的声明是分离的。因此,一般来讲,即使向上面一样将定义放在友元声明后,也需要在类外再声明一次。

但是上面的代码没有这样做,这是因为另一个例外:如果这个函数的参数带有这个类,则可以不用声明。上例中Distance带有的Point参数,如果把他改成空参数,就不再能通过编译了。

实际编程中应该避免这种“例外”的发生。友元本就不属于类,应该写在外面;况且“不用声明”这个特性并非标准,某些编译器并不支持。

8. 继承类的默认构造函数

template<class T> class Foo{
        T tVar;
    public:
        Foo(T t) : tVar(t) { }
};

template<class T> class FooDerived:public Foo<T>
{
};

int main()
{
	FooDerived<int> d(5);// 1
	FooDerived<int> d;// 2
    return 0;
}

1,2两处都无法通过编译。Foo类定义了一个构造函数,因此不再有无参的默认构造函数,FooDerived类没有自定义构造函数,因此会合成一个默认构造函数,该函数又会自动调用Foo类的默认构造函数,但是这个函数不存在,所以不能通过编译。

Tip:定义一个类时,对于构造拷贝赋值析构这一系列函数,要么一个都不写,要么就整个系统都要写上,可以避免所有问题,不要把时间浪费在这种事情的思考上

9. 虚函数与结构体对齐

class A {
    virtual void func() { 
        cout << "func" << endl; 
    }
    float f; char p; int adf[3];
};

sizeof(A)是多少呢?答案是:若平台指针占4字节则答案为24,占8字节则是32。以24为例,对于有虚函数的类,每个类中都会生成一个“虚函数指针”,指针指向这个类的虚函数列表。之后是占4字节的float,占1字节但填满4字节的char,和3个int占12字节。

虚函数指针指向的是这个类的虚函数列表,便于运行时高效确定调用哪一个函数,更多内容请见:知乎 c++虚指针和虚函数表

10. 大端模式和小端模式

unsigned int a = 0x12345678;
unsigned char b = *(unsigned char *)&a;

这时b的值是多少呢?答案是未定义的。表达式相当于取a的前一半赋值给b,那么b是0x12还是0x78?这都是可以的。在储存数据时,既可以顺着字节高低顺序内存中的四个字节为0x12 0x34 0x56 0x78(此时b为0x12),也可以反过来排列为0x78 0x56 0x34 0x12(此时b为0x78)。前者叫做大端模式,后者叫做小端模式。容易发现小端模式截断时不需要移动,大端模式要截断则需要额外移动。

实际表现是大端还是小端取决于硬件。更多内容:博客园 详解大端模式和小端模式

11. 枚举的大小

#include <iostream>
using namespace std;
typedef enum
{
    Char ,
    Short,
    Int,
    Double,
    Float,
}TEST_TYPE;
int main() {
    TEST_TYPE val;
    cout<< sizeof(val)<<endl;
        return 0;
}

答案为4。enum里的描述实际上是描述它可能有哪一些取值,不是有多少量在里面,无论写多少个,枚举量所占的大小都是固定不变了,在默认情况下相当于一个int(即4)。在C++11以后可以手动指定枚举的类型:

enum A: unsigned long long {
	X, Y, Z;
};

12. 位域的大小

struct {
    unsigned int a:2;
    unsigned int b:1;
} f;

此时sizeof(f)是4。'a:2'表示给a分配两个位,类似一般结构体的对齐,这里给a分配了2位,b分配了1位,剩下29位没有用到,一共32位。

一个位域相当于不是整字节数的量,相邻的位域会挤在一个字节里,若不够则则继续开,每次开的大小即出现的最大类型大小。

13. BSS段

char s1[100];
int s2 = 0;
static int s3 = 0;

void main() {
    char s4[100];
}

以下变量分配在BSS段的是(s1)。

  • BSS段:通常是指用来存放程序中未初始化的全局变量的一块内存区域;
  • 数据段:通常是指用来存放程序中 已初始化 的 全局变量 的一块内存区域,static意味着在数据段中存放变量;
  • 代码段:通常是指用来存放 程序执行代码 的一块内存区域;
  • 堆:存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减,这一块在程序运行前就已经确定了;
  • 栈又称堆栈, 存放程序的 局部变量 (不包括static声明的变量)。除此以外,在函数被调用时,栈用来传递参数和返回值。

14. 多重指针的多重const

含有const的多重指针是如何工作的?直接摘录牛客网大佬的:

int main()  
{  
    const int x = 1;  
    int* p;  
    int const** q = &p;  //① q为一个指向const int*的指针,现在将其指向non-const int*  p  
    *q = &x;             // ②现在将q指向的指针赋为常量x的地址(*q即为指针p的地址,这时p指向x)
    //**q=2;             //③报错
    *p = 2;               //④因为p是non-const int *,所以可以对其赋值,这时将常量x的值改为了2,明显不符合常识
    printf("%d",x);
}  

再来理一遍:q是指向const int *的指针,q自身没有常量属性,将他赋值为一个int *,然后再把*q(即p)赋值为x的地址,此时将通过p修改x的值,这是不合理的。

因此,C++中不允许将int**赋值给const int**(C中可以),这将导致常量被绑定到非常量指针上。若要实现预想中的功能,可以将int**赋值给const int * const *,表示“指向const int *常量的指针”,此时上面代码*q = &x将报错,因为不能通过q修改*q。

15. STL容器的分类

STL容易是可以分类的。

  • 顺序容器:vector、deque、list、forward_list,他们是线性结构,前两个是顺序表,后两个是链表。

  • 关联容器:set,map,multiset,multimap(以及unordered版本),他们不是线性结构,用平衡树或散列表实现。bitset也算特殊的关联容器

  • 容器适配器:stack、queue、priority_queue,他们基于“vector”并更改了一些功能。但是你也可以在声明中选择用其他结构替代vector。实现链表队列等功能。

  • string形式上相当于变长char数组,但是它不是模板,因此不属于容器。

16. 重载函数隐藏

#include <iostream>
using namespace std;
class A {
public :
    void run(void) {
        cout << "run()" << endl;
    }
    void run(int a){
        cout << "run(A)" << endl;
    }
};
class B : public A {
public :
    void run(int a) {
        cout << "run(B)" << endl;
    }
};
int main(void){
    B b;
    b.run(0);//语句1
    b.A::run(1); //语句2
    b.run(); //语句3
    b.A::run(); //语句4
    return 0;
}

此时哪个语句会报错?答案是只有语句3。如果不是虚函数,B中的run会覆盖掉A中的run,即使A中有多个重载的run,他们都会被覆盖,要调用A中函数只能显式调用。

17. 虚函数的默认参数

考虑下面的代码,调用哪个函数?输出什么?

class A
{
public:
    virtual void func(int val = 1)
    { std::cout<<"A->"<<val <<std::endl;}
    virtual void test()
    { func();}
};
class B : public A
{
public:
    void func(int val=0)
{std::cout<<"B->"<<val <<std::endl;}
};
int main(int argc ,char* argv[])
{
    B*p = new B;
    p->test();
return 0;
}

答案是,调用B::func,但是输出"B->1",这是因为C++中,虚函数可以动态绑定,但是默认参数还是静态的,test函数是A中的,因此调用B中func时仍然传递A中的默认参数0!

这样的语法耗子药没人喜欢,因此请避免在虚函数中使用默认参数!

18. 多继承虚指针布局

image

答案为A。对于含有虚函数的多继承来说,指针经过转化后不一定相等了,对于有包含虚指针的多继承类,他的虚指针和第一个基类对齐。因此指针转化为第一个基类后不变,转化成其他基类后变化。

image

19. 各种alloc的区别

  • malloc:申请一块给定大小的堆中的内存(不会初始化),最常用,失败返回NULL
  • calloc:申请n块给定大小的空间(但是时连在一起的,其实还是一块),并且做初始化,失败返回NULL。
  • realloc:用于malloc和calloc申请的内存,进行扩容到指定大小,有可能失败返回NULL,有可能成功,返回原地址,也有可能返回新地址,此时原地址不会释放。
  • alloc:少见。申请栈内存,不用释放。

20. 特定多态和通用多态

多态是面对对象的三个特性之一,不过多态这个词本身不限于此,只要是“一种函数功能,多种实现”的思想,都可以称之为多态。

C++中多态的形式可以表现为4种,搬运牛客网大佬的:、




//1.参数多态
//包括函数模板和类模板

//2.包含多态  virtual
class A{
	virtual void foo() {printf("A virtual void foo()");}
};
class B : public A {
	void foo() {printf("B void foo()");}
};
void test() {
	A *a = new B();
	a->foo(); // B void foo()
}

//3.重载多态
//重载多态是指函数名相同,但函数的参数个数或者类型不同的函数构成多态

void foo(int);
void foo(int, int);

//4.强制多态

//强制类型转换

重载多态和强制多态称为特定多态。
参数多态和包含多态称为通用多态。

21. 阻止直接创建对象

在C++中,为了让某个类只能通过new来创建(即如果直接创建对象,编译器将报错),应该()

答案:将析构函数声明为private。此时不允许直接在栈中创建对象,但是可以用new创建对象

22. fork()函数

for (int i = 0; i < 2; i++)
{
    fork();
    printf("-\n");
}

这个程序会输出几个-?答案是6个。fork()系统调用是Unix下以自身进程创建子进程的系统调用,调用fork后,会将本进程复制一个一模一样的子进程同时执行,子进程可以继续fork,最终一共6个。

image

23. 线性容器

image

答案是A。线性容器中,如果删除元素,it++可能已经失效,这时应该使用erase的返回值,erase会返回删除掉元素的下一个元素对于的迭代器。

不过实际中要避免这种情况,使用了线性表容器,就代表你不在中间插入或删除元素。

24. 异质链表的意义

异质链表就是每个节点类型不同的链表。在C++中,用到“基类指针指向派生类的特性”,只要让所有节点继承自同一个基类,我们就可以用基类的链表实现异质链表。链表指针都是基类形式,内容则可以是各种派生类。

25. 函数返回时构造函数的调用

image

在生成返回值的过程中,return rhs返回一个新的临时对象,该对象被返回到main处,然后执行拷贝赋值,然后临时对象被销毁,因此选D。

26. C++不是类型安全的语言

在C++中,可以把0当成bool类型的false,也可以当做int中的数字0.则表示C++不是类型安全语言。C风格字符串和std::string的转化等等都相当随意,因此认为它不是类型安全的语言

27. 避免使用虚函数的情况

构造函数不应定义为虚函数,创建派生类对象的时候,应该直接调用派生类构造函数,它会先构造基类,然后再构造自身

析构函数可以定义成为虚函数,这样通过基类指针释放对象时才能正确调用。

构造函数内部不应该使用虚函数。虚函数调用哪一个是动态绑定的,但是此时类还没有构造完成,创建一个派生类对象将调用派生类的构造函数,派生类的构造函数将先使用基类的构造函数,如果基类的构造函数中使用虚函数,将会调用未初始化的派生类。如果你非要调用虚函数,他会直接调用基类里的函数,而不是按照通常的规则。

析构函数也同理,不应调用虚函数,基类调用虚函数时,派生类已经被销毁了。如果非要调用,他会调用基类里的函数

28. volatile的特性

image

volatile会限制该变量访问时的操作顺序,保证其不被编译器优化,volatile和volatile变量之间的操作是不会被优化的。

更多:C/C++ Volatile关键词深度剖析

29. memcpy和memmove

这两个函数都是C风格的拷贝函数,memmove虽然名字为“移动”,其实仍然执行拷贝操作,不同之处是memmove保证在源区间和目标区间重叠时仍能正确处理(相应的开销更大),而memcpy不可以。

30. this指针的常量属性

指针有两种const。对于成员函数的this指针来说,他们都是常量指针(相当于Myclass *const this),因此成员函数内不允许改变this的值。

如果成员函数后面也加上了const,如void f() const{},则表明对象不可修改,相当于传入指向常量的指针(Myclass const*const this),此时this和*this(对象本身)都不允许修改

那么能不能delete this呢?const只限制修改不限制销毁,所以是可以的。此时会调用析构函数销毁对象(所以析构函数里不应这么写,否则就会无限递归)。

31. 派生类的构造顺序

image

构造Derived对象的输出顺序为CBAD。调用顺序于冒号后面的顺序无关,而是按照下面的顺序:基类>自身的class对象>自身的内置对象。对象按声明顺序,由于B声明在前,所以先是B。

posted @ 2022-02-10 22:58  Ofnoname  阅读(791)  评论(0编辑  收藏  举报