C++基础 (Updating)

📘 C++基础


第 1 章:C++介绍

1. 什么是 C++

① 一门比较流行的编程语言

  • TIOBE编程语言排行榜:https://www.tiobe.com/tiobe-index/

② C语言的扩展

  • 关注性能
    • 与底层硬件紧密结合
    • 对象生命周期的精确控制
    • Zero-overhead Abstraction
  • 引入大量特性,便于工程实践:
    • 三种编程范式:面向过程、面向对象、泛型
    • 函数重载、异常处理、引用等

③ 一系列不断演进的标准集合

  • 标准版本:
    • C++98/03,C++11,C++14,C++17,C++20,C++23 ?
  • 改进方向:
    • 语言本身的增强(如 Memory Model、Lambda Expression)
    • 标准库功能扩展(如 type_traitsrangesauto_ptr

C++ 标准的工业实现

  • 常见编译器:

    • MSVC / GCC / Clang
    • 每个编译器可能并不完全遵照标准
    • 不同的实现存在差异
      • 示例:https://godbolt.org/z/6hnPhY

注意

  • C++不能脱离具体上下文来讨论
  • 编写程序时应考虑性能与标准

2. C++的开发环境与相关工具

编译器

  • Visual C++ / GCC(G++) / Clang(Clang++)

集成开发环境(IDE)

  • Visual Studio / CodeLite / Code::Blocks / Eclipse ...

工具链建议


3. C++的编译 / 链接模型

简单加工模型的问题

image

  • 问题:无法高效处理大型程序
    • 加工耗时较长
    • 即使少量修改,也需要全部重新加工

分块处理:解决方案

image

  • 好处
    • 编译耗资源但一次处理输入较少
    • 链接阶段输入较多但处理速度较快
    • 有利于程序修改升级

分块概念的延伸

由 “分块处理” 衍生出的概念

  • 定义 / 声明
  • 头文件 / 源文件
  • 翻译单元:源文件 ➕ 所有直接/间接引用的头文件 ➖应忽略的预处理语句
  • 一处定义原则:
    • 程序级:一般函数
    • 翻译单元级:内联函数、类、模板

编译 / 链接四个阶段

1. 预处理(Preprocessing)
  • 将源文件转换为翻译单元的过程

    处理头文件展开、宏替换等

  • 防止头文件被循环展开:

    • ✅方法一:使用宏定义#ifdef

      这是最常见的写法:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      // File: student.h
      #ifndef STUDENT_H
      #define STUDENT_H

      struct Student {
      int id;
      char name[50];
      };

      #endif // STUDENT_H

      📌 说明

      • #ifndef STUDENT_H:判断这个宏是否没有被定义。
      • #define STUDENT_H:定义宏,防止后续再包含。
      • #endif:结束条件编译。

      如果这个头文件被再次包含,STUDENT_H 已经被定义过了,就不会再展开了。

    • #pragma once

      这是编译器提供的简洁方式:

      1
      2
      3
      4
      5
      6
      7
      8
      // File: student.h
      #pragma once

      struct Student {
      int id;
      char name[50];
      };

      📌 说明

      • #pragma once 告诉编译器只展开一次这个文件。
      • 写法更简洁,但不属于标准C++,不过目前主流编译器(如 GCC、Clang、MSVC)都支持。
2. 编译(Compilation)
  • 将翻译单元转换为相应的汇编语言表示

  • 编译优化示例:

    • https://godbolt.org/z/zh9aqx
  • 增量编译 vs 全量编译

    全量编译 (full compilation) 指的是:从头开始 完全编译 所有编译单元。 增量编译 (incremental compilation) 指的是:在项目已完成 全量编译 的基础上,修改后编译;编译器会根据被修改内容的依赖关系,仅重新编译最少的一部分编译单元。

3. 汇编(Assemble)
  • 将编译阶段生成的汇编代码(文本格式,.s 文件)转换为二进制的机器码(Object code)

    生成的文件称为目标文件(Object File),常见扩展名:

    • Linux / macOS 下:.o
    • Windows 下:.obj

    汇编后变成 .o 文件(已是二进制),但无法直接运行,需要链接阶段生成可执行文件。

4. 链接(Linking)
  • 将多个目标文件.o / .obj)与所需的库文件(静态库 .a / .lib,动态库 .so / .dll)组合在一起,生成最终的可执行文件(Executable File)。

合并多个目标文件,关联声明与定义。这一阶段相当于把各个编译好的“积木块”拼装成一个完整的程序。

  • 三种连接(Linkage)类型:
    • 内部连接(Internal Linkage)
    • 外部连接(External Linkage)
    • 无连接(No Linkage)
  • 常见链接错误:找不到定义

编译链接可能产生的问题

  • C++ 的编译 / 链接过程是复杂的,预处理、编译、汇编与链接任一阶段都可能出错
  • 编译可能产生警告、错误,都要重视

小结

  • C++ 是一门注重性能的程序设计语言
  • C++ 的标准经历了一系列的衍化,还在不断发展
  • 标准具体实现之间存在差距
  • C++ 源程序转换成可执行文件是相对复杂的过程,主要包含预处理、编译、汇编、链接等阶段,每一阶段都可能引入错误

附:与底层硬件紧密结合 & 对象生命周期控制 & 零成本抽象补充(Zero-Overhead Abstraction)

  • 与底层硬件紧密结合:https://godbolt.org/z/xPq6e

  • 对象生命周期的精确控制:

  • Zero-Overhead Abstraction:

    • 不需要为没有使用的语言特性付出成本
      • 虚函数
      • https://godbolt.org/z/fq66hM
    • 使用了一些语言特性 ≠ 付出运行期成本:https://godbolt.org/z/Pv9bWj

第 2 章:C++初探

1. 从 Hello World 谈起

🌟函数(Function)

  • 定义:一段能被反复调用的代码,可以接收输入,进行处理,并(或)产生输出。

  • 组成

    • 返回类型:表示函数返回结果的类型,可以为void

    • 函数名:用于函数调用。

    • 形参列表:表示函数接收的参数类型,可为空、可为 void、可以无形参。

      未命名参数的作用

      1
      void fun(const char* pInfo, int);
      1. 表示参数存在但暂时不使用(开发者想保留这个参数的接口形式,但目前没打算在函数体内使用它)
      2. 保持函数签名一致(在一些框架或库中,函数签名必须完全一致,即使某些实现中参数没用也不能省略)
    • 函数体:包含具体执行逻辑的语句块。

🌟main 函数

  • 特殊的函数,作为整个程序的入口。

  • 形参列表可以为空

  • 返回类型为 int,表示程序的返回值,通常使用 0来表示程序正常结束。

    在 C++ 中,除了void类型外,所有函数如果声明了返回类型(如intdouble等),都必须通过return返回一个相应类型的值;否则将导致编译错误。然而,main函数是个特例,尽管其返回类型是int,即使没有显式写出return语句,C++ 标准也会默认补上一句return 0;,表示程序正常结束。

    1
    2
    3
    4
    5
    6
    7
    int main() {
    return 0;
    }

    int main() {
    // 没有 return,也合法,等价于 return 0;
    }

    C++ 是区分大小写的,在 C++ 中,main 函数必须全小写写作 main,这是标准规定的函数名,不能写成 MainMAIN 或其他形式,否则程序无法作为入口点被编译器识别,导致编译错误或运行失败。

    另外,在 C++ 标准中main 函数的返回类型必须是 int。这是由标准明确规定的,因为操作系统通常依赖 main 函数的返回值来判断程序是否成功执行。

    ⚠️ 注意:

    • 某些编译器可能允许 void main() 通过编译,但这不符合 C++ 标准,是非标准用法,应该避免使用。
  • C++ 标准允许的 main 函数形式有两种:

    1
    2
    3
    4
    5
    6
    7
    8
    int main() {
    // 无参数形式
    return 0;
    }
    int main(int argc, char* argv[]) {
    // 有参数形式,可接收命令行参数
    return 0;
    }
    • 第一种是不带参数的形式,适用于不需要命令行输入的程序。

    • 第二种是带参数的形式,argc 表示参数个数,argv 是参数数组,常用于处理命令行输入。 【argc 和 argv 只是约定俗成的写法,我们只需要保证这两个参数的类型符合标准即可】

    • 除这两种以外的其他的形式不符合 C++ 标准的签名,可能无法正常运行

内建类型(Built-in Types)

​ 内建类型(也称基本类型)由 C++ 语言标准指定,内置于编译器中。在 C++ 中,“类型”并不是计算机硬件本身的概念,而是由编程语言引入的抽象,用于描述和管理数据的属性。类型的主要作用是赋予一段内存以具体含义,明确数据的形状、大小、可能的操作以及在程序中的用途,从而帮助编译器进行类型检查、内存管理和函数解析等工作。

​ 另外,在 C++ 中,变量所携带的数据实际上是保存在系统的内存中的。从硬件角度来看,内存就是无差别的一个序列,每一个序列里包含 n 个单元,每个单元能保留一个字节。C++ 中的每个变量都有指定的类型,该类型决定了变量可以存储的数据种类以及变量在内存中所占用的空间大小。

  • 为一段存储空间赋予实际的意义,如:
    • int:整型
    • byte:字节型(C++ 中并非常用)
    • 更多如 charfloatdoublebool

语句

语句:表明了需要执行的操作

  • 表达式 + 分号的语句:例如 a = b + 1;
  • 语句块:使用 {} 包裹的多条语句
  • ifwhilefor 等语句

注释(Comment)

注释:会被编译器忽略的内容

  • 用于编写代码说明或屏蔽某些代码
  • 两种注释形式:
    • 单行注释://
    • 多行注释:/* ... */

2. 系统 I/O

iostream:C++标准IO库

  • 用于程序与用户之间的交互
  • 输入流cin
  • 输出流
    • cout:标准输出
    • cerr:错误输出(不缓冲)
    • clog:日志输出(缓冲)

输出流区别

流类型 目标 是否缓冲
cout 标准输出
cerr 标准错误输出
clog 标准日志输出

coutclog不立即刷新缓冲区的目的是使整个程序的运行速度变快。

  • 缓冲刷新方式 【必要时才会使用】:
    • std::flush:手动刷新
    • std::endl:换行并刷新缓冲区

缓冲区与 clog / cerr 区别

在 C++ 中,输出到屏幕或文件的过程相对较慢,因此系统会为输出流建立缓冲区(例如 64KB)。程序会先将数据写入缓冲区,再在缓冲区满时一次性输出,从而提升性能。

clog缓冲输出,主要用于日志记录。它的输出会先进入缓冲区,只有缓冲区满或程序结束时才会刷新到终端或文件,因此速度较快,但在程序异常崩溃时,缓冲区内容可能丢失。

cerr 则是不缓冲输出,通常用于重要错误信息的打印。每次输出都会立即刷新缓冲区,保证用户能第一时间看到错误提示,即使程序随后崩溃也不会丢失信息。


手动刷新缓冲区

在某些情况下,我们希望 coutclog 的内容立即输出,可以使用:

  • std::flush:刷新缓冲区,不换行。
  • std::endl:输出换行符并刷新缓冲区。

过度使用刷新会降低性能,因此应只在必要时使用,比如调试或记录关键步骤。

名字空间(namespace)

  • 用于防止名称冲突
  • 常用空间:std
  • 访问方式:
    1. 域解析符:std::cout
    2. using namespace std;
    3. 名字空间别名:namespace io = std;

名称改编(Name Mangling)

  • 编译器在生成符号名时对函数名等进行“重命名”,以支持函数重载等功能

我们之所以需要引入名称改编(Name Mangling),是因为在 C++ 程序中,一个名字可能会与其他名字发生冲突。比如,同一个函数名可能出现在不同的命名空间中;或者在支持函数重载的情况下,不同的参数列表也可能使用相同的函数名。这些情况都会导致链接器在处理符号时无法区分,于是编译器就需要通过名称改编生成唯一的符号名来避免冲突。

但是,在 C++ 里,main 函数是一个特例。

  • C++ 标准规定:main(小写)在一个程序中必须是唯一的,并且它是程序的入口点。
  • 不允许存在另一个同名的 main 函数与之冲突。
  • 因此,编译器没有必要main 进行名称改编,它在符号表中的名字就是 main,不会被编码成类似 _Z4mainv 这样的形式。

所以,main` 的名字在 mangle 或 demangle 过程中都不会发生变化。


C/C++ 系统IO比较

特性 printf(C) cout(C++) std::format(C++20)
写法 简洁但易错 冗长但安全 简洁 + 类型安全 (新的解决方案)
类型检查
缓冲控制 手动 endl/flush 默认高效

3. 猜数字与控制流

if 条件语句

  • 用于分支选择:

    1
    2
    3
    if (condition) {
    // 执行代码块
    }
  • 组成:

    • 条件部分:判断条件是否为真
    • 语句部分:条件为真时执行的语句

image-20250804150351841

=== 区别

  • =:赋值操作,将值赋给变量

  • ==:判断两个值是否相等

  • 建议防误写:

    1
    if (5 == x) { … } // 防止写成 if (x = 5)

while 循环语句

  • 用于在条件满足时重复执行某段代码

    1
    2
    3
    while (condition) {
    // 循环体
    }
  • 组成:

    • 条件部分:每次循环前判断
    • 循环体:条件为真时执行


4. 结构体与自定义数据类型

struct 结构体

  • 用于将多个相关的数据组合在一起

  • 示例定义:

    1
    2
    3
    4
    struct Person {
    std::string name;
    int age;
    };
  • 特性:

    • 使用点操作符(.)访问内部元素:

      1
      2
      3
      Person p;
      p.name = "Alice";
      p.age = 20;
    • 可作为函数的输入参数或返回类型

    • 可引入成员函数,更好地表示函数与数据的相关性:

      1
      2
      3
      4
      5
      6
      struct Person {
      std::string name;
      void greet() {
      std::cout << "Hello, I’m " << name << std::endl;
      }
      };

第 3 章:对象与基本类型

1. 从初始化 / 赋值语句谈起

  • 初始化 / 赋值语句是程序中最基本的操作,其功能是将某个值与一个对象关联起来
    • 值:字面值、对象(变量或常量)所表示的值…
    • 标识符:变量、常量、引用…
    • 初始化基本操作:
      • 在内存中开辟空间,保存相应的数值
      • 在编译器中构造符号表,将标识符与相关内存空间关联起来
    • 值与对象都有类型
    • 赋值:对象已存在,修改内容
      • a = 20; → 不重新分配内存
    • 初始化与赋值可能触发 类型转换

2. 类型详述

  • 类型是一个编译期概念,可执行文件中不存在类型的概念

  • C++ 是强类型语言

  • 引入类型是为了更好地描述程序,防止误用

    数在内存中只是比特的排列,它的含义取决于解析它的类型。同样的比特,按整型解释和按浮点型解释可能完全不同。C++ 引入类型系统,是为了在编译阶段约束数据的使用方式,避免存储与读取方式不一致导致的错误。

  • 类型描述了:

    • 存储所需要的尺寸 (sizeof,标准并没有严格限制 )
    • 取值空间 (std::numeric_limits ,超过范围可能产生溢出 )
    • 对齐信息( alignof
    • 可以执行的操作

对于我们上面提到的一些基本数据类型,比如 intchar 等,C++ 标准并没有对它们所需的存储尺寸做严格限制。换句话说,如果我在一个程序中声明一个变量 int x;,然后执行 sizeof(x),在某个编译环境中可能得到的结果是 4,在另一个环境中可能是 8,而在第三个环境中可能是 2。具体大小取决于编译器和硬件平台。

也就是说,C++ 并不强制规定这些基本类型的具体字节数,只规定了每种基本数据类型的最小尺寸,具体大小取决于编译器和硬件平台。

为什么不做这种限制呢? 通常来讲,int 是程序中使用非常广泛的数据类型。由于它经常参与加、减、乘、除等运算,这些操作往往可以直接映射到非常简洁的汇编指令。而汇编指令的位宽是与硬件架构相关的:

  • 如果是 64 位机器,汇编指令一次性处理 64 位(8 字节)数据会比较高效;
  • 如果是 32 位机器,则一次性处理 32 位(4 字节)数据最自然;
  • 如果是更老或更小型的 嵌入式系统(8 位机或 16 位机),一次处理 8 位或 16 位数据更方便。

因此,即便我们在源代码中都写了 int,编译器也可能会根据目标硬件的特性生成不同位宽的汇编指令,而 int 的实际字节数也会随之变化。

正是基于这种考虑,C++(以及 C 语言)没有严格规定基本类型的存储尺寸,而是让编译器自由选择最适合目标平台的实现方式,以追求更高的性能。

  • C++ 中的类型可分为两大类:基本类型与复杂类型

    1. 基本(内建)类型: C++ 语言中直接支持的类型

      • 数值类型
        • 字符类型char, wchar_t, char16_t, char32_t
        • 整数类型
          • 带符号整数类型:short, int, long, long long
          • 无符号整数类型:unsigned + 带符号整数类型
        • 浮点类型float, double, long double
      • void 类型:表示无值或无返回类型
    2. 复杂类型: 由基本类型通过组合或变种所产生的类型,可能是:

    • 标准库引入(如 std::stringstd::vector 等)
      • 用户自定义类型(类、结构体、枚举等)
    • 复杂类型一定不是c++语言本身所支持的类型,它一定是通过某种方式引入的
  • 与类型相关的标准未定义部分

    • char是否有符号 (char 在 C++ 里是否有符号,其实不是固定的,要看 编译器和平台实现。)

    • 整数在内存中的保存方式:大端 小端

    • 每种类型的大小(间接影响取值范围)

      • C++11 中引入了固定尺寸的整数类型,如int32_t

字面值及其类型

  • 字面值(Literal):在程序中直接表示为一个具体数值或字符串的值。

  • 每个字面值都有其明确的 类型

    1. 整数字面值

      • 十进制:20

      • 八进制:024

      • 十六进制:0x14

      • 类型int

    2. 浮点数字面值

      • 例子:1.3, 1e8

      • 类型double

    3. 字符字面值

      • 普通字符:'c'

      • 转义字符:'\n'

      • 十六进制表示:'\x4d'

      • 类型char

    4. 字符串字面值

      • 例子:"Hello"

      • 类型char[6](包含结尾的 \0

    5. 布尔字面值

      • true, false

      • 类型bool

    6. 指针字面值

      • nullptr

      • 类型nullptr_t

  • 可以为字面值引入前缀或后缀以改变其类型

    • 例如:
      • 1.3(类型为 double
      • 1.3f(类型为 float
    • 例如:
      • 2(类型为 int
      • 2ULL(类型为 unsigned long long
  • 还可以引入自定义后缀来修改字面值类型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    #include <iostream>

    constexpr unsigned long long operator "" _km(unsigned long long v) {
    return v * 1000;
    }

    constexpr unsigned long long operator "" _m(unsigned long long v) {
    return v;
    }

    int main() {
    auto distance = 3_km + 50_m; // 3050
    std::cout << distance << " meters\n";
    }

变量及其类型

  • 变量:对应了一段内存中的存储空间,可以改变其中内容

  • 变量的类型:在首次声明(定义)时指定

    • 例:int x; 定义一个类型为 int 的变量 x

    • 变量声明与定义的区别:使用 extern 关键字为声明

      1
      2
      extern int g_x = 100; // 这其实是定义,而不是单纯的声明。因为一旦有 (= 初始值),编译器必须为它分配存储空间并初始化。
      extern int g_x; // 这是一个声明,告诉编译器 g_x 是一个在其他地方定义的变量。
  • 变量的初始化与赋值

    • 初始化:变量创建时为其赋予初始值
      • 缺省初始化

        存储期 / 作用域 缺省初始化行为
        全局变量 值为 0
        static 局部变量 值为 0
        thread_local 变量 值为 0
        局部变量(非 static) 值未定义
        类对象(有默认构造) 默认构造初始化
      • 直接初始化 / 拷贝初始化

      • 其它初始化

    • 赋值:创建后修改变量所保存的数值

(隐式) 类型转换

ChatGPT Image 2025年8月18日 17_13_07

  • 为变量赋值时可能发生的类型转换

    • bool 与整数之间转换
    • 浮点数与整数之间的类型转换
  • 隐式类型转换不只发生在赋值时

    • if 条件判断
    • 数值比较
      • 注意无符号数据与带符号数据比较的特殊情况

        这是一个经典的 有符号数和无符号数比较 的例子。我们来逐步分析:

        1
        2
        3
        4
        5
        6
        7
        8
        #include <iostream>

        int main(int argc, char* argv[]) {
        int x = -1; // 有符号,值是 -1
        unsigned int y = 3; // 无符号,值是 3

        std::cout << (x < y) << std::endl; // 0
        }

        🔎 规则

        • 在 C++ 中,当一个 有符号整数无符号整数 比较时,编译器会将 有符号数提升为无符号数,然后再进行比较。
        • int x = -1; 在转换为 unsigned int 时,会执行模运算,变成该类型的最大值。

        如果是 32 位 unsigned int,则: \[ −1≡232−1=4294967295-1 \equiv 2^{32} - 1 = 4294967295 \] ⚖️ 实际比较

        1
        2
        x = -1  → 转换为 unsigned int4294967295
        y = 3

        要避免这种 陷阱,可以:

        1. 显式转换:

          1
          std::cout << ( (long long)x < (long long)y ) << std::endl;
        2. 或者保持同类型比较(都用 int 或都用 unsigned int)。

      • C++20 引入了安全比较函数 std::cmp_XXX


3. 复合类型:从指针到引用

🎯指针

指针:一种间接类型

image

特点

  • 可 “指向” 不同的对象或 nullptr
  • 具有相同的尺寸

相关操作

  • 取地址操作符:&;解引用操作符:*
  • 可修改指向;void* 可存任意类型地址

指针的定义

  • int* p = &val; // 指向变量 val 的地址
  • int* p = nullptr; // 空指针

如果你只是写:

1
int* p;
  • 这是一个 局部变量,不会自动清零,内容是 未定义值(野指针)(随机初始化)。

    • 指针里面所存储的内容被强行的解析为一个地址的值,并且我们不确定这个地址的值是否真正指向了一个有效的内存地址。
  • 如果是全局变量或 static 指针:

    1
    static int* q;

    那么会被 自动初始化为 nullptr(即 0)。零地址是一个特殊的地址,我们不能对它进行解引用。

关于nullptr

很多情况下我们是没有办法在初始化一个指针的时候为它赋予一个对象的地址的,那么这个时候我们就应该将这个指针初始化为一个空指针,int *p = nullptr,这样它不会出现指向不可控的随机内存的情况。

  • 一个特殊的对象,类型为 nullptr_t,表示空指针
  • 类似 C 中的 NULL,但更安全

🧩拓展:

1
int *p = 0;

实际上这里涉及到了类型转换,这里的 0 是一个字面值,这个字面值的类型是 int ,而变量 p 的类型是 int * , 我们这里是把一个 int 型的字面值赋予一个 int *型的对象,相应的就会涉及到类型转换,但这种类型转换是比较危险的

代码梳理

1
2
3
4
5
6
7
8
9
10
11
12
13
void fun(int) {
std::cout << "1\n";
}

void fun(int*) {
std::cout <<"2\n";
}

int main() {
fun(0); // 调用 fun(int) —— 输出 1
int *p = 0; // 初始化指针
fun(p); // 调用 fun(int*) —— 输出 2
}

关键点int *p = 0;

  • 0 在 C++ 中是一个 整型字面量int 类型)。
  • 由于历史原因(C 语言兼容性),0 可以 隐式转换nullptr(空指针常量)。
  • 因此 int *p = 0; 实际上就是在用 0 来初始化空指针。

隐式转换的危害

  1. 二义性风险
    • 如果重载函数同时有 fun(int)fun(int*),写 fun(0) 时,编译器需要判断 0 到底是 int 还是“空指针”。
    • 在老的 C++ 中可能会引发二义性错误(因为 0 既能匹配 int,也能匹配 int*)。
  2. 可读性差
    • int *p = 0; 语义模糊,不熟悉规则的人容易误解 0 是整数,而不是空指针。
  3. 潜在 bug
    • 如果以后把 fun(int*) 改成 fun(long*) 或者更复杂的指针类型,0 依然能被错误地隐式匹配过去,导致调用结果和预期不符。

现代 C++ 的改进

C++11 引入了 nullptr

1
2
int *p = nullptr;  // 明确是空指针
fun(nullptr); // 只会匹配 fun(int*), 不会匹配 fun(int)

nullptr 是一个指针,它可以转换为任意类型的指针,但不能隐式转换成一个整数。这样就消除了 0 的二义性,代码更清晰、安全。


总结一句话int *p = 0; 利用了 0 向空指针的隐式转换,但这种写法存在二义性和可读性问题,容易在函数重载或类型变化时引发 bug,应优先使用 nullptr 来初始化指针。

指针与 bool 的隐式转换

  • 非空指针 → true
  • 空指针 → false

指针的主要操作

  • 解引用(*p
  • 增加、减少(指针运算)
  • 判等(比较两个指针是否相等)

void* 指针

在 C/C++ 里,如果函数的逻辑 不依赖具体的指针类型(例如只是保存、传递、比较指针,而不解引用指向的对象),就可以使用 void* 来表示“通用指针”,它可以转换为任意类型的指针,能够做到这一点的原因是因为指针类型的尺寸是一样的。但 void* 同时也丢掉了一些重要的信息,就是它所指向对象的尺寸信息。

  • 没有记录对象的尺寸信息,可以保存任意地址
  • 支持判等操作

指针的指针

  • 形如 int** pp;,表示指向“指针变量”的指针

    2025-08-13 15 43 02

指针 V.S. 对象

  • 指针复制成本低,但读写成本高(需间接访问)

指针的问题

  • 可能为空
  • 地址信息可能非法(悬空指针)
  • 解决方案:使用引用

🎯引用

  • 定义方式

    • int& ref = val; // refval 的别名
  • 基本特性

    • 是对象的别名,不能绑定字面值(需绑定对象)
    • 构造时绑定对象,在其生命周期内不能绑定其它对象(赋值操作会改变绑定对象的内容)
    • 不存在空引用,但可能存在非法引用——总的来说比指针安全
    • 属于编译期概念,在底层还是通过指针实现
  • 指针的引用

    • 指针是对象,因此可以定义引用

    • 例:

      1
      2
      int* p = &val;
      int*& ref = p; // 引用一个指针
    • 类型信息从右向左解析


4. 常量类型与常量表达式

1. 常量的概念

  • 常量与变量相对,表示不可修改的对象
    • 使用 const 声明常量对象
    • 编译期概念,编译器利用常量来:
      • 防止非法修改
      • 优化程序逻辑

2. 常量指针与顶层常量(Top-level const)

  • 形式与含义
    • const int* p; // 指向常量的指针(不能通过 p 修改值,但 p 可改指向)
    • int* const p; // 常量指针(指向固定,但可修改值)
    • const int* const p; // 指向常量的常量指针(值和指向都不可变)
  • 注意:常量指针可以指向变量

3. 常量引用(也可绑定变量)

  • 定义const int& ref = x;
  • 特性:
    • 可读不可写
    • 主要用于函数形参(避免拷贝,保护数据)
    • 可以绑定字面值和临时对象

4. 常量表达式(C++11 起)

  • 定义:使用 constexpr 声明编译期常量

  • 优点:

    • 编译器可以利用其进行优化
    • 保证值在编译期确定
  • 常量表达式指针

    • constexpr 位于 * 左侧 → 表示该指针本身是编译期常量(常量表达式)

      1
      constexpr int* p = nullptr;  // p 是编译期常量指针

5. 类型别名与类型的自动推导

  • 可以为类型引入别名,从而引入特殊的含义或便于使用(如:size_t)。

  • 两种引入类型别名的方式:

    • typedef int MyInt;

    • using MyInt = int;(C++11 起,推荐)

  • 使用 using 引入类型别名更好

    • typedef char MyCharArr[4];

    • using MyCharArr = char[4];

  • 类型别名与指针、引用的关系:

    • 应将指针类型别名视为一个整体,再在其基础上引入常量表示“指针为常量”的类型。

    • 无法通过类型别名构造“引用的引用”。

  • 类型的自动推导

    • 从 C++11 开始,可以通过初始化表达式自动推导对象类型。

    • 自动推导类型并不意味着弱化类型,对象仍是强类型。

    • 自动推导的几种常见形式:

      • auto:最常用,但会发生类型退化。

      • const auto / constexpr auto:推导出的是常量 / 常量表达式类型。

      • auto&:推导为引用类型,避免类型退化。

      • decltype(exp):返回表达式 exp 的类型(左值会加引用)。

      • decltype(val):返回变量 val 的类型。

      • decltype(auto):C++14 起,简化 decltype 使用。

      • concept auto:C++20 起,表示一系列类型(如:std::integral auto x = 3;)。


6. 域与对象的生命周期

  • 域(scope):表示了程序中的一部分,其中的名称在该范围内具有唯一含义。

  • 全局域(global scope):程序最外围的域,其中定义的是全局对象。

  • 块域(block scope):使用大括号 {} 所限定的域,其中定义的是局部对象。

  • 还存在其它的域:类域(class scope)、名字空间域(namespace scope)等。

  • 嵌套与隐藏:域可以嵌套,嵌套域中定义的名称可以隐藏外部域中定义的名称。

  • 对象的生命周期起始于被初始化的时刻,终止于被销毁的时刻

  • 通常来说

    • 全局对象:全局对象的生命周期是整个程序的运行期间。

    • 局部对象:局部对象生命周期起源于对象的初始化位置,终止于所在域被执行完成。

1
2
3
{
int x = 5; // 进入作用域时创建
} // 离开作用域销毁

C++基础 (Updating)
https://devgek.cn/2025/08/04/Fundamentals of cpp/
Author
DXGEK
Posted on
August 4, 2025
Licensed under