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

如何定义命名空间?

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

注意:

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

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

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

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

如何访问变量?

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

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

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

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

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

  1. 局部作用域
  2. 全局作用域
  3. 未找到则报错"未声明的标识符"

若指定了作用域,则直接在该作用域中查找,未找到同样报错。

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

如何使用命名空间?

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

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

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

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

引用的特性:

  1. 引用在定义时必须初始化;
  2. 一个变量可以有多个引用;
  3. 引用一旦引用一个实体,再不能引用其他实体。

引用可以解决大部分指针问题,但在特定场景仍需使用指针(如链表、树等数据结构中的节点定义)。这是因为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;      // 错误:类型转换产生临时对象,引用权限放大

临时对象的产生原因

  1. a*3是表达式求值,其计算结果会被编译器存储在一个临时对象中(临时对象无名称,仅暂存结果);
  2. int& rd = d中,double类型的d转换为int类型时,会生成临时对象存储转换后的中间值

C++ 标准规定:所有临时对象都具有 "常性"(const 属性) 。上述代码中,rbrd是普通非 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引用绑定转换产生的临时对象
  • intdouble 隐式转换生成临时对象,具有常性。
  • 普通引用绑定临时对象属于权限放大,编译报错;const 引用则合法。

2. 强制类型转换

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

复制代码
int i = 1;
// int& rp = (int)&i;  // ❌ 错误:普通引用不能绑定临时对象
const int& rp = (int)&i;  // ✅ 正确:const引用绑定转换产生的临时对象
  • (int)&iint* 强制转为 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 表示空指针。


下一篇再见🫡

相关推荐
Scott9999HH1 小时前
【IIoT流量实战】蒸汽管道阀门全关却仍有流量?用 Python 实现涡街信号 FFT 频谱分析与温压全补偿积算网关,深度拆解靠谱的涡街流量计厂家硬核技术标准
开发语言·python
2401_841495642 小时前
【操作系统】进程同步与互斥实验报告
c++·算法·操作系统·进程·并发·同步·互斥
fqbqrr2 小时前
2607C++,soui与安卓
c++·soui
码智社2 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
AI云海2 小时前
python 列表、元组、集合和字典
开发语言·python
克里斯蒂亚诺更新2 小时前
ruoyi角色权限的对应关系
笔记
萧瑟余晖3 小时前
JDK 26 新特性详解
java·开发语言
马优晨4 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
a1117765 小时前
2FA 验证码生成器(github登录验证 app)
笔记·学习
fqbqrr6 小时前
2607C++,使用微软detours勾挂工具
c++