主题:后端开发面经系列 -- 华为OD -- C++面经(1)
来自:阿Q技术站
一面
1、C++结构体和类的区别,类默认的访问权限,结构体可以定义成员函数吗?
1. 默认访问权限
- 结构体(struct):成员变量和成员函数默认是 public 访问权限。
- 类(class):成员变量和成员函数默认是 private 访问权限。
struct MyStruct {
int x; // 默认是 public
void foo() {} // 默认是 public
};
class MyClass {
int y; // 默认是 private
void bar() {} // 默认是 private
};
2. 语法上和使用上的区别
- 定义方式:虽然 struct 和 class 可以用来定义数据成员和成员函数,但使用 class 更常见于表示具有行为和状态的对象,而 struct 更常用于表示纯粹的数据结构。
- 继承:在继承时,class 的继承默认是 private,而 struct 的继承默认是 public。
struct Base1 {};
class Base2 {};
// 继承默认权限不同
struct Derived1 : Base1 {}; // public 继承
class Derived2 : Base2 {}; // private 继承
3. 结构体是否可以定义成员函数
C++ 的 struct 可以定义成员函数。实际上,struct 和 class 除了默认访问权限不同外,语法上几乎是一样的。
struct MyStruct {
int x;
void setX(int val) {
x = val;
}
int getX() {
return x;
}
};
2、多态的意义?
多态指的是同一个接口可以有不同的实现方式。多态通过允许不同类型的对象以相同的方式进行处理,极大地提高了代码的灵活性和可扩展性。多态主要通过继承和接口实现,并且可以分为编译时多态和运行时多态。
多态的意义
1. 提高代码重用性和可维护性: 多态允许你编写更加通用的代码。例如,可以编写一个函数来处理不同类型的对象,而不需要了解这些对象的具体类型。这样,当需要增加新的类型时,只需要新增类的实现,而不需要修改已经存在的代码。
// 基类
class Animal {
public:
virtual void makeSound() = 0; // 纯虚函数
};
// 派生类
class Dog : public Animal {
public:
void makeSound() override {
cout << "Woof" << endl;
}
};
class Cat : public Animal {
public:
void makeSound() override {
cout << "Meow" << endl;
}
};
void makeAnimalSound(Animal* animal) {
animal->makeSound();
}
int main() {
Dog dog;
Cat cat;
makeAnimalSound(&dog); // 输出 Woof
makeAnimalSound(&cat); // 输出 Meow
}
2. 简化代码: 多态可以用统一的接口来操作不同类型的对象,简化了代码的复杂度和可读性。例如,在上面的代码中,无论是 Dog 还是 Cat,你都可以通过调用 makeSound 来发出声音,而不需要分别为每种类型写不同的处理逻辑。
3. 增强系统的扩展性: 多态使得系统更容易扩展。当需要添加新的功能时,只需要新增实现类,而不需要修改现有的代码。例如,如果需要新增一个 Bird 类,只需要继承 Animal 并实现 makeSound 方法。
4. 实现动态绑定: 通过多态,程序在运行时可以根据对象的实际类型进行方法调用,而不是在编译时确定调用哪个方法。这种动态绑定使得程序更加灵活和动态。
多态的实现方式
- 继承:通过继承基类的接口并重写其方法,子类可以表现出不同的行为。
- 虚函数:在基类中定义虚函数(virtual functions),并在子类中重写这些虚函数。通过基类指针或引用调用虚函数时,会调用实际对象的实现。
- 接口:在某些语言(如 Java 和 C#)中,通过实现接口(interface)来实现多态。接口定义了方法的签名,具体的实现由类提供。
3、重载和重写的区别?
重载(Overloading)
重载是指在同一个类中定义多个同名方法,但这些方法具有不同的参数列表(参数类型或参数个数)。编译器根据方法的参数列表来区分这些方法。在C++中,构造函数也可以被重载。
特点
- 方法名相同:重载的方法必须具有相同的名称。
- 参数列表不同:重载的方法必须具有不同的参数列表(参数类型或参数个数)。
- 返回类型可以不同:虽然返回类型可以不同,但返回类型不是区分重载方法的依据。
- 同一个类中:重载的方法必须定义在同一个类中。
例子
class Print {
public:
void display(int i) {
cout << "整数: " << i << endl;
}
void display(double f) {
cout << "浮点数: " << f << endl;
}
void display(string s) {
cout << "字符串: " << s << endl;
}
};
int main() {
Print obj;
obj.display(5); // 调用 display(int)
obj.display(3.14); // 调用 display(double)
obj.display("Hello");// 调用 display(string)
return 0;
}
重写(Overriding)
重写是指子类重新定义从基类继承的方法,目的是提供子类自己的实现版本。重写的方法必须具有相同的名称、参数列表和返回类型。重写通常与多态(Polymorphism)结合使用。
特点
- 方法名相同:重写的方法必须具有与基类方法相同的名称。
- 参数列表相同:重写的方法必须具有与基类方法相同的参数列表。
- 返回类型相同:重写的方法必须具有与基类方法相同的返回类型。
- 基类和子类之间:重写的方法在基类中声明,在子类中实现。
- 虚函数:在C++中,基类方法通常需要声明为虚函数(virtual)以允许子类重写。
例子
class Animal {
public:
virtual void makeSound() {
cout << "Animal sound" << endl;
}
};
class Dog : public Animal {
public:
void makeSound() override { // 重写基类的 makeSound 方法
cout << "Woof" << endl;
}
};
int main() {
Animal* animal = new Dog();
animal->makeSound(); // 调用的是 Dog 类的 makeSound 方法
delete animal;
return 0;
}
顺便吆喝一下,技术大厂,前后端测试,来看看!
4、TCP/IP 三次握手的过程,为什么要3次?
在建立连接之前,Client处于CLOSED状态,而Server处于LISTEN的状态。
1. 第一次握手(SYN-1):
客户端发送一个带有 SYN 标志的 TCP 报文段给服务器,表示客户端请求建立连接。
客户端选择一个初始序列号(ISN)并将其放入报文段中,进入 SYN_SENT 状态。
2. 第二次握手(SYN + ACK):
服务器收到客户端发送的 SYN 报文段后,如果同意建立连接,会发送一个带有 SYN 和 ACK 标志的报文段给客户端,表示服务器接受了客户端的请求,并带上自己的 ISN。
服务器进入 SYN_RCVD 状态。
3. 第三次握手(ACK):
客户端收到服务器发送的 SYN+ACK 报文段后,会发送一个带有 ACK 标志的报文段给服务器,表示客户端确认了服务器的响应。
客户端和服务器都进入 ESTABLISHED 状态,连接建立成功,可以开始进行数据传输。
为什么需要三次握手?
1. 确保双方都能发送和接收数据:
第一次握手确认客户端的发送能力和服务器的接收能力。
第二次握手确认服务器的发送能力和客户端的接收能力。
第三次握手确认客户端的发送能力和服务器的接收能力。
2. 防止旧的连接请求误导双方:
通过三次握手,双方都能确认对方的状态是最新的,有效避免了网络中的旧的、延迟的SYN包造成的错误连接。
3. 防止重复数据包干扰:
三次握手确保双方都能有效处理重复的数据包,并建立一个唯一的连接。
5、进程和线程的区别?
定义
进程(Process):
进程是操作系统分配资源的基本单位。每个进程有自己独立的内存空间,包括代码段、数据段、堆、和栈。
进程之间相互独立,进程的创建、执行和销毁都是由操作系统管理的。
线程(Thread):
线程是进程的一个执行单元,是CPU调度的基本单位。一个进程可以包含多个线程,这些线程共享进程的资源(如内存和文件描述符)。
线程之间的切换比进程切换更轻量,因为线程共享进程的资源,不需要频繁的资源分配和回收。
区别
1. 内存和资源:
进程:每个进程都有自己独立的地址空间和资源,相互之间不会直接干涉。进程之间的通信需要通过进程间通信(IPC)机制,如管道、消息队列、共享内存等。
线程:线程共享同一个进程的地址空间和资源,所以线程之间的通信更加直接、快速,但也带来了一些同步和互斥的问题。
2.创建和销毁:
进程:创建和销毁进程的开销较大,因为操作系统需要为进程分配和回收大量的资源。
线程:创建和销毁线程的开销较小,因为线程共享进程的资源,不需要重新分配内存。
3. 切换开销:
进程:进程切换需要上下文切换,包括保存和恢复寄存器、内存映射等,开销较大。
线程:线程切换开销较小,因为线程共享同一进程的上下文,只需要保存和恢复少量的状态信息。
4. 独立性和安全性:
进程:由于进程独立,进程间的错误不会相互影响,一个进程崩溃不会影响其他进程。
线程:由于线程共享资源,一个线程的错误(如内存泄漏、死锁)可能影响整个进程中的其他线程。
使用场景
进程:适用于需要高独立性和安全性的任务,比如不同用户的程序、服务和应用之间的隔离。
线程:适用于需要高效并发的任务,如多线程服务器、并行计算等,需要在同一进程内执行多个任务。