面经

C++

动态的多态性和静态的多态性

在C++中,多态性是面向对象编程中的一个重要概念,它允许以统一的方式处理不同类型的对象。C++中的多态性有两种类型:动态多态性(运行时多态性)和静态多态性(编译时多态性)

  1. 动态多态性(运行时多态性):
    • 动态多态性是通过虚函数和指针或引用来实现的
    • 在运行时,系统会根据对象的实际类型来调用相应的函数,而不是根据指针或引用的类型。
    • 动态多态性通过虚函数实现。当基类中的函数被声明为虚函数时,派生类可以重写(覆盖)这些函数,然后在运行时,根据对象的实际类型来调用正确的函数版本。

示例代码:

#include <iostream>

class Base {
public:
    virtual void display() {
        std::cout << "Base class display() called" << std::endl;
    }
};

class Derived : public Base {
public:
    void display() override {
        std::cout << "Derived class display() called" << std::endl;
    }
};

int main() {
    Base* basePtr;
    Derived derivedObj;

    basePtr = &derivedObj;  // 指向派生类对象的基类指针

    // 调用的是派生类中的 display() 函数
    basePtr->display();

    return 0;
}
  1. 静态多态性(编译时多态性):
    • 静态多态性是通过函数重载和运算符重载来实现的。
    • 在编译时,编译器根据函数或运算符的参数类型来确定应该调用哪个函数或运算符重载版本。
    • 静态多态性在编译时就已经确定了调用的函数或运算符,因此也称为编译时多态性。

示例代码:

#include <iostream>

// 函数重载示例
void display(int a) {
    std::cout << "Integer: " << a << std::endl;
}

void display(double b) {
    std::cout << "Double: " << b << std::endl;
}

int main() {
    display(5);      // 调用 display(int)
    display(5.5);    // 调用 display(double)

    return 0;
}

在动态多态性中,函数的调用是在运行时确定的,而在静态多态性中,函数的调用是在编译时确定的。

内联函数

内联函数是一种特殊的函数,它在编译时会被编译器尝试进行内联展开,即将函数调用处直接替换为函数体的内容,而不是像普通函数一样生成调用指令。这样做的好处是可以减少函数调用的开销,提高程序的执行效率。内联函数通常适用于函数体较小、频繁调用的情况。

在C++中,使用 inline 关键字可以声明一个函数为内联函数。声明为内联函数的函数体通常会被直接插入到每个函数调用的地方,而不是像普通函数一样生成单独的函数体。

示例:

#include <iostream>

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

int main() {
    int x = 5;
    int y = 10;

    // 调用内联函数
    int result = add(x, y);

    std::cout << "Result: " << result << std::endl;

    return 0;
}

在上述示例中,add 函数被声明为内联函数。编译器会尝试在 main 函数中的 add(x, y) 调用处直接插入 add 函数的函数体,而不是生成一个函数调用的指令。这样可以减少函数调用的开销,提高程序的执行效率。

需要注意的是,inline 关键字只是对编译器的建议,编译器并不一定会采纳内联展开,它会根据具体情况决定是否进行内联展开。通常情况下,编译器会根据函数体的大小、调用情况等因素来决定是否进行内联展开。

一般1~5行小函数常被设计为内联函数,且内联函数内不允许出现循环语句和开关语句,递归函数不能作为内联函数

注:在类声明里实现的函数(类内部直接实现的函数)也是内联函数

C++空类大小

在C++中空类会占一个字节,这是为了让对象的实例能够相互区别。具体来说,空类同样可以被实例化,并且每个实例在内存中都有独一无二的地址,因此,编译器会给空类隐含加上一个字节,这样空类实例化之后就会拥有独一无二的内存地址。如果没有这一个字节的占位,那么空类就无所谓实例化了,因为实例化的过程就是在内存中分配一块地址。 注意:当该空白类作为基类时,该类的大小就优化为0了,这就是所谓的空白基类最优化。

空类在 C++ 中会占据一个字节的大小是为了确保每个对象的实例在内存中都有唯一的地址,从而能够相互区分。这是由于 C++ 标准要求每个对象的地址必须是唯一的,即使是空类的对象也不例外。为了满足这个要求,编译器在空类中隐含地插入了一个字节的空间,这样每个实例化的对象都有不同的地址。

而当空类作为基类时,情况有所不同。由于空类没有任何成员变量,因此不需要为其分配额外的空间。在这种情况下,编译器会优化空类的大小为0,因为空类没有成员变量,也没有虚函数,所以不需要额外的空间来存储任何信息。因此,空白基类最优化的实现会将空类的大小设置为0,从而节省内存空间。

综上所述,空类之所以会占据一个字节的大小是为了满足 C++ 标准对对象地址唯一性的要求,而空白基类的最优化则是因为空类没有成员变量而被特殊处理。

C++中类的数据成员和成员函数内存分布情况

#include <iostream>
using namespace std;

class Person
{
public:
    Person()
    {
        this->age = 23;
    }
    void printAge()
    {
        cout << this->age <<endl;
    }
    ~Person(){}
public:
    int age;
};

int main()
{
    Person p;
    cout << "对象地址:"<< &p <<endl;
    cout << "age地址:"<< &(p.age) <<endl;
    cout << "对象大小:"<< sizeof(p) <<endl;
    cout << "age大小:"<< sizeof(p.age) <<endl;
    return 0;
}
//输出结果
//对象地址:0x7fffec0f15a8
//age地址:0x7fffec0f15a8
//对象大小:4
//age大小:4

从代码运行结果来看,对象的大小和对象中数据成员的大小是一致的,也就是说,成员函数不占用对象的内存。这是因为所有的函数都是存放在代码区的,不管是全局函数,还是成员函数。

静态成员函数的存放问题:静态成员函数与一般成员函数的唯一区别就是没有this指针,因此不能访问非静态数据成员。

就像我前面提到的,所有函数都存放在代码区,静态函数也不例外。所有有人一看到 static 这个单词就主观的认为是存放在全局数据区,那是不对的。

this指针

  1. this指针创建在调用成员函数之前
  2. 计算对象内存大小时并不计算this指针,因为this指针并不是对象的成员之一,也不影响对象的内存布局
  3. this指针是编译器自动生成的额外内容(虚函数表指针也是)
  4. 静态函数、友元函数的调用都不涉及this指针

智能指针

智能指针是 C++ 中用于管理动态分配的内存的智能工具,可以帮助避免内存泄漏和悬挂指针等问题。常见的智能指针包括 shared_ptrunique_ptrauto_ptrweak_ptr

  1. shared_ptr:允许多个智能指针共享同一个对象。它通过引用计数的方式来跟踪对象的引用数量,当引用计数为零时,会自动释放对象的内存。使用 std::make_sharedstd::shared_ptr 构造函数创建。

  2. unique_ptr:独占所指向的对象,确保在其生命周期结束时自动释放对象的内存。不能进行拷贝构造和拷贝赋值,但可以进行移动构造和移动赋值,因此适合用于管理独占资源。使用 std::make_uniquestd::unique_ptr 构造函数创建。

  3. auto_ptr:已经过时,不建议使用。与 unique_ptr 类似,但其拷贝构造和赋值操作会导致所有权的转移,可能导致悬挂指针的问题。

  4. weak_ptr:是 shared_ptr 的一种辅助类,用于解决 shared_ptr 的循环引用问题。weak_ptr 本身不增加引用计数,因此不会影响对象的生命周期。可以通过 lock 方法将其转换为 shared_ptr,用于访问所指向的对象。

智能指针的使用可以大大简化资源管理的代码,并提高代码的安全性和可靠性。在选择智能指针时,应根据具体的需求和场景来决定使用哪种类型的智能指针。

指针常量和常量指针

  1. 指针常量——指针类型的常量(指针是常量)(int *const p)
    本质上一个常量,指针用来说明常量的类型,表示该常量是一个指针类型的常量。在指针常量中,指针自身的值是一个常量,不可改变,始终指向同一个地址。在定义的同时必须初始化。指针常量在用法和效果上是类似左引用的。用法如下:

    int a = 10, b = 20;
    int * const p = &a;
    *p = 30; // p指向的地址是一定的,但其内容可以修改
    
  2. 常量指针——指向“常量”的指针(常量是指针指向的内容)(const int *p, int const *p)
    常量指针本质上是一个指针,常量表示指针指向的内容,说明该指针指向一个“常量”。在常量指针中,指针指向的内容是不可改变的,指针看起来好像指向了一个常量。用法如下:

    int a = 10, b = 20;
    const int *p = &a;
    p = &b; // 指针可以指向其他地址,但是内容不可以改变
    

指针与引用的相同和区别 如何相互转换

相同点

  1. 间接访问:指针和引用都可以用来间接访问另一个变量的值。
  2. 类型:指针和引用都必须与它们所指向或引用的变量类型相匹配。
  3. 地址:指针存储的是变量的地址,而引用在创建时必须绑定到一个已经存在的变量的地址,之后它就相当于该变量的一个别名。

区别

  1. 存储内容

    • 指针是一个变量,它存储了另一个变量的地址。
    • 引用不是一个变量,它只是一个已存在变量的别名,没有自己的地址。
  2. 初始化

    • 指针可以在任何时候被初始化,也可以被重新指向另一个地址,甚至可以被设置为 NULL
    • 引用必须在声明时立即初始化,并且一旦初始化后就不能改变它所引用的变量。
  3. 指针运算

    • 指针支持多种运算,如地址运算(&)、解引用运算(*)、指针算术运算(++--+-)等。
    • 引用不支持这些运算,它只能作为变量的一个别名来使用。
  4. 空值

    • 指针可以为 NULL,表示它不指向任何对象。
    • 引用必须引用一个有效的对象,不能为 NULL
  5. 内存占用

    • 指针在内存中占用的空间大小与平台相关(通常是 4 或 8 字节)。
    • 引用在内存中不占用额外空间,它只是对已存在变量的一个引用。

相互转换

  1. 从指针到引用

    • 指针不能直接转换为引用。如果指针指向一个有效的对象,你可以创建一个引用并将其初始化为指针所指向的对象。例如:
      int *ptr = &someVariable;
      int& ref = *ptr; // 引用初始化为指针所指向的对象
      
  2. 从引用到指针

    • 引用可以被转换为指针,但这种转换通常没有实际意义,因为引用必须绑定到一个有效的对象。如果需要,可以通过取地址操作来获取引用所引用对象的指针。例如:
      int& ref = someVariable;
      int* ptr = &ref; // 指针指向引用所引用的对象
      

在实际编程中,引用通常用于函数参数,以避免拷贝开销并允许函数修改实参的值。指针则提供了更灵活的内存操作能力,但需要更谨慎地管理内存。

extern “c” 以及 extern

在 C 语言中,extern "C" 是一个链接指示(Linkage Specifier),它用于指定 C++ 代码中的函数或变量应该使用 C 语言的链接方式。这通常用于以下几个方面:

  1. C 语言兼容性
    • 当 C++ 代码需要与 C 语言代码进行交互时,extern "C" 确保 C++ 编译器按照 C 语言的规则来编译和链接函数或变量。C 语言不支持 C++ 的名称修饰(Name Mangling),所以使用 extern "C" 可以避免 C++ 的名称修饰,使得 C 语言代码能够找到并调用这些函数。
  2. 库函数调用
    • 许多 C 语言库(如标准库函数)在编译时不会进行名称修饰。为了在 C++ 程序中正确链接这些库函数,需要使用 extern "C"
  3. 模块间通信
    • 在模块化编程中,如果一个模块是用 C 语言编写的,而另一个模块是用 C++ 编写的,那么在 C++ 模块中引用 C 模块的函数或变量时,需要使用 extern "C"

extern "C" 的使用示例:

// 在 C++ 代码中声明 C 语言风格的函数
extern "C" {
    int printf(const char *format, ...);
}

// 在 C 语言代码中定义的函数
int printf(const char *format, ...) {
    // 实现...
}

// 在 C++ 代码中调用 C 语言风格的 printf 函数
extern "C" void myFunction() {
    printf("Hello, World!\n");
}

在这个例子中,printf 函数在 C++ 代码中被声明为 extern "C",这样它就可以在 C++ 代码中以 C 语言的方式被调用。同样,myFunction 函数也被声明为 extern "C",这样它就可以被 C 语言代码调用。

注:当在头文件中声明全局变量或函数时,应该使用 extern 关键字,而在实际定义这些变量或函数的源文件中,不应该再次使用 extern。这是因为 extern 的作用是告诉编译器这个变量或函数的定义在别的地方,而不是在当前文件中

这里的关键是理解 extern 的作用和 C/C++ 的链接过程:

  1. 头文件中的 extern 声明

    • 当你在头文件中使用 extern 声明一个全局变量或函数时,你是在告诉编译器这个变量或函数的定义在别的地方。这样,编译器在编译包含该头文件的源文件时,不会为这些变量或函数创建新的内部定义
    • 这样做的目的是为了避免在多个源文件中包含同一个头文件时产生重复定义的问题。因为每个源文件在编译时都会看到这些 extern 声明,如果它们尝试定义这些变量或函数,就会导致链接时的多重定义错误
  2. 源文件中的定义

    • 在实际定义全局变量或函数的源文件中,不应该使用 extern。因为这里正在提供变量或函数的实际定义,编译器需要知道这一点,以便在链接时将这些定义与之前在头文件中看到的声明相匹配。
    • 如果在定义时使用了 extern,编译器会认为这个变量或函数的定义在别的地方,而不会在当前文件中创建实际的内部定义,这会导致链接时找不到这些变量或函数的定义。

下面是一个简单的例子来说明这个过程:

// my_header.h
#ifndef MY_HEADER_H
#define MY_HEADER_H

// 使用 extern 声明全局变量和函数
extern int globalVariable; // 声明
extern void myFunction();   // 声明

#endif // MY_HEADER_H
// my_source.c
#include "my_header.h"

// 在这里定义全局变量和函数,不需要使用 extern
int globalVariable = 42; // 定义
void myFunction() {
    // 函数实现...
} // 定义

在这个例子中,my_header.h 中的 extern 声明允许 my_source.c 和其他任何包含该头文件的源文件共享 globalVariablemyFunction 的定义。而 my_source.c 中的实际定义确保了这些变量和函数在链接时有一个且只有一个定义。

函数参数压栈顺序 关于__stdcall和__cdecl调用方式的理解

在 C 和 C++ 语言中,函数调用约定(Calling Convention)定义了函数参数如何被传递、返回值如何处理以及栈如何被管理。__stdcall__cdecl 是两种常见的调用约定,它们在不同的操作系统和编译器中定义了不同的规则。

__cdecl 调用约定

  • 参数压栈顺序:在 __cdecl 调用约定中,函数的参数按照从右到左的顺序被压入栈中。这意味着在函数调用表达式中,最后一个参数是第一个被压入栈的,其次是倒数第二个参数,依此类推。
  • 清理栈:调用者(调用函数的代码)负责在函数调用后清理栈。这通常涉及到在函数返回后,将栈指针(SP)恢复到调用前的状态。
  • 返回值:返回值通常通过寄存器(如 eax 在 x86 架构中)传递回调用者。

__stdcall 调用约定

  • 参数压栈顺序:在 __stdcall 调用约定中,参数的压栈顺序与 __cdecl 相同,即从右到左。
  • 清理栈:与 __cdecl 不同的是,__stdcall 调用约定规定被调用的函数(函数本身)负责在返回前清理栈。这意味着函数需要在执行完成后,将栈指针恢复到调用前的状态。
  • 返回值:返回值的处理方式与 __cdecl 相同,通常通过寄存器传递。

理解这些调用约定的重要性

  • 跨平台兼容性:不同的操作系统和编译器可能使用不同的默认调用约定。了解这些约定有助于编写跨平台的代码。
  • 接口定义:当你在编写库或者接口时,明确指定调用约定可以确保其他开发者在调用你的函数时遵循相同的规则。
  • 性能优化:了解调用约定可以帮助你优化代码,特别是在性能敏感的应用中,正确的栈管理可以减少不必要的开销。

示例

假设有一个使用 __cdecl 调用约定的函数 foo,它接受三个参数:

int foo(int a, int b, int c) {
    // 函数实现...
}

在调用 foo(1, 2, 3) 时,参数会被按照 3, 2, 1 的顺序压入栈中。函数返回后,调用者需要清理栈。

如果函数 foo 使用 __stdcall 调用约定,调用和参数压栈顺序相同,但栈的清理工作由 foo 函数本身在返回前完成。

在实际编程中,通常不需要手动指定调用约定,除非你需要与特定的操作系统 API 或库函数兼容,或者在跨平台编程时需要特别注意。在大多数情况下,编译器会根据目标平台的默认规则来处理调用约定。

关于左右值引用

  1. 从性能上讲,左右值引用没有区别,传参使用左右值引用都可以避免拷贝。
  2. 右值引用可以直接指向右值,也可以通过std::move指向左值;而左值引用只能指向左值(const左值引用也能指向右值)。
  3. 作为函数形参时,右值引用更灵活。虽然const左值引用也可以做到左右值都接受,但它无法修改,有一定局限性。
int a = 5;
// int &ref = 5 // no! 左引用不能用常量(右值)来初始化
// const int &ref = 5; // ok const左值引用是可以指向右值的
int &ref = a; // ok

右值引用有办法指向左值吗

除了通过const左值引用来指向右值,还可以通过std::move将真正的右值引用来指向左值

int a = 5; // a是个左值
int &ref_a_left = a; // 左值引用指向左值
int &&ref_a_right = std::move(a); // 通过std::move将左值转化为右值,可以被右值引用指向
 
cout << a; // 打印结果:5

一个例子

#include <vector>
#include <string>
#include <iostream>
using namespace std;

struct Person
{
    string name;
    int age;
    //初始构造函数
    Person(string p_name, int p_age): name(std::move(p_name)), age(p_age)
    {
         cout << "I have been constructed" <<endl;
    }
     //拷贝构造函数
     Person(const Person& other): name(std::move(other.name)), age(other.age)
    {
         cout << "I have been copy constructed" <<endl;
    }
     //转移构造函数
     Person(Person&& other): name(std::move(other.name)), age(other.age)
    {
         cout << "I have been moved"<<endl;
    }
};

int main()
{
    vector<Person> e;
    cout << "emplace_back:" <<endl;
    e.emplace_back("Jane", 23); //不用构造类对象

    vector<Person> p;
    cout << "push_back:"<<endl;
    p.push_back(Person("Mike",36));
    return 0;
}
//输出结果:
//emplace_back:
//I have been constructed
//push_back:
//I have been constructed
//I am being moved.

C++中类型转换符

  1. static_cast:
    • static_cast 是一种静态类型转换,用于执行编译时的类型转换,主要用于类层次结构中的向上转型(子类指针向父类指针的转换)和向下转型(父类指针向子类指针的转换),以及进行基本数据类型的转换。
    • static_cast 在编译时进行类型检查,不提供运行时的类型检查,因此转换时会忽略指针或引用的动态类型信息,可能导致类型不安全的转换
    • 使用 static_cast 进行转换时,需要程序员保证转换的安全性,否则可能导致未定义的行为
  2. dynamic_cast:
    • dynamic_cast 是一种动态类型转换,用于执行运行时的类型检查和转换,主要用于类层次结构中的向下转型,并且只能用于具有虚函数的类
    • dynamic_cast 在运行时进行类型检查,可以在运行时判断指针或引用的动态类型是否兼容,如果转换不安全则返回空指针或抛出 std::bad_cast 异常。
    • dynamic_cast 可以用于安全地进行向下转型,因为它会检查对象的实际类型,如果转换不安全则返回 nullptr(对于指针)或抛出异常(对于引用)。
  3. const_cast:
    • const_cast 用于去除对象的 constvolatile 限定符,从而允许对对象进行修改或将对象视为非 const 类型。它主要用于转换指向常量对象的指针或引用。
    • 使用 const_cast 进行转换时,需要注意避免修改本来就不应该修改的对象,否则可能导致未定义的行为。
    • 示例:const_cast<int*>(const_ptr) 将指向常量整数的指针 const_ptr 转换为指向非常量整数的指针。
  4. reinterpret_cast:
    • reinterpret_cast 用于执行不同类型之间的强制转换,通常用于在不同类型之间进行位级别的重新解释。它是一种非常底层的转换,几乎不进行类型检查,因此潜在地非常危险,应该谨慎使用
    • reinterpret_cast 可以将任何指针类型转换为另一个指针类型,或者将任何整数类型转换为指针类型,反之亦然。
    • 示例:reinterpret_cast<int*>(ptr) 将指针 ptr 转换为指向整数的指针,或者 reinterpret_cast<float*>(&int_val) 将整数 int_val 的地址重新解释为 float 类型的指针。

C++函数压栈顺序

举例:

#include <iostream>
using namespace std;

int f(int n) 
{
	cout << n << endl;
	return n;
}

void func(int param1, int param2)
{
	int var1 = param1;
	int var2 = param2;
	printf("var1=%d,var2=%d", f(var1), f(var2));//如果将printf换为cout进行输出,输出结果则刚好相反
}

int main(int argc, char* argv[])
{
	func(1, 2);
	return 0;
}
//输出结果
//2
//1
//var1=1,var2=2

当函数从入口函数main函数开始执行时,编译器会将我们操作系统的运行状态,main函数的返回地址、main的参数、mian函数中的变量、进行依次压栈

当main函数开始调用func()函数时,编译器此时会将main函数的运行状态进行压栈,再将func()函数的返回地址、func()函数的参数从右到左、func()定义变量依次压栈

当func()调用f()的时候,编译器此时会将func()函数的运行状态进行压栈,再将的返回地址、f()函数的参数从右到左、f()定义变量依次压栈

从代码的输出结果可以看出,函数f(var1)、f(var2)依次入栈,而后先执行f(var2),再执行f(var1),最后打印整个字符串,将栈中的变量依次弹出,最后主函数返回。

也即除了函数形参是逆序入栈,其他是顺序进栈的。

函数调用过程中栈的变化如下:

  1. 保存返回地址:在调用函数时,当前函数的返回地址会被压入栈顶,以便在函数执行完毕后返回到调用函数的下一条指令处。
  2. 保存参数:接下来,调用函数的参数会依次入栈,从右往左入栈(这是 C++ 和许多其他编程语言的调用约定,称为 "右至左" 或 "从右到左" 的参数传递顺序)。
  3. 保存局部变量:如果被调用的函数内部有局部变量,这些变量会在栈上分配空间,并按照定义顺序入栈。
  4. 保存函数调用的上下文:一些编译器可能会将其他与函数调用相关的上下文信息(如寄存器值)也保存在栈上。

函数执行完成后,栈的变化如下:

  1. 恢复函数调用的上下文:如果在调用函数过程中保存了其他上下文信息,这些信息将会被恢复。
  2. 释放局部变量:函数内部的局部变量空间会被释放,栈指针会向上移动,回到调用函数时的状态。
  3. 弹出参数:函数参数会被依次弹出栈。
  4. 弹出返回地址:最后,返回地址会被弹出栈,并跳转到该地址执行,使程序流程回到调用函数后的下一条指令处。

所以,在函数调用过程中,返回地址先入栈,然后是函数参数。

GDB

GDB是一个由GNU开源组织发布的、UNIX/LINUX操作系统下的、基于命令行的、功能强大的程序调试工具。 对于一名Linux下工作的c/c++程序员,gdb是必不可少的工具;

  1. 运行命令
    run:简记为 r ,其作用是运行程序,当遇到断点后,程序会在断点处停止运行,等待用户输入下一步的命令。
    continue (简写c ):继续执行,到下一个断点处(或运行结束)
    next:(简写 n),单步跟踪程序,当遇到函数调用时,也不进入此函数体;此命令同 step 的主要区别是,step 遇到用户自定义的函数,将步进到函数中去运行,而 next 则直接调用函数,不会进入到函数体内。
    step (简写s):单步调试如果有函数调用,则进入函数;与命令n不同,n是不进入调用的函数的
    until:当你厌倦了在一个循环体内单步跟踪时,这个命令可以运行程序直到退出循环体。
    until+行号: 运行至某行,不仅仅用来跳出循环
    finish: 运行程序,直到当前函数完成返回,并打印函数返回时的堆栈地址和返回值及参数值等信息。
    call 函数(参数):调用程序中可见的函数,并传递“参数”,如:call gdb_test(55)
    quit:简记为 q ,退出gdb
  2. 设置断点
    break n (简写b n):在第n行处设置断点
    (可以带上代码路径和代码名称: b OAGUPDATE.cpp:578)
    b fn1 if a>b:条件断点设置
    break func(break缩写为b):在函数func()的入口处设置断点,如:break cb_button
    delete 断点号n:删除第n个断点
    disable 断点号n:暂停第n个断点
    enable 断点号n:开启第n个断点
    clear 行号n:清除第n行的断点
    info b (info breakpoints) :显示当前程序的断点设置情况
    delete breakpoints:清除所有断点:
  3. 查看源码
    list :简记为 l ,其作用就是列出程序的源代码,默认每次显示10行。
    list 行号:将显示当前文件以“行号”为中心的前后10行代码,如:list 12
    list 函数名:将显示“函数名”所在函数的源代码,如:list main
    list :不带参数,将接着上一次 list 命令的,输出下边的内容。
  4. 打印表达式
    print 表达式:简记为 p ,其中“表达式”可以是任何当前正在被测试程序的有效表达式,比如当前正在调试C语言的程序,那么“表达式”可以是任何C语言的有效表达式,包括数字,变量甚至是函数调用。
    print a:将显示整数 a 的值
    print ++a:将把 a 中的值加1,并显示出来
    print name:将显示字符串 name 的值
    print gdb_test(22):将以整数22作为参数调用 gdb_test() 函数
    print gdb_test(a):将以变量 a 作为参数调用 gdb_test() 函数
    display 表达式:在单步运行时将非常有用,使用display命令设置一个表达式后,它将在每次单步进行指令后,紧接着输出被设置的表达式及值。如: display a
    watch 表达式:设置一个监视点,一旦被监视的“表达式”的值改变,gdb将强行终止正在被调试的程序。如: watch a
    whatis :查询变量或函数
    info function: 查询函数
    扩展info locals: 显示当前堆栈页的所有变量
  5. 查看运行信息
    where/bt :当前运行的堆栈列表;
    bt backtrace 显示当前调用堆栈
    up/down 改变堆栈显示的深度
    set args 参数:指定运行时的参数
    show args:查看设置好的参数
    info program: 来查看程序的是否在运行,进程号,被暂停的原因。
  6. 分割窗口
    layout:用于分割窗口,可以一边查看代码,一边测试:
    layout src:显示源代码窗口
    layout asm:显示反汇编窗口
    layout regs:显示源代码/反汇编和CPU寄存器窗口
    layout split:显示源代码和反汇编窗口
    Ctrl + L:刷新窗口
  7. cgdb强大工具
    cgdb主要功能是在调试时进行代码的同步显示,这无疑增加了调试的方便性,提高了调试效率。界面类似vi,符合unix/linux下开发人员习惯;如果熟悉gdb和vi,几乎可以立即使用cgdb。

C++中的指针参数传递和引用参数传递有什么区别?底层原理你知道吗?

区别:

  1. 语法

    • 指针参数传递使用指针类型作为函数参数,通过指针来传递参数的地址。
    • 引用参数传递使用引用类型作为函数参数,直接传递参数的引用。
  2. 使用方式

    • 指针参数传递需要在函数内部使用指针操作符 * 来访问参数的值。
    • 引用参数传递在函数内部直接使用参数名来访问参数的值,不需要额外的操作符。
  3. 可空性

    • 指针参数可以是空指针(nullptr),即可以指向空地址,需要在函数内部进行空指针的检查。
    • 引用参数不能是空引用,必须传递一个有效的对象或变量的引用。
  4. 传递方式

    • 指针参数传递可以实现指针的重新赋值,即可以改变指针指向的对象。
    • 引用参数传递不会改变引用的绑定对象,而是直接操作原始对象。

底层原理上,指针和引用在传递参数时都是通过将参数的地址传递给函数来实现的。指针传递时,函数参数会创建一个指向原始对象的副本指针,而引用传递时,函数参数会创建一个对原始对象的引用。因此,指针传递需要通过解引用操作来访问原始对象,而引用传递直接访问原始对象,更直观和方便。

为什么模板类声明和定义一般都是放在一个h文件中

模板类通常放在头文件(.h 或 .hpp 文件)中的主要原因是模板类的定义和实现需要在编译时进行实例化,而模板的实现需要对模板的每个使用情况进行展开。将模板类的定义和实现放在头文件中可以确保在每个使用该模板的源文件中都能看到模板的定义,从而在编译时可以正确地实例化模板。

具体原因如下:

  1. 模板实例化时机:模板类的定义和实现都需要在编译时进行实例化,而不像普通的类或函数可以分离定义和实现。因此,将模板类的定义和实现放在头文件中可以确保在每个使用该模板的源文件中都能看到模板的定义,从而能够正确实例化模板

  2. 编译器实现:C++编译器通常在编译每个源文件时独立处理,编译器无法预先知道模板类在其他源文件中的具体实例化情况。因此,将模板类的定义和实现放在头文件中可以让编译器在需要时对模板进行实例化

  3. 链接器查找定义:将模板类的定义和实现放在头文件中可以避免在链接阶段出现找不到定义的情况,因为模板类的实例化是在编译阶段完成的,链接器只需要将已实例化的代码进行链接即可

总之,将模板类的定义和实现放在头文件中可以确保模板的正确实例化,并简化了模板的使用和管理。

cout和printf的区别

coutprintf 是 C++ 和 C 语言中用于输出信息的两种常见方式,它们有以下区别:

  1. 语法和用法

    • cout 是 C++ 标准库中的输出流(cout<<是类std::ostream的全局对象),使用 << 运算符来将数据插入到输出流中,例如 cout << "Hello, world!" << endl;
    • printf 是 C 语言中的输出函数,使用格式化字符串和可变参数列表来指定输出格式,例如 printf("Hello, world!\n");
  2. 类型安全性

    • cout 是类型安全的,它会根据插入的数据类型自动选择合适的输出方式,而且不会发生隐式类型转换的问题。
    • printf 不是类型安全的,因为它依赖于格式化字符串来指定输出格式,如果格式化字符串与参数列表不匹配,会导致未定义的行为或者输出错误。
  3. 可读性

    • cout 的语法更直观,易于理解和使用,特别是对于初学者来说。
    • printf 的格式化字符串可能会比较复杂,需要特别注意格式化字符串与参数列表的匹配关系,可读性相对较差。
  4. 性能

    • printf 通常比 cout 更快,特别是在需要大量输出时,因为 printf 是直接调用系统的底层函数来输出,而 cout 是基于流缓冲区的,可能涉及更多的复制和缓冲操作。

总的来说,对于 C++ 代码,推荐使用 cout 进行输出,因为它更安全、更易读,并且具有更好的类型检查机制。只有在特定的情况下,比如需要使用 C 标准库中提供的格式化功能时,才会使用 printf

声明和定义的区别

  1. 声明(Declaration)
    • 声明指的是告诉编译器某个变量、函数或其他实体的类型和名称,但不分配内存或实现功能的过程。
    • 对于变量,声明只是告诉编译器变量的类型和名称,并不会分配内存或赋初值。
    • 对于函数,声明只是告诉编译器函数的返回类型、名称和参数列表,并不会实现函数的功能。
    • 可以在多个文件中声明同一个实体,但只能在一个文件中定义。
  2. 定义(Definition)
    • 定义指的是为变量、函数或其他实体分配内存并指定其值或实现的过程。
    • 对于变量,定义包括声明并分配内存空间,可以为变量赋初值。
    • 对于函数,定义包括声明并实现函数的功能。
    • 一个实体在程序中只能有一个定义

举例来说,假设有一个整型变量 int x; 和一个函数 void func();

  • int x; 是对变量 x 的定义,分配了内存空间,但没有赋初值。
  • void func(); 是对函数 func 的声明,告诉编译器函数的返回类型、名称和参数列表,但不实现函数的功能。

总之,定义是为实体分配内存并指定其值或实现的过程,而声明是告诉编译器实体的类型和名称,但不分配内存或实现功能。

全局变量和static变量

在C++中,static变量和全局变量之间有一些重要的区别,特别是在函数中使用static修饰的变量和普通变量之间也存在一些区别:

  1. 全局变量
    • 全局变量是在函数外部声明的变量,在整个程序中都是可见的
    • 全局变量的生命周期从程序开始到程序结束,它的内存空间在程序启动时分配,在程序结束时释放。
    • 多个文件中可以通过extern关键字进行声明并共享使用
    • 全局变量的值可以被程序中的任何函数修改
  2. static变量
    • static变量可以在全局作用域或函数内部使用。
    • 在全局作用域中声明的static变量与普通全局变量类似,但作用域仅限于声明它的文件中,其他文件无法访问
    • 在函数内部声明的static变量,其生命周期和作用域仅限于该函数,但其内存空间在程序启动时分配,直到程序结束时才释放。
    • 函数内部的static变量只会在第一次调用函数时进行初始化,并且保留其值直到程序或函数的结束

static变量和全局变量的主要区别在于作用域和链接属性

普通函数和static函数

普通函数和static函数之间有几个重要的区别:

  1. 作用域

    • 普通函数的作用域是全局的,可以在定义它的文件之外被调用。
    • static函数的作用域被限制在定义它的文件内部,只能在同一文件内被调用。
  2. 链接属性

    • 普通函数具有外部链接(external linkage)属性,可以被其他文件中的函数调用
    • static函数具有内部链接(internal linkage)属性,只能在定义它的文件内部被调用,其他文件无法调用它
  3. 生命周期

    • 普通函数的生命周期与程序运行时间相同,从程序启动到结束
    • static函数的生命周期与程序运行时间相同,但它的作用域仅限于定义它的文件
  4. 名称冲突

    • 普通函数的名称可能会与其他文件中的函数名称冲突
    • static函数的名称仅在定义它的文件内部可见,不会与其他文件中的函数名称冲突。

static函数主要用于限制函数的作用域,使其只能在定义它的文件内部使用,以避免函数名称冲突和隐藏实现细节。

c++标准库

C++标准库是C++语言提供的一组标准化的工具和功能,包括各种类、函数和对象,用于实现常见的编程任务。C++标准库分为以下几个部分:

  1. 核心语言支持库(C++ Standard Library Core Language Support):提供了一些基本的功能,如数据类型、变量、类型转换等。

  2. STL(Standard Template Library):包括容器、算法和迭代器等,用于实现数据结构和算法。

  3. 输入输出库(Input/Output Library):提供了文件输入输出、流操作等功能。

  4. 数字库(Numerics Library):提供了数学函数、随机数生成器等。

  5. 字符串库(Strings Library):提供了字符串操作的函数和类。

  6. 容器库(Containers Library):提供了各种数据结构,如数组、链表、栈、队列、集合、映射等。

  7. 算法库(Algorithms Library):提供了各种常用算法,如排序、查找、计数、求和等。

  8. 功能库(Utilities Library):提供了一些通用的功能,如异常处理、类型识别、动态内存分配等。

C++标准库的组成部分在C++标准化过程中不断更新和扩展,以适应不断变化的编程需求。使用C++标准库可以提高程序的可移植性、可维护性和可扩展性,同时也可以减少代码量和开发时间。

C++ Lambda表达式

Lambda 表达式是 C++11 引入的一种匿名函数的语法,它可以用于创建临时的、局部的、一次性的函数对象,通常用于函数参数、算法等场景中,可以大大简化代码。Lambda 表达式的一般形式如下:

[capture](parameters) -> return_type { body }
  • capture:捕获列表,用于捕获外部变量。可以是值捕获 [=]、引用捕获 [&],或者特定变量的捕获 [var][&var] 等。
  • parameters:参数列表,类似于普通函数的参数列表。
  • return_type:返回类型,可以省略,编译器会根据返回语句自动推断。
  • body:函数体,类似于普通函数的函数体。

下面是一些 Lambda 表达式的例子:

// Lambda 表达式没有捕获外部变量,没有参数,返回值为整数
auto func1 = []() -> int { return 42; };

// Lambda 表达式,‘值传递捕’获外部变量 x,有一个参数 y,返回 x+y 的结果
int x = 10;
auto func2 = [x](int y) { return x + y; };

// Lambda 表达式,‘引用传递’捕获外部变量 x,有两个参数 x 和 y,返回 x+y 的结果
auto func3 = [&x](int y) { return x + y; };

// Lambda 表达式使用默认捕获方式,‘=’即按值捕获外部所有变量,有两个参数 x 和 y,返回 x+y 的结果
auto func4 = [=](int x, int y) { return x + y; };

Lambda 表达式的使用非常灵活,可以根据需要捕获外部变量、定义参数、指定返回类型和函数体。它在 C++ 中常用于 STL 算法、回调函数等场景,可以提高代码的可读性和简洁性。

memset

memset是C/C++标准库中的一个函数,用于将一段内存设置为指定的值。其函数声明如下:

void *memset(void *ptr, int value, size_t num);
  • ptr:指向要填充的内存块的指针。
  • value:要设置的值,以int类型表示,但实际上只取其低8位。
  • num:要设置的字节数。

memset函数将ptr指向的内存块的前num个字节都设置为value。这在某些情况下是非常有用的,比如在初始化内存或清零内存时。

但需要注意的是,使用memset时要确保内存块是简单类型的(如intchar等),而不是包含有指针、虚函数表等复杂类型的对象。因为对于复杂类型的对象,直接使用memset来初始化或清零内存会破坏对象的内部结构,可能导致程序运行时出现未定义的行为。memset是C/C++标准库中的一个函数,用于将一段内存设置为指定的值。其函数声明如下:

void *memset(void *ptr, int value, size_t num);
  • ptr:指向要填充的内存块的指针。
  • value:要设置的值,以int类型表示,但实际上只取其低8位。
  • num:要设置的字节数。

memset函数将ptr指向的内存块的前num个字节都设置为value。这在某些情况下是非常有用的,比如在初始化内存或清零内存时。

但需要注意的是,使用memset时要确保内存块是简单类型的(如intchar等),而不是包含有指针、虚函数表等复杂类型的对象。因为对于复杂类型的对象,直接使用memset来初始化或清零内存会破坏对象的内部结构,可能导致程序运行时出现未定义的行为。

  1. 有时候类里面定义了很多int,char,struct等c语言里的那些类型的变量,我习惯在构造函数中将它们初始化为0,但是一句句的写太麻烦,所以直接就memset(this, 0, sizeof(*this));将整个对象的内存全部置为0。对于这种情形可以很好的工作,但是下面几种情形是不可以这么使用的;
  2. 类含有虚函数表:这么做会破坏虚函数表,后续对虚函数的调用都将出现异常;
  3. 类中含有C++类型的对象:例如,类中定义了一个list的对象,由于在构造函数体的代码执行之前就对list对象完成了初始化,假设list在它的构造函数里分配了内存,那么我们这么一做就破坏了list对象的内存

ifdef endif

#ifdef和#endif条件编译

Debug和Release

在软件开发中,通常有两种主要的构建模式:Debug 模式和 Release 模式。它们之间的区别主要体现在以下几个方面:

  1. 编译优化:

    • Debug 模式: 编译器通常会关闭优化,生成的代码较为直观易读,便于调试。编译速度较快,生成的可执行文件大小较大。
    • Release 模式: 编译器会启用各种优化选项,以提高代码的执行效率和性能。生成的代码通常更加精简高效,但不利于调试。编译速度可能较慢,但生成的可执行文件大小较小,运行速度较快。
  2. 符号信息:

    • Debug 模式: 编译器会生成完整的符号信息,包括函数名、变量名等,以便于调试器进行源代码级的调试。
    • Release 模式: 编译器通常会剔除调试信息,以减小可执行文件的大小,提高运行效率。
  3. 优化等级:

    • Debug 模式: 通常使用低优化级别,以便于在调试过程中观察代码的执行流程和变量的取值情况。
    • Release 模式: 可以使用更高级别的优化,以提高程序的性能和效率。
  4. 错误检测:

    • Debug 模式: 通常会启用额外的错误检测功能,例如内存泄漏检测、数组越界检查等,以帮助开发人员及早发现并修复问题。
    • Release 模式: 可以关闭某些较为耗时的错误检测功能,以提高程序的运行速度。

总的来说,Debug 模式适合开发阶段,主要用于调试和定位问题;而 Release 模式适合发布阶段,主要用于生成最终的产品,具有更高的性能和更小的体积。

C

malloc的底层实现

参考:

malloc底层实现:brk、mmap

malloc 的底层实现原理可以从以下几个方面进行总结:

  1. 内存分配方式
    • 当程序需要内存时,malloc 会首先在内存池中寻找足够大的空闲内存块。如果找到合适的内存块,malloc 会分配内存并返回指针。
    • 如果内存池中没有合适的空闲内存块,malloc 会向操作系统请求更多内存。这通常通过系统调用 brkmmap 实现。
  2. brkmmap
    • 对于小于 128KB 的内存请求,malloc 使用 brk 系统调用来扩展数据段,通过移动 _edata 指针(指向堆段末尾的指针)来分配内存。
    • 对于大于 128KB 的内存请求,malloc 使用 mmap 系统调用来在堆和栈之间的文件映射区域分配内存。这种方式允许单独释放内存块,而不需要等待其他内存块的释放。
  3. 内存释放
    • 当使用 free 释放内存时,malloc 不会立即释放对应的虚拟内存和物理内存。对于使用 brk 分配的内存,只有在高地址内存释放后才能释放低地址内存。
    • 对于使用 mmap 分配的内存,可以单独释放,这有助于减少内存碎片。
  4. 内存碎片处理
    • free内存,并不是直接释放对应的虚拟内存和物理内存,特别是堆段中间的一部分的内存,因为内存需要等到高地址内存释放以后才能释放,由于_edata指针是唯一的。当最高地址空间的空闲内存超过128K(可由M_TRIM_THRESHOLD选项调节)时,执行内存紧缩操作(trim),真正的完成内存释放,否则只是让其成为空闲内存,可以重用那段内存
  5. 虚拟内存和物理内存
    • malloc 分配的内存首先是虚拟内存。在程序首次访问这些内存时,如果发生缺页中断,操作系统会分配相应的物理内存,并建立虚拟内存到物理内存的映射关系。
  6. 申请物理/虚拟内存
    • 无论是哪种申请方式,申请都是虚拟内存空间,只有在第一次访问已分配的虚拟地址空间的时候,发生缺页中断,操作系统负责分配物理内存,然后建立虚拟内存和物理内存之间的映射关系。

这些原理解释了 malloc 如何在不同的内存请求大小下选择不同的系统调用,以及如何管理内存分配和释放过程。这些机制确保了内存的有效利用,同时尽量减少内存碎片。

关于mmap

mmap技术是一种将文件或其他对象映射到进程虚拟内存中的方法(这个虚拟内存可以是指定的,也可以由操作系统自动选择),可以实现零拷贝和共享内存的效果。
也就是说mmap不仅限于在内存分配时,很多时候,在外设采集数据,应用层模块读取数据时,因为涉及数据传输,常见的有DMA,它通过在硬件设备到内存间频繁读取拷贝来传输数据,效率高但是对硬件设备还是有影响,对质量没那么好的硬件设备就不推荐,推荐的是采用mmap的方法,它将外设采集的数据映射到虚拟内存中,上层应用只需要读取虚拟内存中的数据即可,虽然它没有DWA那么快,但是它实现了0拷贝和内存共享。

补充部分:

  1. DMA(Direct Memory Access)和mmap是两种不同的数据传输机制。DMA是一种数据传输方式,它允许外设直接将数据写入或读取到内存中,而不需要CPU的干预,因此可以提高数据传输的效率。mmap则是一种内存映射方式,它将文件或设备映射到进程的虚拟地址空间中,使得应用程序可以直接访问文件或设备的内容,而不需要通过标准的读写接口。虽然在一些情况下可以使用DMA和mmap来实现相似的功能,但它们的本质不同。

  2. mmap在数据传输方面的优势主要体现在减少了数据的复制和移动,实现了零拷贝和内存共享。当应用程序需要频繁地读写文件或设备时,使用mmap可以避免将数据从内核空间复制到用户空间,提高了数据传输的效率。此外,mmap还可以将文件或设备的内容共享给多个进程,实现了进程间的数据共享。

  3. DMA在高性能要求和频繁数据传输的场景下更为常见,因为它可以通过直接访问内存来避免CPU的干预,提高数据传输的速度和效率。但是对于一些质量较差的硬件设备,可能会存在DMA操作不稳定或不可靠的问题。在这种情况下,使用mmap可以提供一种更为稳定和可靠的数据传输方式。

mmap和DMA是两种不同的数据传输机制,各有其适用的场景和优势。在需要频繁读写文件或设备、希望实现零拷贝和内存共享的情况下,可以考虑使用mmap来提高数据传输的效率和性能。

C语言检索内存情况 内存分配的方式

在 C 语言中,内存分配和管理是通过一系列标准库函数来实现的,主要包括以下几种方式:

C语言中内存分配方式有三种

  1. 静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序运行期间一直存在。例如全局变量,静态变量。

  2. 栈上分配。在执行函数时,函数内部变量的存储单元都在栈上分配空间,函数执行结束时自动释放这部分空间。

  3. 堆上分配,也叫做动态内存分配。程序在运行时用malloc或new申请任意多的内存,程序员需要手动free或delete释放内存。
    程序运行时的内存空间:

    • 栈区:由编译器自动分配释放,存放为运行函数而分配的局部变量、函数参数、返回数据、返回地址等。

    • 堆区:由程序员分配和释放。

    • 全局区(静态区):存放全局变量、静态数据、常量。程序结束后由操作系统释放空间。

    • 文字常量区:存储常量字符串。

    • 程序代码区:存放二进制代码。

内存检测和调试

  • 使用工具如 valgrindgdb 或编译器的特定选项(如 -fsanitize=address)可以帮助检测内存泄漏和其他内存相关的问题。

在 C 语言中,没有内置的函数或方法来直接检索内存使用情况。开发者通常需要依赖外部工具或编写自定义的内存管理代码来跟踪和监控内存分配和使用情况。

Makefile

一个案例

# 设置编译器
CC = g++
# 设置输出目标名称
OUTPUT = ffmpeg_muxer_h264_acc_ts
# 设置链接的库
LIBS = -lx264 -lpthread -lavcodec -lavdevice -lavfilter -lavformat -lavutil -lswresample -lswscale

# 默认目标,生成输出目标
all: $(OUTPUT)

# 生成输出目标的规则
$(OUTPUT): ffmpeg_muxer_main.o ffmpeg_video_queue.o ffmpeg_audio_queue.o ffmpeg_container.o
	$(CC) $^ -o $@ $(LIBS)

# 编译源文件生成目标文件的规则
%.o: %.cpp
	$(CC) -c $< -o $@

# 清理生成的目标文件和输出文件
clean:
	rm -f *.o $(OUTPUT)

其中执行make or make all效果一致,都是生成对应程序和.o目标文件,显示执行make clean会调用最后的clean规则,清除已经生成的对应程序和.o文件。

OUTPUT = menu
CC = g++
all: $(OUTPUT)
$(OUTPUT): menu.o music.o picture.o
	$(CC) $^ -o $@ 
%.o: %.c
	$(CC) - c $< -o $@
menu: menu.o music.o picture.o
	gcc -o menu menu.o music.o picture.o

menu.o: menu.c menu.h
	gcc -c menu.c -o menu.o

music.o: music.c
	gcc -c music.c -o music.o

picture.o: picture.c
	gcc -c picture.c -o picture.o
posted on 2024-03-28 19:10  DrizzleDrop  阅读(4)  评论(0编辑  收藏  举报