Skip to content

基本概念

指针的大小?

在 32 位操作系统中,指针的大小是 4 个字节 在 64 位操作系统中,指针的大小是 8 个字节

因为地址是 32 位的,8 位一个字节

但是,位数低的老 CPU 和嵌入式 CPU 情况不同

8086 就因为其分段的内存模型,引入了三种不同的指针:

  • Near,大小为 16 位
  • Far,大小为 32 位(段 + 偏移),不进行规范化,指向的数据结构不能跨段
  • Huge,大小为 32 位(段 + 偏移),有规范化:这种指针的算术运算需要特殊实现来支持跨段的大型数据结构

32位系统下 int, float, long 占多少字节?

https://zh.cppreference.com/w/cpp/language/types

什么情况下会使用静态变量?

  1. 静态全局变量和静态函数的作用域只在当前文件中,解决了文件之间的符号污染问题
  2. 局部静态变量,将栈变量生命周期延长到程序执行结束时。在离开当前作用域后,再次调用定义它的函数时,它又可继续使用,而且保存了前次被调用后留下的值。 这么做的用意在于,对于某些局部变量,我们可以保留并使用一些需要的信息,比如记录这个函数被调用了多少次。
  3. 类的静态成员可以实现多个对象之间的数据共享
  4. 类的静态函数和静态数据成员一样,它们都属于类的静态成员,而不是某个具体对象成员,因此,对于静态成员的调用不需要用对象名

讲一讲 extern "C"

extern "C" 在 C++ 中是一个特殊的链接规范,用于确保 C++ 代码可以与C代码进行互操作。以下是关于extern "C"的一些注意点:

  1. 名称修饰(Name Mangling):

    • 当 C++ 编译器编译函数时,为了支持函数重载(即允许多个函数有相同的名字但参数类型不同),它会对函数名进行所谓的“名称修饰”或“名称改编”。
    • C 编译器不支持函数重载,因此不进行名称修饰。
    • extern "C" 告诉 C++ 编译器不要对其内部的代码进行名称修饰。
  2. 用法: 如果你有一些 C 函数在 C++ 代码中需要使用(或反之),你可以使用 extern "C" 来确保链接时使用正确的函数签名。例如:

    extern "C" {
        void my_c_function(int x);
    }
    

  3. 头文件兼容性: 如果你有一个头文件,希望它可以同时被 C 和 C++ 代码所包含,你可以使用以下的结构:

    #ifdef __cplusplus
    extern "C" {
    #endif
    
    // 函数声明
    void my_c_function(int x);
    
    #ifdef __cplusplus
    }
    #endif
    

这样,当该头文件被 C++ 代码包含时,extern "C" 将被应用,而当被C代码包含时,它将被忽略。

  1. 注意事项:

    • 使用 extern "C" 声明的函数不能被重载。
    • C++ 的特性(如类、异常、函数重载等)不能放在 extern "C" 块中。
  2. 链接错误: 如果你忘记为C函数添加 extern "C" 声明,而试图在 C++ 代码中调用它,你可能会遇到链接错误,因为C++编译器会期望一个名称修饰后的版本的函数,而这个版本在链接时可能不存在。

  3. 不仅仅是函数: extern "C" 也可以用于全局变量,确保它们在链接时没有名称修饰。

讲一讲四大类型转换表达式

在 C++ 中,有四个专门的类型转换表达式,它们分别是:

  1. static_cast

    这是最常用的转换运算符,用于处理大部分的类型转换场景,但不进行运行时类型检查。它可以进行如下转换:

    • 基本数据类型之间的转换,例如 intfloatfloatint 等。
    • 指向父类和子类之间的指针或引用的转换。
    • 转换为 void 类型,例如,将任何类型的指针转换为 void*

    例如:

    float f = 3.14;
    int i = static_cast<int>(f);  // 浮点数到整数的转换
    

  2. dynamic_cast

    主要用于对象的多态类型转换。当使用该转换运算符进行向下转型(从基类指针或引用转到派生类指针或引用)时,会在运行时检查转换是否合法。这是唯一一个在运行时执行类型检查的转换运算符。

    它主要用于以下转换:

    • 通过指针或引用从基类转换到派生类。
    • 从基类转换回到它的任何派生类。

    注意:使用 dynamic_cast 进行转换,所涉及的类必须有虚函数,也就是说它们应该是多态的。

    例如:

    class Base { virtual void foo() {} };
    class Derived : public Base {};
    
    Base* b = new Derived();
    Derived* d = dynamic_cast<Derived*>(b);
    

  3. const_cast

    主要用于修改类型的 constvolatile 属性。它不能改变类型本身,只能改变类型的这些属性。

    例如,如果你有一个 const int 的指针,你可以使用 const_cast 来获取一个非 constint 指针。

    const int val = 10;
    int* modifiableVal = const_cast<int*>(&val);
    
  4. reinterpret_cast

    这可能是最危险的转换,因为它允许你进行任何指针或引用类型之间的转换,或任何整型到指针(或反向)的转换。这个转换提供了很少的类型安全,并且在使用时应该非常小心。

    例如:

    int i = 42;
    int* pi = &i;
    long long int address = reinterpret_cast<long long int>(pi);
    

这四个类型转换运算符提供了 C++ 中显式类型转换的机制。虽然 C++ 仍然支持旧的 C 风格的类型转换,但使用这些新的转换运算符更加安全和可读

const 和 define 区别?

  1. 定义方式:

    • #define 是一个预处理指令,它在编译前由预处理器处理。
    • const 是一个语言关键字,其定义的常量在编译时由编译器处理。
  2. 类型安全:

    • #define 只是简单地替换文本,不考虑类型。
    • const 定义的常量具有明确的类型,这使得 const 在类型安全性上优于 #define
  3. 存储:

    • #define 仅仅是一个文本替换工具,并不分配存储空间。
    • const 定义的常量通常需要存储空间(例如,对于全局或静态常量),除非编译器可以进行优化。
  4. 调试:

    • 使用 #define 定义的宏可能会在调试时带来问题,因为它们在预处理阶段就已经被替换掉了。
    • const 定义的常量更容易在调试中进行识别和跟踪。
  5. 作用域:

    • #define 没有作用域的概念,它的定义一旦出现,会持续到文件结束或者到 #undef 指令。
    • const 变量具有作用域和存储类别,可以是局部的或全局的。
  6. 其他特性:

    • #define 可以用于定义除常量之外的其他东西,如宏函数。
    • const 可以与其他C++特性一起使用,例如类成员、引用等。

const 的作用?

  1. 常量变量:你可以使用 const 来定义一个常量,这意味着一旦它被赋值,它的值就不能被改变。

    const int myConst = 10;
    

  2. 常量指针:你可以使用 const 定义指针,使得它指向的值或者它本身的地址不能被修改,具体取决于 const 的位置。

    • 指针指向常量(指针所指向的内容不能被修改,但指针可以重新指向其他地方):
      int x = 10;
      const int* p = &x;
      
    • 常量指针(指针本身的值,即它所指向的地址不能被修改,但所指向的内容可以被修改):
      int x = 10;
      int* const p = &x;
      
    • 常量指针指向常量(既指针所指向的内容也不能被修改,且指针不能重新指向其他地方):
      int x = 10;
      const int* const p = &x;
      
  3. 常量引用:通常用于函数参数,使得函数内部不能修改引用的变量。

    void myFunction(const int& x) {
        // x cannot be modified here
    }
    

  4. 常量成员函数:表示该成员函数不会修改类的任何成员变量。

    class MyClass {
    public:
        int getValue() const {
            return myValue;
        }
    private:
        int myValue;
    };
    

  5. 常量表达式:在编译时计算表达式的值,经常与 constexpr 关键字一起使用。

    constexpr int square(int n) {
        return n * n;
    }
    const int val = square(5);  // This will be computed at compile time
    

  6. 类对象为常量:如果一个类的对象被声明为 const,那么该对象的任何非常量成员函数都不能在该对象上调用。

    const MyClass obj;
    // obj.someNonConstFunction(); // This would be an error
    

指针和引用的区别?

引用和指针都是 C++ 中用于访问或修改其他变量的值的机制,但它们之间存在一些关键区别。以下是引用和指针之间的主要区别:

  1. 定义和初始化

    • 引用 必须在定义时初始化,并且一旦与某个变量绑定,就不能重新绑定到另一个变量。
    • 指针 可以在定义时不进行初始化,后续可以任意地修改它所指向的地址。
    int x = 10;
    int &ref = x;  // 引用初始化
    int *ptr;     // 指针定义但未初始化
    ptr = &x;     // 指针初始化
    
  2. 使用方式

    • 当你通过 引用 访问变量时,你直接使用引用的名字,就像使用普通变量一样。
    • 使用 指针 时,你需要使用 * 运算符来访问它所指向的值。
    ref = 20;  // 修改x的值为20
    *ptr = 30; // 修改x的值为30
    
  3. 空状态

    • 引用 不能为 nullptr 或空状态,它必须总是关联到一个合法的对象。
    • 指针 可以是 nullptr,表示它不指向任何对象。
  4. 修改性

    • 你不能修改 引用 以使其引用另一个对象。
    • 你可以随时修改 指针 以使其指向另一个对象或地址。
  5. sizeof运算

    • sizeof(ref) 返回引用所绑定的对象类型的大小。
    • sizeof(ptr) 返回指针变量的大小,通常与平台的地址大小有关(例如,在32位系统上为4字节,在64位系统上为8字节)。
  6. 类型安全

    • 引用在类型方面比指针更加严格。你不能无故地将一个类型的引用赋值给另一个不同的类型,除非涉及继承关系。
    • 指针有更多的灵活性,但也带来了更大的风险,例如 void* 可以指向任何类型。
  7. 主要用途

    • 引用 通常用于函数参数和返回类型,使得函数可以直接操作传入的对象,而不是拷贝。
    • 指针 有多种用途,包括动态内存分配、数组操作、数据结构(如链表和树)中的链接等。
  8. C++11 之后的引用
    https://www.bilibili.com/video/BV1CV4y1h74t/

int (*a)[10] 和 int *a[10] 的区别

int (*a)[10]int *a[10] 两者都与指针和数组有关,但它们的含义和用途完全不同。

  1. int (*a)[10]:

    • 这是一个指针,指向一个包含 10 个整数的数组。
    • a 是一个指针,它的类型是 "指向一个整数数组的指针",这个数组有 10 个元素。
    • 使用这种声明时,你可以让 a 指向一个已经存在的大小为 10 的整数数组。
    • 例如:
      int array[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
      int (*a)[10] = &array;
      
  2. int *a[10]:

    • 这是一个包含 10 个整数指针的数组。
    • a 是一个数组,它的类型是 "包含 10 个整数指针的数组"。
    • 使用这种声明时,每个数组元素都是一个指针,可以指向一个整数。
    • 例如:
      int x = 5;
      int y = 10;
      int *a[10];
      a[0] = &x;
      a[1] = &y;
      // ... and so on for the rest of the array
      

为了更好地理解这两者之间的区别,可以考虑如何通过它们访问或修改数据:

  • 对于 int (*a)[10](*a)[i] 访问数组的第 i 个元素。
  • 对于 int *a[10]a[i] 直接访问第 i 个指针,而 *(a[i])a[i][0] 访问该指针指向的整数。

string/char[]/char* 是不是以 '\0' 结尾

在 C 和 C++ 中,字符串通常有以下三种表示:

  1. std::string(在 C++ 中):

    • std::string 是一个 C++ 标准库类,用于表示和管理字符串。
    • 它内部管理一个字符数组,但这个数组不一定以 '\0' 结尾。
    • 当你使用 std::string::c_str() 方法时,它会返回一个以 '\0' 结尾的 const char*,这样可以确保与传统的 C 风格字符串兼容。
  2. char[](C 风格字符串数组):

    • 这是一个字符数组,当它被用作字符串时,通常以 '\0' (空字符) 结尾。
    • 当你初始化一个字符数组的时候,如 char str[] = "hello";,编译器会自动在末尾加上 '\0' 字符。
  3. char*(指向 C 风格字符串的指针):

    • 这是一个指针,它指向一个字符或字符数组的首地址。当它指向一个字符串时,那个字符串通常以 '\0' 结尾。
    • 需要注意的是,只有当这个指针确实指向一个以 '\0' 结尾的字符序列时,它才可以被当作一个完整的字符串。否则,许多处理 C 风格字符串的函数(如 strlenstrcpy 等)可能会出现未定义的行为。

总结:char[]char*(当它们用作 C 风格的字符串时)都应该以 '\0' 结尾。而 std::string 不一定以 '\0' 结尾,但当你需要一个以 '\0' 结尾的字符序列时,可以使用 std::stringc_str() 方法来获取。

NULLnullptr 的区别?

在 C++ 中,NULLnullptr 都可以用于表示空指针,但它们之间存在一些关键差异:

  1. 历史与兼容性:

    • NULL:它的使用在 C 和 C++ 中都有很长的历史。在 C++ 中,NULL 通常被定义为整数 0 或 ((void*)0)(取决于实现)。
    • nullptr:这是 C++11 中引入的新关键字,专门用于表示空指针。
  2. 类型:

    • NULL:在 C++ 中,NULL 实际上是整数类型(通常是 int)。这有时会导致某些意料之外的函数重载行为。
    • nullptr:它具有自己的类型——std::nullptr_t。这种类型只有一个可能的值,即 nullptr 本身,可以隐式地转换为所有指针类型,但不能隐式地转换为整数类型。
  3. 安全性:

    • 由于 NULL 是整数类型,使用它可能导致某些意料之外的函数重载解析或隐式转换,这可能不安全。
    • nullptr 提供了更强的类型安全性。例如,考虑以下重载:
      void func(int val);
      void func(char* ptr);
      
      如果你使用 func(NULL);,则会调用 func(int val); 而不是你可能期望的 func(char* ptr);。但如果你使用 func(nullptr);,则会正确地调用 func(char* ptr);

C++程序的编译过程?

  1. 预处理(Preprocessing):

    • 在这一步,预处理器处理所有以 # 开头的预处理指令,如 #include#define#ifdef 等。
    • #include 会将相关的头文件内容直接包含进源文件。
    • 宏会被展开,条件编译指令会根据条件选择性地包括或排除代码部分。
    • 这个步骤不会生成新的文件,除非你显式地请求编译器保存预处理后的文件。例如,使用 GCC 编译器时,你可以用 -E 选项来仅执行预处理并输出结果。

    输入文件:source.cpp

    输出文件(假设你选择保存预处理结果):source.i or source.ii

  2. 编译(Compilation):

    • 在这一步,编译器将预处理后的代码转换为汇编语言。
    • 这是将高级语言转换为更低级语言的过程,但还没有到机器代码的级别。

    输入文件:source.i or source.ii(或直接是源文件)

    输出文件:source.ssource.asm(取决于编译器和平台)

  3. 汇编(Assembly):

    • 汇编器将汇编语言代码转换为机器语言代码,结果是一个对象文件。
    • 对象文件包含了你的代码转换为的机器指令,但它还不是一个完整的程序,因为它可能还依赖于其他对象文件或库。

    输入文件:source.ssource.asm

    输出文件:source.osource.obj(取决于编译器和平台)

  4. 链接(Linking):

    • 在这一步,链接器取多个对象文件和库,将它们链接在一起,生成一个可执行文件或库文件。
    • 如果程序中使用了外部库或分多个源文件编写,那么链接器将确保所有的函数调用都正确地指向了其定义的地方。
    • 链接器还处理静态和动态库的依赖关系。

    输入文件:source.o (或其他对象文件和库)

    输出文件:通常是 a.out (在某些 UNIX 系统上) 或 source.exe(在 Windows 上)或其他可执行文件名

这就是 C++ 程序的标准编译过程。不同的编译器和平台可能会有细微的差异,但基本概念和步骤都是相似的。

动态链接和静态链接的区别?

  1. 定义:

    • 静态链接:在链接阶段,所有程序所需的库函数都被复制并集成到最终的可执行文件中。这意味着可执行文件是独立的,不依赖于外部的库文件。
    • 动态链接:可执行文件不包含实际的库函数,而是包含对动态链接库(如 .dll 在 Windows 或 .so 在 UNIX-like 系统上)的引用。当程序运行时,这些库被加载到内存中并与程序链接。
  2. 文件大小:

    • 静态链接:由于所有必要的库都被链接到可执行文件中,所以这些文件往往更大。
    • 动态链接:可执行文件通常较小,因为它只包含对库的引用而不是库的实际代码。
  3. 独立性:

    • 静态链接:生成的可执行文件是独立的,不依赖于外部库。这使得分发简单,因为用户只需要一个文件。
    • 动态链接:如果动态链接库在目标系统上缺失或版本不匹配,程序可能无法运行。
  4. 运行时开销:

    • 静态链接:运行时没有加载外部库的开销。
    • 动态链接:运行程序时,必须加载和链接动态链接库,这可能会增加一些开销。但多个程序可以共享同一动态库的单一内存实例。

讲一讲 C++ 符号表

https://zhuanlan.zhihu.com/p/600009670

函数重载的底层实现?

https://zhuanlan.zhihu.com/p/359466948