C++八股4
1.malloc、realloc、calloc的区别
malloc
函数:用于分配指定大小的内存块,并返回指向该内存块的指针。如果分配失败,则返回
NULL
。使用
malloc
分配的内存不会被初始化,其中的数据是随机的。
calloc
函数:- 原型为
void* calloc(size_t n, size_t size);
- 第一个参数
n
表示要分配多少个元素;第二个参数size
表示每个元素的大小(以字节为单位)。 calloc
会将分配的内存初始化为零。
- 原型为
realloc
函数:原型为
void* realloc(void *p, size_t new_size);
realloc
函数用来改变已经分配的内存块的大小。它可以扩大或缩小已有的内存块。如果新的内存大小大于原来的大小,那么新增的部分内容未定义(即可能包含任何值)。如果缩小内存块,超出部分会被丢弃。注:C++中可以通过使用STL容器来避免使用
realloc
的情况,可以减少由于手动管理内存而引发错误的可能。
2.类成员初始化方式?构造函数的执行顺序 ?为什么用成员初始化列表会快一些?
类成员初始化方式:
- 赋值初始化:这是通过在构造函数体内对成员变量进行赋值来实现的。
- 成员初始化列表:使用冒号(
:
)后跟随初始化列表的方式,在构造函数体执行前对成员变量进行初始化。
构造函数的执行顺序:(在创建派生类对象时,基类的构造函数会在派生类的构造函数之前执行。)
- 首先执行虚拟基类的构造函数(如果有多个虚拟基类,则按照它们被继承的顺序)。
- 然后是普通基类的构造函数(如果有多个普通基类,则按照它们在派生类继承列表中的顺序)。
- 接着是派生类中对象成员(包含其他类类型的对象作为其成员变量)的构造函数(这些成员对象按照它们在类定义中的声明顺序进行初始化,而不是按照初始化列表中的顺序)。
- 最后是派生类自己的构造函数。
使用成员初始化列表更快的原因:
- C++的赋值操作是会产生临时对象的:使用赋值初始化时,首先会调用默认构造函数为成员变量分配空间并创建一个临时对象,然后将这个临时对象赋值给成员变量,这会产生额外的开销。
- 使用成员初始化列表直接在成员变量创建时就对其进行初始化,避免了创建临时对象的过程。
3.有哪些情况必须用到成员列表初始化?作用是什么?
成员列表初始化的四种必须使用的情况:
初始化引用成员:引用成员必须在创建时初始化,且不能重新赋值。因此,必须在成员初始化列表中进行初始化。
初始化常量成员:常量成员(
const
)一旦初始化后不能修改,因此必须在成员初始化列表中初始化。调用基类的构造函数:如果基类没有默认构造函数,或者需要传递参数给基类构造函数,必须在成员初始化列表中调用基类的构造函数。
调用成员类的构造函数:如果类的成员是另一个类的对象,并且该成员类没有默认构造函数,或者需要传递参数,必须在成员初始化列表中调用该成员类的构造函数。
注:在函数体内进行赋值初始化时,成员就已经存在了
成员初始化列表的作用:
- 初始化顺序:编译器会按照成员初始化列表中的顺序,在构造函数体内插入初始化操作,且这些操作在任何用户代码之前执行。
- 初始化顺序的决定:初始化列表的顺序并不决定实际的初始化顺序,实际的初始化顺序由类中成员的声明顺序决定。
4.C++中新增了string,它与C语言中的 char *有什么区别吗?它是如何实现的?
- string是对char*进行了封装,封装的string包含了char*数组,容量,长度等等属性。
- string可以进行动态扩展,在每次扩展的时候另外申请一块原空间大小两倍的空间(2^n),然后将原字符串拷贝过去,并加上新增的内容。
5.什么是内存泄露,如何检测与避免
内存泄露:指的是堆内存的泄露,分配的内存块在使用完成后没有正确地释放。
避免内存的泄露的方式:
计数法:使用new或者malloc时,让该数+1,delete或free时,该数-1,程序执行完打印这个计数,如果不为0则表示存在内存泄露
将基类的析构函数声明为虚函数:这可以保证当通过基类指针删除派生类对象时,派生类的析构函数能够被正确调用,从而避免因未正确调用析构函数导致的内存泄漏。
注:如果基类的析构函数不是
virtual
,通过基类指针删除派生类对象只会调用基类的析构函数。正确释放对象数组:对于使用
new[]
分配的对象数组,应使用delete[]
来释放,而不是delete
,以避免内存泄漏。- 成对出现原则:确保每一对
new
/delete
和malloc
/free
成对出现,即每次分配内存后都必须有相应的释放操作
检测工具
- Linux下可以使用Valgrind工具
- Windows下可以使用CRT库
6.对象复用的了解,零拷贝的了解
对象复用:在设计模式中通常指的是Flyweight(享元)模式。该模式旨在通过共享尽可能多的数据来最小化内存使用,特别是当系统需要创建大量细粒度的对象时。通过将相似或相同的对象存储在一个“对象池”中,并在需要时重复利用这些对象,可以有效减少内存占用和对象创建的开销。
亨元模式: 由亨元工厂创建新的亨元对象前,先检查“对象池”中是否有相同属性对象,若有可以重复使用这些对象,而不是创建新的对象。
eg:假设我们正在开发一个文本编辑器,其中每个字符都由一个独立的对象表示。如果我们直接为每个字符创建一个新的对象实例,那么对于长文档来说,这将导致大量的内存消耗。Flyweight模式可以帮助我们通过共享相同字符的属性(如字体、颜色等)来优化这一过程。
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
57
58
59
60
61
62
63
64
65
66
67
68
69
// Flyweight接口
class Character {
public:
virtual void display(char ch) const = 0;
};
// 具体Flyweight类
class ConcreteCharacter : public Character {
private:
char font_family_;
int font_size_;
std::string color_;
public:
ConcreteCharacter(char font_family, int font_size, const std::string& color)
: font_family_(font_family), font_size_(font_size), color_(color) {}
void display(char ch) const override {
std::cout << "Character: " << ch
<< ", Font Family: " << font_family_
<< ", Font Size: " << font_size_
<< ", Color: " << color_ << std::endl;
}
};
// Flyweight工厂
class CharacterFactory {
private:
std::unordered_map<std::string, Character*> characters_;
public:
~CharacterFactory() {
for (auto& pair : characters_) {
delete pair.second;
}
}
Character* getCharacter(char font_family, int font_size, const std::string& color) {
std::string key = std::string(1, font_family) + std::to_string(font_size) + color;
if (characters_.find(key) == characters_.end()) {
characters_[key] = new ConcreteCharacter(font_family, font_size, color);
}
return characters_[key];
}
};
int main() {
CharacterFactory factory;
// 创建一些字符对象,但共享相同的属性
Character* charA = factory.getCharacter('T', 12, "red");
Character* charB = factory.getCharacter('T', 12, "red"); // 应该复用charA的实例
charA->display('A');
charB->display('B');
// 即使请求了两次相同的属性组合,实际上只会有一个ConcreteCharacter实例被创建
if (charA == charB) {
std::cout << "charA and charB are the same instance." << std::endl;
} else {
std::cout << "charA and charB are different instances." << std::endl;
}
return 0;
}代码中分别请求了两个具有相同属性的字符对象,但实际上它们引用的是同一个
ConcreteCharacter
实例,实现了对象复用的目的。
零拷贝:一种优化技术,目的是减少数据从一处存储到另一处传输过程中CPU的参与程度,从而提高效率并降低资源消耗。传统上,数据传输可能涉及多次复制操作,比如从磁盘读取数据到内核空间,再复制到用户空间等。零拷贝技术减少了这种不必要的数据复制次数,降低了CPU的工作量和总线活动。
*注:vector
的一个成员函数emplace_back()很好地体现了零拷贝技术。emplace_back()
是在容器内部直接构造对象,而不是先创建一个临时对象再进行拷贝或移动。这意味着它可以在不调用拷贝构造函数或移动构造函数的情况下,在容器预留的位置原地构造对象,这通常更高效。*
7.介绍面向对象的三大特性,并且举例说明
- 继承: 让某种类型对象获得另一个类型对象的属性和方法。
- 实现继承:指使用基类的属性和方法而无需额外编码的能力
- 接口继承:指仅使用属性和方法的名称、但是子类必须提供实现的能力
- 封装: 把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏
- 多态: 同一事物表现出不同事物的能力
- 实现多态有二种方式:覆盖(override),重载(overload)
- 重载实现编译时多态,虚函数实现运行时多态
8.C++的四种强制转换
C++提供了四种类型的显式类型转换运算符:reinterpret_cast
、const_cast
、static_cast
和 dynamic_cast
reinterpret_cast
:在不相关的类型之间进行低级别的强制转换。它可以将任何指针类型转换为另一个指针类型,或指针到整数类型的转换等。转换后的数和原来的数有相同的比特位。1
char *c = reinterpret_cast<char*>(p); // 将int指针转换为char指针
注:
reinterpret_cast
没有进行任何的类型检查,因此很容易发生由于指针类型不匹配而引发的内存错误const_cast
:主要用于添加或移除变量的const
或volatile
属性。它只能改变这些限定符,不能改变对象的实际类型。1
2const char *p = "hello";
char *q = const_cast<char*>(p);注:这里将 `const char
类型的指针
p转换为
char类型的指针
q`。static_cast
:用于类层次结构中的上行转换(派生类到基类)和下行转换(基类到派生类),以及基本数据类型之间的转换。特点:不进行运行时类型检查,因此在下行转换时可能不安全。
1
2int n = 65;
char c = static_cast<char>(n);//这里将 int 类型的变量 n 转换为 char 类型的变量 c。
dynamic_cast
: 主要用于类层次结构中的上行转换和下行转换,特别是用于多态类型的转换。特点:在运行时进行类型检查,如果转换不安全(如下行转换时基类指针不指向派生类对象),则返回
nullptr
。1
2
3
4
5
6
7Base *b = new Son;
Son *s = dynamic_cast<Son*>(b);//这里将 Base* 类型的指针 b 转换为 Son* 类型的指针 s,并在转换失败时返回 nullptr。
if (s) {
// 转换成功
} else {
// 转换失败
}
9.C++函数调用的压栈过程
函数调用的压栈过程:
- 分配栈空间:为被调用函数分配栈空间,用于存储函数的局部变量、参数和返回地址等信息。
- 参数压栈:将实参的值复制到形参的栈空间中。C++中,参数是从右到左依次压栈的。
- 保存返回地址:将当前函数的返回地址(即调用函数的下一条指令地址)压入栈中,以便函数执行完毕后能正确返回到调用点。
- 保存调用函数的运行状态:将调用函数的寄存器状态、局部变量等信息压入栈中,以便函数调用结束后恢复。
- 执行被调用函数:跳转到被调用函数的代码并执行。
- 函数返回:函数执行完毕后,从栈中弹出返回地址、恢复调用函数的运行状态,并释放栈空间。
1 |
|
输出:
1 | 2 |
==10.说说移动构造函数==
既要实现资源的有效转移,又要避免潜在的双重释放的风险
移动构造函数:用“转移”代替“复制”来提高性能
拷贝构造 vs 移动构造
- 拷贝构造函数:通常涉及到深拷贝,特别是对于包含动态分配资源的对象。例如,如果一个类含有指针成员变量,那么在拷贝构造时需要为新对象分配新的内存,并将原始对象的数据复制过去。这增加了额外的时间和空间开销。
- 移动构造函数:则是采用浅层复制的方式,直接将资源的所有权从一个对象转移到另一个对象,而不进行实际的数据复制。这样做的前提是确保源对象不再持有这些资源,以防止重复释放同一块内存导致的错误。
- 参数类型差异
- 拷贝构造函数:接受一个左值引用作为参数,意味着它可以接受任何有效的已有对象。
- 移动构造函数:接受一个右值引用(或称为将亡值引用)作为参数。这意味着它只能用于那些即将销毁或不再使用的对象,比如临时对象或者通过
std::move
显式转换成右值的对象。
如何实现安全的资源转移?
为了避免因浅层复制导致的双重释放问题,移动构造函数会在获取源对象资源的同时,将源对象中的相应指针置为nullptr
(或其他适当值)。
1 | class MyClass { |
在这个例子中,MyClass
的移动构造函数接收一个右值引用 MyClass&&
,并将other
对象的数据成员data
直接赋值给当前对象的data
成员,然后将other.data
设置为nullptr
。这样做既实现了资源的有效转移,又避免了潜在的双重释放风险。
1 | MyClass a; |
注:std::move(a)
使得a
被视为一个右值,从而触发MyClass
的移动构造函数。
11.说一下C++左值引用和右值引用
左值(lvalue):
可以获取地址的表达式。
通常有名字,可以出现在赋值语句的左边。
例如:变量、对象、函数返回的引用等。
1
2int a = 10; // a 是左值
int& b = a; // b 是左值引用
右值(rvalue):
不能获取地址的表达式。
通常是临时的、没有名字的值。
例如:常量、临时对象、函数返回值(非引用)、表达式结果等。
1
2int c = 5 + 10; // 5 + 10 是右值
int&& d = 10; // 10 是右值- 纯右值(prvalue):如字面量、临时对象、表达式结果等。
- 将亡值(xvalue):与右值引用相关的表达式,通常是通过
std::move
或返回右值引用的函数得到的值。
左值引用:用 &
声明。
右值引用:用 &&
声明。主要用于实现 移动语义 和 完美转发。
右值引用的特点:
延长右值的生命周期:右值引用可以将临时对象(右值)的生命周期延长到与右值引用变量的生命周期一致。
1
int&& d = 10; // 临时对象 10 的生命周期被延长
右值引用可能是左值或右值:右值引用类型的变量本身可能是左值或右值,取决于其初始化方式。
1
int&& e = 10; // e 是右值引用,但 e 本身是左值
自动类型推断中的右值引用:在模板函数中,
T&&
可以是左值引用或右值引用,取决于传入的参数类型。1
2
3
4
5
6
7
8
9
10
11template<typename T>
void fun(T&& t) {
cout << t << endl;
}
int main() {
int a = 10;
fun(a); // t 是左值引用
fun(10); // t 是右值引用
return 0;
}
移动语义:通过 std::move
将左值强制转换为右值引用,从而避免不必要的拷贝操作。
1 | std::vector<int> v1 = {1, 2, 3}; |
完美转发:通过右值引用和 std::forward
,将参数按照其原始类型转发给其他函数
1 | template<typename T> |
12.C++中将临时变量作为返回值时的处理过程
在C++中,当函数返回临时变量时,虽然该临时变量会在函数退出时被销毁,但由于返回值是通过寄存器而非栈或堆内存进行传递的,因此返回值的正确性和完整性得到了保证。
13.静态类型和动态类型,静态绑定和动态绑定的介绍
静态类型与动态类型
静态类型:对象在声明时采⽤的类型,在编译期既已确定;例如,
A* pa
的静态类型是A*
,无论pa
指向的是哪个子类对象。动态类型:指针或引用实际指向的对象的类型,是在运行期决定的。例如,
A* pa = new B();
,pa
的静态类型是A*
,但动态类型是B*
。
静态绑定与动态绑定
- 静态绑定:绑定的是静态类型,函数或属性的调用依赖于对象的静态类型,发生在编译期。非虚函数通常使用静态绑定。
- 动态绑定:绑定的是动态类型,函数或属性的调用依赖于对象的动态类型,发生在运行期。虚函数使用动态绑定,从而实现多态性。
eg:
1 |
|
注1:如果 A
中的 func
不是虚函数,那么 pa->func()
将总是调用 A::func()
,因为这是基于 pa
的静态类型(即 `A`)进行的静态绑定。*
注2:如果将 func
声明为虚函数(取消注释 virtual
),那么 pa->func()
将根据 pa
所指向的实际对象类型(即动态类型)调用相应的 func
函数,实现动态绑定。
14.引用是否能实现动态绑定,为什么可以实现?
引用和指针都可以实现动态绑定,但这种动态绑定仅适用于虚函数。
动态绑定的条件
- 虚函数:只有当一个函数被声明为
virtual
时,才能通过基类的引用或指针调用派生类中的重写版本。这是因为虚函数支持运行期确定实际调用哪个函数,这被称为动态绑定或多态性。 - 引用必须初始化:创建引用时必须同时初始化它,这意味着你必须指定引用所绑定的具体对象。一旦初始化完成,引用就不能再指向其他对象。
1 |
|