C++基础

目录

C++基础概念整理及提问

C++开发者应该掌握的能力

C++学习推荐路线

C++基础概念

函数

什么是函数(Function)?

函数和class和void有什么不同

一:函数常见样式

二:函数声明的意义

三:函数分文件编写的意义

四:值传递

五:是不是所有void开头的都是属于函数?

六:函数的调用

Struct结构体

1:Struct结构

2:结构体数组

3:结构体指针

4:结构体嵌套结构体

5:结构体做函数参数

结构体中const使用场景

C++指针

为什么需要指针?

1:C++ 中指针的使用及其意义

2:C++中,->的用法和意义是什么?

3:指针和 -> 的关系

4:指针运算符 &、* 和 -> 的具体示例

5:例子中的运行输出(示例地址,实际环境中地址会不同)

6:指针所占的内存空间大小

7:空指针和野指针

8:const修饰指针

9:指针和数组

10:地址传递

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

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

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

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

15:智能指针(Smart Pointer)和原始指针(Raw Pointer)的区别

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

基础问题提问 Part1

一:C++ 面向对象的构建能力

二:C++ 的基础特性

三:struct 的用法,以及为何用 struct 而不是 class?

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

五:什么是“语法糖”?

六:using namespace std; 什么时候不适用?

七:什么是自定义Node?

八:原生数组T a[N],std::array ,std::vector (动态数组)

九:std是什么意思?

基础问题提问 Part 2

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

二:while 和 if 的“条件”写法

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

四:Class和void的区别是什么?

五:介绍Json和XML

六:Encoding, UTF-8和Unicode是什么?UTF-8属于Unicode吗?

七:什么是左值引用?什么是右值引用?区别在哪

八:什么是完美转发?

九:什么是同一个作用域?

十:什么是全局作用域

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

十二:黑箱白箱/黑盒白盒


C++基础概念整理及提问

以下是我个人在学习C++的时候所遇到问题和概念的一个整理,以下是基础篇

C++开发者应该掌握的能力

作为一名 C++ 开发者,除了熟练掌握语言语法外,需要在以下几个方面具备扎实的能力和实践经验:


1. 语言基础与现代特性

  • C++ 核心语法:基本类型、指针与引用、作用域规则、函数与重载、构造/析构函数、拷贝/移动语义。
  • 面向对象编程:类/继承/多态、虚函数、纯虚接口、抽象类、访问控制。
  • RAII 与资源管理:RAII 原则、智能指针(std::unique_ptr、std::shared_ptr)、自定义 deleter。
  • 模板与泛型编程:函数/类模板、模板特化、模板元编程(constexpr、类型萃取)。
  • 现代 C++(C++11/14/17/20/23)
    • auto、范围 for、初始化列表
    • Lambda 表达式、函数对象
    • constexpr, noexcept
    • 移动语义(std::move、std::forward)
    • 并发库(<thread>、<mutex>、<atomic>)
    • 协程(C++20)与概念(Concepts)

2. 标准库与常用数据结构/算法

  • STL 容器:std::vector、std::list、std::deque、std::map、std::unordered_map、std::string 等。
  • 算法与迭代器:排序、查找、变换、std::algorithm 中的高阶算法(如 std::transform、std::accumulate)。
  • 容器适配器:std::stack、std::queue、std::priority_queue。
  • 复杂度分析:理解时间/空间复杂度,能够根据场景选择合适的结构。

3. 构建系统与工具链

  • 构建工具:CMake(最常用)、Makefile、Ninja 等;能编写跨平台的 CMake 脚本。
  • 编译器与链接:GCC/Clang/MSVC,不同编译器的宏、编译选项、ABI 差异;动态库(.dll/.so)与静态库(.lib/.a)的使用。
  • 版本控制:Git(分支管理、冲突解决、PR 流程),必要时也熟悉 SVN、Perforce。
  • 包管理:Conan、vcpkg 等 C/C++ 包管理工具。

4. 调试、测试与质量保证

  • 调试技巧:gdb、Visual Studio 调试器、断点/条件断点、调用栈分析、核心转储(core dump)。
  • 内存与线程检查:Valgrind(内存泄漏检测)、AddressSanitizer、ThreadSanitizer。
  • 单元测试:Google Test、Catch2 或 Boost.Test;TDD(测试驱动开发)流程。
  • 静态与动态分析:clang-tidy、cppcheck、Coverity,代码覆盖率工具(gcov、lcov)。
  • 代码规范与自动格式化:clang-format、CPPCHECK,遵循公司/社区的编码规范(如 Google C++ Style)。

5. 性能优化与底层原理

  • 性能分析:采样分析(Sampling Profiler)、Instrumentation(插桩)、Linux perf、Visual Studio Profiler。
  • 缓存与内存布局:理解 CPU 缓存(L1/L2/L3)、连续/分散内存访问的性能差异。
  • 并发与锁优化:避免锁竞争、读写锁(shared_mutex)、无锁编程(原子操作)。
  • SIMD 与向量化:使用编译器自动向量化、手写 SSE/AVX 指令加速关键代码。
  • 内存分配器:自定义内存池、对象池以减少频繁分配/释放开销。

6. 操作系统与网络编程

  • 操作系统机制:进程/线程模型、系统调用、文件 I/O、内存映射(mmap)。
  • Socket 网络:TCP/UDP 编程、阻塞/非阻塞 I/O、select/poll/epoll 机制;或使用 boost::asio。
  • 跨平台注意事项:字节序(Endian)、目录分隔符、动态库加载(dlopen/LoadLibrary)。

7. 常用第三方库与框架

  • Boost:常用模块(Boost.Asio、Boost.Filesystem、Boost.Serialization、Boost.Spirit)。
  • GUI/游戏引擎:Qt、MFC、wxWidgets;或 Unreal Engine、Unity Native Plugin 开发。
  • 序列化与消息:Protocol Buffers、FlatBuffers、JSON(RapidJSON)、XML(TinyXML2)。
  • 日志框架:spdlog、log4cplus、glog。

8. 软件工程实践与软技能

  • 设计模式:常见的 GoF 模式(工厂、单例、策略、观察者等)及在 C++ 中的应用。
  • 架构能力:模块化设计、分层架构、接口与实现分离(PImpl)、依赖注入。
  • 文档与沟通:撰写设计文档、代码注释;跨团队沟通与需求澄清。
  • 敏捷/DevOps:Scrum/Kanban 流程,持续集成与持续部署(CI/CD)。
  • 持续学习:关注 ISO C++ 标准进程、阅读优秀开源项目源代码(如 LLVM、Qt)。

小贴士

  • 从基础到进阶,建议先牢固掌握 C++14 核心特性,再逐步学习 C++17/20。
  • 通过参与开源项目、代码审查与实践,快速提升代码质量和架构视野。
  • 定期复盘自己的项目,从需求分析、设计、编码、测试到优化,形成闭环。

希望这些要点能帮助你规划 C++ 学习路线,全面提升从语言、工具到工程实践的综合能力。祝学习顺利!

C++学习推荐路线

1. 基础语法与流程控制

  • 变量与基本类型:int、double、bool、char、auto
  • 运算与表达式
  • 分支与循环:if/else、switch、for、while、do…while
  • 示例

2. 指针与引用

  • 指针(Pointer):地址运算符 &、解引用 *
  • 引用(Reference):语法糖,常用于函数参数
  • 示例

3. 函数与作用域

  • 函数重载默认参数
  • 内联函数 (inline)
  • 命名空间 (namespace) 防止命名冲突
  • 示例

4. 面向对象(OOP)

  • 类与对象:成员变量、成员函数
  • 构造函数 / 析构函数
  • 继承、多态(虚函数)
  • 封装与访问控制 (public/protected/private)
  • 示例

5. 模板与泛型编程

  • 函数模板类模板
  • 模板实参推导
  • 示例

6. 标准库(STL)

  • 容器:vector、list、map、unordered_map…
  • 算法:std::sort、std::find_if…
  • 迭代器:与容器结合使用
  • 示例

7. 现代 C++ 特性(C++11 及以后)

  • 智能指针:std::unique_ptr、std::shared_ptr
  • 移动语义:std::move、右值引用 T&&
  • Lambda 表达式
  • 范围 forautoconstexprenum class
  • 示例(Lambda)

8. 内存与资源管理

  • 动态分配:new / delete
  • RAII(Resource Acquisition Is Initialization)
  • 示例(RAII)

C++基础概念

函数

什么是函数(Function)?

    概念
函数是一段具有独立功能的可重用代码块,可以接收零个或多个参数,对其进行处理,并(可选地)向调用者返回一个值。

    组成要素

  1. 返回值类型:指明函数执行后返回的数据类型(没有返回值则用 void)
  2. 函数名:用来标识和调用该函数的名字。
  3. 参数列表:被调用时传入的变量列表,每个参数需指定类型。
  4. 函数体:由一对花括号 { … } 包围的语句块,完成具体操作。
  5. 返回表达式:通过 return 将值传递给调用者(如果返回类型为 void,则可省略)。

 

函数和class和void有什么不同

下面从“本质定义”、“用途”与“在代码中如何使用”三个维度,简单对比函数(Function)、类(class)和 void 三者的不同。

维度

函数(Function)

类(class)

void

本质定义

一段可复用的、具有输入(参数)和(可选)输出(返回值)的逻辑单元

用户自己定义的一种复合类型(Blueprint),封装数据和相关操作

一种特殊的“无类型”标识:
– 在函数返回值处,表示“无返回值”
– 在参数列表处(C 风格),表示“无参数”

用途

执行某项具体任务(计算、I/O、状态修改等),并可把结果反馈给调用者

描述并创建“对象”,管理状态(成员变量)与行为(成员函数)

修饰函数签名:
– void f(),告诉编译器该函数不返回任何值
– void* p,表示通用指针,指向未知类型

在代码中使用

cpp<br>int add(int a,int b){ return a+b; }<br>…add(1,2);

cpp<br>class Person{<br> public:<br> void greet();<br> private:<br> string name;<br>};<br>Person p; p.greet();

cpp<br>void sayHi(){ cout<<"Hi"; }<br>void foo(void); // C 风格,无参<br>void* buf; // 通用指针

与其它的关系

– 可以返回任何类型(包括自定义的类类型)
– 可以作为类的成员函数

– 可以包含多个成员函数(function)
– 可以用函数来操作其成员数据

– 不是一个可实例化的类型,只用在函数签名中表示“没有”
– 不能用 void x; 声明变量

核心区别:

  • 函数 是“做事情”的基本单元,封装了一段可执行的逻辑。
  • 则是“有状态、有行为”的自定义类型,用来创建对象并组织更复杂的程序结构。
  • void 只是一个修饰符号,表示“此处没有类型”或“此处不保留任何返回结果”,它本身并不承载逻辑或数据。

一:函数常见样式

函数一共就四种样式

 

二:函数声明的意义

问:函数声明的意义是不是就是:如果我没有声明的时候,我先定义一个函数,然后再main()中调用它是可以的。 但是如果我是先在main()中调用,再定义就会报错,但是如果我先声明,再main()中调用,再定义,就不会有问题了。

是的,你的理解基本没错。在 C++ 中,在使用(调用)一个函数之前,编译器必须知道它的签名(返回类型、函数名和参数列表)。函数声明(又叫“函数原型”)正是用来告诉编译器这些信息的。


为什么需要函数声明

  1. 编译器单遍扫描
    C++ 编译器在一趟编译流程里从上到下读取源代码,它在遇到函数调用时,必须已经知道这个函数的签名,才能:
    • 检查参数类型是否匹配
    • 确定调用约定(如何压栈、在哪里找符号)
  2. 两种常见写法
    • 定义在前:把整个函数体写在 main() 之前,这时编译器读到调用处时,已经“见过”函数的定义,自然能正常编译。
    • 声明在前,定义在后:在 main() 之前写一条函数声明,然后在 main() 之后或别的文件里再写函数体。

作用与好处

  • 可读性与结构:把函数声明放在头文件(.h)里,定义放在源文件(.cpp)里,可以更清晰地组织大型项目。
  • 接口与实现分离:调用者只需要看到声明(接口),无需关心实现细节;便于多人协作和模块化开发。
  • 跨文件调用:在不同的 .cpp 之间,函数声明(通常在公共头文件里)保证链接时能正确找到对应的实现。

总结

  • 函数声明 的意义就在于:让编译器在真正看到函数定义之前,就能知道如何正确调用它;
  • 你可以选择把定义写在调用之前,或者先声明再定义,两者都能让 main() 中的调用正常通过编译。

三:函数分文件编写的意义

将函数按功能或模块分散到不同的文件(通常是 .h/.hpp 头文件和对应的 .cpp 源文件)中,有助于树立清晰的项目结构与良好的开发协作习惯,其主要意义包括:

好处

  1. 调用者只需包含头文件,就能使用这些函数,无需关注内部细节。
  2. 实现更改(优化、修复)时,只要保持接口不变,依赖它的代码无需重新修改。

2. 提高可维护性与可读性

  • 将功能相近的函数组织到同一个模块/文件夹,例如 io_utils.cpp、string_utils.cpp,让项目结构一目了然。
  • 大文件拆分后,每个文件更短、更专注于一个责任,阅读和定位问题更快。

3. 支持“增量式编译”与缩短编译时间

  • C++ 编译是基于“翻译单元”(.cpp + 它包含的头文件)逐个编译的。
  • 当修改了一个源文件,只需要重新编译该文件并重新链接,而不必重新编译整个项目,显著节省时间。

4. 便于多人协作

  • 不同开发者可以同时在不同模块(文件)上并行工作,减少冲突。
  • 每个人只需关心自己负责的头文件接口和实现,不会频繁踩到他人代码。

6. 模块化与重用

  • 不同项目可复用已有模块,只需拷贝对应的头文件和编译好的库(或源文件)。
  • 设计良好的功能模块,更容易在后续项目中直接引入。

总结

将函数分文件编写是一种工程化管理手段,通过“接口声明 + 单元实现 + 模块化组织”,实现了:

  1. 解耦:调用者与实现分离
  2. 可维护:修改实现不影响使用方
  3. 高效编译:增量编译,缩短编译时间
  4. 协作友好:多人成果并行开发
  5. 信息隐藏:只暴露必要接口

合理拆分,有助于大中型 C++ 项目的可扩展性与长期维护。

四:值传递

  

五:是不是所有void开头的都是属于函数?

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

小结

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

六:函数的调用

C++ 中,函数定义类定义一般都写在 main() 函数之外(通常放在全局作用域或头文件里),而在 main()(或其他函数)里去 调用 这些函数/创建并操作这些类的对象。

下面给你一个完整的示例骨架,演示定义和调用是如何分开的:

  函数/类的“定义”(implementation)都放在 main 外,便于复用,也能在多个地方调用。

  调用”(calling)和对象实例化放在 main()(或其它函数)里,程序从 main() 开始执行。这样既结构清晰,又符合 C++ 的编译和链接规则。

调用函数,就是在代码中通过函数名加上“实参列表”来执行该函数体内的逻辑。下面分步骤说明:

     

总结:调用函数的核心就是用 函数名(实参列表),根据返回值决定是否赋值或忽略,然后函数体内的代码就会执行。

Struct结构体

1:Struct结构

1 结构体基本概念

结构体属于用户自定义的数据类型,允许用户存储不同的数据类型。

2 结构体定义和使用

语法: struct 结构体名 { 结构体成员列表 };

通过结构体创建变量的方式有三种:

struct 结构体名 变量名

struct 结构体名 变量名 = { 成员1值 , 成员2值…}

定义结构体时顺便创建变量

示例:

总结:

  • 定义结构体时的关键字是struct,不可省略;
  • 创建结构体变量时,关键字struct可以省略;
  • 结构体变量利用操作符 ‘’.‘’ 访问成员。

建议用方法一和方法二,方法三不建议。

2:结构体数组

作用:将自定义的结构体放入到数组中方便维护。

语法: struct 结构体名 数组名[元素个数] = { {} , {} , ... {} }

示例:

3:结构体指针

作用:通过指针访问结构体中的成员。

  • 利用操作符 ->可以通过结构体指针访问结构体属性。

示例:

总结:结构体指针可以通过 -> 操作符来访问结构体中的成员。

4:结构体嵌套结构体

作用: 结构体中的成员可以是另一个结构体。

例如:每个老师辅导一个学员,一个老师的结构体中,记录一个学生的结构体。

示例:

总结:在结构体中可以定义另一个结构体作为成员,用来解决实际问题。

5:结构体做函数参数

作用:将结构体作为参数向函数中传递。

传递方式有两种:

  • 值传递
  • 地址传递

示例:

总结:如果不想修改主函数中的数据,用值传递,反之用地址传递。

结构体中const使用场景

作用:用const来防止误操作。

示例:

C++指针

为什么需要指针?

问:为什么在C++中,要使用指针去间接修改,访问某些变量的地址或者是值,而不是直接去修改或者是访问那个变量本身呢?这不是多此一举吗?

在 C++ 里,指针(pointer)并不是“多此一举”,而是一种“间接访问”机制,它带来了很多灵活性能力,是语言设计中不可或缺的部分。下面从几个角度来说明为什么需要指针,以及它们能做哪些直接访问无法做到的事情。

6. 与底层 / C 接口互操作

  • 许多操作系统 API、第三方 C 库都要求传入指针或返回指针:
    • 文件读写:fread(void* buffer, size_t size, size_t count, FILE* fp)
    • Socket 编程:recv(int sock, void* buf, int len, int flags)

没有指针,你就无法调用这些接口。


7. 指针的“可变性”——空指针与指针运算

  • nullptr:指针可以表示“指向空”,通过检查指针是否为 nullptr 来判断某资源是否有效。
  • 指针运算:在数组遍历、内存块操作里,可以做 p + i、p[i],这是数组之于内存的自然映射。

为什么不用“直接”访问?

  • 直接访问”只适合固定、已知生命周期的变量。
  • 很多场景下,你需要在不同函数、不同作用域之间共享、延长生命周期、动态增减元素、实现多态……指针(或引用、智能指针)是唯一能达成这些目标的手段。

小结

指针核心作用就在于「间接引用」——让你能够:

  1. 在函数之间修改调用者数据;
  2. 动态分配和管理对象;
  3. 构建灵活的数据结构(链表、树等);
  4. 利用运行时多态;
  5. 避免大对象拷贝以提高性能;
  6. 与底层/ C 接口互操作;
  7. 使用空指针与指针运算实现更精细的内存控制。

这些能力在系统级、库级、框架级开发中十分关键,并不是多余的,而是 C++ 追求“零开销抽象”与“高效灵活控制”的必要代价。

1:C++ 中指针的使用及其意义

我的理解:指针就是一个地址,正常的指针变量名都是p (point)

如果把*p改为1000,那么a也会=1000

指针可以间接的访问这块内存并且可以进行一个读和写的操作,我们可以修改它或者去访问它

2:C++中,->的用法和意义是什么?

在 C++ 中,-> 是指针成员访问运算符(pointer-to-member operator),它的主要用途和意义有以下几点:

3:指针和 -> 的关系

4:指针运算符 &、* 和 -> 的具体示例

下面用一个自定义类来演示各种运算符的含义。

  • &alice:取得 alice 变量的内存地址,类型是 Person*。
  • Person *p = &alice;:声明一个指针 p,保存了 alice 的地址。
  • *p:解引用,得到指针指向的那个 Person 对象本身。
  • (*p).age = 31;:先解引用,再用点运算符修改成员。
  • p->name = "Alice Smith";:p->name 就是 (*p).name 的简写,既可读又可写。

通过上面例子,你可以看到:

  1. &:取地址,得到指针。
  2. *:解引用,用指针访问或修改实际对象。
  3. ->:当指针指向结构体/类时,直接访问它的成员,是 (*ptr).member 的快捷写法。

如果你还想了解更多(比如函数指针、指向成员函数的指针、智能指针重载 operator-> 等),请告诉我!

5:例子中的运行输出(示例地址,实际环境中地址会不同)

  第一行是 alice.print() 的输出。

  “alice 的地址” 与 “指针 p 的值” 两行打印的是同一个地址,说明 p == &alice。

  随后三行分别是通过 *p 修改后的输出、通过 p-> 改名后的输出,以及 q(动态分配的 Bob)的输出。

6:指针所占的内存空间大小

7:空指针和野指针

总结:空指针和野指针都不是我们申请的空间,因此不要去访问它

8:const修饰指针

9:指针和数组

 

10:地址传递

不想修改实参就用值传递,想修改实参就用地址传递

指针配合数组和函数案例

 

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

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

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

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

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

指针的作用主要在于:

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

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

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

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

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

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

推荐风格

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

小结

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

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

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

1. 原理:指针加法

假设有一个整型数组:

  • p 当前指向 arr[0]。
  • 指针加法:p + 1 会跳过一个 int 大小,指向 arr[1];
  • 一般地,p + i 就指向 arr[i](前提是 0 <= i < 5)。

2. 访问和修改

  • p[i] 就是 *(p + i) 的语法糖,用来访问第 i 个元素。
  • 同理,你可以写 *(p + i) = newValue; 或者 p[i] = newValue;,都能修改对应元素。

3. 小结

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

15:智能指针(Smart Pointer)和原始指针(Raw Pointer)的区别

 

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

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

为什么这样设计?

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

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

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

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

小结

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

    基础问题提问 Part1

    一:C++ 面向对象的构建能力

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

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

    二: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 等。

    三:struct 的用法,以及为何用 struct 而不是 class?

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

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

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

       常见习惯:

    • 简单的数据聚合(Plain-Old Data)或只包数据、少逻辑的类型常用 struct;
    • 需要封装、接口与实现分离、强访问控制的类常用 class。

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

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

      

    五:什么是“语法糖”?

    六: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;,以提升可读性、可维护性并防止名字冲突。

    七:什么是自定义Node?

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


    1. 为什么要自定义 Node?

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

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

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

      

    5. 小结

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

    八:原生数组T a[N],std::array<T, N>,std::vector<T>(动态数组)

     

    九: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
      • …等等

    基础问题提问 Part 2

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

    二:while 和 if 的“条件”写法

    • if (condition)
    • while (condition)

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

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

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

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

    小结

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

    四:Class和void的区别是什么?

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

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

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

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

    小结

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

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

    五:介绍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:当你需要严谨的结构校验、混合内容文档或标准化的企业级消息格式时。

    六: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 本身。

    七:什么是左值引用?什么是右值引用?区别在哪

    在 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&&:专门用来绑定右值(临时或可移动对象),核心在于移动语义完美转发,让资源管理更高效。

    八:什么是完美转发?

    “完美转发”(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)就能针对左值或右值各自优化或重载,达成“完美”转发。

    九:什么是同一个作用域?

    在 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 块”内。
    • 只有在同一个作用域中声明的名字,才互相可见、可重载(或隐藏)。

    十:什么是全局作用域

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

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

    总结:

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

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

    十一:除了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)里声明的名字也只在本文件可见,但依然是顶层作用域。

    小结

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

    十二:黑箱白箱/黑盒白盒

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

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

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

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

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

      评论
      添加红包

      请填写红包祝福语或标题

      红包个数最小为10个

      红包金额最低5元

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

      抵扣说明:

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

      余额充值