Bootstrap

C++ K2 (3)

提示:文章

前言

前期疑问:
本文目标:


一、背景

接上文

二、题目

命名空间

10、(单选)【作用域】对于名字空间(namespace)以下说法错误的是[x(1] [g2]

A. 使用声明(the using-declaration)可用于类的声明中

B. 使用指示(the using-directive)具有传递性

C. 如果某函数访问其自身所在名字空间中成员元素时,可以直接访问

D. 通过使用指示(the using-directive)引入存在有相同成员元素的多个名字空间,在实际存取这些存在冲突的成员元素之前,也会导致当前作用域中元素冲突的错误发生

只有在使用时才会报错

使用声明(the using-declaration)允许从基类中引入成员到派生类的作用域中

11、(单选)【作用域】关于名字空间说法正确的是?B

A. 定义可以不连续,跨越多个文件;

B. 可以嵌套定义;

C. 可以有很多别名,别名和原来的名字可以交替使用;

D. 名字空间必须有名字;

12、(单选)【作用域】命名空间求输出(B)

int a = 1;
namespace Outer {
	int a = 2;
}

namespace Outer {
    namespace Inner {
        int a = 3;
    }
}

using namespace Outer;

int main()
{
    int a = 4;
    cout << a;
    cout << ::a;
    cout << Outer::a;
    cout << Outer::Inner::a;
    return 0;
}

A、4231

B、4123

C、4321

D、4132

::是全局作用域

13、(单选)【作用域】下面代码运行结果是(A)

int count = 10;

namespace test {
	int count = 1;
    int getCount()
    {
        ::count++;
        return count;
    }
}

int main()
{
    int count = 2;
    count = test::getCount();
    cout<<count<<" "<<::count<<endl;
    return 0;
}

A. 1 11

B. 1 10

C. 2 11

D. 2 2

::是全局作用域

getCount返回的是前面的count =1

这个题目好花啊,没做对

14、(单选)【作用域】以下为3个代码文件中的代码片段(D)

// service.h
Struct Service {
	static const std::string NAME;
};

// service.cpp
#include “service.h”
const std::string Service::NAME=“SERVICE1”;

// main.cpp
#include “service.h”
#include <iostream>
const std::string MSG=“service” + Service::NAME + “ready”;

int main()
{
	std::cout<<MSG<<std::endl;
}

对于程序运行结果,以下说法正确的是

A. 运行输出 service SERVICE1 ready

B. 运行输出 service

C. 运行输出中包含随机字符或乱码

D. 运行结果取决于编译链接过程及std::string的内部实现

static一个文件一份,在当前文件中未初始化就使用,在其他文件中初始化过,未定义行为。

static一个文件一份,在当前文件中未初始化就使用,在其他文件中初始化过,未定义行为。

15、(单选)【作用域】以下程序输出为(D)

#include <iostream>

int i = 3;

namespace A {
	int i = 1;
}

namespace B {
    int i = 2;
    void F1(void);
    void F2(void);
    void F3(void);
    void F4(void);
}

void B::F1(void)
{
    using namespace A;
    std::cout<<i<<std::endl;
}

void B::F2(void)
{
    using A::i;
    std::cout<<i<<std::endl;
}

void B::F3(void)
{
	std::cout<<A::i<<std::endl;
}

void B::F4(void)
{
	std::cout<<i<<std::endl;
}

int main()
{
    using namespace B;
    F1();
    F2();
    F3();
    F4();
    return 0;
}

A. 1 1 1 3

B. 2 3 1 2

C. 1 1 1 2

D. 2 1 1 2

解释

using namespace A是把A中的所有变量提升到其最高的带using关键词的空间下;而用usingA::i,是直接使用,会覆盖当前的i

F1函数中,A提不到B的namespace,可以理解为提升到了一个隐藏的全局namespcae,因此此处还是用B的i

F2函数中,覆盖当前的i,因此是A的i

F3函数中,直接用A的i

F4函数中,默认用B的i

故是2112

16、(单选)【作用域】下述程序将输出什么(A)

#include <iostream>

int count = 10;

class Data {
    public: void printCount() {
        do {
            int count = 30;
            std::cout << count << std::endl;
        } while (0);
        std::cout << count << std::endl;
	}
	private: int count = 40;
}

int main() {
    Data data;
    data.PrintCount();
    std::cout << count << std::endl;
    return 0;
}

A. 30 40 10

B. 30 40 40

C. 30 30 10

D. 40 40 10

限定符

17、(单选)阅读以下程序片段,成员函数setName的访问限定是什么(A)

struct Man
{
    void setName(const std::string& strName);
    void setAge(unsigned int iAge);
    std::string getName() const;
    unsigned int getAge() const;
private:
    unsigned int m_iAge;
    std::string m_strName;
}

A. public

B. private

C. protected

D. friend

struct默认为public,class默认为private

18、(单选)以下关于结构体初始化,效率最低的是()

struct node {
	int a , b ,c ;
};

A node s = { 1 ,2 ,3};

B memset_s( s, 0 , xx)

C node s;

解释:

s.a = 1;

s.b = 2;

s.c = 3;

这个题考点是memset_s 初始化效率最低

指令占用数量上,复合字面量 = {0} = memset <= memset_sp <= memset_s

18、(单选)下面代码Test函数执行的输出是(A)

// a.h
static int g_count;
void AddCount(int count);

// a.c
#include “a.h”
void AddCount(int count)
{
	g_count += count;
}

// b.c
#include “a.h”
void IncrCount(void)
{
	g_count += 1;
}

void Test(void)
{
    print(“Count init = %d,, g_count);
    IncrCount();
    print(“incr = %d,, g_count);
    AddCount(3);
    print(“added = %d”, g_count);
}

A. Count init=0,incr=1,added=1

B. Count init=0,incr=1,added=4

C. 编译报错

D. 不确定与编译器实现有关

解释:静态变量在不同编译单元(.cpp文件)中是相互独立、各自拥有一份的

这个题目的答案是A,我还在Clion上验证一下,但是我并没有完全按照上述代码设计工程,问题在于我把Test函数单独放在main.c文件中了。会报错找不到 IncrCount()函数。我还以为是题目错了。后面才发现test函数是要和IncrCount()函数实现放在一个文件中的。也就是放在b.c文件中。并把test函数改成main函数,执行答案就是A选项。

至于为什么是A选项,因为程序是在b.c文件中执行的。所以使用的g_count都是b.c编译单元的一个单独的副本。所以g_count一开始0,后面自增后为1。经过AddCount函数后a.c编译单元的g_count变成了3,但是b.c中的还是1。

另外一个问题就是静态变量没有初始化是默认初始化为0吗?内置类型(如 int, float 等)在全局或静态存储中会被初始化为零,在局部作用域中则未定义。

异常(这边的题都没看)

19、(单选)【异常】不宜抛出异常的是(ABCD)

A、移动构造

B、拷贝构造

C、析构函数

D、移动赋值

20、(单选)【异常】在哪些函数或操作符不适合抛出异常(ABCD)

A、 全局变量和线程局部变量的构造函数

B、 delete操作符重载函数

C、 只含一个参数的new操作符重载函数

D、 用于交换两个自定义类型实例的swap函数

解析 P.17-CPP:

这些函数会被隐式地调用,或被标准库调用,调用时不期望异常抛出。在这些地方抛出异常将无法处理,或破坏标准库的某些性质。

一些标准库的类或算法会依赖“移动构造函数、移动赋值操作符、swap 函数不会抛异常”这些性质才能保证正常工作。析构函数常常会在异常处理的过程中被隐式调用,以清理资源。operator delete 函数则常常会被析构函数调用。在已经有异常抛出的情况下抛异常,会导致std::terminate 。

21、(单选)【异常】以下不属于c++异常处理的:(C)

A. try

B. catch

C. finally

D. throw

C++没有finally,只有final关键字修饰虚函数

22、(单选)【异常】下列关于异常使用的说法错误的是:(D)

A. “catch(…)” 语句表示捕获所有异常。

B. 异常会带来效率的一部分损失。

C. 如果函数的声明没有异常规格声明,表示该函数可以抛出任何类型的异常。

D. 可以在类的构造函数和析构函数中抛出异常,以解决构造函数和析构函数失败时不能返回值的问题。

本题考察 C++ 异常的基本知识。 虽然可以在构造函数中抛出异常,但应禁止在析构函数中抛出异常

23、(多选)【异常】关于noexcept描述正确的有(BD)

A. 声明带有noexcept(false)的函数不会抛出异常

B. 声明带有noexcept的函数可以告诉调用者,该函数不会抛出异常

C. 父类虚函数中有noexcept声明,子类重写该虚函数时,可不再重复声明

D. 如果一个函数不应抛出异常,应使用noexcept关键词声明

A true才是不抛异常

C中子类重写父类noexcept函数的时候还是需要加上noexcept标志,并且必须和父类的true/false保持一致,否则会报错https://zh.cppreference.com/w/cpp/language/noexcept_spec

**规则1:**如果一个虚函数承诺它一定不会抛出异常,则后续派生出来的虚函数也必须做出相同的承诺

**规则2:**反之,如果基类的虚函数允许抛出异常,则派生类的对应函数允许抛出异常,也可以不允许抛出异常

24、(多选)【异常】程序中异常处理,说法错误的是(ACD)

A. 可以用异常来控制流程走向,异常的优先级高

B. 不要过多的使用异常,在捕获异常处理时,尽量捕获最小类

C. 根据程序情况决定使用异常还是使用错误码,或者二者混合使用

D. 当需要扔出error异常时,应自定义一个Error子类供使用

A 控制流程走向的是if和for

C 不可以混用

D error要尽可能通用

25、(多选)【异常】哪些函数必须显式声明为noexcept(AB)

A. 移动构造函数

B. swap函数

C. …

D. …

应该noexecpt的函数:

移动构造

移动赋值

析构(隐式noexecpt

26、(多选)【异常】cast类型转换以下说法正确的是:(BD)

A.dynamic_cast只在编译时校验

B.static_cast只在编译时校验

C.dynamic_cast不会抛出异常

D.static_cast不会抛出异常

可以记只选static的

A dynamic_cast 程序运行时期,主要用来父类和子类直接的转换

dynamic_cast比static_cast安全。如果转换指针,失败后返回空指针,如果转换引用,失败后抛出bad_cast异常

27、(单选)【字符串】对于以下代码,当用户输入gamma并按回车时,输出的字符是(B)

#include <iostream>

using namespace std;

void Rev()
{
    char c = cin.get();
    if (c != '\n') {
    	Rev();
	}
	cout << c;
}

int main()
{
    Rev();
    return 0;
}

A. gamma

B. ammag

C. g

D. a

28、(单选)【字符串】gamma为键盘输入加换行,问输出?(D)

void rec(void)
{
    char c = cin.get();
    while(c != '\n') {
        rec();
        cout<<c;                                                   
    }
	return;
}

int main()
{
    rec();
    return 0;
}

A 输出 gamma

B 输出 ammag

C 输出 g

D 输出 a

注意函数里是while,单步调可以发现输出a后就卡住一直在等待再输入状态。如果这里改为 if (c != ‘\n’) 输出的就会是 ammag。

29、(单选)【字符串】对于以下代码,当用户输入gamma并按回车时,输出的字符是(B)

#include <iostream>

using namespace std;

void Rev()
{
    char c = cin.get();
    if (c != '\n') {
    	Rev();
    }
    cout << c;
}

int main()
{
    Rev();
    return 0;
}

A. gamma

B. ammag

C. g

D. a

30、(单选)【字符串】gamma为键盘输入加换行,问输出?(D)

void rec(void)
{
	char c = cin.get();
    while(c != '\n') {
        rec();
        cout<<c;                                                   
    }
	return;
}

int main()
{
    rec();
    return 0;
}

A 输出 gamma

B 输出 ammag

C 输出 g

D 输出 a

注意函数里是while,单步调可以发现输出a后就卡住一直在等待再输入状态。如果这里改为 if (c != ‘\n’) 输出的就会是 ammag

31、(单选)【字符串】以下代码的输出是什么?(C)

#include <iostream>
#include <string>
void Alpha(string beta) {
    cout << beta << endl;
}

int main()
{
	Alpha("gamma");
	return 0;
}

A.编译错误

B.g

C.gamma

D.无输出

32、(单选)【字符串】运行结果(C)

string getString();

void main() {
    string str = "gamama";
    cout << getString(str) << endl;
}

string getString(string &str) {
    return str;
}

A. gamama

B. g

C. 编译错误

D. 运行错误

GetString(String &str)定义在后面,main函数找不到匹配的函数

33、(单选)【字符串】以下输出?(C)

cout << “Alpha\0and\0Gamma”;

A. Alpha

B. Alpha\0and\0Gamma

C. Gamma

D. 无输出

cout 遇到/0就会停止输出

34、(单选)【字符串】以下输出结果为 (A)

string s1 =1”
string s2 =2”
s1 = s2 + s1;
cout << s1

A. 21

B. 12

C. 1

D. 2

35、(单选)using String = char[255];

String s;

则s是(B)

A.字符变量

B.字符数组变量

C.字符指针数组变量

D.字符指针变量

36、typedef char STRING [255];STRING*s;,则s是( C )

A.字符数组

B. 字符变量

C.字符指针数组

D.字符指针变量

typedef unsigned long ulong;

// 为 unsigned long 类型创建别名 ulong

typedef int arrayType[10];

// 为包含10个整数的数组创建别名 arrayType

typedef void (*funcPtr)();

// 为指向函数的指针创建别名 funcPtr

37、(单选)阅读下面代码,输入“abcdef” 以下代码的输出是什么(A)

void main(){
    char a[20];
    std::cin.get(a,5,"d");
    cout << a << endl;
}

A.abc

B.abcd

C.abcde

D.abcdef

cin.get(数组名,长度,结束符)。其中结束符为可选参数,读入的字符个数最多为(长度**-1)**个,结束符规定结束字符串读取的字符,默认为ENTER。虽然要读取5个长度,但先遇到结束字符d所以结束。如果结束符是’f’结果是abcd

38、(单选)X64 Linux(小端顺序)下(C)

int main()
{
    unsigned int value = 0x10000;
    unsigned short *p = (unsigned short *)&value;
    *p += 10;
    printf("0x%x", value);
    return 0;
}

输出为(C)

A.0x10

B.0x10010

C.0x1000a

D.0xa

short类型 *p = 0x000a = 0xa

printf(“0x%x”,*p); 是0xa

printf(“0x%x”, value); 是0x1000a

39、(单选)自增相关,运行结果是?(9 6)

int Muti(int a, int b) {
	return a*b;
}

int main()
{
    int num = 2;
    cout<<Muti(++num,num)<<endl;
    num = 2;
    cout<<Muti(num++,num)<<endl;
    return 0;
}

输出是9 6

因为++num先计算,再传参

num++先传参再计算

这个题目感觉也要像下面的题目一样,得看汇编代码才能看懂。

40、(单选)以下代码输出(5,6,8,5,6)

不同编译器结果不一样

cl.exe编译运行:5,6,7,4,5

g++.exe编译运行5,6,8,5,6

#include <iostream>

using namespace std;
int AddNums(int a,int b){
    cout << a <<" "<< b << endl;
    return a + b;
}
int main(){
    int num = 2;
    cout << AddNums(num, num++) << endl;     //输出a = 3 b = 2 输出5
    num = 2;
    cout << AddNums(++num,num++) <<endl;    //输出a = 4;b = 2 输出6
    num = 2;
    cout << AddNums(++num,++num)<<endl;     //输出a = 4;b = 4 输出8
    num = 2;
    cout << AddNums(num++,num)<<endl;       //输出a = 2;b = 3; 输出5
    num = 2;
    cout<<AddNums(++num,num)<<endl;        //输出a = 3;b = 3; 输出6
}

原题找不到,但这是个很好例子

知识点:传参从右往左传参(入栈)

函数在有运算时需要区分先调用还是先计算(++i i++)

https://blog.csdn.net/lwwl12/article/details/108521816

这个题目我也尝试使用Clion进行反汇编查看。

查看下述两行代码

num = 2;
cout << AddNums(++num,num++) <<endl;    //输出a = 4;b = 2 输出6

汇编代码如下

movl   $0x2,-0x4(%rbp)          
mov    -0x4(%rbp),%eax          
lea    0x1(%rax),%edx           
mov    %edx,-0x4(%rbp)          
addl   $0x1,-0x4(%rbp)          
mov    -0x4(%rbp),%ecx          
mov    %eax,%edx                
call   0x7ff7e47d2298 <AddNums(int, int)>

看不懂上述汇编代码,使用copilot解读

让我们逐行分析这段汇编代码:

movl $0x2, -0x4(%rbp):将立即数 0x2(即 2)存储到相对于基指针 rbp 的偏移量 -0x4 处的内存位置。

mov -0x4(%rbp), %eax:将相对于基指针 rbp 的偏移量 -0x4 处的内存位置的值加载到寄存器 eax 中。此时,eax 的值为 2。

lea 0x1(%rax), %edx:计算 rax 寄存器的值加 1 的地址,并将结果存储到寄存器 edx 中。此时,edx 的值为 3(因为 eax 的值为 2)。

mov %edx, -0x4(%rbp):将寄存器 edx 的值存储到相对于基指针 rbp 的偏移量 -0x4 处的内存位置。此时,内存位置 -0x4(%rbp) 的值为 3。

addl $0x1, -0x4(%rbp):将立即数 0x1(即 1)加到相对于基指针 rbp 的偏移量 -0x4 处的内存位置的值。此时,内存位置 -0x4(%rbp) 的值为 4。

mov -0x4(%rbp), %ecx:将相对于基指针 rbp 的偏移量 -0x4 处的内存位置的值加载到寄存器 ecx 中。此时,ecx 的值为 4。

mov %eax, %edx:将寄存器 eax 的值复制到寄存器 edx 中。此时,edx 的值为 2。

call 0x7ff7e47d2298 <AddNums(int, int)>:调用函数 AddNums,传递两个参数:edx(值为 2)和 ecx(值为 4)。

总结一下,这段代码的主要作用是对内存位置 -0x4(%rbp) 进行一系列操作,最终调用 AddNums 函数,传递的参数为 24

解释的还不错,但是eax,ebx,ecx是什么意思,还是得看下。

其中不好理解的就是这一行:lea 0x1(%rax), %edx:计算 rax 寄存器的值加 1 的地址,并将结果存储到寄存器 edx 中。此时,edx 的值为 3(因为 eax 的值为 2)。【疑问:为什么edx 的值为 3(因为 eax 的值为 2】

%RAX%EAX 是 x86-64 架构中的两个寄存器,它们之间有着密切的关系:

  • %RAX 是 64 位寄存器,用于存储和操作 64 位数据。
  • %EAX%RAX 的低 32 位部分,用于存储和操作 32 位数据。

当你对 %EAX 进行操作时,实际上是对 %RAX 的低 32 位进行操作。例如,如果你将一个值存储到 %EAX 中,%RAX 的高 32 位将被清零,而低 32 位将存储你指定的值。

上述表述解决了上述的疑问。还有一个问题,就是copilot所述中call 0x7ff7e47d2298 <AddNums(int, int)>:调用函数 AddNums,传递两个参数:edx(值为 2)和 ecx(值为 4)。为啥知道edx和ecx传入了函数。

这个问题暂时搁置待解决。

题目中有个代码是这样的:

num = 2;
cout << AddNums(++num,++num)<<endl; //输出a = 4;b = 4 输出8

这个看汇编代码就很好理解了。

$0x2,-0x4(%rbp)          
addl   $0x1,-0x4(%rbp)          
addl   $0x1,-0x4(%rbp)          
mov    -0x4(%rbp),%edx          
mov    -0x4(%rbp),%eax          
mov    %eax,%ecx                
call   0x7ff7e47d2298 <AddNums(int, int)>

这种代码还是看汇编更好理解。


总结

未完待续

;