C++八股5
1.怎样判断两个浮点数是否相等?
由于浮点数在计算机中的表示方式和存储限制,直接使用 ==
操作符来判断两个浮点数是否相等可能会导致不准确的结果,即使这两个数看起来应该是相等的。这是因为浮点数的精度有限,在某些计算后可能产生非常小的舍入误差。
正确的做法是比较两个浮点数之间的差值是否在一个很小的范围内,而不是直接检查它们是否完全相等。可以将这两个浮点数相减,然后取结果的绝对值,并与一个预设的小阈值(也称为“精度”)进行比较。
1 | if abs(a - b) < epsilon: |
2.类如何实现只能静态分配和只能动态分配
只能静态分配: 类的对象只能在栈上创建,将
new
和delete
运算符重载并声明为private
属性。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class StackOnly {
private:
// 重载new和delete运算符,并将它们设为private
void* operator new(size_t size) {
std::cout << "Custom new is called" << std::endl;
return nullptr; // 不应该实际调用,因为我们不希望在堆上创建对象
}
void operator delete(void* ptr) noexcept {
std::cout << "Custom delete is called" << std::endl;
// 不应该实际调用
}
public:
StackOnly() {
std::cout << "StackOnly object created on stack." << std::endl;
}
~StackOnly() {
std::cout << "StackOnly object destroyed." << std::endl;
}
};
int main() {
StackOnly obj; // 正确:在栈上创建对象
// 错误:尝试在堆上创建对象(这将导致编译错误)
// StackOnly* obj2 = new StackOnly();
return 0;
}只能动态分配: 类的对象只能在堆上创建,将构造函数和析构函数设为
protected 属性
,并提供一个公有的静态成员函数用来创建对象实例。- 限制直接实例化:通过将构造函数和析构函数设为
protected
,可以防止外部代码直接使用构造函数来创建对象实例。 - 允许继承:将构造函数和析构函数设为
protected
而不是private
,使得派生类仍然能够调用基类的构造函数进行初始化。 - 集中控制对象创建逻辑:通过提供一个静态成员函数如
createInstance
来负责对象的创建,可以集中控制对象的创建过程。 - 静态成员函数无需实例即可调用:静态成员函数不依赖于类的具体实例,可以直接通过类名调用。
- 限制直接实例化:通过将构造函数和析构函数设为
1 | // 定义一个只能动态分配的基类 |
3.继承机制中对象之间如何转换?指针和引用之间如何转换?
向上类型转换: 派生类的指针或引用转换为基类的指针或引用。这种转换是安全的,并且在C++中会自动进行。
1
2
3
4
5class Base {};
class Derived : public Base {};
Derived d;
Base* ptr = &d; // 自动向上类型转换向下类型转换: 将基类的指针或引用转换为派生类的指针或引用。向下类型转换不会自动进行,必须显式地执行,通常是通过
dynamic_cast
来实现的1
2
3
4
5
6
7
8
9Base* basePtr = new Derived(); // 假设我们知道这个基类指针实际指向一个派生类对象
// 使用 dynamic_cast 进行向下类型转换,注意这里需要包含正确的头文件
Derived* derivedPtr = dynamic_cast<Derived*>(basePtr);
if (derivedPtr != nullptr) {
// 转换成功
} else {
// 转换失败
}
4.知道C++中的组合吗?它与继承相比有什么优缺点吗?
继承:
优点:
- 子类可以重写父类的方法来方便地扩展功能。
缺点:
- 高耦合性:子类依赖于父类的实现细节,这增加了耦合度
- 编译时绑定:子类从父类继承的方法在编译期就已经确定,无法在运行时改变行为。
- 维护成本:如果修改了父类的方法(如添加参数),则所有相关的子类也需要相应调整,否则可能导致错误。
组合:一个类中包含另一个类的对象作为成员变量,体现了“有一个”(Has-a)的关系。
优点:
- 低耦合性:通过组合的方式,外部对象只能通过接口访问被包含对象的功能,因此内部实现细节对外部不可见。
- 灵活性:由于当前对象和被包含的对象之间是低耦合的,所以对被包含对象的修改不需要修改当前对象的代码。
- 动态绑定:可以在运行时动态地替换所包含的对象,提供了更大的灵活性。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// 定义一个接口或抽象类,用于定义技能的行为
class Skill {
public:
virtual void use() = 0; // 纯虚函数,定义了技能的使用方法
virtual ~Skill() {} // 虚析构函数,确保派生类能正确释放
};
// 实现具体的技能A
class SkillA : public Skill {
public:
void use() override {
std::cout << "Using Skill A" << std::endl;
}
};
// 实现具体的技能B
class SkillB : public Skill {
public:
void use() override {
std::cout << "Using Skill B" << std::endl;
}
};
// 游戏角色类,通过组合方式包含技能对象
class GameCharacter {
private:
std::unique_ptr<Skill> skill; // 使用智能指针管理技能对象
public:
// 设置技能的方法,允许在运行时更换技能
void setSkill(std::unique_ptr<Skill> newSkill) {
skill = std::move(newSkill);
}
// 角色使用当前装备的技能
void performSkill() {
if (skill) {
skill->use();
} else {
std::cout << "No skill available." << std::endl;
}
}
};
int main() {
GameCharacter character;
// 初始设置为SkillA
character.setSkill(std::make_unique<SkillA>());
character.performSkill(); // 输出: Using Skill A
// 动态替换为SkillB
character.setSkill(std::make_unique<SkillB>());
character.performSkill(); // 输出: Using Skill B
return 0;
}缺点:
- 对象管理复杂:过多使用组合可能会导致创建大量小对象,增加了系统的复杂性。
- 接口设计要求高:为了有效地组合多个对象,必须仔细设计接口,确保它们能够良好协作。
5.函数指针?
函数指针的声明方法:
1 | int (*pf)(const int&, const int&); |
注:pf
是一个指向函数的指针,该函数返回 int
类型并且接受两个 const int&
类型的参数。注意这里的括号非常重要:`(pf)确保了
被应用到
pf上,表明这是一个指针;如果没有这些括号,如
int pf(const int&, const int&);,这将被解析为一个返回
int` 的函数声明。
两种方法赋值:
直接使用函数名:
1
pf = functionName;
使用取地址运算符
&
:1
pf = &functionName;
函数指针的作用:动态选择函数执行
1 |
|
6.结构体变量比较是否相等
重载 ==
操作符
1 | struct foo { |
7.函数调用过程栈的变化,返回值和参数变量哪个先入栈?
参数入栈:调用者(caller)需要将被调函数(callee)所需的参数按照与形参顺序相反的顺序压入栈中。
对于函数调用
func(a, b, c)
,则它们会以c
,b
,a
的顺序依次压入栈中。调用和返回地址入栈: 使用
call
指令进行函数调用时,该指令除了跳转到被调函数的起始地址执行外,还会自动将call
指令的下一条指令的地址(即返回地址)压入栈中。- 设置新的栈帧: 一旦控制权转移到被调函数,首先要做的就是设置一个新的栈帧。
- 局部变量和临时变量入栈: 在新栈帧内,局部变量根据定义顺序分配空间,地址随栈的增长方向递减。
8.C++中类成员的访问权限和继承权限问题
三种访问权限:
- 一个类的public成员变量、成员函数,可以通过类的成员函数、类的实例变量进行访问
- 一个类的protected成员变量、成员函数,无法通过类的实例变量进行访问。但是可以通过类的友元函数、友元类进行访问。
- 一个类的private成员变量、成员函数,无法通过类的实例变量进行访问。但是可以通过类的友元函数、友元类进行访问。
继承权限:
public继承: 基类的各种权限不变 。
protected继承: 派生类通过protected继承,基类的public成员在派生类中的权限变成了protected 。protected和private不变。
private继承: private继承看成派生类将基类的public,protected成员囊括到派生类,全部作为派生类的private成员,但是不包括private成员。
注:基类的私有成员都不会直接被派生类继承
9.cout和printf有什么区别?
cout<<
是一个函数,cout<<
后可以跟不同的类型是因为cout<<
已存在针对各种类型数据的重载,所以会自动识别数据的类型。
缓冲机制:
cout
(有缓冲输出): 输出到cout
的内容首先会被放入缓冲区,然后根据特定条件(如遇到换行符\n
、程序结束、手动刷新缓冲区等)才会从缓冲区输出到屏幕。为了确保立即输出内容,可以使用endl
或者显式地调用flush
方法(如cout << "abc" << endl;
或cout << "abc\n" << flush;
)。endl
不仅插入了一个换行符,还会导致流被刷新;而flush
仅刷新流而不添加任何字符。printf
(无缓冲输出): 传统上认为printf
是无缓冲输出,意味着当你调用printf
时,输出几乎是立即显示的。
10.为什么可以为类的成员函数创建模板,但不能是虚函数和析构函数?
析构函数:没有参数,不需要使用模板
虚函数:由于模板成员函数的实例化是编译时的行为,而虚函数的动态绑定是运行时的行为,这两者无法同时满足。
11.定义和声明的区别
变量的声明和定义:
- 声明:当提到变量时,声明是指告诉编译器存在某个类型的变量,但并不会为这个变量分配内存空间。
- 定义:而定义则是实际为变量分配内存空间的过程。这意味着系统会为该变量预留一定的存储空间,用于存放它的值。
函数的声明和定义:
- 声明:对于函数来说,声明位于头文件(.h文件)中,其目的是通知编译器有关该函数的一些基本信息,比如函数名称、返回类型以及参数列表等。
- 定义:函数的定义则包含了函数的实际实现代码,即函数体。它描述了函数执行的操作,并且位于源文件(.c或.cpp文件)中。
12.静态成员与普通成员的区别是什么?
生命周期:
- 静态成员变量:其生命周期从类被加载到内存开始,直到类被卸载为止。
- 普通成员变量:它们的生命周期与对象实例紧密相关,只有当创建了一个类的对象时,普通成员变量才会存在
共享方式:
- 静态成员变量:所有的对象都共享同一个静态成员变量
- 普通成员变量:每个对象都有自己的一份副本
定义位置:
- 普通成员变量:通常存储在栈或堆中,具体取决于它们是如何被声明和使用的。
- 静态成员变量:存储在静态全局区,这意味着它们的存储空间是在程序启动时分配的,并且不会随对象的创建和销毁而变化。
初始化位置:
- 普通成员变量:可以在类定义中直接初始化
- 静态成员变量:必须在类外部进行初始化(除非是静态常量整型成员,可以直接在类定义中初始化)。
13.说一下你理解的 ifdef endif代表着什么?
#ifdef
和#endif
是C/C++语言中的预处理指令,用于实现条件编译。
防止头文件重复包含(头文件卫士):避免因多次包含同一头文件而导致的重定义问题。
1 |
|
14.隐式转换,如何消除隐式转换?
隐式转换:
- 子类对象可以隐式地转换为父类对象(多态)
- 从小的数据类型(如
char
或short
)向大的数据类型(如int
或long
)转换
消除隐式转换的方法:
使用
explicit
关键字:在构造函数声明时加上explicit
关键字。1
2
3
4class MyClass {
public:
explicit MyClass(int a) { /* 构造函数体 */ }
};
15.如果有一个空类,它会默认添加哪些函数?
- 默认构造函数 (
Empty::Empty()
) - 拷贝构造函数 (
Empty::Empty(const Empty&)
) - 析构函数 (
Empty::~Empty()
) - 赋值运算符重载函数 (
Empty& Empty::operator=(const Empty&)
)
16.你知道const char* 与string之间的关系是什么吗?
std::string
是C++标准库提供的一个类,用于封装字符串操作const char\*
是C风格的字符串表示形式,它实际上是一个指向以空字符(\0
)结尾的字符数组的指针
相互转换:
从
std::string
转换为const char\*
: 使用std::string
的成员函数c_str()
可以获取一个 C 风格的字符串1
2std::string s = "abc";
const char* c_s = s.c_str();从
const char\*
转换为std::string
: 可以直接通过构造函数将const char*
转换为std::string
,这是隐式转换的一个例子1
2const char* c_s = "abc";
std::string s(c_s);
17.你什么情况用指针当参数,什么时候用引用,为什么?
简单的数据类型:可以按值传递,因为复制成本低
数组只能使用指针
类对象使用引用传递
18.如何设计一个类计算子类的个数?
static静态变量coun计数:
为类设计一个static静态变量count作为计数器;
类定义结束后初始化count;
在构造函数中对count进行+1;
设计拷贝构造函数,在进行拷贝构造函数中进行count +1,操作;
设计复制构造函数,在进行复制函数中对count+1操作;
在析构函数中对count进行-1;
19.说一说strcpy、sprintf与memcpy这三个函数的不同之处
strcpy
: 将src
指向的字符串(包括终止符\0
)复制到dest
指向的位置。1
char *strcpy(char *dest, const char *src);
sprintf
: 根据提供的格式化字符串format
和后续参数,生成一个格式化的字符串,并存储在str
指向的缓冲区中1
int sprintf(char *str, const char *format, ...);
1
2
3
4
5
6
7
8
int main() {
char buffer[50];
int number = 42;
sprintf(buffer, "The answer is %d", number);
printf("%s\n", buffer);
return 0;
}memcpy
: 从src
指向的内存地址开始复制n
个字节到dest
指向的内存地址。1
void *memcpy(void *dest, const void *src, size_t n);
1
2
3
4
5
6
7
8
9
int main() {
char src[] = "Hello, World!";
char dest[50];
memcpy(dest, src, strlen(src) + 1); // 包括终止符'\0'
printf("Copied string: %s\n", dest);
return 0;
}
20.如何阻止一个类被实例化?有哪些方法?
将类定义为抽象基类: 抽象基类是一种不能实例化的类,它包含至少一个纯虚函数(即没有实现体的虚函数)
1
2
3
4
5
6
7
8class AbstractClass {
public:
virtual void pureVirtualFunction() = 0; // 纯虚函数
virtual ~AbstractClass() {} // 虚析构函数确保派生类正确释放资源
};
// 下面的代码会导致编译错误,因为不能实例化抽象类
// AbstractClass obj;将构造函数声明为
private
: 提供静态成员函数来允许有限制地创建对象,或者根本不提供创建对象的方式。1
2
3
4
5
6
7
8
9
10
11
12
13class NonInstantiableClass {
private:
NonInstantiableClass() {} // 私有构造函数
~NonInstantiableClass() {} // 私有析构函数
public:
static void doSomething() {
// 可以在这里执行一些操作,但不能创建对象实例
}
};
// 下面的代码会导致编译错误,因为构造函数是私有的
// NonInstantiableClass obj;
静态成员函数来允许有限制地创建对象:
单例模式: 单例模式确保一个类只有一个实例,使用一个静态方法来返回唯一的实例。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class NonInstantiableClass {
private:
// 私有构造函数防止外部实例化
NonInstantiableClass() {
std::cout << "Constructor called" << std::endl;
}
// 私有析构函数
~NonInstantiableClass() {
std::cout << "Destructor called" << std::endl;
}
// 防止拷贝构造和赋值操作
NonInstantiableClass(const NonInstantiableClass&) = delete;
NonInstantiableClass& operator=(const NonInstantiableClass&) = delete;
public:
// 静态方法用于获取唯一实例
static NonInstantiableClass& getInstance() {
static NonInstantiableClass instance; // 局部静态变量保证线程安全
return instance;
}
void doSomething() const {
std::cout << "Doing something..." << std::endl;
}
};
int main() {
// 通过静态方法获取唯一实例并调用其方法
NonInstantiableClass& obj = NonInstantiableClass::getInstance();
obj.doSomething();
return 0;
}工厂方法模式: 工厂方法模式允许类将对象的创建过程封装在一个或多个工厂方法中。这种方式可以控制对象的创建逻辑,并且可以隐藏具体的实现细节。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class NonInstantiableClass {
private:
// 私有构造函数防止外部实例化
NonInstantiableClass() {
std::cout << "Constructor called" << std::endl;
}
// 私有析构函数
~NonInstantiableClass() {
std::cout << "Destructor called" << std::endl;
}
// 防止拷贝构造和赋值操作
NonInstantiableClass(const NonInstantiableClass&) = delete;
NonInstantiableClass& operator=(const NonInstantiableClass&) = delete;
public:
// 静态工厂方法用于创建对象实例
static std::unique_ptr<NonInstantiableClass> createInstance() {
return std::make_unique<NonInstantiableClass>();
}
void doSomething() const {
std::cout << "Doing something..." << std::endl;
}
};
int main() {
// 使用工厂方法创建对象实例
std::unique_ptr<NonInstantiableClass> obj = NonInstantiableClass::createInstance();
if (obj) {
obj->doSomething();
}
return 0;
}
21.虚基类
虚基类主要用于解决多重继承中的菱形继承(多义性)问题
当一个类从多个类继承,而这些类又有一个共同的基类时,如果不使用虚基类,可能会导致基类部分被多次复制。通过将共同基类声明为虚基类,可以确保该基类在派生类中只存在一份实例。
在FinalClass
类的构造函数中,需要显式调用 Base
类的构造函数,因为 Base
是虚基类,必须明确初始化。
1 | #include <iostream> |