C++面试速通宝典

149. 用C编写一个死循环

while(1){}

请注意
‌‌‌‌  很多种途径都可以实现同一种功能,但还是不同的方法时间和空间占用度不同,特别是对于嵌入式软件,处理器速度比较慢,存储空间小,所以时间和空间优势是选择各种方法的首要考虑条件。

150. 编码实现某一变量某位清0或置1

‌‌‌‌  给定一个整型变量a,写两段代码,第一个设置a的bit 3,第二个清a的bit 3,在以上两个操作中,要保持其他位不变。

#define BIT3(0x1 << 3) Satic int a

设置a的bit3:
void set_bit3(void){
	a |= BIT3; // 将a第3位置1
}
清a的bit3
void set_bit3(void){
	a &= ~BIT3; //将a的第3位清零
}

请注意
‌‌‌‌  在置或清变量或寄存器的某一位时,一定要注意不要影响其他位,所以用加减法是很难实现的。

解释

#include <iostream>

#define BIT3 (0x1 << 3) // 将0x1左移3位,得到bit 3的掩码
static int a = 0;      // 定义一个静态整型变量a,初始值为0

‌‌‌‌  这里,我们首先定义了一个宏 BIT3,其值为 0x1 << 3,即将 0x1 左移 3 位。左移运算符 << 会将一个数字左移指定的位数,所以 0x1 << 3 就是 0b0001 变成 0b1000(即十进制的 8)。

void set_bit3(void) {
    a |= BIT3; // 将a的第3位设置为1
}

解释:

  • BIT3 的值是 0x1 << 3,即 0b1000
  • 按位或运算符 | 用于将 a 的第 3 位设置为 1,保持其他位不变。

假设 a 当前值为 0000 0101(即 0x5),调用 set_bit3() 后:

  1. BIT3 的值为 0b1000(即 0x8)。
  2. 执行 a |= BIT3
    • 0000 0101 | 0000 1000 = 0000 1101
  3. a 变为 0000 1101(即 0xD)。
void clear_bit3(void) {
    a &= ~BIT3; // 将a的第3位清除为0
}

解释:

  • BIT3 的值是 0x1 << 3,即 0b1000
  • 按位与运算符 & 与按位取反运算符 ~ 用于将 a 的第 3 位清除为 0,保持其他位不变。

假设 a 当前值为 0000 1101(即 0xD),调用 clear_bit3() 后:

  1. BIT3 的值为 0b1000(即 0x8)。
  2. ~BIT3 的值为 ~0b1000,即 0b0111(即 0x7)。
  3. 执行 a &= ~BIT3
    • 0000 1101 & 0000 0111 = 0000 0101
  4. a 变为 0000 0101(即 0x5)。

151. 评论下面这个中断函数

‌‌‌‌  中断是嵌入式系统中重要的组成部分,这导致了很多编译开发商提供一种扩展——让标准C支持中断。具体代表事实是,产生了一个新的关键字__interrupt。下面的代码就使用了__interrupt关键字去定义一个中断服务子程序(ISR),请评论下面代码:

__interrupt double compute_area (double radius) 

 {  

	double area = PI * radius * radius;  
	
	printf(" Area = %f", area);  
	
	return area;  

}

评论
这段中断服务程序主要有以下四个问题:

  1. ISR不能返回一个值
  2. ISR不能传递参数
  3. 在ISR中做浮点运算是不明智的
  4. printf()经常有重入和性能上的问题

解释
‌‌‌‌  这段话是在指出代码中与中断服务程序(Interrupt Service Routine, ISR)相关的不正确用法和潜在问题。我们来看具体有哪些问题以及它们的影响。

问题 1:ISR不能返回一个值

‌‌‌‌  在中断服务程序中,ISR是不会返回值的。中断处理程序应该在处理完特定的任务后,通过恢复被中断的程序继续执行

示例:
void ISR_example() {
    // ISR logic here
    // ISR不能有return语句
}

问题 2:ISR不能传递参数

‌‌‌‌  通常情况下,ISR的原型由硬件和低级驱动程序定义,不允许传递参数。ISR应该是一个无参数函数。

示例:
void ISR_example() {
    // ISR logic here
}

问题 3:在ISR中做浮点运算是不明智的

‌‌‌‌  浮点运算通常会涉及到较长的处理时间,并且可能需要使用浮点寄存器。ISR应该尽可能短小和快速,避免使用浮点运算。

替代方案:

将需要的浮点运算委托到主程序中处理:

volatile double radius; // 使用全局或静态变量

void ISR_example() {
    // 只设置标志位,并保存必要的数据
    radius = ...; // 从硬件中读取或计算一个简单的值,避免复杂计算
    isr_completed_flag = 1; // 标志位
}

// 主程序中处理复杂计算
if (isr_completed_flag) {
    compute_area(radius);
    isr_completed_flag = 0;
}

问题 4:在ISR中使用printf函数是有问题的

‌‌‌‌  printf 函数不适合在ISR中使用,因为printf 可能不是线程安全的,并且可能会导致重入问题。另外,printf 需要大量的处理时间,影响系统响应速度。

替代方案:

‌‌‌‌  同样,将需要的输出操作放到主程序中:

volatile double area;
volatile int isr_completed_flag;

void ISR_example() {
    // ISR 逻辑
    radius = ...;
    isr_completed_flag = 1;
}

// 主程序
if (isr_completed_flag) {
    area = compute_area(radius);
    printf("Area = %f", area); // 在应用层进行输出
    isr_completed_flag = 0;
}

正确的ISR编写示例:

#include <stdio.h>

#define PI 3.141592653589793

volatile double radius;
volatile int isr_completed_flag = 0;

void ISR_example() {
    // 仅设置标志位,并保存必要的值
    radius = ...; // 从硬件或者中断源读取值
    isr_completed_flag = 1; // 标志位
}

// 主程序复杂计算
double compute_area(double rad) {
    return PI * rad * rad;
}

int main() {
    while (1) {
        if (isr_completed_flag) {
            double area = compute_area(radius);
            printf("Area = %f\n", area); // 在应用层进行输出
            isr_completed_flag = 0;
        }
        // 其他主程序逻辑
    }
    return 0;
}

总结

‌‌‌‌  这段代码指出的四个问题主要集中在ISR的设计和使用上。正确使用ISR对系统的实时性和性能有着至关重要的作用。ISR应该仅仅处理最关键的任务,并使用标志或队列机制将较为复杂的数据处理留给主程序完成,以确保系统的响应速度和稳定性。

‌‌‌‌  重入问题(Reentrancy Issue) 是指在程序运行过程中,一个函数在其执行尚未完成时,被再次调用,引发的一系列问题。这种情况通常在多线程编程或中断处理程序中经常出现。

重入问题的典型场景

  1. 多线程编程
    在多线程环境下,同一个函数可能会被多个线程并发调用。如果这个函数使用了全局变量、静态变量或者对共享资源进行了不安全的操作,就可能会导致数据竞争和不可预测的行为。

  2. 中断处理程序
    当一个ISR正在执行时,另一个中断信号到来,此时ISR可能会被再次调用。如果ISR中的代码没有足够的保护机制(例如互斥锁),这种重入问题会导致数据不一致和系统崩溃。

例子分析

假设有一个中断处理程序ISR正在执行printf函数,而这个函数尚未返回时,另一个中断触发,又进入了同一个ISR并再次调用了printf,这就会导致重入问题,造成输出混乱或系统崩溃。

#include <stdio.h>

volatile int isr_completed_flag = 0;

void ISR_example() {
    static int count = 0; // 使用静态变量作为例子
    printf("ISR execution count: %d\n", ++count); // printf是重入不安全的
    isr_completed_flag = 1;
}

int main() {
    while (1) {
        if (isr_completed_flag) {
            isr_completed_flag = 0;
            // 主程序执行
        }
        // 其他逻辑
    }
    return 0;
}

‌‌‌‌  在这个例子中,如果ISR在第一次调用printf时被中断,然后再次调用printf,两个printf调用会相互干扰,因为printf需要维护自己的一些内部状态(如缓冲区、指针等),而这些状态并没有被保护。

如何避免重入问题

  1. 避免在ISR中使用重入不安全的函数
    尽量避免在ISR中调用如printfmalloc/free等非线程安全和重入不安全的函数。

  2. 使用互斥锁
    在多线程编程中,用互斥锁(Mutex)来保护共享资源,但需要注意,互斥锁不能用在ISR中,因为ISR的执行不能被阻塞。

  3. 禁用中断
    在进入关键区时临时禁用中断,防止ISR被重入,但要慎用,因为长时间禁用中断会影响系统的实时性。

  4. 使用原子操作
    对于需要并发访问的数据,使用原子操作或更高级别的同步机制来保证数据的一致性。

示例:使用标志位解决重入问题

#include <stdio.h>

volatile int isr_started = 0;
volatile int isr_completed_flag = 0;

void ISR_example() {
    if (isr_started == 0) { // 检查是否已经有ISR在执行
        isr_started = 1;
        // 执行安全的操作
        isr_completed_flag = 1;
        isr_started = 0; // 表示ISR执行完毕
    } else {
        // 忽略此中断或将其计入某缓存队列
    }
}

int main() {
    while (1) {
        if (isr_completed_flag) {
            isr_completed_flag = 0;
            printf("ISR executed successfully\n");
        }
        // 其他逻辑
    }
    return 0;
}

‌‌‌‌  在这个示例中,我们使用了一个标志位(isr_started)来检查是否已经有ISR正在执行。这样可以避免同一个ISR被多次重入执行,从而防止重入问题。

总结

‌‌‌‌  重入问题是由于在函数执行过程中再次调用该函数引起的一系列问题,尤其在多线程编程和中断处理程序中容易出现。为了避免重入问题,需要采取适当的同步机制和编程规范,如避开重入不安全的函数、使用互斥锁和原子操作等。

152. 构造函数能否成为虚函数

‌‌‌‌  构造函数不能是虚函数。而且不能在构造函数中调用虚函数,因为那样实际执行的是父类的对应函数,因为自己还没有构造好。析构函数可以是虚函数,而且,在一个复杂类结构中,这往往是必须的。析构函数也可以是纯虚函数,但纯虚析构函数必须有定义体,因为析构函数的调用是在子类中隐含的

请记住
‌‌‌‌  虚函数的动态绑定特性是实现重载的关键技术,动态绑定根据实际的调用情况查询相应类的虚函数表,调用相应的虚函数。

153. 谈谈你对面向对象的认识

‌‌‌‌  面向对象可以理解为对待每一个问题,都是首先要确定这个问题由几个部分组成,而每个部分其实就是一个对象。然后再分别设计这些对象,最后得到整个程序。传统的程序设计多是基于功能的思想来进行考虑和设计的,而面向对象的程序设计则是基于对象的角度来考虑问题。这样做能够使程序更加的简洁清晰。

请记住
‌‌‌‌  编程中接触最多的“面向对象编程技术”仅仅是面向对象技术中的一个组成部分。发挥面向对象技术的优势是一个综合的技术问题,不仅需要面向对象的分析,设计和编程技术,而且需要借助必要的建模和开发工具。

154. 引用与指针有什么区别?

  1. 引用必须被初始化,指针不必
  2. 引用初始化后不能被改变,指针可以改变所指的对象
  3. 不存在指向空值的引用,但是存在指向空值的指针

155. 描述实时系统的基本特性

‌‌‌‌  在特定时间内完成特定的任务,实时性与可靠性

155. 全局变量和局部变量在内存中是否有区别?如果有,是什么区别?

‌‌‌‌  全局变量存储在静态存储区,局部变量在堆栈中。

156. 堆栈溢出一般是由什么原因导致的?

‌‌‌‌  没有回收垃圾资源。

解释
‌‌‌‌  堆栈溢出(Stack Overflow)是程序运行过程中常见的一种错误,通常由以下几个原因导致:

1. 递归深度过深

‌‌‌‌  当一个函数递归调用自身太多次时,会不断在堆栈中分配新的栈帧。每个函数调用都会在栈中保留一些信息(如局部变量、返回地址等),如果递归深度过深或没有正确的递归终止条件,最终会耗尽栈空间,导致堆栈溢出。

  • 示例
  void recursiveFunction() {
      // 没有基准条件,会一直递归下去
      recursiveFunction();
  }
  int main() {
      recursiveFunction();
      return 0;
  }

2. 无限循环的递归

‌‌‌‌  类似于递归深度过深的情况,如果递归函数没有合适的终止条件,或终止条件无法被满足,递归会一直进行下去,导致堆栈溢出。

  void faultyRecursiveFunction(int n) {
      // 错误的终止条件,导致递归无法终止
      if (n == 0) return;
      faultyRecursiveFunction(n - 1);
  }
  int main() {
      faultyRecursiveFunction(10);  // 当n是一个无法到达的值时,会堆栈溢出
      return 0;
  }

3. 巨量局部变量分配

‌‌‌‌  在函数中声明了大量的局部变量,特别是大数组或结构体,这些大数据结构都会占用大量的栈空间。如果总栈空间不足以容纳这些数据,也会导致堆栈溢出。

  void largeArrayFunction() {
      int largeArray[1000000];  // 大量的局部变量会耗尽栈空间
  }
  int main() {
      largeArrayFunction();
      return 0;
  }

4. 无意的无限递归

‌‌‌‌  有时由于程序逻辑错误或意外的条件,可能会导致意外的无限递归,导致堆栈溢出

  void unintendedRecursion() {
      // 逻辑错误导致不断递归
      unintendedRecursion();
  }
  int main() {
      unintendedRecursion();
      return 0;
  }

5. 极深的调用链

‌‌‌‌  即使不是递归函数,如果普通函数之间的调用深度非常深,例如在某些情况下会遇到非常复杂的嵌套调用,也可能导致堆栈溢出。

  void A() { B(); }
  void B() { C(); }
  void C() { D(); }
  void D() { E(); }
  // ...
  int main() {
      A();
      return 0;
  }

预防和解决方法

  1. 限制递归深度
    • 在设计递归算法时,确保设定合理的终止条件。
    • 可以使用尾递归优化(如果编译器支持),某些情况下能减少栈空间的使用。
  2. 局部变量放到堆中
    • 将大数组或大结构体的分配从栈上移到堆上,使用mallocnew进行动态分配。
   void safeFunction() {
       int *largeArray = new int[1000000];  // 使用堆分配
       // 使用 largeArray ...
       delete[] largeArray;
   }
  1. 迭代替换递归
    • 如果递归的深度可能非常深,可以尝试将递归过程转换为迭代过程。
  2. 优化程序结构
    • 避免不必要的深层次函数调用,通过重构代码提升效率。

‌‌‌‌  通过这些方法,可以有效地防止和解决堆栈溢出问题,提高程序的健壮性和稳定性。

157. 什么函数不能声明为虚函数?

‌‌‌‌  构造函数。

158. IP地址的编码分为哪两个部分?

‌‌‌‌  IP地址由两部分组成,网络号和主机号。

解释
‌‌‌‌  IP地址的编码通常分为两个主要部分:

  1. 网络部分(Network Portion)
  2. 主机部分(Host Portion)

网络部分(Network Portion)

‌‌‌‌  网络部分标识一个特定的网络。它表明IP地址属于哪一个子网或网络。路由器使用网络部分来决定数据包传输的路径,即如何把数据包从源网络传输到目标网络。网络部分的长度由子网掩码(Subnetwork Mask,简称子网掩码)决定。

示例

  • 对于一个IPv4地址192.168.1.10,假设子网掩码为255.255.255.0(通常表示为/24),那么网络部分为192.168.1

主机部分(Host Portion)

‌‌‌‌  主机部分标识网络中的特定设备(主机)。在同一个网络中的每个设备(如计算机、服务器、路由器等)都有一个唯一的主机部分。这个部分允许网络内部的设备彼此进行通信。

  • 对于同一个IPv4地址192.168.1.10,假设子网掩码为255.255.255.0,主机部分为最后一个字节10

子网掩码(Subnet Mask)

‌‌‌‌  子网掩码用于区分IP地址中的网络部分和主机部分。子网掩码是一个32位的数字,在表示时常使用点分十进制格式(例如255.255.255.0)。它有一系列的1(对应网络部分)后跟随一系列的0(对应主机部分)。

示例

  • 子网掩码255.255.255.0对应的二进制形式为:11111111.11111111.11111111.00000000

CIDR表示法

‌‌‌‌  IP地址和子网掩码有时会一起使用以便简洁表示。这种表示方法叫做CIDR(Classless Inter-Domain Routing)。在CIDR中,一个IP地址后面跟随一个斜杠和子网掩码中1的数量。

示例

  • IP地址192.168.1.10,子网掩码255.255.255.0,使用CIDR表示法为192.168.1.10/24

实际应用

  • 默认网关:默认网关是一个特殊的IP地址,用于将数据包从一个子网传递到另一个子网。它通常具有主机部分中的最小或最大地址。

  • 网络地址和广播地址:每个子网中有两个特殊的IP地址:

    • 网络地址:网络中所有主机的第一个地址,用于标识子网。它的主机部分全为0。
    • 广播地址:网络中所有主机的最后一个地址,用于向子网中的所有设备发送广播消息。它的主机部分全为1。

示例

  • 对于子网192.168.1.0/24
    • 网络地址192.168.1.0
    • 广播地址192.168.1.255
      通过理解IP地址的这两个部分和子网掩码,可以更好地进行IP规划和网络配置,确保网络通信的高效和可靠。

159. 不能做switch的参数的类型是?

‌‌‌‌  switch的参数不能是实类。

解释
‌‌‌‌  在C/C++编程语言中,switch语句是用于进行多分支选择的一种控制结构。switch语句只能用于某些特定类型的表达式。以下是不能用作switch语句条件的参数类型:

1. 浮点类型 (float, double, long double)

‌‌‌‌  浮点数由于表示精度和比较的复杂性,不能用作switch语句的参数。

  float f = 3.14;
  switch (f) {
      // 编译错误:`switch`表达式的类型无效
  }

2. 字符串类型

‌‌‌‌  字符串本质上是字符数组或指向字符数组的指针,在比较时涉及复杂的内存地址和内容比较,因此也不能用作switch语句的参数。

  std::string str = "hello";
  switch (str) {
      // 编译错误:`switch`表达式的类型无效
  }

3. 布尔类型 (bool)

‌‌‌‌  布尔类型通常用于简单的条件判断,尽管其值只有truefalse两个,但不能用作switch语句的参数。

  bool b = true;
  switch (b) {
      // 编译错误:`switch`表达式的类型无效
  }

4. 指针类型

‌‌‌‌  指针类型指向内存地址,比较指针涉及内存地址而不是值,因此不能用作switch语句的参数。

  int *ptr = nullptr;
  switch (ptr) {
      // 编译错误:`switch`表达式的类型无效
  }

5. 枚举外的自定义类型

‌‌‌‌  除了枚举(Enum)以外的自定义类型(如结构体、类、联合体)不能直接用于switch语句。

  struct MyStruct {
      int a;
  };

  MyStruct s = {1};
  switch (s) {
      // 编译错误:`switch`表达式的类型无效
  }

有效的switch参数类型

1. 整数类型 (int, char, short, long, long long)

‌‌‌‌  任何可以隐式转换为整数的类型都可以用作switch语句的参数。这也包括charunsigned charshortunsigned short等。

  int a = 2;
  switch (a) {
      case 1:
          // Some code
          break;
      case 2:
          // Some code
          break;
      default:
          // Some code
          break;
  }
2. 枚举类型

‌‌‌‌  枚举类型是可以用作switch语句参数的,因为枚举值实质上是整数。

  enum Colors { RED, GREEN, BLUE };
  Colors color = RED;
  switch (color) {
      case RED:
          // Some code
          break;
      case GREEN:
          // Some code
          break;
      case BLUE:
          // Some code
          break;
  }

总结

‌‌‌‌  switch语句的参数类型必须是可以转换为整数的类型,包括整数类型、枚举类型等,不能是浮点数、字符串、布尔、指针等其他复杂类型。这是因为switch语句对条件进行的是整数值比较,而非内容或地址的复杂比较。

160. 如何引用一个已经定义过的全局变量?

‌‌‌‌  答:可以引用头文件的方式,也可以用extern关键字。

  1. 如果用引用头文件方式来引用某个在头文件中声明的全局变量,假定你将那个变量写错了,那么在编译期间会报错;
  2. 如果你用extern方式引用时,假定你犯了同样的错误,那么在编译期间不会报错,而在连接期间报错。

解释
‌‌‌‌  在C和C++编程中,全局变量是指在所有函数之外声明的变量,这使得它们在文件的任何地方都可以访问。如果你需要引用一个已经定义的全局变量,有几种方法可以做到这一点,这取决于你是在同一个源文件中还是跨多个源文件中引用它。

同一个源文件中的全局变量引用

‌‌‌‌  在同一个源文件中引用全局变量非常简单。你只需直接使用它的名称。

#include <iostream>

int globalVariable = 42; // 全局变量定义

void printGlobalVariable() {
    std::cout << "Global Variable: " << globalVariable << std::endl; // 直接引用全局变量
}

int main() {
    printGlobalVariable(); // 输出:Global Variable: 42
    globalVariable = 100; // 修改全局变量
    printGlobalVariable(); // 输出:Global Variable: 100
    return 0;
}

跨源文件引用全局变量

‌‌‌‌  当全局变量跨多个源文件时,通常需要在头文件中声明该变量,并在一个源文件中定义它。然后,其他源文件可以通过extern关键字声明这个全局变量,来引用它。

示例:两个文件之间引用全局变量

File: globals.h (头文件)

‌‌‌‌  头文件中声明外部变量。

#ifndef GLOBALS_H
#define GLOBALS_H

extern int globalVariable; // 声明全局变量

#endif // GLOBALS_H
File: main.cpp (主文件)

‌‌‌‌  在主文件中定义全局变量,并引用它。

#include <iostream>
#include "globals.h"

int globalVariable = 42; // 定义全局变量

void printGlobalVariable();

int main() {
    printGlobalVariable(); // 调用外部函数引用全局变量
    globalVariable = 100; // 修改全局变量
    printGlobalVariable();
    return 0;
}
File: functions.cpp (另一个源文件)

‌‌‌‌  在另一个源文件中引用全局变量。

#include <iostream>
#include "globals.h"

void printGlobalVariable() {
    std::cout << "Global Variable: " << globalVariable << std::endl; // 引用全局变量
}
Compile and Link

‌‌‌‌  要编译并链接这些文件,你可以使用以下命令(假设你使用的是某个命令行编译器,如g++):

g++ main.cpp functions.cpp -o program
./program

注意事项

  1. 初始化和定义:
    • 全局变量的定义必须在一个源文件中,通常是main所在的源文件。
    • 不能在多个源文件中重复定义同一个全局变量,否则会产生链接错误。
  2. 作用域和生存期
    • 全局变量的作用域是从定义它的地方开始到整个程序结束。
    • 全局变量在程序执行期间始终存在(从程序启动到程序结束)。
  3. 命名冲突
    • 在使用全局变量时,需避免命名冲突。因此,通常使用命名规范或命名空间(在C++中)来防止冲突。

‌‌‌‌  通过这些步骤,你可以在单个源文件或多个源文件之间正确引用和使用全局变量。

‌‌‌‌  我们首先需要理解C/C++编译过程的两个重要阶段:编译(Compilation)和链接(Linking)。

编译和链接阶段

  1. 编译阶段(Compilation)
    • 每个源文件独立编译,生成目标文件(.o或.obj)。
    • 编译器检查语法和类型等问题,如果有错误,会在这个阶段报错。
  2. 链接阶段(Linking)
    • 链接器负责将多个目标文件和库文件链接在一起,生成最终的可执行文件。
    • 链接器解决函数和变量的引用和定义之间的对应关系,如果有未解析的符号,链接器会报错。

全局变量通过头文件声明和引用

‌‌‌‌  假设有一个全局变量在多个源文件中使用:

globals.h (头文件)
#ifndef GLOBALS_H
#define GLOBALS_H

extern int globalVariable; // 声明全局变量

#endif // GLOBALS_H
main.cpp (源文件,定义全局变量)
#include "globals.h"

int globalVariable = 42; // 定义全局变量

int main() {
    // 使用全局变量
    return 0;
}
functions.cpp (另一个源文件,引用全局变量)
#include "globals.h"

void useGlobalVariable() {
    // 使用全局变量
}

情况1:错误的关键字或类型(编译期间报错)

‌‌‌‌  
‌‌‌‌  如果在头文件中声明全局变量时,犯了类型错误或拼写错误,那么在包含此头文件的每个源文件编译时,编译器会发现这些错误并报错。

// globals.h
#ifndef GLOBALS_H
#define GLOBALS_H

extern int globaVariable; // 错误的声明

#endif // GLOBALS_H

// main.cpp
#include "globals.h"

int globalVariable = 42; // 定义

int main() {
    return 0;
}

// 编译错误:globaVariable在main.cpp中未声明

情况2:使用extern关键字,声明和定义不一致(链接期间报错)

‌‌‌‌  如果在使用extern关键字时,声明与定义不匹配,可能在编译时不会报错,因为编译器只检查了声明的语法,而未检查是否有真实的定义。但是,在链接时,链接器会发现有未解析的符号,因为定义和引用不匹配。

// globals.h
#ifndef GLOBALS_H
#define GLOBALS_H

extern int globaVariable; // 错误的声明

#endif // GLOBALS_H

// main.cpp
#include "globals.h"

int globalVariable = 42; // 正确的定义

int main() {
    return 0;
}

// 编译成功,但在链接期间报错:未定义引用globaVariable

总结

  • 编译时错误:在头文件中声明全局变量时,如果存在拼写或类型错误,编译器会在编译过程中报告这些错误,因为解析头文件时,编译器会检查语法和类型一致性。
  • 链接时错误:使用extern关键字在多个源文件之间引用全局变量时,如果声明和定义不匹配且声明错误,编译阶段不会发现错误(只要声明的语法正确),但在链接阶段,链接器会发现未解析的符号,从而报错。

正确的做法

  • 确保头文件中声明的全局变量名称和类型与定义完全一致。
  • 使用头文件的#ifndef #define #endif保护机制,确保文件只被包含一次,避免重复定义。
  • 避免在多个文件中定义同一个全局变量,应确保声明和定义分别在头文件和一个源文件中。

下面详细解释一下编译错误和链接错误之间的区别:

编译错误示例

globals.h (错误声明)
#ifndef GLOBALS_H
#define GLOBALS_H

extern int globaVariable; // 错误的声明,应该是globalVariable

#endif // GLOBALS_H
main.cpp (正确的定义)
#include "globals.h" // 包含错误声明的头文件

int globalVariable = 42; // 正确的定义

int main() {
    return 0;
}
functions.cpp (使用错误声明的变量)
#include "globals.h" // 包含错误声明的头文件

void useGlobalVariable() {
    int value = globaVariable; // 使用错误的声明
}

编译阶段

‌‌‌‌  在这个情况下,每个源文件独立编译,编译器会发现globaVariablemain.cpp中未定义,从而产生编译错误。

  • main.cpp 编译器会报错:globaVariable 未定义
  • functions.cpp 编译器也会报错:globaVariable 未定义

因为编译器在解析这些文件时会立即发现这些错误,从而在编译阶段就会报错,无法生成目标文件。

链接错误示例

globals.h (错误声明)
#ifndef GLOBALS_H
#define GLOBALS_H

extern int globaVariable; // 错误的声明,应该是globalVariable

#endif // GLOBALS_H
main.cpp (正确的定义)
#include "globals.h" // 包含错误声明的头文件

int globalVariable = 42; // 正确的定义,注意这里声明的是globalVariable

int main() {
    return 0;
}
functions.cpp (使用和声明不匹配的变量)
#include "globals.h" // 包含错误声明的头文件

void useGlobalVariable() {
    int value = globaVariable; // 使用错误的声明globaVariable,这与main.cpp中的定义不一致
}

编译阶段

‌‌‌‌  在编译阶段,编译器并不会检查变量的定义是否与其他源文件一致,它只会检查语法和类型。因此,这些文件都能通过编译。

  • main.cpp 编译会成功,生成目标文件(main.o)。
  • functions.cpp 编译会成功,生成目标文件(functions.o)。

链接阶段

‌‌‌‌  在链接阶段,链接器会把各个编译生成的目标文件结合起来,尝试解析所有符号(函数和变量)。由于main.o中定义的是globalVariable,而functions.o中引用的是globaVariable,链接器会发现globaVariable没有定义:

  • 链接器报错:undefined reference to 'globaVariable'

总结

‌‌‌‌  通过以上两个示例,我们可以清楚地看到,编译错误和链接错误在流程上的区别:

  1. 编译错误:当声明和定义在编译阶段有明显的语法或类型错误时,编译器会立即报错。

    • 错误示例:编译期会报错,因为globaVariablemain.cpp中未定义。
  2. 链接错误:当在声明和定义存在不匹配或引用未定义的变量、函数时,编译阶段通过,但在链接阶段发现未解析的符号,从而报错。

    • 错误示例:链接期会报错,因为globaVariable在任何地方都没有定义,而main.cpp中定义的是globalVariable

通过涵盖这些不同阶段的错误示例,我们能够更好地理解编译和链接过程中可能遇到的问题。

161. 对于一个频繁使用的短小函数,在C语言中应该用什么实现?在C++中应用什么实现?

‌‌‌‌  答:C用宏定义,C++用inline。

解释
‌‌‌‌  在C和C++中,对于频繁使用的短小函数,最佳实践是使用内联函数。这能提高程序的运行效率,主要是通过减少函数调用的开销。

在C语言中使用inline关键字

C99标准引入了inline关键字,允许你建议编译器将函数内联,从而减少函数调用开销。

#include <stdio.h>

// 内联函数定义
inline int add(int a, int b) {
    return a + b;
}

int main() {
    int result = add(2, 3); // 调用内联函数
    printf("Result: %d\n", result);
    return 0;
}

需要注意的是,虽然你可以建议编译器将函数内联,但最终决定权在编译器,特别是函数比较复杂或过大时,编译器可能会忽略这个建议。

在C++中使用inline关键字

‌‌‌‌  在C++中,inline关键字同样可用,并且通常用于在类定义中实现短小的成员函数。此外,C++还支持在类声明中定义内联函数,这使得代码更加简洁。

#include <iostream>

// 普通的内联函数
inline int add(int a, int b) {
    return a + b;
}

class MyClass {
public:
    // 在类声明中定义的内联成员函数
    inline int multiply(int a, int b) {
        return a * b;
    }
};

int main() {
    int result = add(2, 3); // 调用全局内联函数
    std::cout << "Addition Result: " << result << std::endl;

    MyClass obj;
    result = obj.multiply(4, 5); // 调用成员内联函数
    std::cout << "Multiplication Result: " << result << std::endl;

    return 0;
}

当心事项

  1. 代码膨胀
    虽然内联函数可以减少函数调用开销,但滥用会导致代码膨胀,因为编译器每次遇到内联函数调用时会插入函数体的副本。

  2. 调试复杂性
    内联函数可能会增加调试的复杂性,因为内联函数没有实际的函数调用,这会导致调用栈分析变得困难。

  3. 编译器的自由度
    inline关键字只是一个建议,编译器可能会根据其自身的优化策略选择是否真正内联这个函数。

编译器标志

‌‌‌‌  对于GCC或Clang编译器,可以使用编译器标志来进一步优化内联函数,例如-O3优化级别或更高级的优化标志,这通常能够使编译器内联更多的短小函数。

结论

  • 在C语言中,使用inline关键字来定义需要频繁调用的短小函数。
  • 在C++中,使用inline关键字,并在类定义中内联短小的成员函数以提高性能。

通过适当地使用内联函数,可以显著减少函数调用的开销,从而提高程序的运行效率。

162. C++是不是类型安全的?

‌‌‌‌  答:不是。两个不同类型的指针之间可以强制转换(reinterpret cast)。

解释

‌‌‌‌  C++不是严格意义上的类型安全语言。尽管它提供了一些类型安全的特性,比如强类型检查和类型转换,但仍然存在一些漏洞和特性,使其在某些情况下可能不够类型安全。

以下是一些使C++在某些情况下不够类型安全的特性:

  1. 类型转换:C++允许各种类型转换,包括隐式类型转换和显式类型转换(使用static_castreinterpret_castconst_castdynamic_cast等)。某些类型转换可能会导致类型不安全,比如reinterpret_cast

  2. 指针操作:C++允许直接操作指针,指针运算和类型转换可能会导致内存访问错误和未定义行为,从而破坏类型安全。

  3. 联合体:联合体允许多个数据成员共享同一块内存空间,在读取联合体的某个成员之前,程序员需要确保最近写入的是同一类型,否则可能会导致未定义行为。

  4. 原始数组:C++中的原始数组不进行边界检查,可能会导致缓冲区溢出和类型不安全。

‌‌‌‌  尽管如此,C++11及以后的标准引入了一些新特性,比如nullptrautodecltype、智能指针(如std::shared_ptrstd::unique_ptr)等,这些特性可以帮助提高代码的类型安全性和内存安全性。

‌‌‌‌  总的来说,C++提供了一些机制来支持类型安全,但由于其设计哲学和历史原因,它并不是一门严格的类型安全语言。类型安全在很大程度上依赖于程序员的自律和代码实践。

163. 当一个类A中没有声明任何成员变量与成员函数,这时sizeof(A)的值是多少?请解释一下编译器为什么没有让它为0?

答:1。
‌‌‌‌  举个反例,如果是0的话,声明一个class A[10]对象数组,而每一个对象占用的空间是0,这时候就没有办法区分A[0],A[1]…了。

解释

‌‌‌‌  在C++中,如果一个类A没有声明任何成员变量和成员函数,sizeof(A)的值通常是1。这是因为C++编译器需要确保每个实例对象有一个唯一的地址,以便在内存中进行区分

‌‌‌‌  具体原因如下:

  1. 地址唯一性:C++标准要求每个对象在内存中必须有一个唯一的地址。即使一个类没有任何数据成员,其对象也需要占据一些内存空间,以确保不同实例对象有不同的地址。如果sizeof(A)为0,那么所有实例对象的地址都相同,这就无法区分不同的对象。

  2. 数组索引:正如你提到的例子,如果sizeof(A)为0,那么声明一个包含10个对象的数组时,所有对象都会占据同一个地址,这样无法区分和访问不同的数组元素。例如,A[0]A[1]在内存中没有任何区别。通过将sizeof(A)设为1,可以确保数组中的每个元素都有不同的地址,方便索引和访问。

下面是一个具体的例子来演示这一点:

class A {};

int main() {
    A a1;
    A a2;

    std::cout << "Size of A: " << sizeof(A) << std::endl;
    std::cout << "Address of a1: " << &a1 << std::endl;
    std::cout << "Address of a2: " << &a2 << std::endl;

    A array[10];
    for (int i = 0; i < 10; ++i) {
        std::cout << "Address of array[" << i << "]: " << &array[i] << std::endl;
    }

    return 0;
}

输出示例(具体地址会有所不同):

Size of A: 1
Address of a1: 0x7ffee999d6d0
Address of a2: 0x7ffee999d6d1
Address of array[0]: 0x7ffee999d6d2
Address of array[1]: 0x7ffee999d6d3
Address of array[2]: 0x7ffee999d6d4
Address of array[3]: 0x7ffee999d6d5
Address of array[4]: 0x7ffee999d6d6
Address of array[5]: 0x7ffee999d6d7
Address of array[6]: 0x7ffee999d6d8
Address of array[7]: 0x7ffee999d6d9
Address of array[8]: 0x7ffee999d6da
Address of array[9]: 0x7ffee999d6db

‌‌‌‌  可以看到,尽管类A是一个空类,但它的实例对象在内存中依然有唯一的地址,而sizeof(A)为1确保了数组中每个元素的地址是不同的。

164. 简述数组和指针的区别?

‌‌‌‌  答:数组要么在静态存储区被创建(如全局数组),要么在栈上被创建。指针可以随时指向任意类型的内存块。

1. 修改内容上的区别

char a[] = “hello”;
a[0] = ‘X’;
char * p = “world”; //注意 p 指向常量字符串
p[0] = ‘X’; // 编译器不能发现该错误,运行时错误

2. 用运算符sizeof可以计算出数组的容量(字节数)。sizeof§,p是指针,得到的是一个指针变量的字节数,而不是p所指的内存容量。

解释

‌‌‌‌  数组和指针是C/C++编程中的两个非常重要的概念,尽管它们有很多相似之处,但它们本质上是不同的东西。以下是数组和指针的一些主要区别:

数组和指针的定义

数组(Array)

  • 数组是一个具有相同数据类型的元素的有序集合。在内存中,数组元素是连续存储的。
  • 数组的大小在声明时是固定的,不能在运行时动态改变。

指针(Pointer)

  • 指针是一个变量,用于存储另一个变量的内存地址。
  • 指针可以指向不同类型的数据,并且可以在运行时改变指向的地址。

区别点

1. 内存分配
  • 数组: 在编译时分配,大小固定。例如 int arr[5]; 分配5个 int 类型的连续内存空间。
  • 指针: 可以在运行时分配,大小可以动态变化。例如 int *ptr = (int*)malloc(5 * sizeof(int));分配5个 int 类型的连续内存空间。
2. 声明和初始化
  • 数组: 声明数组会自动分配指定大小的内存。例如 int arr[5];
  • 指针: 必须手动指定分配内存,初始化时通常需要一个已有的地址,例如 int *ptr = NULL;int *ptr = (int*)malloc(5 * sizeof(int));
3. 访问方式
  • 数组: 可以使用数组下标直接访问元素,例如 arr[2]
  • 指针: 可以使用指针算术操作和解引用访问元素,例如 *(ptr + 2) 或者 ptr[2]
4. 类型信息
  • 数组: 类型包含了数组的大小。例如 int arr[5]; 是包含 5 个 int 类型元素的数组。
  • 指针: 类型只包含指向的数据类型,不包含大小信息。例如 int *ptr 是指向 int 类型数据的指针,但不包含指向多少个 int 元素的信息。
5. 存储方式
  • 数组: 数组名代表数组的地址,但类型上也包含了数组长度的信息(仅在声明时)。
  • 指针: 指针变量独立存在,可以指向不同的数据地址,不包含长度信息。

示例代码

#include <stdio.h>
#include <stdlib.h>

int main() {
    // 数组示例
    int arr[5] = {1, 2, 3, 4, 5};
    printf("数组的第二个元素: %d\n", arr[1]);

    // 指针示例
    int *ptr;
    ptr = (int*)malloc(5 * sizeof(int));
    if (ptr == NULL) {
        printf("内存分配失败\n");
        return -1;
    }

    // 分配值
    for (int i = 0; i < 5; i++) {
        ptr[i] = i + 1;  // 等同于 *(ptr + i) = i + 1;
    }
    printf("指针的第二个元素: %d\n", ptr[1]);

    // 释放内存
    free(ptr);
    return 0;
}

总结

  • 数组是具有固定大小的连续内存块,声明时自动分配和初始化,可以用下标直接访问。
  • 指针是一个变量,存储其他变量的地址,可以动态分配和修改内存地址,通过指针算术和解引用来访问数据。

165. C++函数中值的传递方式

‌‌‌‌  值传递、指针传递、引用传递。

解释
‌‌‌‌  在C++中,函数参数的传递方式主要有三种:值传递Pass by Value)、指针传递Pass by Pointer)和引用传递Pass by Reference)。每种传递方式都有其各自的特点和适用场景。下面逐一进行介绍:

1. 值传递(Pass by Value)

特点:

  • 当函数参数使用值传递时,会在调用函数时将实际参数的值拷贝一份传递给函数。函数内部对参数的任何修改都不会影响到原变量。
  • 主要用于传递简单的数据类型,如基本数据类型(int、char、float等)。

示例代码:

#include <iostream>

void increment(int value) {
    value++;
    std::cout << "Inside function: " << value << std::endl;
}

int main() {
    int num = 10;
    increment(num);
    std::cout << "Outside function: " << num << std::endl;
    return 0;
}

输出:

Inside function: 11
Outside function: 10

‌‌‌‌  在这个例子中,increment函数接收到的是num的一个拷贝,因此对value的修改不会影响到num本身。

2. 指针传递(Pass by Pointer)

特点:

  • 当函数参数使用指针传递时,传递的是实际参数的地址。函数内部通过指针可以直接访问和修改实际参数的值。
  • 适用于需要在函数内部修改参数值的情况

示例代码:

#include <iostream>

void increment(int *value) {
    (*value)++;
    std::cout << "Inside function: " << *value << std::endl;
}

int main() {
    int num = 10;
    increment(&num);
    std::cout << "Outside function: " << num << std::endl;
    return 0;
}

输出:

Inside function: 11
Outside function: 11

‌‌‌‌  在这个例子中,increment函数接收到的是num的地址,因此通过解引用指针*value可以直接修改num的值。

3. 引用传递(Pass by Reference)

特点:

  • 当函数参数使用引用传递时,传递的是实际参数的引用(别名)。函数内部对参数的任何修改都会直接影响到原变量。
  • 比指针传递更安全,语法上也更简洁
  • 使用引用传递的参数在函数体内像普通变量一样使用,但实际上是对原始数据的引用。

示例代码:

#include <iostream>

void increment(int &value) {
    value++;
    std::cout << "Inside function: " << value << std::endl;
}

int main() {
    int num = 10;
    increment(num);
    std::cout << "Outside function: " << num << std::endl;
    return 0;
}

输出:

Inside function: 11
Outside function: 11

‌‌‌‌  在这个例子中,increment函数直接接收到num的引用,因此对value的任何修改都会反映到num上。

总结

  • 值传递: 函数接收到的是实际参数的一份拷贝,函数内部对参数的修改不会影响原变量。
  • 指针传递: 函数接收到的是实际参数的地址,函数内部可以通过指针直接修改原变量的值。
  • 引用传递: 函数接收到的是实际参数的引用,函数内部对参数的修改会直接影响原变量,用法上更简单且安全。

‌‌‌‌  这三种传递方式各有优劣,选择哪种方式取决于具体的应用场景和需求。如果需要在函数中修改实际参数的值,推荐使用指针传递或引用传递;而对于传递大对象时,引用传递通常比值传递和指针传递更高效。

166. 内存的分配方式有哪几种?

答:有三种。

  1. 静态存储区:是在程序编译时就已经分配好的,在整个运行期间都存在,如全局变量、常量。
  2. 栈上分配:函数内的局部变量就是从这分配的,但分配的内存容量有限。
  3. 堆上分配:也称动态分配,如我们使用new、malloc分配内存,用delete、free来释放内存。

解释

‌‌‌‌  在讨论内存分配方式时,通常提到的三种方式(静态存储区、栈上分配和堆上分配)主要是指程序在运行时分配和管理内存的机制,而代码区的内存分配则是在程序加载时已经确定的,并且不在程序运行时动态分配或释放。因此,代码区的分配方式一般不包含在这三种运行时内存分配方式的讨论中。具体原因如下:

  1. 静态存储区:静态存储区分配的是全局变量和静态变量的内存,这些变量在程序加载时分配,并在程序整个运行期间保持不变。静态存储区的分配和代码区一样,是在程序加载时确定的,但它主要用于存储数据,而不是代码。

  2. 栈上分配:栈上分配用于存储函数的局部变量和函数调用的上下文信息(如返回地址、参数等)。栈上的内存分配是动态的,在函数调用时分配,函数返回时释放。这是一种运行时的内存管理机制。

  3. 堆上分配:堆上分配用于动态分配内存,例如通过mallocnew等函数分配的内存。堆上的内存分配和释放是由程序员在运行时显式管理的,适用于需要动态管理生命周期的对象。

代码区的分配:代码区的内存分配是由操作系统在程序加载时完成的,并且是只读的。在程序运行期间,代码区的内容不会改变,也不会像栈或堆那样动态分配或释放内存。因此,代码区的分配属于程序加载时的静态分配,与运行时的内存分配机制有所不同。

综上所述,代码区的内存分配是在程序加载时完成的静态分配,与运行时的内存分配方式有所区别,因此通常不在讨论运行时内存分配方式时被提及。

‌‌‌‌  代码区的分配是在程序编译和链接的过程中完成的。具体来说,代码区(又称文本区)存储的是程序的可执行指令,这些指令在程序运行时是只读的。代码区的分配过程可以概括为以下几个步骤:

  1. 编译阶段:在源代码编译时,编译器将源代码翻译成机器代码,并生成目标文件(通常是.o或.obj文件)。这些目标文件中包含了程序的机器指令和一些元数据。

  2. 链接阶段:链接器将一个或多个目标文件与库文件链接,生成最终的可执行文件。在这个过程中,链接器将所有目标文件中的代码段合并,并安排它们在最终可执行文件中的位置。

  3. 加载阶段:当程序被加载到内存中运行时,操作系统的加载器将可执行文件中的代码段加载到内存中的一个专门区域,这个区域就是代码区。加载器负责将代码段映射到内存中的一个只读区域,以确保代码在运行时不会被修改。

总结起来,代码区的分配是在编译和链接过程中由编译器和链接器完成的,最终在程序加载到内存时由操作系统的加载器负责实际的内存分配和映射。

166. extern"C"有什么作用?

答:
‌‌‌‌  Extern"C"是由C++提供的一个连接交换指定符号,用于告诉C++这段代码是C函数。这是因为C++编译后库中函数名会变得很长,与C生成的不一致,造成C++不能直接调用C函数,加上extern"C"后,C++就能直接调用C函数了。

‌‌‌‌  Extern"C"主要使用正规DLL函数的引用和导出,在C++包含C函数或头文件的时候使用。使用时在前面加上extern "C"关键字即可。可以用一句话概括extern"C"这个声明的真实目的:实现C++与C以及其它语言的混合编程。

167. 用什么函数开启新线程、进程?

答:
‌‌‌‌  新线程:CreateThread / AfxBegin Thread 等。
‌‌‌‌  新进程:CreateProcess 等。

168. SendMessage 和 PostMessage 有什么区别?

答:
‌‌‌‌  SendMessage阻塞的,等消息被处理后,代码才能走到SendMessage的下一行。
‌‌‌‌  PostMessage非阻塞的,不管消息是否已经被处理,代码马上走到PostMessage下一行。

解释

‌‌‌‌  这段话描述了SendMessagePostMessage在消息处理机制中的不同行为,特别是在Windows编程中。这两者都是用来将消息发送到指定的窗口或线程,但它们在执行时的行为有所不同。

SendMessage

‌‌‌‌  SendMessage函数是同步的(阻塞的),这意味着它会将消息发送到目标窗口,并等待该消息被处理完毕之后,才返回并继续执行下一行代码。具体来说:

  1. SendMessage将消息发送到目标窗口的消息队列
  2. 目标窗口的窗口过程(Window Procedure)接收并处理该消息。
  3. 处理完消息之后,SendMessage返回结果。
  4. 代码继续执行SendMessage之后的代码行。

‌‌‌‌  因为SendMessage会等待消息被处理完毕,所以它是阻塞的。这意味着如果消息处理需要很长时间,调用SendMessage的线程会被阻塞,直到消息处理完成。

PostMessage

‌‌‌‌  PostMessage函数是异步的(非阻塞的),这意味着它将消息放入目标窗口的消息队列后,立即返回并继续执行下一行代码,而不等待消息被处理。具体来说:

  1. PostMessage将消息放入目标窗口的消息队列。
  2. PostMessage立即返回,不等待消息处理结果。
  3. 代码继续执行PostMessage之后的代码行。
  4. 目标窗口的消息队列会在合适的时间调度并处理该消息。

因为PostMessage不等待消息被处理,所以它是非阻塞的。这意味着调用PostMessage的线程可以继续执行,而不会因为消息处理的时间而被阻塞。

理解的关键点

  • 阻塞和非阻塞SendMessage是阻塞的,调用线程必须等待消息被处理完才会继续执行。PostMessage是非阻塞的,调用线程无需等待消息处理结果,可以立即继续执行。
  • 消息处理机制SendMessage要求同步处理消息,适合需要立即获得处理结果的场景。PostMessage则是异步处理消息,适合不需要立即获得处理结果,或者希望减少线程等待时间的场景。

示例代码

‌‌‌‌  下面是一个简化的示例代码,展示了SendMessagePostMessage的不同行为:

// SendMessage 示例
SendMessage(hWnd, WM_COMMAND, wParam, lParam);
// 只有在消息处理完成后,才会执行下一行代码
printf("SendMessage 后的代码");

// PostMessage 示例
PostMessage(hWnd, WM_COMMAND, wParam, lParam);
// 立即返回并执行下一行代码,而不等待消息处理
printf("PostMessage 后的代码");

‌‌‌‌  在SendMessage的情况下,只有当消息被处理完毕,才会打印"SendMessage 后的代码";而在PostMessage的情况下,消息一发送就立即打印"PostMessage 后的代码",不管消息是否被处理。

169. CMemoryState 的主要功能是什么?

‌‌‌‌  CMemoryState查看内存使用情况,解决内存泄露的问题

解释

‌‌‌‌  CMemoryState 是 MFC(Microsoft Foundation Class)库中的一个类,用于内存调试。它主要用于检测和报告内存泄漏。在调试版本的应用程序中,CMemoryState 可以保存内存的当前状态,并在需要时与另一状态进行比较,以检测内存的分配和释放情况是否一致。

CMemoryState 的主要功能

  1. 保存内存状态
    • 使用 CMemoryState::Checkpoint 方法保存当前的内存状态。
    • 通过保存多个内存状态,可以在不同的时间点记录内存使用情况。
  2. 比较内存状态
    • 使用 CMemoryState::Difference 方法比较两个内存状态。
    • 可以检测出两个检查点之间的内存泄漏情况。
  3. 报告内存泄漏
    • 使用 CMemoryState::DumpStatistics 方法报告内存状态,包括内存泄漏信息。
    • 可以输出内存泄漏的详细信息,帮助开发者定位和修复内存泄漏。

代码示例

以下是一个使用 CMemoryState 检测内存泄漏的简单示例:

#include <afx.h>
#include <iostream>

void TestMemoryLeak()
{
    // 创建一个内存块用于测试
    char* pMemory = new char[100];
    // 注意,这里故意没有删除内存,以测试内存泄漏
}

int main()
{
    // 检查点1:在调用函数之前保存内存状态
    CMemoryState oldState, newState, diffState;
    oldState.Checkpoint();

    // 调用可能产生内存泄漏的函数
    TestMemoryLeak();

    // 检查点2:在调用函数之后保存内存状态
    newState.Checkpoint();

    // 比较两个检查点之间的内存状态
    if (diffState.Difference(oldState, newState))
    {
        std::cout << "Memory leaks detected:" << std::endl;
        diffState.DumpStatistics();
    }
    else
    {
        std::cout << "No memory leaks detected." << std::endl;
    }

    return 0;
}

代码解释

  1. 初始化内存状态
    • CMemoryState oldState, newState, diffState; 创建三个 CMemoryState 对象,用于保存不同时间点的内存状态。
  2. 保存内存状态
    • oldState.Checkpoint(); 在调用 TestMemoryLeak 之前保存当前的内存状态。
    • newState.Checkpoint(); 在调用 TestMemoryLeak 之后保存当前的内存状态。
  3. 比较内存状态
    • diffState.Difference(oldState, newState); 比较两个检查点之间的内存状态。
    • 如果检测到内存泄漏,Difference 方法将返回 TRUE,并且 diffState 将包含泄漏的详细信息。
  4. 报告内存泄漏
    • diffState.DumpStatistics(); 输出内存泄漏的详细信息。

通过这种方式,CMemoryState 可以帮助开发者在调试阶段检测和修复内存泄漏,确保应用程序的内存管理更加健壮和高效。

170. #include <filename>#include "filname.h" 有什么区别?

‌‌‌‌  对于 #include <filename>编译器从标准库路径开始搜索 filename.h。
‌‌‌‌  对于 #include "filename.h编译器从用户的工作路径开始搜索filename.h。

解释
‌‌‌‌  主要是关于编译器搜索头文件路径的优先级和顺序

#include <filename>

  • 搜索路径:编译器首先从标准库的包含路径(例如,编译器安装目录下的标准头文件目录)中开始搜索 filename
  • 用途:通常用于包含标准库头文件或第三方库的头文件,这些文件通常位于系统或编译器指定的标准路径中。

#include "filename"

  • 搜索路径:编译器首先从当前的工作目录或源文件所在的目录开始搜索 filename
  • 用途:通常用于包含用户自定义的头文件,这些文件通常与源文件位于同一目录或项目的相对目录中。
  • 后备搜索路径:如果在当前工作目录或源文件所在的目录中找不到 filename,编译器将退而在标准库路径中继续搜索(类似于 #include <filename> 的路径)。

示例

‌‌‌‌  假设我们有以下文件结构:

project/
│
├── src/
│   ├── main.cpp
│   └── custom.h
└── include/
    └── library.h

main.cpp:

#include "custom.h"  // 编译器首先在 src/ 目录中搜索 custom.h
#include <library.h> // 编译器直接在 include/ 目录(标准路径之一)中搜索 library.h

#include "filename" 示例

  • src/main.cpp:
#include "custom.h"  // 将找到 src/custom.h

‌‌‌‌  编译器会首先在 src 目录中搜索 custom.h,因为它是相对路径。

#include <filename> 示例

  • src/main.cpp:
#include <library.h> // 将找到 include/library.h

‌‌‌‌  编译器会在标准库路径中搜索 library.h,通常配置中的 include 目录或编译器的默认头文件目录。

171. 处理器标识#error的目的是什么?

答:
‌‌‌‌  编译时输出一条错误信息,并中止继续编译

解释

‌‌‌‌  处理器指令 #error 在预处理阶段被用来生成编译错误信息,其主要目的是在特定条件不符合时中止编译过程,并提示开发者相关信息。具体来说,#error 指令的作用包括:

  1. 生成编译错误
    • 当预处理器遇到 #error 指令时,会立即生成一条错误消息,并将其作为编译错误输出。这条错误消息可以由开发者自定义,通常用来指示特定条件或预期行为没有被满足。
  2. 中止编译过程
    • 由于 #error 指令生成的错误是编译时错误,编译器在遇到这条错误消息时将会停止继续编译。这有助于避免在不符合预期条件时生成无效的可执行文件或库文件。

示例

‌‌‌‌  以下是一个简单的示例,演示了如何使用 #error 指令来检查预期条件:

#ifndef DEBUG_MODE
    #error "DEBUG_MODE is not defined. Please define DEBUG_MODE."
#endif

int main() {
    // 一些代码
    return 0;
}

‌‌‌‌  在这个示例中,如果 DEBUG_MODE 宏未被定义,预处理器会生成一条错误消息并停止编译,错误消息为 "DEBUG_MODE is not defined. Please define DEBUG_MODE."

应用场景

  • 条件编译中的验证:可以在代码中使用 #error 来确保特定的预处理条件已经被定义或者符合预期。
  • 特定平台或环境的验证:在跨平台开发中,可以使用 #error 来确保代码在目标平台上能够正常编译和运行。
  • 版本控制:在软件版本控制中,#error 可以用来确保所需的库或模块版本被正确包含或定义。

‌‌‌‌  总之,#error 指令是预处理阶段中的一种工具,用来在编译前验证特定条件的合法性,一旦条件不符合,即可生成错误消息并中止编译过程,有助于提前发现和解决潜在的编译时问题。

172. #if!defined(AFX_…_HADE_H) #define(AFX_…_HADE_H) …… #endif的作用?

‌‌‌‌  防止该头文件被重复引用

解释

#if !defined(AFX_..._HADE_H)
#define AFX_..._HADE_H

// 头文件的具体内容在这里

#endif
#if !defined(AFX_..._HADE_H)
  • #if !defined(...) 是预处理器的条件编译指令。
  • defined(...) 是一个预处理器操作符,用于检查某个宏是否已经被定义。

‌‌‌‌  在这里,AFX_..._HADE_H 是一个自定义的宏名(通常是头文件名称的标识符),通过 #if !defined(...)检查该宏是否未定义。如果 AFX_..._HADE_H 尚未被定义,则条件成立,预处理器会执行下面的代码块,否则会跳过。

#define AFX_..._HADE_H
  • 如果 AFX_..._HADE_H 未定义,预处理器会执行 #define AFX_..._HADE_H 指令来定义这个宏。
  • 这样做的目的是为了标记当前头文件已经被引用,避免再次被重复引用。
头文件的具体内容
  • #define AFX_..._HADE_H#endif 之间,是头文件的实际内容。这些代码会在第一次引用时被预处理器包含到源文件中。
#endif
  • #endif 表示条件编译的结束。它和 #if 配对,用来结束条件编译的代码块。
  • 在这里,它结束了 #if !defined(AFX_..._HADE_H) 条件成立时执行的代码块。

作用和理解

  • 防止重复引用:通过上述方式,只有在第一次引用头文件时,AFX_..._HADE_H 宏才会被定义。在后续的引用中,由于该宏已经被定义,预处理器会跳过 #if !defined(...) 的条件分支,从而避免重复包含头文件的内容,提高编译效率并避免编译错误。

  • 唯一标识符AFX_..._HADE_H 是一个唯一的标识符,通常会包含头文件名称的一部分,以确保在整个项目中的唯一性。

  • 常见应用:这种技术被广泛用于C和C++的头文件中,特别是在大型项目中,用来管理复杂的依赖关系和避免重复定义问题。

‌‌‌‌  综上所述,这种使用方式确保了头文件只被编译一次,同时通过宏定义来标记头文件的引用状态,是 C 和 C++ 中常见且重要的编程实践之一。

173. 在定义一个宏的时候应该注意什么?

‌‌‌‌  定义部分的每个形参和整个表达式都必须用括号括起来,以避免不可预料的错误发生。

解释

  1. 使用括号括起每个形参和整个表达式
    • 每个宏形参和整个宏表达式都应该用括号括起来,这可以防止由于操作符优先级导致的意外行为。例如:
      #define SQUARE(x) ((x) * (x))
      这样定义可以确保在展开宏时,参数 x 的值不受周围环境的影响,同时也避免了乘法操作的优先级问题。
  2. 注意宏展开的副作用
    • 宏展开是简单的文本替换,可能会导致意料之外的副作用,尤其是对于带有副作用的表达式(例如 ++--、函数调用等)。确保宏的定义不会影响代码的可读性和功能的预期行为。
  3. 选择符合语义的命名
    • 宏的命名应该有意义,并符合语言规范。避免使用已有的标准库函数或变量名,以及避免与其他宏冲突。
  4. 考虑可读性和维护性
    • 宏的内容应该简洁明了,易于理解和维护。过于复杂或者不必要的宏定义可能会降低代码的可读性和可维护性。
  5. 避免过度使用宏
    • 宏是一种强大的工具,但过度使用宏可能会导致代码难以调试和理解。在合适的情况下,应优先考虑使用函数或者内联函数代替宏。
  6. 宏定义后不带分号
    • 宏定义通常不应该以分号结尾,因为在展开时会导致意外的语法错误。

示例

‌‌‌‌  考虑下面这个简单的宏定义的例子:

#define MAX(a, b) ((a) > (b) ? (a) : (b))

‌‌‌‌  这个宏定义了一个获取两个数中较大值的宏。注意到它:

  • 每个形参和整个表达式都用括号括起来,避免了因操作符优先级引起的问题。
  • 定义简洁明了,功能清晰。

‌‌‌‌  通过这些注意事项,可以有效地定义宏,避免潜在的问题,并使代码更加健壮和可维护。

174. 数组在做函数实参的时候会转变为什么类型?

‌‌‌‌  数组在做函数实参的时候会变成指针类型

175. 系统会自动打开和关闭的三个标准头文件是什么?

  1. 标准输入——键盘——stdin
  2. 标准输出——显示器——stdout
  3. 标准出错显示——显示器——stderr

176. 在Win32下,char、int、float、double各占多少位?

  1. char 占 8 位
  2. int 占 32 位
  3. float 占 32 位
  4. double 占 64 位

解释

‌‌‌‌  在 Win32 下,通常的数据类型 charintfloatdouble 各占用的位数如下所示:

  1. char:
    • 占用位数:8位
    • 字节大小:1字节(8位)
    • 取值范围:-128 到 127 (有符号)或 0 到 255 (无符号)
  2. int:
    • 占用位数:32位
    • 字节大小:4字节(32位)
    • 取值范围:-2,147,483,648 到 2,147,483,647 (有符号)或 0 到 4,294,967,295 (无符号)
  3. float:
    • 占用位数:32位
    • 字节大小:4字节(32位)
    • 单精度浮点数,通常表示 IEEE 754 标准的单精度浮点数格式,范围和精度有限,但通常足够大多数应用。
  4. double:
    • 占用位数:64位
    • 字节大小:8字节(64位)
    • 双精度浮点数,通常表示 IEEE 754 标准的双精度浮点数格式,提供更大的范围和更高的精度。

注意事项

  • 这些数据类型的大小和取值范围在不同的编译器和系统环境下可能会有所不同,但在大多数现代的 Win32 系统中,上述描述是普遍适用的。
  • 数据类型的确切大小和范围可以使用 sizeof 运算符来确定,例如 sizeof(char)sizeof(int)sizeof(float)sizeof(double)
  • 在不同的操作系统、编译器以及特定的编译器标志(如编译为32位或64位应用程序)下,这些数据类型的大小可能会有所不同,因此在具体开发中,建议通过查阅编译器和操作系统的文档来确认具体的数据类型大小和范围。

177. strcpy() 和 memcpy()的区别?

‌‌‌‌  两者都可以用来拷贝字符串,strcpy()拷贝以’\0’结束,但memcpy必须指定拷贝的长度

解释
‌‌‌‌  strcpy()memcpy() 都是 C 语言中的标准库函数,用于复制数据,但它们的用途和行为有明显的区别。理解这两个函数的不同在于它们的应用场景和数据处理方式。下面我们详细讨论它们的区别。

strcpy()

strcpy() 函数专门用于复制以空字符('\0')结尾的字符串。

特点:
  • 目的:用于复制由 '\0' 结尾的字符串。
  • 终止条件:在复制过程中,它会一直复制字符,直到遇到一个 '\0' 字符,然后再把这个 '\0' 字符也复制到目标位置。
  • 目标与源的类型:目标和源需要都是 char 类型的字符串。
示例:
#include <cstring>

int main() {
    const char* src = "Hello, World!";
    char dest[20];  // 确保目标数组足够大
    strcpy(dest, src);  // 复制 src 到 dest
    printf("%s\n", dest);  // 输出: Hello, World!
    return 0;
}

在这个例子中,strcpy"Hello, World!" 的内容复制到 dest 数组中,并确保 dest'\0' 结尾。

memcpy()

memcpy() 函数是通用的内存复制函数,不考虑数据内容。

特点:
  • 目的:用于复制任意类型的数据。
  • 终止条件:根据传递的字节数进行复制,而不是通过检测 '\0'
  • 目标与源的类型:可以是任何类型的内存块,只要这两个内存块大小不重叠。
示例:
#include <cstring>

int main() {
    int src[5] = {1, 2, 3, 4, 5};
    int dest[5];
    memcpy(dest, src, 5 * sizeof(int));  // 复制 src 到 dest,每个元素占 sizeof(int) 个字节
    for(int i = 0; i < 5; ++i) {
        printf("%d ", dest[i]);  // 输出: 1 2 3 4 5
    }
    return 0;
}

‌‌‌‌  在这个例子中,memcpysrc 数组的内容逐字节复制到 dest 数组中。

主要区别总结:

  1. 类型与语义

    • strcpy 专门用于复制以 '\0' 结尾的字符串,目标和源都是 char 数组。
    • memcpy 用于复制任意类型的内存块,不关心数据类型和内容。
  2. 复制方式

    • strcpy 依赖于源字符串中的 '\0' 终止符,停止复制并复制终止符。
    • memcpy 根据指定的字节数复制,不会自动添加终止符。
  3. 应用场景

    • 使用 strcpy 复制 C 字符串时无需显式指定长度,只要确保目标数组有足够空间保存源字符串以及终止符。
    • 使用 memcpy 复制任意数据类型,需要显式地指定复制的字节数。

形象化理解的例子:

  • strcpy 复制:就像你在复制一段文字,只有在你遇到句号(终止符)时你才知道复制结束。
  • memcpy 复制:就像你在搬家,你要搬 10 个箱子,你只关心数量,不关心箱子里面是什么。

注意事项:

  • 越界问题:使用 strcpy 时如果目标数组空间不足以容纳源字符串,可能会导致越界错误,引发未定义行为。
  • 重叠区域memcpy 不适用于内存重叠区域的复制,如果源和目标内存区域重叠,应使用 memmove

178. 说明define和const在语法和含义上有什么不同?

  1. # define 是C语言中定义符号常量的方法,符号常量只是用来表达一个值,在编译阶段符号就被值替换了,他没有类型
  2. const 是C++语法中定义常变量的方法,常变量具有变量特性,它具有类型内存中存在以它命名的存储单元,可以用sizeof测出长度

179. 说出字符常量和字符串常量的区别,并使用sizeof计算有什么不同?

‌‌‌‌  字符常量是指单个字符,字符串常量以’\0’结束,使用运算符sizeof计算多占一字节的存储空间

解释

字符常量

  • 字符常量是单个字符,用单引号 ' ' 括起来,例如 'a''1''!'
  • 存储空间:字符常量通常占用一个字节(8位),但是也可能因为编码格式原因占用更多的空间(例如UTF-8编码中的某些字符可能占用多个字节)。

字符串常量

  • 字符串常量是由双引号 " " 括起来的一些字符,例如 "hello", "world", "abc123!"
  • 字符串终止符:字符串常量以 '\0'(空字符)结束,这个空字符也称为字符串的终止符。

内存占用和 sizeof 的用法

#include <iostream> 
using namespace std; 
int main() { 
	char c = 'a'; 
	char str[] = "hello"; 
	cout << "Size of character constant 'a': " << sizeof(c) << " byte(s)" << endl; c
	out << "Size of string constant \"hello\": " << sizeof(str) << " byte(s)" << endl; 
	return 0; 
}
  1. 字符常量的大小
    • sizeof(c) 返回字符变量 c 的大小。
    • 输出通常是 1,因为字符常量在大多数系统中占用一个字节。
  2. 字符串常量的大小
    • sizeof(str) 返回字符数组 str 的大小。
    • 这里 str 包含 "hello",但是要注意它的实际存储方式,包含一个终止符 '\0'
    • sizeof(str) 的结果是 6,因为 "hello" 是5个字符,加上一个终止符 '\0',总共6个字符。

180. 简述全局变量的优缺点

‌‌‌‌  全局变量也称为外部变量,他是在函数外部定义的变量,它属于一个源程序文件,他保存上次被修改后的值,便于数据共享,但不方便管理,容易引起意想不到的错误。

解释
‌‌‌‌  全局变量是指在程序中声明的具有全局作用域的变量,即其值可以被整个程序中的任何函数访问和修改。全局变量虽然有其特定的应用场景,但也有明显的优缺点。以下是对全局变量的优缺点的简述:

优点

  1. 方便数据共享
    全局变量可以在整个程序中共享使用,无需通过参数传递即可实现不同函数之间的数据共享。对于需要全局配置或状态信息的情况,全局变量是较为便捷的选择。

  2. 减少参数传递
    当函数之间需要频繁传递相同的数据时,使用全局变量可以减少函数参数的传递,提高代码的可读性和简洁性。

  3. 数据持久性
    全局变量在程序生命周期内始终存在并保持其值(除非显式改变),这适用于需要在多个函数调用中维持状态的数据。

缺点

  1. 命名空间污染
    全局变量存在于全局命名空间,可能会与其他模块或第三方库的全局变量发生命名冲突,增加维护难度。为了避免冲突,通常需要对全局变量进行命名空间管理。

  2. 隐藏依赖关系
    函数依赖于全局变量,这种依赖关系是隐式的,不容易从函数接口看出。使得代码难以理解和维护,特别是在代码规模较大时。

  3. 可维护性差
    全局变量的修改可能在程序的任何地方发生,导致难以跟踪和调试问题。尤其是在团队开发时,不恰当地修改全局变量可能会引入难以察觉的bug。

  4. 难以重用
    带有全局变量依赖的代码难以重用,因为它们之间存在隐式的耦合。函数的独立性降低,在非全局环境下重用变得困难。

  5. 线程安全问题
    在多线程程序中,全局变量的使用需要特别小心。多个线程同时访问和修改全局变量,会导致数据竞争和不一致性,必须引入同步机制(如互斥锁)来确保线程安全,这增加了复杂性。

总结

‌‌‌‌  虽然全局变量在特定情况下可以简化代码和方便数据共享,但在实际开发中,需要谨慎使用全局变量,尽量保持代码模块化和数据隔离。推荐使用局部变量、参数传递和返回值等方式来减少全局变量的使用,提升代码的可维护性和可读性。如果必须使用全局变量,应当对其进行命名空间管理,并在文档中详细说明其用途和修改记录。

181. 总结static的应用和作用

  1. 函数体内static变量的作用范围为该函数体,不同于auto变量,该变量的内存只被分配一次,因此其值在下次调用时仍维持上次的值。
  2. 在模块内的static全局变量可以被模块内所用函数访问,但不能被模块外其他函数访问。
  3. 在模块内的static函数只可被这一模块的其他函数调用,这个函数的适用范围被限制在声明它的模块内。
  4. 在类中的static成员变量属于整个类所拥有,对类的对象只有一份拷贝
  5. 在类中的static成员函数属于整个类所拥有,这个函数不接收this指针,因而只能访问类的static成员变量。

182. 总结const的应用和作用

  1. 阻止一个变量被改变,可以使用const关键字。在定义const变量时,通常需要对它初始化,因为以后就没有机会去改变他了。
  2. 对指针来说,可以指定指针本身为const,也可以指定指针所指的数据为const,或二者同时指定为const。
  3. 在一个函数声明中,const可以修饰形参,表明他是一个输入参数,在函数内部不能改变其值
  4. 对于类的成员函数,若指定其为const类型,则表明其是一个常函数,不能修改类的成员变量
  5. 对于类的成员函数,有时候必须指定其返回值为const类型,以使得其返回值不为“左值”。

183. 什么是指针?谈谈你对指针的理解。

  1. 指针是一个变量,该变量专门存放内存地址。
  2. 指针变量的类型取决于其指向的数据类型,在所指数据类型前加*。
  3. 指针变量的特点是他可以访问所指向的内存

184. 什么是常指针?什么是指向常变量的指针?

‌‌‌‌  常指针的含义是该指针所指向的地址不能改变,但该地址所指向的内容可以变化,使用常指针可以保证我们的指针不能指向其他变量。

‌‌‌‌  指向常变量的指针是指该指针的变量本身的地址可以发生变化,可以指向其他的变量,但是他所指的内容不可以被修改

185. 函数指针和指针函数的区别?

‌‌‌‌  函数指针指向一个函数入口的指针
‌‌‌‌  指针函数函数的返回值是一个指针类型

186. 简述Debug版本和Release版本的区别?

‌‌‌‌  Debug版本是调试版本Release版本是发布给用户的最终非调试版本

187. 指针的几种典型应用情况

  1. int *p[n] —— 指针数组,每个元素均为指向整形数据的指针。
  2. int (*)p[n]——p为指向一维数组的指针,这个一维数组有n个整型数据。
  3. int *p()——函数带回指针,指针指向返回的值。
  4. int (*)p()——p为指向函数的指针。

188. static函数与普通函数有什么区别?

‌‌‌‌  static函数在内存中只有一份普通函数在每个被调用中维持一份拷贝

189. struct(结构)和union(联合)的区别?

  1. struct union 都是由多个不同的数据类型的成员组成但在任何同一时刻联合中只存放了一个被选中的成员所有成员共用一块地址空间),而结构的所有成员都存在(不同成员的存放地址不同)
  2. 对于 union 的不同成员赋值,将会对其他成员重写,原来成员的值就不存在了,而对于结构的不同成员赋值时互不影响的。

解释

结构体 (struct)

  1. 存储:结构体中的所有成员都有自己的独立存储空间。每个成员的地址相对于结构体变量的基地址是固定的,并且所有成员可以同时存在。
  2. 成员访问:访问结构体的不同成员之间互不影响。例如,如果你向一个成员赋值,这个操作不会影响其他成员的值。
#include <stdio.h>

struct ExampleStruct {
    int integer;
    float decimal;
    char character;
};

int main() {
    struct ExampleStruct s;
    s.integer = 10;
    s.decimal = 3.14;
    s.character = 'A';

    printf("integer: %d\n", s.integer);       // 输出: integer: 10
    printf("decimal: %f\n", s.decimal);       // 输出: decimal: 3.140000
    printf("character: %c\n", s.character);   // 输出: character: A

    return 0;
}

‌‌‌‌  在这个例子中,ExampleStruct 结构体中的三个成员 integerdecimalcharacter 拥有独立的存储空间,互不影响。

联合体 (union)

  1. 存储:联合体的所有成员共享同一块存储空间。这意味着在任何给定时间,联合体只能存储其中一个成员的值。联合体的大小是它最大成员的大小
  2. 成员访问:当你给联合体的一个成员赋值时,这个值会影响其他成员,因为所有成员共享同一块存储空间。仅最后赋的值会被保存。
#include <stdio.h>

union ExampleUnion {
    int integer;
    float decimal;
    char character;
};

int main() {
    union ExampleUnion u;
    u.integer = 10;
    printf("integer: %d\n", u.integer);       // 输出: integer: 10
    
    u.decimal = 3.14;
    printf("decimal: %f\n", u.decimal);       // 输出: decimal: 3.140000
    printf("integer: %d\n", u.integer);       // 可能会输出一个不可预测的值,因为它已经被覆盖
    printf("character: %c\n", u.character);   // 可能会输出随机字符,因为它已经被覆盖

    u.character = 'A';
    printf("character: %c\n", u.character);   // 输出: character: A
    printf("decimal: %f\n", u.decimal);       // 可能会输出一个不可预测的值,因为它已经被覆盖

    return 0;
}

‌‌‌‌  在这个例子中,ExampleUnion 联合体中的三个成员 integerdecimalcharacter 共享同一个存储空间。当你改变其中一个成员的值时,其他成员的值会被覆盖且不可预测。

关键区别总结:

  1. 存储空间

    • struct 中每个成员都有自己独立的存储空间,因此可以同时存在。
    • union 中所有成员共享同一个存储空间,因此在任何一个时间点只能存储一个成员的值。
  2. 成员间的关系

    • struct 中,修改一个成员的值不会影响其他成员。
    • union 中,修改一个成员的值会影响其他成员,因为所有成员存储在同一块内存中。
  3. 用途

    • struct 通常用于需要同时保存多个不同类型的数据的场景。
    • union 通常用于需要在不同时间存储不同类型但不需要同时保存不同类型数据的场景,例如保存一个值的不同视图或表示方式。

扩展理解:

  • 大小

    • 一个 struct 的大小是所有成员大小之和,可能还包括内存对齐导致的间隙。
    • 一个 union 的大小是其最大成员的大小,因为所有成员共用一块内存。
  • 内存对齐

    • 结构体成员是按照定义顺序排列的,通常需要考虑内存对齐的问题以提高访问效率。
    • 联合体的所有成员从同一内存地址开始,不存在内存对齐间隙的问题。

190. class 和 struct 的区别?

‌‌‌‌  struct 的成员默认是公有的,而类的成员默认是私有的

191. 简述枚举类型

‌‌‌‌  枚举方便一次定义一组常量,使用起来很方便。

解释

简述

‌‌‌‌  枚举类型,是编程语言(如C、C++、Java, Python等)中的一种数据类型,允许开发者定义一组命名的整数常量。这些常量被称为枚举成员,每个枚举成员隐式地对应一个整数值。

解释

  • 定义固定值:枚举方便一次定义一组相关的常量,类似于有意义的标签,便于代码的阅读和维护。
  • 提高代码可读性:使用枚举可以避免魔法数(magic numbers),这些常量在代码中有清晰的名称,增强了可读性和可维护性。
  • 便于管理:集中管理这些常量,增加新值或修改现有值时,只需在枚举定义中进行修改,减少了疏漏和出错的机会。

示例

C 语言
#include <stdio.h>

// 定义一个枚举类型
enum Day {
    SUNDAY,
    MONDAY,
    TUESDAY,
    WEDNESDAY,
    THURSDAY,
    FRIDAY,
    SATURDAY
};

int main() {
    enum Day today = WEDNESDAY;
    printf("Today is: %d\n", today);  // 输出: Today is: 3
    return 0;
}

‌‌‌‌  在这个例子中, Day 枚举定义了一个一周的天数,每个枚举成员都隐式地被赋值为一个整数,从0开始(SUNDAY是0,MONDAY是1,依次类推)。

Java 语言
public class EnumExample {
    // 定义一个枚举类型
    enum Day {
        SUNDAY,
        MONDAY,
        TUESDAY,
        WEDNESDAY,
        THURSDAY,
        FRIDAY,
        SATURDAY
    }

    public static void main(String[] args) {
        Day today = Day.WEDNESDAY;
        System.out.println("Today is: " + today);  // 输出: Today is: WEDNESDAY
    }
}

‌‌‌‌  Java 的枚举更加强大,它不仅仅是整数常量,还可以定义字段、方法等,像类一样使用。

补充

  1. 默认值与自定义值
    • 在C语言中,枚举成员从0开始递增,你也可以手动为枚举成员指定值。例如:
     enum Day {
         SUNDAY = 1,
         MONDAY,
         TUESDAY = 5,
         WEDNESDAY,
         THURSDAY,
         FRIDAY = 10,
         SATURDAY
     };
     // MONDAY = 2, WEDNESDAY = 6, THURSDAY = 7, SATURDAY = 11
  • 在这种情况下,后续未指定值的成员将按之前的值递增。
  1. 作用域与类型安全

    • 在 C 语言中,枚举成员在编译时期被替换为整数值,具有相同作用域但不严格的类型检查。
    • 在 Java 中,枚举是一个真正的类类型,具有类型安全和严格的作用域管理。
  2. 扩展功能

    • 高级语言中的枚举(如 Java 和 C#)可以包含字段、方法,可以实现接口。
    • 可以为枚举添加构造函数、属性以及方法,甚至使用枚举实现一些设计模式,如单例模式。
  3. 应用场景

    • 枚举常用于定义一组相关但有限的常量值集,如一周的天数、月份、状态码、颜色等等。
    • 在状态机实现、资源标识(如HTTP状态码)等需要预定义一组固定值的地方,枚举是一种非常方便的选择。

总结

‌‌‌‌  枚举类型是一种便捷的方式来定义常量集合,便于后续代码的使用、维护和阅读。通过使用枚举,能有效避免魔法数,提高程序的可读性和可靠性,并在一定程度上帮助实现类型安全。

‌‌‌‌  这里 , “魔法数”(magic number)在编程中指的是直接在代码中使用的具体数字常量,这些数字通常没有明显的意义或解释。使用魔法数会降低代码的可读性和可维护性,因为它们不直观,不能清晰地表达其在程序中的作用或含义。

魔法数的例子

假设有一段代码如下:

int calculateBonus(int level) {
    if (level == 1) {
        return 500;
    } else if (level == 2) {
        return 1000;
    } else if (level == 3) {
        return 1500;
    }
    return 0;
}

‌‌‌‌  在这段代码中,12350010001500 都是魔法数。它们的含义不清晰,读者无法通过代码直接了解这些数字的具体意义。

使用枚举和常量来避免魔法数

‌‌‌‌  通过使用枚举和常量,可以提高代码的可读性和可维护性。重写上述代码,使用枚举类型和常量:

enum Level {
    BEGINNER = 1,
    INTERMEDIATE = 2,
    ADVANCED = 3
};

const int BEGINNER_BONUS = 500;
const int INTERMEDIATE_BONUS = 1000;
const int ADVANCED_BONUS = 1500;

int calculateBonus(Level level) {
    switch(level) {
        case BEGINNER:
            return BEGINNER_BONUS;
        case INTERMEDIATE:
            return INTERMEDIATE_BONUS;
        case ADVANCED:
            return ADVANCED_BONUS;
        default:
            return 0;
    }
}

好处

  1. 可读性

    • 通过使用有意义的枚举值和常量名,代码更加直观,读者能够轻松理解每个值的含义。
  2. 可维护性

    • 如果需要修改某个值,只需更改定义处即可,减少了出错的可能性。
  3. 类型安全

    • 枚举类型提供了一定程度的类型检查,减少了将无效值传递给函数的风险。
  4. 避免重复

    • 避免了在多个地方重复使用相同的魔法数,使代码更干净、更一致。

总结

‌‌‌‌  魔法数是直接在代码中使用的具体数字常量,通常没有明显的解释或含义。通过使用枚举类型和常量,可以有效避免魔法数,提高代码的可读性、可维护性和类型安全性。这是一种良好的编程实践,能够使代码更加清晰、易懂和易于管理。

192. assert()的作用?

‌‌‌‌  assert() 是一个调试程序时经常使用的宏在程序运行时他计算括号内的表达式,如果表达式为False(0),程序将报告错误,并终止执行。如果表达式不为0,则继续执行后面的语句。

‌‌‌‌  这个宏通常用来判断程序中是否出现了明显的非法数据,如果出现了终止程序以免导致更严重的后果,同时也便于查找错误。

193. 局部变量和全局变量是否可以同名?

‌‌‌‌  可以。局部变量会屏蔽全局变量。要用全局变量,需要使用“::”(域运算符)。

解释

‌‌‌‌  在C++中,局部变量和全局变量可以同名。但是,当它们同名时,局部变量会屏蔽(遮蔽)全局变量。这意味着在局部变量的作用域内,使用该变量名会引用局部变量,而不是全局变量。如果需要在这种情况下访问全局变量,可以使用域运算符 ::

示例

‌‌‌‌  以下是一个简单的示例,演示了局部变量和全局变量同名时的情况:

#include <iostream>

int value = 10; // 全局变量

void printValues() {
    int value = 20; // 局部变量
    std::cout << "局部变量 value: " << value << std::endl;
    std::cout << "全局变量 value: " << ::value << std::endl;
}

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

解释

  1. 全局变量

    • 在函数 printValues 之外定义的 int value = 10 是一个全局变量。它的作用域是整个文件。
  2. 局部变量

    • 在函数 printValues 内定义的 int value = 20 是一个局部变量。它的作用域仅限于该函数内部。
  3. 变量遮蔽

    • 在函数 printValues 内,value 这个名字指的是局部变量 value,因此 std::cout << "局部变量 value: " << value << std::endl; 会输出局部变量的值 20
    • 如果需要访问全局变量 value,可以使用域运算符 ::,例如 std::cout << "全局变量 value: " << ::value << std::endl; 会输出全局变量的值 10

关键点

  • 局部变量的优先级高于全局变量:在局部变量的作用域内,局部变量会遮蔽同名的全局变量。
  • 使用域运算符访问全局变量:当局部变量和全局变量同名时,可以使用 ::(域运算符)来访问全局变量。

‌‌‌‌  通过这种方式,可以在必要时区分局部变量和全局变量,确保代码的正确性和可读性。

194. 程序的局部变量存在于()中,全局变量存放在()中,动态申请数据存在于()中。

‌‌‌‌  堆栈 、 静态存储区 、 堆

195. 在什么时候使用常引用?

‌‌‌‌  如果既要利用引用提高程序的效率又要保护传递给函数的数据不在函数中改变,就应该使用常引用。

解释

‌‌‌‌  使用常引用(const reference)有几个主要的场景,尤其是在需要提高程序效率并保护传递给函数的数据不被修改时。以下是一些详细的解释和示例:

使用常引用的场景

  1. 提高效率

    • 通过引用传递可以避免拷贝大对象,从而提高程序的性能。对于大对象,如类实例、容器等,传递引用而不是传递值可以显著减少拷贝操作的开销。
  2. 保护数据

    • 通过 const 修饰符可以防止函数修改传递给它的对象。这确保了数据的完整性,防止意外或未经授权的修改。
  3. 函数参数

    • 对于不需要修改的输入参数,使用常引用可以提高效率并保证数据不被修改。

示例

以下是一些使用常引用的具体示例:

示例 1:传递大型对象

假设有一个大型对象 MyClass,在函数中传递这个对象时使用常引用可以提高性能。

class MyClass {
    // 大量数据成员
};

void processObject(const MyClass& obj) {
    // 读取 obj 的数据,但不修改
}

int main() {
    MyClass myObject;
    processObject(myObject);
    return 0;
}

‌‌‌‌  在这个例子中,processObject 函数通过常引用接收 MyClass 对象,避免了拷贝,同时确保了 obj 不会被修改。

示例 2:传递字符串

‌‌‌‌  对于字符串等数据量较大的对象,使用常引用可以避免不必要的拷贝。

void printString(const std::string& str) {
    std::cout << str << std::endl;
}

int main() {
    std::string text = "Hello, World!";
    printString(text);
    return 0;
}

‌‌‌‌  在这个例子中,printString 函数通过常引用接收 std::string,避免了拷贝,同时保证了字符串内容不会被修改。

示例 3:传递容器

‌‌‌‌  对于容器(如 std::vectorstd::map 等),使用常引用可以避免拷贝容器的数据,提高效率。

void printVector(const std::vector<int>& vec) {
    for (int val : vec) {
        std::cout << val << " ";
    }
    std::cout << std::endl;
}

int main() {
    std::vector<int> numbers = {1, 2, 3, 4, 5};
    printVector(numbers);
    return 0;
}

‌‌‌‌  在这个例子中,printVector 函数通过常引用接收 std::vector,避免了拷贝,同时确保了向量内容不会被修改。

关键点

  • 提高效率:通过引用传递大对象,避免拷贝,提高性能。
  • 数据保护:使用 const 修饰符防止函数修改传递给它的对象。
  • 函数参数:对于不需要修改的输入参数,使用常引用可以提高效率并保证数据不被修改。

总结

‌‌‌‌  使用常引用是一种常见的编程实践,可以在提高程序效率的同时保护数据不被修改。通过合理使用常引用,可以编写出更高效、更安全的代码。

196. 类的声明和实现分开的好处?

  1. 保护作用
  2. 提高编译的效率

解释

‌‌‌‌  将类的声明和实现分开是C++编程中的一种常见实践,带来以下几个主要好处:

1. 起保护作用

保护类的实现细节

  • 通过将类的声明放在头文件中,类的接口(公共成员函数和属性)对外可见,而实现细节(具体的函数实现)放在源文件中,从而隐藏了类的内部实现细节。这种做法被称为信息隐藏或封装,有助于保护类的实现不被外部直接访问和修改。

增强代码的模块化

  • 通过隐藏实现细节,可以实现更好的代码模块化。其他代码只需要了解类的接口,而不需要关心类的具体实现,从而降低了模块之间的耦合度。

2. 提高编译的效率

减少编译时间

  • 当类的实现发生变化时,只需要重新编译实现文件,而不需要重新编译所有包含该类声明的文件。这样可以显著减少编译时间,特别是在大型项目中。

更高效的增量编译

  • 分离声明和实现有助于增量编译。当只修改了源文件中的实现部分时,编译器只需要重新编译这个源文件,而不需要重新编译所有包含头文件的文件,这加快了编译过程。

其他好处

更好的代码组织

  • 分离声明和实现使得代码组织更加清晰。头文件主要包含接口声明,源文件包含实现细节,这样可以使代码更加易于阅读和维护。

便于团队协作

  • 在团队开发中,不同的开发者可以分别处理类的声明和实现部分,减少冲突,提高开发效率。

示例

以下是一个简单的示例,展示了如何将类的声明和实现分开:

头文件(MyClass.h)
#ifndef MYCLASS_H
#define MYCLASS_H

class MyClass {
public:
    MyClass();
    void display() const;

private:
    int value;
};

#endif // MYCLASS_H

源文件(MyClass.cpp)
#include "MyClass.h"
#include <iostream>

MyClass::MyClass() : value(0) {}

void MyClass::display() const {
    std::cout << "Value: " << value << std::endl;
}

总结

‌‌‌‌  将类的声明和实现分开有很多好处,包括保护类的实现细节、提高编译效率、改善代码组织和便于团队协作。这种做法是C++编程中的一种最佳实践,有助于编写出更高效、可维护的代码。

197. windows 消息系统由哪几部分组成?

由 3 部分组成。

  1. 消息队列:操作系统负责为进程维护一个消息队列,程序运行时不断从该消息队列中获取消息、处理消息。
  2. 消息循环应用程序通过消息循环不断获取消息,处理消息。
  3. 消息处理:消息循环负责将消息派发到相关的窗口上,使用关联的窗口过程函数处理。

解释

‌‌‌‌  Windows消息系统是Windows操作系统中的一个核心机制,它主要由以下三部分组成:

1. 消息队列(Message Queue)

  • 作用
    • 操作系统负责为每个GUI线程维护一个消息队列,用于存储该线程的消息。消息可以由用户输入(如键盘、鼠标事件)、系统事件、其他线程或应用程序产生。
  • 特点
    • 消息队列是一个先进先出(FIFO)的队列。应用程序不断从该队列中获取消息进行处理。
    • 每个线程都有自己的消息队列,这些消息队列是相互独立的。

2. 消息循环(Message Loop)

  • 作用

    • 应用程序通过消息循环不断获取消息队列中的消息,并对其进行处理。
    • 消息循环是应用程序的主循环,通常位于主线程中,负责保持应用程序的运行状态。
  • 常见实现

    • 消息循环通常是一个无限循环,调用 GetMessage 函数从消息队列中获取消息,如果消息队列为空,GetMessage 将使线程进入等待状态,直到有新消息到达。
    • 例如:
MSG msg;
while (GetMessage(&msg, NULL, 0, 0)) {
   TranslateMessage(&msg);
   DispatchMessage(&msg);
}

3. 消息处理(Message Processing)

  • 作用

    • 消息处理是指将获取到的消息派发到相应的窗口,由窗口过程函数(Window Procedure)进行处理。
    • 每个窗口都有一个关联的窗口过程函数,用于处理该窗口接收到的所有消息。
  • 窗口过程函数

    • 窗口过程函数是一个回调函数,负责处理窗口的各种消息,如创建、绘制、键盘和鼠标输入等。
    • 例如
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
    switch (uMsg) {
        case WM_PAINT:
            // 处理绘制消息
            break;
        case WM_DESTROY:
            PostQuitMessage(0);
            return 0;
        // 处理其他消息
    }
    return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

综述

‌‌‌‌  Windows消息系统通过这三部分紧密配合,实现了消息的生成、分发和处理。每个GUI应用程序都依赖于这个系统来响应用户输入、系统事件和其他应用程序的交互,从而实现用户界面的动态交互和功能。

  • 消息队列:存储和管理线程的消息。
  • 消息循环:从消息队列中获取消息并派发消息。
  • 消息处理:窗口过程函数处理具体的消息。

‌‌‌‌  这种结构使得Windows应用程序能够高效地处理多种事件,并提供响应迅速的用户体验。

198. 什么是消息映射?

‌‌‌‌  消息映射就是让程序员指定MFC类(有消息处理能力的类)处理某个消息。然后由程序员完成对该处理函数的编写,以实现消息处理功能

解释

‌‌‌‌  消息映射(Message Mapping)是Microsoft Foundation Classes (MFC)中的一个机制,用于将Windows消息与相应的消息处理函数关联起来。它使得程序员可以指定MFC类处理某个特定的消息,并编写相应的处理函数来实现消息处理功能。

消息映射的基本概念

‌‌‌‌  消息映射的主要目的是将Windows消息(如鼠标点击、键盘输入等)与类成员函数关联起来,使得这些函数在相应消息到达时自动被调用。这种机制简化了消息处理的编写和管理,使代码更易于阅读和维护。

消息映射的实现

在MFC中,消息映射通常通过以下几个步骤实现:

  1. 声明消息映射

    • 在类声明中使用宏 DECLARE_MESSAGE_MAP 声明消息映射。
  2. 定义消息映射

    • 在类实现中使用宏 BEGIN_MESSAGE_MAPEND_MESSAGE_MAP 和其他消息映射宏(如 ON_WM_PAINTON_COMMAND 等)定义消息映射。
  3. 编写消息处理函数

    • 编写相应的消息处理函数,这些函数将在对应的消息到达时被调用。

示例

以下是一个简单的MFC消息映射示例:

头文件(MyWindow.h)
#pragma once
#include <afxwin.h>

class CMyWindow : public CFrameWnd {
public:
    CMyWindow();
protected:
    afx_msg void OnPaint();
    afx_msg void OnLButtonDown(UINT nFlags, CPoint point);
    DECLARE_MESSAGE_MAP()
};

源文件(MyWindow.cpp)
#include "MyWindow.h"

BEGIN_MESSAGE_MAP(CMyWindow, CFrameWnd)
    ON_WM_PAINT()
    ON_WM_LBUTTONDOWN()
END_MESSAGE_MAP()

CMyWindow::CMyWindow() {
    Create(NULL, _T("My MFC Window"));
}

void CMyWindow::OnPaint() {
    CPaintDC dc(this);
    dc.TextOut(50, 50, _T("Hello, MFC"));
}

void CMyWindow::OnLButtonDown(UINT nFlags, CPoint point) {
    MessageBox(_T("Left mouse button clicked!"), _T("Message"), MB_OK);
}

解释

  1. 声明消息映射

    • 在类 CMyWindow 的声明中使用 DECLARE_MESSAGE_MAP 声明消息映射。
  2. 定义消息映射

    • 在类 CMyWindow 的实现中使用 BEGIN_MESSAGE_MAPEND_MESSAGE_MAP 定义消息映射。通过 ON_WM_PAINTON_WM_LBUTTONDOWNWM_PAINTWM_LBUTTONDOWN 消息与类的 OnPaintOnLButtonDown 函数关联。
  3. 编写消息处理函数

    • OnPaint 函数用于处理 WM_PAINT 消息,在窗口绘制时被调用。
    • OnLButtonDown 函数用于处理 WM_LBUTTONDOWN 消息,当左键按下时被调用。

好处

  • 简化消息处理:消息映射机制使得消息处理的编写和管理更加简洁、系统化。
  • 增强可读性和可维护性:消息映射将消息与处理函数的关联清晰地表达出来,代码更易于理解和维护。
  • 模块化设计:将消息处理逻辑集中在消息映射中,有助于实现模块化设计,提高代码的组织性。

总结

‌‌‌‌  消息映射是MFC中的一个重要机制,用于将Windows消息与相应的处理函数关联起来。通过消息映射,程序员可以方便地指定MFC类处理某个消息,并编写相应的处理函数来实现消息处理功能。这种机制简化了消息处理的编写和管理,使代码更易于阅读和维护。

199. 什么是UDP和TCP?两者的区别是什么?

‌‌‌‌  TCP 的全称是传输控制协议。这种协议可以提供面向连接的、可靠的、点到点的通信
‌‌‌‌  UDP 的全称为用户报文协议,它可以提供非连接的不可靠的点到多点的通信。

‌‌‌‌  用TCP还是UDP,要看你的程序更注重哪个方面,是可靠还是快速。

解释

‌‌‌‌  UDP(User Datagram Protocol,用户数据报协议)和TCP(Transmission Control Protocol,传输控制协议)是两种常见的传输层协议,它们在网络通信中起着重要作用。以下是对两者的详细解释及其区别:

UDP(User Datagram Protocol)

特点

  • 无连接:UDP是无连接的协议,不建立连接,不保证可靠传输。
  • 不可靠:UDP不保证数据包的顺序到达,也不保证数据包到达目标,即不提供确认和重传机制。
  • 速度快:由于没有连接建立和确认机制,UDP传输速度快,适用于对实时性要求高的场景,如视频会议、在线游戏等。
  • 简单:UDP的头部较短,开销小,协议简单。

应用场景

  • 实时应用(如VoIP、视频会议)
  • 在线游戏
  • 广播和多播传输

TCP(Transmission Control Protocol)

特点

  • 面向连接:TCP是面向连接的协议,在数据传输前需建立连接(通过三次握手)。
  • 可靠传输:TCP保证数据按序到达,并且数据不丢失、不重复,通过确认和重传机制保证数据的可靠传输。
  • 流控制和拥塞控制:TCP通过流控制和拥塞控制机制,确保网络的稳定性和公平性,防止网络拥塞。
  • 有状态:TCP维持连接的状态信息,以便可靠传输。

应用场景

  • 需要可靠传输的应用(如HTTP、HTTPS、FTP、SMTP)
  • 文件传输
  • 电子邮件

UDP和TCP的主要区别

  1. 连接方式

    • UDP:无连接,不建立连接,直接发送数据包。
    • TCP:面向连接,需要建立连接(三次握手)后才能传输数据。
  2. 可靠性

    • UDP:不保证数据包的可靠传输、顺序到达和无重复。
    • TCP:提供可靠传输,保证数据包按序到达、无丢失、无重复。
  3. 速度

    • UDP:传输速度快,适合实时应用。
    • TCP:传输速度相对较慢,因需进行连接建立和确认机制。
  4. 数据传输方式

    • UDP:面向报文,数据包独立发送,每个包大小有限制(通常为64KB)。
    • TCP:面向流,数据作为字节流传输,无固定大小限制。
  5. 开销

    • UDP:头部开销小,协议简单。
    • TCP:头部开销大,协议复杂,需要维护状态信息。

总结

  • UDP:适用于需要快速传输、容忍丢包的场景,如视频流、在线游戏、广播等。
  • TCP:适用于需要可靠传输、保证数据完整性的场景,如网页浏览、文件下载、电子邮件等。

选择使用哪种协议,取决于具体应用对可靠性、速度和传输方式的要求。

200. DPDK了解吗?

‌‌‌‌  DPDK是一套开源高性能网络数据平面库用以加速网络包的处理速度实现高性能的包交换

‌‌‌‌  通过绕过传统的操作系统网络堆栈,直接在用户空间处理网络包,DPDK能够大幅减少延迟,提高数据包处理的吞吐率,广泛用于需要高速网络通信的场景,如高频交易、云计算数据中心和网络功能退化(NFV)。

解释

‌‌‌‌  DPDK(Data Plane Development Kit)的基本功能和应用场景。我们可以将这段话拆解成几部分来理解:

  1. 定义与目的

    • DPDK是一套开源高性能网络数据平面库
    • 它的主要目的是加速网络包的处理速度,实现高性能的包交换
  2. 工作原理

    • 传统的操作系统网络堆栈会导致一定的处理延迟和性能瓶颈。
    • DPDK通过绕过这些传统堆栈,直接在用户空间处理网络包。
    • 这种方法大幅减少了延迟,提高了数据包处理的吞吐率(即单位时间内处理的数据包数量)。
  3. 应用场景

    • 高频交易:在金融领域,高频交易需要极低的延迟和高吞吐率,DPDK能够满足这些需求。
    • 云计算数据中心:大规模的数据中心需要高效的数据包处理来管理大量的数据传输。
    • 网络功能虚拟化(NFV):NFV依赖于快速的数据包处理来实现各种网络功能的虚拟化,DPDK在这方面表现优异。

‌‌‌‌  总结起来,DPDK通过直接在用户空间处理网络包,避开传统操作系统的网络堆栈,从而显著提高网络数据包处理的速度和效率。它在需要高速网络通信的场景中具有广泛的应用,例如高频交易、云计算数据中心和网络功能虚拟化。

201. C++多态的实现,以及应用场景

‌‌‌‌  C++中的多态主要是通过虚函数实现的。当基类声明一个函数为虚函数时,派生类可以对该函数进行覆盖,以实现不同的功能。这就允许通过基类的指针或引用来调用实际派生类对象的成员函数,具体调用哪个类的函数是在运行时决定的

实现机制:

  1. 基类中声明虚函数,使用virtual关键字
  2. 派生类中重写该虚函数
  3. 通过基类指针或引用调用虚函数时,动态绑定至派生类的实现

应用场景:

  1. 当有多个派生类时,可以调用同一个基类指针或引用来操作不同的派生类对象,实现代码的统一管理。
  2. 在设计模式中,例如工厂方法模式、策略模式、状态模式等,多态性允许用户使用接口类型的通用代码,同时传入不同的派生类实例以改变行为。
  3. 当开发库或框架时,多态允许用户通过继承和重写方法来扩展和自定义功能,而不需要修改原有的库代码。

解释

‌‌‌‌  举个例子来解释一下,为什么说“派生类(子类)可以覆盖(重写)基类中的虚函数,实现不同的功能,具体调用哪个类的函数是在运行时决定的”。

class Base {
public:
    virtual void foo() {
        // 基类实现
    }
};

class Derived : public Base {
public:
    void foo() override {
        // 派生类实现
    }
};

void callFoo(Base& base) {
    base.foo(); // 调用的是实际对象(基类或派生类)中的 foo 函数
}

int main() {
    Base base;
    Derived derived;

    callFoo(base);    // 调用 Base::foo()
    callFoo(derived); // 调用 Derived::foo()
}

  • 使用基类的指针或引用,可以调用派生类的函数。
  • 具体调用哪个类的函数是在运行时决定的,这就是运行时多态。

202. class里面定义int a,如果不实现构造函数,实例化这个类,a的值是?

‌‌‌‌  答:a的值是未定义的(在C++标准中成为“未初始化”)。

解释
‌‌‌‌  在C++中,如果一个类中定义了一个成员变量(如 int a),但没有提供构造函数来初始化这个变量,那么在实例化该类时,成员变量 a 的值是未定义的。未定义的意思是它的值可能是任何东西,因为它取决于内存中该位置之前存储的内容。

例如:

class MyClass {
public:
    int a;
};

int main() {
    MyClass obj;
    std::cout << obj.a << std::endl; // a 的值未定义
    return 0;
}

‌‌‌‌  在上述代码中,obj.a 的值是未初始化的,因此它可能是任何值。

‌‌‌‌  为了避免未定义的行为,通常建议在类中提供构造函数来初始化成员变量。例如:

class MyClass {
public:
    int a;
    MyClass() : a(0) {} // 构造函数将 a 初始化为 0
};

int main() {
    MyClass obj;
    std::cout << obj.a << std::endl; // 现在 a 的值是 0
    return 0;
}

‌‌‌‌  通过提供一个构造函数,可以确保成员变量有一个已知的初始值,从而避免未定义行为带来的潜在问题。

203. unique_ptr可以作为函数返回值么?

‌‌‌‌  可以。
‌‌‌‌  当函数返回一个unique_ptr时,他会利用C++移动语义将所有权从函数内部转移给调用方

解释

‌‌‌‌  unique_ptr 可以作为函数的返回值,并且当函数返回一个 unique_ptr 时,它会利用 C++ 的移动语义将所有权从函数内部转移给调用方。我们可以通过几个步骤来理解这段话:

  1. unique_ptr 的基本概念

    • unique_ptr 是 C++11 引入的智能指针类型,位于 <memory> 头文件中。
    • 它是独占所有权的智能指针,这意味着同一时间只能有一个 unique_ptr 拥有某个对象的所有权。
  2. 移动语义

    • 移动语义是 C++11 引入的特性,允许资源的所有权从一个对象转移到另一个对象,而不是复制资源。
    • 移动语义通过移动构造函数和移动赋值运算符实现,用 std::move 函数来显式调用。
  3. 返回 unique_ptr 的函数示例

    • 当函数返回一个 unique_ptr 时,编译器会自动利用移动语义将所有权从函数内部转移给调用方,而不是复制 unique_ptr
    • 这避免了所有权的复制,并且保证 unique_ptr 的独占所有权特性。

以下是一个示例:

#include <iostream>
#include <memory>

class MyClass {
public:
    MyClass() { std::cout << "MyClass Constructor" << std::endl; }
    ~MyClass() { std::cout << "MyClass Destructor" << std::endl; }
};

std::unique_ptr<MyClass> createMyClass() {
    // 创建一个 unique_ptr 并返回
    return std::make_unique<MyClass>();
}

int main() {
    // 调用函数,接收返回的 unique_ptr
    std::unique_ptr<MyClass> ptr = createMyClass();
    // 使用 ptr 访问 MyClass 的成员
    return 0;
}

在这个例子中:

  1. createMyClass 函数创建了一个 unique_ptr 并返回它。
  2. main 函数中,ptr 接收 createMyClass 返回的 unique_ptr
  3. createMyClass 返回时,unique_ptr 的所有权从函数内部转移给 main 函数中的 ptr,而不是复制 unique_ptr

‌‌‌‌  总结来说,当函数返回一个 unique_ptr 时,利用移动语义将所有权从函数内部转移给调用方,确保了资源的独占所有权和高效的资源管理。

204. 快手直播流媒体是走长连接网关推送的嘛?

‌‌‌‌  通常会通过长连接来推送流媒体内容,这是为了确保数据传输的实时性和可靠性

205. HTTP3.0 对比HTTP 2.0 的优势

  1. 基于QUIC协议:HTTP/3使用QUIC(快速UDP互联网协议)代替TCP,使得连接建立更快,减少了握手的时间。
  2. 提高了传输效率:QUIC支持多路复用,但与HTTP/2不同,它避免了TCP的“队头阻塞”问题,使得即使部分数据丢失也不会影响其他数据的传输。
  3. 更好的错误恢复:QUIC在包级别实现了前向纠错和快速重传机制,减少了因为丢包导致的延迟。
  4. 内置TLS加密:QUIC默认内置了TLS1.3,提高了数据传输的安全性
  5. 更好的数据迁移:支持连接ID,即使用户的IP地址变化,也能无缝继续通信。

206. HTTP2.0对比HTTP1.1的优势

  1. 多路复用:HTTP/2通过在一个TCP上连接同时发送多个请求和接收多个响应消息,来消除不必要的延迟并提高页面加载速度。
  2. 头部压缩:HTTP/2引入了头部压缩机制,减小了数据包的大小并提高了传输效率。
  3. 服务器推送:HTTP/2可以让服务器把客户端需要的资源主动“推送”给客户端,减少了往返的数据交换次数。
  4. 流优先级:在HTTP/2中,客户端可以设置资源的优先级,使得重要的资源可以优先加载,从而提高了用户体验。
  5. 二进制协议:HTTP/2是二进制协议,不再是文本协议,二进制协议解析更高效,更少错误。

207. 讲一下进程、线程、协程的区别

  1. 进程:是操作系统分配资源的最小单位具备一定的独立功能,能运行在处理器上每个进程有自己的独立内存空间
  2. 线程:是程序执行的最小单位,线程是进程内部的一个实体,是比进程更小的能独立运行的基本单位,一个进程中可以有多个线程,这些线程共享进程的资源
  3. 协程:是一种用户态的轻量级线程,也就是协程的切换不需要操作系统参与,由系统用户自行调度,因此效率更高。协程可以理解为“协作的线程”。它比线程更小,因为它只使用了很少的内存大小。

208. 一个进程调用malloc最大能分配到多少内存?

‌‌‌‌  在32位系统中,最大理论限制约为2GB到4GB
‌‌‌‌  在64位系统中,这个限制远大于物理内存,但实际上受制于系统的物理内存和交换空间

209. 物理机malloc(10G)会发生什么?

‌‌‌‌  如果物理机的可用内存加上交换空间小于10GB,malloc(10G)会失效,并返回一个空指针,因为没有足够的空间来分配这么大的内存块。
‌‌‌‌  如果可用内存加上交换空间足以满足10GB的要求,malloc将成功分配内存

210. vector线程安全吗?不安全在哪?

‌‌‌‌  Vector在C++STL中是不线程安全的
‌‌‌‌  不安全的原因主要在于他的操作(如增加、删除元素等)在多线程环境下没有同步,可能会导致竞态条件。

12. 多线程下使用vector一定要加锁嘛?

‌‌‌‌  为了保证在多线程环境下对Vector的操作安全,业务代码需要进行手动的锁控制。

解释

‌‌‌‌  在多线程环境中使用 std::vector 时,是否需要加锁取决于如何访问和修改 vector。这里有几个关键点需要考虑:

  1. 只读访问

    • 如果所有线程都只是读取 vector,没有任何线程对其进行修改,那么不需要加锁。这种情况下,vector 的数据是共享的常量数据,线程之间不会互相干扰。
  2. 写操作

    • 如果有任何一个线程对 vector 进行写操作(包括插入、删除、修改元素等),那么就需要加锁。因为 vector 在写操作时可能会改变其内部状态(例如重新分配内存),这会导致数据竞争和未定义行为。
  3. 混合操作

    • 如果一些线程在读取 vector,而另一些线程在写入 vector,那么也需要加锁。加锁可以防止在读取过程中发生修改,导致数据不一致或崩溃。

总结来说,为了确保线程安全,通常需要在访问 vector 时进行加锁:

  • 只读访问:不需要加锁。
  • 写操作:需要加锁。
  • 混合操作:需要加锁。

示例代码

以下是一个简单的示例,展示了如何在多线程环境中使用 std::mutexvector 进行加锁保护:

#include <iostream>
#include <vector>
#include <thread>
#include <mutex>

std::vector<int> vec;
std::mutex mtx;

void addToVector(int val) {
    std::lock_guard<std::mutex> lock(mtx);
    vec.push_back(val);
}

void printVector() {
    std::lock_guard<std::mutex> lock(mtx);
    for (int v : vec) {
        std::cout << v << " ";
    }
    std::cout << std::endl;
}

int main() {
    std::thread t1(addToVector, 1);
    std::thread t2(addToVector, 2);
    std::thread t3(printVector);

    t1.join();
    t2.join();
    t3.join();

    return 0;
}

在这个示例中:

  1. std::mutex mtx 用于保护 vec 的访问。
  2. std::lock_guard<std::mutex> lock(mtx) 确保在作用域结束时自动释放锁。
  3. addToVectorprintVector 函数都使用锁来保护对 vec 的访问,以确保线程安全。

‌‌‌‌  这种加锁机制可以防止数据竞争,并确保在多线程环境中安全地访问和修改 vector

我再详细的解释一下这段代码。

#include <iostream>
#include <vector>
#include <thread>
#include <mutex>
  • #include <iostream>:引入标准输入输出流库,用于打印输出。
  • #include <vector>:引入 std::vector 容器。
  • #include <thread>:引入多线程库,用于创建和管理线程。
  • #include <mutex>:引入互斥量库,用于线程同步,确保对共享资源的访问是安全的。
std::vector<int> vec;
std::mutex mtx;
  • std::vector<int> vec:声明一个全局的整数向量 vec,所有线程共享。
  • std::mutex mtx:声明一个全局的互斥量 mtx,用于保护对 vec 的访问。
void addToVector(int val) {
    std::lock_guard<std::mutex> lock(mtx);
    vec.push_back(val);
}
  • void addToVector(int val):定义一个函数 addToVector,接受一个整数参数 val
  • std::lock_guard<std::mutex> lock(mtx):创建一个 std::lock_guard 对象 lock,并传递互斥量 mtx。这会立即尝试获取锁,如果成功,在 lock 的生命周期内持有锁,并在 lock 离开作用域时自动释放锁。
  • vec.push_back(val):将 val 添加到向量 vec 的末尾。由于加锁保护,这个操作是线程安全的。
void printVector() {
    std::lock_guard<std::mutex> lock(mtx);
    for (int v : vec) {
        std::cout << v << " ";
    }
    std::cout << std::endl;
}
  • void printVector():定义一个函数 printVector,没有参数。
  • std::lock_guard<std::mutex> lock(mtx):同样创建一个 std::lock_guard 对象 lock,并获取互斥量 mtx 的锁。
  • for (int v : vec):范围循环,遍历 vec 中的每个元素 v
  • std::cout << v << " ":将每个元素打印到标准输出,并以空格分隔。
  • std::cout << std::endl:在输出结束后换行。
int main() {
    std::thread t1(addToVector, 1);
    std::thread t2(addToVector, 2);
    std::thread t3(printVector);

    t1.join();
    t2.join();
    t3.join();

    return 0;
}

  • int main():程序的主入口。
  • std::thread t1(addToVector, 1):创建一个线程 t1,运行函数 addToVector,并传递参数 1。此时,addToVector(1) 将在线程 t1 中运行。
  • std::thread t2(addToVector, 2):创建另一个线程 t2,运行函数 addToVector,并传递参数 2
  • std::thread t3(printVector):创建第三个线程 t3,运行函数 printVector
  • t1.join():等待线程 t1 完成。
  • t2.join():等待线程 t2 完成。
  • t3.join():等待线程 t3 完成。

‌‌‌‌  通过 join(),确保主线程在继续执行前等待所有子线程完成,确保所有操作按预期顺序执行。整体来看,这段代码展示了如何在多线程环境下安全地对共享数据结构(std::vector)进行读写操作。

13. 两个线程同时对vector下相同索引的元素修改会发生什么?

‌‌‌‌  两个线程若同时对Vector的相同索引元素进行修改,将会导致未定义行为,结果可能会是线程中的一个或两个的修改发生,或者导致数据损坏。

14. C++内存序介绍一下

  1. memory_order_relax : 放宽内存顺序,不要求操作之间的顺序
  2. memory_order_consume : 较为弱的顺序要求,仅在特定的平台上有效。
  3. memory_order_acquire : 阻止操作重排序到原子操作前。
  4. memory_order_release : 阻止操作重排序到原子操作后。
  5. memory_order_acq_rel : 同时应用acquire和release。
  6. memory_order_seq_cst : 顺序一致,所有线程看到的操作顺序相同。

解释

memory_order_relaxed:放宽内存顺序,不要求操作之间的顺序

  • 特点:不施加任何同步或排序约束,只保证原子操作的原子性。
  • 用途:用于不需要跨线程同步的计数器或统计场景,性能最佳。

memory_order_consume:较为弱的顺序要求,仅在特定平台上有效

  • 特点:确保数据依赖(data dependency)的可见性,但比 memory_order_acquire 更弱。在大多数平台上,memory_order_consume 等同于 memory_order_acquire,只有少数平台(如某些ARM架构)对其有特殊优化。
  • 用途:较少使用,主要用于那些高度依赖数据依赖性传递的平台。

memory_order_acquire:阻止操作重排序到原子操作之前

  • 特点:确保在原子操作之前的所有加载和存储都不会重排序到原子操作之后。通常用于加载操作。
  • 用途:用于获取锁或其他同步机制,确保在此之前的操作在所有线程中都可见。

memory_order_release:阻止操作重排序到原子操作之后

  • 特点:确保在原子操作之后的所有加载和存储都不会重排序到原子操作之前。通常用于存储操作。
  • 用途:用于释放锁或其他同步机制,确保在此之后的操作在所有线程中都可见。

memory_order_acq_rel:同时应用 acquire 和 release

  • 特点:结合了 memory_order_acquirememory_order_release 的效果,确保在原子操作之前的所有加载和存储都不会重排序到原子操作之后,同时确保在原子操作之后的所有加载和存储也不会重排序到原子操作之前。
  • 用途:用于需要在同一个操作中既获取又释放同步的场景,比如读-修改-写操作。

memory_order_seq_cst:顺序一致,所有线程看到的操作顺序相同

  • 特点:提供最强的内存序保证,所有线程都看到相同的操作顺序。所有 memory_order_seq_cst 操作都在全局顺序中执行,确保了最强的一致性。
  • 用途:用于需要强一致性保证的场景,确保所有线程都能以相同的顺序看到原子操作。

示例:

#include <iostream>
#include <atomic>
#include <thread>

std::atomic<int> data(0);
std::atomic<bool> ready(false);

void producer() {
    data.store(42, std::memory_order_relaxed);
    ready.store(true, std::memory_order_release);
}

void consumer() {
    while (!ready.load(std::memory_order_acquire));
    std::cout << "Data: " << data.load(std::memory_order_relaxed) << std::endl;
}

int main() {
    std::thread t1(producer);
    std::thread t2(consumer);
    
    t1.join();
    t2.join();

    return 0;
}

  • producer 线程首先将数据存储为 42,然后设置 ready 为 true。
  • consumer 线程在 ready 被设置为 true 之前,保持轮询。
  • std::memory_order_releasestd::memory_order_acquire 确保了正确的同步,确保 consumer 看到 data 的正确值。

总之,内存序在多线程编程中是非常重要的,它们提供了不同级别的同步和排序保证,以便在不同的场景下选择合适的内存序来确保数据一致性和性能。

这里我再解释一下这段代码:

#include <iostream>
#include <atomic>
#include <thread>
std::atomic<int> data(0);
std::atomic<bool> ready(false);

  • #include <iostream>:用于标准输入输出。
  • #include <atomic>:用于原子操作和原子变量。
  • #include <thread>:用于多线程编程。
  • std::atomic<int> data(0):声明一个原子整数变量 data,并初始化为 0。原子变量保证对该变量的所有操作都是原子的。
  • std::atomic<bool> ready(false):声明一个原子布尔变量 ready,并初始化为 false。

定义生产者线程函数:

void producer() {
    data.store(42, std::memory_order_relaxed);
    ready.store(true, std::memory_order_release);
}

  • data.store(42, std::memory_order_relaxed):将 data 设置为 42。使用 memory_order_relaxed 表示这个存储操作没有任何同步或排序约束,只保证原子性。
  • ready.store(true, std::memory_order_release):将 ready 设置为 true。使用 memory_order_release 确保在此操作之前的所有操作(即 data.store)不会重排序到这个操作之后。这确保了其他线程在看到 ready 为 true 时,也能看到 data 的正确值。

定义消费者线程函数:

void consumer() {
    while (!ready.load(std::memory_order_acquire));
    std::cout << "Data: " << data.load(std::memory_order_relaxed) << std::endl;
}

  • while (!ready.load(std::memory_order_acquire)):循环等待,直到 ready 为 true。使用 memory_order_acquire 确保在这个操作之后的所有操作(即 data.load)不会重排序到这个操作之前。这确保了在读取 data 之前,consumer 一定能看到 ready 为 true。
  • std::cout << "Data: " << data.load(std::memory_order_relaxed) << std::endl:打印 data 的值。使用 memory_order_relaxed 表示这个加载操作没有任何同步或排序约束,只保证原子性。

main 函数中创建和启动线程:

int main() {
    std::thread t1(producer);
    std::thread t2(consumer);
    
    t1.join();
    t2.join();

    return 0;
}
  • std::thread t1(producer):创建一个线程 t1,执行 producer 函数。
  • std::thread t2(consumer):创建另一个线程 t2,执行 consumer 函数。
  • t1.join():等待线程 t1 结束。确保 main 线程在继续执行前等待 t1 完成。
  • t2.join():等待线程 t2 结束。确保 main 线程在继续执行前等待 t2 完成。

内存序的作用

  • memory_order_relaxed:在 producer 中用于 data.store 和在 consumer 中用于 data.load。这种内存序不施加任何同步或排序约束,只保证操作的原子性。它适用于不涉及跨线程同步的简单操作。
  • memory_order_release:在 producer 中用于 ready.store。这种内存序确保在此操作之前的所有操作不会重排序到此操作之后。这意味着当 ready 被设置为 true 时,data 已经被正确地设置为 42。
  • memory_order_acquire:在 consumer 中用于 ready.load。这种内存序确保在此操作之后的所有操作不会重排序到此操作之前。这意味着当 ready 为 true 时,consumer 一定能看到 data 的正确值。

总结

  • producer 线程首先将 data 设置为 42,然后设置 ready 为 true。
  • consumer 线程循环等待,直到看到 ready 为 true,之后打印 data 的值。
  • 使用 memory_order_releasememory_order_acquire 确保了线程之间的同步,使得 consumer 在读取 data 时看到正确的值。

15. 手撕:K个一组反转链表

struct ListNode{
    int val;
    ListNode *next;
    ListNode(int x) : val(x) , next(nullptr){}

};

  
ListNode* reverseKGroup(ListNode* head , int k){
    if(head == nullptr || head->next == nullptr || k == 1)
        return head;

    ListNode *dummy = new ListNode(0);
    dummy->next = head;


    ListNode *prev = dummy;
    ListNode *curr = dummy;
    ListNode *next = dummy;


    int count = 0;
    while(curr->next != nullptr){
        curr = curr->next;
        count++;
    }

    while(count >= k){
        curr = prev->next;
        next = curr->next;
        for(int i = 1 ; i < k ; i++){
            curr->next = next->next;
            next->next = prev->next;
            prev->next = next;
            next = curr->next;
        }

        prev = curr;
        count -= k;

    }
    return dummy->next;

}

16. static关键字的作用

‌‌‌‌  static关键字在编程中有多种作用:

  1. 在类的成员变量前使用,表示该变量属于类本身,而不是任何类的实例。
  2. 在类的成员函数前使用,表示该函数不需要对象实例即可调用,且只能访问类的静态成员变量和其他静态成员函数。
  3. 在局部变量前使用,表示该变量在函数调用结束后不会被销毁,而是保持其值不变。
  4. 在全局变量或函数前使用,限制其作用范围仅在定义的文件内,对其他文件不可见。

17. extern关键字的作用

  1. 允许在多个文件中访问同一个全局变量或者函数
  2. 表明变量或函数的定义存在于其他文件中

18. C++内存分配方式

  1. 静态存储:编译时分配,如全局变量、静态变量
  2. 自动存储:函数内部声明的变量,如局部变量,随着函数调用创建和退出销毁。
  3. 动态存储:使用new和delete进行手动分配和释放的堆内存。

19. 静态内存分配和动态内存分配的区别,静态分配的优缺点

静态内存分配与动态内存分配的区别:

  1. 静态内存分配在编译时就确定了存储空间的大小和生命周期,通常用于全局变量和静态局部变量。
  2. 动态内存分配在运行时根据需要动态的分配和释放内存,通常使用new和delete。

静态内存分配的优点:

  1. 管理简单,没有额外的运行开销。
  2. 生命周期长,随程序启动创建,程序结束时销毁。

静态内存分配的缺点:

  1. 灵活性低,必须提前预知并定义所需内存大小。
  2. 可能会导致内存浪费,若预分配内存未被充分利用。

20. 互斥锁和自旋锁的区别?

互斥锁:
‌‌‌‌  当一个线程获得互斥锁后,其他尝试获得该锁的线程会被挂起(阻塞),直到锁被释放。适用于线程执行时间较长的情况。

自旋锁:
‌‌‌‌  当一个线程尝试获取自旋锁而锁已被占用时,线程会循环等待(自旋),直到锁被释放。适用于线程执行时间非常短的情况,避免了线程挂起的开销。

21. 线程和进程的区别

‌‌‌‌  进程是操作系统进行资源分配和调度的基本单位,每个进程拥有独立的地址空间和系统资源。线程是进程中的执行单元,是CPU调度的基本单位,同一进程中的线程共享该进程的地址空间和资源。

22. 如何进行线程切换的?

‌‌‌‌  线程切换是操作系统的调度器通过保存当前线程的状态到线程的上下文中,然后加载另一个线程的上下文并恢复其状态,这样CPU就可以继续执行新线程的处理。

23. IP寄存器的作用,是通用寄存器么?

‌‌‌‌  IP寄存器,即指令寄存器(在x86架构中称为EIP,在x64架构中称为RIP),其作用是存储吓一跳要执行的命令。他不是通用寄存器,因为他有特定的用途,即指向程序的下一条指令,而不能用于通用数据存储或算术逻辑运算。

24. LR寄存器了解么?

‌‌‌‌  LR寄存器是链接寄存器,在ARM架构中常见。他用于存储子程序调用返回后执行的下一条指令的地址,返回地址会存入LR寄存器。这样,在子程序执行完毕后可以通过LR寄存器找到并返回到调用点继续执行。LR寄存器不是通用寄存器。

25. 线程有哪几种状态?

‌‌‌‌  创建(New),就绪(Runnable)、运行(Running)、阻塞(Blocked)、等待(Waiting)、超时等待(Timed Waiting)、终止(Terminated)。

26. 自旋锁等待时线程处于什么状态?互斥锁呢?

‌‌‌‌  自旋锁等待时线程处于运行状态,因为他在忙等待,不停的检查锁是否可用。
‌‌‌‌  互斥锁等待时,线程处于阻塞状态,不占用CPU资源,直到锁成为可用状态。

27. 讲一下你了解的进程调度算法

进程调度算法是决定哪个进程将接下来使用CPU的规则集合。常见的进程调度算法包括:

  1. 先来先服务(FCFS):按照进程到达的顺序进行调度
  2. 最短作业优先(SJF):先执行预计运行时间最短的进程。
  3. 优先级调度:优先执行优先级最高的进程
  4. 轮转调度(Round Robin,RR):每个进程分配时间片,轮流执行
  5. 多级反馈队列(Multilevel Feedback Queue,,MFQ):动态调整进程的优先级和时间片。

28. 讲一下C++的智能指针

unique_ptr:独占所有权,不支持复制和赋值操作。
shared_ptr:引用计数机制,多个智能指针可以共享一个对象。
weak_ptr:不对对象的所有权计数,用于解决shared_ptr的循环引用的问题。

29. shared_ptr的底层实现了解么?

shared_ptr底层实现通常包括两个主要部分:

  1. 控制块:存储引用计数和指向动态分配的对象的指针。
  2. 智能指针对象:包含对控制块的引用。

30. 讲一下lambda表达式,lambda表达式优点和应用场景

‌‌‌‌  Lambda表达式时C++中的匿名函数,让你能够写出内联的、可调用的代码块,可以捕获并使用其所在作用域中的变量。
优点:

  1. 简洁:减少了编写单独函数或函数对象的必要
  2. 方便:可直接在需要的地方定义和使用
  3. 灵活:能够捕获所在作用域的变量

应用场景:

  1. 作为回调函数,例如给线程或算法传参
  2. 在STL算法中,用于自定义排序准则或操作
  3. 用于定义局部的小功能块,避免编写多余的函数

31. map和unordered_map的区别

map和unordered_map的区别:
map:基于红黑树实现,元素按键自动排序,查找、插入和删除操作的时间复杂度为O(logn)
unordered_map:基于哈希表实现,元素不排序,按哈希值存储;平均情况下查找、插入和删除操作的时间复杂度为O(1),最坏情况为O(n)

32. unordered_map实现了解么?

‌‌‌‌  unordered_map通过哈希表实现。它使用一个哈希函数将键值映射到桶中,并在桶内使用链表处理哈希冲突。当发生哈希冲突时,即多个元素映射到同一个桶,这些元素会以链表的形式存储在该桶中。这样可以通过键的哈希值快速访问到对应的桶,从而进行元素的查找、插入和删除。

33. 哈希冲突是指什么?

‌‌‌‌  哈希冲突是指不同的键通过哈希函数计算后得到相同的哈希值,因此他们被映射到同一个哈希表的存储位置上。

34. 讲一下TCP三次握手

TCP三次握手的过程:

  1. 客户端发送一个含有SYN标志的数据包给服务器,请求连接
  2. 服务器收到SYN包,回应一个含有SYN/ACK标志的数据包,表示确认
  3. 客户端收到SYN/ACK包,再发送一个ACK包给服务器,完成连接建立

35. http协议和TCP协议的关系

‌‌‌‌  HTTP协议运行在TCP协议之上,使用TCP提供可靠传输服务来确保数据正确无误的从客户端传输到服务器,或者反过来。

36. https协议和http协议的关系

‌‌‌‌  HTTPS是HTTP协议的安全版本,它通过SSL/TLS协议提供加密处理数据的功能,保证数据传输的安全性和完整性。

37. 内存分配情况,存放在哪里?

  1. 堆:动态分配内存,用于存放动态分配的对象
  2. 栈:自动分配释放,用于存放函数局部变量和函数参数。
  3. 数据区:存放全局变量和静态变量
  4. 代码区:存放程序的执行代码

38. 函数参数传递的方式和特点

  1. 值传递:函数接收参数值的一个版本,原始数据不会被函数修改
  2. 引用传递:函数接收参数的内存地址(引用),可以直接修改原始数据

39. static和const的区别

‌‌‌‌  static关键字用来定义静态变量,其生命周期为程序执行期间,但它的作用域限定于定义它的文件或函数内。

‌‌‌‌  const关键字定义常量,其值在定义后不能被修改,用来保证数据的不变性。他只影响它所修饰的变量的可变性,而不影响其生命周期。

40. static修饰局部变量、全局变量、函数和类各有什么特点?

  1. 静态局部变量:存储在数据区,生命周期贯穿程序执行期,但只在定义它的函数内可见。
  2. 静态全局变量:作用域限定于定义它的文件内,对其他文件不可见。
  3. 静态函数:其链接属性为内部,只能在定义它的文件内使用。
  4. 静态类成员:属于类本身,而不是类的任何对象,所有对象共享同一个静态成员。

解释

‌‌‌‌  可能你跟我一样,有种疑问,静态局部变量(static 本地变量)在程序的执行期间都保持存活(生命周期),但只在定义它的函数内可见(作用域)。它们听上去似乎会占用过多的系统资源,但它们有其特定的用途和优点:

静态局部变量的特性

  1. 生命周期:静态局部变量的生命周期是从程序开始执行到程序结束,生命周期超级长。
  2. 作用域:它的作用域局限于定义它的函数,换句话说,只能在函数内访问。

为什么需要静态局部变量

  1. 保持状态
    静态局部变量用于保持函数内部的状态信息。例如,可以用它们来记住函数被调用的次数:
#include <iostream> 
void counter() { 
	static int count = 0; 
	count++; 
	std::cout << "Function called " << count << " times\n"; 
} 
int main() { 
	counter(); 
	counter(); 
	counter(); 
	return 0; 
} // 输出: // Function called 1 times // Function called 2 times // Function called 3 times
  1. 优化性能
    对于某些计算密集的任务,静态局部变量可以用作缓存以避免重复计算,从而提高性能。

  2. 数据持久性
    数据需要在多次函数调用之间保持一致而不需要全局变量时,静态局部变量是一个好用的选择。它能保护局部状态,而无需暴露在全局作用域。

内存占用考虑

  • 静态局部变量的内存分配
    静态局部变量在程序的全局数据区(通常称为BSS段或数据段)中分配内存,与堆栈和堆上的变量不同。这种内存分配在编译时进行,因此程序启动时其大小是固定的。

  • 资源占用
    虽然它们确实在整个程序生命周期内占用资源,但它们的使用方式通常非常有限,并且在大多数场景中相比较其带来的好处(如状态保持、性能提升等),这点内存消耗是可以接受的。

何时使用静态局部变量

  • 需要在多次调用之间保持函数内部状态
    当你希望一个函数保持一些数据或状态信息,以便在下次调用时继续使用这些信息,静态局部变量是理想的选择。

  • 不希望全局变量污染命名空间
    静态局部变量提供了一种在函数内部保持状态信息而不需要全局变量的方法,避免了命名冲突和潜在的全局变量滥用。

  • 需要持久化缓存或中间结果
    当某些中间计算结果需要被多次使用时,静态局部变量能减少不必要的重新计算,例如在复杂算法或递归过程中。

#include <iostream> 
int heavyComputation() { 
	// 假设这是一个很复杂的初始化计算 
	return 42; 
} 
int computeSomething() { 
	static int cachedResult = heavyComputation(); // 只会在第一次调用时初始化 
	return cachedResult + 10; // 假设一个计算操作 
} 
int main() { 
	std::cout << computeSomething() << "\n"; // 输出: 52 
	std::cout << computeSomething() << "\n"; // 输出: 52,再次调用时,cachedResult不会被重新计算 
	return 0; 
}

‌‌‌‌  在这个例子中,cachedResult 静态局部变量保留了 heavyComputation() 的计算结果,因此后续 computeSomething() 的调用不需要再次进行复杂的计算。

41. new和malloc、free和delete的区别

  1. new和delete是C++中用于动态内存分配和释放的操作符,new在分配内存的同时调用构造函数初始化对象,delete释放内存前调用对象的析构函数
  2. malloc和free是C语言中的用于动态内存分配和释放的函数,malloc只分配内存,不初始化,free只释放内存,不调用析构函数。

42. 指针和引用的区别

  1. 指针是一个变量,其值为另一个变量的内存地址,通过地址,可以直接直接访问和修改对应内存中的值。
  2. 引用是别名,他为对象提供了一个新的名字,对引用的操作等同于对对象本身的操作。
  3. 指针可以为空,引用必须得绑定到一个对象。
  4. 指针的值(即所指对象的地址)可以改变,但引用一旦与某个对象绑定,就不能再改变引用到其他对象。

43. 深拷贝和浅拷贝

‌‌‌‌  浅拷贝指的是复制对象的引用,而不是对象本身,因此原始对象和副本对象共享同一块内存地址。

‌‌‌‌  深拷贝则是复制对象及其包含的所有对象,副本和原始对象在内存中完全独立。

44. 进程间通信,管道的特点

  1. 半双工通信:数据只能单向流动,如果需要双向通信,需要创建两个管道。
  2. 数据流动是顺序的和阻塞的:数据按照发送的顺序接受,如果管道空则读操作阻塞,如果管道满则写操作阻塞。
  3. 管道是用于有亲缘关系的进程间通信,如父子进程通信。

45. define特点,怎么定义一个a+b的宏

特点

  1. 预处理阶段进行文本替换,不涉及类型检查
  2. 可以定义常量和宏,提高代码复用性
  3. 宏可以包含参数,但不进行正确性检查

定义一个执行a+b的宏,如下所示:

#define ADD(a+b) ((a)+(b))

‌‌‌‌  这里的(a)和(b)被括起来是为了避免展开时出现的运算优先级问题

46. sizeof和strlen区别

  1. sizeof是一个编译时运算符,用来得到某个类型或变量在内存中的大小,单位是字节
  2. strlen是一个运行时函数,用来计算字符串的长度,直到遇到第一个空字符’\0’,不包括’\0’。

47. sprintf、strcpy和memcpy的区别

sprintf:格式化数据并将结果字符串输出到指定的字符数组。
strcpy:将一个以空字符结尾的字符串复制到另一个地址空间,包括空字符。
memcpy:在内存中复制指定数量的字节,从一个位置到另一个位置,不关注数据类型,可能不处理空字符。

48. 虚函数的特点

  1. 支持动态多态性:允许通过基类指针或引用调用派生类的函数。
  2. 运行时绑定:函数调用在运行时解析,而非编译时
  3. 存在虚表(vtable):每个有虚函数的类都有一个虚表
  4. 可以被派生类重写:派生类可以提供自己的虚函数实现
  5. 必须至少有一个函数体,除非声明为纯虚函数

49. 父类子类构造析构函数调用顺序

  1. 构造函数调用顺序:首先调用父类构造函数,然后调用子类构造函数。
  2. 析构函数调用顺序:首先调用子类析构函数,然后调用父类析构函数。

50. 构造析构能否抛出异常?能否是虚函数?

‌‌‌‌  构造函数和析构函数都可以抛出异常,但应该谨慎处理,以避免可能导致的资源泄露或不一致状态。

‌‌‌‌  构造函数不能是虚函数,因为虚函数表在构造时尚未建立。析构函数可以是虚函数,通常在基类中将析构函数声明为虚析构函数,以确保通过基类指针删除派生类对象时,能正确调用派生类的析构函数。

51. 内存对齐

‌‌‌‌  C++内存对齐是为了提高内存访问效率,确保数据结构按照某个固定长度(对齐界限)存储。编译器会自动添加填充字节(padding),使得结构体的每个成员相对于结构体开始位置的偏移量是成员大小或某个特定数值(通常是2的幂)的整数倍。

52. STL有哪些容器?各自特点?

  1. 序列容器:
    1. vector:动态数组,支持快速随机访问。
    2. list:双向链表,支持快速插入和删除。
    3. deque:双端队列,两端都可以快速插入和删除。
  2. 关联容器:
    1. set:集合,元素唯一且自动排序。
    2. map:键值对集合,键唯一且自动排序。
    3. multiset:集合,元素可以重复,自动排序。
    4. multimap:键值对集合,键可以重复,自动排序。
  3. 无序关联容器:
    1. unordered_set:集合,元素唯一,基于哈希表实现,不排序。
    2. unordered_map:键值对集合,键唯一,基于哈希表实现,不排序。
    3. unordered_multiset:集合,元素可以重复,基于哈希表实现,不排序。
    4. unordered_multimap:键值对集合,键可以重复,基于哈希表实现,不排序。
  4. 适配器容器:
    1. stack:栈,后进先出。
    2. queue:队列,先进先出。
    3. priority_queue:优先队列,元素按优先级出队。

53. vector扩容如何实现?

‌‌‌‌  vector扩容通常通过创建一个更大的新内存空间,然后将原有元素复制或移动到新空间中,之后释放原始内存空间来实现。
‌‌‌‌  扩容的大小通常是当前容量的两倍,以减少频繁扩容的开销。

54. map、set特点,怎么实现的?

‌‌‌‌  map和set的特点是他们存储的元素是自动排序的,map存储键值对,其中键是唯一的,而set只存储键且每个键是唯一的。

‌‌‌‌  他们通过平衡二叉搜索树(通常是红黑树)实现的,这个数据结构可以提供对元素的有序存储,以及在对数时间复杂度内进行元素查找、插入和删除操作。

55. 内联函数

‌‌‌‌  C++内联函数是一种通常用于优化小型、频繁调用的函数的编程技术。通过在函数声明前加inline关键字,编译器在调用处直接展开函数代码,以减少函数调用的开销。但最终是否内联取决于编译器的决定。

56. GDB如何debug,怎么传参?

‌‌‌‌  在GDB中进行调试,首先要启动GDB并加载你要调试的程序,可以使用gdb<your_program>命令。传递参数可以在GDB中使用 set args < arg1>< arg2>… 命令来设置命令行参数。然后,你可以使用像run来运行程序、break设置断点、next单步执行等命令来控制程序的执行并进行调试。

解释
‌‌‌‌  GDB(GNU Debugger)是GNU项目下的一个强大的调试工具,用于调试C、C++、Fortran等多种编程语言的程序。它可以帮助开发者追踪程序的执行流程,查找和修复错误。GDB 提供了多种功能,如设置断点、单步执行、查看变量和内存等。

如何使用 GDB 进行调试?

‌‌‌‌  以下是使用 GDB 进行调试的基本步骤:

1. 编译你的程序

‌‌‌‌  在使用 GDB 调试之前,你需要确保你的程序是用调试信息编译的。通常,你可以通过在编译时加上 -g 选项来包含调试信息。例如,对于一个C++程序 main.cpp

g++ -g -o myprogram main.cpp
2. 启动 GDB

‌‌‌‌  启动 GDB 并加载你的程序:

gdb myprogram
3. 设置断点

‌‌‌‌  断点允许你在程序执行到某一行时暂停。例如,要在 main 函数的第一行设置一个断点,可以在 GDB 提示符下输入:

(gdb) break main

‌‌‌‌  你也可以在特定的行号或函数名处设置断点,例如:

(gdb) break 10  # 在第10行设置断点
(gdb) break MyClass::myFunction  # 在类 MyClass 的 myFunction 方法处设置断点

4. 运行程序

‌‌‌‌  在设置断点后,你可以通过以下命令运行程序:

(gdb) run

‌‌‌‌  如果你的程序需要命令行参数,可以在 run 命令后指定:

(gdb) run arg1 arg2 arg3
5. 单步执行

‌‌‌‌  当程序在断点处暂停时,你可以单步执行代码:

  • step(或 s):进入函数调用
  • next(或 n):执行下一行代码,但不进入函数
  • continue(或 c):继续运行程序,直到下一个断点
6. 查看变量和内存

‌‌‌‌  你可以查看变量的值:

(gdb) print variable_name

‌‌‌‌  你也可以查看内存地址:

(gdb) x /nfu address

这里的 n 是单元数,f 是显示格式,u 是单元大小。例如:

(gdb) x /4xb 0xaddress  # 查看地址 0xaddress 处的 4 个字节,以十六进制显示

一个完整的 GDB 调试示例

假设你有一个简单的C++程序 main.cpp

#include <iostream>
using namespace std;

void function(int a) {
    int b = a * 2;
    cout << "Value of b: " << b << endl;
}

int main(int argc, char* argv[]) {
    int x = 5;
    function(x);
    return 0;
}

编译程序

g++ -g -o myprogram main.cpp

启动 GDB

gdb myprogram

设置断点

(gdb) break main

运行程序

(gdb) run

程序在 main 处暂停后,单步执行

(gdb) next  # 执行下一行
(gdb) step  # 进入 function 函数

查看变量

(gdb) print x
(gdb) print b

继续运行程序

(gdb) continue

‌‌‌‌  通过这些步骤,你可以使用 GDB 来有效地调试你的程序,查找并修复错误。

57. 指针的初始化和释放

‌‌‌‌  指针的初始化应该将其设为nullptr(C++11及以后)或NULL,以指示它尚未指向任何对象。当指针分配了动态内存后(例如,使用new),完成使用后应使用delete(对于单个对象)或delete[](对于对象数组)来释放分配的内存,然后把指针重新设置为nullptr,避免悬挂指针的问题。

58. 三个进程都需要读写一块内存,如何调度?

‌‌‌‌  为了协调三个进程对同一块内存区域的读写访问,可以使用互斥锁(mutexes)或读写锁(read-write locks)。

  1. 互斥锁确保任一时间只有一个进程可以访问该内存。
  2. 读写锁允许多个读取者同时访问,但写入者单独访问。
    ‌‌‌‌  
    ‌‌‌‌  适当的锁机制需要根据访问模式和性能要求进行选择。

1. vfork了解么?

‌‌‌‌  vfork是一个UNIX系统调用,用于创建一个新的进程,成为子进程,它与创建它的父进程共享相同的内存空间。vfork被设计用来在执行exec系列函数前作为一个临时步骤。它区别于fork,因为他不会复制父进程的地址空间,而是直接使用父进程的地址空间,直到子进程调用exec或exit。这样做的目的是为了提高性能,但缺点是可能导致父子进程之间的同步问题。现代系统上,vfork的行为通常与fork相似,或被clone系统调用所取代。

2. C++程序编译链接过程

  1. 预处理(Preprocessing):预处理器处理源代码文件中的预处理指令,如宏定义、条件编译指令和文件包含指令等,生成预处理后的代码。
  2. 编译(Compilation):编译期间预处理后的代码转换为汇编指令,并进行语法和语义分析,生成相应平台的汇编代码文件。
  3. 汇编(Assembly):汇编器将汇编代码转换为机器代码,生成目标文件(通常是.obj或.o文件)。
  4. 链接(Linking):链接器将所有目标文件以及所需的库文件合并,解决代码中的外部引用,并生成最终的可执行程序。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值