C++ K2 (6)
提示:文章
前言
前期疑问:
本文目标:
一、背景
最近
二、
21、(多选)【auto】假设函数Foo返回类型为const A&,下列语句与 const A& r = Foo(); 等价的有()
A. auto r = Foo();
B. auto& r = Foo();
C. const auto& r = Foo();
D. decltype(Foo()) r = Foo();
Auto不能变成引用类型。
当有&的时候,const会默认复制过来
Decltype返回传入值的类型
Auto不能变成引用类型。
当有&的时候,const会默认复制过来
Decltype返回传入值的类型
自我解析:这个题目主要考的是Auto不能变成引用类型。
22、(多选)【auto】下面auto推导结果正确的有(ABC)
A.
auto a = 1; // 推导出变量 a 的类型是 int
B.
int a = 1;
auto b = a; // 推导出变量 b 的类型是 int
C.
int a =1;
const auto &c = a; //推导出变量 c 的类型是 const int&
D.
const int a =1;
auto d = a; //推导出变量 d 的类型是const int
auto小记
auto 仅仅是一个占位符,在编译器期间它会被真正的类型所替代。或者说,C++ 中的变量必须是有明确类型的,只是这个类型是由编译器自己推导出来的。
当=右边的表达式是一个引用类型时,auto 会把引用抛弃,直接推导出它的原始类型。
当类型不为引用时,auto 的推导结果将不保留表达式的 const 属性;
当类型为引用时,auto 的推导结果将保留表达式的 const 属性。
编译
23、(单选)【编译】如下代码中,在编译阶段(非链接阶段)会发生错误的是【C】
A、
constexpr int GetValue();
int value = GetValue();
B、
constexpr int GetValue();
const int value = GetValue();
C、
constexpr int GetValue();
constexpr int value = GetValue();
D、
constexpr int GetValue();
constexpr int GetValue() { return 0; };
constexpr int value = GetValue();
Constexpr 需要在编译阶段确定
这个题目在代码中可以得到验证。
C选项报错:Constexpr variable ‘value3’ must be initialized by a constant expression。
24、【编译】以下代码所定义的 Func 函数位于头文件且被多个.cpp文件所包含时,哪个会报编译错误():
A.
class Myclass {
virtual void Func()
{ ... }
};
B.
constexpr void Func()
{ ... }
C.
class Myclass {
void Func()
{ ... }
};
D.
class Myclass {
static void Func();
};
void Myclass::Func()
{ ... }
解释:重定义错误
这个题目感觉莫名其妙。
25、(多选)【编译】下面调用Foo的代码,能够编译成功的有()
constexpr int Foo(int i)
{
return i + 5;
}
int main()
{
// 下面选项的代码会插入到这个位置
return 0;
}
A.
std::array<int, Foo(5)>arr;
B.
int i =10;
std::array<int, Foo(i)> arr;
C.
int i = 10;
Foo(i);
D.
constexpr int i = 5;
Foo(i);
array要求编译期常值才能编译通过,这也是监测你写的代码是否真的产生编译期常值的方法。B选项中,只需将int i改为const int i或者constexpr int i即可通过编译
const和constexpr的区别:前者可以在运行时推出,后者要求在编译阶段就要能够推出。
26、(多选)【编译】下面哪些在编译期报错(非链接期)()
A.
constexpr int GetVal();
constexpr int GetVal() { return 0; }
constexpr int i = GetVal();
B
constexpr int GetVal();
constexpr int i = GetVal();
C
constexpr int GetVal();
int i = GetVal();
D
constexpr int GetVal();
constexpr int i = GetVal();
constexpr int GetVal() { return 0; }
A 选项不会报错,GetVal() 返回的常量表达式,使用它初始化 i 没有问题。
B 选项在编译期报错,是因为 i 是一个 constexpr 修饰的变量,意味着它的值必须是编译期已知的,所以它会在编译期间对它的初始化表达式(即 GetVal())进行求值,但这个函数只有声明,没有定义,所以编译期间就报错了。
C 选项和 B 选项最大的区别就在于 i 没有使用 constexpr 修饰,是因为 i 不需要编译期已知,编译时不会去对 GetVal 求值,尽管 GetVal 没有定义,但编译器假设它可能定义在别的编译单元,留给链接器去决策。所以编译期不会报错,但会在链接期报错(undefined reference to ‘GetVal()’)。
D 选项也会编译期报错,原因和 B 选项是一样的。尽管在这里 GetVal 有了定义,但定义点在使用点之后,所以仍然会报错。
27、(多选)【编译】以下哪些选项中的代码独立填到横线处,可以使程序正常编译运行且不产生未定义行为()
#include <iostream>
#include <memory>
class Foo {
public:
void Show() { std::cout << data; }
private:
int data{1};
};
int main()
{
std::unique_ptr<Foo> tom = std::make_unique<Foo>();
std::unique_ptr<Foo> jerry = std::move(tom);
______________ // 选项中的代码填入此处
return 0;
}
A: tom->Show();
B: std::unique_ptr mary = std::move(tom);
mary->Show();
C: jerry->Show();
D: tom = std::move(jerry);
选项 A 和 B 会导致未定义行为,因为在 std::move(tom) 之后,tom 不再拥有 Foo 对象的所有权,访问 tom会导致未定义行为。
28、(多选)【断言】以下代码划线处填入哪个能编译成功()
#include<assert.h>
#include<stdbool.h>
#define AAA 0
#define BBB AAA||true
const int g_ttt = 2;
int Func()
{
const short t = 3;
____;
return 0;
}
A. static_assert(t > 0, “error”)
B. static_assert(g_ttt > AAA, “error”)
C. static_assert(sizeof(BBB), “error”)
D. static_assert(2&&3, “error”)
静态断言
static_assert声明是C++11标准引入的特性,用于在程序编译阶段评估常量表达式并对返回false 的表达式断言,我们称为静态断言。它具有如下特性:
所有处理都必须在编译期间执行,不允许有空间和时间上的运行成本;
必须具有简单的语法;
断言失败能够显示丰富的错误诊断信息;
可以在命名空间、类和代码块内部使用;
失败的断言会在编译阶段报错
Assert检验前面的是否成功,不成功则输出后面的。
C语言是选CD,C++和C的const有区别,static_assert是一样的
static_assert(t < 0, "error"); //这样代码会提示错误,感觉是静态检查一样
29、(多选)关于inline函数的使用,以下哪些选项的描述是正确的()
A. 在一个.c文件中定义的inline函数可以在另一个.c文件中使用,且如果小于10行,编译器会把该inline函数展开,发挥其性能优势
B. inline函数应该简洁,如果语句较多,不适合于定义为内联函数
C. inline函数体中,一般不建议有循环、if或switch语句,否则,函数定义时即使有inline关键字,编译器也可能会把该函数作为非内联函数处理
D. inline函数要在函数被调用之前声明
30、(多选)内联函数兼具函数和宏的优点(ABCD)
A. 内联函数/函数执行严格的类型检查
B. 内联函数/函数的参数求值只会进行一次
C. 内联函数就地展开,没有函数调用的开销
D. 内联函数比函数优化得更好
31、(多选)以下关于内联的说法,正确的是()
A. 含有inline的函数一定会被内联展开
B. 未包含inline的函数也可能被内联展开
C. 在内联函数中,所有函数定义中的局部静态对象在所有翻译单元间共享
D. 函数的内联替换会避免函数调用的开销(传递实参及返回结果),但它可能导致更大的可执行文件,因为函数体必须被复制多次
解释:此题C选项需要注意,在内联函数中使用static,其实是类似全局变量的,即所有内联函数会公用这个static对象,所以不要在内联函数里面使用static对象
内联函数在什么时候展开 由编译器自己决定
32、(多选)关于变量,正确的说法是()
A. 若全局变量仅在单个C文件中访问,则可以将这个变量修改为静态全局变量,以降低模块间的耦合度
B. 函数内部定义的自动变量过大,可能会导致堆栈溢出
C. 若全局变量仅由单个函数访问,则可以将这个变量改为该函数的静态局部变量,以降低模块间的耦合度
D. 设计和使用访问动态全局变量、静态全局变量、静态局部变量的函数时需要考虑重入问题
自我解析:增加对这个题目的理解
对于A选项,提及若只在单个c文件中使用,可以将这个变量修改为静态全局变量。我的问题是,静态全局变量只能在当前这个编译单元中访问吗?
查一下百度,“静态全局变量只能当前编译单元使用吗?”,
是的,静态全局变量只能在当前编译单元中使用。在C语言中,静态全局变量(也称为全局静态变量)的作用域被限制在其定义的编译单元内。这意味着,如果在某个源文件中定义了一个静态全局变量,那么这个变量只能在定义它的源文件中被访问和使用。其他源文件无法通过
extern
关键字来引用这个变量,因为静态全局变量的作用域仅限于定义它的源文件。具体来说,静态全局变量的定义方式是在变量前加上
static
关键字。这样,该变量就只能在定义它的源文件中被访问和使用,而不会影响到其他源文件中的同名变量。这种特性使得静态全局变量在模块化编程中非常有用,可以避免不同模块之间的变量冲突。
然后看到一篇文章说在C语言中没有全局变量这一说,只有外部变量和自动变量。
对于第一点,c语言中没有全局变量这一说,查了下好像没有得到太确切的文章。
对于第二点,外部变量
- 外部变量:
- 外部变量是指在一个文件中定义,而在另一个文件中声明的全局变量。
- 通过使用extern关键字,可以在一个文件中引用另一个文件中定义的全局变量。
- 外部变量的作用域也是整个程序,但它特别用于在不同文件之间共享全局变量。
- 外部变量实质上仍然是全局变量,只是其定义和声明可能分布在不同的源文件中。
此外,值得注意的是,虽然外部变量和全局变量在概念上有所关联,但过多地使用全局变量(包括外部变量)可能会降低程序的模块化和独立性,因此应谨慎使用。
以及什么是自动变量,
在C语言中,自动变量是指在函数内部声明的局部变量。
33、(多选)函数中使用全局变量将引入哪些问题()
A. 函数对全局变量产生依赖,存在耦合
B. 当多个函数读写全局变量时,某一时刻其取值可能不确定,函数处理逻辑可能更复杂
C. 并发编程环境中,此函数可重入性会被破坏
D. 函数性能会大大降低
34、(多选)在32位系统中,有如下代码
char str[] = “\\\\”
char*p = str;
int n = 1000;
以下说法正确的是()
A. sizeof(str)为3
B. sizeof§为4
C. sizeof(n)为4
D. sizeof(str)为4
E. sizeof(str)为5
str实际上是一个存放两个\的字符串,注意\转义为\。因此在32位下,str的大小为3(还有末尾的\0),指针大小是4,int大小是4
35、(单选)关于const的用法,错误的是()
A. 指针所指向的内容不会被更改,应该声明为指向const的指针
B. const只能用于修饰简单类型,不能用于修饰自定义类型
C. const类型的成员变量、引用类型的成员变量,必须在构造函数初始化列表中进行初始化
D. const对象的成员是不可修改的,然而const对象通过指针维护的对象却是可以修改的
自我解析,关于题目中的选项的意义我有点不太懂
对于选项A,按照A选项描述的,我的理解应该是int *const ptr;但是实际上不是的。应该是const int* ptr;ptr指向cont int;
至于int* const ptr是指向int类型的变量。
关于D选项,什么叫const对象通过指针维护的对象是可以修改的。
基于copilot的解释,结合自己的理解。可能就是如果这个const对象中有个指针,可以更改这个指针指向的内容。
下面的是copilot的解释:
这句话的意思是,当一个对象被声明为 const
时,该对象的成员变量是不可修改的。然而,如果这个 const
对象包含一个指向其他对象的指针,那么通过这个指针指向的对象是可以被修改的。
举个例子:
cpp
class MyClass {
public:
int value;
int* ptr;
};
int main() {
int x = 10;
int y = 20;
const MyClass obj = {5, &x};
// obj.value = 6; // 错误:不能修改 const 对象的成员变量
*obj.ptr = y; // 合法:可以通过指针修改指向的对象
return 0;
}
在这个例子中,obj 是一个 const 对象,因此我们不能修改 obj.value。但是,obj.ptr 是一个指向 int 的指针,通过这个指针我们可以修改它指向的对象 x 的值。
36、(多选)对于常量描述正确的是()
A.对于不变量,函数形参和成员函数使用const来保证数据为不变量
B.函数内的const局部变量必须在声明时初始化
C.在多个.cpp文件中定义的同名全局const变量将导致重复定义错误
D.在多个.h文件中定义的同名全局const变量将导致重复定义错误
C:应该是一个 cpp 中一份
其中C选项我在Clion上验证了一下,确实多个编译单元中可以有相同的const定义。
37、(多选)关于循环的说法正确的是 ()
A. 使用range_base for循环时可以在循环体中增删元素
B. 使用range_base for循环时可以简化对元素相同代码的处理
C. range_base for可以用于STL容器
D. for循环中的表达式都可以省略,但是两个分号不能省略
A会导致迭代器失效?
这边涉及了一个知识点,就是range_base for循环是什么意思,查了copilot,解释如下:
range-based for 循环是 C++11 引入的一种简化遍历容器元素的语法。它使得代码更加简洁和易读,特别是在处理标准模板库(STL)容器时。
以下是一个 range-based for循环的示例:
#include <iostream>
#include <vector>
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};
// 使用 range-based for 循环遍历容器元素
for (int value : vec) {
std::cout << value << " ";
}
return 0;
}
在这个示例中,range-based for 循环遍历 vec容器中的每个元素,并将其值输出到控制台。与传统的 for 循环相比,range-based for 循环更加简洁,不需要显式地使用迭代器或索引。
面向对象知识(操作符重载,多态,成员声明使用,特殊成员函数,继承等)
覆盖的条件:
1. 基类函数带virtual关键字;
2. 派生类函数有相同的函数名、参数、返回值类型
以上两个条件全部满足才能覆盖,否则只能算隐藏(hide)
产生覆盖后,按具体的指针内容所属类型调用方法;一般都是调用派生类的方法
如果没产生覆盖,则按具体类对象的类型调用方法;父调父,子调子
l 基类和其派生类共享该基类的静态成员变量内存
l 父类声明,子类实现时,优先从父类找,父类为虚函数则找子类函数(无论是否为虚),父类不为虚函数则取父类函数。
l 如果父类里声明了某函数为虚函数,则在子类此函数的声明里不管有没有"vitrual"关键子,都是虚函数。即使访问权限发生变化。
1、(单选)类cAAA的一个对象cAAAobj,那么sizeof(cAAAobj) 的值为
A. 1
B. 4
C. 8
D. 16
虚表4(就是一个指针) int4,静态变量不占用
这个题目有问题,应该本身题目有段代码,但是这边没有显示。这个题目实际是之前我在牛客上经常做的题目。
下面是我自己写的验证的代码
class Foo {
public:
void Show() { std::cout << data; }
private:
int data{1};
int data2{1};
};
class Foo2 {
public:
void Show() { std::cout << data; }
private:
int data{1};
int data2{1};
};
int main66()
{
Foo foo;
printf("%d\n",sizeof(foo));
printf("%d\n",sizeof(Foo));
Foo2 foo2;
printf("%d\n",sizeof(foo2));
printf("%d\n",sizeof(Foo2));
return 0;
}
//打印信息
4
4
8
8
由此可见,类对象的内存大小取决了类的成员情况。
2、(单选)下列关于纯虚函数的描述中,正确的是()
A. 纯虚函数是一种特殊的虚函数,它是个空函数
B. 具有纯虚函数的类称为虚基类
C. 一个基类中说明有纯虚函数,其派生类一定要实现该纯虚函数
D. 具有纯虚函数的类不能创建类对象
A 纯虚函数还不是函数
B 是抽象类
C 可以不实现,实例化才实现
http://c.biancheng.net/view/2299.html
纯虚函数没有函数体,只有函数声明,在虚函数声明的结尾加上=0,表明此函数为纯虚函数包含纯虚函数的类称为抽象类(Abstract Class)。之所以说它抽象,是因为它无法实例化,也就是无法创建对象。原因很明显,纯虚函数没有函数体,不是完整的函数,无法调用,也无法为其分配内存空间。
C:派生类可以不实现该纯虚函数,但是该派生类如果要被实例化,就必须实现该纯虚函数了
3、(单选)要将类A说明是类B的虚基类,正确的描述是 ( )
A.class virtual B:public A
B.class B:virtual public A
C.virtual class B:public A
D.class B:public A virtual
虚基类是什么?
查了下资料如下:
[虚基类](Virtual Base Class)在C++中用于解决多重继承中的菱形继承问题。菱形继承是指一个类通过两条不同的路径继承同一个基类,导致基类成员在派生类中出现多份拷贝,从而产生冗余和歧义。虚基类通过共享基类成员,避免了这种冗余和歧义。12
** 定义和用途**
虚基类是通过
virtual
关键字继承的基类。其用途在于解决多重继承中的菱形继承问题,确保基类成员在派生类中只有一份拷贝,从而避免冗余和歧义。示例代码
以下是一个使用虚基类的示例代码:
cppCopy Codeclass Base { public: int x; Base(int val) : x(val) {} }; class Derived1 : virtual public Base { public: Derived1(int val) : Base(val) {} }; class Derived2 : virtual public Base { public: Derived2(int val) : Base(val) {} }; class FinalDerived : public Derived1, public Derived2 { public: FinalDerived(int val) : Base(val), Derived1(val), Derived2(val) {} };
在这个例子中,
FinalDerived
类通过两条路径继承自Base
类。由于Base
被声明为虚基类,FinalDerived
中只包含一个Base
类的实例,避免了冗余。
4、(单选)输出结果为:()
#include < iostream.h>
class A
{
public:
virtual void f() { cout<<"A::f()\n"; }
};
class B:public A
{
private:
char *buf;
public:
B(int i)
{
buf=new char[i];
}
void f()
{
cout<<"B::f()\n";
}
~B() { delete []buf; }
};
void main()
{
A *a = new A;
a->f();
delete a;
a = new B(15);
a->f();
}
A: A::f() A::f()
B: A::f() B::f()
C: B::f() B::f()
D: 程序会抛出异常
B 虚函数、动态联编。
5、(单选)已下哪个选项中的代码段在输出section end 之前会调用Base的析构函数?()
A.auto sp = make_shared<Base>();
{
sp.reset(new Base());
}
cout << "section end" << endl;
B.unique_ptr<Base> up1;
{
auto up2 = make_unique<Base>();
swap(up1, up2);
}
cout << "section end" << endl;
C.shared_ptr<Base> up1;
{
auto up2 = make_shared<Base>();
swap(up1, up2);
}
cout << "section end" << endl;
D.shared_ptr<Base> sp;
{
auto up = make_unique<Base>();
sp = move(up);
}
cout << "section end" << endl;
A选项:在 {}
块内,调用了 sp.reset(new Base())
。这个操作会做两件事:
- 首先,它会销毁
sp
原来持有的Base
对象,这会导致第一次调用析构函数。 - 然后,它会让
sp
持有一个新分配的Base
对象。
当 {}
块结束时,局部作用域内的变量(如果有的话)会被销毁。但在这个例子中,sp
是在外部作用域定义的,所以它不会在这里被销毁。
最后,当 main
函数结束时,sp
会被销毁,这会导致它持有的第二个 Base
对象被析构,从而调用第二次析构函数。
6、(单选)请指出如下程序的输出结果()
class A { public: int n; };
class B: public A { };
class C: public A { };
class D: public B, public C { };
void main() {
D d;
d.B::n=10;
d.C::n=20;
cout << d.B::n << "," << d.C::n;
}
A. 20,20
B. 10,20
C. 20,10
D. 10,10
class B、C都包含有class A,所以都有各自的 n
这个题目就和上面的虚基类的题目对应上了。我在这边尝试下使用虚基类看下积累会被初始化成什么样的。
自己写了代码,但是和预期的不一样
class A { public: int n; };
class B: virtual public A { };
class C: virtual public A { };
class D: public B, public C { };
int main()
{
B b;
C c;
b.n = 10;
c.n = 20;
cout << b.n << "," << c.n << endl;
D d;
d.B::n=10;
d.C::n=20;
cout << d.B::n << "," << d.C::n;
return 0;
}
// 打印信息
10,20
20,20
和预期的不一样,需要找之前的题目类型对比下。
7、(单选)已知CSomething是一个类,执行下面这些语句之后,内存里创建了多少个CSomething对象()
CSomething a; // 1
CSomething b(2); // 1
CSomething c[3]; // 3
CSomething &ra = a; // 0
CSomething d = a; // 1
CSomething *pA = c; // 0
CSomething *p = new CSomething(4); // 1
A. 8
B. 7
C. 10
D. 9
从上到下分别是1 1 3 0 1 0 1一共7个
8、(单选)【继承】有如下的类定义
class Base {
public:
virtual ~Base() { std::cout << "Destroy Base" << std::endl; }
};
class Derive1 : public Base {
public:
~Derive1() { std::cout << "Destroy Derive1" << std::endl; }
};
class Derive2 : public Derive1 {
public:
~Derive2() { std::cout << “Destroy Derive2” << std::endl; }
};
在执行
Derive1* pObj = new Derive2();
delete pObj;
时,输出是()
A. Destroy Derive1 Destroy Base
B. Destroy Base Destroy Derive1 Destroy Derive2
C. Destroy Derive2 Destroy Derive1 Destroy Base
D. 上面都不对
C 基类的析构是虚函数,则派生类析构也是虚函数,从而会一直向下传递,子子孙孙的析构全是虚函数。
如果把virtual去掉,选A
这个题目我一开始看的时候很纳闷为什么答案是C。为啥会显示Destroy Derive1?仔细看了题目才发现class Derive1是class Derive2的基类。所以new Derive2()时,Derive2的构造函数和Derive1的构造函数以及Base的构造函数都被调用了。析构时,也就先从Derive2一直析构到Base类。
另外一个问题 就是为什么把virtual去掉,选A。问了copilot,没有virtual时
解释如下:
- 当
delete pObj;
被调用时,由于Base
类的析构函数不是虚函数,析构函数不会按照从派生类到基类的顺序依次调用。 - 只会调用
pObj
的静态类型(即Derive1
)的析构函数,而不会调用Derive2
的析构函数。 - 因此,首先调用
Derive1
的析构函数,输出 “Destroy Derive1”。 - 然后调用
Base
的析构函数,输出 “Destroy Base”。
为什么只会调用 pObj的静态类型(即 Derive1)的析构函数,而不会调用 Derive2 的析构函数。
这是因为在 C++ 中,如果基类的析构函数不是虚函数,当通过基类指针删除派生类对象时,只会调用基类的析构函数,而不会调用派生类的析构函数。这是由于静态类型绑定的原因。
让我们详细解释一下:
- 静态类型和动态类型:
- 静态类型是在编译时确定的类型。在你的例子中,
pObj
的静态类型是Derive1*
。 - 动态类型是在运行时确定的类型。在你的例子中,
pObj
实际上指向的是一个Derive2
对象。
- 静态类型是在编译时确定的类型。在你的例子中,
- 非虚析构函数:
- 如果基类的析构函数不是虚函数,当通过基类指针删除派生类对象时,编译器只会调用基类的析构函数,而不会调用派生类的析构函数。这是因为编译器在编译时只知道
pObj
的静态类型是Derive1*
,所以它只会调用Derive1
的析构函数。
- 如果基类的析构函数不是虚函数,当通过基类指针删除派生类对象时,编译器只会调用基类的析构函数,而不会调用派生类的析构函数。这是因为编译器在编译时只知道
- 虚析构函数:
- 如果基类的析构函数是虚函数,当通过基类指针删除派生类对象时,编译器会在运行时根据对象的动态类型调用正确的析构函数。这意味着它会按照从派生类到基类的顺序依次调用析构函数。
第三个问题,为什么类Derive2析构函数不需要写成virtual
为什么类Derive1析构函数不需要写成virtual?
在 C++ 中,当基类的析构函数被声明为虚函数时,所有派生类的析构函数都会自动成为虚函数,即使在派生类中没有显式地使用 virtual
关键字。这是因为虚函数的特性会在继承链中传递。
具体来说,当基类的析构函数是虚函数时,通过基类指针删除派生类对象时,会确保调用派生类的析构函数,然后再调用基类的析构函数。这种机制保证了对象的正确销毁,防止资源泄漏。
通透!
总结
未完待续
原文地址:https://blog.csdn.net/2301_77560238/article/details/145213313
免责声明:本站文章内容转载自网络资源,如侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!