C++学习提问

前言

在当今软硬件开发日益融合的时代,C++仍以其高性能、可控性与丰富的特性,成为系统软件、游戏引擎、嵌入式设备等领域的主力语言。对于初学者而言,面对其复杂的语法规则、丰富的标准库和多代特性,往往容易迷失在指针运算、内存管理、模板元编程等概念之中;而对于有经验的开发者,也常因对新标准、新范式了解不足而错失效率与安全的提升机会。

本篇文章基于作者在自学与项目实践过程中所遇到的疑难与思考,将 C++ 的核心能力要素、基础概念与进阶疑问全面梳理,力求做到:

  • 体系化呈现:从“开发者应具备的能力”到“函数、结构体、指针、面向对象”等基础概念,再到“模板、并发、RAII、现代 C++ 特性”等进阶话题,层层递进、脉络清晰。

  • 问题驱动:汇总学习过程中常见的提问与误区,让读者在阅读时即能自我检测理解盲点。

  • 实用导向:结合示例与最佳实践,帮助你在今后的编码、调试与架构设计中少走弯路。

  • 学习路径建议:针对不同阶段的学习者提供循序渐进的学习路线与资源推荐,让你做到有所学、有所问、有所悟。

无论你是初涉 C++、渴望打好根基,还是已有多年经验、希望掌握现代标准与高阶技巧,相信这份“C++ 基础概念整理及提问”都能成为你手边的思维导图与复习锦囊。愿它助你构建扎实的语言底座,提升编程效率,走得更远、更稳、更优雅。

本篇为学习提问篇,另一篇概念介绍篇:C++基础概念-CSDN博客


目录

前言

基础概念提问

指针

1.为什么要先解引用 *p 再用 . 访问成员?

2.指针是不是主要用来访问数组?单个变量能否直接赋值?

3.为什么有的写 (*p).age,有的写 p->name?

4.指针访问数组时,只能访问和修改数组首元素吗?

5.智能指针(Smart Pointer)和原始指针(Raw Pointer)的区别

6.为什么指针只是存了数组第一个元素的地址?

基础问题提问 Part1

1.C++ 面向对象的构建能力

2.C++ 的基础特性

3.struct 的用法,以及为何用 struct 而不是 class?

4.如何在同一个项目中执行不同的main?除了把之前写的代码注释掉之外还有什么办法吗?

5.什么是“语法糖”?

6.using namespace std; 什么时候不适用?

7.什么是自定义Node?

8.std是什么意思?

9.所有void开头的都是属于函数?

基础问题提问 Part 2

1.for 语句的三个“部分”是否都必须写?

2.while 和 if 的“条件”写法

3.while vs. if,何时用哪一个?

4.Class和void的区别是什么?

5.介绍Json和XML

6.Encoding, UTF-8和Unicode是什么?UTF-8属于Unicode吗?

7.什么是左值引用?什么是右值引用?区别在哪

8.什么是完美转发?

9.什么是同一个作用域?

10.什么是全局作用域

11.除了main()前面的这一块属于全局作用域,其他的任何.h或者是.cpp文件中只要在任何函数、类或命名空间之外的那块最外层区域都属于全局作用域?

12.黑箱白箱/黑盒白盒

核心概念提问

函数提高

1. 什么是重载?

2. 重载函数里面的代码可以不一样吗?

3.示例中函数重载,如果把func(10)改为func(int a)会报错吗?

封装

1. 在封装类中使用void setName(std::string name),void setID(int StudentID),和我直接在类中直接使用std::string name和int StudentID有什么区别?

2. 访问权限:public,protected,private这三个都是围绕类来展开的吗,但是如果在全局作用域下面写这三个是不是就没有什么区别了?

C++进阶概念

1.什么是序列化?

2.C++11 到 C++20 各版本的主要差异

3.C++ STL 容器(Container)概述

4.构造函数(Constructor)与析构函数(Destructor)

5.构建工具:CMake的作用是什么

6.for (int k = 0; k < N; ++k) { cout << arr[k] << ' '; } cout << endl;和 for (int k = 0; k < N; ++k) { cout << arr[k] << ' '<

7.冒泡排序中,两层for中的原理(即外层for和里层for)

8.C++中一维数组和二维数组的介绍

9.使用STL容器表达二维数组(推荐)

10.什么是C++的多态?


基础概念提问

    指针

    1.为什么要先解引用 *p 再用 . 访问成员?

    • 运算符只能用于对象本身,不能直接作用于指针。

    • *p 是对指针的解引用,结果类型是它所指向的对象(即 Person&),这时就能用 . 来访问成员:

      (*p).age = 31;  // 先 (*p) 得到 Person 对象,再 .age
      

      -> 就是把这两步合并为一步,“取地址”与“解引用”合并成了直接访问成员。

    2.指针是不是主要用来访问数组?单个变量能否直接赋值?

    指针可以指向任何类型的对象——既可以是数组首元素,也可以是普通的单个变量、类实例、动态分配的内存等。

    如果你操作的是本地的单个变量,像:

    int age = 30;
    age = 31;  // 直接修改最简单
    

    那当然没必要用指针去间接访问。

    指针的作用主要在于:

    1. 动态分配:在堆上分配一块内存,必须通过指针来引用它;
    2. 函数间传递:如果想让函数内部修改调用处的变量,常通过指针参数或引用;
    3. 数据结构:链表、树、图等节点间通过指针连接;
    4. 多态:基类指针指向派生类对象,用于运行时多态。

    结论:只有在需要“间接引用”或“动态管理”时才用指针,否则直接变量名赋值最简单。

    3.为什么有的写 (*p).age,有的写 p->name?

    这两种写法其实等价,都是先对指针 p 做“解引用”(得到它指向的对象),再访问该对象的成员:

    (*p).age       // 先 (*p),再 .age
    p->age         // 箭头运算符正是 (*p).age 的“语法糖”
    

    为什么例子里同时出现两种?

    • (*p).age = 31; 强调了解引用再访问成员的过程,写法上要加括号,否者 *p.age 会被错误解析。
    • p->name = "Alice"; 就是箭头语法,编译器自动帮你做了解引用和再访问成员:
    p->name      // 等价于  (*p).name
    

    推荐风格

    • 日常中只要指针指向的是类/结构体,就统一用 ->,既简洁又避免括号。
    • 只有在讲解原理或演示 operator-> 重载时,才会写出 (*p).member。

    小结

    1. 指针并不限于数组,单个变量完全可以直接赋值,指针主要用于需要间接访问或动态管理的场合。
    2. (*p).member 和 p->member 是一回事,后者是对前者的简写(语法糖),平时习惯用 ->。

    4.指针访问数组时,只能访问和修改数组首元素吗?

    不是的,指针只是存了数组第一个元素的地址,但利用 指针运算(也就是“指针加减”)就能访问数组中的任意元素。

    1. 原理:指针加法

    假设有一个整型数组:

    int arr[5] = {10, 20, 30, 40, 50};
    int* p = arr;  // 等价于 int* p = &arr[0];
    
    • p 当前指向 arr[0]。
    • 指针加法:p + 1 会跳过一个 int 大小,指向 arr[1];
    • 一般地,p + i 就指向 arr[i](前提是 0 <= i < 5)。

    2. 访问和修改

    #include <iostream>
    using namespace std;
    
    int main() {
        int arr[5] = {10, 20, 30, 40, 50};
        int* p = arr;  // 指向 arr[0]
    
        // 访问
        cout << p[0] << endl;        // 10
        cout << p[2] << endl;        // 30
        cout << *(p + 4) << endl;    // 50
    
        // 修改
        p[1] = 25;                   // 改变 arr[1]
        *(p + 3) = 45;               // 改变 arr[3]
    
        // 验证
        for (int i = 0; i < 5; ++i) {
            cout << arr[i] << ' ';   // 输出:10 25 30 45 50
        }
        cout << endl;
    
        return 0;
    }
    
    • p[i] 就是 *(p + i) 的语法糖,用来访问第 i 个元素。
    • 同理,你可以写 *(p + i) = newValue; 或者 p[i] = newValue;,都能修改对应元素。

    3. 小结

    • 指针本身只保存一个地址(初始时通常是数组首元素的地址)。
    • 指针加法让你在内存中按元素大小“跳转”到数组的任意位置。
    • 这样就能访问和修改整个数组,而不仅仅是第一个元素。

    5.智能指针(Smart Pointer)和原始指针(Raw Pointer)的区别

     

    6.为什么指针只是存了数组第一个元素的地址?

    当你把一个数组名赋给一个指针变量时,发生的是“数组到指针”的衰变(decay)——它会变成第一个元素的地址:

    int arr[5] = {10,20,30,40,50};
    int* p = arr;    // 等价于 int* p = &arr[0];
    

    为什么这样设计?

    1. 类型匹配
      • int* p 表示“指向 int 的指针”。
      • arr 本身不是指针,但在大多数表达式里,它会被隐式转换为 “指向第一个元素的指针”——int*。
      • 这样 p = arr; 就是把 &arr[0](类型为 int*)赋给 p。
    2. 方便遍历
      • 数组在内存中是连续存放的:
    [10][20][30][40][50]
     ↑    ↑    ↑    ↑    ↑
    arr  arr+1 arr+2 arr+3 arr+4
    

    有了“首元素地址”后,通过指针加法(p + i)或下标语法(p[i])就能访问整个数组。

       区分“数组地址”与“元素地址”

    • arr 衰变为 &arr[0],类型是 int*。
    • 如果你写 &arr,它的类型是 int (*)[5](指向整个 5 元素数组的指针),虽然数值上也指向同一块内存的起始地址,但类型不同,做指针算术时就不一样。
    int (*pa)[5] = &arr;   // pa 的类型是 “指向 5 个 int 的数组” 
    // pa + 1 会跳过整个数组(5 个 int),而 p + 1 只跳过 1 个 int
    

    小结

    • 指针保存的地址类型 由它的声明决定:
      • int* p → 它存的是 “一个 int” 的地址,所以赋 arr 时得到的是 &arr[0]。
      • int (*pArr)[5] → 它存的是 “一个长度为 5 的 int 数组” 的地址,所以赋 &arr。
    • “数组名衰变为指针” 是 C/C++ 的语法规则,目的是方便你通过指针遍历和操作数组元素。

    基础问题提问 Part1

    1.C++ 面向对象的构建能力

    C++ 支持面向对象编程(OOP),主要体现在以下几方面:

    1. 封装(Encapsulation)
      • 用 class/struct 将数据与操作数据的函数(成员函数)组合在一起,并通过访问控制符 (public/protected/private) 隐藏内部细节。
    2. 继承(Inheritance)
      • 派生类继承基类的成员,可以代码复用和扩展。支持单继承和多重继承(需注意菱形继承问题)。
    3. 多态(Polymorphism)
      • 编译时多态:函数重载、运算符重载、模板实例化。
      • 运行时多态:基类指针/引用调用派生类的虚函数,借助虚函数表(vtable)实现动态绑定。
    4. 抽象(Abstraction)
      • 通过纯虚函数(virtual … = 0)定义接口,将具体实现留给派生类。

    2.C++ 的基础特性

    除了面向对象,C++ 语言本身还包含以下核心特性:

    1. 静态类型系统
      • 编译期类型检查,auto 推导与显式类型声明并存。
    2. 指针与引用
      • 直接操作内存地址(指针 T*),引用 T& 作为别名。
    3. 模板与泛型编程
      • 函数模板类模板,支持在编译期生成类型安全的代码。
    4. 标准模板库(STL)
      • 容器(vector、map 等)、算法(std::sort、std::find_if)、迭代器。
    5. RAII(资源获取即初始化)
      • 通过栈对象的构造/析构管理资源(文件、锁、内存),保证异常安全。
    6. 异常处理
      • try/catch/throw 提供统一的错误处理机制。
    7. 现代语法糖(C++11 及以后)
      • Lambda、智能指针(unique_ptr/shared_ptr)、右值引用 (T&&)、constexpr、enum class 等。

    3.struct 的用法,以及为何用 struct 而不是 class?

       在 C++ 中,struct 和 class 唯一的语法区别是默认的成员访问权限不同:

    • struct:默认公有,即不加访问说明符的成员都是 public。
    • class:默认私有,即不加访问说明符的成员都是 private。

       用法上它们功能完全相同,都可以定义成员变量、成员函数、继承、多态等。

       常见习惯:

    • 简单的数据聚合(Plain-Old Data)或只包数据、少逻辑的类型常用 struct;
    • 需要封装、接口与实现分离、强访问控制的类常用 class。
    struct S {
        int x;            // 等价于 public: int x;
    };
    
    class C {
        int x;            // 等价于 private: int x;
    public:
        void set(int v) { x = v; }
    };
    

    4.如何在同一个项目中执行不同的main?除了把之前写的代码注释掉之外还有什么办法吗?

    在一个 C++ 项目里,编译、链接阶段只能有一个有效的 main,否则会报“重定义 entry point”之类的错误。除了“注释掉”不想跑的那段 main,常用的几种做法是:

    1. 把不同的 main 放在不同的 .cpp 里,按需“排除”/“包含”文件

    • 实现方式

      • 每个 main 写在独立的源文件里:

        src/
          main1.cpp   // 包含 int main() { … }
          main2.cpp   // 包含 int main() { … }
          utils.cpp
          …  
        
         在 Visual Studio 里右键某个 .cpp → 属性 → 排除生成(Exclude From Build),就可以在不同配置下只编译你想跑的那个 main。
        
        优点:
        
        不改代码,只改项目属性;
        
        各个 main 之间互不干扰;
        
        缺点:多了几个配置管理。
        
        

     2. 用预处理器宏控制哪段 main 生效

    // 在 project Settings → C/C++ → Preprocessor → Preprocessor Definitions
    // 为不同的 Configuration 定义 RUN_MAIN1 或 RUN_MAIN2
    
    #if defined(RUN_MAIN1)
    int main() {
        // … main1 的逻辑 …
        return 0;
    }
    #elif defined(RUN_MAIN2)
    int main() {
        // … main2 的逻辑 …
        return 0;
    }
    #endif
    
    • 实现方式

      • 给不同的 Debug/Release 配置分别定义宏 RUN_MAIN1RUN_MAIN2

      • 编译时只有对应宏那段代码会被编译。

    • 优点:不用改文件排除设置,全部源文件都在;

    • 缺点:要手动切换预处理宏。

    3. 统一一个 “真” main,把其他入口做成普通函数

    // entrypoints.h
    int main1();
    int main2();
    
    // main1.cpp
    #include "entrypoints.h"
    int main1() {
        // … 
        return 0;
    }
    
    // main2.cpp
    #include "entrypoints.h"
    int main2() {
        // …
        return 0;
    }
    
    // main.cpp
    #include "entrypoints.h"
    int main() {
    #ifdef RUN_MAIN1
        return main1();
    #elif defined(RUN_MAIN2)
        return main2();
    #else
        // 默认或报错
        return 0;
    #endif
    }
    
    • 优点

      • 源文件都编译,只改 main.cpp 的切换逻辑;

      • 非常灵活,可在运行时或编译时切换。

    • 缺点:有个总 main,其它入口变成子函数,略微改动逻辑。


    4. 用 VS Solution 里多个 Project

    • 做法

      • 把每个 main 放到不同的 Project(比如 Proj_Main1Proj_Main2),都放在同一个 Solution 里。

      • 切换启动项目(右键 Project → Set as Startup Project)。

    • 优点:管理清晰,每个 exe 独立;

    • 缺点:项目数量多,维护稍繁琐。


    推荐

    • 如果只是偶尔切换,方法①“排除/包含文件” 最直观——不改代码,只在文件属性里勾选。

    • 如果需要在同一套代码里频繁切换入口,方法②“预处理宏+多配置”方法③“统一 main 调度” 比较灵活。

    • 而将每个入口做成独立 Project(方法④)适合长期并行维护多个可执行文件的场景。

    5.什么是“语法糖”?

    • 语法糖(syntactic sugar) 指编程语言中用来让代码更易读、更简洁的语法,它本质上并不增加新能力,而是由编译器转换成更底层、更冗长的等价代码。

    • 这里的 ptr->member 就是对 (*ptr).member 的语法糖:

      ptr->name    // 等价于  (*ptr).name
      ptr->func()  // 等价于  (*ptr).func()
      

    6.using namespace std; 什么时候不适用?

    在 C++ 里,using namespace std; 的确可以让你在后面写代码时不用每次都敲 std::,比如

    其背后有几个主要原因和注意事项:

    2. 可读性与维护性

    • 显式来源更清晰
      当你在代码里看到 std::string、std::move、std::map,立刻就能知道这是标准库的类型或函数;如果都省掉了前缀,读者必须去猜它们来自哪个命名空间。
    • 大规模团队协作
      在多人或跨模块协作时,大家更倾向于显式地写 std::,以防某人无意间引入了同名的自定义类型或函数,导致别处编译出奇怪的错误。

    3. 头文件(.h/.hpp)绝对忌用

    千万不要在头文件里写 using namespace std;,因为:

    • 它会“传染”所有包含这个头文件的源文件。
    • 你难以预料在别的源文件里会因此引入哪些名字冲突。

    如果真的想让头文件里少写几次 std::,也只能用更局部的 using std::string; 这样的 using 声明,并且也要非常小心只引入少量标识符。

    何时不该用 using namespace std;

    1. 任何头文件 —— 都不要写。
    2. 大型项目的全局或命名空间顶层 —— 会影响所有代码。
    3. 当你的工程里有多个命名空间,且有可能重名时 —— 绝对不要。

    总结

    • 小型、临时的示例代码(教学、Demo、快速原型)可以用 using namespace std;,让示例更简洁;
    • 生产级代码库/框架头文件 则应显式地用 std:: 或者局部 using std::xxx;,以提升可读性、可维护性并防止名字冲突。

    7.什么是自定义Node?

    “自定义 Node”就是自己用 struct 或 class 定义一个“节点”类型,用来在链表、树、图等数据结构里串联各个元素。它不是标准库里提供的,而是程序员根据需求手写的。


    1. 为什么要自定义 Node?

    标准库里有像 std::list 这样封装好的链表容器,但如果你想自己实现一个链表学习原理,或者要在一个特殊场景下改造链表,就需要自己定义节点类型。

    2. 常见的单向链表 Node 例子

    // 一个简单的单向链表节点
    template<typename T>
    struct Node {
        T val;        // 节点保存的数据
        Node* next;   // 指向下一个节点的指针
    
        // 构造函数:初始化 val,并让 next 指向空
        Node(const T& v)
          : val(v), next(nullptr)
        {}
    };
    

       val:存放用户关心的数据,比如 int、string、或自定义对象。

       next:指向链表中下一个节点,或为 nullptr 表示到达链表尾部。

    3. 如何用自定义 Node 搭建链表?

    #include <iostream>
    
    int main() {
        // 1) 创建三个节点
        Node<int>* n1 = new Node<int>(10);
        Node<int>* n2 = new Node<int>(20);
        Node<int>* n3 = new Node<int>(30);
    
        // 2) 串联它们
        n1->next = n2;
        n2->next = n3;
    
        // 3) 遍历并打印
        for (Node<int>* p = n1; p != nullptr; p = p->next) {
            std::cout << p->val << ' ';
        }
        // 输出:10 20 30
    
        // 4) 记得释放内存
        delete n1;
        delete n2;
        delete n3;
    }
    

    4. 扩展:双向链表节点

    如果要在链表中双向遍历,就再加一个 prev 指针:

    ​
    template<typename T>
    struct DNode {
        T val;
        DNode* prev;  // 指向前一个节点
        DNode* next;  // 指向下一个节点
    
        DNode(const T& v)
          : val(v), prev(nullptr), next(nullptr)
        {}
    };
    
    ​

    5. 小结

    • 自定义 Node”:自己写 struct Node{…}; 来描述“节点”是什么。
    • 用途广泛:学习链表原理、实现各种链式或树形数据结构。
    • 与 STL 区别:STL 已经封装好,你只要直接用;自定义则可完全控制底层逻辑。

    8.std是什么意思?

    在 C++ 中,std 是“standard”(标准)的缩写,用来表示标准库所定义的命名空间(namespace)。

    • 命名空间的作用是把一组相关的名字(类、函数、常量、对象等)组织在一起,避免与其他库或用户定义的名字冲突。
    • C++ 标准库里的所有内容都放在 std 命名空间下,比如:
      • 容器:std::vector、std::map
      • 输入输出:std::cout、std::cin
      • 字符串:std::string
      • 算法:std::sort、std::find
      • 智能指针:std::unique_ptr、std::shared_ptr
      • …等等

    9.所有void开头的都是属于函数?

    不是的,并非凡是以 void 开头的语句都是函数。在 C++ 中,void 本身只是一个“无类型”(no-type)的标识符,常见用法有几种,不同用法对应不同的声明:

    形式含义示例
    void f();函数声明:返回类型是 void 的函数 fcpp<br>void sayHello(); // 声明一个不返回值的函数<br>
    void* p;指针声明:指向任意类型的通用指针 pcpp<br>void* buffer = malloc(1024);<br>
    static_cast<void>(expr);丢弃值:把 expr 的结果转换成 voidcpp<br>static_cast<void>(someFunction());<br>
    在参数列表中 void无参函数(C 风格):表示此函数不接受任何参数cpp<br>void foo(void); // 在 C++ 中写成 void foo() 等价<br>
    template<typename T> using void_t = void;类型别名:元编程中占位用<type_traits> 中的 void_t

    怎么区分函数声明?

    只有“标识符后紧跟圆括号”的,才是函数(或函数指针):

    函数声明/定义

    void f();         // 声明
    void f() { … }    // 定义
    

    函数指针

    void (*fp)();     // 指向无参无返回函数的指针
    

    而像 void* p;void* foo(int);(返回 void* 的函数)就不是“无返回值的函数”,要注意 void*void 的区分。

    小结

    • void 单独 出现在函数名前,才表示“这是个不返回任何值的函数”。
    • 如果是 void*,那是“指向任意类型的指针”。
    • 而且函数声明/定义能被识别的关键在于——名字后面有 ()

    基础问题提问 Part 2

    1.for 语句的三个“部分”是否都必须写?

    2.while 和 if 的“条件”写法

    • if (condition)
    • while (condition)

    两者的括号里都只允许出现一个能转换为 bool 的表达式(条件),不能写三段式。常见写法包括:

    • 关系表达式: i < 5、ptr != nullptr
    • 逻辑表达式: x>0 && x<10、!flag
    • 函数返回值: if (read() > 0) …

    你可以把多个判断用逻辑运算符 &&、||、! 串起来,但整个 (...) 只有一个“条件表达式”。

    3.while vs. if,何时用哪一个?

    小结

    1. for(...) 括号里可写三段,也可各自省略,但之间的分号不能少。
    2. if(...) 和 while(...) 括号内都只要一个布尔表达式(可以通过 &&/||/! 组合)。
    3. if 用于做一次性的条件分支;while 用于在条件为真时重复执行循环体。

    4.Class和void的区别是什么?

    在 C++(以及大多数面向对象语言)里,class 和 void 是两个完全不同层面的关键字,分别用于类型定义类型说明

    一、class —— 定义“类”这种用户自定义类型

    1. 作用
      • class 用来声明一个“类”(class),它是 C++ 的核心——封装数据(成员变量)和行为(成员函数)的模板。
      • 通过 class,你可以创建任意复杂度的类型,实现封装(encapsulation)、继承(inheritance)、多态(polymorphism)等面向对象特性。

    二、void —— 表示“没有类型”或“空”

    小结

    • class:一种用户自定义的类型,用于封装成员变量和成员函数,是面向对象的基石。
    • void:一种类型说明符,表示“没有具体类型”——常用于标记“无返回值的函数”或“通用指针”。

    它们在语法上和语义上完全不冲突,经常在同一份代码里“组合”使用:用 class 定义类型,用 void 指定那些不需要返回值的成员函数。

    5.介绍Json和XML

    JSON(JavaScript Object Notation)

    定义

    • 一种轻量级的数据交换格式,以文本形式表示结构化数据。
    • 源自 JavaScript,但与语言无关,支持多数编程语言。

    特点

    • 简洁:使用少量符号,易读易写。
    • 数据模型:仅支持两种结构:
      • 对象(由 {} 包围,内部是键–值对,如 {"key": value})
      • 数组(由 [] 包围,内部是按顺序的值列表,如 [1,2,3])
    • 数据类型:字符串、数值、布尔、空、对象、数组。
    • 无模式(Schema-less):不强制定义字段类型或顺序。

    优缺点

    • 优点:体积小、解析快、与现代 Web/REST API 天然契合。
    • 缺点:不支持注释;对非常复杂的文档标记(如文档格式)不如 XML 灵活。

    应用场景

    • Web 前后端数据交换(AJAX/RESTful API)。
    • 配置文件(如 .eslintrc.json、.babelrc)。
    • NoSQL 数据库(MongoDB 存储的是 BSON,一种二进制 JSON 变体)。

    XML(eXtensible Markup Language)

    定义

    • 一种可扩展的、标记型的文本格式,用来表示复杂的文档和数据。
    • 由 W3C 标准化,强调可读性和可验证性。

    特点

    • 标签(Tag)结构:数据通过开始标签 <tag> 与结束标签 </tag> 标识,支持自定义标签名。
    • 层次化:天然树状结构,适合描述文档型数据(如网页、文档、配置)。
    • DTD/XSD:可配合文档类型定义(DTD)或 XML Schema(XSD)进行校验,保证结构与数据类型正确。
    • 可混合内容:标签内部既可含文本也可嵌套子元素。

    语法示例

    优缺点

    • 优点:标签自描述、支持注释、标准化的校验机制(XSD)、可混合文本和子元素。
    • 缺点:冗长(标签成对出现)、解析比 JSON 慢、对简单数据交换稍显臃肿。

    应用场景

    • 配置文件(如 Ant、Maven 的 pom.xml)。
    • 文档格式(如 XHTML、SVG、Office Open XML)。
    • 企业级 Web 服务(SOAP,多用 XML 构建请求与响应)。

    总结

    • 选择 JSON:当你追求轻量、快速、与现代 Web/REST 接口无缝集成时。
    • 选择 XML:当你需要严谨的结构校验、混合内容文档或标准化的企业级消息格式时。

    6.Encoding, UTF-8和Unicode是什么?UTF-8属于Unicode吗?

    UnicodeUTF-8 经常一起出现,但它们指的并不完全相同:


    1. Unicode:字符集与代码点

    • Unicode 是一个国际标准,定义了全球绝大多数书写系统中所有字符的“码点”(code point)
    • 每个字符被分配一个唯一的编号,记作 U+XXXX,其中 XXXX 是 16 进制数。例如:
      • U+0041 → 拉丁字母大写 A
      • U+4F60 → 中文“你”
      • U+1F600 → 表情“😀”
    • 注意
      • Unicode 本身只是一张“表”和一套规范,不规定具体在内存或文件里如何存储这些码点。
      • Unicode 的范围很大,目前可用的码点超过 140 000 个,分布在多个平面(Plane 0…Plane 16)。

    2. 字符编码(Encoding)

    要把这些“码点”保存到磁盘或发送到网络上,必须把它们转换成字节序列,这就叫字符编码。常见的 Unicode 编码包括:

    名称

    描述

    UTF-8

    可变长度编码:1~4 个字节表示一个码点。ASCII 范围(U+0000…U+007F)用单字节,高位字符用多字节。

    UTF-16

    可变长度:2 或 4 个字节。BMP(基本多文种平面;U+0000…U+FFFF)用 2 字节,超出部分用一对代理项(surrogate pair)。

    UTF-32

    固定 4 字节:每个码点都占用 4 字节。查表速度最快,但空间利用率最低。


    3. 为什么选择 UTF-8?

    • 向后兼容 ASCII:U+0000…U+007F 区间和 ASCII 完全相同,用单字节保存。
    • 节省空间:对于英文文本,几乎和 ASCII 一样小;对多数字母文字也比 UTF-32 紧凑。
    • 广泛支持:几乎所有 Web、Unix/Linux、现代编程语言和库都默认使用 UTF-8。
    • 自同步性:从任意一个字节读取即可判断这是一个新字符的开始还是续字节,便于流式处理。

    4. UTF-8 的编码规则示例

    字节数

    码点范围

    编码格式(二进制)

    1

    U+0000 … U+007F

    0xxxxxxx

    2

    U+0080 … U+07FF

    110xxxxx 10xxxxxx

    3

    U+0800 … U+FFFF

    1110xxxx 10xxxxxx 10xxxxxx

    4

    U+10000 … U+10FFFF

    11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

     

    5. 小结

    • Unicode:定义“字符 ↔ 码点”的映射,是字符的“编号表”。
    • UTF-8:把这些码点编码成 1~4 个字节的序列,是字符的“存储/传输格式”。

    在文件头看到 encoding="UTF-8",就是告诉程序:

    “后面的文本内容用 UTF-8 编码,把字节流解析成 Unicode 码点,再渲染成真正的字符。”

    UTF-8属于Unicode吗?

    UTF-8 并不是“Unicode”本身,而是 Unicode 标准定义的一种 字符编码(Transformation Format),全称叫 UTF-8(Unicode Transformation Format – 8-bit)。

    • Unicode:定义了一套“字符 ⇄ 码点”(code point)的映射表,比如 “A → U+0041”,“你 → U+4F60”。
    • UTF-8:是把这些 Unicode 码点转换成 1~4 字节 的二进制序列的方式之一。

    换句话说:

    1. Unicode 是一本“字符字典”,告诉你每个字符的编号(U+xxxx)。
    2. UTF-8 是把这些编号按照规则打包成字节流的“打包格式”。

    除了 UTF-8,Unicode 还定义了其他几种打包格式:

    • UTF-16(每个码点用 2 或 4 字节)
    • UTF-32(每个码点固定用 4 字节)

    所以,UTF-8 是实现 Unicode 字符集的三种主要编码之一,而不是 Unicode 本身。

    7.什么是左值引用?什么是右值引用?区别在哪

    在 C++ 中,“引用”(reference)就是一个别名——它让你可以给一个已存在的对象取一个新名字。C++11 在原有“左值引用”(lvalue reference)的基础上,又引入了“右值引用”(rvalue reference),它们最大的区别就在于绑定对象类别不同,以及由此衍生出的语义用途也不一样:

    一、左值引用(lvalue reference)

    • 语法:T&
    • 绑定规则:只能绑定到左值(lvalue),也就是具有持久存储、可取地址、有名字或能出现在赋值语句左边的表达式。
    • 典型用途
      • 传参用来修改调用者的变量:

     

    二、右值引用(rvalue reference)

    • 语法:T&&
    • 绑定规则:只能绑定到右值(rvalue),也就是临时对象、字面常量(如 20)、或 std::move(a) 之后返回的“可移动”对象。
    • 典型用途
      • 实现移动语义(move semantics):在拷贝代价昂贵的类型(如 std::vector)上,把资源(内存、文件句柄等)“偷走”而不是复制。
      • 完美转发(perfect forwarding):模板里用 T&& 接受任意值类别,再把它原样转发给另一个函数。

     

    小结

    • 左值引用 T&:给已有、持久的左值对象取别名,可以修改它。
    • 右值引用 T&&:专门用来绑定右值(临时或可移动对象),核心在于移动语义完美转发,让资源管理更高效。

    8.什么是完美转发?

    “完美转发”(Perfect Forwarding)是 C++11 引入的一种技术,允许函数模板把参数​“完完全全”​、​“原封不动”​地转发给另一个函数——既保持左值/右值的语义,也保留 const/volatile 限定。它的核心要素是:

    1. 当模板参数 T 用于 T&& 时,参数 arg 会根据调用时传入的是左值还是右值推导出不同类型:
      • 传入左值:T 推导为 引用类型(如 int&),此时 T&& 变成 int& &&,根据引用合成规则折叠为 int&。
      • 传入右值:T 推导为 非引用类型(如 int),此时 T&& 即 int&&。
    2. std::forward
      通过 std::forward<T>(arg),编译器会根据 T 的类型决定是把 arg 当作左值传递,还是把它当作右值移动传递。

    为什么需要完美转发?

    • 避免不必要的拷贝/移动:传统地将所有参数都当作左值或右值传递,可能引发多余的拷贝或移动。
    • 保留调用者本意:如果调用者传入的是右值(临时对象),你往往希望把它当作右值“移动”进去;如果传入的是左值,就应当保持左值语义。
    • 通用代码库:在编写通用的模板库(比如工厂函数、容器、算法等)时,完美转发可让内部调用尽可能高效、语义正确。

    小结

    1. 函数模板参数 用 T&& 来接收任意值类别的实参。
    2. 调用时 用 std::forward<T>(arg) 保持实参的左值/右值属性。
    3. 结果:被包装的函数(如 process)就能针对左值或右值各自优化或重载,达成“完美”转发。

    9.什么是同一个作用域?

    在 C++ 中,**作用域(scope)**就是一个名字(变量名、函数名、类型名……)被引入并且有效的区域。简单来说,作用域决定了“在哪儿可以访问这个名字”。


    常见的几种作用域

    1. 全局作用域(global scope)
      在所有函数和命名空间外部定义的名字,就属于全局作用域;从它被定义的点起,到整个程序结束,都可以访问。
    2. 命名空间作用域(namespace scope)
      在某个 namespace { … } 块里定义的名字,只在该命名空间内可见(当然也可以用 using 或显式命名空间限定来访问)。
    3. 类作用域(class scope)
      在类(或结构体)内部定义的成员函数、成员变量,都只在该类的内部或通过该类的对象/指针访问时可见。
    4. 函数作用域(function scope)
      函数内部定义的名字(例如参数名、局部变量名),只在函数体内有效。
    5. 块作用域(block scope)
      任何一对大括号 { … }(比如 if (...) { … }、for (…) { … })内部定义的名字,只在这一对大括号内可见。

    同一个作用域” 是什么意思?

    两个名字如果是在同一对大括号(或同一个命名空间块)里声明的,就说它们处于“同一个作用域”。

    • 如果你把两个函数的声明都写在全局最外层(或同一个 namespace 块里),它们就在同一个作用域,可以进行重载。
    • 如果一个函数声明在全局作用域,另一个在某个命名空间里,它们在同一个作用域;必须用 namespace::func 才能引用。

    小结

    • 作用域由花括号 {} 或 namespace … {} 划定。
    • “同一个作用域”即“同一对 {} 或同一 namespace 块”内。
    • 只有在同一个作用域中声明的名字,才互相可见、可重载(或隐藏)。

    10.什么是全局作用域

    在 C++ 中,全局作用域(Global Scope) 指的是在任何函数、类或命名空间之外的那块最外层区域。在全局作用域中声明的变量、函数或类型,具有以下特点:

    1. 可见性
      • 在同一翻译单元(单个 .cpp 文件)里,任何地方(只要不被更内层作用域屏蔽)都能访问全局作用域中的名字。
      • 如果在其他翻译单元里想使用,需要配合 extern 声明或把它放到头文件里并正确链接。
    2. 生命周期
      • 全局变量从程序启动时就被分配,直到程序结束时才释放,生命周期覆盖整个程序执行期。
      • 全局函数和类型自然“存在”于整个程序里,随时可被调用或实例化。
    3. 上面 g_counter、greetGlobal()、Person 都定义在全局作用域,所以无论在 main() 里,还是在其他非成员函数里,都可以直接访问它们。
    4. ​​​​​

    总结:

    全局作用域 是程序中最外层的区域,放在这里的声明,对整个翻译单元(甚至多个翻译单元,经 extern)都是可见的,并且其生命周期与程序同长。

    与之相对的 局部作用域 则只在小范围内可见且生存期有限。使用全局作用域时要谨慎,最好借助命名空间或文件作用域去控制可见性,避免命名冲突。

    11.除了main()前面的这一块属于全局作用域,其他的任何.h或者是.cpp文件中只要在任何函数、类或命名空间之外的那块最外层区域都属于全局作用域?

    对,全局作用域并不局限于 main() 前面那几行,而是指在 任何 源文件(.cpp)或头文件(.h)里,只要 该声明或定义写在:

    • 任意函数(包括 main)之外
    • 任意类 或结构体之外
    • 任意命名空间(显式的 namespace XXX { … })之外的顶层位置(也叫文件作用域/file scope),它就属于全局作用域,也就是全局命名空间(global namespace)。

    • g_value、foo()、main() 都是在全局作用域中声明/定义的。
    • Utils::helper() 在显式命名空间里,不属于全局作用域。
    • internalVar 虽然也是顶层声明,但因为加了 static,它的链接性(linkage)被限制在本 .cpp 文件;它仍然是文件作用域。
    • namespace { … }(unnamed namespace)里声明的名字也只在本文件可见,但依然是顶层作用域。

    小结

    • “文件作用域”=顶层作用域=全局作用域,只要不在任何函数、类或(显式)命名空间内部。
    • 放在头文件或源文件的最外层部分,都算全局作用域。

    12.黑箱白箱/黑盒白盒

    “黑箱”原本是控制论和系统工程里的术语,用在编程里也是类似的意思:把一个功能模块(比如一个函数、一个类或一个库)当成“黑箱”使用,意味着:

    1. 不关心内部实现
      • 你只知道它的输入是什么输出是什么,而不需要了解它内部是如何一步步运算、做分支、管理内存的。
      • 这样做可以减少认知负担,让你专注在整体架构和流程上。
    2. 只看接口
      • “黑箱”对外暴露的就是函数签名(参数列表和返回类型)、文档里描述的行为和副作用(比如修改了哪些状态、是否会抛异常)。
      • 调用者只要按照接口传入正确参数,就能得到正确结果。
    3. 好处:封装与复用
      • 封装:隐藏细节,内部可以随时优化或改写,只要保证接口兼容,调用它的代码不用修改。
      • 复用:当你习惯把某段逻辑封装成黑箱后,下次直接调用,避免重复造轮子。

    相对的,如果你打开这个“黑箱”——阅读甚至修改它的源码,理解其每行细节,就叫做“白箱”(White-box)或“透明箱”式使用。黑箱思想是软件工程里常用的抽象与封装原则,有助于降低系统复杂度。

    • 为什么两套说法都存在?
      • “箱”字稍显抽象,更常用在“黑箱操作”“黑箱算法”等泛化场景;
      • “盒”字则沿自“Black-box”直译,更常见于“黑盒测试/白盒测试”这类标准术语中。
    • 实际工程中怎么选?
      • 提到测试时,建议用 黑盒测试/白盒测试
      • 要表达“内部实现全然不露”的系统或组件,写成 黑箱系统/黑箱模型 更自然。

    总之,四个词汇指向的是同一对立概念,只是“盒”“箱”二字在不同语境下的偏好而已。

    核心概念提问

    函数提高

    1. 什么是重载?

    在 C++ 中,“重载”(Overloading)指的是在同一作用域内,同名的函数或运算符可以根据参数类型或参数个数的不同,绑定到不同的实现上。编译器在看到调用时,会根据传入实参的类型和数量,选择最合适的那个版本。


    一、重载函数/函数重载(Function Overloading)

    1. 定义
      同一个作用域里,允许多个同名函数只要它们的参数列表(类型、顺序、个数或顶层 const/引用修饰)不同,就构成重载。
    2. 规则
      • 重载函数的 返回类型 可以相同,也可以不同;但不能仅凭返回类型来区分重载。
      • 参数列表必须至少在一个参数的类型、顺序或个数上与其他重载版本有所区别。
      • 默认参数会参与匹配,但可能导致二义;一般尽量避免让默认参数引起调用歧义。
    3. 示例:
      // 版本 ①:两个 int 相加
      int add(int a, int b) {
          return a + b;
      }
      
      // 版本 ②:两个 double 相加
      double add(double a, double b) {
          return a + b;
      }
      
      // 版本 ③:三个 int 相加
      int add(int a, int b, int c) {
          return a + b + c;
      }
      
      // 使用
      int  i = add(1, 2);        // 调用①
      double d = add(1.2, 3.4);  // 调用②
      int  j = add(1, 2, 3);     // 调用③
      

    二、运算符重载(Operator Overloading)

    1. 定义
      通过给类提供特别的成员函数或全局函数,让内置运算符(如 +, -, [], ())作用于自定义类型时也能表现出自然语义。
    2. 语法
      // 作为成员函数
      ReturnType operatorOpSymbol(参数列表) { /*…*/ }
      
      // 或者作为全局(非成员)函数
      ReturnType operatorOpSymbol(LeftType lhs, RightType rhs) { /*…*/ }
      
    3. 示例

      struct Point {
          int x, y;
          Point(int x, int y): x(x), y(y) {}
      
          // 重载 “+” 运算符,绑定到成员函数
          Point operator+(const Point& rhs) const {
              return Point(x + rhs.x, y + rhs.y);
          }
      
          // 重载 “==” 运算符,绑定到全局函数
          friend bool operator==(const Point& a, const Point& b) {
              return a.x == b.x && a.y == b.y;
          }
      };
      
      // 使用
      Point p1(1,2), p2(3,4);
      Point sum = p1 + p2;         // 调用 operator+
      bool eq  = (p1 == p2);       // 调用 operator==
      

    三、为什么要重载?

    1. 接口统一
      同一个功能(比如 “加法”)对不同类型或不同参数个数,用同一个名字就能搞定,调用处更直观。
    2. 可读性强
      例如 a + b 比 add(a, b) 更像数学表达式,代码自然易懂。
    3. 可维护性
      当新增一种类型或参数变化时,只需要添加一个重载版本,无需修改调用处。

    四、注意事项

    • 不要过度重载,以免让调用者难以判断到底执行哪个版本;
    • 避免二义调用,尤其在存在默认参数和类型隐式转换时;
    • 不要仅靠返回类型重载,编译器无法区分;
    • 运算符重载要遵循“自然语义”:别让 + 做“删除”操作,否则容易造成使用混淆。

    通过函数重载和运算符重载,C++ 实现了编译时多态,让同一个接口在不同上下文下“表现不同”,提高了语言的灵活性和可读性。

    2. 重载函数里面的代码可以不一样吗?

    可以的,从语法和编译器的角度来看,重载函数的各个版本只要签名(参数列表)不冲突,函数体里做什么事情都是你自己的自由。把其中一个版本的实现改成乘法操作是完全合法的:

    注意事项

    • 语义一致性
      虽然编译器允许你这样做,但如果把名为 add 的函数改为做乘法,调用者就会感到困惑——建议保持函数名和实现逻辑的一致性,或者给乘法版本取个更恰当的名字(比如 mul)。
    • 重载解析
      重载解析只关心函数名和参数列表(签名),绝不看函数体。因此修改 return a + b → return a * b 不会影响编译器如何选择调用哪个版本。

    问:是不是函数的参数列表不冲突就行,多了也可以,比如一个有int a,另一个是int a, b ,c

    对,只要重载函数的“形参列表”互不冲突,编译器就能区分它们,无论是参数个数不同,还是参数类型不同,都可以构成重载。参数的名字并不影响重载,只看:

    1. 参数个数
    2. 参数类型(包括引用/指针、顶层 const/volatile)
    3. 参数顺序

    例如,你可以这样写:

    需要注意的地方

    • 返回类型不参与重载:你不能仅靠改变返回值类型来重载。
    • 默认参数可能引起二义:如果两个重载版本通过默认值能够互相匹配,就会变得模糊不清,编译器会报二义调用错误。

    3.示例中函数重载,如果把func(10)改为func(int a)会报错吗?

     

    封装

    1. 在封装类中使用void setName(std::string name),void setID(int StudentID),和我直接在类中直接使用std::string name和int StudentID有什么区别?

    1. 封装/信息隐藏

    • 用 setName()、setID()
      这些方法是类对外暴露的“接口”。类的内部(包括成员名字、类型、将来如何存储名字、ID)对使用者是隐藏的。如果你以后要
      • 在设置名字时做长度检查、过滤敏感字符
      • 或者把名字改成 wstring、ID 从 int 改成 long long
        你只需要改 setName()、setID() 的实现,调用者的代码不用改。
    • 直接用公有成员
      任何地方都可以直接写入、读出,完全没法在写入前或写入后做校验或额外操作,也没法在以后悄悄地更改成员类型或内部存储而不影响外部代码。

    解释:

    3. 日志、线程安全、二次加工

    • 日志:你可以在 setName() 里顺便打个日志,或通知 UI 刷新。
    • 锁(mutex):多线程环境下,加一把锁也只要在这两个方法里做。

    如果直接给成员设为公有,这些额外动作就无法集中管理。


    4. 接口稳定性

    把成员暴露出去,一旦它的名字或类型变了,所有直接访问它的代码都要改——极不利于后期维护。通过 setXXX()/getXXX(),你至少保留了“方法名”这个不变的接口层。


    小结

    • Setter/Get­ter 方法:✔ 良好封装、✔ 可控可验证、✔ 未来易扩展/重构
    • 公有成员直接访问:✘ 无校验、✘ 内外耦合、✘ 维护困难

    因此,推荐把成员变量设为 private,把真正对外的读写操作都放到 setXXX()/getXXX() 里。这样你的类才是真正符合面向对象设计原则的“黑盒”──使用者只能通过你提供的接口跟它打交道。

    2. 访问权限:public,protected,private这三个都是围绕类来展开的吗,但是如果在全局作用域下面写这三个是不是就没有什么区别了?

    是的,public、protected、private 这三大访问说明符只在 类(class/struct/union)内部 有意义,用来控制 成员 对外的可见性/可访问性。


    为什么全局作用域下写它们没用(甚至会报错)

    • 在 命名空间(包括全局命名空间) 或者函数外的顶层作用域,根本不存在「成员」的概念,只有自由(free)函数和全局变量。
    • C++ 标准并没有规定在类定义之外允许写访问说明符,绝大多数编译器都会直接报:

    vbnet

    error: access specifier ‘public’ not allowed here

    • 即使极端情况下某些编译器仅把它当成“标签”(类似 goto 标签),也对访问权限 一丁点 影响都没有——它不会拦截谁能访问哪个符号。

    全局/命名空间作用域下的可见性控制

    • 默认:全局函数和全局变量都是 外部链接(external linkage),任意翻译单元都能看到。
    • 想限制“只在本文件可见”,要用
      • static 关键字(C 风格)
      • 或者更现代的 匿名命名空间

    cpp

    namespace {

      void helper() { /* ... */ }  // helper 只在本 .cpp 文件可见

    }

    • 公有/私有/保护这些概念,只在 继承对象封装成员访问 的场景下才派得上用场。

    小结

    • public/protected/private 只能写在类/结构体/联合体的定义体内部,控制类的成员的访问。
    • 在全局(命名空间)作用域下写它们,要么编译错,要么被当成无意义的标签,并不会限制任何东西。
    • 若要在全局层面隐藏符号,用 static 或匿名命名空间来管理可见性。

      C++进阶概念

      1.什么是序列化?

      Thought for a few seconds

      序列化(Serialization)指的是将程序运行时内存中的数据结构或对象,转换成可存储或传输的格式(如二进制流、JSON、XML 等)的过程;与之相对的“反序列化”(Deserialization)则是把这些存储/传输格式还原回原始的数据结构或对象。


      1. 为什么要序列化?

      • 持久化存储:将对象状态保存到硬盘(文件、数据库),下次运行时读取还原。
      • 网络传输:在不同机器或进程间传输对象,需要把内存结构打包成字节流。
      • 跨语言/跨平台:不同语言或平台间交换数据,通过标准格式(JSON、Protocol Buffers)保证兼容。

           

      小结

      • 序列化 = 对象 → 字节流/文本
      • 反序列化 = 字节流/文本 → 对象
      • 选择合适的格式和库,既能保证性能,也能兼顾可维护性和跨平台兼容。

      2.C++11 到 C++20 各版本的主要差异

      建议

      • 从 C++11 的核心特性(lambda、移动语义、智能指针)入手;
      • 依次学习 C++14/17 的增强;
      • 最后重点掌握 C++20 的 Concepts、Ranges 和协程,这些是现代高质量库和框架(如 ranges-v3、asio)的基石。

      3.C++ STL 容器(Container)概述

      1. 序列式容器 Sequence Containers
        • std::vector:动态数组,连续内存,随机访问高效。
        • std::deque:双端队列,支持头尾高效插入/删除。
        • std::list:双向链表,中间插入/删除高效,但不支持随机访问。
        • std::array<T,N>:定长数组,编译期大小,栈上分配。
      2. 关联式容器 Associative Containers
        • std::set / std::multiset:基于红黑树的有序集合,元素唯一(或可重复)。
        • std::map / std::multimap:基于红黑树的有序键值对映射。
      3. 无序关联容器 Unordered Containers
        • std::unordered_set、std::unordered_map:基于哈希表,平均常数时间查找。
      4. 容器适配器 Container Adapters
        • std::stack:默认基于 deque 或 vector,只允许栈操作(LIFO)。
        • std::queue:队列(FIFO)。
        • std::priority_queue:优先级队列(最大堆或最小堆)。
      5. 算法与迭代器
        • <algorithm> 提供 sort、find、transform、accumulate 等通用算法,可与任何符合迭代器要求的容器配合使用。

      4.构造函数(Constructor)与析构函数(Destructor)

      构造函数(Constructor)

      • 作用:在对象创建时初始化成员、分配资源。
      • 类型
        • 默认构造 MyClass() { … }
        • 参数化构造 MyClass(int x, std::string s) { … }
        • 拷贝构造 MyClass(const MyClass& other)
        • 移动构造 MyClass(MyClass&& other)(C++11)
        • 委托构造(一个构造函数调用另一个)
      • 初始化列表:在冒号后初始化成员,比在函数体中赋值更高效。

      析构函数(Destructor)

      • 作用:在对象生命周期结束时释放资源(delete、文件句柄、网络连接等)。
      • 语法:~MyClass() { … },无参数,无返回值,不可重载或带参数。
      • 自动调用时机
        • 对象离开作用域时(栈对象)。
        • delete 一个 new 出来的对象时。
        • 智能指针析构时(unique_ptr/shared_ptr 引用计数为 0)。

      5.构建工具:CMake的作用是什么

      CMake 是一个跨平台的开源构建系统生成器,其核心作用包括:

      1. 生成平台/编译器无关的构建脚本
        • 在不同操作系统(Windows、Linux、macOS)和不同编译器(MSVC、GCC、Clang)之间,只需编写一套 CMakeLists.txt,CMake 就能生成对应平台下的原生工程文件(如 Visual Studio solution、Makefile、Xcode 工程等)。
      2. 统一管理项目结构与依赖
        • 通过 find_package()、find_library()、find_path() 等指令搜索并配置第三方库和头文件路径。
        • 使用 target_link_libraries()、target_include_directories() 等命令将依赖显式关联到目标(可执行文件或库)。
      3. 简化多模块/多平台编译流程
        • 支持按目录递归添加子项目(add_subdirectory()),轻松组织大型工程。
        • 提供配置选项(option())和变量缓存,方便打开/关闭不同功能(如启用测试、启用优化、切换后端库)。
      4. 自动化生成安装与打包规则
        • 内置 install() 命令可自动生成安装脚本,把编译产物、头文件和资源拷贝到指定目录结构。
        • 配合 CPack,可进一步自动创建 .zip、.tar.gz、.deb、.rpm、.msi 等打包文件。
      5. 支持编译器特性检测与条件编译
        • 通过 check_cxx_compiler_flag()、check_symbol_exists() 等检测目标编译器/平台支持哪些语言特性和库函数,动态设置编译选项或启用兼容性补丁。

      6.for (int k = 0; k < N; ++k) { cout << arr[k] << ' '; } cout << endl;和 for (int k = 0; k < N; ++k) { cout << arr[k] << ' '<<endl; } 有什么区别?

      总结

      • 如果想把数组元素都打印在一行,用写法 A,把 endl 放在循环外。
      • 如果要每打印一个元素就换行,用写法 B,把 endl 放在循环内。
      • 尽量少用 endl,必要时用 '\n' 替代('\n' 只是换行,不刷新缓冲区),以提高性能。

      7.冒泡排序中,两层for中的原理(即外层for和里层for)

      1. 外层 for (int i = 0; i < N-1; ++i)

      • 控制“趟数”:每一趟都要把当前「未排序部分」里最大的元素“冒”到它该去的尾部位置。
      • 共需要做 N-1 趟才能保证整个数组有序——最后一趟剩下两个元素比完就一定对了。

      经过第 i 趟冒泡之后,数组的末尾 i+1 个元素已经是最终排好序的最大元素了,所以下一趟就不必再碰它们。


      2. 内层 for (int j = 0; j < N-i-1; ++j)

      • 遍历未排序区域:下标从 0 比到 N-i-2,每次比较 arr[j] 和 arr[j+1]。
      • 交换逻辑:如果前面的元素更大,就和后面的交换——这样「大」的元素就往右跑了一格。
      • 这一趟跑完以后,最大的那个元素就会“冒泡”到 N-i-1 这个位置。

      举个小例子

      假设 arr = {5, 2, 7, 3},N=4:

      • 第 0 趟(i=0),j 从 0 到 2:
        1. 比较 (5,2) → 交换 → {2,5,7,3}
        2. 比较 (5,7) → 不交换 → {2,5,7,3}
        3. 比较 (7,3) → 交换 → {2,5,3,7}
          ⇒ 最大的 7 跑到末尾(索引 3)。
      • 第 1 趟(i=1),j 从 0 到 1(N-i-1=2):
        1. 比较 (2,5) → 不交换 → {2,5,3,7}
        2. 比较 (5,3) → 交换 → {2,3,5,7}
          ⇒ 第二大的 5 跑到索引 2。
      • 第 2 趟(i=2),j 只到 0(N-i-1=1):
        1. 比较 (2,3) → 不交换 → {2,3,5,7}
          ⇒ 剩下的也都在正确位置。

      这样三趟过后(N-1 = 3),整个数组就变成 {2,3,5,7},排序完成。


      小结

      • 外层 控制「要做几趟冒泡」,每做完一趟,未排序区域就少一个元素。
      • 内层 在每趟里「不断比较—交换」,把当前未排序区里的最大值一步步推到右边。

      这种「一趟确定一个最大值、总共做 N-1 趟」的思路,就是冒泡排序的精髓。

      8.C++中一维数组和二维数组的介绍

      下面从定义语法内存布局初始化与访问以及动态/STL 方式四个方面,分别介绍一维数组和二维数组。

       

         原生静态

      STL

       小结

      • 一维数组:T a[N],连续内存,随下标或指针算术访问。
      • 二维数组:T a[M][N],也是连续内存(行主序),可用 a[i][j] 或指针算术 *(*(a+i)+j)。
      • 动态需求:推荐用 std::vector 或手动 new + 扁平化方案,更灵活也可控制内存布局。

      9.使用STL容器表达二维数组(推荐)

      你可以先把一维数组包装成一个 std::vector<int>,然后按行切分成若干个 std::vector<int>,最后收集到一个二维 std::vector<std::vector<int>>。下面给出两种常见写法:

          

       

      10.什么是C++的多态?

      C++ 的多态(Polymorphism)指同一个接口在不同上下文下、表现出不同行为的能力。通俗地说,就是“同样的操作作用于不同的对象,可以产生不同的效果”。


      一、多态的两大分类

      类型

      特点

      实现机制

      编译时多态

      在编译阶段就能确定调用哪个函数;也称为 静态多态(Static Polymorphism)

      - 函数重载
      - 运算符重载
      - 模板(泛型编程)

      运行时多态

      在运行时根据对象的实际类型选择调用;也称为 动态多态(Dynamic Polymorphism)

      - 虚函数(virtual)与虚表(vtable)

        1. 函数重载(Function Overloading)

      #include <iostream>
      
      int add(int a, int b) {
          return a + b;
      }
      
      double add(double a, double b) {
          return a + b;
      }
      
      int main() {
          std::cout << add(1, 2)      << "\n";  // 调用 add(int,int)
          std::cout << add(1.5, 2.3)  << "\n";  // 调用 add(double,double)
          return 0;
      }
      

      2. 运算符重载(Operator Overloading)

      #include <iostream>
      
      struct Point {
          int x, y;
          Point(int x,int y):x(x),y(y){}
          // 重载 “+” 运算符,使 Point 相加语义更自然
          Point operator+(const Point& rhs) const {
              return Point(x + rhs.x, y + rhs.y);
          }
      };
      
      int main() {
          Point p1(1,2), p2(3,4);
          Point sum = p1 + p2;  // 等价于 p1.operator+(p2)
          std::cout << sum.x << "," << sum.y << "\n";  // 输出 4,6
          return 0;
      }
      

      3. 模板(Template)

      #include <iostream>
      
      // 泛型函数模板:支持任意加法操作
      template<typename T>
      T add(T a, T b) {
          return a + b;
      }
      
      int main() {
          std::cout << add<int>(1, 2)       << "\n";  // 3
          std::cout << add<double>(1.1, 2.2) << "\n";  // 3.3
          return 0;
      }
      

      三、运行时多态示例

      1. 基类和派生类 + 虚函数

      #include <iostream>
      
      // 抽象基类(也可不纯抽象)
      class Shape {
      public:
          virtual ~Shape() {}                // 虚析构保证派生类资源正确释放
          virtual void draw() const {        // 虚函数:允许派生类重写
              std::cout << "Drawing Shape\n";
          }
      };
      
      class Circle : public Shape {
      public:
          void draw() const override {       // override 可选,但推荐标明重写意图
              std::cout << "Drawing Circle\n";
          }
      };
      
      class Square : public Shape {
      public:
          void draw() const override {
              std::cout << "Drawing Square\n";
          }
      };
      
      int main() {
          Shape* s1 = new Circle;
          Shape* s2 = new Square;
      
          s1->draw();  // 运行时根据实际类型调用 Circle::draw
          s2->draw();  // 运行时根据实际类型调用 Square::draw
      
          delete s1;
          delete s2;
          return 0;
      }
      

      原理:每个含虚函数的类都有一张“虚表”(vtable),对象创建时携带一张指向该表的指针(vptr);运行时通过 vptr 找到对应函数地址并调用。

      四、为什么要用多态

      1. 可扩展性:新增派生类无需修改调用者代码,只要遵循同一接口;
      2. 代码复用:把共性逻辑放在基类,个性逻辑由派生类实现;
      3. 解耦合:调用者只与接口(基类)交互,不依赖具体实现。

      五、注意事项

      • 虚函数开销:运行时多态会有一次指针间接调用开销,且内存上多了 vptr;
      • 析构要虚:若通过基类指针删除派生对象,基类析构函数就必须声明为 virtual,否则会导致资源泄漏;
      • 不要滥用:编译时多态开销小、类型安全;运行时多态灵活但稍慢,视场景选择。

      总结

      • 编译时多态(重载、模板)在编译阶段决定调用,效率高;
      • 运行时多态(虚函数)在运行阶段决定调用,更灵活可扩展。
        两者结合使用,可以让 C++ 程序既高效又具有良好的可维护性和扩展性。
      评论
      添加红包

      请填写红包祝福语或标题

      红包个数最小为10个

      红包金额最低5元

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

      抵扣说明:

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

      余额充值