欢迎光临
我们一直在努力

【C++学习笔记】【基础】0.C与C++

🍕 阿i索个人主页
《C语言专栏》 《C++专栏》
《数据结构专栏》 待更新…

前言.

        在熟悉了 C 语言的底层逻辑与高效特性后,我们不妨迈出下一步 —— 走进 C++ 的世界。

从最经典的 hello world 就能直观感受到两者的差异:

                                                    了解C++的产生与发展

C++ 诞生于 1983 年,由 Bjarne Stroustrup 在 C 语言基础上扩展而来,核心是引入面向对象编程,解决了 C 在大型项目中的可维护性短板。1998 年 C++98 标准发布标志着语言成熟,而 C++11 则是现代 C++ 的分水岭 —— 智能指针、Lambda 表达式等特性让开发更简洁安全;后续 C++14/17/20 的迭代,进一步强化了模块化、协程等能力,让它在系统开发、游戏引擎、高性能计算等领域始终占据核心地位。

C++ 在保留 C 语言大部分语法的基础上,针对 C 语言存在的缺陷提供了改进方案,是学习高性能编程的必经之路


 📚 学习资源推荐

1.最新更新至 C++11 的头文件形式参考文档(结构清晰,适合入门查阅)

2.C++ 官方英文文档(权威标准,适合深入研究)

接下来,我们就从基础语法开始,一步步探索 C++ 的强大之处。


一. 命名冲突——命名空间

C++通过引入命名空间机制有效解决了全局作用域中的命名冲突问题。该特性允许将变量、函数和类等标识符限定在特定的命名空间内,从而实现名称的本地化管理。

如何定义命名空间?

  • 使用namespace关键字声明,后跟命名空间名称
  • 在大括号{}内定义命名空间的成员,可以包含变量、函数、类型等
  • 命名空间必须定义在全局作用域中,支持嵌套定义
  • 注意:

    • 同一命名空间可以在多个文件中分别定义,系统会自动合并
    • 不同文件中定义的同名命名空间不会产生冲突

    命名空间的本质是创建一个独立的作用域,该作用域与全局作用域相互隔离。通过这种方式,不同作用域中可以定义同名的变量,从而有效避免了命名冲突问题。需要注意的是,命名空间内定义的变量仍属于全局变量范畴,只有函数内部或代码块(例如for循环)中定义的变量才是局部变量。

    为什么不同的域可以定义同名变量?

    C++中域有函数局部域,全局域,命名空间域,类域。域影响的是编译时语法查找一个变量/函数/类型出处(声明或定义)的逻辑,所有有了域隔离,名字冲突就解决了。局部域和全局域除了会影响编译查找逻辑,还会影响变量的生命周期,命名空间域和类域不影响变量生命周期。

    如何访问变量?

    变量访问遵循"局部优先"原则,即优先查找局部变量。要访问其他作用域的变量,需使用域作用限定符 " :: " 

    • 单独使用 " :: " 表示访问全局变量
    • 前加命名空间名称可访问指定命名空间的变量

    调用其他作用域的函数时,需在函数名前添加命名空间名称。例如调用C++标准库函数(位于std命名空间)的格式为:"std::函数名"。

    访问嵌套命名空间时,需多次使用域作用限定符。

    编译器查找变量的默认顺序:

  • 局部作用域
  • 全局作用域
  • 未找到则报错"未声明的标识符"
  • 若指定了作用域,则直接在该作用域中查找,未找到同样报错。

    命名空间的使用场景:当一个项目包含大量文件时,函数命名容易产生冲突。通过在头文件中定义命名空间,可以为函数提供区分标识。值得注意的是,不同文件中可以定义同名命名空间,它们会被视为同一个命名空间而不会产生冲突。

    如何使用命名空间?

  • 使用域作用限定符"::":通过指定命名空间来访问成员,这是项目开发中的推荐方式。

  • 使用using展开整个命名空间:将命名空间中的所有成员都引入当前作用域,适合练习和刷题时使用。

  • 使用using引入特定成员:只展开命名空间中的某个成员,适合项目中需要频繁访问且不会造成命名冲突的成员。

  • 二. C++的输入输出

    C++的输入输出流库是<iostream>(Input Output Stream的缩写)。

    在标准库中:

    • std::cin是istream类的对象,负责处理窄字符的标准输入
    • std::cout是ostream类的对象,负责处理窄字符的标准输出
    • std::endl是一个函数,在流操作中相当于插入换行符并刷新缓冲区

    主要运算符:

    • << 是流插入运算符

    • >> 是流插入运算符

    这些组件都定义在std命名空间中,使用时需要通过命名空间访问。

      相比C语言,C++的输入输出更加便捷,能够自动识别变量类型而无需手动指定格式。

      一些编译器想要使用printf、scanf需要包含头文件<cstdio>。

      三. 缺省参数

      缺省参数是指在函数声明或定义时为参数预设默认值。当调用函数时,若未传入对应实参,则自动使用该默认值;若传入实参,则使用传入值。缺省参数分为两种类型:全缺省(所有参数都设默认值)和半缺省(部分参数设默认值)。需要注意的是,C语言不支持缺省参数功能,这是C++特有的特性。

      如何使用缺省参数?

      • 声明与定义分离规则:函数声明和定义分离时,缺省参数不可重复指定,需在声明中指定缺省值(无声明时可在定义中指定)。
      • 半缺省参数规则:半缺省参数必须从右往左依次连续指定,禁止间隔跳跃给缺省值。
      • 函数调用规则:调用带缺省参数的函数时,实参需从左到右依次传递,不能跳过前面的参数给后面传值。

      使用缺省参数可以去解决的场景:

      当Stack需要存储n个元素时,如果预先分配足够的空间,就能避免频繁扩容操作。这种情况下,可以在初始化函数的声明或定义中添加缺省参数来实现。

      四. 函数重载

      C++允许在同一作用域内定义多个同名函数,但要求它们的形参列表必须有所区别(参数数量或类型不同)。这种函数重载机制使得C++的函数调用展现出多态特性,使用更加灵活。相比之下,C语言严格禁止同一作用域内出现同名函数,因此不支持函数重载功能。

      五. 引用

      C++引入引用(reference)的概念,本质上是为变量创建别名。例如:"及时雨"和"宋江"都是指同一个人。引用并非定义新变量,而是为现有变量赋予另一个名称。编译器不会为引用分配独立的内存空间,引用与其指向的变量共享相同的内存地址。

      引用格式:类型& 引用别名=引用对象

      引用的特性:

    • 引用在定义时必须初始化;
    • 一个变量可以有多个引用;
    • 引用一旦引用一个实体,再不能引用其他实体。
    • 引用可以解决大部分指针问题,但在特定场景仍需使用指针(如链表、树等数据结构中的节点定义)。这是因为C++引用一旦初始化就无法改变其指向对象,而某些场景需要动态调整节点指向关系,此时只能使用指针来实现。

      引用主要用于传参和返回值场景,能有效减少数据拷贝提升效率。修改引用对象时,原对象也会同步更新。

      1. 引用传参

      引用传参和指针传参在功能上相似,但引用传参使用起来更为便捷。下面以SeqList为例,分别展示指针传参和引用传参的实现方式:

      也有的地方先对结构体地址进行引用:

      2. 传引用返回

      六. const引用

      在 C++ 中,若要引用const对象,必须使用const引用;而const引用也可引用普通(非 const)对象 。这是因为引用在绑定对象时,遵循 权限只能缩小、不能放大 的规则:

      • 权限缩小(允许):普通对象 → const 引用(限制修改权限)
      • 权限放大(禁止):const 对象 / 临时对象 → 普通引用(试图突破只读限制)

      需要特别注意以下两种非法场景,其核心问题均为引用了临时对象:

      int a = 10;
      int& rb = a*3; // 错误:引用临时对象,且未使用const引用
      double d = 12.34;
      int& rd = d; // 错误:类型转换产生临时对象,引用权限放大

      临时对象的产生原因

    • a*3是表达式求值,其计算结果会被编译器存储在一个临时对象中(临时对象无名称,仅暂存结果);
    • int& rd = d中,double类型的d转换为int类型时,会生成临时对象存储转换后的中间值
    • C++ 标准规定:所有临时对象都具有 “常性”(const 属性)。上述代码中,rb和rd是普通非 const 引用,试图引用具有常性的临时对象,本质是 “权限放大”(普通引用试图修改只读的临时对象),因此编译报错。

      正确的写法是使用const引用绑定临时对象:

      int a = 10;
      const int& rb = a*3; // 正确:const引用可绑定临时对象
      double d = 12.34;
      const int& rd = d; // 正确:const引用接受类型转换产生的临时对象

      补充定义:临时对象(Temporary Object)是编译器在求值表达式(如算术运算、类型转换)时,为暂存中间结果临时创建的未命名对象,其生命周期通常仅限于当前表达式,且默认具有 const 属性。

      const 引用的三大用法

      1. 绑定 const 对象

      必须使用 const 引用,否则会因权限放大报错:

      const int x = 10;
      const int& rx = x; // ✅ 正确
      // int& rx = x; // ❌ 错误:const对象 → 普通引用(权限放大)

      2. 绑定普通对象

      const 引用可以安全绑定普通对象,只是限制了修改权限:

      int y = 0;
      const int& ry = y; // ✅ 正确

      3. 绑定临时对象(唯一合法方式)

      临时对象(字面量、表达式、类型转换产生)默认具有常性,只能被 const 引用绑定:

      const int& r1 = 10; // ✅ 绑定字面量临时对象
      const int& r2 = y * 2; // ✅ 绑定表达式临时对象
      double d = 12.34;
      const int& r3 = d; // ✅ 绑定类型转换临时对象

      普通引用的限制

      普通(非const)引用 只能绑定可修改的左值(普通变量),不能绑定:

      • const 对象(权限放大)
      • 临时对象(字面量、表达式、类型转换产生)

      int y = 0;
      int& ry = y; // ✅ 正确

      const int x = 10;
      // int& rx = x; // ❌ 错误:const对象 → 普通引用(权限放大)

      // int& r = 10; // ❌ 错误:绑定临时对象(权限放大)
      // int& r2 = y * 2; // ❌ 错误:绑定临时对象(权限放大)

      类型转换与临时对象

      1. 隐式类型转换

      当不同类型绑定引用时,会生成临时对象:

      int i = 1;
      // double& rd = i; // ❌ 错误:普通引用不能绑定临时对象
      const double& rd = i; // ✅ 正确:const引用绑定转换产生的临时对象

      • int → double 隐式转换生成临时对象,具有常性。
      • 普通引用绑定临时对象属于权限放大,编译报错;const 引用则合法。

      2. 强制类型转换

      强制类型转换同样会生成临时对象:

      int i = 1;
      // int& rp = (int)&i; // ❌ 错误:普通引用不能绑定临时对象
      const int& rp = (int)&i; // ✅ 正确:const引用绑定转换产生的临时对象

      • (int)&i 将 int* 强制转为 int,生成临时对象,具有常性。
      • 只有 const 引用能安全绑定这个临时对象。

      函数传参中的引用

      1. 不修改实参的情况:推荐用 const 引用

      • 兼容普通对象、const 对象、临时对象,保证安全。

      void func(const int& x) { /* 只读访问 */ }

      int main() {
      const int a = 10;
      int y = 0;
      func(a); // ✅ const对象 → const引用
      func(y); // ✅ 普通对象 → const引用
      func(2); // ✅ 临时对象 → const引用
      return 0;
      }

      2. 修改实参的情况:用普通引用

      • 只能接收普通对象,不能接收 const 对象或临时对象。

      void func2(int& x) { x = 100; /* 修改实参 */ }

      int main() {
      int y = 0;
      func2(y); // ✅ 普通对象 → 普通引用

      const int a = 10;
      // func2(a); // ❌ 错误:const对象 → 普通引用(权限放大)
      // func2(2); // ❌ 错误:临时对象 → 普通引用(权限放大)
      return 0;
      }

      指针与引用

      特性引用(Reference)指针(Pointer)
      内存空间

      是变量的 “别名”,不开辟空间

      存储变量地址,需要开辟空间

      初始化要求 定义时必须初始化,不能空引用 建议初始化,允许空指针
      指向性修改 初始化绑定对象后,不能更换绑定目标 可随时修改指向,指向不同对象 / 地址
      访问目标方式 直接访问( 等价于原变量) 需解引用才能访问指向的对象
      sizeof 结果 等于引用类型的大小(如 int& 是 4 字节) 固定为地址空间大小
      安全性 几乎无空 / 野引用问题,使用更安全 易出现空指针、野指针,风险更高

      1. 内存与初始化

      #include <iostream>
      using namespace std;

      int main() {
      int a = 10;

      // 引用:必须初始化,绑定后不能改
      int& ref = a; // ✅ 正确:引用a
      // int& ref2; // ❌ 错误:引用未初始化

      // 指针:可先定义后初始化,可改指向
      int* ptr; // ✅ 语法允许:未初始化(不推荐)
      ptr = &a; // ✅ 指向a
      int b = 20;
      ptr = &b; // ✅ 改为指向b

      return 0;
      }

      2. 访问方式与 sizeof

      #include <iostream>
      using namespace std;

      int main() {
      int a = 10;
      int& ref = a;
      int* ptr = &a;

      // 访问目标
      ref = 20; // ✅ 直接修改a的值
      *ptr = 30; // ✅ 解引用修改a的值

      // sizeof 结果
      cout << "sizeof(ref) = " << sizeof(ref) << endl; // 4(int类型大小)
      cout << "sizeof(ptr) = " << sizeof(ptr) << endl; // 8(64位平台地址大小)

      return 0;
      }

      3. 安全性对比

      #include <iostream>
      using namespace std;

      int main() {
      // 指针风险:空指针/野指针
      int* null_ptr = nullptr;
      // *null_ptr = 10; // ❌ 空指针解引用,程序崩溃

      int* wild_ptr; // 野指针(未初始化)
      // *wild_ptr = 20; // ❌ 野指针访问,内存越界

      // 引用安全:无空/野引用
      int a = 10;
      int& ref = a;
      ref = 30; // ✅ 安全访问

      return 0;
      }

      使用场景建议:

      1. 优先用引用的场景

      • 函数参数传递(避免拷贝,且无需检查空值):void func(int& x)
      • 函数返回值(返回类成员 / 全局变量,避免拷贝):int& getVal()
      • 追求代码简洁、安全的场景

      2. 必须用指针的场景

      • 需要动态分配内存(new/delete):int* arr = new int[10]
      • 需要修改指向目标的场景:ptr = &b
      • 实现多态(基类指针指向派生类对象):Base* p = new Derived()

      七、宏函数与内联函数

      回顾:C 语言宏函数

      1. 宏函数的本质

      宏是预处理阶段的纯文本替换机制,不是真正的函数调用,无栈帧创建、无类型检查。

      2. 宏函数的特点

      优点缺点

      1.高效:高频调用小函数时,宏替换避免了函数调用开销,执行效率更高。

      2.无栈帧:直接替换代码,不占用栈空间。

      1. 极易出错(运算符优先级、重复求值)

      2. 无法调试(预处理后代码已展开)

      3. 类型不安全(不检查参数类型)

      3. 宏函数的安全写法(以 ADD 为例)

      // 错误版本:多分号+无括号
      #define ADD1(a, b) a+b; // 展开后多余分号,语法错误
      // 基础版本:无整体括号,优先级错误
      #define ADD2(a, b) a+b // ADD2(1,2)*3 → 1+2*3=7(错误)
      // 安全版本:参数+整体都加括号(推荐)
      #define ADD4(a, b) ((a)+(b)) // ADD4(1,2)*3 → ((1)+(2))*3=9(正确)

      4. 宏函数建议

      • 每个参数加括号 + 整个表达式加括号,避免优先级问题;
      • 不加分号结尾,由调用者自行添加;
      • 避免带副作用的参数(如ADD(i++, j++)会导致多次自增)。

      C++ 内联函数(inline):替代宏函数的更优方案

      1. 内联函数的本质

      用inline修饰的函数,编译阶段编译器会在调用处直接展开函数体,无需建立栈帧,既保留函数特性,又兼具宏的高效。

      2. 内联函数的特点

      内联函数宏函数
      编译阶段展开,保留函数特性 预处理阶段文本替换,无函数特性
      类型安全(编译检查参数类型) 类型不安全(任意文本均可替换)
      可调试(debug 模式默认不展开) 无法调试(预处理后无宏痕迹)
      语法简单(无需手动加括号) 语法复杂(易因括号 / 分号出错)
      支持函数重载、作用域限制 无重载,全局文本替换

      注意

      • inline是编译器建议:编译器可拒绝展开(如递归函数、代码量过大的函数);
      • 适用场景:频繁调用的短小函数(如几行代码的加减、判断);
      • 不适用场景:递归函数、代码量大的函数(加inline也会被编译器忽略);
      • 声明与定义不可分离:分离到.h 和.cpp 会导致链接错误(展开后无函数地址);
      • VS 调试说明:debug 版本默认不展开(方便调试),release 版本默认展开(追求效率)。

      3. 内联函数的代码示例

      #include <iostream>
      using namespace std;

      // 内联函数:替代ADD宏,简洁且安全
      inline int Add(int a, int b) {
      return a + b;
      }

      int main() {
      // 编译时展开为:int ret = 1 + 2; 无栈帧开销
      int ret = Add(1, 2);
      // 无需担心优先级:Add(1,2)*3 → (1+2)*3=9
      int ret2 = Add(1, 2) * 3;
      cout << ret << " " << ret2 << endl; // 输出:3 9
      return 0;
      }

      4. 内联函数的使用注意

      // ❌ 错误:声明与定义分离(链接错误)
      // test.h 中声明:inline int Add(int a, int b);
      // test.cpp 中定义:int Add(int a, int b) { return a+b; }

      // ✅ 正确:声明+定义写在.h中(或直接写在调用处)
      inline int Add(int a, int b) {
      return a + b;
      }

      5. 宏函数 vs 内联函数

      特性宏函数(C)内联函数(C++)
      处理阶段 预处理阶段(文本替换) 编译阶段(函数体展开)
      类型检查 无(类型不安全) 有(编译检查,类型安全)
      调试性 无法调试 可调试(debug 默认不展开)
      语法复杂度 高(需手动加括号) 低(普通函数写法)
      适用场景 C 语言短小高频函数 C++ 短小高频函数(替代宏)
      重载 / 作用域 不支持 支持
      递归 / 大函数 可替换(但易出错) 编译器自动忽略 inline

      八、NULL 与 nullptr

      回顾NULL 

      NULL 是一个宏,在标准头文件 stddef.h 中定义:

      #ifndef NULL
      #ifdef __cplusplus
      #define NULL 0 // C++ 中通常定义为字面量 0
      #else
      #define NULL ((void *)0) // C 中定义为无类型指针 (void*)
      #endif
      #endif

      NULL容易遇见的情况:

      (1)重载匹配错误

      当函数存在 int 和指针版本的重载时,NULL 会被错误地匹配到 int 版本:

      void f(int x) { cout << "f(int x)" << endl; }
      void f(int* ptr) { cout << "f(int* ptr)" << endl; }

      int main() {
      f(0); // ✅ 匹配 f(int x)
      f(NULL); // ❌ 仍匹配 f(int x),因为 NULL 被定义为 0
      return 0;
      }

      (2)类型转换歧义

      • f((void*)NULL):在 C++ 中会直接编译报错,因为 void* 无法隐式转换为 int*。
      • f((int*)NULL):虽然可以强制转换调用指针版本,但写法繁琐。

      C++11 新关键字:nullptr

      1. nullptr 的本质

      nullptr 是 C++11 引入的特殊关键字,也是一种特殊类型的字面量。它的类型是 std::nullptr_t ,可以隐式转换为任意指针类型,但不能转换为整数类型。

      2. nullptr 解决的问题

      (1)正确匹配指针重载

      void f(int x) { cout << "f(int x)" << endl; }
      void f(int* ptr) { cout << "f(int* ptr)" << endl; }

      int main() {
      f(nullptr); // ✅ 正确匹配 f(int* ptr)
      return 0;
      }

      • nullptr 只能被当作指针,不会被误判为整数,彻底解决了重载匹配错误。
      (2)统一空指针表示

      int* p1 = nullptr; // ✅ 任意指针类型均可接收
      char* p2 = nullptr;
      double* p3 = nullptr;

      • 无需强制类型转换,代码更简洁、安全。

      NULL对比nullptr

      特性NULL (C++)nullptr (C++11+)
      本质 宏,通常被定义为 0 关键字,类型为 std::nullptr_t
      类型安全 可隐式转换为整数,易引发重载匹配错误 只能转换为指针类型,类型安全
      重载匹配 易匹配到 int 版本,而非指针版本 精确匹配指针版本
      代码可读性 语义模糊(是 0 还是空指针?) 语义明确:代表空指针
      兼容性 兼容 C/C++ 旧代码 C++11 及以后支持

      代码示例:

      #include <iostream>
      using namespace std;

      void f(int x) { cout << "f(int x)" << endl; }
      void f(int* ptr) { cout << "f(int* ptr)" << endl; }

      int main() {
      // 1. 字面量 0
      f(0); // 输出: f(int x)

      // 2. NULL 的问题
      f(NULL); // 输出: f(int x) ❌ 错误匹配
      // f((void*)NULL); // 编译错误: 无法从 void* 转换为 int*
      f((int*)NULL); // 输出: f(int* ptr) ✅ 强制转换可行,但繁琐

      // 3. nullptr 的正确用法
      f(nullptr); // 输出: f(int* ptr) ✅ 正确匹配

      // 4. 定义空指针
      int* p1 = NULL; // 旧写法
      int* p2 = nullptr; // 新写法(推荐)

      return 0;
      }

      建议在C++11 及以后的代码中,一律使用 nullptr 表示空指针。


      下一篇再见🫡

      赞(0)
      未经允许不得转载:171主机测评 » 【C++学习笔记】【基础】0.C与C++
      分享到: 更多 (0)

      评论 抢沙发

      • 昵称 (必填)
      • 邮箱 (必填)
      • 网址